diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index 2c6ff77afc..1a010c265b 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -683,7 +683,8 @@ "Make the file its own tokens source when its tokens source is not one of the given library-ids, as happens when the library was deleted. The tokens lib of the source is copied while the library still exists, even - if deleted; otherwise the tokens source and status are removed." + if deleted; otherwise the tokens source is removed and the file gets back + the tokens status kept aside for its own tokens lib, if any." [cfg data library-ids] (let [source-id (cfo/get-tokens-source data)] (if (and (some? source-id) @@ -692,7 +693,7 @@ :include-deleted? true :throw-if-not-exists? false)] (cfo/absorb-tokens-lib data (:data library)) - (dissoc data :tokens-source :tokens-status)) + (cfo/detach-tokens-source data)) data))) (defn embed-assets diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index ce45edd485..8e4bbb67d9 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -407,6 +407,12 @@ [:theme-ids [:set ::sm/uuid]] [:set-ids [:set ::sm/uuid]]]] + [:set-own-tokens-status + [:map {:title "SetOwnTokensStatus"} + [:type [:= :set-own-tokens-status]] + [:theme-ids [:maybe [:set ::sm/uuid]]] + [:set-ids [:maybe [:set ::sm/uuid]]]]] + [:rename-token-set-group [:map {:title "RenameTokenSetGroup"} [:type [:= :rename-token-set-group]] @@ -1067,6 +1073,13 @@ (-> (cfo/ensure-tokens-lib data) (cfo/update-tokens-status ctos/set-tokens-status theme-ids set-ids))) +(defmethod process-change :set-own-tokens-status + [data {:keys [theme-ids set-ids]}] + (cfo/set-own-tokens-status data + (when (or (some? theme-ids) (some? set-ids)) + (ctos/make-tokens-status :active-theme-ids theme-ids + :active-set-ids set-ids)))) + (defmethod process-change :rename-token-set-group [data {:keys [set-group-path set-group-fname]}] (-> (cfo/ensure-tokens-lib data) diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 1e71d05b4d..22f7369a19 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -1077,6 +1077,24 @@ (update :undo-changes conj {:type :set-tokens-status :theme-ids prev-theme-ids :set-ids prev-set-ids}) (apply-changes-local))))) +(defn set-own-tokens-status + "Keep aside the tokens status of the file's own tokens-lib, or remove it + when `tokens-status` is nil." + [changes tokens-status] + (assert-library! changes) + (assert (or (ctos/tokens-status? tokens-status) + (nil? tokens-status))) + (let [library-data (::library-data (meta changes)) + prev-tokens-status (cfo/get-own-tokens-status library-data) + ->change (fn [tokens-status] + {:type :set-own-tokens-status + :theme-ids (some-> tokens-status ctos/get-active-theme-ids) + :set-ids (some-> tokens-status ctos/get-active-set-ids)})] + (-> changes + (update :redo-changes conj (->change tokens-status)) + (update :undo-changes conj (->change prev-tokens-status)) + (apply-changes-local)))) + (defn rename-token-set-group [changes set-group-path set-group-fname] (let [undo-path (ctob/replace-last-path-name set-group-path set-group-fname) diff --git a/common/src/app/common/files/tokens.cljc b/common/src/app/common/files/tokens.cljc index e8a28e7ca2..f8fb4e32aa 100644 --- a/common/src/app/common/files/tokens.cljc +++ b/common/src/app/common/files/tokens.cljc @@ -549,6 +549,37 @@ [file-data f & args] (d/update-when file-data :tokens-status #(apply f % args))) +(defn get-own-tokens-status + "The tokens status of the file's own tokens-lib, kept aside while the + tokens source is an external library." + [file-data] + (:own-tokens-status file-data)) + +(defn set-own-tokens-status + "Keep aside the tokens status of the file's own tokens-lib. Setting it to + nil removes the attribute." + [file-data tokens-status] + (assert (or (nil? tokens-status) (ctos/tokens-status? tokens-status)) + "expected nil or valid tokens-status") + (if (nil? tokens-status) + (dissoc file-data :own-tokens-status) + (assoc file-data :own-tokens-status tokens-status))) + +(defn detach-tokens-source + "Make the file its own tokens source again, restoring the tokens status + kept aside for its own tokens-lib. If none was kept, the tokens status + is removed." + [file-data] + (let [own-tokens-status (get-own-tokens-status file-data)] + (if (some? own-tokens-status) + (-> file-data + (set-tokens-source nil) + (set-own-tokens-status nil) + (assoc :tokens-status own-tokens-status)) + (-> file-data + (set-tokens-source nil) + (dissoc :tokens-status))))) + (defn absorb-tokens-lib "Copy the tokens-lib of the library into the file, replacing the previous one if any, and unset the tokens-source, so the file becomes @@ -558,19 +589,26 @@ one of the library is copied along with the tokens-lib. If the library has no tokens-lib, only the tokens-source is unset, and - the file keeps its own tokens-lib and tokens-status. If there is no - library-data, there is nothing to absorb and the file is returned - unchanged." + the file keeps its own tokens-lib, with the tokens status kept aside for + it if any. If there is no library-data, there is nothing to absorb and + the file is returned unchanged." [file-data library-data] (if (nil? library-data) file-data (let [tokens-lib (get-tokens-lib library-data)] (if (nil? tokens-lib) - (set-tokens-source file-data nil) + (let [own-tokens-status (get-own-tokens-status file-data)] + (cond-> (-> file-data + (set-tokens-source nil) + (set-own-tokens-status nil)) + (some? own-tokens-status) + (assoc :tokens-status own-tokens-status))) (let [tokens-status (or (get-tokens-status file-data) (get-tokens-status library-data))] (cond-> (-> file-data (set-tokens-source nil) + ;; The kept status refers to the replaced tokens-lib + (set-own-tokens-status nil) (assoc :tokens-lib tokens-lib)) (some? tokens-status) (assoc :tokens-status tokens-status))))))) diff --git a/common/src/app/common/logic/tokens.cljc b/common/src/app/common/logic/tokens.cljc index 53d2ae5396..c1f948e8c7 100644 --- a/common/src/app/common/logic/tokens.cljc +++ b/common/src/app/common/logic/tokens.cljc @@ -16,36 +16,58 @@ ;; Tokens source (defn- own-tokens-status - "The current tokens status of `file-data`, keeping only the themes and sets - that exist in its own tokens lib." - [file-data] - (let [tokens-status (cfo/get-tokens-status file-data) - tokens-lib (cfo/get-tokens-lib file-data)] + "`tokens-status` keeping only the themes and sets that exist in the own + tokens lib of `file-data`." + [file-data tokens-status] + (let [tokens-lib (cfo/get-tokens-lib file-data)] (if (and tokens-status tokens-lib) (cfo/sync-tokens-status-with-lib tokens-status tokens-lib) (ctos/make-tokens-status)))) (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 the file itself, - the themes and sets that don't exist in its own tokens lib are deactivated. - If the library is nil, the tokens source is removed and the tokens status - is cleared." + copying the library's tokens status. + + While an external library is the source, the tokens status of the file's + own tokens lib is kept aside, and it is restored when the file goes back + to being its own source (the library is the file itself, or nil). With + nothing kept aside, going back to the file itself deactivates the themes + and sets that don't exist in its own tokens lib, and a nil library clears + the tokens status." [changes library] - (let [library-id (:id library) - file-data (pcb/get-library-data changes) - library-tokens-status (cond - (nil? library) - (ctos/make-tokens-status) + (let [file-data (pcb/get-library-data changes) + file-id (:id file-data) + library-id (:id library) + to-own? (or (nil? library) (= library-id file-id)) + from-own? (cfo/effective-tokens-source? file-data file-id) + current-status (cfo/get-tokens-status file-data) + kept-status (cfo/get-own-tokens-status file-data) - (= library-id (:id file-data)) - (own-tokens-status file-data) + tokens-status + (cond + (and to-own? (some? kept-status)) + (own-tokens-status file-data kept-status) - :else - (-> library ctf/file-data cfo/get-tokens-status))] - (-> changes - (pcb/set-tokens-source library-id) - (pcb/set-tokens-status library-tokens-status)))) + (nil? library) + (ctos/make-tokens-status) + + to-own? + (own-tokens-status file-data current-status) + + :else + (-> library ctf/file-data cfo/get-tokens-status)) + + kept-status' + (cond + to-own? nil + from-own? (own-tokens-status file-data current-status) + :else kept-status)] + + (cond-> (-> changes + (pcb/set-tokens-source library-id) + (pcb/set-tokens-status tokens-status)) + (not (identical? kept-status kept-status')) + (pcb/set-own-tokens-status kept-status')))) ;; Tokens lib diff --git a/common/src/app/common/test_helpers/tokens.cljc b/common/src/app/common/test_helpers/tokens.cljc index 23a0550a4a..88923c691b 100644 --- a/common/src/app/common/test_helpers/tokens.cljc +++ b/common/src/app/common/test_helpers/tokens.cljc @@ -30,6 +30,10 @@ [file] (-> file (ctf/file-data) (cfo/get-tokens-status))) +(defn get-own-tokens-status + [file] + (-> file (ctf/file-data) (cfo/get-own-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] diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 56ac4a747d..0d589eba41 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -90,7 +90,8 @@ [:plugin-data {:optional true} schema:plugin-data] [: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]]) + [:tokens-status {:optional true} ctos/schema:tokens-status] + [:own-tokens-status {:optional true} ctos/schema:tokens-status]]) ;; Kept aside while the tokens source is an external library (def schema:file-metadata [:map {:title "Metadata"} @@ -1242,7 +1243,7 @@ (cond-> (update-in file [:data :pages-index] detach-pages) (not (cfo/effective-tokens-source? (:data file) file-id)) - (update :data dissoc :tokens-source :tokens-status)))) + (update :data cfo/detach-tokens-source)))) ;; Base font size diff --git a/common/test/common_tests/logic/token_test.cljc b/common/test/common_tests/logic/token_test.cljc index 799d895f44..f9b11e2fb3 100644 --- a/common/test/common_tests/logic/token_test.cljc +++ b/common/test/common_tests/logic/token_test.cljc @@ -115,6 +115,54 @@ (t/is (= (ctos/get-active-set-ids file-tokens-status) (ctos/get-active-set-ids undo-tokens-status))))) + (t/testing "the file's own tokens status is kept aside while a library is the source, and restored when going back" + (let [own-set-id (uuid/next) + lib-set-id (uuid/next) + lib2-set-id (uuid/next) + library-file (tht/sample-file-with-tokens + :file-id :library + :lib-fn #(ctob/add-set % (ctob/make-token-set :id lib-set-id :name "library set")) + :status-fn #(ctos/set-tokens-status % #{} #{lib-set-id})) + library2-file (tht/sample-file-with-tokens + :file-id :library2 + :lib-fn #(ctob/add-set % (ctob/make-token-set :id lib2-set-id :name "library2 set")) + :status-fn #(ctos/set-tokens-status % #{} #{lib2-set-id})) + file (tht/sample-file-with-tokens + :file-id :file1 + :lib-fn #(ctob/add-set % (ctob/make-token-set :id own-set-id :name "own set")) + :status-fn #(ctos/set-tokens-status % #{} #{own-set-id})) + set-source (fn [file library] + (let [changes (-> (pcb/empty-changes) + (pcb/with-library-data (ctf/file-data file)) + (clt/generate-set-tokens-source library))] + [(thf/apply-changes file changes) changes])) + active-sets #(some-> % ctos/get-active-set-ids) + + [to-library to-library-changes] (set-source file library-file) + [to-library2 _] (set-source to-library library2-file) + [back-to-own back-changes] (set-source to-library2 to-library2) + undo-to-library (thf/apply-undo-changes to-library to-library-changes) + undo-back-to-own (thf/apply-undo-changes back-to-own back-changes)] + + ;; A library as source: its status is used and the own status is kept aside + (t/is (= #{lib-set-id} (active-sets (tht/get-tokens-status to-library)))) + (t/is (= #{own-set-id} (active-sets (tht/get-own-tokens-status to-library)))) + + ;; Another library as source: the own status is still kept aside + (t/is (= #{lib2-set-id} (active-sets (tht/get-tokens-status to-library2)))) + (t/is (= #{own-set-id} (active-sets (tht/get-own-tokens-status to-library2)))) + + ;; Back to the file itself: the own status is restored, nothing is kept aside + (t/is (nil? (tht/get-tokens-source back-to-own))) + (t/is (= #{own-set-id} (active-sets (tht/get-tokens-status back-to-own)))) + (t/is (nil? (tht/get-own-tokens-status back-to-own))) + + ;; Undo works both ways + (t/is (= #{own-set-id} (active-sets (tht/get-tokens-status undo-to-library)))) + (t/is (nil? (tht/get-own-tokens-status undo-to-library))) + (t/is (= #{lib2-set-id} (active-sets (tht/get-tokens-status undo-back-to-own)))) + (t/is (= #{own-set-id} (active-sets (tht/get-own-tokens-status undo-back-to-own)))))) + (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 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 6b22169719..cb0f4ea573 100644 --- a/frontend/src/app/main/data/workspace/tokens/library_edit.cljs +++ b/frontend/src/app/main/data/workspace/tokens/library_edit.cljs @@ -510,7 +510,8 @@ (pcb/with-library-data data) (clo/generate-set-tokens-source library) (pcb/set-tokens-source library-id))] - (rx/of (dch/commit-changes changes)))))) + (rx/of (dch/commit-changes changes) + (dwtp/propagate-workspace-tokens)))))) (defn delete-token-set [id] diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index 8cea58e9b6..1471547c4d 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -97,6 +97,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-source-test] [frontend-tests.tokens.logic.tokens-status-test] [frontend-tests.tokens.style-dictionary-test] [frontend-tests.tokens.token-errors-test] @@ -246,6 +247,7 @@ 'frontend-tests.tokens.style-dictionary-test 'frontend-tests.tokens.token-errors-test 'frontend-tests.tokens.token-path-test + 'frontend-tests.tokens.logic.tokens-source-test 'frontend-tests.tokens.logic.tokens-status-test 'frontend-tests.tokens.workspace-tokens-remap-test 'frontend-tests.ui.check-updates-test diff --git a/frontend/test/frontend_tests/tokens/logic/tokens_source_test.cljs b/frontend/test/frontend_tests/tokens/logic/tokens_source_test.cljs new file mode 100644 index 0000000000..6813fd1da4 --- /dev/null +++ b/frontend/test/frontend_tests/tokens/logic/tokens_source_test.cljs @@ -0,0 +1,136 @@ +;; 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 frontend-tests.tokens.logic.tokens-source-test + (:require + [app.common.test-helpers.compositions :as ctho] + [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.workspace.libraries :as dwl] + [app.main.data.workspace.tokens.library-edit :as dwtl] + [app.main.repo :as rp] + [beicon.v2.core :as rx] + [cljs.test :as t :include-macros true] + [frontend-tests.helpers.async :as async] + [frontend-tests.helpers.mock :as mock] + [frontend-tests.helpers.pages :as thp] + [frontend-tests.helpers.state :as ths] + [frontend-tests.helpers.wasm :as thw] + [potok.v2.core :as ptk])) + +(t/use-fixtures :each + {:before (fn [] + (thp/reset-idmap!) + (thw/setup-wasm-mocks!)) + :after thw/teardown-wasm-mocks!}) + +(defn- setup-library + "A library whose active set defines `red` as green." + [] + (ctht/sample-file-with-tokens + :file-id :library + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :library-set) + :name "library-set")) + (ctob/add-token (cthi/id :library-set) + (ctob/make-token :id (cthi/new-id! :library-red) + :name "red" + :type :color + :value "#00ff00"))) + :status-fn #(ctos/set-tokens-status % #{} #{(cthi/id :library-set)}))) + +(defn- setup-file + "A file whose own active set defines `red` as dark red, applied to the + fill of a rectangle." + [] + (-> (ctht/sample-file-with-tokens + :file-id :local + :lib-fn #(-> % + (ctob/add-set (ctob/make-token-set :id (cthi/new-id! :local-set) + :name "local-set")) + (ctob/add-token (cthi/id :local-set) + (ctob/make-token :id (cthi/new-id! :local-red) + :name "red" + :type :color + :value "rgb(123, 34, 34)"))) + :status-fn #(ctos/set-tokens-status % #{} #{(cthi/id :local-set)})) + (ctho/add-rect :rect1) + (ctht/apply-token-to-shape :rect1 "red" [:fill] [:fill] "#7b2222"))) + +(defn- fill-color + [store] + (let [file (ths/get-file-from-state @store)] + (-> (cths/get-shape file :rect1) :fills first :fill-color))) + +(defn- active-set-ids + [store] + (-> (ths/get-file-from-state @store) ctht/get-tokens-status ctos/get-active-set-ids)) + +(defn- tokens-source + [store] + (-> (ths/get-file-from-state @store) ctht/get-tokens-source)) + +(defn- ^:async use-library-tokens + "Set `library` as tokens source and wait for the canvas to use it." + [store library] + (ptk/emit! store (dwtl/set-tokens-source (:id library))) + (await (async/wait-for #(= "#00ff00" (fill-color store)) + "the fill uses the library value of `red`" + {:max-ticks 200}))) + +(t/deftest ^:async switching-tokens-source-updates-the-canvas + (let [library (setup-library) + file (setup-file) + store (ths/setup-store file {:libraries [library]})] + + (t/is (= "#7b2222" (fill-color store))) + + (ptk/emit! store (dwtl/set-tokens-source (:id library))) + (await (async/wait-for #(= "#00ff00" (fill-color store)) + "the fill uses the library value of `red`" + {:max-ticks 200})) + (t/is (= "#00ff00" (fill-color store)) + "Setting the library as tokens source resolves `red` from the library") + + (ptk/emit! store (dwtl/set-tokens-source (:id file))) + (await (async/settle)) + (t/is (contains? (active-set-ids store) (cthi/id :local-set)) + "Going back to the local tokens keeps the local set active") + (await (async/wait-for #(= "#7b2222" (fill-color store)) + "the fill uses the local value of `red` again" + {:max-ticks 200})) + (t/is (= "#7b2222" (fill-color store)) + "Going back to the local tokens resolves `red` from the file again") + + (rx/dispose! store))) + +(t/deftest ^:async unlinking-the-tokens-source-library-restores-the-local-tokens + (let [library (setup-library) + file (setup-file) + store (ths/setup-store file {:libraries [library]})] + (await (use-library-tokens store library)) + + (await + (mock/with-mocks* + {rp/cmd! (mock/stub (fn [_ _] (rx/observe-on :async (rx/of []))))} + ;; The same events the libraries dialog emits on unlink + (ptk/emit! store + (dwl/unlink-file-from-library (:id file) (:id library)) + (dwtl/set-tokens-source (:id file))) + (await (async/wait-for #(= "#7b2222" (fill-color store)) + "the fill uses the local value of `red` again" + {:max-ticks 200})))) + + (t/is (nil? (tokens-source store))) + (t/is (contains? (active-set-ids store) (cthi/id :local-set)) + "Unlinking the library keeps the local set active") + (t/is (= "#7b2222" (fill-color store))) + + (rx/dispose! store))) +