♻️ Share code that the component events repeated

- component-changed marked the component modified and sent
  launch-component-sync, its only caller, which opened an undo
  transaction around the syncs of the copies. component-changed sends
  those events itself, in the same order.
- The library update notification found the libraries that would
  change with a lazy filter, so the check ran up to the first one when
  the notification was built and the rest when the user clicked
  "Update". It is a filterv, so the whole check runs when the
  notification is built, as the comment above it says.
- The check that a new name is valid (trimmed, not blank, no trailing
  separator) was written in the shape rename and the component rename;
  cpn/clean-valid-name gives the cleaned name, or nil.
- Paste and the move of shapes into a parent each tested whether they
  put a component head inside a variant; ctn/head-into-variant? does.
- Going to the local main of a component and combining components into
  variants each opened another page and ran their work once it was
  initialized, with the same stream code; dwl/go-to-page-then does it.
- Making components from the selection mapped the check over the
  shapes and then tested every result; every? takes the check.
- generate-components-menu-entries reads the current file and page
  with mf/use-ctx, so it is a hook, and called conditionally it would
  break the order of its caller's hooks. It is
  use-components-menu-entries, and its docstring says why.

The analytics events that paste and duplicate send for each new shape
are built the same way but look up the parent's layout in different
objects, so they stay apart.

No behavior change: the suites are unchanged.

AI-assisted-by: claude-opus-5-5

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alonso.torres 2026-10-14 16:25:51 +02:00
parent 53d99af5a3
commit 2663e1e2a7
10 changed files with 70 additions and 86 deletions

View File

@ -55,6 +55,17 @@ Some naming conventions:
(->> (split-path path)
(join-path)))
(defn clean-valid-name
"The path string `name` with its empty items removed (`clean-path`), or nil
when it is not a valid name: blank, or ending in a separator."
[name]
(let [name (str/trim name)
clean (clean-path name)]
(when (and (not (str/ends-with? name "/"))
(string? clean)
(not (str/blank? clean)))
clean)))
(defn merge-path-item
"Put the item at the end of the path."
[path name]

View File

@ -388,6 +388,13 @@
[(fix-parent-id new-shape)
(map fix-parent-id new-shapes)])))
(defn head-into-variant?
"Whether placing `shapes` under `parent-id` puts a component head inside
a variant."
[objects parent-id shapes]
(and (some ctk/instance-head? shapes)
(some ctk/is-variant? (cfh/get-parents-with-self objects parent-id))))
(defn get-first-valid-parent
"Go through the parents until we find a shape that is not a copy of a component nor
a variant container."

View File

@ -761,15 +761,11 @@
(watch [_ state _]
(when-let [shape-id (d/nilv shape-id (dm/get-in state [:workspace-local :shape-for-rename]))]
(let [shape (dsh/lookup-shape state shape-id)
name (str/trim name)
clean-name (cpn/clean-path name)
valid? (and (not (str/ends-with? name "/"))
(string? clean-name)
(not (str/blank? clean-name)))
clean-name (cpn/clean-valid-name name)
component-id (:component-id shape)
undo-id (js/Symbol)]
(when valid?
(when (some? clean-name)
(if (ctc/is-variant-container? shape)
;; Rename the full variant when it is a variant container
(rx/of (dwva/rename-variant shape-id clean-name))

View File

@ -975,12 +975,7 @@
(pcb/update-shapes [parent-id]
#(ctl/add-children-to-cell % selected all-objects drop-cell)))
add-component-to-variant? (and
;; Any of the shapes is a head
(some ctk/instance-head? orig-shapes)
;; Any ancestor of the destination parent is a variant
(->> (cfh/get-parents-with-self page-objects parent-id)
(some ctk/is-variant?)))
add-component-to-variant? (ctn/head-into-variant? page-objects parent-id orig-shapes)
undo-id (js/Symbol)]
(rx/concat

View File

@ -389,7 +389,7 @@
(cfh/clean-loops objects))
selected-objects (map #(get objects %) selected)
;; We don't want to change the structure of component copies
can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects))]
can-make-component (every? #(ctn/valid-shape-for-component? objects %) selected-objects)]
(when can-make-component
(rx/of (-> (add-component2 id-ref selected)
@ -406,7 +406,7 @@
(cfh/clean-loops objects))
selected-objects (map #(get objects %) selected)
;; We don't want to change the structure of component copies
can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects))
can-make-component (every? #(ctn/valid-shape-for-component? objects %) selected-objects)
added-components (map (fn [id]
(with-meta (add-component2 [id])
{:multiple true}))
@ -445,15 +445,11 @@
(ptk/reify ::rename-component-and-main-instance
ptk/WatchEvent
(watch [_ state _]
(let [name (str/trim name)
clean-name (cpn/clean-path name)
valid? (and (not (str/ends-with? name "/"))
(string? clean-name)
(not (str/blank? clean-name)))
(let [clean-name (cpn/clean-valid-name name)
data (dsh/lookup-file-data state)
component (dm/get-in data [:components component-id])]
(when (and valid? component)
(when (and (some? clean-name) component)
(let [shape-id (:main-instance-id component)
page-id (:main-instance-page component)]
@ -690,6 +686,18 @@
(assoc :update-layout update-layout?))]
(rx/of (rt/nav :workspace params ::rt/new-window true))))))
(defn go-to-page-then
"Open the page `page-id` and, once it is initialized, emit the events of
`(f)`."
[stream page-id f]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::dwpg/initialize-page))
(rx/take 1)
(rx/observe-on :async)
(rx/mapcat (fn [_] (f))))
(rx/of (dcm/go-to-workspace :page-id page-id))))
(defn go-to-local-component
;; id is the id of the component to go
;; additional-ids are ids of additional components on the same page
@ -719,16 +727,6 @@
(when update-layout?
(rx/of (ptk/data-event :layout/update {:ids parent-ids})))))))
redirect-to-page
(fn [page-id ids]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::dwpg/initialize-page))
(rx/take 1)
(rx/observe-on :async)
(rx/mapcat (fn [_] (select-and-zoom ids))))
(rx/of (dcm/go-to-workspace :page-id page-id))))
get-main-instance-id
(fn [id page-id]
(let [component (dm/get-in data [:components id])]
@ -744,7 +742,7 @@
(when (some? page-id)
(if (= page-id current-page-id)
(select-and-zoom ids)
(redirect-to-page page-id ids)))))))))
(go-to-page-then stream page-id #(select-and-zoom ids))))))))))
(defn library-thumbnails-fetched
[thumbnails]
@ -958,20 +956,6 @@
(component-sync-events state component-id file-id undo-group)
[(dwu/commit-undo-transaction undo-id)])))))))
(defn launch-component-sync
"Launch a sync of the current file and of the library file of the given
component, in one undo entry."
[component-id file-id undo-group]
(ptk/reify ::launch-component-sync
ptk/WatchEvent
(watch [_ state _]
(let [undo-id (js/Symbol)]
(apply rx/of
(concat
[(dwu/start-undo-transaction undo-id)]
(component-sync-events state component-id file-id undo-group)
[(dwu/commit-undo-transaction undo-id)]))))))
(defn update-component-thumbnail
"Update the thumbnail of the component with the given id, in the
current file and in the imported libraries.
@ -1281,19 +1265,19 @@
;; This runs asynchronously on the next tick.
;; Filter libraries to only those that would produce actual sync changes.
(let [libraries (dsh/lookup-libraries state)]
(filter (fn [library]
(seq (:redo-changes
(cll/generate-sync-file-changes
(pcb/empty-changes)
nil
nil
file-id
nil
(:id library)
libraries
file-id
true))))
libraries-need-sync))))
(filterv (fn [library]
(seq (:redo-changes
(cll/generate-sync-file-changes
(pcb/empty-changes)
nil
nil
file-id
nil
(:id library)
libraries
file-id
true))))
libraries-need-sync))))
(rx/filter seq)
(rx/map (fn [libraries-with-changes]
(let [do-more-info
@ -1344,18 +1328,23 @@
:save-undo? false}))))))
(defn component-changed
"Notify that the component with the given id has changed, so it needs to be updated
in the current file and in the copies. And also update its thumbnails."
"Notify that the component with the given id has changed: mark it modified
and sync its copies, in the current file and in its library, in one undo
entry. The watcher also updates its thumbnails."
[component-id file-id undo-group]
(ptk/reify ::component-changed
cljs.core/IDeref
(-deref [_] [component-id file-id])
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(touch-component component-id)
(launch-component-sync component-id file-id undo-group)))))
(watch [_ state _]
(let [undo-id (js/Symbol)]
(apply rx/of
(concat
[(touch-component component-id)
(dwu/start-undo-transaction undo-id)]
(component-sync-events state component-id file-id undo-group)
[(dwu/commit-undo-transaction undo-id)]))))))
(defn watch-component-changes
"Watch the state for changes that affect to any main instance. If a change is detected will call

View File

@ -627,12 +627,7 @@
ids
:ignore-parents? ignore-parents?))
add-component-to-variant? (and
;; Any of the shapes is a head
(some (comp ctc/instance-head? objects) ids)
;; Any ancestor of the destination parent is a variant
(->> (cfh/get-parents-with-self objects parent-id)
(some ctc/is-variant?)))
add-component-to-variant? (ctn/head-into-variant? objects parent-id (map objects ids))
add-new-variant? (and
;; The parent is a variant container

View File

@ -28,7 +28,6 @@
[app.main.data.helpers :as dsh]
[app.main.data.workspace.colors :as cl]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.pages :as dwpg]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh]
@ -620,20 +619,10 @@
;; microtask of previous events execute before the
;; commit
(->> (rx/of (dwu/commit-undo-transaction undo-id))
(rx/observe-on :async)))))))
redirect-to-page
(fn [page-id]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::dwpg/initialize-page))
(rx/take 1)
(rx/observe-on :async)
(rx/mapcat (fn [_] (combine page-id))))
(rx/of (dcm/go-to-workspace :page-id page-id))))]
(rx/observe-on :async)))))))]
(if (and page-id (not= page-id current-page))
(redirect-to-page page-id)
(dwl/go-to-page-then stream page-id #(combine page-id))
(combine current-page))))))
(defn valid-components-for-variants?

View File

@ -676,7 +676,7 @@
single? (= (count shapes) 1)
objects (deref refs/workspace-page-objects)
can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) shapes))
components-menu-entries (cmm/generate-components-menu-entries shapes)
components-menu-entries (cmm/use-components-menu-entries shapes)
variant-container? (and single? (ctk/is-variant-container? (first shapes)))
all-main? (every? ctk/main-instance? shapes)
any-variant? (some ctk/is-variant? shapes)

View File

@ -392,7 +392,9 @@
:show-grids? true
:is-hidden is-hidden}])))
(defn generate-components-menu-entries
(defn use-components-menu-entries
"The entries of the component menu of `shapes`: a hook, as it reads the
current file and page from the context."
[shapes & {:keys [for-design-tab?]}]
(let [multi (> (count shapes) 1)
copies (filter ctk/in-component-copy? shapes)

View File

@ -1009,7 +1009,7 @@
(fn []
(swap! state* update :render inc)))
menu-entries (cmm/generate-components-menu-entries shapes {:for-design-tab? true})
menu-entries (cmm/use-components-menu-entries shapes {:for-design-tab? true})
path (->> component (:path) (cpn/split-path) (cpn/join-path-with-dot))]
(when (seq shapes)