| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961 |
- use crate::api::ApiClient;
- use crate::core::components::form_builder::FormBuilder;
- use crate::core::components::interactions::ConfirmDialog;
- use crate::core::{ColumnConfig, TableEventHandler, TableRenderer};
- use crate::core::{EditorField, FieldType};
- use crate::ui::ribbon::RibbonUI;
- use eframe::egui;
- use serde_json::Value;
- const SYSTEM_PRINTER_SETTINGS_TEMPLATE: &str = r#"{
- "paper_size": "Custom",
- "orientation": "landscape",
- "margins": {
- "top": 0.0,
- "right": 0.0,
- "bottom": 0.0,
- "left": 0.0
- },
- "color": false,
- "quality": "high",
- "copies": 1,
- "duplex": false,
- "center": "both",
- "center_disabled": false,
- "scale_mode": "fit",
- "scale_factor": 1.0,
- "custom_width_mm": 50.8,
- "custom_height_mm": 76.2,
- "printer_name": null,
- "show_dialog_if_unfound": true
- }"#;
- const PDF_PRINTER_SETTINGS_TEMPLATE: &str = SYSTEM_PRINTER_SETTINGS_TEMPLATE;
- const SYSTEM_PRINTER_JSON_HELP: &str = r#"# System printer JSON
- Use this payload when registering the `System` printer plugin. Leave fields out to fall back to BeepZone's legacy sizing.
- ## Core fields
- - `paper_size` *(string)* — Named stock such as `A4`, `Letter`, `A5`, or `Custom`.
- - `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.
- - `margins` *(object in millimetres)* — Trim space on each edge with `top`, `right`, `bottom`, `left` properties.
- - `scale_mode` *(string)* — Scaling behavior: `fit` (proportional fit), `fit-x` (fit width), `fit-y` (fit height), `max-both`, `max-x`, `max-y`, or `manual`.
- - `scale_factor` *(number ≥ 0)* — Manual multiplier applied according to scale_mode.
- - `duplex`, `color`, `quality` *(optional)* — Mirrors the underlying OS print options.
- - `copies` *(number)* — Number of copies to print.
- - `custom_width_mm` / `custom_height_mm` *(numbers)* — Provide both to describe bespoke media using the printer's normal portrait orientation.
- ## Layout control
- - `center` *("none" | "horizontal" | "vertical" | "both" | null)* — Centers content when not disabled.
- - `center_disabled` *(bool)* — When `true`, ignores the `center` setting while keeping the last chosen mode for later.
- ## Direct print (optional)
- - `printer_name` *(string | null)* — If set, the System plugin will attempt to print directly to this OS printer by name.
- - `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.
- - `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.
- ## Examples
- ### Custom Label Printer (e.g., ZQ510)
- ```json
- {
- "paper_size": "Custom",
- "orientation": "landscape",
- "margins": {
- "top": 0.0,
- "right": 0.0,
- "bottom": 0.0,
- "left": 0.0
- },
- "scale_mode": "fit",
- "scale_factor": 1.0,
- "color": false,
- "quality": "high",
- "copies": 1,
- "duplex": false,
- "custom_width_mm": 50.8,
- "custom_height_mm": 76.2,
- "center": "both",
- "center_disabled": false,
- "printer_name": "ZQ510",
- "show_dialog_if_unfound": true
- }
- ```
- ### Standard A4 Office Printer
- ```json
- {
- "paper_size": "A4",
- "orientation": "portrait",
- "margins": {
- "top": 12.7,
- "right": 12.7,
- "bottom": 12.7,
- "left": 12.7
- },
- "scale_mode": "fit",
- "scale_factor": 1.0,
- "color": true,
- "quality": "high",
- "copies": 1,
- "duplex": false,
- "center": "both",
- "center_disabled": false,
- "printer_name": "HP LaserJet Pro",
- "show_dialog_if_unfound": true
- }
- ```
- "#;
- const PDF_PRINTER_JSON_HELP: &str = r#"# PDF export JSON
- 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.
- ## Typical usage
- - 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.
- - Reuse the `margins` block from your system printers so labels line up identically.
- - `scale_mode`, `scale_factor`, `center`, `center_disabled` behave exactly the same as the System plugin.
- - The exported file path is still chosen through the PDF save dialog; these settings only influence page geometry.
- ## Available scale modes
- - `fit` — Proportionally fit the design within the printable area
- - `fit-x` — Fit to page width only
- - `fit-y` — Fit to page height only
- - `max-both` — Maximum size that fits both dimensions
- - `max-x` — Maximum width scaling
- - `max-y` — Maximum height scaling
- - `manual` — Use exact `scale_factor` value
- ## Example
- ```json
- {
- "paper_size": "Letter",
- "orientation": "portrait",
- "margins": { "top": 5.0, "right": 5.0, "bottom": 5.0, "left": 5.0 },
- "scale_mode": "manual",
- "scale_factor": 0.92,
- "center": "horizontal",
- "center_disabled": false
- }
- ```
- "#;
- pub struct PrintersView {
- printers: Vec<serde_json::Value>,
- is_loading: bool,
- last_error: Option<String>,
- initial_load_done: bool,
- // Table renderer
- table_renderer: TableRenderer,
- // Editor dialogs
- edit_dialog: FormBuilder,
- add_dialog: FormBuilder,
- delete_dialog: ConfirmDialog,
- // Pending operations
- pending_delete_id: Option<i64>,
- pending_edit_id: Option<i64>,
- // Navigation
- pub switch_to_print_history: bool,
- // Track last selected plugin to detect changes
- last_add_dialog_plugin: Option<String>,
- }
- impl PrintersView {
- pub fn new() -> Self {
- let edit_dialog = Self::create_edit_dialog();
- let add_dialog = Self::create_add_dialog();
- // Define columns for printer_settings table
- let columns = vec![
- ColumnConfig::new("ID", "id").with_width(60.0).hidden(),
- ColumnConfig::new("Printer Name", "printer_name").with_width(150.0),
- ColumnConfig::new("Description", "description").with_width(200.0),
- ColumnConfig::new("Plugin", "printer_plugin").with_width(100.0),
- ColumnConfig::new("Log Prints", "log").with_width(90.0),
- ColumnConfig::new("Use for Reports", "can_be_used_for_reports").with_width(120.0),
- ColumnConfig::new("Min Power Level", "min_powerlevel_to_use").with_width(110.0),
- ColumnConfig::new("Settings JSON", "printer_settings")
- .with_width(150.0)
- .hidden(),
- ];
- Self {
- printers: Vec::new(),
- is_loading: false,
- last_error: None,
- initial_load_done: false,
- table_renderer: TableRenderer::new()
- .with_columns(columns)
- .with_default_sort("printer_name", true)
- .with_search_fields(vec![
- "printer_name".to_string(),
- "description".to_string(),
- "printer_plugin".to_string(),
- ]),
- edit_dialog,
- add_dialog,
- delete_dialog: ConfirmDialog::new(
- "Delete Printer",
- "Are you sure you want to delete this printer configuration?",
- ),
- pending_delete_id: None,
- pending_edit_id: None,
- switch_to_print_history: false,
- last_add_dialog_plugin: None,
- }
- }
- fn plugin_help_text(plugin: &str) -> Option<&'static str> {
- match plugin {
- "System" => Some(SYSTEM_PRINTER_JSON_HELP),
- "PDF" => Some(PDF_PRINTER_JSON_HELP),
- _ => None,
- }
- }
- fn apply_plugin_help(editor: &mut FormBuilder, plugin: Option<&str>) {
- if let Some(plugin) = plugin {
- if let Some(help) = Self::plugin_help_text(plugin) {
- editor.form_help_text = Some(help.to_string());
- return;
- }
- }
- editor.form_help_text = None;
- }
- fn create_edit_dialog() -> FormBuilder {
- let plugin_options = vec![
- ("Ptouch".to_string(), "Brother P-Touch".to_string()),
- ("Brother".to_string(), "Brother (Generic)".to_string()),
- ("Zebra".to_string(), "Zebra".to_string()),
- ("System".to_string(), "System Printer".to_string()),
- ("PDF".to_string(), "PDF Export".to_string()),
- ("Network".to_string(), "Network Printer".to_string()),
- ("Custom".to_string(), "Custom".to_string()),
- ];
- FormBuilder::new(
- "Edit Printer",
- vec![
- EditorField {
- name: "id".into(),
- label: "ID".into(),
- field_type: FieldType::Text,
- required: false,
- read_only: true,
- },
- EditorField {
- name: "printer_name".into(),
- label: "Printer Name".into(),
- field_type: FieldType::Text,
- required: true,
- read_only: false,
- },
- EditorField {
- name: "description".into(),
- label: "Description".into(),
- field_type: FieldType::MultilineText,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "printer_plugin".into(),
- label: "Printer Plugin".into(),
- field_type: FieldType::Dropdown(plugin_options.clone()),
- required: true,
- read_only: false,
- },
- EditorField {
- name: "log".into(),
- label: "Log Print Jobs".into(),
- field_type: FieldType::Checkbox,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "can_be_used_for_reports".into(),
- label: "Can Print Reports".into(),
- field_type: FieldType::Checkbox,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "min_powerlevel_to_use".into(),
- label: "Minimum Power Level".into(),
- field_type: FieldType::Text,
- required: true,
- read_only: false,
- },
- EditorField {
- name: "printer_settings".into(),
- label: "Printer Settings Required".into(),
- field_type: FieldType::MultilineText,
- required: false,
- read_only: false,
- },
- ],
- )
- }
- fn create_add_dialog() -> FormBuilder {
- let plugin_options = vec![
- ("Ptouch".to_string(), "Brother P-Touch".to_string()),
- ("Brother".to_string(), "Brother (Generic)".to_string()),
- ("Zebra".to_string(), "Zebra".to_string()),
- ("System".to_string(), "System Printer".to_string()),
- ("PDF".to_string(), "PDF Export".to_string()),
- ("Network".to_string(), "Network Printer".to_string()),
- ("Custom".to_string(), "Custom".to_string()),
- ];
- FormBuilder::new(
- "Add Printer",
- vec![
- EditorField {
- name: "printer_name".into(),
- label: "Printer Name".into(),
- field_type: FieldType::Text,
- required: true,
- read_only: false,
- },
- EditorField {
- name: "description".into(),
- label: "Description".into(),
- field_type: FieldType::MultilineText,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "printer_plugin".into(),
- label: "Printer Plugin".into(),
- field_type: FieldType::Dropdown(plugin_options),
- required: true,
- read_only: false,
- },
- EditorField {
- name: "log".into(),
- label: "Log Print Jobs".into(),
- field_type: FieldType::Checkbox,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "can_be_used_for_reports".into(),
- label: "Can Print Reports".into(),
- field_type: FieldType::Checkbox,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "min_powerlevel_to_use".into(),
- label: "Minimum Power Level".into(),
- field_type: FieldType::Text,
- required: true,
- read_only: false,
- },
- EditorField {
- name: "printer_settings".into(),
- label: "Printer Settings (JSON)".into(),
- field_type: FieldType::MultilineText,
- required: true,
- read_only: false,
- },
- ],
- )
- }
- fn ensure_loaded(&mut self, api_client: Option<&ApiClient>) {
- if self.is_loading || self.initial_load_done {
- return;
- }
- if let Some(client) = api_client {
- self.load_printers(client);
- }
- }
- fn load_printers(&mut self, api_client: &ApiClient) {
- use crate::core::tables::get_printers;
- self.is_loading = true;
- self.last_error = None;
- match get_printers(api_client) {
- Ok(list) => {
- self.printers = list;
- self.is_loading = false;
- self.initial_load_done = true;
- }
- Err(e) => {
- self.last_error = Some(e.to_string());
- self.is_loading = false;
- self.initial_load_done = true;
- }
- }
- }
- pub fn render(
- &mut self,
- ui: &mut egui::Ui,
- api_client: Option<&ApiClient>,
- ribbon_ui: Option<&mut RibbonUI>,
- session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
- permissions: Option<&serde_json::Value>,
- ) {
- self.ensure_loaded(api_client);
- // Get search query from ribbon first (before mutable borrow)
- let search_query = ribbon_ui
- .as_ref()
- .and_then(|r| r.search_texts.get("printers_search"))
- .map(|s| s.clone())
- .unwrap_or_default();
- // Apply search to table renderer
- self.table_renderer.search_query = search_query;
- // Handle ribbon actions and default printer dropdown
- if let Some(ribbon) = ribbon_ui {
- if ribbon
- .checkboxes
- .get("printers_action_add")
- .copied()
- .unwrap_or(false)
- {
- if RibbonUI::check_permission(permissions, "create_printer") {
- // Provide default values - printer_settings will get plugin-specific template
- let default_data = serde_json::json!({
- "printer_settings": "{}",
- "log": true,
- "can_be_used_for_reports": false,
- "min_powerlevel_to_use": "0"
- });
- self.add_dialog.open(&default_data);
- }
- }
- if ribbon
- .checkboxes
- .get("printers_action_refresh")
- .copied()
- .unwrap_or(false)
- {
- if let Some(client) = api_client {
- self.load_printers(client);
- }
- }
- if ribbon
- .checkboxes
- .get("printers_view_print_history")
- .copied()
- .unwrap_or(false)
- {
- self.switch_to_print_history = true;
- }
- // Handle default printer dropdown (will be rendered in Settings group)
- // Store selected printer ID change flag
- if ribbon
- .checkboxes
- .get("printers_default_changed")
- .copied()
- .unwrap_or(false)
- {
- if let Some(printer_id_str) = ribbon.search_texts.get("printers_default_id") {
- if printer_id_str.is_empty() {
- // Clear default printer
- if let Ok(mut session) = session_manager.try_lock() {
- if let Err(e) = session.update_default_printer(None) {
- log::error!("Failed to clear default printer: {}", e);
- } else {
- log::info!("Default printer cleared");
- }
- }
- } else if let Ok(printer_id) = printer_id_str.parse::<i64>() {
- // Set default printer
- if let Ok(mut session) = session_manager.try_lock() {
- if let Err(e) = session.update_default_printer(Some(printer_id)) {
- log::error!("Failed to update default printer: {}", e);
- } else {
- log::info!("Default printer set to ID: {}", printer_id);
- }
- }
- }
- }
- }
- }
- // Error message
- let mut clear_error = false;
- if let Some(err) = &self.last_error {
- ui.horizontal(|ui| {
- ui.colored_label(egui::Color32::RED, format!("Error: {}", err));
- if ui.button("Close").clicked() {
- clear_error = true;
- }
- });
- ui.separator();
- }
- if clear_error {
- self.last_error = None;
- }
- // Loading indicator
- if self.is_loading {
- ui.spinner();
- ui.label("Loading printers...");
- return;
- }
- // Render table with event handling
- self.render_table_with_events(ui, api_client, permissions);
- // Handle dialogs
- self.handle_dialogs(ui, api_client);
- // Process deferred actions from context menus
- self.process_deferred_actions(ui, api_client);
- }
- /// Called before rendering to inject printer dropdown data into ribbon
- pub fn inject_dropdown_into_ribbon(
- &self,
- ribbon_ui: &mut RibbonUI,
- session_manager: &std::sync::Arc<tokio::sync::Mutex<crate::session::SessionManager>>,
- ) {
- // Try to get current default printer ID without blocking (avoid Tokio panic)
- let current_default = session_manager
- .try_lock()
- .ok()
- .and_then(|s| s.get_default_printer_id());
- // Store current default for ribbon rendering
- if let Some(id) = current_default {
- ribbon_ui
- .search_texts
- .insert("_printers_current_default".to_string(), id.to_string());
- } else {
- ribbon_ui
- .search_texts
- .insert("_printers_current_default".to_string(), "".to_string());
- }
- // Store printer list as JSON string for ribbon to parse
- let printers_json = serde_json::to_string(&self.printers).unwrap_or_default();
- ribbon_ui
- .search_texts
- .insert("_printers_list".to_string(), printers_json);
- }
- fn render_table_with_events(
- &mut self,
- ui: &mut egui::Ui,
- api_client: Option<&ApiClient>,
- permissions: Option<&serde_json::Value>,
- ) {
- let printers_clone = self.printers.clone();
- let prepared_data = self.table_renderer.prepare_json_data(&printers_clone);
- let mut deferred_actions: Vec<DeferredAction> = Vec::new();
- let mut temp_handler = TempPrintersEventHandler {
- api_client,
- deferred_actions: &mut deferred_actions,
- permissions,
- };
- self.table_renderer
- .render_json_table(ui, &prepared_data, Some(&mut temp_handler));
- self.process_temp_deferred_actions(deferred_actions, api_client);
- }
- fn process_temp_deferred_actions(
- &mut self,
- actions: Vec<DeferredAction>,
- _api_client: Option<&ApiClient>,
- ) {
- for action in actions {
- match action {
- DeferredAction::DoubleClick(printer) => {
- log::info!(
- "Processing double-click edit for printer: {:?}",
- printer.get("printer_name")
- );
- self.edit_dialog.open(&printer);
- if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
- self.pending_edit_id = Some(id);
- }
- }
- DeferredAction::ContextEdit(printer) => {
- log::info!(
- "Processing context menu edit for printer: {:?}",
- printer.get("printer_name")
- );
- self.edit_dialog.open(&printer);
- if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
- self.pending_edit_id = Some(id);
- }
- }
- DeferredAction::ContextDelete(printer) => {
- let name = printer
- .get("printer_name")
- .and_then(|v| v.as_str())
- .unwrap_or("Unknown");
- let id = printer.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
- log::info!("Processing context menu delete for printer: {}", name);
- self.pending_delete_id = Some(id);
- self.delete_dialog.open(name.to_string(), id.to_string());
- }
- DeferredAction::ContextClone(printer) => {
- log::info!(
- "Processing context menu clone for printer: {:?}",
- printer.get("printer_name")
- );
- let mut cloned = crate::core::components::prepare_cloned_value(
- &printer,
- &["id"],
- Some("printer_name"),
- Some(""),
- );
- if let Some(obj) = cloned.as_object_mut() {
- if let Some(ps) = obj.get("printer_settings") {
- let as_str = if ps.is_string() {
- ps.as_str().unwrap_or("{}").to_string()
- } else {
- serde_json::to_string_pretty(ps)
- .unwrap_or_else(|_| "{}".to_string())
- };
- obj.insert(
- "printer_settings".to_string(),
- serde_json::Value::String(as_str),
- );
- }
- self.add_dialog.open_new(Some(obj));
- }
- }
- }
- }
- }
- fn handle_dialogs(&mut self, ui: &mut egui::Ui, api_client: Option<&ApiClient>) {
- // BEFORE showing add dialog, check if printer_plugin changed and auto-populate printer_settings
- if self.add_dialog.show {
- let current_plugin = self
- .add_dialog
- .data
- .get("printer_plugin")
- .map(|s| s.clone());
- // Detect if plugin changed to "System"
- if current_plugin != self.last_add_dialog_plugin {
- if let Some(ref plugin) = current_plugin {
- let template = match plugin.as_str() {
- "System" => Some(SYSTEM_PRINTER_SETTINGS_TEMPLATE),
- "PDF" => Some(PDF_PRINTER_SETTINGS_TEMPLATE),
- _ => None,
- };
- if let Some(template) = template {
- let current_settings = self
- .add_dialog
- .data
- .get("printer_settings")
- .map(|s| s.as_str())
- .unwrap_or("{}");
- if current_settings.trim().is_empty() || current_settings.trim() == "{}" {
- self.add_dialog
- .data
- .insert("printer_settings".to_string(), template.to_string());
- }
- }
- }
- self.last_add_dialog_plugin = current_plugin.clone();
- }
- Self::apply_plugin_help(&mut self.add_dialog, current_plugin.as_deref());
- } else {
- // Reset tracking when dialog closes
- self.last_add_dialog_plugin = None;
- self.add_dialog.form_help_text = None;
- }
- if self.edit_dialog.show {
- let edit_plugin = self
- .edit_dialog
- .data
- .get("printer_plugin")
- .map(|s| s.clone());
- Self::apply_plugin_help(&mut self.edit_dialog, edit_plugin.as_deref());
- } else {
- self.edit_dialog.form_help_text = None;
- }
- // Delete confirmation dialog
- if let Some(confirmed) = self.delete_dialog.show_dialog(ui.ctx()) {
- if confirmed {
- if let (Some(id), Some(client)) = (self.pending_delete_id, api_client) {
- let where_clause = serde_json::json!({"id": id});
- match client.delete("printer_settings", where_clause) {
- Ok(_) => {
- log::info!("Printer {} deleted successfully", id);
- self.load_printers(client);
- }
- Err(e) => {
- self.last_error = Some(format!("Failed to delete printer: {}", e));
- log::error!("Failed to delete printer: {}", e);
- }
- }
- }
- self.pending_delete_id = None;
- }
- }
- // Edit dialog
- if let Some(Some(updated)) = self.edit_dialog.show_editor(ui.ctx()) {
- if let (Some(id), Some(client)) = (self.pending_edit_id, api_client) {
- let where_clause = serde_json::json!({"id": id});
- // Ensure printer_settings field is valid JSON and send as JSON object
- let mut to_update = updated;
- // Remove generic editor metadata keys (avoid backend invalid column errors)
- let mut meta_keys: Vec<String> = to_update
- .keys()
- .filter(|k| k.starts_with("__editor_"))
- .cloned()
- .collect();
- // Also remove __editor_item_id specifically
- if to_update.contains_key("__editor_item_id") {
- meta_keys.push("__editor_item_id".to_string());
- }
- for k in meta_keys {
- to_update.remove(&k);
- }
- if let Some(val) = to_update.get_mut("printer_settings") {
- if let Some(s) = val.as_str() {
- match serde_json::from_str::<serde_json::Value>(s) {
- Ok(json_val) => {
- // Send as actual JSON object, not base64 string
- *val = json_val;
- }
- Err(e) => {
- self.last_error =
- Some(format!("Printer Settings JSON is invalid: {}", e));
- return;
- }
- }
- }
- }
- match client.update(
- "printer_settings",
- serde_json::Value::Object(to_update.clone()),
- where_clause,
- ) {
- Ok(resp) => {
- if resp.success {
- log::info!("Printer {} updated successfully", id);
- self.load_printers(client);
- } else {
- self.last_error = Some(format!("Update failed: {:?}", resp.error));
- log::error!("Update failed: {:?}", resp.error);
- }
- }
- Err(e) => {
- self.last_error = Some(format!("Failed to update printer: {}", e));
- log::error!("Failed to update printer: {}", e);
- }
- }
- self.pending_edit_id = None;
- }
- }
- // Add dialog
- if let Some(Some(new_data)) = self.add_dialog.show_editor(ui.ctx()) {
- if let Some(client) = api_client {
- // Parse printer_settings JSON and send as JSON object
- let mut payload = new_data;
- // Strip any editor metadata that may have leaked in
- let meta_strip: Vec<String> = payload
- .keys()
- .filter(|k| k.starts_with("__editor_"))
- .cloned()
- .collect();
- for k in meta_strip {
- payload.remove(&k);
- }
- if let Some(val) = payload.get_mut("printer_settings") {
- if let Some(s) = val.as_str() {
- match serde_json::from_str::<serde_json::Value>(s) {
- Ok(json_val) => {
- // Send as actual JSON object, not base64 string
- *val = json_val;
- }
- Err(e) => {
- self.last_error =
- Some(format!("Printer Settings JSON is invalid: {}", e));
- return;
- }
- }
- }
- }
- match client.insert(
- "printer_settings",
- serde_json::Value::Object(payload.clone()),
- ) {
- Ok(resp) => {
- if resp.success {
- log::info!("Printer added successfully");
- self.load_printers(client);
- } else {
- self.last_error = Some(format!("Insert failed: {:?}", resp.error));
- log::error!("Insert failed: {:?}", resp.error);
- }
- }
- Err(e) => {
- self.last_error = Some(format!("Failed to add printer: {}", e));
- log::error!("Failed to add printer: {}", e);
- }
- }
- }
- }
- }
- fn process_deferred_actions(&mut self, ui: &mut egui::Ui, _api_client: Option<&ApiClient>) {
- // Handle double-click edit
- if let Some(printer) = ui
- .ctx()
- .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("printer_double_click_edit")))
- {
- log::info!(
- "Processing double-click edit for printer: {:?}",
- printer.get("printer_name")
- );
- self.edit_dialog.open(&printer);
- if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
- self.pending_edit_id = Some(id);
- }
- }
- // Handle context menu actions
- if let Some(printer) = ui
- .ctx()
- .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("printer_context_menu_edit")))
- {
- log::info!(
- "Processing context menu edit for printer: {:?}",
- printer.get("printer_name")
- );
- self.edit_dialog.open(&printer);
- if let Some(id) = printer.get("id").and_then(|v| v.as_i64()) {
- self.pending_edit_id = Some(id);
- }
- }
- if let Some(printer) = ui
- .ctx()
- .data_mut(|d| d.remove_temp::<Value>(egui::Id::new("printer_context_menu_delete")))
- {
- let name = printer
- .get("printer_name")
- .and_then(|v| v.as_str())
- .unwrap_or("Unknown");
- let id = printer.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
- log::info!("Processing context menu delete for printer: {}", name);
- self.pending_delete_id = Some(id);
- self.delete_dialog.open(name.to_string(), id.to_string());
- }
- }
- }
- impl Default for PrintersView {
- fn default() -> Self {
- Self::new()
- }
- }
- #[derive(Clone)]
- enum DeferredAction {
- DoubleClick(Value),
- ContextEdit(Value),
- ContextDelete(Value),
- ContextClone(Value),
- }
- // Temporary event handler that collects actions for later processing
- struct TempPrintersEventHandler<'a> {
- #[allow(dead_code)]
- api_client: Option<&'a ApiClient>,
- deferred_actions: &'a mut Vec<DeferredAction>,
- permissions: Option<&'a serde_json::Value>,
- }
- impl<'a> TableEventHandler<Value> for TempPrintersEventHandler<'a> {
- fn on_double_click(&mut self, item: &Value, _row_index: usize) {
- if RibbonUI::check_permission(self.permissions, "edit_printer") {
- log::info!(
- "Double-click detected on printer: {:?}",
- item.get("printer_name")
- );
- self.deferred_actions
- .push(DeferredAction::DoubleClick(item.clone()));
- }
- }
- fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
- if RibbonUI::check_permission(self.permissions, "edit_printer") {
- if ui
- .button(format!("{} Edit Printer", egui_phosphor::regular::PENCIL))
- .clicked()
- {
- log::info!(
- "Context menu edit clicked for printer: {:?}",
- item.get("printer_name")
- );
- self.deferred_actions
- .push(DeferredAction::ContextEdit(item.clone()));
- ui.close();
- }
- ui.separator();
- }
- if RibbonUI::check_permission(self.permissions, "create_printer") {
- if ui
- .button(format!("{} Clone Printer", egui_phosphor::regular::COPY))
- .clicked()
- {
- log::info!(
- "Context menu clone clicked for printer: {:?}",
- item.get("printer_name")
- );
- self.deferred_actions
- .push(DeferredAction::ContextClone(item.clone()));
- ui.close();
- }
- ui.separator();
- }
- if RibbonUI::check_permission(self.permissions, "delete_printer") {
- if ui
- .button(format!("{} Delete Printer", egui_phosphor::regular::TRASH))
- .clicked()
- {
- log::info!(
- "Context menu delete clicked for printer: {:?}",
- item.get("printer_name")
- );
- self.deferred_actions
- .push(DeferredAction::ContextDelete(item.clone()));
- ui.close();
- }
- }
- }
- fn on_selection_changed(&mut self, selected_indices: &[usize]) {
- log::debug!("Printer selection changed: {:?}", selected_indices);
- }
- }
|