Files
penpot/frontend/src/app/main/data/dashboard.cljs
Andrey Antukh 0ab126748f 💄 Add format rule for code comments (#8211)
* 💄 Add format rule for code comments

* ⬆️ Update linter and formatter on devenv
2026-01-27 15:07:18 +01:00

955 lines
31 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) KALEIDOS INC
(ns app.main.data.dashboard
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.features :as cfeat]
[app.common.files.helpers :as cfh]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.types.project :refer [valid-project?]]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.constants :as mconst]
[app.main.data.common :as dcm]
[app.main.data.event :as ev]
[app.main.data.fonts :as df]
[app.main.data.helpers :as dsh]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.websocket :as dws]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.i18n :as i18n :refer [tr]]
[app.util.sse :as sse]
[beicon.v2.core :as rx]
[clojure.set :as set]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(log/set-level! :warn)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Initialization
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare fetch-projects)
(declare process-message)
(defn initialize
[team-id]
(assert (uuid? team-id) "expected uuid instance for `team-id`")
(ptk/reify ::initialize
ptk/WatchEvent
(watch [_ state stream]
(let [stopper (rx/filter (ptk/type? ::finalize) stream)
profile-id (:profile-id state)]
(->> (rx/merge
(rx/of (fetch-projects team-id)
(df/fetch-fonts team-id))
(->> stream
(rx/filter (ptk/type? ::dws/message))
(rx/map deref)
(rx/filter (fn [{:keys [topic] :as msg}]
(or (= topic uuid/zero)
(= topic profile-id))))
(rx/map process-message)))
(rx/take-until stopper))))))
(defn finalize
[team-id]
(ptk/data-event ::finalize {:team-id team-id}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Fetching (context aware: current team)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- EVENT: fetch-projects
(defn- projects-fetched
[projects]
(ptk/reify ::projects-fetched
ptk/UpdateEvent
(update [_ state]
(reduce (fn [state {:keys [id] :as project}]
;; Replace completely instead of merge to ensure deleted-at is removed
(assoc-in state [:projects id] project))
state
projects))))
(defn fetch-projects
[team-id]
(ptk/reify ::fetch-projects
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-projects {:team-id team-id})
(rx/map projects-fetched)))))
;; --- EVENT: search
(def ^:private schema:search-params
[:map {:closed true}
[:search-term [:maybe :string]]])
(def ^:private check-search-params
(sm/check-fn schema:search-params))
(defn search
[params]
(let [params (check-search-params params)]
(ptk/reify ::search
ptk/UpdateEvent
(update [_ state]
(dissoc state :search-result))
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
params (assoc params :team-id team-id)]
(->> (rp/cmd! :search-files params)
(rx/map (fn [result]
#(assoc % :search-result result)))))))))
;; --- EVENT: recent-files
(defn- recent-files-fetched
[files]
(ptk/reify ::recent-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [files (d/index-by :id files)]
(-> state
(assoc :recent-files files)
(update :files d/merge files))))))
(defn fetch-recent-files
([] (fetch-recent-files nil))
([team-id]
(ptk/reify ::fetch-recent-files
ptk/WatchEvent
(watch [_ state _]
(when-let [team-id (or team-id (:current-team-id state))]
(->> (rp/cmd! :get-team-recent-files {:team-id team-id})
(rx/map recent-files-fetched)))))))
;; --- EVENT: fetch-template-files
(defn builtin-templates-fetched
[libraries]
(ptk/reify ::libraries-fetched
ptk/UpdateEvent
(update [_ state]
(assoc state :builtin-templates libraries))))
(defn fetch-builtin-templates
[]
(ptk/reify ::fetch-builtin-templates
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-builtin-templates)
(rx/map builtin-templates-fetched)))))
;; --- EVENT: deleted-files
(defn- deleted-files-fetched
[files]
(ptk/reify ::deleted-files-fetched
ptk/UpdateEvent
(update [_ state]
(let [now (ct/now)
filtered-files (filterv (fn [file]
(let [will-be-deleted-at (:will-be-deleted-at file)]
(or (nil? will-be-deleted-at)
(ct/is-after? will-be-deleted-at now))))
files)
files (d/index-by :id filtered-files)]
(-> state
(assoc :deleted-files files)
(update :files d/merge files))))))
(defn fetch-deleted-files
([] (fetch-deleted-files nil))
([team-id]
(ptk/reify ::fetch-deleted-files
ptk/WatchEvent
(watch [_ state _]
(when-let [team-id (or team-id (:current-team-id state))]
(->> (rp/cmd! :get-team-deleted-files {:team-id team-id})
(rx/map deleted-files-fetched)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Selection
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn clear-selected-files
[]
(ptk/reify ::clear-file-select
ptk/UpdateEvent
(update [_ state]
(-> state
(dissoc :selected-files)
(dissoc :selected-project)
(update :dashboard-local dissoc :menu-open :menu-pos)))))
(defn toggle-file-select
[{:keys [id project-id] :as file}]
(ptk/reify ::toggle-file-select
ptk/UpdateEvent
(update [_ state]
(let [selected-project-id (get state :selected-project)]
(if (or (nil? selected-project-id)
(= selected-project-id project-id))
(-> state
(update :selected-files #(if (contains? % id) (disj % id) (conj % id)))
(assoc :selected-project project-id))
state)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Show grid menu
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn show-file-menu-with-position
[file-id pos]
(ptk/reify ::show-file-menu-with-position
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-local assoc
:menu-open true
:menu-pos pos
:file-id file-id))))
(defn show-file-menu
[]
(ptk/reify ::show-file-menu
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-local
assoc :menu-open true))))
(defn hide-file-menu
[]
(ptk/reify ::hide-file-menu
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-local
assoc :menu-open false))))
(defn start-edit-file-name
[file-id]
(ptk/reify ::start-edit-file-menu
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-local
assoc :edition true
:file-id file-id))))
(defn stop-edit-file-name
[]
(ptk/reify ::stop-edit-file-name
ptk/UpdateEvent
(update [_ state]
(update state :dashboard-local
assoc :edition false))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Modification
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; --- EVENT: create-project
(defn- project-created
[{:keys [id] :as project}]
(ptk/reify ::project-created
IDeref
(-deref [_] project)
ptk/UpdateEvent
(update [_ state]
(-> state
(assoc-in [:projects id] project)
(assoc-in [:dashboard-local :project-for-edit] id)))))
(defn create-project
[]
(ptk/reify ::create-project
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
projects (dsh/lookup-team-projects state team-id)
unames (cfh/get-used-names projects)
base-name (tr "dashboard.new-project-prefix")
name (cfh/generate-unique-name base-name unames :immediate-suffix? true)
team-id (:current-team-id state)
params {:name name
:team-id team-id}
{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}}
(meta params)]
(->> (rp/cmd! :create-project params)
(rx/tap on-success)
(rx/map project-created)
(rx/catch on-error))))))
;; --- EVENT: duplicate-project
(defn project-duplicated
[{:keys [id] :as project}]
(ptk/reify ::project-duplicated
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:projects id] project))))
(defn duplicate-project
[{:keys [id name] :as params}]
(dm/assert! (uuid? id))
(ptk/reify ::duplicate-project
ev/Event
(-data [_]
{:project-id id
:name name})
ptk/WatchEvent
(watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
projects (get state :projects)
unames (cfh/get-used-names projects)
suffix-fn (fn [copy-count]
(str/concat " "
(tr "dashboard.copy-suffix")
(when (> copy-count 1)
(str " " copy-count))))
new-name (cfh/generate-unique-name name unames :suffix-fn suffix-fn)]
(->> (rp/cmd! :duplicate-project {:project-id id :name new-name})
(rx/tap on-success)
(rx/map project-duplicated)
(rx/catch on-error))))))
(defn move-project
[{:keys [id team-id] :as params}]
(dm/assert! (uuid? id))
(dm/assert! (uuid? team-id))
(ptk/reify ::move-project
ev/Event
(-data [_]
{:id id
:team-id team-id})
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :move-project {:project-id id :team-id team-id})
(rx/tap on-success)
(rx/catch on-error))))))
(defn toggle-project-pin
[{:keys [id is-pinned] :as project}]
(ptk/reify ::toggle-project-pin
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:projects id :is-pinned] (not is-pinned)))
ptk/WatchEvent
(watch [_ state _]
(let [project (get-in state [:projects id])
params (select-keys project [:id :is-pinned :team-id])]
(->> (rp/cmd! :update-project-pin params)
(rx/ignore))))))
;; --- EVENT: rename-project
(defn rename-project
[{:keys [id name] :as params}]
(ptk/reify ::rename-project
ptk/UpdateEvent
(update [_ state]
(-> state
(update-in [:projects id :name] (constantly name))
(update :dashboard-local dissoc :project-for-edit)))
ptk/WatchEvent
(watch [_ _ _]
(let [params {:id id :name name}]
(->> (rp/cmd! :rename-project params)
(rx/ignore))))))
;; --- EVENT: delete-project
(defn delete-project
[{:keys [id] :as params}]
(ptk/reify ::delete-project
ptk/UpdateEvent
(update [_ state]
(update state :projects dissoc id))
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :delete-project {:id id})
(rx/ignore)))))
;; --- EVENT: delete-file
(defn file-deleted
[project-id]
(ptk/reify ::file-deleted
ptk/UpdateEvent
(update [_ state]
(update-in state [:projects project-id :count] dec))))
(defn delete-file
[{:keys [id project-id] :as params}]
(ptk/reify ::delete-file
ptk/UpdateEvent
(update [_ state]
(-> state
(d/update-when :files dissoc id)
(d/update-when :shared-files dissoc id)
(d/update-when :recent-files dissoc id)))
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :delete-file {:id id})
(rx/map (partial file-deleted project-id))))))
;; --- Rename File
(defn rename-file
[{:keys [id name] :as params}]
(ptk/reify ::rename-file
ev/Event
(-data [_]
{::ev/origin "dashboard"
:id id
:name name})
ptk/UpdateEvent
(update [_ state]
(-> state
(d/update-in-when [:files id :name] (constantly name))
(d/update-in-when [:shared-files id :name] (constantly name))
(d/update-in-when [:recent-files id :name] (constantly name))))
ptk/WatchEvent
(watch [_ _ _]
(let [params (select-keys params [:id :name])]
(->> (rp/cmd! :rename-file params)
(rx/ignore))))))
;; --- Set File shared
(defn set-file-shared
[{:keys [id is-shared] :as params}]
(ptk/reify ::set-file-shared
ev/Event
(-data [_]
{::ev/origin "dashboard"
:id id
:shared is-shared})
ptk/UpdateEvent
(update [_ state]
(-> state
(d/update-in-when [:files id :is-shared] (constantly is-shared))
(d/update-in-when [:recent-files id :is-shared] (constantly is-shared))
(cond-> (not is-shared)
(d/update-when :shared-files dissoc id))))
ptk/WatchEvent
(watch [_ _ _]
(let [params {:id id :is-shared is-shared}]
(rx/concat
(->> (rp/cmd! :set-file-shared params)
(rx/ignore))
(when is-shared
(->> (rp/cmd! :get-file-summary {:id id})
(rx/map (fn [summary]
(when (-> summary :variants :count pos?)
(ptk/event ::ev/event {::ev/name "set-file-variants-shared" ::ev/origin "dashboard"})))))))))))
(defn set-file-thumbnail
[file-id thumbnail-id]
(ptk/reify ::set-file-thumbnail
ptk/UpdateEvent
(update [_ state]
(letfn [(update-search-files [files]
(->> files
(mapv #(cond-> %
(= file-id (:id %))
(assoc :thumbnail-id thumbnail-id)))))]
(-> state
(d/update-in-when [:files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:recent-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-in-when [:deleted-files file-id] assoc :thumbnail-id thumbnail-id)
(d/update-when :dashboard-search-result update-search-files))))))
;; --- EVENT: create-file
(declare file-created)
(defn file-created
[{:keys [id project-id] :as file}]
(ptk/reify ::file-created
IDeref
(-deref [_] {:file-id id
:file-name (:name file)})
ptk/UpdateEvent
(update [_ state]
(let [file (dissoc file :data)]
(-> state
(assoc-in [:files id] file)
(assoc-in [:recent-files id] file)
(update-in [:projects project-id :count] inc))))))
(defn create-file
[{:keys [project-id name] :as params}]
(dm/assert! (uuid? project-id))
(ptk/reify ::create-file
ev/Event
(-data [_] {:project-id project-id})
ptk/WatchEvent
(watch [it state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}}
(meta params)
files (dsh/lookup-team-files state)
unames (cfh/get-used-names files)
base-name (tr "dashboard.new-file-prefix")
name (or name
(cfh/generate-unique-name base-name unames :immediate-suffix? true))
features (-> (get state :features)
(set/difference cfeat/frontend-only-features))
params (-> params
(assoc :name name)
(assoc :features features))]
(->> (rp/cmd! :create-file params)
(rx/tap on-success)
(rx/map #(with-meta (file-created %) (meta it)))
(rx/catch on-error))))))
;; --- EVENT: duplicate-file
(defn duplicate-file
[{:keys [id name] :as params}]
(dm/assert! (uuid? id))
(dm/assert! (string? name))
(ptk/reify ::duplicate-file
ptk/WatchEvent
(watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
unames (cfh/get-used-names (get state :files))
suffix-fn (fn [copy-count]
(str/concat " "
(tr "dashboard.copy-suffix")
(when (> copy-count 1)
(str " " copy-count))))
new-name (cfh/generate-unique-name name unames :suffix-fn suffix-fn)]
(->> (rp/cmd! :duplicate-file {:file-id id :name new-name})
(rx/tap on-success)
(rx/map file-created)
(rx/catch on-error))))))
;; --- EVENT: move-files
(defn move-files
[{:keys [ids project-id] :as params}]
(assert (uuid? project-id))
(assert (sm/check-set-of-uuid ids))
(ptk/reify ::move-files
ev/Event
(-data [_]
{:num-files (count ids)
:project-id project-id})
ptk/UpdateEvent
(update [_ state]
(let [origin-project (get-in state [:files (first ids) :project-id])
update-project (fn [project delta op]
(-> project
(update :count #(op % (count ids)))
(assoc :modified-at (ct/in-future {:milliseconds delta}))))]
(-> state
(d/update-in-when [:projects origin-project] update-project 0 -)
(d/update-in-when [:projects project-id] update-project 10 +))))
ptk/WatchEvent
(watch [_ _ _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)]
(->> (rp/cmd! :move-files {:ids ids :project-id project-id})
(rx/tap on-success)
(rx/catch on-error))))))
;; --- EVENT: clone-template
(defn clone-template
[{:keys [template-id project-id] :as params}]
(ptk/reify ::clone-template
ev/Event
(-data [_]
{:template-id template-id})
ptk/WatchEvent
(watch [_ state _]
(let [{:keys [on-success on-error]
:or {on-success identity
on-error rx/throw}} (meta params)
project-id (or project-id (:current-project-id state))]
(->> (rp/cmd! ::sse/clone-template {:project-id project-id
:template-id template-id})
(rx/tap (fn [event]
(let [payload (sse/get-payload event)
type (sse/get-type event)]
(if (= type "progress")
(log/dbg :hint "clone-template: progress" :section (:section payload) :name (:name payload))
(log/dbg :hint "clone-template: end")))))
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/tap on-success)
(rx/catch on-error))))))
(defn create-element
[]
(ptk/reify ::create-element
ptk/WatchEvent
(watch [_ state _]
(let [team-id (:current-team-id state)
route (:route state)
pparams (:path-params route)
in-project? (contains? pparams :project-id)
name (if in-project?
(let [files (dsh/lookup-team-files state team-id)
unames (cfh/get-used-names files)]
(cfh/generate-unique-name (tr "dashboard.new-file-prefix") unames :immediate-suffix? true))
(let [projects (dsh/lookup-team-projects state team-id)
unames (cfh/get-used-names projects)]
(cfh/generate-unique-name (tr "dashboard.new-project-prefix") unames :immediate-suffix? true)))
params (if in-project?
{:project-id (:project-id pparams)
:name name}
{:name name
:team-id team-id})
action-name (if in-project? :create-file :create-project)
action (if in-project? file-created project-created)
can-edit? (dm/get-in state [:teams team-id :permissions :can-edit])]
(when can-edit?
(->> (rp/cmd! action-name params)
(rx/map action)))))))
(defn open-selected-file
[]
(ptk/reify ::open-selected-file
ptk/WatchEvent
(watch [_ state _]
(let [[file-id :as files] (get state :selected-files)]
(if (= 1 (count files))
(rx/of (dcm/go-to-workspace :file-id file-id))
(rx/empty))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Notifications
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- handle-change-team-role
[params]
(ptk/reify ::handle-change-team-role
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dcm/change-team-role params)
(modal/hide)))))
(defn handle-change-team-org
[{:keys [team-id organization-id organization-name]}]
(ptk/reify ::handle-change-team-org
ptk/UpdateEvent
(update [_ state]
(if (contains? cf/flags :nitrate)
(d/update-in-when state [:teams team-id] assoc
:organization-id organization-id
:organization-name organization-name)
state))))
(defn- process-message
[{:keys [type] :as msg}]
(case type
:notification (dcm/handle-notification msg)
:team-role-change (handle-change-team-role msg)
:team-membership-change (dcm/team-membership-change msg)
:team-org-change (handle-change-team-org msg)
nil))
;; --- Delete files immediately
(defn- delete-files
[{:keys [team-id ids on-success on-error]}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(assert (fn? on-success))
(assert (fn? on-error))
(ptk/reify ::delete-files
ptk/WatchEvent
(watch [_ _ _]
(let [progress-hint #(tr "dashboard.progress-notification.deleting-files")
slow-hint #(tr "dashboard.progress-notification.slow-delete")
stream (->> (rp/cmd! ::sse/permanently-delete-team-files {:team-id team-id :ids ids})
(rx/share))]
(rx/merge
(rx/of (dcm/initialize-progress
{:slow-progress-threshold
mconst/default-slow-progress-threshold
:total (count ids)
:hints {:progress progress-hint
:slow slow-hint}}))
(->> stream
(rx/filter sse/progress?)
(rx/mapcat (fn [event]
(if-let [payload (sse/get-payload event)]
(let [{:keys [index total]} payload]
(if (and index total)
(rx/of (dcm/update-progress {:index index :total total}))
(rx/empty)))
(rx/empty))))
(rx/catch rx/empty))
(->> stream
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/merge-map (fn [_]
(rx/concat
(rx/of (dcm/clear-progress)
(fetch-projects team-id)
(fetch-deleted-files team-id)
(fetch-projects team-id))
(on-success))))
(rx/catch (fn [error]
(rx/concat
(rx/of (dcm/clear-progress))
(on-error error))))))))))
(defn delete-files-immediately
[{:keys [team-id ids] :as params}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(ptk/reify ::delete-files-immediately
ptk/WatchEvent
(watch [_ state _]
(let [deleted-files
(get state :deleted-files)
on-success
(fn []
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/success (tr "dashboard.delete-success-notification" fname))))
(rx/of (ntf/success (tr "dashboard.delete-files-success-notification" (count ids))))))
on-error
#(rx/of (ntf/error (tr "dashboard.errors.error-on-delete-files")))]
(rx/of (ev/event
{::ev/name "delete-files"
::ev/origin "dashboard:trash"
:team-id team-id
:num-files (count ids)})
(delete-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
(defn delete-project-immediately
[{:keys [team-id id name] :as project}]
(assert (valid-project? project))
(ptk/reify ::delete-project-immediately
ptk/WatchEvent
(watch [_ state _]
(let [ids
(reduce-kv (fn [acc file-id file]
(if (= (:project-id file) id)
(conj acc file-id)
acc))
#{}
(get state :deleted-files))
on-success
#(rx/of (ntf/success (tr "dashboard.delete-success-notification" name)))
on-error
#(rx/of (ntf/error (tr "dashboard.errors.error-on-delete-project" name)))]
(rx/of (ev/event
{::ev/name "delete-files"
::ev/origin "dashboard:trash"
:team-id team-id
:project-id id
:num-files (count ids)})
(delete-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
;; --- Restore deleted files immediately
(defn- restore-files
[{:keys [team-id ids on-success on-error]}]
(assert (uuid? team-id))
(assert (set? ids))
(assert (every? uuid? ids))
(assert (fn? on-success))
(assert (fn? on-error))
(ptk/reify ::restore-files
ptk/WatchEvent
(watch [_ _ _]
(let [progress-hint #(tr "dashboard.progress-notification.restoring-files")
slow-hint #(tr "dashboard.progress-notification.slow-restore")]
(rx/merge
(rx/of (dcm/initialize-progress
{:slow-progress-threshold
mconst/default-slow-progress-threshold
:total (count ids)
:hints {:progress progress-hint
:slow slow-hint}}))
(let [stream (->> (rp/cmd! ::sse/restore-deleted-team-files {:team-id team-id :ids ids})
(rx/share))]
(rx/merge
(->> stream
(rx/filter sse/progress?)
(rx/mapcat (fn [event]
(if-let [payload (sse/get-payload event)]
(let [{:keys [index total]} payload]
(if (and index total)
(rx/of (dcm/update-progress {:index index :total total}))
(rx/empty)))
(rx/empty))))
(rx/catch rx/empty))
(->> stream
(rx/filter sse/end-of-stream?)
(rx/map sse/get-payload)
(rx/mapcat (fn [_]
(rx/concat
(rx/of (dcm/clear-progress)
;; (ntf/success (tr "dashboard.restore-success-notification"))
(fetch-projects team-id)
(fetch-deleted-files team-id)
(fetch-projects team-id))
(on-success))))
(rx/catch (fn [error]
(rx/concat
(rx/of (dcm/clear-progress))
(on-error error))))))))))))
(defn restore-files-immediately
[{:keys [team-id ids]}]
(assert (uuid? team-id))
(assert (set? ids))
(ptk/reify ::restore-files-immediately
ptk/WatchEvent
(watch [_ state _]
(let [deleted-files
(get state :deleted-files)
on-success
(fn []
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/success (tr "dashboard.restore-success-notification" fname))))
(rx/of (ntf/success (tr "dashboard.restore-files-success-notification" (count ids))))))
on-error
(fn [_cause]
(if (= 1 (count ids))
(let [fname (get-in deleted-files [(first ids) :name])]
(rx/of (ntf/error (tr "dashboard.errors.error-on-restore-file" fname))))
(rx/of (ntf/error (tr "dashboard.errors.error-on-restore-files")))))]
(rx/of (ev/event
{::ev/name "restore-files"
::ev/origin "dashboard:trash"
:team-id team-id
:num-files (count ids)})
(restore-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))
(defn restore-project-immediately
[{:keys [team-id id name] :as project}]
(assert (valid-project? project))
(ptk/reify ::restore-project-immediately
ptk/WatchEvent
(watch [_ state _]
(let [ids
(reduce-kv (fn [acc file-id file]
(if (= (:project-id file) id)
(conj acc file-id)
acc))
#{}
(get state :deleted-files))
on-success
#(st/emit! (ntf/success (tr "dashboard.restore-success-notification" name)))
on-error
#(st/emit! (ntf/error (tr "dashboard.errors.error-on-restoring-project" name)))]
(rx/of (ev/event
{::ev/name "restore-files"
::ev/origin "dashboard:trash"
:team-id team-id
:project-id id
:num-files (count ids)})
(restore-files
{:team-id team-id
:ids ids
:on-success on-success
:on-error on-error}))))))