printers.rs 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  1. use crate::api::ApiClient;
  2. use crate::core::components::form_builder::FormBuilder;
  3. use crate::core::components::interactions::ConfirmDialog;
  4. use crate::core::{ColumnConfig, TableEventHandler, TableRenderer};
  5. use crate::core::{EditorField, FieldType};
  6. use crate::ui::ribbon::RibbonUI;
  7. use eframe::egui;
  8. use serde_json::Value;
  9. const SYSTEM_PRINTER_SETTINGS_TEMPLATE: &str = r#"{
  10. "paper_size": "Custom",
  11. "orientation": "landscape",
  12. "margins": {
  13. "top": 0.0,
  14. "right": 0.0,
  15. "bottom": 0.0,
  16. "left": 0.0
  17. },
  18. "color": false,
  19. "quality": "high",
  20. "copies": 1,
  21. "duplex": false,
  22. "center": "both",
  23. "center_disabled": false,
  24. "scale_mode": "fit",
  25. "scale_factor": 1.0,
  26. "custom_width_mm": 50.8,
  27. "custom_height_mm": 76.2,
  28. "printer_name": null,
  29. "show_dialog_if_unfound": true
  30. }"#;
  31. const PDF_PRINTER_SETTINGS_TEMPLATE: &str = SYSTEM_PRINTER_SETTINGS_TEMPLATE;
  32. const SYSTEM_PRINTER_JSON_HELP: &str = r#"# System printer JSON
  33. Use this payload when registering the `System` printer plugin. Leave fields out to fall back to BeepZone's legacy sizing.
  34. ## Core fields
  35. - `paper_size` *(string)* — Named stock such as `A4`, `Letter`, `A5`, or `Custom`.
  36. - `orientation` *(string)* — Either `portrait` or `landscape`. Selecting `landscape` rotates the page 90°; any custom width/height you supply are interpreted in the stock's natural (portrait) orientation and the app flips them automatically while printing.
  37. - `margins` *(object in millimetres)* — Trim space on each edge with `top`, `right`, `bottom`, `left` properties.
  38. - `scale_mode` *(string)* — Scaling behavior: `fit` (proportional fit), `fit-x` (fit width), `fit-y` (fit height), `max-both`, `max-x`, `max-y`, or `manual`.
  39. - `scale_factor` *(number ≥ 0)* — Manual multiplier applied according to scale_mode.
  40. - `duplex`, `color`, `quality` *(optional)* — Mirrors the underlying OS print options.
  41. - `copies` *(number)* — Number of copies to print.
  42. - `custom_width_mm` / `custom_height_mm` *(numbers)* — Provide both to describe bespoke media using the printer's normal portrait orientation.
  43. ## Layout control
  44. - `center` *("none" | "horizontal" | "vertical" | "both" | null)* — Centers content when not disabled.
  45. - `center_disabled` *(bool)* — When `true`, ignores the `center` setting while keeping the last chosen mode for later.
  46. ## Direct print (optional)
  47. - `printer_name` *(string | null)* — If set, the System plugin will attempt to print directly to this OS printer by name.
  48. - `show_dialog_if_unfound` *(bool, default: true)* — When `true` (or omitted) and the named printer can't be resolved, a lightweight popup chooser appears. Set to `false` to skip the chooser and only open the PDF viewer.
  49. - `compatibility_mode` *(bool, default: false)* — When `true`, sends NO CUPS job options at all - only the raw PDF. Use this for severely broken printer filters (e.g., Kyocera network printers with crashing filters). The printer will use its default settings.
  50. ## Examples
  51. ### Custom Label Printer (e.g., ZQ510)
  52. ```json
  53. {
  54. "paper_size": "Custom",
  55. "orientation": "landscape",
  56. "margins": {
  57. "top": 0.0,
  58. "right": 0.0,
  59. "bottom": 0.0,
  60. "left": 0.0
  61. },
  62. "scale_mode": "fit",
  63. "scale_factor": 1.0,
  64. "color": false,
  65. "quality": "high",
  66. "copies": 1,
  67. "duplex": false,
  68. "custom_width_mm": 50.8,
  69. "custom_height_mm": 76.2,
  70. "center": "both",
  71. "center_disabled": false,
  72. "printer_name": "ZQ510",
  73. "show_dialog_if_unfound": true
  74. }
  75. ```
  76. ### Standard A4 Office Printer
  77. ```json
  78. {
  79. "paper_size": "A4",
  80. "orientation": "portrait",
  81. "margins": {
  82. "top": 12.7,
  83. "right": 12.7,
  84. "bottom": 12.7,
  85. "left": 12.7
  86. },
  87. "scale_mode": "fit",
  88. "scale_factor": 1.0,
  89. "color": true,
  90. "quality": "high",
  91. "copies": 1,
  92. "duplex": false,
  93. "center": "both",
  94. "center_disabled": false,
  95. "printer_name": "HP LaserJet Pro",
  96. "show_dialog_if_unfound": true
  97. }
  98. ```
  99. "#;
  100. const PDF_PRINTER_JSON_HELP: &str = r#"# PDF export JSON
  101. The PDF plugin understands the same shape as the System printer. Use the optional flags only when you want the enhanced layout controls; otherwise omit them for the classic renderer settings.
  102. ## Typical usage
  103. - Provide `paper_size` / `orientation` or include `custom_width_mm` + `custom_height_mm` for bespoke sheets. Enter the measurements in the stock's natural portrait orientation; landscape output is handled automatically.
  104. - Reuse the `margins` block from your system printers so labels line up identically.
  105. - `scale_mode`, `scale_factor`, `center`, `center_disabled` behave exactly the same as the System plugin.
  106. - The exported file path is still chosen through the PDF save dialog; these settings only influence page geometry.
  107. ## Available scale modes
  108. - `fit` — Proportionally fit the design within the printable area
  109. - `fit-x` — Fit to page width only
  110. - `fit-y` — Fit to page height only
  111. - `max-both` — Maximum size that fits both dimensions
  112. - `max-x` — Maximum width scaling
  113. - `max-y` — Maximum height scaling
  114. - `manual` — Use exact `scale_factor` value
  115. ## Example
  116. ```json
  117. {
  118. "paper_size": "Letter",
  119. "orientation": "portrait",
  120. "margins": { "top": 5.0, "right": 5.0, "bottom": 5.0, "left": 5.0 },
  121. "scale_mode": "manual",
  122. "scale_factor": 0.92,
  123. "center": "horizontal",
  124. "center_disabled": false
  125. }
  126. ```
  127. "#;
  128. pub struct PrintersView {
  129. printers: Vec<serde_json::Value>,
  130. is_loading: bool,
  131. last_error: Option<String>,
  132. initial_load_done: bool,
  133. // Table renderer
  134. table_renderer: TableRenderer,
  135. // Editor dialogs
  136. edit_dialog: FormBuilder,
  137. add_dialog: FormBuilder,
  138. delete_dialog: ConfirmDialog,
  139. // Pending operations
  140. pending_delete_id: Option<i64>,
  141. pending_edit_id: Option<i64>,
  142. // Navigation
  143. pub switch_to_print_history: bool,
  144. // Track last selected plugin to detect changes
  145. last_add_dialog_plugin: Option<String>,
  146. }
  147. impl PrintersView {
  148. pub fn new() -> Self {
  149. let edit_dialog = Self::create_edit_dialog();
  150. let add_dialog = Self::create_add_dialog();
  151. // Define columns for printer_settings table
  152. let columns = vec![
  153. ColumnConfig::new("ID", "id").with_width(60.0).hidden(),
  154. ColumnConfig::new("Printer Name", "printer_name").with_width(150.0),
  155. ColumnConfig::new("Description", "description").with_width(200.0),
  156. ColumnConfig::new("Plugin", "printer_plugin").with_width(100.0),
  157. ColumnConfig::new("Log Prints", "log").with_width(90.0),
  158. ColumnConfig::new("Use for Reports", "can_be_used_for_reports").with_width(120.0),
  159. ColumnConfig::new("Min Power Level", "min_powerlevel_to_use").with_width(110.0),
  160. ColumnConfig::new("Settings JSON", "printer_settings")
  161. .with_width(150.0)
  162. .hidden(),
  163. ];
  164. Self {
  165. printers: Vec::new(),
  166. is_loading: false,
  167. last_error: None,
  168. initial_load_done: false,
  169. table_renderer: TableRenderer::new()
  170. .with_columns(columns)
  171. .with_default_sort("printer_name", true)
  172. .with_search_fields(vec![
  173. "printer_name".to_string(),
  174. "description".to_string(),
  175. "printer_plugin".to_string(),
  176. ]),
  177. edit_dialog,
  178. add_dialog,
  179. delete_dialog: ConfirmDialog::new(
  180. "Delete Printer",
  181. "Are you sure you want to delete this printer configuration?",
  182. ),
  183. pending_delete_id: None,
  184. pending_edit_id: None,
  185. switch_to_print_history: false,
  186. last_add_dialog_plugin: None,
  187. }
  188. }
  189. fn plugin_help_text(plugin: &str) -> Option<&'static str> {
  190. match plugin {
  191. "System" => Some(SYSTEM_PRINTER_JSON_HELP),
  192. "PDF" => Some(PDF_PRINTER_JSON_HELP),
  193. _ => None,
  194. }
  195. }
  196. fn apply_plugin_help(editor: &mut FormBuilder, plugin: Option<&str>) {
  197. if let Some(plugin) = plugin {
  198. if let Some(help) = Self::plugin_help_text(plugin) {
  199. editor.form_help_text = Some(help.to_string());
  200. return;
  201. }
  202. }
  203. editor.form_help_text = None;
  204. }
  205. fn create_edit_dialog() -> FormBuilder {
  206. let plugin_options = vec![
  207. ("Ptouch".to_string(), "Brother P-Touch".to_string()),
  208. ("Brother".to_string(), "Brother (Generic)".to_string()),
  209. ("Zebra".to_string(), "Zebra".to_string()),
  210. ("System".to_string(), "System Printer".to_string()),
  211. ("PDF".to_string(), "PDF Export".to_string()),
  212. ("Network".to_string(), "Network Printer".to_string()),
  213. ("Custom".to_string(), "Custom".to_string()),
  214. ];
  215. FormBuilder::new(
  216. "Edit Printer",
  217. vec![
  218. EditorField {
  219. name: "id".into(),
  220. label: "ID".into(),
  221. field_type: FieldType::Text,
  222. required: false,
  223. read_only: true,
  224. },
  225. EditorField {
  226. name: "printer_name".into(),
  227. label: "Printer Name".into(),
  228. field_type: FieldType::Text,
  229. required: true,
  230. read_only: false,
  231. },
  232. EditorField {
  233. name: "description".into(),
  234. label: "Description".into(),
  235. field_type: FieldType::MultilineText,
  236. required: false,
  237. read_only: false,
  238. },
  239. EditorField {
  240. name: "printer_plugin".into(),
  241. label: "Printer Plugin".into(),
  242. field_type: FieldType::Dropdown(plugin_options.clone()),
  243. required: true,
  244. read_only: false,
  245. },
  246. EditorField {
  247. name: "log".into(),
  248. label: "Log Print Jobs".into(),
  249. field_type: FieldType::Checkbox,
  250. required: false,
  251. read_only: false,
  252. },
  253. EditorField {
  254. name: "can_be_used_for_reports".into(),
  255. label: "Can Print Reports".into(),
  256. field_type: FieldType::Checkbox,
  257. required: false,
  258. read_only: false,
  259. },
  260. EditorField {
  261. name: "min_powerlevel_to_use".into(),
  262. label: "Minimum Power Level".into(),
  263. field_type: FieldType::Text,
  264. required: true,
  265. read_only: false,
  266. },
  267. EditorField {
  268. name: "printer_settings".into(),
  269. label: "Printer Settings Required".into(),
  270. field_type: FieldType::MultilineText,
  271. required: false,
  272. read_only: false,
  273. },
  274. ],
  275. )
  276. }
  277. fn create_add_dialog() -> FormBuilder {
  278. let plugin_options = vec![
  279. ("Ptouch".to_string(), "Brother P-Touch".to_string()),
  280. ("Brother".to_string(), "Brother (Generic)".to_string()),
  281. ("Zebra".to_string(), "Zebra".to_string()),
  282. ("System".to_string(), "System Printer".to_string()),
  283. ("PDF".to_string(), "PDF Export".to_string()),
  284. ("Network".to_string(), "Network Printer".to_string()),
  285. ("Custom".to_string(), "Custom".to_string()),
  286. ];
  287. FormBuilder::new(
  288. "Add Printer",
  289. vec![
  290. EditorField {
  291. name: "printer_name".into(),
  292. label: "Printer Name".into(),
  293. field_type: FieldType::Text,
  294. required: true,
  295. read_only: false,
  296. },
  297. EditorField {
  298. name: "description".into(),
  299. label: "Description".into(),
  300. field_type: FieldType::MultilineText,
  301. required: false,
  302. read_only: false,
  303. },
  304. EditorField {
  305. name: "printer_plugin".into(),
  306. label: "Printer Plugin".into(),
  307. field_type: FieldType::Dropdown(plugin_options),
  308. required: true,
  309. read_only: false,
  310. },
  311. EditorField {
  312. name: "log".into(),
  313. label: "Log Print Jobs".into(),
  314. field_type: FieldType::Checkbox,
  315. required: false,
  316. read_only: false,
  317. },
  318. EditorField {
  319. name: "can_be_used_for_reports".into(),
  320. label: "Can Print Reports".into(),
  321. field_type: FieldType::Checkbox,
  322. required: false,
  323. read_only: false,
  324. },
  325. EditorField {
  326. name: "min_powerlevel_to_use".into(),
  327. label: "Minimum Power Level".into(),
  328. field_type: FieldType::Text,
  329. required: true,
  330. read_only: false,
  331. },
  332. EditorField {
  333. name: "printer_settings".into(),
  334. label: "Printer Settings (JSON)".into(),
  335. field_type: FieldType::MultilineText,
  336. required: true,
  337. read_only: false,
  338. },
  339. ],
  340. )
  341. }
  342. fn ensure_loaded(&mut self, api_client: Option<&ApiClient>) {
  343. if self.is_loading || self.initial_load_done {
  344. return;
  345. }
  346. if let Some(client) = api_client {
  347. self.load_printers(client);
  348. }
  349. }
  350. fn load_printers(&mut self, api_client: &ApiClient) {
  351. use crate::core::tables::get_printers;
  352. self.is_loading = true;
  353. self.last_error = None;
  354. match get_printers(api_client) {
  355. Ok(list) => {
  356. self.printers = list;
  357. self.is_loading = false;
  358. self.initial_load_done = true;
  359. }
  360. Err(e) => {
  361. self.last_error = Some(e.to_string());
  362. self.is_loading = false;
  363. self.initial_load_done = true;
  364. }
  365. }
  366. }
  367. pub fn render(
  368. &mut self,
  369. ui: &mut egui::Ui,
  370. api_client: Option<&ApiClient>,
  371. ribbon_ui: Option<&mut RibbonUI>,
  372. session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
  373. permissions: Option<&serde_json::Value>,
  374. ) {
  375. self.ensure_loaded(api_client);
  376. // Get search query from ribbon first (before mutable borrow)
  377. let search_query = ribbon_ui
  378. .as_ref()
  379. .and_then(|r| r.search_texts.get("printers_search"))
  380. .map(|s| s.clone())
  381. .unwrap_or_default();
  382. // Apply search to table renderer
  383. self.table_renderer.search_query = search_query;
  384. // Handle ribbon actions and default printer dropdown
  385. if let Some(ribbon) = ribbon_ui {
  386. if ribbon
  387. .checkboxes
  388. .get("printers_action_add")
  389. .copied()
  390. .unwrap_or(false)
  391. {
  392. if RibbonUI::check_permission(permissions, "create_printer") {
  393. // Provide default values - printer_settings will get plugin-specific template
  394. let default_data = serde_json::json!({
  395. "printer_settings": "{}",
  396. "log": true,
  397. "can_be_used_for_reports": false,
  398. "min_powerlevel_to_use": "0"
  399. });
  400. self.add_dialog.open(&default_data);
  401. }
  402. }
  403. if ribbon
  404. .checkboxes
  405. .get("printers_action_refresh")
  406. .copied()
  407. .unwrap_or(false)
  408. {
  409. if let Some(client) = api_client {
  410. self.load_printers(client);
  411. }
  412. }
  413. if ribbon
  414. .checkboxes
  415. .get("printers_view_print_history")
  416. .copied()
  417. .unwrap_or(false)
  418. {
  419. self.switch_to_print_history = true;
  420. }
  421. // Handle default printer dropdown (will be rendered in Settings group)
  422. // Store selected printer ID change flag
  423. if ribbon
  424. .checkboxes
  425. .get("printers_default_changed")
  426. .copied()
  427. .unwrap_or(false)
  428. {
  429. if let Some(printer_id_str) = ribbon.search_texts.get("printers_default_id") {
  430. if printer_id_str.is_empty() {
  431. // Clear default printer
  432. if let Ok(mut session) = session_manager.try_lock() {
  433. if let Err(e) = session.update_default_printer(None) {
  434. log::error!("Failed to clear default printer: {}", e);
  435. } else {
  436. log::info!("Default printer cleared");
  437. }
  438. }
  439. } else if let Ok(printer_id) = printer_id_str.parse::<i64>() {
  440. // Set default printer
  441. if let Ok(mut session) = session_manager.try_lock() {
  442. if let Err(e) = session.update_default_printer(Some(printer_id)) {
  443. log::error!("Failed to update default printer: {}", e);
  444. } else {
  445. log::info!("Default printer set to ID: {}", printer_id);
  446. }
  447. }
  448. }
  449. }
  450. }
  451. }
  452. // Error message
  453. let mut clear_error = false;
  454. if let Some(err) = &self.last_error {
  455. ui.horizontal(|ui| {
  456. ui.colored_label(egui::Color32::RED, format!("Error: {}", err));
  457. if ui.button("Close").clicked() {
  458. clear_error = true;
  459. }
  460. });
  461. ui.separator();
  462. }
  463. if clear_error {
  464. self.last_error = None;
  465. }
  466. // Loading indicator
  467. if self.is_loading {
  468. ui.spinner();
  469. ui.label("Loading printers...");
  470. return;
  471. }
  472. // Render table with event handling
  473. self.render_table_with_events(ui, api_client, permissions);
  474. // Handle dialogs
  475. self.handle_dialogs(ui, api_client);
  476. // Process deferred actions from context menus
  477. self.process_deferred_actions(ui, api_client);
  478. }
  479. /// Called before rendering to inject printer dropdown data into ribbon
  480. pub fn inject_dropdown_into_ribbon(
  481. &self,
  482. ribbon_ui: &mut RibbonUI,
  483. session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
  484. ) {
  485. // Try to get current default printer ID without blocking (avoid Tokio panic)
  486. let current_default = session_manager
  487. .try_lock()
  488. .ok()
  489. .and_then(|s| s.get_default_printer_id());
  490. // Store current default for ribbon rendering
  491. if let Some(id) = current_default {
  492. ribbon_ui
  493. .search_texts
  494. .insert("_printers_current_default".to_string(), id.to_string());
  495. } else {
  496. ribbon_ui
  497. .search_texts
  498. .insert("_printers_current_default".to_string(), "".to_string());
  499. }
  500. // Store printer list as JSON string for ribbon to parse
  501. let printers_json = serde_json::to_string(&self.printers).unwrap_or_default();
  502. ribbon_ui
  503. .search_texts
  504. .insert("_printers_list".to_string(), printers_json);
  505. }
  506. fn render_table_with_events(
  507. &mut self,
  508. ui: &mut egui::Ui,
  509. api_client: Option<&ApiClient>,
  510. permissions: Option<&serde_json::Value>,
  511. ) {
  512. let printers_clone = self.printers.clone();
  513. let prepared_data = self.table_renderer.prepare_json_data(&printers_clone);
  514. let mut deferred_actions: Vec<DeferredAction> = Vec::new();
  515. let mut temp_handler = TempPrintersEventHandler {
  516. api_client,
  517. deferred_actions: &mut deferred_actions,
  518. permissions,
  519. };
  520. self.table_renderer
  521. .render_json_table(ui, &prepared_data, Some(&mut temp_handler));
  522. self.process_temp_deferred_actions(deferred_actions, api_client);
  523. }
  524. fn process_temp_deferred_actions(
  525. &mut self,
  526. actions: Vec<DeferredAction>,
  527. _api_client: Option<&ApiClient>,
  528. ) {
  529. for action in actions {
  530. match action {
  531. DeferredAction::DoubleClick(printer) => {
  532. log::info!(
  533. "Processing double-click edit for printer: {:?}",
  534. printer.get("printer_name")
  535. );
  536. self.edit_dialog.open(&printer);
  537. if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
  538. self.pending_edit_id = Some(id);
  539. }
  540. }
  541. DeferredAction::ContextEdit(printer) => {
  542. log::info!(
  543. "Processing context menu edit for printer: {:?}",
  544. printer.get("printer_name")
  545. );
  546. self.edit_dialog.open(&printer);
  547. if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
  548. self.pending_edit_id = Some(id);
  549. }
  550. }
  551. DeferredAction::ContextDelete(printer) => {
  552. let name = printer
  553. .get("printer_name")
  554. .and_then(|v| v.as_str())
  555. .unwrap_or("Unknown");
  556. let id = printer.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
  557. log::info!("Processing context menu delete for printer: {}", name);
  558. self.pending_delete_id = Some(id);
  559. self.delete_dialog.open(name.to_string(), id.to_string());
  560. }
  561. DeferredAction::ContextClone(printer) => {
  562. log::info!(
  563. "Processing context menu clone for printer: {:?}",
  564. printer.get("printer_name")
  565. );
  566. let mut cloned = crate::core::components::prepare_cloned_value(
  567. &printer,
  568. &["id"],
  569. Some("printer_name"),
  570. Some(""),
  571. );
  572. if let Some(obj) = cloned.as_object_mut() {
  573. if let Some(ps) = obj.get("printer_settings") {
  574. let as_str = if ps.is_string() {
  575. ps.as_str().unwrap_or("{}").to_string()
  576. } else {
  577. serde_json::to_string_pretty(ps)
  578. .unwrap_or_else(|_| "{}".to_string())
  579. };
  580. obj.insert(
  581. "printer_settings".to_string(),
  582. serde_json::Value::String(as_str),
  583. );
  584. }
  585. self.add_dialog.open_new(Some(obj));
  586. }
  587. }
  588. }
  589. }
  590. }
  591. fn handle_dialogs(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) {
  592. // BEFORE showing add dialog, check if printer_plugin changed and auto-populate printer_settings
  593. if self.add_dialog.show {
  594. let current_plugin = self
  595. .add_dialog
  596. .data
  597. .get("printer_plugin")
  598. .map(|s| s.clone());
  599. // Detect if plugin changed to "System"
  600. if current_plugin != self.last_add_dialog_plugin {
  601. if let Some(ref plugin) = current_plugin {
  602. let template = match plugin.as_str() {
  603. "System" => Some(SYSTEM_PRINTER_SETTINGS_TEMPLATE),
  604. "PDF" => Some(PDF_PRINTER_SETTINGS_TEMPLATE),
  605. _ => None,
  606. };
  607. if let Some(template) = template {
  608. let current_settings = self
  609. .add_dialog
  610. .data
  611. .get("printer_settings")
  612. .map(|s| s.as_str())
  613. .unwrap_or("{}");
  614. if current_settings.trim().is_empty() || current_settings.trim() == "{}" {
  615. self.add_dialog
  616. .data
  617. .insert("printer_settings".to_string(), template.to_string());
  618. }
  619. }
  620. }
  621. self.last_add_dialog_plugin = current_plugin.clone();
  622. }
  623. Self::apply_plugin_help(&mut self.add_dialog, current_plugin.as_deref());
  624. } else {
  625. // Reset tracking when dialog closes
  626. self.last_add_dialog_plugin = None;
  627. self.add_dialog.form_help_text = None;
  628. }
  629. if self.edit_dialog.show {
  630. let edit_plugin = self
  631. .edit_dialog
  632. .data
  633. .get("printer_plugin")
  634. .map(|s| s.clone());
  635. Self::apply_plugin_help(&mut self.edit_dialog, edit_plugin.as_deref());
  636. } else {
  637. self.edit_dialog.form_help_text = None;
  638. }
  639. // Delete confirmation dialog
  640. if let Some(confirmed) = self.delete_dialog.show_dialog(ui.ctx()) {
  641. if confirmed {
  642. if let (Some(id), Some(client)) = (self.pending_delete_id, api_client) {
  643. let where_clause = serde_json::json!({"id": id});
  644. match client.delete("printer_settings", where_clause) {
  645. Ok(_) => {
  646. log::info!("Printer {} deleted successfully", id);
  647. self.load_printers(client);
  648. }
  649. Err(e) => {
  650. self.last_error = Some(format!("Failed to delete printer: {}", e));
  651. log::error!("Failed to delete printer: {}", e);
  652. }
  653. }
  654. }
  655. self.pending_delete_id = None;
  656. }
  657. }
  658. // Edit dialog
  659. if let Some(Some(updated)) = self.edit_dialog.show_editor(ui.ctx()) {
  660. if let (Some(id), Some(client)) = (self.pending_edit_id, api_client) {
  661. let where_clause = serde_json::json!({"id": id});
  662. // Ensure printer_settings field is valid JSON and send as JSON object
  663. let mut to_update = updated;
  664. // Remove generic editor metadata keys (avoid backend invalid column errors)
  665. let mut meta_keys: Vec<String> = to_update
  666. .keys()
  667. .filter(|k| k.starts_with("__editor_"))
  668. .cloned()
  669. .collect();
  670. // Also remove __editor_item_id specifically
  671. if to_update.contains_key("__editor_item_id") {
  672. meta_keys.push("__editor_item_id".to_string());
  673. }
  674. for k in meta_keys {
  675. to_update.remove(&k);
  676. }
  677. if let Some(val) = to_update.get_mut("printer_settings") {
  678. if let Some(s) = val.as_str() {
  679. match serde_json::from_str::<serde_json::Value>(s) {
  680. Ok(json_val) => {
  681. // Send as actual JSON object, not base64 string
  682. *val = json_val;
  683. }
  684. Err(e) => {
  685. self.last_error =
  686. Some(format!("Printer Settings JSON is invalid: {}", e));
  687. return;
  688. }
  689. }
  690. }
  691. }
  692. match client.update(
  693. "printer_settings",
  694. serde_json::Value::Object(to_update.clone()),
  695. where_clause,
  696. ) {
  697. Ok(resp) => {
  698. if resp.success {
  699. log::info!("Printer {} updated successfully", id);
  700. self.load_printers(client);
  701. } else {
  702. self.last_error = Some(format!("Update failed: {:?}", resp.error));
  703. log::error!("Update failed: {:?}", resp.error);
  704. }
  705. }
  706. Err(e) => {
  707. self.last_error = Some(format!("Failed to update printer: {}", e));
  708. log::error!("Failed to update printer: {}", e);
  709. }
  710. }
  711. self.pending_edit_id = None;
  712. }
  713. }
  714. // Add dialog
  715. if let Some(Some(new_data)) = self.add_dialog.show_editor(ui.ctx()) {
  716. if let Some(client) = api_client {
  717. // Parse printer_settings JSON and send as JSON object
  718. let mut payload = new_data;
  719. // Strip any editor metadata that may have leaked in
  720. let meta_strip: Vec<String> = payload
  721. .keys()
  722. .filter(|k| k.starts_with("__editor_"))
  723. .cloned()
  724. .collect();
  725. for k in meta_strip {
  726. payload.remove(&k);
  727. }
  728. if let Some(val) = payload.get_mut("printer_settings") {
  729. if let Some(s) = val.as_str() {
  730. match serde_json::from_str::<serde_json::Value>(s) {
  731. Ok(json_val) => {
  732. // Send as actual JSON object, not base64 string
  733. *val = json_val;
  734. }
  735. Err(e) => {
  736. self.last_error =
  737. Some(format!("Printer Settings JSON is invalid: {}", e));
  738. return;
  739. }
  740. }
  741. }
  742. }
  743. match client.insert(
  744. "printer_settings",
  745. serde_json::Value::Object(payload.clone()),
  746. ) {
  747. Ok(resp) => {
  748. if resp.success {
  749. log::info!("Printer added successfully");
  750. self.load_printers(client);
  751. } else {
  752. self.last_error = Some(format!("Insert failed: {:?}", resp.error));
  753. log::error!("Insert failed: {:?}", resp.error);
  754. }
  755. }
  756. Err(e) => {
  757. self.last_error = Some(format!("Failed to add printer: {}", e));
  758. log::error!("Failed to add printer: {}", e);
  759. }
  760. }
  761. }
  762. }
  763. }
  764. fn process_deferred_actions(&mut self, ui: &mut egui::Ui, _api_client: Option<&ApiClient>) {
  765. // Handle double-click edit
  766. if let Some(printer) = ui
  767. .ctx()
  768. .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("printer_double_click_edit")))
  769. {
  770. log::info!(
  771. "Processing double-click edit for printer: {:?}",
  772. printer.get("printer_name")
  773. );
  774. self.edit_dialog.open(&printer);
  775. if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
  776. self.pending_edit_id = Some(id);
  777. }
  778. }
  779. // Handle context menu actions
  780. if let Some(printer) = ui
  781. .ctx()
  782. .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("printer_context_menu_edit")))
  783. {
  784. log::info!(
  785. "Processing context menu edit for printer: {:?}",
  786. printer.get("printer_name")
  787. );
  788. self.edit_dialog.open(&printer);
  789. if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
  790. self.pending_edit_id = Some(id);
  791. }
  792. }
  793. if let Some(printer) = ui
  794. .ctx()
  795. .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("printer_context_menu_delete")))
  796. {
  797. let name = printer
  798. .get("printer_name")
  799. .and_then(|v| v.as_str())
  800. .unwrap_or("Unknown");
  801. let id = printer.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
  802. log::info!("Processing context menu delete for printer: {}", name);
  803. self.pending_delete_id = Some(id);
  804. self.delete_dialog.open(name.to_string(), id.to_string());
  805. }
  806. }
  807. }
  808. impl Default for PrintersView {
  809. fn default() -> Self {
  810. Self::new()
  811. }
  812. }
  813. #[derive(Clone)]
  814. enum DeferredAction {
  815. DoubleClick(Value),
  816. ContextEdit(Value),
  817. ContextDelete(Value),
  818. ContextClone(Value),
  819. }
  820. // Temporary event handler that collects actions for later processing
  821. struct TempPrintersEventHandler<'a> {
  822. #[allow(dead_code)]
  823. api_client: Option<&'a ApiClient>,
  824. deferred_actions: &'a mut Vec<DeferredAction>,
  825. permissions: Option<&'a serde_json::Value>,
  826. }
  827. impl<'a> TableEventHandler<Value> for TempPrintersEventHandler<'a> {
  828. fn on_double_click(&mut self, item: &Value, _row_index: usize) {
  829. if RibbonUI::check_permission(self.permissions, "edit_printer") {
  830. log::info!(
  831. "Double-click detected on printer: {:?}",
  832. item.get("printer_name")
  833. );
  834. self.deferred_actions
  835. .push(DeferredAction::DoubleClick(item.clone()));
  836. }
  837. }
  838. fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
  839. if RibbonUI::check_permission(self.permissions, "edit_printer") {
  840. if ui
  841. .button(format!("{} Edit Printer", egui_phosphor::regular::PENCIL))
  842. .clicked()
  843. {
  844. log::info!(
  845. "Context menu edit clicked for printer: {:?}",
  846. item.get("printer_name")
  847. );
  848. self.deferred_actions
  849. .push(DeferredAction::ContextEdit(item.clone()));
  850. ui.close();
  851. }
  852. ui.separator();
  853. }
  854. if RibbonUI::check_permission(self.permissions, "create_printer") {
  855. if ui
  856. .button(format!("{} Clone Printer", egui_phosphor::regular::COPY))
  857. .clicked()
  858. {
  859. log::info!(
  860. "Context menu clone clicked for printer: {:?}",
  861. item.get("printer_name")
  862. );
  863. self.deferred_actions
  864. .push(DeferredAction::ContextClone(item.clone()));
  865. ui.close();
  866. }
  867. ui.separator();
  868. }
  869. if RibbonUI::check_permission(self.permissions, "delete_printer") {
  870. if ui
  871. .button(format!("{} Delete Printer", egui_phosphor::regular::TRASH))
  872. .clicked()
  873. {
  874. log::info!(
  875. "Context menu delete clicked for printer: {:?}",
  876. item.get("printer_name")
  877. );
  878. self.deferred_actions
  879. .push(DeferredAction::ContextDelete(item.clone()));
  880. ui.close();
  881. }
  882. }
  883. }
  884. fn on_selection_changed(&mut self, selected_indices: &[usize]) {
  885. log::debug!("Printer selection changed: {:?}", selected_indices);
  886. }
  887. }