From 8a42a535225aaeb79dda60cf54296afcfa3d54ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Thu, 4 May 2023 09:34:38 +0200 Subject: [PATCH] :sparkles: Notify library updates when really needed --- backend/src/app/rpc/commands/files.clj | 2 - backend/src/app/rpc/commands/files_update.clj | 3 +- backend/src/app/util/time.clj | 6 +- common/package.json | 2 +- common/src/app/common/pages/changes.cljc | 71 ++++++--- common/src/app/common/pages/helpers.cljc | 11 -- common/src/app/common/spec.cljc | 2 +- common/src/app/common/time.cljc | 27 ++++ common/src/app/common/types/color.cljc | 4 +- common/src/app/common/types/colors_list.cljc | 34 ++++- common/src/app/common/types/component.cljc | 10 +- .../src/app/common/types/components_list.cljc | 67 ++++++--- common/src/app/common/types/container.cljc | 24 ++- common/src/app/common/types/file.cljc | 141 +++++++++++------- common/src/app/common/types/shape_tree.cljc | 4 +- .../app/common/types/typographies_list.cljc | 36 ++++- common/src/app/common/types/typography.cljc | 5 +- common/yarn.lock | 8 +- frontend/package.json | 2 +- frontend/src/app/main/data/workspace.cljs | 13 +- .../src/app/main/data/workspace/groups.cljs | 4 +- .../app/main/data/workspace/libraries.cljs | 36 +++-- .../data/workspace/libraries_helpers.cljs | 8 +- .../app/main/data/workspace/modifiers.cljs | 5 +- .../src/app/main/data/workspace/shapes.cljs | 2 +- .../app/main/ui/components/shape_icon.cljs | 2 +- .../ui/components/shape_icon_refactor.cljs | 2 +- .../workspace/context_menu/context_menu.cljs | 2 +- .../app/main/ui/workspace/viewport/hooks.cljs | 2 +- frontend/src/app/util/time.cljs | 11 +- frontend/src/debug.cljs | 14 +- frontend/yarn.lock | 8 +- 32 files changed, 373 insertions(+), 195 deletions(-) create mode 100644 common/src/app/common/time.cljc diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index 6c41bde18..81be1a900 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -253,7 +253,6 @@ :code :feature-mismatch :feature "components/v2" :hint "file has 'components/v2' feature enabled but frontend didn't specifies it")) - (cond-> file (and (contains? client-features "components/v2") (not (contains? features "components/v2"))) @@ -263,7 +262,6 @@ (not (contains? client-features "storage/pointer-map"))) (process-pointers deref))) - ;; --- COMMAND QUERY: get-file (by id) (defn get-file diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj index 239999977..f25dc8997 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -78,8 +78,7 @@ (defn- library-change? [{:keys [type] :as change}] (or (contains? library-change-types type) - (and (contains? file-change-types type) - (some? (:component-id change))))) + (contains? file-change-types type))) (def ^:private sql:get-file "SELECT f.*, p.team_id diff --git a/backend/src/app/util/time.clj b/backend/src/app/util/time.clj index 69d401efa..b9d6f4eeb 100644 --- a/backend/src/app/util/time.clj +++ b/backend/src/app/util/time.clj @@ -6,7 +6,9 @@ (ns app.util.time (:require + [app.common.data.macros :as dm] [app.common.exceptions :as ex] + [app.common.time :as common-time] [clojure.spec.alpha :as s] [cuerdas.core :as str] [fipp.ednize :as fez]) @@ -186,9 +188,7 @@ :else (throw (UnsupportedOperationException. "unsupported type"))))) -(defn now - [] - (Instant/now)) +(dm/export common-time/now) (defn in-future [v] diff --git a/common/package.json b/common/package.json index 6f0984c7f..74298bf21 100644 --- a/common/package.json +++ b/common/package.json @@ -4,7 +4,7 @@ "main": "index.js", "license": "MPL-2.0", "dependencies": { - "luxon": "^3.1.1" + "luxon": "^3.3.0" }, "scripts": { "compile-and-watch-test": "clojure -M:dev:shadow-cljs watch test", diff --git a/common/src/app/common/pages/changes.cljc b/common/src/app/common/pages/changes.cljc index 2de4b4910..d95ec6110 100644 --- a/common/src/app/common/pages/changes.cljc +++ b/common/src/app/common/pages/changes.cljc @@ -16,6 +16,7 @@ [app.common.pages.helpers :as cph] [app.common.spec :as us] [app.common.pages.changes-spec :as pcs] + [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.colors-list :as ctcl] @@ -53,7 +54,7 @@ (run! validate-shape!))))) (defmulti process-change (fn [_ change] (:type change))) -(defmulti process-operation (fn [_ op] (:type op))) +(defmulti process-operation (fn [_ _ op] (:type op))) (defn process-changes ([data items] @@ -91,14 +92,34 @@ (defmethod process-change :mod-obj [data {:keys [id page-id component-id operations]}] - (let [update-fn (fn [objects] + (let [objects (if page-id + (-> data :pages-index (get page-id) :objects) + (-> data :components (get component-id) :objects)) + + modified-component-ids (atom #{}) + + on-touched (fn [shape] + ;; When a shape is modified, if it belongs to a main component instance, + ;; the component needs to be marked as modified. + (let [component-root (ctn/get-component-shape objects shape {:allow-main? true})] + (when (ctk/main-instance? component-root) + (swap! modified-component-ids conj (:component-id component-root))))) + + update-fn (fn [objects] (if-let [obj (get objects id)] - (let [result (reduce process-operation obj operations)] + (let [result (reduce (partial process-operation on-touched) obj operations)] (assoc objects id result)) - objects))] - (if page-id - (d/update-in-when data [:pages-index page-id :objects] update-fn) - (d/update-in-when data [:components component-id :objects] update-fn)))) + objects)) + + modify-components (fn [data] + (reduce ctkl/set-component-modified + data @modified-component-ids))] + + (as-> data $ + (if page-id + (d/update-in-when $ [:pages-index page-id :objects] update-fn) + (d/update-in-when $ [:components component-id :objects] update-fn)) + (modify-components $)))) (defmethod process-change :del-obj [data {:keys [page-id component-id id ignore-touched]}] @@ -223,8 +244,6 @@ (not= :frame (:type obj)) (as-> $$ (reduce (partial assign-frame-id frame-id) $$ (:shapes obj)))))) - - (move-objects [objects] (let [valid? (every? (partial is-valid-move? objects) shapes) parent (get objects parent-id) @@ -284,7 +303,7 @@ (defmethod process-change :mod-color [data {:keys [color]}] - (d/assoc-in-when data [:colors (:id color)] color)) + (ctcl/set-color data color)) (defmethod process-change :del-color [data {:keys [id]}] @@ -343,7 +362,7 @@ (defmethod process-change :mod-typography [data {:keys [typography]}] - (d/update-in-when data [:typographies (:id typography)] merge typography)) + (ctyl/update-typography data (:id typography) merge typography)) (defmethod process-change :del-typography [data {:keys [id]}] @@ -352,7 +371,7 @@ ;; === Operations (defmethod process-operation :set - [shape op] + [on-touched shape op] (let [attr (:attr op) group (get component-sync-attrs attr) val (:val op) @@ -367,7 +386,7 @@ ;; after the check added in data/workspace/modifiers/check-delta ;; function. Better check it and test toroughly when activating ;; components-v2 mode. - shape-ref (:shape-ref shape) + in-copy? (ctk/in-component-copy? shape) root-name? (and (= group :name-group) (:component-root? shape)) @@ -379,17 +398,23 @@ (gsh/close-attrs? attr val shape-val 1) (gsh/close-attrs? attr val shape-val))] + (when (and group (not ignore) (not equal?) + (not root-name?) + (not (and ignore-geometry is-geometry?))) + ;; Notify touched even if it's not copy, because it may be a main instance + (on-touched shape)) + (cond-> shape ;; Depending on the origin of the attribute change, we need or not to ;; set the "touched" flag for the group the attribute belongs to. ;; In some cases we need to ignore touched only if the attribute is ;; geometric (position, width or transformation). - (and shape-ref group (not ignore) (not equal?) + (and in-copy? group (not ignore) (not equal?) (not root-name?) (not (and ignore-geometry is-geometry?))) (-> - (update :touched cph/set-touched-group group) - (dissoc :remote-synced?)) + (update :touched cph/set-touched-group group) + (dissoc :remote-synced?)) (nil? val) (dissoc attr) @@ -398,23 +423,23 @@ (assoc attr val)))) (defmethod process-operation :set-touched - [shape op] + [_ shape op] (let [touched (:touched op) - shape-ref (:shape-ref shape)] - (if (or (nil? shape-ref) (nil? touched) (empty? touched)) + in-copy? (ctk/in-component-copy? shape)] + (if (or (not in-copy?) (nil? touched) (empty? touched)) (dissoc shape :touched) (assoc shape :touched touched)))) (defmethod process-operation :set-remote-synced - [shape op] + [_ shape op] (let [remote-synced? (:remote-synced? op) - shape-ref (:shape-ref shape)] - (if (or (nil? shape-ref) (not remote-synced?)) + in-copy? (ctk/in-component-copy? shape)] + (if (or (not in-copy?) (not remote-synced?)) (dissoc shape :remote-synced?) (assoc shape :remote-synced? true)))) (defmethod process-operation :default - [_ op] + [_ _ op] (ex/raise :type :not-implemented :code :operation-not-implemented :context {:type (:type op)})) diff --git a/common/src/app/common/pages/helpers.cljc b/common/src/app/common/pages/helpers.cljc index 2202b50a3..58cc81bfa 100644 --- a/common/src/app/common/pages/helpers.cljc +++ b/common/src/app/common/pages/helpers.cljc @@ -272,17 +272,6 @@ [shape group] ((or (:touched shape) #{}) group)) -(defn get-root-shape - "Get the root shape linked to a component for this shape, if any." - [objects shape] - - (cond - (some? (:component-root? shape)) - shape - - (some? (:shape-ref shape)) - (recur objects (get objects (:parent-id shape))))) - (defn make-container [page-or-component type] (assoc page-or-component :type type)) diff --git a/common/src/app/common/spec.cljc b/common/src/app/common/spec.cljc index 435512f93..acb3c698b 100644 --- a/common/src/app/common/spec.cljc +++ b/common/src/app/common/spec.cljc @@ -235,7 +235,7 @@ ;; --- SPECS WITHOUT CONFORMER -(s/def ::inst inst?) +(s/def ::inst inst?) ;; A clojure instant (date and time) (s/def ::string (s/with-gen string? diff --git a/common/src/app/common/time.cljc b/common/src/app/common/time.cljc new file mode 100644 index 000000000..c32c82411 --- /dev/null +++ b/common/src/app/common/time.cljc @@ -0,0 +1,27 @@ +;; 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 + +;; Here we put the time functions that are common between frontend and backend. +;; In the future we may create an unified API for both. + +(ns app.common.time + #?(:cljs + (:require + ["luxon" :as lxn]) + :clj + (:import + java.time.Instant))) + +#?(:cljs + (def DateTime lxn/DateTime)) + +#?(:cljs + (def Duration lxn/Duration)) + +(defn now + [] + #?(:clj (Instant/now) + :cljs (.local ^js DateTime))) \ No newline at end of file diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index 2032cd047..68c221c64 100644 --- a/common/src/app/common/types/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -58,6 +58,7 @@ (s/def ::color-generic/gradient (s/nilable ::gradient)) (s/def ::color-generic/ref-id uuid?) (s/def ::color-generic/ref-file uuid?) +(s/def ::color-generic/modified-at ::us/inst) (s/def ::shape-color (s/keys :req-un [:us/color @@ -73,7 +74,8 @@ ::color-generic/value ::color-generic/color ::color-generic/opacity - ::color-generic/gradient])) + ::color-generic/gradient + ::color-generic/modified-at])) (s/def ::recent-color (s/and diff --git a/common/src/app/common/types/colors_list.cljc b/common/src/app/common/types/colors_list.cljc index 0739ae4a2..d47900669 100644 --- a/common/src/app/common/types/colors_list.cljc +++ b/common/src/app/common/types/colors_list.cljc @@ -4,25 +4,51 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.types.colors-list) +(ns app.common.types.colors-list + (:require + [app.common.data :as d] + [app.common.time :as dt] + [app.common.types.color :as ctc])) (defn colors-seq [file-data] (vals (:colors file-data))) +(defn- touch + [color] + (assoc color :modified-at (dt/now))) + (defn add-color [file-data color] - (update file-data :colors assoc (:id color) color)) + (update file-data :colors assoc (:id color) (touch color))) (defn get-color [file-data color-id] (get-in file-data [:colors color-id])) +(defn get-ref-color + [library-data color] + (when (= (:ref-file color) (:id library-data)) + (get-color library-data (:ref-id color)))) + +(defn set-color + [file-data color] + (d/assoc-in-when file-data [:colors (:id color)] (touch color))) + (defn update-color - [file-data color-id f] - (update-in file-data [:colors color-id] f)) + [file-data color-id f & args] + (d/update-in-when file-data [:colors color-id] #(-> (apply f % args) + (touch)))) (defn delete-color [file-data color-id] (update file-data :colors dissoc color-id)) +(defn used-colors-changed-since + "Find all usages of any color in the library by the given shape, of colors + that have ben modified after the date." + [shape library since-date] + (->> (ctc/get-all-colors shape) + (keep #(get-ref-color (:data library) %)) + (remove #(< (:modified-at %) since-date)) ;; Note that :modified-at may be nil + (map #(vector (:id shape) (:id %) :color)))) diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index d963f441b..ad37b189c 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -7,6 +7,12 @@ (ns app.common.types.component) (defn instance-root? + "Check if this shape is the head of a top instance." + [shape] + (some? (:component-root? shape))) + +(defn instance-head? + "Check if this shape is the head of a top instance or a subinstance." [shape] (some? (:component-id shape))) @@ -46,12 +52,12 @@ (and (some? (:component-id shape)) (= (:component-file shape) library-id))) -(defn in-component-instance? +(defn in-component-copy? "Check if the shape is inside a component non-main instance." [shape] (some? (:shape-ref shape))) -(defn in-component-instance-not-root? +(defn in-component-copy-not-root? "Check if the shape is inside a component non-main instance and is not the root shape." [shape] diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc index 7c2a920ee..70af30230 100644 --- a/common/src/app/common/types/components_list.cljc +++ b/common/src/app/common/types/components_list.cljc @@ -8,7 +8,9 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.files.features :as feat])) + [app.common.files.features :as feat] + [app.common.time :as dt] + [app.common.types.component :as ctk])) (defn components [file-data] @@ -23,6 +25,10 @@ [file-data] (filter :deleted (vals (:components file-data)))) +(defn- touch + [component] + (assoc component :modified-at (dt/now))) + (defn add-component [file-data {:keys [id name path main-instance-id main-instance-page shapes]}] (let [components-v2 (dm/get-in file-data [:options :components-v2]) @@ -30,9 +36,9 @@ (cond-> file-data :always (assoc-in [:components id] - {:id id - :name name - :path path}) + (touch {:id id + :name name + :path path})) (not components-v2) (assoc-in [:components id :objects] @@ -47,24 +53,27 @@ (defn mod-component [file-data {:keys [id name path objects annotation]}] (let [wrap-objects-fn feat/*wrap-with-objects-map-fn*] - (update-in file-data [:components id] - (fn [component] - (let [objects (some-> objects wrap-objects-fn)] - (cond-> component - (some? name) - (assoc :name name) + (d/update-in-when file-data [:components id] + (fn [component] + (let [objects (some-> objects wrap-objects-fn)] + (cond-> component + (some? name) + (assoc :name name) - (some? path) - (assoc :path path) + (some? path) + (assoc :path path) - (some? objects) - (assoc :objects objects) + (some? objects) + (assoc :objects objects) - (some? annotation) - (assoc :annotation annotation) + (some? annotation) + (assoc :annotation annotation) - (nil? annotation) - (dissoc :annotation))))))) + (nil? annotation) + (dissoc :annotation) + + :always + (touch))))))) (defn get-component ([file-data component-id] @@ -83,8 +92,13 @@ component))) (defn update-component - [file-data component-id f] - (update-in file-data [:components component-id] f)) + [file-data component-id f & args] + (d/update-in-when file-data [:components component-id] #(-> (apply f % args) + (touch)))) + +(defn set-component-modified + [file-data component-id] + (update-component file-data component-id identity)) (defn delete-component [file-data component-id] @@ -92,8 +106,19 @@ (defn mark-component-deleted [file-data component-id] - (assoc-in file-data [:components component-id :deleted] true)) + (d/update-in-when file-data [:components component-id] assoc :deleted true)) (defn mark-component-undeleted [file-data component-id] (d/dissoc-in file-data [:components component-id :deleted])) + +(defn used-components-changed-since + "Check if the shape is an instance of any component in the library, and + the component has been modified after the date." + [shape library since-date] + (if (ctk/uses-library-components? shape (:id library)) + (let [component (get-component (:data library) (:component-id shape))] + (if (< (:modified-at component) since-date) ;; Note that :modified-at may be nil + [] + [[(:id shape) (:component-id shape) :component]])) + [])) diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index dc226ff8b..ceeff8b94 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -10,6 +10,7 @@ [app.common.geom.shapes :as gsh] [app.common.pages.common :as common] [app.common.spec :as us] + [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.pages-list :as ctpl] [app.common.types.shape-tree :as ctst] @@ -20,10 +21,11 @@ (s/def ::id uuid?) (s/def ::name ::us/string) (s/def ::path (s/nilable ::us/string)) +(s/def ::modified-at ::us/inst) (s/def ::container (s/keys :req-un [::id ::name] - :opt-un [::type ::path ::ctst/objects])) + :opt-un [::type ::path ::modified-at ::ctst/objects])) (defn make-container [page-or-component type] @@ -70,14 +72,20 @@ (defn get-component-shape "Get the parent shape linked to a component for this shape, if any" - [objects shape] - (if-not (:shape-ref shape) + ([objects shape] (get-component-shape objects shape nil)) + ([objects shape {:keys [allow-main?] :or {allow-main? false} :as options}] + (cond + (nil? shape) nil - (if (:component-id shape) - shape - (if-let [parent-id (:parent-id shape)] - (get-component-shape objects (get objects parent-id)) - nil)))) + + (and (not (ctk/in-component-copy? shape)) (not allow-main?)) + nil + + (ctk/instance-root? shape) + shape + + :else + (get-component-shape objects (get objects (:parent-id shape)) options)))) (defn make-component-shape "Clone the shape and all children. Generate new ids and detach diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 5886233b7..06e606849 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -281,6 +281,22 @@ (some used-in-container? (containers-seq file-data)))) +(defn used-assets-changed-since + "Get a lazy sequence of all assets in the library that are in use by the file and have + been modified after the given date." + [file-data library since-date] + (letfn [(used-assets-shape [shape] + (concat + (ctkl/used-components-changed-since shape library since-date) + (ctcl/used-colors-changed-since shape library since-date) + (ctyl/used-typographies-changed-since shape library since-date))) + + (used-assets-container [container] + (->> (mapcat used-assets-shape (ctn/shapes-seq container)) + (map #(cons (:id container) %))))] + + (mapcat used-assets-container (containers-seq file-data)))) + (defn get-or-add-library-page "If exists a page named 'Library backup', get the id and calculate the position to start adding new components. If not, create it and start at (0, 0)." @@ -370,7 +386,7 @@ root-to-board (fn [shape] (cond-> shape - (and (ctk/instance-root? shape) + (and (ctk/instance-head? shape) (not= (:type shape) :frame)) (assoc :type :frame :fills [] @@ -548,75 +564,81 @@ (defn dump-tree ([file-data page-id libraries] - (dump-tree file-data page-id libraries false false)) + (dump-tree file-data page-id libraries false false false)) ([file-data page-id libraries show-ids] - (dump-tree file-data page-id libraries show-ids false)) + (dump-tree file-data page-id libraries show-ids false false)) ([file-data page-id libraries show-ids show-touched] + (dump-tree file-data page-id libraries show-ids show-touched false)) + + ([file-data page-id libraries show-ids show-touched show-modified] (let [page (ctpl/get-page file-data page-id) objects (:objects page) components (ctkl/components file-data) root (d/seek #(nil? (:parent-id %)) (vals objects))] (letfn [(show-shape [shape-id level objects] - (let [shape (get objects shape-id)] - (println (str/pad (str (str/repeat " " level) - (when (:main-instance? shape) "{") - (:name shape) - (when (:main-instance? shape) "}") - (when (seq (:touched shape)) "*") - (when show-ids (str/format " <%s>" (:id shape)))) - {:length 20 - :type :right}) - (show-component-info shape objects)) - (when show-touched - (when (seq (:touched shape)) - (println (str (str/repeat " " level) - " " - (str (:touched shape))))) - (when (:remote-synced? shape) - (println (str (str/repeat " " level) - " (remote-synced)")))) - (when (:shapes shape) - (dorun (for [shape-id (:shapes shape)] - (show-shape shape-id (inc level) objects)))))) + (let [shape (get objects shape-id)] + (println (str/pad (str (str/repeat " " level) + (when (:main-instance? shape) "{") + (:name shape) + (when (:main-instance? shape) "}") + (when (seq (:touched shape)) "*") + (when show-ids (str/format " <%s>" (:id shape)))) + {:length 20 + :type :right}) + (show-component-info shape objects)) + (when show-touched + (when (seq (:touched shape)) + (println (str (str/repeat " " level) + " " + (str (:touched shape))))) + (when (:remote-synced? shape) + (println (str (str/repeat " " level) + " (remote-synced)")))) + (when (:shapes shape) + (dorun (for [shape-id (:shapes shape)] + (show-shape shape-id (inc level) objects)))))) (show-component-info [shape objects] - (if (nil? (:shape-ref shape)) - (if (:component-root? shape) " #" "") - (let [root-shape (ctn/get-component-shape objects shape) - component-id (when root-shape (:component-id root-shape)) - component-file-id (when root-shape (:component-file root-shape)) - component-file (when component-file-id (get libraries component-file-id nil)) - component (when component-id - (if component-file - (ctkl/get-component (:data component-file) component-id) - (get components component-id))) - component-shape (when component - (if component-file - (get-ref-shape (:data component-file) component shape) - (get-ref-shape file-data component shape)))] + (if (nil? (:shape-ref shape)) + (if (:component-root? shape) " #" "") + (let [root-shape (ctn/get-component-shape objects shape) + component-id (when root-shape (:component-id root-shape)) + component-file-id (when root-shape (:component-file root-shape)) + component-file (when component-file-id (get libraries component-file-id nil)) + component (when component-id + (if component-file + (ctkl/get-component (:data component-file) component-id) + (get components component-id))) + component-shape (when component + (if component-file + (get-ref-shape (:data component-file) component shape) + (get-ref-shape file-data component shape)))] - (str/format " %s--> %s%s%s" - (cond (:component-root? shape) "#" - (:component-id shape) "@" - :else "-") - (when component-file (str/format "<%s> " (:name component-file))) - (or (:name component-shape) "?") - (if (or (:component-root? shape) - (nil? (:component-id shape)) - true) - "" - (let [component-id (:component-id shape) - component-file-id (:component-file shape) - component-file (when component-file-id (get libraries component-file-id nil)) - component (if component-file - (ctkl/get-component (:data component-file) component-id) - (get components component-id))] - (str/format " (%s%s)" - (when component-file (str/format "<%s> " (:name component-file))) - (:name component)))))))) + (str/format " %s--> %s%s%s" + (cond (:component-root? shape) "#" + (:component-id shape) "@" + :else "-") + + (when component-file (str/format "<%s> " (:name component-file))) + + (or (:name component-shape) "?") + + (if (or (:component-root? shape) + (nil? (:component-id shape)) + true) + "" + (let [component-id (:component-id shape) + component-file-id (:component-file shape) + component-file (when component-file-id (get libraries component-file-id nil)) + component (if component-file + (ctkl/get-component (:data component-file) component-id) + (get components component-id))] + (str/format " (%s%s)" + (when component-file (str/format "<%s> " (:name component-file))) + (:name component)))))))) (show-component-instance [component] (let [page (get-component-page file-data component) @@ -633,7 +655,10 @@ (dorun (for [component (vals components)] (do (println) - (println (str/format "[%s]" (:name component))) + (println (str/format "[%s]%s%s" + (:name component) + (when show-ids (str " " (:id component))) + (when show-modified (str " " (:modified-at component))))) (when (:objects component) (show-shape (:id component) 0 (:objects component))) (when (:main-instance-page component) diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index bc219b0fb..335efb7f8 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -118,8 +118,8 @@ (filter cph/frame-shape?))] (->> (keys objects) (into [] xform)))) - (remove #(or (and skip-components? (ctk/instance-root? %)) - (and skip-copies? (and (ctk/instance-root? %) (not (ctk/main-instance? %))))))))) + (remove #(or (and skip-components? (ctk/instance-head? %)) + (and skip-copies? (and (ctk/instance-head? %) (not (ctk/main-instance? %))))))))) (defn get-frames-ids "Retrieves all frame ids as vector" diff --git a/common/src/app/common/types/typographies_list.cljc b/common/src/app/common/types/typographies_list.cljc index f388d0dd4..04c19075c 100644 --- a/common/src/app/common/types/typographies_list.cljc +++ b/common/src/app/common/types/typographies_list.cljc @@ -4,25 +4,53 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.common.types.typographies-list) +(ns app.common.types.typographies-list + (:require + [app.common.data :as d] + [app.common.text :as txt] + [app.common.time :as dt])) (defn typographies-seq [file-data] (vals (:typographies file-data))) +(defn- touch + [typography] + (assoc typography :modified-at (dt/now))) + (defn add-typography [file-data typography] - (update file-data :typographies assoc (:id typography) typography)) + (update file-data :typographies assoc (:id typography) (touch typography))) (defn get-typography [file-data typography-id] (get-in file-data [:typographies typography-id])) +(defn get-ref-typography + [library-data typography] + (when (= (:typography-ref-file typography) (:id library-data)) + (get-typography library-data (:typography-ref-id typography)))) + +(defn set-typography + [file-data typography] + (d/assoc-in-when file-data [:typographies (:id typography)] (touch typography))) + (defn update-typography - [file-data typography-id f] - (update-in file-data [:typographies typography-id] f)) + [file-data typography-id f & args] + (d/update-in-when file-data [:typographies typography-id] #(-> (apply f % args) + (touch)))) (defn delete-typography [file-data typography-id] (update file-data :typographies dissoc typography-id)) +(defn used-typographies-changed-since + "Find all usages of any typography in the library by the given shape, of + typographies that have ben modified after the date.." + [shape library since-date] + (->> shape + :content + txt/node-seq + (keep #(get-ref-typography (:data library) %)) + (remove #(< (:modified-at %) since-date)) ;; Note that :modified-at may be nil + (map #(vector (:id shape) (:id %) :typography)))) diff --git a/common/src/app/common/types/typography.cljc b/common/src/app/common/types/typography.cljc index 995552ffe..6a6f51595 100644 --- a/common/src/app/common/types/typography.cljc +++ b/common/src/app/common/types/typography.cljc @@ -6,6 +6,7 @@ (ns app.common.types.typography (:require + [app.common.spec :as us] [app.common.text :as txt] [clojure.spec.alpha :as s])) @@ -21,6 +22,7 @@ (s/def ::line-height string?) (s/def ::letter-spacing string?) (s/def ::text-transform string?) +(s/def ::modified-at ::us/inst) (s/def ::typography (s/keys :req-un [::id @@ -34,7 +36,8 @@ ::line-height ::letter-spacing ::text-transform] - :opt-un [::path])) + :opt-un [::path + ::modified-at])) (defn uses-library-typographies? "Check if the shape uses any typography in the given library." diff --git a/common/yarn.lock b/common/yarn.lock index 359d69826..05fe15aba 100644 --- a/common/yarn.lock +++ b/common/yarn.lock @@ -305,10 +305,10 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= -luxon@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.1.1.tgz#b492c645b2474fb86f3bd3283213846b99c32c1e" - integrity sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw== +luxon@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48" + integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg== md5.js@^1.3.4: version "1.3.5" diff --git a/frontend/package.json b/frontend/package.json index 46c917463..ea4c77dc7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -59,7 +59,7 @@ "highlight.js": "^11.7.0", "js-beautify": "^1.14.7", "jszip": "^3.10.1", - "luxon": "^3.1.1", + "luxon": "^3.3.0", "mousetrap": "^1.6.5", "opentype.js": "^1.3.4", "postcss-modules": "^6.0.0", diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 97d1d5b9d..e0722c189 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -260,12 +260,11 @@ ptk/WatchEvent (watch [_ state _] - (let [ignore-until (-> state :workspace-file :ignore-sync-until) - file-id (-> state :workspace-file :id) - needs-update? (some #(and (> (:modified-at %) (:synced-at %)) - (or (not ignore-until) - (> (:modified-at %) ignore-until))) - libraries)] + (let [file-data (:workspace-data state) + ignore-until (dm/get-in state [:workspace-file :ignore-sync-until]) + file-id (dm/get-in state [:workspace-file :id]) + needs-update? (seq (filter #(dwl/assets-need-sync % file-data ignore-until) + libraries))] (when needs-update? (rx/of (dwl/notify-sync-file file-id))))))) @@ -1633,7 +1632,7 @@ ;; Check if the shape is an instance whose master is defined in a ;; library that is not linked to the current file (foreign-instance? [shape paste-objects state] - (let [root (cph/get-root-shape paste-objects shape) + (let [root (ctn/get-component-shape paste-objects shape) root-file-id (:component-file root)] (and (some? root) (not= root-file-id (:current-file-id state)) diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index d04beadc9..a1162c388 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -91,7 +91,7 @@ ;; Shapes that are in a component, but are not root, must be detached, ;; because they will be now children of a non instance group. - shapes-to-detach (filter ctk/in-component-instance-not-root? shapes) + shapes-to-detach (filter ctk/in-component-copy-not-root? shapes) ;; Look at the `get-empty-groups-after-group-creation` ;; docstring to understand the real purpose of this code @@ -124,7 +124,7 @@ ;; Shapes that are in a component (including root) must be detached, ;; because cannot be easyly synchronized back to the main component. - shapes-to-detach (filter ctk/in-component-instance? + shapes-to-detach (filter ctk/in-component-copy? (cph/get-children-with-self objects (:id group)))] (-> (pcb/empty-changes it page-id) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index 24c3deb29..c39a0191c 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -605,7 +605,7 @@ container (cph/get-container local-file :page page-id) shape (ctn/get-shape container id)] - (when (ctk/in-component-instance? shape) + (when (ctk/instance-head? shape) (let [libraries (wsh/get-libraries state) changes @@ -801,6 +801,8 @@ (rx/of (dch/commit-changes (assoc changes :file-id file-id)))))))) (def ignore-sync + "Mark the file as ignore syncs. All library changes before this moment will not + ber notified to sync." (ptk/reify ::ignore-sync ptk/UpdateEvent (update [_ state] @@ -812,13 +814,26 @@ {:file-id (get-in state [:workspace-file :id]) :date (dt/now)})))) +(defn assets-need-sync + "Get a lazy sequence of all the assets of each type in the library that have + been modified after the last sync of the library. The sync date may be + overriden by providing a ignore-until parameter. + + The sequence items are tuples of (page-id shape-id asset-id asset-type)." + ([library file-data] (assets-need-sync library file-data nil)) + ([library file-data ignore-until] + (let [sync-date (max (:synced-at library) (or ignore-until 0))] + (when (> (:modified-at library) sync-date) + (ctf/used-assets-changed-since file-data library sync-date))))) + (defn notify-sync-file [file-id] (us/assert ::us/uuid file-id) (ptk/reify ::notify-sync-file ptk/WatchEvent (watch [_ state _] - (let [libraries-need-sync (filter #(> (:modified-at %) (:synced-at %)) + (let [file-data (:workspace-data state) + libraries-need-sync (filter #(seq (assets-need-sync % file-data)) (vals (get state :workspace-libraries))) do-update #(do (apply st/emit! (map (fn [library] (sync-file (:current-file-id state) @@ -828,14 +843,15 @@ do-dismiss #(do (st/emit! ignore-sync) (st/emit! dm/hide))] - (rx/of (dm/info-dialog - (tr "workspace.updates.there-are-updates") - :inline-actions - [{:label (tr "workspace.updates.update") - :callback do-update} - {:label (tr "workspace.updates.dismiss") - :callback do-dismiss}] - :sync-dialog)))))) + (when (seq libraries-need-sync) + (rx/of (dm/info-dialog + (tr "workspace.updates.there-are-updates") + :inline-actions + [{:label (tr "workspace.updates.update") + :callback do-update} + {:label (tr "workspace.updates.dismiss") + :callback do-dismiss}] + :sync-dialog))))))) (defn watch-component-changes "Watch the state for changes that affect to any main instance. If a change is detected will throw diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/frontend/src/app/main/data/workspace/libraries_helpers.cljs index 068968a67..27796ca4b 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/frontend/src/app/main/data/workspace/libraries_helpers.cljs @@ -99,7 +99,7 @@ (if (and (= (count shapes) 1) (or (and (= (:type (first shapes)) :group) (not components-v2)) (= (:type (first shapes)) :frame)) - (not (ctk/instance-root? (first shapes)))) + (not (ctk/instance-head? (first shapes)))) [(first shapes) (-> (pcb/empty-changes it page-id) (pcb/with-objects objects))] (let [root-name (if (= 1 (count shapes)) @@ -111,7 +111,7 @@ page-id shapes root-name - (not (ctk/instance-root? (first shapes)))) + (not (ctk/instance-head? (first shapes)))) (prepare-create-board changes (uuid/next) (:parent-id (first shapes)) @@ -203,7 +203,7 @@ (defn- generate-detach-recursive [changes container shape-id first] (let [shape (ctn/get-shape container shape-id)] - (if (and (ctk/instance-root? shape) (not first)) + (if (and (ctk/instance-head? shape) (not first)) ;; Subinstances are not detached, but converted in top instances (pcb/update-shapes changes [(:id shape)] #(assoc % :component-root? true)) ;; Otherwise, detach the shape and all children @@ -530,7 +530,7 @@ [changes libraries container shape-id reset? components-v2] (log/debug :msg "Sync shape direct" :shape (str shape-id) :reset? reset?) (let [shape-inst (ctn/get-shape container shape-id)] - (if (ctk/in-component-instance? shape-inst) + (if (ctk/in-component-copy? shape-inst) (let [library (dm/get-in libraries [(:component-file shape-inst) :data]) component (or (ctkl/get-component library (:component-id shape-inst)) (and reset? diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 8c3a6dfca..d7ae841eb 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -15,6 +15,7 @@ [app.common.pages.common :as cpc] [app.common.pages.helpers :as cph] [app.common.spec :as us] + [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] [app.common.types.shape.layout :as ctl] [app.main.constants :refer [zoom-half-pixel-precision]] @@ -54,7 +55,7 @@ shape (nil? root) - (cph/get-root-shape objects shape) + (ctn/get-component-shape objects shape {:allow-main? true}) :else root) @@ -64,7 +65,7 @@ transformed-shape (nil? transformed-root) - (as-> (cph/get-root-shape objects transformed-shape) $ + (as-> (ctn/get-component-shape objects transformed-shape {:allow-main? true}) $ (gsh/transform-shape (merge $ (get modif-tree (:id $))))) :else transformed-root) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 829977679..33318303f 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -189,7 +189,7 @@ ;; but hidden (to be able to recover them more easily). (let [shape (get objects shape-id) component-shape (ctn/get-component-shape objects shape)] - (and (ctk/in-component-instance? shape) + (and (ctk/in-component-copy? shape) (not= shape component-shape) (not (ctk/main-instance? component-shape))))) diff --git a/frontend/src/app/main/ui/components/shape_icon.cljs b/frontend/src/app/main/ui/components/shape_icon.cljs index fa6d7606b..811cd8ed7 100644 --- a/frontend/src/app/main/ui/components/shape_icon.cljs +++ b/frontend/src/app/main/ui/components/shape_icon.cljs @@ -14,7 +14,7 @@ (mf/defc element-icon [{:keys [shape main-instance?] :as props}] - (if (ctk/instance-root? shape) + (if (ctk/instance-head? shape) (if main-instance? i/component i/component-copy) diff --git a/frontend/src/app/main/ui/components/shape_icon_refactor.cljs b/frontend/src/app/main/ui/components/shape_icon_refactor.cljs index bbb4b4686..706884959 100644 --- a/frontend/src/app/main/ui/components/shape_icon_refactor.cljs +++ b/frontend/src/app/main/ui/components/shape_icon_refactor.cljs @@ -14,7 +14,7 @@ (mf/defc element-icon-refactor [{:keys [shape main-instance?] :as props}] - (if (ctk/instance-root? shape) + (if (ctk/instance-head? shape) (if main-instance? i/component-refactor i/copy-refactor) diff --git a/frontend/src/app/main/ui/workspace/context_menu/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu/context_menu.cljs index f0b03d0b3..90d44447e 100644 --- a/frontend/src/app/main/ui/workspace/context_menu/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu/context_menu.cljs @@ -438,7 +438,7 @@ has-component? (some true? (map #(contains? % :component-id) shapes)) is-component? (and single? (-> shapes first :component-id some?)) - is-non-root? (and single? (ctk/in-component-instance-not-root? (first shapes))) + is-non-root? (and single? (ctk/in-component-copy-not-root? (first shapes))) first-shape (first shapes) {:keys [shape-id component-id component-file main-instance?]} first-shape diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 228597d85..5dac6c294 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -224,7 +224,7 @@ #(as-> (get objects %) obj (and (cph/root-frame? obj) (d/not-empty? (:shapes obj)) - (not (ctk/instance-root? obj)) + (not (ctk/instance-head? obj)) (not (ctk/main-instance? obj)))) ;; Set with the elements to remove from the hover list diff --git a/frontend/src/app/util/time.cljs b/frontend/src/app/util/time.cljs index 3b2b38c7f..cdb92386d 100644 --- a/frontend/src/app/util/time.cljs +++ b/frontend/src/app/util/time.cljs @@ -22,12 +22,13 @@ ["date-fns/locale/ru" :default dateFnsLocalesRu] ["date-fns/locale/tr" :default dateFnsLocalesTr] ["date-fns/locale/zh-CN" :default dateFnsLocalesZhCn] - ["luxon" :as lxn] + [app.common.data.macros :as dm] + [app.common.time :as common-time] [app.util.object :as obj] [cuerdas.core :as str])) -(def DateTime lxn/DateTime) -(def Duration lxn/Duration) +(dm/export common-time/DateTime) +(dm/export common-time/Duration) (defprotocol ITimeMath (plus [_ o]) @@ -89,9 +90,7 @@ :rfc2822 (.fromRFC2822 ^js DateTime s #js {:zone zone :setZone force-zone}) :http (.fromHTTP ^js DateTime s #js {:zone zone :setZone force-zone}))))) -(defn now - [] - (.local ^js DateTime)) +(dm/export common-time/now) (defn utc-now [] diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index c306f5820..ee8f76d87 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -295,18 +295,20 @@ nil)) (defn dump-tree' - ([state] (dump-tree' state false false)) - ([state show-ids] (dump-tree' state show-ids false)) - ([state show-ids show-touched] + ([state] (dump-tree' state false false false)) + ([state show-ids] (dump-tree' state show-ids false false)) + ([state show-ids show-touched] (dump-tree' state show-ids show-touched false)) + ([state show-ids show-touched show-modified] (let [page-id (get state :current-page-id) file-data (get state :workspace-data) libraries (get state :workspace-libraries)] - (ctf/dump-tree file-data page-id libraries show-ids show-touched)))) + (ctf/dump-tree file-data page-id libraries show-ids show-touched show-modified)))) (defn ^:export dump-tree ([] (dump-tree' @st/state)) - ([show-ids] (dump-tree' @st/state show-ids)) - ([show-ids show-touched] (dump-tree' @st/state show-ids show-touched))) + ([show-ids] (dump-tree' @st/state show-ids false false)) + ([show-ids show-touched] (dump-tree' @st/state show-ids show-touched false)) + ([show-ids show-touched show-modified] (dump-tree' @st/state show-ids show-touched show-modified))) (when *assert* (defonce debug-subscription diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 122df5776..7f6d83d9a 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3342,10 +3342,10 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" -luxon@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.1.1.tgz#b492c645b2474fb86f3bd3283213846b99c32c1e" - integrity sha512-Ah6DloGmvseB/pX1cAmjbFvyU/pKuwQMQqz7d0yvuDlVYLTs2WeDHQMpC8tGjm1da+BriHROW/OEIT/KfYg6xw== +luxon@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.3.0.tgz#d73ab5b5d2b49a461c47cedbc7e73309b4805b48" + integrity sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg== make-iterator@^1.0.0: version "1.0.1"