mirror of
https://github.com/penpot/penpot.git
synced 2026-08-31 17:19:11 +00:00
* ♻️ Rename old icons as deprecated * ♻️ Take icons from the namespace for the icon component * ♻️ Take icons from the namespace for the icon-button component
434 lines
15 KiB
Clojure
434 lines
15 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.dashboard.subscription :refer [get-subscription-type]]
|
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
|
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
|
[app.main.ui.ds.product.cta :refer [cta*]]
|
|
[app.main.ui.ds.product.milestone :refer [milestone*]]
|
|
[app.main.ui.ds.product.milestone-group :refer [milestone-group*]]
|
|
[app.util.dom :as dom]
|
|
[app.util.i18n :as i18n :refer [tr]]
|
|
[app.util.keyboard :as kbd]
|
|
[cuerdas.core :as str]
|
|
[lambdaisland.uri :as u]
|
|
[okulary.core :as l]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(def ^:private versions
|
|
(l/derived :workspace-versions st/state))
|
|
|
|
(defn- get-versions-stored-days
|
|
[team]
|
|
(let [subscription-type (get-subscription-type (:subscription team))]
|
|
(cond
|
|
(= subscription-type "unlimited") 30
|
|
(= subscription-type "enterprise") 90
|
|
:else 7)))
|
|
|
|
(defn- get-versions-warning-subtext
|
|
[team]
|
|
(let [subscription-type (get-subscription-type (:subscription team))
|
|
is-owner? (-> team :permissions :is-owner)
|
|
email-owner (:email (some #(when (:is-owner %) %) (:members team)))
|
|
support-email "support@penpot.app"
|
|
go-to-subscription (dm/str (u/join cfg/public-uri "#/settings/subscriptions"))]
|
|
|
|
(if (contains? cfg/flags :subscriptions)
|
|
(if is-owner?
|
|
(if (= "enterprise" subscription-type)
|
|
(tr "subscription.workspace.versions.warning.enterprise.subtext-owner" support-email support-email)
|
|
(tr "subscription.workspace.versions.warning.subtext-owner" go-to-subscription))
|
|
(tr "subscription.workspace.versions.warning.subtext-member" email-owner email-owner))
|
|
(tr "workspace.versions.warning.subtext" support-email))))
|
|
|
|
(defn- group-snapshots
|
|
[data]
|
|
(->> (concat
|
|
(->> data
|
|
(filter #(= "user" (:created-by %)))
|
|
(map #(assoc % :type :version)))
|
|
(->> data
|
|
(filter #(= "system" (:created-by %)))
|
|
(group-by #(ct/format-inst (:created-at %) :iso-date))
|
|
(map (fn [[day entries]]
|
|
{:type :snapshot
|
|
:created-at (ct/inst day)
|
|
:snapshots entries}))))
|
|
(sort-by :created-at)
|
|
(map-indexed (fn [index item]
|
|
(assoc item :index index)))
|
|
(reverse)))
|
|
|
|
(defn- open-restore-version-dialog
|
|
[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)))
|
|
|
|
(mf/defc version-entry*
|
|
{::mf/private true}
|
|
[{:keys [entry current-profile on-restore on-delete on-rename on-lock on-unlock on-edit on-cancel-edit is-editing]}]
|
|
(let [show-menu? (mf/use-state false)
|
|
profiles (mf/deref refs/profiles)
|
|
|
|
created-by (get profiles (:profile-id entry))
|
|
|
|
on-open-menu
|
|
(mf/use-fn #(reset! show-menu? true))
|
|
|
|
on-close-menu
|
|
(mf/use-fn #(reset! show-menu? false))
|
|
|
|
on-edit
|
|
(mf/use-fn
|
|
(mf/deps on-edit entry)
|
|
(fn [event]
|
|
(on-edit (:id entry) event)))
|
|
|
|
on-restore
|
|
(mf/use-fn
|
|
(mf/deps entry on-restore)
|
|
(fn []
|
|
(when (fn? on-restore)
|
|
(on-restore (:id entry)))))
|
|
|
|
on-delete
|
|
(mf/use-callback
|
|
(mf/deps entry on-delete)
|
|
(fn [event]
|
|
(when (fn? on-delete)
|
|
(on-delete (:id entry) event))))
|
|
|
|
on-lock
|
|
(mf/use-callback
|
|
(mf/deps entry on-lock)
|
|
(fn []
|
|
(when on-lock
|
|
(on-lock (:id entry)))))
|
|
|
|
on-unlock
|
|
(mf/use-callback
|
|
(mf/deps entry on-unlock)
|
|
(fn []
|
|
(when on-unlock
|
|
(on-unlock (:id entry)))))
|
|
|
|
on-name-input-focus
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(dom/select-text! (dom/get-target event))))
|
|
|
|
on-name-input-blur
|
|
(mf/use-fn
|
|
(mf/deps entry on-rename on-cancel-edit)
|
|
(fn [event]
|
|
(let [label (str/trim (dom/get-target-val event))]
|
|
(if (and (not (str/empty? label))
|
|
(fn? on-rename))
|
|
(on-rename (:id entry) label event)
|
|
(on-cancel-edit (:id entry) event)))))
|
|
|
|
on-name-input-key-down
|
|
(mf/use-fn
|
|
(mf/deps entry on-cancel-edit on-name-input-blur)
|
|
(fn [event]
|
|
(cond
|
|
(kbd/enter? event)
|
|
(on-name-input-blur event)
|
|
|
|
(kbd/esc? event)
|
|
(when (fn? on-cancel-edit)
|
|
(on-cancel-edit (:id entry) event)))))]
|
|
|
|
[:li {:class (stl/css :version-entry-wrap)}
|
|
[:> milestone* {:label (:label entry)
|
|
:profile created-by
|
|
:editing is-editing
|
|
:created-at (:created-at entry)
|
|
:locked (some? (:locked-by entry))
|
|
:on-open-menu on-open-menu
|
|
:on-focus-input on-name-input-focus
|
|
:on-blur-input on-name-input-blur
|
|
:on-key-down-input on-name-input-key-down}]
|
|
|
|
[:& dropdown {:show @show-menu?
|
|
:on-close on-close-menu}
|
|
(let [current-user-id (:id current-profile)
|
|
locked-by-id (:locked-by entry)
|
|
im-the-owner? (= current-user-id (:id created-by))
|
|
is-locked-by-me? (= current-user-id locked-by-id)
|
|
is-locked? (some? locked-by-id)
|
|
can-delete? (or (not is-locked?)
|
|
(and is-locked?
|
|
is-locked-by-me?))]
|
|
[:ul {:class (stl/css :version-options-dropdown)}
|
|
(when im-the-owner?
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:on-click on-edit}
|
|
(tr "labels.rename")])
|
|
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:on-click on-restore}
|
|
(tr "labels.restore")]
|
|
|
|
(cond
|
|
is-locked-by-me?
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:on-click on-unlock}
|
|
(tr "labels.unlock")]
|
|
|
|
(and im-the-owner? (not is-locked?))
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:on-click on-lock}
|
|
(tr "labels.lock")])
|
|
|
|
(when can-delete?
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:on-click on-delete}
|
|
(tr "labels.delete")])])]]))
|
|
|
|
(mf/defc snapshot-entry*
|
|
[{:keys [entry on-pin-snapshot on-restore-snapshot]}]
|
|
|
|
(let [open-menu* (mf/use-state nil)
|
|
entry-ref (mf/use-ref nil)
|
|
|
|
on-pin-snapshot
|
|
(mf/use-fn
|
|
(mf/deps on-pin-snapshot)
|
|
(fn [event]
|
|
(let [node (dom/get-current-target event)
|
|
id (-> node
|
|
(dom/get-data "id")
|
|
(uuid/parse))]
|
|
(when (fn? on-pin-snapshot)
|
|
(on-pin-snapshot id event)))))
|
|
|
|
on-restore-snapshot
|
|
(mf/use-fn
|
|
(mf/deps on-restore-snapshot)
|
|
(fn [event]
|
|
(let [node (dom/get-current-target event)
|
|
id (-> node
|
|
(dom/get-data "id")
|
|
(uuid/parse))]
|
|
(when (fn? on-restore-snapshot)
|
|
(on-restore-snapshot id event)))))
|
|
|
|
on-open-snapshot-menu
|
|
(mf/use-fn
|
|
(mf/deps entry)
|
|
(fn [index event]
|
|
(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)}
|
|
[:> milestone-group*
|
|
{:label (tr "workspace.versions.autosaved.version"
|
|
(ct/format-inst (:created-at entry) :localized-date))
|
|
:snapshots (mapv :created-at (:snapshots entry))
|
|
:on-menu-click on-open-snapshot-menu}]
|
|
|
|
[:& 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 on-restore-snapshot}
|
|
(tr "workspace.versions.button.restore")]
|
|
[:li {:class (stl/css :menu-option)
|
|
:role "button"
|
|
:data-id (dm/str (:snapshot @open-menu*))
|
|
:on-click on-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)
|
|
|
|
{:keys [status data editing] :as state}
|
|
(mf/deref versions)
|
|
|
|
users
|
|
(mf/with-memo [data]
|
|
(into #{}
|
|
(keep (fn [{:keys [created-by profile-id]}]
|
|
(when (= "user" created-by)
|
|
profile-id)))
|
|
data))
|
|
|
|
entries
|
|
(mf/with-memo [state]
|
|
(->> (:data state)
|
|
(filter #(or (not (:filter state))
|
|
(and (= "user" (:created-by %))
|
|
(= (:filter state) (:profile-id %)))))
|
|
(group-snapshots)))
|
|
|
|
on-create-version
|
|
(mf/use-fn
|
|
(fn [] (st/emit! (dwv/create-version))))
|
|
|
|
on-edit-version
|
|
(mf/use-fn
|
|
(fn [id _event]
|
|
(st/emit! (dwv/update-versions-state {:editing id}))))
|
|
|
|
on-cancel-version-edition
|
|
(mf/use-fn
|
|
(fn [_id _event]
|
|
(st/emit! (dwv/update-versions-state {:editing nil}))))
|
|
|
|
on-rename-version
|
|
(mf/use-fn
|
|
(fn [id label]
|
|
(st/emit! (dwv/rename-version id label))))
|
|
|
|
on-restore-version
|
|
(mf/use-fn
|
|
(fn [id _event]
|
|
(open-restore-version-dialog :version id)))
|
|
|
|
on-restore-snapshot
|
|
(mf/use-fn
|
|
(fn [id _event]
|
|
(open-restore-version-dialog :snapshot id)))
|
|
|
|
on-delete-version
|
|
(mf/use-fn
|
|
(fn [id]
|
|
(st/emit! (dwv/delete-version id))))
|
|
|
|
on-pin-version
|
|
(mf/use-fn
|
|
(fn [id] (st/emit! (dwv/pin-version id))))
|
|
|
|
on-lock-version
|
|
(mf/use-fn
|
|
(fn [id]
|
|
(st/emit! (dwv/lock-version id))))
|
|
|
|
on-unlock-version
|
|
(mf/use-fn
|
|
(fn [id]
|
|
(st/emit! (dwv/unlock-version id))))
|
|
|
|
on-change-filter
|
|
(mf/use-fn
|
|
(fn [filter]
|
|
(cond
|
|
(= :all filter)
|
|
(st/emit! (dwv/update-versions-state {:filter nil}))
|
|
|
|
(= :own filter)
|
|
(st/emit! (dwv/update-versions-state {:filter (:id profile)}))
|
|
|
|
:else
|
|
(st/emit! (dwv/update-versions-state {:filter filter})))))
|
|
|
|
options
|
|
(mf/with-memo [users profile]
|
|
(let [current-profile-id (get profile :id)]
|
|
(into [{:value :all :label (tr "workspace.versions.filter.all")}
|
|
{:value :own :label (tr "workspace.versions.filter.mine")}]
|
|
(keep (fn [id]
|
|
(when (not= id current-profile-id)
|
|
(when-let [fullname (-> profiles (get id) (get :fullname))]
|
|
{:value id :label (tr "workspace.versions.filter.user" fullname)}))))
|
|
users)))]
|
|
|
|
(mf/with-effect []
|
|
(st/emit! (dwv/init-versions-state)))
|
|
|
|
[:div {:class (stl/css :version-toolbox)}
|
|
[:& select
|
|
{:default-value :all
|
|
:aria-label (tr "workspace.versions.filter.label")
|
|
:options options
|
|
:on-change on-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 on-create-version
|
|
:icon i/pin}]]
|
|
|
|
(if (empty? data)
|
|
[:div {:class (stl/css :versions-entry-empty)}
|
|
[:div {:class (stl/css :versions-entry-empty-icon)} [:> icon* {:icon-id i/history}]]
|
|
[:div {:class (stl/css :versions-entry-empty-msg)} (tr "workspace.versions.empty")]]
|
|
|
|
[:ul {:class (stl/css :versions-entries)}
|
|
(for [entry entries]
|
|
(case (:type entry)
|
|
:version
|
|
[:> version-entry* {:key (:index entry)
|
|
:entry entry
|
|
:is-editing (= (:id entry) editing)
|
|
:current-profile profile
|
|
:on-edit on-edit-version
|
|
:on-cancel-edit on-cancel-version-edition
|
|
:on-rename on-rename-version
|
|
:on-restore on-restore-version
|
|
:on-delete on-delete-version
|
|
:on-lock on-lock-version
|
|
:on-unlock on-unlock-version}]
|
|
|
|
:snapshot
|
|
[:> snapshot-entry* {:key (:index entry)
|
|
:entry entry
|
|
:on-restore-snapshot on-restore-snapshot
|
|
:on-pin-snapshot on-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 (get-versions-warning-subtext team)}]]])]))
|