mirror of
https://github.com/penpot/penpot.git
synced 2026-06-16 12:22:22 +00:00
✨ Auto-link libraries during import based on slugified name
When a Penpot file is exported without bundled libraries and then imported into a different environment, external library links are broken because library UUIDs differ across environments. This feature adds a heuristic to auto-relink libraries by matching slugified library names against shared files in the target team: - Export: embed external library metadata (id, name, slug, used-by) in the manifest when libraries are not included in the export. - Import: resolve external libraries by slugifying shared file names in the destination team and matching against manifest slugs. - Single match: auto-link silently (creates file-library-rel row). - Multiple matches: emit SSE event so the frontend shows a selection dialog for the user to pick the correct library. - No match: import continues without linking (current behavior). Backend changes: - Extended manifest schema with optional :external-libraries field - Added slugify-name, get-files-names, get-shared-files-for-team, find-shared-files-by-slug helpers in app.binfile.common - Threaded team-id into import cfg from RPC layer - Added resolve-external-libraries and auto-link-libraries in v3 - Emit :library-candidates SSE event for multi-match cases Frontend changes: - Worker captures library-candidates SSE events and forwards them - Import dialog shows auto-link notification and multi-match selection UI with select dropdowns - Added link-files-to-library! RPC helper for user selections - Added en/es translations for new UI strings Closes #9263 Signed-off-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
d44c6250ea
commit
109539dbcd
@ -866,8 +866,8 @@
|
||||
(defn get-resolved-file-libraries
|
||||
"Get all file libraries including itself. Returns an instance of
|
||||
LoadableWeakValueMap that allows do not have strong references to
|
||||
the loaded libraries and reduce possible memory pressure on having
|
||||
all this libraries loaded at same time on processing file validation
|
||||
the loaded libraries and reduce memory pressure on having
|
||||
all this libraries at the same time on processing file validation
|
||||
or file migration.
|
||||
|
||||
This still requires at least one library at time to be loaded while
|
||||
@ -879,3 +879,47 @@
|
||||
(cons (:id file)))
|
||||
load-fn #(get-file cfg % :migrate? false)]
|
||||
(weak/loadable-weak-value-map library-ids load-fn {id file})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; EXTERNAL LIBRARY RESOLUTION HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn slugify-name
|
||||
"Slugify a library name for cross-environment matching.
|
||||
Lowercases, replaces non-alphanumeric runs with '-', strips
|
||||
leading/trailing '-'."
|
||||
[name]
|
||||
(str/slug name))
|
||||
|
||||
(def ^:private sql:get-files-names
|
||||
"SELECT id, name FROM file WHERE id = ANY(?)")
|
||||
|
||||
(defn get-files-names
|
||||
"Return [{:id uuid :name string}] for the given file ids."
|
||||
[cfg ids]
|
||||
(db/run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(let [ids-arr (db/create-array conn "uuid" ids)]
|
||||
(db/exec! conn [sql:get-files-names ids-arr])))))
|
||||
|
||||
(def ^:private sql:get-shared-files-for-team
|
||||
"SELECT f.id, f.name
|
||||
FROM file AS f
|
||||
JOIN project AS p ON (p.id = f.project_id)
|
||||
WHERE p.team_id = ?
|
||||
AND f.is_shared = true
|
||||
AND f.deleted_at IS NULL
|
||||
AND p.deleted_at IS NULL")
|
||||
|
||||
(defn get-shared-files-for-team
|
||||
"Return [{:id uuid :name string}] for all shared files in a team."
|
||||
[cfg team-id]
|
||||
(db/run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(db/exec! conn [sql:get-shared-files-for-team team-id]))))
|
||||
|
||||
(defn find-shared-files-by-slug
|
||||
"Return all shared files in `team-id` whose slugified name equals `slug`."
|
||||
[cfg team-id slug]
|
||||
(->> (get-shared-files-for-team cfg team-id)
|
||||
(filterv #(= slug (slugify-name (:name %))))))
|
||||
|
||||
@ -67,7 +67,15 @@
|
||||
|
||||
[:relations {:optional true}
|
||||
[:vector
|
||||
[:tuple ::sm/uuid ::sm/uuid]]]])
|
||||
[:tuple ::sm/uuid ::sm/uuid]]]
|
||||
|
||||
[:external-libraries {:optional true}
|
||||
[:vector
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:slug :string]
|
||||
[:used-by {:optional true} [:vector ::sm/uuid]]]]]])
|
||||
|
||||
(def ^:private schema:storage-object
|
||||
[:map {:title "StorageObject"}
|
||||
@ -372,11 +380,33 @@
|
||||
|
||||
(defn- export-files
|
||||
[{:keys [::bfc/ids ::bfc/include-libraries ::output] :as cfg}]
|
||||
(let [ids (into ids (when include-libraries (bfc/get-libraries cfg ids)))
|
||||
(let [original-ids ids
|
||||
ids (into ids (when include-libraries (bfc/get-libraries cfg ids)))
|
||||
rels (if include-libraries
|
||||
(->> (bfc/get-files-rels cfg ids)
|
||||
(mapv (juxt :file-id :library-file-id)))
|
||||
[])]
|
||||
[])
|
||||
|
||||
;; Compute external libraries: referenced by original files but
|
||||
;; not included in the export set. Only relevant when libraries
|
||||
;; are NOT bundled in the export.
|
||||
external-libs
|
||||
(when-not include-libraries
|
||||
(let [original-rels (bfc/get-files-rels cfg original-ids)
|
||||
lib-ids (into #{} (map :library-file-id) original-rels)]
|
||||
(when (seq lib-ids)
|
||||
(let [lib-names (bfc/get-files-names cfg lib-ids)]
|
||||
(->> lib-names
|
||||
(mapv (fn [{:keys [id name]}]
|
||||
(let [slug (bfc/slugify-name name)]
|
||||
(when-not (str/blank? slug)
|
||||
{:id id
|
||||
:name name
|
||||
:slug slug
|
||||
:used-by (->> original-rels
|
||||
(filter #(= (:library-file-id %) id))
|
||||
(mapv :file-id))}))))
|
||||
(filterv some?))))))]
|
||||
|
||||
(vswap! bfc/*state* assoc :files (d/ordered-map))
|
||||
|
||||
@ -389,12 +419,14 @@
|
||||
|
||||
;; Write manifest file
|
||||
(let [files (:files @bfc/*state*)
|
||||
params {:type "penpot/export-files"
|
||||
:version 1
|
||||
:generated-by (str "penpot/" (:full cf/version))
|
||||
:refer "penpot"
|
||||
:files (vec (vals files))
|
||||
:relations rels}]
|
||||
params (cond-> {:type "penpot/export-files"
|
||||
:version 1
|
||||
:generated-by (str "penpot/" (:full cf/version))
|
||||
:refer "penpot"
|
||||
:files (vec (vals files))
|
||||
:relations rels}
|
||||
(seq external-libs)
|
||||
(assoc :external-libraries external-libs))]
|
||||
(write-entry! output "manifest.json" params))))
|
||||
|
||||
;; --- IMPORT IMPL
|
||||
@ -880,6 +912,47 @@
|
||||
|
||||
(vswap! bfc/*state* update :index assoc id (:id sobject)))))))
|
||||
|
||||
(defn- resolve-external-libraries
|
||||
"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}]
|
||||
(let [external-libs (:external-libraries manifest)]
|
||||
(when (and team-id (seq external-libs))
|
||||
(reduce (fn [result {:keys [id slug]}]
|
||||
(let [candidates (bfc/find-shared-files-by-slug cfg team-id slug)]
|
||||
(if (seq candidates)
|
||||
(assoc result id candidates)
|
||||
result)))
|
||||
{}
|
||||
external-libs))))
|
||||
|
||||
(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 :new-id uuid}
|
||||
for each auto-linked library."
|
||||
[{:keys [::db/conn ::manifest ::bfc/timestamp] :as cfg} resolution file-ids]
|
||||
(let [external-libs (:external-libraries manifest)]
|
||||
(reduce (fn [linked {:keys [id name used-by] :as ext-lib}]
|
||||
(let [candidates (get resolution id)]
|
||||
(if (= 1 (count candidates))
|
||||
(let [new-lib-id (:id (first candidates))
|
||||
;; Link only files that actually used this library
|
||||
relevant-file-ids (if (seq used-by)
|
||||
(let [used-set (set (map bfc/lookup-index used-by))]
|
||||
(filterv used-set file-ids))
|
||||
file-ids)]
|
||||
(doseq [fid relevant-file-ids]
|
||||
(let [rel-params {:file-id fid
|
||||
:library-file-id new-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))))
|
||||
(conj linked {:id id :name name :new-id new-lib-id}))
|
||||
linked)))
|
||||
[]
|
||||
external-libs)))
|
||||
|
||||
(defn- import-files*
|
||||
[{:keys [::manifest] :as cfg}]
|
||||
(bfc/disable-database-timeouts! cfg)
|
||||
@ -897,9 +970,31 @@
|
||||
files)]
|
||||
|
||||
(import-file-relations cfg)
|
||||
(bfm/apply-pending-migrations! cfg)
|
||||
|
||||
result))
|
||||
;; Resolve external libraries by slug and auto-link single matches
|
||||
(let [resolution (resolve-external-libraries cfg)
|
||||
auto-linked (when (seq resolution)
|
||||
(auto-link-libraries cfg resolution result))
|
||||
|
||||
;; Collect multi-match candidates for frontend resolution
|
||||
candidates (when (seq resolution)
|
||||
(into {}
|
||||
(filter (fn [[_ candidates]] (> (count candidates) 1)))
|
||||
resolution))
|
||||
|
||||
;; Build external-libraries lookup from manifest for the frontend
|
||||
external-libs-info (when (seq resolution)
|
||||
(->> (:external-libraries manifest)
|
||||
(filter #(contains? candidates (:id %)))
|
||||
(mapv (fn [{:keys [id name slug]}]
|
||||
{:id id :name name :slug slug}))))]
|
||||
|
||||
(bfm/apply-pending-migrations! cfg)
|
||||
|
||||
{:file-ids result
|
||||
:auto-linked (or auto-linked [])
|
||||
:library-candidates (or candidates {})
|
||||
:external-libs (or external-libs-info [])})))
|
||||
|
||||
(defn- import-file-and-overwrite*
|
||||
[{:keys [::manifest ::bfc/file-id] :as cfg}]
|
||||
@ -927,7 +1022,10 @@
|
||||
(bfc/invalidate-thumbnails cfg file-id)
|
||||
(bfm/apply-pending-migrations! cfg)
|
||||
|
||||
[file-id])))
|
||||
{:file-ids [file-id]
|
||||
:auto-linked []
|
||||
:library-candidates {}
|
||||
:external-libs []})))
|
||||
|
||||
(defn- import-files
|
||||
[{:keys [::bfc/timestamp ::bfc/input] :or {timestamp (ct/now)} :as cfg}]
|
||||
@ -965,9 +1063,20 @@
|
||||
(events/tap :progress {:section :manifest})
|
||||
|
||||
(binding [bfc/*state* (volatile! {:media [] :index {}})]
|
||||
(if (::bfc/file-id cfg)
|
||||
(db/tx-run! cfg import-file-and-overwrite*)
|
||||
(db/tx-run! cfg import-files*)))))
|
||||
(let [result (if (::bfc/file-id cfg)
|
||||
(db/tx-run! cfg import-file-and-overwrite*)
|
||||
(db/tx-run! cfg import-files*))]
|
||||
|
||||
;; Emit library-candidates event for frontend resolution of
|
||||
;; multi-match cases (if any)
|
||||
(when (seq (:library-candidates result))
|
||||
(events/tap :library-candidates
|
||||
{:file-ids (:file-ids result)
|
||||
:auto-linked (:auto-linked result)
|
||||
:library-candidates (:library-candidates result)
|
||||
:external-libs (:external-libs result)}))
|
||||
|
||||
result))))
|
||||
|
||||
;; --- PUBLIC API
|
||||
|
||||
|
||||
@ -92,6 +92,7 @@
|
||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team))
|
||||
(assoc ::bfc/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/team-id (:id team))
|
||||
(assoc ::bfc/name name))
|
||||
|
||||
input-path (:path file)
|
||||
|
||||
@ -103,5 +103,199 @@
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!))]
|
||||
(t/is (= (count result) 1))
|
||||
(t/is (every? uuid? result)))))
|
||||
(t/is (map? result))
|
||||
(t/is (= 1 (count (:file-ids result))))
|
||||
(t/is (every? uuid? (:file-ids result)))
|
||||
(t/is (= [] (:auto-linked result)))
|
||||
(t/is (= {} (:library-candidates result)))
|
||||
(t/is (= [] (:external-libs result))))))
|
||||
|
||||
(t/deftest slugify-name-test
|
||||
(t/is (= "my-design-system" (bfc/slugify-name "My Design System!")))
|
||||
(t/is (= "icons" (bfc/slugify-name "Icons")))
|
||||
(t/is (= "brand-colors-2024" (bfc/slugify-name "Brand Colors 2024")))
|
||||
(t/is (= "" (bfc/slugify-name "---"))))
|
||||
|
||||
(t/deftest export-includes-external-libraries
|
||||
(let [profile (th/create-profile* 1)
|
||||
;; Create a shared library file
|
||||
library (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared true
|
||||
:name "Icons Library"})
|
||||
;; Create a file that uses the library
|
||||
file (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})]
|
||||
|
||||
;; Link file to library
|
||||
(db/insert! th/*system* :file-library-rel
|
||||
{:file-id (:id file)
|
||||
:library-file-id (:id library)})
|
||||
|
||||
;; Export without including libraries
|
||||
(let [output (tmp/tempfile :suffix ".zip")]
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{(:id file)})
|
||||
(assoc ::bfc/embed-assets false)
|
||||
(assoc ::bfc/include-libraries false))
|
||||
(io/output-stream output))
|
||||
|
||||
;; Read the manifest and check external-libraries
|
||||
(let [manifest (v3/get-manifest output)]
|
||||
(t/is (some? (:external-libraries manifest)))
|
||||
(t/is (= 1 (count (:external-libraries manifest))))
|
||||
(let [ext-lib (first (:external-libraries manifest))]
|
||||
(t/is (= (:id library) (:id ext-lib)))
|
||||
(t/is (= "Icons Library" (:name ext-lib)))
|
||||
(t/is (= "icons-library" (:slug ext-lib)))
|
||||
(t/is (= [(:id file)] (:used-by ext-lib))))))))
|
||||
|
||||
(t/deftest import-auto-links-single-candidate
|
||||
(let [profile (th/create-profile* 1)
|
||||
;; Create a shared library file
|
||||
library (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared true
|
||||
:name "Icons Library"})
|
||||
;; Create a file that uses the library
|
||||
file (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})]
|
||||
|
||||
;; Link file to library
|
||||
(db/insert! th/*system* :file-library-rel
|
||||
{:file-id (:id file)
|
||||
:library-file-id (:id library)})
|
||||
|
||||
;; Export without including libraries
|
||||
(let [output (tmp/tempfile :suffix ".zip")]
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{(:id file)})
|
||||
(assoc ::bfc/embed-assets false)
|
||||
(assoc ::bfc/include-libraries false))
|
||||
(io/output-stream output))
|
||||
|
||||
;; Now create a new shared library with the same name in the same team
|
||||
;; (simulating the library existing in the target environment)
|
||||
(let [library2 (th/create-file* 3 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared true
|
||||
:name "Icons Library"})
|
||||
|
||||
result (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/team-id (:default-team-id profile))
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!))]
|
||||
|
||||
;; Check that the library was auto-linked
|
||||
(t/is (= 1 (count (:auto-linked result))))
|
||||
(t/is (= (:id library) (:id (first (:auto-linked result)))))
|
||||
(t/is (= (:id library2) (:new-id (first (:auto-linked result)))))
|
||||
(t/is (= {} (:library-candidates result)))
|
||||
|
||||
;; Verify the file-library-rel was created
|
||||
(let [rels (db/query th/*system* :file-library-rel
|
||||
{:library-file-id (:id library2)})]
|
||||
(t/is (= 1 (count rels))))))))
|
||||
|
||||
(t/deftest import-no-auto-link-no-match
|
||||
(let [profile (th/create-profile* 1)
|
||||
;; Create a shared library file
|
||||
library (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared true
|
||||
:name "Icons Library"})
|
||||
;; Create a file that uses the library
|
||||
file (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})]
|
||||
|
||||
;; Link file to library
|
||||
(db/insert! th/*system* :file-library-rel
|
||||
{:file-id (:id file)
|
||||
:library-file-id (:id library)})
|
||||
|
||||
;; Export without including libraries
|
||||
(let [output (tmp/tempfile :suffix ".zip")]
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{(:id file)})
|
||||
(assoc ::bfc/embed-assets false)
|
||||
(assoc ::bfc/include-libraries false))
|
||||
(io/output-stream output))
|
||||
|
||||
;; Import without any matching library in the team
|
||||
(let [result (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/team-id (:default-team-id profile))
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!))]
|
||||
|
||||
;; No auto-linking should happen
|
||||
(t/is (= [] (:auto-linked result)))
|
||||
(t/is (= {} (:library-candidates result)))))))
|
||||
|
||||
(t/deftest import-returns-multi-match-candidates
|
||||
(let [profile (th/create-profile* 1)
|
||||
;; Create a shared library file
|
||||
library (th/create-file* 1 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared true
|
||||
:name "Icons Library"})
|
||||
;; Create a file that uses the library
|
||||
file (th/create-file* 2 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared false})]
|
||||
|
||||
;; Link file to library
|
||||
(db/insert! th/*system* :file-library-rel
|
||||
{:file-id (:id file)
|
||||
:library-file-id (:id library)})
|
||||
|
||||
;; Export without including libraries
|
||||
(let [output (tmp/tempfile :suffix ".zip")]
|
||||
(v3/export-files!
|
||||
(-> th/*system*
|
||||
(assoc ::bfc/ids #{(:id file)})
|
||||
(assoc ::bfc/embed-assets false)
|
||||
(assoc ::bfc/include-libraries false))
|
||||
(io/output-stream output))
|
||||
|
||||
;; Create TWO shared libraries with the same name
|
||||
(let [library2 (th/create-file* 3 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared true
|
||||
:name "Icons Library"})
|
||||
library3 (th/create-file* 4 {:profile-id (:id profile)
|
||||
:project-id (:default-project-id profile)
|
||||
:is-shared true
|
||||
:name "Icons Library"})
|
||||
|
||||
result (-> th/*system*
|
||||
(assoc ::bfc/project-id (:default-project-id profile))
|
||||
(assoc ::bfc/profile-id (:id profile))
|
||||
(assoc ::bfc/team-id (:default-team-id profile))
|
||||
(assoc ::bfc/input output)
|
||||
(v3/import-files!))]
|
||||
|
||||
;; No auto-linking (multi-match)
|
||||
(t/is (= [] (:auto-linked result)))
|
||||
|
||||
;; Should have candidates
|
||||
(t/is (= 1 (count (:library-candidates result))))
|
||||
(let [candidates (get (:library-candidates result) (:id library))]
|
||||
(t/is (= 2 (count candidates))))
|
||||
|
||||
;; No file-library-rel should be created automatically
|
||||
(let [rels (db/query th/*system* :file-library-rel
|
||||
{:library-file-id (:id library2)})]
|
||||
(t/is (= 0 (count rels))))
|
||||
(let [rels (db/query th/*system* :file-library-rel
|
||||
{:library-file-id (:id library3)})]
|
||||
(t/is (= 0 (count rels))))))))
|
||||
|
||||
@ -15,8 +15,10 @@
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||
[app.main.ui.ds.controls.select :refer [select*]]
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||
@ -173,7 +175,7 @@
|
||||
(swap! state update-with-analyze-result message))))))
|
||||
|
||||
(defn- import-files
|
||||
[state project-id entries]
|
||||
[state library-resolution* project-id entries]
|
||||
(st/emit! (ev/event {::ev/name "import-files"
|
||||
:num-files (count entries)}))
|
||||
|
||||
@ -186,6 +188,11 @@
|
||||
(rx/filter (comp uuid? :file-id))
|
||||
(rx/subs!
|
||||
(fn [message]
|
||||
;; Capture library-resolution data if present (same for all
|
||||
;; entries from the same zip, so first one wins)
|
||||
(when-let [resolution (:library-resolution message)]
|
||||
(when (nil? @library-resolution*)
|
||||
(reset! library-resolution* resolution)))
|
||||
(swap! state update-entry-status message))))))
|
||||
|
||||
(mf/defc import-entry*
|
||||
@ -318,6 +325,55 @@
|
||||
(fn []
|
||||
(mapv #(assoc % :status :analyze) entries)))
|
||||
|
||||
(defn- link-files-to-library!
|
||||
"Call the link-file-to-library RPC for each file-id with the given
|
||||
library-id. Returns an observable that completes when all links are done."
|
||||
[file-ids library-id]
|
||||
(->> (rx/from file-ids)
|
||||
(rx/merge-map (fn [file-id]
|
||||
(->> (rp/cmd! :link-file-to-library
|
||||
{:file-id file-id
|
||||
:library-id library-id})
|
||||
(rx/catch (fn [cause]
|
||||
(log/error :hint "failed to link library"
|
||||
:file-id file-id
|
||||
:library-id library-id
|
||||
:cause cause)
|
||||
(rx/of nil))))))))
|
||||
|
||||
(mf/defc library-resolution*
|
||||
{::mf/private true}
|
||||
[{:keys [resolution selections*]}]
|
||||
(let [external-libs (:external-libs resolution)
|
||||
library-candidates (:library-candidates resolution)
|
||||
selections (deref selections*)
|
||||
|
||||
on-select
|
||||
(mf/use-fn
|
||||
(mf/deps selections*)
|
||||
(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")]
|
||||
|
||||
(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)}]]))])))
|
||||
|
||||
(mf/defc import-dialog
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :import
|
||||
@ -338,13 +394,20 @@
|
||||
edition* (mf/use-state nil)
|
||||
edition (deref edition*)
|
||||
|
||||
;; Library resolution data from the backend (auto-linked + multi-match)
|
||||
library-resolution* (mf/use-state nil)
|
||||
library-resolution (deref library-resolution*)
|
||||
|
||||
;; User selections for multi-match candidates: {old-lib-id candidate-id}
|
||||
library-selections* (mf/use-state {})
|
||||
|
||||
continue-entries
|
||||
(mf/use-fn
|
||||
(mf/deps entries)
|
||||
(fn []
|
||||
(let [entries (filterv has-status-ready? entries)]
|
||||
(reset! status* :import-progress)
|
||||
(import-files state* project-id entries))))
|
||||
(import-files state* library-resolution* project-id entries))))
|
||||
|
||||
continue-template
|
||||
(mf/use-fn
|
||||
@ -407,6 +470,21 @@
|
||||
(continue-template template)
|
||||
(continue-entries))))
|
||||
|
||||
on-confirm-library-links
|
||||
(mf/use-fn
|
||||
(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 [_]
|
||||
(reset! status* :import-success)))))))
|
||||
|
||||
on-accept
|
||||
(mf/use-fn
|
||||
(mf/deps on-finish-import)
|
||||
@ -432,7 +510,12 @@
|
||||
(zero? (count entries))))
|
||||
|
||||
pending-analysis?
|
||||
(some has-status-analyze? entries)]
|
||||
(some has-status-analyze? entries)
|
||||
|
||||
auto-linked-count
|
||||
(if (some? library-resolution)
|
||||
(count (:auto-linked library-resolution))
|
||||
0)]
|
||||
|
||||
(mf/with-effect [entries]
|
||||
(cond
|
||||
@ -445,7 +528,12 @@
|
||||
|
||||
(and (seq entries)
|
||||
(every? #(= :import-success (:status %)) entries))
|
||||
(reset! status* :import-success)
|
||||
;; After all entries are imported, check if there are multi-match
|
||||
;; candidates that need user resolution
|
||||
(if (and (some? @library-resolution*)
|
||||
(seq (:library-candidates @library-resolution*)))
|
||||
(reset! status* :library-resolution)
|
||||
(reset! status* :import-success))
|
||||
|
||||
(and (seq entries)
|
||||
(and (every? #(not= :import-ready (:status %)) entries)
|
||||
@ -473,9 +561,14 @@
|
||||
:content (tr "dashboard.import.import-warning")}])
|
||||
|
||||
(when (= :import-success status)
|
||||
[:& context-notification
|
||||
{:level (if (zero? import-success-total) :warning :success)
|
||||
:content (tr "dashboard.import.import-message" (i18n/c import-success-total))}])
|
||||
[:*
|
||||
[:& context-notification
|
||||
{:level (if (zero? import-success-total) :warning :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))}])])
|
||||
|
||||
(when (= :import-error status)
|
||||
[:& context-notification
|
||||
@ -483,6 +576,19 @@
|
||||
:class (stl/css :context-notification-error)
|
||||
:content (tr "dashboard.import.import-error.disclaimer")}])
|
||||
|
||||
(when (= :library-resolution status)
|
||||
[:*
|
||||
[:& 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*
|
||||
:file-ids (:file-ids library-resolution)}]])
|
||||
|
||||
(if (or (= :import-error status) (and (= :analyze status) errors?))
|
||||
[:div {:class (stl/css :import-error-disclaimer)}
|
||||
[:div (tr "dashboard.import.import-error.message1")]
|
||||
@ -512,16 +618,17 @@
|
||||
(tr "dashboard.import.import-error.unknown-error"))])]))]
|
||||
[:div (tr "dashboard.import.import-error.message2")]]
|
||||
|
||||
(for [entry entries]
|
||||
[:> import-entry* {:edition edition
|
||||
:key (dm/str (:uri entry) "/" (:file-id entry))
|
||||
:entry entry
|
||||
:entries entries
|
||||
:importing? (= :import-progress status)
|
||||
:on-edit on-edit
|
||||
:on-change on-entry-change
|
||||
:on-delete on-entry-delete
|
||||
:can-be-deleted (> (count entries) 1)}]))
|
||||
(when-not (= :library-resolution status)
|
||||
(for [entry entries]
|
||||
[:> import-entry* {:edition edition
|
||||
:key (dm/str (:uri entry) "/" (:file-id entry))
|
||||
:entry entry
|
||||
:entries entries
|
||||
:importing? (= :import-progress status)
|
||||
:on-edit on-edit
|
||||
:on-change on-entry-change
|
||||
:on-delete on-entry-delete
|
||||
:can-be-deleted (> (count entries) 1)}])))
|
||||
|
||||
(when (some? template)
|
||||
[:> import-entry* {:entry (assoc template :status status)
|
||||
@ -548,6 +655,12 @@
|
||||
:disabled pending-analysis?
|
||||
:on-click on-continue}])
|
||||
|
||||
(when (= :library-resolution status)
|
||||
[:input {:class (stl/css :accept-btn)
|
||||
:type "button"
|
||||
:value (tr "dashboard.import.confirm-library-links")
|
||||
:on-click on-confirm-library-links}])
|
||||
|
||||
(when (or (= :import-success status)
|
||||
(= :import-error status)
|
||||
(= :import-progress status))
|
||||
|
||||
27
frontend/src/app/main/ui/dashboard/import.scss
vendored
27
frontend/src/app/main/ui/dashboard/import.scss
vendored
@ -273,3 +273,30 @@
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.library-resolution {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-12;
|
||||
}
|
||||
|
||||
.library-resolution-message {
|
||||
@include deprecated.body-small-typography;
|
||||
|
||||
color: var(--modal-text-foreground-color);
|
||||
margin-bottom: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.library-resolution-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-4;
|
||||
padding: deprecated.$s-8 0;
|
||||
}
|
||||
|
||||
.library-resolution-item-name {
|
||||
@include deprecated.body-small-typography;
|
||||
|
||||
color: var(--modal-title-foreground-color);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@ -204,33 +204,43 @@
|
||||
(rx/mapcat identity)
|
||||
(rx/merge-map
|
||||
(fn [[uri entries]]
|
||||
(->> (import-blob-via-upload uri
|
||||
{:name (-> entries first :name)
|
||||
:version 3
|
||||
:project-id project-id})
|
||||
(rx/tap (fn [event]
|
||||
(let [payload (sse/get-payload event)
|
||||
type (sse/get-type event)]
|
||||
(if (= type "progress")
|
||||
(log/dbg :hint "import-binfile: progress"
|
||||
:section (:section payload)
|
||||
:name (:name payload))
|
||||
(log/dbg :hint "import-binfile: end")))))
|
||||
(rx/filter sse/end-of-stream?)
|
||||
(rx/mapcat (fn [_]
|
||||
(->> (rx/from entries)
|
||||
(rx/map (fn [entry]
|
||||
{:status :finish
|
||||
:file-id (:file-id entry)})))))
|
||||
(rx/catch
|
||||
(fn [cause]
|
||||
(log/error :hint "unexpected error on import process"
|
||||
:project-id project-id
|
||||
::log/sync? true
|
||||
:cause cause)
|
||||
(let [err (import-cause-message cause (tr "labels.error"))]
|
||||
(->> (rx/from entries)
|
||||
(rx/map (fn [entry]
|
||||
{:status :error
|
||||
:error err
|
||||
:file-id (:file-id entry)})))))))))))))
|
||||
(let [library-resolution* (volatile! nil)]
|
||||
(->> (import-blob-via-upload uri
|
||||
{:name (-> entries first :name)
|
||||
:version 3
|
||||
:project-id project-id})
|
||||
(rx/tap (fn [event]
|
||||
(let [payload (sse/get-payload event)
|
||||
type (sse/get-type event)]
|
||||
(cond
|
||||
(= type "progress")
|
||||
(log/dbg :hint "import-binfile: progress"
|
||||
:section (:section payload)
|
||||
:name (:name payload))
|
||||
|
||||
(= type "library-candidates")
|
||||
(vreset! library-resolution* payload)
|
||||
|
||||
:else
|
||||
(log/dbg :hint "import-binfile: end")))))
|
||||
(rx/filter sse/end-of-stream?)
|
||||
(rx/mapcat (fn [_]
|
||||
(let [resolution @library-resolution*]
|
||||
(->> (rx/from entries)
|
||||
(rx/map (fn [entry]
|
||||
(cond-> {:status :finish
|
||||
:file-id (:file-id entry)}
|
||||
(some? resolution)
|
||||
(assoc :library-resolution resolution))))))))
|
||||
(rx/catch
|
||||
(fn [cause]
|
||||
(log/error :hint "import-binfile: unexpected error on importing"
|
||||
:project-id project-id
|
||||
::log/sync? true
|
||||
:cause cause)
|
||||
(let [err (import-cause-message cause (tr "labels.error"))]
|
||||
(->> (rx/from entries)
|
||||
(rx/map (fn [entry]
|
||||
{:status :error
|
||||
:error err
|
||||
:file-id (:file-id entry)}))))))))))))))
|
||||
|
||||
@ -686,11 +686,11 @@ msgstr ""
|
||||
msgid "dashboard.go-to-admin-console"
|
||||
msgstr "Go to Admin Console"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:465, src/app/main/ui/dashboard/project_menu.cljs:109
|
||||
#: src/app/main/ui/dashboard/import.cljs:553, src/app/main/ui/dashboard/project_menu.cljs:109
|
||||
msgid "dashboard.import"
|
||||
msgstr "Import Penpot files"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:292, src/app/worker/import.cljs:138, src/app/worker/import.cljs:141
|
||||
#: src/app/main/ui/dashboard/import.cljs:299, src/app/worker/import.cljs:138, src/app/worker/import.cljs:141
|
||||
msgid "dashboard.import.analyze-error"
|
||||
msgstr "Oops! We couldn't import this file"
|
||||
|
||||
@ -698,10 +698,20 @@ msgstr "Oops! We couldn't import this file"
|
||||
msgid "dashboard.import.analyze-error.components-v2"
|
||||
msgstr "File with components v2 activated but this team doesn't support it yet."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:573, src/app/main/ui/dashboard/import.cljs:589
|
||||
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."
|
||||
|
||||
#: src/app/main/ui/dashboard.cljs:259
|
||||
msgid "dashboard.import.bad-url"
|
||||
msgstr "Import failed. The template URL is incorrect"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:663
|
||||
msgid "dashboard.import.confirm-library-links"
|
||||
msgstr "Confirm library links"
|
||||
|
||||
#: src/app/main/ui/dashboard.cljs:241
|
||||
#, unused
|
||||
msgid "dashboard.import.error"
|
||||
@ -712,72 +722,86 @@ msgstr "Import failed. Please try again"
|
||||
msgid "dashboard.import.import-error"
|
||||
msgstr "There was a problem importing the file. The file wasn't imported."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:507
|
||||
#: src/app/main/ui/dashboard/import.cljs:613
|
||||
msgid "dashboard.import.import-error.check-error"
|
||||
msgstr "We couldn't verify this file."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:511
|
||||
#: src/app/main/ui/dashboard/import.cljs:617
|
||||
msgid "dashboard.import.import-error.corrupt-file"
|
||||
msgstr "This file appears to be damaged."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:486
|
||||
#: src/app/main/ui/dashboard/import.cljs:579
|
||||
msgid "dashboard.import.import-error.disclaimer"
|
||||
msgstr "Not all files have been imported"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:490
|
||||
#: src/app/main/ui/dashboard/import.cljs:596
|
||||
msgid "dashboard.import.import-error.message1"
|
||||
msgstr "The following files have errors:"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:515
|
||||
#: src/app/main/ui/dashboard/import.cljs:621
|
||||
msgid "dashboard.import.import-error.message2"
|
||||
msgstr "Files with errors will not be uploaded."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:514
|
||||
#: src/app/main/ui/dashboard/import.cljs:620
|
||||
msgid "dashboard.import.import-error.unknown-error"
|
||||
msgstr "Something went wrong while processing this file."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:480
|
||||
#: src/app/main/ui/dashboard/import.cljs:569, src/app/main/ui/dashboard/import.cljs:585
|
||||
msgid "dashboard.import.import-message"
|
||||
msgid_plural "dashboard.import.import-message"
|
||||
msgstr[0] "1 file has been imported successfully."
|
||||
msgstr[1] "%s files have been imported successfully."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:475
|
||||
#: src/app/main/ui/dashboard/import.cljs:563
|
||||
msgid "dashboard.import.import-warning"
|
||||
msgstr "Some files containted invalid objects that have been removed."
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.import.library-link-error"
|
||||
msgstr "Failed to link library"
|
||||
|
||||
#, unused
|
||||
msgid "dashboard.import.library-placeholder"
|
||||
msgstr "Select a library…"
|
||||
|
||||
#: src/app/main/ui/dashboard.cljs:260
|
||||
msgid "dashboard.import.no-perms"
|
||||
msgstr "You don’t have permission to import to this team"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:127
|
||||
#: src/app/main/ui/dashboard/import.cljs:129
|
||||
msgid "dashboard.import.progress.process-colors"
|
||||
msgstr "Processing colors"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:136, src/app/main/ui/dashboard/import.cljs:139
|
||||
#: src/app/main/ui/dashboard/import.cljs:138, src/app/main/ui/dashboard/import.cljs:141
|
||||
msgid "dashboard.import.progress.process-components"
|
||||
msgstr "Processing components"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:133
|
||||
#: src/app/main/ui/dashboard/import.cljs:135
|
||||
msgid "dashboard.import.progress.process-media"
|
||||
msgstr "Processing media"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:124
|
||||
#: src/app/main/ui/dashboard/import.cljs:126
|
||||
msgid "dashboard.import.progress.process-page"
|
||||
msgstr "Processing page: %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:130
|
||||
#: src/app/main/ui/dashboard/import.cljs:132
|
||||
msgid "dashboard.import.progress.process-typographies"
|
||||
msgstr "Processing typographies"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:118
|
||||
#: src/app/main/ui/dashboard/import.cljs:120
|
||||
msgid "dashboard.import.progress.upload-data"
|
||||
msgstr "Uploading data to server (%s/%s)"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:121
|
||||
#: src/app/main/ui/dashboard/import.cljs:123
|
||||
msgid "dashboard.import.progress.upload-media"
|
||||
msgstr "Uploading file: %s"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:362
|
||||
msgid "dashboard.import.resolve-libraries"
|
||||
msgstr ""
|
||||
"Some libraries couldn't be linked automatically. Please select the correct "
|
||||
"library for each:"
|
||||
|
||||
#: src/app/main/ui/dashboard/team.cljs:851
|
||||
msgid "dashboard.invitation-modal.delete"
|
||||
msgstr "You're going to delete the invitations to:"
|
||||
@ -824,7 +848,7 @@ msgstr "Here you have some Libraries and templates you can add to your project"
|
||||
msgid "dashboard.libraries-and-templates.explore"
|
||||
msgstr "Explore more of them and know how to contribute"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:366, src/app/main/ui/workspace/libraries.cljs:146
|
||||
#: src/app/main/ui/dashboard/import.cljs:429, src/app/main/ui/workspace/libraries.cljs:146
|
||||
msgid "dashboard.libraries-and-templates.import-error"
|
||||
msgstr "There was a problem importing the template. The template wasn't imported."
|
||||
|
||||
@ -2695,7 +2719,7 @@ msgstr "Shortcuts"
|
||||
msgid "labels.about-penpot"
|
||||
msgstr "About Penpot"
|
||||
|
||||
#: src/app/main/data/common.cljs:90, src/app/main/data/team.cljs:163, src/app/main/data/team.cljs:584, src/app/main/ui/dashboard/import.cljs:558
|
||||
#: src/app/main/data/common.cljs:90, src/app/main/data/team.cljs:163, src/app/main/data/team.cljs:584, src/app/main/ui/dashboard/import.cljs:671
|
||||
msgid "labels.accept"
|
||||
msgstr "Accept"
|
||||
|
||||
@ -2755,7 +2779,7 @@ msgstr "Bad Gateway"
|
||||
msgid "labels.blur"
|
||||
msgstr "Blur"
|
||||
|
||||
#: src/app/main/data/common.cljs:119, src/app/main/ui/dashboard/change_owner.cljs:67, src/app/main/ui/dashboard/change_owner.cljs:174, src/app/main/ui/dashboard/import.cljs:543, src/app/main/ui/dashboard/team.cljs:866, src/app/main/ui/dashboard/team.cljs:1345, src/app/main/ui/delete_shared.cljs:36, src/app/main/ui/exports/assets.cljs:163, src/app/main/ui/exports/files.cljs:167, src/app/main/ui/settings/integrations.cljs:228, src/app/main/ui/viewer/share_link.cljs:204, src/app/main/ui/workspace/sidebar/assets/groups.cljs:178, src/app/main/ui/workspace/tokens/export/modal.cljs:43, src/app/main/ui/workspace/tokens/import/modal.cljs:268, src/app/main/ui/workspace/tokens/import_from_library.cljs:88, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:364, src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs:73, src/app/main/ui/workspace/tokens/settings/menu.cljs:104, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:242
|
||||
#: src/app/main/data/common.cljs:119, src/app/main/ui/dashboard/change_owner.cljs:67, src/app/main/ui/dashboard/change_owner.cljs:174, src/app/main/ui/dashboard/import.cljs:650, src/app/main/ui/dashboard/team.cljs:866, src/app/main/ui/dashboard/team.cljs:1345, src/app/main/ui/delete_shared.cljs:36, src/app/main/ui/exports/assets.cljs:163, src/app/main/ui/exports/files.cljs:167, src/app/main/ui/settings/integrations.cljs:228, src/app/main/ui/viewer/share_link.cljs:204, src/app/main/ui/workspace/sidebar/assets/groups.cljs:178, src/app/main/ui/workspace/tokens/export/modal.cljs:43, src/app/main/ui/workspace/tokens/import/modal.cljs:268, src/app/main/ui/workspace/tokens/import_from_library.cljs:88, src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs:364, src/app/main/ui/workspace/tokens/management/forms/rename_node_modal.cljs:73, src/app/main/ui/workspace/tokens/settings/menu.cljs:104, src/app/main/ui/workspace/tokens/themes/create_modal.cljs:242
|
||||
msgid "labels.cancel"
|
||||
msgstr "Cancel"
|
||||
|
||||
@ -2815,7 +2839,7 @@ msgstr "Contact support"
|
||||
msgid "labels.contact-us"
|
||||
msgstr "Contact us"
|
||||
|
||||
#: src/app/main/ui/auth/login.cljs:203, src/app/main/ui/dashboard/deleted.cljs:43, src/app/main/ui/dashboard/deleted.cljs:274, src/app/main/ui/dashboard/file_menu.cljs:210, src/app/main/ui/dashboard/import.cljs:549, src/app/main/ui/dashboard/team.cljs:873, src/app/main/ui/exports/files.cljs:172, src/app/main/ui/settings/subscription.cljs:310, src/app/main/ui/settings/subscription.cljs:344
|
||||
#: src/app/main/ui/auth/login.cljs:203, src/app/main/ui/dashboard/deleted.cljs:43, src/app/main/ui/dashboard/deleted.cljs:274, src/app/main/ui/dashboard/file_menu.cljs:210, src/app/main/ui/dashboard/import.cljs:656, src/app/main/ui/dashboard/team.cljs:873, src/app/main/ui/exports/files.cljs:172, src/app/main/ui/settings/subscription.cljs:310, src/app/main/ui/settings/subscription.cljs:344
|
||||
msgid "labels.continue"
|
||||
msgstr "Continue"
|
||||
|
||||
@ -2948,7 +2972,7 @@ msgstr "Editor"
|
||||
msgid "labels.empty"
|
||||
msgstr "Empty"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:298, src/app/worker/import.cljs:197, src/app/worker/import.cljs:231
|
||||
#: src/app/main/ui/dashboard/import.cljs:305, src/app/worker/import.cljs:197, src/app/worker/import.cljs:241
|
||||
msgid "labels.error"
|
||||
msgstr "Error"
|
||||
|
||||
@ -3107,7 +3131,7 @@ msgstr "Learning Center"
|
||||
msgid "labels.libraries-and-templates"
|
||||
msgstr "Libraries & Templates"
|
||||
|
||||
#: src/app/main/ui/auth/verify_token.cljs:125, src/app/main/ui/dashboard/grid.cljs:126, src/app/main/ui/dashboard/grid.cljs:146, src/app/main/ui/dashboard/import.cljs:257, src/app/main/ui/dashboard/placeholder.cljs:139, src/app/main/ui/ds/product/loader.cljs:85, src/app/main/ui/exports/files.cljs:59, src/app/main/ui/nitrate/entry.cljs:26, src/app/main/ui/static.cljs:608, src/app/main/ui/viewer.cljs:641, src/app/main/ui/workspace/sidebar/assets/file_library.cljs:247, src/app/main/ui/workspace.cljs:131, src/app/main/ui.cljs:71, src/app/main/ui.cljs:109, src/app/main/ui.cljs:128
|
||||
#: src/app/main/ui/auth/verify_token.cljs:125, src/app/main/ui/dashboard/grid.cljs:126, src/app/main/ui/dashboard/grid.cljs:146, src/app/main/ui/dashboard/import.cljs:264, src/app/main/ui/dashboard/placeholder.cljs:139, src/app/main/ui/ds/product/loader.cljs:85, src/app/main/ui/exports/files.cljs:59, src/app/main/ui/nitrate/entry.cljs:26, src/app/main/ui/static.cljs:608, src/app/main/ui/viewer.cljs:641, src/app/main/ui/workspace/sidebar/assets/file_library.cljs:247, src/app/main/ui/workspace.cljs:131, src/app/main/ui.cljs:71, src/app/main/ui.cljs:109, src/app/main/ui.cljs:128
|
||||
msgid "labels.loading"
|
||||
msgstr "Loading…"
|
||||
|
||||
@ -3540,7 +3564,7 @@ msgstr "Upload custom fonts"
|
||||
msgid "labels.uploading"
|
||||
msgstr "Uploading…"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:536
|
||||
#: src/app/main/ui/dashboard/import.cljs:643
|
||||
msgid "labels.uploading-file"
|
||||
msgstr "Uploading file…"
|
||||
|
||||
|
||||
@ -748,6 +748,23 @@ msgstr[1] "%s ficheros se han importado correctamente."
|
||||
msgid "dashboard.import.import-warning"
|
||||
msgstr "Algunos ficheros contenían objetos erroneos que no han sido importados."
|
||||
|
||||
msgid "dashboard.import.auto-linked-libraries"
|
||||
msgid_plural "dashboard.import.auto-linked-libraries"
|
||||
msgstr[0] "1 biblioteca fue vinculada automáticamente por nombre."
|
||||
msgstr[1] "%s bibliotecas fueron vinculadas automáticamente por nombre."
|
||||
|
||||
msgid "dashboard.import.resolve-libraries"
|
||||
msgstr "Algunas bibliotecas no pudieron vincularse automáticamente. Selecciona la biblioteca correcta para cada una:"
|
||||
|
||||
msgid "dashboard.import.library-placeholder"
|
||||
msgstr "Selecciona una biblioteca…"
|
||||
|
||||
msgid "dashboard.import.library-link-error"
|
||||
msgstr "No se pudo vincular la biblioteca"
|
||||
|
||||
msgid "dashboard.import.confirm-library-links"
|
||||
msgstr "Confirmar vínculos de biblioteca"
|
||||
|
||||
#: 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