mirror of
https://github.com/penpot/penpot.git
synced 2026-05-31 20:58:08 +00:00
374 lines
13 KiB
Clojure
374 lines
13 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.ui.workspace.sidebar.versions
|
|
(:require-macros [app.main.style :as stl])
|
|
(:require
|
|
[app.common.data.macros :as dm]
|
|
[app.common.time :as ct]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cfg]
|
|
[app.main.data.notifications :as ntf]
|
|
[app.main.data.workspace.versions :as dwv]
|
|
[app.main.refs :as refs]
|
|
[app.main.store :as st]
|
|
[app.main.ui.components.dropdown :refer [dropdown]]
|
|
[app.main.ui.components.select :refer [select]]
|
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
|
[app.main.ui.ds.product.autosaved-milestone :refer [autosaved-milestone*]]
|
|
[app.main.ui.ds.product.cta :refer [cta*]]
|
|
[app.main.ui.ds.product.user-milestone :refer [user-milestone*]]
|
|
[app.util.dom :as dom]
|
|
[app.util.i18n :as i18n :refer [tr]]
|
|
[app.util.keyboard :as kbd]
|
|
[app.util.time :as dt]
|
|
[cuerdas.core :as str]
|
|
[okulary.core :as l]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(def versions
|
|
(l/derived :workspace-versions st/state))
|
|
|
|
(defn get-versions-stored-days
|
|
[team]
|
|
(let [subscription-name (-> team :subscription :type)]
|
|
(cond
|
|
(= subscription-name "unlimited") 30
|
|
(= subscription-name "enterprise") 90
|
|
:else 7)))
|
|
|
|
(defn group-snapshots
|
|
[data]
|
|
(->> (concat
|
|
(->> data
|
|
(filterv #(= "user" (:created-by %)))
|
|
(map #(assoc % :type :version)))
|
|
(->> data
|
|
(filterv #(= "system" (:created-by %)))
|
|
(group-by #(.toISODate ^js (:created-at %)))
|
|
(map (fn [[day entries]]
|
|
{:type :snapshot
|
|
:created-at (ct/parse-instant day)
|
|
:snapshots entries}))))
|
|
(sort-by :created-at)
|
|
(reverse)))
|
|
|
|
(mf/defc version-entry
|
|
[{:keys [entry profile on-restore-version on-delete-version on-rename-version editing?]}]
|
|
(let [show-menu? (mf/use-state false)
|
|
|
|
handle-open-menu
|
|
(mf/use-fn
|
|
(fn []
|
|
(reset! show-menu? true)))
|
|
|
|
handle-close-menu
|
|
(mf/use-fn
|
|
(fn []
|
|
(reset! show-menu? false)))
|
|
|
|
handle-rename-version
|
|
(mf/use-fn
|
|
(mf/deps entry)
|
|
(fn []
|
|
(st/emit! (dwv/update-version-state {:editing (:id entry)}))))
|
|
|
|
handle-restore-version
|
|
(mf/use-fn
|
|
(mf/deps entry on-restore-version)
|
|
(fn []
|
|
(when on-restore-version
|
|
(on-restore-version (:id entry)))))
|
|
|
|
handle-delete-version
|
|
(mf/use-callback
|
|
(mf/deps entry on-delete-version)
|
|
(fn []
|
|
(when on-delete-version
|
|
(on-delete-version (:id entry)))))
|
|
|
|
handle-name-input-focus
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(dom/select-text! (dom/get-target event))))
|
|
|
|
handle-name-input-blur
|
|
(mf/use-fn
|
|
(mf/deps entry on-rename-version)
|
|
(fn [event]
|
|
(let [label (str/trim (dom/get-target-val event))]
|
|
(when (and (not (str/empty? label))
|
|
(some? on-rename-version))
|
|
(on-rename-version (:id entry) label))
|
|
(st/emit! (dwv/update-version-state {:editing nil})))))
|
|
|
|
handle-name-input-key-down
|
|
(mf/use-fn
|
|
(mf/deps handle-name-input-blur)
|
|
(fn [event]
|
|
(cond
|
|
(kbd/enter? event)
|
|
(handle-name-input-blur event)
|
|
|
|
(kbd/esc? event)
|
|
(st/emit! (dwv/update-version-state {:editing nil})))))]
|
|
|
|
[:li {:class (stl/css :version-entry-wrap)}
|
|
[:> user-milestone* {:label (:label entry)
|
|
:user #js {:name (:fullname profile)
|
|
:avatar (cfg/resolve-profile-photo-url profile)
|
|
:color (:color profile)}
|
|
:editing editing?
|
|
:date (:created-at entry)
|
|
:onOpenMenu handle-open-menu
|
|
:onFocusInput handle-name-input-focus
|
|
:onBlurInput handle-name-input-blur
|
|
:onKeyDownInput handle-name-input-key-down}]
|
|
|
|
[:& dropdown {:show @show-menu? :on-close handle-close-menu}
|
|
[:ul {:class (stl/css :version-options-dropdown)}
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:on-click handle-rename-version} (tr "labels.rename")]
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:on-click handle-restore-version} (tr "labels.restore")]
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:on-click handle-delete-version} (tr "labels.delete")]]]]))
|
|
|
|
(mf/defc snapshot-entry
|
|
[{:keys [index is-expanded entry on-toggle-expand on-pin-snapshot on-restore-snapshot]}]
|
|
|
|
(let [open-menu (mf/use-state nil)
|
|
entry-ref (mf/use-ref nil)
|
|
|
|
handle-toggle-expand
|
|
(mf/use-fn
|
|
(mf/deps index on-toggle-expand)
|
|
(fn []
|
|
(when on-toggle-expand
|
|
(on-toggle-expand index))))
|
|
|
|
handle-pin-snapshot
|
|
(mf/use-fn
|
|
(mf/deps on-pin-snapshot)
|
|
(fn [event]
|
|
(let [node (dom/get-current-target event)
|
|
id (-> (dom/get-data node "id") uuid/parse)]
|
|
(when on-pin-snapshot (on-pin-snapshot id)))))
|
|
|
|
handle-restore-snapshot
|
|
(mf/use-fn
|
|
(mf/deps on-restore-snapshot)
|
|
(fn [event]
|
|
(let [node (dom/get-current-target event)
|
|
id (-> (dom/get-data node "id") uuid/parse)]
|
|
(when on-restore-snapshot (on-restore-snapshot id)))))
|
|
|
|
|
|
handle-open-snapshot-menu
|
|
(mf/use-fn
|
|
(mf/deps entry)
|
|
(fn [event index]
|
|
(let [snapshot (nth (:snapshots entry) index)
|
|
current-bb (-> entry-ref mf/ref-val dom/get-bounding-rect :top)
|
|
target-bb (-> event dom/get-target dom/get-bounding-rect :top)
|
|
offset (+ (- target-bb current-bb) 32)]
|
|
(swap! open-menu assoc
|
|
:snapshot (:id snapshot)
|
|
:offset offset))))]
|
|
|
|
[:li {:ref entry-ref :class (stl/css :version-entry-wrap)}
|
|
[:> autosaved-milestone*
|
|
{:label (tr "workspace.versions.autosaved.version"
|
|
(dt/format (:created-at entry) :date-full))
|
|
:autosavedMessage (tr "workspace.versions.autosaved.entry" (count (:snapshots entry)))
|
|
:snapshots (mapv :created-at (:snapshots entry))
|
|
:versionToggled is-expanded
|
|
:onClickSnapshotMenu handle-open-snapshot-menu
|
|
:onToggleExpandSnapshots handle-toggle-expand}]
|
|
|
|
[:& dropdown {:show (some? @open-menu)
|
|
:on-close #(reset! open-menu nil)}
|
|
[:ul {:class (stl/css :version-options-dropdown)
|
|
:style {"--offset" (dm/str (:offset @open-menu) "px")}}
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:data-id (dm/str (:snapshot @open-menu))
|
|
:on-click handle-restore-snapshot}
|
|
(tr "workspace.versions.button.restore")]
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:data-id (dm/str (:snapshot @open-menu))
|
|
:on-click handle-pin-snapshot}
|
|
(tr "workspace.versions.button.pin")]]]]))
|
|
|
|
(mf/defc versions-toolbox*
|
|
[]
|
|
(let [profiles (mf/deref refs/profiles)
|
|
profile (mf/deref refs/profile)
|
|
team (mf/deref refs/team)
|
|
|
|
expanded (mf/use-state #{})
|
|
|
|
{:keys [status data editing]}
|
|
(mf/deref versions)
|
|
|
|
;; Store users that have a version
|
|
data-users
|
|
(mf/use-memo
|
|
(mf/deps data)
|
|
(fn []
|
|
(into #{} (keep (fn [{:keys [created-by profile-id]}]
|
|
(when (= "user" created-by) profile-id))) data)))
|
|
data
|
|
(mf/use-memo
|
|
(mf/deps @versions)
|
|
(fn []
|
|
(->> data
|
|
(filter #(or (not (:filter @versions))
|
|
(and
|
|
(= "user" (:created-by %))
|
|
(= (:filter @versions) (:profile-id %)))))
|
|
(group-snapshots))))
|
|
|
|
handle-create-version
|
|
(mf/use-fn
|
|
(fn []
|
|
(st/emit! (dwv/create-version))))
|
|
|
|
handle-toggle-expand
|
|
(mf/use-fn
|
|
(fn [id]
|
|
(swap! expanded
|
|
(fn [expanded]
|
|
(let [has-element? (contains? expanded id)]
|
|
(cond-> expanded
|
|
has-element? (disj id)
|
|
(not has-element?) (conj id)))))))
|
|
|
|
handle-rename-version
|
|
(mf/use-fn
|
|
(fn [id label]
|
|
(st/emit! (dwv/rename-version id label))))
|
|
|
|
|
|
handle-restore-version
|
|
(mf/use-fn
|
|
(fn [origin id]
|
|
(st/emit!
|
|
(ntf/dialog
|
|
:content (tr "workspace.versions.restore-warning")
|
|
:controls :inline-actions
|
|
:cancel {:label (tr "workspace.updates.dismiss")
|
|
:callback #(st/emit! (ntf/hide))}
|
|
:accept {:label (tr "labels.restore")
|
|
:callback #(st/emit! (dwv/restore-version id origin))}
|
|
:tag :restore-dialog))))
|
|
|
|
handle-restore-version-pinned
|
|
(mf/use-fn
|
|
(mf/deps handle-restore-version)
|
|
(fn [id]
|
|
(handle-restore-version :version id)))
|
|
|
|
handle-restore-version-snapshot
|
|
(mf/use-fn
|
|
(mf/deps handle-restore-version)
|
|
(fn [id]
|
|
(handle-restore-version :snapshot id)))
|
|
|
|
handle-delete-version
|
|
(mf/use-fn
|
|
(fn [id]
|
|
(st/emit! (dwv/delete-version id))))
|
|
|
|
handle-pin-version
|
|
(mf/use-fn
|
|
(fn [id]
|
|
(st/emit! (dwv/pin-version id))))
|
|
|
|
handle-change-filter
|
|
(mf/use-fn
|
|
(fn [filter]
|
|
(cond
|
|
(= :all filter)
|
|
(st/emit! (dwv/update-version-state {:filter nil}))
|
|
|
|
(= :own filter)
|
|
(st/emit! (dwv/update-version-state {:filter (:id profile)}))
|
|
|
|
:else
|
|
(st/emit! (dwv/update-version-state {:filter filter})))))]
|
|
|
|
(mf/with-effect []
|
|
(st/emit! (dwv/init-version-state)))
|
|
|
|
[:div {:class (stl/css :version-toolbox)}
|
|
[:& select
|
|
{:default-value :all
|
|
:aria-label (tr "workspace.versions.filter.label")
|
|
:options (into [{:value :all :label (tr "workspace.versions.filter.all")}
|
|
{:value :own :label (tr "workspace.versions.filter.mine")}]
|
|
(->> data-users
|
|
(keep
|
|
(fn [id]
|
|
(let [{:keys [fullname]} (get profiles id)]
|
|
(when (not= id (:id profile))
|
|
{:value id :label (tr "workspace.versions.filter.user" fullname)}))))))
|
|
:on-change handle-change-filter}]
|
|
|
|
(cond
|
|
(= status :loading)
|
|
[:div {:class (stl/css :versions-entry-empty)}
|
|
[:div {:class (stl/css :versions-entry-empty-msg)} (tr "workspace.versions.loading")]]
|
|
|
|
(= status :loaded)
|
|
[:*
|
|
[:div {:class (stl/css :version-save-version)}
|
|
(tr "workspace.versions.button.save")
|
|
[:> icon-button* {:variant "ghost"
|
|
:aria-label (tr "workspace.versions.button.save")
|
|
:on-click handle-create-version
|
|
:icon "pin"}]]
|
|
|
|
(if (empty? data)
|
|
[:div {:class (stl/css :versions-entry-empty)}
|
|
[:div {:class (stl/css :versions-entry-empty-icon)} [:> i/icon* {:icon-id i/history}]]
|
|
[:div {:class (stl/css :versions-entry-empty-msg)} (tr "workspace.versions.empty")]]
|
|
|
|
[:ul {:class (stl/css :versions-entries)}
|
|
(for [[idx-entry entry] (->> data (map-indexed vector))]
|
|
(case (:type entry)
|
|
:version
|
|
[:& version-entry {:key idx-entry
|
|
:entry entry
|
|
:editing? (= (:id entry) editing)
|
|
:profile (get profiles (:profile-id entry))
|
|
:on-rename-version handle-rename-version
|
|
:on-restore-version handle-restore-version-pinned
|
|
:on-delete-version handle-delete-version}]
|
|
|
|
:snapshot
|
|
[:& snapshot-entry {:key idx-entry
|
|
:index idx-entry
|
|
:entry entry
|
|
:is-expanded (contains? @expanded idx-entry)
|
|
:on-toggle-expand handle-toggle-expand
|
|
:on-restore-snapshot handle-restore-version-snapshot
|
|
:on-pin-snapshot handle-pin-version}]
|
|
|
|
nil))])
|
|
|
|
[:> cta* {:title (tr "workspace.versions.warning.text" (get-versions-stored-days team))}
|
|
[:> i18n/tr-html*
|
|
{:tag-name "div"
|
|
:class (stl/css :cta)
|
|
:content (tr "workspace.versions.warning.subtext"
|
|
"mailto:support@penpot.app")}]]])]))
|