From 9294b30524ae4c497e46bc79e35ec5cc73557fcc Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Tue, 28 Jul 2026 14:07:28 +0200 Subject: [PATCH] :tada: Add diff modal --- .../settings/import_shortcuts_diff_modal.cljs | 162 +++++++++++++++++ .../settings/import_shortcuts_diff_modal.scss | 166 ++++++++++++++++++ .../ui/settings/restore_shortcuts_modal.cljs | 64 +++---- .../ui/settings/restore_shortcuts_modal.scss | 6 + .../src/app/main/ui/settings/shortcuts.cljs | 8 +- frontend/src/app/main/ui/shortcuts.cljs | 8 +- frontend/src/app/main/ui/shortcuts.scss | 5 + frontend/translations/en.po | 16 ++ frontend/translations/es.po | 16 ++ 9 files changed, 416 insertions(+), 35 deletions(-) create mode 100644 frontend/src/app/main/ui/settings/import_shortcuts_diff_modal.cljs create mode 100644 frontend/src/app/main/ui/settings/import_shortcuts_diff_modal.scss diff --git a/frontend/src/app/main/ui/settings/import_shortcuts_diff_modal.cljs b/frontend/src/app/main/ui/settings/import_shortcuts_diff_modal.cljs new file mode 100644 index 0000000000..a5a78c3331 --- /dev/null +++ b/frontend/src/app/main/ui/settings/import_shortcuts_diff_modal.cljs @@ -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")]]]]])) diff --git a/frontend/src/app/main/ui/settings/import_shortcuts_diff_modal.scss b/frontend/src/app/main/ui/settings/import_shortcuts_diff_modal.scss new file mode 100644 index 0000000000..cf44a39318 --- /dev/null +++ b/frontend/src/app/main/ui/settings/import_shortcuts_diff_modal.scss @@ -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); +} diff --git a/frontend/src/app/main/ui/settings/restore_shortcuts_modal.cljs b/frontend/src/app/main/ui/settings/restore_shortcuts_modal.cljs index a24091f7b2..b35bf2d639 100644 --- a/frontend/src/app/main/ui/settings/restore_shortcuts_modal.cljs +++ b/frontend/src/app/main/ui/settings/restore_shortcuts_modal.cljs @@ -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] diff --git a/frontend/src/app/main/ui/settings/restore_shortcuts_modal.scss b/frontend/src/app/main/ui/settings/restore_shortcuts_modal.scss index 95da5b5147..294952eb95 100644 --- a/frontend/src/app/main/ui/settings/restore_shortcuts_modal.scss +++ b/frontend/src/app/main/ui/settings/restore_shortcuts_modal.scss @@ -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); } diff --git a/frontend/src/app/main/ui/settings/shortcuts.cljs b/frontend/src/app/main/ui/settings/shortcuts.cljs index 24b49cba1d..eb7e4b76e1 100644 --- a/frontend/src/app/main/ui/settings/shortcuts.cljs +++ b/frontend/src/app/main/ui/settings/shortcuts.cljs @@ -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")))))))) diff --git a/frontend/src/app/main/ui/shortcuts.cljs b/frontend/src/app/main/ui/shortcuts.cljs index bc9753305e..f2b9b74727 100644 --- a/frontend/src/app/main/ui/shortcuts.cljs +++ b/frontend/src/app/main/ui/shortcuts.cljs @@ -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]}] diff --git a/frontend/src/app/main/ui/shortcuts.scss b/frontend/src/app/main/ui/shortcuts.scss index cc78a03214..7d582e4e71 100644 --- a/frontend/src/app/main/ui/shortcuts.scss +++ b/frontend/src/app/main/ui/shortcuts.scss @@ -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); diff --git a/frontend/translations/en.po b/frontend/translations/en.po index c5782eea06..1224a573bf 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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 "" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 6543deb807..33d62837fd 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -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 ""