mirror of
https://github.com/penpot/penpot.git
synced 2025-04-04 11:01:20 -05:00
✨ Import/export UI and final touches
This commit is contained in:
parent
1b1c0ff9e4
commit
d0ab813520
12 changed files with 856 additions and 117 deletions
|
@ -202,12 +202,338 @@
|
|||
background: $color-primary;
|
||||
border: 1px solid $color-primary;
|
||||
color: $color-black;
|
||||
|
||||
&:hover {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.import-dialog,
|
||||
.export-dialog {
|
||||
background-color: $color-white;
|
||||
border: 1px solid $color-gray-20;
|
||||
width: 30rem;
|
||||
|
||||
p {
|
||||
font-size: $fs14;
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
.detail {
|
||||
font-size: $fs12;
|
||||
}
|
||||
|
||||
.detail, .explain {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
border: 1px solid $color-gray-20;
|
||||
background: $color-white;
|
||||
border-radius: 3px;
|
||||
padding: 0.3rem 1.25rem;
|
||||
cursor: pointer;
|
||||
margin-right: 8px;
|
||||
|
||||
&:hover {
|
||||
background: $color-gray-20;
|
||||
}
|
||||
}
|
||||
|
||||
.accept-button {
|
||||
background: $color-primary;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $color-primary;
|
||||
color: $color-black;
|
||||
cursor: pointer;
|
||||
padding: 0.3rem 1.25rem;
|
||||
|
||||
&[disabled] {
|
||||
border: 1px solid #E3E3E3;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $color-primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
max-height: calc(65vh);
|
||||
}
|
||||
|
||||
.modal-header-title {
|
||||
padding-left: 2rem;
|
||||
|
||||
h2 {
|
||||
font-size: $fs14;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.import-dialog {
|
||||
min-height: 215px;
|
||||
|
||||
svg {
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
}
|
||||
|
||||
.file-entry {
|
||||
margin: 0.75rem 1rem;
|
||||
user-select: none;
|
||||
|
||||
&.editable:hover {
|
||||
.file-name-label {
|
||||
background-color: $color-primary-lighter;
|
||||
}
|
||||
.edit-entry-buttons {
|
||||
display: flex;
|
||||
background-color: $color-primary-lighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 1rem;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
#loader-pencil {
|
||||
fill: $color-black;
|
||||
}
|
||||
|
||||
.icon-tick {
|
||||
fill: $color-success;
|
||||
}
|
||||
|
||||
.icon-close {
|
||||
transform: rotate(45deg);
|
||||
fill: $color-danger;
|
||||
}
|
||||
}
|
||||
|
||||
.file-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $color-black;
|
||||
|
||||
.file-name-label {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 2rem;
|
||||
margin-left: -0.25rem;
|
||||
padding-left: 0.25rem;
|
||||
|
||||
.icon-library {
|
||||
width: 14px;
|
||||
fill: $color-gray-20;
|
||||
margin-left: 0.5rem;
|
||||
padding-top: 1px
|
||||
}
|
||||
}
|
||||
|
||||
.file-name-edit {
|
||||
width: 100%;
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-bottom: 1px solid $color-gray-20;
|
||||
height: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.feedback-banner {
|
||||
color: $color-black;
|
||||
background: $color-success-lighter;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 1rem;
|
||||
|
||||
.message {
|
||||
padding: 0 1rem;
|
||||
font-size: $fs12;
|
||||
}
|
||||
|
||||
.icon {
|
||||
background: $color-success;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: $color-white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: 0 2rem;
|
||||
color: $color-danger;
|
||||
font-size: $fs12;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.linked-libraries {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-left: 2rem;
|
||||
|
||||
.icon-chain, .icon-unchain {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.linked-library-tag {
|
||||
font-size: $fs10;
|
||||
color: $color-black;
|
||||
background: #d8f7fe;
|
||||
border-radius: 3px;
|
||||
padding: 2px 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0.25rem;
|
||||
|
||||
&.error {
|
||||
background-color: $color-danger-lighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.edit-entry-buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-size: $fs14;
|
||||
height: 2rem;
|
||||
display: none;
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
&:hover svg {
|
||||
fill: $color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.export-dialog {
|
||||
min-height: 24rem;
|
||||
|
||||
.export-option {
|
||||
border-radius: 4px;
|
||||
border: 1px solid $color-gray-10;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
h3 {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h3, p {
|
||||
font-size: $fs12;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
color: $color-black;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: 1px solid $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.option-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
padding-left: 40px;
|
||||
padding-right: 1rem;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
// margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.option-radio-check {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
left: 12px;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
background-color: $color-white;
|
||||
border: 1px solid $color-gray-10;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
&:hover input ~ .option-radio-check {
|
||||
border-color: $color-primary;
|
||||
}
|
||||
|
||||
input:checked ~ .option-radio-check {
|
||||
border-color: $color-primary;
|
||||
background-color: $color-white;
|
||||
}
|
||||
|
||||
.option-radio-check:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
|
||||
input:checked ~ .option-radio-check:after {
|
||||
display: block;
|
||||
background-color: $color-primary;
|
||||
}
|
||||
|
||||
.option-radio-check:after {
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.libraries-dialog {
|
||||
|
@ -564,4 +890,3 @@
|
|||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,8 +13,10 @@
|
|||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.dashboard.export]
|
||||
[app.main.ui.dashboard.files :refer [files-section]]
|
||||
[app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]]
|
||||
[app.main.ui.dashboard.import]
|
||||
[app.main.ui.dashboard.libraries :refer [libraries-page]]
|
||||
[app.main.ui.dashboard.projects :refer [projects-section]]
|
||||
[app.main.ui.dashboard.search :refer [search-page]]
|
||||
|
@ -131,4 +133,3 @@
|
|||
:section section
|
||||
:search-term search-term
|
||||
:team team}])])]]))
|
||||
|
||||
|
|
92
frontend/src/app/main/ui/dashboard/export.cljs
Normal file
92
frontend/src/app/main/ui/dashboard/export.cljs
Normal file
|
@ -0,0 +1,92 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.dashboard.export
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(def ^:const options [:all :merge :detach])
|
||||
|
||||
(mf/defc export-dialog
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :export}
|
||||
[{:keys [team-id files]}]
|
||||
(let [selected-option (mf/use-state :all)
|
||||
|
||||
cancel-fn
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
accept-fn
|
||||
(mf/use-callback
|
||||
(mf/deps @selected-option)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
|
||||
(->> (uw/ask-many!
|
||||
{:cmd :export-file
|
||||
:team-id team-id
|
||||
:export-type @selected-option
|
||||
:files files})
|
||||
(rx/subs
|
||||
(fn [msg]
|
||||
(when (= :finish (:type msg))
|
||||
(dom/trigger-download-uri (:filename msg) (:mtype msg) (:uri msg))))))
|
||||
|
||||
(st/emit! (modal/hide))))
|
||||
|
||||
on-change-handler
|
||||
(mf/use-callback
|
||||
(fn [_ type]
|
||||
(reset! selected-option type)))]
|
||||
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.export-dialog
|
||||
[:div.modal-header
|
||||
[:div.modal-header-title
|
||||
[:h2 (tr "dashboard.export.title")]]
|
||||
|
||||
[:div.modal-close-button
|
||||
{:on-click cancel-fn} i/close]]
|
||||
|
||||
[:div.modal-content
|
||||
[:p.explain (tr "dashboard.export.explain")]
|
||||
[:p.detail (tr "dashboard.export.detail")]
|
||||
|
||||
(for [type [:all :merge :detach]]
|
||||
(let [selected? (= @selected-option type)]
|
||||
[:div.export-option {:class (when selected? "selected")}
|
||||
[:label.option-container
|
||||
[:h3 (tr (str "dashboard.export.options." (d/name type) ".title"))]
|
||||
[:p (tr (str "dashboard.export.options." (d/name type) ".message"))]
|
||||
[:input {:type "radio"
|
||||
:checked selected?
|
||||
:on-change #(on-change-handler % type)
|
||||
:name "export-option"}]
|
||||
[:span {:class "option-radio-check"}]]]))]
|
||||
|
||||
[:div.modal-footer
|
||||
[:div.action-buttons
|
||||
[:input.cancel-button
|
||||
{:type "button"
|
||||
:value (tr "labels.cancel")
|
||||
:on-click cancel-fn}]
|
||||
|
||||
[:input.accept-button
|
||||
{:class "primary"
|
||||
:type "button"
|
||||
:value (tr "labels.export")
|
||||
:on-click accept-fn}]]]]]))
|
|
@ -13,8 +13,6 @@
|
|||
[app.main.store :as st]
|
||||
[app.main.ui.components.context-menu :refer [context-menu]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.debug :as d]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
|
@ -158,18 +156,11 @@
|
|||
|
||||
on-export-files
|
||||
(fn [_]
|
||||
(->> (uw/ask-many!
|
||||
{:cmd :export-file
|
||||
:team-id current-team-id
|
||||
:files (->> files (mapv :id))})
|
||||
(rx/subs
|
||||
(fn [msg]
|
||||
(case (:type msg)
|
||||
:progress
|
||||
(prn "[Progress]" (:data msg))
|
||||
|
||||
:finish
|
||||
(dom/trigger-download-uri (:filename msg) (:mtype msg) (:uri msg)))))))]
|
||||
(st/emit!
|
||||
(modal/show
|
||||
{:type :export
|
||||
:team-id current-team-id
|
||||
:files (->> files (mapv :id))})))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
|
@ -195,8 +186,7 @@
|
|||
[[(tr "dashboard.duplicate-multi" file-count) on-duplicate]
|
||||
(when (or (seq current-projects) (seq other-teams))
|
||||
[(tr "dashboard.move-to-multi" file-count) nil sub-options])
|
||||
(when (d/debug? :export)
|
||||
[(tr "dashboard.export-multi" file-count) on-export-files])
|
||||
[(tr "dashboard.export-multi" file-count) on-export-files]
|
||||
[:separator]
|
||||
[(tr "labels.delete-multi-files" file-count) on-delete]]
|
||||
|
||||
|
@ -208,8 +198,7 @@
|
|||
(if (:is-shared file)
|
||||
[(tr "dashboard.remove-shared") on-del-shared]
|
||||
[(tr "dashboard.add-shared") on-add-shared])
|
||||
(when (d/debug? :export)
|
||||
[(tr "dashboard.export-single") on-export-files])
|
||||
[(tr "dashboard.export-single") on-export-files]
|
||||
[:separator]
|
||||
[(tr "labels.delete") on-delete]])]
|
||||
|
||||
|
|
|
@ -6,37 +6,41 @@
|
|||
|
||||
(ns app.main.ui.dashboard.import
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.worker :as uw]
|
||||
[app.util.data :refer [classnames]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.logging :as log]
|
||||
[beicon.core :as rx]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(log/set-level! :debug)
|
||||
|
||||
(defn rx-delay-emit [ms ob]
|
||||
(->> ob (rx/mapcat #(rx/delay ms (rx/of %)))))
|
||||
|
||||
(defn use-import-file
|
||||
[project-id on-finish-import]
|
||||
(mf/use-callback
|
||||
(mf/deps project-id on-finish-import)
|
||||
(fn [files]
|
||||
(when files
|
||||
(let [files (->> files (mapv dom/create-uri))]
|
||||
(->> (uw/ask-many!
|
||||
{:cmd :import-file
|
||||
:project-id project-id
|
||||
:files files})
|
||||
|
||||
(rx/subs
|
||||
(fn [result]
|
||||
(log/debug :action "import-result" :result result))
|
||||
|
||||
(fn [err]
|
||||
(log/debug :action "import-error" :result err))
|
||||
|
||||
(fn []
|
||||
(log/debug :action "import-end")
|
||||
(when on-finish-import (on-finish-import))))))))))
|
||||
(let [files (->> files
|
||||
(mapv
|
||||
(fn [file]
|
||||
{:name (.-name file)
|
||||
:uri (dom/create-uri file)})))]
|
||||
(st/emit! (modal/show
|
||||
{:type :import
|
||||
:project-id project-id
|
||||
:files files
|
||||
:on-finish-import on-finish-import})))))))
|
||||
|
||||
(mf/defc import-form
|
||||
{::mf/forward-ref true}
|
||||
|
@ -49,6 +53,264 @@
|
|||
:ref external-ref
|
||||
:on-selected on-file-selected}]]))
|
||||
|
||||
(defn update-file [files file-id new-name]
|
||||
(->> files
|
||||
(mapv
|
||||
(fn [file]
|
||||
(cond-> file
|
||||
(= (:file-id file) file-id)
|
||||
(assoc :name new-name))))))
|
||||
|
||||
(defn remove-file [files file-id]
|
||||
(->> files
|
||||
(mapv
|
||||
(fn [file]
|
||||
(cond-> file
|
||||
(= (:file-id file) file-id)
|
||||
(assoc :deleted? true))))))
|
||||
|
||||
(defn set-analyze-error
|
||||
[files uri]
|
||||
(->> files
|
||||
(mapv (fn [file]
|
||||
(cond-> file
|
||||
(= uri (:uri file))
|
||||
(assoc :status :analyze-error))))))
|
||||
|
||||
(defn set-analyze-result [files uri data]
|
||||
(let [exiting-files? (into #{} (->> files (map :file-id) (filter some?)))
|
||||
replace-file
|
||||
(fn [file]
|
||||
(if (and (= uri (:uri file) )
|
||||
(= (:status file) :analyzing))
|
||||
(->> (:files data)
|
||||
(remove (comp exiting-files? first) )
|
||||
(mapv (fn [[file-id file-data]]
|
||||
(-> file-data
|
||||
(assoc :file-id file-id
|
||||
:status :ready
|
||||
:uri uri)))))
|
||||
[file]))]
|
||||
(into [] (mapcat replace-file) files)))
|
||||
|
||||
(defn mark-files-importing [files]
|
||||
(->> files
|
||||
(filter #(= :ready (:status %)))
|
||||
(mapv #(assoc % :status :importing))))
|
||||
|
||||
(defn update-status [files file-id status]
|
||||
(->> files
|
||||
(mapv (fn [file]
|
||||
(cond-> file
|
||||
(= file-id (:file-id file))
|
||||
(assoc :status status))))))
|
||||
|
||||
(mf/defc import-entry
|
||||
[{:keys [state file editing?]}]
|
||||
|
||||
(let [loading? (or (= :analyzing (:status file))
|
||||
(= :importing (:status file)))
|
||||
load-success? (= :import-success (:status file))
|
||||
analyze-error? (= :analyze-error (:status file))
|
||||
import-error? (= :import-error (:status file))
|
||||
ready? (= :ready (:status file))
|
||||
is-shared? (:shared file)
|
||||
|
||||
handle-edit-key-press
|
||||
(mf/use-callback
|
||||
(fn [e]
|
||||
(when (or (kbd/enter? e) (kbd/esc? e))
|
||||
(dom/prevent-default e)
|
||||
(dom/stop-propagation e)
|
||||
(dom/blur! (dom/get-target e)))))
|
||||
|
||||
handle-edit-blur
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn [e]
|
||||
(let [value (dom/get-target-val e)]
|
||||
(swap! state #(-> (assoc % :editing nil)
|
||||
(update :files update-file (:file-id file) value))))))
|
||||
|
||||
handle-edit-entry
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn []
|
||||
(swap! state assoc :editing (:file-id file))))
|
||||
|
||||
handle-remove-entry
|
||||
(mf/use-callback
|
||||
(mf/deps file)
|
||||
(fn []
|
||||
(swap! state update :files remove-file (:file-id file))))]
|
||||
|
||||
[:div.file-entry
|
||||
{:class (classnames :loading loading?
|
||||
:success load-success?
|
||||
:error (or import-error? analyze-error?)
|
||||
:editable (and ready? (not editing?)))}
|
||||
|
||||
[:div.file-name
|
||||
[:div.file-icon
|
||||
(cond loading? i/loader-pencil
|
||||
ready? i/logo-icon
|
||||
load-success? i/tick
|
||||
import-error? i/close
|
||||
analyze-error? i/close)]
|
||||
|
||||
(if editing?
|
||||
[:div.file-name-edit
|
||||
[:input {:type "text"
|
||||
:auto-focus true
|
||||
:default-value (:name file)
|
||||
:on-key-press handle-edit-key-press
|
||||
:on-blur handle-edit-blur}]]
|
||||
|
||||
[:div.file-name-label (:name file) (when is-shared? i/library)])
|
||||
|
||||
[:div.edit-entry-buttons
|
||||
[:button {:on-click handle-edit-entry} i/pencil]
|
||||
[:button {:on-click handle-remove-entry} i/trash]]]
|
||||
|
||||
(when analyze-error?
|
||||
[:div.error-message
|
||||
(tr "dashboard.import.analyze-error")])
|
||||
|
||||
(when import-error?
|
||||
[:div.error-message
|
||||
(tr "dashboard.import.import-error")])
|
||||
|
||||
[:div.linked-libraries
|
||||
(for [library-id (:libraries file)]
|
||||
(let [library-data (->> @state :files (d/seek #(= library-id (:file-id %))))
|
||||
error? (or (:deleted? library-data) (:import-error library-data))]
|
||||
(when (some? library-data)
|
||||
[:div.linked-library-tag {:class (when error? "error")}
|
||||
(if error? i/unchain i/chain) (:name library-data)])))]]))
|
||||
|
||||
(mf/defc import-dialog
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :import}
|
||||
[{:keys [project-id files on-finish-import]}]
|
||||
(let [state (mf/use-state
|
||||
{:status :analyzing
|
||||
:editing nil
|
||||
:files (->> files
|
||||
(mapv #(assoc % :status :analyzing)))})
|
||||
|
||||
analyze-import
|
||||
(mf/use-callback
|
||||
(fn [files]
|
||||
(->> (uw/ask-many!
|
||||
{:cmd :analyze-import
|
||||
:files (->> files (mapv :uri))})
|
||||
(rx-delay-emit 1000)
|
||||
(rx/subs
|
||||
(fn [{:keys [uri data error] :as msg}]
|
||||
(log/debug :msg msg)
|
||||
(if (some? error)
|
||||
(swap! state update :files set-analyze-error uri)
|
||||
(swap! state update :files set-analyze-result uri data)))))))
|
||||
|
||||
import-files
|
||||
(mf/use-callback
|
||||
(fn [project-id files]
|
||||
(->> (uw/ask-many!
|
||||
{:cmd :import-files
|
||||
:project-id project-id
|
||||
:files files})
|
||||
(rx-delay-emit 1000)
|
||||
(rx/subs
|
||||
(fn [{:keys [file-id status] :as msg}]
|
||||
(log/debug :msg msg)
|
||||
(swap! state update :files update-status file-id status))))))
|
||||
|
||||
handle-cancel
|
||||
(mf/use-callback
|
||||
(mf/deps (:editing @state))
|
||||
(fn [event]
|
||||
(when (nil? (:editing @state))
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (modal/hide)))))
|
||||
|
||||
handle-continue
|
||||
(mf/use-callback
|
||||
(mf/deps project-id (:files @state))
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(let [files (->> @state :files (filterv #(= :ready (:status %))))]
|
||||
(import-files project-id files))
|
||||
|
||||
(swap! state
|
||||
(fn [state]
|
||||
(-> state
|
||||
(assoc :status :importing)
|
||||
(update :files mark-files-importing))))))
|
||||
|
||||
handle-accept
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(st/emit! (modal/hide))
|
||||
(when on-finish-import (on-finish-import))))
|
||||
|
||||
success-files (->> @state :files (filter #(= (:status %) :import-success)) count)
|
||||
pending-analysis? (> (->> @state :files (filter #(= (:status %) :analyzing)) count) 0)
|
||||
pending-import? (> (->> @state :files (filter #(= (:status %) :importing)) count) 0)]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [sub (analyze-import files)]
|
||||
#(rx/dispose! sub))))
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
;; dispose uris when the component is umount
|
||||
#(doseq [file files]
|
||||
(dom/revoke-uri (:uri file)))))
|
||||
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.import-dialog
|
||||
[:div.modal-header
|
||||
[:div.modal-header-title
|
||||
[:h2 (tr "dashboard.import")]]
|
||||
|
||||
[:div.modal-close-button
|
||||
{:on-click handle-cancel} i/close]]
|
||||
|
||||
[:div.modal-content
|
||||
(when (and (= :importing (:status @state))
|
||||
(not pending-import?))
|
||||
[:div.feedback-banner
|
||||
[:div.icon i/checkbox-checked]
|
||||
[:div.message (tr "dashboard.import.import-message" success-files)]])
|
||||
|
||||
(for [file (->> (:files @state) (filterv (comp not :deleted?)))]
|
||||
(let [editing? (and (some? (:file-id file))
|
||||
(= (:file-id file) (:editing @state)))]
|
||||
[:& import-entry {:state state
|
||||
:file file
|
||||
:editing? editing?}]))]
|
||||
|
||||
[:div.modal-footer
|
||||
[:div.action-buttons
|
||||
[:input.cancel-button
|
||||
{:type "button"
|
||||
:value (tr "labels.cancel")
|
||||
:on-click handle-cancel}]
|
||||
|
||||
(when (= :analyzing (:status @state))
|
||||
[:input.accept-button
|
||||
{:class "primary"
|
||||
:type "button"
|
||||
:value (tr "labels.continue")
|
||||
:disabled pending-analysis?
|
||||
:on-click handle-continue}])
|
||||
|
||||
(when (= :importing (:status @state))
|
||||
[:input.accept-button
|
||||
{:class "primary"
|
||||
:type "button"
|
||||
:value (tr "labels.accept")
|
||||
:disabled pending-import?
|
||||
:on-click handle-accept}])]]]]))
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
[app.main.ui.components.context-menu :refer [context-menu]]
|
||||
[app.main.ui.context :as ctx]
|
||||
[app.main.ui.dashboard.import :as udi]
|
||||
[app.util.debug :as d]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.router :as rt]
|
||||
|
@ -107,8 +106,7 @@
|
|||
[(tr "dashboard.move-to") nil
|
||||
(for [team teams]
|
||||
[(:name team) (on-move (:id team))])])
|
||||
(when (d/debug? :import)
|
||||
[(tr "dashboard.import") on-import-files])
|
||||
[(tr "dashboard.import") on-import-files]
|
||||
[:separator]
|
||||
[(tr "labels.delete") on-delete]]}]]))
|
||||
|
||||
|
|
|
@ -9,8 +9,9 @@
|
|||
|
||||
(defmacro icon-xref
|
||||
[id]
|
||||
(let [href (str "#icon-" (name id))]
|
||||
(let [href (str "#icon-" (name id))
|
||||
class (str "icon-" (name id))]
|
||||
`(rumext.alpha/html
|
||||
[:svg {:width 500 :height 500}
|
||||
[:svg {:width 500 :height 500 :class ~class}
|
||||
[:use {:xlinkHref ~href}]])))
|
||||
|
||||
|
|
|
@ -15,4 +15,4 @@
|
|||
(mf/defc loader
|
||||
[]
|
||||
(when (mf/deref st/loader)
|
||||
[:div.loader-content i/loader]))
|
||||
[:div.loader-content i/loader-pencil]))
|
||||
|
|
|
@ -213,6 +213,10 @@
|
|||
[node]
|
||||
(.focus node))
|
||||
|
||||
(defn blur!
|
||||
[node]
|
||||
(.blur node))
|
||||
|
||||
(defn fullscreen?
|
||||
[]
|
||||
(cond
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
(defn create-manifest
|
||||
"Creates a manifest entry for the given files"
|
||||
[team-id file-id files]
|
||||
[team-id file-id export-type files]
|
||||
(letfn [(format-page [manifest page]
|
||||
(-> manifest
|
||||
(assoc (str (:id page))
|
||||
|
@ -47,6 +47,7 @@
|
|||
:pages pages
|
||||
:pagesIndex index
|
||||
:libraries (->> (:libraries file) (into #{}) (mapv str))
|
||||
:exportType (d/name export-type)
|
||||
:hasComponents (d/not-empty? (get-in file [:data :components]))
|
||||
:hasMedia (d/not-empty? (get-in file [:data :media]))
|
||||
:hasColors (d/not-empty? (get-in file [:data :colors]))
|
||||
|
@ -158,8 +159,36 @@
|
|||
(-> file
|
||||
(assoc :libraries libraries-ids)))))))
|
||||
|
||||
(defn merge-assets [target-file assets-files]
|
||||
(let [merge-file-assets
|
||||
(fn [target file]
|
||||
(-> target
|
||||
(update-in [:data :colors] merge (get-in file [:data :colors]))
|
||||
(update-in [:data :typographies] merge (get-in file [:data :typographies]))
|
||||
(update-in [:data :media] merge (get-in file [:data :media]))
|
||||
(update-in [:data :components] merge (get-in file [:data :components]))))]
|
||||
|
||||
(->> assets-files
|
||||
(reduce merge-file-assets target-file))))
|
||||
|
||||
(defn detach-libraries
|
||||
[files file-id]
|
||||
files)
|
||||
|
||||
(defn process-export
|
||||
[file-id export-type files]
|
||||
|
||||
(case export-type
|
||||
:all files
|
||||
:merge (let [file-list (-> files (d/without-keys [file-id]) vals)]
|
||||
(-> (select-keys files [file-id])
|
||||
(update file-id merge-assets file-list)
|
||||
(update file-id dissoc :libraries)))
|
||||
:detach (-> (select-keys files [file-id])
|
||||
(update file-id detach-libraries file-id))))
|
||||
|
||||
(defn collect-files
|
||||
[file-id]
|
||||
[file-id export-type]
|
||||
|
||||
(letfn [(fetch-dependencies [[files pending]]
|
||||
(if (empty? pending)
|
||||
|
@ -185,17 +214,18 @@
|
|||
(->> (rx/of [files pending])
|
||||
(rx-expand fetch-dependencies)
|
||||
(rx/last)
|
||||
(rx/map first)))))
|
||||
(rx/map first)
|
||||
(rx/map #(process-export file-id export-type %))))))
|
||||
|
||||
(defn export-file
|
||||
[team-id file-id]
|
||||
[team-id file-id export-type]
|
||||
|
||||
(let [files-stream (->> (collect-files file-id)
|
||||
(let [files-stream (->> (collect-files file-id export-type)
|
||||
(rx/share))
|
||||
|
||||
manifest-stream
|
||||
(->> files-stream
|
||||
(rx/map #(create-manifest team-id file-id %))
|
||||
(rx/map #(create-manifest team-id file-id export-type %))
|
||||
(rx/map #(vector "manifest.json" %)))
|
||||
|
||||
render-stream
|
||||
|
@ -258,10 +288,10 @@
|
|||
(rx/map #(vector (get files file-id) %)))))))))
|
||||
|
||||
(defmethod impl/handler :export-file
|
||||
[{:keys [team-id files] :as message}]
|
||||
[{:keys [team-id files export-type] :as message}]
|
||||
|
||||
(->> (rx/from files)
|
||||
(rx/mapcat #(export-file team-id %))
|
||||
(rx/mapcat #(export-file team-id % export-type))
|
||||
(rx/map
|
||||
(fn [value]
|
||||
(if (contains? value :type)
|
||||
|
|
|
@ -84,25 +84,26 @@
|
|||
(let [id-mapping-atom (atom {})
|
||||
resolve
|
||||
(fn [id-mapping id]
|
||||
(assert (uuid? id))
|
||||
(assert (uuid? id) (str id))
|
||||
(get id-mapping id))
|
||||
|
||||
set-id
|
||||
(fn [id-mapping id]
|
||||
(assert (uuid? id))
|
||||
(assert (uuid? id) (str id))
|
||||
(cond-> id-mapping
|
||||
(nil? (resolve id-mapping id))
|
||||
(assoc id (uuid/next))))]
|
||||
|
||||
(fn [id]
|
||||
(swap! id-mapping-atom set-id id)
|
||||
(resolve @id-mapping-atom id))))
|
||||
(when (some? id)
|
||||
(swap! id-mapping-atom set-id id)
|
||||
(resolve @id-mapping-atom id)))))
|
||||
|
||||
(defn create-file
|
||||
"Create a new file on the back-end"
|
||||
[context file-id]
|
||||
[context]
|
||||
(let [resolve (:resolve context)
|
||||
file-id (resolve file-id)]
|
||||
file-id (resolve (:file-id context))]
|
||||
(rp/mutation
|
||||
:create-temp-file
|
||||
{:id file-id
|
||||
|
@ -111,19 +112,19 @@
|
|||
:project-id (:project-id context)
|
||||
:data (-> cp/empty-file-data (assoc :id file-id))})))
|
||||
|
||||
(defn persist-file [file]
|
||||
(rp/mutation :persist-temp-file {:id (:id file)}))
|
||||
|
||||
(defn link-file-libraries
|
||||
"Create a new file on the back-end"
|
||||
[context file-id]
|
||||
[context]
|
||||
(let [resolve (:resolve context)
|
||||
file-id (resolve file-id)
|
||||
file-id (resolve (:file-id context))
|
||||
libraries (->> context :libraries (mapv resolve))]
|
||||
(->> (rx/from libraries)
|
||||
(rx/map #(hash-map :file-id file-id :library-id %))
|
||||
(rx/flat-map (partial rp/mutation :link-file-to-library)))))
|
||||
|
||||
(defn persist-file [file]
|
||||
(rp/mutation :persist-temp-file {:id (:id file)}))
|
||||
|
||||
(defn send-changes
|
||||
"Creates batches of changes to be sent to the backend"
|
||||
[file]
|
||||
|
@ -391,65 +392,59 @@
|
|||
(rx/flat-map (partial process-library-typographies context))
|
||||
(rx/flat-map (partial process-library-media context))
|
||||
(rx/flat-map (partial process-library-components context))
|
||||
(rx/flat-map send-changes)
|
||||
(rx/ignore)))
|
||||
(rx/flat-map send-changes)))
|
||||
|
||||
(defn create-files [context manifest]
|
||||
(->> manifest :files rx/from
|
||||
(defn create-files
|
||||
[context files]
|
||||
|
||||
(let [data (group-by :file-id files)]
|
||||
(rx/concat
|
||||
(->> (rx/from files)
|
||||
(rx/map #(merge context %))
|
||||
(rx/flat-map
|
||||
(fn [context]
|
||||
(->> (create-file context)
|
||||
(rx/map #(vector % (first (get data (:file-id context)))))))))
|
||||
|
||||
(->> (rx/from files)
|
||||
(rx/map #(merge context %))
|
||||
(rx/flat-map link-file-libraries)
|
||||
(rx/ignore)))))
|
||||
|
||||
(defmethod impl/handler :analyze-import
|
||||
[{:keys [files]}]
|
||||
|
||||
(->> (rx/from files)
|
||||
(rx/flat-map
|
||||
(fn [[file-id file-desc]]
|
||||
(create-file (merge context file-desc) file-id)))
|
||||
(rx/reduce #(assoc %1 (:id %2) %2) {})))
|
||||
(fn [uri]
|
||||
(->> (rx/of uri)
|
||||
(rx/flat-map uz/load-from-url)
|
||||
(rx/flat-map #(get-file {:zip %} :manifest))
|
||||
(rx/map (comp d/kebab-keys cip/string->uuid))
|
||||
(rx/map #(hash-map :uri uri :data %))
|
||||
(rx/catch #(rx/of {:uri uri :error (.-message %)})))))))
|
||||
|
||||
(defn link-libraries [context manifest]
|
||||
(->> manifest :files rx/from
|
||||
(rx/flat-map
|
||||
(fn [[file-id file-desc]]
|
||||
(link-file-libraries (merge context file-desc) file-id)))))
|
||||
|
||||
(defn process-files [context manifest files]
|
||||
(->> manifest :files rx/from
|
||||
(rx/flat-map
|
||||
(fn [[file-id file-desc]]
|
||||
(let [resolve (:resolve context)
|
||||
context (-> context
|
||||
(merge file-desc)
|
||||
(assoc :file-id file-id))
|
||||
file (get files (resolve file-id))]
|
||||
(process-file context file))))))
|
||||
|
||||
(defn process-package
|
||||
[context]
|
||||
(->> (get-file context :manifest)
|
||||
(rx/map (comp d/kebab-keys cip/string->uuid))
|
||||
|
||||
;; Create the temporary files
|
||||
(rx/mapcat (fn [manifest]
|
||||
(->> (create-files context manifest)
|
||||
(rx/map #(vector manifest %)))))
|
||||
|
||||
;; Set-up the files dependencies
|
||||
(rx/mapcat (fn [[manifest files]]
|
||||
(rx/concat
|
||||
(link-libraries context manifest)
|
||||
(rx/of [manifest files]))))
|
||||
|
||||
;; Creates files data
|
||||
(rx/mapcat (fn [[manifest files]]
|
||||
(process-files context manifest files)))
|
||||
|
||||
;; Mark temporary files as persisted
|
||||
(rx/mapcat persist-file)))
|
||||
|
||||
(defmethod impl/handler :import-file
|
||||
(defmethod impl/handler :import-files
|
||||
[{:keys [project-id files]}]
|
||||
|
||||
(let [context {:project-id project-id
|
||||
:resolve (resolve-factory)}]
|
||||
(->> (rx/from files)
|
||||
(rx/flat-map uz/load-from-url)
|
||||
(rx/map #(assoc context :zip %))
|
||||
(rx/flat-map process-package)
|
||||
(rx/catch
|
||||
(fn [err]
|
||||
(.error js/console "ERROR" err (clj->js (.-data err))))))))
|
||||
(->> (create-files context files)
|
||||
(rx/catch #(.error js/console "IMPORT ERROR" %))
|
||||
(rx/flat-map
|
||||
(fn [[file data]]
|
||||
(->> (uz/load-from-url (:uri data))
|
||||
(rx/map #(-> context (assoc :zip %) (merge data)))
|
||||
(rx/flat-map #(process-file % file))
|
||||
(rx/map
|
||||
(fn [_]
|
||||
{:status :import-success
|
||||
:file-id (:file-id data)}))
|
||||
|
||||
(rx/catch
|
||||
(fn [err]
|
||||
(.error js/console "ERROR" (:file-id data) err)
|
||||
(rx/of {:status :import-error
|
||||
:file-id (:file-id data)
|
||||
:error (.-message err)
|
||||
:error-data (clj->js (.-data err))})))))))))
|
||||
|
|
|
@ -822,6 +822,9 @@ msgstr "Admin"
|
|||
msgid "labels.all"
|
||||
msgstr "All"
|
||||
|
||||
msgid "labels.continue"
|
||||
msgstr "Continue"
|
||||
|
||||
#: src/app/main/ui/static.cljs
|
||||
msgid "labels.bad-gateway.desc-message"
|
||||
msgstr ""
|
||||
|
@ -2696,4 +2699,43 @@ msgid "workspace.updates.update"
|
|||
msgstr "Update"
|
||||
|
||||
msgid "workspace.viewport.click-to-close-path"
|
||||
msgstr "Click to close the path"
|
||||
msgstr "Click to close the path"
|
||||
|
||||
msgid "dashboard.import.import-message"
|
||||
msgstr "%s files have been imported succesfully."
|
||||
|
||||
msgid "dashboard.import.import-error"
|
||||
msgstr "There was a problem importing the file. The file wasn't imported."
|
||||
|
||||
msgid "dashboard.import.analyze-error"
|
||||
msgstr "Oops! We couldn't import this file"
|
||||
|
||||
msgid "dashboard.export.title"
|
||||
msgstr "Export files"
|
||||
|
||||
msgid "dashboard.export.explain"
|
||||
msgstr "One or more files that you want to export are using shared libraries. What do you want to do with their assets*?"
|
||||
|
||||
msgid "dashboard.export.detail"
|
||||
msgstr "* Might include components, graphics, colors and/or typographies."
|
||||
|
||||
msgid "dashboard.export.options.all.title"
|
||||
msgstr "Export shared libraries"
|
||||
|
||||
msgid "dashboard.export.options.all.message"
|
||||
msgstr "files with shared libraries will be included in the export, maintaining their linkage."
|
||||
|
||||
msgid "dashboard.export.options.merge.title"
|
||||
msgstr "Include shared library assets in file libraries"
|
||||
|
||||
msgid "dashboard.export.options.merge.message"
|
||||
msgstr "Your file will be exported with all external assets merged into the file library."
|
||||
|
||||
msgid "dashboard.export.options.detach.title"
|
||||
msgstr "Treat shared library assets as basic objects"
|
||||
|
||||
msgid "dashboard.export.options.detach.message"
|
||||
msgstr "Shared libraries will not be included in the export and no assets will be added to the library. "
|
||||
|
||||
msgid "labels.export"
|
||||
msgstr "Export"
|
||||
|
|
Loading…
Add table
Reference in a new issue