From 52825f528c2c06ede2ebb42ad041f45857bc5181 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Mon, 21 Sep 2026 12:29:14 +0200 Subject: [PATCH] :recycle: Update import modal UI (#11704) * :globe_with_meridians: Translate import dialog strings Several plain hardcoded strings in the import dialog (the file rename aria-label, library-resolution messages, table headers, and the manually-linked summary labels) were never wired to the i18n system. Wire them up via tr and add the corresponding English and Spanish entries to the translation catalogs. * :bug: Fix library-resolution summary layout in import dialog The auto-linked/manually-linked summary list had several layout bugs: name/selection columns stretched unevenly, the auto-linked badge sat nested inside the name's ellipsis text (unreliable across browsers), and a long unbreakable library name could grow the whole modal past its fixed width because a bare 1fr grid track has no minimum-size cap. Fix the column sizing, split the badge out as a sibling of the name, and clamp the modal content column with minmax(0, 1fr). * :sparkles: Add Skip button to library resolution wizard Add a "Skip" action to the per-file library resolution step so users can leave all of a file's pending libraries unconnected and move on to the next one, instead of being forced to pick a candidate or go back. Clears any pending selections for the current file before advancing the wizard, so the summary correctly shows those libraries as unlinked. * :bug: Fix stale translation key on library resolution button The primary action button in the library resolution wizard referenced "dashboard.import.review-links", a translation key that no longer existed, leaving the button with no text. Rename it to "dashboard.import.connect-selected-libraries" to match the actual button label. * :bug: Align no-selection state with arrow and file name .summary-no-selection was missing display:flex/align-items:center, so its icon and text weren't vertically centered against each other or against the file name column in the import library summary. * :lipstick: Use select-accent color for auto-linked badge border * :lipstick: Change import summary columns from 50/50 to 40/60 split Give the "new library" column more room than the "original library" column in the manually-linked libraries table (header and rows), since it usually carries the longer "name (project)" text. * :bug: Divide back buttons from forward buttons * :bug: Fix dropdown typography --- .../src/app/main/ui/dashboard/import.cljs | 139 ++++++++++-------- .../src/app/main/ui/dashboard/import.scss | 115 +++++++++------ .../main/ui/ds/controls/shared/option.scss | 2 + frontend/translations/en.po | 36 ++++- frontend/translations/es.po | 36 ++++- 5 files changed, 216 insertions(+), 112 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 882adcbb35..2558ad4ebd 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -293,8 +293,7 @@ [:input {:type "text" :auto-focus true :class (stl/css :file-name-input) - ;;TODO: Add translation for aria-label - :aria-label "File name" + :aria-label (tr "dashboard.import.file-name-label") :default-value (:name entry) :on-key-press on-edit-key-press :on-blur on-edit-blur}]] @@ -412,7 +411,7 @@ [:> text* {:class (stl/css :library-resolution-message) :as "p" :typography t/body-large} - "Some libraries couldn't be linked automatically. Select the correct library for each:"] + (tr "dashboard.import.resolve-libraries")] [:table {:class (stl/css :library-resolution-table)} @@ -422,13 +421,13 @@ [:> icon* {:icon-id i/library :class (stl/css :library-resolution-icon) :size "s"}] - "original library"] + (tr "dashboard.import.resolve-libraries.original-library")] [:th {:class (stl/css :library-resolution-arrow)}] [:th {:class (stl/css :library-resolution-connection)} [:> icon* {:icon-id i/library :class (stl/css :library-resolution-icon) :size "s"}] - "connect to"]]] + (tr "dashboard.import.resolve-libraries.connect-to")]]] [:tbody {:class (stl/css :library-resolution-body)} (for [{:keys [id name candidates]} candidates] (let [options (mapv (fn [c] @@ -477,54 +476,53 @@ :typography t/body-medium} (:name resolution-file)]] - (when (seq done) - [:div {:class (stl/css :summary-section)} - [:ul {:class (stl/css :summary-list)} - (for [{:keys [name]} done] - [:li {:class (stl/css :summary-list-item) - :key (dm/str name)} - [:span {:class (stl/css :summary-item-name)} name] - [:span {:class (stl/css :summary-linked-badge)} - [:> icon* {:icon-id i/status-tick - :class (stl/css :summary-badge-icon) - :size "s"}] - (tr "dashboard.import.summary.linked")]])]]) + [:div {:class (stl/css :summary-body)} + (when (seq done) + [:div {:class (stl/css :summary-section)} + [:ul {:class (stl/css :summary-list)} + (for [{:keys [name]} done] + [:li {:class (stl/css :summary-list-item) + :key (dm/str name)} + [:span {:class (stl/css :summary-item-name)} name] + [:span {:class (stl/css :summary-linked-badge)} + (tr "dashboard.import.summary.linked")]])]]) - (when (seq pending) - [:div {:class (stl/css :summary-section)} - [:div {:class (stl/css :summary-section-header)} - ;; TODO: Add translation for this string + (when (seq pending) + [:div {:class (stl/css :summary-section)} + [:div {:class (stl/css :summary-section-header)} + [:> text* {:as "span" + :class (stl/css :summary-section-title) + :typography t/headline-small} + (tr "dashboard.import.summary.manually-linked")]] + [:ul {:class (stl/css :summary-list)} + [:li {:class (stl/css :summary-list-item) + :key "summary-list-header"} + [:span {:class (stl/css :summary-item-name-header)} + (tr "dashboard.import.summary.original")] - [:> text* {:as "span" - :class (stl/css :summary-section-title) - :typography t/headline-small} - "linked manually"]] - [:ul {:class (stl/css :summary-list)} - [:li {:class (stl/css :summary-list-item) - :key "summary-list-header"} - [:span {:class (stl/css :summary-item-name-header)} - "Original"] + [:span {:class (stl/css :summary-item-name-header)} + (tr "dashboard.import.summary.new")]] + (for [{:keys [id name] :as cand} pending] + (let [selected-id (get selection id) + selected-c (when selected-id + (d/seek #(= (str (:id %)) (str selected-id)) (:candidates cand)))] + [:li {:class (stl/css :summary-list-item) + :key (dm/str id)} + [:span {:class (stl/css-case :summary-item-name true + :summary-item-base true)} name] - [:span {:class (stl/css :summary-item-name-header)} - "New"]] - (for [{:keys [id name] :as cand} pending] - (let [selected-id (get selection id) - selected-c (when selected-id - (d/seek #(= (str (:id %)) (str selected-id)) (:candidates cand)))] - [:li {:class (stl/css :summary-list-item) - :key (dm/str id)} - [:span {:class (stl/css :summary-item-name)} name] - [:> icon* {:icon-id i/row - :size "m" - :class (stl/css :summary-linked-arrow)}] - (if selected-c - [:span {:class (stl/css :summary-linked-info)} - [:span {:class (stl/css :summary-linked-name)} - (:name selected-c)] - [:span {:class (stl/css :summary-linked-project)} - (:project-name selected-c)]] - [:span {:class (stl/css :summary-no-selection)} - (tr "dashboard.import.summary.no-selection")])]))]])])) + (if selected-c + [:span {:class (stl/css :summary-linked-info)} + [:> icon* {:icon-id i/row + :size "m" + :class (stl/css :summary-linked-arrow)}] + [:span {:class (stl/css :summary-linked-name)} + (dm/str (:name selected-c) " (" (:project-name selected-c) ")")]] + [:span {:class (stl/css :summary-no-selection)} + [:> icon* {:icon-id i/row + :size "m" + :class (stl/css :summary-linked-arrow)}] + (tr "dashboard.import.summary.no-selection")])]))]])]])) (mf/defc library-resolution-summary* {::mf/private true} @@ -649,7 +647,7 @@ {::mf/private true} [{:keys [current-unresolved-file selection on-select visited all-visited? - on-wizard-prev on-wizard-next]}] + on-wizard-prev on-wizard-next on-wizard-skip]}] [:* [:div {:class (stl/css :modal-content)} [:> library-resolution* @@ -664,12 +662,18 @@ :variant "secondary" :on-click on-wizard-prev} (tr "labels.previous")]) - [:> button* {:class (stl/css :accept-btn) - :variant "primary" - :on-click on-wizard-next} - (if all-visited? - (tr "labels.next") - (tr "dashboard.import.review-links"))]]]]) + [:div {:class (stl/css :action-buttons-end)} + [:> button* {:class (stl/css :secondary-btn) + :variant "secondary" + :on-click on-wizard-skip} + (tr "labels.skip")] + + [:> button* {:class (stl/css :accept-btn) + :variant "primary" + :on-click on-wizard-next} + (if all-visited? + (tr "labels.next") + (tr "dashboard.import.connect-selected-libraries"))]]]]]) (mf/defc import-library-summary-stage* {::mf/private true} @@ -688,10 +692,11 @@ :variant "secondary" :on-click on-summary-back} (tr "labels.back")]) - [:> button* {:class (stl/css :accept-btn) - :variant "primary" - :on-click on-confirm-library-links} - (tr "dashboard.import.confirm-library-links")]]]]) + [:div {:class (stl/css :action-buttons-end)} + [:> button* {:class (stl/css :accept-btn) + :variant "primary" + :on-click on-confirm-library-links} + (tr "dashboard.import.confirm-library-links")]]]]]) (mf/defc import-dialog {::mf/register modal/components @@ -855,6 +860,15 @@ (let [file-id (:id current-unresolved-file)] (swap! visited* disj file-id)))) + on-wizard-skip + (mf/use-fn + (mf/deps current-unresolved-file) + (fn [] + (let [file-id (:id current-unresolved-file) + pending-ids (mapv :id (:pending current-unresolved-file))] + (swap! selection* #(apply dissoc % pending-ids)) + (swap! visited* conj file-id)))) + on-summary-back (mf/use-fn (mf/deps visited) @@ -971,7 +985,8 @@ :visited visited :all-visited? all-visited? :on-wizard-prev on-wizard-prev - :on-wizard-next on-wizard-next}] + :on-wizard-next on-wizard-next + :on-wizard-skip on-wizard-skip}] :library-summary [:> import-library-summary-stage* diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index 6866eb381f..35889b8389 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -42,9 +42,15 @@ flex: 1; overflow: hidden auto; display: grid; - grid-template-columns: 1fr; + + // minmax(0, 1fr), not a bare 1fr: a bare 1fr track's minimum size + // defaults to its content's min-content width, so a long unbreakable + // string deep inside (e.g. a library name with no spaces) can grow + // this column - and the fixed-width modal around it - past its + // intended size, since overflow:hidden can't shrink a track that + // already expanded to fit its content. + grid-template-columns: minmax(0, 1fr); gap: var(--sp-l); - margin-block-end: var(--sp-xxl); min-block-size: px2rem(40); } @@ -71,6 +77,15 @@ gap: var(--sp-l); } +// Groups the trailing buttons (e.g. skip/accept) and pins them to the end, +// leaving any leading button (e.g. previous/back) at the start - regardless +// of whether that leading button is rendered. +.action-buttons-end { + display: flex; + gap: var(--sp-l); + margin-inline-start: auto; +} + .file-entry { --file-entry-fg-color: var(--color-foreground-secondary); @@ -246,17 +261,20 @@ } .library-resolution-message { + @include use-typography("body-large"); + color: var(--color-foreground-secondary); margin-block-end: var(--sp-s); } .library-resolution-header { - @include use-typography("body-medium"); + @include use-typography("body-small"); color: var(--color-foreground-secondary); display: grid; - grid-template-columns: 1fr 32px 1fr; - border-block-end: $b-1 solid var(--color-foreground-secondary); + font-weight: 500; + grid-template-columns: 1fr $sz-32 1fr; + border-block-end: $b-1 solid var(--color-background-quaternary); } .library-origin-name, @@ -270,9 +288,10 @@ .library-resolution-item { display: grid; - grid-template-columns: 1fr 32px 1fr; - block-size: $sz-32; - margin-block: var(--sp-s); + grid-template-columns: 1fr $sz-32 1fr; + block-size: $sz-40; + margin-block-start: var(--sp-s); + border-block-end: $b-1 solid var(--color-background-quaternary); } .library-resolution-icon { @@ -330,6 +349,12 @@ background: var(--color-background-primary); } +.summary-body { + display: flex; + flex-direction: column; + gap: var(--sp-xxl); +} + .summary-list { display: flex; flex-direction: column; @@ -354,6 +379,7 @@ .summary-linked-arrow { color: var(--color-foreground-secondary); + flex-shrink: 0; } .summary-file-icon { @@ -383,36 +409,39 @@ .summary-list-item { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; gap: var(--sp-s); - padding: var(--sp-xs) var(--sp-s); + padding-block: var(--sp-xs); + padding-inline-start: var(--sp-s); border-radius: $br-4; - - &:hover { - background: var(--color-background-secondary); - } } .summary-item-name { + @include use-typography("body-small"); + color: var(--color-foreground-primary); - flex: 1; + flex: 0 1 auto; min-inline-size: 0; display: block; - max-inline-size: 99%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .summary-item-name-header { + @include use-typography("body-small"); + color: var(--color-foreground-secondary); - flex: 1; + flex: 2 1 0%; min-inline-size: 0; display: block; - max-inline-size: 99%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + + &:last-child { + flex: 3 1 0%; + } } // Auto-linked badge @@ -420,10 +449,12 @@ display: inline-flex; align-items: center; gap: var(--sp-xs); - padding: var(--sp-xxs) var(--sp-s); - border-radius: $br-12; + padding-block: var(--sp-xxs); + padding-inline: var(--sp-s); + border-radius: $br-8; background: var(--color-accent-success-bg); color: var(--color-accent-success); + border: $b-1 solid var(--color-accent-background-select); font-size: px2rem(11); font-weight: 500; flex-shrink: 0; @@ -437,39 +468,39 @@ .summary-linked-info { display: flex; align-items: center; - gap: var(--sp-xs); - flex-shrink: 0; + gap: var(--sp-s); + flex: 3 1 0%; + min-inline-size: 0; } .summary-linked-name { - // TODO: Typography does not match any existing DS typography token. - font-family: "worksans", "vazirmatn", sans-serif; - font-size: px2rem(12); - line-height: 1.4; - color: var(--color-foreground-secondary); + @include use-typography("body-small"); + + color: var(--color-foreground-primary); font-weight: 500; + min-inline-size: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.summary-linked-project { - // TODO: Typography does not match any existing DS typography token. - font-family: "worksans", "vazirmatn", sans-serif; - font-weight: 400; - line-height: 1.4; +.summary-item-base { + flex: 2 1 0%; color: var(--color-foreground-secondary); - padding: var(--sp-xxs) var(--sp-s); - border-radius: $br-12; - background: var(--color-background-quaternary); - font-size: px2rem(11); } // No selection state .summary-no-selection { - // TODO: Typography does not match any existing DS typography token. - font-family: "worksans", "vazirmatn", sans-serif; - font-size: px2rem(12); - font-weight: 400; - line-height: 1.4; + @include use-typography("body-small"); + + display: flex; + align-items: center; + gap: var(--sp-s); color: var(--color-foreground-secondary); font-style: italic; - flex-shrink: 0; + flex: 3 1 0%; + min-inline-size: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/frontend/src/app/main/ui/ds/controls/shared/option.scss b/frontend/src/app/main/ui/ds/controls/shared/option.scss index c604eb0542..550ece7c60 100644 --- a/frontend/src/app/main/ui/ds/controls/shared/option.scss +++ b/frontend/src/app/main/ui/ds/controls/shared/option.scss @@ -44,6 +44,8 @@ } .option-text { + @include use-typography("body-small"); + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 422810133a..90c8cbe2ee 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -788,6 +788,10 @@ msgstr "Confirm library links" msgid "dashboard.import.error" msgstr "Import failed. Please try again" +#: src/app/main/ui/dashboard/import.cljs:296 +msgid "dashboard.import.file-name-label" +msgstr "File name" + #: src/app/main/ui/dashboard/import.cljs:292 #, unused msgid "dashboard.import.import-error" @@ -869,9 +873,17 @@ msgstr "" msgid "dashboard.import.resolve-libraries-summary" msgstr "Review the library links before confirming:" -#: src/app/main/ui/dashboard/import.cljs:672 -msgid "dashboard.import.review-links" -msgstr "Review links" +#: src/app/main/ui/dashboard/import.cljs:430 +msgid "dashboard.import.resolve-libraries.connect-to" +msgstr "connect to" + +#: src/app/main/ui/dashboard/import.cljs:424 +msgid "dashboard.import.resolve-libraries.original-library" +msgstr "original library" + +#: src/app/main/ui/dashboard/import.cljs:675 +msgid "dashboard.import.connect-selected-libraries" +msgstr "Connect selected libraries" #, unused msgid "dashboard.import.summary.auto-linked" @@ -879,12 +891,24 @@ msgstr "Auto-linked" #: src/app/main/ui/dashboard/import.cljs:491 msgid "dashboard.import.summary.linked" -msgstr "Linked" +msgstr "auto-linked" + +#: src/app/main/ui/dashboard/import.cljs:498 +msgid "dashboard.import.summary.manually-linked" +msgstr "linked manually" + +#: src/app/main/ui/dashboard/import.cljs:506 +msgid "dashboard.import.summary.new" +msgstr "New" #: src/app/main/ui/dashboard/import.cljs:527 msgid "dashboard.import.summary.no-selection" msgstr "No library selected" +#: src/app/main/ui/dashboard/import.cljs:503 +msgid "dashboard.import.summary.original" +msgstr "Original" + #, unused msgid "dashboard.import.summary.your-selection" msgstr "Your selection" @@ -3719,6 +3743,10 @@ msgstr "Show only your comments" msgid "labels.sketch" msgstr "Sketch" +#: src/app/main/ui/dashboard/import.cljs:668 +msgid "labels.skip" +msgstr "Skip" + #: src/app/main/ui/dashboard/sidebar.cljs:1144 msgid "labels.sources" msgstr "Sources" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index a2aea8f9a4..03cf573dc3 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -786,6 +786,10 @@ msgstr "Confirmar vínculos de biblioteca" msgid "dashboard.import.error" msgstr "La importación ha fallado. Intentalo de nuevo, por favor" +#: src/app/main/ui/dashboard/import.cljs:296 +msgid "dashboard.import.file-name-label" +msgstr "Nombre del archivo" + #: src/app/main/ui/dashboard/import.cljs:292 #, unused msgid "dashboard.import.import-error" @@ -867,9 +871,17 @@ msgstr "" msgid "dashboard.import.resolve-libraries-summary" msgstr "Revisa los vínculos de biblioteca antes de confirmar:" -#: src/app/main/ui/dashboard/import.cljs:672 -msgid "dashboard.import.review-links" -msgstr "Revisar vínculos" +#: src/app/main/ui/dashboard/import.cljs:430 +msgid "dashboard.import.resolve-libraries.connect-to" +msgstr "conectar a" + +#: src/app/main/ui/dashboard/import.cljs:424 +msgid "dashboard.import.resolve-libraries.original-library" +msgstr "biblioteca original" + +#: src/app/main/ui/dashboard/import.cljs:675 +msgid "dashboard.import.connect-selected-libraries" +msgstr "Conectar bibliotecas seleccionadas" #, unused msgid "dashboard.import.summary.auto-linked" @@ -877,12 +889,24 @@ msgstr "Vinculadas automáticamente" #: src/app/main/ui/dashboard/import.cljs:491 msgid "dashboard.import.summary.linked" -msgstr "Vinculada" +msgstr "auto-vinculada" + +#: src/app/main/ui/dashboard/import.cljs:498 +msgid "dashboard.import.summary.manually-linked" +msgstr "vinculada manualmente" + +#: src/app/main/ui/dashboard/import.cljs:506 +msgid "dashboard.import.summary.new" +msgstr "Nueva" #: src/app/main/ui/dashboard/import.cljs:527 msgid "dashboard.import.summary.no-selection" msgstr "Ninguna biblioteca seleccionada" +#: src/app/main/ui/dashboard/import.cljs:503 +msgid "dashboard.import.summary.original" +msgstr "Original" + #, unused msgid "dashboard.import.summary.your-selection" msgstr "Tu selección" @@ -3620,6 +3644,10 @@ msgstr "Mostrar sólo tus comentarios" msgid "labels.sketch" msgstr "Sketch" +#: src/app/main/ui/dashboard/import.cljs:668 +msgid "labels.skip" +msgstr "Saltar" + #: src/app/main/ui/dashboard/sidebar.cljs:1144 msgid "labels.sources" msgstr "Recursos"