🐛 Fix ambiguation on import modal

This commit is contained in:
Eva Marco 2026-09-16 14:22:53 +02:00
parent 76e9289fcf
commit 20243b82f4
4 changed files with 111 additions and 44 deletions

View File

@ -390,25 +390,55 @@
(mf/defc library-resolution* (mf/defc library-resolution*
{::mf/private true} {::mf/private true}
[{:keys [unresolved-file selection on-select]}] [{:keys [unresolved-file selection on-select on-disconnect]}]
(let [candidates (:pending unresolved-file) (let [file-id (:id unresolved-file)
disconnected* (mf/use-state #{}) candidates (:pending unresolved-file)
disconnected (deref disconnected*) file-selection (get selection file-id)
on-select-file
(mf/use-fn
(mf/deps file-id on-select)
(partial on-select file-id))
on-disconnect-file
(mf/use-fn
(mf/deps file-id on-disconnect)
(partial on-disconnect file-id))
on-change-disconnected on-change-disconnected
(mf/use-fn (mf/use-fn
(mf/deps file-selection candidates on-select-file on-disconnect-file)
(fn [id] (fn [id]
(swap! disconnected* (if (contains? file-selection id)
(fn [s] (on-disconnect-file id)
(if (contains? s id) (disj s id) (conj s id))))))] (let [{:keys [candidates]} (d/seek #(= id (:id %)) candidates)]
(when-let [first-c (first candidates)]
(on-select-file id (str (:id first-c))))))))]
;; Pre-select first candidate for each library ;; Pre-select a candidate for each of this file's pending libraries not
(mf/with-effect [candidates] ;; yet resolved for this file: reuse another file's choice for the same
;; source library when one already exists (so files sharing a library
;; default to the same target), otherwise fall back to the first
;; candidate. Each file keeps its own entry in `selection`, so this is
;; only ever a starting point - it never overwrites another file's pick.
(mf/with-effect [candidates file-id]
(doseq [{:keys [id candidates]} candidates] (doseq [{:keys [id candidates]} candidates]
(when-not (contains? selection id) (when-not (contains? file-selection id)
(when-let [first-c (first candidates)] (let [other-choice (some #(get % id) (vals selection))
(on-select id (str (:id first-c))))))) default-id (or other-choice (some-> candidates first :id str))]
(when default-id
(on-select-file id default-id))))))
[:div {:class (stl/css :library-resolution)} [:div {:class (stl/css :library-resolution)}
[:div {:class (stl/css :library-resolution-file-header)}
[:> icon* {:icon-id i/document
:class (stl/css :library-resolution-file-icon)
:size "s"}]
[:> text* {:class (stl/css :library-resolution-file-name)
:as "span"
:typography t/body-medium}
(:name unresolved-file)]]
[:> text* {:class (stl/css :library-resolution-message) [:> text* {:class (stl/css :library-resolution-message)
:as "p" :as "p"
:typography t/body-large} :typography t/body-large}
@ -435,8 +465,8 @@
{:id (str (:id c)) {:id (str (:id c))
:label (str (:name c) " (" (:project-name c) ")")}) :label (str (:name c) " (" (:project-name c) ")")})
candidates) candidates)
selected (get selection id) selected (get file-selection id)
is-conected (not (contains? disconnected id))] is-conected (contains? file-selection id)]
[:tr {:class (stl/css :library-resolution-item) [:tr {:class (stl/css :library-resolution-item)
:key (dm/str id)} :key (dm/str id)}
[:td {:class (stl/css :library-resolution-item-name)} [:td {:class (stl/css :library-resolution-item-name)}
@ -453,7 +483,7 @@
:class (stl/css :library-resolution-select) :class (stl/css :library-resolution-select)
:default-selected (or (some-> selected str) "") :default-selected (or (some-> selected str) "")
:has-portal true :has-portal true
:on-change (partial on-select id)}] :on-change (partial on-select-file id)}]
[:> text* {:class (stl/css :library-resolution-no-selection) [:> text* {:class (stl/css :library-resolution-no-selection)
:as "span" :as "span"
@ -465,8 +495,9 @@
(mf/defc library-resolution-summary-file* (mf/defc library-resolution-summary-file*
{::mf/private true} {::mf/private true}
[{:keys [resolution-file selection]}] [{:keys [resolution-file selection]}]
(let [done (:done resolution-file) (let [done (:done resolution-file)
pending (:pending resolution-file)] pending (:pending resolution-file)
file-selection (get selection (:id resolution-file))]
[:div {:class (stl/css :summary-file)} [:div {:class (stl/css :summary-file)}
[:div {:class (stl/css :summary-file-header)} [:div {:class (stl/css :summary-file-header)}
[:> icon* {:icon-id i/document [:> icon* {:icon-id i/document
@ -508,7 +539,7 @@
[:span {:class (stl/css :summary-item-name-header)} [:span {:class (stl/css :summary-item-name-header)}
"New"]] "New"]]
(for [{:keys [id name] :as cand} pending] (for [{:keys [id name] :as cand} pending]
(let [selected-id (get selection id) (let [selected-id (get file-selection id)
selected-c (when selected-id selected-c (when selected-id
(d/seek #(= (str (:id %)) (str selected-id)) (:candidates cand)))] (d/seek #(= (str (:id %)) (str selected-id)) (:candidates cand)))]
[:li {:class (stl/css :summary-list-item) [:li {:class (stl/css :summary-list-item)
@ -647,15 +678,16 @@
(mf/defc import-library-resolution-stage* (mf/defc import-library-resolution-stage*
{::mf/private true} {::mf/private true}
[{:keys [current-unresolved-file selection on-select [{:keys [current-unresolved-file selection on-select on-disconnect
visited all-visited? visited last-file?
on-wizard-prev on-wizard-next]}] on-wizard-prev on-wizard-next]}]
[:* [:*
[:div {:class (stl/css :modal-content)} [:div {:class (stl/css :modal-content)}
[:> library-resolution* [:> library-resolution*
{:unresolved-file current-unresolved-file {:unresolved-file current-unresolved-file
:selection selection :selection selection
:on-select on-select}]] :on-select on-select
:on-disconnect on-disconnect}]]
[:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)} [:div {:class (stl/css :action-buttons)}
@ -667,9 +699,9 @@
[:> button* {:class (stl/css :accept-btn) [:> button* {:class (stl/css :accept-btn)
:variant "primary" :variant "primary"
:on-click on-wizard-next} :on-click on-wizard-next}
(if all-visited? (if last-file?
(tr "labels.next") (tr "dashboard.import.review-links")
(tr "dashboard.import.review-links"))]]]]) (tr "dashboard.import.next-file"))]]]])
(mf/defc import-library-summary-stage* (mf/defc import-library-summary-stage*
{::mf/private true} {::mf/private true}
@ -717,7 +749,9 @@
resolution* (mf/use-state nil) resolution* (mf/use-state nil)
resolution (not-empty (deref resolution*)) resolution (not-empty (deref resolution*))
;; User selection for multi-match candidates: {old-lib-id candidate-id} ;; User selection for multi-match candidates, scoped per file so two
;; files referencing the same source library can each resolve it to
;; a different target: {file-id {old-lib-id candidate-id}}
selection* (mf/use-state {}) selection* (mf/use-state {})
selection (deref selection*) selection (deref selection*)
@ -738,16 +772,21 @@
[] []
resolution))) resolution)))
all-visited?
(mf/with-memo [visited unresolved-files]
(when (seq unresolved-files)
(every? #(contains? visited (:id %)) unresolved-files)))
;; Current file shown in the wizard step: first unresolved file not yet visited. ;; Current file shown in the wizard step: first unresolved file not yet visited.
current-unresolved-file current-unresolved-file
(mf/with-memo [unresolved-files visited] (mf/with-memo [unresolved-files visited]
(d/seek #(not (contains? visited (:id %))) unresolved-files)) (d/seek #(not (contains? visited (:id %))) unresolved-files))
;; Whether the file currently shown is the last one still needing
;; resolution: true once every other unresolved file has already
;; been visited, so moving past this one reaches the summary.
last-unresolved-file?
(mf/with-memo [unresolved-files visited current-unresolved-file]
(when (some? current-unresolved-file)
(every? #(or (= (:id %) (:id current-unresolved-file))
(contains? visited (:id %)))
unresolved-files)))
continue-entries continue-entries
(mf/use-fn (mf/use-fn
(mf/deps entries) (mf/deps entries)
@ -830,7 +869,7 @@
(->> (rx/from (:pending resolution-file)) (->> (rx/from (:pending resolution-file))
(rx/merge-map (rx/merge-map
(fn [{:keys [id]}] (fn [{:keys [id]}]
(when-let [selected-lib (get slc id)] (when-let [selected-lib (get-in slc [file-id id])]
(link-files-to-library! [file-id] selected-lib))))))) (link-files-to-library! [file-id] selected-lib)))))))
(rx/subs! (constantly nil) (rx/subs! (constantly nil)
(constantly nil) (constantly nil)
@ -848,12 +887,12 @@
on-wizard-prev on-wizard-prev
(mf/use-fn (mf/use-fn
(mf/deps current-unresolved-file) (mf/deps visited)
(fn [] (fn []
;; Remove the current file from visited; it becomes current again after re-render, ;; Remove the last-visited file from visited; it becomes current
;; because it's no longer in visited. ;; again after re-render, because it's no longer in visited.
(let [file-id (:id current-unresolved-file)] (let [last-id (last visited)]
(swap! visited* disj file-id)))) (swap! visited* disj last-id))))
on-summary-back on-summary-back
(mf/use-fn (mf/use-fn
@ -897,9 +936,13 @@
manage-on-select manage-on-select
(mf/use-fn (mf/use-fn
(mf/deps selection) (fn [file-id old-lib-id candidate-id]
(fn [old-lib-id candidate-id] (swap! selection* assoc-in [file-id old-lib-id] candidate-id)))
(swap! selection* assoc old-lib-id candidate-id)))]
manage-on-disconnect
(mf/use-fn
(fn [file-id old-lib-id]
(swap! selection* update file-id (fnil dissoc {}) old-lib-id)))]
(mf/with-effect [visited unresolved-files] (mf/with-effect [visited unresolved-files]
(when (and (seq unresolved-files) (when (and (seq unresolved-files)
@ -968,8 +1011,9 @@
{:current-unresolved-file current-unresolved-file {:current-unresolved-file current-unresolved-file
:selection selection :selection selection
:on-select manage-on-select :on-select manage-on-select
:on-disconnect manage-on-disconnect
:visited visited :visited visited
:all-visited? all-visited? :last-file? last-unresolved-file?
:on-wizard-prev on-wizard-prev :on-wizard-prev on-wizard-prev
:on-wizard-next on-wizard-next}] :on-wizard-next on-wizard-next}]

View File

@ -250,6 +250,21 @@
margin-block-end: var(--sp-s); margin-block-end: var(--sp-s);
} }
.library-resolution-file-header {
display: flex;
align-items: center;
gap: var(--sp-s);
}
.library-resolution-file-icon {
color: var(--color-foreground-secondary);
flex-shrink: 0;
}
.library-resolution-file-name {
color: var(--color-foreground-primary);
}
.library-resolution-header { .library-resolution-header {
@include use-typography("body-medium"); @include use-typography("body-medium");

View File

@ -827,6 +827,10 @@ msgstr[1] "%s files have been imported successfully."
msgid "dashboard.import.import-warning" msgid "dashboard.import.import-warning"
msgstr "Some files containted invalid objects that have been removed." msgstr "Some files containted invalid objects that have been removed."
#: src/app/main/ui/dashboard/import.cljs:704
msgid "dashboard.import.next-file"
msgstr "Next file"
#: src/app/main/ui/dashboard.cljs:265 #: src/app/main/ui/dashboard.cljs:265
msgid "dashboard.import.no-perms" msgid "dashboard.import.no-perms"
msgstr "You dont have permission to import to this team" msgstr "You dont have permission to import to this team"
@ -869,9 +873,9 @@ msgstr ""
msgid "dashboard.import.resolve-libraries-summary" msgid "dashboard.import.resolve-libraries-summary"
msgstr "Review the library links before confirming:" msgstr "Review the library links before confirming:"
#: src/app/main/ui/dashboard/import.cljs:672 #: src/app/main/ui/dashboard/import.cljs:703
msgid "dashboard.import.review-links" msgid "dashboard.import.review-links"
msgstr "Review links" msgstr "Review library links"
#, unused #, unused
msgid "dashboard.import.summary.auto-linked" msgid "dashboard.import.summary.auto-linked"

View File

@ -825,6 +825,10 @@ msgstr[1] "%s ficheros se han importado correctamente."
msgid "dashboard.import.import-warning" msgid "dashboard.import.import-warning"
msgstr "Algunos ficheros contenían objetos erroneos que no han sido importados." msgstr "Algunos ficheros contenían objetos erroneos que no han sido importados."
#: src/app/main/ui/dashboard/import.cljs:704
msgid "dashboard.import.next-file"
msgstr "Siguiente archivo"
#: src/app/main/ui/dashboard.cljs:265 #: src/app/main/ui/dashboard.cljs:265
msgid "dashboard.import.no-perms" msgid "dashboard.import.no-perms"
msgstr "No tienes permisos para importar en este equipo" msgstr "No tienes permisos para importar en este equipo"
@ -867,9 +871,9 @@ msgstr ""
msgid "dashboard.import.resolve-libraries-summary" msgid "dashboard.import.resolve-libraries-summary"
msgstr "Revisa los vínculos de biblioteca antes de confirmar:" msgstr "Revisa los vínculos de biblioteca antes de confirmar:"
#: src/app/main/ui/dashboard/import.cljs:672 #: src/app/main/ui/dashboard/import.cljs:703
msgid "dashboard.import.review-links" msgid "dashboard.import.review-links"
msgstr "Revisar vínculos" msgstr "Revisar vínculos de biblioteca"
#, unused #, unused
msgid "dashboard.import.summary.auto-linked" msgid "dashboard.import.summary.auto-linked"