mirror of
https://github.com/penpot/penpot.git
synced 2026-10-29 14:00:37 +00:00
✨ Rework the export dialog
Every exported file is its own job, so each row of the dialog reports its own state: waiting for a worker, running with the milestone of the job and an animated bar, exported once the artifact downloads, or failed in a red row with the hint of the job. The header only counts the files and subtitles the artifact format and, once it exists, its size. Closing changes too: the Close button is gone, the X cancels like Cancel Export and the dialog stays with the terminal states, and Done dismisses it. Success, failure and cancellation replace each other, so a row never reports two outcomes at once, and rows are matched by id canonicalized to string. The retry button of the finished design is left out: no retry mechanism exists on the client yet. AI-assisted-by: muse-spark-1.3
This commit is contained in:
parent
464756ec59
commit
8cdbdbb6da
@ -105,7 +105,8 @@
|
||||
(= "completed" status)
|
||||
(rx/of {:file-id (:id file)
|
||||
:uri (:resource-uri result)
|
||||
:filename (:name file)})
|
||||
:filename (:name file)
|
||||
:size (:size result)})
|
||||
|
||||
(= "failed" status)
|
||||
(rx/of {:file-id (:id file)
|
||||
@ -121,9 +122,14 @@
|
||||
|
||||
:else
|
||||
(rx/empty)))))))
|
||||
;; a transport failure carries no job error: a nil `ex-data` would
|
||||
;; make the dialog drop the message and leave the row hanging, so a
|
||||
;; synthetic error takes its place and the row reports it
|
||||
(rx/catch (fn [cause]
|
||||
(rx/of {:file-id (:id file)
|
||||
:error (ex-data cause)})))))
|
||||
:error (or (ex-data cause)
|
||||
{:type :internal
|
||||
:code :unknown-error})})))))
|
||||
|
||||
(defn export-files
|
||||
"Start files exportation process.
|
||||
|
||||
@ -11,55 +11,107 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.config :as cf]
|
||||
[app.main.data.exports.files :as fexp]
|
||||
[app.main.data.imports :as imp]
|
||||
[app.main.data.jobs :as dj]
|
||||
[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.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.jobs.progress :as jp]
|
||||
[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]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- same-file?
|
||||
"Whether an id from a message names a row of the dialog. The ids cross
|
||||
serialization boundaries, so a message can carry the id as a string
|
||||
while the row keeps the uuid (or the other way round): both sides are
|
||||
canonicalized instead of assuming one type."
|
||||
[a b]
|
||||
(= (str a) (str b)))
|
||||
|
||||
(defn- error-hint
|
||||
"What a failed row reports: the message of the failure when it says
|
||||
something useful, or nil. A string error already is the message; a map
|
||||
carries it under `:hint`; anything else is unwrapped with `ex-data`
|
||||
first, which itself can answer a string."
|
||||
[error]
|
||||
(let [data (cond
|
||||
(string? error) error
|
||||
(map? error) error
|
||||
:else (ex-data error))]
|
||||
(some-> (if (string? data) data (:hint data))
|
||||
(str/trim)
|
||||
(not-empty))))
|
||||
|
||||
(defn- mark-file-error
|
||||
[files file-id]
|
||||
(mapv #(cond-> %
|
||||
(= file-id (:id %))
|
||||
(assoc :export-error true
|
||||
:loading false))
|
||||
"The export of a file failed: a terminal state that replaces any
|
||||
other, so a row never reports success and failure at once."
|
||||
[files file-id error]
|
||||
(mapv (fn [file]
|
||||
(if (same-file? file-id (:id file))
|
||||
(-> file
|
||||
(assoc :export-error true
|
||||
:error error
|
||||
:loading false)
|
||||
(dissoc :export-success :size :progress :queued))
|
||||
file))
|
||||
files))
|
||||
|
||||
(defn- mark-file-cancelled
|
||||
"The job of a file was cancelled elsewhere: a neutral terminal state,
|
||||
neither success nor error."
|
||||
neither success nor error, replacing any other."
|
||||
[files file-id]
|
||||
(mapv #(cond-> %
|
||||
(= file-id (:id %))
|
||||
(assoc :export-cancelled true
|
||||
:loading false))
|
||||
(mapv (fn [file]
|
||||
(if (same-file? file-id (:id file))
|
||||
(-> file
|
||||
(assoc :export-cancelled true
|
||||
:loading false)
|
||||
(dissoc :export-success :size :export-error :error
|
||||
:progress :queued))
|
||||
file))
|
||||
files))
|
||||
|
||||
(defn- mark-all-cancelled
|
||||
"Every file still loading is over: its job was just cancelled, or it
|
||||
was never created because the exports run one after another."
|
||||
[files]
|
||||
(mapv (fn [file]
|
||||
(if (:loading file)
|
||||
(-> file
|
||||
(assoc :export-cancelled true
|
||||
:loading false)
|
||||
(dissoc :progress :queued))
|
||||
file))
|
||||
files))
|
||||
|
||||
(defn- mark-file-success
|
||||
[files file-id]
|
||||
(mapv #(cond-> %
|
||||
(= file-id (:id %))
|
||||
(assoc :export-success true
|
||||
:loading false))
|
||||
"The export of a file produced its artifact: a terminal state that
|
||||
replaces any other, so a row never reports success and failure at
|
||||
once."
|
||||
[files file-id size]
|
||||
(mapv (fn [file]
|
||||
(if (same-file? file-id (:id file))
|
||||
(-> file
|
||||
(assoc :export-success true
|
||||
:size size
|
||||
:loading false)
|
||||
(dissoc :export-error :error :progress :queued))
|
||||
file))
|
||||
files))
|
||||
|
||||
(defn- mark-file-progress
|
||||
"The milestone the job of a file is in, to show it while it runs."
|
||||
[files file-id progress]
|
||||
(mapv #(cond-> %
|
||||
(= file-id (:id %))
|
||||
(same-file? file-id (:id %))
|
||||
(assoc :progress progress
|
||||
:queued false))
|
||||
files))
|
||||
@ -68,7 +120,7 @@
|
||||
"The job of a file exists but no worker picked it up yet."
|
||||
[files file-id]
|
||||
(mapv #(cond-> %
|
||||
(= file-id (:id %))
|
||||
(same-file? file-id (:id %))
|
||||
(assoc :queued true))
|
||||
files))
|
||||
|
||||
@ -83,53 +135,114 @@
|
||||
(mf/defc export-entry*
|
||||
{::mf/private true}
|
||||
[{:keys [file]}]
|
||||
(let [level (cond
|
||||
(:export-success file) :success
|
||||
(:export-error file) :error
|
||||
(:export-cancelled file) :warning
|
||||
:else :info)]
|
||||
(let [success? (:export-success file)
|
||||
error? (:export-error file)
|
||||
cancelled? (:export-cancelled file)
|
||||
progress (:progress file)
|
||||
running? (and (:loading file) (some? progress))
|
||||
;; the job of the file exists but no worker picked it up yet: its
|
||||
;; queue only ends with a milestone, so until then the row waits
|
||||
waiting? (and (:loading file) (nil? progress))
|
||||
size (imp/format-file-size (:size file))
|
||||
;; the size of the artifact only exists once the job produced it:
|
||||
;; until then the subtitle only says what it will be
|
||||
subtitle (if (some? size) (dm/str size " .penpot") ".penpot")]
|
||||
[:div {:class (stl/css-case
|
||||
:file-entry true
|
||||
:loading (:loading file)
|
||||
:success (:export-success file)
|
||||
:error (:export-error file))}
|
||||
:success success?
|
||||
:error error?)}
|
||||
|
||||
(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-main)}
|
||||
[:div {:class (stl/css :file-icon)
|
||||
:aria-hidden "true"}
|
||||
[:> icon* {:icon-id (if error? i/close i/folder) :size "m"}]]
|
||||
|
||||
;; while the file is being produced: waiting for a worker, then
|
||||
;; the milestone of the job
|
||||
(when (or (some? (:progress file)) (:queued file))
|
||||
(let [progress (:progress file)]
|
||||
[:> text* {:class (stl/css :status-message)
|
||||
:as "span"
|
||||
:typography (if (some? progress) t/body-large t/body-medium)
|
||||
:role "status"
|
||||
:aria-live "polite"}
|
||||
(if (some? progress)
|
||||
(jp/milestone-text progress)
|
||||
(tr "jobs.queued"))]))]
|
||||
[:div {:class (stl/css :file-meta)}
|
||||
[:> text* {:class (stl/css :file-name-label)
|
||||
:as "span"
|
||||
:typography t/body-medium}
|
||||
(:name file)]
|
||||
[:> text* {:class (stl/css :file-subtitle)
|
||||
:as "span"
|
||||
:typography t/body-small}
|
||||
subtitle]]
|
||||
|
||||
[:*
|
||||
[:> context-notification {:level level
|
||||
:content (:name file)}]
|
||||
;; a row carries its name and what its artifact will weigh, and only
|
||||
;; the state that is its own: while the file runs, the milestone and
|
||||
;; the bar live under it
|
||||
(cond
|
||||
success?
|
||||
[:div {:class (stl/css :file-status)}
|
||||
[:> icon* {:icon-id i/status-tick :size "s"}]
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-medium}
|
||||
(tr "files-export-modal.status.exported")]]
|
||||
|
||||
;; a cancelled file is a neutral terminal state, but the label is
|
||||
;; what tells the user why no artifact came out
|
||||
(when (:export-cancelled file)
|
||||
[:> text* {:class (stl/css :status-message)
|
||||
:as "span"
|
||||
:typography t/body-medium
|
||||
:role "status"
|
||||
:aria-live "polite"}
|
||||
(tr "jobs.export-cancelled")])])]))
|
||||
waiting?
|
||||
[:div {:class (stl/css :file-status)}
|
||||
[:> icon* {:icon-id i/clock :size "s"}]
|
||||
[:> text* {:as "span"
|
||||
:typography t/body-medium}
|
||||
(tr "files-export-modal.status.waiting")]])]
|
||||
|
||||
;; the milestone of the job, with an animated bar: the stages of an
|
||||
;; export are not comparable one with another, so the client cannot
|
||||
;; know a percentage of the file, only that it moves
|
||||
(when running?
|
||||
[:div {:class (stl/css :file-progress)}
|
||||
[:> text* {:class (stl/css :progress-message)
|
||||
:as "span"
|
||||
:typography t/body-small
|
||||
:role "status"
|
||||
:aria-live "polite"}
|
||||
(jp/milestone-text progress)]
|
||||
[:div {:class (stl/css :progress-bar)
|
||||
:role "progressbar"}
|
||||
[:div {:class (stl/css :progress-fill)}]]])
|
||||
|
||||
(when error?
|
||||
[:div {:class (stl/css :file-error)}
|
||||
[:> text* {:class (stl/css :error-message)
|
||||
:as "span"
|
||||
:typography t/body-small}
|
||||
;; the reason is user-facing text already (the hint of the job),
|
||||
;; never a key
|
||||
(tr "files-export-modal.export-error"
|
||||
(or (error-hint (:error file))
|
||||
(tr "labels.error")))]])
|
||||
|
||||
;; a cancelled file is a neutral terminal state, but the label is
|
||||
;; what tells the user why no artifact came out
|
||||
(when cancelled?
|
||||
[:div {:class (stl/css :file-error)}
|
||||
[:> text* {:class (stl/css :status-message)
|
||||
:as "span"
|
||||
:typography t/body-small
|
||||
:role "status"
|
||||
:aria-live "polite"}
|
||||
(tr "jobs.export-cancelled")]])]))
|
||||
|
||||
(mf/defc export-phase-header*
|
||||
{::mf/private true}
|
||||
[{:keys [files]}]
|
||||
(let [total (count files)
|
||||
done (count (filterv :export-success files))
|
||||
;; every file is its own job, so the header only counts them: the
|
||||
;; progress of each one lives in its row, there is no global bar
|
||||
running? (boolean (some :loading files))]
|
||||
[:div {:class (stl/css :phase-header)}
|
||||
[:> heading* {:level 3
|
||||
:typography t/headline-medium
|
||||
:class (stl/css :phase-title)}
|
||||
(if running?
|
||||
(tr "files-export-modal.exporting.title" (i18n/c total))
|
||||
(tr "files-export-modal.exported.title" (i18n/c done)))]
|
||||
[:> text* {:as "p"
|
||||
:typography t/body-medium
|
||||
:class (stl/css :phase-message)}
|
||||
(if running?
|
||||
(tr "files-export-modal.exporting.message")
|
||||
(tr "files-export-modal.exported.message"))]]))
|
||||
|
||||
(mf/defc export-dialog
|
||||
{::mf/register modal/components
|
||||
@ -145,6 +258,10 @@
|
||||
state (deref state*)
|
||||
selected (:selected state)
|
||||
status (:status state)
|
||||
;; files still in flight: the dialog status never leaves
|
||||
;; :exporting, so loading rows are the signal of a running
|
||||
;; export, not the status
|
||||
in-progress? (boolean (->> state :files (some :loading)))
|
||||
|
||||
;; Jobs still running, as file-id -> job-id, to cancel them on
|
||||
;; demand; a file that reaches its outcome leaves the map, so
|
||||
@ -164,7 +281,7 @@
|
||||
(reset! sub* (->> (fexp/export-files :files files :type selected
|
||||
:on-job #(swap! jobs* assoc (:file-id %) (:job-id %)))
|
||||
(rx/subs!
|
||||
(fn [{:keys [file-id error filename uri progress queued
|
||||
(fn [{:keys [file-id error filename uri size progress queued
|
||||
cancelled]}]
|
||||
(cond
|
||||
(some? progress)
|
||||
@ -175,7 +292,7 @@
|
||||
|
||||
(some? error)
|
||||
(swap! jobs* dissoc file-id)
|
||||
(swap! state* update :files mark-file-error file-id)
|
||||
(swap! state* update :files mark-file-error file-id error)
|
||||
|
||||
(true? cancelled)
|
||||
(swap! jobs* dissoc file-id)
|
||||
@ -186,35 +303,52 @@
|
||||
(some? uri)
|
||||
(do
|
||||
(swap! jobs* dissoc file-id)
|
||||
(swap! state* update :files mark-file-success file-id)
|
||||
(swap! state* update :files mark-file-success file-id size)
|
||||
(dom/trigger-download-uri filename "application/penpot" uri)))))))))
|
||||
|
||||
on-cancel-export
|
||||
on-stop-jobs
|
||||
(mf/use-fn
|
||||
(mf/deps jobs sub)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
;; stop listening first, so no late message repaints the
|
||||
;; entries of a dialog that is going away
|
||||
(fn []
|
||||
;; stop listening first, so no late message repaints the rows
|
||||
(when (some? sub)
|
||||
(rx/dispose! sub))
|
||||
;; only the jobs still in flight are cancelled: finished ones
|
||||
;; already left the map, and failures are ignored because a job may have just finished on its own
|
||||
(run! dj/cancel-job (vals jobs))
|
||||
(reset! jobs* {})
|
||||
(reset! sub* nil)
|
||||
(reset! sub* nil)))
|
||||
|
||||
on-cancel-export
|
||||
(mf/use-fn
|
||||
(mf/deps on-stop-jobs state*)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(on-stop-jobs)
|
||||
;; the dialog stays open with the terminal states: closing it
|
||||
;; is what dismisses it, cancelling whatever is still pending
|
||||
(swap! state* update :files mark-all-cancelled)))
|
||||
|
||||
on-close
|
||||
(mf/use-fn
|
||||
(mf/deps on-stop-jobs)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
;; closing dismisses the dialog, cancelling whatever is still
|
||||
;; pending, so an export never outlives its dialog
|
||||
(on-stop-jobs)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
on-cancel
|
||||
(mf/use-fn
|
||||
(mf/deps status on-cancel-export)
|
||||
(mf/deps status in-progress? on-cancel-export on-close)
|
||||
(fn [event]
|
||||
(if (= :exporting status)
|
||||
;; closing mid-export stops the jobs, like Cancel
|
||||
;; the X cancels like Cancel Export while files are still in
|
||||
;; flight, and dismisses like Done once everything reached a
|
||||
;; terminal state
|
||||
(if (and (= :exporting status) in-progress?)
|
||||
(on-cancel-export event)
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (modal/hide))))))
|
||||
(on-close event))))
|
||||
|
||||
on-accept
|
||||
(mf/use-fn
|
||||
@ -243,7 +377,7 @@
|
||||
[:> heading* {:level 2
|
||||
:typography t/headline-large
|
||||
:class (stl/css :modal-title)}
|
||||
(tr "files-download-modal.title")]
|
||||
(tr "files-export-modal.title")]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click on-cancel
|
||||
@ -308,7 +442,7 @@
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
[:> button* {:variant "secondary"
|
||||
:type "button"
|
||||
:on-click on-cancel}
|
||||
:on-click on-close}
|
||||
(tr "labels.cancel")]
|
||||
|
||||
[:> button* {:variant "primary"
|
||||
@ -317,21 +451,20 @@
|
||||
(tr "labels.continue")]]]]
|
||||
|
||||
(= status :exporting)
|
||||
(let [in-progress? (->> state :files (some :loading))]
|
||||
[:*
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
(for [file (:files state)]
|
||||
[:> export-entry* {:file file :key (dm/str (:id file))}])]
|
||||
[:*
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:> export-phase-header* {:files (:files state)}]
|
||||
(for [file (:files state)]
|
||||
[:> export-entry* {:file file :key (dm/str (:id file))}])]
|
||||
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
(when in-progress?
|
||||
[:> button* {:variant "secondary"
|
||||
:type "button"
|
||||
:on-click on-cancel-export}
|
||||
(tr "labels.cancel")])
|
||||
[:div {:class (stl/css :modal-footer)}
|
||||
[:div {:class (stl/css :action-buttons)}
|
||||
(if in-progress?
|
||||
[:> button* {:variant "secondary"
|
||||
:type "button"
|
||||
:on-click on-cancel-export}
|
||||
(tr "files-export-modal.cancel-export")]
|
||||
[:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:disabled in-progress?
|
||||
:on-click on-cancel}
|
||||
(tr "labels.close")]]]]))]]))
|
||||
:on-click on-close}
|
||||
(tr "files-export-modal.done")])]]])]]))
|
||||
|
||||
@ -48,6 +48,12 @@
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
margin-block-end: var(--sp-xxl);
|
||||
|
||||
// like the import wizard: the rows scroll and the header and footer
|
||||
// stay put, instead of the content pushing the footer out of the box
|
||||
flex: 1;
|
||||
min-block-size: 0;
|
||||
overflow: hidden auto;
|
||||
}
|
||||
|
||||
.modal-content-extended {
|
||||
@ -153,60 +159,153 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.file-entry {
|
||||
--file-entry-color: var(--color-foreground-secondary);
|
||||
.phase-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
&.loading {
|
||||
.file-name {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
.phase-title {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.phase-message {
|
||||
color: var(--color-foreground-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.file-entry {
|
||||
--file-entry-fg-color: var(--color-foreground-secondary);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
|
||||
&.success {
|
||||
--file-entry-fg-color: var(--color-accent-success);
|
||||
}
|
||||
|
||||
&.error {
|
||||
.file-name {
|
||||
color: var(--color-foreground-secondary);
|
||||
--file-entry-fg-color: var(--color-accent-error);
|
||||
|
||||
.file-icon svg {
|
||||
stroke: var(--color-foreground-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.success {
|
||||
.file-name {
|
||||
color: var(--color-foreground-secondary);
|
||||
|
||||
.file-icon svg {
|
||||
stroke: var(--color-foreground-secondary);
|
||||
}
|
||||
}
|
||||
padding: var(--sp-m);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-error);
|
||||
}
|
||||
}
|
||||
|
||||
.file-name {
|
||||
.file-main {
|
||||
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-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
block-size: px2rem(40);
|
||||
inline-size: px2rem(40);
|
||||
flex-shrink: 0;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
color: var(--color-foreground-secondary);
|
||||
|
||||
.file-name-label {
|
||||
display: block;
|
||||
max-inline-size: 99%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
.error & {
|
||||
background-color: var(--color-accent-error);
|
||||
color: var(--color-background-error);
|
||||
}
|
||||
}
|
||||
|
||||
.icon-status-tick {
|
||||
fill: none;
|
||||
stroke: var(--color-accent-primary);
|
||||
.file-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xxs);
|
||||
flex: 1 1 auto;
|
||||
min-inline-size: 0;
|
||||
|
||||
.error & {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.file-name-label {
|
||||
display: block;
|
||||
max-inline-size: 99%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.file-subtitle {
|
||||
color: var(--color-foreground-secondary);
|
||||
|
||||
.error & {
|
||||
color: var(--file-entry-fg-color);
|
||||
}
|
||||
}
|
||||
|
||||
.file-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-s);
|
||||
flex-shrink: 0;
|
||||
color: var(--file-entry-fg-color);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.progress-message {
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
block-size: px2rem(4);
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-background-tertiary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
block-size: 100%;
|
||||
border-radius: $br-8;
|
||||
background-color: var(--color-accent-primary);
|
||||
|
||||
// the twin of the indeterminate bar of the import wizard: the stages
|
||||
// of an export are not comparable one with another, so the fill slides
|
||||
// through the bar while the job of the file runs
|
||||
width: 40%;
|
||||
animation: progress-slide 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes progress-slide {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(250%);
|
||||
}
|
||||
}
|
||||
|
||||
.file-error {
|
||||
padding-inline-start: calc(px2rem(40) + var(--sp-m));
|
||||
|
||||
.error & {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
color: var(--file-entry-fg-color);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@ -2153,9 +2153,37 @@ msgstr "Idea"
|
||||
msgid "feedback.type.issue"
|
||||
msgstr "Issue"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs:140
|
||||
msgid "files-download-modal.title"
|
||||
msgstr "Download files"
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.cancel-export"
|
||||
msgstr "Cancel export"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.done"
|
||||
msgstr "Done"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.export-error"
|
||||
msgstr "Couldn't export: %s"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.exported.message"
|
||||
msgstr "Check your downloads folder."
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.exported.title"
|
||||
msgid_plural "files-export-modal.exported.title"
|
||||
msgstr[0] "1 file exported"
|
||||
msgstr[1] "%s files exported"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.exporting.message"
|
||||
msgstr "Each file downloads as soon as it's ready."
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.exporting.title"
|
||||
msgid_plural "files-export-modal.exporting.title"
|
||||
msgstr[0] "Exporting 1 file"
|
||||
msgstr[1] "Exporting %s files"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs:183
|
||||
msgid "files-export-modal.options.detach-libraries.message"
|
||||
@ -2197,6 +2225,18 @@ msgstr "Embed library assets in the file"
|
||||
msgid "files-download-modal.unsaved-changes"
|
||||
msgstr "Some of your latest changes are not saved yet, so this download will not include them."
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.status.exported"
|
||||
msgstr "Exported"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.status.waiting"
|
||||
msgstr "Waiting"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.title"
|
||||
msgstr "Export file"
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs:45
|
||||
msgid "generic.error"
|
||||
msgstr "An error has occurred"
|
||||
|
||||
@ -728,12 +728,6 @@ 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."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs
|
||||
msgid "dashboard.import.importing.title"
|
||||
msgid_plural "dashboard.import.importing.title"
|
||||
msgstr[0] "Importando 1 archivo"
|
||||
msgstr[1] "Importando %s archivos"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs
|
||||
msgid "dashboard.import.importing.done"
|
||||
msgid_plural "dashboard.import.importing.done"
|
||||
@ -744,6 +738,12 @@ msgstr[1] "%s hechos"
|
||||
msgid "dashboard.import.importing.message"
|
||||
msgstr "Reconstruyendo páginas, componentes y recursos en tu proyecto."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs
|
||||
msgid "dashboard.import.importing.title"
|
||||
msgid_plural "dashboard.import.importing.title"
|
||||
msgstr[0] "Importando 1 archivo"
|
||||
msgstr[1] "Importando %s archivos"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs:710
|
||||
msgid "dashboard.import.next-file"
|
||||
msgstr "Siguiente archivo"
|
||||
@ -752,15 +752,15 @@ msgstr "Siguiente archivo"
|
||||
msgid "dashboard.import.no-perms"
|
||||
msgstr "No tienes permisos para importar en este equipo"
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs
|
||||
msgid "dashboard.import.queued.title"
|
||||
msgstr "Esperando a empezar..."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs
|
||||
msgid "dashboard.import.queued.message"
|
||||
msgstr ""
|
||||
"Tus archivos ya están subidos. La importación empieza en cuanto haya un hueco "
|
||||
"libre."
|
||||
"Tus archivos ya están subidos. La importación empieza en cuanto haya un "
|
||||
"hueco libre."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs
|
||||
msgid "dashboard.import.queued.title"
|
||||
msgstr "Esperando a empezar..."
|
||||
|
||||
#: src/app/main/ui/dashboard/import.cljs
|
||||
msgid "dashboard.import.ready.message"
|
||||
@ -2193,9 +2193,37 @@ msgstr "Idea"
|
||||
msgid "feedback.type.issue"
|
||||
msgstr "Problema"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs:140
|
||||
msgid "files-download-modal.title"
|
||||
msgstr "Descargar ficheros"
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.cancel-export"
|
||||
msgstr "Cancelar exportación"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.done"
|
||||
msgstr "Listo"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.export-error"
|
||||
msgstr "No se ha podido exportar: %s"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.exported.message"
|
||||
msgstr "Revisa tu carpeta de descargas."
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.exported.title"
|
||||
msgid_plural "files-export-modal.exported.title"
|
||||
msgstr[0] "1 archivo exportado"
|
||||
msgstr[1] "%s archivos exportados"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.exporting.message"
|
||||
msgstr "Cada archivo se descarga en cuanto está listo."
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.exporting.title"
|
||||
msgid_plural "files-export-modal.exporting.title"
|
||||
msgstr[0] "Exportando 1 archivo"
|
||||
msgstr[1] "Exportando %s archivos"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs:183
|
||||
msgid "files-export-modal.options.detach-libraries.message"
|
||||
@ -2239,6 +2267,18 @@ msgstr "Incluir librerias compartidas dentro de las librerias del fichero"
|
||||
msgid "files-download-modal.unsaved-changes"
|
||||
msgstr "Algunos de tus últimos cambios aún no se han guardado, así que esta descarga no los incluirá."
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.status.exported"
|
||||
msgstr "Exportado"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.status.waiting"
|
||||
msgstr "En espera"
|
||||
|
||||
#: src/app/main/ui/exports/files.cljs
|
||||
msgid "files-export-modal.title"
|
||||
msgstr "Exportar archivo"
|
||||
|
||||
#: src/app/main/ui/settings/password.cljs:45
|
||||
msgid "generic.error"
|
||||
msgstr "Ha ocurrido un error"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user