(ns app.main.ui.workspace.tokens.management (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] [app.common.path-names :as cpn] [app.common.types.shape.layout :as ctsl] [app.common.types.tokens-lib :as ctob] [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [app.config :as cf] [app.main.data.helpers :as dh] [app.main.data.modal :as modal] [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.data.workspace.tokens.propagation :as dwtp] [app.main.data.workspace.tokens.remapping :as remap] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]] [app.main.ui.workspace.tokens.management.group :refer [token-group*]] [app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]] [app.util.array :as array] [app.util.i18n :refer [tr]] [cuerdas.core :as str] [rumext.v2 :as mf])) (defn- get-sorted-token-groups "Separate token-types into groups of `empty` or `filled` depending if tokens exist for that type. Sort each group alphabetically (by their type)." [tokens-by-type] (let [token-shadow? (contains? cf/flags :token-shadow) all-types (cond-> dwta/token-properties (not token-shadow?) (dissoc :shadow)) all-types (-> all-types keys seq)] (loop [empty #js [] filled #js [] types all-types] (if-let [type (first types)] (if (not-empty (get tokens-by-type type)) (recur empty (array/conj! filled type) (rest types)) (recur (array/conj! empty type) filled (rest types))) [(seq (array/sort! empty)) (seq (array/sort! filled))])))) (mf/defc selected-set-info* {::mf/private true} [{:keys [tokens-lib tokens-status selected-token-set-id]}] (let [selected-token-set (mf/with-memo [tokens-lib selected-token-set-id] (when selected-token-set-id (some-> tokens-lib (ctob/get-set selected-token-set-id)))) token-set-active? (mf/use-fn (mf/deps tokens-status) (fn [id] (ctos/set-active? tokens-status id)))] [:div {:class (stl/css :sets-header-container)} [:> text* {:as "span" :typography "headline-small" :class (stl/css :sets-header) :data-testid "active-token-set-title"} (tr "workspace.tokens.tokens-section-title" (ctob/get-name selected-token-set))] (when (and (some? selected-token-set-id) (not (token-set-active? selected-token-set-id))) [:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")} ;; NOTE: when no set in tokens-lib, the selected-token-set-id ;; will be `nil`, so for properly hide the inactive message we ;; check that at least `selected-token-set-id` has a value [:* [:> icon* {:class (stl/css :sets-header-status-icon) :icon-id i/eye-off}] [:> text* {:as "span" :typography "body-small" :class (stl/css :sets-header-status-text)} (tr "workspace.tokens.inactive-set")]]])])) (mf/defc tokens-section* {::mf/private true} [{:keys [tokens-lib tokens-status active-tokens resolved-active-tokens]}] (let [objects (mf/deref refs/workspace-page-objects) selected (mf/deref refs/selected-shapes) selected-shapes (mf/with-memo [selected objects] (into [] (keep (d/getf objects)) selected)) is-selected-inside-layout (mf/with-memo [selected-shapes objects] (some #(ctsl/any-layout-immediate-child? objects %) selected-shapes)) ;; This only checks for the currently explicitly selected set ;; id, it is ephimeral and can be nil ;; FIXME: this is a repeated deref for the same `:workspace-tokens` state selected-token-set-id (mf/deref refs/selected-token-set-id) ;; If we have not selected any set explicitly we just ;; select the first one from the list of sets selected-token-set-tokens (when selected-token-set-id (some-> tokens-lib (ctob/get-tokens selected-token-set-id))) tokens (mf/with-memo [active-tokens selected-token-set-tokens] (merge active-tokens selected-token-set-tokens)) ;; Group tokens by their type tokens-by-type (mf/with-memo [tokens selected-token-set-tokens] (let [tokens (reduce-kv (fn [tokens k _] (if (contains? selected-token-set-tokens k) tokens (dissoc tokens k))) tokens tokens)] (ctob/group-by-type tokens))) [empty-group filled-group] (mf/with-memo [tokens-by-type] (get-sorted-token-groups tokens-by-type)) ;; Filter tokens by their path and return the tokens filter-tokens-by-path (mf/use-fn (fn [tokens-filtered-by-type node] (->> tokens-filtered-by-type (filter (fn [token] (let [token-path (cpn/split-path (:name token) :separator ".")] (and (> (count token-path) 0) (str/starts-with? (:name token) (str (:path node) "."))))))))) ;; Filter tokens by their path and return their ids filter-tokens-by-path-ids (mf/use-fn (mf/deps selected-token-set-tokens) (fn [type path] (->> selected-token-set-tokens (filter (fn [token] (let [[_ token-value] token] (and (= (:type token-value) type) (str/starts-with? (:name token-value) (str path ".")))))) (mapv (fn [token] (let [[_ token-value] token] (:id token-value))))))) remaining-tokens-of-type-in-set? (mf/use-fn (fn [selected-token-set-tokens tokens-in-path-ids] (let [token-ids (set tokens-in-path-ids) remaining-tokens (filter (fn [token] (not (contains? token-ids (:id token)))) selected-token-set-tokens)] (seq remaining-tokens)))) delete-token (mf/with-memo [selected-token-set-tokens selected-token-set-id] (fn [token] (let [id (:id token) type (:type token) path (:name token) tokens-by-type (ctob/group-by-type selected-token-set-tokens) tokens-filtered-by-type (get tokens-by-type type) remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type [id])] (st/emit! (dwtl/delete-token selected-token-set-id id)) (if remaining-tokens? (st/emit! (dwtl/toggle-token-path (str (name type) "." path))) (st/emit! (dwtl/close-token-type type)))))) delete-node (mf/with-memo [selected-token-set-tokens selected-token-set-id] (fn [node type] (let [path (:path node) tokens-by-type (ctob/group-by-type selected-token-set-tokens) tokens-filtered-by-type (get tokens-by-type type) tokens-in-path-ids (filter-tokens-by-path-ids type path) remaining-tokens? (remaining-tokens-of-type-in-set? tokens-filtered-by-type tokens-in-path-ids)] ;; Delete tokens in path (st/emit! (dwtl/bulk-delete-tokens selected-token-set-id tokens-in-path-ids)) ;; Remove from unfolded tree path (if remaining-tokens? (st/emit! (dwtl/toggle-token-path (str (name type) "." path))) (st/emit! (dwtl/close-token-type type)))))) bulk-rename-tokens-in-path ;; Rename tokens in bulk affected by a node rename. (mf/use-fn (mf/deps filter-tokens-by-path-ids selected-token-set-id) (fn [node type new-node-name] (let [old-path (:path node) new-path (ctob/rename-path node new-node-name) tokens-in-path-ids (filter-tokens-by-path-ids type old-path)] (st/emit! (modal/hide) (dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-path))))) bulk-remap-tokens-in-path ;; Remap tokens in bulk affected by a node rename. ;; It will update the token names and propagate the changes to the workspace. (mf/use-fn (mf/deps filter-tokens-by-path filter-tokens-by-path-ids selected-token-set-tokens selected-token-set-id) (fn [node type new-node-name] (let [old-path (:path node) ;; Get tokens in path to remap their names after remapping the node tokens-by-type (ctob/group-by-type selected-token-set-tokens) tokens-filtered-by-type (get tokens-by-type type) tokens-in-path (filter-tokens-by-path tokens-filtered-by-type node) tokens-in-path-ids (filter-tokens-by-path-ids type old-path) new-node-path (ctob/rename-path node new-node-name) new-tokens (map (fn [token] (let [new-token-path (ctob/rename-path node token new-node-name)] (assoc token :name new-token-path))) tokens-in-path) undo-group (uuid/next)] (st/emit! (dwtl/bulk-update-tokens selected-token-set-id tokens-in-path-ids type old-path new-node-path :undo-group undo-group) (remap/bulk-remap-tokens tokens-in-path new-tokens :undo-group undo-group) (dwtp/propagate-workspace-tokens) (modal/hide))))) on-remap-node-warning ;; If there are tokens that will be affected by the node rename, we show the remap modal (mf/use-fn (mf/deps bulk-remap-tokens-in-path bulk-rename-tokens-in-path) (fn [node type new-node-name] (let [remap-data {:new-name new-node-name :old-name (:name node) :type "node"} remap-handler #(bulk-remap-tokens-in-path node type new-node-name) rename-handler #(bulk-rename-tokens-in-path node type new-node-name)] (st/emit! (modal/hide) (modal/show :tokens/remapping-confirmation {:remap-data remap-data :on-remap remap-handler :on-rename rename-handler}))))) on-rename-node ;; When user renames a node, we need to check if there are tokens that will be affected by this change. ;; If there are, we display the remap modal, otherwise, we rename the tokens directly. (mf/use-fn (mf/deps selected-token-set-tokens filter-tokens-by-path on-remap-node-warning bulk-rename-tokens-in-path) (fn [node type new-node-name] (let [state @st/state file-data (dh/lookup-file-data state) tokens-by-type (ctob/group-by-type selected-token-set-tokens) tokens-filtered-by-type (get tokens-by-type type) tokens-in-current-path (filter-tokens-by-path tokens-filtered-by-type node) token-references-count (reduce (fn [count token] (+ count (remap/count-token-references file-data (:name token)))) 0 tokens-in-current-path)] (if (> token-references-count 0) (on-remap-node-warning node type new-node-name) (bulk-rename-tokens-in-path node type new-node-name))))) on-duplicate-node (fn [node type new-node-name] (let [tokens-in-path-ids (filter-tokens-by-path-ids type (:path node))] (st/emit! (modal/hide) (dwtl/bulk-create-tokens selected-token-set-id tokens-in-path-ids type node new-node-name)))) open-rename-node-modal ;; When user renames a node, we display a form modal (mf/use-fn (mf/deps selected-token-set-tokens on-rename-node) (fn [node type] (let [on-rename-node-handler #(on-rename-node node type %)] (st/emit! (modal/show :tokens/rename-node {:node node :tokens-in-active-set selected-token-set-tokens :on-rename on-rename-node-handler}))))) open-duplicate-node-modal (mf/use-fn (mf/deps selected-token-set-tokens on-duplicate-node) (fn [node type] (let [on-duplicate-node-handler #(on-duplicate-node node type %)] (st/emit! (modal/show :tokens/rename-node {:new-node-name (str (:name node) "-copy") :node node :variant "duplicate" :tokens-in-active-set selected-token-set-tokens :on-rename on-duplicate-node-handler})))))] (mf/with-effect [tokens-lib selected-token-set-id] (when (and tokens-lib (or (nil? selected-token-set-id) (and selected-token-set-id (not (ctob/get-set tokens-lib selected-token-set-id))))) (let [match (->> (ctob/get-sets tokens-lib) (first))] (when match (st/emit! (dwtl/set-selected-token-set-id (ctob/get-id match))))))) [:* [:& token-context-menu {:on-delete-token delete-token}] [:> token-node-context-menu* {:on-rename-node open-rename-node-modal :on-duplicate-node open-duplicate-node-modal :on-delete-node delete-node}] [:> selected-set-info* {:tokens-lib tokens-lib :tokens-status tokens-status :selected-token-set-id selected-token-set-id}] (for [type filled-group] (let [tokens (get tokens-by-type type)] [:> token-group* {:key (name type) :tokens tokens :type type :selected-ids selected :selected-shapes selected-shapes :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens resolved-active-tokens :tokens-lib tokens-lib :selected-token-set-id selected-token-set-id}])) (for [type empty-group] [:> token-group* {:key (name type) :tokens [] :type type :selected-shapes selected-shapes :is-selected-inside-layout is-selected-inside-layout :active-theme-tokens resolved-active-tokens}])]))