mirror of
https://github.com/penpot/penpot.git
synced 2026-08-28 07:38:42 +00:00
♻️ Consolidate auto-link libraries with unified export-type and fix ref integrity (#9958)
* ✨ 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> * ✨ Add UI for the auto-link plumbing * ♻️ Consolidate auto-link libraries with unified export-type and fix ref integrity Consolidates the auto-link libraries feature into a single coherent implementation: - Unify boolean flags (embed-assets, include-libraries, link-later) into single ::bfc/export-type parameter - Fix critical reference-integrity bug: pre-resolution no longer remaps :component-file refs when no link is created (multi-match / no-permission) - compute-link-decisions as single source of truth for auto-link logic - 80+ backend tests covering round-trip, cross-team, permissions, edge cases, and reference integrity AI-assisted-by: longcat-2.0 --------- Signed-off-by: Andrey Antukh <niwi@niwi.nz> Co-authored-by: Eva Marco <evamarcod@gmail.com>
This commit is contained in:
parent
17befc1db9
commit
03cd3fa70f
@ -875,8 +875,8 @@
|
|||||||
(defn get-resolved-file-libraries
|
(defn get-resolved-file-libraries
|
||||||
"Get all file libraries including itself. Returns an instance of
|
"Get all file libraries including itself. Returns an instance of
|
||||||
LoadableWeakValueMap that allows do not have strong references to
|
LoadableWeakValueMap that allows do not have strong references to
|
||||||
the loaded libraries and reduce possible memory pressure on having
|
the loaded libraries and reduce memory pressure on having
|
||||||
all this libraries loaded at same time on processing file validation
|
all this libraries at the same time on processing file validation
|
||||||
or file migration.
|
or file migration.
|
||||||
|
|
||||||
This still requires at least one library at time to be loaded while
|
This still requires at least one library at time to be loaded while
|
||||||
@ -888,3 +888,47 @@
|
|||||||
(cons (:id file)))
|
(cons (:id file)))
|
||||||
load-fn #(get-file cfg % :migrate? false)]
|
load-fn #(get-file cfg % :migrate? false)]
|
||||||
(weak/loadable-weak-value-map library-ids load-fn {id file})))
|
(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, f.project_id
|
||||||
|
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)
|
||||||
|
(filter #(= slug (slugify-name (:name %))))))
|
||||||
|
|||||||
@ -67,7 +67,16 @@
|
|||||||
|
|
||||||
[:relations {:optional true}
|
[:relations {:optional true}
|
||||||
[:vector
|
[:vector
|
||||||
[:tuple ::sm/uuid ::sm/uuid]]]])
|
[:tuple ::sm/uuid ::sm/uuid]]]
|
||||||
|
|
||||||
|
;; TODO: rename to :links
|
||||||
|
[: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
|
(def ^:private schema:storage-object
|
||||||
[:map {:title "StorageObject"}
|
[:map {:title "StorageObject"}
|
||||||
@ -217,14 +226,12 @@
|
|||||||
(.flush writer))
|
(.flush writer))
|
||||||
(.closeEntry output))
|
(.closeEntry output))
|
||||||
|
|
||||||
|
|
||||||
(defn- get-file
|
(defn- get-file
|
||||||
[{:keys [::bfc/embed-assets ::bfc/include-libraries] :as cfg} file-id]
|
[{:keys [::bfc/export-type] :as cfg} file-id]
|
||||||
|
|
||||||
(when (and include-libraries embed-assets)
|
(let [detach? (= export-type :detach-libraries)
|
||||||
(throw (IllegalArgumentException.
|
embed? (= export-type :merge-libraries)]
|
||||||
"the `include-libraries` and `embed-assets` are mutally excluding options")))
|
|
||||||
|
|
||||||
(let [detach? (and (not embed-assets) (not include-libraries))]
|
|
||||||
(db/tx-run! cfg (fn [cfg]
|
(db/tx-run! cfg (fn [cfg]
|
||||||
(cond-> (bfc/get-file cfg file-id
|
(cond-> (bfc/get-file cfg file-id
|
||||||
{:realize? true
|
{:realize? true
|
||||||
@ -234,7 +241,7 @@
|
|||||||
(-> (ctf/detach-external-references file-id)
|
(-> (ctf/detach-external-references file-id)
|
||||||
(dissoc :libraries))
|
(dissoc :libraries))
|
||||||
|
|
||||||
embed-assets
|
embed?
|
||||||
(update :data #(bfc/embed-assets cfg % file-id))
|
(update :data #(bfc/embed-assets cfg % file-id))
|
||||||
|
|
||||||
:always
|
:always
|
||||||
@ -371,12 +378,34 @@
|
|||||||
(write-entry! output path encoded-tokens)))))
|
(write-entry! output path encoded-tokens)))))
|
||||||
|
|
||||||
(defn- export-files
|
(defn- export-files
|
||||||
[{:keys [::bfc/ids ::bfc/include-libraries ::output] :as cfg}]
|
[{:keys [::bfc/ids ::bfc/export-type ::output] :as cfg}]
|
||||||
(let [ids (into ids (when include-libraries (bfc/get-libraries cfg ids)))
|
|
||||||
rels (if include-libraries
|
(let [original-ids ids
|
||||||
|
ids (into ids (when (= export-type :include-libraries) (bfc/get-libraries cfg ids)))
|
||||||
|
rels (if (= export-type :include-libraries)
|
||||||
(->> (bfc/get-files-rels cfg ids)
|
(->> (bfc/get-files-rels cfg ids)
|
||||||
(mapv (juxt :file-id :library-file-id)))
|
(mapv (juxt :file-id :library-file-id)))
|
||||||
[])]
|
[])
|
||||||
|
|
||||||
|
;; Compute external libraries: referenced by original files but
|
||||||
|
;; not included in the export set. Only relevant for :link-later.
|
||||||
|
external-libs
|
||||||
|
(when (= export-type :link-later)
|
||||||
|
(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))
|
(vswap! bfc/*state* assoc :files (d/ordered-map))
|
||||||
|
|
||||||
@ -389,12 +418,14 @@
|
|||||||
|
|
||||||
;; Write manifest file
|
;; Write manifest file
|
||||||
(let [files (:files @bfc/*state*)
|
(let [files (:files @bfc/*state*)
|
||||||
params {:type "penpot/export-files"
|
params (cond-> {:type "penpot/export-files"
|
||||||
:version 1
|
:version 1
|
||||||
:generated-by (str "penpot/" (:full cf/version))
|
:generated-by (str "penpot/" (:full cf/version))
|
||||||
:referer "penpot"
|
:referer "penpot"
|
||||||
:files (vec (vals files))
|
:files (vec (vals files))
|
||||||
:relations rels}]
|
:relations rels}
|
||||||
|
(seq external-libs)
|
||||||
|
(assoc :external-libraries external-libs))]
|
||||||
(write-entry! output "manifest.json" params))))
|
(write-entry! output "manifest.json" params))))
|
||||||
|
|
||||||
;; --- IMPORT IMPL
|
;; --- IMPORT IMPL
|
||||||
@ -882,6 +913,104 @@
|
|||||||
|
|
||||||
(vswap! bfc/*state* update :index assoc id (:id sobject)))))))
|
(vswap! bfc/*state* update :index assoc id (:id sobject)))))))
|
||||||
|
|
||||||
|
(defn- add-to-file
|
||||||
|
"Add a resolved library entry to a file in the file-grouped resolution.
|
||||||
|
`key` is :done (auto-linked) or :pending (needs resolution)."
|
||||||
|
[acc file-id file-name key entry]
|
||||||
|
(update acc file-id (fn [file]
|
||||||
|
(let [file (or file {:id file-id
|
||||||
|
:name file-name
|
||||||
|
:done []
|
||||||
|
:pending []})]
|
||||||
|
(update file key conj entry)))))
|
||||||
|
|
||||||
|
(defn- compute-link-decisions
|
||||||
|
"Returns a map of {old-lib-id -> {:library-id ... :library ...}} for external
|
||||||
|
libraries that should be auto-linked (single candidate AND importer has edit
|
||||||
|
permission). Libraries with zero or multiple candidates, or where the importer
|
||||||
|
lacks permission, are excluded — their refs should remain dangling."
|
||||||
|
[{:keys [::db/conn ::manifest ::bfc/team-id ::bfc/profile-id] :as cfg}]
|
||||||
|
(reduce
|
||||||
|
(fn [acc ext-lib]
|
||||||
|
(let [slug (:slug ext-lib)]
|
||||||
|
(if (nil? slug)
|
||||||
|
acc
|
||||||
|
(let [matching (into [] (bfc/find-shared-files-by-slug cfg team-id slug))]
|
||||||
|
(if (not= 1 (count matching))
|
||||||
|
acc
|
||||||
|
(let [library (first matching)
|
||||||
|
perms (bfc/get-file-permissions conn profile-id (:id library))]
|
||||||
|
(if (:can-edit perms)
|
||||||
|
(assoc acc (:id ext-lib) {:library-id (:id library)
|
||||||
|
:library library})
|
||||||
|
acc)))))))
|
||||||
|
{}
|
||||||
|
(:external-libraries manifest)))
|
||||||
|
|
||||||
|
(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)."
|
||||||
|
|
||||||
|
[{:keys [::db/conn ::manifest ::bfc/team-id ::bfc/timestamp] :as cfg} files-info]
|
||||||
|
(assert (uuid? team-id) "team-id should be provided")
|
||||||
|
|
||||||
|
(let [file-ids (keys files-info)
|
||||||
|
decisions (compute-link-decisions cfg)]
|
||||||
|
|
||||||
|
(reduce
|
||||||
|
(fn [acc ext-lib]
|
||||||
|
(assert (contains? ext-lib :id) "expected `:id` on ext-lib")
|
||||||
|
(assert (contains? ext-lib :name) "expected `:name` on ext-lib")
|
||||||
|
(assert (contains? ext-lib :used-by) "expected `:used-by` on ext-lib")
|
||||||
|
(assert (contains? ext-lib :slug) "expected `:slug` on ext-lib")
|
||||||
|
|
||||||
|
(let [used-by (into #{} (map bfc/lookup-index) (:used-by ext-lib))]
|
||||||
|
(cond
|
||||||
|
;; No slug → skip
|
||||||
|
(nil? (:slug ext-lib))
|
||||||
|
acc
|
||||||
|
|
||||||
|
;; Has decision → auto-link (single match + can-edit)
|
||||||
|
(contains? decisions (:id ext-lib))
|
||||||
|
(let [{:keys [library-id]} (get decisions (:id ext-lib))
|
||||||
|
used-by (filter used-by file-ids)]
|
||||||
|
(doseq [file-id used-by]
|
||||||
|
(let [rel-params {:file-id file-id :library-file-id library-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))))
|
||||||
|
(let [entry {:id (:id ext-lib)
|
||||||
|
:name (:name ext-lib)
|
||||||
|
:linked-to library-id}]
|
||||||
|
(reduce (fn [acc file-id]
|
||||||
|
(add-to-file acc file-id (get files-info file-id) :done entry))
|
||||||
|
acc used-by)))
|
||||||
|
|
||||||
|
;; Has candidates but no decision → multi-match or no permission → pending
|
||||||
|
:else
|
||||||
|
(let [matching-libraries (into [] (bfc/find-shared-files-by-slug cfg team-id (:slug ext-lib)))]
|
||||||
|
(if (empty? matching-libraries)
|
||||||
|
acc
|
||||||
|
(let [candidates (mapv (fn [lib]
|
||||||
|
(let [project-id (:project-id lib)
|
||||||
|
project (bfc/get-project cfg project-id)
|
||||||
|
project-name (:name project)]
|
||||||
|
{:id (:id lib)
|
||||||
|
:name (:name lib)
|
||||||
|
:project-id project-id
|
||||||
|
:project-name project-name}))
|
||||||
|
matching-libraries)
|
||||||
|
entry {:id (:id ext-lib)
|
||||||
|
:name (:name ext-lib)
|
||||||
|
:candidates candidates}]
|
||||||
|
(reduce (fn [acc file-id]
|
||||||
|
(add-to-file acc file-id (get files-info file-id) :pending entry))
|
||||||
|
acc used-by)))))))
|
||||||
|
|
||||||
|
{}
|
||||||
|
(:external-libraries manifest))))
|
||||||
|
|
||||||
(defn- import-files*
|
(defn- import-files*
|
||||||
[{:keys [::manifest] :as cfg}]
|
[{:keys [::manifest] :as cfg}]
|
||||||
(bfc/disable-database-timeouts! cfg)
|
(bfc/disable-database-timeouts! cfg)
|
||||||
@ -890,18 +1019,37 @@
|
|||||||
|
|
||||||
(import-storage-objects cfg)
|
(import-storage-objects cfg)
|
||||||
|
|
||||||
(let [files (get manifest :files)
|
;; Pre-resolve external libraries and add their id mappings to the index
|
||||||
result (reduce (fn [result file]
|
;; BEFORE importing files. This allows relink-refs (inside process-file)
|
||||||
(let [name' (get file :name)
|
;; to correctly remap :component-file references to the destination library.
|
||||||
file (assoc file :name name')]
|
;; Only remap when a link will actually be created (single match + can-edit).
|
||||||
(conj result (import-file cfg file))))
|
(let [decisions (compute-link-decisions cfg)]
|
||||||
[]
|
(doseq [[old-lib-id {:keys [library-id]}] decisions]
|
||||||
files)]
|
(l/trc :hint "pre-resolving external library"
|
||||||
|
:old-id (str old-lib-id)
|
||||||
|
:new-id (str library-id))
|
||||||
|
(vswap! bfc/*state* update :index assoc old-lib-id library-id)))
|
||||||
|
|
||||||
|
(let [files (get manifest :files)
|
||||||
|
file-ids (reduce (fn [result file]
|
||||||
|
(let [name' (get file :name)
|
||||||
|
file (assoc file :name name')]
|
||||||
|
(conj result (import-file cfg file))))
|
||||||
|
[]
|
||||||
|
files)
|
||||||
|
;; Build map of file-id to file-name for resolution
|
||||||
|
files-info (into {} (map (fn [file-id manifest-file]
|
||||||
|
[file-id (:name manifest-file)])
|
||||||
|
file-ids
|
||||||
|
files))]
|
||||||
|
|
||||||
(import-file-relations cfg)
|
(import-file-relations cfg)
|
||||||
(bfm/apply-pending-migrations! cfg)
|
|
||||||
|
|
||||||
result))
|
(let [resolution (resolve-and-link-libraries cfg files-info)]
|
||||||
|
|
||||||
|
(bfm/apply-pending-migrations! cfg)
|
||||||
|
{:file-ids file-ids
|
||||||
|
:resolution resolution})))
|
||||||
|
|
||||||
(defn- import-file-and-overwrite*
|
(defn- import-file-and-overwrite*
|
||||||
[{:keys [::manifest ::bfc/file-id] :as cfg}]
|
[{:keys [::manifest ::bfc/file-id] :as cfg}]
|
||||||
@ -929,7 +1077,8 @@
|
|||||||
(bfc/invalidate-thumbnails cfg file-id)
|
(bfc/invalidate-thumbnails cfg file-id)
|
||||||
(bfm/apply-pending-migrations! cfg)
|
(bfm/apply-pending-migrations! cfg)
|
||||||
|
|
||||||
[file-id])))
|
{:file-ids [file-id]
|
||||||
|
:resolution {}})))
|
||||||
|
|
||||||
(defn- import-files
|
(defn- import-files
|
||||||
[{:keys [::bfc/timestamp ::bfc/input] :or {timestamp (ct/now)} :as cfg}]
|
[{:keys [::bfc/timestamp ::bfc/input] :or {timestamp (ct/now)} :as cfg}]
|
||||||
@ -977,12 +1126,11 @@
|
|||||||
"Do the exportation of a specified file in custom penpot binary
|
"Do the exportation of a specified file in custom penpot binary
|
||||||
format. There are some options available for customize the output:
|
format. There are some options available for customize the output:
|
||||||
|
|
||||||
`::bfc/include-libraries`: additionally to the specified file, all the
|
`::bfc/export-type`: determines how linked libraries are handled.
|
||||||
linked libraries also will be included (including transitive
|
Valid values: `:include-libraries` (include linked libraries),
|
||||||
dependencies).
|
`:merge-libraries` (embed library assets in the file),
|
||||||
|
`:detach-libraries` (treat assets as basic objects),
|
||||||
`::bfc/embed-assets`: instead of including the libraries, embed in the
|
`:link-later` (preserve component metadata for relinking on import)."
|
||||||
same file library all assets used from external libraries."
|
|
||||||
|
|
||||||
[{:keys [::bfc/ids] :as cfg} output]
|
[{:keys [::bfc/ids] :as cfg} output]
|
||||||
|
|
||||||
@ -998,6 +1146,7 @@
|
|||||||
tp (ct/tpoint)
|
tp (ct/tpoint)
|
||||||
ab (volatile! false)
|
ab (volatile! false)
|
||||||
cs (volatile! nil)]
|
cs (volatile! nil)]
|
||||||
|
|
||||||
(try
|
(try
|
||||||
(l/info :hint "start exportation" :export-id (str id))
|
(l/info :hint "start exportation" :export-id (str id))
|
||||||
(binding [bfc/*state* (volatile! (bfc/initial-state))]
|
(binding [bfc/*state* (volatile! (bfc/initial-state))]
|
||||||
|
|||||||
@ -42,17 +42,24 @@
|
|||||||
schema:export-binfile
|
schema:export-binfile
|
||||||
[:map {:title "export-binfile"}
|
[:map {:title "export-binfile"}
|
||||||
[:file-id ::sm/uuid]
|
[:file-id ::sm/uuid]
|
||||||
[:include-libraries ::sm/boolean]
|
[:type {:optional true} [::sm/one-of #{:include-libraries :merge-libraries :detach-libraries :link-later}]]
|
||||||
[:embed-assets ::sm/boolean]])
|
[:include-libraries {:optional true} ::sm/boolean]
|
||||||
|
[:embed-assets {:optional true} ::sm/boolean]])
|
||||||
|
|
||||||
(defn- export-binfile
|
(defn- export-binfile
|
||||||
[{:keys [::sto/storage] :as cfg} {:keys [file-id include-libraries embed-assets]}]
|
[{:keys [::sto/storage] :as cfg} {:keys [type file-id include-libraries embed-assets]}]
|
||||||
(let [output (tmp/tempfile*)]
|
(let [output (tmp/tempfile*)
|
||||||
|
;; Convert legacy boolean flags to unified export-type
|
||||||
|
export-type (cond
|
||||||
|
(some? type) type
|
||||||
|
(true? include-libraries) :include-libraries
|
||||||
|
(true? embed-assets) :merge-libraries
|
||||||
|
:else :detach-libraries)]
|
||||||
|
|
||||||
(try
|
(try
|
||||||
(-> cfg
|
(-> cfg
|
||||||
(assoc ::bfc/ids #{file-id})
|
(assoc ::bfc/ids #{file-id})
|
||||||
(assoc ::bfc/embed-assets embed-assets)
|
(assoc ::bfc/export-type export-type)
|
||||||
(assoc ::bfc/include-libraries include-libraries)
|
|
||||||
(bf.v3/export-files! output))
|
(bf.v3/export-files! output))
|
||||||
|
|
||||||
(let [data (sto/content output)
|
(let [data (sto/content output)
|
||||||
@ -73,7 +80,8 @@
|
|||||||
(sv/defmethod ::export-binfile
|
(sv/defmethod ::export-binfile
|
||||||
"Export a penpot file in a binary format."
|
"Export a penpot file in a binary format."
|
||||||
{::doc/added "1.15"
|
{::doc/added "1.15"
|
||||||
::doc/changes [["2.12" "Remove version parameter, only one version is supported"]]
|
::doc/changes [["2.12" "Remove version parameter, only one version is supported"]
|
||||||
|
["2.19" "Deprecated `include-libraries` and `embed-assets` params"]]
|
||||||
::webhooks/event? true
|
::webhooks/event? true
|
||||||
::sm/params schema:export-binfile}
|
::sm/params schema:export-binfile}
|
||||||
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
[cfg {:keys [::rpc/profile-id file-id] :as params}]
|
||||||
@ -94,6 +102,7 @@
|
|||||||
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team))
|
(assoc ::bfc/features (cfeat/get-team-enabled-features cf/flags team))
|
||||||
(assoc ::bfc/project-id project-id)
|
(assoc ::bfc/project-id project-id)
|
||||||
(assoc ::bfc/profile-id profile-id)
|
(assoc ::bfc/profile-id profile-id)
|
||||||
|
(assoc ::bfc/team-id (:id team))
|
||||||
(assoc ::bfc/name name))
|
(assoc ::bfc/name name))
|
||||||
|
|
||||||
input-path (:path file)
|
input-path (:path file)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
BIN
backend/test/backend_tests/test_files/file-with-library.penpot
Normal file
BIN
backend/test/backend_tests/test_files/file-with-library.penpot
Normal file
Binary file not shown.
@ -100,7 +100,7 @@ RUN set -eux; \
|
|||||||
|
|
||||||
FROM base AS setup-opencode
|
FROM base AS setup-opencode
|
||||||
|
|
||||||
ENV OPENCODE_VERSION=1.18.19
|
ENV OPENCODE_VERSION=1.18.21
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
ARCH="$(dpkg --print-architecture)"; \
|
||||||
|
|||||||
@ -21,8 +21,8 @@
|
|||||||
:exclusions [funcool/beicon2]}
|
:exclusions [funcool/beicon2]}
|
||||||
|
|
||||||
funcool/beicon2
|
funcool/beicon2
|
||||||
{:git/tag "v2.2"
|
{:git/tag "v2.3"
|
||||||
:git/sha "8744c66"
|
:git/sha "df7058a"
|
||||||
:git/url "https://github.com/funcool/beicon.git"}
|
:git/url "https://github.com/funcool/beicon.git"}
|
||||||
|
|
||||||
funcool/rumext
|
funcool/rumext
|
||||||
|
|||||||
@ -17,18 +17,21 @@
|
|||||||
[potok.v2.core :as ptk]))
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
(def valid-types
|
(def valid-types
|
||||||
(d/ordered-set :all :merge :detach))
|
(d/ordered-set :include-libraries :merge-libraries :detach-libraries :link-later))
|
||||||
|
|
||||||
(def valid-formats
|
(def valid-formats
|
||||||
#{:binfile-v1 :binfile-v3 :legacy-zip})
|
#{:binfile-v1 :binfile-v3})
|
||||||
|
|
||||||
|
(def ^:private schema:export-file-param
|
||||||
|
[:map {:title "FileParam"}
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:name :string]
|
||||||
|
[:project-id ::sm/uuid]
|
||||||
|
[:is-shared ::sm/boolean]
|
||||||
|
#_[:has-libraries ::sm/boolean]])
|
||||||
|
|
||||||
(def ^:private schema:export-files
|
(def ^:private schema:export-files
|
||||||
[:sequential {:title "Files"}
|
[:sequential {:title "Files"} schema:export-file-param])
|
||||||
[:map {:title "FileParam"}
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:name :string]
|
|
||||||
[:project-id ::sm/uuid]
|
|
||||||
[:is-shared ::sm/boolean]]])
|
|
||||||
|
|
||||||
(def check-export-files
|
(def check-export-files
|
||||||
(sm/check-fn schema:export-files))
|
(sm/check-fn schema:export-files))
|
||||||
@ -57,14 +60,17 @@
|
|||||||
:files files}))))))))))
|
:files files}))))))))))
|
||||||
|
|
||||||
(defn export-files
|
(defn export-files
|
||||||
|
"Start files exportation process"
|
||||||
[& {:keys [type files]}]
|
[& {:keys [type files]}]
|
||||||
|
(assert (check-export-files files) "expected a sequence of files")
|
||||||
|
(assert (valid-types type) "expected valid export type")
|
||||||
|
|
||||||
(->> (rx/from files)
|
(->> (rx/from files)
|
||||||
(rx/mapcat
|
(rx/mapcat
|
||||||
(fn [file]
|
(fn [file]
|
||||||
(->> (rp/cmd! ::sse/export-binfile {:file-id (:id file)
|
(->> (rp/cmd! ::sse/export-binfile {:file-id (:id file)
|
||||||
:version 3
|
:version 3
|
||||||
:include-libraries (= type :all)
|
:type type})
|
||||||
:embed-assets (= type :merge)})
|
|
||||||
(rx/filter sse/end-of-stream?)
|
(rx/filter sse/end-of-stream?)
|
||||||
(rx/map sse/get-payload)
|
(rx/map sse/get-payload)
|
||||||
(rx/map (fn [uri]
|
(rx/map (fn [uri]
|
||||||
|
|||||||
@ -15,11 +15,20 @@
|
|||||||
[app.main.data.event :as ev]
|
[app.main.data.event :as ev]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.notifications :as ntf]
|
[app.main.data.notifications :as ntf]
|
||||||
|
[app.main.repo :as rp]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||||
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||||
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
|
[app.main.ui.ds.controls.checkbox :refer [checkbox*]]
|
||||||
|
[app.main.ui.ds.controls.select :refer [select*]]
|
||||||
|
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||||
|
[app.main.ui.ds.foundations.typography :as t]
|
||||||
|
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||||
|
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||||
|
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||||
[app.main.ui.icons :as deprecated-icon]
|
[app.main.ui.icons :as deprecated-icon]
|
||||||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
|
||||||
[app.main.worker :as mw]
|
[app.main.worker :as mw]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
@ -54,7 +63,7 @@
|
|||||||
{::mf/forward-ref true}
|
{::mf/forward-ref true}
|
||||||
[{:keys [project-id on-finish-import]} external-ref]
|
[{:keys [project-id on-finish-import]} external-ref]
|
||||||
(let [on-file-selected (use-import-file project-id on-finish-import)]
|
(let [on-file-selected (use-import-file project-id on-finish-import)]
|
||||||
[:form.import-file {:aria-hidden "true"}
|
[:form {:aria-hidden "true"}
|
||||||
[:& file-uploader {:accept ".penpot,.zip"
|
[:& file-uploader {:accept ".penpot,.zip"
|
||||||
:multi true
|
:multi true
|
||||||
:ref external-ref
|
:ref external-ref
|
||||||
@ -156,6 +165,19 @@
|
|||||||
(and (= :import-ready (:status item))
|
(and (= :import-ready (:status item))
|
||||||
(not (:deleted item))))
|
(not (:deleted item))))
|
||||||
|
|
||||||
|
(defn- has-unresolved?
|
||||||
|
"Return true if a file-resolution has any :pending needing user choice."
|
||||||
|
[file-resolution]
|
||||||
|
(some? (seq (:pending file-resolution))))
|
||||||
|
|
||||||
|
(defn- count-auto-linked
|
||||||
|
"Count auto-linked libraries across all file resolutions."
|
||||||
|
[resolution]
|
||||||
|
(reduce-kv (fn [acc _ {:keys [done]}]
|
||||||
|
(+ acc (count done)))
|
||||||
|
0
|
||||||
|
resolution))
|
||||||
|
|
||||||
(defn- analyze-entries
|
(defn- analyze-entries
|
||||||
[state entries]
|
[state entries]
|
||||||
(let [features (get @st/state :features)]
|
(let [features (get @st/state :features)]
|
||||||
@ -173,7 +195,7 @@
|
|||||||
(swap! state update-with-analyze-result message))))))
|
(swap! state update-with-analyze-result message))))))
|
||||||
|
|
||||||
(defn- import-files
|
(defn- import-files
|
||||||
[state project-id entries]
|
[state library-resolution-data* project-id entries]
|
||||||
(st/emit! (ev/event {::ev/name "import-files"
|
(st/emit! (ev/event {::ev/name "import-files"
|
||||||
:num-files (count entries)}))
|
:num-files (count entries)}))
|
||||||
|
|
||||||
@ -183,27 +205,40 @@
|
|||||||
:project-id project-id
|
:project-id project-id
|
||||||
:files entries
|
:files entries
|
||||||
:features features})
|
:features features})
|
||||||
(rx/filter (comp uuid? :file-id))
|
(rx/filter some?)
|
||||||
(rx/subs!
|
(rx/subs!
|
||||||
(fn [message]
|
(fn [message]
|
||||||
(swap! state update-entry-status message))))))
|
;; Capture library-resolution data if present (same for all
|
||||||
|
;; entries from the same zip, so first one wins)
|
||||||
|
(if-let [resolution (-> (:libraries-resolution message)
|
||||||
|
(not-empty))]
|
||||||
|
(reset! library-resolution-data* resolution)
|
||||||
|
(swap! state update-entry-status message)))))))
|
||||||
|
|
||||||
(mf/defc import-entry*
|
(mf/defc import-entry*
|
||||||
{::mf/memo true
|
{::mf/memo true
|
||||||
::mf/private true}
|
::mf/private true}
|
||||||
[{:keys [entries entry edition can-be-deleted importing? on-edit on-change on-delete]}]
|
[{:keys [entries entry edition can-be-deleted is-progress on-edit on-change on-delete]}]
|
||||||
(let [status (:status entry)
|
(let [status (:status entry)
|
||||||
;; FIXME: rename to format
|
;; FIXME: rename to format
|
||||||
format (:type entry)
|
format (:type entry)
|
||||||
|
|
||||||
loading? (or (= :analyze status)
|
loading? (or (= :analyze status)
|
||||||
(= :import-progress status)
|
(= :import-progress status)
|
||||||
(and importing? (= :import-ready status)))
|
(and is-progress (= :import-ready status)))
|
||||||
analyze-error? (= :analyze-error status)
|
analyze-error? (= :analyze-error status)
|
||||||
import-success? (= :import-success status)
|
import-success? (= :import-success status)
|
||||||
import-error? (= :import-error status)
|
import-error? (= :import-error status)
|
||||||
import-ready? (= :import-ready status)
|
import-ready? (= :import-ready status)
|
||||||
|
|
||||||
|
level (cond
|
||||||
|
import-success? :success
|
||||||
|
import-ready? :success
|
||||||
|
import-error? :error
|
||||||
|
analyze-error? :error
|
||||||
|
loading? nil
|
||||||
|
:else :default)
|
||||||
|
|
||||||
is-shared? (:shared entry)
|
is-shared? (:shared entry)
|
||||||
progress (:progress entry)
|
progress (:progress entry)
|
||||||
|
|
||||||
@ -251,46 +286,65 @@
|
|||||||
:editable (and import-ready? (not editing?)))}
|
:editable (and import-ready? (not editing?)))}
|
||||||
|
|
||||||
[:div {:class (stl/css :file-name)}
|
[:div {:class (stl/css :file-name)}
|
||||||
(if loading?
|
(when loading? [:> loader* {:width 26 :title (tr "labels.loading")}])
|
||||||
[:> loader* {:width 16 :title (tr "labels.loading")}]
|
|
||||||
[:div {:class (stl/css-case
|
|
||||||
:file-icon true
|
|
||||||
:icon-fill import-ready?)}
|
|
||||||
(cond
|
|
||||||
import-ready? deprecated-icon/logo-icon
|
|
||||||
import-error? deprecated-icon/close
|
|
||||||
import-success? deprecated-icon/tick
|
|
||||||
analyze-error? deprecated-icon/close)])
|
|
||||||
|
|
||||||
(if editing?
|
(if editing?
|
||||||
[:div {:class (stl/css :file-name-edit)}
|
[:div {:class (stl/css :file-name-edit)}
|
||||||
[:input {:type "text"
|
[:input {:type "text"
|
||||||
:auto-focus true
|
:auto-focus true
|
||||||
|
:class (stl/css :file-name-input)
|
||||||
|
;;TODO: Add translation for aria-label
|
||||||
|
:aria-label "File name"
|
||||||
:default-value (:name entry)
|
:default-value (:name entry)
|
||||||
:on-key-press on-edit-key-press
|
:on-key-press on-edit-key-press
|
||||||
:on-blur on-edit-blur}]]
|
:on-blur on-edit-blur}]]
|
||||||
|
|
||||||
[:div {:class (stl/css :file-name-label)}
|
[:div {:class (stl/css :file-name-label)}
|
||||||
(:name entry)
|
(if loading?
|
||||||
(when ^boolean is-shared?
|
[:> text* {:class (stl/css :file-name-label)
|
||||||
[:span {:class (stl/css :icon)}
|
:as "span"
|
||||||
deprecated-icon/library])])
|
:typography t/body-medium}
|
||||||
|
(:name entry)
|
||||||
[:div {:class (stl/css :edit-entry-buttons)}
|
(when ^boolean is-shared?
|
||||||
(when ^boolean editable?
|
[:> icon* {:icon-id i/library :class (stl/css :file-label-icon)}])]
|
||||||
[:button {:on-click on-edit'} deprecated-icon/curve])
|
[:> context-notification*
|
||||||
(when ^boolean can-be-deleted
|
{:level level
|
||||||
[:button {:on-click on-delete'} deprecated-icon/delete])]]
|
:appearance :ghost
|
||||||
|
:class (stl/css :file-name-notification)}
|
||||||
|
[:> text* {:class (stl/css :file-name-label)
|
||||||
|
:as "span"
|
||||||
|
:typography t/body-medium}
|
||||||
|
(:name entry)
|
||||||
|
(when ^boolean is-shared?
|
||||||
|
[:> icon* {:icon-id i/library :class (stl/css :file-label-icon)}])]])])
|
||||||
|
|
||||||
|
(when ^boolean (or editable? can-be-deleted)
|
||||||
|
[:div {:class (stl/css :edit-entry-buttons)}
|
||||||
|
(when ^boolean editable?
|
||||||
|
[:> icon-button* {:on-click on-edit'
|
||||||
|
:variant "ghost"
|
||||||
|
:icon-size "s"
|
||||||
|
:aria-label (tr "labels.edit")
|
||||||
|
:icon i/curve}])
|
||||||
|
(when ^boolean can-be-deleted
|
||||||
|
[:> icon-button* {:on-click on-delete'
|
||||||
|
:variant "ghost"
|
||||||
|
:icon-size "s"
|
||||||
|
:aria-label (tr "labels.delete")
|
||||||
|
:icon i/delete}])])]
|
||||||
(cond
|
(cond
|
||||||
analyze-error?
|
analyze-error?
|
||||||
[:div {:class (stl/css :error-message)}
|
[:> text* {:class (stl/css :error-message)
|
||||||
|
:as "span"
|
||||||
|
:typography t/body-small}
|
||||||
(if (some? (:error entry))
|
(if (some? (:error entry))
|
||||||
(tr (:error entry))
|
(tr (:error entry))
|
||||||
(tr "dashboard.import.analyze-error"))]
|
(tr "dashboard.import.analyze-error"))]
|
||||||
|
|
||||||
import-error?
|
import-error?
|
||||||
[:div {:class (stl/css :error-message)}
|
[:> text* {:class (stl/css :error-message)
|
||||||
|
:as "span"
|
||||||
|
:typography t/body-small}
|
||||||
(if (some? (:error entry))
|
(if (some? (:error entry))
|
||||||
(tr (:error entry))
|
(tr (:error entry))
|
||||||
(tr "labels.error"))]
|
(tr "labels.error"))]
|
||||||
@ -318,6 +372,327 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(mapv #(assoc % :status :analyze) entries)))
|
(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 [unresolved-file selection on-select]}]
|
||||||
|
(let [candidates (:pending unresolved-file)
|
||||||
|
disconnected* (mf/use-state #{})
|
||||||
|
disconnected (deref disconnected*)
|
||||||
|
on-change-disconnected
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [id]
|
||||||
|
(swap! disconnected*
|
||||||
|
(fn [s]
|
||||||
|
(if (contains? s id) (disj s id) (conj s id))))))]
|
||||||
|
|
||||||
|
;; Pre-select first candidate for each library
|
||||||
|
(mf/with-effect [candidates]
|
||||||
|
(doseq [{:keys [id candidates]} candidates]
|
||||||
|
(when-not (contains? selection id)
|
||||||
|
(when-let [first-c (first candidates)]
|
||||||
|
(on-select id (str (:id first-c)))))))
|
||||||
|
|
||||||
|
[:div {:class (stl/css :library-resolution)}
|
||||||
|
[:> 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:"]
|
||||||
|
|
||||||
|
|
||||||
|
[:table {:class (stl/css :library-resolution-table)}
|
||||||
|
[:thead
|
||||||
|
[:tr {:class (stl/css :library-resolution-header)}
|
||||||
|
[:th {:class (stl/css :library-origin-name)}
|
||||||
|
[:> icon* {:icon-id i/library
|
||||||
|
:class (stl/css :library-resolution-icon)
|
||||||
|
:size "s"}]
|
||||||
|
"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"]]]
|
||||||
|
[:tbody {:class (stl/css :library-resolution-body)}
|
||||||
|
(for [{:keys [id name candidates]} candidates]
|
||||||
|
(let [options (mapv (fn [c]
|
||||||
|
{:id (str (:id c))
|
||||||
|
:label (str (:name c) " (" (:project-name c) ")")})
|
||||||
|
candidates)
|
||||||
|
selected (get selection id)
|
||||||
|
is-conected (not (contains? disconnected id))]
|
||||||
|
[:tr {:class (stl/css :library-resolution-item)
|
||||||
|
:key (dm/str id)}
|
||||||
|
[:td {:class (stl/css :library-resolution-item-name)}
|
||||||
|
[:> checkbox* {:id (dm/str id)
|
||||||
|
:label name
|
||||||
|
:checked is-conected
|
||||||
|
:on-change #(on-change-disconnected id)}]]
|
||||||
|
[:td {:class (stl/css :library-resolution-arrow)}
|
||||||
|
[:> icon* {:icon-id i/row
|
||||||
|
:size "m"}]]
|
||||||
|
[:td
|
||||||
|
(if is-conected
|
||||||
|
[:> select* {:options options
|
||||||
|
:class (stl/css :library-resolution-select)
|
||||||
|
:default-selected (or (some-> selected str) "")
|
||||||
|
:has-portal true
|
||||||
|
:on-change (partial on-select id)}]
|
||||||
|
|
||||||
|
[:> text* {:class (stl/css :library-resolution-no-selection)
|
||||||
|
:as "span"
|
||||||
|
:typography t/body-small}
|
||||||
|
(let [selected-c (or (some #(when (= (str (:id %)) selected) %) candidates)
|
||||||
|
(first candidates))]
|
||||||
|
(dm/str (:name selected-c) " (" (:project-name selected-c) ")"))])]]))]]]))
|
||||||
|
|
||||||
|
(mf/defc library-resolution-summary-file*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [resolution-file selection]}]
|
||||||
|
(let [done (:done resolution-file)
|
||||||
|
pending (:pending resolution-file)]
|
||||||
|
[:div {:class (stl/css :summary-file)}
|
||||||
|
[:div {:class (stl/css :summary-file-header)}
|
||||||
|
[:> icon* {:icon-id i/document
|
||||||
|
:class (stl/css :summary-file-icon)
|
||||||
|
:size "s"}]
|
||||||
|
[:> text* {:class (stl/css :summary-file-name)
|
||||||
|
:as "span"
|
||||||
|
: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")]])]])
|
||||||
|
|
||||||
|
(when (seq pending)
|
||||||
|
[:div {:class (stl/css :summary-section)}
|
||||||
|
[:div {:class (stl/css :summary-section-header)}
|
||||||
|
;; TODO: Add translation for this string
|
||||||
|
|
||||||
|
[:> 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)}
|
||||||
|
"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")])]))]])]))
|
||||||
|
|
||||||
|
(mf/defc library-resolution-summary*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [resolution selection]}]
|
||||||
|
[:div {:class (stl/css :library-resolution)}
|
||||||
|
[:p {:class (stl/css :library-resolution-message)}
|
||||||
|
(tr "dashboard.import.resolve-libraries-summary")]
|
||||||
|
|
||||||
|
(for [[file-id resolution-file] resolution]
|
||||||
|
[:> library-resolution-summary-file*
|
||||||
|
{:key (dm/str file-id)
|
||||||
|
:resolution-file resolution-file
|
||||||
|
:selection selection}])])
|
||||||
|
|
||||||
|
|
||||||
|
;; ── Stage components ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
(mf/defc import-files-stage*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [entries template status errors? import-success-total auto-linked-count
|
||||||
|
edition on-edit on-change on-delete
|
||||||
|
on-cancel on-continue on-accept pending-analysis?]}]
|
||||||
|
[:*
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
(when (and (= :analyze status) errors?)
|
||||||
|
[:> context-notification*
|
||||||
|
{:level :warning
|
||||||
|
:class (stl/css :context-notification-error)}
|
||||||
|
(tr "dashboard.import.import-warning")])
|
||||||
|
|
||||||
|
(when (= :import-success status)
|
||||||
|
[:*
|
||||||
|
[:> context-notification*
|
||||||
|
{:level (if (zero? import-success-total) :warning :success)}
|
||||||
|
(tr "dashboard.import.import-message" (i18n/c import-success-total))]
|
||||||
|
(when (pos? auto-linked-count)
|
||||||
|
[:> context-notification*
|
||||||
|
{:level :success}
|
||||||
|
(tr "dashboard.import.auto-linked-libraries" (i18n/c auto-linked-count))])])
|
||||||
|
|
||||||
|
(when (= :import-error status)
|
||||||
|
[:> context-notification*
|
||||||
|
{:level :error
|
||||||
|
:class (stl/css :context-notification-error)}
|
||||||
|
(tr "dashboard.import.import-error.disclaimer")])
|
||||||
|
|
||||||
|
(when (or (= :import-error status) (and (= :analyze status) errors?))
|
||||||
|
[:div {:class (stl/css :import-error-disclaimer)}
|
||||||
|
[:div (tr "dashboard.import.import-error.message1")]
|
||||||
|
[:ul {:class (stl/css :import-error-list)}
|
||||||
|
(for [entry entries]
|
||||||
|
(when (contains? #{:import-error :analyze-error} (:status entry))
|
||||||
|
[:li {:class (stl/css :import-error-list-enry)
|
||||||
|
:key (dm/str (or (:file-id entry) (:uri entry) (:name entry)))}
|
||||||
|
[:div (:name entry)]
|
||||||
|
(when-let [err (:error entry)]
|
||||||
|
[:div {:class (stl/css :import-error-detail)}
|
||||||
|
(cond
|
||||||
|
(and (string? err)
|
||||||
|
(str/includes? (str/lower err) "check error"))
|
||||||
|
(tr "dashboard.import.import-error.check-error")
|
||||||
|
|
||||||
|
(and (string? err)
|
||||||
|
(str/includes? (str/lower err) "corrupt"))
|
||||||
|
(tr "dashboard.import.import-error.corrupt-file")
|
||||||
|
|
||||||
|
:else
|
||||||
|
(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
|
||||||
|
:is-progress (= :import-progress status)
|
||||||
|
:on-edit on-edit
|
||||||
|
:on-change on-change
|
||||||
|
:on-delete on-delete
|
||||||
|
:can-be-deleted (> (count entries) 1)}])
|
||||||
|
|
||||||
|
(when (some? template)
|
||||||
|
[:> import-entry* {:entry (assoc template :status status)
|
||||||
|
:can-be-deleted false}])
|
||||||
|
|
||||||
|
(when (= :import-progress status)
|
||||||
|
[:div {:class (stl/css :status-message)
|
||||||
|
:role "status"
|
||||||
|
:aria-live "polite"}
|
||||||
|
(tr "labels.uploading-file")])]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-footer)}
|
||||||
|
[:div {:class (stl/css :action-buttons)}
|
||||||
|
(case status
|
||||||
|
:analyze
|
||||||
|
[:> button* {:class (stl/css :cancel-button)
|
||||||
|
:variant "secondary"
|
||||||
|
:on-click on-cancel}
|
||||||
|
(tr "labels.cancel")]
|
||||||
|
|
||||||
|
:import-ready
|
||||||
|
[:> button* {:class (stl/css :accept-btn)
|
||||||
|
:variant "primary"
|
||||||
|
:disabled pending-analysis?
|
||||||
|
:on-click on-continue}
|
||||||
|
(tr "labels.continue")]
|
||||||
|
|
||||||
|
:import-progress
|
||||||
|
[:> button* {:class (stl/css :accept-btn)
|
||||||
|
:variant "primary"
|
||||||
|
:disabled true
|
||||||
|
:on-click on-accept}
|
||||||
|
(tr "labels.accept")]
|
||||||
|
|
||||||
|
(:import-success :import-error)
|
||||||
|
[:> button* {:class (stl/css :accept-btn)
|
||||||
|
:variant "primary"
|
||||||
|
:on-click on-accept}
|
||||||
|
(tr "labels.accept")])]]])
|
||||||
|
|
||||||
|
(mf/defc import-library-resolution-stage*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [current-unresolved-file selection on-select
|
||||||
|
visited all-visited?
|
||||||
|
on-wizard-prev on-wizard-next]}]
|
||||||
|
[:*
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:> library-resolution*
|
||||||
|
{:unresolved-file current-unresolved-file
|
||||||
|
:selection selection
|
||||||
|
:on-select on-select}]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-footer)}
|
||||||
|
[:div {:class (stl/css :action-buttons)}
|
||||||
|
(when (seq visited)
|
||||||
|
[:> button* {:class (stl/css :cancel-button)
|
||||||
|
: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"))]]]])
|
||||||
|
|
||||||
|
(mf/defc import-library-summary-stage*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [resolution selection visited
|
||||||
|
on-summary-back on-confirm-library-links]}]
|
||||||
|
[:*
|
||||||
|
[:div {:class (stl/css :modal-content)}
|
||||||
|
[:> library-resolution-summary*
|
||||||
|
{:resolution resolution
|
||||||
|
:selection selection}]]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :modal-footer)}
|
||||||
|
[:div {:class (stl/css :action-buttons)}
|
||||||
|
(when (seq visited)
|
||||||
|
[:> button* {:class (stl/css :cancel-button)
|
||||||
|
: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")]]]])
|
||||||
|
|
||||||
(mf/defc import-dialog
|
(mf/defc import-dialog
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
::mf/register-as :import
|
::mf/register-as :import
|
||||||
@ -329,14 +704,49 @@
|
|||||||
;; Revoke all uri's on commonent unmount
|
;; Revoke all uri's on commonent unmount
|
||||||
(fn [] (run! wapi/revoke-uri (map :uri entries))))
|
(fn [] (run! wapi/revoke-uri (map :uri entries))))
|
||||||
|
|
||||||
(let [state* (mf/use-state (initialize-state entries))
|
(let [state* (mf/use-state (initialize-state entries))
|
||||||
entries (deref state*)
|
entries (deref state*)
|
||||||
|
|
||||||
status* (mf/use-state :analyze)
|
status* (mf/use-state :analyze)
|
||||||
status (deref status*)
|
status (deref status*)
|
||||||
|
|
||||||
edition* (mf/use-state nil)
|
edition* (mf/use-state nil)
|
||||||
edition (deref edition*)
|
edition (deref edition*)
|
||||||
|
|
||||||
|
;; Library resolution data from the backend (auto-linked + multi-match)
|
||||||
|
resolution* (mf/use-state nil)
|
||||||
|
resolution (not-empty (deref resolution*))
|
||||||
|
|
||||||
|
;; User selection for multi-match candidates: {old-lib-id candidate-id}
|
||||||
|
selection* (mf/use-state {})
|
||||||
|
selection (deref selection*)
|
||||||
|
|
||||||
|
;; 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 #(d/ordered-set))
|
||||||
|
visited (deref visited*)
|
||||||
|
|
||||||
|
;; Derived: files that need user resolution (have :candidates)
|
||||||
|
unresolved-files
|
||||||
|
(mf/with-memo [resolution]
|
||||||
|
(when resolution
|
||||||
|
(reduce-kv (fn [acc _ v]
|
||||||
|
(if (has-unresolved? v)
|
||||||
|
(conj acc v)
|
||||||
|
acc))
|
||||||
|
[]
|
||||||
|
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-unresolved-file
|
||||||
|
(mf/with-memo [unresolved-files visited]
|
||||||
|
(d/seek #(not (contains? visited (:id %))) unresolved-files))
|
||||||
|
|
||||||
continue-entries
|
continue-entries
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -344,7 +754,7 @@
|
|||||||
(fn []
|
(fn []
|
||||||
(let [entries (filterv has-status-ready? entries)]
|
(let [entries (filterv has-status-ready? entries)]
|
||||||
(reset! status* :import-progress)
|
(reset! status* :import-progress)
|
||||||
(import-files state* project-id entries))))
|
(import-files state* resolution* project-id entries))))
|
||||||
|
|
||||||
continue-template
|
continue-template
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -407,6 +817,52 @@
|
|||||||
(continue-template template)
|
(continue-template template)
|
||||||
(continue-entries))))
|
(continue-entries))))
|
||||||
|
|
||||||
|
on-confirm-library-links
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps resolution selection on-finish-import)
|
||||||
|
(fn [event]
|
||||||
|
(dom/prevent-default event)
|
||||||
|
(let [slc selection]
|
||||||
|
;; For each file with pending candidates, link it to the selected libraries
|
||||||
|
(->> (rx/from (seq resolution))
|
||||||
|
(rx/merge-map
|
||||||
|
(fn [[file-id resolution-file]]
|
||||||
|
(->> (rx/from (:pending resolution-file))
|
||||||
|
(rx/merge-map
|
||||||
|
(fn [{:keys [id]}]
|
||||||
|
(when-let [selected-lib (get slc id)]
|
||||||
|
(link-files-to-library! [file-id] selected-lib)))))))
|
||||||
|
(rx/subs! (constantly nil)
|
||||||
|
(constantly nil)
|
||||||
|
(fn []
|
||||||
|
(st/emit! (modal/hide))
|
||||||
|
(when (fn? on-finish-import)
|
||||||
|
(on-finish-import))))))))
|
||||||
|
|
||||||
|
on-wizard-next
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps current-unresolved-file visited)
|
||||||
|
(fn []
|
||||||
|
(let [file-id (:id current-unresolved-file)]
|
||||||
|
(swap! visited* conj file-id))))
|
||||||
|
|
||||||
|
on-wizard-prev
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps current-unresolved-file)
|
||||||
|
(fn []
|
||||||
|
;; Remove the current file from visited; it becomes current again after re-render,
|
||||||
|
;; because it's no longer in visited.
|
||||||
|
(let [file-id (:id current-unresolved-file)]
|
||||||
|
(swap! visited* disj file-id))))
|
||||||
|
|
||||||
|
on-summary-back
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps visited)
|
||||||
|
(fn []
|
||||||
|
(let [last-id (last visited)]
|
||||||
|
(swap! visited* disj last-id)
|
||||||
|
(reset! status* :library-resolution))))
|
||||||
|
|
||||||
on-accept
|
on-accept
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-finish-import)
|
(mf/deps on-finish-import)
|
||||||
@ -432,9 +888,25 @@
|
|||||||
(zero? (count entries))))
|
(zero? (count entries))))
|
||||||
|
|
||||||
pending-analysis?
|
pending-analysis?
|
||||||
(some has-status-analyze? entries)]
|
(some has-status-analyze? entries)
|
||||||
|
|
||||||
(mf/with-effect [entries]
|
auto-linked-count
|
||||||
|
(if (some? resolution)
|
||||||
|
(count-auto-linked resolution)
|
||||||
|
0)
|
||||||
|
|
||||||
|
manage-on-select
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps selection)
|
||||||
|
(fn [old-lib-id candidate-id]
|
||||||
|
(swap! selection* assoc old-lib-id candidate-id)))]
|
||||||
|
|
||||||
|
(mf/with-effect [visited unresolved-files]
|
||||||
|
(when (and (seq unresolved-files)
|
||||||
|
(every? #(contains? visited (:id %)) unresolved-files))
|
||||||
|
(reset! status* :library-summary)))
|
||||||
|
|
||||||
|
(mf/with-effect [entries resolution]
|
||||||
(cond
|
(cond
|
||||||
(some? template)
|
(some? template)
|
||||||
(reset! status* :import-ready)
|
(reset! status* :import-ready)
|
||||||
@ -445,8 +917,11 @@
|
|||||||
|
|
||||||
(and (seq entries)
|
(and (seq entries)
|
||||||
(every? #(= :import-success (:status %)) entries))
|
(every? #(= :import-success (:status %)) entries))
|
||||||
(reset! status* :import-success)
|
(reset! status* (if (seq resolution)
|
||||||
|
(if (seq (filter has-unresolved? (vals resolution)))
|
||||||
|
:library-resolution
|
||||||
|
:library-summary)
|
||||||
|
:import-success))
|
||||||
(and (seq entries)
|
(and (seq entries)
|
||||||
(and (every? #(not= :import-ready (:status %)) entries)
|
(and (every? #(not= :import-ready (:status %)) entries)
|
||||||
(some #(= :import-error (:status %)) entries)))
|
(some #(= :import-error (:status %)) entries)))
|
||||||
@ -460,99 +935,50 @@
|
|||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css :modal-overlay)}
|
||||||
[:div {:class (stl/css :modal-container)}
|
[:div {:class (stl/css :modal-container)}
|
||||||
[:div {:class (stl/css :modal-header)}
|
[:div {:class (stl/css :modal-header)}
|
||||||
[:h2 {:class (stl/css :modal-title)} (tr "dashboard.import")]
|
[:> heading* {:level 2
|
||||||
|
:typography t/headline-large
|
||||||
|
:class (stl/css :modal-title)}
|
||||||
|
(tr "dashboard.import")]
|
||||||
|
[:> icon-button* {:variant "ghost"
|
||||||
|
:aria-label (tr "labels.close")
|
||||||
|
:on-click on-cancel
|
||||||
|
:class (stl/css :modal-close-btn)
|
||||||
|
:icon i/close}]]
|
||||||
|
|
||||||
[:button {:class (stl/css :modal-close-btn)
|
(case status
|
||||||
:on-click on-cancel} deprecated-icon/close]]
|
(:analyze :import-ready :import-progress :import-success :import-error)
|
||||||
|
[:> import-files-stage*
|
||||||
|
{:entries entries
|
||||||
|
:template template
|
||||||
|
:status status
|
||||||
|
:errors? errors?
|
||||||
|
:import-success-total import-success-total
|
||||||
|
:auto-linked-count auto-linked-count
|
||||||
|
:edition edition
|
||||||
|
:on-edit on-edit
|
||||||
|
:on-change on-entry-change
|
||||||
|
:on-delete on-entry-delete
|
||||||
|
:on-cancel on-cancel
|
||||||
|
:on-continue on-continue
|
||||||
|
:on-accept on-accept
|
||||||
|
:pending-analysis? pending-analysis?}]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-content)}
|
:library-resolution
|
||||||
(when (and (= :analyze status) errors?)
|
[:> import-library-resolution-stage*
|
||||||
[:& context-notification
|
{:current-unresolved-file current-unresolved-file
|
||||||
{:level :warning
|
:selection selection
|
||||||
:class (stl/css :context-notification-error)
|
:on-select manage-on-select
|
||||||
:content (tr "dashboard.import.import-warning")}])
|
:visited visited
|
||||||
|
:all-visited? all-visited?
|
||||||
|
:on-wizard-prev on-wizard-prev
|
||||||
|
:on-wizard-next on-wizard-next}]
|
||||||
|
|
||||||
(when (= :import-success status)
|
:library-summary
|
||||||
[:& context-notification
|
[:> import-library-summary-stage*
|
||||||
{:level (if (zero? import-success-total) :warning :success)
|
{:resolution resolution
|
||||||
:content (tr "dashboard.import.import-message" (i18n/c import-success-total))}])
|
:selection selection
|
||||||
|
:visited visited
|
||||||
|
:on-summary-back on-summary-back
|
||||||
|
:on-confirm-library-links on-confirm-library-links}]
|
||||||
|
|
||||||
(when (= :import-error status)
|
nil)]]))
|
||||||
[:& context-notification
|
|
||||||
{:level :error
|
|
||||||
:class (stl/css :context-notification-error)
|
|
||||||
:content (tr "dashboard.import.import-error.disclaimer")}])
|
|
||||||
|
|
||||||
(if (or (= :import-error status) (and (= :analyze status) errors?))
|
|
||||||
[:div {:class (stl/css :import-error-disclaimer)}
|
|
||||||
[:div (tr "dashboard.import.import-error.message1")]
|
|
||||||
[:ul {:class (stl/css :import-error-list)}
|
|
||||||
(for [entry entries]
|
|
||||||
(when (contains? #{:import-error :analyze-error} (:status entry))
|
|
||||||
[:li {:class (stl/css :import-error-list-enry)
|
|
||||||
:key (dm/str (or (:file-id entry) (:uri entry) (:name entry)))}
|
|
||||||
[:div (:name entry)]
|
|
||||||
(when-let [err (:error entry)]
|
|
||||||
[:div {:class (stl/css :import-error-detail)}
|
|
||||||
;; Temporary frontend-side error translations to provide more meaningful
|
|
||||||
;; messages until backend error handling is improved and standardized.
|
|
||||||
;; These mappings are only a short-term workaround and should be removed
|
|
||||||
;; once the error handling enhancement is implemented.
|
|
||||||
;; https://github.com/penpot/penpot/issues/9884
|
|
||||||
(cond
|
|
||||||
(and (string? err)
|
|
||||||
(str/includes? (str/lower err) "check error"))
|
|
||||||
(tr "dashboard.import.import-error.check-error")
|
|
||||||
|
|
||||||
(and (string? err)
|
|
||||||
(str/includes? (str/lower err) "corrupt"))
|
|
||||||
(tr "dashboard.import.import-error.corrupt-file")
|
|
||||||
|
|
||||||
:else
|
|
||||||
(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 (some? template)
|
|
||||||
[:> import-entry* {:entry (assoc template :status status)
|
|
||||||
:can-be-deleted false}])
|
|
||||||
|
|
||||||
(when (= :import-progress status)
|
|
||||||
[:div {:class (stl/css :status-message)
|
|
||||||
:role "status"
|
|
||||||
:aria-live "polite"}
|
|
||||||
(tr "labels.uploading-file")])]
|
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-footer)}
|
|
||||||
[:div {:class (stl/css :action-buttons)}
|
|
||||||
(when (= :analyze status)
|
|
||||||
[:input {:class (stl/css :cancel-button)
|
|
||||||
:type "button"
|
|
||||||
:value (tr "labels.cancel")
|
|
||||||
:on-click on-cancel}])
|
|
||||||
|
|
||||||
(when (= status :import-ready)
|
|
||||||
[:input {:class (stl/css :accept-btn)
|
|
||||||
:type "button"
|
|
||||||
:value (tr "labels.continue")
|
|
||||||
:disabled pending-analysis?
|
|
||||||
:on-click on-continue}])
|
|
||||||
|
|
||||||
(when (or (= :import-success status)
|
|
||||||
(= :import-error status)
|
|
||||||
(= :import-progress status))
|
|
||||||
[:input {:class (stl/css :accept-btn)
|
|
||||||
:type "button"
|
|
||||||
:value (tr "labels.accept")
|
|
||||||
:disabled (= :import-progress status)
|
|
||||||
:on-click on-accept}])]]]]))
|
|
||||||
618
frontend/src/app/main/ui/dashboard/import.scss
vendored
618
frontend/src/app/main/ui/dashboard/import.scss
vendored
@ -4,252 +4,210 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||||
|
|
||||||
@use "refactor/common-refactor.scss" as deprecated;
|
@use "ds/typography.scss" as *;
|
||||||
|
@use "ds/_borders.scss" as *;
|
||||||
|
@use "ds/spacing.scss" as *;
|
||||||
|
@use "ds/_sizes.scss" as *;
|
||||||
|
@use "ds/_utils.scss" as *;
|
||||||
|
@use "ds/z-index.scss" as *;
|
||||||
|
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
@extend %modal-overlay-base;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
position: fixed;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
inset-block-start: 0;
|
||||||
|
block-size: 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
z-index: var(--z-index-set);
|
||||||
|
background-color: var(--overlay-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
@extend %modal-container-base;
|
position: relative;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
gap: var(--sp-xxxl);
|
||||||
|
padding: var(--sp-xxxl);
|
||||||
.modal-header {
|
border-radius: $br-8;
|
||||||
margin-bottom: deprecated.$s-24;
|
background-color: var(--color-background-primary);
|
||||||
}
|
border: $b-2 solid var(--color-background-quaternary);
|
||||||
|
min-block-size: $sz-192;
|
||||||
.modal-title {
|
inline-size: $sz-512;
|
||||||
@include deprecated.uppercase-title-typography;
|
max-block-size: px2rem(800);
|
||||||
|
|
||||||
color: var(--modal-title-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close-btn {
|
|
||||||
@extend %modal-close-btn-base;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@include deprecated.body-small-typography;
|
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: deprecated.$s-16;
|
gap: var(--sp-l);
|
||||||
margin-bottom: deprecated.$s-24;
|
margin-block-end: var(--sp-xxl);
|
||||||
min-height: 40px;
|
min-block-size: px2rem(40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
inset-block-start: var(--sp-s);
|
||||||
|
inset-inline-end: px2rem(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-message {
|
.status-message {
|
||||||
@include deprecated.body-small-typography;
|
@include use-typography("body-small");
|
||||||
|
|
||||||
color: var(--modal-title-foreground-color);
|
color: var(--color-foreground-primary);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@extend %modal-action-btns;
|
display: flex;
|
||||||
}
|
justify-content: flex-end;
|
||||||
|
gap: var(--sp-l);
|
||||||
.cancel-button {
|
|
||||||
@extend %modal-cancel-btn;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accept-btn {
|
|
||||||
@extend %modal-accept-btn;
|
|
||||||
|
|
||||||
&.danger {
|
|
||||||
@extend %modal-danger-btn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-scd-msg,
|
|
||||||
.modal-subtitle,
|
|
||||||
.modal-msg {
|
|
||||||
@include deprecated.body-small-typography;
|
|
||||||
|
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-entry {
|
.file-entry {
|
||||||
|
--file-entry-fg-color: var(--color-foreground-secondary);
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.file-name {
|
gap: var(--sp-m);
|
||||||
@include deprecated.flex-row;
|
|
||||||
|
|
||||||
.file-icon {
|
|
||||||
@include deprecated.flex-center;
|
|
||||||
|
|
||||||
height: deprecated.$s-24;
|
|
||||||
width: deprecated.$s-16;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
@extend %button-icon;
|
|
||||||
|
|
||||||
stroke: var(--icon-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon-fill svg {
|
|
||||||
fill: var(--icon-foreground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name-edit {
|
|
||||||
@extend %input-element;
|
|
||||||
@include deprecated.body-small-typography;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name-label {
|
|
||||||
@include deprecated.body-small-typography;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: deprecated.$s-12;
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
@include deprecated.flex-center;
|
|
||||||
|
|
||||||
height: deprecated.$s-16;
|
|
||||||
width: deprecated.$s-16;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
@extend %button-icon-small;
|
|
||||||
|
|
||||||
stroke: var(--icon-foreground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.edit-entry-buttons {
|
|
||||||
@include deprecated.flex-row;
|
|
||||||
|
|
||||||
button {
|
|
||||||
@extend %button-tertiary;
|
|
||||||
|
|
||||||
width: deprecated.$s-28;
|
|
||||||
height: deprecated.$s-32;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
@extend %button-icon;
|
|
||||||
|
|
||||||
stroke: var(--icon-foreground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message,
|
|
||||||
.progress-message {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
min-height: deprecated.$s-32;
|
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-message {
|
|
||||||
align-items: flex-start;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.linked-library {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: deprecated.$s-12;
|
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
|
|
||||||
.linked-library-tag {
|
|
||||||
@include deprecated.flex-center;
|
|
||||||
|
|
||||||
height: deprecated.$s-24;
|
|
||||||
width: deprecated.$s-24;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
@extend %button-icon;
|
|
||||||
|
|
||||||
stroke: var(--icon-foreground);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
svg {
|
|
||||||
stroke: var(--element-foreground-error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.loading {
|
|
||||||
.file-name {
|
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.warning {
|
|
||||||
.file-name {
|
|
||||||
color: var(--element-foreground-warning);
|
|
||||||
|
|
||||||
.file-icon svg {
|
|
||||||
stroke: var(--element-foreground-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-icon.icon-fill svg {
|
|
||||||
fill: var(--element-foreground-warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
.file-name {
|
--file-entry-fg-color: var(--color-accent-sucess);
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
|
|
||||||
.file-icon svg {
|
|
||||||
stroke: var(--modal-text-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-icon.icon-fill svg {
|
|
||||||
fill: var(--modal-text-foreground-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
.file-name {
|
--file-entry-fg-color: var(--color-accent-error);
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
|
|
||||||
.file-icon svg {
|
|
||||||
stroke: var(--modal-text-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-icon.icon-fill svg {
|
|
||||||
fill: var(--modal-text-foreground-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.editable {
|
&.editable {
|
||||||
.file-name {
|
--file-entry-fg-color: var(--color-foreground-primary);
|
||||||
color: var(--modal-text-foreground-color);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.file-icon svg {
|
.error-message,
|
||||||
stroke: var(--modal-text-foreground-color);
|
.progress-message {
|
||||||
}
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-block-size: $sz-32;
|
||||||
|
color: var(--file-entry-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
.file-icon.icon-fill svg {
|
.error-message {
|
||||||
fill: var(--modal-text-foreground-color);
|
align-items: flex-start;
|
||||||
}
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-library {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-m);
|
||||||
|
color: var(--file-entry-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-library-tag {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
block-size: $sz-24;
|
||||||
|
inline-size: $sz-24;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
block-size: $sz-16;
|
||||||
|
inline-size: $sz-16;
|
||||||
|
color: transparent;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 1px;
|
||||||
|
stroke: var(--file-entry-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
svg {
|
||||||
|
stroke: var(--element-foreground-error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-l);
|
||||||
|
color: var(--file-entry-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-entry-buttons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-edit {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
block-size: $sz-32;
|
||||||
|
border: $b-1 solid var(--color-background-tertiary);
|
||||||
|
color: var(--file-entry-fg-color);
|
||||||
|
flex-grow: 1;
|
||||||
|
position: relative;
|
||||||
|
border-radius: $br-4;
|
||||||
|
background-color: var(--color-background-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-m);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-label-icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
block-size: $sz-16;
|
||||||
|
inline-size: $sz-16;
|
||||||
|
color: var(--file-entry-fg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-input {
|
||||||
|
@include use-typography("body-medium");
|
||||||
|
|
||||||
|
--edit-input-background-color: var(--color-background-tertiary);
|
||||||
|
--edit-input-border-color: transparent;
|
||||||
|
|
||||||
|
block-size: $sz-32;
|
||||||
|
inline-size: 100%;
|
||||||
|
padding: px2rem(6);
|
||||||
|
margin: 0;
|
||||||
|
border-radius: $br-8;
|
||||||
|
border: $b-1 solid var(--edit-input-border-color);
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
background-color: var(--edit-input-background-color);
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
--edit-input-background-color: var(--color-background-primary);
|
||||||
|
--edit-input-border-color: var(--color-accent-primary);
|
||||||
|
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.context-notification-error {
|
.context-notification-error {
|
||||||
--context-notification-bg-color: var(--modal-background-color);
|
--context-notification-bg-color: var(--color-background-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-notification {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-error-disclaimer {
|
.import-error-disclaimer {
|
||||||
@ -266,10 +224,252 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.import-error-detail {
|
.import-error-detail {
|
||||||
@include deprecated.body-small-typography;
|
// TODO: Typography does not match any existing DS typography token.
|
||||||
|
font-family: "worksans", "vazirmatn", sans-serif;
|
||||||
margin-top: var(--sp-xs);
|
font-size: px2rem(12);
|
||||||
color: var(--modal-text-foreground-color);
|
font-weight: 400;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-block-start: var(--sp-xs);
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ################################
|
||||||
|
// LIBRARY RESOLUTION
|
||||||
|
// ################################
|
||||||
|
|
||||||
|
.library-resolution {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-resolution-message {
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
margin-block-end: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-resolution-header {
|
||||||
|
@include use-typography("body-medium");
|
||||||
|
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 32px 1fr;
|
||||||
|
border-block-end: $b-1 solid var(--color-foreground-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-origin-name,
|
||||||
|
.library-resolution-connection {
|
||||||
|
block-size: $sz-32;
|
||||||
|
text-align: start;
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-resolution-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 32px 1fr;
|
||||||
|
block-size: $sz-32;
|
||||||
|
margin-block: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-resolution-icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
margin-inline-end: var(--sp-s);
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-resolution-arrow {
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-block-size: $sz-32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-resolution-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-resolution-item-name {
|
||||||
|
@include use-typography("body-medium");
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--sp-s);
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
padding-inline-start: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.library-resolution-no-selection {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
padding: var(--sp-s);
|
||||||
|
margin-inline-start: var(--sp-xxs);
|
||||||
|
block-size: $sz-32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ################################
|
||||||
|
// Summary file card
|
||||||
|
// ################################
|
||||||
|
|
||||||
|
.summary-file {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-s);
|
||||||
|
padding: var(--sp-m);
|
||||||
|
border-radius: $br-8;
|
||||||
|
background: var(--color-background-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-xxs);
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border-inline-start: $b-1 solid var(--color-background-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-file-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-s);
|
||||||
|
padding-block-end: var(--sp-s);
|
||||||
|
border-bottom: $b-1 solid var(--color-background-quaternary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-section-title {
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-linked-arrow {
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-file-icon {
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-file-name {
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section within a file (auto-linked or user selection)
|
||||||
|
.summary-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
padding-inline-start: var(--sp-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: px2rem(6);
|
||||||
|
padding: var(--sp-xs) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--sp-s);
|
||||||
|
padding: var(--sp-xs) var(--sp-s);
|
||||||
|
border-radius: $br-4;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-background-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item-name {
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
flex: 1;
|
||||||
|
min-inline-size: 0;
|
||||||
|
display: block;
|
||||||
|
max-inline-size: 99%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-item-name-header {
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
flex: 1;
|
||||||
|
min-inline-size: 0;
|
||||||
|
display: block;
|
||||||
|
max-inline-size: 99%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-linked badge
|
||||||
|
.summary-linked-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
padding: var(--sp-xxs) var(--sp-s);
|
||||||
|
border-radius: $br-12;
|
||||||
|
background: var(--color-accent-success-bg);
|
||||||
|
color: var(--color-accent-success);
|
||||||
|
font-size: px2rem(11);
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-badge-icon {
|
||||||
|
color: var(--color-accent-success);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User-selected library info
|
||||||
|
.summary-linked-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
flex-shrink: 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);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
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;
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|||||||
@ -13,9 +13,11 @@
|
|||||||
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]]
|
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown* schema:option]]
|
||||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||||
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
|
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
|
||||||
|
[app.main.ui.hooks :as hooks]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.keyboard :as kbd]
|
[app.util.keyboard :as kbd]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
|
[app.util.timers :as timers]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[rumext.v2 :as mf]
|
[rumext.v2 :as mf]
|
||||||
[rumext.v2.util :as mfu]))
|
[rumext.v2.util :as mfu]))
|
||||||
@ -58,11 +60,12 @@
|
|||||||
[:empty-to-end {:optional true} [:maybe :boolean]]
|
[:empty-to-end {:optional true} [:maybe :boolean]]
|
||||||
[:on-change {:optional true} fn?]
|
[:on-change {:optional true} fn?]
|
||||||
[:dropdown-alignment {:optional true} [:maybe [:enum :left :right]]]
|
[:dropdown-alignment {:optional true} [:maybe [:enum :left :right]]]
|
||||||
[:variant {:optional true} [:maybe [:enum "default" "ghost" "icon-only"]]]])
|
[:variant {:optional true} [:maybe [:enum "default" "ghost" "icon-only"]]]
|
||||||
|
[:has-portal {:optional true} :boolean]])
|
||||||
|
|
||||||
(mf/defc select*
|
(mf/defc select*
|
||||||
{::mf/schema schema:select}
|
{::mf/schema schema:select}
|
||||||
[{:keys [options class disabled default-selected empty-to-end on-change variant wrapper-class dropdown-alignment] :rest props}]
|
[{:keys [options class disabled default-selected empty-to-end on-change variant wrapper-class dropdown-alignment has-portal] :rest props}]
|
||||||
(let [;; NOTE: we use mfu/bean here for transparently handle
|
(let [;; NOTE: we use mfu/bean here for transparently handle
|
||||||
;; options provide as clojure data structures or javascript
|
;; options provide as clojure data structures or javascript
|
||||||
;; plain objects and lists.
|
;; plain objects and lists.
|
||||||
@ -88,6 +91,9 @@
|
|||||||
options-ref (mf/use-ref nil)
|
options-ref (mf/use-ref nil)
|
||||||
select-ref (mf/use-ref nil)
|
select-ref (mf/use-ref nil)
|
||||||
|
|
||||||
|
container (hooks/use-portal-container :popup)
|
||||||
|
dropdown-wrapper-ref (mf/use-ref nil)
|
||||||
|
|
||||||
empty-selected-id?
|
empty-selected-id?
|
||||||
(str/blank? selected-id)
|
(str/blank? selected-id)
|
||||||
|
|
||||||
@ -208,10 +214,63 @@
|
|||||||
(reset! selected-id*
|
(reset! selected-id*
|
||||||
(get-selected-option-id options default-selected)))
|
(get-selected-option-id options default-selected)))
|
||||||
|
|
||||||
|
;; Portal mode: click-outside + floating positioning
|
||||||
|
(mf/with-effect [is-open has-portal]
|
||||||
|
(when (and is-open has-portal)
|
||||||
|
(let [handler
|
||||||
|
(fn [event]
|
||||||
|
(let [wrapper-node (mf/ref-val select-ref)
|
||||||
|
dropdown-node (mf/ref-val dropdown-wrapper-ref)
|
||||||
|
target (dom/get-target event)]
|
||||||
|
(when (and wrapper-node dropdown-node
|
||||||
|
(not (dom/child? target wrapper-node))
|
||||||
|
(not (dom/child? target dropdown-node)))
|
||||||
|
(reset! is-open* false)
|
||||||
|
(reset! focused-id* nil))))
|
||||||
|
|
||||||
|
calculate
|
||||||
|
(fn []
|
||||||
|
(timers/raf
|
||||||
|
(fn []
|
||||||
|
(when-let [select-node (mf/ref-val select-ref)]
|
||||||
|
(when-let [dropdown-node (mf/ref-val dropdown-wrapper-ref)]
|
||||||
|
(let [select-rect (dom/get-bounding-rect select-node)
|
||||||
|
dropdown-rect (dom/get-bounding-rect dropdown-node)
|
||||||
|
window-height (.-innerHeight js/window)
|
||||||
|
space-below (- window-height (:bottom select-rect))
|
||||||
|
open-up? (> (:height dropdown-rect) space-below)]
|
||||||
|
(if open-up?
|
||||||
|
(let [bottom (+ (- window-height (:top select-rect)) 4)]
|
||||||
|
(dom/set-css-property! dropdown-node "top" "unset")
|
||||||
|
(dom/set-css-property! dropdown-node "bottom" (str bottom "px")))
|
||||||
|
(let [top (+ (:bottom select-rect) 4)]
|
||||||
|
(dom/set-css-property! dropdown-node "bottom" "unset")
|
||||||
|
(dom/set-css-property! dropdown-node "top" (str top "px"))))
|
||||||
|
(dom/set-css-property! dropdown-node "left" (str (:left select-rect) "px"))
|
||||||
|
(dom/set-css-property! dropdown-node "width" (str (:width select-rect) "px"))
|
||||||
|
(dom/set-css-property! dropdown-node "position" "fixed")))))))]
|
||||||
|
|
||||||
|
(.addEventListener js/document "mousedown" handler)
|
||||||
|
|
||||||
|
(let [ro (js/ResizeObserver. (fn [_] (calculate)))]
|
||||||
|
(when-let [node (mf/ref-val select-ref)]
|
||||||
|
(.observe ro node))
|
||||||
|
|
||||||
|
(.addEventListener js/window "resize" calculate)
|
||||||
|
(.addEventListener js/window "scroll" calculate true)
|
||||||
|
|
||||||
|
(calculate)
|
||||||
|
|
||||||
|
(fn []
|
||||||
|
(.removeEventListener js/document "mousedown" handler)
|
||||||
|
(.disconnect ro)
|
||||||
|
(.removeEventListener js/window "resize" calculate)
|
||||||
|
(.removeEventListener js/window "scroll" calculate true))))))
|
||||||
|
|
||||||
[:div {:class [wrapper-class (stl/css :select-wrapper)]
|
[:div {:class [wrapper-class (stl/css :select-wrapper)]
|
||||||
:on-click on-click
|
:on-click on-click
|
||||||
:ref select-ref
|
:ref select-ref
|
||||||
:on-blur on-blur}
|
:on-blur (when-not has-portal on-blur)}
|
||||||
|
|
||||||
[:> :button props
|
[:> :button props
|
||||||
[:span {:class (stl/css-case :select-header true
|
[:span {:class (stl/css-case :select-header true
|
||||||
@ -241,11 +300,24 @@
|
|||||||
:aria-hidden true}]]
|
:aria-hidden true}]]
|
||||||
|
|
||||||
(when ^boolean is-open
|
(when ^boolean is-open
|
||||||
[:> options-dropdown* {:on-click on-option-click
|
(if has-portal
|
||||||
:id listbox-id
|
(mf/portal
|
||||||
:options options
|
(mf/html
|
||||||
:selected selected-id
|
[:> options-dropdown* {:on-click on-option-click
|
||||||
:focused focused-id
|
:id listbox-id
|
||||||
:align dropdown-alignment
|
:options options
|
||||||
:empty-to-end empty-to-end
|
:selected selected-id
|
||||||
:ref set-option-ref}])]))
|
:focused focused-id
|
||||||
|
:align dropdown-alignment
|
||||||
|
:empty-to-end empty-to-end
|
||||||
|
:ref set-option-ref
|
||||||
|
:wrapper-ref dropdown-wrapper-ref}])
|
||||||
|
container)
|
||||||
|
[:> options-dropdown* {:on-click on-option-click
|
||||||
|
:id listbox-id
|
||||||
|
:options options
|
||||||
|
:selected selected-id
|
||||||
|
:focused focused-id
|
||||||
|
:align dropdown-alignment
|
||||||
|
:empty-to-end empty-to-end
|
||||||
|
:ref set-option-ref}]))]))
|
||||||
|
|||||||
@ -13,8 +13,14 @@
|
|||||||
[app.main.data.exports.files :as fexp]
|
[app.main.data.exports.files :as fexp]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
|
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||||
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
|
[app.main.ui.ds.foundations.typography :as t]
|
||||||
|
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||||
|
[app.main.ui.ds.foundations.typography.text :refer [text*]]
|
||||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||||
[app.main.ui.icons :as deprecated-icon]
|
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
@ -41,28 +47,33 @@
|
|||||||
[files]
|
[files]
|
||||||
(let [files (mapv (fn [file] (assoc file :loading true)) files)]
|
(let [files (mapv (fn [file] (assoc file :loading true)) files)]
|
||||||
{:status :prepare
|
{:status :prepare
|
||||||
:selected :all
|
:selected :include-libraries
|
||||||
:files files}))
|
:files files}))
|
||||||
|
|
||||||
(mf/defc export-entry*
|
(mf/defc export-entry*
|
||||||
{::mf/private true}
|
{::mf/private true}
|
||||||
[{:keys [file]}]
|
[{:keys [file]}]
|
||||||
[:div {:class (stl/css-case
|
(let [level (cond
|
||||||
:file-entry true
|
(:export-success? file) :success
|
||||||
:loading (:loading file)
|
(:export-error? file) :error
|
||||||
:success (:export-success? file)
|
:else :info)]
|
||||||
:error (:export-error? file))}
|
[:div {:class (stl/css-case
|
||||||
|
:file-entry true
|
||||||
|
:loading (:loading file)
|
||||||
|
:success (:export-success? file)
|
||||||
|
:error (:export-error? file))}
|
||||||
|
|
||||||
[:div {:class (stl/css :file-name)}
|
(if (:loading file)
|
||||||
(if (:loading file)
|
[:div {:class (stl/css :file-name)}
|
||||||
[:> loader* {:width 16
|
[:> loader* {:width 26
|
||||||
:title (tr "labels.loading")}]
|
:title (tr "labels.loading")}]
|
||||||
[:span {:class (stl/css :file-icon)}
|
[:> text* {:class (stl/css :file-name-label)
|
||||||
(cond (:export-success? file) deprecated-icon/tick
|
:as "span"
|
||||||
(:export-error? file) deprecated-icon/close)])
|
:typography t/body-large}
|
||||||
|
(:name file)]]
|
||||||
|
|
||||||
[:div {:class (stl/css :file-name-label)}
|
[:> context-notification {:level level
|
||||||
(:name file)]]])
|
:content (:name file)}])]))
|
||||||
|
|
||||||
(mf/defc export-dialog
|
(mf/defc export-dialog
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
@ -109,6 +120,7 @@
|
|||||||
(let [type (-> (dom/get-target event)
|
(let [type (-> (dom/get-target event)
|
||||||
(dom/get-data "type")
|
(dom/get-data "type")
|
||||||
(keyword))]
|
(keyword))]
|
||||||
|
(prn "AAA" selected type)
|
||||||
(swap! state* assoc :selected type))))]
|
(swap! state* assoc :selected type))))]
|
||||||
|
|
||||||
(mf/with-effect [has-libs?]
|
(mf/with-effect [has-libs?]
|
||||||
@ -119,38 +131,59 @@
|
|||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css :modal-overlay)}
|
||||||
[:div {:class (stl/css :modal-container)}
|
[:div {:class (stl/css :modal-container)}
|
||||||
[:div {:class (stl/css :modal-header)}
|
[:div {:class (stl/css :modal-header)}
|
||||||
[:h2 {:class (stl/css :modal-title)}
|
[:> heading* {:level 2
|
||||||
|
:typography t/headline-large
|
||||||
|
:class (stl/css :modal-title)}
|
||||||
(tr "files-download-modal.title")]
|
(tr "files-download-modal.title")]
|
||||||
[:button {:class (stl/css :modal-close-btn)
|
[:> icon-button* {:variant "ghost"
|
||||||
:on-click on-cancel} deprecated-icon/close]]
|
:aria-label (tr "labels.close")
|
||||||
|
:on-click on-cancel
|
||||||
|
:class (stl/css :modal-close-btn)
|
||||||
|
:icon i/close}]]
|
||||||
(cond
|
(cond
|
||||||
(= status :prepare)
|
(= status :prepare)
|
||||||
[:*
|
[:*
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
[:p {:class (stl/css :modal-msg)} (tr "files-download-modal.description-1")]
|
;; TODO: Add translation
|
||||||
[:p {:class (stl/css :modal-scd-msg)} (tr "files-download-modal.description-2")]
|
[:> text* {:as "p" :typography t/body-large :class (stl/css :modal-msg)}
|
||||||
|
"What do you want to do with linked libraries?"]
|
||||||
|
|
||||||
(for [type fexp/valid-types]
|
(for [type fexp/valid-types]
|
||||||
[:div {:class (stl/css :export-option true)
|
[:div {:class (stl/css :export-option true)
|
||||||
:key (name type)}
|
:key (name type)}
|
||||||
[:label {:for (str "export-" type)
|
[:label {:for (str "export-" type)
|
||||||
:class (stl/css-case :global/checked (= selected type))}
|
:class (stl/css :export-option-label)}
|
||||||
;; Execution time translation strings:
|
;; Execution time translation strings:
|
||||||
;; (tr "files-download-modal.options.all.message")
|
;; (tr "files-export-modal.options.include-libraries.title")
|
||||||
;; (tr "files-download-modal.options.all.title")
|
;; (tr "files-export-modal.options.include-libraries.message")
|
||||||
;; (tr "files-download-modal.options.detach.message")
|
|
||||||
;; (tr "files-download-modal.options.detach.title")
|
;; (tr "files-export-modal.options.merge-libraries.title")
|
||||||
;; (tr "files-download-modal.options.merge.message")
|
;; (tr "files-export-modal.options.merge-libraries.message")
|
||||||
;; (tr "files-download-modal.options.merge.title")
|
|
||||||
[:span {:class (stl/css-case :global/checked (= selected type))}
|
;; (tr "files-export-modal.options.detach-libraries.title")
|
||||||
|
;; (tr "files-export-modal.options.detach-libraries.message")
|
||||||
|
|
||||||
|
;; (tr "files-export-modal.options.link-later.title")
|
||||||
|
;; (tr "files-export-modal.options.link-later.message")
|
||||||
|
|
||||||
|
[:span {:class (stl/css-case
|
||||||
|
:option-icon-wrapper true
|
||||||
|
:checked (= selected type))}
|
||||||
(when (= selected type)
|
(when (= selected type)
|
||||||
deprecated-icon/status-tick)]
|
[:svg {:class (stl/css :option-icon)
|
||||||
|
:viewBox "0 0 8 8"
|
||||||
|
:width 8
|
||||||
|
:height 8
|
||||||
|
:aria-hidden true}
|
||||||
|
[:circle {:cx 4 :cy 4 :r 4}]])]
|
||||||
|
|
||||||
[:div {:class (stl/css :option-content)}
|
[:div {:class (stl/css :option-content)}
|
||||||
[:h3 {:class (stl/css :modal-subtitle)}
|
[:> heading* {:level 3
|
||||||
(tr (dm/str "files-download-modal.options." (d/name type) ".title"))]
|
:typography t/body-large
|
||||||
[:p {:class (stl/css :modal-msg)}
|
:class (stl/css :option-title)}
|
||||||
(tr (dm/str "files-download-modal.options." (d/name type) ".message"))]]
|
(tr (dm/str "files-export-modal.options." (d/name type) ".title"))]
|
||||||
|
[:> text* {:as "p" :typography t/body-large :class (stl/css :modal-msg)}
|
||||||
|
(tr (dm/str "files-export-modal.options." (d/name type) ".message"))]]
|
||||||
|
|
||||||
[:input {:type "radio"
|
[:input {:type "radio"
|
||||||
:class (stl/css :option-input)
|
:class (stl/css :option-input)
|
||||||
@ -162,15 +195,15 @@
|
|||||||
|
|
||||||
[:div {:class (stl/css :modal-footer)}
|
[:div {:class (stl/css :modal-footer)}
|
||||||
[:div {:class (stl/css :action-buttons)}
|
[:div {:class (stl/css :action-buttons)}
|
||||||
[:input {:class (stl/css :cancel-button)
|
[:> button* {:variant "secondary"
|
||||||
:type "button"
|
:type "button"
|
||||||
:value (tr "labels.cancel")
|
:on-click on-cancel}
|
||||||
:on-click on-cancel}]
|
(tr "labels.cancel")]
|
||||||
|
|
||||||
[:input {:class (stl/css :accept-btn)
|
[:> button* {:variant "primary"
|
||||||
:type "button"
|
:type "button"
|
||||||
:value (tr "labels.continue")
|
:on-click on-accept}
|
||||||
:on-click on-accept}]]]]
|
(tr "labels.continue")]]]]
|
||||||
|
|
||||||
(= status :exporting)
|
(= status :exporting)
|
||||||
(let [in-progress? (->> state :files (some :loading))]
|
(let [in-progress? (->> state :files (some :loading))]
|
||||||
@ -180,15 +213,15 @@
|
|||||||
[:> export-entry* {:file file :key (dm/str (:id file))}])
|
[:> export-entry* {:file file :key (dm/str (:id file))}])
|
||||||
|
|
||||||
(when in-progress?
|
(when in-progress?
|
||||||
[:div {:class (stl/css :status-message)
|
[:> text* {:as "span" :typography t/body-large :class (stl/css :status-message)
|
||||||
:role "status"
|
:role "status"
|
||||||
:aria-live "polite"}
|
:aria-live "polite"}
|
||||||
(tr "labels.downloading-file")])]
|
(tr "labels.downloading-file")])]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-footer)}
|
[:div {:class (stl/css :modal-footer)}
|
||||||
[:div {:class (stl/css :action-buttons)}
|
[:div {:class (stl/css :action-buttons)}
|
||||||
[:input {:class (stl/css :accept-btn)
|
[:> button* {:variant "primary"
|
||||||
:type "button"
|
:type "button"
|
||||||
:value (tr "labels.close")
|
:disabled in-progress?
|
||||||
:disabled in-progress?
|
:on-click on-cancel}
|
||||||
:on-click on-cancel}]]]]))]]))
|
(tr "labels.close")]]]]))]]))
|
||||||
|
|||||||
@ -4,289 +4,205 @@
|
|||||||
//
|
//
|
||||||
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
// Copyright (c) KALEIDOS SUBSIDIARY SL
|
||||||
|
|
||||||
@use "refactor/common-refactor.scss" as deprecated;
|
@use "ds/typography.scss" as *;
|
||||||
|
@use "ds/_borders.scss" as *;
|
||||||
|
@use "ds/spacing.scss" as *;
|
||||||
|
@use "ds/_sizes.scss" as *;
|
||||||
|
@use "ds/_utils.scss" as *;
|
||||||
|
@use "ds/z-index.scss" as *;
|
||||||
|
|
||||||
// EXPORT MODAL
|
// EXPORT MODAL
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
@extend %modal-overlay-base;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
&.transparent {
|
align-items: center;
|
||||||
background-color: transparent;
|
position: fixed;
|
||||||
}
|
inset-inline-start: 0;
|
||||||
|
inset-block-start: 0;
|
||||||
|
block-size: 100%;
|
||||||
|
inline-size: 100%;
|
||||||
|
z-index: var(--z-index-set);
|
||||||
|
background-color: var(--overlay-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container {
|
.modal-container {
|
||||||
@extend %modal-container-base;
|
position: relative;
|
||||||
|
display: flex;
|
||||||
max-height: calc(10 * deprecated.$s-80);
|
flex-direction: column;
|
||||||
}
|
gap: var(--sp-xxxl);
|
||||||
|
padding: var(--sp-xxxl);
|
||||||
.modal-header {
|
border-radius: $br-8;
|
||||||
margin-bottom: deprecated.$s-24;
|
background-color: var(--color-background-primary);
|
||||||
}
|
border: $b-2 solid var(--color-background-quaternary);
|
||||||
|
min-block-size: $sz-192;
|
||||||
.modal-title {
|
inline-size: $sz-512;
|
||||||
@include deprecated.headline-medium-typography;
|
max-block-size: calc(10 * px2rem(80));
|
||||||
|
|
||||||
color: var(--modal-title-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-close-btn {
|
|
||||||
@extend %modal-close-btn-base;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
@include deprecated.body-small-typography;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--sp-l);
|
||||||
|
margin-block-end: var(--sp-xxl);
|
||||||
|
}
|
||||||
|
|
||||||
margin-bottom: deprecated.$s-24;
|
.modal-content-extended {
|
||||||
|
gap: var(--sp-xxl);
|
||||||
|
}
|
||||||
|
|
||||||
.modal-link {
|
.modal-title {
|
||||||
@include deprecated.body-large-typography;
|
color: var(--color-foreground-primary);
|
||||||
|
}
|
||||||
|
|
||||||
text-decoration: none;
|
.modal-close-btn {
|
||||||
cursor: pointer;
|
position: absolute;
|
||||||
color: var(--modal-link-foreground-color);
|
inset-block-start: var(--sp-m);
|
||||||
}
|
inset-inline-end: var(--sp-m);
|
||||||
|
}
|
||||||
|
|
||||||
.selection-header {
|
.modal-msg {
|
||||||
@include deprecated.flex-row;
|
color: var(--color-foreground-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
height: deprecated.$s-32;
|
.option-content {
|
||||||
margin-bottom: deprecated.$s-4;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
.selection-btn {
|
|
||||||
@include deprecated.button-style;
|
|
||||||
@extend %input-checkbox;
|
|
||||||
@include deprecated.flex-center;
|
|
||||||
|
|
||||||
height: deprecated.$s-24;
|
|
||||||
width: deprecated.$s-24;
|
|
||||||
padding: 0;
|
|
||||||
margin-left: deprecated.$s-16;
|
|
||||||
|
|
||||||
span {
|
|
||||||
@extend %checkbox-icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-title {
|
|
||||||
@include deprecated.body-large-typography;
|
|
||||||
|
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-wrapper {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-shadow {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 50px;
|
|
||||||
background: linear-gradient(to top, rgb(24 24 26 / 1) 0%, rgb(24 24 26 / 0) 100%);
|
|
||||||
content: "";
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-list {
|
|
||||||
@include deprecated.flex-column;
|
|
||||||
|
|
||||||
max-height: deprecated.$s-400;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-bottom: deprecated.$s-12;
|
|
||||||
|
|
||||||
.selection-row {
|
|
||||||
@include deprecated.flex-row;
|
|
||||||
|
|
||||||
background-color: var(--entry-background-color);
|
|
||||||
min-height: deprecated.$s-40;
|
|
||||||
border-radius: deprecated.$br-8;
|
|
||||||
|
|
||||||
.selection-btn {
|
|
||||||
@include deprecated.button-style;
|
|
||||||
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: min-content auto 1fr auto auto;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 10%;
|
|
||||||
gap: deprecated.$s-8;
|
|
||||||
padding: 0 deprecated.$s-16;
|
|
||||||
|
|
||||||
.checkbox-wrapper {
|
|
||||||
@extend %input-checkbox;
|
|
||||||
@include deprecated.flex-center;
|
|
||||||
|
|
||||||
height: deprecated.$s-24;
|
|
||||||
width: deprecated.$s-24;
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
.checkobox-tick {
|
|
||||||
@extend %checkbox-icon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-name {
|
|
||||||
@include deprecated.body-large-typography;
|
|
||||||
@include deprecated.text-ellipsis;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-scale {
|
|
||||||
@include deprecated.body-large-typography;
|
|
||||||
@include deprecated.text-ellipsis;
|
|
||||||
|
|
||||||
min-width: deprecated.$s-108;
|
|
||||||
padding: deprecated.$s-12;
|
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.selection-extension {
|
|
||||||
@include deprecated.body-large-typography;
|
|
||||||
@include deprecated.text-ellipsis;
|
|
||||||
|
|
||||||
min-width: deprecated.$s-72;
|
|
||||||
padding: deprecated.$s-12;
|
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-wrapper {
|
|
||||||
@include deprecated.flex-center;
|
|
||||||
|
|
||||||
min-height: deprecated.$s-32;
|
|
||||||
min-width: deprecated.$s-32;
|
|
||||||
background-color: var(--app-white);
|
|
||||||
border-radius: deprecated.$br-6;
|
|
||||||
margin: auto 0;
|
|
||||||
|
|
||||||
img,
|
|
||||||
svg {
|
|
||||||
object-fit: contain;
|
|
||||||
max-height: deprecated.$s-40;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-message {
|
.status-message {
|
||||||
@include deprecated.body-small-typography;
|
color: var(--color-foreground-primary);
|
||||||
|
|
||||||
color: var(--modal-title-foreground-color);
|
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@extend %modal-action-btns;
|
display: flex;
|
||||||
}
|
justify-content: flex-end;
|
||||||
|
gap: var(--sp-s);
|
||||||
.cancel-button {
|
|
||||||
@extend %modal-cancel-btn;
|
|
||||||
}
|
|
||||||
|
|
||||||
.accept-btn {
|
|
||||||
@extend %modal-accept-btn;
|
|
||||||
|
|
||||||
&.danger {
|
|
||||||
@extend %modal-danger-btn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-scd-msg,
|
|
||||||
.modal-subtitle,
|
|
||||||
.modal-msg {
|
|
||||||
@include deprecated.body-large-typography;
|
|
||||||
|
|
||||||
color: var(--modal-text-foreground-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.export-option {
|
.export-option {
|
||||||
@extend %input-checkbox;
|
display: flex;
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
inline-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
label {
|
.export-option-label {
|
||||||
align-items: flex-start;
|
--input-border-color: var(--input-checkbox-border-color-rest);
|
||||||
|
--input-icon-color: var(--color-background-primary);
|
||||||
|
|
||||||
.modal-subtitle {
|
display: flex;
|
||||||
@include deprecated.body-large-typography;
|
align-items: flex-start;
|
||||||
|
gap: px2rem(6);
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-foreground-primary);
|
||||||
|
|
||||||
color: var(--modal-title-foreground-color);
|
&:hover {
|
||||||
padding: 0.25rem 0;
|
--input-border-color: var(--color-accent-primary-muted);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
&:focus,
|
||||||
margin-top: deprecated.$s-8;
|
&:focus-within {
|
||||||
|
--input-border-color: var(--color-accent-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-content {
|
.option-icon-wrapper {
|
||||||
@include deprecated.flex-column;
|
--icon-display: none;
|
||||||
@include deprecated.body-large-typography;
|
--background-color: var(--color-background-quaternary);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
inline-size: px2rem(16);
|
||||||
|
min-inline-size: px2rem(16);
|
||||||
|
block-size: px2rem(16);
|
||||||
|
margin-block-start: px2rem(10);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: px2rem(1) solid var(--input-border-color);
|
||||||
|
border-radius: $br-circle;
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
--icon-display: block;
|
||||||
|
--input-border-color: var(--color-background-quaternary);
|
||||||
|
--input-icon-color: var(--color-background-primary);
|
||||||
|
--background-color: var(--color-accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--input-border-color: var(--color-accent-primary-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
--input-border-color: var(--color-accent-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-icon {
|
||||||
|
inline-size: px2rem(8);
|
||||||
|
block-size: px2rem(8);
|
||||||
|
display: var(--icon-display);
|
||||||
|
fill: var(--input-icon-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-input {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-entry {
|
.file-entry {
|
||||||
.file-name {
|
--file-entry-color: var(--color-foreground-secondary);
|
||||||
@include deprecated.flex-row;
|
|
||||||
|
|
||||||
.file-icon {
|
|
||||||
@include deprecated.flex-center;
|
|
||||||
|
|
||||||
height: deprecated.$s-16;
|
|
||||||
width: deprecated.$s-16;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
@extend %button-icon-small;
|
|
||||||
|
|
||||||
stroke: var(--input-foreground);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-name-label {
|
|
||||||
@include deprecated.body-large-typography;
|
|
||||||
@include deprecated.text-ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.loading {
|
&.loading {
|
||||||
.file-name {
|
.file-name {
|
||||||
color: var(--modal-text-foreground-color);
|
color: var(--color-foreground-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
.file-name {
|
.file-name {
|
||||||
color: var(--modal-text-foreground-color);
|
color: var(--color-foreground-secondary);
|
||||||
|
|
||||||
.file-icon svg {
|
.file-icon svg {
|
||||||
stroke: var(--modal-text-foreground-color);
|
stroke: var(--color-foreground-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
.file-name {
|
.file-name {
|
||||||
color: var(--modal-text-foreground-color);
|
color: var(--color-foreground-secondary);
|
||||||
|
|
||||||
.file-icon svg {
|
.file-icon svg {
|
||||||
stroke: var(--modal-text-foreground-color);
|
stroke: var(--color-foreground-secondary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-m);
|
||||||
|
|
||||||
|
.file-icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
block-size: px2rem(16);
|
||||||
|
inline-size: px2rem(16);
|
||||||
|
color: var(--color-foreground-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name-label {
|
||||||
|
display: block;
|
||||||
|
max-inline-size: 99%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-status-tick {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--color-accent-primary);
|
||||||
|
}
|
||||||
|
|||||||
@ -258,7 +258,13 @@
|
|||||||
(fn [format type]
|
(fn [format type]
|
||||||
(js/Promise.
|
(js/Promise.
|
||||||
(fn [resolve reject]
|
(fn [resolve reject]
|
||||||
(let [type (or (parser/parse-keyword type) :all)]
|
(let [type (or (parser/parse-keyword type) :all)
|
||||||
|
;; Backward compatibility: convert old values to new
|
||||||
|
type (case type
|
||||||
|
:all :include-libraries
|
||||||
|
:merge :merge-libraries
|
||||||
|
:detach :detach-libraries
|
||||||
|
type)]
|
||||||
(cond
|
(cond
|
||||||
(and (some? format) (not (contains? #{"penpot" "zip"} format)))
|
(and (some? format) (not (contains? #{"penpot" "zip"} format)))
|
||||||
(u/reject-not-valid reject :format (dm/str "Invalid format: " format))
|
(u/reject-not-valid reject :format (dm/str "Invalid format: " format))
|
||||||
|
|||||||
@ -171,8 +171,9 @@
|
|||||||
|
|
||||||
(defmethod impl/handler :import-files
|
(defmethod impl/handler :import-files
|
||||||
[{:keys [project-id files]}]
|
[{:keys [project-id files]}]
|
||||||
(let [binfile-v1 (filter #(= :binfile-v1 (:type %)) files)
|
(let [binfile-v1 (filter #(= :binfile-v1 (:type %)) files)
|
||||||
binfile-v3 (filter #(= :binfile-v3 (:type %)) files)]
|
binfile-v3 (filter #(= :binfile-v3 (:type %)) files)
|
||||||
|
resolutions (volatile! {})]
|
||||||
|
|
||||||
(rx/merge
|
(rx/merge
|
||||||
(->> (rx/from binfile-v1)
|
(->> (rx/from binfile-v1)
|
||||||
@ -203,40 +204,50 @@
|
|||||||
:error (import-cause-message cause (tr "labels.error"))
|
:error (import-cause-message cause (tr "labels.error"))
|
||||||
:file-id (:file-id data)})))))))
|
:file-id (:file-id data)})))))))
|
||||||
|
|
||||||
(->> (rx/from binfile-v3)
|
|
||||||
(rx/reduce (fn [result file]
|
(rx/concat
|
||||||
(update result (:uri file) (fnil conj []) file))
|
(->> (rx/from binfile-v3)
|
||||||
{})
|
(rx/reduce (fn [result file]
|
||||||
(rx/mapcat identity)
|
(update result (:uri file) (fnil conj []) file))
|
||||||
(rx/merge-map
|
{})
|
||||||
(fn [[uri entries]]
|
(rx/mapcat identity)
|
||||||
(->> (import-blob-via-upload uri
|
(rx/merge-map
|
||||||
{:name (-> entries first :name)
|
(fn [[uri entries]]
|
||||||
:version 3
|
(->> (import-blob-via-upload uri
|
||||||
:project-id project-id})
|
{:name (-> entries first :name)
|
||||||
(rx/tap (fn [event]
|
:version 3
|
||||||
(let [payload (sse/get-payload event)
|
:project-id project-id})
|
||||||
type (sse/get-type event)]
|
(rx/tap (fn [event]
|
||||||
(if (= type "progress")
|
(let [payload (sse/get-payload event)
|
||||||
(log/dbg :hint "import-binfile: progress"
|
type (sse/get-type event)]
|
||||||
:section (:section payload)
|
(cond
|
||||||
:name (:name payload))
|
(= type "progress")
|
||||||
(log/dbg :hint "import-binfile: end")))))
|
(log/dbg :hint "import-binfile: progress"
|
||||||
(rx/filter sse/end-of-stream?)
|
:section (:section payload)
|
||||||
(rx/mapcat (fn [_]
|
:name (:name payload))
|
||||||
(->> (rx/from entries)
|
|
||||||
(rx/map (fn [entry]
|
:else
|
||||||
{:status :finish
|
(log/dbg :hint "import-binfile: end")))))
|
||||||
:file-id (:file-id entry)})))))
|
(rx/filter sse/end-of-stream?)
|
||||||
(rx/catch
|
(rx/mapcat (fn [message]
|
||||||
(fn [cause]
|
(let [{:keys [resolution]} (sse/get-payload message)]
|
||||||
(log/error :hint "unexpected error on import process"
|
(when (seq resolution)
|
||||||
:project-id project-id
|
(vswap! resolutions merge resolution))
|
||||||
::log/sync? true
|
(->> (rx/from entries)
|
||||||
:cause cause)
|
(rx/map (fn [entry]
|
||||||
(let [err (import-cause-message cause (tr "labels.error"))]
|
{:status :finish
|
||||||
(->> (rx/from entries)
|
:file-id (:file-id entry)}))))))
|
||||||
(rx/map (fn [entry]
|
(rx/catch (fn [cause]
|
||||||
{:status :error
|
(log/error :hint "import-binfile: unexpected error on importing"
|
||||||
:error err
|
:project-id project-id
|
||||||
:file-id (:file-id entry)})))))))))))))
|
::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)}))))))))))
|
||||||
|
(->> (rx/defer #(rx/of @resolutions))
|
||||||
|
(rx/map (fn [resolutions]
|
||||||
|
{:libraries-resolution resolutions})))))))
|
||||||
|
|||||||
@ -761,6 +761,35 @@ 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."
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
msgid "dashboard.import.summary.auto-linked"
|
||||||
|
msgstr "Auto-linked"
|
||||||
|
|
||||||
|
msgid "dashboard.import.summary.your-selection"
|
||||||
|
msgstr "Your selection"
|
||||||
|
|
||||||
|
msgid "dashboard.import.summary.linked"
|
||||||
|
msgstr "Linked"
|
||||||
|
|
||||||
|
msgid "dashboard.import.summary.no-selection"
|
||||||
|
msgstr "No library selected"
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard.cljs:260
|
#: src/app/main/ui/dashboard.cljs:260
|
||||||
msgid "dashboard.import.no-perms"
|
msgid "dashboard.import.no-perms"
|
||||||
msgstr "You don’t have permission to import to this team"
|
msgstr "You don’t have permission to import to this team"
|
||||||
@ -2122,34 +2151,70 @@ msgid "files-download-modal.description-2"
|
|||||||
msgstr "* Might include components, graphics, colors and/or typographies."
|
msgstr "* Might include components, graphics, colors and/or typographies."
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:140
|
#: src/app/main/ui/exports/files.cljs:140
|
||||||
msgid "files-download-modal.options.all.message"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
|
msgid "files-export-modal.options.all.message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Files with shared libraries will be included in the export, maintaining "
|
|
||||||
"their linkage."
|
#: src/app/main/ui/exports/files.cljs:140
|
||||||
|
msgid "files-export-modal.options.include-libraries.message"
|
||||||
|
msgstr ""
|
||||||
|
"Files with linked libraries will be included in the export, maintaining "
|
||||||
|
"their linkage. "
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:141
|
#: src/app/main/ui/exports/files.cljs:141
|
||||||
msgid "files-download-modal.options.all.title"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
msgstr "Export shared libraries"
|
msgid "files-export-modal.options.all.title"
|
||||||
|
msgstr "Export file + libraries"
|
||||||
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs:141
|
||||||
|
msgid "files-export-modal.options.include-libraries.title"
|
||||||
|
msgstr "Export file + libraries"
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:142
|
#: src/app/main/ui/exports/files.cljs:142
|
||||||
msgid "files-download-modal.options.detach.message"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
msgstr ""
|
msgid "files-export-modal.options.detach.message"
|
||||||
"Shared libraries will not be included in the export and no assets will be "
|
msgstr "Linked library assets won't be included in the file."
|
||||||
"added to the library. "
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs:142
|
||||||
|
msgid "files-export-modal.options.detach-libraries.message"
|
||||||
|
msgstr "Linked library assets won't be included in the file."
|
||||||
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs
|
||||||
|
msgid "files-export-modal.options.link-later.title"
|
||||||
|
msgstr "Link matching libraries on import"
|
||||||
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs
|
||||||
|
msgid "files-export-modal.options.link-later.message"
|
||||||
|
msgstr "When imported, you'll be able to link existing libraries with matching names."
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:143
|
#: src/app/main/ui/exports/files.cljs:143
|
||||||
msgid "files-download-modal.options.detach.title"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
msgstr "Treat shared library assets as basic objects"
|
msgid "files-export-modal.options.detach.title"
|
||||||
|
msgstr "Treat assets as basic objects"
|
||||||
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs:143
|
||||||
|
msgid "files-export-modal.options.detach-libraries.title"
|
||||||
|
msgstr "Treat assets as basic objects"
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:144
|
#: src/app/main/ui/exports/files.cljs:144
|
||||||
msgid "files-download-modal.options.merge.message"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
|
msgid "files-export-modal.options.merge.message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Your file will be exported with all external assets merged into the file "
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs:144
|
||||||
|
msgid "files-export-modal.options.merge-libraries.message"
|
||||||
|
msgstr ""
|
||||||
|
"Your file will be exported with library asset merged into the local "
|
||||||
"library."
|
"library."
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:145
|
#: src/app/main/ui/exports/files.cljs:145
|
||||||
msgid "files-download-modal.options.merge.title"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
msgstr "Include shared library assets in file libraries"
|
msgid "files-export-modal.options.merge.title"
|
||||||
|
msgstr "Embed library assets in the file"
|
||||||
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs:145
|
||||||
|
msgid "files-export-modal.options.merge-libraries.title"
|
||||||
|
msgstr "Embed library assets in the file"
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:123
|
#: src/app/main/ui/exports/files.cljs:123
|
||||||
msgid "files-download-modal.title"
|
msgid "files-download-modal.title"
|
||||||
|
|||||||
@ -765,6 +765,35 @@ 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."
|
||||||
|
|
||||||
|
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.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"
|
||||||
|
|
||||||
|
msgid "dashboard.import.summary.auto-linked"
|
||||||
|
msgstr "Vinculadas automáticamente"
|
||||||
|
|
||||||
|
msgid "dashboard.import.summary.your-selection"
|
||||||
|
msgstr "Tu selección"
|
||||||
|
|
||||||
|
msgid "dashboard.import.summary.linked"
|
||||||
|
msgstr "Vinculada"
|
||||||
|
|
||||||
|
msgid "dashboard.import.summary.no-selection"
|
||||||
|
msgstr "Ninguna biblioteca seleccionada"
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard.cljs:260
|
#: src/app/main/ui/dashboard.cljs:260
|
||||||
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"
|
||||||
@ -2066,33 +2095,70 @@ msgid "files-download-modal.description-2"
|
|||||||
msgstr "* Pueden incluir components, gráficos, colores y/o tipografias."
|
msgstr "* Pueden incluir components, gráficos, colores y/o tipografias."
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:140
|
#: src/app/main/ui/exports/files.cljs:140
|
||||||
msgid "files-download-modal.options.all.message"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
|
msgid "files-export-modal.options.all.message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ficheros con librerias compartidas se inclurán en el paquete de exportación "
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs:140
|
||||||
|
msgid "files-export-modal.options.include-libraries.message"
|
||||||
|
msgstr ""
|
||||||
|
"Los ficheros con librerias compartidas se inclurán en el paquete de exportación "
|
||||||
"y mantendrán los enlaces."
|
"y mantendrán los enlaces."
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:141
|
#: src/app/main/ui/exports/files.cljs:141
|
||||||
msgid "files-download-modal.options.all.title"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
msgstr "Exportar librerias compartidas"
|
msgid "files-export-modal.options.all.title"
|
||||||
|
msgstr "Exportar archivo + librerías"
|
||||||
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs:141
|
||||||
|
msgid "files-export-modal.options.include-libraries.title"
|
||||||
|
msgstr "Exportar archivo + librerías"
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:142
|
#: src/app/main/ui/exports/files.cljs:142
|
||||||
msgid "files-download-modal.options.detach.message"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
|
msgid "files-export-modal.options.detach.message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Las biblioteca compartidas no se incluirán en la exportación y ningún "
|
|
||||||
"recurso será incluido en la biblioteca. "
|
#: src/app/main/ui/exports/files.cljs:142
|
||||||
|
msgid "files-export-modal.options.detach-libraries.message"
|
||||||
|
msgstr ""
|
||||||
|
"Las recursos de las bibliotecas compartidas no se incluirán en la exportación."
|
||||||
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs
|
||||||
|
msgid "files-export-modal.options.link-later.title"
|
||||||
|
msgstr "Vincular librerías al importar"
|
||||||
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs
|
||||||
|
msgid "files-export-modal.options.link-later.message"
|
||||||
|
msgstr "Al importar, podrás vincular bibliotecas existentes con el mismo nombre."
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:143
|
#: src/app/main/ui/exports/files.cljs:143
|
||||||
msgid "files-download-modal.options.detach.title"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
msgstr "Usar los recursos como objetos básicos"
|
msgid "files-export-modal.options.detach.title"
|
||||||
|
msgstr "Tratar los recursos como objetos básicos"
|
||||||
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs:143
|
||||||
|
msgid "files-export-modal.options.detach-libraries.title"
|
||||||
|
msgstr "Tratar los recursos como objetos básicos"
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:144
|
#: src/app/main/ui/exports/files.cljs:144
|
||||||
msgid "files-download-modal.options.merge.message"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
|
msgid "files-export-modal.options.merge.message"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Tu fichero será exportado con todos los recursos dentro de la libreria del "
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs:144
|
||||||
|
msgid "files-export-modal.options.merge-libraries.message"
|
||||||
|
msgstr ""
|
||||||
|
"Tu fichero será exportado con todos los recursos externos dentro de la libreria del "
|
||||||
"propio fichero."
|
"propio fichero."
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:145
|
#: src/app/main/ui/exports/files.cljs:145
|
||||||
msgid "files-download-modal.options.merge.title"
|
# DEPRECATED: use the equivalent "-libraries" key
|
||||||
|
msgid "files-export-modal.options.merge.title"
|
||||||
|
msgstr "Incluir librerias compartidas dentro de las librerias del fichero"
|
||||||
|
|
||||||
|
#: src/app/main/ui/exports/files.cljs:145
|
||||||
|
msgid "files-export-modal.options.merge-libraries.title"
|
||||||
msgstr "Incluir librerias compartidas dentro de las librerias del fichero"
|
msgstr "Incluir librerias compartidas dentro de las librerias del fichero"
|
||||||
|
|
||||||
#: src/app/main/ui/exports/files.cljs:123
|
#: src/app/main/ui/exports/files.cljs:123
|
||||||
|
|||||||
17
plugins/libs/plugin-types/index.d.ts
vendored
17
plugins/libs/plugin-types/index.d.ts
vendored
@ -1646,19 +1646,24 @@ export interface File extends PluginData {
|
|||||||
* - `'penpot'` will create a *.penpot file with a binary representation of the file
|
* - `'penpot'` will create a *.penpot file with a binary representation of the file
|
||||||
* - `'zip'` will create a *.zip with the file exported in several SVG files with some JSON metadata
|
* - `'zip'` will create a *.zip with the file exported in several SVG files with some JSON metadata
|
||||||
* @param `libraryExportType` indicates what to do with the linked libraries of the file when
|
* @param `libraryExportType` indicates what to do with the linked libraries of the file when
|
||||||
* exporting it. Defaults to `all` if not sent.
|
* exporting it. Defaults to `'include-libraries'` if not sent.
|
||||||
* - `'all'` will include the libraries as external files that will be exported in a single bundle
|
* - `'include-libraries'` will include the libraries as external files that will be exported in a single bundle
|
||||||
* - `'merge'` will add all the assets into the main file and only one file will be imported
|
* - `'merge-libraries'` will add all the assets into the main file and only one file will be imported
|
||||||
* - `'detach'` will unlink all the external assets and no libraries will be imported
|
* - `'detach-libraries'` will unlink all the external assets and no libraries will be imported
|
||||||
|
* - `'link-later'` will preserve component metadata so instances can be relinked on import
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```js
|
* ```js
|
||||||
* const exportedData = await file.export('penpot', 'all');
|
* const exportedData = await file.export('penpot', 'include-libraries');
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export(
|
export(
|
||||||
exportType: 'penpot' | 'zip',
|
exportType: 'penpot' | 'zip',
|
||||||
libraryExportType?: 'all' | 'merge' | 'detach',
|
libraryExportType?:
|
||||||
|
| 'include-libraries'
|
||||||
|
| 'merge-libraries'
|
||||||
|
| 'detach-libraries'
|
||||||
|
| 'link-later',
|
||||||
): Promise<Uint8Array>;
|
): Promise<Uint8Array>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user