2022-07-07 11:37:34 +02:00

370 lines
12 KiB
Clojure

;; 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.import
(:require
[app.common.data :as d]
[app.common.logging :as log]
[app.main.data.events :as ev]
[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.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.webapi :as wapi]
[beicon.core :as rx]
[potok.core :as ptk]
[rumext.alpha :as mf]))
(log/set-level! :debug)
(def ^:const emit-delay 1000)
(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
(fn [file]
{:name (.-name file)
:uri (wapi/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}
[{:keys [project-id on-finish-import]} external-ref]
(let [on-file-selected (use-import-file project-id on-finish-import)]
[:form.import-file
[:& file-uploader {:accept ".penpot,.zip"
:multi true
: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 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 mark-files-importing [files]
(->> files
(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)
(and (= file-id (:file-id file)) (= status :import-progress))
(assoc :progress progress)
(= file-id (:file-id file))
(assoc :errors errors))))))
(defn parse-progress-message
[message]
(case (:type message)
:upload-data
(tr "dashboard.import.progress.upload-data" (:current message) (:total message))
:upload-media
(tr "dashboard.import.progress.upload-media" (:file message))
:process-page
(tr "dashboard.import.progress.process-page" (:file message))
:process-colors
(tr "dashboard.import.progress.process-colors")
:process-typographies
(tr "dashboard.import.progress.process-typographies")
:process-media
(tr "dashboard.import.progress.process-media")
:process-components
(tr "dashboard.import.progress.process-components")
(str message)))
(mf/defc import-entry
[{: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)
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 (dom/classnames
: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.file-name
[:div.file-icon
(cond loading? i/loader-pencil
ready? i/logo-icon
import-warn? i/msg-warning
import-error? i/close
import-finish? i/tick
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
(when (= "application/zip" (:type file))
[:button {:on-click handle-edit-entry} i/pencil])
(when can-be-deleted?
[:button {:on-click handle-remove-entry} i/trash])]]
(cond
analyze-error?
[:div.error-message
(tr "dashboard.import.analyze-error")]
import-error?
[:div.error-message
(tr "dashboard.import.import-error")]
(and (not import-finish?) (some? progress))
[:div.progress-message (parse-progress-message progress)])
[: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})
(rx/delay-emit emit-delay)
(rx/subs
(fn [{:keys [uri data error type] :as msg}]
(log/debug :uri uri :data data :error error)
(if (some? error)
(swap! state update :files set-analyze-error uri)
(swap! state update :files set-analyze-result uri type data)))))))
import-files
(mf/use-callback
(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})
(rx/subs
(fn [{:keys [file-id status message errors] :as msg}]
(swap! state update :files update-status file-id status message errors))))))
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 #(and (= :ready (:status %)) (not (:deleted? %)))))]
(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))))
warning-files (->> @state :files (filter #(and (= (:status %) :import-finish) (d/not-empty? (:errors %)))) count)
success-files (->> @state :files (filter #(and (= (:status %) :import-finish) (empty? (:errors %)))) count)
pending-analysis? (> (->> @state :files (filter #(= (:status %) :analyzing)) count) 0)
pending-import? (> (->> @state :files (filter #(= (:status %) :importing)) count) 0)
files (->> (:files @state) (filterv (comp not :deleted?)))]
(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]
(wapi/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?))
(if (> warning-files 0)
[:div.feedback-banner.warning
[:div.icon i/msg-warning]
[:div.message (tr "dashboard.import.import-warning" warning-files success-files)]]
[:div.feedback-banner
[:div.icon i/checkbox-checked]
[:div.message (tr "dashboard.import.import-message" success-files)]]))
(for [file files]
(let [editing? (and (some? (:file-id file))
(= (:file-id file) (:editing @state)))]
[:& import-entry {:state state
:file file
:editing? editing?
:can-be-deleted? (> (count files) 1)}]))]
[:div.modal-footer
[:div.action-buttons
(when (or (= :analyzing (:status @state)) pending-import?)
[: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}])]]]]))