mirror of
https://github.com/penpot/penpot.git
synced 2026-06-29 10:42:06 +00:00
WIP: plan for improvement
This commit is contained in:
parent
2979f354af
commit
73d9f60d6d
@ -913,58 +913,60 @@
|
||||
|
||||
(vswap! bfc/*state* update :index assoc id (:id sobject)))))))
|
||||
|
||||
(defn- resolve-external-libraries-references
|
||||
"For each external library in the manifest, look for matching shared
|
||||
files in the team by slugified name. Returns a map of
|
||||
old-lib-id -> [{:id uuid :name string}] for libraries with matches."
|
||||
[{:keys [::manifest ::bfc/team-id] :as cfg}]
|
||||
(defn- add-to-file
|
||||
"Add a resolved library entry to a file in the file-grouped resolution.
|
||||
`key` is :libraries (auto-linked) or :candidates (needs resolution)."
|
||||
[acc file-id key entry]
|
||||
(let [file-entry (get acc file-id {:file-id file-id :libraries [] :candidates []})]
|
||||
(assoc acc file-id (update file-entry key conj entry))))
|
||||
|
||||
(defn- resolve-and-link-libraries
|
||||
"For each external library in the manifest, resolve candidates by slug.
|
||||
Auto-links single matches (creating DB rows) and builds a file-grouped
|
||||
resolution map keyed by imported file-id (new UUID).
|
||||
|
||||
Returns {file-id {:file-id :libraries :candidates}}
|
||||
where :libraries = [{:id :name :linked-to}]
|
||||
and :candidates = [{:id :name :candidates [...]}]."
|
||||
[{:keys [::db/conn ::manifest ::bfc/team-id ::bfc/timestamp ::bfc/profile-id] :as cfg}
|
||||
file-ids]
|
||||
(assert (uuid? team-id) "team-id should be provided")
|
||||
(reduce (fn [result {:keys [id slug] :as lib}]
|
||||
(if-let [candidates (-> (bfc/find-shared-files-by-slug cfg team-id slug)
|
||||
(vec)
|
||||
(not-empty))]
|
||||
(assoc result id (assoc lib :candidates candidates))
|
||||
result))
|
||||
{}
|
||||
(:external-libraries manifest)))
|
||||
(reduce
|
||||
(fn [acc {:keys [id name slug used-by] :as _ext-lib}]
|
||||
(let [candidates (-> (bfc/find-shared-files-by-slug cfg team-id slug)
|
||||
(vec)
|
||||
(not-empty))]
|
||||
(cond
|
||||
;; No candidates → skip this library
|
||||
(nil? candidates)
|
||||
acc
|
||||
|
||||
(defn- auto-link-libraries
|
||||
"Auto-link imported files to libraries that have exactly one candidate
|
||||
match. Returns a vector of {:id old-lib-id :name string :linked-to uuid}
|
||||
for each auto-linked library."
|
||||
[{:keys [::db/conn ::manifest ::bfc/timestamp ::bfc/profile-id] :as cfg} resolution file-ids]
|
||||
(reduce-kv (fn [result id {:keys [name used-by candidates]}]
|
||||
(if (= 1 (count candidates))
|
||||
(let [candidate (first candidates)
|
||||
lib-id (get candidate :id)
|
||||
perms (bfc/get-file-permissions conn profile-id lib-id)]
|
||||
;; Single candidate → auto-link if importer has edit permission
|
||||
(= 1 (count candidates))
|
||||
(let [candidate (first candidates)
|
||||
lib-id (:id candidate)
|
||||
perms (bfc/get-file-permissions conn profile-id lib-id)]
|
||||
(if (not (:can-edit perms))
|
||||
acc ;; no edit permission → skip
|
||||
(let [used-by-set (into #{} (map bfc/lookup-index) used-by)
|
||||
linked-fids (filterv #(contains? used-by-set %) file-ids)]
|
||||
;; Create DB links for the files that used this library
|
||||
(doseq [file-id linked-fids]
|
||||
(let [rel-params {:file-id file-id :library-file-id lib-id}]
|
||||
(db/insert! conn :file-library-rel rel-params
|
||||
{::db/on-conflict-do-nothing? true})
|
||||
(bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp))))
|
||||
;; Add to file-grouped structure (reduce on [] returns acc)
|
||||
(let [entry {:id id :name name :linked-to lib-id}]
|
||||
(reduce #(add-to-file %1 %2 :libraries entry) acc linked-fids)))))
|
||||
|
||||
;; Only auto-link when the importer has edit permission
|
||||
;; on the matched library, matching the manual link RPC.
|
||||
(if (:can-edit perms)
|
||||
;; Link only files that actually used this library
|
||||
(let [used-by (into #{} (map bfc/lookup-index) used-by)
|
||||
file-ids (-> (filter #(contains? used-by %) file-ids)
|
||||
(not-empty))]
|
||||
|
||||
(doseq [file-id file-ids]
|
||||
(when (contains? used-by file-id)
|
||||
(let [rel-params {:file-id file-id
|
||||
:library-file-id lib-id}]
|
||||
(db/insert! conn :file-library-rel rel-params
|
||||
{::db/on-conflict-do-nothing? true})
|
||||
(bfc/upsert-file-library-sync! conn (assoc rel-params :synced-at timestamp)))))
|
||||
|
||||
(if file-ids
|
||||
(update result id (fn [lib]
|
||||
(-> lib
|
||||
(assoc :linked-to lib-id)
|
||||
(dissoc :candidates))))
|
||||
(dissoc result id)))
|
||||
result))
|
||||
result))
|
||||
resolution
|
||||
resolution))
|
||||
;; Multiple candidates → needs user resolution
|
||||
:else
|
||||
(let [new-fids (into [] (keep bfc/lookup-index) used-by)
|
||||
entry {:id id :name name :candidates candidates}]
|
||||
(reduce #(add-to-file %1 %2 :candidates entry) acc new-fids)))))
|
||||
{}
|
||||
(:external-libraries manifest)))
|
||||
|
||||
(defn- import-files*
|
||||
[{:keys [::manifest] :as cfg}]
|
||||
@ -984,15 +986,7 @@
|
||||
|
||||
(import-file-relations cfg)
|
||||
|
||||
;; Resolve external libraries by slug and auto-link single matches
|
||||
(let [resolution
|
||||
(resolve-external-libraries-references cfg)
|
||||
|
||||
resolution
|
||||
(auto-link-libraries cfg resolution file-ids)]
|
||||
|
||||
(app.common.pprint/pprint resolution)
|
||||
|
||||
(let [resolution (resolve-and-link-libraries cfg file-ids)]
|
||||
(bfm/apply-pending-migrations! cfg)
|
||||
{:file-ids file-ids
|
||||
:resolution resolution})))
|
||||
|
||||
@ -158,6 +158,19 @@
|
||||
(and (= :import-ready (:status item))
|
||||
(not (:deleted item))))
|
||||
|
||||
(defn- has-unresolved?
|
||||
"Return true if a file-resolution has any :candidates needing user choice."
|
||||
[file-resolution]
|
||||
(some? (seq (:candidates file-resolution))))
|
||||
|
||||
(defn- count-auto-linked
|
||||
"Count auto-linked libraries across all file resolutions."
|
||||
[resolution]
|
||||
(reduce-kv (fn [acc _ {:keys [libraries]}]
|
||||
(+ acc (count libraries)))
|
||||
0
|
||||
resolution))
|
||||
|
||||
(defn- analyze-entries
|
||||
[state entries]
|
||||
(let [features (get @st/state :features)]
|
||||
@ -343,18 +356,16 @@
|
||||
|
||||
(mf/defc library-resolution*
|
||||
{::mf/private true}
|
||||
[{:keys [resolution selections*]}]
|
||||
(let [external-libs (:external-libs resolution)
|
||||
library-candidates (:library-candidates resolution)
|
||||
selections (deref selections*)]
|
||||
[{:keys [file-resolution on-next selections*]}]
|
||||
(let [candidates (:candidates file-resolution)
|
||||
selections (deref selections*)]
|
||||
|
||||
;; Pre-select the first candidate for each unresolved library so that
|
||||
;; confirming without interaction still links the default choice.
|
||||
(mf/with-effect []
|
||||
(doseq [[old-lib-id candidates] library-candidates]
|
||||
(when-not (contains? @selections* old-lib-id)
|
||||
(when-let [first-candidate (first candidates)]
|
||||
(swap! selections* assoc old-lib-id (:id first-candidate))))))
|
||||
;; Pre-select first candidate for each library
|
||||
(mf/with-effect [candidates]
|
||||
(doseq [{:keys [id candidates]} candidates]
|
||||
(when-not (contains? @selections* id)
|
||||
(when-let [first-c (first candidates)]
|
||||
(swap! selections* assoc id (:id first-c))))))
|
||||
|
||||
(let [on-select
|
||||
(mf/use-fn
|
||||
@ -362,25 +373,49 @@
|
||||
(fn [old-lib-id candidate-id]
|
||||
(swap! selections* assoc old-lib-id candidate-id)))]
|
||||
|
||||
(when (seq library-candidates)
|
||||
[:div {:class (stl/css :library-resolution)}
|
||||
[:p {:class (stl/css :library-resolution-message)}
|
||||
(tr "dashboard.import.resolve-libraries")]
|
||||
[:div {:class (stl/css :library-resolution)}
|
||||
[:p {:class (stl/css :library-resolution-message)}
|
||||
(tr "dashboard.import.resolve-libraries")]
|
||||
|
||||
(for [{:keys [id name]} external-libs]
|
||||
(let [candidates (get library-candidates id)
|
||||
options (mapv (fn [c]
|
||||
{:id (str (:id c))
|
||||
:label (:name c)})
|
||||
candidates)
|
||||
selected (get selections id)]
|
||||
[:div {:class (stl/css :library-resolution-item)
|
||||
:key (dm/str id)}
|
||||
[:div {:class (stl/css :library-resolution-item-name)}
|
||||
name]
|
||||
[:> select* {:options options
|
||||
:default-selected (or (some-> selected str) "")
|
||||
:on-change (partial on-select id)}]]))]))))
|
||||
(for [{:keys [id name candidates]} candidates]
|
||||
(let [options (mapv (fn [c]
|
||||
{:id (str (:id c))
|
||||
:label (:name c)})
|
||||
candidates)
|
||||
selected (get selections id)]
|
||||
[:div {:class (stl/css :library-resolution-item)
|
||||
:key (dm/str id)}
|
||||
[:div {:class (stl/css :library-resolution-item-name)}
|
||||
name]
|
||||
[:> select* {:options options
|
||||
:default-selected (or (some-> selected str) "")
|
||||
:on-change (partial on-select id)}]]))])))
|
||||
|
||||
(mf/defc library-resolution-summary
|
||||
{::mf/private true}
|
||||
[{:keys [resolution selections]}]
|
||||
[:div {:class (stl/css :library-resolution)}
|
||||
[:p {:class (stl/css :library-resolution-message)}
|
||||
(tr "dashboard.import.resolve-libraries-summary")]
|
||||
|
||||
(for [[file-id file-res] resolution]
|
||||
[:div {:class (stl/css :library-resolution-summary-file)
|
||||
:key (dm/str file-id)}
|
||||
;; Show auto-linked libraries
|
||||
(for [{:keys [name linked-to]} (:libraries file-res)]
|
||||
[:div {:class (stl/css :library-resolution-item)}
|
||||
[:div {:class (stl/css :library-resolution-item-name)} name]
|
||||
[:div {:class (stl/css :library-resolution-linked)} "→ linked"]])
|
||||
|
||||
;; Show user-selected libraries
|
||||
(for [{:keys [id name] :as cand} (:candidates file-res)]
|
||||
(let [selected-id (get selections id)
|
||||
selected-c (when selected-id
|
||||
(d/seek #(= (:id %) selected-id) (:candidates cand)))]
|
||||
[:div {:class (stl/css :library-resolution-item)}
|
||||
[:div {:class (stl/css :library-resolution-item-name)} name]
|
||||
[:div {:class (stl/css :library-resolution-linked)}
|
||||
"→ " (:name selected-c)]]))])])
|
||||
|
||||
(mf/defc import-dialog
|
||||
{::mf/register modal/components
|
||||
@ -410,6 +445,24 @@
|
||||
library-selections* (mf/use-state {})
|
||||
library-selections (deref library-selections*)
|
||||
|
||||
;; Wizard progression as an ordered "visited" stack of file-ids.
|
||||
;; `current-file` is derived: the first unresolved file NOT yet in `visited`.
|
||||
;; No numeric step counter — forward = conj, back = pop.
|
||||
visited* (mf/use-state [])
|
||||
visited (deref visited*)
|
||||
visited-set (set visited)
|
||||
|
||||
;; Derived: files that need user resolution (have :candidates)
|
||||
unresolved-files
|
||||
(mf/use-memo [library-resolution]
|
||||
(when library-resolution
|
||||
(filterv has-unresolved? (vals library-resolution))))
|
||||
|
||||
;; Current file shown in the wizard step: first unresolved file not yet visited.
|
||||
current-file
|
||||
(mf/use-memo [unresolved-files visited-set]
|
||||
(d/seek #(not (contains? visited-set (:file-id %))) unresolved-files))
|
||||
|
||||
continue-entries
|
||||
(mf/use-fn
|
||||
(mf/deps entries)
|
||||
@ -484,16 +537,41 @@
|
||||
(mf/deps library-resolution library-selections*)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(let [selections @library-selections*
|
||||
file-ids (:file-ids library-resolution)]
|
||||
;; Link each user-selected library to all imported files
|
||||
(->> (rx/from (seq selections))
|
||||
(rx/merge-map (fn [[_old-lib-id candidate-id]]
|
||||
(link-files-to-library! file-ids candidate-id)))
|
||||
(rx/reduce conj [])
|
||||
(rx/subs! (fn [_]
|
||||
(let [selections @library-selections*]
|
||||
;; For each file with candidates, link it to the selected libraries
|
||||
(->> (rx/from (seq library-resolution))
|
||||
(rx/merge-map
|
||||
(fn [[file-id file-res]]
|
||||
(let [file-ids [file-id]] ;; this specific file's new UUID
|
||||
(->> (rx/from (:candidates file-res))
|
||||
(rx/merge-map
|
||||
(fn [{:keys [id]}]
|
||||
(when-let [selected-lib (get selections id)]
|
||||
(link-files-to-library! file-ids selected-lib))))))))
|
||||
(rx/subs! (fn []
|
||||
(reset! status* :import-success)))))))
|
||||
|
||||
on-wizard-next
|
||||
(mf/use-fn
|
||||
(mf/deps current-file unresolved-files visited*)
|
||||
(fn []
|
||||
(let [new-visited (conj @visited* (:file-id current-file))
|
||||
new-set (set new-visited)
|
||||
has-more? (some #(not (contains? new-set (:file-id %)))
|
||||
unresolved-files)]
|
||||
(reset! visited* new-visited)
|
||||
(when-not has-more?
|
||||
(reset! status* :library-summary)))))
|
||||
|
||||
on-wizard-prev
|
||||
(mf/use-fn
|
||||
(mf/deps visited*)
|
||||
(fn []
|
||||
;; Pop the last visited file-id; it becomes current again after re-render,
|
||||
;; because it's no longer in visited-set.
|
||||
(when (seq @visited*)
|
||||
(swap! visited* pop))))
|
||||
|
||||
on-accept
|
||||
(mf/use-fn
|
||||
(mf/deps on-finish-import)
|
||||
@ -538,7 +616,10 @@
|
||||
|
||||
(and (seq entries)
|
||||
(every? #(= :import-success (:status %)) entries))
|
||||
(reset! status* :import-success)
|
||||
(reset! status* (if (and (some? @library-resolution*)
|
||||
(seq (filter has-unresolved? (vals @library-resolution*))))
|
||||
:library-resolution
|
||||
:import-success))
|
||||
|
||||
(and (seq entries)
|
||||
(and (every? #(not= :import-ready (:status %)) entries)
|
||||
@ -581,17 +662,19 @@
|
||||
:class (stl/css :context-notification-error)
|
||||
:content (tr "dashboard.import.import-error.disclaimer")}])
|
||||
|
||||
(when (some? library-resolution)
|
||||
[:*
|
||||
[:& context-notification
|
||||
{:level :success
|
||||
:content (tr "dashboard.import.import-message" (i18n/c import-success-total))}]
|
||||
(when (pos? auto-linked-count)
|
||||
[:& context-notification
|
||||
{:level :success
|
||||
:content (tr "dashboard.import.auto-linked-libraries" (i18n/c auto-linked-count))}])
|
||||
[:> library-resolution* {:resolution library-resolution
|
||||
:selections* library-selections*}]])
|
||||
;; :library-resolution — wizard step (current derived file)
|
||||
(when (= :library-resolution status)
|
||||
(when (some? current-file)
|
||||
[:> library-resolution*
|
||||
{:file-resolution current-file
|
||||
:selections* library-selections*
|
||||
:on-next on-wizard-next}]))
|
||||
|
||||
;; :library-summary — show all files with final resolution
|
||||
(when (= :library-summary status)
|
||||
[:> library-resolution-summary
|
||||
{:resolution library-resolution
|
||||
:selections library-selections}])
|
||||
|
||||
(if (or (= :import-error status) (and (= :analyze status) errors?))
|
||||
[:div {:class (stl/css :import-error-disclaimer)}
|
||||
@ -648,11 +731,41 @@
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
(cond
|
||||
(some? library-resolution)
|
||||
[:input {:class (stl/css :accept-btn)
|
||||
:type "button"
|
||||
:value (tr "dashboard.import.confirm-library-links")
|
||||
:on-click on-confirm-library-links}]
|
||||
;; Wizard step: Next / Previous (Previous only shown if stack is non-empty)
|
||||
(= :library-resolution status)
|
||||
[:*
|
||||
(when (seq @visited*)
|
||||
[:input {:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:value (tr "labels.previous")
|
||||
:on-click on-wizard-prev}])
|
||||
;; Label flips to "Review" when this is the last unvisited unresolved file.
|
||||
[:input {:class (stl/css :accept-btn)
|
||||
:type "button"
|
||||
:value (if (some? current-file)
|
||||
;; Peek: is there another unvisited unresolved file after this one?
|
||||
(if (let [new-set (conj (set @visited*) (:file-id current-file))]
|
||||
(some #(not (contains? new-set (:file-id %))) unresolved-files))
|
||||
(tr "labels.next")
|
||||
(tr "dashboard.import.review-links"))
|
||||
(tr "dashboard.import.review-links"))
|
||||
:on-click on-wizard-next}]]
|
||||
|
||||
;; Summary: Confirm / Back
|
||||
;; Back pops the stack once and re-enters the wizard at the popped file.
|
||||
(= :library-summary status)
|
||||
[:*
|
||||
(when (seq @visited*)
|
||||
[:input {:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:value (tr "labels.back")
|
||||
:on-click (fn []
|
||||
(swap! visited* pop)
|
||||
(reset! status* :library-resolution))}])
|
||||
[:input {:class (stl/css :accept-btn)
|
||||
:type "button"
|
||||
:value (tr "dashboard.import.confirm-library-links")
|
||||
:on-click on-confirm-library-links}]]
|
||||
|
||||
(= :analyze status)
|
||||
[:input {:class (stl/css :cancel-button)
|
||||
|
||||
11
frontend/src/app/main/ui/dashboard/import.scss
vendored
11
frontend/src/app/main/ui/dashboard/import.scss
vendored
@ -300,3 +300,14 @@
|
||||
color: var(--modal-title-foreground-color);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.library-resolution-summary-file {
|
||||
margin-bottom: deprecated.$s-16;
|
||||
padding-bottom: deprecated.$s-12;
|
||||
border-bottom: 1px solid var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.library-resolution-linked {
|
||||
color: var(--color-foreground-secondary);
|
||||
font-size: deprecated.$fs-12;
|
||||
}
|
||||
|
||||
@ -243,5 +243,4 @@
|
||||
:file-id (:file-id entry)}))))))))))
|
||||
(->> (rx/defer #(deref resolutions))
|
||||
(rx/map (fn [resolutions]
|
||||
{:type :libraries-resolution
|
||||
:value resolutions})))))))
|
||||
{:libraries-resolution resolutions})))))))
|
||||
|
||||
@ -756,6 +756,23 @@ msgstr[1] "%s files have been imported successfully."
|
||||
msgid "dashboard.import.import-warning"
|
||||
msgstr "Some files containted invalid objects that have been removed."
|
||||
|
||||
msgid "dashboard.import.auto-linked-libraries"
|
||||
msgid_plural "dashboard.import.auto-linked-libraries"
|
||||
msgstr[0] "1 library was automatically linked by name."
|
||||
msgstr[1] "%s libraries were automatically linked by name."
|
||||
|
||||
msgid "dashboard.import.resolve-libraries"
|
||||
msgstr "Some libraries couldn't be linked automatically. Select the correct library for each:"
|
||||
|
||||
msgid "dashboard.import.resolve-libraries-summary"
|
||||
msgstr "Review the library links before confirming:"
|
||||
|
||||
msgid "dashboard.import.confirm-library-links"
|
||||
msgstr "Confirm library links"
|
||||
|
||||
msgid "dashboard.import.review-links"
|
||||
msgstr "Review links"
|
||||
|
||||
#: src/app/main/ui/dashboard.cljs:260
|
||||
msgid "dashboard.import.no-perms"
|
||||
msgstr "You don’t have permission to import to this team"
|
||||
|
||||
@ -769,6 +769,12 @@ msgstr "Algunas bibliotecas no pudieron vincularse automáticamente. Selecciona
|
||||
msgid "dashboard.import.confirm-library-links"
|
||||
msgstr "Confirmar vínculos de biblioteca"
|
||||
|
||||
msgid "dashboard.import.resolve-libraries-summary"
|
||||
msgstr "Revisa los vínculos de biblioteca antes de confirmar:"
|
||||
|
||||
msgid "dashboard.import.review-links"
|
||||
msgstr "Revisar vínculos"
|
||||
|
||||
#: src/app/main/ui/dashboard.cljs:260
|
||||
msgid "dashboard.import.no-perms"
|
||||
msgstr "No tienes permisos para importar en este equipo"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user