mirror of
https://github.com/penpot/penpot.git
synced 2026-05-24 17:33:41 +00:00
536 lines
22 KiB
Clojure
536 lines
22 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 :refer [css]])
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.types.components-list :as ctkl]
|
|
[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.store :as st]
|
|
[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.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 (css :element-count)}
|
|
(tr "workspace.libraries.components" components-count)]
|
|
(when (not= last-one :components)
|
|
[:span " · "])])
|
|
|
|
(when (pos? graphics-count)
|
|
[:*
|
|
[:span {:class (css :element-count)}
|
|
(tr "workspace.libraries.graphics" graphics-count)]
|
|
(when (not= last-one :graphics)
|
|
[:span " · "])])
|
|
|
|
(when (pos? colors-count)
|
|
[:*
|
|
[:span {:class (css :element-count)}
|
|
(tr "workspace.libraries.colors" colors-count)]
|
|
(when (not= last-one :colors)
|
|
[:span " · "])])
|
|
|
|
(when (pos? typography-count)
|
|
[:span {:class (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 (css :section)}
|
|
[:& title-bar {:collapsable? false
|
|
:title (tr "workspace.libraries.in-this-file")
|
|
:klass :title-spacing-lib}]
|
|
[:div {:class (css :section-list)}
|
|
|
|
[:div {:class (css :section-list-item)}
|
|
[:div
|
|
[:div {:class (css :item-name)} (tr "workspace.libraries.file-library")]
|
|
[:div {:class (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 (css :item-unpublish)
|
|
:type "button"
|
|
:value (tr "common.unpublish")
|
|
:on-click unpublish}]
|
|
[:input {:class (css :item-publish)
|
|
:type "button"
|
|
:value (tr "common.publish")
|
|
:on-click publish}])]]
|
|
|
|
(for [{:keys [id name] :as library} linked-libraries]
|
|
[:div {:class (css :section-list-item)
|
|
:key (dm/str id)}
|
|
[:div
|
|
[:div {:class (css :item-name)} name]
|
|
[:div {:class (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 (css :item-button)
|
|
:type "button"
|
|
:data-library-id (dm/str id)
|
|
:on-click unlink-library}
|
|
i/delete-refactor]])]]
|
|
|
|
[:div {:class (css :section)}
|
|
[:& title-bar {:collapsable? false
|
|
:title (tr "workspace.libraries.shared-libraries")
|
|
:klass :title-spacing-lib}]
|
|
[:div {:class (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 (css :search-icon)} i/search-refactor])}]]
|
|
|
|
(if (seq shared-libraries)
|
|
[:div {:class (css :section-list-shared)}
|
|
(for [{:keys [id name] :as library} shared-libraries]
|
|
[:div {:class (css :section-list-item)
|
|
:key (dm/str id)}
|
|
[:div
|
|
[:div {:class (css :item-name)} name]
|
|
[:div {:class (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 (css :item-button-shared)
|
|
:data-library-id (dm/str id)
|
|
:on-click link-library}
|
|
i/add-refactor]])]
|
|
|
|
[:div {:class (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))])])]])))
|
|
|
|
(mf/defc updates-tab
|
|
{::mf/wrap-props false}
|
|
[{:keys [file-id file-data libraries]}]
|
|
(let [libraries (mf/with-memo [file-data libraries]
|
|
(filter #(seq (dwl/assets-need-sync % file-data)) (vals libraries)))
|
|
new-css-system (mf/use-ctx ctx/new-css-system)
|
|
|
|
update (mf/use-fn
|
|
(mf/deps file-id)
|
|
(fn [event]
|
|
(let [library-id (some-> (dom/get-target event)
|
|
(dom/get-data "library-id")
|
|
(parse-uuid))]
|
|
(st/emit! (dwl/sync-file file-id library-id)))))]
|
|
(if new-css-system
|
|
[:div {:class (css :section)}
|
|
(if (empty? libraries)
|
|
[:div {:class (css :section-list-empty)}
|
|
(tr "workspace.libraries.no-libraries-need-sync")]
|
|
[:*
|
|
[:div {:class (css :section-title)} (tr "workspace.libraries.library")]
|
|
|
|
[:div {:class (css :section-list)}
|
|
(for [{:keys [id name] :as library} libraries]
|
|
[:div {:class (css :section-list-item)
|
|
:key (dm/str id)}
|
|
[:div
|
|
[:div {:class (css :item-name)} name]
|
|
[:div {:class (css :item-contents)} (describe-external-library library)]]
|
|
[:input {:class (css :item-update)
|
|
:type "button"
|
|
:value (tr "workspace.libraries.update")
|
|
:data-library-id (dm/str id)
|
|
:on-click update}]])]])]
|
|
|
|
[:div.section
|
|
(if (empty? libraries)
|
|
[:div.section-list-empty
|
|
i/library
|
|
(tr "workspace.libraries.no-libraries-need-sync")]
|
|
[:*
|
|
[:div.section-title (tr "workspace.libraries.library")]
|
|
|
|
[:div.section-list
|
|
(for [{:keys [id name] :as library} 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.update")
|
|
:data-library-id (dm/str id)
|
|
:on-click update}]])]])])))
|
|
|
|
(mf/defc libraries-dialog
|
|
{::mf/register modal/components
|
|
::mf/register-as :libraries-dialog}
|
|
[]
|
|
(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 :libraries)
|
|
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 (css :modal-overlay)}
|
|
[:div {:class (css :modal-dialog)}
|
|
[:button {:class (css :close)
|
|
:on-click close-dialog }
|
|
i/close-refactor]
|
|
[:div {:class (css :modal-title)}
|
|
"Libraries"]
|
|
[:div {:class (css :modal-content)}
|
|
[:div {:class (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 (dom/classnames (css :libraries-content) true)}
|
|
[:& 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 (dom/classnames (css :updates-content) true)}
|
|
[:& 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 (dom/classnames :active (= selected-tab :libraries))
|
|
:on-click select-libraries-tab}
|
|
(tr "workspace.libraries.libraries")]
|
|
[:div.header-item
|
|
{:class (dom/classnames :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}])]]]])]))
|
|
|