mirror of
https://github.com/penpot/penpot.git
synced 2026-08-27 23:28:38 +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
|
||||
"Get all file libraries including itself. Returns an instance of
|
||||
LoadableWeakValueMap that allows do not have strong references to
|
||||
the loaded libraries and reduce possible memory pressure on having
|
||||
all this libraries loaded at same time on processing file validation
|
||||
the loaded libraries and reduce memory pressure on having
|
||||
all this libraries at the same time on processing file validation
|
||||
or file migration.
|
||||
|
||||
This still requires at least one library at time to be loaded while
|
||||
@ -888,3 +888,47 @@
|
||||
(cons (:id file)))
|
||||
load-fn #(get-file cfg % :migrate? false)]
|
||||
(weak/loadable-weak-value-map library-ids load-fn {id file})))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; EXTERNAL LIBRARY RESOLUTION HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn slugify-name
|
||||
"Slugify a library name for cross-environment matching.
|
||||
Lowercases, replaces non-alphanumeric runs with '-', strips
|
||||
leading/trailing '-'."
|
||||
[name]
|
||||
(str/slug name))
|
||||
|
||||
(def ^:private sql:get-files-names
|
||||
"SELECT id, name FROM file WHERE id = ANY(?)")
|
||||
|
||||
(defn get-files-names
|
||||
"Return [{:id uuid :name string}] for the given file ids."
|
||||
[cfg ids]
|
||||
(db/run! cfg
|
||||
(fn [{:keys [::db/conn]}]
|
||||
(let [ids-arr (db/create-array conn "uuid" ids)]
|
||||
(db/exec! conn [sql:get-files-names ids-arr])))))
|
||||
|
||||
(def ^:private sql:get-shared-files-for-team
|
||||
"SELECT f.id, f.name, 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}
|
||||
[: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
|
||||
[:map {:title "StorageObject"}
|
||||
@ -217,14 +226,12 @@
|
||||
(.flush writer))
|
||||
(.closeEntry output))
|
||||
|
||||
|
||||
(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)
|
||||
(throw (IllegalArgumentException.
|
||||
"the `include-libraries` and `embed-assets` are mutally excluding options")))
|
||||
|
||||
(let [detach? (and (not embed-assets) (not include-libraries))]
|
||||
(let [detach? (= export-type :detach-libraries)
|
||||
embed? (= export-type :merge-libraries)]
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(cond-> (bfc/get-file cfg file-id
|
||||
{:realize? true
|
||||
@ -234,7 +241,7 @@
|
||||
(-> (ctf/detach-external-references file-id)
|
||||
(dissoc :libraries))
|
||||
|
||||
embed-assets
|
||||
embed?
|
||||
(update :data #(bfc/embed-assets cfg % file-id))
|
||||
|
||||
:always
|
||||
@ -371,12 +378,34 @@
|
||||
(write-entry! output path encoded-tokens)))))
|
||||
|
||||
(defn- export-files
|
||||
[{:keys [::bfc/ids ::bfc/include-libraries ::output] :as cfg}]
|
||||
(let [ids (into ids (when include-libraries (bfc/get-libraries cfg ids)))
|
||||
rels (if include-libraries
|
||||
[{:keys [::bfc/ids ::bfc/export-type ::output] :as cfg}]
|
||||
|
||||
(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)
|
||||
(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))
|
||||
|
||||
@ -389,12 +418,14 @@
|
||||
|
||||
;; Write manifest file
|
||||
(let [files (:files @bfc/*state*)
|
||||
params {:type "penpot/export-files"
|
||||
:version 1
|
||||
:generated-by (str "penpot/" (:full cf/version))
|
||||
:referer "penpot"
|
||||
:files (vec (vals files))
|
||||
:relations rels}]
|
||||
params (cond-> {:type "penpot/export-files"
|
||||
:version 1
|
||||
:generated-by (str "penpot/" (:full cf/version))
|
||||
:referer "penpot"
|
||||
:files (vec (vals files))
|
||||
:relations rels}
|
||||
(seq external-libs)
|
||||
(assoc :external-libraries external-libs))]
|
||||
(write-entry! output "manifest.json" params))))
|
||||
|
||||
;; --- IMPORT IMPL
|
||||
@ -882,6 +913,104 @@
|
||||
|
||||
(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*
|
||||
[{:keys [::manifest] :as cfg}]
|
||||
(bfc/disable-database-timeouts! cfg)
|
||||
@ -890,18 +1019,37 @@
|
||||
|
||||
(import-storage-objects cfg)
|
||||
|
||||
(let [files (get manifest :files)
|
||||
result (reduce (fn [result file]
|
||||
(let [name' (get file :name)
|
||||
file (assoc file :name name')]
|
||||
(conj result (import-file cfg file))))
|
||||
[]
|
||||
files)]
|
||||
;; Pre-resolve external libraries and add their id mappings to the index
|
||||
;; BEFORE importing files. This allows relink-refs (inside process-file)
|
||||
;; to correctly remap :component-file references to the destination library.
|
||||
;; Only remap when a link will actually be created (single match + can-edit).
|
||||
(let [decisions (compute-link-decisions cfg)]
|
||||
(doseq [[old-lib-id {:keys [library-id]}] decisions]
|
||||
(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)
|
||||
(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*
|
||||
[{:keys [::manifest ::bfc/file-id] :as cfg}]
|
||||
@ -929,7 +1077,8 @@
|
||||
(bfc/invalidate-thumbnails cfg file-id)
|
||||
(bfm/apply-pending-migrations! cfg)
|
||||
|
||||
[file-id])))
|
||||
{:file-ids [file-id]
|
||||
:resolution {}})))
|
||||
|
||||
(defn- import-files
|
||||
[{: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
|
||||
format. There are some options available for customize the output:
|
||||
|
||||
`::bfc/include-libraries`: additionally to the specified file, all the
|
||||
linked libraries also will be included (including transitive
|
||||
dependencies).
|
||||
|
||||
`::bfc/embed-assets`: instead of including the libraries, embed in the
|
||||
same file library all assets used from external libraries."
|
||||
`::bfc/export-type`: determines how linked libraries are handled.
|
||||
Valid values: `:include-libraries` (include linked libraries),
|
||||
`:merge-libraries` (embed library assets in the file),
|
||||
`:detach-libraries` (treat assets as basic objects),
|
||||
`:link-later` (preserve component metadata for relinking on import)."
|
||||
|
||||
[{:keys [::bfc/ids] :as cfg} output]
|
||||
|
||||
@ -998,6 +1146,7 @@
|
||||
tp (ct/tpoint)
|
||||
ab (volatile! false)
|
||||
cs (volatile! nil)]
|
||||
|
||||
(try
|
||||
(l/info :hint "start exportation" :export-id (str id))
|
||||
(binding [bfc/*state* (volatile! (bfc/initial-state))]
|
||||
|
||||
@ -42,17 +42,24 @@
|
||||
schema:export-binfile
|
||||
[:map {:title "export-binfile"}
|
||||
[:file-id ::sm/uuid]
|
||||
[:include-libraries ::sm/boolean]
|
||||
[:embed-assets ::sm/boolean]])
|
||||
[:type {:optional true} [::sm/one-of #{:include-libraries :merge-libraries :detach-libraries :link-later}]]
|
||||
[:include-libraries {:optional true} ::sm/boolean]
|
||||
[:embed-assets {:optional true} ::sm/boolean]])
|
||||
|
||||
(defn- export-binfile
|
||||
[{:keys [::sto/storage] :as cfg} {:keys [file-id include-libraries embed-assets]}]
|
||||
(let [output (tmp/tempfile*)]
|
||||
[{:keys [::sto/storage] :as cfg} {:keys [type file-id include-libraries embed-assets]}]
|
||||
(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
|
||||
(-> cfg
|
||||
(assoc ::bfc/ids #{file-id})
|
||||
(assoc ::bfc/embed-assets embed-assets)
|
||||
(assoc ::bfc/include-libraries include-libraries)
|
||||
(assoc ::bfc/export-type export-type)
|
||||
(bf.v3/export-files! output))
|
||||
|
||||
(let [data (sto/content output)
|
||||
@ -73,7 +80,8 @@
|
||||
(sv/defmethod ::export-binfile
|
||||
"Export a penpot file in a binary format."
|
||||
{::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
|
||||
::sm/params schema:export-binfile}
|
||||
[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/project-id project-id)
|
||||
(assoc ::bfc/profile-id profile-id)
|
||||
(assoc ::bfc/team-id (:id team))
|
||||
(assoc ::bfc/name name))
|
||||
|
||||
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
|
||||
|
||||
ENV OPENCODE_VERSION=1.18.19
|
||||
ENV OPENCODE_VERSION=1.18.21
|
||||
|
||||
RUN set -ex; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
|
||||
@ -21,8 +21,8 @@
|
||||
:exclusions [funcool/beicon2]}
|
||||
|
||||
funcool/beicon2
|
||||
{:git/tag "v2.2"
|
||||
:git/sha "8744c66"
|
||||
{:git/tag "v2.3"
|
||||
:git/sha "df7058a"
|
||||
:git/url "https://github.com/funcool/beicon.git"}
|
||||
|
||||
funcool/rumext
|
||||
|
||||
@ -17,18 +17,21 @@
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(def valid-types
|
||||
(d/ordered-set :all :merge :detach))
|
||||
(d/ordered-set :include-libraries :merge-libraries :detach-libraries :link-later))
|
||||
|
||||
(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
|
||||
[:sequential {:title "Files"}
|
||||
[:map {:title "FileParam"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:project-id ::sm/uuid]
|
||||
[:is-shared ::sm/boolean]]])
|
||||
[:sequential {:title "Files"} schema:export-file-param])
|
||||
|
||||
(def check-export-files
|
||||
(sm/check-fn schema:export-files))
|
||||
@ -57,14 +60,17 @@
|
||||
:files files}))))))))))
|
||||
|
||||
(defn export-files
|
||||
"Start files exportation process"
|
||||
[& {: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/mapcat
|
||||
(fn [file]
|
||||
(->> (rp/cmd! ::sse/export-binfile {:file-id (:id file)
|
||||
:version 3
|
||||
:include-libraries (= type :all)
|
||||
:embed-assets (= type :merge)})
|
||||
:type type})
|
||||
(rx/filter sse/end-of-stream?)
|
||||
(rx/map sse/get-payload)
|
||||
(rx/map (fn [uri]
|
||||
|
||||
@ -15,11 +15,20 @@
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||
[app.main.ui.ds.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.icons :as deprecated-icon]
|
||||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||
[app.main.worker :as mw]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@ -54,7 +63,7 @@
|
||||
{::mf/forward-ref true}
|
||||
[{:keys [project-id on-finish-import]} external-ref]
|
||||
(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"
|
||||
:multi true
|
||||
:ref external-ref
|
||||
@ -156,6 +165,19 @@
|
||||
(and (= :import-ready (:status 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
|
||||
[state entries]
|
||||
(let [features (get @st/state :features)]
|
||||
@ -173,7 +195,7 @@
|
||||
(swap! state update-with-analyze-result message))))))
|
||||
|
||||
(defn- import-files
|
||||
[state project-id entries]
|
||||
[state library-resolution-data* project-id entries]
|
||||
(st/emit! (ev/event {::ev/name "import-files"
|
||||
:num-files (count entries)}))
|
||||
|
||||
@ -183,27 +205,40 @@
|
||||
:project-id project-id
|
||||
:files entries
|
||||
:features features})
|
||||
(rx/filter (comp uuid? :file-id))
|
||||
(rx/filter some?)
|
||||
(rx/subs!
|
||||
(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/memo 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)
|
||||
;; FIXME: rename to format
|
||||
format (:type entry)
|
||||
|
||||
loading? (or (= :analyze status)
|
||||
(= :import-progress status)
|
||||
(and importing? (= :import-ready status)))
|
||||
(and is-progress (= :import-ready status)))
|
||||
analyze-error? (= :analyze-error status)
|
||||
import-success? (= :import-success status)
|
||||
import-error? (= :import-error 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)
|
||||
progress (:progress entry)
|
||||
|
||||
@ -251,46 +286,65 @@
|
||||
:editable (and import-ready? (not editing?)))}
|
||||
|
||||
[:div {:class (stl/css :file-name)}
|
||||
(if 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)])
|
||||
(when loading? [:> loader* {:width 26 :title (tr "labels.loading")}])
|
||||
|
||||
(if editing?
|
||||
[:div {:class (stl/css :file-name-edit)}
|
||||
[:input {:type "text"
|
||||
:auto-focus true
|
||||
:class (stl/css :file-name-input)
|
||||
;;TODO: Add translation for aria-label
|
||||
:aria-label "File name"
|
||||
:default-value (:name entry)
|
||||
:on-key-press on-edit-key-press
|
||||
:on-blur on-edit-blur}]]
|
||||
|
||||
[:div {:class (stl/css :file-name-label)}
|
||||
(:name entry)
|
||||
(when ^boolean is-shared?
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/library])])
|
||||
|
||||
[:div {:class (stl/css :edit-entry-buttons)}
|
||||
(when ^boolean editable?
|
||||
[:button {:on-click on-edit'} deprecated-icon/curve])
|
||||
(when ^boolean can-be-deleted
|
||||
[:button {:on-click on-delete'} deprecated-icon/delete])]]
|
||||
(if loading?
|
||||
[:> 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)}])]
|
||||
[:> context-notification*
|
||||
{:level level
|
||||
: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
|
||||
analyze-error?
|
||||
[:div {:class (stl/css :error-message)}
|
||||
[:> text* {:class (stl/css :error-message)
|
||||
:as "span"
|
||||
:typography t/body-small}
|
||||
(if (some? (:error entry))
|
||||
(tr (:error entry))
|
||||
(tr "dashboard.import.analyze-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))
|
||||
(tr (:error entry))
|
||||
(tr "labels.error"))]
|
||||
@ -318,6 +372,327 @@
|
||||
(fn []
|
||||
(mapv #(assoc % :status :analyze) entries)))
|
||||
|
||||
(defn- link-files-to-library!
|
||||
"Call the link-file-to-library RPC for each file-id with the given
|
||||
library-id. Returns an observable that completes when all links are done."
|
||||
[file-ids library-id]
|
||||
(->> (rx/from file-ids)
|
||||
(rx/merge-map (fn [file-id]
|
||||
(->> (rp/cmd! :link-file-to-library
|
||||
{:file-id file-id
|
||||
:library-id library-id})
|
||||
(rx/catch (fn [cause]
|
||||
(log/error :hint "failed to link library"
|
||||
:file-id file-id
|
||||
:library-id library-id
|
||||
:cause cause)
|
||||
(rx/of nil))))))))
|
||||
|
||||
(mf/defc library-resolution*
|
||||
{::mf/private true}
|
||||
[{:keys [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/register modal/components
|
||||
::mf/register-as :import
|
||||
@ -329,14 +704,49 @@
|
||||
;; Revoke all uri's on commonent unmount
|
||||
(fn [] (run! wapi/revoke-uri (map :uri entries))))
|
||||
|
||||
(let [state* (mf/use-state (initialize-state entries))
|
||||
entries (deref state*)
|
||||
(let [state* (mf/use-state (initialize-state entries))
|
||||
entries (deref state*)
|
||||
|
||||
status* (mf/use-state :analyze)
|
||||
status (deref status*)
|
||||
status* (mf/use-state :analyze)
|
||||
status (deref status*)
|
||||
|
||||
edition* (mf/use-state nil)
|
||||
edition (deref edition*)
|
||||
edition* (mf/use-state nil)
|
||||
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
|
||||
(mf/use-fn
|
||||
@ -344,7 +754,7 @@
|
||||
(fn []
|
||||
(let [entries (filterv has-status-ready? entries)]
|
||||
(reset! status* :import-progress)
|
||||
(import-files state* project-id entries))))
|
||||
(import-files state* resolution* project-id entries))))
|
||||
|
||||
continue-template
|
||||
(mf/use-fn
|
||||
@ -407,6 +817,52 @@
|
||||
(continue-template template)
|
||||
(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
|
||||
(mf/use-fn
|
||||
(mf/deps on-finish-import)
|
||||
@ -432,9 +888,25 @@
|
||||
(zero? (count entries))))
|
||||
|
||||
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
|
||||
(some? template)
|
||||
(reset! status* :import-ready)
|
||||
@ -445,8 +917,11 @@
|
||||
|
||||
(and (seq 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 (every? #(not= :import-ready (:status %)) entries)
|
||||
(some #(= :import-error (:status %)) entries)))
|
||||
@ -460,99 +935,50 @@
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[: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)
|
||||
:on-click on-cancel} deprecated-icon/close]]
|
||||
(case status
|
||||
(: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)}
|
||||
(when (and (= :analyze status) errors?)
|
||||
[:& context-notification
|
||||
{:level :warning
|
||||
:class (stl/css :context-notification-error)
|
||||
:content (tr "dashboard.import.import-warning")}])
|
||||
:library-resolution
|
||||
[:> import-library-resolution-stage*
|
||||
{:current-unresolved-file current-unresolved-file
|
||||
:selection selection
|
||||
:on-select manage-on-select
|
||||
:visited visited
|
||||
:all-visited? all-visited?
|
||||
:on-wizard-prev on-wizard-prev
|
||||
:on-wizard-next on-wizard-next}]
|
||||
|
||||
(when (= :import-success status)
|
||||
[:& context-notification
|
||||
{:level (if (zero? import-success-total) :warning :success)
|
||||
:content (tr "dashboard.import.import-message" (i18n/c import-success-total))}])
|
||||
:library-summary
|
||||
[:> import-library-summary-stage*
|
||||
{:resolution resolution
|
||||
:selection selection
|
||||
:visited visited
|
||||
:on-summary-back on-summary-back
|
||||
:on-confirm-library-links on-confirm-library-links}]
|
||||
|
||||
(when (= :import-error status)
|
||||
[:& 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}])]]]]))
|
||||
nil)]]))
|
||||
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
|
||||
|
||||
@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 {
|
||||
@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 {
|
||||
@extend %modal-container-base;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.uppercase-title-typography;
|
||||
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
@extend %modal-close-btn-base;
|
||||
gap: var(--sp-xxxl);
|
||||
padding: var(--sp-xxxl);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-primary);
|
||||
border: $b-2 solid var(--color-background-quaternary);
|
||||
min-block-size: $sz-192;
|
||||
inline-size: $sz-512;
|
||||
max-block-size: px2rem(800);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
@include deprecated.body-small-typography;
|
||||
|
||||
flex: 1;
|
||||
overflow: hidden auto;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: deprecated.$s-16;
|
||||
margin-bottom: deprecated.$s-24;
|
||||
min-height: 40px;
|
||||
gap: var(--sp-l);
|
||||
margin-block-end: var(--sp-xxl);
|
||||
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 {
|
||||
@include deprecated.body-small-typography;
|
||||
@include use-typography("body-small");
|
||||
|
||||
color: var(--modal-title-foreground-color);
|
||||
color: var(--color-foreground-primary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@extend %modal-action-btns;
|
||||
}
|
||||
|
||||
.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;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--sp-l);
|
||||
}
|
||||
|
||||
.file-entry {
|
||||
--file-entry-fg-color: var(--color-foreground-secondary);
|
||||
|
||||
display: flex;
|
||||
|
||||
.file-name {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
|
||||
&.success {
|
||||
.file-name {
|
||||
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);
|
||||
}
|
||||
}
|
||||
--file-entry-fg-color: var(--color-accent-sucess);
|
||||
}
|
||||
|
||||
&.error {
|
||||
.file-name {
|
||||
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);
|
||||
}
|
||||
}
|
||||
--file-entry-fg-color: var(--color-accent-error);
|
||||
}
|
||||
|
||||
&.editable {
|
||||
.file-name {
|
||||
color: var(--modal-text-foreground-color);
|
||||
--file-entry-fg-color: var(--color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.file-icon svg {
|
||||
stroke: var(--modal-text-foreground-color);
|
||||
}
|
||||
.error-message,
|
||||
.progress-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-block-size: $sz-32;
|
||||
color: var(--file-entry-fg-color);
|
||||
}
|
||||
|
||||
.file-icon.icon-fill svg {
|
||||
fill: 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: 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-bg-color: var(--modal-background-color);
|
||||
--context-notification-bg-color: var(--color-background-primary);
|
||||
}
|
||||
|
||||
.file-name-notification {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.import-error-disclaimer {
|
||||
@ -266,10 +224,252 @@
|
||||
}
|
||||
|
||||
.import-error-detail {
|
||||
@include deprecated.body-small-typography;
|
||||
|
||||
margin-top: var(--sp-xs);
|
||||
color: var(--modal-text-foreground-color);
|
||||
// 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;
|
||||
margin-block-start: var(--sp-xs);
|
||||
color: var(--color-foreground-secondary);
|
||||
white-space: pre-wrap;
|
||||
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.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.timers :as timers]
|
||||
[clojure.string :as str]
|
||||
[rumext.v2 :as mf]
|
||||
[rumext.v2.util :as mfu]))
|
||||
@ -58,11 +60,12 @@
|
||||
[:empty-to-end {:optional true} [:maybe :boolean]]
|
||||
[:on-change {:optional true} fn?]
|
||||
[: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/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
|
||||
;; options provide as clojure data structures or javascript
|
||||
;; plain objects and lists.
|
||||
@ -88,6 +91,9 @@
|
||||
options-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?
|
||||
(str/blank? selected-id)
|
||||
|
||||
@ -208,10 +214,63 @@
|
||||
(reset! selected-id*
|
||||
(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)]
|
||||
:on-click on-click
|
||||
:ref select-ref
|
||||
:on-blur on-blur}
|
||||
:on-blur (when-not has-portal on-blur)}
|
||||
|
||||
[:> :button props
|
||||
[:span {:class (stl/css-case :select-header true
|
||||
@ -241,11 +300,24 @@
|
||||
:aria-hidden true}]]
|
||||
|
||||
(when ^boolean is-open
|
||||
[:> 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}])]))
|
||||
(if has-portal
|
||||
(mf/portal
|
||||
(mf/html
|
||||
[:> 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
|
||||
: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.modal :as modal]
|
||||
[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.icons :as deprecated-icon]
|
||||
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
@ -41,28 +47,33 @@
|
||||
[files]
|
||||
(let [files (mapv (fn [file] (assoc file :loading true)) files)]
|
||||
{:status :prepare
|
||||
:selected :all
|
||||
:selected :include-libraries
|
||||
:files files}))
|
||||
|
||||
(mf/defc export-entry*
|
||||
{::mf/private true}
|
||||
[{:keys [file]}]
|
||||
[:div {:class (stl/css-case
|
||||
:file-entry true
|
||||
:loading (:loading file)
|
||||
:success (:export-success? file)
|
||||
:error (:export-error? file))}
|
||||
(let [level (cond
|
||||
(:export-success? file) :success
|
||||
(:export-error? file) :error
|
||||
:else :info)]
|
||||
[: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)
|
||||
[:> loader* {:width 16
|
||||
:title (tr "labels.loading")}]
|
||||
[:span {:class (stl/css :file-icon)}
|
||||
(cond (:export-success? file) deprecated-icon/tick
|
||||
(:export-error? file) deprecated-icon/close)])
|
||||
(if (:loading file)
|
||||
[:div {:class (stl/css :file-name)}
|
||||
[:> loader* {:width 26
|
||||
:title (tr "labels.loading")}]
|
||||
[:> text* {:class (stl/css :file-name-label)
|
||||
:as "span"
|
||||
:typography t/body-large}
|
||||
(:name file)]]
|
||||
|
||||
[:div {:class (stl/css :file-name-label)}
|
||||
(:name file)]]])
|
||||
[:> context-notification {:level level
|
||||
:content (:name file)}])]))
|
||||
|
||||
(mf/defc export-dialog
|
||||
{::mf/register modal/components
|
||||
@ -109,6 +120,7 @@
|
||||
(let [type (-> (dom/get-target event)
|
||||
(dom/get-data "type")
|
||||
(keyword))]
|
||||
(prn "AAA" selected type)
|
||||
(swap! state* assoc :selected type))))]
|
||||
|
||||
(mf/with-effect [has-libs?]
|
||||
@ -119,38 +131,59 @@
|
||||
[:div {:class (stl/css :modal-overlay)}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[: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")]
|
||||
[:button {:class (stl/css :modal-close-btn)
|
||||
:on-click on-cancel} deprecated-icon/close]]
|
||||
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-cancel
|
||||
:class (stl/css :modal-close-btn)
|
||||
:icon i/close}]]
|
||||
(cond
|
||||
(= status :prepare)
|
||||
[:*
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:p {:class (stl/css :modal-msg)} (tr "files-download-modal.description-1")]
|
||||
[:p {:class (stl/css :modal-scd-msg)} (tr "files-download-modal.description-2")]
|
||||
;; TODO: Add translation
|
||||
[:> 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]
|
||||
[:div {:class (stl/css :export-option true)
|
||||
:key (name type)}
|
||||
[:label {:for (str "export-" type)
|
||||
:class (stl/css-case :global/checked (= selected type))}
|
||||
:class (stl/css :export-option-label)}
|
||||
;; Execution time translation strings:
|
||||
;; (tr "files-download-modal.options.all.message")
|
||||
;; (tr "files-download-modal.options.all.title")
|
||||
;; (tr "files-download-modal.options.detach.message")
|
||||
;; (tr "files-download-modal.options.detach.title")
|
||||
;; (tr "files-download-modal.options.merge.message")
|
||||
;; (tr "files-download-modal.options.merge.title")
|
||||
[:span {:class (stl/css-case :global/checked (= selected type))}
|
||||
;; (tr "files-export-modal.options.include-libraries.title")
|
||||
;; (tr "files-export-modal.options.include-libraries.message")
|
||||
|
||||
;; (tr "files-export-modal.options.merge-libraries.title")
|
||||
;; (tr "files-export-modal.options.merge-libraries.message")
|
||||
|
||||
;; (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)
|
||||
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)}
|
||||
[:h3 {:class (stl/css :modal-subtitle)}
|
||||
(tr (dm/str "files-download-modal.options." (d/name type) ".title"))]
|
||||
[:p {:class (stl/css :modal-msg)}
|
||||
(tr (dm/str "files-download-modal.options." (d/name type) ".message"))]]
|
||||
[:> heading* {:level 3
|
||||
:typography t/body-large
|
||||
:class (stl/css :option-title)}
|
||||
(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"
|
||||
:class (stl/css :option-input)
|
||||
@ -162,15 +195,15 @@
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:input {:class (stl/css :cancel-button)
|
||||
:type "button"
|
||||
:value (tr "labels.cancel")
|
||||
:on-click on-cancel}]
|
||||
[:> button* {:variant "secondary"
|
||||
:type "button"
|
||||
:on-click on-cancel}
|
||||
(tr "labels.cancel")]
|
||||
|
||||
[:input {:class (stl/css :accept-btn)
|
||||
:type "button"
|
||||
:value (tr "labels.continue")
|
||||
:on-click on-accept}]]]]
|
||||
[:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:on-click on-accept}
|
||||
(tr "labels.continue")]]]]
|
||||
|
||||
(= status :exporting)
|
||||
(let [in-progress? (->> state :files (some :loading))]
|
||||
@ -180,15 +213,15 @@
|
||||
[:> export-entry* {:file file :key (dm/str (:id file))}])
|
||||
|
||||
(when in-progress?
|
||||
[:div {:class (stl/css :status-message)
|
||||
:role "status"
|
||||
:aria-live "polite"}
|
||||
[:> text* {:as "span" :typography t/body-large :class (stl/css :status-message)
|
||||
:role "status"
|
||||
:aria-live "polite"}
|
||||
(tr "labels.downloading-file")])]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:input {:class (stl/css :accept-btn)
|
||||
:type "button"
|
||||
:value (tr "labels.close")
|
||||
:disabled in-progress?
|
||||
:on-click on-cancel}]]]]))]]))
|
||||
[:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:disabled in-progress?
|
||||
:on-click on-cancel}
|
||||
(tr "labels.close")]]]]))]]))
|
||||
|
||||
@ -4,289 +4,205 @@
|
||||
//
|
||||
// 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
|
||||
.modal-overlay {
|
||||
@extend %modal-overlay-base;
|
||||
|
||||
&.transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
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 {
|
||||
@extend %modal-container-base;
|
||||
|
||||
max-height: calc(10 * deprecated.$s-80);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
margin-bottom: deprecated.$s-24;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.headline-medium-typography;
|
||||
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
@extend %modal-close-btn-base;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxxl);
|
||||
padding: var(--sp-xxxl);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-primary);
|
||||
border: $b-2 solid var(--color-background-quaternary);
|
||||
min-block-size: $sz-192;
|
||||
inline-size: $sz-512;
|
||||
max-block-size: calc(10 * px2rem(80));
|
||||
}
|
||||
|
||||
.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 {
|
||||
@include deprecated.body-large-typography;
|
||||
.modal-title {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: var(--modal-link-foreground-color);
|
||||
}
|
||||
.modal-close-btn {
|
||||
position: absolute;
|
||||
inset-block-start: var(--sp-m);
|
||||
inset-inline-end: var(--sp-m);
|
||||
}
|
||||
|
||||
.selection-header {
|
||||
@include deprecated.flex-row;
|
||||
.modal-msg {
|
||||
color: var(--color-foreground-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
height: deprecated.$s-32;
|
||||
margin-bottom: deprecated.$s-4;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-message {
|
||||
@include deprecated.body-small-typography;
|
||||
|
||||
color: var(--modal-title-foreground-color);
|
||||
color: var(--color-foreground-primary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@extend %modal-action-btns;
|
||||
}
|
||||
|
||||
.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);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--sp-s);
|
||||
}
|
||||
|
||||
.export-option {
|
||||
@extend %input-checkbox;
|
||||
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
align-items: flex-start;
|
||||
.export-option-label {
|
||||
--input-border-color: var(--input-checkbox-border-color-rest);
|
||||
--input-icon-color: var(--color-background-primary);
|
||||
|
||||
.modal-subtitle {
|
||||
@include deprecated.body-large-typography;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: px2rem(6);
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground-primary);
|
||||
|
||||
color: var(--modal-title-foreground-color);
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
&:hover {
|
||||
--input-border-color: var(--color-accent-primary-muted);
|
||||
}
|
||||
|
||||
span {
|
||||
margin-top: deprecated.$s-8;
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
--input-border-color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.option-content {
|
||||
@include deprecated.flex-column;
|
||||
@include deprecated.body-large-typography;
|
||||
.option-icon-wrapper {
|
||||
--icon-display: none;
|
||||
--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-name {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
--file-entry-color: var(--color-foreground-secondary);
|
||||
|
||||
&.loading {
|
||||
.file-name {
|
||||
color: var(--modal-text-foreground-color);
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
&.error {
|
||||
.file-name {
|
||||
color: var(--modal-text-foreground-color);
|
||||
color: var(--color-foreground-secondary);
|
||||
|
||||
.file-icon svg {
|
||||
stroke: var(--modal-text-foreground-color);
|
||||
stroke: var(--color-foreground-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
.file-name {
|
||||
color: var(--modal-text-foreground-color);
|
||||
color: var(--color-foreground-secondary);
|
||||
|
||||
.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]
|
||||
(js/Promise.
|
||||
(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
|
||||
(and (some? format) (not (contains? #{"penpot" "zip"} format)))
|
||||
(u/reject-not-valid reject :format (dm/str "Invalid format: " format))
|
||||
|
||||
@ -171,8 +171,9 @@
|
||||
|
||||
(defmethod impl/handler :import-files
|
||||
[{:keys [project-id files]}]
|
||||
(let [binfile-v1 (filter #(= :binfile-v1 (:type %)) files)
|
||||
binfile-v3 (filter #(= :binfile-v3 (:type %)) files)]
|
||||
(let [binfile-v1 (filter #(= :binfile-v1 (:type %)) files)
|
||||
binfile-v3 (filter #(= :binfile-v3 (:type %)) files)
|
||||
resolutions (volatile! {})]
|
||||
|
||||
(rx/merge
|
||||
(->> (rx/from binfile-v1)
|
||||
@ -203,40 +204,50 @@
|
||||
:error (import-cause-message cause (tr "labels.error"))
|
||||
:file-id (:file-id data)})))))))
|
||||
|
||||
(->> (rx/from binfile-v3)
|
||||
(rx/reduce (fn [result file]
|
||||
(update result (:uri file) (fnil conj []) file))
|
||||
{})
|
||||
(rx/mapcat identity)
|
||||
(rx/merge-map
|
||||
(fn [[uri entries]]
|
||||
(->> (import-blob-via-upload uri
|
||||
{:name (-> entries first :name)
|
||||
:version 3
|
||||
:project-id project-id})
|
||||
(rx/tap (fn [event]
|
||||
(let [payload (sse/get-payload event)
|
||||
type (sse/get-type event)]
|
||||
(if (= type "progress")
|
||||
(log/dbg :hint "import-binfile: progress"
|
||||
:section (:section payload)
|
||||
:name (:name payload))
|
||||
(log/dbg :hint "import-binfile: end")))))
|
||||
(rx/filter sse/end-of-stream?)
|
||||
(rx/mapcat (fn [_]
|
||||
(->> (rx/from entries)
|
||||
(rx/map (fn [entry]
|
||||
{:status :finish
|
||||
:file-id (:file-id entry)})))))
|
||||
(rx/catch
|
||||
(fn [cause]
|
||||
(log/error :hint "unexpected error on import process"
|
||||
:project-id project-id
|
||||
::log/sync? true
|
||||
:cause cause)
|
||||
(let [err (import-cause-message cause (tr "labels.error"))]
|
||||
(->> (rx/from entries)
|
||||
(rx/map (fn [entry]
|
||||
{:status :error
|
||||
:error err
|
||||
:file-id (:file-id entry)})))))))))))))
|
||||
|
||||
(rx/concat
|
||||
(->> (rx/from binfile-v3)
|
||||
(rx/reduce (fn [result file]
|
||||
(update result (:uri file) (fnil conj []) file))
|
||||
{})
|
||||
(rx/mapcat identity)
|
||||
(rx/merge-map
|
||||
(fn [[uri entries]]
|
||||
(->> (import-blob-via-upload uri
|
||||
{:name (-> entries first :name)
|
||||
:version 3
|
||||
:project-id project-id})
|
||||
(rx/tap (fn [event]
|
||||
(let [payload (sse/get-payload event)
|
||||
type (sse/get-type event)]
|
||||
(cond
|
||||
(= type "progress")
|
||||
(log/dbg :hint "import-binfile: progress"
|
||||
:section (:section payload)
|
||||
:name (:name payload))
|
||||
|
||||
:else
|
||||
(log/dbg :hint "import-binfile: end")))))
|
||||
(rx/filter sse/end-of-stream?)
|
||||
(rx/mapcat (fn [message]
|
||||
(let [{:keys [resolution]} (sse/get-payload message)]
|
||||
(when (seq resolution)
|
||||
(vswap! resolutions merge resolution))
|
||||
(->> (rx/from entries)
|
||||
(rx/map (fn [entry]
|
||||
{:status :finish
|
||||
:file-id (:file-id entry)}))))))
|
||||
(rx/catch (fn [cause]
|
||||
(log/error :hint "import-binfile: unexpected error on importing"
|
||||
:project-id project-id
|
||||
::log/sync? true
|
||||
:cause cause)
|
||||
(let [err (import-cause-message cause (tr "labels.error"))]
|
||||
(->> (rx/from entries)
|
||||
(rx/map (fn [entry]
|
||||
{:status :error
|
||||
:error err
|
||||
:file-id (:file-id entry)}))))))))))
|
||||
(->> (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"
|
||||
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
|
||||
msgid "dashboard.import.no-perms"
|
||||
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."
|
||||
|
||||
#: 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 ""
|
||||
"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
|
||||
msgid "files-download-modal.options.all.title"
|
||||
msgstr "Export shared libraries"
|
||||
# DEPRECATED: use the equivalent "-libraries" key
|
||||
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
|
||||
msgid "files-download-modal.options.detach.message"
|
||||
msgstr ""
|
||||
"Shared libraries will not be included in the export and no assets will be "
|
||||
"added to the library. "
|
||||
# DEPRECATED: use the equivalent "-libraries" key
|
||||
msgid "files-export-modal.options.detach.message"
|
||||
msgstr "Linked library assets won't be included in the file."
|
||||
|
||||
#: 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
|
||||
msgid "files-download-modal.options.detach.title"
|
||||
msgstr "Treat shared library assets as basic objects"
|
||||
# DEPRECATED: use the equivalent "-libraries" key
|
||||
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
|
||||
msgid "files-download-modal.options.merge.message"
|
||||
# DEPRECATED: use the equivalent "-libraries" key
|
||||
msgid "files-export-modal.options.merge.message"
|
||||
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."
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs:145
|
||||
msgid "files-download-modal.options.merge.title"
|
||||
msgstr "Include shared library assets in file libraries"
|
||||
# DEPRECATED: use the equivalent "-libraries" key
|
||||
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
|
||||
msgid "files-download-modal.title"
|
||||
|
||||
@ -765,6 +765,35 @@ msgstr[1] "%s ficheros se han importado correctamente."
|
||||
msgid "dashboard.import.import-warning"
|
||||
msgstr "Algunos ficheros contenían objetos erroneos que no han sido importados."
|
||||
|
||||
msgid "dashboard.import.auto-linked-libraries"
|
||||
msgid_plural "dashboard.import.auto-linked-libraries"
|
||||
msgstr[0] "1 biblioteca fue vinculada automáticamente por nombre."
|
||||
msgstr[1] "%s bibliotecas fueron vinculadas automáticamente por nombre."
|
||||
|
||||
msgid "dashboard.import.resolve-libraries"
|
||||
msgstr "Algunas bibliotecas no pudieron vincularse automáticamente. Selecciona la biblioteca correcta para cada una:"
|
||||
|
||||
msgid "dashboard.import.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
|
||||
msgid "dashboard.import.no-perms"
|
||||
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."
|
||||
|
||||
#: 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 ""
|
||||
"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."
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs:141
|
||||
msgid "files-download-modal.options.all.title"
|
||||
msgstr "Exportar librerias compartidas"
|
||||
# DEPRECATED: use the equivalent "-libraries" key
|
||||
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
|
||||
msgid "files-download-modal.options.detach.message"
|
||||
# DEPRECATED: use the equivalent "-libraries" key
|
||||
msgid "files-export-modal.options.detach.message"
|
||||
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
|
||||
msgid "files-download-modal.options.detach.title"
|
||||
msgstr "Usar los recursos como objetos básicos"
|
||||
# DEPRECATED: use the equivalent "-libraries" key
|
||||
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
|
||||
msgid "files-download-modal.options.merge.message"
|
||||
# DEPRECATED: use the equivalent "-libraries" key
|
||||
msgid "files-export-modal.options.merge.message"
|
||||
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."
|
||||
|
||||
#: 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"
|
||||
|
||||
#: 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
|
||||
* - `'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
|
||||
* exporting it. Defaults to `all` if not sent.
|
||||
* - `'all'` 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
|
||||
* - `'detach'` will unlink all the external assets and no libraries will be imported
|
||||
* exporting it. Defaults to `'include-libraries'` if not sent.
|
||||
* - `'include-libraries'` will include the libraries as external files that will be exported in a single bundle
|
||||
* - `'merge-libraries'` will add all the assets into the main file and only one file 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
|
||||
* ```js
|
||||
* const exportedData = await file.export('penpot', 'all');
|
||||
* const exportedData = await file.export('penpot', 'include-libraries');
|
||||
* ```
|
||||
*/
|
||||
export(
|
||||
exportType: 'penpot' | 'zip',
|
||||
libraryExportType?: 'all' | 'merge' | 'detach',
|
||||
libraryExportType?:
|
||||
| 'include-libraries'
|
||||
| 'merge-libraries'
|
||||
| 'detach-libraries'
|
||||
| 'link-later',
|
||||
): Promise<Uint8Array>;
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user