diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/import.cljs b/frontend/src/app/main/ui/workspace/tokens/modals/import.cljs index 21aa246c09..7df91f9a57 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/import.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/modals/import.cljs @@ -14,15 +14,17 @@ [app.main.data.workspace.tokens.import-export :as dwti] [app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.store :as st] + [app.main.ui.components.dropdown :refer [dropdown]] [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.controls.shared.options-dropdown :refer [options-dropdown*]] [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.util.dom :as dom] [app.util.i18n :refer [tr]] [app.util.webapi :as wapi] + [app.util.zip :as uz] [beicon.v2.core :as rx] [cuerdas.core :as str] [potok.v2.core :as ptk] @@ -42,11 +44,82 @@ :type :toast :level :error}))))) +(mf/defc import-type-dropdown* + {::mf/private true} + [{:keys [options on-click text-render default]}] + (let [initial-option (or (first (filter #(= (:value %) default) options)) + (first options)) + selected-option* (mf/use-state initial-option) + selected-option @selected-option* + show-dropdown? (mf/use-state false) + + js-options (mf/use-memo + (mf/deps options) + #(clj->js + (mapv (fn [option] + {:id (str (:value option)) + :label (:label option) + :aria-label (:label option)}) + options))) + button-text (if text-render + (text-render selected-option) + (:label selected-option)) + + toggle-dropdown + (mf/use-fn + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (swap! show-dropdown? not))) + + close-dropdown + (mf/use-fn #(reset! show-dropdown? false)) + + handle-option-click + (mf/use-fn + (mf/deps options) + (fn [event] + (let [target (dom/get-current-target event) + option-id (dom/get-attribute target "id") + option (first (filter #(= (str (:value %)) option-id) options))] + (close-dropdown) + (when option + (reset! selected-option* option))))) + + handle-main-click + (mf/use-fn + (mf/deps on-click selected-option) + (fn [event] + (dom/prevent-default event) + (when (and selected-option on-click) + (on-click (:value selected-option)))))] + + [:div {:class (stl/css :dropdown-btn-wrapper)} + [:> button* {:variant "primary" + :type "button" + :class (stl/css :dropdown-btn) + :on-click handle-main-click} + button-text] + + [:> button* {:variant "primary" + :type "button" + :class (stl/css :dropdown-trigger-btn) + :icon "arrow-down" + :on-click toggle-dropdown + :aria-label "Show options"}] + + [:& dropdown {:show @show-dropdown? :on-close close-dropdown} + [:> options-dropdown* {:options js-options + :selected (str (:value selected-option)) + :on-click handle-option-click + :set-ref (fn [_] nil)}]]])) + (mf/defc import-modal-body* {::mf/private true} [] (let [file-input-ref (mf/use-ref) dir-input-ref (mf/use-ref) + zip-input-ref (mf/use-ref) on-display-file-explorer (mf/use-fn #(dom/click (mf/ref-val file-input-ref))) @@ -54,6 +127,19 @@ on-display-dir-explorer (mf/use-fn #(dom/click (mf/ref-val dir-input-ref))) + on-display-zip-explorer + (mf/use-fn #(dom/click (mf/ref-val zip-input-ref))) + + handle-import-action + (mf/use-fn + (mf/deps on-display-file-explorer on-display-dir-explorer on-display-zip-explorer) + (fn [val] + (case val + :file (on-display-file-explorer) + :folder (on-display-dir-explorer) + :zip (on-display-zip-explorer) + nil))) + on-import-directory (mf/use-fn (fn [event] @@ -77,7 +163,38 @@ (-> (mf/ref-val dir-input-ref) (dom/set-value! ""))))) - on-import-file + on-import-zip-file + (mf/use-fn + (fn [event] + (let [zipfile (-> (dom/get-target event) + (dom/get-files) + (first)) + zipfile-name (str/strip-suffix (.-name zipfile) ".zip")] + (->> (wapi/read-file-as-array-buffer zipfile) + (rx/mapcat (fn [file-content] + (let [zip-reader (uz/reader file-content)] + (->> (rx/from (uz/get-entries zip-reader)) + (rx/mapcat + (fn [entries] + (->> (rx/from entries) + (rx/filter (fn [entry] + (let [filename (.-filename entry)] + (str/ends-with? filename ".json")))) + (rx/merge-map (fn [entry] + (let [filename (str/concat zipfile-name "/" (.-filename entry)) + content-promise (uz/read-as-text entry)] + (-> content-promise + (.then (fn [text] + [filename text])) + (rx/from)))))))) + (rx/finalize (partial uz/close zip-reader)))))) + (dwti/import-directory-stream) + (on-stream-imported "zip")) + + (-> (mf/ref-val zip-input-ref) + (dom/set-value! ""))))) + + on-import-json-file (mf/use-fn (fn [event] (let [file (-> (dom/get-target event) @@ -88,7 +205,12 @@ (on-stream-imported "single")) (-> (mf/ref-val file-input-ref) - (dom/set-value! "")))))] + (dom/set-value! ""))))) + + render-button-text + (mf/use-fn + (fn [option] + (tr "workspace.tokens.import-button-prefix" (:label option))))] [:div {:class (stl/css :import-modal-wrapper)} [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :import-modal-title)} @@ -109,7 +231,12 @@ :ref file-input-ref :style {:display "none"} :accept ".json" - :on-change on-import-file}] + :on-change on-import-json-file}] + [:input {:type "file" + :ref zip-input-ref + :style {:display "none"} + :accept ".zip" + :on-change on-import-zip-file}] [:input {:type "file" :ref dir-input-ref :style {:display "none"} @@ -120,16 +247,13 @@ :type "button" :on-click modal/hide!} (tr "labels.cancel")] - [:> button* {:variant "primary" - :type "button" - :icon i/document - :on-click on-display-file-explorer} - (tr "workspace.tokens.choose-file")] - [:> button* {:variant "primary" - :type "button" - :icon i/folder - :on-click on-display-dir-explorer} - (tr "workspace.tokens.choose-folder")]]])) + [:> import-type-dropdown* + {:options [{:label (tr "workspace.tokens.import-menu-zip-option") :value :zip} + {:label (tr "workspace.tokens.import-menu-json-option") :value :file} + {:label (tr "workspace.tokens.import-menu-folder-option") :value :folder}] + :on-click handle-import-action + :text-render render-button-text + :default :zip}]]])) (mf/defc import-modal* {::mf/register modal/components diff --git a/frontend/src/app/main/ui/workspace/tokens/modals/import.scss b/frontend/src/app/main/ui/workspace/tokens/modals/import.scss index e16c669f2d..5e2ba5fba9 100644 --- a/frontend/src/app/main/ui/workspace/tokens/modals/import.scss +++ b/frontend/src/app/main/ui/workspace/tokens/modals/import.scss @@ -5,6 +5,7 @@ // Copyright (c) KALEIDOS INC @use "../../../ds/typography.scss" as t; +@use "../../../ds/_borders.scss" as *; @use "../../../ds/_sizes.scss" as *; @import "refactor/common-refactor.scss"; @@ -48,3 +49,23 @@ inset-block-start: var(--sp-s); inset-inline-end: var(--sp-s); } + +.dropdown-trigger-btn { + border-start-start-radius: 0; + border-end-start-radius: 0; + border-inline-start: $b-1 solid var(--color-accent-tertiary); + width: var(--sp-xxxl); + padding-inline-start: 0; + padding-inline-end: 0; + justify-content: center; +} + +.dropdown-btn { + border-end-end-radius: 0; + border-start-end-radius: 0; +} + +.dropdown-btn-wrapper { + position: relative; + display: flex; +} diff --git a/frontend/src/app/util/zip.cljs b/frontend/src/app/util/zip.cljs index 0ddf6d90b5..ae0d484133 100644 --- a/frontend/src/app/util/zip.cljs +++ b/frontend/src/app/util/zip.cljs @@ -74,6 +74,12 @@ (p/fmap (fn [entries] (array/find #(= (.-filename ^js %) path) entries))))) +(defn get-entries + [reader] + (assert (instance? zip/ZipReader reader)) + (.getEntries reader)) + + (defn read-as-text [entry] (let [writer (new zip/TextWriter)] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 9aaffe059b..c2149468fc 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7122,6 +7122,22 @@ msgstr "Choose file" msgid "workspace.tokens.choose-folder" msgstr "Choose folder" +#: src/app/main/ui/workspace/tokens/modals/import.cljs:251 +msgid "workspace.tokens.import-button-prefix" +msgstr "Import %s" + +#: src/app/main/ui/workspace/tokens/modals/import.cljs:245 +msgid "workspace.tokens.import-menu-zip-option" +msgstr "ZIP file" + +#: src/app/main/ui/workspace/tokens/modals/import.cljs:246 +msgid "workspace.tokens.import-menu-json-option" +msgstr "Single JSON file" + +#: src/app/main/ui/workspace/tokens/modals/import.cljs:247 +msgid "workspace.tokens.import-menu-folder-option" +msgstr "Folder" + #: src/app/main/ui/workspace/tokens/context_menu.cljs:245 msgid "workspace.tokens.color" msgstr "Color"