♻️ Fixes on shortcuts (#10906)

* 🎉 Improve reset to default flow

* 🎉 Add diff modal

* 🐛 Fix measurement shortcuts

* 🎉 Separate open-sections for each tab

* 🐛 Align button link icon

* 🎉 Reset only works after press save

* 🐛 Fix little things

* 🎉 Make the import export row to be fixed

* ♻️ Fix CI

* 🐛 Cancel shortcut is not appearing on disabled tab

* 🐛 Fix tests

* 🐛 Fix loop on personalized

* 🐛 Fix show measurements shortcut
This commit is contained in:
Eva Marco 2026-07-30 16:56:10 +02:00 committed by GitHub
parent 370e91c3e3
commit fe85a5717e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 695 additions and 123 deletions

View File

@ -305,6 +305,13 @@ export class ShortcutsPage extends BaseWebSocketPage {
});
}
async confirmImportApply() {
const applyButton = this.page.getByRole("button", {
name: /apply/i,
});
await applyButton.click();
}
async getExportedJson() {
const [download] = await Promise.all([
this.page.waitForEvent("download"),

View File

@ -77,10 +77,12 @@ test.describe("Shortcut Import", () => {
workspace: { "align-bottom": "ctrl+y" },
});
await shortcutsPage.confirmImportApply();
await shortcutsPage.expectShortcutCustomized("Align bottom");
await shortcutsPage.searchForShortcut("Align bottom");
await shortcutsPage.clickTab("Personalized");
await shortcutsPage.searchForShortcut("Align bottom");
await shortcutsPage.expectShortcutVisible("Align bottom");
});
@ -116,6 +118,8 @@ test.describe("Shortcut Import", () => {
dashboard: { "toggle-theme": "alt+m" },
});
await shortcutsPage.confirmImportApply();
await expect(
page.getByRole("alert").filter({ hasText: /Invalid data/i }),
).not.toBeVisible();
@ -144,6 +148,8 @@ test.describe("Shortcut Import", () => {
},
});
await shortcutsPage.confirmImportApply();
await shortcutsPage.expectShortcutCustomized("Align bottom");
await shortcutsPage.expandSubsection("Alignment");
@ -183,6 +189,8 @@ test.describe("Shortcut Export", () => {
workspace: { "align-bottom": "" },
});
await shortcutsPage.confirmImportApply();
await shortcutsPage.searchForShortcut("Align bottom");
await shortcutsPage.expectShortcutDisabled("Align bottom");
@ -228,11 +236,36 @@ test.describe("Shortcut Reset", () => {
await shortcutsPage.importShortcuts({
workspace: { "align-bottom": "ctrl+y" },
});
await shortcutsPage.confirmImportApply();
await shortcutsPage.expectShortcutCustomized("Align bottom");
await shortcutsPage.restoreAllShortcuts();
await shortcutsPage.expectShortcutNotCustomized("Align bottom");
});
test("Reset button shows pending message and requires save to apply", async ({
page,
}) => {
const shortcutsPage = new ShortcutsPage(page);
await shortcutsPage.goToShortcuts();
await shortcutsPage.expandSubsection("Alignment");
await shortcutsPage.clickEditShortcut("Align bottom");
await shortcutsPage.pressKey("Control+y");
await shortcutsPage.saveShortcut();
await shortcutsPage.expectShortcutCustomized("Align bottom");
await shortcutsPage.clickEditShortcut("Align bottom");
await shortcutsPage.resetShortcut("Align bottom");
await expect(
page.getByText(/If you save, this shortcut will return/i),
).toBeVisible();
await shortcutsPage.saveShortcut();
await shortcutsPage.expectShortcutNotCustomized("Align bottom");
});
});
test.describe("Shortcut Persistence", () => {
@ -315,6 +348,7 @@ test.describe("Shortcut Round-Trip", () => {
await shortcutsPage.expectShortcutNotCustomized("Align bottom");
await shortcutsPage.importShortcuts(exported);
await shortcutsPage.confirmImportApply();
await shortcutsPage.expectShortcutCustomized("Align bottom");
const reExported = await shortcutsPage.getExportedJson();

View File

@ -207,13 +207,16 @@
(remove #(:disabled (second %)))
(run! (fn [[key {:keys [command fn type overwrite]}]]
(let [callback (wrap-cb key fn)
undefined (js* "(void 0)")
commands (if (vector? command)
(into-array command)
#js [command])]
(if type
(mousetrap/bind commands callback type overwrite)
(mousetrap/bind commands callback undefined overwrite)))))))
(if (vector? type)
(do (mousetrap/bind commands callback (nth type 0) overwrite)
(mousetrap/bind commands callback (nth type 1) overwrite))
(let [undefined (js* "(void 0)")]
(if type
(mousetrap/bind commands callback type overwrite)
(mousetrap/bind commands callback undefined overwrite)))))))))
(defn- reset!
([]

View File

@ -34,38 +34,46 @@
(def shortcuts
{:escape {:tooltip (ds/esc)
:command ["escape" "enter" "v"]
:section [:workspace]
:fn #(st/emit! (esc-pressed))}
:undo {:tooltip (ds/meta "Z")
:command (ds/c-mod "z")
:section [:workspace]
:fn #(st/emit! dwu/undo)}
:redo {:tooltip (ds/meta "Y")
:command [(ds/c-mod "shift+z") (ds/c-mod "y")]
:section [:workspace]
:fn #(st/emit! dwu/redo)}
;; ZOOM
:increase-zoom {:tooltip "+"
:command "+"
:section [:workspace]
:fn #(st/emit! (dw/increase-zoom nil))}
:decrease-zoom {:tooltip "-"
:command "-"
:section [:workspace]
:fn #(st/emit! (dw/decrease-zoom nil))}
:reset-zoom {:tooltip (ds/shift "0")
:command "shift+0"
:section [:workspace]
:fn #(st/emit! dw/reset-zoom)}
:fit-all {:tooltip (ds/shift "1")
:command "shift+1"
:section [:workspace]
:fn #(st/emit! dw/zoom-to-fit-all)}
:zoom-selected {:tooltip (ds/shift "2")
:command "shift+2"
:section [:workspace]
:fn #(st/emit! dw/zoom-to-selected-shape)}})
(defn get-tooltip [shortcut]
(assert (contains? shortcuts shortcut) (str shortcut))
(get-in shortcuts [shortcut :tooltip]))
(get-in shortcuts [shortcut :tooltip]))

View File

@ -30,120 +30,146 @@
{:move-nodes {:tooltip "M"
:command "m"
:subsections [:path-editor]
:section [:workspace]
:fn #(st/emit! (drp/change-edit-mode :move))}
:draw-nodes {:tooltip "P"
:command "p"
:subsections [:path-editor]
:section [:workspace]
:fn #(st/emit! (drp/change-edit-mode :draw))}
:add-node {:tooltip (ds/shift "+")
:command "shift++"
:subsections [:path-editor]
:section [:workspace]
:fn #(st/emit! (drp/add-node))}
:delete-node {:tooltip (ds/supr)
:command ["del" "backspace"]
:subsections [:path-editor]
:section [:workspace]
:fn #(st/emit! (drp/remove-node))}
:merge-nodes {:tooltip (ds/meta "J")
:command (ds/c-mod "j")
:subsections [:path-editor]
:section [:workspace]
:fn #(st/emit! (drp/merge-nodes))}
:join-nodes {:tooltip "J"
:command "j"
:subsections [:path-editor]
:section [:workspace]
:fn #(st/emit! (drp/join-nodes))}
:separate-nodes {:tooltip "K"
:command "k"
:subsections [:path-editor]
:section [:workspace]
:fn #(st/emit! (drp/separate-nodes))}
:make-corner {:tooltip "X"
:command "x"
:subsections [:path-editor]
:section [:workspace]
:fn #(st/emit! (drp/make-corner))}
:make-curve {:tooltip "C"
:command "c"
:subsections [:path-editor]
:section [:workspace]
:fn #(st/emit! (drp/make-curve))}
:snap-nodes {:tooltip (ds/meta "'")
;;https://github.com/ccampbell/mousetrap/issues/85
:command [(ds/c-mod "'") (ds/c-mod "219")]
:subsections [:path-editor]
:section [:workspace]
:fn #(st/emit! (drp/toggle-snap))}
:escape {:tooltip (ds/esc)
:command ["escape" "enter" "v"]
:section [:workspace]
:fn #(st/emit! (esc-pressed))}
:undo {:tooltip (ds/meta "Z")
:command (ds/c-mod "z")
:section [:workspace]
:fn #(st/emit! (drp/undo-path))}
:redo {:tooltip (ds/meta "Y")
:command [(ds/c-mod "shift+z") (ds/c-mod "y")]
:section [:workspace]
:fn #(st/emit! (drp/redo-path))}
;; ZOOM
:increase-zoom {:tooltip "+"
:command "+"
:section [:workspace]
:fn #(st/emit! (dw/increase-zoom nil))}
:decrease-zoom {:tooltip "-"
:command "-"
:section [:workspace]
:fn #(st/emit! (dw/decrease-zoom nil))}
:reset-zoom {:tooltip (ds/shift "0")
:command "shift+0"
:section [:workspace]
:fn #(st/emit! dw/reset-zoom)}
:fit-all {:tooltip (ds/shift "1")
:command "shift+1"
:section [:workspace]
:fn #(st/emit! dw/zoom-to-fit-all)}
:zoom-selected {:tooltip (ds/shift "2")
:command "shift+2"
:section [:workspace]
:fn #(st/emit! dw/zoom-to-selected-shape)}
;; Arrow movement
:move-fast-up {:tooltip (ds/shift ds/up-arrow)
:command "shift+up"
:section [:workspace]
:fn #(st/emit! (drp/move-selected :up true))}
:move-fast-down {:tooltip (ds/shift ds/down-arrow)
:command "shift+down"
:section [:workspace]
:fn #(st/emit! (drp/move-selected :down true))}
:move-fast-right {:tooltip (ds/shift ds/right-arrow)
:command "shift+right"
:section [:workspace]
:fn #(st/emit! (drp/move-selected :right true))}
:move-fast-left {:tooltip (ds/shift ds/left-arrow)
:command "shift+left"
:section [:workspace]
:fn #(st/emit! (drp/move-selected :left true))}
:move-unit-up {:tooltip ds/up-arrow
:command "up"
:section [:workspace]
:fn #(st/emit! (drp/move-selected :up false))}
:move-unit-down {:tooltip ds/down-arrow
:command "down"
:section [:workspace]
:fn #(st/emit! (drp/move-selected :down false))}
:move-unit-left {:tooltip ds/right-arrow
:command "right"
:section [:workspace]
:fn #(st/emit! (drp/move-selected :right false))}
:move-unit-right {:tooltip ds/left-arrow
:command "left"
:section [:workspace]
:fn #(st/emit! (drp/move-selected :left false))}})
(defn get-tooltip [shortcut]

View File

@ -148,23 +148,19 @@
:section [:workspace]
:fn #(emit-when-no-readonly (dw/start-editing-selected))}
:start-measure {:tooltip (ds/alt "")
:command ["alt" "."]
:type "keydown"
:subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/toggle-distances-display true))}
:stop-measure {:tooltip (ds/alt "")
:command ["alt" "."]
:type "keyup"
:subsections [:edit]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/toggle-distances-display false))}
:show-measure {:tooltip (ds/alt "")
:command ["alt" "."]
:type ["keydown" "keyup"]
:subsections [:tools]
:section [:workspace]
:fn #(emit-when-no-readonly
(let [type (.-type %)]
(dw/toggle-distances-display (if (= type "keydown") true false))))}
:escape {:tooltip (ds/esc)
:command "escape"
:subsections [:edit]
:section [:workspace]
:fn #(st/emit! :interrupt (dwdc/clear-drawing) (dw/deselect-all true))}
:find {:tooltip (ds/meta "F") :command (ds/c-mod "f") :subsections [:edit]
@ -191,6 +187,7 @@
:ungroup {:tooltip (ds/shift "G")
:command "shift+g"
:subsections [:modify-layers]
:section [:workspace]
:fn #(emit-when-no-readonly (dw/ungroup-selected))}
:mask {:tooltip (ds/meta "M")
@ -363,11 +360,13 @@
:draw-line {:tooltip "L"
:command "l"
:subsections [:tools]
:section [:workspace]
:fn #(emit-when-no-readonly (dwd/select-for-drawing :line))}
:draw-arrow {:tooltip (ds/shift "L")
:command "shift+l"
:subsections [:tools]
:section [:workspace]
:fn #(emit-when-no-readonly (dwd/select-for-drawing :arrow))}
:draw-curve {:tooltip (ds/shift "C")

View File

@ -23,9 +23,9 @@
transition: all 0.4s;
text-decoration: none !important;
block-size: $sz-40;
.logo {
block-size: px2rem(20);
inline-size: px2rem(20);
}
}
.logo {
block-size: px2rem(20);
inline-size: px2rem(20);
}

View File

@ -0,0 +1,161 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.main.ui.settings.import-shortcuts-diff-modal
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.i18n :refer [tr]]
[app.main.data.dashboard.shortcuts :as dsc]
[app.main.data.modal :as modal]
[app.main.data.viewer.shortcuts :as vsc]
[app.main.data.workspace.path.shortcuts :as psc]
[app.main.data.workspace.shortcuts :as wsc]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.shortcuts :as ss]
[app.util.dom :as dom]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(def ^:private context->defaults
{:workspace (d/deep-merge psc/shortcuts wsc/shortcuts)
:dashboard dsc/shortcuts
:viewer vsc/shortcuts})
(defn compute-diff
[imported-shortcuts custom-shortcuts]
(let [contexts (filter #(contains? imported-shortcuts %) [:workspace :dashboard :viewer])]
(mapcat
(fn [ctx]
(let [imported-ctx (get imported-shortcuts ctx)
current-ctx (get custom-shortcuts ctx {})
defaults-ctx (get context->defaults ctx)]
(keep
(fn [[shortcut-key imported-binding]]
(let [current-binding (get current-ctx shortcut-key)
default-cmd (:command (get defaults-ctx shortcut-key))
effective-binding (or current-binding default-cmd)
customized? (and (some? current-binding)
(seq current-binding)
(not= current-binding default-cmd))]
(when (and (some? imported-binding)
(not= effective-binding imported-binding))
{:context ctx
:key shortcut-key
:current effective-binding
:imported imported-binding
:customized? customized?})))
imported-ctx)))
contexts)))
(def ^:private context-order {:workspace 0 :dashboard 1 :viewer 2})
(def ^:private context-name
{:workspace "Workspace"
:dashboard "Dashboard"
:viewer "Viewer"})
(mf/defc import-shortcuts-diff-modal
{::mf/register modal/components
::mf/register-as :import-shortcuts-diff-modal}
[{:keys [imported-shortcuts custom-shortcuts all-shortcuts-raw]}]
(let [diff-entries
(mf/with-memo [imported-shortcuts custom-shortcuts]
(->> (compute-diff imported-shortcuts custom-shortcuts)
(sort-by (fn [e] [(get context-order (:context e) 99)
(name (:key e))]))))
handle-close-dialog
(mf/use-fn
(fn [event]
(dom/stop-propagation event)
(st/emit! (modal/hide))))
handle-apply
(mf/use-fn
(mf/deps imported-shortcuts all-shortcuts-raw)
(fn [event]
(dom/stop-propagation event)
(st/emit! (ss/import-custom-shortcuts imported-shortcuts all-shortcuts-raw))
(st/emit! (modal/hide))))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog)}
[:> icon-button* {:class (stl/css :close-btn)
:variant "ghost"
:aria-label (tr "labels.close")
:on-click handle-close-dialog
:tooltip-class (stl/css :close-btn-tooltip)
:icon i/close}]
[:div {:class (stl/css :modal-title)}
(tr "import-shortcuts.diff-modal-title")]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-content-text)}
(tr "import-shortcuts.diff-modal-text")]
(if (seq diff-entries)
[:table {:class (stl/css :shortcuts-table)}
[:thead
[:tr {:class (stl/css :shortcuts-list-header)}
[:th {:class (stl/css :shortcut-header-name)}
(tr "restore-shortcuts.acction")]
[:th {:class (stl/css :shortcut-header-command)}
(tr "labels.current")]
[:th {:class (stl/css :shortcut-header-command)}
(tr "labels.import")]]]
[:tbody {:class (stl/css :shortcuts-list-body)}
(let [last-ctx* (volatile! nil)]
(for [entry diff-entries]
(let [{:keys [context key current imported customized?]} entry
show-context-label? (not= @last-ctx* context)]
(vreset! last-ctx* context)
[:* {:key (dm/str (name context) "-" (name key) "-group")}
(when show-context-label?
[:tr {:key (dm/str "ctx-" (name context))
:class (stl/css :context-separator)}
[:td {:colSpan 3
:class (stl/css :context-label)}
(get context-name context)]])
[:tr {:key (dm/str (name context) "-" (name key))
:class (stl/css :shortcuts-list-item)}
[:td {:class (stl/css :shortcut-name)}
(ss/translation-keyname :sc key)]
[:td {:class (stl/css :shortcut-command)}
(if (str/blank? current)
[:span {:class (stl/css :shortcut-empty)} "-"]
[:> ss/shortcuts-keys* {:content current
:command key
:is-customized customized?
:light-shortcut true
:has-conflict? false}])]
[:td {:class (stl/css :shortcut-command)}
(if (str/blank? imported)
[:span {:class (stl/css :shortcut-empty)} "-"]
[:> ss/shortcuts-keys* {:content imported
:command key
:light-shortcut true
:is-customized true
:has-conflict? false}])]]])))]]
[:div {:class (stl/css :no-changes)}
(tr "import-shortcuts.no-changes")])]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
[:> button* {:class (stl/css :cancel-button)
:variant "secondary"
:type "button"
:on-click handle-close-dialog}
(tr "labels.cancel")]
[:> button* {:class (stl/css :cancel-button)
:variant "primary"
:type "button"
:on-click handle-apply}
(tr "import-shortcuts.apply")]]]]]))

View File

@ -0,0 +1,167 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC Sucursal en España SL
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.modal-overlay {
display: flex;
justify-content: center;
align-items: center;
position: fixed;
inset-inline-start: 0;
inset-block-start: 0;
block-size: 100%;
inline-size: 100%;
z-index: var(--z-index-set);
background-color: var(--overlay-color);
padding-inline-start: px2rem(272);
}
.modal-dialog {
position: relative;
padding: var(--sp-xxxl);
border-radius: $br-8;
background-color: var(--color-background-primary);
border: $b-2 solid var(--color-background-quaternary);
display: grid;
grid-template-rows: auto 1fr auto;
gap: var(--sp-xxxl);
min-inline-size: $sz-364;
min-block-size: $sz-192;
max-block-size: $sz-520;
inline-size: px2rem(584);
max-inline-size: $sz-712;
}
.modal-title {
@include t.use-typography("headline-medium");
color: var(--color-foreground-primary);
block-size: px2rem(26);
}
.close-btn {
position: absolute;
inset-block-start: px2rem(8);
inset-inline-end: px2rem(6);
block-size: $sz-32;
inline-size: $sz-28;
}
.close-btn-tooltip {
position: absolute;
inset-block-start: px2rem(8);
inset-inline-end: px2rem(6);
}
.modal-content {
display: flex;
flex-direction: column;
gap: var(--sp-l);
block-size: fit-content;
overflow-y: auto;
}
.modal-content-text {
@include t.use-typography("body-medium");
color: var(--color-foreground-primary);
}
.modal-footer {
justify-self: end;
display: flex;
flex-direction: row;
gap: var(--sp-s);
}
.action-buttons {
display: flex;
justify-content: flex-end;
gap: var(--sp-s);
}
.shortcuts-table {
inline-size: 100%;
table-layout: fixed;
border-collapse: collapse;
}
.shortcuts-list-item,
.shortcuts-list-header {
@include t.use-typography("body-medium");
color: var(--color-foreground-secondary);
display: grid;
grid-template-columns: 35% 30% 35%;
}
.shortcuts-list-header {
border-block-end: $b-1 solid var(--color-foreground-secondary);
block-size: $sz-32;
text-align: start;
padding-block-end: var(--sp-s);
}
.shortcuts-list-body {
display: flex;
flex-direction: column;
gap: var(--sp-s);
}
.shortcuts-list-item {
color: var(--color-foreground-primary);
}
.shortcuts-list-item:first-child {
padding-block-start: var(--sp-s);
}
.shortcut-header-name,
.shortcut-header-command {
text-align: start;
}
.shortcut-name {
text-align: start;
}
.shortcut-command {
display: flex;
gap: var(--sp-s);
align-items: center;
justify-content: start;
text-align: start;
flex-wrap: wrap;
}
.shortcut-empty {
color: var(--color-foreground-secondary);
}
.context-separator {
border-block-end: none;
}
.context-label {
@include t.use-typography("title-small");
color: var(--color-foreground-secondary);
text-align: start;
padding-block: var(--sp-m) var(--sp-xxs);
}
.no-changes {
@include t.use-typography("body-medium");
color: var(--color-foreground-secondary);
text-align: center;
padding-block: var(--sp-xl);
}

View File

@ -46,7 +46,9 @@
(drop-last default-chars-list))
ctx-customs (get custom-shortcuts context {})
current-command (or (get ctx-customs shortcut-key) default-command)
custom-val (get ctx-customs shortcut-key)
current-command (or custom-val default-command)
customized? (and (some? custom-val) (seq custom-val) (not= custom-val default-command))
current-managed-list (if (coll? current-command)
current-command
@ -59,7 +61,7 @@
current-short-char-list (if (= 1 (count current-chars-list))
current-chars-list
(drop-last current-chars-list))]
[default-last-element default-short-char-list current-last-element current-command current-short-char-list]))
[default-last-element default-short-char-list current-last-element current-command current-short-char-list customized?]))
(mf/defc restore-all-modal
{::mf/register modal/components
@ -107,9 +109,11 @@
default-short-char-list
current-last-element
current-command
current-short-char-list] (extract-shortcut-keys shortcut-key custom-shortcuts context)
current-short-char-list
customized?] (extract-shortcut-keys shortcut-key custom-shortcuts context)
current-penultimate (last current-short-char-list)
default-penultimate (last default-short-char-list)]
default-penultimate (last default-short-char-list)
command-class (if customized? (stl/css :customized-command) (stl/css :default-command))]
[:tr {:key (dm/str (name context) "-" (name shortcut-key))
:class (stl/css :shortcuts-list-item)}
[:td {:class (stl/css :shortcut-name)}
@ -122,7 +126,7 @@
(for [char chars]
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
:char char
:class (stl/css :default-command)
:class command-class
:command shortcut-key}])
(when (not= chars current-penultimate) [:span {:class (stl/css :space)} ","])]))
(when (not= current-last-element current-penultimate)
@ -131,7 +135,7 @@
(for [char current-last-element]
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
:char char
:class (stl/css :default-command)
:class command-class
:command shortcut-key}])])]
[:td {:class (stl/css :shortcut-command)}

View File

@ -21,6 +21,7 @@
inline-size: 100%;
z-index: var(--z-index-set);
background-color: var(--overlay-color);
padding-inline-start: px2rem(272);
}
.modal-dialog {
@ -142,6 +143,12 @@
border-radius: $br-6;
}
.customized-command {
background-color: var(--color-background-info);
color: var(--color-foreground-primary);
border-radius: $br-6;
}
.shortcut-empty {
color: var(--color-foreground-secondary);
}

View File

@ -24,6 +24,7 @@
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.settings.import-shortcuts-diff-modal]
[app.main.ui.settings.restore-shortcuts-modal]
[app.main.ui.shortcuts :as ss]
[app.util.dom :as dom]
@ -35,7 +36,7 @@
[rumext.v2 :as mf]))
(mf/defc search-section*
[{:keys [filter-term on-search-term-change on-search-clear-click on-restore-all show-restore-all? has-custom-shortcuts]}]
[{:keys [filter-term on-search-term-change on-search-clear-click on-restore-all show-restore-all has-custom-shortcuts]}]
[:div {:class (stl/css :shortcuts-search-section)}
[:> search-bar* {:on-change on-search-term-change
:on-clear on-search-clear-click
@ -43,7 +44,7 @@
:placeholder (tr "shortcuts.title")
:icon-id i/search
:auto-focus true}]
(when (and show-restore-all? has-custom-shortcuts)
(when (and show-restore-all has-custom-shortcuts)
[:> button*
{:variant "secondary"
:on-click on-restore-all
@ -171,10 +172,9 @@
:conflicts conflicts}]))]))
(mf/defc shortcuts-tab-section*
[{:keys [shortcut-filter show-restore-all? on-restore-all empty-str custom-shortcuts conflicts]}]
[{:keys [shortcut-filter show-restore-all on-restore-all empty-str custom-shortcuts conflicts expand-all-by-default
open-sections update-open-sections]}]
(let [{:keys [all-shortcuts]} (mf/use-ctx ctx/shortcuts-ctx)
open-sections* (mf/use-state [[:workspace]])
open-sections (deref open-sections*)
filter-term* (mf/use-state "")
filter-term (deref filter-term*)
@ -186,8 +186,15 @@
(seq (:children (:none children)))
(seq children))))
all-shortcuts (into {} (filter (fn [[_ v]] (section-has-content? v)) all-shortcuts))
filtered-shortcuts (filter-shortcuts-tree all-shortcuts shortcut-filter filter-term)
all-shortcuts (mf/with-memo [all-shortcuts]
(into {} (filter (fn [[_ v]] (section-has-content? v)) all-shortcuts)))
filtered-shortcuts (mf/with-memo [all-shortcuts shortcut-filter filter-term]
(filter-shortcuts-tree all-shortcuts shortcut-filter filter-term))
expand-all-sections
(fn []
(let [all-ids (collect-open-section-ids filtered-shortcuts)]
(update-open-sections (if (seq all-ids) (vec all-ids) [[:workspace]]))))
on-search-term-change
(mf/use-fn
@ -195,16 +202,20 @@
(fn [term]
(reset! filter-term* term)
(if (str/blank? term)
(reset! open-sections* [[:workspace]])
(if expand-all-by-default
(expand-all-sections)
(update-open-sections [[:workspace]]))
(let [filtered-tree (filter-shortcuts-tree all-shortcuts shortcut-filter term)
open-ids (collect-open-section-ids filtered-tree)]
(reset! open-sections* open-ids)))))
(update-open-sections (vec open-ids))))))
on-search-clear-click
(mf/use-fn
(fn [_]
(reset! open-sections* [[:workspace]])
(reset! filter-term* "")))
(reset! filter-term* "")
(if expand-all-by-default
(expand-all-sections)
(update-open-sections [[:workspace]]))))
manage-sections
(fn [item]
@ -214,15 +225,28 @@
new-value (if is-present?
(filterv (fn [element] (not= element item)) open-sections)
(conj open-sections item))]
(reset! open-sections* new-value))))]
(update-open-sections new-value))))
has-custom-shortcuts (some #(seq (val %)) custom-shortcuts)
empty-str
(if (and has-custom-shortcuts (not (str/blank? filter-term)))
(tr "shortcuts.no-shortcuts")
empty-str)]
(mf/with-effect [filtered-shortcuts filter-term expand-all-by-default open-sections]
(when (and expand-all-by-default (str/blank? filter-term))
(let [all-ids (vec (collect-open-section-ids filtered-shortcuts))]
(when (and (seq all-ids) (not= all-ids open-sections))
(update-open-sections all-ids)))))
[:div {:class (stl/css :shortcuts-section)}
[:> search-section* {:filter-term filter-term
:on-search-term-change on-search-term-change
:on-search-clear-click on-search-clear-click
:on-restore-all on-restore-all
:show-restore-all? show-restore-all?
:has-custom-shortcuts (some #(seq (val %)) custom-shortcuts)}]
:show-restore-all show-restore-all
:has-custom-shortcuts has-custom-shortcuts}]
(if (seq filtered-shortcuts)
[:> shortcuts-list* {:shortcuts filtered-shortcuts
:open-sections open-sections
@ -273,6 +297,14 @@
(fn [new-section]
(reset! section* (keyword new-section))))
open-sections-by-tab* (mf/use-state {})
open-sections-by-tab (deref open-sections-by-tab*)
make-update-open-sections
(fn [tab-key]
(fn [new-val]
(swap! open-sections-by-tab* assoc tab-key new-val)))
custom-shortcuts (get-in profile [:props :custom-shortcuts])
has-custom-shortcuts (some #(seq (val %)) custom-shortcuts)
path-shortcuts-custom (ds/apply-custom-overrides
@ -322,11 +354,12 @@
:viewer-orig vsc/shortcuts)
shortcuts-ctx-value
{:workspace-sc-trans (d/deep-merge workspace-shortcuts-with-translation path-shortcuts-with-translation)
:dashboard-sc-trans dashboard-shortcuts-with-translation
:viewer-sc-trans viewer-shortcuts-with-translation
:all-shortcuts all-shortcuts
:all-sc-raw all-shortcuts-raw}
(mf/with-memo [custom-shortcuts]
{:workspace-sc-trans (d/deep-merge workspace-shortcuts-with-translation path-shortcuts-with-translation)
:dashboard-sc-trans dashboard-shortcuts-with-translation
:viewer-sc-trans viewer-shortcuts-with-translation
:all-shortcuts all-shortcuts
:all-sc-raw all-shortcuts-raw})
on-restore-all
(mf/use-fn
@ -336,6 +369,38 @@
(st/emit! (modal/show {:type :restore-all-modal
:custom-shortcuts custom-shortcuts}))))
filter-all
(mf/with-memo []
(fn [_ shortcut search-term]
(or (str/blank? search-term)
(matches-search (:translation shortcut) search-term))))
filter-personalized
(mf/use-fn
(mf/deps custom-shortcuts)
(fn [shortcut-key shortcut search-term]
(let [shortcut-group (first (:section shortcut))
group-map (get custom-shortcuts shortcut-group)
group-map (if (map? group-map) group-map {})
customized? (and (contains? group-map shortcut-key)
(not (str/blank? (get group-map shortcut-key))))]
(and customized?
(or (str/blank? search-term)
(matches-search (:translation shortcut) search-term))))))
filter-disabled
(mf/use-fn
(mf/deps custom-shortcuts)
(fn [shortcut-key shortcut search-term]
(let [shortcut-group (first (:section shortcut))
group-map (get custom-shortcuts shortcut-group)
group-map (if (map? group-map) group-map {})
in-group? (contains? group-map shortcut-key)
blank? (str/blank? (get group-map shortcut-key))]
(and in-group? blank?
(or (str/blank? search-term)
(matches-search (:translation shortcut) search-term))))))
on-import-file
(mf/use-fn
(fn [_]
@ -357,7 +422,7 @@
on-file-selected
(mf/use-fn
(mf/deps all-shortcuts-raw)
(mf/deps all-shortcuts-raw custom-shortcuts)
(fn [event]
(let [file (-> (dom/get-target event)
(dom/get-files)
@ -370,7 +435,10 @@
:keywordize-keys true)
validation (validate-imported-shortcuts shortcuts)]
(if (:valid? validation)
(st/emit! (ss/import-custom-shortcuts shortcuts all-shortcuts-raw))
(st/emit! (modal/show {:type :import-shortcuts-diff-modal
:imported-shortcuts shortcuts
:custom-shortcuts custom-shortcuts
:all-shortcuts-raw all-shortcuts-raw}))
(st/emit! (ntf/error (tr "errors.invalid-data")))))
(catch :default _
(st/emit! (ntf/error (tr "errors.invalid-data"))))))))
@ -400,43 +468,33 @@
:class (stl/css :shortcuts-switcher)}
(case section
:all
[:> shortcuts-tab-section* {:shortcut-filter (fn [_ shortcut search-term]
(or (str/blank? search-term)
(matches-search (:translation shortcut) search-term)))
:show-restore-all? true
[:> shortcuts-tab-section* {:shortcut-filter filter-all
:show-restore-all true
:empty-str (tr "shortcuts.no-shortcuts")
:on-restore-all on-restore-all
:custom-shortcuts custom-shortcuts}]
:custom-shortcuts custom-shortcuts
:open-sections (get open-sections-by-tab :all [[:workspace]])
:update-open-sections (make-update-open-sections :all)}]
:personalized
[:> shortcuts-tab-section* {:shortcut-filter (fn [shortcut-key shortcut search-term]
(let [shortcut-group (first (:section shortcut))
group-map (get custom-shortcuts shortcut-group)
group-map (if (map? group-map) group-map {})
customized? (and (contains? group-map shortcut-key)
(not (str/blank? (get group-map shortcut-key))))]
(and customized?
(or (str/blank? search-term)
(matches-search (:translation shortcut) search-term)))))
:show-restore-all? true
[:> shortcuts-tab-section* {:shortcut-filter filter-personalized
:show-restore-all true
:empty-str (tr "shortcuts.no-personalized")
:on-restore-all on-restore-all
:custom-shortcuts custom-shortcuts}]
:expand-all-by-default true
:custom-shortcuts custom-shortcuts
:open-sections (get open-sections-by-tab :personalized [[:workspace]])
:update-open-sections (make-update-open-sections :personalized)}]
:disabled
[:> shortcuts-tab-section* {:shortcut-filter (fn [shortcut-key shortcut search-term]
(let [shortcut-group (first (:section shortcut))
group-map (get custom-shortcuts shortcut-group)
group-map (if (map? group-map) group-map {})
in-group? (contains? group-map shortcut-key)
blank? (str/blank? (get group-map shortcut-key))]
(and in-group? blank?
(or (str/blank? search-term)
(matches-search (:translation shortcut) search-term)))))
:show-restore-all? true
[:> shortcuts-tab-section* {:shortcut-filter filter-disabled
:show-restore-all true
:empty-str (tr "shortcuts.no-disabled")
:on-restore-all on-restore-all
:custom-shortcuts custom-shortcuts}])]]
:expand-all-by-default true
:custom-shortcuts custom-shortcuts
:open-sections (get open-sections-by-tab :disabled [[:workspace]])
:update-open-sections (make-update-open-sections :disabled)}])]]
[:div {:class (stl/css :shortcuts-page-footer)}
[:div {:class (stl/css :shortcuts-info)}

View File

@ -14,6 +14,9 @@
.shortcuts-page {
display: flex;
justify-content: center;
// box-sizing: border-box;
// min-block-size: 100%;
padding-block-end: px2rem(32);
}
@ -194,26 +197,20 @@
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
padding-block-start: var(--sp-m);
}
.shortcuts-info {
display: flex;
gap: var(--sp-s);
align-items: center;
}
.shortcuts-page-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
padding-block-start: var(--sp-m);
margin-block-start: auto;
padding-block: var(--sp-m) var(--sp-s);
position: fixed;
bottom: 0;
background-color: var(--app-background);
min-inline-size: px2rem(590);
}
.shortcuts-info {
@include t.use-typography("body-small");
display: flex;
gap: var(--sp-m);
align-items: center;
color: var(--color-foreground-secondary);
}
@ -229,11 +226,6 @@
background-color: var(--color-accent-error);
}
.shortcuts-info {
display: flex;
gap: var(--sp-m);
}
.shortcuts-info-wrapper {
display: flex;
align-items: center;

View File

@ -305,7 +305,7 @@
:key unique-key} char]))
(mf/defc shortcuts-keys*
[{:keys [content command is-customized has-conflict?]}]
[{:keys [content command is-customized has-conflict? light-shortcut]}]
(let [managed-list (if (coll? content)
content
(conj () content))
@ -324,7 +324,8 @@
:command command
:class (cond
has-conflict? (stl/css :conflict-key)
is-customized (stl/css :customized-key))}])
is-customized (stl/css :customized-key)
light-shortcut (stl/css :light-key))}])
(when (not= chars penultimate) [:span {:class (stl/css :space)} ","])])
(when (not= last-element penultimate)
@ -336,7 +337,8 @@
:command command
:class (cond
has-conflict? (stl/css :conflict-key)
is-customized (stl/css :customized-key))}])])]))
is-customized (stl/css :customized-key)
light-shortcut (stl/css :light-key))}])])]))
(mf/defc shortcut-row-editable*
[{:keys [elements custom-shortcuts command-translate conflicts section-key]}]
@ -371,21 +373,30 @@
conflict* (mf/use-state nil)
conflict (deref conflict*)
reset-notification* (mf/use-state nil)
reset-pending* (mf/use-state false)
clean-editing-state
(fn []
(reset! is-editing* false)
(reset! display-parts* nil)
(reset! recorded-command* nil)
(reset! conflict* nil))
(reset! conflict* nil)
(reset! reset-notification* nil)
(reset! reset-pending* false))
effective-section-key (if (= section-key :basics) :workspace section-key)
on-reset-shortcut
(mf/use-fn
(mf/deps effective-section-key command-info)
(fn [shortcut-key]
(st/emit! (customize/reset-custom-shortcut shortcut-key (:original-command command-info) effective-section-key))
(clean-editing-state)))
(fn [_]
(let [original-cmd (:original-command command-info)]
(reset! reset-pending* true)
(reset! recorded-command* original-cmd)
(reset! reset-notification* original-cmd)
(reset! conflict* nil)
(reset! display-parts* nil))))
start-editing
(mf/use-fn
@ -419,11 +430,13 @@
(fn [event]
(dom/prevent-default event)
(when recorded-command
(st/emit! (customize/set-custom-shortcut
command
recorded-command
(:key conflict)
effective-section-key)))
(if (deref reset-pending*)
(st/emit! (customize/reset-custom-shortcut command recorded-command effective-section-key))
(st/emit! (customize/set-custom-shortcut
command
recorded-command
(:key conflict)
effective-section-key))))
(clean-editing-state)))
on-editable-container-blur
@ -437,6 +450,11 @@
(when is-editing
(some-> (mf/ref-val recording-ref) (dom/focus!))))
(mf/with-effect [display-parts]
(when (some? display-parts)
(reset! reset-notification* nil)
(reset! reset-pending* false)))
(mf/with-effect [is-editing]
(when is-editing
(letfn [(on-keydown [^js event]
@ -506,7 +524,15 @@
:customized-key customized?)} (:final-key display-parts)])
(when-not (:finalized? display-parts)
[:span {:class (stl/css :recording-ellipsis)} "..."])])]
(when recorded-command
(when-let [default-cmd @reset-notification*]
[:> context-notification* {:level :info :class (stl/css :modal-reset-msg)}
(tr "shortcuts.edit-modal.reset-pending")
[:> shortcuts-keys* {:content default-cmd
:command (keyword "default")
:is-customized false
:has-conflict? false}]])
(when (and recorded-command (not (deref reset-pending*)))
(if conflict
[:> context-notification* {:level :warning :class (stl/css :modal-error-msg)}
(tr "shortcuts.edit-modal.conflict" (:name conflict))]

View File

@ -187,6 +187,11 @@
color: var(--color-foreground-error);
}
.light-key {
background-color: var(--color-background-tertiary);
color: var(--color-foreground-secondary);
}
.space {
margin: 0 var(--sp-xxs);
color: var(--color-foreground-secondary);

View File

@ -1,6 +1,7 @@
(ns frontend-tests.ui.settings-shortcuts-test
(:require
[app.main.data.profile :as du]
[app.main.ui.settings.import-shortcuts-diff-modal :as diff-modal]
[app.main.ui.settings.restore-shortcuts-modal :as restore-modal]
[app.main.ui.settings.shortcuts :as sut]
[app.main.ui.shortcuts :as ui-shortcuts]
@ -150,6 +151,48 @@
(t/is (= "ctrl+up" (get customs :move-to-top))
"Last imported entry with duplicate command should survive")))))
(t/deftest compute-diff-shows-new-shortcut-changes
(let [imported {:workspace {:add-comment "alt+c"}}
diff (diff-modal/compute-diff imported {})]
(t/is (= 1 (count diff)))
(let [entry (first diff)]
(t/is (= :workspace (:context entry)))
(t/is (= :add-comment (:key entry)))
(t/is (= "alt+c" (:imported entry)))
(t/is (not= "alt+c" (:current entry))))))
(t/deftest compute-diff-shows-conflict-as-disabled
(let [imported {:workspace {:select-all "ctrl+shift+a"}}
diff (diff-modal/compute-diff imported {})]
(t/is (pos? (count diff)))
(let [select-all-entry (first (filter #(= :select-all (:key %)) diff))]
(t/is (= :workspace (:context select-all-entry)))
(t/is (= "ctrl+shift+a" (:imported select-all-entry))))))
(t/deftest compute-diff-shows-empty-string-as-disabled-shortcut
(let [imported {:workspace {:escape ""}}
current {}
diff (diff-modal/compute-diff imported current)]
(t/is (pos? (count diff)))
(let [entry (first (filter #(= :escape (:key %)) diff))]
(t/is (= :workspace (:context entry)))
(t/is (= "" (:imported entry))))))
(t/deftest compute-diff-excludes-unchanged-shortcuts
(let [imported {:workspace {:escape ""}}
current {:workspace {:escape ""}}
diff (diff-modal/compute-diff imported current)]
(t/is (empty? diff))))
(t/deftest compute-diff-detects-multiple-contexts
(let [imported {:workspace {:add-comment "alt+c"}
:viewer {:next-frame "right"}}
diff (diff-modal/compute-diff imported {})]
(t/is (pos? (count diff)))
(let [contexts (set (map :context diff))]
(t/is (contains? contexts :workspace))
(t/is (contains? contexts :viewer)))))
(t/deftest extract-shortcut-keys-uses-correct-context-override
(let [customs {:workspace {:undo "shift+z"
:move-nodes "shift+m"}

View File

@ -5631,12 +5631,8 @@ msgid "shortcuts.start-editing"
msgstr "Start editing"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:182
msgid "shortcuts.start-measure"
msgstr "Start measurement"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:183
msgid "shortcuts.stop-measure"
msgstr "Stop measurement"
msgid "shortcuts.show-measure"
msgstr "Show measurements"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:184
msgid "shortcuts.thumbnail-set"
@ -5710,6 +5706,10 @@ msgstr "Press the key combination"
msgid "shortcuts.edit-modal.conflict"
msgstr "Combination assigned to \"%s\". Saving will remove it there."
#: src/app/main/ui/shortcuts.cljs
msgid "shortcuts.edit-modal.reset-pending"
msgstr "If you save, this shortcut will return to the original value:"
#: src/app/main/ui/shortcuts.cljs
msgid "shortcuts.edit-modal.success"
msgstr "This key combination is available and can be assigned without conflicts."
@ -5846,6 +5846,22 @@ msgstr "Acction"
msgid "restore-shortcuts.restore"
msgstr "Restore"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs
msgid "import-shortcuts.diff-modal-title"
msgstr "Import shortcuts"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs
msgid "import-shortcuts.diff-modal-text"
msgstr "The following shortcuts will be changed by the imported configuration."
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs
msgid "import-shortcuts.no-changes"
msgstr "No differences found between the current and imported shortcuts."
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs
msgid "import-shortcuts.apply"
msgstr "Apply"
#: src/app/main/ui/dashboard/subscription.cljs:191
msgid "subscription.banner.create-org-info"
msgstr ""

View File

@ -5486,12 +5486,8 @@ msgid "shortcuts.start-editing"
msgstr "Comenzar edición"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:182
msgid "shortcuts.start-measure"
msgstr "Comenzar medida"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:183
msgid "shortcuts.stop-measure"
msgstr "Terminar medida"
msgid "shortcuts.show-measure"
msgstr "Mostrar medidas"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:184
msgid "shortcuts.thumbnail-set"
@ -5565,6 +5561,10 @@ msgstr "Pulsa la combinación de teclas deseada"
msgid "shortcuts.edit-modal.conflict"
msgstr "Combinación usada en \"%s\". Guardar este atajo hará que se elimine allí."
#: src/app/main/ui/shortcuts.cljs
msgid "shortcuts.edit-modal.reset-pending"
msgstr "Si guardas, este atajo volvera a su valor original:"
#: src/app/main/ui/shortcuts.cljs
msgid "shortcuts.edit-modal.success"
msgstr "Esta combinación de teclas está disponible y puede asignarse sin conflictos."
@ -5701,6 +5701,22 @@ msgstr "Acción"
msgid "restore-shortcuts.restore"
msgstr "Restaurar"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs
msgid "import-shortcuts.diff-modal-title"
msgstr "Importar atajos"
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs
msgid "import-shortcuts.diff-modal-text"
msgstr "Los siguientes atajos se modificarán con la configuración importada."
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs
msgid "import-shortcuts.no-changes"
msgstr "No se encontraron diferencias entre los atajos actuales y los importados."
#: src/app/main/ui/settings/import_shortcuts_diff_modal.cljs
msgid "import-shortcuts.apply"
msgstr "Aplicar"
#: src/app/main/ui/dashboard/subscription.cljs:191
msgid "subscription.banner.create-org-info"
msgstr ""