diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 08f86bdb5b..79c50e7d3d 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -29,6 +29,7 @@ [app.common.types.shape-tree :as ctst] [app.common.types.text :as txt] [app.common.types.tokens-lib :as ctob] + [app.common.types.token-status :as ctos] [app.common.types.typographies-list :as ctyl] [app.common.types.typography :as cty] [app.common.uuid :as uuid] @@ -86,7 +87,8 @@ [:components {:optional true} schema:components] [:typographies {:optional true} schema:typographies] [:plugin-data {:optional true} schema:plugin-data] - [:tokens-lib {:optional true} ctob/schema:tokens-lib]]) + [:tokens-lib {:optional true} ctob/schema:tokens-lib] + [:token-status {:optional true} ctos/schema:token-status]]) (def schema:file "A schema for validate a file data structure; data is optional @@ -199,7 +201,12 @@ (defn ensure-tokens-lib "Ensure file-data has a :tokens-lib key, creating one if necessary." [file-data] - (update file-data :tokens-lib #(or % (ctob/make-tokens-lib)))) + (if (and (some? (:tokens-lib file-data)) (nil? (:token-status file-data))) + ;; TODO: remove this when we deprecate old-style files without token-status + (assoc file-data :token-status (ctos/make-token-status-from-lib (:tokens-lib file-data))) + (-> file-data + (update :tokens-lib #(or % (ctob/make-tokens-lib))) + (update :token-status #(or % (ctos/make-token-status)))))) ;; Helpers diff --git a/common/src/app/common/types/token_status.cljc b/common/src/app/common/types/token_status.cljc new file mode 100644 index 0000000000..f3cb9dc8c2 --- /dev/null +++ b/common/src/app/common/types/token_status.cljc @@ -0,0 +1,184 @@ +;; 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.common.types.token-status + (:require + #?(:clj [app.common.fressian :as fres]) + #?(:clj [clojure.data.json :as c.json]) + [app.common.schema :as sm] + [app.common.schema.generators :as sg] + [app.common.transit :as t] + [app.common.types.tokens-lib :as ctob] + [clojure.core.protocols :as cp] + [clojure.datafy :refer [datafy]] + [clojure.pprint :as pp])) + +;; TokenStatus datatype contains the status of the active themes and sets +;; in a tokens library. + +(defprotocol ITokenStatus + (activate-theme [_ theme-id] "Add a theme uuid to active themes") + (deactivate-theme [_ theme-id] "Remove a theme uuid from active themes") + (toggle-theme-active [_ theme-id] "Toggle a theme uuid in active themes") + (theme-active? [_ theme-id] "Check if a theme uuid is active") + (active-theme-count [_] "Return the number of active themes") + (activate-set [_ set-id] "Add a set uuid to active sets") + (deactivate-set [_ set-id] "Remove a set uuid from active sets") + (toggle-set-active [_ set-id] "Toggle a set uuid in active sets") + (set-active? [_ set-id] "Check if a set uuid is active") + (active-set-count [_] "Return the number of active sets")) + +(deftype TokenStatus [active-themes active-sets] + cp/Datafiable + (datafy [_] + {:active-themes active-themes + :active-sets active-sets}) + + #?@(:clj + [c.json/JSONWriter + (-write [this writter options] + (c.json/-write (datafy this) writter options))]) + + ITokenStatus + (activate-theme [_ theme-id] + (TokenStatus. (conj active-themes theme-id) active-sets)) + + (deactivate-theme [_ theme-id] + (TokenStatus. (disj active-themes theme-id) active-sets)) + + (toggle-theme-active [this theme-id] + (if (contains? active-themes theme-id) + (deactivate-theme this theme-id) + (activate-theme this theme-id))) + + (theme-active? [_ theme-id] + (contains? active-themes theme-id)) + + (active-theme-count [_] + (prn active-themes) + (count active-themes)) + + (activate-set [_ set-id] + (TokenStatus. active-themes (conj active-sets set-id))) + + (deactivate-set [_ set-id] + (TokenStatus. active-themes (disj active-sets set-id))) + + (toggle-set-active [this set-id] + (if (contains? active-sets set-id) + (deactivate-set this set-id) + (activate-set this set-id))) + + (set-active? [_ set-id] + (contains? active-sets set-id)) + + (active-set-count [_] + (count active-sets))) + +;; === Helper & Predicate === + +(defn map->TokenStatus + [{:keys [active-themes active-sets]}] + (TokenStatus. active-themes active-sets)) + +(defn token-status? + [o] + (instance? TokenStatus o)) + +;; === Schemas, Check functions & Constructor === + +(declare make-token-status) + +(def schema:token-status-attrs + [:map {:title "TokenStatus"} + [:active-themes [:set {:gen/max 5} ::sm/uuid]] + [:active-sets [:set {:gen/max 5} ::sm/uuid]]]) + +(def schema:token-status + [:and {:gen/gen (->> (sg/generator schema:token-status-attrs) + (sg/fmap #(make-token-status %)))} + [:fn token-status?]]) + +(def ^:private check-token-status-attrs + (sm/check-fn schema:token-status-attrs + :hint "expected valid params for token-status")) + +(def check-token-status + (sm/check-fn schema:token-status + :hint "expected valid token-status")) + +(defn make-token-status + [& {:as attrs}] + (-> attrs + (update :active-themes #(or % #{})) + (update :active-sets #(or % #{})) + (check-token-status-attrs) + (map->TokenStatus))) + +(defn make-token-status-from-lib + "Make a TokenStatus from a TokensLib, activating the themes and sets + marked as active in the library (to migrate from legacy files)." + [tokens-lib] + (let [active-themes (into #{} + (comp (map :id) + (filter #(not= % ctob/hidden-theme-id))) + (ctob/get-active-themes tokens-lib)) + active-sets (into #{} + (comp (map #(ctob/get-set-by-name tokens-lib %)) + (map ctob/get-id)) + (ctob/get-active-themes-set-names tokens-lib))] + (make-token-status :active-themes active-themes + :active-sets active-sets))) + +;; === Pretty-print for debugging === + +(defmethod pp/simple-dispatch TokenStatus [^TokenStatus obj] + (.write *out* "#penpot/token-status ") + (pp/pprint-newline :miser) + (pp/pprint (datafy obj))) + +#?(:clj + (do + (defmethod print-method TokenStatus + [^TokenStatus this ^java.io.Writer w] + (.write w "#penpot/token-status ") + (print-method (datafy this) w)) + + (defmethod print-dup TokenStatus + [^TokenStatus this ^java.io.Writer w] + (print-method this w))) + + :cljs + (extend-type TokenStatus + cljs.core/IPrintWithWriter + (-pr-writer [this writer opts] + (-write writer "#penpot/token-status ") + (-pr-writer (datafy this) writer opts)) + + cljs.core/IEncodeJS + (-clj->js [this] + (clj->js (datafy this))))) + +;; === Transit serialization === + +(t/add-handlers! + {:id "penpot/token-status" + :class TokenStatus + :wfn datafy + :rfn #(make-token-status %)}) + +;; === Fressian serialization === + +#?(:clj + (fres/add-handlers! + {:name "penpot/token-status/v1" + :class TokenStatus + :wfn (fn [n w o] + (fres/write-tag! w n 1) + (fres/write-object! w (datafy o))) + :rfn (fn [r] + (let [obj (fres/read-object! r)] + (make-token-status obj)))})) diff --git a/common/test/common_tests/types/file_test.cljc b/common/test/common_tests/types/file_test.cljc new file mode 100644 index 0000000000..05e34e4d72 --- /dev/null +++ b/common/test/common_tests/types/file_test.cljc @@ -0,0 +1,58 @@ +;; 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 common-tests.types.file-test + (:require + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.types.file :as ctf] + [app.common.types.tokens-lib :as ctob] + [app.common.types.token-status :as ctos] + [clojure.test :as t])) + +(t/deftest test-ensure-tokens-lib + (t/testing "ensure-tokens-lib should add a tokens-lib and token-status to the file data if they are missing, and should not modify them if they already exist" + (let [file (thf/sample-file :file1) + file-data (ctf/file-data file) + file-data' (ctf/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' :token-status)) + (t/is (ctos/token-status? (:token-status file-data'))))) + + (t/testing "ensure-tokens-lib should create a token-status from an existing tokens-lib") + (let [file (thf/sample-file :file1) + file-data (-> (ctf/file-data file) + (assoc :tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set + :id (thi/new-id! :set1) + :name "set1")) + (ctob/add-set (ctob/make-token-set + :id (thi/new-id! :set2) + :name "set2")) + (ctob/add-theme (ctob/make-token-theme + :id (thi/new-id! :theme1) + :name "theme1" + :sets #{"set1"})) + (ctob/add-theme (ctob/make-token-theme + :id ctob/hidden-theme-id + :name "HIDDEN_THEME" + :sets #{"set2"})) + (ctob/activate-theme ctob/hidden-theme-id) + (ctob/activate-theme (thi/id :theme1))))) + file-data' (ctf/ensure-tokens-lib file-data) + token-status' (:token-status file-data')] + + (t/is (contains? file-data' :tokens-lib)) + (t/is (ctob/tokens-lib? (:tokens-lib file-data'))) + (t/is (contains? file-data' :token-status)) + (t/is (ctos/token-status? token-status')) + (t/is (ctos/check-token-status token-status')) + (t/is (= 1 (ctos/active-theme-count token-status'))) + (t/is (true? (ctos/theme-active? token-status' (thi/id :theme1)))) + (t/is (= 1 (ctos/active-set-count token-status') 1)) + (t/is (ctos/set-active? token-status' (thi/id :set1))))) + diff --git a/common/test/common_tests/types/token_status_test.cljc b/common/test/common_tests/types/token_status_test.cljc new file mode 100644 index 0000000000..2252cfb2dd --- /dev/null +++ b/common/test/common_tests/types/token_status_test.cljc @@ -0,0 +1,173 @@ +;; 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 common-tests.types.token-status-test + (:require + #?(:clj [app.common.fressian :as fres]) + #?(:clj [clojure.data.json :as json]) + [app.common.test-helpers.ids-map :as thi] + [app.common.transit :as tr] + [app.common.types.tokens-lib :as ctob] + [app.common.types.token-status :as ctos] + [app.common.uuid :as uuid] + [clojure.datafy :refer [datafy]] + [clojure.test :as t])) + +(t/deftest make-token-status + (let [theme-id (uuid/next) + set-id (uuid/next) + status (ctos/make-token-status :active-themes #{theme-id} + :active-sets #{set-id})] + (t/is (ctos/token-status? status)) + (t/is (ctos/check-token-status status)) + (t/is (= (ctos/active-theme-count status) 1)) + (t/is (ctos/theme-active? status theme-id)) + (t/is (= (ctos/active-set-count status) 1)) + (t/is (ctos/set-active? status set-id)))) + +(t/deftest make-token-status-defaults + (let [status (ctos/make-token-status)] + (t/is (ctos/token-status? status)) + (t/is (ctos/check-token-status status)) + (t/is (= (ctos/active-theme-count status) 0)) + (t/is (= (ctos/active-set-count status) 0)))) + +(t/deftest make-invalid-token-status + (t/testing "non-set for active-themes" + (t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception) + #"expected valid params for token-status" + (ctos/make-token-status :active-themes [])))) + (t/testing "non-uuid in active-sets" + (t/is (thrown-with-msg? #?(:cljs js/Error :clj Exception) + #"expected valid params for token-status" + (ctos/make-token-status :active-sets #{"not-a-uuid"}))))) + +(t/deftest activate-theme + (let [theme-id (uuid/next) + status (ctos/make-token-status) + status' (ctos/activate-theme status theme-id)] + (t/is (not (ctos/theme-active? status theme-id))) + (t/is (ctos/theme-active? status' theme-id)) + (t/is (= (ctos/active-theme-count status') 1)))) + +(t/deftest deactivate-theme + (let [theme-id (uuid/next) + status (ctos/make-token-status :active-themes #{theme-id}) + status' (ctos/deactivate-theme status theme-id)] + (t/is (ctos/theme-active? status theme-id)) + (t/is (not (ctos/theme-active? status' theme-id))) + (t/is (= (ctos/active-theme-count status') 0)))) + +(t/deftest toggle-theme-active + (let [theme-id (uuid/next) + status (ctos/make-token-status) + status' (ctos/toggle-theme-active status theme-id) + status'' (ctos/toggle-theme-active status' theme-id)] + (t/is (ctos/theme-active? status' theme-id)) + (t/is (not (ctos/theme-active? status'' theme-id))) + (t/is (= (ctos/active-theme-count status') 1)) + (t/is (= (ctos/active-theme-count status'') 0)))) + +(t/deftest activate-set + (let [set-id (uuid/next) + status (ctos/make-token-status) + status' (ctos/activate-set status set-id)] + (t/is (not (ctos/set-active? status set-id))) + (t/is (ctos/set-active? status' set-id)) + (t/is (= (ctos/active-set-count status') 1)))) + +(t/deftest deactivate-set + (let [set-id (uuid/next) + status (ctos/make-token-status :active-sets #{set-id}) + status' (ctos/deactivate-set status set-id)] + (t/is (ctos/set-active? status set-id)) + (t/is (not (ctos/set-active? status' set-id))) + (t/is (= (ctos/active-set-count status') 0)))) + +(t/deftest toggle-set-active + (let [set-id (uuid/next) + status (ctos/make-token-status) + status' (ctos/toggle-set-active status set-id) + status'' (ctos/toggle-set-active status' set-id)] + (t/is (ctos/set-active? status' set-id)) + (t/is (not (ctos/set-active? status'' set-id))) + (t/is (= (ctos/active-set-count status') 1)) + (t/is (= (ctos/active-set-count status'') 0)))) + +(t/deftest datafy-token-status + (let [theme-id (uuid/next) + set-id (uuid/next) + status (ctos/make-token-status :active-themes #{theme-id} + :active-sets #{set-id}) + result (datafy status)] + (t/is (map? result)) + (t/is (not (ctos/token-status? result))) + (t/is (= (:active-themes result) #{theme-id})) + (t/is (= (:active-sets result) #{set-id})))) + +(t/deftest transit-serialization + (let [theme-id (uuid/next) + set-id (uuid/next) + status (ctos/make-token-status :active-themes #{theme-id} + :active-sets #{set-id}) + encoded (tr/encode-str status) + status' (tr/decode-str encoded)] + (t/is (ctos/token-status? status')) + (t/is (= (datafy status') (datafy status))))) + +#?(:clj + (t/deftest fressian-serialization + (let [theme-id (uuid/next) + set-id (uuid/next) + status (ctos/make-token-status :active-themes #{theme-id} + :active-sets #{set-id}) + encoded (fres/encode status) + status' (fres/decode encoded)] + (t/is (ctos/token-status? status')) + (t/is (= (datafy status') (datafy status)))))) + +#?(:clj + (t/deftest json-serialization + (let [theme-id (uuid/next) + set-id (uuid/next) + status (ctos/make-token-status :active-themes #{theme-id} + :active-sets #{set-id}) + json-str (json/write-str status) + parsed (json/read-str json-str :key-fn keyword)] + (t/is (map? parsed)) + (t/is (= [(str theme-id)] (:active-themes parsed))) + (t/is (= [(str set-id)] (:active-sets parsed)))))) + +;; Make TokenStatus from a TokensLib (to migrate from legacy files) +(t/deftest make-token-status-from-tokens-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-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"})) + (ctob/set-active-themes #{"/theme-1" "/theme-2"})) + token-status (ctos/make-token-status-from-lib tokens-lib)] + (t/is (ctos/token-status? token-status)) + (t/is (ctos/check-token-status token-status)) + (t/is (= (ctos/active-theme-count token-status) 2)) + (t/is (ctos/theme-active? token-status (thi/id :theme-1))) + (t/is (ctos/theme-active? token-status (thi/id :theme-2))) + (t/is (= (ctos/active-set-count token-status) 2)) + (t/is (ctos/set-active? token-status (thi/id :set-a))) + (t/is (ctos/set-active? token-status (thi/id :set-b))))) diff --git a/common/test/common_tests/types/tokens_migrations_test.cljc b/common/test/common_tests/types/tokens_migrations_test.cljc index 04bbd9d7a1..dbf728dbfe 100644 --- a/common/test/common_tests/types/tokens_migrations_test.cljc +++ b/common/test/common_tests/types/tokens_migrations_test.cljc @@ -8,7 +8,6 @@ (:require [app.common.data :as d] [app.common.test-helpers.ids-map :as thi] - [app.common.time :as ct] [app.common.types.tokens-lib :as ctob] [clojure.datafy :refer [datafy]] [clojure.test :as t]))