From d0ab8135203cf1cedd43c3325772cf2f37cf0ae8 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 1 Jul 2021 17:47:32 +0200 Subject: [PATCH] :sparkles: Import/export UI and final touches --- .../resources/styles/main/partials/modal.scss | 327 +++++++++++++++++- frontend/src/app/main/ui/dashboard.cljs | 3 +- .../src/app/main/ui/dashboard/export.cljs | 92 +++++ .../src/app/main/ui/dashboard/file_menu.cljs | 25 +- .../src/app/main/ui/dashboard/import.cljs | 294 +++++++++++++++- .../app/main/ui/dashboard/project_menu.cljs | 4 +- frontend/src/app/main/ui/icons.clj | 5 +- frontend/src/app/main/ui/loader.cljs | 2 +- frontend/src/app/util/dom.cljs | 4 + frontend/src/app/worker/export.cljs | 46 ++- frontend/src/app/worker/import.cljs | 127 ++++--- frontend/translations/en.po | 44 ++- 12 files changed, 856 insertions(+), 117 deletions(-) create mode 100644 frontend/src/app/main/ui/dashboard/export.cljs diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index bb0b12751..e61ec1423 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -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; } } - diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index ef8721697..9d2f00178 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -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}])])]])) - diff --git a/frontend/src/app/main/ui/dashboard/export.cljs b/frontend/src/app/main/ui/dashboard/export.cljs new file mode 100644 index 000000000..059c91f05 --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/export.cljs @@ -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}]]]]])) diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 8da556dbc..8f73075b5 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -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]])] diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index e3f63b38d..005ba8789 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -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}])]]]])) diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs index 48e0ab05e..8967e8a0c 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -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]]}]])) diff --git a/frontend/src/app/main/ui/icons.clj b/frontend/src/app/main/ui/icons.clj index c44da08e5..a31a1fa92 100644 --- a/frontend/src/app/main/ui/icons.clj +++ b/frontend/src/app/main/ui/icons.clj @@ -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}]]))) diff --git a/frontend/src/app/main/ui/loader.cljs b/frontend/src/app/main/ui/loader.cljs index bd5500b18..424e071dd 100644 --- a/frontend/src/app/main/ui/loader.cljs +++ b/frontend/src/app/main/ui/loader.cljs @@ -15,4 +15,4 @@ (mf/defc loader [] (when (mf/deref st/loader) - [:div.loader-content i/loader])) + [:div.loader-content i/loader-pencil])) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 66a5f53f2..ccdf6be61 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -213,6 +213,10 @@ [node] (.focus node)) +(defn blur! + [node] + (.blur node)) + (defn fullscreen? [] (cond diff --git a/frontend/src/app/worker/export.cljs b/frontend/src/app/worker/export.cljs index 34377667b..045e122d4 100644 --- a/frontend/src/app/worker/export.cljs +++ b/frontend/src/app/worker/export.cljs @@ -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) diff --git a/frontend/src/app/worker/import.cljs b/frontend/src/app/worker/import.cljs index 21a3dc911..79a33129a 100644 --- a/frontend/src/app/worker/import.cljs +++ b/frontend/src/app/worker/import.cljs @@ -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))}))))))))) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 6d3a52282..ec28a4922 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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" \ No newline at end of file +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"