mirror of
https://github.com/penpot/penpot.git
synced 2026-05-19 23:13:39 +00:00
Merge remote-tracking branch 'origin/staging' into develop
This commit is contained in:
commit
6f41a2b729
@ -86,13 +86,32 @@ batches of 50 via GraphQL to stay within API limits.
|
||||
|
||||
### 5. Categorize entries
|
||||
|
||||
Check the labels on each issue to determine which section it belongs to:
|
||||
Use the **Issue Type** field (GitHub's native issue type, accessible via GraphQL
|
||||
`issueType { name }`) to determine which section an entry belongs to.
|
||||
**Do not** use labels or title emoji prefixes as the source of truth — they are
|
||||
often inaccurate or missing.
|
||||
|
||||
| Label / Title prefix | Changelog section |
|
||||
|----------------------|-------------------|
|
||||
| `bug` label or `:bug:` title prefix | `### :bug: Bugs fixed` |
|
||||
| `enhancement` label or `:sparkles:` prefix | `### :sparkles: New features & Enhancements` |
|
||||
| No label | Infer from title convention, default to bug fix |
|
||||
| Issue Type (`issueType.name`) | Changelog section |
|
||||
|------------------------------|-------------------|
|
||||
| `Bug` | `### :bug: Bugs fixed` |
|
||||
| `Feature` or `Enhancement` | `### :sparkles: New features & Enhancements` |
|
||||
| No type set | Fetch the issue and check its labels as a fallback: `bug` label → bugs section, otherwise default to enhancements |
|
||||
|
||||
To fetch Issue Types for all issues in a milestone efficiently, use a single
|
||||
GraphQL query with aliases rather than N+1 REST calls:
|
||||
|
||||
```graphql
|
||||
query {
|
||||
repository(owner: "penpot", name: "penpot") {
|
||||
i123: issue(number: 123) {
|
||||
number state milestone { number } issueType { name }
|
||||
}
|
||||
i456: issue(number: 456) {
|
||||
number state milestone { number } issueType { name }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Community contribution attribution:** If the issue or its fix PR has the
|
||||
`community contribution` label, add an attribution `(by @<github_username>)`
|
||||
@ -205,6 +224,7 @@ Read the top of `CHANGES.md` and confirm:
|
||||
can find the code changes.
|
||||
- **Latest version first.** New sections are inserted at the top of the
|
||||
changelog, below the `# CHANGELOG` header.
|
||||
- **Issue Type determines section.** Use GitHub's `issueType` field (Bug → `:bug:`, Feature/Enhancement → `:sparkles:`) to categorize entries. Ignore labels and title emoji prefixes — they are unreliable for categorization.
|
||||
- **User-facing descriptions.** Write from the user's perspective — describe
|
||||
what broke and what was fixed, not internal implementation details.
|
||||
- **Community attribution.** When the issue or fix PR has the
|
||||
|
||||
27
CHANGES.md
27
CHANGES.md
@ -32,6 +32,8 @@
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
- WebGL rendering (beta) user preference [#9683](https://github.com/penpot/penpot/issues/9683) (PR:[9113](https://github.com/penpot/penpot/pull/9113))
|
||||
- Design Tokens at the design tab: numeric fields with token selection in place [#9358](https://github.com/penpot/penpot/issues/9358)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
@ -49,11 +51,8 @@
|
||||
- Rename token group [#9637](https://github.com/penpot/penpot/issues/9637) (PR: [#8275](https://github.com/penpot/penpot/pull/8275))
|
||||
- Duplicate token group [#9638](https://github.com/penpot/penpot/issues/9638) (PR: [#8886](https://github.com/penpot/penpot/pull/8886))
|
||||
- Copy token name from contextual menu [#9639](https://github.com/penpot/penpot/issues/9639) (PR: [#8566](https://github.com/penpot/penpot/pull/8566))
|
||||
- Add natural sorting on token names [#8635](https://github.com/penpot/penpot/issues/8635) (PR: [#8672](https://github.com/penpot/penpot/pull/8672))
|
||||
- Add drag-to-change for numeric inputs in workspace sidebar (by @RenzoMXD) [#2466](https://github.com/penpot/penpot/issues/2466) (PR: [#8536](https://github.com/penpot/penpot/pull/8536))
|
||||
- Add CSS linter [#9636](https://github.com/penpot/penpot/issues/9636) (PR: [#8592](https://github.com/penpot/penpot/pull/8592))
|
||||
- Save and restore selection state in undo/redo (by @eureka0928) [#6007](https://github.com/penpot/penpot/issues/6007) (PR: [#8652](https://github.com/penpot/penpot/pull/8652))
|
||||
- Fix warnings for unsupported token $type (by @Dexterity104) [#8790](https://github.com/penpot/penpot/issues/8790) (PR: [#8873](https://github.com/penpot/penpot/pull/8873))
|
||||
- Add per-group add button for typographies (by @eureka0928) [#5275](https://github.com/penpot/penpot/issues/5275) (PR: [#8895](https://github.com/penpot/penpot/pull/8895))
|
||||
- Add Find & Replace for text content and layer names (by @statxc) [#7108](https://github.com/penpot/penpot/issues/7108) (PR: [#8899](https://github.com/penpot/penpot/pull/8899))
|
||||
- Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [#8773](https://github.com/penpot/penpot/issues/8773) (PR: [#8874](https://github.com/penpot/penpot/pull/8874))
|
||||
@ -62,8 +61,6 @@
|
||||
- Sort asset library subfolders alphabetically at every nesting level (by @eureka0928) [#2572](https://github.com/penpot/penpot/issues/2572) (PR: [#8952](https://github.com/penpot/penpot/pull/8952))
|
||||
- Add Paste to replace (Cmd+Shift+V) for selected shapes (by @eureka0928) [#4240](https://github.com/penpot/penpot/issues/4240) (PR: [#9033](https://github.com/penpot/penpot/pull/9033))
|
||||
- Differentiate incoming and outgoing interaction link colors (by @claytonlin1110) [#7794](https://github.com/penpot/penpot/issues/7794) (PR: [#8923](https://github.com/penpot/penpot/pull/8923))
|
||||
- Add guide locking and fix locked element selection in viewer (by @Dexterity104) [#8358](https://github.com/penpot/penpot/issues/8358) (PR: [#8949](https://github.com/penpot/penpot/pull/8949))
|
||||
- Apply styles to selection (by @AzazelN28) [#9661](https://github.com/penpot/penpot/issues/9661) (PR: [#8625](https://github.com/penpot/penpot/pull/8625))
|
||||
- Reorder prototyping overlay options to show Position before Relative to (by @rockchris099) [#2910](https://github.com/penpot/penpot/issues/2910)
|
||||
- Add customizable colors for ruler guides (by @Dexterity104) [#5199](https://github.com/penpot/penpot/issues/5199) (PR: [#8986](https://github.com/penpot/penpot/pull/8986))
|
||||
- Persist asset search and section filter across sidebar tabs (by @eureka0928) [#2913](https://github.com/penpot/penpot/issues/2913) (PR: [#8985](https://github.com/penpot/penpot/pull/8985))
|
||||
@ -74,7 +71,6 @@
|
||||
- Allow customising the OIDC login button label (by @wdeveloper16) [#7027](https://github.com/penpot/penpot/issues/7027) (PR: [#9026](https://github.com/penpot/penpot/pull/9026))
|
||||
- Add page separators in Workspace [#9180](https://github.com/penpot/penpot/issues/9180) (PR: [#8561](https://github.com/penpot/penpot/pull/8561))
|
||||
- Preserve vector content when pasting SVG from external tools (by @RenzoMXD) [#546](https://github.com/penpot/penpot/issues/546) (PR: [#9182](https://github.com/penpot/penpot/pull/9182))
|
||||
- Add Shift+Numpad aliases for zoom shortcuts (by @RenzoMXD) [#2457](https://github.com/penpot/penpot/issues/2457) (PR: [#9063](https://github.com/penpot/penpot/pull/9063))
|
||||
- Add pixel grid color picker in viewport settings (by @jack-stormentswe) [#7750](https://github.com/penpot/penpot/issues/7750) (PR: [#9155](https://github.com/penpot/penpot/pull/9155))
|
||||
- Add HEX/HSB/HSL support to color picker with persistent model switcher (by @edwin-rivera-dev) [#9133](https://github.com/penpot/penpot/issues/9133) (PR: [#9134](https://github.com/penpot/penpot/pull/9134))
|
||||
- Show specific invitation-link error messages (by @niwinz) [#9220](https://github.com/penpot/penpot/issues/9220) (PR: [#9223](https://github.com/penpot/penpot/pull/9223))
|
||||
@ -83,15 +79,21 @@
|
||||
- Add clipboard read/write permissions to the plugin system (by @wdeveloper16) [#6980](https://github.com/penpot/penpot/issues/6980) (PR: [#9053](https://github.com/penpot/penpot/pull/9053))
|
||||
- Update auth hero illustration on login screen [#9532](https://github.com/penpot/penpot/issues/9532) (PR: [#9552](https://github.com/penpot/penpot/pull/9552))
|
||||
- Update Open Graph link preview metadata [#9555](https://github.com/penpot/penpot/issues/9555) (PR: [#9557](https://github.com/penpot/penpot/pull/9557))
|
||||
- Fix library update button freezing [#9330](https://github.com/penpot/penpot/issues/9330) (PR: [#9513](https://github.com/penpot/penpot/pull/9513))
|
||||
- Add new numeric inputs for token management on the right sidebar [#9358](https://github.com/penpot/penpot/issues/9358)
|
||||
- Restore deleted team files in bulk instead of per file (by @Dexterity104) [#9246](https://github.com/penpot/penpot/issues/9246) (PR: [#9248](https://github.com/penpot/penpot/pull/9248))
|
||||
- Preserve Inkscape labels when pasting SVGs (by @jeffrey701) [#7869](https://github.com/penpot/penpot/issues/7869) (PR: [#9252](https://github.com/penpot/penpot/pull/9252))
|
||||
- Add Alt+click to expand layer subtree (by @MilosM348) [#7736](https://github.com/penpot/penpot/issues/7736) (PR: [#9179](https://github.com/penpot/penpot/pull/9179))
|
||||
- Fix typo in subscription settings success key (by @jack-stormentswe) [#9203](https://github.com/penpot/penpot/issues/9203) (PR: [#9204](https://github.com/penpot/penpot/pull/9204))
|
||||
### :bug: Bugs fixed
|
||||
|
||||
### :bug: Bugs fixed
|
||||
- Add Shift+Numpad aliases for zoom shortcuts (by @RenzoMXD) [#2457](https://github.com/penpot/penpot/issues/2457) (PR: [#9063](https://github.com/penpot/penpot/pull/9063))
|
||||
- Save and restore selection state in undo/redo (by @eureka0928) [#6007](https://github.com/penpot/penpot/issues/6007) (PR: [#8652](https://github.com/penpot/penpot/pull/8652))
|
||||
- Add guide locking and fix locked element selection in viewer (by @Dexterity104) [#8358](https://github.com/penpot/penpot/issues/8358) (PR: [#8949](https://github.com/penpot/penpot/pull/8949))
|
||||
- Add natural sorting on token names [#8635](https://github.com/penpot/penpot/issues/8635) (PR: [#8672](https://github.com/penpot/penpot/pull/8672))
|
||||
- Fix warnings for unsupported token $type (by @Dexterity104) [#8790](https://github.com/penpot/penpot/issues/8790) (PR: [#8873](https://github.com/penpot/penpot/pull/8873))
|
||||
- Allow deleting the profile avatar after uploading (by @moorsecopers99) [#9067](https://github.com/penpot/penpot/issues/9067) (PR: [#9068](https://github.com/penpot/penpot/pull/9068))
|
||||
- Apply styles to selection (by @AzazelN28) [#9661](https://github.com/penpot/penpot/issues/9661) (PR: [#8625](https://github.com/penpot/penpot/pull/8625))
|
||||
- Fix Alt/Option to draw shapes from center point (by @offreal) [#8360](https://github.com/penpot/penpot/issues/8360) (PR: [#8361](https://github.com/penpot/penpot/pull/8361))
|
||||
- Fix library update button freezing [#9330](https://github.com/penpot/penpot/issues/9330) (PR: [#9513](https://github.com/penpot/penpot/pull/9513))
|
||||
- Fix typo in subscription settings success key (by @jack-stormentswe) [#9203](https://github.com/penpot/penpot/issues/9203) (PR: [#9204](https://github.com/penpot/penpot/pull/9204))
|
||||
- Add token name on broken token pill on sidebar [#9534](https://github.com/penpot/penpot/issues/9534) (PR: [#8527](https://github.com/penpot/penpot/pull/8527))
|
||||
- Fix tooltip activated when tab change [#9539](https://github.com/penpot/penpot/issues/9539) (PR: [#8719](https://github.com/penpot/penpot/pull/8719))
|
||||
- Fix title on shared button [#9541](https://github.com/penpot/penpot/issues/9541) (PR: [#8696](https://github.com/penpot/penpot/pull/8696))
|
||||
@ -127,7 +129,6 @@
|
||||
- Fix app crash on multiselection with hidden shapes and opacity mixed value [#9666](https://github.com/penpot/penpot/issues/9666) (PR: [#8932](https://github.com/penpot/penpot/pull/8932))
|
||||
- Fix gap input throwing an error [#9667](https://github.com/penpot/penpot/issues/9667) (PR: [#8984](https://github.com/penpot/penpot/pull/8984))
|
||||
- Fix copy to be more specific [#9668](https://github.com/penpot/penpot/issues/9668) (PR: [#9028](https://github.com/penpot/penpot/pull/9028))
|
||||
- Allow deleting the profile avatar after uploading (by @moorsecopers99) [#9067](https://github.com/penpot/penpot/issues/9067) (PR: [#9068](https://github.com/penpot/penpot/pull/9068))
|
||||
- Fix incorrect rendering when exporting text as SVG, PNG and JPG (by @edwin-rivera-dev) [#8516](https://github.com/penpot/penpot/issues/8516) (PR: [#9094](https://github.com/penpot/penpot/pull/9094))
|
||||
- Fix typography style creation with tokenized line-height (by @juan-flores077) [#8479](https://github.com/penpot/penpot/issues/8479) (PR: [#9121](https://github.com/penpot/penpot/pull/9121))
|
||||
- Fix colorpicker layout hiding eyedropper button [#9669](https://github.com/penpot/penpot/issues/9669) (PR: [#9125](https://github.com/penpot/penpot/pull/9125))
|
||||
@ -149,6 +150,10 @@
|
||||
- Fix several color picker issues [#9556](https://github.com/penpot/penpot/issues/9556) (PR: [#9558](https://github.com/penpot/penpot/pull/9558))
|
||||
- Fix asset icon broken on Asset tab [#9587](https://github.com/penpot/penpot/issues/9587) (PR: [#9612](https://github.com/penpot/penpot/pull/9612))
|
||||
- Fix text fill color stops updating in multiselect with texts [#9608](https://github.com/penpot/penpot/issues/9608) (PR: [#9549](https://github.com/penpot/penpot/pull/9549))
|
||||
- Fix editing a legacy text element silently detaches its color token [#9255](https://github.com/penpot/penpot/issues/9255)
|
||||
- Fix token application to grid paddings [#9494](https://github.com/penpot/penpot/issues/9494)
|
||||
- Fix file crashing when switching a variant [#9259](https://github.com/penpot/penpot/issues/9259)
|
||||
|
||||
|
||||
## 2.15.4 (Unreleased)
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
[app.common.time :as ct]
|
||||
[app.common.uri :as u]
|
||||
[app.db :as db]
|
||||
[app.http.access-token :as actoken]
|
||||
[app.http.session :as session]
|
||||
[app.storage :as sto]
|
||||
[integrant.core :as ig]
|
||||
@ -79,9 +80,17 @@
|
||||
(let [bucket (-> obj meta :bucket)]
|
||||
(not (contains? public-buckets bucket))))
|
||||
|
||||
(defn- authenticated?
|
||||
"Check if the request has an authenticated profile, either via session
|
||||
or access token."
|
||||
[request]
|
||||
(or (some? (::session/profile-id request))
|
||||
(some? (::actoken/profile-id request))))
|
||||
|
||||
(defn objects-handler
|
||||
"Handler that serves storage objects by id.
|
||||
For non-public buckets (e.g. profile), requires an authenticated session."
|
||||
For non-public buckets (e.g. profile), requires authentication
|
||||
via session cookie or access token."
|
||||
[{:keys [::sto/storage] :as cfg} request]
|
||||
(let [id (get-id request)
|
||||
obj (sto/get-object storage id)]
|
||||
@ -90,7 +99,7 @@
|
||||
{::yres/status 404}
|
||||
|
||||
(and (requires-auth? obj)
|
||||
(nil? (::session/profile-id request)))
|
||||
(not (authenticated? request)))
|
||||
{::yres/status 401}
|
||||
|
||||
:else
|
||||
@ -128,7 +137,8 @@
|
||||
|
||||
(defmethod ig/init-key ::routes
|
||||
[_ cfg]
|
||||
["/assets" {:middleware [[session/authz cfg]]}
|
||||
["/assets" {:middleware [[session/authz cfg]
|
||||
[actoken/authz cfg]]}
|
||||
["/by-id/:id" {:handler (partial objects-handler cfg)}]
|
||||
["/by-file-media-id/:id" {:handler (partial file-objects-handler cfg)}]
|
||||
["/by-file-media-id/:id/thumbnail" {:handler (partial file-thumbnails-handler cfg)}]])
|
||||
|
||||
@ -302,7 +302,9 @@
|
||||
::http.assets/cache-max-age (ct/duration {:hours 24})
|
||||
::http.assets/signature-max-age (ct/duration {:hours 24 :minutes 15})
|
||||
::sto/storage (ig/ref ::sto/storage)
|
||||
::session/manager (ig/ref ::session/manager)}
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::setup/props (ig/ref ::setup/props)
|
||||
::db/pool (ig/ref ::db/pool)}
|
||||
|
||||
::rpc/climit
|
||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
|
||||
@ -96,6 +96,7 @@
|
||||
context (assoc @context :param-style pstyle)]
|
||||
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/html; charset=utf-8"}
|
||||
::yres/body (-> (io/resource template)
|
||||
(tmpl/render context))})))
|
||||
(fn [_]
|
||||
|
||||
461
backend/test/backend_tests/http_assets_test.clj
Normal file
461
backend/test/backend_tests/http_assets_test.clj
Normal file
@ -0,0 +1,461 @@
|
||||
;; 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
|
||||
|
||||
(ns backend-tests.http-assets-test
|
||||
(:require
|
||||
[app.common.time :as ct]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.http :as-alias http]
|
||||
[app.http.access-token :as actoken]
|
||||
[app.http.assets :as assets]
|
||||
[app.http.session :as session]
|
||||
[app.rpc.commands.access-token :as access-token]
|
||||
[app.storage :as sto]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
[datoteka.fs :as fs]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
(t/use-fixtures :each (th/serial
|
||||
th/database-reset
|
||||
th/clean-storage))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Helpers
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(defn- configure-storage-backend
|
||||
"Given storage map, returns a storage configured with the
|
||||
appropriate backend for assets."
|
||||
[storage]
|
||||
(assoc storage ::sto/backend :fs))
|
||||
|
||||
(defn- create-storage-object!
|
||||
"Create a storage object with the given bucket and content."
|
||||
[storage bucket content]
|
||||
(sto/put-object! storage {::sto/content (sto/content content)
|
||||
:bucket bucket
|
||||
:content-type "text/plain"}))
|
||||
|
||||
(defn- make-handler-cfg
|
||||
"Build a minimal cfg map for the assets handlers."
|
||||
[storage]
|
||||
{::sto/storage storage
|
||||
::assets/path "/assets"})
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: get-id
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest get-id-with-valid-uuid
|
||||
(let [id (uuid/next)
|
||||
request {:path-params {:id (str id)}}
|
||||
result (assets/get-id request)]
|
||||
(t/is (= id result))))
|
||||
|
||||
(t/deftest get-id-with-invalid-uuid
|
||||
(let [request {:path-params {:id "not-a-uuid"}}]
|
||||
(try
|
||||
(assets/get-id request)
|
||||
(t/is false "should have thrown")
|
||||
(catch Exception e
|
||||
(t/is (= :not-found (:type (ex-data e))))))))
|
||||
|
||||
(t/deftest get-id-with-missing-id
|
||||
(let [request {:path-params {}}]
|
||||
(try
|
||||
(assets/get-id request)
|
||||
(t/is false "should have thrown")
|
||||
(catch Exception e
|
||||
(t/is (= :not-found (:type (ex-data e))))))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: objects-handler — non-existent objects
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest objects-handler-non-existent-object
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
request {:path-params {:id (str (uuid/next))}}
|
||||
response (assets/objects-handler cfg request)]
|
||||
(t/is (= 404 (::yres/status response)))))
|
||||
|
||||
(t/deftest objects-handler-invalid-uuid
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
request {:path-params {:id "not-a-uuid"}}]
|
||||
(try
|
||||
(assets/objects-handler cfg request)
|
||||
(t/is false "should have thrown")
|
||||
(catch Exception e
|
||||
(t/is (= :not-found (:type (ex-data e))))))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: objects-handler — public buckets (no auth required)
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest objects-handler-public-bucket-no-auth
|
||||
;; Objects in public buckets should be accessible without authentication.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)]
|
||||
|
||||
(doseq [bucket ["file-media-object"
|
||||
"file-object-thumbnail"
|
||||
"team-font-variant"
|
||||
"file-data-fragment"]]
|
||||
(t/testing (str "bucket: " bucket)
|
||||
(let [object (create-storage-object! storage bucket "public data")
|
||||
request {:path-params {:id (str (:id object))}}
|
||||
response (assets/objects-handler cfg request)]
|
||||
(t/is (not= 401 (::yres/status response))
|
||||
(str "bucket " bucket " should not require auth"))
|
||||
(t/is (not= 404 (::yres/status response))
|
||||
(str "bucket " bucket " object should exist")))))))
|
||||
|
||||
(t/deftest objects-handler-public-bucket-with-auth
|
||||
;; Objects in public buckets should also be accessible WITH authentication.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
profile (th/create-profile* 1)
|
||||
object (create-storage-object! storage "file-media-object" "public data")
|
||||
request {:path-params {:id (str (:id object))}
|
||||
::session/profile-id (:id profile)}
|
||||
response (assets/objects-handler cfg request)]
|
||||
(t/is (not= 401 (::yres/status response)))
|
||||
(t/is (not= 404 (::yres/status response)))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: objects-handler — non-public buckets (auth required)
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest objects-handler-non-public-bucket-no-auth
|
||||
;; Objects in non-public buckets should return 401 without authentication.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
object (create-storage-object! storage "profile" "profile photo")
|
||||
request {:path-params {:id (str (:id object))}}
|
||||
response (assets/objects-handler cfg request)]
|
||||
(t/is (= 401 (::yres/status response)))))
|
||||
|
||||
(t/deftest objects-handler-non-public-bucket-with-session-auth
|
||||
;; Objects in non-public buckets should be accessible with session auth.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
profile (th/create-profile* 1)
|
||||
object (create-storage-object! storage "profile" "profile photo")
|
||||
request {:path-params {:id (str (:id object))}
|
||||
::session/profile-id (:id profile)}
|
||||
response (assets/objects-handler cfg request)]
|
||||
(t/is (not= 401 (::yres/status response)))
|
||||
(t/is (not= 404 (::yres/status response)))))
|
||||
|
||||
(t/deftest objects-handler-non-public-bucket-with-access-token-auth
|
||||
;; Objects in non-public buckets should be accessible with access token auth.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
profile (th/create-profile* 1)
|
||||
object (create-storage-object! storage "profile" "profile photo")
|
||||
request {:path-params {:id (str (:id object))}
|
||||
::actoken/profile-id (:id profile)}
|
||||
response (assets/objects-handler cfg request)]
|
||||
(t/is (not= 401 (::yres/status response)))
|
||||
(t/is (not= 404 (::yres/status response)))))
|
||||
|
||||
(t/deftest objects-handler-non-public-bucket-with-both-auth
|
||||
;; Objects should be accessible when both session and access token auth are present.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
profile (th/create-profile* 1)
|
||||
object (create-storage-object! storage "profile" "profile photo")
|
||||
request {:path-params {:id (str (:id object))}
|
||||
::session/profile-id (:id profile)
|
||||
::actoken/profile-id (:id profile)}
|
||||
response (assets/objects-handler cfg request)]
|
||||
(t/is (not= 401 (::yres/status response)))
|
||||
(t/is (not= 404 (::yres/status response)))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: objects-handler — all non-public buckets
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest objects-handler-all-non-public-buckets-require-auth
|
||||
;; Verify that all buckets NOT in the public set require authentication.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
profile (th/create-profile* 1)]
|
||||
|
||||
(doseq [bucket ["profile"
|
||||
"tempfile"
|
||||
"file-data"
|
||||
"file-thumbnail"
|
||||
"file-change"]]
|
||||
(t/testing (str "bucket: " bucket)
|
||||
(let [object (create-storage-object! storage bucket "some data")
|
||||
request {:path-params {:id (str (:id object))}}]
|
||||
|
||||
;; Without auth → 401
|
||||
(let [response (assets/objects-handler cfg request)]
|
||||
(t/is (= 401 (::yres/status response))
|
||||
(str "bucket " bucket " should require auth")))
|
||||
|
||||
;; With session auth → not 401
|
||||
(let [response (assets/objects-handler cfg (assoc request ::session/profile-id (:id profile)))]
|
||||
(t/is (not= 401 (::yres/status response))
|
||||
(str "bucket " bucket " should be accessible with session auth")))
|
||||
|
||||
;; With access token auth → not 401
|
||||
(let [response (assets/objects-handler cfg (assoc request ::actoken/profile-id (:id profile)))]
|
||||
(t/is (not= 401 (::yres/status response))
|
||||
(str "bucket " bucket " should be accessible with access token auth"))))))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: objects-handler — serve-object response (FS backend)
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest objects-handler-fs-backend-serves-object
|
||||
;; Verify that the FS backend returns the correct response structure.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
profile (th/create-profile* 1)
|
||||
object (create-storage-object! storage "file-media-object" "file content")
|
||||
request {:path-params {:id (str (:id object))}
|
||||
::session/profile-id (:id profile)}
|
||||
response (assets/objects-handler cfg request)]
|
||||
;; FS backend returns 204 with x-accel-redirect header
|
||||
(t/is (= 204 (::yres/status response)))
|
||||
(t/is (some? (get (::yres/headers response) "x-accel-redirect")))
|
||||
(t/is (= "text/plain" (get (::yres/headers response) "content-type")))
|
||||
(t/is (some? (get (::yres/headers response) "cache-control")))))
|
||||
|
||||
(t/deftest objects-handler-fs-backend-accel-redirect-path
|
||||
;; Verify that x-accel-redirect contains the object's relative path.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
object (create-storage-object! storage "file-media-object" "file content")
|
||||
request {:path-params {:id (str (:id object))}}
|
||||
response (assets/objects-handler cfg request)
|
||||
redirect (get (::yres/headers response) "x-accel-redirect")]
|
||||
;; The redirect path should contain the object's relative path
|
||||
(t/is (string? redirect))
|
||||
(t/is (clojure.string/includes? redirect (sto/object->relative-path object)))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: objects-handler — cache headers
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest objects-handler-cache-control-header
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
object (create-storage-object! storage "file-media-object" "content")
|
||||
request {:path-params {:id (str (:id object))}}
|
||||
response (assets/objects-handler cfg request)
|
||||
cc (get (::yres/headers response) "cache-control")]
|
||||
(t/is (string? cc))
|
||||
(t/is (clojure.string/starts-with? cc "max-age="))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: middleware integration — session auth end-to-end
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest session-auth-integration
|
||||
;; Test the full session auth flow: create session → assign token →
|
||||
;; authenticate request → access protected asset.
|
||||
(let [cfg th/*system*
|
||||
manager (session/inmemory-manager)
|
||||
profile (th/create-profile* 1)
|
||||
|
||||
;; Create a session and generate a token
|
||||
session (->> (session/create-session manager {:profile-id (:id profile)
|
||||
:user-agent "test-agent"})
|
||||
(#'session/assign-token cfg))
|
||||
|
||||
;; Create a storage object in a non-public bucket
|
||||
storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
object (create-storage-object! storage "profile" "profile data")
|
||||
|
||||
;; Simulate what the middleware chain does:
|
||||
;; 1. mw/auth extracts token from cookie and sets ::http/auth-data
|
||||
;; 2. session/authz reads ::http/auth-data and sets ::session/profile-id
|
||||
request {::http/auth-data {:type :cookie
|
||||
:token (:token session)
|
||||
:claims {:sid (:id session)
|
||||
:uid (:id profile)}
|
||||
:metadata {:ver 1}}
|
||||
:path-params {:id (str (:id object))}}
|
||||
|
||||
;; Apply session/authz middleware
|
||||
handler (#'session/wrap-authz
|
||||
(fn [req]
|
||||
;; This is where the actual handler would be called
|
||||
;; We verify that ::session/profile-id is set
|
||||
req)
|
||||
{::session/manager manager})
|
||||
result (handler request)]
|
||||
|
||||
;; Verify the session auth set the profile-id
|
||||
(t/is (= (:id profile) (::session/profile-id result)))
|
||||
(t/is (some? (::session/session result)))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: middleware integration — access token auth end-to-end
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest access-token-auth-integration
|
||||
;; Test the full access token flow: create token → authenticate
|
||||
;; request → access protected asset.
|
||||
(let [profile (th/create-profile* 1)
|
||||
|
||||
;; Create an access token in the database
|
||||
atoken (db/tx-run! th/*system*
|
||||
access-token/create-access-token
|
||||
(:id profile) "test-token" nil nil)
|
||||
|
||||
;; Create a storage object in a non-public bucket
|
||||
storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
object (create-storage-object! storage "profile" "profile data")
|
||||
|
||||
;; Simulate what the middleware chain does:
|
||||
;; 1. mw/auth extracts token from Authorization header and sets ::http/auth-data
|
||||
;; 2. actoken/authz reads ::http/auth-data and sets ::actoken/profile-id
|
||||
request {::http/auth-data {:type :token
|
||||
:token (:token atoken)
|
||||
:claims {:tid (:id atoken)}}
|
||||
:path-params {:id (str (:id object))}}
|
||||
|
||||
;; Apply actoken/authz middleware
|
||||
handler (#'actoken/wrap-authz
|
||||
(fn [req]
|
||||
;; Verify that ::actoken/profile-id is set
|
||||
req)
|
||||
th/*system*)
|
||||
result (handler request)]
|
||||
|
||||
;; Verify the access token auth set the profile-id
|
||||
(t/is (= (:id profile) (::actoken/profile-id result)))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: middleware chain — combined session + access token
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest combined-middleware-chain
|
||||
;; Test that both session/authz and actoken/authz work together
|
||||
;; in the middleware chain, matching the assets route configuration.
|
||||
(let [cfg th/*system*
|
||||
manager (session/inmemory-manager)
|
||||
profile (th/create-profile* 1)
|
||||
|
||||
;; Create a session
|
||||
session (->> (session/create-session manager {:profile-id (:id profile)
|
||||
:user-agent "test-agent"})
|
||||
(#'session/assign-token cfg))
|
||||
|
||||
;; Create an access token
|
||||
atoken (db/tx-run! th/*system*
|
||||
access-token/create-access-token
|
||||
(:id profile) "test-token" nil nil)
|
||||
|
||||
;; Build the middleware chain like assets routes do:
|
||||
;; session/authz → actoken/authz → handler
|
||||
inner-handler (fn [request] request)
|
||||
with-actoken (#'actoken/wrap-authz inner-handler th/*system*)
|
||||
with-session (#'session/wrap-authz with-actoken {::session/manager manager})]
|
||||
|
||||
(t/testing "session cookie auth sets ::session/profile-id"
|
||||
(let [request {::http/auth-data {:type :cookie
|
||||
:token (:token session)
|
||||
:claims {:sid (:id session)
|
||||
:uid (:id profile)}
|
||||
:metadata {:ver 1}}}
|
||||
result (with-session request)]
|
||||
(t/is (= (:id profile) (::session/profile-id result)))))
|
||||
|
||||
(t/testing "access token auth sets ::actoken/profile-id"
|
||||
(let [request {::http/auth-data {:type :token
|
||||
:token (:token atoken)
|
||||
:claims {:tid (:id atoken)}}}
|
||||
result (with-session request)]
|
||||
(t/is (= (:id profile) (::actoken/profile-id result)))))
|
||||
|
||||
(t/testing "no auth sets neither profile-id"
|
||||
(let [request {}
|
||||
result (with-session request)]
|
||||
(t/is (nil? (::session/profile-id result)))
|
||||
(t/is (nil? (::actoken/profile-id result)))))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: objects-handler — edge cases
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest objects-handler-nil-profile-id-in-session
|
||||
;; When session auth is present but profile-id is nil (e.g. invalid session),
|
||||
;; non-public objects should still be denied.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
object (create-storage-object! storage "profile" "data")
|
||||
request {:path-params {:id (str (:id object))}
|
||||
::session/profile-id nil}
|
||||
response (assets/objects-handler cfg request)]
|
||||
(t/is (= 401 (::yres/status response)))))
|
||||
|
||||
(t/deftest objects-handler-nil-profile-id-in-access-token
|
||||
;; When access token auth is present but profile-id is nil,
|
||||
;; non-public objects should still be denied.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
object (create-storage-object! storage "profile" "data")
|
||||
request {:path-params {:id (str (:id object))}
|
||||
::actoken/profile-id nil}
|
||||
response (assets/objects-handler cfg request)]
|
||||
(t/is (= 401 (::yres/status response)))))
|
||||
|
||||
(t/deftest objects-handler-empty-request
|
||||
;; A request with no path-params should raise a not-found error.
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
request {}]
|
||||
(try
|
||||
(assets/objects-handler cfg request)
|
||||
(t/is false "should have thrown")
|
||||
(catch Exception e
|
||||
(t/is (= :not-found (:type (ex-data e))))))))
|
||||
|
||||
;; ----------------------------------------------------------------
|
||||
;; Tests: objects-handler — expired objects
|
||||
;; ----------------------------------------------------------------
|
||||
|
||||
(t/deftest objects-handler-expired-object
|
||||
;; Expired objects should return 404 (get-object filters them out).
|
||||
(let [storage (-> (:app.storage/storage th/*system*)
|
||||
(configure-storage-backend))
|
||||
cfg (make-handler-cfg storage)
|
||||
profile (th/create-profile* 1)
|
||||
object (sto/put-object! storage {::sto/content (sto/content "expired")
|
||||
::sto/expired-at (ct/now)
|
||||
:bucket "profile"
|
||||
:content-type "text/plain"})
|
||||
request {:path-params {:id (str (:id object))}
|
||||
::session/profile-id (:id profile)}
|
||||
response (assets/objects-handler cfg request)]
|
||||
(t/is (= 404 (::yres/status response)))))
|
||||
@ -12,10 +12,12 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[app.common.schema.test :as smt]
|
||||
[app.config :as cf]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.doc :as rpc.doc]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]))
|
||||
[clojure.test :as t]
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
(t/use-fixtures :once th/state-init)
|
||||
|
||||
@ -31,6 +33,17 @@
|
||||
false)))
|
||||
{:num 15}))
|
||||
|
||||
|
||||
|
||||
(t/deftest doc-handler-returns-html-content-type
|
||||
(with-redefs [cf/flags #{:backend-api-doc}]
|
||||
(let [methods (::rpc/methods th/*system*)
|
||||
handler (#'rpc.doc/handler :methods methods
|
||||
:label "main"
|
||||
:entrypoint "http://localhost/api/main/methods"
|
||||
:openapi "http://localhost/api/main/doc/openapi"
|
||||
:template "app/templates/main-api-doc.tmpl")
|
||||
request {}
|
||||
response (handler request)]
|
||||
(t/is (= 200 (::yres/status response)))
|
||||
(t/is (= "text/html; charset=utf-8"
|
||||
(get-in response [::yres/headers "content-type"]))))))
|
||||
|
||||
|
||||
@ -177,7 +177,9 @@
|
||||
|
||||
(defn not-empty?
|
||||
[coll]
|
||||
(boolean (seq coll)))
|
||||
(if (coll? coll)
|
||||
(boolean (seq coll))
|
||||
(not (nil? coll))))
|
||||
|
||||
(defn editable-collection?
|
||||
[m]
|
||||
|
||||
@ -46,8 +46,8 @@
|
||||
(with-meta changes
|
||||
{::page-id page-id})))
|
||||
([]
|
||||
{:redo-changes []
|
||||
:undo-changes '()})
|
||||
{:redo-changes [] ;; redo-changes is a vector so that conj adds things at the end, in order of execution
|
||||
:undo-changes '()}) ;; undo-changes is a list to conj things at the beginning, so they execute in the reverse order when undoing several changes
|
||||
([origin]
|
||||
{:redo-changes []
|
||||
:undo-changes '()
|
||||
|
||||
@ -2129,8 +2129,8 @@
|
||||
(contains? #{:auto-height :auto-width} (:grow-type current-shape)))]
|
||||
|
||||
(loop [attrs updatable-attrs
|
||||
roperations [{:type :set-touched :touched (:touched previous-shape)}]
|
||||
uoperations (list {:type :set-touched :touched (:touched current-shape)})]
|
||||
roperations []
|
||||
uoperations '()]
|
||||
(if-let [attr (first attrs)]
|
||||
(let [sync-group
|
||||
(ctk/resolve-sync-group (:type previous-shape) attr)
|
||||
@ -2272,7 +2272,13 @@
|
||||
|
||||
(let [updated-attrs (into #{} (comp (filter #(= :set (:type %)))
|
||||
(map :attr))
|
||||
roperations)]
|
||||
roperations)
|
||||
updated-sync-groups (into #{}
|
||||
(keep #(ctk/resolve-sync-group (:type previous-shape) %))
|
||||
updated-attrs)
|
||||
new-touched (set/union (or (:touched current-shape) #{}) updated-sync-groups)
|
||||
roperations (into [{:type :set-touched :touched new-touched}] roperations)
|
||||
uoperations (into (list {:type :set-touched :touched (:touched current-shape)}) uoperations)]
|
||||
(cond-> changes
|
||||
(> (count roperations) 1)
|
||||
(-> (add-update-attr-changes current-shape container roperations uoperations)
|
||||
|
||||
@ -242,8 +242,11 @@
|
||||
acc)
|
||||
|
||||
:else
|
||||
;; If the key is not :text, and they are different, it is an attribute differece
|
||||
(if (not= v1 v2)
|
||||
;; If the key is not :text, and they are different, it is an attribute difference.
|
||||
;; Take into account that some processes remove empty attributes, so in some
|
||||
;; cases we will compare [] with nil, and this is not a difference.
|
||||
(if (and (not= v1 v2)
|
||||
(or (d/not-empty? v1) (d/not-empty? v2)))
|
||||
(attribute-cb acc k)
|
||||
acc))))
|
||||
#{}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
[app.common.test-helpers.ids-map :as thi]
|
||||
[app.common.test-helpers.shapes :as ths]
|
||||
[app.common.test-helpers.variants :as thv]
|
||||
[app.common.types.component :as ctk]
|
||||
[clojure.test :as t]))
|
||||
|
||||
(t/use-fixtures :each thi/test-fixture)
|
||||
@ -38,13 +39,13 @@
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
|
||||
|
||||
copy01' (ths/get-shape file' :copy02)]
|
||||
copy01' (ths/get-shape file' :copy01)]
|
||||
(thf/dump-file file :keys [:width])
|
||||
;; The copy had width 5 before the switch
|
||||
(t/is (= (:width copy01) 5))
|
||||
;; The rect has width 15 after the switch
|
||||
;; The copy has width 15 after the switch
|
||||
(t/is (= (:width copy01') 15))))
|
||||
|
||||
(t/deftest test-simple-switch
|
||||
@ -64,15 +65,15 @@
|
||||
rect01 (get-in page [:objects (-> copy01 :shapes first)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
copy01' (ths/get-shape file' :copy01)
|
||||
rect01' (get-in page' [:objects (-> copy01' :shapes first)])]
|
||||
;; The rect had width 5 before the switch
|
||||
(t/is (= (:width rect01) 5))
|
||||
;; The rect has width 15 after the switch
|
||||
(t/is (= (:width rect02') 15))))
|
||||
(t/is (= (:width rect01') 15))))
|
||||
|
||||
;; ============================================================
|
||||
;; SIMPLE ATTRIBUTE OVERRIDES (identical variants)
|
||||
@ -103,9 +104,9 @@
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
|
||||
|
||||
copy01' (ths/get-shape file' :copy02)]
|
||||
copy01' (ths/get-shape file' :copy01)]
|
||||
(thf/dump-file file :keys [:width])
|
||||
;; The copy had width 25 before the switch
|
||||
(t/is (= (:width copy01) 25))
|
||||
@ -140,16 +141,16 @@
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
copy01' (ths/get-shape file' :copy01)
|
||||
rect01' (get-in page' [:objects (-> copy01' :shapes first)])]
|
||||
|
||||
;; The rect had width 25 before the switch
|
||||
(t/is (= (:width rect01) 25))
|
||||
;; The override is keept: The rect still has width 25 after the switch
|
||||
(t/is (= (:width rect02') 25))))
|
||||
(t/is (= (:width rect01') 25))))
|
||||
|
||||
;; ============================================================
|
||||
;; SIMPLE ATTRIBUTE OVERRIDES (different variants)
|
||||
@ -183,17 +184,193 @@
|
||||
rect01 (get-in page [:objects (:id rect01)])
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
rect02' (get-in page' [:objects (-> copy02' :shapes first)])]
|
||||
copy01' (ths/get-shape file' :copy01)
|
||||
rect01' (get-in page' [:objects (-> copy01' :shapes first)])]
|
||||
|
||||
;; The rect had width 25 before the switch
|
||||
(t/is (= (:width rect01) 25))
|
||||
;; The override isn't keept, because the property is different in the mains
|
||||
;; The rect has width 15 after the switch
|
||||
(t/is (= (:width rect02') 15))))
|
||||
(t/is (= (:width rect01') 15))))
|
||||
|
||||
;; ============================================================
|
||||
;; NESTED COPY SWITCH (no overrides)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-nested-switch-in-main
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant
|
||||
:v01 :c01 :m01 :c02 :m02
|
||||
{:variant1-params {:width 5}
|
||||
:variant2-params {:width 15}})
|
||||
|
||||
(tho/add-frame :m03)
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:parent-label :m03)
|
||||
(thc/make-component :c03 :m03))
|
||||
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
|
||||
|
||||
copy01' (ths/get-shape file' :copy01)]
|
||||
|
||||
(thf/dump-file file :keys [:width])
|
||||
|
||||
;; The copy had width 5 before the switch
|
||||
(t/is (= (:width copy01) 5))
|
||||
;; The copy has width 15 after the switch
|
||||
(t/is (= (:width copy01') 15))
|
||||
;; The copy is not touched but has swap slot
|
||||
(t/is (= (count (:touched copy01')) 1))
|
||||
(t/is (= (ctk/get-swap-slot copy01') (thi/id :copy01)))))
|
||||
|
||||
(t/deftest test-nested-switch-in-copy
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant
|
||||
:v01 :c01 :m01 :c02 :m02
|
||||
{:variant1-params {:width 5}
|
||||
:variant2-params {:width 15}})
|
||||
|
||||
(tho/add-frame :m03)
|
||||
(thc/instantiate-component :c01
|
||||
:nested01
|
||||
:parent-label :m03)
|
||||
(thc/make-component :c03 :m03)
|
||||
|
||||
(thc/instantiate-component :c03
|
||||
:nested02
|
||||
:children-labels [:child01]))
|
||||
|
||||
child01 (ths/get-shape file :child01)
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component-in-shape file :child01 :c02 {:keep-touched? true})
|
||||
|
||||
child01' (ths/get-shape file' :child01)]
|
||||
|
||||
(thf/dump-file file :keys [:width])
|
||||
|
||||
;; The copy had width 5 before the switch
|
||||
(t/is (= (:width child01) 5))
|
||||
;; The copy has width 15 after the switch
|
||||
(t/is (= (:width child01') 15))
|
||||
;; The copy is not touched but has swap slot
|
||||
(t/is (= (count (:touched child01')) 1))
|
||||
(t/is (= (ctk/get-swap-slot child01') (thi/id :nested01)))))
|
||||
|
||||
;; ============================================================
|
||||
;; NESTED COPY SWITCH (with overrides)
|
||||
;; ============================================================
|
||||
|
||||
(t/deftest test-nested-switch-in-main-with-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant
|
||||
:v01 :c01 :m01 :c02 :m02
|
||||
{:variant1-params {:width 5}
|
||||
:variant2-params {:width 15}})
|
||||
|
||||
(tho/add-frame :m03)
|
||||
(thc/instantiate-component :c01
|
||||
:copy01
|
||||
:parent-label :m03)
|
||||
(thc/make-component :c03 :m03))
|
||||
|
||||
page (thf/current-page file)
|
||||
fills (ths/sample-fills-color :fill-color "#fabada")
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(thi/id :copy01)}
|
||||
(fn [shape]
|
||||
(assoc shape
|
||||
:width 25
|
||||
:fills fills))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
|
||||
copy01 (ths/get-shape file :copy01)
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component-in-shape file :copy01 :c02 {:keep-touched? true})
|
||||
|
||||
copy01' (ths/get-shape file' :copy01)]
|
||||
|
||||
(thf/dump-file file :keys [:width :touched])
|
||||
|
||||
;; The copy had fill color before the switch
|
||||
(t/is (= (:fills copy01) fills))
|
||||
;; The copy still has fill color after the switch
|
||||
(t/is (= (:fills copy01') fills))
|
||||
;; The copy had width 25 before the switch
|
||||
(t/is (= (:width copy01) 25))
|
||||
;; The copy gets the switched variant width 15, because this is the value changed in the variant
|
||||
(t/is (= (:width copy01') 15))
|
||||
;; The copy is fills touched and has swap slot
|
||||
(t/is (= (count (:touched copy01')) 2))
|
||||
(t/is (= (ctk/get-swap-slot copy01') (thi/id :copy01)))
|
||||
(t/is (contains? (:touched copy01') :fill-group))))
|
||||
|
||||
(t/deftest test-nested-switch-in-copy-with-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant
|
||||
:v01 :c01 :m01 :c02 :m02
|
||||
{:variant1-params {:width 5}
|
||||
:variant2-params {:width 15}})
|
||||
|
||||
(tho/add-frame :m03)
|
||||
(thc/instantiate-component :c01
|
||||
:nested01
|
||||
:parent-label :m03)
|
||||
(thc/make-component :c03 :m03)
|
||||
|
||||
(thc/instantiate-component :c03
|
||||
:copy02
|
||||
:children-labels [:nested02]))
|
||||
|
||||
page (thf/current-page file)
|
||||
fills (ths/sample-fills-color :fill-color "#fabada")
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(thi/id :nested02)}
|
||||
(fn [shape]
|
||||
(assoc shape
|
||||
:width 25
|
||||
:fills fills))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
|
||||
nested02 (ths/get-shape file :nested02)
|
||||
|
||||
;; ==== Action
|
||||
file' (tho/swap-component-in-shape file :nested02 :c02 {:keep-touched? true})
|
||||
|
||||
nested02' (ths/get-shape file' :nested02)]
|
||||
|
||||
(thf/dump-file file :keys [:width])
|
||||
|
||||
;; The copy had fill color before the switch
|
||||
(t/is (= (:fills nested02) fills))
|
||||
;; The copy still has fill color after the switch
|
||||
(t/is (= (:fills nested02') fills))
|
||||
;; The copy had width 5 before the switch
|
||||
(t/is (not= (:width nested02) 5))
|
||||
;; The copy gets the switched variant width 15, because this is the value changed in the variant
|
||||
(t/is (= (:width nested02') 15))
|
||||
;; The copy is fills touched and has swap slot
|
||||
(t/is (= (count (:touched nested02')) 2))
|
||||
(t/is (= (ctk/get-swap-slot nested02') (thi/id :nested01)))
|
||||
(t/is (contains? (:touched nested02') :fill-group))))
|
||||
|
||||
;; ============================================================
|
||||
;; TEXT OVERRIDES (identical variants)
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"archiver": "^8.0.0",
|
||||
"archiver": "7.0.1",
|
||||
"cookies": "^0.9.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"generic-pool": "^3.9.0",
|
||||
|
||||
365
exporter/pnpm-lock.yaml
generated
365
exporter/pnpm-lock.yaml
generated
@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
archiver:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0
|
||||
specifier: 7.0.1
|
||||
version: 7.0.1
|
||||
cookies:
|
||||
specifier: ^0.9.1
|
||||
version: 0.9.1
|
||||
@ -61,6 +61,14 @@ packages:
|
||||
'@ioredis/commands@1.5.1':
|
||||
resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@trysound/sax@0.2.0':
|
||||
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@ -69,9 +77,29 @@ packages:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
|
||||
archiver@8.0.0:
|
||||
resolution: {integrity: sha512-fV1orZfsnPn9BaSByR/qE67rJCLJEy2Ox5bq7nJh+jquWaNh6Sfec75kJ2T6PtdGUbPQlrVoSVCEOa5SdiTQ1g==}
|
||||
engines: {node: '>=18'}
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-regex@6.2.2:
|
||||
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@6.2.3:
|
||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
archiver-utils@5.0.2:
|
||||
resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
archiver@7.0.1:
|
||||
resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
@ -84,9 +112,8 @@ packages:
|
||||
react-native-b4a:
|
||||
optional: true
|
||||
|
||||
balanced-match@4.0.4:
|
||||
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
bare-events@2.8.2:
|
||||
resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==}
|
||||
@ -135,9 +162,8 @@ packages:
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
brace-expansion@5.0.6:
|
||||
resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
brace-expansion@2.1.0:
|
||||
resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
|
||||
|
||||
buffer-crc32@1.0.0:
|
||||
resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==}
|
||||
@ -157,9 +183,16 @@ packages:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
compress-commons@7.0.1:
|
||||
resolution: {integrity: sha512-g0S8KAD8qf4+V//pr3BfB1aBnARLXNz2Gx+jmHU0LEriUuoQUOPOulVquHKTJ8+EAIIO7fhseNDr9wK5Q9FKBQ==}
|
||||
engines: {node: '>=18'}
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
cookies@0.9.1:
|
||||
resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
|
||||
@ -176,9 +209,13 @@ packages:
|
||||
engines: {node: '>=0.8'}
|
||||
hasBin: true
|
||||
|
||||
crc32-stream@7.0.1:
|
||||
resolution: {integrity: sha512-IBWsY8xznyQrcHn8h4bC8/4ErNke5elzgG8GcqF4RFPw6aHkWWRc7Tgw6upjaTX/CT/yQgqYENkxYsTYN+hW2g==}
|
||||
engines: {node: '>=18'}
|
||||
crc32-stream@6.0.0:
|
||||
resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
css-select@5.2.2:
|
||||
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
|
||||
@ -232,6 +269,15 @@ packages:
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
@ -250,6 +296,10 @@ packages:
|
||||
fast-fifo@1.3.2:
|
||||
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
@ -259,6 +309,14 @@ packages:
|
||||
resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
hasBin: true
|
||||
|
||||
graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
|
||||
http-errors@2.0.1:
|
||||
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@ -281,13 +339,23 @@ packages:
|
||||
resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
|
||||
is-stream@4.0.1:
|
||||
resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==}
|
||||
engines: {node: '>=18'}
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-stream@2.0.1:
|
||||
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
keygrip@1.1.0:
|
||||
resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@ -306,15 +374,26 @@ packages:
|
||||
lodash@4.17.21:
|
||||
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
mdn-data@2.0.28:
|
||||
resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
|
||||
|
||||
mdn-data@2.12.2:
|
||||
resolution: {integrity: sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==}
|
||||
|
||||
minimatch@10.2.5:
|
||||
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||
engines: {node: 18 || 20 || >=22}
|
||||
minimatch@5.1.9:
|
||||
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimatch@9.0.9:
|
||||
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minipass@7.1.3:
|
||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
@ -326,6 +405,17 @@ packages:
|
||||
nth-check@2.1.1:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
playwright-core@1.60.0:
|
||||
resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==}
|
||||
engines: {node: '>=18'}
|
||||
@ -354,9 +444,8 @@ packages:
|
||||
resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
|
||||
readdir-glob@3.0.0:
|
||||
resolution: {integrity: sha512-AhNB2KgKeVJr16nK9LLZbJNWnYoT23ZrumNKFDebHBdkC8KHSqWo871JAUhoWC/RtjEVdqNMFpM6qrwRbaUqpw==}
|
||||
engines: {node: '>=18'}
|
||||
readdir-glob@1.1.3:
|
||||
resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==}
|
||||
|
||||
redis-errors@1.2.0:
|
||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||
@ -381,6 +470,18 @@ packages:
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shebang-regex@3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -402,12 +503,28 @@ packages:
|
||||
streamx@2.25.0:
|
||||
resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string-width@5.1.2:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
|
||||
string_decoder@1.3.0:
|
||||
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@7.2.0:
|
||||
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
svgo@https://codeload.github.com/penpot/svgo/tar.gz/a46262c12c0d967708395972c374eb2adead4180:
|
||||
resolution: {tarball: https://codeload.github.com/penpot/svgo/tar.gz/a46262c12c0d967708395972c374eb2adead4180}
|
||||
version: 4.0.0
|
||||
@ -441,6 +558,19 @@ packages:
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ws@8.20.1:
|
||||
resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@ -460,9 +590,9 @@ packages:
|
||||
xregexp@5.1.2:
|
||||
resolution: {integrity: sha512-6hGgEMCGhqCTFEJbqmWrNIPqfpdirdGWkqshu7fFZddmTSfgv5Sn9D2SaKloR79s5VUiUlpwzg3CM3G6D3VIlw==}
|
||||
|
||||
zip-stream@7.0.5:
|
||||
resolution: {integrity: sha512-dSvYKdvLsAHCDqPOhIwk/q5CvuWtTB3Dgpoe0uVEFjTzIOAmsQpprX25InCvrvJsirEbu1OHyy67n/kAj1Sw/w==}
|
||||
engines: {node: '>=18'}
|
||||
zip-stream@6.0.1:
|
||||
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
snapshots:
|
||||
|
||||
@ -472,23 +602,53 @@ snapshots:
|
||||
|
||||
'@ioredis/commands@1.5.1': {}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.2.0
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@trysound/sax@0.2.0': {}
|
||||
|
||||
abort-controller@3.0.0:
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
|
||||
archiver@8.0.0:
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
async: 3.2.6
|
||||
buffer-crc32: 1.0.0
|
||||
is-stream: 4.0.1
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@6.2.3: {}
|
||||
|
||||
archiver-utils@5.0.2:
|
||||
dependencies:
|
||||
glob: 10.5.0
|
||||
graceful-fs: 4.2.11
|
||||
is-stream: 2.0.1
|
||||
lazystream: 1.0.1
|
||||
lodash: 4.17.21
|
||||
normalize-path: 3.0.0
|
||||
readable-stream: 4.7.0
|
||||
readdir-glob: 3.0.0
|
||||
|
||||
archiver@7.0.1:
|
||||
dependencies:
|
||||
archiver-utils: 5.0.2
|
||||
async: 3.2.6
|
||||
buffer-crc32: 1.0.0
|
||||
readable-stream: 4.7.0
|
||||
readdir-glob: 1.1.3
|
||||
tar-stream: 3.2.0
|
||||
zip-stream: 7.0.5
|
||||
zip-stream: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- bare-abort-controller
|
||||
- bare-buffer
|
||||
@ -498,7 +658,7 @@ snapshots:
|
||||
|
||||
b4a@1.8.1: {}
|
||||
|
||||
balanced-match@4.0.4: {}
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
bare-events@2.8.2: {}
|
||||
|
||||
@ -536,9 +696,9 @@ snapshots:
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
brace-expansion@5.0.6:
|
||||
brace-expansion@2.1.0:
|
||||
dependencies:
|
||||
balanced-match: 4.0.4
|
||||
balanced-match: 1.0.2
|
||||
|
||||
buffer-crc32@1.0.0: {}
|
||||
|
||||
@ -553,11 +713,17 @@ snapshots:
|
||||
|
||||
cluster-key-slot@1.1.2: {}
|
||||
|
||||
compress-commons@7.0.1:
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
compress-commons@6.0.2:
|
||||
dependencies:
|
||||
crc-32: 1.2.2
|
||||
crc32-stream: 7.0.1
|
||||
is-stream: 4.0.1
|
||||
crc32-stream: 6.0.0
|
||||
is-stream: 2.0.1
|
||||
normalize-path: 3.0.0
|
||||
readable-stream: 4.7.0
|
||||
|
||||
@ -572,11 +738,17 @@ snapshots:
|
||||
|
||||
crc-32@1.2.2: {}
|
||||
|
||||
crc32-stream@7.0.1:
|
||||
crc32-stream@6.0.0:
|
||||
dependencies:
|
||||
crc-32: 1.2.2
|
||||
readable-stream: 4.7.0
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
css-select@5.2.2:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
@ -629,6 +801,12 @@ snapshots:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
entities@4.5.0: {}
|
||||
|
||||
event-target-shim@5.0.1: {}
|
||||
@ -643,11 +821,27 @@ snapshots:
|
||||
|
||||
fast-fifo@1.3.2: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
generic-pool@3.9.0: {}
|
||||
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.9
|
||||
minipass: 7.1.3
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
graceful-fs@4.2.11: {}
|
||||
|
||||
http-errors@2.0.1:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
@ -680,10 +874,20 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
is-stream@4.0.1: {}
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-stream@2.0.1: {}
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
keygrip@1.1.0:
|
||||
dependencies:
|
||||
tsscmp: 1.0.6
|
||||
@ -698,13 +902,21 @@ snapshots:
|
||||
|
||||
lodash@4.17.21: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
mdn-data@2.0.28: {}
|
||||
|
||||
mdn-data@2.12.2: {}
|
||||
|
||||
minimatch@10.2.5:
|
||||
minimatch@5.1.9:
|
||||
dependencies:
|
||||
brace-expansion: 5.0.6
|
||||
brace-expansion: 2.1.0
|
||||
|
||||
minimatch@9.0.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.1.0
|
||||
|
||||
minipass@7.1.3: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
@ -714,6 +926,15 @@ snapshots:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.3
|
||||
|
||||
playwright-core@1.60.0: {}
|
||||
|
||||
playwright@1.60.0:
|
||||
@ -751,9 +972,9 @@ snapshots:
|
||||
process: 0.11.10
|
||||
string_decoder: 1.3.0
|
||||
|
||||
readdir-glob@3.0.0:
|
||||
readdir-glob@1.1.3:
|
||||
dependencies:
|
||||
minimatch: 10.2.5
|
||||
minimatch: 5.1.9
|
||||
|
||||
redis-errors@1.2.0: {}
|
||||
|
||||
@ -771,6 +992,14 @@ snapshots:
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-support@0.5.21:
|
||||
@ -793,6 +1022,18 @@ snapshots:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string-width@5.1.2:
|
||||
dependencies:
|
||||
eastasianwidth: 0.2.0
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
string_decoder@1.1.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
@ -801,6 +1042,14 @@ snapshots:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-ansi@7.2.0:
|
||||
dependencies:
|
||||
ansi-regex: 6.2.2
|
||||
|
||||
svgo@https://codeload.github.com/penpot/svgo/tar.gz/a46262c12c0d967708395972c374eb2adead4180:
|
||||
dependencies:
|
||||
'@trysound/sax': 0.2.0
|
||||
@ -843,6 +1092,22 @@ snapshots:
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.3
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
ws@8.20.1: {}
|
||||
|
||||
xml-js@1.6.11:
|
||||
@ -853,8 +1118,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@babel/runtime-corejs3': 7.28.4
|
||||
|
||||
zip-stream@7.0.5:
|
||||
zip-stream@6.0.1:
|
||||
dependencies:
|
||||
compress-commons: 7.0.1
|
||||
normalize-path: 3.0.0
|
||||
archiver-utils: 5.0.2
|
||||
compress-commons: 6.0.2
|
||||
readable-stream: 4.7.0
|
||||
|
||||
4321
frontend/playwright/data/workspace/get-file-13958.json
Normal file
4321
frontend/playwright/data/workspace/get-file-13958.json
Normal file
File diff suppressed because one or more lines are too long
@ -172,6 +172,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
this.toolbarOptions = page.getByTestId("toolbar-options");
|
||||
this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" });
|
||||
this.ellipseShapeButton = page.getByRole("button", { name: "Ellipse (E)" });
|
||||
this.textShapeButton = page.getByRole("button", { name: "Text (T)" });
|
||||
this.moveButton = page.getByRole("button", { name: "Move (V)" });
|
||||
this.boardButton = page.getByRole("button", { name: "Board (B)" });
|
||||
this.toggleToolbarButton = page.getByRole("button", {
|
||||
|
||||
@ -214,6 +214,11 @@ test("Multiselection of text and typographies", async ({ page }) => {
|
||||
pageId: "1062e0a0-8fe0-80ae-8007-e70b4993f5f0",
|
||||
});
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-empty.json",
|
||||
);
|
||||
|
||||
const plainTextLayer = workspacePage.layers.getByTestId("layer-row").nth(5);
|
||||
const plainTextLayerTwo = workspacePage.layers
|
||||
.getByTestId("layer-row")
|
||||
|
||||
@ -315,3 +315,19 @@ test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
await expect(fontSizeInput).toHaveValue("");
|
||||
await expect(fontSizeInput).toHaveAttribute("placeholder", "Mixed");
|
||||
});
|
||||
|
||||
// This is to prevent QA tests from failing due to playwright
|
||||
// considering 0-width text boxes as invisible
|
||||
test("BUG 14098 - Fix text editor having 0 width or height", async ({ page }) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await workspace.textShapeButton.click();
|
||||
await workspace.clickAt(200, 200);
|
||||
|
||||
const textEditor = workspace.page.locator(`div[class*="viewport"]`).first().getByRole('textbox').first();
|
||||
await expect(textEditor).toBeVisible();
|
||||
});
|
||||
|
||||
@ -1030,6 +1030,56 @@ test("BUG: 13930, Token colors are shown on selected colors section", async ({
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("BUG: 14136 Apply grid layout padding token to a shape from the sidebar does not change values", async ({
|
||||
page,
|
||||
}) => {
|
||||
// Setup the workspace with token features enabled
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFileRender(page, {
|
||||
flags: ["enable-token-combobox", "enable-feature-token-input"],
|
||||
});
|
||||
|
||||
// Transform a rectangle into a grid container to expose gap properties
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
|
||||
|
||||
const layoutSection = page.getByTestId("inspect-layout");
|
||||
await expect(layoutSection).toBeVisible();
|
||||
|
||||
const addLayoutButton = layoutSection
|
||||
.getByRole("button", { name: "Add layout" })
|
||||
.first();
|
||||
await addLayoutButton.click();
|
||||
await page.getByText("Grid layout").click();
|
||||
|
||||
// Apply a dimension token to the vertical padding property
|
||||
await layoutSection.getByLabel("Open token list").nth(2).click();
|
||||
const tokenDimensionMd = layoutSection.getByRole("option", {
|
||||
name: "dimension.md",
|
||||
});
|
||||
await expect(tokenDimensionMd).toBeVisible();
|
||||
await tokenDimensionMd.click();
|
||||
|
||||
// Expand padding to all sides
|
||||
await layoutSection.getByRole('button', { name: 'Show 4 sided padding options' }).click();
|
||||
const topPaddingSection = layoutSection.getByLabel("Top padding");
|
||||
const bottomPaddingSection = layoutSection.getByLabel("Bottom padding");
|
||||
await expect(topPaddingSection).toBeVisible();
|
||||
|
||||
// Check if token is still applied to top and bottom padding
|
||||
await expect(topPaddingSection.getByLabel("Detach token")).toBeVisible();
|
||||
await expect(bottomPaddingSection.getByLabel("Detach token")).toBeVisible();
|
||||
|
||||
// Check if the value of the attribute is still correct
|
||||
await expect(
|
||||
await topPaddingSection.getByRole("button", { name: "dimension.md" }).textContent()
|
||||
).toBe("16");
|
||||
await expect(
|
||||
await bottomPaddingSection.getByRole("button", { name: "dimension.md" }).textContent()
|
||||
).toBe("16");
|
||||
});
|
||||
|
||||
test.describe("Numeric Input and Token Integration Tests", () => {
|
||||
test("Token pill persists after blur in gap inputs", async ({ page }) => {
|
||||
// Setup the workspace with token features enabled
|
||||
|
||||
29
frontend/playwright/ui/specs/tokens/text.spec.js
Normal file
29
frontend/playwright/ui/specs/tokens/text.spec.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WasmWorkspacePage.init(page);
|
||||
});
|
||||
|
||||
test("BUG 13958 - Fill token gets detached when editing text shape", async ({ page }) => {
|
||||
const workspacePage = new WasmWorkspacePage(page);
|
||||
await workspacePage.setupEmptyFile();
|
||||
// Load a file that has a text shape, whose content contains a `:fills []` in the root node.
|
||||
// This attribute is removed when editing the text, causing the old code to detect that the
|
||||
// fills had changed and detaching the token.
|
||||
await workspacePage.mockGetFile("workspace/get-file-13958.json");
|
||||
await workspacePage.goToWorkspace();
|
||||
|
||||
// Check token is attached to the shape
|
||||
await workspacePage.clickLeafLayer("Design tokens are a set");
|
||||
await expect(workspacePage.rightSidebar.getByLabel("xx.alias.color.text.default", { exact: true })).toBeVisible();
|
||||
|
||||
// Enter and exit the text editor
|
||||
await workspacePage.page.keyboard.press("Enter");
|
||||
await expect(workspacePage.page.getByTestId("text-editor")).toBeVisible();
|
||||
await workspacePage.page.keyboard.press("Escape");
|
||||
await expect(workspacePage.page.getByTestId("text-editor")).not.toBeAttached();
|
||||
|
||||
// Assert token is still attached to the shape
|
||||
await expect(workspacePage.rightSidebar.getByLabel("xx.alias.color.text.default", { exact: true })).toBeVisible();
|
||||
});
|
||||
@ -121,6 +121,11 @@ test("BUG 13385 - Fix viewport not updating when restoring version", async ({ pa
|
||||
await workspacePage.mockGetFile("workspace/get-file-13385.json");
|
||||
await workspacePage.mockRPC("get-profiles-for-file-comments?file-id=*", "workspace/get-profiles-for-file-comments-13385.json");
|
||||
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-empty.json",
|
||||
);
|
||||
|
||||
// navigate to workspace and check that the circle shape is not there
|
||||
await workspacePage.goToWorkspace();
|
||||
await expect(workspacePage.layers.getByText("Ellipse")).not.toBeVisible();
|
||||
@ -141,4 +146,4 @@ test("BUG 13385 - Fix viewport not updating when restoring version", async ({ pa
|
||||
|
||||
// assert that the circle shape exists
|
||||
await expect(workspacePage.layers.getByText("Ellipse")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@ -203,6 +203,38 @@
|
||||
(rx/of (ptk/data-event ::all-libraries-resolved {:file-id file-id})))
|
||||
(rx/take-until stopper-s))))))
|
||||
|
||||
|
||||
(defn check-file-position-data
|
||||
[file-id]
|
||||
(ptk/reify ::fix-position-data
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [file (dsh/lookup-file state file-id)
|
||||
changes
|
||||
(->> file :data :pages
|
||||
(mapcat
|
||||
(fn [page-id]
|
||||
(->> (dsh/lookup-page-objects state file-id page-id)
|
||||
(vals)
|
||||
(filter cfh/text-shape?)
|
||||
(filter #(nil? (:position-data %)))
|
||||
(map (fn [shape]
|
||||
{:type :mod-obj
|
||||
:id (:id shape)
|
||||
:page-id page-id
|
||||
:operations
|
||||
[{:type :set
|
||||
:attr :position-data
|
||||
:val (wasm.api/calculate-position-data shape)
|
||||
:ignore-touched true
|
||||
:ignore-geometry true}]})))))
|
||||
(into []))]
|
||||
(rx/of (dch/commit-changes
|
||||
{:redo-changes changes :undo-changes []
|
||||
:save-undo? false
|
||||
:origin it
|
||||
:tags #{:position-data}}))))))
|
||||
|
||||
(defn- workspace-initialized
|
||||
[file-id]
|
||||
(ptk/reify ::workspace-initialized
|
||||
|
||||
@ -423,8 +423,8 @@
|
||||
(obj/merge!
|
||||
#js {"--editor-container-width" "auto"
|
||||
"--editor-container-height" "auto"
|
||||
"--editor-container-min-width" (dm/str selrect-width "px")
|
||||
"--editor-container-min-height" (dm/str selrect-height "px")
|
||||
"--editor-container-min-width" (dm/str (max 1 selrect-width) "px")
|
||||
"--editor-container-min-height" (dm/str (max 1 selrect-height) "px")
|
||||
"--fallback-families" (if (seq fallback-families) (dm/str (str/join ", " fallback-families)) "sourcesanspro")
|
||||
:display "flex"})
|
||||
|
||||
|
||||
@ -1377,6 +1377,7 @@
|
||||
[:div {:class (stl/css :padding-row)}
|
||||
[:> padding-section* {:value (:layout-padding values)
|
||||
:type (:layout-padding-type values)
|
||||
:ids ids
|
||||
:applied-tokens applied-tokens
|
||||
:on-type-change on-padding-type-change
|
||||
:on-change on-padding-change}]]]
|
||||
|
||||
@ -220,10 +220,11 @@
|
||||
name]
|
||||
[:div {:class (stl/css :page-actions)}
|
||||
(when (and deletable? (not read-only?))
|
||||
[:> icon-button* {:variant "ghost"
|
||||
[:> icon-button* {:variant "action"
|
||||
:aria-label (tr "modals.delete-page.title")
|
||||
:on-click on-delete
|
||||
:icon-size "s"
|
||||
:icon-class (stl/css :page-delete-button-icon)
|
||||
:icon i/delete}])]])])]])))
|
||||
|
||||
;; --- Page Item Wrapper
|
||||
|
||||
@ -55,6 +55,10 @@
|
||||
margin-bottom: deprecated.$s-12;
|
||||
}
|
||||
|
||||
.page-delete-button-icon {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.page-element {
|
||||
@include deprecated.body-small-typography;
|
||||
|
||||
@ -102,23 +106,6 @@
|
||||
height: deprecated.$s-32;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
@include deprecated.button-style;
|
||||
@include deprecated.flex-center;
|
||||
|
||||
width: deprecated.$s-24;
|
||||
height: 100%;
|
||||
opacity: deprecated.$op-0;
|
||||
|
||||
svg {
|
||||
@extend %button-icon-small;
|
||||
|
||||
color: transparent;
|
||||
fill: none;
|
||||
stroke: var(--icon-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.element-name {
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.data.workspace.variants :as dwv]
|
||||
[app.main.features :as features]
|
||||
@ -455,7 +456,10 @@
|
||||
;; blank canvas (first load) visible while shapes load.
|
||||
;; The loading overlay is suppressed because on-shapes-ready
|
||||
;; is set.
|
||||
(wasm.api/initialize-viewport base-objects zoom vbox :background background)
|
||||
(wasm.api/initialize-viewport base-objects zoom vbox
|
||||
:background background
|
||||
:on-shapes-ready
|
||||
#(st/emit! (dw/check-file-position-data file-id)))
|
||||
(reset! initialized? true))
|
||||
|
||||
(when (and (some? vern) (not= vern (mf/ref-val last-vern-ref)))
|
||||
|
||||
@ -121,9 +121,11 @@ pub extern "C" fn render(timestamp: i32) -> Result<()> {
|
||||
// modifier set, so the cost is paid once per rAF rather than
|
||||
// once per pointer move.
|
||||
if get_render_state().options.is_interactive_transform() {
|
||||
let ids = state.shapes.modifier_ids();
|
||||
// Collect into an owned Vec to release the immutable borrow on
|
||||
// `state.shapes` before the mutable `rebuild_modifier_tiles` call.
|
||||
let ids = state.shapes.modifier_ids().to_vec();
|
||||
if !ids.is_empty() {
|
||||
state.rebuild_modifier_tiles(ids)?;
|
||||
state.rebuild_modifier_tiles(&ids)?;
|
||||
}
|
||||
}
|
||||
state
|
||||
@ -856,9 +858,8 @@ pub extern "C" fn set_modifiers() -> Result<()> {
|
||||
|
||||
with_state!(state, {
|
||||
state.set_modifiers(modifiers);
|
||||
// TO CHECK
|
||||
if !get_render_state().options.is_interactive_transform() {
|
||||
state.rebuild_modifier_tiles(ids)?;
|
||||
state.rebuild_modifier_tiles(&ids)?;
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
|
||||
@ -3040,24 +3040,27 @@ impl RenderState {
|
||||
// modified shapes (doc-space @ 100% zoom, scale=1.0). This is used as a cheap overlap
|
||||
// guard to decide when cached top-level crops are unsafe to reuse (something is moving
|
||||
// over/inside them), without doing expensive ancestor walks per node.
|
||||
let moved_bounds =
|
||||
if self.options.is_interactive_transform() && !tree.modifier_ids().is_empty() {
|
||||
let mut acc: Option<Rect> = None;
|
||||
for id in tree.modifier_ids().iter() {
|
||||
let Some(s) = tree.get(id) else { continue };
|
||||
let r = self.get_cached_extrect(s, tree, 1.0);
|
||||
acc = Some(match acc {
|
||||
None => r,
|
||||
Some(mut prev) => {
|
||||
prev.join(r);
|
||||
prev
|
||||
}
|
||||
});
|
||||
}
|
||||
acc
|
||||
} else {
|
||||
None
|
||||
};
|
||||
//
|
||||
// `modifier_ids` is pre-computed once here and reused throughout the loop to avoid
|
||||
// repeated allocations (formerly O(N_shapes) HashMap builds) per node.
|
||||
let modifier_ids = tree.modifier_ids();
|
||||
let moved_bounds = if self.options.is_interactive_transform() && !modifier_ids.is_empty() {
|
||||
let mut acc: Option<Rect> = None;
|
||||
for id in modifier_ids.iter() {
|
||||
let Some(s) = tree.get(id) else { continue };
|
||||
let r = self.get_cached_extrect(s, tree, 1.0);
|
||||
acc = Some(match acc {
|
||||
None => r,
|
||||
Some(mut prev) => {
|
||||
prev.join(r);
|
||||
prev
|
||||
}
|
||||
});
|
||||
}
|
||||
acc
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
while let Some(node_render_state) = self.pending_nodes.pop() {
|
||||
let node_id = node_render_state.id;
|
||||
@ -3136,7 +3139,7 @@ impl RenderState {
|
||||
let use_cached = self.should_use_cached_top_level_during_interactive(
|
||||
node_id,
|
||||
tree,
|
||||
&tree.modifier_ids(),
|
||||
modifier_ids,
|
||||
moved_bounds,
|
||||
);
|
||||
|
||||
@ -3857,7 +3860,7 @@ impl RenderState {
|
||||
pub fn rebuild_modifier_tiles(
|
||||
&mut self,
|
||||
tree: ShapesPoolMutRef<'_>,
|
||||
ids: Vec<Uuid>,
|
||||
ids: &[Uuid],
|
||||
) -> Result<()> {
|
||||
// During interactive transform, skip ancestor invalidation: walking up to the
|
||||
// parent frame evicts every tile the frame covers, including dense tiles with
|
||||
@ -3865,9 +3868,9 @@ impl RenderState {
|
||||
// `ShapesPool::set_modifiers`; the tile index is reconciled post-gesture by
|
||||
// the committing code path (rebuild_touched_tiles).
|
||||
if self.options.is_interactive_transform() {
|
||||
self.update_tiles_shapes(&ids, tree)?;
|
||||
self.update_tiles_shapes(ids, tree)?;
|
||||
} else {
|
||||
let ancestors = all_with_ancestors(&ids, tree, false);
|
||||
let ancestors = all_with_ancestors(ids, tree, false);
|
||||
self.update_tiles_shapes(&ancestors, tree)?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@ -225,8 +225,7 @@ impl State {
|
||||
let _ = get_render_state().render_preview(&self.shapes, timestamp);
|
||||
}
|
||||
|
||||
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) -> Result<()> {
|
||||
// Index-based storage is safe
|
||||
pub fn rebuild_modifier_tiles(&mut self, ids: &[Uuid]) -> Result<()> {
|
||||
get_render_state().rebuild_modifier_tiles(&mut self.shapes, ids)
|
||||
}
|
||||
|
||||
|
||||
@ -49,6 +49,11 @@ pub struct ShapesPoolImpl {
|
||||
modified_shape_cache: HashMap<usize, OnceCell<Shape>>,
|
||||
/// Transform modifiers, keyed by index
|
||||
modifiers: HashMap<usize, skia::Matrix>,
|
||||
/// UUIDs of shapes that have an active transform modifier, kept in sync
|
||||
/// with `modifiers`. Stored explicitly so that `modifier_ids()` is O(K)
|
||||
/// (K = number of modified shapes) instead of O(N_shapes) — avoids
|
||||
/// building a full reverse-index HashMap on every call.
|
||||
modifier_uuids: Vec<Uuid>,
|
||||
/// Structure entries, keyed by index
|
||||
structure: HashMap<usize, Vec<StructureEntry>>,
|
||||
/// Scale content values, keyed by index
|
||||
@ -69,6 +74,7 @@ impl ShapesPoolImpl {
|
||||
|
||||
modified_shape_cache: HashMap::default(),
|
||||
modifiers: HashMap::default(),
|
||||
modifier_uuids: Vec::new(),
|
||||
structure: HashMap::default(),
|
||||
scale_content: HashMap::default(),
|
||||
}
|
||||
@ -238,7 +244,11 @@ impl ShapesPoolImpl {
|
||||
}
|
||||
self.modifiers = modifiers_with_idx;
|
||||
|
||||
// Compute ancestors before consuming `ids` so we can move it into
|
||||
// `modifier_uuids` without a clone.
|
||||
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
||||
// Keep modifier_uuids in sync so modifier_ids() is O(K) not O(N_shapes).
|
||||
self.modifier_uuids = ids;
|
||||
for uuid in all_ids {
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
self.modified_shape_cache.insert(idx, OnceCell::new());
|
||||
@ -300,19 +310,9 @@ impl ShapesPoolImpl {
|
||||
pub fn clean_all(&mut self) -> Vec<Uuid> {
|
||||
self.clean_shape_cache();
|
||||
|
||||
let modified_uuids: Vec<Uuid> = if self.modifiers.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let mut idx_to_uuid: HashMap<usize, Uuid> =
|
||||
HashMap::with_capacity(self.uuid_to_idx.len());
|
||||
for (uuid, idx) in self.uuid_to_idx.iter() {
|
||||
idx_to_uuid.insert(*idx, *uuid);
|
||||
}
|
||||
self.modifiers
|
||||
.keys()
|
||||
.filter_map(|idx| idx_to_uuid.get(idx).copied())
|
||||
.collect()
|
||||
};
|
||||
// `modifier_uuids` is kept in sync with `modifiers` by `set_modifiers`,
|
||||
// so we can take it directly — no need to rebuild a reverse index.
|
||||
let modified_uuids = std::mem::take(&mut self.modifier_uuids);
|
||||
|
||||
self.modifiers = HashMap::default();
|
||||
self.structure = HashMap::default();
|
||||
@ -325,18 +325,12 @@ impl ShapesPoolImpl {
|
||||
/// Used by the throttled drag path so per-rAF tile invalidation can
|
||||
/// be done once with the current modifier set instead of once per
|
||||
/// pointer move.
|
||||
pub fn modifier_ids(&self) -> Vec<Uuid> {
|
||||
if self.modifiers.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut idx_to_uuid: HashMap<usize, Uuid> = HashMap::with_capacity(self.uuid_to_idx.len());
|
||||
for (uuid, idx) in self.uuid_to_idx.iter() {
|
||||
idx_to_uuid.insert(*idx, *uuid);
|
||||
}
|
||||
self.modifiers
|
||||
.keys()
|
||||
.filter_map(|idx| idx_to_uuid.get(idx).copied())
|
||||
.collect()
|
||||
///
|
||||
/// Returns a reference to avoid allocation on every call — callers
|
||||
/// inside hot render loops should hold this reference rather than
|
||||
/// calling `modifier_ids()` repeatedly.
|
||||
pub fn modifier_ids(&self) -> &[Uuid] {
|
||||
&self.modifier_uuids
|
||||
}
|
||||
|
||||
pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl {
|
||||
@ -363,6 +357,7 @@ impl ShapesPoolImpl {
|
||||
uuid_to_idx,
|
||||
modified_shape_cache: HashMap::default(),
|
||||
modifiers: HashMap::default(),
|
||||
modifier_uuids: Vec::new(),
|
||||
structure: HashMap::default(),
|
||||
scale_content: HashMap::default(),
|
||||
}
|
||||
@ -409,6 +404,7 @@ impl Clone for ShapesPoolImpl {
|
||||
// so it gets lazily rebuilt on demand rather than cloning OnceCell state.
|
||||
modified_shape_cache: HashMap::default(),
|
||||
modifiers: self.modifiers.clone(),
|
||||
modifier_uuids: self.modifier_uuids.clone(),
|
||||
structure: self.structure.clone(),
|
||||
scale_content: self.scale_content.clone(),
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user