penpot/frontend/src/app/main/data/helpers.cljs
Andrés Moya 3cd9bfa9de
✨ Add sync with design tokens in external libraries (#10293)
* ✨ Auto link tokens when adding external libraries (provisional)

* 🔧 Refactor tokens-lib initialization

* 🔧 Add separated TokenStatus to store status apart of TokensLib

* 🔧 Make all status operations use the new data structure

* 🔧 Normalize status helper functions and access token sets by id

* 🔧 Rename :tokens-file to :tokens-source

* 🎉 Allow the user to choose the tokens-source of a file

* 🎉 Make tokens library readonly when it's in an external file

* 🎉 Show tokens in library summaries

* 🎉 Show source info in sidebar

* 🔧 Fix integration tests

* 🐛 Propagate changes of token values in external library

* 🎉 Layout updates

* 🔧 Refactor tokens source calculations

* 🔧 Add harder checks for nil or empty values in everything

* 🐛 Fix some integration tests

* 🔧 Add integration tests for tokens in external libs

* 🔧 Validate and repair missing tokens status

* 🎉 Make ui changes optional with config flag

* 🐛 Propagate tokens after synchronizing components in ext library

* 🐛 Propagate tokens after creating new instances

* 🐛 Propagate tokens after synchronizing tokens in ext library

* 🐛 Add a tokens source icon to libraries section (#11439)

* 🐛 Add a tokens source icon to libraries section

* 🐛 Fix ellipsis on library names

* ♻️ Remove code under flag on legacy component

* 🐛 Fix token theme name on inspect tab

* 🎉 Add changes notification (#11476)

* 🎉 Add changes notification

* ♻️ Change fn names

* 🐛 Fix tokens source label truncation and missing translations (#11533)

* 🐛 Fix tokens source label truncation and missing translations

The tokens source file name always showed, even for the current file,
and long names wrapped onto a second line instead of truncating
because the header used flex-wrap and overflow-wrap: break-word
instead of single-line ellipsis.

Show the source row unconditionally (it now displays "This file" when
the source is the current file, matching the connected-library case),
truncate the file name to one line with an ellipsis, and only attach a
tooltip with the full name when the text is actually truncated.

Replace the hardcoded UI strings with translated ones and add their
English and Spanish entries.

AI-assisted-by: claude-sonnet-5

* 🐛 Remove redundant effect dependency in tokens source

file-name-truncated? was listed as a dependency of the with-effect
that checks and observes label truncation, even though it isn't
read inside the effect body. Since the effect itself flips that
state via check-file-name-truncated, including it as a dependency
caused the ResizeObserver to be needlessly disconnected and
reconnected on every truncation change.

AI-assisted-by: claude-sonnet-5

* 🐛 Fix small visual error

* 🐛 Fix problem with plugins

* 🐛 Fix playwright tests

---------

Co-authored-by: Eva Marco <evamarcod@gmail.com>
Co-authored-by: Eva Marco <eva.marco@kaleidos.net>
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
2026-09-21 15:37:45 +02:00

252 lines
7.5 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS SUBSIDIARY SL
(ns app.main.data.helpers
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.files.tokens :as cfo]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.types.path :as path]))
(defn lookup-profile
([state]
(:profile state))
([state profile-id]
(dm/get-in state [:profiles profile-id])))
(defn lookup-libraries
"Retrieve all libraries, including the local file."
[state]
(:files state))
(defn lookup-file
([state]
(lookup-file state (:current-file-id state)))
([state file-id]
(dm/get-in state [:files file-id])))
(defn lookup-file-data
([state]
(lookup-file-data state (:current-file-id state)))
([state file-id]
(dm/get-in state [:files file-id :data])))
;; TODOstatus perhaps this is not necessary, lookup-tokens-lib should be enough
(defn lookup-tokens-source-data
[state]
(let [current-file-data (lookup-file-data state)
tokens-source-id (cfo/get-effective-tokens-source current-file-data)]
(lookup-file-data state tokens-source-id)))
(defn lookup-tokens-lib
[state]
(let [tokens-source-data (lookup-tokens-source-data state)]
(cfo/get-tokens-lib tokens-source-data)))
(defn lookup-tokens-status
[state]
(let [current-file-data (lookup-file-data state)] ;; Tokens status is always in the current file
(cfo/get-tokens-status current-file-data)))
(defn get-page
[fdata page-id]
(dm/get-in fdata [:pages-index page-id]))
(defn lookup-page
([state]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)]
(lookup-page state file-id page-id)))
([state page-id]
(let [file-id (:current-file-id state)]
(lookup-page state file-id page-id)))
([state file-id page-id]
(dm/get-in state [:files file-id :data :pages-index page-id])))
(defn lookup-page-objects
([state]
(lookup-page-objects state
(:current-file-id state)
(:current-page-id state)))
([state page-id]
(lookup-page-objects state
(:current-file-id state)
page-id))
([state file-id page-id]
(-> (lookup-page state file-id page-id)
(get :objects))))
(defn process-selected
([objects selected]
(process-selected objects selected nil))
([objects selected {:keys [omit-blocked?] :or {omit-blocked? false}}]
(let [selectable?
(fn [id]
(and (contains? objects id)
(or (not omit-blocked?)
(not (dm/get-in objects [id :blocked] false)))))
selected
(cfh/clean-loops objects selected)]
(into (d/ordered-set)
(filter selectable?)
selected))))
(defn split-text-shapes
"Split text shapes from non-text shapes"
[objects ids]
(loop [ids (seq ids)
text-ids []
shape-ids []]
(if-let [id (first ids)]
(let [shape (get objects id)]
(if (cfh/text-shape? shape)
(recur (rest ids)
(conj text-ids id)
shape-ids)
(recur (rest ids)
text-ids
(conj shape-ids id))))
[text-ids shape-ids])))
;; DEPRECATED
(defn lookup-selected-raw
[state]
(dm/get-in state [:workspace-local :selected]))
(defn get-selected-ids
[state]
(dm/get-in state [:workspace-local :selected]))
(defn lookup-selected
([state]
(lookup-selected state (:current-page-id state) nil))
([state options]
(lookup-selected state (:current-page-id state) options))
([state page-id options]
(let [objects (lookup-page-objects state page-id)
selected (dm/get-in state [:workspace-local :selected])]
(process-selected objects selected options))))
(defn lookup-shape
([state id]
(lookup-shape state (:current-page-id state) id))
([state page-id id]
(let [objects (lookup-page-objects state page-id)]
(get objects id))))
(defn lookup-shapes
([state ids]
(lookup-shapes state (:current-page-id state) ids))
([state page-id ids]
(let [objects (lookup-page-objects state page-id)]
(into [] (keep (d/getf objects)) ids))))
(defn update-file
([state f]
(update-file state (:current-file-id state) f))
([state file-id f]
(d/update-in-when state [:files file-id] f)))
(defn update-page
([state f]
(update-page state
(:current-file-id state)
(:current-page-id state)
f))
([state page-id f]
(update-page state
(:current-file-id state)
page-id
f))
([state file-id page-id f]
(d/update-in-when state [:files file-id :data :pages-index page-id] f)))
(defn filter-shapes
([state filter-fn]
(filter-shapes state (:current-page-id state) filter-fn))
([state page-id filter-fn]
(let [objects (lookup-page-objects state page-id)]
(into [] (filter filter-fn) (vals objects)))))
(defn select-bool-children
[state parent-id]
(let [objects (lookup-page-objects state)
shape-modifiers
(:workspace-modifiers state)
content-modifiers
(dm/get-in state [:workspace-local :edit-path])]
(reduce (fn [result id]
(if-let [shape (get objects id)]
(let [modifiers (dm/get-in shape-modifiers [id :modifiers])
shape (if (some? modifiers)
(gsh/transform-shape shape modifiers)
shape)
modifiers (dm/get-in content-modifiers [id :content-modifiers])
shape (if (some? modifiers)
(update shape :content path/apply-content-modifiers modifiers)
shape)]
(assoc result id shape))
result))
{}
(cfh/get-children-ids objects parent-id))))
(defn get-viewport-center
[state]
(when-let [{:keys [x y width height]} (get-in state [:workspace-local :vbox])]
(gpt/point (+ x (/ width 2)) (+ y (/ height 2)))))
(defn lookup-team-files
([state]
(lookup-team-files state (:current-team-id state)))
([state team-id]
(->> state
:files
(filter #(= team-id (:team-id (val %))))
(into {}))))
(defn lookup-team-projects
([state]
(lookup-team-projects (:current-team-id state)))
([state team-id]
(->> state
:projects
(filter #(= team-id (:team-id (val %))))
(into {}))))
(defn lookup-team
"The team identified by `team-id`, looked up first in the membership
`:teams` map and falling back to the directly-opened `:current-team`.
The fallback covers organization-owner access to teams the profile is not a
member of, which are kept out of `:teams` so they don't leak into the
teams listing."
([state]
(lookup-team state (:current-team-id state)))
([state team-id]
(or (dm/get-in state [:teams team-id])
(let [current (:current-team state)]
(when (= team-id (:id current))
current)))))
(defn get-selrect
[selrect-transform shape]
(if (some? selrect-transform)
(let [{:keys [center width height transform]} selrect-transform]
[(gsh/center->rect center width height)
(gmt/transform-in center transform)])
[(dm/get-prop shape :selrect)
(gsh/transform-matrix shape)]))