| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905 |
- use crate::api::ApiClient;
- use crate::core::components::form_builder::FormBuilder;
- use crate::core::components::interactions::ConfirmDialog;
- use crate::core::table_renderer::{ColumnConfig, TableEventHandler, TableRenderer};
- use crate::core::tables::get_categories;
- use crate::core::{EditorField, FieldType};
- use crate::ui::ribbon::RibbonUI;
- use eframe::egui;
- use serde_json::Value;
- pub struct CategoriesView {
- categories: Vec<serde_json::Value>,
- is_loading: bool,
- last_error: Option<String>,
- initial_load_done: bool,
- load_attempted: bool, // New field to track if we've tried loading
- // Editor dialogs
- edit_dialog: FormBuilder,
- add_dialog: FormBuilder,
- delete_dialog: ConfirmDialog,
- // Pending operations
- pending_delete_ids: Vec<i64>, // Changed from Option<i64> to Vec<i64> for bulk delete support
- pending_edit_ids: Vec<i64>, // Changed from Option<i64> to Vec<i64> for bulk edit support
- // Table rendering
- table_renderer: crate::core::table_renderer::TableRenderer,
- }
- impl CategoriesView {
- pub fn new() -> Self {
- let edit_dialog = Self::create_edit_dialog();
- let add_dialog = Self::create_placeholder_add_dialog();
- // Define columns for categories table - code before name as requested
- let columns = vec![
- ColumnConfig::new("ID", "id").with_width(60.0).hidden(),
- ColumnConfig::new("Category Code", "category_code").with_width(120.0),
- ColumnConfig::new("Category Name", "category_name").with_width(200.0),
- ColumnConfig::new("Description", "category_description").with_width(300.0),
- ColumnConfig::new("Parent ID", "parent_id")
- .with_width(80.0)
- .hidden(),
- ColumnConfig::new("Parent Category", "parent_category_name").with_width(150.0),
- ];
- Self {
- categories: Vec::new(),
- is_loading: false,
- last_error: None,
- initial_load_done: false,
- load_attempted: false,
- edit_dialog,
- add_dialog,
- delete_dialog: ConfirmDialog::new(
- "Delete Category",
- "Are you sure you want to delete this category? This will affect all assets using this category.",
- ),
- pending_delete_ids: Vec::new(),
- pending_edit_ids: Vec::new(),
- table_renderer: TableRenderer::new()
- .with_columns(columns)
- .with_default_sort("category_code", true) // Sort by category code alphabetically
- .with_search_fields(vec![
- "category_name".to_string(),
- "category_code".to_string(),
- "category_description".to_string(),
- "parent_category_name".to_string(),
- ]),
- }
- }
- fn create_edit_dialog() -> FormBuilder {
- FormBuilder::new(
- "Edit Category",
- vec![
- EditorField {
- name: "id".into(),
- label: "ID".into(),
- field_type: FieldType::Text,
- required: false,
- read_only: true,
- },
- EditorField {
- name: "category_name".into(),
- label: "Category Name".into(),
- field_type: FieldType::Text,
- required: true,
- read_only: false,
- },
- EditorField {
- name: "category_code".into(),
- label: "Category Code".into(),
- field_type: FieldType::Text,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "category_description".into(),
- label: "Description".into(),
- field_type: FieldType::MultilineText,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "parent_id".into(),
- label: "Parent Category".into(),
- field_type: FieldType::Text, // TODO: Make this a dropdown with other categories
- required: false,
- read_only: false,
- },
- ],
- )
- }
- fn create_placeholder_add_dialog() -> FormBuilder {
- FormBuilder::new(
- "Add Category",
- vec![
- EditorField {
- name: "category_name".into(),
- label: "Category Name".into(),
- field_type: FieldType::Text,
- required: true,
- read_only: false,
- },
- EditorField {
- name: "category_code".into(),
- label: "Category Code".into(),
- field_type: FieldType::Text,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "category_description".into(),
- label: "Description".into(),
- field_type: FieldType::MultilineText,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "parent_id".into(),
- label: "Parent Category".into(),
- field_type: FieldType::Text, // Will be updated to dropdown when opened
- required: false,
- read_only: false,
- },
- ],
- )
- }
- fn create_add_dialog_with_options(&self) -> FormBuilder {
- let category_options = self.create_category_dropdown_options(None);
- FormBuilder::new(
- "Add Category",
- vec![
- EditorField {
- name: "category_name".into(),
- label: "Category Name".into(),
- field_type: FieldType::Text,
- required: true,
- read_only: false,
- },
- EditorField {
- name: "category_code".into(),
- label: "Category Code".into(),
- field_type: FieldType::Text,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "category_description".into(),
- label: "Description".into(),
- field_type: FieldType::MultilineText,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "parent_id".into(),
- label: "Parent Category".into(),
- field_type: FieldType::Dropdown(category_options),
- required: false,
- read_only: false,
- },
- ],
- )
- }
- fn load_categories(&mut self, api_client: &ApiClient) {
- // Don't start a new load if we're already loading
- if self.is_loading {
- return;
- }
- self.is_loading = true;
- self.last_error = None;
- self.load_attempted = true;
- match get_categories(api_client, Some(200)) {
- Ok(categories) => {
- self.categories = categories;
- self.initial_load_done = true;
- log::info!(
- "Categories loaded successfully: {} items",
- self.categories.len()
- );
- }
- Err(e) => {
- let error_msg = format!("Error loading categories: {}", e);
- log::error!("{}", error_msg);
- self.last_error = Some(error_msg);
- }
- }
- self.is_loading = false;
- }
- /// Get selected category IDs for bulk operations (works with filtered view)
- fn get_selected_ids(&self) -> Vec<i64> {
- let filtered_data = self.table_renderer.prepare_json_data(&self.categories);
- let mut ids = Vec::new();
- for &row_idx in &self.table_renderer.selection.selected_rows {
- // prepared_data contains tuples of (original_index, &Value)
- if let Some((_orig_idx, category)) = filtered_data.get(row_idx) {
- if let Some(id) = category.get("id").and_then(|v| v.as_i64()) {
- ids.push(id);
- }
- }
- }
- ids
- }
- /// Sanitize form data for categories before sending to the API.
- /// - Removes internal editor fields prefixed with __editor_
- /// - Converts empty-string parent_id to JSON null
- /// - Coerces numeric parent_id strings to numbers
- fn sanitize_category_map(
- form_data: &serde_json::Map<String, Value>,
- ) -> serde_json::Map<String, Value> {
- let mut out = serde_json::Map::new();
- for (k, v) in form_data.iter() {
- // Skip internal editor fields
- if k.starts_with("__editor_") {
- continue;
- }
- if k == "parent_id" {
- // parent_id might be sent as "" for None. Convert to null.
- if v.is_null() {
- out.insert(k.clone(), Value::Null);
- continue;
- }
- if let Some(s) = v.as_str() {
- let s_trim = s.trim();
- if s_trim.is_empty() {
- out.insert(k.clone(), Value::Null);
- continue;
- }
- // Try parse integer
- if let Ok(n) = s_trim.parse::<i64>() {
- out.insert(k.clone(), Value::Number((n).into()));
- continue;
- }
- // Fallback: keep as string
- out.insert(k.clone(), Value::String(s_trim.to_string()));
- continue;
- }
- // If it's already a number, keep it
- if v.is_i64() || v.is_u64() || v.is_f64() {
- out.insert(k.clone(), v.clone());
- continue;
- }
- // Anything else -> keep as-is
- out.insert(k.clone(), v.clone());
- continue;
- }
- // For everything else, just copy through
- out.insert(k.clone(), v.clone());
- }
- out
- }
- fn create_category(
- &mut self,
- api_client: &ApiClient,
- form_data: &serde_json::Map<String, Value>,
- ) {
- // Sanitize and coerce form data (convert empty parent_id -> null, remove internal fields)
- let sanitized = Self::sanitize_category_map(form_data);
- let values = serde_json::Value::Object(sanitized);
- match api_client.insert("categories", values) {
- Ok(resp) if resp.success => {
- log::info!("Category created successfully");
- self.load_categories(api_client); // Reload to get fresh data
- }
- Ok(resp) => {
- let error_msg = format!("Create failed: {:?}", resp.error);
- log::error!("{}", error_msg);
- self.last_error = Some(error_msg);
- }
- Err(e) => {
- let error_msg = format!("Create error: {}", e);
- log::error!("{}", error_msg);
- self.last_error = Some(error_msg);
- }
- }
- }
- fn update_category(
- &mut self,
- api_client: &ApiClient,
- category_id: i64,
- form_data: &serde_json::Map<String, Value>,
- ) {
- // Sanitize form data (remove internal fields, coerce parent_id). Also ensure we don't send id.
- let mut filtered_data = Self::sanitize_category_map(form_data);
- filtered_data.remove("id");
- // Convert form data to JSON object
- let values = serde_json::Value::Object(filtered_data);
- let where_clause = serde_json::json!({"id": category_id});
- match api_client.update("categories", values, where_clause) {
- Ok(resp) if resp.success => {
- log::info!("Category updated successfully");
- self.load_categories(api_client); // Reload to get fresh data
- }
- Ok(resp) => {
- let error_msg = format!("Update failed: {:?}", resp.error);
- log::error!("{}", error_msg);
- // Check for foreign key constraint errors
- if let Some(err_str) = resp.error.as_ref() {
- if err_str.contains("foreign key constraint") {
- self.last_error = Some(
- "Cannot update category: Invalid parent category reference."
- .to_string(),
- );
- } else {
- self.last_error = Some(error_msg);
- }
- } else {
- self.last_error = Some(error_msg);
- }
- }
- Err(e) => {
- let error_msg = format!("Update error: {}", e);
- log::error!("{}", error_msg);
- self.last_error = Some(error_msg);
- }
- }
- }
- fn delete_category(&mut self, api_client: &ApiClient, category_id: i64) {
- let where_clause = serde_json::json!({"id": category_id});
- match api_client.delete("categories", where_clause) {
- Ok(resp) if resp.success => {
- log::info!("Category deleted successfully");
- self.load_categories(api_client); // Reload to get fresh data
- }
- Ok(resp) => {
- let error_msg = format!("Delete failed: {:?}", resp.error);
- log::error!("{}", error_msg);
- // Check for foreign key constraint errors and provide user-friendly message
- if let Some(err_str) = resp.error.as_ref() {
- if err_str.contains("foreign key constraint")
- || err_str.contains("Cannot delete or update a parent row")
- {
- self.last_error = Some(
- "Cannot delete category: It is being used by other categories as their parent, or by assets. \
- Please reassign dependent items first.".to_string()
- );
- } else {
- self.last_error = Some(error_msg);
- }
- } else {
- self.last_error = Some(error_msg);
- }
- }
- Err(e) => {
- let error_msg = format!("Delete error: {}", e);
- log::error!("{}", error_msg);
- // Check for foreign key constraint in error message
- let err_lower = error_msg.to_lowercase();
- if err_lower.contains("foreign key") || err_lower.contains("constraint") {
- self.last_error = Some(
- "Cannot delete category: It is being used by other categories as their parent, or by assets. \
- Please reassign dependent items first.".to_string()
- );
- } else {
- self.last_error = Some(error_msg);
- }
- }
- }
- }
- pub fn show(
- &mut self,
- ui: &mut egui::Ui,
- api_client: Option<&ApiClient>,
- ribbon: Option<&mut RibbonUI>,
- permissions: Option<&serde_json::Value>,
- ) -> Vec<String> {
- let mut flags_to_clear = Vec::new();
- // Handle context menu actions and double-click
- if let Some(item) = ui.ctx().data_mut(|d| {
- d.remove_temp::<serde_json::Value>(egui::Id::new("cat_double_click_edit"))
- }) {
- if RibbonUI::check_permission(permissions, "edit_category") {
- self.open_editor_with(&item);
- ui.ctx().request_repaint();
- }
- }
- if let Some(item) = ui.ctx().data_mut(|d| {
- d.remove_temp::<serde_json::Value>(egui::Id::new("cat_context_menu_edit"))
- }) {
- if RibbonUI::check_permission(permissions, "edit_category") {
- self.open_editor_with(&item);
- ui.ctx().request_repaint();
- }
- }
- if let Some(item) = ui.ctx().data_mut(|d| {
- d.remove_temp::<serde_json::Value>(egui::Id::new("cat_context_menu_delete"))
- }) {
- if RibbonUI::check_permission(permissions, "delete_category") {
- let name = item
- .get("category_name")
- .and_then(|v| v.as_str())
- .unwrap_or("Unknown")
- .to_string();
- let id = item.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
- self.pending_delete_ids = vec![id]; // Changed to vector
- self.delete_dialog.open(name, id.to_string());
- ui.ctx().request_repaint();
- }
- }
- // Auto-load on first show, but only try once unless user explicitly requests retry
- if !self.initial_load_done && !self.is_loading && !self.load_attempted {
- if let Some(client) = api_client {
- log::info!("Categories view never loaded, triggering initial auto-load");
- self.load_categories(client);
- }
- }
- // Extract search query and handle ribbon actions
- let search_query = if let Some(ribbon) = ribbon.as_ref() {
- let query = ribbon
- .search_texts
- .get("categories_search")
- .filter(|s| !s.trim().is_empty())
- .map(|s| s.as_str());
- // Handle ribbon actions
- if ribbon
- .checkboxes
- .get("categories_refresh")
- .copied()
- .unwrap_or(false)
- {
- if let Some(client) = api_client {
- // Reset error state and allow fresh load
- self.last_error = None;
- self.load_categories(client);
- }
- flags_to_clear.push("categories_refresh".to_string());
- }
- if ribbon
- .checkboxes
- .get("categories_add")
- .copied()
- .unwrap_or(false)
- {
- if RibbonUI::check_permission(permissions, "create_category") {
- // Create a new add dialog with current category options
- self.add_dialog = self.create_add_dialog_with_options();
- self.add_dialog.open(&serde_json::json!({})); // Open with empty data
- }
- flags_to_clear.push("categories_add".to_string());
- }
- if ribbon
- .checkboxes
- .get("categories_edit")
- .copied()
- .unwrap_or(false)
- {
- if RibbonUI::check_permission(permissions, "edit_category") {
- // Get selected category IDs
- let selected_ids = self.get_selected_ids();
- if !selected_ids.is_empty() {
- // For edit, only edit the first selected category (bulk edit of categories is complex)
- if let Some(&first_id) = selected_ids.first() {
- // Clone the category to avoid borrowing issues
- let category = self
- .categories
- .iter()
- .find(|c| c.get("id").and_then(|v| v.as_i64()) == Some(first_id))
- .cloned();
- if let Some(cat) = category {
- self.open_editor_with(&cat);
- }
- }
- } else {
- log::warn!("Edit requested but no categories selected");
- }
- }
- flags_to_clear.push("categories_edit".to_string());
- }
- if ribbon
- .checkboxes
- .get("categories_delete")
- .copied()
- .unwrap_or(false)
- {
- if RibbonUI::check_permission(permissions, "delete_category") {
- // Get selected category IDs for bulk delete
- let selected_ids = self.get_selected_ids();
- if !selected_ids.is_empty() {
- self.pending_delete_ids = selected_ids.clone();
- let count = selected_ids.len();
- // Show dialog with appropriate message for single or multiple deletes
- let message =
- if count == 1 {
- // Get the category name for single delete
- if let Some(category) = self.categories.iter().find(|c| {
- c.get("id").and_then(|v| v.as_i64()) == Some(selected_ids[0])
- }) {
- category
- .get("category_name")
- .and_then(|v| v.as_str())
- .unwrap_or("Unknown")
- .to_string()
- } else {
- "Unknown".to_string()
- }
- } else {
- format!("{} categories", count)
- };
- self.delete_dialog
- .open(message, format!("IDs: {:?}", selected_ids));
- } else {
- log::warn!("Delete requested but no categories selected");
- }
- }
- flags_to_clear.push("categories_delete".to_string());
- }
- query
- } else {
- None
- };
- // Top toolbar
- ui.horizontal(|ui| {
- ui.heading("Categories");
- if self.is_loading {
- ui.spinner();
- ui.label("Loading...");
- } else {
- ui.label(format!("{} categories", self.categories.len()));
- }
- ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
- if ui.button("➕ Add Category").clicked() {
- self.add_dialog.open_new(None);
- }
- if ui.button("Refresh").clicked() {
- if let Some(client) = api_client {
- // Reset error state and allow fresh load
- self.last_error = None;
- self.load_categories(client);
- }
- }
- });
- });
- ui.separator();
- // Error display with retry option
- if let Some(error) = &self.last_error {
- ui.colored_label(egui::Color32::RED, format!("Error: {}", error));
- ui.horizontal(|ui| {
- if ui.button("Try Again").clicked() {
- if let Some(client) = api_client {
- // Reset state and try loading again
- self.load_attempted = false;
- self.initial_load_done = false;
- self.load_categories(client);
- }
- }
- if ui.button("Clear Error").clicked() {
- self.last_error = None;
- }
- });
- ui.separator();
- }
- // Categories table
- if !self.is_loading && !self.categories.is_empty() {
- self.render_table(ui, search_query);
- } else if !self.is_loading {
- ui.centered_and_justified(|ui| {
- ui.label("No categories found. Click 'Add Category' to create one.");
- });
- }
- // Handle dialogs
- if let Some(api_client) = api_client {
- // Add dialog
- if let Some(result) = self.add_dialog.show_editor(ui.ctx()) {
- if let Some(category_data) = result {
- log::info!("Creating new category: {:?}", category_data);
- self.create_category(api_client, &category_data);
- }
- }
- // Edit dialog
- if let Some(result) = self.edit_dialog.show_editor(ui.ctx()) {
- if let Some(category_data) = result {
- // Support bulk edit: if pending_edit_ids is empty, try to get ID from dialog
- let ids_to_edit: Vec<i64> = if !self.pending_edit_ids.is_empty() {
- std::mem::take(&mut self.pending_edit_ids)
- } else {
- // Single edit from dialog - extract ID from __editor_item_id or category data
- category_data
- .get("__editor_item_id")
- .and_then(|v| v.as_str())
- .or_else(|| category_data.get("id").and_then(|v| v.as_str()))
- .and_then(|s| s.parse::<i64>().ok())
- .map(|id| vec![id])
- .unwrap_or_default()
- };
- for category_id in ids_to_edit {
- log::info!("Updating category {}: {:?}", category_id, category_data);
- self.update_category(api_client, category_id, &category_data);
- }
- }
- }
- // Delete dialog - support bulk delete
- if let Some(confirmed) = self.delete_dialog.show_dialog(ui.ctx()) {
- log::info!(
- "Delete dialog result: confirmed={}, pending_delete_ids={:?}",
- confirmed,
- self.pending_delete_ids
- );
- if confirmed && !self.pending_delete_ids.is_empty() {
- // Clone the IDs to avoid borrowing issues
- let ids_to_delete = self.pending_delete_ids.clone();
- for category_id in ids_to_delete {
- log::info!("Deleting category: {}", category_id);
- self.delete_category(api_client, category_id);
- }
- }
- self.pending_delete_ids.clear();
- }
- }
- flags_to_clear
- }
- fn render_table(&mut self, ui: &mut egui::Ui, search_query: Option<&str>) {
- // Apply search query to TableRenderer (clear if empty)
- match search_query {
- Some(query) => self.table_renderer.set_search_query(query.to_string()),
- None => self.table_renderer.set_search_query(String::new()), // Clear search when empty
- }
- // Prepare sorted/filtered data
- let prepared_data = self.table_renderer.prepare_json_data(&self.categories);
- // Create temporary event handler for deferred actions
- let mut deferred_actions = Vec::new();
- let mut event_handler = TempCategoriesEventHandler {
- deferred_actions: &mut deferred_actions,
- };
- // Render the table with TableRenderer
- self.table_renderer
- .render_json_table(ui, &prepared_data, Some(&mut event_handler));
- // Process deferred actions
- for action in deferred_actions {
- match action {
- DeferredCategoryAction::DoubleClick(category) => {
- self.open_editor_with(&category);
- }
- DeferredCategoryAction::ContextEdit(category) => {
- self.open_editor_with(&category);
- }
- DeferredCategoryAction::ContextDelete(category) => {
- let name = category
- .get("category_name")
- .and_then(|v| v.as_str())
- .unwrap_or("Unknown")
- .to_string();
- let id = category.get("id").and_then(|v| v.as_i64()).unwrap_or(-1);
- self.pending_delete_ids = vec![id]; // Changed to vector
- self.delete_dialog.open(name, id.to_string());
- }
- DeferredCategoryAction::ContextClone(category) => {
- // Prepare Add dialog with up-to-date dropdown options
- self.add_dialog = self.create_add_dialog_with_options();
- // Use the shared helper to clear ID/code and suffix the name
- let cloned = crate::core::components::prepare_cloned_value(
- &category,
- &["id", "category_code"],
- Some("category_name"),
- Some(""),
- );
- self.add_dialog.title = "Add Category".to_string();
- self.add_dialog.open(&cloned);
- }
- }
- }
- }
- fn create_category_dropdown_options(&self, exclude_id: Option<i64>) -> Vec<(String, String)> {
- let mut options = vec![("".to_string(), "None (Root Category)".to_string())];
- for category in &self.categories {
- if let Some(id) = category.get("id").and_then(|v| v.as_i64()) {
- // Exclude the current category to prevent circular references
- if let Some(exclude) = exclude_id {
- if id == exclude {
- continue;
- }
- }
- let name = category
- .get("category_name")
- .and_then(|v| v.as_str())
- .unwrap_or("Unknown")
- .to_string();
- let code = category
- .get("category_code")
- .and_then(|v| v.as_str())
- .unwrap_or("");
- let display_name = if code.is_empty() {
- name
- } else {
- format!("{} - {}", code, name)
- };
- options.push((id.to_string(), display_name));
- }
- }
- options
- }
- fn create_edit_dialog_with_options(&self, exclude_id: Option<i64>) -> FormBuilder {
- let category_options = self.create_category_dropdown_options(exclude_id);
- FormBuilder::new(
- "Edit Category",
- vec![
- EditorField {
- name: "id".into(),
- label: "ID".into(),
- field_type: FieldType::Text,
- required: false,
- read_only: true,
- },
- EditorField {
- name: "category_name".into(),
- label: "Category Name".into(),
- field_type: FieldType::Text,
- required: true,
- read_only: false,
- },
- EditorField {
- name: "category_code".into(),
- label: "Category Code".into(),
- field_type: FieldType::Text,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "category_description".into(),
- label: "Description".into(),
- field_type: FieldType::MultilineText,
- required: false,
- read_only: false,
- },
- EditorField {
- name: "parent_id".into(),
- label: "Parent Category".into(),
- field_type: FieldType::Dropdown(category_options),
- required: false,
- read_only: false,
- },
- ],
- )
- }
- fn open_editor_with(&mut self, item: &serde_json::Value) {
- let category_id = item.get("id").and_then(|v| v.as_i64());
- // Clear pending_edit_ids since we're opening a single-item editor
- // The ID will be extracted from the dialog data when saving
- self.pending_edit_ids.clear();
- // Create a new editor with current category options (excluding this category)
- self.edit_dialog = self.create_edit_dialog_with_options(category_id);
- self.edit_dialog.open(item);
- }
- }
- // Deferred actions for categories table
- #[derive(Debug)]
- enum DeferredCategoryAction {
- DoubleClick(Value),
- ContextEdit(Value),
- ContextDelete(Value),
- ContextClone(Value),
- }
- // Temporary event handler that collects actions for later processing
- struct TempCategoriesEventHandler<'a> {
- deferred_actions: &'a mut Vec<DeferredCategoryAction>,
- }
- impl<'a> TableEventHandler<Value> for TempCategoriesEventHandler<'a> {
- fn on_double_click(&mut self, item: &Value, _row_index: usize) {
- log::info!(
- "Double-click detected on category: {:?}",
- item.get("category_name")
- );
- self.deferred_actions
- .push(DeferredCategoryAction::DoubleClick(item.clone()));
- }
- fn on_context_menu(&mut self, ui: &mut egui::Ui, item: &Value, _row_index: usize) {
- if ui
- .button(format!("{} Edit", egui_phosphor::regular::PENCIL))
- .clicked()
- {
- log::info!(
- "Context menu edit clicked for category: {:?}",
- item.get("category_name")
- );
- self.deferred_actions
- .push(DeferredCategoryAction::ContextEdit(item.clone()));
- ui.close();
- }
- ui.separator();
- if ui
- .button(format!("{} Clone Category", egui_phosphor::regular::COPY))
- .clicked()
- {
- log::info!(
- "Context menu clone clicked for category: {:?}",
- item.get("category_name")
- );
- self.deferred_actions
- .push(DeferredCategoryAction::ContextClone(item.clone()));
- ui.close();
- }
- ui.separator();
- if ui
- .button(format!("{} Delete", egui_phosphor::regular::TRASH))
- .clicked()
- {
- log::info!(
- "Context menu delete clicked for category: {:?}",
- item.get("category_name")
- );
- self.deferred_actions
- .push(DeferredCategoryAction::ContextDelete(item.clone()));
- ui.close();
- }
- }
- fn on_selection_changed(&mut self, _selected_indices: &[usize]) {
- // Selection handling is managed by the main CategoriesView
- // We don't need to do anything here for now
- }
- }
|