🎉 Add diff modal

This commit is contained in:
Eva Marco 2026-07-28 14:07:28 +02:00
parent 14931c1bac
commit 9294b30524
9 changed files with 416 additions and 35 deletions

View File

@ -0,0 +1,162 @@
;; 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.shortcuts :as ds]
[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,166 @@
// 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);
}
.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
@ -103,36 +105,38 @@
[:tbody {:class (stl/css :shortcuts-list-body)}
(for [context [:workspace :dashboard :viewer]
shortcut-key (keys (get custom-shortcuts context {}))]
(let [[default-last-element
default-short-char-list
current-last-element
current-command
current-short-char-list] (extract-shortcut-keys shortcut-key custom-shortcuts context)
current-penultimate (last current-short-char-list)
default-penultimate (last default-short-char-list)]
(let [[default-last-element
default-short-char-list
current-last-element
current-command
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)
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)}
(ss/translation-keyname :sc shortcut-key)]
[:td {:class (stl/css :shortcut-command)}
(if (str/blank? current-command)
[:span {:class (stl/css :shortcut-empty)} "-"]
(for [chars current-short-char-list]
[:* {:key (str/join chars)}
(for [char chars]
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
:char char
:class (stl/css :default-command)
:command shortcut-key}])
(when (not= chars current-penultimate) [:span {:class (stl/css :space)} ","])]))
(when (not= current-last-element current-penultimate)
[:*
[:span {:class (stl/css :space)} (tr "shortcuts.or")]
(for [char current-last-element]
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
:char char
:class (stl/css :default-command)
:command shortcut-key}])])]
[:td {:class (stl/css :shortcut-name)}
(ss/translation-keyname :sc shortcut-key)]
[:td {:class (stl/css :shortcut-command)}
(if (str/blank? current-command)
[:span {:class (stl/css :shortcut-empty)} "-"]
(for [chars current-short-char-list]
[:* {:key (str/join chars)}
(for [char chars]
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
:char char
:class command-class
:command shortcut-key}])
(when (not= chars current-penultimate) [:span {:class (stl/css :space)} ","])]))
(when (not= current-last-element current-penultimate)
[:*
[:span {:class (stl/css :space)} (tr "shortcuts.or")]
(for [char current-last-element]
[:> ss/converted-chars* {:key (dm/str char "-" (name shortcut-key))
:char char
:class command-class
:command shortcut-key}])])]
[:td {:class (stl/css :shortcut-command)}
(for [chars default-short-char-list]

View File

@ -142,6 +142,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]
@ -357,7 +358,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 +371,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"))))))))

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]}]

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

@ -5850,6 +5850,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

@ -5705,6 +5705,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 ""