From 3cd9bfa9de1cda539caad87fac486afa7230db91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 21 Sep 2026 15:37:45 +0200 Subject: [PATCH] :sparkles: Add sync with design tokens in external libraries (#10293) * :sparkles: Auto link tokens when adding external libraries (provisional) * :wrench: Refactor tokens-lib initialization * :wrench: Add separated TokenStatus to store status apart of TokensLib * :wrench: Make all status operations use the new data structure * :wrench: Normalize status helper functions and access token sets by id * :wrench: Rename :tokens-file to :tokens-source * :tada: Allow the user to choose the tokens-source of a file * :tada: Make tokens library readonly when it's in an external file * :tada: Show tokens in library summaries * :tada: Show source info in sidebar * :wrench: Fix integration tests * :bug: Propagate changes of token values in external library * :tada: Layout updates * :wrench: Refactor tokens source calculations * :wrench: Add harder checks for nil or empty values in everything * :bug: Fix some integration tests * :wrench: Add integration tests for tokens in external libs * :wrench: Validate and repair missing tokens status * :tada: Make ui changes optional with config flag * :bug: Propagate tokens after synchronizing components in ext library * :bug: Propagate tokens after creating new instances * :bug: Propagate tokens after synchronizing tokens in ext library * :bug: Add a tokens source icon to libraries section (#11439) * :bug: Add a tokens source icon to libraries section * :bug: Fix ellipsis on library names * :recycle: Remove code under flag on legacy component * :bug: Fix token theme name on inspect tab * :tada: Add changes notification (#11476) * :tada: Add changes notification * :recycle: Change fn names * :bug: Fix tokens source label truncation and missing translations (#11533) * :bug: Fix tokens source label truncation and missing translations The tokens source file name always showed, even for the current file, and long names wrapped onto a second line instead of truncating because the header used flex-wrap and overflow-wrap: break-word instead of single-line ellipsis. Show the source row unconditionally (it now displays "This file" when the source is the current file, matching the connected-library case), truncate the file name to one line with an ellipsis, and only attach a tooltip with the full name when the text is actually truncated. Replace the hardcoded UI strings with translated ones and add their English and Spanish entries. AI-assisted-by: claude-sonnet-5 * :bug: Remove redundant effect dependency in tokens source file-name-truncated? was listed as a dependency of the with-effect that checks and observes label truncation, even though it isn't read inside the effect body. Since the effect itself flips that state via check-file-name-truncated, including it as a dependency caused the ResizeObserver to be needlessly disconnected and reconnected on every truncation change. AI-assisted-by: claude-sonnet-5 * :bug: Fix small visual error * :bug: Fix problem with plugins * :bug: Fix playwright tests --------- Co-authored-by: Eva Marco Co-authored-by: Eva Marco Co-authored-by: alonso.torres --- backend/src/app/rpc/commands/files.clj | 14 +- backend/src/app/rpc/commands/files_update.clj | 1 - common/src/app/common/files/changes.cljc | 152 +- .../src/app/common/files/changes_builder.cljc | 53 +- common/src/app/common/files/migrations.cljc | 11 +- common/src/app/common/files/repair.cljc | 11 + common/src/app/common/files/tokens.cljc | 319 +++- common/src/app/common/files/validate.cljc | 16 +- common/src/app/common/flags.cljc | 1 + common/src/app/common/logic/tokens.cljc | 191 ++- .../src/app/common/test_helpers/tokens.cljc | 29 +- common/src/app/common/types/file.cljc | 2 +- common/src/app/common/types/tokens_lib.cljc | 428 ++--- .../test/common_tests/files/tokens_test.cljc | 1385 +++++++++++++++++ .../common_tests/logic/move_shapes_test.cljc | 40 +- .../common_tests/logic/token_apply_test.cljc | 129 +- .../test/common_tests/logic/token_test.cljc | 1304 ++++++++++++---- .../common_tests/types/tokens_lib_test.cljc | 229 +-- .../types/tokens_migrations_test.cljc | 56 +- .../data/workspace/get-file-13958.json | 21 +- .../data/workspace/get-file-inspect-tab.json | 10 + .../get-file-layout-stroke-token-json | 12 + .../workspace/get-file-libraries-tokens.json | 23 + .../workspace/get-file-token-tooltip.json | 14 +- .../workspace/get-file-tokens-all-types.json | 16 +- .../data/workspace/get-file-tokens.json | 16 +- .../workspace/get-file-typography-tokens.json | 13 + .../data/workspace/get-file-with-lib.json | 60 + .../workspace/multiselection-typography.json | 24 +- frontend/playwright/ui/pages/WorkspacePage.js | 7 +- .../playwright/ui/specs/assets-tab.spec.js | 3 + .../ui/specs/inspect-layout.spec.js | 1 + .../ui/specs/multiseleccion.spec.js | 2 +- .../specs/text-options-missing-font.spec.js | 2 +- .../ui/specs/tokens/libraries.spec.js | 163 ++ .../playwright/ui/specs/tokens/themes.spec.js | 6 + .../ui/specs/workspace-shared-library.spec.js | 4 + ...workspace-texts-typography-persist.spec.js | 2 +- .../playwright/ui/specs/workspace.spec.js | 1 + frontend/src/app/main/data/helpers.cljs | 18 + .../app/main/data/workspace/libraries.cljs | 95 +- .../src/app/main/data/workspace/pages.cljs | 1 + .../src/app/main/data/workspace/texts.cljs | 62 - .../app/main/data/workspace/texts_events.cljs | 85 + .../data/workspace/tokens/application.cljs | 18 +- .../data/workspace/tokens/library_edit.cljs | 189 ++- .../data/workspace/tokens/propagation.cljs | 49 +- .../data/workspace/tokens/selected_set.cljs | 12 +- frontend/src/app/main/refs.cljs | 29 +- .../src/app/main/ui/components/title_bar.scss | 1 + frontend/src/app/main/ui/context.cljs | 3 + .../main/ui/ds/foundations/assets/icon.cljs | 8 +- .../app/main/ui/ds/product/empty_state.cljs | 2 +- frontend/src/app/main/ui/inspect/styles.cljs | 19 +- .../inspect/styles/panels/tokens_panel.cljs | 6 +- .../app/main/ui/workspace/colorpicker.cljs | 14 +- .../src/app/main/ui/workspace/libraries.cljs | 581 ++++++- .../src/app/main/ui/workspace/libraries.scss | 137 +- .../src/app/main/ui/workspace/sidebar.cljs | 74 +- .../app/main/ui/workspace/sidebar/assets.cljs | 15 +- .../sidebar/assets/file_library.cljs | 39 +- .../sidebar/assets/file_library.scss | 138 +- .../sidebar/assets/typographies.cljs | 5 +- .../main/ui/workspace/tokens/management.cljs | 19 +- .../tokens/management/context_menu.cljs | 55 +- .../ui/workspace/tokens/management/group.cljs | 6 +- .../tokens/management/token_tree.cljs | 15 +- .../app/main/ui/workspace/tokens/sets.cljs | 35 +- .../main/ui/workspace/tokens/sets/lists.cljs | 51 +- .../app/main/ui/workspace/tokens/sidebar.cljs | 75 +- .../app/main/ui/workspace/tokens/sidebar.scss | 2 +- .../app/main/ui/workspace/tokens/themes.cljs | 16 +- .../app/main/ui/workspace/tokens/themes.scss | 2 +- .../workspace/tokens/themes/create_modal.cljs | 74 +- .../tokens/themes/theme_selector.cljs | 116 +- .../ui/workspace/tokens/tokens_source.cljs | 104 ++ .../ui/workspace/tokens/tokens_source.scss | 77 + frontend/src/app/plugins/library.cljs | 2 +- frontend/src/app/plugins/tokens.cljs | 231 +-- frontend/src/app/plugins/utils.cljs | 21 +- .../data/workspace_texts_test.cljs | 7 +- .../logic/components_and_tokens.cljs | 126 +- .../frontend_tests/plugins/tokens_test.cljs | 132 +- frontend/test/frontend_tests/runner.cljs | 2 + .../tokens/context_menu_test.cljs | 80 +- .../frontend_tests/tokens/helpers/state.cljs | 41 +- .../frontend_tests/tokens/helpers/tokens.cljs | 8 +- .../tokens/logic/token_actions_test.cljs | 43 +- .../tokens/logic/token_data_test.cljs | 814 +++++++++- .../tokens/logic/token_remapping_test.cljs | 41 +- .../tokens/logic/tokens_status_test.cljs | 118 ++ .../tokens/workspace_tokens_remap_test.cljs | 29 +- frontend/translations/en.po | 70 +- frontend/translations/es.po | 68 + .../src/app/app.component.html | 134 +- .../src/app/app.component.ts | 7 + plugins/apps/poc-tokens-plugin/src/plugin.ts | 8 + plugins/libs/plugin-types/index.d.ts | 8 + 98 files changed, 7013 insertions(+), 1995 deletions(-) create mode 100644 frontend/playwright/data/workspace/get-file-libraries-tokens.json create mode 100644 frontend/playwright/data/workspace/get-file-with-lib.json create mode 100644 frontend/playwright/ui/specs/tokens/libraries.spec.js create mode 100644 frontend/src/app/main/data/workspace/texts_events.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/tokens_source.cljs create mode 100644 frontend/src/app/main/ui/workspace/tokens/tokens_source.scss create mode 100644 frontend/test/frontend_tests/tokens/logic/tokens_status_test.cljs diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 9b518ca9d6..e10dcd4b9e 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -14,6 +14,7 @@ [app.common.files.helpers :as cfh] [app.common.files.migrations :as fmg] [app.common.files.stats :as cfs] + [app.common.files.tokens :as cfo] [app.common.logging :as l] [app.common.schema :as sm] [app.common.schema.desc-js-like :as-alias smdj] @@ -21,6 +22,7 @@ [app.common.transit :as t] [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] + [app.common.types.tokens-lib :as ctob] [app.common.uri :as uri] [app.config :as cf] [app.db :as db] @@ -520,12 +522,20 @@ components-sample (-> (sample-assets components 4) - (update :sample load-objects))] + (update :sample load-objects)) + + tokens-lib (cfo/get-tokens-lib data) + tokens-count (if (some? tokens-lib) (count (ctob/get-all-tokens tokens-lib)) 0) + token-sets-count (if (some? tokens-lib) (count (ctob/get-sets tokens-lib)) 0) + token-themes-count (if (some? tokens-lib) (count (ctob/get-themes-no-hidden tokens-lib)) 0)] {:components components-sample :variants {:count (count variant-ids)} :colors (sample-assets (:colors data) 3) - :typographies (sample-assets (:typographies data) 3)})) + :typographies (sample-assets (:typographies data) 3) + :tokens-count tokens-count + :token-sets-count token-sets-count + :token-themes-count token-themes-count})) (def ^:private file-summary-cache-key-ttl (ct/duration {:days 30})) diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj index 519a32ba07..9d996c6dd4 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -106,7 +106,6 @@ :set-token :set-token-set :set-token-theme - :set-active-token-themes :rename-token-set-group :move-token-set :move-token-set-group diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index 34148aa0b2..0c6700deb9 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.files.helpers :as cfh] + [app.common.files.tokens :as cfo] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.schema :as sm] @@ -29,6 +30,7 @@ [app.common.types.shape-tree :as ctst] [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.types.typographies-list :as ctyl] [app.common.types.typography :as ctt] [app.common.types.variant :as ctv] @@ -398,10 +400,11 @@ [:id ::sm/uuid] [:attrs [:maybe ctob/schema:token-theme-attrs]]]] - [:set-active-token-themes - [:map {:title "SetActiveTokenThemes"} - [:type [:= :set-active-token-themes]] - [:theme-paths [:set :string]]]] + [:set-tokens-status + [:map {:title "SetTokensStatus"} + [:type [:= :set-tokens-status]] + [:theme-ids [:set ::sm/uuid]] + [:set-ids [:set ::sm/uuid]]]] [:rename-token-set-group [:map {:title "RenameTokenSetGroup"} @@ -428,7 +431,13 @@ [:set-base-font-size [:map {:title "ModBaseFontSize"} [:type [:= :set-base-font-size]] - [:base-font-size :string]]]]) + [:base-font-size :string]]] + + [:set-tokens-source + [:map {:title "SetTokensSource"} + [:type [:= :set-tokens-source]] + [:file-id ::sm/uuid] + [:library-id [:maybe ::sm/uuid]]]]]) (def schema:changes [:sequential {:gen/max 5 :gen/min 1} schema:change]) @@ -992,77 +1001,73 @@ (defmethod process-change :set-token [data {:keys [set-id token-id attrs]}] - (update data :tokens-lib - (fn [lib] - (let [lib' (ctob/ensure-tokens-lib lib)] - (cond - (not attrs) - (ctob/delete-token lib' set-id token-id) + (-> (cfo/ensure-tokens-lib data) + (cfo/update-tokens-lib + (fn [lib'] + (cond + (not attrs) + (ctob/delete-token lib' set-id token-id) - (not (ctob/get-token lib' set-id token-id)) - (ctob/add-token lib' set-id (ctob/make-token attrs)) + (not (ctob/get-token lib' set-id token-id)) + (ctob/add-token lib' set-id (ctob/make-token attrs)) - :else - (ctob/update-token lib' set-id token-id - (fn [prev-token] - (ctob/make-token (merge prev-token attrs))))))))) + :else + (ctob/update-token lib' set-id token-id + (fn [prev-token] + (ctob/make-token (merge prev-token attrs))))))))) (defmethod process-change :set-token-set [data {:keys [id attrs]}] - (update data :tokens-lib - (fn [lib] - (let [lib' (ctob/ensure-tokens-lib lib)] - (cond - (not attrs) - (ctob/delete-set lib' id) + (-> (cfo/ensure-tokens-lib data) + (cfo/update-tokens-lib + (fn [lib'] + (cond + (not attrs) + (ctob/delete-set lib' id) - (not (ctob/get-set lib' id)) - (ctob/add-set lib' (ctob/make-token-set attrs)) + (not (ctob/get-set lib' id)) + (ctob/add-set lib' (ctob/make-token-set attrs)) - :else - (ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs)))))))) + :else + (ctob/update-set lib' id (fn [_] (ctob/make-token-set attrs)))))))) (defmethod process-change :set-token-theme [data {:keys [id attrs]}] - (update data :tokens-lib - (fn [lib] - (let [lib' (ctob/ensure-tokens-lib lib)] - (cond - (not attrs) - (ctob/delete-theme lib' id) + (-> (cfo/ensure-tokens-lib data) + (cfo/update-tokens-lib + (fn [lib'] + (cond + (not attrs) + (ctob/delete-theme lib' id) - (not (ctob/get-theme lib' id)) - (ctob/add-theme lib' (ctob/make-token-theme attrs)) + (not (ctob/get-theme lib' id)) + (ctob/add-theme lib' (ctob/make-token-theme attrs)) - :else - (ctob/update-theme lib' - id - (fn [prev-token-theme] - (ctob/make-token-theme (merge prev-token-theme attrs))))))))) + :else + (ctob/update-theme lib' + id + (fn [prev-token-theme] + (ctob/make-token-theme (merge prev-token-theme attrs))))))))) -(defmethod process-change :set-active-token-themes - [data {:keys [theme-paths]}] - (update data :tokens-lib #(-> % (ctob/ensure-tokens-lib) - (ctob/set-active-themes theme-paths)))) +(defmethod process-change :set-tokens-status + [data {:keys [theme-ids set-ids]}] + (-> (cfo/ensure-tokens-lib data) + (cfo/update-tokens-status ctos/set-tokens-status theme-ids set-ids))) (defmethod process-change :rename-token-set-group [data {:keys [set-group-path set-group-fname]}] - (update data :tokens-lib (fn [lib] - (-> lib - (ctob/ensure-tokens-lib) - (ctob/rename-set-group set-group-path set-group-fname))))) + (-> (cfo/ensure-tokens-lib data) + (cfo/update-tokens-lib ctob/rename-set-group set-group-path set-group-fname))) (defmethod process-change :move-token-set [data {:keys [from-path to-path before-path before-group] :as changes}] - (update data :tokens-lib #(-> % - (ctob/ensure-tokens-lib) - (ctob/move-set from-path to-path before-path before-group)))) + (-> (cfo/ensure-tokens-lib data) + (cfo/update-tokens-lib ctob/move-set from-path to-path before-path before-group))) (defmethod process-change :move-token-set-group [data {:keys [from-path to-path before-path before-group]}] - (update data :tokens-lib #(-> % - (ctob/ensure-tokens-lib) - (ctob/move-set-group from-path to-path before-path before-group)))) + (-> (cfo/ensure-tokens-lib data) + (cfo/update-tokens-lib ctob/move-set-group from-path to-path before-path before-group))) ;; === Design Tokens configuration @@ -1070,10 +1075,13 @@ [data {:keys [base-font-size]}] (ctf/set-base-font-size data base-font-size)) +(defmethod process-change :set-tokens-source + [data {:keys [library-id]}] + (cfo/set-tokens-source data library-id)) ;; === Operations -(def decode-shape-attrs +(def decode-shape-attrs (sm/decoder cts/schema:shape-attrs sm/json-transformer)) (defmethod process-operation :assign @@ -1242,3 +1250,37 @@ (defmethod frames-changed :default [_ _] nil) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Design Tokens changes detection +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private tokens-lib-change-types + "Set of change types that modify the tokens library." + #{:set-tokens-lib + :set-token + :set-token-set + :set-token-theme + :rename-token-set-group + :move-token-set + :move-token-set-group}) + +(defn tokens-lib-changed? + "Check if a commit contains changes that modify the tokens library." + [changes] + (some #(tokens-lib-change-types (:type %)) changes)) + +(def ^:private notifiable-token-change-types + "Set of change types that modify the structure of the tokens library sets + or themes (as opposed to `:set-token`, which only edits a token's value)." + #{:set-token-set + :set-token-theme + :rename-token-set-group + :move-token-set + :move-token-set-group}) + +(defn notifiable-token-change-occured? + "Check if a commit contains changes that modify the tokens library sets or + themes structure (renamed/added/removed sets, theme set-membership, etc.)." + [changes] + (some #(notifiable-token-change-types (:type %)) changes)) diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index a0c733fecb..b138c48b41 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -10,6 +10,7 @@ [app.common.data.macros :as dm] [app.common.files.changes :as cfc] [app.common.files.helpers :as cfh] + [app.common.files.tokens :as cfo] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.rect :as grc] @@ -22,6 +23,7 @@ [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [clojure.datafy :refer [datafy]])) @@ -987,7 +989,7 @@ [changes tokens-lib] (assert-library! changes) (let [library-data (::library-data (meta changes)) - prev-tokens-lib (get library-data :tokens-lib)] + prev-tokens-lib (cfo/get-tokens-lib library-data)] (-> changes (update :redo-changes conj {:type :set-tokens-lib :tokens-lib tokens-lib}) (update :undo-changes conj {:type :set-tokens-lib :tokens-lib prev-tokens-lib}) @@ -996,7 +998,7 @@ (defn set-token [changes set-id token-id token] (assert-library! changes) (let [library-data (::library-data (meta changes)) - prev-token (some-> (get library-data :tokens-lib) + prev-token (some-> (cfo/get-tokens-lib library-data) (ctob/get-token set-id token-id))] (-> changes (update :redo-changes conj {:type :set-token @@ -1013,7 +1015,7 @@ [changes id token-set] (assert-library! changes) (let [library-data (::library-data (meta changes)) - prev-token-set (some-> (get library-data :tokens-lib) + prev-token-set (some-> (cfo/get-tokens-lib library-data) (ctob/get-set id))] (-> changes (update :redo-changes conj {:type :set-token-set @@ -1028,7 +1030,7 @@ [changes id new-name] (assert-library! changes) (let [library-data (::library-data (meta changes)) - prev-token-set (some-> (get library-data :tokens-lib) + prev-token-set (some-> (cfo/get-tokens-lib library-data) (ctob/get-set id))] (-> changes (update :redo-changes conj {:type :set-token-set @@ -1042,7 +1044,7 @@ (defn set-token-theme [changes id theme] (assert-library! changes) (let [library-data (::library-data (meta changes)) - prev-theme (some-> (get library-data :tokens-lib) + prev-theme (some-> (cfo/get-tokens-lib library-data) (ctob/get-theme id))] (-> changes (update :redo-changes conj {:type :set-token-theme @@ -1053,17 +1055,21 @@ :attrs (datafy prev-theme)}) (apply-changes-local)))) -(defn set-active-token-themes - [changes active-theme-paths] - (assert-library! changes) - (let [library-data (::library-data (meta changes)) - prev-active-theme-paths (d/nilv (some-> (get library-data :tokens-lib) - (ctob/get-active-theme-paths)) - #{})] - (-> changes - (update :redo-changes conj {:type :set-active-token-themes :theme-paths active-theme-paths}) - (update :undo-changes conj {:type :set-active-token-themes :theme-paths prev-active-theme-paths}) - (apply-changes-local)))) +(defn set-tokens-status + ([changes tokens-status] + (assert-library! changes) + (assert (or (ctos/tokens-status? tokens-status) + (nil? tokens-status))) + (let [theme-ids (if tokens-status (ctos/get-active-theme-ids tokens-status) #{}) + set-ids (if tokens-status (ctos/get-active-set-ids tokens-status) #{}) + library-data (::library-data (meta changes)) + prev-tokens-status (cfo/get-tokens-status library-data) + prev-theme-ids (if prev-tokens-status (ctos/get-active-theme-ids prev-tokens-status) #{}) + prev-set-ids (if prev-tokens-status (ctos/get-active-set-ids prev-tokens-status) #{})] + (-> changes + (update :redo-changes conj {:type :set-tokens-status :theme-ids theme-ids :set-ids set-ids}) + (update :undo-changes conj {:type :set-tokens-status :theme-ids prev-theme-ids :set-ids prev-set-ids}) + (apply-changes-local))))) (defn rename-token-set-group [changes set-group-path set-group-fname] @@ -1117,6 +1123,21 @@ :base-font-size previous-font-size}) (apply-changes-local)))) +(defn set-tokens-source + [changes library-id] + (assert-library! changes) + (let [library-data (::library-data (meta changes)) + file-id (:id library-data) + prev-val (cfo/get-tokens-source library-data)] + (-> changes + (update :redo-changes conj {:type :set-tokens-source + :file-id file-id + :library-id library-id}) + (update :undo-changes conj {:type :set-tokens-source + :file-id file-id + :library-id prev-val}) + (apply-changes-local)))) + ;; Misc changes (defn reorder-children diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index de8d1b8f96..cdc990a746 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -13,6 +13,7 @@ [app.common.files.comp-processors :as cfcp] [app.common.files.defaults :as cfd] [app.common.files.helpers :as cfh] + [app.common.files.tokens :as cfo] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.rect :as grc] @@ -1998,6 +1999,13 @@ (update :pages-index d/update-vals update-container) (d/update-when :components d/update-vals update-container)))) +(defmethod migrate-data "0027-separate-tokens-status" + [data _] + (if-let [tokens-lib (:tokens-lib data)] + (assoc data :tokens-status + (cfo/make-tokens-status-from-lib tokens-lib)) + data)) + (def available-migrations (into (d/ordered-set) ["legacy-2" @@ -2081,4 +2089,5 @@ "0023-repair-token-themes-with-inexistent-sets" "0024b-fix-stroke-cap-placement" "0025-repair-empty-text-content" - "0026-fix-svg-raw-shapes-uuids"])) + "0026-fix-svg-raw-shapes-uuids" + "0027-separate-tokens-status"])) diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc index 0bbec120bb..9fb37c5e37 100644 --- a/common/src/app/common/files/repair.cljc +++ b/common/src/app/common/files/repair.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] + [app.common.files.tokens :as cfo] [app.common.logging :as log] [app.common.path-names :as cpn] [app.common.types.component :as ctk] @@ -783,6 +784,16 @@ (pcb/with-file-data file-data) (pcb/update-shapes [(:id shape)] repair-shape)))) +(defmethod repair-error :missing-tokens-status + [_ _ file-data _] + (let [tokens-lib (cfo/get-tokens-lib file-data) + tokens-status (cfo/make-tokens-status-from-lib tokens-lib)] + (log/debug :hint "repairing shape :tokens-status-missing") + (log/debug :hint " -> add a tokens-status generated from the library") + (-> (pcb/empty-changes nil) + (pcb/with-library-data file-data) + (pcb/set-tokens-status tokens-status)))) + (defmethod repair-error :default [_ error file _] (log/error :hint "Unknown error code, don't know how to repair" :code (:code error)) diff --git a/common/src/app/common/files/tokens.cljc b/common/src/app/common/files/tokens.cljc index 89678417d0..a673e393d1 100644 --- a/common/src/app/common/files/tokens.cljc +++ b/common/src/app/common/files/tokens.cljc @@ -9,13 +9,18 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.i18n :refer [tr]] + [app.common.logging :as log] [app.common.schema :as sm] [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [clojure.set :as set] [cuerdas.core :as str] [malli.core :as m])) +;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default +(log/set-level! :warn) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HIGH LEVEL SCHEMAS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -165,7 +170,8 @@ (some (fn [[token-name _]] (not (ctob/token-name-path-exists? token-name tokens-tree))) new-tokens))))]]) -(defn find-refs [value] + +(defn- find-refs [value] (cond (string? value) (cto/find-token-value-references value) @@ -344,6 +350,8 @@ ;; HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Token + (def parseable-token-value-regexp "Regexp that can be used to parse a number value out of resolved token value. This regexp also trims whitespace around the value." @@ -415,3 +423,312 @@ ;; FIXME: this should be precalculated ? (defn is-reference? [token] (str/includes? (:value token) "{")) + +;; Tokens lib and status in file data + +(defn make-tokens-status-from-lib + "Make a TokensStatus from a TokensLib, activating the themes and sets + marked as active in the library (to migrate from legacy files)." + [tokens-lib] + (assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib") + (let [active-theme-paths (ctob/get-legacy-active-themes tokens-lib) + active-theme-ids (into #{} + (comp (map #(ctob/get-theme-by-path tokens-lib %)) + (remove nil?) + (map ctob/get-id) + (filter #(not= % ctob/hidden-theme-id))) + active-theme-paths) + ;; Get active set names from the active themes + active-set-names (into #{} + (comp (filter #(and (ctob/token-theme? %) + (contains? active-theme-paths (ctob/get-theme-path %)))) + (mapcat :sets)) + (ctob/get-themes tokens-lib)) + active-set-ids (into #{} + (comp (map #(ctob/get-set-by-name tokens-lib %)) + (remove nil?) + (map ctob/get-id)) + active-set-names)] + (ctos/make-tokens-status :active-theme-ids active-theme-ids + :active-set-ids active-set-ids))) + +(defn ensure-tokens-lib + "Ensure file-data has a :tokens-lib or :tokens-source, and also a :tokens-status, creating them if necessary." + [file-data] + (cond-> file-data + (nil? (:tokens-source file-data)) + (update :tokens-lib #(or % (ctob/make-tokens-lib))) + + :always + (update :tokens-status #(or % (ctos/make-tokens-status))))) + +(defn ensure-tokens-status + "Ensure file-data has a :tokens-status, creating it empty if necessary." + [file-data] + (cond-> file-data + (nil? (:tokens-status file-data)) + (update :tokens-status #(or % (ctos/make-tokens-status))))) + +(defn get-tokens-source + "Return the current value of :tokens-source attribute." + [file-data] + (:tokens-source file-data)) + +(defn get-effective-tokens-source + "Return the current tokens source of the file. When there is no explicit + :tokens-source attribute, the source is the file itself." + [file-data] + (or (:tokens-source file-data) (:id file-data))) + +(defn set-tokens-source + [file-data tokens-source] + (assert (or (nil? tokens-source) (uuid? tokens-source)) "expected nil or valid uuid") + (if (nil? tokens-source) + (dissoc file-data :tokens-source) + (assoc file-data :tokens-source tokens-source))) + +(defn effective-tokens-source? + "Returns true if the given id is the current tokens source of the file-data." + [file-data id] + (assert (uuid? id) "expected valid uuid") + (= (get-effective-tokens-source file-data) id)) + +(defn has-own-tokens? + "Returns true if the file-data contains a tokens-library itself and the library + contains some tokens. Note that it still may be true even if the tokens source + is external (in this case the own library is inactive, but still exists)." + [file-data] + (let [tokens-lib (:tokens-lib file-data)] + (and (some? tokens-lib) + (not (ctob/empty-lib? tokens-lib))))) + +(defn tokens-provider? + "Returns true if the file MAY become a tokens source. This is if the file has tokens and has + not configured another tokens source." + [file-data] + (and (has-own-tokens? file-data) + (effective-tokens-source? file-data (:id file-data)))) + +(defn editable-tokens? + "Returns true if the file-data is its own tokens source." + [file-data] + (effective-tokens-source? file-data (:id file-data))) + +(defn get-tokens-lib + [file-data] + (:tokens-lib file-data)) + +(defn get-tokens-status + [file-data] + (:tokens-status file-data)) + +(defn update-tokens-lib + "Update the tokens-lib inside file-data through a callback function. + The function will receive the tokens lib and the rest of args." + [file-data f & args] + (d/update-when file-data :tokens-lib #(apply f % args))) + +(defn update-tokens-status + "Update the tokens-status inside file-data through a callback function. + The function will receive the tokens status and the rest of args." + [file-data f & args] + (d/update-when file-data :tokens-status #(apply f % args))) + +;; Tokens status with tokens lib + +(defn- calculate-active-sets + "Obtain the set-ids that needs to be active for a particular set of theme ids" + [active-theme-ids tokens-lib] + (let [active-themes (map #(ctob/get-theme tokens-lib %) active-theme-ids) + active-set-names (reduce set/union #{} (map :sets active-themes)) + active-sets (map #(ctob/get-set-by-name tokens-lib %) active-set-names) + active-set-ids (into #{} (map ctob/get-id) active-sets)] + active-set-ids)) + +(defn get-active-themes + "Return an ordered sequence of active themes" + [tokens-status tokens-lib] + (assert (ctos/tokens-status? tokens-status) "expected valid tokens-status") + (assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib") + (->> (ctob/get-themes tokens-lib) + (filter #(ctos/theme-active? tokens-status (ctob/get-id %))))) + +(defn activate-theme + "Activate a theme and all its sets. Deactivate any other theme in the same group." + [tokens-status tokens-lib id] + (assert (ctos/tokens-status? tokens-status) "expected valid tokens-status") + (assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib") + (assert (uuid? id) "expected valid theme id") + (if-not (ctos/theme-active? tokens-status id) + (if-let [theme (ctob/get-theme tokens-lib id)] + (let [group-themes (into #{} (ctob/get-themes-in-group tokens-lib (:group theme))) + active-theme-ids (ctos/get-active-theme-ids tokens-status) + active-theme-ids' (-> (set/difference active-theme-ids group-themes) + (conj id)) + active-set-ids' (calculate-active-sets active-theme-ids' tokens-lib)] + (ctos/set-tokens-status tokens-status active-theme-ids' active-set-ids')) + tokens-status) + tokens-status)) + +(defn deactivate-theme + "Deactivate a theme and all its sets that are not in other active themes" + [tokens-status tokens-lib id] + (assert (ctos/tokens-status? tokens-status) "expected valid tokens-status") + (assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib") + (assert (uuid? id) "expected valid theme id") + (if (ctos/theme-active? tokens-status id) + (let [active-theme-ids' (disj (ctos/get-active-theme-ids tokens-status) id) + active-set-ids' (calculate-active-sets active-theme-ids' tokens-lib)] + (ctos/set-tokens-status tokens-status active-theme-ids' active-set-ids')) + tokens-status)) + +(defn toggle-theme-active + "Toggle a theme's active state and update active sets accordingly." + [tokens-status tokens-lib id] + (if (ctos/theme-active? tokens-status id) + (deactivate-theme tokens-status tokens-lib id) + (activate-theme tokens-status tokens-lib id))) + +(defn set-theme-active + "Set a theme's active state and update active sets accordingly." + [tokens-status tokens-lib id active?] + (if active? + (activate-theme tokens-status tokens-lib id) + (deactivate-theme tokens-status tokens-lib id))) + +(defn get-active-sets + "Return a clojure set of the active sets, resolved from TokensStatus + TokensLib." + [tokens-status tokens-lib] + (assert (ctos/tokens-status? tokens-status) "expected valid tokens-status") + (assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib") + (let [active-set-ids (ctos/get-active-set-ids tokens-status)] + (into #{} + (comp (map #(ctob/get-set tokens-lib %)) + (filter some?)) + active-set-ids))) + +(defn set-set-active + "Set directly a set's active state and deactivate all themes." + [tokens-status tokens-lib id active?] + (assert (ctos/tokens-status? tokens-status) "expected valid tokens-status") + (assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib") + (assert (uuid? id) "expected valid set id") + (if (and (ctob/get-set tokens-lib id) + (not= active? (ctos/set-active? tokens-status id))) + (let [active-set-ids (ctos/get-active-set-ids tokens-status) + active-set-ids' (if active? + (conj active-set-ids id) + (disj active-set-ids id))] + (ctos/set-tokens-status tokens-status #{} active-set-ids')) + tokens-status)) + +(defn activate-set + "Activate directly a set, and deactivate all themes." + [tokens-status tokens-lib id] + (set-set-active tokens-status tokens-lib id true)) + +(defn deactivate-set + "Deactivate directly a set, and deactivate all themes." + [tokens-status tokens-lib id] + (set-set-active tokens-status tokens-lib id false)) + +(defn toggle-set-active + "Toggle a set's active state and deactivate all themes." + [tokens-status tokens-lib id] + (set-set-active tokens-status tokens-lib id (not (ctos/set-active? tokens-status id)))) + +(defn sets-at-path-all-active? + "Check active state of sets at a group path. + Returns :none, :all, or :partial." + [tokens-status tokens-lib group-path] + (assert (ctos/tokens-status? tokens-status) "expected valid tokens-status") + (assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib") + (assert (vector? group-path) "expected valid group path") + (let [active-set-ids (ctos/get-active-set-ids tokens-status) + path-set-ids (into #{} + (map ctob/get-id) + (ctob/get-sets-at-path tokens-lib group-path))] + (if (seq active-set-ids) + (let [difference (set/difference path-set-ids active-set-ids)] + (cond + (empty? difference) :all + (seq (set/intersection path-set-ids active-set-ids)) :partial + :else :none)) + :none))) + +(defn toggle-set-group-active + "Toggle the active state of all sets at a group path, and deactivate all themes. + If all sets are active, deactivate all. If none or some are active, activate all." + [tokens-status tokens-lib group-path] + (let [all-active? (sets-at-path-all-active? tokens-status tokens-lib group-path) + + sets-at-path (ctob/get-sets-at-path tokens-lib group-path) + set-ids-at-path (into #{} (map ctob/get-id) sets-at-path) + + active-set-ids (ctos/get-active-set-ids tokens-status) + active-set-ids' (if (contains? #{:all :partial} all-active?) + (set/difference active-set-ids set-ids-at-path) + (set/union active-set-ids set-ids-at-path))] + + (if (not= active-set-ids active-set-ids') + (ctos/set-tokens-status tokens-status #{} active-set-ids') + tokens-status))) + +(defn get-tokens-in-active-sets + "Get merged tokens from all active sets, in set order." + [tokens-status tokens-lib] + (assert (ctos/tokens-status? tokens-status) "expected valid tokens-status") + (assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib") + (let [active-set-ids (ctos/get-active-set-ids tokens-status) + all-set-ids (map ctob/get-id (ctob/get-sets tokens-lib)) + ordered-active (filter active-set-ids all-set-ids)] + (reduce (fn [tokens set-id] + (merge tokens (ctob/get-tokens tokens-lib set-id))) + (d/ordered-map) + ordered-active))) + +(defn get-tokens-in-active-sets-force + "Same as get-tokens-in-active-sets but force-including a set by id." + [tokens-status tokens-lib force-set-id] + (assert (ctos/tokens-status? tokens-status) "expected valid tokens-status") + (assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib") + (assert (uuid? force-set-id) "expected valid set id") + (let [active-set-ids (ctos/get-active-set-ids tokens-status) + all-set-ids (map ctob/get-id (ctob/get-sets tokens-lib)) + ordered-active (cond-> (filter active-set-ids all-set-ids) + (some? force-set-id) + (conj force-set-id))] + (reduce (fn [tokens set-id] + (let [set (ctob/get-set tokens-lib set-id)] + (if set + (merge tokens (ctob/get-tokens- set)) + tokens))) + (d/ordered-map) + ordered-active))) + +(defn sync-tokens-status-with-lib + "Synchronizes tokens status with the current tokens lib: + - Delete any theme or set that no longer exists in the lib. + - Recalculate the list of active sets from the new themes." + [tokens-status tokens-lib] + (assert (ctos/tokens-status? tokens-status) "expected valid tokens-status") + (assert (ctob/tokens-lib? tokens-lib) "expected valid tokens-lib") + (let [active-theme-ids (ctos/get-active-theme-ids tokens-status) + valid-theme-ids (into #{} + (filter #(some? (ctob/get-theme tokens-lib %))) + active-theme-ids) + active-set-ids (ctos/get-active-set-ids tokens-status) + valid-set-ids (if (empty? valid-theme-ids) + (into #{} + (filter #(some? (ctob/get-set tokens-lib %))) + active-set-ids) + (calculate-active-sets valid-theme-ids tokens-lib))] + + (if (or (not= active-theme-ids valid-theme-ids) + (not= active-set-ids valid-set-ids)) + (do + (log/info :hint "syncing token status" + :removed-themes (count (set/difference active-theme-ids valid-theme-ids)) + :removed-sets (count (set/difference active-set-ids valid-set-ids))) + (ctos/set-tokens-status tokens-status valid-theme-ids valid-set-ids)) + tokens-status))) diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index 23a7b46957..280f468d10 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -73,7 +73,8 @@ :variant-main-bad-name :variant-main-bad-variant-name :variant-component-bad-name - :variant-component-bad-id}) + :variant-component-bad-id + :missing-tokens-status}) (def ^:private schema:error [:map {:title "ValidationError"} @@ -816,6 +817,17 @@ (transient []) objects))) +(defn check-tokens + "Check tokens in the file. The internal structure should be correct because + of the schemas and protocol functions. But we need to check for the case that + the tokens-status is missing due to a migration or import bug." + [{:keys [data] :as file}] + (when (and (some? (:tokens-lib data)) + (nil? (:tokens-status data))) + (report-error :missing-tokens-status + "Tokens library is present but tokens-status is missing" + nil file nil))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PUBLIC API: VALIDATION FUNCTIONS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -853,6 +865,8 @@ nil (:components data)) + (check-tokens file) + (-> *errors* deref not-empty)))) (defn validate-shape diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 86dbf36fd1..f15d153085 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -137,6 +137,7 @@ :token-tokenscript :token-import-from-library :token-typography-row + :token-lib-sync ;; Only for developtment. :transit-readable-response diff --git a/common/src/app/common/logic/tokens.cljc b/common/src/app/common/logic/tokens.cljc index 493d23d1f4..b09145ec1c 100644 --- a/common/src/app/common/logic/tokens.cljc +++ b/common/src/app/common/logic/tokens.cljc @@ -6,84 +6,31 @@ (ns app.common.logic.tokens (:require + [app.common.files.changes :as ch] [app.common.files.changes-builder :as pcb] - [app.common.types.tokens-lib :as ctob])) + [app.common.files.tokens :as cfo] + [app.common.types.file :as ctf] + [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos])) -(defn- generate-update-active-sets - "Copy the active sets from the currently active themes and move them - to the hidden token theme and update the theme with - `update-theme-fn`. +;; Tokens source - Use this for managing sets active state without having to modify a - user created theme (\"no themes selected\" state in the ui)." - [changes tokens-lib update-theme-fn] - (let [active-token-set-names (ctob/get-active-themes-set-names tokens-lib) - - hidden-theme (ctob/get-hidden-theme tokens-lib) - hidden-theme' (-> (some-> hidden-theme - (ctob/set-sets active-token-set-names)) - (update-theme-fn))] +(defn generate-set-tokens-source + "Create changes for setting the tokens source of a file to `library`, + copying the library's tokens status. If the library is nil, the tokens source + is removed and the tokens status is cleared." + [changes library] + (let [library-id (:id library) + library-tokens-status (if library + (-> library ctf/file-data cfo/get-tokens-status) + (ctos/make-tokens-status))] (-> changes - (pcb/set-active-token-themes #{(ctob/get-theme-path hidden-theme')}) - (pcb/set-token-theme (ctob/get-id hidden-theme) - hidden-theme')))) + (pcb/set-tokens-source library-id) + (pcb/set-tokens-status library-tokens-status)))) -(defn generate-set-enabled-token-set - "Enable or disable a token set at `set-name` in `tokens-lib` without modifying a user theme." - [changes tokens-lib set-name enabled?] - (if enabled? - (generate-update-active-sets changes tokens-lib #(ctob/enable-set % set-name)) - (generate-update-active-sets changes tokens-lib #(ctob/disable-set % set-name)))) +;; Tokens lib -(defn generate-toggle-token-set - "Toggle a token set at `set-name` in `tokens-lib` without modifying a user theme." - [changes tokens-lib set-name] - (generate-update-active-sets changes tokens-lib #(ctob/toggle-set % set-name))) - -(defn- generate-update-active-token-theme - "Change the active state of a theme in `tokens-lib`. If after the change there is - any active theme other than the hidden one, deactivate the hidden theme." - [changes tokens-lib update-fn] - (let [active-token-themes (some-> tokens-lib - (update-fn) - (ctob/get-active-theme-paths)) - active-token-themes' (if (= active-token-themes #{ctob/hidden-theme-path}) - active-token-themes - (disj active-token-themes ctob/hidden-theme-path))] - (pcb/set-active-token-themes changes active-token-themes'))) - -(defn generate-set-active-token-theme - "Activate or deactivate a token theme in `tokens-lib`." - [changes tokens-lib id active?] - (if active? - (generate-update-active-token-theme changes tokens-lib - #(ctob/activate-theme % id)) - (generate-update-active-token-theme changes tokens-lib - #(ctob/deactivate-theme % id)))) - -(defn generate-toggle-token-theme - "Toggle the active state of a token theme in `tokens-lib`." - [changes tokens-lib id] - (generate-update-active-token-theme changes tokens-lib - #(ctob/toggle-theme-active % id))) - -(defn toggle-token-set-group - "Toggle a token set group at `group-path` in `tokens-lib` for a `tokens-lib-theme`." - [group-path tokens-lib tokens-lib-theme] - (let [deactivate? (contains? #{:all :partial} (ctob/sets-at-path-all-active? tokens-lib group-path)) - sets-names (->> (ctob/get-sets-at-path tokens-lib group-path) - (map ctob/get-name) - (into #{}))] - (if deactivate? - (ctob/disable-sets tokens-lib-theme sets-names) - (ctob/enable-sets tokens-lib-theme sets-names)))) - -(defn generate-toggle-token-set-group - "Toggle a token set group at `group-path` in `tokens-lib` without modifying a user theme." - [changes tokens-lib group-path] - (generate-update-active-sets changes tokens-lib #(toggle-token-set-group group-path tokens-lib %))) - -(defn vec-starts-with? [v1 v2] +(defn- vec-starts-with? [v1 v2] (= (subvec v1 0 (min (count v1) (count v2))) v2)) (defn- calculate-move-token-set-or-set-group @@ -155,27 +102,109 @@ prev-before (assoc :prev-before-path (:path prev-before) :prev-before-group? (:group? prev-before)))))) +(declare generate-sync-tokens-status-with-lib) + +(defn generate-update-token-theme + "Create changes for updating a token theme and regenerating the activation status + of the sets inside it." + [changes token-theme] + (let [changes' (pcb/set-token-theme changes (ctob/get-id token-theme) token-theme) + data (pcb/get-library-data changes') + tokens-status (cfo/get-tokens-status data) + tokens-lib' (-> data + (ch/process-changes (:redo-changes changes')) + (cfo/get-tokens-lib))] + (generate-sync-tokens-status-with-lib changes' + tokens-status + tokens-lib'))) + (defn generate-move-token-set "Create changes for dropping a token set or token set. Throws for impossible moves." [changes tokens-lib params] - (if-let [params (calculate-move-token-set-or-set-group tokens-lib params)] - (pcb/move-token-set changes params) + (if tokens-lib + (if-let [params (calculate-move-token-set-or-set-group tokens-lib params)] + (pcb/move-token-set changes params) + changes) changes)) (defn generate-move-token-set-group "Create changes for dropping a token set or token set group. Throws for impossible moves" [changes tokens-lib params] - (if-let [params (calculate-move-token-set-or-set-group tokens-lib params)] - (pcb/move-token-set-group changes params) + (if tokens-lib + (if-let [params (calculate-move-token-set-or-set-group tokens-lib params)] + (pcb/move-token-set-group changes params) + changes) changes)) (defn generate-delete-token-set-group "Create changes for deleting a token set group." [changes tokens-lib path] - (let [sets (ctob/get-sets-at-path tokens-lib path)] - (reduce (fn [changes set] - (pcb/set-token-set changes (ctob/get-id set) nil)) - changes - sets))) \ No newline at end of file + (if tokens-lib + (let [sets (ctob/get-sets-at-path tokens-lib path)] + (reduce (fn [changes set] + (pcb/set-token-set changes (ctob/get-id set) nil)) + changes + sets)) + changes)) + +;; Tokens Status + +(defn- update-tokens-status + [changes tokens-status update-fn & args] + (if tokens-status + (let [tokens-status' (apply update-fn tokens-status args)] + (if (not= tokens-status tokens-status') + (pcb/set-tokens-status changes tokens-status') + changes)) + changes)) + +(defn generate-activate-theme + [changes tokens-status tokens-lib id] + (if tokens-lib + (update-tokens-status changes tokens-status cfo/activate-theme tokens-lib id) + changes)) + +(defn generate-deactivate-theme + [changes tokens-status tokens-lib id] + (if tokens-lib + (update-tokens-status changes tokens-status cfo/deactivate-theme tokens-lib id) + changes)) + +(defn generate-set-theme-status + [changes tokens-status tokens-lib id active?] + (if tokens-lib + (update-tokens-status changes tokens-status cfo/set-theme-active tokens-lib id active?) + changes)) + +(defn generate-toggle-theme + [changes tokens-status tokens-lib id] + (if tokens-lib + (update-tokens-status changes tokens-status cfo/toggle-theme-active tokens-lib id) + changes)) + +(defn generate-set-enabled-token-set + [changes tokens-status tokens-lib id enabled?] + (if tokens-lib + (update-tokens-status changes tokens-status cfo/set-set-active tokens-lib id enabled?) + changes)) + +(defn generate-toggle-token-set + [changes tokens-status tokens-lib id] + (if tokens-lib + (update-tokens-status changes tokens-status cfo/toggle-set-active tokens-lib id) + changes)) + +(defn generate-toggle-token-set-group + [changes tokens-status tokens-lib group-path] + (if tokens-lib + (update-tokens-status changes tokens-status cfo/toggle-set-group-active tokens-lib group-path) + changes)) + +(defn generate-sync-tokens-status-with-lib + [changes tokens-status tokens-lib] + (if tokens-lib + (update-tokens-status changes tokens-status cfo/sync-tokens-status-with-lib tokens-lib) + changes)) + diff --git a/common/src/app/common/test_helpers/tokens.cljc b/common/src/app/common/test_helpers/tokens.cljc index 81697ac4b8..23a0550a4a 100644 --- a/common/src/app/common/test_helpers/tokens.cljc +++ b/common/src/app/common/test_helpers/tokens.cljc @@ -7,6 +7,7 @@ (ns app.common.test-helpers.tokens (:require [app.common.data :as d] + [app.common.files.tokens :as cfo] [app.common.test-helpers.files :as thf] [app.common.test-helpers.shapes :as ths] [app.common.types.container :as ctn] @@ -17,17 +18,39 @@ [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob])) +(defn get-tokens-source + [file] + (-> file (ctf/file-data) (cfo/get-tokens-source))) + (defn get-tokens-lib [file] - (:tokens-lib (ctf/file-data file))) + (-> file (ctf/file-data) (cfo/get-tokens-lib))) + +(defn get-tokens-status + [file] + (-> file (ctf/file-data) (cfo/get-tokens-status))) (defn add-tokens-lib + "Ensure the file has a tokens-lib and a tokens-status in its data, creating empty ones if not" [file] - (ctf/update-file-data file #(update % :tokens-lib ctob/ensure-tokens-lib))) + (ctf/update-file-data file cfo/ensure-tokens-lib)) (defn update-tokens-lib + "Modify the tokens-lib of a file " [file f] - (ctf/update-file-data file #(update % :tokens-lib f))) + (ctf/update-file-data file #(cfo/update-tokens-lib % f))) + +(defn update-tokens-status + [file f] + (ctf/update-file-data file #(cfo/update-tokens-status % f))) + +(defn sample-file-with-tokens + [& {:keys [lib-fn status-fn file-id] :as params + :or {lib-fn identity status-fn identity file-id :file1}}] + (-> (thf/sample-file file-id (dissoc params :lib-fn :status-fn)) + (add-tokens-lib) + (update-tokens-lib lib-fn) + (update-tokens-status status-fn))) (defn get-token [file set-id token-id] diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index bcc4ddc44d..c4ab29e3da 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -87,7 +87,7 @@ [:components {:optional true} schema:components] [:typographies {:optional true} schema:typographies] [:plugin-data {:optional true} schema:plugin-data] - [:tokens-source {:optional true} ::sm/uuid] ;; Forward-compat: UUID of external library containing tokens-lib (full support in follow-up PR) + [:tokens-source {:optional true} ::sm/uuid] ;; The tokens-lib may be in this file or in an external library [:tokens-lib {:optional true} ctob/schema:tokens-lib] [:tokens-status {:optional true} ctos/schema:tokens-status]]) diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 1fe5b87658..a99a9502d2 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -18,6 +18,7 @@ [app.common.time :as ct] [app.common.transit :as t] [app.common.types.token :as cto] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [clojure.core.protocols :as cp] [clojure.datafy :refer [datafy]] @@ -479,11 +480,6 @@ [path] (mapv add-set-path-group-prefix path)) -(defn- set-group-path->set-group-prefixed-path-str - [path] - (-> (set-group-path->set-group-prefixed-path path) - (join-set-path))) - (defn add-set-group-prefix [group-path] (str set-group-prefix group-path)) @@ -547,7 +543,7 @@ "Get an ordered sequence of all sets names in the library") (get-set-tree [_] "Get a nested tree of all sets in the library") - (get-sets-at-path [_ path-str] + (get-sets-at-path [_ path-vec] "Get an ordered sequence of sets under `path` in the library")) (def ^:private schema:token-set-node @@ -584,8 +580,7 @@ (disable-sets [_ set-names] "disable several sets in theme") (toggle-set [_ set-name] "toggle a set enabled / disabled in the theme") (update-set-name [_ prev-set-name set-name] "update set-name when it exists") - (theme-matches-group-name [_ group name] "if a theme matches the given group & name") - (hidden-theme? [_] "if a theme is the (from the user ui) hidden temporary theme")) + (theme-matches-group-name [_ group name] "if a theme matches the given group & name")) (def hidden-theme-id uuid/zero) @@ -659,10 +654,12 @@ (theme-matches-group-name [this group name] (and (= (:group this) group) - (= (:name this) name))) + (= (:name this) name)))) - (hidden-theme? [this] - (theme-matches-group-name this hidden-theme-group hidden-theme-name))) +(defn hidden-theme? + "Check if a theme is the hidden temporary theme." + [theme] + (theme-matches-group-name theme hidden-theme-group hidden-theme-name)) (defmethod pp/simple-dispatch TokenTheme [^TokenTheme obj] @@ -755,11 +752,15 @@ (def ^:private theme-separator "/") -(defn- join-theme-path [group name] - (cpn/join-path [group name] :separator theme-separator :with-spaces? false)) +(defn- join-theme-path [group name with-spaces?] + (let [path (if (and (str/empty? group) with-spaces?) [name] [group name])] + (cpn/join-path path :separator theme-separator :with-spaces? with-spaces?))) -(defn get-theme-path [theme] - (join-theme-path (:group theme) (:name theme))) +(defn get-theme-path + ([theme] + (get-theme-path theme false)) + ([theme with-spaces?] + (join-theme-path (:group theme) (:name theme) with-spaces?))) (defn split-theme-path [path] (cpn/split-group-name path @@ -767,7 +768,7 @@ :with-spaces? false)) (def hidden-theme-path - (join-theme-path hidden-theme-group hidden-theme-name)) + (join-theme-path hidden-theme-group hidden-theme-name false)) ;; === TokenThemes (collection) @@ -778,18 +779,13 @@ (delete-theme [_ id] "delete a theme in the library") (theme-count [_] "get the total number if themes in the library") (get-theme-tree [_] "get a nested tree of all themes in the library") + (get-theme-tree-no-hidden [_] "get a nested tree of all themes in the library except the hidden theme") (get-themes [_] "get an ordered sequence of all themes in the library") + (get-themes-no-hidden [_] "get an ordered sequence of all themes in the library except the hidden theme") + (get-themes-in-group [_ group] "get an ordered sequence of the themes in the group") (get-theme [_ id] "get one theme looking for id") (get-theme-by-name [_ group name] "get one theme looking for group and name") - (get-theme-groups [_] "get a sequence of group names by order") - (get-active-theme-paths [_] "get the active theme paths") - (get-active-themes [_] "get an ordered sequence of active themes in the library") - (set-active-themes [_ active-themes] "set active themes in library") - (theme-active? [_ id] "predicate if token theme is active") - (activate-theme [_ id] "adds theme from the active-themes") - (deactivate-theme [_ id] "removes theme from the active-themes") - (toggle-theme-active [_ id] "toggles theme in the active-themes") - (get-hidden-theme [_] "get the hidden temporary theme")) + (get-theme-groups [_] "get a sequence of group names by order")) (def schema:token-themes [:and @@ -930,18 +926,10 @@ (update-token [_ set-id token-id f] "update a token in a set") (delete-token [_ set-id token-id] "delete a token from a set") (toggle-set-in-theme [_ theme-id set-name] "toggle a set used / not used in a theme") - (get-active-themes-set-names [_] "set of set names that are active in the the active themes") - (token-set-active? [_ set-name] "if a set is active in any of the active themes") - (sets-at-path-all-active? [_ group-path] "compute active state for child sets at `group-path`. -Will return a value that matches this schema: -`:none` None of the nested sets are active -`:all` All of the nested sets are active -`:partial` Mixed active state of nested sets") - (get-tokens-in-active-sets [_] "set of set names that are active in the the active themes") - (get-tokens-in-active-sets-force [_ force-set-id] "same as above but forcing a set to be active, even if it's not in the active themes") (get-all-tokens [_] "all tokens in the lib, as a sequence") (get-all-tokens-map [_] "all tokens in the lib, as a map name -> token") - (get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name")) + (get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name") + (get-legacy-active-themes [_] "only for legacy compatibility; return active themes inside library")) (declare parse-multi-set-dtcg-json) (declare read-multi-set-dtcg) @@ -1116,8 +1104,8 @@ Will return a value that matches this schema: (->> (tree-seq d/ordered-map? vals sets) (filter (partial instance? TokenSet)))) - (get-sets-at-path [_ path] - (some->> (map add-set-path-group-prefix path) + (get-sets-at-path [_ path-vec] + (some->> (map add-set-path-group-prefix path-vec) (get-in sets) (tree-seq d/ordered-map? vals) (filter (partial instance? TokenSet)))) @@ -1177,7 +1165,7 @@ Will return a value that matches this schema: (d/dissoc-in [group name]))) (if same-path? active-themes - (disj active-themes (join-theme-path group name))))))) + (disj active-themes (join-theme-path group name false))))))) this)) (delete-theme [this id] @@ -1186,12 +1174,15 @@ Will return a value that matches this schema: (if theme (TokensLib. sets (d/dissoc-in themes [group name]) - (disj active-themes (join-theme-path group name))) + (disj active-themes (join-theme-path group name false))) this))) (get-theme-tree [_] themes) + (get-theme-tree-no-hidden [_] + (d/dissoc-in themes [hidden-theme-group hidden-theme-name])) + (get-theme-groups [_] (into [] (comp (map key) @@ -1202,6 +1193,14 @@ Will return a value that matches this schema: (->> (tree-seq d/ordered-map? vals themes) (filter (partial instance? TokenTheme)))) + (get-themes-no-hidden [this] + (->> (get-themes this) + (remove #(hidden-theme? %)))) + + (get-themes-in-group [_ group] + (->> (get themes group) + (map (comp get-id val)))) + (theme-count [this] (count (get-themes this))) @@ -1212,60 +1211,12 @@ Will return a value that matches this schema: (get-theme-by-name [_ group name] (dm/get-in themes [group name])) - (set-active-themes [_ active-themes] - (TokensLib. sets - themes - active-themes)) - - (activate-theme [this id] - (if-let [theme (get-theme this id)] - (let [group (:group theme) - group-themes (->> (get themes group) - (map (comp get-theme-path val)) - (into #{})) - active-themes' (-> (set/difference active-themes group-themes) - (conj (get-theme-path theme)))] - (TokensLib. sets - themes - active-themes')) - this)) - - (deactivate-theme [this id] - (if-let [theme (get-theme this id)] - (TokensLib. sets - themes - (disj active-themes (get-theme-path theme))) - this)) - - (theme-active? [this id] - (when-let [theme (get-theme this id)] - (contains? active-themes (get-theme-path theme)))) - - (toggle-theme-active [this id] - (if (theme-active? this id) - (deactivate-theme this id) - (activate-theme this id))) - - (get-active-theme-paths [_] - active-themes) - - (get-active-themes [this] - (into - (list) - (comp - (filter (partial instance? TokenTheme)) - (filter #(theme-active? this (get-id %)))) - (tree-seq d/ordered-map? vals themes))) - - (get-hidden-theme [this] - (get-theme this hidden-theme-id)) - ITokensLib (empty-lib? [this] (and (empty? sets) (or (empty? themes) (and (= (theme-count this) 1) - (get-hidden-theme this))))) + (get-theme this hidden-theme-id))))) (set-path-exists? [_ set-path] (some? (get-in sets (set-full-path->set-prefixed-full-path set-path)))) @@ -1300,58 +1251,6 @@ Will return a value that matches this schema: active-themes) this)) - (get-active-themes-set-names [this] - (into #{} - (mapcat :sets) - (get-active-themes this))) - - (token-set-active? [this set-name] - (let [set-names (get-active-themes-set-names this)] - (contains? set-names set-name))) - - (sets-at-path-all-active? [this group-path] - (let [active-set-names (get-active-themes-set-names this) - prefixed-path-str (set-group-path->set-group-prefixed-path-str group-path)] - - (if (seq active-set-names) - (let [path-active-set-names (some->> (get-in sets (split-set-name prefixed-path-str)) - (tree-seq d/ordered-map? vals) - (filter (partial instance? TokenSet)) - (map get-name) - (into #{})) - difference (set/difference path-active-set-names active-set-names)] - (cond - (empty? difference) :all - (seq (set/intersection path-active-set-names active-set-names)) :partial - :else :none)) - :none))) - - (get-tokens-in-active-sets [this] - (let [theme-set-names (get-active-themes-set-names this) - all-set-names (get-set-names this) - active-set-names (filter theme-set-names all-set-names) - tokens (reduce (fn [tokens set-name] - (let [set (get-set-by-name this set-name)] - (merge tokens (get-tokens- set)))) - (d/ordered-map) - active-set-names)] - tokens)) - - (get-tokens-in-active-sets-force [this force-set-id] - (let [theme-set-names (get-active-themes-set-names this) - all-set-names (get-set-names this) - force-set (get-set this force-set-id) - active-set-names (cond-> (filter theme-set-names all-set-names) - (some? force-set) - (conj (get-name force-set))) - - tokens (reduce (fn [tokens set-name] - (let [set (get-set-by-name this set-name)] - (merge tokens (get-tokens- set)))) - (d/ordered-map) - active-set-names)] - tokens)) - (get-all-tokens [this] (mapcat #(vals (get-tokens- %)) (get-sets this))) @@ -1368,6 +1267,9 @@ Will return a value that matches this schema: (get-set set-id) (get-tokens-))) + (get-legacy-active-themes [_] + active-themes) + IValidation (valid? [this] (valid-tokens-lib-map? (datafy this))) @@ -1424,6 +1326,12 @@ Will return a value that matches this schema: [o] (and (tokens-lib? o) (valid? o))) +(defn get-theme-by-path + "Get a theme by its path string (e.g. \"/Theme Name\" or \"group/Theme Name\")." + [tokens-lib path] + (let [[group name] (split-theme-path path)] + (get-theme-by-name tokens-lib group name))) + (defn- ensure-hidden-theme "A helper that is responsible to ensure that the hidden theme always exists on the themes data structure" @@ -1452,10 +1360,6 @@ Will return a value that matches this schema: (check-tokens-lib-map) (map->tokens-lib))) -(defn ensure-tokens-lib - [tokens-lib] - (or tokens-lib (make-tokens-lib))) - (def schema:tokens-lib (sm/type-schema {:type ::tokens-lib @@ -1924,11 +1828,22 @@ Will return a value that matches this schema: library (reduce add-theme library themes) + ;; Activate themes by directly manipulating the internal active-themes field + ;; (since activate-theme is being removed from the protocol) library (reduce (fn [library theme-path] (let [[group name] (split-theme-path theme-path) theme (get-theme-by-name library group name)] - (activate-theme library (get-id theme)))) + (if theme + (let [group-themes (->> (get themes group) + (map (comp get-theme-path val)) + (into #{})) + active-themes' (-> (set/difference (.-active-themes ^TokensLib library) group-themes) + (conj (get-theme-path theme)))] + (TokensLib. (.-sets ^TokensLib library) + (.-themes ^TokensLib library) + active-themes')) + library))) library active-theme-names)] @@ -2033,82 +1948,183 @@ Will return a value that matches this schema: (defn- dtcg-export-themes "Extract themes for a dtcg json export." - [tokens-lib] - (let [themes-xform - (comp - (filter #(and (instance? TokenTheme %) - (not (hidden-theme? %)))) - (map (fn [token-theme] - ;; NOTE: this probaly can be implemented as type method - (d/without-nils - {"id" (:external-id token-theme) - "name" (:name token-theme) - "group" (:group token-theme) - "description" (:description token-theme) - "isSource" (:is-source token-theme) - "selectedTokenSets" (reduce #(assoc %1 %2 "enabled") {} (:sets token-theme))})))) + ([tokens-lib] + ;; Backward-compatible version for print/serialization without tokens-status + (let [themes-xform + (comp + (filter #(and (instance? TokenTheme %) + (not (hidden-theme? %)))) + (map (fn [token-theme] + ;; NOTE: this probaly can be implemented as type method + (d/without-nils + {"id" (:external-id token-theme) + "name" (:name token-theme) + "group" (:group token-theme) + "description" (:description token-theme) + "isSource" (:is-source token-theme) + "selectedTokenSets" (reduce #(assoc %1 %2 "enabled") {} (:sets token-theme))})))) - themes - (->> (get-theme-tree tokens-lib) - (tree-seq d/ordered-map? vals) - (into [] themes-xform)) + themes + (->> (get-theme-tree tokens-lib) + (tree-seq d/ordered-map? vals) + (into [] themes-xform)) - ;; Active themes without exposing hidden penpot theme - active-themes - (-> (get-active-theme-paths tokens-lib) - (disj hidden-theme-path))] - [themes active-themes])) + ;; Active themes without exposing hidden penpot theme + active-themes + (-> (.-active-themes ^TokensLib tokens-lib) + (disj hidden-theme-path))] + [themes active-themes])) + ([tokens-lib active-theme-ids] + ;; Version with explicit active-theme-ids from TokensStatus + (let [themes-xform + (comp + (filter #(and (instance? TokenTheme %) + (not (hidden-theme? %)))) + (map (fn [token-theme] + (d/without-nils + {"id" (:external-id token-theme) + "name" (:name token-theme) + "group" (:group token-theme) + "description" (:description token-theme) + "isSource" (:is-source token-theme) + "selectedTokenSets" (reduce #(assoc %1 %2 "enabled") {} (:sets token-theme))})))) + + themes + (->> (get-theme-tree tokens-lib) + (tree-seq d/ordered-map? vals) + (into [] themes-xform)) + + ;; Convert theme ids to theme paths, excluding hidden theme + active-themes + (into #{} + (comp (map #(get-theme tokens-lib %)) + (filter some?) + (map get-theme-path) + (filter #(not= % hidden-theme-path))) + active-theme-ids)] + [themes active-themes]))) (defn export-dtcg-multi-file "Convert a TokensLib into a plain clojure map, suitable to be encoded as a multi json files each encoded in DTCG format." - [tokens-lib] - (let [[themes active-themes] - (dtcg-export-themes tokens-lib) + ([tokens-lib] + ;; Backward-compatible version for print/serialization without tokens-status + (let [[themes active-themes] + (dtcg-export-themes tokens-lib) - sets - (->> (get-sets tokens-lib) - (map (fn [token-set] - (let [name (get-name token-set) - tokens (get-tokens- token-set)] - [(str name ".json") (tokens-tree tokens :update-token-fn token->dtcg-token)]))) - (into {}))] + sets + (->> (get-sets tokens-lib) + (map (fn [token-set] + (let [name (get-name token-set) + tokens (get-tokens- token-set)] + [(str name ".json") (tokens-tree tokens :update-token-fn token->dtcg-token)]))) + (into {}))] - (-> sets - (assoc "$themes.json" themes) - (assoc "$metadata.json" - {"tokenSetOrder" (get-set-names tokens-lib) - "activeThemes" active-themes - "activeSets" (get-active-themes-set-names tokens-lib)})))) + (-> sets + (assoc "$themes.json" themes) + (assoc "$metadata.json" + {"tokenSetOrder" (get-set-names tokens-lib) + "activeThemes" active-themes + ;; For backward compat, use internal active-themes field + "activeSets" (into #{} + (comp (filter #(and (instance? TokenTheme %) + (contains? (.-active-themes ^TokensLib tokens-lib) + (get-theme-path %)))) + (mapcat :sets)) + (get-themes tokens-lib))})))) + ([tokens-lib tokens-status] + ;; Version with tokens-status for real exports + (let [active-theme-ids (ctos/get-active-theme-ids tokens-status) + [themes active-themes] + (dtcg-export-themes tokens-lib active-theme-ids) + + sets + (->> (get-sets tokens-lib) + (map (fn [token-set] + (let [name (get-name token-set) + tokens (get-tokens- token-set)] + [(str name ".json") (tokens-tree tokens :update-token-fn token->dtcg-token)]))) + (into {})) + + active-set-ids (ctos/get-active-set-ids tokens-status) + active-set-names (into #{} + (comp (filter #(contains? active-set-ids (get-id %))) + (map get-name)) + (get-sets tokens-lib))] + + (-> sets + (assoc "$themes.json" themes) + (assoc "$metadata.json" + {"tokenSetOrder" (get-set-names tokens-lib) + "activeThemes" active-themes + "activeSets" active-set-names}))))) (defn export-dtcg-json "Convert a TokensLib into a plain clojure map, suitable to be encoded as a multi sets json string in DTCG format." - [tokens-lib] - (let [[themes active-themes] - (dtcg-export-themes tokens-lib) + ([tokens-lib] + ;; Backward-compatible version for print/serialization without tokens-status + (let [[themes active-themes] + (dtcg-export-themes tokens-lib) - name-set-tuples - (->> (get-set-tree tokens-lib) - (tree-seq d/ordered-map? vals) - (filter (partial instance? TokenSet)) - (map (fn [set] - [(get-name set) - (tokens-tree (get-tokens- set) :update-token-fn token->dtcg-token)]))) + name-set-tuples + (->> (get-set-tree tokens-lib) + (tree-seq d/ordered-map? vals) + (filter (partial instance? TokenSet)) + (map (fn [set] + [(get-name set) + (tokens-tree (get-tokens- set) :update-token-fn token->dtcg-token)]))) - ordered-set-names - (mapv first name-set-tuples) + ordered-set-names + (mapv first name-set-tuples) - sets - (into {} name-set-tuples) + sets + (into {} name-set-tuples) - active-set-names - (get-active-themes-set-names tokens-lib)] + ;; For backward compat, use internal active-themes field + active-set-names (into #{} + (comp (filter #(and (instance? TokenTheme %) + (contains? (.-active-themes ^TokensLib tokens-lib) + (get-theme-path %)))) + (mapcat :sets)) + (get-themes tokens-lib))] - (when-not (empty-lib? tokens-lib) - (-> sets - (assoc "$themes" themes) - (assoc "$metadata" {"tokenSetOrder" ordered-set-names - "activeThemes" active-themes - "activeSets" active-set-names}))))) + (when-not (empty-lib? tokens-lib) + (-> sets + (assoc "$themes" themes) + (assoc "$metadata" {"tokenSetOrder" ordered-set-names + "activeThemes" active-themes + "activeSets" active-set-names}))))) + ([tokens-lib tokens-status] + ;; Version with tokens-status for real exports + (let [active-theme-ids (ctos/get-active-theme-ids tokens-status) + [themes active-themes] + (dtcg-export-themes tokens-lib active-theme-ids) + + name-set-tuples + (->> (get-set-tree tokens-lib) + (tree-seq d/ordered-map? vals) + (filter (partial instance? TokenSet)) + (map (fn [set] + [(get-name set) + (tokens-tree (get-tokens- set) :update-token-fn token->dtcg-token)]))) + + ordered-set-names + (mapv first name-set-tuples) + + sets + (into {} name-set-tuples) + + active-set-ids (ctos/get-active-set-ids tokens-status) + active-set-names (into #{} + (comp (filter #(contains? active-set-ids (get-id %))) + (map get-name)) + (get-sets tokens-lib))] + + (when-not (empty-lib? tokens-lib) + (-> sets + (assoc "$themes" themes) + (assoc "$metadata" {"tokenSetOrder" ordered-set-names + "activeThemes" active-themes + "activeSets" active-set-names})))))) (defn get-tokens-of-unknown-type "Search for all tokens in the decoded json file that have a type that is not currently diff --git a/common/test/common_tests/files/tokens_test.cljc b/common/test/common_tests/files/tokens_test.cljc index e196ba4ac1..93b87a12e1 100644 --- a/common/test/common_tests/files/tokens_test.cljc +++ b/common/test/common_tests/files/tokens_test.cljc @@ -7,8 +7,116 @@ (ns common-tests.files.tokens-test (:require [app.common.files.tokens :as cfo] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.tokens :as tht] + [app.common.types.file :as ctf] + [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] + [app.common.uuid :as uuid] [clojure.test :as t])) +;; Helper functions + +(t/deftest test-token-circular-reference? + (t/testing "returns nil when token has no references" + (let [tokens {"spacing" {:name "spacing" :value "16px"}}] + (t/is (nil? (cfo/token-circular-reference? tokens "spacing"))))) + + (t/testing "returns nil for a simple non-circular reference chain" + (let [tokens {"base" {:name "base" :value "10"} + "large" {:name "large" :value "{base}"}}] + (t/is (nil? (cfo/token-circular-reference? tokens "large"))))) + + (t/testing "detects direct self-reference" + (let [tokens {"self" {:name "self" :value "{self}"}}] + (t/is (= "self" (cfo/token-circular-reference? tokens "self"))))) + + (t/testing "detects indirect circular reference" + (let [tokens {"a" {:name "a" :value "{b}"} + "b" {:name "b" :value "{c}"} + "c" {:name "c" :value "{a}"}}] + (t/is (some? (cfo/token-circular-reference? tokens "a"))))) + + (t/testing "returns nil for diamond dependency (no cycle)" + (let [tokens {"base" {:name "base" :value "10"} + "left" {:name "left" :value "{base}"} + "right" {:name "right" :value "{base}"} + "combine" {:name "combine" :value "{left} {right}"}}] + (t/is (nil? (cfo/token-circular-reference? tokens "combine"))))) + + (t/testing "returns nil when token name does not exist in tokens map" + (let [tokens {"a" {:name "a" :value "10"}}] + (t/is (nil? (cfo/token-circular-reference? tokens "nonexistent"))))) + + (t/testing "detects circular reference in map-typed values (typography composite)" + (let [tokens {"family" {:name "family" :value "Arial"} + "size" {:name "size" :value "16px"} + "typog" {:name "typog" :value {:font-family {:reference "{family}"} + :font-size {:reference "{size}"}}} + "cycle" {:name "cycle" :value {:font-family {:reference "{typog}"}}}}] + ;; typog references family and size (no cycle) + (t/is (nil? (cfo/token-circular-reference? tokens "typog")))))) + +(t/deftest test-attributes-map + (t/testing "creates map from attributes to token name" + (t/is (= {:fill "primary" :stroke "primary"} + (cfo/attributes-map [:fill :stroke] {:name "primary"})))) + + (t/testing "returns empty map for empty attributes" + (t/is (= {} (cfo/attributes-map [] {:name "primary"})))) + + (t/testing "maps each attribute independently" + (t/is (= {:x "t" :y "t" :z "t"} + (cfo/attributes-map [:x :y :z] {:name "t"}))))) + +(t/deftest test-remove-attributes-for-token + (t/testing "removes entries matching both attribute set and token name" + (t/is (= {:stroke "other"} + (cfo/remove-attributes-for-token + [:fill :stroke] "primary" + {:fill "primary" :stroke "other"})))) + + (t/testing "keeps entries with matching attribute but different token name" + (t/is (= {:fill "secondary" :stroke "other"} + (cfo/remove-attributes-for-token + [:fill :stroke] "primary" + {:fill "secondary" :stroke "other"})))) + + (t/testing "keeps entries with matching token name but non-matching attribute" + (t/is (= {:opacity "primary"} + (cfo/remove-attributes-for-token + [:fill :stroke] "primary" + {:fill "primary" :opacity "primary"})))) + + (t/testing "returns empty map when all entries match" + (t/is (= {} + (cfo/remove-attributes-for-token + [:fill] "primary" + {:fill "primary"})))) + + (t/testing "returns empty map for empty applied-tokens" + (t/is (= {} + (cfo/remove-attributes-for-token [:fill] "primary" {}))))) + +(t/deftest test-color-token? + (t/testing "returns true for color type" + (t/is (true? (cfo/color-token? {:type :color})))) + + (t/testing "returns false for non-color types" + (t/is (false? (cfo/color-token? {:type :spacing}))) + (t/is (false? (cfo/color-token? {:type :border-radius}))) + (t/is (false? (cfo/color-token? {}))))) + +(t/deftest test-is-reference? + (t/testing "returns truthy when value contains opening brace" + (t/is (cfo/is-reference? {:value "{foo}"})) + (t/is (cfo/is-reference? {:value "calc({bar} + 1)"}))) + + (t/testing "returns falsy when value has no brace" + (t/is (not (cfo/is-reference? {:value "16px"}))) + (t/is (not (cfo/is-reference? {:value ""}))))) + (t/deftest test-parse-token-value (t/testing "parses double from a token value" (t/is (= {:value 100.1 :unit nil} (cfo/parse-token-value "100.1"))) @@ -92,3 +200,1280 @@ (t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}} {:applied-tokens {:x "a"}}] #{:y}))))) + +;; Tokens lib in file data + +(t/deftest test-ensure-tokens-lib + (t/testing "ensure-tokens-lib should add a tokens-lib and tokens-status to the file data if they are missing" + (let [file (thf/sample-file :file1) + file-data (ctf/file-data file) + file-data' (cfo/ensure-tokens-lib file-data)] + (t/is (contains? file-data' :tokens-lib)) + (t/is (ctob/tokens-lib? (:tokens-lib file-data'))) + (t/is (contains? file-data' :tokens-status)) + (t/is (ctos/tokens-status? (:tokens-status file-data'))))) + + (t/testing "ensure-tokens-lib should add a tokens-lib to the file data if it's missing" + (let [tokens-status (ctos/make-tokens-status) + file (-> (thf/sample-file :file1) + (assoc-in [:data :tokens-status] tokens-status)) + file-data (ctf/file-data file) + file-data' (cfo/ensure-tokens-lib file-data)] + (t/is (contains? file-data' :tokens-lib)) + (t/is (ctob/tokens-lib? (:tokens-lib file-data'))) + (t/is (= tokens-status (:tokens-status file-data'))))) + + (t/testing "ensure-tokens-lib should add a tokens-status to the file data if it's missing" + (let [tokens-lib (ctob/make-tokens-lib) + file (-> (thf/sample-file :file1) + (assoc-in [:data :tokens-lib] tokens-lib)) + file-data (ctf/file-data file) + file-data' (cfo/ensure-tokens-lib file-data)] + (t/is (= tokens-lib (:tokens-lib file-data'))) + (t/is (contains? file-data' :tokens-status)) + (t/is (ctos/tokens-status? (:tokens-status file-data'))))) + + (t/testing "ensure-tokens-lib should not add a tokens-lib if there is a tokens-source" + (let [source-id (uuid/next) + tokens-status (ctos/make-tokens-status) + file (-> (thf/sample-file :file1) + (assoc-in [:data :tokens-source] source-id) + (assoc-in [:data :tokens-status] tokens-status)) + file-data (ctf/file-data file) + file-data' (cfo/ensure-tokens-lib file-data)] + (t/is (not (contains? file-data' :tokens-lib))) + (t/is (= source-id (:tokens-source file-data'))) + (t/is (= tokens-status (:tokens-status file-data'))))) + + (t/testing "ensure-tokens-lib should not add a tokens-lib if there is a tokens-source, but should add a tokens-status if it's missing" + (let [source-id (uuid/next) + file (-> (thf/sample-file :file1) + (assoc-in [:data :tokens-source] source-id)) + file-data (ctf/file-data file) + file-data' (cfo/ensure-tokens-lib file-data)] + (t/is (not (contains? file-data' :tokens-lib))) + (t/is (= source-id (:tokens-source file-data'))) + (t/is (contains? file-data' :tokens-status)) + (t/is (ctos/tokens-status? (:tokens-status file-data')))))) + +(t/deftest test-get-tokens-lib + (t/testing "returns tokens-lib from file data" + (let [tokens-lib (ctob/make-tokens-lib) + file-data {:tokens-lib tokens-lib}] + (t/is (= tokens-lib (cfo/get-tokens-lib file-data))))) + + (t/testing "returns nil when no tokens-lib" + (t/is (nil? (cfo/get-tokens-lib {}))))) + +(t/deftest test-get-tokens-source + (t/testing "returns tokens-source from file data" + (let [source-id (thi/new-id! :tokens-source) + file (-> (thf/sample-file :file1) + (assoc-in [:data :tokens-source] source-id))] + (t/is (= source-id (cfo/get-tokens-source (:data file)))))) + + (t/testing "returns nil when no tokens-source" + (let [file (thf/sample-file :file1)] + (t/is (nil? (cfo/get-tokens-source (:data file))))))) + +(t/deftest test-get-effective-tokens-source + (t/testing "returns tokens-source from file data" + (let [source-id (thi/new-id! :tokens-source) + file (-> (thf/sample-file :file1) + (assoc-in [:data :tokens-source] source-id))] + (t/is (= source-id (cfo/get-effective-tokens-source (:data file)))))) + + (t/testing "returns file id when no tokens-source" + (let [file (thf/sample-file :file1)] + (t/is (= (:id file) (cfo/get-effective-tokens-source file)))))) + +(t/deftest test-set-tokens-source + (t/testing "sets tokens-source on file data" + (let [source-id (thi/new-id! :tokens-source) + file (thf/sample-file :file1) + file-data (:data file) + file-data' (cfo/set-tokens-source file-data source-id)] + (t/is (= source-id (:tokens-source file-data')))))) + +(t/deftest test-effective-tokens-source? + (t/testing "no tokens, no tokens-source, ask for file id -> true" + (let [file (thf/sample-file :file1) + file-data (:data file)] + (t/is (true? (cfo/effective-tokens-source? file-data (:id file-data)))))) + + (t/testing "empty tokens-lib, no tokens-source, ask for file id -> true" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file-data (:data file)] + (t/is (true? (cfo/effective-tokens-source? file-data (:id file-data)))))) + + (t/testing "tokens, no tokens-source, ask for file id -> true" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (:data file)] + (t/is (true? (cfo/effective-tokens-source? file-data (:id file-data)))))) + + (t/testing "no tokens, no tokens source, ask for other id -> false" + (let [file (thf/sample-file :file1) + file-data (:data file)] + (t/is (false? (cfo/effective-tokens-source? file-data (uuid/next)))))) + + (t/testing "empty tokens-lib, no tokens source, ask for other id -> false" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file-data (:data file)] + (t/is (false? (cfo/effective-tokens-source? file-data (uuid/next)))))) + + (t/testing "tokens, no tokens source, ask for other id -> false" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (:data file)] + (t/is (false? (cfo/effective-tokens-source? file-data (uuid/next)))))) + + ;; --- + + (t/testing "no tokens, tokens source = file id, ask for file id -> true" ;; This case should not occur + (let [file (thf/sample-file :file1) + file-data (cfo/set-tokens-source (:data file) (:id (:data file)))] + (t/is (true? (cfo/effective-tokens-source? file-data (:id file-data)))))) + + (t/testing "empty tokens-lib, tokens source = file id, ask for file id -> true" ;; This case should not occur + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file-data (cfo/set-tokens-source (:data file) (:id (:data file)))] + (t/is (true? (cfo/effective-tokens-source? file-data (:id file-data)))))) + + (t/testing "tokens, tokens source = file id, ask for file id -> true" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (cfo/set-tokens-source (:data file) (:id (:data file)))] + (t/is (true? (cfo/effective-tokens-source? file-data (:id file-data)))))) + + (t/testing "no tokens, tokens source = file id, ask for other id -> false" ;; This case should not occur + (let [file (thf/sample-file :file1) + file-data (cfo/set-tokens-source (:data file) (:id (:data file)))] + (t/is (false? (cfo/effective-tokens-source? file-data (uuid/next)))))) + + (t/testing "empty tokens-lib, tokens source = file id, ask for other id -> false" ;; This case should not occur + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file-data (cfo/set-tokens-source (:data file) (:id (:data file)))] + (t/is (false? (cfo/effective-tokens-source? file-data (uuid/next)))))) + + (t/testing "tokens, tokens source = file id, ask for other id -> false" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (cfo/set-tokens-source (:data file) (:id (:data file)))] + (t/is (false? (cfo/effective-tokens-source? file-data (uuid/next)))))) + + ;; --- + + (t/testing "no tokens, tokens source = other id, ask for file id -> false" + (let [source-id (thi/new-id! :tokens-source) + file (thf/sample-file :file1) + file-data (cfo/set-tokens-source (:data file) source-id)] + (t/is (false? (cfo/effective-tokens-source? file-data (:id file-data)))))) + + (t/testing "empty tokens-lib, tokens source = other id, ask for file id -> false" + (let [source-id (thi/new-id! :tokens-source) + file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file-data (cfo/set-tokens-source (:data file) source-id)] + (t/is (false? (cfo/effective-tokens-source? file-data (:id file-data)))))) + + (t/testing "tokens, tokens source = other id, ask for file id -> false" + (let [source-id (thi/new-id! :tokens-source) + file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (cfo/set-tokens-source (:data file) source-id)] + (t/is (false? (cfo/effective-tokens-source? file-data (:id file-data)))))) + + (t/testing "no tokens, tokens source = other id, ask for this same id -> true" + (let [source-id (thi/new-id! :tokens-source) + file (thf/sample-file :file1) + file-data (cfo/set-tokens-source (:data file) source-id)] + (t/is (true? (cfo/effective-tokens-source? file-data source-id))))) + + (t/testing "empty tokens-lib, tokens source = other id, ask for this same id -> true" + (let [source-id (thi/new-id! :tokens-source) + file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file-data (cfo/set-tokens-source (:data file) source-id)] + (t/is (true? (cfo/effective-tokens-source? file-data source-id))))) + + (t/testing "tokens, tokens source = other id, ask for this same id -> true" + (let [source-id (thi/new-id! :tokens-source) + file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (cfo/set-tokens-source (:data file) source-id)] + (t/is (true? (cfo/effective-tokens-source? file-data source-id))))) + + (t/testing "no tokens, tokens source = other id, ask for a different id -> false" + (let [source-id (thi/new-id! :tokens-source) + file (thf/sample-file :file1) + file-data (cfo/set-tokens-source (:data file) source-id)] + (t/is (false? (cfo/effective-tokens-source? file-data (uuid/next)))))) + + (t/testing "empty tokens-lib, tokens source = other id, ask for a different id -> false" + (let [source-id (thi/new-id! :tokens-source) + file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file-data (cfo/set-tokens-source (:data file) source-id)] + (t/is (false? (cfo/effective-tokens-source? file-data (uuid/next)))))) + + (t/testing "tokens, tokens source = other id, ask for a different id -> false" + (let [source-id (thi/new-id! :tokens-source) + file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (cfo/set-tokens-source (:data file) source-id)] + (t/is (false? (cfo/effective-tokens-source? file-data (uuid/next))))))) + +(t/deftest test-has-own-tokens? + (t/testing "no tokens-lib -> false" + (let [file (thf/sample-file :file1) + file-data (:data file)] + (t/is (false? (cfo/has-own-tokens? file-data))))) + + (t/testing "empty tokens-lib -> false" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file-data (:data file)] + (t/is (false? (cfo/has-own-tokens? file-data))))) + + (t/testing "tokens lib with data -> true" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (:data file)] + (t/is (true? (cfo/has-own-tokens? file-data)))))) + +(t/deftest test-tokens-provider? + (t/testing "returns false if it has no tokens" + (let [file (thf/sample-file :file1) + file-data (:data file)] + (t/is (false? (cfo/tokens-provider? file-data))))) + + (t/testing "returns false if it has an empty tokens-lib and no tokens-source" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file-data (:data file)] + (t/is (false? (cfo/tokens-provider? file-data))))) + + (t/testing "returns false if it has an empty tokens lib and tokens-source is its own id" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file-data (cfo/set-tokens-source (:data file) (:id file))] + (t/is (false? (cfo/tokens-provider? file-data))))) + + (t/testing "returns true if it has tokens and no tokens-source" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (:data file)] + (t/is (true? (cfo/tokens-provider? file-data))))) + + (t/testing "returns true if it has tokens and tokens-source is its own id" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (cfo/set-tokens-source (:data file) (:id file))] + (t/is (true? (cfo/tokens-provider? file-data))))) + + (t/testing "returns false if it has tokens and tokens-source is other id" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-token (thi/id :set-a) (ctob/make-token :id (thi/new-id! :tok1) :name "spacing" :type :spacing :value "8px"))))) + file-data (cfo/set-tokens-source (:data file) (uuid/next))] + (t/is (false? (cfo/tokens-provider? file-data)))))) + +(t/deftest test-editable-tokens? + (t/testing "returns true when file-data has no tokens-source" + (let [file (thf/sample-file :file1) + file-data (:data file)] + (t/is (true? (cfo/editable-tokens? file-data))))) + + (t/testing "returns true when tokens-source is the file's own id" + (let [file (thf/sample-file :file1) + file-data (cfo/set-tokens-source (:data file) (:id (:data file)))] + (t/is (true? (cfo/editable-tokens? file-data))))) + + (t/testing "returns false when tokens-source is a different uuid" + (let [source-id (thi/new-id! :tokens-source) + file (thf/sample-file :file1) + file-data (cfo/set-tokens-source (:data file) source-id)] + (t/is (false? (cfo/editable-tokens? file-data)))))) + +(t/deftest test-get-tokens-status + (t/testing "returns tokens-status from file data" + (let [tokens-status (ctos/make-tokens-status) + file-data {:tokens-status tokens-status}] + (t/is (= tokens-status (cfo/get-tokens-status file-data))))) + + (t/testing "returns nil when no tokens-status" + (t/is (nil? (cfo/get-tokens-status {}))))) + +(t/deftest test-update-tokens-lib + (t/testing "update when there is no tokens-lib has no effect" + (let [file (thf/sample-file :file1) + file' (cfo/update-tokens-lib file #(t/is false "This should not be called"))] + (t/is (= file file')))) + + (t/testing "update a tokens-lib applies the changes correctly" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib)) + file' (ctf/update-file-data file + #(cfo/update-tokens-lib + % + ctob/add-theme + (ctob/make-token-theme :id (thi/new-id! :theme1) + :name "theme 1"))) + + tokens-lib' (tht/get-tokens-lib file')] + (t/is (= 2 (ctob/theme-count tokens-lib'))) ;; Count the hidden theme + (t/is (ctob/token-theme? (ctob/get-theme tokens-lib' (thi/id :theme1))))))) + +(t/deftest test-update-tokens-status + (t/testing "update when there is no tokens-status has no effect" + (let [file (thf/sample-file :file1) + file' (cfo/update-tokens-status file #(t/is false "This should not be called"))] + (t/is (= file file')))) + + (t/testing "update a tokens-status applies the changes correctly" + (let [file (-> (thf/sample-file :file1) + (tht/add-tokens-lib) + (tht/update-tokens-lib + (fn [tokens-lib] + (-> tokens-lib + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme1) :name "theme")))))) + file' (ctf/update-file-data file + #(cfo/update-tokens-status + % + ctos/set-tokens-status + #{(thi/id :theme1)} + #{})) + tokens-status' (tht/get-tokens-status file')] + (t/is (= 1 (count (ctos/get-active-theme-ids tokens-status')))) + (t/is (ctos/theme-active? tokens-status' (thi/id :theme1))) + (t/is (= 0 (count (ctos/get-active-set-ids tokens-status'))))))) + +;; Tokens status with tokens lib + +;; This is a private function but it deserves specific tests for all use cases +(def calculate-active-sets #'app.common.files.tokens/calculate-active-sets) + +(t/deftest test-calculate-active-sets + (t/testing "returns union of sets from all active themes" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) :name "set-b")) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-c) :name "set-c")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a" "set-b"})) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2) + :name "theme-2" + :group "" + :sets #{"set-c"})))] + (t/is (= #{(thi/id :set-a) (thi/id :set-b) (thi/id :set-c)} + (calculate-active-sets #{(thi/id :theme-1) (thi/id :theme-2)} tokens-lib))))) + + (t/testing "deduplicates sets shared by multiple active themes" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) :name "set-b")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a" "set-b"})) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2) + :name "theme-2" + :group "" + :sets #{"set-b"})))] + (t/is (= #{(thi/id :set-a) (thi/id :set-b)} + (calculate-active-sets #{(thi/id :theme-1) (thi/id :theme-2)} tokens-lib))))) + + (t/testing "returns empty set when active-theme-ids is empty" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a"})))] + (t/is (= #{} + (calculate-active-sets #{} tokens-lib))))) + + (t/testing "ignores active themes that do not exist in the lib" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a"})))] + (t/is (= #{(thi/id :set-a)} + (calculate-active-sets #{(thi/id :theme-1) (thi/new-id! :nonexistent)} tokens-lib))))) + + (t/testing "ignores set names that do not exist in the lib" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a" "nonexistent-set"})))] + (t/is (contains? (calculate-active-sets #{(thi/id :theme-1)} tokens-lib) (thi/id :set-a))) + (t/is (= 2 (count (calculate-active-sets #{(thi/id :theme-1)} tokens-lib)))))) + + (t/testing "returns empty set when themes have no sets" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{})))] + (t/is (= #{} + (calculate-active-sets #{(thi/id :theme-1)} tokens-lib))))) + + (t/testing "returns empty set when there are no themes" + (let [tokens-lib (-> (ctob/make-tokens-lib))] + (t/is (= #{} + (calculate-active-sets #{} tokens-lib)))))) + +(t/deftest test-get-active-themes + (t/testing "returns only themes that are active in the status" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) :name "theme-1" :group "")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2) :name "theme-2" :group "")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-3) :name "theme-3" :group ""))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1) (thi/id :theme-3)} + :active-set-ids #{})] + (t/is (= 2 (count (cfo/get-active-themes tokens-status tokens-lib)))) + (let [active-ids (into #{} (map ctob/get-id) (cfo/get-active-themes tokens-status tokens-lib))] + (t/is (contains? active-ids (thi/id :theme-1))) + (t/is (contains? active-ids (thi/id :theme-3))) + (t/is (not (contains? active-ids (thi/id :theme-2))))))) + + (t/testing "returns empty sequence when no themes are active" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) :name "theme-1" :group ""))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{} + :active-set-ids #{})] + (t/is (empty? (cfo/get-active-themes tokens-status tokens-lib))))) + + (t/testing "returns empty sequence when tokens-lib has no themes" + (let [tokens-lib (ctob/make-tokens-lib) + tokens-status (ctos/make-tokens-status)] + (t/is (empty? (cfo/get-active-themes tokens-status tokens-lib))))) + + (t/testing "active theme ids in status that do not exist in lib are silently ignored" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) :name "theme-1" :group ""))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1) (thi/new-id! :nonexistent)} + :active-set-ids #{})] + (t/is (= 1 (count (cfo/get-active-themes tokens-status tokens-lib))))))) + +(t/deftest test-activate-theme + (t/testing "activating a theme sets it as active" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status) + tokens-status' (cfo/activate-theme tokens-status tokens-lib (thi/id :theme-1))] + (t/is (ctos/theme-active? tokens-status' (thi/id :theme-1))))) + + (t/testing "activating a theme deactivates other themes in the same group" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) :name "set-b")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "colors" + :sets #{"set-a"})) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2) + :name "theme-2" + :group "colors" + :sets #{"set-b"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)} + :active-set-ids #{(thi/id :set-a)}) + tokens-status' (cfo/activate-theme tokens-status tokens-lib (thi/id :theme-2))] + (t/is (ctos/theme-active? tokens-status' (thi/id :theme-2))) + (t/is (not (ctos/theme-active? tokens-status' (thi/id :theme-1)))))) + + (t/testing "themes in different groups can be active simultaneously" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) :name "set-b")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "colors" + :sets #{"set-a"})) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2) + :name "theme-2" + :group "spacing" + :sets #{"set-b"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)} + :active-set-ids #{(thi/id :set-a)}) + tokens-status' (cfo/activate-theme tokens-status tokens-lib (thi/id :theme-2))] + (t/is (ctos/theme-active? tokens-status' (thi/id :theme-1))) + (t/is (ctos/theme-active? tokens-status' (thi/id :theme-2))))) + + (t/testing "activating an already-active theme returns status unchanged" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)} + :active-set-ids #{(thi/id :set-a)}) + tokens-status' (cfo/activate-theme tokens-status tokens-lib (thi/id :theme-1))] + (t/is (identical? tokens-status tokens-status')))) + + (t/testing "activating a non-existent theme returns status unchanged" + (let [tokens-lib (ctob/make-tokens-lib) + tokens-status (ctos/make-tokens-status) + tokens-status' (cfo/activate-theme tokens-status tokens-lib (thi/new-id! :nonexistent))] + (t/is (identical? tokens-status tokens-status'))))) + +(t/deftest test-deactivate-theme + (t/testing "deactivating an active theme removes it from active themes" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)} + :active-set-ids #{(thi/id :set-a)}) + tokens-status' (cfo/deactivate-theme tokens-status tokens-lib (thi/id :theme-1))] + (t/is (not (ctos/theme-active? tokens-status' (thi/id :theme-1)))))) + + (t/testing "deactivating a theme removes sets not used by remaining active themes" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) :name "set-b")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a"})) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2) + :name "theme-2" + :group "" + :sets #{"set-b"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1) (thi/id :theme-2)} + :active-set-ids #{(thi/id :set-a) (thi/id :set-b)}) + tokens-status' (cfo/deactivate-theme tokens-status tokens-lib (thi/id :theme-1))] + (t/is (not (ctos/theme-active? tokens-status' (thi/id :theme-1)))) + (t/is (ctos/theme-active? tokens-status' (thi/id :theme-2))))) + + (t/testing "deactivating an already-inactive theme returns status unchanged" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group ""))) + tokens-status (ctos/make-tokens-status) + tokens-status' (cfo/deactivate-theme tokens-status tokens-lib (thi/id :theme-1))] + (t/is (identical? tokens-status tokens-status')))) + + (t/testing "deactivating a non-existent theme returns status unchanged" + (let [tokens-lib (ctob/make-tokens-lib) + tokens-status (ctos/make-tokens-status) + tokens-status' (cfo/deactivate-theme tokens-status tokens-lib (thi/new-id! :nonexistent))] + (t/is (identical? tokens-status tokens-status'))))) + +(t/deftest test-toggle-theme-active + (t/testing "activates an inactive theme" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status) + tokens-status' (cfo/toggle-theme-active tokens-status tokens-lib (thi/id :theme-1))] + (t/is (ctos/theme-active? tokens-status' (thi/id :theme-1))))) + + (t/testing "deactivates an active theme" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)} + :active-set-ids #{(thi/id :set-a)}) + tokens-status' (cfo/toggle-theme-active tokens-status tokens-lib (thi/id :theme-1))] + (t/is (not (ctos/theme-active? tokens-status' (thi/id :theme-1))))))) + +(t/deftest test-set-theme-active + (t/testing "activates a theme when active? is true" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status) + tokens-status' (cfo/set-theme-active tokens-status tokens-lib (thi/id :theme-1) true)] + (t/is (ctos/theme-active? tokens-status' (thi/id :theme-1))))) + + (t/testing "deactivates a theme when active? is false" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/id :theme-1)} + :active-set-ids #{(thi/id :set-a)}) + tokens-status' (cfo/set-theme-active tokens-status tokens-lib (thi/id :theme-1) false)] + (t/is (not (ctos/theme-active? tokens-status' (thi/id :theme-1))))))) + +(t/deftest test-get-active-sets + (t/testing "returns active sets resolved from status and lib" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b"))) + tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id}) + active-sets (cfo/get-active-sets tokens-status tokens-lib)] + (t/is (= 1 (count active-sets))) + (t/is (= set-a-id (ctob/get-id (first active-sets)))))) + + (t/testing "returns empty set when no sets are active" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))) + tokens-status (ctos/make-tokens-status) + active-sets (cfo/get-active-sets tokens-status tokens-lib)] + (t/is (empty? active-sets))))) + +(t/deftest test-set-set-active + (t/testing "activates a set and clears all themes" + (let [set-a-id (thi/new-id! :set-a) + theme-1-id (thi/new-id! :theme-1) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{}) + tokens-status' (cfo/set-set-active tokens-status tokens-lib set-a-id true)] + (t/is (ctos/set-active? tokens-status' set-a-id)) + (t/is (= #{} (ctos/get-active-theme-ids tokens-status'))))) + + (t/testing "deactivates a set and clears all themes" + (let [set-a-id (thi/new-id! :set-a) + theme-1-id (thi/new-id! :theme-1) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{set-a-id}) + tokens-status' (cfo/set-set-active tokens-status tokens-lib set-a-id false)] + (t/is (not (ctos/set-active? tokens-status' set-a-id))) + (t/is (= #{} (ctos/get-active-theme-ids tokens-status'))))) + + (t/testing "returns same status when set is already in target state" + (let [set-a-id (thi/new-id! :set-a) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a"))) + tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id}) + tokens-status' (cfo/set-set-active tokens-status tokens-lib set-a-id true)] + (t/is (identical? tokens-status tokens-status')))) + + (t/testing "returns same status for non-existent set" + (let [tokens-lib (ctob/make-tokens-lib) + tokens-status (ctos/make-tokens-status) + tokens-status' (cfo/set-set-active tokens-status tokens-lib (thi/new-id! :nonexistent) true)] + (t/is (identical? tokens-status tokens-status'))))) + +(t/deftest test-activate-set + (t/testing "delegates to set-set-active with true" + (let [set-a-id (thi/new-id! :set-a) + theme-1-id (thi/new-id! :theme-1) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status) + tokens-status' (cfo/activate-set tokens-status tokens-lib set-a-id)] + (t/is (ctos/set-active? tokens-status' set-a-id)) + (t/is (= #{} (ctos/get-active-theme-ids tokens-status')))))) + +(t/deftest test-deactivate-set + (t/testing "delegates to set-set-active with false" + (let [set-a-id (thi/new-id! :set-a) + theme-1-id (thi/new-id! :theme-1) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{set-a-id}) + tokens-status' (cfo/deactivate-set tokens-status tokens-lib set-a-id)] + (t/is (not (ctos/set-active? tokens-status' set-a-id))) + (t/is (= #{} (ctos/get-active-theme-ids tokens-status')))))) + +(t/deftest test-toggle-set-active + (t/testing "activates an inactive set" + (let [set-a-id (thi/new-id! :set-a) + theme-1-id (thi/new-id! :theme-1) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{}) + tokens-status' (cfo/toggle-set-active tokens-status tokens-lib set-a-id)] + (t/is (ctos/set-active? tokens-status' set-a-id)) + (t/is (= #{} (ctos/get-active-theme-ids tokens-status'))))) + + (t/testing "deactivates an active set" + (let [set-a-id (thi/new-id! :set-a) + theme-1-id (thi/new-id! :theme-1) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{set-a-id}) + tokens-status' (cfo/toggle-set-active tokens-status tokens-lib set-a-id)] + (t/is (not (ctos/set-active? tokens-status' set-a-id))) + (t/is (= #{} (ctos/get-active-theme-ids tokens-status')))))) + +(t/deftest test-sets-at-path-all-active? + (t/testing "returns :all when all sets at path are active" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b"))) + tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id set-b-id})] + (t/is (= :all (cfo/sets-at-path-all-active? tokens-status tokens-lib ["group"]))))) + + (t/testing "returns :none when no sets at path are active" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b"))) + tokens-status (ctos/make-tokens-status :active-set-ids #{})] + (t/is (= :none (cfo/sets-at-path-all-active? tokens-status tokens-lib ["group"]))))) + + (t/testing "returns :partial when some sets at path are active" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b"))) + tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id})] + (t/is (= :partial (cfo/sets-at-path-all-active? tokens-status tokens-lib ["group"]))))) + + (t/testing "returns :none for empty path when no sets exist at root" + (let [tokens-lib (ctob/make-tokens-lib) + tokens-status (ctos/make-tokens-status)] + (t/is (= :none (cfo/sets-at-path-all-active? tokens-status tokens-lib []))))) + + (t/testing "several combinations with longer paths" + (let [baz-id (thi/new-id! :baz) + bam-id (thi/new-id! :bam) + none-id (thi/new-id! :none) + partial-id (thi/new-id! :partial) + all-id (thi/new-id! :all) + invalid-id (thi/new-id! :invalid) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id baz-id :name "foo/bar/baz")) + (ctob/add-set (ctob/make-token-set :id bam-id :name "foo/bar/bam")) + (ctob/add-theme (ctob/make-token-theme :id none-id + :name "none")) + (ctob/add-theme (ctob/make-token-theme :id partial-id + :name "partial" + :sets #{"foo/bar/baz"})) + (ctob/add-theme (ctob/make-token-theme :id all-id + :name "all" + :sets #{"foo/bar/baz" + "foo/bar/bam"})) + (ctob/add-theme (ctob/make-token-theme :id invalid-id + :name "invalid" + :sets #{"foo/missing"}))) + + expected-none (-> (ctos/make-tokens-status {:active-theme-ids #{none-id} + :active-set-ids #{}}) + (cfo/sets-at-path-all-active? tokens-lib ["foo"])) + expected-all (-> (ctos/make-tokens-status {:active-theme-ids #{all-id} + :active-set-ids #{baz-id bam-id}}) + (cfo/sets-at-path-all-active? tokens-lib ["foo"])) + expected-partial (-> (ctos/make-tokens-status {:active-theme-ids #{partial-id} + :active-set-ids #{baz-id}}) + (cfo/sets-at-path-all-active? tokens-lib ["foo"])) + expected-invalid-none (-> (ctos/make-tokens-status {:active-theme-ids #{invalid-id} + :active-set-ids #{}}) + (cfo/sets-at-path-all-active? tokens-lib ["foo"]))] + (t/is (= :none expected-none)) + (t/is (= :all expected-all)) + (t/is (= :partial expected-partial)) + (t/is (= :none expected-invalid-none))))) + +(t/deftest test-toggle-set-group-active + (t/testing "activates all sets in group when none are active, and deactivate all themes" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + set-c-id (thi/new-id! :set-c) + theme-1-id (thi/new-id! :theme-1) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b")) + (ctob/add-set (ctob/make-token-set :id set-c-id :name "group/set-b/child")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{}) + tokens-status' (cfo/toggle-set-group-active tokens-status tokens-lib ["group"])] + (t/is (ctos/set-active? tokens-status' set-a-id)) + (t/is (ctos/set-active? tokens-status' set-b-id)) + (t/is (ctos/set-active? tokens-status' set-c-id)) + (t/is (= #{} (ctos/get-active-theme-ids tokens-status'))))) + + (t/testing "deactivates all sets in group when all are active, and deactivate all themes" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + set-c-id (thi/new-id! :set-c) + theme-1-id (thi/new-id! :theme-1) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b")) + (ctob/add-set (ctob/make-token-set :id set-c-id :name "group/set-b/child")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"group/set-a" "group/set-b" "group/set-b/child"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{set-a-id set-b-id set-c-id}) + tokens-status' (cfo/toggle-set-group-active tokens-status tokens-lib ["group"])] + (t/is (not (ctos/set-active? tokens-status' set-a-id))) + (t/is (not (ctos/set-active? tokens-status' set-b-id))) + (t/is (not (ctos/set-active? tokens-status' set-c-id))) + (t/is (= #{} (ctos/get-active-theme-ids tokens-status'))))) + + (t/testing "deactivates all sets in group when only some are active, and deactivate all themes" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + set-c-id (thi/new-id! :set-c) + theme-1-id (thi/new-id! :theme-1) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b")) + (ctob/add-set (ctob/make-token-set :id set-c-id :name "group/set-b/child")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{set-a-id}) + tokens-status' (cfo/toggle-set-group-active tokens-status tokens-lib ["group"])] + (t/is (not (ctos/set-active? tokens-status' set-a-id))) + (t/is (not (ctos/set-active? tokens-status' set-b-id))) + (t/is (not (ctos/set-active? tokens-status' set-c-id))) + (t/is (= #{} (ctos/get-active-theme-ids tokens-status')))))) + +(t/deftest test-get-tokens-in-active-sets + (t/testing "returns merged tokens from active sets in set order" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + tok-a (ctob/make-token :name "spacing" :type :spacing :value "8px") + tok-b (ctob/make-token :name "radius" :type :border-radius :value "4px") + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b")) + (ctob/add-token set-a-id tok-a) + (ctob/add-token set-b-id tok-b)) + tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id set-b-id}) + tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)] + (t/is (= 2 (count tokens))) + (t/is (contains? tokens "spacing")) + (t/is (contains? tokens "radius")))) + + (t/testing "later active sets override earlier sets with same token name" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + tok-a (ctob/make-token :name "spacing" :type :spacing :value "8px") + tok-b (ctob/make-token :name "spacing" :type :spacing :value "16px") + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b")) + (ctob/add-token set-a-id tok-a) + (ctob/add-token set-b-id tok-b)) + tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id set-b-id}) + tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)] + (t/is (= 1 (count tokens))) + (t/is (= "16px" (:value (get tokens "spacing")))))) + + (t/testing "tokens in inactive sets are not included" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + tok-a (ctob/make-token :name "spacing" :type :spacing :value "8px") + tok-b (ctob/make-token :name "spacing" :type :spacing :value "16px") + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b")) + (ctob/add-token set-a-id tok-a) + (ctob/add-token set-b-id tok-b)) + tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id}) + tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)] + (t/is (= 1 (count tokens))) + (t/is (= "8px" (:value (get tokens "spacing")))))) + + (t/testing "returns empty map when no sets are active" + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) :name "set-a"))) + tokens-status (ctos/make-tokens-status) + tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)] + (t/is (empty? tokens)))) + + (t/testing "returns empty map when empty lib" + (let [tokens-lib (ctob/make-tokens-lib) + tokens-status (ctos/make-tokens-status) + tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)] + (t/is (empty? tokens))))) + +(t/deftest test-get-tokens-in-active-sets-force + (t/testing "includes force-set-id even when not in active sets" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + tok-a (ctob/make-token :name "spacing" :type :spacing :value "8px") + tok-b (ctob/make-token :name "radius" :type :border-radius :value "4px") + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b")) + (ctob/add-token set-a-id tok-a) + (ctob/add-token set-b-id tok-b)) + tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id}) + tokens (cfo/get-tokens-in-active-sets-force tokens-status tokens-lib set-b-id)] + (t/is (= 2 (count tokens))) + (t/is (contains? tokens "spacing")) + (t/is (contains? tokens "radius")))) + + (t/testing "active set tokens override force-set tokens with same name" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + tok-a (ctob/make-token :name "spacing" :type :spacing :value "8px") + tok-b (ctob/make-token :name "spacing" :type :spacing :value "32px") + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b")) + (ctob/add-token set-a-id tok-a) + (ctob/add-token set-b-id tok-b)) + tokens-status (ctos/make-tokens-status :active-set-ids #{set-a-id}) + tokens (cfo/get-tokens-in-active-sets-force tokens-status tokens-lib set-b-id)] + (t/is (= 1 (count tokens))) + (t/is (= "8px" (:value (get tokens "spacing"))))))) + +(t/deftest test-sync-tokens-status-with-lib + (t/testing "removes theme ids that no longer exist in the lib" + (let [theme-1-id (thi/new-id! :theme-1) + set-a-id (thi/new-id! :set-a) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id (thi/new-id! :removed-theme)} + :active-set-ids #{set-a-id}) + tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)] + (t/is (= #{theme-1-id} (ctos/get-active-theme-ids tokens-status'))) + (t/is (ctos/theme-active? tokens-status' theme-1-id)))) + + (t/testing "removes set ids that no longer exist in the lib (active themes)" + (let [theme-1-id (thi/new-id! :theme-1) + set-a-id (thi/new-id! :set-a) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{set-a-id (thi/new-id! :removed-set)}) + tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)] + (t/is (= #{set-a-id} (ctos/get-active-set-ids tokens-status'))))) + + (t/testing "removes set ids that no longer exist in the lib (no active themes)" + (let [theme-1-id (thi/new-id! :theme-1) + set-a-id (thi/new-id! :set-a) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{} + :active-set-ids #{set-a-id (thi/new-id! :removed-set)}) + tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)] + (t/is (= #{set-a-id} (ctos/get-active-set-ids tokens-status'))))) + + (t/testing "updates active sets when theme changes" + (let [theme-1-id (thi/new-id! :theme-1) + set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "set-b")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-b"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{set-a-id (thi/new-id! :removed-set)}) + tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)] + (t/is (= #{set-b-id} (ctos/get-active-set-ids tokens-status'))))) + + (t/testing "returns same status object when everything is valid" + (let [theme-1-id (thi/new-id! :theme-1) + set-a-id (thi/new-id! :set-a) + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id set-a-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-1-id + :name "theme-1" + :group "" + :sets #{"set-a"}))) + tokens-status (ctos/make-tokens-status :active-theme-ids #{theme-1-id} + :active-set-ids #{set-a-id}) + tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)] + (t/is (identical? tokens-status tokens-status')))) + + (t/testing "returns same status object when both theme and set ids are empty" + (let [tokens-lib (ctob/make-tokens-lib) + tokens-status (ctos/make-tokens-status) + tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)] + (t/is (identical? tokens-status tokens-status')))) + + (t/testing "removes all themes and sets when lib is empty" + (let [tokens-lib (ctob/make-tokens-lib) + tokens-status (ctos/make-tokens-status :active-theme-ids #{(thi/new-id! :old-theme)} + :active-set-ids #{(thi/new-id! :old-set)}) + tokens-status' (cfo/sync-tokens-status-with-lib tokens-status tokens-lib)] + (t/is (= #{} (ctos/get-active-theme-ids tokens-status'))) + (t/is (= #{} (ctos/get-active-set-ids tokens-status')))))) + +;; Mismatched tokens-status and tokens-lib +;; +;; The tokens-status may contain IDs for themes and sets that do not exist in +;; the tokens-lib (e.g. when the status comes from the tokens lib of a different source). +;; All functions must handle this gracefully: return a reasonable result, never +;; throw an exception. + +(t/deftest test-mismatched-status-lib + (let [real-theme-1 (thi/new-id! :real-theme-1) + real-theme-2 (thi/new-id! :real-theme-2) + real-set-a (thi/new-id! :real-set-a) + real-set-b (thi/new-id! :real-set-b) + real-set-c (thi/new-id! :real-set-c) + invalid-theme-id (thi/new-id! :invalid-theme) + invalid-set-id (thi/new-id! :invalid-set) + + tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id real-set-a :name "real-set-a")) + (ctob/add-set (ctob/make-token-set :id real-set-b :name "real-set-b")) + (ctob/add-set (ctob/make-token-set :id real-set-c :name "real-set-c")) + (ctob/add-token real-set-a (ctob/make-token :name "spacing" :type :spacing :value "8px")) + (ctob/add-token real-set-b (ctob/make-token :name "radius" :type :border-radius :value "4px")) + (ctob/add-theme (ctob/make-token-theme :id real-theme-1 + :name "theme-1" + :group "colors" + :sets #{"real-set-a"})) + (ctob/add-theme (ctob/make-token-theme :id real-theme-2 + :name "theme-2" + :group "colors" + :sets #{"real-set-b" "real-set-c"}))) + + ;; Status has one valid and one invalid theme, one valid and one invalid set + tokens-status (ctos/make-tokens-status :active-theme-ids #{real-theme-1 invalid-theme-id} + :active-set-ids #{real-set-a invalid-set-id})] + + (t/testing "activate-theme with non-existent theme id returns status unchanged" + (let [status' (cfo/activate-theme tokens-status tokens-lib invalid-theme-id)] + (t/is (identical? tokens-status status')))) + + (t/testing "activate-theme with valid theme works despite invalid ids in status" + (let [status' (cfo/activate-theme tokens-status tokens-lib real-theme-2)] + (t/is (ctos/theme-active? status' real-theme-2)) + (t/is (not (ctos/theme-active? status' real-theme-1))) + (t/is (ctos/theme-active? status' invalid-theme-id)) + (t/is (contains? (ctos/get-active-set-ids status') real-set-b)) + (t/is (contains? (ctos/get-active-set-ids status') real-set-c)))) + + (t/testing "deactivate-theme with non-existent theme id recalculates sets" + (let [status' (cfo/deactivate-theme tokens-status tokens-lib invalid-theme-id)] + (t/is (not (ctos/theme-active? status' invalid-theme-id))) + (t/is (ctos/theme-active? status' real-theme-1)) + (t/is (contains? (ctos/get-active-set-ids status') real-set-a)))) + + (t/testing "toggle-theme-active with non-existent theme id deactivates it" + (let [status' (cfo/toggle-theme-active tokens-status tokens-lib invalid-theme-id)] + (t/is (not (ctos/theme-active? status' invalid-theme-id))) + (t/is (ctos/theme-active? status' real-theme-1)))) + + (t/testing "toggle-theme-active with valid inactive theme activates it" + (let [status' (cfo/toggle-theme-active tokens-status tokens-lib real-theme-2)] + (t/is (ctos/theme-active? status' real-theme-2)) + (t/is (contains? (ctos/get-active-set-ids status') real-set-b)) + (t/is (contains? (ctos/get-active-set-ids status') real-set-c)))) + + (t/testing "set-theme-active false with non-existent theme recalculates sets" + (let [status' (cfo/set-theme-active tokens-status tokens-lib invalid-theme-id false)] + (t/is (not (ctos/theme-active? status' invalid-theme-id))) + (t/is (contains? (ctos/get-active-set-ids status') real-set-a)))) + + (t/testing "set-theme-active true with non-existent theme returns status unchanged" + (let [status' (cfo/set-theme-active tokens-status tokens-lib invalid-theme-id true)] + (t/is (identical? tokens-status status')))) + + (t/testing "get-active-themes filters out non-existent theme ids" + (let [active-themes (cfo/get-active-themes tokens-status tokens-lib)] + (t/is (= 1 (count active-themes))) + (t/is (= real-theme-1 (ctob/get-id (first active-themes)))))) + + (t/testing "get-active-sets filters out non-existent set ids" + (let [active-sets (cfo/get-active-sets tokens-status tokens-lib)] + (t/is (= 1 (count active-sets))) + (t/is (= real-set-a (ctob/get-id (first active-sets)))))) + + (t/testing "set-set-active with non-existent set id returns status unchanged" + (t/is (identical? tokens-status (cfo/set-set-active tokens-status tokens-lib invalid-set-id true))) + (t/is (identical? tokens-status (cfo/set-set-active tokens-status tokens-lib invalid-set-id false)))) + + (t/testing "activate-set with non-existent set id returns status unchanged" + (t/is (identical? tokens-status (cfo/activate-set tokens-status tokens-lib invalid-set-id)))) + + (t/testing "deactivate-set with non-existent set id returns status unchanged" + (t/is (identical? tokens-status (cfo/deactivate-set tokens-status tokens-lib invalid-set-id)))) + + (t/testing "toggle-set-active with non-existent set id returns status unchanged" + (t/is (identical? tokens-status (cfo/toggle-set-active tokens-status tokens-lib invalid-set-id)))) + + (t/testing "sets-at-path-all-active? with invalid set ids in status don't cause errors" + ;; real-set-a is at [] and is active (plus invalid-set-id); real-set-b, real-set-c at [] inactive + (t/is (= :partial (cfo/sets-at-path-all-active? tokens-status tokens-lib []))) + ;; All sets at [] active (with invalid id still in status) + (let [all-active-status (ctos/make-tokens-status :active-theme-ids #{} + :active-set-ids #{real-set-a real-set-b real-set-c invalid-set-id})] + (t/is (= :all (cfo/sets-at-path-all-active? all-active-status tokens-lib []))))) + + (t/testing "toggle-set-group-active with invalid set ids in status don't cause errors" + ;; Status has real-set-a + invalid-set-id active -> :partial at [] + ;; Toggling :partial deactivates all at [], invalid-set-id remains + (let [status' (cfo/toggle-set-group-active tokens-status tokens-lib [])] + (t/is (not (ctos/set-active? status' real-set-a))) + (t/is (not (ctos/set-active? status' real-set-b))) + (t/is (not (ctos/set-active? status' real-set-c))) + (t/is (ctos/set-active? status' invalid-set-id))) + ;; All sets at [] active + invalid-set-id -> :all at [] + ;; Toggling :all deactivates all at [], invalid-set-id remains + (let [all-active (ctos/make-tokens-status :active-theme-ids #{} + :active-set-ids #{real-set-a real-set-b real-set-c invalid-set-id}) + status' (cfo/toggle-set-group-active all-active tokens-lib [])] + (t/is (not (ctos/set-active? status' real-set-a))) + (t/is (not (ctos/set-active? status' real-set-b))) + (t/is (not (ctos/set-active? status' real-set-c))) + (t/is (ctos/set-active? status' invalid-set-id)))) + + (t/testing "get-tokens-in-active-sets with invalid set ids returns tokens from valid sets only" + (let [tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)] + (t/is (= 1 (count tokens))) + (t/is (contains? tokens "spacing")) + (t/is (not (contains? tokens "radius"))))) + + (t/testing "get-tokens-in-active-sets-force with invalid set ids and valid force set" + (let [tokens (cfo/get-tokens-in-active-sets-force tokens-status tokens-lib real-set-b)] + (t/is (= 2 (count tokens))) + (t/is (contains? tokens "spacing")) + (t/is (contains? tokens "radius")))) + + (t/testing "get-tokens-in-active-sets-force with non-existent force set id" + (let [tokens (cfo/get-tokens-in-active-sets-force tokens-status tokens-lib invalid-set-id)] + (t/is (= 1 (count tokens))) + (t/is (contains? tokens "spacing")))) + + (t/testing "all-invalid status: every id is non-existent in lib" + (let [all-invalid (ctos/make-tokens-status :active-theme-ids #{invalid-theme-id} + :active-set-ids #{invalid-set-id})] + ;; get-active-themes returns empty + (t/is (empty? (cfo/get-active-themes all-invalid tokens-lib))) + ;; get-active-sets returns empty + (t/is (empty? (cfo/get-active-sets all-invalid tokens-lib))) + ;; get-tokens-in-active-sets returns empty + (t/is (empty? (cfo/get-tokens-in-active-sets all-invalid tokens-lib))) + ;; get-tokens-in-active-sets-force returns only force set tokens + (let [tokens (cfo/get-tokens-in-active-sets-force all-invalid tokens-lib real-set-a)] + (t/is (= 1 (count tokens))) + (t/is (contains? tokens "spacing"))) + ;; activate-theme for non-existent id is no-op + (t/is (identical? all-invalid (cfo/activate-theme all-invalid tokens-lib invalid-theme-id))) + ;; deactivate-theme removes non-existent id and recalculates (empty) sets + (let [status' (cfo/deactivate-theme all-invalid tokens-lib invalid-theme-id)] + (t/is (not (ctos/theme-active? status' invalid-theme-id))) + (t/is (= #{} (ctos/get-active-set-ids status')))) + ;; toggle-theme-active toggles non-existent id (deactivates) + (let [status' (cfo/toggle-theme-active all-invalid tokens-lib invalid-theme-id)] + (t/is (not (ctos/theme-active? status' invalid-theme-id)))) + ;; set-theme-active false removes non-existent id + (let [status' (cfo/set-theme-active all-invalid tokens-lib invalid-theme-id false)] + (t/is (not (ctos/theme-active? status' invalid-theme-id)))))))) diff --git a/common/test/common_tests/logic/move_shapes_test.cljc b/common/test/common_tests/logic/move_shapes_test.cljc index 846f9c98f3..98bf21f6f7 100644 --- a/common/test/common_tests/logic/move_shapes_test.cljc +++ b/common/test/common_tests/logic/move_shapes_test.cljc @@ -14,6 +14,7 @@ [app.common.test-helpers.shapes :as ths] [app.common.test-helpers.tokens :as tht] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [clojure.test :as t])) @@ -141,24 +142,27 @@ (t/deftest test-relocate-shape-out-of-layout-with-tokens (let [;; ==== Setup - file (-> (thf/sample-file :file1) - (tht/add-tokens-lib) - (tht/update-tokens-lib #(-> % - (ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set) - :name "test-token-set")) - (ctob/add-theme (ctob/make-token-theme :name "test-theme" - :sets #{"test-token-set"})) - (ctob/set-active-themes #{"/test-theme"}) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-sizing) - :name "token-sizing" - :type :sizing - :value 10)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-spacing) - :name "token-spacing" - :type :spacing - :value 30)))) + file (-> (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set) + :name "test-token-set")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :test-theme) + :name "test-theme" + :sets #{"test-token-set"})) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-sizing) + :name "token-sizing" + :type :sizing + :value 10)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-spacing) + :name "token-spacing" + :type :spacing + :value 30))) + :status-fn #(-> % + (ctos/set-tokens-status #{(thi/id :test-theme)} + #{(thi/id :test-token-set)}))) + ;; Sync tokens-status from lib's internal active-themes (tho/add-frame :frame-1 :layout :flex ;; TODO: those values come from main.data.workspace.shape_layout/default-layout-params :layout-flex-dir :row ;; it should be good to use it directly, but first it should be moved to common.logic diff --git a/common/test/common_tests/logic/token_apply_test.cljc b/common/test/common_tests/logic/token_apply_test.cljc index b4a2c92c1f..8ca70b93e0 100644 --- a/common/test/common_tests/logic/token_apply_test.cljc +++ b/common/test/common_tests/logic/token_apply_test.cljc @@ -18,75 +18,78 @@ [app.common.types.text :as txt] [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) (defn- setup-file [] - (-> (thf/sample-file :file1) - (tht/add-tokens-lib) - (tht/update-tokens-lib #(-> % - (ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set) - :name "test-token-set")) - (ctob/add-theme (ctob/make-token-theme :name "test-theme" - :sets #{"test-token-set"})) - (ctob/set-active-themes #{"/test-theme"}) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-radius) - :name "token-radius" - :type :border-radius - :value 10)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-rotation) - :name "token-rotation" - :type :rotation - :value 30)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-opacity) - :name "token-opacity" - :type :opacity - :value 0.7)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-stroke-width) - :name "token-stroke-width" - :type :stroke-width - :value 2)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-color) - :name "token-color" - :type :color - :value "#00ff00")) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-dimensions) - :name "token-dimensions" - :type :dimensions - :value 100)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-font-size) - :name "token-font-size" - :type :font-size - :value 24)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-letter-spacing) - :name "token-letter-spacing" - :type :letter-spacing - :value 2)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-font-family) - :name "token-font-family" - :type :font-family - :value ["Helvetica" "Arial" "sans-serif"])) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-sizing) - :name "token-sizing" - :type :sizing - :value 10)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :id (thi/new-id! :token-spacing) - :name "token-spacing" - :type :spacing - :value 30)))) + (-> (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set) + :name "test-token-set")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :test-theme) + :name "test-theme" + :sets #{"test-token-set"})) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-radius) + :name "token-radius" + :type :border-radius + :value 10)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-rotation) + :name "token-rotation" + :type :rotation + :value 30)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-opacity) + :name "token-opacity" + :type :opacity + :value 0.7)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-stroke-width) + :name "token-stroke-width" + :type :stroke-width + :value 2)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-color) + :name "token-color" + :type :color + :value "#00ff00")) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-dimensions) + :name "token-dimensions" + :type :dimensions + :value 100)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-font-size) + :name "token-font-size" + :type :font-size + :value 24)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-letter-spacing) + :name "token-letter-spacing" + :type :letter-spacing + :value 2)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-font-family) + :name "token-font-family" + :type :font-family + :value ["Helvetica" "Arial" "sans-serif"])) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-sizing) + :name "token-sizing" + :type :sizing + :value 10)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :id (thi/new-id! :token-spacing) + :name "token-spacing" + :type :spacing + :value 30))) + :status-fn #(-> % + (ctos/set-tokens-status #{(thi/id :test-theme)} + #{(thi/id :test-token-set)}))) (tho/add-frame :frame1 :layout :flex ;; TODO: those values come from main.data.workspace.shape_layout/default-layout-params :layout-flex-dir :row ;; it should be good to use it directly, but first it should be moved to common.logic diff --git a/common/test/common_tests/logic/token_test.cljc b/common/test/common_tests/logic/token_test.cljc index 16258b8c8f..4ff3d97a10 100644 --- a/common/test/common_tests/logic/token_test.cljc +++ b/common/test/common_tests/logic/token_test.cljc @@ -11,335 +11,182 @@ [app.common.test-helpers.files :as thf] [app.common.test-helpers.ids-map :as thi] [app.common.test-helpers.tokens :as tht] + [app.common.types.file :as ctf] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] - [clojure.datafy :refer [datafy]] [clojure.test :as t])) (t/use-fixtures :each thi/test-fixture) -(defn- setup-file [lib-fn] - (-> (thf/sample-file :file1) - (tht/add-tokens-lib) - (tht/update-tokens-lib lib-fn))) +;; Tokens Source -(t/deftest generate-toggle-token-set-test - (t/testing "toggling an active set will switch to hidden theme without user sets" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/bar")) - (ctob/add-theme (ctob/make-token-theme :name "theme" - :sets #{"foo/bar"})) - (ctob/set-active-themes #{"/theme"}))) - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (clt/generate-toggle-token-set (tht/get-tokens-lib file) "foo/bar")) - - redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) - undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - (t/is (= #{ctob/hidden-theme-path} (ctob/get-active-theme-paths redo-lib))) - (t/is (= #{} (:sets (ctob/get-hidden-theme redo-lib)))) - - ;; Undo - (t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib))))) - - (t/testing "toggling an inactive set will switch to hidden theme without user sets" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/bar")) - (ctob/add-theme (ctob/make-token-theme :name "theme" - :sets #{"foo/bar"})) - (ctob/set-active-themes #{"/theme"}))) - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (clt/generate-toggle-token-set (tht/get-tokens-lib file) "foo/bar")) - - redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) - undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - (t/is (= #{ctob/hidden-theme-path} (ctob/get-active-theme-paths redo-lib))) - (t/is (= #{} (:sets (ctob/get-hidden-theme redo-lib)))) - - ;; Undo - (t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib))))) - - (t/testing "toggling an set with hidden theme already active will toggle set in hidden theme" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/bar")) - (ctob/add-theme (ctob/make-hidden-theme)) - (ctob/set-active-themes #{ctob/hidden-theme-path}))) - - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (clt/generate-toggle-token-set-group (tht/get-tokens-lib file) ["foo"])) - - redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) - undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - (t/is (= (ctob/get-active-theme-paths redo-lib) (ctob/get-active-theme-paths undo-lib))) - - (t/is (= #{"foo/bar"} (:sets (ctob/get-hidden-theme redo-lib))))))) - -(t/deftest set-token-theme-test - (t/testing "delete token theme" - (let [theme-id (uuid/next) - file (setup-file #(-> % - (ctob/add-theme (ctob/make-token-theme :id theme-id - :name "foo" - :group "main")))) - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (pcb/set-token-theme theme-id nil)) - - redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) - undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - ;; Redo - (t/is (nil? (ctob/get-theme redo-lib theme-id))) - ;; Undo - (t/is (some? (ctob/get-theme undo-lib theme-id))))) - - (t/testing "add token theme" - (let [theme-id (uuid/next) - theme (ctob/make-token-theme :id theme-id - :name "foo" - :group "main") - file (setup-file identity) - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (pcb/set-token-theme theme-id theme)) - redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) - undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - ;; Redo - (t/is (some? (ctob/get-theme redo-lib theme-id))) - ;; Undo - (t/is (nil? (ctob/get-theme undo-lib theme-id))))) - - (t/testing "update token theme" - (let [theme-id (uuid/next) - prev-theme-name "foo" - prev-theme (ctob/make-token-theme :id theme-id - :name prev-theme-name - :group "main") - file (setup-file #(ctob/add-theme % prev-theme)) - new-theme-name "foo1" - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (pcb/set-token-theme theme-id (ctob/rename prev-theme new-theme-name))) - redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) - redo-theme (ctob/get-theme redo-lib theme-id) - undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo) - undo-theme (ctob/get-theme undo-lib theme-id)] - ;; Redo - (t/is (= new-theme-name (ctob/get-name redo-theme))) - ;; Undo - (t/is (= prev-theme-name (ctob/get-name undo-theme))))) - - (t/testing "toggling token theme updates using changes history" - (let [theme-id (uuid/next) - theme (ctob/make-token-theme :id theme-id - :name "foo-theme" - :group "main") - set-name "bar-set" - token-set (ctob/make-token-set :name set-name) - file (setup-file #(-> % - (ctob/add-theme theme) - (ctob/add-set token-set))) - theme' (assoc theme :sets #{set-name}) - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (pcb/set-token-theme theme-id theme')) - changed-file (-> file - (thf/apply-changes changes) - (thf/apply-undo-changes changes) - (thf/apply-changes changes)) - changed-lib (tht/get-tokens-lib changed-file)] - (t/is (= #{set-name} - (-> changed-lib (ctob/get-theme theme-id) :sets)))))) - -(t/deftest set-token-test - (t/testing "delete token" - (let [set-name "foo" - set-id (uuid/next) +(t/deftest generate-set-tokens-source-test + (t/testing "setting a library as tokens source copies its tokens status" + (let [set-id (uuid/next) + theme-id (uuid/next) token-id (uuid/next) - file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :id set-id - :name set-name)) - (ctob/add-token set-id (ctob/make-token {:name "to.delete.color.red" - :id token-id - :value "red" - :type :color})))) + library-file (tht/sample-file-with-tokens + :file-id :library + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id + :name "core")) + (ctob/add-token set-id (ctob/make-token {:id token-id + :name "color.red" + :value "#ff0000" + :type :color})) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "light" + :group "main" + :sets #{"core"}))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{set-id})) + file (thf/sample-file :file1) changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (pcb/set-token set-id token-id nil)) - + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-set-tokens-source library-file)) redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - (t/is (nil? (ctob/get-token redo-lib set-id token-id))) - ;; Undo - (t/is (some? (ctob/get-token undo-lib set-id token-id))))) + library-tokens-status (tht/get-tokens-status library-file) + redo-tokens-source (tht/get-tokens-source redo) + redo-tokens-status (tht/get-tokens-status redo) + undo-tokens-source (tht/get-tokens-source undo) + undo-tokens-status (tht/get-tokens-status undo)] - (t/testing "add token" - (let [set-name "foo" - set-id (uuid/next) - token (ctob/make-token {:name "to.add.color.red" - :value "red" - :type :color}) - file (setup-file #(-> % (ctob/add-set (ctob/make-token-set :id set-id - :name set-name)))) + ;; Redo: source and status copied from library + (t/is (= (:id library-file) redo-tokens-source)) + (t/is (= (ctos/get-active-theme-ids library-tokens-status) + (ctos/get-active-theme-ids redo-tokens-status))) + (t/is (= (ctos/get-active-set-ids library-tokens-status) + (ctos/get-active-set-ids redo-tokens-status))) + + ;; Undo: source and status restored to the original empty state + (t/is (nil? undo-tokens-source)) + (t/is (= #{} (ctos/get-active-theme-ids undo-tokens-status))) + (t/is (= #{} (ctos/get-active-set-ids undo-tokens-status))))) + + (t/testing "any previous tokens status is replaced by the library's tokens status" + (let [set-id (uuid/next) + theme-id (uuid/next) + token-id (uuid/next) + other-set-id (uuid/next) + library-file (tht/sample-file-with-tokens + :file-id :library + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id + :name "core")) + (ctob/add-token set-id (ctob/make-token {:id token-id + :name "color.red" + :value "#ff0000" + :type :color})) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "light" + :group "main" + :sets #{"core"}))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{set-id})) + file (tht/sample-file-with-tokens + :file-id :file1 + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id other-set-id + :name "other"))) + :status-fn #(ctos/set-tokens-status % #{} #{other-set-id})) changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (pcb/set-token set-id (:id token) token)) - + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-set-tokens-source library-file)) redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - (t/is (= token (ctob/get-token redo-lib set-id (:id token)))) - ;; Undo - (t/is (nil? (ctob/get-token undo-lib set-id (:id token)))))) + file-tokens-status (tht/get-tokens-status file) + library-tokens-status (tht/get-tokens-status library-file) + redo-tokens-source (tht/get-tokens-source redo) + redo-tokens-status (tht/get-tokens-status redo) + undo-tokens-source (tht/get-tokens-source undo) + undo-tokens-status (tht/get-tokens-status undo)] - (t/testing "update token" - (let [set-name "foo" - set-id (uuid/next) - prev-token (ctob/make-token {:name "to.update.color.red" - :value "red" - :type :color}) - token (-> prev-token - (assoc :name "color.red.changed") - (assoc :value "blue")) - file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :id set-id - :name set-name)) - (ctob/add-token set-id prev-token))) + ;; Redo: source and status copied from library, replacing the previous status + (t/is (= (:id library-file) redo-tokens-source)) + (t/is (= (ctos/get-active-theme-ids library-tokens-status) + (ctos/get-active-theme-ids redo-tokens-status))) + (t/is (= (ctos/get-active-set-ids library-tokens-status) + (ctos/get-active-set-ids redo-tokens-status))) + + ;; Undo: source and status restored to the original state + (t/is (nil? undo-tokens-source)) + (t/is (= (ctos/get-active-theme-ids file-tokens-status) + (ctos/get-active-theme-ids undo-tokens-status))) + (t/is (= (ctos/get-active-set-ids file-tokens-status) + (ctos/get-active-set-ids undo-tokens-status))))) + + (t/testing "if the library is nil, the tokens source is cleared and the tokens status is reset to empty" + (let [set-id (uuid/next) + file (tht/sample-file-with-tokens + :file-id :file1 + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id + :name "one set"))) + :status-fn #(ctos/set-tokens-status % #{} #{set-id})) changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (pcb/set-token set-id (:id prev-token) token)) - + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-set-tokens-source nil)) redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - (t/is (tht/token-data-eq? token (ctob/get-token redo-lib set-id (:id token)))) - ;; Undo - (t/is (tht/token-data-eq? prev-token (ctob/get-token undo-lib set-id (:id prev-token))))))) + file-tokens-status (tht/get-tokens-status file) + redo-tokens-source (tht/get-tokens-source redo) + redo-tokens-status (tht/get-tokens-status redo) + undo-tokens-source (tht/get-tokens-source undo) + undo-tokens-status (tht/get-tokens-status undo)] -(t/deftest set-token-set-test - (t/testing "delete token set" - (let [set-name "foo" - set-id (uuid/next) - file (setup-file #(ctob/add-set % (ctob/make-token-set :id set-id :name set-name))) + ;; Redo: source and status copied from library, replacing the previous status + (t/is (= nil redo-tokens-source)) + (t/is (= #{} (ctos/get-active-theme-ids redo-tokens-status))) + (t/is (= #{} (ctos/get-active-set-ids redo-tokens-status))) + + ;; Undo: source and status restored to the original state + (t/is (nil? undo-tokens-source)) + (t/is (= (ctos/get-active-theme-ids file-tokens-status) + (ctos/get-active-theme-ids undo-tokens-status))) + (t/is (= (ctos/get-active-set-ids file-tokens-status) + (ctos/get-active-set-ids undo-tokens-status))))) + + (t/testing "if the library is the same file, the source is set to self and the status is the same one" + (let [set-id (uuid/next) + file (tht/sample-file-with-tokens + :file-id :file1 + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id + :name "one set"))) + :status-fn #(ctos/set-tokens-status % #{} #{set-id})) changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (pcb/set-token-set set-id nil)) - + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-set-tokens-source file)) redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - (t/is (not (ctob/set-path-exists? redo-lib [set-name]))) - ;; Undo - (t/is (ctob/set-path-exists? undo-lib [set-name])))) + file-tokens-status (tht/get-tokens-status file) + redo-tokens-source (tht/get-tokens-source redo) + redo-tokens-status (tht/get-tokens-status redo) + undo-tokens-source (tht/get-tokens-source undo) + undo-tokens-status (tht/get-tokens-status undo)] - (t/testing "add token set" - (let [set-name "foo" - set-id (uuid/next) - token-set (ctob/make-token-set :id set-id :name set-name) - file (setup-file identity) - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (pcb/set-token-set set-id token-set)) + ;; Redo: source and status copied from library, replacing the previous status + (t/is (= (:id file) redo-tokens-source)) + (t/is (= (ctos/get-active-theme-ids file-tokens-status) + (ctos/get-active-theme-ids redo-tokens-status))) + (t/is (= (ctos/get-active-set-ids file-tokens-status) + (ctos/get-active-set-ids redo-tokens-status))) - redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) - undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - (t/is (not (ctob/set-path-exists? undo-lib [set-name]))) - ;; Undo - (t/is (ctob/set-path-exists? redo-lib [set-name])))) + ;; Undo: source and status restored to the original state + (t/is (nil? undo-tokens-source)) + (t/is (= (ctos/get-active-theme-ids file-tokens-status) + (ctos/get-active-theme-ids undo-tokens-status))) + (t/is (= (ctos/get-active-set-ids file-tokens-status) + (ctos/get-active-set-ids undo-tokens-status)))))) - (t/testing "update token set" - (let [set-name "foo" - set-id (uuid/next) - token-set (ctob/make-token-set :id set-id :name set-name) - file (setup-file #(-> (ctob/add-set % token-set))) - new-set-name "foo1" - - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (pcb/set-token-set set-id (ctob/rename token-set new-set-name))) - - redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) - redo-token-set (ctob/get-set redo-lib set-id) - - undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo) - undo-token-set (ctob/get-set undo-lib set-id)] - - (t/is (= (ctob/get-name redo-token-set) new-set-name)) - ;; Undo - (t/is (= (ctob/get-name undo-token-set) set-name))))) - -(t/deftest generate-toggle-token-set-group-test - (t/testing "toggling set group with no active sets inside will activate all child sets" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/bar")) - (ctob/add-set (ctob/make-token-set :name "foo/bar/baz")) - (ctob/add-set (ctob/make-token-set :name "foo/bar/baz/baz-child")) - (ctob/add-theme (ctob/make-token-theme :name "theme")) - (ctob/set-active-themes #{"/theme"}))) - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (clt/generate-toggle-token-set-group (tht/get-tokens-lib file) ["foo" "bar"])) - - redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) - undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - (t/is (= #{ctob/hidden-theme-path} (ctob/get-active-theme-paths redo-lib))) - (t/is (= #{"foo/bar/baz" "foo/bar/baz/baz-child"} (:sets (ctob/get-hidden-theme redo-lib)))) - - ;; Undo - (t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib))))) - - (t/testing "toggling set group with partially active sets inside will deactivate all child sets" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/bar")) - (ctob/add-set (ctob/make-token-set :name "foo/bar/baz")) - (ctob/add-set (ctob/make-token-set :name "foo/bar/baz/baz-child")) - (ctob/add-theme (ctob/make-token-theme :name "theme" - :sets #{"foo/bar/baz"})) - (ctob/set-active-themes #{"/theme"}))) - - changes (-> (pcb/empty-changes) - (pcb/with-library-data (:data file)) - (clt/generate-toggle-token-set-group (tht/get-tokens-lib file) ["foo" "bar"])) - - redo (thf/apply-changes file changes) - redo-lib (tht/get-tokens-lib redo) - undo (thf/apply-undo-changes redo changes) - undo-lib (tht/get-tokens-lib undo)] - (t/is (= #{} (:sets (ctob/get-hidden-theme redo-lib)))) - (t/is (= #{ctob/hidden-theme-path} (ctob/get-active-theme-paths redo-lib))) - - ;; Undo - (t/is (= #{"/theme"} (ctob/get-active-theme-paths undo-lib)))))) +;; Tokens lib (t/deftest generate-move-token-set-test (t/testing "Ignore dropping set to the same position:" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo")) - (ctob/add-set (ctob/make-token-set :name "bar/baz")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo")) + (ctob/add-set (ctob/make-token-set :name "bar/baz")))) drop (partial clt/generate-move-token-set (pcb/empty-changes) (tht/get-tokens-lib file))] (t/testing "on top of identical" (t/is (= (pcb/empty-changes) @@ -359,10 +206,11 @@ (t/testing "Reorder sets when dropping next to a set:" (t/testing "at top" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo")) - (ctob/add-set (ctob/make-token-set :name "bar")) - (ctob/add-set (ctob/make-token-set :name "baz")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo")) + (ctob/add-set (ctob/make-token-set :name "bar")) + (ctob/add-set (ctob/make-token-set :name "baz")))) lib (tht/get-tokens-lib file) changes (clt/generate-move-token-set (pcb/empty-changes) lib {:from-index 1 :to-index 0 @@ -378,10 +226,11 @@ (t/is (= (ctob/get-set-names lib) undo-sets))))) (t/testing "at bottom" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo")) - (ctob/add-set (ctob/make-token-set :name "bar")) - (ctob/add-set (ctob/make-token-set :name "baz")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo")) + (ctob/add-set (ctob/make-token-set :name "bar")) + (ctob/add-set (ctob/make-token-set :name "baz")))) lib (tht/get-tokens-lib file) changes (clt/generate-move-token-set (pcb/empty-changes) lib {:from-index 0 :to-index 2 @@ -397,9 +246,10 @@ (t/is (= (ctob/get-set-names lib) undo-sets))))) (t/testing "dropping out of set group" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/bar")) - (ctob/add-set (ctob/make-token-set :name "foo")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo/bar")) + (ctob/add-set (ctob/make-token-set :name "foo")))) lib (tht/get-tokens-lib file) changes (clt/generate-move-token-set (pcb/empty-changes) lib {:from-index 1 :to-index 0 @@ -415,9 +265,10 @@ (t/is (= (ctob/get-set-names lib) undo-sets))))) (t/testing "into set group" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/bar")) - (ctob/add-set (ctob/make-token-set :name "foo")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo/bar")) + (ctob/add-set (ctob/make-token-set :name "foo")))) lib (tht/get-tokens-lib file) changes (clt/generate-move-token-set (pcb/empty-changes) lib {:from-index 2 :to-index 1 @@ -434,9 +285,10 @@ (t/testing "edge-cases:" (t/testing "prevent overriding set to identical path" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/foo")) - (ctob/add-set (ctob/make-token-set :name "foo")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo/foo")) + (ctob/add-set (ctob/make-token-set :name "foo")))) lib (tht/get-tokens-lib file)] (t/is (thrown? #?(:cljs js/Error :clj Exception) @@ -452,9 +304,10 @@ #"move token set error: path exists")))) (t/testing "dropping below collapsed group doesnt add as child" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo")) - (ctob/add-set (ctob/make-token-set :name "foo/bar")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo")) + (ctob/add-set (ctob/make-token-set :name "foo/bar")))) lib (tht/get-tokens-lib file) changes (clt/generate-move-token-set (pcb/empty-changes) lib {:from-index 0 :to-index 1 @@ -472,9 +325,10 @@ (t/deftest generate-move-token-group-test (t/testing "Ignore dropping set group to the same position" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo")) - (ctob/add-set (ctob/make-token-set :name "bar/baz")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo")) + (ctob/add-set (ctob/make-token-set :name "bar/baz")))) drop (partial clt/generate-move-token-set-group (pcb/empty-changes) (tht/get-tokens-lib file))] (t/testing "on top of identical" (t/is (= (pcb/empty-changes) @@ -494,10 +348,11 @@ (t/testing "Move set groups" (t/testing "to top" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/foo")) - (ctob/add-set (ctob/make-token-set :name "bar/bar")) - (ctob/add-set (ctob/make-token-set :name "baz/baz")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo/foo")) + (ctob/add-set (ctob/make-token-set :name "bar/bar")) + (ctob/add-set (ctob/make-token-set :name "baz/baz")))) lib (tht/get-tokens-lib file) changes (clt/generate-move-token-set-group (pcb/empty-changes) lib {:from-index 2 :to-index 0 @@ -514,9 +369,10 @@ (t/is (= (ctob/get-set-names lib) undo-sets))))) (t/testing "to bottom" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/foo")) - (ctob/add-set (ctob/make-token-set :name "bar")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo/foo")) + (ctob/add-set (ctob/make-token-set :name "bar")))) lib (tht/get-tokens-lib file) changes (clt/generate-move-token-set-group (pcb/empty-changes) lib {:from-index 0 :to-index 2 @@ -533,9 +389,10 @@ (t/is (= (ctob/get-set-names lib) undo-sets))))) (t/testing "into set group" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/foo")) - (ctob/add-set (ctob/make-token-set :name "bar/bar")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo/foo")) + (ctob/add-set (ctob/make-token-set :name "bar/bar")))) lib (tht/get-tokens-lib file) changes (clt/generate-move-token-set-group (pcb/empty-changes) lib {:from-index 0 :to-index 2 @@ -552,9 +409,10 @@ (t/testing "edge-cases:" (t/testing "prevent overriding set to identical path" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/identical/foo")) - (ctob/add-set (ctob/make-token-set :name "identical/bar")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo/identical/foo")) + (ctob/add-set (ctob/make-token-set :name "identical/bar")))) lib (tht/get-tokens-lib file)] (t/is (thrown? #?(:cljs js/Error :clj Exception) @@ -564,8 +422,9 @@ #"move token set error: path exists")))) (t/testing "prevent dropping parent to child" - (let [file (setup-file #(-> % - (ctob/add-set (ctob/make-token-set :name "foo/bar/baz")))) + (let [file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "foo/bar/baz")))) lib (tht/get-tokens-lib file)] (t/is (thrown? #?(:cljs js/Error :clj Exception) @@ -573,3 +432,772 @@ :to-index 1 :position :bot}) #"move token set error: parent-to-child")))))))) + +(t/deftest generate-delete-token-set-group-test + (t/testing "deletes all sets under the given group path" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + set-c-id (thi/new-id! :set-c) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-a-id :name "group/set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b")) + (ctob/add-set (ctob/make-token-set :id set-c-id :name "other/set-c")))) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-delete-token-set-group tokens-lib ["group"])) + + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo)] + ;; Redo: group sets deleted, other set remains + (t/is (not (ctob/set-path-exists? redo-lib ["group" "set-a"]))) + (t/is (not (ctob/set-path-exists? redo-lib ["group" "set-b"]))) + (t/is (ctob/set-path-exists? redo-lib ["other" "set-c"])) + ;; Undo: group sets restored + (t/is (ctob/set-path-exists? undo-lib ["group" "set-a"])) + (t/is (ctob/set-path-exists? undo-lib ["group" "set-b"])))) + + (t/testing "deleting a group with nested subgroups deletes all descendant sets" + (let [set-a-id (thi/new-id! :set-a) + set-b-id (thi/new-id! :set-b) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-a-id :name "group/sub/set-a")) + (ctob/add-set (ctob/make-token-set :id set-b-id :name "group/set-b")))) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-delete-token-set-group tokens-lib ["group"])) + + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo)] + ;; Redo: all group sets deleted + (t/is (not (ctob/set-path-exists? redo-lib ["group" "sub" "set-a"]))) + (t/is (not (ctob/set-path-exists? redo-lib ["group" "set-b"]))) + ;; Undo: all restored + (t/is (ctob/set-path-exists? undo-lib ["group" "sub" "set-a"])) + (t/is (ctob/set-path-exists? undo-lib ["group" "set-b"])))) + + (t/testing "deleting an empty group produces no changes" + (let [file (tht/sample-file-with-tokens + :lib-fn #(ctob/add-set % (ctob/make-token-set :name "other/set-a"))) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-delete-token-set-group tokens-lib ["nonexistent"]))] + (t/is (= [] (:redo-changes changes)))))) + +(t/deftest set-token-theme-test + (t/testing "delete token theme" + (let [theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "foo" + :group "main")))) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (pcb/set-token-theme theme-id nil)) + + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo)] + ;; Redo + (t/is (nil? (ctob/get-theme redo-lib theme-id))) + ;; Undo + (t/is (some? (ctob/get-theme undo-lib theme-id))))) + + (t/testing "add token theme" + (let [theme-id (uuid/next) + theme (ctob/make-token-theme :id theme-id + :name "foo" + :group "main") + file (tht/sample-file-with-tokens) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (pcb/set-token-theme theme-id theme)) + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo)] + ;; Redo + (t/is (some? (ctob/get-theme redo-lib theme-id))) + ;; Undo + (t/is (nil? (ctob/get-theme undo-lib theme-id))))) + + (t/testing "update token theme" + (let [theme-id (uuid/next) + prev-theme-name "foo" + prev-theme (ctob/make-token-theme :id theme-id + :name prev-theme-name + :group "main") + file (tht/sample-file-with-tokens + :lib-fn #(ctob/add-theme % prev-theme)) + new-theme-name "foo1" + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (pcb/set-token-theme theme-id (ctob/rename prev-theme new-theme-name))) + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + redo-theme (ctob/get-theme redo-lib theme-id) + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo) + undo-theme (ctob/get-theme undo-lib theme-id)] + ;; Redo + (t/is (= new-theme-name (ctob/get-name redo-theme))) + ;; Undo + (t/is (= prev-theme-name (ctob/get-name undo-theme))))) + + (t/testing "toggling token theme updates using changes history" + (let [theme-id (uuid/next) + theme (ctob/make-token-theme :id theme-id + :name "foo-theme" + :group "main") + set-name "bar-set" + token-set (ctob/make-token-set :name set-name) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-theme theme) + (ctob/add-set token-set))) + theme' (assoc theme :sets #{set-name}) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (pcb/set-token-theme theme-id theme')) + changed-file (-> file + (thf/apply-changes changes) + (thf/apply-undo-changes changes) + (thf/apply-changes changes)) + changed-lib (tht/get-tokens-lib changed-file)] + (t/is (= #{set-name} + (-> changed-lib (ctob/get-theme theme-id) :sets)))))) + +(t/deftest set-token-test + (t/testing "delete token" + (let [set-name "foo" + set-id (uuid/next) + token-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id + :name set-name)) + (ctob/add-token set-id (ctob/make-token {:name "to.delete.color.red" + :id token-id + :value "red" + :type :color})))) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (pcb/set-token set-id token-id nil)) + + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo)] + (t/is (nil? (ctob/get-token redo-lib set-id token-id))) + ;; Undo + (t/is (some? (ctob/get-token undo-lib set-id token-id))))) + + (t/testing "add token" + (let [set-name "foo" + set-id (uuid/next) + token (ctob/make-token {:name "to.add.color.red" + :value "red" + :type :color}) + file (tht/sample-file-with-tokens + :lib-fn #(-> % (ctob/add-set (ctob/make-token-set :id set-id + :name set-name)))) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (pcb/set-token set-id (:id token) token)) + + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo)] + (t/is (= token (ctob/get-token redo-lib set-id (:id token)))) + ;; Undo + (t/is (nil? (ctob/get-token undo-lib set-id (:id token)))))) + + (t/testing "update token" + (let [set-name "foo" + set-id (uuid/next) + prev-token (ctob/make-token {:name "to.update.color.red" + :value "red" + :type :color}) + token (-> prev-token + (assoc :name "color.red.changed") + (assoc :value "blue")) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id + :name set-name)) + (ctob/add-token set-id prev-token))) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (pcb/set-token set-id (:id prev-token) token)) + + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo)] + (t/is (tht/token-data-eq? token (ctob/get-token redo-lib set-id (:id token)))) + ;; Undo + (t/is (tht/token-data-eq? prev-token (ctob/get-token undo-lib set-id (:id prev-token))))))) + +(t/deftest set-token-set-test + (t/testing "delete token set" + (let [set-name "foo" + set-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(ctob/add-set % (ctob/make-token-set :id set-id :name set-name))) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (pcb/set-token-set set-id nil)) + + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo)] + (t/is (not (ctob/set-path-exists? redo-lib [set-name]))) + ;; Undo + (t/is (ctob/set-path-exists? undo-lib [set-name])))) + + (t/testing "add token set" + (let [set-name "foo" + set-id (uuid/next) + token-set (ctob/make-token-set :id set-id :name set-name) + file (tht/sample-file-with-tokens) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (pcb/set-token-set set-id token-set)) + + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo)] + (t/is (not (ctob/set-path-exists? undo-lib [set-name]))) + ;; Undo + (t/is (ctob/set-path-exists? redo-lib [set-name])))) + + (t/testing "update token set" + (let [set-name "foo" + set-id (uuid/next) + token-set (ctob/make-token-set :id set-id :name set-name) + file (tht/sample-file-with-tokens + :lib-fn #(-> (ctob/add-set % token-set))) + new-set-name "foo1" + + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (pcb/set-token-set set-id (ctob/rename token-set new-set-name))) + + redo (thf/apply-changes file changes) + redo-lib (tht/get-tokens-lib redo) + redo-token-set (ctob/get-set redo-lib set-id) + + undo (thf/apply-undo-changes redo changes) + undo-lib (tht/get-tokens-lib undo) + undo-token-set (ctob/get-set undo-lib set-id)] + + (t/is (= (ctob/get-name redo-token-set) new-set-name)) + ;; Undo + (t/is (= (ctob/get-name undo-token-set) set-name))))) + +;; Tokens Status + +(t/deftest generate-activate-theme-test + (t/testing "activating an inactive theme creates changes" + (let [set-id (uuid/next) + theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id + :name "s1")) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "t1" + :sets #{"s1"})))) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-activate-theme tokens-status tokens-lib theme-id)) + file' (thf/apply-changes file changes) + tokens-status' (tht/get-tokens-status file') + file'' (thf/apply-undo-changes file' changes) + tokens-status'' (tht/get-tokens-status file'')] + ;; Redo: theme and set are active + (t/is (ctos/theme-active? tokens-status' theme-id)) + (t/is (ctos/set-active? tokens-status' set-id)) + ;; Undo: theme and set are inactive + (t/is (not (ctos/theme-active? tokens-status'' theme-id))) + (t/is (not (ctos/set-active? tokens-status'' set-id))))) + + (t/testing "activating an already-active theme returns same changes" + (let [theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "t1" + :sets #{"s1"}))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{})) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-activate-theme tokens-status tokens-lib theme-id))] + (t/is (= [] (:redo-changes changes))) + (t/is (= [] (:undo-changes changes)))))) + +(t/deftest generate-deactivate-theme-test + (t/testing "deactivating an active theme creates changes" + (let [theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "t1" + :sets #{}))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{})) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-deactivate-theme tokens-status tokens-lib theme-id)) + file' (thf/apply-changes file changes) + tokens-status' (tht/get-tokens-status file') + file'' (thf/apply-undo-changes file' changes) + tokens-status'' (tht/get-tokens-status file'')] + ;; Redo: theme is inactive + (t/is (not (ctos/theme-active? tokens-status' theme-id))) + ;; Undo: theme is active again + (t/is (ctos/theme-active? tokens-status'' theme-id)))) + + (t/testing "deactivating an inactive theme returns same changes" + (let [theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :name "s1")) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "t1" + :sets #{"s1"})))) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-deactivate-theme tokens-status tokens-lib theme-id))] + (t/is (= [] (:redo-changes changes))) + (t/is (= [] (:undo-changes changes)))))) + +(t/deftest generate-set-theme-status-test + (t/testing "setting active? to true activates the theme" + (let [theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "t1" + :sets #{})))) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-set-theme-status tokens-status tokens-lib theme-id true)) + file' (thf/apply-changes file changes) + tokens-status' (tht/get-tokens-status file') + file'' (thf/apply-undo-changes file changes) + tokens-status'' (tht/get-tokens-status file'')] + ;; Redo: theme is active + (t/is (ctos/theme-active? tokens-status' theme-id)) + ;; Undo: theme is inactive + (t/is (not (ctos/theme-active? tokens-status'' theme-id))))) + + (t/testing "setting active? to same state returns same changes" + (let [theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "t1" + :sets #{})))) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-set-theme-status tokens-status tokens-lib theme-id false))] + (t/is (= [] (:redo-changes changes))) + (t/is (= [] (:undo-changes changes)))))) + +(t/deftest generate-toggle-theme-test + (t/testing "toggling an active theme deactivates it" + (let [theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "t1" + :sets #{}))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{})) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-toggle-theme tokens-status tokens-lib theme-id)) + file' (thf/apply-changes file changes) + tokens-status' (tht/get-tokens-status file') + file'' (thf/apply-undo-changes file' changes) + tokens-status'' (tht/get-tokens-status file'')] + ;; Redo: theme is inactive + (t/is (not (ctos/theme-active? tokens-status' theme-id))) + ;; Undo: theme is active again + (t/is (ctos/theme-active? tokens-status'' theme-id)))) + + (t/testing "toggling an inexistent theme returns same changes" + (let [theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "t1" + :sets #{})))) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-toggle-theme tokens-status tokens-lib (uuid/next)))] + (t/is (= [] (:redo-changes changes))) + (t/is (= [] (:undo-changes changes)))))) + +(t/deftest generate-set-enabled-token-set-test + (t/testing "enabling an inactive set creates changes that activate it and clear themes" + (let [set-id (thi/new-id! :set-a) + theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id :name "set-a")) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "theme" + :sets #{"set-a"}))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{})) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-set-enabled-token-set tokens-status tokens-lib set-id true)) + + redo (thf/apply-changes file changes) + redo-status (tht/get-tokens-status redo) + undo (thf/apply-undo-changes redo changes) + undo-status (tht/get-tokens-status undo)] + ;; Redo: set active, themes cleared + (t/is (ctos/set-active? redo-status set-id)) + (t/is (= #{} (ctos/get-active-theme-ids redo-status))) + ;; Undo: set inactive, themes restored + (t/is (not (ctos/set-active? undo-status set-id))) + (t/is (= #{theme-id} (ctos/get-active-theme-ids undo-status))))) + + (t/testing "enabling an already-active set returns no changes" + (let [set-id (thi/new-id! :set-a) + file (tht/sample-file-with-tokens + :lib-fn #(ctob/add-set % (ctob/make-token-set :id set-id :name "set-a")) + :status-fn #(ctos/set-tokens-status % #{} #{set-id})) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-set-enabled-token-set tokens-status tokens-lib set-id true))] + (t/is (= [] (:redo-changes changes))) + (t/is (= [] (:undo-changes changes)))))) + +(t/deftest generate-toggle-token-set-test + (t/testing "toggling an active set will deactivate it" + (let [theme-id (uuid/next) + set-id (thi/new-id! :foo-bar) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id :name "foo/bar")) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "theme" + :sets #{"foo/bar"}))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{set-id})) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-toggle-token-set tokens-status tokens-lib set-id)) + + redo (thf/apply-changes file changes) + redo-status (tht/get-tokens-status redo) + undo (thf/apply-undo-changes redo changes) + undo-status (tht/get-tokens-status undo)] + ;; Redo: set is deactivated, all themes deactivated + (t/is (not (ctos/set-active? redo-status set-id))) + (t/is (= #{} (ctos/get-active-theme-ids redo-status))) + ;; Undo: set is active again, themes restored + (t/is (ctos/set-active? undo-status set-id)) + (t/is (= #{theme-id} (ctos/get-active-theme-ids undo-status))))) + + (t/testing "toggling an inexistent set will return no changes" + (let [theme-id (uuid/next) + set-id (thi/new-id! :foo-bar) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id :name "foo/bar")) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "theme" + :sets #{"foo/bar"}))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{})) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-toggle-token-set tokens-status tokens-lib (uuid/next)))] + (t/is (= [] (:redo-changes changes))) + (t/is (= [] (:undo-changes changes)))))) + +(t/deftest generate-toggle-token-set-group-test + (t/testing "toggling set group with no active sets inside will activate all child sets" + (let [set-bar-id (thi/new-id! :foo-bar) + set-baz-id (thi/new-id! :foo-bar-baz) + set-child-id (thi/new-id! :baz-child) + theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-bar-id :name "foo/bar")) + (ctob/add-set (ctob/make-token-set :id set-baz-id :name "foo/bar/baz")) + (ctob/add-set (ctob/make-token-set :id set-child-id :name "foo/bar/baz/baz-child")) + (ctob/add-theme (ctob/make-token-theme :id theme-id :name "theme"))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{})) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-toggle-token-set-group tokens-status tokens-lib ["foo" "bar"])) + + redo (thf/apply-changes file changes) + redo-status (tht/get-tokens-status redo) + undo (thf/apply-undo-changes redo changes) + undo-status (tht/get-tokens-status undo)] + ;; Redo: all child sets activated, all themes deactivated + (t/is (ctos/set-active? redo-status set-baz-id)) + (t/is (ctos/set-active? redo-status set-child-id)) + (t/is (= #{} (ctos/get-active-theme-ids redo-status))) + ;; Undo: child sets deactivated, themes restored + (t/is (not (ctos/set-active? undo-status set-baz-id))) + (t/is (not (ctos/set-active? undo-status set-child-id))) + (t/is (= #{theme-id} (ctos/get-active-theme-ids undo-status))))) + + (t/testing "toggling an inexistent set group will return no changes" + (let [set-bar-id (thi/new-id! :foo-bar) + set-baz-id (thi/new-id! :foo-bar-baz) + set-child-id (thi/new-id! :baz-child) + theme-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-bar-id :name "foo/bar")) + (ctob/add-set (ctob/make-token-set :id set-baz-id :name "foo/bar/baz")) + (ctob/add-set (ctob/make-token-set :id set-child-id :name "foo/bar/baz/baz-child")) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "theme" + :sets #{"foo/bar/baz"}))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{set-baz-id})) + + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-toggle-token-set-group tokens-status tokens-lib ["foo" "nonexistent"]))] + (t/is (= [] (:redo-changes changes))) + (t/is (= [] (:undo-changes changes)))))) + +(t/deftest generate-sync-tokens-status-with-lib-test + (t/testing "sync removes stale theme and set ids from status" + (let [stale-theme-id (uuid/next) + stale-set-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn identity + :status-fn #(ctos/set-tokens-status % #{stale-theme-id} #{stale-set-id})) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-sync-tokens-status-with-lib tokens-status tokens-lib)) + file' (thf/apply-changes file changes) + tokens-status' (tht/get-tokens-status file') + file'' (thf/apply-undo-changes file' changes) + tokens-status'' (tht/get-tokens-status file'')] + ;; Redo: stale ids are removed + (t/is (= #{} (ctos/get-active-theme-ids tokens-status'))) + (t/is (= #{} (ctos/get-active-set-ids tokens-status'))) + ;; Undo: stale ids are restored + (t/is (= #{stale-theme-id} (ctos/get-active-theme-ids tokens-status''))) + (t/is (= #{stale-set-id} (ctos/get-active-set-ids tokens-status''))))) + + (t/testing "sync with status already in sync returns same changes" + (let [theme-id (uuid/next) + set-id (uuid/next) + file (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id set-id :name "s1")) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "t1" + :sets #{"s1"}))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{set-id})) + tokens-status (tht/get-tokens-status file) + tokens-lib (tht/get-tokens-lib file) + changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-sync-tokens-status-with-lib tokens-status tokens-lib))] + (t/is (= [] (:redo-changes changes))) + (t/is (= [] (:undo-changes changes)))))) + +;; Nil-safety: all public functions must be no-op with nil tokens-lib or tokens-status + +(t/deftest nil-tokens-lib-generate-move-token-set-test + (t/testing "generate-move-token-set is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes)] + (t/is (= changes + (clt/generate-move-token-set changes nil {:from-index 0 + :to-index 1 + :position :top})))))) + +(t/deftest nil-tokens-lib-generate-move-token-set-group-test + (t/testing "generate-move-token-set-group is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes)] + (t/is (= changes + (clt/generate-move-token-set-group changes nil {:from-index 0 + :to-index 1 + :position :top})))))) + +(t/deftest nil-tokens-lib-generate-delete-token-set-group-test + (t/testing "generate-delete-token-set-group is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes)] + (t/is (= changes + (clt/generate-delete-token-set-group changes nil ["group"])))))) + +(t/deftest nil-tokens-status-generate-activate-theme-test + (t/testing "generate-activate-theme is no-op with nil tokens-status" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-lib (tht/get-tokens-lib file)] + (t/is (= changes + (clt/generate-activate-theme changes nil tokens-lib (uuid/next))))))) + +(t/deftest nil-tokens-lib-generate-activate-theme-test + (t/testing "generate-activate-theme is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-status (tht/get-tokens-status file)] + (t/is (= changes + (clt/generate-activate-theme changes tokens-status nil (uuid/next))))))) + +(t/deftest nil-tokens-status-generate-deactivate-theme-test + (t/testing "generate-deactivate-theme is no-op with nil tokens-status" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-lib (tht/get-tokens-lib file)] + (t/is (= changes + (clt/generate-deactivate-theme changes nil tokens-lib (uuid/next))))))) + +(t/deftest nil-tokens-lib-generate-deactivate-theme-test + (t/testing "generate-deactivate-theme is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-status (tht/get-tokens-status file)] + (t/is (= changes + (clt/generate-deactivate-theme changes tokens-status nil (uuid/next))))))) + +(t/deftest nil-tokens-status-generate-set-theme-status-test + (t/testing "generate-set-theme-status is no-op with nil tokens-status" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-lib (tht/get-tokens-lib file)] + (t/is (= changes + (clt/generate-set-theme-status changes nil tokens-lib (uuid/next) true)))))) + +(t/deftest nil-tokens-lib-generate-set-theme-status-test + (t/testing "generate-set-theme-status is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-status (tht/get-tokens-status file)] + (t/is (= changes + (clt/generate-set-theme-status changes tokens-status nil (uuid/next) true)))))) + +(t/deftest nil-tokens-status-generate-toggle-theme-test + (t/testing "generate-toggle-theme is no-op with nil tokens-status" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-lib (tht/get-tokens-lib file)] + (t/is (= changes + (clt/generate-toggle-theme changes nil tokens-lib (uuid/next))))))) + +(t/deftest nil-tokens-lib-generate-toggle-theme-test + (t/testing "generate-toggle-theme is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-status (tht/get-tokens-status file)] + (t/is (= changes + (clt/generate-toggle-theme changes tokens-status nil (uuid/next))))))) + +(t/deftest nil-tokens-status-generate-set-enabled-token-set-test + (t/testing "generate-set-enabled-token-set is no-op with nil tokens-status" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-lib (tht/get-tokens-lib file)] + (t/is (= changes + (clt/generate-set-enabled-token-set changes nil tokens-lib (uuid/next) true)))))) + +(t/deftest nil-tokens-lib-generate-set-enabled-token-set-test + (t/testing "generate-set-enabled-token-set is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-status (tht/get-tokens-status file)] + (t/is (= changes + (clt/generate-set-enabled-token-set changes tokens-status nil (uuid/next) true)))))) + +(t/deftest nil-tokens-status-generate-toggle-token-set-test + (t/testing "generate-toggle-token-set is no-op with nil tokens-status" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-lib (tht/get-tokens-lib file)] + (t/is (= changes + (clt/generate-toggle-token-set changes nil tokens-lib (uuid/next))))))) + +(t/deftest nil-tokens-lib-generate-toggle-token-set-test + (t/testing "generate-toggle-token-set is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-status (tht/get-tokens-status file)] + (t/is (= changes + (clt/generate-toggle-token-set changes tokens-status nil (uuid/next))))))) + +(t/deftest nil-tokens-status-generate-toggle-token-set-group-test + (t/testing "generate-toggle-token-set-group is no-op with nil tokens-status" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-lib (tht/get-tokens-lib file)] + (t/is (= changes + (clt/generate-toggle-token-set-group changes nil tokens-lib ["foo" "bar"])))))) + +(t/deftest nil-tokens-lib-generate-toggle-token-set-group-test + (t/testing "generate-toggle-token-set-group is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-status (tht/get-tokens-status file)] + (t/is (= changes + (clt/generate-toggle-token-set-group changes tokens-status nil ["foo" "bar"])))))) + +(t/deftest nil-tokens-status-generate-sync-tokens-status-with-lib-test + (t/testing "generate-sync-tokens-status-with-lib is no-op with nil tokens-status" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-lib (tht/get-tokens-lib file)] + (t/is (= changes + (clt/generate-sync-tokens-status-with-lib changes nil tokens-lib)))))) + +(t/deftest nil-tokens-lib-generate-sync-tokens-status-with-lib-test + (t/testing "generate-sync-tokens-status-with-lib is no-op with nil tokens-lib" + (let [changes (pcb/empty-changes) + file (tht/sample-file-with-tokens) + tokens-status (tht/get-tokens-status file)] + (t/is (= changes + (clt/generate-sync-tokens-status-with-lib changes tokens-status nil)))))) diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc index 3a37c82a51..0c06f7d9df 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -11,11 +11,13 @@ #?(:clj [app.common.test-helpers.tokens :as tht]) #?(:clj [clojure.datafy :refer [datafy]]) [app.common.data :as d] + [app.common.files.tokens :as cfo] [app.common.test-helpers.ids-map :as thi] [app.common.time :as ct] [app.common.transit :as tr] [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [clojure.test :as t])) @@ -247,6 +249,16 @@ theme' (ctob/toggle-set theme nil)] (t/is (= (:sets theme') #{}))))) +(t/deftest get-theme-path + (let [theme1 (ctob/make-token-theme :name "theme1") + theme2 (ctob/make-token-theme :group "group1" :name "theme2")] + (t/is (= "/theme1" (ctob/get-theme-path theme1))) + (t/is (= "/theme1" (ctob/get-theme-path theme1 false))) + (t/is (= "theme1" (ctob/get-theme-path theme1 true))) + (t/is (= "group1/theme2" (ctob/get-theme-path theme2))) + (t/is (= "group1/theme2" (ctob/get-theme-path theme2 false))) + (t/is (= "group1 / theme2" (ctob/get-theme-path theme2 true))))) + (t/deftest make-tokens-lib (let [tokens-lib (ctob/make-tokens-lib)] (t/is (= (ctob/set-count tokens-lib) 0)))) @@ -574,208 +586,19 @@ "token-4" (ctob/make-token :name "token-4" :type :border-radius - :value 4000)})) - (ctob/update-theme ctob/hidden-theme-id - #(ctob/enable-sets % #{"set-a" "set-b"}))) + :value 4000)}))) + set-a-id (ctob/get-id (ctob/get-set-by-name tokens-lib "set-a")) + set-b-id (ctob/get-id (ctob/get-set-by-name tokens-lib "set-b")) + tokens-status (ctos/make-tokens-status :active-theme-ids #{} + :active-set-ids #{set-a-id set-b-id}) - tokens (ctob/get-tokens-in-active-sets tokens-lib)] + tokens (cfo/get-tokens-in-active-sets tokens-status tokens-lib)] (t/is (= (mapv key tokens) ["token-1" "token-2" "token-3"])) (t/is (= (get-in tokens ["token-1" :value]) 100)) (t/is (= (get-in tokens ["token-2" :value]) 20)) (t/is (= (get-in tokens ["token-3" :value]) 300)))) -(t/deftest list-active-themes-tokens-one-theme - (let [tokens-lib (-> (ctob/make-tokens-lib) - (ctob/add-set (ctob/make-token-set :name "set-a" - :tokens {"token-1" - (ctob/make-token :name "token-1" - :type :border-radius - :value 10) - "token-2" - (ctob/make-token :name "token-2" - :type :border-radius - :value 20)})) - (ctob/add-set (ctob/make-token-set :name "set-b" - :tokens {"token-1" - (ctob/make-token :name "token-1" - :type :border-radius - :value 100) - "token-3" - (ctob/make-token :name "token-3" - :type :border-radius - :value 300)})) - (ctob/add-set (ctob/make-token-set :name "set-c" - :tokens {"token-1" - (ctob/make-token :name "token-1" - :type :border-radius - :value 1000) - "token-2" - (ctob/make-token :name "token-2" - :type :border-radius - :value 2000) - "token-3" - (ctob/make-token :name "token-3" - :type :border-radius - :value 3000) - "token-4" - (ctob/make-token :name "token-4" - :type :border-radius - :value 4000)})) - (ctob/add-theme (ctob/make-token-theme :name "single-theme" - :sets #{"set-b" "set-c" "set-a"})) - (ctob/set-active-themes #{"/single-theme"})) - - tokens (ctob/get-tokens-in-active-sets tokens-lib)] - - ;; Note that sets order inside the theme is undefined. What matters is order in that the - ;; sets have been added to the library. - (t/is (= (mapv key tokens) ["token-1" "token-2" "token-3" "token-4"])) - (t/is (= (get-in tokens ["token-1" :value]) 1000)) - (t/is (= (get-in tokens ["token-2" :value]) 2000)) - (t/is (= (get-in tokens ["token-3" :value]) 3000)) - (t/is (= (get-in tokens ["token-4" :value]) 4000)))) - -(t/deftest list-active-themes-tokens-two-themes - (let [tokens-lib (-> (ctob/make-tokens-lib) - (ctob/add-set (ctob/make-token-set :name "set-a" - :tokens {"token-1" - (ctob/make-token :name "token-1" - :type :border-radius - :value 10) - "token-2" - (ctob/make-token :name "token-2" - :type :border-radius - :value 20)})) - (ctob/add-set (ctob/make-token-set :name "set-b" - :tokens {"token-1" - (ctob/make-token :name "token-1" - :type :border-radius - :value 100) - "token-3" - (ctob/make-token :name "token-3" - :type :border-radius - :value 300)})) - (ctob/add-set (ctob/make-token-set :name "set-c" - :tokens {"token-1" - (ctob/make-token :name "token-1" - :type :border-radius - :value 1000) - "token-2" - (ctob/make-token :name "token-2" - :type :border-radius - :value 2000) - "token-3" - (ctob/make-token :name "token-3" - :type :border-radius - :value 3000) - "token-4" - (ctob/make-token :name "token-4" - :type :border-radius - :value 4000)})) - (ctob/add-theme (ctob/make-token-theme :name "theme-1" - :sets #{"set-b"})) - (ctob/add-theme (ctob/make-token-theme :name "theme-2" - :sets #{"set-b" "set-a"})) - (ctob/set-active-themes #{"/theme-1" "/theme-2"})) - - tokens (ctob/get-tokens-in-active-sets tokens-lib)] - - ;; Note that themes order is irrelevant. What matters is the union of the active sets - ;; and the order of the sets in the library. - (t/is (= (mapv key tokens) ["token-1" "token-2" "token-3"])) - (t/is (= (get-in tokens ["token-1" :value]) 100)) - (t/is (= (get-in tokens ["token-2" :value]) 20)) - (t/is (= (get-in tokens ["token-3" :value]) 300)))) - -(t/deftest list-active-themes-tokens-bug-taiga-10617 - (let [tokens-lib (-> (ctob/make-tokens-lib) - (ctob/add-set (ctob/make-token-set :name "Mode/Dark" - :tokens {"red" - (ctob/make-token :name "red" - :type :color - :value "#700000")})) - (ctob/add-set (ctob/make-token-set :name "Mode/Light" - :tokens {"red" - (ctob/make-token :name "red" - :type :color - :value "#ff0000")})) - (ctob/add-set (ctob/make-token-set :name "Device/Desktop" - :tokens {"border1" - (ctob/make-token :name "border1" - :type :border-radius - :value 30)})) - (ctob/add-set (ctob/make-token-set :name "Device/Mobile" - :tokens {"border1" - (ctob/make-token :name "border1" - :type :border-radius - :value 50)})) - (ctob/add-theme (ctob/make-token-theme :group "App" - :name "Mobile" - :sets #{"Mode/Dark" "Device/Mobile"})) - (ctob/add-theme (ctob/make-token-theme :group "App" - :name "Web" - :sets #{"Mode/Dark" "Mode/Light" "Device/Desktop"})) - (ctob/add-theme (ctob/make-token-theme :group "Brand" - :name "Brand A" - :sets #{"Mode/Dark" "Mode/Light" "Device/Desktop" "Device/Mobile"})) - (ctob/add-theme (ctob/make-token-theme :group "Brand" - :name "Brand B" - :sets #{})) - (ctob/set-active-themes #{"App/Web" "Brand/Brand A"})) - - tokens (ctob/get-tokens-in-active-sets tokens-lib)] - - (t/is (= (mapv key tokens) ["red" "border1"])) - (t/is (= (get-in tokens ["red" :value]) "#ff0000")) - (t/is (= (get-in tokens ["border1" :value]) 50)))) - -(t/deftest list-active-themes-tokens-no-tokens - (let [tokens-lib (-> (ctob/make-tokens-lib) - (ctob/add-set (ctob/make-token-set :name "set-a"))) - - tokens (ctob/get-tokens-in-active-sets tokens-lib)] - - (t/is (empty? tokens)))) - -(t/deftest list-active-themes-tokens-no-sets - (let [tokens-lib (ctob/make-tokens-lib) - tokens (ctob/get-tokens-in-active-sets tokens-lib)] - - (t/is (empty? tokens)))) - -(t/deftest sets-at-path-active-state - (let [tokens-lib (-> (ctob/make-tokens-lib) - - (ctob/add-set (ctob/make-token-set :name "foo/bar/baz")) - (ctob/add-set (ctob/make-token-set :name "foo/bar/bam")) - - (ctob/add-theme (ctob/make-token-theme :name "none")) - (ctob/add-theme (ctob/make-token-theme :name "partial" - :sets #{"foo/bar/baz"})) - (ctob/add-theme (ctob/make-token-theme :name "all" - :sets #{"foo/bar/baz" - "foo/bar/bam"})) - (ctob/add-theme (ctob/make-token-theme :name "invalid" - :sets #{"foo/missing"}))) - - expected-none (-> tokens-lib - (ctob/set-active-themes #{"/none"}) - (ctob/sets-at-path-all-active? ["foo"])) - expected-all (-> tokens-lib - (ctob/set-active-themes #{"/all"}) - (ctob/sets-at-path-all-active? ["foo"])) - expected-partial (-> tokens-lib - (ctob/set-active-themes #{"/partial"}) - (ctob/sets-at-path-all-active? ["foo"])) - expected-invalid-none (-> tokens-lib - (ctob/set-active-themes #{"/invalid"}) - (ctob/sets-at-path-all-active? ["foo"]))] - (t/is (= :none expected-none)) - (t/is (= :all expected-all)) - (t/is (= :partial expected-partial)) - (t/is (= :none expected-invalid-none)))) - (t/deftest add-token-theme (let [theme-id (uuid/next) tokens-lib (ctob/make-tokens-lib) @@ -1283,12 +1106,12 @@ (let [theme-1-id (uuid/next) theme-2-id (uuid/next) theme-3-id (uuid/next) - theme-4-id (uuid/next) + ;; theme-4-id (uuid/next) tokens-lib (-> (ctob/make-tokens-lib) (ctob/add-theme (ctob/make-token-theme :id theme-1-id :group "" :name "token-theme-1")) (ctob/add-theme (ctob/make-token-theme :id theme-2-id :group "group1" :name "token-theme-2")) (ctob/add-theme (ctob/make-token-theme :id theme-3-id :group "group1" :name "token-theme-3")) - #_(ctob/add-theme (ctob/make-token-theme :in-theme-4-id :group "group2" :name "token-theme-4"))) + #_(ctob/add-theme (ctob/make-token-theme :id theme-4-id :group "group2" :name "token-theme-4"))) tokens-lib' (-> tokens-lib (ctob/update-theme theme-2-id @@ -1612,9 +1435,9 @@ :group "group-1" :external-id "test-id-01" :modified-at now - :sets #{"core"})) - (ctob/toggle-theme-active (thi/id :theme-1))) - result (ctob/export-dtcg-json tokens-lib) + :sets #{"core"}))) + tokens-status (cfo/toggle-theme-active (ctos/make-tokens-status) tokens-lib (thi/id :theme-1)) + result (ctob/export-dtcg-json tokens-lib tokens-status) expected {"$themes" [{"description" "" "group" "group-1" "isSource" false @@ -1665,9 +1488,9 @@ :group "group-1" :external-id "test-id-01" :modified-at now - :sets #{"some/set"})) - (ctob/toggle-theme-active (thi/id :theme-1))) - result (ctob/export-dtcg-multi-file tokens-lib) + :sets #{"some/set"}))) + tokens-status (cfo/toggle-theme-active (ctos/make-tokens-status) tokens-lib (thi/id :theme-1)) + result (ctob/export-dtcg-multi-file tokens-lib tokens-status) expected {"$themes.json" [{"description" "" "group" "group-1" "isSource" false diff --git a/common/test/common_tests/types/tokens_migrations_test.cljc b/common/test/common_tests/types/tokens_migrations_test.cljc index 779c24395f..31b8441e92 100644 --- a/common/test/common_tests/types/tokens_migrations_test.cljc +++ b/common/test/common_tests/types/tokens_migrations_test.cljc @@ -7,9 +7,10 @@ (ns common-tests.types.tokens-migrations-test (:require [app.common.data :as d] + [app.common.files.tokens :as cfo] [app.common.test-helpers.ids-map :as thi] - [app.common.time :as ct] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [clojure.datafy :refer [datafy]] [clojure.test :as t])) @@ -79,4 +80,55 @@ (t/is (= (ctob/get-description theme1') (ctob/get-description theme1))) (t/is (= (ctob/get-id theme2') (ctob/get-id theme2))) (t/is (= (ctob/get-name theme2') (ctob/get-name theme2))) - (t/is (= (ctob/get-description theme2') (ctob/get-description theme2)))))) \ No newline at end of file + (t/is (= (ctob/get-description theme2') (ctob/get-description theme2)))))) + +(t/deftest make-tokens-status-from-tokens-lib + (t/testing "active themes and sets from a legacy tokens-lib should be added to the status" + (let [base-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-a) + :name "set-a")) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-b) + :name "set-b")) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-c) + :name "set-c")) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-d) + :name "set-d")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-1) + :name "theme-1" + :sets #{"set-a" "set-b"})) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-2) + :name "theme-2" + :sets #{"set-b"})) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-3) + :name "theme-3" + :sets #{"set-c" "set-d"}))) + ;; Build a legacy tokens lib to test migration + tokens-lib (ctob/map->tokens-lib {:sets (.-sets base-lib) + :themes (.-themes base-lib) + :active-themes #{"/theme-1" "/theme-2"}}) + + tokens-status (cfo/make-tokens-status-from-lib tokens-lib)] + + (t/is (ctos/tokens-status? tokens-status)) + (t/is (ctos/check-tokens-status tokens-status)) + (t/is (= (count (ctos/get-active-theme-ids tokens-status)) 2)) + (t/is (ctos/theme-active? tokens-status (thi/id :theme-1))) + (t/is (ctos/theme-active? tokens-status (thi/id :theme-2))) + (t/is (= 2 (count (ctos/get-active-set-ids tokens-status)))) + (t/is (ctos/set-active? tokens-status (thi/id :set-a))) + (t/is (ctos/set-active? tokens-status (thi/id :set-b))))) + + (t/testing "hidden theme should be excluded from active theme ids" + (let [tokens-lib (ctob/make-tokens-lib) + status (cfo/make-tokens-status-from-lib tokens-lib)] + (t/is (not (contains? (ctos/get-active-set-ids status) ctob/hidden-theme-id))))) + + (t/testing "returns empty sets when no active themes" + (let [base-lib (ctob/make-tokens-lib) + tokens-lib (ctob/map->tokens-lib + {:sets (:sets (.sets base-lib)) + :themes (:themes (.themes base-lib)) + :active-themes #{}}) + status (cfo/make-tokens-status-from-lib tokens-lib)] + (t/is (= #{} (ctos/get-active-theme-ids status))) + (t/is (= #{} (ctos/get-active-set-ids status)))))) diff --git a/frontend/playwright/data/workspace/get-file-13958.json b/frontend/playwright/data/workspace/get-file-13958.json index 77b9815d1a..958c7952a7 100644 --- a/frontend/playwright/data/workspace/get-file-13958.json +++ b/frontend/playwright/data/workspace/get-file-13958.json @@ -25,7 +25,7 @@ "~:revn": 11, "~:modified-at": "~m1778577544627", "~:vern": 151920609, - "~:id": "~u1bac06a1-a942-80a6-8008-0222e1ec38d5", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", "~:is-shared": false, "~:migrations": { "~#ordered-set": [ @@ -128,6 +128,23 @@ } } }, + "~:tokens-status": { + "~#penpot/tokens-status": { + "~:active-theme-ids": { + "~#set": ["~uc00d9d2e-76f9-815b-8005-e1260332abac", "~uc00d9d2e-76f9-815b-8005-e1260332abb0"] + }, + "~:active-set-ids": { + "~#set": [ + "~u4cdd76d8-0e6d-8168-8008-01189f0ece1d", + "~u4cdd76d8-0e6d-8168-8008-01189f161e35", + "~u4cdd76d8-0e6d-8168-8008-01189f101a3f", + "~u4cdd76d8-0e6d-8168-8008-01189f0e2151", + "~u4cdd76d8-0e6d-8168-8008-01189f0cf6d3", + "~u4cdd76d8-0e6d-8168-8008-01189f0e7da8" + ] + } + } + }, "~:tokens-lib": { "~#penpot/tokens-lib": { "~:sets": { @@ -4312,7 +4329,7 @@ } } }, - "~:id": "~u1bac06a1-a942-80a6-8008-0222e1ec38d5", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", "~:options": { "~:components-v2": true, "~:base-font-size": "16px" diff --git a/frontend/playwright/data/workspace/get-file-inspect-tab.json b/frontend/playwright/data/workspace/get-file-inspect-tab.json index 37220dad65..5b11da7500 100644 --- a/frontend/playwright/data/workspace/get-file-inspect-tab.json +++ b/frontend/playwright/data/workspace/get-file-inspect-tab.json @@ -5771,6 +5771,16 @@ }, "~:id": "~u7b2da435-6186-815a-8007-0daa95d2f26d", "~:options": { "~:components-v2": true, "~:base-font-size": "16px" }, + "~:tokens-status": { + "~#penpot/tokens-status": { + "~:active-theme-ids": { + "~#set": [] + }, + "~:active-set-ids": { + "~#set": ["~u5403f14e-eb02-80be-8007-0494f09cefca"] + } + } + }, "~:tokens-lib": { "~#penpot/tokens-lib": { "~:sets": { diff --git a/frontend/playwright/data/workspace/get-file-layout-stroke-token-json b/frontend/playwright/data/workspace/get-file-layout-stroke-token-json index b690e5505c..2975acf029 100644 --- a/frontend/playwright/data/workspace/get-file-layout-stroke-token-json +++ b/frontend/playwright/data/workspace/get-file-layout-stroke-token-json @@ -134,6 +134,18 @@ "~:components-v2": true, "~:base-font-size": "16px" }, + "~:tokens-status": { + "~#penpot/tokens-status": { + "~:active-theme-ids": { + "~#set": [] + }, + "~:active-set-ids": { + "~#set": [ + "~u7c1a66f7-5186-8060-8007-6ce67f8f2592" + ] + } + } + }, "~:tokens-lib": { "~#penpot/tokens-lib": { "~:sets": { diff --git a/frontend/playwright/data/workspace/get-file-libraries-tokens.json b/frontend/playwright/data/workspace/get-file-libraries-tokens.json new file mode 100644 index 0000000000..5163f45542 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-libraries-tokens.json @@ -0,0 +1,23 @@ +[ + { + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:is-indirect": false, + "~:name": "published component", + "~:revn": 3, + "~:modified-at": "~m1730103592325", + "~:vern": 0, + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:project-id": "~u3622460c-3408-81e2-8005-2fc9059741e0", + "~:synced-at": "~m1730103575856", + "~:created-at": "~m1730101410834" + } +] diff --git a/frontend/playwright/data/workspace/get-file-token-tooltip.json b/frontend/playwright/data/workspace/get-file-token-tooltip.json index 55a35d9d60..2474f4df79 100644 --- a/frontend/playwright/data/workspace/get-file-token-tooltip.json +++ b/frontend/playwright/data/workspace/get-file-token-tooltip.json @@ -53,6 +53,18 @@ } } }, + "~:tokens-status": { + "~#penpot/tokens-status": { + "~:active-theme-ids": { + "~#set": [] + }, + "~:active-set-ids": { + "~#set": [ + "~uf71719b3-63a1-8157-8008-40346917de8c" + ] + } + } + }, "~:tokens-lib": { "~#penpot/tokens-lib": { "~:sets": { @@ -373,7 +385,7 @@ } } }, - "~:id": "~u55608328-aed3-807a-8008-41341f2f7247", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", "~:options": { "~:components-v2": true, "~:base-font-size": "16px" diff --git a/frontend/playwright/data/workspace/get-file-tokens-all-types.json b/frontend/playwright/data/workspace/get-file-tokens-all-types.json index 75700e00fa..17584ba7df 100644 --- a/frontend/playwright/data/workspace/get-file-tokens-all-types.json +++ b/frontend/playwright/data/workspace/get-file-tokens-all-types.json @@ -29,7 +29,7 @@ "~:revn": 3, "~:modified-at": "~m1779204621124", "~:vern": 0, - "~:id": "~u9fb430ed-e1e9-81bc-8008-0b7ae978d9c4", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", "~:is-shared": false, "~:migrations": { "~#ordered-set": [ @@ -130,11 +130,23 @@ "~:name": "Page 1" } }, - "~:id": "~u9fb430ed-e1e9-81bc-8008-0b7ae978d9c4", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", "~:options": { "~:components-v2": true, "~:base-font-size": "16px" }, + "~:tokens-status": { + "~#penpot/tokens-status": { + "~:active-theme-ids": { + "~#set": [] + }, + "~:active-set-ids": { + "~#set": [ + "~u1dc5cafc-6d57-808d-8008-0b7af8180cbd" + ] + } + } + }, "~:tokens-lib": { "~#penpot/tokens-lib": { "~:sets": { diff --git a/frontend/playwright/data/workspace/get-file-tokens.json b/frontend/playwright/data/workspace/get-file-tokens.json index 597aa2d55b..07332c72f9 100644 --- a/frontend/playwright/data/workspace/get-file-tokens.json +++ b/frontend/playwright/data/workspace/get-file-tokens.json @@ -42,10 +42,24 @@ ] } }, - "~:id": "~u51e13852-1a8e-8037-8005-9e9413a1f1f6", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", "~:options": { "~:components-v2": true }, + "~:tokens-status": { + "~#penpot/tokens-status": { + "~:active-theme-ids": { + "~#set": ["~u66697432-c33d-8055-8006-2c62de27d738"] + }, + "~:active-set-ids": { + "~#set": [ + "~u66697432-c33d-8055-8006-2c62de27d709", + "~u51e13852-1a8e-8037-8005-9e9413a1f1f6", + "~u66697432-c33d-8055-8006-2c62de27d731" + ] + } + } + }, "~:tokens-lib": { "~#penpot/tokens-lib": { "~:sets": { diff --git a/frontend/playwright/data/workspace/get-file-typography-tokens.json b/frontend/playwright/data/workspace/get-file-typography-tokens.json index 8e3497af70..0377a574a3 100644 --- a/frontend/playwright/data/workspace/get-file-typography-tokens.json +++ b/frontend/playwright/data/workspace/get-file-typography-tokens.json @@ -54,6 +54,19 @@ "~:components-v2": true, "~:base-font-size": "16px" }, + "~:tokens-status": { + "~#penpot/tokens-status": { + "~:active-theme-ids": { + "~#set": [] + }, + "~:active-set-ids": { + "~#set": [ + "~u7239ff52-c1d9-8014-8006-ae7ad49690e3", + "~u97915939-ccca-808f-8006-aeb3c391e0f4" + ] + } + } + }, "~:tokens-lib": { "~#penpot/tokens-lib": { "~:sets": { diff --git a/frontend/playwright/data/workspace/get-file-with-lib.json b/frontend/playwright/data/workspace/get-file-with-lib.json new file mode 100644 index 0000000000..b3133a6722 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-with-lib.json @@ -0,0 +1,60 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "styles/v2", + "fdata/objects-map", + "tokens/numeric-input", + "render-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~u5519794a-7293-802b-8008-16c227930902", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "File with attached lib", + "~:revn": 3, + "~:modified-at": "~m1787664703168", + "~:vern": 0, + "~:id": "~ua604eafd-4265-803c-8008-898ba4b3c8d2", + "~:is-shared": false, + "~:version": 67, + "~:project-id": "~ua604eafd-4265-803c-8008-898b8b680ab4", + "~:created-at": "~m1787664674511", + "~:backend": "db", + "~:data": { + "~:pages": [ + "~ua604eafd-4265-803c-8008-898ba4b3c8d3" + ], + "~:pages-index": { + "~ua604eafd-4265-803c-8008-898ba4b3c8d3": { + "~:objects": { + "~#penpot/objects-map/v2": { + "~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ud9574b23-9bcc-80d2-8008-898baf40c2e8\"]]]", + "~ud9574b23-9bcc-80d2-8008-898baf40c2e8": "[\"~#shape\",[\"^ \",\"~:y\",100,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",199.9999966621399,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",100,\"~:y\",100]],[\"^<\",[\"^ \",\"~:x\",299.9999966621399,\"~:y\",100]],[\"^<\",[\"^ \",\"~:x\",299.9999966621399,\"~:y\",199.99999511241913]],[\"^<\",[\"^ \",\"~:x\",100,\"~:y\",199.99999511241913]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ud9574b23-9bcc-80d2-8008-898baf40c2e8\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",100,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",100,\"~:y\",100,\"^8\",199.9999966621399,\"~:height\",99.99999511241913,\"~:x1\",100,\"~:y1\",100,\"~:x2\",299.9999966621399,\"~:y2\",199.99999511241913]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",99.99999511241913,\"~:flip-y\",null]]" + } + }, + "~:id": "~ua604eafd-4265-803c-8008-898ba4b3c8d3", + "~:name": "Page 1" + } + }, + "~:id": "~ua604eafd-4265-803c-8008-898ba4b3c8d2", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/multiselection-typography.json b/frontend/playwright/data/workspace/multiselection-typography.json index 8fbb18f600..f462b5a55b 100644 --- a/frontend/playwright/data/workspace/multiselection-typography.json +++ b/frontend/playwright/data/workspace/multiselection-typography.json @@ -29,7 +29,7 @@ "~:revn": 36, "~:modified-at": "~m1776760054954", "~:vern": 0, - "~:id": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", "~:is-shared": false, "~:migrations": { "~#ordered-set": [ @@ -423,7 +423,7 @@ "~:key": "wgjr6b27pa", "~:font-size": "16", "~:font-weight": "400", - "~:typography-ref-file": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:typography-ref-file": "~uc7ce0794-0992-8105-8004-38f280443849", "~:modified-at": "~m1776759448186", "~:font-variant-id": "regular", "~:text-decoration": "none", @@ -445,7 +445,7 @@ "~:key": "1dpjnycmsmq", "~:font-size": "16", "~:font-weight": "400", - "~:typography-ref-file": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:typography-ref-file": "~uc7ce0794-0992-8105-8004-38f280443849", "~:text-direction": "ltr", "~:type": "paragraph", "~:modified-at": "~m1776759448186", @@ -1079,7 +1079,7 @@ "~:key": "wgjr6b27pa", "~:font-size": "18", "~:font-weight": "700", - "~:typography-ref-file": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:typography-ref-file": "~uc7ce0794-0992-8105-8004-38f280443849", "~:modified-at": "~m1776759420985", "~:font-variant-id": "700", "~:text-decoration": "none", @@ -1101,7 +1101,7 @@ "~:key": "1dpjnycmsmq", "~:font-size": "18", "~:font-weight": "700", - "~:typography-ref-file": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:typography-ref-file": "~uc7ce0794-0992-8105-8004-38f280443849", "~:text-direction": "ltr", "~:type": "paragraph", "~:modified-at": "~m1776759420985", @@ -1517,7 +1517,7 @@ "~:name": "Page 1" } }, - "~:id": "~u1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", "~:options": { "~:components-v2": true, "~:base-font-size": "16px" @@ -1554,6 +1554,18 @@ "~:font-family": "IM Fell French Canon SC" } }, + "~:tokens-status": { + "~#penpot/tokens-status": { + "~:active-theme-ids": { + "~#set": [] + }, + "~:active-set-ids": { + "~#set": [ + "~u12f7a4ff-ddae-80ff-8007-e70bba7e2db2" + ] + } + } + }, "~:tokens-lib": { "~#penpot/tokens-lib": { "~:sets": { diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 3e1758d491..dd5f66fb04 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -583,11 +583,14 @@ export class WorkspacePage extends BaseWebSocketPage { await expect(this.toolbarOptions).toHaveCSS("opacity", "0"); } + async clickLayers(clickOptions = {}) { + await this.sidebar.getByText("Layers").click(clickOptions); + } async clickAssets(clickOptions = {}) { await this.sidebar.getByText("Assets").click(clickOptions); } - async clickLayers(clickOptions = {}) { - await this.sidebar.getByText("Layers").click(clickOptions); + async clickTokens(clickOptions = {}) { + await this.sidebar.getByText("Tokens").click(clickOptions); } async openLibrariesModal(clickOptions = {}) { diff --git a/frontend/playwright/ui/specs/assets-tab.spec.js b/frontend/playwright/ui/specs/assets-tab.spec.js index 7b946c980e..77487c1356 100644 --- a/frontend/playwright/ui/specs/assets-tab.spec.js +++ b/frontend/playwright/ui/specs/assets-tab.spec.js @@ -3,6 +3,7 @@ import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; test.beforeEach(async ({ page }) => { await WasmWorkspacePage.init(page); + await WasmWorkspacePage.mockConfigFlags(page, ["enable-token-lib-sync"]); }); test("User adds a library and its automatically selected in the color palette", async ({ @@ -31,6 +32,7 @@ test("User adds a library and its automatically selected in the color palette", // Now the get-file call should return a library await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json"); await workspacePage.openLibrariesModal(); + await workspacePage.librariesModal.getByRole("tab", { name: "Libraries" }).click(); await workspacePage.clickLibrary("Testing library 1"); await workspacePage.closeLibrariesModal(); @@ -40,6 +42,7 @@ test("User adds a library and its automatically selected in the color palette", // Remove Testing library 1 await workspacePage.openLibrariesModal(); + await workspacePage.librariesModal.getByRole("tab", { name: "This file" }).click(); await workspacePage.clickLibrary("Testing library 1"); await workspacePage.closeLibrariesModal(); diff --git a/frontend/playwright/ui/specs/inspect-layout.spec.js b/frontend/playwright/ui/specs/inspect-layout.spec.js index 3006cebf9b..1d154d653a 100644 --- a/frontend/playwright/ui/specs/inspect-layout.spec.js +++ b/frontend/playwright/ui/specs/inspect-layout.spec.js @@ -152,6 +152,7 @@ test("[Taiga #10630] [INSPECT] Style assets not being displayed on info tab", as await workspacePage.clickAssets(); await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json"); await workspacePage.openLibrariesModal(); + await workspacePage.librariesModal.getByRole("tab", { name: "Libraries" }).click(); await workspacePage.clickLibrary("Testing library 1"); await workspacePage.closeLibrariesModal(); diff --git a/frontend/playwright/ui/specs/multiseleccion.spec.js b/frontend/playwright/ui/specs/multiseleccion.spec.js index fc9ca391db..ca0804f45f 100644 --- a/frontend/playwright/ui/specs/multiseleccion.spec.js +++ b/frontend/playwright/ui/specs/multiseleccion.spec.js @@ -210,7 +210,7 @@ test("Multiselection of text and typographies", async ({ page }) => { ); await workspacePage.goToWorkspace({ - fileId: "1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + fileId: "c7ce0794-0992-8105-8004-38f280443849", pageId: "1062e0a0-8fe0-80ae-8007-e70b4993f5f0", }); diff --git a/frontend/playwright/ui/specs/text-options-missing-font.spec.js b/frontend/playwright/ui/specs/text-options-missing-font.spec.js index dfac536d89..f8a50343a2 100644 --- a/frontend/playwright/ui/specs/text-options-missing-font.spec.js +++ b/frontend/playwright/ui/specs/text-options-missing-font.spec.js @@ -84,7 +84,7 @@ test.describe("typography asset applied", () => { // gfont-agdasima, a built-in Google font that resolves with no extra // mocking), and is not multi-selected or token-applied. const FILE = { - id: "1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + id: "c7ce0794-0992-8105-8004-38f280443849", pageId: "1062e0a0-8fe0-80ae-8007-e70b4993f5f0", }; diff --git a/frontend/playwright/ui/specs/tokens/libraries.spec.js b/frontend/playwright/ui/specs/tokens/libraries.spec.js new file mode 100644 index 0000000000..424d1be95f --- /dev/null +++ b/frontend/playwright/ui/specs/tokens/libraries.spec.js @@ -0,0 +1,163 @@ +import { test, expect } from "@playwright/test"; +import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage"; +import { + setupEmptyTokensFileRender, + createToken, + unfoldTokenType, +} from "./helpers"; + +test.beforeEach(async ({ page }) => { + await WasmWorkspacePage.init(page); +}); + +test("File with tokens is its own tokens source", async ({ page }) => { + await WasmWorkspacePage.mockConfigFlags(page, ["enable-token-lib-sync"]); + const workspacePage = new WasmWorkspacePage(page); + + await workspacePage.setupEmptyFile(page); + + await workspacePage.mockRPC( + "get-team-shared-files?team-id=*", + "workspace/get-team-shared-libraries-non-empty.json", + ); + + await workspacePage.goToWorkspace(); + + // A file without tokens is not tokens source + await workspacePage.clickAssets(); + await workspacePage.openLibrariesModal(); + await workspacePage.librariesModal + .getByRole("tab", { name: "This file" }) + .click(); + + await expect( + workspacePage.librariesModal.getByText("Tokens source"), + ).not.toBeVisible(); + + await workspacePage.closeLibrariesModal(); + + // Create a token in the file + await workspacePage.clickTokens(); + await createToken( + page, + "Color", + "color.primary", + "Value", + "textbox", + "#ff0000", + ); + + // Now it is a tokens source + await workspacePage.clickAssets(); + await workspacePage.openLibrariesModal(); + await workspacePage.librariesModal + .getByRole("tab", { name: "This file" }) + .click(); + + await expect( + workspacePage.librariesModal.getByText("Tokens source"), + ).toBeVisible(); +}); + +test("User sets a library as tokens source and then they can use the tokens in it", async ({ + page, +}) => { + await WasmWorkspacePage.mockConfigFlags(page, ["enable-token-lib-sync"]); + + // Set up a file linked to a library with tokens + + const { workspacePage, tokenThemesSetsSidebar, tokenContextMenuForSet } = + await setupEmptyTokensFileRender(page); + + await workspacePage.mockGetFile("workspace/get-file-with-lib.json"); + + await workspacePage.mockRPC( + "get-file-libraries?file-id=*", + "workspace/get-file-libraries-tokens.json", + ); + + await workspacePage.mockRPC( + /get\-file\?id=c7ce0794-0992-8105-8004-38f280443849/, + "workspace/get-file-tokens.json", + ); + + await workspacePage.mockRPC( + /get\-file\-fragment\?/, + "workspace/get-file-fragment-tokens.json", + ); + + await workspacePage.mockRPC( + "get-team-shared-files?team-id=*", + "workspace/get-team-shared-libraries-non-empty.json", + ); + + await workspacePage.goToWorkspace(); + + // Set the external library as the tokens source + + await workspacePage.clickAssets(); + await workspacePage.openLibrariesModal(); + await workspacePage.librariesModal + .getByRole("tab", { name: "This file" }) + .click(); + await workspacePage.librariesModal + .getByRole("button", { name: "Set as tokens source" }) + .click(); + await workspacePage.closeLibrariesModal(); + + // Check that we can apply a token from the library to a shape + + await workspacePage.sidebar.getByRole("tab", { name: "Layers" }).click(); + await workspacePage.layers + .getByTestId("layer-row") + .filter({ hasText: "Rectangle" }) + .click(); + + // Apply a color token from the library as the shape fill + await workspacePage.sidebar.getByRole("tab", { name: "Tokens" }).click(); + await unfoldTokenType(workspacePage.tokensSidebar, "Color"); + + const colorTokenName = "colors.red.600"; + await workspacePage.tokensSidebar + .getByRole("button", { name: colorTokenName }) + .click({ button: "right" }); + await workspacePage.tokenContextMenuForToken.getByText("Fill").click(); + + // Check that the token has been correctly applied in the right sidebar + const fillSection = workspacePage.rightSidebar.getByRole("region", { + name: "Fill section", + }); + await expect(fillSection).toBeVisible(); + + const fillTokenPill = fillSection.getByLabel(colorTokenName, { + exact: true, + }); + await expect(fillTokenPill).toBeVisible(); + + // Go back to the libraries modal and set the current file as tokens source + await workspacePage.clickAssets(); + await workspacePage.openLibrariesModal(); + await workspacePage.librariesModal + .getByRole("tab", { name: "This file" }) + .click(); + await workspacePage.librariesModal + .getByRole("button", { name: "Set as tokens source" }) + .click(); + await workspacePage.closeLibrariesModal(); + + // Check that the applied token now looks broken + await expect(fillSection).toBeVisible(); + const brokenTokenPill = fillSection.getByLabel(colorTokenName, { + exact: true, + }); + await expect(brokenTokenPill).toBeVisible(); + await brokenTokenPill.hover(); + + const brokenTokenTooltip = page.getByRole("tooltip", { + name: colorTokenName, + }); + await expect(brokenTokenTooltip).toBeVisible(); + await expect(brokenTokenTooltip).toHaveText( + `{${colorTokenName}} token does not exist or has been deleted.`, + ); +}); diff --git a/frontend/playwright/ui/specs/tokens/themes.spec.js b/frontend/playwright/ui/specs/tokens/themes.spec.js index 0685aaf959..855c0716ca 100644 --- a/frontend/playwright/ui/specs/tokens/themes.spec.js +++ b/frontend/playwright/ui/specs/tokens/themes.spec.js @@ -89,6 +89,12 @@ test.describe("Tokens Themes", () => { tokenThemeUpdateCreateModal.getByText("Changed" + "4 active sets"), ).toBeVisible(); + await tokenThemeUpdateCreateModal + .getByRole("switch", { + name: "Changed", + }) + .click(); + await tokenThemeUpdateCreateModal .getByRole("button") .getByText("close") diff --git a/frontend/playwright/ui/specs/workspace-shared-library.spec.js b/frontend/playwright/ui/specs/workspace-shared-library.spec.js index 9080be9a09..b9bab493f1 100644 --- a/frontend/playwright/ui/specs/workspace-shared-library.spec.js +++ b/frontend/playwright/ui/specs/workspace-shared-library.spec.js @@ -143,6 +143,10 @@ test("BUG 14214 - Updates tab refreshes after syncing a freshly linked library", // Open the library modal await workspace.clickAssets(); await workspace.openLibrariesModal(); + + // Switch to the Libraries tab + await workspace.librariesModal.getByRole("tab", { name: "Libraries" }).click(); + await workspace.librariesModal .getByRole("button", { name: "Connect library" }) .click(); diff --git a/frontend/playwright/ui/specs/workspace-texts-typography-persist.spec.js b/frontend/playwright/ui/specs/workspace-texts-typography-persist.spec.js index d9b6c98c02..518fa9c8d4 100644 --- a/frontend/playwright/ui/specs/workspace-texts-typography-persist.spec.js +++ b/frontend/playwright/ui/specs/workspace-texts-typography-persist.spec.js @@ -25,7 +25,7 @@ import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; // --------------------------------------------------------------------------- const FILE_A = { - id: "1062e0a0-8fe0-80ae-8007-e70b4993f5ef", + id: "c7ce0794-0992-8105-8004-38f280443849", pageId: "1062e0a0-8fe0-80ae-8007-e70b4993f5f0", // "Text with typography asset one" carries a ref to in-file typography whose // font-family is "IM Fell French Canon SC" (multiselection-typography.json). diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js index 702b3b3aef..ee3400079b 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -315,6 +315,7 @@ test("User adds a library and its automatically selected in the color palette", // Now the get-file call should return a library await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json"); await workspacePage.openLibrariesModal(); + await workspacePage.librariesModal.getByRole("tab", { name: "Libraries" }).click(); await workspacePage.clickLibrary("Testing library 1"); await workspacePage.closeLibrariesModal(); diff --git a/frontend/src/app/main/data/helpers.cljs b/frontend/src/app/main/data/helpers.cljs index a0f46e30e1..f228d6afcf 100644 --- a/frontend/src/app/main/data/helpers.cljs +++ b/frontend/src/app/main/data/helpers.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] + [app.common.files.tokens :as cfo] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -37,6 +38,23 @@ ([state file-id] (dm/get-in state [:files file-id :data]))) +;; TODOstatus perhaps this is not necessary, lookup-tokens-lib should be enough +(defn lookup-tokens-source-data + [state] + (let [current-file-data (lookup-file-data state) + tokens-source-id (cfo/get-effective-tokens-source current-file-data)] + (lookup-file-data state tokens-source-id))) + +(defn lookup-tokens-lib + [state] + (let [tokens-source-data (lookup-tokens-source-data state)] + (cfo/get-tokens-lib tokens-source-data))) + +(defn lookup-tokens-status + [state] + (let [current-file-data (lookup-file-data state)] ;; Tokens status is always in the current file + (cfo/get-tokens-status current-file-data))) + (defn get-page [fdata page-id] (dm/get-in fdata [:pages-index page-id])) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 60baa7e15d..da2b59b341 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -16,6 +16,7 @@ [app.common.logging :as log] [app.common.logic.libraries :as cll] [app.common.logic.shapes :as cls] + [app.common.logic.tokens :as clo] [app.common.logic.variants :as clv] [app.common.path-names :as cpn] [app.common.time :as ct] @@ -46,6 +47,7 @@ [app.main.data.workspace.specialized-panel :as dwsp] [app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.thumbnails-wasm :as dwt.wasm] + [app.main.data.workspace.tokens.propagation :as dwtp] [app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.wasm-text :as dwwt] @@ -654,6 +656,7 @@ (merge (meta it)))) (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) + (dwtp/propagate-workspace-tokens) ;; Make the new instance get the token values from the current file, not from the component's library (ptk/data-event :layout/update {:ids [(:id new-shape)]}) (dws/select-shapes (d/ordered-set (:id new-shape))) (when start-move? @@ -837,7 +840,11 @@ (rx/merge (->> (rx/of library-id) (rx/delay 5000) - (rx/map fetch-library-thumbnails))) + (rx/map fetch-library-thumbnails)) + (when (ch/tokens-lib-changed? changes) + (rx/of (dwtp/propagate-workspace-tokens))) + (when (ch/notifiable-token-change-occured? changes) + (rx/of (ntf/info (tr "workspace.tokens.notifications.source-sets-or-themes-updated"))))) (rx/take-until stopper-s)))))) @@ -1096,6 +1103,7 @@ (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) + (dwtp/propagate-workspace-tokens) ;; Make the new instance get the token values from the current file, not from the component's library (when (and (features/active-feature? state "render-wasm/v1") (seq new-text-ids)) (dwwt/resize-wasm-text-all new-text-ids)) @@ -1345,6 +1353,9 @@ #(do (apply st/emit! (map (fn [library] (sync-file file-id (:id library))) libraries-with-changes)) + ;; Launch a local tokens propagation, so that the copies have the token values + ;; with the local tokens status, not the one coming from the external library. + (st/emit! (dwtp/propagate-workspace-tokens)) (st/emit! (ntf/hide))) do-dismiss @@ -1397,8 +1408,8 @@ (launch-component-sync component-id file-id undo-group))))) (defn watch-component-changes - "Watch the state for changes that affect to any main instance. If a change is detected will throw - an update-component-sync, so changes are immediately propagated to the component and copies." + "Watch the state for changes that affect to any main instance. If a change is detected will call + component-changed, so changes are immediately propagated to the component and copies." [] (ptk/reify ::watch-component-changes ptk/WatchEvent @@ -1530,6 +1541,47 @@ (rx/take-until stopper-s))))))) +(defn sync-tokens-status-with-lib + [] + (ptk/reify ::sync-tokens-status-with-lib + ptk/WatchEvent + (watch [_ state _] + (let [tokens-lib (dsh/lookup-tokens-lib state) + tokens-status (dsh/lookup-tokens-status state)] + (when (and tokens-lib tokens-status) + (let [data (dsh/lookup-file-data state) + changes (-> (pcb/empty-changes) + (pcb/with-library-data data) + (clo/generate-sync-tokens-status-with-lib tokens-status tokens-lib))] + (rx/of (dch/commit-changes changes)))))))) + +(defn watch-token-changes + "Watch the state for changes that affect the tokens library. If a change is detected, + launches a sync-tokens-status event so the tokens-status is kept in sync with the library." + [] + (ptk/reify ::watch-token-changes + ptk/WatchEvent + (watch [_ _ stream] + (let [stopper-s + (->> stream + (rx/map ptk/type) + (rx/filter (fn [event-type] + (or (= ::dwpg/finalize-page event-type) + (= ::watch-token-changes event-type))))) + + changes-s + (->> stream + (rx/filter dch/commit?) + (rx/map deref) + (rx/filter #(= :local (:source %))) + (rx/observe-on :async))] + + (->> changes-s + (rx/filter (comp ch/tokens-lib-changed? :changes)) + (rx/debounce 5000) + (rx/map (fn [_] (sync-tokens-status-with-lib))) + (rx/take-until stopper-s)))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Backend interactions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1575,6 +1627,7 @@ (map #(assoc % :library-of file-id)) (d/index-by :id)))))) + (defn- load-library-file [file-id library-id] (ptk/reify ::load-library-file @@ -1584,8 +1637,9 @@ (rx/merge (->> (rp/cmd! :get-file {:id library-id :features features}) (rx/merge-map fpmap/resolve-file) - (rx/map (fn [file] - (libraries-fetched file-id [file])))) + (rx/mapcat (fn [file] + (rx/of + (libraries-fetched file-id [file]))))) (->> (rp/cmd! :get-file-object-thumbnails {:file-id library-id :tag "component"}) (rx/map (fn [thumbnails] (fn [state] @@ -1594,10 +1648,10 @@ (defn link-file-to-library [file-id library-id] - (ptk/reify ::attach-library + (ptk/reify ::link-file-to-library ev/Event (-data [_] - {::ev/name "attach-library" + {::ev/name "link-file-to-library" :file-id file-id :library-id library-id}) @@ -1614,23 +1668,22 @@ (map first) set)] (rx/concat - (rx/merge - (->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id}) - (rx/merge-map (fn [libraries-to-load] - (as-> libraries-to-load $ - (remove loaded-libraries $) - (conj $ library-id) - (map #(load-library-file file-id %) $)))) - (rx/catch (fn [cause] - (let [error (ex-data cause)] - (if (= (:code error) :circular-library-reference) - (rx/of (ntf/error (tr "errors.circular-library-reference"))) - (rx/throw cause))))))) - (rx/of (ptk/reify ::attach-library-finished)) + (->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id}) + (rx/merge-map (fn [libraries-to-load] + (as-> libraries-to-load $ + (remove loaded-libraries $) + (conj $ library-id) + (map #(load-library-file file-id %) $)))) + (rx/catch (fn [cause] + (let [error (ex-data cause)] + (if (= (:code error) :circular-library-reference) + (rx/of (ntf/error (tr "errors.circular-library-reference"))) + (rx/throw cause)))))) + (rx/of (ptk/reify ::link-file-to-library-finished)) (when (pos? variants-count) (->> (rp/cmd! :get-library-usage {:file-id library-id}) (rx/map (fn [library-usage] - (ev/event {::ev/name "attach-library-variants" + (ev/event {::ev/name "link-file-to-library-variants" :file-id file-id :library-id library-id :variants-count variants-count diff --git a/frontend/src/app/main/data/workspace/pages.cljs b/frontend/src/app/main/data/workspace/pages.cljs index 44c3ccab94..c8b8180c41 100644 --- a/frontend/src/app/main/data/workspace/pages.cljs +++ b/frontend/src/app/main/data/workspace/pages.cljs @@ -114,6 +114,7 @@ (rx/of (dwth/watch-state-changes file-id page-id))) (rx/of (dwl/watch-component-changes)) + (rx/of (dwl/watch-token-changes)) (let [profile (:profile state) props (get profile :props)] diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 6877610caf..4ddaf0eded 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -22,12 +22,10 @@ [app.common.types.text :as txt] [app.common.uuid :as uuid] [app.main.data.changes :as dch] - [app.main.data.event :as ev] [app.main.data.helpers :as dsh] [app.main.data.notifications :as ntf] [app.main.data.workspace :as-alias dw] [app.main.data.workspace.common :as dwc] - [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.modifiers :as dwm] [app.main.data.workspace.pages :as-alias dwpg] [app.main.data.workspace.reflow :as wrf] @@ -1171,66 +1169,6 @@ (let [{:keys [name]} (fonts/get-font-data font-id)] (assoc typography :name (str name " " (str/title font-variant-id))))) -(defn add-typography - "A higher level version of dwl/add-typography, and has mainly two - responsabilities: add the typography to the library and apply it to - the currently selected text shapes (being aware of the open text - editors. - Optionally accepts a group-path to place the new typography inside - a specific group." - ([file-id] (add-typography file-id nil)) - ([file-id group-path] - (ptk/reify ::add-typography - ptk/WatchEvent - (watch [_ state _] - (let [selected (dsh/lookup-selected state) - objects (dsh/lookup-page-objects state) - - xform (comp (keep (d/getf objects)) - (filter cfh/text-shape?)) - shapes (into [] xform selected) - shape (first shapes) - - values (current-text-values - {:editor-state (dm/get-in state [:workspace-editor-state (:id shape)]) - :shape shape - :attrs txt/text-node-attrs}) - - multiple? (or (> 1 (count shapes)) - (d/seek (partial = :multiple) - (vals values))) - - values (-> (d/without-nils values) - (select-keys - (d/concat-vec txt/text-font-attrs - txt/text-spacing-attrs - txt/text-transform-attrs))) - values (cond-> values - (number? (:line-height values)) - (update :line-height #(str (mth/precision % 2))) - - (number? (:letter-spacing values)) - (update :letter-spacing #(str (mth/precision % 2)))) - - typ-id (uuid/next) - typ (-> (if multiple? - txt/default-typography - (merge txt/default-typography values)) - (generate-typography-name) - (assoc :id typ-id) - (cond-> (string? group-path) - (update :name #(str group-path " / " %))))] - - (rx/concat - (rx/of (dwl/add-typography typ) - (ev/event {::ev/name "add-asset-to-library" - :asset-type "typography"})) - - (when (not multiple?) - (rx/of (update-attrs (:id shape) - {:typography-ref-id typ-id - :typography-ref-file file-id}))))))))) - ;; -- Text Editor v2 (defn v2-update-text-editor-styles diff --git a/frontend/src/app/main/data/workspace/texts_events.cljs b/frontend/src/app/main/data/workspace/texts_events.cljs new file mode 100644 index 0000000000..4dc4591cf9 --- /dev/null +++ b/frontend/src/app/main/data/workspace/texts_events.cljs @@ -0,0 +1,85 @@ +;; 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 SUBSIDIARY SL + +(ns app.main.data.workspace.texts-events + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.helpers :as cfh] + [app.common.math :as mth] + [app.common.types.text :as txt] + [app.common.uuid :as uuid] + [app.main.data.event :as ev] + [app.main.data.helpers :as dsh] + [app.main.data.workspace :as-alias dw] + [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.pages :as-alias dwpg] + [app.main.data.workspace.texts :as dwt] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) + +;; This function must be separated from app.main.data.workspace.texts to avoid a circular +;; dependency due main.data.workspace.libraries eventually calling app.main.data.workspace.texts. + +(defn add-typography + "A higher level version of dwl/add-typography, and has mainly two + responsabilities: add the typography to the library and apply it to + the currently selected text shapes (being aware of the open text + editors. + Optionally accepts a group-path to place the new typography inside + a specific group." + ([file-id] (add-typography file-id nil)) + ([file-id group-path] + (ptk/reify ::add-typography + ptk/WatchEvent + (watch [_ state _] + (let [selected (dsh/lookup-selected state) + objects (dsh/lookup-page-objects state) + + xform (comp (keep (d/getf objects)) + (filter cfh/text-shape?)) + shapes (into [] xform selected) + shape (first shapes) + + values (dwt/current-text-values + {:editor-state (dm/get-in state [:workspace-editor-state (:id shape)]) + :shape shape + :attrs txt/text-node-attrs}) + + multiple? (or (> 1 (count shapes)) + (d/seek (partial = :multiple) + (vals values))) + + values (-> (d/without-nils values) + (select-keys + (d/concat-vec txt/text-font-attrs + txt/text-spacing-attrs + txt/text-transform-attrs))) + values (cond-> values + (number? (:line-height values)) + (update :line-height #(str (mth/precision % 2))) + + (number? (:letter-spacing values)) + (update :letter-spacing #(str (mth/precision % 2)))) + + typ-id (uuid/next) + typ (-> (if multiple? + txt/default-typography + (merge txt/default-typography values)) + (dwt/generate-typography-name) + (assoc :id typ-id) + (cond-> (string? group-path) + (update :name #(str group-path " / " %))))] + + (rx/concat + (rx/of (dwl/add-typography typ) + (ev/event {::ev/name "add-asset-to-library" + :asset-type "typography"})) + + (when (not multiple?) + (rx/of (dwt/update-attrs (:id shape) + {:typography-ref-id typ-id + :typography-ref-file file-id}))))))))) diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index dd96ebf89c..b2d79480a4 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -25,7 +25,6 @@ [app.main.data.style-dictionary :as sd] [app.main.data.tinycolor :as tinycolor] [app.main.data.tokenscript :as ts] - [app.main.data.workspace :as udw] [app.main.data.workspace.colors :as wdc] [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shapes :as dwsh] @@ -95,11 +94,11 @@ (watch [_ _ _] (when (number? value) (rx/of - (udw/trigger-bounding-box-cloaking shape-ids) - (udw/increase-rotation shape-ids value nil - {:page-id page-id - :ignore-touched true - :no-wasm? true}))))))) + (dwtr/trigger-bounding-box-cloaking shape-ids) + (dwtr/increase-rotation shape-ids value nil + {:page-id page-id + :ignore-touched true + :no-wasm? true}))))))) (defn update-stroke-width ([value shape-ids attributes] (update-stroke-width value shape-ids attributes nil)) @@ -675,9 +674,10 @@ (ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys) (ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys) :else attributes-to-remove)] - (when-let [tokens (some-> (dsh/lookup-file-data state) - (get :tokens-lib) - (ctob/get-tokens-in-active-sets))] + (when-let [tokens (let [tokens-lib (dsh/lookup-tokens-lib state) + tokens-status (dsh/lookup-tokens-status state)] + (when (and tokens-lib tokens-status) + (cfo/get-tokens-in-active-sets tokens-status tokens-lib)))] (->> (if (contains? cf/flags :tokenscript) (rx/of (ts/resolve-tokens tokens)) (sd/resolve-tokens tokens)) diff --git a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs index b5b39a467e..da0bdf11e2 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -9,12 +9,14 @@ [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] + [app.common.files.tokens :as cfo] [app.common.geom.point :as gpt] - [app.common.logic.tokens :as clt] + [app.common.logic.tokens :as clo] [app.common.path-names :as cpn] [app.common.test-helpers.ids-map :as cthi] [app.common.types.shape :as cts] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [app.main.data.changes :as dch] [app.main.data.event :as ev] @@ -34,19 +36,12 @@ ;; TOKENS Getters ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; FIXME: lookup rename - -(defn get-tokens-lib - [state] - (-> (dsh/lookup-file-data state) - (get :tokens-lib))) - (defn lookup-token-set ([state] (when-let [selected (dm/get-in state [:workspace-tokens :selected-token-set-id])] (lookup-token-set state selected))) ([state id] - (some-> (get-tokens-lib state) + (some-> (dsh/lookup-tokens-lib state) (ctob/get-set id)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -231,8 +226,7 @@ ptk/WatchEvent (watch [it state _] (let [data (dsh/lookup-file-data state) - tokens-lib (get data :tokens-lib)] - + tokens-lib (dsh/lookup-tokens-lib state)] (if (and tokens-lib (ctob/get-theme tokens-lib (ctob/get-id token-theme))) (rx/of (ntf/show {:content (tr "errors.token-theme-already-exists") :type :toast @@ -249,8 +243,8 @@ (ptk/reify ::update-token-theme ptk/WatchEvent (watch [it state _] - (let [data (dsh/lookup-file-data state) - tokens-lib (get data :tokens-lib)] + (let [data (dsh/lookup-file-data state) + tokens-lib (dsh/lookup-tokens-lib state)] (if (and (not= id (ctob/get-id token-theme)) (ctob/get-theme tokens-lib (ctob/get-id token-theme))) (rx/of (ntf/show {:content (tr "errors.token-theme-already-exists") @@ -259,7 +253,7 @@ :timeout 9000})) (let [changes (-> (pcb/empty-changes it) (pcb/with-library-data data) - (pcb/set-token-theme (ctob/get-id token-theme) token-theme))] + (clo/generate-update-token-theme token-theme))] (rx/of (dch/commit-changes changes)))))))) (defn set-token-theme-active @@ -269,31 +263,35 @@ (ptk/reify ::set-token-theme-active ptk/WatchEvent (watch [_ state _] - (let [data (dsh/lookup-file-data state) - tokens-lib (get-tokens-lib state) - changes (-> (pcb/empty-changes) - (pcb/with-library-data data) - (clt/generate-set-active-token-theme tokens-lib id active?))] + (let [data (dsh/lookup-file-data state) + tokens-status (dsh/lookup-tokens-status state) + tokens-lib (dsh/lookup-tokens-lib state) + changes (-> (pcb/empty-changes) + (pcb/with-library-data data) + (clo/generate-set-theme-status tokens-status tokens-lib id active?))] (rx/of (dch/commit-changes changes) (dwtp/propagate-workspace-tokens)))))) (defn toggle-token-theme-active [id] + (assert (uuid? id) "expected a uuid for `id`") (ptk/reify ::toggle-token-theme-active ptk/WatchEvent (watch [it state _] - (let [data (dsh/lookup-file-data state) - tokens-lib (get-tokens-lib state) - changes (-> (pcb/empty-changes it) - (pcb/with-library-data data) - (clt/generate-toggle-token-theme tokens-lib id))] + (let [data (dsh/lookup-tokens-source-data state) + tokens-status (dsh/lookup-tokens-status state) + tokens-lib (dsh/lookup-tokens-lib state) + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (clo/generate-toggle-theme tokens-status tokens-lib id))] (rx/of (dch/commit-changes changes) (dwtp/propagate-workspace-tokens)))))) (defn delete-token-theme [id] + (assert (uuid? id) "expected a uuid for `id`") (ptk/reify ::delete-token-theme ptk/WatchEvent (watch [it state _] @@ -350,10 +348,11 @@ ptk/WatchEvent (watch [it state _] (let [data (dsh/lookup-file-data state) - tokens-lib (get data :tokens-lib) + tokens-lib (dsh/lookup-tokens-lib state) suffix (tr "workspace.tokens.duplicate-suffix")] - (when-let [token-set (ctob/duplicate-set id tokens-lib {:suffix suffix})] + (when-let [token-set (when tokens-lib + (ctob/duplicate-set id tokens-lib {:suffix suffix}))] (when id-ref (reset! id-ref (ctob/get-id token-set))) (let [changes (-> (pcb/empty-changes it) (pcb/with-library-data data) @@ -362,32 +361,34 @@ (dch/commit-changes changes))))))))) (defn set-enabled-token-set - [name enabled?] - (assert (string? name) "expected a string for `name`") + [id enabled?] + (assert (uuid? id) "expected a uuid for `id`") (assert (boolean? enabled?) "expected a boolean for `enabled?`") (ptk/reify ::set-enabled-token-set ptk/WatchEvent (watch [_ state _] - (let [data (dsh/lookup-file-data state) - tlib (get-tokens-lib state) - changes (-> (pcb/empty-changes) - (pcb/with-library-data data) - (clt/generate-set-enabled-token-set tlib name enabled?))] + (let [data (dsh/lookup-file-data state) + tokens-lib (dsh/lookup-tokens-lib state) + tokens-status (dsh/lookup-tokens-status state) + changes (-> (pcb/empty-changes) + (pcb/with-library-data data) + (clo/generate-set-enabled-token-set tokens-status tokens-lib id enabled?))] (rx/of (dch/commit-changes changes) (dwtp/propagate-workspace-tokens)))))) (defn toggle-token-set - [name] - (assert (string? name) "expected a string for `name`") + [id] + (assert (uuid? id) "expected a uuid for `id`") (ptk/reify ::toggle-token-set ptk/WatchEvent (watch [_ state _] - (let [data (dsh/lookup-file-data state) - tlib (get-tokens-lib state) - changes (-> (pcb/empty-changes) - (pcb/with-library-data data) - (clt/generate-toggle-token-set tlib name))] + (let [data (dsh/lookup-file-data state) + tokens-lib (dsh/lookup-tokens-lib state) + tokens-status (dsh/lookup-tokens-status state) + changes (-> (pcb/empty-changes) + (pcb/with-library-data data) + (clo/generate-toggle-token-set tokens-status tokens-lib id))] (rx/of (dch/commit-changes changes) @@ -398,10 +399,12 @@ (ptk/reify ::toggle-token-set-group ptk/WatchEvent (watch [_ state _] - (let [data (dsh/lookup-file-data state) - changes (-> (pcb/empty-changes) - (pcb/with-library-data data) - (clt/generate-toggle-token-set-group (get-tokens-lib state) group-path))] + (let [data (dsh/lookup-file-data state) + tokens-lib (dsh/lookup-tokens-lib state) + tokens-status (dsh/lookup-tokens-status state) + changes (-> (pcb/empty-changes) + (pcb/with-library-data data) + (clo/generate-toggle-token-set-group tokens-status tokens-lib group-path))] (rx/of (dch/commit-changes changes) @@ -413,12 +416,27 @@ ptk/WatchEvent (watch [it state _] (let [data (dsh/lookup-file-data state) + status (cfo/make-tokens-status-from-lib lib) changes (-> (pcb/empty-changes it) (pcb/with-library-data data) - (pcb/set-tokens-lib lib))] + (pcb/set-tokens-lib lib) + (pcb/set-tokens-status status))] (rx/of (dch/commit-changes changes) (dwtp/propagate-workspace-tokens)))))) +(defn set-tokens-source + [library-id] + (ptk/reify ::set-tokens-source + ptk/WatchEvent + (watch [it state _] + (let [data (dsh/lookup-file-data state) + library (dsh/lookup-file state library-id) + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (clo/generate-set-tokens-source library) + (pcb/set-tokens-source library-id))] + (rx/of (dch/commit-changes changes)))))) + (defn delete-token-set [id] (ptk/reify ::delete-token-set @@ -439,7 +457,7 @@ (let [data (dsh/lookup-file-data state) changes (-> (pcb/empty-changes it) (pcb/with-library-data data) - (clt/generate-delete-token-set-group (get-tokens-lib state) path))] + (clo/generate-delete-token-set-group (dsh/lookup-tokens-lib state) path))] (rx/of (dch/commit-changes changes) (dwtp/propagate-workspace-tokens)))))) @@ -467,7 +485,7 @@ ptk/WatchEvent (watch [it state _] (try - (when-let [changes (clt/generate-move-token-set-group (pcb/empty-changes it) (get-tokens-lib state) drop-opts)] + (when-let [changes (clo/generate-move-token-set-group (pcb/empty-changes it) (dsh/lookup-tokens-lib state) drop-opts)] (rx/of (dch/commit-changes changes) (dwtp/propagate-workspace-tokens))) @@ -483,9 +501,9 @@ ptk/WatchEvent (watch [it state _] (try - (let [tokens-lib (get-tokens-lib state) + (let [tokens-lib (dsh/lookup-tokens-lib state) changes (-> (pcb/empty-changes it) - (clt/generate-move-token-set tokens-lib params))] + (clo/generate-move-token-set tokens-lib params))] (rx/of (dch/commit-changes changes) (dwtp/propagate-workspace-tokens))) (catch :default cause @@ -506,11 +524,13 @@ token-set (ctob/make-token-set :name set-name) - hidden-theme - (ctob/make-hidden-theme) + hidden-theme ;; For legacy compatibility only + (-> (ctob/make-hidden-theme) + (ctob/enable-set set-name)) - hidden-theme-with-set - (ctob/enable-set hidden-theme set-name) + token-status + (ctos/make-tokens-status :active-theme-ids #{} + :active-set-ids #{(ctob/get-id token-set)}) changes (-> (pcb/empty-changes) @@ -518,8 +538,9 @@ (pcb/set-token-set (ctob/get-id token-set) token-set) (pcb/set-token (ctob/get-id token-set) (:id token) token) (pcb/set-token-theme (ctob/get-id hidden-theme) - hidden-theme-with-set) - (pcb/set-active-token-themes #{ctob/hidden-theme-path}))] + hidden-theme) + (pcb/set-tokens-status token-status))] + (rx/of (dch/commit-changes changes) (set-selected-token-set-id (ctob/get-id token-set))))))) @@ -559,7 +580,7 @@ (let [token-set (lookup-token-set state set-id) data (dsh/lookup-file-data state) changes (reduce (fn [changes token-id] - (let [token (-> (get-tokens-lib state) + (let [token (-> (dsh/lookup-tokens-lib state) (ctob/get-token (ctob/get-id token-set) token-id)) new-name (-> (cpn/split-path (:name token) :separator ".") @@ -589,21 +610,22 @@ (lookup-token-set state set-id) (lookup-token-set state)) data (dsh/lookup-file-data state) - token (-> (get-tokens-lib state) - (ctob/get-token (ctob/get-id token-set) id)) - token' (->> (merge token params) - (into {}) - (ctob/make-token)) - token-type (:type token) - changes (-> (pcb/empty-changes it) - (pcb/with-library-data data) - (pcb/set-token (ctob/get-id token-set) - id - token'))] - (toggle-token-path (str (name token-type) "." (:name token))) - (rx/of (dch/commit-changes changes) - (ev/event (-> {::ev/name "edit-token" :type token-type} - (merge (meta it)))))))))) + tokens-lib (dsh/lookup-tokens-lib state)] + (when (and tokens-lib token-set) + (let [token (ctob/get-token tokens-lib (ctob/get-id token-set) id) + token' (->> (merge token params) + (into {}) + (ctob/make-token)) + token-type (:type token) + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/set-token (ctob/get-id token-set) + id + token'))] + (toggle-token-path (str (name token-type) "." (:name token))) + (rx/of (dch/commit-changes changes) + (ev/event (-> {::ev/name "edit-token" :type token-type} + (merge (meta it)))))))))))) (defn bulk-update-tokens [set-id token-ids type old-path new-path & {:keys [undo-group]}] @@ -617,7 +639,7 @@ (lookup-token-set state)) data (dsh/lookup-file-data state) changes (reduce (fn [changes token-id] - (let [token (-> (get-tokens-lib state) + (let [token (-> (dsh/lookup-tokens-lib state) (ctob/get-token (ctob/get-id token-set) token-id)) new-name (str/replace (:name token) old-path new-path) token' (->> (merge token {:name new-name}) @@ -643,19 +665,20 @@ ptk/WatchEvent (watch [it state _] (let [data (dsh/lookup-file-data state) + tokens-lib (dsh/lookup-tokens-lib state) token-set (if set-id (lookup-token-set state set-id) - (lookup-token-set state)) - token (-> (get-tokens-lib state) - (ctob/get-token (ctob/get-id token-set) token-id)) - token-type (:type token) + (lookup-token-set state))] + (when (and tokens-lib token-set) + (let [token (ctob/get-token tokens-lib (ctob/get-id token-set) token-id) + token-type (:type token) - changes (-> (pcb/empty-changes it) - (pcb/with-library-data data) - (pcb/set-token set-id token-id nil))] - (rx/of (dch/commit-changes changes) - (ev/event (-> {::ev/name "delete-token" :type token-type} - (merge (meta it))))))))) + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/set-token set-id token-id nil))] + (rx/of (dch/commit-changes changes) + (ev/event (-> {::ev/name "delete-token" :type token-type} + (merge (meta it))))))))))) (defn bulk-delete-tokens [set-id token-ids] @@ -680,7 +703,7 @@ ptk/WatchEvent (watch [_ state _] (when-let [token-set (lookup-token-set state)] - (when-let [tokens-lib (get-tokens-lib state)] + (when-let [tokens-lib (dsh/lookup-tokens-lib state)] (when-let [token (ctob/get-token tokens-lib (ctob/get-id token-set) token-id)] diff --git a/frontend/src/app/main/data/workspace/tokens/propagation.cljs b/frontend/src/app/main/data/workspace/tokens/propagation.cljs index 9440b32c81..aa8a4b3733 100644 --- a/frontend/src/app/main/data/workspace/tokens/propagation.cljs +++ b/frontend/src/app/main/data/workspace/tokens/propagation.cljs @@ -8,10 +8,10 @@ (:require [app.common.data :as d] [app.common.files.helpers :as cfh] + [app.common.files.tokens :as cfo] [app.common.logging :as l] [app.common.time :as ct] [app.common.types.token :as ctt] - [app.common.types.tokens-lib :as ctob] [app.config :as cf] [app.main.data.helpers :as dsh] [app.main.data.style-dictionary :as sd] @@ -188,27 +188,28 @@ (ptk/reify ::propagate-workspace-tokens ptk/WatchEvent (watch [_ state _] - (when-let [tokens-tree (-> (dsh/lookup-file-data state) - (get :tokens-lib) - (ctob/get-tokens-in-active-sets))] - (->> (if (contains? cf/flags :tokenscript) - (rx/of (-> (ts/resolve-tokens tokens-tree) - (d/update-vals #(update % :resolved-value ts/tokenscript-symbols->penpot-unit)))) - (sd/resolve-tokens tokens-tree)) - (rx/mapcat - (fn [sd-tokens] - (let [undo-id (js/Symbol)] - (rx/concat - (rx/of (dwu/start-undo-transaction undo-id :timeout false)) + (let [tokens-lib (dsh/lookup-tokens-lib state) + tokens-status (dsh/lookup-tokens-status state)] + (when (and tokens-lib tokens-status) + (when-let [tokens-tree (cfo/get-tokens-in-active-sets tokens-status tokens-lib)] + (->> (if (contains? cf/flags :tokenscript) + (rx/of (-> (ts/resolve-tokens tokens-tree) + (d/update-vals #(update % :resolved-value ts/tokenscript-symbols->penpot-unit)))) + (sd/resolve-tokens tokens-tree)) + (rx/mapcat + (fn [sd-tokens] + (let [undo-id (js/Symbol)] + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id :timeout false)) - ;; FIXME: now the tokens propagations is done by accumulating the update-shapes - ;; into a single commit-changes. This is not really the best way, the token application - ;; should be done with a changes_builder and sending only one `commit-changes` instead - ;; of creating lots of `update-shapes`. - (rx/of (dwsh/update-shapes-buffer-start)) - (->> (propagate-tokens state sd-tokens) - (rx/catch #(rx/concat - (rx/of (dwsh/update-shapes-buffer-stop)) - (rx/throw %)))) - (rx/of (dwsh/update-shapes-buffer-stop)) - (rx/of (dwu/commit-undo-transaction undo-id))))))))))) + ;; FIXME: now the tokens propagations is done by accumulating the update-shapes + ;; into a single commit-changes. This is not really the best way, the token application + ;; should be done with a changes_builder and sending only one `commit-changes` instead + ;; of creating lots of `update-shapes`. + (rx/of (dwsh/update-shapes-buffer-start)) + (->> (propagate-tokens state sd-tokens) + (rx/catch #(rx/concat + (rx/of (dwsh/update-shapes-buffer-stop)) + (rx/throw %)))) + (rx/of (dwsh/update-shapes-buffer-stop)) + (rx/of (dwu/commit-undo-transaction undo-id))))))))))))) diff --git a/frontend/src/app/main/data/workspace/tokens/selected_set.cljs b/frontend/src/app/main/data/workspace/tokens/selected_set.cljs index 29036cd8cb..0689516d2f 100644 --- a/frontend/src/app/main/data/workspace/tokens/selected_set.cljs +++ b/frontend/src/app/main/data/workspace/tokens/selected_set.cljs @@ -13,26 +13,22 @@ (defn- get-selected-token-set-id [state] (or (get-in state [:workspace-tokens :selected-token-set-id]) - (some-> (dsh/lookup-file-data state) - (get :tokens-lib) + (some-> (dsh/lookup-tokens-lib state) (ctob/get-sets) (first) (ctob/get-id)))) (defn get-selected-token-set [state] (when-let [set-id (get-selected-token-set-id state)] - (some-> (dsh/lookup-file-data state) - (get :tokens-lib) + (some-> (dsh/lookup-tokens-lib state) (ctob/get-set set-id)))) (defn get-token-in-selected-set [state token-id] (when-let [set-id (get-selected-token-set-id state)] - (some-> (dsh/lookup-file-data state) - (get :tokens-lib) + (some-> (dsh/lookup-tokens-lib state) (ctob/get-token set-id token-id)))) (defn get-all-tokens-in-selected-set [state] (when-let [set-id (get-selected-token-set-id state)] - (some-> (dsh/lookup-file-data state) - (get :tokens-lib) + (some-> (dsh/lookup-tokens-lib state) (ctob/get-tokens set-id)))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 6bdc399d82..c6158548e5 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -10,9 +10,11 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cph] + [app.common.files.tokens :as cfo] [app.common.types.shape-tree :as ctt] [app.common.types.shape.layout :as ctl] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.config :as cf] [app.main.data.helpers :as dsh] [app.main.data.workspace.tokens.selected-set :as dwts] @@ -465,7 +467,10 @@ ;; ---- Token refs (def tokens-lib - (l/derived :tokens-lib workspace-data)) + (l/derived dsh/lookup-tokens-lib st/state)) + +(def tokens-status + (l/derived dsh/lookup-tokens-status st/state)) (def workspace-token-theme-groups (l/derived (d/nilf ctob/get-theme-groups) tokens-lib)) @@ -502,26 +507,28 @@ (def workspace-token-sets-tree (l/derived (d/nilf ctob/get-set-tree) tokens-lib)) -(def workspace-active-theme-paths - (l/derived (d/nilf ctob/get-active-theme-paths) tokens-lib)) +(def workspace-active-theme-ids + (l/derived (d/nilf ctos/get-active-theme-ids) tokens-status)) (def workspace-all-tokens-map (l/derived (d/nilf ctob/get-all-tokens-map) tokens-lib)) +;; TODOstatus ver una forma eficiente de hacer un derived de lib y status a la vez (defn token-sets-at-path-all-active [group-path] (l/derived - (fn [lib] - (when lib - (ctob/sets-at-path-all-active? lib group-path))) - tokens-lib)) - -(def workspace-active-theme-paths-no-hidden - (l/derived #(disj % ctob/hidden-theme-path) workspace-active-theme-paths)) + (fn [status] + (when status + (cfo/sets-at-path-all-active? status @(l/derived identity tokens-lib) group-path))) + tokens-status)) ;; FIXME: deprecated, it should not be implemented with ref (still used in form) (def workspace-active-theme-sets-tokens - (l/derived #(or (some-> % ctob/get-tokens-in-active-sets) {}) tokens-lib)) + (l/derived (fn [[status lib]] + (if (and status lib) + (cfo/get-tokens-in-active-sets status lib) + {})) + [tokens-status tokens-lib])) (def workspace-token-in-selected-set (fn [token-id] diff --git a/frontend/src/app/main/ui/components/title_bar.scss b/frontend/src/app/main/ui/components/title_bar.scss index 198ca4b657..78fd3b915f 100644 --- a/frontend/src/app/main/ui/components/title_bar.scss +++ b/frontend/src/app/main/ui/components/title_bar.scss @@ -57,6 +57,7 @@ .icon { color: var(--arrow-icon-color); + flex-shrink: 0; } .title-wrapper { diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index 3e1312c417..9ec62358d7 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -25,6 +25,8 @@ (def libraries (mf/create-context nil)) (def design-tokens (mf/create-context nil)) (def token-inputs (mf/create-context nil)) +(def tokens-lib (mf/create-context nil)) +(def tokens-status (mf/create-context nil)) (def current-scroll (mf/create-context nil)) (def current-zoom (mf/create-context nil)) @@ -45,6 +47,7 @@ (def permissions (mf/create-context nil)) (def can-edit? (mf/create-context nil)) +(def can-edit-tokens? (mf/create-context nil)) (def active-tokens-by-type "Active tokens by type, used mainly for provide tokens data to the diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs index efe004275c..da902d690e 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs @@ -314,7 +314,8 @@ "A collection of all icons" (collect-icons)) -(def ^:private ^:const icon-size-l 32) +(def ^:private ^:const icon-size-xl 32) +(def ^:private ^:const icon-size-l 24) (def ^:private ^:const icon-size-m 16) (def ^:private ^:const icon-size-s 12) @@ -323,12 +324,13 @@ [:class {:optional true} [:maybe :string]] [:icon-id [:and :string [:fn #(contains? icon-list %)]]] [:size {:optional true} - [:maybe [:enum "s" "m" "l"]]]]) + [:maybe [:enum "s" "m" "l" "xl"]]]]) (mf/defc icon* {::mf/schema schema:icon} [{:keys [icon-id size class] :rest props}] - (let [size-px (cond (= size "l") icon-size-l + (let [size-px (cond (= size "xl") icon-size-xl + (= size "l") icon-size-l (= size "s") icon-size-s :else icon-size-m) offset (if (or (= size "s") (= size "m")) diff --git a/frontend/src/app/main/ui/ds/product/empty_state.cljs b/frontend/src/app/main/ui/ds/product/empty_state.cljs index bad9db3781..f5b2b09e48 100644 --- a/frontend/src/app/main/ui/ds/product/empty_state.cljs +++ b/frontend/src/app/main/ui/ds/product/empty_state.cljs @@ -25,6 +25,6 @@ (when icon [:div {:class (stl/css :icon-wrapper)} [:> icon* {:icon-id icon - :size "l" + :size "xl" :class (stl/css :icon)}]]) [:div {:class (stl/css :text)} text]])) diff --git a/frontend/src/app/main/ui/inspect/styles.cljs b/frontend/src/app/main/ui/inspect/styles.cljs index 1d1cb2cc1b..8eddf353e8 100644 --- a/frontend/src/app/main/ui/inspect/styles.cljs +++ b/frontend/src/app/main/ui/inspect/styles.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.tokens :as cfo] [app.common.types.component :as ctc] [app.common.types.components-list :as ctkl] [app.common.types.shape.layout :as ctl] @@ -111,11 +112,17 @@ panels (type->panel-group shape-type) tokens-lib (mf/deref refs/tokens-lib) - active-themes (mf/deref refs/workspace-active-theme-paths-no-hidden) - active-sets (mf/with-memo [tokens-lib] - (some-> tokens-lib (ctob/get-active-themes-set-names))) - active-tokens (mf/with-memo [tokens-lib] - (some-> tokens-lib (ctob/get-tokens-in-active-sets))) + tokens-status (mf/deref refs/tokens-status) + active-theme-ids (mf/deref refs/workspace-active-theme-ids) + active-themes (mf/with-memo [active-theme-ids tokens-lib] + (when tokens-lib + (keep #(some-> (ctob/get-theme tokens-lib %) ctob/get-name) active-theme-ids))) + active-sets (mf/with-memo [tokens-status tokens-lib] + (when (and tokens-status tokens-lib) + (cfo/get-active-sets tokens-status tokens-lib))) + active-tokens (mf/with-memo [tokens-status tokens-lib] + (when (and tokens-status tokens-lib) + (cfo/get-tokens-in-active-sets tokens-status tokens-lib))) resolved-active-tokens (sd/use-resolved-tokens* active-tokens) has-visibility-props? (mf/use-fn (fn [shape] @@ -152,7 +159,7 @@ (when (or (seq active-themes) (seq active-sets)) [:li [:> style-box* {:panel :token} - [:> tokens-panel* {:theme-paths active-themes :set-names active-sets}]]]) + [:> tokens-panel* {:theme-names active-themes :set-names (map ctob/get-name active-sets)}]]]) (for [panel panels] [:li {:key (d/name panel)} (case panel diff --git a/frontend/src/app/main/ui/inspect/styles/panels/tokens_panel.cljs b/frontend/src/app/main/ui/inspect/styles/panels/tokens_panel.cljs index 2a86d196ca..836a10c547 100644 --- a/frontend/src/app/main/ui/inspect/styles/panels/tokens_panel.cljs +++ b/frontend/src/app/main/ui/inspect/styles/panels/tokens_panel.cljs @@ -13,10 +13,10 @@ [rumext.v2 :as mf])) (mf/defc tokens-panel* - [{:keys [theme-paths set-names]}] + [{:keys [theme-names set-names]}] [:div {:class (stl/css :tokens-panel)} - (when (seq theme-paths) - (let [theme-list (str/join ", " theme-paths)] + (when (seq theme-names) + (let [theme-list (str/join ", " theme-names)] [:> properties-row* {:class (stl/css :token-theme) :term (tr "inspect.tabs.styles.active-themes") :detail theme-list}])) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index b5613862af..3465e6a70c 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.tokens :as cfo] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.types.color :as cc] @@ -759,10 +760,15 @@ tokens-lib (mf/deref refs/tokens-lib) + tokens-status + (mf/deref refs/tokens-status) + active-sets-names - (mf/with-memo [tokens-lib] - (some-> tokens-lib - (ctob/get-active-themes-set-names))) + (mf/with-memo [tokens-status tokens-lib] + (when (and tokens-status tokens-lib) + (into #{} + (map ctob/get-name) + (cfo/get-active-sets tokens-status tokens-lib)))) active-tokens (if (delay? active-tokens) @active-tokens @@ -782,7 +788,7 @@ (filter-active-sets active-sets-names) (filter-non-empty-sets) (group-sets) - (combine-groups-with-resolved color-tokens)))] + (combine-groups-with-resolved color-tokens)))] (mf/with-effect [] (st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts :workspace))) diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index 17a896c286..b2e64096a3 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.tokens :as cfo] [app.common.files.variant :as cfv] [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] @@ -24,6 +25,7 @@ [app.main.data.team :as dtm] [app.main.data.workspace.colors :as mdc] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.refs :as refs] [app.main.render :refer [component-svg]] [app.main.store :as st] @@ -34,7 +36,8 @@ [app.main.ui.context :as ctx] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] - [app.main.ui.ds.foundations.assets.icon :as i] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] + [app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]] [app.main.ui.ds.product.empty-state :refer [empty-state*]] [app.main.ui.hooks :as h] @@ -56,16 +59,27 @@ typographies (count (:typographies data)) components (count (->> (ctkl/components-seq data) (remove #(cfv/is-secondary-variant? % data)))) + tokens-lib (cfo/get-tokens-lib data) + tokens (if (some? tokens-lib) (count (ctob/get-all-tokens tokens-lib)) 0) + token-sets (if (some? tokens-lib) (count (ctob/get-sets tokens-lib)) 0) + token-themes (if (some? tokens-lib) (count (ctob/get-themes-no-hidden tokens-lib)) 0) + empty? (and (zero? components) (zero? graphics) (zero? colors) - (zero? typographies))] + (zero? typographies) + (zero? tokens) + (zero? token-sets) + (zero? token-themes))] {:is-empty empty? :colors colors :graphics graphics :typographies typographies - :components components})) + :components components + :tokens tokens + :token-sets token-sets + :token-themes token-themes})) (defn- adapt-backend-summary [summary] @@ -73,16 +87,25 @@ graphics (or (-> summary :media :count) 0) typographies (or (-> summary :typographies :count) 0) colors (or (-> summary :colors :count) 0) + tokens (or (-> summary :tokens-count) 0) + token-sets (or (-> summary :token-sets-count) 0) + token-themes (or (-> summary :token-themes-count) 0) empty? (and (zero? components) (zero? graphics) (zero? colors) - (zero? typographies))] - {:is-empty empty? - :components components - :graphics graphics + (zero? typographies) + (zero? tokens) + (zero? token-sets) + (zero? token-themes))] + {:is-empty empty? + :components components + :graphics graphics :typographies typographies - :colors colors})) + :colors colors + :tokens tokens + :token-sets token-sets + :token-themes token-themes})) (defn- describe-library [components-count graphics-count colors-count typography-count] @@ -108,28 +131,88 @@ (mf/defc library-description* {::mf/private true} - [{:keys [summary]}] - (let [components-count (get summary :components) - graphics-count (get summary :graphics) - typography-count (get summary :typographies) - colors-count (get summary :colors)] + [{:keys [summary hint-name]}] + (let [components-count (get summary :components) + graphics-count (get summary :graphics) + typography-count (get summary :typographies) + colors-count (get summary :colors) + tokens-count (get summary :tokens) + token-sets-count (get summary :token-sets) + token-themes-count (get summary :token-themes) - [:* - (when (pos? components-count) - [:li {:class (stl/css :element-count)} - (tr "workspace.libraries.components" (c components-count))]) + token-lib-sync? (contains? cf/flags :token-lib-sync) - (when (pos? graphics-count) - [:li {:class (stl/css :element-count)} - (tr "workspace.libraries.graphics" (c graphics-count))]) + elements-line + (str/join " · " + (cond-> [] + (pos? components-count) + (conj (tr "workspace.libraries.components" (c components-count))) - (when (pos? colors-count) - [:li {:class (stl/css :element-count)} - (tr "workspace.libraries.colors" (c colors-count))]) + (pos? graphics-count) + (conj (tr "workspace.libraries.graphics" (c graphics-count))) - (when (pos? typography-count) - [:li {:class (stl/css :element-count)} - (tr "workspace.libraries.typography" (c typography-count))])])) + (pos? colors-count) + (conj (tr "workspace.libraries.colors" (c colors-count))) + + (pos? typography-count) + (conj (tr "workspace.libraries.typography" (c typography-count))))) + + tokens-line + (str/join " · " + (cond-> [] + (pos? tokens-count) + (conj (tr "workspace.libraries.tokens" (c tokens-count))) + + (pos? token-sets-count) + (conj (tr "workspace.libraries.token-sets" (c token-sets-count))) + + (pos? token-themes-count) + (conj (tr "workspace.libraries.token-themes" (c token-themes-count)))))] + + (if token-lib-sync? + ;; New format: show elements and tokens in separate lines + [:* + (when-not (str/empty? elements-line) + [:li {:class (stl/css :element-count-line)} elements-line]) + + (when-not (str/empty? tokens-line) + [:li {:class (stl/css :element-count-line)} tokens-line]) + + (when hint-name + [:li {:class (stl/css :hint-line)} + "(" (tr "workspace.libraries.connected-through") " " + [:span {:class (stl/css :hint-library-name)} hint-name] + ")"])] + + ;; Old format: show each item individually + [:* + (when (pos? components-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.components" (c components-count))]) + + (when (pos? graphics-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.graphics" (c graphics-count))]) + + (when (pos? colors-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.colors" (c colors-count))]) + + (when (pos? typography-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.typography" (c typography-count))]) + + (when (pos? tokens-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.tokens" (c tokens-count))]) + + (when (pos? token-sets-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.token-sets" (c token-sets-count))]) + + (when (pos? token-themes-count) + [:li {:class (stl/css :element-count-legacy)} + (tr "workspace.libraries.token-themes" (c token-themes-count))])]))) (mf/defc sample-library-entry* {::mf/private true} @@ -182,7 +265,7 @@ (when-let [tokens-lib (get data :tokens-lib)] (not (ctob/empty-lib? tokens-lib)))) -(mf/defc libraries-tab* +(mf/defc libraries-tab-legacy* {::mf/private true} [{:keys [is-shared linked-libraries shared-libraries]}] (let [file-id (mf/use-ctx ctx/current-file-id) @@ -262,7 +345,7 @@ unlink-library (mf/use-fn - (mf/deps file-id) + (mf/deps file-id local-library) (fn [event] (let [library-id (some-> (dom/get-current-target event) (dom/get-data "library-id") @@ -270,7 +353,11 @@ (when (= library-id @selected) (reset! selected :file)) (st/emit! (dwl/unlink-file-from-library file-id library-id) - (dwl/sync-file file-id library-id))))) + (dwl/sync-file file-id library-id)) + ;; When the unlinked library is the current tokens source, + ;; we must reset it to the local library. + (when (cfo/effective-tokens-source? local-library library-id) + (st/emit! (dwtl/set-tokens-source (:id local-library))))))) import-tokens (mf/use-fn @@ -283,6 +370,7 @@ :tokens/import-from-library {:file-id file-id :library-id library-id}))))) + on-delete-accept (mf/use-fn (mf/deps file-id) @@ -324,7 +412,7 @@ :on-cancel on-delete-cancel :count-libraries 1}))))] - [:div {:class (stl/css :libraries-content)} + [:div {:class (stl/css :libraries-content-legacy)} [:div {:class (stl/css :lib-section)} [:> title-bar* {:collapsable false :title (tr "workspace.libraries.in-this-file") @@ -334,7 +422,7 @@ [:div {:class (stl/css :section-list-publish)} [:div {:class (stl/css :item-content)} [:div {:class (stl/css :item-title)} (tr "workspace.libraries.file-library")] - [:ul {:class (stl/css :item-contents)} + [:ul {:class (stl/css :item-contents-legacy)} [:> library-description* {:summary summary}]]] (if ^boolean is-shared @@ -352,21 +440,21 @@ (let [disabled? (some #(contains? linked-libraries-ids %) connected-to) has-tokens? (and (has-tokens? library) (contains? cf/flags :token-import-from-library))] - [:div {:class (stl/css :section-list-item) + [:div {:class (stl/css :section-list-item-legacy) :key (dm/str id) :data-testid "library-item"} [:div {:class (stl/css :item-content)} - [:div {:class (stl/css-case :item-name true - :item-name-short has-tokens?)} name] - [:ul {:class (stl/css :item-contents)} + [:div {:class (stl/css :item-name)} name] + [:ul {:class (stl/css :item-contents-legacy)} (let [summary (get-library-summary data)] [:* [:> library-description* {:summary summary}] (when (seq connected-to) - [:div {:class (stl/css :connected-to-wrapper)} + [:div {:class (stl/css :connected-to-wrapper-legacy)} [:span "(" (tr "workspace.libraries.connected-to") " "] [:span {:class (stl/css :connected-to-values)} (str/join ", " connected-to-names)] [:span ")"]])])]] + [:div {:class (stl/css :library-actions)} (when ^boolean has-tokens? [:> icon-button* @@ -397,12 +485,12 @@ (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) + [:div {:class (stl/css :section-list-item-legacy) :key (dm/str id) :data-testid "library-item"} [:div {:class (stl/css :item-content)} [:div {:class (stl/css :item-name)} name] - [:ul {:class (stl/css :item-contents)} + [:ul {:class (stl/css :item-contents-legacy)} (let [summary (-> (:library-summary library) (adapt-backend-summary))] [:> library-description* {:summary summary}])]] @@ -439,6 +527,372 @@ :else (tr "workspace.libraries.no-matches-for" search-term))]))]])) +(mf/defc libraries-tab* + {::mf/private true} + [{:keys [linked-libraries shared-libraries]}] + (let [file-id (mf/use-ctx ctx/current-file-id) + search-term* (mf/use-state "") + search-term (deref search-term*) + + + selected (h/use-shared-state mdc/colorpalette-selected-broadcast-key :recent) + dependencies (mf/with-memo [shared-libraries] + (into {} (map (juxt :id :library-file-ids) (vals shared-libraries)))) + + library-names (mf/with-memo [shared-libraries] + (into {} (map (fn [{:keys [id name]}] + [id name]) + (vals shared-libraries)))) + + find-connected-to + (mf/use-fn + (mf/deps dependencies) + (fn [library-id] + (->> dependencies + (keep (fn [[k v]] (when (contains? v library-id) k)))))) + + shared-libraries + (mf/with-memo [shared-libraries linked-libraries file-id search-term] + (when shared-libraries + (->> (vals shared-libraries) + (remove #(= (:id %) file-id)) + (filter #(matches-search (:name %) search-term)) + (map #(assoc % :connected-to (find-connected-to (:id %)))) + (map #(assoc % :connected-to-names (->> (:connected-to %) + (keep library-names)))) + (map #(assoc % :linked? (contains? linked-libraries (:id %)))) + (sort-by (comp str/lower :name))))) + + + importing* + (mf/use-state nil) + + sample-libraries + (mf/with-memo [] + [{:id "penpot-design-system", :name "Design system example"} + {:id "wireframing-kit", :name "Wireframe library"} + {:id "whiteboarding-kit", :name "Whiteboarding Kit"}]) + + + change-search-term + (mf/use-fn + (fn [event] + (reset! search-term* event))) + + link-library + (mf/use-fn + (mf/deps file-id) + (fn [event] + (let [library-id (some-> (dom/get-current-target event) + (dom/get-data "library-id") + (uuid/parse))] + (reset! selected library-id) + (st/emit! (dwl/link-file-to-library file-id library-id)))))] + + [:div {:class (stl/css :libraries-content)} + [:div {:class (stl/css :shared-section)} + [:> title-bar* {:collapsable false + :title (tr "workspace.libraries.shared-libraries") + :class (stl/css :title-spacing-lib)}] + [:> search-bar* {:on-change change-search-term + :value search-term + :placeholder (tr "workspace.libraries.search-shared-libraries") + :icon-id i/search}] + + (if (seq shared-libraries) + [:div {:class (stl/css :section-list-shared)} + (for [{:keys [id name linked?] :as library} shared-libraries] + [:div {:class (stl/css :section-list-item) + :key (dm/str id) + :data-testid "library-item"} + [:div {:class (stl/css :item-content)} + [:div {:class (stl/css :item-name)} name] + [:ul {:class (stl/css :item-contents)} + (let [summary (-> (:library-summary library) + (adapt-backend-summary))] + [:> library-description* {:summary summary}])]] + + (if ^boolean linked? + [:> text* {:class (stl/css :shared-library-added) + :as "span" + :typography "body-medium"} + [:> icon* {:icon-id i/tick + :class (stl/css :shared-library-added-icon)}] + (tr "workspace.libraries.shared-library-added")] + [:> icon-button* {:class (stl/css :item-button-shared) + :variant "secondary" + :data-library-id (dm/str id) + :icon "add" + :aria-label (tr "workspace.libraries.shared-library-btn") + :on-click link-library}])])] + + (when (empty? shared-libraries) + [:div {:class (stl/css :section-list-empty)} + (cond + (nil? shared-libraries) + (tr "workspace.libraries.loading") + + (str/empty? search-term) + [:* + [:div {:class (stl/css :sample-libraries-info)} + (tr "workspace.libraries.empty.no-libraries") + [:a {:target "_blank" + :class (stl/css :sample-libraries-link) + :href "https://penpot.app/libraries-templates"} + (tr "workspace.libraries.empty.some-templates")]] + [:div {:class (stl/css :sample-libraries-container)} + (tr "workspace.libraries.empty.add-some") + (for [library sample-libraries] + [:> sample-library-entry* + {:library library + :key (dm/str (:id library)) + :importing importing*}])]] + + :else + (tr "workspace.libraries.no-matches-for" search-term))]))]])) + +(mf/defc file-tab* + {::mf/private true} + [{:keys [is-shared linked-libraries shared-libraries on-change-tab]}] + (let [file-id (mf/use-ctx ctx/current-file-id) + + ;; The summary of the current/local library + ;; NOTE: we only need a snapshot of current library + local-library (deref refs/workspace-data) + summary (get-library-summary local-library) + empty-library? (empty-library? summary) + + selected (h/use-shared-state mdc/colorpalette-selected-broadcast-key :recent) + dependencies (mf/with-memo [shared-libraries] + (into {} (map (juxt :id :library-file-ids) (vals shared-libraries)))) + + library-names (mf/with-memo [shared-libraries] + (into {} (map (fn [{:keys [id name]}] + [id name]) + (vals shared-libraries)))) + + find-connected-to + (mf/use-fn + (mf/deps dependencies) + (fn [library-id] + (->> dependencies + (keep (fn [[k v]] (when (contains? v library-id) k)))))) + + linked-libraries + (mf/with-memo [linked-libraries find-connected-to library-names] + (let [libs + (->> (vals linked-libraries) + (map #(assoc % :connected-to (find-connected-to (:id %))))) + + linked-ids + (into #{} (map :id) libs) + + sort-by-name + (partial sort-by (comp str/lower :name)) + + parent-id + (fn [{:keys [connected-to]}] + (first (filter linked-ids connected-to))) + + nested-by-parent + (->> libs + (filter parent-id) + (map #(assoc % + :nested? true + :parent-name (library-names (parent-id %)))) + (group-by parent-id) + (d/mapm (fn [_ v] (sort-by-name v))))] + + (->> libs + (remove parent-id) + (map #(assoc % :nested? false)) + (sort-by-name) + (mapcat (fn [{:keys [id] :as library}] + (cons library (get nested-by-parent id []))))))) + + linked-libraries-ids + (mf/with-memo [linked-libraries] + (into #{} d/xf:map-id linked-libraries)) + + unlink-library + (mf/use-fn + (mf/deps file-id local-library) + (fn [event] + (let [library-id (some-> (dom/get-current-target event) + (dom/get-data "library-id") + (uuid/parse))] + (when (= library-id @selected) + (reset! selected :file)) + (st/emit! (dwl/unlink-file-from-library file-id library-id) + (dwl/sync-file file-id library-id)) + ;; When the unlinked library is the current tokens source, + ;; we must reset it to the local library. + (when (cfo/effective-tokens-source? local-library library-id) + (st/emit! (dwtl/set-tokens-source (:id local-library))))))) + + import-tokens + (mf/use-fn + (mf/deps file-id) + (fn [event] + (let [library-id (some-> (dom/get-current-target event) + (dom/get-data "library-id") + (uuid/parse))] + (st/emit! (modal/show + :tokens/import-from-library {:file-id file-id + :library-id library-id}))))) + + set-as-tokens-source + (mf/use-fn + (fn [event] + (let [library-id (some-> (dom/get-current-target event) + (dom/get-data "library-id") + (uuid/parse))] + (st/emit! (dwtl/set-tokens-source 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 {:file-id file-id}))) + + on-delete-cancel + (mf/use-fn + (mf/deps file-id) + #(st/emit! (modal/show :libraries-dialog {:file-id file-id}))) + + publish + (mf/use-fn + (mf/deps file-id) + (fn [event] + (let [input-node (dom/get-target event) + publish-library #(st/emit! (dwl/set-file-shared file-id true)) + cancel-publish #(st/emit! (modal/show :libraries-dialog {:file-id file-id}))] + (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})))) + + go-to-shared + (mf/use-fn + (mf/deps on-change-tab) + (fn [_] + (on-change-tab "libraries")))] + + [:div {:class (stl/css :lib-section)} + [:> title-bar* {:collapsable false + :title "ASSETS IN THIS FILE" + :class (stl/css :title-spacing-lib)}] + [:div {:class (stl/css :section-list)} + [:div {:class (stl/css :section-list-publish)} + [:div {:class (stl/css :item-content)} + [:div {:class (stl/css :item-title)} (tr "workspace.libraries.file-library")] + [:ul {:class (stl/css :item-contents)} + [:> library-description* {:summary summary}]]] + (when (and (contains? cf/flags :token-lib-sync) + (cfo/effective-tokens-source? local-library (:id local-library)) + (cfo/has-own-tokens? local-library)) + [:> text* {:class (stl/css :tokens-source-label) + :as "span" + :typography "body-medium"} + (tr "workspace.libraries.tokens-source")]) + (when (and (contains? cf/flags :token-lib-sync) + (not (cfo/effective-tokens-source? local-library (:id local-library)))) + [:> button* {:variant "secondary" + :type "button" + :data-library-id (dm/str (:id local-library)) + :on-click set-as-tokens-source} + (tr "workspace.libraries.set-as-tokens-source")]) + + (if ^boolean is-shared + [:> button* {:variant "secondary" + :type "button" + :on-click unpublish} + (tr "common.unpublish")] + + [:> button* {:variant "primary" + :type "button" + :on-click publish} + (tr "common.publish")])] + + [:div {:class (stl/css :section-list-linked)} + [:> title-bar* {:collapsable false + :title (tr "workspace.libraries.connected-libraries") + :class (stl/css :title-spacing-lib) + :title-class (stl/css :connected-libraries-title)}] + (if (seq linked-libraries) + (for [{:keys [id name data connected-to nested? parent-name] :as library} linked-libraries] + (let [disabled? (some #(contains? linked-libraries-ids %) connected-to) + has-tokens? (and (has-tokens? library) + (contains? cf/flags :token-import-from-library))] + [:div {:class (stl/css-case :section-list-item true + :section-list-item-nested nested?) + :key (dm/str id) + :data-testid "library-item"} + [:div {:class (stl/css :item-content)} + [:div {:class (stl/css :item-name)} name] + [:ul {:class (stl/css :item-contents)} + (let [summary (get-library-summary data)] + [:> library-description* {:summary summary :hint-name parent-name}])]] + + [:div {:class (stl/css :library-actions)} + (when (and (contains? cf/flags :token-lib-sync) + (cfo/effective-tokens-source? local-library id)) + [:> text* {:class (stl/css :tokens-source-label) + :as "span" + :typography "body-medium"} + (tr "workspace.libraries.tokens-source")]) + (if (contains? cf/flags :token-lib-sync) + (when (and (cfo/tokens-provider? (:data library)) + (not (cfo/effective-tokens-source? local-library id))) + [:> button* {:variant "secondary" + :type "button" + :data-library-id (dm/str id) + :on-click set-as-tokens-source} + (tr "workspace.libraries.set-as-tokens-source")]) + (when ^boolean has-tokens? + [:> icon-button* + {:type "button" + :aria-label (tr "workspace.tokens.import-tokens") + :icon i/import-export + :data-library-id (dm/str id) + :variant "secondary" + :on-click import-tokens}])) + + [:> icon-button* {:type "button" + :aria-label (tr "workspace.libraries.unlink-library-btn") + :icon i/detach + :data-library-id (dm/str id) + :variant "secondary" + :disabled disabled? + :on-click unlink-library}]]])) + [:div {:class (stl/css :shared-libraries-empty)} + [:> text* {:class (stl/css :empty-libraries-text) + :as "span" + :typography "body-small"} + (tr "workspace.libraries.empty.no-connected-libraries")] + [:button {:class (stl/css :go-to-shared-button) + :on-click go-to-shared} + "Add a shared library"]])]]])) + + (defn- extract-assets [file-data library summary?] (let [exceeded (volatile! {:components false @@ -483,7 +937,7 @@ (mf/defc updates-tab* {::mf/private true} - [{:keys [file-id libraries]}] + [{:keys [file-id libraries is-legacy]}] ;; FIXME: naming (let [summary?* (mf/use-state true) summary? (deref summary?*) @@ -518,14 +972,17 @@ (dwl/set-updating-library true) (dwl/sync-file file-id library-id)))))))] - [:div {:class (stl/css :updates-content)} + [:div {:class (stl/css-case :updates-content (not is-legacy) + :updates-content-legacy is-legacy)} [:div {:class (stl/css :update-section)} (if (empty? libs-assets) [:div {:class (stl/css :section-list-empty)} [:> empty-state* {:icon i/library :text (tr "workspace.libraries.no-libraries-need-sync")}]] [:* - [:div {:class (stl/css :section-title)} (tr "workspace.libraries.library-updates")] + [:> title-bar* {:collapsable false + :title (tr "workspace.libraries.library-updates") + :class (stl/css :title-spacing-lib)}] [:div {:class (stl/css :section-list)} (for [[{:keys [id name] :as library} @@ -534,7 +991,7 @@ [:div {:class (stl/css :section-list-item) :key (dm/str id)} [:div {:class (stl/css :item-content)} - [:div {:class (stl/css :item-name-long)} name] + [:div {:class (stl/css :item-name)} name] [:ul {:class (stl/css :item-contents)} (describe-library (count components) 0 @@ -655,8 +1112,10 @@ (fn [_] (modal/hide!))) + token-lib-sync? (contains? cf/flags :token-lib-sync) + selected-tab* - (mf/use-state #(d/nilv starting-tab "libraries")) + (mf/use-state #(d/nilv starting-tab (if token-lib-sync? "file" "libraries"))) selected-tab (deref selected-tab*) @@ -665,11 +1124,18 @@ (mf/use-fn #(reset! selected-tab* %)) tabs - (mf/with-memo [] - [{:label (tr "workspace.libraries.libraries") - :id "libraries"} - {:label (tr "workspace.libraries.updates") - :id "updates"}])] + (mf/with-memo [token-lib-sync?] + (if token-lib-sync? + [{:label "This file" + :id "file"} + {:label (tr "workspace.libraries.libraries") + :id "libraries"} + {:label (tr "workspace.libraries.updates") + :id "updates"}] + [{:label (tr "workspace.libraries.libraries") + :id "libraries"} + {:label (tr "workspace.libraries.updates") + :id "updates"}]))] (mf/with-effect [] (st/emit! (dtm/fetch-shared-files))) @@ -690,15 +1156,26 @@ :selected selected-tab :on-change on-change-tab} (case selected-tab + "file" + (when token-lib-sync? + [:> file-tab* {:is-shared shared? + :on-change-tab on-change-tab + :linked-libraries linked-libraries + :shared-libraries shared-libraries}]) "libraries" - [:> libraries-tab* - {:is-shared shared? - :linked-libraries linked-libraries - :shared-libraries shared-libraries}] + (if token-lib-sync? + [:> libraries-tab* + {:linked-libraries linked-libraries + :shared-libraries shared-libraries}] + [:> libraries-tab-legacy* + {:is-shared shared? + :linked-libraries linked-libraries + :shared-libraries shared-libraries}]) "updates" [:> updates-tab* {:file-id file-id + :is-legacy (not token-lib-sync?) :libraries linked-libraries}])]]])) (mf/defc v2-info-dialog diff --git a/frontend/src/app/main/ui/workspace/libraries.scss b/frontend/src/app/main/ui/workspace/libraries.scss index 21a7ad9a10..87ad47c9a1 100644 --- a/frontend/src/app/main/ui/workspace/libraries.scss +++ b/frontend/src/app/main/ui/workspace/libraries.scss @@ -51,6 +51,13 @@ // Tabs content .libraries-content, .updates-content { + display: grid; + gap: var(--sp-xxxl); + max-height: $sz-400; +} + +.libraries-content-legacy, +.updates-content-legacy { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-xxxl); @@ -64,6 +71,7 @@ display: grid; grid-template-rows: auto 1fr; gap: var(--sp-s); + padding-block-start: var(--sp-xs); } .shared-section { @@ -75,6 +83,10 @@ margin: 0 0 0 calc(-1 * var(--sp-s)); } +.connected-libraries-title { + @include t.use-typography("headline-small"); +} + .section-list, .section-list-shared { display: grid; @@ -86,7 +98,8 @@ %section-list-item-placeholder { display: grid; - grid-template-columns: 1fr auto; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; gap: var(--sp-s); border: $b-1 solid var(--color-background-quaternary); padding: var(--sp-s) var(--sp-s) var(--sp-m) var(--sp-m); @@ -96,21 +109,33 @@ .section-list-publish { @extend %section-list-item-placeholder; + grid-template-columns: minmax(0, 1fr) auto auto; border: none; } +.section-list-item-legacy { + @extend %section-list-item-placeholder; +} + .section-list-item { @extend %section-list-item-placeholder; + + padding: var(--sp-m); +} + +.section-list-item-nested { + margin-inline-start: var(--sp-l); } .section-list-item-double-icon { @extend %section-list-item-placeholder; - grid-template-columns: 1fr auto auto; + grid-template-columns: minmax(0, 1fr) auto auto; } .item-content { height: fit-content; + min-width: 0; } .close-btn { @@ -130,6 +155,13 @@ color: var(--title-foreground-color); } +.section-list-linked { + display: grid; + grid-auto-rows: min-content; + gap: var(--sp-s); + max-height: px2rem(320); +} + // empty state .section-list-empty { display: grid; @@ -141,6 +173,23 @@ margin-block: var(--sp-l); } +.shared-libraries-empty { + border: $b-1 solid var(--color-background-quaternary); + padding: px2rem(40) var(--sp-s); + border-radius: $br-8; + display: grid; + grid-template-rows: auto 1fr; + justify-items: center; + gap: var(--sp-m); + text-align: center; + height: fit-content; + margin-block: var(--sp-l); +} + +.empty-libraries-text { + color: var(--color-foreground-secondary); +} + // Update library tab .libraries-updates-see-all { background: unset; @@ -155,6 +204,7 @@ margin: 0; } +.updates-content-legacy, .updates-content { grid-template-columns: 1fr; } @@ -206,7 +256,6 @@ @include text-ellipsis; margin: 0; - max-width: px2rem(236); color: var(--library-name-foreground-color); } @@ -214,16 +263,6 @@ @extend %item-name; } -.item-name-short { - max-width: px2rem(206); -} - -.item-name-long { - @extend %item-name; - - max-width: px2rem(450); -} - .item-title { @include t.use-typography("body-large"); @@ -241,7 +280,7 @@ border-radius: $br-8; } -.item-contents { +.item-contents-legacy { @include t.use-typography("body-small"); color: var(--library-content-foreground-color); @@ -250,12 +289,22 @@ margin: 0; } +.item-contents { + @include t.use-typography("body-small"); + + color: var(--library-content-foreground-color); + display: flex; + flex-direction: column; + margin: 0; +} + .library-actions { display: flex; + flex-shrink: 0; gap: var(--sp-xs); } -.element-count { +.element-count-legacy { white-space: nowrap; &:not(:last-child)::after { @@ -264,14 +313,43 @@ } } -.connected-to-wrapper { +.element-count-line { + display: flex; + align-items: baseline; + gap: var(--sp-xs); + + &::before { + content: "•"; + } +} + +.connected-to-wrapper-legacy { display: block; } -.connected-to-values { +.connected-to-values-legacy { color: var(--color-foreground-primary); } +.hint-line { + color: var(--color-foreground-secondary); +} + +.hint-library-name { + color: var(--color-foreground-primary); +} + +.shared-library-added { + display: flex; + justify-content: center; + align-items: center; + color: var(--color-accent-tertiary); + block-size: fit-content; + border: $b-1 solid var(--color-accent-select); + border-radius: $br-8; + padding-inline: var(--sp-xxs) px2rem(6); +} + // Modal Component v2 update .modal-v2-info { width: px2rem(664); @@ -364,6 +442,19 @@ } } +.go-to-shared-button { + @include t.use-typography("body-small"); + + color: var(--color-accent-primary); + border: none; + background: none; + cursor: pointer; + + &:hover { + text-decoration: underline; + } +} + .sample-libraries-container { @include t.use-typography("body-small"); @@ -403,3 +494,15 @@ overflow: hidden; text-overflow: ellipsis; } + +.tokens-source-label { + block-size: $sz-32; + display: flex; + justify-content: center; + align-items: center; + padding: var(--sp-s) var(--sp-xl); + background: var(--color-background-primary); + border: $b-1 solid var(--color-accent-select); + color: var(--color-accent-tertiary); + border-radius: $br-8; +} diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index d33272ccac..899624002b 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -8,6 +8,7 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] + [app.common.files.tokens :as cfo] [app.common.types.tokens-lib :as ctob] [app.config :as cf] [app.main.constants :refer [left-sidebar-default-max-width @@ -22,7 +23,7 @@ [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.context :as muc] + [app.main.ui.context :as ctx] [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.layout.tab-switcher :refer [tab-switcher*]] @@ -116,7 +117,7 @@ (mf/defc left-sidebar* {::mf/memo true} - [{:keys [layout file tokens-lib active-tokens resolved-active-tokens]}] + [{:keys [layout file tokens-lib tokens-status active-tokens resolved-active-tokens]}] (let [options-mode (mf/deref refs/options-mode-global) project (mf/deref refs/project) file-id (get file :id) @@ -173,7 +174,7 @@ (mf/with-memo [] (mf/html [:> collapse-button* {}]))] - [:> (mf/provider muc/sidebar) {:value :left} + [:> (mf/provider ctx/sidebar) {:value :left} [:aside {:ref parent-ref :id "left-sidebar-aside" :data-testid "left-sidebar" @@ -215,9 +216,11 @@ :file-id file-id}] :tokens - [:> tokens-sidebar-tab* {:tokens-lib tokens-lib - :active-tokens active-tokens - :resolved-active-tokens resolved-active-tokens}] + [:> tokens-sidebar-tab* + {:tokens-lib tokens-lib + :tokens-status tokens-status + :active-tokens active-tokens + :resolved-active-tokens resolved-active-tokens}] :layers [:> layers-content* {:layout layout @@ -313,8 +316,8 @@ (mf/with-memo [active-tokens] (delay (ctob/group-by-type active-tokens)))] - [:> (mf/provider muc/sidebar) {:value :right} - [:> (mf/provider muc/active-tokens-by-type) {:value active-tokens-by-type} + [:> (mf/provider ctx/sidebar) {:value :right} + [:> (mf/provider ctx/active-tokens-by-type) {:value active-tokens-by-type} [:aside {:class (stl/css-case :right-sidebar true :not-expand (not can-be-expanded?) @@ -358,19 +361,22 @@ (let [tokens-lib (mf/deref refs/tokens-lib) + tokens-status + (mf/deref refs/tokens-status) + active-tokens - (mf/with-memo [tokens-lib] - (if tokens-lib - (ctob/get-tokens-in-active-sets tokens-lib) + (mf/with-memo [tokens-status tokens-lib] + (if (and tokens-status tokens-lib) + (cfo/get-tokens-in-active-sets tokens-status tokens-lib) {})) selected-token-set-id (mf/deref refs/selected-token-set-id) active-tokens-force-set - (mf/with-memo [tokens-lib selected-token-set-id] - (if (and tokens-lib selected-token-set-id) - (ctob/get-tokens-in-active-sets-force tokens-lib selected-token-set-id) + (mf/with-memo [tokens-status tokens-lib selected-token-set-id] + (if (and tokens-status tokens-lib selected-token-set-id) + (cfo/get-tokens-in-active-sets-force tokens-status tokens-lib selected-token-set-id) {})) tokenscript? (contains? cf/flags :tokenscript) @@ -389,25 +395,27 @@ resolved-active-tokens-force-set (sd/use-resolved-tokens* active-tokens-force-set)] - [:* - (if (:collapse-left-sidebar layout) - [:> collapsed-button*] - [:> left-sidebar* {:layout layout + [:> (mf/provider ctx/tokens-lib) {:value tokens-lib} + [:> (mf/provider ctx/tokens-status) {:value tokens-status} + (if (:collapse-left-sidebar layout) + [:> collapsed-button*] + [:> left-sidebar* {:layout layout + :file file + :page-id page-id + :tokens-lib tokens-lib + :tokens-status tokens-status + :active-tokens active-tokens-force-set + :resolved-active-tokens (if tokenscript? + tokenscript-resolved-active-tokens-force-set + resolved-active-tokens-force-set)}]) + [:> right-sidebar* {:section section + :selected selected + :drawing-tool drawing-tool + :layout layout :file file + :file-id file-id :page-id page-id :tokens-lib tokens-lib - :active-tokens active-tokens-force-set - :resolved-active-tokens (if tokenscript? - tokenscript-resolved-active-tokens-force-set - resolved-active-tokens-force-set)}]) - [:> right-sidebar* {:section section - :selected selected - :drawing-tool drawing-tool - :layout layout - :file file - :file-id file-id - :page-id page-id - :tokens-lib tokens-lib - :active-tokens (if tokenscript? - tokenscript-resolved-active-tokens - resolved-active-tokens)}]])) + :active-tokens (if tokenscript? + tokenscript-resolved-active-tokens + resolved-active-tokens)}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index f345c514f2..3c196b1986 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -8,6 +8,7 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] + [app.common.files.tokens :as cfo] [app.common.types.components-list :as ctkl] [app.main.data.modal :as modal] [app.main.data.workspace :as dw] @@ -33,6 +34,12 @@ [{:keys [filters]}] (let [file-id (mf/use-ctx ctx/current-file-id) files (mf/deref refs/files) + current-file-data + (mf/deref refs/workspace-data) + tokens-source + (mf/with-memo [current-file-data] + (cfo/get-effective-tokens-source current-file-data)) + libraries (mf/with-memo [files file-id] (->> (refs/select-libraries files file-id) (vals) @@ -46,6 +53,7 @@ {:key (dm/str (:id file)) :file file :is-local false + :is-tokens-source (= (:id file) tokens-source) :is-default-open false :filters filters}]))) @@ -57,11 +65,16 @@ (mf/defc assets-local-library* {::mf/private true} [{:keys [filters]}] - (let [file (mf/deref ref:local-library)] + (let [file (mf/deref ref:local-library) + + is-tokens-source + (mf/with-memo [file] + (cfo/effective-tokens-source? (:data file) (:id file)))] [:> file-library* {:file file :is-local true :is-default-open true + :is-tokens-source is-tokens-source :filters filters}])) (defn- toggle-values diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs index 29a6190d12..8596dfd11a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.cljs @@ -22,7 +22,8 @@ [app.main.store :as st] [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.context :as ctx] - [app.main.ui.icons :as deprecated-icon] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] + [app.main.ui.ds.tooltip :refer [tooltip*]] [app.main.ui.workspace.sidebar.assets.colors :refer [colors-section*]] [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.main.ui.workspace.sidebar.assets.components :refer [components-section*]] @@ -76,7 +77,7 @@ (mf/defc file-library-title* {::mf/private true} - [{:keys [is-open is-local file-id page-id file-name]}] + [{:keys [is-open is-local file-id page-id file-name is-tokens-source]}] (let [router (mf/deref refs/router) team-id (mf/use-ctx ctx/current-team-id) url (rt/resolve-uri router :workspace @@ -93,7 +94,15 @@ (mf/use-fn (fn [ev] (dom/stop-propagation ev) - (st/emit! (ev/event {::ev/name "navigate-to-library-file"}))))] + (st/emit! (ev/event {::ev/name "navigate-to-library-file"})))) + + tokens-source-icon + (when is-tokens-source + (mf/html [:> tooltip* {:content (tr "workspace.tokens.current-tokens-source-tooltip")} + [:span {:class (stl/css :tokens-source-icon-wrapper)} + [:> icon* {:icon-id i/tokens + :size "m" + :class (stl/css :tokens-source-icon)}]]]))] [:div {:class (stl/css-case :library-title true @@ -103,17 +112,24 @@ :on-collapsed toggle-open :title (if is-local (mf/html [:div {:class (stl/css :special-title)} - (tr "workspace.assets.local-library")]) + [:span {:class (stl/css :special-title-text)} + (tr "workspace.assets.local-library")] + tokens-source-icon]) ;; Do we need to add shared info here? (mf/html [:div {:class (stl/css :special-title)} - file-name]))} + [:span {:class (stl/css :special-title-text)} + file-name] + tokens-source-icon]))} (when-not ^boolean is-local [:span {:title (tr "workspace.assets.open-library")} [:a {:class (stl/css :file-link) :href url :target "_blank" :on-click on-click} - deprecated-icon/open-link]])]])) + [:> icon* {:icon-id i/open-link + :size "m" + :class (stl/css :file-link-icon)}]]])]])) + (defn- extend-selected [selected type asset-groups asset-id file-id] @@ -299,13 +315,15 @@ (not ^boolean show-colors?) (not ^boolean show-typography?)) [:div {:class (stl/css :asset-title)} - [:span {:class (stl/css :no-found-icon)} - deprecated-icon/search] + [:span {:class (stl/css :no-found-icon-wrapper)} + [:> icon* {:icon-id i/search + :size "l" + :class (stl/css :no-found-icon)}]] [:span {:class (stl/css :no-found-text)} (tr "workspace.assets.not-found")]])])])) (mf/defc file-library* - [{:keys [file is-local is-default-open filters]}] + [{:keys [file is-local is-default-open filters is-tokens-source]}] (let [file-id (:id file) file-name (:name file) page-id (dm/get-in file [:data :pages 0]) @@ -381,7 +399,8 @@ :page-id page-id :file-name file-name :is-open open? - :is-local is-local}] + :is-local is-local + :is-tokens-source is-tokens-source}] (when ^boolean open? [:> file-library-content* diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss index 266718c00b..d365207070 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/file_library.scss @@ -4,95 +4,131 @@ // // Copyright (c) KALEIDOS SUBSIDIARY SL -@use "ds/typography.scss" as t; -@use "refactor/common-refactor.scss" as deprecated; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/typography.scss" as *; .tool-window { - padding: 0 0 deprecated.$s-24 deprecated.$s-12; + padding-inline-start: var(--sp-m); + padding-block-end: var(--sp-xxl); display: grid; grid-auto-rows: max-content; - gap: deprecated.$s-4; - height: 100%; -} - -.file-name { - @include t.use-typography("body-small"); - - display: flex; - justify-content: flex-start; - align-items: center; - flex-grow: 100; - height: 100%; + gap: var(--sp-xs); + block-size: 100%; } .loading { - @include t.use-typography("body-small"); + @include use-typography("body-small"); display: flex; align-items: center; justify-content: flex-start; - height: deprecated.$s-32; - padding-left: calc(deprecated.$s-12 + deprecated.$s-2); + block-size: $sz-32; + padding-inline-start: calc(var(--sp-m) + var(--sp-xxs)); color: var(--color-foreground-secondary); } .special-title { - @include deprecated.text-ellipsis; - - color: var(--title-foreground-color-hover); - margin-left: deprecated.$s-2; + box-sizing: border-box; + inline-size: 100%; + min-inline-size: 0; + display: flex; + align-items: center; + gap: var(--sp-xs); + color: var(--color-foreground-primary); + padding-inline-start: var(--sp-xxs); text-align: left; } +.special-title-text { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.tokens-source-icon-wrapper { + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border: $b-1 solid var(--color-accent-select); + border-radius: $br-4; + padding: var(--sp-xxs); +} + +.tokens-source-icon { + flex-shrink: 0; + color: var(--color-accent-tertiary); +} + .file-link { - @extend %button-tertiary; + --file-link-background-color: transparent; + --file-link-border-color: transparent; + --file-link-foreground-color: var(--color-foreground-secondary); - height: deprecated.$s-32; - width: deprecated.$s-28; - border-radius: deprecated.$br-8; + display: grid; + place-content: center; + block-size: $sz-32; + inline-size: $sz-28; + border: $b-2 solid var(--file-link-border-color); + border-radius: $br-8; + background-color: var(--file-link-background-color); + color: var(--file-link-foreground-color); + cursor: pointer; - svg { - @extend %button-icon; + &:hover { + --file-link-background-color: var(--color-background-quaternary); + --file-link-border-color: var(--color-background-quaternary); + --file-link-foreground-color: var(--color-accent-primary); + } - stroke: var(--icon-foreground); - fill: var(--title-foreground-color-hover); + &:active { + --file-link-background-color: var(--color-background-secondary); + --file-link-border-color: transparent; + --file-link-foreground-color: var(--color-accent-primary); + + outline: none; + } + + &:focus-visible { + --file-link-background-color: var(--color-background-tertiary); + --file-link-foreground-color: var(--color-foreground-primary); + + outline: none; + border: $b-1 solid var(--color-accent-primary); } } .library-content { - width: 100%; + inline-size: 100%; display: grid; grid-auto-rows: max-content; - gap: deprecated.$s-4; + gap: var(--sp-xs); } .asset-title { - margin-left: deprecated.$s-28; + margin-inline-start: px2rem(28); display: flex; flex-direction: column; align-items: center; - gap: deprecated.$s-8; + gap: var(--sp-s); } -.no-found-icon { - @include deprecated.flex-center; - - background-color: var(--not-found-background-color); - border-radius: deprecated.$br-circle; - height: deprecated.$s-48; - width: deprecated.$s-48; - - svg { - @extend %button-icon; - - height: deprecated.$s-24; - width: deprecated.$s-24; - stroke: var(--not-found-foreground-color); - } +.no-found-icon-wrapper { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--color-background-tertiary); + border-radius: $br-circle; + block-size: $sz-48; + inline-size: $sz-48; + color: var(--color-foreground-secondary); } .no-found-text { - @include deprecated.body-small-typography; + @include use-typography("body-small"); - color: var(--not-found-foreground-color); + color: var(--color-foreground-secondary); } diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs index 8d40408436..5ad9cae78e 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs @@ -15,6 +15,7 @@ [app.main.data.workspace :as dw] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.texts :as dwt] + [app.main.data.workspace.texts-events :as dwte] [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] @@ -170,7 +171,7 @@ (mf/deps file-id prefix) (fn [_] (st/emit! (dw/set-assets-section-open file-id :typographies true) - (dwt/add-typography file-id prefix))))] + (dwte/add-typography file-id prefix))))] [:div {:class (stl/css :typographies-group) :on-drag-enter on-drag-enter @@ -278,7 +279,7 @@ (mf/deps file-id) (fn [_] (st/emit! (dw/set-assets-section-open file-id :typographies true)) - (st/emit! (dwt/add-typography file-id)))) + (st/emit! (dwte/add-typography file-id)))) handle-change (mf/use-fn diff --git a/frontend/src/app/main/ui/workspace/tokens/management.cljs b/frontend/src/app/main/ui/workspace/tokens/management.cljs index 7281679549..c50c8e0d58 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management.cljs @@ -5,6 +5,7 @@ [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] @@ -49,21 +50,18 @@ (mf/defc selected-set-info* {::mf/private true} - [{:keys [tokens-lib selected-token-set-id]}] + [{: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)))) - active-token-sets-names - (mf/with-memo [tokens-lib] - (some-> tokens-lib (ctob/get-active-themes-set-names))) - token-set-active? (mf/use-fn - (mf/deps active-token-sets-names) - (fn [name] - (contains? active-token-sets-names name)))] + (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" @@ -71,7 +69,7 @@ :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? (ctob/get-name selected-token-set)))) + (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 @@ -84,7 +82,7 @@ (mf/defc tokens-section* {::mf/private true} - [{:keys [tokens-lib active-tokens resolved-active-tokens]}] + [{:keys [tokens-lib tokens-status active-tokens resolved-active-tokens]}] (let [objects (mf/deref refs/workspace-page-objects) selected (mf/deref refs/selected-shapes) @@ -305,6 +303,7 @@ :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] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index 99af4965fd..b9c114bf67 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -19,6 +19,7 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.context :as ctx] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.hooks :as hooks] [app.util.clipboard :as clipboard] @@ -346,31 +347,39 @@ (defn default-actions [{:keys [token selected-token-set-id on-delete-token errors]}] (let [{:keys [modal]} (dwta/get-token-properties token) + + can-edit-tokens? + (mf/use-ctx ctx/can-edit-tokens?) + on-copy-name #(clipboard/to-clipboard (:name token)) on-duplicate-token #(st/emit! (dwtl/duplicate-token (:id token)))] - [{:title (tr "workspace.tokens.edit") - :no-selectable true - :action (fn [event] - (let [{:keys [key fields]} modal] - (dom/stop-propagation event) - (st/emit! (dwtl/assign-token-context-menu nil) - (modal/show key {:x (.-clientX ^js event) - :y (.-clientY ^js event) - :position :right - :fields fields - :initial-errors errors - :action "edit" - :selected-token-set-id selected-token-set-id - :token token}))))} - {:title (tr "workspace.tokens.duplicate") - :no-selectable true - :action on-duplicate-token} - {:title (tr "workspace.tokens.copy-name") - :no-selectable true - :action on-copy-name} - {:title (tr "workspace.tokens.delete") - :no-selectable true - :action #(on-delete-token token)}])) + (concat + [] + (when can-edit-tokens? + [{:title (tr "workspace.tokens.edit") + :no-selectable true + :action (fn [event] + (let [{:keys [key fields]} modal] + (dom/stop-propagation event) + (st/emit! (dwtl/assign-token-context-menu nil) + (modal/show key {:x (.-clientX ^js event) + :y (.-clientY ^js event) + :position :right + :fields fields + :initial-errors errors + :action "edit" + :selected-token-set-id selected-token-set-id + :token token}))))} + {:title (tr "workspace.tokens.duplicate") + :no-selectable true + :action on-duplicate-token}]) + [{:title (tr "workspace.tokens.copy-name") + :no-selectable true + :action on-copy-name}] + (when can-edit-tokens? + [{:title (tr "workspace.tokens.delete") + :no-selectable true + :action #(on-delete-token token)}])))) (defn- allowed-shape-attributes [shapes] diff --git a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs index 4872c116d9..a658001be1 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/group.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/group.cljs @@ -92,8 +92,8 @@ (not (and (some? edition) (= :text (:type (get objects edition)))))) - can-edit? - (mf/use-ctx ctx/can-edit?) + can-edit-tokens? + (mf/use-ctx ctx/can-edit-tokens?) is-selected-inside-layout (d/nilv is-selected-inside-layout false) @@ -177,7 +177,7 @@ :aria-controls (dm/str "token-tree-" (name type)) :on-toggle-expand on-toggle-open-click :icon (token-section-icon type)} - (when can-edit? + (when can-edit-tokens? [:> icon-button* {:id (str "add-token-button-" title) :icon "add" :aria-label (tr "workspace.tokens.add-token" title) diff --git a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs index c7528941bb..7930f784fc 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/token_tree.cljs @@ -140,14 +140,17 @@ on-pill-context-menu on-node-context-menu]}] (let [separator "." - raw-tree (mf/with-memo [tokens] - (cpn/build-tree-root tokens separator)) - permissions (mf/use-ctx ctx/permissions) - can-edit? (:can-edit permissions) + raw-tree + (mf/with-memo [tokens] + (cpn/build-tree-root tokens separator)) + + can-edit-file? + (mf/use-ctx ctx/can-edit?) + on-node-context-menu (mf/use-fn - (mf/deps can-edit? on-node-context-menu) + (mf/deps can-edit-file? on-node-context-menu) (fn [event node] - (when can-edit? + (when can-edit-file? (on-node-context-menu event node)))) ordered-nodes (mf/with-memo [raw-tree] diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index 2e3bc6478d..fd5f41f6ed 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.tokens.sets (:require [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.refs :as refs] [app.main.store :as st] @@ -19,27 +20,32 @@ (st/emit! (dwtl/clear-tokens-paths) (dwtl/set-selected-token-set-id id))) -(defn- on-toggle-token-set-click [name] - (st/emit! (dwtl/toggle-token-set name))) +(defn- on-toggle-token-set-click [id] + (st/emit! (dwtl/toggle-token-set id))) (defn- on-toggle-token-set-group-click [path] (st/emit! (dwtl/toggle-token-set-group path))) (mf/defc sets-list* - [{:keys [tokens-lib selected new-path edition-id]}] + [{:keys [selected new-path edition-id]}] - (let [token-sets + (let [tokens-lib + (mf/use-ctx ctx/tokens-lib) + + tokens-status + (mf/use-ctx ctx/tokens-status) + + token-sets (some-> tokens-lib (ctob/get-set-tree)) - can-edit? - (mf/use-ctx ctx/can-edit?) + can-edit-tokens? + (mf/use-ctx ctx/can-edit-tokens?) token-set-active? (mf/use-fn - (mf/deps tokens-lib) - (fn [name] - (when tokens-lib - (ctob/token-set-active? tokens-lib name)))) + (mf/deps tokens-status) + (fn [set-id] + (ctos/set-active? tokens-status set-id))) token-set-group-active? (mf/use-fn @@ -49,17 +55,17 @@ on-reset-edition (mf/use-fn - (mf/deps can-edit?) + (mf/deps can-edit-tokens?) (fn [_] - (when can-edit? + (when can-edit-tokens? (st/emit! (dwtl/clear-token-set-edition) (dwtl/clear-token-set-creation))))) on-start-edition (mf/use-fn - (mf/deps can-edit?) + (mf/deps can-edit-tokens?) (fn [id] - (when can-edit? + (when can-edit-tokens? (st/emit! (dwtl/start-token-set-edition id)))))] [:> controlled-sets-list* @@ -75,7 +81,6 @@ :edition-id edition-id :origin "set-panel" - :can-edit can-edit? :on-start-edition on-start-edition :on-reset-edition on-reset-edition diff --git a/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs b/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs index 4b778941b4..0004502661 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets/lists.cljs @@ -86,8 +86,8 @@ (mf/defc inline-add-button* [] - (let [can-edit? (mf/use-ctx ctx/can-edit?)] - (if can-edit? + (let [can-edit-tokens? (mf/use-ctx ctx/can-edit-tokens?)] + (if can-edit-tokens? [:div {:class (stl/css :empty-sets-wrapper)} [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} (tr "workspace.tokens.no-sets-yet")] @@ -110,7 +110,10 @@ [{:keys [id label is-editing is-active is-selected is-draggable is-collapsed path depth index on-toggle on-drop on-start-edition on-reset-edition on-edit-submit on-toggle-collapse]}] - (let [can-edit? + (let [can-edit-tokens? + (mf/use-ctx ctx/can-edit-tokens?) + + can-edit-file? (mf/use-ctx ctx/can-edit?) label-id @@ -118,11 +121,11 @@ on-context-menu (mf/use-fn - (mf/deps is-editing id path can-edit?) + (mf/deps is-editing id path can-edit-tokens?) (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (when (and can-edit? (not is-editing)) + (when (and can-edit-tokens? (not is-editing)) (st/emit! (dwtl/assign-token-set-context-menu {:position (dom/get-client-position event) :is-group true @@ -141,12 +144,12 @@ on-checkbox-click (mf/use-fn - (mf/deps on-toggle path can-edit?) + (mf/deps on-toggle path can-edit-file?) #(on-toggle path)) on-edit-submit' (mf/use-fn - (mf/deps path on-edit-submit can-edit?) + (mf/deps path on-edit-submit can-edit-tokens?) #(on-edit-submit path %)) on-drop @@ -196,7 +199,7 @@ label] [:> checkbox* {:on-click on-checkbox-click - :disabled (not can-edit?) + :disabled (not can-edit-file?) :checked (case is-active :all true :partial "mixed" @@ -207,7 +210,11 @@ [{:keys [id set label is-editing is-active is-selected is-draggable is-new path depth index on-select on-toggle on-drop on-start-edition on-reset-edition on-edit-submit]}] - (let [can-edit? (mf/use-ctx ctx/can-edit?) + (let [can-edit-tokens? + (mf/use-ctx ctx/can-edit-tokens?) + + can-edit-file? + (mf/use-ctx ctx/can-edit?) on-click (mf/use-fn @@ -220,11 +227,11 @@ on-context-menu (mf/use-fn - (mf/deps is-editing id path can-edit?) + (mf/deps is-editing id path can-edit-tokens?) (fn [event] (dom/prevent-default event) (dom/stop-propagation event) - (when (and can-edit? (not is-editing)) + (when (and can-edit-tokens? (not is-editing)) (st/emit! (dwtl/assign-token-set-context-menu {:position (dom/get-client-position event) :is-group false @@ -244,7 +251,7 @@ (fn [event] (dom/stop-propagation event) (when (fn? on-toggle) - (on-toggle (ctob/get-name set))))) + (on-toggle (ctob/get-id set))))) on-edit-submit' (mf/use-fn @@ -305,15 +312,15 @@ label] [:> checkbox* {:on-click on-checkbox-click - :disabled (not can-edit?) + :disabled (not can-edit-file?) :arial-label (tr "workspace.tokens.select-set") :checked is-active}]])])) (mf/defc token-sets-tree* [{:keys [is-draggable selected - is-token-set-group-active is-token-set-active + is-token-set-group-active on-start-edition on-reset-edition on-edit-submit-set @@ -418,7 +425,7 @@ :set token-set :label (peek path) :is-editing (= edition-id id) - :is-active (is-token-set-active (ctob/get-name token-set)) + :is-active (is-token-set-active id) :is-selected (= selected id) :is-draggable is-draggable :is-new false @@ -452,15 +459,15 @@ new-path edition-id]}] - (assert (fn? is-token-set-group-active) "expected a function for `is-token-set-group-active` prop") (assert (fn? is-token-set-active) "expected a function for `is-token-set-active` prop") + (assert (fn? is-token-set-group-active) "expected a function for `is-token-set-group-active` prop") - (let [theme-modal? (= origin "theme-modal") - can-edit? (mf/use-ctx ctx/can-edit?) - draggable? (and (not theme-modal?) can-edit?) - empty-state? (and theme-modal? - (empty? token-sets) - (not new-path)) + (let [theme-modal? (= origin "theme-modal") + can-edit-tokens? (mf/use-ctx ctx/can-edit-tokens?) + draggable? (and (not theme-modal?) can-edit-tokens?) + empty-state? (and theme-modal? + (empty? token-sets) + (not new-path)) ;; NOTE: on-reset-edition and on-start-edition function can ;; come as nil, in this case we need to provide a safe diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index 4d2f77863f..d14f4913cc 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.workspace.tokens.sidebar (:require-macros [app.main.style :as stl]) (:require + [app.common.files.tokens :as cfo] [app.common.types.tokens-lib :as ctob] [app.config :as cf] [app.main.data.modal :as modal] @@ -25,6 +26,7 @@ [app.main.ui.workspace.tokens.sets.context-menu :refer [token-set-context-menu*]] [app.main.ui.workspace.tokens.sets.lists :as tsetslist] [app.main.ui.workspace.tokens.themes :refer [themes-header*]] + [app.main.ui.workspace.tokens.tokens-source :refer [tokens-source-info*]] [app.util.dom :as dom] [app.util.i18n :refer [tr]] [rumext.v2 :as mf] @@ -34,8 +36,11 @@ (mf/defc token-sets-list* {::mf/private true} - [{:keys [tokens-lib]}] - (let [token-sets + [] + (let [tokens-lib + (mf/use-ctx ctx/tokens-lib) + + token-sets (some-> tokens-lib (ctob/get-set-tree)) selected-token-set-id @@ -60,20 +65,25 @@ (mf/defc token-management-section* {::mf/private true} - [{:keys [resize-height] :as props}] + [{:keys [resize-height current-file-data] :as props}] - (let [can-edit? - (mf/use-ctx ctx/can-edit?)] + (let [can-edit-tokens? + (mf/use-ctx ctx/can-edit-tokens?) + tokens-source + (mf/with-memo [current-file-data] + (cfo/get-effective-tokens-source current-file-data))] [:* [:> token-set-context-menu*] [:section {:data-testid "token-management-sidebar" :class (stl/css :token-management-section-wrapper) :style {"--resize-height" (str resize-height "px")}} - [:> themes-header*] + [:> tokens-source-info* {:tokens-source tokens-source + :file-id (:id current-file-data)}] + [:> themes-header* {:tokens-source tokens-source}] [:div {:class (stl/css :sidebar-header)} [:> title-bar* {:title (tr "labels.sets")} - (when can-edit? + (when can-edit-tokens? [:> tsetslist/add-button*])]] [:> token-sets-list* props]]])) @@ -83,8 +93,8 @@ (let [show-menu* (mf/use-state false) show-menu? (deref show-menu*) - can-edit? - (mf/use-ctx ctx/can-edit?) + can-edit-tokens? + (mf/use-ctx ctx/can-edit-tokens?) open-menu (mf/use-fn @@ -124,7 +134,7 @@ :on-close close-menu :id "tokens-menu" :class (stl/css :import-export-menu)} - (when can-edit? + (when can-edit-tokens? [:> dropdown-menu-item* {:class (stl/css :import-export-menu-item) :on-click on-modal-show} [:div {:class (stl/css :import-menu-item)} @@ -133,31 +143,42 @@ :on-click on-export} (tr "labels.export")]] - - (when (and can-edit? (contains? cf/flags :token-base-font-size)) + (when (and can-edit-tokens? (contains? cf/flags :token-base-font-size)) [:> icon-button* {:variant "secondary" :icon i/settings :aria-label "Settings" :on-click open-settings-modal}])])) (mf/defc tokens-sidebar-tab* - [{:keys [tokens-lib] :as props}] + [{:keys [] :as props}] (let [{on-pointer-down-pages :on-pointer-down on-lost-pointer-capture-pages :on-lost-pointer-capture on-pointer-move-pages :on-pointer-move size-pages-opened :size} - (use-resize-hook :tokens 200 38 "0.6" :y false nil)] + (use-resize-hook :tokens 200 38 "0.6" :y false nil) - [:div {:class (stl/css :sidebar-wrapper)} - [:> token-management-section* - {:resize-height size-pages-opened - :tokens-lib tokens-lib}] - [:article {:class (stl/css :tokens-section-wrapper) - :data-testid "tokens-sidebar"} - [:div {:class (stl/css :resize-area-horiz) - :on-pointer-down on-pointer-down-pages - :on-lost-pointer-capture on-lost-pointer-capture-pages - :on-pointer-move on-pointer-move-pages} - [:div {:class (stl/css :resize-handle-horiz)}]] - [:> tokens-section* props]] - [:> import-export-button*]])) + current-file-data + (mf/deref refs/workspace-data) + + can-edit-file? + (mf/use-ctx ctx/can-edit?) + + can-edit-tokens? + (mf/with-memo [can-edit-file? current-file-data] + (and can-edit-file? + (cfo/editable-tokens? current-file-data)))] + + [:> (mf/provider ctx/can-edit-tokens?) {:value can-edit-tokens?} + [:div {:class (stl/css :sidebar-wrapper)} + [:> token-management-section* + {:resize-height size-pages-opened + :current-file-data current-file-data}] + [:article {:class (stl/css :tokens-section-wrapper) + :data-testid "tokens-sidebar"} + [:div {:class (stl/css :resize-area-horiz) + :on-pointer-down on-pointer-down-pages + :on-lost-pointer-capture on-lost-pointer-capture-pages + :on-pointer-move on-pointer-move-pages} + [:div {:class (stl/css :resize-handle-horiz)}]] + [:> tokens-section* props]] + [:> import-export-button*]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss index 7fcbb8a2e7..4e205d1a3c 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.scss @@ -26,7 +26,7 @@ overflow-y: auto; scrollbar-gutter: stable; position: relative; - padding-block-end: var(--sp-l); + padding-block: var(--sp-m) var(--sp-l); } .tokens-section-wrapper { diff --git a/frontend/src/app/main/ui/workspace/tokens/themes.cljs b/frontend/src/app/main/ui/workspace/tokens/themes.cljs index 7661287472..5c116ca4ad 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/themes.cljs @@ -19,12 +19,12 @@ (mf/defc themes-header* {::mf/private true} - [] + [{:keys [tokens-source]}] (let [ordered-themes (mf/deref refs/workspace-token-themes-no-hidden) - can-edit? - (mf/use-ctx ctx/can-edit?) + can-edit-tokens? + (mf/use-ctx ctx/can-edit-tokens?) open-modal (mf/use-fn @@ -38,18 +38,18 @@ [:div {:class (stl/css :empty-theme-wrapper)} [:> text* {:as "span" :typography "body-small" :class (stl/css :empty-state-message)} (tr "workspace.tokens.no-themes")] - (when can-edit? + (when can-edit-tokens? [:button {:on-click open-modal :class (stl/css :create-theme-button)} (tr "workspace.tokens.create-one")])] - (if can-edit? + (if can-edit-tokens? [:div {:class (stl/css :theme-selector-wrapper)} - [:> theme-selector*] + [:> theme-selector* {:tokens-source tokens-source}] [:> button* {:variant "secondary" :type "button" :class (stl/css :edit-theme-button) :on-click open-modal} (tr "labels.edit")]] - [:div {:title (when-not can-edit? + [:div {:title (when-not can-edit-tokens? (tr "workspace.tokens.no-permission-themes"))} - [:> theme-selector*]]))])) + [:> theme-selector* {:tokens-source tokens-source}]]))])) diff --git a/frontend/src/app/main/ui/workspace/tokens/themes.scss b/frontend/src/app/main/ui/workspace/tokens/themes.scss index 326596b479..c066cd51b0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes.scss +++ b/frontend/src/app/main/ui/workspace/tokens/themes.scss @@ -9,7 +9,7 @@ @use "ds/spacing.scss" as *; .themes-wrapper { - padding: var(--sp-m) 0 0 var(--sp-m); + padding: 0 0 0 var(--sp-m); } .themes-header { diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs index 1e9e8d1ce7..3053141215 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/themes/create_modal.cljs @@ -10,15 +10,16 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.tokens :as cfo] - [app.common.logic.tokens :as clt] [app.common.schema :as sm] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.main.constants :refer [max-input-length]] [app.main.data.event :as ev] [app.main.data.modal :as modal] [app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.context :as ctx] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.controls.combobox :refer [combobox*]] @@ -66,7 +67,7 @@ (mf/defc themes-overview [{:keys [change-view]}] - (let [active-theme-paths (mf/deref refs/workspace-active-theme-paths) + (let [active-theme-ids (mf/deref refs/workspace-active-theme-ids) themes-groups (mf/deref refs/workspace-token-theme-tree-no-hidden) create-theme @@ -95,7 +96,7 @@ [:ul {:class (stl/css :theme-group-rows-wrapper)} (for [[_ {:keys [id name] :as theme}] themes :let [theme-path (ctob/get-theme-path theme) - selected? (some? (get active-theme-paths theme-path)) + selected? (some? (get active-theme-ids (ctob/get-id theme))) delete-theme (fn [e] (dom/prevent-default e) @@ -249,10 +250,15 @@ (defn- make-lib-with-theme [theme sets] - (let [tlib (-> (ctob/make-tokens-lib) - (ctob/add-theme theme)) - tlib (reduce ctob/add-set tlib sets)] - (ctob/activate-theme tlib (ctob/get-id theme)))) + (as-> (ctob/make-tokens-lib) $ + (ctob/add-theme $ theme) + (reduce ctob/add-set $ sets))) + +(defn- make-status-with-theme + [tokens-lib theme] + (let [sets (map #(ctob/get-set-by-name tokens-lib %) (:sets theme))] + (ctos/make-tokens-status :active-theme-ids #{(:id theme)} + :active-set-ids (into #{} (map ctob/get-id sets))))) (mf/defc edit-create-theme* [{:keys [change-view theme on-save is-editing has-prev-view]}] @@ -262,7 +268,8 @@ current-theme* (mf/use-state theme) current-theme (deref current-theme*) - lib (make-lib-with-theme current-theme ordered-token-sets) + lib-with-theme (make-lib-with-theme current-theme ordered-token-sets) + status-with-theme (make-status-with-theme lib-with-theme current-theme) ;; Form / Modal handlers on-back (mf/use-fn @@ -306,35 +313,44 @@ ;; Sets tree handlers token-set-group-active? (mf/use-fn - (mf/deps current-theme) + (mf/deps status-with-theme lib-with-theme) (fn [group-path] - (ctob/sets-at-path-all-active? lib group-path))) + (cfo/sets-at-path-all-active? status-with-theme lib-with-theme group-path))) token-set-active? (mf/use-fn - (mf/deps current-theme) - (fn [name] - (contains? (:sets current-theme) name))) + (mf/deps status-with-theme) + (fn [id] + (ctos/set-active? status-with-theme id))) on-toggle-token-set (mf/use-fn - (mf/deps current-theme) - (fn [set-name] - (swap! current-theme* #(ctob/toggle-set % set-name)))) + (mf/deps current-theme tokens-lib) + (fn [set-id] + (let [set (ctob/get-set tokens-lib set-id)] + (swap! current-theme* #(ctob/toggle-set % (ctob/get-name set)))))) on-toggle-token-set-group (mf/use-fn (mf/deps current-theme ordered-token-sets) (fn [group-path] (swap! current-theme* (fn [theme'] - (let [lib' (make-lib-with-theme theme' ordered-token-sets)] - (clt/toggle-token-set-group group-path lib' theme')))))) + ; TODOstatus ver si se puede añadir una función en files.tokens o types.tokens + ; según si necesitamos al final el status o no (que creo que no) + (let [lib' (make-lib-with-theme theme' ordered-token-sets) + status' (make-status-with-theme lib' theme') + active? (cfo/sets-at-path-all-active? status' lib' group-path) + sets-at-path (ctob/get-sets-at-path lib' group-path) + set-names (into #{} (map ctob/get-name) sets-at-path)] + (if (contains? #{:all :partial} active?) + (ctob/disable-sets theme' set-names) + (ctob/enable-sets theme' set-names))))))) on-click-token-set (mf/use-fn (mf/deps on-toggle-token-set) (fn [set-id] - (let [set (ctob/get-set lib set-id)] + (let [set (ctob/get-set lib-with-theme set-id)] (on-toggle-token-set (ctob/get-name set)))))] [:div {:class (stl/css :themes-modal-wrapper)} @@ -450,12 +466,14 @@ ::mf/register modal/components ::mf/register-as :tokens/themes} [] - [:div {:class (stl/css :modal-overlay)} - [:div {:class (stl/css :modal-dialog) - :data-testid "token-theme-update-create-modal"} - [:> icon-button* {:class (stl/css :close-btn) - :on-click modal/hide! - :aria-label (tr "labels.close") - :variant "action" - :icon i/close}] - [:> themes-modal-body*]]]) + (let [tokens-status (mf/deref refs/tokens-status)] + [:> (mf/provider ctx/tokens-status) {:value tokens-status} + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog) + :data-testid "token-theme-update-create-modal"} + [:> icon-button* {:class (stl/css :close-btn) + :on-click modal/hide! + :aria-label (tr "labels.close") + :variant "action" + :icon i/close}] + [:> themes-modal-body*]]]])) diff --git a/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.cljs b/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.cljs index 65c50373c8..6282c77b69 100644 --- a/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/themes/theme_selector.cljs @@ -8,13 +8,16 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] + [app.common.files.tokens :as cfo] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [app.main.data.modal :as modal] [app.main.data.workspace.tokens.library-edit :as dwtl] - [app.main.refs :as refs] + [app.main.router :as rt] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.context :as ctx] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.hooks :as hooks] @@ -24,17 +27,16 @@ [rumext.v2 :as mf])) (mf/defc themes-list* - [{:keys [themes active-theme-paths on-close is-grouped]}] + [{:keys [themes tokens-status on-close is-grouped]}] (when (seq themes) [:ul {:class (stl/css :theme-options)} (for [[_ {:keys [id name] :as theme}] themes - :let [theme-id (ctob/get-theme-path theme) - selected? (get active-theme-paths theme-id) + :let [selected? (ctos/theme-active? tokens-status id) select-theme (fn [e] (dom/stop-propagation e) (st/emit! (dwtl/toggle-token-theme-active id)) (on-close))]] - [:li {:key theme-id + [:li {:key (str "theme-" id) :role "option" :aria-selected selected? :class (stl/css-case @@ -48,47 +50,68 @@ :class (stl/css-case :check-icon true :check-icon-visible selected?)}]])])) -(defn- open-tokens-theme-modal - [] - (modal/show! :tokens/themes {})) - (mf/defc theme-options* - [{:keys [active-theme-paths themes on-close]}] - [:ul {:class (stl/css :theme-options :custom-select-dropdown) - :role "listbox"} - (for [[group themes] themes] - [:li {:key group - :aria-labelledby (dm/str group "-label") - :role "group"} - (when (seq group) - [:> text* {:as "span" :typography "headline-small" :class (stl/css :group) :id (dm/str (str/kebab group) "-label") :title group} group]) - [:> themes-list* {:themes themes - :active-theme-paths active-theme-paths - :on-close on-close - :is-grouped true}]]) - [:li {:class (stl/css :separator) - :aria-hidden true}] - [:li {:class (stl/css-case :checked-element true - :checked-element-button true) - :role "option" - :on-click open-tokens-theme-modal} - [:> text* {:as "span" :typography "body-small"} (tr "workspace.tokens.edit-themes")] - [:> icon* {:icon-id i/arrow-right :aria-hidden true}]]]) + [{:keys [tokens-lib tokens-status tokens-source can-edit-tokens team-id on-close]}] + (let [themes + (mf/with-memo [tokens-lib] + (ctob/get-theme-tree-no-hidden tokens-lib)) + + edit-token-themes + (fn [] + (if can-edit-tokens + (modal/show! :tokens/themes {}) + (st/emit! (rt/nav :workspace + {:team-id team-id + :file-id tokens-source + :layout :tokens} + ::rt/new-window true))))] + + [:ul {:class (stl/css :theme-options :custom-select-dropdown) + :role "listbox"} + (for [[group themes] themes] + [:li {:key group + :aria-labelledby (dm/str group "-label") + :role "group"} + (when (seq group) + [:> text* {:as "span" :typography "headline-small" :class (stl/css :group) :id (dm/str (str/kebab group) "-label") :title group} group]) + [:> themes-list* {:themes themes + :tokens-status tokens-status + :on-close on-close + :is-grouped true}]]) + [:li {:class (stl/css :separator) + :aria-hidden true}] + [:li {:class (stl/css-case :checked-element true + :checked-element-button true) + :role "option" + :on-click edit-token-themes} + [:> text* {:as "span" :typography "body-small"} (tr "workspace.tokens.edit-themes")] + [:> icon* {:icon-id (if can-edit-tokens + i/arrow-right + i/open-link) + :aria-hidden true}]]])) (mf/defc theme-selector* - [{:keys []}] + [{:keys [tokens-source]}] (let [;; Store - active-theme-paths (mf/deref refs/workspace-active-theme-paths-no-hidden) - active-themes-count (count active-theme-paths) - themes (mf/deref refs/workspace-token-theme-tree-no-hidden) - can-edit? (:can-edit (deref refs/permissions)) + tokens-lib (mf/use-ctx ctx/tokens-lib) + tokens-status (mf/use-ctx ctx/tokens-status) + can-edit-file? (mf/use-ctx ctx/can-edit?) + can-edit-tokens (mf/use-ctx ctx/can-edit-tokens?) + team-id (mf/use-ctx ctx/current-team-id) + + active-themes + (mf/with-memo [tokens-lib tokens-status] + (cfo/get-active-themes tokens-status tokens-lib)) + + active-themes-count + (mf/with-memo [active-themes] + (count active-themes)) + ;; Data current-label (cond (> active-themes-count 1) (tr "workspace.tokens.active-themes" active-themes-count) - (= active-themes-count 1) (some->> (first active-theme-paths) - (ctob/split-theme-path) - (remove empty?) - (str/join " / ")) + (= active-themes-count 1) (-> (first active-themes) + (ctob/get-theme-path true)) :else (tr "workspace.tokens.no-active-theme")) ;; State @@ -105,9 +128,9 @@ on-open-dropdown (mf/use-fn - (mf/deps can-edit?) + (mf/deps can-edit-file?) (fn [event] - (when can-edit? + (when can-edit-file? (when-let [node (dom/get-current-target event)] (let [rect (dom/get-bounding-rect node)] (swap! state* assoc @@ -117,14 +140,14 @@ container (hooks/use-portal-container :popup)] [:div {:on-click on-open-dropdown - :disabled (not can-edit?) + :disabled (not can-edit-file?) :aria-expanded is-open? :aria-haspopup "listbox" :tab-index "0" :role "combobox" :data-testid "theme-select" :class (stl/css-case :custom-select true - :disabled-select (not can-edit?))} + :disabled-select (not can-edit-file?))} [:> text* {:as "span" :typography "body-small" :class (stl/css :current-label)} current-label] [:> icon* {:icon-id i/arrow-down :class (stl/css :dropdown-button) :aria-hidden true}] @@ -140,7 +163,10 @@ [:& dropdown {:show is-open? :on-close on-close-dropdown} - [:> theme-options* {:active-theme-paths active-theme-paths - :themes themes + [:> theme-options* {:tokens-lib tokens-lib + :tokens-status tokens-status + :tokens-source tokens-source + :can-edit-tokens can-edit-tokens + :team-id team-id :on-close on-close-dropdown}]]]) container))])) diff --git a/frontend/src/app/main/ui/workspace/tokens/tokens_source.cljs b/frontend/src/app/main/ui/workspace/tokens/tokens_source.cljs new file mode 100644 index 0000000000..4f95c7521e --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/tokens_source.cljs @@ -0,0 +1,104 @@ +;; 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 Sucursal en España SL + +(ns app.main.ui.workspace.tokens.tokens-source + (:require-macros [app.main.style :as stl]) + (:require + [app.main.data.modal :as modal] + [app.main.refs :as refs] + [app.main.router :as rt] + [app.main.store :as st] + [app.main.ui.context :as ctx] + [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.foundations.typography.text :refer [text*]] + [app.main.ui.ds.tooltip.tooltip :refer [tooltip*]] + [app.util.i18n :refer [tr]] + [rumext.v2 :as mf])) + +(mf/defc tokens-source-info* + {::mf/private true} + [{:keys [tokens-source file-id] :as props}] + + (let [files (mf/deref refs/files) + tokens-source-file (get files tokens-source) + source-file-id (:id tokens-source-file) + file-name (:name tokens-source-file) + + file-name-ref (mf/use-ref nil) + file-name-truncated?* (mf/use-state false) + file-name-truncated? (deref file-name-truncated?*) + + check-file-name-truncated + (mf/use-fn + (fn [] + (when-let [node (mf/ref-val file-name-ref)] + (reset! file-name-truncated?* + (> (.-scrollWidth node) (.-clientWidth node)))))) + + team-id + (mf/use-ctx ctx/current-team-id) + + open-library-new-window + (mf/use-fn + (fn [] + (st/emit! (rt/nav :workspace + {:team-id team-id + :file-id tokens-source + :layout :tokens} + ::rt/new-window true)))) + show-libraries-dialog + (mf/use-fn + (mf/deps file-id) + (fn [] + (modal/show! :libraries-dialog {:file-id file-id})))] + + (mf/with-effect [file-name] + (check-file-name-truncated) + (when-let [node (mf/ref-val file-name-ref)] + (let [ro (js/ResizeObserver. check-file-name-truncated)] + (.observe ro node) + #(.disconnect ro)))) + + [:div {:class (stl/css :tokens-source-wrapper)} + [:div {:class (stl/css :tokens-source-header)} + [:> icon* {:icon-id "tokens" + :size "m" + :class (stl/css :tokens-source-icon)}] + [:> text* {:as "span" + :typography "body-small" + :class (stl/css :tokens-source-text)} + (tr "workspace.tokens.source")] + (if (= source-file-id file-id) + [:> text* {:as "span" + :typography "body-small" + :class (stl/css :this-file)} + (tr "workspace.tokens.this-file")] + (if file-name-truncated? + [:> tooltip* {:content file-name + :trigger-ref file-name-ref + :class (stl/css :file-name-tooltip)} + [:> text* {:as "span" + :typography "body-small" + :class (stl/css :file-name) + :ref file-name-ref} + file-name]] + [:> text* {:as "span" + :typography "body-small" + :class (stl/css :file-name) + :ref file-name-ref} + file-name]))] + [:> icon-button* + {:variant "ghost" + :aria-label (tr "workspace.tokens.change-token-source") + :on-click show-libraries-dialog + :icon "switch"}] + (when-not (= source-file-id file-id) + [:> icon-button* + {:variant "ghost" + :aria-label (tr "workspace.tokens.open-source-new-tab") + :on-click open-library-new-window + :icon "open-link"}])])) diff --git a/frontend/src/app/main/ui/workspace/tokens/tokens_source.scss b/frontend/src/app/main/ui/workspace/tokens/tokens_source.scss new file mode 100644 index 0000000000..0b6dfed9dd --- /dev/null +++ b/frontend/src/app/main/ui/workspace/tokens/tokens_source.scss @@ -0,0 +1,77 @@ +// 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 Sucursal en España SL + +@use "ds/typography.scss" as *; +@use "ds/_sizes" as *; +@use "ds/_borders.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/mixins.scss" as *; + +// TODO: this must be a custom property in the design system +:global(.light) { + --low-emphasis-background: #fafafa; +} + +:global(.default) { + --low-emphasis-background: #121214; +} + +.tokens-source-wrapper { + display: flex; + padding: 0 0 0 var(--sp-m); + background-color: var(--low-emphasis-background); +} + +.tokens-source-header { + display: flex; + align-items: center; + flex-wrap: nowrap; + gap: var(--sp-s); + flex-grow: 1; + min-width: 0; + padding: var(--sp-xs); +} + +.tokens-source-icon { + flex-shrink: 0; + color: var(--color-foreground-secondary); +} + +.tokens-source-text { + flex-shrink: 0; + color: var(--color-foreground-secondary); +} + +.file-name, +.this-file { + flex: 1 1 auto; + min-width: 0; + color: var(--color-foreground-primary); +} + +.file-name { + @include text-ellipsis; +} + +.file-name-tooltip { + flex: 1 1 auto; + min-width: 0; +} + +.this-file { + font-style: italic; + color: var(--color-foreground-secondary); + overflow-wrap: break-word; +} + +.tokens-source-description { + flex-shrink: 0; + color: var(--color-foreground-secondary); + border: $b-1 solid var(--color-foreground-secondary); + padding: var(--sp-xxs) px2rem(6); + border-radius: $br-6; +} diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs index 35ff505a65..234a470b8c 100644 --- a/frontend/src/app/plugins/library.cljs +++ b/frontend/src/app/plugins/library.cljs @@ -1213,7 +1213,7 @@ (let [file-id (:current-file-id @st/state) library-id (uuid/parse library-id)] (->> st/stream - (rx/filter (ptk/type? ::dwl/attach-library-finished)) + (rx/filter (ptk/type? ::dwl/link-file-to-library-finished)) (rx/take 1) (rx/subs! #(resolve (library-proxy plugin-id library-id)) reject)) (st/emit! (-> (dwl/link-file-to-library file-id library-id) diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index b27019c7ba..922ae739d4 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -10,8 +10,10 @@ [app.common.files.tokens :as cfo] [app.common.json :as json] [app.common.schema :as sm] + [app.common.types.file :as ctf] [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [app.main.data.tokenscript :as ts] [app.main.data.workspace.tokens.application :as dwta] @@ -216,8 +218,10 @@ (u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission") :else - (st/emit! (-> (dwtl/update-token set-id id {:name value}) - (se/add-event plugin-id)))))} + (do + (u/check-editable-tokens file-id) + (st/emit! (-> (dwtl/update-token set-id id {:name value}) + (se/add-event plugin-id))))))} :type {:this true @@ -247,11 +251,13 @@ (u/not-valid plugin-id :value "Plugin doesn't have 'content:write' permission") :else - (let [token (u/locate-token file-id set-id id) - value (cond-> value - (= :font-family (:type token)) - (ctob/convert-dtcg-font-family))] - (st/emit! (dwtl/update-token set-id id {:value value})))))} + (do + (u/check-editable-tokens file-id) + (let [token (u/locate-token file-id set-id id) + value (cond-> value + (= :font-family (:type token)) + (ctob/convert-dtcg-font-family))] + (st/emit! (dwtl/update-token set-id id {:value value}))))))} :resolvedValue {:this true @@ -260,7 +266,8 @@ (fn [_] (let [token (u/locate-token file-id set-id id) tokens-lib (u/locate-tokens-lib file-id) - tokens-tree (ctob/get-tokens-in-active-sets tokens-lib)] + tokens-status (u/locate-tokens-status file-id) + tokens-tree (cfo/get-tokens-in-active-sets tokens-status tokens-lib)] (get-resolved-value token tokens-tree)))} :resolvedValueString @@ -270,7 +277,8 @@ (fn [_] (let [token (u/locate-token file-id set-id id) tokens-lib (u/locate-tokens-lib file-id) - tokens-tree (ctob/get-tokens-in-active-sets tokens-lib)] + tokens-status (u/locate-tokens-status file-id) + tokens-tree (cfo/get-tokens-in-active-sets tokens-status tokens-lib)] (str (get-resolved-value token tokens-tree))))} :description @@ -287,8 +295,10 @@ (u/not-valid plugin-id :description "Plugin doesn't have 'content:write' permission") :else - (st/emit! (-> (dwtl/update-token set-id id {:description value}) - (se/add-event plugin-id)))))} + (do + (u/check-editable-tokens file-id) + (st/emit! (-> (dwtl/update-token set-id id {:description value}) + (se/add-event plugin-id))))))} :duplicate (fn [] @@ -302,13 +312,15 @@ ;; - use this function in dwtl/duplicate-token ;; - return the new token proxy using the locally forced id ;; - do the same with sets and themes - (let [token (u/locate-token file-id set-id id) - token' (ctob/make-token (-> (datafy token) - (dissoc :id - :modified-at)))] - (st/emit! (-> (dwtl/create-token set-id token') - (se/add-event plugin-id))) - (token-proxy plugin-id file-id set-id (:id token'))))) + (do + (u/check-editable-tokens file-id) + (let [token (u/locate-token file-id set-id id) + token' (ctob/make-token (-> (datafy token) + (dissoc :id + :modified-at)))] + (st/emit! (-> (dwtl/create-token set-id token') + (se/add-event plugin-id))) + (token-proxy plugin-id file-id set-id (:id token')))))) :remove (fn [] @@ -317,8 +329,10 @@ (u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission") :else - (st/emit! (-> (dwtl/delete-token set-id id) - (se/add-event plugin-id))))) + (do + (u/check-editable-tokens file-id) + (st/emit! (-> (dwtl/delete-token set-id id) + (se/add-event plugin-id)))))) :applyToShapes {:enumerable false @@ -374,17 +388,18 @@ (u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission") :else - (let [set (u/locate-token-set file-id id)] - (st/emit! (dwtl/rename-token-set set name)))))} + (do + (u/check-editable-tokens file-id) + (let [set (u/locate-token-set file-id id)] + (st/emit! (dwtl/rename-token-set set name))))))} :active {:this true :enumerable false :get (fn [_] - (let [tokens-lib (u/locate-tokens-lib file-id) - set (u/locate-token-set file-id id)] - (ctob/token-set-active? tokens-lib (ctob/get-name set)))) + (let [tokens-status (u/locate-tokens-status file-id)] + (ctos/set-active? tokens-status id))) :schema ::sm/boolean :set (fn [_ value] @@ -393,8 +408,7 @@ (u/not-valid plugin-id :active "Plugin doesn't have 'content:write' permission") :else - (let [set (u/locate-token-set file-id id)] - (st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) value)))))} + (st/emit! (dwtl/set-enabled-token-set id value))))} ;; This can be done even with tokens in an external library :toggleActive (fn [] @@ -403,8 +417,7 @@ (u/not-valid plugin-id :toggleActive "Plugin doesn't have 'content:write' permission") :else - (let [set (u/locate-token-set file-id id)] - (st/emit! (dwtl/toggle-token-set (ctob/get-name set)))))) + (st/emit! (dwtl/toggle-token-set id)))) :tokens {:this true @@ -468,28 +481,30 @@ (u/not-valid plugin-id :addToken "Plugin doesn't have 'content:write' permission") :else - (let [tokens-lib (u/locate-tokens-lib file-id) - token (ctob/make-token attrs) - ;; Resolve against all tokens in the library (including those - ;; in inactive sets) so that references to structurally - ;; existing tokens resolve even if their set is not active. - ;; The target set's tokens take precedence over equally named - ;; tokens in other sets, and the new token takes precedence - ;; over all. - tokens-tree (-> (merge (ctob/get-all-tokens-map tokens-lib) - (ctob/get-tokens tokens-lib id)) - (assoc (:name token) token)) - resolved-tokens (ts/resolve-tokens tokens-tree) + (do + (u/check-editable-tokens file-id) + (let [tokens-lib (u/locate-tokens-lib file-id) + token (ctob/make-token attrs) + ;; Resolve against all tokens in the library (including those + ;; in inactive sets) so that references to structurally + ;; existing tokens resolve even if their set is not active. + ;; The target set's tokens take precedence over equally named + ;; tokens in other sets, and the new token takes precedence + ;; over all. + tokens-tree (-> (merge (ctob/get-all-tokens-map tokens-lib) + (ctob/get-tokens tokens-lib id)) + (assoc (:name token) token)) + resolved-tokens (ts/resolve-tokens tokens-tree) - {:keys [errors resolved-value] :as resolved-token} - (get resolved-tokens (:name token))] + {:keys [errors resolved-value] :as resolved-token} + (get resolved-tokens (:name token))] - (if resolved-value - (do (st/emit! (-> (dwtl/create-token id token) - (se/add-event plugin-id))) - (token-proxy plugin-id file-id id (:id token))) - (do (u/not-valid plugin-id :addToken (str errors)) - nil)))))} + (if resolved-value + (do (st/emit! (-> (dwtl/create-token id token) + (se/add-event plugin-id))) + (token-proxy plugin-id file-id id (:id token))) + (do (u/not-valid plugin-id :addToken (str errors)) + nil))))))} :duplicate (fn [] @@ -498,10 +513,12 @@ (u/not-valid plugin-id :duplicate "Plugin doesn't have 'content:write' permission") :else - (let [id-ref (atom nil)] - (st/emit! (dwtl/duplicate-token-set id {:id-ref id-ref})) - (when (some? @id-ref) - (token-set-proxy plugin-id file-id @id-ref))))) + (do + (u/check-editable-tokens file-id) + (let [id-ref (atom nil)] + (st/emit! (dwtl/duplicate-token-set id {:id-ref id-ref})) + (when (some? @id-ref) + (token-set-proxy plugin-id file-id @id-ref)))))) :remove (fn [] @@ -510,7 +527,9 @@ (u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission") :else - (st/emit! (dwtl/delete-token-set id))))))) + (do + (u/check-editable-tokens file-id) + (st/emit! (dwtl/delete-token-set id)))))))) (defn token-theme-proxy? [p] (obj/type-of? p "TokenThemeProxy")) @@ -568,8 +587,10 @@ (u/not-valid plugin-id :group "Plugin doesn't have 'content:write' permission") :else - (let [theme (u/locate-token-theme file-id id)] - (st/emit! (dwtl/update-token-theme id (assoc theme :group group))))))} + (do + (u/check-editable-tokens file-id) + (let [theme (u/locate-token-theme file-id id)] + (st/emit! (dwtl/update-token-theme id (assoc theme :group group)))))))} :name {:this true @@ -589,17 +610,19 @@ (u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission") :else - (let [theme (u/locate-token-theme file-id id)] - (when name - (st/emit! (dwtl/update-token-theme id (assoc theme :name name)))))))} + (do + (u/check-editable-tokens file-id) + (let [theme (u/locate-token-theme file-id id)] + (when name + (st/emit! (dwtl/update-token-theme id (assoc theme :name name))))))))} :active {:this true :enumerable false :get (fn [_] - (let [tokens-lib (u/locate-tokens-lib file-id)] - (ctob/theme-active? tokens-lib id))) + (let [tokens-status (u/locate-tokens-status file-id)] + (ctos/theme-active? tokens-status id))) :schema ::sm/boolean :set (fn [_ value] @@ -641,10 +664,12 @@ (u/not-valid plugin-id :addSet "Plugin doesn't have 'content:write' permission") :else - (let [set-name (token-set-name (resolve-token-set file-id set-arg)) - theme (u/locate-token-theme file-id id)] - (when (and set-name theme) - (st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name)))))))} + (do + (u/check-editable-tokens file-id) + (let [set-name (token-set-name (resolve-token-set file-id set-arg)) + theme (u/locate-token-theme file-id id)] + (when (and set-name theme) + (st/emit! (dwtl/update-token-theme id (ctob/enable-set theme set-name))))))))} :removeSet {:enumerable false @@ -655,10 +680,12 @@ (u/not-valid plugin-id :removeSet "Plugin doesn't have 'content:write' permission") :else - (let [set-name (token-set-name (resolve-token-set file-id set-arg)) - theme (u/locate-token-theme file-id id)] - (when (and set-name theme) - (st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name)))))))} + (do + (u/check-editable-tokens file-id) + (let [set-name (token-set-name (resolve-token-set file-id set-arg)) + theme (u/locate-token-theme file-id id)] + (when (and set-name theme) + (st/emit! (dwtl/update-token-theme id (ctob/disable-set theme set-name))))))))} :duplicate (fn [] @@ -667,12 +694,14 @@ (u/not-valid plugin-id :duplicate "Plugin doesn't have 'content:write' permission") :else - (let [theme (u/locate-token-theme file-id id) - theme' (ctob/make-token-theme (-> (datafy theme) - (dissoc :id - :modified-at)))] - (st/emit! (dwtl/create-token-theme theme')) - (token-theme-proxy plugin-id file-id (:id theme'))))) + (do + (u/check-editable-tokens file-id) + (let [theme (u/locate-token-theme file-id id) + theme' (ctob/make-token-theme (-> (datafy theme) + (dissoc :id + :modified-at)))] + (st/emit! (dwtl/create-token-theme theme')) + (token-theme-proxy plugin-id file-id (:id theme')))))) :remove (fn [] @@ -681,7 +710,9 @@ (u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission") :else - (st/emit! (dwtl/delete-token-theme id)))))) + (do + (u/check-editable-tokens file-id) + (st/emit! (dwtl/delete-token-theme id))))))) (defn tokens-catalog [plugin-id file-id] @@ -690,6 +721,14 @@ :$plugin {:enumerable false :get (constantly plugin-id)} :$id {:enumerable false :get (constantly file-id)} + :isEditableTokens + {:this false + :enumerable false + :get + (fn [] + (let [file (u/locate-file file-id)] + (cfo/editable-tokens? (ctf/file-data file))))} + :themes {:this true :enumerable false @@ -726,9 +765,11 @@ (u/not-valid plugin-id :addTheme "Plugin doesn't have 'content:write' permission") :else - (let [theme (ctob/make-token-theme attrs)] - (st/emit! (dwtl/create-token-theme theme)) - (token-theme-proxy plugin-id file-id (:id theme)))))} + (do + (u/check-editable-tokens file-id) + (let [theme (ctob/make-token-theme attrs)] + (st/emit! (dwtl/create-token-theme theme)) + (token-theme-proxy plugin-id file-id (:id theme))))))} :addSet {:enumerable false @@ -750,21 +791,23 @@ (u/not-valid plugin-id :addSet "Plugin doesn't have 'content:write' permission") :else - (let [active? (boolean (:active attrs)) - attrs (-> attrs - (dissoc :active) - (update :name ctob/normalize-set-name)) - set (ctob/make-token-set attrs)] - (st/emit! (dwtl/create-token-set set)) - ;; Newly created sets are inactive by default; activate it when - ;; requested. Enabling only adds the set name to the hidden theme, - ;; so it does not depend on the create event having propagated yet. - (when active? - (st/emit! (dwtl/set-enabled-token-set (ctob/get-name set) true))) - ;; Pass the set name as `initial-name` so the proxy can resolve - ;; it immediately, before the async `st/emit!` above propagates - ;; the new set into `@st/state`. - (token-set-proxy plugin-id file-id (ctob/get-id set) (ctob/get-name set)))))} + (do + (u/check-editable-tokens file-id) + (let [active? (boolean (:active attrs)) + attrs (-> attrs + (dissoc :active) + (update :name ctob/normalize-set-name)) + set (ctob/make-token-set attrs)] + (st/emit! (dwtl/create-token-set set)) + ;; Newly created sets are inactive by default; activate it when + ;; requested. Enabling only adds the set name to the hidden theme, + ;; so it does not depend on the create event having propagated yet. + (when active? + (st/emit! (dwtl/set-enabled-token-set (ctob/get-id set) true))) + ;; Pass the set name as `initial-name` so the proxy can resolve + ;; it immediately, before the async `st/emit!` above propagates + ;; the new set into `@st/state`. + (token-set-proxy plugin-id file-id (ctob/get-id set) (ctob/get-name set))))))} :getThemeById {:enumerable false diff --git a/frontend/src/app/plugins/utils.cljs b/frontend/src/app/plugins/utils.cljs index 163c77be59..fc6a921799 100644 --- a/frontend/src/app/plugins/utils.cljs +++ b/frontend/src/app/plugins/utils.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.files.tokens :as cfo] [app.common.i18n :as i18n :refer [tr]] [app.common.schema :as sm] [app.common.schema.messages :as csm] @@ -67,8 +68,18 @@ (defn locate-tokens-lib [file-id] - (let [file (locate-file file-id)] - (->> file :data :tokens-lib))) + (let [file-data (-> (locate-file file-id) (ctf/file-data)) + tokens-source-id (cfo/get-effective-tokens-source file-data)] + (some-> tokens-source-id + (locate-file) + (ctf/file-data) + (cfo/get-tokens-lib)))) + +(defn locate-tokens-status + [file-id] + (let [file (locate-file file-id) + file-data (ctf/file-data file)] + (cfo/get-tokens-status file-data))) (defn locate-token-theme [file-id id] @@ -275,6 +286,12 @@ (boolean (dm/get-in @st/state [:plugins :flags plugin-id :natural-child-ordering]))) +(defn check-editable-tokens + [file-id] + (let [file (locate-file file-id)] + (when-not (cfo/editable-tokens? (ctf/file-data file)) + (throw (js/Error. (dm/str "[PENPOT PLUGIN] Cannot modify tokens in an external library")))))) + (defn throw-validation-errors? [plugin-id] (boolean diff --git a/frontend/test/frontend_tests/data/workspace_texts_test.cljs b/frontend/test/frontend_tests/data/workspace_texts_test.cljs index 8e41b36de8..e460e68aa5 100644 --- a/frontend/test/frontend_tests/data/workspace_texts_test.cljs +++ b/frontend/test/frontend_tests/data/workspace_texts_test.cljs @@ -14,6 +14,7 @@ [app.common.types.text :as txt] [app.common.uuid :as uuid] [app.main.data.workspace.texts :as dwt] + [app.main.data.workspace.texts-events :as dwte] [app.main.ui.workspace.shapes.text.viewport-texts-html :as vth] [cljs.test :as t :include-macros true] [frontend-tests.helpers.state :as ths])) @@ -312,7 +313,7 @@ store (ths/setup-store file) events [;; Pre-select the text shape so add-typography can find it (fn [state] (assoc-in state [:workspace-local :selected] #{shape-id})) - (dwt/add-typography file-id)]] + (dwte/add-typography file-id)]] (ths/run-store store done events @@ -339,7 +340,7 @@ file-id (:id file) store (ths/setup-store file) events [(fn [state] (assoc-in state [:workspace-local :selected] #{shape-id})) - (dwt/add-typography file-id)]] + (dwte/add-typography file-id)]] (ths/run-store store done events @@ -366,7 +367,7 @@ file-id (:id file) store (ths/setup-store file) events [(fn [state] (assoc-in state [:workspace-local :selected] #{shape-id})) - (dwt/add-typography file-id)]] + (dwte/add-typography file-id)]] (ths/run-store store done events diff --git a/frontend/test/frontend_tests/logic/components_and_tokens.cljs b/frontend/test/frontend_tests/logic/components_and_tokens.cljs index 1be799f880..3e0bc8f579 100644 --- a/frontend/test/frontend_tests/logic/components_and_tokens.cljs +++ b/frontend/test/frontend_tests/logic/components_and_tokens.cljs @@ -9,11 +9,11 @@ [app.common.math :as mth] [app.common.test-helpers.components :as cthc] [app.common.test-helpers.compositions :as ctho] - [app.common.test-helpers.files :as cthf] [app.common.test-helpers.ids-map :as cthi] [app.common.test-helpers.shapes :as cths] [app.common.test-helpers.tokens :as ctht] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.main.data.helpers :as dsh] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.selection :as dws] @@ -35,29 +35,31 @@ (defn- setup-base-file [] - (-> (cthf/sample-file :file1) - (ctht/add-tokens-lib) - (ctht/update-tokens-lib #(-> % - (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :test-token-set) - :name "test-token-set")) - (ctob/add-theme (ctob/make-token-theme :name "test-theme" - :sets #{"test-token-set"})) - (ctob/set-active-themes #{"/test-theme"}) - (ctob/add-token (cthi/id :test-token-set) - (ctob/make-token :id (cthi/new-id! :test-token-1) - :name "test-token-1" - :type :border-radius - :value 25)) - (ctob/add-token (cthi/id :test-token-set) - (ctob/make-token :id (cthi/new-id! :test-token-2) - :name "test-token-2" - :type :border-radius - :value 50)) - (ctob/add-token (cthi/id :test-token-set) - (ctob/make-token :id (cthi/new-id! :test-token-3) - :name "test-token-3" - :type :border-radius - :value 75)))) + (-> (ctht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :test-token-set) + :name "test-token-set")) + (ctob/add-theme (ctob/make-token-theme :id (cthi/new-id! :test-theme) + :name "test-theme" + :sets #{"test-token-set"})) + (ctob/add-token (cthi/id :test-token-set) + (ctob/make-token :id (cthi/new-id! :test-token-1) + :name "test-token-1" + :type :border-radius + :value 25)) + (ctob/add-token (cthi/id :test-token-set) + (ctob/make-token :id (cthi/new-id! :test-token-2) + :name "test-token-2" + :type :border-radius + :value 50)) + (ctob/add-token (cthi/id :test-token-set) + (ctob/make-token :id (cthi/new-id! :test-token-3) + :name "test-token-3" + :type :border-radius + :value 75))) + :status-fn #(-> % + (ctos/set-tokens-status #{(cthi/id :test-theme)} + #{(cthi/id :test-token-set)}))) (ctho/add-frame :frame1) (ctht/apply-token-to-shape :frame1 "test-token-1" [:r1 :r2 :r3 :r4] [:r1 :r2 :r3 :r4] 25))) @@ -323,44 +325,44 @@ (t/async done (let [;; ==== Setup - file (-> (cthf/sample-file :file1) - (ctht/add-tokens-lib) - (ctht/update-tokens-lib #(-> % - (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :test-token-set) - :name "test-token-set")) - (ctob/add-theme (ctob/make-token-theme :name "test-theme" - :sets #{"test-token-set"})) - (ctob/set-active-themes #{"/test-theme"}) - (ctob/add-token (cthi/id :test-token-set) - (ctob/make-token :id (cthi/new-id! :token-radius) - :name "token-radius" - :type :border-radius - :value 10)) - (ctob/add-token (cthi/id :test-token-set) - (ctob/make-token :id (cthi/new-id! :token-rotation) - :name "token-rotation" - :type :rotation - :value 30)) - (ctob/add-token (cthi/id :test-token-set) - (ctob/make-token :id (cthi/new-id! :token-opacity) - :name "token-opacity" - :type :opacity - :value 0.7)) - (ctob/add-token (cthi/id :test-token-set) - (ctob/make-token :id (cthi/new-id! :token-stroke-width) - :name "token-stroke-width" - :type :stroke-width - :value 2)) - (ctob/add-token (cthi/id :test-token-set) - (ctob/make-token :id (cthi/new-id! :token-color) - :name "token-color" - :type :color - :value "#00ff00")) - (ctob/add-token (cthi/id :test-token-set) - (ctob/make-token :id (cthi/new-id! :token-dimensions) - :name "token-dimensions" - :type :dimensions - :value 100)))) + file (-> (ctht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :test-token-set) + :name "test-token-set")) + (ctob/add-theme (ctob/make-token-theme :id (cthi/new-id! :test-theme) + :name "test-theme" + :sets #{"test-token-set"})) + (ctob/add-token (cthi/id :test-token-set) + (ctob/make-token :id (cthi/new-id! :token-radius) + :name "token-radius" + :type :border-radius + :value 10)) + (ctob/add-token (cthi/id :test-token-set) + (ctob/make-token :id (cthi/new-id! :token-rotation) + :name "token-rotation" + :type :rotation + :value 30)) + (ctob/add-token (cthi/id :test-token-set) + (ctob/make-token :id (cthi/new-id! :token-opacity) + :name "token-opacity" + :type :opacity + :value 0.7)) + (ctob/add-token (cthi/id :test-token-set) + (ctob/make-token :id (cthi/new-id! :token-stroke-width) + :name "token-stroke-width" + :type :stroke-width + :value 2)) + (ctob/add-token (cthi/id :test-token-set) + (ctob/make-token :id (cthi/new-id! :token-color) + :name "token-color" + :type :color + :value "#00ff00")) + (ctob/add-token (cthi/id :test-token-set) + (ctob/make-token :id (cthi/new-id! :token-dimensions) + :name "token-dimensions" + :type :dimensions + :value 100))) + :status-fn #(ctos/set-tokens-status % #{(cthi/id :test-theme)} #{(cthi/id :test-token-set)})) (ctho/add-frame :frame1) (ctht/apply-token-to-shape :frame1 "token-radius" [:r1 :r2 :r3 :r4] [:r1 :r2 :r3 :r4] 10) (ctht/apply-token-to-shape :frame1 "token-rotation" [:rotation] [:rotation] 30) diff --git a/frontend/test/frontend_tests/plugins/tokens_test.cljs b/frontend/test/frontend_tests/plugins/tokens_test.cljs index 3f4f56f6d9..99ef7f4685 100644 --- a/frontend/test/frontend_tests/plugins/tokens_test.cljs +++ b/frontend/test/frontend_tests/plugins/tokens_test.cljs @@ -6,11 +6,13 @@ (ns frontend-tests.plugins.tokens-test (:require + [app.common.files.tokens :as cfo] [app.common.test-helpers.compositions :as ctho] [app.common.test-helpers.files :as cthf] [app.common.test-helpers.ids-map :as cthi] [app.common.test-helpers.tokens :as ctht] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [app.main.data.tokenscript :as ts] [app.main.data.workspace.tokens.library-edit :as dwtl] @@ -125,24 +127,25 @@ done (let [set-id (cthi/new-id! :token-set) token-id (cthi/new-id! :spacing-token) - file (-> (cthf/sample-file :file1 :page-label :page1) - (ctho/add-frame :frame1 {:layout :flex}) - (ctht/add-tokens-lib) - (ctht/update-tokens-lib - #(-> % - (ctob/add-set - (ctob/make-token-set :id set-id - :name "spacing")) - (ctob/add-theme - (ctob/make-token-theme :name "theme" - :sets #{"spacing"})) - (ctob/set-active-themes #{"/theme"}) - (ctob/add-token - set-id - (ctob/make-token :id token-id - :name "spacing.medium" - :type :spacing - :value 16))))) + theme-id (cthi/new-id! :theme) + file (-> (ctht/sample-file-with-tokens + :file-id :file1 + :lib-fn #(-> % + (ctob/add-set + (ctob/make-token-set :id set-id + :name "spacing")) + (ctob/add-theme + (ctob/make-token-theme :id theme-id + :name "theme" + :sets #{"spacing"})) + (ctob/add-token + set-id + (ctob/make-token :id token-id + :name "spacing.medium" + :type :spacing + :value 16))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{set-id})) + (ctho/add-frame :frame1 {:layout :flex})) store (ths/setup-store file) _ (set! st/state store) _ (set! st/stream (ptk/input-stream store)) @@ -170,7 +173,8 @@ (t/deftest shape-apply-token-accepts-font-families (t/async done - (let [set-id (cthi/new-id! :token-set) + (let [theme-id (cthi/new-id! :token-theme) + set-id (cthi/new-id! :token-set) token-id (cthi/new-id! :font-family-token) file (-> (cthf/sample-file :file1 :page-label :page1) (ctho/add-text :text1 "Hello World!") @@ -183,13 +187,14 @@ (ctob/add-theme (ctob/make-token-theme :name "theme" :sets #{"fonts"})) - (ctob/set-active-themes #{"/theme"}) (ctob/add-token set-id (ctob/make-token :id token-id :name "font.primary" :type :font-family - :value ["Inter"]))))) + :value ["Inter"])))) + (ctht/update-tokens-status + #(ctos/set-tokens-status % #{theme-id} #{set-id}))) store (ths/setup-store file) _ (set! st/state store) _ (set! st/stream (ptk/input-stream store)) @@ -260,10 +265,11 @@ ;; Demonstrates the bug: resolving the new token against active sets ;; only leaves the reference unresolved. (let [tokens-lib (inactive-set-library) + tokens-status (cfo/make-tokens-status-from-lib tokens-lib) token (ctob/make-token {:name "color.bg.default" :value "{color.gray.50}" :type :color}) - tokens-tree (-> (ctob/get-tokens-in-active-sets tokens-lib) + tokens-tree (-> (cfo/get-tokens-in-active-sets tokens-status tokens-lib) (assoc (:name token) token)) resolved (ts/resolve-tokens tokens-tree) {:keys [errors resolved-value]} (get resolved (:name token))] @@ -303,16 +309,18 @@ (t/deftest token-set-duplicate-returns-the-duplicated-set (let [file-id (cthi/new-id! :file) set-id (cthi/new-id! :set) - dup-id (cthi/new-id! :dup) - proxy (ptok/token-set-proxy "plugin-id" file-id set-id)] - (with-redefs [r/check-permission (constantly true) + dup-id (cthi/new-id! :dup)] + (with-redefs [u/locate-tokens-lib (constantly nil) + r/check-permission (constantly true) + u/check-editable-tokens (constantly nil) dwtl/duplicate-token-set (mock/stub (fn [id {:keys [id-ref]}] (t/is (= set-id id)) (reset! id-ref dup-id) :duplicate-token-set)) st/emit! mock/noop] - (let [dup (.duplicate proxy)] + (let [proxy (ptok/token-set-proxy "plugin-id" file-id set-id) + dup (.duplicate proxy)] (t/is (ptok/token-set-proxy? dup)) (t/is (= (str dup-id) (.-id dup))))))) @@ -320,10 +328,10 @@ (let [file-id (cthi/new-id! :file) theme-id (cthi/new-id! :theme) set-id (cthi/new-id! :set) - set (ptok/token-set-proxy "plugin-id" file-id set-id "Primitives") - theme (ptok/token-theme-proxy "plugin-id" file-id theme-id) captured (atom [])] - (with-redefs [r/check-permission (constantly true) + (with-redefs [u/locate-tokens-lib (constantly nil) + r/check-permission (constantly true) + u/check-editable-tokens (constantly nil) u/locate-token-theme (fn [_file _theme] (ctob/make-token-theme :id theme-id @@ -334,18 +342,22 @@ (swap! captured conj {:id id :theme theme}) :update-token-theme) st/emit! mock/noop] - (.addSet theme set) - (.removeSet theme set) - (t/is (= [theme-id theme-id] (mapv :id @captured))) - (t/is (contains? (-> @captured first :theme :sets) "Primitives")) - (t/is (not (contains? (-> @captured second :theme :sets) "Primitives")))))) + (let [set (ptok/token-set-proxy "plugin-id" file-id set-id "Primitives") + theme (ptok/token-theme-proxy "plugin-id" file-id theme-id)] + (.addSet theme set) + (.removeSet theme set) + (t/is (= [theme-id theme-id] (mapv :id @captured))) + (t/is (contains? (-> @captured first :theme :sets) "Primitives")) + (t/is (not (contains? (-> @captured second :theme :sets) "Primitives"))))))) (t/deftest font-family-token-value-accepts-a-string (let [file-id (cthi/new-id! :file) set-id (cthi/new-id! :set) token-id (cthi/new-id! :token) captured (atom nil)] - (with-redefs [r/check-permission (constantly true) + (with-redefs [u/locate-tokens-lib (constantly nil) + r/check-permission (constantly true) + u/check-editable-tokens (constantly nil) u/locate-token (constantly {:id token-id :name "font.primary" :type :font-family @@ -419,13 +431,15 @@ theme (ctob/make-token-theme :id theme-id :group "mode" :name "Light") emitted (atom []) invalid (atom [])] - (with-redefs [r/check-permission (constantly true) - u/locate-token-set (fn [_ id] (when (= id set-id) token-set)) - u/locate-token-theme (fn [_ id] (when (= id theme-id) theme)) - u/not-valid (fn [_ code value] (swap! invalid conj [code value])) + (with-redefs [r/check-permission (constantly true) + u/locate-tokens-lib (constantly nil) + u/locate-token-set (fn [_ id] (when (= id set-id) token-set)) + u/locate-token-theme (fn [_ id] (when (= id theme-id) theme)) + u/check-editable-tokens (constantly nil) + u/not-valid (fn [_ code value] (swap! invalid conj [code value])) dwtl/update-token-theme (fn [id theme] {:id id :theme theme}) - st/emit! (fn ([event] (swap! emitted conj event) nil) - ([event & _] (swap! emitted conj event) nil))] + st/emit! (fn ([event] (swap! emitted conj event) nil) + ([event & _] (swap! emitted conj event) nil))] (let [theme-proxy (ptok/token-theme-proxy plugin-id file-id theme-id)] (.addSet theme-proxy (str set-id)) (t/is (= #{"Core"} (-> @emitted first :theme :sets))) @@ -440,13 +454,15 @@ theme (ctob/make-token-theme :id theme-id :group "mode" :name "Light") emitted (atom []) invalid (atom [])] - (with-redefs [r/check-permission (constantly true) - u/locate-token-set (fn [_ id] (when (= id set-id) token-set)) - u/locate-token-theme (fn [_ id] (when (= id theme-id) theme)) - u/not-valid (fn [_ code value] (swap! invalid conj [code value])) + (with-redefs [r/check-permission (constantly true) + u/locate-tokens-lib (constantly nil) + u/locate-token-set (fn [_ id] (when (= id set-id) token-set)) + u/locate-token-theme (fn [_ id] (when (= id theme-id) theme)) + u/check-editable-tokens (constantly nil) + u/not-valid (fn [_ code value] (swap! invalid conj [code value])) dwtl/update-token-theme (fn [id theme] {:id id :theme theme}) - st/emit! (fn ([event] (swap! emitted conj event) nil) - ([event & _] (swap! emitted conj event) nil))] + st/emit! (fn ([event] (swap! emitted conj event) nil) + ([event & _] (swap! emitted conj event) nil))] (let [theme-proxy (ptok/token-theme-proxy plugin-id file-id theme-id) set-proxy (ptok/token-set-proxy plugin-id file-id set-id "Core")] (.addSet theme-proxy set-proxy) @@ -459,20 +475,24 @@ theme-id (uuid/next) theme (ctob/make-token-theme :id theme-id :group "mode" :name "Light") emitted (atom []) - errors (atom [])] - (with-redefs [u/locate-token-set (constantly nil) - u/locate-token-theme (fn [_ id] (when (= id theme-id) theme)) + invalid (atom [])] + (with-redefs [u/locate-tokens-lib (constantly nil) + u/locate-token-set (constantly nil) u/throw-validation-errors? (constantly true) + u/locate-token-theme (fn [_ id] (when (= id theme-id) theme)) + u/check-editable-tokens (constantly nil) + u/handle-error (fn [plugin-id] + (fn [cause] + (u/not-valid plugin-id :error (str cause)))) dwtl/update-token-theme (fn [id theme] {:id id :theme theme}) st/emit! (fn ([event] (swap! emitted conj event) nil) ([event & _] (swap! emitted conj event) nil))] (let [theme-proxy (ptok/token-theme-proxy plugin-id file-id theme-id)] - ;; Non-id, non-proxy arguments are rejected by the schema coercer. - (try (.addSet theme-proxy 42) (catch :default e (swap! errors conj e))) - (try (.removeSet theme-proxy nil) (catch :default e (swap! errors conj e))) + (try (.addSet theme-proxy 42) (catch :default e (swap! invalid conj e))) + (try (.removeSet theme-proxy nil) (catch :default e (swap! invalid conj e))) (t/is (empty? @emitted)) - (t/is (= 2 (count @errors))) - (t/is (every? #(instance? js/Error %) @errors)))))) + (t/is (= 2 (count @invalid))) + (t/is (every? #(instance? js/Error %) @invalid)))))) ;; ═══════════════════════════════════════════════════════════════ ;; Permission check tests (T9-F-01) @@ -582,7 +602,7 @@ u/not-valid (mock/stub (fn [pid prop msg] (swap! errors conj [pid prop msg]))) r/check-permission (constantly false) st/emit! mock/noop] - (let [proxy (ptok/token-set-proxy plugin-id file-id set-id)] + (let [proxy (ptok/token-set-proxy plugin-id file-id set-id "test-plugin")] (.toggleActive proxy) (t/is (= 1 (count @errors))) (t/is (= [plugin-id :toggleActive "Plugin doesn't have 'content:write' permission"] (first @errors))))))) diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 49d834faa0..1dae772e0f 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -82,6 +82,7 @@ [frontend-tests.tokens.logic.token-actions-test] [frontend-tests.tokens.logic.token-data-test] [frontend-tests.tokens.logic.token-remapping-test] + [frontend-tests.tokens.logic.tokens-status-test] [frontend-tests.tokens.style-dictionary-test] [frontend-tests.tokens.token-errors-test] [frontend-tests.tokens.workspace-tokens-remap-test] @@ -198,6 +199,7 @@ 'frontend-tests.tokens.logic.token-remapping-test 'frontend-tests.tokens.style-dictionary-test 'frontend-tests.tokens.token-errors-test + 'frontend-tests.tokens.logic.tokens-status-test 'frontend-tests.tokens.workspace-tokens-remap-test 'frontend-tests.ui.check-updates-test 'frontend-tests.ui.colorpicker-token-set-order-test diff --git a/frontend/test/frontend_tests/tokens/context_menu_test.cljs b/frontend/test/frontend_tests/tokens/context_menu_test.cljs index 5037b69043..7094b28536 100644 --- a/frontend/test/frontend_tests/tokens/context_menu_test.cljs +++ b/frontend/test/frontend_tests/tokens/context_menu_test.cljs @@ -6,49 +6,51 @@ [app.common.test-helpers.shapes :as ths] [app.common.test-helpers.tokens :as tht] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.main.ui.workspace.tokens.management.context-menu :as wtcm] [clojure.test :as t])) (defn setup-file [] - (-> (thf/sample-file :file-1) - (tht/add-tokens-lib) - (tht/update-tokens-lib #(-> % - (ctob/add-set (ctob/make-token-set :name "test-token-set")) - (ctob/add-theme (ctob/make-token-theme :name "test-theme" - :sets #{"test-token-set"})) - (ctob/set-active-themes #{"/test-theme"}) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :name "token-radius" - :type :border-radius - :value 10)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :name "token-color" - :type :color - :value "red")) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :name "token-spacing" - :type :spacing - :value 10)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :name "token-sizing" - :type :sizing - :value 10)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :name "token-rotation" - :type :rotation - :value 10)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :name "token-opacity" - :type :opacity - :value 10)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :name "token-dimensions" - :type :dimensions - :value 10)) - (ctob/add-token (thi/id :test-token-set) - (ctob/make-token :name "token-number" - :type :number - :value 10)))) + (-> (tht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :test-token-set) + :name "test-token-set")) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :test-theme) + :name "test-theme" + :sets #{"test-token-set"})) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :name "token-radius" + :type :border-radius + :value 10)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :name "token-color" + :type :color + :value "red")) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :name "token-spacing" + :type :spacing + :value 10)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :name "token-sizing" + :type :sizing + :value 10)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :name "token-rotation" + :type :rotation + :value 10)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :name "token-opacity" + :type :opacity + :value 10)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :name "token-dimensions" + :type :dimensions + :value 10)) + (ctob/add-token (thi/id :test-token-set) + (ctob/make-token :name "token-number" + :type :number + :value 10))) + :status-fn #(ctos/set-tokens-status % #{(thi/id :test-theme)} #{(thi/id :test-token-set)})) ;; app.main.data.workspace.tokens.application/generic-attributes (tho/add-group :group1) ;; app.main.data.workspace.tokens.application/rect-attributes diff --git a/frontend/test/frontend_tests/tokens/helpers/state.cljs b/frontend/test/frontend_tests/tokens/helpers/state.cljs index 5e0f549d94..e03b15b099 100644 --- a/frontend/test/frontend_tests/tokens/helpers/state.cljs +++ b/frontend/test/frontend_tests/tokens/helpers/state.cljs @@ -6,9 +6,12 @@ (ns frontend-tests.tokens.helpers.state (:require + [app.common.files.tokens :as cfo] [app.common.types.tokens-lib :as ctob] + [app.main.data.changes :as dch] [app.main.data.helpers :as dsh] [app.main.data.style-dictionary :as sd] + [app.main.data.workspace.undo :as dwu] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -29,11 +32,14 @@ (ptk/reify ::end+ ptk/WatchEvent (watch [_ state _] - (let [data (dsh/lookup-file-data state)] - (->> (get data :tokens-lib) - (ctob/get-tokens-in-active-sets) - (sd/resolve-tokens) - (rx/mapcat #(rx/of (end)))))))) + (let [data (dsh/lookup-file-data state) + tokens-status (cfo/get-tokens-status data) + tokens-lib (cfo/get-tokens-lib data)] + (if (and tokens-status tokens-lib) + (->> (cfo/get-tokens-in-active-sets tokens-status tokens-lib) + (sd/resolve-tokens) + (rx/mapcat #(rx/of (end)))) + (rx/of (end))))))) (defn stop-on "Helper function to be used with async version of run-store. @@ -51,6 +57,31 @@ "Stops on `send-update-indices` function being called, which should be the last function of an event chain." (stop-on ::end)) +(defn watch-undo-stack + "Maintain the workspace undo stack by watching for commit events. + Must be emitted before the events under test so undo entries are populated." + [] + (ptk/reify ::watch-undo-stack + ptk/WatchEvent + (watch [_ _ stream] + (let [stopper-s (->> stream (rx/filter (ptk/type? ::watch-undo-stack)))] + (->> stream + (rx/filter dch/commit?) + (rx/map deref) + (rx/filter #(= :local (:source %))) + (rx/mapcat + (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo? selected-before]}] + (if (and save-undo? (seq undo-changes)) + (rx/of (dwu/append-undo + {:undo-changes undo-changes + :redo-changes redo-changes + :undo-group undo-group + :tags tags + :selected-before selected-before} + stack-undo?)) + (rx/empty)))) + (rx/take-until stopper-s)))))) + ;; Support for async events in tests ;; https://chat.kaleidos.net/penpot-partners/pl/tz1yoes3w3fr9qanxqpuhoz3ch (defn run-store diff --git a/frontend/test/frontend_tests/tokens/helpers/tokens.cljs b/frontend/test/frontend_tests/tokens/helpers/tokens.cljs index f5a0699c85..44b0e18cb9 100644 --- a/frontend/test/frontend_tests/tokens/helpers/tokens.cljs +++ b/frontend/test/frontend_tests/tokens/helpers/tokens.cljs @@ -11,9 +11,11 @@ [app.common.types.tokens-lib :as ctob])) (defn get-token [file name] - (some-> (get-in file [:data :tokens-lib]) - (ctob/get-tokens-in-active-sets) - (get name))) + (let [data (get-in file [:data]) + tokens-status (cfo/get-tokens-status data) + tokens-lib (cfo/get-tokens-lib data)] + (some-> (cfo/get-tokens-in-active-sets tokens-status tokens-lib) + (get name)))) (defn apply-token-to-shape [file shape-label token-label attributes] diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs index bd711f6b12..19fbf5250b 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -10,8 +10,10 @@ [app.common.test-helpers.files :as cthf] [app.common.test-helpers.ids-map :as cthi] [app.common.test-helpers.shapes :as cths] + [app.common.test-helpers.tokens :as ctht] [app.common.types.text :as txt] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.data.workspace.wasm-text :as dwwt] @@ -44,21 +46,22 @@ (defn setup-file-with-tokens [& {:keys [rect-1 rect-2 rect-3]}] - (-> (setup-file) - (ctho/add-rect :rect-1 rect-1) - (ctho/add-rect :rect-2 rect-2) - (ctho/add-rect :rect-3 rect-3) - (ctho/add-text :text-1 "Hello World!") - (assoc-in [:data :tokens-lib] - (-> (ctob/make-tokens-lib) - (ctob/add-theme (ctob/make-token-theme :name "Theme A" :sets #{"Set A"})) - (ctob/set-active-themes #{"/Theme A"}) + (-> (ctht/sample-file-with-tokens + :lib-fn #(-> % (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a) :name "Set A")) + (ctob/add-theme (ctob/make-token-theme :id (cthi/new-id! :theme-a) + :name "Theme A" + :sets #{"Set A"})) (ctob/add-token (cthi/id :set-a) (ctob/make-token border-radius-token)) (ctob/add-token (cthi/id :set-a) - (ctob/make-token reference-border-radius-token)))))) + (ctob/make-token reference-border-radius-token))) + :status-fn #(ctos/set-tokens-status % #{(cthi/id :theme-a)} #{(cthi/id :set-a)})) + (ctho/add-rect :rect-1 rect-1) + (ctho/add-rect :rect-2 rect-2) + (ctho/add-rect :rect-3 rect-3) + (ctho/add-text :text-1 "Hello World!"))) (def debounce-text-stop (tohs/stop-on ::dwwt/resize-wasm-text-debounce-commit)) @@ -83,15 +86,16 @@ done (let [file (setup-file-with-empty-lib) store (ths/setup-store file) - set (ctob/make-token-set :name "primitives") + set (ctob/make-token-set :id (cthi/new-id! :set1) :name "primitives") events [(dwtl/create-token-set set)]] (tohs/run-store-async store done events (fn [new-state] - (let [file' (ths/get-file-from-state new-state) - lib (get-in file' [:data :tokens-lib])] + (let [file' (ths/get-file-from-state new-state) + lib (ctht/get-tokens-lib file') + status (ctht/get-tokens-status file')] (t/is (some? (ctob/get-set lib (ctob/get-id set)))) - (t/is (false? (ctob/token-set-active? lib "primitives")))))))))) + (t/is (false? (ctos/set-active? status (cthi/id :set1))))))))))) (t/deftest test-create-then-enable-token-set (t/testing "create followed by set-enabled (as the plugin addSet does) yields an active set" @@ -99,16 +103,17 @@ done (let [file (setup-file-with-empty-lib) store (ths/setup-store file) - set (ctob/make-token-set :name "primitives") + set (ctob/make-token-set :id (cthi/new-id! :set1) :name "primitives") events [(dwtl/create-token-set set) - (dwtl/set-enabled-token-set "primitives" true)]] + (dwtl/set-enabled-token-set (cthi/id :set1) true)]] (tohs/run-store-async store done events (fn [new-state] - (let [file' (ths/get-file-from-state new-state) - lib (get-in file' [:data :tokens-lib])] + (let [file' (ths/get-file-from-state new-state) + lib (ctht/get-tokens-lib file') + status (ctht/get-tokens-status file')] (t/is (some? (ctob/get-set lib (ctob/get-id set)))) - (t/is (true? (ctob/token-set-active? lib "primitives")))))))))) + (t/is (true? (ctos/set-active? status (cthi/id :set1))))))))))) (t/deftest test-apply-token (t/testing "applies token to shape and updates shape attributes to resolved value" diff --git a/frontend/test/frontend_tests/tokens/logic/token_data_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_data_test.cljs index 54badf55a2..ee2d94a31f 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_data_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_data_test.cljs @@ -8,9 +8,12 @@ (:require [app.common.test-helpers.files :as cthf] [app.common.test-helpers.ids-map :as cthi] + [app.common.test-helpers.tokens :as ctho] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.common.uuid :as uuid] [app.main.data.workspace.tokens.library-edit :as dwtl] + [app.main.data.workspace.undo :as dwu] [cljs.test :as t :include-macros true] [frontend-tests.helpers.pages :as thp] [frontend-tests.helpers.state :as ths] @@ -25,11 +28,40 @@ (defn setup-file-with-token-lib [] - (-> (setup-file) - (assoc-in [:data :tokens-lib] - (-> (ctob/make-tokens-lib) - (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :test-token-set) - :name "Set A")))))) + (ctho/sample-file-with-tokens + :file-id :file-1 + :page-label :page-1 + :lib-fn #(ctob/add-set % (ctob/make-token-set :id (cthi/new-id! :test-token-set) + :name "Set A")) + :status-fn #(ctos/set-tokens-status % #{} #{(cthi/id :test-token-set)}))) + +(defn setup-file-with-token-lib-and-theme + [theme-id] + (ctho/sample-file-with-tokens + :file-id :file-1 + :page-label :page-1 + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :test-token-set) + :name "Set A")) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name "Theme A" + :group "default"))) + :status-fn #(ctos/set-tokens-status % #{theme-id} #{(cthi/id :test-token-set)}))) + +(defn setup-file-with-token-lib-and-token + [] + (ctho/sample-file-with-tokens + :file-id :file-1 + :page-label :page-1 + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :test-token-set) + :name "Set A")) + (ctob/add-token (cthi/id :test-token-set) + (ctob/make-token :id (cthi/new-id! :color.primary) + :name "color.primary" + :type :color + :value "#000000"))) + :status-fn #(ctos/set-tokens-status % #{} #{(cthi/id :test-token-set)}))) (t/deftest add-set (t/async @@ -50,6 +82,20 @@ (t/is (= (count sets') 2)) (t/is (some? set-b'))))))))) +(t/deftest add-set-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/create-token-set (ctob/make-token-set :name "Set A"))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + (t/deftest rename-set (t/async done @@ -71,6 +117,21 @@ (t/is (= (count sets') 1)) (t/is (some? set-a'))))))))) +(t/deftest rename-set-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + set-a (ctob/make-token-set :name "Set A") + events [(dwtl/rename-token-set set-a "Set A updated")]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + (t/deftest duplicate-set (t/async done @@ -103,21 +164,734 @@ sets (ctob/get-sets token-lib)] (t/testing "Token lib contains one set" - (t/is (= (count sets) 1)))))))) + (t/is (= (count sets) 1))))))))) - (t/deftest delete-set - (t/async - done - (let [file (setup-file-with-token-lib) - store (ths/setup-store file) - events [(dwtl/delete-token-set (cthi/id :test-token-set))]] +(t/deftest duplicate-set-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/duplicate-token-set (uuid/next))]] - (tohs/run-store-async - store done events - (fn [new-state] - (let [file' (ths/get-file-from-state new-state) - tokens-lib' (toht/get-tokens-lib file') - sets' (ctob/get-sets tokens-lib')] + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) - (t/testing "Set has been deleted" - (t/is (= (count sets') 0)))))))))) \ No newline at end of file +(t/deftest delete-set + (t/async + done + (let [file (setup-file-with-token-lib) + store (ths/setup-store file) + events [(dwtl/delete-token-set (cthi/id :test-token-set))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file') + sets' (ctob/get-sets tokens-lib')] + + (t/testing "Set has been deleted" + (t/is (= (count sets') 0))))))))) + +(t/deftest delete-set-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/delete-token-set (uuid/next))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest set-tokens-source + (t/async + done + (let [file (setup-file-with-token-lib) + store (ths/setup-store file) + library-id (uuid/next)] + + ;; Phase 1: set tokens-source with undo watcher active + (tohs/run-store + store identity + [(tohs/watch-undo-stack) + (dwtl/set-tokens-source library-id)] + (fn [new-state] + (let [file-data' (-> (ths/get-file-from-state new-state) :data)] + (t/testing "tokens-source is set to the library id" + (t/is (= library-id (:tokens-source file-data')))))) + (tohs/stop-on ::dwtl/set-tokens-source)) + + ;; Phase 2: undo and verify restoration + (tohs/run-store + store done + [dwu/undo] + (fn [undone-state] + (let [file-data'' (-> (ths/get-file-from-state undone-state) :data)] + (t/testing "tokens-source is restored to nil" + (t/is (nil? (:tokens-source file-data'')))))) + (tohs/stop-on ::dwu/undo))))) + +(t/deftest set-tokens-source-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + library-id (uuid/next)] + + (tohs/run-store-async + store done + [(dwtl/set-tokens-source library-id)] + (fn [new-state] + (let [file-data' (-> (ths/get-file-from-state new-state) :data)] + (t/testing "tokens-source is set even without tokens-lib" + (t/is (= library-id (:tokens-source file-data')))))))))) + +;; ========================================================================== +;; Token Themes +;; ========================================================================== + +(t/deftest create-token-theme + (t/async + done + (let [file (setup-file-with-token-lib) + store (ths/setup-store file) + theme-id (uuid/next) + events [(dwtl/create-token-theme (ctob/make-token-theme :id theme-id + :name "Theme B" + :group "default"))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file') + theme' (ctob/get-theme tokens-lib' theme-id)] + + (t/testing "Theme has been created" + (t/is (some? theme')) + (t/is (= "Theme B" (:name theme')))))))))) + +(t/deftest create-token-theme-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + theme-id (uuid/next) + events [(dwtl/create-token-theme (ctob/make-token-theme :id theme-id + :name "Theme A" + :group "default"))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest update-token-theme + (t/async + done + (let [theme-id (uuid/next) + file (setup-file-with-token-lib-and-theme theme-id) + store (ths/setup-store file) + events [(dwtl/update-token-theme theme-id + (ctob/make-token-theme :id theme-id + :name "Theme A renamed" + :group "default"))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file') + theme' (ctob/get-theme tokens-lib' theme-id)] + + (t/testing "Theme has been renamed" + (t/is (= "Theme A renamed" (:name theme')))))))))) + +(t/deftest update-token-theme-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + theme-id (uuid/next) + events [(dwtl/update-token-theme theme-id + (ctob/make-token-theme :id theme-id + :name "Theme A" + :group "default"))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest set-token-theme-active + (t/async + done + (let [file (setup-file-with-token-lib) + store (ths/setup-store file) + theme-id (uuid/next) + events [(dwtl/create-token-theme (ctob/make-token-theme :id theme-id + :name "Theme B" + :group "default")) + (dwtl/set-token-theme-active theme-id true)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-status (ctho/get-tokens-status file')] + + (t/testing "Theme has been activated" + (t/is (ctos/theme-active? tokens-status theme-id))))))))) + +(t/deftest set-token-theme-active-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + theme-id (uuid/next) + events [(dwtl/set-token-theme-active theme-id true)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest toggle-token-theme-active + (t/async + done + (let [theme-id (uuid/next) + file (setup-file-with-token-lib-and-theme theme-id) + store (ths/setup-store file) + events [(dwtl/toggle-token-theme-active theme-id)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-status (ctho/get-tokens-status file')] + + (t/testing "Theme has been deactivated" + (t/is (not (ctos/theme-active? tokens-status theme-id)))))))))) + +(t/deftest toggle-token-theme-active-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + theme-id (uuid/next) + events [(dwtl/toggle-token-theme-active theme-id)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest delete-token-theme + (t/async + done + (let [theme-id (uuid/next) + file (setup-file-with-token-lib-and-theme theme-id) + store (ths/setup-store file) + events [(dwtl/delete-token-theme theme-id)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file') + theme' (ctob/get-theme tokens-lib' theme-id)] + + (t/testing "Theme has been deleted" + (t/is (nil? theme'))))))))) + +(t/deftest delete-token-theme-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + theme-id (uuid/next) + events [(dwtl/delete-token-theme theme-id)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +;; ========================================================================== +;; Token Set Operations +;; ========================================================================== + +(t/deftest set-enabled-token-set + (t/async + done + (let [file (setup-file-with-token-lib) + store (ths/setup-store file) + events [(dwtl/set-enabled-token-set (cthi/id :test-token-set) true)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-status (ctho/get-tokens-status file')] + + (t/testing "Set has been enabled" + (t/is (ctos/set-active? tokens-status (cthi/id :test-token-set)))))))))) + +(t/deftest set-enabled-token-set-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/set-enabled-token-set (uuid/next) true)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest toggle-token-set + (t/async + done + (let [file (setup-file-with-token-lib) + store (ths/setup-store file) + events [(dwtl/toggle-token-set (cthi/id :test-token-set))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-status (ctho/get-tokens-status file')] + + (t/testing "Set has been toggled (deactivated)" + (t/is (not (ctos/set-active? tokens-status (cthi/id :test-token-set))))))))))) + +(t/deftest toggle-token-set-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/toggle-token-set (uuid/next))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest toggle-token-set-group + (t/async + done + (let [file (ctho/sample-file-with-tokens + :file-id :file-1 + :page-label :page-1 + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a) + :name "group/set-a")) + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-b) + :name "group/set-b"))) + :status-fn #(ctos/set-tokens-status % #{} #{})) + store (ths/setup-store file) + events [(dwtl/toggle-token-set-group ["group"])]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-status (ctho/get-tokens-status file')] + + (t/testing "Set group has been toggled (activated)" + (t/is (ctos/set-active? tokens-status (cthi/id :set-a))) + (t/is (ctos/set-active? tokens-status (cthi/id :set-b)))))))))) + +(t/deftest toggle-token-set-group-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/toggle-token-set-group ["group"])]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest delete-token-set-group + (t/async + done + (let [file (ctho/sample-file-with-tokens + :file-id :file-1 + :page-label :page-1 + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a) + :name "group/set-a")) + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-b) + :name "group/set-b")) + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-c) + :name "other/set-c")))) + store (ths/setup-store file) + events [(dwtl/delete-token-set-group ["group"])]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file')] + + (t/testing "Group sets deleted, other set remains" + (t/is (nil? (ctob/get-set-by-name tokens-lib' "group/set-a"))) + (t/is (nil? (ctob/get-set-by-name tokens-lib' "group/set-b"))) + (t/is (some? (ctob/get-set-by-name tokens-lib' "other/set-c")))))))))) + +(t/deftest delete-token-set-group-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/delete-token-set-group ["group"])]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest rename-token-set-group + (t/async + done + (let [file (ctho/sample-file-with-tokens + :file-id :file-1 + :page-label :page-1 + :lib-fn #(ctob/add-set % (ctob/make-token-set :id (cthi/new-id! :set-a) + :name "old-group/set-a"))) + store (ths/setup-store file) + events [(dwtl/rename-token-set-group ["old-group"] "new-group")]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file')] + + (t/testing "Set group has been renamed" + (t/is (some? (ctob/get-set-by-name tokens-lib' "new-group/set-a"))) + (t/is (nil? (ctob/get-set-by-name tokens-lib' "old-group/set-a")))))))))) + +(t/deftest rename-token-set-group-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/rename-token-set-group ["old-group"] "new-group")]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +;; ========================================================================== +;; Drop Operations +;; ========================================================================== + +(t/deftest drop-token-set-group + (t/async + done + (let [file (ctho/sample-file-with-tokens + :file-id :file-1 + :page-label :page-1 + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a) + :name "foo/foo")) + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-b) + :name "bar/bar")) + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-c) + :name "baz/baz")))) + store (ths/setup-store file) + events [(dwtl/drop-token-set-group {:from-index 2 + :to-index 0 + :position :top})]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file') + sets' (ctob/get-set-names tokens-lib')] + + (t/testing "Set groups have been reordered" + (t/is (= ["bar/bar" "foo/foo" "baz/baz"] (vec sets')))))))))) + +(t/deftest drop-token-set-group-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/drop-token-set-group {:from-index 0 + :to-index 1 + :position :top})]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest drop-token-set + (t/async + done + (let [file (ctho/sample-file-with-tokens + :file-id :file-1 + :page-label :page-1 + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a) + :name "foo")) + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-b) + :name "bar")) + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-c) + :name "baz")))) + store (ths/setup-store file) + events [(dwtl/drop-token-set {:from-index 0 + :to-index 2 + :position :bot})]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file') + sets' (ctob/get-set-names tokens-lib')] + + (t/testing "Token sets have been reordered" + (t/is (= ["bar" "baz" "foo"] (vec sets')))))))))) + +(t/deftest drop-token-set-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/drop-token-set {:from-index 0 + :to-index 1 + :position :top})]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +;; ========================================================================== +;; Token CRUD +;; ========================================================================== + +(t/deftest create-token-in-set + (t/async + done + (let [file (setup-file-with-token-lib) + store (ths/setup-store file) + token-id (cthi/new-id! :color.primary) + events [(dwtl/create-token (cthi/id :test-token-set) + (ctob/make-token :id token-id + :name "color.primary" + :type :color + :value "#000000"))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file') + token (ctob/get-token tokens-lib' (cthi/id :test-token-set) + (cthi/id :color.primary))] + + (t/testing "Token has been created in set" + (t/is (some? token))))))))) + +(t/deftest create-token-no-set + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + token-id (cthi/new-id! :color.primary) + events [(dwtl/create-token (ctob/make-token :id token-id + :name "color.primary" + :type :color + :value "#000000"))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file')] + + (t/testing "Global set has been created with the token" + (t/is (some? tokens-lib')) + (t/is (some? (ctob/get-set-by-name tokens-lib' "Global")))))))))) + +(t/deftest update-token + (t/async + done + (let [file (setup-file-with-token-lib-and-token) + store (ths/setup-store file) + events [(dwtl/update-token (cthi/id :test-token-set) + (cthi/id :color.primary) + {:value "#ffffff" + :name "color.primary.updated"})]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file') + token' (ctob/get-token tokens-lib' (cthi/id :test-token-set) + (cthi/id :color.primary))] + + (t/testing "Token has been updated" + (t/is (= "color.primary.updated" (:name token'))) + (t/is (= "#ffffff" (:value token')))))))))) + +(t/deftest update-token-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/update-token (uuid/next) (uuid/next) {:value "#ffffff"})]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest delete-token + (t/async + done + (let [file (setup-file-with-token-lib-and-token) + store (ths/setup-store file) + events [(dwtl/delete-token (cthi/id :test-token-set) + (cthi/id :color.primary))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file') + token' (ctob/get-token tokens-lib' (cthi/id :test-token-set) + (cthi/id :color.primary))] + + (t/testing "Token has been deleted" + (t/is (nil? token'))))))))) + +(t/deftest delete-token-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/delete-token (uuid/next) (uuid/next))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +(t/deftest duplicate-token + (t/async + done + (let [file (setup-file-with-token-lib-and-token) + store (ths/setup-store file) + events [(dwtl/set-selected-token-set-id (cthi/id :test-token-set)) + (dwtl/duplicate-token (cthi/id :color.primary))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file') + tokens (ctob/get-tokens tokens-lib' (cthi/id :test-token-set))] + + (t/testing "Token has been duplicated" + (t/is (= 2 (count tokens)))))))))) + +(t/deftest duplicate-token-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + events [(dwtl/duplicate-token (uuid/next))]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file-data (-> (ths/get-file-from-state new-state) :data)] + (t/testing "No crash when file has no tokens-lib" + (t/is (some? file-data))))))))) + +;; ========================================================================== +;; Import Tokens Lib +;; ========================================================================== + +(t/deftest import-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + new-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "Imported Set"))) + events [(dwtl/import-tokens-lib new-lib)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file')] + + (t/testing "Tokens lib has been imported" + (t/is (some? tokens-lib')) + (t/is (some? (ctob/get-set-by-name tokens-lib' "Imported Set")))))))))) + +(t/deftest import-tokens-lib-no-tokens-lib + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + new-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "Imported Set"))) + events [(dwtl/import-tokens-lib new-lib)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + tokens-lib' (toht/get-tokens-lib file')] + (t/testing "Tokens lib has been imported into file without existing lib" + (t/is (some? tokens-lib')) + (t/is (some? (ctob/get-set-by-name tokens-lib' "Imported Set")))))))))) diff --git a/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs index 83bec23824..acad25311c 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_remapping_test.cljs @@ -13,6 +13,7 @@ [app.common.test-helpers.tokens :as ctht] [app.common.types.token :as cto] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.main.data.workspace.tokens.remapping :as dwtr] [cljs.test :as t :include-macros true])) @@ -27,19 +28,20 @@ :name "color.secondary" :value "{color.primary}" :type :color}] - (-> (cthf/sample-file :file-1 :page-label :page-1) - (ctho/add-rect :rect-1) - (ctho/add-rect :rect-2) - (assoc-in [:data :tokens-lib] - (-> (ctob/make-tokens-lib) - (ctob/add-theme (ctob/make-token-theme :name "Theme A" :sets #{"Set A"})) - (ctob/set-active-themes #{"/Theme A"}) + (-> (ctht/sample-file-with-tokens + :lib-fn #(-> % (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a) :name "Set A")) + (ctob/add-theme (ctob/make-token-theme :id (cthi/new-id! :theme-a) + :name "Theme A" + :sets #{"Set A"})) (ctob/add-token (cthi/id :set-a) (ctob/make-token color-token)) (ctob/add-token (cthi/id :set-a) - (ctob/make-token alias-token)))) + (ctob/make-token alias-token))) + :status-fn #(ctos/set-tokens-status % #{(cthi/id :theme-a)} #{(cthi/id :set-a)})) + (ctho/add-rect :rect-1) + (ctho/add-rect :rect-2) ;; Apply the token to rect-1 (ctht/apply-token-to-shape :rect-1 "color.primary" [:fill] [:fill] "#FF0000")))) @@ -132,22 +134,23 @@ :name "color.secondary" :value "#00FF00" :type :color}] - (-> (cthf/sample-file :file-1 :page-label :page-1) + (-> (ctht/sample-file-with-tokens + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a) + :name "Set A")) + (ctob/add-theme (ctob/make-token-theme :id (cthi/new-id! :theme-a) + :name "Theme A" + :sets #{"Set A"})) + (ctob/add-token (cthi/id :set-a) + (ctob/make-token color-primary)) + (ctob/add-token (cthi/id :set-a) + (ctob/make-token color-secondary))) + :status-fn #(ctos/set-tokens-status % #{(cthi/id :theme-a)} #{(cthi/id :set-a)})) (ctho/add-simple-component-with-copy :component1 :main-root :main-child :copy-root :copy-root-params {:children-labels [:copy-child]}) - (assoc-in [:data :tokens-lib] - (-> (ctob/make-tokens-lib) - (ctob/add-theme (ctob/make-token-theme :name "Theme A" :sets #{"Set A"})) - (ctob/set-active-themes #{"/Theme A"}) - (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :set-a) - :name "Set A")) - (ctob/add-token (cthi/id :set-a) - (ctob/make-token color-primary)) - (ctob/add-token (cthi/id :set-a) - (ctob/make-token color-secondary)))) (ctht/apply-token-to-shape :main-child "color.primary" [:fill] [:fill] "#FF0000") (ctht/apply-token-to-shape :copy-child "color.primary" [:fill] [:fill] "#FF0000")))) diff --git a/frontend/test/frontend_tests/tokens/logic/tokens_status_test.cljs b/frontend/test/frontend_tests/tokens/logic/tokens_status_test.cljs new file mode 100644 index 0000000000..641dcf0f87 --- /dev/null +++ b/frontend/test/frontend_tests/tokens/logic/tokens_status_test.cljs @@ -0,0 +1,118 @@ +(ns frontend-tests.tokens.logic.tokens-status-test + (:require + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.tokens :as tho] + [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] + [app.main.data.workspace.tokens.library-edit :as dwtl] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.pages :as thp] + [frontend-tests.helpers.state :as ths] + [frontend-tests.tokens.helpers.state :as tohs])) + +(t/use-fixtures :each + {:before thp/reset-idmap!}) + +(defn- setup-file + "Create a file with several token themes and token sets, some active and + some inactive. Returns the file map with :tokens-lib and :tokens-status + in its :data." + [] + (-> (tho/sample-file-with-tokens + :lib-fn #(-> % + ;; Add token sets + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-active) + :name "set-active")) + (ctob/add-set (ctob/make-token-set :id (thi/new-id! :set-inactive) + :name "set-inactive")) + ;; Add themes + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-active) + :name "theme-active" + :group "group-1" + :sets #{"set-active"})) + (ctob/add-theme (ctob/make-token-theme :id (thi/new-id! :theme-inactive) + :name "theme-inactive" + :group "group-1" + :sets #{"set-inactive"}))) + :status-fn #(-> % + (ctos/set-tokens-status #{(thi/id :theme-active)} + #{(thi/id :set-active)}))))) + +(defn- get-tokens-status [file] + (get-in file [:data :tokens-status])) + +(t/deftest test-set-token-theme-active-deactivate + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + theme-id (thi/id :theme-active) + set-id (thi/id :set-active) + events [(dwtl/set-token-theme-active theme-id false)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + status (get-tokens-status file')] + (t/is (not (ctos/theme-active? status theme-id)) + "theme should be inactive after deactivation") + (t/is (not (ctos/set-active? status set-id)) + "set should be inactive when its theme is deactivated"))))))) + +(t/deftest test-set-token-theme-active-activate + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + theme-id (thi/id :theme-inactive) + set-id (thi/id :set-inactive) + events [(dwtl/set-token-theme-active theme-id true)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + status (get-tokens-status file')] + (t/is (ctos/theme-active? status theme-id) + "theme should be active after activation") + (t/is (ctos/set-active? status set-id) + "set should be active when its theme is activated"))))))) + +(t/deftest test-toggle-token-theme-active-deactivate + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + theme-id (thi/id :theme-active) + set-id (thi/id :set-active) + events [(dwtl/toggle-token-theme-active theme-id)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + status (get-tokens-status file')] + (t/is (not (ctos/theme-active? status theme-id)) + "active theme should become inactive after toggle") + (t/is (not (ctos/set-active? status set-id)) + "set should be inactive when its theme is toggled off"))))))) + +(t/deftest test-toggle-token-theme-active-activate + (t/async + done + (let [file (setup-file) + store (ths/setup-store file) + theme-id (thi/id :theme-inactive) + set-id (thi/id :set-inactive) + events [(dwtl/toggle-token-theme-active theme-id)]] + + (tohs/run-store-async + store done events + (fn [new-state] + (let [file' (ths/get-file-from-state new-state) + status (get-tokens-status file')] + (t/is (ctos/theme-active? status theme-id) + "inactive theme should become active after toggle") + (t/is (ctos/set-active? status set-id) + "set should be active when its theme is toggled on"))))))) diff --git a/frontend/test/frontend_tests/tokens/workspace_tokens_remap_test.cljs b/frontend/test/frontend_tests/tokens/workspace_tokens_remap_test.cljs index 679592d6bb..40c20f1ce0 100644 --- a/frontend/test/frontend_tests/tokens/workspace_tokens_remap_test.cljs +++ b/frontend/test/frontend_tests/tokens/workspace_tokens_remap_test.cljs @@ -10,8 +10,10 @@ [app.common.test-helpers.files :as cthf] [app.common.test-helpers.ids-map :as cthi] [app.common.test-helpers.shapes :as cths] + [app.common.test-helpers.tokens :as ctht] [app.common.types.text :as txt] [app.common.types.tokens-lib :as ctob] + [app.common.types.tokens-status :as ctos] [app.main.data.workspace.tokens.remapping :as remap] [cljs.test :as t :include-macros true] [frontend-tests.helpers.state :as ths] @@ -31,17 +33,22 @@ (defn- attach-token-set [file tokens] (let [set-id (cthi/new-id! :token-set) - tokens-lib (reduce - (fn [lib token] - (ctob/add-token lib set-id (ctob/make-token token))) - (-> (ctob/make-tokens-lib) - (ctob/add-set (ctob/make-token-set :id set-id - :name token-set-name)) - (ctob/add-theme (ctob/make-token-theme :name token-theme-name - :sets #{token-set-name})) - (ctob/set-active-themes #{(str "/" token-theme-name)})) - tokens)] - (assoc-in file [:data :tokens-lib] tokens-lib))) + theme-id (cthi/new-id! :theme)] + (-> file + (ctht/add-tokens-lib) + (ctht/update-tokens-lib + #(reduce + (fn [lib token] + (ctob/add-token lib set-id (ctob/make-token token))) + (-> % + (ctob/add-set (ctob/make-token-set :id set-id + :name token-set-name)) + (ctob/add-theme (ctob/make-token-theme :id theme-id + :name token-theme-name + :sets #{token-set-name}))) + tokens)) + (ctht/update-tokens-status + #(ctos/set-tokens-status % #{theme-id} #{set-id}))))) (defn- tokens-lib [file] (get-in file [:data :tokens-lib])) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 6506243c4c..e30569db24 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -7712,14 +7712,22 @@ msgid_plural "workspace.libraries.components" msgstr[0] "1 component" msgstr[1] "%s components" -#: src/app/main/ui/workspace/libraries.cljs:367 -msgid "workspace.libraries.connected-to" -msgstr "Connected to" +#: src/app/main/ui/workspace/libraries.cljs:541 +msgid "workspace.libraries.connected-libraries" +msgstr "Connected library" + +#: src/app/main/ui/workspace/libraries.cljs:550 +msgid "workspace.libraries.connected-through" +msgstr "Connected through" #: src/app/main/ui/workspace/libraries.cljs:432 msgid "workspace.libraries.empty.add-some" msgstr "Or add some of these to try:" +#: src/app/main/ui/workspace/libraries.cljs:600 +msgid "workspace.libraries.empty.no-connected-libraries" +msgstr "No connected libraries yet." + #: src/app/main/ui/workspace/libraries.cljs:426 msgid "workspace.libraries.empty.no-libraries" msgstr "There are no Shared Libraries at your team, you can look for" @@ -7732,6 +7740,14 @@ msgstr "some templates in here" msgid "workspace.libraries.file-library" msgstr "File library" +#: src/app/main/ui/workspace/libraries.cljs:336 +msgid "workspace.libraries.tokens-source" +msgstr "Tokens source" + +#: src/app/main/ui/workspace/libraries.cljs:336 +msgid "workspace.libraries.set-as-tokens-source" +msgstr "Set as tokens source" + #: src/app/main/ui/workspace/libraries.cljs:100, src/app/main/ui/workspace/libraries.cljs:124 msgid "workspace.libraries.graphics" msgid_plural "workspace.libraries.graphics" @@ -7780,6 +7796,10 @@ msgstr "Search shared libraries" msgid "workspace.libraries.shared-libraries" msgstr "SHARED LIBRARIES" +#: src/app/main/ui/workspace/libraries.cljs:311 +msgid "workspace.libraries.shared-library-added" +msgstr "Added" + #: src/app/main/ui/workspace/libraries.cljs:414 msgid "workspace.libraries.shared-library-btn" msgstr "Connect library" @@ -7825,6 +7845,24 @@ msgid_plural "workspace.libraries.typography" msgstr[0] "1 typography" msgstr[1] "%s typographies" +#: src/app/main/ui/workspace/libraries.cljs:106, src/app/main/ui/workspace/libraries.cljs:150 +msgid "workspace.libraries.tokens" +msgid_plural "workspace.libraries.tokens" +msgstr[0] "1 token" +msgstr[1] "%s tokens" + +#: src/app/main/ui/workspace/libraries.cljs:106, src/app/main/ui/workspace/libraries.cljs:150 +msgid "workspace.libraries.token-sets" +msgid_plural "workspace.libraries.token-sets" +msgstr[0] "1 set" +msgstr[1] "%s sets" + +#: src/app/main/ui/workspace/libraries.cljs:106, src/app/main/ui/workspace/libraries.cljs:150 +msgid "workspace.libraries.token-themes" +msgid_plural "workspace.libraries.token-themes" +msgstr[0] "1 theme" +msgstr[1] "%s themes" + #: src/app/main/ui/workspace/libraries.cljs:381 msgid "workspace.libraries.unlink-library-btn" msgstr "Disconnect library" @@ -10437,6 +10475,32 @@ msgstr "Use a reference" msgid "workspace.tokens.value-not-valid" msgstr "The value is not valid" + +#: src/app/main/ui/workspace/sidebar/assets/file_library.cljs:102 +msgid "workspace.tokens.current-tokens-source-tooltip" +msgstr "This is the current Tokens source" + + +#: src/app/main/data/workspace/libraries.cljs:847 +msgid "workspace.tokens.notifications.source-sets-or-themes-updated" +msgstr "Tokens source sets or themes have been modified" + +#: src/app/main/ui/workspace/tokens/tokens_source.cljs:74 +msgid "workspace.tokens.source" +msgstr "Source:" + +#: src/app/main/ui/workspace/tokens/tokens_source.cljs:79 +msgid "workspace.tokens.this-file" +msgstr "This file" + +#: src/app/main/ui/workspace/tokens/tokens_source.cljs:96 +msgid "workspace.tokens.change-token-source" +msgstr "Change token source" + +#: src/app/main/ui/workspace/tokens/tokens_source.cljs:102 +msgid "workspace.tokens.open-source-new-tab" +msgstr "Open tokens' source in a new tab" + #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs #, unused msgid "workspace.tokens.warning-name-change" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 93b54788af..6863ca36cd 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -7546,10 +7546,22 @@ msgid_plural "workspace.libraries.components" msgstr[0] "1 componente" msgstr[1] "%s componentes" +#: src/app/main/ui/workspace/libraries.cljs:541 +msgid "workspace.libraries.connected-libraries" +msgstr "Biblioteca conectada" + +#: src/app/main/ui/workspace/libraries.cljs:550 +msgid "workspace.libraries.connected-through" +msgstr "Conectada mediante" + #: src/app/main/ui/workspace/libraries.cljs:367 msgid "workspace.libraries.connected-to" msgstr "Conectada con" +#: src/app/main/ui/workspace/libraries.cljs:600 +msgid "workspace.libraries.empty.no-connected-libraries" +msgstr "Aun no hay bibliotecas conectadas." + #: src/app/main/ui/workspace/libraries.cljs:432 msgid "workspace.libraries.empty.add-some" msgstr "O añadir algunas de éstas para probar:" @@ -7566,6 +7578,14 @@ msgstr "algunas plantillas aquí" msgid "workspace.libraries.file-library" msgstr "Biblioteca del archivo" +#: src/app/main/ui/workspace/libraries.cljs:336 +msgid "workspace.libraries.tokens-source" +msgstr "Fuente de tokens" + +#: src/app/main/ui/workspace/libraries.cljs:336 +msgid "workspace.libraries.set-as-tokens-source" +msgstr "Activar fuente de tokens" + #: src/app/main/ui/workspace/libraries.cljs:100, src/app/main/ui/workspace/libraries.cljs:124 msgid "workspace.libraries.graphics" msgid_plural "workspace.libraries.graphics" @@ -7614,6 +7634,10 @@ msgstr "Buscar bibliotecas compartidas" msgid "workspace.libraries.shared-libraries" msgstr "BIBLIOTECAS COMPARTIDAS" +#: src/app/main/ui/workspace/libraries.cljs:311 +msgid "workspace.libraries.shared-library-added" +msgstr "Añadido" + #: src/app/main/ui/workspace/libraries.cljs:414 msgid "workspace.libraries.shared-library-btn" msgstr "Conectar biblioteca" @@ -7654,6 +7678,24 @@ msgid_plural "workspace.libraries.typography" msgstr[0] "1 tipografía" msgstr[1] "%s tipografías" +#: src/app/main/ui/workspace/libraries.cljs:106, src/app/main/ui/workspace/libraries.cljs:150 +msgid "workspace.libraries.tokens" +msgid_plural "workspace.libraries.tokens" +msgstr[0] "1 token" +msgstr[1] "%s tokens" + +#: src/app/main/ui/workspace/libraries.cljs:106, src/app/main/ui/workspace/libraries.cljs:150 +msgid "workspace.libraries.token-sets" +msgid_plural "workspace.libraries.token-sets" +msgstr[0] "1 token set" +msgstr[1] "%s token sets" + +#: src/app/main/ui/workspace/libraries.cljs:106, src/app/main/ui/workspace/libraries.cljs:150 +msgid "workspace.libraries.token-themes" +msgid_plural "workspace.libraries.token-themes" +msgstr[0] "1 token theme" +msgstr[1] "%s token themes" + #: src/app/main/ui/workspace/libraries.cljs:381 msgid "workspace.libraries.unlink-library-btn" msgstr "Desconectar biblioteca" @@ -10106,6 +10148,32 @@ msgstr "Usa una referencia" msgid "workspace.tokens.value-not-valid" msgstr "El valor no es válido" + +#: src/app/main/ui/workspace/sidebar/assets/file_library.cljs:102 +msgid "workspace.tokens.current-tokens-source-tooltip" +msgstr "Esta es la actual fuente de Tokens" + + +#: src/app/main/data/workspace/libraries.cljs:847 +msgid "workspace.tokens.notifications.source-sets-or-themes-updated" +msgstr "Los sets o temas de la fuente de tokens se han modificado" + +#: src/app/main/ui/workspace/tokens/tokens_source.cljs:74 +msgid "workspace.tokens.source" +msgstr "Fuente:" + +#: src/app/main/ui/workspace/tokens/tokens_source.cljs:79 +msgid "workspace.tokens.this-file" +msgstr "Este archivo" + +#: src/app/main/ui/workspace/tokens/tokens_source.cljs:96 +msgid "workspace.tokens.change-token-source" +msgstr "Cambiar fuente de tokens" + +#: src/app/main/ui/workspace/tokens/tokens_source.cljs:102 +msgid "workspace.tokens.open-source-new-tab" +msgstr "Abrir la fuente de tokens en una pestaña nueva" + #: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs #, unused msgid "workspace.tokens.warning-name-change" diff --git a/plugins/apps/poc-tokens-plugin/src/app/app.component.html b/plugins/apps/poc-tokens-plugin/src/app/app.component.html index 9bb6fc8b9a..8a1ce8b9f3 100644 --- a/plugins/apps/poc-tokens-plugin/src/app/app.component.html +++ b/plugins/apps/poc-tokens-plugin/src/app/app.component.html @@ -6,13 +6,15 @@

THEMES

- + @if (isEditableTokens) { + + }
    @@ -24,20 +26,22 @@ } {{ theme.name }} - - + @if (isEditableTokens) { + + + }

    SETS

    - + @if (isEditableTokens) { + + }
      @@ -69,20 +79,22 @@ {{ set.name }} - - + @if (isEditableTokens) { + + + }
      {{ group[0] }} - + @if (isEditableTokens) { + + } @for (token of group[1]; track token.id) {
    • {{ token.name }} - - + @if (isEditableTokens) { + + + }
    • } } diff --git a/plugins/apps/poc-tokens-plugin/src/app/app.component.ts b/plugins/apps/poc-tokens-plugin/src/app/app.component.ts index 089649f6ac..849e890e16 100644 --- a/plugins/apps/poc-tokens-plugin/src/app/app.component.ts +++ b/plugins/apps/poc-tokens-plugin/src/app/app.component.ts @@ -59,6 +59,7 @@ export class AppComponent { ), ); + public isEditableTokens: boolean = false; public themes: TokenTheme[] = []; public sets: TokenSet[] = []; public tokenGroups: TokensGroup[] = []; @@ -72,6 +73,8 @@ export class AppComponent { this.#setSets(event.data.setsData); } else if (event.data.type === 'set-tokens') { this.#setTokens(event.data.tokenGroupsData); + } else if (event.data.type === 'set-is-editable-tokens') { + this.#setIsEditableTokens(event.data.isEditableTokens); } }); } @@ -268,6 +271,10 @@ export class AppComponent { parent.postMessage(message, '*'); } + #setIsEditableTokens(isEditableTokens: boolean) { + this.isEditableTokens = isEditableTokens; + } + #setThemes(themes: TokenTheme[]) { this.themes = themes; } diff --git a/plugins/apps/poc-tokens-plugin/src/plugin.ts b/plugins/apps/poc-tokens-plugin/src/plugin.ts index 48ef5149cc..e11fa17ec9 100644 --- a/plugins/apps/poc-tokens-plugin/src/plugin.ts +++ b/plugins/apps/poc-tokens-plugin/src/plugin.ts @@ -58,6 +58,14 @@ function sendMessage(message: PluginMessageEvent) { function loadLibrary() { const tokensCatalog = penpot.library.local.tokens; + const isEditableTokens = tokensCatalog.isEditableTokens; + + penpot.ui.sendMessage({ + source: 'penpot', + type: 'set-is-editable-tokens', + isEditableTokens, + }); + const themes = tokensCatalog.themes; const themesData = themes.map((theme) => { diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index 83a0c9ec66..b7dbcc02fc 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -5220,6 +5220,14 @@ export type Token = * Themes. */ export interface TokenCatalog { + /** + * Returns true if the tokens can be edited (it can't + * if the tokens of the file are in an external library). + * Note that even in this case, Themes and Sets status still + * can be changed and tokens can be applied to shapes. + */ + readonly isEditableTokens: boolean; + /** * The list of themes in this catalog, in creation order. */