feat(ui): bring ALIASEDIT to shortcut-editor parity

Port press-to-capture dialog safeguards to the alias editor: transient top draft row with explicit accept, duplicate-alias and unknown-command danger styling plus persistent banners, reset-to-defaults with inline confirmation, Apply and Apply-and-Exit, alias count, Esc-cancels-draft-first, and unsaved-changes guard on close.
This commit is contained in:
Karim Jerbi committed 2026-09-16 09:32:06 +01:00
1 parent 457f4b3dbb
commit 1057790456
5 files changed
+513 -23

No files matched your search

+86 -1
View File
@@ -164,6 +164,13 @@ fn default_map() -> FxHashMap<String, String> {
parse_pgp(DEFAULT_ALIASES_PGP)
}
/// Sorted factory-default rows for the ALIASEDIT reset action.
pub(super) fn default_alias_rows() -> Vec<(String, String)> {
let mut rows: Vec<(String, String)> = default_map().into_iter().collect();
rows.sort_by(|a, b| a.0.cmp(&b.0));
rows
}
/// Load the alias table at boot. Native reads `ocad.pgp`; web reads the same
/// text from `localStorage`. Missing or unavailable storage falls back to the
/// embedded defaults.
@@ -315,10 +322,88 @@ impl OpenCADStudio {
.alias_editor_rows
.iter()
.filter(|(a, c)| !a.trim().is_empty() && !c.trim().is_empty())
.map(|(a, c)| (a.trim().to_string(), c.trim().to_string()))
.map(|(a, c)| {
(
a.trim().to_uppercase(),
c.trim().to_uppercase(),
)
})
.collect();
self.set_command_aliases(map);
}
/// Commit the working rows, discarding an unfinished draft and reporting
/// duplicates. Shared by Apply and Apply-and-Exit. Mirrors
/// `finish_shortcut_editor`.
pub(super) fn finish_alias_editor(&mut self) {
if self.alias_pending_add && !self.alias_editor_rows.is_empty() {
self.alias_editor_rows.remove(0);
self.alias_pending_add = false;
self.command_line
.push_error(crate::tf!("Incomplete alias row discarded.").as_ref());
}
let mut seen = rustc_hash::FxHashSet::default();
let mut duplicates = Vec::new();
for (alias, _) in &self.alias_editor_rows {
let alias = alias.trim().to_uppercase();
if alias.is_empty() {
continue;
}
if !seen.insert(alias.clone()) {
duplicates.push(alias);
}
}
if !duplicates.is_empty() {
self.command_line.push_error(
crate::tf!(
"Duplicate alias(es) ignored on Apply: {}",
duplicates.join(", ")
)
.as_ref(),
);
}
self.apply_alias_editor_rows();
self.command_line.push_info(
crate::tf!("{} alias(es) applied.", self.command_aliases.len()).as_ref(),
);
}
/// Reset the working rows and live table to the shipped defaults.
pub(super) fn reset_aliases_to_defaults(&mut self) {
self.alias_editor_rows = default_alias_rows();
self.alias_pending_add = false;
self.alias_reset_confirm = false;
self.apply_alias_editor_rows();
}
/// True when the working rows differ from the live table — the editor
/// has un-applied changes a close would discard.
pub(super) fn alias_editor_dirty(&self) -> bool {
let rows: FxHashMap<String, String> = self
.alias_editor_rows
.iter()
.filter_map(|(alias, command)| {
let alias = alias.trim().to_uppercase();
let command = command.trim().to_uppercase();
(!alias.is_empty() && !command.is_empty()).then_some((alias, command))
})
.collect();
rows != self.command_aliases
}
/// A pending add finishes once its draft row (row 0) has both an alias
/// and a command; it then becomes a regular row. Called by the draft
/// row's check button — typing alone never finishes it.
pub(super) fn finish_pending_alias_add(&mut self) {
if !self.alias_pending_add {
return;
}
if let Some((alias, command)) = self.alias_editor_rows.first() {
if !alias.trim().is_empty() && !command.trim().is_empty() {
self.alias_pending_add = false;
}
}
}
}
#[cfg(test)]