From c3f37fb8a363adc244f6af0497e7fc1a1155b4c1 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 5 Mar 2024 13:08:55 +0100 Subject: [PATCH] :recycle: Refactor import dialog on dashboard --- .../src/app/main/ui/dashboard/import.cljs | 546 ++++++++++-------- .../src/app/main/ui/dashboard/import.scss | 1 + 2 files changed, 310 insertions(+), 237 deletions(-) diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 2d218e06c..2d079a778 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -38,17 +38,17 @@ [project-id on-finish-import] (mf/use-fn (mf/deps project-id on-finish-import) - (fn [files] - (when files - (let [files (->> files - (mapv - (fn [file] - {:name (.-name file) - :uri (wapi/create-uri file)})))] + (fn [entries] + (let [entries (->> entries + (mapv (fn [file] + {:name (.-name file) + :uri (wapi/create-uri file)})) + (not-empty))] + (when entries (st/emit! (modal/show {:type :import :project-id project-id - :files files + :entries entries :on-finish-import on-finish-import}))))))) (mf/defc import-form @@ -56,7 +56,6 @@ ::mf/props :obj} [{:keys [project-id on-finish-import]} external-ref] - (let [on-file-selected (use-import-file project-id on-finish-import)] [:form.import-file {:aria-hidden "true"} [:& file-uploader {:accept ".penpot,.zip" @@ -64,72 +63,70 @@ :ref external-ref :on-selected on-file-selected}]])) -(defn update-file - [files file-id new-name] - (mapv - (fn [file] - (let [new-name (str/trim new-name)] - (cond-> file - (and (= (:file-id file) file-id) - (not= "" new-name)) - (assoc :name new-name)))) - files)) +(defn- update-entry-name + [entries file-id new-name] + (mapv (fn [entry] + (let [new-name (str/trim new-name)] + (cond-> entry + (and (= (:file-id entry) file-id) + (not= "" new-name)) + (assoc :name new-name)))) + entries)) -(defn remove-file - [files file-id] - (mapv - (fn [file] - (cond-> file - (= (:file-id file) file-id) - (assoc :deleted? true))) - files)) +(defn- remove-entry + [entries file-id] + (mapv (fn [entry] + (cond-> entry + (= (:file-id entry) file-id) + (assoc :deleted true))) + entries)) -(defn set-analyze-error - [files uri error] - (->> files - (mapv (fn [file] - (cond-> file - (= uri (:uri file)) +(defn- update-with-analyze-error + [entries uri error] + (->> entries + (mapv (fn [entry] + (cond-> entry + (= uri (:uri entry)) (-> (assoc :status :analyze-error) (assoc :error error))))))) -(defn set-analyze-result - [files uri type data] - (let [existing-files? (into #{} (->> files (map :file-id) (filter some?))) - replace-file - (fn [file] - (if (and (= uri (:uri file)) - (= (:status file) :analyzing)) - (->> (:files data) - (remove (comp existing-files? first)) - (mapv (fn [[file-id file-data]] - (-> file-data - (assoc :file-id file-id - :status :ready - :uri uri - :type type))))) - [file]))] - (into [] (mapcat replace-file) files))) +(defn- update-with-analyze-result + [entries uri type result] + (let [existing-entries? (into #{} (keep :file-id) entries) + replace-entry + (fn [entry] + (if (and (= uri (:uri entry)) + (= (:status entry) :analyzing)) + (->> (:files result) + (remove (comp existing-entries? first)) + (map (fn [[file-id file-data]] + (-> file-data + (assoc :file-id file-id) + (assoc :status :ready) + (assoc :uri uri) + (assoc :type type))))) + [entry]))] + (into [] (mapcat replace-entry) entries))) -(defn mark-files-importing - [files] - (->> files +(defn- mark-entries-importing + [entries] + (->> entries (filter #(= :ready (:status %))) (mapv #(assoc % :status :importing)))) -(defn update-status - [files file-id status progress errors] - (->> files - (mapv (fn [file] - (cond-> file - (and (= file-id (:file-id file)) (not= status :import-progress)) - (assoc :status status) +(defn- update-entry-status + [entries file-id status progress errors] + (mapv (fn [entry] + (cond-> entry + (and (= file-id (:file-id entry)) (not= status :import-progress)) + (assoc :status status) - (and (= file-id (:file-id file)) (= status :import-progress)) - (assoc :progress progress) + (and (= file-id (:file-id entry)) (= status :import-progress)) + (assoc :progress progress) - (= file-id (:file-id file)) - (assoc :errors errors)))))) + (= file-id (:file-id entry)) + (assoc :errors errors))) + entries)) (defn- parse-progress-message [message] @@ -157,53 +154,116 @@ (str message))) +(defn- has-status-importing? + [item] + (= (:status item) :importing)) + +(defn- has-status-analyzing? + [item] + (= (:status item) :analyzing)) + +(defn- has-status-analyze-error? + [item] + (= (:status item) :analyzing)) + +(defn- has-status-success? + [item] + (and (= (:status item) :import-finish) + (empty? (:errors item)))) + +(defn- has-status-error? + [item] + (and (= (:status item) :import-finish) + (d/not-empty? (:errors item)))) + +(defn- has-status-ready? + [item] + (and (= :ready (:status item)) + (not (:deleted item)))) + +(defn- analyze-entries + [state entries] + (->> (uw/ask-many! + {:cmd :analyze-import + :files entries + :features @features/features-ref}) + (rx/mapcat #(rx/delay emit-delay (rx/of %))) + (rx/filter some?) + (rx/subs! + (fn [{:keys [uri data error type] :as msg}] + (if (some? error) + (swap! state update-with-analyze-error uri error) + (swap! state update-with-analyze-result uri type data)))))) + +(defn- import-files! + [state project-id entries] + (st/emit! (ptk/data-event ::ev/event {::ev/name "import-files" + :num-files (count entries)})) + (->> (uw/ask-many! + {:cmd :import-files + :project-id project-id + :files entries + :features @features/features-ref}) + (rx/subs! + (fn [{:keys [file-id status message errors] :as msg}] + (swap! state update-entry-status file-id status message errors))))) + (mf/defc import-entry - {::mf/props :obj} - [{:keys [state file editing? can-be-deleted]}] - (let [loading? (or (= :analyzing (:status file)) - (= :importing (:status file))) - analyze-error? (= :analyze-error (:status file)) - import-finish? (= :import-finish (:status file)) - import-error? (= :import-error (:status file)) - import-warn? (d/not-empty? (:errors file)) - ready? (= :ready (:status file)) - is-shared? (:shared file) - progress (:progress file) + {::mf/props :obj + ::mf/memo true + ::mf/private true} + [{:keys [entries entry edition can-be-deleted on-edit on-change on-delete]}] + (let [status (:status entry) + loading? (or (= :analyzing status) + (= :importing status)) + analyze-error? (= :analyze-error status) + import-finish? (= :import-finish status) + import-error? (= :import-error status) + import-warn? (d/not-empty? (:errors entry)) + ready? (= :ready status) + is-shared? (:shared entry) + progress (:progress entry) - handle-edit-key-press + file-id (:file-id entry) + editing? (and (some? file-id) (= edition file-id)) + + on-edit-key-press (mf/use-fn - (fn [e] - (when (or (kbd/enter? e) (kbd/esc? e)) - (dom/prevent-default e) - (dom/stop-propagation e) - (dom/blur! (dom/get-target e))))) + (fn [event] + (when (or (kbd/enter? event) + (kbd/esc? event)) + (dom/prevent-default event) + (dom/stop-propagation event) + (dom/blur! (dom/get-target event))))) - handle-edit-blur + on-edit-blur (mf/use-fn - (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)))))) + (mf/deps file-id on-change) + (fn [event] + (let [value (dom/get-target-val event)] + (on-change file-id value event)))) - handle-edit-entry + on-edit' (mf/use-fn - (mf/deps file) - (fn [] - (swap! state assoc :editing (:file-id file)))) + (mf/deps file-id on-change) + (fn [event] + (when (fn? on-edit) + (on-edit file-id event)))) - handle-remove-entry + on-delete' (mf/use-fn - (mf/deps file) - (fn [] - (swap! state update :files remove-file (:file-id file))))] + (mf/deps file-id on-delete) + (fn [event] + (when (fn? on-delete) + (on-delete file-id event))))] - [:div {:class (stl/css-case :file-entry true - :loading loading? - :success (and import-finish? (not import-warn?) (not import-error?)) - :warning (and import-finish? import-warn? (not import-error?)) - :error (or import-error? analyze-error?) - :editable (and ready? (not editing?)))} + [:div {:class (stl/css-case + :file-entry true + :loading loading? + :success (and import-finish? (not import-warn?) (not import-error?)) + :warning (and import-finish? import-warn? (not import-error?)) + :error (or import-error? analyze-error?) + :editable (and ready? (not editing?)))} [:div {:class (stl/css :file-name)} [:div {:class (stl/css-case :file-icon true @@ -219,26 +279,28 @@ [:div {:class (stl/css :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}]] + :default-value (:name entry) + :on-key-press on-edit-key-press + :on-blur on-edit-blur}]] [:div {:class (stl/css :file-name-label)} - (:name file) - (when is-shared? + (:name entry) + (when ^boolean is-shared? [:span {:class (stl/css :icon)} i/library-refactor])]) [:div {:class (stl/css :edit-entry-buttons)} - (when (= "application/zip" (:type file)) - [:button {:on-click handle-edit-entry} i/curve-refactor]) + (when (and (= "application/zip" (:type entry)) + (= status :ready)) + [:button {:on-click on-edit'} i/curve-refactor]) (when can-be-deleted - [:button {:on-click handle-remove-entry} i/delete-refactor])]] + [:button {:on-click on-delete'} i/delete-refactor])]] + (cond analyze-error? [:div {:class (stl/css :error-message)} - (if (some? (:error file)) - (tr (:error file)) + (if (some? (:error entry)) + (tr (:error entry)) (tr "dashboard.import.analyze-error"))] import-error? @@ -249,139 +311,143 @@ [:div {:class (stl/css :progress-message)} (parse-progress-message progress)]) [:div {:class (stl/css :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))] + (for [library-id (:libraries entry)] + (let [library-data (d/seek #(= library-id (:file-id %)) entries) + error? (or (:deleted library-data) + (:import-error library-data))] (when (some? library-data) - [:div {:class (stl/css :linked-library)} + [:div {:class (stl/css :linked-library) + :key (dm/str library-id)} (:name library-data) - [:span {:class (stl/css-case :linked-library-tag true - :error error?)} i/detach-refactor]])))]])) + [:span {:class (stl/css-case + :linked-library-tag true + :error error?)} + i/detach-refactor]])))]])) (mf/defc import-dialog {::mf/register modal/components ::mf/register-as :import ::mf/props :obj} - [{:keys [project-id files template on-finish-import]}] - (let [state (mf/use-state - {:status :analyzing - :editing nil - :importing-templates 0 - :files (->> files - (mapv #(assoc % :status :analyzing)))}) - analyze-import - (mf/use-fn - (fn [files] - (->> (uw/ask-many! - {:cmd :analyze-import - :files files - :features @features/features-ref}) - (rx/mapcat #(rx/delay emit-delay (rx/of %))) - (rx/filter some?) - (rx/subs! - (fn [{:keys [uri data error type] :as msg}] - (if (some? error) - (swap! state update :files set-analyze-error uri error) - (swap! state update :files set-analyze-result uri type data))))))) + [{:keys [project-id entries template on-finish-import]}] - import-files - (mf/use-fn - (fn [project-id files] - (st/emit! (ptk/event ::ev/event {::ev/name "import-files" - :num-files (count files)})) - (->> (uw/ask-many! - {:cmd :import-files - :project-id project-id - :files files - :features @features/features-ref}) - (rx/subs! - (fn [{:keys [file-id status message errors] :as msg}] - (swap! state update :files update-status file-id status message errors)))))) + (mf/with-effect [] + ;; dispose uris when the component is umount + (fn [] (run! wapi/revoke-uri (map :uri entries)))) - handle-cancel + (let [entries* (mf/use-state + (fn [] (mapv #(assoc % :status :analyzing) entries))) + entries (deref entries*) + + status* (mf/use-state :analyzing) + status (deref status*) + + edition* (mf/use-state nil) + edition (deref edition*) + + on-template-cloned-success (mf/use-fn - (mf/deps (:editing @state)) + (fn [] + (swap! status* (constantly :importing)) + ;; (swap! state assoc :status :importing :importing-templates 0) + (st/emit! (dd/fetch-recent-files)))) + + on-template-cloned-error + (mf/use-fn + (fn [cause] + (swap! status* (constantly :error)) + ;; (swap! state assoc :status :error :importing-templates 0) + (errors/print-error! cause) + (rx/of (modal/hide) + (msg/error (tr "dashboard.libraries-and-templates.import-error"))))) + + continue-entries + (mf/use-fn + (mf/deps entries) + (fn [] + (let [entries (filterv has-status-ready? entries)] + (swap! status* (constantly :importing)) + (swap! entries* mark-entries-importing) + (import-files! entries* project-id entries)))) + + continue-template + (mf/use-fn + (mf/deps on-template-cloned-success + on-template-cloned-error + template) + (fn [] + (let [mdata {:on-success on-template-cloned-success + :on-error on-template-cloned-error} + params {:project-id project-id :template-id (:id template)}] + (swap! status* (constantly :importing)) + (st/emit! (dd/clone-template (with-meta params mdata)))))) + + on-edit + (mf/use-fn + (fn [file-id _event] + (swap! edition* (constantly file-id)))) + + on-entry-change + (mf/use-fn + (fn [file-id value] + (swap! edition* (constantly nil)) + (swap! entries* update-entry-name file-id value))) + + on-entry-delete + (mf/use-fn + (fn [file-id] + (swap! entries* remove-entry file-id))) + + on-cancel + (mf/use-fn + (mf/deps edition) (fn [event] - (when (nil? (:editing @state)) + (when (nil? edition) (dom/prevent-default event) (st/emit! (modal/hide))))) - on-template-cloned-success - (fn [] - (swap! state assoc :status :importing :importing-templates 0) - (st/emit! (dd/fetch-recent-files))) - - on-template-cloned-error - (fn [cause] - (swap! state assoc :status :error :importing-templates 0) - (errors/print-error! cause) - (rx/of (modal/hide) - (msg/error (tr "dashboard.libraries-and-templates.import-error")))) - - continue-files - (fn [] - (let [files (->> @state :files (filterv #(and (= :ready (:status %)) (not (:deleted? %)))))] - (import-files project-id files)) - - (swap! state - (fn [state] - (-> state - (assoc :status :importing) - (update :files mark-files-importing))))) - - continue-template - (fn [] - (let [mdata {:on-success on-template-cloned-success - :on-error on-template-cloned-error} - params {:project-id project-id :template-id (:id template)}] - (swap! state - (fn [state] - (-> state - (assoc :status :importing :importing-templates 1)))) - (st/emit! (dd/clone-template (with-meta params mdata))))) - - - handle-continue + on-continue (mf/use-fn - (mf/deps project-id (:files @state)) + (mf/deps template + continue-template + continue-entries) (fn [event] (dom/prevent-default event) (if (some? template) (continue-template) - (continue-files)))) + (continue-entries)))) - handle-accept + on-accept (mf/use-fn + (mf/deps on-finish-import) (fn [event] (dom/prevent-default event) (st/emit! (modal/hide)) - (when on-finish-import (on-finish-import)))) + (when (fn? on-finish-import) + (on-finish-import)))) - files (->> (:files @state) (filterv (comp not :deleted?))) + entries (filterv (comp not :deleted) entries) + num-importing (+ (count (filterv has-status-importing? entries)) + (if (some? template) 1 0)) - num-importing (+ - (->> files (filter #(= (:status %) :importing)) count) - (:importing-templates @state)) + success-num (if (some? template) + 1 + (count (filterv has-status-success? entries))) - warning-files (->> files (filter #(and (= (:status %) :import-finish) (d/not-empty? (:errors %)))) count) - success-files (->> files (filter #(and (= (:status %) :import-finish) (empty? (:errors %)))) count) - pending-analysis? (> (->> files (filter #(= (:status %) :analyzing)) count) 0) - pending-import? (> num-importing 0) + errors? (or (some has-status-error? entries) + (zero? (count entries))) - valid-files? (or (some? template) - (> (+ (->> files (filterv (fn [x] (not= (:status x) :analyze-error))) count)) 0))] - (mf/use-effect - (fn [] - (let [sub (analyze-import files)] - #(rx/dispose! sub)))) + pending-analysis? (some has-status-analyzing? entries) + pending-import? (pos? num-importing) + valid-all-entries? (or (some? template) + (not (some has-status-analyze-error? entries)))] - (mf/use-effect - (fn [] - ;; dispose uris when the component is umount - #(doseq [file files] - (wapi/revoke-uri (:uri file))))) + + ;; Run analyze operation on component mount + (mf/with-effect [] + (let [sub (analyze-entries entries* entries)] + (partial rx/dispose! sub))) [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-container)} @@ -389,52 +455,58 @@ [:h2 {:class (stl/css :modal-title)} (tr "dashboard.import")] [:button {:class (stl/css :modal-close-btn) - :on-click handle-cancel} i/close-refactor]] + :on-click on-cancel} i/close-refactor]] [:div {:class (stl/css :modal-content)} + (when (and (= :analyzing status) errors?) + [:& context-notification + {:type :warning + :content (tr "dashboard.import.import-warning")}]) - (when (and (= :importing (:status @state)) (not pending-import?)) - (if (> warning-files 0) + (when (and (= :importing status) (not ^boolean pending-import?)) + (cond + errors? [:& context-notification {:type :warning - :content (tr "dashboard.import.import-warning" warning-files success-files)}] + :content (tr "dashboard.import.import-warning")}] + + :else [:& context-notification {:type :success - :content (tr "dashboard.import.import-message" (i18n/c (if (some? template) 1 success-files)))}])) + :content (tr "dashboard.import.import-message" (i18n/c success-num))}])) - (for [file files] - (let [editing? (and (some? (:file-id file)) - (= (:file-id file) (:editing @state)))] - [:& import-entry {:state state - :key (dm/str (:uri file)) - :file file - :editing? editing? - :can-be-deleted (> (count files) 1)}])) + (for [entry entries] + [:& import-entry {:edition edition + :key (dm/str (:uri entry)) + :entry entry + :entries entries + :on-edit on-edit + :on-change on-entry-change + :on-delete on-entry-delete + :can-be-deleted (> (count entries) 1)}]) (when (some? template) - [:& import-entry {:state state - :file (assoc template :status (if (= 1 (:importing-templates @state)) :importing :ready)) - :editing? false + [:& import-entry {:entry (assoc template :status :ready) :can-be-deleted false}])] [:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :action-buttons)} - (when (= :analyzing (:status @state)) + (when (= :analyzing status) [:input {:class (stl/css :cancel-button) :type "button" :value (tr "labels.cancel") - :on-click handle-cancel}]) + :on-click on-cancel}]) - (when (= :analyzing (:status @state)) + (when (and (= :analyzing status) (not errors?)) [:input {:class (stl/css :accept-btn) :type "button" :value (tr "labels.continue") - :disabled (or pending-analysis? (not valid-files?)) - :on-click handle-continue}]) + :disabled (or pending-analysis? (not valid-all-entries?)) + :on-click on-continue}]) - (when (= :importing (:status @state)) + (when (and (= :importing status) (not errors?)) [:input {:class (stl/css :accept-btn) :type "button" :value (tr "labels.accept") - :disabled (or pending-import? (not valid-files?)) - :on-click handle-accept}])]]]])) + :disabled (or pending-import? (not valid-all-entries?)) + :on-click on-accept}])]]]])) diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index 0fc0c620f..a89dda6da 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -33,6 +33,7 @@ grid-template-columns: 1fr; gap: $s-16; margin-bottom: $s-24; + min-height: 40px; } .action-buttons {