aboutsummaryrefslogtreecommitdiff
path: root/src/ui/suppliers.rs
diff options
context:
space:
mode:
authorUMTS at Teleco <crt@teleco.ch>2025-12-13 02:51:15 +0100
committerUMTS at Teleco <crt@teleco.ch>2025-12-13 02:51:15 +0100
commit8323fdd73272a2882781aba3c499ba0be3dff2a6 (patch)
treeffbf86473933e69cfaeef30d5c6ea7e5b494856c /src/ui/suppliers.rs
committing to insanityHEADmaster
Diffstat (limited to 'src/ui/suppliers.rs')
-rw-r--r--src/ui/suppliers.rs802
1 files changed, 802 insertions, 0 deletions
diff --git a/src/ui/suppliers.rs b/src/ui/suppliers.rs
new file mode 100644
index 0000000..ce7679f
--- /dev/null
+++ b/src/ui/suppliers.rs
@@ -0,0 +1,802 @@
+use eframe::egui;
+use std::collections::HashSet;
+
+use crate::api::ApiClient;
+use crate::core::components::form_builder::FormBuilder;
+use crate::core::components::interactions::ConfirmDialog;
+use crate::core::counters::count_entities;
+use crate::core::tables::get_suppliers;
+use crate::core::{EditorField, FieldType};
+use crate::ui::ribbon::RibbonUI;
+
+#[derive(Clone)]
+struct ColumnConfig {
+ name: String,
+ field: String,
+ visible: bool,
+}
+
+pub struct SuppliersView {
+ rows: Vec<serde_json::Value>,
+ display_rows: Vec<serde_json::Value>,
+ is_loading: bool,
+ last_error: Option<String>,
+ init_loaded: bool,
+ // Columns & selector
+ columns: Vec<ColumnConfig>,
+ show_column_panel: bool,
+ // Selection & interactions
+ selected_row: Option<usize>,
+ last_click_time: Option<std::time::Instant>,
+ last_click_row: Option<usize>,
+ selected_rows: HashSet<usize>,
+ selection_anchor: Option<usize>,
+ // Dialogs
+ delete_dialog: ConfirmDialog,
+ edit_dialog: FormBuilder,
+ add_dialog: FormBuilder,
+ // Track ids for operations
+ edit_current_id: Option<i64>,
+ delete_current_id: Option<i64>,
+}
+
+impl SuppliersView {
+ pub fn new() -> Self {
+ let columns = vec![
+ ColumnConfig {
+ name: "Name".into(),
+ field: "name".into(),
+ visible: true,
+ },
+ ColumnConfig {
+ name: "Contact".into(),
+ field: "contact".into(),
+ visible: true,
+ },
+ ColumnConfig {
+ name: "Email".into(),
+ field: "email".into(),
+ visible: true,
+ },
+ ColumnConfig {
+ name: "Phone".into(),
+ field: "phone".into(),
+ visible: true,
+ },
+ ColumnConfig {
+ name: "Website".into(),
+ field: "website".into(),
+ visible: true,
+ },
+ ColumnConfig {
+ name: "Items".into(),
+ field: "items_count".into(),
+ visible: true,
+ },
+ // Hidden by default
+ ColumnConfig {
+ name: "Notes".into(),
+ field: "notes".into(),
+ visible: false,
+ },
+ ColumnConfig {
+ name: "Created".into(),
+ field: "created_at".into(),
+ visible: false,
+ },
+ ColumnConfig {
+ name: "ID".into(),
+ field: "id".into(),
+ visible: false,
+ },
+ ];
+ Self {
+ rows: vec![],
+ display_rows: vec![],
+ is_loading: false,
+ last_error: None,
+ init_loaded: false,
+ columns,
+ show_column_panel: false,
+ selected_row: None,
+ last_click_time: None,
+ last_click_row: None,
+ selected_rows: HashSet::new(),
+ selection_anchor: None,
+ delete_dialog: ConfirmDialog::new(
+ "Delete Supplier",
+ "Are you sure you want to delete this supplier?",
+ ),
+ edit_dialog: FormBuilder::new(
+ "Edit Supplier",
+ vec![
+ EditorField {
+ name: "name".into(),
+ label: "Name".into(),
+ field_type: FieldType::Text,
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "contact".into(),
+ label: "Contact".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "email".into(),
+ label: "Email".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "phone".into(),
+ label: "Phone".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "website".into(),
+ label: "Website".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "notes".into(),
+ label: "Notes".into(),
+ field_type: FieldType::MultilineText,
+ required: false,
+ read_only: false,
+ },
+ ],
+ ),
+ add_dialog: FormBuilder::new(
+ "Add Supplier",
+ vec![
+ EditorField {
+ name: "name".into(),
+ label: "Name".into(),
+ field_type: FieldType::Text,
+ required: true,
+ read_only: false,
+ },
+ EditorField {
+ name: "contact".into(),
+ label: "Contact".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "email".into(),
+ label: "Email".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "phone".into(),
+ label: "Phone".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "website".into(),
+ label: "Website".into(),
+ field_type: FieldType::Text,
+ required: false,
+ read_only: false,
+ },
+ EditorField {
+ name: "notes".into(),
+ label: "Notes".into(),
+ field_type: FieldType::MultilineText,
+ required: false,
+ read_only: false,
+ },
+ ],
+ ),
+ edit_current_id: None,
+ delete_current_id: None,
+ }
+ }
+
+ fn load(&mut self, api: &ApiClient) {
+ if self.is_loading {
+ return;
+ }
+ self.is_loading = true;
+ self.last_error = None;
+ match get_suppliers(api, Some(200)) {
+ Ok(mut list) => {
+ // Compute items_count per supplier
+ for row in &mut list {
+ if let Some(id) = row.get("id").and_then(|v| v.as_i64()) {
+ let where_clause = serde_json::json!({"supplier_id": id});
+ let count = count_entities(api, "assets", Some(where_clause)).unwrap_or(0);
+ row.as_object_mut().map(|o| {
+ o.insert("items_count".into(), serde_json::json!(count));
+ });
+ }
+ }
+ self.rows = list;
+ self.display_rows = self.rows.clone();
+ }
+ Err(e) => self.last_error = Some(e.to_string()),
+ }
+ self.is_loading = false;
+ self.init_loaded = true;
+ }
+
+ pub fn show(
+ &mut self,
+ ui: &mut egui::Ui,
+ api_client: Option<&ApiClient>,
+ ribbon: Option<&mut RibbonUI>,
+ ) -> Vec<String> {
+ let mut flags_to_clear = Vec::new();
+
+ // Apply simple search from ribbon (by name/code/contact/email/phone/website)
+ if let Some(ribbon) = ribbon.as_ref() {
+ let term = ribbon
+ .search_texts
+ .get("supplier_search")
+ .cloned()
+ .unwrap_or_default();
+ let tl = term.to_lowercase();
+ if tl.is_empty() {
+ self.display_rows = self.rows.clone();
+ } else {
+ self.display_rows = self
+ .rows
+ .iter()
+ .filter(|row| {
+ let fields = ["name", "contact", "email", "phone", "website"];
+ fields.iter().any(|f| {
+ row.get(*f)
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_lowercase().contains(&tl))
+ .unwrap_or(false)
+ })
+ })
+ .cloned()
+ .collect();
+ }
+ }
+
+ if let Some(ribbon) = ribbon.as_ref() {
+ // Handle ribbon actions before rendering
+ if ribbon
+ .checkboxes
+ .get("suppliers_action_new")
+ .copied()
+ .unwrap_or(false)
+ {
+ self.add_dialog.open_new(None);
+ flags_to_clear.push("suppliers_action_new".to_string());
+ }
+
+ if ribbon
+ .checkboxes
+ .get("suppliers_action_edit")
+ .copied()
+ .unwrap_or(false)
+ {
+ if let Some(selected) = self.first_selected_supplier() {
+ self.open_editor_with(&selected);
+ } else {
+ log::warn!("Ribbon edit triggered but no supplier selected");
+ }
+ flags_to_clear.push("suppliers_action_edit".to_string());
+ }
+
+ if ribbon
+ .checkboxes
+ .get("suppliers_action_delete")
+ .copied()
+ .unwrap_or(false)
+ {
+ if let Some((name, id)) = self.first_selected_supplier_id_name() {
+ self.delete_current_id = Some(id);
+ self.delete_dialog.open(name, id.to_string());
+ } else {
+ log::warn!("Ribbon delete triggered but no supplier selected");
+ }
+ flags_to_clear.push("suppliers_action_delete".to_string());
+ }
+ }
+
+ ui.horizontal(|ui| {
+ ui.heading("Suppliers");
+ if self.is_loading {
+ ui.spinner();
+ ui.label("Loading...");
+ }
+ if let Some(err) = &self.last_error {
+ ui.colored_label(egui::Color32::RED, err);
+ if ui.button("Refresh").clicked() {
+ if let Some(api) = api_client {
+ self.load(api);
+ }
+ }
+ } else if ui.button("Refresh").clicked() {
+ if let Some(api) = api_client {
+ self.load(api);
+ }
+ }
+
+ ui.separator();
+ let button_text = if self.show_column_panel {
+ "Hide Column Selector"
+ } else {
+ "Show Column Selector"
+ };
+ if ui.button(button_text).clicked() {
+ self.show_column_panel = !self.show_column_panel;
+ }
+ });
+ ui.separator();
+
+ if !self.init_loaded {
+ if let Some(api) = api_client {
+ self.load(api);
+ }
+ }
+
+ // Column selector window
+ if self.show_column_panel {
+ let ctx = ui.ctx();
+ let screen_rect = ctx.input(|i| {
+ i.viewport().inner_rect.unwrap_or(egui::Rect::from_min_max(
+ egui::pos2(0.0, 0.0),
+ egui::pos2(1920.0, 1080.0),
+ ))
+ });
+ let max_w = (screen_rect.width() - 20.0).max(220.0);
+ let max_h = (screen_rect.height() - 100.0).max(200.0);
+
+ egui::Window::new("Column Selector")
+ .collapsible(false)
+ .resizable(true)
+ .default_width(260.0)
+ .default_height(360.0)
+ .anchor(egui::Align2::RIGHT_TOP, [-10.0, 90.0])
+ .open(&mut self.show_column_panel)
+ .min_size(egui::vec2(220.0, 200.0))
+ .max_size(egui::vec2(max_w, max_h))
+ .frame(egui::Frame {
+ fill: egui::Color32::from_rgb(30, 30, 30),
+ stroke: egui::Stroke::new(1.0, egui::Color32::from_gray(60)),
+ inner_margin: egui::Margin::from(10.0),
+ outer_margin: egui::Margin::ZERO,
+ corner_radius: 6.0.into(),
+ shadow: egui::epaint::Shadow::NONE,
+ })
+ .show(ctx, |ui| {
+ ui.heading("Columns");
+ ui.separator();
+ ui.add_space(8.0);
+ egui::ScrollArea::vertical()
+ .auto_shrink([false; 2])
+ .show(ui, |ui| {
+ for column in &mut self.columns {
+ ui.checkbox(&mut column.visible, &column.name);
+ }
+ });
+ ui.add_space(8.0);
+ ui.separator();
+ ui.horizontal(|ui| {
+ if ui.button("Show All").clicked() {
+ for col in &mut self.columns {
+ col.visible = true;
+ }
+ }
+ if ui.button("Hide All").clicked() {
+ for col in &mut self.columns {
+ col.visible = false;
+ }
+ }
+ });
+ });
+ }
+
+ // compute visible columns and render table
+ let visible_columns: Vec<ColumnConfig> =
+ self.columns.iter().filter(|c| c.visible).cloned().collect();
+ self.render_table(ui, &visible_columns);
+
+ // Process selection and dialog events
+ let ctx = ui.ctx();
+ // selection handled inline via checkbox/row clicks
+ if let Some(row_idx) =
+ ctx.data_mut(|d| d.remove_temp::<usize>(egui::Id::new("sup_double_click_idx")))
+ {
+ self.selected_row = Some(row_idx);
+ self.last_click_row = None;
+ self.last_click_time = None;
+ }
+ if let Some(item) = ctx.data_mut(|d| {
+ d.remove_temp::<serde_json::Value>(egui::Id::new("sup_double_click_edit"))
+ }) {
+ self.open_editor_with(&item);
+ ctx.request_repaint();
+ }
+ if let Some(item) = ctx.data_mut(|d| {
+ d.remove_temp::<serde_json::Value>(egui::Id::new("sup_context_menu_edit"))
+ }) {
+ self.open_editor_with(&item);
+ ctx.request_repaint();
+ }
+ if let Some(item) = ctx.data_mut(|d| {
+ d.remove_temp::<serde_json::Value>(egui::Id::new("sup_context_menu_clone"))
+ }) {
+ // Prepare cloned payload: clear id, suffix name, keep other fields
+ let cloned = crate::core::components::prepare_cloned_value(
+ &item,
+ &["id"],
+ Some("name"),
+ Some(""),
+ );
+ if let Some(obj) = cloned.as_object() {
+ self.add_dialog.open_new(Some(obj));
+ }
+ ctx.request_repaint();
+ }
+ if let Some(item) = ctx.data_mut(|d| {
+ d.remove_temp::<serde_json::Value>(egui::Id::new("sup_context_menu_delete"))
+ }) {
+ let name = item
+ .get("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.delete_current_id = Some(id);
+ self.delete_dialog.open(name, id.to_string());
+ ctx.request_repaint();
+ }
+
+ // Handle delete confirmation dialog
+ if let Some(confirmed) = self.delete_dialog.show_dialog(ctx) {
+ if confirmed {
+ if let (Some(api), Some(id)) = (api_client, self.delete_current_id) {
+ let where_clause = serde_json::json!({"id": id});
+ match api.delete("suppliers", where_clause) {
+ Ok(resp) if resp.success => {
+ self.load(api);
+ }
+ Ok(resp) => {
+ self.last_error = Some(format!("Delete failed: {:?}", resp.error));
+ }
+ Err(e) => {
+ self.last_error = Some(format!("Delete error: {}", e));
+ }
+ }
+ }
+ }
+ }
+
+ // Handle edit dialog save
+ if let Some(result) = self.edit_dialog.show_editor(ctx) {
+ if let Some(updated) = result {
+ if let (Some(api), Some(id)) = (api_client, self.edit_current_id) {
+ let mut updated = updated;
+ strip_editor_metadata(&mut updated);
+ let values = serde_json::Value::Object(updated);
+ let where_clause = serde_json::json!({"id": id});
+ match api.update("suppliers", values, where_clause) {
+ Ok(resp) if resp.success => {
+ self.load(api);
+ }
+ Ok(resp) => {
+ self.last_error = Some(format!("Update failed: {:?}", resp.error));
+ }
+ Err(e) => {
+ self.last_error = Some(format!("Update error: {}", e));
+ }
+ }
+ }
+ }
+ }
+
+ // Handle add dialog (clone -> insert)
+ self.handle_add_dialog(ui.ctx(), api_client);
+
+ flags_to_clear
+ }
+
+ fn render_table(&mut self, ui: &mut egui::Ui, visible_columns: &Vec<ColumnConfig>) {
+ use egui_extras::{Column, TableBuilder};
+ let mut table = TableBuilder::new(ui)
+ .striped(true)
+ .resizable(true)
+ .cell_layout(egui::Layout::left_to_right(egui::Align::Center));
+ // Add selection checkbox column first
+ table = table.column(Column::initial(28.0));
+ for _ in 0..visible_columns.len() {
+ table = table.column(Column::remainder());
+ }
+ table
+ .header(22.0, |mut header| {
+ // Select-all checkbox
+ header.col(|ui| {
+ let all_selected = self
+ .display_rows
+ .iter()
+ .enumerate()
+ .all(|(i, _)| self.selected_rows.contains(&i));
+ let mut chk = all_selected;
+ if ui
+ .checkbox(&mut chk, "")
+ .on_hover_text("Select All")
+ .clicked()
+ {
+ if chk {
+ self.selected_rows = (0..self.display_rows.len()).collect();
+ } else {
+ self.selected_rows.clear();
+ }
+ }
+ });
+ for col in visible_columns {
+ header.col(|ui| {
+ ui.strong(&col.name);
+ });
+ }
+ })
+ .body(|mut body| {
+ for (idx, r) in self.display_rows.iter().enumerate() {
+ let r_clone = r.clone();
+ let is_selected = self.selected_rows.contains(&idx);
+ body.row(20.0, |mut row| {
+ if is_selected {
+ row.set_selected(true);
+ }
+ // Checkbox column
+ row.col(|ui| {
+ let mut checked = self.selected_rows.contains(&idx);
+ let resp = ui.checkbox(&mut checked, "");
+ if resp.changed() {
+ let mods = ui.input(|i| i.modifiers);
+ if mods.shift {
+ let anchor = self.selection_anchor.unwrap_or(idx);
+ let (a, b) = if anchor <= idx {
+ (anchor, idx)
+ } else {
+ (idx, anchor)
+ };
+ for i in a..=b {
+ if checked {
+ self.selected_rows.insert(i);
+ } else {
+ self.selected_rows.remove(&i);
+ }
+ }
+ } else if mods.command || mods.ctrl {
+ if checked {
+ self.selected_rows.insert(idx);
+ } else {
+ self.selected_rows.remove(&idx);
+ }
+ self.selection_anchor = Some(idx);
+ } else {
+ self.selected_rows.clear();
+ if checked {
+ self.selected_rows.insert(idx);
+ self.selection_anchor = Some(idx);
+ }
+ }
+ }
+ });
+ let mut combined_cell_response: Option<egui::Response> = None;
+ for col in visible_columns {
+ row.col(|ui| {
+ let resp = render_supplier_cell(ui, &r_clone, &col.field);
+ combined_cell_response =
+ Some(match combined_cell_response.take() {
+ Some(prev) => prev.union(resp),
+ None => resp,
+ });
+ });
+ }
+ let mut row_resp = row.response();
+ if let Some(cell_r) = combined_cell_response {
+ row_resp = row_resp.union(cell_r);
+ }
+ if row_resp.clicked() {
+ let now = std::time::Instant::now();
+ let is_double_click = if let (Some(last_time), Some(last_row)) =
+ (self.last_click_time, self.last_click_row)
+ {
+ last_row == idx && now.duration_since(last_time).as_millis() < 500
+ } else {
+ false
+ };
+ if is_double_click {
+ row_resp.ctx.data_mut(|d| {
+ d.insert_temp(egui::Id::new("sup_double_click_idx"), idx);
+ d.insert_temp(
+ egui::Id::new("sup_double_click_edit"),
+ r_clone.clone(),
+ );
+ });
+ } else {
+ // Multi-select on row click
+ let mods = row_resp.ctx.input(|i| i.modifiers);
+ if mods.shift {
+ let anchor = self.selection_anchor.unwrap_or(idx);
+ let (a, b) = if anchor <= idx {
+ (anchor, idx)
+ } else {
+ (idx, anchor)
+ };
+ for i in a..=b {
+ self.selected_rows.insert(i);
+ }
+ } else if mods.command || mods.ctrl {
+ if self.selected_rows.contains(&idx) {
+ self.selected_rows.remove(&idx);
+ } else {
+ self.selected_rows.insert(idx);
+ }
+ self.selection_anchor = Some(idx);
+ } else {
+ self.selected_rows.clear();
+ self.selected_rows.insert(idx);
+ self.selection_anchor = Some(idx);
+ }
+ self.last_click_time = Some(now);
+ self.last_click_row = Some(idx);
+ }
+ }
+ row_resp.context_menu(|ui| {
+ if ui
+ .button(format!("{} Edit", egui_phosphor::regular::PENCIL))
+ .clicked()
+ {
+ ui.ctx().data_mut(|d| {
+ d.insert_temp(
+ egui::Id::new("sup_context_menu_edit"),
+ r_clone.clone(),
+ )
+ });
+ ui.close();
+ }
+ if ui
+ .button(format!("{} Clone", egui_phosphor::regular::COPY))
+ .clicked()
+ {
+ ui.ctx().data_mut(|d| {
+ d.insert_temp(
+ egui::Id::new("sup_context_menu_clone"),
+ r_clone.clone(),
+ )
+ });
+ ui.close();
+ }
+ ui.separator();
+ if ui
+ .button(format!("{} Delete", egui_phosphor::regular::TRASH))
+ .clicked()
+ {
+ ui.ctx().data_mut(|d| {
+ d.insert_temp(
+ egui::Id::new("sup_context_menu_delete"),
+ r_clone.clone(),
+ )
+ });
+ ui.close();
+ }
+ });
+ });
+ }
+ });
+ }
+
+ fn open_editor_with(&mut self, item: &serde_json::Value) {
+ self.edit_current_id = item.get("id").and_then(|v| v.as_i64());
+ self.edit_dialog.open(item);
+ }
+
+ fn first_selected_supplier(&self) -> Option<serde_json::Value> {
+ self.first_selected_index()
+ .and_then(|idx| self.display_rows.get(idx).cloned())
+ }
+
+ fn first_selected_supplier_id_name(&self) -> Option<(String, i64)> {
+ self.first_selected_index().and_then(|idx| {
+ self.display_rows.get(idx).and_then(|row| {
+ let name = row
+ .get("name")
+ .and_then(|v| v.as_str())
+ .unwrap_or("Unknown")
+ .to_string();
+ let id = row.get("id").and_then(|v| v.as_i64())?;
+ Some((name, id))
+ })
+ })
+ }
+
+ fn first_selected_index(&self) -> Option<usize> {
+ if self.selected_rows.is_empty() {
+ None
+ } else {
+ self.selected_rows.iter().copied().min()
+ }
+ }
+}
+
+fn label_trunc(ui: &mut egui::Ui, text: &str, max: usize) -> egui::Response {
+ if text.len() > max {
+ let short = format!("{}…", &text[..max]);
+ ui.label(short).on_hover_ui(|ui| {
+ ui.label(text);
+ })
+ } else {
+ ui.label(text)
+ }
+}
+
+fn render_supplier_cell(ui: &mut egui::Ui, row: &serde_json::Value, field: &str) -> egui::Response {
+ let t = |k: &str| row.get(k).and_then(|v| v.as_str()).unwrap_or("");
+ match field {
+ "id" => ui.label(
+ row.get("id")
+ .and_then(|v| v.as_i64())
+ .unwrap_or(0)
+ .to_string(),
+ ),
+ "name" => ui.label(t("name")),
+ "contact" => ui.label(t("contact")),
+ "email" => ui.label(t("email")),
+ "phone" => ui.label(t("phone")),
+ "website" => ui.label(t("website")),
+ "notes" => label_trunc(ui, t("notes"), 60),
+ "created_at" => ui.label(t("created_at")),
+ "items_count" => ui.label(
+ row.get("items_count")
+ .and_then(|v| v.as_i64())
+ .unwrap_or(0)
+ .to_string(),
+ ),
+ other => ui.label(format!("{}", other)),
+ }
+}
+
+// Handle add dialog save and insert
+impl SuppliersView {
+ fn handle_add_dialog(&mut self, ctx: &egui::Context, api_client: Option<&ApiClient>) {
+ if let Some(Some(mut new_data)) = self.add_dialog.show_editor(ctx) {
+ if let Some(api) = api_client {
+ strip_editor_metadata(&mut new_data);
+ match api.insert("suppliers", serde_json::Value::Object(new_data)) {
+ Ok(resp) if resp.success => {
+ self.load(api);
+ }
+ Ok(resp) => {
+ self.last_error = Some(format!("Insert failed: {:?}", resp.error));
+ }
+ Err(e) => {
+ self.last_error = Some(format!("Insert error: {}", e));
+ }
+ }
+ }
+ }
+ }
+}
+
+fn strip_editor_metadata(map: &mut serde_json::Map<String, serde_json::Value>) {
+ let meta_keys: Vec<String> = map
+ .keys()
+ .filter(|k| k.starts_with("__editor_"))
+ .cloned()
+ .collect();
+ for key in meta_keys {
+ map.remove(&key);
+ }
+}