penpot/frontend/src/app/main/ui/workspace/libraries.cljs
2023-09-07 13:59:06 +02:00

756 lines
34 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.libraries
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.colors-list :as ctcl]
[app.common.types.components-list :as ctkl]
[app.common.types.file :as ctf]
[app.common.types.typographies-list :as ctyl]
[app.common.uuid :as uuid]
[app.main.data.modal :as modal]
[app.main.data.workspace.libraries :as dwl]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.render :refer [component-svg]]
[app.main.store :as st]
[app.main.ui.components.color-bullet :as bc]
[app.main.ui.components.link-button :as lb]
[app.main.ui.components.search-bar :refer [search-bar]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.context :as ctx]
[app.main.ui.icons :as i]
[app.util.color :as uc]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.strings :refer [matches-search]]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ref:workspace-file
(l/derived :workspace-file st/state))
(defn create-file-library-ref
[library-id]
(letfn [(getter-fn [state]
(let [fdata (let [{:keys [id] :as wfile} (:workspace-data state)]
(if (= id library-id)
wfile
(dm/get-in state [:workspace-libraries library-id :data])))]
{:colors (-> fdata :colors vals)
:media (-> fdata :media vals)
:components (ctkl/components-seq fdata)
:typographies (-> fdata :typographies vals)}))]
(l/derived getter-fn st/state =)))
(defn- describe-library
[components-count graphics-count colors-count typography-count]
(let [all-zero? (and (zero? components-count) (zero? graphics-count) (zero? colors-count) (zero? typography-count))]
(str
(str/join " · "
(cond-> []
(or all-zero? (pos? components-count))
(conj (tr "workspace.libraries.components" components-count))
(or all-zero? (pos? graphics-count))
(conj (tr "workspace.libraries.graphics" graphics-count))
(or all-zero? (pos? colors-count))
(conj (tr "workspace.libraries.colors" colors-count))
(or all-zero? (pos? typography-count))
(conj (tr "workspace.libraries.typography" typography-count))))
"\u00A0")))
(mf/defc describe-library-blocks
[{:keys [components-count graphics-count colors-count typography-count] :as props}]
(let [last-one (cond
(> colors-count 0) :color
(> graphics-count 0) :graphics
(> components-count 0) :components)]
[:*
(when (pos? components-count)
[:*
[:span {:class (stl/css :element-count)}
(tr "workspace.libraries.components" components-count)]
(when (not= last-one :components)
[:span " · "])])
(when (pos? graphics-count)
[:*
[:span {:class (stl/css :element-count)}
(tr "workspace.libraries.graphics" graphics-count)]
(when (not= last-one :graphics)
[:span " · "])])
(when (pos? colors-count)
[:*
[:span {:class (stl/css :element-count)}
(tr "workspace.libraries.colors" colors-count)]
(when (not= last-one :colors)
[:span " · "])])
(when (pos? typography-count)
[:span {:class (stl/css :element-count)}
(tr "workspace.libraries.typography" typography-count)])]))
(defn- describe-linked-library
[library]
(let [components-count (count (or (ctkl/components-seq (:data library)) []))
graphics-count (count (dm/get-in library [:data :media] []))
colors-count (count (dm/get-in library [:data :colors] []))
typography-count (count (dm/get-in library [:data :typographies] []))]
(describe-library components-count graphics-count colors-count typography-count)))
(defn- describe-external-library
[library]
(let [components-count (dm/get-in library [:library-summary :components :count] 0)
graphics-count (dm/get-in library [:library-summary :media :count] 0)
colors-count (dm/get-in library [:library-summary :colors :count] 0)
typography-count (dm/get-in library [:library-summary :typographies :count] 0)]
(describe-library components-count graphics-count colors-count typography-count)))
(mf/defc libraries-tab
{::mf/wrap-props false}
[{:keys [file-id shared? linked-libraries shared-libraries]}]
(let [search-term* (mf/use-state "")
search-term (deref search-term*)
new-css-system (mf/use-ctx ctx/new-css-system)
library-ref (mf/with-memo [file-id]
(create-file-library-ref file-id))
library (deref library-ref)
colors (:colors library)
components (:components library)
media (:media library)
typographies (:typographies library)
empty-library? (and
(zero? (count colors))
(zero? (count components))
(zero? (count media))
(zero? (count typographies)))
shared-libraries
(mf/with-memo [shared-libraries linked-libraries file-id search-term]
(->> shared-libraries
(remove #(= (:id %) file-id))
(remove #(contains? linked-libraries (:id %)))
(filter #(matches-search (:name %) search-term))
(sort-by (comp str/lower :name))))
linked-libraries
(mf/with-memo [linked-libraries]
(->> (vals linked-libraries)
(sort-by (comp str/lower :name))))
change-search-term
(mf/use-fn
(mf/deps new-css-system)
(fn [event]
(let [value (if new-css-system
event
(-> (dom/get-target event)
(dom/get-value)))]
(reset! search-term* value))))
clear-search-term
(mf/use-fn #(reset! search-term* ""))
link-library
(mf/use-fn
(mf/deps file-id new-css-system)
(fn [event]
(let [library-id (if new-css-system
(some-> (dom/get-current-target event)
(dom/get-data "library-id")
(parse-uuid))
(some-> (dom/get-target event)
(dom/get-data "library-id")
(parse-uuid)))]
(st/emit! (dwl/link-file-to-library file-id library-id)))))
unlink-library
(mf/use-fn
(mf/deps file-id)
(fn [event]
(let [library-id (if new-css-system
(some-> (dom/get-current-target event)
(dom/get-data "library-id")
(parse-uuid))
(some-> (dom/get-target event)
(dom/get-data "library-id")
(parse-uuid)))]
(st/emit! (dwl/unlink-file-from-library file-id library-id)
(dwl/sync-file file-id library-id)))))
on-delete-accept
(mf/use-fn
(mf/deps file-id)
#(st/emit! (dwl/set-file-shared file-id false)
(modal/show :libraries-dialog {})))
on-delete-cancel
(mf/use-fn #(st/emit! (modal/show :libraries-dialog {})))
publish
(mf/use-fn
(mf/deps file-id)
(fn [event]
(let [input-node (dom/event->target event)
publish-library #(st/emit! (dwl/set-file-shared file-id true))
cancel-publish #(st/emit! (modal/show :libraries-dialog {}))]
(if empty-library?
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.publish-empty-library.title")
:message (tr "modals.publish-empty-library.message")
:accept-label (tr "modals.publish-empty-library.accept")
:on-accept publish-library
:on-cancel cancel-publish}))
(publish-library))
(dom/blur! input-node))))
unpublish
(mf/use-fn
(mf/deps file-id)
(fn [_]
(st/emit! (modal/show
{:type :delete-shared-libraries
:ids #{file-id}
:origin :unpublish
:on-accept on-delete-accept
:on-cancel on-delete-cancel
:count-libraries 1}))))
handle-key-down
(mf/use-fn
(fn [event]
(let [enter? (kbd/enter? event)
esc? (kbd/esc? event)
input-node (dom/event->target event)]
(when ^boolean enter?
(dom/blur! input-node))
(when ^boolean esc?
(dom/blur! input-node)))))]
(if new-css-system
[:*
[:div {:class (stl/css :section)}
[:& title-bar {:collapsable? false
:title (tr "workspace.libraries.in-this-file")
:class (stl/css :title-spacing-lib)}]
[:div {:class (stl/css :section-list)}
[:div {:class (stl/css :section-list-item)}
[:div
[:div {:class (stl/css :item-name)} (tr "workspace.libraries.file-library")]
[:div {:class (stl/css :item-contents)}
[:& describe-library-blocks {:components-count (count components)
:graphics-count (count media)
:colors-count (count colors)
:typography-count (count typographies)}]]]
[:div
(if ^boolean shared?
[:input {:class (stl/css :item-unpublish)
:type "button"
:value (tr "common.unpublish")
:on-click unpublish}]
[:input {:class (stl/css :item-publish)
:type "button"
:value (tr "common.publish")
:on-click publish}])]]
(for [{:keys [id name] :as library} linked-libraries]
[:div {:class (stl/css :section-list-item)
:key (dm/str id)}
[:div
[:div {:class (stl/css :item-name)} name]
[:div {:class (stl/css :item-contents)}
(let [components-count (count (or (ctkl/components-seq (:data library)) []))
graphics-count (count (dm/get-in library [:data :media] []))
colors-count (count (dm/get-in library [:data :colors] []))
typography-count (count (dm/get-in library [:data :typographies] []))]
[:& describe-library-blocks {:components-count components-count
:graphics-count graphics-count
:colors-count colors-count
:typography-count typography-count}])]]
[:button {:class (stl/css :item-button)
:type "button"
:data-library-id (dm/str id)
:on-click unlink-library}
i/delete-refactor]])]]
[:div {:class (stl/css :section)}
[:& title-bar {:collapsable? false
:title (tr "workspace.libraries.shared-libraries")
:class (stl/css :title-spacing-lib)}]
[:div {:class (stl/css :libraries-search)}
[:& search-bar {:on-change change-search-term
:value search-term
:placeholder (tr "workspace.libraries.search-shared-libraries")
:icon (mf/html [:span {:class (stl/css :search-icon)} i/search-refactor])}]]
(if (seq shared-libraries)
[:div {:class (stl/css :section-list-shared)}
(for [{:keys [id name] :as library} shared-libraries]
[:div {:class (stl/css :section-list-item)
:key (dm/str id)}
[:div
[:div {:class (stl/css :item-name)} name]
[:div {:class (stl/css :item-contents)}
(let [components-count (dm/get-in library [:library-summary :components :count] 0)
graphics-count (dm/get-in library [:library-summary :media :count] 0)
colors-count (dm/get-in library [:library-summary :colors :count] 0)
typography-count (dm/get-in library [:library-summary :typographies :count] 0)]
[:& describe-library-blocks {:components-count components-count
:graphics-count graphics-count
:colors-count colors-count
:typography-count typography-count}])]]
[:button {:class (stl/css :item-button-shared)
:data-library-id (dm/str id)
:on-click link-library}
i/add-refactor]])]
[:div {:class (stl/css :section-list-empty)}
(if (nil? shared-libraries)
i/loader-pencil
(if (str/empty? search-term)
(tr "workspace.libraries.no-shared-libraries-available")
(tr "workspace.libraries.no-matches-for" search-term)))])]]
[:*
[:div.section
[:div.section-title (tr "workspace.libraries.in-this-file")]
[:div.section-list
[:div.section-list-item
[:div
[:div.item-name (tr "workspace.libraries.file-library")]
[:div.item-contents (describe-library
(count components)
(count media)
(count colors)
(count typographies))]]
[:div
(if ^boolean shared?
[:input.item-button {:type "button"
:value (tr "common.unpublish")
:on-click unpublish}]
[:input.item-button {:type "button"
:value (tr "common.publish")
:on-click publish}])]]
(for [{:keys [id name] :as library} linked-libraries]
[:div.section-list-item {:key (dm/str id)}
[:div.item-name name]
[:div.item-contents (describe-linked-library library)]
[:input.item-button {:type "button"
:value (tr "labels.remove")
:data-library-id (dm/str id)
:on-click unlink-library}]])]]
[:div.section
[:div.section-title (tr "workspace.libraries.shared-libraries")]
[:div.libraries-search
[:input.search-input
{:placeholder (tr "workspace.libraries.search-shared-libraries")
:type "text"
:value search-term
:on-change change-search-term
:on-key-down handle-key-down}]
(if (str/empty? search-term)
[:div.search-icon
i/search]
[:div.search-icon.search-close
{:on-click clear-search-term}
i/close])]
(if (seq shared-libraries)
[:div.section-list
(for [{:keys [id name] :as library} shared-libraries]
[:div.section-list-item {:key (dm/str id)}
[:div.item-name name]
[:div.item-contents (describe-external-library library)]
[:input.item-button {:type "button"
:value (tr "workspace.libraries.add")
:data-library-id (dm/str id)
:on-click link-library}]])]
[:div.section-list-empty
(if (nil? shared-libraries)
i/loader-pencil
[:* i/library
(if (str/empty? search-term)
(tr "workspace.libraries.no-shared-libraries-available")
(tr "workspace.libraries.no-matches-for" search-term))])])]])))
(defn- extract-assets
[file-data library summary?]
(let [exceeded (volatile! {:components false
:colors false
:typographies false})
truncate (fn [asset-type items]
(if (and summary? (> (count items) 5))
(do
(vswap! exceeded assoc asset-type true)
(take 5 items))
items))
assets (dwl/assets-need-sync library file-data)
component-ids (into #{} (->> assets
(filter #(= (:asset-type %) :component))
(map :asset-id)))
color-ids (into #{} (->> assets
(filter #(= (:asset-type %) :color))
(map :asset-id)))
typography-ids (into #{} (->> assets
(filter #(= (:asset-type %) :typography))
(map :asset-id)))
components (->> component-ids
(map #(ctkl/get-component (:data library) %))
(sort-by #(str/lower (:name %)))
(truncate :components))
colors (->> color-ids
(map #(ctcl/get-color (:data library) %))
(sort-by #(str/lower (:name %)))
(truncate :colors))
typographies (->> typography-ids
(map #(ctyl/get-typography (:data library) %))
(sort-by #(str/lower (:name %)))
(truncate :typographies))]
[library @exceeded {:components components
:colors colors
:typographies typographies}]))
(mf/defc updates-tab
{::mf/wrap-props false}
[{:keys [file-id file-data libraries]}]
(let [summary?* (mf/use-state true)
updating?* (mf/use-state false)
summary? (deref summary?*)
updating? (deref updating?*)
see-all-assets
(mf/use-fn
(fn []
(reset! summary?* false)))
libs-assets (mf/with-memo [file-data libraries summary?*]
(->> (vals libraries)
(map #(extract-assets file-data % summary?))
(filter (fn [[_ _ {:keys [components colors typographies]}]]
(or (seq components)
(seq colors)
(seq typographies))))))
new-css-system (mf/use-ctx ctx/new-css-system)
update (mf/use-fn
(mf/deps file-id)
(fn [event]
(when-not updating?
(let [library-id (some-> (dom/get-target event)
(dom/get-data "library-id")
(parse-uuid))]
(reset! updating?* true)
(st/emit! (dwl/sync-file file-id library-id))))))]
(if new-css-system
[:div {:class (stl/css :section)}
(if (empty? libs-assets)
[:div {:class (stl/css :section-list-empty)}
(tr "workspace.libraries.no-libraries-need-sync")]
[:*
[:div {:class (stl/css :section-title)} (tr "workspace.libraries.library-updates")]
[:div {:class (stl/css :section-list)}
(for [[{:keys [id name] :as library}
exceeded
{:keys [components colors typographies]}] libs-assets]
[:div {:class (stl/css :section-list-item)
:key (dm/str id)}
[:div
[:div {:class (stl/css :item-name)} name]
[:div {:class (stl/css :item-contents)} (describe-library
(count components)
0
(count colors)
(count typographies))]]
[:input {:type "button"
:class (stl/css-case :item-update true
:disabled updating?)
:value (tr "workspace.libraries.update")
:data-library-id (dm/str id)
:on-click update}]
[:div {:class (stl/css :libraries-updates)}
(when-not (empty? components)
[:div {:class (stl/css :libraries-updates-column)}
(for [component components]
[:div {:class (stl/css :libraries-updates-item)
:key (dm/str (:id component))}
(let [component (ctf/load-component-objects (:data library) component)
root-shape (ctf/get-component-root (:data library) component)]
[:*
[:& component-svg {:root-shape root-shape
:objects (:objects component)}]
[:div {:class (stl/css :name-block)}
[:span {:class (stl/css :item-name)
:title (:name component)}
(:name component)]]])])
(when (:components exceeded)
[:div {:class (stl/css :libraries-updates-item)
:key (uuid/next)}
[:div {:class (stl/css :name-block.ellipsis)}
[:span {:class (stl/css :item-name)} "(...)"]]])])
(when-not (empty? colors)
[:div {:class (stl/css :libraries-updates-column)
:style #js {"--bullet-size" "24px"}}
(for [color colors]
(let [default-name (cond
(:gradient color) (uc/gradient-type->string (get-in color [:gradient :type]))
(:color color) (:color color)
:else (:value color))]
[:div {:class (stl/css :libraries-updates-item)
:key (dm/str (:id color))}
[:*
[:& bc/color-bullet {:color {:color (:color color)
:opacity (:opacity color)}}]
[:div {:class (stl/css :name-block)}
[:span {:class (stl/css :item-name)
:title (:name color)}
(:name color)]
(when-not (= (:name color) default-name)
[:span.color-value (:color color)])]]]))
(when (:colors exceeded)
[:div {:class (stl/css :libraries-updates-item)
:key (uuid/next)}
[:div {:class (stl/css :name-block.ellipsis)}
[:span {:class (stl/css :item-name)} "(...)"]]])])
(when-not (empty? typographies)
[:div {:class (stl/css :libraries-updates-column)}
(for [typography typographies]
[:div {:class (stl/css :libraries-updates-item)
:key (dm/str (:id typography))}
[:*
[:div {:style {:font-family (:font-family typography)
:font-weight (:font-weight typography)
:font-style (:font-style typography)}}
(tr "workspace.assets.typography.sample")]
[:div {:class (stl/css :name-block)}
[:span {:class (stl/css :item-name)
:title (:name typography)}
(:name typography)]]]])
(when (:typographies exceeded)
[:div {:class (stl/css :libraries-updates-item)
:key (uuid/next)}
[:div {:class (stl/css :name-block.ellipsis)}
[:span {:class (stl/css :item-name)} "(...)"]]])])]
(when (or (pos? (:components exceeded))
(pos? (:colors exceeded))
(pos? (:typographies exceeded)))
[:div {:class (stl/css :libraries-updates-see-all)}
[:& lb/link-button {:on-click see-all-assets
:value (str "(" (tr "workspace.libraries.update.see-all-changes") ")")}]])])]])]
[:div.section
(if (empty? libs-assets)
[:div.section-list-empty
i/library
(tr "workspace.libraries.no-libraries-need-sync")]
[:*
[:div.section-title (tr "workspace.libraries.library-updates")]
[:div.section-list
(for [[{:keys [id name] :as library}
exceeded
{:keys [components colors typographies]}] libs-assets]
[:div.section-list-item {:key (dm/str id)}
[:div.item-name name]
[:div.item-contents (describe-library
(count components)
0
(count colors)
(count typographies))]
[:input.item-button.item-update {:type "button"
:class (stl/css-case new-css-system
:disabled updating?)
:value (tr "workspace.libraries.update")
:data-library-id (dm/str id)
:on-click update}]
[:div.libraries-updates
(when-not (empty? components)
[:div.libraries-updates-column
(for [component components]
[:div.libraries-updates-item {:key (dm/str (:id component))}
(let [component (ctf/load-component-objects (:data library) component)
root-shape (ctf/get-component-root (:data library) component)]
[:*
[:& component-svg {:root-shape root-shape
:objects (:objects component)}]
[:div.name-block
[:span.item-name {:title (:name component)}
(:name component)]]])])
(when (:components exceeded)
[:div.libraries-updates-item {:key (uuid/next)}
[:div.name-block.ellipsis
[:span.item-name "(...)"]]])])
(when-not (empty? colors)
[:div.libraries-updates-column {:style #js {"--bullet-size" "24px"}}
(for [color colors]
(let [default-name (cond
(:gradient color) (uc/gradient-type->string (get-in color [:gradient :type]))
(:color color) (:color color)
:else (:value color))]
[:div.libraries-updates-item {:key (dm/str (:id color))}
[:*
[:& bc/color-bullet {:color {:color (:color color)
:opacity (:opacity color)}}]
[:div.name-block
[:span.item-name {:title (:name color)}
(:name color)]
(when-not (= (:name color) default-name)
[:span.color-value (:color color)])]]]))
(when (:colors exceeded)
[:div.libraries-updates-item {:key (uuid/next)}
[:div.name-block.ellipsis
[:span.item-name "(...)"]]])])
(when-not (empty? typographies)
[:div.libraries-updates-column
(for [typography typographies]
[:div.libraries-updates-item {:key (dm/str (:id typography))}
[:*
[:div.typography-sample
{:style {:font-family (:font-family typography)
:font-weight (:font-weight typography)
:font-style (:font-style typography)}}
(tr "workspace.assets.typography.sample")]
[:div.name-block
[:span.item-name {:title (:name typography)}
(:name typography)]]]])
(when (:typographies exceeded)
[:div.libraries-updates-item {:key (uuid/next)}
[:div.name-block.ellipsis
[:span.item-name "(...)"]]])])]
(when (or (pos? (:components exceeded))
(pos? (:colors exceeded))
(pos? (:typographies exceeded)))
[:div.libraries-updates-see-all
[:& lb/link-button {:on-click see-all-assets
:value (str "(" (tr "workspace.libraries.update.see-all-changes") ")")}]])])]])])))
(mf/defc libraries-dialog
{::mf/register modal/components
::mf/register-as :libraries-dialog}
[{:keys [starting-tab] :as props :or {starting-tab :libraries}}]
(let [new-css-system (features/use-feature :new-css-system)
project (mf/deref refs/workspace-project)
file-data (mf/deref refs/workspace-data)
file (mf/deref ref:workspace-file)
team-id (:team-id project)
file-id (:id file)
shared? (:is-shared file)
selected-tab* (mf/use-state starting-tab)
selected-tab (deref selected-tab*)
libraries (mf/deref refs/workspace-libraries)
libraries (mf/with-memo [libraries]
(d/removem (fn [[_ val]] (:is-indirect val)) libraries))
;; NOTE: we really don't need react on shared files
shared-libraries
(mf/deref refs/workspace-shared-files)
select-libraries-tab
(mf/use-fn #(reset! selected-tab* :libraries))
select-updates-tab
(mf/use-fn #(reset! selected-tab* :updates))
on-tab-change
(mf/use-fn #(reset! selected-tab* %))
close-dialog
(mf/use-fn (fn [_]
(modal/hide!)
(modal/disallow-click-outside!)))]
(mf/with-effect [team-id]
(when team-id
(st/emit! (dwl/fetch-shared-files {:team-id team-id}))))
[:& (mf/provider ctx/new-css-system) {:value new-css-system}
(if new-css-system
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog)}
[:button {:class (stl/css :close)
:on-click close-dialog}
i/close-refactor]
[:div {:class (stl/css :modal-title)}
"Libraries"]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :libraries-header)}
[:& tab-container
{:on-change-tab on-tab-change
:selected selected-tab
:collapsable? false}
[:& tab-element {:id :libraries :title (tr "workspace.libraries.libraries")}
[:div {:class (stl/css :libraries-content)}
[:& libraries-tab {:file-id file-id
:shared? shared?
:linked-libraries libraries
:shared-libraries shared-libraries}]]]
[:& tab-element {:id :updates :title (tr "workspace.libraries.updates")}
[:div {:class (stl/css :updates-content)}
[:& updates-tab {:file-id file-id
:file-data file-data
:libraries libraries}]]]]]]]]
[:div.modal-overlay
[:div.modal.libraries-dialog
[:a.close {:on-click close-dialog} i/close]
[:div.modal-content
[:div.libraries-header
[:div.header-item
{:class (stl/css-case new-css-system :active (= selected-tab :libraries))
:on-click select-libraries-tab}
(tr "workspace.libraries.libraries")]
[:div.header-item
{:class (stl/css-case new-css-system :active (= selected-tab :updates))
:on-click select-updates-tab}
(tr "workspace.libraries.updates")]]
[:div.libraries-content
(case selected-tab
:libraries
[:& libraries-tab {:file-id file-id
:shared? shared?
:linked-libraries libraries
:shared-libraries shared-libraries}]
:updates
[:& updates-tab {:file-id file-id
:file-data file-data
:libraries libraries}])]]]])]))