diff --git a/.circleci/config.yml b/.circleci/config.yml index e1a6e629febfc56a5f554ccb5945c8816330505c..09cc658e4354f927a25f0dc4f0eb27668831550b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,52 +22,53 @@ jobs: # Download and cache dependencies - restore_cache: - keys: - - v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- + keys: + - v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- - run: cd .clj-kondo && cat config.edn - run: cat .cljfmt.edn - run: clj-kondo --version - run: - name: "fmt check backend [clj]" + name: "backend fmt check" working_directory: "./backend" command: | yarn install yarn run fmt:clj:check - run: - name: "fmt check exporter [clj]" + name: "exporter fmt check" working_directory: "./exporter" command: | yarn install yarn run fmt:clj:check - run: - name: "fmt check common [clj]" + name: "common fmt check" working_directory: "./common" command: | yarn install yarn run fmt:clj:check - run: - name: "fmt check frontend [clj]" + name: "frontend fmt check" working_directory: "./frontend" command: | yarn install yarn run fmt:clj:check + yarn run fmt:js:check - run: - name: common lint + name: "common linter check" working_directory: "./common" command: | yarn install yarn run lint:clj - run: - name: frontend lint + name: "frontend linter check" working_directory: "./frontend" command: | yarn install @@ -75,14 +76,14 @@ jobs: yarn run lint:clj - run: - name: backend lint + name: "backend linter check" working_directory: "./backend" command: | yarn install yarn run lint:clj - run: - name: exporter lint + name: "exporter linter check" working_directory: "./exporter" command: | yarn install @@ -93,7 +94,7 @@ jobs: working_directory: "./common" command: | yarn test - clojure -X:dev:test :patterns '["common-tests.*-test"]' + clojure -M:dev:test - run: name: "frontend tests" @@ -102,11 +103,21 @@ jobs: yarn install yarn test + - run: + name: "frontend integration tests" + working_directory: "./frontend" + command: | + yarn install + yarn run build:app:assets + clojure -M:dev:shadow-cljs release main + yarn playwright install --with-deps chromium + yarn e2e:test + - run: name: "backend tests" working_directory: "./backend" command: | - clojure -X:dev:test :patterns '["backend-tests.*-test"]' + clojure -M:dev:test environment: PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test" @@ -115,7 +126,6 @@ jobs: PENPOT_TEST_REDIS_URI: "redis://localhost/1" - save_cache: - paths: - - ~/.m2 - key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} - + paths: + - ~/.m2 + key: v1-dependencies-{{ checksum "backend/deps.edn" }}-{{ checksum "frontend/deps.edn"}}-{{ checksum "common/deps.edn"}} diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index fe1d14d908e73a2b12619f85294600199cfc9d84..5675b9d5d328421c6b734c85bdc58b2b082161b9 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -3,7 +3,6 @@ promesa.core/->> clojure.core/->> promesa.core/-> clojure.core/-> promesa.exec.csp/go-loop clojure.core/loop - rumext.v2/defc clojure.core/defn promesa.util/with-open clojure.core/with-open app.common.schema.generators/let clojure.core/let app.common.data/export clojure.core/def @@ -20,6 +19,7 @@ app.db/with-atomic hooks.export/penpot-with-atomic potok.v2.core/reify hooks.export/potok-reify rumext.v2/fnc hooks.export/rumext-fnc + rumext.v2/defc hooks.export/rumext-defc rumext.v2/lazy-component hooks.export/rumext-lazycomponent shadow.lazy/loadable hooks.export/rumext-lazycomponent }} diff --git a/.clj-kondo/hooks/export.clj b/.clj-kondo/hooks/export.clj index a209cf018fc0e3766c11f8f4623821f131629944..50e617de55144f569bb06df52fb14cb2344aa18c 100644 --- a/.clj-kondo/hooks/export.clj +++ b/.clj-kondo/hooks/export.clj @@ -12,6 +12,7 @@ (def registry (atom {})) + (defn potok-reify [{:keys [:node :filename] :as params}] (let [[rnode rtype & other] (:children node) @@ -66,12 +67,86 @@ (let [[cname mdata params & body] (rest (:children node)) [params body] (if (api/vector-node? mdata) [mdata (cons params body)] - [params body])] - (let [result (api/list-node - (into [(api/token-node 'fn) - params] - (cons mdata body)))] - {:node result}))) + [params body]) + + result (api/list-node + (into [(api/token-node 'fn) params] + (cons mdata body)))] + + {:node result})) + + +(defn- parse-defc + [{:keys [children] :as node}] + (let [args (rest children) + + [cname args] + (if (api/token-node? (first args)) + [(first args) (rest args)] + (throw (ex-info "unexpected1" {}))) + + [docs args] + (if (api/string-node? (first args)) + [(first args) (rest args)] + ["" args]) + + [mdata args] + (if (api/map-node? (first args)) + [(first args) (rest args)] + [(api/map-node []) args]) + + [params body] + (if (api/vector-node? (first args)) + [(first args) (rest args)] + (throw (ex-info "unexpected2" {})))] + + [cname docs mdata params body])) + +(defn rumext-defc + [{:keys [node]}] + (let [[cname docs mdata params body] (parse-defc node) + + param1 (first (:children params)) + paramN (rest (:children params)) + + param1 (if (api/map-node? param1) + (let [param1 (into {} (comp + (partition-all 2) + (map (fn [[k v]] + [(if (api/keyword-node? k) + (:k k) + k) + (if (api/vector-node? v) + (vec (:children v)) + v)]))) + (:children param1)) + + binding (:rest param1) + param1 (if binding + (if (contains? param1 :as) + (update param1 :keys (fnil conj []) binding) + (assoc param1 :as binding)) + param1)] + (->> (dissoc param1 :rest) + (mapcat (fn [[k v]] + [(if (keyword? k) + (api/keyword-node k) + k) + (if (vector? v) + (api/vector-node v) + v)])) + (api/map-node))) + param1) + + result (api/list-node + (into [(api/token-node 'defn) + cname + (api/vector-node (filter some? (cons param1 paramN)))] + (cons mdata body)))] + + ;; (prn (api/sexpr result)) + + {:node result})) (defn rumext-lazycomponent diff --git a/.cljfmt.edn b/.cljfmt.edn index 38cfeb89b649d71b1c504ac395e44e3dc91773d1..02c567b2e34ddf6d46230487ed51f1c966f4d638 100644 --- a/.cljfmt.edn +++ b/.cljfmt.edn @@ -4,6 +4,7 @@ :remove-consecutive-blank-lines? false :extra-indents {rumext.v2/fnc [[:inner 0]] cljs.test/async [[:inner 0]] + app.common.schema/register! [[:inner 0] [:inner 1]] promesa.exec/thread [[:inner 0]] specify! [[:inner 0] [:inner 1]]} } diff --git a/.gitignore b/.gitignore index 0e271d1257a553db15dcf56936377adac53b15b5..b0b2074d8d4310cc087865378ded8da4f3ef9983 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,8 @@ /deploy /docker/images/bundle* /exporter/target +/frontend/.storybook/preview-body.html +/frontend/.storybook/preview-head.html /frontend/cypress/fixtures/validuser.json /frontend/cypress/videos/*/ /frontend/cypress/videos/*/ @@ -57,6 +59,7 @@ /frontend/package-lock.json /frontend/resources/fonts/experiments /frontend/resources/public/* +/frontend/storybook-static/ /frontend/target/ /other/ /scripts/ @@ -67,4 +70,7 @@ /web clj-profiler/ node_modules -frontend/.storybook/preview-body.html +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.yarnrc.yml b/.yarnrc.yml index 896c0eefc0f033cf2d39b43614e5769fc5f7e458..5a0ce9a8b33b26df1f12b7e659377847d760911b 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -6,4 +6,6 @@ enableImmutableInstalls: false enableTelemetry: false +httpTimeout: 600000 + nodeLinker: node-modules diff --git a/CHANGES.md b/CHANGES.md index f80e9724571c9e98c4bd528b3d87962f05f95c13..57a907f6f4ffb7cbe33a52769f351dd69071b1c7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,221 @@ # CHANGELOG +## 2.3.0 + +### :rocket: Epics and highlights + +### :boom: Breaking changes & Deprecations + +### :heart: Community contributions (Thank you!) + +### :sparkles: New features + +### :bug: Bugs fixed + +## 2.2.0 + +### :rocket: Epics and highlights + +### :boom: Breaking changes & Deprecations + +### :heart: Community contributions (Thank you!) + +- Set proper default tenant on exporter (by @june128) [#4946](https://github.com/penpot/penpot/pull/4946) +- Correct a spelling in onboarding.edn (by @n-stha) [#4936](https://github.com/penpot/penpot/pull/4936) + +### :sparkles: New features + +- **Tiered File Data Storage** [Taiga #8376](https://tree.taiga.io/project/penpot/us/8376) + + This feature allows offloading file data that is not actively used + from the database to object storage (e.g., filesystem, S3), thereby + freeing up space in the database. It can be enabled with the + `enable-enable-tiered-file-data-storage` flag. + + *(On-Premise feature, EXPERIMENTAL).* + +- **JSON Interoperability for HTTP API** [Taiga #8372](https://tree.taiga.io/project/penpot/us/8372) + + Enables full JSON interoperability for our HTTP API. Previously, + JSON was only barely supported for output when the + `application/json` media type was specified in the `Accept` header, + or when `_fmt=json` was passed as a query parameter. With this + update, we now offer proper bi-directional support for using our API + with plain JSON, instead of Transit. + +- **Automatic File Snapshotting** + + Adds the ability to automatically take and maintain a limited set of + snapshots of active files without explicit user intervention. This + feature allows on-premise administrators to recover the state of a + file from a past point in time in a limited manner. + + It can be enabled with the `enable-auto-file-snapshot` flag and + configured with the following settings: + + ```bash + # Take snapshots every 10 update operations + PENPOT_AUTO_FILE_SNAPSHOT_EVERY=10 + + # Take a snapshot if it has been more than 3 hours since the file was last modified + PENPOT_AUTO_FILE_SNAPSHOT_TIMEOUT=3h + + # The total number of snapshots to keep + PENPOT_AUTO_FILE_SNAPSHOT_TOTAL=10 + ``` + + Snapshots are only taken during update operations; there is NO + active background process for this. + +- Add separated flag `enable-oidc-registration` for enable the + registration only for OIDC authentication backend [Github + #4882](https://github.com/penpot/penpot/issues/4882) + +- Update templates in libraries & templates in dashboard modal [Taiga #8145](https://tree.taiga.io/project/penpot/us/8145) + +- **Design System** + + We implemented and subbed in new components from our Design System: `loader*` ([Taiga #8355](https://tree.taiga.io/project/penpot/task/8355)) and `tab-switcher*` ([Taiga #8518](https://tree.taiga.io/project/penpot/task/8518)). + +- **Storybook** [Taiga #6329](https://tree.taiga.io/project/penpot/us/6329) + + The Design System components are now published in a Storybook, available at `/storybook`. + +### :bug: Bugs fixed + +- Fix webhook checkbox position [Taiga #8634](https://tree.taiga.io/project/penpot/issue/8634) +- Fix wrong props on padding input [Taiga #8254](https://tree.taiga.io/project/penpot/issue/8254) +- Fix fill collapsed options [Taiga #8351](https://tree.taiga.io/project/penpot/issue/8351) +- Fix scroll on color picker modal [Taiga #8353](https://tree.taiga.io/project/penpot/issue/8353) +- Fix components are not dragged from the group to the assets tab [Taiga #8273](https://tree.taiga.io/project/penpot/issue/8273) +- Fix problem with SVG import [Github #4888](https://github.com/penpot/penpot/issues/4888) +- Fix problem with overlay positions in viewer [Taiga #8464](https://tree.taiga.io/project/penpot/issue/8464) +- Fix layer panel overflowing [Taiga #8665](https://tree.taiga.io/project/penpot/issue/8665) +- Fix problem when creating a component instance from grid layout [Github #4881](https://github.com/penpot/penpot/issues/4881) +- Fix problem when dismissing shared library update [Taiga #8669](https://tree.taiga.io/project/penpot/issue/8669) + +## 2.1.5 + +### :bug: Bugs fixed + +- Fix broken webhooks [Taiga #8370](https://tree.taiga.io/project/penpot/issue/8370) + +## 2.1.4 + +### :bug: Bugs fixed + +- Fix json encoding on zip encoding decoding. +- Add schema validation for color changes. +- Fix render of some texts without position data. + +## 2.1.3 + +- Don't allow registration when registration is disabled and invitation token is used [Github #4975](https://github.com/penpot/penpot/issues/4975) + +## 2.1.2 + +### :bug: Bugs fixed + +- User switch language to "zh_hant" will get 400 [Github #4884](https://github.com/penpot/penpot/issues/4884) +- Smtp config ignoring port if ssl is set [Github #4872](https://github.com/penpot/penpot/issues/4872) +- Ability to let users to authenticate with a private oidc provider only [Github #4963](https://github.com/penpot/penpot/issues/4963) + +## 2.1.1 + +### :sparkles: New features + +- Consolidate templates new order and naming [Taiga #8392](https://tree.taiga.io/project/penpot/task/8392) + +### :bug: Bugs fixed + +- Fix the “search” label in translations [Taiga #8402](https://tree.taiga.io/project/penpot/issue/8402) +- Fix pencil loader [Taiga #8348](https://tree.taiga.io/project/penpot/issue/8348) +- Fix several issues on the OIDC. +- Fix regression on the `email-verification` flag [Taiga #8398](https://tree.taiga.io/project/penpot/issue/8398) + +## 2.1.0 - Things can only get better! + +### :rocket: Epics and highlights + +### :boom: Breaking changes & Deprecations + +### :heart: Community contributions (Thank you!) + +### :sparkles: New features + +- Improve auth process [Taiga #7094](https://tree.taiga.io/project/penpot/us/7094) +- Add locking degrees increment (hold shift) on path edition [Taiga #7761](https://tree.taiga.io/project/penpot/issue/7761) +- Persistence & Concurrent Edition Enhancements [Taiga #5657](https://tree.taiga.io/project/penpot/us/5657) +- Allow library colors as recent colors [Taiga #7640](https://tree.taiga.io/project/penpot/issue/7640) +- Missing scroll in viewmode comments [Taiga #7427](https://tree.taiga.io/project/penpot/issue/7427) +- Comments in View mode should mimic the positioning behavior of the Workspace [Taiga #7346](https://tree.taiga.io/project/penpot/issue/7346) +- Misaligned input on comments [Taiga #7461](https://tree.taiga.io/project/penpot/issue/7461) + +### :bug: Bugs fixed + +- Fix selection rectangle appears on scroll [Taiga #7525](https://tree.taiga.io/project/penpot/issue/7525) +- Fix layer tree not expanding to the bottom edge [Taiga #7466](https://tree.taiga.io/project/penpot/issue/7466) +- Fix guides move when board is moved by inputs [Taiga #8010](https://tree.taiga.io/project/penpot/issue/8010) +- Fix clickable area of Penptot logo in the viewer [Taiga #7988](https://tree.taiga.io/project/penpot/issue/7988) +- Fix constraints dropdown when selecting multiple shapes [Taiga #7686](https://tree.taiga.io/project/penpot/issue/7686) +- Layout and scrollign fixes for the bottom palette [Taiga #7559](https://tree.taiga.io/project/penpot/issue/7559) +- Fix expand libraries when search results are present [Taiga #7876](https://tree.taiga.io/project/penpot/issue/7876) +- Fix color palette default library [Taiga #8029](https://tree.taiga.io/project/penpot/issue/8029) +- Component Library is lost after exporting/importing in .zip format [Github #4672](https://github.com/penpot/penpot/issues/4672) +- Fix problem with moving+selection not working properly [Taiga #7943](https://tree.taiga.io/project/penpot/issue/7943) +- Fix problem with flex layout fit to content not positioning correctly children [Taiga #7537](https://tree.taiga.io/project/penpot/issue/7537) +- Fix black line is displaying after show main [Taiga #7653](https://tree.taiga.io/project/penpot/issue/7653) +- Fix "Share prototypes" modal remains open [Taiga #7442](https://tree.taiga.io/project/penpot/issue/7442) +- Fix "Components visibility and opacity" [#4694](https://github.com/penpot/penpot/issues/4694) +- Fix "Attribute overrides in copies are not exported in zip file" [Taiga #8072](https://tree.taiga.io/project/penpot/issue/8072) +- Fix group not automatically selected in the Layers panel after creation [Taiga #8078](https://tree.taiga.io/project/penpot/issue/8078) +- Fix export boards loses opacity [Taiga #7592](https://tree.taiga.io/project/penpot/issue/7592) +- Fix change color on imported svg also changes the stroke alignment[Taiga #7673](https://github.com/penpot/penpot/pull/7673) +- Fix show in view mode and interactions workflow [Taiga #4711](https://github.com/penpot/penpot/pull/4711) +- Fix internal error when I set up a stroke for some objects without and with stroke [Taiga #7558](https://tree.taiga.io/project/penpot/issue/7558) +- Toolbar keeps toggling on and off on spacebar press [Taiga #7654](https://github.com/penpot/penpot/pull/7654) +- Fix toolbar keeps hiding when click outside workspace [Taiga #7776](https://tree.taiga.io/project/penpot/issue/7776) +- Fix open overlay relative to a frame [Taiga #7563](https://tree.taiga.io/project/penpot/issue/7563) +- Workspace-palette items stay hidden when opening with keyboard-shortcut [Taiga #7489](https://tree.taiga.io/project/penpot/issue/7489) +- Fix SVG attrs are not handled correctly when exporting/importing in .zip [Taiga #7920](https://tree.taiga.io/project/penpot/issue/7920) +- Fix validation error when detaching with two nested copies and a swap [Taiga #8095](https://tree.taiga.io/project/penpot/issue/8095) +- Export shapes that are rotated act a bit strange when reimported [Taiga #7585](https://tree.taiga.io/project/penpot/issue/7585) +- Penpot crashes when a new colorpicker is created while uploading an image to another instance [Taiga #8119](https://tree.taiga.io/project/penpot/issue/8119) +- Removing Underline and Strikethrough Affects the Previous Text Object [Taiga #8103](https://tree.taiga.io/project/penpot/issue/8103) +- Color library loses association with shapes when exporting/importing the document [Taiga #8132](https://tree.taiga.io/project/penpot/issue/8132) +- Fix can't collapse groups when searching in the assets tab [Taiga #8125](https://tree.taiga.io/project/penpot/issue/8125) +- Fix 'Detach instance' shortcut is not working [Taiga #8102](https://tree.taiga.io/project/penpot/issue/8102) +- Fix import file message does not detect 0 as error [Taiga #6824](https://tree.taiga.io/project/penpot/issue/6824) +- Image Color Library is not persisted when exporting/importing in .zip [Taiga #8131](https://tree.taiga.io/project/penpot/issue/8131) +- Fix export files including libraries [Taiga #8266](https://tree.taiga.io/project/penpot/issue/8266) + +## 2.0.3 + +### :bug: Bugs fixed + +- Fix chrome scrollbar styling [Taiga #7852](https://tree.taiga.io/project/penpot/issue/7852) +- Fix incorrect password encoding on create-profile manage scritp [Github #3651](https://github.com/penpot/penpot/issues/3651) + +## 2.0.2 + +### :sparkles: Enhancements + +- Fix locking contention on cron subsystem (causes backend start blocking) +- Fix locking contention on file object thumbails backend RPC calls + +### :bug: Bugs fixed + +- Fix color palette sorting [Taiga #7458](https://tree.taiga.io/project/penpot/issue/7458) +- Fix style scoping problem with imported SVG [Taiga #7671](https://tree.taiga.io/project/penpot/issue/7671) + + +## 2.0.1 + +### :bug: Bugs fixed + +- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443) + + ## 2.0.0 - I Just Can't Get Enough ### :rocket: Epics and highlights @@ -14,6 +230,8 @@ ### :boom: Breaking changes & Deprecations - New strokes default to inside border [Taiga #6847](https://tree.taiga.io/project/penpot/issue/6847) +- Change default z ordering on layers in flex layout. The previous behavior was inconsistent with how HTML works and we changed it to be more consistent. Previous layers that overlapped could be hidden, the fastest way to fix this is changing the z-index property but a better way is to change the order of your layers. + ### :heart: Community contributions (Thank you!) - New Hausa, Yoruba and Igbo translations and update translation files (by All For Tech Empowerment Foundation) [Taiga #6950](https://tree.taiga.io/project/penpot/us/6950), [Taiga #6534](https://tree.taiga.io/project/penpot/us/6534) @@ -91,7 +309,7 @@ - [REDESIGN] Panels visual separations [Taiga #6692](https://tree.taiga.io/project/penpot/us/6692) - [REDESIGN] Onboarding slides [Taiga #6678](https://tree.taiga.io/project/penpot/us/6678) -### :bug Bugs fixed +### :bug: Bugs fixed - Fix pixelated thumbnails [Github #3681](https://github.com/penpot/penpot/issues/3681), [Github #3661](https://github.com/penpot/penpot/issues/3661) - Fix problem with not applying colors to boards [Github #3941](https://github.com/penpot/penpot/issues/3941) - Fix problem with path editor undoing changes [Github #3998](https://github.com/penpot/penpot/issues/3998) @@ -130,7 +348,9 @@ - Fix problem with hand tool stuck [Github #3318](https://github.com/penpot/penpot/issues/3318) - Fix problem with fix scrolling on nested elements [Github #3508](https://github.com/penpot/penpot/issues/3508) - Fix problem when changing typography assets [Github #3683](https://github.com/penpot/penpot/issues/3683) - +- Internal error when you copy and paste some main components between files [Taiga #7397](https://tree.taiga.io/project/penpot/issue/7397) +- Fix toolbar disappearing [Taiga #7411](https://tree.taiga.io/project/penpot/issue/7411) +- Fix long text on tab breaks UI [Taiga #7421](https://tree.taiga.io/project/penpot/issue/7421) ## 1.19.5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ae27a013537105b1a7c13b2252a51bc4ab5d8029..9e2091679b9ded22559cf4a8c9e6ade4cc61f6b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,7 +48,7 @@ quick win. If is going to be your first pull request, You can learn how from this free video series: -https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github +https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github We will use the `easy fix` mark for tag for indicate issues that are easy for beginners. diff --git a/README.md b/README.md index 0b662801767711cd87214b2f9e4782178afd333d..848b3efd18ad11b8c88e6a76b39888c77a67bc64 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,11 @@ [uri_license]: https://www.mozilla.org/en-US/MPL/2.0 [uri_license_image]: https://img.shields.io/badge/MPL-2.0-blue.svg -

-
- PENPOT -

+ + + + penpot header image +

License: MPL-2.0 Gitter @@ -13,75 +14,89 @@ Gitpod ready-to-code

- Website • - Getting Started • - User Guide • - Tutorials & Info • - Community • - Twitter • - Instagram • - Mastodon • - Youtube + Website • + Getting Started • + User Guide • + Tutorials & Info • + Community

+

+ Youtube • + Peertube • + Linkedin • + Instagram • + Mastodon • + X + +

+ +
+ +[Penpot video](https://github.com/penpot/penpot/assets/5446186/b8ad0764-585e-4ddc-b098-9b4090d337cc) + +
-![feature-readme](https://user-images.githubusercontent.com/1045247/189871786-0b44f7cf-3a0a-4445-a87b-9919ec398bf7.gif) +Penpot is the first **open-source** design tool for design and code collaboration. Designers can create stunning designs, interactive prototypes, design systems at scale, while developers enjoy ready-to-use code and make their workflow easy and fast. And all of this with no handoff drama. -🎇 **Penpot Fest exceeded all expectations - it was a complete success!** 🎇 Penpot Fest is our first Design event that brought designers and developers from the Open Source communities and beyond. Watch the replay of the talks on our [Youtube channel](https://www.youtube.com/playlist?list=PLgcCPfOv5v56-fghJo2dHNBqL9zlDTslh) or [Peertube channel](https://peertube.kaleidos.net/w/p/1tWgyJTt8sKbWwCEcBimZW) +Penpot is available on browser and [self host](https://penpot.app/self-host). It’s web-based and works with open standards (SVG, CSS and HTML). And last but not least, it’s free! -Penpot is the first **Open Source** design and prototyping platform meant for cross-domain teams. Non dependent on operating systems, Penpot is web based and works with open standards (SVG). Penpot invites designers all over the world to fall in love with open source while getting developers excited about the design process in return. +Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the platform to a whole new level. This update introduces the ground-breaking [CSS Grid Layout feature](https://penpot.app/penpot-2.0), a complete UI redesign, a new Components system, and much more. Plus, it's faster and more accessible. + + +🎇 **Penpot Fest** is our design, code & Open Source event. Check out the highlights from [Penpot Fest 2023 edition](https://www.youtube.com/watch?v=sOpLZaK5mDc)! ## Table of contents ## - [Why Penpot](#why-penpot) - [Getting Started](#getting-started) - [Community](#community) +- [Contributing](#contributing) - [Resources](#resources) - [License](#license) ## Why Penpot ## -Penpot makes design and prototyping accessible to every team in the world. +Penpot expresses designs as code. Designers can do their best work and see it will be beautifully implemented by developers in a two-way collaboration. + +### Designed for developers ### +Penpot was built to serve both designers and developers and create a fluid design-code process. You have the choice to enjoy real-time collaboration or play "solo". + +### Inspect mode ### +Work with ready-to-use code and make your workflow easy and fast. The inspect tab gives instant access to SVG, CSS and HTML code. + +### Self host your own instance ### +Provide your team or organization with a completely owned collaborative design tool. Use Penpot's cloud service or deploy your own Penpot server. -### For cross-domain teams ### -We have a clear focus on design and code teams and our capabilities reflect exactly that. The less hand-off mindset, the more fun for everyone. +### Integrations ### +Penpot offers integration into the development toolchain, thanks to its support for webhooks and an API accessible through access tokens. -### Multiplatform ### -Being web based, Penpot is not dependent on operating systems or local installations, you will only need to run a modern browser. +### What’s great for design ### +With Penpot you can design libraries to share and reuse; turn design elements into components and tokens to allow reusability and scalability; and build realistic user flows and interactions. -### Open Standards ### -Using SVG as no other design and prototyping tool does, Penpot files sport compatibility with most of the vectorial tools, are tech friendly and extremely easy to use on the web. We make sure you will always own your work. +

- Open Source + Open Source

+
## Getting started ## ### Install with Elestio ### -[Elestio](https://elest.io/) offers a fully managed service for on-premise instances of a selection of open-source software! This means you can deploy a dedicated instance of Penpot in just 3 minutes with no technical knowledge needed. +Penpot is the only design & prototype platform that is deployment agnostic. You can use it or deploy it anywhere. -You don’t need to worry about DNS configuration, SMTP, backups, SSL certificates, OS & Penpot upgrades, and much more. - -[Get started with Elestio.](https://help.penpot.app/technical-guide/getting-started/#install-with-elestio) - -### Install with Docker ### - -You can also get started with Penpot locally or self-host it with **docker** and **docker-compose**. - -Here’s a step-by-step guide on [getting started with Docker.](https://help.penpot.app/technical-guide/getting-started/#install-with-docker) - -### Penpot cloud app ### - -If you prefer not to install Penpot in a local environment, [login or register on our Penpot cloud app](https://design.penpot.app). Create a team to work together on projects and share design assets or jump right away into Penpot and **start designing** on your own. +Learn how to install it with Elestio and Docker, or other options on [our website](https://penpot.app/self-host). +

- Getting started + Open Source

+
## Community ## -We love the open source software community. Contributing is our passion and if it’s yours too, [participate](https://community.penpot.app/) and [improve](https://community.penpot.app/c/help-us-improve-penpot/7) Penpot. All your ideas and code are welcome! +We love the Open Source software community. Contributing is our passion and if it’s yours too, participate and [improve](https://community.penpot.app/c/help-us-improve-penpot/7) Penpot. All your designs, code and ideas are welcome! If you need help or have any questions; if you’d like to share your experience using Penpot or get inspired; if you’d rather meet our community of developers and designers, [join our Community](https://community.penpot.app/)! @@ -93,30 +108,41 @@ You will find the following categories: - [Events and Announcements](https://community.penpot.app/c/announcements/5) - [Inside Penpot](https://community.penpot.app/c/inside-penpot/21) - [Penpot in your language](https://community.penpot.app/c/penpot-in-your-language/12) +- [Design and Code Essentials](https://community.penpot.app/c/design-and-code-essentials/22) + + +

- Communnity + Community

+
## Contributing ## -Every sort of contribution will be very helpful to enhance Penpot. How you’ll participate? All your ideas, designs and code are welcome: +Any contribution will make a difference to improve Penpot. How can you get involved? + +Choose your way: +- Create and [share Libraries & Templates](https://penpot.app/libraries-templates.html) that will be helpful for the community - Invite your [team to join](https://design.penpot.app/#/auth/register) -- Star this repo and follow us on Social Media: [Twitter](https://twitter.com/penpotapp), [Instagram](https://instagram.com/penpot.app), [Youtube](https://www.youtube.com/c/Penpot) or [Mastodon](https://fosstodon.org/@penpot/). -- Participate in the [Community](https://community.penpot.app/) asking and answering questions, reacting to others’ articles or opening your own conversations. +- Star this repo and follow us on Social Media: [Mastodon](https://fosstodon.org/@penpot/), [Youtube](https://www.youtube.com/c/Penpot), [Instagram](https://instagram.com/penpot.app), [Linkedin](https://www.linkedin.com/company/penpotdesign), [Peertube](https://peertube.kaleidos.net/a/penpot_app) and [X](https://twitter.com/penpotapp). +- Participate in the [Community](https://community.penpot.app/) space by asking and answering questions; reacting to others’ articles; opening your own conversations and following along on decisions affecting the project. - Report bugs with our easy [guide for bugs hunting](https://help.penpot.app/contributing-guide/reporting-bugs/) or [GitHub issues](https://github.com/penpot/penpot/issues) -- Create and [share Libraries & templates](https://penpot.app/libraries-templates.html) that will be helpful for the community - Become a [translator](https://help.penpot.app/contributing-guide/translations) -- Give feedback: [Mail us](mailto:support@penpot.app) +- Give feedback: [Email us](mailto:support@penpot.app) - **Contribute to Penpot's code:** [Watch this video](https://www.youtube.com/watch?v=TpN0osiY-8k) by Alejandro Alonso, CIO and developer at Penpot, where he gives us a hands-on demo of how to use Penpot’s repository and make changes in both front and back end -To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing-guide](https://help.penpot.app/contributing-guide/). +To find (almost) everything you need to know on how to contribute to Penpot, refer to the [contributing guide](https://help.penpot.app/contributing-guide/). + +

- Contributing + Libraries and templates

+
+ ## Resources ## You can ask and answer questions, have open-ended conversations, and follow along on decisions affecting the project. @@ -141,4 +167,4 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. Copyright (c) KALEIDOS INC ``` -Penpot is a Kaleidos’ [open source project](https://kaleidos.net/products) +Penpot is a Kaleidos’ [open source project](https://kaleidos.net/) diff --git a/THANKYOU.md b/THANKYOU.md index 1a27aa8eb923e9d81ace5a09d2ebe712adc52aec..8c075112b3c50fced57ce289f3c5b9b5e2a9542c 100644 --- a/THANKYOU.md +++ b/THANKYOU.md @@ -2,12 +2,19 @@ We want to thank to the amazing people that help us! Thank you! You're the best! +Feel free you make a PR updating this file if you miss you in the +list. + ## Security + * Husnain Iqbal (CEO OF ALPHA INFERNO PVT LTD) * [Shiraz Ali Khan](https://www.linkedin.com/in/shiraz-ali-khan-1ba508180/) * Vaibhav Shukla +* Hassan Ahmed (Alias Xen Lee) +* Michal Biesiada (@mbiesiad) ## Internationalization + * [00ff88](https://hosted.weblate.org/user/00ff88) * [AhmadHB](https://hosted.weblate.org/user/AhmadHB) * [Aimee](https://hosted.weblate.org/user/Aimee) @@ -89,6 +96,7 @@ We want to thank to the amazing people that help us! Thank you! You're the best! * [zcraber](https://hosted.weblate.org/user/zcraber) ## Libraries & templates + * systxema * plumilla * victor crespo diff --git a/backend/deps.edn b/backend/deps.edn index afd1e6840b67ee44af11d7d9a0c7a754948b8c9e..a8840282911386114340778fadb88a809e5c5305 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -3,10 +3,10 @@ :deps {penpot/common {:local/root "../common"} - org.clojure/clojure {:mvn/version "1.12.0-alpha5"} - org.clojure/tools.namespace {:mvn/version "1.4.4"} + org.clojure/clojure {:mvn/version "1.12.0-alpha12"} + org.clojure/tools.namespace {:mvn/version "1.5.0"} - com.github.luben/zstd-jni {:mvn/version "1.5.5-11"} + com.github.luben/zstd-jni {:mvn/version "1.5.6-3"} io.prometheus/simpleclient {:mvn/version "0.16.0"} io.prometheus/simpleclient_hotspot {:mvn/version "0.16.0"} @@ -17,7 +17,7 @@ io.prometheus/simpleclient_httpserver {:mvn/version "0.16.0"} - io.lettuce/lettuce-core {:mvn/version "6.3.0.RELEASE"} + io.lettuce/lettuce-core {:mvn/version "6.3.2.RELEASE"} java-http-clj/java-http-clj {:mvn/version "0.4.3"} funcool/yetti @@ -26,13 +26,13 @@ :git/url "https://github.com/funcool/yetti.git" :exclusions [org.slf4j/slf4j-api]} - com.github.seancorfield/next.jdbc {:mvn/version "1.3.909"} - metosin/reitit-core {:mvn/version "0.6.0"} - nrepl/nrepl {:mvn/version "1.1.0"} - cider/cider-nrepl {:mvn/version "0.44.0"} + com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"} + metosin/reitit-core {:mvn/version "0.7.0"} + nrepl/nrepl {:mvn/version "1.1.2"} + cider/cider-nrepl {:mvn/version "0.48.0"} - org.postgresql/postgresql {:mvn/version "42.7.1"} - org.xerial/sqlite-jdbc {:mvn/version "3.44.1.0"} + org.postgresql/postgresql {:mvn/version "42.7.3"} + org.xerial/sqlite-jdbc {:mvn/version "3.46.0.0"} com.zaxxer/HikariCP {:mvn/version "5.1.0"} @@ -54,11 +54,11 @@ org.clojars.pntblnk/clj-ldap {:mvn/version "0.0.17"} dawran6/emoji {:mvn/version "0.1.5"} - markdown-clj/markdown-clj {:mvn/version "1.11.7"} + markdown-clj/markdown-clj {:mvn/version "1.12.1"} ;; Pretty Print specs pretty-spec/pretty-spec {:mvn/version "0.1.4"} - software.amazon.awssdk/s3 {:mvn/version "2.22.12"} + software.amazon.awssdk/s3 {:mvn/version "2.25.63"} } :paths ["src" "resources" "target/classes"] @@ -74,16 +74,13 @@ :build {:extra-deps - {io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}} + {io.github.clojure/tools.build {:git/tag "v0.10.3" :git/sha "15ead66"}} :ns-default build} :test - {:extra-paths ["test"] - :extra-deps - {io.github.cognitect-labs/test-runner - {:git/tag "v0.5.1" :git/sha "dfb30dd"}} - :main-opts ["-m" "cognitect.test-runner"] - :exec-fn cognitect.test-runner.api/test} + {:main-opts ["-m" "kaocha.runner"] + :jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"] + :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}} :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}} @@ -91,8 +88,8 @@ :jmx-remote {:jvm-opts ["-Dcom.sun.management.jmxremote" - "-Dcom.sun.management.jmxremote.port=9090" - "-Dcom.sun.management.jmxremote.rmi.port=9090" + "-Dcom.sun.management.jmxremote.port=9091" + "-Dcom.sun.management.jmxremote.rmi.port=9091" "-Dcom.sun.management.jmxremote.local.only=false" "-Dcom.sun.management.jmxremote.authenticate=false" "-Dcom.sun.management.jmxremote.ssl=false" diff --git a/backend/dev/user.clj b/backend/dev/user.clj index 9fc59d5e1187c61a0a3069196f52a21313a01d2f..5f742ff15627f94f7a99aaec3631ce8a1efece31 100644 --- a/backend/dev/user.clj +++ b/backend/dev/user.clj @@ -20,6 +20,7 @@ [app.common.schema.desc-native :as smdn] [app.common.schema.generators :as sg] [app.common.spec :as us] + [app.common.json :as json] [app.common.transit :as t] [app.common.types.file :as ctf] [app.common.uuid :as uuid] @@ -29,7 +30,6 @@ [app.srepl.helpers :as srepl.helpers] [app.srepl.main :as srepl] [app.util.blob :as blob] - [app.util.json :as json] [app.util.time :as dt] [clj-async-profiler.core :as prof] [clojure.contrib.humanize :as hum] diff --git a/backend/package.json b/backend/package.json index 18b183c3aeca0c67c16935ecfa6b204610df0b5e..0855bf4ebbc090948067f1c2a4e6d6639d6d6140 100644 --- a/backend/package.json +++ b/backend/package.json @@ -4,19 +4,19 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.0.2", + "packageManager": "yarn@4.3.1", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" }, "dependencies": { - "luxon": "^3.4.2", - "sax": "^1.2.4" + "luxon": "^3.4.4", + "sax": "^1.4.1" }, "devDependencies": { - "nodemon": "^3.0.1", + "nodemon": "^3.1.2", "source-map-support": "^0.5.21", - "ws": "^8.13.0" + "ws": "^8.17.0" }, "scripts": { "fmt:clj:check": "cljfmt check --parallel=false src/ test/", diff --git a/backend/resources/app/email/change-email/en.html b/backend/resources/app/email/change-email/en.html index ad95fa6414e5d461d3931dc59dd61c53452cb3f8..d63efa72f7ddf439e7227d43039b1bfc24a1ab0f 100644 --- a/backend/resources/app/email/change-email/en.html +++ b/backend/resources/app/email/change-email/en.html @@ -168,7 +168,7 @@ @@ -475,4 +475,4 @@ - \ No newline at end of file + diff --git a/backend/resources/app/email/change-email/en.txt b/backend/resources/app/email/change-email/en.txt index 0a688cb0dcc779a4dec9c2c090d6d9554b3b5dce..09d6e84a5626b3588f9c8688b33a1df599de496d 100644 --- a/backend/resources/app/email/change-email/en.txt +++ b/backend/resources/app/email/change-email/en.txt @@ -1,4 +1,4 @@ -Hello {{name}}! +Hello {{name|abbreviate:25}}! We received a request to change your current email to {{ pending-email }}. diff --git a/backend/resources/app/email/feedback/en.html b/backend/resources/app/email/feedback/en.html index 478a3cc3cf6080d2b0476bd26d4785454e51d065..6de9cda624c25717de95e59b675de2bbae58f5c5 100644 --- a/backend/resources/app/email/feedback/en.html +++ b/backend/resources/app/email/feedback/en.html @@ -11,7 +11,7 @@ {% if profile %} Name: - {{profile.fullname}} + {{profile.fullname|abbreviate:25}}
@@ -34,7 +34,7 @@

Subject:
- {{subject}} + {{subject|abbreviate:300}}

diff --git a/backend/resources/app/email/invite-to-team/en.html b/backend/resources/app/email/invite-to-team/en.html index 881af47f43779f3ebfee1a78cb68746bf85452b0..93763c106c6cca1ff64fc238d46eed7380bdf7bd 100644 --- a/backend/resources/app/email/invite-to-team/en.html +++ b/backend/resources/app/email/invite-to-team/en.html @@ -173,7 +173,7 @@

@@ -465,4 +465,4 @@ - \ No newline at end of file + diff --git a/backend/resources/app/email/invite-to-team/en.txt b/backend/resources/app/email/invite-to-team/en.txt index ea85c084fff045002ee7c016a6da304d8cc641aa..55e61d8e23277f3c7d635de6b4192784075cbdc3 100644 --- a/backend/resources/app/email/invite-to-team/en.txt +++ b/backend/resources/app/email/invite-to-team/en.txt @@ -1,6 +1,6 @@ Hello! -{{invited-by}} has invited you to join the team “{{ team }}”. +{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”. Accept invitation using this link: diff --git a/backend/resources/app/email/join-team/en.html b/backend/resources/app/email/join-team/en.html new file mode 100644 index 0000000000000000000000000000000000000000..7668ee0fb43ae8e74980c2176810d27fe0599fdd --- /dev/null +++ b/backend/resources/app/email/join-team/en.html @@ -0,0 +1,518 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
-
Hello {{name}}!
+
Hello {{name|abbreviate:25}}!
-
{{invited-by}} has invited you to join the team “{{ team }}”.
+
{{invited-by|abbreviate:25}} has invited you to join the team “{{ team|abbreviate:25 }}”.
+ + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ Hello!
+
+
+ As you requested, {{invited-by|abbreviate:25}} has added you to the team “{{ + team|abbreviate:25}}”.
+
+ + + + +
+ Go to the Team +
+
+
+ Enjoy!
+
+
+ The Penpot team.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot | Made with <3 and Open Source
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/backend/resources/app/email/join-team/en.subj b/backend/resources/app/email/join-team/en.subj new file mode 100644 index 0000000000000000000000000000000000000000..296ce140f64964f5b99ca8eabeee469a68e27004 --- /dev/null +++ b/backend/resources/app/email/join-team/en.subj @@ -0,0 +1 @@ +You have joined {{team}} diff --git a/backend/resources/app/email/join-team/en.txt b/backend/resources/app/email/join-team/en.txt new file mode 100644 index 0000000000000000000000000000000000000000..78cba680e093f52aa7d9df342219422e2f721121 --- /dev/null +++ b/backend/resources/app/email/join-team/en.txt @@ -0,0 +1,10 @@ +Hello! + +As you requested, {{invited-by|abbreviate:25}} has added you to the team “{{ team|abbreviate:25}}”. + +Go to the team with this link: + +{{ public-uri }}/#/dashboard/team/{{team-id}} + +Enjoy! +The Penpot team. diff --git a/backend/resources/app/email/password-recovery/en.html b/backend/resources/app/email/password-recovery/en.html index 14fe1a5f2d7b8671e1084203b9796bada6eb4c93..ed18ef12cfdd0185011b5f19b9dd98a048f25795 100644 --- a/backend/resources/app/email/password-recovery/en.html +++ b/backend/resources/app/email/password-recovery/en.html @@ -168,7 +168,7 @@ @@ -470,4 +470,4 @@ - \ No newline at end of file + diff --git a/backend/resources/app/email/password-recovery/en.txt b/backend/resources/app/email/password-recovery/en.txt index ad314b41d465cb9e079e5155fd2b514cf5abd724..3bac8f815fe8b83f4ef9bc54e84d1b0c1c40fe95 100644 --- a/backend/resources/app/email/password-recovery/en.txt +++ b/backend/resources/app/email/password-recovery/en.txt @@ -1,4 +1,4 @@ -Hello {{name}}! +Hello {{name|abbreviate:25}}! We received a request to reset your password. Click the link below to choose a new one: diff --git a/backend/resources/app/email/register/en.html b/backend/resources/app/email/register/en.html index 4a425b69f56d707ca81250a755ee27741e8a862b..3f058b184b852fe3aa25d3977521bf40f9cd0b3c 100644 --- a/backend/resources/app/email/register/en.html +++ b/backend/resources/app/email/register/en.html @@ -168,7 +168,7 @@
-
Hello {{name}}!
+
Hello {{name|abbreviate:25}}!
diff --git a/backend/resources/app/email/register/en.txt b/backend/resources/app/email/register/en.txt index 41a9bd8d9f0fcee74ac2063dc524d537a9796d6f..c38454ccde8aed32f311738c3608a6090abd0001 100644 --- a/backend/resources/app/email/register/en.txt +++ b/backend/resources/app/email/register/en.txt @@ -1,4 +1,4 @@ -Hello {{name}}! +Hello {{name|abbreviate:25}}! Thanks for signing up for your Penpot account! Please verify your email using the link below and get started building mockups and prototypes today! diff --git a/backend/resources/app/email/request-file-access-yourpenpot-view/en.html b/backend/resources/app/email/request-file-access-yourpenpot-view/en.html new file mode 100644 index 0000000000000000000000000000000000000000..53e217cf75019ad42ac6ef9802e9bcf78fc576f1 --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot-view/en.html @@ -0,0 +1,528 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
-
Hello {{name}}!
+
Hello {{name|abbreviate:25}}!
+ + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+ + +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ Hello!
+
+
+

+ {{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have view-only access to the + file named “{{file-name|abbreviate:25}}”. +

+

+ Since this file is in your Penpot team, you can provide access by sending a view-only link. + This will allow {{requested-by|abbreviate:25}} to view the content without making any changes. +

+

To proceed, please click the button below to generate and send the view-only link:

+
+
+ + + + +
+ Send a View-Only link +
+
+
+

If you do not wish to grant access at this time, you can simply disregard this email.

+

Thank you

+
+
+
+ The Penpot team.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot | Made with <3 and Open Source
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/backend/resources/app/email/request-file-access-yourpenpot-view/en.subj b/backend/resources/app/email/request-file-access-yourpenpot-view/en.subj new file mode 100644 index 0000000000000000000000000000000000000000..2e577c3e0e587f9dd23a44cb0af27487980ccf4e --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot-view/en.subj @@ -0,0 +1 @@ +Request View-Only Access to “{{file-name|abbreviate:25}}” diff --git a/backend/resources/app/email/request-file-access-yourpenpot-view/en.txt b/backend/resources/app/email/request-file-access-yourpenpot-view/en.txt new file mode 100644 index 0000000000000000000000000000000000000000..67eb6cedf748913ba56da05da681d98197fb5447 --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot-view/en.txt @@ -0,0 +1,17 @@ +Hello! + +{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have view-only access to the file named “{{file-name|abbreviate:25}}”. + +Since this file is in your Penpot team, you can provide access by sending a view-only link. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes. + +To proceed, please click the link below to generate and send the view-only link: + +{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true + + + +If you do not wish to grant access at this time, you can simply disregard this email. +Thank you + + +The Penpot team. diff --git a/backend/resources/app/email/request-file-access-yourpenpot/en.html b/backend/resources/app/email/request-file-access-yourpenpot/en.html new file mode 100644 index 0000000000000000000000000000000000000000..f168168babfed6c0ef2ddfba80be55ff00a61c18 --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot/en.html @@ -0,0 +1,551 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + +
+
+ Hello!
+
+
+

+ {{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named + “{{file-name|abbreviate:25}}”. +

+

+ Please note that the file is currently in Your Penpot 's team, so direct access cannot be + granted. However, you have two options to provide the requested access: +

+
    +
  • +

    Move the File to Another Team:

    +

    You can move the file to another team and then give access to that team, inviting + {{requested-by|abbreviate:25}}.

    +
  • +
+

+
+
+
+
    +
  • +

    Send a View-Only Link:

    +

    Alternatively, you can create and share a view-only link to the file. This will allow + {{requested-by|abbreviate:25}} to view the content without making any changes.

    +

    Click the button below to generate and send the link:

    +
  • +
+

+
+
+ + + + +
+ Send a View-Only link +
+
+
+

If you do not wish to grant access at this time, you can simply disregard this email.

+

Thank you

+
+
+
+ The Penpot team.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot | Made with <3 and Open Source
+
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/backend/resources/app/email/request-file-access-yourpenpot/en.subj b/backend/resources/app/email/request-file-access-yourpenpot/en.subj new file mode 100644 index 0000000000000000000000000000000000000000..d4a90980b39c24bbb85a27e50308b53e87a6aa40 --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot/en.subj @@ -0,0 +1 @@ +Request Access to “{{file-name|abbreviate:25}}” diff --git a/backend/resources/app/email/request-file-access-yourpenpot/en.txt b/backend/resources/app/email/request-file-access-yourpenpot/en.txt new file mode 100644 index 0000000000000000000000000000000000000000..140cb044531dcb12a4a596082264a39471dfc419 --- /dev/null +++ b/backend/resources/app/email/request-file-access-yourpenpot/en.txt @@ -0,0 +1,30 @@ +Hello! + + +Hello! + +{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named “{{file-name|abbreviate:25}}”. + +Please note that the file is currently in Your Penpot 's team, so direct access cannot be granted. However, you have two options to provide the requested access: + +- Move the File to Another Team: + +You can move the file to another team and then give access to that team, inviting {{requested-by|abbreviate:25}}. + + + +- Send a View-Only Link: + +Alternatively, you can create and share a view-only link to the file. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes. + +Click the link below to generate and send the link: + +{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true + + + +If you do not wish to grant access at this time, you can simply disregard this email. +Thank you + + +The Penpot team. diff --git a/backend/resources/app/email/request-file-access/en.html b/backend/resources/app/email/request-file-access/en.html new file mode 100644 index 0000000000000000000000000000000000000000..23254524b34f155a9db83c835766a1e7f3d9c2ad --- /dev/null +++ b/backend/resources/app/email/request-file-access/en.html @@ -0,0 +1,568 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ Hello!
+
+
+

+ {{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named + “{{file-name|abbreviate:25}}”. +

+

+ To provide this access, you have the following options: +

+
    +
  • +

    Give Access to the “{{team-name|abbreviate:25}}” Team:

    +

    This will automatically include {{requested-by|abbreviate:25}} in the team, so the user + can see all the projects and files in it.

    +

    Click the button below to provide team access:

    +
  • +
+

+
+
+ + + + +
+ Give access to “{{team-name|abbreviate:25}}” Team +
+
+
+
    +
  • +

    Send a View-Only Link:

    +

    Alternatively, you can create and share a view-only link to the file. This will allow + {{requested-by|abbreviate:25}} to view the content without making any changes.

    +

    Click the button below to generate and send the link:

    +
  • +
+

+
+
+ + + + +
+ Send a View-Only link +
+
+
+

If you do not wish to grant access at this time, you can simply disregard this email.

+

Thank you

+
+
+
+ The Penpot team.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot | Made with <3 and Open Source
+
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/backend/resources/app/email/request-file-access/en.subj b/backend/resources/app/email/request-file-access/en.subj new file mode 100644 index 0000000000000000000000000000000000000000..d4a90980b39c24bbb85a27e50308b53e87a6aa40 --- /dev/null +++ b/backend/resources/app/email/request-file-access/en.subj @@ -0,0 +1 @@ +Request Access to “{{file-name|abbreviate:25}}” diff --git a/backend/resources/app/email/request-file-access/en.txt b/backend/resources/app/email/request-file-access/en.txt new file mode 100644 index 0000000000000000000000000000000000000000..d327e478034e1af348747c5b7a63e0ef269a5860 --- /dev/null +++ b/backend/resources/app/email/request-file-access/en.txt @@ -0,0 +1,34 @@ +Hello! + + +Hello! + +{{requested-by|abbreviate:25}} ({{requested-by-email}}) has requested access to the file named “{{file-name|abbreviate:25}}”. + +To provide this access, you have the following options: + +- Give Access to the “{{team-name|abbreviate:25}}” Team: + +This will automatically include {{requested-by|abbreviate:25}} in the team, so the user can see all the projects and files in it. + +Click the link below to provide team access: + +{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}} + + + +- Send a View-Only Link: + +Alternatively, you can create and share a view-only link to the file. This will allow {{requested-by|abbreviate:25}} to view the content without making any changes. + +Click the link below to generate and send the link: + +{{ public-uri }}/#/view/{{file-id}}?page-id={{page-id}}§ion=interactions&index=0&share=true + + + +If you do not wish to grant access at this time, you can simply disregard this email. +Thank you + + +The Penpot team. diff --git a/backend/resources/app/email/request-team-access/en.html b/backend/resources/app/email/request-team-access/en.html new file mode 100644 index 0000000000000000000000000000000000000000..103b0870b9c19c8fc76ba8b42caa13e946424f63 --- /dev/null +++ b/backend/resources/app/email/request-team-access/en.html @@ -0,0 +1,526 @@ + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+
+ Hello!
+
+
+

+ {{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have access to the + “{{team-name|abbreviate:25}}” Team. +

+

+ To provide access, please click the button below: +

+
+
+ + + + +
+ Give access to “{{team-name|abbreviate:25}}” +
+
+
+

If you do not wish to grant access at this time, you can simply disregard this email.

+

Thank you

+
+
+
+ The Penpot team.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot is the first Open Source design and prototyping platform meant for cross-domain teams. +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ + + + + +
+ + + + +
+ + + +
+
+ +
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + +
+
+ Penpot | Made with <3 and Open Source
+
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/backend/resources/app/email/request-team-access/en.subj b/backend/resources/app/email/request-team-access/en.subj new file mode 100644 index 0000000000000000000000000000000000000000..d455c082b49cf98fae7662e8569b2892088bf2bc --- /dev/null +++ b/backend/resources/app/email/request-team-access/en.subj @@ -0,0 +1 @@ +Request Access to “{{team-name|abbreviate:25}}” diff --git a/backend/resources/app/email/request-team-access/en.txt b/backend/resources/app/email/request-team-access/en.txt new file mode 100644 index 0000000000000000000000000000000000000000..225bc1e26f42be55d5482925c09bb690b7a0ee7c --- /dev/null +++ b/backend/resources/app/email/request-team-access/en.txt @@ -0,0 +1,14 @@ +Hello! + +{{requested-by|abbreviate:25}} ({{requested-by-email}}) wants to have access to the “{{team-name|abbreviate:25}}” Team. + +To provide access, please click the link below: + +{{ public-uri }}/#/dashboard/team/{{team-id}}/members?invite-email={{requested-by-email|urlescape}} + + +If you do not wish to grant access at this time, you can simply disregard this email. +Thank you + + +The Penpot team. diff --git a/backend/resources/app/onboarding.edn b/backend/resources/app/onboarding.edn index 0438d25ba067a138f8b8353b95c0d9076493635b..07a11859d89f81a99dcf8c93e104738ccf03df6f 100644 --- a/backend/resources/app/onboarding.edn +++ b/backend/resources/app/onboarding.edn @@ -1,30 +1,42 @@ -[{:id "material-design-3" - :name "Material Design 3" - :file-uri "https://github.com/penpot/penpot-files/raw/main/Material%20Design%203.penpot"} +[{:id "wireframing-kit" + :name "Wireframe library" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"} + {:id "prototype-examples" + :name "Prototype template" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/prototype-examples.penpot"} + {:id "plants-app" + :name "UI mockup example" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"} + {:id "penpot-design-system" + :name "Design system example" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"} {:id "tutorial-for-beginners" :name "Tutorial for beginners" :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/tutorial-for-beginners.penpot"} - {:id "penpot-design-system" - :name "Penpot Design System" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Penpot-Design-system.penpot"} - {:id "flex-layout-playground" - :name "Flex Layout Playground" - :file-uri "https://github.com/penpot/penpot-files/raw/main/Flex%20Layout%20Playground.penpot"} - {:id "wireframing-kit" - :name "Wireframing Kit" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.penpot"} - {:id "ant-design" - :name "Ant Design UI Kit (lite)" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Ant-Design-UI-Kit-Lite.penpot"} - {:id "cocomaterial" - :name "Cocomaterial" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Cocomaterial.penpot"} - {:id "circum-icons" - :name "Circum Icons pack" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/CircumIcons.penpot"} - {:id "coreui" - :name "CoreUI" - :file-uri "https://github.com/penpot/penpot-files/raw/main/CoreUI%20DesignSystem%20(DEMO).penpot"} + {:id "lucide-icons" + :name "Lucide Icons" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Lucide-icons.penpot"} + {:id "font-awesome" + :name "Font Awesome" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Font-Awesome.penpot"} + {:id "black-white-mobile-templates" + :name "Black & White Mobile Templates" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Black-White-Mobile-Templates.penpot"} + {:id "avataaars" + :name "Avataaars" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Avataaars-by-Pablo-Stanley.penpot"} + {:id "ux-notes" + :name "UX Notes" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/UX-Notes.penpot"} {:id "whiteboarding-kit" :name "Whiteboarding Kit" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"}] + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Whiteboarding-mapping-kit.penpot"} + {:id "open-color-scheme" + :name "Open Color Scheme" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Open-Color-Scheme.penpot"} + {:id "flex-layout-playground" + :name "Flex Layout Playground" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Flex-Layout-Playground.penpot"} + {:id "welcome" + :name "Welcome" + :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/welcome.penpot"}] diff --git a/backend/resources/app/templates/api-doc-entry.tmpl b/backend/resources/app/templates/api-doc-entry.tmpl index 31c48deebf7721e327e05d5f5336294a9f26c06f..9123233a832a60a778ca17564e90d5d5d66a330a 100644 --- a/backend/resources/app/templates/api-doc-entry.tmpl +++ b/backend/resources/app/templates/api-doc-entry.tmpl @@ -20,12 +20,19 @@ WEBHOOK {% endif %} + {% if item.params-schema-js %} SCHEMA {% endif %} + {% if item.spec %} + + SPEC + + {% endif %} + {% if item.sse %} SSE diff --git a/backend/resources/log4j2-devenv.xml b/backend/resources/log4j2-devenv.xml index 31e1968294c41a122e7a36c98b883aa7a61c6816..3cf7ab00b78f8e862d82246a6e7f9dbac7b85e00 100644 --- a/backend/resources/log4j2-devenv.xml +++ b/backend/resources/log4j2-devenv.xml @@ -40,7 +40,7 @@ - + diff --git a/backend/resources/log4j2-experiments.xml b/backend/resources/log4j2-experiments.xml index 3357aae31f88f0800d8084f604203fa04043fcb4..a874af5d84f617208510e7c9b2eac4a8416155fd 100644 --- a/backend/resources/log4j2-experiments.xml +++ b/backend/resources/log4j2-experiments.xml @@ -6,7 +6,7 @@ alwaysWriteExceptions="true" /> - + @@ -15,7 +15,7 @@ - + diff --git a/backend/scripts/manage.py b/backend/scripts/manage.py index 564c0e2d52b895671347fd62d968ad4612c24d31..d3971e68d481356d666c31de29ef6668be5b2f2c 100755 --- a/backend/scripts/manage.py +++ b/backend/scripts/manage.py @@ -160,7 +160,6 @@ available_commands = ( "delete-profile", "search-profile", "derive-password", - "migrate-components-v2", ) parser = argparse.ArgumentParser( @@ -233,7 +232,4 @@ elif args.action == "search-profile": search_profile(email) -elif args.action == "migrate-components-v2": - migrate_components_v2() - diff --git a/backend/scripts/repl b/backend/scripts/repl index 4e454b761434928ad660e9e5b7e2aa7be9a997c9..9d3a5a808e50cfedf95f1c5db7b08bfcda0718ed 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -4,7 +4,7 @@ export PENPOT_HOST=devenv export PENPOT_TENANT=dev export PENPOT_FLAGS="\ $PENPOT_FLAGS \ - enable-registration + enable-login-with-ldap \ enable-login-with-password enable-login-with-oidc \ enable-login-with-google \ @@ -24,14 +24,15 @@ export PENPOT_FLAGS="\ enable-rpc-climit \ enable-rpc-rlimit \ enable-soft-rpc-rlimit \ + enable-auto-file-snapshot \ enable-webhooks \ enable-access-tokens \ - disable-feature-components-v2 \ + enable-tiered-file-data-storage \ enable-file-validation \ - enable-file-schema-validation \ - disable-soft-file-schema-validation \ - disable-soft-file-validation"; + enable-file-schema-validation"; +# Default deletion delay for devenv +export PENPOT_DELETION_DELAY="24h" # Setup default upload media file size to 100MiB export PENPOT_MEDIA_MAX_FILE_SIZE=104857600 @@ -62,9 +63,10 @@ mc mb penpot-s3/penpot -p -q export AWS_ACCESS_KEY_ID=penpot-devenv export AWS_SECRET_ACCESS_KEY=penpot-devenv -export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3 -export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000 -export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot + +export PENPOT_OBJECTS_STORAGE_BACKEND=s3 +export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000 +export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot export OPTIONS=" -A:jmx-remote -A:dev \ diff --git a/backend/scripts/repl-test b/backend/scripts/repl-test index a1333a531715db2995e3090aec1de4615a590f3f..2ba1acdbfdce03d67f4edb06c5403c68ff2af91c 100755 --- a/backend/scripts/repl-test +++ b/backend/scripts/repl-test @@ -1,6 +1,6 @@ #!/usr/bin/env bash -source /home/penpot/backend/environ +source /home/penpot/environ export PENPOT_FLAGS="$PENPOT_FLAGS disable-backend-worker" export OPTIONS=" @@ -12,13 +12,13 @@ export OPTIONS=" -J-XX:+UnlockDiagnosticVMOptions \ -J-XX:+DebugNonSafepoints \ -J-Djdk.tracePinnedThreads=full \ + -J-XX:+UseTransparentHugePages \ + -J-XX:ReservedCodeCacheSize=1g \ -J-Dpolyglot.engine.WarnInterpreterOnly=false \ -J--enable-preview"; # Setup HEAP -#export OPTIONS="$OPTIONS -J-Xms900m -J-Xmx900m -J-XX:+AlwaysPreTouch" -export OPTIONS="$OPTIONS -J-Xms1g -J-Xmx25g" -#export OPTIONS="$OPTIONS -J-Xms900m -J-Xmx900m -J-XX:+AlwaysPreTouch" +export OPTIONS="$OPTIONS -J-Xms320g -J-Xmx320g -J-XX:+AlwaysPreTouch" export PENPOT_HTTP_SERVER_IO_THREADS=2 export PENPOT_HTTP_SERVER_WORKER_THREADS=2 @@ -33,11 +33,10 @@ export PENPOT_HTTP_SERVER_WORKER_THREADS=2 # export OPTIONS="$OPTIONS -J-Xint" # Setup GC -export OPTIONS="$OPTIONS -J-XX:+UseG1GC -J-Xlog:gc:logs/gc.log" - +export OPTIONS="$OPTIONS -J-XX:+UseG1GC -J-Xlog:gc:logs/gc.log" # Setup GC -#export OPTIONS="$OPTIONS -J-XX:+UseZGC -J-XX:+ZGenerational -J-Xlog:gc:gc.log" +#export OPTIONS="$OPTIONS -J-XX:+UseZGC -J-XX:+ZGenerational -J-Xlog:gc:logs/gc.log" # Enable ImageMagick v7.x support # export OPTIONS="-J-Dim4java.useV7=true $OPTIONS"; @@ -46,4 +45,4 @@ export OPTIONS_EVAL="nil" # export OPTIONS_EVAL="(set! *warn-on-reflection* true)" set -ex -exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main \ No newline at end of file +exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main diff --git a/backend/scripts/run.template.sh b/backend/scripts/run.template.sh index d2e20ca91964cbdaddee9b0a22dd603ae349d228..3d04cdcec8bbfbc0974f1993016ae4797e7a5667 100644 --- a/backend/scripts/run.template.sh +++ b/backend/scripts/run.template.sh @@ -18,7 +18,9 @@ if [ -f ./environ ]; then source ./environ fi -export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow --enable-preview $JVM_OPTS" +export JVM_OPTS="-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager -Dlog4j2.configurationFile=log4j2.xml -XX:-OmitStackTraceInFastThrow -Dpolyglot.engine.WarnInterpreterOnly=false --enable-preview $JVM_OPTS" -set -x -exec $JAVA_CMD $JVM_OPTS "$@" -jar penpot.jar -m app.main +ENTRYPOINT=${1:-app.main}; + +set -ex +exec $JAVA_CMD $JVM_OPTS -jar penpot.jar -m $ENTRYPOINT diff --git a/backend/scripts/start-dev b/backend/scripts/start-dev index 89df83d96ee06951b34aa09cb6ebfa33da5f742a..b137af101e247584fc138a41e8f9a0e4b2f22350 100755 --- a/backend/scripts/start-dev +++ b/backend/scripts/start-dev @@ -15,13 +15,13 @@ export PENPOT_FLAGS="\ enable-feature-fdata-pointer-map \ enable-feature-fdata-objects-map \ disable-secure-session-cookies \ + enable-rpc-climit \ enable-smtp \ + enable-file-snapshot \ enable-access-tokens \ - disable-feature-components-v2 \ + enable-tiered-file-data-storage \ enable-file-validation \ - enable-file-schema-validation \ - disable-soft-file-schema-validation \ - disable-soft-file-validation"; + enable-file-schema-validation"; export OPTIONS=" -A:jmx-remote -A:dev \ @@ -34,35 +34,18 @@ export OPTIONS=" -J-XX:+UnlockDiagnosticVMOptions \ -J-XX:+DebugNonSafepoints" +# Default deletion delay for devenv +export PENPOT_DELETION_DELAY="24h" + # Setup default upload media file size to 100MiB export PENPOT_MEDIA_MAX_FILE_SIZE=104857600 # Setup default multipart upload size to 300MiB export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=314572800 -# Setup HEAP -# export OPTIONS="$OPTIONS -J-Xms50m -J-Xmx1024m" -# export OPTIONS="$OPTIONS -J-Xms1100m -J-Xmx1100m -J-XX:+AlwaysPreTouch" - -# Increase virtual thread pool size -# export OPTIONS="$OPTIONS -J-Djdk.virtualThreadScheduler.parallelism=16" - -# Disable C2 Compiler -# export OPTIONS="$OPTIONS -J-XX:TieredStopAtLevel=1" - -# Disable all compilers -# export OPTIONS="$OPTIONS -J-Xint" - -# Setup GC -# export OPTIONS="$OPTIONS -J-XX:+UseG1GC" - -# Setup GC -# export OPTIONS="$OPTIONS -J-XX:+UseZGC" - # Enable ImageMagick v7.x support # export OPTIONS="-J-Dim4java.useV7=true $OPTIONS"; - # Initialize MINIO config mc alias set penpot-s3/ http://minio:9000 minioadmin minioadmin -q mc admin user add penpot-s3 penpot-devenv penpot-devenv -q @@ -74,28 +57,12 @@ mc mb penpot-s3/penpot -p -q export AWS_ACCESS_KEY_ID=penpot-devenv export AWS_SECRET_ACCESS_KEY=penpot-devenv -export PENPOT_ASSETS_STORAGE_BACKEND=assets-s3 -export PENPOT_STORAGE_ASSETS_S3_ENDPOINT=http://minio:9000 -export PENPOT_STORAGE_ASSETS_S3_BUCKET=penpot - -if [ "$1" = "--watch" ]; then - trap "exit" INT TERM ERR - trap "kill 0" EXIT +export PENPOT_OBJECTS_STORAGE_BACKEND=s3 +export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000 +export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot - echo "Start Watch..." +entrypoint=${1:-app.main}; - clojure $OPTIONS -A:dev -M -m app.main & +set -ex - npx nodemon \ - --watch src \ - --watch ../common \ - --ext "clj" \ - --signal SIGKILL \ - --exec 'echo "(app.main/stop)\n\r(repl/refresh)\n\r(app.main/start)\n" | nc -N localhost 6062' - - wait; - -else - set -x - clojure $OPTIONS -A:dev -M -m app.main; -fi +clojure $OPTIONS -A:dev -M -m $entrypoint; diff --git a/backend/src/app/auth.clj b/backend/src/app/auth.clj index 5bde8aa79bd532f15391dbf19e600325612e5c30..fc6d2548104e3b07909e39e3ef604db57beb3d6b 100644 --- a/backend/src/app/auth.clj +++ b/backend/src/app/auth.clj @@ -6,9 +6,7 @@ (ns app.auth (:require - [app.config :as cf] - [buddy.hashers :as hashers] - [cuerdas.core :as str])) + [buddy.hashers :as hashers])) (def default-params {:alg :argon2id @@ -27,17 +25,3 @@ (catch Throwable _ {:update false :valid false}))) - -(defn email-domain-in-whitelist? - "Returns true if email's domain is in the given whitelist or if - given whitelist is an empty string." - ([email] - (let [domains (cf/get :registration-domain-whitelist)] - (email-domain-in-whitelist? domains email))) - ([domains email] - (if (or (nil? domains) (empty? domains)) - true - (let [[_ candidate] (-> (str/lower email) - (str/split #"@" 2))] - (contains? domains candidate))))) - diff --git a/backend/src/app/auth/ldap.clj b/backend/src/app/auth/ldap.clj index 5100abff9b448bf50fd1710506ac658935a206b4..c430a794d6e017ad71dbd6c1ef5b1062bbb43143 100644 --- a/backend/src/app/auth/ldap.clj +++ b/backend/src/app/auth/ldap.clj @@ -9,7 +9,6 @@ [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.spec :as us] - [app.config :as cf] [clj-ldap.client :as ldap] [clojure.spec.alpha :as s] [clojure.string] @@ -104,17 +103,17 @@ nil)))) (s/def ::enabled? ::us/boolean) -(s/def ::host ::cf/ldap-host) -(s/def ::port ::cf/ldap-port) -(s/def ::ssl ::cf/ldap-ssl) -(s/def ::tls ::cf/ldap-starttls) -(s/def ::query ::cf/ldap-user-query) -(s/def ::base-dn ::cf/ldap-base-dn) -(s/def ::bind-dn ::cf/ldap-bind-dn) -(s/def ::bind-password ::cf/ldap-bind-password) -(s/def ::attrs-email ::cf/ldap-attrs-email) -(s/def ::attrs-fullname ::cf/ldap-attrs-fullname) -(s/def ::attrs-username ::cf/ldap-attrs-username) +(s/def ::host ::us/string) +(s/def ::port ::us/integer) +(s/def ::ssl ::us/boolean) +(s/def ::tls ::us/boolean) +(s/def ::query ::us/string) +(s/def ::base-dn ::us/string) +(s/def ::bind-dn ::us/string) +(s/def ::bind-password ::us/string) +(s/def ::attrs-email ::us/string) +(s/def ::attrs-fullname ::us/string) +(s/def ::attrs-username ::us/string) (s/def ::provider-params (s/keys :opt-un [::host ::port @@ -126,6 +125,7 @@ ::attrs-email ::attrs-username ::attrs-fullname])) + (s/def ::provider (s/nilable ::provider-params)) diff --git a/backend/src/app/auth/oidc.clj b/backend/src/app/auth/oidc.clj index 243c08da5f99be112a206cdd9a9a8ee04e0c7154..2ac5ab8308d3d699487ac870f4e42f515f18b54b 100644 --- a/backend/src/app/auth/oidc.clj +++ b/backend/src/app/auth/oidc.clj @@ -7,7 +7,6 @@ (ns app.auth.oidc "OIDC client implementation." (:require - [app.auth :as auth] [app.auth.oidc.providers :as-alias providers] [app.common.data :as d] [app.common.data.macros :as dm] @@ -17,12 +16,17 @@ [app.common.uri :as u] [app.config :as cf] [app.db :as db] + [app.email.blacklist :as email.blacklist] + [app.email.whitelist :as email.whitelist] [app.http.client :as http] + [app.http.errors :as errors] [app.http.session :as session] [app.loggers.audit :as audit] - [app.main :as-alias main] + [app.rpc :as rpc] [app.rpc.commands.profile :as profile] + [app.setup :as-alias setup] [app.tokens :as tokens] + [app.util.inet :as inet] [app.util.json :as json] [app.util.time :as dt] [buddy.sign.jwk :as jwk] @@ -31,13 +35,14 @@ [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig] + [ring.request :as rreq] [ring.response :as-alias rres])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- obfuscate-string +(defn obfuscate-string [s] (if (< (count s) 10) (apply str (take (count s) (repeat "*"))) @@ -128,8 +133,8 @@ (-> body json/decode :keys process-oidc-jwks) (do (l/warn :hint "unable to retrieve JWKs (unexpected response status code)" - :http-status status - :http-body body) + :response-status status + :response-body body) nil))) (catch Throwable cause (l/warn :hint "unable to retrieve JWKs (unexpected exception)" @@ -143,18 +148,18 @@ (when (contains? cf/flags :login-with-oidc) (if-let [opts (prepare-oidc-opts cfg)] (let [jwks (fetch-oidc-jwks cfg opts)] - (l/info :hint "provider initialized" - :provider "oidc" - :method (if (:discover? opts) "discover" "manual") - :client-id (:client-id opts) - :client-secret (obfuscate-string (:client-secret opts)) - :scopes (str/join "," (:scopes opts)) - :auth-uri (:auth-uri opts) - :user-uri (:user-uri opts) - :token-uri (:token-uri opts) - :roles-attr (:roles-attr opts) - :roles (:roles opts) - :keys (str/join "," (map str (keys jwks)))) + (l/inf :hint "provider initialized" + :provider "oidc" + :method (if (:discover? opts) "discover" "manual") + :client-id (:client-id opts) + :client-secret (obfuscate-string (:client-secret opts)) + :scopes (str/join "," (:scopes opts)) + :auth-uri (:auth-uri opts) + :user-uri (:user-uri opts) + :token-uri (:token-uri opts) + :roles-attr (:roles-attr opts) + :roles (:roles opts) + :keys (str/join "," (map str (keys jwks)))) (assoc opts :jwks jwks)) (do (l/warn :hint "unable to initialize auth provider, missing configuration" :provider "oidc") @@ -178,10 +183,10 @@ (if (and (string? (:client-id opts)) (string? (:client-secret opts))) (do - (l/info :hint "provider initialized" - :provider "google" - :client-id (:client-id opts) - :client-secret (obfuscate-string (:client-secret opts))) + (l/inf :hint "provider initialized" + :provider "google" + :client-id (:client-id opts) + :client-secret (obfuscate-string (:client-secret opts))) opts) (do @@ -206,8 +211,9 @@ (ex/raise :type :internal :code :unable-to-retrieve-github-emails :hint "unable to retrieve github emails" - :http-status status - :http-body body)) + :request-uri (:uri params) + :response-status status + :response-body body)) (->> body json/decode (filter :primary) first :email)))) @@ -232,10 +238,10 @@ (if (and (string? (:client-id opts)) (string? (:client-secret opts))) (do - (l/info :hint "provider initialized" - :provider "github" - :client-id (:client-id opts) - :client-secret (obfuscate-string (:client-secret opts))) + (l/inf :hint "provider initialized" + :provider "github" + :client-id (:client-id opts) + :client-secret (obfuscate-string (:client-secret opts))) opts) (do @@ -247,7 +253,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defmethod ig/init-key ::providers/gitlab - [_ _] + [_ cfg] (let [base (cf/get :gitlab-base-uri "https://gitlab.com") opts {:base-uri base :client-id (cf/get :gitlab-client-id) @@ -256,17 +262,18 @@ :auth-uri (str base "/oauth/authorize") :token-uri (str base "/oauth/token") :user-uri (str base "/oauth/userinfo") + :jwks-uri (str base "/oauth/discovery/keys") :name "gitlab"}] (when (contains? cf/flags :login-with-gitlab) (if (and (string? (:client-id opts)) (string? (:client-secret opts))) - (do - (l/info :hint "provider initialized" - :provider "gitlab" - :base-uri base - :client-id (:client-id opts) - :client-secret (obfuscate-string (:client-secret opts))) - opts) + (let [jwks (fetch-oidc-jwks cfg opts)] + (l/inf :hint "provider initialized" + :provider "gitlab" + :base-uri base + :client-id (:client-id opts) + :client-secret (obfuscate-string (:client-secret opts))) + (assoc opts :jwks jwks)) (do (l/warn :hint "unable to initialize auth provider, missing configuration" :provider "gitlab") @@ -282,12 +289,12 @@ (into [(keyword (:name provider) fitem)] (map keyword) items))) (defn- build-redirect-uri - [{:keys [provider] :as cfg}] + [{:keys [::provider] :as cfg}] (let [public (u/uri (cf/get :public-uri))] (str (assoc public :path (str "/api/auth/oauth/" (:name provider) "/callback"))))) (defn- build-auth-uri - [{:keys [provider] :as cfg} state] + [{:keys [::provider] :as cfg} state] (let [params {:client_id (:client-id provider) :redirect_uri (build-redirect-uri cfg) :response_type "code" @@ -298,15 +305,19 @@ (assoc :query query) (str)))) +(defn- qualify-prop-key + [provider k] + (keyword (:name provider) (name k))) + (defn- qualify-props [provider props] (reduce-kv (fn [result k v] - (assoc result (keyword (:name provider) (name k)) v)) + (assoc result (qualify-prop-key provider k) v)) {} props)) -(defn fetch-access-token - [{:keys [provider] :as cfg} code] +(defn- fetch-access-token + [{:keys [::provider] :as cfg} code] (let [params {:client_id (:client-id provider) :client_secret (:client-secret provider) :code code @@ -318,26 +329,31 @@ :uri (:token-uri provider) :body (u/map->query-string params)}] - (l/trace :hint "request access token" - :provider (:name provider) - :client-id (:client-id provider) - :client-secret (obfuscate-string (:client-secret provider)) - :grant-type (:grant_type params) - :redirect-uri (:redirect_uri params)) + (l/trc :hint "fetch access token" + :provider (:name provider) + :client-id (:client-id provider) + :client-secret (obfuscate-string (:client-secret provider)) + :grant-type (:grant_type params) + :redirect-uri (:redirect_uri params)) (let [{:keys [status body]} (http/req! cfg req {:sync? true})] - (l/trace :hint "access token response" :status status :body body) + (l/trc :hint "access token fetched" :status status :body body) (if (= status 200) - (let [data (json/decode body)] - {:token/access (get data :access_token) - :token/id (get data :id_token) - :token/type (get data :token_type)}) - + (let [data (json/decode body) + data {:token/access (get data :access_token) + :token/id (get data :id_token) + :token/type (get data :token_type)}] + (l/trc :hint "access token fetched" + :token-id (:token/id data) + :token-type (:token/type data) + :token (:token/access data)) + data) (ex/raise :type :internal - :code :unable-to-retrieve-token - :hint "unable to retrieve token" - :http-status status - :http-body body))))) + :code :unable-to-fetch-access-token + :hint "unable to fetch access token" + :request-uri (:uri req) + :response-status status + :response-body body))))) (defn- process-user-info [provider tdata info] @@ -363,10 +379,10 @@ :props props}))) (defn- fetch-user-info - [{:keys [provider] :as cfg} tdata] - (l/trace :hint "fetch user info" - :uri (:user-uri provider) - :token (obfuscate-string (:token/access tdata))) + [{:keys [::provider] :as cfg} tdata] + (l/trc :hint "fetch user info" + :uri (:user-uri provider) + :token (obfuscate-string (:token/access tdata))) (let [params {:uri (:user-uri provider) :headers {"Authorization" (str (:token/type tdata) " " (:token/access tdata))} @@ -374,9 +390,9 @@ :method :get} response (http/req! cfg params {:sync? true})] - (l/trace :hint "user info response" - :status (:status response) - :body (:body response)) + (l/trc :hint "user info response" + :status (:status response) + :body (:body response)) (when-not (s/int-in-range? 200 300 (:status response)) (ex/raise :type :internal @@ -388,7 +404,7 @@ (-> response :body json/decode))) (defn- get-user-info - [{:keys [provider]} tdata] + [{:keys [::provider]} tdata] (try (when (:token/id tdata) (let [{:keys [kid alg] :as theader} (jwt/decode-header (:token/id tdata))] @@ -412,14 +428,8 @@ ::fullname ::props])) -(defn get-info - [{:keys [provider ::main/props] :as cfg} {:keys [params] :as request}] - (when-let [error (get params :error)] - (ex/raise :type :internal - :code :error-on-retrieving-code - :error-id error - :error-desc (get params :error_description))) - +(defn- get-info + [{:keys [::provider ::setup/props] :as cfg} {:keys [params] :as request}] (let [state (get params :state) code (get params :code) state (tokens/verify props {:token state :iss :oauth}) @@ -432,7 +442,7 @@ info (process-user-info provider tdata info)] - (l/trace :hint "user info" :info info) + (l/trc :hint "user info" :info info) (when-not (s/valid? ::info info) (l/warn :hint "received incomplete profile info object (please set correct scopes)" :info info) @@ -465,110 +475,173 @@ (some? (:invitation-token state)) (assoc :invitation-token (:invitation-token state)) + (some? (:external-session-id state)) + (assoc :external-session-id (:external-session-id state)) + ;; If state token comes with props, merge them. The state token ;; props can contain pm_ and utm_ prefixed query params. (map? (:props state)) (update :props merge (:props state))))) (defn- get-profile - [{:keys [::db/pool] :as cfg} info] - (dm/with-open [conn (db/open pool)] - (some->> (:email info) - (profile/clean-email) - (profile/get-profile-by-email conn)))) + [cfg info] + (db/run! cfg (fn [{:keys [::db/conn]}] + (some->> (:email info) + (profile/clean-email) + (profile/get-profile-by-email conn))))) (defn- redirect-response [uri] {::rres/status 302 ::rres/headers {"location" (str uri)}}) -(defn- generate-error-redirect - [_ cause] - (let [data (if (ex/error? cause) (ex-data cause) nil) - code (or (:code data) :unexpected) - type (or (:type data) :internal) - hint (or (:hint data) - (if (ex/exception? cause) - (ex-message cause) - (str cause))) - - params {:error "unable-to-auth" - :hint hint - :type type - :code code} - +(defn- redirect-with-error + ([error] (redirect-with-error error nil)) + ([error hint] + (let [params {:error error :hint hint} + params (d/without-nils params) + uri (-> (u/uri (cf/get :public-uri)) + (assoc :path "/#/auth/login") + (assoc :query (u/map->query-string params)))] + (redirect-response uri)))) + +(defn- redirect-to-register + [cfg info request] + (let [info (assoc info + :iss :prepared-register + :exp (dt/in-future {:hours 48})) + + params {:token (tokens/generate (::setup/props cfg) info) + :provider (:provider (:path-params request)) + :fullname (:fullname info)} + params (d/without-nils params)] + + (redirect-response + (-> (u/uri (cf/get :public-uri)) + (assoc :path "/#/auth/register/validate") + (assoc :query (u/map->query-string params)))))) + +(defn- redirect-to-verify-token + [token] + (let [params {:token token} uri (-> (u/uri (cf/get :public-uri)) - (assoc :path "/#/auth/login") + (assoc :path "/#/auth/verify-token") (assoc :query (u/map->query-string params)))] (redirect-response uri))) -(defn- generate-redirect - [cfg request info profile] - (if profile - (let [sxf (session/create-fn cfg (:id profile)) - token (or (:invitation-token info) - (tokens/generate (::main/props cfg) - {:iss :auth - :exp (dt/in-future "15m") - :profile-id (:id profile)})) - params {:token token} - uri (-> (u/uri (cf/get :public-uri)) - (assoc :path "/#/auth/verify-token") - (assoc :query (u/map->query-string params)))] - - (when (:is-blocked profile) - (ex/raise :type :restriction - :code :profile-blocked)) - - (audit/submit! cfg {::audit/type "command" - ::audit/name "login-with-oidc" - ::audit/profile-id (:id profile) - ::audit/ip-addr (audit/parse-client-ip request) - ::audit/props (audit/profile->props profile)}) - - (->> (redirect-response uri) - (sxf request))) - - - (if (auth/email-domain-in-whitelist? (:email info)) - (let [info (assoc info - :iss :prepared-register - :is-active true - :exp (dt/in-future {:hours 48})) - token (tokens/generate (::main/props cfg) info) - params (d/without-nils - {:token token - :fullname (:fullname info)}) - uri (-> (u/uri (cf/get :public-uri)) - (assoc :path "/#/auth/register/validate") - (assoc :query (u/map->query-string params)))] - - (redirect-response uri)) - (generate-error-redirect cfg "email-domain-not-allowed")))) +(defn- provider-has-email-verified? + [{:keys [::provider] :as cfg} {:keys [props] :as info}] + (let [prop (qualify-prop-key provider :email_verified)] + (true? (get props prop)))) + +(defn- profile-has-provider-props? + [{:keys [::provider] :as cfg} profile] + (let [prop (qualify-prop-key provider :email)] + (contains? (:props profile) prop))) + +(defn- provider-matches-profile? + [{:keys [::provider] :as cfg} profile info] + (or (= (:auth-backend profile) (:name provider)) + (profile-has-provider-props? cfg profile) + (provider-has-email-verified? cfg info))) +(defn- process-callback + [cfg request info profile] + (cond + (some? profile) + (cond + (:is-blocked profile) + (redirect-with-error "profile-blocked") + + (not (provider-matches-profile? cfg profile info)) + (redirect-with-error "auth-provider-not-allowed") + + (not (:is-active profile)) + (let [info (assoc info :profile-id (:id profile))] + (redirect-to-register cfg info request)) + + :else + (let [sxf (session/create-fn cfg (:id profile)) + token (or (:invitation-token info) + (tokens/generate (::setup/props cfg) + {:iss :auth + :exp (dt/in-future "15m") + :props (:props info) + :profile-id (:id profile)})) + props (audit/profile->props profile) + context (d/without-nils {:external-session-id (:external-session-id info)})] + + (audit/submit! cfg {::audit/type "action" + ::audit/name "login-with-oidc" + ::audit/profile-id (:id profile) + ::audit/ip-addr (inet/parse-request request) + ::audit/props props + ::audit/context context}) + + (->> (redirect-to-verify-token token) + (sxf request)))) + + (and (email.blacklist/enabled? cfg) + (email.blacklist/contains? cfg (:email info))) + (redirect-with-error "email-domain-not-allowed") + + (and (email.whitelist/enabled? cfg) + (not (email.whitelist/contains? cfg (:email info)))) + (redirect-with-error "email-domain-not-allowed") + + :else + (let [info (assoc info :is-active (provider-has-email-verified? cfg info))] + (if (or (contains? cf/flags :registration) + (contains? cf/flags :oidc-registration)) + (redirect-to-register cfg info request) + (redirect-with-error "registration-disabled"))))) + +(defn- get-external-session-id + [request] + (let [session-id (rreq/get-header request "x-external-session-id")] + (when (string? session-id) + (if (or (> (count session-id) 256) + (= session-id "null") + (str/blank? session-id)) + nil + session-id)))) (defn- auth-handler [cfg {:keys [params] :as request}] - (let [props (audit/extract-utm-params params) - state (tokens/generate (::main/props cfg) - {:iss :oauth - :invitation-token (:invitation-token params) - :props props - :exp (dt/in-future "4h")}) - uri (build-auth-uri cfg state)] + (let [props (audit/extract-utm-params params) + esid (rpc/get-external-session-id request) + params {:iss :oauth + :invitation-token (:invitation-token params) + :external-session-id esid + :props props + :exp (dt/in-future "4h")} + state (tokens/generate (::setup/props cfg) + (d/without-nils params)) + uri (build-auth-uri cfg state)] {::rres/status 200 ::rres/body {:redirect-uri uri}})) (defn- callback-handler - [cfg request] + [{:keys [::provider] :as cfg} request] (try - (let [info (get-info cfg request) - profile (get-profile cfg info)] - (generate-redirect cfg request info profile)) + (if-let [error (dm/get-in request [:params :error])] + (redirect-with-error "unable-to-auth" error) + (let [info (get-info cfg request) + profile (get-profile cfg info)] + (process-callback cfg request info profile))) (catch Throwable cause - (l/warn :hint "error on oauth process" :cause cause) - (generate-error-redirect cfg cause)))) + (binding [l/*context* (-> (errors/request->context request) + (assoc :auth/provider (:name provider)))] + (let [edata (ex-data cause)] + (cond + (= :validation (:type edata)) + (l/wrn :hint "invalid token received" :cause cause) + + :else + (l/err :hint "error on oauth process" :cause cause)))) + + (redirect-with-error "unable-to-auth" (ex-message cause))))) (def provider-lookup {:compile @@ -577,26 +650,24 @@ (fn [request] (let [provider (some-> request :path-params :provider keyword)] (if-let [provider (get providers provider)] - (handler (assoc cfg :provider provider) request) + (handler (assoc cfg ::provider provider) request) (ex/raise :type :restriction :code :provider-not-configured :provider provider :hint "provider not configured"))))))}) +(s/def ::client-id ::us/string) +(s/def ::client-secret ::us/string) +(s/def ::base-uri ::us/string) +(s/def ::token-uri ::us/string) +(s/def ::auth-uri ::us/string) +(s/def ::user-uri ::us/string) +(s/def ::scopes ::us/set-of-strings) +(s/def ::roles ::us/set-of-strings) +(s/def ::roles-attr ::us/string) +(s/def ::email-attr ::us/string) +(s/def ::name-attr ::us/string) -(s/def ::client-id ::cf/oidc-client-id) -(s/def ::client-secret ::cf/oidc-client-secret) -(s/def ::base-uri ::cf/oidc-base-uri) -(s/def ::token-uri ::cf/oidc-token-uri) -(s/def ::auth-uri ::cf/oidc-auth-uri) -(s/def ::user-uri ::cf/oidc-user-uri) -(s/def ::scopes ::cf/oidc-scopes) -(s/def ::roles ::cf/oidc-roles) -(s/def ::roles-attr ::cf/oidc-roles-attr) -(s/def ::email-attr ::cf/oidc-email-attr) -(s/def ::name-attr ::cf/oidc-name-attr) - -;; FIXME: migrate to qualified-keywords (s/def ::provider (s/keys :req-un [::client-id ::client-secret] @@ -618,7 +689,7 @@ [_] (s/keys :req [::session/manager ::http/client - ::main/props + ::setup/props ::db/pool ::providers])) diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index ace98c80ec39fd56607057f46f90a614f2e76b60..d8c381174214f066cce1fb8ed9ff0a051268659f 100644 --- a/backend/src/app/binfile/common.clj +++ b/backend/src/app/binfile/common.clj @@ -15,6 +15,7 @@ [app.common.files.migrations :as fmg] [app.common.files.validate :as fval] [app.common.logging :as l] + [app.common.types.file :as ctf] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -331,54 +332,12 @@ (defn embed-assets [cfg data file-id] - (letfn [(walk-map-form [form state] - (cond - (uuid? (:fill-color-ref-file form)) - (do - (vswap! state conj [(:fill-color-ref-file form) :colors (:fill-color-ref-id form)]) - (assoc form :fill-color-ref-file file-id)) - - (uuid? (:stroke-color-ref-file form)) - (do - (vswap! state conj [(:stroke-color-ref-file form) :colors (:stroke-color-ref-id form)]) - (assoc form :stroke-color-ref-file file-id)) - - (uuid? (:typography-ref-file form)) - (do - (vswap! state conj [(:typography-ref-file form) :typographies (:typography-ref-id form)]) - (assoc form :typography-ref-file file-id)) - - (uuid? (:component-file form)) - (do - (vswap! state conj [(:component-file form) :components (:component-id form)]) - (assoc form :component-file file-id)) - - :else - form)) - - (process-group-of-assets [data [lib-id items]] - ;; NOTE: there is a possibility that shape refers to an - ;; non-existant file because the file was removed. In this - ;; case we just ignore the asset. - (if-let [lib (get-file cfg lib-id)] - (reduce (partial process-asset lib) data items) - data)) - - (process-asset [lib data [bucket asset-id]] - (let [asset (get-in lib [:data bucket asset-id]) - ;; Add a special case for colors that need to have - ;; correctly set the :file-id prop (pending of the - ;; refactor that will remove it). - asset (cond-> asset - (= bucket :colors) (assoc :file-id file-id))] - (update data bucket assoc asset-id asset)))] - - (let [assets (volatile! [])] - (walk/postwalk #(cond-> % (map? %) (walk-map-form assets)) data) - (->> (deref assets) - (filter #(as-> (first %) $ (and (uuid? $) (not= $ file-id)))) - (d/group-by first rest) - (reduce (partial process-group-of-assets) data))))) + (let [library-ids (get-libraries cfg [file-id])] + (reduce (fn [data library-id] + (let [library (get-file cfg library-id)] + (ctf/absorb-assets data (:data library)))) + data + library-ids))) (defn- fix-version [file] @@ -479,7 +438,9 @@ (doseq [[feature file-id] (-> *state* deref :pending-to-migrate)] (case feature "components/v2" - (feat.compv2/migrate-file! cfg file-id :validate? (::validate cfg true)) + (feat.compv2/migrate-file! cfg file-id + :validate? (::validate cfg true) + :skip-on-graphic-error? true) "fdata/shape-data-type" nil diff --git a/backend/src/app/binfile/v1.clj b/backend/src/app/binfile/v1.clj index d2b7cdf7f23f9c69a0feca79a3783c10a8aea863..87f02d391afdc59e943bac83b653032e489f51b0 100644 --- a/backend/src/app/binfile/v1.clj +++ b/backend/src/app/binfile/v1.clj @@ -22,7 +22,6 @@ [app.db :as db] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] - [app.media :as media] [app.rpc :as-alias rpc] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] @@ -130,7 +129,6 @@ (.writeLong output (long data)) (swap! *position* + 8)) - (defn read-long! [^DataInputStream input] (let [v (.readLong input)] @@ -404,9 +402,9 @@ (write-obj! output rels))) (defmethod write-section :v1/sobjects - [{:keys [::sto/storage ::output]}] + [{:keys [::output] :as cfg}] (let [sids (-> bfc/*state* deref :sids) - storage (media/configure-assets-storage storage)] + storage (sto/resolve cfg)] (l/dbg :hint "found sobjects" :items (count sids) @@ -468,7 +466,7 @@ (defn- read-import-v1 [{:keys [::db/conn ::project-id ::profile-id ::input] :as cfg}] - (db/exec-one! conn ["SET idle_in_transaction_session_timeout = 0"]) + (db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"]) (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) (pu/with-open [input (zstd-input-stream input) @@ -621,8 +619,8 @@ ::l/sync? true)))))) (defmethod read-section :v1/sobjects - [{:keys [::sto/storage ::db/conn ::input ::bfc/overwrite ::bfc/timestamp]}] - (let [storage (media/configure-assets-storage storage) + [{:keys [::db/conn ::input ::bfc/overwrite ::bfc/timestamp] :as cfg}] + (let [storage (sto/resolve cfg) ids (read-obj! input) thumb? (into #{} (map :media-id) (:thumbnails @bfc/*state*))] diff --git a/backend/src/app/binfile/v2.clj b/backend/src/app/binfile/v2.clj index 1a5f103425cbbaa190bb5be91c8e09aad5b79851..bef327acc1724ebdb9c3b4a3f8bdffd5312d21d6 100644 --- a/backend/src/app/binfile/v2.clj +++ b/backend/src/app/binfile/v2.clj @@ -20,7 +20,6 @@ [app.db.sql :as sql] [app.loggers.audit :as-alias audit] [app.loggers.webhooks :as-alias webhooks] - [app.media :as media] [app.storage :as sto] [app.storage.tmp :as tmp] [app.util.events :as events] @@ -347,9 +346,7 @@ [cfg team-id] (let [id (uuid/next) tp (dt/tpoint) - - cfg (-> (create-database cfg) - (update ::sto/storage media/configure-assets-storage))] + cfg (create-database cfg)] (l/inf :hint "start" :operation "export" @@ -390,7 +387,6 @@ tp (dt/tpoint) cfg (-> (create-database cfg path) - (update ::sto/storage media/configure-assets-storage) (assoc ::bfc/timestamp (dt/now)))] (l/inf :hint "start" diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index a9e883b8ff1ea9afc163f4568677f034e0311a19..a06f52950cafcbd0218d6f074514b893581c2b36 100644 --- a/backend/src/app/config.clj +++ b/backend/src/app/config.clj @@ -11,30 +11,17 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.flags :as flags] - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.version :as v] + [app.util.overrides] [app.util.time :as dt] [clojure.core :as c] [clojure.java.io :as io] - [clojure.pprint :as pprint] - [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.fs :as fs] [environ.core :refer [env]] [integrant.core :as ig])) -(prefer-method print-method - clojure.lang.IRecord - clojure.lang.IDeref) - -(prefer-method print-method - clojure.lang.IPersistentMap - clojure.lang.IDeref) - -(prefer-method pprint/simple-dispatch - clojure.lang.IPersistentMap - clojure.lang.IDeref) - (defmethod ig/init-key :default [_ data] (d/without-nils data)) @@ -45,18 +32,19 @@ (d/without-nils data) data)) -(def defaults +(def default {:database-uri "postgresql://postgres/penpot" :database-username "penpot" :database-password "penpot" - :default-blob-version 5 + :default-blob-version 4 - :rpc-rlimit-config (fs/path "resources/rlimit.edn") - :rpc-climit-config (fs/path "resources/climit.edn") + :rpc-rlimit-config "resources/rlimit.edn" + :rpc-climit-config "resources/climit.edn" - :file-change-snapshot-every 5 - :file-change-snapshot-timeout "3h" + :auto-file-snapshot-total 10 + :auto-file-snapshot-every 5 + :auto-file-snapshot-timeout "3h" :public-uri "http://localhost:3449" :host "localhost" @@ -64,8 +52,8 @@ :redis-uri "redis://redis/0" - :assets-storage-backend :assets-fs - :storage-assets-fs-directory "assets" + :objects-storage-backend "fs" + :objects-storage-fs-directory "assets" :assets-path "/internal/assets/" :smtp-default-reply-to "Penpot " @@ -87,255 +75,164 @@ :ldap-attrs-fullname "cn" ;; a server prop key where initial project is stored. - :initial-project-skey "initial-project"}) - -(s/def ::default-rpc-rlimit ::us/vector-of-strings) -(s/def ::rpc-rlimit-config ::fs/path) -(s/def ::rpc-climit-config ::fs/path) - -(s/def ::media-max-file-size ::us/integer) - -(s/def ::flags ::us/vector-of-keywords) -(s/def ::telemetry-enabled ::us/boolean) - -(s/def ::audit-log-archive-uri ::us/string) -(s/def ::audit-log-http-handler-concurrency ::us/integer) - -(s/def ::admins ::us/set-of-valid-emails) -(s/def ::file-change-snapshot-every ::us/integer) -(s/def ::file-change-snapshot-timeout ::dt/duration) - -(s/def ::default-executor-parallelism ::us/integer) -(s/def ::scheduled-executor-parallelism ::us/integer) - -(s/def ::worker-default-parallelism ::us/integer) -(s/def ::worker-webhook-parallelism ::us/integer) - -(s/def ::authenticated-cookie-domain ::us/string) -(s/def ::authenticated-cookie-name ::us/string) -(s/def ::auth-token-cookie-name ::us/string) -(s/def ::auth-token-cookie-max-age ::dt/duration) - -(s/def ::secret-key ::us/string) -(s/def ::allow-demo-users ::us/boolean) -(s/def ::assets-path ::us/string) -(s/def ::database-password (s/nilable ::us/string)) -(s/def ::database-uri ::us/string) -(s/def ::database-username (s/nilable ::us/string)) -(s/def ::database-readonly ::us/boolean) -(s/def ::database-min-pool-size ::us/integer) -(s/def ::database-max-pool-size ::us/integer) - -(s/def ::quotes-teams-per-profile ::us/integer) -(s/def ::quotes-access-tokens-per-profile ::us/integer) -(s/def ::quotes-projects-per-team ::us/integer) -(s/def ::quotes-invitations-per-team ::us/integer) -(s/def ::quotes-profiles-per-team ::us/integer) -(s/def ::quotes-files-per-project ::us/integer) -(s/def ::quotes-files-per-team ::us/integer) -(s/def ::quotes-font-variants-per-team ::us/integer) -(s/def ::quotes-comment-threads-per-file ::us/integer) -(s/def ::quotes-comments-per-file ::us/integer) - -(s/def ::default-blob-version ::us/integer) -(s/def ::error-report-webhook ::us/string) -(s/def ::user-feedback-destination ::us/string) -(s/def ::github-client-id ::us/string) -(s/def ::github-client-secret ::us/string) -(s/def ::gitlab-base-uri ::us/string) -(s/def ::gitlab-client-id ::us/string) -(s/def ::gitlab-client-secret ::us/string) -(s/def ::google-client-id ::us/string) -(s/def ::google-client-secret ::us/string) -(s/def ::oidc-client-id ::us/string) -(s/def ::oidc-user-info-source ::us/keyword) -(s/def ::oidc-client-secret ::us/string) -(s/def ::oidc-base-uri ::us/string) -(s/def ::oidc-token-uri ::us/string) -(s/def ::oidc-auth-uri ::us/string) -(s/def ::oidc-user-uri ::us/string) -(s/def ::oidc-jwks-uri ::us/string) -(s/def ::oidc-scopes ::us/set-of-strings) -(s/def ::oidc-roles ::us/set-of-strings) -(s/def ::oidc-roles-attr ::us/string) -(s/def ::oidc-email-attr ::us/string) -(s/def ::oidc-name-attr ::us/string) -(s/def ::host ::us/string) -(s/def ::http-server-port ::us/integer) -(s/def ::http-server-host ::us/string) -(s/def ::http-server-max-body-size ::us/integer) -(s/def ::http-server-max-multipart-body-size ::us/integer) -(s/def ::http-server-io-threads ::us/integer) -(s/def ::http-server-worker-threads ::us/integer) -(s/def ::ldap-attrs-email ::us/string) -(s/def ::ldap-attrs-fullname ::us/string) -(s/def ::ldap-attrs-username ::us/string) -(s/def ::ldap-base-dn ::us/string) -(s/def ::ldap-bind-dn ::us/string) -(s/def ::ldap-bind-password ::us/string) -(s/def ::ldap-host ::us/string) -(s/def ::ldap-port ::us/integer) -(s/def ::ldap-ssl ::us/boolean) -(s/def ::ldap-starttls ::us/boolean) -(s/def ::ldap-user-query ::us/string) -(s/def ::media-directory ::us/string) -(s/def ::media-uri ::us/string) -(s/def ::profile-bounce-max-age ::dt/duration) -(s/def ::profile-bounce-threshold ::us/integer) -(s/def ::profile-complaint-max-age ::dt/duration) -(s/def ::profile-complaint-threshold ::us/integer) -(s/def ::public-uri ::us/string) -(s/def ::redis-uri ::us/string) -(s/def ::registration-domain-whitelist ::us/set-of-strings) - -(s/def ::smtp-default-from ::us/string) -(s/def ::smtp-default-reply-to ::us/string) -(s/def ::smtp-host ::us/string) -(s/def ::smtp-password (s/nilable ::us/string)) -(s/def ::smtp-port ::us/integer) -(s/def ::smtp-ssl ::us/boolean) -(s/def ::smtp-tls ::us/boolean) -(s/def ::smtp-username (s/nilable ::us/string)) -(s/def ::urepl-host ::us/string) -(s/def ::urepl-port ::us/integer) -(s/def ::prepl-host ::us/string) -(s/def ::prepl-port ::us/integer) -(s/def ::assets-storage-backend ::us/keyword) -(s/def ::storage-assets-fs-directory ::us/string) -(s/def ::storage-assets-s3-bucket ::us/string) -(s/def ::storage-assets-s3-region ::us/keyword) -(s/def ::storage-assets-s3-endpoint ::us/string) -(s/def ::storage-assets-s3-io-threads ::us/integer) -(s/def ::telemetry-uri ::us/string) -(s/def ::telemetry-with-taiga ::us/boolean) -(s/def ::tenant ::us/string) - -(s/def ::config - (s/keys :opt-un [::secret-key - ::flags - ::admins - ::allow-demo-users - ::audit-log-archive-uri - ::audit-log-http-handler-concurrency - ::auth-token-cookie-name - ::auth-token-cookie-max-age - ::authenticated-cookie-name - ::authenticated-cookie-domain - ::database-password - ::database-uri - ::database-username - ::database-readonly - ::database-min-pool-size - ::database-max-pool-size - ::default-blob-version - ::default-rpc-rlimit - ::error-report-webhook - ::default-executor-parallelism - ::scheduled-executor-parallelism - ::worker-default-parallelism - ::worker-webhook-parallelism - ::file-change-snapshot-every - ::file-change-snapshot-timeout - ::user-feedback-destination - ::github-client-id - ::github-client-secret - ::gitlab-base-uri - ::gitlab-client-id - ::gitlab-client-secret - ::google-client-id - ::google-client-secret - ::oidc-client-id - ::oidc-client-secret - ::oidc-user-info-source - ::oidc-base-uri - ::oidc-token-uri - ::oidc-auth-uri - ::oidc-user-uri - ::oidc-jwks-uri - ::oidc-scopes - ::oidc-roles-attr - ::oidc-email-attr - ::oidc-name-attr - ::oidc-roles - ::host - ::http-server-host - ::http-server-port - ::http-server-max-body-size - ::http-server-max-multipart-body-size - ::http-server-io-threads - ::http-server-worker-threads - ::ldap-attrs-email - ::ldap-attrs-fullname - ::ldap-attrs-username - ::ldap-base-dn - ::ldap-bind-dn - ::ldap-bind-password - ::ldap-host - ::ldap-port - ::ldap-ssl - ::ldap-starttls - ::ldap-user-query - ::local-assets-uri - ::media-max-file-size - ::profile-bounce-max-age - ::profile-bounce-threshold - ::profile-complaint-max-age - ::profile-complaint-threshold - ::public-uri - - ::quotes-teams-per-profile - ::quotes-access-tokens-per-profile - ::quotes-projects-per-team - ::quotes-invitations-per-team - ::quotes-profiles-per-team - ::quotes-files-per-project - ::quotes-files-per-team - ::quotes-font-variants-per-team - ::quotes-comment-threads-per-file - ::quotes-comments-per-file - - ::redis-uri - ::registration-domain-whitelist - ::rpc-rlimit-config - ::rpc-climit-config - - ::semaphore-process-font - ::semaphore-process-image - ::semaphore-update-file - ::semaphore-auth - - ::smtp-default-from - ::smtp-default-reply-to - ::smtp-host - ::smtp-password - ::smtp-port - ::smtp-ssl - ::smtp-tls - ::smtp-username - - ::urepl-host - ::urepl-port - ::prepl-host - ::prepl-port - - ::assets-storage-backend - ::storage-assets-fs-directory - ::storage-assets-s3-bucket - ::storage-assets-s3-region - ::storage-assets-s3-endpoint - ::storage-assets-s3-io-threads - ::telemetry-enabled - ::telemetry-uri - ::telemetry-referer - ::telemetry-with-taiga - ::tenant])) + :initial-project-skey "initial-project" + + ;; time to avoid email sending after profile modification + :email-verify-threshold "15m"}) + +(def schema:config + (do #_sm/optional-keys + [:map {:title "config"} + [:flags {:optional true} [::sm/set :string]] + [:admins {:optional true} [::sm/set ::sm/email]] + [:secret-key {:optional true} :string] + + [:tenant {:optional false} :string] + [:public-uri {:optional false} :string] + [:host {:optional false} :string] + + [:http-server-port {:optional true} ::sm/int] + [:http-server-host {:optional true} :string] + [:http-server-max-body-size {:optional true} ::sm/int] + [:http-server-max-multipart-body-size {:optional true} ::sm/int] + [:http-server-io-threads {:optional true} ::sm/int] + [:http-server-worker-threads {:optional true} ::sm/int] + + [:telemetry-uri {:optional true} :string] + [:telemetry-with-taiga {:optional true} ::sm/boolean] ;; DELETE + + [:auto-file-snapshot-total {:optional true} ::sm/int] + [:auto-file-snapshot-every {:optional true} ::sm/int] + [:auto-file-snapshot-timeout {:optional true} ::dt/duration] + + [:media-max-file-size {:optional true} ::sm/int] + [:deletion-delay {:optional true} ::dt/duration] ;; REVIEW + [:telemetry-enabled {:optional true} ::sm/boolean] + [:default-blob-version {:optional true} ::sm/int] + [:allow-demo-users {:optional true} ::sm/boolean] + [:error-report-webhook {:optional true} :string] + [:user-feedback-destination {:optional true} :string] + + [:default-rpc-rlimit {:optional true} [::sm/vec :string]] + [:rpc-rlimit-config {:optional true} ::fs/path] + [:rpc-climit-config {:optional true} ::fs/path] + + [:audit-log-archive-uri {:optional true} :string] + [:audit-log-http-handler-concurrency {:optional true} ::sm/int] + + [:default-executor-parallelism {:optional true} ::sm/int] ;; REVIEW + [:scheduled-executor-parallelism {:optional true} ::sm/int] ;; REVIEW + [:worker-default-parallelism {:optional true} ::sm/int] + [:worker-webhook-parallelism {:optional true} ::sm/int] + + [:database-password {:optional true} [:maybe :string]] + [:database-uri {:optional true} :string] + [:database-username {:optional true} [:maybe :string]] + [:database-readonly {:optional true} ::sm/boolean] + [:database-min-pool-size {:optional true} ::sm/int] + [:database-max-pool-size {:optional true} ::sm/int] + + [:quotes-teams-per-profile {:optional true} ::sm/int] + [:quotes-access-tokens-per-profile {:optional true} ::sm/int] + [:quotes-projects-per-team {:optional true} ::sm/int] + [:quotes-invitations-per-team {:optional true} ::sm/int] + [:quotes-profiles-per-team {:optional true} ::sm/int] + [:quotes-files-per-project {:optional true} ::sm/int] + [:quotes-files-per-team {:optional true} ::sm/int] + [:quotes-font-variants-per-team {:optional true} ::sm/int] + [:quotes-comment-threads-per-file {:optional true} ::sm/int] + [:quotes-comments-per-file {:optional true} ::sm/int] + + [:auth-data-cookie-domain {:optional true} :string] + [:auth-token-cookie-name {:optional true} :string] + [:auth-token-cookie-max-age {:optional true} ::dt/duration] + + [:registration-domain-whitelist {:optional true} [::sm/set :string]] + [:email-verify-threshold {:optional true} ::dt/duration] + + [:github-client-id {:optional true} :string] + [:github-client-secret {:optional true} :string] + [:gitlab-base-uri {:optional true} :string] + [:gitlab-client-id {:optional true} :string] + [:gitlab-client-secret {:optional true} :string] + [:google-client-id {:optional true} :string] + [:google-client-secret {:optional true} :string] + [:oidc-client-id {:optional true} :string] + [:oidc-user-info-source {:optional true} :keyword] + [:oidc-client-secret {:optional true} :string] + [:oidc-base-uri {:optional true} :string] + [:oidc-token-uri {:optional true} :string] + [:oidc-auth-uri {:optional true} :string] + [:oidc-user-uri {:optional true} :string] + [:oidc-jwks-uri {:optional true} :string] + [:oidc-scopes {:optional true} [::sm/set :string]] + [:oidc-roles {:optional true} [::sm/set :string]] + [:oidc-roles-attr {:optional true} :string] + [:oidc-email-attr {:optional true} :string] + [:oidc-name-attr {:optional true} :string] + + [:ldap-attrs-email {:optional true} :string] + [:ldap-attrs-fullname {:optional true} :string] + [:ldap-attrs-username {:optional true} :string] + [:ldap-base-dn {:optional true} :string] + [:ldap-bind-dn {:optional true} :string] + [:ldap-bind-password {:optional true} :string] + [:ldap-host {:optional true} :string] + [:ldap-port {:optional true} ::sm/int] + [:ldap-ssl {:optional true} ::sm/boolean] + [:ldap-starttls {:optional true} ::sm/boolean] + [:ldap-user-query {:optional true} :string] + + [:profile-bounce-max-age {:optional true} ::dt/duration] + [:profile-bounce-threshold {:optional true} ::sm/int] + [:profile-complaint-max-age {:optional true} ::dt/duration] + [:profile-complaint-threshold {:optional true} ::sm/int] + + [:redis-uri {:optional true} :string] + + [:email-domain-blacklist {:optional true} ::fs/path] + [:email-domain-whitelist {:optional true} ::fs/path] + + [:smtp-default-from {:optional true} :string] + [:smtp-default-reply-to {:optional true} :string] + [:smtp-host {:optional true} :string] + [:smtp-password {:optional true} [:maybe :string]] + [:smtp-port {:optional true} ::sm/int] + [:smtp-ssl {:optional true} ::sm/boolean] + [:smtp-tls {:optional true} ::sm/boolean] + [:smtp-username {:optional true} [:maybe :string]] + + [:urepl-host {:optional true} :string] + [:urepl-port {:optional true} ::sm/int] + [:prepl-host {:optional true} :string] + [:prepl-port {:optional true} ::sm/int] + + [:media-directory {:optional true} :string] ;; REVIEW + [:media-uri {:optional true} :string] + [:assets-path {:optional true} :string] + + ;; Legacy, will be removed in 2.5 + [:assets-storage-backend {:optional true} :keyword] + [:storage-assets-fs-directory {:optional true} :string] + [:storage-assets-s3-bucket {:optional true} :string] + [:storage-assets-s3-region {:optional true} :keyword] + [:storage-assets-s3-endpoint {:optional true} :string] + [:storage-assets-s3-io-threads {:optional true} ::sm/int] + + [:objects-storage-backend {:optional true} :keyword] + [:objects-storage-fs-directory {:optional true} :string] + [:objects-storage-s3-bucket {:optional true} :string] + [:objects-storage-s3-region {:optional true} :keyword] + [:objects-storage-s3-endpoint {:optional true} :string] + [:objects-storage-s3-io-threads {:optional true} ::sm/int]])) (def default-flags [:enable-backend-api-doc :enable-backend-openapi-doc :enable-backend-worker :enable-secure-session-cookies - :enable-email-verification]) + :enable-email-verification + :enable-v2-migration]) (defn- parse-flags [config] @@ -355,20 +252,22 @@ {} env))) -(defn- read-config - [] - (try - (->> (read-env "penpot") - (merge defaults) - (us/conform ::config)) - (catch Throwable e - (when (ex/error? e) - (println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;") - (println "Error on validating configuration:") - (println (some-> e ex-data ex/explain)) - (println (ex/explain (ex-data e))) - (println ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;")) - (throw e)))) +(def decode-config + (sm/decoder schema:config sm/string-transformer)) + +(def validate-config + (sm/validator schema:config)) + +(def explain-config + (sm/explainer schema:config)) + +(defn read-config + "Reads the configuration from enviroment variables and decodes all + known values." + [& {:keys [prefix default] :or {prefix "penpot"}}] + (->> (read-env prefix) + (merge default) + (decode-config))) (def version (v/parse (or (some-> (io/resource "version.txt") @@ -376,11 +275,30 @@ (str/trim)) "%version%"))) -(defonce ^:dynamic config (read-config)) +(defonce ^:dynamic config (read-config :default default)) (defonce ^:dynamic flags (parse-flags config)) -(def deletion-delay - (dt/duration {:days 7})) +(defn validate! + "Validate the currently loaded configuration data." + [& {:keys [exit-on-error?] :or {exit-on-error? true}}] + (if (validate-config config) + true + (let [explain (explain-config config)] + (println "Error on validating configuration:") + (sm/pretty-explain explain + :variant ::sm/schemaless-explain + :message "Configuration Validation Error") + (flush) + (if exit-on-error? + (System/exit -1) + (ex/raise :type :validation + :code :config-validaton + ::sm/explain explain))))) + +(defn get-deletion-delay + [] + (or (c/get config :deletion-delay) + (dt/duration {:days 7}))) (defn get "A configuration getter. Helps code be more testable." diff --git a/backend/src/app/db.clj b/backend/src/app/db.clj index 097ada50a12e1a21a102e5c4de0a4cdaca747004..2df9a53b118d9d48347962575f9c2cbcedc73eeb 100644 --- a/backend/src/app/db.clj +++ b/backend/src/app/db.clj @@ -153,7 +153,7 @@ (s/def ::conn some?) (s/def ::nilable-pool (s/nilable ::pool)) (s/def ::pool pool?) -(s/def ::pool-or-conn some?) +(s/def ::connectable some?) (defn closed? [pool] @@ -407,6 +407,7 @@ (ex/raise :type :not-found :code :object-not-found :table table + :params params :hint "database object not found")) row)) diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index 2cc47a37ac2bb29f530ffd6803a6bc4484733e32..617ef73a0cb682d63ea2aedee30d5fd75a9c38a2 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -7,14 +7,18 @@ (ns app.email "Main api for send emails." (:require + [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.pprint :as pp] + [app.common.schema :as sm] [app.common.spec :as us] [app.config :as cf] [app.db :as db] [app.db.sql :as sql] [app.email.invite-to-team :as-alias email.invite-to-team] + [app.email.join-team :as-alias email.join-team] + [app.email.request-team-access :as-alias email.request-team-access] [app.metrics :as mtx] [app.util.template :as tmpl] [app.worker :as wrk] @@ -149,9 +153,27 @@ "mail.smtp.timeout" timeout "mail.smtp.connectiontimeout" timeout})) +(def ^:private schema:smtp-config + [:map + [::username {:optional true} :string] + [::password {:optional true} :string] + [::tls {:optional true} ::sm/boolean] + [::ssl {:optional true} ::sm/boolean] + [::host {:optional true} :string] + [::port {:optional true} ::sm/int] + [::default-from {:optional true} :string] + [::default-reply-to {:optional true} :string]]) + +(def valid-smtp-config? + (sm/check-fn schema:smtp-config)) + (defn- create-smtp-session ^Session [cfg] + (dm/assert! + "expected valid smtp config" + (valid-smtp-config? cfg)) + (let [props (opts->props cfg)] (Session/getInstance props))) @@ -262,44 +284,21 @@ (let [email (if factory (factory context) (dissoc context ::conn))] - (wrk/submit! (merge - {::wrk/task :sendmail - ::wrk/delay 0 - ::wrk/max-retries 4 - ::wrk/priority 200 - ::wrk/conn conn} - email)))) + (wrk/submit! {::wrk/task :sendmail + ::wrk/delay 0 + ::wrk/max-retries 4 + ::wrk/priority 200 + ::db/conn conn + ::wrk/params email}))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SENDMAIL FN / TASK HANDLER ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::username ::cf/smtp-username) -(s/def ::password ::cf/smtp-password) -(s/def ::tls ::cf/smtp-tls) -(s/def ::ssl ::cf/smtp-ssl) -(s/def ::host ::cf/smtp-host) -(s/def ::port ::cf/smtp-port) -(s/def ::default-reply-to ::cf/smtp-default-reply-to) -(s/def ::default-from ::cf/smtp-default-from) - -(s/def ::smtp-config - (s/keys :opt [::username - ::password - ::tls - ::ssl - ::host - ::port - ::default-from - ::default-reply-to])) - (declare send-to-logger!) (s/def ::sendmail fn?) -(defmethod ig/pre-init-spec ::sendmail [_] - (s/spec ::smtp-config)) - (defmethod ig/init-key ::sendmail [_ cfg] (fn [params] @@ -307,6 +306,8 @@ (let [session (create-smtp-session cfg)] (with-open [transport (.getTransport session (if (::ssl cfg) "smtps" "smtp"))] (.connect ^Transport transport + ^String (::host cfg) + ^String (::port cfg) ^String (::username cfg) ^String (::password cfg)) @@ -400,6 +401,79 @@ "Teams member invitation email." (template-factory ::invite-to-team)) + +(s/def ::email.join-team/invited-by ::us/string) +(s/def ::email.join-team/team ::us/string) +(s/def ::email.join-team/team-id ::us/uuid) + +(s/def ::join-team + (s/keys :req-un [::email.join-team/invited-by + ::email.join-team/team-id + ::email.join-team/team])) + +(def join-team + "Teams member joined after request email." + (template-factory ::join-team)) + +(s/def ::email.request-team-access/requested-by ::us/string) +(s/def ::email.request-team-access/requested-by-email ::us/string) +(s/def ::email.request-team-access/team-name ::us/string) +(s/def ::email.request-team-access/team-id ::us/uuid) +(s/def ::email.request-team-access/file-name ::us/string) +(s/def ::email.request-team-access/file-id ::us/uuid) +(s/def ::email.request-team-access/page-id ::us/uuid) + +(s/def ::request-file-access + (s/keys :req-un [::email.request-team-access/requested-by + ::email.request-team-access/requested-by-email + ::email.request-team-access/team-name + ::email.request-team-access/team-id + ::email.request-team-access/file-name + ::email.request-team-access/file-id + ::email.request-team-access/page-id])) + +(def request-file-access + "File access request email." + (template-factory ::request-file-access)) + + +(s/def ::request-file-access-yourpenpot + (s/keys :req-un [::email.request-team-access/requested-by + ::email.request-team-access/requested-by-email + ::email.request-team-access/team-name + ::email.request-team-access/team-id + ::email.request-team-access/file-name + ::email.request-team-access/file-id + ::email.request-team-access/page-id])) + +(def request-file-access-yourpenpot + "File access on Your Penpot request email." + (template-factory ::request-file-access-yourpenpot)) + +(s/def ::request-file-access-yourpenpot-view + (s/keys :req-un [::email.request-team-access/requested-by + ::email.request-team-access/requested-by-email + ::email.request-team-access/team-name + ::email.request-team-access/team-id + ::email.request-team-access/file-name + ::email.request-team-access/file-id + ::email.request-team-access/page-id])) + +(def request-file-access-yourpenpot-view + "File access on Your Penpot view mode request email." + (template-factory ::request-file-access-yourpenpot-view)) + +(s/def ::request-team-access + (s/keys :req-un [::email.request-team-access/requested-by + ::email.request-team-access/requested-by-email + ::email.request-team-access/team-name + ::email.request-team-access/team-id])) + +(def request-team-access + "Team access request email." + (template-factory ::request-team-access)) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; BOUNCE/COMPLAINS HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -449,3 +523,11 @@ {:email email :type "bounce"} {:limit 10}))] (>= (count reports) threshold)))) + +(defn has-reports? + ([conn email] (has-reports? conn email nil)) + ([conn email {:keys [threshold] :or {threshold 1}}] + (let [reports (db/exec! conn (sql/select :global-complaint-report + {:email email} + {:limit 10}))] + (>= (count reports) threshold)))) diff --git a/backend/src/app/email/blacklist.clj b/backend/src/app/email/blacklist.clj new file mode 100644 index 0000000000000000000000000000000000000000..ca80afb6c98b22205ba118e8ea9bc8c17c826d2d --- /dev/null +++ b/backend/src/app/email/blacklist.clj @@ -0,0 +1,47 @@ +;; 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 app.email.blacklist + "Email blacklist provider" + (:refer-clojure :exclude [contains?]) + (:require + [app.common.logging :as l] + [app.config :as cf] + [app.email :as-alias email] + [clojure.core :as c] + [clojure.java.io :as io] + [cuerdas.core :as str] + [integrant.core :as ig])) + +(defmethod ig/init-key ::email/blacklist + [_ _] + (when (c/contains? cf/flags :email-blacklist) + (try + (let [path (cf/get :email-domain-blacklist) + result (with-open [reader (io/reader path)] + (reduce (fn [result line] + (if (str/starts-with? line "#") + result + (conj result (-> line str/trim str/lower)))) + #{} + (line-seq reader)))] + (l/inf :hint "initializing email blacklist" :domains (count result)) + (not-empty result)) + + (catch Throwable cause + (l/wrn :hint "unexpected exception on initializing email blacklist" + :cause cause))))) + +(defn contains? + "Check if email is in the blacklist." + [{:keys [::email/blacklist]} email] + (let [[_ domain] (str/split email "@" 2)] + (c/contains? blacklist (str/lower domain)))) + +(defn enabled? + "Check if the blacklist is enabled" + [{:keys [::email/blacklist]}] + (some? blacklist)) diff --git a/backend/src/app/email/whitelist.clj b/backend/src/app/email/whitelist.clj new file mode 100644 index 0000000000000000000000000000000000000000..85c137bfb06bea2c8f5c74878f439c1cbbe8dc83 --- /dev/null +++ b/backend/src/app/email/whitelist.clj @@ -0,0 +1,59 @@ +;; 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 app.email.whitelist + "Email whitelist provider" + (:refer-clojure :exclude [contains?]) + (:require + [app.common.logging :as l] + [app.config :as cf] + [app.email :as-alias email] + [clojure.core :as c] + [clojure.java.io :as io] + [cuerdas.core :as str] + [datoteka.fs :as fs] + [integrant.core :as ig])) + +(defn- read-whitelist + [path] + (when (and path (fs/exists? path)) + (try + (with-open [reader (io/reader path)] + (reduce (fn [result line] + (if (str/starts-with? line "#") + result + (conj result (-> line str/trim str/lower)))) + #{} + (line-seq reader))) + + (catch Throwable cause + (l/wrn :hint "unexpected exception on reading email whitelist" + :cause cause))))) + +(defmethod ig/init-key ::email/whitelist + [_ _] + (let [whitelist (or (cf/get :registration-domain-whitelist) #{}) + whitelist (if (c/contains? cf/flags :email-whitelist) + (into whitelist (read-whitelist (cf/get :email-domain-whitelist))) + whitelist) + whitelist (not-empty whitelist)] + + + (when whitelist + (l/inf :hint "initializing email whitelist" :domains (count whitelist))) + + whitelist)) + +(defn contains? + "Check if email is in the whitelist." + [{:keys [::email/whitelist]} email] + (let [[_ domain] (str/split email "@" 2)] + (c/contains? whitelist (str/lower domain)))) + +(defn enabled? + "Check if the whitelist is enabled" + [{:keys [::email/whitelist]}] + (some? whitelist)) diff --git a/backend/src/app/features/components_v2.clj b/backend/src/app/features/components_v2.clj index 8c7933160af6d6b15d76eb0a65d27a622860c8c4..5415e70d42d479070236a3e13d28b5627b985769 100644 --- a/backend/src/app/features/components_v2.clj +++ b/backend/src/app/features/components_v2.clj @@ -12,16 +12,17 @@ [app.common.files.changes :as cp] [app.common.files.changes-builder :as fcb] [app.common.files.helpers :as cfh] - [app.common.files.libraries-helpers :as cflh] [app.common.files.migrations :as fmg] [app.common.files.shapes-helpers :as cfsh] [app.common.files.validate :as cfv] + [app.common.fressian :as fres] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.path :as gshp] [app.common.logging :as l] + [app.common.logic.libraries :as cll] [app.common.math :as mth] [app.common.schema :as sm] [app.common.svg :as csvg] @@ -32,6 +33,7 @@ [app.common.types.container :as ctn] [app.common.types.file :as ctf] [app.common.types.grid :as ctg] + [app.common.types.modifiers :as ctm] [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] [app.common.types.shape :as cts] @@ -47,27 +49,27 @@ [app.rpc.commands.files-snapshot :as fsnap] [app.rpc.commands.media :as cmd.media] [app.storage :as sto] + [app.storage.impl :as impl] [app.storage.tmp :as tmp] [app.svgo :as svgo] [app.util.blob :as blob] - [app.util.cache :as cache] - [app.util.events :as events] [app.util.pointer-map :as pmap] [app.util.time :as dt] [buddy.core.codecs :as bc] [clojure.set :refer [rename-keys]] [cuerdas.core :as str] + [datoteka.fs :as fs] [datoteka.io :as io] - [promesa.exec :as px] [promesa.util :as pu])) + (def ^:dynamic *stats* "A dynamic var for setting up state for collect stats globally." nil) (def ^:dynamic *cache* "A dynamic var for setting up a cache instance." - nil) + false) (def ^:dynamic *skip-on-graphic-error* "A dynamic var for setting up the default error behavior for graphics processing." @@ -99,6 +101,8 @@ (some? data) (assoc :data (blob/decode data)))) +(set! *warn-on-reflection* true) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; FILE PREPARATION BEFORE MIGRATION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -110,7 +114,7 @@ (sm/lazy-validator ::ctc/color)) (def valid-fill? - (sm/lazy-validator ::cts/fill)) + (sm/lazy-validator cts/schema:fill)) (def valid-stroke? (sm/lazy-validator ::cts/stroke)) @@ -131,10 +135,10 @@ (sm/lazy-validator ::ctc/rgb-color)) (def valid-shape-points? - (sm/lazy-validator ::cts/points)) + (sm/lazy-validator cts/schema:points)) (def valid-image-attrs? - (sm/lazy-validator ::cts/image-attrs)) + (sm/lazy-validator cts/schema:image-attrs)) (def valid-column-grid-params? (sm/lazy-validator ::ctg/column-params)) @@ -215,10 +219,15 @@ (update :pages-index update-vals fix-container) (d/update-when :components update-vals fix-container)))) - fix-page-invalid-options + fix-invalid-page (fn [file-data] (letfn [(update-page [page] - (update page :options fix-options)) + (-> page + (update :name (fn [name] + (if (nil? name) + "Page" + name))) + (update :options fix-options))) (fix-background [options] (if (and (contains? options :background) @@ -433,7 +442,8 @@ (letfn [(fix-component [components id component] (let [root-shape (ctst/get-shape component (:id component))] (if (or (empty? (:objects component)) - (nil? root-shape)) + (nil? root-shape) + (nil? (:type root-shape))) (dissoc components id) components)))] @@ -731,43 +741,61 @@ (fn [file-data] ;; Remap shape-refs so that they point to the near main. ;; At the same time, if there are any dangling ref, detach the shape and its children. - (letfn [(fix-container [container] - (reduce fix-shape container (ctn/shapes-seq container))) - - (fix-shape [container shape] - (if (ctk/in-component-copy? shape) - ;; First look for the direct shape. - (let [root (ctn/get-component-shape (:objects container) shape) - libraries (assoc-in libraries [(:id file-data) :data] file-data) - library (get libraries (:component-file root)) - component (ctkl/get-component (:data library) (:component-id root) true) - direct-shape (ctf/get-component-shape (:data library) component (:shape-ref shape))] - (if (some? direct-shape) - ;; If it exists, there is nothing else to do. - container - ;; If not found, find the near shape. - (let [near-shape (d/seek #(= (:shape-ref %) (:shape-ref shape)) - (ctf/get-component-shapes (:data library) component))] - (if (some? near-shape) - ;; If found, update the ref to point to the near shape. - (ctn/update-shape container (:id shape) #(assoc % :shape-ref (:id near-shape))) - ;; If not found, it may be a fostered component. Try to locate a direct shape - ;; in the head component. - (let [head (ctn/get-head-shape (:objects container) shape) - library-2 (get libraries (:component-file head)) - component-2 (ctkl/get-component (:data library-2) (:component-id head) true) - direct-shape-2 (ctf/get-component-shape (:data library-2) component-2 (:shape-ref shape))] - (if (some? direct-shape-2) - ;; If it exists, there is nothing else to do. - container - ;; If not found, detach shape and all children. - ;; container - (detach-shape container shape))))))) - container))] - - (-> file-data - (update :pages-index update-vals fix-container) - (d/update-when :components update-vals fix-container)))) + (let [count (volatile! 0) + + fix-shape + (fn [container shape] + (if (ctk/in-component-copy? shape) + ;; First look for the direct shape. + (let [root (ctn/get-component-shape (:objects container) shape) + libraries (assoc-in libraries [(:id file-data) :data] file-data) + library (get libraries (:component-file root)) + component (ctkl/get-component (:data library) (:component-id root) true) + direct-shape (ctf/get-component-shape (:data library) component (:shape-ref shape))] + (if (some? direct-shape) + ;; If it exists, there is nothing else to do. + container + ;; If not found, find the near shape. + (let [near-shape (d/seek #(= (:shape-ref %) (:shape-ref shape)) + (ctf/get-component-shapes (:data library) component))] + (if (some? near-shape) + ;; If found, update the ref to point to the near shape. + (do + (vswap! count inc) + (ctn/update-shape container (:id shape) #(assoc % :shape-ref (:id near-shape)))) + ;; If not found, it may be a fostered component. Try to locate a direct shape + ;; in the head component. + (let [head (ctn/get-head-shape (:objects container) shape) + library-2 (get libraries (:component-file head)) + component-2 (ctkl/get-component (:data library-2) (:component-id head) true) + direct-shape-2 (ctf/get-component-shape (:data library-2) component-2 (:shape-ref shape))] + (if (some? direct-shape-2) + ;; If it exists, there is nothing else to do. + container + ;; If not found, detach shape and all children. + ;; container + (do + (vswap! count inc) + (detach-shape container shape)))))))) + container)) + + fix-container + (fn [container] + (reduce fix-shape container (ctn/shapes-seq container)))] + + [(-> file-data + (update :pages-index update-vals fix-container) + (d/update-when :components update-vals fix-container)) + @count])) + + remap-refs-recur + ;; remapping refs can generate cascade changes so we call it until no changes are done + (fn [file-data] + (loop [f-data file-data] + (let [[f-data count] (remap-refs f-data)] + (if (= count 0) + f-data + (recur f-data))))) fix-converted-copies (fn [file-data] @@ -806,7 +834,8 @@ :width (:width (:selrect root-shape)) :height (:height (:selrect root-shape)) :name (:name component) - :shapes [new-id]}) + :shapes [new-id] + :show-content true}) (assoc :frame-id nil :parent-id nil)) root-shape' (assoc root-shape @@ -846,13 +875,16 @@ (fix-shape [shape] (if (or (nil? (:parent-id shape)) (ctk/instance-head? shape)) - (assoc shape - :type :frame ; Old groups must be converted - :fills (or (:fills shape) []) ; to frames and conform to spec - :shapes (or (:shapes shape) []) - :hide-in-viewer (or (:hide-in-viewer shape) true) - :rx (or (:rx shape) 0) - :ry (or (:ry shape) 0)) + (let [frame? (= :frame (:type shape)) + not-group? (not= :group (:type shape))] + (assoc shape ; Old groups must be converted + :type :frame ; to frames and conform to spec + :fills (if not-group? (d/nilv (:fills shape) []) []) ; Groups never should have fill + :shapes (or (:shapes shape) []) + :hide-in-viewer (if frame? (boolean (:hide-in-viewer shape)) true) + :show-content (if frame? (boolean (:show-content shape)) true) + :rx (or (:rx shape) 0) + :ry (or (:ry shape) 0))) shape))] (-> file-data (update :pages-index update-vals fix-container) @@ -951,6 +983,29 @@ (-> file-data (update :pages-index update-vals fix-container)))) + + fix-copies-names + (fn [file-data] + ;; Rename component heads to add the component path to the name + (letfn [(fix-container [container] + (d/update-when container :objects #(cfh/reduce-objects % fix-shape %))) + + (fix-shape [objects shape] + (let [root (ctn/get-component-shape objects shape) + libraries (assoc-in libraries [(:id file-data) :data] file-data) + library (get libraries (:component-file root)) + component (ctkl/get-component (:data library) (:component-id root) true) + path (str/trim (:path component))] + (if (and (ctk/instance-head? shape) + (some? component) + (= (:name component) (:name shape)) + (not (str/empty? path))) + (update objects (:id shape) assoc :name (str path " / " (:name component))) + objects)))] + + (-> file-data + (update :pages-index update-vals fix-container)))) + fix-copies-of-detached (fn [file-data] ;; Find any copy that is referencing a shape inside a component that have @@ -971,7 +1026,7 @@ (-> file-data (fix-file-data) - (fix-page-invalid-options) + (fix-invalid-page) (fix-misc-shape-issues) (fix-recent-colors) (fix-missing-image-metadata) @@ -990,8 +1045,8 @@ (remove-nested-roots) (add-not-nested-roots) (fix-components-without-id) - (remap-refs) (fix-converted-copies) + (remap-refs-recur) (wrap-non-group-component-roots) (detach-non-group-instance-roots) (transform-to-frames) @@ -1000,8 +1055,9 @@ (fix-component-nil-objects) (fix-false-copies) (fix-component-root-without-component) - (fix-copies-of-detached); <- Do not add fixes after this and fix-orphan-copies call - ))) + (fix-copies-names) + (fix-copies-of-detached)))); <- Do not add fixes after this and fix-orphan-copies call + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; COMPONENTS MIGRATION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1050,8 +1106,8 @@ {:type :frame :x (:x position) :y (:y position) - :width (+ width (* 2 grid-gap)) - :height (+ height (* 2 grid-gap)) + :width (+ width grid-gap) + :height (+ height grid-gap) :name name :frame-id uuid/zero :parent-id uuid/zero})) @@ -1089,7 +1145,12 @@ (let [shapes (cfh/get-children-with-self (:objects component) (:id component)) - root-shape (first shapes) + ;; Let's calculate the top shame name from the components path and name + root-shape (-> (first shapes) + (assoc :name (cfh/merge-path-item (:path component) (:name component)))) + + shapes (assoc shapes 0 root-shape) + orig-pos (gpt/point (:x root-shape) (:y root-shape)) delta (gpt/subtract position orig-pos) @@ -1135,9 +1196,6 @@ add-instance-grid (fn [fdata frame-id grid assets] (reduce (fn [result [component position]] - (events/tap :progress {:op :migrate-component - :id (:id component) - :name (:name component)}) (add-main-instance result component frame-id (gpt/add position (gpt/point grid-gap grid-gap)))) fdata @@ -1197,7 +1255,7 @@ :frame-id frame-id :parent-id frame-id}) (assoc - :proportion (/ width height) + :proportion (float (/ width height)) :proportion-lock true)) img-shape (cts/setup-shape @@ -1238,7 +1296,7 @@ (try (let [item (if (str/starts-with? href "data:") (let [[mtype data] (parse-datauri href) - size (alength data) + size (alength ^bytes data) path (tmp/tempfile :prefix "penpot.media.download.") written (io/write-to-file! data path :size size)] @@ -1307,32 +1365,54 @@ {::sql/columns [:media-id]})] (:media-id fmobject))) -(defn- get-sobject-content +(defn get-sobject-content [id] (let [storage (::sto/storage *system*) sobject (sto/get-object storage id)] + + (when-not sobject + (throw (RuntimeException. "sobject is nil"))) + (when (> (:size sobject) 1135899) + (throw (RuntimeException. "svg too big"))) + (with-open [stream (sto/get-object-data storage sobject)] (slurp stream)))) -(defn- create-shapes-for-svg - [{:keys [id] :as mobj} file-id objects frame-id position] - (let [get-svg (fn [sid] - (let [svg-text (get-sobject-content sid) - svg-text (svgo/optimize *system* svg-text)] - (-> (csvg/parse svg-text) - (assoc :name (:name mobj))))) +(defn get-optimized-svg + [sid] + (let [svg-text (get-sobject-content sid) + svg-text (svgo/optimize *system* svg-text)] + (csvg/parse svg-text))) + +(def base-path "/data/cache") - sid (resolve-sobject-id id) - svg-data (if (cache/cache? *cache*) - (cache/get *cache* sid (px/wrap-bindings get-svg)) - (get-svg sid)) +(defn get-sobject-cache-path + [sid] + (let [path (impl/id->path sid)] + (fs/join base-path path))) - svg-data (collect-and-persist-images svg-data file-id id)] +(defn get-cached-svg + [sid] + (let [path (get-sobject-cache-path sid)] + (if (fs/exists? path) + (with-open [^java.lang.AutoCloseable stream (io/input-stream path)] + (let [reader (fres/reader stream)] + (fres/read! reader))) + (get-optimized-svg sid)))) + +(defn- create-shapes-for-svg + [{:keys [id] :as mobj} file-id objects frame-id position] + (let [sid (resolve-sobject-id id) + svg-data (if *cache* + (get-cached-svg sid) + (get-optimized-svg sid)) + svg-data (collect-and-persist-images svg-data file-id id) + svg-data (assoc svg-data :name (:name mobj))] (sbuilder/create-svg-shapes svg-data position objects frame-id frame-id #{} false))) (defn- process-media-object - [fdata page-id frame-id mobj position] + [fdata page-id frame-id mobj position shape-cb] (let [page (ctpl/get-page fdata page-id) file-id (get fdata :id) @@ -1371,27 +1451,27 @@ page (cons shape children)) - [_ _ changes2] - (cflh/generate-add-component nil - [shape] - (:objects page) - (:id page) - file-id - true - nil - cfsh/prepare-create-artboard-from-selection) - changes (fcb/concat-changes changes changes2)] - + [_ _ changes] + (cll/generate-add-component changes + [shape] + (:objects page) + (:id page) + file-id + true + nil + cfsh/prepare-create-artboard-from-selection)] + + (shape-cb shape) (:redo-changes changes))) (defn- create-media-grid - [fdata page-id frame-id grid media-group] + [fdata page-id frame-id grid media-group shape-cb] (letfn [(process [fdata mobj position] (let [position (gpt/add position (gpt/point grid-gap grid-gap)) tp (dt/tpoint) err (volatile! false)] (try - (let [changes (process-media-object fdata page-id frame-id mobj position)] + (let [changes (process-media-object fdata page-id frame-id mobj position shape-cb)] (cp/process-changes fdata changes false)) (catch Throwable cause @@ -1434,12 +1514,46 @@ (->> (d/zip media-group grid) (reduce (fn [fdata [mobj position]] - (events/tap :progress {:op :migrate-graphic - :id (:id mobj) - :name (:name mobj)}) (or (process fdata mobj position) fdata)) (assoc-in fdata [:options :components-v2] true))))) +(defn- fix-graphics-size + [fdata new-grid page-id frame-id] + (let [modify-shape (fn [page shape-id modifiers] + (ctn/update-shape page shape-id #(gsh/transform-shape % modifiers))) + + resize-frame (fn [page] + (let [{:keys [width height]} (meta new-grid) + + frame (ctst/get-shape page frame-id) + width (+ width grid-gap) + height (+ height grid-gap) + + modif-frame (ctm/resize nil + (gpt/point (/ width (:width frame)) + (/ height (:height frame))) + (gpt/point (:x frame) (:y frame)))] + + (modify-shape page frame-id modif-frame))) + + move-components (fn [page] + (let [frame (get (:objects page) frame-id) + shapes (map (d/getf (:objects page)) (:shapes frame))] + (->> (d/zip shapes new-grid) + (reduce (fn [page [shape position]] + (let [position (gpt/add position (gpt/point grid-gap grid-gap)) + modif-shape (ctm/move nil + (gpt/point (- (:x position) (:x (:selrect shape))) + (- (:y position) (:y (:selrect shape))))) + children-ids (cfh/get-children-ids-with-self (:objects page) (:id shape))] + (reduce #(modify-shape %1 %2 modif-shape) + page + children-ids))) + page))))] + (-> fdata + (ctpl/update-page page-id resize-frame) + (ctpl/update-page page-id move-components)))) + (defn- migrate-graphics [fdata] (if (empty? (:media fdata)) @@ -1477,12 +1591,32 @@ (:id frame) (:id frame) nil - true))] + true)) + new-shapes (volatile! []) + add-shape #(vswap! new-shapes conj %) + + fdata' (create-media-grid fdata page-id (:id frame) grid assets add-shape) + + ;; When svgs had different width&height and viewport, + ;; sometimes the old graphics importer didn't + ;; calculate well the media object size. So, after + ;; migration we recalculate grid size from the actual + ;; size of the created shapes. + fdata' (if-let [grid (ctst/generate-shape-grid @new-shapes position grid-gap)] + (let [{new-width :width new-height :height} (meta grid)] + (if-not (and (mth/close? width new-width) (mth/close? height new-height)) + (do + (l/inf :hint "fixing graphics sizes" + :file-id (str (:id fdata)) + :group group-name) + (fix-graphics-size fdata' grid page-id (:id frame))) + fdata')) + fdata')] + (recur (next groups) - (create-media-grid fdata page-id (:id frame) grid assets) + fdata' (gpt/add position (gpt/point 0 (+ height (* 2 grid-gap) frame-gap)))))))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PRIVATE HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1547,6 +1681,7 @@ (db/update! conn :file {:data (blob/encode (:data file)) :features (db/create-array conn "text" (:features file)) + :version (:version file) :revn (:revn file)} {:id (:id file)}))) @@ -1596,7 +1731,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn migrate-file! - [system file-id & {:keys [validate? skip-on-graphic-error? label]}] + [system file-id & {:keys [validate? skip-on-graphic-error? label rown]}] (let [tpoint (dt/tpoint) err (volatile! false)] @@ -1608,7 +1743,7 @@ :validate validate? :skip-on-graphic-error skip-on-graphic-error?) - (db/tx-run! (update system ::sto/storage media/configure-assets-storage) + (db/tx-run! system (fn [system] (binding [*system* system] (when (string? label) @@ -1617,11 +1752,6 @@ (let [file (get-file system file-id) file (process-file! system file :validate? validate?)] - (events/tap :progress - {:op :migrate-file - :name (:name file) - :id (:id file)}) - (persist-file! system file))))) (catch Throwable cause @@ -1636,33 +1766,24 @@ components (get @*file-stats* :processed-components 0) graphics (get @*file-stats* :processed-graphics 0)] - (if (cache/cache? *cache*) - (let [cache-stats (cache/stats *cache*)] - (l/dbg :hint "migrate:file:end" - :file-id (str file-id) - :graphics graphics - :components components - :validate validate? - :crt (mth/to-fixed (:hit-rate cache-stats) 2) - :crq (str (:req-count cache-stats)) - :error @err - :elapsed (dt/format-duration elapsed))) - (l/dbg :hint "migrate:file:end" - :file-id (str file-id) - :graphics graphics - :components components - :validate validate? - :error @err - :elapsed (dt/format-duration elapsed))) + (l/dbg :hint "migrate:file:end" + :file-id (str file-id) + :graphics graphics + :components components + :validate validate? + :rown rown + :error @err + :elapsed (dt/format-duration elapsed)) (some-> *stats* (swap! update :processed-files (fnil inc 0))) (some-> *team-stats* (swap! update :processed-files (fnil inc 0))))))))) (defn migrate-team! - [system team-id & {:keys [validate? skip-on-graphic-error? label]}] + [system team-id & {:keys [validate? rown skip-on-graphic-error? label]}] (l/dbg :hint "migrate:team:start" - :team-id (dm/str team-id)) + :team-id (dm/str team-id) + :rown rown) (let [tpoint (dt/tpoint) err (volatile! false) @@ -1684,11 +1805,6 @@ (conj "layout/grid") (conj "styles/v2"))] - (events/tap :progress - {:op :migrate-team - :name (:name team) - :id id}) - (run! (partial migrate-file system) (get-and-lock-team-files conn id)) @@ -1715,21 +1831,10 @@ (when-not @err (some-> *stats* (swap! update :processed-teams (fnil inc 0)))) - (if (cache/cache? *cache*) - (let [cache-stats (cache/stats *cache*)] - (l/dbg :hint "migrate:team:end" - :team-id (dm/str team-id) - :files files - :components components - :graphics graphics - :crt (mth/to-fixed (:hit-rate cache-stats) 2) - :crq (str (:req-count cache-stats)) - :error @err - :elapsed (dt/format-duration elapsed))) - - (l/dbg :hint "migrate:team:end" - :team-id (dm/str team-id) - :files files - :components components - :graphics graphics - :elapsed (dt/format-duration elapsed))))))))) + (l/dbg :hint "migrate:team:end" + :team-id (dm/str team-id) + :rown rown + :files files + :components components + :graphics graphics + :elapsed (dt/format-duration elapsed)))))))) diff --git a/backend/src/app/features/fdata.clj b/backend/src/app/features/fdata.clj index 3ec5fdfb574fd4779e574c04792eafdadabafb4a..1d9a649f3d155c54e70a5bc1b7772e46933a6cbb 100644 --- a/backend/src/app/features/fdata.clj +++ b/backend/src/app/features/fdata.clj @@ -12,10 +12,19 @@ [app.common.logging :as l] [app.db :as db] [app.db.sql :as-alias sql] + [app.storage :as sto] [app.util.blob :as blob] [app.util.objects-map :as omap] [app.util.pointer-map :as pmap])) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; OFFLOAD +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn offloaded? + [file] + (= "objects-storage" (:data-backend file))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; OBJECTS-MAP ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -55,25 +64,45 @@ ;; POINTER-MAP ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn get-file-data + "Get file data given a file instance." + [system file] + (if (offloaded? file) + (let [storage (sto/resolve system ::db/reuse-conn true)] + (->> (sto/get-object storage (:data-ref-id file)) + (sto/get-object-bytes storage))) + (:data file))) + +(defn resolve-file-data + [system file] + (let [data (get-file-data system file)] + (assoc file :data data))) + (defn load-pointer "A database loader pointer helper" [system file-id id] - (let [{:keys [content]} (db/get system :file-data-fragment - {:id id :file-id file-id} - {::sql/columns [:content] - ::db/check-deleted false})] - (when-not content + (let [fragment (db/get* system :file-data-fragment + {:id id :file-id file-id} + {::sql/columns [:data :data-backend :data-ref-id :id]})] + + (l/trc :hint "load pointer" + :file-id (str file-id) + :id (str id) + :found (some? fragment)) + + (when-not fragment (ex/raise :type :internal :code :fragment-not-found :hint "fragment not found" :file-id file-id :fragment-id id)) - (blob/decode content))) + (let [data (get-file-data system fragment)] + ;; FIXME: conditional thread scheduling for decoding big objects + (blob/decode data)))) (defn persist-pointers! - "Given a database connection and the final file-id, persist all - pointers to the underlying storage (the database)." + "Persist all currently tracked pointer objects" [system file-id] (let [conn (db/get-connection system)] (doseq [[id item] @pmap/*tracked*] @@ -83,7 +112,7 @@ (db/insert! conn :file-data-fragment {:id id :file-id file-id - :content content})))))) + :data content})))))) (defn process-pointers "Apply a function to all pointers on the file. Usuly used for diff --git a/backend/src/app/http.clj b/backend/src/app/http.clj index 1e605cdb08733baf609ab0ba66d0950ee73664b4..672d1ec60f2c41d8229a621cb58eb1579fe6ab0f 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -23,6 +23,7 @@ [app.metrics :as mtx] [app.rpc :as-alias rpc] [app.rpc.doc :as-alias rpc.doc] + [app.setup :as-alias setup] [clojure.spec.alpha :as s] [integrant.core :as ig] [promesa.exec :as px] @@ -113,7 +114,7 @@ (partial not-found-handler request))) (on-error [cause request] - (let [{:keys [body] :as response} (errors/handle cause request)] + (let [{:keys [::rres/body] :as response} (errors/handle cause request)] (cond-> response (map? body) (-> (update ::rres/headers assoc "content-type" "application/transit+json") @@ -136,7 +137,7 @@ ::rpc/routes ::rpc.doc/routes ::oidc/routes - ::main/props + ::setup/props ::assets/routes ::debug/routes ::db/pool @@ -150,9 +151,9 @@ [mw/params] [mw/format-response] [mw/parse-request] + [mw/errors errors/handle] [session/soft-auth cfg] [actoken/soft-auth cfg] - [mw/errors errors/handle] [mw/restrict-methods]]} (::mtx/routes cfg) diff --git a/backend/src/app/http/access_token.clj b/backend/src/app/http/access_token.clj index bfddbb42d7b8fc952b2fc94438af3ed03b824bbb..0d1865f100049325824a9bf1b31a904fe94dca45 100644 --- a/backend/src/app/http/access_token.clj +++ b/backend/src/app/http/access_token.clj @@ -10,6 +10,7 @@ [app.config :as cf] [app.db :as db] [app.main :as-alias main] + [app.setup :as-alias setup] [app.tokens :as tokens] [ring.request :as rreq])) @@ -42,7 +43,7 @@ (defn- wrap-soft-auth "Soft Authentication, will be executed synchronously on the undertow worker thread." - [handler {:keys [::main/props]}] + [handler {:keys [::setup/props]}] (letfn [(handle-request [request] (try (let [token (get-token request) diff --git a/backend/src/app/http/assets.clj b/backend/src/app/http/assets.clj index 06c3318490fe1db39c9344fbb49a8ca483d3a06e..9a8e69dbf2e6b446133ea481e0cc95e8074d2d10 100644 --- a/backend/src/app/http/assets.clj +++ b/backend/src/app/http/assets.clj @@ -57,11 +57,10 @@ (defn- serve-object "Helper function that returns the appropriate response depending on the storage object backend type." - [{:keys [::sto/storage] :as cfg} {:keys [backend] :as obj}] - (let [backend (sto/resolve-backend storage backend)] - (case (::sto/type backend) - :s3 (serve-object-from-s3 cfg obj) - :fs (serve-object-from-fs cfg obj)))) + [cfg {:keys [backend] :as obj}] + (case backend + (:s3 :assets-s3) (serve-object-from-s3 cfg obj) + (:fs :assets-fs) (serve-object-from-fs cfg obj))) (defn objects-handler "Handler that servers storage objects by id." diff --git a/backend/src/app/http/awsns.clj b/backend/src/app/http/awsns.clj index 7508be8a2de49988d71c76981c961b0ae622e6c6..77ae6c5d622d0d1b97297563874b8e4a89d61eac 100644 --- a/backend/src/app/http/awsns.clj +++ b/backend/src/app/http/awsns.clj @@ -9,16 +9,18 @@ (:require [app.common.exceptions :as ex] [app.common.logging :as l] + [app.common.pprint :as pp] [app.db :as db] [app.db.sql :as sql] [app.http.client :as http] [app.main :as-alias main] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.worker :as-alias wrk] + [clojure.data.json :as j] [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig] - [jsonista.core :as j] [promesa.exec :as px] [ring.request :as rreq] [ring.response :as-alias rres])) @@ -30,7 +32,7 @@ (defmethod ig/pre-init-spec ::routes [_] (s/keys :req [::http/client - ::main/props + ::setup/props ::db/pool])) (defmethod ig/init-key ::routes @@ -106,7 +108,7 @@ [cfg headers] (let [tdata (get headers "x-penpot-data")] (when-not (str/empty? tdata) - (let [result (tokens/verify (::main/props cfg) {:token tdata :iss :profile-identity})] + (let [result (tokens/verify (::setup/props cfg) {:token tdata :iss :profile-identity})] (:profile-id result))))) (defn- parse-notification @@ -135,83 +137,110 @@ (defn- parse-json [v] - (ex/ignoring - (j/read-value v))) + (try + (j/read-str v) + (catch Throwable cause + (l/wrn :hint "unable to decode request body" + :cause cause)))) (defn- register-bounce-for-profile [{:keys [::db/pool]} {:keys [type kind profile-id] :as report}] (when (= kind "permanent") - (db/with-atomic [conn pool] - (db/insert! conn :profile-complaint-report + (try + (db/insert! pool :profile-complaint-report {:profile-id profile-id :type (name type) :content (db/tjson report)}) - ;; TODO: maybe also try to find profiles by mail and if exists - ;; register profile reports for them? - (doseq [recipient (:recipients report)] - (db/insert! conn :global-complaint-report - {:email (:email recipient) - :type (name type) - :content (db/tjson report)})) - - (let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))] - (when (some #(= (:email profile) (:email %)) (:recipients report)) - ;; If the report matches the profile email, this means that - ;; the report is for itself, can be caused when a user - ;; registers with an invalid email or the user email is - ;; permanently rejecting receiving the email. In this case we - ;; have no option to mark the user as muted (and in this case - ;; the profile will be also inactive. - (db/update! conn :profile - {:is-muted true} - {:id profile-id})))))) - -(defn- register-complaint-for-profile - [{:keys [::db/pool]} {:keys [type profile-id] :as report}] - (db/with-atomic [conn pool] - (db/insert! conn :profile-complaint-report - {:profile-id profile-id - :type (name type) - :content (db/tjson report)}) + (catch Throwable cause + (l/warn :hint "unable to persist profile complaint" + :cause cause))) - ;; TODO: maybe also try to find profiles by email and if exists - ;; register profile reports for them? - (doseq [email (:recipients report)] - (db/insert! conn :global-complaint-report - {:email email + (doseq [recipient (:recipients report)] + (db/insert! pool :global-complaint-report + {:email (:email recipient) :type (name type) :content (db/tjson report)})) - (let [profile (db/exec-one! conn (sql/select :profile {:id profile-id}))] - (when (some #(= % (:email profile)) (:recipients report)) + (let [profile (db/exec-one! pool (sql/select :profile {:id profile-id}))] + (when (some #(= (:email profile) (:email %)) (:recipients report)) ;; If the report matches the profile email, this means that - ;; the report is for itself, rare case but can happen; In this - ;; case just mark profile as muted (very rare case). - (db/update! conn :profile + ;; the report is for itself, can be caused when a user + ;; registers with an invalid email or the user email is + ;; permanently rejecting receiving the email. In this case we + ;; have no option to mark the user as muted (and in this case + ;; the profile will be also inactive. + + (l/inf :hint "mark profile: muted" + :profile-id (str (:id profile)) + :email (:email profile) + :reason "bounce report" + :report-id (:feedback-id report)) + + (db/update! pool :profile {:is-muted true} - {:id profile-id}))))) + {:id profile-id} + {::db/return-keys false}))))) + +(defn- register-complaint-for-profile + [{:keys [::db/pool]} {:keys [type profile-id] :as report}] + + (try + (db/insert! pool :profile-complaint-report + {:profile-id profile-id + :type (name type) + :content (db/tjson report)}) + (catch Throwable cause + (l/warn :hint "unable to persist profile complaint" + :cause cause))) + + ;; TODO: maybe also try to find profiles by email and if exists + ;; register profile reports for them? + (doseq [email (:recipients report)] + (db/insert! pool :global-complaint-report + {:email email + :type (name type) + :content (db/tjson report)})) + + (let [profile (db/exec-one! pool (sql/select :profile {:id profile-id}))] + (when (some #(= % (:email profile)) (:recipients report)) + ;; If the report matches the profile email, this means that + ;; the report is for itself, rare case but can happen; In this + ;; case just mark profile as muted (very rare case). + (l/inf :hint "mark profile: muted" + :profile-id (str (:id profile)) + :email (:email profile) + :reason "complaint report" + :report-id (:feedback-id report)) + + (db/update! pool :profile + {:is-muted true} + {:id profile-id} + {::db/return-keys false})))) (defn- process-report [cfg {:keys [type profile-id] :as report}] - (l/trace :action "processing report" :report (pr-str report)) (cond ;; In this case we receive a bounce/complaint notification without ;; confirmed identity, we just emit a warning but do nothing about ;; it because this is not a normal case. All notifications should ;; come with profile identity. (nil? profile-id) - (l/warn :msg "a notification without identity received from AWS" - :report (pr-str report)) + (l/wrn :hint "not-identified report" + ::l/body (pp/pprint-str report {:length 40 :level 6})) (= "bounce" type) - (register-bounce-for-profile cfg report) + (do + (l/trc :hint "bounce report" + ::l/body (pp/pprint-str report {:length 40 :level 6})) + (register-bounce-for-profile cfg report)) (= "complaint" type) - (register-complaint-for-profile cfg report) + (do + (l/trc :hint "complaint report" + ::l/body (pp/pprint-str report {:length 40 :level 6})) + (register-complaint-for-profile cfg report)) :else - (l/warn :msg "unrecognized report received from AWS" - :report (pr-str report)))) - - + (l/wrn :hint "unrecognized report" + ::l/body (pp/pprint-str report {:length 20 :level 4})))) diff --git a/backend/src/app/http/client.clj b/backend/src/app/http/client.clj index 5b4a8541c8b9ec027c68493e0bc999b0adfa755d..4494a1bb0cab25d227d5dc537531c8dd875678db 100644 --- a/backend/src/app/http/client.clj +++ b/backend/src/app/http/client.clj @@ -54,9 +54,10 @@ "A convencience toplevel function for gradual migration to a new API convention." ([cfg-or-client request] - (let [client (resolve-client cfg-or-client)] - (send! client request {}))) + (let [client (resolve-client cfg-or-client) + request (update request :uri str)] + (send! client request {:sync? true}))) ([cfg-or-client request options] - (let [client (resolve-client cfg-or-client)] - (send! client request options)))) - + (let [client (resolve-client cfg-or-client) + request (update request :uri str)] + (send! client request (merge {:sync? true} options))))) diff --git a/backend/src/app/http/debug.clj b/backend/src/app/http/debug.clj index 1d2a129cf303a5a339e7abf9cb92dfb101bbd729..c62202572ede650fa44be5c78cbcb14bb0a162d9 100644 --- a/backend/src/app/http/debug.clj +++ b/backend/src/app/http/debug.clj @@ -16,10 +16,10 @@ [app.config :as cf] [app.db :as db] [app.http.session :as session] - [app.main :as-alias main] [app.rpc.commands.auth :as auth] [app.rpc.commands.files-create :refer [create-file]] [app.rpc.commands.profile :as profile] + [app.setup :as-alias setup] [app.srepl.helpers :as srepl] [app.storage :as-alias sto] [app.storage.tmp :as tmp] @@ -340,57 +340,57 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- resend-email-notification - [{:keys [::db/pool ::main/props] :as cfg} {:keys [params] :as request}] - - (when-not (contains? params :force) - (ex/raise :type :validation - :code :missing-force - :hint "missing force checkbox")) - - (let [profile (some->> params - :email - (profile/clean-email) - (profile/get-profile-by-email pool))] - - (when-not profile - (ex/raise :type :validation - :code :missing-profile - :hint "unable to find profile by email")) - - (cond - (contains? params :block) - (do - (db/update! pool :profile {:is-blocked true} {:id (:id profile)}) - (db/delete! pool :http-session {:profile-id (:id profile)}) - - {::rres/status 200 - ::rres/headers {"content-type" "text/plain"} - ::rres/body (str/ffmt "PROFILE '%' BLOCKED" (:email profile))}) - - (contains? params :unblock) - (do - (db/update! pool :profile {:is-blocked false} {:id (:id profile)}) - {::rres/status 200 - ::rres/headers {"content-type" "text/plain"} - ::rres/body (str/ffmt "PROFILE '%' UNBLOCKED" (:email profile))}) - - (contains? params :resend) - (if (:is-blocked profile) - {::rres/status 200 - ::rres/headers {"content-type" "text/plain"} - ::rres/body "PROFILE ALREADY BLOCKED"} - (do - (auth/send-email-verification! pool props profile) - {::rres/status 200 - ::rres/headers {"content-type" "text/plain"} - ::rres/body (str/ffmt "RESENDED FOR '%'" (:email profile))})) - - :else - (do - (db/update! pool :profile {:is-active true} {:id (:id profile)}) - {::rres/status 200 - ::rres/headers {"content-type" "text/plain"} - ::rres/body (str/ffmt "PROFILE '%' ACTIVATED" (:email profile))})))) + [cfg {:keys [params] :as request}] + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (when-not (contains? params :force) + (ex/raise :type :validation + :code :missing-force + :hint "missing force checkbox")) + + (let [profile (some->> params + :email + (profile/clean-email) + (profile/get-profile-by-email conn))] + + (when-not profile + (ex/raise :type :validation + :code :missing-profile + :hint "unable to find profile by email")) + + (cond + (contains? params :block) + (do + (db/update! conn :profile {:is-blocked true} {:id (:id profile)}) + (db/delete! conn :http-session {:profile-id (:id profile)}) + + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body (str/ffmt "PROFILE '%' BLOCKED" (:email profile))}) + + (contains? params :unblock) + (do + (db/update! conn :profile {:is-blocked false} {:id (:id profile)}) + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body (str/ffmt "PROFILE '%' UNBLOCKED" (:email profile))}) + + (contains? params :resend) + (if (:is-blocked profile) + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body "PROFILE ALREADY BLOCKED"} + (do + (#'auth/send-email-verification! cfg profile) + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body (str/ffmt "RESENDED FOR '%'" (:email profile))})) + + :else + (do + (db/update! conn :profile {:is-active true} {:id (:id profile)}) + {::rres/status 200 + ::rres/headers {"content-type" "text/plain"} + ::rres/body (str/ffmt "PROFILE '%' ACTIVATED" (:email profile))})))))) (defn- reset-file-version diff --git a/backend/src/app/http/errors.clj b/backend/src/app/http/errors.clj index 18350d21d857528e35aa18318e3ddae3421d6250..8101db116cd3cf3ba3df306c38c3f00d78d88926 100644 --- a/backend/src/app/http/errors.clj +++ b/backend/src/app/http/errors.clj @@ -14,32 +14,28 @@ [app.http :as-alias http] [app.http.access-token :as-alias actoken] [app.http.session :as-alias session] + [app.util.inet :as inet] [clojure.spec.alpha :as s] - [cuerdas.core :as str] [ring.request :as rreq] [ring.response :as rres])) -(defn- parse-client-ip - [request] - (or (some-> (rreq/get-header request "x-forwarded-for") (str/split ",") first) - (rreq/get-header request "x-real-ip") - (rreq/remote-addr request))) - (defn request->context "Extracts error report relevant context data from request." [request] (let [claims (-> {} (into (::session/token-claims request)) (into (::actoken/token-claims request)))] + {:request/path (:path request) :request/method (:method request) :request/params (:params request) :request/user-agent (rreq/get-header request "user-agent") - :request/ip-addr (parse-client-ip request) + :request/ip-addr (inet/parse-request request) :request/profile-id (:uid claims) :version/frontend (or (rreq/get-header request "x-frontend-version") "unknown") :version/backend (:full cf/version)})) + (defmulti handle-error (fn [cause _ _] (-> cause ex-data :type))) @@ -99,7 +95,7 @@ (= code :invalid-image) (binding [l/*context* (request->context request)] (let [cause (or parent-cause err)] - (l/error :hint "unexpected error on processing image" :cause cause) + (l/warn :hint "unexpected error on processing image" :cause cause) {::rres/status 400 ::rres/body data})) :else @@ -218,6 +214,14 @@ :hint (ex-message error) :data edata}})))) +(defmethod handle-exception java.io.IOException + [cause _ _] + (l/wrn :hint "io exception" :cause cause) + {::rres/status 500 + ::rres/body {:type :server-error + :code :io-exception + :hint (ex-message cause)}}) + (defmethod handle-exception java.util.concurrent.CompletionException [cause request _] (let [cause' (ex-cause cause)] diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index 4ea815f07f1ca058f5fc39716c3096cfc88f0933..de098ad1053716a3b7daa38d6802c738095c372e 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -7,19 +7,19 @@ (ns app.http.middleware (:require [app.common.exceptions :as ex] + [app.common.json :as json] [app.common.logging :as l] + [app.common.schema :as-alias sm] [app.common.transit :as t] [app.config :as cf] - [app.util.json :as json] + [app.http.errors :as errors] + [app.util.pointer-map :as pmap] [cuerdas.core :as str] [ring.request :as rreq] [ring.response :as rres] [yetti.adapter :as yt] [yetti.middleware :as ymw]) (:import - com.fasterxml.jackson.core.JsonParseException - com.fasterxml.jackson.core.io.JsonEOFException - com.fasterxml.jackson.databind.exc.MismatchedInputException io.undertow.server.RequestTooBigException java.io.InputStream java.io.OutputStream)) @@ -34,11 +34,12 @@ {:name ::params :compile (constantly ymw/wrap-params)}) -(def ^:private json-mapper - (json/mapper - {:encode-key-fn str/camel - :decode-key-fn (comp keyword str/kebab) - :pretty true})) +(defn- get-reader + ^java.io.BufferedReader + [request] + (let [^InputStream body (rreq/body request)] + (java.io.BufferedReader. + (java.io.InputStreamReader. body)))) (defn wrap-parse-request [handler] @@ -53,8 +54,8 @@ (update :params merge params)))) (str/starts-with? header "application/json") - (with-open [^InputStream is (rreq/body request)] - (let [params (json/decode is json-mapper)] + (with-open [reader (get-reader request)] + (let [params (json/read reader :key-fn json/read-kebab-key)] (-> request (assoc :body-params params) (update :params merge params)))) @@ -62,35 +63,33 @@ :else request))) - (handle-error [cause] + (handle-error [cause request] (cond (instance? RuntimeException cause) (if-let [cause (ex-cause cause)] - (handle-error cause) - (throw cause)) + (handle-error cause request) + (errors/handle cause request)) (instance? RequestTooBigException cause) (ex/raise :type :validation :code :request-body-too-large :hint (ex-message cause)) - (or (instance? JsonEOFException cause) - (instance? JsonParseException cause) - (instance? MismatchedInputException cause)) + (instance? java.io.EOFException cause) (ex/raise :type :validation :code :malformed-json :hint (ex-message cause) :cause cause) :else - (throw cause)))] + (errors/handle cause request)))] (fn [request] (if (= (rreq/method request) :post) - (let [request (ex/try! (process-request request))] - (if (ex/exception? request) - (handle-error request) - (handler request))) + (try + (-> request process-request handler) + (catch Throwable cause + (handle-error cause request))) (handler request))))) (def parse-request @@ -106,6 +105,12 @@ (def ^:const buffer-size (:xnio/buffer-size yt/defaults)) +(defn- write-json-value + [_ val] + (if (pmap/pointer-map? val) + [(pmap/get-id val) (meta val)] + val)) + (defn wrap-format-response [handler] (letfn [(transit-streamable-body [data opts] @@ -127,9 +132,11 @@ (reify rres/StreamableResponseBody (-write-body-to-stream [_ _ output-stream] (try - (with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)] - (json/write! bos data json-mapper)) - + (let [encode (or (-> data meta :encode/json) identity) + data (encode data)] + (with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)] + (with-open [^java.io.OutputStreamWriter writer (java.io.OutputStreamWriter. bos)] + (json/write writer data :key-fn json/write-camel-key :value-fn write-json-value)))) (catch java.io.IOException _) (catch Throwable cause (binding [l/*context* {:value data}] diff --git a/backend/src/app/http/session.clj b/backend/src/app/http/session.clj index 7ff6dfa0168598203416ffc4834ef874ccb9a5e3..bf8fea2dc87488dd6899b9a22212680cdfcd03ae 100644 --- a/backend/src/app/http/session.clj +++ b/backend/src/app/http/session.clj @@ -10,11 +10,13 @@ [app.common.data :as d] [app.common.logging :as l] [app.common.spec :as us] + [app.common.uri :as u] [app.config :as cf] [app.db :as db] [app.db.sql :as sql] [app.http.session.tasks :as-alias tasks] [app.main :as-alias main] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.util.time :as dt] [clojure.spec.alpha :as s] @@ -32,7 +34,7 @@ ;; A cookie that we can use to check from other sites of the same ;; domain if a user is authenticated. -(def default-authenticated-cookie-name "authenticated") +(def default-auth-data-cookie-name "auth-data") ;; Default value for cookie max-age (def default-cookie-max-age (dt/duration {:days 7})) @@ -132,13 +134,13 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (declare ^:private assign-auth-token-cookie) -(declare ^:private assign-authenticated-cookie) +(declare ^:private assign-auth-data-cookie) (declare ^:private clear-auth-token-cookie) -(declare ^:private clear-authenticated-cookie) +(declare ^:private clear-auth-data-cookie) (declare ^:private gen-token) (defn create-fn - [{:keys [::manager ::main/props]} profile-id] + [{:keys [::manager ::setup/props]} profile-id] (us/assert! ::manager manager) (us/assert! ::us/uuid profile-id) @@ -152,7 +154,7 @@ (l/trace :hint "create" :profile-id (str profile-id)) (-> response (assign-auth-token-cookie session) - (assign-authenticated-cookie session))))) + (assign-auth-data-cookie session))))) (defn delete-fn [{:keys [::manager]}] @@ -166,7 +168,7 @@ (assoc :status 204) (assoc :body nil) (clear-auth-token-cookie) - (clear-authenticated-cookie))))) + (clear-auth-data-cookie))))) (defn- gen-token [props {:keys [profile-id created-at]}] @@ -196,7 +198,7 @@ (neg? (compare default-renewal-max-age elapsed))))) (defn- wrap-soft-auth - [handler {:keys [::manager ::main/props]}] + [handler {:keys [::manager ::setup/props]}] (us/assert! ::manager manager) (letfn [(handle-request [request] (try @@ -228,7 +230,7 @@ (let [session (update! manager session)] (-> response (assign-auth-token-cookie session) - (assign-authenticated-cookie session))) + (assign-auth-data-cookie session))) response)))) (def soft-auth @@ -261,11 +263,11 @@ :secure secure?}] (update response :cookies assoc name cookie))) -(defn- assign-authenticated-cookie - [response {updated-at :updated-at}] +(defn- assign-auth-data-cookie + [response {profile-id :profile-id updated-at :updated-at}] (let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age) - domain (cf/get :authenticated-cookie-domain) - cname (cf/get :authenticated-cookie-name "authenticated") + domain (cf/get :auth-data-cookie-domain) + cname default-auth-data-cookie-name created-at (or updated-at (dt/now)) renewal (dt/plus created-at default-renewal-max-age) @@ -273,14 +275,17 @@ comment (str "Renewal at: " (dt/format-instant renewal :rfc1123)) secure? (contains? cf/flags :secure-session-cookies) + strict? (contains? cf/flags :strict-session-cookies) + cors? (contains? cf/flags :cors) cookie {:domain domain :expires expires :path "/" :comment comment - :value true - :same-site :strict + :value (u/map->query-string {:profile-id profile-id}) + :same-site (if cors? :none (if strict? :strict :lax)) :secure secure?}] + (cond-> response (string? domain) (update :cookies assoc cname cookie)))) @@ -290,10 +295,10 @@ (let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)] (update response :cookies assoc cname {:path "/" :value "" :max-age 0}))) -(defn- clear-authenticated-cookie +(defn- clear-auth-data-cookie [response] - (let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name) - domain (cf/get :authenticated-cookie-domain)] + (let [cname default-auth-data-cookie-name + domain (cf/get :auth-data-cookie-domain)] (cond-> response (string? domain) (update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0})))) diff --git a/backend/src/app/http/sse.clj b/backend/src/app/http/sse.clj index 86880109167fd8b2d93fbc6cd3ed7677d0f1d0c9..3da84322cbdd477dea66b17bf2c0944e91007440 100644 --- a/backend/src/app/http/sse.clj +++ b/backend/src/app/http/sse.clj @@ -61,6 +61,8 @@ (let [result (handler)] (events/tap :end result)) (catch Throwable cause + (l/err :hint "unexpected error on processing sse response" + :cause cause) (events/tap :error (errors/handle' cause request))) (finally (sp/close! events/*channel*) diff --git a/backend/src/app/loggers/audit.clj b/backend/src/app/loggers/audit.clj index 45c36334a78d043485c1ced58d7d24bf31652697..6b1e7ea28f7a25cdd8ee684fa275f963b8428701 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -9,42 +9,30 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.spec :as us] - [app.common.transit :as t] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.http :as-alias http] [app.http.access-token :as-alias actoken] - [app.http.client :as http.client] [app.loggers.audit.tasks :as-alias tasks] [app.loggers.webhooks :as-alias webhooks] - [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.retry :as rtry] - [app.tokens :as tokens] + [app.setup :as-alias setup] + [app.util.inet :as inet] [app.util.services :as-alias sv] [app.util.time :as dt] [app.worker :as wrk] [clojure.spec.alpha :as s] [cuerdas.core :as str] - [integrant.core :as ig] - [lambdaisland.uri :as u] - [promesa.exec :as px] - [ring.request :as rreq])) + [integrant.core :as ig])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn parse-client-ip - [request] - (or (some-> (rreq/get-header request "x-forwarded-for") (str/split ",") first) - (rreq/get-header request "x-real-ip") - (some-> (rreq/remote-addr request) str))) - (defn extract-utm-params "Extracts additional data from params and namespace them under `penpot` ns." @@ -59,8 +47,7 @@ (assoc (->> sk str/kebab (keyword "penpot")) v))))] (reduce-kv process-param {} params))) -(def ^:private - profile-props +(def profile-props [:id :is-active :is-muted @@ -93,8 +80,19 @@ (remove #(contains? reserved-props (key %)))) props)) -;; --- SPECS +(defn event-from-rpc-params + "Create a base event skeleton with pre-filled some important + data that can be extracted from RPC params object" + [params] + (let [context {:external-session-id (::rpc/external-session-id params) + :external-event-origin (::rpc/external-event-origin params) + :triggered-by (::rpc/handler-name params)}] + {::type "action" + ::profile-id (::rpc/profile-id params) + ::ip-addr (::rpc/ip-addr params) + ::context (d/without-nils context)})) +;; --- SPECS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; COLLECTOR @@ -147,24 +145,31 @@ (::rpc/profile-id params) uuid/zero) - props (-> (or (::replace-props resultm) - (-> params - (merge (::props resultm)) - (dissoc :profile-id) - (dissoc :type))) + session-id (get params ::rpc/external-session-id) + event-origin (get params ::rpc/external-event-origin) + props (-> (or (::replace-props resultm) + (-> params + (merge (::props resultm)) + (dissoc :profile-id) + (dissoc :type))) - (clean-props)) + (clean-props)) token-id (::actoken/id request) - context (d/without-nils - {:access-token-id (some-> token-id str)})] + context (-> (::context resultm) + (assoc :external-session-id session-id) + (assoc :external-event-origin event-origin) + (assoc :access-token-id (some-> token-id str)) + (d/without-nils)) + + ip-addr (inet/parse-request request)] {::type (or (::type resultm) (::rpc/type cfg)) ::name (or (::name resultm) (::sv/name mdata)) ::profile-id profile-id - ::ip-addr (some-> request parse-client-ip) + ::ip-addr ip-addr ::props props ::context context @@ -186,29 +191,58 @@ (::webhooks/event? resultm) false)})) -(defn- handle-event! - [cfg event] +(defn- event->params + [event] (let [params {:id (uuid/next) :name (::name event) :type (::type event) :profile-id (::profile-id event) :ip-addr (::ip-addr event) - :context (::context event) - :props (::props event)}] + :context (::context event {}) + :props (::props event {}) + :source "backend"} + tnow (::tracked-at event)] + + (cond-> params + (some? tnow) + (assoc :tracked-at tnow)))) + +(defn- append-audit-entry! + [cfg params] + (let [params (-> params + (update :props db/tjson) + (update :context db/tjson) + (update :ip-addr db/inet))] + (db/insert! cfg :audit-log params))) + +(defn- handle-event! + [cfg event] + (let [params (event->params event) + tnow (dt/now)] (when (contains? cf/flags :audit-log) ;; NOTE: this operation may cause primary key conflicts on inserts ;; because of the timestamp precission (two concurrent requests), in ;; this case we just retry the operation. - (let [tnow (dt/now) - params (-> params + (let [params (-> params + (assoc :created-at tnow) + (update :tracked-at #(or % tnow)))] + (append-audit-entry! cfg params))) + + (when (and (or (contains? cf/flags :telemetry) + (cf/get :telemetry-enabled)) + (not (contains? cf/flags :audit-log))) + ;; NOTE: this operation may cause primary key conflicts on inserts + ;; because of the timestamp precission (two concurrent requests), in + ;; this case we just retry the operation. + ;; + ;; NOTE: this is only executed when general audit log is disabled + (let [params (-> params (assoc :created-at tnow) - (assoc :tracked-at tnow) - (update :props db/tjson) - (update :context db/tjson) - (update :ip-addr db/inet) - (assoc :source "backend"))] - (db/insert! cfg :audit-log params))) + (update :tracked-at #(or % tnow)) + (assoc :props {}) + (assoc :context {}))] + (append-audit-entry! cfg params))) (when (and (contains? cf/flags :webhooks) (::webhooks/event? event)) @@ -221,25 +255,25 @@ :else label) dedupe? (boolean (and batch-key batch-timeout))] - (wrk/submit! ::wrk/conn (::db/conn cfg) - ::wrk/task :process-webhook-event - ::wrk/queue :webhooks - ::wrk/max-retries 0 - ::wrk/delay (or batch-timeout 0) - ::wrk/dedupe dedupe? - ::wrk/label label - - ::webhooks/event - (-> params - (dissoc :ip-addr) - (dissoc :type))))) + (wrk/submit! (-> cfg + (assoc ::wrk/task :process-webhook-event) + (assoc ::wrk/queue :webhooks) + (assoc ::wrk/max-retries 0) + (assoc ::wrk/delay (or batch-timeout 0)) + (assoc ::wrk/dedupe dedupe?) + (assoc ::wrk/label label) + (assoc ::wrk/params (-> params + (dissoc :source) + (dissoc :context) + (dissoc :ip-addr) + (dissoc :type))))))) params)) (defn submit! "Submit audit event to the collector." - [cfg params] + [cfg event] (try - (let [event (d/without-nils params) + (let [event (d/without-nils event) cfg (-> cfg (assoc ::rtry/when rtry/conflict-exception?) (assoc ::rtry/max-retries 6) @@ -249,136 +283,17 @@ (catch Throwable cause (l/error :hint "unexpected error processing event" :cause cause)))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; TASK: ARCHIVE -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; This is a task responsible to send the accumulated events to -;; external service for archival. - -(declare archive-events) - -(s/def ::tasks/uri ::us/string) - -(defmethod ig/pre-init-spec ::tasks/archive-task [_] - (s/keys :req [::db/pool ::main/props ::http.client/client])) - -(defmethod ig/init-key ::tasks/archive - [_ cfg] - (fn [params] - ;; NOTE: this let allows overwrite default configured values from - ;; the repl, when manually invoking the task. - (let [enabled (or (contains? cf/flags :audit-log-archive) - (:enabled params false)) - uri (cf/get :audit-log-archive-uri) - uri (or uri (:uri params)) - cfg (assoc cfg ::uri uri)] - - (when (and enabled (not uri)) - (ex/raise :type :internal - :code :task-not-configured - :hint "archive task not configured, missing uri")) - - (when enabled - (loop [total 0] - (let [n (archive-events cfg)] - (if n - (do - (px/sleep 100) - (recur (+ total ^long n))) - (when (pos? total) - (l/debug :hint "events archived" :total total))))))))) - -(def ^:private sql:retrieve-batch-of-audit-log - "select * - from audit_log - where archived_at is null - order by created_at asc - limit 128 - for update skip locked;") - -(defn archive-events - [{:keys [::db/pool ::uri] :as cfg}] - (letfn [(decode-row [{:keys [props ip-addr context] :as row}] - (cond-> row - (db/pgobject? props) - (assoc :props (db/decode-transit-pgobject props)) - - (db/pgobject? context) - (assoc :context (db/decode-transit-pgobject context)) - - (db/pgobject? ip-addr "inet") - (assoc :ip-addr (db/decode-inet ip-addr)))) - - (row->event [row] - (select-keys row [:type - :name - :source - :created-at - :tracked-at - :profile-id - :ip-addr - :props - :context])) - - (send [events] - (let [token (tokens/generate (::main/props cfg) - {:iss "authentication" - :iat (dt/now) - :uid uuid/zero}) - body (t/encode {:events events}) - headers {"content-type" "application/transit+json" - "origin" (cf/get :public-uri) - "cookie" (u/map->query-string {:auth-token token})} - params {:uri uri - :timeout 6000 - :method :post - :headers headers - :body body} - resp (http.client/req! cfg params {:sync? true})] - (if (= (:status resp) 204) - true - (do - (l/error :hint "unable to archive events" - :resp-status (:status resp) - :resp-body (:body resp)) - false)))) - - (mark-as-archived [conn rows] - (db/exec-one! conn ["update audit_log set archived_at=now() where id = ANY(?)" - (->> (map :id rows) - (db/create-array conn "uuid"))]))] - - (db/with-atomic [conn pool] - (let [rows (db/exec! conn [sql:retrieve-batch-of-audit-log]) - xform (comp (map decode-row) - (map row->event)) - events (into [] xform rows)] - (when-not (empty? events) - (l/trace :hint "archive events chunk" :uri uri :events (count events)) - (when (send events) - (mark-as-archived conn rows) - (count events))))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; GC Task -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(def ^:private sql:clean-archived - "delete from audit_log - where archived_at is not null") - -(defn- clean-archived - [{:keys [::db/pool]}] - (let [result (db/exec-one! pool [sql:clean-archived]) - result (:next.jdbc/update-count result)] - (l/debug :hint "delete archived audit log entries" :deleted result) - result)) - -(defmethod ig/pre-init-spec ::tasks/gc [_] - (s/keys :req [::db/pool])) - -(defmethod ig/init-key ::tasks/gc - [_ cfg] - (fn [_] - (clean-archived cfg))) +(defn insert! + "Submit audit event to the collector, intended to be used only from + command line helpers because this skips all webhooks and telemetry + logic." + [cfg event] + (when (contains? cf/flags :audit-log) + (let [event (d/without-nils event)] + (us/verify! ::event event) + (db/run! cfg (fn [cfg] + (let [tnow (dt/now) + params (-> (event->params event) + (assoc :created-at tnow) + (update :tracked-at #(or % tnow)))] + (append-audit-entry! cfg params))))))) diff --git a/backend/src/app/loggers/audit/archive_task.clj b/backend/src/app/loggers/audit/archive_task.clj new file mode 100644 index 0000000000000000000000000000000000000000..046fb8068dbb9a58c6cd9d1657fd5b31b2d88b05 --- /dev/null +++ b/backend/src/app/loggers/audit/archive_task.clj @@ -0,0 +1,140 @@ +;; 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 app.loggers.audit.archive-task + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.transit :as t] + [app.common.uuid :as uuid] + [app.config :as cf] + [app.db :as db] + [app.http.client :as http] + [app.setup :as-alias setup] + [app.tokens :as tokens] + [app.util.time :as dt] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [lambdaisland.uri :as u] + [promesa.exec :as px])) + +;; This is a task responsible to send the accumulated events to +;; external service for archival. + +(defn- decode-row + [{:keys [props ip-addr context] :as row}] + (cond-> row + (db/pgobject? props) + (assoc :props (db/decode-transit-pgobject props)) + + (db/pgobject? context) + (assoc :context (db/decode-transit-pgobject context)) + + (db/pgobject? ip-addr "inet") + (assoc :ip-addr (db/decode-inet ip-addr)))) + +(def ^:private event-keys + [:type + :name + :source + :created-at + :tracked-at + :profile-id + :ip-addr + :props + :context]) + +(defn- row->event + [row] + (select-keys row event-keys)) + +(defn- send! + [{:keys [::uri] :as cfg} events] + (let [token (tokens/generate (::setup/props cfg) + {:iss "authentication" + :iat (dt/now) + :uid uuid/zero}) + body (t/encode {:events events}) + headers {"content-type" "application/transit+json" + "origin" (cf/get :public-uri) + "cookie" (u/map->query-string {:auth-token token})} + params {:uri uri + :timeout 12000 + :method :post + :headers headers + :body body} + resp (http/req! cfg params)] + (if (= (:status resp) 204) + true + (do + (l/error :hint "unable to archive events" + :resp-status (:status resp) + :resp-body (:body resp)) + false)))) + +(defn- mark-archived! + [{:keys [::db/conn]} rows] + (let [ids (db/create-array conn "uuid" (map :id rows))] + (db/exec-one! conn ["update audit_log set archived_at=now() where id = ANY(?)" ids]))) + +(def ^:private xf:create-event + (comp (map decode-row) + (map row->event))) + +(def ^:private sql:get-audit-log-chunk + "SELECT * + FROM audit_log + WHERE archived_at is null + ORDER BY created_at ASC + LIMIT 128 + FOR UPDATE + SKIP LOCKED") + +(defn- get-event-rows + [{:keys [::db/conn] :as cfg}] + (->> (db/exec! conn [sql:get-audit-log-chunk]) + (not-empty))) + +(defn- archive-events! + [{:keys [::uri] :as cfg}] + (db/tx-run! cfg (fn [cfg] + (when-let [rows (get-event-rows cfg)] + (let [events (into [] xf:create-event rows)] + (l/trc :hint "archive events chunk" :uri uri :events (count events)) + (when (send! cfg events) + (mark-archived! cfg rows) + (count events))))))) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool ::setup/props ::http/client])) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [params] + ;; NOTE: this let allows overwrite default configured values from + ;; the repl, when manually invoking the task. + (let [enabled (or (contains? cf/flags :audit-log-archive) + (:enabled params false)) + + uri (cf/get :audit-log-archive-uri) + uri (or uri (:uri params)) + cfg (assoc cfg ::uri uri)] + + (when (and enabled (not uri)) + (ex/raise :type :internal + :code :task-not-configured + :hint "archive task not configured, missing uri")) + + (when enabled + (loop [total 0] + (if-let [n (archive-events! cfg)] + (do + (px/sleep 100) + (recur (+ total ^long n))) + + (when (pos? total) + (l/dbg :hint "events archived" :total total)))))))) + diff --git a/backend/src/app/loggers/audit/gc_task.clj b/backend/src/app/loggers/audit/gc_task.clj new file mode 100644 index 0000000000000000000000000000000000000000..7f94217a4958687d2c1179ae8085681fdc2036b1 --- /dev/null +++ b/backend/src/app/loggers/audit/gc_task.clj @@ -0,0 +1,31 @@ +;; 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 app.loggers.audit.gc-task + (:require + [app.common.logging :as l] + [app.db :as db] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(def ^:private sql:clean-archived + "DELETE FROM audit_log + WHERE archived_at IS NOT NULL") + +(defn- clean-archived! + [{:keys [::db/pool]}] + (let [result (db/exec-one! pool [sql:clean-archived]) + result (db/get-update-count result)] + (l/debug :hint "delete archived audit log entries" :deleted result) + result)) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool])) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [_] + (clean-archived! cfg))) diff --git a/backend/src/app/loggers/mattermost.clj b/backend/src/app/loggers/mattermost.clj index 8a2117e23cebb3203ad109ebf249ceed7e211057..32fff185be784445e98e6b15085508e68934cf4c 100644 --- a/backend/src/app/loggers/mattermost.clj +++ b/backend/src/app/loggers/mattermost.clj @@ -23,17 +23,20 @@ (defn- send-mattermost-notification! [cfg {:keys [id public-uri] :as report}] + + (let [text (str "Exception: " public-uri "/dbg/error/" id " " (when-let [pid (:profile-id report)] (str "(pid: #uuid-" pid ")")) "\n" - "```\n" - "- host: `" (:host report) "`\n" - "- tenant: `" (:tenant report) "`\n" + "- host: #" (:host report) "\n" + "- tenant: #" (:tenant report) "\n" + "- logger: #" (:logger report) "\n" "- request-path: `" (:request-path report) "`\n" "- frontend-version: `" (:frontend-version report) "`\n" "- backend-version: `" (:backend-version report) "`\n" "\n" + "```\n" "Trace:\n" (:trace report) "```") @@ -60,6 +63,7 @@ :frontend-version (:version/frontend context) :profile-id (:request/profile-id context) :request-path (:request/path context) + :logger (::l/logger record) :trace (ex/format-throwable cause :detail? false :header? false)}) (defn handle-event diff --git a/backend/src/app/loggers/webhooks.clj b/backend/src/app/loggers/webhooks.clj index 00ebd3f383da90609e7330dcd23b354ad5c1d578..371b627bb43e2906edcd6372a5b1c35a99f217bf 100644 --- a/backend/src/app/loggers/webhooks.clj +++ b/backend/src/app/loggers/webhooks.clj @@ -15,9 +15,9 @@ [app.config :as cf] [app.db :as db] [app.http.client :as http] - [app.util.json :as json] [app.util.time :as dt] [app.worker :as wrk] + [clojure.data.json :as json] [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig])) @@ -64,35 +64,28 @@ (s/keys :req [::db/pool])) (defmethod ig/init-key ::process-event-handler - [_ {:keys [::db/pool] :as cfg}] + [_ cfg] (fn [{:keys [props] :as task}] - (let [event (::event props)] - - (l/debug :hint "process webhook event" - :name (:name event)) - - (when-let [items (lookup-webhooks cfg event)] - (l/trace :hint "webhooks found for event" :total (count items)) - - (db/with-atomic [conn pool] - (doseq [item items] - (wrk/submit! ::wrk/conn conn - ::wrk/task :run-webhook - ::wrk/queue :webhooks - ::wrk/max-retries 3 - ::event event - ::config item))))))) - + (l/dbg :hint "process webhook event" :name (:name props)) + + (when-let [items (lookup-webhooks cfg props)] + (l/trc :hint "webhooks found for event" :total (count items)) + (db/tx-run! cfg (fn [cfg] + (doseq [item items] + (wrk/submit! (-> cfg + (assoc ::wrk/task :run-webhook) + (assoc ::wrk/queue :webhooks) + (assoc ::wrk/max-retries 3) + (assoc ::wrk/params {:event props + :config item}))))))))) ;; --- RUN (declare interpret-exception) (declare interpret-response) -(def ^:private json-mapper - (json/mapper - {:encode-key-fn str/camel - :decode-key-fn (comp keyword str/kebab) - :pretty true})) +(def json-write-opts + {:key-fn str/camel + :indent true}) (defmethod ig/pre-init-spec ::run-webhook-handler [_] (s/keys :req [::http/client ::db/pool])) @@ -132,19 +125,19 @@ :rsp-data (db/tjson rsp)}))] (fn [{:keys [props] :as task}] - (let [event (::event props) - whook (::config props) + (let [event (:event props) + whook (:config props) body (case (:mtype whook) - "application/json" (json/encode-str event json-mapper) + "application/json" (json/write-str event json-write-opts) "application/transit+json" (t/encode-str event) "application/x-www-form-urlencoded" (uri/map->query-string event))] - (l/debug :hint "run webhook" - :event-name (:name event) - :webhook-id (:id whook) - :webhook-uri (:uri whook) - :webhook-mtype (:mtype whook)) + (l/dbg :hint "run webhook" + :event-name (:name event) + :webhook-id (:id whook) + :webhook-uri (:uri whook) + :webhook-mtype (:mtype whook)) (let [req {:uri (:uri whook) :headers {"content-type" (:mtype whook) @@ -162,8 +155,8 @@ (report-delivery! whook req nil err) (update-webhook! whook err) (when (= err "unknown") - (l/error :hint "unknown error on webhook request" - :cause cause)))))))))) + (l/err :hint "unknown error on webhook request" + :cause cause)))))))))) (defn interpret-response [{:keys [status] :as response}] diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 47e43f5cf718ec8095f2a022b9d1676dfe975615..314732d9fcbe458417fff7952ab8c9c305d7fe09 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -21,10 +21,10 @@ [app.http.session :as-alias session] [app.http.session.tasks :as-alias session.tasks] [app.http.websocket :as http.ws] - [app.loggers.audit.tasks :as-alias audit.tasks] [app.loggers.webhooks :as-alias webhooks] [app.metrics :as-alias mtx] [app.metrics.definition :as-alias mdef] + [app.migrations.v2 :as migrations.v2] [app.msgbus :as-alias mbus] [app.redis :as-alias rds] [app.rpc :as-alias rpc] @@ -102,13 +102,13 @@ {::mdef/name "penpot_tasks_timing" ::mdef/help "Background tasks timing (milliseconds)." ::mdef/labels ["name"] - ::mdef/type :summary} + ::mdef/type :histogram} :redis-eval-timing {::mdef/name "penpot_redis_eval_timing" ::mdef/help "Redis EVAL commands execution timings (ms)" ::mdef/labels ["name"] - ::mdef/type :summary} + ::mdef/type :histogram} :rpc-climit-queue {::mdef/name "penpot_rpc_climit_queue" @@ -126,7 +126,7 @@ {::mdef/name "penpot_rpc_climit_timing" ::mdef/help "Summary of the time between queuing and executing on the CLIMIT" ::mdef/labels ["name"] - ::mdef/type :summary} + ::mdef/type :histogram} :audit-http-handler-queue-size {::mdef/name "penpot_audit_http_handler_queue_size" @@ -144,7 +144,7 @@ {::mdef/name "penpot_audit_http_handler_timing" ::mdef/help "Summary of the time between queuing and executing on the audit log http handler" ::mdef/labels [] - ::mdef/type :summary} + ::mdef/type :histogram} :executors-active-threads {::mdef/name "penpot_executors_active_threads" @@ -221,7 +221,7 @@ {::db/pool (ig/ref ::db/pool)} ::http.awsns/routes - {::props (ig/ref ::setup/props) + {::setup/props (ig/ref ::setup/props) ::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client)} @@ -254,7 +254,7 @@ {::http.client/client (ig/ref ::http.client/client)} ::oidc.providers/gitlab - {} + {::http.client/client (ig/ref ::http.client/client)} ::oidc.providers/generic {::http.client/client (ig/ref ::http.client/client)} @@ -262,19 +262,21 @@ ::oidc/routes {::http.client/client (ig/ref ::http.client/client) ::db/pool (ig/ref ::db/pool) - ::props (ig/ref ::setup/props) + ::setup/props (ig/ref ::setup/props) ::oidc/providers {:google (ig/ref ::oidc.providers/google) :github (ig/ref ::oidc.providers/github) :gitlab (ig/ref ::oidc.providers/gitlab) :oidc (ig/ref ::oidc.providers/generic)} - ::session/manager (ig/ref ::session/manager)} + ::session/manager (ig/ref ::session/manager) + ::email/blacklist (ig/ref ::email/blacklist) + ::email/whitelist (ig/ref ::email/whitelist)} :app.http/router {::session/manager (ig/ref ::session/manager) ::db/pool (ig/ref ::db/pool) ::rpc/routes (ig/ref ::rpc/routes) ::rpc.doc/routes (ig/ref ::rpc.doc/routes) - ::props (ig/ref ::setup/props) + ::setup/props (ig/ref ::setup/props) ::mtx/routes (ig/ref ::mtx/routes) ::oidc/routes (ig/ref ::oidc/routes) ::http.debug/routes (ig/ref ::http.debug/routes) @@ -286,7 +288,7 @@ {::db/pool (ig/ref ::db/pool) ::session/manager (ig/ref ::session/manager) ::sto/storage (ig/ref ::sto/storage) - ::props (ig/ref ::setup/props)} + ::setup/props (ig/ref ::setup/props)} ::http.ws/routes {::db/pool (ig/ref ::db/pool) @@ -322,7 +324,10 @@ ::rpc/climit (ig/ref ::rpc/climit) ::rpc/rlimit (ig/ref ::rpc/rlimit) ::setup/templates (ig/ref ::setup/templates) - ::props (ig/ref ::setup/props)} + ::setup/props (ig/ref ::setup/props) + + ::email/blacklist (ig/ref ::email/blacklist) + ::email/whitelist (ig/ref ::email/whitelist)} :app.rpc.doc/routes {:methods (ig/ref :app.rpc/methods)} @@ -331,29 +336,38 @@ {::rpc/methods (ig/ref :app.rpc/methods) ::db/pool (ig/ref ::db/pool) ::session/manager (ig/ref ::session/manager) - ::props (ig/ref ::setup/props)} + ::setup/props (ig/ref ::setup/props)} ::wrk/registry {::mtx/metrics (ig/ref ::mtx/metrics) ::wrk/tasks {:sendmail (ig/ref ::email/handler) :objects-gc (ig/ref :app.tasks.objects-gc/handler) - :orphan-teams-gc (ig/ref :app.tasks.orphan-teams-gc/handler) :file-gc (ig/ref :app.tasks.file-gc/handler) + :file-gc-scheduler (ig/ref :app.tasks.file-gc-scheduler/handler) + :offload-file-data (ig/ref :app.tasks.offload-file-data/handler) :file-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler) :tasks-gc (ig/ref :app.tasks.tasks-gc/handler) :telemetry (ig/ref :app.tasks.telemetry/handler) :storage-gc-deleted (ig/ref ::sto.gc-deleted/handler) :storage-gc-touched (ig/ref ::sto.gc-touched/handler) :session-gc (ig/ref ::session.tasks/gc) - :audit-log-archive (ig/ref ::audit.tasks/archive) - :audit-log-gc (ig/ref ::audit.tasks/gc) + :audit-log-archive (ig/ref :app.loggers.audit.archive-task/handler) + :audit-log-gc (ig/ref :app.loggers.audit.gc-task/handler) + :delete-object + (ig/ref :app.tasks.delete-object/handler) :process-webhook-event (ig/ref ::webhooks/process-event-handler) :run-webhook (ig/ref ::webhooks/run-webhook-handler)}} + ::email/blacklist + {} + + ::email/whitelist + {} + ::email/sendmail {::email/host (cf/get :smtp-host) ::email/port (cf/get :smtp-port) @@ -375,20 +389,28 @@ {::db/pool (ig/ref ::db/pool) ::sto/storage (ig/ref ::sto/storage)} - :app.tasks.orphan-teams-gc/handler - {::db/pool (ig/ref ::db/pool)} + :app.tasks.delete-object/handler + {::db/pool (ig/ref ::db/pool)} :app.tasks.file-gc/handler {::db/pool (ig/ref ::db/pool) ::sto/storage (ig/ref ::sto/storage)} - :app.tasks.file-xlog-gc/handler + :app.tasks.file-gc-scheduler/handler {::db/pool (ig/ref ::db/pool)} + :app.tasks.offload-file-data/handler + {::db/pool (ig/ref ::db/pool) + ::sto/storage (ig/ref ::sto/storage)} + + :app.tasks.file-xlog-gc/handler + {::db/pool (ig/ref ::db/pool) + ::sto/storage (ig/ref ::sto/storage)} + :app.tasks.telemetry/handler {::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client) - ::props (ig/ref ::setup/props)} + ::setup/props (ig/ref ::setup/props)} [::srepl/urepl ::srepl/server] {::srepl/port (cf/get :urepl-port 6062) @@ -402,7 +424,7 @@ ::setup/props {::db/pool (ig/ref ::db/pool) - ::key (cf/get :secret-key) + ::setup/key (cf/get :secret-key) ;; NOTE: this dependency is only necessary for proper initialization ordering, props ;; module requires the migrations to run before initialize. @@ -411,12 +433,12 @@ ::svgo/optimizer {} - ::audit.tasks/archive - {::props (ig/ref ::setup/props) + :app.loggers.audit.archive-task/handler + {::setup/props (ig/ref ::setup/props) ::db/pool (ig/ref ::db/pool) ::http.client/client (ig/ref ::http.client/client)} - ::audit.tasks/gc + :app.loggers.audit.gc-task/handler {::db/pool (ig/ref ::db/pool)} ::webhooks/process-event-handler @@ -436,17 +458,28 @@ ::sto/storage {::db/pool (ig/ref ::db/pool) ::sto/backends - {:assets-s3 (ig/ref [::assets :app.storage.s3/backend]) - :assets-fs (ig/ref [::assets :app.storage.fs/backend])}} - - [::assets :app.storage.s3/backend] - {::sto.s3/region (cf/get :storage-assets-s3-region) - ::sto.s3/endpoint (cf/get :storage-assets-s3-endpoint) - ::sto.s3/bucket (cf/get :storage-assets-s3-bucket) - ::sto.s3/io-threads (cf/get :storage-assets-s3-io-threads)} - - [::assets :app.storage.fs/backend] - {::sto.fs/directory (cf/get :storage-assets-fs-directory)}}) + {:s3 (ig/ref :app.storage.s3/backend) + :fs (ig/ref :app.storage.fs/backend) + + ;; LEGACY (should not be removed, can only be removed after an + ;; explicit migration because the database objects/rows will + ;; still reference the old names). + :assets-s3 (ig/ref :app.storage.s3/backend) + :assets-fs (ig/ref :app.storage.fs/backend)}} + + :app.storage.s3/backend + {::sto.s3/region (or (cf/get :storage-assets-s3-region) + (cf/get :objects-storage-s3-region)) + ::sto.s3/endpoint (or (cf/get :storage-assets-s3-endpoint) + (cf/get :objects-storage-s3-endpoint)) + ::sto.s3/bucket (or (cf/get :storage-assets-s3-bucket) + (cf/get :objects-storage-s3-bucket)) + ::sto.s3/io-threads (or (cf/get :storage-assets-s3-io-threads) + (cf/get :objects-storage-s3-io-threads))} + + :app.storage.fs/backend + {::sto.fs/directory (or (cf/get :storage-assets-fs-directory) + (cf/get :objects-storage-fs-directory))}}) (def worker-config @@ -463,9 +496,6 @@ {:cron #app/cron "0 0 0 * * ?" ;; daily :task :objects-gc} - {:cron #app/cron "0 0 0 * * ?" ;; daily - :task :orphan-teams-gc} - {:cron #app/cron "0 0 0 * * ?" ;; daily :task :storage-gc-deleted} @@ -476,7 +506,7 @@ :task :tasks-gc} {:cron #app/cron "0 0 2 * * ?" ;; daily - :task :file-gc} + :task :file-gc-scheduler} {:cron #app/cron "0 30 */3,23 * * ?" :task :telemetry} @@ -494,7 +524,7 @@ ::mtx/metrics (ig/ref ::mtx/metrics) ::db/pool (ig/ref ::db/pool)} - [::default ::wrk/worker] + [::default ::wrk/runner] {::wrk/parallelism (cf/get ::worker-default-parallelism 1) ::wrk/queue :default ::rds/redis (ig/ref ::rds/redis) @@ -502,7 +532,7 @@ ::mtx/metrics (ig/ref ::mtx/metrics) ::db/pool (ig/ref ::db/pool)} - [::webhook ::wrk/worker] + [::webhook ::wrk/runner] {::wrk/parallelism (cf/get ::worker-webhook-parallelism 1) ::wrk/queue :webhooks ::rds/redis (ig/ref ::rds/redis) @@ -515,6 +545,7 @@ (defn start [] + (cf/validate!) (ig/load-namespaces (merge system-config worker-config)) (alter-var-root #'system (fn [sys] (when sys (ig/halt! sys)) @@ -528,6 +559,15 @@ :worker? (contains? cf/flags :backend-worker) :version (:full cf/version))) +(defn start-custom + [config] + (ig/load-namespaces config) + (alter-var-root #'system (fn [sys] + (when sys (ig/halt! sys)) + (-> config + (ig/prep) + (ig/init))))) + (defn stop [] (alter-var-root #'system (fn [sys] @@ -574,6 +614,11 @@ (nrepl/start-server :bind "0.0.0.0" :port 6064 :handler cider-nrepl-handler)) (start) + + (when (contains? cf/flags :v2-migration) + (px/sleep 5000) + (migrations.v2/migrate app.main/system)) + (deref p)) (catch Throwable cause (binding [*out* *err*] diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index 8d2315352a7c7a1287a1f61feae216ece087e44a..4c8ae28aebf1c41885852440d56ebd8660d2243a 100644 --- a/backend/src/app/media.clj +++ b/backend/src/app/media.clj @@ -11,7 +11,6 @@ [app.common.exceptions :as ex] [app.common.media :as cm] [app.common.schema :as sm] - [app.common.schema.generators :as sg] [app.common.schema.openapi :as-alias oapi] [app.common.spec :as us] [app.common.svg :as csvg] @@ -47,22 +46,10 @@ (s/keys :req-un [::path] :opt-un [::mtype])) -(sm/def! ::fs/path - {:type ::fs/path - :pred fs/path? - :type-properties - {:title "path" - :description "filesystem path" - :error/message "expected a valid fs path instance" - :gen/gen (sg/generator :string) - ::oapi/type "string" - ::oapi/format "unix-path" - ::oapi/decode fs/path}}) - -(sm/def! ::upload +(sm/register! ::upload [:map {:title "Upload"} [:filename :string] - [:size :int] + [:size ::sm/int] [:path ::fs/path] [:mtype {:optional true} :string] [:headers {:optional true} @@ -326,17 +313,3 @@ (= stype :ttf) (-> (assoc "font/otf" (ttf->otf sfnt)) (assoc "font/ttf" sfnt))))))))) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Utility functions -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn configure-assets-storage - "Given storage map, returns a storage configured with the appropriate - backend for assets and optional connection attached." - ([storage] - (assoc storage ::sto/backend (cf/get :assets-storage-backend :assets-fs))) - ([storage pool-or-conn] - (-> (configure-assets-storage storage) - (assoc ::db/pool-or-conn pool-or-conn)))) diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 87f3d90b99f42ad429589e5a78a6cfee7f0bd95a..5226e5152f3921b0944dc33e5d322e714f719e50 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -376,7 +376,43 @@ :fn (mg/resource "app/migrations/sql/0118-mod-task-table.sql")} {:name "0119-mod-file-table" - :fn (mg/resource "app/migrations/sql/0119-mod-file-table.sql")}]) + :fn (mg/resource "app/migrations/sql/0119-mod-file-table.sql")} + + {:name "0120-mod-audit-log-table" + :fn (mg/resource "app/migrations/sql/0120-mod-audit-log-table.sql")} + + {:name "0121-mod-file-data-fragment-table" + :fn (mg/resource "app/migrations/sql/0121-mod-file-data-fragment-table.sql")} + + {:name "0122-mod-file-table" + :fn (mg/resource "app/migrations/sql/0122-mod-file-table.sql")} + + {:name "0122-mod-file-data-fragment-table" + :fn (mg/resource "app/migrations/sql/0122-mod-file-data-fragment-table.sql")} + + {:name "0123-mod-file-change-table" + :fn (mg/resource "app/migrations/sql/0123-mod-file-change-table.sql")} + + {:name "0124-mod-profile-table" + :fn (mg/resource "app/migrations/sql/0124-mod-profile-table.sql")} + + {:name "0125-mod-file-table" + :fn (mg/resource "app/migrations/sql/0125-mod-file-table.sql")} + + {:name "0126-add-team-access-request-table" + :fn (mg/resource "app/migrations/sql/0126-add-team-access-request-table.sql")} + + {:name "0127-mod-storage-object-table" + :fn (mg/resource "app/migrations/sql/0127-mod-storage-object-table.sql")} + + {:name "0128-mod-task-table" + :fn (mg/resource "app/migrations/sql/0128-mod-task-table.sql")} + + {:name "0129-mod-file-change-table" + :fn (mg/resource "app/migrations/sql/0129-mod-file-change-table.sql")} + + {:name "0130-mod-file-change-table" + :fn (mg/resource "app/migrations/sql/0130-mod-file-change-table.sql")}]) (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0120-mod-audit-log-table.sql b/backend/src/app/migrations/sql/0120-mod-audit-log-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..e9b4b83c511aea6111028f460b8a554b52e31d06 --- /dev/null +++ b/backend/src/app/migrations/sql/0120-mod-audit-log-table.sql @@ -0,0 +1,11 @@ +CREATE TABLE new_audit_log (LIKE audit_log INCLUDING ALL); +INSERT INTO new_audit_log SELECT * FROM audit_log; +ALTER TABLE audit_log RENAME TO old_audit_log; +ALTER TABLE new_audit_log RENAME TO audit_log; +DROP TABLE old_audit_log; + +DROP INDEX new_audit_log_id_archived_at_idx; +ALTER TABLE audit_log DROP CONSTRAINT new_audit_log_pkey; +ALTER TABLE audit_log ADD PRIMARY KEY (id); +ALTER TABLE audit_log ALTER COLUMN created_at SET DEFAULT now(); +ALTER TABLE audit_log ALTER COLUMN tracked_at SET DEFAULT now(); diff --git a/backend/src/app/migrations/sql/0121-mod-file-data-fragment-table.sql b/backend/src/app/migrations/sql/0121-mod-file-data-fragment-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..bd30e8cb849fd168028146bc6e8e8aa176c23c5f --- /dev/null +++ b/backend/src/app/migrations/sql/0121-mod-file-data-fragment-table.sql @@ -0,0 +1,8 @@ +ALTER TABLE file_data_fragment + ADD COLUMN data bytea NULL; + +UPDATE file_data_fragment + SET data = content; + +ALTER TABLE file_data_fragment + DROP COLUMN content; diff --git a/backend/src/app/migrations/sql/0122-mod-file-data-fragment-table.sql b/backend/src/app/migrations/sql/0122-mod-file-data-fragment-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..87955aea8feccaac47ec69f4c02ae510c0c60a95 --- /dev/null +++ b/backend/src/app/migrations/sql/0122-mod-file-data-fragment-table.sql @@ -0,0 +1,6 @@ +ALTER TABLE file_data_fragment + ADD COLUMN data_backend text NULL, + ADD COLUMN data_ref_id uuid NULL; + +CREATE INDEX IF NOT EXISTS file_data_fragment__data_ref_id__idx + ON file_data_fragment (data_ref_id); diff --git a/backend/src/app/migrations/sql/0122-mod-file-fragment-table.sql b/backend/src/app/migrations/sql/0122-mod-file-fragment-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..87955aea8feccaac47ec69f4c02ae510c0c60a95 --- /dev/null +++ b/backend/src/app/migrations/sql/0122-mod-file-fragment-table.sql @@ -0,0 +1,6 @@ +ALTER TABLE file_data_fragment + ADD COLUMN data_backend text NULL, + ADD COLUMN data_ref_id uuid NULL; + +CREATE INDEX IF NOT EXISTS file_data_fragment__data_ref_id__idx + ON file_data_fragment (data_ref_id); diff --git a/backend/src/app/migrations/sql/0122-mod-file-table.sql b/backend/src/app/migrations/sql/0122-mod-file-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..4f0a05155e20931a37fc00c4b925af4ea8e975c5 --- /dev/null +++ b/backend/src/app/migrations/sql/0122-mod-file-table.sql @@ -0,0 +1,4 @@ +ALTER TABLE file ADD COLUMN data_ref_id uuid NULL; + +CREATE INDEX IF NOT EXISTS file__data_ref_id__idx + ON file (data_ref_id); diff --git a/backend/src/app/migrations/sql/0123-mod-file-change-table.sql b/backend/src/app/migrations/sql/0123-mod-file-change-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..37fccfd51fe472a7ffe59cb17819c7bfd1fc8d60 --- /dev/null +++ b/backend/src/app/migrations/sql/0123-mod-file-change-table.sql @@ -0,0 +1,2 @@ +CREATE INDEX IF NOT EXISTS file_change__created_at__label__idx + ON file_change (created_at, label); diff --git a/backend/src/app/migrations/sql/0124-mod-profile-table.sql b/backend/src/app/migrations/sql/0124-mod-profile-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..e9624abd64031562480bca13184cedcef2209a8b --- /dev/null +++ b/backend/src/app/migrations/sql/0124-mod-profile-table.sql @@ -0,0 +1,2 @@ +CREATE INDEX profile__props__newsletter1__idx ON profile (email) WHERE props->>'~:newsletter-news' = 'true'; +CREATE INDEX profile__props__newsletter2__idx ON profile (email) WHERE props->>'~:newsletter-updates' = 'true'; diff --git a/backend/src/app/migrations/sql/0125-mod-file-table.sql b/backend/src/app/migrations/sql/0125-mod-file-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..20d560bbb55ed549d32d623ff5c9dec0789218eb --- /dev/null +++ b/backend/src/app/migrations/sql/0125-mod-file-table.sql @@ -0,0 +1,3 @@ +--- This setting allow to optimize the table for heavy write workload +--- leaving space on the page for HOT updates +ALTER TABLE file SET (FILLFACTOR=50); diff --git a/backend/src/app/migrations/sql/0126-add-team-access-request-table.sql b/backend/src/app/migrations/sql/0126-add-team-access-request-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..548003adb78b3c58a3ab8f0b0aa96de62e47e8fc --- /dev/null +++ b/backend/src/app/migrations/sql/0126-add-team-access-request-table.sql @@ -0,0 +1,10 @@ +CREATE TABLE team_access_request ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE DEFERRABLE, + requester_id uuid NULL REFERENCES profile(id) ON DELETE CASCADE DEFERRABLE, + valid_until timestamptz NOT NULL, + auto_join_until timestamptz NOT NULL, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE (team_id, requester_id) +); diff --git a/backend/src/app/migrations/sql/0127-mod-storage-object-table.sql b/backend/src/app/migrations/sql/0127-mod-storage-object-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..521a3fcb0a30c1542866ae6f01af74015df9c7a1 --- /dev/null +++ b/backend/src/app/migrations/sql/0127-mod-storage-object-table.sql @@ -0,0 +1,3 @@ +--- This setting allow to optimize the table for heavy write workload +--- leaving space on the page for HOT updates +ALTER TABLE storage_object SET (FILLFACTOR=60); diff --git a/backend/src/app/migrations/sql/0128-mod-task-table.sql b/backend/src/app/migrations/sql/0128-mod-task-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..97fcdbeef01a706ee255dcfe8047371c835c49af --- /dev/null +++ b/backend/src/app/migrations/sql/0128-mod-task-table.sql @@ -0,0 +1,3 @@ +--- This setting allow to optimize the table for heavy write workload +--- leaving space on the page for HOT updates +ALTER TABLE task SET (FILLFACTOR=60); diff --git a/backend/src/app/migrations/sql/0129-mod-file-change-table.sql b/backend/src/app/migrations/sql/0129-mod-file-change-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..fcf1d4f4c187340954f53242a3284f76f8e2ebac --- /dev/null +++ b/backend/src/app/migrations/sql/0129-mod-file-change-table.sql @@ -0,0 +1,6 @@ +ALTER TABLE file_change + ADD COLUMN data_backend text NULL, + ADD COLUMN data_ref_id uuid NULL; + +CREATE INDEX IF NOT EXISTS file_change__data_ref_id__idx + ON file_change (data_ref_id); diff --git a/backend/src/app/migrations/sql/0130-mod-file-change-table.sql b/backend/src/app/migrations/sql/0130-mod-file-change-table.sql new file mode 100644 index 0000000000000000000000000000000000000000..272828fc2bebf63056bae5f271666d42abf2b888 --- /dev/null +++ b/backend/src/app/migrations/sql/0130-mod-file-change-table.sql @@ -0,0 +1,2 @@ +ALTER TABLE file_change + ADD COLUMN version integer NULL; diff --git a/backend/src/app/migrations/v2.clj b/backend/src/app/migrations/v2.clj new file mode 100644 index 0000000000000000000000000000000000000000..1acf7b96b75609997f3405b110e11ec107dc06c8 --- /dev/null +++ b/backend/src/app/migrations/v2.clj @@ -0,0 +1,103 @@ +;; 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 app.migrations.v2 + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.db :as db] + [app.features.components-v2 :as feat] + [app.setup :as setup] + [app.util.time :as dt])) + +(def ^:private sql:get-teams + "SELECT id, features, + row_number() OVER (ORDER BY created_at DESC) AS rown + FROM team + WHERE deleted_at IS NULL + AND (not (features @> '{components/v2}') OR features IS NULL) + ORDER BY created_at DESC") + +(defn- get-teams + [conn] + (->> (db/cursor conn [sql:get-teams] {:chunk-size 1}) + (map feat/decode-row))) + +(defn- migrate-teams + [{:keys [::db/conn] :as system}] + ;; Allow long running transaction for this connection + (db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"]) + + ;; Do not allow other migration running in the same time + (db/xact-lock! conn 0) + + ;; Run teams migration + (run! (fn [{:keys [id rown]}] + (try + (-> (assoc system ::db/rollback false) + (feat/migrate-team! id + :rown rown + :label "v2-migration" + :validate? false + :skip-on-graphics-error? true)) + (catch Throwable _ + (swap! feat/*stats* update :errors (fnil inc 0)) + (l/wrn :hint "error on migrating team (skiping)")))) + (get-teams conn)) + + (setup/set-prop! system :v2-migrated true)) + +(defn migrate + [system] + (let [tpoint (dt/tpoint) + stats (atom {}) + migrated? (setup/get-prop system :v2-migrated false)] + + (when-not migrated? + (l/inf :hint "v2 migration started") + (try + (binding [feat/*stats* stats] + (db/tx-run! system migrate-teams)) + + (let [stats (deref stats) + elapsed (dt/format-duration (tpoint))] + (l/inf :hint "v2 migration finished" + :files (:processed-files stats) + :teams (:processed-teams stats) + :errors (:errors stats) + :elapsed elapsed)) + + (catch Throwable cause + (l/err :hint "error on aplying v2 migration" :cause cause)))))) + +(def ^:private required-services + [[:app.main/assets :app.storage.s3/backend] + [:app.main/assets :app.storage.fs/backend] + :app.storage/storage + :app.db/pool + :app.setup/props + :app.svgo/optimizer + :app.metrics/metrics + :app.migrations/migrations + :app.http.client/client]) + +(defn -main + [& _args] + (try + (let [config-var (requiring-resolve 'app.main/system-config) + start-var (requiring-resolve 'app.main/start-custom) + stop-var (requiring-resolve 'app.main/stop) + system-var (requiring-resolve 'app.main/system) + config (select-keys @config-var required-services)] + + (start-var config) + (migrate @system-var) + (stop-var) + (System/exit 0)) + (catch Throwable cause + (ex/print-throwable cause) + (flush) + (System/exit -1)))) diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 8ae7a38d6e6ff224718a4d0b6ea9a89f73cee439..b51f2cde1041744504518851631fb09b979cc06a 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -27,7 +27,9 @@ [app.rpc.helpers :as rph] [app.rpc.retry :as retry] [app.rpc.rlimit :as rlimit] + [app.setup :as-alias setup] [app.storage :as-alias sto] + [app.util.inet :as inet] [app.util.services :as sv] [app.util.time :as dt] [clojure.spec.alpha :as s] @@ -69,6 +71,22 @@ (handle-response-transformation request mdata) (handle-before-comple-hook mdata)))) +(defn get-external-session-id + [request] + (when-let [session-id (rreq/get-header request "x-external-session-id")] + (when-not (or (> (count session-id) 256) + (= session-id "null") + (str/blank? session-id)) + session-id))) + +(defn- get-external-event-origin + [request] + (when-let [origin (rreq/get-header request "x-event-origin")] + (when-not (or (> (count origin) 256) + (= origin "null") + (str/blank? origin)) + origin))) + (defn- rpc-handler "Ring handler that dispatches cmd requests and convert between internal async flow into ring async flow." @@ -78,8 +96,16 @@ profile-id (or (::session/profile-id request) (::actoken/profile-id request)) + ip-addr (inet/parse-request request) + session-id (get-external-session-id request) + event-origin (get-external-event-origin request) + data (-> params + (assoc ::handler-name handler-name) + (assoc ::ip-addr ip-addr) (assoc ::request-at (dt/now)) + (assoc ::external-session-id session-id) + (assoc ::external-event-origin event-origin) (assoc ::session/id (::session/id request)) (assoc ::cond/key etag) (cond-> (uuid? profile-id) @@ -152,55 +178,37 @@ (if-let [schema (::sm/params mdata)] (let [validate (sm/validator schema) explain (sm/explainer schema) - decode (sm/decoder schema)] + decode (sm/decoder schema sm/json-transformer) + encode (sm/encoder schema sm/json-transformer)] (fn [cfg params] (let [params (decode params)] (if (validate params) - (f cfg params) - + (let [result (f cfg params)] + (if (instance? clojure.lang.IObj result) + (vary-meta result assoc :encode/json encode) + result)) (let [params (d/without-qualified params)] (ex/raise :type :validation :code :params-validation ::sm/explain (explain params))))))) f)) -(defn- wrap-output-validation - [_ f mdata] - (if (contains? cf/flags :rpc-output-validation) - (or (when-let [schema (::sm/result mdata)] - (let [schema (if (sm/lazy-schema? schema) - schema - (sm/define schema)) - validate (sm/validator schema) - explain (sm/explainer schema)] - (fn [cfg params] - (let [response (f cfg params)] - (when (map? response) - (when-not (validate response) - (ex/raise :type :validation - :code :data-validation - ::sm/explain (explain response)))) - response)))) - f) - f)) - (defn- wrap-all [cfg f mdata] (as-> f $ - (wrap-metrics cfg $ mdata) (cond/wrap cfg $ mdata) (retry/wrap-retry cfg $ mdata) (climit/wrap cfg $ mdata) + (wrap-metrics cfg $ mdata) (rlimit/wrap cfg $ mdata) (wrap-audit cfg $ mdata) (wrap-spec-conform cfg $ mdata) - (wrap-output-validation cfg $ mdata) (wrap-params-validation cfg $ mdata) (wrap-authentication cfg $ mdata))) (defn- wrap [cfg f mdata] - (l/debug :hint "register method" :name (::sv/name mdata)) + (l/trc :hint "register method" :name (::sv/name mdata)) (let [f (wrap-all cfg f mdata)] (partial f cfg))) @@ -248,7 +256,7 @@ ::ldap/provider ::sto/storage ::mtx/metrics - ::main/props] + ::setup/props] :opt [::climit ::rlimit])) @@ -265,7 +273,7 @@ (defmethod ig/pre-init-spec ::routes [_] (s/keys :req [::methods ::db/pool - ::main/props + ::setup/props ::session/manager])) (defmethod ig/init-key ::routes diff --git a/backend/src/app/rpc/climit.clj b/backend/src/app/rpc/climit.clj index 988fe29ad089c3a786b3cbbe75dbb1e43fb4a6d1..3ca348e0b941c1af94075f8aa9735221f8684ef0 100644 --- a/backend/src/app/rpc/climit.clj +++ b/backend/src/app/rpc/climit.clj @@ -20,8 +20,8 @@ [app.util.time :as dt] [app.worker :as-alias wrk] [clojure.edn :as edn] + [clojure.set :as set] [clojure.spec.alpha :as s] - [cuerdas.core :as str] [datoteka.fs :as fs] [integrant.core :as ig] [promesa.exec :as px] @@ -92,67 +92,77 @@ :timeout (:timeout config) :type :semaphore)) -(defmacro ^:private measure-and-log! - [metrics mlabels stats id action limit-id limit-label profile-id elapsed] - `(let [mpermits# (:max-permits ~stats) - mqueue# (:max-queue ~stats) - permits# (:permits ~stats) - queue# (:queue ~stats) - queue# (- queue# mpermits#) - queue# (if (neg? queue#) 0 queue#) - level# (if (pos? queue#) :warn :trace)] - - (mtx/run! ~metrics - :id :rpc-climit-queue - :val queue# - :labels ~mlabels) - - (mtx/run! ~metrics - :id :rpc-climit-permits - :val permits# - :labels ~mlabels) - - (l/log level# - :hint ~action - :req ~id - :id ~limit-id - :label ~limit-label - :profile-id (str ~profile-id) - :permits permits# - :queue queue# - :max-permits mpermits# - :max-queue mqueue# - ~@(if (some? elapsed) - [:elapsed `(dt/format-duration ~elapsed)] - [])))) + +(defn measure! + [metrics mlabels stats elapsed] + (let [mpermits (:max-permits stats) + permits (:permits stats) + queue (:queue stats) + queue (- queue mpermits) + queue (if (neg? queue) 0 queue)] + + (mtx/run! metrics + :id :rpc-climit-queue + :val queue + :labels mlabels) + + (mtx/run! metrics + :id :rpc-climit-permits + :val permits + :labels mlabels) + + (when elapsed + (mtx/run! metrics + :id :rpc-climit-timing + :val (inst-ms elapsed) + :labels mlabels)))) + +(defn log! + [action req-id stats limit-id limit-label params elapsed] + (let [mpermits (:max-permits stats) + queue (:queue stats) + queue (- queue mpermits) + queue (if (neg? queue) 0 queue) + level (if (pos? queue) :warn :trace)] + + (l/log level + :hint action + :req req-id + :id limit-id + :label limit-label + :queue queue + :elapsed (some-> elapsed dt/format-duration) + :params (-> (select-keys params [::rpc/profile-id :file-id :profile-id]) + (set/rename-keys {::rpc/profile-id :profile-id}) + (update-vals str))))) (def ^:private idseq (AtomicLong. 0)) (defn- invoke - [limiter metrics limit-id limit-key limit-label profile-id f params] + [limiter metrics limit-id limit-key limit-label handler params] (let [tpoint (dt/tpoint) mlabels (into-array String [(id->str limit-id)]) limit-id (id->str limit-id limit-key) stats (pbh/get-stats limiter) - id (.incrementAndGet ^AtomicLong idseq)] + req-id (.incrementAndGet ^AtomicLong idseq)] (try - (measure-and-log! metrics mlabels stats id "enqueued" limit-id limit-label profile-id nil) + (measure! metrics mlabels stats nil) + (log! "enqueued" req-id stats limit-id limit-label params nil) (px/invoke! limiter (fn [] (let [elapsed (tpoint) stats (pbh/get-stats limiter)] - (measure-and-log! metrics mlabels stats id "acquired" limit-id limit-label profile-id elapsed) - (mtx/run! metrics - :id :rpc-climit-timing - :val (inst-ms elapsed) - :labels mlabels) - (apply f params)))) + + (measure! metrics mlabels stats elapsed) + (log! "acquired" req-id stats limit-id limit-label params elapsed) + + (handler params)))) (catch ExceptionInfo cause (let [{:keys [type code]} (ex-data cause)] (if (= :bulkhead-error type) (let [elapsed (tpoint)] - (measure-and-log! metrics mlabels stats id "reject" limit-id limit-label profile-id elapsed) + (log! "rejected" req-id stats limit-id limit-label params elapsed) (ex/raise :type :concurrency-limit :code code :hint "concurrency limit reached" @@ -162,7 +172,9 @@ (finally (let [elapsed (tpoint) stats (pbh/get-stats limiter)] - (measure-and-log! metrics mlabels stats id "finished" limit-id limit-label profile-id elapsed)))))) + + (measure! metrics mlabels stats nil) + (log! "finished" req-id stats limit-id limit-label params elapsed)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MIDDLEWARE @@ -201,7 +213,7 @@ (reduce (fn [handler [limit-id key-fn]] (if-let [config (get config limit-id)] (let [key-fn (or key-fn noop-fn)] - (l/dbg :hint "instrumenting method" + (l/trc :hint "instrumenting method" :method label :limit (id->str limit-id) :timeout (:timeout config) @@ -220,10 +232,8 @@ (let [limit-key (key-fn params) cache-key [limit-id limit-key] limiter (cache/get cache cache-key (partial create-limiter config)) - profile-id (if (= key-fn ::rpc/profile-id) - limit-key - (get params ::rpc/profile-id))] - (invoke limiter metrics limit-id limit-key label profile-id handler [cfg params]))))) + handler (partial handler cfg)] + (invoke limiter metrics limit-id limit-key label handler params))))) (do (l/wrn :hint "no config found for specified queue" :id (id->str limit-id)) @@ -238,28 +248,27 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- build-exec-chain - [{:keys [::label ::profile-id ::rpc/climit ::mtx/metrics] :as cfg} f] + [{:keys [::label ::rpc/climit ::mtx/metrics] :as cfg} f] (let [config (get climit ::config) cache (get climit ::cache)] - (reduce (fn [handler [limit-id limit-key :as ckey]] - (let [config (get config limit-id)] - (when-not config - (throw (IllegalArgumentException. - (str/ffmt "config not found for: %" limit-id)))) - - (fn [& params] - (let [limiter (cache/get cache ckey (partial create-limiter config))] - (invoke limiter metrics limit-id limit-key label profile-id handler params))))) + (if-let [config (get config limit-id)] + (fn [cfg params] + (let [limiter (cache/get cache ckey (partial create-limiter config)) + handler (partial handler cfg)] + (invoke limiter metrics limit-id limit-key label handler params))) + (do + (l/wrn :hint "config not found" :label label :id limit-id) + f))) f (get-limits cfg)))) (defn invoke! "Run a function in context of climit. Intended to be used in virtual threads." - [{:keys [::executor] :as cfg} f & params] + [{:keys [::executor] :as cfg} f params] (let [f (if (some? executor) - (fn [& params] (px/await! (px/submit! executor (fn [] (apply f params))))) + (fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params))))) f) f (build-exec-chain cfg f)] - (apply f params))) + (f cfg params))) diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj index dd10f33719ba0138fe471455ab6a6afd1fbb4bcc..e8d9675f995fe29830d35689aae3e3387989511c 100644 --- a/backend/src/app/rpc/commands/access_token.clj +++ b/backend/src/app/rpc/commands/access_token.clj @@ -6,24 +6,24 @@ (ns app.rpc.commands.access-token (:require - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.uuid :as uuid] [app.db :as db] [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.doc :as-alias doc] [app.rpc.quotes :as quotes] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.util.services :as sv] - [app.util.time :as dt] - [clojure.spec.alpha :as s])) + [app.util.time :as dt])) (defn- decode-row [row] (dissoc row :perms)) (defn create-access-token - [{:keys [::db/conn ::main/props]} profile-id name expiration] + [{:keys [::db/conn ::setup/props]} profile-id name expiration] (let [created-at (dt/now) token-id (uuid/next) token (tokens/generate props {:iss "access-token" @@ -43,25 +43,23 @@ :perms (db/create-array conn "text" [])}))) -(defn repl-create-access-token +(defn repl:create-access-token [{:keys [::db/pool] :as system} profile-id name expiration] (db/with-atomic [conn pool] (let [props (:app.setup/props system)] - (create-access-token {::db/conn conn ::main/props props} + (create-access-token {::db/conn conn ::setup/props props} profile-id name expiration)))) -(s/def ::name ::us/not-empty-string) -(s/def ::expiration ::dt/duration) - -(s/def ::create-access-token - (s/keys :req [::rpc/profile-id] - :req-un [::name] - :opt-un [::expiration])) +(def ^:private schema:create-access-token + [:map {:title "create-access-token"} + [:name [:string {:max 250 :min 1}]] + [:expiration {:optional true} ::dt/duration]]) (sv/defmethod ::create-access-token - {::doc/added "1.18"} + {::doc/added "1.18" + ::sm/params schema:create-access-token} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name expiration]}] (db/with-atomic [conn pool] (let [cfg (assoc cfg ::db/conn conn)] @@ -71,21 +69,23 @@ (-> (create-access-token cfg profile-id name expiration) (decode-row))))) -(s/def ::delete-access-token - (s/keys :req [::rpc/profile-id] - :req-un [::us/id])) +(def ^:private schema:delete-access-token + [:map {:title "delete-access-token"} + [:id ::sm/uuid]]) (sv/defmethod ::delete-access-token - {::doc/added "1.18"} + {::doc/added "1.18" + ::sm/params schema:delete-access-token} [{:keys [::db/pool]} {:keys [::rpc/profile-id id]}] (db/delete! pool :access-token {:id id :profile-id profile-id}) nil) -(s/def ::get-access-tokens - (s/keys :req [::rpc/profile-id])) +(def ^:private schema:get-access-tokens + [:map {:title "get-access-tokens"}]) (sv/defmethod ::get-access-tokens - {::doc/added "1.18"} + {::doc/added "1.18" + ::sm/params schema:get-access-tokens} [{:keys [::db/pool]} {:keys [::rpc/profile-id]}] (->> (db/query pool :access-token {:profile-id profile-id} diff --git a/backend/src/app/rpc/commands/audit.clj b/backend/src/app/rpc/commands/audit.clj index 76bd6e1880723db25adb8af59ad4466532fb063b..f43195dd72d7f5dd88c80394e971b04f00233d84 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -14,12 +14,26 @@ [app.config :as cf] [app.db :as db] [app.http :as-alias http] - [app.loggers.audit :as audit] + [app.loggers.audit :as-alias audit] [app.rpc :as-alias rpc] [app.rpc.climit :as-alias climit] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] - [app.util.services :as sv])) + [app.util.inet :as inet] + [app.util.services :as sv] + [app.util.time :as dt])) + +(def ^:private event-columns + [:id + :name + :source + :type + :tracked-at + :created-at + :profile-id + :ip-addr + :props + :context]) (defn- event->row [event] [(uuid/next) @@ -27,33 +41,56 @@ (:source event) (:type event) (:timestamp event) + (:created-at event) (:profile-id event) (db/inet (:ip-addr event)) (db/tjson (:props event)) (db/tjson (d/without-nils (:context event)))]) -(def ^:private event-columns - [:id :name :source :type :tracked-at - :profile-id :ip-addr :props :context]) +(defn- adjust-timestamp + [{:keys [timestamp created-at] :as event}] + (let [margin (inst-ms (dt/diff timestamp created-at))] + (if (or (neg? margin) + (> margin 3600000)) + ;; If event is in future or lags more than 1 hour, we reasign + ;; timestamp to the server creation date + (-> event + (assoc :timestamp created-at) + (update :context assoc :original-timestamp timestamp)) + event))) (defn- handle-events [{:keys [::db/pool]} {:keys [::rpc/profile-id events] :as params}] (let [request (-> params meta ::http/request) - ip-addr (audit/parse-client-ip request) + ip-addr (inet/parse-request request) + tnow (dt/now) xform (comp - (map #(assoc % :profile-id profile-id)) - (map #(assoc % :ip-addr ip-addr)) - (map #(assoc % :source "frontend")) + (map (fn [event] + (-> event + (assoc :created-at tnow) + (assoc :profile-id profile-id) + (assoc :ip-addr ip-addr) + (assoc :source "frontend")))) (filter :profile-id) + (map adjust-timestamp) (map event->row)) events (sequence xform events)] (when (seq events) (db/insert-many! pool :audit-log event-columns events)))) +(def valid-event-types + #{"action" "identify"}) + (def schema:event [:map {:title "Event"} - [:name [:string {:max 250}]] - [:type [:string {:max 250}]] + [:name + [:and {:gen/elements ["update-file", "get-profile"]} + [:string {:max 250}] + [:re #"[\d\w-]{1,50}"]]] + [:type + [:and {:gen/elements valid-event-types} + [:string {:max 250}] + [::sm/one-of {:format "string"} valid-event-types]]] [:props [:map-of :keyword :any]] [:context {:optional true} diff --git a/backend/src/app/rpc/commands/auth.clj b/backend/src/app/rpc/commands/auth.clj index 8e9671e5982eb672a1da17edc51d20d4e64ec0ec..1ed3fa364d20b19f70af0c5a4afb426becd31b92 100644 --- a/backend/src/app/rpc/commands/auth.clj +++ b/backend/src/app/rpc/commands/auth.clj @@ -6,7 +6,6 @@ (ns app.rpc.commands.auth (:require - [app.auth :as auth] [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] @@ -17,18 +16,22 @@ [app.config :as cf] [app.db :as db] [app.email :as eml] + [app.email.blacklist :as email.blacklist] + [app.email.whitelist :as email.whitelist] [app.http.session :as session] [app.loggers.audit :as audit] - [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.climit :as-alias climit] [app.rpc.commands.profile :as profile] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] + [app.setup :as-alias setup] + [app.setup.welcome-file :refer [create-welcome-file]] [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as wrk] [cuerdas.core :as str])) (def schema:password @@ -37,6 +40,12 @@ (def schema:token [::sm/word-string {:max 6000}]) +(defn- elapsed-verify-threshold? + [profile] + (let [elapsed (dt/diff (:modified-at profile) (dt/now)) + verify-threshold (cf/get :email-verify-threshold)] + (pos? (compare elapsed verify-threshold)))) + ;; ---- COMMAND: login with password (defn login-with-password @@ -88,7 +97,7 @@ (profile/strip-private-attrs)) invitation (when-let [token (:invitation-token params)] - (tokens/verify (::main/props cfg) {:token token :iss :team-invitation})) + (tokens/verify (::setup/props cfg) {:token token :iss :team-invitation})) ;; If invitation member-id does not matches the profile-id, we just proceed to ignore the ;; invitation because invitations matches exactly; and user can't login with other email and @@ -121,24 +130,33 @@ ;; ---- COMMAND: Logout +(def ^:private schema:logout + [:map {:title "logoug"} + [:profile-id {:optional true} ::sm/uuid]]) + (sv/defmethod ::logout "Clears the authentication cookie and logout the current session." {::rpc/auth false - ::doc/added "1.15"} - [cfg _] - (rph/with-transform {} (session/delete-fn cfg))) + ::doc/changes [["2.1" "Now requires profile-id passed in the body"]] + ::doc/added "1.0" + ::sm/params schema:logout} + [cfg params] + (if (= (:profile-id params) + (::rpc/profile-id params)) + (rph/with-transform {} (session/delete-fn cfg)) + {})) ;; ---- COMMAND: Recover Profile (defn recover-profile [{:keys [::db/pool] :as cfg} {:keys [token password]}] (letfn [(validate-token [token] - (let [tdata (tokens/verify (::main/props cfg) {:token token :iss :password-recovery})] + (let [tdata (tokens/verify (::setup/props cfg) {:token token :iss :password-recovery})] (:profile-id tdata))) (update-password [conn profile-id] (let [pwd (profile/derive-password cfg password)] - (db/update! conn :profile {:password pwd} {:id profile-id}) + (db/update! conn :profile {:password pwd :is-active true} {:id profile-id}) nil))] (db/with-atomic [conn pool] @@ -161,46 +179,52 @@ ;; ---- COMMAND: Prepare Register -(defn validate-register-attempt! - [{:keys [::db/pool] :as cfg} params] +(defn- validate-register-attempt! + [cfg params] - (when-not (contains? cf/flags :registration) - (when-not (contains? params :invitation-token) - (ex/raise :type :restriction - :code :registration-disabled))) + (when (or + (not (contains? cf/flags :registration)) + (not (contains? cf/flags :login-with-password))) + (ex/raise :type :restriction + :code :registration-disabled)) (when (contains? params :invitation-token) - (let [invitation (tokens/verify (::main/props cfg) {:token (:invitation-token params) :iss :team-invitation})] + (let [invitation (tokens/verify (::setup/props cfg) + {:token (:invitation-token params) + :iss :team-invitation})] (when-not (= (:email params) (:member-email invitation)) (ex/raise :type :restriction :code :email-does-not-match-invitation :hint "email should match the invitation")))) - (when-not (auth/email-domain-in-whitelist? (:email params)) - (ex/raise :type :validation + (when (and (email.blacklist/enabled? cfg) + (email.blacklist/contains? cfg (:email params))) + (ex/raise :type :restriction :code :email-domain-is-not-allowed)) - ;; Don't allow proceed in preparing registration if the profile is - ;; already reported as spammer. - (when (eml/has-bounce-reports? pool (:email params)) - (ex/raise :type :validation - :code :email-has-permanent-bounces - :hint "looks like the email has one or many bounces reported")) + (when (and (email.whitelist/enabled? cfg) + (not (email.whitelist/contains? cfg (:email params)))) + (ex/raise :type :restriction + :code :email-domain-is-not-allowed)) ;; Perform a basic validation of email & password (when (= (str/lower (:email params)) (str/lower (:password params))) (ex/raise :type :validation :code :email-as-password - :hint "you can't use your email as password"))) + :hint "you can't use your email as password")) -(def register-retry-threshold - (dt/duration "15m")) + (when (eml/has-bounce-reports? cfg (:email params)) + (ex/raise :type :restriction + :code :email-has-permanent-bounces + :email (:email params) + :hint "looks like the email has bounce reports")) -(defn- elapsed-register-retry-threshold? - [profile] - (let [elapsed (dt/diff (:modified-at profile) (dt/now))] - (pos? (compare elapsed register-retry-threshold)))) + (when (eml/has-complaint-reports? cfg (:email params)) + (ex/raise :type :restriction + :code :email-has-complaints + :email (:email params) + :hint "looks like the email has complaint reports"))) (defn prepare-register [{:keys [::db/pool] :as cfg} {:keys [email] :as params}] @@ -208,21 +232,7 @@ (validate-register-attempt! cfg params) (let [email (profile/clean-email email) - profile (when-let [profile (profile/get-profile-by-email pool email)] - (cond - (:is-blocked profile) - (ex/raise :type :restriction - :code :profile-blocked) - - (and (not (:is-active profile)) - (elapsed-register-retry-threshold? profile)) - profile - - :else - (ex/raise :type :validation - :code :email-already-exists - :hint "profile already exists"))) - + profile (profile/get-profile-by-email pool email) params {:email email :password (:password params) :invitation-token (:invitation-token params) @@ -232,8 +242,8 @@ :exp (dt/in-future {:days 7})} params (d/without-nils params) + token (tokens/generate (::setup/props cfg) params)] - token (tokens/generate (::main/props cfg) params)] (with-meta {:token token} {::audit/profile-id uuid/zero}))) @@ -276,6 +286,7 @@ is-demo (:is-demo params false) is-muted (:is-muted params false) is-active (:is-active params false) + theme (:theme params nil) email (str/lower email) params {:id id @@ -286,20 +297,24 @@ :password password :deleted-at (:deleted-at params) :props props + :theme theme :is-active is-active :is-muted is-muted :is-demo is-demo}] (try (-> (db/insert! conn :profile params) (profile/decode-row)) - (catch org.postgresql.util.PSQLException e - (let [state (.getSQLState e)] + (catch org.postgresql.util.PSQLException cause + (let [state (.getSQLState cause)] (if (not= state "23505") - (throw e) - (ex/raise :type :validation - :code :email-already-exists - :hint "email already exists" - :cause e))))))) + (throw cause) + + (do + (l/error :hint "not an error" :cause cause) + (ex/raise :type :validation + :code :email-already-exists + :hint "email already exists" + :cause cause)))))))) (defn create-profile-rels! [conn {:keys [id] :as profile}] @@ -316,17 +331,16 @@ {::db/return-keys true}) (profile/decode-row)))) - (defn send-email-verification! - [conn props profile] - (let [vtoken (tokens/generate props + [{:keys [::db/conn] :as cfg} profile] + (let [vtoken (tokens/generate (::setup/props cfg) {:iss :verify-email :exp (dt/in-future "72h") :profile-id (:id profile) :email (:email profile)}) ;; NOTE: this token is mainly used for possible complains ;; identification on the sns webhook - ptoken (tokens/generate props + ptoken (tokens/generate (::setup/props cfg) {:iss :profile-identity :profile-id (:id profile) :exp (dt/in-future {:days 30})})] @@ -339,104 +353,150 @@ :extra-data ptoken}))) (defn register-profile - [{:keys [::db/conn] :as cfg} {:keys [token fullname] :as params}] - (let [claims (tokens/verify (::main/props cfg) {:token token :iss :prepared-register}) + [{:keys [::db/conn ::wrk/executor] :as cfg} {:keys [token fullname theme] :as params}] + (let [theme (when (= theme "light") theme) + claims (tokens/verify (::setup/props cfg) {:token token :iss :prepared-register}) params (-> claims (into params) - (assoc :fullname fullname)) - - is-active (or (:is-active params) - (not (contains? cf/flags :email-verification))) + (assoc :fullname fullname) + (assoc :theme theme)) profile (if-let [profile-id (:profile-id claims)] (profile/get-profile conn profile-id) - (let [params (-> params - (assoc :is-active is-active) - (update :password #(profile/derive-password cfg %)))] - (->> (create-profile! conn params) - (create-profile-rels! conn)))) + ;; NOTE: we first try to match existing profile + ;; by email, that in normal circumstances will + ;; not return anything, but when a user tries to + ;; reuse the same token multiple times, we need + ;; to detect if the profile is already registered + (or (profile/get-profile-by-email conn (:email claims)) + (let [is-active (or (boolean (:is-active claims)) + (not (contains? cf/flags :email-verification))) + params (-> params + (assoc :is-active is-active) + (update :password #(profile/derive-password cfg %))) + profile (->> (create-profile! conn params) + (create-profile-rels! conn))] + (vary-meta profile assoc :created true)))) + + created? (-> profile meta :created true?) invitation (when-let [token (:invitation-token params)] - (tokens/verify (::main/props cfg) {:token token :iss :team-invitation}))] - - ;; If profile is filled in claims, means it tries to register - ;; again, so we proceed to update the modified-at attr - ;; accordingly. - (when-let [id (:profile-id claims)] - (db/update! conn :profile {:modified-at (dt/now)} {:id id}) - (audit/submit! cfg - {::audit/type "fact" - ::audit/name "register-profile-retry" - ::audit/profile-id id})) + (tokens/verify (::setup/props cfg) {:token token :iss :team-invitation})) + + props (audit/profile->props profile) + + create-welcome-file-when-needed + (fn [] + (when (:create-welcome-file params) + (let [cfg (dissoc cfg ::db/conn)] + (wrk/submit! executor (create-welcome-file cfg profile)))))] (cond - ;; If invitation token comes in params, this is because the - ;; user comes from team-invitation process; in this case, - ;; regenerate token and send back to the user a new invitation - ;; token (and mark current session as logged). This happens - ;; only if the invitation email matches with the register - ;; email. - (and (some? invitation) (= (:email profile) (:member-email invitation))) + ;; When profile is blocked, we just ignore it and return plain data + (:is-blocked profile) + (do + (l/wrn :hint "register attempt for already blocked profile" + :profile-id (str (:id profile)) + :profile-email (:email profile)) + (rph/with-meta {:email (:email profile)} + {::audit/replace-props props + ::audit/context {:action "ignore-because-blocked"} + ::audit/profile-id (:id profile) + ::audit/name "register-profile-retry"})) + + ;; If invitation token comes in params, this is because the user + ;; comes from team-invitation process; in this case, regenerate + ;; token and send back to the user a new invitation token (and + ;; mark current session as logged). This happens only if the + ;; invitation email matches with the register email. + (and (some? invitation) + (= (:email profile) + (:member-email invitation))) (let [claims (assoc invitation :member-id (:id profile)) - token (tokens/generate (::main/props cfg) claims) - resp {:invitation-token token}] - (-> resp + token (tokens/generate (::setup/props cfg) claims)] + (-> {:invitation-token token} (rph/with-transform (session/create-fn cfg (:id profile))) - (rph/with-meta {::audit/replace-props (audit/profile->props profile) + (rph/with-meta {::audit/replace-props props + ::audit/context {:action "accept-invitation"} ::audit/profile-id (:id profile)}))) - ;; If auth backend is different from "penpot" means user is - ;; registering using third party auth mechanism; in this case - ;; we need to mark this session as logged. - (not= "penpot" (:auth-backend profile)) - (-> (profile/strip-private-attrs profile) - (rph/with-transform (session/create-fn cfg (:id profile))) - (rph/with-meta {::audit/replace-props (audit/profile->props profile) - ::audit/profile-id (:id profile)})) - - ;; If the `:enable-insecure-register` flag is set, we proceed - ;; to sign in the user directly, without email verification. - (true? is-active) - (-> (profile/strip-private-attrs profile) - (rph/with-transform (session/create-fn cfg (:id profile))) - (rph/with-meta {::audit/replace-props (audit/profile->props profile) - ::audit/profile-id (:id profile)})) - - ;; In all other cases, send a verification email. + ;; When a new user is created and it is already activated by + ;; configuration or specified by OIDC, we just mark the profile + ;; as logged-in + created? + (if (:is-active profile) + (-> (profile/strip-private-attrs profile) + (rph/with-transform (session/create-fn cfg (:id profile))) + (rph/with-defer create-welcome-file-when-needed) + (rph/with-meta + {::audit/replace-props props + ::audit/context {:action "login"} + ::audit/profile-id (:id profile)})) + + (do + (when-not (eml/has-reports? conn (:email profile)) + (send-email-verification! cfg profile)) + + (-> {:email (:email profile)} + (rph/with-defer create-welcome-file-when-needed) + (rph/with-meta + {::audit/replace-props props + ::audit/context {:action "email-verification"} + ::audit/profile-id (:id profile)})))) + :else - (do - (send-email-verification! conn (::main/props cfg) profile) - (rph/with-meta profile + (let [elapsed? (elapsed-verify-threshold? profile) + reports? (eml/has-reports? conn (:email profile)) + action (if reports? + "ignore-because-complaints" + (if elapsed? + "resend-email-verification" + "ignore"))] + + (l/wrn :hint "repeated registry detected" + :profile-id (str (:id profile)) + :profile-email (:email profile) + :context-action action) + + (when (= action "resend-email-verification") + (db/update! conn :profile + {:modified-at (dt/now)} + {:id (:id profile)}) + (send-email-verification! cfg profile)) + + (rph/with-meta {:email (:email profile)} {::audit/replace-props (audit/profile->props profile) - ::audit/profile-id (:id profile)}))))) + ::audit/context {:action action} + ::audit/profile-id (:id profile) + ::audit/name "register-profile-retry"}))))) (def schema:register-profile [:map {:title "register-profile"} [:token schema:token] - [:fullname [::sm/word-string {:max 100}]]]) + [:fullname [::sm/word-string {:max 100}]] + [:theme {:optional true} [:string {:max 10}]] + [:create-welcome-file {:optional true} :boolean]]) (sv/defmethod ::register-profile {::rpc/auth false ::doc/added "1.15" ::sm/params schema:register-profile ::climit/id :auth/global} - [{:keys [::db/pool] :as cfg} params] - (db/with-atomic [conn pool] - (-> (assoc cfg ::db/conn conn) - (register-profile params)))) + [cfg params] + (db/tx-run! cfg register-profile params)) ;; ---- COMMAND: Request Profile Recovery -(defn request-profile-recovery - [{:keys [::db/pool] :as cfg} {:keys [email] :as params}] +(defn- request-profile-recovery + [{:keys [::db/conn] :as cfg} {:keys [email] :as params}] (letfn [(create-recovery-token [{:keys [id] :as profile}] - (let [token (tokens/generate (::main/props cfg) + (let [token (tokens/generate (::setup/props cfg) {:iss :password-recovery :exp (dt/in-future "15m") :profile-id id})] (assoc profile :token token))) (send-email-notification [conn profile] - (let [ptoken (tokens/generate (::main/props cfg) + (let [ptoken (tokens/generate (::setup/props cfg) {:iss :profile-identity :profile-id (:id profile) :exp (dt/in-future {:days 30})})] @@ -449,28 +509,42 @@ :extra-data ptoken}) nil))] - (db/with-atomic [conn pool] - (when-let [profile (->> (profile/clean-email email) - (profile/get-profile-by-email conn))] - (when-not (eml/allow-send-emails? conn profile) - (ex/raise :type :validation - :code :profile-is-muted - :hint "looks like the profile has reported repeatedly as spam or has permanent bounces.")) - - (when-not (:is-active profile) - (ex/raise :type :validation - :code :profile-not-verified - :hint "the user need to validate profile before recover password")) - - (when (eml/has-bounce-reports? conn (:email profile)) - (ex/raise :type :validation - :code :email-has-permanent-bounces - :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) - - (->> profile - (create-recovery-token) - (send-email-notification conn)))))) - + (let [profile (->> (profile/clean-email email) + (profile/get-profile-by-email conn))] + + (cond + (not profile) + (l/wrn :hint "attempt of profile recovery: no profile found" + :profile-email email) + + (not (eml/allow-send-emails? conn profile)) + (l/wrn :hint "attempt of profile recovery: profile is muted" + :profile-id (str (:id profile)) + :profile-email (:email profile)) + + (eml/has-bounce-reports? conn (:email profile)) + (l/wrn :hint "attempt of profile recovery: email has bounces" + :profile-id (str (:id profile)) + :profile-email (:email profile)) + + (eml/has-complaint-reports? conn (:email profile)) + (l/wrn :hint "attempt of profile recovery: email has complaints" + :profile-id (str (:id profile)) + :profile-email (:email profile)) + + (not (elapsed-verify-threshold? profile)) + (l/wrn :hint "attempt of profile recovery: retry attempt threshold not elapsed" + :profile-id (str (:id profile)) + :profile-email (:email profile)) + + :else + (do + (db/update! conn :profile + {:modified-at (dt/now)} + {:id (:id profile)}) + (->> profile + (create-recovery-token) + (send-email-notification conn))))))) (def schema:request-profile-recovery [:map {:title "request-profile-recovery"} @@ -481,6 +555,6 @@ ::doc/added "1.15" ::sm/params schema:request-profile-recovery} [cfg params] - (request-profile-recovery cfg params)) + (db/tx-run! cfg request-profile-recovery params)) diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 8f2216e636070d397cd1a47122838db9afcc9b85..6b2b69c900f3ea7c7e5c6afeeb1f43b9e16886dc 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -32,12 +32,11 @@ (def ^:private schema:export-binfile - (sm/define - [:map {:title "export-binfile"} - [:name :string] - [:file-id ::sm/uuid] - [:include-libraries :boolean] - [:embed-assets :boolean]])) + [:map {:title "export-binfile"} + [:name [:string {:max 250}]] + [:file-id ::sm/uuid] + [:include-libraries :boolean] + [:embed-assets :boolean]]) (sv/defmethod ::export-binfile "Export a penpot file in a binary format." @@ -65,26 +64,23 @@ ;; --- Command: import-binfile (defn- import-binfile - [{:keys [::wrk/executor ::bf.v1/project-id] :as cfg} input] - (db/tx-run! cfg - (fn [{:keys [::db/conn] :as cfg}] - ;; NOTE: the importation process performs some operations that - ;; are not very friendly with virtual threads, and for avoid - ;; unexpected blocking of other concurrent operations we - ;; dispatch that operation to a dedicated executor. - (let [result (px/submit! executor (partial bf.v1/import-files! cfg input))] - (db/update! conn :project - {:modified-at (dt/now)} - {:id project-id}) - (deref result))))) + [{:keys [::wrk/executor ::bf.v1/project-id ::db/pool] :as cfg} input] + ;; NOTE: the importation process performs some operations that + ;; are not very friendly with virtual threads, and for avoid + ;; unexpected blocking of other concurrent operations we + ;; dispatch that operation to a dedicated executor. + (let [result (px/invoke! executor (partial bf.v1/import-files! cfg input))] + (db/update! pool :project + {:modified-at (dt/now)} + {:id project-id}) + result)) (def ^:private schema:import-binfile - (sm/define - [:map {:title "import-binfile"} - [:name :string] - [:project-id ::sm/uuid] - [:file ::media/upload]])) + [:map {:title "import-binfile"} + [:name [:string {:max 250}]] + [:project-id ::sm/uuid] + [:file ::media/upload]]) (sv/defmethod ::import-binfile "Import a penpot file in a binary format." diff --git a/backend/src/app/rpc/commands/comments.clj b/backend/src/app/rpc/commands/comments.clj index 4949f1a4359270dcab430a4e555e44eee25d8bc1..41645a8be37bd712cfe7d92cc61ae8d0f444172a 100644 --- a/backend/src/app/rpc/commands/comments.clj +++ b/backend/src/app/rpc/commands/comments.clj @@ -292,7 +292,7 @@ [:map {:title "create-comment-thread"} [:file-id ::sm/uuid] [:position ::gpt/point] - [:content :string] + [:content [:string {:max 250}]] [:page-id ::sm/uuid] [:frame-id ::sm/uuid] [:share-id {:optional true} [:maybe ::sm/uuid]]]) @@ -418,7 +418,7 @@ schema:create-comment [:map {:title "create-comment"} [:thread-id ::sm/uuid] - [:content :string] + [:content [:string {:max 250}]] [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::create-comment @@ -477,7 +477,7 @@ schema:update-comment [:map {:title "update-comment"} [:id ::sm/uuid] - [:content :string] + [:content [:string {:max 250}]] [:share-id {:optional true} [:maybe ::sm/uuid]]]) (sv/defmethod ::update-comment diff --git a/backend/src/app/rpc/commands/demo.clj b/backend/src/app/rpc/commands/demo.clj index 3dabb96fba7e5d4f8a88e31d00e751dec5703f2e..059548f2299c791977139d41b5d696c046188da3 100644 --- a/backend/src/app/rpc/commands/demo.clj +++ b/backend/src/app/rpc/commands/demo.clj @@ -18,10 +18,7 @@ [app.util.services :as sv] [app.util.time :as dt] [buddy.core.codecs :as bc] - [buddy.core.nonce :as bn] - [clojure.spec.alpha :as s])) - -(s/def ::create-demo-profile any?) + [buddy.core.nonce :as bn])) (sv/defmethod ::create-demo-profile "A command that is responsible of creating a demo purpose @@ -48,7 +45,7 @@ params {:email email :fullname fullname :is-active true - :deleted-at (dt/in-future cf/deletion-delay) + :deleted-at (dt/in-future (cf/get-deletion-delay)) :password (profile/derive-password cfg password) :props {}}] diff --git a/backend/src/app/rpc/commands/feedback.clj b/backend/src/app/rpc/commands/feedback.clj index 7d2ab1c883a32a3fbf9c7978e7f814a53e7dfe7a..29b79a87baf21428b3a84ff1da0f2a13e258a607 100644 --- a/backend/src/app/rpc/commands/feedback.clj +++ b/backend/src/app/rpc/commands/feedback.clj @@ -8,29 +8,25 @@ "A general purpose feedback module." (:require [app.common.exceptions :as ex] - [app.common.spec :as us] + [app.common.schema :as sm] [app.config :as cf] [app.db :as db] [app.email :as eml] [app.rpc :as-alias rpc] [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) + [app.util.services :as sv])) (declare ^:private send-feedback!) -(s/def ::content ::us/string) -(s/def ::from ::us/email) -(s/def ::subject ::us/string) - -(s/def ::send-user-feedback - (s/keys :req [::rpc/profile-id] - :req-un [::subject - ::content])) +(def ^:private schema:send-user-feedback + [:map {:title "send-user-feedback"} + [:subject [:string {:max 250}]] + [:content [:string {:max 250}]]]) (sv/defmethod ::send-user-feedback - {::doc/added "1.18"} + {::doc/added "1.18" + ::sm/params schema:send-user-feedback} [{:keys [::db/pool]} {:keys [::rpc/profile-id] :as params}] (when-not (contains? cf/flags :user-feedback) (ex/raise :type :restriction diff --git a/backend/src/app/rpc/commands/files.clj b/backend/src/app/rpc/commands/files.clj index b2a97c6cc88f9df72897ef2e5ba8910b8dfd879a..79381d34f89a1877f820219f777ca901cb8490bb 100644 --- a/backend/src/app/rpc/commands/files.clj +++ b/backend/src/app/rpc/commands/files.clj @@ -15,7 +15,6 @@ [app.common.logging :as l] [app.common.schema :as sm] [app.common.schema.desc-js-like :as-alias smdj] - [app.common.spec :as us] [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] [app.config :as cf] @@ -35,7 +34,7 @@ [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s] + [app.worker :as wrk] [cuerdas.core :as str])) ;; --- FEATURES @@ -45,18 +44,6 @@ (when media-id (str (cf/get :public-uri) "/assets/by-id/" media-id))) -;; --- SPECS - -(s/def ::features ::us/set-of-strings) -(s/def ::file-id ::us/uuid) -(s/def ::frame-id ::us/uuid) -(s/def ::id ::us/uuid) -(s/def ::is-shared ::us/boolean) -(s/def ::name ::us/string) -(s/def ::project-id ::us/uuid) -(s/def ::search-term ::us/string) -(s/def ::team-id ::us/uuid) - ;; --- HELPERS (def long-cache-duration @@ -81,6 +68,9 @@ :max-version fmg/version)) file)) + +;; --- FILE DATA + ;; --- FILE PERMISSIONS (def ^:private sql:file-permissions @@ -188,12 +178,12 @@ [:map {:title "File"} [:id ::sm/uuid] [:features ::cfeat/features] - [:has-media-trimmed :boolean] - [:comment-thread-seqn {:min 0} :int] - [:name :string] - [:revn {:min 0} :int] + [:has-media-trimmed ::sm/boolean] + [:comment-thread-seqn [::sm/int {:min 0}]] + [:name [:string {:max 250}]] + [:revn [::sm/int {:min 0}]] [:modified-at ::dt/instant] - [:is-shared :boolean] + [:is-shared ::sm/boolean] [:project-id ::sm/uuid] [:created-at ::dt/instant] [:data {:optional true} :any]])) @@ -271,18 +261,19 @@ (let [params (merge {:id id} (when (some? project-id) {:project-id project-id})) - file (-> (db/get conn :file params - {::db/check-deleted (not include-deleted?) - ::db/remove-deleted (not include-deleted?) - ::sql/for-update lock-for-update?}) - (decode-row))] + file (->> (db/get conn :file params + {::db/check-deleted (not include-deleted?) + ::db/remove-deleted (not include-deleted?) + ::sql/for-update lock-for-update?}) + (feat.fdata/resolve-file-data cfg) + (decode-row))] (if (and migrate? (fmg/need-migration? file)) (migrate-file cfg file) file))) (defn get-minimal-file [cfg id & {:as opts}] - (let [opts (assoc opts ::sql/columns [:id :modified-at :revn])] + (let [opts (assoc opts ::sql/columns [:id :modified-at :revn :data-ref-id :data-backend])] (db/get cfg :file {:id id} opts))) (defn get-file-etag @@ -340,9 +331,11 @@ [:share-id {:optional true} ::sm/uuid]]) (defn- get-file-fragment - [conn file-id fragment-id] - (some-> (db/get conn :file-data-fragment {:file-id file-id :id fragment-id}) - (update :content blob/decode))) + [cfg file-id fragment-id] + (let [resolve-file-data (partial feat.fdata/resolve-file-data cfg)] + (some-> (db/get cfg :file-data-fragment {:file-id file-id :id fragment-id}) + (resolve-file-data) + (update :data blob/decode)))) (sv/defmethod ::get-file-fragment "Retrieve a file fragment by its ID. Only authenticated users." @@ -350,12 +343,12 @@ ::rpc/auth false ::sm/params schema:get-file-fragment ::sm/result schema:file-fragment} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id fragment-id share-id]}] - (dm/with-open [conn (db/open pool)] - (let [perms (get-permissions conn profile-id file-id share-id)] - (check-read-permissions! perms) - (-> (get-file-fragment conn file-id fragment-id) - (rph/with-http-cache long-cache-duration))))) + [cfg {:keys [::rpc/profile-id file-id fragment-id share-id]}] + (db/run! cfg (fn [cfg] + (let [perms (get-permissions cfg profile-id file-id share-id)] + (check-read-permissions! perms) + (-> (get-file-fragment cfg file-id fragment-id) + (rph/with-http-cache long-cache-duration)))))) ;; --- COMMAND QUERY: get-project-files @@ -415,7 +408,7 @@ "Checks if the file has libraries. Returns a boolean" {::doc/added "1.15.1" ::sm/params schema:has-file-libraries - ::sm/result :boolean} + ::sm/result ::sm/boolean} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id]}] (dm/with-open [conn (db/open pool)] (check-read-permissions! pool profile-id file-id) @@ -670,7 +663,7 @@ f.modified_at, f.name, f.is_shared, - ft.media_id, + ft.media_id AS thumbnail_id, row_number() over w as row_num from file as f inner join project as p on (p.id = f.project_id) @@ -689,10 +682,8 @@ [conn team-id] (->> (db/exec! conn [sql:team-recent-files team-id]) (mapv (fn [row] - (if-let [media-id (:media-id row)] - (-> row - (dissoc :media-id) - (assoc :thumbnail-uri (resolve-public-uri media-id))) + (if-let [media-id (:thumbnail-id row)] + (assoc row :thumbnail-uri (resolve-public-uri media-id)) (dissoc row :media-id)))))) (def ^:private schema:get-team-recent-files @@ -738,6 +729,23 @@ [cfg {:keys [::rpc/profile-id] :as params}] (db/tx-run! cfg get-file-summary (assoc params :profile-id profile-id))) + +;; --- COMMAND QUERY: get-file-info + +(defn- get-file-info + [{:keys [::db/conn] :as cfg} {:keys [id] :as params}] + (db/get* conn :file + {:id id} + {::sql/columns [:id]})) + +(sv/defmethod ::get-file-info + "Retrieve minimal file info by its ID." + {::rpc/auth false + ::doc/added "2.2.0" + ::sm/params schema:get-file} + [cfg params] + (db/tx-run! cfg get-file-info params)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MUTATION COMMANDS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -760,19 +768,19 @@ [:map {:title "RenameFileEvent"} [:id ::sm/uuid] [:project-id ::sm/uuid] - [:name :string] + [:name [:string {:max 250}]] [:created-at ::dt/instant] [:modified-at ::dt/instant]] ::sm/params [:map {:title "RenameFileParams"} - [:name {:min 1} :string] + [:name [:string {:min 1 :max 250}]] [:id ::sm/uuid]] ::sm/result [:map {:title "SimplifiedFile"} [:id ::sm/uuid] - [:name :string] + [:name [:string {:max 250}]] [:created-at ::dt/instant] [:modified-at ::dt/instant]]} @@ -817,12 +825,13 @@ (db/update! cfg :file {:revn (inc (:revn file)) :data (blob/encode (:data file)) - :modified-at (dt/now)} + :modified-at (dt/now) + :has-media-trimmed false} {:id file-id}) (feat.fdata/persist-pointers! cfg file-id)))) -(defn- absorb-library! +(defn- absorb-library "Find all files using a shared library, and absorb all library assets into the file local libraries" [cfg {:keys [id] :as library}] @@ -840,7 +849,26 @@ :library-id (str id) :files (str/join "," (map str ids))) - (run! (partial absorb-library-by-file! cfg ldata) ids))) + (run! (partial absorb-library-by-file! cfg ldata) ids) + library)) + +(defn absorb-library! + [{:keys [::db/conn] :as cfg} id] + (let [file (-> (get-file cfg id + :lock-for-update? true + :include-deleted? true) + (check-version!)) + + proj (db/get* conn :project {:id (:project-id file)} + {::db/remove-deleted false}) + team (-> (db/get* conn :team {:id (:team-id proj)} + {::db/remove-deleted false}) + (teams/decode-row))] + + (-> (cfeat/get-team-enabled-features cf/flags team) + (cfeat/check-file-features! (:features file))) + + (absorb-library cfg file))) (defn- set-file-shared [{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}] @@ -853,30 +881,21 @@ ;; file, we need to perform more complex operation, ;; so in this case we retrieve the complete file and ;; perform all required validations. - (let [file (-> (get-file cfg id :lock-for-update? true) - (check-version!) - (assoc :is-shared false)) - team (teams/get-team conn - :profile-id profile-id - :project-id (:project-id file))] - - (-> (cfeat/get-team-enabled-features cf/flags team) - (cfeat/check-client-features! (:features params)) - (cfeat/check-file-features! (:features file))) - - (absorb-library! cfg file) - + (let [file (-> (absorb-library! cfg id) + (assoc :is-shared false))] (db/delete! conn :file-library-rel {:library-file-id id}) (db/update! conn :file - {:is-shared false} + {:is-shared false + :modified-at (dt/now)} {:id id}) - file) + (select-keys file [:id :name :is-shared])) (and (false? (:is-shared file)) (true? (:is-shared params))) (let [file (assoc file :is-shared true)] (db/update! conn :file - {:is-shared true} + {:is-shared true + :modified-at (dt/now)} {:id id}) file) @@ -898,7 +917,7 @@ (sm/define [:map {:title "set-file-shared"} [:id ::sm/uuid] - [:is-shared :boolean]])) + [:is-shared ::sm/boolean]])) (sv/defmethod ::set-file-shared {::doc/added "1.17" @@ -909,12 +928,19 @@ ;; --- MUTATION COMMAND: delete-file -(defn- mark-file-deleted! +(defn- mark-file-deleted [conn file-id] - (db/update! conn :file - {:deleted-at (dt/now)} - {:id file-id} - {::db/return-keys [:id :name :is-shared :project-id :created-at :modified-at]})) + (let [file (db/update! conn :file + {:deleted-at (dt/now)} + {:id file-id} + {::db/return-keys [:id :name :is-shared :deleted-at + :project-id :created-at :modified-at]})] + (wrk/submit! {::db/conn conn + ::wrk/task :delete-object + ::wrk/params {:object :file + :deleted-at (:deleted-at file) + :id file-id}}) + file)) (def ^:private schema:delete-file @@ -925,29 +951,7 @@ (defn- delete-file [{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}] (check-edition-permissions! conn profile-id id) - (let [file (mark-file-deleted! conn id)] - - ;; NOTE: when a file is a shared library, then we proceed to load - ;; the whole file, proceed with feature checking and properly execute - ;; the absorb-library procedure - (when (:is-shared file) - (let [file (-> (get-file cfg id - :lock-for-update? true - :include-deleted? true) - (check-version!)) - - team (teams/get-team conn - :profile-id profile-id - :project-id (:project-id file))] - - - - (-> (cfeat/get-team-enabled-features cf/flags team) - (cfeat/check-client-features! (:features params)) - (cfeat/check-file-features! (:features file))) - - (absorb-library! cfg file))) - + (let [file (mark-file-deleted conn id)] (rph/with-meta (rph/wrap) {::audit/props {:project-id (:project-id file) :name (:name file) @@ -1051,14 +1055,16 @@ {:id file-id} {::db/return-keys true})) -(s/def ::ignore-file-library-sync-status - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::date])) +(def ^:private schema:ignore-file-library-sync-status + [:map {:title "ignore-file-library-sync-status"} + [:file-id ::sm/uuid] + [:date ::dt/instant]]) ;; TODO: improve naming (sv/defmethod ::ignore-file-library-sync-status "Ignore updates in linked files" - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:ignore-file-library-sync-status} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id file-id) diff --git a/backend/src/app/rpc/commands/files_create.clj b/backend/src/app/rpc/commands/files_create.clj index cc15830d47862a2524f9acc9ff22f0872c8dd955..bbff2a1731ed747849beac73651162c172a5b510 100644 --- a/backend/src/app/rpc/commands/files_create.clj +++ b/backend/src/app/rpc/commands/files_create.clj @@ -6,13 +6,10 @@ (ns app.rpc.commands.files-create (:require - [app.common.data :as d] [app.common.data.macros :as dm] [app.common.features :as cfeat] - [app.common.files.defaults :refer [version]] [app.common.schema :as sm] [app.common.types.file :as ctf] - [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.features.fdata :as feat.fdata] @@ -40,7 +37,7 @@ (defn create-file [{:keys [::db/conn] :as cfg} {:keys [id name project-id is-shared revn - modified-at deleted-at create-page + modified-at deleted-at create-page page-id ignore-sync-until features] :or {is-shared false revn 0 create-page true} :as params}] @@ -51,23 +48,17 @@ (binding [pmap/*tracked* (pmap/create-tracked) cfeat/*current* features] - (let [id (or id (uuid/next)) - - data (if create-page - (ctf/make-file-data id) - (ctf/make-file-data id nil)) - - file {:id id - :project-id project-id - :name name - :revn revn - :is-shared is-shared - :version version - :data data - :features features - :ignore-sync-until ignore-sync-until - :modified-at modified-at - :deleted-at deleted-at} + (let [file (ctf/make-file {:id id + :project-id project-id + :name name + :revn revn + :is-shared is-shared + :features features + :ignore-sync-until ignore-sync-until + :modified-at modified-at + :deleted-at deleted-at + :create-page create-page + :page-id page-id}) file (if (contains? features "fdata/objects-map") (feat.fdata/enable-objects-map file) @@ -75,9 +66,7 @@ file (if (contains? features "fdata/pointer-map") (feat.fdata/enable-pointer-map file) - file) - - file (d/without-nils file)] + file)] (db/insert! conn :file (-> file @@ -86,9 +75,9 @@ {::db/return-keys false}) (when (contains? features "fdata/pointer-map") - (feat.fdata/persist-pointers! cfg id)) + (feat.fdata/persist-pointers! cfg (:id file))) - (->> (assoc params :file-id id :role :owner) + (->> (assoc params :file-id (:id file) :role :owner) (create-file-role! conn)) (db/update! conn :project @@ -99,10 +88,10 @@ (def ^:private schema:create-file [:map {:title "create-file"} - [:name :string] + [:name [:string {:max 250}]] [:project-id ::sm/uuid] [:id {:optional true} ::sm/uuid] - [:is-shared {:optional true} :boolean] + [:is-shared {:optional true} ::sm/boolean] [:features {:optional true} ::cfeat/features]]) (sv/defmethod ::create-file diff --git a/backend/src/app/rpc/commands/files_share.clj b/backend/src/app/rpc/commands/files_share.clj index bf761b5bf9df1f397f5518729393fddf9b9cde0e..98132e06ec277929e99a2f7820b000af615764fc 100644 --- a/backend/src/app/rpc/commands/files_share.clj +++ b/backend/src/app/rpc/commands/files_share.clj @@ -7,29 +7,24 @@ (ns app.rpc.commands.files-share "Share link related rpc mutation methods." (:require - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.uuid :as uuid] [app.db :as db] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] [app.rpc.doc :as-alias doc] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) - -;; --- Helpers & Specs - -(s/def ::file-id ::us/uuid) -(s/def ::who-comment ::us/string) -(s/def ::who-inspect ::us/string) -(s/def ::pages (s/every ::us/uuid :kind set?)) + [app.util.services :as sv])) ;; --- MUTATION: Create Share Link (declare create-share-link) -(s/def ::create-share-link - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::who-comment ::who-inspect ::pages])) +(def ^:private schema:create-share-link + [:map {:title "create-share-link"} + [:file-id ::sm/uuid] + [:who-comment [:string {:max 250}]] + [:who-inspect [:string {:max 250}]] + [:pages [:set ::sm/uuid]]]) (sv/defmethod ::create-share-link "Creates a share-link object. @@ -37,7 +32,8 @@ Share links are resources that allows external users access to specific pages of a file with specific permissions (who-comment and who-inspect)." {::doc/added "1.18" - ::doc/module :files} + ::doc/module :files + ::sm/params schema:create-share-link} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) @@ -58,13 +54,14 @@ ;; --- MUTATION: Delete Share Link -(s/def ::delete-share-link - (s/keys :req [::rpc/profile-id] - :req-un [::us/id])) +(def ^:private schema:delete-share-link + [:map {:title "delete-share-link"} + [:id ::sm/uuid]]) (sv/defmethod ::delete-share-link {::doc/added "1.18" - ::doc/module ::files} + ::doc/module ::files + ::sm/params schema:delete-share-link} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (let [slink (db/get-by-id conn :share-link id)] diff --git a/backend/src/app/rpc/commands/files_snapshot.clj b/backend/src/app/rpc/commands/files_snapshot.clj index 1e9c3081a5b1c9d200f93ca16f0feb87f1b7306f..2fdb262a008140171400a817cb4647aa824e341a 100644 --- a/backend/src/app/rpc/commands/files_snapshot.clj +++ b/backend/src/app/rpc/commands/files_snapshot.clj @@ -13,13 +13,15 @@ [app.config :as cf] [app.db :as db] [app.db.sql :as-alias sql] + [app.features.fdata :as feat.fdata] [app.main :as-alias main] - [app.media :as media] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] [app.storage :as sto] + [app.util.blob :as blob] + [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] [cuerdas.core :as str])) @@ -34,20 +36,21 @@ :code :authentication-required :hint "only admins allowed"))) +(def sql:get-file-snapshots + "SELECT id, label, revn, created_at + FROM file_change + WHERE file_id = ? + AND created_at < ? + AND label IS NOT NULL + ORDER BY created_at DESC + LIMIT ?") + (defn get-file-snapshots [{:keys [::db/conn]} {:keys [file-id limit start-at] :or {limit Long/MAX_VALUE}}] - (let [query (str "select id, label, revn, created_at " - " from file_change " - " where file_id = ? " - " and created_at < ? " - " and data is not null " - " order by created_at desc " - " limit ?") - start-at (or start-at (dt/now)) + (let [start-at (or start-at (dt/now)) limit (min limit 20)] - - (->> (db/exec! conn [query file-id start-at limit]) + (->> (db/exec! conn [sql:get-file-snapshots file-id start-at limit]) (mapv (fn [row] (update row :created-at dt/format-instant :rfc1123)))))) @@ -63,8 +66,8 @@ (db/run! cfg get-file-snapshots params)) (defn restore-file-snapshot! - [{:keys [::db/conn ::sto/storage] :as cfg} {:keys [file-id id]}] - (let [storage (media/configure-assets-storage storage conn) + [{:keys [::db/conn] :as cfg} {:keys [file-id id]}] + (let [storage (sto/resolve cfg {::db/reuse-conn true}) file (files/get-minimal-file conn file-id {::db/for-update true}) snapshot (db/get* conn :file-change {:file-id file-id @@ -78,43 +81,53 @@ :id id :file-id file-id)) - (when-not (:data snapshot) - (ex/raise :type :precondition - :code :snapshot-without-data - :hint "snapshot has no data" - :label (:label snapshot) - :file-id file-id)) + (let [snapshot (feat.fdata/resolve-file-data cfg snapshot)] + (when-not (:data snapshot) + (ex/raise :type :precondition + :code :snapshot-without-data + :hint "snapshot has no data" + :label (:label snapshot) + :file-id file-id)) - (l/dbg :hint "restoring snapshot" - :file-id (str file-id) - :label (:label snapshot) - :snapshot-id (str (:id snapshot))) - - (db/update! conn :file - {:data (:data snapshot) - :revn (inc (:revn file)) - :features (:features snapshot)} - {:id file-id}) - - ;; clean object thumbnails - (let [sql (str "update file_tagged_object_thumbnail " - " set deleted_at = now() " - " where file_id=? returning media_id") - res (db/exec! conn [sql file-id])] - - (doseq [media-id (into #{} (keep :media-id) res)] - (sto/touch-object! storage media-id))) - - ;; clean object thumbnails - (let [sql (str "update file_thumbnail " - " set deleted_at = now() " - " where file_id=? returning media_id") - res (db/exec! conn [sql file-id])] - (doseq [media-id (into #{} (keep :media-id) res)] - (sto/touch-object! storage media-id))) - - {:id (:id snapshot) - :label (:label snapshot)})) + (l/dbg :hint "restoring snapshot" + :file-id (str file-id) + :label (:label snapshot) + :snapshot-id (str (:id snapshot))) + + ;; If the file was already offloaded, on restring the snapshot + ;; we are going to replace the file data, so we need to touch + ;; the old referenced storage object and avoid possible leaks + (when (feat.fdata/offloaded? file) + (sto/touch-object! storage (:data-ref-id file))) + + (db/update! conn :file + {:data (:data snapshot) + :revn (inc (:revn file)) + :version (:version snapshot) + :data-backend nil + :data-ref-id nil + :has-media-trimmed false + :features (:features snapshot)} + {:id file-id}) + + ;; clean object thumbnails + (let [sql (str "update file_tagged_object_thumbnail " + " set deleted_at = now() " + " where file_id=? returning media_id") + res (db/exec! conn [sql file-id])] + (doseq [media-id (into #{} (keep :media-id) res)] + (sto/touch-object! storage media-id))) + + ;; clean file thumbnails + (let [sql (str "update file_thumbnail " + " set deleted_at = now() " + " where file_id=? returning media_id") + res (db/exec! conn [sql file-id])] + (doseq [media-id (into #{} (keep :media-id) res)] + (sto/touch-object! storage media-id))) + + {:id (:id snapshot) + :label (:label snapshot)}))) (defn- resolve-snapshot-by-label [conn file-id label] @@ -146,21 +159,33 @@ (merge (resolve-snapshot-by-label conn file-id label)))] (restore-file-snapshot! cfg params))))) +(defn- get-file + [cfg file-id] + (let [file (->> (db/get cfg :file {:id file-id}) + (feat.fdata/resolve-file-data cfg))] + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg file-id)] + (-> file + (update :data blob/decode) + (update :data feat.fdata/process-pointers deref) + (update :data feat.fdata/process-objects (partial into {})) + (update :data blob/encode))))) + (defn take-file-snapshot! - [cfg {:keys [file-id label]}] - (let [conn (db/get-connection cfg) - file (db/get conn :file {:id file-id}) + [cfg {:keys [file-id label ::rpc/profile-id]}] + (let [file (get-file cfg file-id) id (uuid/next)] (l/debug :hint "creating file snapshot" :file-id (str file-id) :label label) - (db/insert! conn :file-change + (db/insert! cfg :file-change {:id id :revn (:revn file) :data (:data file) + :version (:version file) :features (:features file) + :profile-id profile-id :file-id (:id file) :label label} {::db/return-keys false}) diff --git a/backend/src/app/rpc/commands/files_temp.clj b/backend/src/app/rpc/commands/files_temp.clj index 9ac4f7e5e6237a80a7380ee43c88b9270d513283..17000d79cd806fa916a27134cd536a305effffd8 100644 --- a/backend/src/app/rpc/commands/files_temp.clj +++ b/backend/src/app/rpc/commands/files_temp.clj @@ -16,6 +16,7 @@ [app.db.sql :as sql] [app.features.components-v2 :as feat.compv2] [app.features.fdata :as fdata] + [app.loggers.audit :as audit] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] [app.rpc.commands.files-create :as files.create] @@ -23,6 +24,7 @@ [app.rpc.commands.projects :as projects] [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] + [app.rpc.helpers :as rph] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.services :as sv] @@ -33,12 +35,12 @@ (def ^:private schema:create-temp-file [:map {:title "create-temp-file"} - [:name :string] + [:name [:string {:max 250}]] [:project-id ::sm/uuid] [:id {:optional true} ::sm/uuid] - [:is-shared :boolean] + [:is-shared ::sm/boolean] [:features ::cfeat/features] - [:create-page :boolean]]) + [:create-page ::sm/boolean]]) (sv/defmethod ::create-temp-file {::doc/added "1.17" @@ -81,7 +83,7 @@ (def ^:private schema:update-temp-file [:map {:title "update-temp-file"} [:changes [:vector ::cpc/change]] - [:revn {:min 0} :int] + [:revn [::sm/int {:min 0}]] [:session-id ::sm/uuid] [:id ::sm/uuid]]) @@ -100,7 +102,9 @@ :revn revn :data nil :changes (blob/encode changes)}) - nil))) + (rph/with-meta (rph/wrap nil) + {::audit/replace-props {:file-id id + :revn revn}})))) ;; --- MUTATION COMMAND: persist-temp-file @@ -134,6 +138,9 @@ file)) file)] + ;; Delete changes from the changes history + (db/delete! conn :file-change {:file-id id}) + (db/update! conn :file {:deleted-at nil :revn 1 diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index a44a8bdbd554f3fa83bee7afc795d9bf4232e16c..8e9c2da1c3ec96ab0fc5a61a180090148cc7a16d 100644 --- a/backend/src/app/rpc/commands/files_thumbnails.clj +++ b/backend/src/app/rpc/commands/files_thumbnails.clj @@ -33,7 +33,6 @@ [app.util.pointer-map :as pmap] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s] [cuerdas.core :as str])) ;; --- FEATURES @@ -86,11 +85,8 @@ ::doc/module :files ::sm/params [:map {:title "get-file-object-thumbnails"} [:file-id ::sm/uuid] - [:tag {:optional true} :string]] - ::sm/result [:map-of :string :string] - ::cond/get-object #(files/get-minimal-file %1 (:file-id %2)) - ::cond/reuse-key? true - ::cond/key-fn files/get-file-etag} + [:tag {:optional true} [:string {:max 50}]]] + ::sm/result [:map-of [:string {:max 250}] [:string {:max 250}]]} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id tag] :as params}] (dm/with-open [conn (db/open pool)] (files/check-read-permissions! conn profile-id file-id) @@ -193,7 +189,7 @@ (sm/define [:map {:title "PartialFile"} [:id ::sm/uuid] - [:revn {:min 0} :int] + [:revn {:min 0} ::sm/int] [:page :any]])) (sv/defmethod ::get-file-data-for-thumbnail @@ -228,59 +224,60 @@ ;; MUTATION COMMANDS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; MUTATION COMMAND: create-file-object-thumbnail - -(defn- create-file-object-thumbnail! - [{:keys [::db/conn ::sto/storage]} file-id object-id media tag] - - (let [thumb (db/get* conn :file-tagged-object-thumbnail - {:file-id file-id - :object-id object-id - :tag tag} - {::db/remove-deleted false - ::sql/for-update true}) - - path (:path media) +(def sql:get-file-object-thumbnail + "SELECT * FROM file_tagged_object_thumbnail + WHERE file_id = ? AND object_id = ? AND tag = ? + FOR UPDATE") + +(def sql:create-file-object-thumbnail + "INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id) + VALUES (?, ?, ?, ?) + ON CONFLICT (file_id, object_id, tag) + DO UPDATE SET updated_at=?, media_id=?, deleted_at=null + RETURNING *") + +(defn- persist-thumbnail! + [storage media created-at] + (let [path (:path media) mtype (:mtype media) hash (sto/calculate-hash path) data (-> (sto/content path) - (sto/wrap-with-hash hash)) - tnow (dt/now) + (sto/wrap-with-hash hash))] + + (sto/put-object! storage + {::sto/content data + ::sto/deduplicate? true + ::sto/touched-at created-at + :content-type mtype + :bucket "file-object-thumbnail"}))) - media (sto/put-object! storage - {::sto/content data - ::sto/deduplicate? true - ::sto/touched-at tnow - :content-type mtype - :bucket "file-object-thumbnail"})] - (if (some? thumb) - (do - ;; We mark the old media id as touched if it does not matches - (when (not= (:id media) (:media-id thumb)) - (sto/touch-object! storage (:media-id thumb))) - (db/update! conn :file-tagged-object-thumbnail - {:media-id (:id media) - :deleted-at nil - :updated-at tnow} - {:file-id file-id - :object-id object-id - :tag tag})) - (db/insert! conn :file-tagged-object-thumbnail - {:file-id file-id - :object-id object-id - :created-at tnow - :updated-at tnow - :tag tag - :media-id (:id media)})))) + +(defn- create-file-object-thumbnail! + [{:keys [::sto/storage] :as cfg} file-id object-id media tag] + (let [tsnow (dt/now) + media (persist-thumbnail! storage media tsnow) + [th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag]) + th2 (db/exec-one! conn [sql:create-file-object-thumbnail + file-id object-id tag (:id media) + tsnow (:id media)])] + [th1 th2])))] + + (when (and (some? th1) + (not= (:media-id th1) + (:media-id th2))) + (sto/touch-object! storage (:media-id th1))) + + th2)) (def ^:private schema:create-file-object-thumbnail [:map {:title "create-file-object-thumbnail"} [:file-id ::sm/uuid] - [:object-id :string] + [:object-id [:string {:max 250}]] [:media ::media/upload] - [:tag {:optional true} :string]]) + [:tag {:optional true} [:string {:max 50}]]]) (sv/defmethod ::create-file-object-thumbnail {::doc/added "1.19" @@ -296,16 +293,9 @@ (media/validate-media-type! media) (media/validate-media-size! media) - (db/tx-run! cfg - (fn [{:keys [::db/conn] :as cfg}] - (files/check-edition-permissions! conn profile-id file-id) - (when-not (db/read-only? conn) - (let [cfg (-> cfg - (update ::sto/storage media/configure-assets-storage) - (assoc ::rtry/when rtry/conflict-exception?) - (assoc ::rtry/max-retries 5) - (assoc ::rtry/label "create-file-object-thumbnail"))] - (create-file-object-thumbnail! cfg file-id object-id media (or tag "frame"))))))) + (db/run! cfg files/check-edition-permissions! profile-id file-id) + + (create-file-object-thumbnail! cfg file-id object-id media (or tag "frame"))) ;; --- MUTATION COMMAND: delete-file-object-thumbnail @@ -322,25 +312,23 @@ :object-id object-id :tag tag}))) -(s/def ::delete-file-object-thumbnail - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::object-id])) +(def ^:private schema:delete-file-object-thumbnail + [:map {:title "delete-file-object-thumbnail"} + [:file-id ::sm/uuid] + [:object-id [:string {:max 250}]]]) (sv/defmethod ::delete-file-object-thumbnail {::doc/added "1.19" ::doc/module :files - ::doc/deprecated "1.20" - ::climit/id [[:file-thumbnail-ops/by-profile ::rpc/profile-id] - [:file-thumbnail-ops/global]] + ::sm/params schema:delete-file-object-thumbnail ::audit/skip true} [cfg {:keys [::rpc/profile-id file-id object-id]}] + (files/check-edition-permissions! cfg profile-id file-id) (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] - (files/check-edition-permissions! conn profile-id file-id) - (when-not (db/read-only? conn) - (-> cfg - (update ::sto/storage media/configure-assets-storage conn) - (delete-file-object-thumbnail! file-id object-id)) - nil)))) + (-> cfg + (update ::sto/storage sto/configure conn) + (delete-file-object-thumbnail! file-id object-id)) + nil))) ;; --- MUTATION COMMAND: create-file-thumbnail @@ -397,7 +385,7 @@ schema:create-file-thumbnail [:map {:title "create-file-thumbnail"} [:file-id ::sm/uuid] - [:revn :int] + [:revn ::sm/int] [:media ::media/upload]]) (sv/defmethod ::create-file-thumbnail @@ -416,6 +404,6 @@ (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (files/check-edition-permissions! conn profile-id file-id) (when-not (db/read-only? conn) - (let [cfg (update cfg ::sto/storage media/configure-assets-storage) - media (create-file-thumbnail! cfg params)] - {:uri (files/resolve-public-uri (:id media))}))))) + (let [media (create-file-thumbnail! cfg params)] + {:uri (files/resolve-public-uri (:id media)) + :id (:id media)}))))) diff --git a/backend/src/app/rpc/commands/files_update.clj b/backend/src/app/rpc/commands/files_update.clj index cf9e9b590aa7734212c3bf2a676401ced9aa6460..bd98b707153edacf6156b6e3329b182f26da2836 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -29,6 +29,7 @@ [app.rpc.commands.teams :as teams] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] + [app.storage :as sto] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.services :as sv] @@ -37,6 +38,20 @@ [clojure.set :as set] [promesa.exec :as px])) +(declare ^:private get-lagged-changes) +(declare ^:private send-notifications!) +(declare ^:private update-file) +(declare ^:private update-file*) +(declare ^:private process-changes-and-validate) +(declare ^:private take-snapshot?) +(declare ^:private delete-old-snapshots!) + +;; PUBLIC API; intended to be used outside of this module +(declare update-file!) +(declare update-file-data!) +(declare persist-file!) +(declare get-file) + ;; --- SCHEMA (def ^:private @@ -44,15 +59,15 @@ [:map {:title "update-file"} [:id ::sm/uuid] [:session-id ::sm/uuid] - [:revn {:min 0} :int] + [:revn {:min 0} ::sm/int] [:features {:optional true} ::cfeat/features] [:changes {:optional true} [:vector ::cpc/change]] [:changes-with-metadata {:optional true} [:vector [:map [:changes [:vector ::cpc/change]] [:hint-origin {:optional true} :keyword] - [:hint-events {:optional true} [:vector :string]]]]] - [:skip-validate {:optional true} :boolean]]) + [:hint-events {:optional true} [:vector [:string {:max 250}]]]]]] + [:skip-validate {:optional true} ::sm/boolean]]) (def ^:private schema:update-file-result @@ -61,7 +76,7 @@ [:changes [:vector ::cpc/change]] [:file-id ::sm/uuid] [:id ::sm/uuid] - [:revn {:min 0} :int] + [:revn {:min 0} ::sm/int] [:session-id ::sm/uuid]]]) ;; --- HELPERS @@ -96,40 +111,6 @@ (or (contains? library-change-types type) (contains? file-change-types type))) -(def ^:private sql:get-file - "SELECT f.*, p.team_id - FROM file AS f - JOIN project AS p ON (p.id = f.project_id) - WHERE f.id = ? - AND (f.deleted_at IS NULL OR - f.deleted_at > now()) - FOR KEY SHARE") - -(defn get-file - [conn id] - (let [file (db/exec-one! conn [sql:get-file id])] - (when-not file - (ex/raise :type :not-found - :code :object-not-found - :hint (format "file with id '%s' does not exists" id))) - (update file :features db/decode-pgarray #{}))) - -(defn- wrap-with-pointer-map-context - [f] - (fn [cfg {:keys [id] :as file}] - (binding [pmap/*tracked* (pmap/create-tracked) - pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] - (let [result (f cfg file)] - (feat.fdata/persist-pointers! cfg id) - result)))) - -(declare get-lagged-changes) -(declare send-notifications!) -(declare update-file) -(declare update-file*) -(declare update-file-data) -(declare take-snapshot?) - ;; If features are specified from params and the final feature ;; set is different than the persisted one, update it on the ;; database. @@ -145,7 +126,8 @@ ::sm/result schema:update-file-result ::doc/module :files ::doc/added "1.17"} - [cfg {:keys [::rpc/profile-id id] :as params}] + [{:keys [::mtx/metrics] :as cfg} + {:keys [::rpc/profile-id id changes changes-with-metadata] :as params}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] (files/check-edition-permissions! conn profile-id id) (db/xact-lock! conn id) @@ -159,14 +141,30 @@ (cfeat/check-client-features! (:features params)) (cfeat/check-file-features! (:features file) (:features params))) - params (assoc params - :profile-id profile-id - :features features - :team team - :file file) + changes (if changes-with-metadata + (->> changes-with-metadata (mapcat :changes) vec) + (vec changes)) + + params (-> params + (assoc :profile-id profile-id) + (assoc :features features) + (assoc :team team) + (assoc :file file) + (assoc :changes changes)) + + cfg (assoc cfg ::timestamp (dt/now)) tpoint (dt/tpoint)] + + (when (> (:revn params) + (:revn file)) + (ex/raise :type :validation + :code :revn-conflict + :hint "The incoming revision number is greater that stored version." + :context {:incoming-revn (:revn params) + :stored-revn (:revn file)})) + ;; When newly computed features does not match exactly with ;; the features defined on team row, we update it. (when (not= features (:features team)) @@ -175,73 +173,124 @@ {:features features} {:id (:id team)}))) + (mtx/run! metrics {:id :update-file-changes :inc (count changes)}) + (binding [l/*context* (some-> (meta params) (get :app.http/request) (errors/request->context))] - (-> (update-file cfg params) + (-> (update-file* cfg params) (rph/with-defer #(let [elapsed (tpoint)] (l/trace :hint "update-file" :time (dt/format-duration elapsed)))))))))) -(defn update-file - [{:keys [::mtx/metrics] :as cfg} - {:keys [file features changes changes-with-metadata] :as params}] - (let [features (-> features - (set/difference cfeat/frontend-only-features) - (set/union (:features file))) - - update-fn (cond-> update-file* - (contains? features "fdata/pointer-map") - (wrap-with-pointer-map-context)) - - changes (if changes-with-metadata - (->> changes-with-metadata (mapcat :changes) vec) - (vec changes))] - - (when (> (:revn params) - (:revn file)) - (ex/raise :type :validation - :code :revn-conflict - :hint "The incoming revision number is greater that stored version." - :context {:incoming-revn (:revn params) - :stored-revn (:revn file)})) - - (mtx/run! metrics {:id :update-file-changes :inc (count changes)}) - - (binding [cfeat/*current* features - cfeat/*previous* (:features file)] - (let [file (assoc file :features features) - params (-> params - (assoc :file file) - (assoc :changes changes) - (assoc ::created-at (dt/now)))] - - (-> (update-fn cfg params) - (vary-meta assoc ::audit/replace-props - {:id (:id file) - :name (:name file) - :features (:features file) - :project-id (:project-id file) - :team-id (:team-id file)})))))) - (defn- update-file* - [{:keys [::db/conn ::wrk/executor] :as cfg} - {:keys [profile-id file changes session-id ::created-at skip-validate] :as params}] - (let [;; Process the file data on separated thread for avoid to do + "Internal function, part of the update-file process, that encapsulates + the changes application offload to a separated thread and emit all + corresponding notifications. + + Follow the inner implementation to `update-file-data!` function. + + Only intended for internal use on this module." + [{:keys [::db/conn ::wrk/executor ::timestamp] :as cfg} + {:keys [profile-id file features changes session-id skip-validate] :as params}] + + (let [;; Retrieve the file data + file (feat.fdata/resolve-file-data cfg file) + + file (assoc file :features + (-> features + (set/difference cfeat/frontend-only-features) + (set/union (:features file)))) + + ;; Process the file data on separated thread for avoid to do ;; the CPU intensive operation on vthread. - file (px/invoke! executor (partial update-file-data cfg file changes skip-validate)) - features (db/create-array conn "text" (:features file))] - - (db/insert! conn :file-change - {:id (uuid/next) - :session-id session-id - :profile-id profile-id - :created-at created-at - :file-id (:id file) - :revn (:revn file) - :features (db/create-array conn "text" (:features file)) - :data (when (take-snapshot? file) - (:data file)) - :changes (blob/encode changes)} + file (px/invoke! executor + (fn [] + (binding [cfeat/*current* features + cfeat/*previous* (:features file)] + (update-file-data! cfg file + process-changes-and-validate + changes skip-validate))))] + + (when (feat.fdata/offloaded? file) + (let [storage (sto/resolve cfg ::db/reuse-conn true)] + (some->> (:data-ref-id file) (sto/touch-object! storage)))) + + ;; TODO: move this to asynchronous task + (when (::snapshot-data file) + (delete-old-snapshots! cfg file)) + + (persist-file! cfg file) + + (let [params (assoc params :file file) + response {:revn (:revn file) + :lagged (get-lagged-changes conn params)} + features (db/create-array conn "text" (:features file))] + + ;; Insert change (xlog) + (db/insert! conn :file-change + {:id (uuid/next) + :session-id session-id + :profile-id profile-id + :created-at timestamp + :file-id (:id file) + :revn (:revn file) + :version (:version file) + :features features + :label (::snapshot-label file) + :data (::snapshot-data file) + :changes (blob/encode changes)} + {::db/return-keys false}) + + ;; Send asynchronous notifications + (send-notifications! cfg params) + + (vary-meta response assoc ::audit/replace-props + {:id (:id file) + :name (:name file) + :features (:features file) + :project-id (:project-id file) + :team-id (:team-id file)})))) + +(defn update-file! + "A public api that allows apply a transformation to a file with all context setup." + [cfg file-id update-fn & args] + (let [file (get-file cfg file-id) + file (apply update-file-data! cfg file update-fn args)] + (persist-file! cfg file))) + +(def ^:private sql:get-file + "SELECT f.*, p.team_id + FROM file AS f + JOIN project AS p ON (p.id = f.project_id) + WHERE f.id = ? + AND (f.deleted_at IS NULL OR + f.deleted_at > now()) + FOR KEY SHARE") + +(defn get-file + "Get not-decoded file, only decodes the features set." + [conn id] + (let [file (db/exec-one! conn [sql:get-file id])] + (when-not file + (ex/raise :type :not-found + :code :object-not-found + :hint (format "file with id '%s' does not exists" id))) + (update file :features db/decode-pgarray #{}))) + +(defn persist-file! + "Function responsible of persisting already encoded file. Should be + used together with `get-file` and `update-file-data!`. + + It also updates the project modified-at attr." + [{:keys [::db/conn ::timestamp]} file] + (let [features (db/create-array conn "text" (:features file)) + ;; The timestamp can be nil because this function is also + ;; intended to be used outside of this module + modified-at (or timestamp (dt/now))] + + (db/update! conn :project + {:modified-at modified-at} + {:id (:project-id file)} {::db/return-keys false}) (db/update! conn :file @@ -250,20 +299,96 @@ :version (:version file) :features features :data-backend nil - :modified-at created-at + :data-ref-id nil + :modified-at modified-at :has-media-trimmed false} - {:id (:id file)}) - - (db/update! conn :project - {:modified-at created-at} - {:id (:project-id file)}) - - (let [params (assoc params :file file)] - ;; Send asynchronous notifications - (send-notifications! cfg params) - - ;; Retrieve and return lagged data - (get-lagged-changes conn params)))) + {:id (:id file)} + {::db/return-keys false}))) + +(defn- update-file-data! + "Perform a file data transformation in with all update context setup. + + This function expected not-decoded file and transformation function. Returns + an encoded file. + + This function is not responsible of saving the file. It only saves + fdata/pointer-map modified fragments." + + [cfg {:keys [id] :as file} update-fn & args] + (binding [pmap/*tracked* (pmap/create-tracked) + pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] + (let [file (update file :data (fn [data] + (-> data + (blob/decode) + (assoc :id (:id file))))) + + ;; For avoid unnecesary overhead of creating multiple pointers + ;; and handly internally with objects map in their worst + ;; case (when probably all shapes and all pointers will be + ;; readed in any case), we just realize/resolve them before + ;; applying the migration to the file + file (if (fmg/need-migration? file) + (-> file + (update :data feat.fdata/process-pointers deref) + (update :data feat.fdata/process-objects (partial into {})) + (fmg/migrate-file)) + file) + + file (apply update-fn cfg file args) + + ;; TODO: reuse operations if file is migrated + ;; TODO: move encoding to a separated thread + file (if (take-snapshot? file) + (let [tpoint (dt/tpoint) + snapshot (-> (:data file) + (feat.fdata/process-pointers deref) + (feat.fdata/process-objects (partial into {})) + (blob/encode)) + elapsed (tpoint) + label (str "internal/snapshot/" (:revn file))] + + (l/trc :hint "take snapshot" + :file-id (str (:id file)) + :revn (:revn file) + :label label + :elapsed (dt/format-duration elapsed)) + + (-> file + (assoc ::snapshot-data snapshot) + (assoc ::snapshot-label label))) + file) + + file (cond-> file + (contains? cfeat/*current* "fdata/objects-map") + (feat.fdata/enable-objects-map) + + (contains? cfeat/*current* "fdata/pointer-map") + (feat.fdata/enable-pointer-map) + + :always + (update :data blob/encode))] + + (feat.fdata/persist-pointers! cfg id) + + file))) + +(defn- get-file-libraries + "A helper for preload file libraries, mainly used for perform file + semantical and structural validation" + [{:keys [::db/conn] :as cfg} file] + (->> (files/get-file-libraries conn (:id file)) + (into [file] (map (fn [{:keys [id]}] + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id) + pmap/*tracked* nil] + ;; We do not resolve the objects maps here + ;; because there is a lower probability that all + ;; shapes needed to be loded into memory, so we + ;; leeave it on lazy status + (-> (files/get-file cfg id :migrate? false) + (update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved + (update :data feat.fdata/process-objects (partial into {})) + (fmg/migrate-file)))))) + (d/index-by :id))) (defn- soft-validate-file-schema! [file] @@ -280,87 +405,78 @@ (l/error :hint "file validation error" :cause cause)))) -(defn- update-file-data - [{:keys [::db/conn] :as cfg} file changes skip-validate] - (let [file (update file :data (fn [data] - (-> data - (blob/decode) - (assoc :id (:id file))))) - - ;; For avoid unnecesary overhead of creating multiple pointers - ;; and handly internally with objects map in their worst - ;; case (when probably all shapes and all pointers will be - ;; readed in any case), we just realize/resolve them before - ;; applying the migration to the file - file (if (fmg/need-migration? file) - (-> file - (update :data feat.fdata/process-pointers deref) - (update :data feat.fdata/process-objects (partial into {})) - (fmg/migrate-file)) - file) - - ;; WARNING: this ruins performance; maybe we need to find +(defn- process-changes-and-validate + [cfg file changes skip-validate] + (let [;; WARNING: this ruins performance; maybe we need to find ;; some other way to do general validation libs (when (and (or (contains? cf/flags :file-validation) (contains? cf/flags :soft-file-validation)) (not skip-validate)) - (->> (files/get-file-libraries conn (:id file)) - (into [file] (map (fn [{:keys [id]}] - (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id) - pmap/*tracked* nil] - ;; We do not resolve the objects maps here - ;; because there is a lower probability that all - ;; shapes needed to be loded into memory, so we - ;; leeave it on lazy status - (-> (files/get-file cfg id :migrate? false) - (update :data feat.fdata/process-pointers deref) ; ensure all pointers resolved - (update :data feat.fdata/process-objects (partial into {})) - (fmg/migrate-file)))))) - (d/index-by :id))) - + (get-file-libraries cfg file)) file (-> (files/check-version! file) (update :revn inc) (update :data cpc/process-changes changes) (update :data d/without-nils))] - (when (contains? cf/flags :soft-file-validation) - (soft-validate-file! file libs)) + (binding [pmap/*tracked* nil] + (when (contains? cf/flags :soft-file-validation) + (soft-validate-file! file libs)) - (when (contains? cf/flags :soft-file-schema-validation) - (soft-validate-file-schema! file)) + (when (contains? cf/flags :soft-file-schema-validation) + (soft-validate-file-schema! file)) - (when (and (contains? cf/flags :file-validation) - (not skip-validate)) - (val/validate-file! file libs)) + (when (and (contains? cf/flags :file-validation) + (not skip-validate)) + (val/validate-file! file libs)) - (when (and (contains? cf/flags :file-schema-validation) - (not skip-validate)) - (val/validate-file-schema! file)) + (when (and (contains? cf/flags :file-schema-validation) + (not skip-validate)) + (val/validate-file-schema! file))) - (cond-> file - (contains? cfeat/*current* "fdata/objects-map") - (feat.fdata/enable-objects-map) - - (contains? cfeat/*current* "fdata/pointer-map") - (feat.fdata/enable-pointer-map) - - :always - (update :data blob/encode)))) + file)) (defn- take-snapshot? "Defines the rule when file `data` snapshot should be saved." [{:keys [revn modified-at] :as file}] - (let [freq (or (cf/get :file-change-snapshot-every) 20) - timeout (or (cf/get :file-change-snapshot-timeout) - (dt/duration {:hours 1}))] - (or (= 1 freq) - (zero? (mod revn freq)) - (> (inst-ms (dt/diff modified-at (dt/now))) - (inst-ms timeout))))) - -(def ^:private - sql:lagged-changes + (when (contains? cf/flags :auto-file-snapshot) + (let [freq (or (cf/get :auto-file-snapshot-every) 20) + timeout (or (cf/get :auto-file-snapshot-timeout) + (dt/duration {:hours 1}))] + + (or (= 1 freq) + (zero? (mod revn freq)) + (> (inst-ms (dt/diff modified-at (dt/now))) + (inst-ms timeout)))))) + +;; Get the latest available snapshots without exceeding the total +;; snapshot limit. +(def ^:private sql:get-latest-snapshots + "SELECT fch.id, fch.created_at + FROM file_change AS fch + WHERE fch.file_id = ? + AND fch.label LIKE 'internal/%' + ORDER BY fch.created_at DESC + LIMIT ?") + +;; Mark all snapshots that are outside the allowed total threshold +;; available for the GC. +(def ^:private sql:delete-snapshots + "UPDATE file_change + SET label = NULL + WHERE file_id = ? + AND label LIKE 'internal/%' + AND created_at < ?") + +(defn- delete-old-snapshots! + [{:keys [::db/conn] :as cfg} {:keys [id] :as file}] + (when-let [snapshots (not-empty (db/exec! conn [sql:get-latest-snapshots id + (cf/get :auto-file-snapshot-total 10)]))] + (let [last-date (-> snapshots peek :created-at) + result (db/exec-one! conn [sql:delete-snapshots id last-date])] + (l/trc :hint "delete old snapshots" :file-id (str id) :total (db/get-update-count result))))) + +(def ^:private sql:lagged-changes "select s.id, s.revn, s.file_id, s.session_id, s.changes from file_change as s diff --git a/backend/src/app/rpc/commands/fonts.clj b/backend/src/app/rpc/commands/fonts.clj index 0942da601d1487020a8262929671b45f166f01e7..51081eb19e3c4ae10ed4223dbe19afc796b2435f 100644 --- a/backend/src/app/rpc/commands/fonts.clj +++ b/backend/src/app/rpc/commands/fonts.clj @@ -95,12 +95,11 @@ [cfg {:keys [::rpc/profile-id team-id] :as params}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] - (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] - (teams/check-edition-permissions! conn profile-id team-id) - (quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team - ::quotes/profile-id profile-id - ::quotes/team-id team-id}) - (create-font-variant cfg (assoc params :profile-id profile-id)))))) + (teams/check-edition-permissions! conn profile-id team-id) + (quotes/check-quote! conn {::quotes/id ::quotes/font-variants-per-team + ::quotes/profile-id profile-id + ::quotes/team-id team-id}) + (create-font-variant cfg (assoc params :profile-id profile-id))))) (defn create-font-variant [{:keys [::sto/storage ::db/conn ::wrk/executor]} {:keys [data] :as params}] @@ -203,14 +202,13 @@ ::sm/params schema:delete-font} [cfg {:keys [::rpc/profile-id id team-id]}] (db/tx-run! cfg - (fn [{:keys [::db/conn ::sto/storage] :as cfg}] + (fn [{:keys [::db/conn] :as cfg}] (teams/check-edition-permissions! conn profile-id team-id) (let [fonts (db/query conn :team-font-variant {:team-id team-id :font-id id :deleted-at nil} {::sql/for-update true}) - storage (media/configure-assets-storage storage conn) tnow (dt/now)] (when-not (seq fonts) @@ -220,11 +218,7 @@ (doseq [font fonts] (db/update! conn :team-font-variant {:deleted-at tnow} - {:id (:id font)}) - (some->> (:woff1-file-id font) (sto/touch-object! storage)) - (some->> (:woff2-file-id font) (sto/touch-object! storage)) - (some->> (:ttf-file-id font) (sto/touch-object! storage)) - (some->> (:otf-file-id font) (sto/touch-object! storage))) + {:id (:id font)})) (rph/with-meta (rph/wrap) {::audit/props {:id id @@ -245,22 +239,16 @@ ::sm/params schema:delete-font-variant} [cfg {:keys [::rpc/profile-id id team-id]}] (db/tx-run! cfg - (fn [{:keys [::db/conn ::sto/storage] :as cfg}] + (fn [{:keys [::db/conn] :as cfg}] (teams/check-edition-permissions! conn profile-id team-id) (let [variant (db/get conn :team-font-variant {:id id :team-id team-id} - {::sql/for-update true}) - storage (media/configure-assets-storage storage conn)] + {::sql/for-update true})] (db/update! conn :team-font-variant {:deleted-at (dt/now)} {:id (:id variant)}) - (some->> (:woff1-file-id variant) (sto/touch-object! storage)) - (some->> (:woff2-file-id variant) (sto/touch-object! storage)) - (some->> (:ttf-file-id variant) (sto/touch-object! storage)) - (some->> (:otf-file-id variant) (sto/touch-object! storage)) - (rph/with-meta (rph/wrap) {::audit/props {:font-family (:font-family variant) :font-id (:font-id variant)}}))))) diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index bb86aec90db4d35874132c051d18a3b08c26b389..0829987d4d1f4f92626dc6bad48045098a1c39a8 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -8,39 +8,37 @@ (:require [app.auth.ldap :as ldap] [app.common.exceptions :as ex] - [app.common.spec :as us] + [app.common.schema :as sm] [app.db :as db] [app.http.session :as session] [app.loggers.audit :as-alias audit] - [app.main :as-alias main] [app.rpc :as-alias rpc] [app.rpc.commands.auth :as auth] [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] + [app.setup :as-alias setup] [app.tokens :as tokens] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) + [app.util.services :as sv])) ;; --- COMMAND: login-with-ldap (declare login-or-register) -(s/def ::email ::us/email) -(s/def ::password ::us/string) -(s/def ::invitation-token ::us/string) - -(s/def ::login-with-ldap - (s/keys :req-un [::email ::password] - :opt-un [::invitation-token])) +(def schema:login-with-ldap + [:map {:title "login-with-ldap"} + [:email ::sm/email] + [:password auth/schema:password] + [:invitation-token {:optional true} auth/schema:token]]) (sv/defmethod ::login-with-ldap "Performs the authentication using LDAP backend. Only works if LDAP is properly configured and enabled with `login-with-ldap` flag." {::rpc/auth false ::doc/added "1.15" - ::doc/module :auth} - [{:keys [::main/props ::ldap/provider] :as cfg} params] + ::doc/module :auth + ::sm/params schema:login-with-ldap} + [{:keys [::setup/props ::ldap/provider] :as cfg} params] (when-not provider (ex/raise :type :restriction :code :ldap-not-initialized @@ -72,7 +70,7 @@ (rph/with-meta {::audit/props (:props profile) ::audit/profile-id (:id profile)}))) - (-> profile + (-> (profile/strip-private-attrs profile) (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta {::audit/props (:props profile) ::audit/profile-id (:id profile)})))))) diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 5d01d9ec60ca8c907e15c878e13af9ed3f0fbfbb..30d0d3460f0e0f0a41fdedf0d3149c8fefd468e6 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -16,6 +16,7 @@ [app.config :as cf] [app.db :as db] [app.http.sse :as sse] + [app.loggers.audit :as audit] [app.loggers.webhooks :as-alias webhooks] [app.rpc :as-alias rpc] [app.rpc.commands.files :as files] @@ -90,7 +91,7 @@ (sm/define [:map {:title "duplicate-file"} [:file-id ::sm/uuid] - [:name {:optional true} :string]])) + [:name {:optional true} [:string {:max 250}]]])) (sv/defmethod ::duplicate-file "Duplicate a single file in the same team." @@ -152,7 +153,7 @@ (sm/define [:map {:title "duplicate-project"} [:project-id ::sm/uuid] - [:name {:optional true} :string]])) + [:name {:optional true} [:string {:max 250}]]])) (sv/defmethod ::duplicate-project "Duplicate an entire project with all the files" @@ -381,10 +382,9 @@ (def ^:private schema:move-project - (sm/define - [:map {:title "move-project"} - [:team-id ::sm/uuid] - [:project-id ::sm/uuid]])) + [:map {:title "move-project"} + [:team-id ::sm/uuid] + [:project-id ::sm/uuid]]) (sv/defmethod ::move-project "Move projects between teams" @@ -396,25 +396,38 @@ ;; --- COMMAND: Clone Template -(defn- clone-template - [{:keys [::wrk/executor ::bf.v1/project-id] :as cfg} template] - (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] +(defn clone-template + [cfg {:keys [project-id profile-id] :as params} template] + (db/tx-run! cfg (fn [{:keys [::db/conn ::wrk/executor] :as cfg}] ;; NOTE: the importation process performs some operations that ;; are not very friendly with virtual threads, and for avoid ;; unexpected blocking of other concurrent operations we ;; dispatch that operation to a dedicated executor. - (let [result (px/submit! executor (partial bf.v1/import-files! cfg template))] + (let [cfg (-> cfg + (assoc ::bf.v1/project-id project-id) + (assoc ::bf.v1/profile-id profile-id)) + result (px/invoke! executor (partial bf.v1/import-files! cfg template))] + (db/update! conn :project {:modified-at (dt/now)} {:id project-id}) - (deref result))))) + + (let [props (audit/clean-props params)] + (doseq [file-id result] + (let [props (assoc props :id file-id) + event (-> (audit/event-from-rpc-params params) + (assoc ::audit/profile-id profile-id) + (assoc ::audit/name "create-file") + (assoc ::audit/props props))] + (audit/submit! cfg event)))) + + result)))) (def ^:private schema:clone-template - (sm/define - [:map {:title "clone-template"} - [:project-id ::sm/uuid] - [:template-id ::sm/word-string]])) + [:map {:title "clone-template"} + [:project-id ::sm/uuid] + [:template-id ::sm/word-string]]) (sv/defmethod ::clone-template "Clone into the specified project the template by its id." @@ -426,15 +439,14 @@ (let [project (db/get-by-id pool :project project-id {:columns [:id :team-id]}) _ (teams/check-edition-permissions! pool profile-id (:team-id project)) template (tmpl/get-template-stream cfg template-id) - params (-> cfg - (assoc ::bf.v1/project-id (:id project)) - (assoc ::bf.v1/profile-id profile-id))] + params (assoc params :profile-id profile-id)] + (when-not template (ex/raise :type :not-found :code :template-not-found :hint "template not found")) - (sse/response #(clone-template params template)))) + (sse/response #(clone-template cfg params template)))) ;; --- COMMAND: Get list of builtin templates diff --git a/backend/src/app/rpc/commands/media.clj b/backend/src/app/rpc/commands/media.clj index 1bdcd3c5028efc9aab053e94477b0408259ca070..0a5c38e348d0d132025a44ab29c006ef0e8a9094 100644 --- a/backend/src/app/rpc/commands/media.clj +++ b/backend/src/app/rpc/commands/media.clj @@ -9,7 +9,7 @@ [app.common.data :as d] [app.common.exceptions :as ex] [app.common.media :as cm] - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -25,7 +25,6 @@ [app.util.services :as sv] [app.util.time :as dt] [app.worker :as-alias wrk] - [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.io :as io] [promesa.exec :as px])) @@ -39,43 +38,37 @@ :quality 85 :format :jpeg}) -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) -(s/def ::file-id ::us/uuid) -(s/def ::team-id ::us/uuid) - ;; --- Create File Media object (upload) (declare create-file-media-object) -(s/def ::content ::media/upload) -(s/def ::is-local ::us/boolean) - -(s/def ::upload-file-media-object - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::is-local ::name ::content] - :opt-un [::id])) +(def ^:private schema:upload-file-media-object + [:map {:title "upload-file-media-object"} + [:id {:optional true} ::sm/uuid] + [:file-id ::sm/uuid] + [:is-local ::sm/boolean] + [:name [:string {:max 250}]] + [:content ::media/upload]]) (sv/defmethod ::upload-file-media-object {::doc/added "1.17" + ::sm/params schema:upload-file-media-object ::climit/id [[:process-image/by-profile ::rpc/profile-id] [:process-image/global]]} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id content] :as params}] - (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] - - (files/check-edition-permissions! pool profile-id file-id) - (media/validate-media-type! content) - (media/validate-media-size! content) - - (db/run! cfg (fn [cfg] - (let [object (create-file-media-object cfg params) - props {:name (:name params) - :file-id file-id - :is-local (:is-local params) - :size (:size content) - :mtype (:mtype content)}] - (with-meta object - {::audit/replace-props props})))))) + (files/check-edition-permissions! pool profile-id file-id) + (media/validate-media-type! content) + (media/validate-media-size! content) + + (db/run! cfg (fn [cfg] + (let [object (create-file-media-object cfg params) + props {:name (:name params) + :file-id file-id + :is-local (:is-local params) + :size (:size content) + :mtype (:mtype content)}] + (with-meta object + {::audit/replace-props props}))))) (defn- big-enough-for-thumbnail? "Checks if the provided image info is big enough for @@ -176,18 +169,20 @@ (declare ^:private create-file-media-object-from-url) -(s/def ::create-file-media-object-from-url - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::is-local ::url] - :opt-un [::id ::name])) +(def ^:private schema:create-file-media-object-from-url + [:map {:title "create-file-media-object-from-url"} + [:file-id ::sm/uuid] + [:is-local ::sm/boolean] + [:url ::sm/uri] + [:id {:optional true} ::sm/uuid] + [:name {:optional true} [:string {:max 250}]]]) (sv/defmethod ::create-file-media-object-from-url {::doc/added "1.17" - ::doc/deprecated "1.19"} + ::sm/params schema:create-file-media-object-from-url} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] - (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] - (files/check-edition-permissions! pool profile-id file-id) - (create-file-media-object-from-url cfg (assoc params :profile-id profile-id)))) + (files/check-edition-permissions! pool profile-id file-id) + (create-file-media-object-from-url cfg (assoc params :profile-id profile-id))) (defn download-image [{:keys [::http/client]} uri] @@ -243,23 +238,27 @@ ;; NOTE: we use the climit here in a dynamic invocation because we ;; don't want saturate the process-image limit with IO (download ;; of external image) + (-> cfg (assoc ::climit/id [[:process-image/by-profile (:profile-id params)] [:process-image/global]]) - (assoc ::climit/profile-id (:profile-id params)) (assoc ::climit/label "create-file-media-object-from-url") - (climit/invoke! db/run! cfg create-file-media-object params)))) + (climit/invoke! #(db/run! %1 create-file-media-object %2) params)))) + ;; --- Clone File Media object (Upload and create from url) (declare clone-file-media-object) -(s/def ::clone-file-media-object - (s/keys :req [::rpc/profile-id] - :req-un [::file-id ::is-local ::id])) +(def ^:private schema:clone-file-media-object + [:map {:title "clone-file-media-object"} + [:file-id ::sm/uuid] + [:is-local ::sm/boolean] + [:id ::sm/uuid]]) (sv/defmethod ::clone-file-media-object - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:clone-file-media-object} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id file-id] :as params}] (db/with-atomic [conn pool] (files/check-edition-permissions! conn profile-id file-id) diff --git a/backend/src/app/rpc/commands/profile.clj b/backend/src/app/rpc/commands/profile.clj index 6ef2ef90d2e1818dfa5455beadcf7b5ef0a036b6..3108fcbb2eefa527af9bb54a000aacfed6937627 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -23,11 +23,12 @@ [app.rpc.climit :as climit] [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] + [app.setup :as-alias setup] [app.storage :as sto] [app.tokens :as tokens] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as-alias wrk] + [app.worker :as wrk] [cuerdas.core :as str] [promesa.exec :as px])) @@ -45,6 +46,10 @@ (let [email (str/lower email) email (if (str/starts-with? email "mailto:") (subs email 7) + email) + email (if (or (str/starts-with? email "<") + (str/ends-with? email ">")) + (str/trim email "<>") email)] email)) @@ -55,10 +60,10 @@ [:id ::sm/uuid] [:fullname [::sm/word-string {:max 250}]] [:email ::sm/email] - [:is-active {:optional true} :boolean] - [:is-blocked {:optional true} :boolean] - [:is-demo {:optional true} :boolean] - [:is-muted {:optional true} :boolean] + [:is-active {:optional true} ::sm/boolean] + [:is-blocked {:optional true} ::sm/boolean] + [:is-demo {:optional true} ::sm/boolean] + [:is-muted {:optional true} ::sm/boolean] [:created-at {:optional true} ::sm/inst] [:modified-at {:optional true} ::sm/inst] [:default-project-id {:optional true} ::sm/uuid] @@ -86,8 +91,8 @@ (defn get-profile "Get profile by id. Throws not-found exception if no profile found." - [conn id & {:as attrs}] - (-> (db/get-by-id conn :profile id attrs) + [conn id & {:as opts}] + (-> (db/get-by-id conn :profile id opts) (decode-row))) ;; --- MUTATION: Update Profile (own) @@ -97,7 +102,7 @@ (sm/define [:map {:title "update-profile"} [:fullname [::sm/word-string {:max 250}]] - [:lang {:optional true} [:string {:max 5}]] + [:lang {:optional true} [:string {:max 8}]] [:theme {:optional true} [:string {:max 250}]]])) (sv/defmethod ::update-profile @@ -105,7 +110,6 @@ ::sm/params schema:update-profile ::sm/result schema:profile} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}] - (db/with-atomic [conn pool] ;; NOTE: we need to retrieve the profile independently if we use ;; it or not for explicit locking and avoid concurrent updates of @@ -206,8 +210,7 @@ [cfg {:keys [::rpc/profile-id file] :as params}] ;; Validate incoming mime type (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) - (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] - (update-profile-photo cfg (assoc params :profile-id profile-id)))) + (update-profile-photo cfg (assoc params :profile-id profile-id))) (defn update-profile-photo [{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id file] :as params}] @@ -232,7 +235,7 @@ :file-mtype (:mtype file)}})))) (defn- generate-thumbnail! - [file] + [_ file] (let [input (media/run {:cmd :info :input file}) thumb (media/run {:cmd :profile-thumbnail :format :jpeg @@ -249,15 +252,15 @@ :content-type (:mtype thumb)})) (defn upload-photo - [{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file]}] + [{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file] :as params}] (let [params (-> cfg - (assoc ::climit/id :process-image/global) + (assoc ::climit/id [[:process-image/by-profile (:profile-id params)] + [:process-image/global]]) (assoc ::climit/label "upload-photo") (assoc ::climit/executor executor) (climit/invoke! generate-thumbnail! file))] (sto/put-object! storage params))) - ;; --- MUTATION: Request Email Change (declare ^:private request-email-change!) @@ -272,19 +275,19 @@ (sv/defmethod ::request-email-change {::doc/added "1.0" ::sm/params schema:request-email-change} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id email] :as params}] - (db/with-atomic [conn pool] - (let [profile (db/get-by-id conn :profile profile-id) - cfg (assoc cfg ::conn conn) - params (assoc params - :profile profile - :email (clean-email email))] - (if (contains? cf/flags :smtp) - (request-email-change! cfg params) - (change-email-immediately! cfg params))))) + [cfg {:keys [::rpc/profile-id email] :as params}] + (db/tx-run! cfg + (fn [cfg] + (let [profile (db/get-by-id cfg :profile profile-id) + params (assoc params + :profile profile + :email (clean-email email))] + (if (contains? cf/flags :smtp) + (request-email-change! cfg params) + (change-email-immediately! cfg params)))))) (defn- change-email-immediately! - [{:keys [::conn]} {:keys [profile email] :as params}] + [{:keys [::db/conn]} {:keys [profile email] :as params}] (when (not= email (:email profile)) (check-profile-existence! conn params)) @@ -295,13 +298,13 @@ {:changed true}) (defn- request-email-change! - [{:keys [::conn] :as cfg} {:keys [profile email] :as params}] - (let [token (tokens/generate (::main/props cfg) + [{:keys [::db/conn] :as cfg} {:keys [profile email] :as params}] + (let [token (tokens/generate (::setup/props cfg) {:iss :change-email :exp (dt/in-future "15m") :profile-id (:id profile) :email email}) - ptoken (tokens/generate (::main/props cfg) + ptoken (tokens/generate (::setup/props cfg) {:iss :profile-identity :profile-id (:id profile) :exp (dt/in-future {:days 30})})] @@ -315,9 +318,28 @@ :hint "looks like the profile has reported repeatedly as spam or has permanent bounces.")) (when (eml/has-bounce-reports? conn email) - (ex/raise :type :validation + (ex/raise :type :restriction :code :email-has-permanent-bounces - :hint "looks like the email you invite has been repeatedly reported as spam or permanent bounce")) + :email email + :hint "looks like the email has bounce reports")) + + (when (eml/has-complaint-reports? conn email) + (ex/raise :type :restriction + :code :email-has-complaints + :email email + :hint "looks like the email has spam complaint reports")) + + (when (eml/has-bounce-reports? conn (:email profile)) + (ex/raise :type :restriction + :code :email-has-permanent-bounces + :email (:email profile) + :hint "looks like the email has bounce reports")) + + (when (eml/has-complaint-reports? conn (:email profile)) + (ex/raise :type :restriction + :code :email-has-complaints + :email (:email profile) + :hint "looks like the email has spam complaint reports")) (eml/send! {::eml/conn conn ::eml/factory eml/change-email @@ -338,37 +360,41 @@ [:map {:title "update-profile-props"} [:props [:map-of :keyword :any]]])) +(defn update-profile-props + [{:keys [::db/conn] :as cfg} profile-id props] + (let [profile (get-profile conn profile-id ::sql/for-update true) + props (reduce-kv (fn [props k v] + ;; We don't accept namespaced keys + (if (simple-ident? k) + (if (nil? v) + (dissoc props k) + (assoc props k v)) + props)) + (:props profile) + props)] + + (db/update! conn :profile + {:props (db/tjson props)} + {:id profile-id}) + + (filter-props props))) + (sv/defmethod ::update-profile-props {::doc/added "1.0" ::sm/params schema:update-profile-props} - [{:keys [::db/pool]} {:keys [::rpc/profile-id props]}] - (db/with-atomic [conn pool] - (let [profile (get-profile conn profile-id ::sql/for-update true) - props (reduce-kv (fn [props k v] - ;; We don't accept namespaced keys - (if (simple-ident? k) - (if (nil? v) - (dissoc props k) - (assoc props k v)) - props)) - (:props profile) - props)] - - (db/update! conn :profile - {:props (db/tjson props)} - {:id profile-id}) - - (filter-props props)))) + [cfg {:keys [::rpc/profile-id props]}] + (db/tx-run! cfg (fn [cfg] + (update-profile-props cfg profile-id props)))) ;; --- MUTATION: Delete Profile -(declare ^:private get-owned-teams-with-participants) +(declare ^:private get-owned-teams) (sv/defmethod ::delete-profile {::doc/added "1.0"} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] - (let [teams (get-owned-teams-with-participants conn profile-id) + (let [teams (get-owned-teams conn profile-id) deleted-at (dt/now)] ;; If we found owned teams with participants, we don't allow @@ -380,37 +406,39 @@ :hint "The user need to transfer ownership of owned teams." :context {:teams (mapv :id teams)})) - (doseq [{:keys [id]} teams] - (db/update! conn :team - {:deleted-at deleted-at} - {:id id})) - + ;; Mark profile deleted immediatelly (db/update! conn :profile {:deleted-at deleted-at} {:id profile-id}) + ;; Schedule cascade deletion to a worker + (wrk/submit! {::db/conn conn + ::wrk/task :delete-object + ::wrk/params {:object :profile + :deleted-at deleted-at + :id profile-id}}) + (rph/with-transform {} (session/delete-fn cfg))))) ;; --- HELPERS (def sql:owned-teams - "with owner_teams as ( - select tpr.team_id as id - from team_profile_rel as tpr - where tpr.is_owner is true - and tpr.profile_id = ? + "WITH owner_teams AS ( + SELECT tpr.team_id AS id + FROM team_profile_rel AS tpr + WHERE tpr.is_owner IS TRUE + AND tpr.profile_id = ? ) - select tpr.team_id as id, - count(tpr.profile_id) - 1 as participants - from team_profile_rel as tpr - where tpr.team_id in (select id from owner_teams) - and tpr.profile_id != ? - group by 1") - -(defn- get-owned-teams-with-participants + SELECT tpr.team_id AS id, + count(tpr.profile_id) - 1 AS participants + FROM team_profile_rel AS tpr + WHERE tpr.team_id IN (SELECT id from owner_teams) + GROUP BY 1") + +(defn get-owned-teams [conn profile-id] - (db/exec! conn [sql:owned-teams profile-id profile-id])) + (db/exec! conn [sql:owned-teams profile-id])) (def ^:private sql:profile-existence "select exists (select * from profile diff --git a/backend/src/app/rpc/commands/projects.clj b/backend/src/app/rpc/commands/projects.clj index b8a555f449ad460d2869d9133a8169a228237f04..3442fe80fc3d40ed1fd873c3532c31aaff95c329 100644 --- a/backend/src/app/rpc/commands/projects.clj +++ b/backend/src/app/rpc/commands/projects.clj @@ -7,7 +7,8 @@ (ns app.rpc.commands.projects (:require [app.common.data.macros :as dm] - [app.common.spec :as us] + [app.common.exceptions :as ex] + [app.common.schema :as sm] [app.db :as db] [app.db.sql :as-alias sql] [app.loggers.audit :as-alias audit] @@ -20,10 +21,7 @@ [app.rpc.quotes :as quotes] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s])) - -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) + [app.worker :as wrk])) ;; --- Check Project Permissions @@ -73,13 +71,13 @@ (declare get-projects) -(s/def ::team-id ::us/uuid) -(s/def ::get-projects - (s/keys :req [::rpc/profile-id] - :req-un [::team-id])) +(def ^:private schema:get-projects + [:map {:title "get-projects"} + [:team-id ::sm/uuid]]) (sv/defmethod ::get-projects - {::doc/added "1.18"} + {::doc/added "1.18" + ::sm/params schema:get-projects} [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id]}] (dm/with-open [conn (db/open pool)] (teams/check-read-permissions! conn profile-id team-id) @@ -110,11 +108,12 @@ (declare get-all-projects) -(s/def ::get-all-projects - (s/keys :req [::rpc/profile-id])) +(def ^:private schema:get-all-projects + [:map {:title "get-all-projects"}]) (sv/defmethod ::get-all-projects - {::doc/added "1.18"} + {::doc/added "1.18" + ::sm/params schema:get-all-projects} [{:keys [::db/pool]} {:keys [::rpc/profile-id]}] (dm/with-open [conn (db/open pool)] (get-all-projects conn profile-id))) @@ -152,12 +151,13 @@ ;; --- QUERY: Get project -(s/def ::get-project - (s/keys :req [::rpc/profile-id] - :req-un [::id])) +(def ^:private schema:get-project + [:map {:title "get-project"} + [:id ::sm/uuid]]) (sv/defmethod ::get-project - {::doc/added "1.18"} + {::doc/added "1.18" + ::sm/params schema:get-project} [{:keys [::db/pool]} {:keys [::rpc/profile-id id]}] (dm/with-open [conn (db/open pool)] (let [project (db/get-by-id conn :project id)] @@ -168,14 +168,16 @@ ;; --- MUTATION: Create Project -(s/def ::create-project - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::name] - :opt-un [::id])) +(def ^:private schema:create-project + [:map {:title "create-project"} + [:team-id ::sm/uuid] + [:name [:string {:max 250 :min 1}]] + [:id {:optional true} ::sm/uuid]]) (sv/defmethod ::create-project {::doc/added "1.18" - ::webhooks/event? true} + ::webhooks/event? true + ::sm/params schema:create-project} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] (db/with-atomic [conn pool] (teams/check-edition-permissions! conn profile-id team-id) @@ -190,8 +192,8 @@ {:project-id (:id project) :profile-id profile-id :team-id team-id - :is-pinned true}) - (assoc project :is-pinned true)))) + :is-pinned false}) + (assoc project :is-pinned false)))) ;; --- MUTATION: Toggle Project Pin @@ -203,14 +205,15 @@ on conflict (team_id, project_id, profile_id) do update set is_pinned=?") -(s/def ::is-pinned ::us/boolean) -(s/def ::project-id ::us/uuid) -(s/def ::update-project-pin - (s/keys :req [::rpc/profile-id] - :req-un [::id ::team-id ::is-pinned])) +(def ^:private schema:update-project-pin + [:map {:title "update-project-pin"} + [:team-id ::sm/uuid] + [:is-pinned ::sm/boolean] + [:id ::sm/uuid]]) (sv/defmethod ::update-project-pin {::doc/added "1.18" + ::sm/params schema:update-project-pin ::webhooks/batch-timeout (dt/duration "5s") ::webhooks/batch-key (webhooks/key-fn ::rpc/profile-id :id) ::webhooks/event? true} @@ -224,12 +227,14 @@ (declare rename-project) -(s/def ::rename-project - (s/keys :req [::rpc/profile-id] - :req-un [::name ::id])) +(def ^:private schema:rename-project + [:map {:title "rename-project"} + [:name [:string {:max 250 :min 1}]] + [:id ::sm/uuid]]) (sv/defmethod ::rename-project {::doc/added "1.18" + ::sm/params schema:rename-project ::webhooks/event? true} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}] (db/with-atomic [conn pool] @@ -244,28 +249,41 @@ ;; --- MUTATION: Delete Project -(s/def ::delete-project - (s/keys :req [::rpc/profile-id] - :req-un [::id])) +(defn- delete-project + [conn project-id] + (let [project (db/update! conn :project + {:deleted-at (dt/now)} + {:id project-id} + {::db/return-keys true})] + + (when (:is-default project) + (ex/raise :type :validation + :code :non-deletable-project + :hint "impossible to delete default project")) -;; TODO: right now, we just don't allow delete default projects, in a -;; future we need to ensure raise a correct exception signaling that -;; this is not allowed. + (wrk/submit! {::db/conn conn + ::wrk/task :delete-object + ::wrk/params {:object :project + :deleted-at (:deleted-at project) + :id project-id}}) + + project)) + + +(def ^:private schema:delete-project + [:map {:title "delete-project"} + [:id ::sm/uuid]]) (sv/defmethod ::delete-project {::doc/added "1.18" + ::sm/params schema:delete-project ::webhooks/event? true} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) - (let [project (db/update! conn :project - {:deleted-at (dt/now)} - {:id id :is-default false} - {::db/return-keys true})] + (let [project (delete-project conn id)] (rph/with-meta (rph/wrap) {::audit/props {:team-id (:team-id project) :name (:name project) :created-at (:created-at project) :modified-at (:modified-at project)}})))) - - diff --git a/backend/src/app/rpc/commands/search.clj b/backend/src/app/rpc/commands/search.clj index 57101564588f5776a51c65c285540e376d93046f..1a25a6dcfd7d90be689c073c1de73d8247efc77e 100644 --- a/backend/src/app/rpc/commands/search.clj +++ b/backend/src/app/rpc/commands/search.clj @@ -6,13 +6,12 @@ (ns app.rpc.commands.search (:require - [app.common.spec :as us] + [app.common.schema :as sm] [app.db :as db] [app.rpc :as-alias rpc] [app.rpc.commands.files :refer [resolve-public-uri]] [app.rpc.doc :as-alias doc] - [app.util.services :as sv] - [clojure.spec.alpha :as s])) + [app.util.services :as sv])) (def ^:private sql:search-files "with projects as ( @@ -65,16 +64,14 @@ (assoc :thumbnail-uri (resolve-public-uri media-id))) (dissoc row :media-id)))))) -(s/def ::team-id ::us/uuid) -(s/def ::search-files ::us/string) - -(s/def ::search-files - (s/keys :req [::rpc/profile-id] - :req-un [::team-id] - :opt-un [::search-term])) +(def ^:private schema:search-files + [:map {:title "search-files"} + [:team-id ::sm/uuid] + [:search-term {:optional true} :string]]) (sv/defmethod ::search-files {::doc/added "1.17" - ::doc/module :files} + ::doc/module :files + ::sm/params schema:search-files} [{:keys [::db/pool]} {:keys [::rpc/profile-id team-id search-term]}] (some->> search-term (search-files pool profile-id team-id))) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 4b5f077001249b3c7ac1c91de46cea1d9526b63d..444b89184666ac10e6ba08b5aaf4762bf03562f4 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -12,10 +12,10 @@ [app.common.features :as cfeat] [app.common.logging :as l] [app.common.schema :as sm] - [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.db.sql :as sql] [app.email :as eml] [app.loggers.audit :as audit] [app.main :as-alias main] @@ -26,20 +26,17 @@ [app.rpc.helpers :as rph] [app.rpc.permissions :as perms] [app.rpc.quotes :as quotes] + [app.setup :as-alias setup] [app.storage :as sto] [app.tokens :as tokens] + [app.util.blob :as blob] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s] + [app.worker :as wrk] [cuerdas.core :as str])) ;; --- Helpers & Specs -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) -(s/def ::file-id ::us/uuid) -(s/def ::team-id ::us/uuid) - (def ^:private sql:team-permissions "select tpr.is_owner, tpr.is_admin, @@ -85,6 +82,37 @@ (cond-> row (some? features) (assoc :features (db/decode-pgarray features #{})))) + + +(defn- check-valid-email-muted + "Check if the member's email is part of the global bounce report." + [conn member] + (let [email (profile/clean-email (:email member))] + (when (and member (not (eml/allow-send-emails? conn member))) + (ex/raise :type :validation + :code :member-is-muted + :email email + :hint "the profile has reported repeatedly as spam or has bounces")))) + +(defn- check-valid-email-bounce + "Check if the email is part of the global complain report" + [conn email show?] + (when (eml/has-bounce-reports? conn email) + (ex/raise :type :restriction + :code :email-has-permanent-bounces + :email (if show? email "private") + :hint "this email has been repeatedly reported as bounce"))) + +(defn- check-valid-email-spam + "Check if the member email is part of the global complain report" + [conn email show?] + (when (eml/has-complaint-reports? conn email) + (ex/raise :type :restriction + :code :email-has-complaints + :email (if show? email "private") + :hint "this email has been repeatedly reported as spam"))) + + ;; --- Query: Teams (declare get-teams) @@ -338,6 +366,24 @@ (check-read-permissions! conn profile-id team-id) (get-team-invitations conn team-id))) + +;; --- COMMAND QUERY: get-team-info + +(defn- get-team-info + [{:keys [::db/conn] :as cfg} {:keys [id] :as params}] + (db/get* conn :team + {:id id} + {::sql/columns [:id :is-default]})) + +(sv/defmethod ::get-team-info + "Retrieve minimal team info by its ID." + {::rpc/auth false + ::doc/added "2.2.0" + ::sm/params schema:get-team} + [cfg params] + (db/tx-run! cfg get-team-info params)) + + ;; --- Mutation: Create Team (declare create-team) @@ -349,7 +395,7 @@ (def ^:private schema:create-team [:map {:title "create-team"} - [:name :string] + [:name [:string {:max 250}]] [:features {:optional true} ::cfeat/features] [:id {:optional true} ::sm/uuid]]) @@ -362,10 +408,12 @@ ::quotes/profile-id profile-id}) (let [features (-> (cfeat/get-enabled-features cf/flags) - (cfeat/check-client-features! (:features params)))] - (create-team cfg (assoc params - :profile-id profile-id - :features features)))))) + (cfeat/check-client-features! (:features params))) + team (create-team cfg (assoc params + :profile-id profile-id + :features features))] + (with-meta team + {::audit/props {:id (:id team)}}))))) (defn create-team "This is a complete team creation process, it creates the team @@ -436,12 +484,14 @@ ;; --- Mutation: Update Team -(s/def ::update-team - (s/keys :req [::rpc/profile-id] - :req-un [::name ::id])) +(def ^:private schema:update-team + [:map {:title "update-team"} + [:name [:string {:max 250}]] + [:id ::sm/uuid]]) (sv/defmethod ::update-team - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:update-team} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id name] :as params}] (db/with-atomic [conn pool] (check-edition-permissions! conn profile-id id) @@ -501,30 +551,49 @@ nil)) -(s/def ::reassign-to ::us/uuid) -(s/def ::leave-team - (s/keys :req [::rpc/profile-id] - :req-un [::id] - :opt-un [::reassign-to])) +(def ^:private schema:leave-team + [:map {:title "leave-team"} + [:id ::sm/uuid] + [:reassign-to {:optional true} ::sm/uuid]]) (sv/defmethod ::leave-team - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:leave-team} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (leave-team conn (assoc params :profile-id profile-id)))) ;; --- Mutation: Delete Team -(s/def ::delete-team - (s/keys :req [::rpc/profile-id] - :req-un [::id])) +(defn- delete-team + "Mark a team for deletion" + [conn team-id] + + (let [deleted-at (dt/now) + team (db/update! conn :team + {:deleted-at deleted-at} + {:id team-id} + {::db/return-keys true})] + + (when (:is-default team) + (ex/raise :type :validation + :code :non-deletable-team + :hint "impossible to delete default team")) + + (wrk/submit! {::db/conn conn + ::wrk/task :delete-object + ::wrk/params {:object :team + :deleted-at deleted-at + :id team-id}}) + team)) -;; TODO: right now just don't allow delete default team, in future it -;; should raise a specific exception for signal that this action is -;; not allowed. +(def ^:private schema:delete-team + [:map {:title "delete-team"} + [:id ::sm/uuid]]) (sv/defmethod ::delete-team - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:delete-team} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id id)] @@ -532,18 +601,11 @@ (ex/raise :type :validation :code :only-owner-can-delete-team)) - (db/update! conn :team - {:deleted-at (dt/now)} - {:id id :is-default false}) + (delete-team conn id) nil))) - ;; --- Mutation: Team Update Role -(s/def ::team-id ::us/uuid) -(s/def ::member-id ::us/uuid) -(s/def ::role #{:owner :admin :editor}) - ;; Temporarily disabled viewer role ;; https://tree.taiga.io/project/penpot/issue/1083 (def valid-roles @@ -607,25 +669,29 @@ :profile-id member-id}) nil))) -(s/def ::update-team-member-role - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::member-id ::role])) +(def ^:private schema:update-team-member-role + [:map {:title "update-team-member-role"} + [:team-id ::sm/uuid] + [:member-id ::sm/uuid] + [:role schema:role]]) (sv/defmethod ::update-team-member-role - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:update-team-member-role} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] (db/with-atomic [conn pool] (update-team-member-role conn (assoc params :profile-id profile-id)))) - ;; --- Mutation: Delete Team Member -(s/def ::delete-team-member - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::member-id])) +(def ^:private schema:delete-team-member + [:map {:title "delete-team-member"} + [:team-id ::sm/uuid] + [:member-id ::sm/uuid]]) (sv/defmethod ::delete-team-member - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:delete-team-member} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] @@ -648,18 +714,18 @@ (declare upload-photo) (declare ^:private update-team-photo) -(s/def ::file ::media/upload) -(s/def ::update-team-photo - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::file])) +(def ^:private schema:update-team-photo + [:map {:title "update-team-photo"} + [:team-id ::sm/uuid] + [:file ::media/upload]]) (sv/defmethod ::update-team-photo - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:update-team-photo} [cfg {:keys [::rpc/profile-id file] :as params}] ;; Validate incoming mime type (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) - (let [cfg (update cfg ::sto/storage media/configure-assets-storage)] - (update-team-photo cfg (assoc params :profile-id profile-id)))) + (update-team-photo cfg (assoc params :profile-id profile-id))) (defn update-team-photo [{:keys [::db/pool ::sto/storage] :as cfg} {:keys [profile-id team-id] :as params}] @@ -691,7 +757,7 @@ (defn- create-invitation-token [cfg {:keys [profile-id valid-until team-id member-id member-email role]}] - (tokens/generate (::main/props cfg) + (tokens/generate (::setup/props cfg) {:iss :team-invitation :exp valid-until :profile-id profile-id @@ -702,7 +768,7 @@ (defn- create-profile-identity-token [cfg profile] - (tokens/generate (::main/props cfg) + (tokens/generate (::setup/props cfg) {:iss :profile-identity :profile-id (:id profile) :exp (dt/in-future {:days 30})})) @@ -712,18 +778,10 @@ (let [email (profile/clean-email email) member (profile/get-profile-by-email conn email)] - (when (and member (not (eml/allow-send-emails? conn member))) - (ex/raise :type :validation - :code :member-is-muted - :email email - :hint "the profile has reported repeatedly as spam or has bounces")) + (check-valid-email-muted conn member) + (check-valid-email-bounce conn email true) + (check-valid-email-spam conn email true) - ;; Secondly check if the invited member email is part of the global spam/bounce report. - (when (eml/has-bounce-reports? conn email) - (ex/raise :type :validation - :code :email-has-permanent-bounces - :email email - :hint "the email you invite has been repeatedly reported as spam or bounce")) ;; When we have email verification disabled and invitation user is ;; already present in the database, we proceed to add it to the @@ -749,6 +807,7 @@ {:id (:id member)})) nil) + (let [id (uuid/next) expire (dt/in-future "168h") ;; 7 days invitation (db/exec-one! conn [sql:upsert-team-invitation id @@ -769,14 +828,16 @@ (when (contains? cf/flags :log-invitation-tokens) (l/info :hint "invitation token" :token itoken)) - (audit/submit! cfg - {::audit/type "action" - ::audit/name (if updated? - "update-team-invitation" - "create-team-invitation") - ::audit/profile-id (:id profile) - ::audit/props (-> (dissoc tprops :profile-id) - (d/without-nils))}) + + (let [props (-> (dissoc tprops :profile-id) + (audit/clean-props)) + evname (if updated? + "update-team-invitation" + "create-team-invitation") + event (-> (audit/event-from-rpc-params params) + (assoc ::audit/name evname) + (assoc ::audit/props props))] + (audit/submit! cfg event)) (eml/send! {::eml/conn conn ::eml/factory eml/invite-to-team @@ -789,11 +850,63 @@ itoken)))) +(defn- add-user-to-team + [conn profile team email role] + + (let [team-id (:id team) + member (db/get* conn :profile + {:email (str/lower email)} + {::sql/columns [:id :email]}) + params (merge + {:team-id team-id + :profile-id (:id member)} + (role->params role))] + + ;; Do not allow blocked users to join teams. + (when (:is-blocked member) + (ex/raise :type :restriction + :code :profile-blocked)) + + (quotes/check-quote! conn + {::quotes/id ::quotes/profiles-per-team + ::quotes/profile-id (:id member) + ::quotes/team-id team-id}) + + ;; Insert the member to the team + (db/insert! conn :team-profile-rel params {::db/on-conflict-do-nothing? true}) + + ;; Delete any request + (db/delete! conn :team-access-request + {:team-id team-id :requester-id (:id member)}) + + ;; Delete any invitation + (db/delete! conn :team-invitation + {:team-id team-id :email-to (:email member)}) + + (eml/send! {::eml/conn conn + ::eml/factory eml/join-team + :public-uri (cf/get :public-uri) + :to email + :invited-by (:fullname profile) + :team (:name team) + :team-id (:id team)}))) + +(def sql:valid-requests-email + "SELECT p.email + FROM team_access_request AS tr + JOIN profile AS p ON (tr.requester_id = p.id) + WHERE tr.team_id = ? + AND tr.auto_join_until > now()") + +(defn- get-valid-requests-email + [conn team-id] + (db/exec! conn [sql:valid-requests-email team-id])) + (def ^:private schema:create-team-invitations [:map {:title "create-team-invitations"} [:team-id ::sm/uuid] - [:role [::sm/one-of #{:owner :admin :editor}]] - [:emails ::sm/set-of-emails]]) + [:role schema:role] + [:emails [::sm/set ::sm/email]]]) (sv/defmethod ::create-team-invitations "A rpc call that allow to send a single or multiple invitations to @@ -821,13 +934,14 @@ (ex/raise :type :validation :code :insufficient-permissions)) - ;; First check if the current profile is allowed to send emails. - (when-not (eml/allow-send-emails? conn profile) - (ex/raise :type :validation - :code :profile-is-muted - :hint "looks like the profile has reported repeatedly as spam or has permanent bounces")) + ;; Check if the current profile is allowed to send emails. + (check-valid-email-muted conn profile) + - (let [cfg (assoc cfg ::db/conn conn) + (let [requested (into #{} (map :email) (get-valid-requests-email conn team-id)) + emails-to-add (filter #(contains? requested %) emails) + emails (remove #(contains? requested %) emails) + cfg (assoc cfg ::db/conn conn) members (->> (db/exec! conn [sql:team-members team-id]) (into #{} (map :email))) @@ -836,84 +950,90 @@ ;; We don't re-send inviation to already existing members (remove (partial contains? members)) (map (fn [email] - {:email email - :team team - :profile profile - :role role})) + (-> params + (assoc :email email) + (assoc :team team) + (assoc :profile profile) + (assoc :role role)))) (keep (partial create-invitation cfg))) emails)] + ;; For requested invitations, do not send invitation emails, add the user directly to the team + (doseq [email emails-to-add] + (add-user-to-team conn profile team email role)) + (with-meta {:total (count invitations) :invitations invitations} {::audit/props {:invitations (count invitations)}}))))) - ;; --- Mutation: Create Team & Invite Members -(s/def ::emails ::us/set-of-valid-emails) -(s/def ::create-team-with-invitations - (s/merge ::create-team - (s/keys :req-un [::emails ::role]))) - - (def ^:private schema:create-team-with-invitations [:map {:title "create-team-with-invitations"} - [:name :string] + [:name [:string {:max 250}]] [:features {:optional true} ::cfeat/features] [:id {:optional true} ::sm/uuid] - [:emails ::sm/set-of-emails] + [:emails [::sm/set ::sm/email]] [:role schema:role]]) (sv/defmethod ::create-team-with-invitations {::doc/added "1.17" ::sm/params schema:create-team-with-invitations} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id emails role] :as params}] - (db/with-atomic [conn pool] - (let [params (assoc params :profile-id profile-id) - cfg (assoc cfg ::db/conn conn) - team (create-team cfg params) - profile (db/get-by-id conn :profile profile-id) - emails (into #{} (map profile/clean-email) emails)] - - ;; Create invitations for all provided emails. - (->> emails - (map (fn [email] - {:team team - :profile profile - :email email - :role role})) - (run! (partial create-invitation cfg))) - - (run! (partial quotes/check-quote! conn) - (list {::quotes/id ::quotes/teams-per-profile - ::quotes/profile-id profile-id} - {::quotes/id ::quotes/invitations-per-team - ::quotes/profile-id profile-id - ::quotes/team-id (:id team) - ::quotes/incr (count emails)} - {::quotes/id ::quotes/profiles-per-team - ::quotes/profile-id profile-id - ::quotes/team-id (:id team) - ::quotes/incr (count emails)})) - - (audit/submit! cfg - {::audit/type "command" - ::audit/name "create-team-invitations" - ::audit/profile-id profile-id - ::audit/props {:emails emails - :role role - :profile-id profile-id - :invitations (count emails)}}) - - (vary-meta team assoc ::audit/props {:invitations (count emails)})))) + [cfg {:keys [::rpc/profile-id emails role name] :as params}] + + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [features (-> (cfeat/get-enabled-features cf/flags) + (cfeat/check-client-features! (:features params))) + + params (-> params + (assoc :profile-id profile-id) + (assoc :features features)) + + cfg (assoc cfg ::db/conn conn) + team (create-team cfg params) + profile (db/get-by-id conn :profile profile-id) + emails (into #{} (map profile/clean-email) emails)] + + (let [props {:name name :features features} + event (-> (audit/event-from-rpc-params params) + (assoc ::audit/name "create-team") + (assoc ::audit/props props))] + (audit/submit! cfg event)) + + ;; Create invitations for all provided emails. + (->> emails + (map (fn [email] + (-> params + (assoc :team team) + (assoc :profile profile) + (assoc :email email) + (assoc :role role)))) + (run! (partial create-invitation cfg))) + + (run! (partial quotes/check-quote! conn) + (list {::quotes/id ::quotes/teams-per-profile + ::quotes/profile-id profile-id} + {::quotes/id ::quotes/invitations-per-team + ::quotes/profile-id profile-id + ::quotes/team-id (:id team) + ::quotes/incr (count emails)} + {::quotes/id ::quotes/profiles-per-team + ::quotes/profile-id profile-id + ::quotes/team-id (:id team) + ::quotes/incr (count emails)})) + + (vary-meta team assoc ::audit/props {:invitations (count emails)}))))) ;; --- Query: get-team-invitation-token -(s/def ::get-team-invitation-token - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::email])) +(def ^:private schema:get-team-invitation-token + [:map {:title "get-team-invitation-token"} + [:team-id ::sm/uuid] + [:email ::sm/email]]) (sv/defmethod ::get-team-invitation-token - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:get-team-invitation-token} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}] (check-read-permissions! pool profile-id team-id) (let [email (profile/clean-email email) @@ -934,12 +1054,15 @@ ;; --- Mutation: Update invitation role -(s/def ::update-team-invitation-role - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::email ::role])) +(def ^:private schema:update-team-invitation-role + [:map {:title "update-team-invitation-role"} + [:team-id ::sm/uuid] + [:email ::sm/email] + [:role schema:role]]) (sv/defmethod ::update-team-invitation-role - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:update-team-invitation-role} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] @@ -955,12 +1078,14 @@ ;; --- Mutation: Delete invitation -(s/def ::delete-team-invitation - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::email])) +(def ^:private schema:delete-team-invition + [:map {:title "delete-team-invitation"} + [:team-id ::sm/uuid] + [:email ::sm/email]]) (sv/defmethod ::delete-team-invitation - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:delete-team-invition} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email] :as params}] (db/with-atomic [conn pool] (let [perms (get-permissions conn profile-id team-id)] @@ -974,3 +1099,130 @@ :email-to (profile/clean-email email)} {::db/return-keys true})] (rph/wrap nil {::audit/props {:invitation-id (:id invitation)}}))))) + + + + +;; --- Mutation: Request Team Invitation + +(def sql:upsert-team-access-request + "INSERT INTO team_access_request (id, team_id, requester_id, valid_until, auto_join_until) + VALUES (?, ?, ?, ?, ?) + ON conflict(id) + DO UPDATE SET valid_until = ?, auto_join_until = ?, updated_at = now() + RETURNING *") + + +(def sql:team-access-request + "SELECT id, (valid_until < now()) AS expired + FROM team_access_request + WHERE team_id = ? + AND requester_id = ?") + +(def sql:team-owner + "SELECT profile_id + FROM team_profile_rel + WHERE team_id = ? + AND is_owner = true") + + +(defn- create-team-access-request + [{:keys [::db/conn] :as cfg} {:keys [team requester team-owner file is-viewer] :as params}] + (let [old-request (->> (db/exec-one! conn [sql:team-access-request (:id team) (:id requester)]) + (decode-row))] + (when (false? (:expired old-request)) + (ex/raise :type :validation + :code :request-already-sent + :hint "you have already made a request to join this team less than 24 hours ago")) + + (let [id (or (:id old-request) (uuid/next)) + valid_until (dt/in-future "24h") + auto_join_until (dt/in-future "168h") ;; 7 days + request (db/exec-one! conn [sql:upsert-team-access-request + id (:id team) (:id requester) valid_until auto_join_until + valid_until auto_join_until]) + factory (cond + (and (some? file) (:is-default team) is-viewer) + eml/request-file-access-yourpenpot-view + (and (some? file) (:is-default team)) + eml/request-file-access-yourpenpot + (some? file) + eml/request-file-access + :else + eml/request-team-access) + page-id (when (some? file) + (-> file :data :pages first))] + + ;; TODO needs audit? + + (eml/send! {::eml/conn conn + ::eml/factory factory + :public-uri (cf/get :public-uri) + :to (:email team-owner) + :requested-by (:fullname requester) + :requested-by-email (:email requester) + :team-name (:name team) + :team-id (:id team) + :file-name (:name file) + :file-id (:id file) + :page-id page-id}) + + request))) + + +(def ^:private schema:create-team-access-request + [:and + [:map {:title "create-team-access-request"} + [:file-id {:optional true} ::sm/uuid] + [:team-id {:optional true} ::sm/uuid] + [:is-viewer {:optional true} ::sm/boolean]] + + [:fn (fn [params] + (or (contains? params :file-id) + (contains? params :team-id)))]]) + + +(sv/defmethod ::create-team-access-request + "A rpc call that allow to request for an invitations to join the team." + {::doc/added "2.2.0" + ::sm/params schema:create-team-access-request} + [cfg {:keys [::rpc/profile-id file-id team-id is-viewer] :as params}] + + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + + (let [requester (db/get-by-id conn :profile profile-id) + team-id (if (some? team-id) + team-id + (:id (get-team-for-file conn file-id))) + team (db/get-by-id conn :team team-id) + owner-id (->> (db/exec! conn [sql:team-owner (:id team)]) + (map decode-row) + (first) + :profile-id) + team-owner (db/get-by-id conn :profile owner-id) + file (when (some? file-id) + (db/get* conn :file + {:id file-id} + {::sql/columns [:id :name :data]})) + file (when (some? file) + (assoc file :data (blob/decode (:data file))))] + + ;;TODO needs quotes? + + (when (or (nil? requester) (nil? team) (nil? team-owner) (and (some? file-id) (nil? file))) + (ex/raise :type :validation + :code :invalid-parameters)) + + ;; Check that the requester is not muted + (check-valid-email-muted conn requester) + + ;; Check that the owner is not marked as bounce nor spam + (check-valid-email-bounce conn (:email team-owner) false) + (check-valid-email-spam conn (:email team-owner) true) + + (let [request (create-team-access-request + cfg {:team team :requester requester :team-owner team-owner :file file :is-viewer is-viewer})] + (when request + (with-meta {:request request} + {::audit/props {:request 1}}))))))) diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index 49c76c110231bf42c9a2723b780b4a6e21510570..67b5425b779cab248140be449137a506e7b5cba8 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -7,8 +7,10 @@ (ns app.rpc.commands.verify-token (:require [app.common.exceptions :as ex] - [app.common.spec :as us] + [app.common.schema :as sm] + [app.config :as cf] [app.db :as db] + [app.db.sql :as-alias sql] [app.http.session :as session] [app.loggers.audit :as audit] [app.main :as-alias main] @@ -18,27 +20,26 @@ [app.rpc.doc :as-alias doc] [app.rpc.helpers :as rph] [app.rpc.quotes :as quotes] + [app.setup :as-alias setup] [app.tokens :as tokens] [app.tokens.spec.team-invitation :as-alias spec.team-invitation] [app.util.services :as sv] - [clojure.spec.alpha :as s])) - -(s/def ::iss keyword?) -(s/def ::exp ::us/inst) + [app.util.time :as dt])) (defmulti process-token (fn [_ _ claims] (:iss claims))) -(s/def ::verify-token - (s/keys :req-un [::token] - :opt [::rpc/profile-id])) +(def ^:private schema:verify-token + [:map {:title "verify-token"} + [:token [:string {:max 1000}]]]) (sv/defmethod ::verify-token {::rpc/auth false ::doc/added "1.15" - ::doc/module :auth} + ::doc/module :auth + ::sm/params schema:verify-token} [{:keys [::db/pool] :as cfg} {:keys [token] :as params}] (db/with-atomic [conn pool] - (let [claims (tokens/verify (::main/props cfg) {:token token}) + (let [claims (tokens/verify (::setup/props cfg) {:token token}) cfg (assoc cfg :conn conn)] (process-token cfg params claims)))) @@ -81,8 +82,16 @@ (defmethod process-token :auth [{:keys [conn] :as cfg} _params {:keys [profile-id] :as claims}] - (let [profile (profile/get-profile conn profile-id)] - (assoc claims :profile profile))) + (let [profile (profile/get-profile conn profile-id {::sql/for-update true}) + props (merge (:props profile) + (:props claims))] + (when (not= props (:props profile)) + (db/update! conn :profile + {:props (db/tjson props)} + {:id profile-id})) + + (let [profile (assoc profile :props props)] + (assoc claims :profile profile)))) ;; --- Team Invitation @@ -119,34 +128,41 @@ (db/delete! conn :team-invitation {:team-id team-id :email-to member-email}) + ;; Delete any request + (db/delete! conn :team-access-request + {:team-id team-id :requester-id (:id member)}) + (assoc member :is-active true))) -(s/def ::spec.team-invitation/profile-id ::us/uuid) -(s/def ::spec.team-invitation/role ::us/keyword) -(s/def ::spec.team-invitation/team-id ::us/uuid) -(s/def ::spec.team-invitation/member-email ::us/email) -(s/def ::spec.team-invitation/member-id (s/nilable ::us/uuid)) +(def schema:team-invitation-claims + [:map {:title "TeamInvitationClaims"} + [:iss :keyword] + [:exp ::dt/instant] + [:profile-id ::sm/uuid] + [:role teams/schema:role] + [:team-id ::sm/uuid] + [:member-email ::sm/email] + [:member-id {:optional true} ::sm/uuid]]) -(s/def ::team-invitation-claims - (s/keys :req-un [::iss ::exp - ::spec.team-invitation/profile-id - ::spec.team-invitation/role - ::spec.team-invitation/team-id - ::spec.team-invitation/member-email] - :opt-un [::spec.team-invitation/member-id])) +(def valid-team-invitation-claims? + (sm/lazy-validator schema:team-invitation-claims)) (defmethod process-token :team-invitation [{:keys [conn] :as cfg} - {:keys [::rpc/profile-id token]} + {:keys [::rpc/profile-id token] :as params} {:keys [member-id team-id member-email] :as claims}] - (us/verify! ::team-invitation-claims claims) - - (let [invitation (db/get* conn :team-invitation - {:team-id team-id :email-to member-email}) - profile (db/get* conn :profile - {:id profile-id} - {:columns [:id :email]})] + (when-not (valid-team-invitation-claims? claims) + (ex/raise :type :validation + :code :invalid-invitation-token + :hint "invitation token contains unexpected data")) + + (let [invitation (db/get* conn :team-invitation + {:team-id team-id :email-to member-email}) + profile (db/get* conn :profile + {:id profile-id} + {:columns [:id :email]}) + registration-disabled? (not (contains? cf/flags :registration))] (when (nil? invitation) (ex/raise :type :validation :code :invalid-token @@ -159,25 +175,28 @@ ;; if we have logged-in user and it matches the invitation we proceed ;; with accepting the invitation and joining the current profile to the ;; invited team. - (let [profile (accept-invitation cfg claims invitation profile)] - (-> (assoc claims :state :created) - (rph/with-meta {::audit/name "accept-team-invitation" - ::audit/profile-id (:id profile) - ::audit/props {:team-id (:team-id claims) - :role (:role claims) - :invitation-id (:id invitation)}}))) + (let [props {:team-id (:team-id claims) + :role (:role claims) + :invitation-id (:id invitation)} + event (-> (audit/event-from-rpc-params params) + (assoc ::audit/name "accept-team-invitation") + (assoc ::audit/props props))] + + (accept-invitation cfg claims invitation profile) + (audit/submit! cfg event) + (assoc claims :state :created)) (ex/raise :type :validation :code :invalid-token :hint "logged-in user does not matches the invitation")) ;; If we have not logged-in user, and invitation comes with member-id we - ;; redirect user to login, if no memeber-id is present in the invitation - ;; token, we redirect user the the register page. + ;; redirect user to login, if no memeber-id is present and in the invitation + ;; token and registration is enabled, we redirect user the the register page. {:invitation-token token :iss :team-invitation - :redirect-to (if member-id :auth-login :auth-register) + :redirect-to (if (or member-id registration-disabled?) :auth-login :auth-register) :state :pending}))) ;; --- Default diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index c2887ef967762c5274122e907ebdfabf83b97d8f..9d15b3e8fab31ce9238c147c5a9e21e2643d0ed5 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -38,6 +38,11 @@ team (-> (db/get conn :team {:id (:team-id project)}) (teams/decode-row)) + members (into #{} (->> (teams/get-team-members conn (:team-id project)) + (map :id))) + + perms (assoc perms :in-team (contains? members profile-id)) + _ (-> (cfeat/get-team-enabled-features cf/flags team) (cfeat/check-client-features! (:features params)) (cfeat/check-file-features! (:features file))) @@ -93,7 +98,7 @@ (assoc ::perms perms) (assoc :profile-id profile-id))] - ;; When we have neither profile nor share, we just return a not + ;; When we have neither profile nor share, we just return a not ;; found response to the user. (when-not perms (ex/raise :type :not-found diff --git a/backend/src/app/rpc/commands/webhooks.clj b/backend/src/app/rpc/commands/webhooks.clj index 13a5d02101129560df422f7222ded4978d1147d7..e2a56691e51175e4c131415482b59ee9f39ad835 100644 --- a/backend/src/app/rpc/commands/webhooks.clj +++ b/backend/src/app/rpc/commands/webhooks.clj @@ -8,7 +8,7 @@ (:require [app.common.data.macros :as dm] [app.common.exceptions :as ex] - [app.common.spec :as us] + [app.common.schema :as sm] [app.common.uri :as u] [app.common.uuid :as uuid] [app.db :as db] @@ -19,7 +19,6 @@ [app.rpc.doc :as-alias doc] [app.util.services :as sv] [app.util.time :as dt] - [clojure.spec.alpha :as s] [cuerdas.core :as str])) (defn decode-row @@ -29,18 +28,6 @@ ;; --- Mutation: Create Webhook -(s/def ::team-id ::us/uuid) -(s/def ::uri ::us/uri) -(s/def ::is-active ::us/boolean) -(s/def ::mtype - #{"application/json" - "application/transit+json"}) - -(s/def ::create-webhook - (s/keys :req [::rpc/profile-id] - :req-un [::team-id ::uri ::mtype] - :opt-un [::is-active])) - ;; NOTE: for now the quote is hardcoded but this need to be solved in ;; a more universal way for handling properly object quotes (def max-hooks-for-team 8) @@ -99,31 +86,49 @@ {::db/return-keys true}) (decode-row))) + +(def valid-mtypes + #{"application/json" + "application/transit+json"}) + +(def ^:private schema:create-webhook + [:map {:title "create-webhook"} + [:team-id ::sm/uuid] + [:uri ::sm/uri] + [:mtype [::sm/one-of {:format "string"} valid-mtypes]]]) + (sv/defmethod ::create-webhook - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:create-webhook} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] (check-edition-permissions! pool profile-id team-id) (validate-quotes! cfg params) (validate-webhook! cfg nil params) (insert-webhook! cfg params)) -(s/def ::update-webhook - (s/keys :req-un [::id ::uri ::mtype ::is-active])) +(def ^:private schema:update-webhook + [:map {:title "update-webhook"} + [:id ::sm/uuid] + [:uri ::sm/uri] + [:mtype [::sm/one-of {:format "string"} valid-mtypes]] + [:is-active ::sm/boolean]]) (sv/defmethod ::update-webhook - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:update-webhook} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (let [whook (-> (db/get pool :webhook {:id id}) (decode-row))] (check-edition-permissions! pool profile-id (:team-id whook)) (validate-webhook! cfg whook params) (update-webhook! cfg whook params))) -(s/def ::delete-webhook - (s/keys :req [::rpc/profile-id] - :req-un [::id])) +(def ^:private schema:delete-webhook + [:map {:title "delete-webhook"} + [:id ::sm/uuid]]) (sv/defmethod ::delete-webhook - {::doc/added "1.17"} + {::doc/added "1.17" + ::sm/params schema:delete-webhook} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}] (db/with-atomic [conn pool] (let [whook (-> (db/get conn :webhook {:id id}) decode-row)] @@ -133,16 +138,17 @@ ;; --- Query: Webhooks -(s/def ::team-id ::us/uuid) -(s/def ::get-webhooks - (s/keys :req [::rpc/profile-id] - :req-un [::team-id])) - (def sql:get-webhooks "select id, uri, mtype, is_active, error_code, error_count from webhook where team_id = ? order by uri") +(def ^:private schema:get-webhooks + [:map {:title "get-webhooks"} + [:team-id ::sm/uuid]]) + (sv/defmethod ::get-webhooks + {::doc/added "1.17" + ::sm/params schema:get-webhooks} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id]}] (dm/with-open [conn (db/open pool)] (check-read-permissions! conn profile-id team-id) diff --git a/backend/src/app/rpc/cond.clj b/backend/src/app/rpc/cond.clj index a7db513b8b2c5eae129d3757ffbc468dcc11b441..3fe03c8210837b4a08c51f464b3829895a9e5038 100644 --- a/backend/src/app/rpc/cond.clj +++ b/backend/src/app/rpc/cond.clj @@ -51,7 +51,7 @@ [_ f {:keys [::get-object ::key-fn ::reuse-key?] :as mdata}] (if (and (ifn? get-object) (ifn? key-fn)) (do - (l/debug :hint "instrumenting method" :service (::sv/name mdata)) + (l/trc :hint "instrumenting method" :service (::sv/name mdata)) (fn [cfg {:keys [::key] :as params}] (if *enabled* (let [key' (when (or key reuse-key?) diff --git a/backend/src/app/rpc/doc.clj b/backend/src/app/rpc/doc.clj index 185f3fc4c25b1e6afddafe6b4a7f432138984017..ea973ff7a46c32eeba2747b92eb72cbd37a6ee3e 100644 --- a/backend/src/app/rpc/doc.clj +++ b/backend/src/app/rpc/doc.clj @@ -26,7 +26,6 @@ [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig] - [malli.transform :as mt] [pretty-spec.core :as ps] [ring.response :as-alias rres])) @@ -98,77 +97,79 @@ ;; OPENAPI / SWAGGER (v3.1) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def output-transformer - (mt/transformer - sm/default-transformer - (mt/key-transformer {:encode str/camel - :decode (comp keyword str/kebab)}))) - (defn prepare-openapi-context [methods] - (letfn [(gen-response-doc [tsx schema] - (let [schema (sm/schema schema) - example (sm/generate schema) - example (sm/encode schema example output-transformer)] - {:default - {:description "A default response" - :content - {"application/json" - {:schema tsx - :example example}}}})) - - (gen-params-doc [tsx schema] - (let [example (sm/generate schema) - example (sm/encode schema example output-transformer)] - {:required true - :content - {"application/json" - {:schema tsx - :example example}}})) - - (gen-method-doc [options mdata] - (let [pschema (::sm/params mdata) - rschema (::sm/result mdata) - - sparams (-> pschema (oapi/transform options) (gen-params-doc pschema)) - sresp (some-> rschema (oapi/transform options) (gen-response-doc rschema)) - - rpost {:description (::sv/docstring mdata) - :deprecated (::deprecated mdata false) - :requestBody sparams} - - rpost (cond-> rpost - (some? sresp) - (assoc :responses sresp))] - - {:name (-> mdata ::sv/name d/name) - :module (-> (:ns mdata) (str/split ".") last) - :repr {:post rpost}}))] - - (let [definitions (atom {}) - options {:registry sr/default-registry - ::oapi/definitions-path "#/components/schemas/" - ::oapi/definitions definitions} - - paths (binding [oapi/*definitions* definitions] - (->> methods - (map (comp first val)) - (filter ::sm/params) - (map (partial gen-method-doc options)) - (sort-by (juxt :module :name)) - (map (fn [doc] - [(str/ffmt "/command/%" (:name doc)) (:repr doc)])) - (into {})))] - {:openapi "3.0.0" - :info {:version (:main cf/version)} - :servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri)) + (let [definitions (atom {}) + options {:registry sr/default-registry + ::oapi/definitions-path "#/components/schemas/" + ::oapi/definitions definitions} + + output-transformer + (sm/json-transformer) + + gen-response-doc + (fn [tsx schema] + (let [schema (sm/schema schema) + example (sm/generate schema) + example (sm/encode schema example output-transformer)] + {:default + {:description "A default response" + :content + {"application/json" + {:schema tsx + :example example}}}})) + + gen-params-doc + (fn [tsx schema] + (let [example (sm/generate schema) + example (sm/encode schema example output-transformer)] + {:required true + :content + {"application/json" + {:schema tsx + :example example}}})) + + gen-method-doc + (fn [mdata] + (let [pschema (::sm/params mdata) + rschema (::sm/result mdata) + + sparams (-> pschema (oapi/transform options) (gen-params-doc pschema)) + sresp (some-> rschema (oapi/transform options) (gen-response-doc rschema)) + + rpost {:description (::sv/docstring mdata) + :deprecated (::deprecated mdata false) + :requestBody sparams} + + rpost (cond-> rpost + (some? sresp) + (assoc :responses sresp))] + + {:name (-> mdata ::sv/name d/name) + :module (-> (:ns mdata) (str/split ".") last) + :repr {:post rpost}})) + + paths + (binding [oapi/*definitions* definitions] + (->> methods + (map (comp first val)) + (filter ::sm/params) + (map gen-method-doc) + (sort-by (juxt :module :name)) + (map (fn [doc] + [(str/ffmt "/command/%" (:name doc)) (:repr doc)])) + (into {})))] + + {:openapi "3.0.0" + :info {:version (:main cf/version)} + :servers [{:url (str/ffmt "%/api/rpc" (cf/get :public-uri)) ;; :description "penpot backend" - }] - :security - {:api_key []} + }] + :security + {:api_key []} - :paths paths - :components {:schemas @definitions}}))) + :paths paths + :components {:schemas @definitions}})) (defn openapi-json-handler [context] diff --git a/backend/src/app/rpc/permissions.clj b/backend/src/app/rpc/permissions.clj index d2f1719ee6da87af48cd3878e7c799d3cafceb29..0704d70ed7c6be5a47ff7ceb37fe0bac6a8b037a 100644 --- a/backend/src/app/rpc/permissions.clj +++ b/backend/src/app/rpc/permissions.clj @@ -12,14 +12,14 @@ [app.common.spec :as us] [clojure.spec.alpha :as s])) -(sm/def! ::permissions +(sm/register! ::permissions [:map {:title "Permissions"} [:type {:gen/elements [:membership :share-link]} :keyword] - [:is-owner :boolean] - [:is-admin :boolean] - [:can-edit :boolean] - [:can-read :boolean] - [:is-logged :boolean]]) + [:is-owner ::sm/boolean] + [:is-admin ::sm/boolean] + [:can-edit ::sm/boolean] + [:can-read ::sm/boolean] + [:is-logged ::sm/boolean]]) (s/def ::role #{:admin :owner :editor :viewer}) diff --git a/backend/src/app/rpc/quotes.clj b/backend/src/app/rpc/quotes.clj index 3244bd03f9e3bb9926e61e5f777def82213ad44e..c42a2de81c67a9a4f6e96c98150209b1675a980c 100644 --- a/backend/src/app/rpc/quotes.clj +++ b/backend/src/app/rpc/quotes.clj @@ -31,7 +31,7 @@ [::team-id {:optional true} ::sm/uuid] [::project-id {:optional true} ::sm/uuid] [::file-id {:optional true} ::sm/uuid] - [::incr {:optional true} [:int {:min 0}]] + [::incr {:optional true} [::sm/int {:min 0}]] [::id :keyword] [::profile-id ::sm/uuid]])) @@ -83,17 +83,17 @@ "- Quote ID: '~(::target params)'\n" "- Max: ~(::quote params)\n" "- Total: ~(::total params) (INCR ~(::incr params 1))\n")] - (wrk/submit! {::wrk/task :sendmail + (wrk/submit! {::db/conn conn + ::wrk/task :sendmail ::wrk/delay (dt/duration "30s") ::wrk/max-retries 4 ::wrk/priority 200 - ::wrk/conn conn ::wrk/dedupe true ::wrk/label "quotes-notification" - :to (vec admins) - :subject subject - :body [{:type "text/plain" - :content content}]})))) + ::wrk/params {:to (vec admins) + :subject subject + :body [{:type "text/plain" + :content content}]}})))) (defn- generic-check! [{:keys [::db/conn ::incr ::quote-sql ::count-sql ::default ::target] :or {incr 1} :as params}] diff --git a/backend/src/app/rpc/retry.clj b/backend/src/app/rpc/retry.clj index 3745b9d8f1ca2ebed52c0ff73456bf4c026bc450..5e2d620131d08775fa3e58c1819ca45a0a776a84 100644 --- a/backend/src/app/rpc/retry.clj +++ b/backend/src/app/rpc/retry.clj @@ -44,7 +44,7 @@ (if (::enabled mdata) (let [max-retries (get mdata ::max-retries 3) matches? (get mdata ::when always-false)] - (l/dbg :hint "wrapping retry" :name name :max-retries max-retries) + (l/trc :hint "wrapping retry" :name name :max-retries max-retries) (fn [cfg params] (-> cfg (assoc ::max-retries max-retries) diff --git a/backend/src/app/rpc/rlimit.clj b/backend/src/app/rpc/rlimit.clj index 0c0868f93c049c0cc2ecfd73ca03241950d0cfc3..4e092449032571bdf471ca28f5dc3abcd27659bb 100644 --- a/backend/src/app/rpc/rlimit.clj +++ b/backend/src/app/rpc/rlimit.clj @@ -51,12 +51,12 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.http :as-alias http] - [app.loggers.audit :refer [parse-client-ip]] [app.redis :as rds] [app.redis.script :as-alias rscript] [app.rpc :as-alias rpc] [app.rpc.helpers :as rph] [app.rpc.rlimit.result :as-alias lresult] + [app.util.inet :as inet] [app.util.services :as-alias sv] [app.util.time :as dt] [app.worker :as wrk] @@ -215,7 +215,7 @@ [{:keys [::rpc/profile-id] :as params}] (let [request (-> params meta ::http/request)] (or profile-id - (some-> request parse-client-ip) + (some-> request inet/parse-request) uuid/zero))) (defn process-request! diff --git a/backend/src/app/setup.clj b/backend/src/app/setup.clj index 8e889e2b461d675f12954af29fed0ead2596e770..68df58330f356eb9a077278a403b42b135296266 100644 --- a/backend/src/app/setup.clj +++ b/backend/src/app/setup.clj @@ -7,6 +7,7 @@ (ns app.setup "Initial data setup of instance." (:require + [app.common.data :as d] [app.common.logging :as l] [app.common.spec :as us] [app.common.uuid :as uuid] @@ -25,7 +26,7 @@ (bc/bytes->b64u) (bc/bytes->str))) -(defn- retrieve-all +(defn- get-all-props [conn] (->> (db/query conn :server-prop {:preload true}) (filter #(not= "secret-key" (:id %))) @@ -50,16 +51,37 @@ :cause cause)))) instance-id))) -(s/def ::main/key ::us/string) -(s/def ::main/props - (s/map-of ::us/keyword some?)) + +(def sql:add-prop + "INSERT INTO server_prop (id, content, preload) + VALUES (?, ?, ?) + ON CONFLICT (id) + DO UPDATE SET content=?, preload=?") + +(defn get-prop + ([system prop] (get-prop system prop nil)) + ([system prop default] + (let [prop (d/name prop)] + (db/run! system (fn [{:keys [::db/conn]}] + (or (db/get* conn :server-prop {:id prop}) + default)))))) + +(defn set-prop! + [system prop value] + (let [value (db/tjson value) + prop (d/name prop)] + (db/run! system (fn [{:keys [::db/conn]}] + (db/exec-one! conn [sql:add-prop prop value false value false]))))) + +(s/def ::key ::us/string) +(s/def ::props (s/map-of ::us/keyword some?)) (defmethod ig/pre-init-spec ::props [_] (s/keys :req [::db/pool] - :opt [::main/key])) + :opt [::key])) (defmethod ig/init-key ::props - [_ {:keys [::db/pool ::main/key] :as cfg}] + [_ {:keys [::db/pool ::key] :as cfg}] (db/with-atomic [conn pool] (db/xact-lock! conn 0) (when-not key @@ -68,7 +90,7 @@ "PENPOT_SECRET_KEY environment variable"))) (let [secret (or key (generate-random-key))] - (-> (retrieve-all conn) + (-> (get-all-props conn) (assoc :secret-key secret) (assoc :tokens-key (keys/derive secret :salt "tokens")) (update :instance-id handle-instance-id conn (db/read-only? pool)))))) diff --git a/backend/src/app/setup/welcome_file.clj b/backend/src/app/setup/welcome_file.clj new file mode 100644 index 0000000000000000000000000000000000000000..8de4acaa76f78100d323c32fd9658869c445a1b0 --- /dev/null +++ b/backend/src/app/setup/welcome_file.clj @@ -0,0 +1,64 @@ +;; 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 app.setup.welcome-file + (:require + [app.common.logging :as l] + [app.db :as db] + [app.rpc :as-alias rpc] + [app.rpc.climit :as-alias climit] + [app.rpc.commands.files-update :as fupdate] + [app.rpc.commands.management :as management] + [app.rpc.commands.profile :as profile] + [app.rpc.doc :as-alias doc] + [app.setup :as-alias setup] + [app.setup.templates :as tmpl] + [app.worker :as-alias wrk])) + +(def ^:private page-id #uuid "2c6952ee-d00e-8160-8004-d2250b7210cb") +(def ^:private shape-id #uuid "765e9f82-c44e-802e-8004-d72a10b7b445") + +(def ^:private update-path + [:data :pages-index page-id :objects shape-id + :content :children 0 :children 0 :children 0]) + +(def ^:private sql:mark-file-object-thumbnails-deleted + "UPDATE file_tagged_object_thumbnail + SET deleted_at = now() + WHERE file_id = ?") + +(def ^:private sql:mark-file-thumbnail-deleted + "UPDATE file_thumbnail + SET deleted_at = now() + WHERE file_id = ?") + +(defn- update-welcome-shape + [_ file name] + (let [text (str "Welcome to Penpot, " name "!")] + (-> file + (update-in update-path assoc :text text) + (update-in [:data :pages-index page-id :objects shape-id] assoc :name "Welcome to Penpot!") + (update-in [:data :pages-index page-id :objects shape-id] dissoc :position-data)))) + +(defn create-welcome-file + [cfg {:keys [id fullname] :as profile}] + (try + (let [cfg (dissoc cfg ::db/conn) + params {:profile-id (:id profile) + :project-id (:default-project-id profile)} + template-stream (tmpl/get-template-stream cfg "welcome") + file-id (-> (management/clone-template cfg params template-stream) + first)] + + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (fupdate/update-file! cfg file-id update-welcome-shape fullname) + (profile/update-profile-props cfg id {:welcome-file-id file-id}) + (db/exec-one! conn [sql:mark-file-object-thumbnails-deleted file-id]) + (db/exec-one! conn [sql:mark-file-thumbnail-deleted file-id])))) + + (catch Throwable cause + (l/error :hint "unexpected error on create welcome file " :cause cause)))) + diff --git a/backend/src/app/srepl/cli.clj b/backend/src/app/srepl/cli.clj index 6bcca5c0c88afd5e26122a6628210c6617e4fbf4..0cfa43ed25971da1b5cbba19cf5016aabf807731 100644 --- a/backend/src/app/srepl/cli.clj +++ b/backend/src/app/srepl/cli.clj @@ -11,10 +11,8 @@ [app.common.exceptions :as ex] [app.common.uuid :as uuid] [app.db :as db] - [app.main :as main] [app.rpc.commands.auth :as cmd.auth] - [app.srepl.components-v2 :refer [migrate-teams!]] - [app.util.events :as events] + [app.rpc.commands.profile :as cmd.profile] [app.util.json :as json] [app.util.time :as dt] [cuerdas.core :as str])) @@ -40,12 +38,13 @@ :or {is-active true}}] (when-let [system (get-current-system)] (db/with-atomic [conn (:app.db/pool system)] - (let [params {:id (uuid/next) - :email email - :fullname fullname - :is-active is-active - :password password - :props {}}] + (let [password (cmd.profile/derive-password system password) + params {:id (uuid/next) + :email email + :fullname fullname + :is-active is-active + :password password + :props {}}] (->> (cmd.auth/create-profile! conn params) (cmd.auth/create-profile-rels! conn)))))) @@ -105,39 +104,6 @@ [{:keys [password]}] (auth/derive-password password)) -(defmethod exec-command :migrate-v2 - [_] - (letfn [(on-progress-report [{:keys [elapsed completed errors]}] - (println (str/ffmt "-> Progress: completed: %, errors: %, elapsed: %" - completed errors elapsed))) - - (on-progress [{:keys [op name]}] - (case op - :migrate-team - (println (str/ffmt "-> Migrating team: \"%\"" name)) - :migrate-file - (println (str/ffmt "=> Migrating file: \"%\"" name)) - nil)) - - (on-event [[type payload]] - (case type - :progress-report (on-progress-report payload) - :progress (on-progress payload) - :error (on-error payload) - nil)) - - (on-error [cause] - (println "EE:" (ex-message cause)))] - - (println "The components/v2 migration started...") - - (try - (let [result (-> (partial migrate-teams! main/system {:rollback? true}) - (events/run-with! on-event))] - (println (str/ffmt "Migration process finished (elapsed: %)" (:elapsed result)))) - (catch Throwable cause - (on-error cause))))) - (defmethod exec-command :default [{:keys [::cmd]}] (ex/raise :type :internal diff --git a/backend/src/app/srepl/components_v2.clj b/backend/src/app/srepl/components_v2.clj index 0922904938ba29d63bf9207655db7cd49485825b..27a8d98251225001e4685d5ffd1afbcd4dd067ef 100644 --- a/backend/src/app/srepl/components_v2.clj +++ b/backend/src/app/srepl/components_v2.clj @@ -6,19 +6,17 @@ (ns app.srepl.components-v2 (:require - [app.common.data :as d] + [app.common.fressian :as fres] [app.common.logging :as l] - [app.common.uuid :as uuid] [app.db :as db] [app.features.components-v2 :as feat] [app.main :as main] [app.srepl.helpers :as h] - [app.svgo :as svgo] - [app.util.cache :as cache] [app.util.events :as events] [app.util.time :as dt] [app.worker :as-alias wrk] - [cuerdas.core :as str] + [datoteka.fs :as fs] + [datoteka.io :as io] [promesa.exec :as px] [promesa.exec.semaphore :as ps] [promesa.util :as pu])) @@ -30,244 +28,19 @@ ;; PRIVATE HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- report-progress-files - [tpoint] - (fn [_ _ oldv newv] - (when (or (not= (:processed-files oldv) - (:processed-files newv)) - (not= (:errors oldv) - (:errors newv))) - (let [completed (:processed-files newv 0) - errors (:errors newv 0) - elapsed (dt/format-duration (tpoint))] - (events/tap :progress-report - {:elapsed elapsed - :completed completed - :errors errors}) - (l/dbg :hint "progress" - :completed completed - :elapsed elapsed))))) - -(defn- report-progress-teams - [tpoint] - (fn [_ _ oldv newv] - (when (or (not= (:processed-teams oldv) - (:processed-teams newv)) - (not= (:errors oldv) - (:errors newv))) - (let [completed (:processed-teams newv 0) - errors (:errors newv 0) - elapsed (dt/format-duration (tpoint))] - (events/tap :progress-report - {:elapsed elapsed - :completed completed - :errors errors}) - (l/dbg :hint "progress" - :completed completed - :elapsed elapsed))))) - -(def ^:private sql:get-teams-by-created-at - "WITH teams AS ( - SELECT id, features - FROM team - WHERE deleted_at IS NULL - ORDER BY created_at DESC - ) SELECT * FROM TEAMS %(pred)s") - -(def ^:private sql:get-teams-by-graphics - "WITH teams AS ( - SELECT t.id, t.features, - (SELECT count(*) - FROM file_media_object AS fmo - JOIN file AS f ON (f.id = fmo.file_id) - JOIN project AS p ON (p.id = f.project_id) - WHERE p.team_id = t.id - AND fmo.mtype = 'image/svg+xml' - AND fmo.is_local = false) AS graphics - FROM team AS t - WHERE t.deleted_at IS NULL - ORDER BY 3 ASC - ) - SELECT * FROM teams %(pred)s") - -(def ^:private sql:get-teams-by-activity - "WITH teams AS ( - SELECT t.id, t.features, - (SELECT coalesce(max(date_trunc('month', f.modified_at)), date_trunc('month', t.modified_at)) - FROM file AS f - JOIN project AS p ON (f.project_id = p.id) - WHERE p.team_id = t.id) AS updated_at, - (SELECT coalesce(count(*), 0) - FROM file AS f - JOIN project AS p ON (f.project_id = p.id) - WHERE p.team_id = t.id) AS total_files - FROM team AS t - WHERE t.deleted_at IS NULL - ORDER BY 3 DESC, 4 DESC - ) - SELECT * FROM teams %(pred)s") - -(def ^:private sql:get-teams-by-report - "WITH teams AS ( - SELECT t.id t.features, mr.name - FROM migration_team_report AS mr - JOIN team AS t ON (t.id = mr.team_id) - WHERE t.deleted_at IS NULL - AND mr.error IS NOT NULL - ORDER BY mr.created_at - ) SELECT id, features FROM teams %(pred)s") - (def ^:private sql:get-files-by-created-at - "SELECT id, features + "SELECT id, features, + row_number() OVER (ORDER BY created_at DESC) AS rown FROM file WHERE deleted_at IS NULL ORDER BY created_at DESC") -(def ^:private sql:get-files-by-modified-at - "SELECT id, features - FROM file - WHERE deleted_at IS NULL - ORDER BY modified_at DESC") - -(def ^:private sql:get-files-by-graphics - "WITH files AS ( - SELECT f.id, f.features, - (SELECT count(*) FROM file_media_object AS fmo - WHERE fmo.mtype = 'image/svg+xml' - AND fmo.is_local = false - AND fmo.file_id = f.id) AS graphics - FROM file AS f - WHERE f.deleted_at IS NULL - ORDER BY 3 ASC - ) SELECT * FROM files %(pred)s") - -(def ^:private sql:get-files-by-report - "WITH files AS ( - SELECT f.id, f.features, mr.label - FROM migration_file_report AS mr - JOIN file AS f ON (f.id = mr.file_id) - WHERE f.deleted_at IS NULL - AND mr.error IS NOT NULL - ORDER BY mr.created_at - ) SELECT id, features FROM files %(pred)s") - -(defn- read-pred - [entries] - (let [entries (if (and (vector? entries) - (keyword? (first entries))) - [entries] - entries)] - (loop [params [] - queries [] - entries (seq entries)] - (if-let [[op val field] (first entries)] - (let [field (name field) - cond (case op - :lt (str/ffmt "% < ?" field) - :lte (str/ffmt "% <= ?" field) - :gt (str/ffmt "% > ?" field) - :gte (str/ffmt "% >= ?" field) - :eq (str/ffmt "% = ?" field))] - (recur (conj params val) - (conj queries cond) - (rest entries))) - - (let [sql (apply str "WHERE " (str/join " AND " queries))] - (apply vector sql params)))))) - -(defn- get-teams - [conn query pred] - (let [query (d/nilv query :created-at) - sql (case query - :created-at sql:get-teams-by-created-at - :activity sql:get-teams-by-activity - :graphics sql:get-teams-by-graphics - :report sql:get-teams-by-report) - sql (if pred - (let [[pred-sql & pred-params] (read-pred pred)] - (apply vector - (str/format sql {:pred pred-sql}) - pred-params)) - [(str/format sql {:pred ""})])] - - (->> (db/cursor conn sql {:chunk-size 500}) - (map feat/decode-row) - (remove (fn [{:keys [features]}] - (contains? features "components/v2"))) - (map :id)))) - (defn- get-files - [conn query pred] - (let [query (d/nilv query :created-at) - sql (case query - :created-at sql:get-files-by-created-at - :modified-at sql:get-files-by-modified-at - :graphics sql:get-files-by-graphics - :report sql:get-files-by-report) - sql (if pred - (let [[pred-sql & pred-params] (read-pred pred)] - (apply vector - (str/format sql {:pred pred-sql}) - pred-params)) - [(str/format sql {:pred ""})])] - - (->> (db/cursor conn sql {:chunk-size 500}) - (map feat/decode-row) - (remove (fn [{:keys [features]}] - (contains? features "components/v2"))) - (map :id)))) - -(def ^:private sql:team-report-table - "CREATE UNLOGGED TABLE IF NOT EXISTS migration_team_report ( - id bigserial NOT NULL, - label text NOT NULL, - team_id UUID NOT NULL, - error text NULL, - created_at timestamptz NOT NULL DEFAULT now(), - elapsed bigint NOT NULL, - PRIMARY KEY (label, created_at, id))") - -(def ^:private sql:file-report-table - "CREATE UNLOGGED TABLE IF NOT EXISTS migration_file_report ( - id bigserial NOT NULL, - label text NOT NULL, - file_id UUID NOT NULL, - error text NULL, - created_at timestamptz NOT NULL DEFAULT now(), - elapsed bigint NOT NULL, - PRIMARY KEY (label, created_at, id))") - -(defn- create-report-tables! - [system] - (db/exec-one! system [sql:team-report-table]) - (db/exec-one! system [sql:file-report-table])) - -(defn- clean-team-reports! - [system label] - (db/delete! system :migration-team-report {:label label})) - -(defn- team-report! - [system team-id label elapsed error] - (db/insert! system :migration-team-report - {:label label - :team-id team-id - :elapsed (inst-ms elapsed) - :error error} - {::db/return-keys false})) - -(defn- clean-file-reports! - [system label] - (db/delete! system :migration-file-report {:label label})) - -(defn- file-report! - [system file-id label elapsed error] - (db/insert! system :migration-file-report - {:label label - :file-id file-id - :elapsed (inst-ms elapsed) - :error error} - {::db/return-keys false})) - + [conn] + (->> (db/cursor conn [sql:get-files-by-created-at] {:chunk-size 500}) + (map feat/decode-row) + (remove (fn [{:keys [features]}] + (contains? features "components/v2"))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PUBLIC API @@ -280,11 +53,7 @@ skip-on-graphic-error? true}}] (l/dbg :hint "migrate:start" :rollback rollback?) (let [tpoint (dt/tpoint) - file-id (h/parse-uuid file-id) - cache (if (int? cache) - (cache/create :executor (::wrk/executor main/system) - :max-items cache) - nil)] + file-id (h/parse-uuid file-id)] (binding [feat/*stats* (atom {}) feat/*cache* cache] @@ -315,14 +84,7 @@ (let [team-id (h/parse-uuid team-id) stats (atom {}) - tpoint (dt/tpoint) - - cache (if (int? cache) - (cache/create :executor (::wrk/executor main/system) - :max-items cache) - nil)] - - (add-watch stats :progress-report (report-progress-files tpoint)) + tpoint (dt/tpoint)] (binding [feat/*stats* stats feat/*cache* cache] @@ -343,10 +105,10 @@ (let [elapsed (dt/format-duration (tpoint))] (l/dbg :hint "migrate:end" :rollback rollback? :elapsed elapsed))))))) -(defn migrate-teams! - "A REPL helper for migrate all teams. +(defn migrate-files! + "A REPL helper for migrate all files. - This function starts multiple concurrent team migration processes + This function starts multiple concurrent file migration processes until thw maximum number of jobs is reached which by default has the value of `1`. This is controled with the `:max-jobs` option. @@ -356,8 +118,8 @@ In order to get the report table populated, you will need to provide a correct `:label`. That label is also used for persist a file snaphot before continue with the migration." - [& {:keys [max-jobs max-items max-time rollback? validate? query - pred max-procs cache skip-on-graphic-error? + [& {:keys [max-jobs max-items rollback? validate? + cache skip-on-graphic-error? label partitions current-partition] :or {validate? false rollback? true @@ -374,64 +136,41 @@ (let [stats (atom {}) tpoint (dt/tpoint) - mtime (some-> max-time dt/duration) - factory (px/thread-factory :virtual false :prefix "penpot/migration/") executor (px/cached-executor :factory factory) - max-procs (or max-procs max-jobs) sjobs (ps/create :permits max-jobs) - sprocs (ps/create :permits max-procs) - - cache (if (int? cache) - (cache/create :executor (::wrk/executor main/system) - :max-items cache) - nil) - migrate-team - (fn [team-id] - (let [tpoint (dt/tpoint)] - (try - (db/tx-run! (assoc main/system ::db/rollback rollback?) - (fn [system] - (db/exec-one! system ["SET idle_in_transaction_session_timeout = 0"]) - (feat/migrate-team! system team-id - :label label - :validate? validate? - :skip-on-graphic-error? skip-on-graphic-error?))) - - (when (string? label) - (team-report! main/system team-id label (tpoint) nil)) - - (catch Throwable cause - (l/wrn :hint "unexpected error on processing team (skiping)" - :team-id (str team-id)) - - (events/tap :error - (ex-info "unexpected error on processing team (skiping)" - {:team-id team-id} - cause)) - - (swap! stats update :errors (fnil inc 0)) - (when (string? label) - (team-report! main/system team-id label (tpoint) (ex-message cause)))) - - (finally - (ps/release! sjobs))))) + migrate-file + (fn [file-id rown] + (try + (db/tx-run! (assoc main/system ::db/rollback rollback?) + (fn [system] + (db/exec-one! system ["SET LOCAL idle_in_transaction_session_timeout = 0"]) + (feat/migrate-file! system file-id + :rown rown + :label label + :validate? validate? + :skip-on-graphic-error? skip-on-graphic-error?))) + + (catch Throwable cause + (l/wrn :hint "unexpected error on processing file (skiping)" + :file-id (str file-id)) + + (events/tap :error + (ex-info "unexpected error on processing file (skiping)" + {:file-id file-id} + cause)) + + (swap! stats update :errors (fnil inc 0))) + + (finally + (ps/release! sjobs)))) - process-team - (fn [team-id] + process-file + (fn [{:keys [id rown]}] (ps/acquire! sjobs) - (let [ts (tpoint)] - (if (and mtime (neg? (compare mtime ts))) - (do - (l/inf :hint "max time constraint reached" - :team-id (str team-id) - :elapsed (dt/format-duration ts)) - (ps/release! sjobs) - (reduced nil)) - - (px/run! executor (partial migrate-team team-id)))))] + (px/run! executor (partial migrate-file id rown)))] (l/dbg :hint "migrate:start" :label label @@ -439,28 +178,19 @@ :max-jobs max-jobs :max-items max-items) - (add-watch stats :progress-report (report-progress-teams tpoint)) - (binding [feat/*stats* stats - feat/*cache* cache - svgo/*semaphore* sprocs] + feat/*cache* cache] (try - (when (string? label) - (create-report-tables! main/system) - (clean-team-reports! main/system label)) - (db/tx-run! main/system (fn [{:keys [::db/conn] :as system}] - (db/exec! conn ["SET statement_timeout = 0"]) - (db/exec! conn ["SET idle_in_transaction_session_timeout = 0"]) + (db/exec! conn ["SET LOCAL statement_timeout = 0"]) + (db/exec! conn ["SET LOCAL idle_in_transaction_session_timeout = 0"]) - (run! process-team - (->> (get-teams conn query pred) - (filter (fn [team-id] + (run! process-file + (->> (get-files conn) + (filter (fn [{:keys [rown] :as row}] (if (int? partitions) - (= current-partition (-> (uuid/hash-int team-id) - (mod partitions) - (inc))) + (= current-partition (inc (mod rown partitions))) true))) (take max-items))) @@ -480,8 +210,22 @@ :rollback rollback? :elapsed elapsed))))))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; CACHE POPULATE +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn migrate-files! +(def sql:sobjects-for-cache + "SELECT id, + row_number() OVER (ORDER BY created_at) AS index + FROM storage_object + WHERE (metadata->>'~:bucket' = 'file-media-object' OR + metadata->>'~:bucket' IS NULL) + AND metadata->>'~:content-type' = 'image/svg+xml' + AND deleted_at IS NULL + AND size < 1135899 + ORDER BY created_at ASC") + +(defn populate-cache! "A REPL helper for migrate all files. This function starts multiple concurrent file migration processes @@ -494,142 +238,69 @@ In order to get the report table populated, you will need to provide a correct `:label`. That label is also used for persist a file snaphot before continue with the migration." - [& {:keys [max-jobs max-items max-time rollback? validate? query - pred max-procs cache skip-on-graphic-error? - label partitions current-partition] - :or {validate? false - rollback? true - max-jobs 1 - current-partition 1 - skip-on-graphic-error? true - max-items Long/MAX_VALUE}}] + [& {:keys [max-jobs] :or {max-jobs 1}}] - (when (int? partitions) - (when-not (int? current-partition) - (throw (IllegalArgumentException. "missing `current-partition` parameter"))) - (when-not (<= 0 current-partition partitions) - (throw (IllegalArgumentException. "invalid value on `current-partition` parameter")))) + (let [tpoint (dt/tpoint) - (let [stats (atom {}) - tpoint (dt/tpoint) - mtime (some-> max-time dt/duration) - - factory (px/thread-factory :virtual false :prefix "penpot/migration/") + factory (px/thread-factory :virtual false :prefix "penpot/cache/") executor (px/cached-executor :factory factory) - max-procs (or max-procs max-jobs) sjobs (ps/create :permits max-jobs) - sprocs (ps/create :permits max-procs) - cache (if (int? cache) - (cache/create :executor (::wrk/executor main/system) - :max-items cache) - nil) + retrieve-sobject + (fn [id index] + (let [path (feat/get-sobject-cache-path id) + parent (fs/parent path)] - migrate-file - (fn [file-id] - (let [tpoint (dt/tpoint)] (try - (db/tx-run! (assoc main/system ::db/rollback rollback?) - (fn [system] - (db/exec-one! system ["SET idle_in_transaction_session_timeout = 0"]) - (feat/migrate-file! system file-id - :label label - :validate? validate? - :skip-on-graphic-error? skip-on-graphic-error?))) - - (when (string? label) - (file-report! main/system file-id label (tpoint) nil)) + (when-not (fs/exists? parent) + (fs/create-dir parent)) - (catch Throwable cause - (l/wrn :hint "unexpected error on processing file (skiping)" - :file-id (str file-id)) + (if (fs/exists? path) + (l/inf :hint "create cache entry" :status "exists" :index index :id (str id) :path (str path)) + (let [svg-data (feat/get-optimized-svg id)] + (with-open [^java.lang.AutoCloseable stream (io/output-stream path)] + (let [writer (fres/writer stream)] + (fres/write! writer svg-data))) - (events/tap :error - (ex-info "unexpected error on processing file (skiping)" - {:file-id file-id} - cause)) + (l/inf :hint "create cache entry" :status "created" + :index index + :id (str id) + :path (str path)))) - (swap! stats update :errors (fnil inc 0)) - - (when (string? label) - (file-report! main/system file-id label (tpoint) (ex-message cause)))) + (catch Throwable cause + (l/wrn :hint "create cache entry" + :status "error" + :index index + :id (str id) + :path (str path) + :cause cause)) (finally (ps/release! sjobs))))) - process-file - (fn [file-id] + process-sobject + (fn [{:keys [id index]}] (ps/acquire! sjobs) - (let [ts (tpoint)] - (if (and mtime (neg? (compare mtime ts))) - (do - (l/inf :hint "max time constraint reached" - :file-id (str file-id) - :elapsed (dt/format-duration ts)) - (ps/release! sjobs) - (reduced nil)) - - (px/run! executor (partial migrate-file file-id)))))] + (px/run! executor (partial retrieve-sobject id index)))] (l/dbg :hint "migrate:start" - :label label - :rollback rollback? - :max-jobs max-jobs - :max-items max-items) - - (add-watch stats :progress-report (report-progress-files tpoint)) - - (binding [feat/*stats* stats - feat/*cache* cache - svgo/*semaphore* sprocs] - (try - (when (string? label) - (create-report-tables! main/system) - (clean-file-reports! main/system label)) - - (db/tx-run! main/system - (fn [{:keys [::db/conn] :as system}] - (db/exec! conn ["SET statement_timeout = 0"]) - (db/exec! conn ["SET idle_in_transaction_session_timeout = 0"]) + :max-jobs max-jobs) - (run! process-file - (->> (get-files conn query pred) - (filter (fn [file-id] - (if (int? partitions) - (= current-partition (-> (uuid/hash-int file-id) - (mod partitions) - (inc))) - true))) - (take max-items))) + (try + (binding [feat/*system* main/system] + (run! process-sobject + (db/exec! main/system [sql:sobjects-for-cache])) - ;; Close and await tasks - (pu/close! executor))) + ;; Close and await tasks + (pu/close! executor)) - (-> (deref stats) - (assoc :elapsed (dt/format-duration (tpoint)))) + {:elapsed (dt/format-duration (tpoint))} - (catch Throwable cause - (l/dbg :hint "migrate:error" :cause cause) - (events/tap :error cause)) - - (finally - (let [elapsed (dt/format-duration (tpoint))] - (l/dbg :hint "migrate:end" - :rollback rollback? - :elapsed elapsed))))))) - - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; FILE PROCESS HELPERS -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (catch Throwable cause + (l/dbg :hint "populate:error" :cause cause)) -(defn delete-broken-files - [{:keys [id data] :as file}] - (if (-> data :options :components-v2 true?) - (do - (l/wrn :hint "found old components-v2 format" - :file-id (str id) - :file-name (:name file)) - (assoc file :deleted-at (dt/now))) - file)) + (finally + (let [elapsed (dt/format-duration (tpoint))] + (l/dbg :hint "populate:end" + :elapsed elapsed)))))) diff --git a/backend/src/app/srepl/fixes.clj b/backend/src/app/srepl/fixes.clj index 955f08366fab1234c53d5009916fb2330c672235..5e80516b85f7df939ca94f5b747a3ed6b9d558d9 100644 --- a/backend/src/app/srepl/fixes.clj +++ b/backend/src/app/srepl/fixes.clj @@ -10,10 +10,15 @@ (:require [app.binfile.common :as bfc] [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.files.changes :as cpc] + [app.common.files.helpers :as cfh] [app.common.files.repair :as cfr] [app.common.files.validate :as cfv] [app.common.logging :as l] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] [app.common.uuid :as uuid] [app.db :as db] [app.features.fdata :as feat.fdata] @@ -133,4 +138,140 @@ (fn [touched] (disj touched :shapes-group))))] file (-> file - (update :data fix-fdata)))) \ No newline at end of file + (update :data fix-fdata)))) + +(defn add-swap-slots + [file libs _opts] + ;; Detect swapped copies and try to generate a valid swap-slot. + (letfn [(process-fdata [data] + ;; Walk through all containers in the file, both pages and deleted components. + (reduce process-container data (ctf/object-containers-seq data))) + + (process-container [data container] + ;; Walk through all shapes in depth-first tree order. + (l/dbg :hint "Processing container" :type (:type container) :name (:name container)) + (let [root-shape (ctn/get-container-root container)] + (ctf/update-container data + container + #(reduce process-shape % (ctn/get-direct-children container root-shape))))) + + (process-shape [container shape] + ;; Look for head copies in the first level (either component roots or inside main components). + ;; Even if they have been swapped, we don't add slot to them because there is no way to know + ;; the original shape. Only children. + (if (and (ctk/instance-head? shape) + (ctk/in-component-copy? shape) + (nil? (ctk/get-swap-slot shape))) + (process-copy-head container shape) + (reduce process-shape container (ctn/get-direct-children container shape)))) + + (process-copy-head [container head-shape] + ;; Process recursively all children, comparing each one with the corresponding child in the main + ;; component, looking by position. If the shape-ref does not point to the found child, then it has + ;; been swapped and need to set up a slot. + (l/trc :hint "Processing copy-head" :id (:id head-shape) :name (:name head-shape)) + (let [component-shape (ctf/find-ref-shape file container libs head-shape :include-deleted? true :with-context? true) + component-container (:container (meta component-shape))] + (loop [container container + children (map #(ctn/get-shape container %) (:shapes head-shape)) + component-children (map #(ctn/get-shape component-container %) (:shapes component-shape))] + (let [child (first children) + component-child (first component-children)] + (if (or (nil? child) (nil? component-child)) + container + (let [container (if (and (not (ctk/is-main-of? component-child child true)) + (nil? (ctk/get-swap-slot child)) + (ctk/instance-head? child)) + (let [slot (guess-swap-slot component-child component-container)] + (l/dbg :hint "child" :id (:id child) :name (:name child) :slot slot) + (ctn/update-shape container (:id child) #(ctk/set-swap-slot % slot))) + container)] + (recur (process-copy-head container child) + (rest children) + (rest component-children)))))))) + + (guess-swap-slot [shape container] + ;; To guess the slot, we must follow the chain until we find the definitive main. But + ;; we cannot navigate by shape-ref, because main shapes may also have been swapped. So + ;; chain by position, too. + (if-let [slot (ctk/get-swap-slot shape)] + slot + (if-not (ctk/in-component-copy? shape) + (:id shape) + (let [head-copy (ctn/get-component-shape (:objects container) shape)] + (if (= (:id head-copy) (:id shape)) + (:id shape) + (let [head-main (ctf/find-ref-shape file + container + libs + head-copy + :include-deleted? true + :with-context? true) + container-main (:container (meta head-main)) + shape-main (find-match-by-position shape + head-copy + container + head-main + container-main)] + (guess-swap-slot shape-main container-main))))))) + + (find-match-by-position [shape-copy head-copy container-copy head-main container-main] + ;; Find the shape in the main that has the same position under its parent than + ;; the copy under its one. To get the parent we must process recursively until + ;; the component head, because mains may also have been swapped. + (let [parent-copy (ctn/get-shape container-copy (:parent-id shape-copy)) + parent-main (if (= (:id parent-copy) (:id head-copy)) + head-main + (find-match-by-position parent-copy + head-copy + container-copy + head-main + container-main)) + index (cfh/get-position-on-parent (:objects container-copy) + (:id shape-copy)) + shape-main-id (dm/get-in parent-main [:shapes index])] + (ctn/get-shape container-main shape-main-id)))] + + file (-> file + (update :data process-fdata)))) + + + +(defn fix-find-duplicated-slots + [file _] + ;; Find the shapes whose children have duplicated slots + (let [check-duplicate-swap-slot + (fn [shape page] + (let [shapes (map #(get (:objects page) %) (:shapes shape)) + slots (->> (map #(ctk/get-swap-slot %) shapes) + (remove nil?)) + counts (frequencies slots)] + #_(when (some (fn [[_ count]] (> count 1)) counts) + (l/trc :info "This shape has children with the same swap slot" :id (:id shape) :file-id (str (:id file)))) + (some (fn [[_ count]] (> count 1)) counts))) + + count-slots-shape + (fn [page shape] + (if (ctk/instance-root? shape) + (check-duplicate-swap-slot shape page) + false)) + + count-slots-page + (fn [page] + (->> (:objects page) + (vals) + (mapv #(count-slots-shape page %)) + (filter true?) + count)) + + count-slots-data + (fn [data] + (->> (:pages-index data) + (vals) + (mapv count-slots-page) + (reduce +))) + + num-missing-slots (count-slots-data (:data file))] + (when (pos? num-missing-slots) + (l/trc :info (str "Shapes with children with the same swap slot: " num-missing-slots) :file-id (str (:id file)))) + file)) diff --git a/backend/src/app/srepl/helpers.clj b/backend/src/app/srepl/helpers.clj index 010a0aa7a3a8e24a9bff4783988125bcfa99fef3..38ea61dd89aa5fd401d8489b2ed77ac9f4ff673a 100644 --- a/backend/src/app/srepl/helpers.clj +++ b/backend/src/app/srepl/helpers.clj @@ -35,15 +35,18 @@ (defn get-file "Get the migrated data of one file." - ([id] (get-file (or *system* main/system) id)) - ([system id] + ([id] (get-file (or *system* main/system) id nil)) + ([system id & {:keys [raw?] :as opts}] (db/run! system (fn [system] - (binding [pmap/*load-fn* (partial feat.fdata/load-pointer system id)] - (-> (files/get-file system id :migrate? false) - (update :data feat.fdata/process-pointers deref) - (update :data feat.fdata/process-objects (partial into {})) - (fmg/migrate-file))))))) + (let [file (files/get-file system id :migrate? false)] + (if raw? + file + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer system id)] + (-> file + (update :data feat.fdata/process-pointers deref) + (update :data feat.fdata/process-objects (partial into {})) + (fmg/migrate-file))))))))) (defn update-file! [system {:keys [id] :as file}] @@ -166,7 +169,7 @@ (fsnap/take-file-snapshot! system {:file-id file-id :label label})) (let [conn (db/get-connection system) - file (get-file system file-id) + file (get-file system file-id opts) libs (when with-libraries? (->> (files/get-file-libraries conn file-id) (into [file] (map (fn [{:keys [id]}] diff --git a/backend/src/app/srepl/main.clj b/backend/src/app/srepl/main.clj index 17df79a9a79ae577da15fce9cd791daf7377aa22..2085205cb9a75343c147728f64c5c61fa38582e4 100644 --- a/backend/src/app/srepl/main.clj +++ b/backend/src/app/srepl/main.clj @@ -21,8 +21,10 @@ [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.db.sql :as-alias sql] [app.features.components-v2 :as feat.comp-v2] [app.features.fdata :as feat.fdata] + [app.loggers.audit :as audit] [app.main :as main] [app.msgbus :as mbus] [app.rpc.commands.auth :as auth] @@ -30,16 +32,20 @@ [app.rpc.commands.files-snapshot :as fsnap] [app.rpc.commands.management :as mgmt] [app.rpc.commands.profile :as profile] + [app.rpc.commands.projects :as projects] + [app.rpc.commands.teams :as teams] [app.srepl.fixes :as fixes] [app.srepl.helpers :as h] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.time :as dt] [app.worker :as wrk] + [clojure.java.io :as io] [clojure.pprint :refer [print-table]] [clojure.stacktrace :as strace] [clojure.tools.namespace.repl :as repl] [cuerdas.core :as str] + [datoteka.fs :as fs] [promesa.exec :as px] [promesa.exec.semaphore :as ps] [promesa.util :as pu])) @@ -57,42 +63,35 @@ ([tname] (run-task! tname {})) ([tname params] - (let [tasks (:app.worker/registry main/system) - tname (if (keyword? tname) (name tname) name)] - (if-let [task-fn (get tasks tname)] - (task-fn params) - (println (format "no task '%s' found" tname)))))) + (wrk/invoke! (-> main/system + (assoc ::wrk/task tname) + (assoc ::wrk/params params))))) (defn schedule-task! ([name] (schedule-task! name {})) - ([name props] - (let [pool (:app.db/pool main/system)] - (wrk/submit! - ::wrk/conn pool - ::wrk/task name - ::wrk/props props)))) + ([name params] + (wrk/submit! (-> main/system + (assoc ::wrk/task name) + (assoc ::wrk/params params))))) (defn send-test-email! [destination] - (us/verify! - :expr (string? destination) - :hint "destination should be provided") - - (let [handler (:app.email/sendmail main/system)] - (handler {:body "test email" - :subject "test email" - :to [destination]}))) + (assert (string? destination) "destination should be provided") + (-> main/system + (assoc ::wrk/task :sendmail) + (assoc ::wrk/params {:body "test email" + :subject "test email" + :to [destination]}) + (wrk/invoke!))) (defn resend-email-verification-email! [email] - (let [sprops (:app.setup/props main/system) - pool (:app.db/pool main/system) - email (profile/clean-email email) - profile (profile/get-profile-by-email pool email)] - - (auth/send-email-verification! pool sprops profile) - :email-sent)) + (db/tx-run! main/system + (fn [{:keys [::db/conn] :as cfg}] + (let [email (profile/clean-email email) + profile (profile/get-profile-by-email conn email)] + (#'auth/send-email-verification! cfg profile))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; PROFILES MANAGEMENT @@ -194,8 +193,13 @@ ;; NOTIFICATIONS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - (defn notify! + "Send flash notifications. + + This method allows send flash notifications to specified target destinations. + The message can be a free text or a preconfigured one. + + The destination can be: all, profile-id, team-id, or a coll of them." [{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level] :or {code :generic level :info} :as params}] @@ -203,10 +207,6 @@ ["invalid level %" level] (contains? #{:success :error :info :warning} level)) - (dm/verify! - ["invalid code: %" code] - (contains? #{:generic :upgrade-version} code)) - (letfn [(send [dest] (l/inf :hint "sending notification" :dest (str dest)) (let [message {:type :notification @@ -232,6 +232,9 @@ (resolve-dest [dest] (cond + (= :all dest) + [uuid/zero] + (uuid? dest) [dest] @@ -247,14 +250,15 @@ (mapcat resolve-dest)) dest) - (and (coll? dest) - (every? coll? dest)) + (and (vector? dest) + (every? vector? dest)) (sequence (comp (map vec) (mapcat resolve-dest)) dest) - (vector? dest) + (and (vector? dest) + (keyword? (first dest))) (let [[op param] dest] (cond (= op :email) @@ -429,7 +433,9 @@ (try (l/trc :hint "process:file:start" :file-id (str file-id) :index idx) (let [system (assoc main/system ::db/rollback rollback?)] - (db/tx-run! system h/process-file! file-id update-fn opts)) + (db/tx-run! system (fn [system] + (binding [h/*system* system] + (h/process-file! system file-id update-fn opts))))) (catch Throwable cause (l/wrn :hint "unexpected error on processing file (skiping)" @@ -474,6 +480,334 @@ :rollback rollback? :elapsed elapsed)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; DELETE/RESTORE OBJECTS (WITH CASCADE, SOFT) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn delete-file! + "Mark a project for deletion" + [file-id] + (let [file-id (h/parse-uuid file-id) + tnow (dt/now)] + + (audit/insert! main/system + {::audit/name "delete-file" + ::audit/type "action" + ::audit/profile-id uuid/zero + ::audit/props {:id file-id} + ::audit/context {:triggered-by "srepl" + :cause "explicit call to delete-file!"} + ::audit/tracked-at tnow}) + (wrk/invoke! (-> main/system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :file + :deleted-at tnow + :id file-id}))) + :deleted)) + +(defn- restore-file* + [{:keys [::db/conn]} file-id] + (db/update! conn :file + {:deleted-at nil + :has-media-trimmed false} + {:id file-id}) + + ;; Fragments are not handled here because they + ;; use the database cascade operation and they + ;; are not marked for deletion with objects-gc + ;; task + + (db/update! conn :file-media-object + {:deleted-at nil} + {:file-id file-id}) + + ;; Mark thumbnails to be deleted + (db/update! conn :file-thumbnail + {:deleted-at nil} + {:file-id file-id}) + + (db/update! conn :file-tagged-object-thumbnail + {:deleted-at nil} + {:file-id file-id}) + + :restored) + +(defn restore-file! + "Mark a file and all related objects as not deleted" + [file-id] + (let [file-id (h/parse-uuid file-id)] + (db/tx-run! main/system + (fn [system] + (when-let [file (some-> (db/get* system :file + {:id file-id} + {::db/remove-deleted false + ::sql/columns [:id :name]}) + (files/decode-row))] + (audit/insert! system + {::audit/name "restore-file" + ::audit/type "action" + ::audit/profile-id uuid/zero + ::audit/props file + ::audit/context {:triggered-by "srepl" + :cause "explicit call to restore-file!"} + ::audit/tracked-at (dt/now)}) + + (restore-file* system file-id)))))) + +(defn delete-project! + "Mark a project for deletion" + [project-id] + (let [project-id (h/parse-uuid project-id) + tnow (dt/now)] + + (audit/insert! main/system + {::audit/name "delete-project" + ::audit/type "action" + ::audit/profile-id uuid/zero + ::audit/props {:id project-id} + ::audit/context {:triggered-by "srepl" + :cause "explicit call to delete-project!"} + ::audit/tracked-at tnow}) + + (wrk/invoke! (-> main/system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :project + :deleted-at tnow + :id project-id}))) + :deleted)) + +(defn- restore-project* + [{:keys [::db/conn] :as cfg} project-id] + (db/update! conn :project + {:deleted-at nil} + {:id project-id}) + + (doseq [{:keys [id]} (db/query conn :file + {:project-id project-id} + {::sql/columns [:id]})] + (restore-file* cfg id)) + + :restored) + +(defn restore-project! + "Mark a project and all related objects as not deleted" + [project-id] + (let [project-id (h/parse-uuid project-id)] + (db/tx-run! main/system + (fn [system] + (when-let [project (db/get* system :project + {:id project-id} + {::db/remove-deleted false})] + (audit/insert! system + {::audit/name "restore-project" + ::audit/type "action" + ::audit/profile-id uuid/zero + ::audit/props project + ::audit/context {:triggered-by "srepl" + :cause "explicit call to restore-team!"} + ::audit/tracked-at (dt/now)}) + + (restore-project* system project-id)))))) + +(defn delete-team! + "Mark a team for deletion" + [team-id] + (let [team-id (h/parse-uuid team-id) + tnow (dt/now)] + + (audit/insert! main/system + {::audit/name "delete-team" + ::audit/type "action" + ::audit/profile-id uuid/zero + ::audit/props {:id team-id} + ::audit/context {:triggered-by "srepl" + :cause "explicit call to delete-profile!"} + ::audit/tracked-at tnow}) + + (wrk/invoke! (-> main/system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :team + :deleted-at tnow + :id team-id}))) + :deleted)) + +(defn- restore-team* + [{:keys [::db/conn] :as cfg} team-id] + (db/update! conn :team + {:deleted-at nil} + {:id team-id}) + + (db/update! conn :team-font-variant + {:deleted-at nil} + {:team-id team-id}) + + (doseq [{:keys [id]} (db/query conn :project + {:team-id team-id} + {::sql/columns [:id]})] + (restore-project* cfg id)) + + :restored) + +(defn restore-team! + "Mark a team and all related objects as not deleted" + [team-id] + (let [team-id (h/parse-uuid team-id)] + (db/tx-run! main/system + (fn [system] + (when-let [team (some-> (db/get* system :team + {:id team-id} + {::db/remove-deleted false}) + (teams/decode-row))] + (audit/insert! system + {::audit/name "restore-team" + ::audit/type "action" + ::audit/profile-id uuid/zero + ::audit/props team + ::audit/context {:triggered-by "srepl" + :cause "explicit call to restore-team!"} + ::audit/tracked-at (dt/now)}) + + (restore-team* system team-id)))))) + +(defn delete-profile! + "Mark a profile for deletion." + [profile-id] + (let [profile-id (h/parse-uuid profile-id) + tnow (dt/now)] + + (audit/insert! main/system + {::audit/name "delete-profile" + ::audit/type "action" + ::audit/profile-id uuid/zero + ::audit/context {:triggered-by "srepl" + :cause "explicit call to delete-profile!"} + ::audit/tracked-at tnow}) + + (wrk/invoke! (-> main/system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :profile + :deleted-at tnow + :id profile-id}))) + :deleted)) + +(defn restore-profile! + "Mark a team and all related objects as not deleted" + [profile-id] + (let [profile-id (h/parse-uuid profile-id)] + (db/tx-run! main/system + (fn [system] + (when-let [profile (some-> (db/get* system :profile + {:id profile-id} + {::db/remove-deleted false}) + (profile/decode-row))] + (audit/insert! system + {::audit/name "restore-profile" + ::audit/type "action" + ::audit/profile-id uuid/zero + ::audit/props (audit/profile->props profile) + ::audit/context {:triggered-by "srepl" + :cause "explicit call to restore-profile!"} + ::audit/tracked-at (dt/now)}) + + (db/update! system :profile + {:deleted-at nil} + {:id profile-id} + {::db/return-keys false}) + + (doseq [{:keys [id]} (profile/get-owned-teams system profile-id)] + (restore-team* system id)) + + :restored))))) + +(defn delete-profiles-in-bulk! + [system path] + (letfn [(process-data! [system deleted-at emails] + (loop [emails emails + deleted 0 + total 0] + (if-let [email (first emails)] + (if-let [profile (some-> (db/get* system :profile + {:email (str/lower email)} + {::db/remove-deleted false}) + (profile/decode-row))] + (do + (audit/insert! system + {::audit/name "delete-profile" + ::audit/type "action" + ::audit/profile-id (:id profile) + ::audit/tracked-at deleted-at + ::audit/props (audit/profile->props profile) + ::audit/context {:triggered-by "srepl" + :cause "explicit call to delete-profiles-in-bulk!"}}) + (wrk/invoke! (-> system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :profile + :deleted-at deleted-at + :id (:id profile)}))) + (recur (rest emails) + (inc deleted) + (inc total))) + (recur (rest emails) + deleted + (inc total))) + {:deleted deleted :total total})))] + + (let [path (fs/path path) + deleted-at (dt/minus (dt/now) (cf/get-deletion-delay))] + + (when-not (fs/exists? path) + (throw (ex-info "path does not exists" {:path path}))) + + (db/tx-run! system + (fn [system] + (with-open [reader (io/reader path)] + (process-data! system deleted-at (line-seq reader)))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; CASCADE FIXING +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn process-deleted-profiles-cascade + [] + (->> (db/exec! main/system ["select id, deleted_at from profile where deleted_at is not null"]) + (run! (fn [{:keys [id deleted-at]}] + (wrk/invoke! (-> main/system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :profile + :deleted-at deleted-at + :id id}))))))) + +(defn process-deleted-teams-cascade + [] + (->> (db/exec! main/system ["select id, deleted_at from team where deleted_at is not null"]) + (run! (fn [{:keys [id deleted-at]}] + (wrk/invoke! (-> main/system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :team + :deleted-at deleted-at + :id id}))))))) + +(defn process-deleted-projects-cascade + [] + (->> (db/exec! main/system ["select id, deleted_at from project where deleted_at is not null"]) + (run! (fn [{:keys [id deleted-at]}] + (wrk/invoke! (-> main/system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :project + :deleted-at deleted-at + :id id}))))))) + +(defn process-deleted-files-cascade + [] + (->> (db/exec! main/system ["select id, deleted_at from file where deleted_at is not null"]) + (run! (fn [{:keys [id deleted-at]}] + (wrk/invoke! (-> main/system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :file + :deleted-at deleted-at + :id id}))))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; MISC ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/backend/src/app/storage.clj b/backend/src/app/storage.clj index f6924aedb435c6467360131f505327340008be58..861730e3361458f9ebed46bc34de8f51bacedfce 100644 --- a/backend/src/app/storage.clj +++ b/backend/src/app/storage.clj @@ -6,11 +6,13 @@ (ns app.storage "Objects storage abstraction layer." + (:refer-clojure :exclude [resolve]) (:require [app.common.data :as d] [app.common.data.macros :as dm] [app.common.spec :as us] [app.common.uuid :as uuid] + [app.config :as cf] [app.db :as db] [app.storage.fs :as sfs] [app.storage.impl :as impl] @@ -18,16 +20,23 @@ [app.util.time :as dt] [clojure.spec.alpha :as s] [datoteka.fs :as fs] - [integrant.core :as ig] - [promesa.core :as p]) + [integrant.core :as ig]) (:import java.io.InputStream)) +(defn get-legacy-backend + [] + (let [name (cf/get :assets-storage-backend)] + (case name + :assets-fs :fs + :assets-s3 :s3 + :fs))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Storage Module State ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::id #{:assets-fs :assets-s3}) +(s/def ::id #{:assets-fs :assets-s3 :fs :s3}) (s/def ::s3 ::ss3/backend) (s/def ::fs ::sfs/backend) (s/def ::type #{:fs :s3}) @@ -45,11 +54,13 @@ [_ {:keys [::backends ::db/pool] :as cfg}] (-> (d/without-nils cfg) (assoc ::backends (d/without-nils backends)) - (assoc ::db/pool-or-conn pool))) + (assoc ::backend (or (get-legacy-backend) + (cf/get :objects-storage-backend :fs))) + (assoc ::db/connectable pool))) (s/def ::backend keyword?) (s/def ::storage - (s/keys :req [::backends ::db/pool ::db/pool-or-conn] + (s/keys :req [::backends ::db/pool ::db/connectable] :opt [::backend])) (s/def ::storage-with-backend @@ -61,23 +72,26 @@ (defn get-metadata [params] - (into {} - (remove (fn [[k _]] (qualified-keyword? k))) - params)) + (reduce-kv (fn [res k _] + (if (qualified-keyword? k) + (dissoc res k) + res)) + params + params)) (defn- get-database-object-by-hash - [pool-or-conn backend bucket hash] + [connectable backend bucket hash] (let [sql (str "select * from storage_object " " where (metadata->>'~:hash') = ? " " and (metadata->>'~:bucket') = ? " " and backend = ?" " and deleted_at is null" " limit 1")] - (some-> (db/exec-one! pool-or-conn [sql hash bucket (name backend)]) + (some-> (db/exec-one! connectable [sql hash bucket (name backend)]) (update :metadata db/decode-transit-pgobject)))) (defn- create-database-object - [{:keys [::backend ::db/pool-or-conn]} {:keys [::content ::expired-at ::touched-at] :as params}] + [{:keys [::backend ::db/connectable]} {:keys [::content ::expired-at ::touched-at ::touch] :as params}] (let [id (or (:id params) (uuid/random)) mdata (cond-> (get-metadata params) (satisfies? impl/IContentHash content) @@ -86,7 +100,9 @@ :always (dissoc :id)) - ;; FIXME: touch object on deduplicated put operation ?? + touched-at (if touch + (or touched-at (dt/now)) + touched-at) ;; NOTE: for now we don't reuse the deleted objects, but in ;; futute we can consider reusing deleted objects if we @@ -95,10 +111,20 @@ result (when (and (::deduplicate? params) (:hash mdata) (:bucket mdata)) - (get-database-object-by-hash pool-or-conn backend (:bucket mdata) (:hash mdata))) + (let [result (get-database-object-by-hash connectable backend + (:bucket mdata) + (:hash mdata))] + (if touch + (do + (db/update! connectable :storage-object + {:touched-at touched-at} + {:id (:id result)} + {::db/return-keys false}) + (assoc result :touced-at touched-at)) + result))) result (or result - (-> (db/insert! pool-or-conn :storage-object + (-> (db/insert! connectable :storage-object {:id id :size (impl/get-size content) :backend (name backend) @@ -154,9 +180,9 @@ (dm/export impl/object?) (defn get-object - [{:keys [::db/pool-or-conn] :as storage} id] + [{:keys [::db/connectable] :as storage} id] (us/assert! ::storage storage) - (retrieve-database-object pool-or-conn id)) + (retrieve-database-object connectable id)) (defn put-object! "Creates a new object with the provided content." @@ -172,13 +198,14 @@ (defn touch-object! "Mark object as touched." - [{:keys [::db/pool-or-conn] :as storage} object-or-id] + [{:keys [::db/connectable] :as storage} object-or-id] (us/assert! ::storage storage) - (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id) - rs (db/update! pool-or-conn :storage-object - {:touched-at (dt/now)} - {:id id})] - (pos? (db/get-update-count rs)))) + (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)] + (-> (db/update! connectable :storage-object + {:touched-at (dt/now)} + {:id id}) + (db/get-update-count) + (pos?)))) (defn get-object-data "Return an input stream instance of the object content." @@ -194,11 +221,10 @@ "Returns a byte array of object content." [storage object] (us/assert! ::storage storage) - (if (or (nil? (:expired-at object)) - (dt/is-after? (:expired-at object) (dt/now))) + (when (or (nil? (:expired-at object)) + (dt/is-after? (:expired-at object) (dt/now))) (-> (impl/resolve-backend storage (:backend object)) - (impl/get-object-bytes object)) - (p/resolved nil))) + (impl/get-object-bytes object)))) (defn get-object-url ([storage object] @@ -222,13 +248,26 @@ (-> (impl/get-object-url backend object nil) file-url->path)))) (defn del-object! - [{:keys [::db/pool-or-conn] :as storage} object-or-id] + [{:keys [::db/connectable] :as storage} object-or-id] (us/assert! ::storage storage) (let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id) - res (db/update! pool-or-conn :storage-object + res (db/update! connectable :storage-object {:deleted-at (dt/now)} {:id id})] (pos? (db/get-update-count res)))) -(dm/export impl/resolve-backend) (dm/export impl/calculate-hash) + +(defn configure + [storage connectable] + (assoc storage ::db/connectable connectable)) + +(defn resolve + "Resolves the storage instance with preconfigured backend. You can + specify to reuse the database connection from provided + cfg/system (default false)." + [cfg & {:as opts}] + (let [storage (::storage cfg)] + (if (::db/reuse-conn opts false) + (configure storage (db/get-connectable cfg)) + storage))) diff --git a/backend/src/app/storage/gc_deleted.clj b/backend/src/app/storage/gc_deleted.clj index 8d1d0e5ad1762ae129fba82694c0d8175e4e6084..7f903b0000ec8b5a76187b40ce68a4257ce0525e 100644 --- a/backend/src/app/storage/gc_deleted.clj +++ b/backend/src/app/storage/gc_deleted.clj @@ -110,8 +110,8 @@ (defmethod ig/init-key ::handler [_ {:keys [::min-age] :as cfg}] - (fn [params] - (let [min-age (dt/duration (or (:min-age params) min-age))] + (fn [{:keys [props] :as task}] + (let [min-age (dt/duration (or (:min-age props) min-age))] (db/tx-run! cfg (fn [cfg] (let [cfg (assoc cfg ::min-age min-age) total (clean-deleted! cfg)] @@ -121,5 +121,3 @@ :total total) {:deleted total})))))) - - diff --git a/backend/src/app/storage/gc_touched.clj b/backend/src/app/storage/gc_touched.clj index bd499bb6559c8265b0ce19e298d15a7c058b6422..03fe0f426cf17576580e3803cc517f07abe9cd78 100644 --- a/backend/src/app/storage/gc_touched.clj +++ b/backend/src/app/storage/gc_touched.clj @@ -28,58 +28,80 @@ [clojure.spec.alpha :as s] [integrant.core :as ig])) -(def ^:private sql:get-team-font-variant-nrefs - "SELECT ((SELECT count(*) FROM team_font_variant WHERE woff1_file_id = ?) + - (SELECT count(*) FROM team_font_variant WHERE woff2_file_id = ?) + - (SELECT count(*) FROM team_font_variant WHERE otf_file_id = ?) + - (SELECT count(*) FROM team_font_variant WHERE ttf_file_id = ?)) AS nrefs") +(def ^:private sql:has-team-font-variant-refs + "SELECT ((SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE woff1_file_id = ?)) OR + (SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE woff2_file_id = ?)) OR + (SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE otf_file_id = ?)) OR + (SELECT EXISTS (SELECT 1 FROM team_font_variant WHERE ttf_file_id = ?))) AS has_refs") -(defn- get-team-font-variant-nrefs +(defn- has-team-font-variant-refs? [conn id] - (-> (db/exec-one! conn [sql:get-team-font-variant-nrefs id id id id]) - (get :nrefs))) - + (-> (db/exec-one! conn [sql:has-team-font-variant-refs id id id id]) + (get :has-refs))) (def ^:private - sql:get-file-media-object-nrefs - "SELECT ((SELECT count(*) FROM file_media_object WHERE media_id = ?) + - (SELECT count(*) FROM file_media_object WHERE thumbnail_id = ?)) AS nrefs") + sql:has-file-media-object-refs + "SELECT ((SELECT EXISTS (SELECT 1 FROM file_media_object WHERE media_id = ?)) OR + (SELECT EXISTS (SELECT 1 FROM file_media_object WHERE thumbnail_id = ?))) AS has_refs") -(defn- get-file-media-object-nrefs +(defn- has-file-media-object-refs? [conn id] - (-> (db/exec-one! conn [sql:get-file-media-object-nrefs id id]) - (get :nrefs))) - + (-> (db/exec-one! conn [sql:has-file-media-object-refs id id]) + (get :has-refs))) -(def ^:private sql:get-profile-nrefs - "SELECT ((SELECT count(*) FROM profile WHERE photo_id = ?) + - (SELECT count(*) FROM team WHERE photo_id = ?)) AS nrefs") +(def ^:private sql:has-profile-refs + "SELECT ((SELECT EXISTS (SELECT 1 FROM profile WHERE photo_id = ?)) OR + (SELECT EXISTS (SELECT 1 FROM team WHERE photo_id = ?))) AS has_refs") -(defn- get-profile-nrefs +(defn- has-profile-refs? [conn id] - (-> (db/exec-one! conn [sql:get-profile-nrefs id id]) - (get :nrefs))) + (-> (db/exec-one! conn [sql:has-profile-refs id id]) + (get :has-refs))) +(def ^:private + sql:has-file-object-thumbnail-refs + "SELECT EXISTS (SELECT 1 FROM file_tagged_object_thumbnail WHERE media_id = ?) AS has_refs") + +(defn- has-file-object-thumbnails-refs? + [conn id] + (-> (db/exec-one! conn [sql:has-file-object-thumbnail-refs id]) + (get :has-refs))) (def ^:private - sql:get-file-object-thumbnail-nrefs - "SELECT (SELECT count(*) FROM file_tagged_object_thumbnail WHERE media_id = ?) AS nrefs") + sql:has-file-thumbnail-refs + "SELECT EXISTS (SELECT 1 FROM file_thumbnail WHERE media_id = ?) AS has_refs") -(defn- get-file-object-thumbnails +(defn- has-file-thumbnails-refs? [conn id] - (-> (db/exec-one! conn [sql:get-file-object-thumbnail-nrefs id]) - (get :nrefs))) + (-> (db/exec-one! conn [sql:has-file-thumbnail-refs id]) + (get :has-refs))) + +(def ^:private + sql:has-file-data-refs + "SELECT EXISTS (SELECT 1 FROM file WHERE data_ref_id = ?) AS has_refs") +(defn- has-file-data-refs? + [conn id] + (-> (db/exec-one! conn [sql:has-file-data-refs id]) + (get :has-refs))) (def ^:private - sql:get-file-thumbnail-nrefs - "SELECT (SELECT count(*) FROM file_thumbnail WHERE media_id = ?) AS nrefs") + sql:has-file-data-fragment-refs + "SELECT EXISTS (SELECT 1 FROM file_data_fragment WHERE data_ref_id = ?) AS has_refs") -(defn- get-file-thumbnails +(defn- has-file-data-fragment-refs? [conn id] - (-> (db/exec-one! conn [sql:get-file-thumbnail-nrefs id]) - (get :nrefs))) + (-> (db/exec-one! conn [sql:has-file-data-fragment-refs id]) + (get :has-refs))) + +(def ^:private + sql:has-file-change-refs + "SELECT EXISTS (SELECT 1 FROM file_change WHERE data_ref_id = ?) AS has_refs") +(defn- has-file-change-refs? + [conn id] + (-> (db/exec-one! conn [sql:has-file-change-refs id]) + (get :has-refs))) (def ^:private sql:mark-freeze-in-bulk "UPDATE storage_object @@ -91,7 +113,6 @@ (let [ids (db/create-array conn "uuid" ids)] (db/exec-one! conn [sql:mark-freeze-in-bulk ids]))) - (def ^:private sql:mark-delete-in-bulk "UPDATE storage_object SET deleted_at = now(), @@ -123,25 +144,24 @@ "file-media-object")) (defn- process-objects! - [conn get-fn ids bucket] + [conn has-refs? ids bucket] (loop [to-freeze #{} to-delete #{} ids (seq ids)] (if-let [id (first ids)] - (let [nrefs (get-fn conn id)] - (if (pos? nrefs) - (do - (l/debug :hint "processing object" - :id (str id) - :status "freeze" - :bucket bucket :refs nrefs) - (recur (conj to-freeze id) to-delete (rest ids))) - (do - (l/debug :hint "processing object" - :id (str id) - :status "delete" - :bucket bucket :refs nrefs) - (recur to-freeze (conj to-delete id) (rest ids))))) + (if (has-refs? conn id) + (do + (l/debug :hint "processing object" + :id (str id) + :status "freeze" + :bucket bucket) + (recur (conj to-freeze id) to-delete (rest ids))) + (do + (l/debug :hint "processing object" + :id (str id) + :status "delete" + :bucket bucket) + (recur to-freeze (conj to-delete id) (rest ids)))) (do (some->> (seq to-freeze) (mark-freeze-in-bulk! conn)) (some->> (seq to-delete) (mark-delete-in-bulk! conn)) @@ -150,15 +170,26 @@ (defn- process-bucket! [conn bucket ids] (case bucket - "file-media-object" (process-objects! conn get-file-media-object-nrefs ids bucket) - "team-font-variant" (process-objects! conn get-team-font-variant-nrefs ids bucket) - "file-object-thumbnail" (process-objects! conn get-file-object-thumbnails ids bucket) - "file-thumbnail" (process-objects! conn get-file-thumbnails ids bucket) - "profile" (process-objects! conn get-profile-nrefs ids bucket) + "file-media-object" (process-objects! conn has-file-media-object-refs? ids bucket) + "team-font-variant" (process-objects! conn has-team-font-variant-refs? ids bucket) + "file-object-thumbnail" (process-objects! conn has-file-object-thumbnails-refs? ids bucket) + "file-thumbnail" (process-objects! conn has-file-thumbnails-refs? ids bucket) + "profile" (process-objects! conn has-profile-refs? ids bucket) + "file-data" (process-objects! conn has-file-data-refs? ids bucket) + "file-data-fragment" (process-objects! conn has-file-data-fragment-refs? ids bucket) + "file-change" (process-objects! conn has-file-change-refs? ids bucket) (ex/raise :type :internal :code :unexpected-unknown-reference - :hint (dm/fmt "unknown reference %" bucket)))) + :hint (dm/fmt "unknown reference '%'" bucket)))) +(defn process-chunk! + [{:keys [::db/conn]} chunk] + (reduce-kv (fn [[nfo ndo] bucket ids] + (let [[nfo' ndo'] (process-bucket! conn bucket ids)] + [(+ nfo nfo') + (+ ndo ndo')])) + [0 0] + (d/group-by lookup-bucket :id #{} chunk))) (def ^:private sql:get-touched-storage-objects @@ -167,29 +198,22 @@ WHERE so.touched_at IS NOT NULL ORDER BY touched_at ASC FOR UPDATE - SKIP LOCKED") - -(defn- group-by-bucket - [row] - (d/group-by lookup-bucket :id #{} row)) + SKIP LOCKED + LIMIT 10") -(defn- get-buckets +(defn get-chunk [conn] - (sequence - (comp (map impl/decode-row) - (partition-all 25) - (mapcat group-by-bucket)) - (db/cursor conn sql:get-touched-storage-objects))) + (->> (db/exec! conn [sql:get-touched-storage-objects]) + (map impl/decode-row) + (not-empty))) (defn- process-touched! - [{:keys [::db/conn]}] - (loop [buckets (get-buckets conn) - freezed 0 + [{:keys [::db/pool] :as cfg}] + (loop [freezed 0 deleted 0] - (if-let [[bucket ids] (first buckets)] - (let [[nfo ndo] (process-bucket! conn bucket ids)] - (recur (rest buckets) - (+ freezed nfo) + (if-let [chunk (get-chunk pool)] + (let [[nfo ndo] (db/tx-run! cfg process-chunk! chunk)] + (recur (+ freezed nfo) (+ deleted ndo))) (do (l/inf :hint "task finished" @@ -198,11 +222,14 @@ {:freeze freezed :delete deleted})))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; HANDLER +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + (defmethod ig/pre-init-spec ::handler [_] (s/keys :req [::db/pool])) (defmethod ig/init-key ::handler [_ cfg] - (fn [_] - (db/tx-run! cfg process-touched!))) + (fn [_] (process-touched! cfg))) diff --git a/backend/src/app/storage/impl.clj b/backend/src/app/storage/impl.clj index 156d86b87293578ba379f06c242a91b26706b1e2..6de48b68227d627f36e5b78c0c600ed37038c6f9 100644 --- a/backend/src/app/storage/impl.clj +++ b/backend/src/app/storage/impl.clj @@ -207,15 +207,13 @@ (str "blake2b:" result))) (defn resolve-backend - [{:keys [::db/pool] :as storage} backend-id] + [storage backend-id] (let [backend (get-in storage [::sto/backends backend-id])] (when-not backend (ex/raise :type :internal :code :backend-not-configured :hint (dm/fmt "backend '%' not configured" backend-id))) - (-> backend - (assoc ::sto/id backend-id) - (assoc ::db/pool pool)))) + (assoc backend ::sto/id backend-id))) (defrecord StorageObject [id size created-at expired-at touched-at backend]) diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj new file mode 100644 index 0000000000000000000000000000000000000000..9c48d23091c39592410f32b6e60bbfce284fd75b --- /dev/null +++ b/backend/src/app/tasks/delete_object.clj @@ -0,0 +1,122 @@ +;; 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 app.tasks.delete-object + "A generic task for object deletion cascade handling" + (:require + [app.common.logging :as l] + [app.db :as db] + [app.rpc.commands.files :as files] + [app.rpc.commands.profile :as profile] + [app.util.time :as dt] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(def ^:dynamic *team-deletion* false) + +(defmulti delete-object + (fn [_ props] (:object props))) + +(defmethod delete-object :file + [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] + (when-let [file (db/get* conn :file {:id id} {::db/remove-deleted false})] + (l/trc :hint "marking for deletion" :rel "file" :id (str id) + :deleted-at (dt/format-instant deleted-at)) + + (db/update! conn :file + {:deleted-at deleted-at} + {:id id} + {::db/return-keys false}) + + (when (and (:is-shared file) + (not *team-deletion*)) + ;; NOTE: we don't prevent file deletion on absorb operation failure + (try + (db/tx-run! cfg files/absorb-library! id) + (catch Throwable cause + (l/warn :hint "error on absorbing library" + :file-id id + :cause cause)))) + + ;; Mark file media objects to be deleted + (db/update! conn :file-media-object + {:deleted-at deleted-at} + {:file-id id}) + + ;; Mark thumbnails to be deleted + (db/update! conn :file-thumbnail + {:deleted-at deleted-at} + {:file-id id}) + + (db/update! conn :file-tagged-object-thumbnail + {:deleted-at deleted-at} + {:file-id id}))) + +(defmethod delete-object :project + [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] + (l/trc :hint "marking for deletion" :rel "project" :id (str id) + :deleted-at (dt/format-instant deleted-at)) + + (db/update! conn :project + {:deleted-at deleted-at} + {:id id} + {::db/return-keys false}) + + (doseq [file (db/query conn :file + {:project-id id} + {::db/columns [:id :deleted-at]})] + (delete-object cfg (assoc file + :object :file + :deleted-at deleted-at)))) + +(defmethod delete-object :team + [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] + (l/trc :hint "marking for deletion" :rel "team" :id (str id) + :deleted-at (dt/format-instant deleted-at)) + (db/update! conn :team + {:deleted-at deleted-at} + {:id id} + {::db/return-keys false}) + + (db/update! conn :team-font-variant + {:deleted-at deleted-at} + {:team-id id} + {::db/return-keys false}) + + (binding [*team-deletion* true] + (doseq [project (db/query conn :project + {:team-id id} + {::db/columns [:id :deleted-at]})] + (delete-object cfg (assoc project + :object :project + :deleted-at deleted-at))))) + +(defmethod delete-object :profile + [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] + (l/trc :hint "marking for deletion" :rel "profile" :id (str id) + :deleted-at (dt/format-instant deleted-at)) + + (db/update! conn :profile + {:deleted-at deleted-at} + {:id id} + {::db/return-keys false}) + + (doseq [team (profile/get-owned-teams conn id)] + (delete-object cfg (assoc team + :object :team + :deleted-at deleted-at)))) + +(defmethod delete-object :default + [_cfg props] + (l/wrn :hint "not implementation found" :rel (:object props))) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool])) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [{:keys [props] :as task}] + (db/tx-run! cfg delete-object props))) diff --git a/backend/src/app/tasks/file_gc.clj b/backend/src/app/tasks/file_gc.clj index 29c07f78b5ba952479948f130785889127bb871b..07bc7ffba565963b2892ca6df8fadf9fa0030421 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -21,88 +21,31 @@ [app.config :as cf] [app.db :as db] [app.features.fdata :as feat.fdata] - [app.media :as media] [app.storage :as sto] [app.util.blob :as blob] [app.util.pointer-map :as pmap] [app.util.time :as dt] + [app.worker :as wrk] [clojure.set :as set] [clojure.spec.alpha :as s] [integrant.core :as ig])) -(declare ^:private clean-file!) +(declare ^:private get-file) +(declare ^:private decode-file) +(declare ^:private persist-file!) -(defn- decode-file - [cfg {:keys [id] :as file}] - (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] - (-> file - (update :features db/decode-pgarray #{}) - (update :data blob/decode) - (update :data feat.fdata/process-pointers deref) - (update :data feat.fdata/process-objects (partial into {})) - (update :data assoc :id id) - (fmg/migrate-file)))) - -(defn- update-file! - [{:keys [::db/conn] :as cfg} {:keys [id] :as file}] - (let [file (if (contains? (:features file) "fdata/objects-map") - (feat.fdata/enable-objects-map file) - file) - - file (if (contains? (:features file) "fdata/pointer-map") - (binding [pmap/*tracked* (pmap/create-tracked)] - (let [file (feat.fdata/enable-pointer-map file)] - (feat.fdata/persist-pointers! cfg id) - file)) - file) - - file (-> file - (update :features db/encode-pgarray conn "text") - (update :data blob/encode))] - - (db/update! conn :file - {:has-media-trimmed true - :features (:features file) - :version (:version file) - :data (:data file)} - {:id id}))) - -(defn- process-file! - [cfg file] - (try - (let [file (decode-file cfg file) - file (clean-file! cfg file)] - (cfv/validate-file-schema! file) - (update-file! cfg file)) - (catch Throwable cause - (l/err :hint "error on cleaning file (skiping)" - :file-id (str (:id file)) - :cause cause)))) - -(def ^:private - sql:get-candidates - "SELECT f.id, +(def ^:private sql:get-snapshots + "SELECT f.file_id AS id, f.data, f.revn, f.version, f.features, - f.modified_at - FROM file AS f - WHERE f.has_media_trimmed IS false - AND f.modified_at < now() - ?::interval - ORDER BY f.modified_at DESC - FOR UPDATE - SKIP LOCKED") - -(defn- get-candidates - [{:keys [::db/conn ::min-age ::file-id]}] - (if (uuid? file-id) - (do - (l/warn :hint "explicit file id passed on params" :file-id (str file-id)) - (db/query conn :file {:id file-id})) - - (let [min-age (db/interval min-age)] - (db/cursor conn [sql:get-candidates min-age] {:chunk-size 1})))) + f.data_backend, + f.data_ref_id + FROM file_change AS f + WHERE f.file_id = ? + AND f.label IS NOT NULL + ORDER BY f.created_at ASC") (def ^:private sql:mark-file-media-object-deleted "UPDATE file_media_object @@ -110,21 +53,30 @@ WHERE file_id = ? AND id != ALL(?::uuid[]) RETURNING id") +(def ^:private xf:collect-used-media + (comp (map :data) (mapcat bfc/collect-used-media))) + (defn- clean-file-media! "Performs the garbage collection of file media objects." - [{:keys [::db/conn]} {:keys [id data] :as file}] - (let [used (bfc/collect-used-media data) + [{:keys [::db/conn] :as cfg} {:keys [id] :as file}] + (let [used (into #{} + xf:collect-used-media + (cons file + (->> (db/cursor conn [sql:get-snapshots id]) + (map (partial decode-file cfg))))) ids (db/create-array conn "uuid" used) unused (->> (db/exec! conn [sql:mark-file-media-object-deleted id ids]) (into #{} (map :id)))] + (l/dbg :hint "clean" :rel "file-media-object" :file-id (str id) :total (count unused)) + (doseq [id unused] (l/trc :hint "mark deleted" :rel "file-media-object" :id (str id) :file-id (str id))) - [(count unused) file])) + file)) (def ^:private sql:mark-file-object-thumbnails-deleted "UPDATE file_tagged_object_thumbnail @@ -149,13 +101,15 @@ unused (->> (db/exec! conn [sql:mark-file-object-thumbnails-deleted file-id ids]) (into #{} (map :object-id)))] + (l/dbg :hint "clean" :rel "file-object-thumbnail" :file-id (str file-id) :total (count unused)) + (doseq [object-id unused] (l/trc :hint "mark deleted" :rel "file-tagged-object-thumbnail" :object-id object-id :file-id (str file-id))) - [(count unused) file])) + file)) (def ^:private sql:mark-file-thumbnails-deleted "UPDATE file_thumbnail @@ -168,14 +122,15 @@ (let [unused (->> (db/exec! conn [sql:mark-file-thumbnails-deleted id revn]) (into #{} (map :revn)))] + (l/dbg :hint "clean" :rel "file-thumbnail" :file-id (str id) :total (count unused)) + (doseq [revn unused] (l/trc :hint "mark deleted" :rel "file-thumbnail" :revn revn :file-id (str id))) - [(count unused) file])) - + file)) (def ^:private sql:get-files-for-library "SELECT f.id, f.data, f.modified_at, f.features, f.version @@ -230,58 +185,125 @@ file (update file :data process-fdata unused)] - [(count unused) file])) -(def ^:private sql:get-changes - "SELECT id, data FROM file_change - WHERE file_id = ? AND data IS NOT NULL - ORDER BY created_at ASC") + (l/dbg :hint "clean" :rel "components" :file-id (str file-id) :total (count unused)) + file)) (def ^:private sql:mark-deleted-data-fragments "UPDATE file_data_fragment SET deleted_at = now() WHERE file_id = ? AND id != ALL(?::uuid[]) + AND deleted_at IS NULL RETURNING id") +(def ^:private xf:collect-pointers + (comp (map :data) + (map blob/decode) + (mapcat feat.fdata/get-used-pointer-ids))) + (defn- clean-data-fragments! - [{:keys [::db/conn]} {:keys [id data] :as file}] - (let [used (->> (db/cursor conn [sql:get-changes id]) - (into (feat.fdata/get-used-pointer-ids data) - (comp (map :data) - (map blob/decode) - (mapcat feat.fdata/get-used-pointer-ids)))) + [{:keys [::db/conn]} {:keys [id] :as file}] + (let [used (into #{} xf:collect-pointers [file]) unused (let [ids (db/create-array conn "uuid" used)] (->> (db/exec! conn [sql:mark-deleted-data-fragments id ids]) (into #{} (map :id))))] + (l/dbg :hint "clean" :rel "file-data-fragment" :file-id (str id) :total (count unused)) (doseq [id unused] (l/trc :hint "mark deleted" :rel "file-data-fragment" :id (str id) - :file-id (str id))) + :file-id (str id))))) - [(count unused) file])) +(defn- clean-media! + [cfg file] + (let [file (->> file + (clean-file-media! cfg) + (clean-file-thumbnails! cfg) + (clean-file-object-thumbnails! cfg) + (clean-deleted-components! cfg))] + (cfv/validate-file-schema! file) + file)) -(defn- clean-file! +(def ^:private sql:get-file + "SELECT f.id, + f.data, + f.revn, + f.version, + f.features, + f.modified_at, + f.data_backend, + f.data_ref_id + FROM file AS f + WHERE f.has_media_trimmed IS false + AND f.modified_at < now() - ?::interval + AND f.deleted_at IS NULL + AND f.id = ? + FOR UPDATE + SKIP LOCKED") + +(defn- get-file + [{:keys [::db/conn ::min-age ::file-id]}] + (->> (db/exec! conn [sql:get-file min-age file-id]) + (first))) + +(defn- decode-file [cfg {:keys [id] :as file}] - (let [[n1 file] (clean-file-media! cfg file) - [n2 file] (clean-file-thumbnails! cfg file) - [n3 file] (clean-file-object-thumbnails! cfg file) - [n4 file] (clean-deleted-components! cfg file) - [n5 file] (clean-data-fragments! cfg file)] - - (l/dbg :hint "file clened" - :file-id (str id) - :modified-at (dt/format-instant (:modified-at file)) - :media-objects n1 - :thumbnails n2 - :object-thumbnails n3 - :components n4 - :data-fragments n5) + (binding [pmap/*load-fn* (partial feat.fdata/load-pointer cfg id)] + (-> (feat.fdata/resolve-file-data cfg file) + (update :features db/decode-pgarray #{}) + (update :data blob/decode) + (update :data feat.fdata/process-pointers deref) + (update :data feat.fdata/process-objects (partial into {})) + (update :data assoc :id id) + (fmg/migrate-file)))) - file)) +(defn- persist-file! + [{:keys [::db/conn ::sto/storage] :as cfg} {:keys [id] :as file}] + (let [file (if (contains? (:features file) "fdata/objects-map") + (feat.fdata/enable-objects-map file) + file) + + file (if (contains? (:features file) "fdata/pointer-map") + (binding [pmap/*tracked* (pmap/create-tracked)] + (let [file (feat.fdata/enable-pointer-map file)] + (feat.fdata/persist-pointers! cfg id) + file)) + file) + + file (-> file + (update :features db/encode-pgarray conn "text") + (update :data blob/encode))] + + ;; If file was already offloaded, we touch the underlying storage + ;; object for properly trigger storage-gc-touched task + (when (feat.fdata/offloaded? file) + (some->> (:data-ref-id file) (sto/touch-object! storage))) + + (db/update! conn :file + {:has-media-trimmed true + :features (:features file) + :version (:version file) + :data (:data file) + :data-backend nil + :data-ref-id nil} + {:id id} + {::db/return-keys true}))) + +(defn- process-file! + [cfg] + (if-let [file (get-file cfg)] + (let [file (decode-file cfg file) + file (clean-media! cfg file) + file (persist-file! cfg file)] + (clean-data-fragments! cfg file) + true) + + (do + (l/dbg :hint "skip" :file-id (str (::file-id cfg))) + false))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HANDLER @@ -290,33 +312,29 @@ (defmethod ig/pre-init-spec ::handler [_] (s/keys :req [::db/pool ::sto/storage])) -(defmethod ig/prep-key ::handler - [_ cfg] - (assoc cfg ::min-age cf/deletion-delay)) - (defmethod ig/init-key ::handler [_ cfg] - (fn [{:keys [file-id] :as params}] - (db/tx-run! cfg - (fn [{:keys [::db/conn] :as cfg}] - (let [min-age (dt/duration (or (:min-age params) (::min-age cfg))) - cfg (-> cfg - (update ::sto/storage media/configure-assets-storage conn) - (assoc ::file-id file-id) - (assoc ::min-age min-age)) - - total (reduce (fn [total file] - (process-file! cfg file) - (inc total)) - 0 - (get-candidates cfg))] - - (l/inf :hint "task finished" - :min-age (dt/format-duration min-age) - :processed total) - - ;; Allow optional rollback passed by params - (when (:rollback? params) - (db/rollback! conn)) - - {:processed total}))))) + (fn [{:keys [props] :as task}] + (let [min-age (dt/duration (or (:min-age props) + (cf/get-deletion-delay))) + cfg (-> cfg + (assoc ::db/rollback (:rollback? props)) + (assoc ::file-id (:file-id props)) + (assoc ::min-age (db/interval min-age)))] + + (try + (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (let [cfg (update cfg ::sto/storage sto/configure conn) + processed? (process-file! cfg)] + (when (and processed? (contains? cf/flags :tiered-file-data-storage)) + (wrk/submit! (-> cfg + (assoc ::wrk/task :offload-file-data) + (assoc ::wrk/params props) + (assoc ::wrk/priority 10) + (assoc ::wrk/delay 1000)))) + processed?))) + + (catch Throwable cause + (l/err :hint "error on cleaning file" + :file-id (str (:file-id props)) + :cause cause)))))) diff --git a/backend/src/app/tasks/file_gc_scheduler.clj b/backend/src/app/tasks/file_gc_scheduler.clj new file mode 100644 index 0000000000000000000000000000000000000000..a133b6c412cfb71b4d35cc6ed50a5ad9a0bfb782 --- /dev/null +++ b/backend/src/app/tasks/file_gc_scheduler.clj @@ -0,0 +1,64 @@ +;; 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 app.tasks.file-gc-scheduler + "A maintenance task that is responsible of properly scheduling the + file-gc task for all files that matches the eligibility threshold." + (:require + [app.config :as cf] + [app.db :as db] + [app.util.time :as dt] + [app.worker :as wrk] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(def ^:private + sql:get-candidates + "SELECT f.id, + f.modified_at + FROM file AS f + WHERE f.has_media_trimmed IS false + AND f.modified_at < now() - ?::interval + AND f.deleted_at IS NULL + ORDER BY f.modified_at DESC + FOR UPDATE + SKIP LOCKED") + +(defn- get-candidates + [{:keys [::db/conn ::min-age] :as cfg}] + (let [min-age (db/interval min-age)] + (db/cursor conn [sql:get-candidates min-age] {:chunk-size 10}))) + +(defn- schedule! + [{:keys [::min-age] :as cfg}] + (let [total (reduce (fn [total {:keys [id]}] + (let [params {:file-id id :min-age min-age}] + (wrk/submit! (assoc cfg ::wrk/params params)) + (inc total))) + 0 + (get-candidates cfg))] + + {:processed total})) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool])) + +(defmethod ig/prep-key ::handler + [_ cfg] + (assoc cfg ::min-age (cf/get-deletion-delay))) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [{:keys [props] :as task}] + (let [min-age (dt/duration (or (:min-age props) (::min-age cfg)))] + (-> cfg + (assoc ::db/rollback (:rollback? props)) + (assoc ::min-age min-age) + (assoc ::wrk/task :file-gc) + (assoc ::wrk/priority 10) + (assoc ::wrk/mark-retries 0) + (assoc ::wrk/delay 1000) + (db/tx-run! schedule!))))) diff --git a/backend/src/app/tasks/file_xlog_gc.clj b/backend/src/app/tasks/file_xlog_gc.clj index c88f42a8407616c4b5f7ca5587f39c68f1ed45b4..6bbacd250f724a2b997a21b64d221c274872e0d9 100644 --- a/backend/src/app/tasks/file_xlog_gc.clj +++ b/backend/src/app/tasks/file_xlog_gc.clj @@ -10,35 +10,59 @@ (:require [app.common.logging :as l] [app.db :as db] + [app.features.fdata :as feat.fdata] + [app.storage :as sto] [app.util.time :as dt] [clojure.spec.alpha :as s] [integrant.core :as ig])) (def ^:private sql:delete-files-xlog - "delete from file_change - where created_at < now() - ?::interval - and label is NULL") + "DELETE FROM file_change + WHERE id IN (SELECT id FROM file_change + WHERE label IS NULL + AND created_at < ? + ORDER BY created_at LIMIT ?) + RETURNING id, data_backend, data_ref_id") -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req [::db/pool])) +(def xf:filter-offloded + (comp + (filter feat.fdata/offloaded?) + (keep :data-ref-id))) -(defmethod ig/prep-key ::handler - [_ cfg] - (assoc cfg ::min-age (dt/duration {:hours 72}))) +(defn- delete-in-chunks + [{:keys [::chunk-size ::threshold] :as cfg}] + (let [storage (sto/resolve cfg ::db/reuse-conn true)] + (loop [total 0] + (let [chunk (db/exec! cfg [sql:delete-files-xlog threshold chunk-size]) + length (count chunk)] -(defmethod ig/init-key ::handler - [_ {:keys [::db/pool] :as cfg}] - (fn [params] - (let [min-age (or (:min-age params) (::min-age cfg))] - (db/with-atomic [conn pool] - (let [interval (db/interval min-age) - result (db/exec-one! conn [sql:delete-files-xlog interval]) - result (db/get-update-count result)] + ;; touch all references on offloaded changes entries + (doseq [data-ref-id (sequence xf:filter-offloded chunk)] + (l/trc :hint "touching referenced storage object" + :storage-object-id (str data-ref-id)) + (sto/touch-object! storage data-ref-id)) - (l/info :hint "task finished" :min-age (dt/format-duration min-age) :total result) + (if (pos? length) + (recur (+ total length)) + total))))) - (when (:rollback? params) - (db/rollback! conn)) +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool])) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [{:keys [props] :as task}] + (let [min-age (or (:min-age props) + (dt/duration "72h")) + chunk-size (:chunk-size props 5000) + threshold (dt/minus (dt/now) min-age)] - result))))) + (-> cfg + (assoc ::db/rollback (:rollback props false)) + (assoc ::threshold threshold) + (assoc ::chunk-size chunk-size) + (db/tx-run! (fn [cfg] + (let [total (delete-in-chunks cfg)] + (l/trc :hint "file xlog cleaned" :total total) + total))))))) diff --git a/backend/src/app/tasks/objects_gc.clj b/backend/src/app/tasks/objects_gc.clj index c5e74ce3acb582ff4d728aa259e4a86a10902d2f..67ed8f9aa0c7a7405197f70b57dfe16b6fb59cb5 100644 --- a/backend/src/app/tasks/objects_gc.clj +++ b/backend/src/app/tasks/objects_gc.clj @@ -11,84 +11,29 @@ [app.common.logging :as l] [app.config :as cf] [app.db :as db] - [app.media :as media] [app.storage :as sto] [app.util.time :as dt] [clojure.spec.alpha :as s] [integrant.core :as ig])) -(declare ^:private delete-file-data-fragments!) -(declare ^:private delete-file-media-objects!) -(declare ^:private delete-file-object-thumbnails!) -(declare ^:private delete-file-thumbnails!) -(declare ^:private delete-files!) -(declare ^:private delete-fonts!) -(declare ^:private delete-profiles!) -(declare ^:private delete-projects!) -(declare ^:private delete-teams!) - -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req [::db/pool ::sto/storage])) - -(defmethod ig/prep-key ::handler - [_ cfg] - (assoc cfg ::min-age cf/deletion-delay)) - -(defmethod ig/init-key ::handler - [_ cfg] - (fn [params] - (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] - ;; Disable deletion protection for the current transaction - (db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"]) - (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) - - (let [min-age (dt/duration (or (:min-age params) (::min-age cfg))) - cfg (-> cfg - (assoc ::min-age (db/interval min-age)) - (update ::sto/storage media/configure-assets-storage conn)) - - total (reduce + 0 - [(delete-profiles! cfg) - (delete-teams! cfg) - (delete-fonts! cfg) - (delete-projects! cfg) - (delete-files! cfg) - (delete-file-thumbnails! cfg) - (delete-file-object-thumbnails! cfg) - (delete-file-data-fragments! cfg) - (delete-file-media-objects! cfg)])] - - (l/info :hint "task finished" - :deleted total - :rollback? (boolean (:rollback? params))) - - (when (:rollback? params) - (db/rollback! conn)) - - {:processed total}))))) - (def ^:private sql:get-profiles "SELECT id, photo_id FROM profile WHERE deleted_at IS NOT NULL AND deleted_at < now() - ?::interval ORDER BY deleted_at ASC + LIMIT ? FOR UPDATE SKIP LOCKED") (defn- delete-profiles! - [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-profiles min-age]) + [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-profiles min-age chunk-size] {:chunk-size 1}) (reduce (fn [total {:keys [id photo-id]}] (l/trc :hint "permanently delete" :rel "profile" :id (str id)) ;; Mark as deleted the storage object (some->> photo-id (sto/touch-object! storage)) - ;; And finally, permanently delete the profile. The - ;; relevant objects will be deleted using DELETE - ;; CASCADE database triggers. This may leave orphan - ;; teams, but there is a special task for deleting - ;; orphaned teams. (db/delete! conn :profile {:id id}) (inc total)) @@ -99,13 +44,13 @@ WHERE deleted_at IS NOT NULL AND deleted_at < now() - ?::interval ORDER BY deleted_at ASC + LIMIT ? FOR UPDATE SKIP LOCKED") (defn- delete-teams! - [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] - - (->> (db/cursor conn [sql:get-teams min-age]) + [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-teams min-age chunk-size] {:chunk-size 1}) (reduce (fn [total {:keys [id photo-id deleted-at]}] (l/trc :hint "permanently delete" :rel "team" @@ -118,15 +63,6 @@ ;; And finally, permanently delete the team. (db/delete! conn :team {:id id}) - ;; Mark for deletion in cascade - (db/update! conn :team-font-variant - {:deleted-at deleted-at} - {:team-id id}) - - (db/update! conn :project - {:deleted-at deleted-at} - {:team-id id}) - (inc total)) 0))) @@ -136,12 +72,13 @@ WHERE deleted_at IS NOT NULL AND deleted_at < now() - ?::interval ORDER BY deleted_at ASC + LIMIT ? FOR UPDATE SKIP LOCKED") (defn- delete-fonts! - [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-fonts min-age]) + [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-fonts min-age chunk-size] {:chunk-size 1}) (reduce (fn [total {:keys [id team-id deleted-at] :as font}] (l/trc :hint "permanently delete" :rel "team-font-variant" @@ -167,12 +104,13 @@ WHERE deleted_at IS NOT NULL AND deleted_at < now() - ?::interval ORDER BY deleted_at ASC + LIMIT ? FOR UPDATE SKIP LOCKED") (defn- delete-projects! - [{:keys [::db/conn ::min-age] :as cfg}] - (->> (db/cursor conn [sql:get-projects min-age]) + [{:keys [::db/conn ::min-age ::chunk-size] :as cfg}] + (->> (db/cursor conn [sql:get-projects min-age chunk-size] {:chunk-size 1}) (reduce (fn [total {:keys [id team-id deleted-at]}] (l/trc :hint "permanently delete" :rel "project" @@ -183,66 +121,51 @@ ;; And finally, permanently delete the project. (db/delete! conn :project {:id id}) - ;; Mark files to be deleted - (db/update! conn :file - {:deleted-at deleted-at} - {:project-id id}) - (inc total)) 0))) (def ^:private sql:get-files - "SELECT id, deleted_at, project_id + "SELECT id, deleted_at, project_id, data_backend, data_ref_id FROM file WHERE deleted_at IS NOT NULL AND deleted_at < now() - ?::interval ORDER BY deleted_at ASC + LIMIT ? FOR UPDATE SKIP LOCKED") (defn- delete-files! - [{:keys [::db/conn ::min-age] :as cfg}] - (->> (db/cursor conn [sql:get-files min-age]) - (reduce (fn [total {:keys [id deleted-at project-id]}] + [{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}] + (->> (db/cursor conn [sql:get-files min-age chunk-size] {:chunk-size 1}) + (reduce (fn [total {:keys [id deleted-at project-id] :as file}] (l/trc :hint "permanently delete" :rel "file" :id (str id) :project-id (str project-id) :deleted-at (dt/format-instant deleted-at)) + (when (= "objects-storage" (:data-backend file)) + (sto/touch-object! storage (:data-ref-id file))) + ;; And finally, permanently delete the file. (db/delete! conn :file {:id id}) - ;; Mark file media objects to be deleted - (db/update! conn :file-media-object - {:deleted-at deleted-at} - {:file-id id}) - - ;; Mark thumbnails to be deleted - (db/update! conn :file-thumbnail - {:deleted-at deleted-at} - {:file-id id}) - - (db/update! conn :file-tagged-object-thumbnail - {:deleted-at deleted-at} - {:file-id id}) - (inc total)) 0))) - (def ^:private sql:get-file-thumbnails "SELECT file_id, revn, media_id, deleted_at FROM file_thumbnail WHERE deleted_at IS NOT NULL AND deleted_at < now() - ?::interval ORDER BY deleted_at ASC + LIMIT ? FOR UPDATE SKIP LOCKED") (defn delete-file-thumbnails! - [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-file-thumbnails min-age]) + [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-file-thumbnails min-age chunk-size] {:chunk-size 1}) (reduce (fn [total {:keys [file-id revn media-id deleted-at]}] (l/trc :hint "permanently delete" :rel "file-thumbnail" @@ -265,12 +188,13 @@ WHERE deleted_at IS NOT NULL AND deleted_at < now() - ?::interval ORDER BY deleted_at ASC + LIMIT ? FOR UPDATE SKIP LOCKED") (defn delete-file-object-thumbnails! - [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-file-object-thumbnails min-age]) + [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-file-object-thumbnails min-age chunk-size] {:chunk-size 1}) (reduce (fn [total {:keys [file-id object-id media-id deleted-at]}] (l/trc :hint "permanently delete" :rel "file-tagged-object-thumbnail" @@ -288,24 +212,26 @@ 0))) (def ^:private sql:get-file-data-fragments - "SELECT file_id, id, deleted_at + "SELECT file_id, id, deleted_at, data_ref_id FROM file_data_fragment WHERE deleted_at IS NOT NULL AND deleted_at < now() - ?::interval ORDER BY deleted_at ASC + LIMIT ? FOR UPDATE SKIP LOCKED") (defn- delete-file-data-fragments! - [{:keys [::db/conn ::min-age] :as cfg}] - (->> (db/cursor conn [sql:get-file-data-fragments min-age]) - (reduce (fn [total {:keys [file-id id deleted-at]}] + [{:keys [::db/conn ::sto/storage ::min-age ::chunk-size] :as cfg}] + (->> (db/cursor conn [sql:get-file-data-fragments min-age chunk-size] {:chunk-size 1}) + (reduce (fn [total {:keys [file-id id deleted-at data-ref-id]}] (l/trc :hint "permanently delete" :rel "file-data-fragment" :id (str id) :file-id (str file-id) :deleted-at (dt/format-instant deleted-at)) + (some->> data-ref-id (sto/touch-object! storage)) (db/delete! conn :file-data-fragment {:file-id file-id :id id}) (inc total)) @@ -317,12 +243,13 @@ WHERE deleted_at IS NOT NULL AND deleted_at < now() - ?::interval ORDER BY deleted_at ASC + LIMIT ? FOR UPDATE SKIP LOCKED") (defn- delete-file-media-objects! - [{:keys [::db/conn ::min-age ::sto/storage] :as cfg}] - (->> (db/cursor conn [sql:get-file-media-objects min-age]) + [{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}] + (->> (db/cursor conn [sql:get-file-media-objects min-age chunk-size] {:chunk-size 1}) (reduce (fn [total {:keys [id file-id deleted-at] :as fmo}] (l/trc :hint "permanently delete" :rel "file-media-object" @@ -338,3 +265,51 @@ (inc total)) 0))) + +(def ^:private deletion-proc-vars + [#'delete-profiles! + #'delete-file-media-objects! + #'delete-file-data-fragments! + #'delete-file-object-thumbnails! + #'delete-file-thumbnails! + #'delete-files! + #'delete-projects! + #'delete-fonts! + #'delete-teams!]) + +(defn- execute-proc! + "A generic function that executes the specified proc iterativelly + until 0 results is returned" + [cfg proc-fn] + (loop [total 0] + (let [result (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] + (db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"]) + (proc-fn cfg)))] + (if (pos? result) + (recur (+ total result)) + total)))) + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool ::sto/storage])) + +(defmethod ig/prep-key ::handler + [_ cfg] + (assoc cfg + ::min-age (cf/get-deletion-delay) + ::chunk-size 10)) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [{:keys [props] :as task}] + (let [min-age (dt/duration (or (:min-age props) (::min-age cfg))) + cfg (assoc cfg ::min-age (db/interval min-age))] + + (loop [procs (map deref deletion-proc-vars) + total 0] + (if-let [proc-fn (first procs)] + (let [result (execute-proc! cfg proc-fn)] + (recur (rest procs) + (+ total result))) + (do + (l/inf :hint "task finished" :deleted total) + {:processed total})))))) diff --git a/backend/src/app/tasks/offload_file_data.clj b/backend/src/app/tasks/offload_file_data.clj new file mode 100644 index 0000000000000000000000000000000000000000..cfe50970f2ab24b5a56a75e29b38f580c346627c --- /dev/null +++ b/backend/src/app/tasks/offload_file_data.clj @@ -0,0 +1,124 @@ +;; 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 app.tasks.offload-file-data + "A maintenance task responsible of moving file data from hot + storage (the database row) to a cold storage (fs or s3)." + (:require + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.db :as db] + [app.db.sql :as-alias sql] + [app.storage :as sto] + [clojure.spec.alpha :as s] + [integrant.core :as ig])) + +(defn- offload-file-data! + [{:keys [::db/conn ::sto/storage ::file-id] :as cfg}] + (let [file (db/get conn :file {:id file-id} + {::sql/for-update true})] + (when (nil? (:data file)) + (ex/raise :hint "file already offloaded" + :type :internal + :code :file-already-offloaded + :file-id file-id)) + + (let [data (sto/content (:data file)) + sobj (sto/put-object! storage + {::sto/content data + ::sto/touch true + :bucket "file-data" + :content-type "application/octet-stream" + :file-id file-id})] + + (l/trc :hint "offload file data" + :file-id (str file-id) + :storage-id (str (:id sobj))) + + (db/update! conn :file + {:data-backend "objects-storage" + :data-ref-id (:id sobj) + :data nil} + {:id file-id} + {::db/return-keys false})))) + +(defn- offload-file-data-fragments! + [{:keys [::db/conn ::sto/storage ::file-id] :as cfg}] + (doseq [fragment (db/query conn :file-data-fragment + {:file-id file-id + :deleted-at nil + :data-backend nil} + {::db/for-update true})] + (let [data (sto/content (:data fragment)) + sobj (sto/put-object! storage + {::sto/content data + ::sto/touch true + :bucket "file-data-fragment" + :content-type "application/octet-stream" + :file-id file-id + :file-fragment-id (:id fragment)})] + + (l/trc :hint "offload file data fragment" + :file-id (str file-id) + :file-fragment-id (str (:id fragment)) + :storage-id (str (:id sobj))) + + (db/update! conn :file-data-fragment + {:data-backend "objects-storage" + :data-ref-id (:id sobj) + :data nil} + {:id (:id fragment)} + {::db/return-keys false})))) + +(def sql:get-snapshots + "SELECT fc.* + FROM file_change AS fc + WHERE fc.file_id = ? + AND fc.label IS NOT NULL + AND fc.data IS NOT NULL + AND fc.data_backend IS NULL") + +(defn- offload-file-snapshots! + [{:keys [::db/conn ::sto/storage ::file-id] :as cfg}] + (doseq [snapshot (db/exec! conn [sql:get-snapshots file-id])] + (let [data (sto/content (:data snapshot)) + sobj (sto/put-object! storage + {::sto/content data + ::sto/touch true + :bucket "file-change" + :content-type "application/octet-stream" + :file-id file-id + :file-change-id (:id snapshot)})] + + (l/trc :hint "offload file change" + :file-id (str file-id) + :file-change-id (str (:id snapshot)) + :storage-id (str (:id sobj))) + + (db/update! conn :file-change + {:data-backend "objects-storage" + :data-ref-id (:id sobj) + :data nil} + {:id (:id snapshot)} + {::db/return-keys false})))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; HANDLER +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::db/pool ::sto/storage])) + +(defmethod ig/init-key ::handler + [_ cfg] + (fn [{:keys [props] :as task}] + (-> cfg + (assoc ::db/rollback (:rollback? props)) + (assoc ::file-id (:file-id props)) + (db/tx-run! (fn [cfg] + (offload-file-data! cfg) + (offload-file-data-fragments! cfg) + (offload-file-snapshots! cfg)))))) diff --git a/backend/src/app/tasks/orphan_teams_gc.clj b/backend/src/app/tasks/orphan_teams_gc.clj deleted file mode 100644 index c04123a8314949b8727e46b0c5cf123943b57942..0000000000000000000000000000000000000000 --- a/backend/src/app/tasks/orphan_teams_gc.clj +++ /dev/null @@ -1,59 +0,0 @@ -;; 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 app.tasks.orphan-teams-gc - "A maintenance task that performs orphan teams GC." - (:require - [app.common.logging :as l] - [app.db :as db] - [app.util.time :as dt] - [clojure.spec.alpha :as s] - [integrant.core :as ig])) - -(declare ^:private delete-orphan-teams!) - -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req [::db/pool])) - -(defmethod ig/init-key ::handler - [_ cfg] - (fn [params] - (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] - (l/inf :hint "gc started" :rollback? (boolean (:rollback? params))) - (let [total (delete-orphan-teams! cfg)] - (l/inf :hint "task finished" - :teams total - :rollback? (boolean (:rollback? params))) - - (when (:rollback? params) - (db/rollback! conn)) - - {:processed total}))))) - -(def ^:private sql:get-orphan-teams - "SELECT t.id - FROM team AS t - LEFT JOIN team_profile_rel AS tpr - ON (t.id = tpr.team_id) - WHERE tpr.profile_id IS NULL - AND t.deleted_at IS NULL - ORDER BY t.created_at ASC - FOR UPDATE OF t - SKIP LOCKED") - -(defn- delete-orphan-teams! - "Find all orphan teams (with no members) and mark them for - deletion (soft delete)." - [{:keys [::db/conn] :as cfg}] - (->> (db/cursor conn sql:get-orphan-teams) - (map :id) - (reduce (fn [total team-id] - (l/trc :hint "mark orphan team for deletion" :id (str team-id)) - (db/update! conn :team - {:deleted-at (dt/now)} - {:id team-id}) - (inc total)) - 0))) diff --git a/backend/src/app/tasks/tasks_gc.clj b/backend/src/app/tasks/tasks_gc.clj index 77f1f92fa8889ff235040ef6e6708a193eb73a1f..0e93ea0d0a5684f98f411fa81bfb8ffd7bbff280 100644 --- a/backend/src/app/tasks/tasks_gc.clj +++ b/backend/src/app/tasks/tasks_gc.clj @@ -23,12 +23,12 @@ (defmethod ig/prep-key ::handler [_ cfg] - (assoc cfg ::min-age cf/deletion-delay)) + (assoc cfg ::min-age (cf/get-deletion-delay))) (defmethod ig/init-key ::handler [_ {:keys [::db/pool ::min-age] :as cfg}] - (fn [params] - (let [min-age (or (:min-age params) min-age)] + (fn [{:keys [props] :as task}] + (let [min-age (or (:min-age props) min-age)] (db/with-atomic [conn pool] (let [interval (db/interval min-age) result (db/exec-one! conn [sql:delete-completed-tasks interval]) @@ -36,7 +36,7 @@ (l/debug :hint "task finished" :total result) - (when (:rollback? params) + (when (:rollback? props) (db/rollback! conn)) result))))) diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj index 70fa8c35b683d71219af70f07fdc8591dfd6b9e8..204d6be0c1d3ec76669d7a9ff275366d2f983481 100644 --- a/backend/src/app/tasks/telemetry.clj +++ b/backend/src/app/tasks/telemetry.clj @@ -15,74 +15,23 @@ [app.db :as db] [app.http.client :as http] [app.main :as-alias main] + [app.setup :as-alias setup] [app.util.json :as json] [clojure.spec.alpha :as s] [integrant.core :as ig] [promesa.exec :as px])) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; TASK ENTRY POINT -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare get-stats) -(declare send!) -(declare get-subscriptions-newsletter-updates) -(declare get-subscriptions-newsletter-news) - -(defmethod ig/pre-init-spec ::handler [_] - (s/keys :req [::http/client - ::db/pool - ::main/props])) - -(defmethod ig/init-key ::handler - [_ {:keys [::db/pool ::main/props] :as cfg}] - (fn [{:keys [send? enabled?] :or {send? true enabled? false}}] - (let [subs {:newsletter-updates (get-subscriptions-newsletter-updates pool) - :newsletter-news (get-subscriptions-newsletter-news pool)} - enabled? (or enabled? - (contains? cf/flags :telemetry) - (cf/get :telemetry-enabled)) - - data {:subscriptions subs - :version (:full cf/version) - :instance-id (:instance-id props)}] - (cond - ;; If we have telemetry enabled, then proceed the normal - ;; operation. - enabled? - (let [data (merge data (get-stats pool))] - (when send? - (px/sleep (rand-int 10000)) - (send! cfg data)) - data) - - ;; If we have telemetry disabled, but there are users that are - ;; explicitly checked the newsletter subscription on the - ;; onboarding dialog or the profile section, then proceed to - ;; send a limited telemetry data, that consists in the list of - ;; subscribed emails and the running penpot version. - (seq subs) - (do - (when send? - (px/sleep (rand-int 10000)) - (send! cfg data)) - data) - - :else - data)))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; IMPL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn- send! [cfg data] - (let [response (http/req! cfg - {:method :post - :uri (cf/get :telemetry-uri) - :headers {"content-type" "application/json"} - :body (json/encode-str data)} - {:sync? true})] + (let [request {:method :post + :uri (cf/get :telemetry-uri) + :headers {"content-type" "application/json"} + :body (json/encode-str data)} + response (http/req! cfg request)] (when (> (:status response) 206) (ex/raise :type :internal :code :invalid-response @@ -91,94 +40,100 @@ (defn- get-subscriptions-newsletter-updates [conn] - (let [sql "select email from profile where props->>'~:newsletter-updates' = 'true'"] + (let [sql "SELECT email FROM profile where props->>'~:newsletter-updates' = 'true'"] (->> (db/exec! conn [sql]) (mapv :email)))) (defn- get-subscriptions-newsletter-news [conn] - (let [sql "select email from profile where props->>'~:newsletter-news' = 'true'"] + (let [sql "SELECT email FROM profile where props->>'~:newsletter-news' = 'true'"] (->> (db/exec! conn [sql]) (mapv :email)))) -(defn- retrieve-num-teams +(defn- get-num-teams [conn] - (-> (db/exec-one! conn ["select count(*) as count from team;"]) :count)) + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM team"]) :count)) -(defn- retrieve-num-projects +(defn- get-num-projects [conn] - (-> (db/exec-one! conn ["select count(*) as count from project;"]) :count)) + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM project"]) :count)) -(defn- retrieve-num-files +(defn- get-num-files [conn] - (-> (db/exec-one! conn ["select count(*) as count from file;"]) :count)) + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM file"]) :count)) -(defn- retrieve-num-file-changes +(def ^:private sql:num-file-changes + "SELECT count(*) AS count + FROM file_change + WHERE created_at < date_trunc('day', now()) + '24 hours'::interval + AND created_at > date_trunc('day', now())") + +(defn- get-num-file-changes [conn] - (let [sql (str "select count(*) as count " - " from file_change " - " where date_trunc('day', created_at) = date_trunc('day', now())")] - (-> (db/exec-one! conn [sql]) :count))) + (-> (db/exec-one! conn [sql:num-file-changes]) :count)) + +(def ^:private sql:num-touched-files + "SELECT count(distinct file_id) AS count + FROM file_change + WHERE created_at < date_trunc('day', now()) + '24 hours'::interval + AND created_at > date_trunc('day', now())") -(defn- retrieve-num-touched-files +(defn- get-num-touched-files [conn] - (let [sql (str "select count(distinct file_id) as count " - " from file_change " - " where date_trunc('day', created_at) = date_trunc('day', now())")] - (-> (db/exec-one! conn [sql]) :count))) + (-> (db/exec-one! conn [sql:num-touched-files]) :count)) -(defn- retrieve-num-users +(defn- get-num-users [conn] - (-> (db/exec-one! conn ["select count(*) as count from profile;"]) :count)) + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM profile"]) :count)) -(defn- retrieve-num-fonts +(defn- get-num-fonts [conn] - (-> (db/exec-one! conn ["select count(*) as count from team_font_variant;"]) :count)) + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM team_font_variant"]) :count)) -(defn- retrieve-num-comments +(defn- get-num-comments [conn] - (-> (db/exec-one! conn ["select count(*) as count from comment;"]) :count)) + (-> (db/exec-one! conn ["SELECT count(*) AS count FROM comment"]) :count)) (def sql:team-averages - "with projects_by_team as ( - select t.id, count(p.id) as num_projects - from team as t - left join project as p on (p.team_id = t.id) - group by 1 - ), files_by_project as ( - select p.id, count(f.id) as num_files - from project as p - left join file as f on (f.project_id = p.id) - group by 1 - ), comment_threads_by_file as ( - select f.id, count(ct.id) as num_comment_threads - from file as f - left join comment_thread as ct on (ct.file_id = f.id) - group by 1 - ), users_by_team as ( - select t.id, count(tp.profile_id) as num_users - from team as t - left join team_profile_rel as tp on(tp.team_id = t.id) - group by 1 + "with projects_by_team AS ( + SELECT t.id, count(p.id) AS num_projects + FROM team AS t + LEFT JOIN project AS p ON (p.team_id = t.id) + GROUP BY 1 + ), files_by_project AS ( + SELECT p.id, count(f.id) AS num_files + FROM project AS p + LEFT JOIN file AS f ON (f.project_id = p.id) + GROUP BY 1 + ), comment_threads_by_file AS ( + SELECT f.id, count(ct.id) AS num_comment_threads + FROM file AS f + LEFT JOIN comment_thread AS ct ON (ct.file_id = f.id) + GROUP BY 1 + ), users_by_team AS ( + SELECT t.id, count(tp.profile_id) AS num_users + FROM team AS t + LEFT JOIN team_profile_rel AS tp ON(tp.team_id = t.id) + GROUP BY 1 ) - select (select avg(num_projects)::integer from projects_by_team) as avg_projects_on_team, - (select max(num_projects)::integer from projects_by_team) as max_projects_on_team, - (select avg(num_files)::integer from files_by_project) as avg_files_on_project, - (select max(num_files)::integer from files_by_project) as max_files_on_project, - (select avg(num_comment_threads)::integer from comment_threads_by_file) as avg_comment_threads_on_file, - (select max(num_comment_threads)::integer from comment_threads_by_file) as max_comment_threads_on_file, - (select avg(num_users)::integer from users_by_team) as avg_users_on_team, - (select max(num_users)::integer from users_by_team) as max_users_on_team;") - -(defn- retrieve-team-averages + SELECT (SELECT avg(num_projects)::integer FROM projects_by_team) AS avg_projects_on_team, + (SELECT max(num_projects)::integer FROM projects_by_team) AS max_projects_on_team, + (SELECT avg(num_files)::integer FROM files_by_project) AS avg_files_on_project, + (SELECT max(num_files)::integer FROM files_by_project) AS max_files_on_project, + (SELECT avg(num_comment_threads)::integer FROM comment_threads_by_file) AS avg_comment_threads_on_file, + (SELECT max(num_comment_threads)::integer FROM comment_threads_by_file) AS max_comment_threads_on_file, + (SELECT avg(num_users)::integer FROM users_by_team) AS avg_users_on_team, + (SELECT max(num_users)::integer FROM users_by_team) AS max_users_on_team") + +(defn- get-team-averages [conn] (->> [sql:team-averages] (db/exec-one! conn))) -(defn- retrieve-enabled-auth-providers +(defn- get-enabled-auth-providers [conn] - (let [sql (str "select auth_backend as backend, count(*) as total " - " from profile group by 1") + (let [sql (str "SELECT auth_backend AS backend, count(*) AS total " + " FROM profile GROUP BY 1") rows (db/exec! conn [sql])] (->> rows (map (fn [{:keys [backend total]}] @@ -187,7 +142,7 @@ total]))) (into {})))) -(defn- retrieve-jvm-stats +(defn- get-jvm-stats [] (let [^Runtime runtime (Runtime/getRuntime)] {:jvm-heap-current (.totalMemory runtime) @@ -198,24 +153,104 @@ :os-version (System/getProperty "os.version") :user-tz (System/getProperty "user.timezone")})) -(defn get-stats +(def ^:private sql:get-counters + "SELECT name, count(*) AS count + FROM audit_log + WHERE source = 'backend' + AND tracked_at >= date_trunc('day', now()) + GROUP BY 1 + ORDER BY 2 DESC") + +(defn- get-action-counters + [conn] + (let [counters (->> (db/exec! conn [sql:get-counters]) + (d/index-by (comp keyword :name) :count)) + total (reduce + 0 (vals counters))] + {:total-accomulated-events total + :event-counters counters})) + +(def ^:private sql:clean-counters + "DELETE FROM audit_log + WHERE ip_addr = '0.0.0.0'::inet -- we know this is from telemetry + AND tracked_at < (date_trunc('day', now()) - '1 day'::interval)") + +(defn- clean-counters-data! + [conn] + (when-not (contains? cf/flags :audit-log) + (db/exec-one! conn [sql:clean-counters]))) + +(defn- get-stats [conn] (let [referer (if (cf/get :telemetry-with-taiga) "taiga" (cf/get :telemetry-referer))] - (-> {:referer referer - :public-uri (cf/get :public-uri) - :total-teams (retrieve-num-teams conn) - :total-projects (retrieve-num-projects conn) - :total-files (retrieve-num-files conn) - :total-users (retrieve-num-users conn) - :total-fonts (retrieve-num-fonts conn) - :total-comments (retrieve-num-comments conn) - :total-file-changes (retrieve-num-file-changes conn) - :total-touched-files (retrieve-num-touched-files conn)} - (d/merge - (retrieve-team-averages conn) - (retrieve-jvm-stats) - (retrieve-enabled-auth-providers conn)) + (-> {:referer referer + :public-uri (cf/get :public-uri) + :total-teams (get-num-teams conn) + :total-projects (get-num-projects conn) + :total-files (get-num-files conn) + :total-users (get-num-users conn) + :total-fonts (get-num-fonts conn) + :total-comments (get-num-comments conn) + :total-file-changes (get-num-file-changes conn) + :total-touched-files (get-num-touched-files conn)} + (merge + (get-team-averages conn) + (get-jvm-stats) + (get-enabled-auth-providers conn) + (get-action-counters conn)) (d/without-nils)))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; TASK ENTRY POINT +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defmethod ig/pre-init-spec ::handler [_] + (s/keys :req [::http/client + ::db/pool + ::setup/props])) + +(defmethod ig/init-key ::handler + [_ {:keys [::db/pool ::setup/props] :as cfg}] + (fn [task] + (let [params (:props task) + send? (get params :send? true) + enabled? (or (get params :enabled? false) + (contains? cf/flags :telemetry) + (cf/get :telemetry-enabled)) + + subs {:newsletter-updates (get-subscriptions-newsletter-updates pool) + :newsletter-news (get-subscriptions-newsletter-news pool)} + + data {:subscriptions subs + :version (:full cf/version) + :instance-id (:instance-id props)}] + + (when enabled? + (clean-counters-data! pool)) + + (cond + ;; If we have telemetry enabled, then proceed the normal + ;; operation. + enabled? + (let [data (merge data (get-stats pool))] + (when send? + (px/sleep (rand-int 10000)) + (send! cfg data)) + data) + + ;; If we have telemetry disabled, but there are users that are + ;; explicitly checked the newsletter subscription on the + ;; onboarding dialog or the profile section, then proceed to + ;; send a limited telemetry data, that consists in the list of + ;; subscribed emails and the running penpot version. + (or (seq (:newsletter-updates subs)) + (seq (:newsletter-news subs))) + (do + (when send? + (px/sleep (rand-int 10000)) + (send! cfg data)) + data) + + :else + data)))) diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj index 30ca32b3b1f7124774384a34d980bcf21e2b8a70..60b0d50b22dcf5501f9a44d1d37435b431c1f7e5 100644 --- a/backend/src/app/tokens.clj +++ b/backend/src/app/tokens.clj @@ -8,18 +8,19 @@ "Tokens generation API." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.exceptions :as ex] - [app.common.spec :as us] [app.common.transit :as t] [app.util.time :as dt] - [buddy.sign.jwe :as jwe] - [clojure.spec.alpha :as s])) - -(s/def ::tokens-key bytes?) + [buddy.sign.jwe :as jwe])) (defn generate [{:keys [tokens-key]} claims] - (us/assert! ::tokens-key tokens-key) + + (dm/assert! + "expexted token-key to be bytes instance" + (bytes? tokens-key)) + (let [payload (-> claims (assoc :iat (dt/now)) (d/without-nils) @@ -39,15 +40,13 @@ (ex/raise :type :validation :code :invalid-token :reason :token-expired - :params params - :claims claims)) + :params params)) (when (and (contains? params :iss) (not= (:iss claims) (:iss params))) (ex/raise :type :validation :code :invalid-token :reason :invalid-issuer - :claims claims :params params)) claims)) diff --git a/backend/src/app/util/inet.clj b/backend/src/app/util/inet.clj new file mode 100644 index 0000000000000000000000000000000000000000..9e3fca606528ba726de744413f5af2c2802af83a --- /dev/null +++ b/backend/src/app/util/inet.clj @@ -0,0 +1,37 @@ +;; 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 app.util.inet + "INET addr parsing and validation helpers" + (:require + [cuerdas.core :as str] + [ring.request :as rreq]) + (:import + com.google.common.net.InetAddresses + java.net.InetAddress)) + +(defn valid? + [s] + (InetAddresses/isInetAddress s)) + +(defn normalize + [s] + (try + (let [addr (InetAddresses/forString s)] + (.getHostAddress ^InetAddress addr)) + (catch Throwable _cause + nil))) + +(defn parse-request + [request] + (or (some-> (rreq/get-header request "x-real-ip") + (normalize)) + (some-> (rreq/get-header request "x-forwarded-for") + (str/split #"\s*,\s*") + (first) + (normalize)) + (some-> (rreq/remote-addr request) + (normalize)))) diff --git a/backend/src/app/util/objects_map.clj b/backend/src/app/util/objects_map.clj index 19a7bdea6381b0ea6a486cdf9282d051a2c74d81..c7e4f42eb409735003a943d772fb78a6a70ea284 100644 --- a/backend/src/app/util/objects_map.clj +++ b/backend/src/app/util/objects_map.clj @@ -19,7 +19,8 @@ [app.common.fressian :as fres] [app.common.transit :as t] [app.common.uuid :as uuid] - [clojure.core :as c]) + [clojure.core :as c] + [clojure.data.json :as json]) (:import clojure.lang.Counted clojure.lang.IHashEq @@ -83,6 +84,10 @@ ^:unsynchronized-mutable loaded? ^:unsynchronized-mutable modified?] + json/JSONWriter + (-write [this writter options] + (json/-write (into {} this) writter options)) + IHashEq (hasheq [this] (when-not hash diff --git a/backend/src/app/util/overrides.clj b/backend/src/app/util/overrides.clj new file mode 100644 index 0000000000000000000000000000000000000000..71b2c0c23f22bef7f001fae15db89c26aa348a21 --- /dev/null +++ b/backend/src/app/util/overrides.clj @@ -0,0 +1,39 @@ +;; 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 app.util.overrides + "A utility ns for declare default overrides over clojure runtime" + (:require + [app.common.schema :as sm] + [app.common.schema.generators :as sg] + [app.common.schema.openapi :as-alias oapi] + [clojure.pprint :as pprint] + [datoteka.fs :as fs])) + +(prefer-method print-method + clojure.lang.IRecord + clojure.lang.IDeref) + +(prefer-method print-method + clojure.lang.IPersistentMap + clojure.lang.IDeref) + +(prefer-method pprint/simple-dispatch + clojure.lang.IPersistentMap + clojure.lang.IDeref) + +(sm/register! ::fs/path + {:type ::fs/path + :pred fs/path? + :type-properties + {:title "path" + :description "filesystem path" + :error/message "expected a valid fs path instance" + :error/code "errors.invalid-path" + :gen/gen (sg/generator :string) + :decode/string fs/path + ::oapi/type "string" + ::oapi/format "unix-path"}}) diff --git a/backend/src/app/util/pointer_map.clj b/backend/src/app/util/pointer_map.clj index f5933ecd6ab0837db999a97d03e3dc23239f3954..ba84d3d4be111384650b223fc1ed42a0711edefa 100644 --- a/backend/src/app/util/pointer_map.clj +++ b/backend/src/app/util/pointer_map.clj @@ -37,11 +37,11 @@ (:require [app.common.fressian :as fres] - [app.common.logging :as l] [app.common.transit :as t] [app.common.uuid :as uuid] [app.util.time :as dt] - [clojure.core :as c]) + [clojure.core :as c] + [clojure.data.json :as json]) (:import clojure.lang.Counted clojure.lang.IDeref @@ -76,10 +76,16 @@ ^:unsynchronized-mutable modified? ^:unsynchronized-mutable loaded?] + json/JSONWriter + (-write [this writter options] + (json/-write {:type "pointer" + :id (get-id this) + :meta (meta this)} + writter + options)) + IPointerMap (load! [_] - (l/trace :hint "pointer-map:load" :id (str id)) - (when-not *load-fn* (throw (UnsupportedOperationException. "load is not supported when *load-fn* is not bind"))) diff --git a/backend/src/app/util/time.clj b/backend/src/app/util/time.clj index 778596624535907db19e53ee24093d13fff58eb6..c1526bfb4fe0cb23cba02d5918b1823d2456bbb6 100644 --- a/backend/src/app/util/time.clj +++ b/backend/src/app/util/time.clj @@ -141,21 +141,22 @@ ;; --- INSTANT +(defn instant? + [v] + (instance? Instant v)) + (defn instant ([s] - (if (int? s) - (Instant/ofEpochMilli s) - (Instant/parse s))) + (cond + (instant? s) s + (int? s) (Instant/ofEpochMilli s) + :else (Instant/parse s))) ([s fmt] (case fmt :rfc1123 (Instant/from (.parse DateTimeFormatter/RFC_1123_DATE_TIME ^String s)) :iso (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s)) :iso8601 (Instant/from (.parse DateTimeFormatter/ISO_INSTANT ^String s))))) -(defn instant? - [v] - (instance? Instant v)) - (defn is-after? [da db] (.isAfter ^Instant da ^Instant db)) @@ -368,24 +369,30 @@ (let [p1 (System/nanoTime)] #(duration {:nanos (- (System/nanoTime) p1)}))) -(sm/def! ::instant +(sm/register! ::instant {:type ::instant :pred instant? :type-properties {:error/message "should be an instant" :title "instant" - ::sm/decode instant + :decode/string instant + :encode/string format-instant + :decode/json instant + :encode/json format-instant :gen/gen (tgen/fmap (fn [i] (in-past i)) tgen/pos-int) ::oapi/type "string" ::oapi/format "iso"}}) -(sm/def! ::duration +(sm/register! ::duration {:type :durations :pred duration? :type-properties {:error/message "should be a duration" :gen/gen (tgen/fmap duration tgen/pos-int) :title "duration" - ::sm/decode duration + :decode/string duration + :encode/string format-duration + :decode/json duration + :encode/json format-duration ::oapi/type "string" ::oapi/format "duration"}}) diff --git a/backend/src/app/util/websocket.clj b/backend/src/app/util/websocket.clj index 70d8eb406220ef5f718b9914667e4e4f2a3eae14..b468c0e2869f5d5dbe10f4d44adb98ba343f8d69 100644 --- a/backend/src/app/util/websocket.clj +++ b/backend/src/app/util/websocket.clj @@ -11,7 +11,7 @@ [app.common.logging :as l] [app.common.transit :as t] [app.common.uuid :as uuid] - [app.loggers.audit :refer [parse-client-ip]] + [app.util.inet :as inet] [app.util.time :as dt] [promesa.exec :as px] [promesa.exec.csp :as sp] @@ -84,7 +84,7 @@ output-ch (sp/chan :buf output-buff-size) hbeat-ch (sp/chan :buf (sp/sliding-buffer 6)) close-ch (sp/chan) - ip-addr (parse-client-ip request) + ip-addr (inet/parse-request request) uagent (rreq/get-header request "user-agent") id (uuid/next) state (atom {}) diff --git a/backend/src/app/worker.clj b/backend/src/app/worker.clj index 3a6bfe897cdd270c1106047ac9e240ed0d1fae4d..d5a2a8551a34daeeb5deafa834ee5b45a377c2c6 100644 --- a/backend/src/app/worker.clj +++ b/backend/src/app/worker.clj @@ -9,68 +9,25 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.exceptions :as ex] [app.common.logging :as l] [app.common.spec :as us] - [app.common.transit :as t] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] [app.metrics :as mtx] - [app.redis :as rds] [app.util.time :as dt] [clojure.spec.alpha :as s] [cuerdas.core :as str] - [integrant.core :as ig] - [promesa.core :as p] - [promesa.exec :as px]) - (:import - java.util.concurrent.Executor - java.util.concurrent.Future - java.util.concurrent.ThreadPoolExecutor)) + [integrant.core :as ig])) (set! *warn-on-reflection* true) -(s/def ::executor #(instance? Executor %)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Executor -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defmethod ig/pre-init-spec ::executor [_] - (s/keys :req [])) - -(defmethod ig/init-key ::executor - [_ _] - (let [factory (px/thread-factory :prefix "penpot/default/") - executor (px/cached-executor :factory factory :keepalive 60000)] - (l/inf :hint "starting executor") - (reify - java.lang.AutoCloseable - (close [_] - (l/inf :hint "stoping executor") - (px/shutdown! executor)) - - clojure.lang.IDeref - (deref [_] - {:active (.getPoolSize ^ThreadPoolExecutor executor) - :running (.getActiveCount ^ThreadPoolExecutor executor) - :completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)}) - - Executor - (execute [_ runnable] - (.execute ^Executor executor ^Runnable runnable))))) - -(defmethod ig/halt-key! ::executor - [_ instance] - (.close ^java.lang.AutoCloseable instance)) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; TASKS REGISTRY ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- wrap-task-handler - [metrics tname f] +(defn- wrap-with-metrics + [f metrics tname] (let [labels (into-array String [tname])] (fn [params] (let [tp (dt/tpoint)] @@ -83,6 +40,7 @@ :labels labels}))))))) (s/def ::registry (s/map-of ::us/string fn?)) +(s/def ::tasks (s/map-of keyword? fn?)) (defmethod ig/pre-init-spec ::registry [_] (s/keys :req [::mtx/metrics ::tasks])) @@ -90,552 +48,17 @@ (defmethod ig/init-key ::registry [_ {:keys [::mtx/metrics ::tasks]}] (l/inf :hint "registry initialized" :tasks (count tasks)) - (reduce-kv (fn [registry k v] + (reduce-kv (fn [registry k f] (let [tname (name k)] (l/trc :hint "register task" :name tname) - (assoc registry tname (wrap-task-handler metrics tname v)))) + (assoc registry tname (wrap-with-metrics f metrics tname)))) {} tasks)) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; EXECUTOR MONITOR -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(s/def ::name ::us/keyword) - -(defmethod ig/pre-init-spec ::monitor [_] - (s/keys :req [::name ::executor ::mtx/metrics])) - -(defmethod ig/prep-key ::monitor - [_ cfg] - (merge {::interval (dt/duration "2s")} - (d/without-nils cfg))) - -(defmethod ig/init-key ::monitor - [_ {:keys [::executor ::mtx/metrics ::interval ::name]}] - (letfn [(monitor! [executor prev-completed] - (let [labels (into-array String [(d/name name)]) - stats (deref executor) - - completed (:completed stats) - completed-inc (- completed prev-completed) - completed-inc (if (neg? completed-inc) 0 completed-inc)] - - (mtx/run! metrics - :id :executor-active-threads - :labels labels - :val (:active stats)) - - (mtx/run! metrics - :id :executor-running-threads - :labels labels - :val (:running stats)) - - (mtx/run! metrics - :id :executors-completed-tasks - :labels labels - :inc completed-inc) - - completed-inc))] - - (px/thread - {:name "penpot/executors-monitor" :virtual true} - (l/inf :hint "monitor: started" :name name) - (try - (loop [completed 0] - (px/sleep interval) - (recur (long (monitor! executor completed)))) - (catch InterruptedException _cause - (l/trc :hint "monitor: interrupted" :name name)) - (catch Throwable cause - (l/err :hint "monitor: unexpected error" :name name :cause cause)) - (finally - (l/inf :hint "monitor: terminated" :name name)))))) - -(defmethod ig/halt-key! ::monitor - [_ thread] - (px/interrupt! thread)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; SCHEDULER -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- decode-task-row - [{:keys [props] :as row}] - (cond-> row - (db/pgobject? props) - (assoc :props (db/decode-transit-pgobject props)))) - -(s/def ::wait-duration ::dt/duration) - -(defmethod ig/pre-init-spec ::dispatcher [_] - (s/keys :req [::mtx/metrics - ::db/pool - ::rds/redis] - :opt [::wait-duration - ::batch-size])) - -(defmethod ig/prep-key ::dispatcher - [_ cfg] - (merge {::batch-size 100 - ::wait-duration (dt/duration "5s")} - (d/without-nils cfg))) - -(def ^:private sql:select-next-tasks - "select id, queue from task as t - where t.scheduled_at <= now() - and (t.status = 'new' or t.status = 'retry') - and queue ~~* ?::text - order by t.priority desc, t.scheduled_at - limit ? - for update skip locked") - -(defmethod ig/init-key ::dispatcher - [_ {:keys [::db/pool ::rds/redis ::batch-size] :as cfg}] - (letfn [(get-tasks [conn] - (let [prefix (str (cf/get :tenant) ":%")] - (seq (db/exec! conn [sql:select-next-tasks prefix batch-size])))) - - (push-tasks! [conn rconn [queue tasks]] - (let [ids (mapv :id tasks) - key (str/ffmt "taskq:%" queue) - res (rds/rpush! rconn key (mapv t/encode ids)) - sql [(str "update task set status = 'scheduled'" - " where id = ANY(?)") - (db/create-array conn "uuid" ids)]] - - (db/exec-one! conn sql) - (l/trc :hist "dispatcher: queue tasks" - :queue queue - :tasks (count ids) - :queued res))) - - (run-batch! [rconn] - (try - (db/with-atomic [conn pool] - (if-let [tasks (get-tasks conn)] - (->> (group-by :queue tasks) - (run! (partial push-tasks! conn rconn))) - (px/sleep (::wait-duration cfg)))) - (catch InterruptedException cause - (throw cause)) - (catch Exception cause - (cond - (rds/exception? cause) - (do - (l/wrn :hint "dispatcher: redis exception (will retry in an instant)" :cause cause) - (px/sleep (::rds/timeout rconn))) - - (db/sql-exception? cause) - (do - (l/wrn :hint "dispatcher: database exception (will retry in an instant)" :cause cause) - (px/sleep (::rds/timeout rconn))) - - :else - (do - (l/err :hint "dispatcher: unhandled exception (will retry in an instant)" :cause cause) - (px/sleep (::rds/timeout rconn))))))) - - (dispatcher [] - (l/inf :hint "dispatcher: started") - (try - (dm/with-open [rconn (rds/connect redis)] - (loop [] - (run-batch! rconn) - (recur))) - (catch InterruptedException _ - (l/trc :hint "dispatcher: interrupted")) - (catch Throwable cause - (l/err :hint "dispatcher: unexpected exception" :cause cause)) - (finally - (l/inf :hint "dispatcher: terminated"))))] - - (if (db/read-only? pool) - (l/wrn :hint "dispatcher: not started (db is read-only)") - (px/fn->thread dispatcher :name "penpot/worker/dispatcher" :virtual true)))) - -(defmethod ig/halt-key! ::dispatcher - [_ thread] - (some-> thread px/interrupt!)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; WORKER -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare ^:private run-worker-loop!) -(declare ^:private start-worker!) -(declare ^:private get-error-context) - -(defmethod ig/pre-init-spec ::worker [_] - (s/keys :req [::parallelism - ::mtx/metrics - ::db/pool - ::rds/redis - ::queue - ::registry])) - -(defmethod ig/prep-key ::worker - [_ cfg] - (merge {::parallelism 1} - (d/without-nils cfg))) - -(defmethod ig/init-key ::worker - [_ {:keys [::db/pool ::queue ::parallelism] :as cfg}] - (let [queue (d/name queue) - cfg (assoc cfg ::queue queue)] - (if (db/read-only? pool) - (l/wrn :hint "worker: not started (db is read-only)" :queue queue :parallelism parallelism) - (doall - (->> (range parallelism) - (map #(assoc cfg ::worker-id %)) - (map start-worker!)))))) - -(defmethod ig/halt-key! ::worker - [_ threads] - (run! px/interrupt! threads)) - -(defn- start-worker! - [{:keys [::rds/redis ::worker-id ::queue] :as cfg}] - (px/thread - {:name (format "penpot/worker/runner:%s" worker-id)} - (l/inf :hint "worker: started" :worker-id worker-id :queue queue) - (try - (dm/with-open [rconn (rds/connect redis)] - (let [tenant (cf/get :tenant "main") - cfg (-> cfg - (assoc ::queue (str/ffmt "taskq:%:%" tenant queue)) - (assoc ::rds/rconn rconn) - (assoc ::timeout (dt/duration "5s")))] - (loop [] - (when (px/interrupted?) - (throw (InterruptedException. "interrupted"))) - - (run-worker-loop! cfg) - (recur)))) - - (catch InterruptedException _ - (l/debug :hint "worker: interrupted" - :worker-id worker-id - :queue queue)) - (catch Throwable cause - (l/err :hint "worker: unexpected exception" - :worker-id worker-id - :queue queue - :cause cause)) - (finally - (l/inf :hint "worker: terminated" - :worker-id worker-id - :queue queue))))) - -(defn- run-worker-loop! - [{:keys [::db/pool ::rds/rconn ::timeout ::queue ::registry ::worker-id]}] - (letfn [(handle-task-retry [{:keys [task error inc-by delay] :or {inc-by 1 delay 1000}}] - (let [explain (ex-message error) - nretry (+ (:retry-num task) inc-by) - now (dt/now) - delay (->> (iterate #(* % 2) delay) (take nretry) (last))] - (db/update! pool :task - {:error explain - :status "retry" - :modified-at now - :scheduled-at (dt/plus now delay) - :retry-num nretry} - {:id (:id task)}) - nil)) - - (handle-task-failure [{:keys [task error]}] - (let [explain (ex-message error)] - (db/update! pool :task - {:error explain - :modified-at (dt/now) - :status "failed"} - {:id (:id task)}) - nil)) - - (handle-task-completion [{:keys [task]}] - (let [now (dt/now)] - (db/update! pool :task - {:completed-at now - :modified-at now - :status "completed"} - {:id (:id task)}) - nil)) - - (decode-payload [^bytes payload] - (try - (let [task-id (t/decode payload)] - (if (uuid? task-id) - task-id - (l/err :hint "worker: received unexpected payload (uuid expected)" - :payload task-id))) - (catch Throwable cause - (l/err :hint "worker: unable to decode payload" - :payload payload - :length (alength payload) - :cause cause)))) - - (handle-task [{:keys [name] :as task}] - (let [task-fn (get registry name)] - (if task-fn - (task-fn task) - (l/wrn :hint "no task handler found" :name name)) - {:status :completed :task task})) - - (handle-task-exception [cause task] - (let [edata (ex-data cause)] - (if (and (< (:retry-num task) - (:max-retries task)) - (= ::retry (:type edata))) - (cond-> {:status :retry :task task :error cause} - (dt/duration? (:delay edata)) - (assoc :delay (:delay edata)) - - (= ::noop (:strategy edata)) - (assoc :inc-by 0)) - (do - (l/err :hint "worker: unhandled exception on task" - ::l/context (get-error-context cause task) - :cause cause) - (if (>= (:retry-num task) (:max-retries task)) - {:status :failed :task task :error cause} - {:status :retry :task task :error cause}))))) - - (get-task [task-id] - (ex/try! - (some-> (db/get* pool :task {:id task-id}) - (decode-task-row)))) - - (run-task [task-id] - (loop [task (get-task task-id)] - (cond - (ex/exception? task) - (if (or (db/connection-error? task) - (db/serialization-error? task)) - (do - (l/wrn :hint "worker: connection error on retrieving task from database (retrying in some instants)" - :worker-id worker-id - :cause task) - (px/sleep (::rds/timeout rconn)) - (recur (get-task task-id))) - (do - (l/err :hint "worker: unhandled exception on retrieving task from database (retrying in some instants)" - :worker-id worker-id - :cause task) - (px/sleep (::rds/timeout rconn)) - (recur (get-task task-id)))) - - (nil? task) - (l/wrn :hint "worker: no task found on the database" - :worker-id worker-id - :task-id task-id) - - :else - (try - (l/trc :hint "executing task" - :name (:name task) - :id (str (:id task)) - :queue queue - :worker-id worker-id - :retry (:retry-num task)) - (handle-task task) - (catch InterruptedException cause - (throw cause)) - (catch Throwable cause - (handle-task-exception cause task)))))) - - (process-result [{:keys [status] :as result}] - (ex/try! - (case status - :retry (handle-task-retry result) - :failed (handle-task-failure result) - :completed (handle-task-completion result) - nil))) - - (run-task-loop [task-id] - (loop [result (run-task task-id)] - (when-let [cause (process-result result)] - (if (or (db/connection-error? cause) - (db/serialization-error? cause)) - (do - (l/wrn :hint "worker: database exeption on processing task result (retrying in some instants)" - :cause cause) - (px/sleep (::rds/timeout rconn)) - (recur result)) - (do - (l/err :hint "worker: unhandled exception on processing task result (retrying in some instants)" - :cause cause) - (px/sleep (::rds/timeout rconn)) - (recur result))))))] - - (try - (let [[_ payload] (rds/blpop! rconn timeout queue)] - (some-> payload - decode-payload - run-task-loop)) - - (catch InterruptedException cause - (throw cause)) - - (catch Exception cause - (if (rds/timeout-exception? cause) - (do - (l/err :hint "worker: redis pop operation timeout, consider increasing redis timeout (will retry in some instants)" - :timeout timeout - :cause cause) - (px/sleep timeout)) - - (l/err :hint "worker: unhandled exception" :cause cause)))))) - -(defn- get-error-context - [_ item] - {:params item}) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; CRON -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare schedule-cron-task) -(declare synchronize-cron-entries!) - -(s/def ::fn (s/or :var var? :fn fn?)) -(s/def ::id keyword?) -(s/def ::cron dt/cron?) -(s/def ::props (s/nilable map?)) -(s/def ::task keyword?) - -(s/def ::cron-task - (s/keys :req-un [::cron ::task] - :opt-un [::props ::id])) - -(s/def ::entries (s/coll-of (s/nilable ::cron-task))) - -(defmethod ig/pre-init-spec ::cron [_] - (s/keys :req [::db/pool ::entries ::registry])) - -(defmethod ig/init-key ::cron - [_ {:keys [::entries ::registry ::db/pool] :as cfg}] - (if (db/read-only? pool) - (l/wrn :hint "cron: not started (db is read-only)") - (let [running (atom #{}) - entries (->> entries - (filter some?) - ;; If id is not defined, use the task as id. - (map (fn [{:keys [id task] :as item}] - (if (some? id) - (assoc item :id (d/name id)) - (assoc item :id (d/name task))))) - (map (fn [item] - (update item :task d/name))) - (map (fn [{:keys [task] :as item}] - (let [f (get registry task)] - (when-not f - (ex/raise :type :internal - :code :task-not-found - :hint (str/fmt "task %s not configured" task))) - (-> item - (dissoc :task) - (assoc :fn f)))))) - - cfg (assoc cfg ::entries entries ::running running)] - - (l/inf :hint "cron: started" :tasks (count entries)) - (synchronize-cron-entries! cfg) - - (->> (filter some? entries) - (run! (partial schedule-cron-task cfg))) - - (reify - clojure.lang.IDeref - (deref [_] @running) - - java.lang.AutoCloseable - (close [_] - (l/inf :hint "cron: terminated") - (doseq [item @running] - (when-not (.isDone ^Future item) - (.cancel ^Future item true)))))))) - -(defmethod ig/halt-key! ::cron - [_ instance] - (some-> instance d/close!)) - -(def sql:upsert-cron-task - "insert into scheduled_task (id, cron_expr) - values (?, ?) - on conflict (id) - do update set cron_expr=?") - -(defn- synchronize-cron-entries! - [{:keys [::db/pool ::entries]}] - (db/with-atomic [conn pool] - (doseq [{:keys [id cron]} entries] - (l/trc :hint "register cron task" :id id :cron (str cron)) - (db/exec-one! conn [sql:upsert-cron-task id (str cron) (str cron)])))) - -(defn- lock-scheduled-task! - [conn id] - (let [sql (str "SELECT id FROM scheduled_task " - " WHERE id=? FOR UPDATE SKIP LOCKED")] - (some? (db/exec-one! conn [sql (d/name id)])))) - -(defn- execute-cron-task - [{:keys [::db/pool] :as cfg} {:keys [id] :as task}] - (px/thread - {:name (str "penpot/cront-task/" id)} - (try - (db/with-atomic [conn pool] - (db/exec-one! conn ["SET statement_timeout=0;"]) - (db/exec-one! conn ["SET idle_in_transaction_session_timeout=0;"]) - (when (lock-scheduled-task! conn id) - (l/dbg :hint "cron: execute task" :task-id id) - ((:fn task) task)) - (db/rollback! conn)) - - (catch InterruptedException _ - (l/debug :hint "cron: task interrupted" :task-id id)) - - (catch Throwable cause - (binding [l/*context* (get-error-context cause task)] - (l/err :hint "cron: unhandled exception on running task" - :task-id id - :cause cause))) - (finally - (when-not (px/interrupted? :current) - (schedule-cron-task cfg task)))))) - -(defn- ms-until-valid - [cron] - (s/assert dt/cron? cron) - (let [now (dt/now) - next (dt/next-valid-instant-from cron now)] - (dt/diff now next))) - -(defn- schedule-cron-task - [{:keys [::running] :as cfg} {:keys [cron id] :as task}] - (let [ts (ms-until-valid cron) - ft (px/schedule! ts (partial execute-cron-task cfg task))] - - (l/dbg :hint "cron: schedule task" :task-id id - :ts (dt/format-duration ts) - :at (dt/format-instant (dt/in-future ts))) - (swap! running #(into #{ft} (filter p/pending?) %)))) - - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SUBMIT API ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn- extract-props - [options] - (let [cns (namespace ::sample)] - (persistent! - (reduce-kv (fn [res k v] - (cond-> res - (not= (namespace k) cns) - (assoc! k v))) - (transient {}) - options)))) - (def ^:private sql:insert-new-task "insert into task (id, name, props, queue, label, priority, max_retries, scheduled_at) values (?, ?, ?, ?, ?, ?, ?, now() + ?) @@ -654,14 +77,13 @@ (s/def ::task (s/or :kw keyword? :str string?)) (s/def ::queue (s/or :kw keyword? :str string?)) (s/def ::delay (s/or :int integer? :duration dt/duration?)) -(s/def ::conn (s/or :pool ::db/pool :connection some?)) (s/def ::priority integer?) (s/def ::max-retries integer?) (s/def ::dedupe boolean?) (s/def ::submit-options (s/and - (s/keys :req [::task ::conn] + (s/keys :req [::task] :opt [::label ::delay ::queue ::priority ::max-retries ::dedupe]) (fn [{:keys [::dedupe ::label] :or {label ""}}] (if dedupe @@ -669,29 +91,39 @@ true)))) (defn submit! - [& {:keys [::task ::delay ::queue ::priority ::max-retries ::conn ::dedupe ::label] + [& {:keys [::params ::task ::delay ::queue ::priority ::max-retries ::dedupe ::label] :or {delay 0 queue :default priority 100 max-retries 3 label ""} :as options}] + (us/verify! ::submit-options options) (let [duration (dt/duration delay) interval (db/interval duration) - props (-> options extract-props db/tjson) + props (db/tjson params) id (uuid/next) tenant (cf/get :tenant) task (d/name task) queue (str/ffmt "%:%" tenant (d/name queue)) + conn (db/get-connectable options) deleted (when dedupe (-> (db/exec-one! conn [sql:remove-not-started-tasks task queue label]) :next.jdbc/update-count))] (l/trc :hint "submit task" :name task + :task-id (str id) :queue queue :label label :dedupe (boolean dedupe) - :deleted (or deleted 0) - :in (dt/format-duration duration)) + :delay (dt/format-duration duration) + :replace (or deleted 0)) (db/exec-one! conn [sql:insert-new-task id task props queue label priority max-retries interval]) id)) + +(defn invoke! + [{:keys [::task ::params] :as cfg}] + (assert (contains? cfg :app.worker/registry) + "missing worker registry on `cfg`") + (let [task-fn (dm/get-in cfg [:app.worker/registry (name task)])] + (task-fn {:props params}))) diff --git a/backend/src/app/worker/cron.clj b/backend/src/app/worker/cron.clj new file mode 100644 index 0000000000000000000000000000000000000000..cb5a69d88272efca2f49100a9d33a5d24a0423fd --- /dev/null +++ b/backend/src/app/worker/cron.clj @@ -0,0 +1,164 @@ +;; 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 app.worker.cron + (:require + [app.common.data :as d] + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.db :as db] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [app.worker.runner :refer [get-error-context]] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [integrant.core :as ig] + [promesa.core :as p] + [promesa.exec :as px]) + (:import + java.util.concurrent.Future)) + +(set! *warn-on-reflection* true) + +(def sql:upsert-cron-task + "insert into scheduled_task (id, cron_expr) + values (?, ?) + on conflict (id) + do nothing") + +(defn- synchronize-cron-entries! + [{:keys [::db/conn ::entries]}] + (doseq [{:keys [id cron]} entries] + (let [result (db/exec-one! conn [sql:upsert-cron-task id (str cron)]) + updated? (pos? (db/get-update-count result))] + (l/dbg :hint "register task" :id id :cron (str cron) + :status (if updated? "created" "exists"))))) + +(defn- lock-scheduled-task! + [conn id] + (let [sql (str "SELECT id FROM scheduled_task " + " WHERE id=? FOR UPDATE SKIP LOCKED")] + (some? (db/exec-one! conn [sql (d/name id)])))) + +(declare ^:private schedule-cron-task) + +(defn- execute-cron-task + [cfg {:keys [id cron] :as task}] + (px/thread + {:name (str "penpot/cron-task/" id)} + (let [tpoint (dt/tpoint)] + (try + (db/tx-run! cfg (fn [{:keys [::db/conn]}] + (db/exec-one! conn ["SET LOCAL statement_timeout=0;"]) + (db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout=0;"]) + (when (lock-scheduled-task! conn id) + (db/update! conn :scheduled-task + {:cron-expr (str cron) + :modified-at (dt/now)} + {:id id} + {::db/return-keys false}) + (l/dbg :hint "start" :id id) + ((:fn task) task) + (let [elapsed (dt/format-duration (tpoint))] + (l/dbg :hint "end" :id id :elapsed elapsed))))) + + (catch InterruptedException _ + (let [elapsed (dt/format-duration (tpoint))] + (l/debug :hint "task interrupted" :id id :elapsed elapsed))) + + (catch Throwable cause + (let [elapsed (dt/format-duration (tpoint))] + (binding [l/*context* (get-error-context cause task)] + (l/err :hint "unhandled exception on running task" + :id id + :elapsed elapsed + :cause cause)))) + (finally + (when-not (px/interrupted? :current) + (schedule-cron-task cfg task))))))) + +(defn- ms-until-valid + [cron] + (s/assert dt/cron? cron) + (let [now (dt/now) + next (dt/next-valid-instant-from cron now)] + (dt/diff now next))) + +(defn- schedule-cron-task + [{:keys [::running] :as cfg} {:keys [cron id] :as task}] + (let [ts (ms-until-valid cron) + ft (px/schedule! ts (partial execute-cron-task cfg task))] + + (l/dbg :hint "schedule" :id id + :ts (dt/format-duration ts) + :at (dt/format-instant (dt/in-future ts))) + + (swap! running #(into #{ft} (filter p/pending?) %)))) + + +(s/def ::fn (s/or :var var? :fn fn?)) +(s/def ::id keyword?) +(s/def ::cron dt/cron?) +(s/def ::props (s/nilable map?)) +(s/def ::task keyword?) + +(s/def ::task-item + (s/keys :req-un [::cron ::task] + :opt-un [::props ::id])) + +(s/def ::wrk/entries (s/coll-of (s/nilable ::task-item))) + +(defmethod ig/pre-init-spec ::wrk/cron [_] + (s/keys :req [::db/pool ::wrk/entries ::wrk/registry])) + +(defmethod ig/init-key ::wrk/cron + [_ {:keys [::wrk/entries ::wrk/registry ::db/pool] :as cfg}] + (if (db/read-only? pool) + (l/wrn :hint "service not started (db is read-only)") + (let [running (atom #{}) + entries (->> entries + (filter some?) + ;; If id is not defined, use the task as id. + (map (fn [{:keys [id task] :as item}] + (if (some? id) + (assoc item :id (d/name id)) + (assoc item :id (d/name task))))) + (map (fn [item] + (update item :task d/name))) + (map (fn [{:keys [task] :as item}] + (let [f (get registry task)] + (when-not f + (ex/raise :type :internal + :code :task-not-found + :hint (str/fmt "task %s not configured" task))) + (-> item + (dissoc :task) + (assoc :fn f)))))) + + cfg (assoc cfg ::entries entries ::running running)] + + (l/inf :hint "started" :tasks (count entries)) + + (db/tx-run! cfg synchronize-cron-entries!) + + (->> (filter some? entries) + (run! (partial schedule-cron-task cfg))) + + (reify + clojure.lang.IDeref + (deref [_] @running) + + java.lang.AutoCloseable + (close [_] + (l/inf :hint "terminated") + (doseq [item @running] + (when-not (.isDone ^Future item) + (.cancel ^Future item true)))))))) + +(defmethod ig/halt-key! ::wrk/cron + [_ instance] + (some-> instance d/close!)) + diff --git a/backend/src/app/worker/dispatcher.clj b/backend/src/app/worker/dispatcher.clj new file mode 100644 index 0000000000000000000000000000000000000000..9b901747f9a11cdfd74e7dcb3d8bf4e1fb1e0634 --- /dev/null +++ b/backend/src/app/worker/dispatcher.clj @@ -0,0 +1,110 @@ +;; 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 app.worker.dispatcher + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.logging :as l] + [app.common.transit :as t] + [app.config :as cf] + [app.db :as db] + [app.metrics :as mtx] + [app.redis :as rds] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [integrant.core :as ig] + [promesa.exec :as px])) + +(set! *warn-on-reflection* true) + +(defmethod ig/pre-init-spec ::wrk/dispatcher [_] + (s/keys :req [::mtx/metrics ::db/pool ::rds/redis])) + +(defmethod ig/prep-key ::wrk/dispatcher + [_ cfg] + (merge {::batch-size 100 + ::wait-duration (dt/duration "5s")} + (d/without-nils cfg))) + +(def ^:private sql:select-next-tasks + "select id, queue from task as t + where t.scheduled_at <= now() + and (t.status = 'new' or t.status = 'retry') + and queue ~~* ?::text + order by t.priority desc, t.scheduled_at + limit ? + for update skip locked") + +(defmethod ig/init-key ::wrk/dispatcher + [_ {:keys [::db/pool ::rds/redis ::batch-size] :as cfg}] + (letfn [(get-tasks [conn] + (let [prefix (str (cf/get :tenant) ":%")] + (seq (db/exec! conn [sql:select-next-tasks prefix batch-size])))) + + (push-tasks! [conn rconn [queue tasks]] + (let [ids (mapv :id tasks) + key (str/ffmt "taskq:%" queue) + res (rds/rpush! rconn key (mapv t/encode ids)) + sql [(str "update task set status = 'scheduled'" + " where id = ANY(?)") + (db/create-array conn "uuid" ids)]] + + (db/exec-one! conn sql) + (l/trc :hist "enqueue tasks on redis" + :queue queue + :tasks (count ids) + :queued res))) + + (run-batch! [rconn] + (try + (db/with-atomic [conn pool] + (if-let [tasks (get-tasks conn)] + (->> (group-by :queue tasks) + (run! (partial push-tasks! conn rconn))) + (px/sleep (::wait-duration cfg)))) + (catch InterruptedException cause + (throw cause)) + (catch Exception cause + (cond + (rds/exception? cause) + (do + (l/wrn :hint "redis exception (will retry in an instant)" :cause cause) + (px/sleep (::rds/timeout rconn))) + + (db/sql-exception? cause) + (do + (l/wrn :hint "database exception (will retry in an instant)" :cause cause) + (px/sleep (::rds/timeout rconn))) + + :else + (do + (l/err :hint "unhandled exception (will retry in an instant)" :cause cause) + (px/sleep (::rds/timeout rconn))))))) + + (dispatcher [] + (l/inf :hint "started") + (try + (dm/with-open [rconn (rds/connect redis)] + (loop [] + (run-batch! rconn) + (recur))) + (catch InterruptedException _ + (l/trc :hint "interrupted")) + (catch Throwable cause + (l/err :hint " unexpected exception" :cause cause)) + (finally + (l/inf :hint "terminated"))))] + + (if (db/read-only? pool) + (l/wrn :hint "not started (db is read-only)") + (px/fn->thread dispatcher :name "penpot/worker/dispatcher" :virtual false)))) + +(defmethod ig/halt-key! ::wrk/dispatcher + [_ thread] + (some-> thread px/interrupt!)) diff --git a/backend/src/app/worker/executor.clj b/backend/src/app/worker/executor.clj new file mode 100644 index 0000000000000000000000000000000000000000..c1d10122c7ac31095ce8b201179d1514f756c5c4 --- /dev/null +++ b/backend/src/app/worker/executor.clj @@ -0,0 +1,116 @@ +;; 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 app.worker.executor + "Async tasks abstraction (impl)." + (:require + [app.common.data :as d] + [app.common.logging :as l] + [app.common.spec :as us] + [app.metrics :as mtx] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [integrant.core :as ig] + [promesa.exec :as px]) + (:import + java.util.concurrent.Executor + java.util.concurrent.ThreadPoolExecutor)) + +(set! *warn-on-reflection* true) + +(s/def ::wrk/executor #(instance? Executor %)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; EXECUTOR +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defmethod ig/pre-init-spec ::wrk/executor [_] + (s/keys :req [])) + +(defmethod ig/init-key ::wrk/executor + [_ _] + (let [factory (px/thread-factory :prefix "penpot/default/") + executor (px/cached-executor :factory factory :keepalive 60000)] + (l/inf :hint "executor started") + (reify + java.lang.AutoCloseable + (close [_] + (l/inf :hint "stoping executor") + (px/shutdown! executor)) + + clojure.lang.IDeref + (deref [_] + {:active (.getPoolSize ^ThreadPoolExecutor executor) + :running (.getActiveCount ^ThreadPoolExecutor executor) + :completed (.getCompletedTaskCount ^ThreadPoolExecutor executor)}) + + Executor + (execute [_ runnable] + (.execute ^Executor executor ^Runnable runnable))))) + +(defmethod ig/halt-key! ::wrk/executor + [_ instance] + (.close ^java.lang.AutoCloseable instance)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; MONITOR +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(s/def ::name ::us/keyword) + +(defmethod ig/pre-init-spec ::wrk/monitor [_] + (s/keys :req [::wrk/name ::wrk/executor ::mtx/metrics])) + +(defmethod ig/prep-key ::wrk/monitor + [_ cfg] + (merge {::interval (dt/duration "2s")} + (d/without-nils cfg))) + +(defmethod ig/init-key ::wrk/monitor + [_ {:keys [::wrk/executor ::mtx/metrics ::interval ::wrk/name]}] + (letfn [(monitor! [executor prev-completed] + (let [labels (into-array String [(d/name name)]) + stats (deref executor) + + completed (:completed stats) + completed-inc (- completed prev-completed) + completed-inc (if (neg? completed-inc) 0 completed-inc)] + + (mtx/run! metrics + :id :executor-active-threads + :labels labels + :val (:active stats)) + + (mtx/run! metrics + :id :executor-running-threads + :labels labels + :val (:running stats)) + + (mtx/run! metrics + :id :executors-completed-tasks + :labels labels + :inc completed-inc) + + completed-inc))] + + (px/thread + {:name "penpot/executors-monitor" :virtual true} + (l/inf :hint "monitor started" :name name) + (try + (loop [completed 0] + (px/sleep interval) + (recur (long (monitor! executor completed)))) + (catch InterruptedException _cause + (l/trc :hint "monitor: interrupted" :name name)) + (catch Throwable cause + (l/err :hint "monitor: unexpected error" :name name :cause cause)) + (finally + (l/inf :hint "monitor: terminated" :name name)))))) + +(defmethod ig/halt-key! ::wrk/monitor + [_ thread] + (px/interrupt! thread)) diff --git a/backend/src/app/worker/runner.clj b/backend/src/app/worker/runner.clj new file mode 100644 index 0000000000000000000000000000000000000000..4082c4a3a4ba1c81d8f63b351bef31aa9da1ea94 --- /dev/null +++ b/backend/src/app/worker/runner.clj @@ -0,0 +1,274 @@ +;; 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 app.worker.runner + "Async tasks abstraction (impl)." + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.exceptions :as ex] + [app.common.logging :as l] + [app.common.transit :as t] + [app.config :as cf] + [app.db :as db] + [app.metrics :as mtx] + [app.redis :as rds] + [app.util.time :as dt] + [app.worker :as-alias wrk] + [clojure.spec.alpha :as s] + [cuerdas.core :as str] + [integrant.core :as ig] + [promesa.exec :as px])) + +(set! *warn-on-reflection* true) + +(defn- decode-task-row + [{:keys [props] :as row}] + (cond-> row + (db/pgobject? props) + (assoc :props (db/decode-transit-pgobject props)))) + +(defn get-error-context + [_ item] + {:params item}) + +(defn- get-task + [{:keys [::db/pool]} task-id] + (ex/try! + (some-> (db/get* pool :task {:id task-id}) + (decode-task-row)))) + +(defn- run-task + [{:keys [::wrk/registry ::id ::queue] :as cfg} task] + (try + (l/dbg :hint "start" + :name (:name task) + :task-id (str (:id task)) + :queue queue + :runner-id id + :retry (:retry-num task)) + (let [tpoint (dt/tpoint) + task-fn (get registry (:name task)) + result (if task-fn + (task-fn task) + {:status :completed :task task}) + elapsed (dt/format-duration (tpoint))] + + (when-not task-fn + (l/wrn :hint "no task handler found" :name (:name task))) + + (l/dbg :hint "end" + :name (:name task) + :task-id (str (:id task)) + :queue queue + :runner-id id + :retry (:retry-num task) + :elapsed elapsed) + + result) + + (catch InterruptedException cause + (throw cause)) + (catch Throwable cause + (let [edata (ex-data cause)] + (if (and (< (:retry-num task) + (:max-retries task)) + (= ::retry (:type edata))) + (cond-> {:status :retry :task task :error cause} + (dt/duration? (:delay edata)) + (assoc :delay (:delay edata)) + + (= ::noop (:strategy edata)) + (assoc :inc-by 0)) + (do + (l/err :hint "unhandled exception on task" + ::l/context (get-error-context cause task) + :cause cause) + (if (>= (:retry-num task) (:max-retries task)) + {:status :failed :task task :error cause} + {:status :retry :task task :error cause}))))))) + +(defn- run-task! + [{:keys [::rds/rconn ::id] :as cfg} task-id] + (loop [task (get-task cfg task-id)] + (cond + (ex/exception? task) + (if (or (db/connection-error? task) + (db/serialization-error? task)) + (do + (l/wrn :hint "connection error on retrieving task from database (retrying in some instants)" + :id id + :cause task) + (px/sleep (::rds/timeout rconn)) + (recur (get-task cfg task-id))) + (do + (l/err :hint "unhandled exception on retrieving task from database (retrying in some instants)" + :id id + :cause task) + (px/sleep (::rds/timeout rconn)) + (recur (get-task cfg task-id)))) + + (nil? task) + (l/wrn :hint "no task found on the database" + :id id + :task-id task-id) + + :else + (run-task cfg task)))) + +(defn- run-worker-loop! + [{:keys [::db/pool ::rds/rconn ::timeout ::queue] :as cfg}] + (letfn [(handle-task-retry [{:keys [task error inc-by delay] :or {inc-by 1 delay 1000}}] + (let [explain (ex-message error) + nretry (+ (:retry-num task) inc-by) + now (dt/now) + delay (->> (iterate #(* % 2) delay) (take nretry) (last))] + (db/update! pool :task + {:error explain + :status "retry" + :modified-at now + :scheduled-at (dt/plus now delay) + :retry-num nretry} + {:id (:id task)}) + nil)) + + (handle-task-failure [{:keys [task error]}] + (let [explain (ex-message error)] + (db/update! pool :task + {:error explain + :modified-at (dt/now) + :status "failed"} + {:id (:id task)}) + nil)) + + (handle-task-completion [{:keys [task]}] + (let [now (dt/now)] + (db/update! pool :task + {:completed-at now + :modified-at now + :status "completed"} + {:id (:id task)}) + nil)) + + (decode-payload [^bytes payload] + (try + (let [task-id (t/decode payload)] + (if (uuid? task-id) + task-id + (l/err :hint "received unexpected payload (uuid expected)" + :payload task-id))) + (catch Throwable cause + (l/err :hint "unable to decode payload" + :payload payload + :length (alength payload) + :cause cause)))) + + (process-result [{:keys [status] :as result}] + (ex/try! + (case status + :retry (handle-task-retry result) + :failed (handle-task-failure result) + :completed (handle-task-completion result) + nil))) + + (run-task-loop [task-id] + (loop [result (run-task! cfg task-id)] + (when-let [cause (process-result result)] + (if (or (db/connection-error? cause) + (db/serialization-error? cause)) + (do + (l/wrn :hint "database exeption on processing task result (retrying in some instants)" + :cause cause) + (px/sleep (::rds/timeout rconn)) + (recur result)) + (do + (l/err :hint "unhandled exception on processing task result (retrying in some instants)" + :cause cause) + (px/sleep (::rds/timeout rconn)) + (recur result))))))] + + (try + (let [queue (str/ffmt "taskq:%" queue) + [_ payload] (rds/blpop! rconn timeout queue)] + (some-> payload + decode-payload + run-task-loop)) + + (catch InterruptedException cause + (throw cause)) + + (catch Exception cause + (if (rds/timeout-exception? cause) + (do + (l/err :hint "redis pop operation timeout, consider increasing redis timeout (will retry in some instants)" + :timeout timeout + :cause cause) + (px/sleep timeout)) + + (l/err :hint "unhandled exception" :cause cause)))))) + +(defn- start-thread! + [{:keys [::rds/redis ::id ::queue] :as cfg}] + (px/thread + {:name (format "penpot/worker/runner:%s" id)} + (l/inf :hint "started" :id id :queue queue) + (try + (dm/with-open [rconn (rds/connect redis)] + (let [tenant (cf/get :tenant "main") + cfg (-> cfg + (assoc ::queue (str/ffmt "%:%" tenant queue)) + (assoc ::rds/rconn rconn) + (assoc ::timeout (dt/duration "5s")))] + (loop [] + (when (px/interrupted?) + (throw (InterruptedException. "interrupted"))) + + (run-worker-loop! cfg) + (recur)))) + + (catch InterruptedException _ + (l/dbg :hint "interrupted" + :id id + :queue queue)) + (catch Throwable cause + (l/err :hint "unexpected exception" + :id id + :queue queue + :cause cause)) + (finally + (l/inf :hint "terminated" + :id id + :queue queue))))) + +(s/def ::wrk/queue keyword?) + +(defmethod ig/pre-init-spec ::runner [_] + (s/keys :req [::wrk/parallelism + ::mtx/metrics + ::db/pool + ::rds/redis + ::wrk/queue + ::wrk/registry])) + +(defmethod ig/prep-key ::wrk/runner + [_ cfg] + (merge {::wrk/parallelism 1} + (d/without-nils cfg))) + +(defmethod ig/init-key ::wrk/runner + [_ {:keys [::db/pool ::wrk/queue ::wrk/parallelism] :as cfg}] + (let [queue (d/name queue) + cfg (assoc cfg ::queue queue)] + (if (db/read-only? pool) + (l/wrn :hint "not started (db is read-only)" :queue queue :parallelism parallelism) + (doall + (->> (range parallelism) + (map #(assoc cfg ::id %)) + (map start-thread!)))))) + +(defmethod ig/halt-key! ::wrk/runner + [_ threads] + (run! px/interrupt! threads)) diff --git a/backend/test/backend_tests/bounce_handling_test.clj b/backend/test/backend_tests/bounce_handling_test.clj index 61a322084875d9087c538cc9c03f85a7c456e95b..13774e042eaa6fd0f1a9aacf78bf8c3a3a4d7b1e 100644 --- a/backend/test/backend_tests/bounce_handling_test.clj +++ b/backend/test/backend_tests/bounce_handling_test.clj @@ -102,7 +102,7 @@ (t/deftest test-parse-bounce-report (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) - cfg {:app.main/props props} + cfg {:app.setup/props props} report (bounce-report {:token (tokens/generate props {:iss :profile-identity :profile-id (:id profile)})}) @@ -118,7 +118,7 @@ (t/deftest test-parse-complaint-report (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) - cfg {:app.main/props props} + cfg {:app.setup/props props} report (complaint-report {:token (tokens/generate props {:iss :profile-identity :profile-id (:id profile)})}) @@ -132,7 +132,7 @@ (t/deftest test-parse-complaint-report-without-token (let [props (:app.setup/props th/*system*) - cfg {:app.main/props props} + cfg {:app.setup/props props} report (complaint-report {:token ""}) result (#'awsns/parse-notification cfg report)] (t/is (= "complaint" (:type result))) @@ -145,7 +145,7 @@ (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.main/props props :app.db/pool pool} + cfg {:app.setup/props props :app.db/pool pool} report (bounce-report {:token (tokens/generate props {:iss :profile-identity :profile-id (:id profile)})}) @@ -172,7 +172,7 @@ (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.main/props props + cfg {:app.setup/props props :app.db/pool pool} report (complaint-report {:token (tokens/generate props {:iss :profile-identity @@ -202,7 +202,7 @@ (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.main/props props :app.db/pool pool} + cfg {:app.setup/props props :app.db/pool pool} report (bounce-report {:email (:email profile) :token (tokens/generate props {:iss :profile-identity @@ -224,7 +224,7 @@ (let [profile (th/create-profile* 1) props (:app.setup/props th/*system*) pool (:app.db/pool th/*system*) - cfg {:app.main/props props :app.db/pool pool} + cfg {:app.setup/props props :app.db/pool pool} report (complaint-report {:email (:email profile) :token (tokens/generate props {:iss :profile-identity diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index 987b553042f4fc723ecdd00cdd594568239153ef..e77b51d6a5188272e0b6020c80afada921d54c3e 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -34,6 +34,8 @@ [app.util.blob :as blob] [app.util.services :as sv] [app.util.time :as dt] + [app.worker :as wrk] + [app.worker.runner] [clojure.java.io :as io] [clojure.spec.alpha :as s] [clojure.test :as t] @@ -56,15 +58,14 @@ (def ^:dynamic *system* nil) (def ^:dynamic *pool* nil) -(def defaults +(def default {:database-uri "postgresql://postgres/penpot_test" :redis-uri "redis://redis/1" - :file-change-snapshot-every 1}) + :file-snapshot-every 1}) (def config - (->> (cf/read-env "penpot-test") - (merge cf/defaults defaults) - (us/conform ::cf/config))) + (cf/read-config :prefix "penpot-test" + :default (merge cf/default default))) (def default-flags [:enable-secure-session-cookies @@ -75,49 +76,9 @@ :enable-feature-fdata-pointer-map :enable-feature-fdata-objets-map :enable-feature-components-v2 + :enable-auto-file-snapshot :disable-file-validation]) -(def test-init-sql - ["alter table project_profile_rel set unlogged;\n" - "alter table file_profile_rel set unlogged;\n" - "alter table presence set unlogged;\n" - "alter table presence set unlogged;\n" - "alter table http_session set unlogged;\n" - "alter table team_profile_rel set unlogged;\n" - "alter table team_project_profile_rel set unlogged;\n" - "alter table comment_thread_status set unlogged;\n" - "alter table comment set unlogged;\n" - "alter table comment_thread set unlogged;\n" - "alter table profile_complaint_report set unlogged;\n" - "alter table file_change set unlogged;\n" - "alter table team_font_variant set unlogged;\n" - "alter table share_link set unlogged;\n" - "alter table usage_quote set unlogged;\n" - "alter table access_token set unlogged;\n" - "alter table profile set unlogged;\n" - "alter table file_library_rel set unlogged;\n" - "alter table file_thumbnail set unlogged;\n" - "alter table file_object_thumbnail set unlogged;\n" - "alter table file_tagged_object_thumbnail set unlogged;\n" - "alter table file_media_object set unlogged;\n" - "alter table file_data_fragment set unlogged;\n" - "alter table file set unlogged;\n" - "alter table project set unlogged;\n" - "alter table team_invitation set unlogged;\n" - "alter table webhook_delivery set unlogged;\n" - "alter table webhook set unlogged;\n" - "alter table team set unlogged;\n" - ;; For some reason, modifying the task realted tables is very very - ;; slow (5s); so we just don't alter them - ;; "alter table task set unlogged;\n" - ;; "alter table task_default set unlogged;\n" - ;; "alter table task_completed set unlogged;\n" - "alter table audit_log_default set unlogged ;\n" - "alter table storage_object set unlogged;\n" - "alter table server_error_report set unlogged;\n" - "alter table server_prop set unlogged;\n" - "alter table global_complaint_report set unlogged;\n"]) - (defn state-init [next] (with-redefs [app.config/flags (flags/parse flags/default default-flags) @@ -127,6 +88,8 @@ app.auth/verify-password (fn [a b] {:valid (= a b)}) app.common.features/get-enabled-features (fn [& _] app.common.features/supported-features)] + (cf/validate! :exit-on-error? false) + (fs/create-dir "/tmp/penpot") (let [templates [{:id "test" @@ -143,10 +106,10 @@ (dissoc :app.srepl/server :app.http/server :app.http/router - :app.auth.oidc/google-provider - :app.auth.oidc/gitlab-provider - :app.auth.oidc/github-provider - :app.auth.oidc/generic-provider + :app.auth.oidc.providers/google + :app.auth.oidc.providers/gitlab + :app.auth.oidc.providers/github + :app.auth.oidc.providers/generic :app.setup/templates :app.auth.oidc/routes :app.worker/monitor @@ -156,17 +119,14 @@ :app.loggers.database/reporter :app.worker/cron :app.worker/dispatcher - [:app.main/default :app.worker/worker] - [:app.main/webhook :app.worker/worker])) + [:app.main/default :app.worker/runner] + [:app.main/webhook :app.worker/runner])) _ (ig/load-namespaces system) system (-> (ig/prep system) (ig/init))] (try (binding [*system* system *pool* (:app.db/pool system)] - (db/with-atomic [conn *pool*] - (doseq [sql test-init-sql] - (db/exec! conn [sql]))) (next)) (finally (ig/halt! system)))))) @@ -181,8 +141,7 @@ (db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"]) (db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"]) (let [result (->> (db/exec! conn [sql]) - (map :table-name) - (remove #(= "task" %)))] + (map :table-name))] (doseq [table result] (db/exec! conn [(str "delete from " table ";")])))) @@ -263,7 +222,7 @@ ([params] (mark-file-deleted* *system* params)) ([conn {:keys [id] :as params}] - (#'files/mark-file-deleted! conn id))) + (#'files/mark-file-deleted conn id))) (defn create-team* ([i params] (create-team* *system* i params)) @@ -345,16 +304,18 @@ ([params] (update-file* *system* params)) ([system {:keys [file-id changes session-id profile-id revn] :or {session-id (uuid/next) revn 0}}] - (db/tx-run! system (fn [{:keys [::db/conn] :as system}] - (let [file (files.update/get-file conn file-id)] - (files.update/update-file system + (-> system + (assoc ::files.update/timestamp (dt/now)) + (db/tx-run! (fn [{:keys [::db/conn] :as system}] + (let [file (files.update/get-file conn file-id)] + (#'files.update/update-file* system {:id file-id :revn revn :file file :features (:features file) :changes changes :session-id session-id - :profile-id profile-id})))))) + :profile-id profile-id}))))))) (declare command!) @@ -421,9 +382,21 @@ ([name] (run-task! name {})) ([name params] - (let [tasks (:app.worker/registry *system*)] - (let [task-fn (get tasks (d/name name))] - (task-fn params))))) + (wrk/invoke! (-> *system* + (assoc ::wrk/task name) + (assoc ::wrk/params params))))) + +(def sql:pending-tasks + "select t.* from task as t + where t.status = 'new' + order by t.priority desc, t.scheduled_at") + +(defn run-pending-tasks! + [] + (db/tx-run! *system* (fn [{:keys [::db/conn] :as cfg}] + (let [tasks (->> (db/exec! conn [sql:pending-tasks]) + (map #'app.worker.runner/decode-task-row))] + (run! (partial #'app.worker.runner/run-task cfg) tasks))))) ;; --- UTILS @@ -555,7 +528,6 @@ ([key default] (get data key (get cf/config key default))))) - (defn reset-mock! [m] (swap! m (fn [m] diff --git a/backend/test/backend_tests/loggers_webhooks_test.clj b/backend/test/backend_tests/loggers_webhooks_test.clj index ab3a4e82edae918349c56f926c72ef0a343a9537..c34df715436af1169693a43b07139ca15bf12129 100644 --- a/backend/test/backend_tests/loggers_webhooks_test.clj +++ b/backend/test/backend_tests/loggers_webhooks_test.clj @@ -21,11 +21,9 @@ (with-mocks [submit-mock {:target 'app.worker/submit! :return nil}] (let [prof (th/create-profile* 1 {:is-active true}) res (th/run-task! :process-webhook-event - {:props - {:app.loggers.webhooks/event - {:type "command" - :name "create-project" - :props {:team-id (:default-team-id prof)}}}})] + {:type "command" + :name "create-project" + :props {:team-id (:default-team-id prof)}})] (t/is (= 0 (:call-count @submit-mock))) (t/is (nil? res))))) @@ -35,11 +33,9 @@ (let [prof (th/create-profile* 1 {:is-active true}) whk (th/create-webhook* {:team-id (:default-team-id prof)}) res (th/run-task! :process-webhook-event - {:props - {:app.loggers.webhooks/event - {:type "command" - :name "create-project" - :props {:team-id (:default-team-id prof)}}}})] + {:type "command" + :name "create-project" + :props {:team-id (:default-team-id prof)}})] (t/is (= 1 (:call-count @submit-mock))) (t/is (nil? res))))) @@ -52,9 +48,8 @@ :name "create-project" :props {:team-id (:default-team-id prof)}} res (th/run-task! :run-webhook - {:props - {:app.loggers.webhooks/event evt - :app.loggers.webhooks/config whk}})] + {:event evt + :config whk})] (t/is (= 1 (:call-count @http-mock))) @@ -75,9 +70,8 @@ :name "create-project" :props {:team-id (:default-team-id prof)}} res (th/run-task! :run-webhook - {:props - {:app.loggers.webhooks/event evt - :app.loggers.webhooks/config whk}})] + {:event evt + :config whk})] (t/is (= 1 (:call-count @http-mock))) @@ -94,14 +88,12 @@ ;; RUN 2 times more (th/run-task! :run-webhook - {:props - {:app.loggers.webhooks/event evt - :app.loggers.webhooks/config whk}}) + {:event evt + :config whk}) (th/run-task! :run-webhook - {:props - {:app.loggers.webhooks/event evt - :app.loggers.webhooks/config whk}}) + {:event evt + :config whk}) (let [rows (th/db-query :webhook-delivery {:webhook-id (:id whk)})] diff --git a/backend/test/backend_tests/rpc_audit_test.clj b/backend/test/backend_tests/rpc_audit_test.clj index 78d0e4d41007f281b3417c63b0b0d70508a24f49..14bff7ea658dfbcef8dfdf26c50a689a8fb41549 100644 --- a/backend/test/backend_tests/rpc_audit_test.clj +++ b/backend/test/backend_tests/rpc_audit_test.clj @@ -28,7 +28,8 @@ ring.request/Request (get-header [_ name] (case name - "x-forwarded-for" "127.0.0.44")))) + "x-forwarded-for" "127.0.0.44" + "x-real-ip" "127.0.0.43")))) (t/deftest push-events-1 (with-redefs [app.config/flags #{:audit-log}] @@ -46,6 +47,7 @@ :profile-id (:id prof) :timestamp (dt/now) :type "action"}]} + params (with-meta params {:app.http/request http-request}) diff --git a/backend/test/backend_tests/rpc_file_test.clj b/backend/test/backend_tests/rpc_file_test.clj index a684227c85b503cabe8068f44506a17ea38c36cd..9a072eaa83a87187e81a55b0fa646339adf9b70b 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -25,6 +25,20 @@ (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) +(defn- update-file! + [& {:keys [profile-id file-id changes revn] :or {revn 0}}] + (let [params {::th/type :update-file + ::rpc/profile-id profile-id + :id file-id + :session-id (uuid/random) + :revn revn + :features cfeat/supported-features + :changes changes} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (:result out))) + (t/deftest files-crud (let [prof (th/create-profile* 1 {:is-active true}) team-id (:default-team-id prof) @@ -149,8 +163,7 @@ shape-id (uuid/random)] ;; Preventive file-gc - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) ;; Check the number of fragments before adding the page (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] @@ -166,9 +179,12 @@ :name "test" :id page-id}]) + ;; Check the number of fragments before adding the page + (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] + (t/is (= 3 (count rows)))) + ;; The file-gc should mark for remove unused fragments - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) ;; Check the number of fragments (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] @@ -176,11 +192,11 @@ ;; The objects-gc should remove unused fragments (let [res (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (= 3 (:processed res)))) ;; Check the number of fragments (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] - (t/is (= 4 (count rows)))) + (t/is (= 2 (count rows)))) ;; Add shape to page that should add a new fragment (update-file! @@ -203,21 +219,19 @@ ;; Check the number of fragments (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] - (t/is (= 5 (count rows)))) + (t/is (= 3 (count rows)))) ;; The file-gc should mark for remove unused fragments - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) ;; The objects-gc should remove unused fragments (let [res (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (= 3 (:processed res)))) - ;; Check the number of fragments; should be 3 because changes - ;; are also holding pointers to fragments; + ;; Check the number of fragments; (let [rows (th/db-query :file-data-fragment {:file-id (:id file) :deleted-at nil})] - (t/is (= 6 (count rows)))) + (t/is (= 2 (count rows)))) ;; Lets proceed to delete all changes (th/db-delete! :file-change {:file-id (:id file)}) @@ -227,17 +241,16 @@ ;; The file-gc should remove fragments related to changes ;; snapshots previously deleted. - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) ;; Check the number of fragments; (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] ;; (pp/pprint rows) - (t/is (= 8 (count rows))) + (t/is (= 4 (count rows))) (t/is (= 2 (count (remove (comp some? :deleted-at) rows))))) (let [res (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 6 (:processed res)))) + (t/is (= 2 (:processed res)))) (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] (t/is (= 2 (count rows))))))) @@ -321,12 +334,10 @@ (t/is (= 0 (:delete res)))) ;; run the file-gc task immediately without forced min-age - (let [res (th/run-task! :file-gc)] - (t/is (= 0 (:processed res)))) + (t/is (false? (th/run-task! :file-gc {:file-id (:id file)}))) ;; run the task again - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) ;; retrieve file and check trimmed attribute (let [row (th/db-get :file {:id (:id file)})] @@ -338,7 +349,7 @@ (t/is (= 1 (count (remove (comp some? :deleted-at) rows))))) (let [res (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 2 (:processed res)))) + (t/is (= 3 (:processed res)))) ;; check file media objects (let [rows (th/db-query :file-media-object {:file-id (:id file)})] @@ -363,11 +374,10 @@ ;; Now, we have deleted the usage of pointers to the ;; file-media-objects, if we paste file-gc, they should be marked ;; as deleted. - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (let [res (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 2 (:processed res)))) + (t/is (= 3 (:processed res)))) ;; Now that file-gc have deleted the file-media-object usage, ;; lets execute the touched-gc task, we should see that two of @@ -486,19 +496,17 @@ :strokes [{:opacity 1 :stroke-image {:id (:id fmo5) :width 100 :height 100 :mtype "image/jpeg"}}]})}]) ;; run the file-gc task immediately without forced min-age - (let [res (th/run-task! :file-gc)] - (t/is (= 0 (:processed res)))) + (t/is (false? (th/run-task! :file-gc {:file-id (:id file)}))) ;; run the task again - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (let [res (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (= 2 (:processed res)))) (let [rows (th/db-query :file-data-fragment {:file-id (:id file) :deleted-at nil})] - (t/is (= (count rows) 2))) + (t/is (= (count rows) 1))) ;; retrieve file and check trimmed attribute (let [row (th/db-get :file {:id (:id file)})] @@ -530,16 +538,14 @@ ;; Now, we have deleted the usage of pointers to the ;; file-media-objects, if we paste file-gc, they should be marked ;; as deleted. - - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (let [res (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 6 (:processed res)))) + (t/is (= 7 (:processed res)))) (let [rows (th/db-query :file-data-fragment {:file-id (:id file) :deleted-at nil})] - (t/is (= (count rows) 3))) + (t/is (= (count rows) 1))) ;; Now that file-gc have deleted the file-media-object usage, ;; lets execute the touched-gc task, we should see that two of @@ -577,18 +583,18 @@ (t/is (nil? (:error out))) (:result out))) - (update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}] - (let [params {::th/type :update-file - ::rpc/profile-id profile-id - :id file-id - :session-id (uuid/random) - :revn revn - :features cfeat/supported-features - :changes changes} - out (th/command! params)] + #_(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}] + (let [params {::th/type :update-file + ::rpc/profile-id profile-id + :id file-id + :session-id (uuid/random) + :revn revn + :features cfeat/supported-features + :changes changes} + out (th/command! params)] ;; (th/print-result! out) - (t/is (nil? (:error out))) - (:result out)))] + (t/is (nil? (:error out))) + (:result out)))] (let [storage (:app.storage/storage th/*system*) profile (th/create-profile* 1) @@ -612,7 +618,6 @@ :frame-id frame-id-2)] ;; Add a two frames - (update-file! :file-id (:id file) :profile-id (:id profile) @@ -655,12 +660,10 @@ (t/is (= 0 (:delete res)))) ;; run the file-gc task immediately without forced min-age - (let [res (th/run-task! :file-gc)] - (t/is (= 0 (:processed res)))) + (t/is (false? (th/run-task! :file-gc {:file-id (:id file)}))) ;; run the task again - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) ;; retrieve file and check trimmed attribute (let [row (th/db-get :file {:id (:id file)})] @@ -689,8 +692,7 @@ :page-id page-id :id frame-id-2}]) - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})] (t/is (= 2 (count rows))) @@ -702,7 +704,7 @@ ;; thumbnail lets execute the objects-gc task which remove ;; the rows and mark as touched the storage object rows (let [res (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 3 (:processed res)))) + (t/is (= 5 (:processed res)))) ;; Now that objects-gc have deleted the object thumbnail lets ;; execute the touched-gc task @@ -723,8 +725,7 @@ :page-id page-id :id frame-id-1}]) - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id file-id})] (t/is (= 1 (count rows))) @@ -732,7 +733,7 @@ (let [res (th/run-task! :objects-gc {:min-age 0})] ;; (pp/pprint res) - (t/is (= 2 (:processed res)))) + (t/is (= 3 (:processed res)))) ;; We still have th storage objects in the table (let [rows (th/db-query :storage-object {:deleted-at nil})] @@ -1123,13 +1124,12 @@ (th/sleep 300) ;; run the task - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) ;; check that object thumbnails are still here - (let [res (th/db-exec! ["select * from file_tagged_object_thumbnail"])] - ;; (th/print-result! res) - (t/is (= 1 (count res)))) + (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})] + ;; (app.common.pprint/pprint rows) + (t/is (= 1 (count rows)))) ;; insert object snapshot for for unknown frame (let [data {::th/type :create-file-object-thumbnail @@ -1148,22 +1148,29 @@ (th/db-exec! ["update file set has_media_trimmed=false where id=?" (:id file)]) ;; check that we have all object thumbnails - (let [res (th/db-exec! ["select * from file_tagged_object_thumbnail"])] - (t/is (= 2 (count res)))) + (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})] + ;; (app.common.pprint/pprint rows) + (t/is (= 2 (count rows)))) ;; run the task again - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) + + ;; check that we have all object thumbnails + (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})] + ;; (app.common.pprint/pprint rows) + (t/is (= 2 (count rows)))) + ;; check that the unknown frame thumbnail is deleted (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})] (t/is (= 2 (count rows))) - (t/is (= 1 (count (remove (comp some? :deleted-at) rows))))) + (t/is (= 1 (count (remove :deleted-at rows))))) (let [res (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 3 (:processed res)))) + (t/is (= 4 (:processed res)))) (let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})] + ;; (app.common.pprint/pprint rows) (t/is (= 1 (count rows))))))) (t/deftest file-thumbnail-ops @@ -1189,6 +1196,7 @@ (t/is (nil? error)) (t/is (map? result))) + ;; insert another thumbnail with different revn (let [data {::th/type :create-file-thumbnail ::rpc/profile-id (:id prof) :file-id (:id file) @@ -1207,10 +1215,7 @@ (t/is (= 2 (count rows))))) (t/testing "gc task" - ;; make the file eligible for GC waiting 300ms (configured - ;; timeout for testing) - (let [res (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed res)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (let [rows (th/db-query :file-thumbnail {:file-id (:id file)})] (t/is (= 2 (count rows))) @@ -1222,6 +1227,97 @@ (let [rows (th/db-query :file-thumbnail {:file-id (:id file)})] (t/is (= 1 (count rows))))))) +(t/deftest file-tiered-storage + (let [profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + + page-id (uuid/random) + shape-id (uuid/random)] + + ;; Preventive file-gc + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) + ;; Preventive objects-gc + (let [result (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 1 (:processed result)))) + + ;; Check the number of fragments before adding the page + (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] + (t/is (= 1 (count rows))) + (t/is (every? #(some? (:data %)) rows))) + ;; Mark the file ellegible again for GC + (th/db-update! :file + {:has-media-trimmed false} + {:id (:id file)}) + ;; Run FileGC again, with tiered storage activated + (with-redefs [app.config/flags (conj app.config/flags :tiered-file-data-storage)] + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) + + ;; The FileGC task will schedule an inner taskq + (th/run-pending-tasks!)) + + ;; Clean objects after file-gc + (let [result (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 1 (:processed result)))) + + ;; Check the number of fragments before adding the page + (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] + (t/is (= 1 (count rows))) + (t/is (every? #(nil? (:data %)) rows)) + (t/is (every? #(uuid? (:data-ref-id %)) rows)) + (t/is (every? #(= "objects-storage" (:data-backend %)) rows))) + + (let [file (th/db-get :file {:id (:id file)}) + storage (sto/resolve th/*system*)] + (t/is (= "objects-storage" (:data-backend file))) + (t/is (nil? (:data file))) + (t/is (uuid? (:data-ref-id file))) + + (let [sobj (sto/get-object storage (:data-ref-id file))] + (t/is (= "file-data" (:bucket (meta sobj)))) + (t/is (= (:id file) (:file-id (meta sobj)))))) + + ;; Add shape to page that should load from cold storage again into the hot storage (db) + (update-file! + :file-id (:id file) + :profile-id (:id profile) + :revn 0 + :changes + [{:type :add-page + :name "test" + :id page-id}]) + + ;; Check the number of fragments + (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] + (t/is (= 2 (count rows)))) + + ;; Check the number of fragments + (let [[row1 row2 :as rows] + (th/db-query :file-data-fragment + {:file-id (:id file) + :deleted-at nil} + {:order-by [:created-at]})] + ;; (pp/pprint rows) + (t/is (= 2 (count rows))) + (t/is (nil? (:data row1))) + (t/is (= "objects-storage" (:data-backend row1))) + (t/is (bytes? (:data row2))) + (t/is (nil? (:data-backend row2)))) + + ;; The file-gc should mark for remove unused fragments + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) + + ;; The objects-gc should remove unused fragments + (let [res (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 2 (:processed res)))) + + ;; Check the number of fragments before adding the page + (let [rows (th/db-query :file-data-fragment {:file-id (:id file)})] + (t/is (= 2 (count rows))) + (t/is (every? #(bytes? (:data %)) rows)) + (t/is (every? #(nil? (:data-ref-id %)) rows)) + (t/is (every? #(nil? (:data-backend %)) rows))))) diff --git a/backend/test/backend_tests/rpc_file_thumbnails_test.clj b/backend/test/backend_tests/rpc_file_thumbnails_test.clj index f0cfc963756625584414687738e110b8c4551753..2ceffbddfac3f4628705cc360cebb6b8a617c075 100644 --- a/backend/test/backend_tests/rpc_file_thumbnails_test.clj +++ b/backend/test/backend_tests/rpc_file_thumbnails_test.clj @@ -114,11 +114,10 @@ ;; Run the File GC task that should remove unused file object ;; thumbnails - (let [result (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed result)))) + (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}) (let [result (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 2 (:processed result)))) + (t/is (= 3 (:processed result)))) ;; check if row2 related thumbnail row still exists (let [[row :as rows] (th/db-query :file-tagged-object-thumbnail @@ -134,7 +133,7 @@ (t/is (some? (sto/get-object storage (:media-id row2)))) ;; run the task again - (let [res (th/run-task! "storage-gc-touched" {:min-age 0})] + (let [res (th/run-task! :storage-gc-touched {:min-age 0})] (t/is (= 1 (:delete res))) (t/is (= 0 (:freeze res)))) @@ -217,8 +216,7 @@ ;; Run the File GC task that should remove unused file object ;; thumbnails - (let [result (th/run-task! :file-gc {:min-age 0})] - (t/is (= 1 (:processed result)))) + (t/is (true? (th/run-task! :file-gc {:min-age 0 :file-id (:id file)}))) (let [result (th/run-task! :objects-gc {:min-age 0})] (t/is (= 2 (:processed result)))) @@ -277,8 +275,6 @@ (t/is (thrown? org.postgresql.util.PSQLException (th/db-delete! :storage-object {:id (:media-id row1)})))))) - - (t/deftest get-file-object-thumbnail (let [storage (::sto/storage th/*system*) profile (th/create-profile* 1) @@ -317,3 +313,36 @@ (let [result (:result out)] (t/is (contains? result "test-key-2")))))) + +(t/deftest create-file-object-thumbnail + (th/db-delete! :task {:name "object-update"}) + (let [storage (::sto/storage th/*system*) + profile (th/create-profile* 1) + file (th/create-file* 1 {:profile-id (:id profile) + :project-id (:default-project-id profile) + :is-shared false}) + data {::th/type :create-file-object-thumbnail + ::rpc/profile-id (:id profile) + :file-id (:id file) + :object-id "test-key-2" + :media {:filename "sample.jpg" + :mtype "image/jpeg"}}] + + (let [data (update data :media + (fn [media] + (-> media + (assoc :path (th/tempfile "backend_tests/test_files/sample2.jpg")) + (assoc :size 7923)))) + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))) + + (let [data (update data :media + (fn [media] + (-> media + (assoc :path (th/tempfile "backend_tests/test_files/sample.jpg")) + (assoc :size 312043)))) + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (map? (:result out)))))) + diff --git a/backend/test/backend_tests/rpc_font_test.clj b/backend/test/backend_tests/rpc_font_test.clj index 2d640443511bbdb621b79906c12d7880bdd052f7..ab9b57f4bc736aab318beea44147fdd1af98be82 100644 --- a/backend/test/backend_tests/rpc_font_test.clj +++ b/backend/test/backend_tests/rpc_font_test.clj @@ -145,7 +145,7 @@ (t/is (nil? (:result out)))) (let [res (th/run-task! :storage-gc-touched {:min-age 0})] - (t/is (= 6 (:freeze res))) + (t/is (= 0 (:freeze res))) (t/is (= 0 (:delete res)))) (let [res (th/run-task! :objects-gc {:min-age 0})] @@ -207,7 +207,7 @@ (t/is (nil? (:result out)))) (let [res (th/run-task! :storage-gc-touched {:min-age 0})] - (t/is (= 3 (:freeze res))) + (t/is (= 0 (:freeze res))) (t/is (= 0 (:delete res)))) (let [res (th/run-task! :objects-gc {:min-age 0})] @@ -268,7 +268,7 @@ (t/is (nil? (:result out)))) (let [res (th/run-task! :storage-gc-touched {:min-age 0})] - (t/is (= 3 (:freeze res))) + (t/is (= 0 (:freeze res))) (t/is (= 0 (:delete res)))) (let [res (th/run-task! :objects-gc {:min-age 0})] diff --git a/backend/test/backend_tests/rpc_management_test.clj b/backend/test/backend_tests/rpc_management_test.clj index 63018af3337a9cd3da80e0e07d331a84f85cdda2..e4dc70dacb2e7a4f2464dcb36c79db278f6554f3 100644 --- a/backend/test/backend_tests/rpc_management_test.clj +++ b/backend/test/backend_tests/rpc_management_test.clj @@ -612,7 +612,7 @@ (t/is (fn? result)) (let [events (th/consume-sse result)] - (t/is (= 6 (count events))) + (t/is (= 5 (count events))) (t/is (= :end (first (last events)))))))) (t/deftest get-list-of-buitin-templates diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index cbaff60380eecc0be73bb094532c94838bc61151..1bd49db4855892f1ae362bac5034a846585b35de 100644 --- a/backend/test/backend_tests/rpc_profile_test.clj +++ b/backend/test/backend_tests/rpc_profile_test.clj @@ -6,10 +6,11 @@ (ns backend-tests.rpc-profile-test (:require - [app.auth :as auth] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] + [app.email.blacklist :as email.blacklist] + [app.email.whitelist :as email.whitelist] [app.rpc :as-alias rpc] [app.rpc.commands.profile :as profile] [app.tokens :as tokens] @@ -27,6 +28,14 @@ (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) + +(t/deftest clean-email + (t/is "foo@example.com" (profile/clean-email "mailto:foo@example.com")) + (t/is "foo@example.com" (profile/clean-email "mailto:")) + (t/is "foo@example.com" (profile/clean-email "")) + (t/is "foo@example.com" (profile/clean-email "foo@example.com>")) + (t/is "foo@example.com" (profile/clean-email " out :error th/ex-type))) - (t/is (= :email-already-exists (-> out :error th/ex-code)))) + (t/is (= 1 (:call-count @mock)))))))) - (th/sleep {:millis 500}) - (th/reset-mock! mock) +(t/deftest prepare-register-and-register-profile-3 + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [current-token (atom nil)] + ;; PREPARE REGISTER + (let [data {::th/type :prepare-register-profile + :email "hello@example.com" + :password "foobar"} + out (th/command! data) + token (get-in out [:result :token])] + (t/is (th/success? out)) + (reset! current-token token)) + + ;; DO REGISTRATION + (let [data {::th/type :register-profile + :token @current-token + :fullname "foobar" + :accept-terms-and-privacy true + :accept-newsletter-subscription true} + out (th/command! data)] + (t/is (nil? (:error out))) + (t/is (= 1 (:call-count @mock)))) - ;; PREPARE REGISTER waiting the threshold - (let [data {::th/type :prepare-register-profile - :email "hello@example.com" - :password "foobar"} - out (th/command! data)] + (th/reset-mock! mock) - (t/is (th/success? out)) - (t/is (= 0 (:call-count @mock))) + (th/db-update! :profile + {:is-blocked true} + {:email "hello@example.com"}) - (let [result (:result out)] - (t/is (contains? result :token)) - (reset! current-token (:token result)))) + ;; PREPARE REGISTER: second attempt + (let [data {::th/type :prepare-register-profile + :email "hello@example.com" + :password "foobar"} + out (th/command! data) + token (get-in out [:result :token])] + (t/is (th/success? out)) + (reset! current-token token)) - ;; DO REGISTRATION: try correct register attempt 1 + (with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold? + :return true}] + ;; DO REGISTRATION: second attempt (let [data {::th/type :register-profile :token @current-token :fullname "foobar" :accept-terms-and-privacy true :accept-newsletter-subscription true} out (th/command! data)] - (t/is (th/success? out)) - (t/is (= 1 (:call-count @mock)))))))) + (t/is (nil? (:error out))) + (t/is (= 0 (:call-count @mock)))))))) + +(t/deftest prepare-and-register-with-invitation-and-enabled-registration-1 + (let [sprops (:app.setup/props th/*system*) + itoken (tokens/generate sprops + {:iss :team-invitation + :exp (dt/in-future "48h") + :role :editor + :team-id uuid/zero + :member-email "user@example.com"}) + data {::th/type :prepare-register-profile + :invitation-token itoken + :email "user@example.com" + :password "foobar"} + + {:keys [result error] :as out} (th/command! data)] + (t/is (nil? error)) + (t/is (map? result)) + (t/is (string? (:token result))) + (let [rtoken (:token result) + data {::th/type :register-profile + :token rtoken + :fullname "foobar"} + + {:keys [result error] :as out} (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? error)) + (t/is (map? result)) + (t/is (string? (:invitation-token result)))))) + +(t/deftest prepare-and-register-with-invitation-and-enabled-registration-2 + (let [sprops (:app.setup/props th/*system*) + itoken (tokens/generate sprops + {:iss :team-invitation + :exp (dt/in-future "48h") + :role :editor + :team-id uuid/zero + :member-email "user2@example.com"}) + + data {::th/type :prepare-register-profile + :invitation-token itoken + :email "user@example.com" + :password "foobar"} + out (th/command! data)] + + (t/is (not (th/success? out))) + (let [edata (-> out :error ex-data)] + (t/is (= :restriction (:type edata))) + (t/is (= :email-does-not-match-invitation (:code edata)))))) (t/deftest prepare-and-register-with-invitation-and-disabled-registration-1 (with-redefs [app.config/flags [:disable-registration]] @@ -295,22 +567,12 @@ :invitation-token itoken :email "user@example.com" :password "foobar"} + out (th/command! data)] - {:keys [result error] :as out} (th/command! data)] - (t/is (nil? error)) - (t/is (map? result)) - (t/is (string? (:token result))) - - (let [rtoken (:token result) - data {::th/type :register-profile - :token rtoken - :fullname "foobar"} - - {:keys [result error] :as out} (th/command! data)] - ;; (th/print-result! out) - (t/is (nil? error)) - (t/is (map? result)) - (t/is (string? (:invitation-token result))))))) + (t/is (not (th/success? out))) + (let [edata (-> out :error ex-data)] + (t/is (= :restriction (:type edata))) + (t/is (= :registration-disabled (:code edata))))))) (t/deftest prepare-and-register-with-invitation-and-disabled-registration-2 (with-redefs [app.config/flags [:disable-registration]] @@ -331,7 +593,28 @@ (t/is (not (th/success? out))) (let [edata (-> out :error ex-data)] (t/is (= :restriction (:type edata))) - (t/is (= :email-does-not-match-invitation (:code edata))))))) + (t/is (= :registration-disabled (:code edata))))))) + +(t/deftest prepare-and-register-with-invitation-and-disabled-login-with-password + (with-redefs [app.config/flags [:disable-login-with-password]] + (let [sprops (:app.setup/props th/*system*) + itoken (tokens/generate sprops + {:iss :team-invitation + :exp (dt/in-future "48h") + :role :editor + :team-id uuid/zero + :member-email "user2@example.com"}) + + data {::th/type :prepare-register-profile + :invitation-token itoken + :email "user@example.com" + :password "foobar"} + out (th/command! data)] + + (t/is (not (th/success? out))) + (let [edata (-> out :error ex-data)] + (t/is (= :restriction (:type edata))) + (t/is (= :registration-disabled (:code edata))))))) (t/deftest prepare-register-with-registration-disabled (with-redefs [app.config/flags #{}] @@ -351,13 +634,13 @@ :email (:email profile) :password "foobar"} out (th/command! data)] + ;; (th/print-result! out) + (t/is (th/success? out)) + (let [result (:result out)] + (t/is (contains? result :token))))) - (t/is (not (th/success? out))) - (let [edata (-> out :error ex-data)] - (t/is (= :validation (:type edata))) - (t/is (= :email-already-exists (:code edata)))))) +(t/deftest prepare-register-profile-with-bounced-email -(t/deftest register-profile-with-bounced-email (let [pool (:app.db/pool th/*system*) data {::th/type :prepare-register-profile :email "user@example.com" @@ -368,7 +651,7 @@ (let [out (th/command! data)] (t/is (not (th/success? out))) (let [edata (-> out :error ex-data)] - (t/is (= :validation (:type edata))) + (t/is (= :restriction (:type edata))) (t/is (= :email-has-permanent-bounces (:code edata))))))) (t/deftest register-profile-with-complained-email @@ -380,9 +663,11 @@ (th/create-global-complaint-for pool {:type :complaint :email "user@example.com"}) (let [out (th/command! data)] - (t/is (th/success? out)) - (let [result (:result out)] - (t/is (contains? result :token)))))) + (t/is (not (th/success? out))) + + (let [edata (-> out :error ex-data)] + (t/is (= :restriction (:type edata))) + (t/is (= :email-has-complaints (:code edata))))))) (t/deftest register-profile-with-email-as-password (let [data {::th/type :prepare-register-profile @@ -413,20 +698,26 @@ ;; with complaints (th/create-global-complaint-for pool {:type :complaint :email (:email data)}) - (let [out (th/command! data)] + (let [out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:result out))) - (t/is (= 2 (:call-count @mock)))) + + (let [edata (-> out :error ex-data)] + (t/is (= :restriction (:type edata))) + (t/is (= :email-has-complaints (:code edata)))) + + (t/is (= 1 (:call-count @mock)))) ;; with bounces (th/create-global-complaint-for pool {:type :bounce :email (:email data)}) - (let [out (th/command! data) - error (:error out)] + (let [out (th/command! data)] ;; (th/print-result! out) - (t/is (th/ex-info? error)) - (t/is (th/ex-of-type? error :validation)) - (t/is (th/ex-of-code? error :email-has-permanent-bounces)) - (t/is (= 2 (:call-count @mock))))))) + + (let [edata (-> out :error ex-data)] + (t/is (= :restriction (:type edata))) + (t/is (= :email-has-permanent-bounces (:code edata)))) + + (t/is (= 1 (:call-count @mock))))))) (t/deftest email-change-request-without-smtp @@ -447,7 +738,7 @@ (t/deftest request-profile-recovery (with-mocks [mock {:target 'app.email/send! :return nil}] - (let [profile1 (th/create-profile* 1) + (let [profile1 (th/create-profile* 1 {:is-active false}) profile2 (th/create-profile* 2 {:is-active true}) pool (:app.db/pool th/*system*) data {::th/type :request-profile-recovery}] @@ -460,38 +751,47 @@ ;; with valid email inactive user (let [data (assoc data :email (:email profile1)) - out (th/command! data) - error (:error out)] + out (th/command! data)] (t/is (= 0 (:call-count @mock))) - (t/is (th/ex-info? error)) - (t/is (th/ex-of-type? error :validation)) - (t/is (th/ex-of-code? error :profile-not-verified))) + (t/is (nil? (:result out))) + (t/is (nil? (:error out)))) + + (with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold? + :return true}] + ;; with valid email inactive user + (let [data (assoc data :email (:email profile1)) + out (th/command! data)] + (t/is (= 1 (:call-count @mock))) + (t/is (nil? (:result out))) + (t/is (nil? (:error out))))) + + (th/reset-mock! mock) ;; with valid email and active user - (let [data (assoc data :email (:email profile2)) - out (th/command! data)] - ;; (th/print-result! out) - (t/is (nil? (:result out))) - (t/is (= 1 (:call-count @mock)))) + (with-mocks [_ {:target 'app.rpc.commands.auth/elapsed-verify-threshold? + :return true}] + (let [data (assoc data :email (:email profile2)) + out (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? (:result out))) + (t/is (= 1 (:call-count @mock)))) - ;; with valid email and active user with global complaints - (th/create-global-complaint-for pool {:type :complaint :email (:email profile2)}) - (let [data (assoc data :email (:email profile2)) - out (th/command! data)] - ;; (th/print-result! out) - (t/is (nil? (:result out))) - (t/is (= 2 (:call-count @mock)))) + ;; with valid email and active user with global complaints + (th/create-global-complaint-for pool {:type :complaint :email (:email profile2)}) + (let [data (assoc data :email (:email profile2)) + out (th/command! data)] + ;; (th/print-result! out) + (t/is (nil? (:result out))) + (t/is (= 1 (:call-count @mock)))) - ;; with valid email and active user with global bounce - (th/create-global-complaint-for pool {:type :bounce :email (:email profile2)}) - (let [data (assoc data :email (:email profile2)) - out (th/command! data) - error (:error out)] - ;; (th/print-result! out) - (t/is (= 2 (:call-count @mock))) - (t/is (th/ex-info? error)) - (t/is (th/ex-of-type? error :validation)) - (t/is (th/ex-of-code? error :email-has-permanent-bounces)))))) + ;; with valid email and active user with global bounce + (th/create-global-complaint-for pool {:type :bounce :email (:email profile2)}) + (let [data (assoc data :email (:email profile2)) + out (th/command! data)] + (t/is (nil? (:result out))) + (t/is (nil? (:error out))) + ;; (th/print-result! out) + (t/is (= 1 (:call-count @mock)))))))) (t/deftest update-profile-password diff --git a/backend/test/backend_tests/rpc_team_test.clj b/backend/test/backend_tests/rpc_team_test.clj index 65acef49d8bf57d352dc409c77ab8f5f7e6d242d..dd614151e8f36506373369ce9cb178d9189b53d2 100644 --- a/backend/test/backend_tests/rpc_team_test.clj +++ b/backend/test/backend_tests/rpc_team_test.clj @@ -62,8 +62,8 @@ (th/reset-mock! mock) (let [data (assoc data :emails ["foo@bar.com"]) out (th/command! data)] - (t/is (th/success? out)) - (t/is (= 1 (:call-count (deref mock))))) + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count (deref mock))))) ;; get invitation token (let [params {::th/type :get-team-invitation-token @@ -86,7 +86,7 @@ (t/is (= 0 (:call-count @mock))) (let [edata (-> out :error ex-data)] - (t/is (= :validation (:type edata))) + (t/is (= :restriction (:type edata))) (t/is (= :email-has-permanent-bounces (:code edata))))) ;; invite internal user that is muted @@ -260,6 +260,7 @@ (th/reset-mock! mock) (let [data (assoc data :emails [(:email profile2)]) out (th/command! data)] + ;; (th/print-result! out) (t/is (th/success? out)) (t/is (= 0 (:call-count (deref mock))))) @@ -391,6 +392,8 @@ (t/is (= 1 (count result))) (t/is (= (:default-team-id profile1) (get-in result [0 :id]))))) + (th/run-pending-tasks!) + ;; run permanent deletion (should be noop) (let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})] (t/is (= 0 (:processed result)))) @@ -457,9 +460,154 @@ #_(th/print-result! out) (t/is (nil? (:error out)))) + (th/run-pending-tasks!) + (let [rows (th/db-exec! ["select * from team where id = ?" (:id team)])] (t/is (= 1 (count rows))) (t/is (dt/instant? (:deleted-at (first rows))))) (let [result (th/run-task! :objects-gc {:min-age 0})] (t/is (= 5 (:processed result)))))) + +(t/deftest create-team-access-request + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [owner (th/create-profile* 1 {:is-active true :email "owner@bar.com"}) + requester (th/create-profile* 3 {:is-active true :email "requester@bar.com"}) + team (th/create-team* 1 {:profile-id (:id owner)}) + proj (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id proj)}) + + data {::th/type :create-team-access-request + ::rpc/profile-id (:id requester) + :file-id (:id file)}] + + ;; request success + (let [out (th/command! data) + ;; retrieve the value from the database and check its content + request (db/exec-one! + th/*pool* + ["select count(*) as num from team_access_request where team_id = ? and requester_id = ?" + (:id team) (:id requester)])] + + (t/is (th/success? out)) + (t/is (= 1 (:call-count @mock))) + (t/is (= 1 (:num request)))) + + ;; request again fails + (th/reset-mock! mock) + (let [out (th/command! data) + edata (-> out :error ex-data)] + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count @mock))) + + (t/is (= :validation (:type edata))) + (t/is (= :request-already-sent (:code edata)))) + + + ;; request again when is expired success + (th/reset-mock! mock) + + (db/exec-one! + th/*pool* + ["update team_access_request set valid_until = ? where team_id = ? and requester_id = ?" + (dt/in-past "1h") (:id team) (:id requester)]) + + (t/is (th/success? (th/command! data))) + (t/is (= 1 (:call-count @mock)))))) + + +(t/deftest create-team-access-request-owner-muted + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [owner (th/create-profile* 1 {:is-active true :is-muted true :email "owner@bar.com"}) + requester (th/create-profile* 2 {:is-active true :email "requester@bar.com"}) + team (th/create-team* 1 {:profile-id (:id owner)}) + proj (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id proj)}) + + data {::th/type :create-team-access-request + ::rpc/profile-id (:id requester) + :file-id (:id file)}] + + ;; request to team with owner muted should success + (t/is (th/success? (th/command! data))) + (t/is (= 1 (:call-count @mock)))))) + + +(t/deftest create-team-access-request-requester-muted + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [owner (th/create-profile* 1 {:is-active true :email "owner@bar.com"}) + requester (th/create-profile* 2 {:is-active true :is-muted true :email "requester@bar.com"}) + team (th/create-team* 1 {:profile-id (:id owner)}) + proj (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id proj)}) + + data {::th/type :create-team-access-request + ::rpc/profile-id (:id requester) + :file-id (:id file)} + + out (th/command! data) + edata (-> out :error ex-data)] + + ;; request with requester muted should fail + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count @mock))) + + (t/is (= :validation (:type edata))) + (t/is (= :member-is-muted (:code edata))) + (t/is (= (:email requester) (:email edata)))))) + + +(t/deftest create-team-access-request-owner-bounce + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [owner (th/create-profile* 1 {:is-active true :email "owner@bar.com"}) + requester (th/create-profile* 2 {:is-active true :email "requester@bar.com"}) + team (th/create-team* 1 {:profile-id (:id owner)}) + proj (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id proj)}) + + pool (:app.db/pool th/*system*) + data {::th/type :create-team-access-request + ::rpc/profile-id (:id requester) + :file-id (:id file)}] + + + (th/create-global-complaint-for pool {:type :bounce :email "owner@bar.com"}) + (let [out (th/command! data) + edata (-> out :error ex-data)] + + ;; request with owner bounce should fail + (t/is (not (th/success? out))) + (t/is (= 0 (:call-count @mock))) + + (t/is (= :restriction (:type edata))) + (t/is (= :email-has-permanent-bounces (:code edata))) + (t/is (= "private" (:email edata))))))) + +(t/deftest create-team-access-request-requester-bounce + (with-mocks [mock {:target 'app.email/send! :return nil}] + (let [owner (th/create-profile* 1 {:is-active true :email "owner@bar.com"}) + requester (th/create-profile* 2 {:is-active true :email "requester@bar.com"}) + team (th/create-team* 1 {:profile-id (:id owner)}) + proj (th/create-project* 1 {:profile-id (:id owner) + :team-id (:id team)}) + file (th/create-file* 1 {:profile-id (:id owner) + :project-id (:id proj)}) + + pool (:app.db/pool th/*system*) + data {::th/type :create-team-access-request + ::rpc/profile-id (:id requester) + :file-id (:id file)}] + + ;; request with requester bounce should success + (th/create-global-complaint-for pool {:type :bounce :email "requester@bar.com"}) + (t/is (th/success? (th/command! data))) + (t/is (= 1 (:call-count @mock)))))) + diff --git a/backend/test/backend_tests/rpc_webhooks_test.clj b/backend/test/backend_tests/rpc_webhooks_test.clj index 76c3de763c9ec6662efbb34d5162abb4b29549bf..c020c54854d4adae0a0c3366efe3caa8cff9db18 100644 --- a/backend/test/backend_tests/rpc_webhooks_test.clj +++ b/backend/test/backend_tests/rpc_webhooks_test.clj @@ -39,6 +39,8 @@ (t/is (nil? (:error out))) (t/is (= 1 (:call-count @http-mock))) + ;; (th/print-result! out) + (let [result (:result out)] (t/is (contains? result :id)) (t/is (contains? result :team-id)) @@ -164,7 +166,6 @@ out9 (th/command! params)] (t/is (= 8 (:call-count @http-mock))) - (t/is (nil? (:error out1))) (t/is (nil? (:error out2))) (t/is (nil? (:error out3))) diff --git a/backend/tests.edn b/backend/tests.edn index 6d31eb3c651beed8e6a60983d87bf13d5c28b109..7af172c443186459ec93e3239ae9b6f81ee37c97 100644 --- a/backend/tests.edn +++ b/backend/tests.edn @@ -2,4 +2,5 @@ {:tests [{:id :unit :test-paths ["test" "src"] - :ns-patterns [".*-test$"]}]} + :ns-patterns [".*-test$"] + :kaocha/reporter [kaocha.report/dots]}]} diff --git a/backend/yarn.lock b/backend/yarn.lock index 1144439c639d83047277b707374640c8059b460b..7e062d72757f603daa2f7c81c0e6743aa4efb423 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -15,59 +15,52 @@ __metadata: strip-ansi-cjs: "npm:strip-ansi@^6.0.1" wrap-ansi: "npm:^8.1.0" wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" - checksum: b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e languageName: node linkType: hard "@npmcli/agent@npm:^2.0.0": - version: 2.2.0 - resolution: "@npmcli/agent@npm:2.2.0" + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" dependencies: agent-base: "npm:^7.1.0" http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.1" lru-cache: "npm:^10.0.1" - socks-proxy-agent: "npm:^8.0.1" - checksum: 7b89590598476dda88e79c473766b67c682aae6e0ab0213491daa6083dcc0c171f86b3868f5506f22c09aa5ea69ad7dfb78f4bf39a8dca375d89a42f408645b3 + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae languageName: node linkType: hard "@npmcli/fs@npm:^3.1.0": - version: 3.1.0 - resolution: "@npmcli/fs@npm:3.1.0" + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" dependencies: semver: "npm:^7.3.5" - checksum: 162b4a0b8705cd6f5c2470b851d1dc6cd228c86d2170e1769d738c1fbb69a87160901411c3c035331e9e99db72f1f1099a8b734bf1637cc32b9a5be1660e4e1e + checksum: 10c0/c37a5b4842bfdece3d14dfdb054f73fe15ed2d3da61b34ff76629fb5b1731647c49166fd2a8bf8b56fcfa51200382385ea8909a3cbecdad612310c114d3f6c99 languageName: node linkType: hard "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd - languageName: node - linkType: hard - -"abbrev@npm:1": - version: 1.1.1 - resolution: "abbrev@npm:1.1.1" - checksum: 3f762677702acb24f65e813070e306c61fafe25d4b2583f9dfc935131f774863f3addd5741572ed576bd69cabe473c5af18e1e108b829cb7b6b4747884f726e6 + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd languageName: node linkType: hard "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" - checksum: f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + checksum: 10c0/f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 languageName: node linkType: hard -"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": - version: 7.1.0 - resolution: "agent-base@npm:7.1.0" +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" dependencies: debug: "npm:^4.3.4" - checksum: fc974ab57ffdd8421a2bc339644d312a9cca320c20c3393c9d8b1fd91731b9bbabdb985df5fc860f5b79d81c3e350daa3fcb31c5c07c0bb385aafc817df004ce + checksum: 10c0/e59ce7bed9c63bf071a30cc471f2933862044c97fd9958967bfe22521d7a0f601ce4ed5a8c011799d0c726ca70312142ae193bbebb60f576b52be19d4a363b50 languageName: node linkType: hard @@ -77,21 +70,21 @@ __metadata: dependencies: clean-stack: "npm:^2.0.0" indent-string: "npm:^4.0.0" - checksum: a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 languageName: node linkType: hard "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" - checksum: 9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 languageName: node linkType: hard "ansi-regex@npm:^6.0.1": version: 6.0.1 resolution: "ansi-regex@npm:6.0.1" - checksum: cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 + checksum: 10c0/cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 languageName: node linkType: hard @@ -100,14 +93,14 @@ __metadata: resolution: "ansi-styles@npm:4.3.0" dependencies: color-convert: "npm:^2.0.1" - checksum: 895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 languageName: node linkType: hard "ansi-styles@npm:^6.1.0": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" - checksum: 5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c languageName: node linkType: hard @@ -117,7 +110,7 @@ __metadata: dependencies: normalize-path: "npm:^3.0.0" picomatch: "npm:^2.0.4" - checksum: 57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac languageName: node linkType: hard @@ -125,25 +118,25 @@ __metadata: version: 0.0.0-use.local resolution: "backend@workspace:." dependencies: - luxon: "npm:^3.4.2" - nodemon: "npm:^3.0.1" - sax: "npm:^1.2.4" + luxon: "npm:^3.4.4" + nodemon: "npm:^3.1.2" + sax: "npm:^1.4.1" source-map-support: "npm:^0.5.21" - ws: "npm:^8.13.0" + ws: "npm:^8.17.0" languageName: unknown linkType: soft "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" - checksum: 9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee languageName: node linkType: hard "binary-extensions@npm:^2.0.0": - version: 2.2.0 - resolution: "binary-extensions@npm:2.2.0" - checksum: d73d8b897238a2d3ffa5f59c0241870043aa7471335e89ea5e1ff48edb7c2d0bb471517a3e4c5c3f4c043615caa2717b5f80a5e61e07503d51dc85cb848e665d + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 languageName: node linkType: hard @@ -153,7 +146,7 @@ __metadata: dependencies: balanced-match: "npm:^1.0.0" concat-map: "npm:0.0.1" - checksum: 695a56cd058096a7cb71fb09d9d6a7070113c7be516699ed361317aca2ec169f618e28b8af352e02ab4233fb54eb0168460a40dc320bab0034b36ab59aaad668 + checksum: 10c0/695a56cd058096a7cb71fb09d9d6a7070113c7be516699ed361317aca2ec169f618e28b8af352e02ab4233fb54eb0168460a40dc320bab0034b36ab59aaad668 languageName: node linkType: hard @@ -162,29 +155,29 @@ __metadata: resolution: "brace-expansion@npm:2.0.1" dependencies: balanced-match: "npm:^1.0.0" - checksum: b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f languageName: node linkType: hard "braces@npm:~3.0.2": - version: 3.0.2 - resolution: "braces@npm:3.0.2" + version: 3.0.3 + resolution: "braces@npm:3.0.3" dependencies: - fill-range: "npm:^7.0.1" - checksum: 321b4d675791479293264019156ca322163f02dc06e3c4cab33bb15cd43d80b51efef69b0930cfde3acd63d126ebca24cd0544fa6f261e093a0fb41ab9dda381 + fill-range: "npm:^7.1.1" + checksum: 10c0/7c6dfd30c338d2997ba77500539227b9d1f85e388a5f43220865201e407e076783d0881f2d297b9f80951b4c957fcf0b51c1d2d24227631643c3f7c284b0aa04 languageName: node linkType: hard "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" - checksum: 124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 + checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 languageName: node linkType: hard "cacache@npm:^18.0.0": - version: 18.0.1 - resolution: "cacache@npm:18.0.1" + version: 18.0.3 + resolution: "cacache@npm:18.0.3" dependencies: "@npmcli/fs": "npm:^3.1.0" fs-minipass: "npm:^3.0.0" @@ -198,13 +191,13 @@ __metadata: ssri: "npm:^10.0.0" tar: "npm:^6.1.11" unique-filename: "npm:^3.0.0" - checksum: a31666805a80a8b16ad3f85faf66750275a9175a3480896f4f6d31b5d53ef190484fabd71bdb6d2ea5603c717fbef09f4af03d6a65b525c8ef0afaa44c361866 + checksum: 10c0/dfda92840bb371fb66b88c087c61a74544363b37a265023223a99965b16a16bbb87661fe4948718d79df6e0cc04e85e62784fbcf1832b2a5e54ff4c46fbb45b7 languageName: node linkType: hard "chokidar@npm:^3.5.2": - version: 3.5.3 - resolution: "chokidar@npm:3.5.3" + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" dependencies: anymatch: "npm:~3.1.2" braces: "npm:~3.0.2" @@ -217,21 +210,21 @@ __metadata: dependenciesMeta: fsevents: optional: true - checksum: 1076953093e0707c882a92c66c0f56ba6187831aa51bb4de878c1fec59ae611a3bf02898f190efec8e77a086b8df61c2b2a3ea324642a0558bdf8ee6c5dc9ca1 + checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 languageName: node linkType: hard "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" - checksum: 594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + checksum: 10c0/594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 languageName: node linkType: hard "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" - checksum: 1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 languageName: node linkType: hard @@ -240,21 +233,21 @@ __metadata: resolution: "color-convert@npm:2.0.1" dependencies: color-name: "npm:~1.1.4" - checksum: 37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 languageName: node linkType: hard "color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" - checksum: a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 languageName: node linkType: hard "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" - checksum: c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f + checksum: 10c0/c996b1cfdf95b6c90fee4dae37e332c8b6eb7d106430c17d538034c0ad9a1630cb194d2ab37293b1bdd4d779494beee7786d586a50bd9376fd6f7bcc2bd4c98f languageName: node linkType: hard @@ -265,11 +258,11 @@ __metadata: path-key: "npm:^3.1.0" shebang-command: "npm:^2.0.0" which: "npm:^2.0.1" - checksum: 5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + checksum: 10c0/5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.3.4": +"debug@npm:4, debug@npm:^4, debug@npm:^4.3.4": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -277,37 +270,28 @@ __metadata: peerDependenciesMeta: supports-color: optional: true - checksum: cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 - languageName: node - linkType: hard - -"debug@npm:^3.2.7": - version: 3.2.7 - resolution: "debug@npm:3.2.7" - dependencies: - ms: "npm:^2.1.1" - checksum: 37d96ae42cbc71c14844d2ae3ba55adf462ec89fd3a999459dec3833944cd999af6007ff29c780f1c61153bcaaf2c842d1e4ce1ec621e4fc4923244942e4a02a + checksum: 10c0/cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 languageName: node linkType: hard "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" - checksum: 26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 languageName: node linkType: hard "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" - checksum: b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 languageName: node linkType: hard "emoji-regex@npm:^9.2.2": version: 9.2.2 resolution: "emoji-regex@npm:9.2.2" - checksum: af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 languageName: node linkType: hard @@ -316,37 +300,37 @@ __metadata: resolution: "encoding@npm:0.1.13" dependencies: iconv-lite: "npm:^0.6.2" - checksum: 36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 languageName: node linkType: hard "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" - checksum: 285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 languageName: node linkType: hard "err-code@npm:^2.0.2": version: 2.0.3 resolution: "err-code@npm:2.0.3" - checksum: b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 languageName: node linkType: hard "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" - checksum: 160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + checksum: 10c0/160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 languageName: node linkType: hard -"fill-range@npm:^7.0.1": - version: 7.0.1 - resolution: "fill-range@npm:7.0.1" +"fill-range@npm:^7.1.1": + version: 7.1.1 + resolution: "fill-range@npm:7.1.1" dependencies: to-regex-range: "npm:^5.0.1" - checksum: 7cdad7d426ffbaadf45aeb5d15ec675bbd77f7597ad5399e3d2766987ed20bda24d5fac64b3ee79d93276f5865608bb22344a26b9b1ae6c4d00bd94bf611623f + checksum: 10c0/b75b691bbe065472f38824f694c2f7449d7f5004aa950426a2c28f0306c60db9b880c0b0e4ed819997ffb882d1da02cfcfc819bddc94d71627f5269682edf018 languageName: node linkType: hard @@ -356,7 +340,7 @@ __metadata: dependencies: cross-spawn: "npm:^7.0.0" signal-exit: "npm:^4.0.1" - checksum: 9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 + checksum: 10c0/9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 languageName: node linkType: hard @@ -365,7 +349,7 @@ __metadata: resolution: "fs-minipass@npm:2.1.0" dependencies: minipass: "npm:^3.0.0" - checksum: 703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 languageName: node linkType: hard @@ -374,7 +358,7 @@ __metadata: resolution: "fs-minipass@npm:3.0.3" dependencies: minipass: "npm:^7.0.3" - checksum: 63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 languageName: node linkType: hard @@ -383,7 +367,7 @@ __metadata: resolution: "fsevents@npm:2.3.3" dependencies: node-gyp: "npm:latest" - checksum: a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 + checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 conditions: os=darwin languageName: node linkType: hard @@ -402,63 +386,63 @@ __metadata: resolution: "glob-parent@npm:5.1.2" dependencies: is-glob: "npm:^4.0.1" - checksum: cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee + checksum: 10c0/cab87638e2112bee3f839ef5f6e0765057163d39c66be8ec1602f3823da4692297ad4e972de876ea17c44d652978638d2fd583c6713d0eb6591706825020c9ee languageName: node linkType: hard "glob@npm:^10.2.2, glob@npm:^10.3.10": - version: 10.3.10 - resolution: "glob@npm:10.3.10" + version: 10.3.16 + resolution: "glob@npm:10.3.16" dependencies: foreground-child: "npm:^3.1.0" - jackspeak: "npm:^2.3.5" + jackspeak: "npm:^3.1.2" minimatch: "npm:^9.0.1" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry: "npm:^1.10.1" + minipass: "npm:^7.0.4" + path-scurry: "npm:^1.11.0" bin: glob: dist/esm/bin.mjs - checksum: 13d8a1feb7eac7945f8c8480e11cd4a44b24d26503d99a8d8ac8d5aefbf3e9802a2b6087318a829fad04cb4e829f25c5f4f1110c68966c498720dd261c7e344d + checksum: 10c0/f7eb4c3e66f221f0be3967c02527047167967549bdf8ed1bd5f6277d43a35191af4e2bb8c89f07a79664958bae088fd06659e69a0f1de462972f1eab52a715e8 languageName: node linkType: hard "graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" - checksum: 386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 languageName: node linkType: hard "has-flag@npm:^3.0.0": version: 3.0.0 resolution: "has-flag@npm:3.0.0" - checksum: 1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473 + checksum: 10c0/1c6c83b14b8b1b3c25b0727b8ba3e3b647f99e9e6e13eb7322107261de07a4c1be56fc0d45678fc376e09772a3a1642ccdaf8fc69bdf123b6c086598397ce473 languageName: node linkType: hard "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" - checksum: ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc languageName: node linkType: hard "http-proxy-agent@npm:^7.0.0": - version: 7.0.0 - resolution: "http-proxy-agent@npm:7.0.0" + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" dependencies: agent-base: "npm:^7.1.0" debug: "npm:^4.3.4" - checksum: a11574ff39436cee3c7bc67f259444097b09474605846ddd8edf0bf4ad8644be8533db1aa463426e376865047d05dc22755e638632819317c0c2f1b2196657c8 + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 languageName: node linkType: hard "https-proxy-agent@npm:^7.0.1": - version: 7.0.2 - resolution: "https-proxy-agent@npm:7.0.2" + version: 7.0.4 + resolution: "https-proxy-agent@npm:7.0.4" dependencies: agent-base: "npm:^7.0.2" debug: "npm:4" - checksum: 7735eb90073db087e7e79312e3d97c8c04baf7ea7ca7b013382b6a45abbaa61b281041a98f4e13c8c80d88f843785bcc84ba189165b4b4087b1e3496ba656d77 + checksum: 10c0/bc4f7c38da32a5fc622450b6cb49a24ff596f9bd48dcedb52d2da3fa1c1a80e100fb506bd59b326c012f21c863c69b275c23de1a01d0b84db396822fdf25e52b languageName: node linkType: hard @@ -467,35 +451,38 @@ __metadata: resolution: "iconv-lite@npm:0.6.3" dependencies: safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 languageName: node linkType: hard "ignore-by-default@npm:^1.0.1": version: 1.0.1 resolution: "ignore-by-default@npm:1.0.1" - checksum: 9ab6e70e80f7cc12735def7ecb5527cfa56ab4e1152cd64d294522827f2dcf1f6d85531241537dc3713544e88dd888f65cb3c49c7b2cddb9009087c75274e533 + checksum: 10c0/9ab6e70e80f7cc12735def7ecb5527cfa56ab4e1152cd64d294522827f2dcf1f6d85531241537dc3713544e88dd888f65cb3c49c7b2cddb9009087c75274e533 languageName: node linkType: hard "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" - checksum: 8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 languageName: node linkType: hard "indent-string@npm:^4.0.0": version: 4.0.0 resolution: "indent-string@npm:4.0.0" - checksum: 1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f languageName: node linkType: hard -"ip@npm:^2.0.0": - version: 2.0.0 - resolution: "ip@npm:2.0.0" - checksum: 8d186cc5585f57372847ae29b6eba258c68862055e18a75cc4933327232cb5c107f89800ce29715d542eef2c254fbb68b382e780a7414f9ee7caf60b7a473958 +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc languageName: node linkType: hard @@ -504,21 +491,21 @@ __metadata: resolution: "is-binary-path@npm:2.1.0" dependencies: binary-extensions: "npm:^2.0.0" - checksum: a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 languageName: node linkType: hard "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" - checksum: 5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 + checksum: 10c0/5487da35691fbc339700bbb2730430b07777a3c21b9ebaecb3072512dfd7b4ba78ac2381a87e8d78d20ea08affb3f1971b4af629173a6bf435ff8a4c47747912 languageName: node linkType: hard "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" - checksum: bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc languageName: node linkType: hard @@ -527,77 +514,75 @@ __metadata: resolution: "is-glob@npm:4.0.3" dependencies: is-extglob: "npm:^2.1.1" - checksum: 17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a + checksum: 10c0/17fb4014e22be3bbecea9b2e3a76e9e34ff645466be702f1693e8f1ee1adac84710d0be0bd9f967d6354036fd51ab7c2741d954d6e91dae6bb69714de92c197a languageName: node linkType: hard "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" - checksum: 85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d languageName: node linkType: hard "is-number@npm:^7.0.0": version: 7.0.0 resolution: "is-number@npm:7.0.0" - checksum: b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 + checksum: 10c0/b4686d0d3053146095ccd45346461bc8e53b80aeb7671cc52a4de02dbbf7dc0d1d2a986e2fe4ae206984b4d34ef37e8b795ebc4f4295c978373e6575e295d811 languageName: node linkType: hard "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" - checksum: 228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d languageName: node linkType: hard "isexe@npm:^3.1.1": version: 3.1.1 resolution: "isexe@npm:3.1.1" - checksum: 9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 languageName: node linkType: hard -"jackspeak@npm:^2.3.5": - version: 2.3.6 - resolution: "jackspeak@npm:2.3.6" +"jackspeak@npm:^3.1.2": + version: 3.1.2 + resolution: "jackspeak@npm:3.1.2" dependencies: "@isaacs/cliui": "npm:^8.0.2" "@pkgjs/parseargs": "npm:^0.11.0" dependenciesMeta: "@pkgjs/parseargs": optional: true - checksum: f01d8f972d894cd7638bc338e9ef5ddb86f7b208ce177a36d718eac96ec86638a6efa17d0221b10073e64b45edc2ce15340db9380b1f5d5c5d000cbc517dc111 + checksum: 10c0/5f1922a1ca0f19869e23f0dc4374c60d36e922f7926c76fecf8080cc6f7f798d6a9caac1b9428327d14c67731fd551bb3454cb270a5e13a0718f3b3660ec3d5d languageName: node linkType: hard -"lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.1.0 - resolution: "lru-cache@npm:10.1.0" - checksum: 778bc8b2626daccd75f24c4b4d10632496e21ba064b126f526c626fbdbc5b28c472013fccd45d7646b9e1ef052444824854aed617b59cd570d01a8b7d651fc1e +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 languageName: node linkType: hard -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: "npm:^4.0.0" - checksum: cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.2.2 + resolution: "lru-cache@npm:10.2.2" + checksum: 10c0/402d31094335851220d0b00985084288136136992979d0e015f0f1697e15d1c86052d7d53ae86b614e5b058425606efffc6969a31a091085d7a2b80a8a1e26d6 languageName: node linkType: hard -"luxon@npm:^3.4.2": +"luxon@npm:^3.4.4": version: 3.4.4 resolution: "luxon@npm:3.4.4" - checksum: 02e26a0b039c11fd5b75e1d734c8f0332c95510f6a514a9a0991023e43fb233884da02d7f966823ffb230632a733fc86d4a4b1e63c3fbe00058b8ee0f8c728af + checksum: 10c0/02e26a0b039c11fd5b75e1d734c8f0332c95510f6a514a9a0991023e43fb233884da02d7f966823ffb230632a733fc86d4a4b1e63c3fbe00058b8ee0f8c728af languageName: node linkType: hard "make-fetch-happen@npm:^13.0.0": - version: 13.0.0 - resolution: "make-fetch-happen@npm:13.0.0" + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" dependencies: "@npmcli/agent": "npm:^2.0.0" cacache: "npm:^18.0.0" @@ -608,9 +593,10 @@ __metadata: minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" promise-retry: "npm:^2.0.1" ssri: "npm:^10.0.0" - checksum: 43b9f6dcbc6fe8b8604cb6396957c3698857a15ba4dbc38284f7f0e61f248300585ef1eb8cc62df54e9c724af977e45b5cdfd88320ef7f53e45070ed3488da55 + checksum: 10c0/df5f4dbb6d98153b751bccf4dc4cc500de85a96a9331db9805596c46aa9f99d9555983954e6c1266d9f981ae37a9e4647f42b9a4bb5466f867f4012e582c9e7e languageName: node linkType: hard @@ -619,16 +605,16 @@ __metadata: resolution: "minimatch@npm:3.1.2" dependencies: brace-expansion: "npm:^1.1.7" - checksum: 0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 languageName: node linkType: hard "minimatch@npm:^9.0.1": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" + version: 9.0.4 + resolution: "minimatch@npm:9.0.4" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 85f407dcd38ac3e180f425e86553911d101455ca3ad5544d6a7cec16286657e4f8a9aa6695803025c55e31e35a91a2252b5dc8e7d527211278b8b65b4dbd5eac + checksum: 10c0/2c16f21f50e64922864e560ff97c587d15fd491f65d92a677a344e970fe62aafdbeafe648965fa96d33c061b4d0eabfe0213466203dd793367e7f28658cf6414 languageName: node linkType: hard @@ -637,13 +623,13 @@ __metadata: resolution: "minipass-collect@npm:2.0.1" dependencies: minipass: "npm:^7.0.3" - checksum: 5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e languageName: node linkType: hard "minipass-fetch@npm:^3.0.0": - version: 3.0.4 - resolution: "minipass-fetch@npm:3.0.4" + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" dependencies: encoding: "npm:^0.1.13" minipass: "npm:^7.0.3" @@ -652,7 +638,7 @@ __metadata: dependenciesMeta: encoding: optional: true - checksum: 1b63c1f3313e88eeac4689f1b71c9f086598db9a189400e3ee960c32ed89e06737fa23976c9305c2d57464fb3fcdc12749d3378805c9d6176f5569b0d0ee8a75 + checksum: 10c0/9d702d57f556274286fdd97e406fc38a2f5c8d15e158b498d7393b1105974b21249289ec571fa2b51e038a4872bfc82710111cf75fae98c662f3d6f95e72152b languageName: node linkType: hard @@ -661,7 +647,7 @@ __metadata: resolution: "minipass-flush@npm:1.0.5" dependencies: minipass: "npm:^3.0.0" - checksum: 2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd languageName: node linkType: hard @@ -670,7 +656,7 @@ __metadata: resolution: "minipass-pipeline@npm:1.2.4" dependencies: minipass: "npm:^3.0.0" - checksum: cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 languageName: node linkType: hard @@ -679,7 +665,7 @@ __metadata: resolution: "minipass-sized@npm:1.0.3" dependencies: minipass: "npm:^3.0.0" - checksum: 298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb languageName: node linkType: hard @@ -688,21 +674,21 @@ __metadata: resolution: "minipass@npm:3.3.6" dependencies: yallist: "npm:^4.0.0" - checksum: a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c languageName: node linkType: hard "minipass@npm:^5.0.0": version: 5.0.0 resolution: "minipass@npm:5.0.0" - checksum: a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + checksum: 10c0/a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": - version: 7.0.4 - resolution: "minipass@npm:7.0.4" - checksum: 6c7370a6dfd257bf18222da581ba89a5eaedca10e158781232a8b5542a90547540b4b9b7e7f490e4cda43acfbd12e086f0453728ecf8c19e0ef6921bc5958ac5 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4": + version: 7.1.1 + resolution: "minipass@npm:7.1.1" + checksum: 10c0/fdccc2f99c31083f45f881fd1e6971d798e333e078ab3c8988fb818c470fbd5e935388ad9adb286397eba50baebf46ef8ff487c8d3f455a69c6f3efc327bdff9 languageName: node linkType: hard @@ -712,7 +698,7 @@ __metadata: dependencies: minipass: "npm:^3.0.0" yallist: "npm:^4.0.0" - checksum: 64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + checksum: 10c0/64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 languageName: node linkType: hard @@ -721,34 +707,27 @@ __metadata: resolution: "mkdirp@npm:1.0.4" bin: mkdirp: bin/cmd.js - checksum: 46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + checksum: 10c0/46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf languageName: node linkType: hard "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" - checksum: a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc - languageName: node - linkType: hard - -"ms@npm:^2.1.1": - version: 2.1.3 - resolution: "ms@npm:2.1.3" - checksum: d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 + checksum: 10c0/a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc languageName: node linkType: hard "negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" - checksum: 3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + checksum: 10c0/3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 languageName: node linkType: hard "node-gyp@npm:latest": - version: 10.0.1 - resolution: "node-gyp@npm:10.0.1" + version: 10.1.0 + resolution: "node-gyp@npm:10.1.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" @@ -762,16 +741,16 @@ __metadata: which: "npm:^4.0.0" bin: node-gyp: bin/node-gyp.js - checksum: abddfff7d873312e4ed4a5fb75ce893a5c4fb69e7fcb1dfa71c28a6b92a7f1ef6b62790dffb39181b5a82728ba8f2f32d229cf8cbe66769fe02cea7db4a555aa + checksum: 10c0/9cc821111ca244a01fb7f054db7523ab0a0cd837f665267eb962eb87695d71fb1e681f9e21464cc2fd7c05530dc4c81b810bca1a88f7d7186909b74477491a3c languageName: node linkType: hard -"nodemon@npm:^3.0.1": - version: 3.0.1 - resolution: "nodemon@npm:3.0.1" +"nodemon@npm:^3.1.2": + version: 3.1.2 + resolution: "nodemon@npm:3.1.2" dependencies: chokidar: "npm:^3.5.2" - debug: "npm:^3.2.7" + debug: "npm:^4" ignore-by-default: "npm:^1.0.1" minimatch: "npm:^3.1.2" pstree.remy: "npm:^1.1.8" @@ -782,36 +761,25 @@ __metadata: undefsafe: "npm:^2.0.5" bin: nodemon: bin/nodemon.js - checksum: 471a218227949b38926de78237004c91e226b63ee06f433cf85c2f1c1f8b6bfbef9bceaa8d27786e7cfb539eb84da357d01741884d08a3ae172bebecd0f1de5b + checksum: 10c0/7a091067d766768fb6660b796194b01748bba5dc3f1e3ed3dd5f804bfa305e207d24635755078ee5e7cc53848cea35204901e0a6e51ac64483bb8e9ecb237c95 languageName: node linkType: hard "nopt@npm:^7.0.0": - version: 7.2.0 - resolution: "nopt@npm:7.2.0" + version: 7.2.1 + resolution: "nopt@npm:7.2.1" dependencies: abbrev: "npm:^2.0.0" bin: nopt: bin/nopt.js - checksum: 9bd7198df6f16eb29ff16892c77bcf7f0cc41f9fb5c26280ac0def2cf8cf319f3b821b3af83eba0e74c85807cc430a16efe0db58fe6ae1f41e69519f585b6aff - languageName: node - linkType: hard - -"nopt@npm:~1.0.10": - version: 1.0.10 - resolution: "nopt@npm:1.0.10" - dependencies: - abbrev: "npm:1" - bin: - nopt: ./bin/nopt.js - checksum: ddfbd892116a125fd68849ef564dd5b1f0a5ba0dbbf18782e9499e2efad8f4d3790635b47c6b5d3f7e014069e7b3ce5b8112687e9ae093fcd2678188c866fe28 + checksum: 10c0/a069c7c736767121242037a22a788863accfa932ab285a1eb569eb8cd534b09d17206f68c37f096ae785647435e0c5a5a0a67b42ec743e481a455e5ae6a6df81 languageName: node linkType: hard "normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" - checksum: e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 + checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 languageName: node linkType: hard @@ -820,38 +788,45 @@ __metadata: resolution: "p-map@npm:4.0.0" dependencies: aggregate-error: "npm:^3.0.0" - checksum: 592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 languageName: node linkType: hard "path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" - checksum: 748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c languageName: node linkType: hard -"path-scurry@npm:^1.10.1": - version: 1.10.1 - resolution: "path-scurry@npm:1.10.1" +"path-scurry@npm:^1.11.0": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" dependencies: - lru-cache: "npm:^9.1.1 || ^10.0.0" + lru-cache: "npm:^10.2.0" minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: e5dc78a7348d25eec61ab166317e9e9c7b46818aa2c2b9006c507a6ff48c672d011292d9662527213e558f5652ce0afcc788663a061d8b59ab495681840c0c1e + checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d languageName: node linkType: hard "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" - checksum: 26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be + checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be languageName: node linkType: hard "proc-log@npm:^3.0.0": version: 3.0.0 resolution: "proc-log@npm:3.0.0" - checksum: f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + checksum: 10c0/f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + languageName: node + linkType: hard + +"proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 10c0/17db4757c2a5c44c1e545170e6c70a26f7de58feb985091fb1763f5081cab3d01b181fb2dd240c9f4a4255a1d9227d163d5771b7e69c9e49a561692db865efb9 languageName: node linkType: hard @@ -861,14 +836,14 @@ __metadata: dependencies: err-code: "npm:^2.0.2" retry: "npm:^0.12.0" - checksum: 9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 languageName: node linkType: hard "pstree.remy@npm:^1.1.8": version: 1.1.8 resolution: "pstree.remy@npm:1.1.8" - checksum: 30f78c88ce6393cb3f7834216cb6e282eb83c92ccb227430d4590298ab2811bc4a4745f850a27c5178e79a8f3e316591de0fec87abc19da648c2b3c6eb766d14 + checksum: 10c0/30f78c88ce6393cb3f7834216cb6e282eb83c92ccb227430d4590298ab2811bc4a4745f850a27c5178e79a8f3e316591de0fec87abc19da648c2b3c6eb766d14 languageName: node linkType: hard @@ -877,39 +852,37 @@ __metadata: resolution: "readdirp@npm:3.6.0" dependencies: picomatch: "npm:^2.2.1" - checksum: 6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b languageName: node linkType: hard "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" - checksum: 59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe languageName: node linkType: hard "safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" - checksum: 7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 languageName: node linkType: hard -"sax@npm:^1.2.4": - version: 1.3.0 - resolution: "sax@npm:1.3.0" - checksum: 599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea +"sax@npm:^1.4.1": + version: 1.4.1 + resolution: "sax@npm:1.4.1" + checksum: 10c0/6bf86318a254c5d898ede6bd3ded15daf68ae08a5495a2739564eb265cd13bcc64a07ab466fb204f67ce472bb534eb8612dac587435515169593f4fffa11de7c languageName: node linkType: hard "semver@npm:^7.3.5, semver@npm:^7.5.3": - version: 7.5.4 - resolution: "semver@npm:7.5.4" - dependencies: - lru-cache: "npm:^6.0.0" + version: 7.6.2 + resolution: "semver@npm:7.6.2" bin: semver: bin/semver.js - checksum: 5160b06975a38b11c1ab55950cb5b8a23db78df88275d3d8a42ccf1f29e55112ac995b3a26a522c36e3b5f76b0445f1eef70d696b8c7862a2b4303d7b0e7609e + checksum: 10c0/97d3441e97ace8be4b1976433d1c32658f6afaff09f143e52c593bae7eef33de19e3e369c88bd985ce1042c6f441c80c6803078d1de2a9988080b66684cbb30c languageName: node linkType: hard @@ -918,21 +891,21 @@ __metadata: resolution: "shebang-command@npm:2.0.0" dependencies: shebang-regex: "npm:^3.0.0" - checksum: a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e languageName: node linkType: hard "shebang-regex@npm:^3.0.0": version: 3.0.0 resolution: "shebang-regex@npm:3.0.0" - checksum: 1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 languageName: node linkType: hard "signal-exit@npm:^4.0.1": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" - checksum: 41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 languageName: node linkType: hard @@ -941,35 +914,35 @@ __metadata: resolution: "simple-update-notifier@npm:2.0.0" dependencies: semver: "npm:^7.5.3" - checksum: 2a00bd03bfbcbf8a737c47ab230d7920f8bfb92d1159d421bdd194479f6d01ebc995d13fbe13d45dace23066a78a3dc6642999b4e3b38b847e6664191575b20c + checksum: 10c0/2a00bd03bfbcbf8a737c47ab230d7920f8bfb92d1159d421bdd194479f6d01ebc995d13fbe13d45dace23066a78a3dc6642999b4e3b38b847e6664191575b20c languageName: node linkType: hard "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" - checksum: a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.1": - version: 8.0.2 - resolution: "socks-proxy-agent@npm:8.0.2" +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.3 + resolution: "socks-proxy-agent@npm:8.0.3" dependencies: - agent-base: "npm:^7.0.2" + agent-base: "npm:^7.1.1" debug: "npm:^4.3.4" socks: "npm:^2.7.1" - checksum: a842402fc9b8848a31367f2811ca3cd14c4106588b39a0901cd7a69029998adfc6456b0203617c18ed090542ad0c24ee4e9d4c75a0c4b75071e214227c177eb7 + checksum: 10c0/4950529affd8ccd6951575e21c1b7be8531b24d924aa4df3ee32df506af34b618c4e50d261f4cc603f1bfd8d426915b7d629966c8ce45b05fb5ad8c8b9a6459d languageName: node linkType: hard "socks@npm:^2.7.1": - version: 2.7.1 - resolution: "socks@npm:2.7.1" + version: 2.8.3 + resolution: "socks@npm:2.8.3" dependencies: - ip: "npm:^2.0.0" + ip-address: "npm:^9.0.5" smart-buffer: "npm:^4.2.0" - checksum: 43f69dbc9f34fc8220bc51c6eea1c39715ab3cfdb115d6e3285f6c7d1a603c5c75655668a5bbc11e3c7e2c99d60321fb8d7ab6f38cda6a215fadd0d6d0b52130 + checksum: 10c0/d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 languageName: node linkType: hard @@ -979,23 +952,30 @@ __metadata: dependencies: buffer-from: "npm:^1.0.0" source-map: "npm:^0.6.0" - checksum: 9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d + checksum: 10c0/9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d languageName: node linkType: hard "source-map@npm:^0.6.0": version: 0.6.1 resolution: "source-map@npm:0.6.1" - checksum: ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec languageName: node linkType: hard "ssri@npm:^10.0.0": - version: 10.0.5 - resolution: "ssri@npm:10.0.5" + version: 10.0.6 + resolution: "ssri@npm:10.0.6" dependencies: minipass: "npm:^7.0.3" - checksum: b091f2ae92474183c7ac5ed3f9811457e1df23df7a7e70c9476eaa9a0c4a0c8fc190fb45acefbf023ca9ee864dd6754237a697dc52a0fb182afe65d8e77443d8 + checksum: 10c0/e5a1e23a4057a86a97971465418f22ea89bd439ac36ade88812dd920e4e61873e8abd6a9b72a03a67ef50faa00a2daf1ab745c5a15b46d03e0544a0296354227 languageName: node linkType: hard @@ -1006,7 +986,7 @@ __metadata: emoji-regex: "npm:^8.0.0" is-fullwidth-code-point: "npm:^3.0.0" strip-ansi: "npm:^6.0.1" - checksum: 1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b languageName: node linkType: hard @@ -1017,7 +997,7 @@ __metadata: eastasianwidth: "npm:^0.2.0" emoji-regex: "npm:^9.2.2" strip-ansi: "npm:^7.0.1" - checksum: ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca languageName: node linkType: hard @@ -1026,7 +1006,7 @@ __metadata: resolution: "strip-ansi@npm:6.0.1" dependencies: ansi-regex: "npm:^5.0.1" - checksum: 1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 languageName: node linkType: hard @@ -1035,7 +1015,7 @@ __metadata: resolution: "strip-ansi@npm:7.1.0" dependencies: ansi-regex: "npm:^6.0.1" - checksum: a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 languageName: node linkType: hard @@ -1044,13 +1024,13 @@ __metadata: resolution: "supports-color@npm:5.5.0" dependencies: has-flag: "npm:^3.0.0" - checksum: 6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05 + checksum: 10c0/6ae5ff319bfbb021f8a86da8ea1f8db52fac8bd4d499492e30ec17095b58af11f0c55f8577390a749b1c4dde691b6a0315dab78f5f54c9b3d83f8fb5905c1c05 languageName: node linkType: hard "tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.2.0 - resolution: "tar@npm:6.2.0" + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: chownr: "npm:^2.0.0" fs-minipass: "npm:^2.0.0" @@ -1058,7 +1038,7 @@ __metadata: minizlib: "npm:^2.1.1" mkdirp: "npm:^1.0.3" yallist: "npm:^4.0.0" - checksum: 02ca064a1a6b4521fef88c07d389ac0936730091f8c02d30ea60d472e0378768e870769ab9e986d87807bfee5654359cf29ff4372746cc65e30cbddc352660d8 + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 languageName: node linkType: hard @@ -1067,25 +1047,23 @@ __metadata: resolution: "to-regex-range@npm:5.0.1" dependencies: is-number: "npm:^7.0.0" - checksum: 487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 + checksum: 10c0/487988b0a19c654ff3e1961b87f471702e708fa8a8dd02a298ef16da7206692e8552a0250e8b3e8759270f62e9d8314616f6da274734d3b558b1fc7b7724e892 languageName: node linkType: hard "touch@npm:^3.1.0": - version: 3.1.0 - resolution: "touch@npm:3.1.0" - dependencies: - nopt: "npm:~1.0.10" + version: 3.1.1 + resolution: "touch@npm:3.1.1" bin: - nodetouch: ./bin/nodetouch.js - checksum: dacb4a639401b83b0a40b56c0565e01096e5ecf38b22a4840d9eeb642a5bea136c6a119e4543f9b172349a5ee343b10cda0880eb47f7d7ddfd6eac59dcf53244 + nodetouch: bin/nodetouch.js + checksum: 10c0/d2e4d269a42c846a22a29065b9af0b263de58effc85a1764bb7a2e8fc4b47700e9e2fcbd7eb1f5bffbb7c73d860f93600cef282b93ddac8f0b62321cb498b36e languageName: node linkType: hard "undefsafe@npm:^2.0.5": version: 2.0.5 resolution: "undefsafe@npm:2.0.5" - checksum: 96c0466a5fbf395917974a921d5d4eee67bca4b30d3a31ce7e621e0228c479cf893e783a109af6e14329b52fe2f0cb4108665fad2b87b0018c0df6ac771261d5 + checksum: 10c0/96c0466a5fbf395917974a921d5d4eee67bca4b30d3a31ce7e621e0228c479cf893e783a109af6e14329b52fe2f0cb4108665fad2b87b0018c0df6ac771261d5 languageName: node linkType: hard @@ -1094,7 +1072,7 @@ __metadata: resolution: "unique-filename@npm:3.0.0" dependencies: unique-slug: "npm:^4.0.0" - checksum: 6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + checksum: 10c0/6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f languageName: node linkType: hard @@ -1103,7 +1081,7 @@ __metadata: resolution: "unique-slug@npm:4.0.0" dependencies: imurmurhash: "npm:^0.1.4" - checksum: cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + checksum: 10c0/cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 languageName: node linkType: hard @@ -1114,7 +1092,7 @@ __metadata: isexe: "npm:^2.0.0" bin: node-which: ./bin/node-which - checksum: 66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f languageName: node linkType: hard @@ -1125,7 +1103,7 @@ __metadata: isexe: "npm:^3.1.1" bin: node-which: bin/which.js - checksum: 449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + checksum: 10c0/449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a languageName: node linkType: hard @@ -1136,7 +1114,7 @@ __metadata: ansi-styles: "npm:^4.0.0" string-width: "npm:^4.1.0" strip-ansi: "npm:^6.0.0" - checksum: d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da languageName: node linkType: hard @@ -1147,13 +1125,13 @@ __metadata: ansi-styles: "npm:^6.1.0" string-width: "npm:^5.0.1" strip-ansi: "npm:^7.0.1" - checksum: 138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 languageName: node linkType: hard -"ws@npm:^8.13.0": - version: 8.14.2 - resolution: "ws@npm:8.14.2" +"ws@npm:^8.17.0": + version: 8.17.0 + resolution: "ws@npm:8.17.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -1162,13 +1140,13 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 35b4c2da048b8015c797fd14bcb5a5766216ce65c8a5965616a5440ca7b6c3681ee3cbd0ea0c184a59975556e9d58f2002abf8485a14d11d3371770811050a16 + checksum: 10c0/55241ec93a66fdfc4bf4f8bc66c8eb038fda2c7a4ee8f6f157f2ca7dc7aa76aea0c0da0bf3adb2af390074a70a0e45456a2eaf80e581e630b75df10a64b0a990 languageName: node linkType: hard "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" - checksum: 2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a languageName: node linkType: hard diff --git a/common/deps.edn b/common/deps.edn index 9819697cf7044900be4bb9da7d9ce681d3ec654e..57379fc355d6a8ab5d549bb104c8241fa9c13e4d 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -1,25 +1,25 @@ {:deps - {org.clojure/clojure {:mvn/version "1.11.1"} + {org.clojure/clojure {:mvn/version "1.11.2"} org.clojure/data.json {:mvn/version "2.5.0"} - org.clojure/tools.cli {:mvn/version "1.0.219"} + org.clojure/tools.cli {:mvn/version "1.1.230"} org.clojure/clojurescript {:mvn/version "1.11.132"} org.clojure/test.check {:mvn/version "1.1.1"} - org.clojure/data.fressian {:mvn/version "1.0.0"} + org.clojure/data.fressian {:mvn/version "1.1.0"} ;; Logging - org.apache.logging.log4j/log4j-api {:mvn/version "2.22.1"} - org.apache.logging.log4j/log4j-core {:mvn/version "2.22.1"} - org.apache.logging.log4j/log4j-web {:mvn/version "2.22.1"} - org.apache.logging.log4j/log4j-jul {:mvn/version "2.22.1"} - org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.22.1"} - org.slf4j/slf4j-api {:mvn/version "2.0.10"} + org.apache.logging.log4j/log4j-api {:mvn/version "2.23.1"} + org.apache.logging.log4j/log4j-core {:mvn/version "2.23.1"} + org.apache.logging.log4j/log4j-web {:mvn/version "2.23.1"} + org.apache.logging.log4j/log4j-jul {:mvn/version "2.23.1"} + org.apache.logging.log4j/log4j-slf4j2-impl {:mvn/version "2.23.1"} + org.slf4j/slf4j-api {:mvn/version "2.0.13"} pl.tkowalcz.tjahzi/log4j2-appender {:mvn/version "0.9.32"} - selmer/selmer {:mvn/version "1.12.59"} + selmer/selmer {:mvn/version "1.12.61"} criterium/criterium {:mvn/version "0.4.6"} metosin/jsonista {:mvn/version "0.3.8"} - metosin/malli {:mvn/version "0.14.0"} + metosin/malli {:mvn/version "0.16.1"} expound/expound {:mvn/version "0.9.0"} com.cognitect/transit-clj {:mvn/version "1.0.333"} @@ -28,7 +28,7 @@ integrant/integrant {:mvn/version "0.8.1"} org.apache.commons/commons-pool2 {:mvn/version "2.12.0"} - org.graalvm.js/js {:mvn/version "23.0.2"} + org.graalvm.js/js {:mvn/version "23.0.4"} funcool/tubax {:mvn/version "2021.05.20-0"} funcool/cuerdas {:mvn/version "2023.11.09-407"} @@ -41,7 +41,7 @@ :git/tag "3.0.0" :git/url "https://github.com/funcool/datoteka"} - lambdaisland/uri {:mvn/version "1.16.134" + lambdaisland/uri {:mvn/version "1.19.155" :exclusions [org.clojure/data.json]} frankiesardo/linked {:mvn/version "1.3.0"} @@ -63,7 +63,7 @@ {:dev {:extra-deps {org.clojure/tools.namespace {:mvn/version "RELEASE"} - thheller/shadow-cljs {:mvn/version "2.27.4"} + thheller/shadow-cljs {:mvn/version "2.28.8"} com.clojure-goes-fast/clj-async-profiler {:mvn/version "RELEASE"} com.bhauman/rebel-readline {:mvn/version "RELEASE"} criterium/criterium {:mvn/version "RELEASE"} @@ -72,16 +72,12 @@ :build {:extra-deps - {io.github.clojure/tools.build {:git/tag "v0.9.5" :git/sha "24f2894"}} + {io.github.clojure/tools.build {:git/tag "v0.10.3" :git/sha "15ead66"}} :ns-default build} :test - {:extra-paths ["test"] - :extra-deps - {io.github.cognitect-labs/test-runner - {:git/tag "v0.5.1" :git/sha "dfb30dd"}} - :main-opts ["-m" "cognitect.test-runner"] - :exec-fn cognitect.test-runner.api/test} + {:main-opts ["-m" "kaocha.runner"] + :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}} :shadow-cljs {:main-opts ["-m" "shadow.cljs.devtools.cli"]} diff --git a/common/dev/user.clj b/common/dev/user.clj index 3cb650922f7157becfc577a3baae4111b0895e43..cb907fade07e1037e4bc8dc5c250e23d2708aada 100644 --- a/common/dev/user.clj +++ b/common/dev/user.clj @@ -6,11 +6,16 @@ (ns user (:require + [app.common.data :as d] + [app.common.fressian :as fres] + [app.common.json :as json] [app.common.pprint :as pp] [app.common.schema :as sm] [app.common.schema.desc-js-like :as smdj] [app.common.schema.desc-native :as smdn] [app.common.schema.generators :as sg] + [malli.core :as m] + [malli.util :as mu] [clojure.java.io :as io] [clojure.pprint :refer [pprint print-table]] [clojure.repl :refer :all] diff --git a/common/package.json b/common/package.json index 4d2e78a7912d3e9aef8dfa6e777ec8e6f7b76125..93a456d1dd19f0162f0a444704806574a8fafc18 100644 --- a/common/package.json +++ b/common/package.json @@ -5,19 +5,19 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.0.2", + "packageManager": "yarn@4.3.1", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" }, "dependencies": { - "luxon": "^3.4.2", - "sax": "^1.2.4" + "luxon": "^3.4.4", + "sax": "^1.4.1" }, "devDependencies": { - "shadow-cljs": "2.27.4", + "shadow-cljs": "2.28.11", "source-map-support": "^0.5.21", - "ws": "^8.13.0" + "ws": "^8.17.0" }, "scripts": { "fmt:clj:check": "cljfmt check --parallel=false src/ test/", diff --git a/common/src/app/common/colors.cljc b/common/src/app/common/colors.cljc index 69de60bd51e9f387be3b2e9070ace58e3145820b..1f34903a4a26f26058c6ce86ffdce3571bbc07cf 100644 --- a/common/src/app/common/colors.cljc +++ b/common/src/app/common/colors.cljc @@ -274,6 +274,13 @@ (catch #?(:clj Throwable :cljs :default) _cause [0 0 0]))) +(defn hex->lum + [color] + (let [[r g b] (hex->rgb color)] + (mth/sqrt (+ (* 0.241 r) + (* 0.691 g) + (* 0.068 b))))) + (defn- int->hex "Convert integer to hex string" [v] @@ -455,3 +462,19 @@ :else [r g (inc b)])) + +(defn reduce-range + [value range] + (/ (mth/floor (* value range)) range)) + +(defn sort-colors + [a b] + (let [[ah _ av] (hex->hsv (:color a)) + [bh _ bv] (hex->hsv (:color b)) + ah (reduce-range (/ ah 60) 8) + bh (reduce-range (/ bh 60) 8) + av (/ av 255) + bv (/ bv 255) + a (+ (* ah 100) (* av 10)) + b (+ (* bh 100) (* bv 10))] + (compare a b))) diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 73680363144abd6e89cf644d567f8456a9900686..77e9af51b9672d78f98a130be667f557be691e02 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -9,7 +9,7 @@ data resources." (:refer-clojure :exclude [read-string hash-map merge name update-vals parse-double group-by iteration concat mapcat - parse-uuid max min]) + parse-uuid max min regexp?]) #?(:cljs (:require-macros [app.common.data])) @@ -44,8 +44,8 @@ (defn ordered-map ([] lkm/empty-linked-map) - ([a] (conj lkm/empty-linked-map a)) - ([a & xs] (apply conj lkm/empty-linked-map a xs))) + ([k a] (assoc lkm/empty-linked-map k a)) + ([k a & xs] (apply assoc lkm/empty-linked-map k a xs))) (defn ordered-set? [o] @@ -224,7 +224,6 @@ [coll] (into [] (remove nil?) coll)) - (defn without-nils "Given a map, return a map removing key-value pairs when value is `nil`." @@ -565,6 +564,41 @@ new-elems (remove p? after)))) +(defn addm-at-index + "Insert an element in an ordered map at an arbitrary index" + [coll index key element] + (assert (ordered-map? coll)) + (-> (ordered-map) + (into (take index coll)) + (assoc key element) + (into (drop index coll)))) + +(defn insertm-at-index + "Insert a map {k v} of elements in an ordered map at an arbitrary index" + [coll index new-elems] + (assert (ordered-map? coll)) + (-> (ordered-map) + (into (take index coll)) + (into new-elems) + (into (drop index coll)))) + +(defn adds-at-index + "Insert an element in an ordered set at an arbitrary index" + [coll index element] + (assert (ordered-set? coll)) + (-> (ordered-set) + (into (take index coll)) + (conj element) + (into (drop index coll)))) + +(defn inserts-at-index + "Insert a list of elements in an ordered set at an arbitrary index" + [coll index new-elems] + (assert (ordered-set? coll)) + (-> (ordered-set) + (into (take index coll)) + (into new-elems) + (into (drop index coll)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Parsing / Conversion @@ -642,6 +676,13 @@ ;; Utilities ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn regexp? + "Return `true` if `x` is a regexp pattern + instance." + [x] + #?(:cljs (cljs.core/regexp? x) + :clj (instance? java.util.regex.Pattern x))) + (defn nilf "Returns a new function that if you pass nil as any argument will return nil" diff --git a/common/src/app/common/features.cljc b/common/src/app/common/features.cljc index f4751da8f35d2639e1e4f231eeaeb9f819f45b3e..6306488587af5d893f62a212d0d3bdd2301e4040 100644 --- a/common/src/app/common/features.cljc +++ b/common/src/app/common/features.cljc @@ -48,22 +48,23 @@ "fdata/shape-data-type" "components/v2" "styles/v2" - "layout/grid"}) + "layout/grid" + "plugins/runtime"}) -;; A set of features enabled by default for each file, they are -;; implicit and are enabled by default and can't be disabled. The -;; features listed in this set are mainly freatures addedby file -;; migrations process, so all features referenced in migrations should -;; be here. -(def default-enabled-features - #{"fdata/shape-data-type"}) +;; A set of features enabled by default +(def default-features + #{"fdata/shape-data-type" + "styles/v2" + "layout/grid" + "components/v2"}) ;; A set of features which only affects on frontend and can be enabled ;; and disabled freely by the user any time. This features does not ;; persist on file features field but can be permanently enabled on ;; team feature field (def frontend-only-features - #{"styles/v2"}) + #{"styles/v2" + "plugins/runtime"}) ;; Features that are mainly backend only or there are a proper ;; fallback when frontend reports no support for it @@ -78,15 +79,17 @@ (def no-migration-features (-> #{"fdata/objects-map" "fdata/pointer-map" - "layout/grid"} + "layout/grid" + "fdata/shape-data-type" + "plugins/runtime"} (into frontend-only-features))) -(sm/def! ::features +(sm/register! ::features [:schema {:title "FileFeatures" ::smdj/inline true :gen/gen (smg/subseq supported-features)} - ::sm/set-of-strings]) + [::sm/set :string]]) (defn- flag->feature "Translate a flag to a feature name" @@ -97,6 +100,7 @@ :feature-grid-layout "layout/grid" :feature-fdata-objects-map "fdata/objects-map" :feature-fdata-pointer-map "fdata/pointer-map" + :feature-plugins "plugins/runtime" nil)) (defn migrate-legacy-features @@ -129,7 +133,7 @@ (defn get-enabled-features "Get the globally enabled fratures set." [flags] - (into default-enabled-features xf-flag-to-feature flags)) + (into default-features xf-flag-to-feature flags)) (defn get-team-enabled-features "Get the team enabled features. @@ -141,7 +145,6 @@ team-features (into #{} xf-remove-ephimeral (:features team))] (-> enabled-features (set/intersection no-migration-features) - (set/union default-enabled-features) (set/union team-features)))) (defn check-client-features! @@ -244,7 +247,7 @@ (let [not-supported (-> (or source-features #{}) (set/difference destination-features) (set/difference no-migration-features) - (set/difference default-enabled-features) + (set/difference default-features) (seq))] (when not-supported (ex/raise :type :restriction @@ -256,7 +259,7 @@ (let [not-supported (-> (or destination-features #{}) (set/difference source-features) (set/difference no-migration-features) - (set/difference default-enabled-features) + (set/difference default-features) (seq))] (when not-supported (ex/raise :type :restriction diff --git a/common/src/app/common/files/builder.cljc b/common/src/app/common/files/builder.cljc index d10b494d952ab0111d8228f40cc100c930da73be..988164c20a3c64a579fe091128ab8e1903c68637 100644 --- a/common/src/app/common/files/builder.cljc +++ b/common/src/app/common/files/builder.cljc @@ -16,6 +16,7 @@ [app.common.pprint :as pp] [app.common.schema :as sm] [app.common.svg :as csvg] + [app.common.text :as txt] [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.file :as ctf] @@ -37,17 +38,22 @@ fail-on-spec?] :or {add-container? false fail-on-spec? false}}] - (let [component-id (:current-component-id file) - change (cond-> change - (and add-container? (some? component-id)) - (-> (assoc :component-id component-id) - (cond-> (some? (:current-frame-id file)) - (assoc :frame-id (:current-frame-id file)))) - - (and add-container? (nil? component-id)) - (assoc :page-id (:current-page-id file) - :frame-id (:current-frame-id file))) - valid? (ch/check-change! change)] + (let [components-v2 (dm/get-in file [:data :options :components-v2]) + component-id (:current-component-id file) + change (cond-> change + (and add-container? (some? component-id) (not components-v2)) + (-> (assoc :component-id component-id) + (cond-> (some? (:current-frame-id file)) + (assoc :frame-id (:current-frame-id file)))) + + (and add-container? (or (nil? component-id) components-v2)) + (assoc :page-id (:current-page-id file) + :frame-id (:current-frame-id file))) + + valid? (or (and components-v2 + (nil? (:component-id change)) + (nil? (:page-id change))) + (ch/check-change! change))] (when-not valid? (let [explain (sm/explain ::ch/change change)] @@ -59,12 +65,12 @@ ::sm/explain explain)))) (cond-> file - valid? - (-> (update :changes conjv change) - (update :data ch/process-changes [change] false)) + (and valid? (or (not add-container?) (some? (:component-id change)) (some? (:page-id change)))) + (-> (update :changes conjv change) ;; In components-v2 we do not add shapes + (update :data ch/process-changes [change] false)) ;; inside a component (not valid?) - (update :errors conjv change))))) + (update :errors conjv change)))));) (defn- lookup-objects ([file] @@ -134,19 +140,14 @@ (create-file (uuid/next) name)) ([id name] - {:id id - :name name - :data (-> ctf/empty-file-data - (assoc :id id)) - - ;; We keep the changes so we can send them to the backend - :changes []})) + (-> (ctf/make-file {:id id :name name :create-page false}) + (assoc :changes [])))) ;; We keep the changes so we can send them to the backend (defn add-page [file data] (dm/assert! (nil? (:current-component-id file))) (let [page-id (or (:id data) (uuid/next)) - page (-> (ctp/make-empty-page page-id "Page 1") + page (-> (ctp/make-empty-page {:id page-id :name "Page 1"}) (d/deep-merge data))] (-> file (commit-change @@ -184,10 +185,11 @@ (update :parent-stack conjv (:id obj))))) (defn close-artboard [file] - (let [parent-id (-> file :parent-stack peek) + (let [components-v2 (dm/get-in file [:data :options :components-v2]) + parent-id (-> file :parent-stack peek) parent (lookup-shape file parent-id) current-frame-id (or (:frame-id parent) - (when (nil? (:current-component-id file)) + (when (or (nil? (:current-component-id file)) components-v2) root-id))] (-> file (assoc :current-frame-id current-frame-id) @@ -322,8 +324,19 @@ (defn create-path [file data] (create-shape file :path data)) +(defn- clean-text-content + "Clean the content data so it doesn't break the validation" + [content] + (letfn [(update-fill [fill] + (d/update-in-when fill [:fill-color-gradient :type] keyword))] + (txt/transform-nodes + (fn [node] + (d/update-when node :fills #(mapv update-fill %))) + content))) + (defn create-text [file data] - (create-shape file :text data)) + (let [data (d/update-when data :content clean-text-content)] + (create-shape file :text data))) (defn create-image [file data] (create-shape file :image data)) @@ -499,17 +512,26 @@ {:type :del-media :id id})))) - (defn start-component - ([file data] (start-component file data :group)) + ([file data] + (let [components-v2 (dm/get-in file [:data :options :components-v2]) + root-type (if components-v2 :frame :group)] + (start-component file data root-type))) + ([file data root-type] ;; FIXME: data probably can be a shape instance, then we can use gsh/shape->rect - (let [selrect (or (grc/make-rect (:x data) (:y data) (:width data) (:height data)) + (let [components-v2 (dm/get-in file [:data :options :components-v2]) + selrect (or (grc/make-rect (:x data) (:y data) (:width data) (:height data)) grc/empty-rect) name (:name data) path (:path data) main-instance-id (:main-instance-id data) main-instance-page (:main-instance-page data) + + ;; In components v1 we must create the root shape and set it inside + ;; the :objects attribute of the component. When in components-v2, + ;; this will be ignored as the root shape has already been created + ;; in its page, by the normal page import. attrs (-> data (assoc :type root-type) (assoc :x (:x selrect)) @@ -531,19 +553,43 @@ (-> file (commit-change - {:type :add-component - :id (:id obj) - :name name - :path path - :main-instance-id main-instance-id - :main-instance-page main-instance-page - :shapes [obj]}) + (cond-> {:type :add-component + :id (:id obj) + :name name + :path path + :main-instance-id main-instance-id + :main-instance-page main-instance-page} + (not components-v2) + (assoc :shapes [obj]))) (assoc :last-id (:id obj)) (assoc :parent-stack [(:id obj)]) (assoc :current-component-id (:id obj)) (assoc :current-frame-id (if (= (:type obj) :frame) (:id obj) uuid/zero)))))) +(defn start-deleted-component + [file data] + (let [attrs (-> data + (assoc :id (:main-instance-id data)) + (assoc :component-file (:id file)) + (assoc :component-id (:id data)) + (assoc :x (:main-instance-x data)) + (assoc :y (:main-instance-y data)) + (dissoc :path) + (dissoc :main-instance-id) + (dissoc :main-instance-page) + (dissoc :main-instance-x) + (dissoc :main-instance-y) + (dissoc :main-instance-parent) + (dissoc :main-instance-frame))] + ;; To create a deleted component, first we add all shapes of the main instance + ;; in the main instance page, and in the finish event we delete it. + (-> file + (update :parent-stack conjv (:main-instance-parent data)) + (assoc :current-page-id (:main-instance-page data)) + (assoc :current-frame-id (:main-instance-frame data)) + (add-artboard attrs)))) + (defn finish-component [file] (let [component-id (:current-component-id file) @@ -554,9 +600,11 @@ file (cond - ;; Components-v2 component we skip this step + ;; In components-v2 components haven't any shape inside them. (and component-data (:main-instance-id component-data)) - file + (update file :data + (fn [data] + (ctkl/update-component data component-id dissoc :objects))) (empty? children) (commit-change @@ -606,43 +654,18 @@ (update :parent-stack pop)))) (defn finish-deleted-component - [component-id page-id main-instance-x main-instance-y file] + [component-id file] (let [file (assoc file :current-component-id component-id) - page (ctpl/get-page (:data file) page-id) - component (ctkl/get-component (:data file) component-id) - main-instance-id (:main-instance-id component) - - ; To obtain a deleted component, we first create the component - ; and the main instance in the workspace, and then delete them. - [_ shapes] - (ctn/make-component-instance page - component - (:data file) - (gpt/point main-instance-x - main-instance-y) - true - {:main-instance true - :force-id main-instance-id})] - (as-> file $ - (reduce #(commit-change %1 - {:type :add-obj - :id (:id %2) - :page-id (:id page) - :parent-id (:parent-id %2) - :frame-id (:frame-id %2) - :ignore-touched true - :obj %2}) - $ - shapes) - (commit-change $ {:type :del-component + component (ctkl/get-component (:data file) component-id)] + (-> file + (close-artboard) + (commit-change {:type :del-component :id component-id}) - (reduce #(commit-change %1 {:type :del-obj - :page-id page-id - :ignore-touched true - :id (:id %2)}) - $ - shapes) - (dissoc $ :current-component-id)))) + (commit-change {:type :del-obj + :page-id (:main-instance-page component) + :id (:main-instance-id component) + :ignore-touched true}) + (dissoc :current-page-id)))) (defn create-component-instance [file data] @@ -653,7 +676,6 @@ page-id (:current-page-id file) page (ctpl/get-page (:data file) page-id) component (ctkl/get-component (:data file) component-id) - ;; main-instance-id (:main-instance-id component) components-v2 (dm/get-in file [:options :components-v2]) diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index c200749d8a061e026bc2805f08f982708cd06bbb..9df9759375a776a76903ea8dd604197501883a39 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -31,29 +31,41 @@ ;; SCHEMAS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def ^:private - schema:operation - (sm/define - [:multi {:dispatch :type :title "Operation" ::smd/simplified true} - [:set - [:map {:title "SetOperation"} - [:type [:= :set]] - [:attr :keyword] - [:val :any] - [:ignore-touched {:optional true} :boolean] - [:ignore-geometry {:optional true} :boolean]]] - [:set-touched - [:map {:title "SetTouchedOperation"} - [:type [:= :set-touched]] - [:touched [:maybe [:set :keyword]]]]] - [:set-remote-synced - [:map {:title "SetRemoteSyncedOperation"} - [:type [:= :set-remote-synced]] - [:remote-synced {:optional true} [:maybe :boolean]]]]])) - -(sm/define! ::change +(def schema:operation + [:multi {:dispatch :type + :title "Operation" + :decode/json #(update % :type keyword) + ::smd/simplified true} + [:assign + [:map {:title "AssignOperation"} + [:type [:= :assign]] + ;; NOTE: the full decoding is happening on the handler because it + ;; needs a proper context of the current shape and its type + [:value [:map-of :keyword :any]] + [:ignore-touched {:optional true} :boolean] + [:ignore-geometry {:optional true} :boolean]]] + [:set + [:map {:title "SetOperation"} + [:type [:= :set]] + [:attr :keyword] + [:val :any] + [:ignore-touched {:optional true} :boolean] + [:ignore-geometry {:optional true} :boolean]]] + [:set-touched + [:map {:title "SetTouchedOperation"} + [:type [:= :set-touched]] + [:touched [:maybe [:set :keyword]]]]] + [:set-remote-synced + [:map {:title "SetRemoteSyncedOperation"} + [:type [:= :set-remote-synced]] + [:remote-synced {:optional true} [:maybe :boolean]]]]]) + +(def schema:change [:schema - [:multi {:dispatch :type :title "Change" ::smd/simplified true} + [:multi {:dispatch :type + :title "Change" + :decode/json #(update % :type keyword) + ::smd/simplified true} [:set-option [:map {:title "SetOptionChange"} [:type [:= :set-option]] @@ -133,6 +145,18 @@ [:id ::sm/uuid] [:name :string]]] + [:mod-plugin-data + [:map {:title "ModPagePluginData"} + [:type [:= :mod-plugin-data]] + [:object-type [::sm/one-of #{:file :page :shape :color :typography :component}]] + ;; It's optional because files don't need the id for type :file + [:object-id {:optional true} [:maybe ::sm/uuid]] + ;; Only needed in type shape + [:page-id {:optional true} [:maybe ::sm/uuid]] + [:namespace :keyword] + [:key :string] + [:value [:maybe :string]]]] + [:del-page [:map {:title "DelPageChange"} [:type [:= :del-page]] @@ -154,22 +178,21 @@ [:add-color [:map {:title "AddColorChange"} [:type [:= :add-color]] - [:color :any]]] + [:color ::ctc/color]]] [:mod-color [:map {:title "ModColorChange"} [:type [:= :mod-color]] - [:color :any]]] + [:color ::ctc/color]]] [:del-color [:map {:title "DelColorChange"} [:type [:= :del-color]] [:id ::sm/uuid]]] + ;; DEPRECATED: remove before 2.3 [:add-recent-color - [:map {:title "AddRecentColorChange"} - [:type [:= :add-recent-color]] - [:color ::ctc/recent-color]]] + [:map {:title "AddRecentColorChange"}]] [:add-media [:map {:title "AddMediaChange"} @@ -234,8 +257,11 @@ [:type [:= :del-typography]] [:id ::sm/uuid]]]]]) -(sm/define! ::changes - [:sequential {:gen/max 2} ::change]) +(def schema:changes + [:sequential {:gen/max 5 :gen/min 1} schema:change]) + +(sm/register! ::change schema:change) +(sm/register! ::changes schema:changes) (def check-change! (sm/check-fn ::change)) @@ -256,6 +282,16 @@ ;; Page Transformation Changes ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(def ^:dynamic *touched-changes* + "A dynamic var that used for track changes that touch shapes on + first processing phase of changes. Should be set to a hash-set + instance and will contain changes that caused the touched + modification." + nil) + +(defmulti process-change (fn [_ change] (:type change))) +(defmulti process-operation (fn [_ op] (:type op))) + ;; Changes Processing Impl (defn validate-shapes! @@ -284,8 +320,19 @@ nil)))) (run! validate-shape!)))) -(defmulti process-change (fn [_ change] (:type change))) -(defmulti process-operation (fn [_ _ op] (:type op))) +(defn- process-touched-change + [data {:keys [id page-id component-id]}] + (let [objects (if page-id + (-> data :pages-index (get page-id) :objects) + (-> data :components (get component-id) :objects)) + shape (get objects id) + croot (ctn/get-component-shape objects shape {:allow-main? true})] + + (if (and (some? croot) (ctk/main-instance? croot)) + (ctkl/set-component-modified data (:component-id croot)) + (if (some? component-id) + (ctkl/set-component-modified data component-id) + data)))) (defn process-changes ([data items] @@ -299,10 +346,15 @@ "expected valid changes" (check-changes! items))) - (let [result (reduce #(or (process-change %1 %2) %1) data items)] - ;; Validate result shapes (only on the backend) - #?(:clj (validate-shapes! data result items)) - result))) + (binding [*touched-changes* (volatile! #{})] + (let [result (reduce #(or (process-change %1 %2) %1) data items) + result (reduce process-touched-change result @*touched-changes*)] + ;; Validate result shapes (only on the backend) + ;; + ;; TODO: (PERF) add changed shapes tracking and only validate + ;; the tracked changes instead of iterate over all shapes + #?(:clj (validate-shapes! data result items)) + result)))) (defmethod process-change :set-option [data {:keys [page-id option value]}] @@ -323,83 +375,51 @@ (d/update-in-when data [:pages-index page-id] update-container) (d/update-in-when data [:components component-id] update-container)))) +(defn- process-operations + [objects {:keys [id operations] :as change}] + (if-let [shape (get objects id)] + (let [shape (reduce process-operation shape operations) + touched? (-> shape meta ::ctn/touched)] + ;; NOTE: processing operation functions can assign + ;; the ::ctn/touched metadata on shapes, in this case we + ;; need to report them for to be used in the second + ;; phase of changes procesing + (when touched? (some-> *touched-changes* (vswap! conj change))) + (assoc objects id shape)) + + objects)) + (defmethod process-change :mod-obj - [data {:keys [id page-id component-id operations]}] - (let [changed? (atom false) - - process-and-register (partial process-operation - (fn [_shape] (reset! changed? true))) - - update-fn (fn [objects] - (d/update-when objects id - #(reduce process-and-register % operations))) - - check-modify-component (fn [data] - (if @changed? - ;; When a shape is modified, if it belongs to a main component instance, - ;; the component needs to be marked as modified. - (let [objects (if page-id - (-> data :pages-index (get page-id) :objects) - (-> data :components (get component-id) :objects)) - shape (get objects id) - component-root (ctn/get-component-shape objects shape {:allow-main? true})] - (if (and (some? component-root) (ctk/main-instance? component-root)) - (ctkl/set-component-modified data (:component-id component-root)) - (if (some? component-id) - (ctkl/set-component-modified data component-id) - data))) - data))] - - (as-> data $ - (if page-id - (d/update-in-when $ [:pages-index page-id :objects] update-fn) - (d/update-in-when $ [:components component-id :objects] update-fn)) - (check-modify-component $)))) + [data {:keys [page-id component-id] :as change}] + (if page-id + (d/update-in-when data [:pages-index page-id :objects] process-operations change) + (d/update-in-when data [:components component-id :objects] process-operations change))) + +(defn- process-children-reordering + [objects {:keys [parent-id shapes] :as change}] + (if-let [old-shapes (dm/get-in objects [parent-id :shapes])] + (let [id->idx + (update-vals + (->> (d/enumerate shapes) + (group-by second)) + (comp first first)) + + new-shapes + (vec (sort-by #(d/nilv (id->idx %) -1) < old-shapes))] + + (if (not= old-shapes new-shapes) + (do + (some-> *touched-changes* (vswap! conj change)) + (update objects parent-id assoc :shapes new-shapes)) + objects)) + + objects)) (defmethod process-change :reorder-children - [data {:keys [parent-id shapes page-id component-id]}] - (let [changed? (atom false) - - update-fn - (fn [objects] - (let [old-shapes (dm/get-in objects [parent-id :shapes]) - - id->idx - (update-vals - (->> shapes - d/enumerate - (group-by second)) - (comp first first)) - - new-shapes - (into [] (sort-by #(d/nilv (id->idx %) -1) < old-shapes))] - - (reset! changed? (not= old-shapes new-shapes)) - - (cond-> objects - @changed? - (d/assoc-in-when [parent-id :shapes] new-shapes)))) - - check-modify-component - (fn [data] - (if @changed? - ;; When a shape is modified, if it belongs to a main component instance, - ;; the component needs to be marked as modified. - (let [objects (if page-id - (-> data :pages-index (get page-id) :objects) - (-> data :components (get component-id) :objects)) - shape (get objects parent-id) - component-root (ctn/get-component-shape objects shape {:allow-main? true})] - (if (and (some? component-root) (ctk/main-instance? component-root)) - (ctkl/set-component-modified data (:component-id component-root)) - data)) - data))] - - (as-> data $ - (if page-id - (d/update-in-when $ [:pages-index page-id :objects] update-fn) - (d/update-in-when $ [:components component-id :objects] update-fn)) - (check-modify-component $)))) + [data {:keys [page-id component-id] :as change}] + (if page-id + (d/update-in-when data [:pages-index page-id :objects] process-children-reordering change) + (d/update-in-when data [:components component-id :objects] process-children-reordering change))) (defmethod process-change :del-obj [data {:keys [page-id component-id id ignore-touched]}] @@ -578,7 +598,7 @@ (ex/raise :type :conflict :hint "id+name or page should be provided, never both")) (let [page (if (and (string? name) (uuid? id)) - (ctp/make-empty-page id name) + (ctp/make-empty-page {:id id :name name}) page)] (ctpl/add-page data page))) @@ -586,6 +606,35 @@ [data {:keys [id name]}] (d/update-in-when data [:pages-index id] assoc :name name)) +(defmethod process-change :mod-plugin-data + [data {:keys [object-type object-id page-id namespace key value]}] + + (when (and (= object-type :shape) (nil? page-id)) + (ex/raise :type :internal :hint "update for shapes needs a page-id")) + + (letfn [(update-fn [data] + (if (some? value) + (assoc-in data [:plugin-data namespace key] value) + (update-in data [:plugin-data namespace] (fnil dissoc {}) key)))] + (case object-type + :file + (update-fn data) + + :page + (d/update-in-when data [:pages-index object-id :options] update-fn) + + :shape + (d/update-in-when data [:pages-index page-id :objects object-id] update-fn) + + :color + (d/update-in-when data [:colors object-id] update-fn) + + :typography + (d/update-in-when data [:typographies object-id] update-fn) + + :component + (d/update-in-when data [:components object-id] update-fn)))) + (defmethod process-change :del-page [data {:keys [id]}] (ctpl/delete-page data id)) @@ -606,14 +655,10 @@ [data {:keys [id]}] (ctcl/delete-color data id)) +;; DEPRECATED: remove before 2.3 (defmethod process-change :add-recent-color - [data {:keys [color]}] - ;; Moves the color to the top of the list and then truncates up to 15 - (update data :recent-colors (fn [rc] - (let [rc (conj (filterv (comp not #{color}) (or rc [])) color)] - (if (> (count rc) 15) - (subvec rc 1) - rc))))) + [data _] + data) ;; -- Media @@ -667,70 +712,48 @@ ;; === Operations +(def ^:private decode-shape + (sm/decoder cts/schema:shape sm/json-transformer)) + +(defmethod process-operation :assign + [{:keys [type] :as shape} {:keys [value] :as op}] + (let [modifications (assoc value :type type) + modifications (decode-shape modifications)] + (reduce-kv (fn [shape k v] + (process-operation shape {:type :set + :attr k + :val v + :ignore-touched (:ignore-touched op) + :ignore-geometry (:ignore-geometry op)})) + shape + modifications))) + (defmethod process-operation :set - [on-changed shape op] - (let [attr (:attr op) - group (get ctk/sync-attrs attr) - val (:val op) - shape-val (get shape attr) - ignore (or (:ignore-touched op) (= attr :position-data)) ;; position-data is a derived attribute and - ignore-geometry (:ignore-geometry op) ;; never triggers touched by itself - is-geometry? (and (or (= group :geometry-group) - (and (= group :content-group) (= (:type shape) :path))) - (not (#{:width :height} attr))) ;; :content in paths are also considered geometric - ;; TODO: the check of :width and :height probably may be removed - ;; after the check added in data/workspace/modifiers/check-delta - ;; function. Better check it and test toroughly when activating - ;; components-v2 mode. - in-copy? (ctk/in-component-copy? shape) - - ;; For geometric attributes, there are cases in that the value changes - ;; slightly (e.g. when rounding to pixel, or when recalculating text - ;; positions in different zoom levels). To take this into account, we - ;; ignore geometric changes smaller than 1 pixel. - equal? (if is-geometry? - (gsh/close-attrs? attr val shape-val 1) - (gsh/close-attrs? attr val shape-val))] - - ;; Notify when value has changed, except when it has not moved relative to the - ;; component head. - (when (and group (not equal?) (not (and ignore-geometry is-geometry?))) - (on-changed shape)) - - (cond-> shape - ;; Depending on the origin of the attribute change, we need or not to - ;; set the "touched" flag for the group the attribute belongs to. - ;; In some cases we need to ignore touched only if the attribute is - ;; geometric (position, width or transformation). - (and in-copy? group (not ignore) (not equal?) - (not (and ignore-geometry is-geometry?))) - (-> (update :touched cfh/set-touched-group group) - (dissoc :remote-synced)) - - (nil? val) - (dissoc attr) - - (some? val) - (assoc attr val)))) + [shape op] + (ctn/set-shape-attr shape + (:attr op) + (:val op) + :ignore-touched (:ignore-touched op) + :ignore-geometry (:ignore-geometry op))) (defmethod process-operation :set-touched - [_ shape op] - (let [touched (:touched op) + [shape op] + (let [touched (:touched op) in-copy? (ctk/in-component-copy? shape)] (if (or (not in-copy?) (nil? touched) (empty? touched)) (dissoc shape :touched) (assoc shape :touched touched)))) (defmethod process-operation :set-remote-synced - [_ shape op] + [shape op] (let [remote-synced (:remote-synced op) - in-copy? (ctk/in-component-copy? shape)] + in-copy? (ctk/in-component-copy? shape)] (if (or (not in-copy?) (not remote-synced)) (dissoc shape :remote-synced) (assoc shape :remote-synced true)))) (defmethod process-operation :default - [_ _ op] + [_ op] (ex/raise :type :not-implemented :code :operation-not-implemented :context {:type (:type op)})) @@ -856,5 +879,3 @@ (defmethod frames-changed :default [_ _] nil) - - diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index d219fc365ca53e7432849b03a3aea979269bea64..9c613a91d05d4efeed4d35771784b69ee6d5adf1 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -24,7 +24,7 @@ ;; Auxiliary functions to help create a set of changes (undo + redo) -(sm/define! ::changes +(sm/register! ::changes [:map {:title "changes"} [:redo-changes vector?] [:undo-changes seq?] @@ -69,6 +69,11 @@ ::page page ::page-id (:id page))) +(defn with-page-id + [changes page-id] + (vary-meta changes assoc + ::page-id page-id)) + (defn with-container [changes container] (if (cfh/page? container) @@ -196,6 +201,37 @@ (update :undo-changes conj {:type :mod-page :id (:id page) :name (:name page)}) (apply-changes-local))) +(defn mod-plugin-data + ([changes namespace key value] + (mod-plugin-data changes :file nil nil namespace key value)) + ([changes type id namespace key value] + (mod-plugin-data changes type id nil namespace key value)) + ([changes type id page-id namespace key value] + (let [data (::file-data (meta changes)) + old-val + (case type + :file + (get-in data [:plugin-data namespace key]) + + :page + (get-in data [:pages-index id :options :plugin-data namespace key]) + + :shape + (get-in data [:pages-index page-id :objects id :plugin-data namespace key]) + + :color + (get-in data [:colors id :plugin-data namespace key]) + + :typography + (get-in data [:typographies id :plugin-data namespace key]) + + :component + (get-in data [:components id :plugin-data namespace key]))] + (-> changes + (update :redo-changes conj {:type :mod-plugin-data :object-type type :object-id id :page-id page-id :namespace namespace :key key :value value}) + (update :undo-changes conj {:type :mod-plugin-data :object-type type :object-id id :page-id page-id :namespace namespace :key key :value old-val}) + (apply-changes-local))))) + (defn del-page [changes page] (-> changes @@ -571,13 +607,6 @@ (reduce resize-parent changes all-parents))) ;; Library changes - -(defn add-recent-color - [changes color] - (-> changes - (update :redo-changes conj {:type :add-recent-color :color color}) - (apply-changes-local))) - (defn add-color [changes color] (-> changes @@ -715,6 +744,7 @@ (map lookupf) (map mk-change)) updated-shapes)))) + (apply-changes-local))))) (defn update-component @@ -732,7 +762,8 @@ :main-instance-id (:main-instance-id new-component) :main-instance-page (:main-instance-page new-component) :annotation (:annotation new-component) - :objects (:objects new-component)}) ;; this won't exist in components-v2 (except for deleted components) + :objects (:objects new-component) ;; this won't exist in components-v2 (except for deleted components) + :modified-at (:modified-at new-component)}) (update :undo-changes conj {:type :mod-component :id id :name (:name prev-component) @@ -763,15 +794,6 @@ (update :undo-changes conj {:type :del-component :id id :main-instance main-instance}))) -(defn ignore-remote - [changes] - (letfn [(add-ignore-remote - [change-list] - (->> change-list - (mapv #(assoc % :ignore-remote? true))))] - (-> changes - (update :redo-changes add-ignore-remote) - (update :undo-changes add-ignore-remote)))) (defn reorder-grid-children [changes ids] diff --git a/common/src/app/common/files/defaults.cljc b/common/src/app/common/files/defaults.cljc index 61cd7f1188199c2b945ad4bca7eb9304d84ecc13..6ef70b5ea515de783d32f0d7645694e74c12b3d0 100644 --- a/common/src/app/common/files/defaults.cljc +++ b/common/src/app/common/files/defaults.cljc @@ -6,4 +6,4 @@ (ns app.common.files.defaults) -(def version 46) +(def version 51) diff --git a/common/src/app/common/files/helpers.cljc b/common/src/app/common/files/helpers.cljc index 287bb7bb86aa6916eb96ea3209700591e9b62fab..508bea799771c0129fc1d568406a38edcb6000b4 100644 --- a/common/src/app/common/files/helpers.cljc +++ b/common/src/app/common/files/helpers.cljc @@ -16,6 +16,8 @@ [clojure.set :as set] [cuerdas.core :as str])) +#?(:clj (set! *warn-on-reflection* true)) + (declare reduce-objects) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -327,12 +329,9 @@ "Selects the shape that will be the base to add the shapes over" [objects selected] (let [;; Gets the tree-index for all the shapes - indexed-shapes (indexed-shapes objects) - + indexed-shapes (indexed-shapes objects selected) ;; Filters the selected and retrieve a list of ids - sorted-ids (->> indexed-shapes - (filter (comp selected second)) - (map second))] + sorted-ids (map val indexed-shapes)] ;; The first id will be the top-most (get objects (first sorted-ids)))) @@ -358,15 +357,6 @@ ;; COMPONENTS HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn set-touched-group - [touched group] - (when group - (conj (or touched #{}) group))) - -(defn touched-group? - [shape group] - ((or (:touched shape) #{}) group)) - (defn make-container [page-or-component type] (assoc page-or-component :type type)) @@ -486,43 +476,65 @@ (reduce add-element (d/ordered-set) ids))) -(defn indexed-shapes - "Retrieves a list with the indexes for each element in the layer tree. - This will be used for shift+selection." - [objects] - (letfn [(red-fn [cur-idx id] - (let [[prev-idx _] (first cur-idx) - prev-idx (or prev-idx 0) - cur-idx (conj cur-idx (d/vec2 (inc prev-idx) id))] - (rec-index cur-idx id))) - (rec-index [cur-idx id] - (let [object (get objects id)] - (reduce red-fn cur-idx (reverse (:shapes object)))))] - (into {} (rec-index '() uuid/zero)))) +(defn- indexed-shapes + "Retrieves a vector with the indexes for each element in the layer + tree. This will be used for shift+selection." + [objects selected] + (loop [index 1 + result (transient []) + ;; Flag to start adding elements to the index + add? false + ;; Only add elements while we're in the selection, we finish when the selection is over + pending (set selected) + shapes (-> objects + (get uuid/zero) + (get :shapes) + (rseq))] + + (let [shape-id (first shapes)] + (if (and (d/not-empty? pending) shape-id) + (let [shape (get objects shape-id) + add? (or add? (contains? selected shape-id)) + pending (disj pending shape-id) + result (if add? + (conj! result (d/vec2 index shape-id)) + result)] + (if-let [children (get shape :shapes)] + (recur (inc index) + result + add? + pending + (concat (rseq children) (rest shapes))) + (recur (inc index) + result + add? + pending + (rest shapes)))) + (persistent! result))))) (defn expand-region-selection "Given a selection selects all the shapes between the first and last in an indexed manner (shift selection)" [objects selection] - (let [indexed-shapes (indexed-shapes objects) - filter-indexes (->> indexed-shapes - (filter (comp selection second)) - (map first)) - - from (apply min filter-indexes) - to (apply max filter-indexes)] - (->> indexed-shapes - (filter (fn [[idx _]] (and (>= idx from) (<= idx to)))) - (map second) - (into #{})))) + (let [selection (if (set? selection) selection (set selection)) + indexed-shapes (indexed-shapes objects selection) + indexes (map key indexed-shapes) + from (apply min indexes) + to (apply max indexes) + xform (comp + (filter (fn [[idx _]] (and (>= idx from) (<= idx to)))) + (map val))] + (into #{} xform indexed-shapes))) (defn order-by-indexed-shapes - [objects ids] - (let [ids (if (set? ids) ids (set ids))] - (->> (indexed-shapes objects) - (filter (fn [o] (contains? ids (val o)))) - (sort-by key) - (map val)))) + "Retrieves a ordered vector for each element in the layer tree and + filted by selected set" + [objects selected] + (let [selected (if (set? selected) selected (set selected))] + (sequence + (comp (filter (fn [o] (contains? selected (val o)))) + (map val)) + (indexed-shapes objects selected)))) (defn get-index-replacement "Given a collection of shapes, calculate their positions diff --git a/common/src/app/common/files/libraries_helpers.cljc b/common/src/app/common/files/libraries_helpers.cljc deleted file mode 100644 index 8b7f34acae0fce70c7370b8709e6346456fd1b9a..0000000000000000000000000000000000000000 --- a/common/src/app/common/files/libraries_helpers.cljc +++ /dev/null @@ -1,103 +0,0 @@ -;; 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 app.common.files.libraries-helpers - (:require - [app.common.data :as d] - [app.common.files.changes-builder :as pcb] - [app.common.files.helpers :as cfh] - [app.common.types.component :as ctk] - [app.common.types.container :as ctn] - [app.common.uuid :as uuid])) - -(defn generate-add-component-changes - [changes root objects file-id page-id components-v2] - (let [name (:name root) - [path name] (cfh/parse-path-name name) - - [root-shape new-shapes updated-shapes] - (if-not components-v2 - (ctn/make-component-shape root objects file-id components-v2) - (ctn/convert-shape-in-component root objects file-id)) - - changes (-> changes - (pcb/add-component (:id root-shape) - path - name - new-shapes - updated-shapes - (:id root) - page-id))] - [root-shape changes])) - -(defn generate-add-component - "If there is exactly one id, and it's a frame (or a group in v1), and not already a component, - use it as root. Otherwise, create a frame (v2) or group (v1) that contains all ids. Then, make a - component with it, and link all shapes to their corresponding one in the component." - [it shapes objects page-id file-id components-v2 prepare-create-group prepare-create-board] - - (let [changes (pcb/empty-changes it page-id) - shapes-count (count shapes) - first-shape (first shapes) - - from-singe-frame? - (and (= 1 shapes-count) - (cfh/frame-shape? first-shape)) - - [root changes old-root-ids] - (if (and (= shapes-count 1) - (or (and (cfh/group-shape? first-shape) - (not components-v2)) - (cfh/frame-shape? first-shape)) - (not (ctk/instance-head? first-shape))) - [first-shape - (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects)) - (:shapes first-shape)] - - (let [root-name (if (= 1 shapes-count) - (:name first-shape) - "Component 1") - - shape-ids (into (d/ordered-set) (map :id) shapes) - - [root changes] - (if-not components-v2 - (prepare-create-group it ; These functions needs to be passed as argument - objects ; to avoid a circular dependence - page-id - shapes - root-name - (not (ctk/instance-head? first-shape))) - (prepare-create-board changes - (uuid/next) - (:parent-id first-shape) - objects - shape-ids - nil - root-name - true))] - - [root changes shape-ids])) - - changes - (cond-> changes - (not from-singe-frame?) - (pcb/update-shapes - (:shapes root) - (fn [shape] - (assoc shape :constraints-h :scale :constraints-v :scale)))) - - objects' (assoc objects (:id root) root) - - [root-shape changes] (generate-add-component-changes changes root objects' file-id page-id components-v2) - - changes (pcb/update-shapes changes - old-root-ids - #(dissoc % :component-root) - [:component-root])] - - [root (:id root-shape) changes])) diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index 8ba08578021d15d23c8bc44e819a12eebf880b63..111d05072645c67eddf205d7a115e0ff2c29da24 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -22,6 +22,9 @@ [app.common.schema :as sm] [app.common.svg :as csvg] [app.common.text :as txt] + [app.common.types.color :as ctc] + [app.common.types.component :as ctk] + [app.common.types.file :as ctf] [app.common.types.shape :as cts] [app.common.types.shape.shadow :as ctss] [app.common.uuid :as uuid] @@ -45,7 +48,7 @@ data data] (if-let [[to-version migrate-fn] (first migrations)] (let [migrate-fn (or migrate-fn identity)] - (l/inf :hint "migrate file" + (l/trc :hint "migrate file" :op (if (>= from-version to-version) "down" "up") :file-id (str (:id data)) :version to-version) @@ -73,7 +76,7 @@ [{:keys [version] :as file}] (if (int? version) file - (let [version (or version (-> file :data :version))] + (let [version (or (-> file :data :version) 0)] (-> file (assoc :version version) (update :data dissoc :version))))) @@ -853,11 +856,11 @@ (defn migrate-up-44 [data] (letfn [(fix-shadow [shadow] - (if (string? (:color shadow)) - (let [color {:color (:color shadow) - :opacity 1}] - (assoc shadow :color color)) - shadow)) + (let [color (if (string? (:color shadow)) + {:color (:color shadow) + :opacity 1} + (d/without-nils (:color shadow)))] + (assoc shadow :color color))) (update-object [object] (d/update-when object :shadow @@ -898,6 +901,122 @@ (update :pages-index update-vals update-container) (update :components update-vals update-container)))) +(defn migrate-up-47 + [data] + (letfn [(fix-shape [page shape] + (let [file {:id (:id data) :data data} + component-file (:component-file shape) + ;; On cloning a file, the component-file of the shapes point to the old file id + ;; this is a workaround to be able to found the components in that case + libraries {component-file {:id component-file :data data}} + ref-shape (ctf/find-ref-shape file page libraries shape {:include-deleted? true :with-context? true}) + ref-parent (get (:objects (:container (meta ref-shape))) (:parent-id ref-shape)) + shape-swap-slot (ctk/get-swap-slot shape) + ref-swap-slot (ctk/get-swap-slot ref-shape)] + (if (and (some? shape-swap-slot) + (= shape-swap-slot ref-swap-slot) + (ctk/main-instance? ref-parent)) + (ctk/remove-swap-slot shape) + shape))) + + (update-page [page] + (d/update-when page :objects update-vals (partial fix-shape page)))] + (-> data + (update :pages-index update-vals update-page)))) + +(defn migrate-up-48 + [data] + (letfn [(fix-shape [shape] + (let [swap-slot (ctk/get-swap-slot shape)] + (if (and (some? swap-slot) + (not (ctk/subcopy-head? shape))) + (ctk/remove-swap-slot shape) + shape))) + + (update-page [page] + (d/update-when page :objects update-vals fix-shape))] + (-> data + (update :pages-index update-vals update-page)))) + +(defn migrate-up-49 + "Remove hide-in-viewer for shapes that are origin or destination of an interaction" + [data] + (letfn [(update-object [destinations object] + (cond-> object + (or (:interactions object) + (contains? destinations (:id object))) + (dissoc object :hide-in-viewer))) + + (update-page [page] + (let [destinations (->> page + :objects + (vals) + (mapcat :interactions) + (map :destination) + (set))] + (update page :objects update-vals (partial update-object destinations))))] + + (update data :pages-index update-vals update-page))) + +(defn migrate-up-50 + "This migration mainly fixes paths with curve-to segments + without :c1x :c1y :c2x :c2y properties. Additionally, we found a + case where the params instead to be plain hash-map, is a points + instance. This migration normalizes all params to plain map." + + [data] + (let [update-segment + (fn [{:keys [command params] :as segment}] + (let [params (into {} params) + params (cond + (= :curve-to command) + (let [x (get params :x) + y (get params :y)] + + (cond-> params + (nil? (:c1x params)) + (assoc :c1x x) + + (nil? (:c1y params)) + (assoc :c1y y) + + (nil? (:c2x params)) + (assoc :c2x x) + + (nil? (:c2y params)) + (assoc :c2y y))) + + :else + params)] + + (assoc segment :params params))) + + update-shape + (fn [shape] + (if (cfh/path-shape? shape) + (d/update-when shape :content (fn [content] (mapv update-segment content))) + shape)) + + update-container + (fn [page] + (d/update-when page :objects update-vals update-shape))] + + (-> data + (update :pages-index update-vals update-container) + (update :components update-vals update-container)))) + +(def ^:private valid-color? + (sm/lazy-validator ::ctc/color)) + +(defn migrate-up-51 + "This migration fixes library invalid colors" + + [data] + (let [update-colors + (fn [colors] + (into {} (filter #(-> % val valid-color?) colors)))] + (update data :colors update-colors))) + (def migrations "A vector of all applicable migrations" [{:id 2 :migrate-up migrate-up-2} @@ -935,4 +1054,9 @@ {:id 43 :migrate-up migrate-up-43} {:id 44 :migrate-up migrate-up-44} {:id 45 :migrate-up migrate-up-45} - {:id 46 :migrate-up migrate-up-46}]) + {:id 46 :migrate-up migrate-up-46} + {:id 47 :migrate-up migrate-up-47} + {:id 48 :migrate-up migrate-up-48} + {:id 49 :migrate-up migrate-up-49} + {:id 50 :migrate-up migrate-up-50} + {:id 51 :migrate-up migrate-up-51}]) diff --git a/common/src/app/common/files/repair.cljc b/common/src/app/common/files/repair.cljc index 4c42d9225bd14f3580a5e4d5c993aaa8e80976f1..67f90dafebc3f4801c83584ddf4b71be70dbdae1 100644 --- a/common/src/app/common/files/repair.cljc +++ b/common/src/app/common/files/repair.cljc @@ -460,6 +460,87 @@ (pcb/with-library-data file-data) (pcb/update-component (:id shape) repair-component)))) +(defmethod repair-error :misplaced-slot + [_ {:keys [shape page-id] :as error} file-data _] + (let [repair-shape + (fn [shape] + ;; Remove the swap slot + (log/debug :hint (str " -> remove swap-slot")) + (ctk/remove-swap-slot shape))] + + (log/dbg :hint "repairing shape :misplaced-slot" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + +(defmethod repair-error :duplicate-slot + [_ {:keys [shape page-id] :as error} file-data _] + (let [page (ctpl/get-page file-data page-id) + childs (map #(get (:objects page) %) (:shapes shape)) + child-with-duplicate (let [result (reduce (fn [[seen duplicates] item] + (let [swap-slot (ctk/get-swap-slot item)] + (if (contains? seen swap-slot) + [seen (conj duplicates item)] + [(conj seen swap-slot) duplicates]))) + [#{} []] + childs)] + (second result)) + repair-shape + (fn [shape] + ;; Remove the swap slot + (log/debug :hint " -> remove swap-slot" :child-id (:id shape)) + (ctk/remove-swap-slot shape))] + + (log/dbg :hint "repairing shape :duplicated-slot" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes (map :id child-with-duplicate) repair-shape)))) + + + +(defmethod repair-error :component-duplicate-slot + [_ {:keys [shape] :as error} file-data _] + (let [main-shape (get-in shape [:objects (:main-instance-id shape)]) + childs (map #(get (:objects shape) %) (:shapes main-shape)) + childs-with-duplicate (let [result (reduce (fn [[seen duplicates] item] + (let [swap-slot (ctk/get-swap-slot item)] + (if (contains? seen swap-slot) + [seen (conj duplicates item)] + [(conj seen swap-slot) duplicates]))) + [#{} []] + childs)] + (second result)) + duplicated-ids (set (mapv :id childs-with-duplicate)) + repair-component + (fn [component] + (let [objects (reduce-kv (fn [acc k v] + (if (contains? duplicated-ids k) + (assoc acc k (ctk/remove-swap-slot v)) + (assoc acc k v))) + {} + (:objects component))] + (assoc component :objects objects)))] + + (log/dbg :hint "repairing component :component-duplicated-slot" :id (:id shape) :name (:name shape)) + (-> (pcb/empty-changes nil) + (pcb/with-library-data file-data) + (pcb/update-component (:id shape) repair-component)))) + +(defmethod repair-error :missing-slot + [_ {:keys [shape page-id args] :as error} file-data _] + (let [repair-shape + (fn [shape] + ;; Set the desired swap slot + (let [slot (:swap-slot args)] + (when (some? slot) + (log/debug :hint (str " -> set swap-slot to " slot)) + (ctk/set-swap-slot shape slot))))] + + (log/dbg :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id) + (-> (pcb/empty-changes nil page-id) + (pcb/with-file-data file-data) + (pcb/update-shapes [(:id shape)] repair-shape)))) + (defmethod repair-error :default [_ error file _] (log/error :hint "Unknown error code, don't know how to repair" :code (:code error)) diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index a5ce2e1da5f2ab3ab46cf655832b56c8ef7d2d85..5eb708ab34a57cbeb452be0085aaf499d145908c 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -6,6 +6,7 @@ (ns app.common.files.validate (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.exceptions :as ex] [app.common.files.helpers :as cfh] @@ -30,9 +31,11 @@ :child-not-found :frame-not-found :invalid-frame + :component-duplicate-slot :component-not-main :component-main-external :component-not-found + :duplicate-slot :invalid-main-instance-id :invalid-main-instance-page :invalid-main-instance @@ -50,7 +53,9 @@ :not-head-copy-not-allowed :not-component-not-allowed :component-nil-objects-not-allowed - :instance-head-not-frame}) + :instance-head-not-frame + :misplaced-slot + :missing-slot}) (def ^:private schema:error @@ -61,7 +66,7 @@ [:shape {:optional true} :map] ; Cannot validate a shape because here it may be broken [:shape-id {:optional true} ::sm/uuid] [:file-id ::sm/uuid] - [:page-id ::sm/uuid]])) + [:page-id {:optional true} [:maybe ::sm/uuid]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ERROR HANDLING @@ -69,6 +74,11 @@ (def ^:dynamic ^:private *errors* nil) +(defn- library-exists? + [file libraries shape] + (or (= (:component-file shape) (:id file)) + (contains? libraries (:component-file shape)))) + (defn- report-error [code hint shape file page & {:as args}] (let [error {:code code @@ -218,12 +228,11 @@ "Shape not expected to be main instance" shape file page)) - (let [library-exists? (or (= (:component-file shape) (:id file)) - (contains? libraries (:component-file shape))) - component (when library-exists? + (let [library-exists (library-exists? file libraries shape) + component (when library-exists (ctf/resolve-component shape file libraries {:include-deleted? true}))] (if (nil? component) - (when library-exists? + (when library-exists (report-error :component-not-found (str/ffmt "Component % not found in file %" (:component-id shape) (:component-file shape)) shape file page)) @@ -265,11 +274,10 @@ (defn- check-component-ref "Validate that the referenced shape exists in the near component." [shape file page libraries] - (let [library-exists? (or (= (:component-file shape) (:id file)) - (contains? libraries (:component-file shape))) - ref-shape (when library-exists? + (let [library-exists (library-exists? file libraries shape) + ref-shape (when library-exists (ctf/find-ref-shape file page libraries shape :include-deleted? true))] - (when (and library-exists? (nil? ref-shape)) + (when (and library-exists (nil? ref-shape)) (report-error :ref-shape-not-found (str/ffmt "Referenced shape % not found in near component" (:shape-ref shape)) shape file page)))) @@ -282,6 +290,30 @@ "Shape inside main instance should not have shape-ref" shape file page))) +(defn- check-empty-swap-slot + "Validate that this shape does not have any swap slot." + [shape file page] + (when (some? (ctk/get-swap-slot shape)) + (report-error :misplaced-slot + "This shape should not have swap slot" + shape file page))) + +(defn- has-duplicate-swap-slot? + [shape container] + (let [shapes (map #(get (:objects container) %) (:shapes shape)) + slots (->> (map #(ctk/get-swap-slot %) shapes) + (remove nil?)) + counts (frequencies slots)] + (some (fn [[_ count]] (> count 1)) counts))) + +(defn- check-duplicate-swap-slot + "Validate that the children of this shape does not have duplicated slots." + [shape file page] + (when (has-duplicate-swap-slot? shape page) + (report-error :duplicate-slot + "This shape has children with the same swap slot" + shape file page))) + (defn- check-shape-main-root-top "Root shape of a top main instance: @@ -293,6 +325,8 @@ (check-component-main-head shape file page libraries) (check-component-root shape file page) (check-component-not-ref shape file page) + (check-empty-swap-slot shape file page) + (check-duplicate-swap-slot shape file page) (run! #(check-shape % file page libraries :context :main-top) (:shapes shape))) (defn- check-shape-main-root-nested @@ -304,6 +338,7 @@ (check-component-main-head shape file page libraries) (check-component-not-root shape file page) (check-component-not-ref shape file page) + (check-empty-swap-slot shape file page) (run! #(check-shape % file page libraries :context :main-nested) (:shapes shape))) (defn- check-shape-copy-root-top @@ -313,20 +348,27 @@ - :component-root - :shape-ref" [shape file page libraries] - (check-component-not-main-head shape file page libraries) - (check-component-root shape file page) - (check-component-ref shape file page libraries) - (run! #(check-shape % file page libraries :context :copy-top) (:shapes shape))) + ;; We propagate have to propagate to nested shapes if library is valid or not + (let [library-exists (library-exists? file libraries shape)] + (check-component-not-main-head shape file page libraries) + (check-component-root shape file page) + (check-component-ref shape file page libraries) + (check-empty-swap-slot shape file page) + (check-duplicate-swap-slot shape file page) + (run! #(check-shape % file page libraries :context :copy-top :library-exists library-exists) (:shapes shape)))) (defn- check-shape-copy-root-nested "Root shape of a nested copy instance - :component-id - :component-file - :shape-ref" - [shape file page libraries] + [shape file page libraries library-exists] (check-component-not-main-head shape file page libraries) (check-component-not-root shape file page) - (check-component-ref shape file page libraries) + ;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached + ;; so we only validate the shape-ref if the ancestor is from a valid library + (when library-exists + (check-component-ref shape file page libraries)) (run! #(check-shape % file page libraries :context :copy-nested) (:shapes shape))) (defn- check-shape-main-not-root @@ -335,6 +377,7 @@ (check-component-not-main-not-head shape file page) (check-component-not-root shape file page) (check-component-not-ref shape file page) + (check-empty-swap-slot shape file page) (run! #(check-shape % file page libraries :context :main-any) (:shapes shape))) (defn- check-shape-copy-not-root @@ -343,6 +386,7 @@ (check-component-not-main-not-head shape file page) (check-component-not-root shape file page) (check-component-ref shape file page libraries) + (check-empty-swap-slot shape file page) (run! #(check-shape % file page libraries :context :copy-any) (:shapes shape))) (defn- check-shape-not-component @@ -352,6 +396,7 @@ (check-component-not-main-not-head shape file page) (check-component-not-root shape file page) (check-component-not-ref shape file page) + (check-empty-swap-slot shape file page) (run! #(check-shape % file page libraries :context :not-component) (:shapes shape))) (defn- check-shape @@ -367,7 +412,7 @@ - :main-any - :copy-any " - [shape-id file page libraries & {:keys [context] :or {context :not-component}}] + [shape-id file page libraries & {:keys [context library-exists] :or {context :not-component library-exists false}}] (let [shape (ctst/get-shape page shape-id)] (when (some? shape) (check-geometry shape file page) @@ -406,7 +451,7 @@ (report-error :nested-copy-not-allowed "Nested copy component only allowed inside other component" shape file page) - (check-shape-copy-root-nested shape file page libraries))))) + (check-shape-copy-root-nested shape file page libraries library-exists))))) (if (ctk/in-component-copy? shape) (if-not (#{:copy-top :copy-nested :copy-any} context) @@ -428,13 +473,24 @@ shape file page) (check-shape-not-component shape file page libraries)))))))) +(defn check-component-duplicate-swap-slot + [component file] + (let [shape (get-in component [:objects (:main-instance-id component)])] + (when (has-duplicate-swap-slot? shape component) + (report-error :component-duplicate-slot + "This deleted component has children with the same swap slot" + component file nil)))) + + (defn- check-component "Validate semantic coherence of a component. Report all errors found." [component file] (when (and (contains? component :objects) (nil? (:objects component))) (report-error :component-nil-objects-not-allowed "Objects list cannot be nil" - component file nil))) + component file nil)) + (when (:deleted component) + (check-component-duplicate-swap-slot component file))) (defn- get-orphan-shapes [{:keys [objects] :as page}] @@ -446,6 +502,8 @@ ;; PUBLIC API: VALIDATION FUNCTIONS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(declare check-swap-slots) + (defn validate-file "Validate full referential integrity and semantic coherence on file data. @@ -456,6 +514,8 @@ (doseq [page (filter :id (ctpl/pages-seq data))] (check-shape uuid/zero file page libraries) + (when (str/includes? (:name file) "check-swap-slot") + (check-swap-slots uuid/zero file page libraries)) (->> (get-orphan-shapes page) (run! #(check-shape % file page libraries)))) @@ -509,3 +569,41 @@ :hint "error on validating file referential integrity" :file-id (:id file) :details errors))) + + +(declare compare-slots) + +;; Optional check to look for missing swap slots. +;; Search for copies that do not point the shape-ref to the near component but don't have swap slot +;; (looking for position relative to the parent, in the copy and the main). +;; +;; This check cannot be generally enabled, because files that have been migrated from components v1 +;; may have copies with shapes that do not match by position, but have not been swapped. So we enable +;; it for specific files only. To activate the check, you need to add the string "check-swap-slot" to +;; the name of the file. +(defn- check-swap-slots + [shape-id file page libraries] + (let [shape (ctst/get-shape page shape-id)] + (if (and (ctk/instance-root? shape) (ctk/in-component-copy? shape)) + (let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true :with-context? true) + container (:container (meta ref-shape))] + (when (some? ref-shape) + (compare-slots shape ref-shape file page container))) + (doall (for [child-id (:shapes shape)] + (check-swap-slots child-id file page libraries)))))) + +(defn- compare-slots + [shape-copy shape-main file container-copy container-main] + (if (and (not= (:shape-ref shape-copy) (:id shape-main)) + (nil? (ctk/get-swap-slot shape-copy))) + (report-error :missing-slot + "Shape has been swapped, should have swap slot" + shape-copy file container-copy + :swap-slot (or (ctk/get-swap-slot shape-main) (:id shape-main))) + (when (nil? (ctk/get-swap-slot shape-copy)) + (let [children-id-pairs (d/zip-all (:shapes shape-copy) (:shapes shape-main))] + (doall (for [[child-copy-id child-main-id] children-id-pairs] + (let [child-copy (ctst/get-shape container-copy child-copy-id) + child-main (ctst/get-shape container-main child-main-id)] + (when (and (some? child-copy) (some? child-main)) + (compare-slots child-copy child-main file container-copy container-main))))))))) diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index ee86f729abe7043cf04d07d4e6ab3721dafcf14c..93b88f87eb6a03ffb3a2f2372e035ed37cef8bdd 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -12,9 +12,7 @@ (def default "A common flags that affects both: backend and frontend." [:enable-registration - :enable-login-with-password - :enable-login-illustration - :enable-feature-styles-v2]) + :enable-login-with-password]) (defn parse [& flags] @@ -35,5 +33,3 @@ :else (recur (rest flags) result))))))) - - diff --git a/common/src/app/common/fressian.clj b/common/src/app/common/fressian.clj index 282620698a3e2be3a86376dc04edb9dcec3e384c..4a640cd8cbf05f9a087e70356f8c8de89d89cefc 100644 --- a/common/src/app/common/fressian.clj +++ b/common/src/app/common/fressian.clj @@ -15,6 +15,7 @@ java.time.Instant java.time.OffsetDateTime java.util.List + linked.map.LinkedMap org.fressian.Reader org.fressian.StreamingWriter org.fressian.Writer @@ -109,6 +110,13 @@ (clojure.lang.PersistentArrayMap. (.toArray kvs)) (clojure.lang.PersistentHashMap/create (seq kvs))))) +(defn read-ordered-map + [^Reader rdr] + (let [kvs ^java.util.List (read-object! rdr)] + (reduce #(assoc %1 (first %2) (second %2)) + (d/ordered-map) + (partition-all 2 (seq kvs))))) + (def ^:dynamic *write-handler-lookup* nil) (def ^:dynamic *read-handler-lookup* nil) @@ -225,6 +233,11 @@ :wfn write-map-like :rfn read-map-like} + {:name "linked/map" + :class LinkedMap + :wfn write-map-like + :rfn read-ordered-map} + {:name "clj/keyword" :class clojure.lang.Keyword :wfn write-named diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc index d435d861cc1634dd4da2991f9a317a2cfb6161ab..d6e545cd903fd04979612a53a46d1876f83896b9 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -67,16 +67,6 @@ ([a b c d e f] (pos->Matrix a b c d e f))) -(def number-regex - #"[+-]?\d*(\.\d+)?([eE][+-]?\d+)?") - -(defn str->matrix - [matrix-str] - (let [params (->> (re-seq number-regex matrix-str) - (filter #(-> % first seq)) - (map (comp d/parse-double first)))] - (apply matrix params))) - (def ^:private schema:matrix-attrs [:map {:title "MatrixAttrs"} [:a ::sm/safe-double] @@ -87,41 +77,70 @@ [:f ::sm/safe-double]]) (def valid-matrix? - (sm/lazy-validator + (sm/validator [:and [:fn matrix?] schema:matrix-attrs])) -(sm/def! ::matrix - (letfn [(decode [o] - (if (map? o) - (map->Matrix o) - (if (string? o) - (str->matrix o) - o))) - (encode [o] - (dm/str (dm/get-prop o :a) "," - (dm/get-prop o :b) "," - (dm/get-prop o :c) "," - (dm/get-prop o :d) "," - (dm/get-prop o :e) "," - (dm/get-prop o :f) ","))] - - {:type ::matrix - :pred valid-matrix? - :type-properties - {:title "matrix" - :description "Matrix instance" - :error/message "expected a valid point" - :gen/gen (->> (sg/tuple (sg/small-double) - (sg/small-double) - (sg/small-double) - (sg/small-double) - (sg/small-double) - (sg/small-double)) - (sg/fmap #(apply pos->Matrix %))) - ::oapi/type "string" - ::oapi/format "matrix" - ::oapi/decode decode - ::oapi/encode encode}})) +(defn matrix-generator + [] + (->> (sg/tuple (sg/small-double) + (sg/small-double) + (sg/small-double) + (sg/small-double) + (sg/small-double) + (sg/small-double)) + (sg/fmap #(apply pos->Matrix %)))) + +(def ^:private number-regex + #"[+-]?\d*(\.\d+)?([eE][+-]?\d+)?") + +(defn str->matrix + [matrix-str] + (let [params (->> (re-seq number-regex matrix-str) + (filter #(-> % first seq)) + (map (comp d/parse-double first)))] + (apply matrix params))) + +(defn- matrix->str + [o] + (if (matrix? o) + (dm/str (dm/get-prop o :a) "," + (dm/get-prop o :b) "," + (dm/get-prop o :c) "," + (dm/get-prop o :d) "," + (dm/get-prop o :e) "," + (dm/get-prop o :f) ",") + o)) + +(defn- matrix->json + [o] + (if (matrix? o) + (into {} o) + o)) + +(defn- decode-matrix + [o] + (if (map? o) + (map->Matrix o) + (if (string? o) + (str->matrix o) + o))) + +(def schema:matrix + {:type :map + :pred valid-matrix? + :type-properties + {:title "matrix" + :description "Matrix instance" + :error/message "expected a valid matrix instance" + :gen/gen (matrix-generator) + :decode/json decode-matrix + :decode/string decode-matrix + :encode/json matrix->json + :encode/string matrix->str + ::oapi/type "string" + ::oapi/format "matrix"}}) + +(sm/register! ::matrix schema:matrix) ;; FIXME: deprecated (s/def ::a ::us/safe-float) diff --git a/common/src/app/common/geom/modifiers.cljc b/common/src/app/common/geom/modifiers.cljc index 813fd784a2db05aada9ed1582db953c9fc4b5ad7..ec4646f666de313fcdb48704be9cfce37e164011 100644 --- a/common/src/app/common/geom/modifiers.cljc +++ b/common/src/app/common/geom/modifiers.cljc @@ -269,6 +269,13 @@ (keep (mk-check-auto-layout objects)) shapes))) +(defn full-tree? + "Checks if we need to calculate the full tree or we can calculate just a partial tree. Partial + trees are more efficient but cannot be done when the layout is centered." + [objects layout-id] + (let [layout-justify-content (get-in objects [layout-id :layout-justify-content])] + (contains? #{:center :end :space-around :space-evenly :stretch} layout-justify-content))) + (defn sizing-auto-modifiers "Recalculates the layouts to adjust the sizing: auto new sizes" [modif-tree sizing-auto-layouts objects bounds ignore-constraints] @@ -286,7 +293,7 @@ (d/seek sizing-auto-layouts)) shapes - (if from-layout + (if (and from-layout (not (full-tree? objects from-layout))) (cgst/resolve-subtree from-layout layout-id objects) (cgst/resolve-tree #{layout-id} objects)) diff --git a/common/src/app/common/geom/point.cljc b/common/src/app/common/geom/point.cljc index 0a04fa747647ee0a74066c95d6b23ee3d6f3d1fa..2ac57cdbc394e293f3ef24222b860c3e97c1ec29 100644 --- a/common/src/app/common/geom/point.cljc +++ b/common/src/app/common/geom/point.cljc @@ -51,41 +51,55 @@ (s/def ::point (s/and ::point-attrs point?)) - (def ^:private schema:point-attrs [:map {:title "PointAttrs"} [:x ::sm/safe-number] [:y ::sm/safe-number]]) (def valid-point? - (sm/lazy-validator + (sm/validator [:and [:fn point?] schema:point-attrs])) -(sm/def! ::point - (letfn [(decode [p] - (if (map? p) - (map->Point p) - (if (string? p) - (let [[x y] (->> (str/split p #",") (mapv parse-double))] - (pos->Point x y)) - p))) - - (encode [p] - (dm/str (dm/get-prop p :x) "," - (dm/get-prop p :y)))] - - {:type ::point - :pred valid-point? - :type-properties - {:title "point" - :description "Point" - :error/message "expected a valid point" - :gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int)) - (sg/fmap #(apply pos->Point %))) - ::oapi/type "string" - ::oapi/format "point" - ::oapi/decode decode - ::oapi/encode encode}})) +(defn decode-point + [p] + (if (map? p) + (map->Point p) + (if (string? p) + (let [[x y] (->> (str/split p #",") (mapv parse-double))] + (pos->Point x y)) + p))) + +(defn point->str + [p] + (if (point? p) + (dm/str (dm/get-prop p :x) "," + (dm/get-prop p :y)) + p)) + +(defn point->json + [p] + (if (point? p) + (into {} p) + p)) + +;; FIXME: make like matrix +(def schema:point + {:type :map + :pred valid-point? + :type-properties + {:title "point" + :description "Point" + :error/message "expected a valid point" + :gen/gen (->> (sg/tuple (sg/small-int) (sg/small-int)) + (sg/fmap #(apply pos->Point %))) + ::oapi/type "string" + ::oapi/format "point" + :decode/json decode-point + :decode/string decode-point + :encode/json point->json + :encode/string point->str}}) + +(sm/register! ::point schema:point) (defn point-like? [{:keys [x y] :as v}] diff --git a/common/src/app/common/geom/proportions.cljc b/common/src/app/common/geom/proportions.cljc index c884aa369373e064063af908653b4ceca5871ac9..342145b68d6ac42aa0431f793209b14bde9d7c61 100644 --- a/common/src/app/common/geom/proportions.cljc +++ b/common/src/app/common/geom/proportions.cljc @@ -13,27 +13,27 @@ (defn assign-proportions [shape] (let [{:keys [width height]} (:selrect shape)] - (assoc shape :proportion (/ width height)))) - -;; --- Setup Proportions - + (assoc shape :proportion (float (/ width height))))) ; Note: we need to convert explicitly to float. + ; In Clojure (not clojurescript) when we divide +;; --- Setup Proportions ; two integers it does not create a float, but + ; a clojure.lang.Ratio object. (defn setup-proportions-image [{:keys [metadata] :as shape}] (let [{:keys [width height]} metadata] (assoc shape - :proportion (/ width height) + :proportion (float (/ width height)) :proportion-lock true))) (defn setup-proportions-size [{{:keys [width height]} :selrect :as shape}] (assoc shape - :proportion (/ width height) + :proportion (float (/ width height)) :proportion-lock true)) (defn setup-proportions-const [shape] (assoc shape - :proportion 1 + :proportion 1.0 :proportion-lock false)) (defn setup-proportions diff --git a/common/src/app/common/geom/rect.cljc b/common/src/app/common/geom/rect.cljc index 48d620adfc517e3735c8eae7ad427d6b48797588..3308b9256191188ffbb3900c906f0260b41a2690 100644 --- a/common/src/app/common/geom/rect.cljc +++ b/common/src/app/common/geom/rect.cljc @@ -80,19 +80,38 @@ [:x2 ::sm/safe-number] [:y2 ::sm/safe-number]]) -(sm/define! ::rect - [:and - {:gen/gen (->> (sg/tuple (sg/small-double) - (sg/small-double) - (sg/small-double) - (sg/small-double)) - (sg/fmap #(apply make-rect %)))} - [:fn rect?] - schema:rect-attrs]) +(defn- rect-generator + [] + (->> (sg/tuple (sg/small-double) + (sg/small-double) + (sg/small-double) + (sg/small-double)) + (sg/fmap #(apply make-rect %)))) + +(defn- decode-rect + [o] + (if (map? o) + (map->Rect o) + o)) + +(defn- rect->json + [o] + (if (rect? o) + (into {} o) + o)) + +(def schema:rect + [:and {:error/message "errors.invalid-rect" + :gen/gen (rect-generator) + :decode/json {:leave decode-rect} + :encode/json rect->json} + schema:rect-attrs + [:fn rect?]]) (def valid-rect? - (sm/lazy-validator - [:and [:fn rect?] schema:rect-attrs])) + (sm/validator schema:rect)) + +(sm/register! ::rect schema:rect) (def empty-rect (make-rect 0 0 0.01 0.01)) diff --git a/common/src/app/common/geom/shapes/bounds.cljc b/common/src/app/common/geom/shapes/bounds.cljc index 6935c0d97169ae0de58a33784d4b1b97f82a6a0f..5612837b4fcae2e7fdd81f03de4c95002e405ae4 100644 --- a/common/src/app/common/geom/shapes/bounds.cljc +++ b/common/src/app/common/geom/shapes/bounds.cljc @@ -190,3 +190,8 @@ (get-rect-filter-bounds children-bounds filters blur-value)))) +(defn get-frame-bounds + ([shape] + (get-frame-bounds shape nil)) + ([shape {:keys [ignore-margin?] :or {ignore-margin? false}}] + (get-object-bounds [] shape {:ignore-margin? ignore-margin?}))) diff --git a/common/src/app/common/geom/shapes/constraints.cljc b/common/src/app/common/geom/shapes/constraints.cljc index 3a2ab58b252d3562da3a41375a08a3fdcfcae820..120d22290178bec8bc1c3ed361f47df6fd108379 100644 --- a/common/src/app/common/geom/shapes/constraints.cljc +++ b/common/src/app/common/geom/shapes/constraints.cljc @@ -326,7 +326,9 @@ reset-modifiers? (and (gpo/axis-aligned? parent-bounds) (gpo/axis-aligned? child-bounds) - (gpo/axis-aligned? transformed-parent-bounds)) + (gpo/axis-aligned? transformed-parent-bounds) + (not= :scale constraints-h) + (not= :scale constraints-v)) modifiers (if reset-modifiers? diff --git a/common/src/app/common/geom/shapes/effects.cljc b/common/src/app/common/geom/shapes/effects.cljc index 912b2de6c0a1bf9aaef9a63a3ef332aad14470c2..8cac7e25b11d261050a6eb2ed78748eb34781268 100644 --- a/common/src/app/common/geom/shapes/effects.cljc +++ b/common/src/app/common/geom/shapes/effects.cljc @@ -1,3 +1,9 @@ +;; 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 app.common.geom.shapes.effects) (defn update-shadow-scale diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index 941b3ffc2f444a0b8174c6a64ce9f1461865b6f6..aaaf9dc3e83a0f086ec9c308766698d836d38c4b 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -845,13 +845,11 @@ (defn close-content [content] (into [] - (comp (filter sp/is-closed?) - (mapcat :data)) + (mapcat :data) (->> content (sp/close-subpaths) (sp/get-subpaths)))) - (defn ray-overlaps? [ray-point {selrect :selrect}] (and (>= (:y ray-point) (:y1 selrect)) @@ -895,6 +893,7 @@ (reduce +) (not= 0)))) +;; FIXME: this should be on upc/ namespace (defn split-line-to "Given a point and a line-to command will create a two new line-to commands that will split the original line into two given a value between 0-1" @@ -903,6 +902,7 @@ sp (gpt/lerp from-p to-p t-val)] [(upc/make-line-to sp) cmd])) +;; FIXME: this should be on upc/ namespace (defn split-curve-to "Given the point and a curve-to command will split the curve into two new curve-to commands given a value between 0-1" diff --git a/common/src/app/common/geom/shapes/strokes.cljc b/common/src/app/common/geom/shapes/strokes.cljc index 272e5e12b654d976233b83919bb3296319b45d81..905aac030c55e5da154461e551e1f380a6a80c2b 100644 --- a/common/src/app/common/geom/shapes/strokes.cljc +++ b/common/src/app/common/geom/shapes/strokes.cljc @@ -1,3 +1,9 @@ +;; 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 app.common.geom.shapes.strokes) (defn update-stroke-width diff --git a/common/src/app/common/geom/shapes/transforms.cljc b/common/src/app/common/geom/shapes/transforms.cljc index e5eae48eb76659abdf2450f3afc93370f97d7c29..ebde6bf80634918b1c4c51be4953adb47b4ca6e1 100644 --- a/common/src/app/common/geom/shapes/transforms.cljc +++ b/common/src/app/common/geom/shapes/transforms.cljc @@ -299,12 +299,16 @@ (cond-> shape (neg? dot-x) - (-> (cr/update! :flip-x not) - (cr/update! :rotation -)) + (cr/update! :flip-x not) + + (neg? dot-x) + (cr/update! :rotation -) + + (neg? dot-y) + (cr/update! :flip-y not) (neg? dot-y) - (-> (cr/update! :flip-y not) - (cr/update! :rotation -))))) + (cr/update! :rotation -)))) (defn- apply-transform-move "Given a new set of points transformed, set up the rectangle so it keeps diff --git a/common/src/app/common/json.cljc b/common/src/app/common/json.cljc new file mode 100644 index 0000000000000000000000000000000000000000..2b6fd0e6b5ad3e8e0890c4ccd76e8cbc2f4ffd3d --- /dev/null +++ b/common/src/app/common/json.cljc @@ -0,0 +1,106 @@ +;; 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 app.common.json + (:refer-clojure :exclude [read clj->js js->clj]) + (:require + #?(:clj [clojure.data.json :as j]) + [cuerdas.core :as str])) + +#?(:clj + (defn read + [reader & {:as opts}] + (j/read reader opts))) + +#?(:clj + (defn write + [writer data & {:as opts}] + (j/write data writer opts))) + +(defn read-kebab-key + [k] + (if (and (string? k) (not (str/includes? k "/"))) + (-> k str/kebab keyword) + k)) + +(defn write-camel-key + [k] + (if (or (keyword? k) (symbol? k)) + (str/camel k) + (str k))) + +#?(:cljs + (defn ->js + [x & {:keys [key-fn] + :or {key-fn write-camel-key} :as opts}] + (let [f (fn this-fn [x] + (cond + (nil? x) + nil + + (satisfies? cljs.core/IEncodeJS x) + (cljs.core/-clj->js x) + + (or (keyword? x) + (symbol? x)) + (name x) + + (number? x) + x + + (boolean? x) + x + + (map? x) + (reduce-kv (fn [m k v] + (let [k (key-fn k)] + (unchecked-set m k (this-fn v)) + m)) + #js {} + x) + + (coll? x) + (reduce (fn [arr v] + (.push arr (this-fn v)) + arr) + (array) + x) + + :else + (str x)))] + (f x)))) + +#?(:cljs + (defn ->clj + [o & {:keys [key-fn val-fn] :or {key-fn read-kebab-key val-fn identity}}] + (let [f (fn this-fn [x] + (let [x (val-fn x)] + (cond + (array? x) + (persistent! + (.reduce ^js/Array x + #(conj! %1 (this-fn %2)) + (transient []))) + + (identical? (type x) js/Object) + (persistent! + (.reduce ^js/Array (js-keys x) + #(assoc! %1 (key-fn %2) (this-fn (unchecked-get x %2))) + (transient {}))) + + :else + x)))] + (f o)))) + +(defn encode + [data & {:as opts}] + #?(:clj (j/write-str data opts) + :cljs (.stringify js/JSON (->js data opts)))) + +(defn decode + [data & {:as opts}] + #?(:clj (j/read-str data opts) + :cljs (->clj (.parse js/JSON data) opts))) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index d7780ef70ef0d822cb813df93a108ea0cd1bd6a6..f1eaee79d86b91347a9d5de37a74f5e39b56f241 100644 --- a/common/src/app/common/logging.cljc +++ b/common/src/app/common/logging.cljc @@ -153,14 +153,29 @@ (defn build-message [props] (loop [props (seq props) - result []] + result [] + body nil] (if-let [[k v] (first props)] - (if (simple-ident? k) + (cond + (simple-ident? k) (recur (next props) - (conj result (str (name k) "=" (pr-str v)))) + (conj result (str (name k) "=" (pr-str v))) + body) + + (= ::body k) + (recur (next props) + result + v) + + :else (recur (next props) - result)) - (str/join ", " result)))) + result + body)) + + (let [message (str/join ", " result)] + (if (string? body) + (str message "\n" body) + message))))) (defn build-stack-trace [cause] diff --git a/frontend/src/app/main/data/workspace/libraries_helpers.cljs b/common/src/app/common/logic/libraries.cljc similarity index 68% rename from frontend/src/app/main/data/workspace/libraries_helpers.cljs rename to common/src/app/common/logic/libraries.cljc index 296cd7c92ca77c1919a67d8884215d057ac88bb2..85382be2c10f9a517847066d2fbbd2ce8b7eaa6c 100644 --- a/frontend/src/app/main/data/workspace/libraries_helpers.cljs +++ b/common/src/app/common/logic/libraries.cljc @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.main.data.workspace.libraries-helpers +(ns app.common.logic.libraries (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -14,6 +14,7 @@ [app.common.geom.shapes :as gsh] [app.common.geom.shapes.grid-layout :as gslg] [app.common.logging :as log] + [app.common.logic.shapes :as cls] [app.common.spec :as us] [app.common.text :as txt] [app.common.types.color :as ctc] @@ -21,13 +22,15 @@ [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] [app.common.types.file :as ctf] + [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] [app.common.types.shape-tree :as ctst] + [app.common.types.shape.interactions :as ctsi] [app.common.types.shape.layout :as ctl] [app.common.types.typography :as cty] - [app.main.data.workspace.state-helpers :as wsh] - [cljs.spec.alpha :as s] - [clojure.set :as set])) + [app.common.uuid :as uuid] + [clojure.set :as set] + [clojure.spec.alpha :as s])) ;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default (log/set-level! :warn) @@ -37,10 +40,8 @@ (declare generate-sync-text-shape) (declare uses-assets?) -(declare get-assets) (declare generate-sync-shape-direct) (declare generate-sync-shape-direct-recursive) -(declare generate-sync-shape-inverse) (declare generate-sync-shape-inverse-recursive) (declare compare-children) @@ -59,10 +60,10 @@ (declare make-change) (defn pretty-file - [file-id state] - (if (= file-id (:current-file-id state)) + [file-id libraries current-file-id] + (if (= file-id current-file-id) "" - (str "<" (get-in state [:workspace-libraries file-id :name]) ">"))) + (str "<" (get-in libraries [file-id :name]) ">"))) (defn pretty-uuid [uuid] @@ -149,12 +150,45 @@ [new-component-shape new-component-shapes nil nil])))) +(defn generate-duplicate-component + "Create a new component copied from the one with the given id." + [changes library component-id components-v2] + (let [component (ctkl/get-component (:data library) component-id) + new-name (:name component) + + main-instance-page (when components-v2 + (ctf/get-component-page (:data library) component)) + + new-component-id (when components-v2 + (uuid/next)) + + [new-component-shape new-component-shapes ; <- null in components-v2 + new-main-instance-shape new-main-instance-shapes] + (duplicate-component component new-component-id (:data library))] + + (-> changes + (pcb/with-page main-instance-page) + (pcb/with-objects (:objects main-instance-page)) + (pcb/add-objects new-main-instance-shapes {:ignore-touched true}) + (pcb/add-component (if components-v2 + new-component-id + (:id new-component-shape)) + (:path component) + new-name + new-component-shapes + [] + (:id new-main-instance-shape) + (:id main-instance-page) + (:annotation component))))) + (defn generate-instantiate-component "Generate changes to create a new instance from a component." ([changes objects file-id component-id position page libraries] - (generate-instantiate-component changes objects file-id component-id position page libraries nil nil nil)) + (generate-instantiate-component changes objects file-id component-id position page libraries nil nil nil {})) - ([changes objects file-id component-id position page libraries old-id parent-id frame-id] + ([changes objects file-id component-id position page libraries old-id parent-id frame-id + {:keys [force-frame?] + :or {force-frame? false}}] (let [component (ctf/get-component libraries file-id component-id) parent (when parent-id (get objects parent-id)) library (get libraries file-id) @@ -166,7 +200,9 @@ component (:data library) position - components-v2) + components-v2 + (cond-> {} + force-frame? (assoc :force-frame-id frame-id))) first-shape (cond-> (first new-shapes) (not (nil? parent-id)) @@ -230,7 +266,11 @@ (pcb/update-shapes [shape-id] #(assoc % :component-root true)) :always - ; Near shape-refs need to be advanced one level + ; First level subinstances of a detached component can't have swap-slot + (pcb/update-shapes [shape-id] ctk/remove-swap-slot) + + (nil? (ctk/get-swap-slot shape)) + ; Near shape-refs need to be advanced one level (except if the head is already swapped) (generate-advance-nesting-level nil container libraries (:id shape))) ;; Otherwise, detach the shape and all children @@ -244,28 +284,43 @@ (let [children (cfh/get-children-with-self (:objects container) shape-id) skip-near (fn [changes shape] (let [ref-shape (ctf/find-ref-shape file container libraries shape {:include-deleted? true})] - (if (some? (:shape-ref ref-shape)) - (pcb/update-shapes changes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape))) - changes)))] + (cond-> changes + (some? (:shape-ref ref-shape)) + (pcb/update-shapes [(:id shape)] #(assoc % :shape-ref (:shape-ref ref-shape))) + + ;; When advancing level, the normal touched groups (not swap slots) of the + ;; ref-shape must be merged into the current shape, because they refer to + ;; the new referenced shape. + (some? ref-shape) + (pcb/update-shapes + [(:id shape)] + #(assoc % :touched + (clojure.set/union (:touched shape) + (ctk/normal-touched-groups ref-shape)))) + + ;; Swap slot must also be copied if the current shape has not any, + ;; except if this is the first level subcopy. + (and (some? (ctk/get-swap-slot ref-shape)) + (nil? (ctk/get-swap-slot shape)) + (not= (:id shape) shape-id)) + (pcb/update-shapes [(:id shape)] #(ctk/set-swap-slot % (ctk/get-swap-slot ref-shape))))))] + (reduce skip-near changes children))) (defn prepare-restore-component - ([library-data component-id current-page it] + ([changes library-data component-id current-page] (let [component (ctkl/get-deleted-component library-data component-id) page (or (ctf/get-component-page library-data component) (when (some #(= (:id current-page) %) (:pages library-data)) ;; If the page doesn't belong to the library, it's not valid current-page) (ctpl/get-last-page library-data))] - (prepare-restore-component nil library-data component-id it page (gpt/point 0 0) nil nil nil))) + (prepare-restore-component changes library-data component-id page (gpt/point 0 0) nil nil nil))) - ([changes library-data component-id it page delta old-id parent-id frame-id] + ([changes library-data component-id page delta old-id parent-id frame-id] (let [component (ctkl/get-deleted-component library-data component-id) parent (get-in page [:objects parent-id]) main-inst (get-in component [:objects (:main-instance-id component)]) inside-component? (some? (ctn/get-instance-root (:objects page) parent)) - origin-frame (get-in page [:objects (:frame-id main-inst)]) - ;; We are using a deleted component andit's coordenates are absolute, we must adjust them to its containing frame to adjust the delta - delta (gpt/subtract delta (-> origin-frame :selrect gpt/point)) shapes (cfh/get-children-with-self (:objects component) (:main-instance-id component)) shapes (map #(gsh/move % delta) shapes) @@ -283,7 +338,7 @@ (not inside-component?) (assoc :component-root true)) - changes (-> (or changes (pcb/empty-changes it)) + changes (-> changes (pcb/with-page page) (pcb/with-objects (:objects page)) (pcb/with-library-data library-data)) @@ -303,7 +358,7 @@ If an asset id is given, only shapes linked to this particular asset will be synchronized." - [it file-id asset-type asset-id library-id state] + [changes file-id asset-type asset-id library-id libraries current-file-id] (s/assert #{:colors :components :typographies} asset-type) (s/assert (s/nilable ::us/uuid) asset-id) (s/assert ::us/uuid file-id) @@ -312,25 +367,26 @@ (log/info :msg "Sync file with library" :asset-type asset-type :asset-id asset-id - :file (pretty-file file-id state) - :library (pretty-file library-id state)) + :file (pretty-file file-id libraries current-file-id) + :library (pretty-file library-id libraries current-file-id)) - (let [file (wsh/get-file state file-id) + (let [file (get-in libraries [file-id :data]) components-v2 (get-in file [:options :components-v2])] (loop [containers (ctf/object-containers-seq file) - changes (pcb/empty-changes it)] + changes changes] (if-let [container (first containers)] (do (recur (next containers) - (pcb/concat-changes + (pcb/concat-changes ;;TODO Remove concat changes changes - (generate-sync-container it + (generate-sync-container (pcb/empty-changes nil) asset-type asset-id library-id - state container - components-v2)))) + components-v2 + libraries + current-file-id)))) changes)))) (defn generate-sync-library @@ -340,7 +396,7 @@ If an asset id is given, only shapes linked to this particular asset will be synchronized." - [it file-id asset-type asset-id library-id state] + [changes file-id asset-type asset-id library-id libraries current-file-id] (s/assert #{:colors :components :typographies} asset-type) (s/assert (s/nilable ::us/uuid) asset-id) (s/assert ::us/uuid file-id) @@ -349,30 +405,31 @@ (log/info :msg "Sync local components with library" :asset-type asset-type :asset-id asset-id - :file (pretty-file file-id state) - :library (pretty-file library-id state)) + :file (pretty-file file-id libraries current-file-id) + :library (pretty-file library-id libraries current-file-id)) - (let [file (wsh/get-file state file-id) + (let [file (get-in libraries [file-id :data]) components-v2 (get-in file [:options :components-v2])] (loop [local-components (ctkl/components-seq file) - changes (pcb/empty-changes it)] + changes changes] (if-let [local-component (first local-components)] (recur (next local-components) - (pcb/concat-changes + (pcb/concat-changes ;;TODO Remove concat changes changes - (generate-sync-container it - asset-type - asset-id - library-id - state - (cfh/make-container local-component :component) - components-v2))) + (generate-sync-container (pcb/empty-changes nil) + asset-type + asset-id + library-id + (cfh/make-container local-component :component) + components-v2 + libraries + current-file-id))) changes)))) (defn- generate-sync-container "Generate changes to synchronize all shapes in a particular container (a page or a component) that use assets of the given type in the given library." - [it asset-type asset-id library-id state container components-v2] + [changes asset-type asset-id library-id container components-v2 libraries current-file-id] (if (cfh/page? container) (log/debug :msg "Sync page in local file" :page-id (:id container)) @@ -381,7 +438,7 @@ (let [linked-shapes (->> (vals (:objects container)) (filter #(uses-assets? asset-type asset-id % library-id)))] (loop [shapes (seq linked-shapes) - changes (-> (pcb/empty-changes it) + changes (-> changes (pcb/with-container container) (pcb/with-objects (:objects container)))] (if-let [shape (first shapes)] @@ -389,10 +446,11 @@ (generate-sync-shape asset-type changes library-id - state container shape - components-v2)) + components-v2 + libraries + current-file-id)) changes)))) (defmulti uses-assets? @@ -420,33 +478,32 @@ (defmulti generate-sync-shape "Generate changes to synchronize one shape from all assets of the given type that is using, in the given library." - (fn [asset-type _changes _library-id _state _container _shape _components-v2] asset-type)) + (fn [asset-type _changes _library-id _container _shape _components-v2 _libraries _current-file-id] asset-type)) (defmethod generate-sync-shape :components - [_ changes _library-id state container shape components-v2] + [_ changes _library-id container shape components-v2 libraries current-file-id] (let [shape-id (:id shape) - file (wsh/get-local-file-full state) - libraries (wsh/get-libraries state)] + file (get current-file-id libraries)] (generate-sync-shape-direct changes file libraries container shape-id false components-v2))) (defmethod generate-sync-shape :colors - [_ changes library-id state _ shape _] + [_ changes library-id _ shape _ libraries _] (log/debug :msg "Sync colors of shape" :shape (:name shape)) ;; Synchronize a shape that uses some colors of the library. The value of the ;; color in the library is copied to the shape. - (let [library-colors (get-assets library-id :colors state)] + (let [library-colors (get-in libraries [library-id :data :colors])] (pcb/update-shapes changes [(:id shape)] #(ctc/sync-shape-colors % library-id library-colors)))) (defmethod generate-sync-shape :typographies - [_ changes library-id state container shape _] + [_ changes library-id container shape _ libraries _] (log/debug :msg "Sync typographies of shape" :shape (:name shape)) ;; Synchronize a shape that uses some typographies of the library. The attributes ;; of the typography are copied to the shape." - (let [typographies (get-assets library-id :typographies state) + (let [typographies (get-in libraries [library-id :data :typographies]) update-node (fn [node] (if-let [typography (get typographies (:typography-ref-id node))] (merge node (dissoc typography :name :id)) @@ -454,12 +511,6 @@ :typography-ref-file)))] (generate-sync-text-shape changes shape container update-node))) -(defn- get-assets - [library-id asset-type state] - (if (= library-id (:current-file-id state)) - (get-in state [:workspace-data asset-type]) - (get-in state [:workspace-libraries library-id :data asset-type]))) - (defn- generate-sync-text-shape [changes shape container update-node] (let [old-content (:content shape) @@ -620,7 +671,8 @@ shape-main (when component (if (and reset? components-v2) - (ctf/find-remote-shape container libraries shape-inst) + ;; the reset is against the ref-shape, not against the original shape of the component + (ctf/find-ref-shape file container libraries shape-inst) (ctf/get-ref-shape library component shape-inst))) shape-inst (if (and reset? components-v2) @@ -757,7 +809,8 @@ root-inst root-main omit-touched? - set-remote-synced?) + set-remote-synced? + components-v2) changes)) both (fn [changes child-inst child-main] @@ -811,10 +864,11 @@ swapped moved false - reset?)))) + reset? + components-v2)))) - -(defn- generate-rename-component +(defn generate-rename-component + "Generate the changes for rename the component with the given id, in the current file library." [changes id new-name library-data components-v2] (let [[path name] (cfh/parse-path-name new-name) update-fn @@ -946,7 +1000,8 @@ component-container container root-inst - root-main)) + root-main + components-v2)) only-main (fn [changes child-main] (remove-shape changes @@ -999,7 +1054,8 @@ swapped moved true - true) + true + components-v2) ;; The inverse sync may be made on a component that is inside a ;; remote library. We need to separate changes that are from @@ -1017,7 +1073,7 @@ ;; ---- Operation generation helpers ---- (defn- compare-children - [changes children-inst children-main container-inst container-main file libraries only-inst-cb only-main-cb both-cb swapped-cb moved-cb inverse? reset?] + [changes children-inst children-main container-inst container-main file libraries only-inst-cb only-main-cb both-cb swapped-cb moved-cb inverse? reset? components-v2] (log/trace :msg "Compare children") (loop [children-inst (seq (or children-inst [])) children-main (seq (or children-main [])) @@ -1037,18 +1093,18 @@ (reduce only-inst-cb changes children-inst) :else - (if (or (ctk/is-main-of? child-main child-inst) + (if (or (ctk/is-main-of? child-main child-inst components-v2) (and (ctf/match-swap-slot? child-main child-inst container-inst container-main file libraries) (not reset?))) (recur (next children-inst) (next children-main) - (if (ctk/is-main-of? child-main child-inst) + (if (ctk/is-main-of? child-main child-inst components-v2) (both-cb changes child-inst child-main) (swapped-cb changes child-inst child-main))) - (let [child-inst' (d/seek #(or (ctk/is-main-of? child-main %) + (let [child-inst' (d/seek #(or (ctk/is-main-of? child-main % components-v2) (and (ctf/match-swap-slot? child-main % container-inst container-main file libraries) (not reset?))) children-inst) - child-main' (d/seek #(or (ctk/is-main-of? % child-inst) + child-main' (d/seek #(or (ctk/is-main-of? % child-inst components-v2) (and (ctf/match-swap-slot? % child-inst container-inst container-main file libraries) (not reset?))) children-main)] (cond @@ -1064,7 +1120,7 @@ :else (if inverse? - (let [is-main? (ctk/is-main-of? child-inst child-main')] + (let [is-main? (ctk/is-main-of? child-inst child-main' components-v2)] (recur (next children-inst) (remove #(= (:id %) (:id child-main')) children-main) (cond-> changes @@ -1074,7 +1130,7 @@ (swapped-cb child-inst child-main') :always (moved-cb child-inst child-main')))) - (let [is-main? (ctk/is-main-of? child-inst' child-main)] + (let [is-main? (ctk/is-main-of? child-inst' child-main components-v2)] (recur (remove #(= (:id %) (:id child-inst')) children-inst) (next children-main) (cond-> changes @@ -1086,13 +1142,13 @@ (moved-cb child-inst' child-main)))))))))))) (defn- add-shape-to-instance - [changes component-shape index component-page container root-instance root-main omit-touched? set-remote-synced?] + [changes component-shape index component-page container root-instance root-main omit-touched? set-remote-synced? components-v2] (log/info :msg (str "ADD [P " (pretty-uuid (:id container)) "] " (:name component-shape) " " (pretty-uuid (:id component-shape)))) (let [component-parent-shape (ctn/get-shape component-page (:parent-id component-shape)) - parent-shape (d/seek #(ctk/is-main-of? component-parent-shape %) + parent-shape (d/seek #(ctk/is-main-of? component-parent-shape % components-v2) (cfh/get-children-with-self (:objects container) (:id root-instance))) all-parents (into [(:id parent-shape)] @@ -1156,18 +1212,18 @@ :shapes all-parents})) changes' (reduce del-obj-change changes' new-shapes)] - (if (and (cfh/touched-group? parent-shape :shapes-group) omit-touched?) + (if (and (ctk/touched-group? parent-shape :shapes-group) omit-touched?) changes changes'))) (defn- add-shape-to-main - [changes shape index component component-container page root-instance root-main] + [changes shape index component component-container page root-instance root-main components-v2] (log/info :msg (str "ADD [C " (pretty-uuid (:id component-container)) "] " (:name shape) " " (pretty-uuid (:id shape)))) (let [parent-shape (ctn/get-shape page (:parent-id shape)) - component-parent-shape (d/seek #(ctk/is-main-of? % parent-shape) + component-parent-shape (d/seek #(ctk/is-main-of? % parent-shape components-v2) (cfh/get-children-with-self (:objects component-container) (:id root-main))) all-parents (into [(:id component-parent-shape)] @@ -1311,7 +1367,7 @@ changes' ids)] - (if (and (cfh/touched-group? parent :shapes-group) omit-touched?) + (if (and (ctk/touched-group? parent :shapes-group) omit-touched?) changes changes'))) @@ -1347,7 +1403,7 @@ :ignore-touched true :syncing true})))] - (if (and (cfh/touched-group? parent :shapes-group) omit-touched?) + (if (and (ctk/touched-group? parent :shapes-group) omit-touched?) changes changes'))) @@ -1485,9 +1541,22 @@ container {:type :reg-objects :shapes all-parents})])))) - (let [roperation {:type :set + (let [;; position-data is a special case because can be affected by :geometry-group and :content-group + ;; so, if the position-data changes but the geometry is touched we need to reset the position-data + ;; so it's calculated again + reset-pos-data? + (and (cfh/text-shape? origin-shape) + (= attr :position-data) + (not= (get origin-shape attr) (get dest-shape attr)) + (touched :geometry-group)) + + roperation {:type :set :attr attr - :val (get origin-shape attr) + :val (cond + ;; If position data changes and the geometry group is touched + ;; we need to put to nil so we can regenerate it + reset-pos-data? nil + :else (get origin-shape attr)) :ignore-touched true} uoperation {:type :set :attr attr @@ -1521,11 +1590,26 @@ (defn- update-flex-child-copy-attrs "Synchronizes the attributes inside the flex-child items (main->copy)" - [changes _shape-main shape-copy main-container main-component copy-container omit-touched?] + [changes shape-main shape-copy main-container main-component copy-container omit-touched?] + (let [new-changes (-> (pcb/empty-changes) (pcb/with-container copy-container) (pcb/with-objects (:objects copy-container)) + + ;; The layout-item-sizing needs to be update when the parent is auto or fix + (pcb/update-shapes + [(:id shape-copy)] + (fn [shape-copy] + (cond-> shape-copy + (contains? #{:auto :fix} (:layout-item-h-sizing shape-main)) + (propagate-attrs shape-main #{:layout-item-h-sizing} omit-touched?) + + (contains? #{:auto :fix} (:layout-item-h-sizing shape-main)) + (propagate-attrs shape-main #{:layout-item-v-sizing} omit-touched?))) + {:ignore-touched true}) + + ;; Update the child flex properties from the parent (pcb/update-shapes (:shapes shape-copy) (fn [child-copy] @@ -1542,6 +1626,20 @@ (-> (pcb/empty-changes) (pcb/with-page main-container) (pcb/with-objects (:objects main-container)) + + ;; The layout-item-sizing needs to be update when the parent is auto or fix + (pcb/update-shapes + [(:id shape-main)] + (fn [shape-main] + (cond-> shape-main + (contains? #{:auto :fix} (:layout-item-h-sizing shape-copy)) + (propagate-attrs shape-copy #{:layout-item-h-sizing} omit-touched?) + + (contains? #{:auto :fix} (:layout-item-h-sizing shape-copy)) + (propagate-attrs shape-copy #{:layout-item-v-sizing} omit-touched?))) + {:ignore-touched true}) + + ;; Updates the children properties from the parent (pcb/update-shapes (:shapes shape-main) (fn [child-main] @@ -1621,3 +1719,520 @@ (assoc change :page-id (:id container)) (assoc change :component-id (:id container)))) +(defn generate-add-component-changes + [changes root objects file-id page-id components-v2] + (let [name (:name root) + [path name] (cfh/parse-path-name name) + + [root-shape new-shapes updated-shapes] + (if-not components-v2 + (ctn/make-component-shape root objects file-id components-v2) + (ctn/convert-shape-in-component root objects file-id)) + + changes (-> changes + (pcb/add-component (:id root-shape) + path + name + new-shapes + updated-shapes + (:id root) + page-id))] + [root-shape changes])) + +(defn generate-add-component + "If there is exactly one id, and it's a frame (or a group in v1), and not already a component, + use it as root. Otherwise, create a frame (v2) or group (v1) that contains all ids. Then, make a + component with it, and link all shapes to their corresponding one in the component." + [changes shapes objects page-id file-id components-v2 prepare-create-group prepare-create-board] + + (let [changes (pcb/with-page-id changes page-id) + shapes-count (count shapes) + first-shape (first shapes) + + from-singe-frame? + (and (= 1 shapes-count) + (cfh/frame-shape? first-shape)) + + [root changes old-root-ids] + (if (and (= shapes-count 1) + (or (and (cfh/group-shape? first-shape) + (not components-v2)) + (cfh/frame-shape? first-shape)) + (not (ctk/instance-head? first-shape))) + [first-shape + (-> changes + (pcb/with-page-id page-id) + (pcb/with-objects objects)) + (:shapes first-shape)] + + (let [root-name (if (= 1 shapes-count) + (:name first-shape) + "Component 1") + + shape-ids (into (d/ordered-set) (map :id) shapes) + + [root changes] + (if-not components-v2 + (prepare-create-group changes ; These functions needs to be passed as argument + objects ; to avoid a circular dependence + page-id + shapes + root-name + (not (ctk/instance-head? first-shape))) + (prepare-create-board changes + (uuid/next) + (:parent-id first-shape) + objects + shape-ids + nil + root-name + true))] + + [root changes shape-ids])) + + changes + (cond-> changes + (not from-singe-frame?) + (pcb/update-shapes + (:shapes root) + (fn [shape] + (assoc shape :constraints-h :scale :constraints-v :scale)))) + + objects' (assoc objects (:id root) root) + + [root-shape changes] (generate-add-component-changes changes root objects' file-id page-id components-v2) + + changes (pcb/update-shapes changes + old-root-ids + #(dissoc % :component-root) + [:component-root])] + + [root (:id root-shape) changes])) + +(defn generate-restore-component + "Restore a deleted component, with the given id, in the given file library." + [changes library-data component-id library-id current-page objects] + (let [{:keys [changes shape]} (prepare-restore-component changes library-data component-id current-page) + parent-id (:parent-id shape) + objects (cond-> (assoc objects (:id shape) shape) + (not (nil? parent-id)) + (update-in [parent-id :shapes] + #(conj % (:id shape)))) + + ;; Adds a resize-parents operation so the groups are updated. We add all the new objects + new-objects-ids (->> changes :redo-changes (filter #(= (:type %) :add-obj)) (mapv :id)) + changes (-> changes + (pcb/with-objects objects) + (pcb/resize-parents new-objects-ids))] + + (assoc changes :file-id library-id))) + +(defn generate-detach-component + "Generate changes for remove all references to components in the shape, + with the given id and all its children, at the current page." + [changes id file page-id libraries] + (let [container (cfh/get-container file :page page-id)] + (-> changes + (pcb/with-container container) + (pcb/with-objects (:objects container)) + (generate-detach-instance container libraries id)))) + +(defn generate-new-shape-for-swap + [changes shape file page libraries id-new-component index target-cell keep-props-values] + (let [objects (:objects page) + position (gpt/point (:x shape) (:y shape)) + changes (-> changes + (pcb/with-objects objects)) + position (-> position (with-meta {:cell target-cell})) + parent (get objects (:parent-id shape)) + inside-comp? (ctn/in-any-component? objects parent) + + [new-shape changes] + (generate-instantiate-component changes + objects + (:id file) + id-new-component + position + page + libraries + nil + (:parent-id shape) + (:frame-id shape) + {:force-frame? true}) + + new-shape (cond-> new-shape + ;; if the shape isn't inside a main component, it shouldn't have a swap slot + (and (nil? (ctk/get-swap-slot new-shape)) + inside-comp?) + (ctk/set-swap-slot (ctf/find-swap-slot shape + page + {:id (:id file) + :data file} + libraries)))] + + [new-shape (-> changes + ;; Restore the properties + (pcb/update-shapes [(:id new-shape)] #(d/patch-object % keep-props-values)) + + ;; We need to set the same index as the original shape + (pcb/change-parent (:parent-id shape) [new-shape] index {:component-swap true + :ignore-touched true}) + (change-touched new-shape + shape + (ctn/make-container page :page) + {}))])) + +(defn generate-component-swap + [changes objects shape file page libraries id-new-component index target-cell keep-props-values] + (let [[all-parents changes] + (-> changes + (cls/generate-delete-shapes file page objects (d/ordered-set (:id shape)) {:components-v2 true + :component-swap true})) + [new-shape changes] + (-> changes + (generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))] + [new-shape all-parents changes])) + +(defn generate-sync-file-changes + [changes undo-group asset-type file-id asset-id library-id libraries current-file-id] + (let [sync-components? (or (nil? asset-type) (= asset-type :components)) + sync-colors? (or (nil? asset-type) (= asset-type :colors)) + sync-typographies? (or (nil? asset-type) (= asset-type :typographies))] + (cond-> changes + :always + (pcb/set-undo-group undo-group) + ;; library-changes + sync-components? + (generate-sync-library file-id :components asset-id library-id libraries current-file-id) + sync-colors? + (generate-sync-library file-id :colors asset-id library-id libraries current-file-id) + sync-typographies? + (generate-sync-library file-id :typographies asset-id library-id libraries current-file-id) + + ;; file-changes + sync-components? + (generate-sync-file file-id :components asset-id library-id libraries current-file-id) + sync-colors? + (generate-sync-file file-id :colors asset-id library-id libraries current-file-id) + sync-typographies? + (generate-sync-file file-id :typographies asset-id library-id libraries current-file-id)))) + +(defn generate-sync-head + [changes file-full libraries container id components-v2 reset?] + (let [shape-inst (ctn/get-shape container id) + objects (:objects container) + parent (get objects (:parent-id shape-inst)) + head (ctn/get-component-shape container parent) + changes + (-> changes + (pcb/with-container container) + (pcb/with-objects (:objects container)) + (generate-sync-shape-direct file-full libraries container (:id head) reset? components-v2))] + changes)) + +(defn generate-reset-component + [changes file-full libraries container id components-v2] + (let [objects (:objects container) + swap-slot (-> (ctn/get-shape container id) + (ctk/get-swap-slot)) + changes + (-> changes + (pcb/with-container container) + (pcb/with-objects objects) + (generate-sync-shape-direct file-full libraries container id true components-v2))] + + (cond-> changes + (some? swap-slot) + (generate-sync-head file-full libraries container id components-v2 true)))) + +(defn generate-duplicate-flows + [changes shapes page ids-map] + (let [flows (-> page :options :flows) + unames (volatile! (into #{} (map :name flows))) + frames-with-flow (->> shapes + (filter #(= (:type %) :frame)) + (filter #(some? (ctp/get-frame-flow flows (:id %)))))] + (if-not (empty? frames-with-flow) + (let [update-flows (fn [flows] + (reduce + (fn [flows frame] + (let [name (cfh/generate-unique-name @unames "Flow 1") + _ (vswap! unames conj name) + new-flow {:id (uuid/next) + :name name + :starting-frame (get ids-map (:id frame))}] + (ctp/add-flow flows new-flow))) + flows + frames-with-flow))] + (pcb/update-page-option changes :flows update-flows)) + changes))) + +(defn generate-duplicate-guides + [changes shapes page ids-map delta] + (let [guides (get-in page [:options :guides]) + frames (->> shapes (filter cfh/frame-shape?)) + + new-guides + (reduce + (fn [g frame] + (let [new-id (ids-map (:id frame)) + new-frame (-> frame (gsh/move delta)) + + new-guides + (->> guides + (vals) + (filter #(= (:frame-id %) (:id frame))) + (map #(-> % + (assoc :id (uuid/next)) + (assoc :frame-id new-id) + (assoc :position (if (= (:axis %) :x) + (+ (:position %) (- (:x new-frame) (:x frame))) + (+ (:position %) (- (:y new-frame) (:y frame))))))))] + (cond-> g + (not-empty new-guides) + (conj (into {} (map (juxt :id identity) new-guides)))))) + guides + frames)] + (-> (pcb/with-page changes page) + (pcb/set-page-option :guides new-guides)))) + +(defn generate-duplicate-component-change + [changes objects page component-root parent-id frame-id delta libraries library-data] + (let [component-id (:component-id component-root) + file-id (:component-file component-root) + main-component (ctf/get-component libraries file-id component-id) + moved-component (gsh/move component-root delta) + pos (gpt/point (:x moved-component) (:y moved-component)) + origin-frame (get-in page [:objects frame-id]) + delta (cond-> delta + (some? origin-frame) + (gpt/subtract (-> origin-frame :selrect gpt/point))) + + instantiate-component + #(generate-instantiate-component changes + objects + file-id + (:component-id component-root) + pos + page + libraries + (:id component-root) + parent-id + frame-id + {}) + + restore-component + #(let [restore (prepare-restore-component changes library-data (:component-id component-root) page delta (:id component-root) parent-id frame-id)] + [(:shape restore) (:changes restore)]) + + [_shape changes] + (if (nil? main-component) + (restore-component) + (instantiate-component))] + changes)) + +(defn generate-duplicate-shape-change + ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id] + (generate-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id (:frame-id obj) (:parent-id obj) false false true)) + + ([changes objects page unames update-unames! ids-map obj delta level-delta libraries library-data file-id frame-id parent-id duplicating-component? child? remove-swap-slot?] + (cond + (nil? obj) + changes + + (ctf/is-main-of-known-component? obj libraries) + (generate-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data) + + :else + (let [frame? (cfh/frame-shape? obj) + group? (cfh/group-shape? obj) + bool? (cfh/bool-shape? obj) + new-id (ids-map (:id obj)) + parent-id (or parent-id frame-id) + parent (get objects parent-id) + name (:name obj) + + is-component-root? (or (:saved-component-root obj) + ;; Backward compatibility + (:saved-component-root? obj) + (ctk/instance-root? obj)) + duplicating-component? (or duplicating-component? (ctk/instance-head? obj)) + is-component-main? (ctk/main-instance? obj) + subinstance-head? (ctk/subinstance-head? obj) + instance-root? (ctk/instance-root? obj) + + into-component? (and duplicating-component? + (ctn/in-any-component? objects parent)) + + level-delta (if (some? level-delta) + level-delta + (ctn/get-nesting-level-delta objects obj parent)) + new-shape-ref (ctf/advance-shape-ref nil page libraries obj level-delta {:include-deleted? true}) + + regenerate-component + (fn [changes shape] + (let [components-v2 (dm/get-in library-data [:options :components-v2]) + [_ changes] (generate-add-component-changes changes shape objects file-id (:id page) components-v2)] + changes)) + + new-obj + (-> obj + (assoc :id new-id + :name name + :parent-id parent-id + :frame-id frame-id) + + (cond-> (and (not instance-root?) + subinstance-head? + remove-swap-slot?) + (ctk/remove-swap-slot)) + + (dissoc :shapes + :use-for-thumbnail) + + (cond-> (not is-component-root?) + (dissoc :main-instance)) + + (cond-> into-component? + (dissoc :component-root)) + + (cond-> (and (ctk/instance-head? obj) + (not into-component?)) + (assoc :component-root true)) + + (cond-> (or frame? group? bool?) + (assoc :shapes [])) + + (cond-> (and (some? new-shape-ref) + (not= new-shape-ref (:shape-ref obj))) + (assoc :shape-ref new-shape-ref)) + + (gsh/move delta) + (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)) + + (cond-> (ctl/grid-layout? obj) + (ctl/remap-grid-cells ids-map))) + + new-obj (cond-> new-obj + (not duplicating-component?) + (ctk/detach-shape)) + + ;; We want the first added object to touch it's parent, but not subsequent children + changes (-> (pcb/add-object changes new-obj {:ignore-touched (and duplicating-component? child?)}) + (pcb/amend-last-change #(assoc % :old-id (:id obj))) + (cond-> (ctl/grid-layout? objects (:parent-id obj)) + (-> (pcb/update-shapes [(:parent-id obj)] ctl/assign-cells {:with-objects? true}) + (pcb/reorder-grid-children [(:parent-id obj)])))) + + changes (cond-> changes + (and is-component-root? is-component-main?) + (regenerate-component new-obj)) + + ;; This is needed for the recursive call to find the new object as parent + page' (ctst/add-shape (:id new-obj) + new-obj + {:objects objects} + (:frame-id new-obj) + (:parent-id new-obj) + nil + true)] + + (reduce (fn [changes child] + (generate-duplicate-shape-change changes + (:objects page') + page + unames + update-unames! + ids-map + child + delta + level-delta + libraries + library-data + file-id + (if frame? new-id frame-id) + new-id + duplicating-component? + true + (and remove-swap-slot? + ;; only remove swap slot of children when the current shape + ;; is not a subinstance head nor a instance root + (not subinstance-head?) + (not instance-root?)))) + changes + (map (d/getf objects) (:shapes obj))))))) + +(defn generate-duplicate-changes + "Prepare objects to duplicate: generate new id, give them unique names, + move to the desired position, and recalculate parents and frames as needed." + [changes all-objects page ids delta libraries library-data file-id] + (let [shapes (map (d/getf all-objects) ids) + unames (volatile! (cfh/get-used-names (:objects page))) + update-unames! (fn [new-name] (vswap! unames conj new-name)) + all-ids (reduce #(into %1 (cons %2 (cfh/get-children-ids all-objects %2))) (d/ordered-set) ids) + + ;; We need ids-map for remapping the grid layout. But when duplicating the guides + ;; we calculate a new one because the components will have created new shapes. + ids-map (into {} (map #(vector % (uuid/next))) all-ids) + + changes (-> changes + (pcb/with-page page) + (pcb/with-objects all-objects)) + changes + (->> shapes + (reduce #(generate-duplicate-shape-change %1 + all-objects + page + unames + update-unames! + ids-map + %2 + delta + nil + libraries + library-data + file-id) + changes)) + + ;; We need to check the changes to get the ids-map + ids-map + (into {} + (comp + (filter #(= :add-obj (:type %))) + (map #(vector (:old-id %) (-> % :obj :id)))) + (:redo-changes changes))] + + (-> changes + (generate-duplicate-flows shapes page ids-map) + (generate-duplicate-guides shapes page ids-map delta)))) + +(defn generate-duplicate-changes-update-indices + "Updates the changes to correctly set the indexes of the duplicated objects, + depending on the index of the original object respect their parent." + [changes objects ids] + (let [;; index-map is a map that goes from parent-id => vector([id index-in-parent]) + index-map (reduce (fn [index-map id] + (let [parent-id (get-in objects [id :parent-id]) + parent-index (cfh/get-position-on-parent objects id)] + (update index-map parent-id (fnil conj []) [id parent-index]))) + {} + ids) + + inc-indices + (fn [[offset result] [id index]] + [(inc offset) (conj result [id (+ index offset)])]) + + fix-indices + (fn [_ entry] + (->> entry + (sort-by second) + (reduce inc-indices [1 []]) + (second) + (into {}))) + + objects-indices (->> index-map (d/mapm fix-indices) (vals) (reduce merge))] + + (pcb/amend-changes + changes + (fn [change] + (assoc change :index (get objects-indices (:old-id change))))))) diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc new file mode 100644 index 0000000000000000000000000000000000000000..f5d38f0c27279c9ae7706286d5fc5b46b202ef21 --- /dev/null +++ b/common/src/app/common/logic/shapes.cljc @@ -0,0 +1,422 @@ +;; 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 app.common.logic.shapes + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.geom.shapes :as gsh] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.page :as ctp] + [app.common.types.shape.interactions :as ctsi] + [app.common.types.shape.layout :as ctl] + [app.common.uuid :as uuid])) + +(defn generate-update-shapes + [changes ids update-fn objects {:keys [attrs ignore-tree ignore-touched with-objects?]}] + (let [changes (reduce + (fn [changes id] + (let [opts {:attrs attrs + :ignore-geometry? (get ignore-tree id) + :ignore-touched ignore-touched + :with-objects? with-objects?}] + (pcb/update-shapes changes [id] update-fn (d/without-nils opts)))) + (-> changes + (pcb/with-objects objects)) + ids) + grid-ids (->> ids (filter (partial ctl/grid-layout? objects))) + changes (pcb/update-shapes changes grid-ids ctl/assign-cell-positions {:with-objects? true}) + changes (pcb/reorder-grid-children changes ids)] + changes)) + +(defn- generate-update-shape-flags + [changes ids objects {:keys [blocked hidden] :as flags}] + (let [update-fn + (fn [obj] + (cond-> obj + (boolean? blocked) (assoc :blocked blocked) + (boolean? hidden) (assoc :hidden hidden))) + + ids (if (boolean? blocked) + (into ids (->> ids (mapcat #(cfh/get-children-ids objects %)))) + ids)] + (-> changes + (pcb/update-shapes ids update-fn {:attrs #{:blocked :hidden}})))) + +(defn generate-delete-shapes + [changes file page objects ids {:keys [components-v2 ignore-touched component-swap]}] + (let [ids (cfh/clean-loops objects ids) + + in-component-copy? + (fn [shape-id] + ;; Look for shapes that are inside a component copy, but are + ;; not the root. In this case, they must not be deleted, + ;; but hidden (to be able to recover them more easily). + ;; Unless we are doing a component swap, in which case we want + ;; to delete the old shape + (let [shape (get objects shape-id)] + (and (ctn/has-any-copy-parent? objects shape) + (not component-swap)))) + + [ids-to-delete ids-to-hide] + (if components-v2 + (loop [ids-seq (seq ids) + ids-to-delete [] + ids-to-hide []] + (let [id (first ids-seq)] + (if (nil? id) + [ids-to-delete ids-to-hide] + (if (in-component-copy? id) + (recur (rest ids-seq) + ids-to-delete + (conj ids-to-hide id)) + (recur (rest ids-seq) + (conj ids-to-delete id) + ids-to-hide))))) + [ids []]) + + changes (-> changes + (pcb/with-page page) + (pcb/with-objects objects) + (pcb/with-library-data file)) + lookup (d/getf objects) + groups-to-unmask + (reduce (fn [group-ids id] + ;; When the shape to delete is the mask of a masked group, + ;; the mask condition must be removed, and it must be + ;; converted to a normal group. + (let [obj (lookup id) + parent (lookup (:parent-id obj))] + (if (and (:masked-group parent) + (= id (first (:shapes parent)))) + (conj group-ids (:id parent)) + group-ids))) + #{} + ids-to-delete) + + interacting-shapes + (filter (fn [shape] + ;; If any of the deleted shapes is the destination of + ;; some interaction, this must be deleted, too. + (let [interactions (:interactions shape)] + (some #(and (ctsi/has-destination %) + (contains? ids-to-delete (:destination %))) + interactions))) + (vals objects)) + + ids-set (set ids-to-delete) + guides-to-remove + (->> (dm/get-in page [:options :guides]) + (vals) + (filter #(contains? ids-set (:frame-id %))) + (map :id)) + + guides + (->> guides-to-remove + (reduce dissoc (dm/get-in page [:options :guides]))) + + starting-flows + (filter (fn [flow] + ;; If any of the deleted is a frame that starts a flow, + ;; this must be deleted, too. + (contains? ids-to-delete (:starting-frame flow))) + (-> page :options :flows)) + + all-parents + (reduce (fn [res id] + ;; All parents of any deleted shape must be resized. + (into res (cfh/get-parent-ids objects id))) + (d/ordered-set) + ids-to-delete) + + all-children + (->> ids-to-delete ;; Children of deleted shapes must be also deleted. + (reduce (fn [res id] + (into res (cfh/get-children-ids objects id))) + []) + (reverse) + (into (d/ordered-set))) + + find-all-empty-parents + (fn recursive-find-empty-parents [empty-parents] + (let [all-ids (into empty-parents ids-to-delete) + contains? (partial contains? all-ids) + xform (comp (map lookup) + (filter #(or (cfh/group-shape? %) (cfh/bool-shape? %))) + (remove #(->> (:shapes %) (remove contains?) seq)) + (map :id)) + parents (into #{} xform all-parents)] + (if (= empty-parents parents) + empty-parents + (recursive-find-empty-parents parents)))) + + empty-parents + ;; Any parent whose children are all deleted, must be deleted too. + (into (d/ordered-set) (find-all-empty-parents #{})) + + components-to-delete + (if components-v2 + (reduce (fn [components id] + (let [shape (get objects id)] + (if (and (= (:component-file shape) (:id file)) ;; Main instances should exist only in local file + (:main-instance shape)) ;; but check anyway + (conj components (:component-id shape)) + components))) + [] + (into ids-to-delete all-children)) + []) + + changes (-> changes + (pcb/set-page-option :guides guides)) + + changes (reduce (fn [changes component-id] + ;; It's important to delete the component before the main instance, because we + ;; need to store the instance position if we want to restore it later. + (pcb/delete-component changes component-id (:id page))) + changes + components-to-delete) + changes (-> changes + (generate-update-shape-flags ids-to-hide objects {:hidden true}) + (pcb/remove-objects all-children {:ignore-touched true}) + (pcb/remove-objects ids-to-delete {:ignore-touched ignore-touched}) + (pcb/remove-objects empty-parents) + (pcb/resize-parents all-parents) + (pcb/update-shapes groups-to-unmask + (fn [shape] + (assoc shape :masked-group false))) + (pcb/update-shapes (map :id interacting-shapes) + (fn [shape] + (d/update-when shape :interactions + (fn [interactions] + (into [] + (remove #(and (ctsi/has-destination %) + (contains? ids-to-delete (:destination %)))) + interactions))))) + (cond-> (seq starting-flows) + (pcb/update-page-option :flows (fn [flows] + (->> (map :id starting-flows) + (reduce ctp/remove-flow flows))))))] + [all-parents changes])) + + +(defn generate-relocate + [changes objects parent-id page-id to-index ids & {:keys [cell ignore-parents?]}] + (let [ids (cfh/order-by-indexed-shapes objects ids) + shapes (map (d/getf objects) ids) + parent (get objects parent-id) + all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids) + parents (if ignore-parents? #{parent-id} all-parents) + + children-ids + (->> ids + (mapcat #(cfh/get-children-ids-with-self objects %))) + + child-heads + (->> ids + (mapcat #(ctn/get-child-heads objects %)) + (map :id)) + + component-main-parent + (ctn/find-component-main objects parent false) + + groups-to-delete + (loop [current-id (first parents) + to-check (rest parents) + removed-id? (set ids) + result #{}] + + (if-not current-id + ;; Base case, no next element + result + + (let [group (get objects current-id)] + (if (and (not= :frame (:type group)) + (not= current-id parent-id) + (empty? (remove removed-id? (:shapes group)))) + + ;; Adds group to the remove and check its parent + (let [to-check (concat to-check [(cfh/get-parent-id objects current-id)])] + (recur (first to-check) + (rest to-check) + (conj removed-id? current-id) + (conj result current-id))) + + ;; otherwise recur + (recur (first to-check) + (rest to-check) + removed-id? + result))))) + + groups-to-unmask + (reduce (fn [group-ids id] + ;; When a masked group loses its mask shape, because it's + ;; moved outside the group, the mask condition must be + ;; removed, and it must be converted to a normal group. + (let [obj (get objects id) + parent (get objects (:parent-id obj))] + (if (and (:masked-group parent) + (= id (first (:shapes parent))) + (not= (:id parent) parent-id)) + (conj group-ids (:id parent)) + group-ids))) + #{} + ids) + + ;; TODO: Probably implementing this using loop/recur will + ;; be more efficient than using reduce and continuous data + ;; desturcturing. + + ;; Sets the correct components metadata for the moved shapes + ;; `shapes-to-detach` Detach from a component instance a shape that was inside a component and is moved outside + ;; `shapes-to-deroot` Removes the root flag from a component instance moved inside another component + ;; `shapes-to-reroot` Adds a root flag when a nested component instance is moved outside + [shapes-to-detach shapes-to-deroot shapes-to-reroot] + (reduce (fn [[shapes-to-detach shapes-to-deroot shapes-to-reroot] id] + (let [shape (get objects id) + parent (get objects parent-id) + component-shape (ctn/get-component-shape objects shape) + component-shape-parent (ctn/get-component-shape objects parent {:allow-main? true}) + root-parent (ctn/get-instance-root objects parent) + + detach? (and (ctk/in-component-copy-not-head? shape) + (not= (:id component-shape) + (:id component-shape-parent))) + deroot? (and (ctk/instance-root? shape) + root-parent) + reroot? (and (ctk/subinstance-head? shape) + (not component-shape-parent)) + + ids-to-detach (when detach? + (cons id (cfh/get-children-ids objects id)))] + + [(cond-> shapes-to-detach detach? (into ids-to-detach)) + (cond-> shapes-to-deroot deroot? (conj id)) + (cond-> shapes-to-reroot reroot? (conj id))])) + [[] [] []] + (->> ids + (mapcat #(ctn/get-child-heads objects %)) + (map :id))) + cell (or cell (ctl/get-cell-by-index parent to-index))] + + (-> changes + (pcb/with-page-id page-id) + (pcb/with-objects objects) + + ;; Remove layout-item properties when moving a shape outside a layout + (cond-> (not (ctl/any-layout? parent)) + (pcb/update-shapes ids ctl/remove-layout-item-data)) + + ;; Remove the hide in viewer flag + (cond-> (and (not= uuid/zero parent-id) (cfh/frame-shape? parent)) + (pcb/update-shapes ids #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true)))) + + ;; Remove the swap slots if it is moving to a different component + (pcb/update-shapes + child-heads + (fn [shape] + (cond-> shape + (not= component-main-parent (ctn/find-component-main objects shape false)) + (ctk/remove-swap-slot)))) + + ;; Remove component-root property when moving a shape inside a component + (cond-> (ctn/get-instance-root objects parent) + (pcb/update-shapes children-ids #(dissoc % :component-root))) + + ;; Add component-root property when moving a component outside a component + (cond-> (not (ctn/get-instance-root objects parent)) + (pcb/update-shapes child-heads #(assoc % :component-root true))) + + ;; Move the shapes + (pcb/change-parent parent-id + shapes + to-index) + + ;; Remove empty groups + (pcb/remove-objects groups-to-delete) + + ;; Unmask groups whose mask have moved outside + (pcb/update-shapes groups-to-unmask + (fn [shape] + (assoc shape :masked-group false))) + + ;; Detach shapes moved out of their component + (pcb/update-shapes shapes-to-detach ctk/detach-shape) + + ;; Make non root a component moved inside another one + (pcb/update-shapes shapes-to-deroot + (fn [shape] + (assoc shape :component-root nil))) + + ;; Make root a subcomponent moved outside its parent component + (pcb/update-shapes shapes-to-reroot + (fn [shape] + (assoc shape :component-root true))) + + ;; Reset constraints depending on the new parent + (pcb/update-shapes ids + (fn [shape] + (let [frame-id (if (= (:type parent) :frame) + (:id parent) + (:frame-id parent)) + moved-shape (assoc shape + :parent-id parent-id + :frame-id frame-id)] + (assoc shape + :constraints-h (gsh/default-constraints-h moved-shape) + :constraints-v (gsh/default-constraints-v moved-shape)))) + {:ignore-touched true}) + + ;; Fix the sizing when moving a shape + (pcb/update-shapes parents + (fn [parent] + (if (ctl/flex-layout? parent) + (cond-> parent + (ctl/change-h-sizing? (:id parent) objects (:shapes parent)) + (assoc :layout-item-h-sizing :fix) + + (ctl/change-v-sizing? (:id parent) objects (:shapes parent)) + (assoc :layout-item-v-sizing :fix)) + parent))) + + ;; Change the grid cell in a grid layout + (cond-> (ctl/grid-layout? objects parent-id) + (-> (pcb/update-shapes + [parent-id] + (fn [frame objects] + (-> frame + ;; Assign the cell when pushing into a specific grid cell + (cond-> (some? cell) + (-> (ctl/free-cell-shapes ids) + (ctl/push-into-cell ids (:row cell) (:column cell)) + (ctl/assign-cells objects))) + (ctl/assign-cell-positions objects))) + {:with-objects? true}) + (pcb/reorder-grid-children [parent-id]))) + + ;; If parent locked, lock the added shapes + (cond-> (:blocked parent) + (pcb/update-shapes ids #(assoc % :blocked true))) + + ;; Resize parent containers that need to + (pcb/resize-parents parents)))) + + + + +(defn change-show-in-viewer [shape hide?] + (cond-> (assoc shape :hide-in-viewer hide?) + ;; When a frame is no longer shown in view mode, it cannot have interactions + hide? + (dissoc :interactions))) + +(defn add-new-interaction [shape interaction] + (-> shape + (update :interactions ctsi/add-interaction interaction) + ;; When a interaction is created, the frame must be shown in view mode + (dissoc :hide-in-viewer))) diff --git a/common/src/app/common/media.cljc b/common/src/app/common/media.cljc index 212d43f2a5f67875922c479dc0a22c7c3d2d6f7f..a342a227f0321ee7270fe494676f7801d1dd7a5e 100644 --- a/common/src/app/common/media.cljc +++ b/common/src/app/common/media.cljc @@ -10,7 +10,7 @@ [cuerdas.core :as str])) ;; We have added ".ttf" as string to solve a problem with chrome input selector -(def valid-font-types #{"font/ttf", ".ttf", "font/woff", "application/font-woff", "font/otf"}) +(def valid-font-types #{"font/ttf" ".ttf" "font/woff", "application/font-woff" "woff" "font/otf" ".otf" "font/opentype"}) (def valid-image-types #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"}) (def str-image-types (str/join "," valid-image-types)) (def str-font-types (str/join "," valid-font-types)) diff --git a/common/src/app/common/record.cljc b/common/src/app/common/record.cljc index f9d10df0cea0b5949b3bc2c3d878edfa43b1e4ff..385917a0a9a27ca6a16b45d72ef5d389f2e32478 100644 --- a/common/src/app/common/record.cljc +++ b/common/src/app/common/record.cljc @@ -429,22 +429,80 @@ `(update ~ssym ~ksym ~f ~@params))) (defmacro define-properties! + "Define properties in the prototype with `.defineProperty`" [rsym & properties] (let [rsym (with-meta rsym {:tag 'js})] `(do ~@(for [params properties :let [pname (get params :name) get-fn (get params :get) - set-fn (get params :set)]] - `(.defineProperty js/Object - (.-prototype ~rsym) - ~pname + set-fn (get params :set) + enum-p (get params :enumerable) + conf-p (get params :configurable) + writ-p (get params :writable)]] + `(.defineProperty js/Object (.-prototype ~rsym) ~pname (cljs.core/js-obj - "enumerable" true - "configurable" true ~@(concat + (if (some? enum-p) + ["enumerable" enum-p] + ["enumerable" true]) + + (if (some? conf-p) + ["configurable" conf-p] + ["configurable" true]) + + (when (some? writ-p) + ["writable" writ-p]) + (when get-fn ["get" get-fn]) + (when set-fn ["set" set-fn])))))))) +(defmacro add-properties! + "Adds properties to an object using `.defineProperty`" + [rsym & properties] + (let [rsym (with-meta rsym {:tag 'js}) + getf-sym (with-meta (gensym "get-fn") {:tag 'js}) + setf-sym (with-meta (gensym "set-fn") {:tag 'js}) + this-sym (with-meta (gensym "this") {:tag 'js}) + target-sym (with-meta (gensym "target") {:tag 'js})] + `(let [~target-sym ~rsym] + ;; Creates the `.defineProperty` per property + ~@(for [params properties + :let [pname (get params :name) + get-fn (get params :get) + set-fn (get params :set) + enum-p (get params :enumerable) + conf-p (get params :configurable) + writ-p (get params :writable)]] + `(let [~getf-sym ~get-fn + ~setf-sym ~set-fn] + (.defineProperty + js/Object + ~target-sym + ~pname + (cljs.core/js-obj + ~@(concat + (if (some? enum-p) + ["enumerable" enum-p] + ;; Default in JS is false. We default to true + ["enumerable" true]) + + (when (some? conf-p) + ["configurable" conf-p]) + + (when (some? writ-p) + ["writable" writ-p]) + + (when get-fn + ["get" `(fn [] + (cljs.core/this-as ~this-sym + (~getf-sym ~this-sym)))]) + (when set-fn + ["set" `(fn [value#] + (cljs.core/this-as ~this-sym + (~setf-sym ~this-sym value#)))])))))) + ;; Returns the object + ~target-sym))) diff --git a/common/src/app/common/schema.cljc b/common/src/app/common/schema.cljc index b1e743f6437c246271791b6b1f8300f0e16dd153..493311af2cae0085c619644ff4c828facaeb4cb8 100644 --- a/common/src/app/common/schema.cljc +++ b/common/src/app/common/schema.cljc @@ -5,7 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.schema - (:refer-clojure :exclude [deref merge parse-uuid]) + (:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean]) #?(:cljs (:require-macros [app.common.schema :refer [ignoring]])) (:require [app.common.data :as d] @@ -21,6 +21,7 @@ [cuerdas.core :as str] [malli.core :as m] [malli.dev.pretty :as mdp] + [malli.dev.virhe :as v] [malli.error :as me] [malli.generator :as mg] [malli.registry :as mr] @@ -44,6 +45,14 @@ [o] (m/schema? o)) +(defn properties + [s] + (m/properties s)) + +(defn type-properties + [s] + (m/type-properties s)) + (defn lazy-schema? [s] (satisfies? ILazySchema s)) @@ -66,7 +75,8 @@ (-explain s value) (m/explain s value default-options))) -(defn humanize +(defn simplify + "Given an explain data structure, return a simplified version of it" [exp] (me/humanize exp)) @@ -77,10 +87,12 @@ (mg/generate (schema s) o))) (defn form + "Returns a readable form of the schema" [s] (m/form s default-options)) (defn merge + "Merge two schemas" [& items] (apply mu/merge (map schema items))) @@ -93,49 +105,57 @@ (m/deref s)) (defn error-values + "Get error values form explain data structure" [exp] (malli.error/error-value exp {:malli.error/mask-valid-values '...})) -(def default-transformer - (let [default-decoder - {:compile (fn [s _registry] - (let [props (m/type-properties s)] - (or (::oapi/decode props) - (::decode props))))} - - default-encoder - {:compile (fn [s _] - (let [props (m/type-properties s)] - (or (::oapi/encode props) - (::encode props))))} - - coders {:vector mt/-sequential-or-set->vector - :sequential mt/-sequential-or-set->seq - :set mt/-sequential->set - :tuple mt/-sequential->vector}] - - (mt/transformer - {:name :penpot - :default-decoder default-decoder - :default-encoder default-encoder} - {:name :string - :decoders (mt/-string-decoders) - :encoders (mt/-string-encoders)} - {:name :collections - :decoders coders - :encoders coders}))) - -(defn validator - [s] - (if (lazy-schema? s) - (-get-validator s) - (-> s schema m/validator))) - -(defn explainer - [s] - (if (lazy-schema? s) - (-get-explainer s) - (-> s schema m/explainer))) +(defn optional-keys + [schema] + (mu/optional-keys schema default-options)) + +(defn transformer + [& transformers] + (apply mt/transformer transformers)) + +;; (defn key-transformer +;; [& {:as opts}] +;; (mt/key-transformer opts)) + +;; (defn- transform-map-keys +;; [f o] +;; (cond +;; (record? o) +;; (reduce-kv (fn [res k v] +;; (let [k' (f k)] +;; (if (= k k') +;; res +;; (-> res +;; (assoc k' v) +;; (dissoc k))))) +;; o +;; o) + +;; (map? o) +;; (persistent! +;; (reduce-kv (fn [res k v] +;; (assoc! res (f k) v)) +;; (transient {}) +;; o)) + +;; :else +;; o)) + +(defn json-transformer + [] + (mt/transformer + (mt/json-transformer) + (mt/collection-transformer))) + +(defn string-transformer + [] + (mt/transformer + (mt/string-transformer) + (mt/collection-transformer))) (defn encode ([s val transformer] @@ -149,11 +169,22 @@ ([s val options transformer] (m/decode s val options transformer))) +(defn validator + [s] + (if (lazy-schema? s) + (-get-validator s) + (-> s schema m/validator))) + +(defn explainer + [s] + (if (lazy-schema? s) + (-get-explainer s) + (-> s schema m/explainer))) + (defn encoder ([s] - (if (lazy-schema? s) - (-get-decoder s) - (encoder s default-options default-transformer))) + (assert (lazy-schema? s) "expected lazy schema") + (-get-decoder s)) ([s transformer] (m/encoder s default-options transformer)) ([s options transformer] @@ -161,9 +192,8 @@ (defn decoder ([s] - (if (lazy-schema? s) - (-get-decoder s) - (decoder s default-options default-transformer))) + (assert (lazy-schema? s) "expected lazy schema") + (-get-decoder s)) ([s transformer] (m/decoder s default-options transformer)) ([s options transformer] @@ -185,6 +215,7 @@ (fn [v] (@vfn v)))) (defn humanize-explain + "Returns a string representation of the explain data structure" [{:keys [schema errors value]} & {:keys [length level]}] (let [errors (mapv #(update % :schema form) errors)] (with-out-str @@ -197,9 +228,25 @@ :level (d/nilv level 8) :length (d/nilv length 12)}))))) +(defmethod v/-format ::schemaless-explain + [_ explanation printer] + {:body [:group + (v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break + (v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer)]}) + +(defmethod v/-format ::explain + [_ {:keys [schema] :as explanation} printer] + {:body [:group + (v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break + (v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break + (v/-block "Schema" (v/-visit schema printer) printer)]}) + (defn pretty-explain - [s d] - (mdp/explain (schema s) d)) + [explain & {:keys [variant message] + :or {variant ::explain + message "Validation Error"}}] + (let [explain (fn [] (me/with-error-messages explain))] + ((mdp/prettifier variant message explain default-options)))) (defmacro ignoring [expr] @@ -207,27 +254,6 @@ `(try ~expr (catch :default e# nil)) `(try ~expr (catch Throwable e# nil)))) -(defn simple-schema - [& {:keys [pred] :as options}] - (cond-> options - (contains? options :type-properties) - (update :type-properties (fn [props] - (cond-> props - (contains? props :decode/string) - (update :decode/string (fn [decode-fn] - (fn [s] - (if (pred s) - s - (or (ignoring (decode-fn s)) s))))) - (contains? props ::decode) - (update ::decode (fn [decode-fn] - (fn [s] - (if (pred s) - s - (or (ignoring (decode-fn s)) s)))))))) - :always - (m/-simple-schema))) - (defn lookup "Lookups schema from registry." ([s] (lookup sr/default-registry s)) @@ -271,7 +297,6 @@ ::explain explain})))) true))) - (defn fast-validate! "A fast path for validation process, assumes the ILazySchema protocol implemented on the provided `s` schema. Sould not be used directly." @@ -287,7 +312,7 @@ (throw (ex-info hint options)))))) (defn validate-fn - "Create a predefined validate function" + "Create a predefined validate function that raises an expception" [s] (let [schema (if (lazy-schema? s) s (define s))] (partial fast-validate! schema))) @@ -307,6 +332,7 @@ hint (get options :hint "schema validation error")] (throw (ex-info hint options))))))) +;; FIXME: revisit (defn conform! [schema value] (assert (lazy-schema? schema) "expected `schema` to satisfy ILazySchema protocol") @@ -315,26 +341,18 @@ params)) (defn register! [type s] - (let [s (if (map? s) (simple-schema s) s)] - (swap! sr/registry assoc type s))) - -(defn def! [type s] - (register! type s) - nil) - -(defn define! [id s] - (register! id s) - nil) + (let [s (if (map? s) (m/-simple-schema s) s)] + (swap! sr/registry assoc type s) + nil)) (defn define "Create ans instance of ILazySchema" - [s & {:keys [transformer] :as options}] + [s & {:keys [transformer] :or {transformer json-transformer} :as options}] (let [schema (delay (schema s)) validator (delay (m/validator @schema)) explainer (delay (m/explainer @schema)) options (c/merge default-options (dissoc options :transformer)) - transformer (or transformer default-transformer) decoder (delay (m/decoder @schema options transformer)) encoder (delay (m/encoder @schema options transformer))] @@ -398,8 +416,8 @@ ;; --- BUILTIN SCHEMAS -(define! :merge (mu/-merge)) -(define! :union (mu/-union)) +(register! :merge (mu/-merge)) +(register! :union (mu/-union)) (def uuid-rx #"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") @@ -410,7 +428,7 @@ (some->> (re-matches uuid-rx s) uuid/uuid) s)) -(define! ::uuid +(register! ::uuid {:type ::uuid :pred uuid? :type-properties @@ -418,9 +436,12 @@ :description "UUID formatted string" :error/message "should be an uuid" :gen/gen (sg/uuid) + :decode/string parse-uuid + :decode/json parse-uuid + :encode/string str + :encode/json str ::oapi/type "string" - ::oapi/format "uuid" - ::oapi/decode parse-uuid}}) + ::oapi/format "uuid"}}) (def email-re #"[a-zA-Z0-9_.+-\\\\]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+") @@ -428,31 +449,215 @@ [s] (if (string? s) (re-matches email-re s) - s)) + nil)) + +(defn email-string? + [s] + (and (string? s) + (re-seq email-re s))) + +(register! ::email + {:type :string + :pred email-string? + :property-pred + (fn [{:keys [max] :as props}] + (if (some? max) + (fn [value] + (<= (count value) max)) + (constantly true))) -;; FIXME: add proper email generator -(define! ::email - {:type ::email - :pred (fn [s] - (and (string? s) - (< (count s) 250) - (re-seq email-re s))) :type-properties {:title "email" :description "string with valid email address" - :error/message "expected valid email" - :gen/gen (-> :string sg/generator) + :error/code "errors.invalid-email" + :gen/gen (sg/email) + :decode/string (fn [v] (or (parse-email v) v)) + :decode/json (fn [v] (or (parse-email v) v)) ::oapi/type "string" - ::oapi/format "email" - ::oapi/decode parse-email}}) + ::oapi/format "email"}}) -(def non-empty-strings-xf +(def xf:filter-word-strings (comp (filter string?) (remove str/empty?) (remove str/blank?))) -(define! ::set-of-strings +;; NOTE: this is general purpose set spec and should be used over the other + +(register! ::set + {:type :set + :min 0 + :max 1 + :compile + (fn [{:keys [kind max min] :as props} children _] + (let [kind (or (last children) kind) + + pred + (cond + (fn? kind) kind + (nil? kind) any? + :else (validator kind)) + + pred + (cond + (and max min) + (fn [value] + (let [size (count value)] + (and (set? value) + (<= min size max) + (every? pred value)))) + + min + (fn [value] + (let [size (count value)] + (and (set? value) + (<= min size) + (every? pred value)))) + + max + (fn [value] + (let [size (count value)] + (and (set? value) + (<= size max) + (every? pred value)))) + + :else + (fn [value] + (every? pred value))) + + + decode-string-child + (decoder kind string-transformer) + + decode-string + (fn [v] + (let [v (if (string? v) (str/split v #"[\s,]+") v) + x (comp xf:filter-word-strings (map decode-string-child))] + (into #{} x v))) + + decode-json-child + (decoder kind json-transformer) + + decode-json + (fn [v] + (let [v (if (string? v) (str/split v #"[\s,]+") v) + x (comp xf:filter-word-strings (map decode-json-child))] + (into #{} x v))) + + encode-string-child + (encoder kind string-transformer) + + encode-string + (fn [o] + (if (set? o) + (str/join ", " (map encode-string-child o)) + o)) + + encode-json + (fn [o] + (if (set? o) + (vec o) + o))] + + + {:pred pred + :type-properties + {:title "set" + :description "Set of Strings" + :error/message "should be a set of strings" + :gen/gen (-> kind sg/generator sg/set) + :decode/string decode-string + :decode/json decode-json + :encode/string encode-string + :encode/json encode-json + ::oapi/type "array" + ::oapi/format "set" + ::oapi/items {:type "string"} + ::oapi/unique-items true}}))}) + + +(register! ::vec + {:type :vector + :min 0 + :max 1 + :compile + (fn [{:keys [kind max min] :as props} children _] + (let [kind (or (last children) kind) + pred + (cond + (fn? kind) kind + (nil? kind) any? + :else (validator kind)) + + pred + (cond + (and max min) + (fn [value] + (let [size (count value)] + (and (set? value) + (<= min size max) + (every? pred value)))) + + min + (fn [value] + (let [size (count value)] + (and (set? value) + (<= min size) + (every? pred value)))) + + max + (fn [value] + (let [size (count value)] + (and (set? value) + (<= size max) + (every? pred value)))) + + :else + (fn [value] + (every? pred value))) + + decode-string-child + (decoder kind string-transformer) + + decode-json-child + (decoder kind json-transformer) + + decode-string + (fn [v] + (let [v (if (string? v) (str/split v #"[\s,]+") v) + x (comp xf:filter-word-strings (map decode-string-child))] + (into #{} x v))) + + decode-json + (fn [v] + (let [v (if (string? v) (str/split v #"[\s,]+") v) + x (comp xf:filter-word-strings (map decode-json-child))] + (into #{} x v))) + + encode-string-child + (encoder kind string-transformer) + + encode-string + (fn [o] + (if (vector? o) + (str/join ", " (map encode-string-child o)) + o))] + + {:pred pred + :type-properties + {:title "set" + :description "Set of Strings" + :error/message "should be a set of strings" + :gen/gen (-> kind sg/generator sg/set) + :decode/string decode-string + :decode/json decode-json + :encode/string encode-string + ::oapi/type "array" + ::oapi/format "set" + ::oapi/items {:type "string"} + ::oapi/unique-items true}}))}) + +(register! ::set-of-strings {:type ::set-of-strings :pred #(and (set? %) (every? string? %)) :type-properties @@ -460,15 +665,15 @@ :description "Set of Strings" :error/message "should be a set of strings" :gen/gen (-> :string sg/generator sg/set) + :decode/string (fn [v] + (let [v (if (string? v) (str/split v #"[\s,]+") v)] + (into #{} xf:filter-word-strings v))) ::oapi/type "array" ::oapi/format "set" ::oapi/items {:type "string"} - ::oapi/unique-items true - ::oapi/decode (fn [v] - (let [v (if (string? v) (str/split v #"[\s,]+") v)] - (into #{} non-empty-strings-xf v)))}}) + ::oapi/unique-items true}}) -(define! ::set-of-keywords +(register! ::set-of-keywords {:type ::set-of-keywords :pred #(and (set? %) (every? keyword? %)) :type-properties @@ -476,31 +681,15 @@ :description "Set of Strings" :error/message "should be a set of strings" :gen/gen (-> :keyword sg/generator sg/set) + :decode/string (fn [v] + (let [v (if (string? v) (str/split v #"[\s,]+") v)] + (into #{} (comp xf:filter-word-strings (map keyword)) v))) ::oapi/type "array" ::oapi/format "set" ::oapi/items {:type "string" :format "keyword"} - ::oapi/unique-items true - ::oapi/decode (fn [v] - (let [v (if (string? v) (str/split v #"[\s,]+") v)] - (into #{} (comp non-empty-strings-xf (map keyword)) v)))}}) - -(define! ::set-of-emails - {:type ::set-of-emails - :pred #(and (set? %) (every? string? %)) - :type-properties - {:title "set[email]" - :description "Set of Emails" - :error/message "should be a set of emails" - :gen/gen (-> ::email sg/generator sg/set) - ::oapi/type "array" - ::oapi/format "set" - ::oapi/items {:type "string" :format "email"} - ::oapi/unique-items true - ::decode (fn [v] - (let [v (if (string? v) (str/split v #"[\s,]+") v)] - (into #{} (keep parse-email) v)))}}) + ::oapi/unique-items true}}) -(define! ::set-of-uuid +(register! ::set-of-uuid {:type ::set-of-uuid :pred #(and (set? %) (every? uuid? %)) :type-properties @@ -508,15 +697,15 @@ :description "Set of UUID" :error/message "should be a set of UUID instances" :gen/gen (-> ::uuid sg/generator sg/set) + :decode/string (fn [v] + (let [v (if (string? v) (str/split v #"[\s,]+") v)] + (into #{} (keep parse-uuid) v))) ::oapi/type "array" ::oapi/format "set" ::oapi/items {:type "string" :format "uuid"} - ::oapi/unique-items true - ::oapi/decode (fn [v] - (let [v (if (string? v) (str/split v #"[\s,]+") v)] - (into #{} (keep parse-uuid) v)))}}) + ::oapi/unique-items true}}) -(define! ::coll-of-uuid +(register! ::coll-of-uuid {:type ::set-of-uuid :pred (partial every? uuid?) :type-properties @@ -524,84 +713,182 @@ :description "Coll of UUID" :error/message "should be a coll of UUID instances" :gen/gen (-> ::uuid sg/generator sg/set) + :decode/string (fn [v] + (let [v (if (string? v) (str/split v #"[\s,]+") v)] + (into [] (keep parse-uuid) v))) ::oapi/type "array" ::oapi/format "array" ::oapi/items {:type "string" :format "uuid"} - ::oapi/unique-items false - ::oapi/decode (fn [v] - (let [v (if (string? v) (str/split v #"[\s,]+") v)] - (into [] (keep parse-uuid) v)))}}) + ::oapi/unique-items false}}) -(define! ::one-of +(register! ::one-of {:type ::one-of :min 1 :max 1 :compile (fn [props children _] (let [options (into #{} (last children)) - format (:format props "keyword")] + format (:format props "keyword") + decode (if (= format "keyword") + keyword + identity)] {:pred #(contains? options %) :type-properties {:title "one-of" :description "One of the Set" :gen/gen (sg/elements options) + :decode/string decode + :decode/json decode ::oapi/type "string" - ::oapi/format (:format props "keyword") - ::oapi/decode (if (= format "keyword") - keyword - identity)}}))}) + ::oapi/format (:format props "keyword")}}))}) ;; Integer/MAX_VALUE (def max-safe-int 2147483647) ;; Integer/MIN_VALUE (def min-safe-int -2147483648) -(define! ::safe-int - {:type ::safe-int - :pred #(and (int? %) (>= max-safe-int %) (>= % min-safe-int)) - :type-properties - {:title "int" - :description "Safe Integer" - :error/message "expected to be int in safe range" - :gen/gen (sg/small-int) - ::oapi/type "integer" - ::oapi/format "int64" - ::oapi/decode (fn [s] - (if (string? s) - (parse-long s) - s))}}) - -(define! ::safe-number - {:type ::safe-number - :pred #(and (number? %) (>= max-safe-int %) (>= % min-safe-int)) +(defn parse-long + [v] + (or (ignoring + (if (string? v) + (c/parse-long v) + v)) + v)) + +(def type:int + {:type :int + :min 0 + :max 0 + :compile + (fn [{:keys [max min] :as props} _ _] + (let [pred int? + pred (if (some? min) + (fn [v] + (and (>= v min) + (pred v))) + pred) + pred (if (some? max) + (fn [v] + (and (>= max v) + (pred v))) + pred)] + + {:pred pred + :type-properties + {:title "int" + :description "int" + :error/message "expected to be int/long" + :error/code "errors.invalid-integer" + :gen/gen (sg/small-int :max max :min min) + :decode/string parse-long + :decode/json parse-long + ::oapi/type "integer" + ::oapi/format "int64"}}))}) + +(defn parse-double + [v] + (or (ignoring + (if (string? v) + (c/parse-double v) + v)) + v)) + +(def type:double + {:type :double + :min 0 + :max 0 + :compile + (fn [{:keys [max min] :as props} _ _] + (let [pred double? + pred (if (some? min) + (fn [v] + (and (>= v min) + (pred v))) + pred) + pred (if (some? max) + (fn [v] + (and (>= max v) + (pred v))) + pred)] + + {:pred pred + :type-properties + {:title "doble" + :description "double number" + :error/message "expected to be double" + :error/code "errors.invalid-double" + :gen/gen (sg/small-double :max max :min min) + :decode/string parse-double + :decode/json parse-double + ::oapi/type "number" + ::oapi/format "double"}}))}) + +(def type:number + {:type :number + :min 0 + :max 0 + :compile + (fn [{:keys [max min] :as props} _ _] + (let [pred number? + pred (if (some? min) + (fn [v] + (and (>= v min) + (pred v))) + pred) + pred (if (some? max) + (fn [v] + (and (>= max v) + (pred v))) + pred) + + gen (sg/one-of + (sg/small-int :max max :min min) + (sg/small-double :max max :min min))] + + {:pred pred + :type-properties + {:title "int" + :description "int" + :error/message "expected to be number" + :error/code "errors.invalid-number" + :gen/gen gen + :decode/string parse-double + :decode/json parse-double + ::oapi/type "number"}}))}) + +(register! ::int type:int) +(register! ::double type:double) +(register! ::number type:number) + +(register! ::safe-int [::int {:max max-safe-int :min min-safe-int}]) +(register! ::safe-double [::double {:max max-safe-int :min min-safe-int}]) +(register! ::safe-number [::number {:max max-safe-int :min min-safe-int}]) + +(defn parse-boolean + [v] + (if (string? v) + (case v + ("true" "t" "1") true + ("false" "f" "0") false + v) + v)) + +(def type:boolean + {:type :boolean + :pred boolean? :type-properties - {:title "number" - :description "Safe Number" - :error/message "expected to be number in safe range" - :gen/gen (sg/one-of (sg/small-int) - (sg/small-double)) - ::oapi/type "number" - ::oapi/format "double" - ::oapi/decode (fn [s] - (if (string? s) - (parse-double s) - s))}}) - -(define! ::safe-double - {:type ::safe-double - :pred #(and (double? %) (>= max-safe-int %) (>= % min-safe-int)) - :type-properties - {:title "number" - :description "Safe Number" - :error/message "expected to be number in safe range" - :gen/gen (sg/small-double) - ::oapi/type "number" - ::oapi/format "double" - ::oapi/decode (fn [s] - (if (string? s) - (parse-double s) - s))}}) - -(define! ::contains-any + {:title "boolean" + :description "boolean" + :error/message "expected boolean" + :error/code "errors.invalid-boolean" + :gen/gen sg/boolean + :decode/string parse-boolean + :decode/json parse-boolean + :encode/string str + ::oapi/type "boolean"}}) + +(register! ::boolean type:boolean) + +(def type:contains-any {:type ::contains-any :min 1 :max 1 @@ -619,22 +906,32 @@ {:title "contains" :description "contains predicate"}}))}) -(define! ::inst +(register! ::contains-any type:contains-any) + +(def type:inst {:type ::inst :pred inst? :type-properties {:title "inst" :description "Satisfies Inst protocol" - :error/message "expected to be number in safe range" + :error/message "should be an instant" :gen/gen (->> (sg/small-int) - (sg/fmap (fn [v] (tm/instant v)))) - ::oapi/type "number" - ::oapi/format "int64"}}) + (sg/fmap (fn [v] (tm/parse-instant v)))) + + :decode/string tm/parse-instant + :encode/string tm/format-instant + :decode/json tm/parse-instant + :encode/json tm/format-instant + ::oapi/type "string" + ::oapi/format "iso"}}) + +(register! ::inst type:inst) + +(register! ::fn [:schema fn?]) -(define! ::fn - [:schema fn?]) +;; FIXME: deprecated, replace with ::text -(define! ::word-string +(register! ::word-string {:type ::word-string :pred #(and (string? %) (not (str/blank? %))) :property-pred (m/-min-max-pred count) @@ -646,17 +943,110 @@ ::oapi/type "string" ::oapi/format "string"}}) -(define! ::uri + +(defn decode-uri + [val] + (if (u/uri? val) + val + (-> val str/trim u/uri))) + +(register! ::uri {:type ::uri :pred u/uri? + :property-pred + (fn [{:keys [min max prefix] :as props}] + (if (seq props) + (fn [value] + (let [value (str value) + size (count value)] + + (and + (cond + (and min max) + (<= min size max) + + min + (<= min size) + + max + (<= size max)) + + (cond + (d/regexp? prefix) + (some? (re-seq prefix value)) + + :else + true)))) + + (constantly true))) + :type-properties {:title "uri" :description "URI formatted string" - :error/message "expected URI instance" + :error/code "errors.invalid-uri" :gen/gen (sg/uri) + :decode/string decode-uri + :decode/json decode-uri ::oapi/type "string" - ::oapi/format "uri" - ::oapi/decode (comp u/uri str/trim)}}) + ::oapi/format "uri"}}) + +(register! ::text + {:type :string + :pred #(and (string? %) (not (str/blank? %))) + :property-pred + (fn [{:keys [min max] :as props}] + (if (seq props) + (fn [value] + (let [size (count value)] + (cond + (and min max) + (<= min size max) + + min + (<= min size) + + max + (<= size max)))) + (constantly true))) + + :type-properties + {:title "string" + :description "not whitespace string" + :gen/gen (sg/word-string) + :error/code "errors.invalid-text" + :error/fn + (fn [{:keys [value schema]}] + (let [{:keys [max min] :as props} (properties schema)] + (cond + (and (string? value) + (number? max) + (> (count value) max)) + ["errors.field-max-length" max] + + (and (string? value) + (number? min) + (< (count value) min)) + ["errors.field-min-length" min] + + (and (string? value) + (str/blank? value)) + "errors.field-not-all-whitespace")))}}) + +(register! ::password + {:type :string + :pred + (fn [value] + (and (string? value) + (>= (count value) 8) + (not (str/blank? value)))) + :type-properties + {:title "password" + :gen/gen (->> (sg/word-string) + (sg/filter #(>= (count %) 8))) + :error/code "errors.password-too-short" + ::oapi/type "string" + ::oapi/format "password"}}) + ;; ---- PREDICATES @@ -679,4 +1069,4 @@ (check-fn ::set-of-uuid)) (def check-set-of-emails! - (check-fn ::set-of-emails)) + (check-fn [::set ::email])) diff --git a/common/src/app/common/schema/generators.cljc b/common/src/app/common/schema/generators.cljc index 83e00bfd87932603f9d57ae55a7aebf67722a0ff..925568f0eead20741df2115d9de4a3c1e4479a21 100644 --- a/common/src/app/common/schema/generators.cljc +++ b/common/src/app/common/schema/generators.cljc @@ -5,7 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.schema.generators - (:refer-clojure :exclude [set subseq uuid for filter map let]) + (:refer-clojure :exclude [set subseq uuid for filter map let boolean]) #?(:cljs (:require-macros [app.common.schema.generators])) (:require [app.common.schema.registry :as sr] @@ -77,10 +77,20 @@ (defn word-string [] - (->> (tg/such-that #(re-matches #"\w+" %) - tg/string-alphanumeric - 50) - (tg/such-that (complement str/blank?)))) + (as-> tg/string-ascii $$ + (tg/resize 10 $$) + (tg/fmap (fn [v] (apply str (re-seq #"[A-Za-z]+" v))) $$) + (tg/such-that (fn [v] (>= (count v) 4)) $$ 100) + (tg/fmap str/lower $$))) + +(defn email + [] + (->> (word-string) + (tg/such-that (fn [v] (>= (count v) 4))) + (tg/fmap str/lower) + (tg/fmap (fn [v] + (str v "@example.net"))))) + (defn uri [] @@ -112,6 +122,9 @@ (c/map second)) (c/map list bools elements))))))) +(def any tg/any) +(def boolean tg/boolean) + (defn set [g] (tg/set g)) diff --git a/common/src/app/common/svg.cljc b/common/src/app/common/svg.cljc index d400f01f1a65b9b7df86b8c0da89f7988e72e6cf..4b9f71572193783f4f86e86ee8c9aa650ab96f5d 100644 --- a/common/src/app/common/svg.cljc +++ b/common/src/app/common/svg.cljc @@ -995,6 +995,9 @@ (= key :style) attrs + (= key :unicode) + attrs + (str/starts-with? (d/name key) "data-") attrs @@ -1043,7 +1046,6 @@ (str/includes? data "]*>" ""))) - (defn parse [text] #?(:cljs (tubax/xml->clj text) diff --git a/common/src/app/common/svg/shapes_builder.cljc b/common/src/app/common/svg/shapes_builder.cljc index 1796d08a56215fad2cceed26b085457592f542fd..41f25e1e2f366ab84bce75d189d6316fcbd29078 100644 --- a/common/src/app/common/svg/shapes_builder.cljc +++ b/common/src/app/common/svg/shapes_builder.cljc @@ -22,6 +22,7 @@ [app.common.svg :as csvg] [app.common.svg.path :as path] [app.common.types.shape :as cts] + [app.common.uuid :as uuid] [cuerdas.core :as str])) (def default-rect @@ -78,67 +79,68 @@ (declare parse-svg-element) (defn create-svg-shapes - [svg-data {:keys [x y]} objects frame-id parent-id selected center?] - (let [[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data) - - - unames (cfh/get-used-names objects) - svg-name (str/replace (:name svg-data) ".svg" "") - - svg-data (-> svg-data - (assoc :x (mth/round - (if center? - (- x vb-x (/ vb-width 2)) - x))) - (assoc :y (mth/round - (if center? - (- y vb-y (/ vb-height 2)) - y))) - (assoc :offset-x vb-x) - (assoc :offset-y vb-y) - (assoc :width vb-width) - (assoc :height vb-height) - (assoc :name svg-name)) - - [def-nodes svg-data] - (-> svg-data - (csvg/fix-default-values) - (csvg/fix-percents) - (csvg/extract-defs)) - - ;; In penpot groups have the size of their children. To - ;; respect the imported svg size and empty space let's create - ;; a transparent shape as background to respect the imported - ;; size - background - {:tag :rect - :attrs {:x (dm/str vb-x) - :y (dm/str vb-y) - :width (dm/str vb-width) - :height (dm/str vb-height) - :fill "none" - :id "base-background"} - :hidden true - :content []} - - svg-data (-> svg-data - (assoc :defs def-nodes) - (assoc :content (into [background] (:content svg-data)))) - - root-shape (create-svg-root frame-id parent-id svg-data) - root-id (:id root-shape) - - ;; Create the root shape - root-attrs (-> (:attrs svg-data) - (csvg/format-styles)) - - [_ children] - (reduce (partial create-svg-children objects selected frame-id root-id svg-data) - [unames []] - (d/enumerate (->> (:content svg-data) - (mapv #(csvg/inherit-attributes root-attrs %)))))] - - [root-shape children])) + ([svg-data pos objects frame-id parent-id selected center?] + (create-svg-shapes (uuid/next) svg-data pos objects frame-id parent-id selected center?)) + ([id svg-data {:keys [x y]} objects frame-id parent-id selected center?] + (let [[vb-x vb-y vb-width vb-height] (svg-dimensions svg-data) + + unames (cfh/get-used-names objects) + svg-name (str/replace (:name svg-data) ".svg" "") + + svg-data (-> svg-data + (assoc :x (mth/round + (if center? + (- x vb-x (/ vb-width 2)) + x))) + (assoc :y (mth/round + (if center? + (- y vb-y (/ vb-height 2)) + y))) + (assoc :offset-x vb-x) + (assoc :offset-y vb-y) + (assoc :width vb-width) + (assoc :height vb-height) + (assoc :name svg-name)) + + [def-nodes svg-data] + (-> svg-data + (csvg/fix-default-values) + (csvg/fix-percents) + (csvg/extract-defs)) + + ;; In penpot groups have the size of their children. To + ;; respect the imported svg size and empty space let's create + ;; a transparent shape as background to respect the imported + ;; size + background + {:tag :rect + :attrs {:x (dm/str vb-x) + :y (dm/str vb-y) + :width (dm/str vb-width) + :height (dm/str vb-height) + :fill "none" + :id "base-background"} + :hidden true + :content []} + + svg-data (-> svg-data + (assoc :defs def-nodes) + (assoc :content (into [background] (:content svg-data)))) + + root-shape (create-svg-root id frame-id parent-id svg-data) + root-id (:id root-shape) + + ;; Create the root shape + root-attrs (-> (:attrs svg-data) + (csvg/format-styles)) + + [_ children] + (reduce (partial create-svg-children objects selected frame-id root-id svg-data) + [unames []] + (d/enumerate (->> (:content svg-data) + (mapv #(csvg/inherit-attributes root-attrs %)))))] + + [root-shape children]))) (defn create-raw-svg [name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}] @@ -157,12 +159,13 @@ :svg-viewbox vbox}))) (defn create-svg-root - [frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}] + [id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}] (let [props (-> (dissoc attrs :viewBox :view-box :xmlns) (d/without-keys csvg/inheritable-props) (csvg/attrs->props))] (cts/setup-shape - {:type :group + {:id id + :type :group :name name :frame-id frame-id :parent-id parent-id @@ -383,13 +386,16 @@ (update :svg-attrs dissoc :fill) (assoc-in [:fills 0 :fill-color] (clr/parse color-style))) - (dm/get-in shape [:svg-attrs :fillOpacity]) + ;; Only create an opacity if the color is setted. Othewise can create problems down the line + (and (or (clr/color-string? color-attr) (clr/color-string? color-style)) + (dm/get-in shape [:svg-attrs :fillOpacity])) (-> (update :svg-attrs dissoc :fillOpacity) (update-in [:svg-attrs :style] dissoc :fillOpacity) (assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :fillOpacity]) (d/parse-double 1)))) - (dm/get-in shape [:svg-attrs :style :fillOpacity]) + (and (or (clr/color-string? color-attr) (clr/color-string? color-style)) + (dm/get-in shape [:svg-attrs :style :fillOpacity])) (-> (update-in [:svg-attrs :style] dissoc :fillOpacity) (update :svg-attrs dissoc :fillOpacity) (assoc-in [:fills 0 :fill-opacity] (-> (dm/get-in shape [:svg-attrs :style :fillOpacity]) diff --git a/common/src/app/common/test_helpers/components.cljc b/common/src/app/common/test_helpers/components.cljc new file mode 100644 index 0000000000000000000000000000000000000000..150bbeeb4d595fbb8f7fcc26e8bba594c45d3c9d --- /dev/null +++ b/common/src/app/common/test_helpers/components.cljc @@ -0,0 +1,162 @@ +;; 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 app.common.test-helpers.components + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.changes-builder :as pcb] + [app.common.files.helpers :as cfh] + [app.common.geom.point :as gpt] + [app.common.logic.libraries :as cll] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape-tree :as ctst])) + +(defn make-component + [file label root-label & {:keys [] :as params}] + (let [page (thf/current-page file) + root (ths/get-shape file root-label)] + + (dm/assert! + "Need that root is already a frame" + (cfh/frame-shape? root)) + + (let [[_new-root _new-shapes updated-shapes] + (ctn/convert-shape-in-component root (:objects page) (:id file)) + + updated-root (first updated-shapes) ; Can't use new-root because it has a new id + + [path name] (cfh/parse-path-name (:name updated-root))] + (thi/set-id! label (:component-id updated-root)) + + (ctf/update-file-data + file + (fn [file-data] + (as-> file-data $ + (reduce (fn [file-data shape] + (ctpl/update-page file-data + (:id page) + #(update % :objects assoc (:id shape) shape))) + $ + updated-shapes) + (ctkl/add-component $ (assoc params + :id (:component-id updated-root) + :name name + :path path + :main-instance-id (:id updated-root) + :main-instance-page (:id page) + :shapes updated-shapes)))))))) + +(defn get-component + [file label & {:keys [include-deleted?] :or {include-deleted? false}}] + (ctkl/get-component (:data file) (thi/id label) include-deleted?)) + +(defn get-component-by-id + [file id] + (ctkl/get-component (:data file) id)) + +(defn- set-children-labels! + [file shape-label children-labels] + (doseq [[label id] + (d/zip children-labels (cfh/get-children-ids (-> (thf/current-page file) :objects) + (thi/id shape-label)))] + (thi/set-id! label id))) + +(defn instantiate-component + [file component-label copy-root-label & {:keys [parent-label library children-labels] :as params}] + (let [page (thf/current-page file) + library (or library file) + component (get-component library component-label) + parent-id (when parent-label + (thi/id parent-label)) + parent (when parent-id + (ctst/get-shape page parent-id)) + frame-id (if (cfh/frame-shape? parent) + (:id parent) + (:frame-id parent)) + + [copy-root copy-shapes] + (ctn/make-component-instance page + component + (:data library) + (gpt/point 100 100) + true + {:force-id (thi/new-id! copy-root-label) + :force-frame-id frame-id}) + + copy-root' (cond-> copy-root + (some? parent) + (assoc :parent-id parent-id) + + (some? frame-id) + (assoc :frame-id frame-id) + + (and (some? parent) (ctn/in-any-component? (:objects page) parent)) + (dissoc :component-root)) + + file' (ctf/update-file-data + file + (fn [file-data] + (as-> file-data $ + (ctpl/update-page $ + (:id page) + #(ctst/add-shape (:id copy-root') + copy-root' + % + frame-id + parent-id + nil + true)) + (reduce (fn [file-data shape] + (ctpl/update-page file-data + (:id page) + #(ctst/add-shape (:id shape) + shape + % + (:frame-id shape) + (:parent-id shape) + nil + true))) + $ + (remove #(= (:id %) (:id copy-root')) copy-shapes)))))] + + (when children-labels + (set-children-labels! file' copy-root-label children-labels)) + + file')) + +(defn component-swap + [file shape-label new-component-label new-shape-label & {:keys [library children-labels] :as params}] + (let [shape (ths/get-shape file shape-label) + library (or library file) + libraries {(:id library) library} + page (thf/current-page file) + objects (:objects page) + id-new-component (-> (get-component library new-component-label) + :id) + + ;; Store the properties that need to be maintained when the component is swapped + keep-props-values (select-keys shape ctk/swap-keep-attrs) + + [new_shape _ changes] + (-> (pcb/empty-changes nil (:id page)) + (cll/generate-component-swap objects shape (:data file) page libraries id-new-component 0 nil keep-props-values)) + + file' (thf/apply-changes file changes)] + + (thi/set-id! new-shape-label (:id new_shape)) + + (when children-labels + (set-children-labels! file' new-shape-label children-labels)) + + file')) diff --git a/common/src/app/common/test_helpers/compositions.cljc b/common/src/app/common/test_helpers/compositions.cljc new file mode 100644 index 0000000000000000000000000000000000000000..82ebf5c58cc4e3c9d017b7f7b232e8f24f61c1c7 --- /dev/null +++ b/common/src/app/common/test_helpers/compositions.cljc @@ -0,0 +1,390 @@ +;; 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 app.common.test-helpers.compositions + (:require + [app.common.data :as d] + [app.common.files.changes-builder :as pcb] + [app.common.geom.point :as gpt] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.shapes :as ths] + [app.common.types.container :as ctn])) + +;; ----- File building + +(defn add-rect + [file rect-label & {:keys [] :as params}] + ;; Generated shape tree: + ;; :rect-label [:type :rect :name Rect1] + (ths/add-sample-shape file rect-label + (merge {:type :rect + :name "Rect1"} + params))) + +(defn add-frame + [file frame-label & {:keys [] :as params}] + ;; Generated shape tree: + ;; :frame-label [:type :frame :name Frame1] + (ths/add-sample-shape file frame-label + (merge {:type :frame + :name "Frame1"} + params))) + +(defn add-group + [file group-label & {:keys [] :as params}] + ;; Generated shape tree: + ;; :group-label [:type :group :name Group1] + (ths/add-sample-shape file group-label + (merge {:type :group + :name "Group1"} + params))) + +(defn add-frame-with-child + [file frame-label child-label & {:keys [frame-params child-params]}] + ;; Generated shape tree: + ;; :frame-label [:name Frame1] + ;; :child-label [:name Rect1] + (-> file + (add-frame frame-label frame-params) + (ths/add-sample-shape child-label + (merge {:type :rect + :name "Rect1" + :parent-label frame-label} + child-params)))) + +(defn add-minimal-component + [file component-label root-label + & {:keys [component-params root-params]}] + ;; Generated shape tree: + ;; {:root-label} [:name Frame1] # [Component :component-label] + (-> file + (add-frame root-label root-params) + (thc/make-component component-label root-label component-params))) + +(defn add-minimal-component-with-copy + [file component-label main-root-label copy-root-label + & {:keys [component-params main-root-params copy-root-params]}] + ;; Generated shape tree: + ;; {:main-root-label} [:name Frame1] # [Component :component-label] + ;; :copy-root-label [:name Frame1] #--> [Component :component-label] :main-root-label + (-> file + (add-minimal-component component-label + main-root-label + :component-params component-params + :root-params main-root-params) + (thc/instantiate-component component-label copy-root-label copy-root-params))) + +(defn add-simple-component + [file component-label root-label child-label + & {:keys [component-params root-params child-params]}] + ;; Generated shape tree: + ;; {:root-label} [:name Frame1] # [Component :component-label] + ;; :child-label [:name Rect1] + (-> file + (add-frame-with-child root-label child-label :frame-params root-params :child-params child-params) + (thc/make-component component-label root-label component-params))) + +(defn add-simple-component-with-copy + [file component-label main-root-label main-child-label copy-root-label + & {:keys [component-params main-root-params main-child-params copy-root-params]}] + ;; Generated shape tree: + ;; {:main-root-label} [:name Frame1] # [Component :component-label] + ;; :main-child-label [:name Rect1] + ;; + ;; :copy-root-label [:name Frame1] #--> [Component :component-label] :main-root-label + ;; [:name Rect1] ---> :main-child-label + (-> file + (add-simple-component component-label + main-root-label + main-child-label + :component-params component-params + :root-params main-root-params + :child-params main-child-params) + (thc/instantiate-component component-label copy-root-label copy-root-params))) + +(defn add-component-with-many-children + [file component-label root-label child-labels + & {:keys [component-params root-params child-params-list]}] + ;; Generated shape tree: + ;; {:root-label} [:name Frame1] # [Component :component-label] + ;; :child1-label [:name Rect1] + ;; :child2-label [:name Rect2] + ;; :child3-label [:name Rect3] + (as-> file $ + (add-frame $ root-label root-params) + (reduce (fn [file [index [label params]]] + (ths/add-sample-shape file + label + (merge {:type :rect + :name (str "Rect" (inc index)) + :parent-label root-label} + params))) + $ + (d/enumerate (d/zip-all child-labels child-params-list))) + (thc/make-component $ component-label root-label component-params))) + +(defn add-component-with-many-children-and-copy + [file component-label main-root-label main-child-labels copy-root-label + & {:keys [component-params main-root-params main-child-params-list copy-root-params]}] + ;; Generated shape tree: + ;; {:root-label} [:name Frame1] # [Component :component-label] + ;; :child1-label [:name Rect1] + ;; :child2-label [:name Rect2] + ;; :child3-label [:name Rect3] + ;; + ;; :copy-root-label [:name Frame1] #--> [Component :component-label] :root-label + ;; [:name Rect1] ---> :child1-label + ;; [:name Rect2] ---> :child2-label + ;; [:name Rect3] ---> :child3-label + (-> file + (add-component-with-many-children component-label + main-root-label + main-child-labels + :component-params component-params + :root-params main-root-params + :child-params-list main-child-params-list) + (thc/instantiate-component component-label copy-root-label copy-root-params))) + +(defn add-nested-component + [file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label + & {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params]}] + ;; Generated shape tree: + ;; {:main1-root-label} [:name Frame1] # [Component :component1-label] + ;; :main1-child-label [:name Rect1] + ;; + ;; {:main2-root-label} [:name Frame2] # [Component :component2-label] + ;; :nested-head-label [:name Frame1] @--> [Component :component1-label] :main1-root-label + ;; [:name Rect1] ---> :main1-child-label + (-> file + (add-simple-component component1-label + main1-root-label + main1-child-label + :component-params component1-params + :root-params root1-params + :child-params main1-child-params) + (add-frame main2-root-label (merge {:name "Frame2"} + main2-root-params)) + (thc/instantiate-component component1-label + nested-head-label + (assoc nested-head-params + :parent-label main2-root-label)) + (thc/make-component component2-label + main2-root-label + component2-params))) + +(defn add-nested-component-with-copy + [file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label copy2-root-label + & {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params copy2-root-params]}] + ;; Generated shape tree: + ;; {:main1-root-label} [:name Frame1] # [Component :component1-label] + ;; :main1-child-label [:name Rect1] + ;; + ;; {:main2-root-label} [:name Frame2] # [Component :component2-label] + ;; :nested-head-label [:name Frame1] @--> [Component :component1-label] :main1-root-label + ;; [:name Rect1] ---> :main1-child-label + ;; + ;; :copy2-label [:name Frame2] #--> [Component :component2-label] :main2-root-label + ;; [:name Frame1] @--> [Component :component1-label] :nested-head-label + ;; [:name Rect1] ---> + (-> file + (add-nested-component component1-label + main1-root-label + main1-child-label + component2-label + main2-root-label + nested-head-label + :component1-params component1-params + :root1-params root1-params + :main1-child-params main1-child-params + :component2-params component2-params + :main2-root-params main2-root-params + :nested-head-params nested-head-params) + (thc/instantiate-component component2-label copy2-root-label copy2-root-params))) + +;; ----- Getters + +(defn bottom-shape-by-id + "Get the deepest descendant of a shape by id" + [file id & {:keys [page-label]}] + (let [shape (ths/get-shape-by-id file id :page-label page-label)] + (if (some? (:shapes shape)) + (let [child-id (-> (:shapes shape) + first)] + (bottom-shape-by-id file child-id :page-label page-label)) + shape))) + +(defn bottom-shape + "Get the deepest descendant of a shape by tag" + [file tag & {:keys [page-label]}] + (let [shape (ths/get-shape file tag :page-label page-label)] + (bottom-shape-by-id file (:id shape) :page-label page-label))) + +(defn bottom-fill-color + "Get the first fill color of the deepest descendant of a shape by tag" + [file tag & {:keys [page-label]}] + (-> (bottom-shape file tag :page-label page-label) + :fills + first + :fill-color)) + +;; ----- File modifiers + +(defn propagate-component-changes + "Propagates the component changes for component specified by component-tag" + [file component-tag] + (let [file-id (:id file) + + changes (-> (pcb/empty-changes) + (cll/generate-sync-file-changes + nil + :components + file-id + (:id (thc/get-component file component-tag)) + file-id + {file-id file} + file-id))] + (thf/apply-changes file changes))) + +(defn swap-component + "Swap the specified shape by the component specified by component-tag" + [file shape component-tag & {:keys [page-label propagate-fn]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file)) + + [_ _all-parents changes] + (cll/generate-component-swap (pcb/empty-changes) + (:objects page) + shape + (:data file) + page + {(:id file) file} + (-> (thc/get-component file component-tag) + :id) + 0 + nil + {}) + + file' (thf/apply-changes file changes)] + (if propagate-fn + (propagate-fn file') + file'))) + +(defn swap-component-in-shape [file shape-tag component-tag & {:keys [page-label propagate-fn]}] + (swap-component file (ths/get-shape file shape-tag :page-label page-label) component-tag :page-label page-label :propagate-fn propagate-fn)) + +(defn swap-component-in-first-child [file shape-tag component-tag & {:keys [page-label propagate-fn]}] + (let [first-child-id (->> (ths/get-shape file shape-tag :page-label page-label) + :shapes + first)] + (swap-component file + (ths/get-shape-by-id file first-child-id :page-label page-label) + component-tag + :page-label page-label + :propagate-fn propagate-fn))) + +(defn update-color + "Update the first fill color for the shape identified by shape-tag" + [file shape-tag color & {:keys [page-label propagate-fn]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file)) + changes + (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id (ths/get-shape file shape-tag :page-label page-label))} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color color))) + (:objects page) + {}) + file' (thf/apply-changes file changes)] + (if propagate-fn + (propagate-fn file') + file'))) + +(defn update-bottom-color + "Update the first fill color of the deepest descendant for the shape identified by shape-tag" + [file shape-tag color & {:keys [page-label propagate-fn]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file)) + changes + (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id (bottom-shape file shape-tag :page-label page-label))} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color color))) + (:objects page) + {}) + file' (thf/apply-changes file changes)] + (if propagate-fn + (propagate-fn file') + file'))) + +(defn reset-overrides [file shape & {:keys [page-label propagate-fn]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file)) + container (ctn/make-container page :page) + file-id (:id file) + changes (-> (pcb/empty-changes) + (cll/generate-reset-component + file + {file-id file} + (ctn/make-container container :page) + (:id shape) + true)) + file' (thf/apply-changes file changes)] + (if propagate-fn + (propagate-fn file') + file'))) + +(defn reset-overrides-in-first-child [file shape-tag & {:keys [page-label propagate-fn]}] + (let [first-child-id (->> + (ths/get-shape file shape-tag :page-label page-label) + :shapes + first) + shape (ths/get-shape-by-id file first-child-id :page-label page-label)] + (reset-overrides file shape :page-label page-label :propagate-fn propagate-fn))) + +(defn delete-shape [file shape-tag & {:keys [page-label propagate-fn]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file)) + [_ changes] (cls/generate-delete-shapes (pcb/empty-changes nil (:id page)) + file + page + (:objects page) + #{(-> (ths/get-shape file shape-tag :page-label page-label) + :id)} + {:components-v2 true}) + file' (thf/apply-changes file changes)] + (if propagate-fn + (propagate-fn file') + file'))) + +(defn duplicate-shape [file shape-tag & {:keys [page-label propagate-fn]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file)) + shape (ths/get-shape file shape-tag :page-label page-label) + changes + (-> (pcb/empty-changes nil) + (cll/generate-duplicate-changes (:objects page) ;; objects + page ;; page + #{(:id shape)} ;; ids + (gpt/point 0 0) ;; delta + {(:id file) file} ;; libraries + (:data file) ;; library-data + (:id file)) ;; file-id + (cll/generate-duplicate-changes-update-indices (:objects page) ;; objects + #{(:id shape)})) + file' (thf/apply-changes file changes)] + (if propagate-fn + (propagate-fn file') + file'))) + diff --git a/common/src/app/common/test_helpers/files.cljc b/common/src/app/common/test_helpers/files.cljc new file mode 100644 index 0000000000000000000000000000000000000000..59b1665553ff13f131ae1ee10ac9a76eee0cd296 --- /dev/null +++ b/common/src/app/common/test_helpers/files.cljc @@ -0,0 +1,189 @@ +;; 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 app.common.test-helpers.files + (:require + [app.common.data :as d] + [app.common.features :as ffeat] + [app.common.files.changes :as cfc] + [app.common.files.validate :as cfv] + [app.common.pprint :refer [pprint]] + [app.common.test-helpers.ids-map :as thi] + [app.common.types.component :as ctk] + [app.common.types.file :as ctf] + [app.common.types.page :as ctp] + [app.common.types.pages-list :as ctpl] + [app.common.uuid :as uuid] + [cuerdas.core :as str])) + +;; ----- Files + +(defn sample-file + [label & {:keys [page-label name] :as params}] + (binding [ffeat/*current* #{"components/v2"}] + (let [params (cond-> params + label + (assoc :id (thi/new-id! label)) + + page-label + (assoc :page-id (thi/new-id! page-label)) + + (nil? name) + (assoc :name "Test file")) + + file (-> (ctf/make-file (dissoc params :page-label)) + (assoc :features #{"components/v2"})) + + page (-> file + :data + (ctpl/pages-seq) + (first))] + + (with-meta file + {:current-page-id (:id page)})))) + +(defn validate-file! + ([file] (validate-file! file {})) + ([file libraries] + (cfv/validate-file-schema! file) + (cfv/validate-file! file libraries))) + +(defn apply-changes + [file changes] + (let [file' (ctf/update-file-data file #(cfc/process-changes % (:redo-changes changes) true))] + (validate-file! file') + file')) + +;; ----- Pages + +(defn sample-page + [label & {:keys [] :as params}] + (ctp/make-empty-page (assoc params :id (thi/new-id! label)))) + +(defn add-sample-page + [file label & {:keys [] :as params}] + (let [page (sample-page label params)] + (-> file + (ctf/update-file-data #(ctpl/add-page % page)) + (vary-meta assoc :current-page-id (:id page))))) + +(defn get-page + [file label] + (ctpl/get-page (:data file) (thi/id label))) + +(defn current-page-id + [file] + (:current-page-id (meta file))) + +(defn current-page + [file] + (ctpl/get-page (:data file) (current-page-id file))) + +(defn switch-to-page + [file label] + (vary-meta file assoc :current-page-id (thi/id label))) + +;; ----- Debug + +(defn dump-tree + "Dump a file using dump-tree function in common.types.file." + [file & {:keys [page-label libraries] :as params}] + (let [params (-> params + (or {:show-ids true :show-touched true}) + (dissoc page-label libraries)) + page (if (some? page-label) + (:id (get-page file page-label)) + (current-page-id file)) + libraries (or libraries {})] + + (ctf/dump-tree file page libraries params))) + +(defn pprint-file + "Pretry print a file trying to limit the quantity of info shown." + [file & {:keys [level length] :or {level 10 length 1000}}] + (pprint file {:level level :length length})) + +(defn dump-shape + "Dump a shape, with each attribute in a line." + [shape] + (println "{") + (doseq [[k v] (sort shape)] + (when (some? v) + (println (str " " k " : " v)))) + (println "}")) + +(defn- stringify-keys [m keys] + (let [kv (-> (select-keys m keys) + (assoc :swap-slot (when ((set keys) :swap-slot) + (ctk/get-swap-slot m))) + (assoc :swap-slot-label (when ((set keys) :swap-slot-label) + (when-let [slot (ctk/get-swap-slot m)] + (thi/label slot)))) + (d/without-nils)) + + pretty-uuid (fn [id] + (let [id (str id)] + (str "#" (subs id (- (count id) 6))))) + + format-kv (fn [[k v]] + (cond + (uuid? v) + (str k " " (pretty-uuid v)) + + :else + (str k " " v)))] + + (when (seq kv) + (str " [" (apply str (interpose ", " (map format-kv kv))) "]")))) + +(defn- dump-page-shape + [shape keys padding show-refs?] + (println (str/pad (str padding + (when (and (:main-instance shape) show-refs?) "{") + (thi/label (:id shape)) + (when (and (:main-instance shape) show-refs?) "}") + (when (seq keys) + (stringify-keys shape keys))) + {:length 50 :type :right}) + (if (nil? (:shape-ref shape)) + (if (and (:component-root shape) show-refs?) + (str "# [Component " (thi/label (:component-id shape)) "]") + "") + (if show-refs? + (str/format "%s--> %s%s" + (cond (:component-root shape) "#" + (:component-id shape) "@" + :else "-") + (if (:component-root shape) + (str "[Component " (thi/label (:component-id shape)) "] ") + "") + (thi/label (:shape-ref shape))) + "")))) + +(defn dump-page + "Dump the layer tree of the page, showing labels of the shapes. + - keys: a list of attributes of the shapes you want to show. In addition, you + can add :swap-slot to show the slot id (if any) or :swap-slot-label + to show the corresponding label. + - show-refs?: if true, the component references will be shown." + [page & {:keys [keys root-id padding show-refs?] + :or {keys [:name :swap-slot-label] root-id uuid/zero padding "" show-refs? true}}] + (let [lookupf (d/getf (:objects page)) + root-shape (lookupf root-id) + shapes (map lookupf (:shapes root-shape))] + (doseq [shape shapes] + (dump-page-shape shape keys padding show-refs?) + (dump-page page + :keys keys + :root-id (:id shape) + :padding (str padding " ") + :show-refs? show-refs?)))) + +(defn dump-file + "Dump the current page of the file, using dump-page above. + Example: (thf/dump-file file :keys [:name :swap-slot-label] :show-refs? false)" + [file & {:keys [] :as params}] + (dump-page (current-page file) params)) diff --git a/common/src/app/common/test_helpers/ids_map.cljc b/common/src/app/common/test_helpers/ids_map.cljc new file mode 100644 index 0000000000000000000000000000000000000000..b249962874a61e6933bc60aee6665682f166a1f6 --- /dev/null +++ b/common/src/app/common/test_helpers/ids_map.cljc @@ -0,0 +1,50 @@ +;; 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 app.common.test-helpers.ids-map + (:require + [app.common.uuid :as uuid])) + +;; ---- Helpers to manage ids as known identifiers + +(def ^:private idmap (atom {})) +(def ^:private next-uuid-val (atom 1)) + +(defn reset-idmap! [] + (reset! idmap {}) + (reset! next-uuid-val 1)) + +(defn set-id! + [label id] + (swap! idmap assoc label id)) + +(defn new-id! + [label] + (let [id (uuid/next)] + (set-id! label id) + id)) + +(defn id + [label] + (get @idmap label)) + +(defn test-fixture + ;; Ensure that each test starts with a clean ids map + [f] + (reset-idmap!) + (f)) + +(defn label [id] + (or (->> @idmap + (filter #(= id (val %))) + (map key) + (first)) + (str ""))) + +(defn next-uuid [] + (let [current (uuid/custom @next-uuid-val)] + (swap! next-uuid-val inc) + current)) diff --git a/common/src/app/common/test_helpers/shapes.cljc b/common/src/app/common/test_helpers/shapes.cljc new file mode 100644 index 0000000000000000000000000000000000000000..408e1233e10b8db93b41208baf61765a1441cd18 --- /dev/null +++ b/common/src/app/common/test_helpers/shapes.cljc @@ -0,0 +1,132 @@ +;; 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 app.common.test-helpers.shapes + (:require + [app.common.colors :as clr] + [app.common.files.helpers :as cfh] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.types.color :as ctc] + [app.common.types.colors-list :as ctcl] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.shape :as cts] + [app.common.types.shape-tree :as ctst] + [app.common.types.shape.interactions :as ctsi] + [app.common.types.typographies-list :as cttl] + [app.common.types.typography :as ctt])) + +(defn sample-shape + [label & {:keys [type] :as params}] + (let [params (cond-> params + label + (assoc :id (thi/new-id! label)) + + (nil? type) + (assoc :type :rect))] + + (cts/setup-shape params))) + +(defn add-sample-shape + [file label & {:keys [parent-label] :as params}] + (let [page (thf/current-page file) + shape (sample-shape label (dissoc params :parent-label)) + parent-id (when parent-label + (thi/id parent-label)) + parent (when parent-id + (ctst/get-shape page parent-id)) + frame-id (if (cfh/frame-shape? parent) + (:id parent) + (:frame-id parent))] + (ctf/update-file-data + file + (fn [file-data] + (ctpl/update-page file-data + (:id page) + #(ctst/add-shape (:id shape) + shape + % + frame-id + parent-id + nil + true)))))) + +(defn get-shape + [file label & {:keys [page-label]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file))] + (ctst/get-shape page (thi/id label)))) + +(defn get-shape-by-id + [file id & {:keys [page-label]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file))] + (ctst/get-shape page id))) + +(defn update-shape + [file shape-label attr val & {:keys [page-label]}] + (let [page (if page-label + (thf/get-page file page-label) + (thf/current-page file)) + shape (ctst/get-shape page (thi/id shape-label))] + (ctf/update-file-data + file + (fn [file-data] + (ctpl/update-page file-data + (:id page) + #(ctst/set-shape % (ctn/set-shape-attr shape attr val))))))) + +(defn sample-color + [label & {:keys [] :as params}] + (ctc/make-color (assoc params :id (thi/new-id! label)))) + +(defn sample-fill-color + [& {:keys [fill-color fill-opacity] :as params}] + (let [params (cond-> params + (nil? fill-color) + (assoc :fill-color clr/black) + + (nil? fill-opacity) + (assoc :fill-opacity 1))] + params)) + +(defn sample-fills-color + [& {:keys [] :as params}] + [(sample-fill-color params)]) + +(defn add-sample-library-color + [file label & {:keys [] :as params}] + (let [color (sample-color label params)] + (ctf/update-file-data file #(ctcl/add-color % color)))) + +(defn sample-typography + [label & {:keys [] :as params}] + (ctt/make-typography (assoc params :id (thi/new-id! label)))) + +(defn add-sample-typography + [file label & {:keys [] :as params}] + (let [typography (sample-typography label params)] + (ctf/update-file-data file #(cttl/add-typography % typography)))) + +(defn add-interaction + [file origin-label dest-label] + (let [page (thf/current-page file) + origin (get-shape file origin-label) + dest (get-shape file dest-label) + interaction (-> ctsi/default-interaction + (ctsi/set-destination (:id dest)) + (assoc :position-relative-to (:id origin))) + interactions (ctsi/add-interaction (:interactions origin) interaction)] + (ctf/update-file-data + file + (fn [file-data] + (ctpl/update-page file-data + (:id page) + #(ctst/set-shape % (assoc origin :interactions interactions))))))) diff --git a/common/src/app/common/text.cljc b/common/src/app/common/text.cljc index 8b301d2d404768ac6990201919924dbf0413beae..c5d14f5498bbacab33429ce81466cdec02bc7a05 100644 --- a/common/src/app/common/text.cljc +++ b/common/src/app/common/text.cljc @@ -361,7 +361,7 @@ new-acc (cond - (:children node) + (not (is-text-node? node)) (reduce #(rec-style-text-map %1 %2 node-style) acc (:children node)) (not= head-style node-style) @@ -381,6 +381,83 @@ (-> (rec-style-text-map [] node {}) reverse))) +(defn content-range->text+styles + "Given a root node of a text content extracts the texts with its associated styles" + [node start end] + (let [sss (content->text+styles node)] + (loop [styles (seq sss) + taking? false + acc 0 + result []] + (if styles + (let [[node-style text] (first styles) + from acc + to (+ acc (count text)) + taking? (or taking? (and (<= from start) (< start to))) + text (subs text (max 0 (- start acc)) (- end acc)) + result (cond-> result + (and taking? (d/not-empty? text)) + (conj (assoc node-style :text text))) + continue? (or (> from end) (>= end to))] + (recur (when continue? (rest styles)) taking? to result)) + result)))) + + +(defn content->text + "Given a root node of a text content extracts the texts with its associated styles" + [content] + (letfn [(add-node [acc node] + (cond + (is-paragraph-node? node) + (conj acc []) + + (is-text-node? node) + (let [i (dec (count acc))] + (update acc i conj (:text node))) + + :else + acc))] + (->> (node-seq content) + (reduce add-node []) + (map #(str/join "" %)) + (str/join "\n")))) + +(defn change-text + "Changes the content of the text shape to use the text as argument. Will use the styles of the + first paragraph and text that is present in the shape (and override the rest)" + [shape text] + (let [content (:content shape) + + root-styles (select-keys content root-attrs) + + paragraph-style (merge + default-text-attrs + (select-keys (->> content (node-seq is-paragraph-node?) first) text-all-attrs)) + text-style (merge + default-text-attrs + (select-keys (->> content (node-seq is-text-node?) first) text-all-attrs)) + + paragraph-texts (str/split text "\n") + + paragraphs + (->> paragraph-texts + (mapv + (fn [pt] + (merge + paragraph-style + {:type "paragraph" + :children [(merge {:text pt} text-style)]})))) + + new-content + (d/patch-object + {:type "root" + :children + [{:type "paragraph-set" + :children paragraphs}]} + root-styles)] + + (assoc shape :content new-content))) + (defn index-content "Adds a property `$id` that identifies the current node inside" ([content] diff --git a/common/src/app/common/time.cljc b/common/src/app/common/time.cljc index 6cd8601d66b12365d9d58dc6df7d33a7b24123d9..02c41f94670686ced2bd02a83dbef243d0956539 100644 --- a/common/src/app/common/time.cljc +++ b/common/src/app/common/time.cljc @@ -5,13 +5,14 @@ ;; Copyright (c) KALEIDOS INC (ns app.common.time - "A new cross-platform date and time API. It should be prefered over - a platform specific implementation found on `app.util.time`." + "Minimal cross-platoform date time api for specific use cases on types + definition and other common code." #?(:cljs (:require ["luxon" :as lxn]) :clj (:import + java.time.format.DateTimeFormatter java.time.Instant java.time.Duration))) @@ -26,10 +27,29 @@ #?(:clj (Instant/now) :cljs (.local ^js DateTime))) -(defn instant +(defn instant? + [o] + #?(:clj (instance? Instant o) + :cljs (instance? DateTime o))) + +(defn parse-instant [s] - #?(:clj (Instant/ofEpochMilli s) - :cljs (.fromMillis ^js DateTime s #js {:zone "local" :setZone false}))) + (cond + (instant? s) + s + + (int? s) + #?(:clj (Instant/ofEpochMilli s) + :cljs (.fromMillis ^js DateTime s #js {:zone "local" :setZone false})) + + (string? s) + #?(:clj (Instant/parse s) + :cljs (.fromISO ^js DateTime s)))) + +(defn format-instant + [v] + #?(:clj (.format DateTimeFormatter/ISO_INSTANT ^Instant v) + :cljs (.toISO ^js v))) #?(:cljs (extend-protocol IComparable @@ -45,7 +65,6 @@ 0 (if (< (inst-ms it) (inst-ms other)) -1 1))))) - #?(:cljs (extend-type DateTime cljs.core/IEquiv diff --git a/common/src/app/common/transit.cljc b/common/src/app/common/transit.cljc index 93ab8d4b2a92b68645dcee1540135dd2908b1b30..21673bdb4f1c7ac40933e01ab36938d57873b210 100644 --- a/common/src/app/common/transit.cljc +++ b/common/src/app/common/transit.cljc @@ -12,7 +12,7 @@ [app.common.uri :as uri] [cognitect.transit :as t] [lambdaisland.uri :as luri] - [linked.core :as lk] + [linked.map :as lkm] [linked.set :as lks]) #?(:clj (:import @@ -24,6 +24,7 @@ java.time.Instant java.time.OffsetDateTime lambdaisland.uri.URI + linked.map.LinkedMap linked.set.LinkedSet))) (def write-handlers (atom nil)) @@ -118,10 +119,15 @@ {:id "u" :rfn parse-uuid}) + {:id "ordered-map" + :class #?(:clj LinkedMap :cljs lkm/LinkedMap) + :wfn vec + :rfn #(into lkm/empty-linked-map %)} + {:id "ordered-set" :class #?(:clj LinkedSet :cljs lks/LinkedSet) :wfn vec - :rfn #(into (lk/set) %)} + :rfn #(into lks/empty-linked-set %)} {:id "duration" :class #?(:clj Duration :cljs lxn/Duration) diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index 382530ac2a40857df74b168b321ba7c3cc9ce343..78a7f8114579d837af3d3edb44252ce0b3b65476 100644 --- a/common/src/app/common/types/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -7,46 +7,53 @@ (ns app.common.types.color (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.schema :as sm] + [app.common.schema.generators :as sg] [app.common.schema.openapi :as-alias oapi] [app.common.text :as txt] - [app.common.types.color.generic :as-alias color-generic] - [app.common.types.color.gradient :as-alias color-gradient] - [app.common.types.color.gradient.stop :as-alias color-gradient-stop] - [clojure.test.check.generators :as tgen])) + [app.common.types.plugins :as ctpg] + [app.common.uuid :as uuid] + [cuerdas.core :as str])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; SCHEMAS +;; SCHEMAS & TYPES ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (def rgb-color-re #"^#(?:[0-9a-fA-F]{3}){1,2}$") -(defn- random-rgb-color +(defn- generate-rgb-color [] - #?(:clj (format "#%06x" (rand-int 16rFFFFFF)) - :cljs - (let [r (rand-int 255) - g (rand-int 255) - b (rand-int 255)] - (str "#" - (.. r (toString 16) (padStart 2 "0")) - (.. g (toString 16) (padStart 2 "0")) - (.. b (toString 16) (padStart 2 "0")))))) - -(sm/define! ::rgb-color - {:type ::rgb-color - :pred #(and (string? %) (some? (re-matches rgb-color-re %))) + (sg/fmap (fn [_] + #?(:clj (format "#%06x" (rand-int 16rFFFFFF)) + :cljs + (let [r (rand-int 255) + g (rand-int 255) + b (rand-int 255)] + (str "#" + (.. r (toString 16) (padStart 2 "0")) + (.. g (toString 16) (padStart 2 "0")) + (.. b (toString 16) (padStart 2 "0")))))) + sg/any)) + +(defn rgb-color-string? + [o] + (and (string? o) (some? (re-matches rgb-color-re o)))) + +(def ^:private type:rgb-color + {:type :string + :pred rgb-color-string? :type-properties {:title "rgb-color" :description "RGB Color String" :error/message "expected a valid RGB color" - :gen/gen (->> tgen/any (tgen/fmap (fn [_] (random-rgb-color)))) - + :error/code "errors.invalid-rgb-color" + :gen/gen (generate-rgb-color) ::oapi/type "integer" ::oapi/format "int64"}}) -(sm/define! ::image-color +(def schema:image-color [:map {:title "ImageColor"} [:name {:optional true} :string] [:width :int] @@ -55,7 +62,10 @@ [:id ::sm/uuid] [:keep-aspect-ratio {:optional true} :boolean]]) -(sm/define! ::gradient +(def gradient-types + #{:linear :radial}) + +(def schema:gradient [:map {:title "Gradient"} [:type [::sm/one-of #{:linear :radial}]] [:start-x ::sm/safe-number] @@ -70,7 +80,7 @@ [:opacity {:optional true} [:maybe ::sm/safe-number]] [:offset ::sm/safe-number]]]]]) -(sm/define! ::color +(def schema:color [:and [:map {:title "Color"} [:id {:optional true} ::sm/uuid] @@ -82,29 +92,52 @@ [:modified-at {:optional true} ::sm/inst] [:ref-id {:optional true} ::sm/uuid] [:ref-file {:optional true} ::sm/uuid] - [:gradient {:optional true} [:maybe ::gradient]] - [:image {:optional true} [:maybe ::image-color]]] + [:gradient {:optional true} [:maybe schema:gradient]] + [:image {:optional true} [:maybe schema:image-color]] + [:plugin-data {:optional true} ::ctpg/plugin-data]] [::sm/contains-any {:strict true} [:color :gradient :image]]]) -(sm/define! ::recent-color +(def schema:recent-color [:and [:map {:title "RecentColor"} [:opacity {:optional true} [:maybe ::sm/safe-number]] [:color {:optional true} [:maybe ::rgb-color]] - [:gradient {:optional true} [:maybe ::gradient]] - [:image {:optional true} [:maybe ::image-color]]] + [:gradient {:optional true} [:maybe schema:gradient]] + [:image {:optional true} [:maybe schema:image-color]]] [::sm/contains-any {:strict true} [:color :gradient :image]]]) -(def check-color! - (sm/check-fn ::color)) +(sm/register! ::rgb-color type:rgb-color) +(sm/register! ::color schema:color) +(sm/register! ::gradient schema:gradient) +(sm/register! ::image-color schema:image-color) +(sm/register! ::recent-color schema:recent-color) -(def check-recent-color! - (sm/check-fn ::recent-color)) +(def valid-color? + (sm/lazy-validator schema:color)) + +(def valid-recent-color? + (sm/lazy-validator schema:recent-color)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; --- factory + +(defn make-color + [{:keys [id name path value color opacity ref-id ref-file gradient image]}] + (-> {:id (or id (uuid/next)) + :name (or name color "Black") + :path path + :value value + :color (or color "#000000") + :opacity (or opacity 1) + :ref-id ref-id + :ref-file ref-file + :gradient gradient + :image image} + (d/without-nils))) + ;; --- fill (defn fill->shape-color @@ -358,3 +391,136 @@ (process-shape-colors shape sync-color))) +(defn- eq-recent-color? + [c1 c2] + (or (= c1 c2) + (and (some? (:color c1)) + (some? (:color c2)) + (= (:color c1) (:color c2))))) + +(defn add-recent-color + "Moves the color to the top of the list and then truncates up to 15" + [state file-id color] + (update state file-id (fn [colors] + (let [colors (d/removev (partial eq-recent-color? color) colors) + colors (conj colors color)] + (cond-> colors + (> (count colors) 15) + (subvec 1)))))) + +(defn stroke->color-att + [stroke file-id shared-libs] + (let [color-file-id (:stroke-color-ref-file stroke) + color-id (:stroke-color-ref-id stroke) + shared-libs-colors (dm/get-in shared-libs [color-file-id :data :colors]) + is-shared? (contains? shared-libs-colors color-id) + has-color? (or (not (nil? (:stroke-color stroke))) (not (nil? (:stroke-color-gradient stroke)))) + attrs (if (or is-shared? (= color-file-id file-id)) + (d/without-nils {:color (str/lower (:stroke-color stroke)) + :opacity (:stroke-opacity stroke) + :id color-id + :file-id color-file-id + :gradient (:stroke-color-gradient stroke)}) + (d/without-nils {:color (str/lower (:stroke-color stroke)) + :opacity (:stroke-opacity stroke) + :gradient (:stroke-color-gradient stroke)}))] + (when has-color? + {:attrs attrs + :prop :stroke + :shape-id (:shape-id stroke) + :index (:index stroke)}))) + +(defn shadow->color-att + [shadow file-id shared-libs] + (let [color-file-id (dm/get-in shadow [:color :file-id]) + color-id (dm/get-in shadow [:color :id]) + shared-libs-colors (dm/get-in shared-libs [color-file-id :data :colors]) + is-shared? (contains? shared-libs-colors color-id) + attrs (if (or is-shared? (= color-file-id file-id)) + (d/without-nils {:color (str/lower (dm/get-in shadow [:color :color])) + :opacity (dm/get-in shadow [:color :opacity]) + :id color-id + :file-id (dm/get-in shadow [:color :file-id]) + :gradient (dm/get-in shadow [:color :gradient])}) + (d/without-nils {:color (str/lower (dm/get-in shadow [:color :color])) + :opacity (dm/get-in shadow [:color :opacity]) + :gradient (dm/get-in shadow [:color :gradient])}))] + + + {:attrs attrs + :prop :shadow + :shape-id (:shape-id shadow) + :index (:index shadow)})) + +(defn text->color-att + [fill file-id shared-libs] + (let [color-file-id (:fill-color-ref-file fill) + color-id (:fill-color-ref-id fill) + shared-libs-colors (dm/get-in shared-libs [color-file-id :data :colors]) + is-shared? (contains? shared-libs-colors color-id) + attrs (if (or is-shared? (= color-file-id file-id)) + (d/without-nils {:color (str/lower (:fill-color fill)) + :opacity (:fill-opacity fill) + :id color-id + :file-id color-file-id + :gradient (:fill-color-gradient fill)}) + (d/without-nils {:color (str/lower (:fill-color fill)) + :opacity (:fill-opacity fill) + :gradient (:fill-color-gradient fill)}))] + {:attrs attrs + :prop :content + :shape-id (:shape-id fill) + :index (:index fill)})) + +(defn treat-node + [node shape-id] + (map-indexed #(assoc %2 :shape-id shape-id :index %1) node)) + +(defn extract-text-colors + [text file-id shared-libs] + (let [content (txt/node-seq txt/is-text-node? (:content text)) + content-filtered (map :fills content) + indexed (mapcat #(treat-node % (:id text)) content-filtered)] + (map #(text->color-att % file-id shared-libs) indexed))) + +(defn fill->color-att + [fill file-id shared-libs] + (let [color-file-id (:fill-color-ref-file fill) + color-id (:fill-color-ref-id fill) + shared-libs-colors (dm/get-in shared-libs [color-file-id :data :colors]) + is-shared? (contains? shared-libs-colors color-id) + has-color? (or (not (nil? (:fill-color fill))) (not (nil? (:fill-color-gradient fill)))) + attrs (if (or is-shared? (= color-file-id file-id)) + (d/without-nils {:color (str/lower (:fill-color fill)) + :opacity (:fill-opacity fill) + :id color-id + :file-id color-file-id + :gradient (:fill-color-gradient fill)}) + (d/without-nils {:color (str/lower (:fill-color fill)) + :opacity (:fill-opacity fill) + :gradient (:fill-color-gradient fill)}))] + (when has-color? + {:attrs attrs + :prop :fill + :shape-id (:shape-id fill) + :index (:index fill)}))) + +(defn extract-all-colors + [shapes file-id shared-libs] + (reduce + (fn [list shape] + (let [fill-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:fills shape)) + stroke-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:strokes shape)) + shadow-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:shadow shape))] + (if (= :text (:type shape)) + (-> list + (into (map #(stroke->color-att % file-id shared-libs)) stroke-obj) + (into (map #(shadow->color-att % file-id shared-libs)) shadow-obj) + (into (extract-text-colors shape file-id shared-libs))) + + (-> list + (into (map #(fill->color-att % file-id shared-libs)) fill-obj) + (into (map #(stroke->color-att % file-id shared-libs)) stroke-obj) + (into (map #(shadow->color-att % file-id shared-libs)) shadow-obj))))) + [] + shapes)) diff --git a/common/src/app/common/types/component.cljc b/common/src/app/common/types/component.cljc index 70b030eaf17c6b2226c0e8d56bc08fc1b665a879..cc064be509aa9b3215e33e08f996a4c4e9226c9d 100644 --- a/common/src/app/common/types/component.cljc +++ b/common/src/app/common/types/component.cljc @@ -130,6 +130,15 @@ (and (some? (:component-id shape)) (nil? (:component-root shape)))) +(defn subcopy-head? + "Check if this shape is the head of a subinstance that is a copy." + [shape] + ;; This is redundant with the previous one, but may give more security + ;; in case of bugs. + (and (some? (:component-id shape)) + (nil? (:component-root shape)) + (some? (:shape-ref shape)))) + (defn instance-of? [shape file-id component-id] (and (some? (:component-id shape)) @@ -138,10 +147,10 @@ (= (:component-file shape) file-id))) (defn is-main-of? - [shape-main shape-inst] - (and (:shape-ref shape-inst) - (or (= (:shape-ref shape-inst) (:id shape-main)) - (= (:shape-ref shape-inst) (:shape-ref shape-main))))) + [shape-main shape-inst components-v2] + (or (= (:shape-ref shape-inst) (:id shape-main)) + (and (= (:shape-ref shape-inst) (:shape-ref shape-main)) + (not components-v2)))) (defn main-instance? "Check if this shape is the root of the main instance of some @@ -174,20 +183,47 @@ (and (= shape-id (:main-instance-id component)) (= page-id (:main-instance-page component)))) +(defn set-touched-group + [touched group] + (when group + (conj (or touched #{}) group))) + +(defn touched-group? + [shape group] + ((or (:touched shape) #{}) group)) + (defn build-swap-slot-group "Convert a swap-slot into a :touched group" [swap-slot] (when swap-slot (keyword (str "swap-slot-" swap-slot)))) +(defn swap-slot? + [group] + (str/starts-with? (name group) "swap-slot-")) + +(defn normal-touched-groups + "Gets all touched groups that are not swap slots." + [shape] + (into #{} (remove swap-slot? (:touched shape)))) + +(defn group->swap-slot + [group] + (uuid/uuid (subs (name group) 10))) + (defn get-swap-slot "If the shape has a :touched group in the form :swap-slot-, get the id." [shape] - (let [group (->> (:touched shape) - (map name) - (d/seek #(str/starts-with? % "swap-slot-")))] + (let [group (d/seek swap-slot? (:touched shape))] (when group - (uuid/uuid (subs group 10))))) + (group->swap-slot group)))) + +(defn set-swap-slot + "Add a touched group with a form :swap-slot-." + [shape swap-slot] + (cond-> shape + (some? swap-slot) + (update :touched set-touched-group (build-swap-slot-group swap-slot)))) (defn match-swap-slot? [shape-main shape-inst] @@ -197,6 +233,12 @@ (or (= slot-main slot-inst) (= (:id shape-main) slot-inst))))) +(defn remove-swap-slot + [shape] + (update shape :touched + (fn [touched] + (into #{} (remove #(str/starts-with? (name %) "swap-slot-") touched))))) + (defn get-component-root [component] (if (true? (:main-instance-id component)) @@ -221,7 +263,6 @@ :shape-ref :touched)) - (defn- extract-ids [shape] (if (map? shape) (let [current-id (:id shape) @@ -240,3 +281,26 @@ (distinct) (filter #(not (eq % (get comp1 %) (get comp2 %)))) set))) + +(defn allow-duplicate? + [objects shape] + + (let [parent (get objects (:parent-id shape))] + ;; We don't want to change the structure of component copies + (and (not (in-component-copy-not-head? shape)) + ;; Non instance, non copy. We allow + (or (not (instance-head? shape)) + (not (in-component-copy? parent)))))) + +(defn all-touched-groups + [] + (into #{} (vals sync-attrs))) + +(defn valid-touched-group? + [group] + (try + (or ((all-touched-groups) group) + (and (swap-slot? group) + (some? (group->swap-slot group)))) + (catch #?(:clj Throwable :cljs :default) _ + false))) \ No newline at end of file diff --git a/common/src/app/common/types/components_list.cljc b/common/src/app/common/types/components_list.cljc index 1eefe302162f00876fc7a75d89c6c672a1a68bcd..8165c2d2329568cb8fb994f89b607c1a1822fe23 100644 --- a/common/src/app/common/types/components_list.cljc +++ b/common/src/app/common/types/components_list.cljc @@ -48,7 +48,7 @@ (wrap-object-fn))))))) (defn mod-component - [file-data {:keys [id name path main-instance-id main-instance-page objects annotation]}] + [file-data {:keys [id name path main-instance-id main-instance-page objects annotation modified-at]}] (let [wrap-objects-fn cfeat/*wrap-with-objects-map-fn*] (d/update-in-when file-data [:components id] (fn [component] @@ -69,6 +69,9 @@ (some? objects) (assoc :objects objects) + (some? modified-at) + (assoc :modified-at modified-at) + (some? annotation) (assoc :annotation annotation) @@ -76,7 +79,7 @@ (dissoc :annotation)) diff (set/difference (ctk/diff-components component new-comp) - #{:annotation})] ;; The set of properties that doesn't mark a component as touched + #{:annotation :modified-at})] ;; The set of properties that doesn't mark a component as touched (if (empty? diff) new-comp diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index ddeeea7342cf9532a720e45399c6d5df15368da4..9cecfac38968726bd126b9e8983a235ae07a32d8 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -15,6 +15,7 @@ [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.pages-list :as ctpl] + [app.common.types.plugins :as ctpg] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid])) @@ -26,7 +27,7 @@ (def valid-container-types #{:page :component}) -(sm/define! ::container +(sm/register! ::container [:map [:id ::sm/uuid] [:type {:optional true} @@ -35,7 +36,8 @@ [:path {:optional true} [:maybe :string]] [:modified-at {:optional true} ::sm/inst] [:objects {:optional true} - [:map-of {:gen/max 10} ::sm/uuid :map]]]) + [:map-of {:gen/max 10} ::sm/uuid :map]] + [:plugin-data {:optional true} ::ctpg/plugin-data]]) (def check-container! (sm/check-fn ::container)) @@ -90,6 +92,10 @@ [container shape-id f] (update-in container [:objects shape-id] f)) +(defn get-container-root + [container] + (d/seek #(or (nil? (:parent-id %)) (= (:parent-id %) uuid/zero)) (shapes-seq container))) + (defn get-direct-children [container shape] (map #(get-shape container %) (:shapes shape))) @@ -107,7 +113,7 @@ (get-children-rec [] id))) (defn get-component-shape - "Get the parent top shape linked to a component for this shape, if any" + "Get the parent top shape linked to a component main for this shape, if any" ([objects shape] (get-component-shape objects shape nil)) ([objects shape {:keys [allow-main?] :or {allow-main? false} :as options}] (let [parent (get objects (:parent-id shape))] @@ -150,6 +156,17 @@ :else (get-head-shape objects (get objects (:parent-id shape)) options)))) +(defn get-child-heads + "Get all recursive childs that are heads (when a head is found, do not + continue down looking for subsequent nested heads)." + [objects shape-id] + (let [shape (get objects shape-id)] + (if (nil? shape) + [] + (if (ctk/instance-head? shape) + [shape] + (mapcat #(get-child-heads objects %) (:shapes shape)))))) + (defn get-parent-heads "Get all component heads that are ancestors of the shape, in top-down order (include self if it's also a head)." @@ -166,6 +183,20 @@ (filter #(and (ctk/instance-head? %) (ctk/in-component-copy? %))) (reverse))) +(defn get-nesting-level-delta + "Get how many levels a shape will 'go up' if moved under the new parent." + [objects shape new-parent] + (let [orig-heads (->> (get-parent-copy-heads objects shape) + (remove #(= (:id %) (:id shape)))) + dest-heads (get-parent-copy-heads objects new-parent) + + ;; Calculate how many parent heads share in common the original + ;; shape and the new parent. + pairs (map vector orig-heads dest-heads) + common-count (count (take-while (fn [a b] (= a b)) pairs))] + + (- (count orig-heads) common-count))) + (defn get-instance-root "Get the parent shape at the top of the component instance (main or copy)." [objects shape] @@ -182,20 +213,33 @@ :else (get-instance-root objects (get objects (:parent-id shape))))) +(defn find-component-main + "If the shape is a component main instance or is inside one, return that instance" + ([objects shape] + (find-component-main objects shape true)) + ([objects shape only-direct-child?] + (cond + (or (nil? shape) (cfh/root? shape)) + nil + (nil? (:parent-id shape)) ; This occurs in the root of components v1 + shape + (ctk/main-instance? shape) + shape + (and only-direct-child? ;; If we are asking only for direct childs of a component-main, + (ctk/instance-head? shape)) ;; stop when we found a instance-head that isn't main-instance + nil + (and (not only-direct-child?) + (ctk/instance-root? shape)) + nil + :else + (find-component-main objects (get objects (:parent-id shape)))))) + (defn inside-component-main? "Check if the shape is a component main instance or is inside one." - [objects shape] - (cond - (or (nil? shape) (cfh/root? shape)) - false - (nil? (:parent-id shape)) ; This occurs in the root of components v1 - true - (ctk/main-instance? shape) - true - (ctk/instance-head? shape) - false - :else - (inside-component-main? objects (get objects (:parent-id shape))))) + ([objects shape] + (inside-component-main? objects shape true)) + ([objects shape only-direct-child?] + (some? (find-component-main objects shape only-direct-child?)))) (defn in-any-component? "Check if the shape is part of any component (main or copy), wether it's @@ -282,6 +326,19 @@ [new-root-shape (map remap-frame-id new-shapes) updated-shapes])) +(defn remove-swap-keep-attrs + "Remove flex children properties except the fit-content for flex layouts. These are properties + that we don't have to propagate to copies but will be respected when swapping components" + [shape] + (let [layout-item-h-sizing (when (and (ctl/flex-layout? shape) (ctl/auto-width? shape)) :auto) + layout-item-v-sizing (when (and (ctl/flex-layout? shape) (ctl/auto-height? shape)) :auto)] + (-> shape + (d/without-keys ctk/swap-keep-attrs) + (cond-> (some? layout-item-h-sizing) + (assoc :layout-item-h-sizing layout-item-h-sizing)) + (cond-> (some? layout-item-v-sizing) + (assoc :layout-item-v-sizing layout-item-v-sizing))))) + (defn make-component-instance "Generate a new instance of the component inside the given container. @@ -302,7 +359,7 @@ (-> (get-shape component-page (:main-instance-id component)) (assoc :parent-id nil) ;; On v2 we force parent-id to nil in order to behave like v1 (assoc :frame-id uuid/zero) - (d/without-keys ctk/swap-keep-attrs)) + (remove-swap-keep-attrs)) (get-shape component (:id component))) orig-pos (gpt/point (:x component-shape) (:y component-shape)) @@ -320,8 +377,11 @@ {:skip-components? true :bottom-frames? true ;; We must avoid that destiny frame is inside the component frame - :validator #(nil? (get component-children (:id %)))})) - + :validator #(and + ;; We must avoid that destiny frame is inside the component frame + (nil? (get component-children (:id %))) + ;; We must avoid that destiny frame is inside a copy + (not (ctk/in-component-copy? %)))})) frame (get-shape container frame-id) component-frame (get-component-shape objects frame {:allow-main? true}) @@ -440,7 +500,7 @@ ; original component doesn't exist or is deleted. So for this function purposes, they ; are removed from the list remove? (fn [shape] - (let [component (get-in libraries [(:component-file shape) :data :components (:component-id shape)])] + (let [component (get-in libraries [(:component-file shape) :data :components (:component-id shape)])] (and component (not (:deleted component))))) selected-components (cond->> (mapcat collect-main-shapes children objects) @@ -455,6 +515,7 @@ (or ;;We don't want to change the structure of component copies (ctk/in-component-copy? parent) + (has-any-copy-parent? objects parent) ;; If we are moving something containing a main instance the container can't be part of a component (neither main nor copy) (and selected-main-instance? parent-in-component?) ;; Avoid placing a shape as a direct or indirect child of itself, @@ -475,3 +536,64 @@ (if (or no-changes? (not (invalid-structure-for-component? objects parent children pasting? libraries))) [parent-id (get-frame parent-id)] (recur (:parent-id parent) objects children pasting? libraries)))))) + +;; --- SHAPE UPDATE + +(defn set-shape-attr + "Assign attribute to shape with touched logic. + + The returned shape will contain a metadata associated with it + indicating if shape is touched or not." + [shape attr val & {:keys [ignore-touched ignore-geometry]}] + (let [group (get ctk/sync-attrs attr) + shape-val (get shape attr) + + ignore? + (or ignore-touched + ;; position-data is a derived attribute + (= attr :position-data)) + + is-geometry? + (and (or (= group :geometry-group) ;; never triggers touched by itself + (and (= group :content-group) + (= (:type shape) :path))) + ;; :content in paths are also considered geometric + (not (#{:width :height} attr))) + + ;; TODO: the check of :width and :height probably may be + ;; removed after the check added in + ;; data/workspace/modifiers/check-delta function. Better check + ;; it and test toroughly when activating components-v2 mode. + in-copy? + (ctk/in-component-copy? shape) + + ;; For geometric attributes, there are cases in that the value changes + ;; slightly (e.g. when rounding to pixel, or when recalculating text + ;; positions in different zoom levels). To take this into account, we + ;; ignore geometric changes smaller than 1 pixel. + equal? + (if is-geometry? + (gsh/close-attrs? attr val shape-val 1) + (gsh/close-attrs? attr val shape-val)) + + touched? + (and group (not equal?) (not (and ignore-geometry is-geometry?)))] + + (cond-> shape + ;; Depending on the origin of the attribute change, we need or not to + ;; set the "touched" flag for the group the attribute belongs to. + ;; In some cases we need to ignore touched only if the attribute is + ;; geometric (position, width or transformation). + (and in-copy? group (not ignore?) (not equal?) + (not (and ignore-geometry is-geometry?))) + (-> (update :touched ctk/set-touched-group group) + (dissoc :remote-synced)) + + (nil? val) + (dissoc attr) + + (some? val) + (assoc attr val) + + :always + (vary-meta assoc ::touched touched?)))) diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index 676db6dc7b7efcd905dbf472b87fb13e4848229a..b0ce412d1acd012610559444c9d537a7a8cd4460 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.features :as cfeat] + [app.common.files.defaults :refer [version]] [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] @@ -23,6 +24,7 @@ [app.common.types.container :as ctn] [app.common.types.page :as ctp] [app.common.types.pages-list :as ctpl] + [app.common.types.plugins :as ctpg] [app.common.types.shape-tree :as ctst] [app.common.types.typographies-list :as ctyl] [app.common.types.typography :as cty] @@ -33,7 +35,7 @@ ;; SCHEMA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(sm/define! ::media-object +(sm/register! ::media-object [:map {:title "FileMediaObject"} [:id ::sm/uuid] [:name :string] @@ -42,7 +44,7 @@ [:mtype :string] [:path {:optional true} [:maybe :string]]]) -(sm/define! ::data +(sm/register! ::data [:map {:title "FileData"} [:pages [:vector ::sm/uuid]] [:pages-index @@ -56,7 +58,8 @@ [:typographies {:optional true} [:map-of {:gen/max 2} ::sm/uuid ::cty/typography]] [:media {:optional true} - [:map-of {:gen/max 5} ::sm/uuid ::media-object]]]) + [:map-of {:gen/max 5} ::sm/uuid ::media-object]] + [:plugin-data {:optional true} ::ctpg/plugin-data]]) (def check-file-data! (sm/check-fn ::data)) @@ -78,7 +81,7 @@ ([file-id page-id] (let [page (when (some? page-id) - (ctp/make-empty-page page-id "Page 1"))] + (ctp/make-empty-page {:id page-id :name "Page 1"}))] (cond-> (assoc empty-file-data :id file-id) (some? page-id) @@ -87,6 +90,34 @@ (contains? cfeat/*current* "components/v2") (assoc-in [:options :components-v2] true))))) +(defn make-file + [{:keys [id project-id name revn is-shared features + ignore-sync-until modified-at deleted-at + create-page page-id] + :or {is-shared false revn 0 create-page true}}] + + (let [id (or id (uuid/next)) + + data (if create-page + (if page-id + (make-file-data id page-id) + (make-file-data id)) + (make-file-data id nil)) + + file {:id id + :project-id project-id + :name name + :revn revn + :is-shared is-shared + :version version + :data data + :features features + :ignore-sync-until ignore-sync-until + :modified-at modified-at + :deleted-at deleted-at}] + + (d/without-nils file))) + ;; Helpers (defn file-data @@ -147,7 +178,7 @@ (defn get-component-container "Retrieve the container that holds the component shapes (the page in components-v2 - or the component itself in v1)" + or the component itself in v1 or deleted component)." [file-data component] (let [components-v2 (dm/get-in file-data [:options :components-v2])] (if (and components-v2 (not (:deleted component))) @@ -182,11 +213,12 @@ :data file-data} :container (ctn/make-container component-page :page)}))))) - (cond-> (dm/get-in component [:objects shape-id]) - with-context? - (with-meta {:file {:id (:id file-data) - :data file-data} - :container (ctn/make-container component :component)}))))) + (let [shape (dm/get-in component [:objects shape-id])] + (cond-> shape + (and shape with-context?) + (with-meta {:file {:id (:id file-data) + :data file-data} + :container (ctn/make-container component :component)})))))) (defn get-ref-shape "Retrieve the shape in the component that is referenced by the instance shape." @@ -215,6 +247,15 @@ (some find-ref-shape-in-head (ctn/get-parent-heads (:objects container) shape)))) +(defn advance-shape-ref + "Get the shape-ref of the near main of the shape, recursively repeated as many times + as the given levels." + [file container libraries shape levels & {:keys [include-deleted?] :or {include-deleted? false}}] + (let [ref-shape (find-ref-shape file container libraries shape :include-deleted? include-deleted? :with-context? true)] + (if (or (nil? (:shape-ref ref-shape)) (not (pos? levels))) + (:id ref-shape) + (advance-shape-ref file (:container (meta ref-shape)) libraries ref-shape (dec levels) :include-deleted? include-deleted?)))) + (defn find-ref-component "Locate the nearest component in the local file or libraries that is referenced by the instance shape." @@ -303,7 +344,7 @@ (vals (:objects component))))) ;; Return true if the object is a component that exists on the file or its libraries (even a deleted one) -(defn is-known-component? +(defn is-main-of-known-component? [shape libraries] (let [main-instance? (ctk/main-instance? shape) component-id (:component-id shape) @@ -447,7 +488,7 @@ (gpt/point 0 0) (ctn/shapes-seq library-page))] [file-data (:id library-page) position]) - (let [library-page (ctp/make-empty-page (uuid/next) "Main components")] + (let [library-page (ctp/make-empty-page {:id (uuid/next) :name "Main components"})] [(ctpl/add-page file-data library-page) (:id library-page) (gpt/point 0 0)])))) (defn- absorb-components @@ -594,19 +635,24 @@ "Find all assets of a library that are used in the file, and move them to the file local library." [file-data library-data] - (let [used-components (find-asset-type-usages file-data library-data :component) - used-colors (find-asset-type-usages file-data library-data :color) - used-typographies (find-asset-type-usages file-data library-data :typography)] - - (cond-> file-data - (d/not-empty? used-components) - (absorb-components used-components library-data) - - (d/not-empty? used-colors) - (absorb-colors used-colors) - - (d/not-empty? used-typographies) - (absorb-typographies used-typographies)))) + (let [used-components (find-asset-type-usages file-data library-data :component) + file-data (cond-> file-data + (d/not-empty? used-components) + (absorb-components used-components library-data)) + ;; Note that absorbed components may also be using colors + ;; and typographies. This is the reason of doing this first + ;; and accumulating file data for the next ones. + + used-colors (find-asset-type-usages file-data library-data :color) + file-data (cond-> file-data + (d/not-empty? used-colors) + (absorb-colors used-colors)) + + used-typographies (find-asset-type-usages file-data library-data :typography) + file-data (cond-> file-data + (d/not-empty? used-typographies) + (absorb-typographies used-typographies))] + file-data)) ;; Debug helpers diff --git a/common/src/app/common/types/grid.cljc b/common/src/app/common/types/grid.cljc index 29e90af4c72009d0a2b0086514f97e30ea837efe..41e7d1c1530d1d1ae946da5a112841fbaee17b1b 100644 --- a/common/src/app/common/types/grid.cljc +++ b/common/src/app/common/types/grid.cljc @@ -13,47 +13,54 @@ ;; SCHEMA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(sm/def! ::grid-color +(def schema:grid-color [:map {:title "PageGridColor"} [:color ::ctc/rgb-color] [:opacity ::sm/safe-number]]) -(sm/def! ::column-params +(def schema:column-params [:map - [:color ::grid-color] + [:color schema:grid-color] [:type {:optional true} [::sm/one-of #{:stretch :left :center :right}]] [:size {:optional true} [:maybe ::sm/safe-number]] [:margin {:optional true} [:maybe ::sm/safe-number]] [:item-length {:optional true} [:maybe ::sm/safe-number]] [:gutter {:optional true} [:maybe ::sm/safe-number]]]) -(sm/def! ::square-params +(def schema:square-params [:map [:size {:optional true} [:maybe ::sm/safe-number]] - [:color ::grid-color]]) + [:color schema:grid-color]]) -(sm/def! ::grid - [:multi {:dispatch :type} +(def schema:grid + [:multi {:title "Grid" + :dispatch :type + :decode/json #(update % :type keyword)} [:column [:map [:type [:= :column]] [:display :boolean] - [:params ::column-params]]] + [:params schema:column-params]]] [:row [:map [:type [:= :row]] [:display :boolean] - [:params ::column-params]]] + [:params schema:column-params]]] [:square [:map [:type [:= :square]] [:display :boolean] - [:params ::square-params]]]]) + [:params schema:square-params]]]]) -(sm/def! ::saved-grids +(def schema:saved-grids [:map {:title "PageGrid"} [:square {:optional true} ::square-params] [:row {:optional true} ::column-params] [:column {:optional true} ::column-params]]) + +(sm/register! ::square-params schema:square-params) +(sm/register! ::column-params schema:column-params) +(sm/register! ::grid schema:grid) +(sm/register! ::saved-grids schema:saved-grids) diff --git a/common/src/app/common/types/modifiers.cljc b/common/src/app/common/types/modifiers.cljc index 7967d037929b9c395c75c1425af1df968739b25e..d0669a024b516e515c83329097d503d0df477a1c 100644 --- a/common/src/app/common/types/modifiers.cljc +++ b/common/src/app/common/types/modifiers.cljc @@ -737,30 +737,32 @@ (apply-scale-content [shape value] - (cond-> shape - (cfh/text-shape? shape) - (update-text-content scale-text-content value) + ;; Scale can only be positive + (let [value (mth/abs value)] + (cond-> shape + (cfh/text-shape? shape) + (update-text-content scale-text-content value) - :always - (gsc/update-corners-scale value) + :always + (gsc/update-corners-scale value) - (d/not-empty? (:strokes shape)) - (gss/update-strokes-width value) + (d/not-empty? (:strokes shape)) + (gss/update-strokes-width value) - (d/not-empty? (:shadow shape)) - (gse/update-shadows-scale value) + (d/not-empty? (:shadow shape)) + (gse/update-shadows-scale value) - (some? (:blur shape)) - (gse/update-blur-scale value) + (some? (:blur shape)) + (gse/update-blur-scale value) - (ctl/flex-layout? shape) - (ctl/update-flex-scale value) + (ctl/flex-layout? shape) + (ctl/update-flex-scale value) - (ctl/grid-layout? shape) - (ctl/update-grid-scale value) + (ctl/grid-layout? shape) + (ctl/update-grid-scale value) - :always - (ctl/update-flex-child value)))] + :always + (ctl/update-flex-child value))))] (let [remove-children (fn [shapes children-to-remove] diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc index 6c1d427dffc2689f5a34b1f88e795d32b3b72c6f..b49755115562dbc06fdb682b8994bdb461524fb0 100644 --- a/common/src/app/common/types/page.cljc +++ b/common/src/app/common/types/page.cljc @@ -10,6 +10,7 @@ [app.common.schema :as sm] [app.common.types.color :as-alias ctc] [app.common.types.grid :as ctg] + [app.common.types.plugins :as ctpg] [app.common.types.shape :as cts] [app.common.uuid :as uuid])) @@ -17,20 +18,20 @@ ;; SCHEMAS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(sm/define! ::flow - [:map {:title "PageFlow"} +(def schema:flow + [:map {:title "Flow"} [:id ::sm/uuid] [:name :string] [:starting-frame ::sm/uuid]]) -(sm/define! ::guide - [:map {:title "PageGuide"} +(def schema:guide + [:map {:title "Guide"} [:id ::sm/uuid] [:axis [::sm/one-of #{:x :y}]] [:position ::sm/safe-number] [:frame-id {:optional true} [:maybe ::sm/uuid]]]) -(sm/define! ::page +(def schema:page [:map {:title "FilePage"} [:id ::sm/uuid] [:name :string] @@ -41,9 +42,14 @@ [:background {:optional true} ::ctc/rgb-color] [:saved-grids {:optional true} ::ctg/saved-grids] [:flows {:optional true} - [:vector {:gen/max 2} ::flow]] + [:vector {:gen/max 2} schema:flow]] [:guides {:optional true} - [:map-of {:gen/max 2} ::sm/uuid ::guide]]]]]) + [:map-of {:gen/max 2} ::sm/uuid schema:guide]] + [:plugin-data {:optional true} ::ctpg/plugin-data]]]]) + +(sm/register! ::page schema:page) +(sm/register! ::guide schema:guide) +(sm/register! ::flow schema:flow) (def check-page-guide! (sm/check-fn ::guide)) @@ -69,10 +75,10 @@ :name "Root Frame"})}}) (defn make-empty-page - [id name] + [{:keys [id name]}] (-> empty-page-data - (assoc :id id) - (assoc :name name))) + (assoc :id (or id (uuid/next))) + (assoc :name (or name "Page 1")))) ;; --- Helpers for flow diff --git a/common/src/app/common/types/plugins.cljc b/common/src/app/common/types/plugins.cljc new file mode 100644 index 0000000000000000000000000000000000000000..494db5ba32ec845218b40c9e6ad451da747b6439 --- /dev/null +++ b/common/src/app/common/types/plugins.cljc @@ -0,0 +1,31 @@ +;; 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 app.common.types.plugins + (:require + [app.common.schema :as sm] + [app.common.schema.generators :as sg])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SCHEMAS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(def ^:private schema:string + [:schema {:gen/gen (sg/word-string)} :string]) + +(def ^:private schema:keyword + [:schema {:gen/gen (->> (sg/word-string) + (sg/fmap keyword))} + :keyword]) + +(def schema:plugin-data + [:map-of {:gen/max 5} + schema:keyword + [:map-of {:gen/max 5} + schema:string + schema:string]]) + +(sm/register! ::plugin-data schema:plugin-data) diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 2101f90f770fa8e1e3e1cd7ad3711f5363219217..b502c4c650a1e76489e51a4b42b973b27925fbdd 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -20,6 +20,7 @@ [app.common.transit :as t] [app.common.types.color :as ctc] [app.common.types.grid :as ctg] + [app.common.types.plugins :as ctpg] [app.common.types.shape.attrs :refer [default-color]] [app.common.types.shape.blur :as ctsb] [app.common.types.shape.export :as ctse] @@ -31,7 +32,7 @@ [app.common.uuid :as uuid] [clojure.set :as set])) -(cr/defrecord Shape [id name type x y width height rotation selrect points transform transform-inverse parent-id frame-id]) +(cr/defrecord Shape [id name type x y width height rotation selrect points transform transform-inverse parent-id frame-id flip-x flip-y]) (defn shape? [o] @@ -79,10 +80,21 @@ (def text-align-types #{"left" "right" "center" "justify"}) -(sm/define! ::points +(def bool-types + #{:union + :difference + :exclude + :intersection}) + +(def grow-types + #{:auto-width + :auto-height + :fixed}) + +(def schema:points [:vector {:gen/max 4 :gen/min 4} ::gpt/point]) -(sm/define! ::fill +(def schema:fill [:map {:title "Fill"} [:fill-color {:optional true} ::ctc/rgb-color] [:fill-opacity {:optional true} ::sm/safe-number] @@ -91,7 +103,9 @@ [:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]] [:fill-image {:optional true} ::ctc/image-color]]) -(sm/define! ::stroke +(sm/register! ::fill schema:fill) + +(def ^:private schema:stroke [:map {:title "Stroke"} [:stroke-color {:optional true} :string] [:stroke-color-ref-file {:optional true} ::sm/uuid] @@ -109,44 +123,43 @@ [:stroke-color-gradient {:optional true} ::ctc/gradient] [:stroke-image {:optional true} ::ctc/image-color]]) -(sm/define! ::shape-base-attrs +(sm/register! ::stroke schema:stroke) + +(def schema:shape-base-attrs [:map {:title "ShapeMinimalRecord"} [:id ::sm/uuid] [:name :string] [:type [::sm/one-of shape-types]] [:selrect ::grc/rect] - [:points ::points] + [:points schema:points] [:transform ::gmt/matrix] [:transform-inverse ::gmt/matrix] [:parent-id ::sm/uuid] [:frame-id ::sm/uuid]]) -(sm/define! ::shape-geom-attrs +(def schema:shape-geom-attrs [:map {:title "ShapeGeometryAttrs"} [:x ::sm/safe-number] [:y ::sm/safe-number] [:width ::sm/safe-number] [:height ::sm/safe-number]]) -(sm/define! ::shape-attrs +;; FIXME: rename to shape-generic-attrs +(def schema:shape-attrs [:map {:title "ShapeAttrs"} - [:name {:optional true} :string] [:component-id {:optional true} ::sm/uuid] [:component-file {:optional true} ::sm/uuid] [:component-root {:optional true} :boolean] [:main-instance {:optional true} :boolean] [:remote-synced {:optional true} :boolean] [:shape-ref {:optional true} ::sm/uuid] - [:selrect {:optional true} ::grc/rect] - [:points {:optional true} ::points] [:blocked {:optional true} :boolean] [:collapsed {:optional true} :boolean] [:locked {:optional true} :boolean] [:hidden {:optional true} :boolean] [:masked-group {:optional true} :boolean] [:fills {:optional true} - [:vector {:gen/max 2} ::fill]] - [:hide-fill-on-export {:optional true} :boolean] + [:vector {:gen/max 2} schema:fill]] [:proportion {:optional true} ::sm/safe-number] [:proportion-lock {:optional true} :boolean] [:constraints-h {:optional true} @@ -160,199 +173,191 @@ [:r2 {:optional true} ::sm/safe-number] [:r3 {:optional true} ::sm/safe-number] [:r4 {:optional true} ::sm/safe-number] - [:x {:optional true} [:maybe ::sm/safe-number]] - [:y {:optional true} [:maybe ::sm/safe-number]] - [:width {:optional true} [:maybe ::sm/safe-number]] - [:height {:optional true} [:maybe ::sm/safe-number]] [:opacity {:optional true} ::sm/safe-number] [:grids {:optional true} [:vector {:gen/max 2} ::ctg/grid]] [:exports {:optional true} [:vector {:gen/max 2} ::ctse/export]] [:strokes {:optional true} - [:vector {:gen/max 2} ::stroke]] - [:transform {:optional true} ::gmt/matrix] - [:transform-inverse {:optional true} ::gmt/matrix] - [:blend-mode {:optional true} [::sm/one-of blend-modes]] + [:vector {:gen/max 2} schema:stroke]] + [:blend-mode {:optional true} + [::sm/one-of blend-modes]] [:interactions {:optional true} [:vector {:gen/max 2} ::ctsi/interaction]] [:shadow {:optional true} [:vector {:gen/max 1} ::ctss/shadow]] [:blur {:optional true} ::ctsb/blur] [:grow-type {:optional true} - [::sm/one-of #{:auto-width :auto-height :fixed}]]]) + [::sm/one-of grow-types]] + [:plugin-data {:optional true} ::ctpg/plugin-data]]) -(sm/define! ::group-attrs +(def schema:group-attrs [:map {:title "GroupAttrs"} - [:type [:= :group]] [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]]) -(sm/define! ::frame-attrs +(def ^:private schema:frame-attrs [:map {:title "FrameAttrs"} - [:type [:= :frame]] [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] [:hide-fill-on-export {:optional true} :boolean] [:show-content {:optional true} :boolean] [:hide-in-viewer {:optional true} :boolean]]) -(sm/define! ::bool-attrs +(def ^:private schema:bool-attrs [:map {:title "BoolAttrs"} - [:type [:= :bool]] [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] + [:bool-type [::sm/one-of bool-types]] + [:bool-content ::ctsp/content]]) - ;; FIXME: improve this schema - [:bool-type :keyword] - - [:bool-content - [:vector {:gen/max 2} - [:map - [:command :keyword] - [:relative {:optional true} :boolean] - [:prev-pos {:optional true} ::gpt/point] - [:params {:optional true} - [:maybe - [:map-of {:gen/max 5} :keyword ::sm/safe-number]]]]]]]) - -(sm/define! ::rect-attrs - [:map {:title "RectAttrs"} - [:type [:= :rect]]]) +(def ^:private schema:rect-attrs + [:map {:title "RectAttrs"}]) -(sm/define! ::circle-attrs - [:map {:title "CircleAttrs"} - [:type [:= :circle]]]) +(def ^:private schema:circle-attrs + [:map {:title "CircleAttrs"}]) -(sm/define! ::svg-raw-attrs - [:map {:title "SvgRawAttrs"} - [:type [:= :svg-raw]]]) +(def ^:private schema:svg-raw-attrs + [:map {:title "SvgRawAttrs"}]) -(sm/define! ::image-attrs +(def schema:image-attrs [:map {:title "ImageAttrs"} - [:type [:= :image]] [:metadata [:map - [:width :int] - [:height :int] - [:mtype {:optional true} [:maybe :string]] + [:width {:gen/gen (sg/small-int :min 1)} :int] + [:height {:gen/gen (sg/small-int :min 1)} :int] + [:mtype {:optional true + :gen/gen (sg/elements ["image/jpeg" + "image/png"])} + [:maybe :string]] [:id ::sm/uuid]]]]) -(sm/define! ::path-attrs +(def ^:private schema:path-attrs [:map {:title "PathAttrs"} - [:type [:= :path]] [:content ::ctsp/content]]) -(sm/define! ::text-attrs +(def ^:private schema:text-attrs [:map {:title "TextAttrs"} - [:type [:= :text]] [:content {:optional true} [:maybe ::ctsx/content]]]) -(sm/define! ::shape-map - [:multi {:dispatch :type :title "Shape"} - [:group - [:and {:title "GroupShape"} - ::shape-base-attrs - ::shape-geom-attrs - ::shape-attrs - ::group-attrs - ::ctsl/layout-child-attrs]] - - [:frame - [:and {:title "FrameShape"} - ::shape-base-attrs - ::shape-geom-attrs - ::frame-attrs - ::ctsl/layout-attrs - ::ctsl/layout-child-attrs]] - - [:bool - [:and {:title "BoolShape"} - ::shape-base-attrs - ::shape-attrs - ::bool-attrs - ::ctsl/layout-child-attrs]] - - [:rect - [:and {:title "RectShape"} - ::shape-base-attrs - ::shape-geom-attrs - ::shape-attrs - ::rect-attrs - ::ctsl/layout-child-attrs]] - - [:circle - [:and {:title "CircleShape"} - ::shape-base-attrs - ::shape-geom-attrs - ::shape-attrs - ::circle-attrs - ::ctsl/layout-child-attrs]] - - [:image - [:and {:title "ImageShape"} - ::shape-base-attrs - ::shape-geom-attrs - ::shape-attrs - ::image-attrs - ::ctsl/layout-child-attrs]] - - [:svg-raw - [:and {:title "SvgRawShape"} - ::shape-base-attrs - ::shape-geom-attrs - ::shape-attrs - ::svg-raw-attrs - ::ctsl/layout-child-attrs]] - - [:path - [:and {:title "PathShape"} - ::shape-base-attrs - ::shape-attrs - ::path-attrs - ::ctsl/layout-child-attrs]] - - [:text - [:and {:title "TextShape"} - ::shape-base-attrs - ::shape-geom-attrs - ::shape-attrs - ::text-attrs - ::ctsl/layout-child-attrs]]]) - -(sm/define! ::shape - [:and - {:title "Shape" - :gen/gen (->> (sg/generator ::shape-base-attrs) - (sg/mcat (fn [{:keys [type] :as shape}] - (sg/let [attrs1 (sg/generator ::shape-attrs) - attrs2 (sg/generator ::shape-geom-attrs) - attrs3 (case type - :text (sg/generator ::text-attrs) - :path (sg/generator ::path-attrs) - :svg-raw (sg/generator ::svg-raw-attrs) - :image (sg/generator ::image-attrs) - :circle (sg/generator ::circle-attrs) - :rect (sg/generator ::rect-attrs) - :bool (sg/generator ::bool-attrs) - :group (sg/generator ::group-attrs) - :frame (sg/generator ::frame-attrs))] - (if (or (= type :path) - (= type :bool)) - (merge attrs1 shape attrs3) - (merge attrs1 shape attrs2 attrs3))))) - (sg/fmap map->Shape))} - ::shape-map - [:fn shape?]]) +(defn- decode-shape + [o] + (if (map? o) + (map->Shape o) + o)) + +(defn- shape-generator + "Get the shape generator." + [] + (->> (sg/generator schema:shape-base-attrs) + (sg/mcat (fn [{:keys [type] :as shape}] + (sg/let [attrs1 (sg/generator schema:shape-attrs) + attrs2 (sg/generator schema:shape-geom-attrs) + attrs3 (case type + :text (sg/generator schema:text-attrs) + :path (sg/generator schema:path-attrs) + :svg-raw (sg/generator schema:svg-raw-attrs) + :image (sg/generator schema:image-attrs) + :circle (sg/generator schema:circle-attrs) + :rect (sg/generator schema:rect-attrs) + :bool (sg/generator schema:bool-attrs) + :group (sg/generator schema:group-attrs) + :frame (sg/generator schema:frame-attrs))] + (if (or (= type :path) + (= type :bool)) + (merge attrs1 shape attrs3) + (merge attrs1 shape attrs2 attrs3))))) + (sg/fmap map->Shape))) + +(def schema:shape + [:and {:title "Shape" + :gen/gen (shape-generator) + :decode/json {:leave decode-shape}} + [:fn shape?] + [:multi {:dispatch :type + :decode/json (fn [shape] + (update shape :type keyword)) + :title "Shape"} + [:group + [:merge {:title "GroupShape"} + ::ctsl/layout-child-attrs + schema:group-attrs + schema:shape-attrs + schema:shape-geom-attrs + schema:shape-base-attrs]] + + [:frame + [:merge {:title "FrameShape"} + ::ctsl/layout-child-attrs + ::ctsl/layout-attrs + schema:frame-attrs + schema:shape-attrs + schema:shape-geom-attrs + schema:shape-base-attrs]] + + [:bool + [:merge {:title "BoolShape"} + ::ctsl/layout-child-attrs + schema:bool-attrs + schema:shape-attrs + schema:shape-base-attrs]] + + [:rect + [:merge {:title "RectShape"} + ::ctsl/layout-child-attrs + schema:rect-attrs + schema:shape-attrs + schema:shape-geom-attrs + schema:shape-base-attrs]] + + [:circle + [:merge {:title "CircleShape"} + ::ctsl/layout-child-attrs + schema:circle-attrs + schema:shape-attrs + schema:shape-geom-attrs + schema:shape-base-attrs]] + + [:image + [:merge {:title "ImageShape"} + ::ctsl/layout-child-attrs + schema:image-attrs + schema:shape-attrs + schema:shape-geom-attrs + schema:shape-base-attrs]] + + [:svg-raw + [:merge {:title "SvgRawShape"} + ::ctsl/layout-child-attrs + schema:svg-raw-attrs + schema:shape-attrs + schema:shape-geom-attrs + schema:shape-base-attrs]] + + [:path + [:merge {:title "PathShape"} + ::ctsl/layout-child-attrs + schema:path-attrs + schema:shape-attrs + schema:shape-base-attrs]] + + [:text + [:merge {:title "TextShape"} + ::ctsl/layout-child-attrs + schema:text-attrs + schema:shape-attrs + schema:shape-geom-attrs + schema:shape-base-attrs]]]]) + +(sm/register! ::shape schema:shape) (def check-shape-attrs! - (sm/check-fn ::shape-attrs)) + (sm/check-fn schema:shape-attrs)) (def check-shape! - (sm/check-fn ::shape)) + (sm/check-fn schema:shape)) (defn has-images? [{:keys [fills strokes]}] - (or - (some :fill-image fills) - (some :stroke-image strokes))) + (or (some :fill-image fills) + (some :stroke-image strokes))) ;; --- Initialization @@ -463,9 +468,14 @@ (defn setup-rect "Initializes the selrect and points for a shape." - [{:keys [selrect points] :as shape}] - (let [selrect (or selrect (gsh/shape->rect shape)) - points (or points (grc/rect->points selrect))] + [{:keys [selrect points transform] :as shape}] + (let [selrect (or selrect (gsh/shape->rect shape)) + center (grc/rect->center selrect) + transform (or transform (gmt/matrix)) + points (or points + (-> selrect + (grc/rect->points) + (gsh/transform-points center transform)))] (-> shape (assoc :selrect selrect) (assoc :points points)))) @@ -483,13 +493,13 @@ (defn- setup-image [{:keys [metadata] :as shape}] (-> shape - (assoc :proportion (/ (:width metadata) - (:height metadata))) + (assoc :proportion (float (/ (:width metadata) + (:height metadata)))) (assoc :proportion-lock true))) (defn setup-shape - "A function that initializes the geometric data of - the shape. The props must have :x :y :width :height." + "A function that initializes the geometric data of the shape. The props must + contain at least :x :y :width :height." [{:keys [type] :as props}] (let [shape (make-minimal-shape type) diff --git a/common/src/app/common/types/shape/blur.cljc b/common/src/app/common/types/shape/blur.cljc index 2c4ce5ab4b091a42fee7f6e8d767e574c73bf55d..796c0d1707da5be658cc9ac3809ed739064f8ed6 100644 --- a/common/src/app/common/types/shape/blur.cljc +++ b/common/src/app/common/types/shape/blur.cljc @@ -26,7 +26,7 @@ ;; SCHEMA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(sm/def! ::blur +(sm/register! ::blur [:map {:title "Blur"} [:id ::sm/uuid] [:type [:= :layer-blur]] diff --git a/common/src/app/common/types/shape/export.cljc b/common/src/app/common/types/shape/export.cljc index 6d7953a88ede167b9efa1803ab535cf4058f4f32..bd2bee0a519208d3e905239bde1f94efb8b68cda 100644 --- a/common/src/app/common/types/shape/export.cljc +++ b/common/src/app/common/types/shape/export.cljc @@ -8,8 +8,12 @@ (:require [app.common.schema :as sm])) -(sm/def! ::export +(def types #{:png :jpeg :svg :pdf}) + +(def schema:export [:map {:title "ShapeExport"} - [:type :keyword] + [:type [::sm/one-of types]] [:scale ::sm/safe-number] [:suffix :string]]) + +(sm/register! ::export schema:export) diff --git a/common/src/app/common/types/shape/interactions.cljc b/common/src/app/common/types/shape/interactions.cljc index 05724ebe88cb135bb2a4b04dcf1c7de71280b03f..29ef5902f3d2beec6f76ad88343f7629ce707503 100644 --- a/common/src/app/common/types/shape/interactions.cljc +++ b/common/src/app/common/types/shape/interactions.cljc @@ -11,7 +11,8 @@ [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.geom.shapes.bounds :as gsb] - [app.common.schema :as sm])) + [app.common.schema :as sm] + [app.common.schema.generators :as sg])) ;; WARNING: options are not deleted when changing event or action type, so it can be ;; restored if the user changes it back later. @@ -71,81 +72,116 @@ (def animation-types #{:dissolve :slide :push}) -(sm/define! ::animation - [:multi {:dispatch :animation-type :title "Animation"} - [:dissolve - [:map {:title "AnimationDisolve"} - [:animation-type [:= :dissolve]] - [:duration ::sm/safe-int] - [:easing [::sm/one-of easing-types]]]] - [:slide - [:map {:title "AnimationSlide"} - [:animation-type [:= :slide]] - [:duration ::sm/safe-int] - [:easing [::sm/one-of easing-types]] - [:way [::sm/one-of way-types]] - [:direction [::sm/one-of direction-types]] - [:offset-effect :boolean]]] - [:push - [:map {:title "AnimationPush"} - [:animation-type [:= :push]] - [:duration ::sm/safe-int] - [:easing [::sm/one-of easing-types]] - [:direction [::sm/one-of direction-types]]]]]) +(def schema:dissolve-animation + [:map {:title "AnimationDisolve"} + [:animation-type [:= :dissolve]] + [:duration ::sm/safe-int] + [:easing [::sm/one-of easing-types]]]) + +(def schema:slide-animation + [:map {:title "AnimationSlide"} + [:animation-type [:= :slide]] + [:duration ::sm/safe-int] + [:easing [::sm/one-of easing-types]] + [:way [::sm/one-of way-types]] + [:direction [::sm/one-of direction-types]] + [:offset-effect :boolean]]) + +(def schema:push-animation + [:map {:title "PushAnimation"} + [:animation-type [:= :push]] + [:duration ::sm/safe-int] + [:easing [::sm/one-of easing-types]] + [:direction [::sm/one-of direction-types]]]) + +(def schema:animation + [:multi {:dispatch :animation-type + :title "Animation" + :gen/gen (sg/one-of (sg/generator schema:dissolve-animation) + (sg/generator schema:slide-animation) + (sg/generator schema:push-animation)) + :decode/json #(update % :animation-type keyword)} + [:dissolve schema:dissolve-animation] + [:slide schema:slide-animation] + [:push schema:push-animation]]) + +(sm/register! ::animation schema:animation) (def check-animation! - (sm/check-fn ::animation)) - -(sm/define! ::interaction - [:multi {:dispatch :action-type} - [:navigate - [:map - [:action-type [:= :navigate]] - [:event-type [::sm/one-of event-types]] - [:destination {:optional true} [:maybe ::sm/uuid]] - [:preserve-scroll {:optional true} :boolean] - [:animation {:optional true} ::animation]]] - [:open-overlay - [:map - [:action-type [:= :open-overlay]] - [:event-type [::sm/one-of event-types]] - [:overlay-position ::gpt/point] - [:overlay-pos-type [::sm/one-of overlay-positioning-types]] - [:destination {:optional true} [:maybe ::sm/uuid]] - [:close-click-outside {:optional true} :boolean] - [:background-overlay {:optional true} :boolean] - [:animation {:optional true} ::animation] - [:position-relative-to {:optional true} [:maybe ::sm/uuid]]]] - [:toggle-overlay - [:map - [:action-type [:= :toggle-overlay]] - [:event-type [::sm/one-of event-types]] - [:overlay-position ::gpt/point] - [:overlay-pos-type [::sm/one-of overlay-positioning-types]] - [:destination {:optional true} [:maybe ::sm/uuid]] - [:close-click-outside {:optional true} :boolean] - [:background-overlay {:optional true} :boolean] - [:animation {:optional true} ::animation] - [:position-relative-to {:optional true} [:maybe ::sm/uuid]]]] - [:close-overlay - [:map - [:action-type [:= :close-overlay]] - [:event-type [::sm/one-of event-types]] - [:destination {:optional true} [:maybe ::sm/uuid]] - [:animation {:optional true} ::animation] - [:position-relative-to {:optional true} [:maybe ::sm/uuid]]]] - [:prev-screen - [:map - [:action-type [:= :prev-screen]] - [:event-type [::sm/one-of event-types]]]] - [:open-url - [:map - [:action-type [:= :open-url]] - [:event-type [::sm/one-of event-types]] - [:url :string]]]]) + (sm/check-fn schema:animation)) + +(def schema:navigate-interaction + [:map + [:action-type [:= :navigate]] + [:event-type [::sm/one-of event-types]] + [:destination {:optional true} [:maybe ::sm/uuid]] + [:preserve-scroll {:optional true} :boolean] + [:animation {:optional true} ::animation]]) + +(def schema:open-overlay-interaction + [:map + [:action-type [:= :open-overlay]] + [:event-type [::sm/one-of event-types]] + [:overlay-position ::gpt/point] + [:overlay-pos-type [::sm/one-of overlay-positioning-types]] + [:destination {:optional true} [:maybe ::sm/uuid]] + [:close-click-outside {:optional true} :boolean] + [:background-overlay {:optional true} :boolean] + [:animation {:optional true} ::animation] + [:position-relative-to {:optional true} [:maybe ::sm/uuid]]]) + +(def schema:toggle-overlay-interaction + [:map + [:action-type [:= :toggle-overlay]] + [:event-type [::sm/one-of event-types]] + [:overlay-position ::gpt/point] + [:overlay-pos-type [::sm/one-of overlay-positioning-types]] + [:destination {:optional true} [:maybe ::sm/uuid]] + [:close-click-outside {:optional true} :boolean] + [:background-overlay {:optional true} :boolean] + [:animation {:optional true} ::animation] + [:position-relative-to {:optional true} [:maybe ::sm/uuid]]]) + +(def schema:close-overlay-interaction + [:map + [:action-type [:= :close-overlay]] + [:event-type [::sm/one-of event-types]] + [:destination {:optional true} [:maybe ::sm/uuid]] + [:animation {:optional true} ::animation] + [:position-relative-to {:optional true} [:maybe ::sm/uuid]]]) + +(def schema:prev-scren-interaction + [:map + [:action-type [:= :prev-screen]] + [:event-type [::sm/one-of event-types]]]) + +(def schema:open-url-interaction + [:map + [:action-type [:= :open-url]] + [:event-type [::sm/one-of event-types]] + [:url :string]]) + +(def schema:interaction + [:multi {:dispatch :action-type + :title "Interaction" + :gen/gen (sg/one-of (sg/generator schema:navigate-interaction) + (sg/generator schema:open-overlay-interaction) + (sg/generator schema:close-overlay-interaction) + (sg/generator schema:toggle-overlay-interaction) + (sg/generator schema:prev-scren-interaction) + (sg/generator schema:open-url-interaction)) + :decode/json #(update % :action-type keyword)} + [:navigate schema:navigate-interaction] + [:open-overlay schema:open-overlay-interaction] + [:toggle-overlay schema:toggle-overlay-interaction] + [:close-overlay schema:close-overlay-interaction] + [:prev-screen schema:prev-scren-interaction] + [:open-url schema:open-url-interaction]]) + +(sm/register! ::interaction schema:interaction) (def check-interaction! - (sm/check-fn ::interaction)) + (sm/check-fn schema:interaction)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; HELPERS diff --git a/common/src/app/common/types/shape/layout.cljc b/common/src/app/common/types/shape/layout.cljc index c395450412fb52882209db9c334ce7f7d76108c1..9a71931cc0e2d60fece7d0dbe775b548db376d72 100644 --- a/common/src/app/common/types/shape/layout.cljc +++ b/common/src/app/common/types/shape/layout.cljc @@ -48,8 +48,7 @@ #{:flex :grid}) (def flex-direction-types - ;;TODO remove reverse-column and reverse-row after script - #{:row :reverse-row :row-reverse :column :reverse-column :column-reverse}) + #{:row :row-reverse :column :column-reverse}) (def grid-direction-types #{:row :column}) @@ -58,7 +57,7 @@ #{:simple :multiple}) (def wrap-types - #{:wrap :nowrap :no-wrap}) ;;TODO remove no-wrap after script + #{:wrap :nowrap}) (def padding-type #{:simple :multiple}) @@ -87,7 +86,7 @@ :layout-item-absolute :layout-item-z-index]) -(sm/def! ::layout-attrs +(sm/register! ::layout-attrs [:map {:title "LayoutAttrs"} [:layout {:optional true} [::sm/one-of layout-types]] [:layout-flex-dir {:optional true} [::sm/one-of flex-direction-types]] @@ -130,7 +129,7 @@ (def grid-cell-justify-self-types #{:auto :start :center :end :stretch}) -(sm/def! ::grid-cell +(sm/register! ::grid-cell [:map {:title "GridCell"} [:id ::sm/uuid] [:area-name {:optional true} :string] @@ -144,7 +143,7 @@ [:shapes [:vector {:gen/max 1} ::sm/uuid]]]) -(sm/def! ::grid-track +(sm/register! ::grid-track [:map {:title "GridTrack"} [:type [::sm/one-of grid-track-types]] [:value {:optional true} [:maybe ::sm/safe-number]]]) @@ -166,7 +165,7 @@ (def item-align-self-types #{:start :end :center :stretch}) -(sm/def! ::layout-child-attrs +(sm/register! ::layout-child-attrs [:map {:title "LayoutChildAttrs"} [:layout-item-margin-type {:optional true} [::sm/one-of item-margin-types]] [:layout-item-margin {:optional true} @@ -192,7 +191,7 @@ (def valid-layouts #{:flex :grid}) -(sm/def! ::layout +(sm/register! ::layout [::sm/one-of valid-layouts]) (defn flex-layout? @@ -495,9 +494,11 @@ (= layout-align-items :stretch)) (defn reverse? - [{:keys [layout-flex-dir]}] - (or (= :row-reverse layout-flex-dir) - (= :column-reverse layout-flex-dir))) + ([objects id] + (reverse? (get objects id))) + ([{:keys [layout-flex-dir]}] + (or (= :row-reverse layout-flex-dir) + (= :column-reverse layout-flex-dir)))) (defn space-between? [{:keys [layout-justify-content]}] @@ -543,19 +544,21 @@ (or (:layout-item-z-index shape) 0))) (defn- comparator-layout-z-index - [[idx-a child-a] [idx-b child-b]] + [reverse? [idx-a child-a] [idx-b child-b]] (cond (> (layout-z-index child-a) (layout-z-index child-b)) 1 (< (layout-z-index child-a) (layout-z-index child-b)) -1 + (and (< idx-a idx-b) reverse?) -1 + (and (> idx-a idx-b) reverse?) 1 (< idx-a idx-b) 1 (> idx-a idx-b) -1 :else 0)) (defn sort-layout-children-z-index - [children] + [children reverse?] (->> children (d/enumerate) - (sort comparator-layout-z-index) + (sort (partial comparator-layout-z-index reverse?)) (mapv second))) (defn change-h-sizing? @@ -600,18 +603,23 @@ (defn remove-layout-item-data [shape] - (dissoc shape - :layout-item-margin - :layout-item-margin-type - :layout-item-h-sizing - :layout-item-v-sizing - :layout-item-max-h - :layout-item-min-h - :layout-item-max-w - :layout-item-min-w - :layout-item-align-self - :layout-item-absolute - :layout-item-z-index)) + (-> shape + (dissoc :layout-item-margin + :layout-item-margin-type + :layout-item-max-h + :layout-item-min-h + :layout-item-max-w + :layout-item-min-w + :layout-item-align-self + :layout-item-absolute + :layout-item-z-index) + (cond-> (or (not (any-layout? shape)) + (= :fill (:layout-item-h-sizing shape))) + (dissoc :layout-item-h-sizing) + + (or (not (any-layout? shape)) + (= :fill (:layout-item-v-sizing shape))) + (dissoc :layout-item-v-sizing)))) (defn update-flex-scale [shape scale] @@ -1277,6 +1285,21 @@ (let [cells+index (d/enumerate cells)] (d/seek #(in-cell? (second %) row column) cells+index))) +(defn free-cell-shapes + "Removes the shape-ids from the cells previously assigned." + [parent shape-ids] + (let [shape-ids (set shape-ids)] + (letfn [(free-cells + [cells] + (reduce-kv + (fn [m k v] + (if (some shape-ids (:shapes v)) + (assoc-in m [k :shapes] []) + m)) + cells + cells))] + (update parent :layout-grid-cells free-cells)))) + (defn push-into-cell "Push the shapes into the row/column cell and moves the rest" [parent shape-ids row column] @@ -1291,13 +1314,17 @@ ;; Move shift the `shapes` attribute between cells (->> (range start-index (inc to-index)) (map vector shape-ids) - (reduce (fn [[parent cells] [shape-id idx]] - (let [[parent cells] (free-cell-push parent cells idx)] - [(update-in parent [:layout-grid-cells (get-in cells [idx :id])] - assoc :position :manual - :shapes [shape-id]) - cells])) - [parent cells]) + (reduce + (fn [[parent cells] [shape-id idx]] + ;; If the shape to put in a cell is the same that is already in the cell we do nothing + (if (= shape-id (get-in parent [:layout-grid-cells (get-in cells [idx :id]) :shapes 0])) + [parent cells] + (let [[parent cells] (free-cell-push parent cells idx)] + [(update-in parent [:layout-grid-cells (get-in cells [idx :id])] + assoc :position :manual + :shapes [shape-id]) + cells]))) + [parent cells]) (first))) parent))) @@ -1446,13 +1473,15 @@ (push-into-cell children row column)) (assign-cells objects)))) +(defn get-cell-by-index + [parent to-index] + (let [cells (get-cells parent {:sort? true :remove-empty? true}) + to-index (- (count cells) to-index)] + (nth cells to-index nil))) + (defn add-children-to-index [parent ids objects to-index] - (let [ids (into (d/ordered-set) ids) - cells (get-cells parent {:sort? true :remove-empty? true}) - to-index (- (count cells) to-index) - target-cell (nth cells to-index nil)] - + (let [target-cell (get-cell-by-index parent to-index)] (cond-> parent (some? target-cell) (add-children-to-cell ids objects [(:row target-cell) (:column target-cell)])))) @@ -1593,13 +1622,17 @@ (defn remap-grid-cells "Remaps the shapes ids inside the cells" [shape ids-map] - (let [do-remap-cells + (let [remap-shape + (fn [id] + (get ids-map id id)) + + remap-cell (fn [cell] (-> cell - (update :shapes #(into [] (keep ids-map) %)))) + (update :shapes #(into [] (keep remap-shape) %)))) shape (-> shape - (update :layout-grid-cells update-vals do-remap-cells))] + (update :layout-grid-cells update-vals remap-cell))] shape)) (defn merge-cells diff --git a/common/src/app/common/types/shape/path.cljc b/common/src/app/common/types/shape/path.cljc index d633bb85c6ab5c5c5a78a186c66590957329dbea..1fd33bd45c9a1611299794ec8b293fce5324a008 100644 --- a/common/src/app/common/types/shape/path.cljc +++ b/common/src/app/common/types/shape/path.cljc @@ -8,40 +8,49 @@ (:require [app.common.schema :as sm])) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; SCHEMA -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(sm/define! ::segment - [:multi {:title "PathSegment" :dispatch :command} - [:line-to - [:map - [:command [:= :line-to]] - [:params - [:map - [:x ::sm/safe-number] - [:y ::sm/safe-number]]]]] - [:close-path +(def schema:line-to-segment + [:map + [:command [:= :line-to]] + [:params [:map - [:command [:= :close-path]]]] - [:move-to + [:x ::sm/safe-number] + [:y ::sm/safe-number]]]]) + +(def schema:close-path-segment + [:map + [:command [:= :close-path]]]) + +(def schema:move-to-segment + [:map + [:command [:= :move-to]] + [:params [:map - [:command [:= :move-to]] - [:params - [:map - [:x ::sm/safe-number] - [:y ::sm/safe-number]]]]] - [:curve-to + [:x ::sm/safe-number] + [:y ::sm/safe-number]]]]) + +(def schema:curve-to-segment + [:map + [:command [:= :curve-to]] + [:params [:map - [:command [:= :curve-to]] - [:params - [:map - [:x ::sm/safe-number] - [:y ::sm/safe-number] - [:c1x ::sm/safe-number] - [:c1y ::sm/safe-number] - [:c2x ::sm/safe-number] - [:c2y ::sm/safe-number]]]]]]) - -(sm/define! ::content - [:vector ::segment]) + [:x ::sm/safe-number] + [:y ::sm/safe-number] + [:c1x ::sm/safe-number] + [:c1y ::sm/safe-number] + [:c2x ::sm/safe-number] + [:c2y ::sm/safe-number]]]]) + +(def schema:path-segment + [:multi {:title "PathSegment" + :dispatch :command + :decode/json #(update % :command keyword)} + [:line-to schema:line-to-segment] + [:close-path schema:close-path-segment] + [:move-to schema:move-to-segment] + [:curve-to schema:curve-to-segment]]) + +(def schema:path-content + [:vector schema:path-segment]) + +(sm/register! ::segment schema:path-segment) +(sm/register! ::content schema:path-content) diff --git a/common/src/app/common/types/shape/shadow.cljc b/common/src/app/common/types/shape/shadow.cljc index cc2fd81c3cff24b6048010283115bb10bf99bcc7..0c33898931ebbe0d3f10e3cc5583b54b6b3d04f5 100644 --- a/common/src/app/common/types/shape/shadow.cljc +++ b/common/src/app/common/types/shape/shadow.cljc @@ -7,17 +7,23 @@ (ns app.common.types.shape.shadow (:require [app.common.schema :as sm] + [app.common.schema.generators :as sg] [app.common.types.color :as ctc])) (def styles #{:drop-shadow :inner-shadow}) -(sm/def! ::shadow +(def schema:shadow [:map {:title "Shadow"} [:id [:maybe ::sm/uuid]] - [:style [::sm/one-of styles]] + [:style + [:and {:gen/gen (sg/elements styles)} + :keyword + [::sm/one-of styles]]] [:offset-x ::sm/safe-number] [:offset-y ::sm/safe-number] [:blur ::sm/safe-number] [:spread ::sm/safe-number] [:hidden :boolean] [:color ::ctc/color]]) + +(sm/register! ::shadow schema:shadow) diff --git a/common/src/app/common/types/shape/text.cljc b/common/src/app/common/types/shape/text.cljc index dff8759561b858879ea8f240b3ec01a7a57e79d8..99d3a55b51d962afacddb72cc18f4a87b1962786 100644 --- a/common/src/app/common/types/shape/text.cljc +++ b/common/src/app/common/types/shape/text.cljc @@ -16,7 +16,7 @@ (def node-types #{"root" "paragraph-set" "paragraph"}) -(sm/def! ::content +(sm/register! ::content [:map [:type [:= "root"]] [:key {:optional true} :string] @@ -64,7 +64,7 @@ -(sm/def! ::position-data +(sm/register! ::position-data [:vector {:min 1 :gen/max 2} [:map [:x ::sm/safe-number] diff --git a/common/src/app/common/types/shape_tree.cljc b/common/src/app/common/types/shape_tree.cljc index 646c26641efec52f348d47bfdb5bce72cfd21b5b..c7a301ca4cbbc872c9ac8da50b484fc02a679374 100644 --- a/common/src/app/common/types/shape_tree.cljc +++ b/common/src/app/common/types/shape_tree.cljc @@ -61,6 +61,10 @@ (update container :objects update-objects parent-id))) +(defn parent-of? + [parent child] + (= (:id parent) (:parent-id child))) + (defn get-shape "Get a shape identified by id" [container id] @@ -461,16 +465,15 @@ row-size (+ (reduce d/max ##-Inf (map :height bounds)) gap) column-size (+ (reduce d/max ##-Inf (map :width bounds)) gap) - get-next (fn get-next - [counter] + get-next (fn get-next [counter] (let [row (quot counter grid-size) column (mod counter grid-size) position (->> (gpt/point (* column column-size) (* row row-size)) (gpt/add start-position))] - (lazy-seq - (cons position (get-next (inc counter))))))] - + (cons position + (lazy-seq + (get-next (inc counter))))))] (with-meta (get-next 0) {:width (* grid-size column-size) :height (* grid-size row-size)})))) diff --git a/common/src/app/common/types/typography.cljc b/common/src/app/common/types/typography.cljc index 4fe5c9565a09707e6940f750315f783661c056b4..0685950631e95b22bbfa61102d6a19a7d6b5fdbc 100644 --- a/common/src/app/common/types/typography.cljc +++ b/common/src/app/common/types/typography.cljc @@ -6,14 +6,17 @@ (ns app.common.types.typography (:require + [app.common.data :as d] [app.common.schema :as sm] - [app.common.text :as txt])) + [app.common.text :as txt] + [app.common.types.plugins :as ctpg] + [app.common.uuid :as uuid])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SCHEMA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(sm/def! ::typography +(sm/register! ::typography [:map {:title "Typography"} [:id ::sm/uuid] [:name :string] @@ -27,7 +30,8 @@ [:letter-spacing :string] [:text-transform :string] [:modified-at {:optional true} ::sm/inst] - [:path {:optional true} [:maybe :string]]]) + [:path {:optional true} [:maybe :string]] + [:plugin-data {:optional true} ::ctpg/plugin-data]]) (def check-typography! (sm/check-fn ::typography)) @@ -36,6 +40,23 @@ ;; HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn make-typography + [{:keys [id name path font-id font-family font-variant-id font-size + font-weight font-style line-height letter-spacing text-transform]}] + (-> {:id (or id (uuid/next)) + :name (or name "Typography 1") + :path path + :font-id (or font-id "sourcesanspro") + :font-family (or font-family "sourcesanspro") + :font-variant-id (or font-variant-id "regular") + :font-size (or font-size "14") + :font-weight (or font-weight "480") + :font-style (or font-style "normal") + :line-height (or line-height "1.2") + :letter-spacing (or letter-spacing "0") + :text-transform (or text-transform "none")} + (d/without-nils))) + (defn uses-library-typographies? "Check if the shape uses any typography in the given library." [shape library-id] diff --git a/common/src/app/common/version.cljc b/common/src/app/common/version.cljc index e73bd426900d9a9cc57dc930bb4a927238001264..20250bcf6d79e93e7bf2a6617abd8163c93f7768 100644 --- a/common/src/app/common/version.cljc +++ b/common/src/app/common/version.cljc @@ -9,7 +9,7 @@ (:require [cuerdas.core :as str])) -(def version-re #"^(([A-Za-z]+)\-?)?((\d+)\.(\d+)\.(\d+))(\-?((alpha|prealpha|beta|rc|dev)(\d+)?))?(\-?(\d+))?(\-?g(\w+))$") +(def version-re #"^(([A-Za-z]+)\-?)?((\d+)\.(\d+)\.(\d+))(\-?((RC|DEV)(\d+)?))?(\-?(\d+))?(\-?g(\w+))?$") (defn parse [data] diff --git a/common/test/cases/chained-components-changes-propagation.penpot b/common/test/cases/chained-components-changes-propagation.penpot new file mode 100644 index 0000000000000000000000000000000000000000..368e9a0bc50ef157d1e19960e5b97f9a6614a577 Binary files /dev/null and b/common/test/cases/chained-components-changes-propagation.penpot differ diff --git a/common/test/cases/copying-and-duplicating.penpot b/common/test/cases/copying-and-duplicating.penpot new file mode 100644 index 0000000000000000000000000000000000000000..23305d928c2fa05b2e58005f93e4ba1cdfdaf303 Binary files /dev/null and b/common/test/cases/copying-and-duplicating.penpot differ diff --git a/common/test/cases/detach-with-nested.penpot b/common/test/cases/detach-with-nested.penpot new file mode 100644 index 0000000000000000000000000000000000000000..2ff274b6d5d66acb190d02c7d0acb811d310874f Binary files /dev/null and b/common/test/cases/detach-with-nested.penpot differ diff --git a/common/test/cases/duplicated-pages.penpot b/common/test/cases/duplicated-pages.penpot new file mode 100644 index 0000000000000000000000000000000000000000..dccccbde44d5ef481488ca2fdd38a1634d733dfd Binary files /dev/null and b/common/test/cases/duplicated-pages.penpot differ diff --git a/common/test/cases/multiple-testing-levels.penpot b/common/test/cases/multiple-testing-levels.penpot new file mode 100644 index 0000000000000000000000000000000000000000..06539e8f7ed513855152844cf449ed104dbcd321 Binary files /dev/null and b/common/test/cases/multiple-testing-levels.penpot differ diff --git a/common/test/cases/remove-swap-slots.penpot b/common/test/cases/remove-swap-slots.penpot new file mode 100644 index 0000000000000000000000000000000000000000..0de71803b11f28a7330dd6fee443440a4211c57f Binary files /dev/null and b/common/test/cases/remove-swap-slots.penpot differ diff --git a/common/test/cases/swap-and-reset.penpot b/common/test/cases/swap-and-reset.penpot new file mode 100644 index 0000000000000000000000000000000000000000..d8ea3188a46b8883eb8f57ffaf42a293dcec5433 Binary files /dev/null and b/common/test/cases/swap-and-reset.penpot differ diff --git a/common/test/cases/swas-as-override.penpot b/common/test/cases/swas-as-override.penpot new file mode 100644 index 0000000000000000000000000000000000000000..acffd93203e334af5bebf7f7068bffe896cb7833 Binary files /dev/null and b/common/test/cases/swas-as-override.penpot differ diff --git a/common/test/common_tests/geom_point_test.cljc b/common/test/common_tests/geom_point_test.cljc index 0490e1d02eaadc3193f7a5a02b142c4a360bb23e..6ba7239f072c71049d89bc472f4035f8351f09ba 100644 --- a/common/test/common_tests/geom_point_test.cljc +++ b/common/test/common_tests/geom_point_test.cljc @@ -203,7 +203,7 @@ (t/is (mth/close? 1.5 (:x rs))) (t/is (mth/close? 3.5 (:y rs))))) -(t/deftest transform-point +(t/deftest ^:kaocha/skip transform-point ;;todo ) diff --git a/common/test/common_tests/helpers/components.cljc b/common/test/common_tests/helpers/components.cljc deleted file mode 100644 index 523438c449f707c71566e462ac2ba4694c48c0bc..0000000000000000000000000000000000000000 --- a/common/test/common_tests/helpers/components.cljc +++ /dev/null @@ -1,146 +0,0 @@ -;; 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 common-tests.helpers.components - (:require - [app.common.files.helpers :as cfh] - [app.common.types.component :as ctk] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [clojure.test :as t])) - -;; ---- Helpers to manage libraries and synchronization - -(defn check-instance-root - [shape] - (t/is (some? (:shape-ref shape))) - (t/is (some? (:component-id shape))) - (t/is (= (:component-root shape) true))) - -(defn check-instance-subroot - [shape] - (t/is (some? (:shape-ref shape))) - (t/is (some? (:component-id shape))) - (t/is (nil? (:component-root shape)))) - -(defn check-instance-child - [shape] - (t/is (some? (:shape-ref shape))) - (t/is (nil? (:component-id shape))) - (t/is (nil? (:component-file shape))) - (t/is (nil? (:component-root shape)))) - -(defn check-instance-inner - [shape] - (if (some? (:component-id shape)) - (check-instance-subroot shape) - (check-instance-child shape))) - -(defn check-noninstance - [shape] - (t/is (nil? (:shape-ref shape))) - (t/is (nil? (:component-id shape))) - (t/is (nil? (:component-file shape))) - (t/is (nil? (:component-root shape))) - (t/is (nil? (:remote-synced? shape))) - (t/is (nil? (:touched shape)))) - -(defn check-from-file - [shape file] - (t/is (= (:component-file shape) - (:id file)))) - -(defn resolve-instance - "Get the shape with the given id and all its children, and - verify that they are a well constructed instance tree." - [page root-inst-id] - (let [root-inst (ctn/get-shape page root-inst-id) - shapes-inst (cfh/get-children-with-self (:objects page) - root-inst-id)] - (check-instance-root (first shapes-inst)) - (run! check-instance-inner (rest shapes-inst)) - - shapes-inst)) - -(defn resolve-noninstance - "Get the shape with the given id and all its children, and - verify that they are not a component instance." - [page root-inst-id] - (let [root-inst (ctn/get-shape page root-inst-id) - shapes-inst (cfh/get-children-with-self (:objects page) - root-inst-id)] - (run! check-noninstance shapes-inst) - - shapes-inst)) - -(defn resolve-instance-and-main - "Get the shape with the given id and all its children, and also - the main component and all its shapes." - [page root-inst-id libraries] - (let [root-inst (ctn/get-shape page root-inst-id) - - component (ctf/get-component libraries (:component-file root-inst) (:component-id root-inst)) - - shapes-inst (cfh/get-children-with-self (:objects page) root-inst-id) - shapes-main (cfh/get-children-with-self (:objects component) (:shape-ref root-inst)) - - unique-refs (into #{} (map :shape-ref) shapes-inst) - - main-exists? (fn [shape] - (let [component-shape - (ctn/get-component-shape (:objects page) shape) - - component - (ctf/get-component libraries (:component-file component-shape) (:component-id component-shape)) - - main-shape - (ctn/get-shape component (:shape-ref shape))] - - (t/is (some? main-shape))))] - - ;; Validate that the instance tree is well constructed - (check-instance-root (first shapes-inst)) - (run! check-instance-inner (rest shapes-inst)) - (t/is (= (count shapes-inst) - (count shapes-main) - (count unique-refs))) - (run! main-exists? shapes-inst) - - [shapes-inst shapes-main component])) - -(defn resolve-instance-and-main-allow-dangling - "Get the shape with the given id and all its children, and also - the main component and all its shapes. Allows shapes with the - corresponding component shape missing." - [page root-inst-id libraries] - (let [root-inst (ctn/get-shape page root-inst-id) - - component (ctf/get-component libraries (:component-file root-inst) (:component-id root-inst)) - - shapes-inst (cfh/get-children-with-self (:objects page) root-inst-id) - shapes-main (cfh/get-children-with-self (:objects component) (:shape-ref root-inst)) - - unique-refs (into #{} (map :shape-ref) shapes-inst) - - main-exists? (fn [shape] - (let [component-shape - (ctn/get-component-shape (:objects page) shape) - - component - (ctf/get-component libraries (:component-file component-shape) (:component-id component-shape)) - - main-shape - (ctn/get-shape component (:shape-ref shape))] - - (t/is (some? main-shape))))] - - ;; Validate that the instance tree is well constructed - (check-instance-root (first shapes-inst)) - - [shapes-inst shapes-main component])) - - - diff --git a/common/test/common_tests/helpers/files.cljc b/common/test/common_tests/helpers/files.cljc deleted file mode 100644 index 3f27d7021bad78d52e231458fe9392374f918715..0000000000000000000000000000000000000000 --- a/common/test/common_tests/helpers/files.cljc +++ /dev/null @@ -1,155 +0,0 @@ -;; 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 common-tests.helpers.files - (:require - [app.common.features :as ffeat] - [app.common.geom.point :as gpt] - [app.common.types.colors-list :as ctcl] - [app.common.types.components-list :as ctkl] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.types.pages-list :as ctpl] - [app.common.types.shape :as cts] - [app.common.types.shape-tree :as ctst] - [app.common.types.typographies-list :as ctyl] - [app.common.uuid :as uuid])) - -(defn- make-file-data - [file-id page-id] - (binding [ffeat/*current* #{"components/v2"}] - (ctf/make-file-data file-id page-id))) - -(def ^:private idmap (atom {})) - -(defn reset-idmap! - [next] - (reset! idmap {}) - (next)) - -(defn id - [label] - (get @idmap label)) - -(defn sample-file - ([file-id page-id] (sample-file file-id page-id nil)) - ([file-id page-id props] - (merge {:id file-id - :name (get props :name "File1") - :data (make-file-data file-id page-id)} - props))) - -(defn sample-shape - [file label type page-id props] - (ctf/update-file-data - file - (fn [file-data] - (let [frame-id (get props :frame-id uuid/zero) - parent-id (get props :parent-id uuid/zero) - shape (cts/setup-shape - (-> {:type type - :width 1 - :height 1} - (merge props)))] - - (swap! idmap assoc label (:id shape)) - (ctpl/update-page file-data - page-id - #(ctst/add-shape (:id shape) - shape - % - frame-id - parent-id - 0 - true)))))) - -(defn sample-component - [file label page-id shape-id] - (ctf/update-file-data - file - (fn [file-data] - (let [page (ctpl/get-page file-data page-id) - - [component-shape component-shapes updated-shapes] - (ctn/make-component-shape (ctn/get-shape page shape-id) - (:objects page) - (:id file) - true)] - - (swap! idmap assoc label (:id component-shape)) - (-> file-data - (ctpl/update-page page-id - #(reduce (fn [page shape] (ctst/set-shape page shape)) - % - updated-shapes)) - (ctkl/add-component {:id (:id component-shape) - :name (:name component-shape) - :path "" - :main-instance-id shape-id - :main-instance-page page-id - :shapes component-shapes})))))) - -(defn sample-instance - [file label page-id library component-id] - (ctf/update-file-data - file - (fn [file-data] - (let [[instance-shape instance-shapes] - (ctn/make-component-instance (ctpl/get-page file-data page-id) - (ctkl/get-component (:data library) component-id) - (:data library) - (gpt/point 0 0) - true)] - - (swap! idmap assoc label (:id instance-shape)) - (-> file-data - (ctpl/update-page page-id - #(reduce (fn [page shape] - (ctst/add-shape (:id shape) - shape - page - uuid/zero - (:parent-id shape) - 0 - true)) - % - instance-shapes))))))) - -(defn sample-color - [file label props] - (ctf/update-file-data - file - (fn [file-data] - (let [id (uuid/next) - props (merge {:id id - :name "Color 1" - :color "#000000" - :opacity 1} - props)] - (swap! idmap assoc label id) - (ctcl/add-color file-data props))))) - -(defn sample-typography - [file label props] - (ctf/update-file-data - file - (fn [file-data] - (let [id (uuid/next) - props (merge {:id id - :name "Typography 1" - :font-id "sourcesanspro" - :font-family "sourcesanspro" - :font-size "14" - :font-style "normal" - :font-variant-id "regular" - :font-weight "400" - :line-height "1.2" - :letter-spacing "0" - :text-transform "none"} - props)] - (swap! idmap assoc label id) - (ctyl/add-typography file-data props))))) - diff --git a/common/test/common_tests/logic/chained_propagation_test.cljc b/common/test/common_tests/logic/chained_propagation_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..dd3f07aa75c39638cf6c6818deb75e84149ca24a --- /dev/null +++ b/common/test/common_tests/logic/chained_propagation_test.cljc @@ -0,0 +1,116 @@ +;; 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 common-tests.logic.chained-propagation-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.container :as ctn] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(defn- first-fill-color [file tag] + (-> (ths/get-shape file tag) + (:fills) + first + :fill-color)) + +(defn- first-child-fill-color [file tag] + (let [shape (ths/get-shape file tag)] + (-> (ths/get-shape-by-id file (first (:shapes shape))) + (:fills) + first + :fill-color))) + +;; Related .penpot file: common/test/cases/chained-components-changes-propagation.penpot +(t/deftest test-propagation-with-anidated-components + (letfn [(setup [] + (-> (thf/sample-file :file1) + (tho/add-frame :frame-comp-1) + (ths/add-sample-shape :rectangle :parent-label :frame-comp-1 :fills (ths/sample-fills-color :fill-color "#2653d8")) + (thc/make-component :comp-1 :frame-comp-1) + + (tho/add-frame :frame-comp-2) + (thc/instantiate-component :comp-1 :copy-comp-1 :parent-label :frame-comp-2 :children-labels [:rect-comp-2]) + (thc/make-component :comp-2 :frame-comp-2) + + (tho/add-frame :frame-comp-3) + (thc/instantiate-component :comp-2 :copy-comp-2 :parent-label :frame-comp-3 :children-labels [:comp-1-comp-2]) + (thc/make-component :comp-3 :frame-comp-3))) + + (propagate-all-component-changes [file] + (-> file + (tho/propagate-component-changes :comp-1) + (tho/propagate-component-changes :comp-2))) + + (fill-colors [file] + [(tho/bottom-fill-color file :frame-comp-1) + (tho/bottom-fill-color file :frame-comp-2) + (tho/bottom-fill-color file :frame-comp-3)]) + + (validate [file validator] + (validator file) + file)] + + (-> (setup) + ;; Change the color of Comp1 inside Comp2 to red. It will propagate to Comp1 inside Comp3 + (tho/update-bottom-color :frame-comp-2 "#FF0000" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#2653d8" "#FF0000" "#FF0000"]))) + + ;; Change the color of Comp1 inside Comp3 to green. + (tho/update-bottom-color :frame-comp-3 "#00FF00" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#2653d8" "#FF0000" "#00FF00"]))) + + ;; Select Comp1 inside Comp3, and do a ‘Reset override’ + ;; Desired result: Comp1 inside Comp3 change its color to red, like Comp1 inside Comp2. + (tho/reset-overrides-in-first-child :copy-comp-2) + (validate #(t/is (= (fill-colors %) ["#2653d8" "#FF0000" "#FF0000"])))))) + +(t/deftest test-propagation-with-deleted-component + (letfn [(setup [] + (-> (thf/sample-file :file1) + (tho/add-frame :frame-comp-4) + (ths/add-sample-shape :rectangle :parent-label :frame-comp-4 :fills (ths/sample-fills-color :fill-color "#b1b2b5")) + (thc/make-component :comp-4 :frame-comp-4) + + (tho/add-frame :frame-comp-5) + (thc/instantiate-component :comp-4 :copy-comp-4 :parent-label :frame-comp-5 :children-labels [:rect-comp-5]) + (thc/make-component :comp-5 :frame-comp-5) + + (tho/add-frame :frame-comp-6) + (thc/instantiate-component :comp-5 :copy-comp-5 :parent-label :frame-comp-6 :children-labels [:comp-4-comp-5]) + (thc/make-component :comp-6 :frame-comp-6))) + + (propagate-all-component-changes [file] + (-> file + (tho/propagate-component-changes :comp-4) + (tho/propagate-component-changes :comp-5))) + + (fill-colors [file] + [(tho/bottom-fill-color file :frame-comp-4) + (tho/bottom-fill-color file :frame-comp-5) + (tho/bottom-fill-color file :frame-comp-6)]) + + (validate [file validator] + (validator file) + file)] + + (-> (setup) + ;; Delete Comp5. + (tho/delete-shape :frame-comp-5) + (validate #(t/is (= (fill-colors %) ["#b1b2b5" nil "#b1b2b5"]))) + + ;; Change the color of Comp4 + ;; Desired result: Comp6 change color + (tho/update-bottom-color :frame-comp-4 "#FF0000" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#FF0000" nil "#FF0000"])))))) diff --git a/common/test/common_tests/logic/comp_creation_test.cljc b/common/test/common_tests/logic/comp_creation_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..c59c14bc1ba418bef95b17413a384e83860ace90 --- /dev/null +++ b/common/test/common_tests/logic/comp_creation_test.cljc @@ -0,0 +1,610 @@ +;; 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 common-tests.logic.comp-creation-test + (:require + [app.common.data :as d] + [app.common.files.changes-builder :as pcb] + [app.common.files.shapes-helpers :as cfsh] + [app.common.geom.point :as gpt] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] + [app.common.types.shape-tree :as ctst] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-add-component-from-single-frame + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (ths/add-sample-shape :frame1 :type :frame)) + + page (thf/current-page file) + frame1 (ths/get-shape file :frame1) + + ;; ==== Action + [_ component-id changes] + (cll/generate-add-component (pcb/empty-changes) + [frame1] + (:objects page) + (:id page) + (:id file) + true + nil + nil) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component (thc/get-component-by-id file' component-id) + root (ths/get-shape-by-id file' (:main-instance-id component)) + frame1' (ths/get-shape file' :frame1)] + + ;; ==== Check + (t/is (some? component)) + (t/is (some? root)) + (t/is (some? frame1')) + (t/is (= (:id root) (:id frame1'))) + (t/is (ctk/main-instance? root)) + (t/is (ctk/main-instance-of? (:id root) (:id page) component)))) + +(t/deftest test-add-component-from-single-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (ths/add-sample-shape :shape1 :type :rect)) + + page (thf/current-page file) + shape1 (ths/get-shape file :shape1) + + ;; ==== Action + [_ component-id changes] + (cll/generate-add-component (pcb/empty-changes) + [shape1] + (:objects page) + (:id page) + (:id file) + true + nil + cfsh/prepare-create-artboard-from-selection) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component (thc/get-component-by-id file' component-id) + root (ths/get-shape-by-id file' (:main-instance-id component)) + shape1' (ths/get-shape file' :shape1)] + + ;; ==== Check + (t/is (some? component)) + (t/is (some? root)) + (t/is (some? shape1')) + (t/is (ctst/parent-of? root shape1')) + (t/is (= (:type root) :frame)) + (t/is (ctk/main-instance? root)) + (t/is (ctk/main-instance-of? (:id root) (:id page) component)))) + +(t/deftest test-add-component-from-several-shapes + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (ths/add-sample-shape :shape1 :type :rect) + (ths/add-sample-shape :shape2 :type :rect)) + + page (thf/current-page file) + shape1 (ths/get-shape file :shape1) + shape2 (ths/get-shape file :shape2) + + ;; ==== Action + [_ component-id changes] + (cll/generate-add-component (pcb/empty-changes) + [shape1 shape2] + (:objects page) + (:id page) + (:id file) + true + nil + cfsh/prepare-create-artboard-from-selection) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component (thc/get-component-by-id file' component-id) + root (ths/get-shape-by-id file' (:main-instance-id component)) + shape1' (ths/get-shape file' :shape1) + shape2' (ths/get-shape file' :shape2)] + + ;; ==== Check + (t/is (some? component)) + (t/is (some? root)) + (t/is (some? shape1')) + (t/is (some? shape2')) + (t/is (ctst/parent-of? root shape1')) + (t/is (ctst/parent-of? root shape2')) + (t/is (= (:type root) :frame)) + (t/is (ctk/main-instance? root)) + (t/is (ctk/main-instance-of? (:id root) (:id page) component)))) + +(t/deftest test-add-component-from-several-frames + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (ths/add-sample-shape :frame1 :type :frame) + (ths/add-sample-shape :frame2 :type :frame)) + + page (thf/current-page file) + frame1 (ths/get-shape file :frame1) + frame2 (ths/get-shape file :frame2) + + ;; ==== Action + [_ component-id changes] + (cll/generate-add-component (pcb/empty-changes) + [frame1 frame2] + (:objects page) + (:id page) + (:id file) + true + nil + cfsh/prepare-create-artboard-from-selection) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component (thc/get-component-by-id file' component-id) + root (ths/get-shape-by-id file' (:main-instance-id component)) + frame1' (ths/get-shape file' :frame1) + frame2' (ths/get-shape file' :frame2)] + + ;; ==== Check + (t/is (some? component)) + (t/is (some? root)) + (t/is (some? frame1')) + (t/is (some? frame2')) + (t/is (ctst/parent-of? root frame1')) + (t/is (ctst/parent-of? root frame2')) + (t/is (= (:type root) :frame)) + (t/is (ctk/main-instance? root)) + (t/is (ctk/main-instance-of? (:id root) (:id page) component)))) + +(t/deftest test-add-component-from-frame-with-children + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (ths/add-sample-shape :frame1 :type :frame) + (ths/add-sample-shape :shape1 :type :rect :parent-label :frame1) + (ths/add-sample-shape :shape2 :type :rect :parent-label :frame1)) + + page (thf/current-page file) + frame1 (ths/get-shape file :frame1) + + ;; ==== Action + [_ component-id changes] + (cll/generate-add-component (pcb/empty-changes) + [frame1] + (:objects page) + (:id page) + (:id file) + true + nil + nil) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component (thc/get-component-by-id file' component-id) + root (ths/get-shape-by-id file' (:main-instance-id component)) + frame1' (ths/get-shape file' :frame1) + shape1' (ths/get-shape file' :shape1) + shape2' (ths/get-shape file' :shape2)] + + ;; ==== Check + (t/is (some? component)) + (t/is (some? root)) + (t/is (some? frame1')) + (t/is (= (:id root) (:id frame1'))) + (t/is (ctst/parent-of? frame1' shape1')) + (t/is (ctst/parent-of? frame1' shape2')) + (t/is (ctk/main-instance? root)) + (t/is (ctk/main-instance-of? (:id root) (:id page) component)))) + +(t/deftest test-add-component-from-copy + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main1-root + :main1-child + :copy1-root)) + + page (thf/current-page file) + copy1-root (ths/get-shape file :copy1-root) + + ;; ==== Action + [_ component2-id changes] + (cll/generate-add-component (pcb/empty-changes) + [copy1-root] + (:objects page) + (:id page) + (:id file) + true + nil + cfsh/prepare-create-artboard-from-selection) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component2' (thc/get-component-by-id file' component2-id) + root2' (ths/get-shape-by-id file' (:main-instance-id component2')) + copy1-root' (ths/get-shape file' :copy1-root)] + + ;; ==== Check + (t/is (some? component2')) + (t/is (some? root2')) + (t/is (some? copy1-root')) + (t/is (ctst/parent-of? root2' copy1-root')) + (t/is (ctk/main-instance? root2')) + (t/is (ctk/main-instance-of? (:id root2') (:id page) component2')))) + +(t/deftest test-rename-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 + :main1-root + :main1-child + :name "Test component before")) + + component (thc/get-component file :component1) + + ;; ==== Action + changes (cll/generate-rename-component (pcb/empty-changes) + (:id component) + "Test component after" + (:data file) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component' (thc/get-component file' :component1)] + + ;; ==== Check + (t/is (= (:name component') "Test component after")))) + +(t/deftest test-duplicate-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 + :main1-root + :main1-child)) + + component (thc/get-component file :component1) + + ;; ==== Action + changes (cll/generate-duplicate-component (pcb/empty-changes) + file + (:id component) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + components' (ctkl/components-seq (:data file')) + component1' (d/seek #(= (:id %) (thi/id :component1)) components') + component2' (d/seek #(not= (:id %) (thi/id :component1)) components') + root1' (ths/get-shape-by-id file' (:main-instance-id component1')) + root2' (ths/get-shape-by-id file' (:main-instance-id component2')) + child1' (ths/get-shape-by-id file' (first (:shapes root1'))) + child2' (ths/get-shape-by-id file' (first (:shapes root2')))] + + ;; ==== Check + (t/is (= 2 (count components'))) + (t/is (some? component1')) + (t/is (some? component2')) + (t/is (some? root1')) + (t/is (some? root2')) + (t/is (= (thi/id :main1-root) (:id root1'))) + (t/is (not= (thi/id :main1-root) (:id root2'))) + (t/is (some? child1')) + (t/is (some? child2')) + (t/is (= (thi/id :main1-child) (:id child1'))) + (t/is (not= (thi/id :main1-child) (:id child2'))))) + +(t/deftest test-delete-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main1-root + :main1-child + :copy1-root)) + + page (thf/current-page file) + root (ths/get-shape file :main1-root) + + ;; ==== Action + [_ changes] + (cls/generate-delete-shapes (pcb/empty-changes) + file + page + (:objects page) + #{(:id root)} + {:components-v2 true}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component1' (thc/get-component file' :component1 :include-deleted? true) + copy1-root' (ths/get-shape file' :copy1-root) + + main1-root' (ths/get-shape file' :main1-root) + main1-child' (ths/get-shape file' :main1-child) + + saved-objects (:objects component1') + saved-main1-root' (get saved-objects (thi/id :main1-root)) + saved-main1-child' (get saved-objects (thi/id :main1-child))] + + ;; ==== Check + (t/is (true? (:deleted component1'))) + (t/is (some? copy1-root')) + (t/is (nil? main1-root')) + (t/is (nil? main1-child')) + (t/is (some? saved-main1-root')) + (t/is (some? saved-main1-child')))) + +(t/deftest test-restore-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main1-root + :main1-child + :copy1-root)) + + page (thf/current-page file) + root (ths/get-shape file :main1-root) + + ;; ==== Action + [_ changes] + (cls/generate-delete-shapes (pcb/empty-changes) + file + page + (:objects page) + #{(:id root)} + {:components-v2 true}) + + file-deleted (thf/apply-changes file changes) + page-deleted (thf/current-page file-deleted) + + changes (cll/generate-restore-component (pcb/empty-changes) + (:data file-deleted) + (thi/id :component1) + (:id file-deleted) + page-deleted + (:objects page-deleted)) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component1' (thc/get-component file' :component1 :include-deleted? false) + copy1-root' (ths/get-shape file' :copy1-root) + + main1-root' (ths/get-shape file' :main1-root) + main1-child' (ths/get-shape file' :main1-child) + + saved-objects' (:objects component1')] + + ;; ==== Check + (t/is (nil? (:deleted component1'))) + (t/is (some? copy1-root')) + (t/is (some? main1-root')) + (t/is (some? main1-child')) + (t/is (ctk/main-instance? main1-root')) + (t/is (ctk/main-instance-of? (:id main1-root') (:id page) component1')) + (t/is (nil? saved-objects')))) + +(t/deftest test-instantiate-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 + :main1-root + :main1-child)) + + page (thf/current-page file) + component (thc/get-component file :component1) + + ;; ==== Action + [new-shape changes] + (cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page)) ;; This may not be moved to generate + (pcb/with-objects (:objects page))) ;; because in some cases the objects + (:objects page) ;; not the same as those on the page + (:id file) + (:id component) + (gpt/point 1000 1000) + page + {(:id file) file}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component' (thc/get-component file' :component1) + main1-root' (ths/get-shape file' :main1-root) + main1-child' (ths/get-shape file' :main1-child) + copy1-root' (ths/get-shape-by-id file' (:id new-shape)) + copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))] + + ;; ==== Check + (t/is (some? main1-root')) + (t/is (some? main1-child')) + (t/is (some? copy1-root')) + (t/is (some? copy1-child')) + (t/is (ctk/instance-root? copy1-root')) + (t/is (ctk/instance-of? copy1-root' (:id file') (:id component'))) + (t/is (ctk/is-main-of? main1-root' copy1-root' true)) + (t/is (ctk/is-main-of? main1-child' copy1-child' true)) + (t/is (ctst/parent-of? copy1-root' copy1-child')))) + +(t/deftest test-instantiate-component-from-lib + (let [;; ==== Setup + library (-> (thf/sample-file :library1) + (tho/add-simple-component :component1 + :main1-root + :main1-child)) + + file (thf/sample-file :file1) + + page (thf/current-page file) + component (thc/get-component library :component1) + + ;; ==== Action + [new-shape changes] + (cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page)) + (pcb/with-objects (:objects page))) + (:objects page) + (:id library) + (:id component) + (gpt/point 1000 1000) + page + {(:id file) file + (:id library) library}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component' (thc/get-component library :component1) + main1-root' (ths/get-shape library :main1-root) + main1-child' (ths/get-shape library :main1-child) + copy1-root' (ths/get-shape-by-id file' (:id new-shape)) + copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))] + + ;; ==== Check + (t/is (some? main1-root')) + (t/is (some? main1-child')) + (t/is (some? copy1-root')) + (t/is (some? copy1-child')) + (t/is (ctk/instance-root? copy1-root')) + (t/is (ctk/instance-of? copy1-root' (:id library) (:id component'))) + (t/is (ctk/is-main-of? main1-root' copy1-root' true)) + (t/is (ctk/is-main-of? main1-child' copy1-child' true)) + (t/is (ctst/parent-of? copy1-root' copy1-child')))) + +(t/deftest test-instantiate-nested-component + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head)) + + page (thf/current-page file) + component (thc/get-component file :component1) + + ;; ==== Action + [new-shape changes] + (cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page)) + (pcb/with-objects (:objects page))) + (:objects page) + (:id file) + (:id component) + (gpt/point 1000 1000) + page + {(:id file) file}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component' (thc/get-component file' :component1) + main1-root' (ths/get-shape file' :main1-root) + main1-child' (ths/get-shape file' :main1-child) + copy1-root' (ths/get-shape-by-id file' (:id new-shape)) + copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))] + + ;; ==== Check + (t/is (some? main1-root')) + (t/is (some? main1-child')) + (t/is (some? copy1-root')) + (t/is (some? copy1-child')) + (t/is (ctk/instance-root? copy1-root')) + (t/is (ctk/instance-of? copy1-root' (:id file') (:id component'))) + (t/is (ctk/is-main-of? main1-root' copy1-root' true)) + (t/is (ctk/is-main-of? main1-child' copy1-child' true)) + (t/is (ctst/parent-of? copy1-root' copy1-child')))) + +(t/deftest test-instantiate-nested-component-from-lib + (let [;; ==== Setup + library (-> (thf/sample-file :file1) + (tho/add-nested-component :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head)) + + file (thf/sample-file :file1) + + page (thf/current-page file) + component (thc/get-component library :component1) + + ;; ==== Action + [new-shape changes] + (cll/generate-instantiate-component (-> (pcb/empty-changes nil (:id page)) + (pcb/with-objects (:objects page))) + (:objects page) + (:id library) + (:id component) + (gpt/point 1000 1000) + page + {(:id file) file + (:id library) library}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + component' (thc/get-component library :component1) + main1-root' (ths/get-shape library :main1-root) + main1-child' (ths/get-shape library :main1-child) + copy1-root' (ths/get-shape-by-id file' (:id new-shape)) + copy1-child' (ths/get-shape-by-id file' (first (:shapes copy1-root')))] + + ;; ==== Check + (t/is (some? main1-root')) + (t/is (some? main1-child')) + (t/is (some? copy1-root')) + (t/is (some? copy1-child')) + (t/is (ctk/instance-root? copy1-root')) + (t/is (ctk/instance-of? copy1-root' (:id library) (:id component'))) + (t/is (ctk/is-main-of? main1-root' copy1-root' true)) + (t/is (ctk/is-main-of? main1-child' copy1-child' true)) + (t/is (ctst/parent-of? copy1-root' copy1-child')))) + +(t/deftest test-detach-copy + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main1-root + :main1-child + :copy1-root)) + + page (thf/current-page file) + copy1-root (ths/get-shape file :copy1-root) + + ;; ==== Action + changes (cll/generate-detach-component (pcb/empty-changes) + (:id copy1-root) + (:data file) + (:id page) + {(:id file) file}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy1-root' (ths/get-shape file' :copy1-root)] + + ;; ==== Check + (t/is (some? copy1-root')) + (t/is (not (ctk/instance-head? copy1-root'))) + (t/is (not (ctk/in-component-copy? copy1-root'))))) diff --git a/common/test/common_tests/logic/comp_detach_with_nested_test.cljc b/common/test/common_tests/logic/comp_detach_with_nested_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..d7b999db311d598b765c1596c605abb357f53801 --- /dev/null +++ b/common/test/common_tests/logic/comp_detach_with_nested_test.cljc @@ -0,0 +1,371 @@ +;; 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 common-tests.logic.comp-detach-with-nested-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +;; Related .penpot file: common/test/cases/detach-with-nested.penpot +(defn- setup-file + [] + ;; {:r-ellipse} [:name Ellipse, :type :frame] # [Component :c-ellipse] + ;; :ellipse [:name Ellipse, :type :circle] + ;; {:r-rectangle} [:name Rectangle, :type :frame] # [Component :c-rectangle] + ;; :rectangle [:name rectangle, :type :rect] + ;; {:board-with-ellipse} [:name Board with ellipse, :type :frame] # [Component :c-board-with-ellipse] + ;; :nested-h-ellipse [:name Ellipse, :type :frame] @--> :r-ellipse + ;; :nested-ellipse [:name Ellipse, :type :circle] ---> :ellipse + ;; {:board-with-rectangle} [:name Board with rectangle, :type :frame] # [Component :c-board-with-rectangle] + ;; :nested-h-rectangle [:name Rectangle, :type :frame] @--> :r-rectangle + ;; :nested-rectangle [:name rectangle, :type :rect] ---> :rectangle + ;; {:big-board} [:name Big Board, :type :frame] # [Component :c-big-board] + ;; :h-board-with-ellipse [:name Board with ellipse, :type :frame] @--> :board-with-ellipse + ;; :nested2-h-ellipse [:name Ellipse, :type :frame] @--> :nested-h-ellipse + ;; :nested2-ellipse [:name Ellipse, :type :circle] ---> :nested-ellipse + (-> (thf/sample-file :file1) + + (tho/add-simple-component :c-ellipse :r-ellipse :ellipse + :root-params {:name "Ellipse"} + :child-params {:name "Ellipse" :type :circle}) + + (tho/add-simple-component :c-rectangle :r-rectangle :rectangle + :root-params {:name "Rectangle"} + :child-params {:name "rectangle" :type :rect}) + + (tho/add-frame :board-with-ellipse :name "Board with ellipse") + (thc/instantiate-component :c-ellipse :nested-h-ellipse :parent-label :board-with-ellipse + :children-labels [:nested-ellipse]) + (thc/make-component :c-board-with-ellipse :board-with-ellipse) + + (tho/add-frame :board-with-rectangle :name "Board with rectangle") + (thc/instantiate-component :c-rectangle :nested-h-rectangle :parent-label :board-with-rectangle + :children-labels [:nested-rectangle]) + (thc/make-component :c-board-with-rectangle :board-with-rectangle) + + (tho/add-frame :big-board :name "Big Board") + (thc/instantiate-component :c-board-with-ellipse + :h-board-with-ellipse + :parent-label :big-board + :children-labels [:nested2-h-ellipse :nested2-ellipse]) + (thc/make-component :c-big-board :big-board))) + +(t/deftest test-advance-when-not-swapped + (let [;; ==== Setup + file (-> (setup-file) + (thc/instantiate-component :c-big-board + :copy-big-board + :children-labels [:copy-h-board-with-ellipse + :copy-nested-h-ellipse + :copy-nested-ellipse])) + + page (thf/current-page file) + + ;; ==== Action + changes (cll/generate-detach-instance (-> (pcb/empty-changes nil) + (pcb/with-page page) + (pcb/with-objects (:objects page))) + page + {(:id file) file} + (thi/id :copy-big-board)) + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-h-board-with-ellipse (ths/get-shape file' :copy-h-board-with-ellipse) + copy-nested-h-ellipse (ths/get-shape file' :copy-nested-h-ellipse) + copy-nested-ellipse (ths/get-shape file' :copy-nested-ellipse)] + + ;; ==== Check + + ;; In the normal case, children's ref (that pointed to the near main inside big-board) + ;; are advanced to point to the new near main inside board-with-ellipse. + (t/is (ctk/instance-root? copy-h-board-with-ellipse)) + (t/is (= (:shape-ref copy-h-board-with-ellipse) (thi/id :board-with-ellipse))) + (t/is (nil? (ctk/get-swap-slot copy-h-board-with-ellipse))) + + (t/is (ctk/instance-head? copy-nested-h-ellipse)) + (t/is (= (:shape-ref copy-nested-h-ellipse) (thi/id :nested-h-ellipse))) + (t/is (nil? (ctk/get-swap-slot copy-nested-h-ellipse))) + + (t/is (not (ctk/instance-head? copy-nested-ellipse))) + (t/is (= (:shape-ref copy-nested-ellipse) (thi/id :nested-ellipse))) + (t/is (nil? (ctk/get-swap-slot copy-nested-ellipse))))) + +(t/deftest test-dont-advance-when-swapped-copy + (let [;; ==== Setup + file (-> (setup-file) + (thc/instantiate-component :c-big-board + :copy-big-board + :children-labels [:copy-h-board-with-ellipse + :copy-nested-h-ellipse + :copy-nested-ellipse]) + (thc/component-swap :copy-h-board-with-ellipse + :c-board-with-rectangle + :copy-h-board-with-rectangle + :children-labels [:copy-nested-h-rectangle + :copy-nested-rectangle])) + + page (thf/current-page file) + + ;; ==== Action + changes (cll/generate-detach-instance (-> (pcb/empty-changes nil) + (pcb/with-page page) + (pcb/with-objects (:objects page))) + page + {(:id file) file} + (thi/id :copy-big-board)) + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-h-board-with-rectangle (ths/get-shape file' :copy-h-board-with-rectangle) + copy-nested-h-rectangle (ths/get-shape file' :copy-nested-h-rectangle) + copy-nested-rectangle (ths/get-shape file' :copy-nested-rectangle)] + + ;; ==== Check + + ;; If the nested copy was swapped, there is no need to advance shape-refs, + ;; as they already pointing to the near main inside board-with-rectangle. + (t/is (ctk/instance-root? copy-h-board-with-rectangle)) + (t/is (= (:shape-ref copy-h-board-with-rectangle) (thi/id :board-with-rectangle))) + (t/is (nil? (ctk/get-swap-slot copy-h-board-with-rectangle))) + + (t/is (ctk/instance-head? copy-nested-h-rectangle)) + (t/is (= (:shape-ref copy-nested-h-rectangle) (thi/id :nested-h-rectangle))) + (t/is (nil? (ctk/get-swap-slot copy-nested-h-rectangle))) + + (t/is (not (ctk/instance-head? copy-nested-rectangle))) + (t/is (= (:shape-ref copy-nested-rectangle) (thi/id :nested-rectangle))) + (t/is (nil? (ctk/get-swap-slot copy-nested-rectangle))))) + +(t/deftest test-propagate-slot-when-swapped-main + (let [;; ==== Setup + file (-> (setup-file) + (thc/component-swap :nested2-h-ellipse + :c-rectangle + :nested2-h-rectangle + :children-labels [:nested2-rectangle]) + (thc/instantiate-component :c-big-board + :copy-big-board + :children-labels [:copy-h-board-with-ellipse + :copy-nested-h-rectangle + :copy-nested-rectangle])) + + page (thf/current-page file) + + ;; ==== Action + changes (cll/generate-detach-instance (-> (pcb/empty-changes nil) + (pcb/with-page page) + (pcb/with-objects (:objects page))) + page + {(:id file) file} + (thi/id :copy-big-board)) + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-h-board-with-ellipse (ths/get-shape file' :copy-h-board-with-ellipse) + copy-nested-h-rectangle (ths/get-shape file' :copy-nested-h-rectangle) + copy-nested-rectangle (ths/get-shape file' :copy-nested-rectangle)] + + ;; ==== Check + + ;; This one is advanced normally, as it has not been swapped. + (t/is (ctk/instance-root? copy-h-board-with-ellipse)) + (t/is (= (:shape-ref copy-h-board-with-ellipse) (thi/id :board-with-ellipse))) + (t/is (nil? (ctk/get-swap-slot copy-h-board-with-ellipse))) + + ;; If the nested copy has been swapped in the main, it does advance, + ;; but the swap slot of the near main is propagated to the copy. + (t/is (ctk/instance-head? copy-nested-h-rectangle)) + (t/is (= (:shape-ref copy-nested-h-rectangle) (thi/id :r-rectangle))) + (t/is (= (ctk/get-swap-slot copy-nested-h-rectangle) (thi/id :nested-h-ellipse))) + + (t/is (not (ctk/instance-head? copy-nested-rectangle))) + (t/is (= (:shape-ref copy-nested-rectangle) (thi/id :rectangle))) + (t/is (nil? (ctk/get-swap-slot copy-nested-rectangle))))) + +(t/deftest test-propagate-touched + (let [;; ==== Setup + file (-> (setup-file) + (ths/update-shape :nested2-ellipse :fills (ths/sample-fills-color :fill-color "#fabada")) + (thc/instantiate-component :c-big-board + :copy-big-board + :children-labels [:copy-h-board-with-ellipse + :copy-nested2-h-ellipse + :copy-nested2-ellipse])) + + page (thf/current-page file) + nested2-ellipse (ths/get-shape file :nested2-ellipse) + copy-nested2-ellipse (ths/get-shape file :copy-nested2-ellipse) + + ;; ==== Action + changes (cll/generate-detach-instance (-> (pcb/empty-changes nil) + (pcb/with-page page) + (pcb/with-objects (:objects page))) + page + {(:id file) file} + (thi/id :copy-big-board)) + file' (thf/apply-changes file changes) + + ;; ==== Get + nested2-ellipse' (ths/get-shape file' :nested2-ellipse) + copy-nested2-ellipse' (ths/get-shape file' :copy-nested2-ellipse) + fills' (:fills copy-nested2-ellipse') + fill' (first fills')] + + ;; ==== Check + + ;; The touched group must be propagated to the copy, because now this copy + ;; has the original ellipse component as near main, but its attributes have + ;; been inherited from the ellipse inside big-board. + (t/is (= (:touched nested2-ellipse) #{:fill-group})) + (t/is (= (:touched copy-nested2-ellipse) nil)) + (t/is (= (:touched nested2-ellipse') #{:fill-group})) + (t/is (= (:touched copy-nested2-ellipse') #{:fill-group})) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)))) + +(t/deftest test-merge-touched + (let [;; ==== Setup + file (-> (setup-file) + (ths/update-shape :nested2-ellipse :fills (ths/sample-fills-color :fill-color "#fabada")) + (thc/instantiate-component :c-big-board + :copy-big-board + :children-labels [:copy-h-board-with-ellipse + :copy-nested2-h-ellipse + :copy-nested2-ellipse]) + (ths/update-shape :copy-nested2-ellipse :name "Modified name") + (ths/update-shape :copy-nested2-ellipse :fills (ths/sample-fills-color :fill-color "#abcdef"))) + + page (thf/current-page file) + nested2-ellipse (ths/get-shape file :nested2-ellipse) + copy-nested2-ellipse (ths/get-shape file :copy-nested2-ellipse) + + ;; ==== Action + changes (cll/generate-detach-instance (-> (pcb/empty-changes nil) + (pcb/with-page page) + (pcb/with-objects (:objects page))) + page + {(:id file) file} + (thi/id :copy-big-board)) + file' (thf/apply-changes file changes) + + ;; ==== Get + nested2-ellipse' (ths/get-shape file' :nested2-ellipse) + copy-nested2-ellipse' (ths/get-shape file' :copy-nested2-ellipse) + fills' (:fills copy-nested2-ellipse') + fill' (first fills')] + + ;; ==== Check + + ;; If the copy have been already touched, merge the groups and preserve the modifications. + (t/is (= (:touched nested2-ellipse) #{:fill-group})) + (t/is (= (:touched copy-nested2-ellipse) #{:name-group :fill-group})) + (t/is (= (:touched nested2-ellipse') #{:fill-group})) + (t/is (= (:touched copy-nested2-ellipse') #{:name-group :fill-group})) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#abcdef")) + (t/is (= (:fill-opacity fill') 1)))) + +(t/deftest test-dont-propagete-touched-when-swapped-copy + (let [;; ==== Setup + file (-> (setup-file) + (ths/update-shape :nested-rectangle :fills (ths/sample-fills-color :fill-color "#fabada")) + (thc/instantiate-component :c-big-board + :copy-big-board + :children-labels [:copy-h-board-with-ellipse + :copy-nested2-h-ellipse + :copy-nested2-ellipse]) + (thc/component-swap :copy-h-board-with-ellipse + :c-board-with-rectangle + :copy-h-board-with-rectangle + :children-labels [:copy-nested2-h-rectangle + :copy-nested2-rectangle])) + + page (thf/current-page file) + nested2-rectangle (ths/get-shape file :nested2-rectangle) + copy-nested2-rectangle (ths/get-shape file :copy-nested2-rectangle) + + ;; ==== Action + changes (cll/generate-detach-instance (-> (pcb/empty-changes nil) + (pcb/with-page page) + (pcb/with-objects (:objects page))) + page + {(:id file) file} + (thi/id :copy-big-board)) + file' (thf/apply-changes file changes) + + ;; ==== Get + nested2-rectangle' (ths/get-shape file' :nested2-rectangle) + copy-nested2-rectangle' (ths/get-shape file' :copy-nested2-rectangle) + fills' (:fills copy-nested2-rectangle') + fill' (first fills')] + + ;; ==== Check + + ;; If the copy has been swapped, there is nothing to propagate since it's already + ;; pointing to the swapped near main. + (t/is (= (:touched nested2-rectangle) nil)) + (t/is (= (:touched copy-nested2-rectangle) nil)) + (t/is (= (:touched nested2-rectangle') nil)) + (t/is (= (:touched copy-nested2-rectangle') nil)) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)))) + +(t/deftest test-propagate-touched-when-swapped-main + (let [;; ==== Setup + file (-> (setup-file) + (thc/component-swap :nested2-h-ellipse + :c-rectangle + :nested2-h-rectangle + :children-labels [:nested2-rectangle]) + (ths/update-shape :nested2-rectangle :fills (ths/sample-fills-color :fill-color "#fabada")) + (thc/instantiate-component :c-big-board + :copy-big-board + :children-labels [:copy-h-board-with-ellipse + :copy-nested2-h-rectangle + :copy-nested2-rectangle])) + + page (thf/current-page file) + nested2-rectangle (ths/get-shape file :nested2-rectangle) + copy-nested2-rectangle (ths/get-shape file :copy-nested2-rectangle) + + ;; ==== Action + changes (cll/generate-detach-instance (-> (pcb/empty-changes nil) + (pcb/with-page page) + (pcb/with-objects (:objects page))) + page + {(:id file) file} + (thi/id :copy-big-board)) + file' (thf/apply-changes file changes) + + ;; ==== Get + nested2-rectangle' (ths/get-shape file' :nested2-rectangle) + copy-nested2-rectangle' (ths/get-shape file' :copy-nested2-rectangle) + fills' (:fills copy-nested2-rectangle') + fill' (first fills')] + + ;; ==== Check + + ;; If the main has been swapped, there is no difference. It propagates the same as + ;; if it were the original component. + (t/is (= (:touched nested2-rectangle) #{:fill-group})) + (t/is (= (:touched copy-nested2-rectangle) nil)) + (t/is (= (:touched nested2-rectangle') #{:fill-group})) + (t/is (= (:touched copy-nested2-rectangle') #{:fill-group})) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)))) diff --git a/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc b/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..0decac57cee9437d670487320d6831f753ed7a80 --- /dev/null +++ b/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc @@ -0,0 +1,569 @@ +;; 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 common-tests.logic.comp-remove-swap-slots-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.geom.point :as gpt] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.uuid :as uuid] + [clojure.test :as t] + [cuerdas.core :as str])) + +(t/use-fixtures :each thi/test-fixture) + +;; Related .penpot file: common/test/cases/remove-swap-slots.penpot +(defn- setup-file + [] + ;; {:frame-red} [:name Frame1] # [Component :red] + ;; {:frame-blue} [:name Frame1] # [Component :blue] + ;; {:frame-green} [:name Frame1] # [Component :green] + ;; :red-copy-green [:name Frame1] @--> :frame-red + ;; {:frame-b1} [:name Frame1] # [Component :b1] + ;; :blue1 [:name Frame1, :swap-slot-label :red-copy] @--> :frame-blue + ;; :frame-yellow [:name Frame1] + ;; :green-copy [:name Frame1] @--> :frame-green + ;; :blue2 [:name Frame1, :swap-slot-label :red-copy-green] @--> :frame-blue + ;; {:frame-b2} [:name Frame1] # [Component :b2] + (-> (thf/sample-file :file1) + (tho/add-frame :frame-red) + (thc/make-component :red :frame-red) + (tho/add-frame :frame-blue :name "frame-blue") + (thc/make-component :blue :frame-blue) + (tho/add-frame :frame-green) + (thc/make-component :green :frame-green) + (thc/instantiate-component :red :red-copy-green :parent-label :frame-green) + (tho/add-frame :frame-b1) + (thc/make-component :b1 :frame-b1) + (tho/add-frame :frame-yellow :parent-label :frame-b1 :name "frame-yellow") + (thc/instantiate-component :red :red-copy :parent-label :frame-b1) + (thc/component-swap :red-copy :blue :blue1) + (thc/instantiate-component :green :green-copy :parent-label :frame-b1 :children-labels [:red-copy-in-green-copy]) + (thc/component-swap :red-copy-in-green-copy :blue :blue2) + (tho/add-frame :frame-b2) + (thc/make-component :b2 :frame-b2))) + +(t/deftest test-remove-swap-slot-relocating-blue1-to-root + (let [;; ==== Setup + file (setup-file) + + page (thf/current-page file) + blue1 (ths/get-shape file :blue1) + + ;; ==== Action + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + uuid/zero ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id blue1)}) ;; ids + + file' (thf/apply-changes file changes) + + ;; ==== Get + blue1' (ths/get-shape file' :blue1)] + + ;; ==== Check + + ;; blue1 had swap-id before move + (t/is (some? (ctk/get-swap-slot blue1))) + + ;; blue1 has not swap-id after move + (t/is (some? blue1')) + (t/is (nil? (ctk/get-swap-slot blue1'))))) + +(t/deftest test-remove-swap-slot-relocating-blue1-to-b2 + (let [;; ==== Setup + file (setup-file) + page (thf/current-page file) + blue1 (ths/get-shape file :blue1) + b2 (ths/get-shape file :frame-b2) + + + ;; ==== Action + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + (:id b2) ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id blue1)}) ;; ids + + file' (thf/apply-changes file changes) + + ;; ==== Get + blue1' (ths/get-shape file' :blue1)] + + ;; ==== Check + + ;; blue1 had swap-id before move + (t/is (some? (ctk/get-swap-slot blue1))) + + ;; blue1 has not swap-id after move + (t/is (some? blue1')) + (t/is (nil? (ctk/get-swap-slot blue1'))))) + +(t/deftest test-remove-swap-slot-relocating-yellow-to-root + (let [;; ==== Setup + file (setup-file) + page (thf/current-page file) + blue1 (ths/get-shape file :blue1) + yellow (ths/get-shape file :frame-yellow) + + ;; ==== Action + ;; Move blue1 into yellow + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + (:id yellow) ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id blue1)}) ;; ids + + + file' (thf/apply-changes file changes) + page' (thf/current-page file') + yellow' (ths/get-shape file' :frame-yellow) + + ;; Move yellow into root + changes' (cls/generate-relocate (pcb/empty-changes nil) + (:objects page') + uuid/zero ;; parent-id + (:id page') ;; page-id + 0 ;; to-index + #{(:id yellow')}) ;; ids + + file'' (thf/apply-changes file' changes') + + ;; ==== Get + blue1'' (ths/get-shape file'' :blue1)] + + ;; ==== Check + + ;; blue1 had swap-id before move + (t/is (some? (ctk/get-swap-slot blue1))) + + ;; blue1 has not swap-id after move + (t/is (some? blue1'')) + (t/is (nil? (ctk/get-swap-slot blue1''))))) + +(t/deftest test-remove-swap-slot-relocating-yellow-to-b2 + (let [;; ==== Setup + file (setup-file) + page (thf/current-page file) + blue1 (ths/get-shape file :blue1) + yellow (ths/get-shape file :frame-yellow) + + ;; ==== Action + ;; Move blue1 into yellow + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + (:id yellow) ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id blue1)}) ;; ids + + + file' (thf/apply-changes file changes) + page' (thf/current-page file') + yellow' (ths/get-shape file' :frame-yellow) + b2' (ths/get-shape file' :frame-b2) + + ;; Move yellow into b2 + changes' (cls/generate-relocate (pcb/empty-changes nil) + (:objects page') + (:id b2') ;; parent-id + (:id page') ;; page-id + 0 ;; to-index + #{(:id yellow')}) ;; ids + + file'' (thf/apply-changes file' changes') + + ;; ==== Get + blue1'' (ths/get-shape file'' :blue1)] + + ;; ==== Check + + ;; blue1 had swap-id before move + (t/is (some? (ctk/get-swap-slot blue1))) + + ;; blue1 has not swap-id after move + (t/is (some? blue1'')) + (t/is (nil? (ctk/get-swap-slot blue1''))))) + +(defn- find-duplicated-shape + [original-shape page] + ;; duplicated shape has the same name, the same parent, and doesn't have a label + (->> (vals (:objects page)) + (filter #(and (= (:name %) (:name original-shape)) + (= (:parent-id %) (:parent-id original-shape)) + (str/starts-with? (thi/label (:id %)) " (pcb/empty-changes nil) + (cll/generate-duplicate-changes (:objects page) ;; objects + page ;; page + #{(:id blue1)} ;; ids + (gpt/point 0 0) ;; delta + {(:id file) file} ;; libraries + (:data file) ;; library-data + (:id file)) ;; file-id + (cll/generate-duplicate-changes-update-indices (:objects page) ;; objects + #{(:id blue1)})) ;; ids + + + + file' (thf/apply-changes file changes) + + ;; ==== Get + page' (thf/current-page file') + blue1' (ths/get-shape file' :blue1) + duplicated-blue1' (find-duplicated-shape blue1' page')] + + ;; ==== Check + + ;; blue1 has swap-id + (t/is (some? (ctk/get-swap-slot blue1'))) + + ;; duplicated-blue1 has not swap-id + (t/is (some? duplicated-blue1')) + (t/is (nil? (ctk/get-swap-slot duplicated-blue1'))))) + +(t/deftest test-remove-swap-slot-duplicate-yellow + (let [;; ==== Setup + file (setup-file) + page (thf/current-page file) + blue1 (ths/get-shape file :blue1) + yellow (ths/get-shape file :frame-yellow) + + ;; ==== Action + ;; Move blue1 into yellow + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + (:id yellow) ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id blue1)}) ;; ids + + file' (thf/apply-changes file changes) + page' (thf/current-page file') + yellow' (ths/get-shape file' :frame-yellow) + + ;; Duplicate yellow + changes' (-> (pcb/empty-changes nil) + (cll/generate-duplicate-changes (:objects page') ;; objects + page' ;; page + #{(:id yellow')} ;; ids + (gpt/point 0 0) ;; delta + {(:id file') file'} ;; libraries + (:data file') ;; library-data + (:id file')) ;; file-id + (cll/generate-duplicate-changes-update-indices (:objects page') ;; objects + #{(:id yellow')})) ;; ids + + file'' (thf/apply-changes file' changes') + + ;; ==== Get + page'' (thf/current-page file'') + blue1'' (ths/get-shape file'' :blue1) + yellow'' (ths/get-shape file'' :frame-yellow) + + + duplicated-yellow'' (find-duplicated-shape yellow'' page'') + duplicated-blue1-id'' (-> duplicated-yellow'' + :shapes + first) + duplicated-blue1'' (get (:objects page'') duplicated-blue1-id'')] + + ;; ==== Check + + ;; blue1'' has swap-id + (t/is (some? (ctk/get-swap-slot blue1''))) + + ;; duplicated-blue1'' has not swap-id + (t/is (some? duplicated-blue1'')) + (t/is (nil? (ctk/get-swap-slot duplicated-blue1''))))) + +(t/deftest test-keep-swap-slot-relocating-blue1-before-yellow + (let [;; ==== Setup + file (setup-file) + + page (thf/current-page file) + blue1 (ths/get-shape file :blue1) + + ;; ==== Action + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + (:parent-id blue1) ;; parent-id + (:id page) ;; page-id + 2 ;; to-index + #{(:id blue1)}) ;; ids + + file' (thf/apply-changes file changes) + + ;; ==== Get + blue1' (ths/get-shape file' :blue1)] + + ;; ==== Check + + ;; blue1 had swap-id before move + (t/is (some? (ctk/get-swap-slot blue1))) + + ;; blue1 still has swap-id after move + (t/is (some? blue1')) + (t/is (some? (ctk/get-swap-slot blue1'))))) + +(t/deftest test-keep-swap-slot-move-blue1-inside-and-outside-yellow + (let [;; ==== Setup + file (setup-file) + page (thf/current-page file) + blue1 (ths/get-shape file :blue1) + yellow (ths/get-shape file :frame-yellow) + + ;; ==== Action + ;; Move blue1 into yellow + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + (:id yellow) ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id blue1)}) ;; ids + + file' (thf/apply-changes file changes) + + ;; Move blue1 outside yellow + page' (thf/current-page file') + blue1' (ths/get-shape file' :blue1) + b1' (ths/get-shape file' :frame-b1) + changes' (cls/generate-relocate (pcb/empty-changes nil) + (:objects page') + (:id b1') ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id blue1')}) ;; ids + + file'' (thf/apply-changes file' changes') + + ;; ==== Get + blue1'' (ths/get-shape file'' :blue1)] + + ;; ==== Check + + ;; blue1 has swap-id before move + (t/is (some? (ctk/get-swap-slot blue1))) + + ;;blue1 still has swap-id after move + (t/is (some? blue1'')) + (t/is (some? (ctk/get-swap-slot blue1''))))) + + +(t/deftest test-keep-swap-slot-relocate-blue1-inside-and-outside-yellow + (let [;; ==== Setup + file (setup-file) + page (thf/current-page file) + blue1 (ths/get-shape file :blue1) + yellow (ths/get-shape file :frame-yellow) + + ;; ==== Action + ;; Relocate blue1 into yellow + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + (:id yellow) ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id blue1)}) ;; ids + + + file' (thf/apply-changes file changes) + + ;; Relocate blue1 outside yellow + page' (thf/current-page file') + blue1' (ths/get-shape file' :blue1) + b1' (ths/get-shape file' :frame-b1) + changes' (cls/generate-relocate (pcb/empty-changes nil) + (:objects page') + (:id b1') ;; parent-id + (:id page') ;; page-id + 0 ;; to-index + #{(:id blue1')}) ;; ids + + + file'' (thf/apply-changes file' changes') + + ;; ==== Get + blue1'' (ths/get-shape file'' :blue1)] + + ;; ==== Check + + ;; blue1 has swap-id before move + (t/is (some? (ctk/get-swap-slot blue1))) + + ;;blue1 still has swap-id after move + (t/is (some? blue1'')) + (t/is (some? (ctk/get-swap-slot blue1''))))) + + +(t/deftest test-remove-swap-slot-relocating-green-copy-to-root + (let [;; ==== Setup + file (setup-file) + + page (thf/current-page file) + blue2 (ths/get-shape file :blue2) + green-copy (ths/get-shape file :green-copy) + + ;; ==== Action + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + uuid/zero ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id green-copy)}) ;; ids + + file' (thf/apply-changes file changes) + + ;; ==== Get + blue2' (ths/get-shape file' :blue2)] + + ;; ==== Check + + ;; blue2 had swap-id before move + (t/is (some? (ctk/get-swap-slot blue2))) + + ;; blue1still has swap-id after move + (t/is (some? blue2')) + (t/is (some? (ctk/get-swap-slot blue2'))))) + +(t/deftest test-remove-swap-slot-relocating-green-copy-to-b2 + (let [;; ==== Setup + file (setup-file) + + page (thf/current-page file) + blue2 (ths/get-shape file :blue2) + green-copy (ths/get-shape file :green-copy) + b2 (ths/get-shape file :frame-b2) + + ;; ==== Action + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + (:id b2) ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id green-copy)}) ;; ids + + file' (thf/apply-changes file changes) + + ;; ==== Get + blue2' (ths/get-shape file' :blue2)] + + ;; ==== Check + + ;; blue2 had swap-id before move + (t/is (some? (ctk/get-swap-slot blue2))) + + ;; blue1still has swap-id after move + (t/is (some? blue2')) + (t/is (some? (ctk/get-swap-slot blue2'))))) + +(t/deftest test-remove-swap-slot-duplicating-green-copy + (let [;; ==== Setup + file (setup-file) + + page (thf/current-page file) + green-copy (ths/get-shape file :green-copy) + + ;; ==== Action + changes (-> (pcb/empty-changes nil) + (cll/generate-duplicate-changes (:objects page) ;; objects + page ;; page + #{(:id green-copy)} ;; ids + (gpt/point 0 0) ;; delta + {(:id file) file} ;; libraries + (:data file) ;; library-data + (:id file)) ;; file-id + (cll/generate-duplicate-changes-update-indices (:objects page) ;; objects + #{(:id green-copy)})) ;; ids + + + + file' (thf/apply-changes file changes) + + ;; ==== Get + page' (thf/current-page file') + blue1' (ths/get-shape file' :blue1) + green-copy' (ths/get-shape file :green-copy) + duplicated-green-copy' (find-duplicated-shape green-copy' page') + duplicated-blue1-id' (-> duplicated-green-copy' + :shapes + first) + duplicated-blue1' (get (:objects page') duplicated-blue1-id')] + + ;; ==== Check + + ;; blue1 has swap-id + (t/is (some? (ctk/get-swap-slot blue1'))) + + ;; duplicated-blue1 also has swap-id + (t/is (some? duplicated-blue1')) + (t/is (some? (ctk/get-swap-slot duplicated-blue1'))))) + +(t/deftest test-swap-outside-component-doesnt-have-swap-slot + (let [;; ==== Setup + file (setup-file) + ;; ==== Action + + file' (-> file + (thc/instantiate-component :red :red-copy1) + (thc/component-swap :red-copy1 :blue :blue-copy1)) + + ;; ==== Get + blue-copy1' (ths/get-shape file' :blue-copy1)] + + ;; ==== Check + + ;; blue-copy1 has not swap-id + (t/is (some? blue-copy1')) + (t/is (nil? (ctk/get-swap-slot blue-copy1'))))) + +(t/deftest test-remove-swap-slot-detach + (let [;; ==== Setup + file (setup-file) + + page (thf/current-page file) + green-copy (ths/get-shape file :green-copy) + blue2 (ths/get-shape file :blue2) + + ;; ==== Action + changes (cll/generate-detach-component (pcb/empty-changes) + (:id green-copy) + (:data file) + (:id page) + {(:id file) file}) + file' (thf/apply-changes file changes) + + ;; ==== Get + blue2' (ths/get-shape file' :blue2)] + + ;; ==== Check + + ;; blue2 had swap-id before move + (t/is (some? (ctk/get-swap-slot blue2))) + + ;; blue2' has not swap-id after move + (t/is (some? blue2')) + (t/is (nil? (ctk/get-swap-slot blue2'))))) diff --git a/common/test/common_tests/logic/comp_reset_test.cljc b/common/test/common_tests/logic/comp_reset_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..1c663917e97d7ebae8a3a60c434a6fc0a1d64a4d --- /dev/null +++ b/common/test/common_tests/logic/comp_reset_test.cljc @@ -0,0 +1,359 @@ +;; 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 common-tests.logic.comp-reset-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-reset-after-changing-attribute + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :main-child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")} + :copy-root-params {:children-labels [:copy-child]})) + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#abcdef")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-reset-from-library + (let [;; ==== Setup + library (-> (thf/sample-file :library :is-shared true) + (tho/add-simple-component :component1 :main-root :main-child + :child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + + file (-> (thf/sample-file :file) + (thc/instantiate-component :component1 :copy-root + :library library + :children-labels [:copy-child])) + + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf + (:id library) library} + page-mdf + (:id copy-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#abcdef")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-reset-after-adding-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action + ;; will not have any effect, and so the parent shape won't also be touched. + changes (cls/generate-relocate (pcb/empty-changes) + (:objects page) + (thi/id :copy-root) ; parent-id + (:id page) ; page-id + 0 ; to-index + #{(thi/id :free-shape)}) ; ids + + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-reset-after-deleting-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]})) + + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action will not + ;; delete the child shape, but hide it (thus setting the visibility group). + [_all-parents changes] + (cls/generate-delete-shapes (pcb/empty-changes) + file + page + (:objects page) + #{(:id copy-child)} + {:components-v2 true}) + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-reset-after-moving-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy :component1 + :main-root + [:main-child1 :main-child2 :main-child3] + :copy-root + :copy-root-params {:children-labels [:copy-child]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + copy-child1 (ths/get-shape file :copy-child) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action + ;; will not have any effect, and so the parent shape won't also be touched. + changes (cls/generate-relocate (pcb/empty-changes) + (:objects page) + (thi/id :copy-root) ; parent-id + (:id page) ; page-id + 2 ; to-index + #{(:id copy-child1)}) ; ids + + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-reset-after-changing-upper + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :main2-root-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + page (thf/current-page file) + copy2-root (ths/get-shape file :copy2-root) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy2-root)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy2-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + fills' (:fills copy2-root') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#abcdef")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)))) + +(t/deftest test-reset-after-changing-lower + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :copy2-root-params {:children-labels [:copy2-child]})) + page (thf/current-page file) + copy2-root (ths/get-shape file :copy2-root) + copy2-child (ths/get-shape file :copy2-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy2-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file-mdf (thf/apply-changes file changes) + page-mdf (thf/current-page file-mdf) + + changes (cll/generate-reset-component (pcb/empty-changes) + file-mdf + {(:id file-mdf) file-mdf} + page-mdf + (:id copy2-root) + true) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + copy2-child' (ths/get-shape file' :copy2-child) + fills' (:fills copy2-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (some? copy2-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#FFFFFF")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)) + (t/is (= (:touched copy2-child') nil)))) \ No newline at end of file diff --git a/common/test/common_tests/logic/comp_sync_test.cljc b/common/test/common_tests/logic/comp_sync_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..94f093ff38f492a4534845967f6d286307f88c1e --- /dev/null +++ b/common/test/common_tests/logic/comp_sync_test.cljc @@ -0,0 +1,494 @@ +;; 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 common-tests.logic.comp-sync-test + (:require + [app.common.data :as d] + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.shape-tree :as ctst] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-sync-when-changing-attribute + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :main-child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")} + :copy-root-params {:children-labels [:copy-child]})) + page (thf/current-page file) + main-child (ths/get-shape file :main-child) + + ;; ==== Action + changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id main-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-sync-when-changing-attribute-from-library + (let [;; ==== Setup + library (-> (thf/sample-file :file1) + (tho/add-simple-component :component1 + :main-root + :main-child + :copy-root + :main-child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + + file (-> (thf/sample-file :file) + (thc/instantiate-component :component1 :copy-root + :library library + :children-labels [:copy-child])) + + page (thf/current-page library) + main-child (ths/get-shape library :main-child) + + ;; ==== Action + changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id main-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + updated-library (thf/apply-changes library changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id file) + (thi/id :component1) + (:id updated-library) + {(:id updated-library) updated-library + (:id file) file} + (:id file)) + + file' (thf/apply-changes file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-sync-when-changing-attribute-preserve-touched + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :main-child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")} + :copy-root-params {:children-labels [:copy-child]})) + page (thf/current-page file) + main-child (ths/get-shape file :main-child) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + changes1 (-> (pcb/empty-changes nil (:id page)) + (cls/generate-update-shapes + #{(:id copy-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#aaaaaa"))) + (:objects page) + {}) + (cls/generate-update-shapes + #{(:id main-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {})) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#aaaaaa")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') #{:fill-group})))) + +(t/deftest test-sync-when-adding-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + main-root (ths/get-shape file :main-root) + + ;; ==== Action + changes1 (cls/generate-relocate (pcb/empty-changes) + (:objects page) + (thi/id :main-root) ; parent-id + (:id page) ; page-id + 0 ; to-index + #{(thi/id :free-shape)}) ; ids + + + + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + main-free-shape' (ths/get-shape file' :free-shape) + copy-root' (ths/get-shape file' :copy-root) + copy-new-child-id' (d/seek #(not= % (thi/id :copy-child)) (:shapes copy-root')) + copy-new-child' (ths/get-shape-by-id file' copy-new-child-id')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-new-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-new-child') nil)) + (t/is (ctst/parent-of? copy-root' copy-new-child')) + (t/is (ctk/is-main-of? main-free-shape' copy-new-child' true)))) + +(t/deftest test-sync-when-deleting-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]})) + + page (thf/current-page file) + main-child (ths/get-shape file :main-child) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action will not + ;; delete the child shape, but hide it (thus setting the visibility group). + [_all-parents changes1] + (cls/generate-delete-shapes (pcb/empty-changes) + file + page + (:objects page) + #{(:id main-child)} + {:components-v2 true}) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (nil? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (empty? (:shapes copy-root'))))) + +(t/deftest test-sync-when-moving-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy :component1 + :main-root + [:main-child1 :main-child2 :main-child3] + :copy-root + :copy-root-params {:children-labels [:copy-child1 + :copy-child2 + :copy-child3]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + main-child1 (ths/get-shape file :main-child1) + + ;; ==== Action + changes1 (cls/generate-relocate (pcb/empty-changes) + (:objects page) + (thi/id :main-root) ; parent-id + (:id page) ; page-id + 2 ; to-index + #{(:id main-child1)}) ; ids + + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child1' (ths/get-shape file' :copy-child1) + copy-child2' (ths/get-shape file' :copy-child2) + copy-child3' (ths/get-shape file' :copy-child3)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child1')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child1') nil)) + (t/is (= (:touched copy-child2') nil)) + (t/is (= (:touched copy-child3') nil)) + (t/is (= (second (:shapes copy-root')) (:id copy-child1'))) + (t/is (= (first (:shapes copy-root')) (:id copy-child2'))) + (t/is (= (nth (:shapes copy-root') 2) (:id copy-child3'))))) + +(t/deftest test-sync-when-changing-upper + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :main2-root-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + page (thf/current-page file) + main2-root (ths/get-shape file :main2-root) + + ;; ==== Action + changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id main2-root)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component2) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + fills' (:fills copy2-root') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)))) + +(t/deftest test-sync-when-changing-lower-near + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :copy2-root-params {:children-labels [:copy2-child]})) + page (thf/current-page file) + main2-nested-head (ths/get-shape file :main2-nested-head) + + ;; ==== Action + changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id main2-nested-head)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component2) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + file' (thf/apply-changes updated-file changes2) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + copy2-child' (ths/get-shape file' :copy2-child) + fills' (:fills copy2-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (some? copy2-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)) + (t/is (= (:touched copy2-child') nil)))) + +(t/deftest test-sync-when-changing-lower-remote + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :copy2-root-params {:children-labels [:copy2-child]})) + page (thf/current-page file) + main1-root (ths/get-shape file :main1-root) + + ;; ==== Action + changes1 (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id main1-root)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + updated-file (thf/apply-changes file changes1) + + changes2 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id updated-file) + (thi/id :component1) + (:id updated-file) + {(:id updated-file) updated-file} + (:id updated-file)) + + synced-file (thf/apply-changes updated-file changes2) + + changes3 (cll/generate-sync-file-changes (pcb/empty-changes) + nil + :components + (:id synced-file) + (thi/id :component2) + (:id synced-file) + {(:id synced-file) synced-file} + (:id synced-file)) + + file' (thf/apply-changes synced-file changes3) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + copy2-child' (ths/get-shape file' :copy2-child) + fills' (:fills copy2-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (some? copy2-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)) + (t/is (= (:touched copy2-child') nil)))) \ No newline at end of file diff --git a/common/test/common_tests/logic/comp_touched_test.cljc b/common/test/common_tests/logic/comp_touched_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..1f16a21070160b0bed14632124f7f39b9ddc3d01 --- /dev/null +++ b/common/test/common_tests/logic/comp_touched_test.cljc @@ -0,0 +1,330 @@ +;; 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 common-tests.logic.comp-touched-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-touched-when-changing-attribute + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :main-child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")} + :copy-root-params {:children-labels [:copy-child]})) + page (thf/current-page file) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') #{:fill-group})))) + +(t/deftest test-touched-from-library + (let [;; ==== Setup + library (-> (thf/sample-file :library :is-shared true) + (tho/add-simple-component :component1 :main-root :main-child + :child-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + + file (-> (thf/sample-file :file) + (thc/instantiate-component :component1 :copy-root + :library library + :children-labels [:copy-child])) + + page (thf/current-page file) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child) + fills' (:fills copy-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') #{:fill-group})))) + +(t/deftest test-not-touched-when-adding-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + copy-root (ths/get-shape file :copy-root) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action + ;; will not have any effect, and so the parent shape won't also be touched. + changes (cls/generate-relocate (pcb/empty-changes) + (:objects page) + (thi/id :copy-root) ; parent-id + (:id page) ; page-id + 0 ; to-index + #{(thi/id :free-shape)}) ; ids + + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') nil)))) + +(t/deftest test-not-touched-when-deleting-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 + :main-root + :main-child + :copy-root + :copy-root-params {:children-labels [:copy-child]})) + + page (thf/current-page file) + copy-child (ths/get-shape file :copy-child) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action will not + ;; delete the child shape, but hide it (thus setting the visibility group). + [_all-parents changes] + (cls/generate-delete-shapes (pcb/empty-changes) + file + page + (:objects page) + #{(:id copy-child)} + {:components-v2 true}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child' (ths/get-shape file' :copy-child)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child') #{:visibility-group})))) + +(t/deftest test-not-touched-when-moving-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-component-with-many-children-and-copy :component1 + :main-root + [:main-child1 :main-child2 :main-child3] + :copy-root + :copy-root-params {:children-labels [:copy-child1 + :copy-child2 + :copy-child3]}) + (ths/add-sample-shape :free-shape)) + + page (thf/current-page file) + copy-child1 (ths/get-shape file :copy-child1) + + ;; ==== Action + + ;; IMPORTANT: as modifying copies structure is now forbidden, this action + ;; will not have any effect, and so the parent shape won't also be touched. + changes (cls/generate-relocate (pcb/empty-changes) + (:objects page) + (thi/id :copy-root) ; parent-id + (:id page) ; page-id + 2 ; to-index + #{(:id copy-child1)}) ; ids + + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy-root' (ths/get-shape file' :copy-root) + copy-child1' (ths/get-shape file' :copy-child1) + copy-child2' (ths/get-shape file' :copy-child2) + copy-child3' (ths/get-shape file' :copy-child3)] + + ;; ==== Check + (t/is (some? copy-root')) + (t/is (some? copy-child1')) + (t/is (= (:touched copy-root') nil)) + (t/is (= (:touched copy-child1') nil)) + (t/is (= (:touched copy-child2') nil)) + (t/is (= (:touched copy-child3') nil)) + (t/is (= (first (:shapes copy-root')) (:id copy-child1'))) + (t/is (= (second (:shapes copy-root')) (:id copy-child2'))) + (t/is (= (nth (:shapes copy-root') 2) (:id copy-child3'))))) + +(t/deftest test-touched-when-changing-upper + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :main2-root-params {:fills (ths/sample-fills-color + :fill-color "#abcdef")})) + page (thf/current-page file) + copy2-root (ths/get-shape file :copy2-root) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy2-root)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + fills' (:fills copy2-root') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') #{:fill-group})))) + +(t/deftest test-touched-when-changing-lower + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :copy2-root-params {:children-labels [:copy2-child]})) + page (thf/current-page file) + copy2-child (ths/get-shape file :copy2-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy2-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + copy2-child' (ths/get-shape file' :copy2-child) + fills' (:fills copy2-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (some? copy2-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)) + (t/is (= (:touched copy2-child') #{:fill-group})))) + +(t/deftest test-touched-when-changing-lower + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-nested-component-with-copy :component1 + :main1-root + :main1-child + :component2 + :main2-root + :main2-nested-head + :copy2-root + :copy2-root-params {:children-labels [:copy2-child]})) + page (thf/current-page file) + copy2-child (ths/get-shape file :copy2-child) + + ;; ==== Action + changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page)) + #{(:id copy2-child)} + (fn [shape] + (assoc shape :fills (ths/sample-fills-color :fill-color "#fabada"))) + (:objects page) + {}) + + file' (thf/apply-changes file changes) + + ;; ==== Get + copy2-root' (ths/get-shape file' :copy2-root) + copy2-child' (ths/get-shape file' :copy2-child) + fills' (:fills copy2-child') + fill' (first fills')] + + ;; ==== Check + (t/is (some? copy2-root')) + (t/is (some? copy2-child')) + (t/is (= (count fills') 1)) + (t/is (= (:fill-color fill') "#fabada")) + (t/is (= (:fill-opacity fill') 1)) + (t/is (= (:touched copy2-root') nil)) + (t/is (= (:touched copy2-child') #{:fill-group})))) \ No newline at end of file diff --git a/common/test/common_tests/logic/copying_and_duplicating_test.cljc b/common/test/common_tests/logic/copying_and_duplicating_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..c5ab6287cf1efee1cb3af1bc6b233f5937e19687 --- /dev/null +++ b/common/test/common_tests/logic/copying_and_duplicating_test.cljc @@ -0,0 +1,122 @@ +;; 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 common-tests.logic.copying-and-duplicating-test + (:require + [app.common.files.changes :as ch] + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.pprint :as pp] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(defn- setup [] + (-> (thf/sample-file :file1) + (tho/add-simple-component :simple-1 :frame-simple-1 :rect-simple-1 + :child-params {:type :rect :fills (ths/sample-fills-color :fill-color "#2152e5") :name "rect-simple-1"}) + + (tho/add-frame :frame-composed-1 :name "frame-composed-1") + (thc/instantiate-component :simple-1 :copy-simple-1 :parent-label :frame-composed-1 :children-labels [:composed-1-simple-1]) + (ths/add-sample-shape :rect-composed-1 :parent-label :frame-composed-1 :fills (ths/sample-fills-color :fill-color "#B1B2B5")) + (thc/make-component :composed-1 :frame-composed-1) + + (tho/add-frame :frame-composed-2 :name "frame-composed-2") + (thc/instantiate-component :composed-1 :copy-composed-1-composed-2 :parent-label :frame-composed-2 :children-labels [:composed-1-composed-2]) + (thc/make-component :composed-2 :frame-composed-2) + + (thc/instantiate-component :composed-2 :copy-composed-2) + + (tho/add-frame :frame-composed-3 :name "frame-composed-3") + (tho/add-group :group-3 :parent-label :frame-composed-3) + (thc/instantiate-component :composed-2 :copy-composed-1-composed-3 :parent-label :group-3 :children-labels [:composed-1-composed-2]) + (ths/add-sample-shape :circle-composed-3 :parent-label :group-3 :fills (ths/sample-fills-color :fill-color "#B1B2B5")) + (thc/make-component :composed-3 :frame-composed-3) + + (thc/instantiate-component :composed-3 :copy-composed-3 :children-labels [:composed-2-composed-3]))) + +(defn- propagate-all-component-changes [file] + (-> file + (tho/propagate-component-changes :simple-1) + (tho/propagate-component-changes :composed-1) + (tho/propagate-component-changes :composed-2) + (tho/propagate-component-changes :composed-3))) + +(defn- count-shapes [file name color] + (let [page (thf/current-page file)] + (->> (vals (:objects page)) + (filter #(and + (= (:name %) name) + (-> (ths/get-shape-by-id file (:id %)) + :fills + first + :fill-color + (= color)))) + (count)))) + +(defn- validate [file validator] + (validator file) + file) + +;; Related .penpot file: common/test/cases/copying-and-duplicating.penpot +(t/deftest main-and-first-level-copy + (-> (setup) + ;; For each main and first level copy: + ;; - Duplicate it two times. + (tho/duplicate-shape :frame-simple-1) + (tho/duplicate-shape :frame-simple-1) + (tho/duplicate-shape :frame-composed-1) + (tho/duplicate-shape :frame-composed-1) + (tho/duplicate-shape :frame-composed-2) + (tho/duplicate-shape :frame-composed-2) + (tho/duplicate-shape :frame-composed-3) + (tho/duplicate-shape :frame-composed-3) + (tho/duplicate-shape :copy-composed-2) + (tho/duplicate-shape :copy-composed-2) + (tho/duplicate-shape :copy-composed-3) + (tho/duplicate-shape :copy-composed-3) + + ;; - Change color of Simple1 and check propagation to all copies. + (tho/update-bottom-color :frame-simple-1 "#111111" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (count-shapes % "rect-simple-1" "#111111") 18))) + ;; - Change color of the nearest main and check propagation to duplicated. + (tho/update-bottom-color :frame-composed-1 "#222222" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (count-shapes % "rect-simple-1" "#222222") 15))) + (tho/update-bottom-color :frame-composed-2 "#333333" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (count-shapes % "rect-simple-1" "#333333") 12))) + (tho/update-bottom-color :frame-composed-3 "#444444" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (count-shapes % "rect-simple-1" "#444444") 6))))) + +(t/deftest copy-nested-in-main + (-> (setup) + ;; For each copy of Simple1 nested in a main, and the group inside Composed3 main: + ;; - Duplicate it two times, keeping the duplicated inside the same main. + (tho/duplicate-shape :copy-simple-1) + (tho/duplicate-shape :copy-simple-1) + (tho/duplicate-shape :group-3) + (tho/duplicate-shape :group-3) + + ;; - Change color of Simple1 and check propagation to all copies. + (tho/update-bottom-color :frame-simple-1 "#111111" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (count-shapes % "rect-simple-1" "#111111") 28))) + + ;; - Change color of the nearest main and check propagation to duplicated. + (tho/update-bottom-color :frame-composed-1 "#222222" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (count-shapes % "rect-simple-1" "#222222") 9))) + + ;; - Change color of the copy you duplicated from, and check that it's NOT PROPAGATED. + (tho/update-bottom-color :group-3 "#333333" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (count-shapes % "rect-simple-1" "#333333") 2))))) diff --git a/common/test/common_tests/logic/duplicated_pages_test.cljc b/common/test/common_tests/logic/duplicated_pages_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..d1bafb88d7e384ce5c308fe0769b48c6234c3ae3 --- /dev/null +++ b/common/test/common_tests/logic/duplicated_pages_test.cljc @@ -0,0 +1,117 @@ +;; 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 common-tests.logic.duplicated-pages-test + (:require + [app.common.files.changes :as ch] + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.pprint :as pp] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +;; Related .penpot file: common/test/cases/duplicated-pages.penpot +(t/deftest test-propagation-with-anidated-components + (letfn [(setup [] + (-> (thf/sample-file :file1 :page-label :page-1) + (tho/add-frame :frame-ellipse-1 :fills []) + (ths/add-sample-shape :ellipse-shape-1 :parent-label :frame-ellipse-1 :fills (ths/sample-fills-color :fill-color "#204fdc")) + (thc/make-component :ellipse-1 :frame-ellipse-1) + + (tho/add-frame :frame-ellipse-2 :fills []) + (ths/add-sample-shape :ellipse-shape-2 :parent-label :frame-ellipse-2 :fills (ths/sample-fills-color :fill-color "#dc3020")) + (thc/make-component :ellipse-2 :frame-ellipse-2) + + (tho/add-frame :frame-ellipse-3 :fills []) + (ths/add-sample-shape :ellipse-shape-3 :parent-label :frame-ellipse-3 :fills (ths/sample-fills-color :fill-color "#d8dc20")) + (thc/make-component :ellipse-3 :frame-ellipse-3) + + (tho/add-frame :frame-board-1 :fills (ths/sample-fills-color :fill-color "#FFFFFF")) + (thc/instantiate-component :ellipse-1 :copy-ellipse-1 :parent-label :frame-board-1 :children-labels [:ellipse-shape-1-board-1]) + (thc/make-component :board-1 :frame-board-1) + + (thf/add-sample-page :page-2) + (tho/add-frame :frame-board-2 :fills (ths/sample-fills-color :fill-color "#FFFFFF")) + (thc/instantiate-component :board-1 :copy-board-1 :parent-label :frame-board-2 :children-labels [:board-1-board-2]) + (thc/make-component :board-2 :frame-board-2) + + (thf/add-sample-page :page-3) + (tho/add-frame :frame-board-3 :fills (ths/sample-fills-color :fill-color "#FFFFFF")) + (thc/instantiate-component :board-2 :copy-board-2 :parent-label :frame-board-3 :children-labels [:board-2-board-3]) + (thc/make-component :board-3 :frame-board-3))) + + (propagate-all-component-changes [file] + (-> file + (tho/propagate-component-changes :ellipse-1) + (tho/propagate-component-changes :ellipse-2) + (tho/propagate-component-changes :ellipse-3) + (tho/propagate-component-changes :board-1) + (tho/propagate-component-changes :board-2))) + + (reset-all-overrides [file] + (-> file + (tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1) + (tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2) + (propagate-all-component-changes))) + + (fill-colors [file] + [(tho/bottom-fill-color file :frame-ellipse-1 :page-label :page-1) + (tho/bottom-fill-color file :frame-ellipse-2 :page-label :page-1) + (tho/bottom-fill-color file :frame-ellipse-3 :page-label :page-1) + (tho/bottom-fill-color file :frame-board-1 :page-label :page-1) + (tho/bottom-fill-color file :frame-board-2 :page-label :page-2) + (tho/bottom-fill-color file :frame-board-3 :page-label :page-3) + (-> + (ths/get-shape file :frame-board-1 :page-label :page-1) + :fills + first + :fill-color) + (-> + (ths/get-shape file :copy-board-1 :page-label :page-2) + :fills + first + :fill-color)]) + + (validate [file validator] + (validator file) + file)] + + (-> (setup) + ;; Swap the copy inside main of Board1 to Ellipse2, and see that it propagates to copies in other pages. + (tho/swap-component-in-shape :copy-ellipse-1 :ellipse-2 :page-label :page-1 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#204fdc" "#dc3020" "#d8dc20" "#dc3020" "#dc3020" "#dc3020" "#FFFFFF" "#FFFFFF"]))) + + ;; Change color of Ellipse2 main, and see that it propagates to all copies. + (tho/update-bottom-color :frame-ellipse-2 "#abcdef" :page-label :page-1 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#204fdc" "#abcdef" "#d8dc20" "#abcdef" "#abcdef" "#abcdef" "#FFFFFF" "#FFFFFF"]))) + + ;;Change color of copies of Ellipse2 and see that the override works. + (tho/update-bottom-color :frame-board-1 "#efaade" :page-label :page-1 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#204fdc" "#abcdef" "#d8dc20" "#efaade" "#efaade" "#efaade" "#FFFFFF" "#FFFFFF"]))) + (tho/update-bottom-color :copy-board-1 "#aaefcb" :page-label :page-2 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#204fdc" "#abcdef" "#d8dc20" "#efaade" "#aaefcb" "#aaefcb" "#FFFFFF" "#FFFFFF"]))) + + ;; Reset all overrides. + (reset-all-overrides) + (validate #(t/is (= (fill-colors %) ["#204fdc" "#abcdef" "#d8dc20" "#abcdef" "#abcdef" "#abcdef" "#FFFFFF" "#FFFFFF"]))) + + ;; Swap the copy of Ellipse2 inside copies of Board1 to Ellipse 3. Then make + ;; changes in Board1 main and see that they are not propagated. + (tho/swap-component-in-first-child :copy-board-1 :ellipse-3 :page-label :page-2 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#204fdc" "#abcdef" "#d8dc20" "#abcdef" "#d8dc20" "#d8dc20" "#FFFFFF" "#FFFFFF"]))) + (tho/update-color :frame-board-1 "#fabada" :page-label :page-1 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#204fdc" "#abcdef" "#d8dc20" "#abcdef" "#d8dc20" "#d8dc20" "#fabada" "#fabada"])))))) diff --git a/common/test/common_tests/logic/hide_in_viewer_test.cljc b/common/test/common_tests/logic/hide_in_viewer_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..051a4732eadf4cc558cdce3455b5f97bfec1ac29 --- /dev/null +++ b/common/test/common_tests/logic/hide_in_viewer_test.cljc @@ -0,0 +1,75 @@ +;; 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 common-tests.logic.hide-in-viewer-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.shape.interactions :as ctsi] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + + +(t/deftest test-remove-show-in-view-mode-delete-interactions + (let [;; ==== Setup + + file (-> (thf/sample-file :file1) + (tho/add-frame :frame-dest) + (tho/add-frame :frame-origin) + (ths/add-interaction :frame-origin :frame-dest)) + + frame-origin (ths/get-shape file :frame-origin) + + page (thf/current-page file) + + + ;; ==== Action + changes (-> (pcb/empty-changes nil (:id page)) + (pcb/with-objects (:objects page)) + (pcb/update-shapes [(:id frame-origin)] #(cls/change-show-in-viewer % true))) + file' (thf/apply-changes file changes) + + ;; ==== Get + frame-origin' (ths/get-shape file' :frame-origin)] + + ;; ==== Check + (t/is (some? (:interactions frame-origin))) + (t/is (nil? (:interactions frame-origin'))))) + + + +(t/deftest test-add-new-interaction-updates-show-in-view-mode + (let [;; ==== Setup + + file (-> (thf/sample-file :file1) + (tho/add-frame :frame-dest :hide-in-viewer true) + (tho/add-frame :frame-origin :hide-in-viewer true)) + frame-dest (ths/get-shape file :frame-dest) + frame-origin (ths/get-shape file :frame-origin) + + page (thf/current-page file) + + ;; ==== Action + new-interaction (-> ctsi/default-interaction + (ctsi/set-destination (:id frame-dest)) + (assoc :position-relative-to (:id frame-dest))) + + changes (-> (pcb/empty-changes nil (:id page)) + (pcb/with-objects (:objects page)) + (pcb/update-shapes [(:id frame-origin)] #(cls/add-new-interaction % new-interaction))) + file' (thf/apply-changes file changes) + + ;; ==== Get + frame-origin' (ths/get-shape file' :frame-origin)] + + ;; ==== Check + (t/is (true? (:hide-in-viewer frame-origin))) + (t/is (nil? (:hide-in-viewer frame-origin'))))) diff --git a/common/test/common_tests/logic/move_shapes_test.cljc b/common/test/common_tests/logic/move_shapes_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..f85a72cbeb895067c0e5420e518b526441aa052f --- /dev/null +++ b/common/test/common_tests/logic/move_shapes_test.cljc @@ -0,0 +1,84 @@ +;; 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 common-tests.logic.move-shapes-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.shapes :as cls] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-relocate-shape + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-frame :frame-to-move) + (tho/add-frame :frame-parent)) + + page (thf/current-page file) + frame-to-move (ths/get-shape file :frame-to-move) + frame-parent (ths/get-shape file :frame-parent) + + ;; ==== Action + + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + (:id frame-parent) ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id frame-to-move)}) ;; ids + + file' (thf/apply-changes file changes) + + ;; ==== Get + frame-to-move' (ths/get-shape file' :frame-to-move) + frame-parent' (ths/get-shape file' :frame-parent)] + + ;; ==== Check + ;; frame-to-move has moved + (t/is (= (:parent-id frame-to-move) uuid/zero)) + (t/is (= (:parent-id frame-to-move') (:id frame-parent'))))) + + +(t/deftest test-relocate-shape-out-of-group + (let [;; ==== Setup + file (-> (thf/sample-file :file1) + (tho/add-frame :frame-1) + (tho/add-group :group-1 :parent-label :frame-1) + (ths/add-sample-shape :circle-1 :parent-label :group-1)) + + page (thf/current-page file) + circle (ths/get-shape file :circle-1) + group (ths/get-shape file :group-1) + + ;; ==== Action + + changes (cls/generate-relocate (pcb/empty-changes nil) + (:objects page) + uuid/zero ;; parent-id + (:id page) ;; page-id + 0 ;; to-index + #{(:id circle)}) ;; ids + + + file' (thf/apply-changes file changes) + + ;; ==== Get + circle' (ths/get-shape file' :circle-1) + group' (ths/get-shape file' :group-1)] + + ;; ==== Check + + ;; the circle has moved, and the group is deleted + (t/is (= (:parent-id circle) (:id group))) + (t/is (= (:parent-id circle') uuid/zero)) + (t/is group) + (t/is (nil? group')))) \ No newline at end of file diff --git a/common/test/common_tests/logic/multiple_nesting_levels_test.cljc b/common/test/common_tests/logic/multiple_nesting_levels_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..43b7c7ef0e6c7a270af89127acd071fdfe9f4a1e --- /dev/null +++ b/common/test/common_tests/logic/multiple_nesting_levels_test.cljc @@ -0,0 +1,123 @@ +;; 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 common-tests.logic.multiple-nesting-levels-test + (:require + [app.common.files.changes :as ch] + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.pprint :as pp] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +;; Related .penpot file: common/test/cases/multiple-testing-levels.penpot +(t/deftest test-multiple-nesting-levels + (letfn [(setup [] + (-> (thf/sample-file :file1) + + (tho/add-frame :frame-simple-1) + (ths/add-sample-shape :rectangle :parent-label :frame-simple-1 :fills (ths/sample-fills-color :fill-color "#2152e5")) + (thc/make-component :simple-1 :frame-simple-1) + + (tho/add-frame :frame-simple-2) + (ths/add-sample-shape :circle :parent-label :frame-simple-2 :fills (ths/sample-fills-color :fill-color "#e56d21")) + (thc/make-component :simple-2 :frame-simple-2) + + (tho/add-frame :frame-composed-1) + (thc/instantiate-component :simple-1 :copy-simple-1 :parent-label :frame-composed-1 :children-labels [:simple-1-composed-1]) + (thc/make-component :composed-1 :frame-composed-1) + + (tho/add-frame :frame-composed-2) + (thc/instantiate-component :composed-1 :copy-frame-composed-1 :parent-label :frame-composed-2 :children-labels [:composed-1-composed-2]) + (thc/make-component :composed-2 :frame-composed-2) + + (thc/instantiate-component :composed-2 :copy-frame-composed-2 :children-labels [:composed-1-composed-2-copy]))) + + (propagate-all-component-changes [file] + (-> file + (tho/propagate-component-changes :simple-1) + (tho/propagate-component-changes :simple-2) + (tho/propagate-component-changes :composed-1) + (tho/propagate-component-changes :composed-2))) + + (reset-all-overrides [file] + (-> file + (tho/reset-overrides (ths/get-shape file :copy-simple-1)) + (tho/reset-overrides (ths/get-shape file :copy-frame-composed-1)) + (tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy)) + (propagate-all-component-changes))) + + (fill-colors [file] + [(tho/bottom-fill-color file :frame-simple-1) + (tho/bottom-fill-color file :frame-simple-2) + (tho/bottom-fill-color file :frame-composed-1) + (tho/bottom-fill-color file :frame-composed-2) + (tho/bottom-fill-color file :copy-frame-composed-2)]) + + (validate [file validator] + (validator file) + file)] + (-> (setup) + ;; Change color of Simple1 and see that it's propagated to all copies. + (tho/update-bottom-color :frame-simple-1 "#e521a8" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#e521a8" "#e56d21" "#e521a8" "#e521a8" "#e521a8"]))) + + ;; Override color in copy inside Composed1, Composed2 and the copy + ;; of Composed2 and see in all cases that a change in the main is overriden. + (tho/update-bottom-color :simple-1-composed-1 "#21e59e" :propagate-fn propagate-all-component-changes) + (tho/update-bottom-color :composed-1-composed-2 "#2186e5" :propagate-fn propagate-all-component-changes) + (tho/update-bottom-color :composed-1-composed-2-copy "#e5a221" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#e521a8" "#e56d21" "#21e59e" "#2186e5" "#e5a221"]))) + (tho/update-bottom-color :frame-simple-1 "#b2e521" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#b2e521" "#e56d21" "#21e59e" "#2186e5" "#e5a221"]))) + + ;; Reset all overrides and check again the propagation from mains. + (reset-all-overrides) + (tho/update-bottom-color :frame-simple-1 "#21aae5" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#21aae5" "#e56d21" "#21aae5" "#21aae5" "#21aae5"]))) + + ;; Swap in Composed1 to Simple2 and see that it propagates ok. + (tho/swap-component-in-shape :copy-simple-1 :simple-2 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#21aae5" "#e56d21" "#e56d21" "#e56d21" "#e56d21"]))) + + ;; Change color of Simple 2 and see that it propagates ok. + (tho/update-bottom-color :frame-simple-2 "#c321e5" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#21aae5" "#c321e5" "#c321e5" "#c321e5" "#c321e5"]))) + + ;; Swap Simple 2 copy in Composed2. Check propagations. + (tho/swap-component-in-first-child :copy-frame-composed-1 :simple-1 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#21aae5" "#c321e5" "#c321e5" "#21aae5" "#21aae5"]))) + + ;; Change color of Simple 1 and check propagation. + (tho/update-bottom-color :frame-simple-1 "#e521a8" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#e521a8" "#c321e5" "#c321e5" "#e521a8" "#e521a8"]))) + + ;; Reset overrides in Composed2 main, and swap Simple 2 copy in + ;; Composed2 copy. Change color of Simple 2 and check propatagion. + (tho/reset-overrides-in-first-child :copy-frame-composed-1 :propagate-fn propagate-all-component-changes) + (tho/swap-component-in-first-child :composed-1-composed-2-copy :simple-1 :propagate-fn propagate-all-component-changes) + (tho/update-bottom-color :frame-simple-2 "#21e55d" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#e521a8" "#21e55d" "#21e55d" "#21e55d" "#e521a8"]))) + + ;; Swap all of three (Composed 1, Composed2 and copy of Composed2) + ;; and check propagations from Simple mains. + (tho/swap-component-in-first-child :frame-composed-1 :simple-1 :propagate-fn propagate-all-component-changes) + (tho/swap-component-in-first-child :copy-frame-composed-1 :simple-2 :propagate-fn propagate-all-component-changes) + (tho/swap-component-in-first-child :composed-1-composed-2-copy :simple-2 :propagate-fn propagate-all-component-changes) + (tho/update-bottom-color :frame-simple-1 "#111111" :propagate-fn propagate-all-component-changes) + (tho/update-bottom-color :frame-simple-2 "#222222" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#111111" "#222222" "#111111" "#222222" "#222222"])))))) diff --git a/common/test/common_tests/logic/swap_and_reset_test.cljc b/common/test/common_tests/logic/swap_and_reset_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..0bb3e95ba01f3f8dc72cf50dbcad60dca57fab15 --- /dev/null +++ b/common/test/common_tests/logic/swap_and_reset_test.cljc @@ -0,0 +1,84 @@ +;; 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 common-tests.logic.swap-and-reset-test + (:require + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.file :as ctf] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +;; Related .penpot file: common/test/cases/swap-and-reset.penpot +(t/deftest test-swap-and-reset-override + (letfn [(setup [] + (-> (thf/sample-file :file1) + + (tho/add-frame :frame-rectangle) + (ths/add-sample-shape :rectangle-shape :parent-label :frame-rectangle :type :rect) + (thc/make-component :rectangle :frame-rectangle) + + (tho/add-frame :frame-circle) + (ths/add-sample-shape :circle :parent-label :frame-circle :type :circle) + (thc/make-component :circle :frame-circle) + + (tho/add-frame :frame-main) + (thc/instantiate-component :rectangle :copy-rectangle :parent-label :frame-main :children-labels [:copy-rectangle-shape]) + (thc/make-component :main :frame-main) + + (thc/instantiate-component :main :copy :children-labels [:copy-copy-rectangle]))) + + (copy-type [file] + (:type (tho/bottom-shape file :copy))) + + (nested-component-id [file] + (->> + (ths/get-shape file :copy) + :shapes + first + (ths/get-shape-by-id file) + (:component-id))) + + (nested-swap-slot [file] + (->> + (ths/get-shape file :copy) + :shapes + first + (ths/get-shape-by-id file) + (ctk/get-swap-slot))) + + (circle-component-id [file] + (:id (thc/get-component file :circle))) + + (rectangle-component-id [file] + (:id (thc/get-component file :rectangle))) + + (copy-rectangle-id [file] + (:id (ths/get-shape file :copy-rectangle))) + + (validate [file validator] + (validator file) + file)] + + (-> (setup) + ;; Select the Rectangle inside Copy and swap it for an Ellipse + (tho/swap-component-in-shape :copy-copy-rectangle :circle) + (validate #(t/is (= (copy-type %) :circle))) + (validate #(t/is (= (nested-component-id %) (circle-component-id %)))) + (validate #(t/is (= (copy-rectangle-id %) (nested-swap-slot %)))) + + ;; Do a "Reset override" on the newly created Ellipse. It should swap for a Rectangle + (tho/reset-overrides-in-first-child :copy) + (validate #(t/is (= (copy-type %) :rect))) + (validate #(t/is (= (nested-component-id %) (rectangle-component-id %)))) + (validate #(t/is (nil? (nested-swap-slot %))))))) diff --git a/common/test/common_tests/logic/swap_as_override_test.cljc b/common/test/common_tests/logic/swap_as_override_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..a4a1b5a6324ebcb361cd10f4caa3e6ddd91e9fa2 --- /dev/null +++ b/common/test/common_tests/logic/swap_as_override_test.cljc @@ -0,0 +1,141 @@ +;; 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 common-tests.logic.swap-as-override-test + (:require + [app.common.files.changes :as ch] + [app.common.files.changes-builder :as pcb] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] + [app.common.pprint :as pp] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [app.common.types.container :as ctn] + [app.common.types.file :as ctf] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(defn- setup [] + (-> (thf/sample-file :file1) + + (tho/add-simple-component :component-1 :frame-component-1 :child-component-1 :child-params {:name "child-component-1" :type :rect :fills (ths/sample-fills-color :fill-color "#111111")}) + (tho/add-simple-component :component-2 :frame-component-2 :child-component-2 :child-params {:name "child-component-2" :type :rect :fills (ths/sample-fills-color :fill-color "#222222")}) + (tho/add-simple-component :component-3 :frame-component-3 :child-component-3 :child-params {:name "child-component-3" :type :rect :fills (ths/sample-fills-color :fill-color "#333333")}) + + (tho/add-frame :frame-icon-and-text) + (thc/instantiate-component :component-1 :copy-component-1 :parent-label :frame-icon-and-text :children-labels [:component-1-icon-and-text]) + (ths/add-sample-shape :text + {:type :text + :name "icon+text" + :parent-label :frame-icon-and-text}) + (thc/make-component :icon-and-text :frame-icon-and-text) + + (tho/add-frame :frame-panel) + (thc/instantiate-component :icon-and-text :copy-icon-and-text :parent-label :frame-panel :children-labels [:icon-and-text-panel]) + (thc/make-component :panel :frame-panel) + + (thc/instantiate-component :panel :copy-panel :children-labels [:copy-icon-and-text-panel]))) + +(defn- propagate-all-component-changes [file] + (-> file + (tho/propagate-component-changes :icon-and-text) + (tho/propagate-component-changes :panel))) + +(defn- fill-colors [file] + [(tho/bottom-fill-color file :frame-icon-and-text) + (tho/bottom-fill-color file :frame-panel) + (tho/bottom-fill-color file :copy-panel)]) + +(defn- validate [file validator] + (validator file) + file) + +;; Related .penpot file: common/test/cases/swap-as-override.penpot +(t/deftest swap-main-then-copy + (-> (setup) + ;; Swap icon in icon+text main. Check that it propagates to copies. + (tho/swap-component-in-shape :copy-component-1 :component-2 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#222222" "#222222" "#222222"]))) + + ;; Change color of icon in icon+text main. Check that it propagates to copies. + (tho/update-bottom-color :frame-icon-and-text "#333333" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#333333" "#333333" "#333333"]))) + + ;; Swap icon inside panel main. Check it propagates to panel copy. + (tho/swap-component-in-first-child :copy-icon-and-text :component-1 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#333333" "#111111" "#111111"]))) + + ;; Change color of icon in icon+text. Check that it does not propagate. + (tho/update-bottom-color :frame-icon-and-text "#444444" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#444444" "#111111" "#111111"]))) + + ;; Change color of icon in panel main. Check that it propagates. + (tho/update-bottom-color :frame-panel "#555555" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#444444" "#555555" "#555555"]))))) + +(t/deftest swap-copy-then-main + (-> (setup) + ;; Swap icon inside panel main. Check that it propagates to panel copy. + (tho/swap-component-in-first-child :copy-icon-and-text :component-2 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#111111" "#222222" "#222222"]))) + + ;; Change color of icon in icon+text. Check that it does not propagate. + (tho/update-bottom-color :frame-icon-and-text "#333333" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#333333" "#222222" "#222222"]))) + + ;;Change color of icon in panel main. Check that it propagates + (tho/update-bottom-color :frame-panel "#444444" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#333333" "#444444" "#444444"]))) + + ;; Swap icon in icon+text main. Check that it does not propagate. + (tho/swap-component-in-shape :copy-component-1 :component-2 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#222222" "#444444" "#444444"]))) + + ;; Change color of icon in icon+text. Check that it does not propagate. + (tho/update-bottom-color :frame-icon-and-text "#555555" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#555555" "#444444" "#444444"]))))) + +(t/deftest swap-copy-then-2nd-copy + (-> (setup) + ;; Swap icon inside panel main. Check that it propagates to panel copy. + (tho/swap-component-in-first-child :copy-icon-and-text :component-2 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#111111" "#222222" "#222222"]))) + + ;; Swap icon inside panel copy. + (tho/swap-component-in-first-child :copy-icon-and-text-panel :component-1 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#111111" "#222222" "#111111"]))) + + ;; Change color of icon in icon+text. Check that it does not propagate. + (tho/update-bottom-color :frame-icon-and-text "#333333" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#333333" "#222222" "#111111"]))) + + ;; Change color of icon in panel main. Check that it does not propagate. + (tho/update-bottom-color :frame-panel "#444444" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#333333" "#444444" "#111111"]))))) + +(t/deftest swap-2nd-copy-then-copy + (-> (setup) + ;; Swap icon inside panel copy + (tho/swap-component-in-first-child :copy-icon-and-text-panel :component-2 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#111111" "#111111" "#222222"]))) + + ;; Swap icon inside panel main. Check that it does not propagate. + (tho/swap-component-in-first-child :copy-icon-and-text :component-3 :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#111111" "#333333" "#222222"]))) + + ;; Change color of icon in icon+text. Check that it does not propagate. + (tho/update-bottom-color :frame-icon-and-text "#444444" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#444444" "#333333" "#222222"]))) + + ;; Change color of icon in panel main. Check that it does not propagate. + (tho/update-bottom-color :frame-panel "#555555" :propagate-fn propagate-all-component-changes) + (validate #(t/is (= (fill-colors %) ["#444444" "#555555" "#222222"]))))) diff --git a/common/test/common_tests/record_test.cljc b/common/test/common_tests/record_test.cljc index 64532078b5176b259fa34194be5a08c9facc3708..cbf62d021f3d6e38d821c0ab107d6c05b93f231e 100644 --- a/common/test/common_tests/record_test.cljc +++ b/common/test/common_tests/record_test.cljc @@ -27,7 +27,6 @@ (t/testing "unknown assoc" (let [o (assoc o :c 176)] - (prn o) (t/is (= 1 (:a o))) (t/is (= 2 (:b o))) (t/is (= 176 (:c o))))) diff --git a/common/test/common_tests/schema_test.cljc b/common/test/common_tests/schema_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..05b2c2ae65fad80c9d373432106f60e704391efe --- /dev/null +++ b/common/test/common_tests/schema_test.cljc @@ -0,0 +1,41 @@ +;; 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 common-tests.schema-test + (:require + [app.common.schema :as sm] + [app.common.schema.generators :as sg] + [clojure.test :as t])) + +(t/deftest test-set-of-email + (t/testing "decoding" + (let [candidate1 "a@b.com a@c.net" + schema [::sm/set ::sm/email] + result1 (sm/decode schema candidate1 sm/string-transformer) + result2 (sm/decode schema candidate1 sm/json-transformer)] + (t/is (= result1 #{"a@b.com" "a@c.net"})) + (t/is (= result2 #{"a@b.com" "a@c.net"})))) + + (t/testing "encoding" + (let [candidate #{"a@b.com" "a@c.net"} + schema [::sm/set ::sm/email] + result1 (sm/encode schema candidate sm/string-transformer) + result2 (sm/decode schema candidate sm/json-transformer)] + (t/is (= result1 "a@b.com, a@c.net")) + (t/is (= result2 candidate)))) + + (t/testing "validate" + (let [candidate #{"a@b.com" "a@c.net"} + schema [::sm/set ::sm/email]] + + (t/is (true? (sm/validate schema candidate))) + (t/is (true? (sm/validate schema #{}))) + (t/is (false? (sm/validate schema #{"a"}))))) + + (t/testing "generate" + (let [schema [::sm/set ::sm/email] + value (sg/generate schema)] + (t/is (true? (sm/validate schema (sg/generate schema))))))) diff --git a/common/test/common_tests/types/decoder_test.clj b/common/test/common_tests/types/decoder_test.clj new file mode 100644 index 0000000000000000000000000000000000000000..d1dc3f257a861c7038c5a337dee14f864236b0f4 --- /dev/null +++ b/common/test/common_tests/types/decoder_test.clj @@ -0,0 +1,150 @@ +;; 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 common-tests.types.decoder-test + (:require + [app.common.json :as json] + [app.common.pprint :as pp] + [app.common.schema :as sm] + [app.common.schema.generators :as sg] + [app.common.types.color :refer [schema:color schema:gradient]] + [app.common.types.plugins :refer [schema:plugin-data]] + [app.common.types.shape :as tsh] + [app.common.types.shape.interactions :refer [schema:animation schema:interaction]] + [app.common.types.shape.path :refer [schema:path-content]] + [app.common.types.shape.shadow :refer [schema:shadow]] + [app.common.uuid :as uuid] + [clojure.test :as t])) + +(defn json-roundtrip + [data] + (-> data + (json/encode :key-fn json/write-camel-key) + (json/decode :key-fn json/read-kebab-key))) + +(t/deftest map-of-with-strings + (let [schema [:map [:data [:map-of :string :int]]] + encode (sm/encoder schema (sm/json-transformer)) + decode (sm/decoder schema (sm/json-transformer)) + + data1 {:data {"foo/bar" 1 + "foo-baz" 2}} + + data2 (encode data1) + data3 (json-roundtrip data2) + data4 (decode data3)] + + ;; (pp/pprint data1) + ;; (pp/pprint data2) + ;; (pp/pprint data3) + ;; (pp/pprint data4) + + (t/is (= data1 data2)) + (t/is (= data1 data4)) + (t/is (not= data1 data3)))) + +(t/deftest gradient-json-roundtrip + (let [encode (sm/encoder schema:gradient (sm/json-transformer)) + decode (sm/decoder schema:gradient (sm/json-transformer))] + (sg/check! + (sg/for [gradient (sg/generator schema:gradient)] + (let [gradient-1 (encode gradient) + gradient-2 (json-roundtrip gradient-1) + gradient-3 (decode gradient-2)] + ;; (app.common.pprint/pprint gradient) + ;; (app.common.pprint/pprint gradient-3) + (t/is (= gradient gradient-3)))) + {:num 500}))) + +(t/deftest color-json-roundtrip + (let [encode (sm/encoder schema:color (sm/json-transformer)) + decode (sm/decoder schema:color (sm/json-transformer))] + (sg/check! + (sg/for [color (sg/generator schema:color)] + (let [color-1 (encode color) + color-2 (json-roundtrip color-1) + color-3 (decode color-2)] + ;; (app.common.pprint/pprint color) + ;; (app.common.pprint/pprint color-3) + (t/is (= color color-3)))) + {:num 500}))) + +(t/deftest shape-shadow-json-roundtrip + (let [encode (sm/encoder schema:shadow (sm/json-transformer)) + decode (sm/decoder schema:shadow (sm/json-transformer))] + (sg/check! + (sg/for [shadow (sg/generator schema:shadow)] + (let [shadow-1 (encode shadow) + shadow-2 (json-roundtrip shadow-1) + shadow-3 (decode shadow-2)] + ;; (app.common.pprint/pprint shadow) + ;; (app.common.pprint/pprint shadow-3) + (t/is (= shadow shadow-3)))) + {:num 500}))) + +(t/deftest shape-animation-json-roundtrip + (let [encode (sm/encoder schema:animation (sm/json-transformer)) + decode (sm/decoder schema:animation (sm/json-transformer))] + (sg/check! + (sg/for [animation (sg/generator schema:animation)] + (let [animation-1 (encode animation) + animation-2 (json-roundtrip animation-1) + animation-3 (decode animation-2)] + ;; (app.common.pprint/pprint animation) + ;; (app.common.pprint/pprint animation-3) + (t/is (= animation animation-3)))) + {:num 500}))) + +(t/deftest shape-interaction-json-roundtrip + (let [encode (sm/encoder schema:interaction (sm/json-transformer)) + decode (sm/decoder schema:interaction (sm/json-transformer))] + (sg/check! + (sg/for [interaction (sg/generator schema:interaction)] + (let [interaction-1 (encode interaction) + interaction-2 (json-roundtrip interaction-1) + interaction-3 (decode interaction-2)] + ;; (app.common.pprint/pprint interaction) + ;; (app.common.pprint/pprint interaction-3) + (t/is (= interaction interaction-3)))) + {:num 500}))) + + +(t/deftest shape-path-content-json-roundtrip + (let [encode (sm/encoder schema:path-content (sm/json-transformer)) + decode (sm/decoder schema:path-content (sm/json-transformer))] + (sg/check! + (sg/for [path-content (sg/generator schema:path-content)] + (let [path-content-1 (encode path-content) + path-content-2 (json-roundtrip path-content-1) + path-content-3 (decode path-content-2)] + ;; (app.common.pprint/pprint path-content) + ;; (app.common.pprint/pprint path-content-3) + (t/is (= path-content path-content-3)))) + {:num 500}))) + +(t/deftest plugin-data-json-roundtrip + (let [encode (sm/encoder schema:plugin-data (sm/json-transformer)) + decode (sm/decoder schema:plugin-data (sm/json-transformer))] + (sg/check! + (sg/for [data (sg/generator schema:plugin-data)] + (let [data-1 (encode data) + data-2 (json-roundtrip data-1) + data-3 (decode data-2)] + (t/is (= data data-3)))) + {:num 500}))) + +(t/deftest shape-json-roundtrip + (let [encode (sm/encoder ::tsh/shape (sm/json-transformer)) + decode (sm/decoder ::tsh/shape (sm/json-transformer))] + (sg/check! + (sg/for [shape (sg/generator ::tsh/shape)] + (let [shape-1 (encode shape) + shape-2 (json-roundtrip shape-1) + shape-3 (decode shape-2)] + ;; (app.common.pprint/pprint shape) + ;; (app.common.pprint/pprint shape-3) + (t/is (= shape shape-3)))) + {:num 1000}))) diff --git a/common/test/common_tests/types/types_component_test.cljc b/common/test/common_tests/types/types_component_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..cff174329b9898845b616d65986fd075a358f20f --- /dev/null +++ b/common/test/common_tests/types/types_component_test.cljc @@ -0,0 +1,43 @@ +;; 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 common-tests.types.types-component-test + (:require + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.types.component :as ctk] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-valid-touched-group + (t/is (ctk/valid-touched-group? :name-group)) + (t/is (ctk/valid-touched-group? :geometry-group)) + (t/is (ctk/valid-touched-group? :swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f)) + (t/is (not (ctk/valid-touched-group? :this-is-not-a-group))) + (t/is (not (ctk/valid-touched-group? :swap-slot-))) + (t/is (not (ctk/valid-touched-group? :swap-slot-xxxxxx))) + (t/is (not (ctk/valid-touched-group? :swap-slot-9cc181fa-5eef-8084-8004))) + (t/is (not (ctk/valid-touched-group? nil)))) + +(t/deftest test-get-swap-slot + (let [s1 (ths/sample-shape :s1) + s2 (ths/sample-shape :s2 :touched #{:visibility-group}) + s3 (ths/sample-shape :s3 :touched #{:swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f}) + s4 (ths/sample-shape :s4 :touched #{:fill-group + :swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f}) + s5 (ths/sample-shape :s5 :touched #{:swap-slot-9cc181fa-5eef-8084-8004-7bb2ab45fd1f + :content-group + :geometry-group}) + s6 (ths/sample-shape :s6 :touched #{:swap-slot-9cc181fa})] + (t/is (nil? (ctk/get-swap-slot s1))) + (t/is (nil? (ctk/get-swap-slot s2))) + (t/is (= (ctk/get-swap-slot s3) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) + (t/is (= (ctk/get-swap-slot s4) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) + (t/is (= (ctk/get-swap-slot s5) #uuid "9cc181fa-5eef-8084-8004-7bb2ab45fd1f")) + #?(:clj + (t/is (thrown-with-msg? IllegalArgumentException #"Invalid UUID string" + (ctk/get-swap-slot s6)))))) diff --git a/common/test/common_tests/types/types_libraries_test.cljc b/common/test/common_tests/types/types_libraries_test.cljc new file mode 100644 index 0000000000000000000000000000000000000000..ab13cf8683bf794e1696d3c6e9e4d3b1ee34d776 --- /dev/null +++ b/common/test/common_tests/types/types_libraries_test.cljc @@ -0,0 +1,192 @@ +;; 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 common-tests.types.types-libraries-test + (:require + [app.common.data :as d] + [app.common.test-helpers.components :as thc] + [app.common.test-helpers.compositions :as tho] + [app.common.test-helpers.files :as thf] + [app.common.test-helpers.ids-map :as thi] + [app.common.test-helpers.shapes :as ths] + [app.common.text :as txt] + [app.common.types.colors-list :as ctcl] + [app.common.types.component :as ctk] + [app.common.types.components-list :as ctkl] + [app.common.types.file :as ctf] + [app.common.types.pages-list :as ctpl] + [app.common.types.typographies-list :as ctyl] + [clojure.test :as t])) + +(t/use-fixtures :each thi/test-fixture) + +(t/deftest test-create-file + (let [f1 (thf/sample-file :file1) + f2 (thf/sample-file :file2 :page-label :page1) + f3 (thf/sample-file :file3 :name "testing file") + f4 (-> (thf/sample-file :file4 :page-label :page2) + (thf/add-sample-page :page3 :name "testing page") + (ths/add-sample-shape :shape1)) + f5 (-> f4 + (ths/add-sample-shape :shape2) + (thf/switch-to-page :page2) + (ths/add-sample-shape :shape3 :name "testing shape" :width 100)) + s1 (ths/get-shape f4 :shape1) + s2 (ths/get-shape f5 :shape2 :page-label :page3) + s3 (ths/get-shape f5 :shape3)] + + ;; (thf/pprint-file f4) + + (t/is (= (:name f1) "Test file")) + (t/is (= (:name f3) "testing file")) + (t/is (= (:id f2) (thi/id :file2))) + (t/is (= (:id f4) (thi/id :file4))) + (t/is (= (-> f4 :data :pages-index vals first :id) (thi/id :page2))) + (t/is (= (-> f4 :data :pages-index vals first :name) "Page 1")) + (t/is (= (-> f4 :data :pages-index vals second :id) (thi/id :page3))) + (t/is (= (-> f4 :data :pages-index vals second :name) "testing page")) + + (t/is (= (:id (thf/current-page f2)) (thi/id :page1))) + (t/is (= (:id (thf/current-page f4)) (thi/id :page3))) + (t/is (= (:id (thf/current-page f5)) (thi/id :page2))) + + (t/is (= (:id s1) (thi/id :shape1))) + (t/is (= (:name s1) "Rectangle")) + (t/is (= (:id s2) (thi/id :shape2))) + (t/is (= (:name s2) "Rectangle")) + (t/is (= (:id s3) (thi/id :shape3))) + (t/is (= (:name s3) "testing shape")) + (t/is (= (:width s3) 100)) + (t/is (= (:width (:selrect s3)) 100)))) + +(t/deftest test-create-components + (let [f1 (-> (thf/sample-file :file1) + (tho/add-simple-component-with-copy :component1 :main-root :main-child :copy-root))] + + #_(thf/dump-file f1) + #_(thf/pprint-file f4) + + (t/is (= (:name f1) "Test file")))) + +(t/deftest test-absorb-components + (let [;; Setup + library (-> (thf/sample-file :library :is-shared true) + (tho/add-simple-component :component1 :main-root :rect1)) + + file (-> (thf/sample-file :file) + (thc/instantiate-component :component1 :copy-root :library library)) + + ;; Action + file' (ctf/update-file-data + file + #(ctf/absorb-assets % (:data library))) + + _ (thf/validate-file! file') + + ;; Get + pages' (ctpl/pages-seq (ctf/file-data file')) + components' (ctkl/components-seq (ctf/file-data file')) + component' (first components') + + copy-root' (ths/get-shape file' :copy-root) + main-root' (ctf/get-ref-shape (ctf/file-data file') component' copy-root')] + + ;; Check + (t/is (= (count pages') 2)) + (t/is (= (:name (first pages')) "Page 1")) + (t/is (= (:name (second pages')) "Main components")) + + (t/is (= (count components') 1)) + + (t/is (ctk/instance-of? copy-root' (:id file') (:id component'))) + (t/is (ctk/is-main-of? main-root' copy-root' true)) + (t/is (ctk/main-instance-of? (:id main-root') (:id (second pages')) component')))) + +(t/deftest test-absorb-colors + (let [;; Setup + library (-> (thf/sample-file :library :is-shared true) + (ths/add-sample-library-color :color1 {:name "Test color" + :color "#abcdef"})) + + file (-> (thf/sample-file :file) + (ths/add-sample-shape :shape1 + :type :rect + :name "Rect1" + :fills [{:fill-color "#abcdef" + :fill-opacity 1 + :fill-color-ref-id (thi/id :color1) + :fill-color-ref-file (thi/id :library)}])) + + ;; Action + file' (ctf/update-file-data + file + #(ctf/absorb-assets % (:data library))) + + _ (thf/validate-file! file') + + ;; Get + colors' (ctcl/colors-seq (ctf/file-data file')) + shape1' (ths/get-shape file' :shape1) + fill' (first (:fills shape1'))] + + ;; Check + (t/is (= (count colors') 1)) + (t/is (= (:id (first colors')) (thi/id :color1))) + (t/is (= (:name (first colors')) "Test color")) + (t/is (= (:color (first colors')) "#abcdef")) + + (t/is (= (:fill-color fill') "#abcdef")) + (t/is (= (:fill-color-ref-id fill') (thi/id :color1))) + (t/is (= (:fill-color-ref-file fill') (:id file'))))) + +(t/deftest test-absorb-typographies + (let [;; Setup + library (-> (thf/sample-file :library :is-shared true) + (ths/add-sample-typography :typography1 {:name "Test typography"})) + + file (-> (thf/sample-file :file) + (ths/add-sample-shape :shape1 + :type :text + :name "Text1" + :content {:type "root" + :children [{:type "paragraph-set" + :children [{:type "paragraph" + :key "67uep" + :children [{:text "Example text" + :typography-ref-id (thi/id :typography1) + :typography-ref-file (thi/id :library) + :line-height "1.2" + :font-style "normal" + :text-transform "none" + :text-align "left" + :font-id "sourcesanspro" + :font-family "sourcesanspro" + :font-size "14" + :font-weight "400" + :font-variant-id "regular" + :text-decoration "none" + :letter-spacing "0" + :fills [{:fill-color "#000000" + :fill-opacity 1}]}]}]}]})) + ;; Action + file' (ctf/update-file-data + file + #(ctf/absorb-assets % (:data library))) + + _ (thf/validate-file! file') + + ;; Get + typographies' (ctyl/typographies-seq (ctf/file-data file')) + shape1' (ths/get-shape file' :shape1) + text-node' (d/seek #(some? (:text %)) (txt/node-seq (:content shape1')))] + + ;; Check + (t/is (= (count typographies') 1)) + (t/is (= (:id (first typographies')) (thi/id :typography1))) + (t/is (= (:name (first typographies')) "Test typography")) + + (t/is (= (:typography-ref-id text-node') (thi/id :typography1))) + (t/is (= (:typography-ref-file text-node') (:id file'))))) diff --git a/common/test/common_tests/types_file_test.cljc b/common/test/common_tests/types_file_test.cljc deleted file mode 100644 index c95ea3893448b0297204b7d2d27e83f8647f7b65..0000000000000000000000000000000000000000 --- a/common/test/common_tests/types_file_test.cljc +++ /dev/null @@ -1,207 +0,0 @@ -;; 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 common-tests.types-file-test - (:require - [app.common.data :as d] - [app.common.geom.point :as gpt] - [app.common.text :as txt] - [app.common.types.colors-list :as ctcl] - [app.common.types.component :as ctk] - [app.common.types.components-list :as ctkl] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.types.pages-list :as ctpl] - [app.common.types.shape :as cts] - [app.common.types.shape-tree :as ctst] - [app.common.types.typographies-list :as ctyl] - [app.common.uuid :as uuid] - [clojure.pprint :refer [pprint]] - [clojure.test :as t] - [common-tests.helpers.components :as thk] - [common-tests.helpers.files :as thf] - [cuerdas.core :as str])) - -(t/use-fixtures :each thf/reset-idmap!) - -#_(t/deftest test-absorb-components - (let [library-id (uuid/custom 1 1) - library-page-id (uuid/custom 2 2) - file-id (uuid/custom 3 3) - file-page-id (uuid/custom 4 4) - - library (-> (thf/sample-file library-id library-page-id {:is-shared true}) - (thf/sample-shape :group1 - :group - library-page-id - {:name "Group1"}) - (thf/sample-shape :shape1 - :rect - library-page-id - {:name "Rect1" - :parent-id (thf/id :group1)}) - (thf/sample-component :component1 - library-page-id - (thf/id :group1))) - - file (-> (thf/sample-file file-id file-page-id) - (thf/sample-instance :instance1 - file-page-id - library - (thf/id :component1))) - - absorbed-file (ctf/update-file-data - file - #(ctf/absorb-assets % (:data library))) - - pages (ctpl/pages-seq (ctf/file-data absorbed-file)) - components (ctkl/components-seq (ctf/file-data absorbed-file)) - shapes-1 (ctn/shapes-seq (first pages)) - shapes-2 (ctn/shapes-seq (second pages)) - - [[p-group p-shape] [c-group1 c-shape1] component1] - (thk/resolve-instance-and-main - (first pages) - (:id (second shapes-1)) - {file-id absorbed-file}) - - [[lp-group lp-shape] [c-group2 c-shape2] component2] - (thk/resolve-instance-and-main - (second pages) - (:id (second shapes-2)) - {file-id absorbed-file})] - - ;; Uncomment to debug - - ;; (println "\n===== library") - ;; (ctf/dump-tree (:data library) - ;; library-page-id - ;; {} - ;; true) - - ;; (println "\n===== file") - ;; (ctf/dump-tree (:data file) - ;; file-page-id - ;; {library-id library} - ;; true) - - ;; (println "\n===== absorbed file") - ;; (println (str "\n<" (:name (first pages)) ">")) - ;; (ctf/dump-tree (:data absorbed-file) - ;; (:id (first pages)) - ;; {file-id absorbed-file} - ;; false) - ;; (println (str "\n<" (:name (second pages)) ">")) - ;; (ctf/dump-tree (:data absorbed-file) - ;; (:id (second pages)) - ;; {file-id absorbed-file} - ;; false) - - (t/is (= (count pages) 2)) - (t/is (= (:name (first pages)) "Page 1")) - (t/is (= (:name (second pages)) "Main components")) - - (t/is (= (count components) 1)) - - (t/is (= (:name p-group) "Group1")) - (t/is (ctk/instance-of? p-group file-id (:id component1))) - (t/is (not (:main-instance? p-group))) - (t/is (not (ctk/main-instance-of? (:id p-group) file-page-id component1))) - (t/is (ctk/is-main-of? c-group1 p-group)) - - (t/is (= (:name p-shape) "Rect1")) - (t/is (ctk/is-main-of? c-shape1 p-shape)))) - - -(t/deftest test-absorb-colors - (let [library-id (uuid/custom 1 1) - library-page-id (uuid/custom 2 2) - file-id (uuid/custom 3 3) - file-page-id (uuid/custom 4 4) - - library (-> (thf/sample-file library-id library-page-id {:is-shared true}) - (thf/sample-color :color1 {:name "Test color" - :color "#abcdef"})) - - file (-> (thf/sample-file file-id file-page-id) - (thf/sample-shape :shape1 - :rect - file-page-id - {:name "Rect1" - :fills [{:fill-color "#abcdef" - :fill-opacity 1 - :fill-color-ref-id (thf/id :color1) - :fill-color-ref-file library-id}]})) - - absorbed-file (ctf/update-file-data - file - #(ctf/absorb-assets % (:data library))) - - colors (ctcl/colors-seq (ctf/file-data absorbed-file)) - page (ctpl/get-page (ctf/file-data absorbed-file) file-page-id) - shape1 (ctn/get-shape page (thf/id :shape1)) - fill (first (:fills shape1))] - - (t/is (= (count colors) 1)) - (t/is (= (:id (first colors)) (thf/id :color1))) - (t/is (= (:name (first colors)) "Test color")) - (t/is (= (:color (first colors)) "#abcdef")) - - (t/is (= (:fill-color fill) "#abcdef")) - (t/is (= (:fill-color-ref-id fill) (thf/id :color1))) - (t/is (= (:fill-color-ref-file fill) file-id)))) - -(t/deftest test-absorb-typographies - (let [library-id (uuid/custom 1 1) - library-page-id (uuid/custom 2 2) - file-id (uuid/custom 3 3) - file-page-id (uuid/custom 4 4) - - library (-> (thf/sample-file library-id library-page-id {:is-shared true}) - (thf/sample-typography :typography1 {:name "Test typography"})) - - file (-> (thf/sample-file file-id file-page-id) - (thf/sample-shape :shape1 - :text - file-page-id - {:name "Text1" - :content {:type "root" - :children [{:type "paragraph-set" - :children [{:type "paragraph" - :key "67uep" - :children [{:text "Example text" - :typography-ref-id (thf/id :typography1) - :typography-ref-file library-id - :line-height "1.2" - :font-style "normal" - :text-transform "none" - :text-align "left" - :font-id "sourcesanspro" - :font-family "sourcesanspro" - :font-size "14" - :font-weight "400" - :font-variant-id "regular" - :text-decoration "none" - :letter-spacing "0" - :fills [{:fill-color "#000000" - :fill-opacity 1}]}]}]}]}})) - absorbed-file (ctf/update-file-data - file - #(ctf/absorb-assets % (:data library))) - - typographies (ctyl/typographies-seq (ctf/file-data absorbed-file)) - page (ctpl/get-page (ctf/file-data absorbed-file) file-page-id) - - shape1 (ctn/get-shape page (thf/id :shape1)) - text-node (d/seek #(some? (:text %)) (txt/node-seq (:content shape1)))] - - (t/is (= (count typographies) 1)) - (t/is (= (:id (first typographies)) (thf/id :typography1))) - (t/is (= (:name (first typographies)) "Test typography")) - - (t/is (= (:typography-ref-id text-node) (thf/id :typography1))) - (t/is (= (:typography-ref-file text-node) file-id)))) - diff --git a/common/tests.edn b/common/tests.edn new file mode 100644 index 0000000000000000000000000000000000000000..9f487a7eafec1f2154163adc89928113956b0cc9 --- /dev/null +++ b/common/tests.edn @@ -0,0 +1,4 @@ +#kaocha/v1 + {:tests [{:id :unit + :test-paths ["test"]}] + :kaocha/reporter [kaocha.report/dots]} diff --git a/common/yarn.lock b/common/yarn.lock index 75220279c408da917fb5fea0b6c02d89b3b13182..94f9b89aa841bd2356dec6f3b7c47cddc74bf00b 100644 --- a/common/yarn.lock +++ b/common/yarn.lock @@ -15,52 +15,52 @@ __metadata: strip-ansi-cjs: "npm:strip-ansi@^6.0.1" wrap-ansi: "npm:^8.1.0" wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" - checksum: b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e languageName: node linkType: hard "@npmcli/agent@npm:^2.0.0": - version: 2.2.0 - resolution: "@npmcli/agent@npm:2.2.0" + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" dependencies: agent-base: "npm:^7.1.0" http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.1" lru-cache: "npm:^10.0.1" - socks-proxy-agent: "npm:^8.0.1" - checksum: 7b89590598476dda88e79c473766b67c682aae6e0ab0213491daa6083dcc0c171f86b3868f5506f22c09aa5ea69ad7dfb78f4bf39a8dca375d89a42f408645b3 + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae languageName: node linkType: hard "@npmcli/fs@npm:^3.1.0": - version: 3.1.0 - resolution: "@npmcli/fs@npm:3.1.0" + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" dependencies: semver: "npm:^7.3.5" - checksum: 162b4a0b8705cd6f5c2470b851d1dc6cd228c86d2170e1769d738c1fbb69a87160901411c3c035331e9e99db72f1f1099a8b734bf1637cc32b9a5be1660e4e1e + checksum: 10c0/c37a5b4842bfdece3d14dfdb054f73fe15ed2d3da61b34ff76629fb5b1731647c49166fd2a8bf8b56fcfa51200382385ea8909a3cbecdad612310c114d3f6c99 languageName: node linkType: hard "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd languageName: node linkType: hard "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" - checksum: f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + checksum: 10c0/f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 languageName: node linkType: hard -"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": - version: 7.1.0 - resolution: "agent-base@npm:7.1.0" +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" dependencies: debug: "npm:^4.3.4" - checksum: fc974ab57ffdd8421a2bc339644d312a9cca320c20c3393c9d8b1fd91731b9bbabdb985df5fc860f5b79d81c3e350daa3fcb31c5c07c0bb385aafc817df004ce + checksum: 10c0/e59ce7bed9c63bf071a30cc471f2933862044c97fd9958967bfe22521d7a0f601ce4ed5a8c011799d0c726ca70312142ae193bbebb60f576b52be19d4a363b50 languageName: node linkType: hard @@ -70,21 +70,21 @@ __metadata: dependencies: clean-stack: "npm:^2.0.0" indent-string: "npm:^4.0.0" - checksum: a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 languageName: node linkType: hard "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" - checksum: 9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 languageName: node linkType: hard "ansi-regex@npm:^6.0.1": version: 6.0.1 resolution: "ansi-regex@npm:6.0.1" - checksum: cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 + checksum: 10c0/cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 languageName: node linkType: hard @@ -93,26 +93,25 @@ __metadata: resolution: "ansi-styles@npm:4.3.0" dependencies: color-convert: "npm:^2.0.1" - checksum: 895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 languageName: node linkType: hard "ansi-styles@npm:^6.1.0": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" - checksum: 5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c languageName: node linkType: hard -"asn1.js@npm:^5.2.0": - version: 5.4.1 - resolution: "asn1.js@npm:5.4.1" +"asn1.js@npm:^4.10.1": + version: 4.10.1 + resolution: "asn1.js@npm:4.10.1" dependencies: bn.js: "npm:^4.0.0" inherits: "npm:^2.0.1" minimalistic-assert: "npm:^1.0.0" - safer-buffer: "npm:^2.1.0" - checksum: b577232fa6069cc52bb128e564002c62b2b1fe47f7137bdcd709c0b8495aa79cee0f8cc458a831b2d8675900eea0d05781b006be5e1aa4f0ae3577a73ec20324 + checksum: 10c0/afa7f3ab9e31566c80175a75b182e5dba50589dcc738aa485be42bdd787e2a07246a4b034d481861123cbe646a7656f318f4f1cad2e9e5e808a210d5d6feaa88 languageName: node linkType: hard @@ -122,35 +121,35 @@ __metadata: dependencies: object.assign: "npm:^4.1.4" util: "npm:^0.10.4" - checksum: 836688b928b68b7fc5bbc165443e16a62623d57676a1e8a980a0316f9ae86e5e0a102c63470491bf55a8545e75766303640c0c7ad1cf6bfa5450130396043bbd + checksum: 10c0/836688b928b68b7fc5bbc165443e16a62623d57676a1e8a980a0316f9ae86e5e0a102c63470491bf55a8545e75766303640c0c7ad1cf6bfa5450130396043bbd languageName: node linkType: hard "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" - checksum: 9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee languageName: node linkType: hard "base64-js@npm:^1.0.2": version: 1.5.1 resolution: "base64-js@npm:1.5.1" - checksum: f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf + checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf languageName: node linkType: hard "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9": version: 4.12.0 resolution: "bn.js@npm:4.12.0" - checksum: 9736aaa317421b6b3ed038ff3d4491935a01419ac2d83ddcfebc5717385295fcfcf0c57311d90fe49926d0abbd7a9dbefdd8861e6129939177f7e67ebc645b21 + checksum: 10c0/9736aaa317421b6b3ed038ff3d4491935a01419ac2d83ddcfebc5717385295fcfcf0c57311d90fe49926d0abbd7a9dbefdd8861e6129939177f7e67ebc645b21 languageName: node linkType: hard "bn.js@npm:^5.0.0, bn.js@npm:^5.2.1": version: 5.2.1 resolution: "bn.js@npm:5.2.1" - checksum: bed3d8bd34ec89dbcf9f20f88bd7d4a49c160fda3b561c7bb227501f974d3e435a48fb9b61bc3de304acab9215a3bda0803f7017ffb4d0016a0c3a740a283caa + checksum: 10c0/bed3d8bd34ec89dbcf9f20f88bd7d4a49c160fda3b561c7bb227501f974d3e435a48fb9b61bc3de304acab9215a3bda0803f7017ffb4d0016a0c3a740a283caa languageName: node linkType: hard @@ -159,18 +158,18 @@ __metadata: resolution: "brace-expansion@npm:2.0.1" dependencies: balanced-match: "npm:^1.0.0" - checksum: b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f languageName: node linkType: hard "brorand@npm:^1.0.1, brorand@npm:^1.1.0": version: 1.1.0 resolution: "brorand@npm:1.1.0" - checksum: 6f366d7c4990f82c366e3878492ba9a372a73163c09871e80d82fb4ae0d23f9f8924cb8a662330308206e6b3b76ba1d528b4601c9ef73c2166b440b2ea3b7571 + checksum: 10c0/6f366d7c4990f82c366e3878492ba9a372a73163c09871e80d82fb4ae0d23f9f8924cb8a662330308206e6b3b76ba1d528b4601c9ef73c2166b440b2ea3b7571 languageName: node linkType: hard -"browserify-aes@npm:^1.0.0, browserify-aes@npm:^1.0.4": +"browserify-aes@npm:^1.0.4, browserify-aes@npm:^1.2.0": version: 1.2.0 resolution: "browserify-aes@npm:1.2.0" dependencies: @@ -180,7 +179,7 @@ __metadata: evp_bytestokey: "npm:^1.0.3" inherits: "npm:^2.0.1" safe-buffer: "npm:^5.0.1" - checksum: 967f2ae60d610b7b252a4cbb55a7a3331c78293c94b4dd9c264d384ca93354c089b3af9c0dd023534efdc74ffbc82510f7ad4399cf82bc37bc07052eea485f18 + checksum: 10c0/967f2ae60d610b7b252a4cbb55a7a3331c78293c94b4dd9c264d384ca93354c089b3af9c0dd023534efdc74ffbc82510f7ad4399cf82bc37bc07052eea485f18 languageName: node linkType: hard @@ -191,7 +190,7 @@ __metadata: browserify-aes: "npm:^1.0.4" browserify-des: "npm:^1.0.0" evp_bytestokey: "npm:^1.0.0" - checksum: aa256dcb42bc53a67168bbc94ab85d243b0a3b56109dee3b51230b7d010d9b78985ffc1fb36e145c6e4db151f888076c1cfc207baf1525d3e375cbe8187fe27d + checksum: 10c0/aa256dcb42bc53a67168bbc94ab85d243b0a3b56109dee3b51230b7d010d9b78985ffc1fb36e145c6e4db151f888076c1cfc207baf1525d3e375cbe8187fe27d languageName: node linkType: hard @@ -203,7 +202,7 @@ __metadata: des.js: "npm:^1.0.0" inherits: "npm:^2.0.1" safe-buffer: "npm:^5.1.2" - checksum: 943eb5d4045eff80a6cde5be4e5fbb1f2d5002126b5a4789c3c1aae3cdddb1eb92b00fb92277f512288e5c6af330730b1dbabcf7ce0923e749e151fcee5a074d + checksum: 10c0/943eb5d4045eff80a6cde5be4e5fbb1f2d5002126b5a4789c3c1aae3cdddb1eb92b00fb92277f512288e5c6af330730b1dbabcf7ce0923e749e151fcee5a074d languageName: node linkType: hard @@ -213,24 +212,25 @@ __metadata: dependencies: bn.js: "npm:^5.0.0" randombytes: "npm:^2.0.1" - checksum: fb2b5a8279d8a567a28d8ee03fb62e448428a906bab5c3dc9e9c3253ace551b5ea271db15e566ac78f1b1d71b243559031446604168b9235c351a32cae99d02a + checksum: 10c0/fb2b5a8279d8a567a28d8ee03fb62e448428a906bab5c3dc9e9c3253ace551b5ea271db15e566ac78f1b1d71b243559031446604168b9235c351a32cae99d02a languageName: node linkType: hard "browserify-sign@npm:^4.0.0": - version: 4.2.2 - resolution: "browserify-sign@npm:4.2.2" + version: 4.2.3 + resolution: "browserify-sign@npm:4.2.3" dependencies: bn.js: "npm:^5.2.1" browserify-rsa: "npm:^4.1.0" create-hash: "npm:^1.2.0" create-hmac: "npm:^1.1.7" - elliptic: "npm:^6.5.4" + elliptic: "npm:^6.5.5" + hash-base: "npm:~3.0" inherits: "npm:^2.0.4" - parse-asn1: "npm:^5.1.6" - readable-stream: "npm:^3.6.2" + parse-asn1: "npm:^5.1.7" + readable-stream: "npm:^2.3.8" safe-buffer: "npm:^5.2.1" - checksum: 4d1292e5c165d93455630515003f0e95eed9239c99e2d373920c5b56903d16296a3d23cd4bdc4d298f55ad9b83714a9e63bc4839f1166c303349a16e84e9b016 + checksum: 10c0/30c0eba3f5970a20866a4d3fbba2c5bd1928cd24f47faf995f913f1499214c6f3be14bb4d6ec1ab5c6cafb1eca9cb76ba1c2e1c04ed018370634d4e659c77216 languageName: node linkType: hard @@ -239,21 +239,21 @@ __metadata: resolution: "browserify-zlib@npm:0.2.0" dependencies: pako: "npm:~1.0.5" - checksum: 9ab10b6dc732c6c5ec8ebcbe5cb7fe1467f97402c9b2140113f47b5f187b9438f93a8e065d8baf8b929323c18324fbf1105af479ee86d9d36cab7d7ef3424ad9 + checksum: 10c0/9ab10b6dc732c6c5ec8ebcbe5cb7fe1467f97402c9b2140113f47b5f187b9438f93a8e065d8baf8b929323c18324fbf1105af479ee86d9d36cab7d7ef3424ad9 languageName: node linkType: hard "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" - checksum: 124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 + checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 languageName: node linkType: hard "buffer-xor@npm:^1.0.3": version: 1.0.3 resolution: "buffer-xor@npm:1.0.3" - checksum: fd269d0e0bf71ecac3146187cfc79edc9dbb054e2ee69b4d97dfb857c6d997c33de391696d04bdd669272751fa48e7872a22f3a6c7b07d6c0bc31dbe02a4075c + checksum: 10c0/fd269d0e0bf71ecac3146187cfc79edc9dbb054e2ee69b4d97dfb857c6d997c33de391696d04bdd669272751fa48e7872a22f3a6c7b07d6c0bc31dbe02a4075c languageName: node linkType: hard @@ -264,20 +264,20 @@ __metadata: base64-js: "npm:^1.0.2" ieee754: "npm:^1.1.4" isarray: "npm:^1.0.0" - checksum: dc443d7e7caab23816b58aacdde710b72f525ad6eecd7d738fcaa29f6d6c12e8d9c13fed7219fd502be51ecf0615f5c077d4bdc6f9308dde2e53f8e5393c5b21 + checksum: 10c0/dc443d7e7caab23816b58aacdde710b72f525ad6eecd7d738fcaa29f6d6c12e8d9c13fed7219fd502be51ecf0615f5c077d4bdc6f9308dde2e53f8e5393c5b21 languageName: node linkType: hard "builtin-status-codes@npm:^3.0.0": version: 3.0.0 resolution: "builtin-status-codes@npm:3.0.0" - checksum: c37bbba11a34c4431e56bd681b175512e99147defbe2358318d8152b3a01df7bf25e0305873947e5b350073d5ef41a364a22b37e48f1fb6d2fe6d5286a0f348c + checksum: 10c0/c37bbba11a34c4431e56bd681b175512e99147defbe2358318d8152b3a01df7bf25e0305873947e5b350073d5ef41a364a22b37e48f1fb6d2fe6d5286a0f348c languageName: node linkType: hard "cacache@npm:^18.0.0": - version: 18.0.1 - resolution: "cacache@npm:18.0.1" + version: 18.0.3 + resolution: "cacache@npm:18.0.3" dependencies: "@npmcli/fs": "npm:^3.1.0" fs-minipass: "npm:^3.0.0" @@ -291,25 +291,27 @@ __metadata: ssri: "npm:^10.0.0" tar: "npm:^6.1.11" unique-filename: "npm:^3.0.0" - checksum: a31666805a80a8b16ad3f85faf66750275a9175a3480896f4f6d31b5d53ef190484fabd71bdb6d2ea5603c717fbef09f4af03d6a65b525c8ef0afaa44c361866 + checksum: 10c0/dfda92840bb371fb66b88c087c61a74544363b37a265023223a99965b16a16bbb87661fe4948718d79df6e0cc04e85e62784fbcf1832b2a5e54ff4c46fbb45b7 languageName: node linkType: hard -"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2": - version: 1.0.5 - resolution: "call-bind@npm:1.0.5" +"call-bind@npm:^1.0.5, call-bind@npm:^1.0.7": + version: 1.0.7 + resolution: "call-bind@npm:1.0.7" dependencies: + es-define-property: "npm:^1.0.0" + es-errors: "npm:^1.3.0" function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.1" - set-function-length: "npm:^1.1.1" - checksum: a6172c168fd6dacf744fcde745099218056bd755c50415b592655dcd6562157ed29f130f56c3f6db2250f67e4bd62e5c218cdc56d7bfd76e0bda50770fce2d10 + get-intrinsic: "npm:^1.2.4" + set-function-length: "npm:^1.2.1" + checksum: 10c0/a3ded2e423b8e2a265983dba81c27e125b48eefb2655e7dfab6be597088da3d47c47976c24bc51b8fd9af1061f8f87b4ab78a314f3c77784b2ae2ba535ad8b8d languageName: node linkType: hard "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" - checksum: 594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + checksum: 10c0/594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 languageName: node linkType: hard @@ -319,14 +321,14 @@ __metadata: dependencies: inherits: "npm:^2.0.1" safe-buffer: "npm:^5.0.1" - checksum: d8d005f8b64d8a77b3d3ce531301ae7b45902c9cab4ec8b66bdbd2bf2a1d9fceb9a2133c293eb3c060b2d964da0f14c47fb740366081338aa3795dd1faa8984b + checksum: 10c0/d8d005f8b64d8a77b3d3ce531301ae7b45902c9cab4ec8b66bdbd2bf2a1d9fceb9a2133c293eb3c060b2d964da0f14c47fb740366081338aa3795dd1faa8984b languageName: node linkType: hard "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" - checksum: 1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 languageName: node linkType: hard @@ -335,14 +337,14 @@ __metadata: resolution: "color-convert@npm:2.0.1" dependencies: color-name: "npm:~1.1.4" - checksum: 37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 languageName: node linkType: hard "color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" - checksum: a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 languageName: node linkType: hard @@ -350,32 +352,32 @@ __metadata: version: 0.0.0-use.local resolution: "common@workspace:." dependencies: - luxon: "npm:^3.4.2" - sax: "npm:^1.2.4" - shadow-cljs: "npm:2.27.4" + luxon: "npm:^3.4.4" + sax: "npm:^1.4.1" + shadow-cljs: "npm:2.28.11" source-map-support: "npm:^0.5.21" - ws: "npm:^8.13.0" + ws: "npm:^8.17.0" languageName: unknown linkType: soft "console-browserify@npm:^1.1.0": version: 1.2.0 resolution: "console-browserify@npm:1.2.0" - checksum: 89b99a53b7d6cee54e1e64fa6b1f7ac24b844b4019c5d39db298637e55c1f4ffa5c165457ad984864de1379df2c8e1886cbbdac85d9dbb6876a9f26c3106f226 + checksum: 10c0/89b99a53b7d6cee54e1e64fa6b1f7ac24b844b4019c5d39db298637e55c1f4ffa5c165457ad984864de1379df2c8e1886cbbdac85d9dbb6876a9f26c3106f226 languageName: node linkType: hard "constants-browserify@npm:^1.0.0": version: 1.0.0 resolution: "constants-browserify@npm:1.0.0" - checksum: ab49b1d59a433ed77c964d90d19e08b2f77213fb823da4729c0baead55e3c597f8f97ebccfdfc47bd896d43854a117d114c849a6f659d9986420e97da0f83ac5 + checksum: 10c0/ab49b1d59a433ed77c964d90d19e08b2f77213fb823da4729c0baead55e3c597f8f97ebccfdfc47bd896d43854a117d114c849a6f659d9986420e97da0f83ac5 languageName: node linkType: hard "core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" - checksum: 90a0e40abbddfd7618f8ccd63a74d88deea94e77d0e8dbbea059fa7ebebb8fbb4e2909667fe26f3a467073de1a542ebe6ae4c73a73745ac5833786759cd906c9 + checksum: 10c0/90a0e40abbddfd7618f8ccd63a74d88deea94e77d0e8dbbea059fa7ebebb8fbb4e2909667fe26f3a467073de1a542ebe6ae4c73a73745ac5833786759cd906c9 languageName: node linkType: hard @@ -385,7 +387,7 @@ __metadata: dependencies: bn.js: "npm:^4.1.0" elliptic: "npm:^6.5.3" - checksum: 77b11a51360fec9c3bce7a76288fc0deba4b9c838d5fb354b3e40c59194d23d66efe6355fd4b81df7580da0661e1334a235a2a5c040b7569ba97db428d466e7f + checksum: 10c0/77b11a51360fec9c3bce7a76288fc0deba4b9c838d5fb354b3e40c59194d23d66efe6355fd4b81df7580da0661e1334a235a2a5c040b7569ba97db428d466e7f languageName: node linkType: hard @@ -398,7 +400,7 @@ __metadata: md5.js: "npm:^1.3.4" ripemd160: "npm:^2.0.1" sha.js: "npm:^2.4.0" - checksum: d402e60e65e70e5083cb57af96d89567954d0669e90550d7cec58b56d49c4b193d35c43cec8338bc72358198b8cbf2f0cac14775b651e99238e1cf411490f915 + checksum: 10c0/d402e60e65e70e5083cb57af96d89567954d0669e90550d7cec58b56d49c4b193d35c43cec8338bc72358198b8cbf2f0cac14775b651e99238e1cf411490f915 languageName: node linkType: hard @@ -412,7 +414,7 @@ __metadata: ripemd160: "npm:^2.0.0" safe-buffer: "npm:^5.0.1" sha.js: "npm:^2.4.8" - checksum: 24332bab51011652a9a0a6d160eed1e8caa091b802335324ae056b0dcb5acbc9fcf173cf10d128eba8548c3ce98dfa4eadaa01bd02f44a34414baee26b651835 + checksum: 10c0/24332bab51011652a9a0a6d160eed1e8caa091b802335324ae056b0dcb5acbc9fcf173cf10d128eba8548c3ce98dfa4eadaa01bd02f44a34414baee26b651835 languageName: node linkType: hard @@ -423,7 +425,7 @@ __metadata: path-key: "npm:^3.1.0" shebang-command: "npm:^2.0.0" which: "npm:^2.0.1" - checksum: 5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + checksum: 10c0/5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 languageName: node linkType: hard @@ -442,7 +444,7 @@ __metadata: public-encrypt: "npm:^4.0.0" randombytes: "npm:^2.0.0" randomfill: "npm:^1.0.3" - checksum: 0c20198886576050a6aa5ba6ae42f2b82778bfba1753d80c5e7a090836890dc372bdc780986b2568b4fb8ed2a91c958e61db1f0b6b1cc96af4bd03ffc298ba92 + checksum: 10c0/0c20198886576050a6aa5ba6ae42f2b82778bfba1753d80c5e7a090836890dc372bdc780986b2568b4fb8ed2a91c958e61db1f0b6b1cc96af4bd03ffc298ba92 languageName: node linkType: hard @@ -454,29 +456,29 @@ __metadata: peerDependenciesMeta: supports-color: optional: true - checksum: cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 + checksum: 10c0/cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 languageName: node linkType: hard -"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.1": - version: 1.1.1 - resolution: "define-data-property@npm:1.1.1" +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": + version: 1.1.4 + resolution: "define-data-property@npm:1.1.4" dependencies: - get-intrinsic: "npm:^1.2.1" + es-define-property: "npm:^1.0.0" + es-errors: "npm:^1.3.0" gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.0" - checksum: 77ef6e0bceb515e05b5913ab635a84d537cee84f8a7c37c77fdcb31fc5b80f6dbe81b33375e4b67d96aa04e6a0d8d4ea099e431d83f089af8d93adfb584bcb94 + checksum: 10c0/dea0606d1483eb9db8d930d4eac62ca0fa16738b0b3e07046cddfacf7d8c868bbe13fa0cb263eb91c7d0d527960dc3f2f2471a69ed7816210307f6744fe62e37 languageName: node linkType: hard -"define-properties@npm:^1.1.4": +"define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: define-data-property: "npm:^1.0.1" has-property-descriptors: "npm:^1.0.0" object-keys: "npm:^1.1.1" - checksum: 88a152319ffe1396ccc6ded510a3896e77efac7a1bfbaa174a7b00414a1747377e0bb525d303794a47cf30e805c2ec84e575758512c6e44a993076d29fd4e6c3 + checksum: 10c0/88a152319ffe1396ccc6ded510a3896e77efac7a1bfbaa174a7b00414a1747377e0bb525d303794a47cf30e805c2ec84e575758512c6e44a993076d29fd4e6c3 languageName: node linkType: hard @@ -486,7 +488,7 @@ __metadata: dependencies: inherits: "npm:^2.0.1" minimalistic-assert: "npm:^1.0.0" - checksum: 671354943ad67493e49eb4c555480ab153edd7cee3a51c658082fcde539d2690ed2a4a0b5d1f401f9cde822edf3939a6afb2585f32c091f2d3a1b1665cd45236 + checksum: 10c0/671354943ad67493e49eb4c555480ab153edd7cee3a51c658082fcde539d2690ed2a4a0b5d1f401f9cde822edf3939a6afb2585f32c091f2d3a1b1665cd45236 languageName: node linkType: hard @@ -497,27 +499,27 @@ __metadata: bn.js: "npm:^4.1.0" miller-rabin: "npm:^4.0.0" randombytes: "npm:^2.0.0" - checksum: ce53ccafa9ca544b7fc29b08a626e23a9b6562efc2a98559a0c97b4718937cebaa9b5d7d0a05032cc9c1435e9b3c1532b9e9bf2e0ede868525922807ad6e1ecf + checksum: 10c0/ce53ccafa9ca544b7fc29b08a626e23a9b6562efc2a98559a0c97b4718937cebaa9b5d7d0a05032cc9c1435e9b3c1532b9e9bf2e0ede868525922807ad6e1ecf languageName: node linkType: hard "domain-browser@npm:^1.1.1": version: 1.2.0 resolution: "domain-browser@npm:1.2.0" - checksum: a955f482f4b4710fbd77c12a33e77548d63603c30c80f61a80519f27e3db1ba8530b914584cc9e9365d2038753d6b5bd1f4e6c81e432b007b0ec95b8b5e69b1b + checksum: 10c0/a955f482f4b4710fbd77c12a33e77548d63603c30c80f61a80519f27e3db1ba8530b914584cc9e9365d2038753d6b5bd1f4e6c81e432b007b0ec95b8b5e69b1b languageName: node linkType: hard "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" - checksum: 26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 languageName: node linkType: hard -"elliptic@npm:^6.5.3, elliptic@npm:^6.5.4": - version: 6.5.4 - resolution: "elliptic@npm:6.5.4" +"elliptic@npm:^6.5.3, elliptic@npm:^6.5.5": + version: 6.5.5 + resolution: "elliptic@npm:6.5.5" dependencies: bn.js: "npm:^4.11.9" brorand: "npm:^1.1.0" @@ -526,21 +528,21 @@ __metadata: inherits: "npm:^2.0.4" minimalistic-assert: "npm:^1.0.1" minimalistic-crypto-utils: "npm:^1.0.1" - checksum: 5f361270292c3b27cf0843e84526d11dec31652f03c2763c6c2b8178548175ff5eba95341dd62baff92b2265d1af076526915d8af6cc9cb7559c44a62f8ca6e2 + checksum: 10c0/3e591e93783a1b66f234ebf5bd3a8a9a8e063a75073a35a671e03e3b25253b6e33ac121f7efe9b8808890fffb17b40596cc19d01e6e8d1fa13b9a56ff65597c8 languageName: node linkType: hard "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" - checksum: b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 languageName: node linkType: hard "emoji-regex@npm:^9.2.2": version: 9.2.2 resolution: "emoji-regex@npm:9.2.2" - checksum: af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 languageName: node linkType: hard @@ -549,28 +551,44 @@ __metadata: resolution: "encoding@npm:0.1.13" dependencies: iconv-lite: "npm:^0.6.2" - checksum: 36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 languageName: node linkType: hard "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" - checksum: 285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 languageName: node linkType: hard "err-code@npm:^2.0.2": version: 2.0.3 resolution: "err-code@npm:2.0.3" - checksum: b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"es-define-property@npm:^1.0.0": + version: 1.0.0 + resolution: "es-define-property@npm:1.0.0" + dependencies: + get-intrinsic: "npm:^1.2.4" + checksum: 10c0/6bf3191feb7ea2ebda48b577f69bdfac7a2b3c9bcf97307f55fd6ef1bbca0b49f0c219a935aca506c993d8c5d8bddd937766cb760cd5e5a1071351f2df9f9aa4 + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 languageName: node linkType: hard "events@npm:^3.0.0": version: 3.3.0 resolution: "events@npm:3.3.0" - checksum: d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 + checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 languageName: node linkType: hard @@ -581,14 +599,14 @@ __metadata: md5.js: "npm:^1.3.4" node-gyp: "npm:latest" safe-buffer: "npm:^5.1.1" - checksum: 77fbe2d94a902a80e9b8f5a73dcd695d9c14899c5e82967a61b1fc6cbbb28c46552d9b127cff47c45fcf684748bdbcfa0a50410349109de87ceb4b199ef6ee99 + checksum: 10c0/77fbe2d94a902a80e9b8f5a73dcd695d9c14899c5e82967a61b1fc6cbbb28c46552d9b127cff47c45fcf684748bdbcfa0a50410349109de87ceb4b199ef6ee99 languageName: node linkType: hard "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" - checksum: 160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + checksum: 10c0/160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 languageName: node linkType: hard @@ -598,7 +616,7 @@ __metadata: dependencies: cross-spawn: "npm:^7.0.0" signal-exit: "npm:^4.0.1" - checksum: 9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 + checksum: 10c0/9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 languageName: node linkType: hard @@ -607,7 +625,7 @@ __metadata: resolution: "fs-minipass@npm:2.1.0" dependencies: minipass: "npm:^3.0.0" - checksum: 703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 languageName: node linkType: hard @@ -616,41 +634,42 @@ __metadata: resolution: "fs-minipass@npm:3.0.3" dependencies: minipass: "npm:^7.0.3" - checksum: 63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 languageName: node linkType: hard "function-bind@npm:^1.1.2": version: 1.1.2 resolution: "function-bind@npm:1.1.2" - checksum: d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 + checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 languageName: node linkType: hard -"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2": - version: 1.2.2 - resolution: "get-intrinsic@npm:1.2.2" +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4": + version: 1.2.4 + resolution: "get-intrinsic@npm:1.2.4" dependencies: + es-errors: "npm:^1.3.0" function-bind: "npm:^1.1.2" has-proto: "npm:^1.0.1" has-symbols: "npm:^1.0.3" hasown: "npm:^2.0.0" - checksum: 4e7fb8adc6172bae7c4fe579569b4d5238b3667c07931cd46b4eee74bbe6ff6b91329bec311a638d8e60f5b51f44fe5445693c6be89ae88d4b5c49f7ff12db0b + checksum: 10c0/0a9b82c16696ed6da5e39b1267104475c47e3a9bdbe8b509dfe1710946e38a87be70d759f4bb3cda042d76a41ef47fe769660f3b7c0d1f68750299344ffb15b7 languageName: node linkType: hard "glob@npm:^10.2.2, glob@npm:^10.3.10": - version: 10.3.10 - resolution: "glob@npm:10.3.10" + version: 10.3.16 + resolution: "glob@npm:10.3.16" dependencies: foreground-child: "npm:^3.1.0" - jackspeak: "npm:^2.3.5" + jackspeak: "npm:^3.1.2" minimatch: "npm:^9.0.1" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry: "npm:^1.10.1" + minipass: "npm:^7.0.4" + path-scurry: "npm:^1.11.0" bin: glob: dist/esm/bin.mjs - checksum: 13d8a1feb7eac7945f8c8480e11cd4a44b24d26503d99a8d8ac8d5aefbf3e9802a2b6087318a829fad04cb4e829f25c5f4f1110c68966c498720dd261c7e344d + checksum: 10c0/f7eb4c3e66f221f0be3967c02527047167967549bdf8ed1bd5f6277d43a35191af4e2bb8c89f07a79664958bae088fd06659e69a0f1de462972f1eab52a715e8 languageName: node linkType: hard @@ -659,37 +678,37 @@ __metadata: resolution: "gopd@npm:1.0.1" dependencies: get-intrinsic: "npm:^1.1.3" - checksum: 505c05487f7944c552cee72087bf1567debb470d4355b1335f2c262d218ebbff805cd3715448fe29b4b380bae6912561d0467233e4165830efd28da241418c63 + checksum: 10c0/505c05487f7944c552cee72087bf1567debb470d4355b1335f2c262d218ebbff805cd3715448fe29b4b380bae6912561d0467233e4165830efd28da241418c63 languageName: node linkType: hard "graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" - checksum: 386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 languageName: node linkType: hard -"has-property-descriptors@npm:^1.0.0": - version: 1.0.1 - resolution: "has-property-descriptors@npm:1.0.1" +"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": + version: 1.0.2 + resolution: "has-property-descriptors@npm:1.0.2" dependencies: - get-intrinsic: "npm:^1.2.2" - checksum: d62ba94b40150b00d621bc64a6aedb5bf0ee495308b4b7ed6bac856043db3cdfb1db553ae81cec91c9d2bd82057ff0e94145e7fa25d5aa5985ed32e0921927f6 + es-define-property: "npm:^1.0.0" + checksum: 10c0/253c1f59e80bb476cf0dde8ff5284505d90c3bdb762983c3514d36414290475fe3fd6f574929d84de2a8eec00d35cf07cb6776205ff32efd7c50719125f00236 languageName: node linkType: hard "has-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "has-proto@npm:1.0.1" - checksum: c8a8fe411f810b23a564bd5546a8f3f0fff6f1b692740eb7a2fdc9df716ef870040806891e2f23ff4653f1083e3895bf12088703dd1a0eac3d9202d3a4768cd0 + version: 1.0.3 + resolution: "has-proto@npm:1.0.3" + checksum: 10c0/35a6989f81e9f8022c2f4027f8b48a552de714938765d019dbea6bb547bd49ce5010a3c7c32ec6ddac6e48fc546166a3583b128f5a7add8b058a6d8b4afec205 languageName: node linkType: hard "has-symbols@npm:^1.0.3": version: 1.0.3 resolution: "has-symbols@npm:1.0.3" - checksum: e6922b4345a3f37069cdfe8600febbca791c94988c01af3394d86ca3360b4b93928bbf395859158f88099cb10b19d98e3bbab7c9ff2c1bd09cf665ee90afa2c3 + checksum: 10c0/e6922b4345a3f37069cdfe8600febbca791c94988c01af3394d86ca3360b4b93928bbf395859158f88099cb10b19d98e3bbab7c9ff2c1bd09cf665ee90afa2c3 languageName: node linkType: hard @@ -700,7 +719,17 @@ __metadata: inherits: "npm:^2.0.4" readable-stream: "npm:^3.6.0" safe-buffer: "npm:^5.2.0" - checksum: 663eabcf4173326fbb65a1918a509045590a26cc7e0964b754eef248d281305c6ec9f6b31cb508d02ffca383ab50028180ce5aefe013e942b44a903ac8dc80d0 + checksum: 10c0/663eabcf4173326fbb65a1918a509045590a26cc7e0964b754eef248d281305c6ec9f6b31cb508d02ffca383ab50028180ce5aefe013e942b44a903ac8dc80d0 + languageName: node + linkType: hard + +"hash-base@npm:~3.0": + version: 3.0.4 + resolution: "hash-base@npm:3.0.4" + dependencies: + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/a13357dccb3827f0bb0b56bf928da85c428dc8670f6e4a1c7265e4f1653ce02d69030b40fd01b0f1d218a995a066eea279cded9cec72d207b593bcdfe309c2f0 languageName: node linkType: hard @@ -710,16 +739,16 @@ __metadata: dependencies: inherits: "npm:^2.0.3" minimalistic-assert: "npm:^1.0.1" - checksum: 41ada59494eac5332cfc1ce6b7ebdd7b88a3864a6d6b08a3ea8ef261332ed60f37f10877e0c825aaa4bddebf164fbffa618286aeeec5296675e2671cbfa746c4 + checksum: 10c0/41ada59494eac5332cfc1ce6b7ebdd7b88a3864a6d6b08a3ea8ef261332ed60f37f10877e0c825aaa4bddebf164fbffa618286aeeec5296675e2671cbfa746c4 languageName: node linkType: hard "hasown@npm:^2.0.0": - version: 2.0.0 - resolution: "hasown@npm:2.0.0" + version: 2.0.2 + resolution: "hasown@npm:2.0.2" dependencies: function-bind: "npm:^1.1.2" - checksum: 5d415b114f410661208c95e7ab4879f1cc2765b8daceff4dc8718317d1cb7b9ffa7c5d1eafd9a4389c9aab7445d6ea88e05f3096cb1e529618b55304956b87fc + checksum: 10c0/3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9 languageName: node linkType: hard @@ -730,41 +759,41 @@ __metadata: hash.js: "npm:^1.0.3" minimalistic-assert: "npm:^1.0.0" minimalistic-crypto-utils: "npm:^1.0.1" - checksum: f3d9ba31b40257a573f162176ac5930109816036c59a09f901eb2ffd7e5e705c6832bedfff507957125f2086a0ab8f853c0df225642a88bf1fcaea945f20600d + checksum: 10c0/f3d9ba31b40257a573f162176ac5930109816036c59a09f901eb2ffd7e5e705c6832bedfff507957125f2086a0ab8f853c0df225642a88bf1fcaea945f20600d languageName: node linkType: hard "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" - checksum: ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc languageName: node linkType: hard "http-proxy-agent@npm:^7.0.0": - version: 7.0.0 - resolution: "http-proxy-agent@npm:7.0.0" + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" dependencies: agent-base: "npm:^7.1.0" debug: "npm:^4.3.4" - checksum: a11574ff39436cee3c7bc67f259444097b09474605846ddd8edf0bf4ad8644be8533db1aa463426e376865047d05dc22755e638632819317c0c2f1b2196657c8 + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 languageName: node linkType: hard "https-browserify@npm:^1.0.0": version: 1.0.0 resolution: "https-browserify@npm:1.0.0" - checksum: e17b6943bc24ea9b9a7da5714645d808670af75a425f29baffc3284962626efdc1eb3aa9bbffaa6e64028a6ad98af5b09fabcb454a8f918fb686abfdc9e9b8ae + checksum: 10c0/e17b6943bc24ea9b9a7da5714645d808670af75a425f29baffc3284962626efdc1eb3aa9bbffaa6e64028a6ad98af5b09fabcb454a8f918fb686abfdc9e9b8ae languageName: node linkType: hard "https-proxy-agent@npm:^7.0.1": - version: 7.0.2 - resolution: "https-proxy-agent@npm:7.0.2" + version: 7.0.4 + resolution: "https-proxy-agent@npm:7.0.4" dependencies: agent-base: "npm:^7.0.2" debug: "npm:4" - checksum: 7735eb90073db087e7e79312e3d97c8c04baf7ea7ca7b013382b6a45abbaa61b281041a98f4e13c8c80d88f843785bcc84ba189165b4b4087b1e3496ba656d77 + checksum: 10c0/bc4f7c38da32a5fc622450b6cb49a24ff596f9bd48dcedb52d2da3fa1c1a80e100fb506bd59b326c012f21c863c69b275c23de1a01d0b84db396822fdf25e52b languageName: node linkType: hard @@ -773,126 +802,127 @@ __metadata: resolution: "iconv-lite@npm:0.6.3" dependencies: safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 languageName: node linkType: hard "ieee754@npm:^1.1.4": version: 1.2.1 resolution: "ieee754@npm:1.2.1" - checksum: b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb + checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb languageName: node linkType: hard "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" - checksum: 8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 languageName: node linkType: hard "indent-string@npm:^4.0.0": version: 4.0.0 resolution: "indent-string@npm:4.0.0" - checksum: 1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f languageName: node linkType: hard "inherits@npm:2.0.3": version: 2.0.3 resolution: "inherits@npm:2.0.3" - checksum: 6e56402373149ea076a434072671f9982f5fad030c7662be0332122fe6c0fa490acb3cc1010d90b6eff8d640b1167d77674add52dfd1bb85d545cf29e80e73e7 + checksum: 10c0/6e56402373149ea076a434072671f9982f5fad030c7662be0332122fe6c0fa490acb3cc1010d90b6eff8d640b1167d77674add52dfd1bb85d545cf29e80e73e7 languageName: node linkType: hard "inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" - checksum: 4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 + checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 languageName: node linkType: hard -"ip@npm:^2.0.0": - version: 2.0.0 - resolution: "ip@npm:2.0.0" - checksum: 8d186cc5585f57372847ae29b6eba258c68862055e18a75cc4933327232cb5c107f89800ce29715d542eef2c254fbb68b382e780a7414f9ee7caf60b7a473958 +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc languageName: node linkType: hard "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" - checksum: bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc languageName: node linkType: hard "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" - checksum: 85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d languageName: node linkType: hard "isarray@npm:^1.0.0, isarray@npm:~1.0.0": version: 1.0.0 resolution: "isarray@npm:1.0.0" - checksum: 18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d + checksum: 10c0/18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d languageName: node linkType: hard "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" - checksum: 228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d languageName: node linkType: hard "isexe@npm:^3.1.1": version: 3.1.1 resolution: "isexe@npm:3.1.1" - checksum: 9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 languageName: node linkType: hard -"jackspeak@npm:^2.3.5": - version: 2.3.6 - resolution: "jackspeak@npm:2.3.6" +"jackspeak@npm:^3.1.2": + version: 3.1.2 + resolution: "jackspeak@npm:3.1.2" dependencies: "@isaacs/cliui": "npm:^8.0.2" "@pkgjs/parseargs": "npm:^0.11.0" dependenciesMeta: "@pkgjs/parseargs": optional: true - checksum: f01d8f972d894cd7638bc338e9ef5ddb86f7b208ce177a36d718eac96ec86638a6efa17d0221b10073e64b45edc2ce15340db9380b1f5d5c5d000cbc517dc111 + checksum: 10c0/5f1922a1ca0f19869e23f0dc4374c60d36e922f7926c76fecf8080cc6f7f798d6a9caac1b9428327d14c67731fd551bb3454cb270a5e13a0718f3b3660ec3d5d languageName: node linkType: hard -"lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.1.0 - resolution: "lru-cache@npm:10.1.0" - checksum: 778bc8b2626daccd75f24c4b4d10632496e21ba064b126f526c626fbdbc5b28c472013fccd45d7646b9e1ef052444824854aed617b59cd570d01a8b7d651fc1e +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 languageName: node linkType: hard -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: "npm:^4.0.0" - checksum: cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.2.2 + resolution: "lru-cache@npm:10.2.2" + checksum: 10c0/402d31094335851220d0b00985084288136136992979d0e015f0f1697e15d1c86052d7d53ae86b614e5b058425606efffc6969a31a091085d7a2b80a8a1e26d6 languageName: node linkType: hard -"luxon@npm:^3.4.2": +"luxon@npm:^3.4.4": version: 3.4.4 resolution: "luxon@npm:3.4.4" - checksum: 02e26a0b039c11fd5b75e1d734c8f0332c95510f6a514a9a0991023e43fb233884da02d7f966823ffb230632a733fc86d4a4b1e63c3fbe00058b8ee0f8c728af + checksum: 10c0/02e26a0b039c11fd5b75e1d734c8f0332c95510f6a514a9a0991023e43fb233884da02d7f966823ffb230632a733fc86d4a4b1e63c3fbe00058b8ee0f8c728af languageName: node linkType: hard "make-fetch-happen@npm:^13.0.0": - version: 13.0.0 - resolution: "make-fetch-happen@npm:13.0.0" + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" dependencies: "@npmcli/agent": "npm:^2.0.0" cacache: "npm:^18.0.0" @@ -903,9 +933,10 @@ __metadata: minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" promise-retry: "npm:^2.0.1" ssri: "npm:^10.0.0" - checksum: 43b9f6dcbc6fe8b8604cb6396957c3698857a15ba4dbc38284f7f0e61f248300585ef1eb8cc62df54e9c724af977e45b5cdfd88320ef7f53e45070ed3488da55 + checksum: 10c0/df5f4dbb6d98153b751bccf4dc4cc500de85a96a9331db9805596c46aa9f99d9555983954e6c1266d9f981ae37a9e4647f42b9a4bb5466f867f4012e582c9e7e languageName: node linkType: hard @@ -916,7 +947,7 @@ __metadata: hash-base: "npm:^3.0.0" inherits: "npm:^2.0.1" safe-buffer: "npm:^5.1.2" - checksum: b7bd75077f419c8e013fc4d4dada48be71882e37d69a44af65a2f2804b91e253441eb43a0614423a1c91bb830b8140b0dc906bc797245e2e275759584f4efcc5 + checksum: 10c0/b7bd75077f419c8e013fc4d4dada48be71882e37d69a44af65a2f2804b91e253441eb43a0614423a1c91bb830b8140b0dc906bc797245e2e275759584f4efcc5 languageName: node linkType: hard @@ -928,30 +959,30 @@ __metadata: brorand: "npm:^1.0.1" bin: miller-rabin: bin/miller-rabin - checksum: 26b2b96f6e49dbcff7faebb78708ed2f5f9ae27ac8cbbf1d7c08f83cf39bed3d418c0c11034dce997da70d135cc0ff6f3a4c15dc452f8e114c11986388a64346 + checksum: 10c0/26b2b96f6e49dbcff7faebb78708ed2f5f9ae27ac8cbbf1d7c08f83cf39bed3d418c0c11034dce997da70d135cc0ff6f3a4c15dc452f8e114c11986388a64346 languageName: node linkType: hard "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" - checksum: 96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd + checksum: 10c0/96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd languageName: node linkType: hard "minimalistic-crypto-utils@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-crypto-utils@npm:1.0.1" - checksum: 790ecec8c5c73973a4fbf2c663d911033e8494d5fb0960a4500634766ab05d6107d20af896ca2132e7031741f19888154d44b2408ada0852446705441383e9f8 + checksum: 10c0/790ecec8c5c73973a4fbf2c663d911033e8494d5fb0960a4500634766ab05d6107d20af896ca2132e7031741f19888154d44b2408ada0852446705441383e9f8 languageName: node linkType: hard "minimatch@npm:^9.0.1": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" + version: 9.0.4 + resolution: "minimatch@npm:9.0.4" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 85f407dcd38ac3e180f425e86553911d101455ca3ad5544d6a7cec16286657e4f8a9aa6695803025c55e31e35a91a2252b5dc8e7d527211278b8b65b4dbd5eac + checksum: 10c0/2c16f21f50e64922864e560ff97c587d15fd491f65d92a677a344e970fe62aafdbeafe648965fa96d33c061b4d0eabfe0213466203dd793367e7f28658cf6414 languageName: node linkType: hard @@ -960,13 +991,13 @@ __metadata: resolution: "minipass-collect@npm:2.0.1" dependencies: minipass: "npm:^7.0.3" - checksum: 5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e languageName: node linkType: hard "minipass-fetch@npm:^3.0.0": - version: 3.0.4 - resolution: "minipass-fetch@npm:3.0.4" + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" dependencies: encoding: "npm:^0.1.13" minipass: "npm:^7.0.3" @@ -975,7 +1006,7 @@ __metadata: dependenciesMeta: encoding: optional: true - checksum: 1b63c1f3313e88eeac4689f1b71c9f086598db9a189400e3ee960c32ed89e06737fa23976c9305c2d57464fb3fcdc12749d3378805c9d6176f5569b0d0ee8a75 + checksum: 10c0/9d702d57f556274286fdd97e406fc38a2f5c8d15e158b498d7393b1105974b21249289ec571fa2b51e038a4872bfc82710111cf75fae98c662f3d6f95e72152b languageName: node linkType: hard @@ -984,7 +1015,7 @@ __metadata: resolution: "minipass-flush@npm:1.0.5" dependencies: minipass: "npm:^3.0.0" - checksum: 2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd languageName: node linkType: hard @@ -993,7 +1024,7 @@ __metadata: resolution: "minipass-pipeline@npm:1.2.4" dependencies: minipass: "npm:^3.0.0" - checksum: cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 languageName: node linkType: hard @@ -1002,7 +1033,7 @@ __metadata: resolution: "minipass-sized@npm:1.0.3" dependencies: minipass: "npm:^3.0.0" - checksum: 298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb languageName: node linkType: hard @@ -1011,21 +1042,21 @@ __metadata: resolution: "minipass@npm:3.3.6" dependencies: yallist: "npm:^4.0.0" - checksum: a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c languageName: node linkType: hard "minipass@npm:^5.0.0": version: 5.0.0 resolution: "minipass@npm:5.0.0" - checksum: a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + checksum: 10c0/a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": - version: 7.0.4 - resolution: "minipass@npm:7.0.4" - checksum: 6c7370a6dfd257bf18222da581ba89a5eaedca10e158781232a8b5542a90547540b4b9b7e7f490e4cda43acfbd12e086f0453728ecf8c19e0ef6921bc5958ac5 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4": + version: 7.1.1 + resolution: "minipass@npm:7.1.1" + checksum: 10c0/fdccc2f99c31083f45f881fd1e6971d798e333e078ab3c8988fb818c470fbd5e935388ad9adb286397eba50baebf46ef8ff487c8d3f455a69c6f3efc327bdff9 languageName: node linkType: hard @@ -1035,7 +1066,7 @@ __metadata: dependencies: minipass: "npm:^3.0.0" yallist: "npm:^4.0.0" - checksum: 64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + checksum: 10c0/64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 languageName: node linkType: hard @@ -1044,27 +1075,27 @@ __metadata: resolution: "mkdirp@npm:1.0.4" bin: mkdirp: bin/cmd.js - checksum: 46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + checksum: 10c0/46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf languageName: node linkType: hard "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" - checksum: a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc + checksum: 10c0/a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc languageName: node linkType: hard "negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" - checksum: 3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + checksum: 10c0/3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 languageName: node linkType: hard "node-gyp@npm:latest": - version: 10.0.1 - resolution: "node-gyp@npm:10.0.1" + version: 10.1.0 + resolution: "node-gyp@npm:10.1.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" @@ -1078,7 +1109,7 @@ __metadata: which: "npm:^4.0.0" bin: node-gyp: bin/node-gyp.js - checksum: abddfff7d873312e4ed4a5fb75ce893a5c4fb69e7fcb1dfa71c28a6b92a7f1ef6b62790dffb39181b5a82728ba8f2f32d229cf8cbe66769fe02cea7db4a555aa + checksum: 10c0/9cc821111ca244a01fb7f054db7523ab0a0cd837f665267eb962eb87695d71fb1e681f9e21464cc2fd7c05530dc4c81b810bca1a88f7d7186909b74477491a3c languageName: node linkType: hard @@ -1109,51 +1140,51 @@ __metadata: url: "npm:^0.11.0" util: "npm:^0.11.0" vm-browserify: "npm:^1.0.1" - checksum: 0e05321a6396408903ed642231d2bca7dd96492d074c7af161ba06a63c95378bd3de50b4105eccbbc02d93ba3da69f0ff5e624bc2a8c92ca462ceb6a403e7986 + checksum: 10c0/0e05321a6396408903ed642231d2bca7dd96492d074c7af161ba06a63c95378bd3de50b4105eccbbc02d93ba3da69f0ff5e624bc2a8c92ca462ceb6a403e7986 languageName: node linkType: hard "nopt@npm:^7.0.0": - version: 7.2.0 - resolution: "nopt@npm:7.2.0" + version: 7.2.1 + resolution: "nopt@npm:7.2.1" dependencies: abbrev: "npm:^2.0.0" bin: nopt: bin/nopt.js - checksum: 9bd7198df6f16eb29ff16892c77bcf7f0cc41f9fb5c26280ac0def2cf8cf319f3b821b3af83eba0e74c85807cc430a16efe0db58fe6ae1f41e69519f585b6aff + checksum: 10c0/a069c7c736767121242037a22a788863accfa932ab285a1eb569eb8cd534b09d17206f68c37f096ae785647435e0c5a5a0a67b42ec743e481a455e5ae6a6df81 languageName: node linkType: hard -"object-inspect@npm:^1.9.0": +"object-inspect@npm:^1.13.1": version: 1.13.1 resolution: "object-inspect@npm:1.13.1" - checksum: fad603f408e345c82e946abdf4bfd774260a5ed3e5997a0b057c44153ac32c7271ff19e3a5ae39c858da683ba045ccac2f65245c12763ce4e8594f818f4a648d + checksum: 10c0/fad603f408e345c82e946abdf4bfd774260a5ed3e5997a0b057c44153ac32c7271ff19e3a5ae39c858da683ba045ccac2f65245c12763ce4e8594f818f4a648d languageName: node linkType: hard "object-keys@npm:^1.1.1": version: 1.1.1 resolution: "object-keys@npm:1.1.1" - checksum: b11f7ccdbc6d406d1f186cdadb9d54738e347b2692a14439ca5ac70c225fa6db46db809711b78589866d47b25fc3e8dee0b4c722ac751e11180f9380e3d8601d + checksum: 10c0/b11f7ccdbc6d406d1f186cdadb9d54738e347b2692a14439ca5ac70c225fa6db46db809711b78589866d47b25fc3e8dee0b4c722ac751e11180f9380e3d8601d languageName: node linkType: hard "object.assign@npm:^4.1.4": - version: 4.1.4 - resolution: "object.assign@npm:4.1.4" + version: 4.1.5 + resolution: "object.assign@npm:4.1.5" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.1.4" + call-bind: "npm:^1.0.5" + define-properties: "npm:^1.2.1" has-symbols: "npm:^1.0.3" object-keys: "npm:^1.1.1" - checksum: 2f286118c023e557757620e647b02e7c88d3d417e0c568fca0820de8ec9cca68928304854d5b03e99763eddad6e78a6716e2930f7e6372e4b9b843f3fd3056f3 + checksum: 10c0/60108e1fa2706f22554a4648299b0955236c62b3685c52abf4988d14fffb0e7731e00aa8c6448397e3eb63d087dcc124a9f21e1980f36d0b2667f3c18bacd469 languageName: node linkType: hard "os-browserify@npm:^0.3.0": version: 0.3.0 resolution: "os-browserify@npm:0.3.0" - checksum: 6ff32cb1efe2bc6930ad0fd4c50e30c38010aee909eba8d65be60af55efd6cbb48f0287e3649b4e3f3a63dce5a667b23c187c4293a75e557f0d5489d735bcf52 + checksum: 10c0/6ff32cb1efe2bc6930ad0fd4c50e30c38010aee909eba8d65be60af55efd6cbb48f0287e3649b4e3f3a63dce5a667b23c187c4293a75e557f0d5489d735bcf52 languageName: node linkType: hard @@ -1162,55 +1193,56 @@ __metadata: resolution: "p-map@npm:4.0.0" dependencies: aggregate-error: "npm:^3.0.0" - checksum: 592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 languageName: node linkType: hard "pako@npm:~1.0.5": version: 1.0.11 resolution: "pako@npm:1.0.11" - checksum: 86dd99d8b34c3930345b8bbeb5e1cd8a05f608eeb40967b293f72fe469d0e9c88b783a8777e4cc7dc7c91ce54c5e93d88ff4b4f060e6ff18408fd21030d9ffbe + checksum: 10c0/86dd99d8b34c3930345b8bbeb5e1cd8a05f608eeb40967b293f72fe469d0e9c88b783a8777e4cc7dc7c91ce54c5e93d88ff4b4f060e6ff18408fd21030d9ffbe languageName: node linkType: hard -"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.6": - version: 5.1.6 - resolution: "parse-asn1@npm:5.1.6" +"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.7": + version: 5.1.7 + resolution: "parse-asn1@npm:5.1.7" dependencies: - asn1.js: "npm:^5.2.0" - browserify-aes: "npm:^1.0.0" - evp_bytestokey: "npm:^1.0.0" - pbkdf2: "npm:^3.0.3" - safe-buffer: "npm:^5.1.1" - checksum: 4ed1d9b9e120c5484d29d67bb90171aac0b73422bc016d6294160aea983275c28a27ab85d862059a36a86a97dd31b7ddd97486802ca9fac67115fe3409e9dcbd + asn1.js: "npm:^4.10.1" + browserify-aes: "npm:^1.2.0" + evp_bytestokey: "npm:^1.0.3" + hash-base: "npm:~3.0" + pbkdf2: "npm:^3.1.2" + safe-buffer: "npm:^5.2.1" + checksum: 10c0/05eb5937405c904eb5a7f3633bab1acc11f4ae3478a07ef5c6d81ce88c3c0e505ff51f9c7b935ebc1265c868343793698fc91025755a895d0276f620f95e8a82 languageName: node linkType: hard "path-browserify@npm:0.0.1": version: 0.0.1 resolution: "path-browserify@npm:0.0.1" - checksum: 3d59710cddeea06509d91935196185900f3d9d29376dff68ff0e146fbd41d0fb304e983d0158f30cabe4dd2ffcc6a7d3d977631994ee984c88e66aed50a1ccd3 + checksum: 10c0/3d59710cddeea06509d91935196185900f3d9d29376dff68ff0e146fbd41d0fb304e983d0158f30cabe4dd2ffcc6a7d3d977631994ee984c88e66aed50a1ccd3 languageName: node linkType: hard "path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" - checksum: 748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c languageName: node linkType: hard -"path-scurry@npm:^1.10.1": - version: 1.10.1 - resolution: "path-scurry@npm:1.10.1" +"path-scurry@npm:^1.11.0": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" dependencies: - lru-cache: "npm:^9.1.1 || ^10.0.0" + lru-cache: "npm:^10.2.0" minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: e5dc78a7348d25eec61ab166317e9e9c7b46818aa2c2b9006c507a6ff48c672d011292d9662527213e558f5652ce0afcc788663a061d8b59ab495681840c0c1e + checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d languageName: node linkType: hard -"pbkdf2@npm:^3.0.3": +"pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.1.2": version: 3.1.2 resolution: "pbkdf2@npm:3.1.2" dependencies: @@ -1219,28 +1251,35 @@ __metadata: ripemd160: "npm:^2.0.1" safe-buffer: "npm:^5.0.1" sha.js: "npm:^2.4.8" - checksum: 5a30374e87d33fa080a92734d778cf172542cc7e41b96198c4c88763997b62d7850de3fbda5c3111ddf79805ee7c1da7046881c90ac4920b5e324204518b05fd + checksum: 10c0/5a30374e87d33fa080a92734d778cf172542cc7e41b96198c4c88763997b62d7850de3fbda5c3111ddf79805ee7c1da7046881c90ac4920b5e324204518b05fd languageName: node linkType: hard "proc-log@npm:^3.0.0": version: 3.0.0 resolution: "proc-log@npm:3.0.0" - checksum: f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + checksum: 10c0/f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + languageName: node + linkType: hard + +"proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 10c0/17db4757c2a5c44c1e545170e6c70a26f7de58feb985091fb1763f5081cab3d01b181fb2dd240c9f4a4255a1d9227d163d5771b7e69c9e49a561692db865efb9 languageName: node linkType: hard "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" - checksum: bec089239487833d46b59d80327a1605e1c5287eaad770a291add7f45fda1bb5e28b38e0e061add0a1d0ee0984788ce74fa394d345eed1c420cacf392c554367 + checksum: 10c0/bec089239487833d46b59d80327a1605e1c5287eaad770a291add7f45fda1bb5e28b38e0e061add0a1d0ee0984788ce74fa394d345eed1c420cacf392c554367 languageName: node linkType: hard "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" - checksum: 40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3 + checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3 languageName: node linkType: hard @@ -1250,7 +1289,7 @@ __metadata: dependencies: err-code: "npm:^2.0.2" retry: "npm:^0.12.0" - checksum: 9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 languageName: node linkType: hard @@ -1264,30 +1303,30 @@ __metadata: parse-asn1: "npm:^5.0.0" randombytes: "npm:^2.0.1" safe-buffer: "npm:^5.1.2" - checksum: 6c2cc19fbb554449e47f2175065d6b32f828f9b3badbee4c76585ac28ae8641aafb9bb107afc430c33c5edd6b05dbe318df4f7d6d7712b1093407b11c4280700 + checksum: 10c0/6c2cc19fbb554449e47f2175065d6b32f828f9b3badbee4c76585ac28ae8641aafb9bb107afc430c33c5edd6b05dbe318df4f7d6d7712b1093407b11c4280700 languageName: node linkType: hard "punycode@npm:^1.2.4, punycode@npm:^1.4.1": version: 1.4.1 resolution: "punycode@npm:1.4.1" - checksum: 354b743320518aef36f77013be6e15da4db24c2b4f62c5f1eb0529a6ed02fbaf1cb52925785f6ab85a962f2b590d9cd5ad730b70da72b5f180e2556b8bd3ca08 + checksum: 10c0/354b743320518aef36f77013be6e15da4db24c2b4f62c5f1eb0529a6ed02fbaf1cb52925785f6ab85a962f2b590d9cd5ad730b70da72b5f180e2556b8bd3ca08 languageName: node linkType: hard "qs@npm:^6.11.2": - version: 6.11.2 - resolution: "qs@npm:6.11.2" + version: 6.12.1 + resolution: "qs@npm:6.12.1" dependencies: - side-channel: "npm:^1.0.4" - checksum: 4f95d4ff18ed480befcafa3390022817ffd3087fc65f146cceb40fc5edb9fa96cb31f648cae2fa96ca23818f0798bd63ad4ca369a0e22702fcd41379b3ab6571 + side-channel: "npm:^1.0.6" + checksum: 10c0/439e6d7c6583e7c69f2cab2c39c55b97db7ce576e4c7c469082b938b7fc8746e8d547baacb69b4cd2b6666484776c3f4840ad7163a4c5326300b0afa0acdd84b languageName: node linkType: hard "querystring-es3@npm:^0.2.0": version: 0.2.1 resolution: "querystring-es3@npm:0.2.1" - checksum: 476938c1adb45c141f024fccd2ffd919a3746e79ed444d00e670aad68532977b793889648980e7ca7ff5ffc7bfece623118d0fbadcaf217495eeb7059ae51580 + checksum: 10c0/476938c1adb45c141f024fccd2ffd919a3746e79ed444d00e670aad68532977b793889648980e7ca7ff5ffc7bfece623118d0fbadcaf217495eeb7059ae51580 languageName: node linkType: hard @@ -1296,7 +1335,7 @@ __metadata: resolution: "randombytes@npm:2.1.0" dependencies: safe-buffer: "npm:^5.1.0" - checksum: 50395efda7a8c94f5dffab564f9ff89736064d32addf0cc7e8bf5e4166f09f8ded7a0849ca6c2d2a59478f7d90f78f20d8048bca3cdf8be09d8e8a10790388f3 + checksum: 10c0/50395efda7a8c94f5dffab564f9ff89736064d32addf0cc7e8bf5e4166f09f8ded7a0849ca6c2d2a59478f7d90f78f20d8048bca3cdf8be09d8e8a10790388f3 languageName: node linkType: hard @@ -1306,11 +1345,11 @@ __metadata: dependencies: randombytes: "npm:^2.0.5" safe-buffer: "npm:^5.1.0" - checksum: 11aeed35515872e8f8a2edec306734e6b74c39c46653607f03c68385ab8030e2adcc4215f76b5e4598e028c4750d820afd5c65202527d831d2a5f207fe2bc87c + checksum: 10c0/11aeed35515872e8f8a2edec306734e6b74c39c46653607f03c68385ab8030e2adcc4215f76b5e4598e028c4750d820afd5c65202527d831d2a5f207fe2bc87c languageName: node linkType: hard -"readable-stream@npm:^2.0.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.6": +"readable-stream@npm:^2.0.2, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.6, readable-stream@npm:^2.3.8": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -1321,32 +1360,32 @@ __metadata: safe-buffer: "npm:~5.1.1" string_decoder: "npm:~1.1.1" util-deprecate: "npm:~1.0.1" - checksum: 7efdb01f3853bc35ac62ea25493567bf588773213f5f4a79f9c365e1ad13bab845ac0dae7bc946270dc40c3929483228415e92a3fc600cc7e4548992f41ee3fa + checksum: 10c0/7efdb01f3853bc35ac62ea25493567bf588773213f5f4a79f9c365e1ad13bab845ac0dae7bc946270dc40c3929483228415e92a3fc600cc7e4548992f41ee3fa languageName: node linkType: hard -"readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": +"readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: inherits: "npm:^2.0.3" string_decoder: "npm:^1.1.1" util-deprecate: "npm:^1.0.1" - checksum: e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7 + checksum: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7 languageName: node linkType: hard "readline-sync@npm:^1.4.7": version: 1.4.10 resolution: "readline-sync@npm:1.4.10" - checksum: 0a4d0fe4ad501f8f005a3c9cbf3cc0ae6ca2ced93e9a1c7c46f226bdfcb6ef5d3f437ae7e9d2e1098ee13524a3739c830e4c8dbc7f543a693eecd293e41093a3 + checksum: 10c0/0a4d0fe4ad501f8f005a3c9cbf3cc0ae6ca2ced93e9a1c7c46f226bdfcb6ef5d3f437ae7e9d2e1098ee13524a3739c830e4c8dbc7f543a693eecd293e41093a3 languageName: node linkType: hard "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" - checksum: 59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe languageName: node linkType: hard @@ -1356,65 +1395,65 @@ __metadata: dependencies: hash-base: "npm:^3.0.0" inherits: "npm:^2.0.1" - checksum: f6f0df78817e78287c766687aed4d5accbebc308a8e7e673fb085b9977473c1f139f0c5335d353f172a915bb288098430755d2ad3c4f30612f4dd0c901cd2c3a + checksum: 10c0/f6f0df78817e78287c766687aed4d5accbebc308a8e7e673fb085b9977473c1f139f0c5335d353f172a915bb288098430755d2ad3c4f30612f4dd0c901cd2c3a languageName: node linkType: hard "safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" - checksum: 6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 languageName: node linkType: hard "safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": version: 5.1.2 resolution: "safe-buffer@npm:5.1.2" - checksum: 780ba6b5d99cc9a40f7b951d47152297d0e260f0df01472a1b99d4889679a4b94a13d644f7dbc4f022572f09ae9005fa2fbb93bbbd83643316f365a3e9a45b21 + checksum: 10c0/780ba6b5d99cc9a40f7b951d47152297d0e260f0df01472a1b99d4889679a4b94a13d644f7dbc4f022572f09ae9005fa2fbb93bbbd83643316f365a3e9a45b21 languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0": +"safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" - checksum: 7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 languageName: node linkType: hard -"sax@npm:^1.2.4": - version: 1.3.0 - resolution: "sax@npm:1.3.0" - checksum: 599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea +"sax@npm:^1.4.1": + version: 1.4.1 + resolution: "sax@npm:1.4.1" + checksum: 10c0/6bf86318a254c5d898ede6bd3ded15daf68ae08a5495a2739564eb265cd13bcc64a07ab466fb204f67ce472bb534eb8612dac587435515169593f4fffa11de7c languageName: node linkType: hard "semver@npm:^7.3.5": - version: 7.5.4 - resolution: "semver@npm:7.5.4" - dependencies: - lru-cache: "npm:^6.0.0" + version: 7.6.2 + resolution: "semver@npm:7.6.2" bin: semver: bin/semver.js - checksum: 5160b06975a38b11c1ab55950cb5b8a23db78df88275d3d8a42ccf1f29e55112ac995b3a26a522c36e3b5f76b0445f1eef70d696b8c7862a2b4303d7b0e7609e + checksum: 10c0/97d3441e97ace8be4b1976433d1c32658f6afaff09f143e52c593bae7eef33de19e3e369c88bd985ce1042c6f441c80c6803078d1de2a9988080b66684cbb30c languageName: node linkType: hard -"set-function-length@npm:^1.1.1": - version: 1.1.1 - resolution: "set-function-length@npm:1.1.1" +"set-function-length@npm:^1.2.1": + version: 1.2.2 + resolution: "set-function-length@npm:1.2.2" dependencies: - define-data-property: "npm:^1.1.1" - get-intrinsic: "npm:^1.2.1" + define-data-property: "npm:^1.1.4" + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + get-intrinsic: "npm:^1.2.4" gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.0" - checksum: a29e255c116c29e3323b851c4f46c58c91be9bb8b065f191e2ea1807cb2c839df56e3175732a498e0c6d54626ba6b6fef896bf699feb7ab70c42dc47eb247c95 + has-property-descriptors: "npm:^1.0.2" + checksum: 10c0/82850e62f412a258b71e123d4ed3873fa9377c216809551192bb6769329340176f109c2eeae8c22a8d386c76739855f78e8716515c818bcaef384b51110f0f3c languageName: node linkType: hard "setimmediate@npm:^1.0.4": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" - checksum: 5bae81bfdbfbd0ce992893286d49c9693c82b1bcc00dcaaf3a09c8f428fdeacf4190c013598b81875dfac2b08a572422db7df779a99332d0fce186d15a3e4d49 + checksum: 10c0/5bae81bfdbfbd0ce992893286d49c9693c82b1bcc00dcaaf3a09c8f428fdeacf4190c013598b81875dfac2b08a572422db7df779a99332d0fce186d15a3e4d49 languageName: node linkType: hard @@ -1426,20 +1465,20 @@ __metadata: safe-buffer: "npm:^5.0.1" bin: sha.js: ./bin.js - checksum: b7a371bca8821c9cc98a0aeff67444a03d48d745cb103f17228b96793f455f0eb0a691941b89ea1e60f6359207e36081d9be193252b0f128e0daf9cfea2815a5 + checksum: 10c0/b7a371bca8821c9cc98a0aeff67444a03d48d745cb103f17228b96793f455f0eb0a691941b89ea1e60f6359207e36081d9be193252b0f128e0daf9cfea2815a5 languageName: node linkType: hard "shadow-cljs-jar@npm:1.3.4": version: 1.3.4 resolution: "shadow-cljs-jar@npm:1.3.4" - checksum: c5548bb5f2bda5e0a90df6f42e4ec3a07ed4c72cdebb87619e8d9a2167bb3d4b60d6f6a305a3e15cbfb379d5fdbe2a989a0e7059b667cfb3911bc198a4489e94 + checksum: 10c0/c5548bb5f2bda5e0a90df6f42e4ec3a07ed4c72cdebb87619e8d9a2167bb3d4b60d6f6a305a3e15cbfb379d5fdbe2a989a0e7059b667cfb3911bc198a4489e94 languageName: node linkType: hard -"shadow-cljs@npm:2.27.4": - version: 2.27.4 - resolution: "shadow-cljs@npm:2.27.4" +"shadow-cljs@npm:2.28.11": + version: 2.28.11 + resolution: "shadow-cljs@npm:2.28.11" dependencies: node-libs-browser: "npm:^2.2.1" readline-sync: "npm:^1.4.7" @@ -1449,7 +1488,7 @@ __metadata: ws: "npm:^7.4.6" bin: shadow-cljs: cli/runner.js - checksum: bae23e71df9c2b2979259a0cde8747c923ee295f58ab4637c9d6b103d82542b40ef39172d4be2dbb94af2e6458a177d1ec96c1eb1e73b1d8f3a4ddb5eaaba7d4 + checksum: 10c0/c5c77d524ee8f44e4ae2ddc196af170d02405cc8731ea71f852c7b220fc1ba8aaf5cf33753fd8a7566c8749bb75d360f903dfb0d131bcdc6c2c33f44404bd6a3 languageName: node linkType: hard @@ -1458,60 +1497,61 @@ __metadata: resolution: "shebang-command@npm:2.0.0" dependencies: shebang-regex: "npm:^3.0.0" - checksum: a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e languageName: node linkType: hard "shebang-regex@npm:^3.0.0": version: 3.0.0 resolution: "shebang-regex@npm:3.0.0" - checksum: 1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 languageName: node linkType: hard -"side-channel@npm:^1.0.4": - version: 1.0.4 - resolution: "side-channel@npm:1.0.4" +"side-channel@npm:^1.0.6": + version: 1.0.6 + resolution: "side-channel@npm:1.0.6" dependencies: - call-bind: "npm:^1.0.0" - get-intrinsic: "npm:^1.0.2" - object-inspect: "npm:^1.9.0" - checksum: 054a5d23ee35054b2c4609b9fd2a0587760737782b5d765a9c7852264710cc39c6dcb56a9bbd6c12cd84071648aea3edb2359d2f6e560677eedadce511ac1da5 + call-bind: "npm:^1.0.7" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.4" + object-inspect: "npm:^1.13.1" + checksum: 10c0/d2afd163dc733cc0a39aa6f7e39bf0c436293510dbccbff446733daeaf295857dbccf94297092ec8c53e2503acac30f0b78830876f0485991d62a90e9cad305f languageName: node linkType: hard "signal-exit@npm:^4.0.1": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" - checksum: 41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 languageName: node linkType: hard "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" - checksum: a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.1": - version: 8.0.2 - resolution: "socks-proxy-agent@npm:8.0.2" +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.3 + resolution: "socks-proxy-agent@npm:8.0.3" dependencies: - agent-base: "npm:^7.0.2" + agent-base: "npm:^7.1.1" debug: "npm:^4.3.4" socks: "npm:^2.7.1" - checksum: a842402fc9b8848a31367f2811ca3cd14c4106588b39a0901cd7a69029998adfc6456b0203617c18ed090542ad0c24ee4e9d4c75a0c4b75071e214227c177eb7 + checksum: 10c0/4950529affd8ccd6951575e21c1b7be8531b24d924aa4df3ee32df506af34b618c4e50d261f4cc603f1bfd8d426915b7d629966c8ce45b05fb5ad8c8b9a6459d languageName: node linkType: hard "socks@npm:^2.7.1": - version: 2.7.1 - resolution: "socks@npm:2.7.1" + version: 2.8.3 + resolution: "socks@npm:2.8.3" dependencies: - ip: "npm:^2.0.0" + ip-address: "npm:^9.0.5" smart-buffer: "npm:^4.2.0" - checksum: 43f69dbc9f34fc8220bc51c6eea1c39715ab3cfdb115d6e3285f6c7d1a603c5c75655668a5bbc11e3c7e2c99d60321fb8d7ab6f38cda6a215fadd0d6d0b52130 + checksum: 10c0/d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 languageName: node linkType: hard @@ -1520,7 +1560,7 @@ __metadata: resolution: "source-map-support@npm:0.4.18" dependencies: source-map: "npm:^0.5.6" - checksum: cd9f0309c1632b1e01a7715a009e0b036d565f3af8930fa8cda2a06aeec05ad1d86180e743b7e1f02cc3c97abe8b6d8de7c3878c2d8e01e86e17f876f7ecf98e + checksum: 10c0/cd9f0309c1632b1e01a7715a009e0b036d565f3af8930fa8cda2a06aeec05ad1d86180e743b7e1f02cc3c97abe8b6d8de7c3878c2d8e01e86e17f876f7ecf98e languageName: node linkType: hard @@ -1530,30 +1570,37 @@ __metadata: dependencies: buffer-from: "npm:^1.0.0" source-map: "npm:^0.6.0" - checksum: 9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d + checksum: 10c0/9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d languageName: node linkType: hard "source-map@npm:^0.5.6": version: 0.5.7 resolution: "source-map@npm:0.5.7" - checksum: 904e767bb9c494929be013017380cbba013637da1b28e5943b566031e29df04fba57edf3f093e0914be094648b577372bd8ad247fa98cfba9c600794cd16b599 + checksum: 10c0/904e767bb9c494929be013017380cbba013637da1b28e5943b566031e29df04fba57edf3f093e0914be094648b577372bd8ad247fa98cfba9c600794cd16b599 languageName: node linkType: hard "source-map@npm:^0.6.0": version: 0.6.1 resolution: "source-map@npm:0.6.1" - checksum: ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec languageName: node linkType: hard "ssri@npm:^10.0.0": - version: 10.0.5 - resolution: "ssri@npm:10.0.5" + version: 10.0.6 + resolution: "ssri@npm:10.0.6" dependencies: minipass: "npm:^7.0.3" - checksum: b091f2ae92474183c7ac5ed3f9811457e1df23df7a7e70c9476eaa9a0c4a0c8fc190fb45acefbf023ca9ee864dd6754237a697dc52a0fb182afe65d8e77443d8 + checksum: 10c0/e5a1e23a4057a86a97971465418f22ea89bd439ac36ade88812dd920e4e61873e8abd6a9b72a03a67ef50faa00a2daf1ab745c5a15b46d03e0544a0296354227 languageName: node linkType: hard @@ -1563,7 +1610,7 @@ __metadata: dependencies: inherits: "npm:~2.0.1" readable-stream: "npm:^2.0.2" - checksum: 485562bd5d962d633ae178449029c6fa2611052e356bdb5668f768544aa4daa94c4f9a97de718f3f30ad98f3cb98a5f396252bb3855aff153c138f79c0e8f6ac + checksum: 10c0/485562bd5d962d633ae178449029c6fa2611052e356bdb5668f768544aa4daa94c4f9a97de718f3f30ad98f3cb98a5f396252bb3855aff153c138f79c0e8f6ac languageName: node linkType: hard @@ -1576,7 +1623,7 @@ __metadata: readable-stream: "npm:^2.3.6" to-arraybuffer: "npm:^1.0.0" xtend: "npm:^4.0.0" - checksum: fbe7d327a29216bbabe88d3819bb8f7a502f11eeacf3212579e5af1f76fa7283f6ffa66134ab7d80928070051f571d1029e85f65ce3369fffd4c4df3669446c4 + checksum: 10c0/fbe7d327a29216bbabe88d3819bb8f7a502f11eeacf3212579e5af1f76fa7283f6ffa66134ab7d80928070051f571d1029e85f65ce3369fffd4c4df3669446c4 languageName: node linkType: hard @@ -1587,7 +1634,7 @@ __metadata: emoji-regex: "npm:^8.0.0" is-fullwidth-code-point: "npm:^3.0.0" strip-ansi: "npm:^6.0.1" - checksum: 1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b languageName: node linkType: hard @@ -1598,7 +1645,7 @@ __metadata: eastasianwidth: "npm:^0.2.0" emoji-regex: "npm:^9.2.2" strip-ansi: "npm:^7.0.1" - checksum: ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca languageName: node linkType: hard @@ -1607,7 +1654,7 @@ __metadata: resolution: "string_decoder@npm:1.3.0" dependencies: safe-buffer: "npm:~5.2.0" - checksum: 810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d + checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d languageName: node linkType: hard @@ -1616,7 +1663,7 @@ __metadata: resolution: "string_decoder@npm:1.1.1" dependencies: safe-buffer: "npm:~5.1.0" - checksum: b4f89f3a92fd101b5653ca3c99550e07bdf9e13b35037e9e2a1c7b47cec4e55e06ff3fc468e314a0b5e80bfbaf65c1ca5a84978764884ae9413bec1fc6ca924e + checksum: 10c0/b4f89f3a92fd101b5653ca3c99550e07bdf9e13b35037e9e2a1c7b47cec4e55e06ff3fc468e314a0b5e80bfbaf65c1ca5a84978764884ae9413bec1fc6ca924e languageName: node linkType: hard @@ -1625,7 +1672,7 @@ __metadata: resolution: "strip-ansi@npm:6.0.1" dependencies: ansi-regex: "npm:^5.0.1" - checksum: 1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 languageName: node linkType: hard @@ -1634,13 +1681,13 @@ __metadata: resolution: "strip-ansi@npm:7.1.0" dependencies: ansi-regex: "npm:^6.0.1" - checksum: a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 languageName: node linkType: hard "tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.2.0 - resolution: "tar@npm:6.2.0" + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: chownr: "npm:^2.0.0" fs-minipass: "npm:^2.0.0" @@ -1648,7 +1695,7 @@ __metadata: minizlib: "npm:^2.1.1" mkdirp: "npm:^1.0.3" yallist: "npm:^4.0.0" - checksum: 02ca064a1a6b4521fef88c07d389ac0936730091f8c02d30ea60d472e0378768e870769ab9e986d87807bfee5654359cf29ff4372746cc65e30cbddc352660d8 + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 languageName: node linkType: hard @@ -1657,21 +1704,21 @@ __metadata: resolution: "timers-browserify@npm:2.0.12" dependencies: setimmediate: "npm:^1.0.4" - checksum: 98e84db1a685bc8827c117a8bc62aac811ad56a995d07938fc7ed8cdc5bf3777bfe2d4e5da868847194e771aac3749a20f6cdd22091300fe889a76fe214a4641 + checksum: 10c0/98e84db1a685bc8827c117a8bc62aac811ad56a995d07938fc7ed8cdc5bf3777bfe2d4e5da868847194e771aac3749a20f6cdd22091300fe889a76fe214a4641 languageName: node linkType: hard "to-arraybuffer@npm:^1.0.0": version: 1.0.1 resolution: "to-arraybuffer@npm:1.0.1" - checksum: 2460bd95524f4845a751e4f8bf9937f9f3dcd1651f104e1512868782f858f8302c1cf25bbc30794bc1b3ff65c4e135158377302f2abaff43a2d8e3c38dfe098c + checksum: 10c0/2460bd95524f4845a751e4f8bf9937f9f3dcd1651f104e1512868782f858f8302c1cf25bbc30794bc1b3ff65c4e135158377302f2abaff43a2d8e3c38dfe098c languageName: node linkType: hard "tty-browserify@npm:0.0.0": version: 0.0.0 resolution: "tty-browserify@npm:0.0.0" - checksum: c0c68206565f1372e924d5cdeeff1a0d9cc729833f1da98c03d78be8f939e5f61a107bd0ab77d1ef6a47d62bb0e48b1081fbea273acf404959e22fd3891439c5 + checksum: 10c0/c0c68206565f1372e924d5cdeeff1a0d9cc729833f1da98c03d78be8f939e5f61a107bd0ab77d1ef6a47d62bb0e48b1081fbea273acf404959e22fd3891439c5 languageName: node linkType: hard @@ -1680,7 +1727,7 @@ __metadata: resolution: "unique-filename@npm:3.0.0" dependencies: unique-slug: "npm:^4.0.0" - checksum: 6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + checksum: 10c0/6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f languageName: node linkType: hard @@ -1689,7 +1736,7 @@ __metadata: resolution: "unique-slug@npm:4.0.0" dependencies: imurmurhash: "npm:^0.1.4" - checksum: cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + checksum: 10c0/cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 languageName: node linkType: hard @@ -1699,14 +1746,14 @@ __metadata: dependencies: punycode: "npm:^1.4.1" qs: "npm:^6.11.2" - checksum: 7546b878ee7927cfc62ca21dbe2dc395cf70e889c3488b2815bf2c63355cb3c7db555128176a01b0af6cccf265667b6fd0b4806de00cb71c143c53986c08c602 + checksum: 10c0/7546b878ee7927cfc62ca21dbe2dc395cf70e889c3488b2815bf2c63355cb3c7db555128176a01b0af6cccf265667b6fd0b4806de00cb71c143c53986c08c602 languageName: node linkType: hard "util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" - checksum: 41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 + checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 languageName: node linkType: hard @@ -1715,7 +1762,7 @@ __metadata: resolution: "util@npm:0.10.4" dependencies: inherits: "npm:2.0.3" - checksum: d29f6893e406b63b088ce9924da03201df89b31490d4d011f1c07a386ea4b3dbe907464c274023c237da470258e1805d806c7e4009a5974cd6b1d474b675852a + checksum: 10c0/d29f6893e406b63b088ce9924da03201df89b31490d4d011f1c07a386ea4b3dbe907464c274023c237da470258e1805d806c7e4009a5974cd6b1d474b675852a languageName: node linkType: hard @@ -1724,14 +1771,14 @@ __metadata: resolution: "util@npm:0.11.1" dependencies: inherits: "npm:2.0.3" - checksum: 8e9d1a85e661c8a8d9883d821aedbff3f8d9c3accd85357020905386ada5653b20389fc3591901e2a0bde64f8dc86b28c3f990114aa5a38eaaf30b455fa3cdf6 + checksum: 10c0/8e9d1a85e661c8a8d9883d821aedbff3f8d9c3accd85357020905386ada5653b20389fc3591901e2a0bde64f8dc86b28c3f990114aa5a38eaaf30b455fa3cdf6 languageName: node linkType: hard "vm-browserify@npm:^1.0.1": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" - checksum: 0cc1af6e0d880deb58bc974921320c187f9e0a94f25570fca6b1bd64e798ce454ab87dfd797551b1b0cc1849307421aae0193cedf5f06bdb5680476780ee344b + checksum: 10c0/0cc1af6e0d880deb58bc974921320c187f9e0a94f25570fca6b1bd64e798ce454ab87dfd797551b1b0cc1849307421aae0193cedf5f06bdb5680476780ee344b languageName: node linkType: hard @@ -1742,7 +1789,7 @@ __metadata: isexe: "npm:^2.0.0" bin: which: ./bin/which - checksum: e945a8b6bbf6821aaaef7f6e0c309d4b615ef35699576d5489b4261da9539f70393c6b2ce700ee4321c18f914ebe5644bc4631b15466ffbaad37d83151f6af59 + checksum: 10c0/e945a8b6bbf6821aaaef7f6e0c309d4b615ef35699576d5489b4261da9539f70393c6b2ce700ee4321c18f914ebe5644bc4631b15466ffbaad37d83151f6af59 languageName: node linkType: hard @@ -1753,7 +1800,7 @@ __metadata: isexe: "npm:^2.0.0" bin: node-which: ./bin/node-which - checksum: 66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f languageName: node linkType: hard @@ -1764,7 +1811,7 @@ __metadata: isexe: "npm:^3.1.1" bin: node-which: bin/which.js - checksum: 449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + checksum: 10c0/449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a languageName: node linkType: hard @@ -1775,7 +1822,7 @@ __metadata: ansi-styles: "npm:^4.0.0" string-width: "npm:^4.1.0" strip-ansi: "npm:^6.0.0" - checksum: d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da languageName: node linkType: hard @@ -1786,7 +1833,7 @@ __metadata: ansi-styles: "npm:^6.1.0" string-width: "npm:^5.0.1" strip-ansi: "npm:^7.0.1" - checksum: 138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 + checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 languageName: node linkType: hard @@ -1801,13 +1848,13 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: aec4ef4eb65821a7dde7b44790f8699cfafb7978c9b080f6d7a98a7f8fc0ce674c027073a78574c94786ba7112cc90fa2cc94fc224ceba4d4b1030cff9662494 + checksum: 10c0/aec4ef4eb65821a7dde7b44790f8699cfafb7978c9b080f6d7a98a7f8fc0ce674c027073a78574c94786ba7112cc90fa2cc94fc224ceba4d4b1030cff9662494 languageName: node linkType: hard -"ws@npm:^8.13.0": - version: 8.14.2 - resolution: "ws@npm:8.14.2" +"ws@npm:^8.17.0": + version: 8.17.0 + resolution: "ws@npm:8.17.0" peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ">=5.0.2" @@ -1816,20 +1863,20 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: 35b4c2da048b8015c797fd14bcb5a5766216ce65c8a5965616a5440ca7b6c3681ee3cbd0ea0c184a59975556e9d58f2002abf8485a14d11d3371770811050a16 + checksum: 10c0/55241ec93a66fdfc4bf4f8bc66c8eb038fda2c7a4ee8f6f157f2ca7dc7aa76aea0c0da0bf3adb2af390074a70a0e45456a2eaf80e581e630b75df10a64b0a990 languageName: node linkType: hard "xtend@npm:^4.0.0": version: 4.0.2 resolution: "xtend@npm:4.0.2" - checksum: 366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e + checksum: 10c0/366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e languageName: node linkType: hard "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" - checksum: 2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a languageName: node linkType: hard diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 5e1aaffcb84c5c1d131a4112b68ef94584634b19..7182905a64ec0ea89b54c63553808a537e7d79c1 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -1,12 +1,12 @@ -FROM ubuntu:22.04 +FROM debian:bookworm LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive ENV NODE_VERSION=v20.11.1 \ CLOJURE_VERSION=1.11.1.1435 \ - CLJKONDO_VERSION=2024.02.12 \ - BABASHKA_VERSION=1.3.188 \ + CLJKONDO_VERSION=2024.03.13 \ + BABASHKA_VERSION=1.3.189 \ CLJFMT_VERSION=0.12.0 \ LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 @@ -33,7 +33,6 @@ RUN set -ex; \ unzip \ rsync \ fakeroot \ - netcat \ file \ less \ jq \ @@ -105,12 +104,12 @@ RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ aarch64|arm64) \ - ESUM='3ce6a2b357e2ef45fd6b53d6587aa05bfec7771e7fb982f2c964f6b771b7526a'; \ - BINARY_URL='https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jdk_aarch64_linux_hotspot_21.0.2_13.tar.gz'; \ + ESUM='7d3ab0e8eba95bd682cfda8041c6cb6fa21e09d0d9131316fd7c96c78969de31'; \ + BINARY_URL='https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.3%2B9/OpenJDK21U-jdk_aarch64_linux_hotspot_21.0.3_9.tar.gz'; \ ;; \ amd64|x86_64) \ - ESUM='454bebb2c9fe48d981341461ffb6bf1017c7b7c6e15c6b0c29b959194ba3aaa5'; \ - BINARY_URL='https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jdk_x64_linux_hotspot_21.0.2_13.tar.gz'; \ + ESUM='fffa52c22d797b715a962e6c8d11ec7d79b90dd819b5bc51d62137ea4b22a340'; \ + BINARY_URL='https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.3%2B9/OpenJDK21U-jdk_x64_linux_hotspot_21.0.3_9.tar.gz'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ @@ -133,33 +132,36 @@ RUN set -ex; \ rm -rf /tmp/clojure.sh; RUN set -ex; \ - curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -; \ - echo "deb http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" >> /etc/apt/sources.list.d/postgresql.list; \ + install -d /usr/share/postgresql-common/pgdg; \ + curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc; \ + echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" >> /etc/apt/sources.list.d/postgresql.list; \ apt-get -qq update; \ - apt-get -qqy install postgresql-client-15; \ + apt-get -qqy install postgresql-client-16; \ rm -rf /var/lib/apt/lists/*; RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ aarch64|arm64) \ - BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.xz"; \ + BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \ ;; \ amd64|x86_64) \ - BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.xz"; \ + BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ exit 1; \ ;; \ esac; \ - curl -LfsSo /tmp/nodejs.tar.xz ${BINARY_URL}; \ + curl -LfsSo /tmp/nodejs.tar.gz ${BINARY_URL}; \ mkdir -p /usr/local/nodejs; \ cd /usr/local/nodejs; \ - tar -xf /tmp/nodejs.tar.xz --strip-components=1; \ + tar -xf /tmp/nodejs.tar.gz --strip-components=1; \ chown -R root /usr/local/nodejs; \ corepack enable; \ - rm -rf /tmp/nodejs.tar.xz; + corepack install -g yarn@4.2.2; \ + npx playwright install --with-deps chromium; \ + rm -rf /tmp/nodejs.tar.gz; RUN set -ex; \ ARCH="$(dpkg --print-architecture)"; \ @@ -242,13 +244,8 @@ RUN set -ex; \ WORKDIR /home -EXPOSE 3447 -EXPOSE 3448 -EXPOSE 3449 -EXPOSE 6060 -EXPOSE 9090 - COPY files/nginx.conf /etc/nginx/nginx.conf +COPY files/nginx-mime.types /etc/nginx/mime.types COPY files/phantomjs-mock /usr/bin/phantomjs COPY files/bashrc /root/.bashrc diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index bd1a3167b37f7e43bb475ab3dd1c6f6e19e8b79e..f4e9b0e792ac3849747c6d086c37334962fca376 100644 --- a/docker/devenv/docker-compose.yaml +++ b/docker/devenv/docker-compose.yaml @@ -8,7 +8,7 @@ networks: - subnet: 172.177.9.0/24 volumes: - postgres_data_pg15: + postgres_data_pg16: user_data: minio_data: redis_data: @@ -86,7 +86,7 @@ services: - 9001:9001 postgres: - image: postgres:15 + image: postgres:16 command: postgres -c config_file=/etc/postgresql.conf restart: always stop_signal: SIGINT @@ -98,7 +98,7 @@ services: volumes: - ./files/postgresql.conf:/etc/postgresql.conf:z - ./files/postgresql_init.sql:/docker-entrypoint-initdb.d/init.sql:z - - postgres_data_pg15:/var/lib/postgresql/data + - postgres_data_pg16:/var/lib/postgresql/data redis: image: redis:7 @@ -125,3 +125,7 @@ services: ports: - "10389:10389" - "10636:10636" + ulimits: + nofile: + soft: "1024" + hard: "1024" \ No newline at end of file diff --git a/docker/devenv/files/bashrc b/docker/devenv/files/bashrc index bb53eb472c7a7698c67a098d3a66f8673e9d9ddc..745e3f901ada28abab4791cee3f1bb2112a01d27 100644 --- a/docker/devenv/files/bashrc +++ b/docker/devenv/files/bashrc @@ -1,7 +1,7 @@ #!/usr/bin/env bash export PATH=/usr/lib/jvm/openjdk/bin:/usr/local/nodejs/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin -export JAVA_OPTS="-Xmx900m -Xms50m" +export JAVA_OPTS="-Xmx1000m -Xms50m" alias l='ls --color -GFlh' alias rm='rm -r' diff --git a/docker/devenv/files/nginx-mime.types b/docker/devenv/files/nginx-mime.types new file mode 100644 index 0000000000000000000000000000000000000000..91f6aba0ac41741eebd71019b5a072b971c41420 --- /dev/null +++ b/docker/devenv/files/nginx-mime.types @@ -0,0 +1,100 @@ + +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/javascript mjs; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/avif avif; + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/wasm wasm; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 24a0f2ddf01f2413b8dded3e02c62cd318fa2b9c..961b3a4fb47a56e491947295c90ee58de4029202 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -14,11 +14,20 @@ http { tcp_nodelay on; keepalive_timeout 65; types_hash_max_size 2048; - # server_tokens off; + server_tokens off; # server_names_hash_bucket_size 64; # server_name_in_redirect off; + reset_timedout_connection on; + client_body_timeout 20s; + client_header_timeout 20s; + + proxy_connect_timeout 300s; + proxy_send_timeout 300s; + proxy_read_timeout 300s; + send_timeout 300s; + include /etc/nginx/mime.types; default_type application/octet-stream; @@ -33,7 +42,7 @@ http { gzip_buffers 16 8k; gzip_http_version 1.1; - gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json; + gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml; map $http_upgrade $connection_upgrade { default upgrade; @@ -59,7 +68,10 @@ http { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - resolver 127.0.0.11; + proxy_buffer_size 16k; + proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k + proxy_buffers 32 4k; + resolver 127.0.0.11 ipv6=off; etag off; @@ -137,6 +149,11 @@ http { proxy_pass http://127.0.0.1:6060/ws/notifications; } + location /storybook { + alias /home/penpot/penpot/frontend/storybook-static/; + autoindex on; + } + location / { location ~ ^/github/penpot-files/(?[a-zA-Z0-9\-\_\.]+) { proxy_pass https://raw.githubusercontent.com/penpot/penpot-files/main/$template_file; @@ -199,13 +216,13 @@ http { add_header Cache-Control "no-store, no-cache, max-age=0" always; } - location ~* \.(js|css|jpg|svg|png)$ { + location ~* \.(js|css|jpg|svg|png|mjs|map)$ { # We set no cache only on devenv add_header Cache-Control "no-store, no-cache, max-age=0" always; # add_header Cache-Control "max-age=604800" always; # 7 days } - location ~ ^/(/|css|fonts|images|js|wasm) { + location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) { } location ~ ^/[^/]+/(.*)$ { @@ -215,7 +232,7 @@ http { add_header Last-Modified $date_gmt; add_header Cache-Control "no-store, no-cache, max-age=0" always; if_modified_since off; - try_files $uri /index.html$is_args$args =404; + try_files $uri /index.html$is_args$args /index.html =404; } } } diff --git a/docker/devenv/files/start-tmux.sh b/docker/devenv/files/start-tmux.sh index cb3048ddc5162493300bdff3407f1b5223ba7406..ec6470ea21537eace71b166ae319a41719d7c1e4 100755 --- a/docker/devenv/files/start-tmux.sh +++ b/docker/devenv/files/start-tmux.sh @@ -6,25 +6,24 @@ cd ~; source ~/.bashrc -set -e; - echo "[start-tmux.sh] Installing node dependencies" pushd ~/penpot/frontend/ yarn install +yarn run playwright install --with-deps chromium popd pushd ~/penpot/exporter/ yarn install -npx playwright install chromium +yarn run playwright install --with-deps chromium popd tmux -2 new-session -d -s penpot -tmux rename-window -t penpot:0 'gulp' +tmux rename-window -t penpot:0 'frontend watch' tmux select-window -t penpot:0 tmux send-keys -t penpot 'cd penpot/frontend' enter C-l -tmux send-keys -t penpot 'npx gulp watch' enter +tmux send-keys -t penpot 'yarn run watch' enter -tmux new-window -t penpot:1 -n 'shadow watch' +tmux new-window -t penpot:1 -n 'frontend shadow' tmux select-window -t penpot:1 tmux send-keys -t penpot 'cd penpot/frontend' enter C-l tmux send-keys -t penpot 'clojure -M:dev:shadow-cljs watch main' enter diff --git a/docker/gitpod/Dockerfile b/docker/gitpod/Dockerfile index cc4f7f9be0c471c90cca49e600b08d6a6c905b91..30ec7da8fe566e6fd28be552a6d7c5689f0280a2 100644 --- a/docker/gitpod/Dockerfile +++ b/docker/gitpod/Dockerfile @@ -17,6 +17,7 @@ RUN set -ex; \ sudo chown gitpod:gitpod /var/log/nginx COPY files/nginx.conf /etc/nginx/nginx.conf +COPY files/nginx-mime.types /etc/nginx/mime.types USER root diff --git a/docker/gitpod/files/nginx-mime.types b/docker/gitpod/files/nginx-mime.types new file mode 100644 index 0000000000000000000000000000000000000000..91f6aba0ac41741eebd71019b5a072b971c41420 --- /dev/null +++ b/docker/gitpod/files/nginx-mime.types @@ -0,0 +1,100 @@ + +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/javascript mjs; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/avif avif; + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/wasm wasm; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/docker/images/Dockerfile.backend b/docker/images/Dockerfile.backend index a878be57ac274f4062b0128eb3d2a47e9f8278b6..db789dfd258a15c5d36db9635cb74b4621fc753b 100644 --- a/docker/images/Dockerfile.backend +++ b/docker/images/Dockerfile.backend @@ -40,16 +40,12 @@ RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ aarch64|arm64) \ - ESUM='1c4be9aa173cb0deb0d215643d9509c8900e5497290b29eee4bee335fa57984f'; \ - BINARY_URL='https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jdk_aarch64_linux_hotspot_19.0.2_7.tar.gz'; \ - ;; \ - armhf|armv7l) \ - ESUM='6a51cb3868b5a3b81848a0d276267230ff3f8639f20ba9ae9ef1d386440bf1fd'; \ - BINARY_URL='https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jdk_arm_linux_hotspot_19.0.2_7.tar.gz'; \ + ESUM='3ce6a2b357e2ef45fd6b53d6587aa05bfec7771e7fb982f2c964f6b771b7526a'; \ + BINARY_URL='https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jdk_aarch64_linux_hotspot_21.0.2_13.tar.gz'; \ ;; \ amd64|x86_64) \ - ESUM='3a3ba7a3f8c3a5999e2c91ea1dca843435a0d1c43737bd2f6822b2f02fc52165'; \ - BINARY_URL='https://github.com/adoptium/temurin19-binaries/releases/download/jdk-19.0.2%2B7/OpenJDK19U-jdk_x64_linux_hotspot_19.0.2_7.tar.gz'; \ + ESUM='454bebb2c9fe48d981341461ffb6bf1017c7b7c6e15c6b0c29b959194ba3aaa5'; \ + BINARY_URL='https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jdk_x64_linux_hotspot_21.0.2_13.tar.gz'; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ @@ -63,7 +59,6 @@ RUN set -eux; \ tar -xf /tmp/openjdk.tar.gz --strip-components=1; \ rm -rf /tmp/openjdk.tar.gz; - COPY --chown=penpot:penpot ./bundle-backend/ /opt/penpot/backend/ USER penpot:penpot diff --git a/docker/images/Dockerfile.exporter b/docker/images/Dockerfile.exporter index 9ddd9ecc788d030f34c48a57aec251b51771b091..e4fceec85fa50616e230bb28360bdf0df1f24a6a 100644 --- a/docker/images/Dockerfile.exporter +++ b/docker/images/Dockerfile.exporter @@ -3,7 +3,7 @@ LABEL maintainer="Andrey Antukh " ENV LANG=en_US.UTF-8 \ LC_ALL=en_US.UTF-8 \ - NODE_VERSION=v18.15.0 \ + NODE_VERSION=v20.11.1 \ DEBIAN_FRONTEND=noninteractive \ PATH=/opt/node/bin:$PATH @@ -75,26 +75,23 @@ RUN set -eux; \ ARCH="$(dpkg --print-architecture)"; \ case "${ARCH}" in \ aarch64|arm64) \ - BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.xz"; \ - ;; \ - armhf|armv7l) \ - BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-armv7l.tar.xz"; \ + BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-arm64.tar.gz"; \ ;; \ amd64|x86_64) \ - BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.xz"; \ + BINARY_URL="https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.gz"; \ ;; \ *) \ echo "Unsupported arch: ${ARCH}"; \ exit 1; \ ;; \ esac; \ - curl -LfsSo /tmp/nodejs.tar.xz ${BINARY_URL}; \ + curl -LfsSo /tmp/nodejs.tar.gz ${BINARY_URL}; \ mkdir -p /opt/node; \ cd /opt/node; \ - tar -xf /tmp/nodejs.tar.xz --strip-components=1; \ + tar -xf /tmp/nodejs.tar.gz --strip-components=1; \ chown -R root /opt/node; \ - npm install -g yarn; \ - rm -rf /tmp/nodejs.tar.xz; \ + corepack enable; \ + rm -rf /tmp/nodejs.tar.gz; \ mkdir -p /opt/penpot; \ chown -R penpot:penpot /opt/penpot; @@ -104,7 +101,7 @@ WORKDIR /opt/penpot/exporter USER penpot:penpot RUN set -ex; \ - yarn --network-timeout 1000000; \ - yarn --network-timeout 1000000 run playwright install chromium; + yarn install; \ + yarn run playwright install chromium; CMD ["node", "app.js"] diff --git a/docker/images/Dockerfile.frontend b/docker/images/Dockerfile.frontend index af5101b299ce40b684c4824d86bb3e4384c36c73..0edc1b2d9d8354d06393db066c16c2fb64d28d78 100644 --- a/docker/images/Dockerfile.frontend +++ b/docker/images/Dockerfile.frontend @@ -9,6 +9,7 @@ RUN set -ex; \ ADD ./bundle-frontend/ /var/www/app/ ADD ./files/config.js /var/www/app/js/config.js ADD ./files/nginx.conf /etc/nginx/nginx.conf.template +ADD ./files/nginx-mime.types /etc/nginx/mime.types ADD ./files/nginx-entrypoint.sh /entrypoint.sh ENTRYPOINT ["/bin/bash", "/entrypoint.sh"] diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index 7e85d178c0e88ce14b59f9f752d05c86eb256fe7..d16402ce54ae78206e7a95722c0e10d19a879920 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -1,6 +1,3 @@ ---- -version: "3.5" - networks: penpot: @@ -12,7 +9,7 @@ volumes: services: ## Traefik service declaration example. Consider using it if you are going to expose - ## penpot to the internet or different host than `localhost`. + ## penpot to the internet, or a different host than `localhost`. # traefik: # image: traefik:v2.9 @@ -53,15 +50,15 @@ services: labels: - "traefik.enable=true" - ## HTTP: example of labels for the case if you are going to expose penpot to the - ## internet using only HTTP (without HTTPS) with traefik + ## HTTP: example of labels for the case where penpot will be exposed to the + ## internet with only HTTP (without HTTPS) using traefik. # - "traefik.http.routers.penpot-http.entrypoints=web" # - "traefik.http.routers.penpot-http.rule=Host(``)" # - "traefik.http.services.penpot-http.loadbalancer.server.port=80" - ## HTTPS: example of labels for the case if you are going to expose penpot to the - ## internet using with HTTPS using traefik + ## HTTPS: example of labels for the case where penpot will be exposed to the + ## internet with HTTPS using traefik. # - "traefik.http.middlewares.http-redirect.redirectscheme.scheme=https" # - "traefik.http.middlewares.http-redirect.redirectscheme.permanent=true" @@ -74,9 +71,9 @@ services: # - "traefik.http.routers.penpot-https.tls=true" # - "traefik.http.routers.penpot-https.tls.certresolver=letsencrypt" - ## Configuration envronment variables for frontend the container. In this case this + ## Configuration envronment variables for the frontend container. In this case, the ## container only needs the `PENPOT_FLAGS`. This environment variable is shared with - ## other services but not all flags are relevant to all services. + ## other services, but not all flags are relevant to all services. environment: ## Relevant flags for frontend: @@ -109,7 +106,7 @@ services: networks: - penpot - ## Configuration envronment variables for backend the + ## Configuration envronment variables for the backend ## container. environment: @@ -142,24 +139,24 @@ services: ## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems ## (eg http sessions, or invitations) are derived. ## - ## If you leve it commented, all created sessions and invitations will + ## If you leave it commented, all created sessions and invitations will ## become invalid on container restart. ## - ## If you going to uncomment this, we recommend use here a trully randomly generated - ## 512 bits base64 encoded string. You can generate one with: + ## If you going to uncomment this, we recommend to use a trully randomly generated + ## 512 bits base64 encoded string here. You can generate one with: ## ## python3 -c "import secrets; print(secrets.token_urlsafe(64))" # - PENPOT_SECRET_KEY=my-insecure-key ## The PREPL host. Mainly used for external programatic access to penpot backend - ## (example: admin). By default it listen on `localhost` but if you are going to use + ## (example: admin). By default it will listen on `localhost` but if you are going to use ## the `admin`, you will need to uncomment this and set the host to `0.0.0.0`. # - PENPOT_PREPL_HOST=0.0.0.0 ## Public URI. If you are going to expose this instance to the internet and use it - ## under different domain than 'localhost', you will need to adjust it to the final + ## under a different domain than 'localhost', you will need to adjust it to the final ## domain. ## ## Consider using traefik and set the 'disable-secure-session-cookies' if you are @@ -195,16 +192,16 @@ services: # - PENPOT_STORAGE_ASSETS_S3_BUCKET= ## Telemetry. When enabled, a periodical process will send anonymous data about this - ## instance. Telemetry data will enable us to learn on how the application is used, + ## instance. Telemetry data will enable us to learn how the application is used, ## based on real scenarios. If you want to help us, please leave it enabled. You can - ## audit what data we send with the code available on github + ## audit what data we send with the code available on github. - PENPOT_TELEMETRY_ENABLED=true ## Example SMTP/Email configuration. By default, emails are sent to the mailcatch - ## service, but for production usage is recommended to setup a real SMTP + ## service, but for production usage it is recommended to setup a real SMTP ## provider. Emails are used to confirm user registrations & invitations. Look below - ## how mailcatch service is configured. + ## how the mailcatch service is configured. - PENPOT_SMTP_DEFAULT_FROM=no-reply@example.com - PENPOT_SMTP_DEFAULT_REPLY_TO=no-reply@example.com @@ -222,7 +219,7 @@ services: - penpot environment: - # Don't touch it; this uses internal docker network to + # Don't touch it; this uses an internal docker network to # communicate with the frontend. - PENPOT_PUBLIC_URI=http://penpot-frontend @@ -254,7 +251,7 @@ services: ## A mailcatch service, used as temporal SMTP server. You can access via HTTP to the ## port 1080 for read all emails the penpot platform has sent. Should be only used as a - ## temporal solution meanwhile you don't have a real SMTP provider configured. + ## temporal solution while no real SMTP provider is configured. penpot-mailcatch: image: sj26/mailcatcher:latest diff --git a/docker/images/files/nginx-mime.types b/docker/images/files/nginx-mime.types new file mode 100644 index 0000000000000000000000000000000000000000..91f6aba0ac41741eebd71019b5a072b971c41420 --- /dev/null +++ b/docker/images/files/nginx-mime.types @@ -0,0 +1,100 @@ + +types { + text/html html htm shtml; + text/css css; + text/xml xml; + image/gif gif; + image/jpeg jpeg jpg; + application/javascript js; + application/javascript mjs; + application/atom+xml atom; + application/rss+xml rss; + + text/mathml mml; + text/plain txt; + text/vnd.sun.j2me.app-descriptor jad; + text/vnd.wap.wml wml; + text/x-component htc; + + image/avif avif; + image/png png; + image/svg+xml svg svgz; + image/tiff tif tiff; + image/vnd.wap.wbmp wbmp; + image/webp webp; + image/x-icon ico; + image/x-jng jng; + image/x-ms-bmp bmp; + + font/woff woff; + font/woff2 woff2; + + application/java-archive jar war ear; + application/json json; + application/mac-binhex40 hqx; + application/msword doc; + application/pdf pdf; + application/postscript ps eps ai; + application/rtf rtf; + application/vnd.apple.mpegurl m3u8; + application/vnd.google-earth.kml+xml kml; + application/vnd.google-earth.kmz kmz; + application/vnd.ms-excel xls; + application/vnd.ms-fontobject eot; + application/vnd.ms-powerpoint ppt; + application/vnd.oasis.opendocument.graphics odg; + application/vnd.oasis.opendocument.presentation odp; + application/vnd.oasis.opendocument.spreadsheet ods; + application/vnd.oasis.opendocument.text odt; + application/vnd.openxmlformats-officedocument.presentationml.presentation + pptx; + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + xlsx; + application/vnd.openxmlformats-officedocument.wordprocessingml.document + docx; + application/vnd.wap.wmlc wmlc; + application/wasm wasm; + application/x-7z-compressed 7z; + application/x-cocoa cco; + application/x-java-archive-diff jardiff; + application/x-java-jnlp-file jnlp; + application/x-makeself run; + application/x-perl pl pm; + application/x-pilot prc pdb; + application/x-rar-compressed rar; + application/x-redhat-package-manager rpm; + application/x-sea sea; + application/x-shockwave-flash swf; + application/x-stuffit sit; + application/x-tcl tcl tk; + application/x-x509-ca-cert der pem crt; + application/x-xpinstall xpi; + application/xhtml+xml xhtml; + application/xspf+xml xspf; + application/zip zip; + + application/octet-stream bin exe dll; + application/octet-stream deb; + application/octet-stream dmg; + application/octet-stream iso img; + application/octet-stream msi msp msm; + + audio/midi mid midi kar; + audio/mpeg mp3; + audio/ogg ogg; + audio/x-m4a m4a; + audio/x-realaudio ra; + + video/3gpp 3gpp 3gp; + video/mp2t ts; + video/mp4 mp4; + video/mpeg mpeg mpg; + video/quicktime mov; + video/webm webm; + video/x-flv flv; + video/x-m4v m4v; + video/x-mng mng; + video/x-ms-asf asx asf; + video/x-ms-wmv wmv; + video/x-msvideo avi; +} diff --git a/docker/images/files/nginx.conf b/docker/images/files/nginx.conf index bdf52f3894795a0a97432f9891ea0d2492059fc1..fee0e3fca6dcf384572cb314d323af63038e21ad 100644 --- a/docker/images/files/nginx.conf +++ b/docker/images/files/nginx.conf @@ -38,7 +38,10 @@ http { gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json; - resolver $PENPOT_INTERNAL_RESOLVER; + proxy_buffer_size 16k; + proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k + proxy_buffers 32 4k; + resolver $PENPOT_INTERNAL_RESOLVER ipv6=off; map $http_upgrade $connection_upgrade { default upgrade; @@ -188,11 +191,11 @@ http { add_header Cache-Control "no-store, no-cache, max-age=0" always; } - location ~* \.(js|css|jpg|svg|png)$ { + location ~* \.(js|css|jpg|svg|png|mjs|map)$ { add_header Cache-Control "max-age=604800" always; # 7 days } - location ~ ^/(/|css|fonts|images|js|wasm) { + location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) { } location ~ ^/[^/]+/(.*)$ { @@ -202,7 +205,7 @@ http { add_header Last-Modified $date_gmt; add_header Cache-Control "no-store, no-cache, max-age=0" always; if_modified_since off; - try_files $uri /index.html$is_args$args =404; + try_files $uri /index.html$is_args$args /index.html =404; } } } diff --git a/exporter/deps.edn b/exporter/deps.edn index 01c4cefcc4aa2223073a24b589d8efc9ac979011..0bcd1ec3b73789748bdde9afafefff7dd319d80f 100644 --- a/exporter/deps.edn +++ b/exporter/deps.edn @@ -1,10 +1,9 @@ {:paths ["src" "vendor" "resources" "test"] :deps {penpot/common {:local/root "../common"} - org.clojure/clojure {:mvn/version "1.11.1"} + org.clojure/clojure {:mvn/version "1.11.3"} binaryage/devtools {:mvn/version "RELEASE"} - metosin/reitit-core {:mvn/version "0.6.0"} - funcool/beicon {:mvn/version "2021.07.05-1"} + metosin/reitit-core {:mvn/version "0.7.0"} } :aliases {:outdated @@ -15,7 +14,7 @@ :dev {:extra-deps - {thheller/shadow-cljs {:mvn/version "2.27.4"}}} + {thheller/shadow-cljs {:mvn/version "2.28.11"}}} :shadow-cljs {:main-opts ["-m" "shadow.cljs.devtools.cli"]} diff --git a/exporter/package.json b/exporter/package.json index 86cb78c8dad0b92e083ccfc09045d46eb9f9efd5..49da8e876e6c2675c5d36c4f645f9423bf4df455 100644 --- a/exporter/package.json +++ b/exporter/package.json @@ -4,25 +4,25 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.0.2", + "packageManager": "yarn@4.3.1", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" }, "dependencies": { - "archiver": "^6.0.1", + "archiver": "^7.0.1", "cookies": "^0.9.1", "generic-pool": "^3.9.0", "inflation": "^2.1.0", - "ioredis": "^5.3.2", + "ioredis": "^5.4.1", "luxon": "^3.4.4", - "playwright": "^1.40.1", + "playwright": "^1.44.1", "raw-body": "^2.5.2", "xml-js": "^1.6.11", "xregexp": "^5.1.1" }, "devDependencies": { - "shadow-cljs": "2.27.4", + "shadow-cljs": "2.28.11", "source-map-support": "^0.5.21" }, "scripts": { diff --git a/exporter/scripts/build b/exporter/scripts/build index 2fd75a3099ea3b7eb8a3c37823bad0a69d9748be..0044605847588828b4181076fef031e3ad33cdce 100755 --- a/exporter/scripts/build +++ b/exporter/scripts/build @@ -16,6 +16,7 @@ clojure -J-Xms100M -J-Xmx1000M -J-XX:+UseSerialGC -M:dev:shadow-cljs release mai rm -rf target/app; # Copy package*.json files +cp ../.yarnrc.yml target/; cp yarn.lock target/; cp package.json target/; diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index 4c008807767552c508885d7650154af81c724a0d..6ca84f584c4fb83cdc06fa1532f7056f65c8a0a9 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -26,16 +26,24 @@ (def ^:private schema:config - (sm/define - [:map {:title "config"} - [:public-uri {:optional true} ::sm/uri] - [:host {:optional true} :string] - [:tenant {:optional true} :string] - [:flags {:optional true} ::sm/set-of-keywords] - [:redis-uri {:optional true} :string] - [:tempdir {:optional true} :string] - [:browser-pool-max {:optional true} :int] - [:browser-pool-min {:optional true} :int]])) + [:map {:title "config"} + [:public-uri {:optional true} ::sm/uri] + [:host {:optional true} :string] + [:tenant {:optional true} :string] + [:flags {:optional true} [::sm/set :keyword]] + [:redis-uri {:optional true} :string] + [:tempdir {:optional true} :string] + [:browser-pool-max {:optional true} ::sm/int] + [:browser-pool-min {:optional true} ::sm/int]]) + +(def ^:private decode-config + (sm/decoder schema:config sm/string-transformer)) + +(def ^:private explain-config + (sm/explainer schema:config)) + +(def ^:private valid-config? + (sm/validator schema:config)) (defn- parse-flags [config] @@ -60,15 +68,15 @@ [] (let [env (read-env "penpot") env (d/without-nils env) - data (merge defaults env)] + data (merge defaults env) + data (decode-config data)] + + (when-not (valid-config? data) + (let [explain (explain-config data)] + (println (sm/humanize-explain explain)) + (process/exit -1))) - (try - (sm/conform! schema:config data) - (catch :default cause - (if-let [explain (some->> cause ex-data ::sm/explain)] - (println (sm/humanize-explain explain)) - (js/console.error cause)) - (process/exit -1))))) + data)) (def config (prepare-config)) diff --git a/exporter/yarn.lock b/exporter/yarn.lock index d5c7dfe795c12c573b2e8d026f34db0b2f10a313..cd19abac0ba9ebb3900ce022344c3fc4ec40f2c9 100644 --- a/exporter/yarn.lock +++ b/exporter/yarn.lock @@ -6,19 +6,19 @@ __metadata: cacheKey: 10c0 "@babel/runtime-corejs3@npm:^7.16.5": - version: 7.23.5 - resolution: "@babel/runtime-corejs3@npm:7.23.5" + version: 7.24.5 + resolution: "@babel/runtime-corejs3@npm:7.24.5" dependencies: core-js-pure: "npm:^3.30.2" regenerator-runtime: "npm:^0.14.0" - checksum: 9bbad4ae7efea21e2c92ddee70b42ce9773a56e044cfc16267f9610b38ee531c87b465d84d39433fca93f7f567b47d5e40383e3d2cfe85dbeceea7fba8a52cc8 + checksum: 10c0/61a16b8031d312752676527746d6a70c759d578cadfdee8c7bea976e848793aa08361ba14602a9ebb3ed60af225bbdff316500b4961df91cb246f2055764f208 languageName: node linkType: hard "@ioredis/commands@npm:^1.1.1": version: 1.2.0 resolution: "@ioredis/commands@npm:1.2.0" - checksum: a5d3c29dd84d8a28b7c67a441ac1715cbd7337a7b88649c0f17c345d89aa218578d2b360760017c48149ef8a70f44b051af9ac0921a0622c2b479614c4f65b36 + checksum: 10c0/a5d3c29dd84d8a28b7c67a441ac1715cbd7337a7b88649c0f17c345d89aa218578d2b360760017c48149ef8a70f44b051af9ac0921a0622c2b479614c4f65b36 languageName: node linkType: hard @@ -32,52 +32,61 @@ __metadata: strip-ansi-cjs: "npm:strip-ansi@^6.0.1" wrap-ansi: "npm:^8.1.0" wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" - checksum: b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + checksum: 10c0/b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e languageName: node linkType: hard "@npmcli/agent@npm:^2.0.0": - version: 2.2.0 - resolution: "@npmcli/agent@npm:2.2.0" + version: 2.2.2 + resolution: "@npmcli/agent@npm:2.2.2" dependencies: agent-base: "npm:^7.1.0" http-proxy-agent: "npm:^7.0.0" https-proxy-agent: "npm:^7.0.1" lru-cache: "npm:^10.0.1" - socks-proxy-agent: "npm:^8.0.1" - checksum: 7b89590598476dda88e79c473766b67c682aae6e0ab0213491daa6083dcc0c171f86b3868f5506f22c09aa5ea69ad7dfb78f4bf39a8dca375d89a42f408645b3 + socks-proxy-agent: "npm:^8.0.3" + checksum: 10c0/325e0db7b287d4154ecd164c0815c08007abfb07653cc57bceded17bb7fd240998a3cbdbe87d700e30bef494885eccc725ab73b668020811d56623d145b524ae languageName: node linkType: hard "@npmcli/fs@npm:^3.1.0": - version: 3.1.0 - resolution: "@npmcli/fs@npm:3.1.0" + version: 3.1.1 + resolution: "@npmcli/fs@npm:3.1.1" dependencies: semver: "npm:^7.3.5" - checksum: 162b4a0b8705cd6f5c2470b851d1dc6cd228c86d2170e1769d738c1fbb69a87160901411c3c035331e9e99db72f1f1099a8b734bf1637cc32b9a5be1660e4e1e + checksum: 10c0/c37a5b4842bfdece3d14dfdb054f73fe15ed2d3da61b34ff76629fb5b1731647c49166fd2a8bf8b56fcfa51200382385ea8909a3cbecdad612310c114d3f6c99 languageName: node linkType: hard "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" - checksum: 5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + checksum: 10c0/5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd languageName: node linkType: hard "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" - checksum: f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 + checksum: 10c0/f742a5a107473946f426c691c08daba61a1d15942616f300b5d32fd735be88fef5cba24201757b6c407fd564555fb48c751cfa33519b2605c8a7aadd22baf372 languageName: node linkType: hard -"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": - version: 7.1.0 - resolution: "agent-base@npm:7.1.0" +"abort-controller@npm:^3.0.0": + version: 3.0.0 + resolution: "abort-controller@npm:3.0.0" + dependencies: + event-target-shim: "npm:^5.0.0" + checksum: 10c0/90ccc50f010250152509a344eb2e71977fbf8db0ab8f1061197e3275ddf6c61a41a6edfd7b9409c664513131dd96e962065415325ef23efa5db931b382d24ca5 + languageName: node + linkType: hard + +"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": + version: 7.1.1 + resolution: "agent-base@npm:7.1.1" dependencies: debug: "npm:^4.3.4" - checksum: fc974ab57ffdd8421a2bc339644d312a9cca320c20c3393c9d8b1fd91731b9bbabdb985df5fc860f5b79d81c3e350daa3fcb31c5c07c0bb385aafc817df004ce + checksum: 10c0/e59ce7bed9c63bf071a30cc471f2933862044c97fd9958967bfe22521d7a0f601ce4ed5a8c011799d0c726ca70312142ae193bbebb60f576b52be19d4a363b50 languageName: node linkType: hard @@ -87,21 +96,21 @@ __metadata: dependencies: clean-stack: "npm:^2.0.0" indent-string: "npm:^4.0.0" - checksum: a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 + checksum: 10c0/a42f67faa79e3e6687a4923050e7c9807db3848a037076f791d10e092677d65c1d2d863b7848560699f40fc0502c19f40963fb1cd1fb3d338a7423df8e45e039 languageName: node linkType: hard "ansi-regex@npm:^5.0.1": version: 5.0.1 resolution: "ansi-regex@npm:5.0.1" - checksum: 9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 + checksum: 10c0/9a64bb8627b434ba9327b60c027742e5d17ac69277960d041898596271d992d4d52ba7267a63ca10232e29f6107fc8a835f6ce8d719b88c5f8493f8254813737 languageName: node linkType: hard "ansi-regex@npm:^6.0.1": version: 6.0.1 resolution: "ansi-regex@npm:6.0.1" - checksum: cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 + checksum: 10c0/cbe16dbd2c6b2735d1df7976a7070dd277326434f0212f43abf6d87674095d247968209babdaad31bb00882fa68807256ba9be340eec2f1004de14ca75f52a08 languageName: node linkType: hard @@ -110,55 +119,55 @@ __metadata: resolution: "ansi-styles@npm:4.3.0" dependencies: color-convert: "npm:^2.0.1" - checksum: 895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 + checksum: 10c0/895a23929da416f2bd3de7e9cb4eabd340949328ab85ddd6e484a637d8f6820d485f53933446f5291c3b760cbc488beb8e88573dd0f9c7daf83dccc8fe81b041 languageName: node linkType: hard "ansi-styles@npm:^6.1.0": version: 6.2.1 resolution: "ansi-styles@npm:6.2.1" - checksum: 5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c + checksum: 10c0/5d1ec38c123984bcedd996eac680d548f31828bd679a66db2bdf11844634dde55fec3efa9c6bb1d89056a5e79c1ac540c4c784d592ea1d25028a92227d2f2d5c languageName: node linkType: hard -"archiver-utils@npm:^4.0.1": - version: 4.0.1 - resolution: "archiver-utils@npm:4.0.1" +"archiver-utils@npm:^5.0.0, archiver-utils@npm:^5.0.2": + version: 5.0.2 + resolution: "archiver-utils@npm:5.0.2" dependencies: - glob: "npm:^8.0.0" + glob: "npm:^10.0.0" graceful-fs: "npm:^4.2.0" + is-stream: "npm:^2.0.1" lazystream: "npm:^1.0.0" lodash: "npm:^4.17.15" normalize-path: "npm:^3.0.0" - readable-stream: "npm:^3.6.0" - checksum: fc646fe1f8e3650383b6f79384e1c8f69caf7685c705221e23393a674ee1d67331e246250a72b03ec2fbdb2cfe30adc2d4287f6357684d6843d604738bf2c870 + readable-stream: "npm:^4.0.0" + checksum: 10c0/3782c5fa9922186aa1a8e41ed0c2867569faa5f15c8e5e6418ea4c1b730b476e21bd68270b3ea457daf459ae23aaea070b2b9f90cf90a59def8dc79b9e4ef538 languageName: node linkType: hard -"archiver@npm:^6.0.1": - version: 6.0.1 - resolution: "archiver@npm:6.0.1" +"archiver@npm:^7.0.1": + version: 7.0.1 + resolution: "archiver@npm:7.0.1" dependencies: - archiver-utils: "npm:^4.0.1" + archiver-utils: "npm:^5.0.2" async: "npm:^3.2.4" - buffer-crc32: "npm:^0.2.1" - readable-stream: "npm:^3.6.0" + buffer-crc32: "npm:^1.0.0" + readable-stream: "npm:^4.0.0" readdir-glob: "npm:^1.1.2" tar-stream: "npm:^3.0.0" - zip-stream: "npm:^5.0.1" - checksum: 54c5a634b39691114e727d4b4f360439fa7cd40b414c9d909606fbfd7048037f7dccefa49337f9ed19b1f5c209e021ce5e1ff9c6b547907257bc71f1af6f8cf3 + zip-stream: "npm:^6.0.1" + checksum: 10c0/02afd87ca16f6184f752db8e26884e6eff911c476812a0e7f7b26c4beb09f06119807f388a8e26ed2558aa8ba9db28646ebd147a4f99e46813b8b43158e1438e languageName: node linkType: hard -"asn1.js@npm:^5.2.0": - version: 5.4.1 - resolution: "asn1.js@npm:5.4.1" +"asn1.js@npm:^4.10.1": + version: 4.10.1 + resolution: "asn1.js@npm:4.10.1" dependencies: bn.js: "npm:^4.0.0" inherits: "npm:^2.0.1" minimalistic-assert: "npm:^1.0.0" - safer-buffer: "npm:^2.1.0" - checksum: b577232fa6069cc52bb128e564002c62b2b1fe47f7137bdcd709c0b8495aa79cee0f8cc458a831b2d8675900eea0d05781b006be5e1aa4f0ae3577a73ec20324 + checksum: 10c0/afa7f3ab9e31566c80175a75b182e5dba50589dcc738aa485be42bdd787e2a07246a4b034d481861123cbe646a7656f318f4f1cad2e9e5e808a210d5d6feaa88 languageName: node linkType: hard @@ -168,49 +177,56 @@ __metadata: dependencies: object.assign: "npm:^4.1.4" util: "npm:^0.10.4" - checksum: 836688b928b68b7fc5bbc165443e16a62623d57676a1e8a980a0316f9ae86e5e0a102c63470491bf55a8545e75766303640c0c7ad1cf6bfa5450130396043bbd + checksum: 10c0/836688b928b68b7fc5bbc165443e16a62623d57676a1e8a980a0316f9ae86e5e0a102c63470491bf55a8545e75766303640c0c7ad1cf6bfa5450130396043bbd languageName: node linkType: hard "async@npm:^3.2.4": version: 3.2.5 resolution: "async@npm:3.2.5" - checksum: 1408287b26c6db67d45cb346e34892cee555b8b59e6c68e6f8c3e495cad5ca13b4f218180e871f3c2ca30df4ab52693b66f2f6ff43644760cab0b2198bda79c1 + checksum: 10c0/1408287b26c6db67d45cb346e34892cee555b8b59e6c68e6f8c3e495cad5ca13b4f218180e871f3c2ca30df4ab52693b66f2f6ff43644760cab0b2198bda79c1 languageName: node linkType: hard "b4a@npm:^1.6.4": - version: 1.6.4 - resolution: "b4a@npm:1.6.4" - checksum: a0af707430c3643fd8d9418c732849d3626f1c9281489e021fcad969fb4808fb9f67b224de36b59c9c3b5a13d853482fee0c0eb53f7aec12d540fa67f63648b6 + version: 1.6.6 + resolution: "b4a@npm:1.6.6" + checksum: 10c0/56f30277666cb511a15829e38d369b114df7dc8cec4cedc09cc5d685bc0f27cb63c7bcfb58e09a19a1b3c4f2541069ab078b5328542e85d74a39620327709a38 languageName: node linkType: hard "balanced-match@npm:^1.0.0": version: 1.0.2 resolution: "balanced-match@npm:1.0.2" - checksum: 9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee + languageName: node + linkType: hard + +"bare-events@npm:^2.2.0": + version: 2.2.2 + resolution: "bare-events@npm:2.2.2" + checksum: 10c0/bacdaaf072f87ab5d2ed0c2fc519ef0fa8f6acd834fee50710a05f416a1b73ed99c9c6dfbefdd462ec4eb726d8f74e4a8476c2f8c3ae8812919c67eacb1f807f languageName: node linkType: hard -"base64-js@npm:^1.0.2": +"base64-js@npm:^1.0.2, base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" - checksum: f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf + checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf languageName: node linkType: hard "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.11.9": version: 4.12.0 resolution: "bn.js@npm:4.12.0" - checksum: 9736aaa317421b6b3ed038ff3d4491935a01419ac2d83ddcfebc5717385295fcfcf0c57311d90fe49926d0abbd7a9dbefdd8861e6129939177f7e67ebc645b21 + checksum: 10c0/9736aaa317421b6b3ed038ff3d4491935a01419ac2d83ddcfebc5717385295fcfcf0c57311d90fe49926d0abbd7a9dbefdd8861e6129939177f7e67ebc645b21 languageName: node linkType: hard "bn.js@npm:^5.0.0, bn.js@npm:^5.2.1": version: 5.2.1 resolution: "bn.js@npm:5.2.1" - checksum: bed3d8bd34ec89dbcf9f20f88bd7d4a49c160fda3b561c7bb227501f974d3e435a48fb9b61bc3de304acab9215a3bda0803f7017ffb4d0016a0c3a740a283caa + checksum: 10c0/bed3d8bd34ec89dbcf9f20f88bd7d4a49c160fda3b561c7bb227501f974d3e435a48fb9b61bc3de304acab9215a3bda0803f7017ffb4d0016a0c3a740a283caa languageName: node linkType: hard @@ -219,18 +235,18 @@ __metadata: resolution: "brace-expansion@npm:2.0.1" dependencies: balanced-match: "npm:^1.0.0" - checksum: b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f + checksum: 10c0/b358f2fe060e2d7a87aa015979ecea07f3c37d4018f8d6deb5bd4c229ad3a0384fe6029bb76cd8be63c81e516ee52d1a0673edbe2023d53a5191732ae3c3e49f languageName: node linkType: hard "brorand@npm:^1.0.1, brorand@npm:^1.1.0": version: 1.1.0 resolution: "brorand@npm:1.1.0" - checksum: 6f366d7c4990f82c366e3878492ba9a372a73163c09871e80d82fb4ae0d23f9f8924cb8a662330308206e6b3b76ba1d528b4601c9ef73c2166b440b2ea3b7571 + checksum: 10c0/6f366d7c4990f82c366e3878492ba9a372a73163c09871e80d82fb4ae0d23f9f8924cb8a662330308206e6b3b76ba1d528b4601c9ef73c2166b440b2ea3b7571 languageName: node linkType: hard -"browserify-aes@npm:^1.0.0, browserify-aes@npm:^1.0.4": +"browserify-aes@npm:^1.0.4, browserify-aes@npm:^1.2.0": version: 1.2.0 resolution: "browserify-aes@npm:1.2.0" dependencies: @@ -240,7 +256,7 @@ __metadata: evp_bytestokey: "npm:^1.0.3" inherits: "npm:^2.0.1" safe-buffer: "npm:^5.0.1" - checksum: 967f2ae60d610b7b252a4cbb55a7a3331c78293c94b4dd9c264d384ca93354c089b3af9c0dd023534efdc74ffbc82510f7ad4399cf82bc37bc07052eea485f18 + checksum: 10c0/967f2ae60d610b7b252a4cbb55a7a3331c78293c94b4dd9c264d384ca93354c089b3af9c0dd023534efdc74ffbc82510f7ad4399cf82bc37bc07052eea485f18 languageName: node linkType: hard @@ -251,7 +267,7 @@ __metadata: browserify-aes: "npm:^1.0.4" browserify-des: "npm:^1.0.0" evp_bytestokey: "npm:^1.0.0" - checksum: aa256dcb42bc53a67168bbc94ab85d243b0a3b56109dee3b51230b7d010d9b78985ffc1fb36e145c6e4db151f888076c1cfc207baf1525d3e375cbe8187fe27d + checksum: 10c0/aa256dcb42bc53a67168bbc94ab85d243b0a3b56109dee3b51230b7d010d9b78985ffc1fb36e145c6e4db151f888076c1cfc207baf1525d3e375cbe8187fe27d languageName: node linkType: hard @@ -263,7 +279,7 @@ __metadata: des.js: "npm:^1.0.0" inherits: "npm:^2.0.1" safe-buffer: "npm:^5.1.2" - checksum: 943eb5d4045eff80a6cde5be4e5fbb1f2d5002126b5a4789c3c1aae3cdddb1eb92b00fb92277f512288e5c6af330730b1dbabcf7ce0923e749e151fcee5a074d + checksum: 10c0/943eb5d4045eff80a6cde5be4e5fbb1f2d5002126b5a4789c3c1aae3cdddb1eb92b00fb92277f512288e5c6af330730b1dbabcf7ce0923e749e151fcee5a074d languageName: node linkType: hard @@ -273,24 +289,25 @@ __metadata: dependencies: bn.js: "npm:^5.0.0" randombytes: "npm:^2.0.1" - checksum: fb2b5a8279d8a567a28d8ee03fb62e448428a906bab5c3dc9e9c3253ace551b5ea271db15e566ac78f1b1d71b243559031446604168b9235c351a32cae99d02a + checksum: 10c0/fb2b5a8279d8a567a28d8ee03fb62e448428a906bab5c3dc9e9c3253ace551b5ea271db15e566ac78f1b1d71b243559031446604168b9235c351a32cae99d02a languageName: node linkType: hard "browserify-sign@npm:^4.0.0": - version: 4.2.2 - resolution: "browserify-sign@npm:4.2.2" + version: 4.2.3 + resolution: "browserify-sign@npm:4.2.3" dependencies: bn.js: "npm:^5.2.1" browserify-rsa: "npm:^4.1.0" create-hash: "npm:^1.2.0" create-hmac: "npm:^1.1.7" - elliptic: "npm:^6.5.4" + elliptic: "npm:^6.5.5" + hash-base: "npm:~3.0" inherits: "npm:^2.0.4" - parse-asn1: "npm:^5.1.6" - readable-stream: "npm:^3.6.2" + parse-asn1: "npm:^5.1.7" + readable-stream: "npm:^2.3.8" safe-buffer: "npm:^5.2.1" - checksum: 4d1292e5c165d93455630515003f0e95eed9239c99e2d373920c5b56903d16296a3d23cd4bdc4d298f55ad9b83714a9e63bc4839f1166c303349a16e84e9b016 + checksum: 10c0/30c0eba3f5970a20866a4d3fbba2c5bd1928cd24f47faf995f913f1499214c6f3be14bb4d6ec1ab5c6cafb1eca9cb76ba1c2e1c04ed018370634d4e659c77216 languageName: node linkType: hard @@ -299,28 +316,28 @@ __metadata: resolution: "browserify-zlib@npm:0.2.0" dependencies: pako: "npm:~1.0.5" - checksum: 9ab10b6dc732c6c5ec8ebcbe5cb7fe1467f97402c9b2140113f47b5f187b9438f93a8e065d8baf8b929323c18324fbf1105af479ee86d9d36cab7d7ef3424ad9 + checksum: 10c0/9ab10b6dc732c6c5ec8ebcbe5cb7fe1467f97402c9b2140113f47b5f187b9438f93a8e065d8baf8b929323c18324fbf1105af479ee86d9d36cab7d7ef3424ad9 languageName: node linkType: hard -"buffer-crc32@npm:^0.2.1": - version: 0.2.13 - resolution: "buffer-crc32@npm:0.2.13" - checksum: cb0a8ddf5cf4f766466db63279e47761eb825693eeba6a5a95ee4ec8cb8f81ede70aa7f9d8aeec083e781d47154290eb5d4d26b3f7a465ec57fb9e7d59c47150 +"buffer-crc32@npm:^1.0.0": + version: 1.0.0 + resolution: "buffer-crc32@npm:1.0.0" + checksum: 10c0/8b86e161cee4bb48d5fa622cbae4c18f25e4857e5203b89e23de59e627ab26beb82d9d7999f2b8de02580165f61f83f997beaf02980cdf06affd175b651921ab languageName: node linkType: hard "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" - checksum: 124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 + checksum: 10c0/124fff9d66d691a86d3b062eff4663fe437a9d9ee4b47b1b9e97f5a5d14f6d5399345db80f796827be7c95e70a8e765dd404b7c3ff3b3324f98e9b0c8826cc34 languageName: node linkType: hard "buffer-xor@npm:^1.0.3": version: 1.0.3 resolution: "buffer-xor@npm:1.0.3" - checksum: fd269d0e0bf71ecac3146187cfc79edc9dbb054e2ee69b4d97dfb857c6d997c33de391696d04bdd669272751fa48e7872a22f3a6c7b07d6c0bc31dbe02a4075c + checksum: 10c0/fd269d0e0bf71ecac3146187cfc79edc9dbb054e2ee69b4d97dfb857c6d997c33de391696d04bdd669272751fa48e7872a22f3a6c7b07d6c0bc31dbe02a4075c languageName: node linkType: hard @@ -331,27 +348,37 @@ __metadata: base64-js: "npm:^1.0.2" ieee754: "npm:^1.1.4" isarray: "npm:^1.0.0" - checksum: dc443d7e7caab23816b58aacdde710b72f525ad6eecd7d738fcaa29f6d6c12e8d9c13fed7219fd502be51ecf0615f5c077d4bdc6f9308dde2e53f8e5393c5b21 + checksum: 10c0/dc443d7e7caab23816b58aacdde710b72f525ad6eecd7d738fcaa29f6d6c12e8d9c13fed7219fd502be51ecf0615f5c077d4bdc6f9308dde2e53f8e5393c5b21 + languageName: node + linkType: hard + +"buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 languageName: node linkType: hard "builtin-status-codes@npm:^3.0.0": version: 3.0.0 resolution: "builtin-status-codes@npm:3.0.0" - checksum: c37bbba11a34c4431e56bd681b175512e99147defbe2358318d8152b3a01df7bf25e0305873947e5b350073d5ef41a364a22b37e48f1fb6d2fe6d5286a0f348c + checksum: 10c0/c37bbba11a34c4431e56bd681b175512e99147defbe2358318d8152b3a01df7bf25e0305873947e5b350073d5ef41a364a22b37e48f1fb6d2fe6d5286a0f348c languageName: node linkType: hard "bytes@npm:3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" - checksum: 76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e + checksum: 10c0/76d1c43cbd602794ad8ad2ae94095cddeb1de78c5dddaa7005c51af10b0176c69971a6d88e805a90c2b6550d76636e43c40d8427a808b8645ede885de4a0358e languageName: node linkType: hard "cacache@npm:^18.0.0": - version: 18.0.1 - resolution: "cacache@npm:18.0.1" + version: 18.0.3 + resolution: "cacache@npm:18.0.3" dependencies: "@npmcli/fs": "npm:^3.1.0" fs-minipass: "npm:^3.0.0" @@ -365,25 +392,27 @@ __metadata: ssri: "npm:^10.0.0" tar: "npm:^6.1.11" unique-filename: "npm:^3.0.0" - checksum: a31666805a80a8b16ad3f85faf66750275a9175a3480896f4f6d31b5d53ef190484fabd71bdb6d2ea5603c717fbef09f4af03d6a65b525c8ef0afaa44c361866 + checksum: 10c0/dfda92840bb371fb66b88c087c61a74544363b37a265023223a99965b16a16bbb87661fe4948718d79df6e0cc04e85e62784fbcf1832b2a5e54ff4c46fbb45b7 languageName: node linkType: hard -"call-bind@npm:^1.0.0, call-bind@npm:^1.0.2": - version: 1.0.5 - resolution: "call-bind@npm:1.0.5" +"call-bind@npm:^1.0.5, call-bind@npm:^1.0.7": + version: 1.0.7 + resolution: "call-bind@npm:1.0.7" dependencies: + es-define-property: "npm:^1.0.0" + es-errors: "npm:^1.3.0" function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.1" - set-function-length: "npm:^1.1.1" - checksum: a6172c168fd6dacf744fcde745099218056bd755c50415b592655dcd6562157ed29f130f56c3f6db2250f67e4bd62e5c218cdc56d7bfd76e0bda50770fce2d10 + get-intrinsic: "npm:^1.2.4" + set-function-length: "npm:^1.2.1" + checksum: 10c0/a3ded2e423b8e2a265983dba81c27e125b48eefb2655e7dfab6be597088da3d47c47976c24bc51b8fd9af1061f8f87b4ab78a314f3c77784b2ae2ba535ad8b8d languageName: node linkType: hard "chownr@npm:^2.0.0": version: 2.0.0 resolution: "chownr@npm:2.0.0" - checksum: 594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 + checksum: 10c0/594754e1303672171cc04e50f6c398ae16128eb134a88f801bf5354fd96f205320f23536a045d9abd8b51024a149696e51231565891d4efdab8846021ecf88e6 languageName: node linkType: hard @@ -393,21 +422,21 @@ __metadata: dependencies: inherits: "npm:^2.0.1" safe-buffer: "npm:^5.0.1" - checksum: d8d005f8b64d8a77b3d3ce531301ae7b45902c9cab4ec8b66bdbd2bf2a1d9fceb9a2133c293eb3c060b2d964da0f14c47fb740366081338aa3795dd1faa8984b + checksum: 10c0/d8d005f8b64d8a77b3d3ce531301ae7b45902c9cab4ec8b66bdbd2bf2a1d9fceb9a2133c293eb3c060b2d964da0f14c47fb740366081338aa3795dd1faa8984b languageName: node linkType: hard "clean-stack@npm:^2.0.0": version: 2.2.0 resolution: "clean-stack@npm:2.2.0" - checksum: 1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 + checksum: 10c0/1f90262d5f6230a17e27d0c190b09d47ebe7efdd76a03b5a1127863f7b3c9aec4c3e6c8bb3a7bbf81d553d56a1fd35728f5a8ef4c63f867ac8d690109742a8c1 languageName: node linkType: hard "cluster-key-slot@npm:^1.1.0": version: 1.1.2 resolution: "cluster-key-slot@npm:1.1.2" - checksum: d7d39ca28a8786e9e801eeb8c770e3c3236a566625d7299a47bb71113fb2298ce1039596acb82590e598c52dbc9b1f088c8f587803e697cb58e1867a95ff94d3 + checksum: 10c0/d7d39ca28a8786e9e801eeb8c770e3c3236a566625d7299a47bb71113fb2298ce1039596acb82590e598c52dbc9b1f088c8f587803e697cb58e1867a95ff94d3 languageName: node linkType: hard @@ -416,40 +445,41 @@ __metadata: resolution: "color-convert@npm:2.0.1" dependencies: color-name: "npm:~1.1.4" - checksum: 37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 + checksum: 10c0/37e1150172f2e311fe1b2df62c6293a342ee7380da7b9cfdba67ea539909afbd74da27033208d01d6d5cfc65ee7868a22e18d7e7648e004425441c0f8a15a7d7 languageName: node linkType: hard "color-name@npm:~1.1.4": version: 1.1.4 resolution: "color-name@npm:1.1.4" - checksum: a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 + checksum: 10c0/a1a3f914156960902f46f7f56bc62effc6c94e84b2cae157a526b1c1f74b677a47ec602bf68a61abfa2b42d15b7c5651c6dbe72a43af720bc588dff885b10f95 languageName: node linkType: hard -"compress-commons@npm:^5.0.1": - version: 5.0.1 - resolution: "compress-commons@npm:5.0.1" +"compress-commons@npm:^6.0.2": + version: 6.0.2 + resolution: "compress-commons@npm:6.0.2" dependencies: crc-32: "npm:^1.2.0" - crc32-stream: "npm:^5.0.0" + crc32-stream: "npm:^6.0.0" + is-stream: "npm:^2.0.1" normalize-path: "npm:^3.0.0" - readable-stream: "npm:^3.6.0" - checksum: 1c604ac753b4ec643a807f3db545bf497d1e9c6f81e9132280c98d972b02bbeba087e7fb2d53f3043f9643a64a6140e9e39b94329040695d404b83a0c7f38fa2 + readable-stream: "npm:^4.0.0" + checksum: 10c0/2347031b7c92c8ed5011b07b93ec53b298fa2cd1800897532ac4d4d1aeae06567883f481b6e35f13b65fc31b190c751df6635434d525562f0203fde76f1f0814 languageName: node linkType: hard "console-browserify@npm:^1.1.0": version: 1.2.0 resolution: "console-browserify@npm:1.2.0" - checksum: 89b99a53b7d6cee54e1e64fa6b1f7ac24b844b4019c5d39db298637e55c1f4ffa5c165457ad984864de1379df2c8e1886cbbdac85d9dbb6876a9f26c3106f226 + checksum: 10c0/89b99a53b7d6cee54e1e64fa6b1f7ac24b844b4019c5d39db298637e55c1f4ffa5c165457ad984864de1379df2c8e1886cbbdac85d9dbb6876a9f26c3106f226 languageName: node linkType: hard "constants-browserify@npm:^1.0.0": version: 1.0.0 resolution: "constants-browserify@npm:1.0.0" - checksum: ab49b1d59a433ed77c964d90d19e08b2f77213fb823da4729c0baead55e3c597f8f97ebccfdfc47bd896d43854a117d114c849a6f659d9986420e97da0f83ac5 + checksum: 10c0/ab49b1d59a433ed77c964d90d19e08b2f77213fb823da4729c0baead55e3c597f8f97ebccfdfc47bd896d43854a117d114c849a6f659d9986420e97da0f83ac5 languageName: node linkType: hard @@ -459,21 +489,21 @@ __metadata: dependencies: depd: "npm:~2.0.0" keygrip: "npm:~1.1.0" - checksum: 3ffa1c0e992b62ee119adae4dd2ddd4a89166fa5434cd9bd9ff84ec4d2f14dfe2318a601280abfe32a4f64f884ec9345fb1912e488b002d188d2efa0d3919ba3 + checksum: 10c0/3ffa1c0e992b62ee119adae4dd2ddd4a89166fa5434cd9bd9ff84ec4d2f14dfe2318a601280abfe32a4f64f884ec9345fb1912e488b002d188d2efa0d3919ba3 languageName: node linkType: hard "core-js-pure@npm:^3.30.2": - version: 3.33.3 - resolution: "core-js-pure@npm:3.33.3" - checksum: 97cf39cc013f6a4f77700762de36b495228b3c087fc04b61e86bfbfb475595529966cabbcf37e738e3a468c486e815c85118d120cc6fc4960da08a14caf69826 + version: 3.37.1 + resolution: "core-js-pure@npm:3.37.1" + checksum: 10c0/38200d08862b4ef2207af72a7525f7b9ac750f5e1d84ef27a3e314aefa69518179a9b732f51ebe35c3b38606d9fa4f686fcf6eff067615cc293a3b1c84041e74 languageName: node linkType: hard "core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" - checksum: 90a0e40abbddfd7618f8ccd63a74d88deea94e77d0e8dbbea059fa7ebebb8fbb4e2909667fe26f3a467073de1a542ebe6ae4c73a73745ac5833786759cd906c9 + checksum: 10c0/90a0e40abbddfd7618f8ccd63a74d88deea94e77d0e8dbbea059fa7ebebb8fbb4e2909667fe26f3a467073de1a542ebe6ae4c73a73745ac5833786759cd906c9 languageName: node linkType: hard @@ -482,17 +512,17 @@ __metadata: resolution: "crc-32@npm:1.2.2" bin: crc32: bin/crc32.njs - checksum: 11dcf4a2e77ee793835d49f2c028838eae58b44f50d1ff08394a610bfd817523f105d6ae4d9b5bef0aad45510f633eb23c903e9902e4409bed1ce70cb82b9bf0 + checksum: 10c0/11dcf4a2e77ee793835d49f2c028838eae58b44f50d1ff08394a610bfd817523f105d6ae4d9b5bef0aad45510f633eb23c903e9902e4409bed1ce70cb82b9bf0 languageName: node linkType: hard -"crc32-stream@npm:^5.0.0": - version: 5.0.0 - resolution: "crc32-stream@npm:5.0.0" +"crc32-stream@npm:^6.0.0": + version: 6.0.0 + resolution: "crc32-stream@npm:6.0.0" dependencies: crc-32: "npm:^1.2.0" - readable-stream: "npm:^3.4.0" - checksum: bd6e6d49b76fd562eef3a4b7b64b1e551fb5dfca0a3b54fb7e59765c57468295b60755f85d3450fd61eee01dcca0974600157717cad8f356d513c28bac726a41 + readable-stream: "npm:^4.0.0" + checksum: 10c0/bf9c84571ede2d119c2b4f3a9ef5eeb9ff94b588493c0d3862259af86d3679dcce1c8569dd2b0a6eff2f35f5e2081cc1263b846d2538d4054da78cf34f262a3d languageName: node linkType: hard @@ -502,7 +532,7 @@ __metadata: dependencies: bn.js: "npm:^4.1.0" elliptic: "npm:^6.5.3" - checksum: 77b11a51360fec9c3bce7a76288fc0deba4b9c838d5fb354b3e40c59194d23d66efe6355fd4b81df7580da0661e1334a235a2a5c040b7569ba97db428d466e7f + checksum: 10c0/77b11a51360fec9c3bce7a76288fc0deba4b9c838d5fb354b3e40c59194d23d66efe6355fd4b81df7580da0661e1334a235a2a5c040b7569ba97db428d466e7f languageName: node linkType: hard @@ -515,7 +545,7 @@ __metadata: md5.js: "npm:^1.3.4" ripemd160: "npm:^2.0.1" sha.js: "npm:^2.4.0" - checksum: d402e60e65e70e5083cb57af96d89567954d0669e90550d7cec58b56d49c4b193d35c43cec8338bc72358198b8cbf2f0cac14775b651e99238e1cf411490f915 + checksum: 10c0/d402e60e65e70e5083cb57af96d89567954d0669e90550d7cec58b56d49c4b193d35c43cec8338bc72358198b8cbf2f0cac14775b651e99238e1cf411490f915 languageName: node linkType: hard @@ -529,7 +559,7 @@ __metadata: ripemd160: "npm:^2.0.0" safe-buffer: "npm:^5.0.1" sha.js: "npm:^2.4.8" - checksum: 24332bab51011652a9a0a6d160eed1e8caa091b802335324ae056b0dcb5acbc9fcf173cf10d128eba8548c3ce98dfa4eadaa01bd02f44a34414baee26b651835 + checksum: 10c0/24332bab51011652a9a0a6d160eed1e8caa091b802335324ae056b0dcb5acbc9fcf173cf10d128eba8548c3ce98dfa4eadaa01bd02f44a34414baee26b651835 languageName: node linkType: hard @@ -540,7 +570,7 @@ __metadata: path-key: "npm:^3.1.0" shebang-command: "npm:^2.0.0" which: "npm:^2.0.1" - checksum: 5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 + checksum: 10c0/5738c312387081c98d69c98e105b6327b069197f864a60593245d64c8089c8a0a744e16349281210d56835bb9274130d825a78b2ad6853ca13cfbeffc0c31750 languageName: node linkType: hard @@ -559,7 +589,7 @@ __metadata: public-encrypt: "npm:^4.0.0" randombytes: "npm:^2.0.0" randomfill: "npm:^1.0.3" - checksum: 0c20198886576050a6aa5ba6ae42f2b82778bfba1753d80c5e7a090836890dc372bdc780986b2568b4fb8ed2a91c958e61db1f0b6b1cc96af4bd03ffc298ba92 + checksum: 10c0/0c20198886576050a6aa5ba6ae42f2b82778bfba1753d80c5e7a090836890dc372bdc780986b2568b4fb8ed2a91c958e61db1f0b6b1cc96af4bd03ffc298ba92 languageName: node linkType: hard @@ -571,43 +601,43 @@ __metadata: peerDependenciesMeta: supports-color: optional: true - checksum: cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 + checksum: 10c0/cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 languageName: node linkType: hard -"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.1": - version: 1.1.1 - resolution: "define-data-property@npm:1.1.1" +"define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": + version: 1.1.4 + resolution: "define-data-property@npm:1.1.4" dependencies: - get-intrinsic: "npm:^1.2.1" + es-define-property: "npm:^1.0.0" + es-errors: "npm:^1.3.0" gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.0" - checksum: 77ef6e0bceb515e05b5913ab635a84d537cee84f8a7c37c77fdcb31fc5b80f6dbe81b33375e4b67d96aa04e6a0d8d4ea099e431d83f089af8d93adfb584bcb94 + checksum: 10c0/dea0606d1483eb9db8d930d4eac62ca0fa16738b0b3e07046cddfacf7d8c868bbe13fa0cb263eb91c7d0d527960dc3f2f2471a69ed7816210307f6744fe62e37 languageName: node linkType: hard -"define-properties@npm:^1.1.4": +"define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" dependencies: define-data-property: "npm:^1.0.1" has-property-descriptors: "npm:^1.0.0" object-keys: "npm:^1.1.1" - checksum: 88a152319ffe1396ccc6ded510a3896e77efac7a1bfbaa174a7b00414a1747377e0bb525d303794a47cf30e805c2ec84e575758512c6e44a993076d29fd4e6c3 + checksum: 10c0/88a152319ffe1396ccc6ded510a3896e77efac7a1bfbaa174a7b00414a1747377e0bb525d303794a47cf30e805c2ec84e575758512c6e44a993076d29fd4e6c3 languageName: node linkType: hard "denque@npm:^2.1.0": version: 2.1.0 resolution: "denque@npm:2.1.0" - checksum: f9ef81aa0af9c6c614a727cb3bd13c5d7db2af1abf9e6352045b86e85873e629690f6222f4edd49d10e4ccf8f078bbeec0794fafaf61b659c0589d0c511ec363 + checksum: 10c0/f9ef81aa0af9c6c614a727cb3bd13c5d7db2af1abf9e6352045b86e85873e629690f6222f4edd49d10e4ccf8f078bbeec0794fafaf61b659c0589d0c511ec363 languageName: node linkType: hard "depd@npm:2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" - checksum: 58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c + checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c languageName: node linkType: hard @@ -617,7 +647,7 @@ __metadata: dependencies: inherits: "npm:^2.0.1" minimalistic-assert: "npm:^1.0.0" - checksum: 671354943ad67493e49eb4c555480ab153edd7cee3a51c658082fcde539d2690ed2a4a0b5d1f401f9cde822edf3939a6afb2585f32c091f2d3a1b1665cd45236 + checksum: 10c0/671354943ad67493e49eb4c555480ab153edd7cee3a51c658082fcde539d2690ed2a4a0b5d1f401f9cde822edf3939a6afb2585f32c091f2d3a1b1665cd45236 languageName: node linkType: hard @@ -628,27 +658,27 @@ __metadata: bn.js: "npm:^4.1.0" miller-rabin: "npm:^4.0.0" randombytes: "npm:^2.0.0" - checksum: ce53ccafa9ca544b7fc29b08a626e23a9b6562efc2a98559a0c97b4718937cebaa9b5d7d0a05032cc9c1435e9b3c1532b9e9bf2e0ede868525922807ad6e1ecf + checksum: 10c0/ce53ccafa9ca544b7fc29b08a626e23a9b6562efc2a98559a0c97b4718937cebaa9b5d7d0a05032cc9c1435e9b3c1532b9e9bf2e0ede868525922807ad6e1ecf languageName: node linkType: hard "domain-browser@npm:^1.1.1": version: 1.2.0 resolution: "domain-browser@npm:1.2.0" - checksum: a955f482f4b4710fbd77c12a33e77548d63603c30c80f61a80519f27e3db1ba8530b914584cc9e9365d2038753d6b5bd1f4e6c81e432b007b0ec95b8b5e69b1b + checksum: 10c0/a955f482f4b4710fbd77c12a33e77548d63603c30c80f61a80519f27e3db1ba8530b914584cc9e9365d2038753d6b5bd1f4e6c81e432b007b0ec95b8b5e69b1b languageName: node linkType: hard "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" - checksum: 26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 + checksum: 10c0/26f364ebcdb6395f95124fda411f63137a4bfb5d3a06453f7f23dfe52502905bd84e0488172e0f9ec295fdc45f05c23d5d91baf16bd26f0fe9acd777a188dc39 languageName: node linkType: hard -"elliptic@npm:^6.5.3, elliptic@npm:^6.5.4": - version: 6.5.4 - resolution: "elliptic@npm:6.5.4" +"elliptic@npm:^6.5.3, elliptic@npm:^6.5.5": + version: 6.5.5 + resolution: "elliptic@npm:6.5.5" dependencies: bn.js: "npm:^4.11.9" brorand: "npm:^1.1.0" @@ -657,21 +687,21 @@ __metadata: inherits: "npm:^2.0.4" minimalistic-assert: "npm:^1.0.1" minimalistic-crypto-utils: "npm:^1.0.1" - checksum: 5f361270292c3b27cf0843e84526d11dec31652f03c2763c6c2b8178548175ff5eba95341dd62baff92b2265d1af076526915d8af6cc9cb7559c44a62f8ca6e2 + checksum: 10c0/3e591e93783a1b66f234ebf5bd3a8a9a8e063a75073a35a671e03e3b25253b6e33ac121f7efe9b8808890fffb17b40596cc19d01e6e8d1fa13b9a56ff65597c8 languageName: node linkType: hard "emoji-regex@npm:^8.0.0": version: 8.0.0 resolution: "emoji-regex@npm:8.0.0" - checksum: b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 + checksum: 10c0/b6053ad39951c4cf338f9092d7bfba448cdfd46fe6a2a034700b149ac9ffbc137e361cbd3c442297f86bed2e5f7576c1b54cc0a6bf8ef5106cc62f496af35010 languageName: node linkType: hard "emoji-regex@npm:^9.2.2": version: 9.2.2 resolution: "emoji-regex@npm:9.2.2" - checksum: af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 + checksum: 10c0/af014e759a72064cf66e6e694a7fc6b0ed3d8db680427b021a89727689671cefe9d04151b2cad51dbaf85d5ba790d061cd167f1cf32eb7b281f6368b3c181639 languageName: node linkType: hard @@ -680,28 +710,51 @@ __metadata: resolution: "encoding@npm:0.1.13" dependencies: iconv-lite: "npm:^0.6.2" - checksum: 36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 + checksum: 10c0/36d938712ff00fe1f4bac88b43bcffb5930c1efa57bbcdca9d67e1d9d6c57cfb1200fb01efe0f3109b2ce99b231f90779532814a81370a1bd3274a0f58585039 languageName: node linkType: hard "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" - checksum: 285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 + checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 languageName: node linkType: hard "err-code@npm:^2.0.2": version: 2.0.3 resolution: "err-code@npm:2.0.3" - checksum: b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + checksum: 10c0/b642f7b4dd4a376e954947550a3065a9ece6733ab8e51ad80db727aaae0817c2e99b02a97a3d6cecc648a97848305e728289cf312d09af395403a90c9d4d8a66 + languageName: node + linkType: hard + +"es-define-property@npm:^1.0.0": + version: 1.0.0 + resolution: "es-define-property@npm:1.0.0" + dependencies: + get-intrinsic: "npm:^1.2.4" + checksum: 10c0/6bf3191feb7ea2ebda48b577f69bdfac7a2b3c9bcf97307f55fd6ef1bbca0b49f0c219a935aca506c993d8c5d8bddd937766cb760cd5e5a1071351f2df9f9aa4 + languageName: node + linkType: hard + +"es-errors@npm:^1.3.0": + version: 1.3.0 + resolution: "es-errors@npm:1.3.0" + checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 + languageName: node + linkType: hard + +"event-target-shim@npm:^5.0.0": + version: 5.0.1 + resolution: "event-target-shim@npm:5.0.1" + checksum: 10c0/0255d9f936215fd206156fd4caa9e8d35e62075d720dc7d847e89b417e5e62cf1ce6c9b4e0a1633a9256de0efefaf9f8d26924b1f3c8620cffb9db78e7d3076b languageName: node linkType: hard -"events@npm:^3.0.0": +"events@npm:^3.0.0, events@npm:^3.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" - checksum: d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 + checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 languageName: node linkType: hard @@ -712,14 +765,14 @@ __metadata: md5.js: "npm:^1.3.4" node-gyp: "npm:latest" safe-buffer: "npm:^5.1.1" - checksum: 77fbe2d94a902a80e9b8f5a73dcd695d9c14899c5e82967a61b1fc6cbbb28c46552d9b127cff47c45fcf684748bdbcfa0a50410349109de87ceb4b199ef6ee99 + checksum: 10c0/77fbe2d94a902a80e9b8f5a73dcd695d9c14899c5e82967a61b1fc6cbbb28c46552d9b127cff47c45fcf684748bdbcfa0a50410349109de87ceb4b199ef6ee99 languageName: node linkType: hard "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" - checksum: 160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 + checksum: 10c0/160456d2d647e6019640bd07111634d8c353038d9fa40176afb7cd49b0548bdae83b56d05e907c2cce2300b81cae35d800ef92fefb9d0208e190fa3b7d6bb579 languageName: node linkType: hard @@ -727,15 +780,15 @@ __metadata: version: 0.0.0-use.local resolution: "exporter@workspace:." dependencies: - archiver: "npm:^6.0.1" + archiver: "npm:^7.0.1" cookies: "npm:^0.9.1" generic-pool: "npm:^3.9.0" inflation: "npm:^2.1.0" - ioredis: "npm:^5.3.2" + ioredis: "npm:^5.4.1" luxon: "npm:^3.4.4" - playwright: "npm:^1.40.1" + playwright: "npm:^1.44.1" raw-body: "npm:^2.5.2" - shadow-cljs: "npm:2.27.4" + shadow-cljs: "npm:2.28.11" source-map-support: "npm:^0.5.21" xml-js: "npm:^1.6.11" xregexp: "npm:^5.1.1" @@ -745,7 +798,7 @@ __metadata: "fast-fifo@npm:^1.1.0, fast-fifo@npm:^1.2.0": version: 1.3.2 resolution: "fast-fifo@npm:1.3.2" - checksum: d53f6f786875e8b0529f784b59b4b05d4b5c31c651710496440006a398389a579c8dbcd2081311478b5bf77f4b0b21de69109c5a4eabea9d8e8783d1eb864e4c + checksum: 10c0/d53f6f786875e8b0529f784b59b4b05d4b5c31c651710496440006a398389a579c8dbcd2081311478b5bf77f4b0b21de69109c5a4eabea9d8e8783d1eb864e4c languageName: node linkType: hard @@ -755,7 +808,7 @@ __metadata: dependencies: cross-spawn: "npm:^7.0.0" signal-exit: "npm:^4.0.1" - checksum: 9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 + checksum: 10c0/9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 languageName: node linkType: hard @@ -764,7 +817,7 @@ __metadata: resolution: "fs-minipass@npm:2.1.0" dependencies: minipass: "npm:^3.0.0" - checksum: 703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 + checksum: 10c0/703d16522b8282d7299337539c3ed6edddd1afe82435e4f5b76e34a79cd74e488a8a0e26a636afc2440e1a23b03878e2122e3a2cfe375a5cf63c37d92b86a004 languageName: node linkType: hard @@ -773,14 +826,7 @@ __metadata: resolution: "fs-minipass@npm:3.0.3" dependencies: minipass: "npm:^7.0.3" - checksum: 63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 - languageName: node - linkType: hard - -"fs.realpath@npm:^1.0.0": - version: 1.0.0 - resolution: "fs.realpath@npm:1.0.0" - checksum: 444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 + checksum: 10c0/63e80da2ff9b621e2cb1596abcb9207f1cf82b968b116ccd7b959e3323144cce7fb141462200971c38bbf2ecca51695069db45265705bed09a7cd93ae5b89f94 languageName: node linkType: hard @@ -789,7 +835,7 @@ __metadata: resolution: "fsevents@npm:2.3.2" dependencies: node-gyp: "npm:latest" - checksum: be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b + checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b conditions: os=darwin languageName: node linkType: hard @@ -806,54 +852,57 @@ __metadata: "function-bind@npm:^1.1.2": version: 1.1.2 resolution: "function-bind@npm:1.1.2" - checksum: d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 + checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 languageName: node linkType: hard "generic-pool@npm:^3.9.0": version: 3.9.0 resolution: "generic-pool@npm:3.9.0" - checksum: 6b314d0d71170d5cbaf7162c423f53f8d6556b2135626a65bcdc03c089840b0a2f59eeb2d907939b8200e945eaf71ceb6630426f22d2128a1d242aec4b232aa7 + checksum: 10c0/6b314d0d71170d5cbaf7162c423f53f8d6556b2135626a65bcdc03c089840b0a2f59eeb2d907939b8200e945eaf71ceb6630426f22d2128a1d242aec4b232aa7 languageName: node linkType: hard -"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.1, get-intrinsic@npm:^1.2.2": - version: 1.2.2 - resolution: "get-intrinsic@npm:1.2.2" +"get-intrinsic@npm:^1.1.3, get-intrinsic@npm:^1.2.4": + version: 1.2.4 + resolution: "get-intrinsic@npm:1.2.4" dependencies: + es-errors: "npm:^1.3.0" function-bind: "npm:^1.1.2" has-proto: "npm:^1.0.1" has-symbols: "npm:^1.0.3" hasown: "npm:^2.0.0" - checksum: 4e7fb8adc6172bae7c4fe579569b4d5238b3667c07931cd46b4eee74bbe6ff6b91329bec311a638d8e60f5b51f44fe5445693c6be89ae88d4b5c49f7ff12db0b + checksum: 10c0/0a9b82c16696ed6da5e39b1267104475c47e3a9bdbe8b509dfe1710946e38a87be70d759f4bb3cda042d76a41ef47fe769660f3b7c0d1f68750299344ffb15b7 languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": - version: 10.3.10 - resolution: "glob@npm:10.3.10" +"glob@npm:^10.0.0": + version: 10.4.1 + resolution: "glob@npm:10.4.1" dependencies: foreground-child: "npm:^3.1.0" - jackspeak: "npm:^2.3.5" - minimatch: "npm:^9.0.1" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry: "npm:^1.10.1" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + path-scurry: "npm:^1.11.1" bin: glob: dist/esm/bin.mjs - checksum: 13d8a1feb7eac7945f8c8480e11cd4a44b24d26503d99a8d8ac8d5aefbf3e9802a2b6087318a829fad04cb4e829f25c5f4f1110c68966c498720dd261c7e344d + checksum: 10c0/77f2900ed98b9cc2a0e1901ee5e476d664dae3cd0f1b662b8bfd4ccf00d0edc31a11595807706a274ca10e1e251411bbf2e8e976c82bed0d879a9b89343ed379 languageName: node linkType: hard -"glob@npm:^8.0.0": - version: 8.1.0 - resolution: "glob@npm:8.1.0" +"glob@npm:^10.2.2, glob@npm:^10.3.10": + version: 10.3.16 + resolution: "glob@npm:10.3.16" dependencies: - fs.realpath: "npm:^1.0.0" - inflight: "npm:^1.0.4" - inherits: "npm:2" - minimatch: "npm:^5.0.1" - once: "npm:^1.3.0" - checksum: cb0b5cab17a59c57299376abe5646c7070f8acb89df5595b492dba3bfb43d301a46c01e5695f01154e6553168207cb60d4eaf07d3be4bc3eb9b0457c5c561d0f + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.1" + minipass: "npm:^7.0.4" + path-scurry: "npm:^1.11.0" + bin: + glob: dist/esm/bin.mjs + checksum: 10c0/f7eb4c3e66f221f0be3967c02527047167967549bdf8ed1bd5f6277d43a35191af4e2bb8c89f07a79664958bae088fd06659e69a0f1de462972f1eab52a715e8 languageName: node linkType: hard @@ -862,37 +911,37 @@ __metadata: resolution: "gopd@npm:1.0.1" dependencies: get-intrinsic: "npm:^1.1.3" - checksum: 505c05487f7944c552cee72087bf1567debb470d4355b1335f2c262d218ebbff805cd3715448fe29b4b380bae6912561d0467233e4165830efd28da241418c63 + checksum: 10c0/505c05487f7944c552cee72087bf1567debb470d4355b1335f2c262d218ebbff805cd3715448fe29b4b380bae6912561d0467233e4165830efd28da241418c63 languageName: node linkType: hard "graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" - checksum: 386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 + checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 languageName: node linkType: hard -"has-property-descriptors@npm:^1.0.0": - version: 1.0.1 - resolution: "has-property-descriptors@npm:1.0.1" +"has-property-descriptors@npm:^1.0.0, has-property-descriptors@npm:^1.0.2": + version: 1.0.2 + resolution: "has-property-descriptors@npm:1.0.2" dependencies: - get-intrinsic: "npm:^1.2.2" - checksum: d62ba94b40150b00d621bc64a6aedb5bf0ee495308b4b7ed6bac856043db3cdfb1db553ae81cec91c9d2bd82057ff0e94145e7fa25d5aa5985ed32e0921927f6 + es-define-property: "npm:^1.0.0" + checksum: 10c0/253c1f59e80bb476cf0dde8ff5284505d90c3bdb762983c3514d36414290475fe3fd6f574929d84de2a8eec00d35cf07cb6776205ff32efd7c50719125f00236 languageName: node linkType: hard "has-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "has-proto@npm:1.0.1" - checksum: c8a8fe411f810b23a564bd5546a8f3f0fff6f1b692740eb7a2fdc9df716ef870040806891e2f23ff4653f1083e3895bf12088703dd1a0eac3d9202d3a4768cd0 + version: 1.0.3 + resolution: "has-proto@npm:1.0.3" + checksum: 10c0/35a6989f81e9f8022c2f4027f8b48a552de714938765d019dbea6bb547bd49ce5010a3c7c32ec6ddac6e48fc546166a3583b128f5a7add8b058a6d8b4afec205 languageName: node linkType: hard "has-symbols@npm:^1.0.3": version: 1.0.3 resolution: "has-symbols@npm:1.0.3" - checksum: e6922b4345a3f37069cdfe8600febbca791c94988c01af3394d86ca3360b4b93928bbf395859158f88099cb10b19d98e3bbab7c9ff2c1bd09cf665ee90afa2c3 + checksum: 10c0/e6922b4345a3f37069cdfe8600febbca791c94988c01af3394d86ca3360b4b93928bbf395859158f88099cb10b19d98e3bbab7c9ff2c1bd09cf665ee90afa2c3 languageName: node linkType: hard @@ -903,7 +952,17 @@ __metadata: inherits: "npm:^2.0.4" readable-stream: "npm:^3.6.0" safe-buffer: "npm:^5.2.0" - checksum: 663eabcf4173326fbb65a1918a509045590a26cc7e0964b754eef248d281305c6ec9f6b31cb508d02ffca383ab50028180ce5aefe013e942b44a903ac8dc80d0 + checksum: 10c0/663eabcf4173326fbb65a1918a509045590a26cc7e0964b754eef248d281305c6ec9f6b31cb508d02ffca383ab50028180ce5aefe013e942b44a903ac8dc80d0 + languageName: node + linkType: hard + +"hash-base@npm:~3.0": + version: 3.0.4 + resolution: "hash-base@npm:3.0.4" + dependencies: + inherits: "npm:^2.0.1" + safe-buffer: "npm:^5.0.1" + checksum: 10c0/a13357dccb3827f0bb0b56bf928da85c428dc8670f6e4a1c7265e4f1653ce02d69030b40fd01b0f1d218a995a066eea279cded9cec72d207b593bcdfe309c2f0 languageName: node linkType: hard @@ -913,16 +972,16 @@ __metadata: dependencies: inherits: "npm:^2.0.3" minimalistic-assert: "npm:^1.0.1" - checksum: 41ada59494eac5332cfc1ce6b7ebdd7b88a3864a6d6b08a3ea8ef261332ed60f37f10877e0c825aaa4bddebf164fbffa618286aeeec5296675e2671cbfa746c4 + checksum: 10c0/41ada59494eac5332cfc1ce6b7ebdd7b88a3864a6d6b08a3ea8ef261332ed60f37f10877e0c825aaa4bddebf164fbffa618286aeeec5296675e2671cbfa746c4 languageName: node linkType: hard "hasown@npm:^2.0.0": - version: 2.0.0 - resolution: "hasown@npm:2.0.0" + version: 2.0.2 + resolution: "hasown@npm:2.0.2" dependencies: function-bind: "npm:^1.1.2" - checksum: 5d415b114f410661208c95e7ab4879f1cc2765b8daceff4dc8718317d1cb7b9ffa7c5d1eafd9a4389c9aab7445d6ea88e05f3096cb1e529618b55304956b87fc + checksum: 10c0/3769d434703b8ac66b209a4cca0737519925bbdb61dd887f93a16372b14694c63ff4e797686d87c90f08168e81082248b9b028bad60d4da9e0d1148766f56eb9 languageName: node linkType: hard @@ -933,14 +992,14 @@ __metadata: hash.js: "npm:^1.0.3" minimalistic-assert: "npm:^1.0.0" minimalistic-crypto-utils: "npm:^1.0.1" - checksum: f3d9ba31b40257a573f162176ac5930109816036c59a09f901eb2ffd7e5e705c6832bedfff507957125f2086a0ab8f853c0df225642a88bf1fcaea945f20600d + checksum: 10c0/f3d9ba31b40257a573f162176ac5930109816036c59a09f901eb2ffd7e5e705c6832bedfff507957125f2086a0ab8f853c0df225642a88bf1fcaea945f20600d languageName: node linkType: hard "http-cache-semantics@npm:^4.1.1": version: 4.1.1 resolution: "http-cache-semantics@npm:4.1.1" - checksum: ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc + checksum: 10c0/ce1319b8a382eb3cbb4a37c19f6bfe14e5bb5be3d09079e885e8c513ab2d3cd9214902f8a31c9dc4e37022633ceabfc2d697405deeaf1b8f3552bb4ed996fdfc languageName: node linkType: hard @@ -953,34 +1012,34 @@ __metadata: setprototypeof: "npm:1.2.0" statuses: "npm:2.0.1" toidentifier: "npm:1.0.1" - checksum: fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19 + checksum: 10c0/fc6f2715fe188d091274b5ffc8b3657bd85c63e969daa68ccb77afb05b071a4b62841acb7a21e417b5539014dff2ebf9550f0b14a9ff126f2734a7c1387f8e19 languageName: node linkType: hard "http-proxy-agent@npm:^7.0.0": - version: 7.0.0 - resolution: "http-proxy-agent@npm:7.0.0" + version: 7.0.2 + resolution: "http-proxy-agent@npm:7.0.2" dependencies: agent-base: "npm:^7.1.0" debug: "npm:^4.3.4" - checksum: a11574ff39436cee3c7bc67f259444097b09474605846ddd8edf0bf4ad8644be8533db1aa463426e376865047d05dc22755e638632819317c0c2f1b2196657c8 + checksum: 10c0/4207b06a4580fb85dd6dff521f0abf6db517489e70863dca1a0291daa7f2d3d2d6015a57bd702af068ea5cf9f1f6ff72314f5f5b4228d299c0904135d2aef921 languageName: node linkType: hard "https-browserify@npm:^1.0.0": version: 1.0.0 resolution: "https-browserify@npm:1.0.0" - checksum: e17b6943bc24ea9b9a7da5714645d808670af75a425f29baffc3284962626efdc1eb3aa9bbffaa6e64028a6ad98af5b09fabcb454a8f918fb686abfdc9e9b8ae + checksum: 10c0/e17b6943bc24ea9b9a7da5714645d808670af75a425f29baffc3284962626efdc1eb3aa9bbffaa6e64028a6ad98af5b09fabcb454a8f918fb686abfdc9e9b8ae languageName: node linkType: hard "https-proxy-agent@npm:^7.0.1": - version: 7.0.2 - resolution: "https-proxy-agent@npm:7.0.2" + version: 7.0.4 + resolution: "https-proxy-agent@npm:7.0.4" dependencies: agent-base: "npm:^7.0.2" debug: "npm:4" - checksum: 7735eb90073db087e7e79312e3d97c8c04baf7ea7ca7b013382b6a45abbaa61b281041a98f4e13c8c80d88f843785bcc84ba189165b4b4087b1e3496ba656d77 + checksum: 10c0/bc4f7c38da32a5fc622450b6cb49a24ff596f9bd48dcedb52d2da3fa1c1a80e100fb506bd59b326c012f21c863c69b275c23de1a01d0b84db396822fdf25e52b languageName: node linkType: hard @@ -989,7 +1048,7 @@ __metadata: resolution: "iconv-lite@npm:0.4.24" dependencies: safer-buffer: "npm:>= 2.1.2 < 3" - checksum: c6886a24cc00f2a059767440ec1bc00d334a89f250db8e0f7feb4961c8727118457e27c495ba94d082e51d3baca378726cd110aaf7ded8b9bbfd6a44760cf1d4 + checksum: 10c0/c6886a24cc00f2a059767440ec1bc00d334a89f250db8e0f7feb4961c8727118457e27c495ba94d082e51d3baca378726cd110aaf7ded8b9bbfd6a44760cf1d4 languageName: node linkType: hard @@ -998,65 +1057,55 @@ __metadata: resolution: "iconv-lite@npm:0.6.3" dependencies: safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 + checksum: 10c0/98102bc66b33fcf5ac044099d1257ba0b7ad5e3ccd3221f34dd508ab4070edff183276221684e1e0555b145fce0850c9f7d2b60a9fcac50fbb4ea0d6e845a3b1 languageName: node linkType: hard -"ieee754@npm:^1.1.4": +"ieee754@npm:^1.1.4, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" - checksum: b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb + checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb languageName: node linkType: hard "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" - checksum: 8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 + checksum: 10c0/8b51313850dd33605c6c9d3fd9638b714f4c4c40250cff658209f30d40da60f78992fb2df5dabee4acf589a6a82bbc79ad5486550754bd9ec4e3fc0d4a57d6a6 languageName: node linkType: hard "indent-string@npm:^4.0.0": version: 4.0.0 resolution: "indent-string@npm:4.0.0" - checksum: 1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f + checksum: 10c0/1e1904ddb0cb3d6cce7cd09e27a90184908b7a5d5c21b92e232c93579d314f0b83c246ffb035493d0504b1e9147ba2c9b21df0030f48673fba0496ecd698161f languageName: node linkType: hard "inflation@npm:^2.1.0": version: 2.1.0 resolution: "inflation@npm:2.1.0" - checksum: aadfcb8047a7e00d644e2e195f901dd9d7266c2be2326b7f8f6a99298f14916f1e322d00108a7e2778d6e76a8dc2174ddb9ac14bcdfe4f4866dfd612b695ab5d + checksum: 10c0/aadfcb8047a7e00d644e2e195f901dd9d7266c2be2326b7f8f6a99298f14916f1e322d00108a7e2778d6e76a8dc2174ddb9ac14bcdfe4f4866dfd612b695ab5d languageName: node linkType: hard -"inflight@npm:^1.0.4": - version: 1.0.6 - resolution: "inflight@npm:1.0.6" - dependencies: - once: "npm:^1.3.0" - wrappy: "npm:1" - checksum: 7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 +"inherits@npm:2.0.3": + version: 2.0.3 + resolution: "inherits@npm:2.0.3" + checksum: 10c0/6e56402373149ea076a434072671f9982f5fad030c7662be0332122fe6c0fa490acb3cc1010d90b6eff8d640b1167d77674add52dfd1bb85d545cf29e80e73e7 languageName: node linkType: hard -"inherits@npm:2, inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3": +"inherits@npm:2.0.4, inherits@npm:^2.0.1, inherits@npm:^2.0.3, inherits@npm:^2.0.4, inherits@npm:~2.0.1, inherits@npm:~2.0.3": version: 2.0.4 resolution: "inherits@npm:2.0.4" - checksum: 4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 - languageName: node - linkType: hard - -"inherits@npm:2.0.3": - version: 2.0.3 - resolution: "inherits@npm:2.0.3" - checksum: 6e56402373149ea076a434072671f9982f5fad030c7662be0332122fe6c0fa490acb3cc1010d90b6eff8d640b1167d77674add52dfd1bb85d545cf29e80e73e7 + checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 languageName: node linkType: hard -"ioredis@npm:^5.3.2": - version: 5.3.2 - resolution: "ioredis@npm:5.3.2" +"ioredis@npm:^5.4.1": + version: 5.4.1 + resolution: "ioredis@npm:5.4.1" dependencies: "@ioredis/commands": "npm:^1.1.1" cluster-key-slot: "npm:^1.1.0" @@ -1067,62 +1116,79 @@ __metadata: redis-errors: "npm:^1.2.0" redis-parser: "npm:^3.0.0" standard-as-callback: "npm:^2.1.0" - checksum: 0dd2b5b8004e891f5b62edf18ac223194f1f5204698ec827c903e789ea05b0b36f73395491749ec63c66470485bdfb228ccdf1714fbf631a0f78f33211f2c883 + checksum: 10c0/5d28b7c89a3cab5b76d75923d7d4ce79172b3a1ca9be690133f6e8e393a7a4b4ffd55513e618bbb5504fed80d9e1395c9d9531a7c5c5c84aa4c4e765cca75456 languageName: node linkType: hard -"ip@npm:^2.0.0": - version: 2.0.0 - resolution: "ip@npm:2.0.0" - checksum: 8d186cc5585f57372847ae29b6eba258c68862055e18a75cc4933327232cb5c107f89800ce29715d542eef2c254fbb68b382e780a7414f9ee7caf60b7a473958 +"ip-address@npm:^9.0.5": + version: 9.0.5 + resolution: "ip-address@npm:9.0.5" + dependencies: + jsbn: "npm:1.1.0" + sprintf-js: "npm:^1.1.3" + checksum: 10c0/331cd07fafcb3b24100613e4b53e1a2b4feab11e671e655d46dc09ee233da5011284d09ca40c4ecbdfe1d0004f462958675c224a804259f2f78d2465a87824bc languageName: node linkType: hard "is-fullwidth-code-point@npm:^3.0.0": version: 3.0.0 resolution: "is-fullwidth-code-point@npm:3.0.0" - checksum: bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc + checksum: 10c0/bb11d825e049f38e04c06373a8d72782eee0205bda9d908cc550ccb3c59b99d750ff9537982e01733c1c94a58e35400661f57042158ff5e8f3e90cf936daf0fc languageName: node linkType: hard "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" - checksum: 85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + checksum: 10c0/85fee098ae62ba6f1e24cf22678805473c7afd0fb3978a3aa260e354cb7bcb3a5806cf0a98403188465efedec41ab4348e8e4e79305d409601323855b3839d4d + languageName: node + linkType: hard + +"is-stream@npm:^2.0.1": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5 languageName: node linkType: hard "isarray@npm:^1.0.0, isarray@npm:~1.0.0": version: 1.0.0 resolution: "isarray@npm:1.0.0" - checksum: 18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d + checksum: 10c0/18b5be6669be53425f0b84098732670ed4e727e3af33bc7f948aac01782110eb9a18b3b329c5323bcdd3acdaae547ee077d3951317e7f133bff7105264b3003d languageName: node linkType: hard "isexe@npm:^2.0.0": version: 2.0.0 resolution: "isexe@npm:2.0.0" - checksum: 228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d + checksum: 10c0/228cfa503fadc2c31596ab06ed6aa82c9976eec2bfd83397e7eaf06d0ccf42cd1dfd6743bf9aeb01aebd4156d009994c5f76ea898d2832c1fe342da923ca457d languageName: node linkType: hard "isexe@npm:^3.1.1": version: 3.1.1 resolution: "isexe@npm:3.1.1" - checksum: 9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 + checksum: 10c0/9ec257654093443eb0a528a9c8cbba9c0ca7616ccb40abd6dde7202734d96bb86e4ac0d764f0f8cd965856aacbff2f4ce23e730dc19dfb41e3b0d865ca6fdcc7 languageName: node linkType: hard -"jackspeak@npm:^2.3.5": - version: 2.3.6 - resolution: "jackspeak@npm:2.3.6" +"jackspeak@npm:^3.1.2": + version: 3.1.2 + resolution: "jackspeak@npm:3.1.2" dependencies: "@isaacs/cliui": "npm:^8.0.2" "@pkgjs/parseargs": "npm:^0.11.0" dependenciesMeta: "@pkgjs/parseargs": optional: true - checksum: f01d8f972d894cd7638bc338e9ef5ddb86f7b208ce177a36d718eac96ec86638a6efa17d0221b10073e64b45edc2ce15340db9380b1f5d5c5d000cbc517dc111 + checksum: 10c0/5f1922a1ca0f19869e23f0dc4374c60d36e922f7926c76fecf8080cc6f7f798d6a9caac1b9428327d14c67731fd551bb3454cb270a5e13a0718f3b3660ec3d5d + languageName: node + linkType: hard + +"jsbn@npm:1.1.0": + version: 1.1.0 + resolution: "jsbn@npm:1.1.0" + checksum: 10c0/4f907fb78d7b712e11dea8c165fe0921f81a657d3443dde75359ed52eb2b5d33ce6773d97985a089f09a65edd80b11cb75c767b57ba47391fee4c969f7215c96 languageName: node linkType: hard @@ -1131,7 +1197,7 @@ __metadata: resolution: "keygrip@npm:1.1.0" dependencies: tsscmp: "npm:1.0.6" - checksum: 2aceec1a1e642a0caf938044056ed67b1909cfe67a93a59b32aae2863e0f35a1a53782ecc8f9cd0e3bdb60863fa0f401ccbd257cd7dfae61915f78445139edea + checksum: 10c0/2aceec1a1e642a0caf938044056ed67b1909cfe67a93a59b32aae2863e0f35a1a53782ecc8f9cd0e3bdb60863fa0f401ccbd257cd7dfae61915f78445139edea languageName: node linkType: hard @@ -1140,57 +1206,48 @@ __metadata: resolution: "lazystream@npm:1.0.1" dependencies: readable-stream: "npm:^2.0.5" - checksum: ea4e509a5226ecfcc303ba6782cc269be8867d372b9bcbd625c88955df1987ea1a20da4643bf9270336415a398d33531ebf0d5f0d393b9283dc7c98bfcbd7b69 + checksum: 10c0/ea4e509a5226ecfcc303ba6782cc269be8867d372b9bcbd625c88955df1987ea1a20da4643bf9270336415a398d33531ebf0d5f0d393b9283dc7c98bfcbd7b69 languageName: node linkType: hard "lodash.defaults@npm:^4.2.0": version: 4.2.0 resolution: "lodash.defaults@npm:4.2.0" - checksum: d5b77aeb702caa69b17be1358faece33a84497bcca814897383c58b28a2f8dfc381b1d9edbec239f8b425126a3bbe4916223da2a576bb0411c2cefd67df80707 + checksum: 10c0/d5b77aeb702caa69b17be1358faece33a84497bcca814897383c58b28a2f8dfc381b1d9edbec239f8b425126a3bbe4916223da2a576bb0411c2cefd67df80707 languageName: node linkType: hard "lodash.isarguments@npm:^3.1.0": version: 3.1.0 resolution: "lodash.isarguments@npm:3.1.0" - checksum: 5e8f95ba10975900a3920fb039a3f89a5a79359a1b5565e4e5b4310ed6ebe64011e31d402e34f577eca983a1fc01ff86c926e3cbe602e1ddfc858fdd353e62d8 + checksum: 10c0/5e8f95ba10975900a3920fb039a3f89a5a79359a1b5565e4e5b4310ed6ebe64011e31d402e34f577eca983a1fc01ff86c926e3cbe602e1ddfc858fdd353e62d8 languageName: node linkType: hard "lodash@npm:^4.17.15": version: 4.17.21 resolution: "lodash@npm:4.17.21" - checksum: d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c - languageName: node - linkType: hard - -"lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.1.0 - resolution: "lru-cache@npm:10.1.0" - checksum: 778bc8b2626daccd75f24c4b4d10632496e21ba064b126f526c626fbdbc5b28c472013fccd45d7646b9e1ef052444824854aed617b59cd570d01a8b7d651fc1e + checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c languageName: node linkType: hard -"lru-cache@npm:^6.0.0": - version: 6.0.0 - resolution: "lru-cache@npm:6.0.0" - dependencies: - yallist: "npm:^4.0.0" - checksum: cb53e582785c48187d7a188d3379c181b5ca2a9c78d2bce3e7dee36f32761d1c42983da3fe12b55cb74e1779fa94cdc2e5367c028a9b35317184ede0c07a30a9 +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.2.2 + resolution: "lru-cache@npm:10.2.2" + checksum: 10c0/402d31094335851220d0b00985084288136136992979d0e015f0f1697e15d1c86052d7d53ae86b614e5b058425606efffc6969a31a091085d7a2b80a8a1e26d6 languageName: node linkType: hard "luxon@npm:^3.4.4": version: 3.4.4 resolution: "luxon@npm:3.4.4" - checksum: 02e26a0b039c11fd5b75e1d734c8f0332c95510f6a514a9a0991023e43fb233884da02d7f966823ffb230632a733fc86d4a4b1e63c3fbe00058b8ee0f8c728af + checksum: 10c0/02e26a0b039c11fd5b75e1d734c8f0332c95510f6a514a9a0991023e43fb233884da02d7f966823ffb230632a733fc86d4a4b1e63c3fbe00058b8ee0f8c728af languageName: node linkType: hard "make-fetch-happen@npm:^13.0.0": - version: 13.0.0 - resolution: "make-fetch-happen@npm:13.0.0" + version: 13.0.1 + resolution: "make-fetch-happen@npm:13.0.1" dependencies: "@npmcli/agent": "npm:^2.0.0" cacache: "npm:^18.0.0" @@ -1201,9 +1258,10 @@ __metadata: minipass-flush: "npm:^1.0.5" minipass-pipeline: "npm:^1.2.4" negotiator: "npm:^0.6.3" + proc-log: "npm:^4.2.0" promise-retry: "npm:^2.0.1" ssri: "npm:^10.0.0" - checksum: 43b9f6dcbc6fe8b8604cb6396957c3698857a15ba4dbc38284f7f0e61f248300585ef1eb8cc62df54e9c724af977e45b5cdfd88320ef7f53e45070ed3488da55 + checksum: 10c0/df5f4dbb6d98153b751bccf4dc4cc500de85a96a9331db9805596c46aa9f99d9555983954e6c1266d9f981ae37a9e4647f42b9a4bb5466f867f4012e582c9e7e languageName: node linkType: hard @@ -1214,7 +1272,7 @@ __metadata: hash-base: "npm:^3.0.0" inherits: "npm:^2.0.1" safe-buffer: "npm:^5.1.2" - checksum: b7bd75077f419c8e013fc4d4dada48be71882e37d69a44af65a2f2804b91e253441eb43a0614423a1c91bb830b8140b0dc906bc797245e2e275759584f4efcc5 + checksum: 10c0/b7bd75077f419c8e013fc4d4dada48be71882e37d69a44af65a2f2804b91e253441eb43a0614423a1c91bb830b8140b0dc906bc797245e2e275759584f4efcc5 languageName: node linkType: hard @@ -1226,39 +1284,39 @@ __metadata: brorand: "npm:^1.0.1" bin: miller-rabin: bin/miller-rabin - checksum: 26b2b96f6e49dbcff7faebb78708ed2f5f9ae27ac8cbbf1d7c08f83cf39bed3d418c0c11034dce997da70d135cc0ff6f3a4c15dc452f8e114c11986388a64346 + checksum: 10c0/26b2b96f6e49dbcff7faebb78708ed2f5f9ae27ac8cbbf1d7c08f83cf39bed3d418c0c11034dce997da70d135cc0ff6f3a4c15dc452f8e114c11986388a64346 languageName: node linkType: hard "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" - checksum: 96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd + checksum: 10c0/96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd languageName: node linkType: hard "minimalistic-crypto-utils@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-crypto-utils@npm:1.0.1" - checksum: 790ecec8c5c73973a4fbf2c663d911033e8494d5fb0960a4500634766ab05d6107d20af896ca2132e7031741f19888154d44b2408ada0852446705441383e9f8 + checksum: 10c0/790ecec8c5c73973a4fbf2c663d911033e8494d5fb0960a4500634766ab05d6107d20af896ca2132e7031741f19888154d44b2408ada0852446705441383e9f8 languageName: node linkType: hard -"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": +"minimatch@npm:^5.1.0": version: 5.1.6 resolution: "minimatch@npm:5.1.6" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 3defdfd230914f22a8da203747c42ee3c405c39d4d37ffda284dac5e45b7e1f6c49aa8be606509002898e73091ff2a3bbfc59c2c6c71d4660609f63aa92f98e3 + checksum: 10c0/3defdfd230914f22a8da203747c42ee3c405c39d4d37ffda284dac5e45b7e1f6c49aa8be606509002898e73091ff2a3bbfc59c2c6c71d4660609f63aa92f98e3 languageName: node linkType: hard -"minimatch@npm:^9.0.1": - version: 9.0.3 - resolution: "minimatch@npm:9.0.3" +"minimatch@npm:^9.0.1, minimatch@npm:^9.0.4": + version: 9.0.4 + resolution: "minimatch@npm:9.0.4" dependencies: brace-expansion: "npm:^2.0.1" - checksum: 85f407dcd38ac3e180f425e86553911d101455ca3ad5544d6a7cec16286657e4f8a9aa6695803025c55e31e35a91a2252b5dc8e7d527211278b8b65b4dbd5eac + checksum: 10c0/2c16f21f50e64922864e560ff97c587d15fd491f65d92a677a344e970fe62aafdbeafe648965fa96d33c061b4d0eabfe0213466203dd793367e7f28658cf6414 languageName: node linkType: hard @@ -1267,13 +1325,13 @@ __metadata: resolution: "minipass-collect@npm:2.0.1" dependencies: minipass: "npm:^7.0.3" - checksum: 5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e + checksum: 10c0/5167e73f62bb74cc5019594709c77e6a742051a647fe9499abf03c71dca75515b7959d67a764bdc4f8b361cf897fbf25e2d9869ee039203ed45240f48b9aa06e languageName: node linkType: hard "minipass-fetch@npm:^3.0.0": - version: 3.0.4 - resolution: "minipass-fetch@npm:3.0.4" + version: 3.0.5 + resolution: "minipass-fetch@npm:3.0.5" dependencies: encoding: "npm:^0.1.13" minipass: "npm:^7.0.3" @@ -1282,7 +1340,7 @@ __metadata: dependenciesMeta: encoding: optional: true - checksum: 1b63c1f3313e88eeac4689f1b71c9f086598db9a189400e3ee960c32ed89e06737fa23976c9305c2d57464fb3fcdc12749d3378805c9d6176f5569b0d0ee8a75 + checksum: 10c0/9d702d57f556274286fdd97e406fc38a2f5c8d15e158b498d7393b1105974b21249289ec571fa2b51e038a4872bfc82710111cf75fae98c662f3d6f95e72152b languageName: node linkType: hard @@ -1291,7 +1349,7 @@ __metadata: resolution: "minipass-flush@npm:1.0.5" dependencies: minipass: "npm:^3.0.0" - checksum: 2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd + checksum: 10c0/2a51b63feb799d2bb34669205eee7c0eaf9dce01883261a5b77410c9408aa447e478efd191b4de6fc1101e796ff5892f8443ef20d9544385819093dbb32d36bd languageName: node linkType: hard @@ -1300,7 +1358,7 @@ __metadata: resolution: "minipass-pipeline@npm:1.2.4" dependencies: minipass: "npm:^3.0.0" - checksum: cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 + checksum: 10c0/cbda57cea20b140b797505dc2cac71581a70b3247b84480c1fed5ca5ba46c25ecc25f68bfc9e6dcb1a6e9017dab5c7ada5eab73ad4f0a49d84e35093e0c643f2 languageName: node linkType: hard @@ -1309,7 +1367,7 @@ __metadata: resolution: "minipass-sized@npm:1.0.3" dependencies: minipass: "npm:^3.0.0" - checksum: 298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb + checksum: 10c0/298f124753efdc745cfe0f2bdfdd81ba25b9f4e753ca4a2066eb17c821f25d48acea607dfc997633ee5bf7b6dfffb4eee4f2051eb168663f0b99fad2fa4829cb languageName: node linkType: hard @@ -1318,21 +1376,28 @@ __metadata: resolution: "minipass@npm:3.3.6" dependencies: yallist: "npm:^4.0.0" - checksum: a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c + checksum: 10c0/a114746943afa1dbbca8249e706d1d38b85ed1298b530f5808ce51f8e9e941962e2a5ad2e00eae7dd21d8a4aae6586a66d4216d1a259385e9d0358f0c1eba16c languageName: node linkType: hard "minipass@npm:^5.0.0": version: 5.0.0 resolution: "minipass@npm:5.0.0" - checksum: a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 + checksum: 10c0/a91d8043f691796a8ac88df039da19933ef0f633e3d7f0d35dcd5373af49131cf2399bfc355f41515dc495e3990369c3858cd319e5c2722b4753c90bf3152462 languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": - version: 7.0.4 - resolution: "minipass@npm:7.0.4" - checksum: 6c7370a6dfd257bf18222da581ba89a5eaedca10e158781232a8b5542a90547540b4b9b7e7f490e4cda43acfbd12e086f0453728ecf8c19e0ef6921bc5958ac5 +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4": + version: 7.1.1 + resolution: "minipass@npm:7.1.1" + checksum: 10c0/fdccc2f99c31083f45f881fd1e6971d798e333e078ab3c8988fb818c470fbd5e935388ad9adb286397eba50baebf46ef8ff487c8d3f455a69c6f3efc327bdff9 + languageName: node + linkType: hard + +"minipass@npm:^7.1.2": + version: 7.1.2 + resolution: "minipass@npm:7.1.2" + checksum: 10c0/b0fd20bb9fb56e5fa9a8bfac539e8915ae07430a619e4b86ff71f5fc757ef3924b23b2c4230393af1eda647ed3d75739e4e0acb250a6b1eb277cf7f8fe449557 languageName: node linkType: hard @@ -1342,7 +1407,7 @@ __metadata: dependencies: minipass: "npm:^3.0.0" yallist: "npm:^4.0.0" - checksum: 64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 + checksum: 10c0/64fae024e1a7d0346a1102bb670085b17b7f95bf6cfdf5b128772ec8faf9ea211464ea4add406a3a6384a7d87a0cd1a96263692134323477b4fb43659a6cab78 languageName: node linkType: hard @@ -1351,27 +1416,27 @@ __metadata: resolution: "mkdirp@npm:1.0.4" bin: mkdirp: bin/cmd.js - checksum: 46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf + checksum: 10c0/46ea0f3ffa8bc6a5bc0c7081ffc3907777f0ed6516888d40a518c5111f8366d97d2678911ad1a6882bf592fa9de6c784fea32e1687bb94e1f4944170af48a5cf languageName: node linkType: hard "ms@npm:2.1.2": version: 2.1.2 resolution: "ms@npm:2.1.2" - checksum: a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc + checksum: 10c0/a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc languageName: node linkType: hard "negotiator@npm:^0.6.3": version: 0.6.3 resolution: "negotiator@npm:0.6.3" - checksum: 3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 + checksum: 10c0/3ec9fd413e7bf071c937ae60d572bc67155262068ed522cf4b3be5edbe6ddf67d095ec03a3a14ebf8fc8e95f8e1d61be4869db0dbb0de696f6b837358bd43fc2 languageName: node linkType: hard "node-gyp@npm:latest": - version: 10.0.1 - resolution: "node-gyp@npm:10.0.1" + version: 10.1.0 + resolution: "node-gyp@npm:10.1.0" dependencies: env-paths: "npm:^2.2.0" exponential-backoff: "npm:^3.1.1" @@ -1385,7 +1450,7 @@ __metadata: which: "npm:^4.0.0" bin: node-gyp: bin/node-gyp.js - checksum: abddfff7d873312e4ed4a5fb75ce893a5c4fb69e7fcb1dfa71c28a6b92a7f1ef6b62790dffb39181b5a82728ba8f2f32d229cf8cbe66769fe02cea7db4a555aa + checksum: 10c0/9cc821111ca244a01fb7f054db7523ab0a0cd837f665267eb962eb87695d71fb1e681f9e21464cc2fd7c05530dc4c81b810bca1a88f7d7186909b74477491a3c languageName: node linkType: hard @@ -1416,67 +1481,58 @@ __metadata: url: "npm:^0.11.0" util: "npm:^0.11.0" vm-browserify: "npm:^1.0.1" - checksum: 0e05321a6396408903ed642231d2bca7dd96492d074c7af161ba06a63c95378bd3de50b4105eccbbc02d93ba3da69f0ff5e624bc2a8c92ca462ceb6a403e7986 + checksum: 10c0/0e05321a6396408903ed642231d2bca7dd96492d074c7af161ba06a63c95378bd3de50b4105eccbbc02d93ba3da69f0ff5e624bc2a8c92ca462ceb6a403e7986 languageName: node linkType: hard "nopt@npm:^7.0.0": - version: 7.2.0 - resolution: "nopt@npm:7.2.0" + version: 7.2.1 + resolution: "nopt@npm:7.2.1" dependencies: abbrev: "npm:^2.0.0" bin: nopt: bin/nopt.js - checksum: 9bd7198df6f16eb29ff16892c77bcf7f0cc41f9fb5c26280ac0def2cf8cf319f3b821b3af83eba0e74c85807cc430a16efe0db58fe6ae1f41e69519f585b6aff + checksum: 10c0/a069c7c736767121242037a22a788863accfa932ab285a1eb569eb8cd534b09d17206f68c37f096ae785647435e0c5a5a0a67b42ec743e481a455e5ae6a6df81 languageName: node linkType: hard "normalize-path@npm:^3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" - checksum: e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 + checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 languageName: node linkType: hard -"object-inspect@npm:^1.9.0": +"object-inspect@npm:^1.13.1": version: 1.13.1 resolution: "object-inspect@npm:1.13.1" - checksum: fad603f408e345c82e946abdf4bfd774260a5ed3e5997a0b057c44153ac32c7271ff19e3a5ae39c858da683ba045ccac2f65245c12763ce4e8594f818f4a648d + checksum: 10c0/fad603f408e345c82e946abdf4bfd774260a5ed3e5997a0b057c44153ac32c7271ff19e3a5ae39c858da683ba045ccac2f65245c12763ce4e8594f818f4a648d languageName: node linkType: hard "object-keys@npm:^1.1.1": version: 1.1.1 resolution: "object-keys@npm:1.1.1" - checksum: b11f7ccdbc6d406d1f186cdadb9d54738e347b2692a14439ca5ac70c225fa6db46db809711b78589866d47b25fc3e8dee0b4c722ac751e11180f9380e3d8601d + checksum: 10c0/b11f7ccdbc6d406d1f186cdadb9d54738e347b2692a14439ca5ac70c225fa6db46db809711b78589866d47b25fc3e8dee0b4c722ac751e11180f9380e3d8601d languageName: node linkType: hard "object.assign@npm:^4.1.4": - version: 4.1.4 - resolution: "object.assign@npm:4.1.4" + version: 4.1.5 + resolution: "object.assign@npm:4.1.5" dependencies: - call-bind: "npm:^1.0.2" - define-properties: "npm:^1.1.4" + call-bind: "npm:^1.0.5" + define-properties: "npm:^1.2.1" has-symbols: "npm:^1.0.3" object-keys: "npm:^1.1.1" - checksum: 2f286118c023e557757620e647b02e7c88d3d417e0c568fca0820de8ec9cca68928304854d5b03e99763eddad6e78a6716e2930f7e6372e4b9b843f3fd3056f3 - languageName: node - linkType: hard - -"once@npm:^1.3.0": - version: 1.4.0 - resolution: "once@npm:1.4.0" - dependencies: - wrappy: "npm:1" - checksum: 5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 + checksum: 10c0/60108e1fa2706f22554a4648299b0955236c62b3685c52abf4988d14fffb0e7731e00aa8c6448397e3eb63d087dcc124a9f21e1980f36d0b2667f3c18bacd469 languageName: node linkType: hard "os-browserify@npm:^0.3.0": version: 0.3.0 resolution: "os-browserify@npm:0.3.0" - checksum: 6ff32cb1efe2bc6930ad0fd4c50e30c38010aee909eba8d65be60af55efd6cbb48f0287e3649b4e3f3a63dce5a667b23c187c4293a75e557f0d5489d735bcf52 + checksum: 10c0/6ff32cb1efe2bc6930ad0fd4c50e30c38010aee909eba8d65be60af55efd6cbb48f0287e3649b4e3f3a63dce5a667b23c187c4293a75e557f0d5489d735bcf52 languageName: node linkType: hard @@ -1485,55 +1541,56 @@ __metadata: resolution: "p-map@npm:4.0.0" dependencies: aggregate-error: "npm:^3.0.0" - checksum: 592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 + checksum: 10c0/592c05bd6262c466ce269ff172bb8de7c6975afca9b50c975135b974e9bdaafbfe80e61aaaf5be6d1200ba08b30ead04b88cfa7e25ff1e3b93ab28c9f62a2c75 languageName: node linkType: hard "pako@npm:~1.0.5": version: 1.0.11 resolution: "pako@npm:1.0.11" - checksum: 86dd99d8b34c3930345b8bbeb5e1cd8a05f608eeb40967b293f72fe469d0e9c88b783a8777e4cc7dc7c91ce54c5e93d88ff4b4f060e6ff18408fd21030d9ffbe + checksum: 10c0/86dd99d8b34c3930345b8bbeb5e1cd8a05f608eeb40967b293f72fe469d0e9c88b783a8777e4cc7dc7c91ce54c5e93d88ff4b4f060e6ff18408fd21030d9ffbe languageName: node linkType: hard -"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.6": - version: 5.1.6 - resolution: "parse-asn1@npm:5.1.6" +"parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.7": + version: 5.1.7 + resolution: "parse-asn1@npm:5.1.7" dependencies: - asn1.js: "npm:^5.2.0" - browserify-aes: "npm:^1.0.0" - evp_bytestokey: "npm:^1.0.0" - pbkdf2: "npm:^3.0.3" - safe-buffer: "npm:^5.1.1" - checksum: 4ed1d9b9e120c5484d29d67bb90171aac0b73422bc016d6294160aea983275c28a27ab85d862059a36a86a97dd31b7ddd97486802ca9fac67115fe3409e9dcbd + asn1.js: "npm:^4.10.1" + browserify-aes: "npm:^1.2.0" + evp_bytestokey: "npm:^1.0.3" + hash-base: "npm:~3.0" + pbkdf2: "npm:^3.1.2" + safe-buffer: "npm:^5.2.1" + checksum: 10c0/05eb5937405c904eb5a7f3633bab1acc11f4ae3478a07ef5c6d81ce88c3c0e505ff51f9c7b935ebc1265c868343793698fc91025755a895d0276f620f95e8a82 languageName: node linkType: hard "path-browserify@npm:0.0.1": version: 0.0.1 resolution: "path-browserify@npm:0.0.1" - checksum: 3d59710cddeea06509d91935196185900f3d9d29376dff68ff0e146fbd41d0fb304e983d0158f30cabe4dd2ffcc6a7d3d977631994ee984c88e66aed50a1ccd3 + checksum: 10c0/3d59710cddeea06509d91935196185900f3d9d29376dff68ff0e146fbd41d0fb304e983d0158f30cabe4dd2ffcc6a7d3d977631994ee984c88e66aed50a1ccd3 languageName: node linkType: hard "path-key@npm:^3.1.0": version: 3.1.1 resolution: "path-key@npm:3.1.1" - checksum: 748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c + checksum: 10c0/748c43efd5a569c039d7a00a03b58eecd1d75f3999f5a28303d75f521288df4823bc057d8784eb72358b2895a05f29a070bc9f1f17d28226cc4e62494cc58c4c languageName: node linkType: hard -"path-scurry@npm:^1.10.1": - version: 1.10.1 - resolution: "path-scurry@npm:1.10.1" +"path-scurry@npm:^1.11.0, path-scurry@npm:^1.11.1": + version: 1.11.1 + resolution: "path-scurry@npm:1.11.1" dependencies: - lru-cache: "npm:^9.1.1 || ^10.0.0" + lru-cache: "npm:^10.2.0" minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: e5dc78a7348d25eec61ab166317e9e9c7b46818aa2c2b9006c507a6ff48c672d011292d9662527213e558f5652ce0afcc788663a061d8b59ab495681840c0c1e + checksum: 10c0/32a13711a2a505616ae1cc1b5076801e453e7aae6ac40ab55b388bb91b9d0547a52f5aaceff710ea400205f18691120d4431e520afbe4266b836fadede15872d languageName: node linkType: hard -"pbkdf2@npm:^3.0.3": +"pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.1.2": version: 3.1.2 resolution: "pbkdf2@npm:3.1.2" dependencies: @@ -1542,52 +1599,59 @@ __metadata: ripemd160: "npm:^2.0.1" safe-buffer: "npm:^5.0.1" sha.js: "npm:^2.4.8" - checksum: 5a30374e87d33fa080a92734d778cf172542cc7e41b96198c4c88763997b62d7850de3fbda5c3111ddf79805ee7c1da7046881c90ac4920b5e324204518b05fd + checksum: 10c0/5a30374e87d33fa080a92734d778cf172542cc7e41b96198c4c88763997b62d7850de3fbda5c3111ddf79805ee7c1da7046881c90ac4920b5e324204518b05fd languageName: node linkType: hard -"playwright-core@npm:1.40.1": - version: 1.40.1 - resolution: "playwright-core@npm:1.40.1" +"playwright-core@npm:1.44.1": + version: 1.44.1 + resolution: "playwright-core@npm:1.44.1" bin: playwright-core: cli.js - checksum: 56c283012974982313a6ae583b975ee4af76d52059fb9a25d9cc616a11224685ec64682b391910c795d2b12d2ab5c7eec31124722c75c0b1703a76ac9b6fd1c2 + checksum: 10c0/6ffa3a04822b3df86d7f47a97e4f20318c0c50868ba4311820e6626ecadaab1424fbd0a3d01f0b4228adc0c781115e44b801742a4970b88739f804d82f142d68 languageName: node linkType: hard -"playwright@npm:^1.40.1": - version: 1.40.1 - resolution: "playwright@npm:1.40.1" +"playwright@npm:^1.44.1": + version: 1.44.1 + resolution: "playwright@npm:1.44.1" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.40.1" + playwright-core: "npm:1.44.1" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 5dae164d1f69162da8d7eee52da651296fb885c76a8b36049f216975c751a0a826ff05795a1c0902dc0bd193fe606ae17d5def655f4cbcccb8d8b71afb74b950 + checksum: 10c0/de827d17746b18ae2ec67d510a640d8ceebf8ee8e3d8399bccffa83b76a967498ca377777e4e6a1daaef4b3c86cb2c44c7468de53d2d915acc61b3b89c032738 languageName: node linkType: hard "proc-log@npm:^3.0.0": version: 3.0.0 resolution: "proc-log@npm:3.0.0" - checksum: f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + checksum: 10c0/f66430e4ff947dbb996058f6fd22de2c66612ae1a89b097744e17fb18a4e8e7a86db99eda52ccf15e53f00b63f4ec0b0911581ff2aac0355b625c8eac509b0dc + languageName: node + linkType: hard + +"proc-log@npm:^4.2.0": + version: 4.2.0 + resolution: "proc-log@npm:4.2.0" + checksum: 10c0/17db4757c2a5c44c1e545170e6c70a26f7de58feb985091fb1763f5081cab3d01b181fb2dd240c9f4a4255a1d9227d163d5771b7e69c9e49a561692db865efb9 languageName: node linkType: hard "process-nextick-args@npm:~2.0.0": version: 2.0.1 resolution: "process-nextick-args@npm:2.0.1" - checksum: bec089239487833d46b59d80327a1605e1c5287eaad770a291add7f45fda1bb5e28b38e0e061add0a1d0ee0984788ce74fa394d345eed1c420cacf392c554367 + checksum: 10c0/bec089239487833d46b59d80327a1605e1c5287eaad770a291add7f45fda1bb5e28b38e0e061add0a1d0ee0984788ce74fa394d345eed1c420cacf392c554367 languageName: node linkType: hard "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" - checksum: 40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3 + checksum: 10c0/40c3ce4b7e6d4b8c3355479df77aeed46f81b279818ccdc500124e6a5ab882c0cc81ff7ea16384873a95a74c4570b01b120f287abbdd4c877931460eca6084b3 languageName: node linkType: hard @@ -1597,7 +1661,7 @@ __metadata: dependencies: err-code: "npm:^2.0.2" retry: "npm:^0.12.0" - checksum: 9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 + checksum: 10c0/9c7045a1a2928094b5b9b15336dcd2a7b1c052f674550df63cc3f36cd44028e5080448175b6f6ca32b642de81150f5e7b1a98b728f15cb069f2dd60ac2616b96 languageName: node linkType: hard @@ -1611,37 +1675,37 @@ __metadata: parse-asn1: "npm:^5.0.0" randombytes: "npm:^2.0.1" safe-buffer: "npm:^5.1.2" - checksum: 6c2cc19fbb554449e47f2175065d6b32f828f9b3badbee4c76585ac28ae8641aafb9bb107afc430c33c5edd6b05dbe318df4f7d6d7712b1093407b11c4280700 + checksum: 10c0/6c2cc19fbb554449e47f2175065d6b32f828f9b3badbee4c76585ac28ae8641aafb9bb107afc430c33c5edd6b05dbe318df4f7d6d7712b1093407b11c4280700 languageName: node linkType: hard "punycode@npm:^1.2.4, punycode@npm:^1.4.1": version: 1.4.1 resolution: "punycode@npm:1.4.1" - checksum: 354b743320518aef36f77013be6e15da4db24c2b4f62c5f1eb0529a6ed02fbaf1cb52925785f6ab85a962f2b590d9cd5ad730b70da72b5f180e2556b8bd3ca08 + checksum: 10c0/354b743320518aef36f77013be6e15da4db24c2b4f62c5f1eb0529a6ed02fbaf1cb52925785f6ab85a962f2b590d9cd5ad730b70da72b5f180e2556b8bd3ca08 languageName: node linkType: hard "qs@npm:^6.11.2": - version: 6.11.2 - resolution: "qs@npm:6.11.2" + version: 6.12.1 + resolution: "qs@npm:6.12.1" dependencies: - side-channel: "npm:^1.0.4" - checksum: 4f95d4ff18ed480befcafa3390022817ffd3087fc65f146cceb40fc5edb9fa96cb31f648cae2fa96ca23818f0798bd63ad4ca369a0e22702fcd41379b3ab6571 + side-channel: "npm:^1.0.6" + checksum: 10c0/439e6d7c6583e7c69f2cab2c39c55b97db7ce576e4c7c469082b938b7fc8746e8d547baacb69b4cd2b6666484776c3f4840ad7163a4c5326300b0afa0acdd84b languageName: node linkType: hard "querystring-es3@npm:^0.2.0": version: 0.2.1 resolution: "querystring-es3@npm:0.2.1" - checksum: 476938c1adb45c141f024fccd2ffd919a3746e79ed444d00e670aad68532977b793889648980e7ca7ff5ffc7bfece623118d0fbadcaf217495eeb7059ae51580 + checksum: 10c0/476938c1adb45c141f024fccd2ffd919a3746e79ed444d00e670aad68532977b793889648980e7ca7ff5ffc7bfece623118d0fbadcaf217495eeb7059ae51580 languageName: node linkType: hard "queue-tick@npm:^1.0.1": version: 1.0.1 resolution: "queue-tick@npm:1.0.1" - checksum: 0db998e2c9b15215317dbcf801e9b23e6bcde4044e115155dae34f8e7454b9a783f737c9a725528d677b7a66c775eb7a955cf144fe0b87f62b575ce5bfd515a9 + checksum: 10c0/0db998e2c9b15215317dbcf801e9b23e6bcde4044e115155dae34f8e7454b9a783f737c9a725528d677b7a66c775eb7a955cf144fe0b87f62b575ce5bfd515a9 languageName: node linkType: hard @@ -1650,7 +1714,7 @@ __metadata: resolution: "randombytes@npm:2.1.0" dependencies: safe-buffer: "npm:^5.1.0" - checksum: 50395efda7a8c94f5dffab564f9ff89736064d32addf0cc7e8bf5e4166f09f8ded7a0849ca6c2d2a59478f7d90f78f20d8048bca3cdf8be09d8e8a10790388f3 + checksum: 10c0/50395efda7a8c94f5dffab564f9ff89736064d32addf0cc7e8bf5e4166f09f8ded7a0849ca6c2d2a59478f7d90f78f20d8048bca3cdf8be09d8e8a10790388f3 languageName: node linkType: hard @@ -1660,7 +1724,7 @@ __metadata: dependencies: randombytes: "npm:^2.0.5" safe-buffer: "npm:^5.1.0" - checksum: 11aeed35515872e8f8a2edec306734e6b74c39c46653607f03c68385ab8030e2adcc4215f76b5e4598e028c4750d820afd5c65202527d831d2a5f207fe2bc87c + checksum: 10c0/11aeed35515872e8f8a2edec306734e6b74c39c46653607f03c68385ab8030e2adcc4215f76b5e4598e028c4750d820afd5c65202527d831d2a5f207fe2bc87c languageName: node linkType: hard @@ -1672,11 +1736,11 @@ __metadata: http-errors: "npm:2.0.0" iconv-lite: "npm:0.4.24" unpipe: "npm:1.0.0" - checksum: b201c4b66049369a60e766318caff5cb3cc5a900efd89bdac431463822d976ad0670912c931fdbdcf5543207daf6f6833bca57aa116e1661d2ea91e12ca692c4 + checksum: 10c0/b201c4b66049369a60e766318caff5cb3cc5a900efd89bdac431463822d976ad0670912c931fdbdcf5543207daf6f6833bca57aa116e1661d2ea91e12ca692c4 languageName: node linkType: hard -"readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.6": +"readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.6, readable-stream@npm:^2.3.8": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -1687,18 +1751,31 @@ __metadata: safe-buffer: "npm:~5.1.1" string_decoder: "npm:~1.1.1" util-deprecate: "npm:~1.0.1" - checksum: 7efdb01f3853bc35ac62ea25493567bf588773213f5f4a79f9c365e1ad13bab845ac0dae7bc946270dc40c3929483228415e92a3fc600cc7e4548992f41ee3fa + checksum: 10c0/7efdb01f3853bc35ac62ea25493567bf588773213f5f4a79f9c365e1ad13bab845ac0dae7bc946270dc40c3929483228415e92a3fc600cc7e4548992f41ee3fa languageName: node linkType: hard -"readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0, readable-stream@npm:^3.6.2": +"readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: inherits: "npm:^2.0.3" string_decoder: "npm:^1.1.1" util-deprecate: "npm:^1.0.1" - checksum: e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7 + checksum: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7 + languageName: node + linkType: hard + +"readable-stream@npm:^4.0.0": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: 10c0/a2c80e0e53aabd91d7df0330929e32d0a73219f9477dbbb18472f6fdd6a11a699fc5d172a1beff98d50eae4f1496c950ffa85b7cc2c4c196963f289a5f39275d languageName: node linkType: hard @@ -1707,21 +1784,21 @@ __metadata: resolution: "readdir-glob@npm:1.1.3" dependencies: minimatch: "npm:^5.1.0" - checksum: a37e0716726650845d761f1041387acd93aa91b28dd5381950733f994b6c349ddc1e21e266ec7cc1f9b92e205a7a972232f9b89d5424d07361c2c3753d5dbace + checksum: 10c0/a37e0716726650845d761f1041387acd93aa91b28dd5381950733f994b6c349ddc1e21e266ec7cc1f9b92e205a7a972232f9b89d5424d07361c2c3753d5dbace languageName: node linkType: hard "readline-sync@npm:^1.4.7": version: 1.4.10 resolution: "readline-sync@npm:1.4.10" - checksum: 0a4d0fe4ad501f8f005a3c9cbf3cc0ae6ca2ced93e9a1c7c46f226bdfcb6ef5d3f437ae7e9d2e1098ee13524a3739c830e4c8dbc7f543a693eecd293e41093a3 + checksum: 10c0/0a4d0fe4ad501f8f005a3c9cbf3cc0ae6ca2ced93e9a1c7c46f226bdfcb6ef5d3f437ae7e9d2e1098ee13524a3739c830e4c8dbc7f543a693eecd293e41093a3 languageName: node linkType: hard "redis-errors@npm:^1.0.0, redis-errors@npm:^1.2.0": version: 1.2.0 resolution: "redis-errors@npm:1.2.0" - checksum: 5b316736e9f532d91a35bff631335137a4f974927bb2fb42bf8c2f18879173a211787db8ac4c3fde8f75ed6233eb0888e55d52510b5620e30d69d7d719c8b8a7 + checksum: 10c0/5b316736e9f532d91a35bff631335137a4f974927bb2fb42bf8c2f18879173a211787db8ac4c3fde8f75ed6233eb0888e55d52510b5620e30d69d7d719c8b8a7 languageName: node linkType: hard @@ -1730,21 +1807,21 @@ __metadata: resolution: "redis-parser@npm:3.0.0" dependencies: redis-errors: "npm:^1.0.0" - checksum: ee16ac4c7b2a60b1f42a2cdaee22b005bd4453eb2d0588b8a4939718997ae269da717434da5d570fe0b05030466eeb3f902a58cf2e8e1ca058bf6c9c596f632f + checksum: 10c0/ee16ac4c7b2a60b1f42a2cdaee22b005bd4453eb2d0588b8a4939718997ae269da717434da5d570fe0b05030466eeb3f902a58cf2e8e1ca058bf6c9c596f632f languageName: node linkType: hard "regenerator-runtime@npm:^0.14.0": - version: 0.14.0 - resolution: "regenerator-runtime@npm:0.14.0" - checksum: e25f062c1a183f81c99681691a342760e65c55e8d3a4d4fe347ebe72433b123754b942b70b622959894e11f8a9131dc549bd3c9a5234677db06a4af42add8d12 + version: 0.14.1 + resolution: "regenerator-runtime@npm:0.14.1" + checksum: 10c0/1b16eb2c4bceb1665c89de70dcb64126a22bc8eb958feef3cd68fe11ac6d2a4899b5cd1b80b0774c7c03591dc57d16631a7f69d2daa2ec98100e2f29f7ec4cc4 languageName: node linkType: hard "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" - checksum: 59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe + checksum: 10c0/59933e8501727ba13ad73ef4a04d5280b3717fd650408460c987392efe9d7be2040778ed8ebe933c5cbd63da3dcc37919c141ef8af0a54a6e4fca5a2af177bfe languageName: node linkType: hard @@ -1754,72 +1831,72 @@ __metadata: dependencies: hash-base: "npm:^3.0.0" inherits: "npm:^2.0.1" - checksum: f6f0df78817e78287c766687aed4d5accbebc308a8e7e673fb085b9977473c1f139f0c5335d353f172a915bb288098430755d2ad3c4f30612f4dd0c901cd2c3a + checksum: 10c0/f6f0df78817e78287c766687aed4d5accbebc308a8e7e673fb085b9977473c1f139f0c5335d353f172a915bb288098430755d2ad3c4f30612f4dd0c901cd2c3a languageName: node linkType: hard "safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" - checksum: 6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 + checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 languageName: node linkType: hard "safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1": version: 5.1.2 resolution: "safe-buffer@npm:5.1.2" - checksum: 780ba6b5d99cc9a40f7b951d47152297d0e260f0df01472a1b99d4889679a4b94a13d644f7dbc4f022572f09ae9005fa2fbb93bbbd83643316f365a3e9a45b21 + checksum: 10c0/780ba6b5d99cc9a40f7b951d47152297d0e260f0df01472a1b99d4889679a4b94a13d644f7dbc4f022572f09ae9005fa2fbb93bbbd83643316f365a3e9a45b21 languageName: node linkType: hard -"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0, safer-buffer@npm:^2.1.0": +"safer-buffer@npm:>= 2.1.2 < 3, safer-buffer@npm:>= 2.1.2 < 3.0.0": version: 2.1.2 resolution: "safer-buffer@npm:2.1.2" - checksum: 7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 + checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 languageName: node linkType: hard "sax@npm:^1.2.4": version: 1.3.0 resolution: "sax@npm:1.3.0" - checksum: 599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea + checksum: 10c0/599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea languageName: node linkType: hard "semver@npm:^7.3.5": - version: 7.5.4 - resolution: "semver@npm:7.5.4" - dependencies: - lru-cache: "npm:^6.0.0" + version: 7.6.2 + resolution: "semver@npm:7.6.2" bin: semver: bin/semver.js - checksum: 5160b06975a38b11c1ab55950cb5b8a23db78df88275d3d8a42ccf1f29e55112ac995b3a26a522c36e3b5f76b0445f1eef70d696b8c7862a2b4303d7b0e7609e + checksum: 10c0/97d3441e97ace8be4b1976433d1c32658f6afaff09f143e52c593bae7eef33de19e3e369c88bd985ce1042c6f441c80c6803078d1de2a9988080b66684cbb30c languageName: node linkType: hard -"set-function-length@npm:^1.1.1": - version: 1.1.1 - resolution: "set-function-length@npm:1.1.1" +"set-function-length@npm:^1.2.1": + version: 1.2.2 + resolution: "set-function-length@npm:1.2.2" dependencies: - define-data-property: "npm:^1.1.1" - get-intrinsic: "npm:^1.2.1" + define-data-property: "npm:^1.1.4" + es-errors: "npm:^1.3.0" + function-bind: "npm:^1.1.2" + get-intrinsic: "npm:^1.2.4" gopd: "npm:^1.0.1" - has-property-descriptors: "npm:^1.0.0" - checksum: a29e255c116c29e3323b851c4f46c58c91be9bb8b065f191e2ea1807cb2c839df56e3175732a498e0c6d54626ba6b6fef896bf699feb7ab70c42dc47eb247c95 + has-property-descriptors: "npm:^1.0.2" + checksum: 10c0/82850e62f412a258b71e123d4ed3873fa9377c216809551192bb6769329340176f109c2eeae8c22a8d386c76739855f78e8716515c818bcaef384b51110f0f3c languageName: node linkType: hard "setimmediate@npm:^1.0.4": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" - checksum: 5bae81bfdbfbd0ce992893286d49c9693c82b1bcc00dcaaf3a09c8f428fdeacf4190c013598b81875dfac2b08a572422db7df779a99332d0fce186d15a3e4d49 + checksum: 10c0/5bae81bfdbfbd0ce992893286d49c9693c82b1bcc00dcaaf3a09c8f428fdeacf4190c013598b81875dfac2b08a572422db7df779a99332d0fce186d15a3e4d49 languageName: node linkType: hard "setprototypeof@npm:1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" - checksum: 68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc + checksum: 10c0/68733173026766fa0d9ecaeb07f0483f4c2dc70ca376b3b7c40b7cda909f94b0918f6c5ad5ce27a9160bdfb475efaa9d5e705a11d8eaae18f9835d20976028bc languageName: node linkType: hard @@ -1831,20 +1908,20 @@ __metadata: safe-buffer: "npm:^5.0.1" bin: sha.js: ./bin.js - checksum: b7a371bca8821c9cc98a0aeff67444a03d48d745cb103f17228b96793f455f0eb0a691941b89ea1e60f6359207e36081d9be193252b0f128e0daf9cfea2815a5 + checksum: 10c0/b7a371bca8821c9cc98a0aeff67444a03d48d745cb103f17228b96793f455f0eb0a691941b89ea1e60f6359207e36081d9be193252b0f128e0daf9cfea2815a5 languageName: node linkType: hard "shadow-cljs-jar@npm:1.3.4": version: 1.3.4 resolution: "shadow-cljs-jar@npm:1.3.4" - checksum: c5548bb5f2bda5e0a90df6f42e4ec3a07ed4c72cdebb87619e8d9a2167bb3d4b60d6f6a305a3e15cbfb379d5fdbe2a989a0e7059b667cfb3911bc198a4489e94 + checksum: 10c0/c5548bb5f2bda5e0a90df6f42e4ec3a07ed4c72cdebb87619e8d9a2167bb3d4b60d6f6a305a3e15cbfb379d5fdbe2a989a0e7059b667cfb3911bc198a4489e94 languageName: node linkType: hard -"shadow-cljs@npm:2.27.4": - version: 2.27.4 - resolution: "shadow-cljs@npm:2.27.4" +"shadow-cljs@npm:2.28.11": + version: 2.28.11 + resolution: "shadow-cljs@npm:2.28.11" dependencies: node-libs-browser: "npm:^2.2.1" readline-sync: "npm:^1.4.7" @@ -1854,7 +1931,7 @@ __metadata: ws: "npm:^7.4.6" bin: shadow-cljs: cli/runner.js - checksum: bae23e71df9c2b2979259a0cde8747c923ee295f58ab4637c9d6b103d82542b40ef39172d4be2dbb94af2e6458a177d1ec96c1eb1e73b1d8f3a4ddb5eaaba7d4 + checksum: 10c0/c5c77d524ee8f44e4ae2ddc196af170d02405cc8731ea71f852c7b220fc1ba8aaf5cf33753fd8a7566c8749bb75d360f903dfb0d131bcdc6c2c33f44404bd6a3 languageName: node linkType: hard @@ -1863,60 +1940,61 @@ __metadata: resolution: "shebang-command@npm:2.0.0" dependencies: shebang-regex: "npm:^3.0.0" - checksum: a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e + checksum: 10c0/a41692e7d89a553ef21d324a5cceb5f686d1f3c040759c50aab69688634688c5c327f26f3ecf7001ebfd78c01f3c7c0a11a7c8bfd0a8bc9f6240d4f40b224e4e languageName: node linkType: hard "shebang-regex@npm:^3.0.0": version: 3.0.0 resolution: "shebang-regex@npm:3.0.0" - checksum: 1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 + checksum: 10c0/1dbed0726dd0e1152a92696c76c7f06084eb32a90f0528d11acd764043aacf76994b2fb30aa1291a21bd019d6699164d048286309a278855ee7bec06cf6fb690 languageName: node linkType: hard -"side-channel@npm:^1.0.4": - version: 1.0.4 - resolution: "side-channel@npm:1.0.4" +"side-channel@npm:^1.0.6": + version: 1.0.6 + resolution: "side-channel@npm:1.0.6" dependencies: - call-bind: "npm:^1.0.0" - get-intrinsic: "npm:^1.0.2" - object-inspect: "npm:^1.9.0" - checksum: 054a5d23ee35054b2c4609b9fd2a0587760737782b5d765a9c7852264710cc39c6dcb56a9bbd6c12cd84071648aea3edb2359d2f6e560677eedadce511ac1da5 + call-bind: "npm:^1.0.7" + es-errors: "npm:^1.3.0" + get-intrinsic: "npm:^1.2.4" + object-inspect: "npm:^1.13.1" + checksum: 10c0/d2afd163dc733cc0a39aa6f7e39bf0c436293510dbccbff446733daeaf295857dbccf94297092ec8c53e2503acac30f0b78830876f0485991d62a90e9cad305f languageName: node linkType: hard "signal-exit@npm:^4.0.1": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" - checksum: 41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 + checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 languageName: node linkType: hard "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" - checksum: a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 + checksum: 10c0/a16775323e1404dd43fabafe7460be13a471e021637bc7889468eb45ce6a6b207261f454e4e530a19500cc962c4cc5348583520843b363f4193cee5c00e1e539 languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.1": - version: 8.0.2 - resolution: "socks-proxy-agent@npm:8.0.2" +"socks-proxy-agent@npm:^8.0.3": + version: 8.0.3 + resolution: "socks-proxy-agent@npm:8.0.3" dependencies: - agent-base: "npm:^7.0.2" + agent-base: "npm:^7.1.1" debug: "npm:^4.3.4" socks: "npm:^2.7.1" - checksum: a842402fc9b8848a31367f2811ca3cd14c4106588b39a0901cd7a69029998adfc6456b0203617c18ed090542ad0c24ee4e9d4c75a0c4b75071e214227c177eb7 + checksum: 10c0/4950529affd8ccd6951575e21c1b7be8531b24d924aa4df3ee32df506af34b618c4e50d261f4cc603f1bfd8d426915b7d629966c8ce45b05fb5ad8c8b9a6459d languageName: node linkType: hard "socks@npm:^2.7.1": - version: 2.7.1 - resolution: "socks@npm:2.7.1" + version: 2.8.3 + resolution: "socks@npm:2.8.3" dependencies: - ip: "npm:^2.0.0" + ip-address: "npm:^9.0.5" smart-buffer: "npm:^4.2.0" - checksum: 43f69dbc9f34fc8220bc51c6eea1c39715ab3cfdb115d6e3285f6c7d1a603c5c75655668a5bbc11e3c7e2c99d60321fb8d7ab6f38cda6a215fadd0d6d0b52130 + checksum: 10c0/d54a52bf9325165770b674a67241143a3d8b4e4c8884560c4e0e078aace2a728dffc7f70150660f51b85797c4e1a3b82f9b7aa25e0a0ceae1a243365da5c51a7 languageName: node linkType: hard @@ -1925,7 +2003,7 @@ __metadata: resolution: "source-map-support@npm:0.4.18" dependencies: source-map: "npm:^0.5.6" - checksum: cd9f0309c1632b1e01a7715a009e0b036d565f3af8930fa8cda2a06aeec05ad1d86180e743b7e1f02cc3c97abe8b6d8de7c3878c2d8e01e86e17f876f7ecf98e + checksum: 10c0/cd9f0309c1632b1e01a7715a009e0b036d565f3af8930fa8cda2a06aeec05ad1d86180e743b7e1f02cc3c97abe8b6d8de7c3878c2d8e01e86e17f876f7ecf98e languageName: node linkType: hard @@ -1935,44 +2013,51 @@ __metadata: dependencies: buffer-from: "npm:^1.0.0" source-map: "npm:^0.6.0" - checksum: 9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d + checksum: 10c0/9ee09942f415e0f721d6daad3917ec1516af746a8120bba7bb56278707a37f1eb8642bde456e98454b8a885023af81a16e646869975f06afc1a711fb90484e7d languageName: node linkType: hard "source-map@npm:^0.5.6": version: 0.5.7 resolution: "source-map@npm:0.5.7" - checksum: 904e767bb9c494929be013017380cbba013637da1b28e5943b566031e29df04fba57edf3f093e0914be094648b577372bd8ad247fa98cfba9c600794cd16b599 + checksum: 10c0/904e767bb9c494929be013017380cbba013637da1b28e5943b566031e29df04fba57edf3f093e0914be094648b577372bd8ad247fa98cfba9c600794cd16b599 languageName: node linkType: hard "source-map@npm:^0.6.0": version: 0.6.1 resolution: "source-map@npm:0.6.1" - checksum: ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 + languageName: node + linkType: hard + +"sprintf-js@npm:^1.1.3": + version: 1.1.3 + resolution: "sprintf-js@npm:1.1.3" + checksum: 10c0/09270dc4f30d479e666aee820eacd9e464215cdff53848b443964202bf4051490538e5dd1b42e1a65cf7296916ca17640aebf63dae9812749c7542ee5f288dec languageName: node linkType: hard "ssri@npm:^10.0.0": - version: 10.0.5 - resolution: "ssri@npm:10.0.5" + version: 10.0.6 + resolution: "ssri@npm:10.0.6" dependencies: minipass: "npm:^7.0.3" - checksum: b091f2ae92474183c7ac5ed3f9811457e1df23df7a7e70c9476eaa9a0c4a0c8fc190fb45acefbf023ca9ee864dd6754237a697dc52a0fb182afe65d8e77443d8 + checksum: 10c0/e5a1e23a4057a86a97971465418f22ea89bd439ac36ade88812dd920e4e61873e8abd6a9b72a03a67ef50faa00a2daf1ab745c5a15b46d03e0544a0296354227 languageName: node linkType: hard "standard-as-callback@npm:^2.1.0": version: 2.1.0 resolution: "standard-as-callback@npm:2.1.0" - checksum: 012677236e3d3fdc5689d29e64ea8a599331c4babe86956bf92fc5e127d53f85411c5536ee0079c52c43beb0026b5ce7aa1d834dd35dd026e82a15d1bcaead1f + checksum: 10c0/012677236e3d3fdc5689d29e64ea8a599331c4babe86956bf92fc5e127d53f85411c5536ee0079c52c43beb0026b5ce7aa1d834dd35dd026e82a15d1bcaead1f languageName: node linkType: hard "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" - checksum: 34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0 + checksum: 10c0/34378b207a1620a24804ce8b5d230fea0c279f00b18a7209646d5d47e419d1cc23e7cbf33a25a1e51ac38973dc2ac2e1e9c647a8e481ef365f77668d72becfd0 languageName: node linkType: hard @@ -1982,7 +2067,7 @@ __metadata: dependencies: inherits: "npm:~2.0.1" readable-stream: "npm:^2.0.2" - checksum: 485562bd5d962d633ae178449029c6fa2611052e356bdb5668f768544aa4daa94c4f9a97de718f3f30ad98f3cb98a5f396252bb3855aff153c138f79c0e8f6ac + checksum: 10c0/485562bd5d962d633ae178449029c6fa2611052e356bdb5668f768544aa4daa94c4f9a97de718f3f30ad98f3cb98a5f396252bb3855aff153c138f79c0e8f6ac languageName: node linkType: hard @@ -1995,17 +2080,21 @@ __metadata: readable-stream: "npm:^2.3.6" to-arraybuffer: "npm:^1.0.0" xtend: "npm:^4.0.0" - checksum: fbe7d327a29216bbabe88d3819bb8f7a502f11eeacf3212579e5af1f76fa7283f6ffa66134ab7d80928070051f571d1029e85f65ce3369fffd4c4df3669446c4 + checksum: 10c0/fbe7d327a29216bbabe88d3819bb8f7a502f11eeacf3212579e5af1f76fa7283f6ffa66134ab7d80928070051f571d1029e85f65ce3369fffd4c4df3669446c4 languageName: node linkType: hard "streamx@npm:^2.15.0": - version: 2.15.5 - resolution: "streamx@npm:2.15.5" + version: 2.16.1 + resolution: "streamx@npm:2.16.1" dependencies: + bare-events: "npm:^2.2.0" fast-fifo: "npm:^1.1.0" queue-tick: "npm:^1.0.1" - checksum: 7998d1fa3324131ed94efc4a4e8b22e0f60267b21d8f8fac8c605eaa1a6d6358adbc38c35b407be0eb8cc09a223c641962afb0db29ecbe92118242118946d93c + dependenciesMeta: + bare-events: + optional: true + checksum: 10c0/202b1d7cb7ceb36cdc5d5d0e2c27deafcc8670a4934cda7a5e3d3d45b8d3a64dc43f1b982b1c1cb316f01964dd5137b7e26af3151582c7c29ad3cf4072c6dbb9 languageName: node linkType: hard @@ -2016,7 +2105,7 @@ __metadata: emoji-regex: "npm:^8.0.0" is-fullwidth-code-point: "npm:^3.0.0" strip-ansi: "npm:^6.0.1" - checksum: 1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b + checksum: 10c0/1e525e92e5eae0afd7454086eed9c818ee84374bb80328fc41217ae72ff5f065ef1c9d7f72da41de40c75fa8bb3dee63d92373fd492c84260a552c636392a47b languageName: node linkType: hard @@ -2027,16 +2116,16 @@ __metadata: eastasianwidth: "npm:^0.2.0" emoji-regex: "npm:^9.2.2" strip-ansi: "npm:^7.0.1" - checksum: ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca + checksum: 10c0/ab9c4264443d35b8b923cbdd513a089a60de339216d3b0ed3be3ba57d6880e1a192b70ae17225f764d7adbf5994e9bb8df253a944736c15a0240eff553c678ca languageName: node linkType: hard -"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1": +"string_decoder@npm:^1.0.0, string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" dependencies: safe-buffer: "npm:~5.2.0" - checksum: 810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d + checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d languageName: node linkType: hard @@ -2045,7 +2134,7 @@ __metadata: resolution: "string_decoder@npm:1.1.1" dependencies: safe-buffer: "npm:~5.1.0" - checksum: b4f89f3a92fd101b5653ca3c99550e07bdf9e13b35037e9e2a1c7b47cec4e55e06ff3fc468e314a0b5e80bfbaf65c1ca5a84978764884ae9413bec1fc6ca924e + checksum: 10c0/b4f89f3a92fd101b5653ca3c99550e07bdf9e13b35037e9e2a1c7b47cec4e55e06ff3fc468e314a0b5e80bfbaf65c1ca5a84978764884ae9413bec1fc6ca924e languageName: node linkType: hard @@ -2054,7 +2143,7 @@ __metadata: resolution: "strip-ansi@npm:6.0.1" dependencies: ansi-regex: "npm:^5.0.1" - checksum: 1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + checksum: 10c0/1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 languageName: node linkType: hard @@ -2063,24 +2152,24 @@ __metadata: resolution: "strip-ansi@npm:7.1.0" dependencies: ansi-regex: "npm:^6.0.1" - checksum: a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 + checksum: 10c0/a198c3762e8832505328cbf9e8c8381de14a4fa50a4f9b2160138158ea88c0f5549fb50cb13c651c3088f47e63a108b34622ec18c0499b6c8c3a5ddf6b305ac4 languageName: node linkType: hard "tar-stream@npm:^3.0.0": - version: 3.1.6 - resolution: "tar-stream@npm:3.1.6" + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" dependencies: b4a: "npm:^1.6.4" fast-fifo: "npm:^1.2.0" streamx: "npm:^2.15.0" - checksum: 7d52d1a56eb25b8434c9544becb737eb6c4f0ed19d205e739fdd2537ad8d1d623a6c93f7f8e58d9028cb0cdf86c0d8b67164e070cd1702cc78b8ab7cba0f3702 + checksum: 10c0/a09199d21f8714bd729993ac49b6c8efcb808b544b89f23378ad6ffff6d1cb540878614ba9d4cfec11a64ef39e1a6f009a5398371491eb1fda606ffc7f70f718 languageName: node linkType: hard "tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.2.0 - resolution: "tar@npm:6.2.0" + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: chownr: "npm:^2.0.0" fs-minipass: "npm:^2.0.0" @@ -2088,7 +2177,7 @@ __metadata: minizlib: "npm:^2.1.1" mkdirp: "npm:^1.0.3" yallist: "npm:^4.0.0" - checksum: 02ca064a1a6b4521fef88c07d389ac0936730091f8c02d30ea60d472e0378768e870769ab9e986d87807bfee5654359cf29ff4372746cc65e30cbddc352660d8 + checksum: 10c0/a5eca3eb50bc11552d453488344e6507156b9193efd7635e98e867fab275d527af53d8866e2370cd09dfe74378a18111622ace35af6a608e5223a7d27fe99537 languageName: node linkType: hard @@ -2097,35 +2186,35 @@ __metadata: resolution: "timers-browserify@npm:2.0.12" dependencies: setimmediate: "npm:^1.0.4" - checksum: 98e84db1a685bc8827c117a8bc62aac811ad56a995d07938fc7ed8cdc5bf3777bfe2d4e5da868847194e771aac3749a20f6cdd22091300fe889a76fe214a4641 + checksum: 10c0/98e84db1a685bc8827c117a8bc62aac811ad56a995d07938fc7ed8cdc5bf3777bfe2d4e5da868847194e771aac3749a20f6cdd22091300fe889a76fe214a4641 languageName: node linkType: hard "to-arraybuffer@npm:^1.0.0": version: 1.0.1 resolution: "to-arraybuffer@npm:1.0.1" - checksum: 2460bd95524f4845a751e4f8bf9937f9f3dcd1651f104e1512868782f858f8302c1cf25bbc30794bc1b3ff65c4e135158377302f2abaff43a2d8e3c38dfe098c + checksum: 10c0/2460bd95524f4845a751e4f8bf9937f9f3dcd1651f104e1512868782f858f8302c1cf25bbc30794bc1b3ff65c4e135158377302f2abaff43a2d8e3c38dfe098c languageName: node linkType: hard "toidentifier@npm:1.0.1": version: 1.0.1 resolution: "toidentifier@npm:1.0.1" - checksum: 93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 + checksum: 10c0/93937279934bd66cc3270016dd8d0afec14fb7c94a05c72dc57321f8bd1fa97e5bea6d1f7c89e728d077ca31ea125b78320a616a6c6cd0e6b9cb94cb864381c1 languageName: node linkType: hard "tsscmp@npm:1.0.6": version: 1.0.6 resolution: "tsscmp@npm:1.0.6" - checksum: 2f79a9455e7e3e8071995f98cdf3487ccfc91b760bec21a9abb4d90519557eafaa37246e87c92fa8bf3fef8fd30cfd0cc3c4212bb929baa9fb62494bfa4d24b2 + checksum: 10c0/2f79a9455e7e3e8071995f98cdf3487ccfc91b760bec21a9abb4d90519557eafaa37246e87c92fa8bf3fef8fd30cfd0cc3c4212bb929baa9fb62494bfa4d24b2 languageName: node linkType: hard "tty-browserify@npm:0.0.0": version: 0.0.0 resolution: "tty-browserify@npm:0.0.0" - checksum: c0c68206565f1372e924d5cdeeff1a0d9cc729833f1da98c03d78be8f939e5f61a107bd0ab77d1ef6a47d62bb0e48b1081fbea273acf404959e22fd3891439c5 + checksum: 10c0/c0c68206565f1372e924d5cdeeff1a0d9cc729833f1da98c03d78be8f939e5f61a107bd0ab77d1ef6a47d62bb0e48b1081fbea273acf404959e22fd3891439c5 languageName: node linkType: hard @@ -2134,7 +2223,7 @@ __metadata: resolution: "unique-filename@npm:3.0.0" dependencies: unique-slug: "npm:^4.0.0" - checksum: 6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f + checksum: 10c0/6363e40b2fa758eb5ec5e21b3c7fb83e5da8dcfbd866cc0c199d5534c42f03b9ea9ab069769cc388e1d7ab93b4eeef28ef506ab5f18d910ef29617715101884f languageName: node linkType: hard @@ -2143,14 +2232,14 @@ __metadata: resolution: "unique-slug@npm:4.0.0" dependencies: imurmurhash: "npm:^0.1.4" - checksum: cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 + checksum: 10c0/cb811d9d54eb5821b81b18205750be84cb015c20a4a44280794e915f5a0a70223ce39066781a354e872df3572e8155c228f43ff0cce94c7cbf4da2cc7cbdd635 languageName: node linkType: hard "unpipe@npm:1.0.0": version: 1.0.0 resolution: "unpipe@npm:1.0.0" - checksum: 193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c + checksum: 10c0/193400255bd48968e5c5383730344fbb4fa114cdedfab26e329e50dd2d81b134244bb8a72c6ac1b10ab0281a58b363d06405632c9d49ca9dfd5e90cbd7d0f32c languageName: node linkType: hard @@ -2160,14 +2249,14 @@ __metadata: dependencies: punycode: "npm:^1.4.1" qs: "npm:^6.11.2" - checksum: 7546b878ee7927cfc62ca21dbe2dc395cf70e889c3488b2815bf2c63355cb3c7db555128176a01b0af6cccf265667b6fd0b4806de00cb71c143c53986c08c602 + checksum: 10c0/7546b878ee7927cfc62ca21dbe2dc395cf70e889c3488b2815bf2c63355cb3c7db555128176a01b0af6cccf265667b6fd0b4806de00cb71c143c53986c08c602 languageName: node linkType: hard "util-deprecate@npm:^1.0.1, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" - checksum: 41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 + checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 languageName: node linkType: hard @@ -2176,7 +2265,7 @@ __metadata: resolution: "util@npm:0.10.4" dependencies: inherits: "npm:2.0.3" - checksum: d29f6893e406b63b088ce9924da03201df89b31490d4d011f1c07a386ea4b3dbe907464c274023c237da470258e1805d806c7e4009a5974cd6b1d474b675852a + checksum: 10c0/d29f6893e406b63b088ce9924da03201df89b31490d4d011f1c07a386ea4b3dbe907464c274023c237da470258e1805d806c7e4009a5974cd6b1d474b675852a languageName: node linkType: hard @@ -2185,14 +2274,14 @@ __metadata: resolution: "util@npm:0.11.1" dependencies: inherits: "npm:2.0.3" - checksum: 8e9d1a85e661c8a8d9883d821aedbff3f8d9c3accd85357020905386ada5653b20389fc3591901e2a0bde64f8dc86b28c3f990114aa5a38eaaf30b455fa3cdf6 + checksum: 10c0/8e9d1a85e661c8a8d9883d821aedbff3f8d9c3accd85357020905386ada5653b20389fc3591901e2a0bde64f8dc86b28c3f990114aa5a38eaaf30b455fa3cdf6 languageName: node linkType: hard "vm-browserify@npm:^1.0.1": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" - checksum: 0cc1af6e0d880deb58bc974921320c187f9e0a94f25570fca6b1bd64e798ce454ab87dfd797551b1b0cc1849307421aae0193cedf5f06bdb5680476780ee344b + checksum: 10c0/0cc1af6e0d880deb58bc974921320c187f9e0a94f25570fca6b1bd64e798ce454ab87dfd797551b1b0cc1849307421aae0193cedf5f06bdb5680476780ee344b languageName: node linkType: hard @@ -2203,7 +2292,7 @@ __metadata: isexe: "npm:^2.0.0" bin: which: ./bin/which - checksum: e945a8b6bbf6821aaaef7f6e0c309d4b615ef35699576d5489b4261da9539f70393c6b2ce700ee4321c18f914ebe5644bc4631b15466ffbaad37d83151f6af59 + checksum: 10c0/e945a8b6bbf6821aaaef7f6e0c309d4b615ef35699576d5489b4261da9539f70393c6b2ce700ee4321c18f914ebe5644bc4631b15466ffbaad37d83151f6af59 languageName: node linkType: hard @@ -2214,7 +2303,7 @@ __metadata: isexe: "npm:^2.0.0" bin: node-which: ./bin/node-which - checksum: 66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f + checksum: 10c0/66522872a768b60c2a65a57e8ad184e5372f5b6a9ca6d5f033d4b0dc98aff63995655a7503b9c0a2598936f532120e81dd8cc155e2e92ed662a2b9377cc4374f languageName: node linkType: hard @@ -2225,7 +2314,7 @@ __metadata: isexe: "npm:^3.1.1" bin: node-which: bin/which.js - checksum: 449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a + checksum: 10c0/449fa5c44ed120ccecfe18c433296a4978a7583bf2391c50abce13f76878d2476defde04d0f79db8165bdf432853c1f8389d0485ca6e8ebce3bbcded513d5e6a languageName: node linkType: hard @@ -2236,7 +2325,7 @@ __metadata: ansi-styles: "npm:^4.0.0" string-width: "npm:^4.1.0" strip-ansi: "npm:^6.0.0" - checksum: d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + checksum: 10c0/d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da languageName: node linkType: hard @@ -2247,14 +2336,7 @@ __metadata: ansi-styles: "npm:^6.1.0" string-width: "npm:^5.0.1" strip-ansi: "npm:^7.0.1" - checksum: 138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 - languageName: node - linkType: hard - -"wrappy@npm:1": - version: 1.0.2 - resolution: "wrappy@npm:1.0.2" - checksum: 56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 + checksum: 10c0/138ff58a41d2f877eae87e3282c0630fc2789012fc1af4d6bd626eeb9a2f9a65ca92005e6e69a75c7b85a68479fe7443c7dbe1eb8fbaa681a4491364b7c55c60 languageName: node linkType: hard @@ -2269,7 +2351,7 @@ __metadata: optional: true utf-8-validate: optional: true - checksum: aec4ef4eb65821a7dde7b44790f8699cfafb7978c9b080f6d7a98a7f8fc0ce674c027073a78574c94786ba7112cc90fa2cc94fc224ceba4d4b1030cff9662494 + checksum: 10c0/aec4ef4eb65821a7dde7b44790f8699cfafb7978c9b080f6d7a98a7f8fc0ce674c027073a78574c94786ba7112cc90fa2cc94fc224ceba4d4b1030cff9662494 languageName: node linkType: hard @@ -2280,7 +2362,7 @@ __metadata: sax: "npm:^1.2.4" bin: xml-js: ./bin/cli.js - checksum: c83631057f10bf90ea785cee434a8a1a0030c7314fe737ad9bf568a281083b565b28b14c9e9ba82f11fc9dc582a3a907904956af60beb725be1c9ad4b030bc5a + checksum: 10c0/c83631057f10bf90ea785cee434a8a1a0030c7314fe737ad9bf568a281083b565b28b14c9e9ba82f11fc9dc582a3a907904956af60beb725be1c9ad4b030bc5a languageName: node linkType: hard @@ -2289,31 +2371,31 @@ __metadata: resolution: "xregexp@npm:5.1.1" dependencies: "@babel/runtime-corejs3": "npm:^7.16.5" - checksum: ae007c7898afd808e7664931228dc4bd38e65ebc24c66318416a038b4351cc73cc9b3b9cea1ab5ffd97933bf9b75afbf848f36e91d22b2416d6bd7d6fcfd2ee6 + checksum: 10c0/ae007c7898afd808e7664931228dc4bd38e65ebc24c66318416a038b4351cc73cc9b3b9cea1ab5ffd97933bf9b75afbf848f36e91d22b2416d6bd7d6fcfd2ee6 languageName: node linkType: hard "xtend@npm:^4.0.0": version: 4.0.2 resolution: "xtend@npm:4.0.2" - checksum: 366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e + checksum: 10c0/366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e languageName: node linkType: hard "yallist@npm:^4.0.0": version: 4.0.0 resolution: "yallist@npm:4.0.0" - checksum: 2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a + checksum: 10c0/2286b5e8dbfe22204ab66e2ef5cc9bbb1e55dfc873bbe0d568aa943eb255d131890dfd5bf243637273d31119b870f49c18fcde2c6ffbb7a7a092b870dc90625a languageName: node linkType: hard -"zip-stream@npm:^5.0.1": - version: 5.0.1 - resolution: "zip-stream@npm:5.0.1" +"zip-stream@npm:^6.0.1": + version: 6.0.1 + resolution: "zip-stream@npm:6.0.1" dependencies: - archiver-utils: "npm:^4.0.1" - compress-commons: "npm:^5.0.1" - readable-stream: "npm:^3.6.0" - checksum: 18b4ecf28824bd165709de5056d53cf611f07e0b7578508fa94c497f17164722dc19a0739ea8b2c1a296de7d3f70f7ad558e7a3a4929240fb2730afc5fd60679 + archiver-utils: "npm:^5.0.0" + compress-commons: "npm:^6.0.2" + readable-stream: "npm:^4.0.0" + checksum: 10c0/50f2fb30327fb9d09879abf7ae2493705313adf403e794b030151aaae00009162419d60d0519e807673ec04d442e140c8879ca14314df0a0192de3b233e8f28b languageName: node linkType: hard diff --git a/frontend/.gitignore b/frontend/.gitignore index 836120c1e2f4ddca6a035900ddbf4b7d0af56e02..dd3776ebd39e81e760742642f4986013cebdc76d 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -5,3 +5,9 @@ !.yarn/releases !.yarn/sdks !.yarn/versions +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/**/visual-specs/**/*.png diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index eb8da3b690bfa95f431374eedfbcf290c686a664..0000000000000000000000000000000000000000 --- a/frontend/.prettierrc +++ /dev/null @@ -1,2 +0,0 @@ ---- -printWidth: 110 \ No newline at end of file diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000000000000000000000000000000000000..f6769669c4553e58dfc896baea3abc204e0fe248 --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "overrides": [ + { + "files": "*.scss", + "options": { + "printWidth": 110 + } + } + ] +} + diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js index a1b422ae61c7a05b97d41cb907c6644456156ac4..cd48f83bc89b3d9dfc9fff0f9e613dd727292d68 100644 --- a/frontend/.storybook/main.js +++ b/frontend/.storybook/main.js @@ -2,18 +2,17 @@ const config = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], staticDirs: ["../resources/public"], - addons: [ - "@storybook/addon-links", - "@storybook/addon-essentials", - "@storybook/addon-onboarding", - "@storybook/addon-interactions", - ], + addons: ["@storybook/addon-essentials", "@storybook/addon-themes"], + core: { + builder: "@storybook/builder-vite", + options: { + viteConfigPath: "../vite.config.js", + }, + }, framework: { name: "@storybook/react-vite", options: {}, }, - docs: { - autodocs: "tag", - }, + docs: {}, }; export default config; diff --git a/frontend/.storybook/preview-head.html b/frontend/.storybook/preview-head.html deleted file mode 100644 index 4c273a63fa2b6c4e948c823e4e377d9219a6899f..0000000000000000000000000000000000000000 --- a/frontend/.storybook/preview-head.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js index de50ecbf73f1e64b9c23ec82c269ed60faf6ae05..d15d79b78b5f2972c48395e308f15387ee228f01 100644 --- a/frontend/.storybook/preview.js +++ b/frontend/.storybook/preview.js @@ -1,15 +1,27 @@ -import "../resources/public/css/main.css"; +import { withThemeByClassName } from "@storybook/addon-themes"; + +export const decorators = [ + withThemeByClassName({ + themes: { + light: "light", + dark: "default", + }, + defaultTheme: "dark", + parentSelector: "body", + }), +]; /** @type { import('@storybook/react').Preview } */ const preview = { + decorators: decorators, parameters: { - actions: { argTypesRegex: "^on[A-Z].*" }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/i, }, }, + backgrounds: { disable: true }, }, }; diff --git a/frontend/deps.edn b/frontend/deps.edn index f4a152eae57f41864ed5b540447b2a844c056898..f13d93fc2f991884c8ad97ae1fc63da5d68aa89e 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -3,27 +3,28 @@ {penpot/common {:local/root "../common"} - org.clojure/clojure {:mvn/version "1.11.1"} + org.clojure/clojure {:mvn/version "1.11.3"} binaryage/devtools {:mvn/version "RELEASE"} - metosin/reitit-core {:mvn/version "0.6.0"} + metosin/reitit-core {:mvn/version "0.7.0"} funcool/okulary {:mvn/version "2022.04.11-16"} funcool/potok2 - {:git/tag "v2.0" - :git/sha "2bb377b" - :git/url "https://github.com/funcool/potok.git"} + {:git/tag "v2.1" + :git/sha "84c97b9" + :git/url "https://github.com/funcool/potok.git" + :exclusions [funcool/beicon2]} funcool/beicon2 - {:git/tag "v2.0" - :git/sha "e7135e0" + {:git/tag "v2.2" + :git/sha "8744c66" :git/url "https://github.com/funcool/beicon.git"} funcool/rumext - {:git/tag "v2.11.3" - :git/sha "b1f6ce4" + {:git/tag "v2.14" + :git/sha "0016623" :git/url "https://github.com/funcool/rumext.git"} - instaparse/instaparse {:mvn/version "1.4.12"} + instaparse/instaparse {:mvn/version "1.5.0"} garden/garden {:git/url "https://github.com/noprompt/garden" :git/sha "05590ecb5f6fa670856f3d1ab400aa4961047480"} } @@ -41,12 +42,11 @@ :dev {:extra-paths ["dev"] :extra-deps - {thheller/shadow-cljs {:mvn/version "2.27.4"} + {thheller/shadow-cljs {:mvn/version "2.28.11"} org.clojure/tools.namespace {:mvn/version "RELEASE"} - cider/cider-nrepl {:mvn/version "0.44.0"}}} + cider/cider-nrepl {:mvn/version "0.48.0"}}} :shadow-cljs {:main-opts ["-m" "shadow.cljs.devtools.cli"]} }} - diff --git a/frontend/package.json b/frontend/package.json index 7ed94d2147a8389992431a96a4867d7ad7894d62..1612361abf651a9e4cc9544be7934f8954395609 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.0.2", + "packageManager": "yarn@4.3.1", "browserslist": [ "defaults" ], @@ -17,42 +17,43 @@ "@vitejs/plugin-react": "^4.2.0" }, "scripts": { - "fmt:clj:check": "cljfmt check --parallel=false src/ test/", + "build:app:assets": "node ./scripts/build-app-assets.js", + "build:storybook": "yarn run build:storybook:assets && yarn run build:storybook:cljs && storybook build", + "build:storybook:assets": "node ./scripts/build-storybook-assets.js", + "build:storybook:cljs": "clojure -M:dev:shadow-cljs release storybook", + "e2e:server": "node ./scripts/e2e-server.js", + "e2e:test": "playwright test --project default", "fmt:clj": "cljfmt fix --parallel=true src/ test/", - "test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'", + "fmt:clj:check": "cljfmt check --parallel=false src/ test/", + "fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -w", + "fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js", + "lint:clj": "clj-kondo --parallel --lint src/", "lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss", "lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w", - "lint:clj": "clj-kondo --parallel --lint src/", + "test": "yarn run test:compile && yarn run test:run", + "test:compile": "clojure -M:dev:shadow-cljs compile test --config-merge '{:autorun false}'", "test:run": "node target/tests.cjs", "test:watch": "clojure -M:dev:shadow-cljs watch test", - "test": "yarn run test:compile && yarn run test:run", - "gulp:watch": "gulp watch", - "watch": "shadow-cljs watch main", - "validate-translations": "node ./scripts/validate-translations.js", - "find-unused-translations": "node ./scripts/find-unused-translations.js", - "build:clean": "gulp clean:output && gulp clean:dist", - "build:styles": "gulp build:styles", - "build:assets": "gulp build:assets", - "build:copy": "gulp build:copy", - "storybook:compile": "gulp template:storybook && clojure -M:dev:shadow-cljs compile storybook", - "storybook:watch": "npm run storybook:compile && concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"storybook dev -p 6006\"", - "storybook:build": "npm run storybook:compile && storybook build" + "translations": "node ./scripts/translations.js", + "watch": "yarn run watch:app:assets", + "watch:app:assets": "node ./scripts/watch.js", + "watch:storybook": "concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"", + "watch:storybook:assets": "node ./scripts/watch-storybook.js" }, "devDependencies": { - "@storybook/addon-essentials": "^7.6.17", - "@storybook/addon-interactions": "^7.6.17", - "@storybook/addon-links": "^7.6.17", - "@storybook/addon-onboarding": "^1.0.11", - "@storybook/blocks": "^7.6.17", - "@storybook/react": "^7.6.17", - "@storybook/react-vite": "^7.6.17", - "@storybook/testing-library": "^0.2.2", + "@playwright/test": "1.44.1", + "@storybook/addon-essentials": "^8.2.2", + "@storybook/addon-themes": "^8.2.2", + "@storybook/blocks": "^8.2.2", + "@storybook/react": "^8.2.2", + "@storybook/react-vite": "^8.2.2", "@types/node": "^20.11.20", - "animate.css": "^4.1.1", - "autoprefixer": "^10.4.17", + "autoprefixer": "^10.4.19", "concurrently": "^8.2.2", - "draft-js": "git+https://github.com/penpot/draft-js.git", + "draft-js": "git+https://github.com/penpot/draft-js.git#commit=4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0", + "express": "^4.19.2", "fancy-log": "^2.0.0", + "getopts": "^2.3.0", "gettext-parser": "^8.0.0", "gulp": "4.0.2", "gulp-concat": "^2.6.1", @@ -63,43 +64,53 @@ "gulp-sass": "^5.1.0", "gulp-sourcemaps": "^3.0.0", "gulp-svg-sprite": "^2.0.3", - "jsdom": "^24.0.0", + "jsdom": "^24.1.0", "map-stream": "0.0.7", - "marked": "^12.0.0", + "marked": "^12.0.2", "mkdirp": "^3.0.1", - "nodemon": "^3.1.0", + "mustache": "^4.2.0", + "nodemon": "^3.1.2", "npm-run-all": "^4.1.5", - "postcss": "^8.4.35", + "p-limit": "^5.0.0", + "postcss": "^8.4.38", "postcss-clean": "^1.2.2", - "prettier": "^3.2.5", + "prettier": "3.3.2", + "pretty-time": "^1.1.0", "prop-types": "^15.8.1", - "rimraf": "^5.0.5", - "sass": "^1.71.1", - "shadow-cljs": "2.27.4", - "storybook": "^7.6.17", - "typescript": "^5.3.3", + "rimraf": "^5.0.7", + "sass": "^1.77.4", + "sass-embedded": "^1.77.2", + "shadow-cljs": "2.28.11", + "storybook": "^8.2.2", + "svg-sprite": "^2.0.4", + "typescript": "^5.4.5", "vite": "^5.1.4", - "vitest": "^1.3.1" + "vitest": "^1.3.1", + "watcher": "^2.3.1", + "workerpool": "^9.1.1" }, "dependencies": { - "date-fns": "^3.3.1", + "compression": "^1.7.4", + "date-fns": "^3.6.0", "eventsource-parser": "^1.1.2", "highlight.js": "^11.9.0", "js-beautify": "^1.15.1", "jszip": "^3.10.1", + "lodash": "^4.17.21", "luxon": "^3.4.4", "mousetrap": "^1.6.5", "opentype.js": "^1.3.4", "postcss-modules": "^6.0.0", "randomcolor": "^0.6.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-error-boundary": "^4.0.13", "react-virtualized": "^9.22.5", "rxjs": "8.0.0-alpha.14", - "sax": "^1.3.0", + "sax": "^1.4.1", "source-map-support": "^0.5.21", "tdigest": "^0.1.2", - "ua-parser-js": "^1.0.37", + "ua-parser-js": "^1.0.38", "xregexp": "^5.1.1" } } diff --git a/frontend/playwright.config.js b/frontend/playwright.config.js new file mode 100644 index 0000000000000000000000000000000000000000..6196826df5f5b6becf84f4bc3bbefb5ecde7ba0b --- /dev/null +++ b/frontend/playwright.config.js @@ -0,0 +1,63 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: "./playwright", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests by default; can be overriden with --workers */ + workers: 1, + /* Timeout for expects (longer in CI) */ + expect: { + timeout: process.env.CI ? 20000 : 5000, + }, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3000", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + + locale: "en-US", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "default", + use: { ...devices["Desktop Chrome"] }, + testDir: "./playwright/ui/specs", + }, + { + name: "ds", + use: { ...devices["Desktop Chrome"] }, + testDir: "./playwright/ui/visual-specs", + expect: { + toHaveScreenshot: { maxDiffPixelRatio: 0.005 }, + }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + timeout: 2 * 60 * 1000, + command: "yarn e2e:server", + url: "http://localhost:3000", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/frontend/playwright/data/assets/get-file-fragment-with-assets-components.json b/frontend/playwright/data/assets/get-file-fragment-with-assets-components.json new file mode 100644 index 0000000000000000000000000000000000000000..053031eb5ea40eca782755c7b7fbaedb23686b9a --- /dev/null +++ b/frontend/playwright/data/assets/get-file-fragment-with-assets-components.json @@ -0,0 +1,31 @@ +{ + "~:id": "~u015fda4f-caa6-8103-8004-862a9e4b4d4b", + "~:file-id": "~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:created-at": "~m1718718436639", + "~:data": { + "~ue117f7f6-433c-807e-8004-862a38e1823d": { + "~:id": "~ue117f7f6-433c-807e-8004-862a38e1823d", + "~:name": "Button", + "~:path": "", + "~:modified-at": "~m1718718335855", + "~:main-instance-id": "~ue117f7f6-433c-807e-8004-862a38e0099a", + "~:main-instance-page": "~u015fda4f-caa6-8103-8004-862a00ddbe94" + }, + "~ue117f7f6-433c-807e-8004-862a51a90ef5": { + "~:id": "~ue117f7f6-433c-807e-8004-862a51a90ef5", + "~:name": "Badge", + "~:path": "", + "~:modified-at": "~m1718718361245", + "~:main-instance-id": "~ue117f7f6-433c-807e-8004-862a51a84a91", + "~:main-instance-page": "~u015fda4f-caa6-8103-8004-862a00ddbe94" + }, + "~ue117f7f6-433c-807e-8004-862a9b541a46": { + "~:id": "~ue117f7f6-433c-807e-8004-862a9b541a46", + "~:name": "Avatar", + "~:path": "", + "~:modified-at": "~m1718718436652", + "~:main-instance-id": "~ue117f7f6-433c-807e-8004-862a9b5374b6", + "~:main-instance-page": "~u015fda4f-caa6-8103-8004-862a00ddbe94" + } + } +} diff --git a/frontend/playwright/data/assets/get-file-fragmnet-with-assets-page.json b/frontend/playwright/data/assets/get-file-fragmnet-with-assets-page.json new file mode 100644 index 0000000000000000000000000000000000000000..bae8fd54efab22d31e89e3fd0fc479dfbbcfc483 --- /dev/null +++ b/frontend/playwright/data/assets/get-file-fragmnet-with-assets-page.json @@ -0,0 +1,630 @@ +{ + "~:id": "~u015fda4f-caa6-8103-8004-862a9e4ad279", + "~:file-id": "~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:created-at": "~m1718718436639", + "~:data": { + "~:options": {}, + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1.0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~ue117f7f6-433c-807e-8004-862a38e0099a", + "~ue117f7f6-433c-807e-8004-862a51a84a91", + "~ue117f7f6-433c-807e-8004-862a9b5374b6" + ] + } + }, + "~ue117f7f6-433c-807e-8004-862a18bba46f": { + "~#shape": { + "~:y": 220, + "~:rx": 0, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Button", + "~:width": 120, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 663, + "~:y": 220 + } + }, + { + "~#point": { + "~:x": 783, + "~:y": 220 + } + }, + { + "~#point": { + "~:x": 783, + "~:y": 274 + } + }, + { + "~#point": { + "~:x": 663, + "~:y": 274 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:constraints-v": "~:scale", + "~:constraints-h": "~:scale", + "~:id": "~ue117f7f6-433c-807e-8004-862a18bba46f", + "~:parent-id": "~ue117f7f6-433c-807e-8004-862a38e0099a", + "~:frame-id": "~ue117f7f6-433c-807e-8004-862a38e0099a", + "~:strokes": [], + "~:x": 663, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 663, + "~:y": 220, + "~:width": 120, + "~:height": 54, + "~:x1": 663, + "~:y1": 220, + "~:x2": 783, + "~:y2": 274 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:ry": 0, + "~:height": 54, + "~:flip-y": null + } + }, + "~ue117f7f6-433c-807e-8004-862a38e0099a": { + "~#shape": { + "~:y": 220, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": true, + "~:name": "Button", + "~:width": 120, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 663, + "~:y": 220 + } + }, + { + "~#point": { + "~:x": 783, + "~:y": 220 + } + }, + { + "~#point": { + "~:x": 783, + "~:y": 274 + } + }, + { + "~#point": { + "~:x": 663, + "~:y": 274 + } + } + ], + "~:component-root": true, + "~:show-content": true, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ue117f7f6-433c-807e-8004-862a38e0099a", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:component-id": "~ue117f7f6-433c-807e-8004-862a38e1823d", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 663, + "~:main-instance": true, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 663, + "~:y": 220, + "~:width": 120, + "~:height": 54, + "~:x1": 663, + "~:y1": 220, + "~:x2": 783, + "~:y2": 274 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": 54, + "~:component-file": "~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:flip-y": null, + "~:shapes": [ + "~ue117f7f6-433c-807e-8004-862a18bba46f" + ] + } + }, + "~ue117f7f6-433c-807e-8004-862a40b7caca": { + "~#shape": { + "~:y": 188, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Badge", + "~:width": 61, + "~:type": "~:circle", + "~:points": [ + { + "~#point": { + "~:x": 860, + "~:y": 188 + } + }, + { + "~#point": { + "~:x": 921, + "~:y": 188 + } + }, + { + "~#point": { + "~:x": 921, + "~:y": 247 + } + }, + { + "~#point": { + "~:x": 860, + "~:y": 247 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:constraints-v": "~:scale", + "~:constraints-h": "~:scale", + "~:id": "~ue117f7f6-433c-807e-8004-862a40b7caca", + "~:parent-id": "~ue117f7f6-433c-807e-8004-862a51a84a91", + "~:frame-id": "~ue117f7f6-433c-807e-8004-862a51a84a91", + "~:strokes": [], + "~:x": 860, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 860, + "~:y": 188, + "~:width": 61, + "~:height": 59, + "~:x1": 860, + "~:y1": 188, + "~:x2": 921, + "~:y2": 247 + } + }, + "~:fills": [ + { + "~:fill-color": "#7798ff", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 59, + "~:flip-y": null + } + }, + "~ue117f7f6-433c-807e-8004-862a51a84a91": { + "~#shape": { + "~:y": 188, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": true, + "~:name": "Badge", + "~:width": 61, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 860, + "~:y": 188 + } + }, + { + "~#point": { + "~:x": 921, + "~:y": 188 + } + }, + { + "~#point": { + "~:x": 921, + "~:y": 247 + } + }, + { + "~#point": { + "~:x": 860, + "~:y": 247 + } + } + ], + "~:component-root": true, + "~:show-content": true, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ue117f7f6-433c-807e-8004-862a51a84a91", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:component-id": "~ue117f7f6-433c-807e-8004-862a51a90ef5", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 860, + "~:main-instance": true, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 860, + "~:y": 188, + "~:width": 61, + "~:height": 59, + "~:x1": 860, + "~:y1": 188, + "~:x2": 921, + "~:y2": 247 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": 59, + "~:component-file": "~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:flip-y": null, + "~:shapes": [ + "~ue117f7f6-433c-807e-8004-862a40b7caca" + ] + } + }, + "~ue117f7f6-433c-807e-8004-862a8c166257": { + "~#shape": { + "~:y": 97, + "~:rx": 0, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Avatar", + "~:width": 66, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 554, + "~:y": 97 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 97 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 163 + } + }, + { + "~#point": { + "~:x": 554, + "~:y": 163 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:constraints-v": "~:scale", + "~:constraints-h": "~:scale", + "~:id": "~ue117f7f6-433c-807e-8004-862a8c166257", + "~:parent-id": "~ue117f7f6-433c-807e-8004-862a9b5374b6", + "~:frame-id": "~ue117f7f6-433c-807e-8004-862a9b5374b6", + "~:strokes": [], + "~:x": 554, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 554, + "~:y": 97, + "~:width": 66, + "~:height": 66, + "~:x1": 554, + "~:y1": 97, + "~:x2": 620, + "~:y2": 163 + } + }, + "~:fills": [ + { + "~:fill-color": "#ff6ffc", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:ry": 0, + "~:height": 66, + "~:flip-y": null + } + }, + "~ue117f7f6-433c-807e-8004-862a9b5374b6": { + "~#shape": { + "~:y": 97, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": true, + "~:name": "Avatar", + "~:width": 66, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 554, + "~:y": 97 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 97 + } + }, + { + "~#point": { + "~:x": 620, + "~:y": 163 + } + }, + { + "~#point": { + "~:x": 554, + "~:y": 163 + } + } + ], + "~:component-root": true, + "~:show-content": true, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ue117f7f6-433c-807e-8004-862a9b5374b6", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:component-id": "~ue117f7f6-433c-807e-8004-862a9b541a46", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 554, + "~:main-instance": true, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 554, + "~:y": 97, + "~:width": 66, + "~:height": 66, + "~:x1": 554, + "~:y1": 97, + "~:x2": 620, + "~:y2": 163 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": 66, + "~:component-file": "~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:flip-y": null, + "~:shapes": [ + "~ue117f7f6-433c-807e-8004-862a8c166257" + ] + } + } + }, + "~:id": "~u015fda4f-caa6-8103-8004-862a00ddbe94", + "~:name": "Page 1" + } +} diff --git a/frontend/playwright/data/assets/get-file-with-assets.json b/frontend/playwright/data/assets/get-file-with-assets.json new file mode 100644 index 0000000000000000000000000000000000000000..29758d1a099e0c3260f8b27d2aec362d63f4cbe8 --- /dev/null +++ b/frontend/playwright/data/assets/get-file-with-assets.json @@ -0,0 +1,105 @@ +{ + "~:features":{ + "~#set":[ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions":{ + "~:type":"~:membership", + "~:is-owner":true, + "~:is-admin":true, + "~:can-edit":true, + "~:can-read":true, + "~:is-logged":true + }, + "~:has-media-trimmed":false, + "~:comment-thread-seqn":0, + "~:name":"Lorem ipsum", + "~:revn":14, + "~:modified-at":"~m1718718464651", + "~:id":"~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:is-shared":false, + "~:version":49, + "~:project-id":"~u0515a066-e303-8169-8004-73eb401b5d55", + "~:created-at":"~m1718718275492", + "~:data":{ + "~:colors":{ + "~ue117f7f6-433c-807e-8004-862aa7732f9c":{ + "~:path":"", + "~:color":"#ff6ffc", + "~:name":"Rosita", + "~:modified-at":"~m1718718452317", + "~:opacity":1, + "~:id":"~ue117f7f6-433c-807e-8004-862aa7732f9c" + }, + "~ue117f7f6-433c-807e-8004-862ab306fa2b":{ + "~:path":"", + "~:color":"#7798ff", + "~:name":"#7798ff", + "~:modified-at":"~m1718718461420", + "~:opacity":1, + "~:id":"~ue117f7f6-433c-807e-8004-862ab306fa2b" + } + }, + "~:typographies":{ + "~ue117f7f6-433c-807e-8004-862ab6ae29d8":{ + "~:line-height":"1.2", + "~:font-style":"normal", + "~:text-transform":"none", + "~:font-id":"sourcesanspro", + "~:font-size":"14", + "~:font-weight":"400", + "~:name":"Source Sans Pro Regular", + "~:modified-at":"~m1718718464655", + "~:font-variant-id":"regular", + "~:id":"~ue117f7f6-433c-807e-8004-862ab6ae29d8", + "~:letter-spacing":"0", + "~:font-family":"sourcesanspro" + } + }, + "~:pages":[ + "~u015fda4f-caa6-8103-8004-862a00ddbe94" + ], + "~:components":{ + "~#penpot/pointer":[ + "~u015fda4f-caa6-8103-8004-862a9e4b4d4b", + { + "~:created-at":"~m1718718436653" + } + ] + }, + "~:id":"~u015fda4f-caa6-8103-8004-862a00dd4f31", + "~:options":{ + "~:components-v2":true + }, + "~:recent-colors":[ + { + "~:color":"#b5b1b4", + "~:opacity":1 + }, + { + "~:color":"#ff6ffc", + "~:opacity":1 + }, + { + "~:color":"#7798ff", + "~:opacity":1 + } + ], + "~:pages-index":{ + "~u015fda4f-caa6-8103-8004-862a00ddbe94":{ + "~#penpot/pointer":[ + "~u015fda4f-caa6-8103-8004-862a9e4ad279", + { + "~:created-at":"~m1718718436653" + } + ] + } + } + } +} \ No newline at end of file diff --git a/frontend/playwright/data/dashboard/create-access-token.json b/frontend/playwright/data/dashboard/create-access-token.json new file mode 100644 index 0000000000000000000000000000000000000000..395e5a1a990420121a556451fe1eb6abf71a1720 --- /dev/null +++ b/frontend/playwright/data/dashboard/create-access-token.json @@ -0,0 +1,8 @@ +{ + "~:id": "~u62edaeb8-e212-81ca-8004-80a6f8a42e8e", + "~:profile-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:created-at": "~m1718348381840", + "~:updated-at": "~m1718348381840", + "~:name": "new token", + "~:token": "eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIn0.9aFN5YdOI-b-NQPos5uqF8J8b9iMyeri3yYhV5FlHuhNbRwk0YuftA.Dygx9O5-KsAHpuqD.ryTDCqelYOk1XYflTlDGFlzG8VLuElKHSGHdJyJvWqcCUANWzl8cVvezvU2GWg1Piin21KNrcV0TEcHPpDggySRbTn01MOIjw3vTVHdGrlHaVq5VpnWb5hCfs_P9kF7Y2IWOa4da4mM.IulvBQUllnay7clORd-NSg" +} diff --git a/frontend/playwright/data/dashboard/create-project.json b/frontend/playwright/data/dashboard/create-project.json new file mode 100644 index 0000000000000000000000000000000000000000..92566a65fbec64b0f024b6d03b81819a67794bc9 --- /dev/null +++ b/frontend/playwright/data/dashboard/create-project.json @@ -0,0 +1,9 @@ +{ + "~:id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6", + "~:created-at": "~m1715266551088", + "~:modified-at": "~m1715266551088", + "~:is-default": false, + "~:name": "New Project 1", + "~:is-pinned": false +} diff --git a/frontend/playwright/data/dashboard/get-access-tokens-empty.json b/frontend/playwright/data/dashboard/get-access-tokens-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..fe51488c7066f6687ef680d6bfaa4f7768ef205c --- /dev/null +++ b/frontend/playwright/data/dashboard/get-access-tokens-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-access-tokens.json b/frontend/playwright/data/dashboard/get-access-tokens.json new file mode 100644 index 0000000000000000000000000000000000000000..ec296ea8abf22235c1e95e183c8744dae1628217 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-access-tokens.json @@ -0,0 +1,8 @@ +[ + { + "~:id": "~u62edaeb8-e212-81ca-8004-80a6f8a42e8e", + "~:name": "new token", + "~:created-at": "~m1718348381840", + "~:updated-at": "~m1718348381840" + } +] diff --git a/frontend/playwright/data/dashboard/get-font-variants.json b/frontend/playwright/data/dashboard/get-font-variants.json new file mode 100644 index 0000000000000000000000000000000000000000..6a16ec574f17c1c1dc311fdfb21dda0969e17ce5 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-font-variants.json @@ -0,0 +1,15 @@ +[ + { + "~:font-style": "normal", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:font-id": "~u838cda51-c50f-8032-8004-6ac92ea6eaea", + "~:font-weight": 400, + "~:ttf-file-id": "~ue3710e43-7e40-405d-a4ea-8bb85443d44b", + "~:modified-at": "~m1716880956479", + "~:otf-file-id": "~u72bd3cda-478a-4e0e-a372-4a4f7cdc1371", + "~:id": "~u28f4b65f-3667-8087-8004-6ac93050433a", + "~:woff1-file-id": "~ua4c0a056-2eb6-47cc-bf80-3115d14e048d", + "~:created-at": "~m1716880956479", + "~:font-family": "Milligram Variable Trial" + } +] diff --git a/frontend/playwright/data/dashboard/get-project-files-empty.json b/frontend/playwright/data/dashboard/get-project-files-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..fe51488c7066f6687ef680d6bfaa4f7768ef205c --- /dev/null +++ b/frontend/playwright/data/dashboard/get-project-files-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-project-files.json b/frontend/playwright/data/dashboard/get-project-files.json new file mode 100644 index 0000000000000000000000000000000000000000..b0394aff1f959763180d98990443f9f5e0271623 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-project-files.json @@ -0,0 +1,20 @@ +[ + { + "~:id": "~u8b479b80-e02d-8074-8004-4088dc6bfd11", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1714045521389", + "~:modified-at": "~m1714045654874", + "~:name": "New File 2", + "~:revn": 1, + "~:is-shared": false + }, + { + "~:id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:name": "New File 1", + "~:revn": 1, + "~:is-shared": false + } +] diff --git a/frontend/playwright/data/dashboard/get-projects-full.json b/frontend/playwright/data/dashboard/get-projects-full.json new file mode 100644 index 0000000000000000000000000000000000000000..491351e86cd583a70f7df2938e43eacd4c18dd71 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-projects-full.json @@ -0,0 +1,19 @@ +[{ + "~:id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1715266551088", + "~:modified-at": "~m1715266551088", + "~:is-default": false, + "~:name": "New Project 1", + "~:is-pinned": false, + "~:count": 1 +}, +{ + "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116382", + "~:modified-at": "~m1713873823633", + "~:is-default": true, + "~:name": "Drafts", + "~:count": 1 +}] diff --git a/frontend/playwright/data/dashboard/get-projects-new.json b/frontend/playwright/data/dashboard/get-projects-new.json new file mode 100644 index 0000000000000000000000000000000000000000..47c85eee4118dfd3179bfed0b1bddbe3840ab1dd --- /dev/null +++ b/frontend/playwright/data/dashboard/get-projects-new.json @@ -0,0 +1,18 @@ +[{ + "~:id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1715266551088", + "~:modified-at": "~m1715266551088", + "~:is-default": false, + "~:name": "New Project 1", + "~:is-pinned": false, + "~:count": 0 +}, +{ + "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116382", + "~:modified-at": "~m1713873823633", + "~:is-default": true, + "~:name": "Drafts" +}] diff --git a/frontend/playwright/data/dashboard/get-shared-files-empty.json b/frontend/playwright/data/dashboard/get-shared-files-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..fe51488c7066f6687ef680d6bfaa4f7768ef205c --- /dev/null +++ b/frontend/playwright/data/dashboard/get-shared-files-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-shared-files.json b/frontend/playwright/data/dashboard/get-shared-files.json new file mode 100644 index 0000000000000000000000000000000000000000..3fffa07f454ad02b3d0b51fe81de6e2c0dcf3237 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-shared-files.json @@ -0,0 +1,219 @@ +{ + "~#set": [ + { + "~:name": "New File 3", + "~:revn": 1, + "~:id": "~u28f4b65f-3667-8087-8004-69eca173cc07", + "~:is-shared": true, + "~:project-id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:library-summary": { + "~:components": { + "~:count": 1, + "~:sample": [ + { + "~:id": "~ua30724ae-f8d8-8003-8004-69ecacfc8a4c", + "~:name": "Rectangle", + "~:path": "", + "~:modified-at": "~m1716823150739", + "~:main-instance-id": "~ua30724ae-f8d8-8003-8004-69ecacfa2045", + "~:main-instance-page": "~u28f4b65f-3667-8087-8004-69eca173cc08", + "~:objects": { + "~ua30724ae-f8d8-8003-8004-69ecacfa2045": { + "~#shape": { + "~:y": 168, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": true, + "~:name": "Rectangle", + "~:width": 553, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 481, + "~:y": 168 + } + }, + { + "~#point": { + "~:x": 1034, + "~:y": 168 + } + }, + { + "~#point": { + "~:x": 1034, + "~:y": 550 + } + }, + { + "~#point": { + "~:x": 481, + "~:y": 550 + } + } + ], + "~:component-root": true, + "~:show-content": true, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ua30724ae-f8d8-8003-8004-69ecacfa2045", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:component-id": "~ua30724ae-f8d8-8003-8004-69ecacfc8a4c", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 481, + "~:main-instance": true, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 481, + "~:y": 168, + "~:width": 553, + "~:height": 382, + "~:x1": 481, + "~:y1": 168, + "~:x2": 1034, + "~:y2": 550 + } + }, + "~:fills": [], + "~:flip-x": null, + "~:height": 382, + "~:component-file": "~u28f4b65f-3667-8087-8004-69eca173cc07", + "~:flip-y": null, + "~:shapes": [ + "~ua30724ae-f8d8-8003-8004-69eca9b27c8c" + ] + } + }, + "~ua30724ae-f8d8-8003-8004-69eca9b27c8c": { + "~#shape": { + "~:y": 168, + "~:rx": 0, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 553, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 481, + "~:y": 168 + } + }, + { + "~#point": { + "~:x": 1034, + "~:y": 168 + } + }, + { + "~#point": { + "~:x": 1034, + "~:y": 550 + } + }, + { + "~#point": { + "~:x": 481, + "~:y": 550 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:constraints-v": "~:scale", + "~:constraints-h": "~:scale", + "~:id": "~ua30724ae-f8d8-8003-8004-69eca9b27c8c", + "~:parent-id": "~ua30724ae-f8d8-8003-8004-69ecacfa2045", + "~:frame-id": "~ua30724ae-f8d8-8003-8004-69ecacfa2045", + "~:strokes": [], + "~:x": 481, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 481, + "~:y": 168, + "~:width": 553, + "~:height": 382, + "~:x1": 481, + "~:y1": 168, + "~:x2": 1034, + "~:y2": 550 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:ry": 0, + "~:height": 382, + "~:flip-y": null + } + } + } + } + ] + }, + "~:media": { + "~:count": 0, + "~:sample": [] + }, + "~:colors": { + "~:count": 0, + "~:sample": [] + }, + "~:typographies": { + "~:count": 0, + "~:sample": [] + } + } + } + ] +} diff --git a/frontend/playwright/data/dashboard/get-team-invitations-empty.json b/frontend/playwright/data/dashboard/get-team-invitations-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..fe51488c7066f6687ef680d6bfaa4f7768ef205c --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-invitations-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-team-invitations.json b/frontend/playwright/data/dashboard/get-team-invitations.json new file mode 100644 index 0000000000000000000000000000000000000000..f7ac7754380d649176c2b76d78a6456060fb0b67 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-invitations.json @@ -0,0 +1,6 @@ +[ + { "~:email": "test1@mail.com", "~:role": "~:editor", "~:expired": true }, + { "~:email": "test2@mail.com", "~:role": "~:editor", "~:expired": false }, + { "~:email": "test3@mail.com", "~:role": "~:admin", "~:expired": true }, + { "~:email": "test4@mail.com", "~:role": "~:admin", "~:expired": false } +] diff --git a/frontend/playwright/data/dashboard/get-team-members.json b/frontend/playwright/data/dashboard/get-team-members.json new file mode 100644 index 0000000000000000000000000000000000000000..a869d5e34aea2a925531af1096333b30505a2361 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-members.json @@ -0,0 +1,16 @@ +[ + { + "~:is-admin": true, + "~:email": "foo@example.com", + "~:team-id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3", + "~:name": "Princesa Leia", + "~:fullname": "Princesa Leia", + "~:is-owner": false, + "~:modified-at": "~m1713533116365", + "~:can-edit": true, + "~:is-active": true, + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:profile-id": "~uf56647eb-19a7-8115-8003-b6bc939ecd1b", + "~:created-at": "~m1713533116365" + } +] diff --git a/frontend/playwright/data/dashboard/get-team-recent-files.json b/frontend/playwright/data/dashboard/get-team-recent-files.json new file mode 100644 index 0000000000000000000000000000000000000000..920bb2df006c75dac9ff5faed78d2b3e5fccce26 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-recent-files.json @@ -0,0 +1,29 @@ +[ + { + "~:id": "~u8b479b80-e02d-8074-8004-4088dc6bfd11", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1714045521389", + "~:modified-at": "~m1714045654874", + "~:name": "New File 2", + "~:revn": 1, + "~:is-shared": false + }, + { + "~:id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:name": "New File 1", + "~:revn": 1, + "~:is-shared": false + }, + { + "~:id": "~u28f4b65f-3667-8087-8004-69eca173cc07", + "~:project-id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:name": "New File 3", + "~:revn": 1, + "~:is-shared": true + } +] diff --git a/frontend/playwright/data/dashboard/get-team-stats.json b/frontend/playwright/data/dashboard/get-team-stats.json new file mode 100644 index 0000000000000000000000000000000000000000..c984f1021f4fc4db390a6b997cfa8577df00b336 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-team-stats.json @@ -0,0 +1 @@ +{"~:projects":1,"~:files":3} diff --git a/frontend/playwright/data/dashboard/get-webhooks-empty.json b/frontend/playwright/data/dashboard/get-webhooks-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..fe51488c7066f6687ef680d6bfaa4f7768ef205c --- /dev/null +++ b/frontend/playwright/data/dashboard/get-webhooks-empty.json @@ -0,0 +1 @@ +[] diff --git a/frontend/playwright/data/dashboard/get-webhooks.json b/frontend/playwright/data/dashboard/get-webhooks.json new file mode 100644 index 0000000000000000000000000000000000000000..3849e360845cd2b26aebdb222a2abc0d26b188a5 --- /dev/null +++ b/frontend/playwright/data/dashboard/get-webhooks.json @@ -0,0 +1,20 @@ +[ + { + "~:id": "~u29ce7ec9-e75d-81b4-8004-08100373558a", + "~:uri": { + "~#uri": "https://www.abc.es" + }, + "~:mtype": "application/json", + "~:is-active": false, + "~:error-count": 0 + }, + { + "~:id": "~u43d6b3b1-40f7-807b-8003-f9846292b4c7", + "~:uri": { + "~#uri": "https://www.google.com" + }, + "~:mtype": "application/json", + "~:is-active": true, + "~:error-count": 0 + } +] diff --git a/frontend/playwright/data/dashboard/search-files-empty.json b/frontend/playwright/data/dashboard/search-files-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/frontend/playwright/data/dashboard/search-files.json b/frontend/playwright/data/dashboard/search-files.json new file mode 100644 index 0000000000000000000000000000000000000000..920bb2df006c75dac9ff5faed78d2b3e5fccce26 --- /dev/null +++ b/frontend/playwright/data/dashboard/search-files.json @@ -0,0 +1,29 @@ +[ + { + "~:id": "~u8b479b80-e02d-8074-8004-4088dc6bfd11", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1714045521389", + "~:modified-at": "~m1714045654874", + "~:name": "New File 2", + "~:revn": 1, + "~:is-shared": false + }, + { + "~:id": "~u95d6fdd8-48d8-8148-8004-38af910d2dbe", + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:name": "New File 1", + "~:revn": 1, + "~:is-shared": false + }, + { + "~:id": "~u28f4b65f-3667-8087-8004-69eca173cc07", + "~:project-id": "~ue5a24d1b-ef1e-812f-8004-52bab84be6f7", + "~:created-at": "~m1713518796912", + "~:modified-at": "~m1713519762931", + "~:name": "New File 3", + "~:revn": 1, + "~:is-shared": true + } +] diff --git a/frontend/playwright/data/design/get-file-fragment-multiple-constraints.json b/frontend/playwright/data/design/get-file-fragment-multiple-constraints.json new file mode 100644 index 0000000000000000000000000000000000000000..0fe5f6a2c509a07fac5892e73b3a4591d8c86759 --- /dev/null +++ b/frontend/playwright/data/design/get-file-fragment-multiple-constraints.json @@ -0,0 +1,363 @@ +{ + "~:id": "~u03bff843-920f-81a1-8004-7563acdc8ca1", + "~:file-id": "~u03bff843-920f-81a1-8004-756365e1eb6a", + "~:created-at": "~m1717592543081", + "~:data": { + "~:options": {}, + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1.0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~ub574c052-1a31-80bb-8004-75636879759b" + ] + } + }, + "~ub574c052-1a31-80bb-8004-75636879759b": { + "~#shape": { + "~:y": 128, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Board", + "~:width": 256, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 128, + "~:y": 128 + } + }, + { + "~#point": { + "~:x": 384, + "~:y": 128 + } + }, + { + "~#point": { + "~:x": 384, + "~:y": 384 + } + }, + { + "~#point": { + "~:x": 128, + "~:y": 384 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ub574c052-1a31-80bb-8004-75636879759b", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 128, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 128, + "~:y": 128, + "~:width": 256, + "~:height": 256, + "~:x1": 128, + "~:y1": 128, + "~:x2": 384, + "~:y2": 384 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 256, + "~:flip-y": null, + "~:shapes": [ + "~ub574c052-1a31-80bb-8004-75636a9b8205", + "~ub574c052-1a31-80bb-8004-756392461069" + ] + } + }, + "~ub574c052-1a31-80bb-8004-75636a9b8205": { + "~#shape": { + "~:y": 136, + "~:rx": 0, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 64, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 136, + "~:y": 136 + } + }, + { + "~#point": { + "~:x": 200, + "~:y": 136 + } + }, + { + "~#point": { + "~:x": 200, + "~:y": 199.99999999999997 + } + }, + { + "~#point": { + "~:x": 136, + "~:y": 199.99999999999997 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~ub574c052-1a31-80bb-8004-75636a9b8205", + "~:parent-id": "~ub574c052-1a31-80bb-8004-75636879759b", + "~:frame-id": "~ub574c052-1a31-80bb-8004-75636879759b", + "~:strokes": [], + "~:x": 136, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 136, + "~:y": 136, + "~:width": 64, + "~:height": 63.99999999999997, + "~:x1": 136, + "~:y1": 136, + "~:x2": 200, + "~:y2": 199.99999999999997 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:ry": 0, + "~:height": 63.99999999999997, + "~:flip-y": null + } + }, + "~ub574c052-1a31-80bb-8004-756392461069": { + "~#shape": { + "~:y": 136, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Ellipse", + "~:width": 64, + "~:type": "~:circle", + "~:points": [ + { + "~#point": { + "~:x": 256, + "~:y": 136 + } + }, + { + "~#point": { + "~:x": 320, + "~:y": 136 + } + }, + { + "~#point": { + "~:x": 320, + "~:y": 200 + } + }, + { + "~#point": { + "~:x": 256, + "~:y": 200 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:constraints-v": "~:bottom", + "~:constraints-h": "~:right", + "~:id": "~ub574c052-1a31-80bb-8004-756392461069", + "~:parent-id": "~ub574c052-1a31-80bb-8004-75636879759b", + "~:frame-id": "~ub574c052-1a31-80bb-8004-75636879759b", + "~:strokes": [], + "~:x": 256, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 256, + "~:y": 136, + "~:width": 64, + "~:height": 64, + "~:x1": 256, + "~:y1": 136, + "~:x2": 320, + "~:y2": 200 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 64, + "~:flip-y": null + } + } + }, + "~:id": "~u03bff843-920f-81a1-8004-756365e1eb6b", + "~:name": "Page 1" + } +} diff --git a/frontend/playwright/data/design/get-file-multiple-attributes.json b/frontend/playwright/data/design/get-file-multiple-attributes.json new file mode 100644 index 0000000000000000000000000000000000000000..c0a67da95ce315c5b4e5c02b08937c2aff353822 --- /dev/null +++ b/frontend/playwright/data/design/get-file-multiple-attributes.json @@ -0,0 +1,343 @@ +{ + "~:features":{ + "~#set":[ + "layout/grid", + "styles/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions":{ + "~:type":"~:membership", + "~:is-owner":true, + "~:is-admin":true, + "~:can-edit":true, + "~:can-read":true, + "~:is-logged":true + }, + "~:has-media-trimmed":false, + "~:comment-thread-seqn":0, + "~:name":"New File 12", + "~:revn":2, + "~:modified-at":"~m1718012938567", + "~:id":"~u1795a568-0df0-8095-8004-7ba741f56be2", + "~:is-shared":false, + "~:version":48, + "~:project-id":"~u4dc640b0-5cbf-11ec-a7c5-91e9eb4f238d", + "~:created-at":"~m1718012912598", + "~:data":{ + "~:pages":[ + "~u1795a568-0df0-8095-8004-7ba741f56be3" + ], + "~:pages-index":{ + "~u1795a568-0df0-8095-8004-7ba741f56be3":{ + "~:options":{ + + }, + "~:objects":{ + "~u00000000-0000-0000-0000-000000000000":{ + "~#shape":{ + "~:y":0, + "~:hide-fill-on-export":false, + "~:transform":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:rotation":0, + "~:name":"Root Frame", + "~:width":0.01, + "~:type":"~:frame", + "~:points":[ + { + "~#point":{ + "~:x":0, + "~:y":0 + } + }, + { + "~#point":{ + "~:x":0.01, + "~:y":0 + } + }, + { + "~#point":{ + "~:x":0.01, + "~:y":0.01 + } + }, + { + "~#point":{ + "~:x":0, + "~:y":0.01 + } + } + ], + "~:proportion-lock":false, + "~:transform-inverse":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:id":"~u00000000-0000-0000-0000-000000000000", + "~:parent-id":"~u00000000-0000-0000-0000-000000000000", + "~:frame-id":"~u00000000-0000-0000-0000-000000000000", + "~:strokes":[ + + ], + "~:x":0, + "~:proportion":1.0, + "~:selrect":{ + "~#rect":{ + "~:x":0, + "~:y":0, + "~:width":0.01, + "~:height":0.01, + "~:x1":0, + "~:y1":0, + "~:x2":0.01, + "~:y2":0.01 + } + }, + "~:fills":[ + { + "~:fill-color":"#FFFFFF", + "~:fill-opacity":1 + } + ], + "~:flip-x":null, + "~:height":0.01, + "~:flip-y":null, + "~:shapes":[ + "~u2ace9ce8-8e01-8086-8004-7ba745d4305a", + "~u2ace9ce8-8e01-8086-8004-7ba748566e02" + ] + } + }, + "~u2ace9ce8-8e01-8086-8004-7ba745d4305a":{ + "~#shape":{ + "~:y":221, + "~:rx":0, + "~:transform":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:rotation":0, + "~:grow-type":"~:fixed", + "~:hide-in-viewer":false, + "~:name":"Rectangle", + "~:width":105, + "~:type":"~:rect", + "~:points":[ + { + "~#point":{ + "~:x":165, + "~:y":221 + } + }, + { + "~#point":{ + "~:x":270, + "~:y":221 + } + }, + { + "~#point":{ + "~:x":270, + "~:y":316 + } + }, + { + "~#point":{ + "~:x":165, + "~:y":316 + } + } + ], + "~:proportion-lock":false, + "~:transform-inverse":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:id":"~u2ace9ce8-8e01-8086-8004-7ba745d4305a", + "~:parent-id":"~u00000000-0000-0000-0000-000000000000", + "~:frame-id":"~u00000000-0000-0000-0000-000000000000", + "~:strokes":[ + + ], + "~:x":165, + "~:proportion":1, + "~:selrect":{ + "~#rect":{ + "~:x":165, + "~:y":221, + "~:width":105, + "~:height":95, + "~:x1":165, + "~:y1":221, + "~:x2":270, + "~:y2":316 + } + }, + "~:fills":[ + { + "~:fill-color":"#B1B2B5", + "~:fill-opacity":1 + } + ], + "~:flip-x":null, + "~:ry":0, + "~:height":95, + "~:flip-y":null + } + }, + "~u2ace9ce8-8e01-8086-8004-7ba748566e02":{ + "~#shape":{ + "~:y":228, + "~:transform":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:rotation":0, + "~:grow-type":"~:fixed", + "~:hide-in-viewer":false, + "~:name":"Ellipse", + "~:width":85, + "~:type":"~:circle", + "~:points":[ + { + "~#point":{ + "~:x":344, + "~:y":228 + } + }, + { + "~#point":{ + "~:x":429, + "~:y":228 + } + }, + { + "~#point":{ + "~:x":429, + "~:y":308 + } + }, + { + "~#point":{ + "~:x":344, + "~:y":308 + } + } + ], + "~:proportion-lock":false, + "~:transform-inverse":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:blur":{ + "~:id":"~u2ace9ce8-8e01-8086-8004-7ba757cdd271", + "~:type":"~:layer-blur", + "~:value":4, + "~:hidden":false + }, + "~:id":"~u2ace9ce8-8e01-8086-8004-7ba748566e02", + "~:parent-id":"~u00000000-0000-0000-0000-000000000000", + "~:frame-id":"~u00000000-0000-0000-0000-000000000000", + "~:strokes":[ + { + "~:stroke-alignment":"~:inner", + "~:stroke-style":"~:solid", + "~:stroke-color":"#000000", + "~:stroke-opacity":1, + "~:stroke-width":1 + } + ], + "~:x":344, + "~:proportion":1, + "~:shadow":[ + { + "~:color":{ + "~:color":"#000000", + "~:opacity":0.2 + }, + "~:spread":0, + "~:offset-y":4, + "~:style":"~:drop-shadow", + "~:blur":4, + "~:hidden":false, + "~:id":"~u2ace9ce8-8e01-8086-8004-7ba756ddebd5", + "~:offset-x":4 + } + ], + "~:selrect":{ + "~#rect":{ + "~:x":344, + "~:y":228, + "~:width":85, + "~:height":80, + "~:x1":344, + "~:y1":228, + "~:x2":429, + "~:y2":308 + } + }, + "~:fills":[ + { + "~:fill-color":"#1247e7", + "~:fill-opacity":1 + } + ], + "~:flip-x":null, + "~:height":80, + "~:flip-y":null + } + } + }, + "~:id":"~u1795a568-0df0-8095-8004-7ba741f56be3", + "~:name":"Page 1" + } + }, + "~:id":"~u1795a568-0df0-8095-8004-7ba741f56be2", + "~:recent-colors":[ + { + "~:color":"#1247e7", + "~:opacity":1 + } + ] + } +} \ No newline at end of file diff --git a/frontend/playwright/data/design/get-file-multiple-constraints.json b/frontend/playwright/data/design/get-file-multiple-constraints.json new file mode 100644 index 0000000000000000000000000000000000000000..76edd0baf0c9bb7f12c8d098a7e1c67ecc9b99b4 --- /dev/null +++ b/frontend/playwright/data/design/get-file-multiple-constraints.json @@ -0,0 +1,49 @@ +{ + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 2", + "~:revn": 9, + "~:modified-at": "~m1717592543083", + "~:id": "~u03bff843-920f-81a1-8004-756365e1eb6a", + "~:is-shared": false, + "~:version": 48, + "~:project-id": "~u0515a066-e303-8169-8004-73eb401b5d55", + "~:created-at": "~m1717592470408", + "~:data": { + "~:pages": [ + "~u03bff843-920f-81a1-8004-756365e1eb6b" + ], + "~:pages-index": { + "~u03bff843-920f-81a1-8004-756365e1eb6b": { + "~#penpot/pointer": [ + "~u03bff843-920f-81a1-8004-7563acdc8ca1", + { + "~:created-at": "~m1717592543090" + } + ] + } + }, + "~:id": "~u03bff843-920f-81a1-8004-756365e1eb6a", + "~:options": { + "~:components-v2": true + } + } +} \ No newline at end of file diff --git a/frontend/playwright/data/design/get-file-object-thumbnails-multiple-attributes.json b/frontend/playwright/data/design/get-file-object-thumbnails-multiple-attributes.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/frontend/playwright/data/design/get-file-object-thumbnails-multiple-attributes.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/design/get-file-object-thumbnails-multiple-constraints.json b/frontend/playwright/data/design/get-file-object-thumbnails-multiple-constraints.json new file mode 100644 index 0000000000000000000000000000000000000000..f3a0d6d27408003e8e3633791a6f424d42e5dffa --- /dev/null +++ b/frontend/playwright/data/design/get-file-object-thumbnails-multiple-constraints.json @@ -0,0 +1,3 @@ +{ + "03bff843-920f-81a1-8004-756365e1eb6a/03bff843-920f-81a1-8004-756365e1eb6b/b574c052-1a31-80bb-8004-75636879759b/frame": "http://localhost:3449/assets/by-id/bdc9e592-f685-4b08-9a44-127ce20efee6" +} \ No newline at end of file diff --git a/frontend/playwright/data/get-profile-anonymous.json b/frontend/playwright/data/get-profile-anonymous.json new file mode 100644 index 0000000000000000000000000000000000000000..3c9c6a6325ff33eb6f3abbe8b8b325632188bbce --- /dev/null +++ b/frontend/playwright/data/get-profile-anonymous.json @@ -0,0 +1,4 @@ +{ + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:fullname": "Anonymous User" +} diff --git a/frontend/playwright/data/logged-in-user/get-built-in-templates-empty.json b/frontend/playwright/data/logged-in-user/get-built-in-templates-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-built-in-templates-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-builtin-templates-empty.json b/frontend/playwright/data/logged-in-user/get-builtin-templates-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-builtin-templates-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-font-variants-empty.json b/frontend/playwright/data/logged-in-user/get-font-variants-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-font-variants-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-profile-logged-in-no-onboarding.json b/frontend/playwright/data/logged-in-user/get-profile-logged-in-no-onboarding.json new file mode 100644 index 0000000000000000000000000000000000000000..0b416835f705416d9477b6a339656e6bb3262d74 --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-profile-logged-in-no-onboarding.json @@ -0,0 +1,26 @@ +{ + "~:email": "foo@example.com", + "~:is-demo": false, + "~:auth-backend": "penpot", + "~:fullname": "Princesa Leia", + "~:modified-at": "~m1713533116365", + "~:is-active": true, + "~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:is-muted": false, + "~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116365", + "~:is-blocked": false, + "~:props": { + "~:nudge": { + "~:big": 10, + "~:small": 1 + }, + "~:v2-info-shown": true, + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false, + "~:onboarding-viewed": true, + "~:builtin-templates-collapsed-status": + true + } +} diff --git a/frontend/playwright/data/logged-in-user/get-profile-logged-in.json b/frontend/playwright/data/logged-in-user/get-profile-logged-in.json new file mode 100644 index 0000000000000000000000000000000000000000..8c1ae192bbb94a4a39c5668a3cfb5dfda7e3e254 --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-profile-logged-in.json @@ -0,0 +1,23 @@ +{ + "~:email": "foo@example.com", + "~:is-demo": false, + "~:auth-backend": "penpot", + "~:fullname": "Princesa Leia", + "~:modified-at": "~m1713533116365", + "~:is-active": true, + "~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:is-muted": false, + "~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116365", + "~:is-blocked": false, + "~:props": { + "~:nudge": { + "~:big": 10, + "~:small": 1 + }, + "~:v2-info-shown": true, + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false + } +} \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-profiles-for-file-comments-empty.json b/frontend/playwright/data/logged-in-user/get-profiles-for-file-comments-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-profiles-for-file-comments-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-projects-default.json b/frontend/playwright/data/logged-in-user/get-projects-default.json new file mode 100644 index 0000000000000000000000000000000000000000..0d9313b1cbff6facdc1220dd0ad6e20392036e1f --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-projects-default.json @@ -0,0 +1,12 @@ +[ + { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116382", + "~:modified-at": "~m1713533116382", + "~:is-default": true, + "~:name": "Drafts", + "~:is-pinned": false, + "~:count": 0 + } +] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-team-members-your-penpot.json b/frontend/playwright/data/logged-in-user/get-team-members-your-penpot.json new file mode 100644 index 0000000000000000000000000000000000000000..3a0aa3ce7753317ba9ac1df3bb4b1494654d0d95 --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-team-members-your-penpot.json @@ -0,0 +1,16 @@ +[ + { + "~:is-admin": true, + "~:email": "foo@example.com", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:name": "Princesa Leia", + "~:fullname": "Princesa Leia", + "~:is-owner": true, + "~:modified-at": "~m1713533116388", + "~:can-edit": true, + "~:is-active": true, + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:profile-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:created-at": "~m1713533116388" + } +] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-team-recent-files-empty.json b/frontend/playwright/data/logged-in-user/get-team-recent-files-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-team-recent-files-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-team-users-single-user.json b/frontend/playwright/data/logged-in-user/get-team-users-single-user.json new file mode 100644 index 0000000000000000000000000000000000000000..eff03c1de4fb4aa8f6404ed7c522bbbca566e3fe --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-team-users-single-user.json @@ -0,0 +1 @@ +[{"~:id":"~uc7ce0794-0992-8105-8004-38e630f29a9b","~:fullname":"Princesa Leia"}] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-teams-complete.json b/frontend/playwright/data/logged-in-user/get-teams-complete.json new file mode 100644 index 0000000000000000000000000000000000000000..910e1543f11f5b08b42f7ba6d88671346178bd8e --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-teams-complete.json @@ -0,0 +1,48 @@ +[ + { + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true + }, + "~:name": "Default", + "~:modified-at": "~m1713533116375", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116375", + "~:is-default": true +}, + { + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true + }, + "~:name": "Second team", + "~:modified-at": "~m1701164272671", + "~:id": "~udd33ff88-f4e5-8033-8003-8096cc07bdf3", + "~:created-at": "~m1701164272671", + "~:is-default": false + } +] diff --git a/frontend/playwright/data/logged-in-user/get-teams-default.json b/frontend/playwright/data/logged-in-user/get-teams-default.json new file mode 100644 index 0000000000000000000000000000000000000000..bf8279c566340f384f079c602feab657f66440de --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-teams-default.json @@ -0,0 +1,25 @@ +[ + { + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true + }, + "~:name": "Default", + "~:modified-at": "~m1713533116375", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116375", + "~:is-default": true + } +] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/get-unread-comment-threads-empty.json b/frontend/playwright/data/logged-in-user/get-unread-comment-threads-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/frontend/playwright/data/logged-in-user/get-unread-comment-threads-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/logged-in-user/login-with-password-success.json b/frontend/playwright/data/logged-in-user/login-with-password-success.json new file mode 100644 index 0000000000000000000000000000000000000000..e4c1b1354c38df7e13d98295f1d7314d6a1e8f63 --- /dev/null +++ b/frontend/playwright/data/logged-in-user/login-with-password-success.json @@ -0,0 +1,24 @@ +{ + "~:is-admin": false, + "~:email": "foo@example.com", + "~:is-demo": false, + "~:auth-backend": "penpot", + "~:fullname": "Princesa Leia", + "~:modified-at": "~m1713533116365", + "~:is-active": true, + "~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:is-muted": false, + "~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116365", + "~:is-blocked": false, + "~:props": { + "~:nudge": { + "~:big": 10, + "~:small": 1 + }, + "~:v2-info-shown": true, + "~:viewed-tutorial?": false, + "~:viewed-walkthrough?": false + } +} \ No newline at end of file diff --git a/frontend/playwright/data/login-with-password-error.json b/frontend/playwright/data/login-with-password-error.json new file mode 100644 index 0000000000000000000000000000000000000000..c54589e42ce2cf81329067dd0a3f1e413b8f0efa --- /dev/null +++ b/frontend/playwright/data/login-with-password-error.json @@ -0,0 +1,4 @@ +{ + "~:type": "~:validation", + "~:code": "~:wrong-credentials" +} diff --git a/frontend/playwright/data/viewer/get-file-fragment-empty-file.json b/frontend/playwright/data/viewer/get-file-fragment-empty-file.json new file mode 100644 index 0000000000000000000000000000000000000000..c4fc2086e8c88d7e5a1c025202411e7e747becd4 --- /dev/null +++ b/frontend/playwright/data/viewer/get-file-fragment-empty-file.json @@ -0,0 +1,97 @@ +{ + "~:id": "~u0515a066-e303-8169-8004-73eb58e899c2", + "~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:created-at": "~m1717493890966", + "~:data": { + "~:options": {}, + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1.0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [] + } + } + }, + "~:id": "~uc7ce0794-0992-8105-8004-38f28044384a", + "~:name": "Page 1" + } +} diff --git a/frontend/playwright/data/viewer/get-file-fragment-single-board.json b/frontend/playwright/data/viewer/get-file-fragment-single-board.json new file mode 100644 index 0000000000000000000000000000000000000000..cf00a2900f5788999a066021962a77442f71b484 --- /dev/null +++ b/frontend/playwright/data/viewer/get-file-fragment-single-board.json @@ -0,0 +1,186 @@ +{ + "~:id": "~udd5cc0bb-91ff-81b9-8004-77dfae2d9e7c", + "~:file-id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb1", + "~:created-at": "~m1717759268004", + "~:data": { + "~:options": {}, + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1.0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~uec508673-9e3b-80bf-8004-77dfa30a2b13" + ] + } + }, + "~uec508673-9e3b-80bf-8004-77dfa30a2b13": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Board", + "~:width": 256.00000000000006, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 256.00000000000006, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 256.00000000000006, + "~:y": 256 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 256 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~uec508673-9e3b-80bf-8004-77dfa30a2b13", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 256.00000000000006, + "~:height": 256, + "~:x1": 0, + "~:y1": 0, + "~:x2": 256.00000000000006, + "~:y2": 256 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 256, + "~:flip-y": null, + "~:shapes": [] + } + } + }, + "~:id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2", + "~:name": "Page 1" + } +} diff --git a/frontend/playwright/data/viewer/get-view-only-bundle-empty-file.json b/frontend/playwright/data/viewer/get-view-only-bundle-empty-file.json new file mode 100644 index 0000000000000000000000000000000000000000..ef001224ab7328dc36147f61a5091cc59db31026 --- /dev/null +++ b/frontend/playwright/data/viewer/get-view-only-bundle-empty-file.json @@ -0,0 +1,86 @@ +{ + "~:users": [ + { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:email": "leia@example.com", + "~:name": "Princesa Leia", + "~:fullname": "Princesa Leia", + "~:is-active": true + } + ], + "~:fonts": [], + "~:project": { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:name": "Drafts", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d" + }, + "~:share-links": [], + "~:libraries": [], + "~:file": { + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 1", + "~:revn": 0, + "~:modified-at": "~m1717493891000", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:is-shared": false, + "~:version": 48, + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1717493891000", + "~:data": { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:options": { + "~:components-v2": true + }, + "~:pages": [ + "~uc7ce0794-0992-8105-8004-38f28044384a" + ], + "~:pages-index": { + "~uc7ce0794-0992-8105-8004-38f28044384a": { + "~#penpot/pointer": [ + "~u0515a066-e303-8169-8004-73eb58e899c2", + { + "~:created-at": "~m1717493890978" + } + ] + } + } + } + }, + "~:team": { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1717493865581", + "~:modified-at": "~m1717493865581", + "~:name": "Default", + "~:is-default": true, + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + } + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true, + "~:in-team": true + } +} \ No newline at end of file diff --git a/frontend/playwright/data/viewer/get-view-only-bundle-single-board.json b/frontend/playwright/data/viewer/get-view-only-bundle-single-board.json new file mode 100644 index 0000000000000000000000000000000000000000..9284de68588d2fd3286ea917c5a1349367f78cfc --- /dev/null +++ b/frontend/playwright/data/viewer/get-view-only-bundle-single-board.json @@ -0,0 +1,86 @@ +{ + "~:users": [ + { + "~:id": "~u0515a066-e303-8169-8004-73eb4018f4e0", + "~:email": "leia@example.com", + "~:name": "Princesa Leia", + "~:fullname": "Princesa Leia", + "~:is-active": true + } + ], + "~:fonts": [], + "~:project": { + "~:id": "~u0515a066-e303-8169-8004-73eb401b5d55", + "~:name": "Drafts", + "~:team-id": "~u0515a066-e303-8169-8004-73eb401977a6" + }, + "~:share-links": [], + "~:libraries": [], + "~:file": { + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 3", + "~:revn": 1, + "~:modified-at": "~m1717759268010", + "~:id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb1", + "~:is-shared": false, + "~:version": 48, + "~:project-id": "~u0515a066-e303-8169-8004-73eb401b5d55", + "~:created-at": "~m1717759250257", + "~:data": { + "~:id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb1", + "~:options": { + "~:components-v2": true + }, + "~:pages": [ + "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2" + ], + "~:pages-index": { + "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2": { + "~#penpot/pointer": [ + "~udd5cc0bb-91ff-81b9-8004-77dfae2d9e7c", + { + "~:created-at": "~m1717759268024" + } + ] + } + } + } + }, + "~:team": { + "~:id": "~u0515a066-e303-8169-8004-73eb401977a6", + "~:created-at": "~m1717493865581", + "~:modified-at": "~m1717493865581", + "~:name": "Default", + "~:is-default": true, + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + } + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true, + "~:in-team": true + } +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/audit-event-empty.json b/frontend/playwright/data/workspace/audit-event-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..9e26dfeeb6e641a33dae4961196235bdb965b21b --- /dev/null +++ b/frontend/playwright/data/workspace/audit-event-empty.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-comment-threads-empty.json b/frontend/playwright/data/workspace/get-comment-threads-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/frontend/playwright/data/workspace/get-comment-threads-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-comment-threads-not-empty.json b/frontend/playwright/data/workspace/get-comment-threads-not-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..503f1069fe1e5fb42325771b7de4bba710cc9a99 --- /dev/null +++ b/frontend/playwright/data/workspace/get-comment-threads-not-empty.json @@ -0,0 +1,58 @@ +[ + { + "~:page-name":"Page 1", + "~:file-id":"~ud192fd06-a3e6-80d5-8004-7b7aaaea2a23", + "~:participants":{ + "~#set":[ + "~u0515a066-e303-8169-8004-73eb4018f4e0" + ] + }, + "~:content":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque lacus tellus, pretium id dapibus in, suscipit eu magna. Duis rhoncus, nisl quis accumsan euismod, dolor ipsum bibendum enim, et varius turpis erat ut purus. Mauris lobortis ullamcorper lacus, sit amet iaculis dolor ultrices vitae. Phasellus sit amet iaculis neque, ac facilisis nisl. Morbi lobortis tellus nec purus elementum, ac vulputate diam vehicula. Quisque ullamcorper lobortis vestibulum. Proin ligula risus, auctor ac mauris sit amet, rhoncus hendrerit elit. Etiam at tempor tortor. Curabitur rutrum neque tortor, nec iaculis lorem varius sit amet.\n\nNunc maximus eget quam quis faucibus. Vivamus tincidunt sed velit non gravida. Vivamus fringilla sem tellus, a varius nisl posuere at. Duis cursus, turpis at vestibulum feugiat, est arcu fermentum ligula, in luctus nibh purus in purus. In vulputate enim non risus condimentum, et volutpat lectus dapibus. Sed elit felis, mattis sed dictum at, malesuada id risus. Proin ut felis sed eros viverra tempus. Proin varius eget erat vitae molestie. Suspendisse vehicula magna sit amet vehicula vehicula. Vestibulum in lorem nisl.\n\nNunc commodo elit sed lorem imperdiet pellentesque. Aliquam porta eget leo eget pretium. Aliquam erat volutpat. Donec condimentum, augue posuere vehicula sagittis, urna odio blandit lectus, id maximus purus leo eu odio. Donec eu tempor augue. Curabitur vitae ipsum non metus tristique posuere. Donec gravida, odio at aliquet consectetur, tellus nisl sollicitudin dui, quis tempus felis est quis odio. Duis sit amet dolor nisi. Sed vitae volutpat ex. Sed viverra sagittis semper. Ut ut enim sed nunc tempus facilisis.\n\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean orci mauris, lacinia ut nulla et, fringilla sollicitudin leo. Pellentesque sit amet euismod urna, quis bibendum nisi. Vivamus vitae lacinia sapien. Praesent consectetur vehicula pulvinar. Nunc varius rutrum risus, ac dictum orci pellentesque vel. Pellentesque sit amet bibendum risus. Quisque suscipit dui in libero posuere porttitor. Curabitur a ultrices sem. Duis maximus, velit ac dapibus venenatis, sapien arcu commodo dolor, eget ultrices ante dui sit amet orci. Cras rutrum nulla nunc, nec efficitur leo efficitur ac. In sollicitudin, mauris eu sollicitudin porta, sapien neque eleifend nibh, id imperdiet quam leo eget ex. Maecenas at leo ornare, lacinia ipsum imperdiet, laoreet turpis. Aliquam sodales ligula urna, vel vulputate orci imperdiet eu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse congue vehicula nisl, vel gravida magna sagittis sit amet.\n\nDonec ultricies placerat justo, id venenatis neque. Praesent eget vulputate est, ac placerat lectus. Curabitur condimentum non lorem id faucibus. Curabitur neque erat, euismod id pellentesque et, suscipit at elit. Curabitur vel nisi maximus, imperdiet lectus eu, tristique neque. Nunc quis quam non velit tristique tincidunt sed eu nisi. Morbi dictum accumsan arcu in consequat. Integer at urna commodo, commodo velit eget, efficitur metus. Proin ornare id velit nec molestie. Nam vitae faucibus enim. Aenean lobortis quam quis leo congue sodales. Pellentesque ornare eu purus quis congue. Cras ultricies nec eros in fringilla.\n\nMorbi egestas, arcu eget sollicitudin lobortis, dui arcu feugiat mi, sit amet commodo magna diam vitae nunc. Nulla varius leo quis ligula porta scelerisque. Morbi dignissim ante nec nisi molestie scelerisque. Sed ac facilisis tortor. Etiam lorem ex, tincidunt ac eros eu, molestie finibus ante. Integer et sollicitudin sem. Duis eu pretium est. Integer sit amet finibus lacus, in placerat ipsum. Phasellus leo ex, ornare semper lorem in, cursus vehicula nisl. Quisque tincidunt blandit est, non convallis justo consectetur vestibulum. Donec laoreet ipsum mauris, quis porttitor quam aliquam vitae.\n\nAliquam pharetra sapien pretium malesuada vehicula. Quisque risus risus, imperdiet at iaculis vel, aliquam quis libero. Sed quis libero imperdiet, volutpat magna vel, sagittis est. Ut eleifend odio in interdum maximus. Aenean libero enim, ornare quis ante pharetra, venenatis elementum est. Mauris sapien tortor, bibendum in elit id, fermentum blandit nisl. Cras eget dictum odio. Vestibulum nec mauris at odio vestibulum placerat. Praesent et placerat mauris. Pellentesque vitae nulla sed velit ornare suscipit eu eget neque. Morbi non ex molestie est congue commodo dictum eu tortor. Nunc hendrerit sodales purus, sit amet maximus est. Sed porta eleifend malesuada.\n\nDuis lobortis ultricies lectus, in tristique tortor. Praesent mauris mi, finibus vel imperdiet quis, congue vel erat. Sed pharetra et ipsum at vestibulum. Vestibulum id molestie urna. Sed at felis gravida, volutpat orci in, pulvinar mauris. Pellentesque sed odio bibendum, molestie risus eu, convallis libero. Etiam in quam dapibus, elementum mi vel, vestibulum est.\n\nCras tristique venenatis pulvinar. Sed id est mi. Ut id lorem volutpat, ullamcorper tellus nec, iaculis ex. Phasellus sed lorem eu turpis pulvinar bibendum ac semper metus. Vestibulum dolor erat, semper at ullamcorper eu, imperdiet at ipsum. Sed mauris erat, sodales non bibendum at, ultricies sed orci. Phasellus sem lacus, dictum a ipsum id, vulputate egestas diam. Suspendisse sit amet volutpat metus, sit amet faucibus eros. Vestibulum ut ante vitae dolor placerat viverra sit amet nec nisl. Nulla consequat, eros at lobortis faucibus, ex eros rhoncus enim, vel egestas nunc ligula a ante. Suspendisse potenti. Nunc magna enim, consectetur in euismod at, accumsan vitae nibh. Suspendisse imperdiet, arcu sit amet congue fringilla, turpis urna venenatis ligula, sit amet laoreet neque erat quis eros.\n\nDonec lobortis blandit justo. Maecenas commodo massa aliquam, elementum ligula tincidunt, iaculis lectus. Aliquam condimentum tortor orci. In molestie augue ac efficitur dignissim. Donec cursus, erat sit amet blandit semper, erat sapien cursus erat, ac consequat magna mi sed est. Morbi at enim non augue gravida pellentesque. Suspendisse eget aliquam dolor.", + "~:count-unread-comments":0, + "~:count-comments":1, + "~:modified-at":"~m1718001240857", + "~:page-id":"~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2", + "~:id": "~udd5cc0bb-91ff-81b9-8004-77df9cd3edb1", + "~:file-name":"New File 3", + "~:seqn":1, + "~:is-resolved":false, + "~:owner-id":"~u2e2da0fa-2d3e-81ec-8003-cb4453324510", + "~:position":{ + "~#point":{ + "~:x":20.0, + "~:y":20.0 + } + }, + "~:frame-id": "~uec508673-9e3b-80bf-8004-77dfa30a2b13", + "~:project-id": "~u0515a066-e303-8169-8004-73eb401b5d55", + "~:created-at":"~m1718001240857" + }, + { + "~:page-name":"Page 1", + "~:file-id":"~ud192fd06-a3e6-80d5-8004-7b7aaaea2a23", + "~:participants":{ + "~#set":[ + "~u0515a066-e303-8169-8004-73eb4018f4e0" + ] + }, + "~:content":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque lacus tellus, pretium id dapibus in, suscipit eu magna. Duis rhoncus, nisl quis accumsan euismod, dolor ipsum bibendum enim, et varius turpis erat ut purus. Mauris lobortis ullamcorper lacus, sit amet iaculis dolor ultrices vitae. Phasellus sit amet iaculis neque, ac facilisis nisl. Morbi lobortis tellus nec purus elementum, ac vulputate diam vehicula. Quisque ullamcorper lobortis vestibulum. Proin ligula risus, auctor ac mauris sit amet, rhoncus hendrerit elit. Etiam at tempor tortor. Curabitur rutrum neque tortor, nec iaculis lorem varius sit amet.\n\nNunc maximus eget quam quis faucibus. Vivamus tincidunt sed velit non gravida. Vivamus fringilla sem tellus, a varius nisl posuere at. Duis cursus, turpis at vestibulum feugiat, est arcu fermentum ligula, in luctus nibh purus in purus. In vulputate enim non risus condimentum, et volutpat lectus dapibus. Sed elit felis, mattis sed dictum at, malesuada id risus. Proin ut felis sed eros viverra tempus. Proin varius eget erat vitae molestie. Suspendisse vehicula magna sit amet vehicula vehicula. Vestibulum in lorem nisl.\n\nNunc commodo elit sed lorem imperdiet pellentesque. Aliquam porta eget leo eget pretium. Aliquam erat volutpat. Donec condimentum, augue posuere vehicula sagittis, urna odio blandit lectus, id maximus purus leo eu odio. Donec eu tempor augue. Curabitur vitae ipsum non metus tristique posuere. Donec gravida, odio at aliquet consectetur, tellus nisl sollicitudin dui, quis tempus felis est quis odio. Duis sit amet dolor nisi. Sed vitae volutpat ex. Sed viverra sagittis semper. Ut ut enim sed nunc tempus facilisis.\n\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean orci mauris, lacinia ut nulla et, fringilla sollicitudin leo. Pellentesque sit amet euismod urna, quis bibendum nisi. Vivamus vitae lacinia sapien. Praesent consectetur vehicula pulvinar. Nunc varius rutrum risus, ac dictum orci pellentesque vel. Pellentesque sit amet bibendum risus. Quisque suscipit dui in libero posuere porttitor. Curabitur a ultrices sem. Duis maximus, velit ac dapibus venenatis, sapien arcu commodo dolor, eget ultrices ante dui sit amet orci. Cras rutrum nulla nunc, nec efficitur leo efficitur ac. In sollicitudin, mauris eu sollicitudin porta, sapien neque eleifend nibh, id imperdiet quam leo eget ex. Maecenas at leo ornare, lacinia ipsum imperdiet, laoreet turpis. Aliquam sodales ligula urna, vel vulputate orci imperdiet eu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse congue vehicula nisl, vel gravida magna sagittis sit amet.\n\nDonec ultricies placerat justo, id venenatis neque. Praesent eget vulputate est, ac placerat lectus. Curabitur condimentum non lorem id faucibus. Curabitur neque erat, euismod id pellentesque et, suscipit at elit. Curabitur vel nisi maximus, imperdiet lectus eu, tristique neque. Nunc quis quam non velit tristique tincidunt sed eu nisi. Morbi dictum accumsan arcu in consequat. Integer at urna commodo, commodo velit eget, efficitur metus. Proin ornare id velit nec molestie. Nam vitae faucibus enim. Aenean lobortis quam quis leo congue sodales. Pellentesque ornare eu purus quis congue. Cras ultricies nec eros in fringilla.\n\nMorbi egestas, arcu eget sollicitudin lobortis, dui arcu feugiat mi, sit amet commodo magna diam vitae nunc. Nulla varius leo quis ligula porta scelerisque. Morbi dignissim ante nec nisi molestie scelerisque. Sed ac facilisis tortor. Etiam lorem ex, tincidunt ac eros eu, molestie finibus ante. Integer et sollicitudin sem. Duis eu pretium est. Integer sit amet finibus lacus, in placerat ipsum. Phasellus leo ex, ornare semper lorem in, cursus vehicula nisl. Quisque tincidunt blandit est, non convallis justo consectetur vestibulum. Donec laoreet ipsum mauris, quis porttitor quam aliquam vitae.\n\nAliquam pharetra sapien pretium malesuada vehicula. Quisque risus risus, imperdiet at iaculis vel, aliquam quis libero. Sed quis libero imperdiet, volutpat magna vel, sagittis est. Ut eleifend odio in interdum maximus. Aenean libero enim, ornare quis ante pharetra, venenatis elementum est. Mauris sapien tortor, bibendum in elit id, fermentum blandit nisl. Cras eget dictum odio. Vestibulum nec mauris at odio vestibulum placerat. Praesent et placerat mauris. Pellentesque vitae nulla sed velit ornare suscipit eu eget neque. Morbi non ex molestie est congue commodo dictum eu tortor. Nunc hendrerit sodales purus, sit amet maximus est. Sed porta eleifend malesuada.\n\nDuis lobortis ultricies lectus, in tristique tortor. Praesent mauris mi, finibus vel imperdiet quis, congue vel erat. Sed pharetra et ipsum at vestibulum. Vestibulum id molestie urna. Sed at felis gravida, volutpat orci in, pulvinar mauris. Pellentesque sed odio bibendum, molestie risus eu, convallis libero. Etiam in quam dapibus, elementum mi vel, vestibulum est.\n\nCras tristique venenatis pulvinar. Sed id est mi. Ut id lorem volutpat, ullamcorper tellus nec, iaculis ex. Phasellus sed lorem eu turpis pulvinar bibendum ac semper metus. Vestibulum dolor erat, semper at ullamcorper eu, imperdiet at ipsum. Sed mauris erat, sodales non bibendum at, ultricies sed orci. Phasellus sem lacus, dictum a ipsum id, vulputate egestas diam. Suspendisse sit amet volutpat metus, sit amet faucibus eros. Vestibulum ut ante vitae dolor placerat viverra sit amet nec nisl. Nulla consequat, eros at lobortis faucibus, ex eros rhoncus enim, vel egestas nunc ligula a ante. Suspendisse potenti. Nunc magna enim, consectetur in euismod at, accumsan vitae nibh. Suspendisse imperdiet, arcu sit amet congue fringilla, turpis urna venenatis ligula, sit amet laoreet neque erat quis eros.\n\nDonec lobortis blandit justo. Maecenas commodo massa aliquam, elementum ligula tincidunt, iaculis lectus. Aliquam condimentum tortor orci. In molestie augue ac efficitur dignissim. Donec cursus, erat sit amet blandit semper, erat sapien cursus erat, ac consequat magna mi sed est. Morbi at enim non augue gravida pellentesque. Suspendisse eget aliquam dolor.", + "~:count-unread-comments":0, + "~:count-comments":1, + "~:modified-at":"~m1718001247587", + "~:page-id":"~udd5cc0bb-91ff-81b9-8004-77df9cd3edb2", + "~:id":"~ud192fd06-a3e6-80d5-8004-7b7ac25ac93a", + "~:file-name":"New File 44", + "~:seqn":2, + "~:is-resolved":false, + "~:owner-id":"~u2e2da0fa-2d3e-81ec-8003-cb4453324510", + "~:position":{ + "~#point":{ + "~:x":235.0, + "~:y":235.0 + } + }, + "~:frame-id": "~uec508673-9e3b-80bf-8004-77dfa30a2b13", + "~:project-id":"~u343837a3-0d75-808a-8004-659df7b7873e", + "~:created-at":"~m1718001247587" + } +] \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-7760.json b/frontend/playwright/data/workspace/get-file-7760.json new file mode 100644 index 0000000000000000000000000000000000000000..ff33a7a941d304d3b32e7381470c0098a00eac41 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-7760.json @@ -0,0 +1,49 @@ +{ + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 6", + "~:revn": 5, + "~:modified-at": "~m1718094617219", + "~:id": "~ucd90e028-326a-80b4-8004-7cdec16ffad5", + "~:is-shared": false, + "~:version": 48, + "~:project-id": "~u128636f9-5e78-812b-8004-350dd1a8869a", + "~:created-at": "~m1718094569923", + "~:data": { + "~:pages": [ + "~ucd90e028-326a-80b4-8004-7cdec16ffad6" + ], + "~:pages-index": { + "~ucd90e028-326a-80b4-8004-7cdec16ffad6": { + "~#penpot/pointer": [ + "~ucd90e028-326a-80b4-8004-7cdeefa23ece", + { + "~:created-at": "~m1718094617224" + } + ] + } + }, + "~:id": "~ucd90e028-326a-80b4-8004-7cdec16ffad5", + "~:options": { + "~:components-v2": true + } + } +} diff --git a/frontend/playwright/data/workspace/get-file-blank.json b/frontend/playwright/data/workspace/get-file-blank.json new file mode 100644 index 0000000000000000000000000000000000000000..9e05e3b50a5a9f2000dce7f1f2ccbdd3537cc73a --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-blank.json @@ -0,0 +1,58 @@ +{ + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "New File 1", + "~:revn": 11, + "~:modified-at": "~m1713873823633", + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:is-shared": false, + "~:version": 46, + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at": "~m1713536343369", + "~:data": { + "~:pages": [ + "~uc7ce0794-0992-8105-8004-38f28044384a" + ], + "~:pages-index": { + "~uc7ce0794-0992-8105-8004-38f28044384a": { + "~#penpot/pointer": [ + "~ude58c8f6-c5c2-8196-8004-3df9e2e52d88", + { + "~:created-at": "~m1713873823636" + } + ] + } + }, + "~:id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:options": { + "~:components-v2": true + }, + "~:recent-colors": [ + { + "~:color": "#0000ff", + "~:opacity": 1, + "~:id": null, + "~:file-id": null, + "~:image": null + } + ] + } +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-fragment-7760.json b/frontend/playwright/data/workspace/get-file-fragment-7760.json new file mode 100644 index 0000000000000000000000000000000000000000..c07d487026481f1ad0cea9d07827cc0c9279c0a8 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-fragment-7760.json @@ -0,0 +1,383 @@ +{ + "~:id": "~ucd90e028-326a-80b4-8004-7cdeefa23ece", + "~:file-id": "~ucd90e028-326a-80b4-8004-7cdec16ffad5", + "~:created-at": "~m1718094617214", + "~:data": { + "~:options": {}, + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1.0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~u86087f92-9a17-8067-8004-7cdec45bee43", + "~u86087f92-9a17-8067-8004-7cded1cbe70e" + ] + } + }, + "~u86087f92-9a17-8067-8004-7cdec45bee43": { + "~#shape": { + "~:y": 341, + "~:hide-fill-on-export": false, + "~:layout-gap-type": "~:multiple", + "~:layout-padding": { + "~:p1": 34, + "~:p2": 36, + "~:p3": 34, + "~:p4": 36 + }, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:layout-wrap-type": "~:nowrap", + "~:grow-type": "~:fixed", + "~:layout": "~:flex", + "~:hide-in-viewer": false, + "~:name": "Flex Board", + "~:layout-align-items": "~:start", + "~:width": 176, + "~:layout-padding-type": "~:simple", + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 217, + "~:y": 341 + } + }, + { + "~#point": { + "~:x": 393, + "~:y": 341 + } + }, + { + "~#point": { + "~:x": 393, + "~:y": 511 + } + }, + { + "~#point": { + "~:x": 217, + "~:y": 511 + } + } + ], + "~:layout-item-h-sizing": "~:auto", + "~:proportion-lock": false, + "~:layout-gap": { + "~:row-gap": 0, + "~:column-gap": 0 + }, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:layout-item-v-sizing": "~:auto", + "~:layout-justify-content": "~:start", + "~:id": "~u86087f92-9a17-8067-8004-7cdec45bee43", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:layout-flex-dir": "~:row", + "~:layout-align-content": "~:stretch", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 217, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 217, + "~:y": 341, + "~:width": 176, + "~:height": 170, + "~:x1": 217, + "~:y1": 341, + "~:x2": 393, + "~:y2": 511 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 170, + "~:flip-y": null, + "~:shapes": [ + "~u86087f92-9a17-8067-8004-7cdec98dfa7f" + ] + } + }, + "~u86087f92-9a17-8067-8004-7cdec98dfa7f": { + "~#shape": { + "~:y": 375, + "~:rx": 0, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 104, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 253, + "~:y": 375 + } + }, + { + "~#point": { + "~:x": 357, + "~:y": 375 + } + }, + { + "~#point": { + "~:x": 357, + "~:y": 477 + } + }, + { + "~#point": { + "~:x": 253, + "~:y": 477 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~u86087f92-9a17-8067-8004-7cdec98dfa7f", + "~:parent-id": "~u86087f92-9a17-8067-8004-7cdec45bee43", + "~:frame-id": "~u86087f92-9a17-8067-8004-7cdec45bee43", + "~:strokes": [], + "~:x": 253, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 253, + "~:y": 375, + "~:width": 104, + "~:height": 102, + "~:x1": 253, + "~:y1": 375, + "~:x2": 357, + "~:y2": 477 + } + }, + "~:fills": [ + { + "~:fill-color": "#B1B2B5", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:ry": 0, + "~:height": 102, + "~:flip-y": null + } + }, + "~u86087f92-9a17-8067-8004-7cded1cbe70e": { + "~#shape": { + "~:y": 300, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:grow-type": "~:fixed", + "~:hide-in-viewer": false, + "~:name": "Container Board", + "~:width": 434, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 689, + "~:y": 300 + } + }, + { + "~#point": { + "~:x": 1123, + "~:y": 300 + } + }, + { + "~#point": { + "~:x": 1123, + "~:y": 741 + } + }, + { + "~#point": { + "~:x": 689, + "~:y": 741 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:id": "~u86087f92-9a17-8067-8004-7cded1cbe70e", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 689, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 689, + "~:y": 300, + "~:width": 434, + "~:height": 441, + "~:x1": 689, + "~:y1": 300, + "~:x2": 1123, + "~:y2": 741 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 441, + "~:flip-y": null, + "~:shapes": [] + } + } + }, + "~:id": "~ucd90e028-326a-80b4-8004-7cdec16ffad6", + "~:name": "Page 1" + } +} diff --git a/frontend/playwright/data/workspace/get-file-fragment-blank.json b/frontend/playwright/data/workspace/get-file-fragment-blank.json new file mode 100644 index 0000000000000000000000000000000000000000..7760aaa9270f3eb1ba330726ee5a4f6a79b4920d --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-fragment-blank.json @@ -0,0 +1,97 @@ +{ + "~:id": "~ude58c8f6-c5c2-8196-8004-3df9e2e52d88", + "~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:created-at": "~m1713873823631", + "~:data": { + "~:options": {}, + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0, + "~:y": 0.01 + } + } + ], + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1, + "~:b": 0, + "~:c": 0, + "~:d": 1, + "~:e": 0, + "~:f": 0 + } + }, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [] + } + } + }, + "~:id": "~uc7ce0794-0992-8105-8004-38f28044384a", + "~:name": "Page 1" + } +} diff --git a/frontend/playwright/data/workspace/get-file-libraries-empty.json b/frontend/playwright/data/workspace/get-file-libraries-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-libraries-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-library.json b/frontend/playwright/data/workspace/get-file-library.json new file mode 100644 index 0000000000000000000000000000000000000000..de477542797770af9e74c26eb4c960638840834d --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-library.json @@ -0,0 +1,242 @@ +{ + "~:features":{ + "~#set":[ + "layout/grid", + "styles/v2", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions":{ + "~:type":"~:membership", + "~:is-owner":true, + "~:is-admin":true, + "~:can-edit":true, + "~:can-read":true, + "~:is-logged":true + }, + "~:has-media-trimmed":false, + "~:comment-thread-seqn":0, + "~:name":"Testing library 1", + "~:revn":2, + "~:modified-at":"~m1717512948250", + "~:id":"~uc1249a66-fce0-8175-8004-7433fe4be8bc", + "~:is-shared":true, + "~:version":48, + "~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at":"~m1717512934704", + "~:data":{ + "~:pages":[ + "~uc1249a66-fce0-8175-8004-7433fe4be8bd" + ], + "~:pages-index":{ + "~uc1249a66-fce0-8175-8004-7433fe4be8bd":{ + "~:options":{ + + }, + "~:objects":{ + "~u00000000-0000-0000-0000-000000000000":{ + "~#shape":{ + "~:y":0, + "~:hide-fill-on-export":false, + "~:transform":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:rotation":0, + "~:name":"Root Frame", + "~:width":0.01, + "~:type":"~:frame", + "~:points":[ + { + "~#point":{ + "~:x":0, + "~:y":0 + } + }, + { + "~#point":{ + "~:x":0.01, + "~:y":0 + } + }, + { + "~#point":{ + "~:x":0.01, + "~:y":0.01 + } + }, + { + "~#point":{ + "~:x":0, + "~:y":0.01 + } + } + ], + "~:proportion-lock":false, + "~:transform-inverse":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:id":"~u00000000-0000-0000-0000-000000000000", + "~:parent-id":"~u00000000-0000-0000-0000-000000000000", + "~:frame-id":"~u00000000-0000-0000-0000-000000000000", + "~:strokes":[ + + ], + "~:x":0, + "~:proportion":1.0, + "~:selrect":{ + "~#rect":{ + "~:x":0, + "~:y":0, + "~:width":0.01, + "~:height":0.01, + "~:x1":0, + "~:y1":0, + "~:x2":0.01, + "~:y2":0.01 + } + }, + "~:fills":[ + { + "~:fill-color":"#FFFFFF", + "~:fill-opacity":1 + } + ], + "~:flip-x":null, + "~:height":0.01, + "~:flip-y":null, + "~:shapes":[ + "~uc70224ec-c410-807b-8004-743400e00be8" + ] + } + }, + "~uc70224ec-c410-807b-8004-743400e00be8":{ + "~#shape":{ + "~:y":255, + "~:rx":0, + "~:transform":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:rotation":0, + "~:grow-type":"~:fixed", + "~:hide-in-viewer":false, + "~:name":"Rectangle", + "~:width":279.0000000000001, + "~:type":"~:rect", + "~:points":[ + { + "~#point":{ + "~:x":523, + "~:y":255 + } + }, + { + "~#point":{ + "~:x":802.0000000000001, + "~:y":255 + } + }, + { + "~#point":{ + "~:x":802.0000000000001, + "~:y":534 + } + }, + { + "~#point":{ + "~:x":523, + "~:y":534 + } + } + ], + "~:proportion-lock":false, + "~:transform-inverse":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:id":"~uc70224ec-c410-807b-8004-743400e00be8", + "~:parent-id":"~u00000000-0000-0000-0000-000000000000", + "~:frame-id":"~u00000000-0000-0000-0000-000000000000", + "~:strokes":[ + + ], + "~:x":523, + "~:proportion":1, + "~:selrect":{ + "~#rect":{ + "~:x":523, + "~:y":255, + "~:width":279.0000000000001, + "~:height":279, + "~:x1":523, + "~:y1":255, + "~:x2":802.0000000000001, + "~:y2":534 + } + }, + "~:fills":[ + { + "~:fill-color":"#B1B2B5", + "~:fill-opacity":1 + } + ], + "~:flip-x":null, + "~:ry":0, + "~:height":279, + "~:flip-y":null + } + } + }, + "~:id":"~uc1249a66-fce0-8175-8004-7433fe4be8bd", + "~:name":"Page 1" + } + }, + "~:id":"~uc1249a66-fce0-8175-8004-7433fe4be8bc", + "~:options":{ + "~:components-v2":true + }, + "~:recent-colors":[ + { + "~:color":"#187cd5", + "~:opacity":1 + } + ], + "~:colors":{ + "~uc70224ec-c410-807b-8004-74340616cffb":{ + "~:path":"", + "~:color":"#187cd5", + "~:name":"test-color-187cd5", + "~:modified-at":"~m1717512945259", + "~:opacity":1, + "~:id":"~uc70224ec-c410-807b-8004-74340616cffb" + } + } + } +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-not-empty.json b/frontend/playwright/data/workspace/get-file-not-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..27a91a25b74ae2dd990f616052b1b335b3a259f0 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-not-empty.json @@ -0,0 +1,222 @@ +{ + "~:features":{ + "~#set":[ + "layout/grid", + "styles/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions":{ + "~:type":"~:membership", + "~:is-owner":true, + "~:is-admin":true, + "~:can-edit":true, + "~:can-read":true, + "~:is-logged":true + }, + "~:has-media-trimmed":false, + "~:comment-thread-seqn":0, + "~:name":"New File 14", + "~:revn":1, + "~:modified-at":"~m1718088151182", + "~:id":"~u6191cd35-bb1f-81f7-8004-7cc63d087374", + "~:is-shared":false, + "~:version":48, + "~:project-id":"~u4dc640b0-5cbf-11ec-a7c5-91e9eb4f238d", + "~:created-at":"~m1718088142886", + "~:data":{ + "~:pages":[ + "~u6191cd35-bb1f-81f7-8004-7cc63d087375" + ], + "~:pages-index":{ + "~u6191cd35-bb1f-81f7-8004-7cc63d087375":{ + "~:options":{ + + }, + "~:objects":{ + "~u00000000-0000-0000-0000-000000000000":{ + "~#shape":{ + "~:y":0, + "~:hide-fill-on-export":false, + "~:transform":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:rotation":0, + "~:name":"Root Frame", + "~:width":0.01, + "~:type":"~:frame", + "~:points":[ + { + "~#point":{ + "~:x":0, + "~:y":0 + } + }, + { + "~#point":{ + "~:x":0.01, + "~:y":0 + } + }, + { + "~#point":{ + "~:x":0.01, + "~:y":0.01 + } + }, + { + "~#point":{ + "~:x":0, + "~:y":0.01 + } + } + ], + "~:proportion-lock":false, + "~:transform-inverse":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:id":"~u00000000-0000-0000-0000-000000000000", + "~:parent-id":"~u00000000-0000-0000-0000-000000000000", + "~:frame-id":"~u00000000-0000-0000-0000-000000000000", + "~:strokes":[ + + ], + "~:x":0, + "~:proportion":1.0, + "~:selrect":{ + "~#rect":{ + "~:x":0, + "~:y":0, + "~:width":0.01, + "~:height":0.01, + "~:x1":0, + "~:y1":0, + "~:x2":0.01, + "~:y2":0.01 + } + }, + "~:fills":[ + { + "~:fill-color":"#FFFFFF", + "~:fill-opacity":1 + } + ], + "~:flip-x":null, + "~:height":0.01, + "~:flip-y":null, + "~:shapes":[ + "~u7c75e310-c3a2-80fd-8004-7cc641479aef" + ] + } + }, + "~u7c75e310-c3a2-80fd-8004-7cc641479aef":{ + "~#shape":{ + "~:y":436, + "~:rx":0, + "~:transform":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:rotation":0, + "~:grow-type":"~:fixed", + "~:hide-in-viewer":false, + "~:name":"Rectangle", + "~:width":126.00000000000006, + "~:type":"~:rect", + "~:points":[ + { + "~#point":{ + "~:x":266, + "~:y":436 + } + }, + { + "~#point":{ + "~:x":392.00000000000006, + "~:y":436 + } + }, + { + "~#point":{ + "~:x":392.00000000000006, + "~:y":570 + } + }, + { + "~#point":{ + "~:x":266, + "~:y":570 + } + } + ], + "~:proportion-lock":false, + "~:transform-inverse":{ + "~#matrix":{ + "~:a":1.0, + "~:b":0.0, + "~:c":0.0, + "~:d":1.0, + "~:e":0.0, + "~:f":0.0 + } + }, + "~:id":"~u7c75e310-c3a2-80fd-8004-7cc641479aef", + "~:parent-id":"~u00000000-0000-0000-0000-000000000000", + "~:frame-id":"~u00000000-0000-0000-0000-000000000000", + "~:strokes":[ + + ], + "~:x":266, + "~:proportion":1, + "~:selrect":{ + "~#rect":{ + "~:x":266, + "~:y":436, + "~:width":126.00000000000006, + "~:height":134, + "~:x1":266, + "~:y1":436, + "~:x2":392.00000000000006, + "~:y2":570 + } + }, + "~:fills":[ + { + "~:fill-color":"#B1B2B5", + "~:fill-opacity":1 + } + ], + "~:flip-x":null, + "~:ry":0, + "~:height":134, + "~:flip-y":null + } + } + }, + "~:id":"~u6191cd35-bb1f-81f7-8004-7cc63d087375", + "~:name":"Page 1" + } + }, + "~:id":"~u6191cd35-bb1f-81f7-8004-7cc63d087374" + } +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-object-thumbnails-blank.json b/frontend/playwright/data/workspace/get-file-object-thumbnails-blank.json new file mode 100644 index 0000000000000000000000000000000000000000..8f55ece27593b523692b17a9db596f881671340a --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-object-thumbnails-blank.json @@ -0,0 +1,3 @@ +{ + "c7ce0794-0992-8105-8004-38f280443849/c7ce0794-0992-8105-8004-38f28044384a/8c1035fa-01f0-8071-8004-3df966ff2c64/frame": "http://localhost:3449/assets/by-id/50d097ed-d321-4319-b00b-e82a9c9435ea" +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-font-variants-empty.json b/frontend/playwright/data/workspace/get-font-variants-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..0637a088a01e8ddab3bf3fa98dbe804cbde1a0dc --- /dev/null +++ b/frontend/playwright/data/workspace/get-font-variants-empty.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-profile-for-file-comments.json b/frontend/playwright/data/workspace/get-profile-for-file-comments.json new file mode 100644 index 0000000000000000000000000000000000000000..f11319ecff887dfeb27515b147505b269eca0226 --- /dev/null +++ b/frontend/playwright/data/workspace/get-profile-for-file-comments.json @@ -0,0 +1,9 @@ +[ + { + "~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:email": "foo@example.com", + "~:name": "Princesa Leia", + "~:fullname": "Princesa Leia", + "~:is-active": true + } +] \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-project-default.json b/frontend/playwright/data/workspace/get-project-default.json new file mode 100644 index 0000000000000000000000000000000000000000..d953da8fd94562db9ebdbfce9ca4058b97fb2eb9 --- /dev/null +++ b/frontend/playwright/data/workspace/get-project-default.json @@ -0,0 +1,8 @@ +{ + "~:id": "~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116382", + "~:modified-at": "~m1713873823633", + "~:is-default": true, + "~:name": "Drafts" +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-team-default.json b/frontend/playwright/data/workspace/get-team-default.json new file mode 100644 index 0000000000000000000000000000000000000000..e31dcf90c7f7ce0445375261085807b33d7189ce --- /dev/null +++ b/frontend/playwright/data/workspace/get-team-default.json @@ -0,0 +1,23 @@ +{ + "~:features": { + "~#set": [ + "layout/grid", + "styles/v2", + "fdata/pointer-map", + "fdata/objects-map", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true + }, + "~:name": "Default", + "~:modified-at": "~m1713533116375", + "~:id": "~uc7ce0794-0992-8105-8004-38e630f40f6d", + "~:created-at": "~m1713533116375", + "~:is-default": true +} diff --git a/frontend/playwright/data/workspace/get-team-shared-libraries-non-empty.json b/frontend/playwright/data/workspace/get-team-shared-libraries-non-empty.json new file mode 100644 index 0000000000000000000000000000000000000000..05a5c8c3c1293e3e7a5c833aa7fd133d55a4483d --- /dev/null +++ b/frontend/playwright/data/workspace/get-team-shared-libraries-non-empty.json @@ -0,0 +1,47 @@ +{ + "~#set":[ + { + "~:name":"Testing library 1", + "~:revn":2, + "~:modified-at":"~m1717512948250", + "~:thumbnail-uri":"http://localhost:3000/assets/by-id/5ad7a7a7-c64e-4bb8-852d-15708d125905", + "~:id":"~uc1249a66-fce0-8175-8004-7433fe4be8bc", + "~:is-shared":true, + "~:project-id":"~uc7ce0794-0992-8105-8004-38e630f7920b", + "~:created-at":"~m1717512934704", + "~:library-summary":{ + "~:components":{ + "~:count":0, + "~:sample":[ + + ] + }, + "~:media":{ + "~:count":0, + "~:sample":[ + + ] + }, + "~:colors":{ + "~:count":1, + "~:sample":[ + { + "~:path":"", + "~:color":"#187cd5", + "~:name":"test-color", + "~:modified-at":"~m1717512945259", + "~:opacity":1, + "~:id":"~uc70224ec-c410-807b-8004-74340616cffb" + } + ] + }, + "~:typographies":{ + "~:count":0, + "~:sample":[ + + ] + } + } + } + ] +} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-thread-comments.json b/frontend/playwright/data/workspace/get-thread-comments.json new file mode 100644 index 0000000000000000000000000000000000000000..b3822d1eb5171c0850c66987739bbf6595ac950c --- /dev/null +++ b/frontend/playwright/data/workspace/get-thread-comments.json @@ -0,0 +1,10 @@ +[ + { + "~:id":"~ud192fd06-a3e6-80d5-8004-7b7abbc8cdf8", + "~:thread-id":"~ud192fd06-a3e6-80d5-8004-7b7abbc8ac30", + "~:owner-id":"~u2e2da0fa-2d3e-81ec-8003-cb4453324510", + "~:created-at":"~m1718001240857", + "~:modified-at":"~m1718001240857", + "~:content":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque lacus tellus, pretium id dapibus in, suscipit eu magna. Duis rhoncus, nisl quis accumsan euismod, dolor ipsum bibendum enim, et varius turpis erat ut purus. Mauris lobortis ullamcorper lacus, sit amet iaculis dolor ultrices vitae. Phasellus sit amet iaculis neque, ac facilisis nisl. Morbi lobortis tellus nec purus elementum, ac vulputate diam vehicula. Quisque ullamcorper lobortis vestibulum. Proin ligula risus, auctor ac mauris sit amet, rhoncus hendrerit elit. Etiam at tempor tortor. Curabitur rutrum neque tortor, nec iaculis lorem varius sit amet.\n\nNunc maximus eget quam quis faucibus. Vivamus tincidunt sed velit non gravida. Vivamus fringilla sem tellus, a varius nisl posuere at. Duis cursus, turpis at vestibulum feugiat, est arcu fermentum ligula, in luctus nibh purus in purus. In vulputate enim non risus condimentum, et volutpat lectus dapibus. Sed elit felis, mattis sed dictum at, malesuada id risus. Proin ut felis sed eros viverra tempus. Proin varius eget erat vitae molestie. Suspendisse vehicula magna sit amet vehicula vehicula. Vestibulum in lorem nisl.\n\nNunc commodo elit sed lorem imperdiet pellentesque. Aliquam porta eget leo eget pretium. Aliquam erat volutpat. Donec condimentum, augue posuere vehicula sagittis, urna odio blandit lectus, id maximus purus leo eu odio. Donec eu tempor augue. Curabitur vitae ipsum non metus tristique posuere. Donec gravida, odio at aliquet consectetur, tellus nisl sollicitudin dui, quis tempus felis est quis odio. Duis sit amet dolor nisi. Sed vitae volutpat ex. Sed viverra sagittis semper. Ut ut enim sed nunc tempus facilisis.\n\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Aenean orci mauris, lacinia ut nulla et, fringilla sollicitudin leo. Pellentesque sit amet euismod urna, quis bibendum nisi. Vivamus vitae lacinia sapien. Praesent consectetur vehicula pulvinar. Nunc varius rutrum risus, ac dictum orci pellentesque vel. Pellentesque sit amet bibendum risus. Quisque suscipit dui in libero posuere porttitor. Curabitur a ultrices sem. Duis maximus, velit ac dapibus venenatis, sapien arcu commodo dolor, eget ultrices ante dui sit amet orci. Cras rutrum nulla nunc, nec efficitur leo efficitur ac. In sollicitudin, mauris eu sollicitudin porta, sapien neque eleifend nibh, id imperdiet quam leo eget ex. Maecenas at leo ornare, lacinia ipsum imperdiet, laoreet turpis. Aliquam sodales ligula urna, vel vulputate orci imperdiet eu. Interdum et malesuada fames ac ante ipsum primis in faucibus. Suspendisse congue vehicula nisl, vel gravida magna sagittis sit amet.\n\nDonec ultricies placerat justo, id venenatis neque. Praesent eget vulputate est, ac placerat lectus. Curabitur condimentum non lorem id faucibus. Curabitur neque erat, euismod id pellentesque et, suscipit at elit. Curabitur vel nisi maximus, imperdiet lectus eu, tristique neque. Nunc quis quam non velit tristique tincidunt sed eu nisi. Morbi dictum accumsan arcu in consequat. Integer at urna commodo, commodo velit eget, efficitur metus. Proin ornare id velit nec molestie. Nam vitae faucibus enim. Aenean lobortis quam quis leo congue sodales. Pellentesque ornare eu purus quis congue. Cras ultricies nec eros in fringilla.\n\nMorbi egestas, arcu eget sollicitudin lobortis, dui arcu feugiat mi, sit amet commodo magna diam vitae nunc. Nulla varius leo quis ligula porta scelerisque. Morbi dignissim ante nec nisi molestie scelerisque. Sed ac facilisis tortor. Etiam lorem ex, tincidunt ac eros eu, molestie finibus ante. Integer et sollicitudin sem. Duis eu pretium est. Integer sit amet finibus lacus, in placerat ipsum. Phasellus leo ex, ornare semper lorem in, cursus vehicula nisl. Quisque tincidunt blandit est, non convallis justo consectetur vestibulum. Donec laoreet ipsum mauris, quis porttitor quam aliquam vitae.\n\nAliquam pharetra sapien pretium malesuada vehicula. Quisque risus risus, imperdiet at iaculis vel, aliquam quis libero. Sed quis libero imperdiet, volutpat magna vel, sagittis est. Ut eleifend odio in interdum maximus. Aenean libero enim, ornare quis ante pharetra, venenatis elementum est. Mauris sapien tortor, bibendum in elit id, fermentum blandit nisl. Cras eget dictum odio. Vestibulum nec mauris at odio vestibulum placerat. Praesent et placerat mauris. Pellentesque vitae nulla sed velit ornare suscipit eu eget neque. Morbi non ex molestie est congue commodo dictum eu tortor. Nunc hendrerit sodales purus, sit amet maximus est. Sed porta eleifend malesuada.\n\nDuis lobortis ultricies lectus, in tristique tortor. Praesent mauris mi, finibus vel imperdiet quis, congue vel erat. Sed pharetra et ipsum at vestibulum. Vestibulum id molestie urna. Sed at felis gravida, volutpat orci in, pulvinar mauris. Pellentesque sed odio bibendum, molestie risus eu, convallis libero. Etiam in quam dapibus, elementum mi vel, vestibulum est.\n\nCras tristique venenatis pulvinar. Sed id est mi. Ut id lorem volutpat, ullamcorper tellus nec, iaculis ex. Phasellus sed lorem eu turpis pulvinar bibendum ac semper metus. Vestibulum dolor erat, semper at ullamcorper eu, imperdiet at ipsum. Sed mauris erat, sodales non bibendum at, ultricies sed orci. Phasellus sem lacus, dictum a ipsum id, vulputate egestas diam. Suspendisse sit amet volutpat metus, sit amet faucibus eros. Vestibulum ut ante vitae dolor placerat viverra sit amet nec nisl. Nulla consequat, eros at lobortis faucibus, ex eros rhoncus enim, vel egestas nunc ligula a ante. Suspendisse potenti. Nunc magna enim, consectetur in euismod at, accumsan vitae nibh. Suspendisse imperdiet, arcu sit amet congue fringilla, turpis urna venenatis ligula, sit amet laoreet neque erat quis eros.\n\nDonec lobortis blandit justo. Maecenas commodo massa aliquam, elementum ligula tincidunt, iaculis lectus. Aliquam condimentum tortor orci. In molestie augue ac efficitur dignissim. Donec cursus, erat sit amet blandit semper, erat sapien cursus erat, ac consequat magna mi sed est. Morbi at enim non augue gravida pellentesque. Suspendisse eget aliquam dolor." + } +] \ No newline at end of file diff --git a/frontend/playwright/data/workspace/link-file-to-library.json b/frontend/playwright/data/workspace/link-file-to-library.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/frontend/playwright/data/workspace/link-file-to-library.json @@ -0,0 +1 @@ +{} diff --git a/frontend/playwright/data/workspace/unlink-file-from-library.json b/frontend/playwright/data/workspace/unlink-file-from-library.json new file mode 100644 index 0000000000000000000000000000000000000000..0967ef424bce6791893e9a57bb952f80fd536e93 --- /dev/null +++ b/frontend/playwright/data/workspace/unlink-file-from-library.json @@ -0,0 +1 @@ +{} diff --git a/frontend/playwright/data/workspace/update-comment-thread-status.json b/frontend/playwright/data/workspace/update-comment-thread-status.json new file mode 100644 index 0000000000000000000000000000000000000000..9e26dfeeb6e641a33dae4961196235bdb965b21b --- /dev/null +++ b/frontend/playwright/data/workspace/update-comment-thread-status.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/frontend/playwright/data/workspace/update-file-create-rect.json b/frontend/playwright/data/workspace/update-file-create-rect.json new file mode 100644 index 0000000000000000000000000000000000000000..671fef98f77d422b026fbd4af0c729e1845bc10c --- /dev/null +++ b/frontend/playwright/data/workspace/update-file-create-rect.json @@ -0,0 +1,9 @@ +[ + { + "~:id": "~u088df3d4-d383-80f6-8004-527e50ea4f1f", + "~:revn": 21, + "~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:session-id": "~u1dc6d4fa-7bd3-803a-8004-527dd9df2c62", + "~:changes": [] + } +] diff --git a/frontend/playwright/data/workspace/ws-notifications.js b/frontend/playwright/data/workspace/ws-notifications.js new file mode 100644 index 0000000000000000000000000000000000000000..4ab58d1475191771ecd6e1ffc4d1d173f4d22672 --- /dev/null +++ b/frontend/playwright/data/workspace/ws-notifications.js @@ -0,0 +1,7 @@ +export const presenceFixture = { + "~:type": "~:presence", + "~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849", + "~:session-id": "~u37730924-d520-80f1-8004-4ae6e5c3942d", + "~:profile-id": "~uc7ce0794-0992-8105-8004-38e630f29a9b", + "~:subs-id": "~uc7ce0794-0992-8105-8004-38f280443849", +}; diff --git a/frontend/playwright/helpers/MockWebSocketHelper.js b/frontend/playwright/helpers/MockWebSocketHelper.js new file mode 100644 index 0000000000000000000000000000000000000000..8cf63f973790eac3c921b50788c635d0c3bd34ac --- /dev/null +++ b/frontend/playwright/helpers/MockWebSocketHelper.js @@ -0,0 +1,94 @@ +export class MockWebSocketHelper extends EventTarget { + static #mocks = new Map(); + + static async init(page) { + this.#mocks = new Map(); + + await page.exposeFunction("onMockWebSocketConstructor", (url) => { + const webSocket = new MockWebSocketHelper(page, url); + this.#mocks.set(url, webSocket); + }); + await page.exposeFunction("onMockWebSocketSpyMessage", (url, data) => { + if (!this.#mocks.has(url)) { + throw new Error(`WebSocket with URL ${url} not found`); + } + this.#mocks.get(url).dispatchEvent(new MessageEvent("message", { data })); + }); + await page.exposeFunction( + "onMockWebSocketSpyClose", + (url, code, reason) => { + if (!this.#mocks.has(url)) { + throw new Error(`WebSocket with URL ${url} not found`); + } + this.#mocks + .get(url) + .dispatchEvent(new CloseEvent("close", { code, reason })); + }, + ); + await page.addInitScript({ path: "playwright/scripts/MockWebSocket.js" }); + } + + static waitForURL(url) { + return new Promise((resolve) => { + const intervalID = setInterval(() => { + for (const [wsURL, ws] of this.#mocks) { + if (wsURL.includes(url)) { + clearInterval(intervalID); + return resolve(ws); + } + } + }, 30); + }); + } + + #page = null; + #url; + + constructor(page, url, protocols) { + super(); + this.#page = page; + this.#url = url; + } + + mockOpen(options) { + return this.#page.evaluate( + ({ url, options }) => { + if (typeof WebSocket.getByURL !== "function") { + throw new Error( + "WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?", + ); + } + WebSocket.getByURL(url).mockOpen(options); + }, + { url: this.#url, options }, + ); + } + + mockMessage(data) { + return this.#page.evaluate( + ({ url, data }) => { + if (typeof WebSocket.getByURL !== "function") { + throw new Error( + "WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?", + ); + } + WebSocket.getByURL(url).mockMessage(data); + }, + { url: this.#url, data }, + ); + } + + mockClose() { + return this.#page.evaluate( + ({ url }) => { + if (typeof WebSocket.getByURL !== "function") { + throw new Error( + "WebSocket.getByURL is not a function. Did you forget to call MockWebSocket.init(page)?", + ); + } + WebSocket.getByURL(url).mockClose(); + }, + { url: this.#url }, + ); + } +} diff --git a/frontend/playwright/scripts/MockWebSocket.js b/frontend/playwright/scripts/MockWebSocket.js new file mode 100644 index 0000000000000000000000000000000000000000..a401e6552f50fb1269b5df7580fa9a3df73585ad --- /dev/null +++ b/frontend/playwright/scripts/MockWebSocket.js @@ -0,0 +1,238 @@ +window.WebSocket = class MockWebSocket extends EventTarget { + static CONNECTING = 0; + static OPEN = 1; + static CLOSING = 2; + static CLOSED = 3; + + static #mocks = new Map(); + + static getAll() { + return this.#mocks.values(); + } + + static getByURL(url) { + if (this.#mocks.has(url)) { + return this.#mocks.get(url); + } + for (const [wsURL, ws] of this.#mocks) { + if (wsURL.includes(url)) { + return ws; + } + } + return undefined; + } + + #url; + #protocols; + #protocol = ""; + #binaryType = "blob"; + #bufferedAmount = 0; + #extensions = ""; + #readyState = MockWebSocket.CONNECTING; + + #onopen = null; + #onerror = null; + #onmessage = null; + #onclose = null; + + #spyMessage = null; + #spyClose = null; + + constructor(url, protocols) { + super(); + + this.#url = url; + this.#protocols = protocols || []; + + MockWebSocket.#mocks.set(this.#url, this); + + if (typeof window["onMockWebSocketConstructor"] === "function") { + onMockWebSocketConstructor(this.#url, this.#protocols); + } + if (typeof window["onMockWebSocketSpyMessage"] === "function") { + this.#spyMessage = onMockWebSocketSpyMessage; + } + if (typeof window["onMockWebSocketSpyClose"] === "function") { + this.#spyClose = onMockWebSocketSpyClose; + } + } + + set binaryType(binaryType) { + if (!["blob", "arraybuffer"].includes(binaryType)) { + return; + } + this.#binaryType = binaryType; + } + + get binaryType() { + return this.#binaryType; + } + + get bufferedAmount() { + return this.#bufferedAmount; + } + + get extensions() { + return this.#extensions; + } + + get readyState() { + return this.#readyState; + } + + get protocol() { + return this.#protocol; + } + + get url() { + return this.#url; + } + + set onopen(callback) { + this.removeEventListener("open", this.#onopen); + this.#onopen = null; + + if (typeof callback === "function") { + this.addEventListener("open", callback); + this.#onopen = callback; + } + } + + get onopen() { + return this.#onopen; + } + + set onerror(callback) { + this.removeEventListener("error", this.#onerror); + this.#onerror = null; + + if (typeof callback === "function") { + this.addEventListener("error", callback); + this.#onerror = callback; + } + } + + get onerror() { + return this.#onerror; + } + + set onmessage(callback) { + this.removeEventListener("message", this.#onmessage); + this.#onmessage = null; + + if (typeof callback === "function") { + this.addEventListener("message", callback); + this.#onmessage = callback; + } + } + + get onmessage() { + return this.#onmessage; + } + + set onclose(callback) { + this.removeEventListener("close", this.#onclose); + this.#onclose = null; + + if (typeof callback === "function") { + this.addEventListener("close", callback); + this.#onclose = callback; + } + } + + get onclose() { + return this.#onclose; + } + + get mockProtocols() { + return this.#protocols; + } + + spyClose(callback) { + if (typeof callback !== "function") { + throw new TypeError("Invalid callback"); + } + this.#spyClose = callback; + return this; + } + + spyMessage(callback) { + if (typeof callback !== "function") { + throw new TypeError("Invalid callback"); + } + this.#spyMessage = callback; + return this; + } + + mockOpen(options) { + this.#protocol = options?.protocol || ""; + this.#extensions = options?.extensions || ""; + this.#readyState = MockWebSocket.OPEN; + this.dispatchEvent(new Event("open")); + return this; + } + + mockError(error) { + this.#readyState = MockWebSocket.CLOSED; + this.dispatchEvent(new ErrorEvent("error", { error })); + return this; + } + + mockMessage(data) { + if (this.#readyState !== MockWebSocket.OPEN) { + throw new Error("MockWebSocket is not connected"); + } + this.dispatchEvent(new MessageEvent("message", { data })); + return this; + } + + mockClose(code, reason) { + this.#readyState = MockWebSocket.CLOSED; + this.dispatchEvent( + new CloseEvent("close", { code: code || 1000, reason: reason || "" }), + ); + return this; + } + + send(data) { + if (this.#readyState === MockWebSocket.CONNECTING) { + throw new DOMException( + "InvalidStateError", + "MockWebSocket is not connected", + ); + } + + if (this.#spyMessage) { + this.#spyMessage(this.url, data); + } + } + + close(code, reason) { + if ( + code && + !Number.isInteger(code) && + code !== 1000 && + (code < 3000 || code > 4999) + ) { + throw new DOMException("InvalidAccessError", "Invalid code"); + } + + if (reason && typeof reason === "string") { + const reasonBytes = new TextEncoder().encode(reason); + if (reasonBytes.length > 123) { + throw new DOMException("SyntaxError", "Reason is too long"); + } + } + + if ( + [MockWebSocket.CLOSED, MockWebSocket.CLOSING].includes(this.#readyState) + ) { + return; + } + + this.#readyState = MockWebSocket.CLOSING; + if (this.#spyClose) { + this.#spyClose(this.url, code, reason); + } + } +}; diff --git a/frontend/playwright/ui/pages/BasePage.js b/frontend/playwright/ui/pages/BasePage.js new file mode 100644 index 0000000000000000000000000000000000000000..b233be060498bf43c12a31ec36e9461290ea06ee --- /dev/null +++ b/frontend/playwright/ui/pages/BasePage.js @@ -0,0 +1,41 @@ +export class BasePage { + static async mockRPC(page, path, jsonFilename, options) { + if (!page) { + throw new TypeError("Invalid page argument. Must be a Playwright page."); + } + if (typeof path !== "string" && !(path instanceof RegExp)) { + throw new TypeError( + "Invalid path argument. Must be a string or a RegExp.", + ); + } + + const url = typeof path === "string" ? `**/api/rpc/command/${path}` : path; + const interceptConfig = { + status: 200, + contentType: "application/transit+json", + ...options, + }; + return page.route(url, (route) => + route.fulfill({ + ...interceptConfig, + path: `playwright/data/${jsonFilename}`, + }), + ); + } + + #page = null; + + constructor(page) { + this.#page = page; + } + + get page() { + return this.#page; + } + + async mockRPC(path, jsonFilename, options) { + return BasePage.mockRPC(this.page, path, jsonFilename, options); + } +} + +export default BasePage; diff --git a/frontend/playwright/ui/pages/BaseWebSocketPage.js b/frontend/playwright/ui/pages/BaseWebSocketPage.js new file mode 100644 index 0000000000000000000000000000000000000000..21855312eebed80311edec1a564c43031d4c084b --- /dev/null +++ b/frontend/playwright/ui/pages/BaseWebSocketPage.js @@ -0,0 +1,32 @@ +import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper"; +import BasePage from "./BasePage"; + +export class BaseWebSocketPage extends BasePage { + /** + * This should be called on `test.beforeEach`. + * + * @param {Page} page + * @returns + */ + static async initWebSockets(page) { + await MockWebSocketHelper.init(page); + } + + /** + * Returns a promise that resolves when a WebSocket with the given URL is created. + * + * @param {string} url + * @returns {Promise} + */ + async waitForWebSocket(url) { + return MockWebSocketHelper.waitForURL(url); + } + + /** + * + * @returns {Promise} + */ + async waitForNotificationsWebSocket() { + return this.waitForWebSocket("ws://localhost:3000/ws/notifications"); + } +} diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js new file mode 100644 index 0000000000000000000000000000000000000000..6d340c62e98df9dea96c3fce5a41c222d3661af2 --- /dev/null +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -0,0 +1,269 @@ +import { expect } from "@playwright/test"; +import { BaseWebSocketPage } from "./BaseWebSocketPage"; + +export class DashboardPage extends BaseWebSocketPage { + static async init(page) { + await BaseWebSocketPage.initWebSockets(page); + + await BaseWebSocketPage.mockRPC( + page, + "get-teams", + "logged-in-user/get-teams-default.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-font-variants?team-id=*", + "workspace/get-font-variants-empty.json", + ); + + await BaseWebSocketPage.mockRPC( + page, + "get-projects?team-id=*", + "logged-in-user/get-projects-default.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-team-members?team-id=*", + "logged-in-user/get-team-members-your-penpot.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-team-users?team-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-unread-comment-threads?team-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-team-recent-files?team-id=*", + "logged-in-user/get-team-recent-files-empty.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-profiles-for-file-comments", + "workspace/get-profile-for-file-comments.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-builtin-templates", + "logged-in-user/get-built-in-templates-empty.json", + ); + } + + static anyTeamId = "c7ce0794-0992-8105-8004-38e630f40f6d"; + static secondTeamId = "dd33ff88-f4e5-8033-8003-8096cc07bdf3"; + static draftProjectId = "c7ce0794-0992-8105-8004-38e630f7920b"; + + constructor(page) { + super(page); + + this.sidebar = page.getByTestId("dashboard-sidebar"); + this.sidebarMenu = this.sidebar.getByRole("menu"); + this.mainHeading = page + .getByTestId("dashboard-header") + .getByRole("heading", { level: 1 }); + + this.addProjectButton = page.getByRole("button", { name: "+ NEW PROJECT" }); + this.projectName = page.getByText("Project 1"); + + this.draftsLink = this.sidebar.getByText("Drafts"); + this.fontsLink = this.sidebar.getByText("Fonts"); + this.libsLink = this.sidebar.getByText("Libraries"); + + this.searchButton = page.getByRole("button", { name: "dashboard-search" }); + this.searchInput = page.getByPlaceholder("Search…"); + + this.teamDropdown = this.sidebar.getByRole("button", { + name: "Your Penpot", + }); + this.userAccount = this.sidebar.getByRole("button", { + name: /Princesa Leia/, + }); + this.userProfileOption = this.sidebarMenu.getByText("Your account"); + } + + async setupDraftsEmpty() { + await this.mockRPC( + "get-project-files?project-id=*", + "dashboard/get-project-files-empty.json", + ); + } + + async setupSearchEmpty() { + await this.mockRPC("search-files", "dashboard/search-files-empty.json", { + method: "POST", + }); + } + + async setupLibrariesEmpty() { + await this.mockRPC( + "get-team-shared-files?team-id=*", + "dashboard/get-shared-files-empty.json", + ); + } + + async setupDrafts() { + await this.mockRPC( + "get-project-files?project-id=*", + "dashboard/get-project-files.json", + ); + } + + async setupNewProject() { + await this.mockRPC("create-project", "dashboard/create-project.json", { + method: "POST", + }); + await this.mockRPC( + "get-projects?team-id=*", + "dashboard/get-projects-new.json", + ); + } + + async setupDashboardFull() { + await this.mockRPC( + "get-projects?team-id=*", + "dashboard/get-projects-full.json", + ); + await this.mockRPC( + "get-project-files?project-id=*", + "dashboard/get-project-files.json", + ); + await this.mockRPC( + "get-team-shared-files?team-id=*", + "dashboard/get-shared-files.json", + ); + await this.mockRPC( + "get-team-shared-files?project-id=*", + "dashboard/get-shared-files.json", + ); + await this.mockRPC( + "get-team-recent-files?team-id=*", + "dashboard/get-team-recent-files.json", + ); + await this.mockRPC( + "get-font-variants?team-id=*", + "dashboard/get-font-variants.json", + ); + await this.mockRPC("search-files", "dashboard/search-files.json", { + method: "POST", + }); + await this.mockRPC("search-files", "dashboard/search-files.json"); + await this.mockRPC("get-teams", "logged-in-user/get-teams-complete.json"); + } + + async setupAccessTokensEmpty() { + await this.mockRPC( + "get-access-tokens", + "dashboard/get-access-tokens-empty.json", + ); + } + + async createAccessToken() { + await this.mockRPC( + "create-access-token", + "dashboard/create-access-token.json", + { method: "POST" }, + ); + } + + async setupAccessTokens() { + await this.mockRPC("get-access-tokens", "dashboard/get-access-tokens.json"); + } + + async setupTeamInvitationsEmpty() { + await this.mockRPC( + "get-team-invitations?team-id=*", + "dashboard/get-team-invitations-empty.json", + ); + } + + async setupTeamInvitations() { + await this.mockRPC( + "get-team-invitations?team-id=*", + "dashboard/get-team-invitations.json", + ); + } + + async setupTeamWebhooksEmpty() { + await this.mockRPC( + "get-webhooks?team-id=*", + "dashboard/get-webhooks-empty.json", + ); + } + + async setupTeamWebhooks() { + await this.mockRPC("get-webhooks?team-id=*", "dashboard/get-webhooks.json"); + } + + async setupTeamSettings() { + await this.mockRPC( + "get-team-stats?team-id=*", + "dashboard/get-team-stats.json", + ); + } + + async goToDashboard() { + await this.page.goto( + `#/dashboard/team/${DashboardPage.anyTeamId}/projects`, + ); + await expect(this.mainHeading).toBeVisible(); + } + + async goToSecondTeamDashboard() { + await this.page.goto( + `#/dashboard/team/${DashboardPage.secondTeamId}/projects`, + ); + } + + async goToSecondTeamMembersSection() { + await this.page.goto( + `#/dashboard/team/${DashboardPage.secondTeamId}/members`, + ); + } + + async goToSecondTeamInvitationsSection() { + await this.page.goto( + `#/dashboard/team/${DashboardPage.secondTeamId}/invitations`, + ); + } + + async goToSecondTeamWebhooksSection() { + await this.page.goto( + `#/dashboard/team/${DashboardPage.secondTeamId}/webhooks`, + ); + } + + async goToSecondTeamWebhooksSection() { + await this.page.goto( + `#/dashboard/team/${DashboardPage.secondTeamId}/webhooks`, + ); + } + + async goToSecondTeamSettingsSection() { + await this.page.goto( + `#/dashboard/team/${DashboardPage.secondTeamId}/settings`, + ); + } + + async goToSearch() { + await this.page.goto(`#/dashboard/team/${DashboardPage.anyTeamId}/search`); + } + + async goToDrafts() { + await this.page.goto( + `#/dashboard/team/${DashboardPage.anyTeamId}/projects/${DashboardPage.draftProjectId}`, + ); + await expect(this.mainHeading).toHaveText("Drafts"); + } + + async goToAccount() { + await this.userAccount.click(); + + await this.userProfileOption.click(); + } +} + +export default DashboardPage; diff --git a/frontend/playwright/ui/pages/LoginPage.js b/frontend/playwright/ui/pages/LoginPage.js new file mode 100644 index 0000000000000000000000000000000000000000..0ee5f863cbf385c2ea89255c6fe34c3dbbfe2631 --- /dev/null +++ b/frontend/playwright/ui/pages/LoginPage.js @@ -0,0 +1,83 @@ +import { BasePage } from "./BasePage"; + +export class LoginPage extends BasePage { + constructor(page) { + super(page); + this.loginButton = page.getByRole("button", { name: "Login" }); + this.password = page.getByLabel("Password"); + this.userName = page.getByLabel("Email"); + this.invalidCredentialsError = page.getByText( + "Email or password is incorrect", + ); + this.invalidEmailError = page.getByText("Enter a valid email please"); + this.initialHeading = page.getByRole("heading", { + name: "Log into my account", + }); + } + + async fillEmailAndPasswordInputs(email, password) { + await this.userName.fill(email); + await this.password.fill(password); + } + + async clickLoginButton() { + await this.loginButton.click(); + } + + async initWithLoggedOutUser() { + await this.mockRPC("get-profile", "get-profile-anonymous.json"); + } + + async setupLoggedInUser() { + await this.mockRPC( + "get-profile", + "logged-in-user/get-profile-logged-in.json", + ); + await this.mockRPC("get-teams", "logged-in-user/get-teams-default.json"); + await this.mockRPC( + "get-font-variants?team-id=*", + "logged-in-user/get-font-variants-empty.json", + ); + await this.mockRPC( + "get-projects?team-id=*", + "logged-in-user/get-projects-default.json", + ); + await this.mockRPC( + "get-team-members?team-id=*", + "logged-in-user/get-team-members-your-penpot.json", + ); + await this.mockRPC( + "get-team-users?team-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await this.mockRPC( + "get-unread-comment-threads?team-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await this.mockRPC( + "get-team-recent-files?team-id=*", + "logged-in-user/get-team-recent-files-empty.json", + ); + await this.mockRPC( + "get-profiles-for-file-comments", + "logged-in-user/get-profiles-for-file-comments-empty.json", + ); + } + + async setupLoginSuccess() { + await this.mockRPC( + "login-with-password", + "logged-in-user/login-with-password-success.json", + ); + } + + async setupLoginError() { + await this.mockRPC( + "login-with-password", + "login-with-password-error.json", + { status: 400 }, + ); + } +} + +export default LoginPage; diff --git a/frontend/playwright/ui/pages/OnboardingPage.js b/frontend/playwright/ui/pages/OnboardingPage.js new file mode 100644 index 0000000000000000000000000000000000000000..81e19958891e0c466835aba76c12c32ae9872394 --- /dev/null +++ b/frontend/playwright/ui/pages/OnboardingPage.js @@ -0,0 +1,45 @@ +import { BaseWebSocketPage } from "./BaseWebSocketPage"; + +export class OnboardingPage extends BaseWebSocketPage { + constructor(page) { + super(page); + this.submitButton = page.getByRole("Button", { name: "Next" }); + } + + async fillOnboardingInputsStep1() { + await this.page.getByText("Personal").click(); + await this.page.getByText("Select option").click(); + await this.page.getByText("Testing before self-hosting").click(); + + await this.submitButton.click(); + } + + async fillOnboardingInputsStep2() { + await this.page.getByText("Figma").click(); + + await this.submitButton.click(); + } + + async fillOnboardingInputsStep3() { + await this.page.getByText("Select option").first().click(); + await this.page.getByText("Product Managment").click(); + await this.page.getByText("Select option").first().click(); + await this.page.getByText("Director").click(); + await this.page.getByText("Select option").click(); + await this.page.getByText("11-30").click(); + + await this.submitButton.click(); + } + + async fillOnboardingInputsStep4() { + await this.page.getByText("Other").click(); + await this.page.getByPlaceholder("Other (specify)").fill("Another"); + await this.submitButton.click(); + } + + async fillOnboardingInputsStep5() { + await this.page.getByText("Event").click(); + } +} + +export default OnboardingPage; diff --git a/frontend/playwright/ui/pages/ViewerPage.js b/frontend/playwright/ui/pages/ViewerPage.js new file mode 100644 index 0000000000000000000000000000000000000000..41fd45a2382c180aac90fe003324d3ad67e84e17 --- /dev/null +++ b/frontend/playwright/ui/pages/ViewerPage.js @@ -0,0 +1,117 @@ +import { BaseWebSocketPage } from "./BaseWebSocketPage"; + +export class ViewerPage extends BaseWebSocketPage { + static anyFileId = "c7ce0794-0992-8105-8004-38f280443849"; + static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a"; + + /** + * This should be called on `test.beforeEach`. + * + * @param {Page} page + * @returns + */ + static async init(page) { + await BaseWebSocketPage.initWebSockets(page); + } + + async setupLoggedInUser() { + await this.mockRPC( + "get-profile", + "logged-in-user/get-profile-logged-in.json", + ); + } + + async setupEmptyFile() { + await this.mockRPC( + /get\-view\-only\-bundle\?/, + "viewer/get-view-only-bundle-empty-file.json", + ); + await this.mockRPC( + "get-comment-threads?file-id=*", + "workspace/get-comment-threads-empty.json", + ); + await this.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "viewer/get-file-fragment-empty-file.json", + ); + } + + async setupFileWithSingleBoard() { + await this.mockRPC( + /get\-view\-only\-bundle\?/, + "viewer/get-view-only-bundle-single-board.json", + ); + await this.mockRPC( + "get-comment-threads?file-id=*", + "workspace/get-comment-threads-empty.json", + ); + await this.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "viewer/get-file-fragment-single-board.json", + ); + } + + async setupFileWithComments() { + await this.mockRPC( + /get\-view\-only\-bundle\?/, + "viewer/get-view-only-bundle-single-board.json", + ); + await this.mockRPC( + "get-comment-threads?file-id=*", + "workspace/get-comment-threads-not-empty.json", + ); + await this.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "viewer/get-file-fragment-single-board.json", + ); + await this.mockRPC( + "get-comments?thread-id=*", + "workspace/get-thread-comments.json", + ); + await this.mockRPC( + "update-comment-thread-status", + "workspace/update-comment-thread-status.json", + ); + } + + #ws = null; + + constructor(page) { + super(page); + } + + async goToViewer({ + fileId = ViewerPage.anyFileId, + pageId = ViewerPage.anyPageId, + } = {}) { + await this.page.goto( + `/#/view/${fileId}?page-id=${pageId}§ion=interactions&index=0`, + ); + + this.#ws = await this.waitForNotificationsWebSocket(); + await this.#ws.mockOpen(); + } + + async cleanUp() { + await this.#ws.mockClose(); + } + + async showComments(clickOptions = {}) { + await this.page + .getByRole("button", { name: "Comments (G C)" }) + .click(clickOptions); + } + + async showCommentsThread(number, clickOptions = {}) { + await this.page + .getByTestId("floating-thread-bubble") + .filter({ hasText: number.toString() }) + .click(clickOptions); + } + + async showCode(clickOptions = {}) { + await this.page + .getByRole("button", { name: "Inspect (G I)" }) + .click(clickOptions); + } +} diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js new file mode 100644 index 0000000000000000000000000000000000000000..7e5bf6b36f6d3a62e74beeed4f94826754e33f3b --- /dev/null +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -0,0 +1,237 @@ +import { expect } from "@playwright/test"; +import { BaseWebSocketPage } from "./BaseWebSocketPage"; + +export class WorkspacePage extends BaseWebSocketPage { + /** + * This should be called on `test.beforeEach`. + * + * @param {Page} page + * @returns + */ + static async init(page) { + await BaseWebSocketPage.initWebSockets(page); + + await BaseWebSocketPage.mockRPC( + page, + "get-profile", + "logged-in-user/get-profile-logged-in.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-team-users?file-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-comment-threads?file-id=*", + "workspace/get-comment-threads-empty.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-project?id=*", + "workspace/get-project-default.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-team?id=*", + "workspace/get-team-default.json", + ); + await BaseWebSocketPage.mockRPC( + page, + "get-profiles-for-file-comments?file-id=*", + "workspace/get-profile-for-file-comments.json", + ); + } + + static anyProjectId = "c7ce0794-0992-8105-8004-38e630f7920b"; + static anyFileId = "c7ce0794-0992-8105-8004-38f280443849"; + static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a"; + + #ws = null; + + constructor(page) { + super(page); + this.pageName = page.getByTestId("page-name"); + this.presentUserListItems = page + .getByTestId("active-users-list") + .getByAltText("Princesa Leia"); + this.viewport = page.getByTestId("viewport"); + this.rootShape = page.locator( + `[id="shape-00000000-0000-0000-0000-000000000000"]`, + ); + this.toolbarOptions = page.getByTestId("toolbar-options"); + this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" }); + this.toggleToolbarButton = page.getByRole("button", { + name: "Toggle toolbar", + }); + this.colorpicker = page.getByTestId("colorpicker"); + this.layers = page.getByTestId("layer-tree"); + this.palette = page.getByTestId("palette"); + this.sidebar = page.getByTestId("left-sidebar"); + this.rightSidebar = page.getByTestId("right-sidebar"); + this.selectionRect = page.getByTestId("workspace-selection-rect"); + this.horizontalScrollbar = page.getByTestId("horizontal-scrollbar"); + this.librariesModal = page.getByTestId("libraries-modal"); + this.togglePalettesVisibility = page.getByTestId( + "toggle-palettes-visibility", + ); + } + + async goToWorkspace({ + fileId = WorkspacePage.anyFileId, + pageId = WorkspacePage.anyPageId, + } = {}) { + await this.page.goto( + `/#/workspace/${WorkspacePage.anyProjectId}/${fileId}?page-id=${pageId}`, + ); + + this.#ws = await this.waitForNotificationsWebSocket(); + await this.#ws.mockOpen(); + await this.#waitForWebSocketReadiness(); + } + + async #waitForWebSocketReadiness() { + // TODO: find a better event to settle whether the app is ready to receive notifications via ws + await expect(this.pageName).toHaveText("Page 1"); + } + + async sendPresenceMessage(fixture) { + await this.#ws.mockMessage(JSON.stringify(fixture)); + } + + async cleanUp() { + await this.#ws.mockClose(); + } + + async setupEmptyFile() { + await this.mockRPC( + "get-profile", + "logged-in-user/get-profile-logged-in.json", + ); + await this.mockRPC( + "get-team-users?file-id=*", + "logged-in-user/get-team-users-single-user.json", + ); + await this.mockRPC( + "get-comment-threads?file-id=*", + "workspace/get-comment-threads-empty.json", + ); + await this.mockRPC( + "get-project?id=*", + "workspace/get-project-default.json", + ); + await this.mockRPC("get-team?id=*", "workspace/get-team-default.json"); + await this.mockRPC( + "get-profiles-for-file-comments?file-id=*", + "workspace/get-profile-for-file-comments.json", + ); + await this.mockRPC(/get\-file\?/, "workspace/get-file-blank.json"); + await this.mockRPC( + "get-file-object-thumbnails?file-id=*", + "workspace/get-file-object-thumbnails-blank.json", + ); + await this.mockRPC( + "get-font-variants?team-id=*", + "workspace/get-font-variants-empty.json", + ); + await this.mockRPC( + "get-file-fragment?file-id=*", + "workspace/get-file-fragment-blank.json", + ); + await this.mockRPC( + "get-file-libraries?file-id=*", + "workspace/get-file-libraries-empty.json", + ); + } + + async clickWithDragViewportAt(x, y, width, height) { + await this.page.waitForTimeout(100); + await this.viewport.hover({ position: { x, y } }); + await this.page.mouse.down(); + await this.viewport.hover({ position: { x: x + width, y: y + height } }); + await this.page.mouse.up(); + } + + async panOnViewportAt(x, y, width, height) { + await this.page.waitForTimeout(100); + await this.viewport.hover({ position: { x, y } }); + await this.page.mouse.down({ button: "middle" }); + await this.viewport.hover({ position: { x: x + width, y: y + height } }); + await this.page.mouse.up({ button: "middle" }); + } + + async togglePages() { + const pagesToggle = this.page.getByText("Pages"); + await pagesToggle.click(); + } + + async moveSelectionToShape(name) { + await this.page.locator("rect.viewport-selrect").hover(); + await this.page.mouse.down(); + await this.viewport.getByTestId(name).first().hover({ force: true }); + await this.page.mouse.up(); + } + + async clickLeafLayer(name, clickOptions = {}) { + const layer = this.layers.getByText(name); + await layer.click(clickOptions); + } + + async clickToggableLayer(name, clickOptions = {}) { + const layer = this.layers + .getByTestId("layer-item") + .filter({ has: this.page.getByText(name) }); + await layer.getByRole("button").click(clickOptions); + } + + async expectSelectedLayer(name) { + await expect( + this.layers + .getByTestId("layer-row") + .filter({ has: this.page.getByText(name) }), + ).toHaveClass(/selected/); + } + + async expectHiddenToolbarOptions() { + await expect(this.toolbarOptions).toHaveCSS("opacity", "0"); + } + + async clickAssets(clickOptions = {}) { + await this.sidebar.getByText("Assets").click(clickOptions); + } + + async openLibrariesModal(clickOptions = {}) { + await this.sidebar.getByText("Libraries").click(clickOptions); + await expect(this.librariesModal).toBeVisible(); + } + + async clickLibrary(name, clickOptions = {}) { + await this.page + .getByTestId("library-item") + .filter({ hasText: name }) + .getByRole("button") + .click(clickOptions); + } + + async closeLibrariesModal(clickOptions = {}) { + await this.librariesModal + .getByRole("button", { name: "Close" }) + .click(clickOptions); + } + + async clickColorPalette(clickOptions = {}) { + await this.palette + .getByRole("button", { name: "Color Palette (Alt+P)" }) + .click(clickOptions); + } + + async clickColorPalette(clickOptions = {}) { + await this.palette + .getByRole("button", { name: "Color Palette (Alt+P)" }) + .click(clickOptions); + } + + async clickTogglePalettesVisibility(clickOptions = {}) { + await this.togglePalettesVisibility.click(clickOptions); + } +} diff --git a/frontend/playwright/ui/specs/colorpicker.spec.js b/frontend/playwright/ui/specs/colorpicker.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..0ea20f52bafc10321c8460ae4a89ddafc21a2869 --- /dev/null +++ b/frontend/playwright/ui/specs/colorpicker.spec.js @@ -0,0 +1,24 @@ +import { test, expect } from "@playwright/test"; +import { WorkspacePage } from "../pages/WorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); +}); + +// Fix for https://tree.taiga.io/project/penpot/issue/7549 +test("Bug 7549 - User clicks on color swatch to display the color picker next to it", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(page); + + await workspacePage.goToWorkspace(); + const swatch = workspacePage.page.getByRole("button", { name: "E8E9EA" }); + const swatchBox = await swatch.boundingBox(); + await swatch.click(); + + await expect(workspacePage.colorpicker).toBeVisible(); + const pickerBox = await workspacePage.colorpicker.boundingBox(); + const distance = swatchBox.x - (pickerBox.x + pickerBox.width); + expect(distance).toBeLessThan(60); +}); diff --git a/frontend/playwright/ui/specs/dashboard.spec.js b/frontend/playwright/ui/specs/dashboard.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..b11e1a3264d106d12806a78ae5400b6ef1884f23 --- /dev/null +++ b/frontend/playwright/ui/specs/dashboard.spec.js @@ -0,0 +1,54 @@ +import { test, expect } from "@playwright/test"; +import DashboardPage from "../pages/DashboardPage"; + +test.beforeEach(async ({ page }) => { + await DashboardPage.init(page); + await DashboardPage.mockRPC( + page, + "get-profile", + "logged-in-user/get-profile-logged-in-no-onboarding.json", + ); +}); + +test("Dashboad page has title ", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await expect(dashboardPage.page).toHaveURL(/dashboard/); + await expect(dashboardPage.mainHeading).toBeVisible(); +}); + +test("User can create a new project", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupNewProject(); + + await dashboardPage.goToDashboard(); + await dashboardPage.addProjectButton.click(); + + await expect(dashboardPage.projectName).toBeVisible(); +}); + +test("User goes to draft page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDraftsEmpty(); + + await dashboardPage.goToDashboard(); + await dashboardPage.draftsLink.click(); + + await expect(dashboardPage.mainHeading).toHaveText("Drafts"); +}); + +test("Lists files in the drafts page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDrafts(); + + await dashboardPage.goToDrafts(); + + await expect( + dashboardPage.page.getByRole("button", { name: /New File 1/ }), + ).toBeVisible(); + await expect( + dashboardPage.page.getByRole("button", { name: /New File 2/ }), + ).toBeVisible(); +}); diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..3b28bb534314318841d594d0b17473862f89d044 --- /dev/null +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -0,0 +1,145 @@ +import { test, expect } from "@playwright/test"; +import { WorkspacePage } from "../pages/WorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); +}); + +const multipleConstraintsFileId = `03bff843-920f-81a1-8004-756365e1eb6a`; +const multipleConstraintsPageId = `03bff843-920f-81a1-8004-756365e1eb6b`; +const multipleAttributesFileId = `1795a568-0df0-8095-8004-7ba741f56be2`; +const multipleAttributesPageId = `1795a568-0df0-8095-8004-7ba741f56be3`; + +const setupFileWithMultipeConstraints = async (workspace) => { + await workspace.setupEmptyFile(); + await workspace.mockRPC( + /get\-file\?/, + "design/get-file-multiple-constraints.json", + ); + await workspace.mockRPC( + "get-file-object-thumbnails?file-id=*", + "design/get-file-object-thumbnails-multiple-constraints.json", + ); + await workspace.mockRPC( + "get-file-fragment?file-id=*", + "design/get-file-fragment-multiple-constraints.json", + ); +}; + +const setupFileWithMultipeAttributes = async (workspace) => { + await workspace.setupEmptyFile(); + await workspace.mockRPC( + /get\-file\?/, + "design/get-file-multiple-attributes.json", + ); + await workspace.mockRPC( + "get-file-object-thumbnails?file-id=*", + "design/get-file-object-thumbnails-multiple-attributes.json", + ); +}; + +test.describe("Constraints", () => { + test("Constraint dropdown shows 'Mixed' when multiple layers are selected with different constraints", async ({ + page, + }) => { + const workspace = new WorkspacePage(page); + await setupFileWithMultipeConstraints(workspace); + await workspace.goToWorkspace({ + fileId: multipleConstraintsFileId, + pageId: multipleConstraintsPageId, + }); + + await workspace.clickToggableLayer("Board"); + await workspace.clickLeafLayer("Ellipse"); + await workspace.clickLeafLayer("Rectangle", { modifiers: ["Shift"] }); + + const constraintVDropdown = workspace.page.getByTestId( + "constraint-v-select", + ); + await expect(constraintVDropdown).toContainText("Mixed"); + const constraintHDropdown = workspace.page.getByTestId( + "constraint-h-select", + ); + await expect(constraintHDropdown).toContainText("Mixed"); + + expect(false); + }); +}); + +test.describe("Multiple shapes attributes", () => { + test("User selects multiple shapes with sames fills, strokes, shadows and blur", async ({ + page, + }) => { + const workspace = new WorkspacePage(page); + await setupFileWithMultipeConstraints(workspace); + await workspace.goToWorkspace({ + fileId: multipleConstraintsFileId, + pageId: multipleConstraintsPageId, + }); + + await workspace.clickToggableLayer("Board"); + await workspace.clickLeafLayer("Ellipse"); + await workspace.clickLeafLayer("Rectangle", { modifiers: ["Shift"] }); + + await expect(workspace.page.getByTestId("add-fill")).toBeVisible(); + await expect(workspace.page.getByTestId("add-stroke")).toBeVisible(); + await expect(workspace.page.getByTestId("add-shadow")).toBeVisible(); + await expect(workspace.page.getByTestId("add-blur")).toBeVisible(); + }); + + test("User selects multiple shapes with different fills, strokes, shadows and blur", async ({ + page, + }) => { + const workspace = new WorkspacePage(page); + await setupFileWithMultipeAttributes(workspace); + await workspace.goToWorkspace({ + fileId: multipleAttributesFileId, + pageId: multipleAttributesPageId, + }); + + await workspace.clickLeafLayer("Ellipse"); + await workspace.clickLeafLayer("Rectangle", { modifiers: ["Shift"] }); + + await expect(workspace.page.getByTestId("add-fill")).toBeHidden(); + await expect(workspace.page.getByTestId("add-stroke")).toBeHidden(); + await expect(workspace.page.getByTestId("add-shadow")).toBeHidden(); + await expect(workspace.page.getByTestId("add-blur")).toBeHidden(); + }); +}); + +test("BUG 7760 - Layout losing properties when changing parents", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-7760.json"); + await workspacePage.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "workspace/get-file-fragment-7760.json", + ); + await workspacePage.mockRPC( + "update-file?id=*", + "workspace/update-file-create-rect.json", + ); + + await workspacePage.goToWorkspace({ + fileId: "cd90e028-326a-80b4-8004-7cdec16ffad5", + pageId: "cd90e028-326a-80b4-8004-7cdec16ffad6", + }); + + // Select the flex board and drag it into the other container board + await workspacePage.clickLeafLayer("Flex Board"); + + // Move the first board into the second + const hAuto = await workspacePage.page.getByTitle("Fit content (Horizontal)"); + const vAuto = await workspacePage.page.getByTitle("Fit content (Vertical)"); + + await expect(vAuto.locator("input")).toBeChecked(); + await expect(hAuto.locator("input")).toBeChecked(); + + await workspacePage.moveSelectionToShape("Container Board"); + + // The first board properties should still be auto width/height + await expect(vAuto.locator("input")).toBeChecked(); + await expect(hAuto.locator("input")).toBeChecked(); +}); diff --git a/frontend/playwright/ui/specs/example.spec.js b/frontend/playwright/ui/specs/example.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ad5e4712dd176f736ec3bed953cf83ed49ff28bc --- /dev/null +++ b/frontend/playwright/ui/specs/example.spec.js @@ -0,0 +1,14 @@ +import { test, expect } from "@playwright/test"; + +test("Has title", async ({ page }) => { + await page.route("**/api/rpc/command/get-profile", (route) => { + route.fulfill({ + status: 200, + contentType: "application/transit+json", + path: "playwright/data/get-profile-anonymous.json", + }); + }); + await page.goto("/"); + + await expect(page).toHaveTitle(/Penpot/); +}); diff --git a/frontend/playwright/ui/specs/login.spec.js b/frontend/playwright/ui/specs/login.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..4a2604f4b16c0bdabf1815d8015cccbb8088bc04 --- /dev/null +++ b/frontend/playwright/ui/specs/login.spec.js @@ -0,0 +1,61 @@ +import { test, expect } from "@playwright/test"; +import { LoginPage } from "../pages/LoginPage"; + +test.beforeEach(async ({ page }) => { + const login = new LoginPage(page); + await login.initWithLoggedOutUser(); + + await page.goto("/#/auth/login"); +}); + +test("User is redirected to the login page when logged out", async ({ + page, +}) => { + const loginPage = new LoginPage(page); + + await loginPage.setupLoggedInUser(); + + await expect(loginPage.page).toHaveURL(/auth\/login$/); + await expect(loginPage.initialHeading).toBeVisible(); +}); + +test.describe("Login form", () => { + test("User logs in by filling the login form", async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.setupLoginSuccess(); + await loginPage.setupLoggedInUser(); + + await loginPage.fillEmailAndPasswordInputs("foo@example.com", "loremipsum"); + await loginPage.clickLoginButton(); + + await page.waitForURL("**/dashboard/**"); + await expect(loginPage.page).toHaveURL(/dashboard/); + }); + + test("User gets error message when submitting an bad formatted email ", async ({ + page, + }) => { + const loginPage = new LoginPage(page); + await loginPage.setupLoginSuccess(); + + await loginPage.fillEmailAndPasswordInputs("foo", "lorenIpsum"); + + await expect(loginPage.invalidEmailError).toBeVisible(); + }); + + test("User gets error message when submitting wrong credentials", async ({ + page, + }) => { + const loginPage = new LoginPage(page); + await loginPage.setupLoginError(); + + await loginPage.fillEmailAndPasswordInputs( + "test@example.com", + "loremipsum", + ); + await loginPage.clickLoginButton(); + + await expect(loginPage.invalidCredentialsError).toBeVisible(); + await expect(loginPage.page).toHaveURL(/auth\/login$/); + }); +}); diff --git a/frontend/playwright/ui/specs/onboarding.spec.js b/frontend/playwright/ui/specs/onboarding.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..968c888251389d1d319afb5adb644825029f5e7b --- /dev/null +++ b/frontend/playwright/ui/specs/onboarding.spec.js @@ -0,0 +1,45 @@ +import { test, expect } from "@playwright/test"; +import DashboardPage from "../pages/DashboardPage"; +import OnboardingPage from "../pages/OnboardingPage"; + +test.beforeEach(async ({ page }) => { + await DashboardPage.init(page); + await DashboardPage.mockRPC( + page, + "get-profile", + "logged-in-user/get-profile-logged-in.json", + ); +}); + +test("User can complete the onboarding", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + const onboardingPage = new OnboardingPage(page); + + await dashboardPage.goToDashboard(); + await expect( + page.getByRole("heading", { name: "Help us get to know you" }), + ).toBeVisible(); + + await onboardingPage.fillOnboardingInputsStep1(); + await expect( + page.getByRole("heading", { name: "Which one of these tools do" }), + ).toBeVisible(); + + await onboardingPage.fillOnboardingInputsStep2(); + await expect( + page.getByRole("heading", { name: "Tell us about your job" }), + ).toBeVisible(); + + await onboardingPage.fillOnboardingInputsStep3(); + await expect( + page.getByRole("heading", { name: "Where would you like to get" }), + ).toBeVisible(); + + await onboardingPage.fillOnboardingInputsStep4(); + await expect( + page.getByRole("heading", { name: "How did you hear about Penpot?" }), + ).toBeVisible(); + + await onboardingPage.fillOnboardingInputsStep5(); + await expect(page.getByRole("button", { name: "Start" })).toBeEnabled(); +}); diff --git a/frontend/playwright/ui/specs/sidebar.spec.js b/frontend/playwright/ui/specs/sidebar.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..b97301889ef9a18e6c5c52751a447afa12cd431f --- /dev/null +++ b/frontend/playwright/ui/specs/sidebar.spec.js @@ -0,0 +1,73 @@ +import { test, expect } from "@playwright/test"; +import { WorkspacePage } from "../pages/WorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); +}); + +test.describe("Layers tab", () => { + test("BUG 7466 - Layers tab height extends to the bottom when 'Pages' is collapsed", async ({ + page, + }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + + await workspace.goToWorkspace(); + + const { height: heightExpanded } = await workspace.layers.boundingBox(); + await workspace.togglePages(); + const { height: heightCollapsed } = await workspace.layers.boundingBox(); + + expect(heightExpanded > heightCollapsed); + }); +}); + +test.describe("Assets tab", () => { + test("User adds a library and its automatically selected in the color palette", async ({ + page, + }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC( + "link-file-to-library", + "workspace/link-file-to-library.json", + ); + await workspacePage.mockRPC( + "unlink-file-from-library", + "workspace/unlink-file-from-library.json", + ); + await workspacePage.mockRPC( + "get-team-shared-files?team-id=*", + "workspace/get-team-shared-libraries-non-empty.json", + ); + + await workspacePage.goToWorkspace(); + + // Add Testing library 1 + await workspacePage.clickColorPalette(); + await workspacePage.clickAssets(); + // Now the get-file call should return a library + await workspacePage.mockRPC( + /get\-file\?/, + "workspace/get-file-library.json", + ); + await workspacePage.openLibrariesModal(); + await workspacePage.clickLibrary("Testing library 1"); + await workspacePage.closeLibrariesModal(); + + await expect( + workspacePage.palette.getByRole("button", { name: "test-color-187cd5" }), + ).toBeVisible(); + + // Remove Testing library 1 + await workspacePage.openLibrariesModal(); + await workspacePage.clickLibrary("Testing library 1"); + await workspacePage.closeLibrariesModal(); + + await expect( + workspacePage.palette.getByText( + "There are no color styles in your library yet", + ), + ).toBeVisible(); + }); +}); diff --git a/frontend/playwright/ui/specs/viewer-comments.spec.js b/frontend/playwright/ui/specs/viewer-comments.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..4ed32135a3769afce899d57de1976880fb3f47ec --- /dev/null +++ b/frontend/playwright/ui/specs/viewer-comments.spec.js @@ -0,0 +1,30 @@ +import { test, expect } from "@playwright/test"; +import { ViewerPage } from "../pages/ViewerPage"; + +test.beforeEach(async ({ page }) => { + await ViewerPage.init(page); +}); + +const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1"; +const singleBoardPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb2"; + +test("Comment is shown with scroll and valid position", async ({ page }) => { + const viewer = new ViewerPage(page); + await viewer.setupLoggedInUser(); + await viewer.setupFileWithComments(); + + await viewer.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + await viewer.showComments(); + await viewer.showCommentsThread(1); + await expect( + viewer.page.getByRole("textbox", { name: "Reply" }), + ).toBeVisible(); + await viewer.showCommentsThread(1); + await viewer.showCommentsThread(2); + await expect( + viewer.page.getByRole("textbox", { name: "Reply" }), + ).toBeVisible(); +}); diff --git a/frontend/playwright/ui/specs/viewer-header.spec.js b/frontend/playwright/ui/specs/viewer-header.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..805d31c66ea49d6069aee70c36f9c4831e63b7a8 --- /dev/null +++ b/frontend/playwright/ui/specs/viewer-header.spec.js @@ -0,0 +1,42 @@ +import { test, expect } from "@playwright/test"; +import { ViewerPage } from "../pages/ViewerPage"; + +test.beforeEach(async ({ page }) => { + await ViewerPage.init(page); +}); + +const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1"; +const singleBoardPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb2"; + +test("Clips link area of the logo", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupEmptyFile(); + + await viewerPage.goToViewer(); + + const viewerUrl = page.url(); + + const logoLink = viewerPage.page.getByTestId("penpot-logo-link"); + await expect(logoLink).toBeVisible(); + + const { x, y } = await logoLink.boundingBox(); + await viewerPage.page.mouse.click(x, y + 100); + await expect(page.url()).toBe(viewerUrl); +}); + +test("Updates URL with zoom type", async ({ page }) => { + const viewer = new ViewerPage(page); + await viewer.setupLoggedInUser(); + await viewer.setupFileWithSingleBoard(viewer); + + await viewer.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + + await viewer.page.getByTitle("Zoom").click(); + await viewer.page.getByText(/Fit/).click(); + + await expect(viewer.page).toHaveURL(/&zoom=fit/); +}); diff --git a/frontend/playwright/ui/specs/workspace.spec.js b/frontend/playwright/ui/specs/workspace.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..066486f50c9bb22948b146bbaf9c1caf2096eb4b --- /dev/null +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -0,0 +1,179 @@ +import { test, expect } from "@playwright/test"; +import { WorkspacePage } from "../pages/WorkspacePage"; +import { presenceFixture } from "../../data/workspace/ws-notifications"; + +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); +}); + +test("User loads worskpace with empty file", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(page); + + await workspacePage.goToWorkspace(); + + await expect(workspacePage.pageName).toHaveText("Page 1"); +}); + +test("User receives presence notifications updates in the workspace", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + + await workspacePage.goToWorkspace(); + await workspacePage.sendPresenceMessage(presenceFixture); + + await expect( + page.getByTestId("active-users-list").getByAltText("Princesa Leia"), + ).toHaveCount(2); +}); + +test("User draws a rect", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC( + "update-file?id=*", + "workspace/update-file-create-rect.json", + ); + + await workspacePage.goToWorkspace(); + await workspacePage.rectShapeButton.click(); + await workspacePage.clickWithDragViewportAt(128, 128, 200, 100); + + const shape = await workspacePage.rootShape.locator("rect"); + await expect(shape).toHaveAttribute("width", "200"); + await expect(shape).toHaveAttribute("height", "100"); +}); + +test("User makes a group", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC( + /get\-file\?/, + "workspace/get-file-not-empty.json", + ); + await workspacePage.mockRPC( + "update-file?id=*", + "workspace/update-file-create-rect.json", + ); + + await workspacePage.goToWorkspace({ + fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", + pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", + }); + await workspacePage.clickLeafLayer("Rectangle"); + await workspacePage.page.keyboard.press("Control+g"); + await workspacePage.expectSelectedLayer("Group"); +}); + +test("Bug 7654 - Toolbar keeps toggling on and off on spacebar press", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.goToWorkspace(); + + await workspacePage.toggleToolbarButton.click(); + await workspacePage.page.keyboard.press("Backspace"); + await workspacePage.page.keyboard.press("Enter"); + await workspacePage.expectHiddenToolbarOptions(); +}); + +test("Bug 7525 - User moves a scrollbar and no selciont rectangle appears", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC( + /get\-file\?/, + "workspace/get-file-not-empty.json", + ); + await workspacePage.mockRPC( + "update-file?id=*", + "workspace/update-file-create-rect.json", + ); + + await workspacePage.goToWorkspace({ + fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", + pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", + }); + + // Move created rect to a corner, in orther to get scrollbars + await workspacePage.panOnViewportAt(128, 128, 300, 300); + + // Check scrollbars appear + const horizontalScrollbar = workspacePage.horizontalScrollbar; + await expect(horizontalScrollbar).toBeVisible(); + + // Grab scrollbar and move + const { x, y } = await horizontalScrollbar.boundingBox(); + await page.waitForTimeout(100); + await workspacePage.viewport.hover({ position: { x: x, y: y + 5 } }); + await page.mouse.down(); + await workspacePage.viewport.hover({ position: { x: x - 130, y: y - 95 } }); + + await expect(workspacePage.selectionRect).not.toBeInViewport(); +}); + +test("User adds a library and its automatically selected in the color palette", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.mockRPC( + "link-file-to-library", + "workspace/link-file-to-library.json", + ); + await workspacePage.mockRPC( + "unlink-file-from-library", + "workspace/unlink-file-from-library.json", + ); + await workspacePage.mockRPC( + "get-team-shared-files?team-id=*", + "workspace/get-team-shared-libraries-non-empty.json", + ); + + await workspacePage.goToWorkspace(); + + // Add Testing library 1 + await workspacePage.clickColorPalette(); + await workspacePage.clickAssets(); + // Now the get-file call should return a library + await workspacePage.mockRPC(/get\-file\?/, "workspace/get-file-library.json"); + await workspacePage.openLibrariesModal(); + await workspacePage.clickLibrary("Testing library 1"); + await workspacePage.closeLibrariesModal(); + + await expect( + workspacePage.palette.getByRole("button", { name: "test-color-187cd5" }), + ).toBeVisible(); + + // Remove Testing library 1 + await workspacePage.openLibrariesModal(); + await workspacePage.clickLibrary("Testing library 1"); + await workspacePage.closeLibrariesModal(); + + await expect( + workspacePage.palette.getByText( + "There are no color styles in your library yet", + ), + ).toBeVisible(); +}); + +test("Bug 7489 - Workspace-palette items stay hidden when opening with keyboard-shortcut", async ({ + page, +}) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.setupEmptyFile(); + await workspacePage.goToWorkspace(); + + await workspacePage.clickTogglePalettesVisibility(); + await workspacePage.page.keyboard.press("Alt+t"); + + await expect( + workspacePage.palette.getByText( + "There are no typography styles in your library yet", + ), + ).toBeVisible(); +}); diff --git a/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..44bfb9abb04ff6c937989428f52bea01ee7a8a2e --- /dev/null +++ b/frontend/playwright/ui/visual-specs/visual-dashboard.spec.js @@ -0,0 +1,317 @@ +import { test, expect } from "@playwright/test"; +import DashboardPage from "../pages/DashboardPage"; + +test.beforeEach(async ({ page }) => { + await DashboardPage.init(page); + await DashboardPage.mockRPC( + page, + "get-profile", + "logged-in-user/get-profile-logged-in-no-onboarding.json", + ); +}); + +test("User goes to an empty dashboard", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + + await expect(dashboardPage.mainHeading).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +// Empty dashboard pages + +test("User goes to an empty draft page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDraftsEmpty(); + + await dashboardPage.goToDashboard(); + await dashboardPage.draftsLink.click(); + + await expect(dashboardPage.mainHeading).toHaveText("Drafts"); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty fonts page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + await dashboardPage.fontsLink.click(); + + await expect(dashboardPage.mainHeading).toHaveText("Fonts"); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty libraries page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupLibrariesEmpty(); + + await dashboardPage.goToDashboard(); + await dashboardPage.libsLink.click(); + + await expect(dashboardPage.mainHeading).toHaveText("Libraries"); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty search page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupSearchEmpty(); + + await dashboardPage.goToSearch(); + + await expect(dashboardPage.mainHeading).toHaveText("Search results"); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to the dashboard with a new project", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupNewProject(); + + await dashboardPage.goToDashboard(); + + await expect(dashboardPage.projectName).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +// Dashboard pages with content + +test("User goes to a full dashboard", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to a full draft page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + await dashboardPage.draftsLink.click(); + + await expect(dashboardPage.mainHeading).toHaveText("Drafts"); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to a full library page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + await dashboardPage.libsLink.click(); + + await expect(dashboardPage.mainHeading).toHaveText("Libraries"); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to a full fonts page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + await dashboardPage.fontsLink.click(); + + await expect(dashboardPage.mainHeading).toHaveText("Fonts"); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to a full search page", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToSearch(); + await expect(dashboardPage.searchInput).toBeVisible(); + + await dashboardPage.searchInput.fill("3"); + + await expect(dashboardPage.mainHeading).toHaveText("Search results"); + await expect( + dashboardPage.page.getByRole("button", { name: "New File 3" }), + ).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +// Account management + +test("User opens user account", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + await expect(dashboardPage.userAccount).toBeVisible(); + await dashboardPage.goToAccount(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to user profile", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + await dashboardPage.goToAccount(); + + await expect(dashboardPage.mainHeading).toHaveText("Your account"); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to password management section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + await dashboardPage.goToAccount(); + + await page.getByText("Password").click(); + + await expect( + page.getByRole("heading", { name: "Change Password" }), + ).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to settings section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + await dashboardPage.goToAccount(); + + await page.getByTestId("settings-profile").click(); + + await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +// Teams management + +test("User opens teams selector with only one team", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + + await dashboardPage.goToDashboard(); + await dashboardPage.teamDropdown.click(); + + await expect(page.getByText("Create new team")).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User opens teams selector with more than one team", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToDashboard(); + await dashboardPage.teamDropdown.click(); + + await expect(page.getByText("Second Team")).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to second team", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.goToDashboard(); + + await dashboardPage.teamDropdown.click(); + await expect(page.getByText("Second Team")).toBeVisible(); + + await page.getByText("Second Team").click(); + + await expect(page.getByText("Team Up")).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User opens team management dropdown", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToSecondTeamDashboard(); + await expect(page.getByText("Team Up")).toBeVisible(); + + await page.getByRole("button", { name: "team-management" }).click(); + + await expect(page.getByTestId("team-members")).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to team management section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + + await dashboardPage.goToSecondTeamMembersSection(); + + await expect(page.getByText("role")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty invitations section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamInvitationsEmpty(); + + await dashboardPage.goToSecondTeamInvitationsSection(); + + await expect(page.getByText("No pending invitations")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to a complete invitations section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamInvitations(); + + await dashboardPage.goToSecondTeamInvitationsSection(); + + await expect(page.getByText("test1@mail.com")).toBeVisible(); + + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User invite people to the team", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamInvitationsEmpty(); + + await dashboardPage.goToSecondTeamInvitationsSection(); + await expect(page.getByTestId("invite-member")).toBeVisible(); + + await page.getByTestId("invite-member").click(); + await expect(page.getByText("Invite with the role")).toBeVisible(); + + await page.getByPlaceholder("Emails, comma separated").fill("test5@mail.com"); + + await expect(page.getByText("Send invitation")).not.toBeDisabled(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to an empty webhook section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamWebhooksEmpty(); + + await dashboardPage.goToSecondTeamWebhooksSection(); + + await expect(page.getByText("No webhooks created so far.")).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to a complete webhook section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamWebhooks(); + + await dashboardPage.goToSecondTeamWebhooksSection(); + + await expect(page.getByText("https://www.google.com")).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); + +test("User goes to the team settings section", async ({ page }) => { + const dashboardPage = new DashboardPage(page); + await dashboardPage.setupDashboardFull(); + await dashboardPage.setupTeamSettings(); + + await dashboardPage.goToSecondTeamSettingsSection(); + + await expect(page.getByText("TEAM INFO")).toBeVisible(); + await expect(dashboardPage.page).toHaveScreenshot(); +}); diff --git a/frontend/playwright/ui/visual-specs/visual-login.spec.js b/frontend/playwright/ui/visual-specs/visual-login.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..b3b63a0c56c0a7c7525b75d187e5615af52484cd --- /dev/null +++ b/frontend/playwright/ui/visual-specs/visual-login.spec.js @@ -0,0 +1,37 @@ +import { test, expect } from "@playwright/test"; +import { LoginPage } from "../pages/LoginPage"; + +test.beforeEach(async ({ page }) => { + const login = new LoginPage(page); + await login.initWithLoggedOutUser(); + await login.page.goto("/#/auth/login"); +}); + +test.describe("Login form", () => { + test("Shows the login form correctly", async ({ page }) => { + const login = new LoginPage(page); + await expect(login.page).toHaveScreenshot(); + }); + + test("Shows form error messages correctly ", async ({ page }) => { + const login = new LoginPage(page); + await login.setupLoginSuccess(); + + await login.fillEmailAndPasswordInputs("foo", "lorenIpsum"); + + await expect(login.invalidEmailError).toBeVisible(); + await expect(login.page).toHaveScreenshot(); + }); + + test("Shows error toasts correctly", async ({ page }) => { + const login = new LoginPage(page); + await login.setupLoginError(); + + await login.fillEmailAndPasswordInputs("test@example.com", "loremipsum"); + await login.clickLoginButton(); + + await expect(login.invalidCredentialsError).toBeVisible(); + await expect(login.page).toHaveURL(/auth\/login$/); + await expect(login.page).toHaveScreenshot(); + }); +}); diff --git a/frontend/playwright/ui/visual-specs/visual-viewer.spec.js b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ef6901f7023d3465a01c019cc7c840069757bf32 --- /dev/null +++ b/frontend/playwright/ui/visual-specs/visual-viewer.spec.js @@ -0,0 +1,145 @@ +import { test, expect } from "@playwright/test"; +import { ViewerPage } from "../pages/ViewerPage"; + +test.beforeEach(async ({ page }) => { + await ViewerPage.init(page); +}); + +const singleBoardFileId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb1"; +const singleBoardPageId = "dd5cc0bb-91ff-81b9-8004-77df9cd3edb2"; + +test("User goes to an empty Viewer", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupEmptyFile(); + + await viewerPage.goToViewer(); + + await expect(viewerPage.page.getByTestId("penpot-logo-link")).toBeVisible(); + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User goes to the Viewer", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithSingleBoard(); + + await viewerPage.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + + await expect(viewerPage.page.getByTestId("penpot-logo-link")).toBeVisible(); + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User goes to the Viewer and opens zoom modal", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithSingleBoard(); + + await viewerPage.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + + await viewerPage.page.getByTitle("Zoom").click(); + + await expect(viewerPage.page.getByTestId("penpot-logo-link")).toBeVisible(); + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User goes to the Viewer Comments", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithComments(); + + await viewerPage.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + + await viewerPage.showComments(); + await viewerPage.showCommentsThread(1); + await expect( + viewerPage.page.getByRole("textbox", { name: "Reply" }), + ).toBeVisible(); + + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User opens Viewer comment list", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithComments(); + + await viewerPage.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + + await viewerPage.showComments(); + await viewerPage.page.getByTestId("viewer-comments-dropdown").click(); + + await viewerPage.page.getByText("Show comments list").click(); + + await expect( + viewerPage.page.getByRole("button", { name: "Show all comments" }), + ).toBeVisible(); + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User goes to the Viewer Inspect code", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithComments(); + + await viewerPage.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + + await viewerPage.showCode(); + + await expect(viewerPage.page.getByText("Size and position")).toBeVisible(); + + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User goes to the Viewer Inspect code, code tab", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithComments(); + + await viewerPage.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + + await viewerPage.showCode(); + await viewerPage.page.getByRole("tab", { name: "code" }).click(); + + await expect( + viewerPage.page.getByRole("button", { name: "Copy all code" }), + ).toBeVisible(); + + await expect(viewerPage.page).toHaveScreenshot(); +}); + +test("User opens Share modal", async ({ page }) => { + const viewerPage = new ViewerPage(page); + await viewerPage.setupLoggedInUser(); + await viewerPage.setupFileWithSingleBoard(); + + await viewerPage.goToViewer({ + fileId: singleBoardFileId, + pageId: singleBoardPageId, + }); + + await viewerPage.page.getByRole("button", { name: "Share" }).click(); + + await expect( + viewerPage.page.getByRole("button", { name: "Get link" }), + ).toBeVisible(); + await expect(viewerPage.page).toHaveScreenshot(); +}); diff --git a/frontend/playwright/ui/visual-specs/workspace.spec.js b/frontend/playwright/ui/visual-specs/workspace.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..e594ea343f8f972f9d4e8fba33c6bb9b70e9e255 --- /dev/null +++ b/frontend/playwright/ui/visual-specs/workspace.spec.js @@ -0,0 +1,150 @@ +import { test, expect } from "@playwright/test"; +import { WorkspacePage } from "../pages/WorkspacePage"; + +test.beforeEach(async ({ page }) => { + await WorkspacePage.init(page); +}); + +const setupFileWithAssets = async (workspace) => { + const fileId = "015fda4f-caa6-8103-8004-862a00dd4f31"; + const pageId = "015fda4f-caa6-8103-8004-862a00ddbe94"; + const fragments = { + "015fda4f-caa6-8103-8004-862a9e4b4d4b": + "assets/get-file-fragment-with-assets-components.json", + "015fda4f-caa6-8103-8004-862a9e4ad279": + "assets/get-file-fragmnet-with-assets-page.json", + }; + + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "assets/get-file-with-assets.json"); + + for (const [id, fixture] of Object.entries(fragments)) { + await workspace.mockRPC( + `get-file-fragment?file-id=*&fragment-id=${id}`, + fixture, + ); + } + + return { fileId, pageId }; +}; + +test("Shows the workspace correctly for a blank file", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + + await workspace.goToWorkspace(); + + await expect(workspace.page).toHaveScreenshot(); +}); + +test.describe("Design tab", () => { + test("Shows the design tab when selecting a shape", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json"); + + await workspace.goToWorkspace({ + fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", + pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", + }); + + await workspace.clickLeafLayer("Rectangle"); + + await expect(workspace.page).toHaveScreenshot(); + }); + + test("Shows expanded sections of the design tab", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json"); + + await workspace.goToWorkspace({ + fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374", + pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375", + }); + + await workspace.clickLeafLayer("Rectangle"); + await workspace.rightSidebar.getByTestId("add-stroke").click(); + + await expect(workspace.page).toHaveScreenshot(); + }); +}); + +test.describe("Assets tab", () => { + test("Shows the libraries modal correctly", async ({ page }) => { + const workspace = new WorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockRPC( + "link-file-to-library", + "workspace/link-file-to-library.json", + ); + await workspace.mockRPC( + "get-team-shared-files?team-id=*", + "workspace/get-team-shared-libraries-non-empty.json", + ); + + await workspace.mockRPC( + "push-audit-events", + "workspace/audit-event-empty.json", + ); + + await workspace.goToWorkspace(); + await workspace.clickAssets(); + await workspace.openLibrariesModal(); + await expect(workspace.page).toHaveScreenshot(); + + await workspace.clickLibrary("Testing library 1"); + await expect( + workspace.librariesModal.getByText( + "There are no Shared Libraries available", + ), + ).toBeVisible(); + await expect(workspace.page).toHaveScreenshot(); + }); + + test("Shows the assets correctly", async ({ page }) => { + const workspace = new WorkspacePage(page); + const { fileId, pageId } = await setupFileWithAssets(workspace); + + await workspace.goToWorkspace({ fileId, pageId }); + + await workspace.clickAssets(); + await workspace.sidebar.getByRole("button", { name: "Components" }).click(); + await workspace.sidebar.getByRole("button", { name: "Colors" }).click(); + await workspace.sidebar + .getByRole("button", { name: "Typographies" }) + .click(); + + await expect(workspace.page).toHaveScreenshot(); + + await workspace.sidebar.getByTitle("List view").click(); + + await expect(workspace.page).toHaveScreenshot(); + }); +}); + +test.describe("Palette", () => { + test("Shows the bottom palette expanded and collapsed", async ({ page }) => { + const workspace = new WorkspacePage(page); + const { fileId, pageId } = await setupFileWithAssets(workspace); + + await workspace.goToWorkspace({ fileId, pageId }); + + await expect(workspace.page).toHaveScreenshot(); + + await workspace.palette + .getByRole("button", { name: "Typographies" }) + .click(); + await expect( + workspace.palette.getByText("Source Sans Pro Regular"), + ).toBeVisible(); + await expect(workspace.page).toHaveScreenshot(); + + await workspace.palette + .getByRole("button", { name: "Color Palette" }) + .click(); + await expect( + workspace.palette.getByRole("button", { name: "#7798ff" }), + ).toBeVisible(); + }); +}); diff --git a/frontend/resources/fonts/Vazirmatn-VariableFont.ttf b/frontend/resources/fonts/Vazirmatn-VariableFont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..22595c49932da4f9bb5178d08b6b49e3d01897a4 Binary files /dev/null and b/frontend/resources/fonts/Vazirmatn-VariableFont.ttf differ diff --git a/frontend/resources/fonts/WorkSans-VariableFont.ttf b/frontend/resources/fonts/WorkSans-VariableFont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..9a827989bdff1feff35f3d8ac8cffe564f3d8535 Binary files /dev/null and b/frontend/resources/fonts/WorkSans-VariableFont.ttf differ diff --git a/frontend/resources/images/assets/brand-github.svg b/frontend/resources/images/assets/brand-github.svg new file mode 100644 index 0000000000000000000000000000000000000000..cfb34953e0664d8febf73bff9ca266350430426e --- /dev/null +++ b/frontend/resources/images/assets/brand-github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/assets/brand-gitlab.svg b/frontend/resources/images/assets/brand-gitlab.svg new file mode 100644 index 0000000000000000000000000000000000000000..591427ec6e7282865ea0e8d2982448a408dbe778 --- /dev/null +++ b/frontend/resources/images/assets/brand-gitlab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/assets/brand-google.svg b/frontend/resources/images/assets/brand-google.svg new file mode 100644 index 0000000000000000000000000000000000000000..eb61aab34818bb07d4373602bf3e8f8cf1d6c829 --- /dev/null +++ b/frontend/resources/images/assets/brand-google.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/assets/brand-openid.svg b/frontend/resources/images/assets/brand-openid.svg new file mode 100644 index 0000000000000000000000000000000000000000..28dd05ed85ade1427bd9f14d3b9242c0e7b5895f --- /dev/null +++ b/frontend/resources/images/assets/brand-openid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/assets/loader.svg b/frontend/resources/images/assets/loader.svg new file mode 100644 index 0000000000000000000000000000000000000000..ee1d9b96bfe49396163ba14c358982c94165f057 --- /dev/null +++ b/frontend/resources/images/assets/loader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/assets/login-illustration.svg b/frontend/resources/images/assets/login-illustration.svg new file mode 100644 index 0000000000000000000000000000000000000000..6e6b7394a30c053726c85bcdf034b784e4be6f1d --- /dev/null +++ b/frontend/resources/images/assets/login-illustration.svg @@ -0,0 +1,686 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/resources/images/assets/logo-error-screen.svg b/frontend/resources/images/assets/logo-error-screen.svg new file mode 100644 index 0000000000000000000000000000000000000000..7e71215fac02cb5c2cd1e10afa44fb3e91035980 --- /dev/null +++ b/frontend/resources/images/assets/logo-error-screen.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/assets/marketing-arrows.svg b/frontend/resources/images/assets/marketing-arrows.svg new file mode 100644 index 0000000000000000000000000000000000000000..7ce38ce15e03c7543012eccb5e0ea392d2c82fde --- /dev/null +++ b/frontend/resources/images/assets/marketing-arrows.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/assets/marketing-exchange.svg b/frontend/resources/images/assets/marketing-exchange.svg new file mode 100644 index 0000000000000000000000000000000000000000..68ea4e6d631a0ea407c414637ca0a45335ad515e --- /dev/null +++ b/frontend/resources/images/assets/marketing-exchange.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/assets/marketing-file.svg b/frontend/resources/images/assets/marketing-file.svg new file mode 100644 index 0000000000000000000000000000000000000000..ce2d299f8b5a2dbe8f7d124141ac1dfe403c9045 --- /dev/null +++ b/frontend/resources/images/assets/marketing-file.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/assets/marketing-layers.svg b/frontend/resources/images/assets/marketing-layers.svg new file mode 100644 index 0000000000000000000000000000000000000000..4b5e97d4a5d016612b894c419a8b72de791eb47b --- /dev/null +++ b/frontend/resources/images/assets/marketing-layers.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/assets/penpot-logo-icon.svg b/frontend/resources/images/assets/penpot-logo-icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..06adb5d443e66126b803bf25c556f1c4fb032796 --- /dev/null +++ b/frontend/resources/images/assets/penpot-logo-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/images/assets/penpot-logo.svg b/frontend/resources/images/assets/penpot-logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..6439292bd223ec0f2b9521680a226f749186088b --- /dev/null +++ b/frontend/resources/images/assets/penpot-logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/resources/images/features/2.0-html.gif b/frontend/resources/images/features/2.0-html.gif new file mode 100644 index 0000000000000000000000000000000000000000..6e10f5ad787f1a117273b02aae1a408def8565dc Binary files /dev/null and b/frontend/resources/images/features/2.0-html.gif differ diff --git a/frontend/resources/images/form/Design.png b/frontend/resources/images/form/Design.png new file mode 100644 index 0000000000000000000000000000000000000000..ae1e28ab720bb8707f1ab501ba16551f8b02105c Binary files /dev/null and b/frontend/resources/images/form/Design.png differ diff --git a/frontend/resources/images/form/Prototype.png b/frontend/resources/images/form/Prototype.png new file mode 100644 index 0000000000000000000000000000000000000000..508d43e1d3abb0f48ebe579e11aedfa8a1e69ba0 Binary files /dev/null and b/frontend/resources/images/form/Prototype.png differ diff --git a/frontend/resources/images/form/components.png b/frontend/resources/images/form/components.png new file mode 100644 index 0000000000000000000000000000000000000000..e1817a6a96a793d7d1af77ad7d63267af666140a Binary files /dev/null and b/frontend/resources/images/form/components.png differ diff --git a/frontend/resources/images/form/design-and-dev.png b/frontend/resources/images/form/design-and-dev.png new file mode 100644 index 0000000000000000000000000000000000000000..b66bddc7930bea0070adcfaae3663ffbf265d84e Binary files /dev/null and b/frontend/resources/images/form/design-and-dev.png differ diff --git a/frontend/resources/images/form/templates.png b/frontend/resources/images/form/templates.png new file mode 100644 index 0000000000000000000000000000000000000000..3c77a7a0fff61a77a1ebd93a07437cf824cddc51 Binary files /dev/null and b/frontend/resources/images/form/templates.png differ diff --git a/frontend/resources/images/icons/brand-github.svg b/frontend/resources/images/icons/brand-github.svg index 91ce17a6092eaf7ab93932491dc98a5b7efdce24..cfb34953e0664d8febf73bff9ca266350430426e 100644 --- a/frontend/resources/images/icons/brand-github.svg +++ b/frontend/resources/images/icons/brand-github.svg @@ -1,3 +1 @@ - - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/resources/images/icons/brand-gitlab.svg b/frontend/resources/images/icons/brand-gitlab.svg index 04993577bd5fcbd72609195defcab357383de427..591427ec6e7282865ea0e8d2982448a408dbe778 100644 --- a/frontend/resources/images/icons/brand-gitlab.svg +++ b/frontend/resources/images/icons/brand-gitlab.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/resources/images/icons/brand-google.svg b/frontend/resources/images/icons/brand-google.svg index dba95de0fef22fcdea575310cb0117183565e850..eb61aab34818bb07d4373602bf3e8f8cf1d6c829 100644 --- a/frontend/resources/images/icons/brand-google.svg +++ b/frontend/resources/images/icons/brand-google.svg @@ -1 +1 @@ - + \ No newline at end of file diff --git a/frontend/resources/images/icons/brand-openid.svg b/frontend/resources/images/icons/brand-openid.svg index a335b6664257ee9607e1e1600f52b0973445c1b1..28dd05ed85ade1427bd9f14d3b9242c0e7b5895f 100644 --- a/frontend/resources/images/icons/brand-openid.svg +++ b/frontend/resources/images/icons/brand-openid.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/resources/images/icons/character-a.svg b/frontend/resources/images/icons/character-a.svg new file mode 100644 index 0000000000000000000000000000000000000000..3a740083ae86c4aaa2c2b923b945946b006e0445 --- /dev/null +++ b/frontend/resources/images/icons/character-a.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-b.svg b/frontend/resources/images/icons/character-b.svg new file mode 100644 index 0000000000000000000000000000000000000000..39fe59dd5871f1d4530646bcdf1d9bb110942e57 --- /dev/null +++ b/frontend/resources/images/icons/character-b.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-c.svg b/frontend/resources/images/icons/character-c.svg new file mode 100644 index 0000000000000000000000000000000000000000..73347384dedc8374ed7f74a05d2b751bc284922f --- /dev/null +++ b/frontend/resources/images/icons/character-c.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-d.svg b/frontend/resources/images/icons/character-d.svg new file mode 100644 index 0000000000000000000000000000000000000000..d585f275b4d264e7bf30bede73578c6755f94ca3 --- /dev/null +++ b/frontend/resources/images/icons/character-d.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-e.svg b/frontend/resources/images/icons/character-e.svg new file mode 100644 index 0000000000000000000000000000000000000000..eb7ac8837621b85103c7d129445212116ace9b4c --- /dev/null +++ b/frontend/resources/images/icons/character-e.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-f.svg b/frontend/resources/images/icons/character-f.svg new file mode 100644 index 0000000000000000000000000000000000000000..c6ddd2c4d2865ecc4cc0fe8e82014b34b097f49d --- /dev/null +++ b/frontend/resources/images/icons/character-f.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-g.svg b/frontend/resources/images/icons/character-g.svg new file mode 100644 index 0000000000000000000000000000000000000000..fd87e7fc08cb47a03bfd9cce4fed9096eb3c9f95 --- /dev/null +++ b/frontend/resources/images/icons/character-g.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-h.svg b/frontend/resources/images/icons/character-h.svg new file mode 100644 index 0000000000000000000000000000000000000000..082571f40df766c225bf2723a7e02542313270ed --- /dev/null +++ b/frontend/resources/images/icons/character-h.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-i.svg b/frontend/resources/images/icons/character-i.svg new file mode 100644 index 0000000000000000000000000000000000000000..567b9f4715376fe4905c5146c0962d6920f0f2d6 --- /dev/null +++ b/frontend/resources/images/icons/character-i.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-j.svg b/frontend/resources/images/icons/character-j.svg new file mode 100644 index 0000000000000000000000000000000000000000..b90b0bca69dec8fd7db730a6d63f651b0b75c068 --- /dev/null +++ b/frontend/resources/images/icons/character-j.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-k.svg b/frontend/resources/images/icons/character-k.svg new file mode 100644 index 0000000000000000000000000000000000000000..dacc939178687a33457bc09aeeea5cdcdef6cd23 --- /dev/null +++ b/frontend/resources/images/icons/character-k.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-l.svg b/frontend/resources/images/icons/character-l.svg new file mode 100644 index 0000000000000000000000000000000000000000..9b4f0b17d9270315491ee6d8f8bb48deab420138 --- /dev/null +++ b/frontend/resources/images/icons/character-l.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-m.svg b/frontend/resources/images/icons/character-m.svg new file mode 100644 index 0000000000000000000000000000000000000000..771d684a06d4d0d0316ea35a74f7005e250347f3 --- /dev/null +++ b/frontend/resources/images/icons/character-m.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-n.svg b/frontend/resources/images/icons/character-n.svg new file mode 100644 index 0000000000000000000000000000000000000000..ec006c85a38b539c6b847505c581621b29e84886 --- /dev/null +++ b/frontend/resources/images/icons/character-n.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-ntilde.svg b/frontend/resources/images/icons/character-ntilde.svg new file mode 100644 index 0000000000000000000000000000000000000000..fded9d9bad4d332250aaf9125532a89a8235ba6b --- /dev/null +++ b/frontend/resources/images/icons/character-ntilde.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-o.svg b/frontend/resources/images/icons/character-o.svg new file mode 100644 index 0000000000000000000000000000000000000000..3d01ad2f74e0869af88a8594dd7f63e0b2bef505 --- /dev/null +++ b/frontend/resources/images/icons/character-o.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-p.svg b/frontend/resources/images/icons/character-p.svg new file mode 100644 index 0000000000000000000000000000000000000000..1e272df2c7e2cc1dedceef729771d75cb012870c --- /dev/null +++ b/frontend/resources/images/icons/character-p.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-q.svg b/frontend/resources/images/icons/character-q.svg new file mode 100644 index 0000000000000000000000000000000000000000..6ead103beef2ae485d03a111d8f87c3e41853e6a --- /dev/null +++ b/frontend/resources/images/icons/character-q.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-r.svg b/frontend/resources/images/icons/character-r.svg new file mode 100644 index 0000000000000000000000000000000000000000..120e254e2f1c3907668f9f565cbd4441726dde9e --- /dev/null +++ b/frontend/resources/images/icons/character-r.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-s.svg b/frontend/resources/images/icons/character-s.svg new file mode 100644 index 0000000000000000000000000000000000000000..796a64e136ec576242b3092f83f0be982a1aa10a --- /dev/null +++ b/frontend/resources/images/icons/character-s.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-t.svg b/frontend/resources/images/icons/character-t.svg new file mode 100644 index 0000000000000000000000000000000000000000..1c8b6ba107065411ebb334aa920f1838a4b1de1e --- /dev/null +++ b/frontend/resources/images/icons/character-t.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-u.svg b/frontend/resources/images/icons/character-u.svg new file mode 100644 index 0000000000000000000000000000000000000000..d07aef54e03d3c0856fd815f071d09cba7dcc86e --- /dev/null +++ b/frontend/resources/images/icons/character-u.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-v.svg b/frontend/resources/images/icons/character-v.svg new file mode 100644 index 0000000000000000000000000000000000000000..d28b777f0adc6f4b84837e7614e3cb206285ced8 --- /dev/null +++ b/frontend/resources/images/icons/character-v.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-w.svg b/frontend/resources/images/icons/character-w.svg new file mode 100644 index 0000000000000000000000000000000000000000..8ecae0bf5ebf08f63b42953b6da07914fb3bacda --- /dev/null +++ b/frontend/resources/images/icons/character-w.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-x.svg b/frontend/resources/images/icons/character-x.svg new file mode 100644 index 0000000000000000000000000000000000000000..2253d0d1718de721069edeead452a9e923c87e49 --- /dev/null +++ b/frontend/resources/images/icons/character-x.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-y.svg b/frontend/resources/images/icons/character-y.svg new file mode 100644 index 0000000000000000000000000000000000000000..add5b34b8b3770f8b5340d5756b1bdb38ae226c7 --- /dev/null +++ b/frontend/resources/images/icons/character-y.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/character-z.svg b/frontend/resources/images/icons/character-z.svg new file mode 100644 index 0000000000000000000000000000000000000000..4df568995b3525fcf60fad1c1fc6dd5194379628 --- /dev/null +++ b/frontend/resources/images/icons/character-z.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/external-link.svg b/frontend/resources/images/icons/external-link.svg new file mode 100644 index 0000000000000000000000000000000000000000..cb66edb02e1096f9d7ea519a2374f6aa1ba2de90 --- /dev/null +++ b/frontend/resources/images/icons/external-link.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/images/icons/oauth-1.svg b/frontend/resources/images/icons/oauth-1.svg new file mode 100644 index 0000000000000000000000000000000000000000..49a0dec9bf8afe24398bf2b9e7443e4c9251dbad --- /dev/null +++ b/frontend/resources/images/icons/oauth-1.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/images/icons/oauth-2.svg b/frontend/resources/images/icons/oauth-2.svg new file mode 100644 index 0000000000000000000000000000000000000000..06c59a18529c3138f865e7f906eb315c7f4ead08 --- /dev/null +++ b/frontend/resources/images/icons/oauth-2.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/images/icons/oauth-3.svg b/frontend/resources/images/icons/oauth-3.svg new file mode 100644 index 0000000000000000000000000000000000000000..db38820bcf4774d55161203bc58af4cf31f674db --- /dev/null +++ b/frontend/resources/images/icons/oauth-3.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/images/icons/penpot-logo.svg b/frontend/resources/images/icons/penpot-logo.svg index 4493b6d91fa315aab85f3ad46c150a4d4722a445..6439292bd223ec0f2b9521680a226f749186088b 100644 --- a/frontend/resources/images/icons/penpot-logo.svg +++ b/frontend/resources/images/icons/penpot-logo.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/frontend/resources/images/icons/percentage.svg b/frontend/resources/images/icons/percentage.svg new file mode 100644 index 0000000000000000000000000000000000000000..faa33803718eae1cb28c24d0ea9da6120d9e372e --- /dev/null +++ b/frontend/resources/images/icons/percentage.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/puzzle.svg b/frontend/resources/images/icons/puzzle.svg new file mode 100644 index 0000000000000000000000000000000000000000..500638984e7f2c18368478d07b49a70f4d2f548c --- /dev/null +++ b/frontend/resources/images/icons/puzzle.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/resources/images/icons/row.svg b/frontend/resources/images/icons/row.svg index 8475819d455d95368f63ecb9f3208eb2e637aa3c..6dbcfe31dc09d14074eac4b94ab03e85b4749e3f 100644 --- a/frontend/resources/images/icons/row.svg +++ b/frontend/resources/images/icons/row.svg @@ -1 +1,3 @@ - \ No newline at end of file + + + diff --git a/frontend/resources/images/icons/wrap-refactor.svg b/frontend/resources/images/icons/wrap.svg similarity index 100% rename from frontend/resources/images/icons/wrap-refactor.svg rename to frontend/resources/images/icons/wrap.svg diff --git a/frontend/resources/images/thumbnails/template-avataaars.jpg b/frontend/resources/images/thumbnails/template-avataaars.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d5abeaf8eb9070d8787c60b16c2eac4037d48276 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-avataaars.jpg differ diff --git a/frontend/resources/images/thumbnails/template-black-white-mobile-templates.jpg b/frontend/resources/images/thumbnails/template-black-white-mobile-templates.jpg new file mode 100644 index 0000000000000000000000000000000000000000..16ce2001c89afcee7fe1877ab8c285fe81260849 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-black-white-mobile-templates.jpg differ diff --git a/frontend/resources/images/thumbnails/template-font-awesome.jpg b/frontend/resources/images/thumbnails/template-font-awesome.jpg new file mode 100644 index 0000000000000000000000000000000000000000..56fda33a6a42846a763761c949502a087c0f013f Binary files /dev/null and b/frontend/resources/images/thumbnails/template-font-awesome.jpg differ diff --git a/frontend/resources/images/thumbnails/template-lucide-icons.jpg b/frontend/resources/images/thumbnails/template-lucide-icons.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0282b70a8e0621ba63733cf63c683d3bfc4d4b20 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-lucide-icons.jpg differ diff --git a/frontend/resources/images/thumbnails/template-open-color-scheme.jpg b/frontend/resources/images/thumbnails/template-open-color-scheme.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cd73ddecc9a05f61d16668f002046faf40a0bc76 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-open-color-scheme.jpg differ diff --git a/frontend/resources/images/thumbnails/template-plants-app.jpg b/frontend/resources/images/thumbnails/template-plants-app.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d2dfbbcabd7463df2b7c9513aee658cebcb5f0cc Binary files /dev/null and b/frontend/resources/images/thumbnails/template-plants-app.jpg differ diff --git a/frontend/resources/images/thumbnails/template-prototype-examples.jpg b/frontend/resources/images/thumbnails/template-prototype-examples.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5e5d4dab4ca048d59c82b6f37539cc37e5e670ef Binary files /dev/null and b/frontend/resources/images/thumbnails/template-prototype-examples.jpg differ diff --git a/frontend/resources/images/thumbnails/template-ux-notes.jpg b/frontend/resources/images/thumbnails/template-ux-notes.jpg new file mode 100644 index 0000000000000000000000000000000000000000..43cc3bd992144fdcdf2c1cd6ea3adc7bd19597e8 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-ux-notes.jpg differ diff --git a/frontend/resources/images/walkthrough-cover.png b/frontend/resources/images/walkthrough-cover.png index eb602f871de73208397acff3d20c0660675e0950..109e00b30d4f4ef92638444f5b6c2aacec35e731 100644 Binary files a/frontend/resources/images/walkthrough-cover.png and b/frontend/resources/images/walkthrough-cover.png differ diff --git a/frontend/resources/plugins-runtime/index.js b/frontend/resources/plugins-runtime/index.js new file mode 100644 index 0000000000000000000000000000000000000000..40ab72dfc11ae19a846820b0f4f785723657c057 --- /dev/null +++ b/frontend/resources/plugins-runtime/index.js @@ -0,0 +1,8179 @@ +var Zn = (t) => { + throw TypeError(t); +}; +var zn = (t, e, r) => e.has(t) || Zn("Cannot " + r); +var Ie = (t, e, r) => (zn(t, e, "read from private field"), r ? r.call(t) : e.get(t)), Wr = (t, e, r) => e.has(t) ? Zn("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, r), qr = (t, e, r, n) => (zn(t, e, "write to private field"), n ? n.call(t, r) : e.set(t, r), r); +const P = globalThis, { + Array: Bs, + Date: Gs, + FinalizationRegistry: Tt, + Float32Array: Vs, + JSON: Hs, + Map: Ce, + Math: Ws, + Number: wo, + Object: yn, + Promise: qs, + Proxy: Nr, + Reflect: Ks, + RegExp: Je, + Set: Nt, + String: ye, + Symbol: wt, + WeakMap: Ue, + WeakSet: Ot +} = globalThis, { + // The feral Error constructor is safe for internal use, but must not be + // revealed to post-lockdown code in any compartment including the start + // compartment since in V8 at least it bears stack inspection capabilities. + Error: oe, + RangeError: Ys, + ReferenceError: zt, + SyntaxError: or, + TypeError: _, + AggregateError: Kr +} = globalThis, { + assign: Or, + create: V, + defineProperties: z, + entries: fe, + freeze: y, + getOwnPropertyDescriptor: Q, + getOwnPropertyDescriptors: je, + getOwnPropertyNames: It, + getPrototypeOf: G, + is: Mr, + isFrozen: jl, + isSealed: Zl, + isExtensible: zl, + keys: xo, + prototype: _n, + seal: Bl, + preventExtensions: Js, + setPrototypeOf: So, + values: Eo, + fromEntries: mt +} = yn, { + species: Yr, + toStringTag: Xe, + iterator: sr, + matchAll: ko, + unscopables: Xs, + keyFor: Qs, + for: ea +} = wt, { isInteger: ta } = wo, { stringify: Po } = Hs, { defineProperty: ra } = yn, U = (t, e, r) => { + const n = ra(t, e, r); + if (n !== t) + throw _( + `Please report that the original defineProperty silently failed to set ${Po( + ye(e) + )}. (SES_DEFINE_PROPERTY_FAILED_SILENTLY)` + ); + return n; +}, { + apply: ae, + construct: yr, + get: na, + getOwnPropertyDescriptor: oa, + has: To, + isExtensible: sa, + ownKeys: Be, + preventExtensions: aa, + set: Io +} = Ks, { isArray: xt, prototype: Ee } = Bs, { prototype: Mt } = Ce, { prototype: Lr } = RegExp, { prototype: ar } = Nt, { prototype: Ze } = ye, { prototype: Fr } = Ue, { prototype: Ao } = Ot, { prototype: vn } = Function, { prototype: Co } = qs, { prototype: $o } = G( + // eslint-disable-next-line no-empty-function, func-names + function* () { + } +), ia = G(Uint8Array.prototype), { bind: on } = vn, T = on.bind(on.call), ie = T(_n.hasOwnProperty), Qe = T(Ee.filter), dt = T(Ee.forEach), Dr = T(Ee.includes), Lt = T(Ee.join), ce = ( + /** @type {any} */ + T(Ee.map) +), Ro = ( + /** @type {any} */ + T(Ee.flatMap) +), _r = T(Ee.pop), ee = T(Ee.push), ca = T(Ee.slice), la = T(Ee.some), No = T(Ee.sort), ua = T(Ee[sr]), ue = T(Mt.set), Ge = T(Mt.get), Ur = T(Mt.has), da = T(Mt.delete), fa = T(Mt.entries), pa = T(Mt[sr]), bn = T(ar.add); +T(ar.delete); +const Bn = T(ar.forEach), wn = T(ar.has), ha = T(ar[sr]), xn = T(Lr.test), Sn = T(Lr.exec), ma = T(Lr[ko]), Oo = T(Ze.endsWith), Mo = T(Ze.includes), ga = T(Ze.indexOf); +T(Ze.match); +const vr = T($o.next), Lo = T($o.throw), br = ( + /** @type {any} */ + T(Ze.replace) +), ya = T(Ze.search), En = T(Ze.slice), kn = T(Ze.split), Fo = T(Ze.startsWith), _a = T(Ze[sr]), va = T(Fr.delete), Z = T(Fr.get), St = T(Fr.has), de = T(Fr.set), jr = T(Ao.add), ir = T(Ao.has), ba = T(vn.toString), wa = T(on); +T(Co.catch); +const Do = ( + /** @type {any} */ + T(Co.then) +), xa = Tt && T(Tt.prototype.register); +Tt && T(Tt.prototype.unregister); +const Pn = y(V(null)), Se = (t) => yn(t) === t, Zr = (t) => t instanceof oe, Uo = eval, xe = Function, Sa = () => { + throw _('Cannot eval with evalTaming set to "noEval" (SES_NO_EVAL)'); +}, qe = Q(Error("er1"), "stack"), Jr = Q(_("er2"), "stack"); +let jo, Zo; +if (qe && Jr && qe.get) + if ( + // In the v8 case as we understand it, all errors have an own stack + // accessor property, but within the same realm, all these accessor + // properties have the same getter and have the same setter. + // This is therefore the case that we repair. + typeof qe.get == "function" && qe.get === Jr.get && typeof qe.set == "function" && qe.set === Jr.set + ) + jo = y(qe.get), Zo = y(qe.set); + else + throw _( + "Unexpected Error own stack accessor functions (SES_UNEXPECTED_ERROR_OWN_STACK_ACCESSOR)" + ); +const Xr = jo, Ea = Zo; +function ka() { + return this; +} +if (ka()) + throw _("SES failed to initialize, sloppy mode (SES_NO_SLOPPY)"); +const { freeze: ct } = Object, { apply: Pa } = Reflect, Tn = (t) => (e, ...r) => Pa(t, e, r), Ta = Tn(Array.prototype.push), Gn = Tn(Array.prototype.includes), Ia = Tn(String.prototype.split), st = JSON.stringify, ur = (t, ...e) => { + let r = t[0]; + for (let n = 0; n < e.length; n += 1) + r = `${r}${e[n]}${t[n + 1]}`; + throw Error(r); +}, zo = (t, e = !1) => { + const r = [], n = (c, l, u = void 0) => { + typeof c == "string" || ur`Environment option name ${st(c)} must be a string.`, typeof l == "string" || ur`Environment option default setting ${st( + l + )} must be a string.`; + let d = l; + const f = t.process || void 0, h = typeof f == "object" && f.env || void 0; + if (typeof h == "object" && c in h) { + e || Ta(r, c); + const p = h[c]; + typeof p == "string" || ur`Environment option named ${st( + c + )}, if present, must have a corresponding string value, got ${st( + p + )}`, d = p; + } + return u === void 0 || d === l || Gn(u, d) || ur`Unrecognized ${st(c)} value ${st( + d + )}. Expected one of ${st([l, ...u])}`, d; + }; + ct(n); + const o = (c) => { + const l = n(c, ""); + return ct(l === "" ? [] : Ia(l, ",")); + }; + ct(o); + const a = (c, l) => Gn(o(c), l), i = () => ct([...r]); + return ct(i), ct({ + getEnvironmentOption: n, + getEnvironmentOptionsList: o, + environmentOptionsListHas: a, + getCapturedEnvironmentOptionNames: i + }); +}; +ct(zo); +const { + getEnvironmentOption: he, + getEnvironmentOptionsList: Gl, + environmentOptionsListHas: Vl +} = zo(globalThis, !0), wr = (t) => (t = `${t}`, t.length >= 1 && Mo("aeiouAEIOU", t[0]) ? `an ${t}` : `a ${t}`); +y(wr); +const Bo = (t, e = void 0) => { + const r = new Nt(), n = (o, a) => { + switch (typeof a) { + case "object": { + if (a === null) + return null; + if (wn(r, a)) + return "[Seen]"; + if (bn(r, a), Zr(a)) + return `[${a.name}: ${a.message}]`; + if (Xe in a) + return `[${a[Xe]}]`; + if (xt(a)) + return a; + const i = xo(a); + if (i.length < 2) + return a; + let c = !0; + for (let u = 1; u < i.length; u += 1) + if (i[u - 1] >= i[u]) { + c = !1; + break; + } + if (c) + return a; + No(i); + const l = ce(i, (u) => [u, a[u]]); + return mt(l); + } + case "function": + return `[Function ${a.name || ""}]`; + case "string": + return Fo(a, "[") ? `[${a}]` : a; + case "undefined": + case "symbol": + return `[${ye(a)}]`; + case "bigint": + return `[${a}n]`; + case "number": + return Mr(a, NaN) ? "[NaN]" : a === 1 / 0 ? "[Infinity]" : a === -1 / 0 ? "[-Infinity]" : a; + default: + return a; + } + }; + try { + return Po(t, n, e); + } catch { + return "[Something that failed to stringify]"; + } +}; +y(Bo); +const { isSafeInteger: Aa } = Number, { freeze: _t } = Object, { toStringTag: Ca } = Symbol, Vn = (t) => { + const r = { + next: void 0, + prev: void 0, + data: t + }; + return r.next = r, r.prev = r, r; +}, Hn = (t, e) => { + if (t === e) + throw TypeError("Cannot splice a cell into itself"); + if (e.next !== e || e.prev !== e) + throw TypeError("Expected self-linked cell"); + const r = e, n = t.next; + return r.prev = t, r.next = n, t.next = r, n.prev = r, r; +}, Qr = (t) => { + const { prev: e, next: r } = t; + e.next = r, r.prev = e, t.prev = t, t.next = t; +}, Go = (t) => { + if (!Aa(t) || t < 0) + throw TypeError("keysBudget must be a safe non-negative integer number"); + const e = /* @__PURE__ */ new WeakMap(); + let r = 0; + const n = Vn(void 0), o = (d) => { + const f = e.get(d); + if (!(f === void 0 || f.data === void 0)) + return Qr(f), Hn(n, f), f; + }, a = (d) => o(d) !== void 0; + _t(a); + const i = (d) => { + const f = o(d); + return f && f.data && f.data.get(d); + }; + _t(i); + const c = (d, f) => { + if (t < 1) + return u; + let h = o(d); + if (h === void 0 && (h = Vn(void 0), Hn(n, h)), !h.data) + for (r += 1, h.data = /* @__PURE__ */ new WeakMap(), e.set(d, h); r > t; ) { + const p = n.prev; + Qr(p), p.data = void 0, r -= 1; + } + return h.data.set(d, f), u; + }; + _t(c); + const l = (d) => { + const f = e.get(d); + return f === void 0 || (Qr(f), e.delete(d), f.data === void 0) ? !1 : (f.data = void 0, r -= 1, !0); + }; + _t(l); + const u = _t({ + has: a, + get: i, + set: c, + delete: l, + // eslint-disable-next-line jsdoc/check-types + [ + /** @type {typeof Symbol.toStringTag} */ + Ca + ]: "LRUCacheMap" + }); + return u; +}; +_t(Go); +const { freeze: mr } = Object, { isSafeInteger: $a } = Number, Ra = 1e3, Na = 100, Vo = (t = Ra, e = Na) => { + if (!$a(e) || e < 1) + throw TypeError( + "argsPerErrorBudget must be a safe positive integer number" + ); + const r = Go(t), n = (a, i) => { + const c = r.get(a); + c !== void 0 ? (c.length >= e && c.shift(), c.push(i)) : r.set(a, [i]); + }; + mr(n); + const o = (a) => { + const i = r.get(a); + return r.delete(a), i; + }; + return mr(o), mr({ + addLogArgs: n, + takeLogArgsArray: o + }); +}; +mr(Vo); +const At = new Ue(), j = (t, e = void 0) => { + const r = y({ + toString: y(() => Bo(t, e)) + }); + return de(At, r, t), r; +}; +y(j); +const Oa = y(/^[\w:-]( ?[\w:-])*$/), xr = (t, e = void 0) => { + if (typeof t != "string" || !xn(Oa, t)) + return j(t, e); + const r = y({ + toString: y(() => t) + }); + return de(At, r, t), r; +}; +y(xr); +const zr = new Ue(), Ho = ({ template: t, args: e }) => { + const r = [t[0]]; + for (let n = 0; n < e.length; n += 1) { + const o = e[n]; + let a; + St(At, o) ? a = `${o}` : Zr(o) ? a = `(${wr(o.name)})` : a = `(${wr(typeof o)})`, ee(r, a, t[n + 1]); + } + return Lt(r, ""); +}, Wo = y({ + toString() { + const t = Z(zr, this); + return t === void 0 ? "[Not a DetailsToken]" : Ho(t); + } +}); +y(Wo.toString); +const se = (t, ...e) => { + const r = y({ __proto__: Wo }); + return de(zr, r, { template: t, args: e }), /** @type {DetailsToken} */ + /** @type {unknown} */ + r; +}; +y(se); +const qo = (t, ...e) => (e = ce( + e, + (r) => St(At, r) ? r : j(r) +), se(t, ...e)); +y(qo); +const Ko = ({ template: t, args: e }) => { + const r = [t[0]]; + for (let n = 0; n < e.length; n += 1) { + let o = e[n]; + St(At, o) && (o = Z(At, o)); + const a = br(_r(r) || "", / $/, ""); + a !== "" && ee(r, a); + const i = br(t[n + 1], /^ /, ""); + ee(r, o, i); + } + return r[r.length - 1] === "" && _r(r), r; +}, gr = new Ue(); +let sn = 0; +const Wn = new Ue(), Yo = (t, e = t.name) => { + let r = Z(Wn, t); + return r !== void 0 || (sn += 1, r = `${e}#${sn}`, de(Wn, t, r)), r; +}, Ma = (t) => { + const e = je(t), { + name: r, + message: n, + errors: o = void 0, + cause: a = void 0, + stack: i = void 0, + ...c + } = e, l = Be(c); + if (l.length >= 1) { + for (const d of l) + delete t[d]; + const u = V(_n, c); + Br( + t, + se`originally with properties ${j(u)}` + ); + } + for (const u of Be(t)) { + const d = e[u]; + d && ie(d, "get") && U(t, u, { + value: t[u] + // invoke the getter to convert to data property + }); + } + y(t); +}, Me = (t = se`Assert failed`, e = P.Error, { + errorName: r = void 0, + cause: n = void 0, + errors: o = void 0, + sanitize: a = !0 +} = {}) => { + typeof t == "string" && (t = se([t])); + const i = Z(zr, t); + if (i === void 0) + throw _(`unrecognized details ${j(t)}`); + const c = Ho(i), l = n && { cause: n }; + let u; + return typeof Kr < "u" && e === Kr ? u = Kr(o || [], c, l) : (u = /** @type {ErrorConstructor} */ + e( + c, + l + ), o !== void 0 && U(u, "errors", { + value: o, + writable: !0, + enumerable: !1, + configurable: !0 + })), de(gr, u, Ko(i)), r !== void 0 && Yo(u, r), a && Ma(u), u; +}; +y(Me); +const { addLogArgs: La, takeLogArgsArray: Fa } = Vo(), an = new Ue(), Br = (t, e) => { + typeof e == "string" && (e = se([e])); + const r = Z(zr, e); + if (r === void 0) + throw _(`unrecognized details ${j(e)}`); + const n = Ko(r), o = Z(an, t); + if (o !== void 0) + for (const a of o) + a(t, n); + else + La(t, n); +}; +y(Br); +const Da = (t) => { + if (!("stack" in t)) + return ""; + const e = `${t.stack}`, r = ga(e, ` +`); + return Fo(e, " ") || r === -1 ? e : En(e, r + 1); +}, Sr = { + getStackString: P.getStackString || Da, + tagError: (t) => Yo(t), + resetErrorTagNum: () => { + sn = 0; + }, + getMessageLogArgs: (t) => Z(gr, t), + takeMessageLogArgs: (t) => { + const e = Z(gr, t); + return va(gr, t), e; + }, + takeNoteLogArgsArray: (t, e) => { + const r = Fa(t); + if (e !== void 0) { + const n = Z(an, t); + n ? ee(n, e) : de(an, t, [e]); + } + return r || []; + } +}; +y(Sr); +const Gr = (t = void 0, e = !1) => { + const r = e ? qo : se, n = r`Check failed`, o = (f = n, h = void 0, p = void 0) => { + const m = Me(f, h, p); + throw t !== void 0 && t(m), m; + }; + y(o); + const a = (f, ...h) => o(r(f, ...h)); + function i(f, h = void 0, p = void 0, m = void 0) { + f || o(h, p, m); + } + const c = (f, h, p = void 0, m = void 0, b = void 0) => { + Mr(f, h) || o( + p || r`Expected ${f} is same as ${h}`, + m || Ys, + b + ); + }; + y(c); + const l = (f, h, p) => { + if (typeof f !== h) { + if (typeof h == "string" || a`${j(h)} must be a string`, p === void 0) { + const m = wr(h); + p = r`${f} must be ${xr(m)}`; + } + o(p, _); + } + }; + y(l); + const d = Or(i, { + error: Me, + fail: o, + equal: c, + typeof: l, + string: (f, h = void 0) => l(f, "string", h), + note: Br, + details: r, + Fail: a, + quote: j, + bare: xr, + makeAssert: Gr + }); + return y(d); +}; +y(Gr); +const Y = Gr(), qn = Y.equal, Jo = Q( + ia, + Xe +); +Y(Jo); +const Xo = Jo.get; +Y(Xo); +const Ua = (t) => ae(Xo, t, []) !== void 0, ja = (t) => { + const e = +ye(t); + return ta(e) && ye(e) === t; +}, Za = (t) => { + Js(t), dt(Be(t), (e) => { + const r = Q(t, e); + Y(r), ja(e) || U(t, e, { + ...r, + writable: !1, + configurable: !1 + }); + }); +}, za = () => { + if (typeof P.harden == "function") + return P.harden; + const t = new Ot(), { harden: e } = { + /** + * @template T + * @param {T} root + * @returns {T} + */ + harden(r) { + const n = new Nt(); + function o(d) { + if (!Se(d)) + return; + const f = typeof d; + if (f !== "object" && f !== "function") + throw _(`Unexpected typeof: ${f}`); + ir(t, d) || wn(n, d) || bn(n, d); + } + const a = (d) => { + Ua(d) ? Za(d) : y(d); + const f = je(d), h = G(d); + o(h), dt(Be(f), (p) => { + const m = f[ + /** @type {string} */ + p + ]; + ie(m, "value") ? o(m.value) : (o(m.get), o(m.set)); + }); + }, i = Xr === void 0 && Ea === void 0 ? ( + // On platforms without v8's error own stack accessor problem, + // don't pay for any extra overhead. + a + ) : (d) => { + if (Zr(d)) { + const f = Q(d, "stack"); + f && f.get === Xr && f.configurable && U(d, "stack", { + // NOTE: Calls getter during harden, which seems dangerous. + // But we're only calling the problematic getter whose + // hazards we think we understand. + // @ts-expect-error TS should know FERAL_STACK_GETTER + // cannot be `undefined` here. + // See https://github.com/endojs/endo/pull/2232#discussion_r1575179471 + value: ae(Xr, d, []) + }); + } + return a(d); + }, c = () => { + Bn(n, i); + }, l = (d) => { + jr(t, d); + }, u = () => { + Bn(n, l); + }; + return o(r), c(), u(), r; + } + }; + return e; +}, Qo = { + // *** Value Properties of the Global Object + Infinity: 1 / 0, + NaN: NaN, + undefined: void 0 +}, es = { + // *** Function Properties of the Global Object + isFinite: "isFinite", + isNaN: "isNaN", + parseFloat: "parseFloat", + parseInt: "parseInt", + decodeURI: "decodeURI", + decodeURIComponent: "decodeURIComponent", + encodeURI: "encodeURI", + encodeURIComponent: "encodeURIComponent", + // *** Constructor Properties of the Global Object + Array: "Array", + ArrayBuffer: "ArrayBuffer", + BigInt: "BigInt", + BigInt64Array: "BigInt64Array", + BigUint64Array: "BigUint64Array", + Boolean: "Boolean", + DataView: "DataView", + EvalError: "EvalError", + // https://github.com/tc39/proposal-float16array + Float16Array: "Float16Array", + Float32Array: "Float32Array", + Float64Array: "Float64Array", + Int8Array: "Int8Array", + Int16Array: "Int16Array", + Int32Array: "Int32Array", + Map: "Map", + Number: "Number", + Object: "Object", + Promise: "Promise", + Proxy: "Proxy", + RangeError: "RangeError", + ReferenceError: "ReferenceError", + Set: "Set", + String: "String", + SyntaxError: "SyntaxError", + TypeError: "TypeError", + Uint8Array: "Uint8Array", + Uint8ClampedArray: "Uint8ClampedArray", + Uint16Array: "Uint16Array", + Uint32Array: "Uint32Array", + URIError: "URIError", + WeakMap: "WeakMap", + WeakSet: "WeakSet", + // https://github.com/tc39/proposal-iterator-helpers + Iterator: "Iterator", + // https://github.com/tc39/proposal-async-iterator-helpers + AsyncIterator: "AsyncIterator", + // https://github.com/endojs/endo/issues/550 + AggregateError: "AggregateError", + // *** Other Properties of the Global Object + JSON: "JSON", + Reflect: "Reflect", + // *** Annex B + escape: "escape", + unescape: "unescape", + // ESNext + lockdown: "lockdown", + harden: "harden", + HandledPromise: "HandledPromise" + // TODO: Until Promise.delegate (see below). +}, Kn = { + // *** Constructor Properties of the Global Object + Date: "%InitialDate%", + Error: "%InitialError%", + RegExp: "%InitialRegExp%", + // Omit `Symbol`, because we want the original to appear on the + // start compartment without passing through the whitelist mechanism, since + // we want to preserve all its properties, even if we never heard of them. + // Symbol: '%InitialSymbol%', + // *** Other Properties of the Global Object + Math: "%InitialMath%", + // ESNext + // From Error-stack proposal + // Only on initial global. No corresponding + // powerless form for other globals. + getStackString: "%InitialGetStackString%" + // TODO https://github.com/Agoric/SES-shim/issues/551 + // Need initial WeakRef and FinalizationGroup in + // start compartment only. +}, ts = { + // *** Constructor Properties of the Global Object + Date: "%SharedDate%", + Error: "%SharedError%", + RegExp: "%SharedRegExp%", + Symbol: "%SharedSymbol%", + // *** Other Properties of the Global Object + Math: "%SharedMath%" +}, rs = [ + EvalError, + RangeError, + ReferenceError, + SyntaxError, + TypeError, + URIError + // https://github.com/endojs/endo/issues/550 + // Commented out to accommodate platforms prior to AggregateError. + // Instead, conditional push below. + // AggregateError, +]; +typeof AggregateError < "u" && ee(rs, AggregateError); +const cn = { + "[[Proto]]": "%FunctionPrototype%", + length: "number", + name: "string" + // Do not specify "prototype" here, since only Function instances that can + // be used as a constructor have a prototype property. For constructors, + // since prototype properties are instance-specific, we define it there. +}, Ba = { + // This property is not mentioned in ECMA 262, but is present in V8 and + // necessary for lockdown to succeed. + "[[Proto]]": "%AsyncFunctionPrototype%" +}, s = cn, Yn = Ba, L = { + get: s, + set: "undefined" +}, Ne = { + get: s, + set: s +}, Jn = (t) => t === L || t === Ne; +function at(t) { + return { + // Properties of the NativeError Constructors + "[[Proto]]": "%SharedError%", + // NativeError.prototype + prototype: t + }; +} +function it(t) { + return { + // Properties of the NativeError Prototype Objects + "[[Proto]]": "%ErrorPrototype%", + constructor: t, + message: "string", + name: "string", + // Redundantly present only on v8. Safe to remove. + toString: !1, + // Superfluously present in some versions of V8. + // https://github.com/tc39/notes/blob/master/meetings/2021-10/oct-26.md#:~:text=However%2C%20Chrome%2093,and%20node%2016.11. + cause: !1 + }; +} +function be(t) { + return { + // Properties of the TypedArray Constructors + "[[Proto]]": "%TypedArray%", + BYTES_PER_ELEMENT: "number", + prototype: t + }; +} +function we(t) { + return { + // Properties of the TypedArray Prototype Objects + "[[Proto]]": "%TypedArrayPrototype%", + BYTES_PER_ELEMENT: "number", + constructor: t + }; +} +const Xn = { + E: "number", + LN10: "number", + LN2: "number", + LOG10E: "number", + LOG2E: "number", + PI: "number", + SQRT1_2: "number", + SQRT2: "number", + "@@toStringTag": "string", + abs: s, + acos: s, + acosh: s, + asin: s, + asinh: s, + atan: s, + atanh: s, + atan2: s, + cbrt: s, + ceil: s, + clz32: s, + cos: s, + cosh: s, + exp: s, + expm1: s, + floor: s, + fround: s, + hypot: s, + imul: s, + log: s, + log1p: s, + log10: s, + log2: s, + max: s, + min: s, + pow: s, + round: s, + sign: s, + sin: s, + sinh: s, + sqrt: s, + tan: s, + tanh: s, + trunc: s, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + idiv: !1, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + idivmod: !1, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + imod: !1, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + imuldiv: !1, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + irem: !1, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + mod: !1, + // See https://github.com/Moddable-OpenSource/moddable/issues/523#issuecomment-1942904505 + irandom: !1 +}, Er = { + // ECMA https://tc39.es/ecma262 + // The intrinsics object has no prototype to avoid conflicts. + "[[Proto]]": null, + // %ThrowTypeError% + "%ThrowTypeError%": s, + // *** The Global Object + // *** Value Properties of the Global Object + Infinity: "number", + NaN: "number", + undefined: "undefined", + // *** Function Properties of the Global Object + // eval + "%UniqueEval%": s, + isFinite: s, + isNaN: s, + parseFloat: s, + parseInt: s, + decodeURI: s, + decodeURIComponent: s, + encodeURI: s, + encodeURIComponent: s, + // *** Fundamental Objects + Object: { + // Properties of the Object Constructor + "[[Proto]]": "%FunctionPrototype%", + assign: s, + create: s, + defineProperties: s, + defineProperty: s, + entries: s, + freeze: s, + fromEntries: s, + getOwnPropertyDescriptor: s, + getOwnPropertyDescriptors: s, + getOwnPropertyNames: s, + getOwnPropertySymbols: s, + getPrototypeOf: s, + hasOwn: s, + is: s, + isExtensible: s, + isFrozen: s, + isSealed: s, + keys: s, + preventExtensions: s, + prototype: "%ObjectPrototype%", + seal: s, + setPrototypeOf: s, + values: s, + // https://github.com/tc39/proposal-array-grouping + groupBy: s, + // Seen on QuickJS + __getClass: !1 + }, + "%ObjectPrototype%": { + // Properties of the Object Prototype Object + "[[Proto]]": null, + constructor: "Object", + hasOwnProperty: s, + isPrototypeOf: s, + propertyIsEnumerable: s, + toLocaleString: s, + toString: s, + valueOf: s, + // Annex B: Additional Properties of the Object.prototype Object + // See note in header about the difference between [[Proto]] and --proto-- + // special notations. + "--proto--": Ne, + __defineGetter__: s, + __defineSetter__: s, + __lookupGetter__: s, + __lookupSetter__: s + }, + "%UniqueFunction%": { + // Properties of the Function Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%FunctionPrototype%" + }, + "%InertFunction%": { + "[[Proto]]": "%FunctionPrototype%", + prototype: "%FunctionPrototype%" + }, + "%FunctionPrototype%": { + apply: s, + bind: s, + call: s, + constructor: "%InertFunction%", + toString: s, + "@@hasInstance": s, + // proposed but not yet std. To be removed if there + caller: !1, + // proposed but not yet std. To be removed if there + arguments: !1, + // Seen on QuickJS. TODO grab getter for use by console + fileName: !1, + // Seen on QuickJS. TODO grab getter for use by console + lineNumber: !1 + }, + Boolean: { + // Properties of the Boolean Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%BooleanPrototype%" + }, + "%BooleanPrototype%": { + constructor: "Boolean", + toString: s, + valueOf: s + }, + "%SharedSymbol%": { + // Properties of the Symbol Constructor + "[[Proto]]": "%FunctionPrototype%", + asyncDispose: "symbol", + asyncIterator: "symbol", + dispose: "symbol", + for: s, + hasInstance: "symbol", + isConcatSpreadable: "symbol", + iterator: "symbol", + keyFor: s, + match: "symbol", + matchAll: "symbol", + prototype: "%SymbolPrototype%", + replace: "symbol", + search: "symbol", + species: "symbol", + split: "symbol", + toPrimitive: "symbol", + toStringTag: "symbol", + unscopables: "symbol", + // Seen at core-js https://github.com/zloirock/core-js#ecmascript-symbol + useSimple: !1, + // Seen at core-js https://github.com/zloirock/core-js#ecmascript-symbol + useSetter: !1, + // Seen on QuickJS + operatorSet: !1 + }, + "%SymbolPrototype%": { + // Properties of the Symbol Prototype Object + constructor: "%SharedSymbol%", + description: L, + toString: s, + valueOf: s, + "@@toPrimitive": s, + "@@toStringTag": "string" + }, + "%InitialError%": { + // Properties of the Error Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%ErrorPrototype%", + // Non standard, v8 only, used by tap + captureStackTrace: s, + // Non standard, v8 only, used by tap, tamed to accessor + stackTraceLimit: Ne, + // Non standard, v8 only, used by several, tamed to accessor + prepareStackTrace: Ne + }, + "%SharedError%": { + // Properties of the Error Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%ErrorPrototype%", + // Non standard, v8 only, used by tap + captureStackTrace: s, + // Non standard, v8 only, used by tap, tamed to accessor + stackTraceLimit: Ne, + // Non standard, v8 only, used by several, tamed to accessor + prepareStackTrace: Ne + }, + "%ErrorPrototype%": { + constructor: "%SharedError%", + message: "string", + name: "string", + toString: s, + // proposed de-facto, assumed TODO + // Seen on FF Nightly 88.0a1 + at: !1, + // Seen on FF and XS + stack: Ne, + // Superfluously present in some versions of V8. + // https://github.com/tc39/notes/blob/master/meetings/2021-10/oct-26.md#:~:text=However%2C%20Chrome%2093,and%20node%2016.11. + cause: !1 + }, + // NativeError + EvalError: at("%EvalErrorPrototype%"), + RangeError: at("%RangeErrorPrototype%"), + ReferenceError: at("%ReferenceErrorPrototype%"), + SyntaxError: at("%SyntaxErrorPrototype%"), + TypeError: at("%TypeErrorPrototype%"), + URIError: at("%URIErrorPrototype%"), + // https://github.com/endojs/endo/issues/550 + AggregateError: at("%AggregateErrorPrototype%"), + "%EvalErrorPrototype%": it("EvalError"), + "%RangeErrorPrototype%": it("RangeError"), + "%ReferenceErrorPrototype%": it("ReferenceError"), + "%SyntaxErrorPrototype%": it("SyntaxError"), + "%TypeErrorPrototype%": it("TypeError"), + "%URIErrorPrototype%": it("URIError"), + // https://github.com/endojs/endo/issues/550 + "%AggregateErrorPrototype%": it("AggregateError"), + // *** Numbers and Dates + Number: { + // Properties of the Number Constructor + "[[Proto]]": "%FunctionPrototype%", + EPSILON: "number", + isFinite: s, + isInteger: s, + isNaN: s, + isSafeInteger: s, + MAX_SAFE_INTEGER: "number", + MAX_VALUE: "number", + MIN_SAFE_INTEGER: "number", + MIN_VALUE: "number", + NaN: "number", + NEGATIVE_INFINITY: "number", + parseFloat: s, + parseInt: s, + POSITIVE_INFINITY: "number", + prototype: "%NumberPrototype%" + }, + "%NumberPrototype%": { + // Properties of the Number Prototype Object + constructor: "Number", + toExponential: s, + toFixed: s, + toLocaleString: s, + toPrecision: s, + toString: s, + valueOf: s + }, + BigInt: { + // Properties of the BigInt Constructor + "[[Proto]]": "%FunctionPrototype%", + asIntN: s, + asUintN: s, + prototype: "%BigIntPrototype%", + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + bitLength: !1, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + fromArrayBuffer: !1, + // Seen on QuickJS + tdiv: !1, + // Seen on QuickJS + fdiv: !1, + // Seen on QuickJS + cdiv: !1, + // Seen on QuickJS + ediv: !1, + // Seen on QuickJS + tdivrem: !1, + // Seen on QuickJS + fdivrem: !1, + // Seen on QuickJS + cdivrem: !1, + // Seen on QuickJS + edivrem: !1, + // Seen on QuickJS + sqrt: !1, + // Seen on QuickJS + sqrtrem: !1, + // Seen on QuickJS + floorLog2: !1, + // Seen on QuickJS + ctz: !1 + }, + "%BigIntPrototype%": { + constructor: "BigInt", + toLocaleString: s, + toString: s, + valueOf: s, + "@@toStringTag": "string" + }, + "%InitialMath%": { + ...Xn, + // `%InitialMath%.random()` has the standard unsafe behavior + random: s + }, + "%SharedMath%": { + ...Xn, + // `%SharedMath%.random()` is tamed to always throw + random: s + }, + "%InitialDate%": { + // Properties of the Date Constructor + "[[Proto]]": "%FunctionPrototype%", + now: s, + parse: s, + prototype: "%DatePrototype%", + UTC: s + }, + "%SharedDate%": { + // Properties of the Date Constructor + "[[Proto]]": "%FunctionPrototype%", + // `%SharedDate%.now()` is tamed to always throw + now: s, + parse: s, + prototype: "%DatePrototype%", + UTC: s + }, + "%DatePrototype%": { + constructor: "%SharedDate%", + getDate: s, + getDay: s, + getFullYear: s, + getHours: s, + getMilliseconds: s, + getMinutes: s, + getMonth: s, + getSeconds: s, + getTime: s, + getTimezoneOffset: s, + getUTCDate: s, + getUTCDay: s, + getUTCFullYear: s, + getUTCHours: s, + getUTCMilliseconds: s, + getUTCMinutes: s, + getUTCMonth: s, + getUTCSeconds: s, + setDate: s, + setFullYear: s, + setHours: s, + setMilliseconds: s, + setMinutes: s, + setMonth: s, + setSeconds: s, + setTime: s, + setUTCDate: s, + setUTCFullYear: s, + setUTCHours: s, + setUTCMilliseconds: s, + setUTCMinutes: s, + setUTCMonth: s, + setUTCSeconds: s, + toDateString: s, + toISOString: s, + toJSON: s, + toLocaleDateString: s, + toLocaleString: s, + toLocaleTimeString: s, + toString: s, + toTimeString: s, + toUTCString: s, + valueOf: s, + "@@toPrimitive": s, + // Annex B: Additional Properties of the Date.prototype Object + getYear: s, + setYear: s, + toGMTString: s + }, + // Text Processing + String: { + // Properties of the String Constructor + "[[Proto]]": "%FunctionPrototype%", + fromCharCode: s, + fromCodePoint: s, + prototype: "%StringPrototype%", + raw: s, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + fromArrayBuffer: !1 + }, + "%StringPrototype%": { + // Properties of the String Prototype Object + length: "number", + at: s, + charAt: s, + charCodeAt: s, + codePointAt: s, + concat: s, + constructor: "String", + endsWith: s, + includes: s, + indexOf: s, + lastIndexOf: s, + localeCompare: s, + match: s, + matchAll: s, + normalize: s, + padEnd: s, + padStart: s, + repeat: s, + replace: s, + replaceAll: s, + // ES2021 + search: s, + slice: s, + split: s, + startsWith: s, + substring: s, + toLocaleLowerCase: s, + toLocaleUpperCase: s, + toLowerCase: s, + toString: s, + toUpperCase: s, + trim: s, + trimEnd: s, + trimStart: s, + valueOf: s, + "@@iterator": s, + // Annex B: Additional Properties of the String.prototype Object + substr: s, + anchor: s, + big: s, + blink: s, + bold: s, + fixed: s, + fontcolor: s, + fontsize: s, + italics: s, + link: s, + small: s, + strike: s, + sub: s, + sup: s, + trimLeft: s, + trimRight: s, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + compare: !1, + // https://github.com/tc39/proposal-is-usv-string + isWellFormed: s, + toWellFormed: s, + unicodeSets: s, + // Seen on QuickJS + __quote: !1 + }, + "%StringIteratorPrototype%": { + "[[Proto]]": "%IteratorPrototype%", + next: s, + "@@toStringTag": "string" + }, + "%InitialRegExp%": { + // Properties of the RegExp Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%RegExpPrototype%", + "@@species": L, + // The https://github.com/tc39/proposal-regexp-legacy-features + // are all optional, unsafe, and omitted + input: !1, + $_: !1, + lastMatch: !1, + "$&": !1, + lastParen: !1, + "$+": !1, + leftContext: !1, + "$`": !1, + rightContext: !1, + "$'": !1, + $1: !1, + $2: !1, + $3: !1, + $4: !1, + $5: !1, + $6: !1, + $7: !1, + $8: !1, + $9: !1 + }, + "%SharedRegExp%": { + // Properties of the RegExp Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%RegExpPrototype%", + "@@species": L + }, + "%RegExpPrototype%": { + // Properties of the RegExp Prototype Object + constructor: "%SharedRegExp%", + exec: s, + dotAll: L, + flags: L, + global: L, + hasIndices: L, + ignoreCase: L, + "@@match": s, + "@@matchAll": s, + multiline: L, + "@@replace": s, + "@@search": s, + source: L, + "@@split": s, + sticky: L, + test: s, + toString: s, + unicode: L, + unicodeSets: L, + // Annex B: Additional Properties of the RegExp.prototype Object + compile: !1 + // UNSAFE and suppressed. + }, + "%RegExpStringIteratorPrototype%": { + // The %RegExpStringIteratorPrototype% Object + "[[Proto]]": "%IteratorPrototype%", + next: s, + "@@toStringTag": "string" + }, + // Indexed Collections + Array: { + // Properties of the Array Constructor + "[[Proto]]": "%FunctionPrototype%", + from: s, + isArray: s, + of: s, + prototype: "%ArrayPrototype%", + "@@species": L, + // Stage 3: + // https://tc39.es/proposal-relative-indexing-method/ + at: s, + // https://tc39.es/proposal-array-from-async/ + fromAsync: s + }, + "%ArrayPrototype%": { + // Properties of the Array Prototype Object + at: s, + length: "number", + concat: s, + constructor: "Array", + copyWithin: s, + entries: s, + every: s, + fill: s, + filter: s, + find: s, + findIndex: s, + flat: s, + flatMap: s, + forEach: s, + includes: s, + indexOf: s, + join: s, + keys: s, + lastIndexOf: s, + map: s, + pop: s, + push: s, + reduce: s, + reduceRight: s, + reverse: s, + shift: s, + slice: s, + some: s, + sort: s, + splice: s, + toLocaleString: s, + toString: s, + unshift: s, + values: s, + "@@iterator": s, + "@@unscopables": { + "[[Proto]]": null, + copyWithin: "boolean", + entries: "boolean", + fill: "boolean", + find: "boolean", + findIndex: "boolean", + flat: "boolean", + flatMap: "boolean", + includes: "boolean", + keys: "boolean", + values: "boolean", + // Failed tc39 proposal + // Seen on FF Nightly 88.0a1 + at: "boolean", + // See https://github.com/tc39/proposal-array-find-from-last + findLast: "boolean", + findLastIndex: "boolean", + // https://github.com/tc39/proposal-change-array-by-copy + toReversed: "boolean", + toSorted: "boolean", + toSpliced: "boolean", + with: "boolean", + // https://github.com/tc39/proposal-array-grouping + group: "boolean", + groupToMap: "boolean", + groupBy: "boolean" + }, + // See https://github.com/tc39/proposal-array-find-from-last + findLast: s, + findLastIndex: s, + // https://github.com/tc39/proposal-change-array-by-copy + toReversed: s, + toSorted: s, + toSpliced: s, + with: s, + // https://github.com/tc39/proposal-array-grouping + group: s, + // Not in proposal? Where? + groupToMap: s, + // Not in proposal? Where? + groupBy: s + }, + "%ArrayIteratorPrototype%": { + // The %ArrayIteratorPrototype% Object + "[[Proto]]": "%IteratorPrototype%", + next: s, + "@@toStringTag": "string" + }, + // *** TypedArray Objects + "%TypedArray%": { + // Properties of the %TypedArray% Intrinsic Object + "[[Proto]]": "%FunctionPrototype%", + from: s, + of: s, + prototype: "%TypedArrayPrototype%", + "@@species": L + }, + "%TypedArrayPrototype%": { + at: s, + buffer: L, + byteLength: L, + byteOffset: L, + constructor: "%TypedArray%", + copyWithin: s, + entries: s, + every: s, + fill: s, + filter: s, + find: s, + findIndex: s, + forEach: s, + includes: s, + indexOf: s, + join: s, + keys: s, + lastIndexOf: s, + length: L, + map: s, + reduce: s, + reduceRight: s, + reverse: s, + set: s, + slice: s, + some: s, + sort: s, + subarray: s, + toLocaleString: s, + toString: s, + values: s, + "@@iterator": s, + "@@toStringTag": L, + // See https://github.com/tc39/proposal-array-find-from-last + findLast: s, + findLastIndex: s, + // https://github.com/tc39/proposal-change-array-by-copy + toReversed: s, + toSorted: s, + with: s + }, + // The TypedArray Constructors + BigInt64Array: be("%BigInt64ArrayPrototype%"), + BigUint64Array: be("%BigUint64ArrayPrototype%"), + // https://github.com/tc39/proposal-float16array + Float16Array: be("%Float16ArrayPrototype%"), + Float32Array: be("%Float32ArrayPrototype%"), + Float64Array: be("%Float64ArrayPrototype%"), + Int16Array: be("%Int16ArrayPrototype%"), + Int32Array: be("%Int32ArrayPrototype%"), + Int8Array: be("%Int8ArrayPrototype%"), + Uint16Array: be("%Uint16ArrayPrototype%"), + Uint32Array: be("%Uint32ArrayPrototype%"), + Uint8ClampedArray: be("%Uint8ClampedArrayPrototype%"), + Uint8Array: { + ...be("%Uint8ArrayPrototype%"), + // https://github.com/tc39/proposal-arraybuffer-base64 + fromBase64: s, + // https://github.com/tc39/proposal-arraybuffer-base64 + fromHex: s + }, + "%BigInt64ArrayPrototype%": we("BigInt64Array"), + "%BigUint64ArrayPrototype%": we("BigUint64Array"), + // https://github.com/tc39/proposal-float16array + "%Float16ArrayPrototype%": we("Float16Array"), + "%Float32ArrayPrototype%": we("Float32Array"), + "%Float64ArrayPrototype%": we("Float64Array"), + "%Int16ArrayPrototype%": we("Int16Array"), + "%Int32ArrayPrototype%": we("Int32Array"), + "%Int8ArrayPrototype%": we("Int8Array"), + "%Uint16ArrayPrototype%": we("Uint16Array"), + "%Uint32ArrayPrototype%": we("Uint32Array"), + "%Uint8ClampedArrayPrototype%": we("Uint8ClampedArray"), + "%Uint8ArrayPrototype%": { + ...we("Uint8Array"), + // https://github.com/tc39/proposal-arraybuffer-base64 + setFromBase64: s, + // https://github.com/tc39/proposal-arraybuffer-base64 + setFromHex: s, + // https://github.com/tc39/proposal-arraybuffer-base64 + toBase64: s, + // https://github.com/tc39/proposal-arraybuffer-base64 + toHex: s + }, + // *** Keyed Collections + Map: { + // Properties of the Map Constructor + "[[Proto]]": "%FunctionPrototype%", + "@@species": L, + prototype: "%MapPrototype%", + // https://github.com/tc39/proposal-array-grouping + groupBy: s + }, + "%MapPrototype%": { + clear: s, + constructor: "Map", + delete: s, + entries: s, + forEach: s, + get: s, + has: s, + keys: s, + set: s, + size: L, + values: s, + "@@iterator": s, + "@@toStringTag": "string" + }, + "%MapIteratorPrototype%": { + // The %MapIteratorPrototype% Object + "[[Proto]]": "%IteratorPrototype%", + next: s, + "@@toStringTag": "string" + }, + Set: { + // Properties of the Set Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%SetPrototype%", + "@@species": L, + // Seen on QuickJS + groupBy: !1 + }, + "%SetPrototype%": { + add: s, + clear: s, + constructor: "Set", + delete: s, + entries: s, + forEach: s, + has: s, + keys: s, + size: L, + values: s, + "@@iterator": s, + "@@toStringTag": "string", + // See https://github.com/tc39/proposal-set-methods + intersection: s, + // See https://github.com/tc39/proposal-set-methods + union: s, + // See https://github.com/tc39/proposal-set-methods + difference: s, + // See https://github.com/tc39/proposal-set-methods + symmetricDifference: s, + // See https://github.com/tc39/proposal-set-methods + isSubsetOf: s, + // See https://github.com/tc39/proposal-set-methods + isSupersetOf: s, + // See https://github.com/tc39/proposal-set-methods + isDisjointFrom: s + }, + "%SetIteratorPrototype%": { + // The %SetIteratorPrototype% Object + "[[Proto]]": "%IteratorPrototype%", + next: s, + "@@toStringTag": "string" + }, + WeakMap: { + // Properties of the WeakMap Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%WeakMapPrototype%" + }, + "%WeakMapPrototype%": { + constructor: "WeakMap", + delete: s, + get: s, + has: s, + set: s, + "@@toStringTag": "string" + }, + WeakSet: { + // Properties of the WeakSet Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%WeakSetPrototype%" + }, + "%WeakSetPrototype%": { + add: s, + constructor: "WeakSet", + delete: s, + has: s, + "@@toStringTag": "string" + }, + // *** Structured Data + ArrayBuffer: { + // Properties of the ArrayBuffer Constructor + "[[Proto]]": "%FunctionPrototype%", + isView: s, + prototype: "%ArrayBufferPrototype%", + "@@species": L, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + fromString: !1, + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + fromBigInt: !1 + }, + "%ArrayBufferPrototype%": { + byteLength: L, + constructor: "ArrayBuffer", + slice: s, + "@@toStringTag": "string", + // See https://github.com/Moddable-OpenSource/moddable/issues/523 + concat: !1, + // See https://github.com/tc39/proposal-resizablearraybuffer + transfer: s, + resize: s, + resizable: L, + maxByteLength: L, + // https://github.com/tc39/proposal-arraybuffer-transfer + transferToFixedLength: s, + detached: L + }, + // SharedArrayBuffer Objects + SharedArrayBuffer: !1, + // UNSAFE and purposely suppressed. + "%SharedArrayBufferPrototype%": !1, + // UNSAFE and purposely suppressed. + DataView: { + // Properties of the DataView Constructor + "[[Proto]]": "%FunctionPrototype%", + BYTES_PER_ELEMENT: "number", + // Non std but undeletable on Safari. + prototype: "%DataViewPrototype%" + }, + "%DataViewPrototype%": { + buffer: L, + byteLength: L, + byteOffset: L, + constructor: "DataView", + getBigInt64: s, + getBigUint64: s, + // https://github.com/tc39/proposal-float16array + getFloat16: s, + getFloat32: s, + getFloat64: s, + getInt8: s, + getInt16: s, + getInt32: s, + getUint8: s, + getUint16: s, + getUint32: s, + setBigInt64: s, + setBigUint64: s, + // https://github.com/tc39/proposal-float16array + setFloat16: s, + setFloat32: s, + setFloat64: s, + setInt8: s, + setInt16: s, + setInt32: s, + setUint8: s, + setUint16: s, + setUint32: s, + "@@toStringTag": "string" + }, + // Atomics + Atomics: !1, + // UNSAFE and suppressed. + JSON: { + parse: s, + stringify: s, + "@@toStringTag": "string", + // https://github.com/tc39/proposal-json-parse-with-source/ + rawJSON: s, + isRawJSON: s + }, + // *** Control Abstraction Objects + // https://github.com/tc39/proposal-iterator-helpers + Iterator: { + // Properties of the Iterator Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%IteratorPrototype%", + from: s + }, + "%IteratorPrototype%": { + // The %IteratorPrototype% Object + "@@iterator": s, + // https://github.com/tc39/proposal-iterator-helpers + constructor: "Iterator", + map: s, + filter: s, + take: s, + drop: s, + flatMap: s, + reduce: s, + toArray: s, + forEach: s, + some: s, + every: s, + find: s, + "@@toStringTag": "string", + // https://github.com/tc39/proposal-async-iterator-helpers + toAsync: s, + // See https://github.com/Moddable-OpenSource/moddable/issues/523#issuecomment-1942904505 + "@@dispose": !1 + }, + // https://github.com/tc39/proposal-iterator-helpers + "%WrapForValidIteratorPrototype%": { + "[[Proto]]": "%IteratorPrototype%", + next: s, + return: s + }, + // https://github.com/tc39/proposal-iterator-helpers + "%IteratorHelperPrototype%": { + "[[Proto]]": "%IteratorPrototype%", + next: s, + return: s, + "@@toStringTag": "string" + }, + // https://github.com/tc39/proposal-async-iterator-helpers + AsyncIterator: { + // Properties of the Iterator Constructor + "[[Proto]]": "%FunctionPrototype%", + prototype: "%AsyncIteratorPrototype%", + from: s + }, + "%AsyncIteratorPrototype%": { + // The %AsyncIteratorPrototype% Object + "@@asyncIterator": s, + // https://github.com/tc39/proposal-async-iterator-helpers + constructor: "AsyncIterator", + map: s, + filter: s, + take: s, + drop: s, + flatMap: s, + reduce: s, + toArray: s, + forEach: s, + some: s, + every: s, + find: s, + "@@toStringTag": "string", + // See https://github.com/Moddable-OpenSource/moddable/issues/523#issuecomment-1942904505 + "@@asyncDispose": !1 + }, + // https://github.com/tc39/proposal-async-iterator-helpers + "%WrapForValidAsyncIteratorPrototype%": { + "[[Proto]]": "%AsyncIteratorPrototype%", + next: s, + return: s + }, + // https://github.com/tc39/proposal-async-iterator-helpers + "%AsyncIteratorHelperPrototype%": { + "[[Proto]]": "%AsyncIteratorPrototype%", + next: s, + return: s, + "@@toStringTag": "string" + }, + "%InertGeneratorFunction%": { + // Properties of the GeneratorFunction Constructor + "[[Proto]]": "%InertFunction%", + prototype: "%Generator%" + }, + "%Generator%": { + // Properties of the GeneratorFunction Prototype Object + "[[Proto]]": "%FunctionPrototype%", + constructor: "%InertGeneratorFunction%", + prototype: "%GeneratorPrototype%", + "@@toStringTag": "string" + }, + "%InertAsyncGeneratorFunction%": { + // Properties of the AsyncGeneratorFunction Constructor + "[[Proto]]": "%InertFunction%", + prototype: "%AsyncGenerator%" + }, + "%AsyncGenerator%": { + // Properties of the AsyncGeneratorFunction Prototype Object + "[[Proto]]": "%FunctionPrototype%", + constructor: "%InertAsyncGeneratorFunction%", + prototype: "%AsyncGeneratorPrototype%", + // length prop added here for React Native jsc-android + // https://github.com/endojs/endo/issues/660 + // https://github.com/react-native-community/jsc-android-buildscripts/issues/181 + length: "number", + "@@toStringTag": "string" + }, + "%GeneratorPrototype%": { + // Properties of the Generator Prototype Object + "[[Proto]]": "%IteratorPrototype%", + constructor: "%Generator%", + next: s, + return: s, + throw: s, + "@@toStringTag": "string" + }, + "%AsyncGeneratorPrototype%": { + // Properties of the AsyncGenerator Prototype Object + "[[Proto]]": "%AsyncIteratorPrototype%", + constructor: "%AsyncGenerator%", + next: s, + return: s, + throw: s, + "@@toStringTag": "string" + }, + // TODO: To be replaced with Promise.delegate + // + // The HandledPromise global variable shimmed by `@agoric/eventual-send/shim` + // implements an initial version of the eventual send specification at: + // https://github.com/tc39/proposal-eventual-send + // + // We will likely change this to add a property to Promise called + // Promise.delegate and put static methods on it, which will necessitate + // another whitelist change to update to the current proposed standard. + HandledPromise: { + "[[Proto]]": "Promise", + applyFunction: s, + applyFunctionSendOnly: s, + applyMethod: s, + applyMethodSendOnly: s, + get: s, + getSendOnly: s, + prototype: "%PromisePrototype%", + resolve: s + }, + Promise: { + // Properties of the Promise Constructor + "[[Proto]]": "%FunctionPrototype%", + all: s, + allSettled: s, + // https://github.com/Agoric/SES-shim/issues/550 + any: s, + prototype: "%PromisePrototype%", + race: s, + reject: s, + resolve: s, + // https://github.com/tc39/proposal-promise-with-resolvers + withResolvers: s, + "@@species": L + }, + "%PromisePrototype%": { + // Properties of the Promise Prototype Object + catch: s, + constructor: "Promise", + finally: s, + then: s, + "@@toStringTag": "string", + // Non-standard, used in node to prevent async_hooks from breaking + "UniqueSymbol(async_id_symbol)": Ne, + "UniqueSymbol(trigger_async_id_symbol)": Ne, + "UniqueSymbol(destroyed)": Ne + }, + "%InertAsyncFunction%": { + // Properties of the AsyncFunction Constructor + "[[Proto]]": "%InertFunction%", + prototype: "%AsyncFunctionPrototype%" + }, + "%AsyncFunctionPrototype%": { + // Properties of the AsyncFunction Prototype Object + "[[Proto]]": "%FunctionPrototype%", + constructor: "%InertAsyncFunction%", + // length prop added here for React Native jsc-android + // https://github.com/endojs/endo/issues/660 + // https://github.com/react-native-community/jsc-android-buildscripts/issues/181 + length: "number", + "@@toStringTag": "string" + }, + // Reflection + Reflect: { + // The Reflect Object + // Not a function object. + apply: s, + construct: s, + defineProperty: s, + deleteProperty: s, + get: s, + getOwnPropertyDescriptor: s, + getPrototypeOf: s, + has: s, + isExtensible: s, + ownKeys: s, + preventExtensions: s, + set: s, + setPrototypeOf: s, + "@@toStringTag": "string" + }, + Proxy: { + // Properties of the Proxy Constructor + "[[Proto]]": "%FunctionPrototype%", + revocable: s + }, + // Appendix B + // Annex B: Additional Properties of the Global Object + escape: s, + unescape: s, + // Proposed + "%UniqueCompartment%": { + "[[Proto]]": "%FunctionPrototype%", + prototype: "%CompartmentPrototype%", + toString: s + }, + "%InertCompartment%": { + "[[Proto]]": "%FunctionPrototype%", + prototype: "%CompartmentPrototype%", + toString: s + }, + "%CompartmentPrototype%": { + constructor: "%InertCompartment%", + evaluate: s, + globalThis: L, + name: L, + import: Yn, + load: Yn, + importNow: s, + module: s, + "@@toStringTag": "string" + }, + lockdown: s, + harden: { ...s, isFake: "boolean" }, + "%InitialGetStackString%": s +}, Ga = (t) => typeof t == "function"; +function Va(t, e, r) { + if (ie(t, e)) { + const n = Q(t, e); + if (!n || !Mr(n.value, r.value) || n.get !== r.get || n.set !== r.set || n.writable !== r.writable || n.enumerable !== r.enumerable || n.configurable !== r.configurable) + throw _(`Conflicting definitions of ${e}`); + } + U(t, e, r); +} +function Ha(t, e) { + for (const [r, n] of fe(e)) + Va(t, r, n); +} +function ns(t, e) { + const r = { __proto__: null }; + for (const [n, o] of fe(e)) + ie(t, n) && (r[o] = t[n]); + return r; +} +const os = () => { + const t = V(null); + let e; + const r = (c) => { + Ha(t, je(c)); + }; + y(r); + const n = () => { + for (const [c, l] of fe(t)) { + if (!Se(l) || !ie(l, "prototype")) + continue; + const u = Er[c]; + if (typeof u != "object") + throw _(`Expected permit object at whitelist.${c}`); + const d = u.prototype; + if (!d) + throw _(`${c}.prototype property not whitelisted`); + if (typeof d != "string" || !ie(Er, d)) + throw _(`Unrecognized ${c}.prototype whitelist entry`); + const f = l.prototype; + if (ie(t, d)) { + if (t[d] !== f) + throw _(`Conflicting bindings of ${d}`); + continue; + } + t[d] = f; + } + }; + y(n); + const o = () => (y(t), e = new Ot(Qe(Eo(t), Ga)), t); + y(o); + const a = (c) => { + if (!e) + throw _( + "isPseudoNative can only be called after finalIntrinsics" + ); + return ir(e, c); + }; + y(a); + const i = { + addIntrinsics: r, + completePrototypes: n, + finalIntrinsics: o, + isPseudoNative: a + }; + return y(i), r(Qo), r(ns(P, es)), i; +}, Wa = (t) => { + const { addIntrinsics: e, finalIntrinsics: r } = os(); + return e(ns(t, ts)), r(); +}; +function qa(t, e) { + let r = !1; + const n = (h, ...p) => (r || (console.groupCollapsed("Removing unpermitted intrinsics"), r = !0), console[h](...p)), o = ["undefined", "boolean", "number", "string", "symbol"], a = new Ce( + wt ? ce( + Qe( + fe(Er["%SharedSymbol%"]), + ([h, p]) => p === "symbol" && typeof wt[h] == "symbol" + ), + ([h]) => [wt[h], `@@${h}`] + ) : [] + ); + function i(h, p) { + if (typeof p == "string") + return p; + const m = Ge(a, p); + if (typeof p == "symbol") { + if (m) + return m; + { + const b = Qs(p); + return b !== void 0 ? `RegisteredSymbol(${b})` : `Unique${ye(p)}`; + } + } + throw _(`Unexpected property name type ${h} ${p}`); + } + function c(h, p, m) { + if (!Se(p)) + throw _(`Object expected: ${h}, ${p}, ${m}`); + const b = G(p); + if (!(b === null && m === null)) { + if (m !== void 0 && typeof m != "string") + throw _(`Malformed whitelist permit ${h}.__proto__`); + if (b !== t[m || "%ObjectPrototype%"]) + throw _(`Unexpected intrinsic ${h}.__proto__ at ${m}`); + } + } + function l(h, p, m, b) { + if (typeof b == "object") + return f(h, p, b), !0; + if (b === !1) + return !1; + if (typeof b == "string") { + if (m === "prototype" || m === "constructor") { + if (ie(t, b)) { + if (p !== t[b]) + throw _(`Does not match whitelist ${h}`); + return !0; + } + } else if (Dr(o, b)) { + if (typeof p !== b) + throw _( + `At ${h} expected ${b} not ${typeof p}` + ); + return !0; + } + } + throw _(`Unexpected whitelist permit ${b} at ${h}`); + } + function u(h, p, m, b) { + const S = Q(p, m); + if (!S) + throw _(`Property ${m} not found at ${h}`); + if (ie(S, "value")) { + if (Jn(b)) + throw _(`Accessor expected at ${h}`); + return l(h, S.value, m, b); + } + if (!Jn(b)) + throw _(`Accessor not expected at ${h}`); + return l(`${h}`, S.get, m, b.get) && l(`${h}`, S.set, m, b.set); + } + function d(h, p, m) { + const b = m === "__proto__" ? "--proto--" : m; + if (ie(p, b)) + return p[b]; + if (typeof h == "function" && ie(cn, b)) + return cn[b]; + } + function f(h, p, m) { + if (p == null) + return; + const b = m["[[Proto]]"]; + c(h, p, b), typeof p == "function" && e(p); + for (const S of Be(p)) { + const x = i(h, S), I = `${h}.${x}`, E = d(p, m, x); + if (!E || !u(I, p, S, E)) { + E !== !1 && n("warn", `Removing ${I}`); + try { + delete p[S]; + } catch (A) { + if (S in p) { + if (typeof p == "function" && S === "prototype" && (p.prototype = void 0, p.prototype === void 0)) { + n( + "warn", + `Tolerating undeletable ${I} === undefined` + ); + continue; + } + n("error", `failed to delete ${I}`, A); + } else + n("error", `deleting ${I} threw`, A); + throw A; + } + } + } + } + try { + f("intrinsics", t, Er); + } finally { + r && console.groupEnd(); + } +} +function Ka() { + try { + xe.prototype.constructor("return 1"); + } catch { + return y({}); + } + const t = {}; + function e(r, n, o) { + let a; + try { + a = (0, eval)(o); + } catch (l) { + if (l instanceof or) + return; + throw l; + } + const i = G(a), c = function() { + throw _( + "Function.prototype.constructor is not a valid constructor." + ); + }; + z(c, { + prototype: { value: i }, + name: { + value: r, + writable: !1, + enumerable: !1, + configurable: !0 + } + }), z(i, { + constructor: { value: c } + }), c !== xe.prototype.constructor && So(c, xe.prototype.constructor), t[n] = c; + } + return e("Function", "%InertFunction%", "(function(){})"), e( + "GeneratorFunction", + "%InertGeneratorFunction%", + "(function*(){})" + ), e( + "AsyncFunction", + "%InertAsyncFunction%", + "(async function(){})" + ), e( + "AsyncGeneratorFunction", + "%InertAsyncGeneratorFunction%", + "(async function*(){})" + ), t; +} +function Ya(t = "safe") { + if (t !== "safe" && t !== "unsafe") + throw _(`unrecognized dateTaming ${t}`); + const e = Gs, r = e.prototype, n = { + /** + * `%SharedDate%.now()` throw a `TypeError` starting with "secure mode". + * See https://github.com/endojs/endo/issues/910#issuecomment-1581855420 + */ + now() { + throw _("secure mode Calling %SharedDate%.now() throws"); + } + }, o = ({ powers: c = "none" } = {}) => { + let l; + return c === "original" ? l = function(...d) { + return new.target === void 0 ? ae(e, void 0, d) : yr(e, d, new.target); + } : l = function(...d) { + if (new.target === void 0) + throw _( + "secure mode Calling %SharedDate% constructor as a function throws" + ); + if (d.length === 0) + throw _( + "secure mode Calling new %SharedDate%() with no arguments throws" + ); + return yr(e, d, new.target); + }, z(l, { + length: { value: 7 }, + prototype: { + value: r, + writable: !1, + enumerable: !1, + configurable: !1 + }, + parse: { + value: e.parse, + writable: !0, + enumerable: !1, + configurable: !0 + }, + UTC: { + value: e.UTC, + writable: !0, + enumerable: !1, + configurable: !0 + } + }), l; + }, a = o({ powers: "original" }), i = o({ powers: "none" }); + return z(a, { + now: { + value: e.now, + writable: !0, + enumerable: !1, + configurable: !0 + } + }), z(i, { + now: { + value: n.now, + writable: !0, + enumerable: !1, + configurable: !0 + } + }), z(r, { + constructor: { value: i } + }), { + "%InitialDate%": a, + "%SharedDate%": i + }; +} +function Ja(t = "safe") { + if (t !== "safe" && t !== "unsafe") + throw _(`unrecognized mathTaming ${t}`); + const e = Ws, r = e, { random: n, ...o } = je(e), i = V(_n, { + ...o, + random: { + value: { + /** + * `%SharedMath%.random()` throws a TypeError starting with "secure mode". + * See https://github.com/endojs/endo/issues/910#issuecomment-1581855420 + */ + random() { + throw _("secure mode %SharedMath%.random() throws"); + } + }.random, + writable: !0, + enumerable: !1, + configurable: !0 + } + }); + return { + "%InitialMath%": r, + "%SharedMath%": i + }; +} +function Xa(t = "safe") { + if (t !== "safe" && t !== "unsafe") + throw _(`unrecognized regExpTaming ${t}`); + const e = Je.prototype, r = (a = {}) => { + const i = function(...l) { + return new.target === void 0 ? Je(...l) : yr(Je, l, new.target); + }; + if (z(i, { + length: { value: 2 }, + prototype: { + value: e, + writable: !1, + enumerable: !1, + configurable: !1 + } + }), Yr) { + const c = Q( + Je, + Yr + ); + if (!c) + throw _("no RegExp[Symbol.species] descriptor"); + z(i, { + [Yr]: c + }); + } + return i; + }, n = r(), o = r(); + return t !== "unsafe" && delete e.compile, z(e, { + constructor: { value: o } + }), { + "%InitialRegExp%": n, + "%SharedRegExp%": o + }; +} +const Qa = { + "%ObjectPrototype%": { + toString: !0 + }, + "%FunctionPrototype%": { + toString: !0 + // set by "rollup" + }, + "%ErrorPrototype%": { + name: !0 + // set by "precond", "ava", "node-fetch" + }, + "%IteratorPrototype%": { + toString: !0, + // https://github.com/tc39/proposal-iterator-helpers + constructor: !0, + // https://github.com/tc39/proposal-iterator-helpers + [Xe]: !0 + } +}, ss = { + "%ObjectPrototype%": { + toString: !0, + valueOf: !0 + }, + "%ArrayPrototype%": { + toString: !0, + push: !0, + // set by "Google Analytics" + concat: !0, + // set by mobx generated code (old TS compiler?) + [sr]: !0 + // set by mobx generated code (old TS compiler?) + }, + // Function.prototype has no 'prototype' property to enable. + // Function instances have their own 'name' and 'length' properties + // which are configurable and non-writable. Thus, they are already + // non-assignable anyway. + "%FunctionPrototype%": { + constructor: !0, + // set by "regenerator-runtime" + bind: !0, + // set by "underscore", "express" + toString: !0 + // set by "rollup" + }, + "%ErrorPrototype%": { + constructor: !0, + // set by "fast-json-patch", "node-fetch" + message: !0, + name: !0, + // set by "precond", "ava", "node-fetch", "node 14" + toString: !0 + // set by "bluebird" + }, + "%TypeErrorPrototype%": { + constructor: !0, + // set by "readable-stream" + message: !0, + // set by "tape" + name: !0 + // set by "readable-stream", "node 14" + }, + "%SyntaxErrorPrototype%": { + message: !0, + // to match TypeErrorPrototype.message + name: !0 + // set by "node 14" + }, + "%RangeErrorPrototype%": { + message: !0, + // to match TypeErrorPrototype.message + name: !0 + // set by "node 14" + }, + "%URIErrorPrototype%": { + message: !0, + // to match TypeErrorPrototype.message + name: !0 + // set by "node 14" + }, + "%EvalErrorPrototype%": { + message: !0, + // to match TypeErrorPrototype.message + name: !0 + // set by "node 14" + }, + "%ReferenceErrorPrototype%": { + message: !0, + // to match TypeErrorPrototype.message + name: !0 + // set by "node 14" + }, + // https://github.com/endojs/endo/issues/550 + "%AggregateErrorPrototype%": { + message: !0, + // to match TypeErrorPrototype.message + name: !0 + // set by "node 14"? + }, + "%PromisePrototype%": { + constructor: !0 + // set by "core-js" + }, + "%TypedArrayPrototype%": "*", + // set by https://github.com/feross/buffer + "%Generator%": { + constructor: !0, + name: !0, + toString: !0 + }, + "%IteratorPrototype%": { + toString: !0, + // https://github.com/tc39/proposal-iterator-helpers + constructor: !0, + // https://github.com/tc39/proposal-iterator-helpers + [Xe]: !0 + } +}, ei = { + ...ss, + /** + * Rollup (as used at least by vega) and webpack + * (as used at least by regenerator) both turn exports into assignments + * to a big `exports` object that inherits directly from + * `Object.prototype`. Some of the exported names we've seen include + * `hasOwnProperty`, `constructor`, and `toString`. But the strategy used + * by rollup and webpack potentionally turns any exported name + * into an assignment rejected by the override mistake. That's why + * the `severe` enablements takes the extreme step of enabling + * everything on `Object.prototype`. + * + * In addition, code doing inheritance manually will often override + * the `constructor` property on the new prototype by assignment. We've + * seen this several times. + * + * The cost of enabling all these is that they create a miserable debugging + * experience specifically on Node. + * https://github.com/Agoric/agoric-sdk/issues/2324 + * explains how it confused the Node console. + * + * (TODO Reexamine the vscode situation. I think it may have improved + * since the following paragraph was written.) + * + * The vscode debugger's object inspector shows the own data properties of + * an object, which is typically what you want, but also shows both getter + * and setter for every accessor property whether inherited or own. + * With the `'*'` setting here, all the properties inherited from + * `Object.prototype` are accessors, creating an unusable display as seen + * at As explained at + * https://github.com/endojs/endo/blob/master/packages/ses/docs/lockdown.md#overridetaming-options + * Open the triangles at the bottom of that section. + */ + "%ObjectPrototype%": "*", + /** + * The widely used Buffer defined at https://github.com/feross/buffer + * on initialization, manually creates the equivalent of a subclass of + * `TypedArray`, which it then initializes by assignment. These assignments + * include enough of the `TypeArray` methods that here, the `severe` + * enablements just enable them all. + */ + "%TypedArrayPrototype%": "*", + /** + * Needed to work with Immer before https://github.com/immerjs/immer/pull/914 + * is accepted. + */ + "%MapPrototype%": "*", + /** + * Needed to work with Immer before https://github.com/immerjs/immer/pull/914 + * is accepted. + */ + "%SetPrototype%": "*" +}; +function ti(t, e, r = []) { + const n = new Nt(r); + function o(u, d, f, h) { + if ("value" in h && h.configurable) { + const { value: p } = h, m = wn(n, f), { get: b, set: S } = Q( + { + get [f]() { + return p; + }, + set [f](x) { + if (d === this) + throw _( + `Cannot assign to read only property '${ye( + f + )}' of '${u}'` + ); + ie(this, f) ? this[f] = x : (m && console.error(_(`Override property ${f}`)), U(this, f, { + value: x, + writable: !0, + enumerable: !0, + configurable: !0 + })); + } + }, + f + ); + U(b, "originalValue", { + value: p, + writable: !1, + enumerable: !1, + configurable: !1 + }), U(d, f, { + get: b, + set: S, + enumerable: h.enumerable, + configurable: h.configurable + }); + } + } + function a(u, d, f) { + const h = Q(d, f); + h && o(u, d, f, h); + } + function i(u, d) { + const f = je(d); + f && dt(Be(f), (h) => o(u, d, h, f[h])); + } + function c(u, d, f) { + for (const h of Be(f)) { + const p = Q(d, h); + if (!p || p.get || p.set) + continue; + const m = `${u}.${ye(h)}`, b = f[h]; + if (b === !0) + a(m, d, h); + else if (b === "*") + i(m, p.value); + else if (Se(b)) + c(m, p.value, b); + else + throw _(`Unexpected override enablement plan ${m}`); + } + } + let l; + switch (e) { + case "min": { + l = Qa; + break; + } + case "moderate": { + l = ss; + break; + } + case "severe": { + l = ei; + break; + } + default: + throw _(`unrecognized overrideTaming ${e}`); + } + c("root", t, l); +} +const { Fail: ln, quote: kr } = Y, ri = /^(\w*[a-z])Locale([A-Z]\w*)$/, as = { + // See https://tc39.es/ecma262/#sec-string.prototype.localecompare + localeCompare(t) { + if (this === null || this === void 0) + throw _( + 'Cannot localeCompare with null or undefined "this" value' + ); + const e = `${this}`, r = `${t}`; + return e < r ? -1 : e > r ? 1 : (e === r || ln`expected ${kr(e)} and ${kr(r)} to compare`, 0); + }, + toString() { + return `${this}`; + } +}, ni = as.localeCompare, oi = as.toString; +function si(t, e = "safe") { + if (e !== "safe" && e !== "unsafe") + throw _(`unrecognized localeTaming ${e}`); + if (e !== "unsafe") { + U(ye.prototype, "localeCompare", { + value: ni + }); + for (const r of It(t)) { + const n = t[r]; + if (Se(n)) + for (const o of It(n)) { + const a = Sn(ri, o); + if (a) { + typeof n[o] == "function" || ln`expected ${kr(o)} to be a function`; + const i = `${a[1]}${a[2]}`, c = n[i]; + typeof c == "function" || ln`function ${kr(i)} not found`, U(n, o, { value: c }); + } + } + } + U(wo.prototype, "toLocaleString", { + value: oi + }); + } +} +const ai = (t) => ({ + eval(r) { + return typeof r != "string" ? r : t(r); + } +}).eval, { Fail: Qn } = Y, ii = (t) => { + const e = function(n) { + const o = `${_r(arguments) || ""}`, a = `${Lt(arguments, ",")}`; + new xe(a, ""), new xe(o); + const i = `(function anonymous(${a} +) { +${o} +})`; + return t(i); + }; + return z(e, { + // Ensure that any function created in any evaluator in a realm is an + // instance of Function in any evaluator of the same realm. + prototype: { + value: xe.prototype, + writable: !1, + enumerable: !1, + configurable: !1 + } + }), G(xe) === xe.prototype || Qn`Function prototype is the same accross compartments`, G(e) === xe.prototype || Qn`Function constructor prototype is the same accross compartments`, e; +}, ci = (t) => { + U( + t, + Xs, + y( + Or(V(null), { + set: y(() => { + throw _( + "Cannot set Symbol.unscopables of a Compartment's globalThis" + ); + }), + enumerable: !1, + configurable: !1 + }) + ) + ); +}, is = (t) => { + for (const [e, r] of fe(Qo)) + U(t, e, { + value: r, + writable: !1, + enumerable: !1, + configurable: !1 + }); +}, cs = (t, { + intrinsics: e, + newGlobalPropertyNames: r, + makeCompartmentConstructor: n, + markVirtualizedNativeFunction: o, + parentCompartment: a +}) => { + for (const [c, l] of fe(es)) + ie(e, l) && U(t, c, { + value: e[l], + writable: !0, + enumerable: !1, + configurable: !0 + }); + for (const [c, l] of fe(r)) + ie(e, l) && U(t, c, { + value: e[l], + writable: !0, + enumerable: !1, + configurable: !0 + }); + const i = { + globalThis: t + }; + i.Compartment = y( + n( + n, + e, + o, + a + ) + ); + for (const [c, l] of fe(i)) + U(t, c, { + value: l, + writable: !0, + enumerable: !1, + configurable: !0 + }), typeof l == "function" && o(l); +}, un = (t, e, r) => { + { + const n = y(ai(e)); + r(n), U(t, "eval", { + value: n, + writable: !0, + enumerable: !1, + configurable: !0 + }); + } + { + const n = y(ii(e)); + r(n), U(t, "Function", { + value: n, + writable: !0, + enumerable: !1, + configurable: !0 + }); + } +}, { Fail: li, quote: ls } = Y, us = new Nr( + Pn, + y({ + get(t, e) { + li`Please report unexpected scope handler trap: ${ls(ye(e))}`; + } + }) +), ui = { + get(t, e) { + }, + set(t, e, r) { + throw zt(`${ye(e)} is not defined`); + }, + has(t, e) { + return e in P; + }, + // note: this is likely a bug of safari + // https://bugs.webkit.org/show_bug.cgi?id=195534 + getPrototypeOf(t) { + return null; + }, + // See https://github.com/endojs/endo/issues/1510 + // TODO: report as bug to v8 or Chrome, and record issue link here. + getOwnPropertyDescriptor(t, e) { + const r = ls(ye(e)); + console.warn( + `getOwnPropertyDescriptor trap on scopeTerminatorHandler for ${r}`, + _().stack + ); + }, + // See https://github.com/endojs/endo/issues/1490 + // TODO Report bug to JSC or Safari + ownKeys(t) { + return []; + } +}, ds = y( + V( + us, + je(ui) + ) +), di = new Nr( + Pn, + ds +), fs = (t) => { + const e = { + // inherit scopeTerminator behavior + ...ds, + // Redirect set properties to the globalObject. + set(o, a, i) { + return Io(t, a, i); + }, + // Always claim to have a potential property in order to be the recipient of a set + has(o, a) { + return !0; + } + }, r = y( + V( + us, + je(e) + ) + ); + return new Nr( + Pn, + r + ); +}; +y(fs); +const { Fail: fi } = Y, pi = () => { + const t = V(null), e = y({ + eval: { + get() { + return delete t.eval, Uo; + }, + enumerable: !1, + configurable: !0 + } + }), r = { + evalScope: t, + allowNextEvalToBeUnsafe() { + const { revoked: n } = r; + n !== null && fi`a handler did not reset allowNextEvalToBeUnsafe ${n.err}`, z(t, e); + }, + /** @type {null | { err: any }} */ + revoked: null + }; + return r; +}, eo = "\\s*[@#]\\s*([a-zA-Z][a-zA-Z0-9]*)\\s*=\\s*([^\\s\\*]*)", hi = new Je( + `(?:\\s*//${eo}|/\\*${eo}\\s*\\*/)\\s*$` +), In = (t) => { + let e = ""; + for (; t.length > 0; ) { + const r = Sn(hi, t); + if (r === null) + break; + t = En(t, 0, t.length - r[0].length), r[3] === "sourceURL" ? e = r[4] : r[1] === "sourceURL" && (e = r[2]); + } + return e; +}; +function An(t, e) { + const r = ya(t, e); + if (r < 0) + return -1; + const n = t[r] === ` +` ? 1 : 0; + return kn(En(t, 0, r), ` +`).length + n; +} +const ps = new Je("(?:)", "g"), hs = (t) => { + const e = An(t, ps); + if (e < 0) + return t; + const r = In(t); + throw or( + `Possible HTML comment rejected at ${r}:${e}. (SES_HTML_COMMENT_REJECTED)` + ); +}, ms = (t) => br(t, ps, (r) => r[0] === "<" ? "< ! --" : "-- >"), gs = new Je( + "(^|[^.]|\\.\\.\\.)\\bimport(\\s*(?:\\(|/[/*]))", + "g" +), ys = (t) => { + const e = An(t, gs); + if (e < 0) + return t; + const r = In(t); + throw or( + `Possible import expression rejected at ${r}:${e}. (SES_IMPORT_REJECTED)` + ); +}, _s = (t) => br(t, gs, (r, n, o) => `${n}__import__${o}`), mi = new Je( + "(^|[^.])\\beval(\\s*\\()", + "g" +), vs = (t) => { + const e = An(t, mi); + if (e < 0) + return t; + const r = In(t); + throw or( + `Possible direct eval expression rejected at ${r}:${e}. (SES_EVAL_REJECTED)` + ); +}, bs = (t) => (t = hs(t), t = ys(t), t), ws = (t, e) => { + for (const r of e) + t = r(t); + return t; +}; +y({ + rejectHtmlComments: y(hs), + evadeHtmlCommentTest: y(ms), + rejectImportExpressions: y(ys), + evadeImportExpressionTest: y(_s), + rejectSomeDirectEvalExpressions: y(vs), + mandatoryTransforms: y(bs), + applyTransforms: y(ws) +}); +const gi = [ + // 11.6.2.1 Keywords + "await", + "break", + "case", + "catch", + "class", + "const", + "continue", + "debugger", + "default", + "delete", + "do", + "else", + "export", + "extends", + "finally", + "for", + "function", + "if", + "import", + "in", + "instanceof", + "new", + "return", + "super", + "switch", + "this", + "throw", + "try", + "typeof", + "var", + "void", + "while", + "with", + "yield", + // Also reserved when parsing strict mode code + "let", + "static", + // 11.6.2.2 Future Reserved Words + "enum", + // Also reserved when parsing strict mode code + "implements", + "package", + "protected", + "interface", + "private", + "public", + // Reserved but not mentioned in specs + "await", + "null", + "true", + "false", + "this", + "arguments" +], yi = /^[a-zA-Z_$][\w$]*$/, to = (t) => t !== "eval" && !Dr(gi, t) && xn(yi, t); +function ro(t, e) { + const r = Q(t, e); + return r && // + // The getters will not have .writable, don't let the falsyness of + // 'undefined' trick us: test with === false, not ! . However descriptors + // inherit from the (potentially poisoned) global object, so we might see + // extra properties which weren't really there. Accessor properties have + // 'get/set/enumerable/configurable', while data properties have + // 'value/writable/enumerable/configurable'. + r.configurable === !1 && r.writable === !1 && // + // Checks for data properties because they're the only ones we can + // optimize (accessors are most likely non-constant). Descriptors can't + // can't have accessors and value properties at the same time, therefore + // this check is sufficient. Using explicit own property deal with the + // case where Object.prototype has been poisoned. + ie(r, "value"); +} +const _i = (t, e = {}) => { + const r = It(t), n = It(e), o = Qe( + n, + (i) => to(i) && ro(e, i) + ); + return { + globalObjectConstants: Qe( + r, + (i) => ( + // Can't define a constant: it would prevent a + // lookup on the endowments. + !Dr(n, i) && to(i) && ro(t, i) + ) + ), + moduleLexicalConstants: o + }; +}; +function no(t, e) { + return t.length === 0 ? "" : `const {${Lt(t, ",")}} = this.${e};`; +} +const vi = (t) => { + const { globalObjectConstants: e, moduleLexicalConstants: r } = _i( + t.globalObject, + t.moduleLexicals + ), n = no( + e, + "globalObject" + ), o = no( + r, + "moduleLexicals" + ), a = xe(` + with (this.scopeTerminator) { + with (this.globalObject) { + with (this.moduleLexicals) { + with (this.evalScope) { + ${n} + ${o} + return function() { + 'use strict'; + return eval(arguments[0]); + }; + } + } + } + } + `); + return ae(a, t, []); +}, { Fail: bi } = Y, Cn = ({ + globalObject: t, + moduleLexicals: e = {}, + globalTransforms: r = [], + sloppyGlobalsMode: n = !1 +}) => { + const o = n ? fs(t) : di, a = pi(), { evalScope: i } = a, c = y({ + evalScope: i, + moduleLexicals: e, + globalObject: t, + scopeTerminator: o + }); + let l; + const u = () => { + l || (l = vi(c)); + }; + return { safeEvaluate: (f, h) => { + const { localTransforms: p = [] } = h || {}; + u(), f = ws(f, [ + ...p, + ...r, + bs + ]); + let m; + try { + return a.allowNextEvalToBeUnsafe(), ae(l, t, [f]); + } catch (b) { + throw m = b, b; + } finally { + const b = "eval" in i; + delete i.eval, b && (a.revoked = { err: m }, bi`handler did not reset allowNextEvalToBeUnsafe ${m}`); + } + } }; +}, wi = ") { [native code] }"; +let en; +const xs = () => { + if (en === void 0) { + const t = new Ot(); + U(vn, "toString", { + value: { + toString() { + const r = ba(this); + return Oo(r, wi) || !ir(t, this) ? r : `function ${this.name}() { [native code] }`; + } + }.toString + }), en = y( + (r) => jr(t, r) + ); + } + return en; +}; +function xi(t = "safe") { + if (t !== "safe" && t !== "unsafe") + throw _(`unrecognized domainTaming ${t}`); + if (t === "unsafe") + return; + const e = P.process || void 0; + if (typeof e == "object") { + const r = Q(e, "domain"); + if (r !== void 0 && r.get !== void 0) + throw _( + "SES failed to lockdown, Node.js domains have been initialized (SES_NO_DOMAINS)" + ); + U(e, "domain", { + value: null, + configurable: !1, + writable: !1, + enumerable: !1 + }); + } +} +const $n = y([ + ["debug", "debug"], + // (fmt?, ...args) verbose level on Chrome + ["log", "log"], + // (fmt?, ...args) info level on Chrome + ["info", "info"], + // (fmt?, ...args) + ["warn", "warn"], + // (fmt?, ...args) + ["error", "error"], + // (fmt?, ...args) + ["trace", "log"], + // (fmt?, ...args) + ["dirxml", "log"], + // (fmt?, ...args) but TS typed (...data) + ["group", "log"], + // (fmt?, ...args) but TS typed (...label) + ["groupCollapsed", "log"] + // (fmt?, ...args) but TS typed (...label) +]), Rn = y([ + ["assert", "error"], + // (value, fmt?, ...args) + ["timeLog", "log"], + // (label?, ...args) no fmt string + // Insensitive to whether any argument is an error. All arguments can pass + // thru to baseConsole as is. + ["clear", void 0], + // () + ["count", "info"], + // (label?) + ["countReset", void 0], + // (label?) + ["dir", "log"], + // (item, options?) + ["groupEnd", "log"], + // () + // In theory tabular data may be or contain an error. However, we currently + // do not detect these and may never. + ["table", "log"], + // (tabularData, properties?) + ["time", "info"], + // (label?) + ["timeEnd", "info"], + // (label?) + // Node Inspector only, MDN, and TypeScript, but not whatwg + ["profile", void 0], + // (label?) + ["profileEnd", void 0], + // (label?) + ["timeStamp", void 0] + // (label?) +]), Ss = y([ + ...$n, + ...Rn +]), Si = (t, { shouldResetForDebugging: e = !1 } = {}) => { + e && t.resetErrorTagNum(); + let r = []; + const n = mt( + ce(Ss, ([i, c]) => { + const l = (...u) => { + ee(r, [i, ...u]); + }; + return U(l, "name", { value: i }), [i, y(l)]; + }) + ); + y(n); + const o = () => { + const i = y(r); + return r = [], i; + }; + return y(o), y({ loggingConsole: ( + /** @type {VirtualConsole} */ + n + ), takeLog: o }); +}; +y(Si); +const lt = { + NOTE: "ERROR_NOTE:", + MESSAGE: "ERROR_MESSAGE:", + CAUSE: "cause:", + ERRORS: "errors:" +}; +y(lt); +const Nn = (t, e) => { + if (!t) + return; + const { getStackString: r, tagError: n, takeMessageLogArgs: o, takeNoteLogArgsArray: a } = e, i = (S, x) => ce(S, (E) => Zr(E) ? (ee(x, E), `(${n(E)})`) : E), c = (S, x, I, E, A) => { + const N = n(x), D = I === lt.MESSAGE ? `${N}:` : `${N} ${I}`, M = i(E, A); + t[S](D, ...M); + }, l = (S, x, I = void 0) => { + if (x.length === 0) + return; + if (x.length === 1 && I === void 0) { + f(S, x[0]); + return; + } + let E; + x.length === 1 ? E = "Nested error" : E = `Nested ${x.length} errors`, I !== void 0 && (E = `${E} under ${I}`), t.group(E); + try { + for (const A of x) + f(S, A); + } finally { + t.groupEnd(); + } + }, u = new Ot(), d = (S) => (x, I) => { + const E = []; + c(S, x, lt.NOTE, I, E), l(S, E, n(x)); + }, f = (S, x) => { + if (ir(u, x)) + return; + const I = n(x); + jr(u, x); + const E = [], A = o(x), N = a( + x, + d(S) + ); + A === void 0 ? t[S](`${I}:`, x.message) : c( + S, + x, + lt.MESSAGE, + A, + E + ); + let D = r(x); + typeof D == "string" && D.length >= 1 && !Oo(D, ` +`) && (D += ` +`), t[S](D), x.cause && c(S, x, lt.CAUSE, [x.cause], E), x.errors && c(S, x, lt.ERRORS, x.errors, E); + for (const M of N) + c(S, x, lt.NOTE, M, E); + l(S, E, I); + }, h = ce($n, ([S, x]) => { + const I = (...E) => { + const A = [], N = i(E, A); + t[S](...N), l(S, A); + }; + return U(I, "name", { value: S }), [S, y(I)]; + }), p = Qe( + Rn, + ([S, x]) => S in t + ), m = ce(p, ([S, x]) => { + const I = (...E) => { + t[S](...E); + }; + return U(I, "name", { value: S }), [S, y(I)]; + }), b = mt([...h, ...m]); + return ( + /** @type {VirtualConsole} */ + y(b) + ); +}; +y(Nn); +const Ei = (t, e, r) => { + const [n, ...o] = kn(t, e), a = Ro(o, (i) => [e, ...r, i]); + return ["", n, ...a]; +}, Es = (t) => y((r) => { + const n = [], o = (...l) => (n.length > 0 && (l = Ro( + l, + (u) => typeof u == "string" && Mo(u, ` +`) ? Ei(u, ` +`, n) : [u] + ), l = [...n, ...l]), r(...l)), a = (l, u) => ({ [l]: (...d) => u(...d) })[l], i = mt([ + ...ce($n, ([l]) => [ + l, + a(l, o) + ]), + ...ce(Rn, ([l]) => [ + l, + a(l, (...u) => o(l, ...u)) + ]) + ]); + for (const l of ["group", "groupCollapsed"]) + i[l] && (i[l] = a(l, (...u) => { + u.length >= 1 && o(...u), ee(n, " "); + })); + return i.groupEnd && (i.groupEnd = a("groupEnd", (...l) => { + _r(n); + })), harden(i), Nn( + /** @type {VirtualConsole} */ + i, + t + ); +}); +y(Es); +const ki = (t, e, r = void 0) => { + const n = Qe( + Ss, + ([i, c]) => i in t + ), o = ce(n, ([i, c]) => [i, y((...u) => { + (c === void 0 || e.canLog(c)) && t[i](...u); + })]), a = mt(o); + return ( + /** @type {VirtualConsole} */ + y(a) + ); +}; +y(ki); +const oo = (t) => { + if (Tt === void 0) + return; + let e = 0; + const r = new Ce(), n = (d) => { + da(r, d); + }, o = new Ue(), a = (d) => { + if (Ur(r, d)) { + const f = Ge(r, d); + n(d), t(f); + } + }, i = new Tt(a); + return { + rejectionHandledHandler: (d) => { + const f = Z(o, d); + n(f); + }, + unhandledRejectionHandler: (d, f) => { + e += 1; + const h = e; + ue(r, h, d), de(o, f, h), xa(i, f, h, f); + }, + processTerminationHandler: () => { + for (const [d, f] of fa(r)) + n(d), t(f); + } + }; +}, tn = (t) => { + throw _(t); +}, so = (t, e) => y((...r) => ae(t, e, r)), Pi = (t = "safe", e = "platform", r = "report", n = void 0) => { + t === "safe" || t === "unsafe" || tn(`unrecognized consoleTaming ${t}`); + let o; + n === void 0 ? o = Sr : o = { + ...Sr, + getStackString: n + }; + const a = ( + /** @type {VirtualConsole} */ + // eslint-disable-next-line no-nested-ternary + typeof P.console < "u" ? P.console : typeof P.print == "function" ? ( + // Make a good-enough console for eshost (including only functions that + // log at a specific level with no special argument interpretation). + // https://console.spec.whatwg.org/#logging + ((u) => y({ debug: u, log: u, info: u, warn: u, error: u }))( + // eslint-disable-next-line no-undef + so(P.print) + ) + ) : void 0 + ); + if (a && a.log) + for (const u of ["warn", "error"]) + a[u] || U(a, u, { + value: so(a.log, a) + }); + const i = ( + /** @type {VirtualConsole} */ + t === "unsafe" ? a : Nn(a, o) + ), c = P.process || void 0; + if (e !== "none" && typeof c == "object" && typeof c.on == "function") { + let u; + if (e === "platform" || e === "exit") { + const { exit: d } = c; + typeof d == "function" || tn("missing process.exit"), u = () => d(c.exitCode || -1); + } else e === "abort" && (u = c.abort, typeof u == "function" || tn("missing process.abort")); + c.on("uncaughtException", (d) => { + i.error(d), u && u(); + }); + } + if (r !== "none" && typeof c == "object" && typeof c.on == "function") { + const d = oo((f) => { + i.error("SES_UNHANDLED_REJECTION:", f); + }); + d && (c.on("unhandledRejection", d.unhandledRejectionHandler), c.on("rejectionHandled", d.rejectionHandledHandler), c.on("exit", d.processTerminationHandler)); + } + const l = P.window || void 0; + if (e !== "none" && typeof l == "object" && typeof l.addEventListener == "function" && l.addEventListener("error", (u) => { + u.preventDefault(), i.error(u.error), (e === "exit" || e === "abort") && (l.location.href = "about:blank"); + }), r !== "none" && typeof l == "object" && typeof l.addEventListener == "function") { + const d = oo((f) => { + i.error("SES_UNHANDLED_REJECTION:", f); + }); + d && (l.addEventListener("unhandledrejection", (f) => { + f.preventDefault(), d.unhandledRejectionHandler(f.reason, f.promise); + }), l.addEventListener("rejectionhandled", (f) => { + f.preventDefault(), d.rejectionHandledHandler(f.promise); + }), l.addEventListener("beforeunload", (f) => { + d.processTerminationHandler(); + })); + } + return { console: i }; +}, Ti = [ + // suppress 'getThis' definitely + "getTypeName", + // suppress 'getFunction' definitely + "getFunctionName", + "getMethodName", + "getFileName", + "getLineNumber", + "getColumnNumber", + "getEvalOrigin", + "isToplevel", + "isEval", + "isNative", + "isConstructor", + "isAsync", + // suppress 'isPromiseAll' for now + // suppress 'getPromiseIndex' for now + // Additional names found by experiment, absent from + // https://v8.dev/docs/stack-trace-api + "getPosition", + "getScriptNameOrSourceURL", + "toString" + // TODO replace to use only whitelisted info +], Ii = (t) => { + const r = mt(ce(Ti, (n) => { + const o = t[n]; + return [n, () => ae(o, t, [])]; + })); + return V(r, {}); +}, Ai = (t) => ce(t, Ii), Ci = /\/node_modules\//, $i = /^(?:node:)?internal\//, Ri = /\/packages\/ses\/src\/error\/assert.js$/, Ni = /\/packages\/eventual-send\/src\//, Oi = [ + Ci, + $i, + Ri, + Ni +], Mi = (t) => { + if (!t) + return !0; + for (const e of Oi) + if (xn(e, t)) + return !1; + return !0; +}, Li = /^((?:.*[( ])?)[:/\w_-]*\/\.\.\.\/(.+)$/, Fi = /^((?:.*[( ])?)[:/\w_-]*\/(packages\/.+)$/, Di = [ + Li, + Fi +], Ui = (t) => { + for (const e of Di) { + const r = Sn(e, t); + if (r) + return Lt(ca(r, 1), ""); + } + return t; +}, ji = (t, e, r, n) => { + if (r === "unsafe-debug") + throw _( + "internal: v8+unsafe-debug special case should already be done" + ); + const o = t.captureStackTrace, a = (p) => n === "verbose" ? !0 : Mi(p.getFileName()), i = (p) => { + let m = `${p}`; + return n === "concise" && (m = Ui(m)), ` + at ${m}`; + }, c = (p, m) => Lt( + ce(Qe(m, a), i), + "" + ), l = new Ue(), u = { + // The optional `optFn` argument is for cutting off the bottom of + // the stack --- for capturing the stack only above the topmost + // call to that function. Since this isn't the "real" captureStackTrace + // but instead calls the real one, if no other cutoff is provided, + // we cut this one off. + captureStackTrace(p, m = u.captureStackTrace) { + if (typeof o == "function") { + ae(o, t, [p, m]); + return; + } + Io(p, "stack", ""); + }, + // Shim of proposed special power, to reside by default only + // in the start compartment, for getting the stack traceback + // string associated with an error. + // See https://tc39.es/proposal-error-stacks/ + getStackString(p) { + let m = Z(l, p); + if (m === void 0 && (p.stack, m = Z(l, p), m || (m = { stackString: "" }, de(l, p, m))), m.stackString !== void 0) + return m.stackString; + const b = c(p, m.callSites); + return de(l, p, { stackString: b }), b; + }, + prepareStackTrace(p, m) { + if (r === "unsafe") { + const b = c(p, m); + return de(l, p, { stackString: b }), `${p}${b}`; + } else + return de(l, p, { callSites: m }), ""; + } + }, d = u.prepareStackTrace; + t.prepareStackTrace = d; + const f = new Ot([d]), h = (p) => { + if (ir(f, p)) + return p; + const m = { + prepareStackTrace(b, S) { + return de(l, b, { callSites: S }), p(b, Ai(S)); + } + }; + return jr(f, m.prepareStackTrace), m.prepareStackTrace; + }; + return z(e, { + captureStackTrace: { + value: u.captureStackTrace, + writable: !0, + enumerable: !1, + configurable: !0 + }, + prepareStackTrace: { + get() { + return t.prepareStackTrace; + }, + set(p) { + if (typeof p == "function") { + const m = h(p); + t.prepareStackTrace = m; + } else + t.prepareStackTrace = d; + }, + enumerable: !1, + configurable: !0 + } + }), u.getStackString; +}, ao = Q(oe.prototype, "stack"), io = ao && ao.get, Zi = { + getStackString(t) { + return typeof io == "function" ? ae(io, t, []) : "stack" in t ? `${t.stack}` : ""; + } +}; +let dr = Zi.getStackString; +function zi(t = "safe", e = "concise") { + if (t !== "safe" && t !== "unsafe" && t !== "unsafe-debug") + throw _(`unrecognized errorTaming ${t}`); + if (e !== "concise" && e !== "verbose") + throw _(`unrecognized stackFiltering ${e}`); + const r = oe.prototype, { captureStackTrace: n } = oe, o = typeof n == "function" ? "v8" : "unknown", a = (l = {}) => { + const u = function(...f) { + let h; + return new.target === void 0 ? h = ae(oe, this, f) : h = yr(oe, f, new.target), o === "v8" && ae(n, oe, [h, u]), h; + }; + return z(u, { + length: { value: 1 }, + prototype: { + value: r, + writable: !1, + enumerable: !1, + configurable: !1 + } + }), u; + }, i = a({ powers: "original" }), c = a({ powers: "none" }); + z(r, { + constructor: { value: c } + }); + for (const l of rs) + So(l, c); + if (z(i, { + stackTraceLimit: { + get() { + if (typeof oe.stackTraceLimit == "number") + return oe.stackTraceLimit; + }, + set(l) { + if (typeof l == "number" && typeof oe.stackTraceLimit == "number") { + oe.stackTraceLimit = l; + return; + } + }, + // WTF on v8 stackTraceLimit is enumerable + enumerable: !1, + configurable: !0 + } + }), t === "unsafe-debug" && o === "v8") { + z(i, { + prepareStackTrace: { + get() { + return oe.prepareStackTrace; + }, + set(u) { + oe.prepareStackTrace = u; + }, + enumerable: !1, + configurable: !0 + }, + captureStackTrace: { + value: oe.captureStackTrace, + writable: !0, + enumerable: !1, + configurable: !0 + } + }); + const l = je(i); + return z(c, { + stackTraceLimit: l.stackTraceLimit, + prepareStackTrace: l.prepareStackTrace, + captureStackTrace: l.captureStackTrace + }), { + "%InitialGetStackString%": dr, + "%InitialError%": i, + "%SharedError%": c + }; + } + return z(c, { + stackTraceLimit: { + get() { + }, + set(l) { + }, + enumerable: !1, + configurable: !0 + } + }), o === "v8" && z(c, { + prepareStackTrace: { + get() { + return () => ""; + }, + set(l) { + }, + enumerable: !1, + configurable: !0 + }, + captureStackTrace: { + value: (l, u) => { + U(l, "stack", { + value: "" + }); + }, + writable: !1, + enumerable: !1, + configurable: !0 + } + }), o === "v8" ? dr = ji( + oe, + i, + t, + e + ) : t === "unsafe" || t === "unsafe-debug" ? z(r, { + stack: { + get() { + return dr(this); + }, + set(l) { + z(this, { + stack: { + value: l, + writable: !0, + enumerable: !0, + configurable: !0 + } + }); + } + } + }) : z(r, { + stack: { + get() { + return `${this}`; + }, + set(l) { + z(this, { + stack: { + value: l, + writable: !0, + enumerable: !0, + configurable: !0 + } + }); + } + } + }), { + "%InitialGetStackString%": dr, + "%InitialError%": i, + "%SharedError%": c + }; +} +const Bi = () => { +}, Gi = async (t, e, r) => { + await null; + const n = t(...e); + let o = vr(n); + for (; !o.done; ) + try { + const a = await o.value; + o = vr(n, a); + } catch (a) { + o = Lo(n, r(a)); + } + return o.value; +}, Vi = (t, e) => { + const r = t(...e); + let n = vr(r); + for (; !n.done; ) + try { + n = vr(r, n.value); + } catch (o) { + n = Lo(r, o); + } + return n.value; +}, Hi = (t, e) => y({ compartment: t, specifier: e }), Wi = (t, e, r) => { + const n = V(null); + for (const o of t) { + const a = e(o, r); + n[o] = a; + } + return y(n); +}, Dt = (t, e, r, n, o, a, i, c, l) => { + const { resolveHook: u } = Z(t, r), d = Wi( + o.imports, + u, + n + ), f = y({ + compartment: r, + moduleSource: o, + moduleSpecifier: n, + resolvedImports: d, + importMeta: l + }); + for (const h of Eo(d)) + a(Et, [ + t, + e, + r, + h, + a, + i, + c + ]); + return f; +}; +function* qi(t, e, r, n, o, a, i) { + const { + importHook: c, + importNowHook: l, + moduleMap: u, + moduleMapHook: d, + moduleRecords: f, + parentCompartment: h + } = Z(t, r); + if (Ur(f, n)) + return Ge(f, n); + let p = u[n]; + if (p === void 0 && d !== void 0 && (p = d(n)), p === void 0) { + const m = a(c, l); + if (m === void 0) { + const b = a( + "importHook", + "importNowHook" + ); + throw Me( + se`${xr(b)} needed to load module ${j( + n + )} in compartment ${j(r.name)}` + ); + } + p = m(n), St(e, p) || (p = yield p); + } + if (typeof p == "string") + throw Me( + se`Cannot map module ${j(n)} to ${j( + p + )} in parent compartment, use {source} module descriptor`, + _ + ); + if (Se(p)) { + let m = Z(e, p); + if (m !== void 0 && (p = m), p.namespace !== void 0) { + if (typeof p.namespace == "string") { + const { + compartment: x = h, + namespace: I + } = p; + if (!Se(x) || !St(t, x)) + throw Me( + se`Invalid compartment in module descriptor for specifier ${j(n)} in compartment ${j(r.name)}` + ); + const E = yield Et( + t, + e, + x, + I, + o, + a, + i + ); + return ue(f, n, E), E; + } + if (Se(p.namespace)) { + const { namespace: x } = p; + if (m = Z(e, x), m !== void 0) + p = m; + else { + const I = It(x), N = Dt( + t, + e, + r, + n, + { + imports: [], + exports: I, + execute(D) { + for (const M of I) + D[M] = x[M]; + } + }, + o, + a, + i, + void 0 + ); + return ue(f, n, N), N; + } + } else + throw Me( + se`Invalid compartment in module descriptor for specifier ${j(n)} in compartment ${j(r.name)}` + ); + } + if (p.source !== void 0) + if (typeof p.source == "string") { + const { + source: x, + specifier: I = n, + compartment: E = h, + importMeta: A = void 0 + } = p, N = yield Et( + t, + e, + E, + x, + o, + a, + i + ), { moduleSource: D } = N, M = Dt( + t, + e, + r, + I, + D, + o, + a, + i, + A + ); + return ue(f, n, M), M; + } else { + const { + source: x, + specifier: I = n, + importMeta: E + } = p, A = Dt( + t, + e, + r, + I, + x, + o, + a, + i, + E + ); + return ue(f, n, A), A; + } + if (p.archive !== void 0) + throw Me( + se`Unsupported archive module descriptor for specifier ${j(n)} in compartment ${j(r.name)}` + ); + if (p.record !== void 0) { + const { + compartment: x = r, + specifier: I = n, + record: E, + importMeta: A + } = p, N = Dt( + t, + e, + x, + I, + E, + o, + a, + i, + A + ); + return ue(f, n, N), ue(f, I, N), N; + } + if (p.compartment !== void 0 && p.specifier !== void 0) { + if (!Se(p.compartment) || !St(t, p.compartment) || typeof p.specifier != "string") + throw Me( + se`Invalid compartment in module descriptor for specifier ${j(n)} in compartment ${j(r.name)}` + ); + const x = yield Et( + t, + e, + p.compartment, + p.specifier, + o, + a, + i + ); + return ue(f, n, x), x; + } + const S = Dt( + t, + e, + r, + n, + p, + o, + a, + i + ); + return ue(f, n, S), S; + } else + throw Me( + se`module descriptor must be a string or object for specifier ${j( + n + )} in compartment ${j(r.name)}` + ); +} +const Et = (t, e, r, n, o, a, i) => { + const { name: c } = Z( + t, + r + ); + let l = Ge(i, r); + l === void 0 && (l = new Ce(), ue(i, r, l)); + let u = Ge(l, n); + return u !== void 0 || (u = a(Gi, Vi)( + qi, + [ + t, + e, + r, + n, + o, + a, + i + ], + (d) => { + throw Br( + d, + se`${d.message}, loading ${j(n)} in compartment ${j( + c + )}` + ), d; + } + ), ue(l, n, u)), u; +}, Ki = () => { + const t = new Nt(), e = []; + return { enqueueJob: (o, a) => { + bn( + t, + Do(o(...a), Bi, (i) => { + ee(e, i); + }) + ); + }, drainQueue: async () => { + await null; + for (const o of t) + await o; + return e; + } }; +}, ks = ({ errors: t, errorPrefix: e }) => { + if (t.length > 0) { + const r = he("COMPARTMENT_LOAD_ERRORS", "", ["verbose"]) === "verbose"; + throw _( + `${e} (${t.length} underlying failures: ${Lt( + ce(t, (n) => n.message + (r ? n.stack : "")), + ", " + )}` + ); + } +}, Yi = (t, e) => e, Ji = (t, e) => t, co = async (t, e, r, n) => { + const { name: o } = Z( + t, + r + ), a = new Ce(), { enqueueJob: i, drainQueue: c } = Ki(); + i(Et, [ + t, + e, + r, + n, + i, + Ji, + a + ]); + const l = await c(); + ks({ + errors: l, + errorPrefix: `Failed to load module ${j(n)} in package ${j( + o + )}` + }); +}, Xi = (t, e, r, n) => { + const { name: o } = Z( + t, + r + ), a = new Ce(), i = [], c = (l, u) => { + try { + l(...u); + } catch (d) { + ee(i, d); + } + }; + c(Et, [ + t, + e, + r, + n, + c, + Yi, + a + ]), ks({ + errors: i, + errorPrefix: `Failed to load module ${j(n)} in package ${j( + o + )}` + }); +}, { quote: yt } = Y, Qi = () => { + let t = !1; + const e = V(null, { + // Make this appear like an ESM module namespace object. + [Xe]: { + value: "Module", + writable: !1, + enumerable: !1, + configurable: !1 + } + }); + return y({ + activate() { + t = !0; + }, + exportsTarget: e, + exportsProxy: new Nr(e, { + get(r, n, o) { + if (!t) + throw _( + `Cannot get property ${yt( + n + )} of module exports namespace, the module has not yet begun to execute` + ); + return na(e, n, o); + }, + set(r, n, o) { + throw _( + `Cannot set property ${yt(n)} of module exports namespace` + ); + }, + has(r, n) { + if (!t) + throw _( + `Cannot check property ${yt( + n + )}, the module has not yet begun to execute` + ); + return To(e, n); + }, + deleteProperty(r, n) { + throw _( + `Cannot delete property ${yt(n)}s of module exports namespace` + ); + }, + ownKeys(r) { + if (!t) + throw _( + "Cannot enumerate keys, the module has not yet begun to execute" + ); + return Be(e); + }, + getOwnPropertyDescriptor(r, n) { + if (!t) + throw _( + `Cannot get own property descriptor ${yt( + n + )}, the module has not yet begun to execute` + ); + return oa(e, n); + }, + preventExtensions(r) { + if (!t) + throw _( + "Cannot prevent extensions of module exports namespace, the module has not yet begun to execute" + ); + return aa(e); + }, + isExtensible() { + if (!t) + throw _( + "Cannot check extensibility of module exports namespace, the module has not yet begun to execute" + ); + return sa(e); + }, + getPrototypeOf(r) { + return null; + }, + setPrototypeOf(r, n) { + throw _("Cannot set prototype of module exports namespace"); + }, + defineProperty(r, n, o) { + throw _( + `Cannot define property ${yt(n)} of module exports namespace` + ); + }, + apply(r, n, o) { + throw _( + "Cannot call module exports namespace, it is not a function" + ); + }, + construct(r, n) { + throw _( + "Cannot construct module exports namespace, it is not a constructor" + ); + } + }) + }); +}, On = (t, e, r, n) => { + const { deferredExports: o } = e; + if (!Ur(o, n)) { + const a = Qi(); + de( + r, + a.exportsProxy, + Hi(t, n) + ), ue(o, n, a); + } + return Ge(o, n); +}, ec = (t, e) => { + const { sloppyGlobalsMode: r = !1, __moduleShimLexicals__: n = void 0 } = e; + let o; + if (n === void 0 && !r) + ({ safeEvaluate: o } = t); + else { + let { globalTransforms: a } = t; + const { globalObject: i } = t; + let c; + n !== void 0 && (a = void 0, c = V( + null, + je(n) + )), { safeEvaluate: o } = Cn({ + globalObject: i, + moduleLexicals: c, + globalTransforms: a, + sloppyGlobalsMode: r + }); + } + return { safeEvaluate: o }; +}, Ps = (t, e, r) => { + if (typeof e != "string") + throw _("first argument of evaluate() must be a string"); + const { + transforms: n = [], + __evadeHtmlCommentTest__: o = !1, + __evadeImportExpressionTest__: a = !1, + __rejectSomeDirectEvalExpressions__: i = !0 + // Note default on + } = r, c = [...n]; + o === !0 && ee(c, ms), a === !0 && ee(c, _s), i === !0 && ee(c, vs); + const { safeEvaluate: l } = ec( + t, + r + ); + return l(e, { + localTransforms: c + }); +}, { quote: fr } = Y, tc = (t, e, r, n, o, a) => { + const { exportsProxy: i, exportsTarget: c, activate: l } = On( + r, + Z(t, r), + n, + o + ), u = V(null); + if (e.exports) { + if (!xt(e.exports) || la(e.exports, (f) => typeof f != "string")) + throw _( + `SES virtual module source "exports" property must be an array of strings for module ${o}` + ); + dt(e.exports, (f) => { + let h = c[f]; + const p = []; + U(c, f, { + get: () => h, + set: (S) => { + h = S; + for (const x of p) + x(S); + }, + enumerable: !0, + configurable: !1 + }), u[f] = (S) => { + ee(p, S), S(h); + }; + }), u["*"] = (f) => { + f(c); + }; + } + const d = { + activated: !1 + }; + return y({ + notifiers: u, + exportsProxy: i, + execute() { + if (To(d, "errorFromExecute")) + throw d.errorFromExecute; + if (!d.activated) { + l(), d.activated = !0; + try { + e.execute(c, r, a); + } catch (f) { + throw d.errorFromExecute = f, f; + } + } + } + }); +}, rc = (t, e, r, n) => { + const { + compartment: o, + moduleSpecifier: a, + moduleSource: i, + importMeta: c + } = r, { + reexports: l = [], + __syncModuleProgram__: u, + __fixedExportMap__: d = {}, + __liveExportMap__: f = {}, + __reexportMap__: h = {}, + __needsImportMeta__: p = !1, + __syncModuleFunctor__: m + } = i, b = Z(t, o), { __shimTransforms__: S, importMetaHook: x } = b, { exportsProxy: I, exportsTarget: E, activate: A } = On( + o, + b, + e, + a + ), N = V(null), D = V(null), M = V(null), J = V(null), _e = V(null); + c && Or(_e, c), p && x && x(a, _e); + const He = V(null), ot = V(null); + dt(fe(d), ([ve, [H]]) => { + let W = He[H]; + if (!W) { + let re, ne = !0, pe = []; + const X = () => { + if (ne) + throw zt(`binding ${fr(H)} not yet initialized`); + return re; + }, ke = y((Pe) => { + if (!ne) + throw _( + `Internal: binding ${fr(H)} already initialized` + ); + re = Pe; + const jn = pe; + pe = null, ne = !1; + for (const Te of jn || []) + Te(Pe); + return Pe; + }); + W = { + get: X, + notify: (Pe) => { + Pe !== ke && (ne ? ee(pe || [], Pe) : Pe(re)); + } + }, He[H] = W, M[H] = ke; + } + N[ve] = { + get: W.get, + set: void 0, + enumerable: !0, + configurable: !1 + }, ot[ve] = W.notify; + }), dt( + fe(f), + ([ve, [H, W]]) => { + let re = He[H]; + if (!re) { + let ne, pe = !0; + const X = [], ke = () => { + if (pe) + throw zt( + `binding ${fr(ve)} not yet initialized` + ); + return ne; + }, gt = y((Te) => { + ne = Te, pe = !1; + for (const Hr of X) + Hr(Te); + }), Pe = (Te) => { + if (pe) + throw zt(`binding ${fr(H)} not yet initialized`); + ne = Te; + for (const Hr of X) + Hr(Te); + }; + re = { + get: ke, + notify: (Te) => { + Te !== gt && (ee(X, Te), pe || Te(ne)); + } + }, He[H] = re, W && U(D, H, { + get: ke, + set: Pe, + enumerable: !0, + configurable: !1 + }), J[H] = gt; + } + N[ve] = { + get: re.get, + set: void 0, + enumerable: !0, + configurable: !1 + }, ot[ve] = re.notify; + } + ); + const We = (ve) => { + ve(E); + }; + ot["*"] = We; + function lr(ve) { + const H = V(null); + H.default = !1; + for (const [W, re] of ve) { + const ne = Ge(n, W); + ne.execute(); + const { notifiers: pe } = ne; + for (const [X, ke] of re) { + const gt = pe[X]; + if (!gt) + throw or( + `The requested module '${W}' does not provide an export named '${X}'` + ); + for (const Pe of ke) + gt(Pe); + } + if (Dr(l, W)) + for (const [X, ke] of fe( + pe + )) + H[X] === void 0 ? H[X] = ke : H[X] = !1; + if (h[W]) + for (const [X, ke] of h[W]) + H[ke] = pe[X]; + } + for (const [W, re] of fe(H)) + if (!ot[W] && re !== !1) { + ot[W] = re; + let ne; + re((X) => ne = X), N[W] = { + get() { + return ne; + }, + set: void 0, + enumerable: !0, + configurable: !1 + }; + } + dt( + No(xo(N)), + (W) => U(E, W, N[W]) + ), y(E), A(); + } + let Ft; + m !== void 0 ? Ft = m : Ft = Ps(b, u, { + globalObject: o.globalThis, + transforms: S, + __moduleShimLexicals__: D + }); + let Dn = !1, Un; + function zs() { + if (Ft) { + const ve = Ft; + Ft = null; + try { + ve( + y({ + imports: y(lr), + onceVar: y(M), + liveVar: y(J), + importMeta: _e + }) + ); + } catch (H) { + Dn = !0, Un = H; + } + } + if (Dn) + throw Un; + } + return y({ + notifiers: ot, + exportsProxy: I, + execute: zs + }); +}, { Fail: ut, quote: K } = Y, Ts = (t, e, r, n) => { + const { name: o, moduleRecords: a } = Z( + t, + r + ), i = Ge(a, n); + if (i === void 0) + throw zt( + `Missing link to module ${K(n)} from compartment ${K( + o + )}` + ); + return cc(t, e, i); +}; +function nc(t) { + return typeof t.__syncModuleProgram__ == "string"; +} +function oc(t, e) { + const { __fixedExportMap__: r, __liveExportMap__: n } = t; + Se(r) || ut`Property '__fixedExportMap__' of a precompiled module source must be an object, got ${K( + r + )}, for module ${K(e)}`, Se(n) || ut`Property '__liveExportMap__' of a precompiled module source must be an object, got ${K( + n + )}, for module ${K(e)}`; +} +function sc(t) { + return typeof t.execute == "function"; +} +function ac(t, e) { + const { exports: r } = t; + xt(r) || ut`Property 'exports' of a third-party module source must be an array, got ${K( + r + )}, for module ${K(e)}`; +} +function ic(t, e) { + Se(t) || ut`Module sources must be of type object, got ${K( + t + )}, for module ${K(e)}`; + const { imports: r, exports: n, reexports: o = [] } = t; + xt(r) || ut`Property 'imports' of a module source must be an array, got ${K( + r + )}, for module ${K(e)}`, xt(n) || ut`Property 'exports' of a precompiled module source must be an array, got ${K( + n + )}, for module ${K(e)}`, xt(o) || ut`Property 'reexports' of a precompiled module source must be an array if present, got ${K( + o + )}, for module ${K(e)}`; +} +const cc = (t, e, r) => { + const { compartment: n, moduleSpecifier: o, resolvedImports: a, moduleSource: i } = r, { instances: c } = Z(t, n); + if (Ur(c, o)) + return Ge(c, o); + ic(i, o); + const l = new Ce(); + let u; + if (nc(i)) + oc(i, o), u = rc( + t, + e, + r, + l + ); + else if (sc(i)) + ac(i, o), u = tc( + t, + i, + n, + e, + o, + a + ); + else + throw _( + `importHook must provide a module source, got ${K(i)}` + ); + ue(c, o, u); + for (const [d, f] of fe(a)) { + const h = Ts( + t, + e, + n, + f + ); + ue(l, d, h); + } + return u; +}, Ut = new Ue(), Oe = new Ue(), Mn = function(e = {}, r = {}, n = {}) { + throw _( + "Compartment.prototype.constructor is not a valid constructor." + ); +}, lo = (t, e) => { + const { execute: r, exportsProxy: n } = Ts( + Oe, + Ut, + t, + e + ); + return r(), n; +}, Ln = { + constructor: Mn, + get globalThis() { + return Z(Oe, this).globalObject; + }, + get name() { + return Z(Oe, this).name; + }, + /** + * @param {string} source is a JavaScript program grammar construction. + * @param {object} [options] + * @param {Array} [options.transforms] + * @param {boolean} [options.sloppyGlobalsMode] + * @param {object} [options.__moduleShimLexicals__] + * @param {boolean} [options.__evadeHtmlCommentTest__] + * @param {boolean} [options.__evadeImportExpressionTest__] + * @param {boolean} [options.__rejectSomeDirectEvalExpressions__] + */ + evaluate(t, e = {}) { + const r = Z(Oe, this); + return Ps(r, t, e); + }, + module(t) { + if (typeof t != "string") + throw _("first argument of module() must be a string"); + const { exportsProxy: e } = On( + this, + Z(Oe, this), + Ut, + t + ); + return e; + }, + async import(t) { + const { noNamespaceBox: e } = Z(Oe, this); + if (typeof t != "string") + throw _("first argument of import() must be a string"); + return Do( + co(Oe, Ut, this, t), + () => { + const r = lo( + /** @type {Compartment} */ + this, + t + ); + return e ? r : { namespace: r }; + } + ); + }, + async load(t) { + if (typeof t != "string") + throw _("first argument of load() must be a string"); + return co(Oe, Ut, this, t); + }, + importNow(t) { + if (typeof t != "string") + throw _("first argument of importNow() must be a string"); + return Xi(Oe, Ut, this, t), lo( + /** @type {Compartment} */ + this, + t + ); + } +}; +z(Ln, { + [Xe]: { + value: "Compartment", + writable: !1, + enumerable: !1, + configurable: !0 + } +}); +z(Mn, { + prototype: { value: Ln } +}); +const lc = (...t) => { + if (t.length === 0) + return {}; + if (t.length === 1 && typeof t[0] == "object" && t[0] !== null && "__options__" in t[0]) { + const { __options__: e, ...r } = t[0]; + return assert( + e === !0, + `Compartment constructor only supports true __options__ sigil, got ${e}` + ), r; + } else { + const [ + e = ( + /** @type {Map} */ + {} + ), + r = ( + /** @type {Map} */ + {} + ), + n = {} + ] = t; + return qn( + n.modules, + void 0, + "Compartment constructor must receive either a module map argument or modules option, not both" + ), qn( + n.globals, + void 0, + "Compartment constructor must receive either globals argument or option, not both" + ), { + ...n, + globals: e, + modules: r + }; + } +}, dn = (t, e, r, n = void 0) => { + function o(...a) { + if (new.target === void 0) + throw _( + "Class constructor Compartment cannot be invoked without 'new'" + ); + const { + name: i = "", + transforms: c = [], + __shimTransforms__: l = [], + globals: u = {}, + modules: d = {}, + resolveHook: f, + importHook: h, + importNowHook: p, + moduleMapHook: m, + importMetaHook: b, + __noNamespaceBox__: S = !1 + } = lc(...a), x = [...c, ...l], I = { __proto__: null, ...u }, E = { __proto__: null, ...d }, A = new Ce(), N = new Ce(), D = new Ce(), M = {}; + ci(M), is(M); + const { safeEvaluate: J } = Cn({ + globalObject: M, + globalTransforms: x, + sloppyGlobalsMode: !1 + }); + cs(M, { + intrinsics: e, + newGlobalPropertyNames: ts, + makeCompartmentConstructor: t, + parentCompartment: this, + markVirtualizedNativeFunction: r + }), un( + M, + J, + r + ), Or(M, I), de(Oe, this, { + name: `${i}`, + globalTransforms: x, + globalObject: M, + safeEvaluate: J, + resolveHook: f, + importHook: h, + importNowHook: p, + moduleMap: E, + moduleMapHook: m, + importMetaHook: b, + moduleRecords: A, + __shimTransforms__: l, + deferredExports: D, + instances: N, + parentCompartment: n, + noNamespaceBox: S + }); + } + return o.prototype = Ln, o; +}; +function rn(t) { + return G(t).constructor; +} +function uc() { + return arguments; +} +const dc = () => { + const t = xe.prototype.constructor, e = Q(uc(), "callee"), r = e && e.get, n = _a(new ye()), o = G(n), a = Lr[ko] && ma(/./), i = a && G(a), c = ua([]), l = G(c), u = G(Vs), d = pa(new Ce()), f = G(d), h = ha(new Nt()), p = G(h), m = G(l); + function* b() { + } + const S = rn(b), x = S.prototype; + async function* I() { + } + const E = rn( + I + ), A = E.prototype, N = A.prototype, D = G(N); + async function M() { + } + const J = rn(M), _e = { + "%InertFunction%": t, + "%ArrayIteratorPrototype%": l, + "%InertAsyncFunction%": J, + "%AsyncGenerator%": A, + "%InertAsyncGeneratorFunction%": E, + "%AsyncGeneratorPrototype%": N, + "%AsyncIteratorPrototype%": D, + "%Generator%": x, + "%InertGeneratorFunction%": S, + "%IteratorPrototype%": m, + "%MapIteratorPrototype%": f, + "%RegExpStringIteratorPrototype%": i, + "%SetIteratorPrototype%": p, + "%StringIteratorPrototype%": o, + "%ThrowTypeError%": r, + "%TypedArray%": u, + "%InertCompartment%": Mn + }; + return P.Iterator && (_e["%IteratorHelperPrototype%"] = G( + // eslint-disable-next-line @endo/no-polymorphic-call + P.Iterator.from([]).take(0) + ), _e["%WrapForValidIteratorPrototype%"] = G( + // eslint-disable-next-line @endo/no-polymorphic-call + P.Iterator.from({ next() { + } }) + )), P.AsyncIterator && (_e["%AsyncIteratorHelperPrototype%"] = G( + // eslint-disable-next-line @endo/no-polymorphic-call + P.AsyncIterator.from([]).take(0) + ), _e["%WrapForValidAsyncIteratorPrototype%"] = G( + // eslint-disable-next-line @endo/no-polymorphic-call + P.AsyncIterator.from({ next() { + } }) + )), _e; +}, Is = (t, e) => { + if (e !== "safe" && e !== "unsafe") + throw _(`unrecognized fakeHardenOption ${e}`); + if (e === "safe" || (Object.isExtensible = () => !1, Object.isFrozen = () => !0, Object.isSealed = () => !0, Reflect.isExtensible = () => !1, t.isFake)) + return t; + const r = (n) => n; + return r.isFake = !0, y(r); +}; +y(Is); +const fc = () => { + const t = wt, e = t.prototype, r = wa(wt, void 0); + z(e, { + constructor: { + value: r + // leave other `constructor` attributes as is + } + }); + const n = fe( + je(t) + ), o = mt( + ce(n, ([a, i]) => [ + a, + { ...i, configurable: !0 } + ]) + ); + return z(r, o), { "%SharedSymbol%": r }; +}, pc = (t) => { + try { + return t(), !1; + } catch { + return !0; + } +}, uo = (t, e, r) => { + if (t === void 0) + return !1; + const n = Q(t, e); + if (!n || "value" in n) + return !1; + const { get: o, set: a } = n; + if (typeof o != "function" || typeof a != "function" || o() !== r || ae(o, t, []) !== r) + return !1; + const i = "Seems to be a setter", c = { __proto__: null }; + if (ae(a, c, [i]), c[e] !== i) + return !1; + const l = { __proto__: t }; + return ae(a, l, [i]), l[e] !== i || !pc(() => ae(a, t, [r])) || "originalValue" in o || n.configurable === !1 ? !1 : (U(t, e, { + value: r, + writable: !0, + enumerable: n.enumerable, + configurable: !0 + }), !0); +}, hc = (t) => { + uo( + t["%IteratorPrototype%"], + "constructor", + t.Iterator + ), uo( + t["%IteratorPrototype%"], + Xe, + "Iterator" + ); +}, { Fail: fo, details: po, quote: ho } = Y; +let pr, hr; +const mc = za(), gc = () => { + let t = !1; + try { + t = xe( + "eval", + "SES_changed", + ` eval("SES_changed = true"); + return SES_changed; + ` + )(Uo, !1), t || delete P.SES_changed; + } catch { + t = !0; + } + if (!t) + throw _( + "SES cannot initialize unless 'eval' is the original intrinsic 'eval', suitable for direct-eval (dynamically scoped eval) (SES_DIRECT_EVAL)" + ); +}, As = (t = {}) => { + const { + errorTaming: e = he("LOCKDOWN_ERROR_TAMING", "safe"), + errorTrapping: r = ( + /** @type {"platform" | "none" | "report" | "abort" | "exit" | undefined} */ + he("LOCKDOWN_ERROR_TRAPPING", "platform") + ), + unhandledRejectionTrapping: n = ( + /** @type {"none" | "report" | undefined} */ + he("LOCKDOWN_UNHANDLED_REJECTION_TRAPPING", "report") + ), + regExpTaming: o = he("LOCKDOWN_REGEXP_TAMING", "safe"), + localeTaming: a = he("LOCKDOWN_LOCALE_TAMING", "safe"), + consoleTaming: i = ( + /** @type {'unsafe' | 'safe' | undefined} */ + he("LOCKDOWN_CONSOLE_TAMING", "safe") + ), + overrideTaming: c = he("LOCKDOWN_OVERRIDE_TAMING", "moderate"), + stackFiltering: l = he("LOCKDOWN_STACK_FILTERING", "concise"), + domainTaming: u = he("LOCKDOWN_DOMAIN_TAMING", "safe"), + evalTaming: d = he("LOCKDOWN_EVAL_TAMING", "safeEval"), + overrideDebug: f = Qe( + kn(he("LOCKDOWN_OVERRIDE_DEBUG", ""), ","), + /** @param {string} debugName */ + (We) => We !== "" + ), + __hardenTaming__: h = he("LOCKDOWN_HARDEN_TAMING", "safe"), + dateTaming: p = "safe", + // deprecated + mathTaming: m = "safe", + // deprecated + ...b + } = t; + d === "unsafeEval" || d === "safeEval" || d === "noEval" || fo`lockdown(): non supported option evalTaming: ${ho(d)}`; + const S = Be(b); + if (S.length === 0 || fo`lockdown(): non supported option ${ho(S)}`, pr === void 0 || // eslint-disable-next-line @endo/no-polymorphic-call + Y.fail( + po`Already locked down at ${pr} (SES_ALREADY_LOCKED_DOWN)`, + _ + ), pr = _("Prior lockdown (SES_ALREADY_LOCKED_DOWN)"), pr.stack, gc(), P.Function.prototype.constructor !== P.Function && // @ts-ignore harden is absent on globalThis type def. + typeof P.harden == "function" && // @ts-ignore lockdown is absent on globalThis type def. + typeof P.lockdown == "function" && P.Date.prototype.constructor !== P.Date && typeof P.Date.now == "function" && // @ts-ignore does not recognize that Date constructor is a special + // Function. + // eslint-disable-next-line @endo/no-polymorphic-call + Mr(P.Date.prototype.constructor.now(), NaN)) + throw _( + "Already locked down but not by this SES instance (SES_MULTIPLE_INSTANCES)" + ); + xi(u); + const I = xs(), { addIntrinsics: E, completePrototypes: A, finalIntrinsics: N } = os(), D = Is(mc, h); + E({ harden: D }), E(Ka()), E(Ya(p)), E(zi(e, l)), E(Ja(m)), E(Xa(o)), E(fc()), E(dc()), A(); + const M = N(), J = { __proto__: null }; + typeof P.Buffer == "function" && (J.Buffer = P.Buffer); + let _e; + e === "safe" && (_e = M["%InitialGetStackString%"]); + const He = Pi( + i, + r, + n, + _e + ); + if (P.console = /** @type {Console} */ + He.console, typeof /** @type {any} */ + He.console._times == "object" && (J.SafeMap = G( + // eslint-disable-next-line no-underscore-dangle + /** @type {any} */ + He.console._times + )), (e === "unsafe" || e === "unsafe-debug") && P.assert === Y && (P.assert = Gr(void 0, !0)), si(M, a), hc(M), qa(M, I), is(P), cs(P, { + intrinsics: M, + newGlobalPropertyNames: Kn, + makeCompartmentConstructor: dn, + markVirtualizedNativeFunction: I + }), d === "noEval") + un( + P, + Sa, + I + ); + else if (d === "safeEval") { + const { safeEvaluate: We } = Cn({ globalObject: P }); + un( + P, + We, + I + ); + } + return () => { + hr === void 0 || // eslint-disable-next-line @endo/no-polymorphic-call + Y.fail( + po`Already locked down at ${hr} (SES_ALREADY_LOCKED_DOWN)`, + _ + ), hr = _( + "Prior lockdown (SES_ALREADY_LOCKED_DOWN)" + ), hr.stack, ti(M, c, f); + const We = { + intrinsics: M, + hostIntrinsics: J, + globals: { + // Harden evaluators + Function: P.Function, + eval: P.eval, + // @ts-ignore Compartment does exist on globalThis + Compartment: P.Compartment, + // Harden Symbol + Symbol: P.Symbol + } + }; + for (const lr of It(Kn)) + We.globals[lr] = P[lr]; + return D(We), D; + }; +}; +P.lockdown = (t) => { + const e = As(t); + P.harden = e(); +}; +P.repairIntrinsics = (t) => { + const e = As(t); + P.hardenIntrinsics = () => { + P.harden = e(); + }; +}; +const yc = xs(); +P.Compartment = dn( + dn, + Wa(P), + yc +); +P.assert = Y; +const _c = Es(Sr), vc = ea( + "MAKE_CAUSAL_CONSOLE_FROM_LOGGER_KEY_FOR_SES_AVA" +); +P[vc] = _c; +const bc = (t, e) => { + let r = { x: 0, y: 0 }, n = { x: 0, y: 0 }, o = { x: 0, y: 0 }; + const a = (l) => { + const { clientX: u, clientY: d } = l, f = u - o.x + n.x, h = d - o.y + n.y; + r = { x: f, y: h }, t.style.transform = `translate(${f}px, ${h}px)`, e == null || e(); + }, i = () => { + document.removeEventListener("mousemove", a), document.removeEventListener("mouseup", i); + }, c = (l) => { + o = { x: l.clientX, y: l.clientY }, n = { x: r.x, y: r.y }, document.addEventListener("mousemove", a), document.addEventListener("mouseup", i); + }; + return t.addEventListener("mousedown", c), i; +}, wc = ":host{--spacing-4: .25rem;--spacing-8: calc(var(--spacing-4) * 2);--spacing-12: calc(var(--spacing-4) * 3);--spacing-16: calc(var(--spacing-4) * 4);--spacing-20: calc(var(--spacing-4) * 5);--spacing-24: calc(var(--spacing-4) * 6);--spacing-28: calc(var(--spacing-4) * 7);--spacing-32: calc(var(--spacing-4) * 8);--spacing-36: calc(var(--spacing-4) * 9);--spacing-40: calc(var(--spacing-4) * 10);--font-weight-regular: 400;--font-weight-bold: 500;--font-line-height-s: 1.2;--font-line-height-m: 1.4;--font-line-height-l: 1.5;--font-size-s: 12px;--font-size-m: 14px;--font-size-l: 16px}[data-theme]{background-color:var(--color-background-primary);color:var(--color-foreground-secondary)}.wrapper{box-sizing:border-box;display:flex;flex-direction:column;position:fixed;inset-block-start:var(--modal-block-start);inset-inline-end:var(--modal-inline-end);z-index:1000;padding:25px;border-radius:15px;border:2px solid var(--color-background-quaternary);box-shadow:0 0 10px #0000004d}.header{align-items:center;display:flex;justify-content:space-between;border-block-end:2px solid var(--color-background-quaternary);padding-block-end:var(--spacing-4)}button{background:transparent;border:0;cursor:pointer;padding:0}h1{font-size:var(--font-size-s);font-weight:var(--font-weight-bold);margin:0;margin-inline-end:var(--spacing-4);-webkit-user-select:none;user-select:none}iframe{border:none;inline-size:100%;block-size:100%}", xc = ` +`; +var me, nr; +class Sc extends HTMLElement { + constructor() { + super(); + Wr(this, me, null); + Wr(this, nr, null); + this.attachShadow({ mode: "open" }); + } + setTheme(r) { + Ie(this, me) && Ie(this, me).setAttribute("data-theme", r); + } + disconnectedCallback() { + var r; + (r = Ie(this, nr)) == null || r.call(this); + } + calculateZIndex() { + const r = document.querySelectorAll("plugin-modal"), n = Array.from(r).filter((a) => a !== this).map((a) => Number(a.style.zIndex)), o = Math.max(...n, 0); + this.style.zIndex = (o + 1).toString(); + } + connectedCallback() { + const r = this.getAttribute("title"), n = this.getAttribute("iframe-src"), o = Number(this.getAttribute("width") || "300"), a = Number(this.getAttribute("height") || "400"); + if (!r || !n) + throw new Error("title and iframe-src attributes are required"); + if (!this.shadowRoot) + throw new Error("Error creating shadow root"); + qr(this, me, document.createElement("div")), Ie(this, me).classList.add("wrapper"), Ie(this, me).style.inlineSize = `${o}px`, Ie(this, me).style.blockSize = `${a}px`, qr(this, nr, bc(Ie(this, me), () => { + this.calculateZIndex(); + })); + const i = document.createElement("div"); + i.classList.add("header"); + const c = document.createElement("h1"); + c.textContent = r, i.appendChild(c); + const l = document.createElement("button"); + l.setAttribute("type", "button"), l.innerHTML = `
${xc}
`, l.addEventListener("click", () => { + this.shadowRoot && this.shadowRoot.dispatchEvent( + new CustomEvent("close", { + composed: !0, + bubbles: !0 + }) + ); + }), i.appendChild(l); + const u = document.createElement("iframe"); + u.src = n, u.allow = "", u.sandbox.add( + "allow-scripts", + "allow-forms", + "allow-modals", + "allow-popups", + "allow-popups-to-escape-sandbox", + "allow-storage-access-by-user-activation" + ), u.addEventListener("load", () => { + var f; + (f = this.shadowRoot) == null || f.dispatchEvent( + new CustomEvent("load", { + composed: !0, + bubbles: !0 + }) + ); + }), this.addEventListener("message", (f) => { + u.contentWindow && u.contentWindow.postMessage(f.detail, "*"); + }), this.shadowRoot.appendChild(Ie(this, me)), Ie(this, me).appendChild(i), Ie(this, me).appendChild(u); + const d = document.createElement("style"); + d.textContent = wc, this.shadowRoot.appendChild(d), this.calculateZIndex(); + } +} +me = new WeakMap(), nr = new WeakMap(); +customElements.define("plugin-modal", Sc); +var F; +(function(t) { + t.assertEqual = (o) => o; + function e(o) { + } + t.assertIs = e; + function r(o) { + throw new Error(); + } + t.assertNever = r, t.arrayToEnum = (o) => { + const a = {}; + for (const i of o) + a[i] = i; + return a; + }, t.getValidEnumValues = (o) => { + const a = t.objectKeys(o).filter((c) => typeof o[o[c]] != "number"), i = {}; + for (const c of a) + i[c] = o[c]; + return t.objectValues(i); + }, t.objectValues = (o) => t.objectKeys(o).map(function(a) { + return o[a]; + }), t.objectKeys = typeof Object.keys == "function" ? (o) => Object.keys(o) : (o) => { + const a = []; + for (const i in o) + Object.prototype.hasOwnProperty.call(o, i) && a.push(i); + return a; + }, t.find = (o, a) => { + for (const i of o) + if (a(i)) + return i; + }, t.isInteger = typeof Number.isInteger == "function" ? (o) => Number.isInteger(o) : (o) => typeof o == "number" && isFinite(o) && Math.floor(o) === o; + function n(o, a = " | ") { + return o.map((i) => typeof i == "string" ? `'${i}'` : i).join(a); + } + t.joinValues = n, t.jsonStringifyReplacer = (o, a) => typeof a == "bigint" ? a.toString() : a; +})(F || (F = {})); +var fn; +(function(t) { + t.mergeShapes = (e, r) => ({ + ...e, + ...r + // second overwrites first + }); +})(fn || (fn = {})); +const w = F.arrayToEnum([ + "string", + "nan", + "number", + "integer", + "float", + "boolean", + "date", + "bigint", + "symbol", + "function", + "undefined", + "null", + "array", + "object", + "unknown", + "promise", + "void", + "never", + "map", + "set" +]), Ye = (t) => { + switch (typeof t) { + case "undefined": + return w.undefined; + case "string": + return w.string; + case "number": + return isNaN(t) ? w.nan : w.number; + case "boolean": + return w.boolean; + case "function": + return w.function; + case "bigint": + return w.bigint; + case "symbol": + return w.symbol; + case "object": + return Array.isArray(t) ? w.array : t === null ? w.null : t.then && typeof t.then == "function" && t.catch && typeof t.catch == "function" ? w.promise : typeof Map < "u" && t instanceof Map ? w.map : typeof Set < "u" && t instanceof Set ? w.set : typeof Date < "u" && t instanceof Date ? w.date : w.object; + default: + return w.unknown; + } +}, g = F.arrayToEnum([ + "invalid_type", + "invalid_literal", + "custom", + "invalid_union", + "invalid_union_discriminator", + "invalid_enum_value", + "unrecognized_keys", + "invalid_arguments", + "invalid_return_type", + "invalid_date", + "invalid_string", + "too_small", + "too_big", + "invalid_intersection_types", + "not_multiple_of", + "not_finite" +]), Ec = (t) => JSON.stringify(t, null, 2).replace(/"([^"]+)":/g, "$1:"); +class ge extends Error { + constructor(e) { + super(), this.issues = [], this.addIssue = (n) => { + this.issues = [...this.issues, n]; + }, this.addIssues = (n = []) => { + this.issues = [...this.issues, ...n]; + }; + const r = new.target.prototype; + Object.setPrototypeOf ? Object.setPrototypeOf(this, r) : this.__proto__ = r, this.name = "ZodError", this.issues = e; + } + get errors() { + return this.issues; + } + format(e) { + const r = e || function(a) { + return a.message; + }, n = { _errors: [] }, o = (a) => { + for (const i of a.issues) + if (i.code === "invalid_union") + i.unionErrors.map(o); + else if (i.code === "invalid_return_type") + o(i.returnTypeError); + else if (i.code === "invalid_arguments") + o(i.argumentsError); + else if (i.path.length === 0) + n._errors.push(r(i)); + else { + let c = n, l = 0; + for (; l < i.path.length; ) { + const u = i.path[l]; + l === i.path.length - 1 ? (c[u] = c[u] || { _errors: [] }, c[u]._errors.push(r(i))) : c[u] = c[u] || { _errors: [] }, c = c[u], l++; + } + } + }; + return o(this), n; + } + static assert(e) { + if (!(e instanceof ge)) + throw new Error(`Not a ZodError: ${e}`); + } + toString() { + return this.message; + } + get message() { + return JSON.stringify(this.issues, F.jsonStringifyReplacer, 2); + } + get isEmpty() { + return this.issues.length === 0; + } + flatten(e = (r) => r.message) { + const r = {}, n = []; + for (const o of this.issues) + o.path.length > 0 ? (r[o.path[0]] = r[o.path[0]] || [], r[o.path[0]].push(e(o))) : n.push(e(o)); + return { formErrors: n, fieldErrors: r }; + } + get formErrors() { + return this.flatten(); + } +} +ge.create = (t) => new ge(t); +const Ct = (t, e) => { + let r; + switch (t.code) { + case g.invalid_type: + t.received === w.undefined ? r = "Required" : r = `Expected ${t.expected}, received ${t.received}`; + break; + case g.invalid_literal: + r = `Invalid literal value, expected ${JSON.stringify(t.expected, F.jsonStringifyReplacer)}`; + break; + case g.unrecognized_keys: + r = `Unrecognized key(s) in object: ${F.joinValues(t.keys, ", ")}`; + break; + case g.invalid_union: + r = "Invalid input"; + break; + case g.invalid_union_discriminator: + r = `Invalid discriminator value. Expected ${F.joinValues(t.options)}`; + break; + case g.invalid_enum_value: + r = `Invalid enum value. Expected ${F.joinValues(t.options)}, received '${t.received}'`; + break; + case g.invalid_arguments: + r = "Invalid function arguments"; + break; + case g.invalid_return_type: + r = "Invalid function return type"; + break; + case g.invalid_date: + r = "Invalid date"; + break; + case g.invalid_string: + typeof t.validation == "object" ? "includes" in t.validation ? (r = `Invalid input: must include "${t.validation.includes}"`, typeof t.validation.position == "number" && (r = `${r} at one or more positions greater than or equal to ${t.validation.position}`)) : "startsWith" in t.validation ? r = `Invalid input: must start with "${t.validation.startsWith}"` : "endsWith" in t.validation ? r = `Invalid input: must end with "${t.validation.endsWith}"` : F.assertNever(t.validation) : t.validation !== "regex" ? r = `Invalid ${t.validation}` : r = "Invalid"; + break; + case g.too_small: + t.type === "array" ? r = `Array must contain ${t.exact ? "exactly" : t.inclusive ? "at least" : "more than"} ${t.minimum} element(s)` : t.type === "string" ? r = `String must contain ${t.exact ? "exactly" : t.inclusive ? "at least" : "over"} ${t.minimum} character(s)` : t.type === "number" ? r = `Number must be ${t.exact ? "exactly equal to " : t.inclusive ? "greater than or equal to " : "greater than "}${t.minimum}` : t.type === "date" ? r = `Date must be ${t.exact ? "exactly equal to " : t.inclusive ? "greater than or equal to " : "greater than "}${new Date(Number(t.minimum))}` : r = "Invalid input"; + break; + case g.too_big: + t.type === "array" ? r = `Array must contain ${t.exact ? "exactly" : t.inclusive ? "at most" : "less than"} ${t.maximum} element(s)` : t.type === "string" ? r = `String must contain ${t.exact ? "exactly" : t.inclusive ? "at most" : "under"} ${t.maximum} character(s)` : t.type === "number" ? r = `Number must be ${t.exact ? "exactly" : t.inclusive ? "less than or equal to" : "less than"} ${t.maximum}` : t.type === "bigint" ? r = `BigInt must be ${t.exact ? "exactly" : t.inclusive ? "less than or equal to" : "less than"} ${t.maximum}` : t.type === "date" ? r = `Date must be ${t.exact ? "exactly" : t.inclusive ? "smaller than or equal to" : "smaller than"} ${new Date(Number(t.maximum))}` : r = "Invalid input"; + break; + case g.custom: + r = "Invalid input"; + break; + case g.invalid_intersection_types: + r = "Intersection results could not be merged"; + break; + case g.not_multiple_of: + r = `Number must be a multiple of ${t.multipleOf}`; + break; + case g.not_finite: + r = "Number must be finite"; + break; + default: + r = e.defaultError, F.assertNever(t); + } + return { message: r }; +}; +let Cs = Ct; +function kc(t) { + Cs = t; +} +function Pr() { + return Cs; +} +const Tr = (t) => { + const { data: e, path: r, errorMaps: n, issueData: o } = t, a = [...r, ...o.path || []], i = { + ...o, + path: a + }; + if (o.message !== void 0) + return { + ...o, + path: a, + message: o.message + }; + let c = ""; + const l = n.filter((u) => !!u).slice().reverse(); + for (const u of l) + c = u(i, { data: e, defaultError: c }).message; + return { + ...o, + path: a, + message: c + }; +}, Pc = []; +function v(t, e) { + const r = Pr(), n = Tr({ + issueData: e, + data: t.data, + path: t.path, + errorMaps: [ + t.common.contextualErrorMap, + t.schemaErrorMap, + r, + r === Ct ? void 0 : Ct + // then global default map + ].filter((o) => !!o) + }); + t.common.issues.push(n); +} +class te { + constructor() { + this.value = "valid"; + } + dirty() { + this.value === "valid" && (this.value = "dirty"); + } + abort() { + this.value !== "aborted" && (this.value = "aborted"); + } + static mergeArray(e, r) { + const n = []; + for (const o of r) { + if (o.status === "aborted") + return $; + o.status === "dirty" && e.dirty(), n.push(o.value); + } + return { status: e.value, value: n }; + } + static async mergeObjectAsync(e, r) { + const n = []; + for (const o of r) { + const a = await o.key, i = await o.value; + n.push({ + key: a, + value: i + }); + } + return te.mergeObjectSync(e, n); + } + static mergeObjectSync(e, r) { + const n = {}; + for (const o of r) { + const { key: a, value: i } = o; + if (a.status === "aborted" || i.status === "aborted") + return $; + a.status === "dirty" && e.dirty(), i.status === "dirty" && e.dirty(), a.value !== "__proto__" && (typeof i.value < "u" || o.alwaysSet) && (n[a.value] = i.value); + } + return { status: e.value, value: n }; + } +} +const $ = Object.freeze({ + status: "aborted" +}), bt = (t) => ({ status: "dirty", value: t }), le = (t) => ({ status: "valid", value: t }), pn = (t) => t.status === "aborted", hn = (t) => t.status === "dirty", Bt = (t) => t.status === "valid", Gt = (t) => typeof Promise < "u" && t instanceof Promise; +function Ir(t, e, r, n) { + if (typeof e == "function" ? t !== e || !n : !e.has(t)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); + return e.get(t); +} +function $s(t, e, r, n, o) { + if (typeof e == "function" ? t !== e || !o : !e.has(t)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); + return e.set(t, r), r; +} +var k; +(function(t) { + t.errToObj = (e) => typeof e == "string" ? { message: e } : e || {}, t.toString = (e) => typeof e == "string" ? e : e == null ? void 0 : e.message; +})(k || (k = {})); +var jt, Zt; +class Fe { + constructor(e, r, n, o) { + this._cachedPath = [], this.parent = e, this.data = r, this._path = n, this._key = o; + } + get path() { + return this._cachedPath.length || (this._key instanceof Array ? this._cachedPath.push(...this._path, ...this._key) : this._cachedPath.push(...this._path, this._key)), this._cachedPath; + } +} +const mo = (t, e) => { + if (Bt(e)) + return { success: !0, data: e.value }; + if (!t.common.issues.length) + throw new Error("Validation failed but no issues detected."); + return { + success: !1, + get error() { + if (this._error) + return this._error; + const r = new ge(t.common.issues); + return this._error = r, this._error; + } + }; +}; +function R(t) { + if (!t) + return {}; + const { errorMap: e, invalid_type_error: r, required_error: n, description: o } = t; + if (e && (r || n)) + throw new Error(`Can't use "invalid_type_error" or "required_error" in conjunction with custom error map.`); + return e ? { errorMap: e, description: o } : { errorMap: (i, c) => { + var l, u; + const { message: d } = t; + return i.code === "invalid_enum_value" ? { message: d ?? c.defaultError } : typeof c.data > "u" ? { message: (l = d ?? n) !== null && l !== void 0 ? l : c.defaultError } : i.code !== "invalid_type" ? { message: c.defaultError } : { message: (u = d ?? r) !== null && u !== void 0 ? u : c.defaultError }; + }, description: o }; +} +class O { + constructor(e) { + this.spa = this.safeParseAsync, this._def = e, this.parse = this.parse.bind(this), this.safeParse = this.safeParse.bind(this), this.parseAsync = this.parseAsync.bind(this), this.safeParseAsync = this.safeParseAsync.bind(this), this.spa = this.spa.bind(this), this.refine = this.refine.bind(this), this.refinement = this.refinement.bind(this), this.superRefine = this.superRefine.bind(this), this.optional = this.optional.bind(this), this.nullable = this.nullable.bind(this), this.nullish = this.nullish.bind(this), this.array = this.array.bind(this), this.promise = this.promise.bind(this), this.or = this.or.bind(this), this.and = this.and.bind(this), this.transform = this.transform.bind(this), this.brand = this.brand.bind(this), this.default = this.default.bind(this), this.catch = this.catch.bind(this), this.describe = this.describe.bind(this), this.pipe = this.pipe.bind(this), this.readonly = this.readonly.bind(this), this.isNullable = this.isNullable.bind(this), this.isOptional = this.isOptional.bind(this); + } + get description() { + return this._def.description; + } + _getType(e) { + return Ye(e.data); + } + _getOrReturnCtx(e, r) { + return r || { + common: e.parent.common, + data: e.data, + parsedType: Ye(e.data), + schemaErrorMap: this._def.errorMap, + path: e.path, + parent: e.parent + }; + } + _processInputParams(e) { + return { + status: new te(), + ctx: { + common: e.parent.common, + data: e.data, + parsedType: Ye(e.data), + schemaErrorMap: this._def.errorMap, + path: e.path, + parent: e.parent + } + }; + } + _parseSync(e) { + const r = this._parse(e); + if (Gt(r)) + throw new Error("Synchronous parse encountered promise."); + return r; + } + _parseAsync(e) { + const r = this._parse(e); + return Promise.resolve(r); + } + parse(e, r) { + const n = this.safeParse(e, r); + if (n.success) + return n.data; + throw n.error; + } + safeParse(e, r) { + var n; + const o = { + common: { + issues: [], + async: (n = r == null ? void 0 : r.async) !== null && n !== void 0 ? n : !1, + contextualErrorMap: r == null ? void 0 : r.errorMap + }, + path: (r == null ? void 0 : r.path) || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data: e, + parsedType: Ye(e) + }, a = this._parseSync({ data: e, path: o.path, parent: o }); + return mo(o, a); + } + async parseAsync(e, r) { + const n = await this.safeParseAsync(e, r); + if (n.success) + return n.data; + throw n.error; + } + async safeParseAsync(e, r) { + const n = { + common: { + issues: [], + contextualErrorMap: r == null ? void 0 : r.errorMap, + async: !0 + }, + path: (r == null ? void 0 : r.path) || [], + schemaErrorMap: this._def.errorMap, + parent: null, + data: e, + parsedType: Ye(e) + }, o = this._parse({ data: e, path: n.path, parent: n }), a = await (Gt(o) ? o : Promise.resolve(o)); + return mo(n, a); + } + refine(e, r) { + const n = (o) => typeof r == "string" || typeof r > "u" ? { message: r } : typeof r == "function" ? r(o) : r; + return this._refinement((o, a) => { + const i = e(o), c = () => a.addIssue({ + code: g.custom, + ...n(o) + }); + return typeof Promise < "u" && i instanceof Promise ? i.then((l) => l ? !0 : (c(), !1)) : i ? !0 : (c(), !1); + }); + } + refinement(e, r) { + return this._refinement((n, o) => e(n) ? !0 : (o.addIssue(typeof r == "function" ? r(n, o) : r), !1)); + } + _refinement(e) { + return new Re({ + schema: this, + typeName: C.ZodEffects, + effect: { type: "refinement", refinement: e } + }); + } + superRefine(e) { + return this._refinement(e); + } + optional() { + return Le.create(this, this._def); + } + nullable() { + return nt.create(this, this._def); + } + nullish() { + return this.nullable().optional(); + } + array() { + return $e.create(this, this._def); + } + promise() { + return Rt.create(this, this._def); + } + or(e) { + return qt.create([this, e], this._def); + } + and(e) { + return Kt.create(this, e, this._def); + } + transform(e) { + return new Re({ + ...R(this._def), + schema: this, + typeName: C.ZodEffects, + effect: { type: "transform", transform: e } + }); + } + default(e) { + const r = typeof e == "function" ? e : () => e; + return new er({ + ...R(this._def), + innerType: this, + defaultValue: r, + typeName: C.ZodDefault + }); + } + brand() { + return new Fn({ + typeName: C.ZodBranded, + type: this, + ...R(this._def) + }); + } + catch(e) { + const r = typeof e == "function" ? e : () => e; + return new tr({ + ...R(this._def), + innerType: this, + catchValue: r, + typeName: C.ZodCatch + }); + } + describe(e) { + const r = this.constructor; + return new r({ + ...this._def, + description: e + }); + } + pipe(e) { + return cr.create(this, e); + } + readonly() { + return rr.create(this); + } + isOptional() { + return this.safeParse(void 0).success; + } + isNullable() { + return this.safeParse(null).success; + } +} +const Tc = /^c[^\s-]{8,}$/i, Ic = /^[0-9a-z]+$/, Ac = /^[0-9A-HJKMNP-TV-Z]{26}$/, Cc = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i, $c = /^[a-z0-9_-]{21}$/i, Rc = /^[-+]?P(?!$)(?:(?:[-+]?\d+Y)|(?:[-+]?\d+[.,]\d+Y$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:(?:[-+]?\d+W)|(?:[-+]?\d+[.,]\d+W$))?(?:(?:[-+]?\d+D)|(?:[-+]?\d+[.,]\d+D$))?(?:T(?=[\d+-])(?:(?:[-+]?\d+H)|(?:[-+]?\d+[.,]\d+H$))?(?:(?:[-+]?\d+M)|(?:[-+]?\d+[.,]\d+M$))?(?:[-+]?\d+(?:[.,]\d+)?S)?)??$/, Nc = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i, Oc = "^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$"; +let nn; +const Mc = /^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])$/, Lc = /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/, Fc = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/, Rs = "((\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\\d|3[01])|(0[469]|11)-(0[1-9]|[12]\\d|30)|(02)-(0[1-9]|1\\d|2[0-8])))", Dc = new RegExp(`^${Rs}$`); +function Ns(t) { + let e = "([01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d"; + return t.precision ? e = `${e}\\.\\d{${t.precision}}` : t.precision == null && (e = `${e}(\\.\\d+)?`), e; +} +function Uc(t) { + return new RegExp(`^${Ns(t)}$`); +} +function Os(t) { + let e = `${Rs}T${Ns(t)}`; + const r = []; + return r.push(t.local ? "Z?" : "Z"), t.offset && r.push("([+-]\\d{2}:?\\d{2})"), e = `${e}(${r.join("|")})`, new RegExp(`^${e}$`); +} +function jc(t, e) { + return !!((e === "v4" || !e) && Mc.test(t) || (e === "v6" || !e) && Lc.test(t)); +} +class Ae extends O { + _parse(e) { + if (this._def.coerce && (e.data = String(e.data)), this._getType(e) !== w.string) { + const a = this._getOrReturnCtx(e); + return v(a, { + code: g.invalid_type, + expected: w.string, + received: a.parsedType + }), $; + } + const n = new te(); + let o; + for (const a of this._def.checks) + if (a.kind === "min") + e.data.length < a.value && (o = this._getOrReturnCtx(e, o), v(o, { + code: g.too_small, + minimum: a.value, + type: "string", + inclusive: !0, + exact: !1, + message: a.message + }), n.dirty()); + else if (a.kind === "max") + e.data.length > a.value && (o = this._getOrReturnCtx(e, o), v(o, { + code: g.too_big, + maximum: a.value, + type: "string", + inclusive: !0, + exact: !1, + message: a.message + }), n.dirty()); + else if (a.kind === "length") { + const i = e.data.length > a.value, c = e.data.length < a.value; + (i || c) && (o = this._getOrReturnCtx(e, o), i ? v(o, { + code: g.too_big, + maximum: a.value, + type: "string", + inclusive: !0, + exact: !0, + message: a.message + }) : c && v(o, { + code: g.too_small, + minimum: a.value, + type: "string", + inclusive: !0, + exact: !0, + message: a.message + }), n.dirty()); + } else if (a.kind === "email") + Nc.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "email", + code: g.invalid_string, + message: a.message + }), n.dirty()); + else if (a.kind === "emoji") + nn || (nn = new RegExp(Oc, "u")), nn.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "emoji", + code: g.invalid_string, + message: a.message + }), n.dirty()); + else if (a.kind === "uuid") + Cc.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "uuid", + code: g.invalid_string, + message: a.message + }), n.dirty()); + else if (a.kind === "nanoid") + $c.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "nanoid", + code: g.invalid_string, + message: a.message + }), n.dirty()); + else if (a.kind === "cuid") + Tc.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "cuid", + code: g.invalid_string, + message: a.message + }), n.dirty()); + else if (a.kind === "cuid2") + Ic.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "cuid2", + code: g.invalid_string, + message: a.message + }), n.dirty()); + else if (a.kind === "ulid") + Ac.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "ulid", + code: g.invalid_string, + message: a.message + }), n.dirty()); + else if (a.kind === "url") + try { + new URL(e.data); + } catch { + o = this._getOrReturnCtx(e, o), v(o, { + validation: "url", + code: g.invalid_string, + message: a.message + }), n.dirty(); + } + else a.kind === "regex" ? (a.regex.lastIndex = 0, a.regex.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "regex", + code: g.invalid_string, + message: a.message + }), n.dirty())) : a.kind === "trim" ? e.data = e.data.trim() : a.kind === "includes" ? e.data.includes(a.value, a.position) || (o = this._getOrReturnCtx(e, o), v(o, { + code: g.invalid_string, + validation: { includes: a.value, position: a.position }, + message: a.message + }), n.dirty()) : a.kind === "toLowerCase" ? e.data = e.data.toLowerCase() : a.kind === "toUpperCase" ? e.data = e.data.toUpperCase() : a.kind === "startsWith" ? e.data.startsWith(a.value) || (o = this._getOrReturnCtx(e, o), v(o, { + code: g.invalid_string, + validation: { startsWith: a.value }, + message: a.message + }), n.dirty()) : a.kind === "endsWith" ? e.data.endsWith(a.value) || (o = this._getOrReturnCtx(e, o), v(o, { + code: g.invalid_string, + validation: { endsWith: a.value }, + message: a.message + }), n.dirty()) : a.kind === "datetime" ? Os(a).test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + code: g.invalid_string, + validation: "datetime", + message: a.message + }), n.dirty()) : a.kind === "date" ? Dc.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + code: g.invalid_string, + validation: "date", + message: a.message + }), n.dirty()) : a.kind === "time" ? Uc(a).test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + code: g.invalid_string, + validation: "time", + message: a.message + }), n.dirty()) : a.kind === "duration" ? Rc.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "duration", + code: g.invalid_string, + message: a.message + }), n.dirty()) : a.kind === "ip" ? jc(e.data, a.version) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "ip", + code: g.invalid_string, + message: a.message + }), n.dirty()) : a.kind === "base64" ? Fc.test(e.data) || (o = this._getOrReturnCtx(e, o), v(o, { + validation: "base64", + code: g.invalid_string, + message: a.message + }), n.dirty()) : F.assertNever(a); + return { status: n.value, value: e.data }; + } + _regex(e, r, n) { + return this.refinement((o) => e.test(o), { + validation: r, + code: g.invalid_string, + ...k.errToObj(n) + }); + } + _addCheck(e) { + return new Ae({ + ...this._def, + checks: [...this._def.checks, e] + }); + } + email(e) { + return this._addCheck({ kind: "email", ...k.errToObj(e) }); + } + url(e) { + return this._addCheck({ kind: "url", ...k.errToObj(e) }); + } + emoji(e) { + return this._addCheck({ kind: "emoji", ...k.errToObj(e) }); + } + uuid(e) { + return this._addCheck({ kind: "uuid", ...k.errToObj(e) }); + } + nanoid(e) { + return this._addCheck({ kind: "nanoid", ...k.errToObj(e) }); + } + cuid(e) { + return this._addCheck({ kind: "cuid", ...k.errToObj(e) }); + } + cuid2(e) { + return this._addCheck({ kind: "cuid2", ...k.errToObj(e) }); + } + ulid(e) { + return this._addCheck({ kind: "ulid", ...k.errToObj(e) }); + } + base64(e) { + return this._addCheck({ kind: "base64", ...k.errToObj(e) }); + } + ip(e) { + return this._addCheck({ kind: "ip", ...k.errToObj(e) }); + } + datetime(e) { + var r, n; + return typeof e == "string" ? this._addCheck({ + kind: "datetime", + precision: null, + offset: !1, + local: !1, + message: e + }) : this._addCheck({ + kind: "datetime", + precision: typeof (e == null ? void 0 : e.precision) > "u" ? null : e == null ? void 0 : e.precision, + offset: (r = e == null ? void 0 : e.offset) !== null && r !== void 0 ? r : !1, + local: (n = e == null ? void 0 : e.local) !== null && n !== void 0 ? n : !1, + ...k.errToObj(e == null ? void 0 : e.message) + }); + } + date(e) { + return this._addCheck({ kind: "date", message: e }); + } + time(e) { + return typeof e == "string" ? this._addCheck({ + kind: "time", + precision: null, + message: e + }) : this._addCheck({ + kind: "time", + precision: typeof (e == null ? void 0 : e.precision) > "u" ? null : e == null ? void 0 : e.precision, + ...k.errToObj(e == null ? void 0 : e.message) + }); + } + duration(e) { + return this._addCheck({ kind: "duration", ...k.errToObj(e) }); + } + regex(e, r) { + return this._addCheck({ + kind: "regex", + regex: e, + ...k.errToObj(r) + }); + } + includes(e, r) { + return this._addCheck({ + kind: "includes", + value: e, + position: r == null ? void 0 : r.position, + ...k.errToObj(r == null ? void 0 : r.message) + }); + } + startsWith(e, r) { + return this._addCheck({ + kind: "startsWith", + value: e, + ...k.errToObj(r) + }); + } + endsWith(e, r) { + return this._addCheck({ + kind: "endsWith", + value: e, + ...k.errToObj(r) + }); + } + min(e, r) { + return this._addCheck({ + kind: "min", + value: e, + ...k.errToObj(r) + }); + } + max(e, r) { + return this._addCheck({ + kind: "max", + value: e, + ...k.errToObj(r) + }); + } + length(e, r) { + return this._addCheck({ + kind: "length", + value: e, + ...k.errToObj(r) + }); + } + /** + * @deprecated Use z.string().min(1) instead. + * @see {@link ZodString.min} + */ + nonempty(e) { + return this.min(1, k.errToObj(e)); + } + trim() { + return new Ae({ + ...this._def, + checks: [...this._def.checks, { kind: "trim" }] + }); + } + toLowerCase() { + return new Ae({ + ...this._def, + checks: [...this._def.checks, { kind: "toLowerCase" }] + }); + } + toUpperCase() { + return new Ae({ + ...this._def, + checks: [...this._def.checks, { kind: "toUpperCase" }] + }); + } + get isDatetime() { + return !!this._def.checks.find((e) => e.kind === "datetime"); + } + get isDate() { + return !!this._def.checks.find((e) => e.kind === "date"); + } + get isTime() { + return !!this._def.checks.find((e) => e.kind === "time"); + } + get isDuration() { + return !!this._def.checks.find((e) => e.kind === "duration"); + } + get isEmail() { + return !!this._def.checks.find((e) => e.kind === "email"); + } + get isURL() { + return !!this._def.checks.find((e) => e.kind === "url"); + } + get isEmoji() { + return !!this._def.checks.find((e) => e.kind === "emoji"); + } + get isUUID() { + return !!this._def.checks.find((e) => e.kind === "uuid"); + } + get isNANOID() { + return !!this._def.checks.find((e) => e.kind === "nanoid"); + } + get isCUID() { + return !!this._def.checks.find((e) => e.kind === "cuid"); + } + get isCUID2() { + return !!this._def.checks.find((e) => e.kind === "cuid2"); + } + get isULID() { + return !!this._def.checks.find((e) => e.kind === "ulid"); + } + get isIP() { + return !!this._def.checks.find((e) => e.kind === "ip"); + } + get isBase64() { + return !!this._def.checks.find((e) => e.kind === "base64"); + } + get minLength() { + let e = null; + for (const r of this._def.checks) + r.kind === "min" && (e === null || r.value > e) && (e = r.value); + return e; + } + get maxLength() { + let e = null; + for (const r of this._def.checks) + r.kind === "max" && (e === null || r.value < e) && (e = r.value); + return e; + } +} +Ae.create = (t) => { + var e; + return new Ae({ + checks: [], + typeName: C.ZodString, + coerce: (e = t == null ? void 0 : t.coerce) !== null && e !== void 0 ? e : !1, + ...R(t) + }); +}; +function Zc(t, e) { + const r = (t.toString().split(".")[1] || "").length, n = (e.toString().split(".")[1] || "").length, o = r > n ? r : n, a = parseInt(t.toFixed(o).replace(".", "")), i = parseInt(e.toFixed(o).replace(".", "")); + return a % i / Math.pow(10, o); +} +class et extends O { + constructor() { + super(...arguments), this.min = this.gte, this.max = this.lte, this.step = this.multipleOf; + } + _parse(e) { + if (this._def.coerce && (e.data = Number(e.data)), this._getType(e) !== w.number) { + const a = this._getOrReturnCtx(e); + return v(a, { + code: g.invalid_type, + expected: w.number, + received: a.parsedType + }), $; + } + let n; + const o = new te(); + for (const a of this._def.checks) + a.kind === "int" ? F.isInteger(e.data) || (n = this._getOrReturnCtx(e, n), v(n, { + code: g.invalid_type, + expected: "integer", + received: "float", + message: a.message + }), o.dirty()) : a.kind === "min" ? (a.inclusive ? e.data < a.value : e.data <= a.value) && (n = this._getOrReturnCtx(e, n), v(n, { + code: g.too_small, + minimum: a.value, + type: "number", + inclusive: a.inclusive, + exact: !1, + message: a.message + }), o.dirty()) : a.kind === "max" ? (a.inclusive ? e.data > a.value : e.data >= a.value) && (n = this._getOrReturnCtx(e, n), v(n, { + code: g.too_big, + maximum: a.value, + type: "number", + inclusive: a.inclusive, + exact: !1, + message: a.message + }), o.dirty()) : a.kind === "multipleOf" ? Zc(e.data, a.value) !== 0 && (n = this._getOrReturnCtx(e, n), v(n, { + code: g.not_multiple_of, + multipleOf: a.value, + message: a.message + }), o.dirty()) : a.kind === "finite" ? Number.isFinite(e.data) || (n = this._getOrReturnCtx(e, n), v(n, { + code: g.not_finite, + message: a.message + }), o.dirty()) : F.assertNever(a); + return { status: o.value, value: e.data }; + } + gte(e, r) { + return this.setLimit("min", e, !0, k.toString(r)); + } + gt(e, r) { + return this.setLimit("min", e, !1, k.toString(r)); + } + lte(e, r) { + return this.setLimit("max", e, !0, k.toString(r)); + } + lt(e, r) { + return this.setLimit("max", e, !1, k.toString(r)); + } + setLimit(e, r, n, o) { + return new et({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind: e, + value: r, + inclusive: n, + message: k.toString(o) + } + ] + }); + } + _addCheck(e) { + return new et({ + ...this._def, + checks: [...this._def.checks, e] + }); + } + int(e) { + return this._addCheck({ + kind: "int", + message: k.toString(e) + }); + } + positive(e) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: !1, + message: k.toString(e) + }); + } + negative(e) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: !1, + message: k.toString(e) + }); + } + nonpositive(e) { + return this._addCheck({ + kind: "max", + value: 0, + inclusive: !0, + message: k.toString(e) + }); + } + nonnegative(e) { + return this._addCheck({ + kind: "min", + value: 0, + inclusive: !0, + message: k.toString(e) + }); + } + multipleOf(e, r) { + return this._addCheck({ + kind: "multipleOf", + value: e, + message: k.toString(r) + }); + } + finite(e) { + return this._addCheck({ + kind: "finite", + message: k.toString(e) + }); + } + safe(e) { + return this._addCheck({ + kind: "min", + inclusive: !0, + value: Number.MIN_SAFE_INTEGER, + message: k.toString(e) + })._addCheck({ + kind: "max", + inclusive: !0, + value: Number.MAX_SAFE_INTEGER, + message: k.toString(e) + }); + } + get minValue() { + let e = null; + for (const r of this._def.checks) + r.kind === "min" && (e === null || r.value > e) && (e = r.value); + return e; + } + get maxValue() { + let e = null; + for (const r of this._def.checks) + r.kind === "max" && (e === null || r.value < e) && (e = r.value); + return e; + } + get isInt() { + return !!this._def.checks.find((e) => e.kind === "int" || e.kind === "multipleOf" && F.isInteger(e.value)); + } + get isFinite() { + let e = null, r = null; + for (const n of this._def.checks) { + if (n.kind === "finite" || n.kind === "int" || n.kind === "multipleOf") + return !0; + n.kind === "min" ? (r === null || n.value > r) && (r = n.value) : n.kind === "max" && (e === null || n.value < e) && (e = n.value); + } + return Number.isFinite(r) && Number.isFinite(e); + } +} +et.create = (t) => new et({ + checks: [], + typeName: C.ZodNumber, + coerce: (t == null ? void 0 : t.coerce) || !1, + ...R(t) +}); +class tt extends O { + constructor() { + super(...arguments), this.min = this.gte, this.max = this.lte; + } + _parse(e) { + if (this._def.coerce && (e.data = BigInt(e.data)), this._getType(e) !== w.bigint) { + const a = this._getOrReturnCtx(e); + return v(a, { + code: g.invalid_type, + expected: w.bigint, + received: a.parsedType + }), $; + } + let n; + const o = new te(); + for (const a of this._def.checks) + a.kind === "min" ? (a.inclusive ? e.data < a.value : e.data <= a.value) && (n = this._getOrReturnCtx(e, n), v(n, { + code: g.too_small, + type: "bigint", + minimum: a.value, + inclusive: a.inclusive, + message: a.message + }), o.dirty()) : a.kind === "max" ? (a.inclusive ? e.data > a.value : e.data >= a.value) && (n = this._getOrReturnCtx(e, n), v(n, { + code: g.too_big, + type: "bigint", + maximum: a.value, + inclusive: a.inclusive, + message: a.message + }), o.dirty()) : a.kind === "multipleOf" ? e.data % a.value !== BigInt(0) && (n = this._getOrReturnCtx(e, n), v(n, { + code: g.not_multiple_of, + multipleOf: a.value, + message: a.message + }), o.dirty()) : F.assertNever(a); + return { status: o.value, value: e.data }; + } + gte(e, r) { + return this.setLimit("min", e, !0, k.toString(r)); + } + gt(e, r) { + return this.setLimit("min", e, !1, k.toString(r)); + } + lte(e, r) { + return this.setLimit("max", e, !0, k.toString(r)); + } + lt(e, r) { + return this.setLimit("max", e, !1, k.toString(r)); + } + setLimit(e, r, n, o) { + return new tt({ + ...this._def, + checks: [ + ...this._def.checks, + { + kind: e, + value: r, + inclusive: n, + message: k.toString(o) + } + ] + }); + } + _addCheck(e) { + return new tt({ + ...this._def, + checks: [...this._def.checks, e] + }); + } + positive(e) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: !1, + message: k.toString(e) + }); + } + negative(e) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: !1, + message: k.toString(e) + }); + } + nonpositive(e) { + return this._addCheck({ + kind: "max", + value: BigInt(0), + inclusive: !0, + message: k.toString(e) + }); + } + nonnegative(e) { + return this._addCheck({ + kind: "min", + value: BigInt(0), + inclusive: !0, + message: k.toString(e) + }); + } + multipleOf(e, r) { + return this._addCheck({ + kind: "multipleOf", + value: e, + message: k.toString(r) + }); + } + get minValue() { + let e = null; + for (const r of this._def.checks) + r.kind === "min" && (e === null || r.value > e) && (e = r.value); + return e; + } + get maxValue() { + let e = null; + for (const r of this._def.checks) + r.kind === "max" && (e === null || r.value < e) && (e = r.value); + return e; + } +} +tt.create = (t) => { + var e; + return new tt({ + checks: [], + typeName: C.ZodBigInt, + coerce: (e = t == null ? void 0 : t.coerce) !== null && e !== void 0 ? e : !1, + ...R(t) + }); +}; +class Vt extends O { + _parse(e) { + if (this._def.coerce && (e.data = !!e.data), this._getType(e) !== w.boolean) { + const n = this._getOrReturnCtx(e); + return v(n, { + code: g.invalid_type, + expected: w.boolean, + received: n.parsedType + }), $; + } + return le(e.data); + } +} +Vt.create = (t) => new Vt({ + typeName: C.ZodBoolean, + coerce: (t == null ? void 0 : t.coerce) || !1, + ...R(t) +}); +class pt extends O { + _parse(e) { + if (this._def.coerce && (e.data = new Date(e.data)), this._getType(e) !== w.date) { + const a = this._getOrReturnCtx(e); + return v(a, { + code: g.invalid_type, + expected: w.date, + received: a.parsedType + }), $; + } + if (isNaN(e.data.getTime())) { + const a = this._getOrReturnCtx(e); + return v(a, { + code: g.invalid_date + }), $; + } + const n = new te(); + let o; + for (const a of this._def.checks) + a.kind === "min" ? e.data.getTime() < a.value && (o = this._getOrReturnCtx(e, o), v(o, { + code: g.too_small, + message: a.message, + inclusive: !0, + exact: !1, + minimum: a.value, + type: "date" + }), n.dirty()) : a.kind === "max" ? e.data.getTime() > a.value && (o = this._getOrReturnCtx(e, o), v(o, { + code: g.too_big, + message: a.message, + inclusive: !0, + exact: !1, + maximum: a.value, + type: "date" + }), n.dirty()) : F.assertNever(a); + return { + status: n.value, + value: new Date(e.data.getTime()) + }; + } + _addCheck(e) { + return new pt({ + ...this._def, + checks: [...this._def.checks, e] + }); + } + min(e, r) { + return this._addCheck({ + kind: "min", + value: e.getTime(), + message: k.toString(r) + }); + } + max(e, r) { + return this._addCheck({ + kind: "max", + value: e.getTime(), + message: k.toString(r) + }); + } + get minDate() { + let e = null; + for (const r of this._def.checks) + r.kind === "min" && (e === null || r.value > e) && (e = r.value); + return e != null ? new Date(e) : null; + } + get maxDate() { + let e = null; + for (const r of this._def.checks) + r.kind === "max" && (e === null || r.value < e) && (e = r.value); + return e != null ? new Date(e) : null; + } +} +pt.create = (t) => new pt({ + checks: [], + coerce: (t == null ? void 0 : t.coerce) || !1, + typeName: C.ZodDate, + ...R(t) +}); +class Ar extends O { + _parse(e) { + if (this._getType(e) !== w.symbol) { + const n = this._getOrReturnCtx(e); + return v(n, { + code: g.invalid_type, + expected: w.symbol, + received: n.parsedType + }), $; + } + return le(e.data); + } +} +Ar.create = (t) => new Ar({ + typeName: C.ZodSymbol, + ...R(t) +}); +class Ht extends O { + _parse(e) { + if (this._getType(e) !== w.undefined) { + const n = this._getOrReturnCtx(e); + return v(n, { + code: g.invalid_type, + expected: w.undefined, + received: n.parsedType + }), $; + } + return le(e.data); + } +} +Ht.create = (t) => new Ht({ + typeName: C.ZodUndefined, + ...R(t) +}); +class Wt extends O { + _parse(e) { + if (this._getType(e) !== w.null) { + const n = this._getOrReturnCtx(e); + return v(n, { + code: g.invalid_type, + expected: w.null, + received: n.parsedType + }), $; + } + return le(e.data); + } +} +Wt.create = (t) => new Wt({ + typeName: C.ZodNull, + ...R(t) +}); +class $t extends O { + constructor() { + super(...arguments), this._any = !0; + } + _parse(e) { + return le(e.data); + } +} +$t.create = (t) => new $t({ + typeName: C.ZodAny, + ...R(t) +}); +class ft extends O { + constructor() { + super(...arguments), this._unknown = !0; + } + _parse(e) { + return le(e.data); + } +} +ft.create = (t) => new ft({ + typeName: C.ZodUnknown, + ...R(t) +}); +class Ve extends O { + _parse(e) { + const r = this._getOrReturnCtx(e); + return v(r, { + code: g.invalid_type, + expected: w.never, + received: r.parsedType + }), $; + } +} +Ve.create = (t) => new Ve({ + typeName: C.ZodNever, + ...R(t) +}); +class Cr extends O { + _parse(e) { + if (this._getType(e) !== w.undefined) { + const n = this._getOrReturnCtx(e); + return v(n, { + code: g.invalid_type, + expected: w.void, + received: n.parsedType + }), $; + } + return le(e.data); + } +} +Cr.create = (t) => new Cr({ + typeName: C.ZodVoid, + ...R(t) +}); +class $e extends O { + _parse(e) { + const { ctx: r, status: n } = this._processInputParams(e), o = this._def; + if (r.parsedType !== w.array) + return v(r, { + code: g.invalid_type, + expected: w.array, + received: r.parsedType + }), $; + if (o.exactLength !== null) { + const i = r.data.length > o.exactLength.value, c = r.data.length < o.exactLength.value; + (i || c) && (v(r, { + code: i ? g.too_big : g.too_small, + minimum: c ? o.exactLength.value : void 0, + maximum: i ? o.exactLength.value : void 0, + type: "array", + inclusive: !0, + exact: !0, + message: o.exactLength.message + }), n.dirty()); + } + if (o.minLength !== null && r.data.length < o.minLength.value && (v(r, { + code: g.too_small, + minimum: o.minLength.value, + type: "array", + inclusive: !0, + exact: !1, + message: o.minLength.message + }), n.dirty()), o.maxLength !== null && r.data.length > o.maxLength.value && (v(r, { + code: g.too_big, + maximum: o.maxLength.value, + type: "array", + inclusive: !0, + exact: !1, + message: o.maxLength.message + }), n.dirty()), r.common.async) + return Promise.all([...r.data].map((i, c) => o.type._parseAsync(new Fe(r, i, r.path, c)))).then((i) => te.mergeArray(n, i)); + const a = [...r.data].map((i, c) => o.type._parseSync(new Fe(r, i, r.path, c))); + return te.mergeArray(n, a); + } + get element() { + return this._def.type; + } + min(e, r) { + return new $e({ + ...this._def, + minLength: { value: e, message: k.toString(r) } + }); + } + max(e, r) { + return new $e({ + ...this._def, + maxLength: { value: e, message: k.toString(r) } + }); + } + length(e, r) { + return new $e({ + ...this._def, + exactLength: { value: e, message: k.toString(r) } + }); + } + nonempty(e) { + return this.min(1, e); + } +} +$e.create = (t, e) => new $e({ + type: t, + minLength: null, + maxLength: null, + exactLength: null, + typeName: C.ZodArray, + ...R(e) +}); +function vt(t) { + if (t instanceof B) { + const e = {}; + for (const r in t.shape) { + const n = t.shape[r]; + e[r] = Le.create(vt(n)); + } + return new B({ + ...t._def, + shape: () => e + }); + } else return t instanceof $e ? new $e({ + ...t._def, + type: vt(t.element) + }) : t instanceof Le ? Le.create(vt(t.unwrap())) : t instanceof nt ? nt.create(vt(t.unwrap())) : t instanceof De ? De.create(t.items.map((e) => vt(e))) : t; +} +class B extends O { + constructor() { + super(...arguments), this._cached = null, this.nonstrict = this.passthrough, this.augment = this.extend; + } + _getCached() { + if (this._cached !== null) + return this._cached; + const e = this._def.shape(), r = F.objectKeys(e); + return this._cached = { shape: e, keys: r }; + } + _parse(e) { + if (this._getType(e) !== w.object) { + const u = this._getOrReturnCtx(e); + return v(u, { + code: g.invalid_type, + expected: w.object, + received: u.parsedType + }), $; + } + const { status: n, ctx: o } = this._processInputParams(e), { shape: a, keys: i } = this._getCached(), c = []; + if (!(this._def.catchall instanceof Ve && this._def.unknownKeys === "strip")) + for (const u in o.data) + i.includes(u) || c.push(u); + const l = []; + for (const u of i) { + const d = a[u], f = o.data[u]; + l.push({ + key: { status: "valid", value: u }, + value: d._parse(new Fe(o, f, o.path, u)), + alwaysSet: u in o.data + }); + } + if (this._def.catchall instanceof Ve) { + const u = this._def.unknownKeys; + if (u === "passthrough") + for (const d of c) + l.push({ + key: { status: "valid", value: d }, + value: { status: "valid", value: o.data[d] } + }); + else if (u === "strict") + c.length > 0 && (v(o, { + code: g.unrecognized_keys, + keys: c + }), n.dirty()); + else if (u !== "strip") throw new Error("Internal ZodObject error: invalid unknownKeys value."); + } else { + const u = this._def.catchall; + for (const d of c) { + const f = o.data[d]; + l.push({ + key: { status: "valid", value: d }, + value: u._parse( + new Fe(o, f, o.path, d) + //, ctx.child(key), value, getParsedType(value) + ), + alwaysSet: d in o.data + }); + } + } + return o.common.async ? Promise.resolve().then(async () => { + const u = []; + for (const d of l) { + const f = await d.key, h = await d.value; + u.push({ + key: f, + value: h, + alwaysSet: d.alwaysSet + }); + } + return u; + }).then((u) => te.mergeObjectSync(n, u)) : te.mergeObjectSync(n, l); + } + get shape() { + return this._def.shape(); + } + strict(e) { + return k.errToObj, new B({ + ...this._def, + unknownKeys: "strict", + ...e !== void 0 ? { + errorMap: (r, n) => { + var o, a, i, c; + const l = (i = (a = (o = this._def).errorMap) === null || a === void 0 ? void 0 : a.call(o, r, n).message) !== null && i !== void 0 ? i : n.defaultError; + return r.code === "unrecognized_keys" ? { + message: (c = k.errToObj(e).message) !== null && c !== void 0 ? c : l + } : { + message: l + }; + } + } : {} + }); + } + strip() { + return new B({ + ...this._def, + unknownKeys: "strip" + }); + } + passthrough() { + return new B({ + ...this._def, + unknownKeys: "passthrough" + }); + } + // const AugmentFactory = + // (def: Def) => + // ( + // augmentation: Augmentation + // ): ZodObject< + // extendShape, Augmentation>, + // Def["unknownKeys"], + // Def["catchall"] + // > => { + // return new ZodObject({ + // ...def, + // shape: () => ({ + // ...def.shape(), + // ...augmentation, + // }), + // }) as any; + // }; + extend(e) { + return new B({ + ...this._def, + shape: () => ({ + ...this._def.shape(), + ...e + }) + }); + } + /** + * Prior to zod@1.0.12 there was a bug in the + * inferred type of merged objects. Please + * upgrade if you are experiencing issues. + */ + merge(e) { + return new B({ + unknownKeys: e._def.unknownKeys, + catchall: e._def.catchall, + shape: () => ({ + ...this._def.shape(), + ...e._def.shape() + }), + typeName: C.ZodObject + }); + } + // merge< + // Incoming extends AnyZodObject, + // Augmentation extends Incoming["shape"], + // NewOutput extends { + // [k in keyof Augmentation | keyof Output]: k extends keyof Augmentation + // ? Augmentation[k]["_output"] + // : k extends keyof Output + // ? Output[k] + // : never; + // }, + // NewInput extends { + // [k in keyof Augmentation | keyof Input]: k extends keyof Augmentation + // ? Augmentation[k]["_input"] + // : k extends keyof Input + // ? Input[k] + // : never; + // } + // >( + // merging: Incoming + // ): ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"], + // NewOutput, + // NewInput + // > { + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + setKey(e, r) { + return this.augment({ [e]: r }); + } + // merge( + // merging: Incoming + // ): //ZodObject = (merging) => { + // ZodObject< + // extendShape>, + // Incoming["_def"]["unknownKeys"], + // Incoming["_def"]["catchall"] + // > { + // // const mergedShape = objectUtil.mergeShapes( + // // this._def.shape(), + // // merging._def.shape() + // // ); + // const merged: any = new ZodObject({ + // unknownKeys: merging._def.unknownKeys, + // catchall: merging._def.catchall, + // shape: () => + // objectUtil.mergeShapes(this._def.shape(), merging._def.shape()), + // typeName: ZodFirstPartyTypeKind.ZodObject, + // }) as any; + // return merged; + // } + catchall(e) { + return new B({ + ...this._def, + catchall: e + }); + } + pick(e) { + const r = {}; + return F.objectKeys(e).forEach((n) => { + e[n] && this.shape[n] && (r[n] = this.shape[n]); + }), new B({ + ...this._def, + shape: () => r + }); + } + omit(e) { + const r = {}; + return F.objectKeys(this.shape).forEach((n) => { + e[n] || (r[n] = this.shape[n]); + }), new B({ + ...this._def, + shape: () => r + }); + } + /** + * @deprecated + */ + deepPartial() { + return vt(this); + } + partial(e) { + const r = {}; + return F.objectKeys(this.shape).forEach((n) => { + const o = this.shape[n]; + e && !e[n] ? r[n] = o : r[n] = o.optional(); + }), new B({ + ...this._def, + shape: () => r + }); + } + required(e) { + const r = {}; + return F.objectKeys(this.shape).forEach((n) => { + if (e && !e[n]) + r[n] = this.shape[n]; + else { + let a = this.shape[n]; + for (; a instanceof Le; ) + a = a._def.innerType; + r[n] = a; + } + }), new B({ + ...this._def, + shape: () => r + }); + } + keyof() { + return Ms(F.objectKeys(this.shape)); + } +} +B.create = (t, e) => new B({ + shape: () => t, + unknownKeys: "strip", + catchall: Ve.create(), + typeName: C.ZodObject, + ...R(e) +}); +B.strictCreate = (t, e) => new B({ + shape: () => t, + unknownKeys: "strict", + catchall: Ve.create(), + typeName: C.ZodObject, + ...R(e) +}); +B.lazycreate = (t, e) => new B({ + shape: t, + unknownKeys: "strip", + catchall: Ve.create(), + typeName: C.ZodObject, + ...R(e) +}); +class qt extends O { + _parse(e) { + const { ctx: r } = this._processInputParams(e), n = this._def.options; + function o(a) { + for (const c of a) + if (c.result.status === "valid") + return c.result; + for (const c of a) + if (c.result.status === "dirty") + return r.common.issues.push(...c.ctx.common.issues), c.result; + const i = a.map((c) => new ge(c.ctx.common.issues)); + return v(r, { + code: g.invalid_union, + unionErrors: i + }), $; + } + if (r.common.async) + return Promise.all(n.map(async (a) => { + const i = { + ...r, + common: { + ...r.common, + issues: [] + }, + parent: null + }; + return { + result: await a._parseAsync({ + data: r.data, + path: r.path, + parent: i + }), + ctx: i + }; + })).then(o); + { + let a; + const i = []; + for (const l of n) { + const u = { + ...r, + common: { + ...r.common, + issues: [] + }, + parent: null + }, d = l._parseSync({ + data: r.data, + path: r.path, + parent: u + }); + if (d.status === "valid") + return d; + d.status === "dirty" && !a && (a = { result: d, ctx: u }), u.common.issues.length && i.push(u.common.issues); + } + if (a) + return r.common.issues.push(...a.ctx.common.issues), a.result; + const c = i.map((l) => new ge(l)); + return v(r, { + code: g.invalid_union, + unionErrors: c + }), $; + } + } + get options() { + return this._def.options; + } +} +qt.create = (t, e) => new qt({ + options: t, + typeName: C.ZodUnion, + ...R(e) +}); +const ze = (t) => t instanceof Jt ? ze(t.schema) : t instanceof Re ? ze(t.innerType()) : t instanceof Xt ? [t.value] : t instanceof rt ? t.options : t instanceof Qt ? F.objectValues(t.enum) : t instanceof er ? ze(t._def.innerType) : t instanceof Ht ? [void 0] : t instanceof Wt ? [null] : t instanceof Le ? [void 0, ...ze(t.unwrap())] : t instanceof nt ? [null, ...ze(t.unwrap())] : t instanceof Fn || t instanceof rr ? ze(t.unwrap()) : t instanceof tr ? ze(t._def.innerType) : []; +class Vr extends O { + _parse(e) { + const { ctx: r } = this._processInputParams(e); + if (r.parsedType !== w.object) + return v(r, { + code: g.invalid_type, + expected: w.object, + received: r.parsedType + }), $; + const n = this.discriminator, o = r.data[n], a = this.optionsMap.get(o); + return a ? r.common.async ? a._parseAsync({ + data: r.data, + path: r.path, + parent: r + }) : a._parseSync({ + data: r.data, + path: r.path, + parent: r + }) : (v(r, { + code: g.invalid_union_discriminator, + options: Array.from(this.optionsMap.keys()), + path: [n] + }), $); + } + get discriminator() { + return this._def.discriminator; + } + get options() { + return this._def.options; + } + get optionsMap() { + return this._def.optionsMap; + } + /** + * The constructor of the discriminated union schema. Its behaviour is very similar to that of the normal z.union() constructor. + * However, it only allows a union of objects, all of which need to share a discriminator property. This property must + * have a different value for each object in the union. + * @param discriminator the name of the discriminator property + * @param types an array of object schemas + * @param params + */ + static create(e, r, n) { + const o = /* @__PURE__ */ new Map(); + for (const a of r) { + const i = ze(a.shape[e]); + if (!i.length) + throw new Error(`A discriminator value for key \`${e}\` could not be extracted from all schema options`); + for (const c of i) { + if (o.has(c)) + throw new Error(`Discriminator property ${String(e)} has duplicate value ${String(c)}`); + o.set(c, a); + } + } + return new Vr({ + typeName: C.ZodDiscriminatedUnion, + discriminator: e, + options: r, + optionsMap: o, + ...R(n) + }); + } +} +function mn(t, e) { + const r = Ye(t), n = Ye(e); + if (t === e) + return { valid: !0, data: t }; + if (r === w.object && n === w.object) { + const o = F.objectKeys(e), a = F.objectKeys(t).filter((c) => o.indexOf(c) !== -1), i = { ...t, ...e }; + for (const c of a) { + const l = mn(t[c], e[c]); + if (!l.valid) + return { valid: !1 }; + i[c] = l.data; + } + return { valid: !0, data: i }; + } else if (r === w.array && n === w.array) { + if (t.length !== e.length) + return { valid: !1 }; + const o = []; + for (let a = 0; a < t.length; a++) { + const i = t[a], c = e[a], l = mn(i, c); + if (!l.valid) + return { valid: !1 }; + o.push(l.data); + } + return { valid: !0, data: o }; + } else return r === w.date && n === w.date && +t == +e ? { valid: !0, data: t } : { valid: !1 }; +} +class Kt extends O { + _parse(e) { + const { status: r, ctx: n } = this._processInputParams(e), o = (a, i) => { + if (pn(a) || pn(i)) + return $; + const c = mn(a.value, i.value); + return c.valid ? ((hn(a) || hn(i)) && r.dirty(), { status: r.value, value: c.data }) : (v(n, { + code: g.invalid_intersection_types + }), $); + }; + return n.common.async ? Promise.all([ + this._def.left._parseAsync({ + data: n.data, + path: n.path, + parent: n + }), + this._def.right._parseAsync({ + data: n.data, + path: n.path, + parent: n + }) + ]).then(([a, i]) => o(a, i)) : o(this._def.left._parseSync({ + data: n.data, + path: n.path, + parent: n + }), this._def.right._parseSync({ + data: n.data, + path: n.path, + parent: n + })); + } +} +Kt.create = (t, e, r) => new Kt({ + left: t, + right: e, + typeName: C.ZodIntersection, + ...R(r) +}); +class De extends O { + _parse(e) { + const { status: r, ctx: n } = this._processInputParams(e); + if (n.parsedType !== w.array) + return v(n, { + code: g.invalid_type, + expected: w.array, + received: n.parsedType + }), $; + if (n.data.length < this._def.items.length) + return v(n, { + code: g.too_small, + minimum: this._def.items.length, + inclusive: !0, + exact: !1, + type: "array" + }), $; + !this._def.rest && n.data.length > this._def.items.length && (v(n, { + code: g.too_big, + maximum: this._def.items.length, + inclusive: !0, + exact: !1, + type: "array" + }), r.dirty()); + const a = [...n.data].map((i, c) => { + const l = this._def.items[c] || this._def.rest; + return l ? l._parse(new Fe(n, i, n.path, c)) : null; + }).filter((i) => !!i); + return n.common.async ? Promise.all(a).then((i) => te.mergeArray(r, i)) : te.mergeArray(r, a); + } + get items() { + return this._def.items; + } + rest(e) { + return new De({ + ...this._def, + rest: e + }); + } +} +De.create = (t, e) => { + if (!Array.isArray(t)) + throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); + return new De({ + items: t, + typeName: C.ZodTuple, + rest: null, + ...R(e) + }); +}; +class Yt extends O { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(e) { + const { status: r, ctx: n } = this._processInputParams(e); + if (n.parsedType !== w.object) + return v(n, { + code: g.invalid_type, + expected: w.object, + received: n.parsedType + }), $; + const o = [], a = this._def.keyType, i = this._def.valueType; + for (const c in n.data) + o.push({ + key: a._parse(new Fe(n, c, n.path, c)), + value: i._parse(new Fe(n, n.data[c], n.path, c)), + alwaysSet: c in n.data + }); + return n.common.async ? te.mergeObjectAsync(r, o) : te.mergeObjectSync(r, o); + } + get element() { + return this._def.valueType; + } + static create(e, r, n) { + return r instanceof O ? new Yt({ + keyType: e, + valueType: r, + typeName: C.ZodRecord, + ...R(n) + }) : new Yt({ + keyType: Ae.create(), + valueType: e, + typeName: C.ZodRecord, + ...R(r) + }); + } +} +class $r extends O { + get keySchema() { + return this._def.keyType; + } + get valueSchema() { + return this._def.valueType; + } + _parse(e) { + const { status: r, ctx: n } = this._processInputParams(e); + if (n.parsedType !== w.map) + return v(n, { + code: g.invalid_type, + expected: w.map, + received: n.parsedType + }), $; + const o = this._def.keyType, a = this._def.valueType, i = [...n.data.entries()].map(([c, l], u) => ({ + key: o._parse(new Fe(n, c, n.path, [u, "key"])), + value: a._parse(new Fe(n, l, n.path, [u, "value"])) + })); + if (n.common.async) { + const c = /* @__PURE__ */ new Map(); + return Promise.resolve().then(async () => { + for (const l of i) { + const u = await l.key, d = await l.value; + if (u.status === "aborted" || d.status === "aborted") + return $; + (u.status === "dirty" || d.status === "dirty") && r.dirty(), c.set(u.value, d.value); + } + return { status: r.value, value: c }; + }); + } else { + const c = /* @__PURE__ */ new Map(); + for (const l of i) { + const u = l.key, d = l.value; + if (u.status === "aborted" || d.status === "aborted") + return $; + (u.status === "dirty" || d.status === "dirty") && r.dirty(), c.set(u.value, d.value); + } + return { status: r.value, value: c }; + } + } +} +$r.create = (t, e, r) => new $r({ + valueType: e, + keyType: t, + typeName: C.ZodMap, + ...R(r) +}); +class ht extends O { + _parse(e) { + const { status: r, ctx: n } = this._processInputParams(e); + if (n.parsedType !== w.set) + return v(n, { + code: g.invalid_type, + expected: w.set, + received: n.parsedType + }), $; + const o = this._def; + o.minSize !== null && n.data.size < o.minSize.value && (v(n, { + code: g.too_small, + minimum: o.minSize.value, + type: "set", + inclusive: !0, + exact: !1, + message: o.minSize.message + }), r.dirty()), o.maxSize !== null && n.data.size > o.maxSize.value && (v(n, { + code: g.too_big, + maximum: o.maxSize.value, + type: "set", + inclusive: !0, + exact: !1, + message: o.maxSize.message + }), r.dirty()); + const a = this._def.valueType; + function i(l) { + const u = /* @__PURE__ */ new Set(); + for (const d of l) { + if (d.status === "aborted") + return $; + d.status === "dirty" && r.dirty(), u.add(d.value); + } + return { status: r.value, value: u }; + } + const c = [...n.data.values()].map((l, u) => a._parse(new Fe(n, l, n.path, u))); + return n.common.async ? Promise.all(c).then((l) => i(l)) : i(c); + } + min(e, r) { + return new ht({ + ...this._def, + minSize: { value: e, message: k.toString(r) } + }); + } + max(e, r) { + return new ht({ + ...this._def, + maxSize: { value: e, message: k.toString(r) } + }); + } + size(e, r) { + return this.min(e, r).max(e, r); + } + nonempty(e) { + return this.min(1, e); + } +} +ht.create = (t, e) => new ht({ + valueType: t, + minSize: null, + maxSize: null, + typeName: C.ZodSet, + ...R(e) +}); +class kt extends O { + constructor() { + super(...arguments), this.validate = this.implement; + } + _parse(e) { + const { ctx: r } = this._processInputParams(e); + if (r.parsedType !== w.function) + return v(r, { + code: g.invalid_type, + expected: w.function, + received: r.parsedType + }), $; + function n(c, l) { + return Tr({ + data: c, + path: r.path, + errorMaps: [ + r.common.contextualErrorMap, + r.schemaErrorMap, + Pr(), + Ct + ].filter((u) => !!u), + issueData: { + code: g.invalid_arguments, + argumentsError: l + } + }); + } + function o(c, l) { + return Tr({ + data: c, + path: r.path, + errorMaps: [ + r.common.contextualErrorMap, + r.schemaErrorMap, + Pr(), + Ct + ].filter((u) => !!u), + issueData: { + code: g.invalid_return_type, + returnTypeError: l + } + }); + } + const a = { errorMap: r.common.contextualErrorMap }, i = r.data; + if (this._def.returns instanceof Rt) { + const c = this; + return le(async function(...l) { + const u = new ge([]), d = await c._def.args.parseAsync(l, a).catch((p) => { + throw u.addIssue(n(l, p)), u; + }), f = await Reflect.apply(i, this, d); + return await c._def.returns._def.type.parseAsync(f, a).catch((p) => { + throw u.addIssue(o(f, p)), u; + }); + }); + } else { + const c = this; + return le(function(...l) { + const u = c._def.args.safeParse(l, a); + if (!u.success) + throw new ge([n(l, u.error)]); + const d = Reflect.apply(i, this, u.data), f = c._def.returns.safeParse(d, a); + if (!f.success) + throw new ge([o(d, f.error)]); + return f.data; + }); + } + } + parameters() { + return this._def.args; + } + returnType() { + return this._def.returns; + } + args(...e) { + return new kt({ + ...this._def, + args: De.create(e).rest(ft.create()) + }); + } + returns(e) { + return new kt({ + ...this._def, + returns: e + }); + } + implement(e) { + return this.parse(e); + } + strictImplement(e) { + return this.parse(e); + } + static create(e, r, n) { + return new kt({ + args: e || De.create([]).rest(ft.create()), + returns: r || ft.create(), + typeName: C.ZodFunction, + ...R(n) + }); + } +} +class Jt extends O { + get schema() { + return this._def.getter(); + } + _parse(e) { + const { ctx: r } = this._processInputParams(e); + return this._def.getter()._parse({ data: r.data, path: r.path, parent: r }); + } +} +Jt.create = (t, e) => new Jt({ + getter: t, + typeName: C.ZodLazy, + ...R(e) +}); +class Xt extends O { + _parse(e) { + if (e.data !== this._def.value) { + const r = this._getOrReturnCtx(e); + return v(r, { + received: r.data, + code: g.invalid_literal, + expected: this._def.value + }), $; + } + return { status: "valid", value: e.data }; + } + get value() { + return this._def.value; + } +} +Xt.create = (t, e) => new Xt({ + value: t, + typeName: C.ZodLiteral, + ...R(e) +}); +function Ms(t, e) { + return new rt({ + values: t, + typeName: C.ZodEnum, + ...R(e) + }); +} +class rt extends O { + constructor() { + super(...arguments), jt.set(this, void 0); + } + _parse(e) { + if (typeof e.data != "string") { + const r = this._getOrReturnCtx(e), n = this._def.values; + return v(r, { + expected: F.joinValues(n), + received: r.parsedType, + code: g.invalid_type + }), $; + } + if (Ir(this, jt) || $s(this, jt, new Set(this._def.values)), !Ir(this, jt).has(e.data)) { + const r = this._getOrReturnCtx(e), n = this._def.values; + return v(r, { + received: r.data, + code: g.invalid_enum_value, + options: n + }), $; + } + return le(e.data); + } + get options() { + return this._def.values; + } + get enum() { + const e = {}; + for (const r of this._def.values) + e[r] = r; + return e; + } + get Values() { + const e = {}; + for (const r of this._def.values) + e[r] = r; + return e; + } + get Enum() { + const e = {}; + for (const r of this._def.values) + e[r] = r; + return e; + } + extract(e, r = this._def) { + return rt.create(e, { + ...this._def, + ...r + }); + } + exclude(e, r = this._def) { + return rt.create(this.options.filter((n) => !e.includes(n)), { + ...this._def, + ...r + }); + } +} +jt = /* @__PURE__ */ new WeakMap(); +rt.create = Ms; +class Qt extends O { + constructor() { + super(...arguments), Zt.set(this, void 0); + } + _parse(e) { + const r = F.getValidEnumValues(this._def.values), n = this._getOrReturnCtx(e); + if (n.parsedType !== w.string && n.parsedType !== w.number) { + const o = F.objectValues(r); + return v(n, { + expected: F.joinValues(o), + received: n.parsedType, + code: g.invalid_type + }), $; + } + if (Ir(this, Zt) || $s(this, Zt, new Set(F.getValidEnumValues(this._def.values))), !Ir(this, Zt).has(e.data)) { + const o = F.objectValues(r); + return v(n, { + received: n.data, + code: g.invalid_enum_value, + options: o + }), $; + } + return le(e.data); + } + get enum() { + return this._def.values; + } +} +Zt = /* @__PURE__ */ new WeakMap(); +Qt.create = (t, e) => new Qt({ + values: t, + typeName: C.ZodNativeEnum, + ...R(e) +}); +class Rt extends O { + unwrap() { + return this._def.type; + } + _parse(e) { + const { ctx: r } = this._processInputParams(e); + if (r.parsedType !== w.promise && r.common.async === !1) + return v(r, { + code: g.invalid_type, + expected: w.promise, + received: r.parsedType + }), $; + const n = r.parsedType === w.promise ? r.data : Promise.resolve(r.data); + return le(n.then((o) => this._def.type.parseAsync(o, { + path: r.path, + errorMap: r.common.contextualErrorMap + }))); + } +} +Rt.create = (t, e) => new Rt({ + type: t, + typeName: C.ZodPromise, + ...R(e) +}); +class Re extends O { + innerType() { + return this._def.schema; + } + sourceType() { + return this._def.schema._def.typeName === C.ZodEffects ? this._def.schema.sourceType() : this._def.schema; + } + _parse(e) { + const { status: r, ctx: n } = this._processInputParams(e), o = this._def.effect || null, a = { + addIssue: (i) => { + v(n, i), i.fatal ? r.abort() : r.dirty(); + }, + get path() { + return n.path; + } + }; + if (a.addIssue = a.addIssue.bind(a), o.type === "preprocess") { + const i = o.transform(n.data, a); + if (n.common.async) + return Promise.resolve(i).then(async (c) => { + if (r.value === "aborted") + return $; + const l = await this._def.schema._parseAsync({ + data: c, + path: n.path, + parent: n + }); + return l.status === "aborted" ? $ : l.status === "dirty" || r.value === "dirty" ? bt(l.value) : l; + }); + { + if (r.value === "aborted") + return $; + const c = this._def.schema._parseSync({ + data: i, + path: n.path, + parent: n + }); + return c.status === "aborted" ? $ : c.status === "dirty" || r.value === "dirty" ? bt(c.value) : c; + } + } + if (o.type === "refinement") { + const i = (c) => { + const l = o.refinement(c, a); + if (n.common.async) + return Promise.resolve(l); + if (l instanceof Promise) + throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); + return c; + }; + if (n.common.async === !1) { + const c = this._def.schema._parseSync({ + data: n.data, + path: n.path, + parent: n + }); + return c.status === "aborted" ? $ : (c.status === "dirty" && r.dirty(), i(c.value), { status: r.value, value: c.value }); + } else + return this._def.schema._parseAsync({ data: n.data, path: n.path, parent: n }).then((c) => c.status === "aborted" ? $ : (c.status === "dirty" && r.dirty(), i(c.value).then(() => ({ status: r.value, value: c.value })))); + } + if (o.type === "transform") + if (n.common.async === !1) { + const i = this._def.schema._parseSync({ + data: n.data, + path: n.path, + parent: n + }); + if (!Bt(i)) + return i; + const c = o.transform(i.value, a); + if (c instanceof Promise) + throw new Error("Asynchronous transform encountered during synchronous parse operation. Use .parseAsync instead."); + return { status: r.value, value: c }; + } else + return this._def.schema._parseAsync({ data: n.data, path: n.path, parent: n }).then((i) => Bt(i) ? Promise.resolve(o.transform(i.value, a)).then((c) => ({ status: r.value, value: c })) : i); + F.assertNever(o); + } +} +Re.create = (t, e, r) => new Re({ + schema: t, + typeName: C.ZodEffects, + effect: e, + ...R(r) +}); +Re.createWithPreprocess = (t, e, r) => new Re({ + schema: e, + effect: { type: "preprocess", transform: t }, + typeName: C.ZodEffects, + ...R(r) +}); +class Le extends O { + _parse(e) { + return this._getType(e) === w.undefined ? le(void 0) : this._def.innerType._parse(e); + } + unwrap() { + return this._def.innerType; + } +} +Le.create = (t, e) => new Le({ + innerType: t, + typeName: C.ZodOptional, + ...R(e) +}); +class nt extends O { + _parse(e) { + return this._getType(e) === w.null ? le(null) : this._def.innerType._parse(e); + } + unwrap() { + return this._def.innerType; + } +} +nt.create = (t, e) => new nt({ + innerType: t, + typeName: C.ZodNullable, + ...R(e) +}); +class er extends O { + _parse(e) { + const { ctx: r } = this._processInputParams(e); + let n = r.data; + return r.parsedType === w.undefined && (n = this._def.defaultValue()), this._def.innerType._parse({ + data: n, + path: r.path, + parent: r + }); + } + removeDefault() { + return this._def.innerType; + } +} +er.create = (t, e) => new er({ + innerType: t, + typeName: C.ZodDefault, + defaultValue: typeof e.default == "function" ? e.default : () => e.default, + ...R(e) +}); +class tr extends O { + _parse(e) { + const { ctx: r } = this._processInputParams(e), n = { + ...r, + common: { + ...r.common, + issues: [] + } + }, o = this._def.innerType._parse({ + data: n.data, + path: n.path, + parent: { + ...n + } + }); + return Gt(o) ? o.then((a) => ({ + status: "valid", + value: a.status === "valid" ? a.value : this._def.catchValue({ + get error() { + return new ge(n.common.issues); + }, + input: n.data + }) + })) : { + status: "valid", + value: o.status === "valid" ? o.value : this._def.catchValue({ + get error() { + return new ge(n.common.issues); + }, + input: n.data + }) + }; + } + removeCatch() { + return this._def.innerType; + } +} +tr.create = (t, e) => new tr({ + innerType: t, + typeName: C.ZodCatch, + catchValue: typeof e.catch == "function" ? e.catch : () => e.catch, + ...R(e) +}); +class Rr extends O { + _parse(e) { + if (this._getType(e) !== w.nan) { + const n = this._getOrReturnCtx(e); + return v(n, { + code: g.invalid_type, + expected: w.nan, + received: n.parsedType + }), $; + } + return { status: "valid", value: e.data }; + } +} +Rr.create = (t) => new Rr({ + typeName: C.ZodNaN, + ...R(t) +}); +const zc = Symbol("zod_brand"); +class Fn extends O { + _parse(e) { + const { ctx: r } = this._processInputParams(e), n = r.data; + return this._def.type._parse({ + data: n, + path: r.path, + parent: r + }); + } + unwrap() { + return this._def.type; + } +} +class cr extends O { + _parse(e) { + const { status: r, ctx: n } = this._processInputParams(e); + if (n.common.async) + return (async () => { + const a = await this._def.in._parseAsync({ + data: n.data, + path: n.path, + parent: n + }); + return a.status === "aborted" ? $ : a.status === "dirty" ? (r.dirty(), bt(a.value)) : this._def.out._parseAsync({ + data: a.value, + path: n.path, + parent: n + }); + })(); + { + const o = this._def.in._parseSync({ + data: n.data, + path: n.path, + parent: n + }); + return o.status === "aborted" ? $ : o.status === "dirty" ? (r.dirty(), { + status: "dirty", + value: o.value + }) : this._def.out._parseSync({ + data: o.value, + path: n.path, + parent: n + }); + } + } + static create(e, r) { + return new cr({ + in: e, + out: r, + typeName: C.ZodPipeline + }); + } +} +class rr extends O { + _parse(e) { + const r = this._def.innerType._parse(e), n = (o) => (Bt(o) && (o.value = Object.freeze(o.value)), o); + return Gt(r) ? r.then((o) => n(o)) : n(r); + } + unwrap() { + return this._def.innerType; + } +} +rr.create = (t, e) => new rr({ + innerType: t, + typeName: C.ZodReadonly, + ...R(e) +}); +function Ls(t, e = {}, r) { + return t ? $t.create().superRefine((n, o) => { + var a, i; + if (!t(n)) { + const c = typeof e == "function" ? e(n) : typeof e == "string" ? { message: e } : e, l = (i = (a = c.fatal) !== null && a !== void 0 ? a : r) !== null && i !== void 0 ? i : !0, u = typeof c == "string" ? { message: c } : c; + o.addIssue({ code: "custom", ...u, fatal: l }); + } + }) : $t.create(); +} +const Bc = { + object: B.lazycreate +}; +var C; +(function(t) { + t.ZodString = "ZodString", t.ZodNumber = "ZodNumber", t.ZodNaN = "ZodNaN", t.ZodBigInt = "ZodBigInt", t.ZodBoolean = "ZodBoolean", t.ZodDate = "ZodDate", t.ZodSymbol = "ZodSymbol", t.ZodUndefined = "ZodUndefined", t.ZodNull = "ZodNull", t.ZodAny = "ZodAny", t.ZodUnknown = "ZodUnknown", t.ZodNever = "ZodNever", t.ZodVoid = "ZodVoid", t.ZodArray = "ZodArray", t.ZodObject = "ZodObject", t.ZodUnion = "ZodUnion", t.ZodDiscriminatedUnion = "ZodDiscriminatedUnion", t.ZodIntersection = "ZodIntersection", t.ZodTuple = "ZodTuple", t.ZodRecord = "ZodRecord", t.ZodMap = "ZodMap", t.ZodSet = "ZodSet", t.ZodFunction = "ZodFunction", t.ZodLazy = "ZodLazy", t.ZodLiteral = "ZodLiteral", t.ZodEnum = "ZodEnum", t.ZodEffects = "ZodEffects", t.ZodNativeEnum = "ZodNativeEnum", t.ZodOptional = "ZodOptional", t.ZodNullable = "ZodNullable", t.ZodDefault = "ZodDefault", t.ZodCatch = "ZodCatch", t.ZodPromise = "ZodPromise", t.ZodBranded = "ZodBranded", t.ZodPipeline = "ZodPipeline", t.ZodReadonly = "ZodReadonly"; +})(C || (C = {})); +const Gc = (t, e = { + message: `Input not instance of ${t.name}` +}) => Ls((r) => r instanceof t, e), Fs = Ae.create, Ds = et.create, Vc = Rr.create, Hc = tt.create, Us = Vt.create, Wc = pt.create, qc = Ar.create, Kc = Ht.create, Yc = Wt.create, Jc = $t.create, Xc = ft.create, Qc = Ve.create, el = Cr.create, tl = $e.create, rl = B.create, nl = B.strictCreate, ol = qt.create, sl = Vr.create, al = Kt.create, il = De.create, cl = Yt.create, ll = $r.create, ul = ht.create, dl = kt.create, fl = Jt.create, pl = Xt.create, hl = rt.create, ml = Qt.create, gl = Rt.create, go = Re.create, yl = Le.create, _l = nt.create, vl = Re.createWithPreprocess, bl = cr.create, wl = () => Fs().optional(), xl = () => Ds().optional(), Sl = () => Us().optional(), El = { + string: (t) => Ae.create({ ...t, coerce: !0 }), + number: (t) => et.create({ ...t, coerce: !0 }), + boolean: (t) => Vt.create({ + ...t, + coerce: !0 + }), + bigint: (t) => tt.create({ ...t, coerce: !0 }), + date: (t) => pt.create({ ...t, coerce: !0 }) +}, kl = $; +var q = /* @__PURE__ */ Object.freeze({ + __proto__: null, + defaultErrorMap: Ct, + setErrorMap: kc, + getErrorMap: Pr, + makeIssue: Tr, + EMPTY_PATH: Pc, + addIssueToContext: v, + ParseStatus: te, + INVALID: $, + DIRTY: bt, + OK: le, + isAborted: pn, + isDirty: hn, + isValid: Bt, + isAsync: Gt, + get util() { + return F; + }, + get objectUtil() { + return fn; + }, + ZodParsedType: w, + getParsedType: Ye, + ZodType: O, + datetimeRegex: Os, + ZodString: Ae, + ZodNumber: et, + ZodBigInt: tt, + ZodBoolean: Vt, + ZodDate: pt, + ZodSymbol: Ar, + ZodUndefined: Ht, + ZodNull: Wt, + ZodAny: $t, + ZodUnknown: ft, + ZodNever: Ve, + ZodVoid: Cr, + ZodArray: $e, + ZodObject: B, + ZodUnion: qt, + ZodDiscriminatedUnion: Vr, + ZodIntersection: Kt, + ZodTuple: De, + ZodRecord: Yt, + ZodMap: $r, + ZodSet: ht, + ZodFunction: kt, + ZodLazy: Jt, + ZodLiteral: Xt, + ZodEnum: rt, + ZodNativeEnum: Qt, + ZodPromise: Rt, + ZodEffects: Re, + ZodTransformer: Re, + ZodOptional: Le, + ZodNullable: nt, + ZodDefault: er, + ZodCatch: tr, + ZodNaN: Rr, + BRAND: zc, + ZodBranded: Fn, + ZodPipeline: cr, + ZodReadonly: rr, + custom: Ls, + Schema: O, + ZodSchema: O, + late: Bc, + get ZodFirstPartyTypeKind() { + return C; + }, + coerce: El, + any: Jc, + array: tl, + bigint: Hc, + boolean: Us, + date: Wc, + discriminatedUnion: sl, + effect: go, + enum: hl, + function: dl, + instanceof: Gc, + intersection: al, + lazy: fl, + literal: pl, + map: ll, + nan: Vc, + nativeEnum: ml, + never: Qc, + null: Yc, + nullable: _l, + number: Ds, + object: rl, + oboolean: Sl, + onumber: xl, + optional: yl, + ostring: wl, + pipeline: bl, + preprocess: vl, + promise: gl, + record: cl, + set: ul, + strictObject: nl, + string: Fs, + symbol: qc, + transformer: go, + tuple: il, + undefined: Kc, + union: ol, + unknown: Xc, + void: el, + NEVER: kl, + ZodIssueCode: g, + quotelessJson: Ec, + ZodError: ge +}); +const Pl = q.object({ + pluginId: q.string(), + name: q.string(), + host: q.string().url(), + code: q.string(), + icon: q.string().optional(), + description: q.string().max(200).optional(), + permissions: q.array( + q.enum([ + "content:read", + "content:write", + "library:read", + "library:write", + "user:read" + ]) + ) +}); +function js(t, e) { + return new URL(e, t).toString(); +} +function Tl(t) { + return fetch(t).then((e) => e.json()).then((e) => { + if (!Pl.safeParse(e).success) + throw new Error("Invalid plugin manifest"); + return e; + }).catch((e) => { + throw console.error(e), e; + }); +} +function yo(t) { + return !t.host && !t.code.startsWith("http") ? Promise.resolve(t.code) : fetch(js(t.host, t.code)).then((e) => { + if (e.ok) + return e.text(); + throw new Error("Failed to load plugin code"); + }); +} +const Il = q.object({ + width: q.number().positive(), + height: q.number().positive() +}); +function Al(t, e, r, n) { + const o = document.createElement("plugin-modal"); + o.setTheme(r); + const a = 200, i = 200, c = 335, l = 590, u = { + blockStart: 40, + inlineEnd: 320 + }; + o.style.setProperty( + "--modal-block-start", + `${u.blockStart}px` + ), o.style.setProperty( + "--modal-inline-end", + `${u.inlineEnd}px` + ); + const d = window.innerWidth - u.inlineEnd, f = window.innerHeight - u.blockStart; + let h = Math.min((n == null ? void 0 : n.width) || c, d), p = Math.min((n == null ? void 0 : n.height) || l, f); + return h = Math.max(h, a), p = Math.max(p, i), o.setAttribute("title", t), o.setAttribute("iframe-src", e), o.setAttribute("width", String(h)), o.setAttribute("height", String(p)), document.body.appendChild(o), o; +} +const Cl = q.function().args( + q.string(), + q.string(), + q.enum(["dark", "light"]), + Il.optional() +).implement((t, e, r, n) => Al(t, e, r, n)); +async function $l(t, e, r, n) { + let o = await yo(e), a = !1, i = !1, c = null, l = []; + const u = /* @__PURE__ */ new Set(), d = t.addListener("themechange", (A) => { + c == null || c.setTheme(A); + }), f = t.addListener("finish", () => { + m(), t == null || t.removeListener(f); + }); + let h = {}; + const p = () => { + t.removeListener(d), Object.entries(h).forEach(([, A]) => { + A.forEach((N) => { + E(N); + }); + }), l = [], h = {}; + }, m = () => { + p(), u.forEach(clearTimeout), u.clear(), c && (c.removeEventListener("close", m), c.remove(), c = null), i = !0, r(); + }, b = async () => { + if (!a) { + a = !0; + return; + } + p(), o = await yo(e), n(o); + }, S = (A, N, D) => { + const M = t.getTheme(), J = js(e.host, N); + (c == null ? void 0 : c.getAttribute("iframe-src")) !== J && (c = Cl(A, J, M, D), c.setTheme(M), c.addEventListener("close", m, { + once: !0 + }), c.addEventListener("load", b)); + }, x = (A) => { + l.push(A); + }, I = (A, N, D) => { + const M = t.addListener( + A, + (...J) => { + i || N(...J); + }, + D + ); + return h[A] || (h[A] = /* @__PURE__ */ new Map()), h[A].set(N, M), M; + }, E = (A, N) => { + let D; + typeof A == "symbol" ? D = A : N && (D = h[A].get(N)), D && t.removeListener(D); + }; + return { + close: m, + destroyListener: E, + openModal: S, + getModal: () => c, + registerListener: I, + registerMessageCallback: x, + sendMessage: (A) => { + l.forEach((N) => N(A)); + }, + get manifest() { + return e; + }, + get context() { + return t; + }, + get timeouts() { + return u; + }, + get code() { + return o; + } + }; +} +const Rl = [ + "finish", + "pagechange", + "filechange", + "selectionchange", + "themechange", + "shapechange", + "contentsave" +]; +function Nl(t) { + const e = (n) => { + if (!t.manifest.permissions.includes(n)) + throw new Error(`Permission ${n} is not granted`); + }; + return { + penpot: { + ui: { + open: (n, o, a) => { + t.openModal(n, o, a); + }, + sendMessage(n) { + var a; + const o = new CustomEvent("message", { + detail: n + }); + (a = t.getModal()) == null || a.dispatchEvent(o); + }, + onMessage: (n) => { + q.function().parse(n), t.registerMessageCallback(n); + } + }, + utils: { + geometry: { + center(n) { + return window.app.plugins.public_utils.centerShapes(n); + } + }, + types: { + isBoard(n) { + return n.type === "board"; + }, + isGroup(n) { + return n.type === "group"; + }, + isMask(n) { + return n.type === "group" && n.isMask(); + }, + isBool(n) { + return n.type === "boolean"; + }, + isRectangle(n) { + return n.type === "rectangle"; + }, + isPath(n) { + return n.type === "path"; + }, + isText(n) { + return n.type === "text"; + }, + isEllipse(n) { + return n.type === "ellipse"; + }, + isSVG(n) { + return n.type === "svg-raw"; + } + } + }, + closePlugin: () => { + t.close(); + }, + on(n, o, a) { + return q.enum(Rl).parse(n), q.function().parse(o), e("content:read"), t.registerListener(n, o, a); + }, + off(n, o) { + t.destroyListener(n, o); + }, + // Penpot State API + get root() { + return e("content:read"), t.context.root; + }, + get currentPage() { + return e("content:read"), t.context.currentPage; + }, + get selection() { + return e("content:read"), t.context.selection; + }, + set selection(n) { + e("content:read"), t.context.selection = n; + }, + get viewport() { + return t.context.viewport; + }, + get history() { + return t.context.history; + }, + get library() { + return e("library:read"), t.context.library; + }, + get fonts() { + return e("content:read"), t.context.fonts; + }, + get currentUser() { + return e("user:read"), t.context.currentUser; + }, + get activeUsers() { + return e("user:read"), t.context.activeUsers; + }, + getFile() { + return e("content:read"), t.context.getFile(); + }, + getPage() { + return e("content:read"), t.context.getPage(); + }, + getSelected() { + return e("content:read"), t.context.getSelected(); + }, + getSelectedShapes() { + return e("content:read"), t.context.getSelectedShapes(); + }, + shapesColors(n) { + return e("content:read"), t.context.shapesColors(n); + }, + replaceColor(n, o, a) { + return e("content:write"), t.context.replaceColor(n, o, a); + }, + getTheme() { + return t.context.getTheme(); + }, + createBoard() { + return e("content:write"), t.context.createBoard(); + }, + createRectangle() { + return e("content:write"), t.context.createRectangle(); + }, + createEllipse() { + return e("content:write"), t.context.createEllipse(); + }, + createText(n) { + return e("content:write"), t.context.createText(n); + }, + createPath() { + return e("content:write"), t.context.createPath(); + }, + createBoolean(n, o) { + return e("content:write"), t.context.createBoolean(n, o); + }, + createShapeFromSvg(n) { + return e("content:write"), t.context.createShapeFromSvg(n); + }, + group(n) { + return e("content:write"), t.context.group(n); + }, + ungroup(n, ...o) { + e("content:write"), t.context.ungroup(n, ...o); + }, + uploadMediaUrl(n, o) { + return e("content:write"), t.context.uploadMediaUrl(n, o); + }, + uploadMediaData(n, o, a) { + return e("content:write"), t.context.uploadMediaData(n, o, a); + }, + generateMarkup(n, o) { + return e("content:read"), t.context.generateMarkup(n, o); + }, + generateStyle(n, o) { + return e("content:read"), t.context.generateStyle(n, o); + }, + openViewer() { + e("content:read"), t.context.openViewer(); + }, + createPage() { + return e("content:write"), t.context.createPage(); + }, + openPage(n) { + e("content:read"), t.context.openPage(n); + } + } + }; +} +let _o = !1; +const Ke = { + hardenIntrinsics: () => { + _o || (_o = !0, hardenIntrinsics()); + }, + createCompartment: (t) => new Compartment(t), + harden: (t) => harden(t) +}; +function Ol(t) { + Ke.hardenIntrinsics(); + const e = Nl(t), r = { + penpot: Ke.harden(e.penpot), + fetch: Ke.harden((...o) => { + const a = { + ...o[1], + credentials: "omit" + }; + return fetch(o[0], a); + }), + console: Ke.harden(window.console), + Math: Ke.harden(Math), + setTimeout: Ke.harden( + (...[o, a]) => { + const i = setTimeout(() => { + o(); + }, a); + return t.timeouts.add(i), i; + } + ), + clearTimeout: Ke.harden((o) => { + clearTimeout(o), t.timeouts.delete(o); + }) + }, n = Ke.createCompartment(r); + return { + evaluate: () => { + n.evaluate(t.code); + }, + cleanGlobalThis: () => { + Object.keys(r).forEach((o) => { + delete n.globalThis[o]; + }); + }, + compartment: n + }; +} +async function Ml(t, e, r) { + const n = await $l( + t, + e, + function() { + o.cleanGlobalThis(), r(); + }, + function() { + o.evaluate(); + } + ), o = Ol(n); + return o.evaluate(), { + plugin: n, + compartment: o + }; +} +let Pt = [], gn = null; +function Ll(t) { + gn = t; +} +const vo = () => { + Pt.forEach((t) => { + t.plugin.close(); + }), Pt = []; +}; +window.addEventListener("message", (t) => { + try { + for (const e of Pt) + e.plugin.sendMessage(t.data); + } catch (e) { + console.error(e); + } +}); +const Fl = async function(t) { + try { + const e = gn && gn(t.pluginId); + if (!e) + return; + vo(); + const r = await Ml(e, t, () => { + Pt = Pt.filter((n) => n !== r); + }); + Pt.push(r); + } catch (e) { + vo(), console.error(e); + } +}, Zs = async function(t) { + Fl(t); +}, Dl = async function(t) { + const e = await Tl(t); + Zs(e); +}; +console.log("%c[PLUGINS] Loading plugin system", "color: #008d7c"); +repairIntrinsics({ + evalTaming: "unsafeEval", + stackFiltering: "verbose", + errorTaming: "unsafe", + consoleTaming: "unsafe", + errorTrapping: "none" +}); +const bo = globalThis; +bo.initPluginsRuntime = (t) => { + try { + console.log("%c[PLUGINS] Initialize runtime", "color: #008d7c"), Ll(t), bo.ɵcontext = t("TEST"), globalThis.ɵloadPlugin = Zs, globalThis.ɵloadPluginByUrl = Dl; + } catch (e) { + console.error(e); + } +}; +//# sourceMappingURL=index.js.map diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss index fd1e4ecb0fa0cfa14677c98fc295d64e49b0dd17..078593de0f9f1ee96eae22b5846d002de9543b79 100644 --- a/frontend/resources/styles/common/base.scss +++ b/frontend/resources/styles/common/base.scss @@ -4,16 +4,26 @@ // // Copyright (c) KALEIDOS INC -:root { - --font-family: "worksans", sans-serif; -} +// TODO: Legacy sass vars. We should use DS tokens. +$color-gray-50: #303236; +$fs12: 0.75rem; +$fs14: 0.875rem; +$fs18: 1.125rem; +$fs24: 1.5rem; +$fs34: 2.125rem; +$fs44: 2.75rem; +$fw300: 300; +$fw500: 500; +$lh-115: 1.15; +$lh-133: 1.33; +$size-4: 1rem; body { - background-color: lighten($color-gray-10, 5%); - color: $color-gray-20; + background-color: var(--color-background-primary); + color: var(--color-foreground-primary); display: flex; flex-direction: column; - font-family: var(--font-family); + font-family: "worksans", "vazirmatn", sans-serif; width: 100vw; height: 100vh; overflow: hidden; @@ -28,18 +38,13 @@ body { * { box-sizing: border-box; - // transition: all .4s ease; + scrollbar-width: thin; } -.global-zeroclipboard-container { - transition: none; - - #global-zeroclipboard-flash-bridge { - transition: none; - } - - object { - transition: none; +// Firefox-only hack +@-moz-document url-prefix() { + * { + scrollbar-width: auto; } } @@ -65,70 +70,33 @@ a { } button { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; } p { font-size: $fs12; margin-bottom: 1rem; line-height: $lh-133; - - @include bp(baby-bear) { - font-size: $fs16; - line-height: $lh-143; - } } li { line-height: $lh-133; - - @include bp(baby-bear) { - line-height: $lh-143; - } } ul { margin-bottom: 1rem; } -strong { - font-weight: $fw700; -} - -.relative { - position: relative; -} - h1 { font-size: $fs34; font-weight: $fw500; line-height: $lh-115; - - @include bp(baby-bear) { - font-size: $fs38; - line-height: $lh-125; - } - - &.supertitle { - font-size: $fs44; - font-weight: $fw300; - line-height: $lh-115; - - @include bp(baby-bear) { - font-size: $fs44; - line-height: $lh-125; - } - } } + h2 { font-size: $fs24; font-weight: $fw300; line-height: $lh-115; - - @include bp(baby-bear) { - font-size: $fs32; - line-height: $lh-125; - } } h3 { @@ -142,106 +110,8 @@ h4 { font-weight: $fw300; } -@-webkit-keyframes rotation { - from { - -webkit-transform: rotate(0deg); - } - to { - -webkit-transform: rotate(359deg); - } -} - -@-webkit-keyframes rotation-negative { - from { - -webkit-transform: rotate(0deg); - } - to { - -webkit-transform: rotate(-359deg); - } -} - -@keyframes tooltipAppear { - 0% { - opacity: 0; - display: none; - } - 1% { - display: block; - opacity: 0; - left: 3rem; - } - 100% { - opacity: 1; - left: 2rem; - } -} - -@keyframes show { - 0% { - opacity: 0; - display: none; - } - 1% { - display: block; - opacity: 0; - } - 100% { - opacity: 1; - } -} - -@keyframes hide { - 0% { - opacity: 1; - display: block; - } - 99% { - opacity: 0; - display: block; - } - 100% { - display: none; - } -} - -.hide { - display: none !important; - transition: all 0.5s ease; -} - -.visuallyHidden { - opacity: 0 !important; - transition: all 0.5s ease; -} - -.show { - animation: show 0.4s linear; - display: block !important; -} - -.center { - margin: 0 auto; - text-align: center; -} - -.hidden-input { - display: none; -} - -.bold { - font-weight: $fw700 !important; -} - -.nopd { - padding: 0 !important; -} - -.move-cursor { - cursor: move; -} - hr { - border-top: solid 1px $color-gray-60; + border-top: solid 1px var(--color-background-primary); border-right: 0; border-left: 0; border-bottom: 0; @@ -263,7 +133,22 @@ input[type="number"] { user-select: text; } -[data-hidden="true"] { - display: none; - pointer-events: none; +input, +select { + box-sizing: border-box; + font-family: "worksans", "vazirmatn", sans-serif; + font-size: $fs14; + margin-bottom: $size-4; + -webkit-appearance: none; + -moz-appearance: none; +} + +[draggable] { + -moz-user-select: none; + -khtml-user-select: none; + -webkit-user-select: none; + user-select: none; + /* Required to make elements draggable in old WebKit */ + -khtml-user-drag: element; + -webkit-user-drag: element; } diff --git a/frontend/resources/styles/common/refactor/themes/hljs-dark-theme.scss b/frontend/resources/styles/common/dependencies/_hljs-dark-theme.scss similarity index 100% rename from frontend/resources/styles/common/refactor/themes/hljs-dark-theme.scss rename to frontend/resources/styles/common/dependencies/_hljs-dark-theme.scss diff --git a/frontend/resources/styles/common/refactor/themes/hljs-light-theme.scss b/frontend/resources/styles/common/dependencies/_hljs-light-theme.scss similarity index 100% rename from frontend/resources/styles/common/refactor/themes/hljs-light-theme.scss rename to frontend/resources/styles/common/dependencies/_hljs-light-theme.scss diff --git a/frontend/resources/styles/common/dependencies/animations.scss b/frontend/resources/styles/common/dependencies/animations.scss index 8b9a0fb0313177c8db9dcc61eb9b88040d1c2375..ea30c21e10284628c917b020549de2c6fbd0352b 100644 --- a/frontend/resources/styles/common/dependencies/animations.scss +++ b/frontend/resources/styles/common/dependencies/animations.scss @@ -6,3429 +6,84 @@ * Copyright (c) 2016 Daniel Eden */ -@mixin animation($delay, $duration, $animation) { - -webkit-animation-delay: $delay; - -webkit-animation-duration: $duration; - -webkit-animation-name: $animation; - -webkit-animation-fill-mode: both; - - -moz-animation-delay: $delay; - -moz-animation-duration: $duration; - -moz-animation-name: $animation; - -moz-animation-fill-mode: both; - - animation-delay: $delay; - animation-duration: $duration; - animation-name: $animation; - animation-fill-mode: both; -} - -.animated { - -webkit-animation-duration: 1s; - animation-duration: 1s; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; -} - -.animated.infinite { - -webkit-animation-iteration-count: infinite; - animation-iteration-count: infinite; -} - -.animated.hinge { - -webkit-animation-duration: 2s; - animation-duration: 2s; -} - -.animated.bounceIn, -.animated.bounceOut { - -webkit-animation-duration: 0.75s; - animation-duration: 0.75s; -} - -.animated.flipOutX, -.animated.flipOutY { - -webkit-animation-duration: 0.75s; - animation-duration: 0.75s; -} - -@-webkit-keyframes bounce { - 0%, - 20%, - 53%, - 80%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 40%, - 43% { - -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - -webkit-transform: translate3d(0, -30px, 0); - transform: translate3d(0, -30px, 0); - } - - 70% { - -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - -webkit-transform: translate3d(0, -15px, 0); - transform: translate3d(0, -15px, 0); - } - - 90% { - -webkit-transform: translate3d(0, -4px, 0); - transform: translate3d(0, -4px, 0); - } -} - -@keyframes bounce { - 0%, - 20%, - 53%, - 80%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 40%, - 43% { - -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - -webkit-transform: translate3d(0, -30px, 0); - transform: translate3d(0, -30px, 0); - } - - 70% { - -webkit-animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - -webkit-transform: translate3d(0, -15px, 0); - transform: translate3d(0, -15px, 0); - } - - 90% { - -webkit-transform: translate3d(0, -4px, 0); - transform: translate3d(0, -4px, 0); - } -} - -.bounce { - -webkit-animation-name: bounce; - animation-name: bounce; - -webkit-transform-origin: center bottom; - transform-origin: center bottom; -} - -@-webkit-keyframes flash { - 0%, - 50%, - 100% { - opacity: 1; - } - - 25%, - 75% { - opacity: 0; - } -} - -@keyframes flash { - 0%, - 50%, - 100% { - opacity: 1; - } - - 25%, - 75% { - opacity: 0; - } -} - -.flash { - -webkit-animation-name: flash; - animation-name: flash; -} - -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes pulse { - 0% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 50% { - -webkit-transform: scale3d(1.05, 1.05, 1.05); - transform: scale3d(1.05, 1.05, 1.05); - } - - 100% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -@keyframes pulse { - 0% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 50% { - -webkit-transform: scale3d(1.05, 1.05, 1.05); - transform: scale3d(1.05, 1.05, 1.05); - } - - 100% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -.pulse { - -webkit-animation-name: pulse; - animation-name: pulse; -} - -@-webkit-keyframes rubberBand { - 0% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 30% { - -webkit-transform: scale3d(1.25, 0.75, 1); - transform: scale3d(1.25, 0.75, 1); - } - - 40% { - -webkit-transform: scale3d(0.75, 1.25, 1); - transform: scale3d(0.75, 1.25, 1); - } - - 50% { - -webkit-transform: scale3d(1.15, 0.85, 1); - transform: scale3d(1.15, 0.85, 1); - } - - 65% { - -webkit-transform: scale3d(0.95, 1.05, 1); - transform: scale3d(0.95, 1.05, 1); - } - - 75% { - -webkit-transform: scale3d(1.05, 0.95, 1); - transform: scale3d(1.05, 0.95, 1); - } - - 100% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -@keyframes rubberBand { - 0% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 30% { - -webkit-transform: scale3d(1.25, 0.75, 1); - transform: scale3d(1.25, 0.75, 1); - } - - 40% { - -webkit-transform: scale3d(0.75, 1.25, 1); - transform: scale3d(0.75, 1.25, 1); - } - - 50% { - -webkit-transform: scale3d(1.15, 0.85, 1); - transform: scale3d(1.15, 0.85, 1); - } - - 65% { - -webkit-transform: scale3d(0.95, 1.05, 1); - transform: scale3d(0.95, 1.05, 1); - } - - 75% { - -webkit-transform: scale3d(1.05, 0.95, 1); - transform: scale3d(1.05, 0.95, 1); - } - - 100% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -.rubberBand { - -webkit-animation-name: rubberBand; - animation-name: rubberBand; -} - -@-webkit-keyframes shake { - 0%, - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 10%, - 30%, - 50%, - 70%, - 90% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } - - 20%, - 40%, - 60%, - 80% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } -} - -@keyframes shake { - 0%, - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 10%, - 30%, - 50%, - 70%, - 90% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } - - 20%, - 40%, - 60%, - 80% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } -} - -.shake { - -webkit-animation-name: shake; - animation-name: shake; -} - -@-webkit-keyframes swing { - 20% { - -webkit-transform: rotate3d(0, 0, 1, 15deg); - transform: rotate3d(0, 0, 1, 15deg); - } - - 40% { - -webkit-transform: rotate3d(0, 0, 1, -10deg); - transform: rotate3d(0, 0, 1, -10deg); - } - - 60% { - -webkit-transform: rotate3d(0, 0, 1, 5deg); - transform: rotate3d(0, 0, 1, 5deg); - } - - 80% { - -webkit-transform: rotate3d(0, 0, 1, -5deg); - transform: rotate3d(0, 0, 1, -5deg); - } - - 100% { - -webkit-transform: rotate3d(0, 0, 1, 0deg); - transform: rotate3d(0, 0, 1, 0deg); - } -} - -@keyframes swing { - 20% { - -webkit-transform: rotate3d(0, 0, 1, 15deg); - transform: rotate3d(0, 0, 1, 15deg); - } - - 40% { - -webkit-transform: rotate3d(0, 0, 1, -10deg); - transform: rotate3d(0, 0, 1, -10deg); - } - - 60% { - -webkit-transform: rotate3d(0, 0, 1, 5deg); - transform: rotate3d(0, 0, 1, 5deg); - } - - 80% { - -webkit-transform: rotate3d(0, 0, 1, -5deg); - transform: rotate3d(0, 0, 1, -5deg); - } - - 100% { - -webkit-transform: rotate3d(0, 0, 1, 0deg); - transform: rotate3d(0, 0, 1, 0deg); - } -} - -.swing { - -webkit-transform-origin: top center; - transform-origin: top center; - -webkit-animation-name: swing; - animation-name: swing; -} - -@-webkit-keyframes tada { - 0% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 10%, - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); - transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); - } - - 30%, - 50%, - 70%, - 90% { - -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); - } - - 40%, - 60%, - 80% { - -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); - } - - 100% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -@keyframes tada { - 0% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - - 10%, - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); - transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); - } - - 30%, - 50%, - 70%, - 90% { - -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); - } - - 40%, - 60%, - 80% { - -webkit-transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); - transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); - } - - 100% { - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -.tada { - -webkit-animation-name: tada; - animation-name: tada; -} - -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes wobble { - 0% { - -webkit-transform: none; - transform: none; - } - - 15% { - -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); - transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); - } - - 30% { - -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); - transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); - } - - 45% { - -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); - transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); - } - - 60% { - -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); - transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); - } - - 75% { - -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); - transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); - } - - 100% { - -webkit-transform: none; - transform: none; - } -} - -@keyframes wobble { - 0% { - -webkit-transform: none; - transform: none; - } - - 15% { - -webkit-transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); - transform: translate3d(-25%, 0, 0) rotate3d(0, 0, 1, -5deg); - } - - 30% { - -webkit-transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); - transform: translate3d(20%, 0, 0) rotate3d(0, 0, 1, 3deg); - } - - 45% { - -webkit-transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); - transform: translate3d(-15%, 0, 0) rotate3d(0, 0, 1, -3deg); - } - - 60% { - -webkit-transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); - transform: translate3d(10%, 0, 0) rotate3d(0, 0, 1, 2deg); - } - - 75% { - -webkit-transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); - transform: translate3d(-5%, 0, 0) rotate3d(0, 0, 1, -1deg); - } - - 100% { - -webkit-transform: none; - transform: none; - } -} - -.wobble { - -webkit-animation-name: wobble; - animation-name: wobble; -} - -@-webkit-keyframes jello { - 11.1% { - -webkit-transform: none; - transform: none; - } - - 22.2% { - -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); - transform: skewX(-12.5deg) skewY(-12.5deg); - } - 33.3% { - -webkit-transform: skewX(6.25deg) skewY(6.25deg); - transform: skewX(6.25deg) skewY(6.25deg); - } - 44.4% { - -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); - transform: skewX(-3.125deg) skewY(-3.125deg); - } - 55.5% { - -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); - transform: skewX(1.5625deg) skewY(1.5625deg); - } - 66.6% { - -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); - transform: skewX(-0.78125deg) skewY(-0.78125deg); - } - 77.7% { - -webkit-transform: skewX(0.390625deg) skewY(0.390625deg); - transform: skewX(0.390625deg) skewY(0.390625deg); - } - 88.8% { - -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg); - transform: skewX(-0.1953125deg) skewY(-0.1953125deg); - } - 100% { - -webkit-transform: none; - transform: none; - } -} - -@keyframes jello { - 11.1% { - -webkit-transform: none; - transform: none; - } - - 22.2% { - -webkit-transform: skewX(-12.5deg) skewY(-12.5deg); - transform: skewX(-12.5deg) skewY(-12.5deg); - } - 33.3% { - -webkit-transform: skewX(6.25deg) skewY(6.25deg); - transform: skewX(6.25deg) skewY(6.25deg); - } - 44.4% { - -webkit-transform: skewX(-3.125deg) skewY(-3.125deg); - transform: skewX(-3.125deg) skewY(-3.125deg); - } - 55.5% { - -webkit-transform: skewX(1.5625deg) skewY(1.5625deg); - transform: skewX(1.5625deg) skewY(1.5625deg); - } - 66.6% { - -webkit-transform: skewX(-0.78125deg) skewY(-0.78125deg); - transform: skewX(-0.78125deg) skewY(-0.78125deg); - } - 77.7% { - -webkit-transform: skewX(0.390625deg) skewY(0.390625deg); - transform: skewX(0.390625deg) skewY(0.390625deg); - } - 88.8% { - -webkit-transform: skewX(-0.1953125deg) skewY(-0.1953125deg); - transform: skewX(-0.1953125deg) skewY(-0.1953125deg); - } - 100% { - -webkit-transform: none; - transform: none; - } -} - -.jello { - -webkit-animation-name: jello; - animation-name: jello; - -webkit-transform-origin: center; - - transform-origin: center; -} - -@-webkit-keyframes bounceIn { - 0%, - 20%, - 40%, - 60%, - 80%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - 20% { - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - - 40% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(1.03, 1.03, 1.03); - transform: scale3d(1.03, 1.03, 1.03); - } - - 80% { - -webkit-transform: scale3d(0.97, 0.97, 0.97); - transform: scale3d(0.97, 0.97, 0.97); - } - - 100% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -@keyframes bounceIn { - 0%, - 20%, - 40%, - 60%, - 80%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - 20% { - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - - 40% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(1.03, 1.03, 1.03); - transform: scale3d(1.03, 1.03, 1.03); - } - - 80% { - -webkit-transform: scale3d(0.97, 0.97, 0.97); - transform: scale3d(0.97, 0.97, 0.97); - } - - 100% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} - -.bounceIn { - -webkit-animation-name: bounceIn; - animation-name: bounceIn; -} - -@-webkit-keyframes bounceInDown { - 0%, - 60%, - 75%, - 90%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(0, -3000px, 0); - transform: translate3d(0, -3000px, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(0, 25px, 0); - transform: translate3d(0, 25px, 0); - } - - 75% { - -webkit-transform: translate3d(0, -10px, 0); - transform: translate3d(0, -10px, 0); - } - - 90% { - -webkit-transform: translate3d(0, 5px, 0); - transform: translate3d(0, 5px, 0); - } - - 100% { - -webkit-transform: none; - transform: none; - } -} - -@keyframes bounceInDown { - 0%, - 60%, - 75%, - 90%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(0, -3000px, 0); - transform: translate3d(0, -3000px, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(0, 25px, 0); - transform: translate3d(0, 25px, 0); - } - - 75% { - -webkit-transform: translate3d(0, -10px, 0); - transform: translate3d(0, -10px, 0); - } - - 90% { - -webkit-transform: translate3d(0, 5px, 0); - transform: translate3d(0, 5px, 0); - } - - 100% { - -webkit-transform: none; - transform: none; - } -} - -.bounceInDown { - -webkit-animation-name: bounceInDown; - animation-name: bounceInDown; -} - -@-webkit-keyframes bounceInLeft { - 0%, - 60%, - 75%, - 90%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(-3000px, 0, 0); - transform: translate3d(-3000px, 0, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(25px, 0, 0); - transform: translate3d(25px, 0, 0); - } - - 75% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } - - 90% { - -webkit-transform: translate3d(5px, 0, 0); - transform: translate3d(5px, 0, 0); - } - - 100% { - -webkit-transform: none; - transform: none; - } -} - -@keyframes bounceInLeft { - 0%, - 60%, - 75%, - 90%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(-3000px, 0, 0); - transform: translate3d(-3000px, 0, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(25px, 0, 0); - transform: translate3d(25px, 0, 0); - } - - 75% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } - - 90% { - -webkit-transform: translate3d(5px, 0, 0); - transform: translate3d(5px, 0, 0); - } - - 100% { - -webkit-transform: none; - transform: none; - } -} - -.bounceInLeft { - -webkit-animation-name: bounceInLeft; - animation-name: bounceInLeft; -} - -@-webkit-keyframes bounceInRight { - 0%, - 60%, - 75%, - 90%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(3000px, 0, 0); - transform: translate3d(3000px, 0, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(-25px, 0, 0); - transform: translate3d(-25px, 0, 0); - } - - 75% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } - - 90% { - -webkit-transform: translate3d(-5px, 0, 0); - transform: translate3d(-5px, 0, 0); - } - - 100% { - -webkit-transform: none; - transform: none; - } -} - -@keyframes bounceInRight { - 0%, - 60%, - 75%, - 90%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(3000px, 0, 0); - transform: translate3d(3000px, 0, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(-25px, 0, 0); - transform: translate3d(-25px, 0, 0); - } - - 75% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } - - 90% { - -webkit-transform: translate3d(-5px, 0, 0); - transform: translate3d(-5px, 0, 0); - } - - 100% { - -webkit-transform: none; - transform: none; - } -} - -.bounceInRight { - -webkit-animation-name: bounceInRight; - animation-name: bounceInRight; -} - -@-webkit-keyframes bounceInUp { - 0%, - 60%, - 75%, - 90%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(0, 3000px, 0); - transform: translate3d(0, 3000px, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(0, -20px, 0); - transform: translate3d(0, -20px, 0); - } - - 75% { - -webkit-transform: translate3d(0, 10px, 0); - transform: translate3d(0, 10px, 0); - } - - 90% { - -webkit-transform: translate3d(0, -5px, 0); - transform: translate3d(0, -5px, 0); - } - - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes bounceInUp { - 0%, - 60%, - 75%, - 90%, - 100% { - -webkit-animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - - 0% { - opacity: 0; - -webkit-transform: translate3d(0, 3000px, 0); - transform: translate3d(0, 3000px, 0); - } - - 60% { - opacity: 1; - -webkit-transform: translate3d(0, -20px, 0); - transform: translate3d(0, -20px, 0); - } - - 75% { - -webkit-transform: translate3d(0, 10px, 0); - transform: translate3d(0, 10px, 0); - } - - 90% { - -webkit-transform: translate3d(0, -5px, 0); - transform: translate3d(0, -5px, 0); - } - - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.bounceInUp { - -webkit-animation-name: bounceInUp; - animation-name: bounceInUp; -} - -@-webkit-keyframes bounceOut { - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - - 50%, - 55% { - opacity: 1; - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - - 100% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } -} - -@keyframes bounceOut { - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - - 50%, - 55% { - opacity: 1; - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - - 100% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } -} - -.bounceOut { - -webkit-animation-name: bounceOut; - animation-name: bounceOut; -} - -@-webkit-keyframes bounceOutDown { - 20% { - -webkit-transform: translate3d(0, 10px, 0); - transform: translate3d(0, 10px, 0); - } - - 40%, - 45% { - opacity: 1; - -webkit-transform: translate3d(0, -20px, 0); - transform: translate3d(0, -20px, 0); - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } -} - -@keyframes bounceOutDown { - 20% { - -webkit-transform: translate3d(0, 10px, 0); - transform: translate3d(0, 10px, 0); - } - - 40%, - 45% { - opacity: 1; - -webkit-transform: translate3d(0, -20px, 0); - transform: translate3d(0, -20px, 0); - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } -} - -.bounceOutDown { - -webkit-animation-name: bounceOutDown; - animation-name: bounceOutDown; -} - -@-webkit-keyframes bounceOutLeft { - 20% { - opacity: 1; - -webkit-transform: translate3d(20px, 0, 0); - transform: translate3d(20px, 0, 0); - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } -} - -@keyframes bounceOutLeft { - 20% { - opacity: 1; - -webkit-transform: translate3d(20px, 0, 0); - transform: translate3d(20px, 0, 0); - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } -} - -.bounceOutLeft { - -webkit-animation-name: bounceOutLeft; - animation-name: bounceOutLeft; -} - -@-webkit-keyframes bounceOutRight { - 20% { - opacity: 1; - -webkit-transform: translate3d(-20px, 0, 0); - transform: translate3d(-20px, 0, 0); - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } -} - -@keyframes bounceOutRight { - 20% { - opacity: 1; - -webkit-transform: translate3d(-20px, 0, 0); - transform: translate3d(-20px, 0, 0); - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } -} - -.bounceOutRight { - -webkit-animation-name: bounceOutRight; - animation-name: bounceOutRight; -} - -@-webkit-keyframes bounceOutUp { - 20% { - -webkit-transform: translate3d(0, -10px, 0); - transform: translate3d(0, -10px, 0); - } - - 40%, - 45% { - opacity: 1; - -webkit-transform: translate3d(0, 20px, 0); - transform: translate3d(0, 20px, 0); - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } -} - -@keyframes bounceOutUp { - 20% { - -webkit-transform: translate3d(0, -10px, 0); - transform: translate3d(0, -10px, 0); - } - - 40%, - 45% { - opacity: 1; - -webkit-transform: translate3d(0, 20px, 0); - transform: translate3d(0, 20px, 0); - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } -} - -.bounceOutUp { - -webkit-animation-name: bounceOutUp; - animation-name: bounceOutUp; -} - -@-webkit-keyframes fadeIn { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -@keyframes fadeIn { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } -} - -.fadeIn { - -webkit-animation-name: fadeIn; - animation-name: fadeIn; -} - -@-webkit-keyframes fadeInDown { - 0% { - opacity: 0; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -@keyframes fadeInDown { - 0% { - opacity: 0; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -.fadeInDown { - -webkit-animation-name: fadeInDown; - animation-name: fadeInDown; -} - -@-webkit-keyframes fadeInDownBig { - 0% { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -@keyframes fadeInDownBig { - 0% { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -.fadeInDownBig { - -webkit-animation-name: fadeInDownBig; - animation-name: fadeInDownBig; -} - -@-webkit-keyframes fadeInLeft { - 0% { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -@keyframes fadeInLeft { - 0% { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -.fadeInLeft { - -webkit-animation-name: fadeInLeft; - animation-name: fadeInLeft; -} - -@-webkit-keyframes fadeInLeftBig { - 0% { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -@keyframes fadeInLeftBig { - 0% { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -.fadeInLeftBig { - -webkit-animation-name: fadeInLeftBig; - animation-name: fadeInLeftBig; -} - -@-webkit-keyframes fadeInRight { - 0% { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -@keyframes fadeInRight { - 0% { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -.fadeInRight { - -webkit-animation-name: fadeInRight; - animation-name: fadeInRight; -} - -@-webkit-keyframes fadeInRightBig { - 0% { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -@keyframes fadeInRightBig { - 0% { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -.fadeInRightBig { - -webkit-animation-name: fadeInRightBig; - animation-name: fadeInRightBig; -} - -@-webkit-keyframes fadeInUp { - 0% { - opacity: 0; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -@keyframes fadeInUp { - 0% { - opacity: 0; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - max-height: 0px; - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -.fadeInUp { - -webkit-animation-name: fadeInUp; - animation-name: fadeInUp; -} - -@-webkit-keyframes fadeInUpBig { - 0% { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -@keyframes fadeInUpBig { - 0% { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -.fadeInUpBig { - -webkit-animation-name: fadeInUpBig; - animation-name: fadeInUpBig; -} - -@-webkit-keyframes fadeOut { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - } -} - -@keyframes fadeOut { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - } -} - -.fadeOut { - -webkit-animation-name: fadeOut; - animation-name: fadeOut; -} - -@-webkit-keyframes fadeOutDown { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - } -} - -@keyframes fadeOutDown { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - max-height: 0px; - } -} - -.fadeOutDown { - -webkit-animation-name: fadeOutDown; - animation-name: fadeOutDown; -} - -@-webkit-keyframes fadeOutDownBig { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } -} - -@keyframes fadeOutDownBig { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, 2000px, 0); - transform: translate3d(0, 2000px, 0); - } -} - -.fadeOutDownBig { - -webkit-animation-name: fadeOutDownBig; - animation-name: fadeOutDownBig; -} - -@-webkit-keyframes fadeOutLeft { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } -} - -@keyframes fadeOutLeft { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } -} - -.fadeOutLeft { - -webkit-animation-name: fadeOutLeft; - animation-name: fadeOutLeft; -} - -@-webkit-keyframes fadeOutLeftBig { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } -} - -@keyframes fadeOutLeftBig { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(-2000px, 0, 0); - transform: translate3d(-2000px, 0, 0); - } -} - -.fadeOutLeftBig { - -webkit-animation-name: fadeOutLeftBig; - animation-name: fadeOutLeftBig; -} - -@-webkit-keyframes fadeOutRight { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } -} - -@keyframes fadeOutRight { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } -} - -.fadeOutRight { - -webkit-animation-name: fadeOutRight; - animation-name: fadeOutRight; -} - -@-webkit-keyframes fadeOutRightBig { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } -} - -@keyframes fadeOutRightBig { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(2000px, 0, 0); - transform: translate3d(2000px, 0, 0); - } -} - -.fadeOutRightBig { - -webkit-animation-name: fadeOutRightBig; - animation-name: fadeOutRightBig; -} - -@-webkit-keyframes fadeOutUp { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } -} - -@keyframes fadeOutUp { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } -} - -.fadeOutUp { - -webkit-animation-name: fadeOutUp; - animation-name: fadeOutUp; -} - -@-webkit-keyframes fadeOutUpBig { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } -} - -@keyframes fadeOutUpBig { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(0, -2000px, 0); - transform: translate3d(0, -2000px, 0); - } -} - -.fadeOutUpBig { - -webkit-animation-name: fadeOutUpBig; - animation-name: fadeOutUpBig; -} - -@-webkit-keyframes flip { - 0% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg); - transform: perspective(400px) rotate3d(0, 1, 0, -360deg); - -webkit-animation-timing-function: ease-out; - animation-timing-function: ease-out; - } - - 40% { - -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); - transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); - -webkit-animation-timing-function: ease-out; - animation-timing-function: ease-out; - } - - 50% { - -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); - transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 80% { - -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95); - transform: perspective(400px) scale3d(0.95, 0.95, 0.95); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } -} - -@keyframes flip { - 0% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -360deg); - transform: perspective(400px) rotate3d(0, 1, 0, -360deg); - -webkit-animation-timing-function: ease-out; - animation-timing-function: ease-out; - } - - 40% { - -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); - transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -190deg); - -webkit-animation-timing-function: ease-out; - animation-timing-function: ease-out; - } - - 50% { - -webkit-transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); - transform: perspective(400px) translate3d(0, 0, 150px) rotate3d(0, 1, 0, -170deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 80% { - -webkit-transform: perspective(400px) scale3d(0.95, 0.95, 0.95); - transform: perspective(400px) scale3d(0.95, 0.95, 0.95); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } -} - -.animated.flip { - -webkit-backface-visibility: visible; - backface-visibility: visible; - -webkit-animation-name: flip; - animation-name: flip; -} - -@-webkit-keyframes flipInX { - 0% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - opacity: 0; - } - - 40% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 60% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - opacity: 1; - } - - 80% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - } - - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} - -@keyframes flipInX { - 0% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - opacity: 0; - } - - 40% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 60% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - opacity: 1; - } - - 80% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - } - - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} - -.flipInX { - -webkit-backface-visibility: visible !important; - backface-visibility: visible !important; - -webkit-animation-name: flipInX; - animation-name: flipInX; -} - -@-webkit-keyframes flipInY { - 0% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - opacity: 0; - } - - 40% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 60% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - opacity: 1; - } - - 80% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - } - - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} - -@keyframes flipInY { - 0% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - opacity: 0; - } - - 40% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; - } - - 60% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - opacity: 1; - } - - 80% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - } - - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} - -.flipInY { - -webkit-backface-visibility: visible !important; - backface-visibility: visible !important; - -webkit-animation-name: flipInY; - animation-name: flipInY; -} - -@-webkit-keyframes flipOutX { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - - 30% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - opacity: 1; - } - - 100% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - opacity: 0; - } -} - -@keyframes flipOutX { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - - 30% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - opacity: 1; - } - - 100% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - opacity: 0; - } -} - -.flipOutX { - -webkit-animation-name: flipOutX; - animation-name: flipOutX; - -webkit-backface-visibility: visible !important; - backface-visibility: visible !important; -} - -@-webkit-keyframes flipOutY { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - - 30% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - opacity: 1; - } - - 100% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - opacity: 0; - } -} - -@keyframes flipOutY { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - - 30% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - opacity: 1; - } - - 100% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - opacity: 0; - } -} - -.flipOutY { - -webkit-backface-visibility: visible !important; - backface-visibility: visible !important; - -webkit-animation-name: flipOutY; - animation-name: flipOutY; -} - -@-webkit-keyframes lightSpeedIn { - 0% { - -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); - transform: translate3d(100%, 0, 0) skewX(-30deg); - opacity: 0; - } - - 60% { - -webkit-transform: skewX(20deg); - transform: skewX(20deg); - opacity: 1; - } - - 80% { - -webkit-transform: skewX(-5deg); - transform: skewX(-5deg); - opacity: 1; - } - - 100% { - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -@keyframes lightSpeedIn { - 0% { - -webkit-transform: translate3d(100%, 0, 0) skewX(-30deg); - transform: translate3d(100%, 0, 0) skewX(-30deg); - opacity: 0; - } - - 60% { - -webkit-transform: skewX(20deg); - transform: skewX(20deg); - opacity: 1; - } - - 80% { - -webkit-transform: skewX(-5deg); - transform: skewX(-5deg); - opacity: 1; - } - - 100% { - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -.lightSpeedIn { - -webkit-animation-name: lightSpeedIn; - animation-name: lightSpeedIn; - -webkit-animation-timing-function: ease-out; - animation-timing-function: ease-out; -} - -@-webkit-keyframes lightSpeedOut { - 0% { - opacity: 1; - } - - 100% { - -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); - transform: translate3d(100%, 0, 0) skewX(30deg); - opacity: 0; - } -} - -@keyframes lightSpeedOut { - 0% { - opacity: 1; - } - - 100% { - -webkit-transform: translate3d(100%, 0, 0) skewX(30deg); - transform: translate3d(100%, 0, 0) skewX(30deg); - opacity: 0; - } -} - -.lightSpeedOut { - -webkit-animation-name: lightSpeedOut; - animation-name: lightSpeedOut; - -webkit-animation-timing-function: ease-in; - animation-timing-function: ease-in; -} - -@-webkit-keyframes rotateIn { - 0% { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: rotate3d(0, 0, 1, -200deg); - transform: rotate3d(0, 0, 1, -200deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -@keyframes rotateIn { - 0% { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: rotate3d(0, 0, 1, -200deg); - transform: rotate3d(0, 0, 1, -200deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -.rotateIn { - -webkit-animation-name: rotateIn; - animation-name: rotateIn; -} - -@-webkit-keyframes rotateInDownLeft { - 0% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -@keyframes rotateInDownLeft { - 0% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -.rotateInDownLeft { - -webkit-animation-name: rotateInDownLeft; - animation-name: rotateInDownLeft; -} - -@-webkit-keyframes rotateInDownRight { - 0% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -@keyframes rotateInDownRight { - 0% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -.rotateInDownRight { - -webkit-animation-name: rotateInDownRight; - animation-name: rotateInDownRight; -} - -@-webkit-keyframes rotateInUpLeft { - 0% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -@keyframes rotateInUpLeft { - 0% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -.rotateInUpLeft { - -webkit-animation-name: rotateInUpLeft; - animation-name: rotateInUpLeft; -} - -@-webkit-keyframes rotateInUpRight { - 0% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, -90deg); - transform: rotate3d(0, 0, 1, -90deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -@keyframes rotateInUpRight { - 0% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, -90deg); - transform: rotate3d(0, 0, 1, -90deg); - opacity: 0; - } - - 100% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: none; - transform: none; - opacity: 1; - } -} - -.rotateInUpRight { - -webkit-animation-name: rotateInUpRight; - animation-name: rotateInUpRight; -} - -@-webkit-keyframes rotateOut { - 0% { - -webkit-transform-origin: center; - transform-origin: center; - opacity: 1; - } - - 100% { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: rotate3d(0, 0, 1, 200deg); - transform: rotate3d(0, 0, 1, 200deg); - opacity: 0; - } -} - -@keyframes rotateOut { - 0% { - -webkit-transform-origin: center; - transform-origin: center; - opacity: 1; - } - - 100% { - -webkit-transform-origin: center; - transform-origin: center; - -webkit-transform: rotate3d(0, 0, 1, 200deg); - transform: rotate3d(0, 0, 1, 200deg); - opacity: 0; - } -} - -.rotateOut { - -webkit-animation-name: rotateOut; - animation-name: rotateOut; -} - -@-webkit-keyframes rotateOutDownLeft { - 0% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - opacity: 1; - } - - 100% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } -} - -@keyframes rotateOutDownLeft { - 0% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - opacity: 1; - } - - 100% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, 45deg); - transform: rotate3d(0, 0, 1, 45deg); - opacity: 0; - } -} - -.rotateOutDownLeft { - -webkit-animation-name: rotateOutDownLeft; - animation-name: rotateOutDownLeft; -} - -@-webkit-keyframes rotateOutDownRight { - 0% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - opacity: 1; - } - - 100% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } -} - -@keyframes rotateOutDownRight { - 0% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - opacity: 1; - } - - 100% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } -} - -.rotateOutDownRight { - -webkit-animation-name: rotateOutDownRight; - animation-name: rotateOutDownRight; -} - -@-webkit-keyframes rotateOutUpLeft { - 0% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - opacity: 1; - } - - 100% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } -} - -@keyframes rotateOutUpLeft { - 0% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - opacity: 1; - } - - 100% { - -webkit-transform-origin: left bottom; - transform-origin: left bottom; - -webkit-transform: rotate3d(0, 0, 1, -45deg); - transform: rotate3d(0, 0, 1, -45deg); - opacity: 0; - } -} - -.rotateOutUpLeft { - -webkit-animation-name: rotateOutUpLeft; - animation-name: rotateOutUpLeft; -} - -@-webkit-keyframes rotateOutUpRight { - 0% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - opacity: 1; - } - - 100% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, 90deg); - transform: rotate3d(0, 0, 1, 90deg); - opacity: 0; - } -} - -@keyframes rotateOutUpRight { - 0% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - opacity: 1; - } - - 100% { - -webkit-transform-origin: right bottom; - transform-origin: right bottom; - -webkit-transform: rotate3d(0, 0, 1, 90deg); - transform: rotate3d(0, 0, 1, 90deg); - opacity: 0; - } -} - -.rotateOutUpRight { - -webkit-animation-name: rotateOutUpRight; - animation-name: rotateOutUpRight; -} - -@-webkit-keyframes hinge { - 0% { - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - } - - 20%, - 60% { - -webkit-transform: rotate3d(0, 0, 1, 80deg); - transform: rotate3d(0, 0, 1, 80deg); - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - } - - 40%, - 80% { - -webkit-transform: rotate3d(0, 0, 1, 60deg); - transform: rotate3d(0, 0, 1, 60deg); - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - opacity: 1; - } - - 100% { - -webkit-transform: translate3d(0, 700px, 0); - transform: translate3d(0, 700px, 0); - opacity: 0; - } -} - -@keyframes hinge { - 0% { - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - } - - 20%, - 60% { - -webkit-transform: rotate3d(0, 0, 1, 80deg); - transform: rotate3d(0, 0, 1, 80deg); - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - } - - 40%, - 80% { - -webkit-transform: rotate3d(0, 0, 1, 60deg); - transform: rotate3d(0, 0, 1, 60deg); - -webkit-transform-origin: top left; - transform-origin: top left; - -webkit-animation-timing-function: ease-in-out; - animation-timing-function: ease-in-out; - opacity: 1; - } - - 100% { - -webkit-transform: translate3d(0, 700px, 0); - transform: translate3d(0, 700px, 0); - opacity: 0; - } -} - -.hinge { - -webkit-animation-name: hinge; - animation-name: hinge; -} - -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes rollIn { - 0% { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); - transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -@keyframes rollIn { - 0% { - opacity: 0; - -webkit-transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); - transform: translate3d(-100%, 0, 0) rotate3d(0, 0, 1, -120deg); - } - - 100% { - opacity: 1; - -webkit-transform: none; - transform: none; - } -} - -.rollIn { - -webkit-animation-name: rollIn; - animation-name: rollIn; -} - -/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */ - -@-webkit-keyframes rollOut { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); - transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); - } -} - -@keyframes rollOut { - 0% { - opacity: 1; - } - - 100% { - opacity: 0; - -webkit-transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); - transform: translate3d(100%, 0, 0) rotate3d(0, 0, 1, 120deg); - } -} - -.rollOut { - -webkit-animation-name: rollOut; - animation-name: rollOut; -} - -@-webkit-keyframes zoomIn { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - 50% { - opacity: 1; - } -} - -@keyframes zoomIn { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - 50% { - opacity: 1; - } -} - -.zoomIn { - -webkit-animation-name: zoomIn; - animation-name: zoomIn; -} - -@-webkit-keyframes zoomInDown { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomInDown { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -1000px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomInDown { - -webkit-animation-name: zoomInDown; - animation-name: zoomInDown; -} - -@-webkit-keyframes zoomInLeft { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomInLeft { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(-1000px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(10px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomInLeft { - -webkit-animation-name: zoomInLeft; - animation-name: zoomInLeft; -} - -@-webkit-keyframes zoomInRight { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomInRight { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(1000px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(-10px, 0, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomInRight { - -webkit-animation-name: zoomInRight; - animation-name: zoomInRight; -} - -@-webkit-keyframes zoomInUp { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomInUp { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 1000px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 60% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomInUp { - -webkit-animation-name: zoomInUp; - animation-name: zoomInUp; -} - -@-webkit-keyframes zoomOut { - 0% { - opacity: 1; - } - - 50% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - 100% { - opacity: 0; - } +.animated { + -webkit-animation-duration: 1s; + animation-duration: 1s; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; } -@keyframes zoomOut { +@-webkit-keyframes fadeIn { 0% { - opacity: 1; - } - - 50% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - - 100% { - opacity: 0; - } -} - -.zoomOut { - -webkit-animation-name: zoomOut; - animation-name: zoomOut; -} - -@-webkit-keyframes zoomOutDown { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 100% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); - -webkit-transform-origin: center bottom; - transform-origin: center bottom; - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomOutDown { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, -60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 100% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, 2000px, 0); - -webkit-transform-origin: center bottom; - transform-origin: center bottom; - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -.zoomOutDown { - -webkit-animation-name: zoomOutDown; - animation-name: zoomOutDown; -} - -@-webkit-keyframes zoomOutLeft { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); - } - - 100% { opacity: 0; - -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); - transform: scale(0.1) translate3d(-2000px, 0, 0); - -webkit-transform-origin: left center; - transform-origin: left center; - } -} - -@keyframes zoomOutLeft { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(42px, 0, 0); } 100% { - opacity: 0; - -webkit-transform: scale(0.1) translate3d(-2000px, 0, 0); - transform: scale(0.1) translate3d(-2000px, 0, 0); - -webkit-transform-origin: left center; - transform-origin: left center; - } -} - -.zoomOutLeft { - -webkit-animation-name: zoomOutLeft; - animation-name: zoomOutLeft; -} - -@-webkit-keyframes zoomOutRight { - 40% { opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); - } - - 100% { - opacity: 0; - -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); - transform: scale(0.1) translate3d(2000px, 0, 0); - -webkit-transform-origin: right center; - transform-origin: right center; } } -@keyframes zoomOutRight { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(-42px, 0, 0); - } - - 100% { +@keyframes fadeIn { + 0% { opacity: 0; - -webkit-transform: scale(0.1) translate3d(2000px, 0, 0); - transform: scale(0.1) translate3d(2000px, 0, 0); - -webkit-transform-origin: right center; - transform-origin: right center; - } -} - -.zoomOutRight { - -webkit-animation-name: zoomOutRight; - animation-name: zoomOutRight; -} - -@-webkit-keyframes zoomOutUp { - 40% { - opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); } 100% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); - -webkit-transform-origin: center bottom; - transform-origin: center bottom; - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - } -} - -@keyframes zoomOutUp { - 40% { opacity: 1; - -webkit-transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - transform: scale3d(0.475, 0.475, 0.475) translate3d(0, 60px, 0); - -webkit-animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); - } - - 100% { - opacity: 0; - -webkit-transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); - transform: scale3d(0.1, 0.1, 0.1) translate3d(0, -2000px, 0); - -webkit-transform-origin: center bottom; - transform-origin: center bottom; - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1); } } -.zoomOutUp { - -webkit-animation-name: zoomOutUp; - animation-name: zoomOutUp; -} - -@-webkit-keyframes slideInDown { - 0% { - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - visibility: visible; - } - - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } +.fadeIn { + -webkit-animation-name: fadeIn; + animation-name: fadeIn; } -@keyframes slideInDown { +@-webkit-keyframes fadeInDown { 0% { + opacity: 0; -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); - visibility: visible; - } - - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.slideInDown { - -webkit-animation-name: slideInDown; - animation-name: slideInDown; -} - -@-webkit-keyframes slideInLeft { - 0% { - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - visibility: visible; - } - - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes slideInLeft { - 0% { - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - visibility: visible; - } - - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.slideInLeft { - -webkit-animation-name: slideInLeft; - animation-name: slideInLeft; -} - -@-webkit-keyframes slideInRight { - 0% { - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - visibility: visible; - } - - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes slideInRight { - 0% { - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - visibility: visible; - } - - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.slideInRight { - -webkit-animation-name: slideInRight; - animation-name: slideInRight; -} - -@-webkit-keyframes slideInUp { - 0% { - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - visibility: visible; - } - - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -@keyframes slideInUp { - 0% { - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - visibility: visible; - } - - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} - -.slideInUp { - -webkit-animation-name: slideInUp; - animation-name: slideInUp; -} - -@-webkit-keyframes slideOutDown { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 100% { - visibility: hidden; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - } -} - -@keyframes slideOutDown { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 100% { - visibility: hidden; - -webkit-transform: translate3d(0, 100%, 0); - transform: translate3d(0, 100%, 0); - } -} - -.slideOutDown { - -webkit-animation-name: slideOutDown; - animation-name: slideOutDown; -} - -@-webkit-keyframes slideOutLeft { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 100% { - visibility: hidden; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } -} - -@keyframes slideOutLeft { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 100% { - visibility: hidden; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } -} - -.slideOutLeft { - -webkit-animation-name: slideOutLeft; - animation-name: slideOutLeft; -} - -@-webkit-keyframes slideOutRight { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 100% { - visibility: hidden; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } -} - -@keyframes slideOutRight { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); } 100% { - visibility: hidden; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); + opacity: 1; + -webkit-transform: none; + transform: none; } } -.slideOutRight { - -webkit-animation-name: slideOutRight; - animation-name: slideOutRight; -} - -@-webkit-keyframes slideOutUp { +@keyframes fadeInDown { 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - - 100% { - visibility: hidden; + opacity: 0; -webkit-transform: translate3d(0, -100%, 0); transform: translate3d(0, -100%, 0); } -} - -@keyframes slideOutUp { - 0% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } 100% { - visibility: hidden; - -webkit-transform: translate3d(0, -100%, 0); - transform: translate3d(0, -100%, 0); - } -} - -.slideOutUp { - -webkit-animation-name: slideOutUp; - animation-name: slideOutUp; -} - -// Loader animation -@keyframes pen1 { - 0% { - transform: translateY(0px); - } - 15% { - transform: translateY(-10px); - } - 30% { - transform: translateY(0px); - } -} - -@keyframes pen2 { - 30% { - transform: translateY(0px); - } - 45% { - transform: translateY(-10px); - } - 60% { - transform: translateY(0px); + opacity: 1; + -webkit-transform: none; + transform: none; } } -@keyframes pen3 { - 60% { - transform: translateY(0px); - } - 75% { - transform: translateY(-10px); - } - 90% { - transform: translateY(0px); - } +.fadeInDown { + -webkit-animation-name: fadeInDown; + animation-name: fadeInDown; } @keyframes loaderColor { 0% { fill: #513b56; } + 33% { fill: #348aa7; } + 66% { fill: #5dd39e; } + 100% { fill: #513b56; } @@ -3439,6 +94,7 @@ 0% { transform: translateY(0); } + 100% { transform: translateY(-150px); } diff --git a/frontend/resources/styles/common/dependencies/colors.scss b/frontend/resources/styles/common/dependencies/colors.scss deleted file mode 100644 index 34bd2dccbb9b541114ebd048e2c754c636f48d82..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/common/dependencies/colors.scss +++ /dev/null @@ -1,89 +0,0 @@ -// 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 - -// New UI colors -$db-primary: #18181a; -$db-secondary: #000000; -$db-tertiary: #212426; -$db-quaternary: #2e3434; - -$df-primary: #ffffff; -$df-secondary: #8f9da3; - -$da-primary: #7efff5; -$da-primary-muted: rgba(126, 255, 245, 0.3); -$da-secondary: #bb97d8; -$da-tertiary: #00d1b8; - -$d-shadow: rgba(0, 0, 0, 0.6); - -// Colors -$color-white: #ffffff; -$color-black: #000000; -$color-canvas: #e8e9ea; -$color-dashboard: #f6f6f6; - -// Main color -$color-primary: #31efb8; - -// Secondary colors -$color-success: #49d793; -$color-complete: #a599c6; -$color-warning: #fc8802; -$color-danger: #e65244; -$color-info: #59b9e2; - -// Gray scale -$color-gray-10: #e3e3e3; -$color-gray-20: #b1b2b5; -$color-gray-30: #7b7d85; -$color-gray-40: #64666a; -$color-gray-50: #303236; -$color-gray-60: #1f1f1f; - -// Mixing Color variable for creating both light and dark colors -$mix-percentage-dark: 81%; -$mix-percentage-darker: 60%; -$mix-percentage-lighter: 20%; - -// Lighter colors - -$color-success-lighter: mix($color-success, $color-white, $mix-percentage-lighter); //#def3de - -$color-primary-lighter: mix($color-primary, $color-white, $mix-percentage-lighter); //#d6fcf1 - -$color-danger-lighter: mix($color-danger, $color-white, $mix-percentage-lighter); //#fadcda - -// Darker colors -$color-success-dark: mix($color-success, $color-black, $mix-percentage-dark); //#479e4b; - -$color-complete-dark: mix($color-complete, $color-black, $mix-percentage-dark); //#867ca0 -$color-complete-darker: mix($color-complete, $color-black, $mix-percentage-darker); //#635c77 - -$color-primary-dark: mix($color-primary, $color-black, $mix-percentage-dark); //#28c295; -$color-primary-darker: mix($color-primary, $color-black, $mix-percentage-darker); // #1d8f6e - -$color-warning-dark: mix($color-warning, $color-black, $mix-percentage-dark); // #cc6e02; - -$color-danger-dark: mix($color-danger, $color-black, $mix-percentage-dark); //#ba4237 - -$color-info-darker: mix($color-info, $color-black, $mix-percentage-darker); // #356f88; - -// bg transparent -$color-dark-bg: rgba(0, 0, 0, 0.4); - -// Transform scss variables into css variables to use them onto cljs files -:root { - // Secondary colors; - - --color-info: #{$color-info}; - --color-canvas: #e8e9ea; - - // Gray scale; - - --color-gray-20: #{$color-gray-20}; - --color-gray-60: #{$color-gray-60}; -} diff --git a/frontend/resources/styles/common/dependencies/fonts.scss b/frontend/resources/styles/common/dependencies/fonts.scss index 74cd11499554746c8968d5128be13cb8c6613bed..7882eb7d3bdf03de6cb90c0e982e4675f81d4c45 100644 --- a/frontend/resources/styles/common/dependencies/fonts.scss +++ b/frontend/resources/styles/common/dependencies/fonts.scss @@ -4,104 +4,51 @@ // // Copyright (c) KALEIDOS INC -// Font sizes -$fs8: 0.5rem; -$fs9: 0.5625rem; -$fs10: 0.625rem; -$fs11: 0.6875rem; -$fs12: 0.75rem; -$fs13: 0.8125rem; -$fs14: 0.875rem; -$fs15: 0.9375rem; -$fs16: 1rem; -$fs17: 1.0625rem; -$fs18: 1.125rem; -$fs19: 1.1875rem; -$fs20: 1.25rem; -$fs21: 1.315rem; -$fs22: 1.375rem; -$fs23: 1.4375rem; -$fs24: 1.5rem; -$fs26: 1.625rem; -$fs30: 1.875rem; -$fs32: 2rem; -$fs34: 2.125rem; -$fs36: 2.25rem; -$fs38: 2.375rem; -$fs40: 2.5rem; -$fs42: 2.675rem; -$fs44: 2.75rem; -$fs80: 5rem; +@mixin font-face($style-name, $file, $unicode-range, $weight: unquote("normal"), $style: unquote("normal")) { + $filepath: "/fonts/" + $file; -// Font weight -// Taken from https://fonts.google.com/specimen/Work+Sans -$fw100: 100; // Thin -$fw200: 200; // Extra Light -$fw300: 300; // Light -$fw400: 400; // Regular (CSS value: 'normal') -$fw500: 500; // Medium -$fw600: 600; // Semi Bold -$fw700: 700; // Bold (CSS value: 'bold') -$fw800: 800; // Extra Bold -$fw900: 900; // Black + @font-face { + font-family: "#{$style-name}"; + src: + url($filepath + ".woff2") format("woff2"), + url($filepath + ".ttf") format("truetype"); + font-weight: unquote($weight); + font-style: unquote($style); + @if $unicode-range { + unicode-range: $unicode-range; + } + } +} -// Line height -// Value are predefined as unitless (ratio to font size in %), because that is the best approach for browsers according to https://developer.mozilla.org/en-US/docs/Web/CSS/line-height#values -$lh-normal: normal; // line-height depends of font-family, font-size, your browser, maybe your OS http://meyerweb.com/eric/thoughts/2008/05/06/line-height-abnormal/ -$lh-088: 0.88; -$lh-100: 1; -$lh-115: 1.15; // original $title-lh-sm -$lh-125: 1.25; // original $title-lh -$lh-128: 1.28; -$lh-133: 1.33; // original $base-lh-sm -$lh-143: 1.43; // original $base-lh -$lh-145: 1.45; -$lh-150: 1.5; -$lh-188: 1.88; -$lh-192: 1.92; -$lh-200: 2; -$lh-236: 2.36; -$lh-500: 5; +@mixin font-face-variable($style-name, $file, $unicode-range) { + $filepath: "/fonts/" + $file; -// Work Sans -@include font-face("worksans", "WorkSans-Thin", "100"); -@include font-face("worksans", "WorkSans-ThinItalic", "100", italic); -@include font-face("worksans", "WorkSans-ExtraLight", "200"); -@include font-face("worksans", "WorkSans-ExtraLightitalic", "200", italic); -@include font-face("worksans", "WorkSans-Light", "300"); -@include font-face("worksans", "WorkSans-LightItalic", "300", italic); -@include font-face("worksans", "WorkSans-Regular", normal); -@include font-face("worksans", "WorkSans-Italic", normal, italic); -@include font-face("worksans", "WorkSans-Medium", "500"); -@include font-face("worksans", "WorkSans-MediumItalic", "500", italic); -@include font-face("worksans", "WorkSans-SemiBold", "600"); -@include font-face("worksans", "WorkSans-SemiBoldItalic", "600", italic); -@include font-face("worksans", "WorkSans-Bold", bold); -@include font-face("worksans", "WorkSans-BoldItalic", bold, italic); -@include font-face("worksans", "WorkSans-Black", "900"); -@include font-face("worksans", "WorkSans-BlackItalic", "900", italic); + @font-face { + font-family: "#{$style-name}"; + src: url($filepath + ".ttf") format("truetype"); + unicode-range: $unicode-range; + } +} + +$_arabic-unicode-list: "U+0600-06FF, U+0750-077F, U+0870-088E, U+0890-0891, U+0898-08E1, U+08E3-08FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE70-FE74, U+FE76-FEFC, U+102E0-102FB, U+10E60-10E7E, U+10EFD-10EFF, U+1EE00-1EE03, U+1EE05-1EE1F, U+1EE21-1EE22, U+1EE24, U+1EE27, U+1EE29-1EE32, U+1EE34-1EE37, U+1EE39, U+1EE3B, U+1EE42, U+1EE47, U+1EE49, U+1EE4B, U+1EE4D-1EE4F, U+1EE51-1EE52, U+1EE54, U+1EE57, U+1EE59, U+1EE5B, U+1EE5D, U+1EE5F, U+1EE61-1EE62, U+1EE64, U+1EE67-1EE6A, U+1EE6C-1EE72, U+1EE74-1EE77, U+1EE79-1EE7C, U+1EE7E, U+1EE80-1EE89, U+1EE8B-1EE9B, U+1EEA1-1EEA3, U+1EEA5-1EEA9, U+1EEAB-1EEBB, U+1EEF0-1EEF1"; +$_latin-unicode-list: "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD, U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF"; + +@include font-face-variable("worksans", "WorkSans-VariableFont", $_latin-unicode-list); +@include font-face-variable("vazirmatn", "Vazirmatn-VariableFont", $_arabic-unicode-list); // Source Sans Pro -@include font-face("sourcesanspro", "sourcesanspro-extralight", "200"); -@include font-face("sourcesanspro", "sourcesanspro-extralightitalic", "200", italic); -@include font-face("sourcesanspro", "sourcesanspro-light", "300"); -@include font-face("sourcesanspro", "sourcesanspro-lightitalic", "300", italic); -@include font-face("sourcesanspro", "sourcesanspro-regular", normal); -@include font-face("sourcesanspro", "sourcesanspro-italic", normal, italic); -@include font-face("sourcesanspro", "sourcesanspro-semibold", "600"); -@include font-face("sourcesanspro", "sourcesanspro-semibolditalic", "600", italic); -@include font-face("sourcesanspro", "sourcesanspro-bold", bold); -@include font-face("sourcesanspro", "sourcesanspro-bolditalic", bold, italic); -@include font-face("sourcesanspro", "sourcesanspro-black", "900"); -@include font-face("sourcesanspro", "sourcesanspro-blackitalic", "900", italic); +@include font-face("sourcesanspro", "sourcesanspro-extralight", null, "200"); +@include font-face("sourcesanspro", "sourcesanspro-extralightitalic", null, "200", italic); +@include font-face("sourcesanspro", "sourcesanspro-light", null, "300"); +@include font-face("sourcesanspro", "sourcesanspro-lightitalic", null, "300", italic); +@include font-face("sourcesanspro", "sourcesanspro-regular", null, normal); +@include font-face("sourcesanspro", "sourcesanspro-italic", null, normal, italic); +@include font-face("sourcesanspro", "sourcesanspro-semibold", null, "600"); +@include font-face("sourcesanspro", "sourcesanspro-semibolditalic", null, "600", italic); +@include font-face("sourcesanspro", "sourcesanspro-bold", null, bold); +@include font-face("sourcesanspro", "sourcesanspro-bolditalic", null, bold, italic); +@include font-face("sourcesanspro", "sourcesanspro-black", null, "900"); +@include font-face("sourcesanspro", "sourcesanspro-blackitalic", null, "900", italic); -// Vazirmatn -@include font-face("vazirmatn", "Vazirmatn-Thin", "100"); -@include font-face("vazirmatn", "Vazirmatn-ExtraLight", "200"); -@include font-face("vazirmatn", "Vazirmatn-Light", "300"); -@include font-face("vazirmatn", "Vazirmatn-Regular", normal); -@include font-face("vazirmatn", "Vazirmatn-Medium", "500"); -@include font-face("vazirmatn", "Vazirmatn-SemiBold", "600"); -@include font-face("vazirmatn", "Vazirmatn-Bold", bold); -@include font-face("vazirmatn", "Vazirmatn-ExtraBold", "800"); -@include font-face("vazirmatn", "Vazirmatn-Black", "900"); +// Roboto mono +@include font-face("robotomono", "RobotoMono-Regular", $_latin-unicode-list, normal); diff --git a/frontend/resources/styles/common/dependencies/helpers.scss b/frontend/resources/styles/common/dependencies/helpers.scss deleted file mode 100644 index 005357e6af4b82c8585083d39d2f7dffbf40d45c..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/common/dependencies/helpers.scss +++ /dev/null @@ -1,69 +0,0 @@ -// 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 - -// Padding & Margin sizes -$size-1: 0.25rem; -$size-2: 0.5rem; -$size-3: 0.75rem; -$size-4: 1rem; -$size-5: 1.5rem; -$size-6: 2rem; - -// Border radius -$br0: 0px; -$br2: 2px; -$br3: 3px; -$br4: 4px; -$br5: 5px; -$br6: 6px; -$br7: 7px; -$br8: 8px; -$br10: 10px; -$br12: 12px; -$br25: 25px; -$br50: 50px; -$br99: 99px; -$br-circle: 50%; // Need to be investigated, before we can use variable - -.row-flex { - align-items: center; - display: flex; - margin-bottom: $size-1; - - &.column { - flex-direction: column; - } - - &.center { - justify-content: center; - } -} - -.row-grid-2 { - display: grid; - grid-template-columns: repeat(2, 1fr); -} - -.flex-grow { - flex-grow: 1; -} - -.column-half { - margin-right: $size-2; -} - -// Display -.hidden { - display: none; -} - -.hide { - opacity: 0; -} - -.display { - opacity: 1 !important; -} diff --git a/frontend/resources/styles/common/dependencies/highlight.scss b/frontend/resources/styles/common/dependencies/highlight.scss new file mode 100644 index 0000000000000000000000000000000000000000..9d53084cb70b681d50dd4d6b0b36ecf4f87a65d9 --- /dev/null +++ b/frontend/resources/styles/common/dependencies/highlight.scss @@ -0,0 +1,15 @@ +// 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 + +@use "sass:meta"; + +:root { + @include meta.load-css("./_hljs-dark-theme.scss"); +} + +.light { + @include meta.load-css("./_hljs-light-theme.scss"); +} diff --git a/frontend/resources/styles/common/dependencies/highlightjs-theme.scss b/frontend/resources/styles/common/dependencies/highlightjs-theme.scss deleted file mode 100644 index 8d8fbd6f99f0e5b6a766068edba6687e44f84521..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/common/dependencies/highlightjs-theme.scss +++ /dev/null @@ -1,81 +0,0 @@ -/* -Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/ -*/ - -.hljs { - display: block; - overflow-x: auto; - padding: 0.5em; - background: #23241f; -} - -.hljs, -.hljs-tag, -.hljs-subst { - color: #f8f8f2; -} - -.hljs-strong, -.hljs-emphasis { - color: #a8a8a2; -} - -.hljs-bullet, -.hljs-quote, -.hljs-number, -.hljs-regexp, -.hljs-literal, -.hljs-link { - color: #ae81ff; -} - -.hljs-code, -.hljs-title, -.hljs-section, -.hljs-selector-class { - color: #a6e22e; -} - -.hljs-strong { - font-weight: $fw700; -} - -.hljs-emphasis { - font-style: italic; -} - -.hljs-keyword, -.hljs-selector-tag, -.hljs-name, -.hljs-attr { - color: #f92672; -} - -.hljs-symbol, -.hljs-attribute { - color: #66d9ef; -} - -.hljs-params, -.hljs-class .hljs-title { - color: #f8f8f2; -} - -.hljs-string, -.hljs-type, -.hljs-built_in, -.hljs-builtin-name, -.hljs-selector-id, -.hljs-selector-attr, -.hljs-selector-pseudo, -.hljs-addition, -.hljs-variable, -.hljs-template-variable { - color: #e6db74; -} - -.hljs-comment, -.hljs-deletion, -.hljs-meta { - color: #75715e; -} diff --git a/frontend/resources/styles/common/dependencies/mixin.scss b/frontend/resources/styles/common/dependencies/mixin.scss deleted file mode 100644 index e8ec11d4081392ceeea8fff00546704b3c0d262b..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/common/dependencies/mixin.scss +++ /dev/null @@ -1,163 +0,0 @@ -// 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 - -/// This mixin allows you to add styles to a specific Media query inside the style selector specifying which Breaking Point you want to choose. -/// @group Mixins -/// @parameter $point - This parameter decide which one of Breaking Point you want to use: "bp-desktop" (Desktop), "bp-tablet" (Tablet) and "bp-mobile" (Mobile). -$bp-min-720: "(min-width: 720px)"; -$bp-min-1020: "(min-width: 1020px)"; -$bp-min-1366: "(min-width: 1366px)"; -$bp-max-1366: "(max-width: 1366px)"; -$bp-min-2556: "(min-width: 2556px)"; - -@mixin bp($point) { - @if $point == mobile { - @media #{$bp-min-720} { - @content; - } - } @else if $point == tablet { - @media #{$bp-min-1020} { - @content; - } - } @else if $point == desktop { - @media #{$bp-min-1366} { - @content; - } - } -} - -// Advanced positioning -// ---------------- -@mixin position( - $type, - $top: $position-default, - $right: $position-default, - $bottom: $position-default, - $left: $position-default -) { - position: $type; - $allowed_types: absolute relative fixed; - @if not index($allowed_types, $type) { - @warn "Unknown position: #{$type}."; - } - @each $data in top $top, right $right, bottom $bottom, left $left { - #{nth($data, 1)}: nth($data, 2); - } -} -@mixin absolute( - $top: $position-default, - $right: $position-default, - $bottom: $position-default, - $left: $position-default -) { - @include position(absolute, $top, $right, $bottom, $left); -} -@mixin relative( - $top: $position-default, - $right: $position-default, - $bottom: $position-default, - $left: $position-default -) { - @include position(relative, $top, $right, $bottom, $left); -} -@mixin fixed( - $top: $position-default, - $right: $position-default, - $bottom: $position-default, - $left: $position-default -) { - @include position(fixed, $top, $right, $bottom, $left); -} - -/// Center an element vertically and horizontally with an absolute position. -/// @group Mixins - -@mixin centerer { - @include absolute(50%, null, null, 50%); - transform: translate(-50%, -50%); -} - -/// This mixing allow you to add placeholder colors in all available browsers -/// @group Mixins - -@mixin placeholder { - &::-webkit-input-placeholder { - @content; - } - - &:-moz-placeholder { - /* Firefox 18- */ - @content; - } - - &::-moz-placeholder { - /* Firefox 19+ */ - @content; - } - - &:-ms-input-placeholder { - @content; - } -} - -/// Allows you to visually -/// @group Mixins - -@mixin hide-text { - font: 0/0 a; - color: transparent; - text-shadow: none; -} - -@mixin font-face($style-name, $file, $weight: unquote("normal"), $style: unquote("normal")) { - $filepath: "/fonts/" + $file; - @font-face { - font-family: "#{$style-name}"; - src: - url($filepath + ".woff2") format("woff2"), - url($filepath + ".ttf") format("truetype"); - font-weight: unquote($weight); - font-style: unquote($style); - } -} - -@mixin tooltipShow { - &:hover { - .icon-tooltip { - display: block; - left: 2rem; - animation: tooltipAppear 0.2s linear; - } - } - &.active { - .icon-tooltip { - display: block; - left: 2rem; - animation: tooltipAppear 0.2s linear; - } - } -} - -@mixin text-ellipsis { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; -} - -@mixin paragraph-ellipsis { - text-overflow: ellipsis; - overflow: hidden; - position: relative; - - &::after { - background-color: $color-gray-50; - bottom: -3px; - content: "..."; - padding-left: 10px; - position: absolute; - right: 2px; - } -} diff --git a/frontend/resources/styles/common/dependencies/reset.scss b/frontend/resources/styles/common/dependencies/reset.scss index 6092943ca38a83b2a40777afdcd77605fea53099..39e198d8d7b8df1b706063a573bde0adda648eb6 100644 --- a/frontend/resources/styles/common/dependencies/reset.scss +++ b/frontend/resources/styles/common/dependencies/reset.scss @@ -1,3 +1,7 @@ +// TODO: Legacy sass vars. We should not be using Sass vars here in this +// file at all. +$lh-143: 1.43; + /* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 @@ -97,6 +101,9 @@ video { border: 0; font: inherit; font-size: 100%; + // TODO: Changing line-height to 1 (as it should be) makes the visual tests + // fail with a max pixel diff ratio of 0.005. + // We should tackle this later. line-height: $lh-143; margin: 0; padding: 0; @@ -118,7 +125,7 @@ section { display: block; } body { - line-height: $lh-100; + line-height: 1; } ol, diff --git a/frontend/resources/styles/common/framework.scss b/frontend/resources/styles/common/framework.scss deleted file mode 100644 index 5a22085ee89d30847e1a887eb9dfa9b226e212e9..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/common/framework.scss +++ /dev/null @@ -1,1170 +0,0 @@ -// 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 - -// Buttons - -%btn { - appearance: none; - align-items: center; - border: none; - border-radius: $br3; - cursor: pointer; - display: flex; - font-family: "worksans", sans-serif; - font-size: $fs12; - height: 30px; - justify-content: center; - min-width: 25px; - padding: 0 1rem; - transition: all 0.4s; - text-decoration: none !important; - svg { - height: 16px; - width: 16px; - } - &.btn-large { - font-size: $fs14; - height: 40px; - svg { - height: 20px; - width: 20px; - } - } - &.btn-small { - height: 25px; - } -} - -.btn-primary { - @extend %btn; - background: $color-primary; - color: $color-black; - &:hover, - &:focus { - background: $color-black; - color: $color-primary; - } -} - -.btn-secondary { - @extend %btn; - background: $color-white; - border: 1px solid $color-gray-20; - color: $color-black; - &:hover { - background: $color-primary; - border-color: $color-primary; - color: $color-black; - } -} - -.btn-warning { - @extend %btn; - background: $color-warning; - color: $color-white; - &:hover { - background: $color-warning-dark; - color: $color-white; - } -} - -.btn-danger { - @extend %btn; - background: $color-danger; - color: $color-white; - &:hover { - background: $color-danger-dark; - color: $color-white; - } -} - -input[type="button"][disabled], -.btn-disabled { - background-color: $color-gray-10; - color: $color-gray-40; - pointer-events: none; -} - -// Slider dots - -ul.slider-dots { - align-items: center; - display: flex; - - li { - background-color: transparent; - border-radius: 50%; - border: 2px solid $color-white; - cursor: pointer; - height: 12px; - flex-shrink: 0; - margin: 6px; - width: 12px; - - &.current, - &:hover { - background-color: $color-gray-10; - } - } - - &.dots-purple { - li { - border-color: $color-complete; - - &.current, - &:hover { - background-color: $color-complete; - } - } - } -} - -// Doted list - -.doted-list { - li { - align-items: center; - display: flex; - padding: $size-2 0; - - &::before { - background-color: $color-complete; - border-radius: 50%; - content: ""; - flex-shrink: 0; - height: 10px; - margin-right: 6px; - width: 10px; - } - - &.not-included { - text-decoration: line-through; - } - } -} - -// Tags - -.tags { - display: flex; - flex-wrap: wrap; - - &:last-child { - margin-right: 0; - } - - .tag { - background-color: $color-gray-20; - border-radius: $br3; - color: $color-white; - cursor: pointer; - font-size: $fs14; - font-weight: $fw700; - margin: 0 $size-2 $size-2 0; - padding: 4px 8px; - text-transform: uppercase; - - &:hover { - background-color: $color-gray-40; - } - - &.tag-primary { - background-color: $color-primary; - color: $color-white; - - &:hover { - background-color: $color-primary-dark; - } - } - - &.tag-green { - background-color: $color-success; - color: $color-white; - - &:hover { - background-color: $color-success-dark; - } - } - - &.tag-purple { - background-color: $color-complete; - color: $color-white; - - &:hover { - background-color: $color-complete-dark; - } - } - - &.tag-orange { - background-color: $color-warning; - color: $color-white; - - &:hover { - background-color: $color-warning-dark; - } - } - - &.tag-red { - background-color: $color-danger; - color: $color-white; - - &:hover { - background-color: $color-danger-dark; - } - } - } -} - -// Input elements -.input-element { - display: flex; - flex-shrink: 0; - position: relative; - width: 75px; - - &::after, - .after { - color: $color-gray-20; - font-size: $fs12; - height: 20px; - position: absolute; - right: $size-2; - text-align: right; - top: 26%; - width: 18px; - - pointer-events: none; - max-width: 4rem; - overflow: hidden; - text-overflow: ellipsis; - } - - .after { - width: auto; - right: 6px; - } - - &.mini { - width: 43px; - } - - &.auto { - width: auto; - } - - // Input amounts - - &.pixels { - & input { - padding-right: 20px; - } - - &::after { - content: "px"; - } - } - - &.percentail { - &::after { - content: "%"; - } - } - - &.milliseconds { - &::after { - content: "ms"; - } - } - - &.degrees { - &::after { - content: "dg"; - } - } - - &.height { - &::after { - content: "H"; - } - } - - &.width { - &::after { - content: "W"; - } - } - - &.Xaxis { - &::after { - content: "X"; - } - } - - &.Yaxis { - &::after { - content: "Y"; - } - } - - &.maxW { - &::after { - content: attr(alt); - } - } - - &.minW { - &::after { - content: attr(alt); - } - } - - &.maxH { - &::after { - content: attr(alt); - } - } - - &.minH { - &::after { - content: attr(alt); - } - } - - &.large { - min-width: 7rem; - } -} - -input, -select { - background-color: $color-white; - box-sizing: border-box; - color: $color-gray-60; - font-family: "worksans", sans-serif; - font-size: $fs14; - margin-bottom: $size-4; - -webkit-appearance: none; - -moz-appearance: none; -} - -input[type="radio"], -input[type="checkbox"] { - box-sizing: border-box; - cursor: pointer; - line-height: $lh-normal; - margin-top: 1px 0 0; -} - -.form-errors { - color: $color-danger; -} - -// Input text - -.input-text { - border: none; - border-bottom: 1px solid transparent; - background-color: $color-white; - box-shadow: none; - outline: none; - padding: $size-2 $size-5 $size-2 $size-2; - position: relative; - - @include placeholder { - transition: all 0.3s ease; - } - - &:focus { - border-color: $color-gray-40; - box-shadow: none; - - @include placeholder { - opacity: 0; - transform: translateY(-20px); - transition: all 0.3s ease; - } - } - - &.success { - background-color: $color-success-lighter; - border-color: $color-success; - color: $color-success-dark; - } - - &.error { - background-color: $color-danger-lighter; - border-color: $color-danger; - color: $color-danger-dark; - } -} - -// Element-name - -input.element-name { - background-color: $color-white; - border: 1px solid $color-gray-40; - border-radius: $br3; - color: $color-gray-60; - font-size: $fs12; - margin: 0px; - padding: 3px; - width: 100%; -} - -// Input select - -.input-select { - @extend .input-text; - background-image: url("/images/icons/arrow-down-white.svg"); - background-repeat: no-repeat; - background-position: calc(100% - 4px) 48%; - background-size: 10px; - cursor: pointer; - - &.small { - padding: $size-1 $size-5 $size-1 $size-1; - } -} - -// Input radio - -.input-radio, -.input-checkbox { - align-items: center; - color: $color-gray-40; - display: flex; - margin-bottom: 10px; - margin-top: 10px; - padding-left: 0px; - - label { - cursor: pointer; - display: flex; - align-items: center; - margin-right: 15px; - font-size: $fs12; - - &:before { - content: ""; - width: 20px; - height: 20px; - margin-right: 10px; - background-color: $color-gray-10; - border: 1px solid $color-gray-30; - box-shadow: inset 0 0 0 0 $color-primary; - box-sizing: border-box; - flex-shrink: 0; - } - } - - &.column { - align-items: flex-start; - flex-direction: column; - } -} - -.input-radio { - label { - margin-bottom: 6px; - - &:before { - border-radius: $br99; - transition: - box-shadow 0.2s linear 0s, - color 0.2s linear 0s; - } - } - - input[type="radio"]:checked { - & + label { - &:before { - box-shadow: inset 0 0 0 5px $color-gray-20; - } - } - } - - input[type="radio"] { - display: none; - } - - input[type="radio"][disabled] { - & + label { - opacity: 0.65; - } - } -} -input[type="radio"]:checked + label:before { - .input-radio.radio-success & { - box-shadow: inset 0 0 0 5px $color-success; - } - - .input-radio.radio-primary & { - box-shadow: inset 0 0 0 5px $color-primary; - } - - .input-radio.radio-info & { - box-shadow: inset 0 0 0 5px $color-info; - } - - .input-radio.radio-warning & { - box-shadow: inset 0 0 0 5px $color-warning; - } - - .input-radio.radio-danger & { - box-shadow: inset 0 0 0 5px $color-danger; - } - - .input-radio.radio-complete & { - box-shadow: inset 0 0 0 5px $color-complete; - } -} - -// Input checkbox - -.input-checkbox { - input[type="radio"][disabled] { - & + label { - &:after { - background-color: $color-gray-10; - } - } - } - - label { - transition: - border 0.2s linear 0s, - color 0.2s linear 0s; - position: relative; - - &:before { - top: 1.4px; - border-radius: $br3; - transition: - border 0.2s linear 0s, - color 0.2s linear 0s; - } - - &::after { - display: inline-block; - width: 20px; - height: 20px; - position: absolute; - left: 3.2px; - top: 0; - font-size: $fs12; - transition: - border 0.2s linear 0s, - color 0.2s linear 0s; - } - - &:after { - border-radius: $br3; - } - } - - input[type="checkbox"] { - display: none; - } - - &.checkbox-circle { - label { - &:after { - border-radius: $br99; - } - - &:before { - border-radius: $br99; - } - } - } - - input[type="checkbox"]:checked { - & + label { - &:before { - border-width: 10px; - } - - &::after { - content: "✓"; - color: #ffffff; - font-size: $fs16; - } - } - } - - input[type="checkbox"][disabled] { - & + label { - opacity: 0.65; - - &:before { - background-color: #eceff3; - } - } - } - - input[type="checkbox"][indeterminate] { - & + label { - &::after { - content: "?"; - left: 4px; - } - } - } - - &.right { - label { - margin-right: 35px; - padding-left: 0 !important; - - &:before { - right: -35px; - left: auto; - } - } - - input[type="checkbox"]:checked { - & + label { - position: relative; - - &::after { - content: "✓"; - position: absolute; - right: -27px; - left: auto; - } - } - } - } -} - -input[type="checkbox"]:checked + label { - .input-checkbox.check-success &:before { - border-color: $color-success; - } - - .input-checkbox.check-primary &:before { - border-color: $color-primary; - } - - .input-checkbox.check-complete &:before { - border-color: $color-complete; - } - - .input-checkbox.check-warning &:before { - border-color: $color-warning; - } - - .input-checkbox.check-danger &:before { - border-color: $color-danger; - } - - .input-checkbox.check-info &:before { - border-color: $color-info; - } - - .input-checkbox.check-success &::after, - .input-checkbox.check-primary &::after, - .input-checkbox.check-complete &::after, - .input-checkbox.check-warning &::after, - .input-checkbox.check-danger &::after, - .input-checkbox.check-info &::after { - color: $color-white; - } -} - -// Input slidebar - -input[type="range"] { - background-color: transparent; - -webkit-appearance: none; - margin: 10px 0 10px 3px; - max-width: 70px; - width: 100%; -} -input[type="range"]:focus { - outline: none; -} -input[type="range"]::-webkit-slider-runnable-track { - width: 100%; - height: 6px; - cursor: pointer; - animate: 0.2s; - box-shadow: - 0px 0px 0px #000000, - 0px 0px 0px #0d0d0d; - background: $color-gray-60; - border-radius: $br25; - border: 0px solid #000101; -} -input[type="range"]::-webkit-slider-thumb { - box-shadow: - 0px 0px 0px #000000, - 0px 0px 0px #0d0d0d; - border: 0px solid #000000; - height: 18px; - width: 6px; - border-radius: $br7; - background: $color-gray-20; - cursor: pointer; - -webkit-appearance: none; - margin-top: -6px; -} -input[type="range"]:focus::-webkit-slider-runnable-track { - background: $color-gray-60; -} -input[type="range"]::-moz-range-track { - width: 100%; - height: 8px; - cursor: pointer; - animate: 0.2s; - box-shadow: - 0px 0px 0px #000000, - 0px 0px 0px #0d0d0d; - background: $color-gray-60; - border-radius: $br25; - border: 0px solid #000101; -} -input[type="range"]::-moz-range-thumb { - box-shadow: - 0px 0px 0px #000000, - 0px 0px 0px #0d0d0d; - border: 0px solid #000000; - height: 24px; - width: 8px; - border-radius: $br7; - background: $color-gray-20; - cursor: pointer; -} -input[type="range"]::-ms-track { - width: 100%; - height: 8px; - cursor: pointer; - animate: 0.2s; - background: transparent; - border-color: transparent; - border-width: 39px 0; - color: transparent; -} -input[type="range"]::-ms-fill-lower { - background: $color-gray-60; - border: 0px solid #000101; - border-radius: $br50; - box-shadow: - 0px 0px 0px #000000, - 0px 0px 0px #0d0d0d; -} -input[type="range"]::-ms-fill-upper { - background: $color-gray-60; - border: 0px solid #000101; - border-radius: $br50; - box-shadow: - 0px 0px 0px #000000, - 0px 0px 0px #0d0d0d; -} -input[type="range"]::-ms-thumb { - box-shadow: - 0px 0px 0px #000000, - 0px 0px 0px #0d0d0d; - border: 0px solid #000000; - height: 24px; - width: 8px; - border-radius: $br7; - background: $color-gray-20; - cursor: pointer; -} -input[type="range"]:focus::-ms-fill-lower { - background: $color-gray-60; -} -input[type="range"]:focus::-ms-fill-upper { - background: $color-gray-60; -} - -// Scroll bar (chrome) - -::-webkit-scrollbar { - background-color: transparent; - cursor: pointer; - height: 8px; - width: 8px; -} - -::-webkit-scrollbar-track, -::-webkit-scrollbar-corner { - background-color: transparent; -} - -::-webkit-scrollbar-thumb { - background-color: $color-gray-20; - - &:hover { - background-color: darken($color-gray-20, 14%); - outline: 2px solid $color-primary; - } -} - -// Tooltip - -.tooltip { - position: relative; - - &:hover { - &::after { - background-color: $color-white; - border-radius: $br3; - color: $color-gray-60; - content: attr(alt); - font-size: $fs12; - font-weight: $fw700; - padding: $size-1; - position: absolute; - left: 130%; - text-align: center; - top: 0; - white-space: nowrap; - z-index: 20; - @include animation(0.3s, 0.6s, fadeIn); - } - } - - // the default is the `right` - &.tooltip-bottom { - &:hover { - &::after { - left: 0; - top: 130%; - } - } - } - - &.tooltip-expand { - &:hover { - &::after { - min-width: 100%; - } - } - } - - &.tooltip-bottom-left { - &:hover { - &::after { - left: unset; - right: 0; - top: 130%; - } - } - } - - &.tooltip-top { - &:hover { - &::after { - top: -165%; - left: -60%; - } - } - } - - &.tooltip-right { - &:hover { - &::after { - top: 15%; - left: 120%; - } - } - } - - &.tooltip-left { - &:hover { - &::after { - left: unset; - right: 130%; - top: 15%; - } - } - } - - &.tooltip-hover { - &:hover { - &::after { - align-items: center; - background-color: $color-white; - box-sizing: border-box; - border-radius: $br0; - color: $color-gray-60; - display: flex; - height: 100%; - justify-content: center; - left: 0; - top: 0; - white-space: normal; - width: 100%; - } - } - } -} - -// Messages - -.banner { - position: relative; - - &.error { - background-color: $color-danger; - } - - &.success { - background-color: $color-success; - } - - &.warning { - background-color: $color-warning; - } - - &.info { - background-color: $color-info; - } - - &.hide { - @include animation(0, 0.6s, fadeOutUp); - } - - .icon { - display: flex; - - svg { - fill: $color-white; - height: 20px; - width: 20px; - } - } - - .content { - &.bottom-actions { - flex-direction: column; - - .actions { - margin-top: $size-4; - display: flex; - justify-content: flex-start; - } - } - - &.inline-actions { - flex-direction: row; - align-items: center; - justify-content: space-between; - - .actions { - display: flex; - justify-content: flex-start; - - .btn-secondary { - margin-left: $size-4; - } - } - } - - .link { - background: none; - border: none; - color: $color-info; - display: inline; - margin: 0; - text-decoration: underline; - } - } - - .btn-close { - position: absolute; - right: 0px; - top: 0px; - width: 48px; - height: 48px; - - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; - opacity: 0.35; - - svg { - fill: $color-black; - height: 18px; - width: 18px; - transform: rotate(45deg); - } - - &:hover { - opacity: 0.8; - } - } - - &.fixed { - border-radius: $br3; - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.2); - height: 48px; - max-width: 1000px; - min-width: 500px; - position: fixed; - padding-left: 16px; - top: 16px; - right: 16px; - z-index: 1005; - - display: flex; - align-items: center; - - .wrapper { - display: flex; - justify-content: center; - align-items: center; - padding-right: 64px; - - .icon { - margin-right: $size-4; - } - - .content { - color: $color-white; - display: flex; - align-items: center; - justify-content: center; - font-size: $fs14; - } - } - } - - &.floating, - &.inline { - min-height: 40px; - - .wrapper { - display: flex; - align-items: center; - - .icon { - padding: $size-2; - width: 48px; - height: 48px; - justify-content: center; - align-items: center; - } - - .content { - color: $color-black; - display: flex; - font-size: $fs14; - padding: $size-2; - width: 100%; - align-items: center; - - padding: 10px 15px; - min-height: 48px; - } - } - - &.error { - .content { - background-color: lighten($color-danger, 30%); - } - } - - &.success { - .content { - background-color: lighten($color-success, 30%); - } - } - - &.warning { - .content { - background-color: lighten($color-warning, 30%); - } - } - - &.info { - .content { - background-color: lighten($color-info, 30%); - } - } - } - - &.floating { - box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.18); - position: absolute; - top: 70px; - left: 0; - right: 0; - width: 40rem; - margin-left: auto; - margin-right: auto; - z-index: 20; - - &.error { - border: 1px solid $color-danger; - } - - &.success { - border: 1px solid $color-success; - } - - &.warning { - border: 1px solid $color-warning; - } - - &.info { - border: 1px solid $color-info; - } - } - - &.inline { - width: 100%; - } -} - -.close-bezier { - fill: $color-danger; - stroke: $color-danger-dark; - stroke-width: 2px; - cursor: pointer; - &:hover { - fill: $color-white; - } -} - -.message-inline { - background-color: $color-info; - color: $color-info-darker; - margin-bottom: 1.2rem; - padding: 0.8rem; - text-align: center; - p { - margin: 0; - } - .code { - font-family: monospace; - } -} - -[draggable] { - -moz-user-select: none; - -khtml-user-select: none; - -webkit-user-select: none; - user-select: none; - /* Required to make elements draggable in old WebKit */ - -khtml-user-drag: element; - -webkit-user-drag: element; -} - -.dnd-over > .element-list-body { - border: 1px solid white !important; -} - -.dnd-over-top { - border-top: 1px solid white !important; -} - -.dnd-over-bot { - border-bottom: 1px solid white !important; -} diff --git a/frontend/resources/styles/common/refactor/basic-rules.scss b/frontend/resources/styles/common/refactor/basic-rules.scss index 61a1c4c497ad187c5389430a8cb61356aded311a..5096ecd6a5cea357881877ccbb03e1c38d2d19e1 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -6,6 +6,14 @@ // SCROLLBAR .new-scrollbar { + scrollbar-width: thin; + scrollbar-color: rgba(170, 181, 186, 0.3) transparent; + &:hover { + scrollbar-color: rgba(170, 181, 186, 0.7) transparent; + } + + // These rules do not apply in chrome - 121 or higher + // We keep them to preserve backward compatibility. ::-webkit-scrollbar { background-color: transparent; cursor: pointer; @@ -32,6 +40,12 @@ background: var(--text-editor-selection-background-color); color: var(--text-editor-selection-foreground-color); } + + ::placeholder, + ::-webkit-input-placeholder { + @include bodySmallTypography; + color: var(--input-placeholder-color); + } } // BUTTONS @@ -39,7 +53,7 @@ @include buttonStyle; @include flexCenter; @include focusPrimary; - @include uppercaseTitleTipography; + @include headlineSmallTypography; background-color: var(--button-primary-background-color-rest); border: $s-1 solid var(--button-primary-border-color-rest); color: var(--button-primary-foreground-color-rest); @@ -223,7 +237,6 @@ .button-disabled { @include buttonStyle; @include flexCenter; - height: $s-32; background-color: var(--button-background-color-disabled); border: $s-1 solid var(--button-border-color-disabled); color: var(--button-foreground-color-disabled); @@ -278,9 +291,7 @@ // INPUTS .input-base { @include removeInputStyle; - @include bodySmallTypography; @include textEllipsis; - // @include focusInput; height: $s-28; width: 100%; flex-grow: 1; @@ -304,7 +315,7 @@ } .input-label { - @include uppercaseTitleTipography; + @include headlineSmallTypography; @include flexCenter; width: $s-20; padding-left: $s-8; @@ -313,7 +324,6 @@ } .input-element { - @include bodySmallTypography; @include focusInput; display: flex; align-items: center; @@ -402,6 +412,7 @@ background-color: var(--input-background-color); } ::placeholder { + @include bodySmallTypography; color: var(--input-placeholder-color); } &:hover { @@ -648,22 +659,6 @@ color: var(--modal-button-foreground-color-error); } -.loader-base { - @include flexCenter; - position: fixed; - top: 0; - left: 0; - height: 100vh; - width: 100vw; - z-index: $z-index-alert; - background-color: var(--loader-background); - :global(svg#loader-pencil) { - height: $s-100; - width: $s-100; - animation: loaderColor 5s infinite ease; - fill: var(--icon-foreground); - } -} // UI ELEMENTS .asset-element { @include bodySmallTypography; @@ -905,7 +900,7 @@ margin: 0; margin-top: $s-1; border-radius: $br-8; - z-index: $z-index-3; + z-index: $z-index-4; overflow-y: auto; overflow-x: hidden; background-color: var(--menu-background-color); diff --git a/frontend/resources/styles/common/refactor/color-defs.scss b/frontend/resources/styles/common/refactor/color-defs.scss index da1f0d784e20aa36152c1a1d1dfc0a3e3c693fe0..46e6ba1136869bdb6fa376d80e09ccca869b4eff 100644 --- a/frontend/resources/styles/common/refactor/color-defs.scss +++ b/frontend/resources/styles/common/refactor/color-defs.scss @@ -9,82 +9,46 @@ :root { // DARK // Dark background - --db-primary: #18181a; - --db-primary-60: #{color.change(#18181a, $alpha: 0.6)}; - --db-secondary: #000000; - --db-secondary-30: #{color.change(#000000, $alpha: 0.3)}; - --db-secondary-80: #{color.change(#000000, $alpha: 0.8)}; - --db-tertiary: #212426; - --db-quaternary: #2e3434; + --db-primary-60: #{color.change(#18181a, $alpha: 0.6)}; // used on overlay dark mode //Dark foreground - --df-primary: #ffffff; - --df-secondary: #8f9da3; - --df-secondary-40: #{color.change(#8f9da3, $alpha: 0.4)}; // TODO: Check if needed + --df-secondary: #8f9da3; // Used on button disabled background dark mode, grid metadata and some svg + --df-secondary-40: #{color.change(#8f9da3, $alpha: 0.4)}; // Used on button disabled foreground dark mode //Dark accent - --da-primary: #7efff5; - --da-primary-muted: #426158; - --da-secondary: #bb97d8; - --da-tertiary: #00d1b8; - --da-tertiary-10: #{color.change(#00d1b8, $alpha: 0.1)}; - --da-tertiary-70: #{color.change(#00d1b8, $alpha: 0.7)}; - --da-quaternary: #ff6fe0; + --da-tertiary-10: #{color.change(#00d1b8, $alpha: 0.1)}; // selection rect dark mode + --da-tertiary-70: #{color.change(#00d1b8, $alpha: 0.7)}; // selection rect background dark mode // LIGHT // Light background - --lb-primary: #ffffff; - --lb-primary-60: #{color.change(#ffffff, $alpha: 0.6)}; - --lb-secondary: #e8eaee; - --lb-secondary-30: #{color.change(#e8eaee, $alpha: 0.3)}; - --lb-secondary-80: #{color.change(#e8eaee, $alpha: 0.8)}; - --lb-tertiary: #f3f4f6; - --lb-quaternary: #eef0f2; + --lb-primary-60: #{color.change(#ffffff, $alpha: 0.6)}; // overlay color light mode + --lb-quaternary: #eef0f2; // background disabled token //Light foreground - --lf-primary: #000; - --lf-secondary: #495e74; - --lf-secondary-40: #{color.change(#495e74, $alpha: 0.4)}; - --lf-secondary-50: #{color.change(#495e74, $alpha: 0.5)}; + --lf-secondary-40: #{color.change(#495e74, $alpha: 0.4)}; // foreground disabled token //Light accent - --la-primary: #6911d4; - --la-primary-muted: #e1d2f5; - --la-secondary: #1345aa; - --la-tertiary: #8c33eb; - --la-tertiary-10: #{color.change(#8c33eb, $alpha: 0.1)}; - --la-tertiary-70: #{color.change(#8c33eb, $alpha: 0.7)}; - --la-quaternary: #ff6fe0; + --la-tertiary-10: #{color.change(#8c33eb, $alpha: 0.1)}; // selection rect light mode + --la-tertiary-70: #{color.change(#8c33eb, $alpha: 0.7)}; // selection rect background light mode // STATUS COLOR - --status-color-success-200: #a7e8d9; - --status-color-success-500: #2d9f8f; - --status-color-success-950: #0a2927; + --status-color-success-200: #a7e8d9; // Used on Register confirmation text + --status-color-success-500: #2d9f8f; // Used on accept icon, and status widget - --status-color-warning-200: #ffc8a8; - --status-color-warning-500: #fe4811; - --status-color-warning-950: #440806; + --status-color-warning-500: #fe4811; // Used on status widget, some buttons and warnings icons and elements - --status-color-error-200: #ffcada; - --status-color-error-500: #ff3277; - --status-color-error-950: #500124; + --status-color-error-500: #ff3277; // Used on discard icon, some borders and svg, and on status widget - --status-color-info-200: #bae3fd; - --status-color-info-500: #0e9be9; - --status-color-info-950: #082c49; - // Status color default will change with theme and will be defined on theme files - - //GENERIC - --color-canvas: #e8e9ea; + --status-color-info-500: #0e9be9; // used on pixel grid and status widget // APP COLORS - --app-white: #ffffff; - --app-black: #000; + --app-white: #ffffff; // Used in several places + --app-black: #000; // Used on interactions, measurements and editor files // SOCIAL LOGIN BUTTONS --google-login-background: #4285f4; --google-login-background-hover: #{color.adjust(#4285f4, $lightness: -15%)}; - --google-login-foreground: var(--df-primary); + --google-login-foreground: var(--app-white); --github-login-background: #4c4c4c; --github-login-background-hover: #{color.adjust(#4c4c4c, $lightness: -15%)}; diff --git a/frontend/resources/styles/common/refactor/common-dashboard.scss b/frontend/resources/styles/common/refactor/common-dashboard.scss index 5404d5630912004693d2662123b0f5a24485dffa..ed30f20a2e980f1166fbf7b457a7225cf5a7911f 100644 --- a/frontend/resources/styles/common/refactor/common-dashboard.scss +++ b/frontend/resources/styles/common/refactor/common-dashboard.scss @@ -53,12 +53,12 @@ align-items: center; flex-basis: $s-140; border-bottom: $s-3 solid transparent; - color: $df-secondary; + color: var(--color-foreground-secondary); height: $s-40; padding: $s-4 $s-24; font-weight: $fw400; &:hover { - color: $db-secondary; + color: var(--color-background-secondary); text-decoration: none; } } @@ -71,7 +71,7 @@ margin-left: $s-12; h1 { - color: $df-primary; + color: var(--color-foreground-primary); display: block; flex-shrink: 0; font-size: $fs-24; @@ -95,12 +95,14 @@ cursor: pointer; svg { - fill: $df-secondary; + stroke: var(--color-foreground-secondary); + fill: none; width: $s-16; height: $s-16; &:hover { - fill: $da-tertiary; + stroke: var(--color-accent-tertiary); + fill: none; } } } @@ -120,15 +122,15 @@ li { a { font-size: $s-16; - color: $df-secondary; + color: var(--color-foreground-secondary); border-color: transparent; &:hover { - color: $df-primary; + color: var(--color-foreground-primary); } } &.active { a { - color: $df-primary; + color: var(--color-foreground-primary); } } } @@ -144,7 +146,7 @@ .btn-secondary { @extend .button-secondary; - color: $df-primary; + color: var(--color-foreground-primary); font-size: $fs-12; text-transform: uppercase; padding: 0 $s-16; diff --git a/frontend/resources/styles/common/refactor/common-refactor.scss b/frontend/resources/styles/common/refactor/common-refactor.scss index b06f8723e8bbcaa3510c2fa9b2b6b490bbb37571..952beb6cc6ad1ca5b53206bc9554c4487b05ea69 100644 --- a/frontend/resources/styles/common/refactor/common-refactor.scss +++ b/frontend/resources/styles/common/refactor/common-refactor.scss @@ -18,19 +18,3 @@ @import "common/refactor/focus.scss"; @import "common/refactor/animations.scss"; @import "common/refactor/basic-rules.scss"; - -// Variables to use the library colors -$db-primary: var(--color-background-primary); -$db-secondary: var(--color-background-secondary); -$db-tertiary: var(--color-background-tertiary); -$db-quaternary: var(--color-background-quaternary); -$db-subtle: var(--color-background-subtle); -$db-disabled: var(--color-background-disabled); - -$df-primary: var(--color-foreground-primary); -$df-secondary: var(--color-foreground-secondary); -$df-tertiary: var(--color-accent-quaternary); -$da-primary: var(--color-accent-primary); -$da-primary-muted: var(--color-accent-primary-muted); -$da-secondary: var(--color-accent-secondary); -$da-tertiary: var(--color-accent-tertiary); diff --git a/frontend/resources/styles/common/refactor/design-tokens.scss b/frontend/resources/styles/common/refactor/design-tokens.scss index 4049cc42e359f5c136e3ce26f9163e5011c4a44c..4ca89d40806b72d7ce9942e58e71f389932bd88d 100644 --- a/frontend/resources/styles/common/refactor/design-tokens.scss +++ b/frontend/resources/styles/common/refactor/design-tokens.scss @@ -215,7 +215,7 @@ --menu-shortcut-foreground-color: var(--color-foreground-secondary); --menu-shortcut-foreground-color-selected: var(--color-foreground-primary); --menu-shortcut-foreground-color-hover: var(--color-foreground-primary); - --menu-shadow-color: var(--shadow-color); + --menu-shadow-color: var(--color-shadow); --menu-background-color-disabled: var(--color-background-primary); --menu-foreground-color-disabled: var(--color-foreground-secondary); --menu-border-color-disabled: var(--color-background-quaternary); @@ -229,7 +229,7 @@ --assets-title-background-color: var(--color-background-primary); --assets-item-background-color: var(--color-background-tertiary); --assets-item-background-color-hover: var(--color-background-quaternary); - --assets-item-name-background-color: var(--db-secondary-80); // TODO: penpot file has a non-existing token + --assets-item-name-background-color: var(--color-background-primary); --assets-item-name-foreground-color-rest: var(--color-foreground-secondary); --assets-item-name-foreground-color: var(--color-foreground-primary); --assets-item-name-foreground-color-hover: var(--color-foreground-primary); @@ -237,10 +237,12 @@ --assets-item-border-color: var(--color-accent-primary); --assets-item-background-color-drag: transparent; --assets-item-border-color-drag: var(--color-accent-primary-muted); - --assets-component-background-color: var(--color-foreground-secondary); - --assets-component-background-color-disabled: var(--df-secondary;); + --assets-component-background-color: var(--color-canvas); + --assets-component-background-color-disabled: var(--color-foreground-secondary); --assets-component-border-color: var(--color-background-tertiary); --assets-component-border-selected: var(--color-accent-tertiary); + --assets-component-second-border-selected: var(--color-background-primary); + --assets-component-hightlight: var(--color-accent-secondary); --radio-btns-background-color: var(--color-background-tertiary); --radio-btn-background-color-selected: var(--color-background-quaternary); @@ -315,47 +317,47 @@ --modal-navigator-foreground-color-rest: var(--color-background-quaternary); --modal-navigator-foreground-color-active: var(--color-accent-primary); - // ALERTS NOTIFICATION TOAST & STATUS WIDGET - --alert-background-color-success: var(--color-success-background); + // ALERTS, NOTIFICATION, TOAST & BADGES + + --alert-background-color-default: var(--color-background-primary); + --alert-text-foreground-color-default: var(--color-foreground-primary); + --alert-icon-foreground-color-default: var(--color-foreground-primary); + --alert-border-color-default: var(--color-background-quaternary); + + --alert-background-color-success: var(--color-background-success); --alert-text-foreground-color-success: var(--color-foreground-primary); - --alert-icon-foreground-color-success: var(--color-success-foreground); - --alert-border-color-success: var(--color-success-foreground); + --alert-icon-foreground-color-success: var(--color-accent-success); + --alert-border-color-success: var(--color-accent-success); - --alert-background-color-warning: var(--color-warning-background); + --alert-background-color-warning: var(--color-background-warning); --alert-text-foreground-color-warning: var(--color-foreground-primary); - --alert-icon-foreground-color-warning: var(--color-warning-foreground); - --alert-border-color-warning: var(--color-warning-foreground); + --alert-icon-foreground-color-warning: var(--color-accent-warning); + --alert-border-color-warning: var(--color-accent-warning); - --alert-background-color-error: var(--color-error-background); + --alert-background-color-error: var(--color-background-error); --alert-text-foreground-color-error: var(--color-foreground-primary); - --alert-icon-foreground-color-error: var(--color-error-foreground); - --alert-border-color-error: var(--color-error-foreground); + --alert-icon-foreground-color-error: var(--color-accent-error); + --alert-border-color-error: var(--color-accent-error); - --alert-background-color-info: var(--color-info-background); + --alert-background-color-info: var(--color-background-info); --alert-text-foreground-color-info: var(--color-foreground-primary); - --alert-icon-foreground-color-info: var(--color-info-foreground); - --alert-border-color-info: var(--color-info-foreground); + --alert-icon-foreground-color-info: var(--color-accent-info); + --alert-border-color-info: var(--color-accent-info); - --alert-background-color-default: var(--color-background-primary); - --alert-text-foreground-color-default: var(--color-foreground-primary); - --alert-icon-foreground-color-default: var(--color-foreground-primary); - --alert-border-color-default: var(--color-background-quaternary); + --alert-text-foreground-color-focus: var(--color-accent-primary); + --alert-border-color-focus: var(--color-accent-primary); - --notification-background-color-success: var(); - --notification-foreground-color-success: var(); - --notification-border-color-success: var(); --notification-foreground-color-default: var(--color-foreground-secondary); + --element-foreground-warning: var(--status-color-warning-500); + --element-foreground-error: var(--status-color-error-500); + + // STATUS WIDGET --status-widget-background-color-success: var(--status-color-success-500); --status-widget-background-color-warning: var(--status-color-warning-500); - --status-widget-background-color-pending: var(--status-color-pending-500); + --status-widget-background-color-pending: var(--status-color-info-500); --status-widget-background-color-error: var(--status-color-error-500); - --status-widget-icon-foreground-color: var(--color-background-primary); // TODO review - - --element-foreground-success: var(--status-color-success-500); - --element-foreground-warning: var(--status-color-warning-500); - --element-foreground-pending: var(--status-color-info-500); - --element-foreground-error: var(--status-color-error-500); + --status-widget-icon-foreground-color: var(--color-background-primary); // INTERFACE ELEMENTS --search-bar-background-color: var(--color-background-primary); @@ -370,10 +372,8 @@ --pill-background-color: var(--color-background-tertiary); --pill-foreground-color: var(--color-foreground-primary); - --tag-background-color: var(--color-accent-primary); - --tag-background-color-disabled: var(--color-foreground-primary); - --link-foreground-color: var(--color-accent-primary); + --register-confirmation-color: var(--status-color-success-200); //TODO: review this color --resize-area-background-color: var(--color-background-primary); --resize-area-border-color: var(--color-background-quaternary); @@ -382,6 +382,7 @@ --dashboard-list-background-color: var(--color-background-tertiary); --dashboard-list-foreground-color: var(--color-foreground-primary); --dashboard-list-text-foreground-color: var(--color-foreground-secondary); + --flow-tag-background-color: var(--color-background-tertiary); --flow-tag-foreground-color: var(--color-foreground-secondary); --flow-tag-background-color-hover: var(--color-background-quaternary); @@ -421,8 +422,6 @@ } .light { - --assets-component-background-color: var(--color-background-secondary); - --tabs-background-color: var(--color-background-tertiary); --tab-background-color-selected: var(--color-background-primary); --tab-border-color: var(--color-background-tertiary); @@ -438,7 +437,6 @@ --button-icon-background-color-selected: var(--color-background-primary); --button-icon-border-color-selected: var(--color-background-secondary); - --assets-item-name-background-color: var(--color-background-primary); --assets-item-name-foreground-color: var(--color-foreground-primary); --text-editor-selection-background-color: var(--la-tertiary-70); diff --git a/frontend/resources/styles/common/refactor/fonts.scss b/frontend/resources/styles/common/refactor/fonts.scss index 81a8b5f67026e3e158023930ca822b863ee5cfc5..015555225adc6cfdd8842314707abc145d0f3e0d 100644 --- a/frontend/resources/styles/common/refactor/fonts.scss +++ b/frontend/resources/styles/common/refactor/fonts.scss @@ -5,7 +5,6 @@ // Copyright (c) KALEIDOS INC @use "sass:math"; -@import "common/dependencies/mixin"; // Typography scale $fs-base: 16; @@ -24,3 +23,6 @@ $fs-36: math.div(36, $fs-base) + rem; $fw400: 400; // Regular (CSS value: 'normal') $fw500: 500; // Medium $fw700: 700; // Bold (CSS value: 'bold') + +// Line heights +$lh-150: 1.5; diff --git a/frontend/resources/styles/common/refactor/mixins.scss b/frontend/resources/styles/common/refactor/mixins.scss index 60f2f1e3d0f12c94f79ca19cdc48cc9831e8bb30..f2d734ccf049fd65c5d5c159b558f7dee63d58af 100644 --- a/frontend/resources/styles/common/refactor/mixins.scss +++ b/frontend/resources/styles/common/refactor/mixins.scss @@ -35,7 +35,7 @@ } @mixin uppercaseTitleTipography { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-11; font-weight: $fw500; line-height: 1.2; @@ -43,28 +43,28 @@ } @mixin bigTitleTipography { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-24; font-weight: $fw400; line-height: 1.2; } @mixin medTitleTipography { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-20; font-weight: $fw400; line-height: 1.2; } @mixin smallTitleTipography { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-14; font-weight: $fw400; line-height: 1.2; } @mixin headlineLargeTypography { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-18; line-height: 1.2; text-transform: uppercase; @@ -72,7 +72,7 @@ } @mixin headlineMediumTypography { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-16; line-height: 1.4; text-transform: uppercase; @@ -80,7 +80,7 @@ } @mixin headlineSmallTypography { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-12; line-height: 1.2; text-transform: uppercase; @@ -88,21 +88,21 @@ } @mixin bodyLargeTypography { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-16; line-height: 1.5; font-weight: $fw400; } @mixin bodyMediumTypography { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-14; line-height: 1.4; font-weight: $fw400; } @mixin bodySmallTypography { - font-family: "worksans", sans-serif; + font-family: "worksans", "vazirmatn", sans-serif; font-size: $fs-12; font-weight: $fw400; line-height: 1.4; @@ -116,11 +116,11 @@ } @mixin textEllipsis { + display: block; max-width: 99%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - display: block; } @mixin twoLineTextEllipsis { diff --git a/frontend/resources/styles/common/refactor/themes/default-theme.scss b/frontend/resources/styles/common/refactor/themes/default-theme.scss index 432906c9e56499b2309e86e3a62d3fd9c1e5e6f7..f7d092338a12cb987ecdc381c26d5e4b5e72f755 100644 --- a/frontend/resources/styles/common/refactor/themes/default-theme.scss +++ b/frontend/resources/styles/common/refactor/themes/default-theme.scss @@ -7,39 +7,9 @@ @use "sass:meta"; :root { - --color-background-primary: var(--db-primary); - --color-background-secondary: var(--db-secondary); - --color-background-tertiary: var(--db-tertiary); - --color-background-quaternary: var(--db-quaternary); - --color-background-subtle: var(--db-secondary-30); --color-background-disabled: var(--df-secondary); - --color-foreground-primary: var(--df-primary); - --color-foreground-secondary: var(--df-secondary); --color-foreground-disabled: var(--df-secondary-40); - --color-accent-primary: var(--da-primary); - --color-accent-primary-muted: var(--da-primary-muted); - --color-accent-secondary: var(--da-secondary); - --color-accent-tertiary: var(--da-tertiary); - --color-accent-tertiary-muted: var(--da-tertiary-10); - --color-accent-quaternary: var(--da-quaternary); - --color-component-highlight: var(--da-secondary); - - --color-success-background: var(--status-color-success-950); - --color-success-foreground: var(--status-color-success-500); - - --color-warning-background: var(--status-color-warning-950); - --color-warning-foreground: var(--status-color-warning-500); - - --color-error-background: var(--status-color-error-950); - --color-error-foreground: var(--status-color-error-500); - - --color-info-background: var(--status-color-info-950); - --color-info-foreground: var(--status-color-info-500); + --color-accent-tertiary-muted: var(--da-tertiary-10); // selection rect --overlay-color: var(--db-primary-60); - - --shadow-color: var(--db-secondary-30); - --radio-button-box-shadow: 0 0 0 1px var(--db-secondary-30) inset; - - @include meta.load-css("hljs-dark-theme"); } diff --git a/frontend/resources/styles/common/refactor/themes/light-theme.scss b/frontend/resources/styles/common/refactor/themes/light-theme.scss index cd9b6e61c7f46a00ff37c92a34e0d97217051126..8baec1aa94d55cde7afad457659f004dc34a4931 100644 --- a/frontend/resources/styles/common/refactor/themes/light-theme.scss +++ b/frontend/resources/styles/common/refactor/themes/light-theme.scss @@ -7,38 +7,9 @@ @use "sass:meta"; .light { - --color-background-primary: var(--lb-primary); - --color-background-secondary: var(--lb-secondary); - --color-background-tertiary: var(--lb-tertiary); - --color-background-quaternary: var(--lb-quaternary); - --color-background-subtle: var(--lb-secondary-30); //Whatch this¡¡ --color-background-disabled: var(--lb-quaternary); - --color-foreground-primary: var(--lf-primary); - --color-foreground-secondary: var(--lf-secondary); --color-foreground-disabled: var(--lf-secondary-40); - --color-accent-primary: var(--la-primary); - --color-accent-primary-muted: var(--la-primary-muted); - --color-accent-secondary: var(--la-secondary); - --color-accent-tertiary: var(--la-tertiary); --color-accent-tertiary-muted: var(--la-tertiary-10); - --color-accent-quaternary: var(--la-quaternary); - --color-component-highlight: var(--la-secondary); - - --color-success-background: var(--status-color-success-200); - --color-success-foreground: var(--status-color-success-500); - - --color-warning-background: var(--status-color-warning-200); - --color-warning-foreground: var(--status-color-warning-500); - - --color-error-background: var(--status-color-error-200); - --color-error-foreground: var(--status-color-error-500); - - --color-info-background: var(--status-color-info-200); - --color-info-foreground: var(--status-color-info-500); --overlay-color: var(--lb-primary-60); - --shadow-color: var(--lf-secondary-40); - --radio-button-box-shadow: 0 0 0 1px var(--lb-secondary) inset; - - @include meta.load-css("hljs-light-theme"); } diff --git a/frontend/resources/styles/common/refactor/z-index.scss b/frontend/resources/styles/common/refactor/z-index.scss index 5be8e2e5bf327c56b75579439fe3cc341695af58..755b2e9fad386fe71d0ff2addee68ff34288ea44 100644 --- a/frontend/resources/styles/common/refactor/z-index.scss +++ b/frontend/resources/styles/common/refactor/z-index.scss @@ -6,8 +6,9 @@ $z-index-1: 1; // floating elements $z-index-2: 2; // sidebars -$z-index-3: 3; // context menu -$z-index-4: 4; // modal +$z-index-3: 3; // topbar +$z-index-4: 4; // context menu +$z-index-5: 5; // modal $z-index-10: 10; $z-index-20: 20; $z-index-modal: 30; // When refactor finish we can reduce this number, diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index f3fd5e5bccdf1e72389168937d9262298414907a..dfd83571dc05e6297dc5e1dd2b6afd5396777d3b 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -4,27 +4,16 @@ // // Copyright (c) KALEIDOS INC -//################################################# -// Import libraries -//################################################# - -@use "sass:color"; - //################################################# // MAIN STYLES //################################################# -@import "common/dependencies/colors"; -@import "common/dependencies/helpers"; -@import "common/dependencies/mixin"; -@import "common/dependencies/fonts"; @import "common/dependencies/reset"; +@import "common/refactor/color-defs.scss"; +@import "common/dependencies/fonts"; @import "common/dependencies/animations"; -@import "common/dependencies/z-index"; -@import "common/dependencies/highlightjs-theme"; +@import "common/dependencies/highlight.scss"; -@import "animate"; -@import "common/refactor/color-defs.scss"; @import "common/refactor/themes.scss"; @import "common/refactor/design-tokens.scss"; @@ -33,36 +22,11 @@ //################################################# @import "common/base"; -@import "main/layouts/login"; -@import "main/layouts/main-layout"; -@import "main/layouts/not-found"; -@import "main/layouts/viewer"; -@import "main/layouts/inspect"; //################################################# // Commons //################################################# -@import "common/framework"; -@import "main/partials/forms"; +// TODO: remove this stylesheet once the new text editor is in place +// https: //tree.taiga.io/project/penpot/us/8165 @import "main/partials/texts"; -@import "main/partials/context-menu"; -@import "main/partials/dropdown"; - -//################################################# -// Partials -//################################################# - -@import "main/partials/activity-bar"; -@import "main/partials/debug-icons-preview"; -@import "main/partials/editable-label"; -@import "main/partials/loader"; -@import "main/partials/project-bar"; -@import "main/partials/sidebar"; -@import "main/partials/tab-container"; -@import "main/partials/tool-bar"; -@import "main/partials/user-settings"; -@import "main/partials/workspace"; -@import "main/partials/color-bullet"; -@import "main/partials/exception-page"; -@import "main/partials/signup-questions"; diff --git a/frontend/resources/styles/main/layouts/inspect.scss b/frontend/resources/styles/main/layouts/inspect.scss deleted file mode 100644 index bbf4d1556b47a1b373138f76696b0485aff20039..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/layouts/inspect.scss +++ /dev/null @@ -1,161 +0,0 @@ -$width-left-toolbar: 48px; -$width-settings-bar: 256px; - -.inspect-layout { - height: 100vh; - display: grid; - grid-template-rows: 48px auto; - grid-template-columns: 1fr; - user-select: none; - - .viewer-header { - grid-column: 1 / span 1; - grid-row: 1 / span 1; - } - - .viewer-content { - grid-column: 1 / span 1; - grid-row: 2 / span 1; - } -} - -.fullscreen.inspect-layout.force-visible { - display: grid; - grid-template-rows: 1fr; - - & .viewer-header { - position: fixed; - top: 0; - transition: top 400ms ease 300ms; - margin-bottom: 0; - z-index: 10; - } - - & .viewer-bottom { - position: fixed; - bottom: 0; - transition: bottom 400ms ease 300ms; - z-index: 2; - } -} - -.fullscreen.inspect-layout:not(.force-visible) { - & .viewer-header { - width: 100%; - position: fixed; - top: -48px; - left: 0; - transition: top 400ms ease 300ms; - z-index: 10; - margin-bottom: 48px; - - &::after { - content: " "; - position: absolute; - width: 100%; - height: 1rem; - left: 0; - top: 48px; - } - } - - & .viewer-header:hover { - top: 0; - transition: top 200ms; - } - - & .viewer-bottom { - width: 100%; - position: fixed; - bottom: -48px; - left: 0; - transition: bottom 400ms ease 300ms; - z-index: 2; - &::after { - content: " "; - position: absolute; - width: 100%; - height: 1rem; - left: 0; - bottom: 0px; - } - } - - & .viewer-bottom:hover { - bottom: 0px; - transition: bottom 200ms; - } - - & .viewer-content { - grid-row: 1 / span 2; - } -} - -.inspect-layout { - .viewer-section { - flex-wrap: nowrap; - margin-top: 0; - &.fullscreen { - .settings-bar, - .settings-bar { - padding-top: 48px; - } - } - } - - .settings-bar { - width: $width-settings-bar; - - &.settings-bar-right, - &.settings-bar-left { - height: 100%; - position: relative; - left: unset; - right: unset; - - .settings-bar-inside { - padding-top: 0.5rem; - overflow-y: auto; - } - } - - &.settings-bar-right { - width: 100%; - grid-area: right-sidebar; - } - } - - .inspect-svg-wrapper { - flex: 1; - overflow: hidden; - flex-direction: column; - justify-content: flex-start; - position: relative; - } - - .inspect-svg-container { - display: grid; - width: 100%; - height: 100%; - overflow: auto; - align-items: center; - justify-content: safe center; - margin: 0 auto; - } -} - -.sidebar-container { - display: flex; - flex-direction: column; - width: var(--width, $width-settings-bar); - height: 100%; - overflow: hidden; - - & > .resize-area { - position: absolute; - width: 8px; - height: 100%; - z-index: 10; - cursor: ew-resize; - } -} diff --git a/frontend/resources/styles/main/layouts/login.scss b/frontend/resources/styles/main/layouts/login.scss deleted file mode 100644 index cee660c18ad7decae3aa79d0bd88ab3f4bd0c94b..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/layouts/login.scss +++ /dev/null @@ -1,250 +0,0 @@ -// 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 - -// TODO: rename to auth.scss - -.auth { - display: grid; - grid-template-rows: auto; - grid-template-columns: 33% auto; - height: 100vh; - overflow-y: scroll; -} - -.auth-sidebar { - grid-column: 1 / span 1; - height: 100vh; - display: flex; - padding-top: 7vh; - flex-direction: column; - align-items: center; - justify-content: flex-start; - background-color: #151035; - background-image: url("/images/login-penpot.svg"); - background-position: center 30vh; - background-size: 96%; - background-repeat: no-repeat; - - .tagline { - text-align: center; - width: 280px; - font-size: $fs18; - margin-top: 2vh; - color: white; - } - - .logo { - svg { - fill: white; - max-width: 11vw; - height: 80px; - } - .hidden-name { - visibility: hidden; - width: 0; - height: 0; - float: left; - } - } -} - -.auth-content { - grid-column: 2 / span 1; - background-color: $color-white; - display: flex; - align-items: center; - justify-content: center; - position: relative; - - .form-container { - width: 412px; - flex-direction: column; - margin-bottom: 30px; - .auth-buttons { - margin: $size-6 0 $size-4 0; - display: flex; - justify-content: center; - column-gap: 17px; - } - - form { - margin: 2rem 0 0.5rem 0; - .accept-terms-and-privacy-wrapper { - position: relative; - .input-checkbox { - margin-bottom: 0; - } - .input-checkbox input[type="checkbox"] { - position: absolute; - display: block; - width: 20px; - height: 20px; - opacity: 0; - top: 22px; - } - label { - margin-left: 40px; - } - label:before { - position: absolute; - top: 15px; - left: -36px; - } - label:after { - position: absolute; - top: 15px; - left: -33px; - } - .input-checkbox input[type="checkbox"]:focus { - opacity: 100%; - } - .auth-links { - margin-left: 40px; - font-size: 0.75rem; - } - } - } - } - - input { - margin-bottom: 0px; - } - - .buttons-stack { - display: flex; - flex-direction: column; - width: 100%; - - *:not(:last-child) { - margin-bottom: $size-4; - } - } - - .btn-large { - flex-grow: 1; - font-size: $fs14; - font-style: normal; - font-weight: $fw400; - } - - .btn-google-auth { - background-color: #4285f4; - color: $color-white; - margin-bottom: $size-4; - text-decoration: none; - .logo { - width: 20px; - height: 20px; - margin-right: 1rem; - } - &:hover, - &:focus { - background-color: #2065d7; - color: $color-white; - } - } - - .btn-gitlab-auth { - background-color: #fc6d26; - color: $color-white; - margin-bottom: $size-4; - text-decoration: none; - - .logo { - width: 20px; - height: 20px; - margin-right: 1rem; - } - - &:hover, - &:focus { - background-color: #ee5f18; - color: $color-white; - } - } - - .btn-github-auth { - background-color: #4c4c4c; - color: $color-white; - margin-bottom: $size-4; - text-decoration: none; - - .logo { - width: 20px; - height: 20px; - margin-right: 1rem; - } - - &:hover, - &:focus { - background-color: #2f2f2f; - color: $color-white; - } - } - - .link-oidc { - text-align: center; - } - - .separator { - display: flex; - justify-content: center; - width: 100%; - text-transform: uppercase; - text-align: center; - - .text { - margin: 0 10px; - color: $color-gray-40; - } - - .line { - border: 1px solid $color-gray-10; - flex-grow: 10; - margin: auto; - } - } - - .links { - display: flex; - font-size: $fs14; - flex-direction: column; - justify-content: space-between; - margin-top: $size-4; - margin-bottom: $size-4; - - &.demo { - justify-content: center; - margin-top: $size-5; - } - - .link-entry { - font-size: $fs14; - color: $color-gray-40; - margin-bottom: 10px; - a { - font-size: $fs14; - font-weight: $fw500; - color: $color-gray-50; - &:hover, - &:focus { - text-decoration: underline; - } - } - } - } - - .terms-login { - bottom: $size-5; - font-size: $fs14; - position: absolute; - - span { - margin: 0 $size-2; - color: $color-gray-40; - } - } -} diff --git a/frontend/resources/styles/main/layouts/not-found.scss b/frontend/resources/styles/main/layouts/not-found.scss deleted file mode 100644 index d9cda82425b294c1f6218b9ea8b4a0c4588c6cb5..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/layouts/not-found.scss +++ /dev/null @@ -1,80 +0,0 @@ -.not-found-layout { - display: grid; - - grid-template-rows: 120px auto; - grid-template-columns: 1fr; -} - -.not-found-header { - grid-column: 1 / span 1; - grid-row: 1 / span 1; - - display: flex; - align-items: center; - padding: 32px; - - svg { - height: 55px; - width: 170px; - } -} - -.not-found-content { - grid-column: 1 / span 1; - grid-row: 1 / span 2; - height: 100vh; - - display: flex; - justify-content: center; - align-items: center; - - .container { - max-width: 600px; - } - - .image { - align-items: center; - display: flex; - justify-content: center; - margin-bottom: 2rem; - - svg { - height: 220px; - width: 220px; - } - } - - .main-message { - color: $color-black; - font-size: $fs80; - line-height: $lh-188; // Original value was 150px; 150px/80px = 187.5 % => lh-188 (rounded) - text-align: center; - } - - .desc-message { - color: $color-black; - font-size: $fs26; - font-weight: $fw300; - text-align: center; - } - - .sign-info { - margin-top: 20px; - color: $color-black; - font-size: $fs16; - font-weight: $fw200; - text-align: center; - - display: flex; - flex-direction: column; - align-items: center; - - b { - font-weight: $fw400; - } - - .btn-primary { - margin-top: 15px; - } - } -} diff --git a/frontend/resources/styles/main/layouts/viewer.scss b/frontend/resources/styles/main/layouts/viewer.scss deleted file mode 100644 index 840c2b55918426271a6cceda5a36afaf6201bd08..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/layouts/viewer.scss +++ /dev/null @@ -1,101 +0,0 @@ -.viewer-layout { - height: 100vh; - display: grid; - grid-template-rows: 48px auto; - grid-template-columns: 1fr; - user-select: none; - - .viewer-header { - grid-column: 1 / span 1; - grid-row: 1 / span 1; - } - - .viewer-content { - grid-column: 1 / span 1; - grid-row: 2 / span 1; - } -} - -.fullscreen.viewer-layout.force-visible { - grid-template-rows: 1fr; - & .viewer-header { - position: fixed; - top: 0; - transition: top 400ms ease 300ms; - margin-bottom: 0; - z-index: 2; - } - - & .viewer-bottom { - position: fixed; - bottom: 0; - transition: bottom 400ms ease 300ms; - z-index: 2; - } -} - -.fullscreen.viewer-layout:not(.force-visible) { - grid-template-rows: 1fr; - & .viewer-header { - width: 100%; - position: fixed; - top: -48px; - left: 0; - transition: top 400ms ease 300ms; - z-index: 2; - margin-bottom: 48px; - &::after { - content: " "; - position: absolute; - width: 100%; - height: 1rem; - left: 0; - top: 48px; - } - } - - & .viewer-header:hover { - top: 0; - transition: top 200ms; - } - - & .viewer-bottom { - width: 100%; - position: fixed; - bottom: -48px; - left: 0; - transition: bottom 400ms ease 300ms; - z-index: 2; - &::after { - content: " "; - position: absolute; - width: 100%; - height: 1rem; - left: 0; - bottom: 0px; - } - } - - & .viewer-bottom:hover { - bottom: 0px; - transition: bottom 200ms; - } - - & .viewer-content { - grid-row: 1 / span 2; - } -} - -.viewer-overlay { - position: absolute; -} - -.viewer-overlay-background { - position: absolute; - top: 0; - left: 0; - - &.visible { - background-color: rgb(0, 0, 0, 0.2); - } -} diff --git a/frontend/resources/styles/main/partials/activity-bar.scss b/frontend/resources/styles/main/partials/activity-bar.scss deleted file mode 100644 index 5e5ad957ba154edc262e3c4c850fbc246aa1237e..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/activity-bar.scss +++ /dev/null @@ -1,77 +0,0 @@ -// 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 - -.activity-bar { - background-color: $color-gray-50; - bottom: 0; - height: 100%; - position: fixed; - right: 0; - width: 250px; - - .activity-bar-inside { - align-items: center; - display: flex; - flex-direction: column; - overflow-y: auto; - padding-top: 70px; - } - - h4 { - color: $color-gray-40; - font-size: $fs16; - font-weight: $fw700; - margin-bottom: $size-1; - } - - .date-ribbon { - background-color: lighten($color-gray-20, 12%); - color: $color-white; - font-size: $fs12; - font-weight: $fw700; - padding: 2px; - text-align: center; - width: 100%; - } - - .activity-input { - border-bottom: 1px solid $color-gray-10; - display: flex; - font-size: $fs12; - padding: $size-2; - width: 100%; - - img.activity-author { - border-radius: 50%; - flex-shrink: 0; - height: 30px; - margin-right: $size-4; - width: 30px; - } - - .activity-content { - display: flex; - flex-direction: column; - - .activity-project { - align-items: center; - display: flex; - flex-wrap: wrap; - - a { - font-weight: $fw700; - margin: 0 3px; - } - } - - .activity-time { - color: $color-gray-20; - font-size: $fs12; - font-style: italic; - } - } - } -} diff --git a/frontend/resources/styles/main/partials/color-bullet.scss b/frontend/resources/styles/main/partials/color-bullet.scss deleted file mode 100644 index 43983d50fb8d840e3d79b061d2e899390e6f42e7..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/color-bullet.scss +++ /dev/null @@ -1,209 +0,0 @@ -// 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 - -.color-bullet { - border: 2px solid $color-gray-30; - position: relative; - width: var(--bullet-size); - height: var(--bullet-size); - - &:hover { - border-color: $color-primary; - } -} -.color-cell { - display: grid; - grid-template-columns: 100%; - grid-template-rows: 1fr auto; - height: 100%; - justify-items: center; - width: 65px; - - .color-bullet { - border: 2px solid $color-gray-30; - position: relative; - width: var(--bullet-size); - height: var(--bullet-size); - - &:hover { - border-color: $color-primary; - } - } - - & > * { - overflow: hidden; - } -} - -.color-cell.current { - .color-bullet { - border-color: $color-gray-30; - } -} - -ul.palette-menu .color-bullet { - width: 20px; - height: 20px; - border: 1px solid $color-gray-30; - margin-right: 5px; - background-size: 8px; -} -.color-cell.add-color .color-bullet { - align-items: center; - background-color: $color-gray-50; - border: 3px dashed $color-gray-30; - cursor: pointer; - display: flex; - justify-content: center; - margin-bottom: 1rem; - padding: 0.6rem; - - svg { - fill: $color-gray-10; - height: 30px; - width: 30px; - } -} - -.colorpicker-content .color-bullet { - grid-area: color; - width: 20px; - height: 20px; - border: 1px solid $color-gray-30; - background-size: 8px; - overflow: hidden; -} - -.asset-section .asset-list-item .color-bullet { - border: 1px solid $color-gray-30; - height: 20px; - margin-right: $size-1; - width: 20px; -} -.tool-window-content .asset-list .asset-list-item { - &:hover { - .color-bullet { - border: 1px solid $color-primary; - } - } -} - -.color-cell.add-color:hover .color-bullet { - border-color: $color-gray-30; - - svg { - fill: $color-gray-30; - } -} - -.color-bullet { - display: flex; - flex-direction: row; - border-radius: 50%; - - & .color-bullet-wrapper { - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") - left center; - background-color: $color-white; - clip-path: circle(50%); - display: flex; - flex-direction: row; - height: 100%; - width: 100%; - } - - &.is-gradient { - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") - left center; - background-color: $color-white; - } - - & .color-bullet-wrapper > * { - width: 100%; - height: 100%; - } -} - -.color-data .color-bullet.multiple { - background: transparent; - - &::before { - content: "?"; - } -} - -.color-data .color-bullet { - border: 1px solid $color-gray-30; - border-radius: 50%; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - color: $color-gray-10; - flex-shrink: 0; - height: 20px; - margin: 5px 4px 0 0; - width: 20px; - - &.palette-th { - align-items: center; - border: 1px solid $color-gray-30; - display: flex; - justify-content: center; - - svg { - fill: $color-gray-30; - height: 16px; - width: 16px; - } - - &:hover { - border-color: $color-primary; - svg { - fill: $color-primary; - } - } - } -} - -.colorpicker-content .libraries .selected-colors .color-bullet { - grid-area: auto; - margin-bottom: 0.25rem; - cursor: pointer; - - &:hover { - border-color: $color-primary; - z-index: 10; - } - - &.button { - background: $color-white; - display: flex; - align-items: center; - justify-content: center; - } - - &.button svg { - width: 12px; - height: 12px; - fill: $color-gray-30; - } - - &.plus-button svg { - width: 8px; - height: 8px; - fill: $color-black; - } -} - -.color-bullet.is-not-library-color { - border-radius: $br3; - overflow: hidden; - - & .color-bullet-wrapper { - clip-path: none; - } -} diff --git a/frontend/resources/styles/main/partials/context-menu.scss b/frontend/resources/styles/main/partials/context-menu.scss deleted file mode 100644 index e7a302655f0b7a1fa53eb1a46da7f56f4222f4e0..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/context-menu.scss +++ /dev/null @@ -1,104 +0,0 @@ -// 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 - -.context-menu { - position: relative; - visibility: hidden; - opacity: 0; - z-index: 100; -} - -.context-menu.is-open { - position: relative; - display: block; - opacity: 1; - visibility: visible; -} - -.context-menu.fixed { - position: fixed; -} - -.context-menu-items { - background: $color-white; - border-radius: $br3; - box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); - left: -$size-4; - max-height: 30rem; - min-width: 7rem; - overflow: auto; - position: absolute; - top: $size-3; - - & .separator { - border-top: 1px solid $color-gray-10; - padding: 0px; - margin: 2px; - } - - &.min-width { - min-width: 13rem; - } -} - -.context-menu-action { - color: $color-black; - display: block; - font-size: $fs14; - font-weight: $fw400; - padding: $size-2 $size-4; - text-align: left; - white-space: nowrap; - - &:hover { - color: $color-black; - background-color: $color-primary-lighter; - text-decoration: none; - } - - &.submenu { - display: flex; - align-items: center; - justify-content: space-between; - - & span { - margin-left: 0.5rem; - } - - & svg { - height: 10px; - width: 10px; - } - } - - &.submenu-back { - color: $color-black; - display: flex; - font-weight: $fw700; - align-items: center; - - & svg { - height: 10px; - width: 10px; - transform: rotate(180deg); - margin-right: $size-2; - } - } -} - -.context-menu.is-selectable { - & .context-menu-action { - padding-left: 1.5rem; - } - - & .context-menu-item.is-selected .context-menu-action { - background-image: url(/images/icons/tick.svg); - background-repeat: no-repeat; - background-position: 5% 48%; - background-size: 10px; - font-weight: $fw700; - } -} diff --git a/frontend/resources/styles/main/partials/debug-icons-preview.scss b/frontend/resources/styles/main/partials/debug-icons-preview.scss deleted file mode 100644 index 76348589029dc8643bae92a2fb165eccddedfbde..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/debug-icons-preview.scss +++ /dev/null @@ -1,71 +0,0 @@ -.debug-preview { - display: flex; - flex-direction: column; - overflow: scroll; - height: 100%; - h1 { - color: white; - font-size: 24px; - display: block; - width: 100vw; - margin: 12px; - } -} - -.debug-icons-preview { - display: flex; - flex-wrap: wrap; - h2 { - color: white; - font-size: 16px; - display: block; - width: 100vw; - margin: 12px; - } - - .subtitle-old { - color: #ff3277; - } - - .icon-item, - .cursor-item, - .icon-item-old { - padding: 10px; - display: flex; - flex-direction: column; - width: 120px; - height: 120px; - margin: 10px; - align-items: center; - - svg { - width: 100%; - height: 100%; - min-width: 16px; - min-height: 16px; - fill: none; - color: transparent; - stroke: #91fadb; - } - - span { - color: white; - max-width: 100px; - overflow: hidden; - font-size: 12px; - margin-top: 4px; - word-break: break-word; - min-height: 40px; - } - } - - .cursor-item div, - .icon-item-old svg { - fill: #aab5ba; - stroke: none; - } - - .cursor-item { - height: auto; - } -} diff --git a/frontend/resources/styles/main/partials/dropdown.scss b/frontend/resources/styles/main/partials/dropdown.scss deleted file mode 100644 index 0abd62a4f07384fa0b3f5c1d53836dfaf3ffd588..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/dropdown.scss +++ /dev/null @@ -1,70 +0,0 @@ -.dropdown { - position: absolute; - max-height: 30rem; - background-color: $color-white; - border-radius: $br2; - box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); - z-index: 12; - - hr { - margin: 0 !important; - border-color: $color-gray-10; - } - - > li { - display: flex; - align-items: center; - color: $color-gray-60; - cursor: pointer; - font-size: $fs14; - height: 40px; - padding: 5px 16px; - - &.warning { - color: $color-danger; - } - - svg { - fill: $color-gray-20; - height: 12px; - width: 12px; - } - - &.title { - font-weight: $fw600; - cursor: default; - } - - &:hover { - background-color: $color-primary-lighter; - } - - &:focus { - border: 1px black solid; - } - } - - &.with-check { - > li { - padding: 5px 10px; - } - - > li:not(.selected) { - svg { - display: none; - } - } - - svg { - fill: $color-gray-50; - } - - .icon { - display: flex; - align-items: center; - width: 20px; - height: 20px; - margin-right: 7px; - } - } -} diff --git a/frontend/resources/styles/main/partials/editable-label.scss b/frontend/resources/styles/main/partials/editable-label.scss deleted file mode 100644 index db2d42a897afd8610850593170942ce6040a5e79..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/editable-label.scss +++ /dev/null @@ -1,30 +0,0 @@ -.editable-label { - display: flex; - - &.is-hidden { - display: none; - } -} - -.editable-label-input { - border: 0; - height: 30px; - padding: 5px; - margin: 0; - width: 100%; - background-color: $color-white; -} - -.editable-label-close { - background-color: $color-white; - cursor: pointer; - padding: 3px 5px; - - & svg { - fill: $color-gray-30; - height: 15px; - transform: rotate(45deg) translateY(7px); - width: 15px; - margin: 0; - } -} diff --git a/frontend/resources/styles/main/partials/exception-page.scss b/frontend/resources/styles/main/partials/exception-page.scss deleted file mode 100644 index 3ffd4ca1bd843e1bd28fbd13e47afc4d1d99f4e7..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/exception-page.scss +++ /dev/null @@ -1,83 +0,0 @@ -.exception-layout { - display: grid; - - grid-template-rows: 120px auto; - grid-template-columns: 1fr; -} - -.exception-header { - grid-column: 1 / span 1; - grid-row: 1 / span 1; - - display: flex; - align-items: center; - padding: 32px; - z-index: 1000; - - cursor: pointer; - - svg { - height: 55px; - width: 170px; - } -} - -.exception-content { - grid-column: 1 / span 1; - grid-row: 1 / span 2; - height: 100vh; - - display: flex; - justify-content: center; - align-items: center; - - .container { - max-width: 600px; - } - - .image { - align-items: center; - display: flex; - justify-content: center; - margin-bottom: 2rem; - - svg { - height: 220px; - width: 220px; - } - } - - .main-message { - color: $color-black; - font-size: $fs80; - line-height: $lh-188; // Original value was 150px; 150px/80px = 187.5 % => $lh-188 (rounded) - text-align: center; - } - - .desc-message { - color: $color-black; - font-size: $fs26; - font-weight: $fw300; - text-align: center; - } - - .sign-info { - margin-top: 20px; - color: $color-black; - font-size: $fs16; - font-weight: $fw200; - text-align: center; - - display: flex; - flex-direction: column; - align-items: center; - - b { - font-weight: $fw400; - } - - .btn-primary { - margin-top: 15px; - } - } -} diff --git a/frontend/resources/styles/main/partials/forms.scss b/frontend/resources/styles/main/partials/forms.scss deleted file mode 100644 index 4d42d4c31ce277a561e55f3827e482bc0bebccf4..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/forms.scss +++ /dev/null @@ -1,390 +0,0 @@ -// 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 - -input, -select, -textarea { - &.invalid { - border-color: $color-danger; - color: $color-danger; - } -} - -.form-container, -.generic-form { - display: flex; - justify-content: center; - flex-direction: column; - - .forms-container { - display: flex; - margin-top: 40px; - width: 536px; - justify-content: center; - } - - form { - display: flex; - flex-direction: column; - // flex-basis: 368px; - } - - .fields-row { - margin-bottom: 20px; - flex-direction: column; - - .options { - display: flex; - justify-content: flex-end; - font-size: $fs14; - margin-top: 13px; - } - } - - .field { - margin-bottom: 20px; - } - - h1 { - font-size: $fs36; - color: #2c233e; - margin-bottom: 20px; - } - - .subtitle { - font-size: $fs24; - color: #2c233e; - margin-bottom: 20px; - } - - .notification-icon { - justify-content: center; - display: flex; - margin-bottom: 3rem; - - svg { - fill: $color-gray-60; - height: 40%; - width: 40%; - } - } - - .notification-text { - font-size: $fs18; - color: $color-gray-60; - margin-bottom: 20px; - } - - .notification-text-email { - background: $color-gray-10; - border-radius: $br3; - color: $color-gray-60; - font-size: $fs18; - font-weight: $fw500; - margin: 1.5rem 0 2.5rem 0; - padding: 1rem; - text-align: center; - } - - h2 { - font-size: $fs24; - color: $color-gray-60; - // height: 40px; - display: flex; - align-items: center; - } - - a { - &:hover { - text-decoration: underline; - } - } - - p { - color: $color-gray-60; - } - - hr { - border-color: $color-gray-20; - } -} - -.custom-input { - display: flex; - flex-direction: column; - position: relative; - - input, - textarea { - background-color: $color-white; - border-radius: $br2; - border: 1px solid $color-gray-20; - color: $color-gray-60; - font-size: $fs14; - height: 40px; - margin: 0; - padding: 15px 15px 0 15px; - width: 100%; - } - - textarea { - height: auto; - font-size: $fs14; - font-family: "worksans", sans-serif; - padding-top: 20px; - } - - // Makes the background for autocomplete white - input:-webkit-autofill, - input:-webkit-autofill:hover, - input:-webkit-autofill:focus, - input:-webkit-autofill:active { - -webkit-box-shadow: 0 0 0 30px $color-white inset !important; - } - - label { - font-size: $fs12; - color: $color-gray-50; - position: absolute; - left: 15px; - top: 6px; - } - - &.invalid { - input { - border-color: $color-danger; - } - label { - color: $color-danger; - } - } - - &.valid { - input { - border-color: $color-success; - } - } - - &.focus { - input { - border-color: $color-gray-60; - } - } - - &.disabled { - input { - background-color: lighten($color-gray-10, 5%); - user-select: none; - } - } - - &.empty { - input { - padding-top: 0; - } - - label { - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - width: 1px; - } - } - - &.with-icon { - input { - padding-right: 50px; - } - } - - .help-icon { - position: absolute; - right: 15px; - top: 12px; - display: flex; - justify-content: center; - align-items: center; - svg { - fill: $color-gray-30; - width: 15px; - height: 15px; - } - } - - .hint { - color: $color-gray-40; - padding: 4px; - font-size: $fs12; - } - - .error { - color: $color-danger; - padding: 4px; - font-size: $fs12; - } -} - -.custom-multi-input { - border-radius: $br2; - border: 1px solid $color-gray-20; - max-height: 300px; - overflow-y: auto; - - &.invalid { - label { - color: unset; - } - } - - input { - border: 0px; - - &.no-padding { - padding-top: 0px; - } - } - - .selected-items { - padding-top: 25px; - padding-left: 15px; - display: flex; - flex-wrap: wrap; - } - - .selected-item { - width: 100%; - - &:not(:last-child) { - margin-right: 3px; - } - - .around { - border: 1px solid $color-gray-20; - padding-left: 5px; - border-radius: $br4; - &.invalid { - border: 1px solid $color-danger; - } - &.caution { - border: 1px solid $color-warning; - } - - .text { - display: inline-block; - max-width: 85%; - overflow: hidden; - text-overflow: ellipsis; - line-height: $lh-115; // Original value was 16px; 16px/14px = 114.285714286% => $lh-115 (rounded) - font-size: $fs14; - color: $color-black; - } - .icon { - cursor: pointer; - margin-left: 10px; - margin-right: 5px; - } - } - } -} - -.custom-select { - display: flex; - flex-direction: column; - position: relative; - justify-content: center; - - label { - font-size: $fs12; - color: $color-gray-30; - } - - select { - cursor: pointer; - font-size: $fs14; - border: 0px; - opacity: 0; - z-index: 10; - padding: 0px; - margin: 0px; - background-color: transparent; - position: absolute; - width: calc(100% - 1px); - height: 100%; - padding: 15px; - } - - .main-content { - flex-grow: 1; - display: flex; - flex-direction: column; - font-family: "worksans", sans-serif; - justify-content: center; - padding-top: 6px; - padding-bottom: 6px; - padding-left: 15px; - } - - .input-container { - display: flex; - flex-direction: row; - - background-color: $color-white; - border-radius: $br2; - border: 1px solid $color-gray-20; - height: 40px; - - &.focus { - border-color: $color-gray-60; - } - - &.invalid { - border-color: $color-danger; - label { - color: $color-danger; - } - } - - &.valid { - border-color: $color-success; - } - - &.focus { - border-color: $color-gray-60; - } - - &.disabled { - background-color: $color-gray-10; - user-select: none; - } - } - - .value { - color: $color-gray-60; - font-size: $fs14; - width: 100%; - border: 0px; - padding: 0px; - margin: 0px; - } - - .icon { - display: flex; - justify-content: center; - align-items: center; - padding-left: 10px; - padding-right: 10px; - pointer-events: none; - - svg { - fill: $color-gray-30; - transform: rotate(90deg); - width: 15px; - height: 15px; - } - } -} diff --git a/frontend/resources/styles/main/partials/loader.scss b/frontend/resources/styles/main/partials/loader.scss deleted file mode 100644 index f42675aaffa7d41d10ceabd928a271d765fbfcc8..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/loader.scss +++ /dev/null @@ -1,42 +0,0 @@ -// full width BG -.loader-content { - align-items: center; - background-color: rgba(255, 255, 255, 0.85); - display: flex; - height: 100vh; - justify-content: center; - left: 0; - position: fixed; - top: 0; - width: 100%; - z-index: 999; -} - -// full with loader CSS -svg#loader-icon { - height: 100px; - width: 100px; - animation: loaderColor 5s infinite ease; -} - -#loader-pen1 { - animation: pen1 2s infinite ease; -} - -#loader-pen2 { - animation: pen2 2s infinite ease; -} - -#loader-pen3 { - animation: pen3 2s infinite ease; -} - -// btn pencil loader -svg#loader-pencil { - fill: $color-primary-darker; - width: 60px; -} - -#loader-line { - animation: linePencil 0.8s infinite linear; -} diff --git a/frontend/resources/styles/main/partials/project-bar.scss b/frontend/resources/styles/main/partials/project-bar.scss deleted file mode 100644 index dc8e761302fcfeede6c31667c542555f4bbe30b6..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/project-bar.scss +++ /dev/null @@ -1,94 +0,0 @@ -// 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 - -.project-bar { - background-color: $color-gray-50; - border-right: 1px solid $color-gray-10; - bottom: 0; - height: 100%; - left: 50px; - position: fixed; - width: 200px; - z-index: 9; - - &.toggle { - left: -201px; - } - - .project-bar-inside { - align-items: center; - display: flex; - flex-direction: column; - overflow-y: auto; - padding-top: 60px; - - .project-name { - border-bottom: 1px solid $color-gray-10; - font-size: $fs14; - font-weight: $fw700; - padding: 0 $size-2; - width: 100%; - } - - .btn-primary, - .btn-warning, - .btn-danger { - font-size: $fs12; - margin-bottom: 0.5rem; - padding: 8px $size-2; - width: 90%; - } - } -} - -.tree-view { - width: 100%; - - li { - align-items: center; - cursor: pointer; - display: flex; - padding: $size-1 $size-2; - position: relative; - - svg { - fill: $color-gray-20; - height: 12px; - margin-right: $size-1; - width: 12px; - } - - span { - font-size: $fs12; - } - - &:hover, - &.current { - span { - color: $color-primary; - } - } - - .options { - align-items: center; - position: absolute; - display: flex; - right: 0; - top: 40%; - - svg { - fill: $color-gray-20; - height: 12px; - margin-right: $size-2; - width: 12px; - - &:hover { - fill: $color-gray-40; - } - } - } - } -} diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss deleted file mode 100644 index 241682f4cdbfaafe6d2f9852231c95baaa0f9f8b..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ /dev/null @@ -1,575 +0,0 @@ -// 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 - -.settings-bar { - background-color: $color-gray-50; - border-left: 1px solid $color-gray-60; - position: relative; - - &.settings-bar-left { - border-left: none; - border-right: 1px solid $color-gray-60; - - & .tab-container-tabs { - padding-left: 1.5rem; - } - } - - .settings-bar-inside { - display: grid; - grid-template-columns: 100%; - grid-template-rows: 100%; - height: calc(100% - 2px); - - .tool-window { - position: relative; - border-bottom: 1px solid $color-gray-60; - display: flex; - flex-direction: column; - flex: 1; - width: 100%; - height: 100%; - - .tool-window-bar { - align-items: center; - display: flex; - flex-shrink: 0; - padding: $size-2; - overflow: hidden; - margin: 0; - - svg { - fill: $color-gray-20; - height: 12px; - width: 12px; - } - - button, - div { - border: none; - background-color: transparent; - color: $color-gray-10; - font-size: $fs14; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - span.pages-title { - color: #e3e3e3; - font-size: 0.875rem; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - span.tool-badge { - border: 1px solid $color-primary; - border-radius: $br2; - font-size: $fs10; - color: $color-primary; - padding: 2px 4px; - margin-left: auto; - } - - span.tool-link, - span.shared-library { - margin-left: auto; - padding-left: 17px; - display: flex; - - svg { - fill: $color-gray-30; - height: 20px; - width: 20px; - } - } - - span.tool-link:hover svg { - fill: $color-primary; - } - - span.library-title { - color: $color-gray-10; - font-size: $fs14; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - &:first-letter { - text-transform: uppercase; - } - } - - .tool-window-bar-icon { - height: 21px; - display: flex; - align-items: center; - justify-content: center; - - svg { - width: 15px; - height: 15px; - } - } - - &.big { - height: 3rem; - padding-bottom: 1rem; - } - - .tool-window-bar-title { - font-size: $fs14; - margin-left: 0.5rem; - } - - .tool-window-icon { - margin-right: $size-2; - display: none; - } - - .tool-window-close { - cursor: pointer; - margin-left: auto; - transform: rotate(45deg); - - &:hover { - svg { - fill: $color-danger; - } - } - } - - & .view-only-mode { - color: $color-primary; - border: 1px solid $color-primary; - border-radius: $br3; - font-size: $fs10; - text-transform: uppercase; - display: flex; - align-items: center; - justify-content: center; - margin-left: auto; - padding: 0.25rem; - } - } - } - } - - .empty { - color: $color-gray-20; - font-size: $fs12; - line-height: $lh-150; - text-align: center; - padding: 0 15px; - display: flex; - flex-direction: column; - gap: 20px; - margin-top: 12px; - - .tool-window-bar-icon { - height: 32px; - display: flex; - align-items: center; - justify-content: center; - margin-top: 16px; - } - - svg { - width: 32px; - height: 32px; - fill: $color-gray-30; - } - - .btn-primary { - margin-top: 10px; - background-color: $color-gray-60; - color: $color-gray-10; - &:hover { - background-color: $color-primary; - color: $color-black; - } - } - } - - & > .resize-area { - position: absolute; - width: 8px; - height: 100%; - z-index: 10; - cursor: ew-resize; - } - - &.settings-bar-left > .resize-area { - right: -8px; - } - - &.settings-bar-right > .resize-area { - left: -4px; - } -} - -.tool-window-content { - display: flex; - flex-direction: column; - height: 100%; - width: 100%; - overflow-y: auto; - overflow-x: hidden; - &.inspect { - .tab-container-tabs { - padding-bottom: 0.5rem; - background-color: $color-gray-50; - border-bottom: 1px solid $color-gray-60; - height: 3rem; - } - - .tab-container-tab-title { - border-radius: $br4; - - &.current { - background-color: $color-primary; - color: black; - } - } - } -} - -.element-list { - margin: 0; - width: 100%; - - ul { - border-left: 9px solid $color-gray-50; - margin: 0 0 0 0.4rem; - - li { - border-left: 1px solid $color-gray-40; - } - } - - li { - cursor: pointer; - display: flex; - flex-direction: column; - width: 100%; - padding-top: 1px; - padding-bottom: 1px; - - &.open { - ul { - li { - .element-list-body { - border-style: dashed; - } - } - } - } - } -} - -.element-list.pages-list { - max-height: 10rem; - - .context-menu { - position: fixed; - } - - .context-menu-items { - border: none; - margin: none; - } - - .context-menu-action { - width: 100%; - } -} - -button.collapse-sidebar { - background: none; - border: none; - cursor: pointer; - height: 2.5rem; - padding-top: 0.75rem; - position: absolute; - width: 1rem; - - & svg { - width: 12px; - height: 12px; - fill: $color-gray-20; - transform: rotate(180deg); - } - - &.collapsed { - background: $color-gray-60; - left: 48px; - top: 48px; - width: 28px; - height: 48px; - padding: 0; - border-radius: 0 $br4 $br4 0; - border-left: 1px solid $color-gray-50; - - & svg { - transform: rotate(0deg); - } - } -} - -.layers-tab { - display: grid; - grid-template-rows: auto 1fr; - grid-template-columns: 100%; - height: 100%; - overflow: hidden; - position: relative; - .resize-area-horiz { - position: absolute; - top: var(--height, 200px); - left: 0; - height: 8px; - width: 100%; - z-index: 10; - cursor: ns-resize; - } -} - -.shortcuts, -.debug-panel { - .shortcuts-header, - .debug-panel-header { - display: flex; - height: 40px; - background-color: $color-gray-60; - - .shortcuts-title, - .debug-panel-title { - color: $color-white; - font-size: $fs12; - display: flex; - justify-content: center; - align-items: center; - flex-grow: 1; - svg { - height: 18px; - width: 18px; - transform: rotate(45deg); - fill: $color-gray-20; - } - } - - .shortcuts-close-button, - .debug-panel-close-button { - display: flex; - justify-content: center; - background-color: transparent; - border: none; - cursor: pointer; - padding: 2px 0 2px 15px; - position: absolute; - top: 8px; - svg { - height: 18px; - width: 18px; - transform: rotate(45deg); - fill: $color-gray-20; - } - } - } - - .search-field { - height: 60px; - display: flex; - justify-content: center; - align-items: center; - padding: 12px 10px; - - .search-box { - display: flex; - justify-content: space-between; - align-items: center; - border: 1px solid $color-gray-30; - border-radius: $br2; - width: 100%; - &:focus-within { - border: 1px solid $color-primary; - } - .input-text { - margin: 0; - background: $color-gray-50; - width: 100%; - color: $color-white; - &:focus { - border-bottom: none; - } - } - .icon-wrapper { - display: flex; - justify-content: center; - align-items: center; - border: none; - background-color: transparent; - padding: 0; - .icon { - display: flex; - justify-content: center; - align-items: center; - &.close { - transform: rotate(45deg); - } - } - } - svg { - width: 16px; - height: 16px; - margin: 0 7px; - cursor: pointer; - fill: $color-gray-20; - } - } - } - - .shortcut-list { - border-top: 1px solid $color-gray-60; - padding: 10px; - overflow-y: auto; - height: 90%; - margin-bottom: 15px; - .section-title { - background-color: $color-gray-60; - padding: 4px 0; - } - .section-title, - .subsection-title { - display: flex; - cursor: pointer; - margin-top: 4px; - font-size: $fs12; - - .section-name { - color: $color-white; - } - .collapesed-shortcuts { - padding: 0 10px; - svg { - height: 8px; - width: 8px; - fill: $color-gray-20; - } - &.open { - svg { - transform: rotate(90deg); - } - } - } - .shortcut-count { - padding-left: 5px; - color: $color-white; - } - } - .subsection-title { - padding: 4px 0px; - .subsection-name { - color: $color-white; - } - } - - .section-title, - .subsection-title { - &:hover { - background-color: $color-primary; - .subsection-name, - .section-name { - color: $color-gray-60; - } - svg { - fill: $color-gray-60; - } - } - } - - .shortcut-name { - border: 1px solid $color-gray-60; - border-radius: $br4; - padding: 7px; - display: flex; - justify-content: space-between; - margin-top: 4px; - color: $color-white; - font-size: $fs12; - .command-name { - display: flex; - align-items: center; - } - .keys { - flex-grow: 1; - display: flex; - align-items: center; - justify-content: flex-end; - } - .char-box { - min-width: 15px; - background-color: $color-white; - color: $color-black; - border-radius: $br3; - padding: 2px 5px; - font-size: $fs11; - font-weight: $fw600; - margin: 0 2px; - text-transform: capitalize; - display: inline-block; - text-align: center; - } - .space { - margin: 0 3px; - } - } - } - .not-found { - background-color: $color-gray-60; - padding: 4px 0; - color: $color-white; - display: flex; - justify-content: center; - margin-top: 4px; - font-size: $fs12; - } -} - -.debug-panel { - .debug-panel-inner { - padding: 8px; - } - .debug-option { - display: flex; - gap: 8px; - margin: 4px 0; - cursor: pointer; - - label { - font-size: 80%; - cursor: pointer; - } - - svg { - width: 15px; - height: 15px; - background: white; - } - - &:hover { - svg { - stroke: $color-primary; - } - label { - color: $color-primary; - } - } - } -} diff --git a/frontend/resources/styles/main/partials/signup-questions.scss b/frontend/resources/styles/main/partials/signup-questions.scss deleted file mode 100644 index 373cae5314a8810b3aa6b610237cc3da76dcd64f..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/signup-questions.scss +++ /dev/null @@ -1,190 +0,0 @@ -// 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 - -.signup-questions { - background-color: $color-white; - color: $color-gray-60; - max-width: 646px; - overflow-y: auto; - padding: 1.5rem 1rem; - position: relative; - width: 100%; - form { - display: flex; - flex-direction: column; - height: 100%; - } - - h1, - h3 { - font-family: "worksans", sans-serif; - font-weight: $fw500; - } - - h1 { - font-size: $fs36; - padding-top: 2.5rem; - } - - h3 { - font-size: $fs23; - } - - .step-header { - height: 2.5rem; - width: 100%; - } - .custom-select { - margin-bottom: 10px; - } - - .step-number { - background-color: $color-gray-10; - border: none; - border-radius: 1rem; // Need to be investigated, before we can use variable - color: $color-gray-40; - float: right; - font-family: "worksans", sans-serif; - font-size: $fs12; - height: 1.5rem; - line-height: $lh-200; // Original value was 1.5rem = 24px; 24px/12px = 200% => lh-200 - text-align: center; - width: 2.5rem; - } - - .header-image { - width: 240px; - } - - .intro { - font-size: $fs16; - padding: 0.5rem 0 1rem 0; - color: $color-gray-40; - } - .section { - display: block; - font-weight: $fw500; - font-size: $fs18; - margin: 0 0 0.3em 0; - padding: 0.8rem 0 0 0; - font-family: "worksans", sans-serif !important; - } - - .other { - .custom-input { - margin: 0.75rem 0 2rem 0; - } - } - .buttons { - flex-grow: 1; - display: grid; - grid-template-columns: 50% 50%; - grid-template-areas: "previous next"; - .step-prev { - display: flex; - align-items: flex-end; - justify-content: flex-start; - grid-area: previous; - button { - background-color: transparent; - border: none; - cursor: pointer; - height: 40px; - font-size: $fs15; - } - } - - .step-next { - display: flex; - align-items: flex-end; - justify-content: flex-end; - grid-area: next; - input { - font-size: $fs15; - color: $color-black; - background-color: $color-primary; - width: 11rem; - margin-left: auto; - margin: 0; - } - } - } - - .custom-radio { - display: flex; - justify-content: space-between; - flex-wrap: wrap; - - .input-radio { - margin: 0; - max-width: 12rem; - width: 100%; - - &.with-image { - display: block; - padding: 0; - } - - label { - font-family: "worksans", sans-serif !important; - color: $color-gray-60; - font-size: $fs15; - padding-left: 0; - position: relative; - text-align: center; - height: 4rem; - margin: 0; - - &.with-image { - min-height: 120px; - display: flex; - padding-top: 4rem; - justify-content: center; - background-size: 50px; - background-repeat: no-repeat; - background-position: center 0.75rem; - } - } - - input[type="radio"] { - /*We need it to be accesible so we can't use display none*/ - display: inline; - opacity: 0; - } - - input[type="radio"] + label:before { - background-color: $color-white; - border: 1px solid $color-gray-10; - } - - input[type="radio"] + label.with-image:before { - background-color: transparent; - border-radius: 4px; - min-width: 100%; - min-height: 100%; - position: absolute; - top: 0; - left: 0; - margin: 0; - } - - input[type="radio"]:focus + label:before { - border: 1px solid $color-gray-60; - } - - input[type="radio"]:checked + label:before { - box-shadow: inset 0 0 0 4px $color-white; - background-color: $color-primary; - border: 1px solid $color-gray-30; - } - - input[type="radio"]:checked + label.with-image:before { - border: 1px solid $color-primary; - background-color: transparent; - } - } - } -} diff --git a/frontend/resources/styles/main/partials/tab-container.scss b/frontend/resources/styles/main/partials/tab-container.scss deleted file mode 100644 index 33c759e0a353da230870149492a2bfd19320d56e..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/tab-container.scss +++ /dev/null @@ -1,45 +0,0 @@ -.tab-container { - display: grid; - grid-template-rows: auto 1fr; - grid-template-columns: 100%; - height: 100%; -} - -.tab-container-tabs { - background: $color-gray-60; - cursor: pointer; - display: flex; - flex-direction: row; - font-size: $fs12; - height: 2.5rem; - padding: 0 0.25rem; -} - -.tab-container-tab-title { - align-items: center; - background: $color-gray-60; - border-radius: $br2 $br2 0 0; - color: $color-white; - display: flex; - justify-content: center; - margin: 0.5rem 0.25rem 0 0.25rem; - width: 100%; - - &.current { - background: $color-gray-50; - } -} - -.tab-container-content { - overflow-y: auto; - overflow-x: hidden; -} - -.inspect .tab-container-content { - overflow: hidden; -} - -.tab-element, -.tab-element-content { - height: 100%; -} diff --git a/frontend/resources/styles/main/partials/texts.scss b/frontend/resources/styles/main/partials/texts.scss index 0bece2924d7b9e2c728e4c64a73c9288e7b6c8a1..aab38a4966ece89ad097dc8792973212e69b1297 100644 --- a/frontend/resources/styles/main/partials/texts.scss +++ b/frontend/resources/styles/main/partials/texts.scss @@ -1,6 +1,6 @@ .text-editor, .rich-text { - color: $color-black; + color: var(--app-black); height: 100%; font-family: sourcesanspro; @@ -21,6 +21,10 @@ flex-direction: column; } + .public-DraftStyleDefault-block { + white-space: pre; + } + &.align-top { .DraftEditor-root { justify-content: flex-start; diff --git a/frontend/resources/styles/main/partials/user-settings.scss b/frontend/resources/styles/main/partials/user-settings.scss deleted file mode 100644 index 8fd19383fcd23a60ed08a56da2dd796023588975..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/user-settings.scss +++ /dev/null @@ -1,188 +0,0 @@ -.settings-content { - header { - display: flex; - flex-direction: column; - height: 160px; - background-color: $color-white; - - .secondary-menu { - display: flex; - justify-content: space-between; - height: 40px; - font-size: $fs14; - color: $color-gray-60; - - .icon { - display: flex; - align-items: center; - } - - .left { - margin-left: 30px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - - .label { - margin-left: 15px; - } - - svg { - fill: $color-gray-60; - width: 14px; - height: 14px; - transform: rotate(180deg); - } - } - .right { - align-items: center; - cursor: pointer; - display: flex; - justify-content: center; - margin-right: 30px; - - .label { - color: $color-primary-dark; - margin-right: 15px; - } - - svg { - fill: $color-primary-dark; - width: 14px; - height: 14px; - } - - &:hover { - .label { - color: $color-danger; - } - svg { - fill: $color-danger; - } - } - } - } - - h1 { - align-items: top; - color: $color-gray-60; - display: flex; - flex-grow: 1; - font-size: $fs24; - font-weight: $fw400; - justify-content: center; - } - - nav { - display: flex; - justify-content: center; - height: 40px; - - .nav-item { - align-items: center; - color: $color-gray-40; - display: flex; - flex-basis: 140px; - justify-content: center; - - &.current { - border-bottom: 3px solid $color-primary; - } - } - } - } - - .settings-profile { - .forms-container { - margin-top: 80px; - } - } - - .avatar-form { - flex-basis: 168px; - height: 100vh; - display: flex; - position: relative; - - .image-change-field { - position: relative; - width: 120px; - height: 120px; - - .update-overlay { - opacity: 0; - cursor: pointer; - position: absolute; - width: 121px; - height: 121px; - border-radius: 50%; - font-size: $fs24; - color: $color-white; - line-height: $lh-500; // Original value was 120px; 120px/24px =500% => $lh-500 - text-align: center; - background: $color-primary-dark; - z-index: 14; - } - - input[type="file"] { - width: 120px; - height: 120px; - position: absolute; - opacity: 0; - cursor: pointer; - top: 0; - z-index: 15; - } - - &:hover { - img { - display: none; - } - .update-overlay { - opacity: 1; - } - } - } - } - - .profile-form { - flex-grow: 1; - flex-basis: 390px; - display: flex; - - flex-direction: column; - - .change-email { - display: flex; - flex-direction: row; - font-size: $fs14; - color: $color-primary-dark; - justify-content: flex-end; - margin-bottom: 20px; - } - } - - .avatar-form { - img { - border-radius: 50%; - flex-shrink: 0; - height: 120px; - margin-right: $size-4; - width: 120px; - } - } - - .options-form, - .password-form { - display: flex; - flex-direction: column; - flex-basis: 368px; - - h2 { - font-size: $fs14; - font-weight: $fw400; - margin-bottom: $size-4; - } - } -} diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss deleted file mode 100644 index 5288b7475f0ce0052a0f6bf1c59f15c22e91766f..0000000000000000000000000000000000000000 --- a/frontend/resources/styles/main/partials/workspace.scss +++ /dev/null @@ -1,455 +0,0 @@ -// 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 - -$width-left-toolbar: 48px; - -$width-settings-bar: 256px; -$width-settings-bar-min: 255px; -$width-settings-bar-max: 500px; - -$height-palette: 79px; -$height-palette-min: 54px; -$height-palette-max: 80px; - -#workspace { - width: 100vw; - height: 100%; - user-select: none; - background-color: $color-canvas; - display: grid; - grid-template-areas: - "header header header header" - "toolbar left-sidebar viewport right-sidebar" - "toolbar left-sidebar color-palette right-sidebar"; - - grid-template-rows: auto 1fr auto; - grid-template-columns: auto auto 1fr auto; - - .workspace-header { - grid-area: header; - height: 48px; - } - - .left-toolbar { - grid-area: toolbar; - width: $width-left-toolbar; - overflow-y: auto; - overflow-x: hidden; - } - - .settings-bar.settings-bar-left { - min-width: $width-settings-bar; - max-width: 500px; - width: var(--width, $width-settings-bar); - grid-area: left-sidebar; - } - - .settings-bar.settings-bar-right { - height: 100%; - width: var(--width, $width-settings-bar); - grid-area: right-sidebar; - - &.not-expand { - max-width: $width-settings-bar; - } - } - - .workspace-content { - grid-area: viewport; - } - - .color-palette { - grid-area: color-palette; - max-height: $height-palette-max; - height: var(--height, $height-palette); - } -} - -.workspace-context-menu { - background-color: $color-white; - border-radius: $br3; - box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.25); - left: 740px; - position: absolute; - top: 40px; - width: 240px; - z-index: 12; - - li { - align-items: center; - font-size: $fs14; - padding: $size-1 $size-4; - cursor: pointer; - - display: flex; - justify-content: space-between; - - &.separator { - border-top: 1px solid $color-gray-10; - padding: 0px; - margin: 2px; - } - - span { - color: $color-gray-60; - } - - span.shortcut { - color: $color-gray-20; - font-size: $fs12; - } - - &:hover { - background-color: $color-primary-lighter; - } - - .submenu-icon { - position: absolute; - right: 1rem; - - svg { - width: 10px; - height: 10px; - } - } - } - - .icon-menu-item { - display: flex; - justify-content: flex-start; - - &:hover { - background-color: $color-primary-lighter; - } - - span.title { - margin-left: 5px; - } - - .selected-icon { - svg { - width: 10px; - height: 10px; - } - } - - .shape-icon { - margin-left: 3px; - svg { - width: 13px; - height: 13px; - } - } - - .icon-wrapper { - display: grid; - grid-template-columns: 1fr 1fr; - margin: 0; - } - } -} - -// .workspace-loader { -// display: flex; -// justify-content: center; -// align-items: center; - -// svg { -// fill: $color-gray-50; -// } -// } - -.workspace-content { - background-color: $color-canvas; - display: flex; - padding: 0; - margin: 0; - grid-area: viewport; - &.scrolling { - cursor: grab; - } - - &.no-tool-bar-right { - width: calc(100% - #{$width-left-toolbar} - #{$width-settings-bar}); - right: 0; - - .coordinates { - right: 10px; - } - } - - &.no-tool-bar-left { - width: calc(100% - #{$width-left-toolbar} - #{$width-settings-bar}); - - &.no-tool-bar-right { - width: 100%; - } - } - - .coordinates { - background-color: $color-dark-bg; - border-radius: $br3; - bottom: 0px; - padding-left: 5px; - position: fixed; - right: calc(#{$width-settings-bar} + 14px); - text-align: center; - width: 125px; - white-space: nowrap; - padding-bottom: 2px; - transition: bottom 0.5s; - z-index: 2; - - &.color-palette-open { - bottom: 5rem; - } - - span { - color: $color-white; - font-size: $fs12; - padding-right: 5px; - } - } - - .cursor-tooltip { - background-color: $color-dark-bg; - border-radius: $br3; - color: $color-white; - font-size: $fs12; - padding: 3px 8px; - transition: none; - text-align: center; - } - - .workspace-viewport { - overflow: hidden; - transition: none; - display: grid; - grid-template-rows: 20px 1fr; - grid-template-columns: 20px 1fr; - flex: 1; - } - - .viewport { - cursor: none; - grid-column: 1 / span 2; - grid-row: 1 / span 2; - overflow: hidden; - position: relative; - - .viewport-overlays { - cursor: initial; - height: 100%; - overflow: hidden; - pointer-events: none; - position: absolute; - width: 100%; - z-index: 10; - - .pixel-overlay { - height: 100%; - left: 0; - pointer-events: initial; - position: absolute; - top: 0; - width: 100%; - z-index: 1; - } - } - - .render-shapes { - height: 100%; - position: absolute; - width: 100%; - } - - .frame-thumbnail-wrapper { - .fills, - .frame-clip-def { - opacity: 0; - } - } - - .viewport-controls { - position: absolute; - width: 100%; - height: 100%; - } - } - - .page-canvas, - .page-layout { - overflow: visible; - } - - /* Rules */ - - .empty-rule-square { - grid-column: 1 / span 1; - grid-row: 1 / span 1; - } - - .horizontal-rule { - transition: none; - pointer-events: none; - grid-column: 2 / span 1; - grid-row: 1 / span 1; - z-index: 13; - - rect { - fill: $color-canvas; - } - path { - stroke: $color-gray-20; - } - } - - .vertical-rule { - transition: none; - pointer-events: none; - grid-column: 1 / span 1; - grid-row: 2 / span 1; - z-index: 13; - - rect { - fill: $color-canvas; - } - path { - stroke: $color-gray-20; - } - } -} - -.workspace-frame-label { - font-size: $fs12; -} - -.multiuser-cursor { - z-index: 10000; - pointer-events: none; -} - -.profile-name { - width: fit-content; - font-family: worksans; - padding: 2px 12px; - border-radius: $br4; - display: flex; - align-items: center; - height: 20px; - font-size: $fs12; - line-height: $lh-150; -} - -.viewport-actions { - align-items: center; - display: flex; - flex-direction: row; - justify-content: center; - margin-left: auto; - margin-top: 2rem; - position: absolute; - width: 100%; - z-index: 12; - pointer-events: none; - - .path-actions, - .viewport-actions-container { - pointer-events: initial; - display: flex; - flex-direction: row; - background: white; - border-radius: $br3; - padding: 0.5rem; - border: 1px solid $color-gray-20; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); - } - - .viewport-actions-container { - padding-left: 1rem; - gap: 12px; - color: var(--color-gray-60); - align-items: center; - font-size: 12px; - - .btn-primary, - .btn-secondary { - height: 24px; - } - - .viewport-actions-title { - margin-right: 2rem; - } - - .grid-edit-board-name { - font-weight: 600; - } - } - - .viewport-actions-group { - display: flex; - flex-direction: row; - border-right: 1px solid $color-gray-20; - } - - .viewport-actions-entry { - width: 28px; - height: 28px; - margin: 0 0.25rem; - cursor: pointer; - display: flex; - justify-content: center; - align-items: center; - border-radius: $br3; - - svg { - pointer-events: none; - width: 20px; - height: 20px; - } - - &:hover svg { - fill: $color-primary; - } - - &.is-disabled { - cursor: initial; - svg { - fill: $color-gray-20; - } - } - - &.is-toggled { - background: $color-black; - - svg { - fill: $color-primary; - } - } - } - - .viewport-actions-entry-wide { - width: 27px; - height: 20px; - - svg { - width: 27px; - height: 20px; - } - } - - .path-actions > :first-child .viewport-actions-entry { - margin-left: 0; - } - - .path-actions > :last-child { - border: none; - } - - .path-actions > :last-child .viewport-actions-entry { - margin-right: 0; - } -} diff --git a/frontend/resources/templates/challenge.mustache b/frontend/resources/templates/challenge.mustache new file mode 100644 index 0000000000000000000000000000000000000000..16bba9b6afc96225b5d8e39442c41d96afada8c5 --- /dev/null +++ b/frontend/resources/templates/challenge.mustache @@ -0,0 +1,18 @@ + + + + + Penpot - Challenge + + + + + + + diff --git a/frontend/resources/templates/index.mustache b/frontend/resources/templates/index.mustache index 77475d7e30ef202499da01bc430bab1e94c2b866..7846bb111b34d4645c992f01b1f5d96eccdfbda1 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -1,5 +1,5 @@ - + @@ -23,29 +23,31 @@ {{/isDebug}} + {{# manifest}} + + + + {{/manifest}} + + - - {{# manifest}} - - - - {{/manifest}} - + - {{>../public/images/sprites/symbol/icons.svg}} - {{>../public/images/sprites/symbol/cursors.svg}} + {{> ../public/images/sprites/symbol/icons.svg }} + {{> ../public/images/sprites/symbol/cursors.svg }} + {{> ../public/images/sprites/assets.svg }}
+ {{# manifest}} - - + + {{/manifest}} diff --git a/frontend/resources/templates/preview-body.mustache b/frontend/resources/templates/preview-body.mustache index fc26837165daf1465ca7f21d4b0d2cf7e4f7fbcf..4ade10b0805496cac8ce51782c51f36f39559630 100644 --- a/frontend/resources/templates/preview-body.mustache +++ b/frontend/resources/templates/preview-body.mustache @@ -1,2 +1,3 @@ -{{>../public/images/sprites/symbol/icons.svg}} +{{> ../public/images/sprites/symbol/icons.svg }} +{{> ../public/images/sprites/assets.svg }} diff --git a/frontend/resources/templates/preview-head.mustache b/frontend/resources/templates/preview-head.mustache new file mode 100644 index 0000000000000000000000000000000000000000..5ac5451b37d6b2be596bc2a7ddb70cd89228b634 --- /dev/null +++ b/frontend/resources/templates/preview-head.mustache @@ -0,0 +1,15 @@ + + + + + diff --git a/frontend/resources/templates/render.mustache b/frontend/resources/templates/render.mustache index 5221030ae1c104d90c13e2e2dae2bcd8d83a4139..cbaad751478afc4ab42f315eb90be5a7ecd7fbef 100644 --- a/frontend/resources/templates/render.mustache +++ b/frontend/resources/templates/render.mustache @@ -7,7 +7,6 @@ diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js new file mode 100644 index 0000000000000000000000000000000000000000..2867d75cf73bf22fb31484b795ef56a0a456941c --- /dev/null +++ b/frontend/scripts/_helpers.js @@ -0,0 +1,553 @@ +import proc from "node:child_process"; +import fs from "node:fs/promises"; +import ph from "node:path"; +import os from "node:os"; +import url from "node:url"; + +import * as marked from "marked"; +import SVGSpriter from "svg-sprite"; +import Watcher from "watcher"; +import gettext from "gettext-parser"; +import l from "lodash"; +import log from "fancy-log"; +import mustache from "mustache"; +import pLimit from "p-limit"; +import ppt from "pretty-time"; +import wpool from "workerpool"; + +function getCoreCount() { + return os.cpus().length; +} + +export const dirname = url.fileURLToPath(new URL(".", import.meta.url)); + +export function startWorker() { + return wpool.pool(dirname + "/_worker.js", { + maxWorkers: getCoreCount(), + }); +} + +export const isDebug = process.env.NODE_ENV !== "production"; + +async function findFiles(basePath, predicate, options = {}) { + predicate = + predicate ?? + function () { + return true; + }; + + let files = await fs.readdir(basePath, { + recursive: options.recursive ?? false, + }); + files = files.map((path) => ph.join(basePath, path)); + + return files; +} + +function syncDirs(originPath, destPath) { + const command = `rsync -ar --delete ${originPath} ${destPath}`; + + return new Promise((resolve, reject) => { + proc.exec(command, (cause, stdout) => { + if (cause) { + reject(cause); + } else { + resolve(); + } + }); + }); +} + +export function isSassFile(path) { + return path.endsWith(".scss"); +} + +export function isSvgFile(path) { + return path.endsWith(".svg"); +} + +export function isJsFile(path) { + return path.endsWith(".js"); +} + +export async function compileSass(worker, path, options) { + path = ph.resolve(path); + + log.info("compile:", path); + return worker.exec("compileSass", [path, options]); +} + +export async function compileSassDebug(worker) { + const result = await compileSass(worker, "resources/styles/debug.scss", {}); + return `${result.css}\n`; +} + +export async function compileSassStorybook(worker) { + const limitFn = pLimit(4); + const sourceDir = ph.join("src", "app", "main", "ui", "ds"); + + const dsFiles = (await fs.readdir(sourceDir, { recursive: true })) + .filter(isSassFile) + .map((filename) => ph.join(sourceDir, filename)); + const procs = [compileSass(worker, "resources/styles/main-default.scss", {})]; + + for (let path of dsFiles) { + const proc = limitFn(() => compileSass(worker, path, { modules: true })); + procs.push(proc); + } + + const result = await Promise.all(procs); + return result.reduce( + (acc, item) => { + acc.index[item.outputPath] = item.css; + acc.items.push(item.outputPath); + return acc; + }, + { index: {}, items: [] }, + ); +} + +export async function compileSassAll(worker) { + const limitFn = pLimit(4); + const sourceDir = "src"; + + const isDesignSystemFile = (path) => { + return path.startsWith("app/main/ui/ds/"); + }; + + const isOldComponentSystemFile = (path) => { + return path.startsWith("app/main/ui/components/"); + }; + + let files = (await fs.readdir(sourceDir, { recursive: true })).filter( + isSassFile, + ); + + const appFiles = files + .filter((path) => !isDesignSystemFile(path)) + .filter((path) => !isOldComponentSystemFile(path)) + .map((path) => ph.join(sourceDir, path)); + + const dsFiles = files + .filter(isDesignSystemFile) + .map((path) => ph.join(sourceDir, path)); + + const oldComponentsFiles = files + .filter(isOldComponentSystemFile) + .map((path) => ph.join(sourceDir, path)); + + const procs = [compileSass(worker, "resources/styles/main-default.scss", {})]; + + for (let path of [...oldComponentsFiles, ...dsFiles, ...appFiles]) { + const proc = limitFn(() => compileSass(worker, path, { modules: true })); + procs.push(proc); + } + + const result = await Promise.all(procs); + + return result.reduce( + (acc, item) => { + acc.index[item.outputPath] = item.css; + acc.items.push(item.outputPath); + return acc; + }, + { index: {}, items: [] }, + ); +} + +export function concatSass(data) { + const output = []; + + for (let path of data.items) { + output.push(data.index[path]); + } + + return output.join("\n"); +} + +export async function watch(baseDir, predicate, callback) { + predicate = predicate ?? (() => true); + + const watcher = new Watcher(baseDir, { + persistent: true, + recursive: true, + }); + + watcher.on("change", (path) => { + if (predicate(path)) { + callback(path); + } + }); +} + +async function readShadowManifest() { + try { + const manifestPath = "resources/public/js/manifest.json"; + let content = await fs.readFile(manifestPath, { encoding: "utf8" }); + content = JSON.parse(content); + + const index = { + config: "js/config.js?ts=" + Date.now(), + polyfills: "js/polyfills.js?ts=" + Date.now(), + }; + + for (let item of content) { + index[item.name] = "js/" + item["output-name"]; + } + + return index; + } catch (cause) { + return { + config: "js/config.js", + polyfills: "js/polyfills.js", + main: "js/main.js", + shared: "js/shared.js", + worker: "js/worker.js", + rasterizer: "js/rasterizer.js", + }; + } +} + +async function renderTemplate(path, context = {}, partials = {}) { + const content = await fs.readFile(path, { encoding: "utf-8" }); + + const ts = Math.floor(new Date()); + + context = Object.assign({}, context, { + ts: ts, + isDebug, + }); + + return mustache.render(content, context, partials); +} + +const renderer = { + link(href, title, text) { + return `${text}`; + }, +}; + +marked.use({ renderer }); + +async function readTranslations() { + const langs = [ + "ar", + "ca", + "de", + "el", + "en", + "eu", + "it", + "es", + "fa", + "fr", + "he", + "nb_NO", + "pl", + "pt_BR", + "ro", + "id", + "ru", + "tr", + "zh_CN", + "zh_Hant", + "hr", + "gl", + "pt_PT", + "cs", + "fo", + "ko", + "lv", + "nl", + // this happens when file does not matches correct + // iso code for the language. + ["ja_jp", "jpn_JP"], + ["uk", "ukr_UA"], + "ha", + ]; + const result = {}; + + for (let lang of langs) { + let filename = `${lang}.po`; + if (l.isArray(lang)) { + filename = `${lang[1]}.po`; + lang = lang[0]; + } + + const content = await fs.readFile(`./translations/${filename}`, { + encoding: "utf-8", + }); + + lang = lang.toLowerCase(); + + const data = gettext.po.parse(content, "utf-8"); + const trdata = data.translations[""]; + + for (let key of Object.keys(trdata)) { + if (key === "") continue; + const comments = trdata[key].comments || {}; + + if (l.isNil(result[key])) { + result[key] = {}; + } + + const isMarkdown = l.includes(comments.flag, "markdown"); + + const msgs = trdata[key].msgstr; + if (msgs.length === 1) { + let message = msgs[0]; + if (isMarkdown) { + message = marked.parseInline(message); + } + + result[key][lang] = message; + } else { + result[key][lang] = msgs.map((item) => { + if (isMarkdown) { + return marked.parseInline(item); + } else { + return item; + } + }); + } + } + } + + return result; +} + +function filterTranslations(translations, langs = [], keyFilter) { + const filteredEntries = Object.entries(translations) + .filter(([translationKey, _]) => keyFilter(translationKey)) + .map(([translationKey, value]) => { + const langEntries = Object.entries(value).filter(([lang, _]) => + langs.includes(lang), + ); + return [translationKey, Object.fromEntries(langEntries)]; + }); + + return Object.fromEntries(filteredEntries); +} + +async function generateSvgSprite(files, prefix) { + const spriter = new SVGSpriter({ + mode: { + symbol: { inline: true }, + }, + }); + + for (let path of files) { + const name = `${prefix}${ph.basename(path)}`; + const content = await fs.readFile(path, { encoding: "utf-8" }); + spriter.add(name, name, content); + } + + const { result } = await spriter.compileAsync(); + const resource = result.symbol.sprite; + return resource.contents; +} + +async function generateSvgSprites() { + await fs.mkdir("resources/public/images/sprites/symbol/", { + recursive: true, + }); + + const icons = await findFiles("resources/images/icons/", isSvgFile); + const iconsSprite = await generateSvgSprite(icons, "icon-"); + await fs.writeFile( + "resources/public/images/sprites/symbol/icons.svg", + iconsSprite, + ); + + const cursors = await findFiles("resources/images/cursors/", isSvgFile); + const cursorsSprite = await generateSvgSprite(cursors, "cursor-"); + await fs.writeFile( + "resources/public/images/sprites/symbol/cursors.svg", + cursorsSprite, + ); + + const assets = await findFiles("resources/images/assets/", isSvgFile); + const assetsSprite = await generateSvgSprite(assets, "asset-"); + await fs.writeFile( + "resources/public/images/sprites/assets.svg", + assetsSprite, + ); +} + +async function generateTemplates() { + const isDebug = process.env.NODE_ENV !== "production"; + await fs.mkdir("./resources/public/", { recursive: true }); + + let translations = await readTranslations(); + const storybookTranslations = JSON.stringify( + filterTranslations(translations, ["en"], (key) => + key.startsWith("labels."), + ), + ); + translations = JSON.stringify(translations); + + const manifest = await readShadowManifest(); + let content; + + const iconsSprite = await fs.readFile( + "resources/public/images/sprites/symbol/icons.svg", + "utf8", + ); + const cursorsSprite = await fs.readFile( + "resources/public/images/sprites/symbol/cursors.svg", + "utf8", + ); + const assetsSprite = await fs.readFile( + "resources/public/images/sprites/assets.svg", + "utf-8", + ); + const partials = { + "../public/images/sprites/symbol/icons.svg": iconsSprite, + "../public/images/sprites/symbol/cursors.svg": cursorsSprite, + "../public/images/sprites/assets.svg": assetsSprite, + }; + + const pluginRuntimeUri = + process.env.PENPOT_PLUGIN_DEV === "true" + ? "http://localhost:4200" + : "./plugins-runtime"; + + content = await renderTemplate( + "resources/templates/index.mustache", + { + manifest: manifest, + translations: JSON.stringify(translations), + pluginRuntimeUri, + isDebug, + }, + partials, + ); + + await fs.writeFile("./resources/public/index.html", content); + + content = await renderTemplate( + "resources/templates/challenge.mustache", + {}, + partials, + ); + await fs.writeFile("./resources/public/challenge.html", content); + + content = await renderTemplate( + "resources/templates/preview-body.mustache", + { + manifest: manifest, + }, + partials, + ); + await fs.writeFile("./.storybook/preview-body.html", content); + + content = await renderTemplate( + "resources/templates/preview-head.mustache", + { + manifest: manifest, + translations: JSON.stringify(storybookTranslations), + }, + partials, + ); + await fs.writeFile("./.storybook/preview-head.html", content); + + content = await renderTemplate("resources/templates/render.mustache", { + manifest: manifest, + translations: JSON.stringify(translations), + }); + + await fs.writeFile("./resources/public/render.html", content); + + content = await renderTemplate("resources/templates/rasterizer.mustache", { + manifest: manifest, + translations: JSON.stringify(translations), + }); + + await fs.writeFile("./resources/public/rasterizer.html", content); +} + +export async function compileStorybookStyles() { + const worker = startWorker(); + const start = process.hrtime(); + + log.info("init: compile storybook styles"); + let result = await compileSassStorybook(worker); + result = concatSass(result); + + await fs.mkdir("./resources/public/css", { recursive: true }); + await fs.writeFile("./resources/public/css/ds.css", result); + + const end = process.hrtime(start); + log.info("done: compile storybook styles", `(${ppt(end)})`); + worker.terminate(); +} + +export async function compileStyles() { + const worker = startWorker(); + const start = process.hrtime(); + + log.info("init: compile styles"); + let result = await compileSassAll(worker); + result = concatSass(result); + + await fs.mkdir("./resources/public/css", { recursive: true }); + await fs.writeFile("./resources/public/css/main.css", result); + + if (isDebug) { + let debugCSS = await compileSassDebug(worker); + await fs.writeFile("./resources/public/css/debug.css", debugCSS); + } + + const end = process.hrtime(start); + log.info("done: compile styles", `(${ppt(end)})`); + worker.terminate(); +} + +export async function compileSvgSprites() { + const start = process.hrtime(); + log.info("init: compile svgsprite"); + await generateSvgSprites(); + const end = process.hrtime(start); + log.info("done: compile svgsprite", `(${ppt(end)})`); +} + +export async function compileTemplates() { + const start = process.hrtime(); + log.info("init: compile templates"); + await generateTemplates(); + const end = process.hrtime(start); + log.info("done: compile templates", `(${ppt(end)})`); +} + +export async function compilePolyfills() { + const start = process.hrtime(); + log.info("init: compile polyfills"); + + const files = await findFiles("resources/polyfills/", isJsFile); + let result = []; + for (let path of files) { + const content = await fs.readFile(path, { encoding: "utf-8" }); + result.push(content); + } + + await fs.mkdir("./resources/public/js", { recursive: true }); + fs.writeFile("resources/public/js/polyfills.js", result.join("\n")); + + const end = process.hrtime(start); + log.info("done: compile polyfills", `(${ppt(end)})`); +} + +export async function copyAssets() { + const start = process.hrtime(); + log.info("init: copy assets"); + + await syncDirs("resources/images/", "resources/public/images/"); + await syncDirs("resources/fonts/", "resources/public/fonts/"); + await syncDirs( + "resources/plugins-runtime/", + "resources/public/plugins-runtime/", + ); + + const end = process.hrtime(start); + log.info("done: copy assets", `(${ppt(end)})`); +} diff --git a/frontend/scripts/_worker.js b/frontend/scripts/_worker.js new file mode 100644 index 0000000000000000000000000000000000000000..47e33366429de3edf5d75130f246e106cb205348 --- /dev/null +++ b/frontend/scripts/_worker.js @@ -0,0 +1,103 @@ +import proc from "node:child_process"; +import fs from "node:fs/promises"; +import ph from "node:path"; +import url from "node:url"; +import * as sass from "sass-embedded"; +import log from "fancy-log"; + +import wpool from "workerpool"; +import postcss from "postcss"; +import modulesProcessor from "postcss-modules"; +import autoprefixerProcessor from "autoprefixer"; + +const compiler = await sass.initAsyncCompiler(); + +async function compileFile(path) { + const dir = ph.dirname(path); + const name = ph.basename(path, ".scss"); + const dest = `${dir}${ph.sep}${name}.css`; + + return new Promise(async (resolve, reject) => { + try { + const result = await compiler.compileAsync(path, { + loadPaths: [ + "node_modules/animate.css", + "resources/styles/common/", + "resources/styles", + ], + sourceMap: false, + }); + // console.dir(result); + resolve({ + inputPath: path, + outputPath: dest, + css: result.css, + }); + } catch (cause) { + // console.error(cause); + reject(cause); + } + }); +} + +function configureModulesProcessor(options) { + const ROOT_NAME = "app"; + + return modulesProcessor({ + getJSON: (cssFileName, json, outputFileName) => { + // We do nothing because we don't want the generated JSON files + }, + // Calculates the whole css-module selector name. + // Should be the same as the one in the file `/src/app/main/style.clj` + generateScopedName: (selector, filename, css) => { + const dir = ph.dirname(filename); + const name = ph.basename(filename, ".css"); + const parts = dir.split("/"); + const rootIdx = parts.findIndex((s) => s === ROOT_NAME); + return parts.slice(rootIdx + 1).join("_") + "_" + name + "__" + selector; + }, + }); +} + +function configureProcessor(options = {}) { + const processors = []; + + if (options.modules) { + processors.push(configureModulesProcessor(options)); + } + processors.push(autoprefixerProcessor); + + return postcss(processors); +} + +async function postProcessFile(data, options) { + const proc = configureProcessor(options); + + // We compile to the same path (all in memory) + const result = await proc.process(data.css, { + from: data.outputPath, + to: data.outputPath, + map: false, + }); + + return Object.assign(data, { + css: result.css, + }); +} + +async function compile(path, options) { + let result = await compileFile(path); + return await postProcessFile(result, options); +} + +wpool.worker( + { + compileSass: compile, + }, + { + onTerminate: async (code) => { + // log.info("worker: terminate"); + await compiler.dispose(); + }, + }, +); diff --git a/frontend/scripts/build b/frontend/scripts/build index ccb9236b78e2c9e16b9da9863146801ee1062b77..97199d20ded94d64403b1fcc7efca7de3ac80ab1 100755 --- a/frontend/scripts/build +++ b/frontend/scripts/build @@ -1,24 +1,37 @@ #!/usr/bin/env bash +# NOTE: this script should be called from the parent directory to +# properly work. set -ex +export INCLUDE_STORYBOOK=${BUILD_STORYBOOK:-no}; + export CURRENT_VERSION=$1; export BUILD_DATE=$(date -R); export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)}; export EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS; +export TS=$(date +%s); # Some cljs reacts on this environment variable for define more # performant code on macros (example: rumext) export NODE_ENV=production; yarn install || exit 1; -yarn run build:clean || exit 1; -yarn run build:styles || exit 1; +rm -rf resources/public; +rm -rf target/dist; + +clojure -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS || exit 1 -clojure -J-Xms100M -J-Xmx1000M -J-XX:+UseSerialGC -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1 +yarn run build:app:assets || exit 1; -yarn run build:assets || exit 1; -yarn run build:copy || exit 1; +mkdir -p target/dist; +rsync -avr resources/public/ target/dist/ sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/index.html; sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html; + +if [ "$INCLUDE_STORYBOOK" = "yes" ]; then + # build storybook + yarn run build:storybook || exit 1; + rsync -avr storybook-static/ target/dist/storybook-static; +fi diff --git a/frontend/scripts/build-app-assets.js b/frontend/scripts/build-app-assets.js new file mode 100644 index 0000000000000000000000000000000000000000..902f4c39e2be8f26447e8024d0afbebfbed97ffc --- /dev/null +++ b/frontend/scripts/build-app-assets.js @@ -0,0 +1,7 @@ +import * as h from "./_helpers.js"; + +await h.compileStyles(); +await h.copyAssets(); +await h.compileSvgSprites(); +await h.compileTemplates(); +await h.compilePolyfills(); diff --git a/frontend/scripts/build-storybook-assets.js b/frontend/scripts/build-storybook-assets.js new file mode 100644 index 0000000000000000000000000000000000000000..c0eb37a36ffdc1d6d235a0394efa39874addfaf6 --- /dev/null +++ b/frontend/scripts/build-storybook-assets.js @@ -0,0 +1,7 @@ +import * as h from "./_helpers.js"; + +await h.compileStorybookStyles(); +await h.copyAssets(); +await h.compileSvgSprites(); +await h.compileTemplates(); +await h.compilePolyfills(); diff --git a/frontend/scripts/compress-png b/frontend/scripts/compress-png deleted file mode 100755 index b18a64b96a3f2abc43884d0fb9a706c2d1fdc2be..0000000000000000000000000000000000000000 --- a/frontend/scripts/compress-png +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -# This script automates compressing PNG images using the lossless Zopfli -# Compression Algorithm. The process is slow but can produce significantly -# better compression and, thus, smaller file sizes. -# -# This script is meant to be run manually, for example, before making a new -# release. -# -# Requirements -# -# zopflipng - https://github.com/google/zopfli -# Debian/Ubuntu: sudo apt install zopfli -# Fedora: sudo dnf install zopfli -# macOS: brew install zopfli -# -# Usage -# -# This script takes a single positional argument which is the path where to -# search for PNG files. By default, the target path is the current working -# directory. Run from the root of the repository to compress all PNG images. Run -# from the `frontend` subdirectory to compress all PNG images within that -# directory. Alternatively, run from any directory and pass an explicit path to -# `compress-png` to limit the script to that path/directory. - -set -o errexit -set -o nounset -set -o pipefail - -readonly TARGET="${1:-.}" -readonly ABS_TARGET="$(command -v realpath &>/dev/null && realpath "$TARGET")" - -function png_total_size() { - find "$TARGET" -type f -iname '*.png' -exec du -ch {} + | tail -1 -} - -echo "Compressing PNGs in ${ABS_TARGET:-$TARGET}" - -echo "Before" -png_total_size - -readonly opts=( - # More iterations means slower, potentially better compression. - #--iterations=500 - -m - # Try all filter strategies (slow). - #--filters=01234mepb - # According to docs, remove colors behind alpha channel 0. No visual - # difference, removes hidden information. - --lossy_transparent - # Avoid information loss that could affect how images are rendered, see - # https://github.com/penpot/penpot/issues/1533#issuecomment-1030005203 - # https://github.com/google/zopfli/issues/113 - --keepchunks=cHRM,gAMA,pHYs,iCCP,sRGB,oFFs,sTER - # Since we have git behind our back, overwrite PNG files in-place (only - # when result is smaller). - -y -) -time find "$TARGET" -type f -iname '*.png' -exec zopflipng "${opts[@]}" {} {} \; - -echo "After" -png_total_size diff --git a/frontend/scripts/e2e-server.js b/frontend/scripts/e2e-server.js new file mode 100644 index 0000000000000000000000000000000000000000..77be5fccac33a3c6f8f83b04b8c3dcd3c07707df --- /dev/null +++ b/frontend/scripts/e2e-server.js @@ -0,0 +1,20 @@ +import express from "express"; +import compression from "compression"; + +import { fileURLToPath } from "url"; +import path from "path"; + +const app = express(); +const port = 3000; + +app.use(compression()); + +const staticPath = path.join( + fileURLToPath(import.meta.url), + "../../resources/public", +); +app.use(express.static(staticPath)); + +app.listen(port, () => { + console.log(`Listening at 0.0.0.0:${port}`); +}); diff --git a/frontend/scripts/find-unused-translations.js b/frontend/scripts/find-unused-translations.js index 3369e4dce9a513c6f4886a0cf640bc7abbb33bc8..7770031f5b1d19683d931fc9e97834d18bc62648 100644 --- a/frontend/scripts/find-unused-translations.js +++ b/frontend/scripts/find-unused-translations.js @@ -1,26 +1,27 @@ -const fs = require('fs').promises; -const gt = require("gettext-parser"); -const path = require('path'); -const util = require('node:util'); -const execFile = util.promisify(require('node:child_process').execFile); - - -async function processMsgId(msgId){ - return execFile('grep', ['-r', '-o', msgId, './src']) - .catch(()=> { return msgId}) +import gt from "gettext-parser"; +import fs from "node:fs/promises"; +import path from "node:path"; +import util from "node:util"; +import { execFile as execFileCb } from "node:child_process"; + +const execFile = util.promisify(execFileCb); + +async function processMsgId(msgId) { + return execFile("grep", ["-r", "-o", msgId, "./src"]).catch(() => { + return msgId; + }); } - async function processFile(f) { const content = await fs.readFile(f); - const data = gt.po.parse(content, "utf-8") - const translations = data.translations['']; + const data = gt.po.parse(content, "utf-8"); + const translations = data.translations[""]; const badIds = []; for (const property in translations) { const data = await processMsgId(translations[property].msgid); - if (data!=null && data.stdout === undefined){ - badIds.push(data) + if (data != null && data.stdout === undefined) { + badIds.push(data); } } @@ -28,63 +29,77 @@ async function processFile(f) { } async function cleanFile(f, badIds) { - console.log ("\n\nDoing automatic cleanup") + console.log("\n\nDoing automatic cleanup"); const content = await fs.readFile(f); const data = gt.po.parse(content, "utf-8"); - const translations = data.translations['']; + const translations = data.translations[""]; const keys = Object.keys(translations); for (const key of keys) { property = translations[key]; - if (badIds.includes(property.msgid)){ - console.log ('----> deleting', property.msgid) - delete data.translations[''][key]; + if (badIds.includes(property.msgid)) { + console.log("----> deleting", property.msgid); + delete data.translations[""][key]; } } - const buff = gt.po.compile(data, {sort: true}); + const buff = gt.po.compile(data, { sort: true }); await fs.writeFile(f, buff); } - - async function findExecutionTimeTranslations() { - const { stdout } = await execFile('grep', ['-r', '-h', '-F', '(tr (', './src']); + const { stdout } = await execFile("grep", [ + "-r", + "-h", + "-F", + "(tr (", + "./src", + ]); console.log(stdout); } async function welcome() { - console.log ('####################################################################') - console.log ('# UNUSED TRANSLATIONS FINDER #') - console.log ('####################################################################') - console.log ('\n'); - console.log ('DISCLAIMER: Some translations are only available at execution time.') - console.log (' This finder can\'t process them, so there can be') - console.log (' false positives.\n') - console.log (' If you want to do an automatic clean anyway,') - console.log (' call the script with:') - console.log (' npm run find-unused-translations -- --clean') - console.log (' For example:'); - console.log ('--------------------------------------------------------------------'); + console.log( + "####################################################################", + ); + console.log( + "# UNUSED TRANSLATIONS FINDER #", + ); + console.log( + "####################################################################", + ); + console.log("\n"); + console.log( + "DISCLAIMER: Some translations are only available at execution time.", + ); + console.log(" This finder can't process them, so there can be"); + console.log(" false positives.\n"); + console.log(" If you want to do an automatic clean anyway,"); + console.log(" call the script with:"); + console.log(" npm run find-unused-translations -- --clean"); + console.log(" For example:"); + console.log( + "--------------------------------------------------------------------", + ); await findExecutionTimeTranslations(); - console.log ('--------------------------------------------------------------------'); + console.log( + "--------------------------------------------------------------------", + ); } - const doCleanup = process.argv.slice(2)[0] == "--clean"; - -;(async () => { +(async () => { await welcome(); const target = path.normalize("./translations/en.po"); const badIds = await processFile(target); - if (doCleanup){ + if (doCleanup) { cleanFile(target, badIds); } else { - for (const badId of badIds){ + for (const badId of badIds) { console.log(badId); } } -})() +})(); diff --git a/frontend/scripts/jvm-repl b/frontend/scripts/jvm-repl deleted file mode 100755 index b59aaaca890f6b7f91b6a664404fbd5c18ab0005..0000000000000000000000000000000000000000 --- a/frontend/scripts/jvm-repl +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# A repl useful for debug macros. - -export OPTIONS="\ - -J-XX:-OmitStackTraceInFastThrow \ - -J-Xms50m -J-Xmx512m \ - -M:dev:jvm-repl"; - -set -ex; -exec clojure $OPTIONS; diff --git a/frontend/scripts/translations.js b/frontend/scripts/translations.js new file mode 100755 index 0000000000000000000000000000000000000000..a781a45d8cc817bf0680652caeb860ce8eb9e46d --- /dev/null +++ b/frontend/scripts/translations.js @@ -0,0 +1,377 @@ +#!/usr/bin/env node + +import getopts from "getopts"; +import { promises as fs, createReadStream } from "fs"; +import gt from "gettext-parser"; +import l from "lodash"; +import path from "path"; +import readline from "readline"; + +const baseLocale = "en"; + +async function* getFiles(dir) { + // console.log("getFiles", dir) + const dirents = await fs.readdir(dir, { withFileTypes: true }); + for (const dirent of dirents) { + let res = path.resolve(dir, dirent.name); + res = path.relative(".", res); + + if (dirent.isDirectory()) { + yield* getFiles(res); + } else { + yield res; + } + } +} + +async function translationExists(locale) { + const target = path.normalize("./translations/"); + const targetPath = path.join(target, `${locale}.po`); + + try { + const result = await fs.stat(targetPath); + return true; + } catch (cause) { + return false; + } +} + +async function readLocaleByPath(path) { + const content = await fs.readFile(path); + return gt.po.parse(content, "utf-8"); +} + +async function writeLocaleByPath(path, data) { + const buff = gt.po.compile(data, { sort: true }); + await fs.writeFile(path, buff); +} + +async function readLocale(locale) { + const target = path.normalize("./translations/"); + const targetPath = path.join(target, `${locale}.po`); + return readLocaleByPath(targetPath); +} + +async function writeLocale(locale, data) { + const target = path.normalize("./translations/"); + const targetPath = path.join(target, `${locale}.po`); + return writeLocaleByPath(targetPath, data); +} + +async function* scanLocales() { + const fileRe = /.+\.po$/; + const target = path.normalize("./translations/"); + const parent = path.join(target, ".."); + + for await (const f of getFiles(target)) { + if (!fileRe.test(f)) continue; + const data = path.parse(f); + yield data; + } +} + +async function processLocale(options, f) { + let locales = options.locale; + if (typeof locales === "string") { + locales = locales.split(/,/); + } else if (Array.isArray(locales)) { + } else if (locales === undefined) { + } else { + console.error(`Invalid value found on locales parameter: '${locales}'`); + process.exit(-1); + } + + for await (const { name } of scanLocales()) { + if (locales === undefined || locales.includes(name)) { + await f(name); + } + } +} + +async function processTranslation(data, prefix, f) { + for (let key of Object.keys(data.translations[""])) { + if (key === prefix || key.startsWith(prefix)) { + let value = data.translations[""][key]; + value = await f(value); + data.translations[""][key] = value; + } + } + return data; +} + +async function* readLines(filePath) { + const fileStream = createReadStream(filePath); + + const reader = readline.createInterface({ + input: fileStream, + crlfDelay: Infinity, + }); + + let counter = 1; + + for await (const line of reader) { + yield [counter, line]; + counter++; + } +} + +const trRe1 = /\(tr\s+"([\w\.\-]+)"/g; + +function getTranslationStrings(line) { + const result = Array.from(line.matchAll(trRe1)).map((match) => { + return match[1]; + }); + + return result; +} + +async function deleteByPrefix(options, prefix, ...params) { + if (!prefix) { + console.error(`Prefix undefined`); + process.exit(1); + } + + await processLocale(options, async (locale) => { + const data = await readLocale(locale); + let deleted = []; + + for (const [key, value] of Object.entries(data.translations[""])) { + if (key.startsWith(prefix)) { + delete data.translations[""][key]; + deleted.push(key); + } + } + + await writeLocale(locale, data); + + console.log( + `=> Processed locale '${locale}': deleting prefix '${prefix}' (deleted=${deleted.length})`, + ); + + if (options.verbose) { + for (let key of deleted) { + console.log(`-> Deleted key: ${key}`); + } + } + }); +} + +async function markFuzzy(options, prefix, ...other) { + if (!prefix) { + console.error(`Prefix undefined`); + process.exit(1); + } + + await processLocale(options, async (locale) => { + let data = await readLocale(locale); + data = await processTranslation(data, prefix, (translation) => { + if (translation.comments === undefined) { + translation.comments = {}; + } + + const flagData = translation.comments.flag ?? ""; + const flags = flagData.split(/\s*,\s*/).filter((s) => s !== ""); + + if (!flags.includes("fuzzy")) { + flags.push("fuzzy"); + } + + translation.comments.flag = flags.join(", "); + + console.log( + `=> Processed '${locale}': marking fuzzy '${translation.msgid}'`, + ); + + return translation; + }); + + await writeLocale(locale, data); + }); +} + +async function rehash(options, ...other) { + const fileRe = /.+\.(?:clj|cljs|cljc)$/; + + // Iteration 1: process all locales and update it with existing + // entries on the source code. + + const used = await (async function () { + const result = {}; + + for await (const f of getFiles("src")) { + if (!fileRe.test(f)) continue; + + for await (const [n, line] of readLines(f)) { + const strings = getTranslationStrings(line); + + strings.forEach((key) => { + const entry = `${f}:${n}`; + if (result[key] !== undefined) { + result[key].push(entry); + } else { + result[key] = [entry]; + } + }); + } + } + + await processLocale({ locale: baseLocale }, async (locale) => { + const data = await readLocale(locale); + + for (let [key, val] of Object.entries(result)) { + let entry = data.translations[""][key]; + + if (entry === undefined) { + entry = { + msgid: key, + comments: { + reference: val.join(", "), + flag: "fuzzy", + }, + msgstr: [""], + }; + } else { + if (entry.comments === undefined) { + entry.comments = {}; + } + + entry.comments.reference = val.join(", "); + + const flagData = entry.comments.flag ?? ""; + const flags = flagData.split(/\s*,\s*/).filter((s) => s !== ""); + + if (flags.includes("unused")) { + flags = flags.filter((o) => o !== "unused"); + } + + entry.comments.flag = flags.join(", "); + } + + data.translations[""][key] = entry; + } + + await writeLocale(locale, data); + + const keys = Object.keys(data.translations[""]); + console.log(`=> Found ${keys.length} used translations`); + }); + + return result; + })(); + + // Iteration 2: process only base locale and properly detect unused + // translation strings. + + await (async function () { + let totalUnused = 0; + + await processLocale({ locale: baseLocale }, async (locale) => { + const data = await readLocale(locale); + + for (let [key, val] of Object.entries(data.translations[""])) { + if (key === "") continue; + + if (!used.hasOwnProperty(key)) { + totalUnused++; + + const entry = data.translations[""][key]; + if (entry.comments === undefined) { + entry.comments = {}; + } + + const flagData = entry.comments.flag ?? ""; + const flags = flagData.split(/\s*,\s*/).filter((s) => s !== ""); + + if (!flags.includes("unused")) { + flags.push("unused"); + } + + entry.comments.flag = flags.join(", "); + + data.translations[""][key] = entry; + } + } + + await writeLocale(locale, data); + }); + + console.log(`=> Found ${totalUnused} unused strings`); + })(); +} + +async function synchronize(options, ...other) { + const baseData = await readLocale(baseLocale); + + await processLocale(options, async (locale) => { + if (locale === baseLocale) return; + + const data = await readLocale(locale); + + for (let [key, val] of Object.entries(baseData.translations[""])) { + if (key === "") continue; + + const baseEntry = baseData.translations[""][key]; + const entry = data.translations[""][key]; + + if (entry === undefined) { + // Do nothing + } else { + entry.comments = baseEntry.comments; + data.translations[""][key] = entry; + } + } + + for (let [key, val] of Object.entries(data.translations[""])) { + if (key === "") continue; + + const baseEntry = baseData.translations[""][key]; + const entry = data.translations[""][key]; + + if (baseEntry === undefined) { + delete data.translations[""][key]; + } + } + + await writeLocale(locale, data); + }); +} + +const options = getopts(process.argv.slice(2), { + boolean: ["h", "v"], + alias: { + help: ["h"], + locale: ["l"], + verbose: ["v"], + }, + stopEarly: true, +}); + +const [command, ...params] = options._; + +if (command === "rehash") { + await rehash(options, ...params); +} else if (command === "sync") { + await synchronize(options, ...params); +} else if (command === "delete") { + await deleteByPrefix(options, ...params); +} else if (command === "fuzzy") { + await markFuzzy(options, ...params); +} else { + console.log(`Translations manipulation script. +How to use: +./scripts/translation.js + +Available options: + + --locale -l : specify a concrete locale + --verbose -v : enables verbose output + --help -h : prints this help + +Available subcommands: + + rehash : reads and writes all translations files, sorting and validating + sync : synchronize baselocale file with all other locale files + delete : delete all entries that matches the prefix + fuzzy : mark as fuzzy all entries that matches the prefix +`); +} diff --git a/frontend/scripts/validate-translations.js b/frontend/scripts/validate-translations.js deleted file mode 100644 index 52674054fe337b7c331f34e305d33f18992e03c4..0000000000000000000000000000000000000000 --- a/frontend/scripts/validate-translations.js +++ /dev/null @@ -1,31 +0,0 @@ -const fs = require('fs').promises; -const gt = require("gettext-parser"); -const l = require("lodash"); -const path = require('path'); - -async function* getFiles(dir) { - const dirents = await fs.readdir(dir, { withFileTypes: true }); - for (const dirent of dirents) { - const res = path.resolve(dir, dirent.name); - if (dirent.isDirectory()) { - yield* getFiles(res); - } else { - yield res; - } - } -} - -;(async () => { - const fileRe = /.+\.po$/; - const target = path.normalize("./translations/"); - const parent = path.join(target, ".."); - for await (const f of getFiles(target)) { - if (!fileRe.test(f)) continue; - const entry = path.relative(parent, f); - console.log(`=> processing: ${entry}`); - const content = await fs.readFile(f); - const data = gt.po.parse(content, "utf-8") - const buff = gt.po.compile(data, {sort: true}); - await fs.writeFile(f, buff); - } -})() diff --git a/frontend/scripts/watch-storybook.js b/frontend/scripts/watch-storybook.js new file mode 100644 index 0000000000000000000000000000000000000000..a82a66932d732bd189818ce28c5124014b435cc8 --- /dev/null +++ b/frontend/scripts/watch-storybook.js @@ -0,0 +1,86 @@ +import fs from "node:fs/promises"; +import ph from "node:path"; + +import log from "fancy-log"; +import * as h from "./_helpers.js"; +import ppt from "pretty-time"; + +const worker = h.startWorker(); +let sass = null; + +async function compileSassAll() { + const start = process.hrtime(); + log.info("init: compile storybook styles"); + + sass = await h.compileSassStorybook(worker); + let output = await h.concatSass(sass); + await fs.writeFile("./resources/public/css/ds.css", output); + + const end = process.hrtime(start); + log.info("done: compile storybook styles", `(${ppt(end)})`); +} + +async function compileSass(path) { + const start = process.hrtime(); + log.info("changed:", path); + const result = await h.compileSass(worker, path, { modules: true }); + sass.index[result.outputPath] = result.css; + + const output = h.concatSass(sass); + await fs.writeFile("./resources/public/css/ds.css", output); + + const end = process.hrtime(start); + log.info("done:", `(${ppt(end)})`); +} + +await fs.mkdir("./resources/public/css/", { recursive: true }); +await compileSassAll(); +await h.copyAssets(); +await h.compileSvgSprites(); +await h.compileTemplates(); +await h.compilePolyfills(); + +log.info("watch: scss src (~)"); + +h.watch("src", h.isSassFile, async function (path) { + const isPartial = ph.basename(path).startsWith("_"); + const isCommon = isPartial || ph.dirname(path).endsWith("/ds"); + + if (isCommon) { + await compileSassAll(path); + } else { + await compileSass(path); + } +}); + +log.info("watch: scss: resources (~)"); +h.watch("resources/styles", h.isSassFile, async function (path) { + log.info("changed:", path); + await compileSassAll(); +}); + +log.info("watch: templates (~)"); +h.watch("resources/templates", null, async function (path) { + log.info("changed:", path); + await h.compileTemplates(); +}); + +log.info("watch: translations (~)"); +h.watch("translations", null, async function (path) { + log.info("changed:", path); + await h.compileTemplates(); +}); + +log.info("watch: assets (~)"); +h.watch( + ["resources/images", "resources/fonts", "resources/plugins-runtime"], + null, + async function (path) { + log.info("changed:", path); + await h.compileSvgSprites(); + await h.copyAssets(); + await h.compileTemplates(); + }, +); + +worker.terminate(); diff --git a/frontend/scripts/watch.js b/frontend/scripts/watch.js new file mode 100644 index 0000000000000000000000000000000000000000..99ced7b70822b5d40103ad6aaf48984a32f79db4 --- /dev/null +++ b/frontend/scripts/watch.js @@ -0,0 +1,85 @@ +import proc from "node:child_process"; +import fs from "node:fs/promises"; +import ph from "node:path"; + +import log from "fancy-log"; +import * as h from "./_helpers.js"; +import ppt from "pretty-time"; + +const worker = h.startWorker(); +let sass = null; + +async function compileSassAll() { + const start = process.hrtime(); + log.info("init: compile styles"); + + sass = await h.compileSassAll(worker); + let output = await h.concatSass(sass); + await fs.writeFile("./resources/public/css/main.css", output); + + const end = process.hrtime(start); + log.info("done: compile styles", `(${ppt(end)})`); +} + +async function compileSass(path) { + const start = process.hrtime(); + log.info("changed:", path); + const result = await h.compileSass(worker, path, { modules: true }); + sass.index[result.outputPath] = result.css; + + const output = h.concatSass(sass); + + await fs.writeFile("./resources/public/css/main.css", output); + + const end = process.hrtime(start); + log.info("done:", `(${ppt(end)})`); +} + +await fs.mkdir("./resources/public/css/", { recursive: true }); +await compileSassAll(); +await h.copyAssets(); +await h.compileSvgSprites(); +await h.compileTemplates(); +await h.compilePolyfills(); + +log.info("watch: scss src (~)"); + +h.watch("src", h.isSassFile, async function (path) { + if (path.includes("common")) { + await compileSassAll(path); + } else { + await compileSass(path); + } +}); + +log.info("watch: scss: resources (~)"); +h.watch("resources/styles", h.isSassFile, async function (path) { + log.info("changed:", path); + await compileSassAll(); +}); + +log.info("watch: templates (~)"); +h.watch("resources/templates", null, async function (path) { + log.info("changed:", path); + await h.compileTemplates(); +}); + +log.info("watch: translations (~)"); +h.watch("translations", null, async function (path) { + log.info("changed:", path); + await h.compileTemplates(); +}); + +log.info("watch: assets (~)"); +h.watch( + ["resources/images", "resources/fonts", "resources/plugins-runtime"], + null, + async function (path) { + log.info("changed:", path); + await h.compileSvgSprites(); + await h.copyAssets(); + await h.compileTemplates(); + }, +); + +worker.terminate(); diff --git a/frontend/shadow-cljs.edn b/frontend/shadow-cljs.edn index 90144819736e8f458dc89bc052a0daa85b77b1a4..0c0b91b577c08e9ce51b2e191f41ef4062a828ba 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -18,7 +18,7 @@ {:entries []} :main - {:entries [app.main] + {:entries [app.main app.plugins.api] :depends-on #{:shared} :init-fn app.main/init} @@ -91,13 +91,8 @@ :modules {:base {:entries []} - - :icons - {:exports {default app.main.ui.icons/default} - :depends-on #{:base}} - :components - {:exports {:default app.main.ui.components/default} + {:exports {default app.main.ui.ds/default} :depends-on #{:base}}} :compiler-options diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index c95f72e1a04c57d05cd755ef1595c42477ac051c..72bbb4d00f49022c19f882f5c7bb422434303a90 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -69,7 +69,8 @@ :enable-onboarding-questions :enable-onboarding-newsletter :enable-dashboard-templates-section - :enable-google-fonts-provider]) + :enable-google-fonts-provider + :enable-component-thumbnails]) (defn- parse-flags [global] @@ -109,6 +110,7 @@ (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" "https://penpot.app/privacy")) (def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) +(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot-docs-plugins.pages.dev/plugins/getting-started/#examples")) (defn- normalize-uri [uri-str] @@ -130,6 +132,17 @@ (def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js")) +(defn external-feature-flag + [flag value] + (let [f (obj/get global "externalFeatureFlag")] + (when (fn? f) + (f flag value)))) + +(defn external-session-id + [] + (let [f (obj/get global "externalSessionId")] + (when (fn? f) (f)))) + ;; --- Helper Functions (defn ^boolean check-browser? [candidate] @@ -154,11 +167,17 @@ (avatars/generate {:name name}) (dm/str (u/join public-uri "assets/by-id/" photo-id)))) +(defn resolve-media + [id] + (dm/str (u/join public-uri "assets/by-id/" (str id)))) + (defn resolve-file-media ([media] (resolve-file-media media false)) - ([{:keys [id] :as media} thumbnail?] - (dm/str - (cond-> (u/join public-uri "assets/by-file-media-id/") - (true? thumbnail?) (u/join (dm/str id "/thumbnail")) - (false? thumbnail?) (u/join (dm/str id)))))) + ([{:keys [id data-uri] :as media} thumbnail?] + (if data-uri + data-uri + (dm/str + (cond-> (u/join public-uri "assets/by-file-media-id/") + (true? thumbnail?) (u/join (dm/str id "/thumbnail")) + (false? thumbnail?) (u/join (dm/str id))))))) diff --git a/frontend/src/app/libs/file_builder.cljs b/frontend/src/app/libs/file_builder.cljs index 4d3b21adcea52998bc3e17947f65a506e0ce37b6..1d8e2815ff6b14bff5d7c7e959485462a91bbf4c 100644 --- a/frontend/src/app/libs/file_builder.cljs +++ b/frontend/src/app/libs/file_builder.cljs @@ -7,17 +7,18 @@ (ns app.libs.file-builder (:require [app.common.data :as d] + [app.common.features :as cfeat] [app.common.files.builder :as fb] [app.common.media :as cm] [app.common.types.components-list :as ctkl] [app.common.uuid :as uuid] - [app.util.dom :as dom] [app.util.json :as json] [app.util.webapi :as wapi] [app.util.zip :as uz] [app.worker.export :as e] [beicon.v2.core :as rx] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [promesa.core :as p])) (defn parse-data [data] (as-> data $ @@ -73,7 +74,7 @@ manifest-stream (->> files-stream - (rx/map #(e/create-manifest (uuid/next) (:id file) :all % false)) + (rx/map #(e/create-manifest (uuid/next) (:id file) :all % cfeat/default-features)) (rx/map (fn [a] (vector "manifest.json" a)))) @@ -144,7 +145,7 @@ (str (:current-page-id file))) (addPage [_ name options] - (set! file (fb/add-page file {:name name :options options})) + (set! file (fb/add-page file {:name name :options (parse-data options)})) (str (:current-page-id file))) (closePage [_] @@ -248,19 +249,33 @@ (deleteObject [_ id] (set! file (fb/delete-object file (uuid/uuid id)))) + (getId [_] + (:id file)) + + (getCurrentPageId [_] + (:current-page-id file)) + (asMap [_] (clj->js file)) + (newId [_] + (uuid/next)) + (export [_] - (->> (export-file file) - (rx/subs - (fn [value] - (when (not (contains? value :type)) - (let [[file export-blob] value] - (dom/trigger-download (:name file) export-blob)))))))) + (p/create + (fn [resolve reject] + (->> (export-file file) + (rx/take 1) + (rx/subs! + (fn [value] + (when (not (contains? value :type)) + (let [[_ export-blob] value] + (resolve export-blob)))) + reject)))))) (defn create-file-export [^string name] - (File. (fb/create-file name))) + (binding [cfeat/*current* cfeat/default-features] + (File. (fb/create-file name)))) (defn exports [] #js {:createFile create-file-export}) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 868d4b9ea8a49e46a73c14c4b602998d313efcba..3cbbe0d701ba509314ad47c0380235d0336368df 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -25,6 +25,7 @@ [app.main.ui.modal :refer [modal]] [app.main.ui.routes :as rt] [app.main.worker :as worker] + [app.plugins :as plugins] [app.util.dom :as dom] [app.util.i18n :as i18n] [app.util.theme :as theme] @@ -114,7 +115,8 @@ (cur/init-styles) (thr/init!) (init-ui) - (st/emit! (initialize))) + (st/emit! (plugins/initialize) + (initialize))) (defn ^:export reinit ([] diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs new file mode 100644 index 0000000000000000000000000000000000000000..a6fd8f73da7c8767fea9466f75abe490c63ab4ca --- /dev/null +++ b/frontend/src/app/main/data/changes.cljs @@ -0,0 +1,193 @@ +;; 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 app.main.data.changes + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.files.changes :as cpc] + [app.common.logging :as log] + [app.common.types.shape-tree :as ctst] + [app.common.uuid :as uuid] + [app.main.features :as features] + [app.main.worker :as uw] + [app.util.time :as dt] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) + +;; Change this to :info :debug or :trace to debug this module +(log/set-level! :debug) + +(def page-change? + #{:add-page :mod-page :del-page :mov-page}) +(def update-layout-attr? + #{:hidden}) + +(def commit? + (ptk/type? ::commit)) + +(defn- fix-page-id + "For events that modifies the page, page-id does not comes + as a property so we assign it from the `id` property." + [{:keys [id type page] :as change}] + (cond-> change + (and (page-change? type) + (nil? (:page-id change))) + (assoc :page-id (or id (:id page))))) + +(defn- update-indexes + "Given a commit, send the changes to the worker for updating the + indexes." + [commit attr] + (ptk/reify ::update-indexes + ptk/WatchEvent + (watch [_ _ _] + (let [changes (->> (get commit attr) + (map fix-page-id) + (filter :page-id) + (group-by :page-id))] + + (->> (rx/from changes) + (rx/merge-map (fn [[page-id changes]] + (log/debug :hint "update-indexes" :page-id page-id :changes (count changes)) + (uw/ask! {:cmd :update-page-index + :page-id page-id + :changes changes}))) + (rx/catch (fn [cause] + (log/warn :hint "unable to update index" + :cause cause) + (rx/empty))) + (rx/ignore)))))) + +(defn- get-pending-commits + [{:keys [persistence]}] + (->> (:queue persistence) + (map (d/getf (:index persistence))) + (not-empty))) + +(def ^:private xf:map-page-id + (map :page-id)) + +(defn- apply-changes-localy + [{:keys [file-id redo-changes] :as commit} pending] + (ptk/reify ::apply-changes-localy + ptk/UpdateEvent + (update [_ state] + (let [current-file-id (get state :current-file-id) + path (if (= file-id current-file-id) + [:workspace-data] + [:workspace-libraries file-id :data]) + + undo-changes (if pending + (->> pending + (map :undo-changes) + (reverse) + (mapcat identity) + (vec)) + nil) + + redo-changes (if pending + (into redo-changes + (mapcat :redo-changes) + pending) + redo-changes)] + + (d/update-in-when state path + (fn [file] + (let [file (cpc/process-changes file undo-changes false) + file (cpc/process-changes file redo-changes false) + pids (into #{} xf:map-page-id redo-changes)] + (reduce #(ctst/update-object-indices %1 %2) file pids)))))))) + + +(defn commit + "Create a commit event instance" + [{:keys [commit-id redo-changes undo-changes origin save-undo? features + file-id file-revn undo-group tags stack-undo? source]}] + + (dm/assert! + "expect valid vector of changes" + (and (cpc/check-changes! redo-changes) + (cpc/check-changes! undo-changes))) + + (let [commit-id (or commit-id (uuid/next)) + source (d/nilv source :local) + local? (= source :local) + commit {:id commit-id + :created-at (dt/now) + :source source + :origin (ptk/type origin) + :features features + :file-id file-id + :file-revn file-revn + :changes redo-changes + :redo-changes redo-changes + :undo-changes undo-changes + :save-undo? save-undo? + :undo-group undo-group + :tags tags + :stack-undo? stack-undo?}] + + (ptk/reify ::commit + cljs.core/IDeref + (-deref [_] commit) + + ptk/WatchEvent + (watch [_ state _] + (let [pending (when-not local? + (get-pending-commits state))] + (rx/concat + (rx/of (apply-changes-localy commit pending)) + (if pending + (rx/concat + (->> (rx/from (reverse pending)) + (rx/map (fn [commit] (update-indexes commit :undo-changes)))) + (rx/of (update-indexes commit :redo-changes)) + (->> (rx/from pending) + (rx/map (fn [commit] (update-indexes commit :redo-changes))))) + (rx/of (update-indexes commit :redo-changes))))))))) + +(defn- resolve-file-revn + [state file-id] + (let [file (:workspace-file state)] + (if (= (:id file) file-id) + (:revn file) + (dm/get-in state [:workspace-libraries file-id :revn])))) + +(defn commit-changes + "Schedules a list of changes to execute now, and add the corresponding undo changes to + the undo stack. + + Options: + - save-undo?: if set to false, do not add undo changes. + - undo-group: if some consecutive changes (or even transactions) share the same + undo-group, they will be undone or redone in a single step + " + [{:keys [redo-changes undo-changes save-undo? undo-group tags stack-undo? file-id] + :or {save-undo? true + stack-undo? false + undo-group (uuid/next) + tags #{}} + :as params}] + (ptk/reify ::commit-changes + ptk/WatchEvent + (watch [_ state _] + (let [file-id (or file-id (:current-file-id state)) + uchg (vec undo-changes) + rchg (vec redo-changes) + features (features/get-team-enabled-features state)] + + (rx/of (-> params + (assoc :undo-group undo-group) + (assoc :features features) + (assoc :tags tags) + (assoc :stack-undo? stack-undo?) + (assoc :save-undo? save-undo?) + (assoc :file-id file-id) + (assoc :file-revn (resolve-file-revn state file-id)) + (assoc :undo-changes uchg) + (assoc :redo-changes rchg) + (commit))))))) diff --git a/frontend/src/app/main/data/comments.cljs b/frontend/src/app/main/data/comments.cljs index ce8e1bd6d5593f1c51712805a596ab60ae399597..0a441068f1492afb5fe3763a969a58bc67bd576d 100644 --- a/frontend/src/app/main/data/comments.cljs +++ b/frontend/src/app/main/data/comments.cljs @@ -12,6 +12,7 @@ [app.common.schema :as sm] [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] + [app.main.data.events :as ev] [app.main.data.workspace.state-helpers :as wsh] [app.main.repo :as rp] [beicon.v2.core :as rx] @@ -61,13 +62,24 @@ (ptk/reify ::created-thread-on-workspace ptk/UpdateEvent (update [_ state] - (-> state - (update :comment-threads assoc id (dissoc thread :comment)) - (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] assoc id (select-keys thread [:position :frame-id])) - (update :comments-local assoc :open id) - (update :comments-local dissoc :draft) - (update :workspace-drawing dissoc :comment) - (update-in [:comments id] assoc (:id comment) comment))))) + (let [position (select-keys thread [:position :frame-id])] + (-> state + (update :comment-threads assoc id (dissoc thread :comment)) + (update-in [:workspace-data :pages-index page-id :options :comment-threads-position] assoc id position) + (update :comments-local assoc :open id) + (update :comments-local assoc :options nil) + (update :comments-local dissoc :draft) + (update :workspace-drawing dissoc :comment) + (update-in [:comments id] assoc (:id comment) comment)))) + + ptk/WatchEvent + (watch [_ _ _] + (rx/of (ptk/data-event ::ev/event + {::ev/name "create-comment-thread" + ::ev/origin "workspace" + :id id + :content-size (count (:content comment))}))))) + (def ^:private @@ -81,8 +93,7 @@ (defn create-thread-on-workspace [params] - (dm/assert! - (sm/check! schema:create-thread-on-workspace params)) + (dm/assert! (sm/check! schema:create-thread-on-workspace params)) (ptk/reify ::create-thread-on-workspace ptk/WatchEvent @@ -105,13 +116,23 @@ (ptk/reify ::created-thread-on-viewer ptk/UpdateEvent (update [_ state] - (-> state - (update :comment-threads assoc id (dissoc thread :comment)) - (update-in [:viewer :pages page-id :options :comment-threads-position] assoc id (select-keys thread [:position :frame-id])) - (update :comments-local assoc :open id) - (update :comments-local dissoc :draft) - (update :workspace-drawing dissoc :comment) - (update-in [:comments id] assoc (:id comment) comment))))) + (let [position (select-keys thread [:position :frame-id])] + (-> state + (update :comment-threads assoc id (dissoc thread :comment)) + (update-in [:viewer :pages page-id :options :comment-threads-position] assoc id position) + (update :comments-local assoc :open id) + (update :comments-local assoc :options nil) + (update :comments-local dissoc :draft) + (update :workspace-drawing dissoc :comment) + (update-in [:comments id] assoc (:id comment) comment)))) + + ptk/WatchEvent + (watch [_ _ _] + (rx/of (ptk/data-event ::ev/event + {::ev/name "create-comment-thread" + ::ev/origin "viewer" + :id id + :content-size (count (:content comment))}))))) (def ^:private schema:create-thread-on-viewer @@ -191,21 +212,27 @@ "expected valid content" (string? content)) - (letfn [(created [comment state] - (update-in state [:comments (:id thread)] assoc (:id comment) comment))] - (ptk/reify ::create-comment - ptk/WatchEvent - (watch [_ state _] - (let [share-id (-> state :viewer-local :share-id)] - (rx/concat - (->> (rp/cmd! :create-comment {:thread-id (:id thread) :content content :share-id share-id}) - (rx/map #(partial created %)) - (rx/catch (fn [{:keys [type code] :as cause}] - (if (and (= type :restriction) - (= code :max-quote-reached)) - (rx/throw cause) - (rx/throw {:type :comment-error}))))) - (rx/of (refresh-comment-thread thread)))))))) + (ptk/reify ::create-comment + ev/Event + (-data [_] + {:thread-id (:id thread) + :file-id (:file-id thread) + :content-size (count content)}) + + ptk/WatchEvent + (watch [_ state _] + (let [share-id (-> state :viewer-local :share-id) + created (fn [comment state] + (update-in state [:comments (:id thread)] assoc (:id comment) comment))] + (rx/concat + (->> (rp/cmd! :create-comment {:thread-id (:id thread) :content content :share-id share-id}) + (rx/map (fn [comment] (partial created comment))) + (rx/catch (fn [{:keys [type code] :as cause}] + (if (and (= type :restriction) + (= code :max-quote-reached)) + (rx/throw cause) + (rx/throw {:type :comment-error}))))) + (rx/of (refresh-comment-thread thread))))))) (defn update-comment [{:keys [id content thread-id] :as comment}] @@ -214,16 +241,24 @@ (check-comment! comment)) (ptk/reify ::update-comment + ev/Event + (-data [_] + {:thread-id thread-id + :id id + :content-size (count content)}) + ptk/UpdateEvent (update [_ state] - (d/update-in-when state [:comments thread-id id] assoc :content content)) + (-> state + (d/update-in-when [:comments thread-id id] assoc :content content))) ptk/WatchEvent (watch [_ state _] - (let [share-id (-> state :viewer-local :share-id)] + (let [file-id (:current-file-id state) + share-id (-> state :viewer-local :share-id)] (->> (rp/cmd! :update-comment {:id id :content content :share-id share-id}) (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore)))))) + (rx/map #(retrieve-comment-threads file-id))))))) (defn delete-comment-thread-on-workspace [{:keys [id] :as thread}] @@ -241,9 +276,14 @@ ptk/WatchEvent (watch [_ _ _] - (->> (rp/cmd! :delete-comment-thread {:id id}) - (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore))))) + (rx/concat + (->> (rp/cmd! :delete-comment-thread {:id id}) + (rx/catch #(rx/throw {:type :comment-error})) + (rx/ignore)) + (rx/of (ptk/data-event ::ev/event + {::ev/name "delete-comment-thread" + ::ev/origin "workspace" + :id id})))))) (defn delete-comment-thread-on-viewer [{:keys [id] :as thread}] @@ -262,19 +302,29 @@ ptk/WatchEvent (watch [_ state _] (let [share-id (-> state :viewer-local :share-id)] - (->> (rp/cmd! :delete-comment-thread {:id id :share-id share-id}) - (rx/catch #(rx/throw {:type :comment-error})) - (rx/ignore)))))) - + (rx/concat + (->> (rp/cmd! :delete-comment-thread {:id id :share-id share-id}) + (rx/catch #(rx/throw {:type :comment-error})) + (rx/ignore)) + (rx/of (ptk/data-event ::ev/event + {::ev/name "delete-comment-thread" + ::ev/origin "viewer" + :id id}))))))) (defn delete-comment [{:keys [id thread-id] :as comment}] (dm/assert! "expected valid comment" (check-comment! comment)) (ptk/reify ::delete-comment + ev/Event + (-data [_] + {:thread-id thread-id}) + ptk/UpdateEvent (update [_ state] - (d/update-in-when state [:comments thread-id] dissoc id)) + (-> state + (d/update-in-when [:comments thread-id] dissoc id) + (d/update-in-when [:comment-threads thread-id :count-comments] dec))) ptk/WatchEvent (watch [_ state _] @@ -373,10 +423,15 @@ "expected valid comment thread" (check-comment-thread! thread)) (ptk/reify ::open-comment-thread + ev/Event + (-data [_] + {:thread-id id}) + ptk/UpdateEvent (update [_ state] (-> state (update :comments-local assoc :open id) + (update :comments-local assoc :options nil) (update :workspace-drawing dissoc :comment))))) (defn close-thread @@ -385,7 +440,7 @@ ptk/UpdateEvent (update [_ state] (-> state - (update :comments-local dissoc :open :draft) + (update :comments-local dissoc :open :draft :options) (update :workspace-drawing dissoc :comment))))) (defn update-filters @@ -440,6 +495,19 @@ (d/update-in-when [:workspace-drawing :comment] merge data) (d/update-in-when [:comments-local :draft] merge data))))) +(defn toggle-comment-options + [comment] + (ptk/reify ::toggle-comment-options + ptk/UpdateEvent + (update [_ state] + (update-in state [:comments-local :options] #(if (= (:id comment) %) nil (:id comment)))))) + +(defn hide-comment-options + [] + (ptk/reify ::hide-comment-options + ptk/UpdateEvent + (update [_ state] + (update-in state [:comments-local :options] (constantly nil))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Helpers diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index 4bab615e94c7a9c2c7d6f4506a1e146e9fc25291..a9b219f7848bb313b928bb38e0fdadf47a63636b 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -9,10 +9,11 @@ (:require [app.common.types.components-list :as ctkl] [app.config :as cf] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] [app.main.features :as features] [app.main.repo :as rp] + [app.main.store :as st] [app.util.i18n :refer [tr]] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -58,6 +59,10 @@ [] (.reload js/location)) +(defn hide-notifications! + [] + (st/emit! (ntf/hide))) + (defn handle-notification [{:keys [message code level] :as params}] (ptk/reify ::show-notification @@ -67,15 +72,24 @@ :upgrade-version (when (or (not= (:version params) (:full cf/version)) (true? (:force params))) - (rx/of (msg/dialog + (rx/of (ntf/dialog :content (tr "notifications.by-code.upgrade-version") :controls :inline-actions - :notification-type :inline - :type level + :type :inline + :level level :actions [{:label "Refresh" :callback force-reload!}] :tag :notification))) - (rx/of (msg/dialog + :maintenance + (rx/of (ntf/dialog + :content (tr "notifications.by-code.maintenance") + :controls :inline-actions + :type level + :actions [{:label (tr "labels.accept") + :callback hide-notifications!}] + :tag :notification)) + + (rx/of (ntf/dialog :content message :controls :close :type level @@ -141,3 +155,18 @@ :files files :binary? binary?})))))))) +;;;;;;;;;;;;;;;;;;;;;; +;; Team Request +;;;;;;;;;;;;;;;;;;;;;; + +(defn create-team-access-request + [params] + (ptk/reify ::create-team-access-request + ptk/WatchEvent + (watch [_ _ _] + (let [{:keys [on-success on-error] + :or {on-success identity + on-error rx/throw}} (meta params)] + (->> (rp/cmd! :create-team-access-request params) + (rx/tap on-success) + (rx/catch on-error)))))) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index b75fddfc9fe2c7943119c5f24b5d35c35119d614..7a0e3297aee498b935f0cbcecd239a669652b288 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -405,12 +405,13 @@ (dm/assert! (string? name)) (ptk/reify ::create-team ptk/WatchEvent - (watch [_ state _] + (watch [it state _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) - features (features/get-enabled-features state)] - (->> (rp/cmd! :create-team {:name name :features features}) + features (features/get-enabled-features state) + params {:name name :features features}] + (->> (rp/cmd! :create-team (with-meta params (meta it))) (rx/tap on-success) (rx/map team-created) (rx/catch on-error)))))) @@ -421,16 +422,16 @@ [{:keys [name emails role] :as params}] (ptk/reify ::create-team-with-invitations ptk/WatchEvent - (watch [_ state _] + (watch [it state _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) - features (features/get-enabled-features state)] - params {:name name - :emails #{emails} - :role role - :features features} - (->> (rp/cmd! :create-team-with-invitations params) + features (features/get-enabled-features state) + params {:name name + :emails emails + :role role + :features features}] + (->> (rp/cmd! :create-team-with-invitations (with-meta params (meta it))) (rx/tap on-success) (rx/map team-created) (rx/catch on-error)))))) @@ -469,7 +470,11 @@ (rx/map prepare) (rx/mapcat #(rp/cmd! :update-team-photo %)) (rx/tap on-success) - (rx/map du/fetch-teams) + (rx/mapcat (fn [_] + (rx/of (du/fetch-teams) + (ptk/data-event ::ev/event + {::ev/name "update-team-photo" + :team-id team-id})))) (rx/catch on-error)))))) (defn update-team-member-role @@ -484,7 +489,12 @@ (->> (rp/cmd! :update-team-member-role params) (rx/mapcat (fn [_] (rx/of (fetch-team-members team-id) - (du/fetch-teams))))))))) + (du/fetch-teams) + (ptk/data-event ::ev/event + {::ev/name "update-team-member-role" + :team-id team-id + :role role + :member-id member-id}))))))))) (defn delete-team-member [{:keys [member-id] :as params}] @@ -497,7 +507,11 @@ (->> (rp/cmd! :delete-team-member params) (rx/mapcat (fn [_] (rx/of (fetch-team-members team-id) - (du/fetch-teams))))))))) + (du/fetch-teams) + (ptk/data-event ::ev/event + {::ev/name "delete-team-member" + :team-id team-id + :member-id member-id}))))))))) (defn leave-team [{:keys [reassign-to] :as params}] @@ -516,6 +530,11 @@ (assoc :reassign-to reassign-to))] (->> (rp/cmd! :leave-team params) (rx/tap #(tm/schedule on-success)) + (rx/map (fn [_] + (ptk/data-event ::ev/event + {::ev/name "leave-team" + :reassign-to reassign-to + :team-id team-id}))) (rx/catch on-error)))))) (defn invite-team-members @@ -528,16 +547,19 @@ (sm/check-set-of-emails! emails)) (ptk/reify ::invite-team-members - IDeref - (-deref [_] {:role role :team-id team-id :resend? resend?}) + ev/Event + (-data [_] + {:role role + :team-id team-id + :resend resend?}) ptk/WatchEvent - (watch [_ _ _] + (watch [it _ _] (let [{:keys [on-success on-error] :or {on-success identity on-error rx/throw}} (meta params) params (dissoc params :resend?)] - (->> (rp/cmd! :create-team-invitations params) + (->> (rp/cmd! :create-team-invitations (with-meta params (meta it))) (rx/tap on-success) (rx/catch on-error)))))) @@ -727,6 +749,11 @@ [{:keys [id name] :as params}] (dm/assert! (uuid? id)) (ptk/reify ::duplicate-project + ev/Event + (-data [_] + {:project-id id + :name name}) + ptk/WatchEvent (watch [_ _ _] (let [{:keys [on-success on-error] @@ -744,10 +771,12 @@ [{:keys [id team-id] :as params}] (dm/assert! (uuid? id)) (dm/assert! (uuid? team-id)) + (ptk/reify ::move-project - IDeref - (-deref [_] - {:id id :team-id team-id}) + ev/Event + (-data [_] + {:id id + :team-id team-id}) ptk/WatchEvent (watch [_ _ _] @@ -834,9 +863,11 @@ (defn rename-file [{:keys [id name] :as params}] (ptk/reify ::rename-file - IDeref - (-deref [_] - {::ev/origin "dashboard" :id id :name name}) + ev/Event + (-data [_] + {::ev/origin "dashboard" + :id id + :name name}) ptk/UpdateEvent (update [_ state] @@ -856,17 +887,18 @@ (defn set-file-shared [{:keys [id is-shared] :as params}] (ptk/reify ::set-file-shared - IDeref - (-deref [_] - {::ev/origin "dashboard" :id id :shared is-shared}) + ev/Event + (-data [_] + {::ev/origin "dashboard" + :id id + :shared is-shared}) ptk/UpdateEvent (update [_ state] (-> state (d/update-in-when [:dashboard-files id :is-shared] (constantly is-shared)) (d/update-in-when [:dashboard-recent-files id :is-shared] (constantly is-shared)) - (cond-> - (not is-shared) + (cond-> (not is-shared) (d/update-when :dashboard-shared-files dissoc id)))) ptk/WatchEvent @@ -876,7 +908,7 @@ (rx/ignore)))))) (defn set-file-thumbnail - [file-id thumbnail-uri] + [file-id thumbnail-id] (ptk/reify ::set-file-thumbnail ptk/UpdateEvent (update [_ state] @@ -884,10 +916,10 @@ (->> files (mapv #(cond-> % (= file-id (:id %)) - (assoc :thumbnail-uri thumbnail-uri)))))] + (assoc :thumbnail-id thumbnail-id)))))] (-> state - (d/update-in-when [:dashboard-files file-id] assoc :thumbnail-uri thumbnail-uri) - (d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-uri thumbnail-uri) + (d/update-in-when [:dashboard-files file-id] assoc :thumbnail-id thumbnail-id) + (d/update-in-when [:dashboard-recent-files file-id] assoc :thumbnail-id thumbnail-id) (d/update-when :dashboard-search-result update-search-files)))))) ;; --- EVENT: create-file @@ -912,9 +944,8 @@ [{:keys [project-id] :as params}] (dm/assert! (uuid? project-id)) (ptk/reify ::create-file - - IDeref - (-deref [_] {:project-id project-id}) + ev/Event + (-data [_] {:project-id project-id}) ptk/WatchEvent (watch [it state _] @@ -967,8 +998,8 @@ (sm/check-set-of-uuid! ids)) (ptk/reify ::move-files - IDeref - (-deref [_] + ev/Event + (-data [_] {:num-files (count ids) :project-id project-id}) @@ -998,8 +1029,8 @@ [{:keys [template-id project-id] :as params}] (dm/assert! (uuid? project-id)) (ptk/reify ::clone-template - IDeref - (-deref [_] + ev/Event + (-data [_] {:template-id template-id :project-id project-id}) diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/events.cljs index a1460eedd071a4dc3a8eab17a4229417330122f9..1e0cc623f3d18eeab31dafe678ed6323cc7922c9 100644 --- a/frontend/src/app/main/data/events.cljs +++ b/frontend/src/app/main/data/events.cljs @@ -75,73 +75,23 @@ ;; --- EVENT TRANSLATION -(derive :app.main.data.comments/create-comment ::generic-action) -(derive :app.main.data.comments/create-comment-thread ::generic-action) -(derive :app.main.data.comments/delete-comment ::generic-action) -(derive :app.main.data.comments/delete-comment-thread ::generic-action) -(derive :app.main.data.comments/open-comment-thread ::generic-action) -(derive :app.main.data.comments/update-comment ::generic-action) -(derive :app.main.data.comments/update-comment-thread ::generic-action) -(derive :app.main.data.comments/update-comment-thread-status ::generic-action) -(derive :app.main.data.dashboard/delete-team-member ::generic-action) -(derive :app.main.data.dashboard/duplicate-project ::generic-action) -(derive :app.main.data.dashboard/create-file ::generic-action) -(derive :app.main.data.dashboard/file-created ::generic-action) -(derive :app.main.data.dashboard/invite-team-members ::generic-action) -(derive :app.main.data.dashboard/leave-team ::generic-action) -(derive :app.main.data.dashboard/move-files ::generic-action) -(derive :app.main.data.dashboard/move-project ::generic-action) -(derive :app.main.data.dashboard/project-created ::generic-action) -(derive :app.main.data.dashboard/rename-file ::generic-action) -(derive :app.main.data.dashboard/set-file-shared ::generic-action) -(derive :app.main.data.dashboard/update-team-member-role ::generic-action) -(derive :app.main.data.dashboard/update-team-photo ::generic-action) -(derive :app.main.data.dashboard/clone-template ::generic-action) -(derive :app.main.data.fonts/add-font ::generic-action) -(derive :app.main.data.fonts/delete-font ::generic-action) -(derive :app.main.data.fonts/delete-font-variant ::generic-action) -(derive :app.main.data.modal/show-modal ::generic-action) -(derive :app.main.data.users/logout ::generic-action) -(derive :app.main.data.users/request-email-change ::generic-action) -(derive :app.main.data.users/update-password ::generic-action) -(derive :app.main.data.users/update-photo ::generic-action) -(derive :app.main.data.workspace.comments/open-comment-thread ::generic-action) -(derive :app.main.data.workspace.guides/update-guides ::generic-action) -(derive :app.main.data.workspace.libraries/add-color ::generic-action) -(derive :app.main.data.workspace.libraries/add-media ::generic-action) -(derive :app.main.data.workspace.libraries/add-typography ::generic-action) -(derive :app.main.data.workspace.libraries/delete-color ::generic-action) -(derive :app.main.data.workspace.libraries/delete-media ::generic-action) -(derive :app.main.data.workspace.libraries/delete-typography ::generic-action) -(derive :app.main.data.workspace.persistence/attach-library ::generic-action) -(derive :app.main.data.workspace.persistence/detach-library ::generic-action) -(derive :app.main.data.workspace.persistence/set-file-shard ::generic-action) -(derive :app.main.data.workspace.selection/toggle-focus-mode ::generic-action) -(derive :app.main.data.workspace/create-page ::generic-action) -(derive :app.main.data.workspace/set-workspace-layout ::generic-action) -(derive :app.main.data.workspace/toggle-layout-flag ::generic-action) - (defprotocol Event (-data [_] "Get event data")) (defn- simplify-props "Removes complex data types from props." [data] - (into {} - (comp - (remove (fn [[_ v]] (nil? v))) - (map (fn [[k v :as kv]] - (cond - (map? v) [k :placeholder/map] - (vector? v) [k :placeholder/vec] - (set? v) [k :placeholder/set] - (coll? v) [k :placeholder/coll] - (fn? v) [k :placeholder/fn] - :else kv)))) - data)) - - -(defmulti process-event-by-type ptk/type) + (reduce-kv (fn [data k v] + (cond + (map? v) (assoc data k :placeholder/map) + (vector? v) (assoc data k :placeholder/vec) + (set? v) (assoc data k :placeholder/set) + (coll? v) (assoc data k :placeholder/coll) + (fn? v) (assoc data k :placeholder/fn) + (nil? v) (dissoc data k) + :else data)) + data + data)) (defn- process-event-by-proto [event] @@ -160,72 +110,30 @@ :context context :props props})) -(defn- process-event +(defn- process-data-event [event] - (if (satisfies? Event event) - (process-event-by-proto event) - (process-event-by-type event))) - -(defmethod process-event-by-type :default [_] nil) + (let [data (deref event) + name (::name data)] + + (when (string? name) + (let [type (::type data "action") + context (-> (::context data) + (assoc :event-origin (::origin data)) + (d/without-nils)) + props (-> data d/without-qualified simplify-props)] + {:type type + :name name + :context context + :props props})))) -(defmethod process-event-by-type ::event - [event] - (let [data (deref event) - context (-> (::context data) - (assoc :event-origin (::origin data)) - (d/without-nils)) - props (-> data d/without-qualified simplify-props)] - - {:type (::type data "action") - :name (::name data "unnamed") - :context context - :props props})) - -(defmethod process-event-by-type ::generic-action - [event] - (let [type (ptk/type event) - data (if (satisfies? IDeref event) - (deref event) - {}) - data (d/deep-merge data (meta event))] - - {:type "action" - :name (or (::name data) (name type)) - :props (-> (d/without-qualified data) - (simplify-props)) - :context (d/without-nils - {:event-origin (::origin data) - :event-namespace (namespace type) - :event-symbol (name type)})})) - -(defmethod process-event-by-type :app.util.router/navigated - [event] - (let [match (deref event) - route (get-in match [:data :name]) - props {:route (name route) - :team-id (get-in match [:path-params :team-id]) - :file-id (get-in match [:path-params :file-id]) - :project-id (get-in match [:path-params :project-id])}] - {:name "navigate" - :type "action" - :props (simplify-props props)})) - -(defmethod process-event-by-type :app.main.data.users/logged-in +(defn- process-event [event] - (let [data (deref event) - mdata (meta data) - props {:signin-source (::source mdata) - :email (:email data) - :auth-backend (:auth-backend data) - :fullname (:fullname data) - :is-muted (:is-muted data) - :default-team-id (str (:default-team-id data)) - :default-project-id (str (:default-project-id data))}] + (cond + (satisfies? Event event) + (process-event-by-proto event) - {:name "signin" - :type "identify" - :profile-id (:id data) - :props (simplify-props props)})) + (ptk/data-event? event) + (process-data-event event))) ;; --- MAIN LOOP @@ -260,7 +168,7 @@ ptk/EffectEvent (effect [_ _ stream] (let [session (atom nil) - stopper (rx/filter (ptk/type? ::initialize) stream) + stopper (rx/filter (ptk/type? ::initialize) stream) buffer (atom #queue []) profile (->> (rx/from-atom storage {:emit-current-value? true}) (rx/map :profile) @@ -305,7 +213,9 @@ (let [session* (or @session (dt/now)) context (-> @context (merge (:context event)) - (assoc :session session*))] + (assoc :session session*) + (assoc :external-session-id (cf/external-session-id)) + (d/without-nils))] (reset! session session*) (-> event (assoc :timestamp (dt/now)) diff --git a/frontend/src/app/main/data/exports.cljs b/frontend/src/app/main/data/exports.cljs index 9894691a2443c0166e03096e1566fd0f216fa204..ebea22149abe8bab3a33a8c836fc3ada89ba75d6 100644 --- a/frontend/src/app/main/data/exports.cljs +++ b/frontend/src/app/main/data/exports.cljs @@ -8,7 +8,7 @@ (:require [app.common.uuid :as uuid] [app.main.data.modal :as modal] - [app.main.data.workspace.persistence :as dwp] + [app.main.data.persistence :as dwp] [app.main.data.workspace.state-helpers :as wsh] [app.main.refs :as refs] [app.main.repo :as rp] @@ -49,31 +49,30 @@ (defn show-workspace-export-dialog - ([] (show-workspace-export-dialog nil)) - ([{:keys [selected]}] - (ptk/reify ::show-workspace-export-dialog - ptk/WatchEvent - (watch [_ state _] - (let [file-id (:current-file-id state) - page-id (:current-page-id state) - selected (or selected (wsh/lookup-selected state page-id {})) - - shapes (if (seq selected) - (wsh/lookup-shapes state selected) - (reverse (wsh/filter-shapes state #(pos? (count (:exports %)))))) - - exports (for [shape shapes - export (:exports shape)] - (-> export - (assoc :enabled true) - (assoc :page-id page-id) - (assoc :file-id file-id) - (assoc :object-id (:id shape)) - (assoc :shape (dissoc shape :exports)) - (assoc :name (:name shape))))] - - (rx/of (modal/show :export-shapes - {:exports (vec exports)}))))))) + [{:keys [selected origin]}] + (ptk/reify ::show-workspace-export-dialog + ptk/WatchEvent + (watch [_ state _] + (let [file-id (:current-file-id state) + page-id (:current-page-id state) + selected (or selected (wsh/lookup-selected state page-id {})) + + shapes (if (seq selected) + (wsh/lookup-shapes state selected) + (reverse (wsh/filter-shapes state #(pos? (count (:exports %)))))) + + exports (for [shape shapes + export (:exports shape)] + (-> export + (assoc :enabled true) + (assoc :page-id page-id) + (assoc :file-id file-id) + (assoc :object-id (:id shape)) + (assoc :shape (dissoc shape :exports)) + (assoc :name (:name shape))))] + + (rx/of (modal/show :export-shapes + {:exports (vec exports) :origin origin})))))) (defn show-viewer-export-dialog [{:keys [shapes page-id file-id share-id exports]}] @@ -90,7 +89,7 @@ (assoc :shape (dissoc shape :exports)) (assoc :name (:name shape)) (cond-> share-id (assoc :share-id share-id))))] - (rx/of (modal/show :export-shapes {:exports (vec exports)})))))) #_TODO + (rx/of (modal/show :export-shapes {:exports (vec exports) :origin "viewer"})))))) #_TODO (defn show-workspace-export-frames-dialog [frames] @@ -108,7 +107,7 @@ :name (:name frame)})] (rx/of (modal/show :export-frames - {:exports (vec exports)})))))) + {:exports (vec exports) :origin "workspace:menu"})))))) (defn- initialize-export-status [exports cmd resource] diff --git a/frontend/src/app/main/data/fonts.cljs b/frontend/src/app/main/data/fonts.cljs index bb8fe193fb79eb223521caa74847d32d13b304e1..6274b33548331d62defa90d984d330d74dc23d81 100644 --- a/frontend/src/app/main/data/fonts.cljs +++ b/frontend/src/app/main/data/fonts.cljs @@ -12,7 +12,8 @@ [app.common.logging :as log] [app.common.media :as cm] [app.common.uuid :as uuid] - [app.main.data.messages :as msg] + [app.main.data.events :as ev] + [app.main.data.notifications :as ntf] [app.main.fonts :as fonts] [app.main.repo :as rp] [app.main.store :as st] @@ -182,7 +183,7 @@ #(when (not-empty %) (st/emit! - (msg/error + (ntf/error (if (> (count %) 1) (tr "errors.bad-font-plural" (str/join ", " %)) (tr "errors.bad-font" (first %))))))) @@ -236,12 +237,19 @@ (defn add-font [font] (ptk/reify ::add-font - IDeref - (-deref [_] (select-keys font [:font-family :font-style :font-weight])) - ptk/UpdateEvent (update [_ state] - (update state :dashboard-fonts assoc (:id font) font)))) + (update state :dashboard-fonts assoc (:id font) font)) + + ptk/WatchEvent + (watch [_ state _] + (let [team-id (:current-team-id state)] + (rx/of (ptk/data-event ::ev/event {::ev/name "add-font" + :team-id team-id + :font-id (:id font) + :font-family (:font-family font) + :font-style (:font-style font) + :font-weight (:font-weight font)})))))) (defn update-font [{:keys [id name] :as params}] @@ -271,6 +279,10 @@ [font-id] (dm/assert! (uuid? font-id)) (ptk/reify ::delete-font + ev/Event + (-data [_] + {:id font-id}) + ptk/UpdateEvent (update [_ state] (update state :dashboard-fonts @@ -280,8 +292,12 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/cmd! :delete-font {:id font-id :team-id team-id}) - (rx/ignore)))))) + (rx/concat + (->> (rp/cmd! :delete-font {:id font-id :team-id team-id}) + (rx/ignore)) + (rx/of (ptk/data-event ::ev/event {::ev/name "delete-font" + :team-id team-id + :font-id font-id}))))))) (defn delete-font-variant [id] @@ -297,8 +313,13 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (->> (rp/cmd! :delete-font-variant {:id id :team-id team-id}) - (rx/ignore)))))) + (rx/concat + (->> (rp/cmd! :delete-font-variant {:id id :team-id team-id}) + (rx/ignore)) + (rx/of (ptk/data-event ::ev/event {::ev/name "delete-font-variant" + :id id + :team-id team-id}))))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Workspace related events diff --git a/frontend/src/app/main/data/media.cljs b/frontend/src/app/main/data/media.cljs index e78892bb1b1ce87a22087145f95e3737fb8f189a..904623505c36e3fc3477c0dabfb05ddc50a03158 100644 --- a/frontend/src/app/main/data/media.cljs +++ b/frontend/src/app/main/data/media.cljs @@ -8,7 +8,7 @@ (:require [app.common.exceptions :as ex] [app.common.media :as cm] - [app.main.data.messages :as msg] + [app.main.data.notifications :as ntf] [app.main.store :as st] [app.util.i18n :refer [tr]] [beicon.v2.core :as rx] @@ -46,14 +46,14 @@ (defn notify-start-loading [] - (st/emit! (msg/show {:content (tr "media.loading") - :notification-type :toast - :type :info + (st/emit! (ntf/show {:content (tr "media.loading") + :type :toast + :level :info :timeout nil}))) (defn notify-finished-loading [] - (st/emit! msg/hide)) + (st/emit! (ntf/hide))) (defn process-error [error] @@ -69,4 +69,4 @@ :else (tr "errors.unexpected-error"))] - (rx/of (msg/error msg)))) + (rx/of (ntf/error msg)))) diff --git a/frontend/src/app/main/data/modal.cljs b/frontend/src/app/main/data/modal.cljs index 5b5b79524a2100af6acc8f0bfa69ef7b728bab89..1055014c2850a95de12427b1dae12c5ba8ed3dea 100644 --- a/frontend/src/app/main/data/modal.cljs +++ b/frontend/src/app/main/data/modal.cljs @@ -8,6 +8,7 @@ (:refer-clojure :exclude [update]) (:require [app.common.uuid :as uuid] + [app.main.data.events :as ev] [app.main.store :as st] [cljs.core :as c] [potok.v2.core :as ptk])) @@ -23,9 +24,11 @@ (show (uuid/next) type props)) ([id type props] (ptk/reify ::show-modal - IDeref - (-deref [_] - (merge (dissoc props :type) {:name type})) + ev/Event + (-data [_] + (-> props + (dissoc :type) + (assoc :name type))) ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/messages.cljs b/frontend/src/app/main/data/notifications.cljs similarity index 44% rename from frontend/src/app/main/data/messages.cljs rename to frontend/src/app/main/data/notifications.cljs index 024fec415a8a3b28306cfd9a84c2bbf97bbc6aac..c58fb4c60337858e2992769b8b40c6fadc2c93a1 100644 --- a/frontend/src/app/main/data/messages.cljs +++ b/frontend/src/app/main/data/notifications.cljs @@ -4,7 +4,7 @@ ;; ;; Copyright (c) KALEIDOS INC -(ns app.main.data.messages +(ns app.main.data.notifications (:require [app.common.data :as d] [app.common.data.macros :as dm] @@ -15,48 +15,49 @@ (declare hide) (declare show) -(def default-animation-timeout 600) (def default-timeout 7000) -(def ^:private - schema:message - (sm/define - [:map {:title "Message"} - [:type [::sm/one-of #{:success :error :info :warning}]] - [:status {:optional true} - [::sm/one-of #{:visible :hide}]] - [:position {:optional true} - [::sm/one-of #{:fixed :floating :inline}]] - [:notification-type {:optional true} - [::sm/one-of #{:inline :context :toast}]] - [:controls {:optional true} - [::sm/one-of #{:none :close :inline-actions :bottom-actions}]] - [:tag {:optional true} - [:or :string :keyword]] - [:timeout {:optional true} - [:maybe :int]] - [:actions {:optional true} - [:vector - [:map - [:label :string] - [:callback ::sm/fn]]]] - [:links {:optional true} - [:vector - [:map - [:label :string] - [:callback ::sm/fn]]]]])) +(def ^:private schema:notification + [:map {:title "Notification"} + [:level [::sm/one-of #{:success :error :info :warning}]] + [:status {:optional true} + [::sm/one-of #{:visible :hide}]] + [:position {:optional true} + [::sm/one-of #{:fixed :floating :inline}]] + [:type {:optional true} + [::sm/one-of #{:inline :context :toast}]] + [:controls {:optional true} + [::sm/one-of #{:none :close :inline-actions :bottom-actions}]] + [:tag {:optional true} + [:or :string :keyword]] + [:timeout {:optional true} + [:maybe :int]] + [:actions {:optional true} + [:vector + [:map + [:label :string] + [:callback ::sm/fn]]]] + [:links {:optional true} + [:vector + [:map + [:label :string] + [:callback ::sm/fn]]]]]) + +(def ^:private valid-notification? + (sm/validator schema:notification)) (defn show [data] + (dm/assert! - "expected valid message map" - (sm/check! schema:message data)) + "expected valid notification map" + (valid-notification? data)) (ptk/reify ::show ptk/UpdateEvent (update [_ state] - (let [message (assoc data :status :visible)] - (assoc state :message message))) + (let [notification (assoc data :status :visible)] + (assoc state :notification notification))) ptk/WatchEvent (watch [_ _ stream] @@ -64,49 +65,39 @@ (let [stopper (rx/filter (ptk/type? ::hide) stream)] (->> stream (rx/filter (ptk/type? :app.util.router/navigate)) - (rx/map (constantly hide)) + (rx/map (fn [_] (hide))) (rx/take-until stopper))) (when (:timeout data) (let [stopper (rx/filter (ptk/type? ::show) stream)] - (->> (rx/of hide) + (->> (rx/of (hide)) (rx/delay (:timeout data)) (rx/take-until stopper)))))))) -(def hide +(defn hide + [& {:keys [tag]}] (ptk/reify ::hide ptk/UpdateEvent (update [_ state] - (d/update-when state :message assoc :status :hide)) - - ptk/WatchEvent - (watch [_ _ stream] - (let [stopper (rx/filter (ptk/type? ::show) stream)] - (->> (rx/of #(dissoc % :message)) - (rx/delay default-animation-timeout) - (rx/take-until stopper)))))) - -(defn hide-tag - [tag] - (ptk/reify ::hide-tag - ptk/WatchEvent - (watch [_ state _] - (let [message (get state :message)] - (when (= (:tag message) tag) - (rx/of hide)))))) + (if (some? tag) + (let [notification (get state :notification)] + (if (= tag (:tag notification)) + (dissoc state :notification) + state)) + (dissoc state :notification))))) (defn error ([content] (show {:content content - :type :error - :notification-type :toast + :level :error + :type :toast :position :fixed}))) (defn info ([content] (info content {})) ([content {:keys [timeout] :or {timeout default-timeout}}] (show {:content content - :type :info - :notification-type :toast + :level :info + :type :toast :position :fixed :timeout timeout}))) @@ -114,8 +105,8 @@ ([content] (success content {})) ([content {:keys [timeout] :or {timeout default-timeout}}] (show {:content content - :type :success - :notification-type :toast + :level :success + :type :toast :position :fixed :timeout timeout}))) @@ -123,31 +114,19 @@ ([content] (warn content {})) ([content {:keys [timeout] :or {timeout default-timeout}}] (show {:content content - :type :warning - :notification-type :toast + :level :warning + :type :toast :position :fixed :timeout timeout}))) (defn dialog - [& {:keys [content controls actions position tag type] - :or {controls :none position :floating type :info}}] + [& {:keys [content controls actions position tag level links] + :or {controls :none position :floating level :info}}] (show (d/without-nils {:content content - :type type + :level level + :links links :position position :controls controls :actions actions :tag tag}))) - -(defn info-dialog - [& {:keys [content controls links actions tag] - :or {controls :none links nil tag nil}}] - (show (d/without-nils - {:content content - :type :info - :position :floating - :notification-type :inline - :controls controls - :links links - :actions actions - :tag tag}))) diff --git a/frontend/src/app/main/data/persistence.cljs b/frontend/src/app/main/data/persistence.cljs new file mode 100644 index 0000000000000000000000000000000000000000..9a17eda3fc36d0f2d779b9f39ab52898632f459c --- /dev/null +++ b/frontend/src/app/main/data/persistence.cljs @@ -0,0 +1,231 @@ +;; 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 app.main.data.persistence + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.logging :as log] + [app.common.uuid :as uuid] + [app.main.data.changes :as dch] + [app.main.repo :as rp] + [beicon.v2.core :as rx] + [potok.v2.core :as ptk])) + +(declare ^:private run-persistence-task) + +(log/set-level! :warn) + +(def running (atom false)) +(def revn-data (atom {})) +(def queue-conj (fnil conj #queue [])) + +(defn- update-status + [status] + (ptk/reify ::update-status + ptk/UpdateEvent + (update [_ state] + (update state :persistence (fn [pstate] + (log/trc :hint "update-status" + :from (:status pstate) + :to status) + (let [status (if (and (= status :pending) + (= (:status pstate) :saving)) + (:status pstate) + status)] + + (-> (assoc pstate :status status) + (cond-> (= status :error) + (dissoc :run-id)) + (cond-> (= status :saved) + (dissoc :run-id))))))))) + +(defn- update-file-revn + [file-id revn] + (ptk/reify ::update-file-revn + ptk/UpdateEvent + (update [_ state] + (log/dbg :hint "update-file-revn" :file-id (dm/str file-id) :revn revn) + (if-let [current-file-id (:current-file-id state)] + (if (= file-id current-file-id) + (update-in state [:workspace-file :revn] max revn) + (d/update-in-when state [:workspace-libraries file-id :revn] max revn)) + state)) + + ptk/EffectEvent + (effect [_ _ _] + (swap! revn-data update file-id (fnil max 0) revn)))) + +(defn- discard-commit + [commit-id] + (ptk/reify ::discard-commit + ptk/UpdateEvent + (update [_ state] + (update state :persistence (fn [pstate] + (-> pstate + (update :queue (fn [queue] + (if (= commit-id (peek queue)) + (pop queue) + (throw (ex-info "invalid state" {}))))) + (update :index dissoc commit-id))))))) + +(defn- append-commit + "Event used internally to append the current change to the + persistence queue." + [{:keys [id] :as commit}] + (let [run-id (uuid/next)] + (ptk/reify ::append-commit + ptk/UpdateEvent + (update [_ state] + (log/trc :hint "append-commit" :method "update" :commit-id (dm/str id)) + (update state :persistence + (fn [pstate] + (-> pstate + (update :run-id d/nilv run-id) + (update :queue queue-conj id) + (update :index assoc id commit))))) + + ptk/WatchEvent + (watch [_ state _] + (let [pstate (:persistence state)] + (when (= run-id (:run-id pstate)) + (rx/of (run-persistence-task) + (update-status :saving)))))))) + +(defn- discard-persistence-state + [] + (ptk/reify ::discard-persistence-state + ptk/UpdateEvent + (update [_ state] + (dissoc state :persistence)))) + +(defn- persist-commit + [commit-id] + (ptk/reify ::persist-commit + ptk/WatchEvent + (watch [_ state _] + (log/dbg :hint "persist-commit" :commit-id (dm/str commit-id)) + (when-let [{:keys [file-id file-revn changes features] :as commit} (dm/get-in state [:persistence :index commit-id])] + (let [sid (:session-id state) + revn (max file-revn (get @revn-data file-id 0)) + params {:id file-id + :revn revn + :session-id sid + :origin (:origin commit) + :created-at (:created-at commit) + :commit-id commit-id + :changes (vec changes) + :features features}] + + (->> (rp/cmd! :update-file params) + (rx/mapcat (fn [{:keys [revn lagged] :as response}] + (log/debug :hint "changes persisted" :commit-id (dm/str commit-id) :lagged (count lagged)) + (rx/of (ptk/data-event ::commit-persisted commit) + (update-file-revn file-id revn)))) + + (rx/catch (fn [cause] + (rx/concat + (if (= :authentication (:type cause)) + (rx/empty) + (rx/of (ptk/data-event ::error cause) + (update-status :error))) + (rx/of (discard-persistence-state)) + (rx/throw cause)))))))))) + + +(defn- run-persistence-task + [] + (ptk/reify ::run-persistence-task + ptk/WatchEvent + (watch [_ state stream] + (let [queue (-> state :persistence :queue)] + (if-let [commit-id (peek queue)] + (let [stoper-s (rx/merge + (rx/filter (ptk/type? ::run-persistence-task) stream) + (rx/filter (ptk/type? ::error) stream))] + + (log/dbg :hint "run-persistence-task" :commit-id (dm/str commit-id)) + (->> (rx/merge + (rx/of (persist-commit commit-id)) + (->> stream + (rx/filter (ptk/type? ::commit-persisted)) + (rx/map deref) + (rx/filter #(= commit-id (:id %))) + (rx/take 1) + (rx/mapcat (fn [_] + (rx/of (discard-commit commit-id) + (run-persistence-task)))))) + (rx/take-until stoper-s))) + (rx/of (update-status :saved))))))) + +(def ^:private xf-mapcat-undo + (mapcat :undo-changes)) + +(def ^:private xf-mapcat-redo + (mapcat :redo-changes)) + +(defn- merge-commit + [buffer] + (->> (rx/from (group-by :file-id buffer)) + (rx/map (fn [[_ [item :as commits]]] + (let [uchg (into [] xf-mapcat-undo commits) + rchg (into [] xf-mapcat-redo commits)] + (-> item + (assoc :undo-changes uchg) + (assoc :redo-changes rchg) + (assoc :changes rchg))))))) + +(defn initialize-persistence + [] + (ptk/reify ::initialize-persistence + ptk/WatchEvent + (watch [_ _ stream] + (log/debug :hint "initialize persistence") + (let [stoper-s (rx/filter (ptk/type? ::initialize-persistence) stream) + + local-commits-s + (->> stream + (rx/filter dch/commit?) + (rx/map deref) + (rx/filter #(= :local (:source %))) + (rx/filter (complement empty?)) + (rx/share)) + + notifier-s + (rx/merge + (->> local-commits-s + (rx/debounce 3000) + (rx/tap #(log/trc :hint "persistence beat"))) + (->> stream + (rx/filter #(= % ::force-persist))))] + + (rx/merge + (->> local-commits-s + (rx/debounce 200) + (rx/map (fn [_] + (update-status :pending))) + (rx/take-until stoper-s)) + + ;; Here we watch for local commits, buffer them in a small + ;; chunks (very near in time commits) and append them to the + ;; persistence queue + (->> local-commits-s + (rx/buffer-until notifier-s) + (rx/mapcat merge-commit) + (rx/map append-commit) + (rx/take-until (rx/delay 100 stoper-s)) + (rx/finalize (fn [] + (log/debug :hint "finalize persistence: changes watcher")))) + + ;; Here we track all incoming remote commits for maintain + ;; updated the local state with the file revn + (->> stream + (rx/filter dch/commit?) + (rx/map deref) + (rx/filter #(= :remote (:source %))) + (rx/mapcat (fn [{:keys [file-id file-revn] :as commit}] + (rx/of (update-file-revn file-id file-revn)))) + (rx/take-until stoper-s))))))) diff --git a/frontend/src/app/main/data/preview.cljs b/frontend/src/app/main/data/preview.cljs index 519ed96d3a82c826a69031383b5c9b14021989f9..e5865e19a7032e40776bdaa863c718b334bf8699 100644 --- a/frontend/src/app/main/data/preview.cljs +++ b/frontend/src/app/main/data/preview.cljs @@ -76,7 +76,7 @@ (let [style-code (dm/str fontfaces-css "\n" - (-> (cg/generate-style-code objects style-type all-children) + (-> (cg/generate-style-code objects style-type [shape] all-children) (cb/format-code style-type))) markup-code @@ -98,7 +98,8 @@ closed-preview (rx/subject) preview (.open js/window "/#/frame-preview") listener-fn #(rx/push! closed-preview true)] - (.addEventListener preview "beforeunload" listener-fn) + (when (some? preview) + (.addEventListener preview "beforeunload" listener-fn)) (->> (rx/from-atom (refs/all-children-objects shape-id) {:emit-current-value? true}) (rx/take-until closed-preview) (rx/debounce 1000) diff --git a/frontend/src/app/main/data/shortcuts_impl.js b/frontend/src/app/main/data/shortcuts_impl.js index 9be5beb05945a11d9447e5ef74e3530a8488ad8d..e381cc150b6ce5f26e00a77421d8fea490966e31 100644 --- a/frontend/src/app/main/data/shortcuts_impl.js +++ b/frontend/src/app/main/data/shortcuts_impl.js @@ -16,7 +16,7 @@ if (Mousetrap.addKeycodes) { } const target = Mousetrap.prototype || Mousetrap; -target.stopCallback = function(e, element, combo) { +target.stopCallback = function (e, element, combo) { // if the element has the data attribute "mousetrap-dont-stop" then no need // to stop. It should be used like
...
// or :div {:data-mousetrap-dont-stop true} @@ -24,6 +24,14 @@ target.stopCallback = function(e, element, combo) { return false } + if ('composedPath' in e && typeof e.composedPath === 'function') { + // For open shadow trees, update `element` so that the following check works. + const initialEventTarget = e.composedPath()[0]; + if (initialEventTarget !== e.target) { + element = initialEventTarget; + } + } + // stop for input, select, textarea and button const shouldStop = element.tagName == "INPUT" || element.tagName == "SELECT" || diff --git a/frontend/src/app/main/data/users.cljs b/frontend/src/app/main/data/users.cljs index 64142d3272e3350859dfb2880d5aafb65e608a32..b3c4e8f05aab291b679691a1ccf9aa01ef7169af 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -15,15 +15,19 @@ [app.config :as cf] [app.main.data.events :as ev] [app.main.data.media :as di] + [app.main.data.notifications :as ntf] [app.main.data.websocket :as ws] [app.main.features :as features] [app.main.repo :as rp] - [app.util.i18n :as i18n] + [app.plugins.register :as register] + [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] - [app.util.storage :refer [storage]] + [app.util.storage :as s] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) +(declare update-profile-props) + ;; --- SCHEMAS (def ^:private @@ -48,14 +52,14 @@ (defn get-current-team-id [profile] - (let [team-id (::current-team-id @storage)] + (let [team-id (::current-team-id @s/storage)] (or team-id (:default-team-id profile)))) (defn set-current-team! [team-id] (if (nil? team-id) - (swap! storage dissoc ::current-team-id) - (swap! storage assoc ::current-team-id team-id))) + (swap! s/storage dissoc ::current-team-id) + (swap! s/storage assoc ::current-team-id team-id))) ;; --- EVENT: fetch-teams @@ -75,9 +79,9 @@ ;; if not, dissoc it from storage. (let [ids (into #{} (map :id) teams)] - (when-let [ctid (::current-team-id @storage)] + (when-let [ctid (::current-team-id @s/storage)] (when-not (contains? ids ctid) - (swap! storage dissoc ::current-team-id))))))) + (swap! s/storage dissoc ::current-team-id))))))) (defn fetch-teams [] @@ -128,13 +132,25 @@ (effect [_ state _] (let [profile (:profile state) email (:email profile) - previous-profile (:profile @storage) + previous-profile (:profile @s/storage) previous-email (:email previous-profile)] (when profile - (swap! storage assoc :profile profile) + (swap! s/storage assoc :profile profile) (i18n/set-locale! (:lang profile)) (when (not= previous-email email) - (set-current-team! nil))))))) + (set-current-team! nil)) + + (register/init)))))) + +(defn- on-fetch-profile-exception + [cause] + (let [data (ex-data cause)] + (if (and (= :authorization (:type data)) + (= :challenge-required (:code data))) + (let [path (rt/get-current-path) + href (str "/challenge.html?redirect=" path)] + (rx/of (rt/nav-raw href))) + (rx/throw cause)))) (defn fetch-profile [] @@ -142,7 +158,8 @@ ptk/WatchEvent (watch [_ _ _] (->> (rp/cmd! :get-profile) - (rx/map profile-fetched))))) + (rx/map profile-fetched) + (rx/catch on-fetch-profile-exception))))) ;; --- EVENT: login @@ -151,26 +168,36 @@ profile. The profile can proceed from standard login or from accepting invitation, or third party auth signup or singin." [profile] - (letfn [(get-redirect-event [] + (letfn [(get-redirect-events [] (let [team-id (get-current-team-id profile) - redirect-url (:redirect-url @storage)] - (if (some? redirect-url) - (do - (swap! storage dissoc :redirect-url) - (.replace js/location redirect-url)) - (rt/nav' :dashboard-projects {:team-id team-id}))))] + welcome-file-id (get-in profile [:props :welcome-file-id])] + (if (some? welcome-file-id) + (rx/of + (rt/nav' :workspace {:project-id (:default-project-id profile) + :file-id welcome-file-id}) + (update-profile-props {:welcome-file-id nil})) + (rx/of (rt/nav' :dashboard-projects {:team-id team-id})))))] (ptk/reify ::logged-in - IDeref - (-deref [_] profile) + ev/Event + (-data [_] + {::ev/name "signin" + ::ev/type "identify" + :email (:email profile) + :auth-backend (:auth-backend profile) + :fullname (:fullname profile) + :is-muted (:is-muted profile) + :default-team-id (:default-team-id profile) + :default-project-id (:default-project-id profile)}) ptk/WatchEvent (watch [_ _ _] (when (is-authenticated? profile) - (->> (rx/of (profile-fetched profile) - (fetch-teams) - (get-redirect-event) - (ws/initialize)) + (->> (rx/concat + (rx/of (profile-fetched profile) + (fetch-teams) + (ws/initialize)) + (get-redirect-events)) (rx/observe-on :async))))))) (declare login-from-register) @@ -223,8 +250,35 @@ (rx/observe-on :async))))) (rx/catch on-error)))))) +(def ^:private schema:login-with-ldap + (sm/define + [:map + [:email ::sm/email] + [:password :string]])) + +(defn login-with-ldap + [params] + + (dm/assert! + "expected valid params" + (sm/check! schema:login-with-ldap params)) + + (ptk/reify ::login-with-ldap + ptk/WatchEvent + (watch [_ _ _] + (let [{:keys [on-error on-success] + :or {on-error rx/throw + on-success identity}} (meta params)] + (->> (rp/cmd! :login-with-ldap params) + (rx/tap on-success) + (rx/map (fn [profile] + (-> profile + (with-meta {::ev/source "login-with-ldap"}) + (logged-in)))) + (rx/catch on-error)))))) (defn login-from-token + "Used mainly as flow continuation after token validation." [{:keys [profile] :as tdata}] (ptk/reify ::login-from-token ptk/WatchEvent @@ -280,20 +334,25 @@ ptk/EffectEvent (effect [_ _ _] ;; We prefer to keek some stuff in the storage like the current-team-id and the profile - (swap! storage dissoc :redirect-url) - (set-current-team! nil) - (i18n/reset-locale))))) + (swap! s/storage (constantly {})))))) (defn logout ([] (logout {})) ([params] (ptk/reify ::logout + ev/Event + (-data [_] {}) + ptk/WatchEvent - (watch [_ _ _] - (->> (rp/cmd! :logout) - (rx/delay-at-least 300) - (rx/catch (constantly (rx/of 1))) - (rx/map #(logged-out params))))))) + (watch [_ state _] + (let [profile-id (:profile-id state)] + (->> (rx/interval 500) + (rx/take 1) + (rx/mapcat (fn [_] + (->> (rp/cmd! :logout {:profile-id profile-id}) + (rx/delay-at-least 300) + (rx/catch (constantly (rx/of 1)))))) + (rx/map #(logged-out params)))))))) ;; --- Update Profile @@ -304,9 +363,9 @@ (watch [_ state _] (let [on-success (:on-success opts identity) on-error (:on-error opts rx/throw) - profile (:profile state)] - - (->> (rp/cmd! :update-profile (dissoc profile :props)) + profile (:profile state) + params (select-keys profile [:fullname :lang :theme])] + (->> (rp/cmd! :update-profile params) (rx/tap on-success) (rx/catch on-error)))))) @@ -360,6 +419,10 @@ [{:keys [email] :as data}] (dm/assert! ::us/email email) (ptk/reify ::request-email-change + ev/Event + (-data [_] + {:email email}) + ptk/WatchEvent (watch [_ _ _] (let [{:keys [on-error on-success] @@ -395,6 +458,9 @@ (sm/check! schema:update-password data)) (ptk/reify ::update-password + ev/Event + (-data [_] {}) + ptk/WatchEvent (watch [_ _ _] (let [{:keys [on-error on-success] @@ -458,6 +524,9 @@ (di/blob? file)) (ptk/reify ::update-photo + ev/Event + (-data [_] {}) + ptk/WatchEvent (watch [_ _ _] (let [on-success di/notify-finished-loading @@ -516,10 +585,9 @@ on-success identity}} (meta params)] (->> (rp/cmd! :delete-profile {}) (rx/tap on-success) - (rx/delay-at-least 300) - (rx/catch (constantly (rx/of 1))) (rx/map logged-out) - (rx/catch on-error)))))) + (rx/catch on-error) + (rx/delay-at-least 300)))))) ;; --- EVENT: request-profile-recovery @@ -635,3 +703,29 @@ (->> (rp/cmd! :delete-access-token params) (rx/tap on-success) (rx/catch on-error)))))) + +(defn show-redirect-error + "A helper event that interprets the OIDC redirect errors on the URI + and shows an appropriate error message using the notification + banners." + [error] + (ptk/reify ::show-redirect-error + ptk/WatchEvent + (watch [_ _ _] + (when-let [hint (case error + "registration-disabled" + (tr "errors.registration-disabled") + "profile-blocked" + (tr "errors.profile-blocked") + "auth-provider-not-allowed" + (tr "errors.auth-provider-not-allowed") + "email-domain-not-allowed" + (tr "errors.email-domain-not-allowed") + + ;; We explicitly do not show any error here, it a explicit user operation. + "unable-to-auth" + nil + + (tr "errors.generic"))] + + (rx/of (ntf/warn hint)))))) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index a45e75939ac700fddbd1174ffcb5d26b465cb78e..68757d15dd137a232ff107a18c27b4b8021c972a 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -134,7 +134,7 @@ (uuid? share-id) (assoc :share-id share-id))] (->> (rp/cmd! :get-file-fragment params) - (rx/map :content) + (rx/map :data) (rx/map #(vector key %)))))] (->> (rp/cmd! :get-view-only-bundle params') @@ -253,6 +253,18 @@ ;; --- Zoom Management +(def update-zoom-querystring + (ptk/reify ::update-zoom-querystring + ptk/WatchEvent + (watch [_ state _] + (let [zoom-type (get-in state [:viewer-local :zoom-type]) + route (:route state) + screen (-> route :data :name keyword) + qparams (:query-params route) + pparams (:path-params route)] + + (rx/of (rt/nav screen pparams (assoc qparams :zoom zoom-type))))))) + (def increase-zoom (ptk/reify ::increase-zoom ptk/UpdateEvent @@ -293,7 +305,10 @@ minzoom (min wdiff hdiff)] (-> state (assoc-in [:viewer-local :zoom] minzoom) - (assoc-in [:viewer-local :zoom-type] :fit)))))) + (assoc-in [:viewer-local :zoom-type] :fit)))) + + ptk/WatchEvent + (watch [_ _ _] (rx/of update-zoom-querystring)))) (def zoom-to-fill (ptk/reify ::zoom-to-fill @@ -309,7 +324,9 @@ maxzoom (max wdiff hdiff)] (-> state (assoc-in [:viewer-local :zoom] maxzoom) - (assoc-in [:viewer-local :zoom-type] :fill)))))) + (assoc-in [:viewer-local :zoom-type] :fill)))) + ptk/WatchEvent + (watch [_ _ _] (rx/of update-zoom-querystring)))) (def toggle-zoom-style (ptk/reify ::toggle-zoom-style diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 41fdb90ba68a9fe5168a718cb23f4ff71895ccbe..dd62ff70d8905510ed0914589678d15e11ef8061 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -19,30 +19,34 @@ [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.grid-layout :as gslg] + [app.common.logging :as log] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] [app.common.schema :as sm] [app.common.text :as txt] [app.common.transit :as t] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] [app.common.types.container :as ctn] + [app.common.types.file :as ctf] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] [app.common.types.typography :as ctt] [app.common.uuid :as uuid] [app.config :as cf] + [app.main.data.changes :as dch] [app.main.data.comments :as dcm] [app.main.data.events :as ev] [app.main.data.fonts :as df] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] + [app.main.data.persistence :as dps] [app.main.data.users :as du] [app.main.data.workspace.bool :as dwb] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.collapse :as dwco] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.edition :as dwe] - [app.main.data.workspace.fix-bool-contents :as fbc] [app.main.data.workspace.fix-broken-shapes :as fbs] [app.main.data.workspace.fix-deleted-fonts :as fdf] [app.main.data.workspace.groups :as dwg] @@ -56,7 +60,6 @@ [app.main.data.workspace.notifications :as dwn] [app.main.data.workspace.path :as dwdp] [app.main.data.workspace.path.shapes-to-path :as dwps] - [app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shapes :as dwsh] @@ -76,6 +79,7 @@ [app.util.http :as http] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] + [app.util.storage :refer [storage]] [app.util.timers :as tm] [app.util.webapi :as wapi] [beicon.v2.core :as rx] @@ -84,6 +88,7 @@ [potok.v2.core :as ptk])) (def default-workspace-local {:zoom 1}) +(log/set-level! :debug) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Workspace Initialization @@ -124,7 +129,6 @@ (when (and (not (boolean (-> state :profile :props :v2-info-shown))) (features/active-feature? state "components/v2")) (modal/show :v2-info {})) - (fbc/fix-bool-contents) (fdf/fix-deleted-fonts) (fbs/fix-broken-shapes))))) @@ -332,21 +336,39 @@ ptk/UpdateEvent (update [_ state] (assoc state + :recent-colors (:recent-colors @storage) :workspace-ready? false :current-file-id file-id :current-project-id project-id :workspace-presence {})) ptk/WatchEvent - (watch [_ _ _] - (rx/of msg/hide - (dcm/retrieve-comment-threads file-id) - (dwp/initialize-file-persistence file-id) - (fetch-bundle project-id file-id))) + (watch [_ _ stream] + (log/debug :hint "initialize-file" :file-id file-id) + (let [stoper-s (rx/filter (ptk/type? ::finalize-file) stream)] + (rx/merge + (rx/of (ntf/hide) + (features/initialize) + (dcm/retrieve-comment-threads file-id) + (fetch-bundle project-id file-id)) + + (->> stream + (rx/filter dch/commit?) + (rx/map deref) + (rx/mapcat (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}] + (if (and save-undo? (seq undo-changes)) + (let [entry {:undo-changes undo-changes + :redo-changes redo-changes + :undo-group undo-group + :tags tags}] + (rx/of (dwu/append-undo entry stack-undo?))) + (rx/empty)))) + + (rx/take-until stoper-s))))) ptk/EffectEvent (effect [_ _ _] - (let [name (str "workspace-" file-id)] + (let [name (dm/str "workspace-" file-id)] (unchecked-set ug/global "name" name))))) (defn finalize-file @@ -354,20 +376,22 @@ (ptk/reify ::finalize-file ptk/UpdateEvent (update [_ state] - (dissoc state - :current-file-id - :current-project-id - :workspace-data - :workspace-editor-state - :workspace-file - :workspace-libraries - :workspace-ready? - :workspace-media-objects - :workspace-persistence - :workspace-presence - :workspace-project - :workspace-project - :workspace-undo)) + (-> state + (dissoc + :current-file-id + :current-project-id + :workspace-data + :workspace-editor-state + :workspace-file + :workspace-libraries + :workspace-media-objects + :workspace-persistence + :workspace-presence + :workspace-project + :workspace-ready? + :workspace-undo) + (update :workspace-global dissoc :read-only?) + (assoc-in [:workspace-global :options-mode] :design))) ptk/WatchEvent (watch [_ _ _] @@ -455,12 +479,13 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn create-page - [{:keys [file-id]}] - (let [id (uuid/next)] + [{:keys [page-id file-id]}] + (let [id (or page-id (uuid/next))] (ptk/reify ::create-page - IDeref - (-deref [_] - {:id id :file-id file-id}) + ev/Event + (-data [_] + {:id id + :file-id file-id}) ptk/WatchEvent (watch [it state _] @@ -543,6 +568,35 @@ (rx/of (dch/commit-changes changes)))))) +(defn set-plugin-data + ([file-id type namespace key value] + (set-plugin-data file-id type nil nil namespace key value)) + ([file-id type id namespace key value] + (set-plugin-data file-id type id nil namespace key value)) + ([file-id type id page-id namespace key value] + (dm/assert! (contains? #{:file :page :shape :color :typography :component} type)) + (dm/assert! (or (nil? id) (uuid? id))) + (dm/assert! (or (nil? page-id) (uuid? page-id))) + (dm/assert! (uuid? file-id)) + (dm/assert! (keyword? namespace)) + (dm/assert! (string? key)) + (dm/assert! (or (nil? value) (string? value))) + + (ptk/reify ::set-file-plugin-data + ptk/WatchEvent + (watch [it state _] + (let [file-data + (if (= file-id (:current-file-id state)) + (:workspace-data state) + (get-in state [:workspace-libraries file-id :data])) + + changes + (-> (pcb/empty-changes it) + (pcb/with-file-data file-data) + (assoc :file-id file-id) + (pcb/mod-plugin-data type id page-id namespace key value))] + (rx/of (dch/commit-changes changes))))))) + (declare purge-page) (declare go-to-file) @@ -665,7 +719,7 @@ (ptk/reify ::update-shape ptk/WatchEvent (watch [_ _ _] - (rx/of (dch/update-shapes [id] #(merge % attrs)))))) + (rx/of (dwsh/update-shapes [id] #(merge % attrs)))))) (defn start-rename-shape "Start shape renaming process" @@ -678,12 +732,12 @@ (defn end-rename-shape "End the ongoing shape rename process" - ([] (end-rename-shape nil)) - ([name] + ([] (end-rename-shape nil nil)) + ([shape-id name] (ptk/reify ::end-rename-shape ptk/WatchEvent (watch [_ state _] - (when-let [shape-id (dm/get-in state [:workspace-local :shape-for-rename])] + (when-let [shape-id (d/nilv shape-id (dm/get-in state [:workspace-local :shape-for-rename]))] (let [shape (wsh/lookup-shape state shape-id) name (str/trim name) clean-name (cfh/clean-path name) @@ -782,99 +836,10 @@ ;; --- Change Shape Order (D&D Ordering) -(defn relocate-shapes-changes [it objects parents parent-id page-id to-index ids - groups-to-delete groups-to-unmask shapes-to-detach - shapes-to-reroot shapes-to-deroot shapes-to-unconstraint] - (let [ordered-indexes (cfh/order-by-indexed-shapes objects ids) - shapes (map (d/getf objects) ordered-indexes) - parent (get objects parent-id)] - - (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - - ;; Remove layout-item properties when moving a shape outside a layout - (cond-> (not (ctl/any-layout? parent)) - (pcb/update-shapes ordered-indexes ctl/remove-layout-item-data)) - - ;; Remove the hide in viewer flag - (cond-> (and (not= uuid/zero parent-id) (cfh/frame-shape? parent)) - (pcb/update-shapes ordered-indexes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true)))) - - ;; Move the shapes - (pcb/change-parent parent-id - shapes - to-index) - - ;; Remove empty groups - (pcb/remove-objects groups-to-delete) - - ;; Unmask groups whose mask have moved outside - (pcb/update-shapes groups-to-unmask - (fn [shape] - (assoc shape :masked-group false))) - - ;; Detach shapes moved out of their component - (pcb/update-shapes shapes-to-detach ctk/detach-shape) - - ;; Make non root a component moved inside another one - (pcb/update-shapes shapes-to-deroot - (fn [shape] - (assoc shape :component-root nil))) - - ;; Make root a subcomponent moved outside its parent component - (pcb/update-shapes shapes-to-reroot - (fn [shape] - (assoc shape :component-root true))) - - ;; Reset constraints depending on the new parent - (pcb/update-shapes shapes-to-unconstraint - (fn [shape] - (let [frame-id (if (= (:type parent) :frame) - (:id parent) - (:frame-id parent)) - moved-shape (assoc shape - :parent-id parent-id - :frame-id frame-id)] - (assoc shape - :constraints-h (gsh/default-constraints-h moved-shape) - :constraints-v (gsh/default-constraints-v moved-shape)))) - {:ignore-touched true}) - - ;; Fix the sizing when moving a shape - (pcb/update-shapes parents - (fn [parent] - (if (ctl/flex-layout? parent) - (cond-> parent - (ctl/change-h-sizing? (:id parent) objects (:shapes parent)) - (assoc :layout-item-h-sizing :fix) - - (ctl/change-v-sizing? (:id parent) objects (:shapes parent)) - (assoc :layout-item-v-sizing :fix)) - parent))) - - ;; Update grid layout - (cond-> (ctl/grid-layout? objects parent-id) - (pcb/update-shapes [parent-id] #(ctl/add-children-to-index % ids objects to-index))) - - (pcb/update-shapes parents - (fn [parent objects] - (cond-> parent - (ctl/grid-layout? parent) - (ctl/assign-cells objects))) - {:with-objects? true}) - - (pcb/reorder-grid-children parents) - - ;; If parent locked, lock the added shapes - (cond-> (:blocked parent) - (pcb/update-shapes ordered-indexes #(assoc % :blocked true))) - - ;; Resize parent containers that need to - (pcb/resize-parents parents)))) - (defn relocate-shapes [ids parent-id to-index & [ignore-parents?]] (dm/assert! (every? uuid? ids)) + (dm/assert! (set? ids)) (dm/assert! (uuid? parent-id)) (dm/assert! (number? to-index)) @@ -891,97 +856,14 @@ ids (filter #(not (cfh/is-parent? objects parent-id %)) ids) all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids) - parents (if ignore-parents? #{parent-id} all-parents) - - groups-to-delete - (loop [current-id (first parents) - to-check (rest parents) - removed-id? (set ids) - result #{}] - - (if-not current-id - ;; Base case, no next element - result - - (let [group (get objects current-id)] - (if (and (not= :frame (:type group)) - (not= current-id parent-id) - (empty? (remove removed-id? (:shapes group)))) - - ;; Adds group to the remove and check its parent - (let [to-check (concat to-check [(cfh/get-parent-id objects current-id)])] - (recur (first to-check) - (rest to-check) - (conj removed-id? current-id) - (conj result current-id))) - - ;; otherwise recur - (recur (first to-check) - (rest to-check) - removed-id? - result))))) - - groups-to-unmask - (reduce (fn [group-ids id] - ;; When a masked group loses its mask shape, because it's - ;; moved outside the group, the mask condition must be - ;; removed, and it must be converted to a normal group. - (let [obj (get objects id) - parent (get objects (:parent-id obj))] - (if (and (:masked-group parent) - (= id (first (:shapes parent))) - (not= (:id parent) parent-id)) - (conj group-ids (:id parent)) - group-ids))) - #{} - ids) - - ;; TODO: Probably implementing this using loop/recur will - ;; be more efficient than using reduce and continuous data - ;; desturcturing. - - ;; Sets the correct components metadata for the moved shapes - ;; `shapes-to-detach` Detach from a component instance a shape that was inside a component and is moved outside - ;; `shapes-to-deroot` Removes the root flag from a component instance moved inside another component - ;; `shapes-to-reroot` Adds a root flag when a nested component instance is moved outside - [shapes-to-detach shapes-to-deroot shapes-to-reroot] - (reduce (fn [[shapes-to-detach shapes-to-deroot shapes-to-reroot] id] - (let [shape (get objects id) - parent (get objects parent-id) - component-shape (ctn/get-component-shape objects shape) - component-shape-parent (ctn/get-component-shape objects parent {:allow-main? true}) - root-parent (ctn/get-instance-root objects parent) - - detach? (and (ctk/in-component-copy-not-head? shape) - (not= (:id component-shape) - (:id component-shape-parent))) - deroot? (and (ctk/instance-root? shape) - root-parent) - reroot? (and (ctk/subinstance-head? shape) - (not component-shape-parent)) - - ids-to-detach (when detach? - (cons id (cfh/get-children-ids objects id)))] - - [(cond-> shapes-to-detach detach? (into ids-to-detach)) - (cond-> shapes-to-deroot deroot? (conj id)) - (cond-> shapes-to-reroot reroot? (conj id))])) - [[] [] []] - ids) - - changes (relocate-shapes-changes it - objects - parents - parent-id - page-id - to-index - ids - groups-to-delete - groups-to-unmask - shapes-to-detach - shapes-to-reroot - shapes-to-deroot - ids) + + changes (cls/generate-relocate (pcb/empty-changes it) + objects + parent-id + page-id + to-index + ids + :ignore-parents? ignore-parents?) undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) @@ -1147,7 +1029,7 @@ (assoc shape :proportion-lock false) (-> (assoc shape :proportion-lock true) (gpp/assign-proportions))))] - (rx/of (dch/update-shapes [id] assign-proportions)))))) + (rx/of (dwsh/update-shapes [id] assign-proportions)))))) (defn toggle-proportion-lock [] @@ -1161,8 +1043,8 @@ multi (attrs/get-attrs-multi selected-obj [:proportion-lock]) multi? (= :multiple (:proportion-lock multi))] (if multi? - (rx/of (dch/update-shapes selected #(assoc % :proportion-lock true))) - (rx/of (dch/update-shapes selected #(update % :proportion-lock not)))))))) + (rx/of (dwsh/update-shapes selected #(assoc % :proportion-lock true))) + (rx/of (dwsh/update-shapes selected #(update % :proportion-lock not)))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Navigation @@ -1242,6 +1124,14 @@ (update [_ state] (assoc-in state [:workspace-assets :open-status file-id section] open?)))) +(defn clear-assets-section-open + [] + (ptk/reify ::clear-assets-section-open + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-assets :open-status] {})))) + + (defn set-assets-group-open [file-id section path open?] (ptk/reify ::set-assets-group-open @@ -1423,7 +1313,7 @@ (assoc :section section) (some? frame-id) (assoc :frame-id frame-id))] - (rx/of ::dwp/force-persist + (rx/of ::dps/force-persist (rt/nav-new-window* {:rname :viewer :path-params pparams :query-params qparams @@ -1436,7 +1326,7 @@ ptk/WatchEvent (watch [_ state _] (when-let [team-id (or team-id (:current-team-id state))] - (rx/of ::dwp/force-persist + (rx/of ::dps/force-persist (rt/nav :dashboard-projects {:team-id team-id}))))))) (defn go-to-dashboard-fonts @@ -1445,7 +1335,7 @@ ptk/WatchEvent (watch [_ state _] (let [team-id (:current-team-id state)] - (rx/of ::dwp/force-persist + (rx/of ::dps/force-persist (rt/nav :dashboard-fonts {:team-id team-id})))))) @@ -1571,7 +1461,9 @@ (->> (:strokes obj) (keep :stroke-image)) (when (cfh/image-shape? obj) - [(:metadata obj)]))] + [(:metadata obj)]) + (when (:fill-image obj) + [(:fill-image obj)]))] (if (seq imgdata) (->> (rx/from imgdata) @@ -1597,6 +1489,34 @@ (let [frame (get objects parent-frame-id)] (gsh/translate-to-frame shape frame)))) + ;; When copying an instance that is nested inside another one, we need to + ;; advance the shape refs to one or more levels of remote mains. + (advance-copies [state selected data] + (let [file (wsh/get-local-file-full state) + libraries (wsh/get-libraries state) + page (wsh/lookup-page state) + heads (mapcat #(ctn/get-child-heads (:objects data) %) selected)] + (update data :objects + #(reduce (partial advance-copy file libraries page) + % + heads)))) + + (advance-copy [file libraries page objects shape] + (if (and (ctk/instance-head? shape) (not (ctk/main-instance? shape))) + (let [level-delta (ctn/get-nesting-level-delta (:objects page) shape uuid/zero)] + (if (pos? level-delta) + (reduce (partial advance-shape file libraries page level-delta) + objects + (cfh/get-children-with-self objects (:id shape))) + objects)) + objects)) + + (advance-shape [file libraries page level-delta objects shape] + (let [new-shape-ref (ctf/advance-shape-ref file page libraries shape level-delta {:include-deleted? true})] + (cond-> objects + (and (some? new-shape-ref) (not= new-shape-ref (:shape-ref shape))) + (assoc-in [(:id shape) :shape-ref] new-shape-ref)))) + (on-copy-error [error] (js/console.error "clipboard blocked:" error) (rx/empty))] @@ -1635,6 +1555,7 @@ (rx/merge-map (partial prepare-object objects frame-id)) (rx/reduce collect-data initial) (rx/map (partial sort-selected state)) + (rx/map (partial advance-copies state selected)) (rx/map #(t/encode-str % {:type :json-verbose})) (rx/map wapi/write-to-clipboard) (rx/catch on-copy-error) @@ -1658,8 +1579,14 @@ (process-entry [[type data]] (case type :text - (if (str/empty? data) + (cond + (str/empty? data) (rx/empty) + + (re-find #" pdata wapi/extract-text) transit-data (ex/ignoring (some-> text-data t/decode-str))] (cond - (and (string? text-data) - (str/includes? text-data " (get-in mdata attr-path) + (:id)) mobj (get media-idx id)] (if mobj - (update-in mdata attr-path (fn [value] - (-> value - (assoc :id (:id mobj)) - (assoc :path (:path mobj))))) + (if (empty? attr-path) + (-> mdata + (assoc :id (:id mobj)) + (assoc :path (:path mobj))) + (update-in mdata attr-path (fn [value] + (-> value + (assoc :id (:id mobj)) + (assoc :path (:path mobj)))))) + mdata))) (add-obj? [chg] @@ -1818,15 +1750,15 @@ ;; references to the new uploaded media-objects. (process-rchange [media-idx change] (let [;; Texts can have different fills for pieces of the text - tr-fill-xf (map #(translate-media % media-idx [:fill-image :id])) - tr-stroke-xf (map #(translate-media % media-idx [:stroke-image :id]))] - + tr-fill-xf (map #(translate-media % media-idx [:fill-image])) + tr-stroke-xf (map #(translate-media % media-idx [:stroke-image]))] (if (add-obj? change) (update change :obj (fn [obj] (-> obj (update :fills #(into [] tr-fill-xf %)) (update :strokes #(into [] tr-stroke-xf %)) - (d/update-when :metadata translate-media media-idx [:id]) + (d/update-when :metadata translate-media media-idx []) + (d/update-when :fill-image translate-media media-idx []) (d/update-when :content (fn [content] (txt/xform-nodes tr-fill-xf content))) @@ -1952,10 +1884,11 @@ (let [file-id (:current-file-id state) page (wsh/lookup-page state) - media-idx (->> (:media pdata) + media-idx (->> (:images pdata) (d/index-by :prev-id)) selected (:selected pdata) + objects (:objects pdata) position (deref ms/mouse-position) @@ -1979,16 +1912,20 @@ index 0) + selected (if (and (ctl/flex-layout? page-objects parent-id) (not (ctl/reverse? page-objects parent-id))) + (into (d/ordered-set) (reverse selected)) + selected) + objects (update-vals objects (partial process-shape file-id frame-id parent-id)) all-objects (merge page-objects objects) - drop-cell (when (ctl/grid-layout? all-objects parent-id) (gslg/get-drop-cell frame-id all-objects position)) - changes (-> (dws/prepare-duplicate-changes all-objects page selected delta it libraries ldata file-id) + changes (-> (pcb/empty-changes it) + (cll/generate-duplicate-changes all-objects page selected delta libraries ldata file-id) (pcb/amend-changes (partial process-rchange media-idx)) (pcb/amend-changes (partial change-add-obj-index objects selected index))) @@ -2115,16 +2052,18 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn change-canvas-color - [color] - (ptk/reify ::change-canvas-color - ptk/WatchEvent - (watch [it state _] - (let [page (wsh/lookup-page state) - changes (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/set-page-option :background (:color color)))] - - (rx/of (dch/commit-changes changes)))))) + ([color] + (change-canvas-color nil color)) + ([page-id color] + (ptk/reify ::change-canvas-color + ptk/WatchEvent + (watch [it state _] + (let [page-id (or page-id (:current-page-id state)) + page (wsh/lookup-page state page-id) + changes (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/set-page-option :background (:color color)))] + (rx/of (dch/commit-changes changes))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Read only diff --git a/frontend/src/app/main/data/workspace/bool.cljs b/frontend/src/app/main/data/workspace/bool.cljs index ac9e06dee4f946f21240210f75c2d11b029b5902..013a2dbe66fa7b441ec7bd86e8d2f3c842e39d50 100644 --- a/frontend/src/app/main/data/workspace/bool.cljs +++ b/frontend/src/app/main/data/workspace/bool.cljs @@ -15,8 +15,9 @@ [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dch] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [beicon.v2.core :as rx] [cuerdas.core :as str] @@ -82,31 +83,38 @@ (gsh/update-group-selrect children)))) (defn create-bool - [bool-type] - (ptk/reify ::create-bool-union - ptk/WatchEvent - (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state) - name (-> bool-type d/name str/capital) - ids (selected-shapes-idx state) - ordered-indexes (cph/order-by-indexed-shapes objects ids) - shapes (->> ordered-indexes - (map (d/getf objects)) - (remove cph/frame-shape?) - (remove #(ctn/has-any-copy-parent? objects %)))] - - (when-not (empty? shapes) - (let [[boolean-data index] (create-bool-data bool-type name (reverse shapes) objects) - index (inc index) - shape-id (:id boolean-data) - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - (pcb/add-object boolean-data {:index index}) - (pcb/update-shapes (map :id shapes) ctl/remove-layout-item-data) - (pcb/change-parent shape-id shapes))] - (rx/of (dch/commit-changes changes) - (dws/select-shapes (d/ordered-set shape-id))))))))) + ([bool-type] + (create-bool bool-type nil nil)) + ([bool-type ids {:keys [id-ret]}] + (assert (or (nil? ids) (set? ids))) + (ptk/reify ::create-bool-union + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state) + name (-> bool-type d/name str/capital) + ids (->> (or ids (wsh/lookup-selected state)) + (cph/clean-loops objects)) + ordered-indexes (cph/order-by-indexed-shapes objects ids) + shapes (->> ordered-indexes + (map (d/getf objects)) + (remove cph/frame-shape?) + (remove #(ctn/has-any-copy-parent? objects %)))] + + (when-not (empty? shapes) + (let [[boolean-data index] (create-bool-data bool-type name (reverse shapes) objects) + index (inc index) + shape-id (:id boolean-data) + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (pcb/add-object boolean-data {:index index}) + (pcb/update-shapes (map :id shapes) ctl/remove-layout-item-data) + (pcb/change-parent shape-id shapes))] + (when id-ret + (reset! id-ret shape-id)) + + (rx/of (dch/commit-changes changes) + (dws/select-shapes (d/ordered-set shape-id)))))))))) (defn group-to-bool [shape-id bool-type] @@ -117,7 +125,7 @@ change-to-bool (fn [shape] (group->bool shape bool-type objects))] (when-not (ctn/has-any-copy-parent? objects (get objects shape-id)) - (rx/of (dch/update-shapes [shape-id] change-to-bool {:reg-objects? true}))))))) + (rx/of (dwsh/update-shapes [shape-id] change-to-bool {:reg-objects? true}))))))) (defn bool-to-group [shape-id] @@ -128,7 +136,7 @@ change-to-group (fn [shape] (bool->group shape objects))] (when-not (ctn/has-any-copy-parent? objects (get objects shape-id)) - (rx/of (dch/update-shapes [shape-id] change-to-group {:reg-objects? true}))))))) + (rx/of (dwsh/update-shapes [shape-id] change-to-group {:reg-objects? true}))))))) (defn change-bool-type @@ -140,4 +148,4 @@ change-type (fn [shape] (assoc shape :bool-type bool-type))] (when-not (ctn/has-any-copy-parent? objects (get objects shape-id)) - (rx/of (dch/update-shapes [shape-id] change-type {:reg-objects? true}))))))) + (rx/of (dwsh/update-shapes [shape-id] change-type {:reg-objects? true}))))))) diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs deleted file mode 100644 index b2d595086525014ada7668856d61cac60cebc2fc..0000000000000000000000000000000000000000 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ /dev/null @@ -1,268 +0,0 @@ -;; 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 app.main.data.workspace.changes - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.exceptions :as ex] - [app.common.files.changes :as cpc] - [app.common.files.changes-builder :as pcb] - [app.common.files.helpers :as cph] - [app.common.logging :as log] - [app.common.schema :as sm] - [app.common.types.shape-tree :as ctst] - [app.common.types.shape.layout :as ctl] - [app.common.uuid :as uuid] - [app.main.data.workspace.state-helpers :as wsh] - [app.main.data.workspace.undo :as dwu] - [app.main.store :as st] - [app.main.worker :as uw] - [beicon.v2.core :as rx] - [potok.v2.core :as ptk])) - -;; Change this to :info :debug or :trace to debug this module -(log/set-level! :warn) - -(defonce page-change? #{:add-page :mod-page :del-page :mov-page}) -(defonce update-layout-attr? #{:hidden}) - -(declare commit-changes) - -(defn- add-undo-group - [changes state] - (let [undo (:workspace-undo state) - items (:items undo) - index (or (:index undo) (dec (count items))) - prev-item (when-not (or (empty? items) (= index -1)) - (get items index)) - undo-group (:undo-group prev-item) - add-undo-group? (and - (not (nil? undo-group)) - (= (get-in changes [:redo-changes 0 :type]) :mod-obj) - (= (get-in prev-item [:redo-changes 0 :type]) :add-obj) - (contains? (:tags prev-item) :alt-duplication))] ;; This is a copy-and-move with mouse+alt - - (cond-> changes add-undo-group? (assoc :undo-group undo-group)))) - -(def commit-changes? (ptk/type? ::commit-changes)) - -(defn update-shapes - ([ids update-fn] (update-shapes ids update-fn nil)) - ([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-remote? ignore-touched undo-group with-objects?] - :or {reg-objects? false save-undo? true stack-undo? false ignore-remote? false ignore-touched false with-objects? false}}] - - (dm/assert! - "expected a valid coll of uuid's" - (sm/check-coll-of-uuid! ids)) - - (dm/assert! (fn? update-fn)) - - (ptk/reify ::update-shapes - ptk/WatchEvent - (watch [it state _] - (let [page-id (or page-id (:current-page-id state)) - objects (wsh/lookup-page-objects state page-id) - ids (into [] (filter some?) ids) - - update-layout-ids - (->> ids - (map (d/getf objects)) - (filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs :with-objects? with-objects?}))) - (map :id)) - - changes (reduce - (fn [changes id] - (let [opts {:attrs attrs - :ignore-geometry? (get ignore-tree id) - :ignore-touched ignore-touched - :with-objects? with-objects?}] - (pcb/update-shapes changes [id] update-fn (d/without-nils opts)))) - (-> (pcb/empty-changes it page-id) - (pcb/set-save-undo? save-undo?) - (pcb/set-stack-undo? stack-undo?) - (pcb/with-objects objects) - (cond-> undo-group - (pcb/set-undo-group undo-group))) - ids) - grid-ids (->> ids (filter (partial ctl/grid-layout? objects))) - changes (pcb/update-shapes changes grid-ids ctl/assign-cell-positions {:with-objects? true}) - changes (pcb/reorder-grid-children changes ids) - changes (add-undo-group changes state)] - (rx/concat - (if (seq (:redo-changes changes)) - (let [changes (cond-> changes reg-objects? (pcb/resize-parents ids)) - changes (cond-> changes ignore-remote? (pcb/ignore-remote))] - (rx/of (commit-changes changes))) - (rx/empty)) - - ;; Update layouts for properties marked - (if (d/not-empty? update-layout-ids) - (rx/of (ptk/data-event :layout/update {:ids update-layout-ids})) - (rx/empty)))))))) - -(defn send-update-indices - [] - (ptk/reify ::send-update-indices - ptk/WatchEvent - (watch [_ _ _] - (->> (rx/of - (fn [state] - (-> state - (dissoc ::update-indices-debounce) - (dissoc ::update-changes)))) - (rx/observe-on :async))) - - ptk/EffectEvent - (effect [_ state _] - (doseq [[page-id changes] (::update-changes state)] - (uw/ask! {:cmd :update-page-index - :page-id page-id - :changes changes}))))) - -;; Update indices will debounce operations so we don't have to update -;; the index several times (which is an expensive operation) -(defn update-indices - [page-id changes] - - (let [start (uuid/next)] - (ptk/reify ::update-indices - ptk/UpdateEvent - (update [_ state] - (if (nil? (::update-indices-debounce state)) - (assoc state ::update-indices-debounce start) - (update-in state [::update-changes page-id] (fnil d/concat-vec []) changes))) - - ptk/WatchEvent - (watch [_ state stream] - (if (= (::update-indices-debounce state) start) - (let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))] - (rx/merge - (->> stream - (rx/filter (ptk/type? ::update-indices)) - (rx/debounce 50) - (rx/take 1) - (rx/map #(send-update-indices)) - (rx/take-until stopper)) - (rx/of (update-indices page-id changes)))) - (rx/empty)))))) - -(defn changed-frames - "Extracts the frame-ids changed in the given changes" - [changes objects] - - (let [change->ids - (fn [change] - (case (:type change) - :add-obj - [(:parent-id change)] - - (:mod-obj :del-obj) - [(:id change)] - - :mov-objects - (d/concat-vec (:shapes change) [(:parent-id change)]) - - []))] - (into #{} - (comp (mapcat change->ids) - (keep #(cph/get-shape-id-root-frame objects %)) - (remove #(= uuid/zero %))) - changes))) - -(defn commit-changes - "Schedules a list of changes to execute now, and add the corresponding undo changes to - the undo stack. - - Options: - - save-undo?: if set to false, do not add undo changes. - - undo-group: if some consecutive changes (or even transactions) share the same - undo-group, they will be undone or redone in a single step - " - [{:keys [redo-changes undo-changes - origin save-undo? file-id undo-group tags stack-undo?] - :or {save-undo? true stack-undo? false tags #{} undo-group (uuid/next)}}] - (let [error (volatile! nil) - page-id (:current-page-id @st/state) - frames (changed-frames redo-changes (wsh/lookup-page-objects @st/state)) - undo-changes (vec undo-changes) - redo-changes (vec redo-changes)] - (ptk/reify ::commit-changes - cljs.core/IDeref - (-deref [_] - {:file-id file-id - :hint-events @st/last-events - :hint-origin (ptk/type origin) - :changes redo-changes - :page-id page-id - :frames frames - :save-undo? save-undo? - :undo-group undo-group - :tags tags - :stack-undo? stack-undo?}) - - ptk/UpdateEvent - (update [_ state] - (log/info :msg "commit-changes" - :js/undo-group (str undo-group) - :js/file-id (str (or file-id "nil")) - :js/redo-changes redo-changes - :js/undo-changes undo-changes) - (let [current-file-id (get state :current-file-id) - file-id (or file-id current-file-id) - path (if (= file-id current-file-id) - [:workspace-data] - [:workspace-libraries file-id :data])] - - (try - (dm/assert! - "expect valid vector of changes" - (and (cpc/check-changes! redo-changes) - (cpc/check-changes! undo-changes))) - - (update-in state path (fn [file] - (-> file - (cpc/process-changes redo-changes false) - (ctst/update-object-indices page-id)))) - - (catch :default err - (when-let [data (ex-data err)] - (js/console.log (ex/explain data))) - - (when (ex/error? err) - (js/console.log (.-stack ^js err))) - (vreset! error err) - state)))) - - ptk/WatchEvent - (watch [_ _ _] - (when-not @error - (let [;; adds page-id to page changes (that have the `id` field instead) - add-page-id - (fn [{:keys [id type page] :as change}] - (cond-> change - (and (page-change? type) (nil? (:page-id change))) - (assoc :page-id (or id (:id page))))) - - changes-by-pages - (->> redo-changes - (map add-page-id) - (remove #(nil? (:page-id %))) - (group-by :page-id)) - - process-page-changes - (fn [[page-id _changes]] - (update-indices page-id redo-changes))] - - (rx/concat - (rx/from (map process-page-changes changes-by-pages)) - - (when (and save-undo? (seq undo-changes)) - (let [entry {:undo-changes undo-changes - :redo-changes redo-changes - :undo-group undo-group - :tags tags}] - (rx/of (dwu/append-undo entry stack-undo?))))))))))) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 3a8bf6e13e4ac3268bc5fad70f10079ef5d022ea..dc0a44d4a246e0c2f0c0ec5777c866610f8073bb 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -15,15 +15,16 @@ [app.main.broadcast :as mbc] [app.main.data.events :as ev] [app.main.data.modal :as md] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.layout :as layout] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.undo :as dwu] [app.util.color :as uc] [app.util.storage :refer [storage]] [beicon.v2.core :as rx] + [cuerdas.core :as str] [potok.v2.core :as ptk])) ;; A set of keys that are used for shared state identifiers @@ -116,7 +117,7 @@ (rx/concat (rx/of (dwu/start-undo-transaction undo-id)) (rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids)) - (rx/of (dch/update-shapes shape-ids transform-attrs)) + (rx/of (dwsh/update-shapes shape-ids transform-attrs)) (rx/of (dwu/commit-undo-transaction undo-id))))) (defn swap-attrs [shape attr index new-index] @@ -140,7 +141,7 @@ (rx/concat (rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids)) - (rx/of (dch/update-shapes shape-ids transform-attrs))))))) + (rx/of (dwsh/update-shapes shape-ids transform-attrs))))))) (defn change-fill [ids color position] @@ -203,10 +204,10 @@ is-text? #(= :text (:type (get objects %))) shape-ids (filter (complement is-text?) ids) attrs {:hide-fill-on-export hide-fill-on-export}] - (rx/of (dch/update-shapes shape-ids (fn [shape] - (if (= (:type shape) :frame) - (d/merge shape attrs) - shape)))))))) + (rx/of (dwsh/update-shapes shape-ids (fn [shape] + (if (= (:type shape) :frame) + (d/merge shape attrs) + shape)))))))) (defn change-stroke [ids attrs index] (ptk/reify ::change-stroke @@ -236,7 +237,7 @@ (dissoc :image) (dissoc :gradient))] - (rx/of (dch/update-shapes + (rx/of (dwsh/update-shapes ids (fn [shape] (let [new-attrs (merge (get-in shape [:strokes index]) attrs) @@ -248,7 +249,7 @@ (assoc :stroke-style :solid) (not (contains? new-attrs :stroke-alignment)) - (assoc :stroke-alignment :inner) + (assoc :stroke-alignment :center) :always (d/without-nils))] @@ -264,7 +265,7 @@ (ptk/reify ::change-shadow ptk/WatchEvent (watch [_ _ _] - (rx/of (dch/update-shapes + (rx/of (dwsh/update-shapes ids (fn [shape] (let [;; If we try to set a gradient to a shadow (for @@ -273,7 +274,8 @@ ;; color attrs (cond-> attrs (:gradient attrs) (get-in [:gradient :stops 0])) - new-attrs (merge (get-in shape [:shadow index :color]) attrs)] + new-attrs (-> (merge (get-in shape [:shadow index :color]) attrs) + (d/without-nils))] (assoc-in shape [:shadow index :color] new-attrs)))))))) (defn add-shadow @@ -287,7 +289,7 @@ (watch [_ _ _] (let [add-shadow (fn [shape] (update shape :shadow #(into [shadow] %)))] - (rx/of (dch/update-shapes ids add-shadow)))))) + (rx/of (dwsh/update-shapes ids add-shadow)))))) (defn add-stroke [ids stroke] @@ -295,7 +297,7 @@ ptk/WatchEvent (watch [_ _ _] (let [add-stroke (fn [shape] (update shape :strokes #(into [stroke] %)))] - (rx/of (dch/update-shapes ids add-stroke)))))) + (rx/of (dwsh/update-shapes ids add-stroke)))))) (defn remove-stroke [ids position] @@ -308,7 +310,7 @@ (mapv second))) (remove-stroke [shape] (update shape :strokes remove-fill-by-index position))] - (rx/of (dch/update-shapes ids remove-stroke)))))) + (rx/of (dwsh/update-shapes ids remove-stroke)))))) (defn remove-all-strokes [ids] @@ -316,14 +318,14 @@ ptk/WatchEvent (watch [_ _ _] (let [remove-all #(assoc % :strokes [])] - (rx/of (dch/update-shapes ids remove-all)))))) + (rx/of (dwsh/update-shapes ids remove-all)))))) (defn reorder-shadows [ids index new-index] (ptk/reify ::reorder-shadow ptk/WatchEvent (watch [_ _ _] - (rx/of (dch/update-shapes + (rx/of (dwsh/update-shapes ids #(swap-attrs % :shadow index new-index)))))) @@ -332,7 +334,7 @@ (ptk/reify ::reorder-strokes ptk/WatchEvent (watch [_ _ _] - (rx/of (dch/update-shapes + (rx/of (dwsh/update-shapes ids #(swap-attrs % :strokes index new-index)))))) @@ -376,7 +378,7 @@ (defn color-att->text [color] - {:fill-color (:color color) + {:fill-color (when (:color color) (str/lower (:color color))) :fill-opacity (:opacity color) :fill-color-ref-id (:id color) :fill-color-ref-file (:file-id color) @@ -589,7 +591,7 @@ (update [_ state] (update state :colorpicker (fn [state] - (let [type (:type state) + (let [type (:type state) state (-> state (update :current-color merge changes) (update :current-color materialize-color-components) @@ -601,12 +603,20 @@ (update-in state [:stops stop] (fn [data] (->> changes (merge data) (materialize-color-components)))) + (-> state - (dissoc :gradient :stops :editing-stop))))))) + (dissoc :gradient :stops :editing-stop) + (cond-> (not= :image type) + (assoc :type :color)))))))) ptk/WatchEvent (watch [_ state _] - (when add-recent? - (let [formated-color (get-color-from-colorpicker-state (:colorpicker state))] + (let [selected-type (-> state + :colorpicker + :type) + formated-color (get-color-from-colorpicker-state (:colorpicker state)) + ;; Type is set to color on closing the colorpicker, but we can can close it while still uploading an image fill + ignore-color? (and (= selected-type :color) (nil? (:color formated-color)))] + (when (and add-recent? (not ignore-color?)) (rx/of (dwl/add-recent-color formated-color))))))) (defn update-colorpicker-gradient diff --git a/frontend/src/app/main/data/workspace/comments.cljs b/frontend/src/app/main/data/workspace/comments.cljs index 3d1457d56f1663c87398c048bbac8f99cf4e0e65..69e2a77eb18a68957b75be9dc61de5eff3502105 100644 --- a/frontend/src/app/main/data/workspace/comments.cljs +++ b/frontend/src/app/main/data/workspace/comments.cljs @@ -12,8 +12,9 @@ [app.common.geom.shapes :as gsh] [app.common.schema :as sm] [app.common.types.shape-tree :as ctst] + [app.main.data.changes :as dch] [app.main.data.comments :as dcm] - [app.main.data.workspace.changes :as dch] + [app.main.data.events :as ev] [app.main.data.workspace.common :as dwco] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.state-helpers :as wsh] @@ -59,8 +60,7 @@ (let [local (:comments-local state)] (cond (:draft local) (rx/of (dcm/close-thread)) - (:open local) (rx/of (dcm/close-thread)) - :else (rx/of #(dissoc % :workspace-drawing))))))) + (:open local) (rx/of (dcm/close-thread))))))) ;; Event responsible of the what should be executed when user clicked ;; on the comments layer. An option can be create a new draft thread, @@ -118,7 +118,8 @@ (rx/take 1) (rx/mapcat #(rx/of (center-to-comment-thread thread) (dwd/select-for-drawing :comments) - (dcm/open-thread thread))))))))) + (with-meta (dcm/open-thread thread) + {::ev/origin "workspace"}))))))))) (defn update-comment-thread-position ([thread [new-x new-y]] diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 063c1e50ac5c5d0e27745e92089f31a34403e6ec..58799510584dcb9abc153db2ab31f07e8c392613 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -6,14 +6,7 @@ (ns app.main.data.workspace.common (:require - [app.common.data.macros :as dm] [app.common.logging :as log] - [app.common.types.shape.layout :as ctl] - [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.state-helpers :as wsh] - [app.main.data.workspace.undo :as dwu] - [app.util.router :as rt] - [beicon.v2.core :as rx] [potok.v2.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module @@ -34,147 +27,15 @@ [e] (= e :interrupt)) -(defn- assure-valid-current-page - [] - (ptk/reify ::assure-valid-current-page - ptk/WatchEvent - (watch [_ state _] - (let [current_page (:current-page-id state) - pages (get-in state [:workspace-data :pages]) - exists? (some #(= current_page %) pages) - - project-id (:current-project-id state) - file-id (:current-file-id state) - pparams {:file-id file-id :project-id project-id} - qparams {:page-id (first pages)}] - (if exists? - (rx/empty) - (rx/of (rt/nav :workspace pparams qparams))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; UNDO ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(declare undo-to-index) - -;; These functions should've been in -;; `src/app/main/data/workspace/undo.cljs` but doing that causes a -;; circular dependency with `src/app/main/data/workspace/changes.cljs` - -(def undo - (ptk/reify ::undo - ptk/WatchEvent - (watch [it state _] - (let [objects (wsh/lookup-page-objects state) - edition (get-in state [:workspace-local :edition]) - drawing (get state :workspace-drawing)] - - ;; Editors handle their own undo's - (when (or (and (nil? edition) (nil? (:object drawing))) - (ctl/grid-layout? objects edition)) - (let [undo (:workspace-undo state) - items (:items undo) - index (or (:index undo) (dec (count items)))] - (when-not (or (empty? items) (= index -1)) - (let [item (get items index) - changes (:undo-changes item) - undo-group (:undo-group item) - - find-first-group-idx - (fn [index] - (if (= (dm/get-in items [index :undo-group]) undo-group) - (recur (dec index)) - (inc index))) - - undo-group-index - (when undo-group - (find-first-group-idx index))] - - (if undo-group - (rx/of (undo-to-index (dec undo-group-index))) - (rx/of (dwu/materialize-undo changes (dec index)) - (dch/commit-changes {:redo-changes changes - :undo-changes [] - :save-undo? false - :origin it}) - (assure-valid-current-page))))))))))) - -(def redo - (ptk/reify ::redo - ptk/WatchEvent - (watch [it state _] - (let [objects (wsh/lookup-page-objects state) - edition (get-in state [:workspace-local :edition]) - drawing (get state :workspace-drawing)] - (when (and (or (nil? edition) (ctl/grid-layout? objects edition)) - (or (empty? drawing) (= :curve (:tool drawing)))) - (let [undo (:workspace-undo state) - items (:items undo) - index (or (:index undo) (dec (count items)))] - (when-not (or (empty? items) (= index (dec (count items)))) - (let [item (get items (inc index)) - changes (:redo-changes item) - undo-group (:undo-group item) - find-last-group-idx (fn flgidx [index] - (let [item (get items index)] - (if (= (:undo-group item) undo-group) - (flgidx (inc index)) - (dec index)))) - - redo-group-index (when undo-group - (find-last-group-idx (inc index)))] - (if undo-group - (rx/of (undo-to-index redo-group-index)) - (rx/of (dwu/materialize-undo changes (inc index)) - (dch/commit-changes {:redo-changes changes - :undo-changes [] - :origin it - :save-undo? false}))))))))))) - -(defn undo-to-index - "Repeat undoing or redoing until dest-index is reached." - [dest-index] - (ptk/reify ::undo-to-index - ptk/WatchEvent - (watch [it state _] - (let [objects (wsh/lookup-page-objects state) - edition (get-in state [:workspace-local :edition]) - drawing (get state :workspace-drawing)] - (when-not (and (or (some? edition) (some? (:object drawing))) - (not (ctl/grid-layout? objects edition))) - (let [undo (:workspace-undo state) - items (:items undo) - index (or (:index undo) (dec (count items)))] - (when (and (some? items) - (<= -1 dest-index (dec (count items)))) - (let [changes (vec (apply concat - (cond - (< dest-index index) - (->> (subvec items (inc dest-index) (inc index)) - (reverse) - (map :undo-changes)) - (> dest-index index) - (->> (subvec items (inc index) (inc dest-index)) - (map :redo-changes)) - :else [])))] - (when (seq changes) - (rx/of (dwu/materialize-undo changes dest-index) - (dch/commit-changes {:redo-changes changes - :undo-changes [] - :origin it - :save-undo? false}))))))))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Toolbar ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn toggle-toolbar-visibility - [] - (ptk/reify ::toggle-toolbar-visibility - ptk/UpdateEvent - (update [_ state] - (update-in state [:workspace-local :hide-toolbar] not)))) - (defn hide-toolbar [] (ptk/reify ::hide-toolbar @@ -188,3 +49,10 @@ ptk/UpdateEvent (update [_ state] (assoc-in state [:workspace-local :hide-toolbar] false)))) + +(defn toggle-toolbar-visibility + [] + (ptk/reify ::toggle-toolbar-visibility + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-local :hide-toolbar] not)))) diff --git a/frontend/src/app/main/data/workspace/drawing.cljs b/frontend/src/app/main/data/workspace/drawing.cljs index c4c3a148ddd4f935d8c012592c4ede4f7846ee26..1b1d96beee3048184df371e337023a95281d53f2 100644 --- a/frontend/src/app/main/data/workspace/drawing.cljs +++ b/frontend/src/app/main/data/workspace/drawing.cljs @@ -28,6 +28,10 @@ ptk/UpdateEvent (update [_ state] (-> state + (update :workspace-layout (fn [workspace-layout] + (if (= tool :comments) + (disj workspace-layout :document-history) + workspace-layout))) (update :workspace-drawing assoc :tool tool) ;; When changing drawing tool disable "scale text" mode ;; automatically, to help users that ignore how this diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index 76a6c3ce8ff6d2340b5f3beb96400b34b585b9a0..a595a8c3fa0ae613aa248bd8786fffc8d5388ecc 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -78,13 +78,7 @@ (ptk/reify ::handle-drawing ptk/WatchEvent (watch [_ state stream] - (let [stopper (rx/merge - (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) - (->> stream - (rx/filter #(= % :interrupt)))) - + (let [stopper (mse/drag-stopper stream) layout (get state :workspace-layout) zoom (dm/get-in state [:workspace-local :zoom] 1) diff --git a/frontend/src/app/main/data/workspace/drawing/curve.cljs b/frontend/src/app/main/data/workspace/drawing/curve.cljs index 8a21949622abfd03c7074f8fcfd8ceda51194f33..5c0d98898e4bf0d6b09e2cce0d9fa96cfc05b917 100644 --- a/frontend/src/app/main/data/workspace/drawing/curve.cljs +++ b/frontend/src/app/main/data/workspace/drawing/curve.cljs @@ -28,11 +28,6 @@ (def simplify-tolerance 0.3) -(defn stopper-event? - [{:keys [type] :as event}] - (and (mse/mouse-event? event) - (= type :up))) - (defn- insert-point [point] (ptk/reify ::insert-point @@ -104,13 +99,13 @@ (ptk/reify ::handle-drawing ptk/WatchEvent (watch [_ _ stream] - (let [stopper (rx/filter stopper-event? stream) - mouse (rx/sample 10 ms/mouse-position) - shape (cts/setup-shape {:type :path - :initialized? true - :frame-id uuid/zero - :parent-id uuid/zero - :segments []})] + (let [stopper (mse/drag-stopper stream) + mouse (rx/sample 10 ms/mouse-position) + shape (cts/setup-shape {:type :path + :initialized? true + :frame-id uuid/zero + :parent-id uuid/zero + :segments []})] (rx/concat (rx/of #(update % :workspace-drawing assoc :object shape)) (->> mouse diff --git a/frontend/src/app/main/data/workspace/fix_bool_contents.cljs b/frontend/src/app/main/data/workspace/fix_bool_contents.cljs deleted file mode 100644 index 8d0ac516eab335a14bca15c21a99f7cac62dfaae..0000000000000000000000000000000000000000 --- a/frontend/src/app/main/data/workspace/fix_bool_contents.cljs +++ /dev/null @@ -1,94 +0,0 @@ -;; 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 app.main.data.workspace.fix-bool-contents - (:require - [app.common.data :as d] - [app.common.geom.shapes :as gsh] - [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.state-helpers :as wsh] - [beicon.v2.core :as rx] - [potok.v2.core :as ptk])) - -;; This event will update the file so the boolean data has a pre-generated path data -;; to increase performance. -;; For new shapes this will be generated in the :reg-objects but we need to do this for -;; old files. - -;; FIXME: Remove me after June 2022 - -(defn fix-bool-contents - "This event will calculate the bool content and update the page. This is kind of a 'addhoc' migration - to fill the optional value 'bool-content'" - [] - - (letfn [(should-migrate-shape? [shape] - (and (= :bool (:type shape)) (not (contains? shape :bool-content)))) - - (should-migrate-component? [component] - (->> (:objects component) - (vals) - (d/seek should-migrate-shape?))) - - (update-shape [shape objects] - (cond-> shape - (should-migrate-shape? shape) - (assoc :bool-content (gsh/calc-bool-content shape objects)))) - - (migrate-component [component] - (-> component - (update - :objects - (fn [objects] - (d/mapm #(update-shape %2 objects) objects))))) - - (update-library - [library] - (-> library - (d/update-in-when - [:data :components] - (fn [components] - (d/mapm #(migrate-component %2) components)))))] - - (ptk/reify ::fix-bool-contents - ptk/UpdateEvent - (update [_ state] - ;; Update (only-local) the imported libraries - (-> state - (d/update-when - :workspace-libraries - (fn [libraries] (d/mapm #(update-library %2) libraries))))) - - ptk/WatchEvent - (watch [it state _] - (let [objects (wsh/lookup-page-objects state) - - ids (into #{} - (comp (filter should-migrate-shape?) (map :id)) - (vals objects)) - - components (->> (wsh/lookup-local-components state) - (vals) - (filter should-migrate-component?)) - - component-changes - (into [] - (map (fn [component] - {:type :mod-component - :id (:id component) - :objects (-> component migrate-component :objects)})) - components)] - - (rx/of (dch/update-shapes ids #(update-shape % objects) {:reg-objects? false - :save-undo? false - :ignore-tree true})) - - (if (empty? component-changes) - (rx/empty) - (rx/of (dch/commit-changes {:origin it - :redo-changes component-changes - :undo-changes [] - :save-undo? false})))))))) diff --git a/frontend/src/app/main/data/workspace/fix_broken_shapes.cljs b/frontend/src/app/main/data/workspace/fix_broken_shapes.cljs index c110de09b0ff9e9db406b6a47f9b5f244106d208..ae19af68a4df1db69577a3b2ab11b7cfc98a5d04 100644 --- a/frontend/src/app/main/data/workspace/fix_broken_shapes.cljs +++ b/frontend/src/app/main/data/workspace/fix_broken_shapes.cljs @@ -6,7 +6,7 @@ (ns app.main.data.workspace.fix-broken-shapes (:require - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dch] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) diff --git a/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs b/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs index 31ec4176efef212cb8ba9bd8c9e9502f1d199396..75f7c83d2f06ea5312e90d877c823660c85d540a 100644 --- a/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs +++ b/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs @@ -6,10 +6,9 @@ (ns app.main.data.workspace.fix-deleted-fonts (:require - [app.common.data :as d] [app.common.files.helpers :as cfh] [app.common.text :as txt] - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dwc] [app.main.data.workspace.state-helpers :as wsh] [app.main.fonts :as fonts] [beicon.v2.core :as rx] @@ -21,14 +20,7 @@ ;; - Moving files from one team to another in the same instance ;; - Custom fonts are explicitly deleted in the team area -(defn has-invalid-font-family - [node] - (let [fonts (deref fonts/fontsdb)] - (and - (some? (:font-family node)) - (nil? (get fonts (:font-id node)))))) - -(defn calculate-alternative-font-id +(defn- calculate-alternative-font-id [value] (let [fonts (deref fonts/fontsdb)] (->> (vals fonts) @@ -36,39 +28,44 @@ (first) :id))) -(defn should-fix-deleted-font-shape? +(defn- has-invalid-font-family? + [node] + (let [fonts (deref fonts/fontsdb) + font-family (:font-family node) + alternative-font-id (calculate-alternative-font-id font-family)] + (and (some? font-family) + (nil? (get fonts (:font-id node))) + (some? alternative-font-id)))) + +(defn- should-fix-deleted-font-shape? [shape] (let [text-nodes (txt/node-seq txt/is-text-node? (:content shape))] - (and (cfh/text-shape? shape) (some has-invalid-font-family text-nodes)))) + (and (cfh/text-shape? shape) + (some has-invalid-font-family? text-nodes)))) -(defn should-fix-deleted-font-component? +(defn- should-fix-deleted-font-component? [component] - (->> (:objects component) - (vals) - (d/seek should-fix-deleted-font-shape?))) + (let [xf (comp (map val) + (filter should-fix-deleted-font-shape?))] + (first (sequence xf (:objects component))))) -(defn should-fix-deleted-font-typography? - [typography] - (let [fonts (deref fonts/fontsdb)] - (nil? (get fonts (:font-id typography))))) - -(defn fix-deleted-font +(defn- fix-deleted-font [node] (let [alternative-font-id (calculate-alternative-font-id (:font-family node))] (cond-> node (some? alternative-font-id) (assoc :font-id alternative-font-id)))) -(defn fix-deleted-font-shape +(defn- fix-deleted-font-shape [shape] - (let [transform (partial txt/transform-nodes has-invalid-font-family fix-deleted-font)] + (let [transform (partial txt/transform-nodes has-invalid-font-family? fix-deleted-font)] (update shape :content transform))) -(defn fix-deleted-font-component +(defn- fix-deleted-font-component [component] (update component :objects (fn [objects] - (d/mapm #(fix-deleted-font-shape %2) objects)))) + (update-vals objects fix-deleted-font-shape)))) (defn fix-deleted-font-typography [typography] @@ -76,54 +73,60 @@ (cond-> typography (some? alternative-font-id) (assoc :font-id alternative-font-id)))) +(defn- generate-deleted-font-shape-changes + [{:keys [objects id]}] + (sequence + (comp (map val) + (filter should-fix-deleted-font-shape?) + (map (fn [shape] + {:type :mod-obj + :id (:id shape) + :page-id id + :operations [{:type :set + :attr :content + :val (:content (fix-deleted-font-shape shape))} + {:type :set + :attr :position-data + :val nil}]}))) + objects)) + +(defn- generate-deleted-font-components-changes + [state] + (sequence + (comp (map val) + (filter should-fix-deleted-font-component?) + (map (fn [component] + {:type :mod-component + :id (:id component) + :objects (-> (fix-deleted-font-component component) :objects)}))) + (wsh/lookup-local-components state))) + +(defn- generate-deleted-font-typography-changes + [state] + (sequence + (comp (map val) + (filter has-invalid-font-family?) + (map (fn [typography] + {:type :mod-typography + :typography (fix-deleted-font-typography typography)}))) + (get-in state [:workspace-data :typographies]))) + (defn fix-deleted-fonts [] (ptk/reify ::fix-deleted-fonts ptk/WatchEvent (watch [it state _] - (let [objects (wsh/lookup-page-objects state) - - ids (into #{} - (comp (filter should-fix-deleted-font-shape?) (map :id)) - (vals objects)) - - components (->> (wsh/lookup-local-components state) - (vals) - (filter should-fix-deleted-font-component?)) - - component-changes - (into [] - (map (fn [component] - {:type :mod-component - :id (:id component) - :objects (-> (fix-deleted-font-component component) :objects)})) - components) - - typographies (->> (get-in state [:workspace-data :typographies]) - (vals) - (filter should-fix-deleted-font-typography?)) - - typography-changes - (into [] - (map (fn [typography] - {:type :mod-typography - :typography (fix-deleted-font-typography typography)})) - typographies)] - - (rx/concat - (rx/of (dch/update-shapes ids #(fix-deleted-font-shape %) {:reg-objects? false - :save-undo? false - :ignore-tree true})) - (if (empty? component-changes) - (rx/empty) - (rx/of (dch/commit-changes {:origin it - :redo-changes component-changes - :undo-changes [] - :save-undo? false}))) - - (if (empty? typography-changes) - (rx/empty) - (rx/of (dch/commit-changes {:origin it - :redo-changes typography-changes - :undo-changes [] - :save-undo? false})))))))) + (let [data (get state :workspace-data) + shape-changes (mapcat generate-deleted-font-shape-changes (vals (:pages-index data))) + components-changes (generate-deleted-font-components-changes state) + typography-changes (generate-deleted-font-typography-changes state) + changes (concat shape-changes + components-changes + typography-changes)] + (if (seq changes) + (rx/of (dwc/commit-changes + {:origin it + :redo-changes (vec changes) + :undo-changes [] + :save-undo? false})) + (rx/empty)))))) diff --git a/frontend/src/app/main/data/workspace/grid.cljs b/frontend/src/app/main/data/workspace/grid.cljs index 50a235107136feba271ce1cbec2159fd44cf0c48..beaff9e61b9b8293018993707c9db9bd04c26ce9 100644 --- a/frontend/src/app/main/data/workspace/grid.cljs +++ b/frontend/src/app/main/data/workspace/grid.cljs @@ -10,7 +10,8 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dch] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -51,8 +52,8 @@ grid {:type :square :params params :display true}] - (rx/of (dch/update-shapes [frame-id] - (fn [obj] (update obj :grids (fnil #(conj % grid) []))))))))) + (rx/of (dwsh/update-shapes [frame-id] + (fn [obj] (update obj :grids (fnil #(conj % grid) []))))))))) (defn remove-frame-grid @@ -60,14 +61,14 @@ (ptk/reify ::remove-frame-grid ptk/WatchEvent (watch [_ _ _] - (rx/of (dch/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) [])))))))) + (rx/of (dwsh/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) [])))))))) (defn set-frame-grid [frame-id index data] (ptk/reify ::set-frame-grid ptk/WatchEvent (watch [_ _ _] - (rx/of (dch/update-shapes [frame-id] #(assoc-in % [:grids index] data)))))) + (rx/of (dwsh/update-shapes [frame-id] #(assoc-in % [:grids index] data)))))) (defn set-default-grid [type params] diff --git a/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs b/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs index 8d76c38caad84e2dc65ab91d7a1e84bbae323232..a0b71b509dd643c8c493dce42902cb76e4aadf25 100644 --- a/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/grid_layout/shortcuts.cljs @@ -8,7 +8,7 @@ (:require [app.main.data.shortcuts :as ds] [app.main.data.workspace :as dw] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.undo :as dwu] [app.main.store :as st] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -38,11 +38,11 @@ :undo {:tooltip (ds/meta "Z") :command (ds/c-mod "z") - :fn #(st/emit! dwc/undo)} + :fn #(st/emit! dwu/undo)} :redo {:tooltip (ds/meta "Y") :command [(ds/c-mod "shift+z") (ds/c-mod "y")] - :fn #(st/emit! dwc/redo)} + :fn #(st/emit! dwu/redo)} ;; ZOOM diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index c3a4032694de3f441718436d4b1d26d1bb430304..04beaa5535e3b943eca03c49959bf9b512614f05 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -15,7 +15,8 @@ [app.common.types.container :as ctn] [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] - [app.main.data.workspace.changes :as dch] + [app.common.uuid :as uuid] + [app.main.data.changes :as dch] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] @@ -68,7 +69,7 @@ result))))))) (defn prepare-create-group - [it objects page-id shapes base-name keep-name?] + [changes id objects page-id shapes base-name keep-name?] (let [frame-id (:frame-id (first shapes)) parent-id (:parent-id (first shapes)) gname (if (and keep-name? @@ -84,7 +85,8 @@ (cfh/get-position-on-parent objects) inc) - group (cts/setup-shape {:type :group + group (cts/setup-shape {:id id + :type :group :name gname :shapes (mapv :id shapes) :selrect selrect @@ -114,7 +116,8 @@ (filter (partial ctl/grid-layout? objects))) shapes) - changes (-> (pcb/empty-changes it page-id) + changes (-> changes + (pcb/with-page-id page-id) (pcb/with-objects objects) (pcb/add-object group {:index group-idx}) (pcb/update-shapes (map :id shapes) ctl/remove-layout-item-data) @@ -143,18 +146,12 @@ (map-indexed vector) (filter #(#{(:id group)} (second %))) (ffirst) - inc) - - ;; Shapes that are in a component (including root) must be detached, - ;; because cannot be easyly synchronized back to the main component. - shapes-to-detach (filter ctk/in-component-copy? - (cfh/get-children-with-self objects (:id group)))] + inc)] (-> (pcb/empty-changes it page-id) (pcb/with-objects objects) (pcb/change-parent parent-id children index-in-parent) - (pcb/remove-objects [(:id group)]) - (pcb/update-shapes (map :id shapes-to-detach) ctk/detach-shape)))) + (pcb/remove-objects [(:id group)])))) (defn remove-frame-changes [it page-id frame objects] @@ -178,30 +175,44 @@ ;; GROUPS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(def group-selected - (ptk/reify ::group-selected +(defn group-shapes + [id ids & {:keys [change-selection?] :or {change-selection? false}}] + (ptk/reify ::group-shapes ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) + (let [id (d/nilv id (uuid/next)) + page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - selected (->> (wsh/lookup-selected state) - (cfh/clean-loops objects) - (remove #(ctn/has-any-copy-parent? objects (get objects %)))) - shapes (shapes-for-grouping objects selected) + + shapes + (->> ids + (cfh/clean-loops objects) + (remove #(ctn/has-any-copy-parent? objects (get objects %))) + (shapes-for-grouping objects)) parents (into #{} (map :parent-id) shapes)] (when-not (empty? shapes) (let [[group changes] - (prepare-create-group it objects page-id shapes "Group" false)] + (prepare-create-group (pcb/empty-changes it) id objects page-id shapes "Group" false)] (rx/of (dch/commit-changes changes) - (dws/select-shapes (d/ordered-set (:id group))) + (when change-selection? + (dws/select-shapes (d/ordered-set (:id group)))) (ptk/data-event :layout/update {:ids parents})))))))) -(def ungroup-selected - (ptk/reify ::ungroup-selected +(defn group-selected + [] + (ptk/reify ::group-selected + ptk/WatchEvent + (watch [_ state _] + (let [selected (wsh/lookup-selected state)] + (rx/of (group-shapes nil selected :change-selection? true)))))) + +(defn ungroup-shapes + [ids & {:keys [change-selection?] :or {change-selection? false}}] + (ptk/reify ::ungroup-shapes ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) prepare (fn [shape-id] @@ -218,99 +229,114 @@ (ctl/grid-layout? objects (:parent-id shape)) (pcb/update-shapes [(:parent-id shape)] ctl/assign-cells {:with-objects? true})))) - selected (->> (wsh/lookup-selected state) - (remove #(ctn/has-any-copy-parent? objects (get objects %))) - ;; components can't be ungrouped - (remove #(ctk/instance-head? (get objects %)))) - changes-list (sequence - (keep prepare) - selected) + ids (->> ids + (remove #(ctn/has-any-copy-parent? objects (get objects %))) + ;; components can't be ungrouped + (remove #(ctk/instance-head? (get objects %)))) + + changes-list (sequence (keep prepare) ids) parents (into #{} (comp (map #(cfh/get-parent objects %)) (keep :id)) - selected) + ids) child-ids (into (d/ordered-set) (mapcat #(dm/get-in objects [% :shapes])) - selected) + ids) changes {:redo-changes (vec (mapcat :redo-changes changes-list)) :undo-changes (vec (mapcat :undo-changes changes-list)) :origin it} undo-id (js/Symbol)] - (when-not (empty? selected) + (when-not (empty? ids) (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (ptk/data-event :layout/update {:ids parents}) (dwu/commit-undo-transaction undo-id) - (dws/select-shapes child-ids))))))) + (when change-selection? + (dws/select-shapes child-ids)))))))) -(def mask-group - (ptk/reify ::mask-group - ptk/WatchEvent - (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - selected (->> (wsh/lookup-selected state) - (cfh/clean-loops objects) - (remove #(ctn/has-any-copy-parent? objects (get objects %)))) - shapes (shapes-for-grouping objects selected) - first-shape (first shapes)] - (when-not (empty? shapes) - (let [;; If the selected shape is a group, we can use it. If not, - ;; create a new group and set it as masked. - [group changes] - (if (and (= (count shapes) 1) - (= (:type (first shapes)) :group)) - [first-shape (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects))] - (prepare-create-group it objects page-id shapes "Mask" true)) - - changes (-> changes - (pcb/update-shapes (:shapes group) - (fn [shape] - (assoc shape - :constraints-h :scale - :constraints-v :scale))) - (pcb/update-shapes [(:id group)] - (fn [group] - (assoc group - :masked-group true - :selrect (:selrect first-shape) - :points (:points first-shape) - :transform (:transform first-shape) - :transform-inverse (:transform-inverse first-shape)))) - (pcb/resize-parents [(:id group)])) - undo-id (js/Symbol)] - - (rx/of (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (dws/select-shapes (d/ordered-set (:id group))) - (ptk/data-event :layout/update {:ids [(:id group)]}) - (dwu/commit-undo-transaction undo-id)))))))) - -(def unmask-group - (ptk/reify ::unmask-group +(defn ungroup-selected + [] + (ptk/reify ::ungroup-selected ptk/WatchEvent - (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - - masked-groups (->> (wsh/lookup-selected state) - (map #(get objects %)) - (filter #(or (= :bool (:type %)) (= :group (:type %))))) - - changes (reduce (fn [changes mask] - (-> changes - (pcb/update-shapes [(:id mask)] - (fn [shape] - (dissoc shape :masked-group))) - (pcb/resize-parents [(:id mask)]))) - (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects)) - masked-groups)] - - (rx/of (dch/commit-changes changes)))))) + (watch [_ state _] + (let [selected (wsh/lookup-selected state)] + (rx/of (ungroup-shapes selected :change-selection? true)))))) + +(defn mask-group + ([] + (mask-group nil)) + ([ids] + (ptk/reify ::mask-group + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + selected (->> (or ids (wsh/lookup-selected state)) + (cfh/clean-loops objects) + (remove #(ctn/has-any-copy-parent? objects (get objects %)))) + shapes (shapes-for-grouping objects selected) + first-shape (first shapes)] + (when-not (empty? shapes) + (let [;; If the selected shape is a group, we can use it. If not, + ;; create a new group and set it as masked. + [group changes] + (if (and (= (count shapes) 1) + (= (:type (first shapes)) :group)) + [first-shape (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects))] + (prepare-create-group (pcb/empty-changes it) (uuid/next) objects page-id shapes "Mask" true)) + + changes (-> changes + (pcb/update-shapes (:shapes group) + (fn [shape] + (assoc shape + :constraints-h :scale + :constraints-v :scale))) + (pcb/update-shapes [(:id group)] + (fn [group] + (assoc group + :masked-group true + :selrect (:selrect first-shape) + :points (:points first-shape) + :transform (:transform first-shape) + :transform-inverse (:transform-inverse first-shape)))) + (pcb/resize-parents [(:id group)])) + undo-id (js/Symbol)] + + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (dws/select-shapes (d/ordered-set (:id group))) + (ptk/data-event :layout/update {:ids [(:id group)]}) + (dwu/commit-undo-transaction undo-id))))))))) + +(defn unmask-group + ([] + (unmask-group nil)) + + ([ids] + (ptk/reify ::unmask-group + ptk/WatchEvent + (watch [it state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + + masked-groups (->> (d/nilv ids (wsh/lookup-selected state)) + (map #(get objects %)) + (filter #(or (= :bool (:type %)) (= :group (:type %))))) + + changes (reduce (fn [changes mask] + (-> changes + (pcb/update-shapes [(:id mask)] + (fn [shape] + (dissoc shape :masked-group))) + (pcb/resize-parents [(:id mask)]))) + (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects)) + masked-groups)] + + (rx/of (dch/commit-changes changes))))))) diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index 2229210103cbd0965dc6ba5b604182bbe259eb06..4e2895bb20286e49ee38a5023a00fa51d760d347 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -11,23 +11,30 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.types.page :as ctp] - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dwc] + [app.main.data.events :as ev] [app.main.data.workspace.state-helpers :as wsh] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) -(defn make-update-guide [guide] +(defn make-update-guide + [guide] (fn [other] (cond-> other (= (:id other) (:id guide)) (merge guide)))) -(defn update-guides [guide] +(defn update-guides + [guide] (dm/assert! "expected valid guide" (ctp/check-page-guide! guide)) (ptk/reify ::update-guides + ev/Event + (-data [_] + (assoc guide ::ev/name "update-guide")) + ptk/WatchEvent (watch [it state _] (let [page (wsh/lookup-page state) @@ -35,19 +42,22 @@ (-> (pcb/empty-changes it) (pcb/with-page page) (pcb/update-page-option :guides assoc (:id guide) guide))] - (rx/of (dch/commit-changes changes)))))) + (rx/of (dwc/commit-changes changes)))))) -(defn remove-guide [guide] +(defn remove-guide + [guide] (dm/assert! "expected valid guide" (ctp/check-page-guide! guide)) (ptk/reify ::remove-guide + ev/Event + (-data [_] guide) + ptk/UpdateEvent (update [_ state] (let [sdisj (fnil disj #{})] - (-> state - (update-in [:workspace-guides :hover] sdisj (:id guide))))) + (update-in state [:workspace-guides :hover] sdisj (:id guide)))) ptk/WatchEvent (watch [it state _] @@ -56,7 +66,7 @@ (-> (pcb/empty-changes it) (pcb/with-page page) (pcb/update-page-option :guides dissoc (:id guide)))] - (rx/of (dch/commit-changes changes)))))) + (rx/of (dwc/commit-changes changes)))))) (defn remove-guides [ids] @@ -69,20 +79,21 @@ (rx/from (->> guides (mapv #(remove-guide %)))))))) (defmethod ptk/resolve ::move-frame-guides - [_ ids] + [_ args] (dm/assert! "expected a coll of uuids" - (every? uuid? ids)) + (every? uuid? (:ids args))) (ptk/reify ::move-frame-guides ptk/WatchEvent (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) + (let [ids (:ids args) + object-modifiers (:modifiers args) + + objects (wsh/lookup-page-objects state) is-frame? (fn [id] (= :frame (get-in objects [id :type]))) frame-ids? (into #{} (filter is-frame?) ids) - object-modifiers (get state :workspace-modifiers) - build-move-event (fn [guide] (let [frame (get objects (:frame-id guide)) diff --git a/frontend/src/app/main/data/workspace/interactions.cljs b/frontend/src/app/main/data/workspace/interactions.cljs index 5708ca3c455db30b79774e02f3fab0d2a40a8510..2fb10ada825d95f4d4d37ef1192dcab8f1ee4952 100644 --- a/frontend/src/app/main/data/workspace/interactions.cljs +++ b/frontend/src/app/main/data/workspace/interactions.cljs @@ -11,11 +11,13 @@ [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] + [app.common.logic.shapes :as cls] [app.common.types.page :as ctp] [app.common.types.shape-tree :as ctst] [app.common.types.shape.interactions :as ctsi] [app.common.uuid :as uuid] - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dch] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.streams :as ms] @@ -26,29 +28,33 @@ ;; --- Flows (defn add-flow - [starting-frame] + ([starting-frame] + (add-flow nil nil nil starting-frame)) - (dm/assert! - "expect uuid" - (uuid? starting-frame)) + ([flow-id page-id name starting-frame] + (dm/assert! + "expect uuid" + (uuid? starting-frame)) - (ptk/reify ::add-flow - ptk/WatchEvent - (watch [it state _] - (let [page (wsh/lookup-page state) + (ptk/reify ::add-flow + ptk/WatchEvent + (watch [it state _] + (let [page (if page-id + (wsh/lookup-page state page-id) + (wsh/lookup-page state)) - flows (get-in page [:options :flows] []) - unames (cfh/get-used-names flows) - name (cfh/generate-unique-name unames "Flow 1") + flows (get-in page [:options :flows] []) + unames (cfh/get-used-names flows) + name (or name (cfh/generate-unique-name unames "Flow 1")) - new-flow {:id (uuid/next) - :name name - :starting-frame starting-frame}] + new-flow {:id (or flow-id (uuid/next)) + :name name + :starting-frame starting-frame}] - (rx/of (dch/commit-changes - (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/update-page-option :flows ctp/add-flow new-flow)))))))) + (rx/of (dch/commit-changes + (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/update-page-option :flows ctp/add-flow new-flow))))))))) (defn add-flow-selected-frame [] @@ -59,16 +65,35 @@ (rx/of (add-flow (first selected))))))) (defn remove-flow - [flow-id] + ([flow-id] + (remove-flow nil flow-id)) + + ([page-id flow-id] + (dm/assert! (uuid? flow-id)) + (ptk/reify ::remove-flow + ptk/WatchEvent + (watch [it state _] + (let [page (if page-id + (wsh/lookup-page state page-id) + (wsh/lookup-page state))] + (rx/of (dch/commit-changes + (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/update-page-option :flows ctp/remove-flow flow-id))))))))) + +(defn update-flow + [page-id flow-id update-fn] (dm/assert! (uuid? flow-id)) - (ptk/reify ::remove-flow + (ptk/reify ::update-flow ptk/WatchEvent (watch [it state _] - (let [page (wsh/lookup-page state)] + (let [page (if page-id + (wsh/lookup-page state page-id) + (wsh/lookup-page state))] (rx/of (dch/commit-changes (-> (pcb/empty-changes it) (pcb/with-page page) - (pcb/update-page-option :flows ctp/remove-flow flow-id)))))))) + (pcb/update-page-option :flows ctp/update-flow flow-id update-fn)))))))) (defn rename-flow [flow-id name] @@ -109,6 +134,18 @@ (or (some ctsi/flow-origin? (map :interactions children)) (some #(ctsi/flow-to? % frame-id) (map :interactions (vals objects)))))) +(defn add-interaction + [page-id shape-id interaction] + (ptk/reify ::add-interaction + ptk/WatchEvent + (watch [_ state _] + (let [page-id (or page-id (:current-page-id state))] + (rx/of (dwsh/update-shapes + [shape-id] + (fn [shape] + (cls/add-new-interaction shape interaction)) + {:page-id page-id})))))) + (defn add-new-interaction ([shape] (add-new-interaction shape nil)) ([shape destination] @@ -125,36 +162,40 @@ :flows] []) flow (ctp/get-frame-flow flows (:id frame))] (rx/concat - (rx/of (dch/update-shapes [(:id shape)] - (fn [shape] - (let [new-interaction (-> ctsi/default-interaction - (ctsi/set-destination destination) - (assoc :position-relative-to (:id shape)))] - (update shape :interactions - ctsi/add-interaction new-interaction))))) + (rx/of (dwsh/update-shapes [(:id shape)] + (fn [shape] + (let [new-interaction (-> ctsi/default-interaction + (ctsi/set-destination destination) + (assoc :position-relative-to (:id shape)))] + (cls/add-new-interaction shape new-interaction))))) (when (and (not (connected-frame? objects (:id frame))) (nil? flow)) (rx/of (add-flow (:id frame)))))))))) (defn remove-interaction - [shape index] - (ptk/reify ::remove-interaction - ptk/WatchEvent - (watch [_ _ _] - (rx/of (dch/update-shapes [(:id shape)] - (fn [shape] - (update shape :interactions - ctsi/remove-interaction index))))))) - + ([shape index] + (remove-interaction nil shape index)) + ([page-id shape index] + (ptk/reify ::remove-interaction + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dwsh/update-shapes [(:id shape)] + (fn [shape] + (update shape :interactions + ctsi/remove-interaction index)) + {:page-id page-id})))))) (defn update-interaction - [shape index update-fn] - (ptk/reify ::update-interaction - ptk/WatchEvent - (watch [_ _ _] - (rx/of (dch/update-shapes [(:id shape)] - (fn [shape] - (update shape :interactions - ctsi/update-interaction index update-fn))))))) + ([shape index update-fn] + (update-interaction shape index update-fn nil)) + ([shape index update-fn options] + (ptk/reify ::update-interaction + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dwsh/update-shapes [(:id shape)] + (fn [shape] + (update shape :interactions + ctsi/update-interaction index update-fn)) + options)))))) (defn remove-all-interactions-nav-to "Remove all interactions that navigate to the given frame." @@ -171,9 +212,9 @@ new-interactions (ctsi/remove-interactions #(ctsi/navs-to? % frame-id) interactions)] (when (not= (count interactions) (count new-interactions)) - (dch/update-shapes [(:id shape)] - (fn [shape] - (assoc shape :interactions new-interactions))))))] + (dwsh/update-shapes [(:id shape)] + (fn [shape] + (assoc shape :interactions new-interactions))))))] (rx/from (->> (vals objects) (map remove-interactions-shape) @@ -193,9 +234,7 @@ (watch [_ state stream] (let [initial-pos @ms/mouse-position selected (wsh/lookup-selected state) - stopper (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?))] + stopper (mse/drag-stopper stream)] (when (= 1 (count selected)) (rx/concat (->> ms/mouse-position @@ -262,20 +301,20 @@ (dwu/start-undo-transaction undo-id) (when (:hide-in-viewer target-frame) - ; If the target frame is hidden, we need to unhide it so - ; users can navigate to it. - (dch/update-shapes [(:id target-frame)] - #(dissoc % :hide-in-viewer))) + ;; If the target frame is hidden, we need to unhide it so + ;; users can navigate to it. + (dwsh/update-shapes [(:id target-frame)] + #(dissoc % :hide-in-viewer))) (cond (or (nil? shape) - ;; Didn't changed the position for the interaction + ;; Didn't changed the position for the interaction (= position initial-pos) - ;; New interaction but invalid target + ;; New interaction but invalid target (and (nil? index) (nil? target-frame))) nil - ;; Dropped interaction in an invalid target. We remove it + ;; Dropped interaction in an invalid target. We remove it (and (some? index) (nil? target-frame)) (remove-interaction shape index) @@ -305,9 +344,7 @@ (watch [_ state stream] (let [initial-pos @ms/mouse-position selected (wsh/lookup-selected state) - stopper (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?))] + stopper (mse/drag-stopper stream)] (when (= 1 (count selected)) (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) @@ -368,5 +405,5 @@ (update interactions index #(ctsi/set-overlay-position % overlay-pos))] - (rx/of (dch/update-shapes [(:id shape)] #(merge % {:interactions new-interactions}))))))) + (rx/of (dwsh/update-shapes [(:id shape)] #(merge % {:interactions new-interactions}))))))) diff --git a/frontend/src/app/main/data/workspace/layers.cljs b/frontend/src/app/main/data/workspace/layers.cljs index afde3c03a03bff8a242665b4706d745b6d6f7420..3425a16a4f561c8e861f8235cb8f0eff9acbc712 100644 --- a/frontend/src/app/main/data/workspace/layers.cljs +++ b/frontend/src/app/main/data/workspace/layers.cljs @@ -9,7 +9,7 @@ (:require [app.common.data :as d] [app.common.math :as mth] - [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [beicon.v2.core :as rx] [cuerdas.core :as str] @@ -48,7 +48,7 @@ shapes (map #(get objects %) selected) shapes-ids (->> shapes (map :id))] - (rx/of (dch/update-shapes shapes-ids #(assoc % :opacity opacity))))))) + (rx/of (dwsh/update-shapes shapes-ids #(assoc % :opacity opacity))))))) (defn pressed-opacity [opacity] diff --git a/frontend/src/app/main/data/workspace/layout.cljs b/frontend/src/app/main/data/workspace/layout.cljs index 24cfc6779dd9cc8e61766138516972344f49472b..6b17a1e7ff337d948111fdb45c0f162c9ac19e7c 100644 --- a/frontend/src/app/main/data/workspace/layout.cljs +++ b/frontend/src/app/main/data/workspace/layout.cljs @@ -20,15 +20,16 @@ :comments :assets :document-history + :hide-palettes :colorpalette :element-options :rulers - :display-grid - :snap-grid + :display-guides + :snap-guides :scale-text :dynamic-alignment :display-artboard-names - :snap-guides + :snap-ruler-guides :show-pixel-grid :snap-pixel-grid}) @@ -53,11 +54,11 @@ :layers :element-options :rulers - :display-grid - :snap-grid + :display-guides + :snap-guides :dynamic-alignment :display-artboard-names - :snap-guides + :snap-ruler-guides :show-pixel-grid :snap-pixel-grid}) @@ -82,8 +83,8 @@ (defn toggle-layout-flag [flag & {:keys [force?] :as opts}] (ptk/reify ::toggle-layout-flag - IDeref - (-deref [_] {:name flag}) + ev/Event + (-data [_] {:name flag}) ptk/UpdateEvent (update [_ state] @@ -134,7 +135,8 @@ "A map of layout flags that should be persisted in local storage; the value corresponds to the key that will be used for save the data in storage object. It should be namespace qualified." - {:colorpalette :app.main.data.workspace/show-colorpalette? + {:hide-palettes :app.main.data.workspace/hide-palettes? + :colorpalette :app.main.data.workspace/show-colorpalette? :textpalette :app.main.data.workspace/show-textpalette?}) (defn load-layout-flags diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index c27c00b0fd65a292ecf1bc8224b24c78c913b458..a6c6cb8b336bfbd6d9799580e5c54b9c0112e83a 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -11,10 +11,11 @@ [app.common.files.changes :as ch] [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] - [app.common.files.libraries-helpers :as cflh] [app.common.files.shapes-helpers :as cfsh] [app.common.geom.point :as gpt] [app.common.logging :as log] + [app.common.logic.libraries :as cll] + [app.common.logic.shapes :as cls] [app.common.types.color :as ctc] [app.common.types.component :as ctk] [app.common.types.components-list :as ctkl] @@ -23,13 +24,14 @@ [app.common.types.shape.layout :as ctl] [app.common.types.typography :as ctt] [app.common.uuid :as uuid] + [app.config :as cf] + [app.main.data.changes :as dch] + [app.main.data.comments :as dc] [app.main.data.events :as ev] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] [app.main.data.workspace :as-alias dw] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.groups :as dwg] - [app.main.data.workspace.libraries-helpers :as dwlh] [app.main.data.workspace.notifications :as-alias dwn] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shapes :as dwsh] @@ -46,6 +48,7 @@ [app.util.color :as uc] [app.util.i18n :refer [tr]] [app.util.router :as rt] + [app.util.storage :as s] [app.util.time :as dt] [beicon.v2.core :as rx] [cuerdas.core :as str] @@ -54,6 +57,12 @@ ;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default (log/set-level! :warn) +(defn- pretty-file + [file-id state] + (if (= file-id (:current-file-id state)) + "" + (str "<" (get-in state [:workspace-libraries file-id :name]) ">"))) + (defn- log-changes [changes file] (let [extract-change @@ -99,37 +108,46 @@ (assoc item :path path :name name)))) (defn add-color - [color] - (let [id (uuid/next) - color (-> color - (assoc :id id) - (assoc :name (or (get-in color [:image :name]) - (:color color) - (uc/gradient-type->string (get-in color [:gradient :type])))))] - (dm/assert! ::ctc/color color) - (ptk/reify ::add-color - IDeref - (-deref [_] color) - - ptk/WatchEvent - (watch [it _ _] - (let [changes (-> (pcb/empty-changes it) - (pcb/add-color color))] - (rx/of #(assoc-in % [:workspace-local :color-for-rename] id) - (dch/commit-changes changes))))))) + ([color] + (add-color color nil)) + + ([color {:keys [rename?] :or {rename? true}}] + (let [color (-> color + (update :id #(or % (uuid/next))) + (assoc :name (or (get-in color [:image :name]) + (:color color) + (uc/gradient-type->string (get-in color [:gradient :type])))))] + (dm/assert! ::ctc/color color) + (ptk/reify ::add-color + ev/Event + (-data [_] color) + + ptk/WatchEvent + (watch [it _ _] + (let [changes (-> (pcb/empty-changes it) + (pcb/add-color color))] + (rx/of + (when rename? + (fn [state] (assoc-in state [:workspace-local :color-for-rename] (:id color)))) + (dch/commit-changes changes)))))))) (defn add-recent-color [color] + (dm/assert! "expected valid recent color map" - (ctc/check-recent-color! color)) + (ctc/valid-recent-color? color)) (ptk/reify ::add-recent-color - ptk/WatchEvent - (watch [it _ _] - (let [changes (-> (pcb/empty-changes it) - (pcb/add-recent-color color))] - (rx/of (dch/commit-changes changes)))))) + ptk/UpdateEvent + (update [_ state] + (let [file-id (:current-file-id state)] + (update state :recent-colors ctc/add-recent-color file-id color))) + + ptk/EffectEvent + (effect [_ state _] + (let [recent-colors (:recent-colors state)] + (swap! s/storage assoc :recent-colors recent-colors))))) (def clear-color-for-rename (ptk/reify ::clear-color-for-rename @@ -156,8 +174,11 @@ (dm/assert! "expected valid parameters" - (and (ctc/check-color! color) - (uuid? file-id))) + (ctc/valid-color? color)) + + (dm/assert! + "expected file-id" + (uuid? file-id)) (ptk/reify ::update-color ptk/WatchEvent @@ -185,6 +206,9 @@ [{:keys [id] :as params}] (dm/assert! (uuid? id)) (ptk/reify ::delete-color + ev/Event + (-data [_] {:id id}) + ptk/WatchEvent (watch [it state _] (let [data (get state :workspace-data) @@ -200,6 +224,9 @@ (ctf/check-media-object! media)) (ptk/reify ::add-media + ev/Event + (-data [_] media) + ptk/WatchEvent (watch [it _ _] (let [obj (select-keys media [:id :name :width :height :mtype]) @@ -230,6 +257,9 @@ [{:keys [id] :as params}] (dm/assert! (uuid? id)) (ptk/reify ::delete-media + ev/Event + (-data [_] {:id id}) + ptk/WatchEvent (watch [it state _] (let [data (get state :workspace-data) @@ -247,8 +277,8 @@ (ctt/check-typography! typography)) (ptk/reify ::add-typography - IDeref - (-deref [_] typography) + ev/Event + (-data [_] typography) ptk/WatchEvent (watch [it _ _] @@ -291,6 +321,9 @@ (dm/assert! (uuid? id)) (dm/assert! (string? new-name)) (ptk/reify ::rename-typography + ev/Event + (-data [_] {:id id :name new-name}) + ptk/WatchEvent (watch [it state _] (when (and (some? new-name) (not= "" new-name)) @@ -304,6 +337,9 @@ [id] (dm/assert! (uuid? id)) (ptk/reify ::delete-typography + ev/Event + (-data [_] {:id id}) + ptk/WatchEvent (watch [it state _] (let [data (get state :workspace-data) @@ -314,47 +350,56 @@ (defn- add-component2 "This is the second step of the component creation." - [selected components-v2] - (ptk/reify ::add-component2 - IDeref - (-deref [_] {:num-shapes (count selected)}) + ([selected components-v2] + (add-component2 nil selected components-v2)) + ([id-ref selected components-v2] + (ptk/reify ::add-component2 + ev/Event + (-data [_] + {::ev/name "add-component" + :shapes (count selected)}) - ptk/WatchEvent - (watch [it state _] - (let [file-id (:current-file-id state) - page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - shapes (dwg/shapes-for-grouping objects selected) - parents (into #{} (map :parent-id) shapes)] - (when-not (empty? shapes) - (let [[root _ changes] - (cflh/generate-add-component it shapes objects page-id file-id components-v2 + ptk/WatchEvent + (watch [it state _] + (let [file-id (:current-file-id state) + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + shapes (dwg/shapes-for-grouping objects selected) + parents (into #{} (map :parent-id) shapes)] + (when-not (empty? shapes) + (let [[root component-id changes] + (cll/generate-add-component (pcb/empty-changes it) shapes objects page-id file-id components-v2 dwg/prepare-create-group cfsh/prepare-create-artboard-from-selection)] - (when-not (empty? (:redo-changes changes)) - (rx/of (dch/commit-changes changes) - (dws/select-shapes (d/ordered-set (:id root))) - (ptk/data-event :layout/update {:ids parents}))))))))) + (when id-ref + (reset! id-ref component-id)) + (when-not (empty? (:redo-changes changes)) + (rx/of (dch/commit-changes changes) + (dws/select-shapes (d/ordered-set (:id root))) + (ptk/data-event :layout/update {:ids parents})))))))))) (defn add-component "Add a new component to current file library, from the currently selected shapes. This operation is made in two steps, first one for calculate the shapes that will be part of the component and the second one with the component creation." - [] - (ptk/reify ::add-component - ptk/WatchEvent - (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) - selected (->> (wsh/lookup-selected state) - (cfh/clean-loops objects)) - selected-objects (map #(get objects %) selected) - components-v2 (features/active-feature? state "components/v2") - ;; We don't want to change the structure of component copies - can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects))] + ([] + (add-component nil nil)) - (when can-make-component - (rx/of (add-component2 selected components-v2))))))) + ([id-ref ids] + (ptk/reify ::add-component + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + selected (->> (d/nilv ids (wsh/lookup-selected state)) + (cfh/clean-loops objects)) + selected-objects (map #(get objects %) selected) + components-v2 (features/active-feature? state "components/v2") + ;; We don't want to change the structure of component copies + can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects))] + + (when can-make-component + (rx/of (add-component2 id-ref selected components-v2)))))))) (defn add-multiple-components "Add several new components to current file library, from the currently selected shapes." @@ -369,9 +414,10 @@ selected-objects (map #(get objects %) selected) ;; We don't want to change the structure of component copies can-make-component (every? true? (map #(ctn/valid-shape-for-component? objects %) selected-objects)) - added-components (map - #(add-component2 [%] components-v2) - selected) + added-components (map (fn [id] + (with-meta (add-component2 [id] components-v2) + {:multiple true})) + selected) undo-id (js/Symbol)] (when can-make-component (rx/concat @@ -390,25 +436,10 @@ (let [new-name (str/trim new-name)] (if (str/empty? new-name) (rx/empty) - (let [data (get state :workspace-data) - [path name] (cfh/parse-path-name new-name) + (let [library-data (get state :workspace-data) components-v2 (features/active-feature? state "components/v2") - - update-fn - (fn [component] - (cond-> component - :always - (assoc :path path - :name name) - - (not components-v2) - (update :objects - ;; Give the same name to the root shape - #(assoc-in % [id :name] name)))) - - changes (-> (pcb/empty-changes it) - (pcb/with-library-data data) - (pcb/update-component id update-fn))] + changes (-> (pcb/empty-changes it) + (cll/generate-rename-component id new-name library-data components-v2))] (rx/of (dch/commit-changes changes)))))))) @@ -431,7 +462,7 @@ ;; NOTE: only when components-v2 is enabled (when (and shape-id page-id) - (rx/of (dch/update-shapes [shape-id] #(assoc % :name clean-name) {:page-id page-id :stack-undo? true})))))))))) + (rx/of (dwsh/update-shapes [shape-id] #(assoc % :name clean-name) {:page-id page-id :stack-undo? true})))))))))) (defn duplicate-component "Create a new component copied from the one with the given id." @@ -441,35 +472,9 @@ (watch [it state _] (let [libraries (wsh/get-libraries state) library (get libraries library-id) - component (ctkl/get-component (:data library) component-id) - new-name (:name component) - components-v2 (features/active-feature? state "components/v2") - - main-instance-page (when components-v2 - (ctf/get-component-page (:data library) component)) - - new-component-id (when components-v2 - (uuid/next)) - - [new-component-shape new-component-shapes ; <- null in components-v2 - new-main-instance-shape new-main-instance-shapes] - (dwlh/duplicate-component component new-component-id (:data library)) - changes (-> (pcb/empty-changes it nil) - (pcb/with-page main-instance-page) - (pcb/with-objects (:objects main-instance-page)) - (pcb/add-objects new-main-instance-shapes {:ignore-touched true}) - (pcb/add-component (if components-v2 - new-component-id - (:id new-component-shape)) - (:path component) - new-name - new-component-shapes - [] - (:id new-main-instance-shape) - (:id main-instance-page) - (:annotation component)))] + (cll/generate-duplicate-component library component-id components-v2))] (rx/of (dch/commit-changes changes)))))) @@ -482,12 +487,29 @@ (watch [it state _] (let [data (get state :workspace-data)] (if (features/active-feature? state "components/v2") - (let [component (ctkl/get-component data id) - page-id (:main-instance-page component) - root-id (:main-instance-id component)] + (let [component (ctkl/get-component data id) + page-id (:main-instance-page component) + root-id (:main-instance-id component) + file-id (:current-file-id state) + file (wsh/get-file state file-id) + page (wsh/lookup-page state page-id) + objects (wsh/lookup-page-objects state page-id) + components-v2 (features/active-feature? state "components/v2") + undo-group (uuid/next) + undo-id (js/Symbol) + [all-parents changes] + (-> (pcb/empty-changes it page-id) + ;; Deleting main root triggers component delete + (cls/generate-delete-shapes file page objects #{root-id} {:components-v2 components-v2 + :undo-group undo-group + :undo-id undo-id}))] (rx/of + (dwu/start-undo-transaction undo-id) (dwt/clear-thumbnail (:current-file-id state) page-id root-id "component") - (dwsh/delete-shapes page-id #{root-id}))) ;; Deleting main root triggers component delete + (dc/detach-comment-thread #{root-id}) + (dch/commit-changes changes) + (ptk/data-event :layout/update {:ids all-parents :undo-group undo-group}) + (dwu/commit-undo-transaction undo-id))) (let [page-id (:current-page-id state) changes (-> (pcb/empty-changes it) (pcb/with-library-data data) @@ -505,22 +527,11 @@ (watch [it state _] (let [page-id (:current-page-id state) current-page (dm/get-in state [:workspace-data :pages-index page-id]) - objects (wsh/lookup-page-objects state page-id) library-data (wsh/get-file state library-id) - {:keys [changes shape]} (dwlh/prepare-restore-component library-data component-id current-page it) - parent-id (:parent-id shape) - objects (cond-> (assoc objects (:id shape) shape) - (not (nil? parent-id)) - (update-in [parent-id :shapes] - #(conj % (:id shape)))) - - ;; Adds a resize-parents operation so the groups are updated. We add all the new objects - new-objects-ids (->> changes :redo-changes (filter #(= (:type %) :add-obj)) (mapv :id)) - changes (-> changes - (pcb/with-objects objects) - (pcb/resize-parents new-objects-ids))] - - (rx/of (dch/commit-changes (assoc changes :file-id library-id))))))) + objects (wsh/lookup-page-objects state page-id) + changes (-> (pcb/empty-changes it) + (cll/generate-restore-component library-data component-id library-id current-page objects))] + (rx/of (dch/commit-changes changes)))))) (defn restore-components @@ -541,7 +552,7 @@ in the given file library. Then selects the newly created instance." ([file-id component-id position] (instantiate-component file-id component-id position nil)) - ([file-id component-id position {:keys [start-move? initial-point]}] + ([file-id component-id position {:keys [start-move? initial-point id-ref]}] (dm/assert! (uuid? file-id)) (dm/assert! (uuid? component-id)) (dm/assert! (gpt/point? position)) @@ -556,14 +567,18 @@ (pcb/with-objects objects)) [new-shape changes] - (dwlh/generate-instantiate-component changes - objects - file-id - component-id - position - page - libraries) + (cll/generate-instantiate-component changes + objects + file-id + component-id + position + page + libraries) undo-id (js/Symbol)] + + (when id-ref + (reset! id-ref (:id new-shape))) + (rx/of (dwu/start-undo-transaction undo-id) (dch/commit-changes changes) (ptk/data-event :layout/update {:ids [(:id new-shape)]}) @@ -582,13 +597,10 @@ (watch [it state _] (let [file (wsh/get-local-file state) page-id (get state :current-page-id) - container (cfh/get-container file :page page-id) libraries (wsh/get-libraries state) changes (-> (pcb/empty-changes it) - (pcb/with-container container) - (pcb/with-objects (:objects container)) - (dwlh/generate-detach-instance container libraries id))] + (cll/generate-detach-component id file page-id libraries))] (rx/of (dch/commit-changes changes)))))) @@ -612,7 +624,6 @@ (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) file (wsh/get-local-file state) - container (cfh/get-container file :page page-id) libraries (wsh/get-libraries state) selected (->> state (wsh/lookup-selected) @@ -624,10 +635,8 @@ changes (when can-detach? (reduce (fn [changes id] - (dwlh/generate-detach-instance changes container libraries id)) - (-> (pcb/empty-changes it) - (pcb/with-container container) - (pcb/with-objects objects)) + (cll/generate-detach-component changes id file page-id libraries)) + (pcb/empty-changes it) selected))] (rx/of (when can-detach? @@ -698,26 +707,30 @@ ptk/WatchEvent (watch [it state _] (log/info :msg "RESET-COMPONENT of shape" :id (str id)) - (let [file (wsh/get-local-file state) - file-full (wsh/get-local-file-full state) - libraries (wsh/get-libraries state) + (let [file (wsh/get-local-file state) + file-full (wsh/get-local-file-full state) + libraries (wsh/get-libraries state) - page-id (:current-page-id state) - container (cfh/get-container file :page page-id) + page-id (:current-page-id state) + container (cfh/get-container file :page page-id) components-v2 (features/active-feature? state "components/v2") + undo-id (js/Symbol) + changes (-> (pcb/empty-changes it) - (pcb/with-container container) - (pcb/with-objects (:objects container)) - (dwlh/generate-sync-shape-direct file-full libraries container id true components-v2))] + (cll/generate-reset-component file-full libraries container id components-v2))] (log/debug :msg "RESET-COMPONENT finished" :js/rchanges (log-changes (:redo-changes changes) file)) - (rx/of (dch/commit-changes changes)))))) + + (rx/of + (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (dwu/commit-undo-transaction undo-id)))))) (defn reset-components "Cancels all modifications in the shapes with the given ids" @@ -763,7 +776,7 @@ (-> (pcb/empty-changes it) (pcb/set-undo-group undo-group) (pcb/with-container container) - (dwlh/generate-sync-shape-inverse full-file libraries container id components-v2)) + (cll/generate-sync-shape-inverse full-file libraries container id components-v2)) file-id (:component-file shape) file (wsh/get-file state file-id) @@ -810,7 +823,7 @@ component (ctkl/get-component data component-id) page-id (:main-instance-page component) root-id (:main-instance-id component)] - (dwt/request-thumbnail file-id page-id root-id tag "update-component-thumbnail-sync"))) + (dwt/update-thumbnail file-id page-id root-id tag "update-component-thumbnail-sync"))) (defn update-component-sync ([shape-id file-id] (update-component-sync shape-id file-id nil)) @@ -871,57 +884,6 @@ second) 0))))) -(defn- add-component-for-swap - [shape file page libraries id-new-component index target-cell keep-props-values {:keys [undo-group]}] - (dm/assert! (uuid? id-new-component)) - (ptk/reify ::add-component-for-swap - ptk/WatchEvent - (watch [it _ _] - (let [objects (:objects page) - position (gpt/point (:x shape) (:y shape)) - changes (-> (pcb/empty-changes it (:id page)) - (pcb/set-undo-group undo-group) - (pcb/with-objects objects)) - position (-> position (with-meta {:cell target-cell})) - - [new-shape changes] - (dwlh/generate-instantiate-component changes - objects - (:id file) - id-new-component - position - page - libraries - nil - (:parent-id shape) - (:frame-id shape)) - - new-shape (cond-> new-shape - (nil? (ctk/get-swap-slot new-shape)) - (update :touched cfh/set-touched-group (-> (ctf/find-swap-slot shape - page - {:id (:id file) - :data file} - libraries) - (ctk/build-swap-slot-group)))) - - changes - (-> changes - ;; Restore the properties - (pcb/update-shapes [(:id new-shape)] #(d/patch-object % keep-props-values)) - - ;; We need to set the same index as the original shape - (pcb/change-parent (:parent-id shape) [new-shape] index {:component-swap true - :ignore-touched true}) - (dwlh/change-touched new-shape - shape - (ctn/make-container page :page) - {}))] - - ;; First delete so we don't break the grid layout cells - (rx/of (dch/commit-changes changes) - (dws/select-shape (:id new-shape) true)))))) - (defn- component-swap "Swaps a component with another one" [shape file-id id-new-component] @@ -929,7 +891,7 @@ (dm/assert! (uuid? file-id)) (ptk/reify ::component-swap ptk/WatchEvent - (watch [_ state _] + (watch [it state _] ;; First delete shapes so we have space in the layout otherwise we can have problems ;; in the grid creating new rows/columns to make space (let [file (wsh/get-file state file-id) @@ -948,15 +910,18 @@ keep-props-values (select-keys shape ctk/swap-keep-attrs) undo-id (js/Symbol) - undo-group (uuid/next)] + undo-group (uuid/next) + + [new-shape all-parents changes] + (-> (pcb/empty-changes it (:id page)) + (pcb/set-undo-group undo-group) + (cll/generate-component-swap objects shape file page libraries id-new-component index target-cell keep-props-values))] + (rx/of (dwu/start-undo-transaction undo-id) - (dwsh/delete-shapes nil (d/ordered-set (:id shape)) {:component-swap true - :undo-id undo-id - :undo-group undo-group}) - (add-component-for-swap shape file page libraries id-new-component index target-cell keep-props-values - {:undo-group undo-group}) - (ptk/data-event :layout/update {:ids [(:parent-id shape)] :undo-group undo-group}) + (dch/commit-changes changes) + (dws/select-shape (:id new-shape) true) + (ptk/data-event :layout/update {:ids all-parents :undo-group undo-group}) (dwu/commit-undo-transaction undo-id)))))) (defn component-multi-swap @@ -974,7 +939,7 @@ (watch [_ state _] (let [undo-id (js/Symbol)] (log/info :msg "COMPONENT-SWAP" - :file (dwlh/pretty-file file-id state) + :file (pretty-file file-id state) :id-new-component id-new-component :undo-id undo-id) (rx/concat @@ -1027,40 +992,24 @@ (watch [it state _] (when (and (some? file-id) (some? library-id)) ; Prevent race conditions while navigating out of the file (log/info :msg "SYNC-FILE" - :file (dwlh/pretty-file file-id state) - :library (dwlh/pretty-file library-id state) + :file (pretty-file file-id state) + :library (pretty-file library-id state) :asset-type asset-type :asset-id asset-id :undo-group undo-group) (let [file (wsh/get-file state file-id) - - sync-components? (or (nil? asset-type) (= asset-type :components)) - sync-colors? (or (nil? asset-type) (= asset-type :colors)) - sync-typographies? (or (nil? asset-type) (= asset-type :typographies)) - - library-changes (reduce - pcb/concat-changes - (-> (pcb/empty-changes it) - (pcb/set-undo-group undo-group)) - [(when sync-components? - (dwlh/generate-sync-library it file-id :components asset-id library-id state)) - (when sync-colors? - (dwlh/generate-sync-library it file-id :colors asset-id library-id state)) - (when sync-typographies? - (dwlh/generate-sync-library it file-id :typographies asset-id library-id state))]) - - file-changes (reduce - pcb/concat-changes - (-> (pcb/empty-changes it) - (pcb/set-undo-group undo-group)) - [(when sync-components? - (dwlh/generate-sync-file it file-id :components asset-id library-id state)) - (when sync-colors? - (dwlh/generate-sync-file it file-id :colors asset-id library-id state)) - (when sync-typographies? - (dwlh/generate-sync-file it file-id :typographies asset-id library-id state))]) - - changes (pcb/concat-changes library-changes file-changes) + libraries (wsh/get-libraries state) + current-file-id (:current-file-id state) + + changes (cll/generate-sync-file-changes + (pcb/empty-changes it) + undo-group + asset-type + file-id + asset-id + library-id + libraries + current-file-id) find-frames (fn [change] (->> (ch/frames-changed file change) @@ -1076,10 +1025,9 @@ file)) (rx/concat (rx/of (set-updating-library false) - (msg/hide-tag :sync-dialog)) + (ntf/hide {:tag :sync-dialog})) (when (seq (:redo-changes changes)) - (rx/of (dch/commit-changes (assoc changes ;; TODO a ver qué pasa con esto - :file-id file-id)))) + (rx/of (dch/commit-changes changes))) (when-not (empty? updated-frames) (rx/merge (rx/of (ptk/data-event :layout/update {:ids (map :id updated-frames) :undo-group undo-group})) @@ -1103,6 +1051,9 @@ {:file-id file-id :library-id library-id})))))))))) + +;; FIXME: the data should be set on the backend for clock consistency + (def ignore-sync "Mark the file as ignore syncs. All library changes before this moment will not ber notified to sync." @@ -1142,12 +1093,12 @@ (sync-file (:current-file-id state) (:id library))) libraries-need-sync)) - (st/emit! msg/hide)) + (st/emit! (ntf/hide))) do-dismiss #(do (st/emit! ignore-sync) - (st/emit! msg/hide))] + (st/emit! (ntf/hide)))] (when (seq libraries-need-sync) - (rx/of (msg/info-dialog + (rx/of (ntf/dialog :content (tr "workspace.updates.there-are-updates") :controls :inline-actions :links [{:label (tr "workspace.updates.more-info") @@ -1160,6 +1111,26 @@ :callback do-update}] :tag :sync-dialog))))))) + +(defn touch-component + "Update the modified-at attribute of the component to now" + [id] + (dm/verify! (uuid? id)) + (ptk/reify ::touch-component + cljs.core/IDeref + (-deref [_] [id]) + + ptk/WatchEvent + (watch [it state _] + (let [data (get state :workspace-data) + changes (-> (pcb/empty-changes it) + (pcb/with-library-data data) + (pcb/update-component id #(assoc % :modified-at (dt/now))))] + (rx/of (dch/commit-changes {:origin it + :redo-changes (:redo-changes changes) + :undo-changes [] + :save-undo? false})))))) + (defn component-changed "Notify that the component with the given id has changed, so it needs to be updated in the current file and in the copies. And also update its thumbnails." @@ -1171,6 +1142,7 @@ ptk/WatchEvent (watch [_ _ _] (rx/of + (touch-component component-id) (launch-component-sync component-id file-id undo-group))))) (defn watch-component-changes @@ -1203,14 +1175,15 @@ changes-s (->> stream - (rx/filter #(or (dch/commit-changes? %) - (ptk/type? % ::dwn/handle-file-change))) + (rx/filter dch/commit?) + (rx/map deref) + (rx/filter #(= :local (:source %))) (rx/observe-on :async)) check-changes (fn [[event [old-data _mid_data _new-data]]] (when old-data - (let [{:keys [file-id changes save-undo? undo-group]} (deref event) + (let [{:keys [file-id changes save-undo? undo-group]} event changed-components (when (or (nil? file-id) (= file-id (:id old-data))) @@ -1218,13 +1191,19 @@ (map (partial ch/components-changed old-data)) (reduce into #{})))] - (if (and (d/not-empty? changed-components) save-undo?) - (do (log/info :msg "DETECTED COMPONENTS CHANGED" - :ids (map str changed-components) - :undo-group undo-group) + (if (d/not-empty? changed-components) + (if save-undo? + (do (log/info :hint "detected component changes" + :ids (map str changed-components) + :undo-group undo-group) + + (->> (rx/from changed-components) + (rx/map #(component-changed % (:id old-data) undo-group)))) + ;; even if save-undo? is false, we need to update the :modified-date of the component + ;; (for example, for undos) + (->> (rx/from changed-components) + (rx/map touch-component))) - (->> (rx/from changed-components) - (rx/map #(component-changed % (:id old-data) undo-group)))) (rx/empty))))) changes-s @@ -1238,7 +1217,7 @@ (rx/debounce 5000) (rx/tap #(log/trc :hint "buffer initialized")))] - (when components-v2? + (when (and components-v2? (contains? cf/flags :component-thumbnails)) (->> (rx/merge changes-s @@ -1259,9 +1238,11 @@ [id is-shared] {:pre [(uuid? id) (boolean? is-shared)]} (ptk/reify ::set-file-shared - IDeref - (-deref [_] - {::ev/origin "workspace" :id id :shared is-shared}) + ev/Event + (-data [_] + {::ev/origin "workspace" + :id id + :shared is-shared}) ptk/UpdateEvent (update [_ state] @@ -1295,6 +1276,12 @@ (defn link-file-to-library [file-id library-id] (ptk/reify ::attach-library + ev/Event + (-data [_] + {::ev/name "attach-library" + :file-id file-id + :library-id library-id}) + ;; NOTE: this event implements UpdateEvent protocol for perform an ;; optimistic update state for make the UI feel more responsive. ptk/UpdateEvent @@ -1308,22 +1295,30 @@ ptk/WatchEvent (watch [_ state _] (let [features (features/get-team-enabled-features state)] - (rx/merge - (->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id}) - (rx/ignore)) - (->> (rp/cmd! :get-file {:id library-id :features features}) - (rx/merge-map fpmap/resolve-file) - (rx/map (fn [file] - (fn [state] - (assoc-in state [:workspace-libraries library-id] file))))) - (->> (rp/cmd! :get-file-object-thumbnails {:file-id library-id :tag "component"}) - (rx/map (fn [thumbnails] - (fn [state] - (update state :workspace-thumbnails merge thumbnails)))))))))) + (rx/concat + (rx/merge + (->> (rp/cmd! :link-file-to-library {:file-id file-id :library-id library-id}) + (rx/ignore)) + (->> (rp/cmd! :get-file {:id library-id :features features}) + (rx/merge-map fpmap/resolve-file) + (rx/map (fn [file] + (fn [state] + (assoc-in state [:workspace-libraries library-id] file))))) + (->> (rp/cmd! :get-file-object-thumbnails {:file-id library-id :tag "component"}) + (rx/map (fn [thumbnails] + (fn [state] + (update state :workspace-thumbnails merge thumbnails)))))) + (rx/of (ptk/reify ::attach-library-finished))))))) (defn unlink-file-from-library [file-id library-id] (ptk/reify ::detach-library + ev/Event + (-data [_] + {::ev/name "detach-library" + :file-id file-id + :library-id library-id}) + ptk/UpdateEvent (update [_ state] (d/dissoc-in state [:workspace-libraries library-id])) diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index 693207d87e9d5eb5b5aafb9fe592530e64faebcc..f14c74e1769f1b2b9b37631ab61d6a3a77bd4611 100644 --- a/frontend/src/app/main/data/workspace/media.cljs +++ b/frontend/src/app/main/data/workspace/media.cljs @@ -20,9 +20,9 @@ [app.common.types.shape :as cts] [app.common.uuid :as uuid] [app.config :as cf] + [app.main.data.changes :as dch] [app.main.data.media :as dmm] - [app.main.data.messages :as msg] - [app.main.data.workspace.changes :as dch] + [app.main.data.notifications :as ntf] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] @@ -87,7 +87,17 @@ (->> (svg/upload-images svg-data file-id) (rx/map #(svg/add-svg-shapes (assoc svg-data :image-data %) position)))))) -(defn- process-uris + +(defn upload-media-url + [name file-id url] + (rp/cmd! + :create-file-media-object-from-url + {:name name + :file-id file-id + :url url + :is-local true})) + +(defn process-uris [{:keys [file-id local? name uris mtype on-image on-svg]}] (letfn [(svg-url? [url] (or (and mtype (= mtype "image/svg+xml")) @@ -121,7 +131,7 @@ (rx/merge-map svg->clj) (rx/tap on-svg))))) -(defn- process-blobs +(defn process-blobs [{:keys [file-id local? name blobs force-media on-image on-svg]}] (letfn [(svg-blob? [blob] (and (not force-media) @@ -159,25 +169,25 @@ (handle-media-error (ex-data error) on-error) (cond (= (:code error) :invalid-svg-file) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + (rx/of (ntf/error (tr "errors.media-type-not-allowed"))) (= (:code error) :media-type-not-allowed) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + (rx/of (ntf/error (tr "errors.media-type-not-allowed"))) (= (:code error) :unable-to-access-to-url) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + (rx/of (ntf/error (tr "errors.media-type-not-allowed"))) (= (:code error) :invalid-image) - (rx/of (msg/error (tr "errors.media-type-not-allowed"))) + (rx/of (ntf/error (tr "errors.media-type-not-allowed"))) (= (:code error) :media-max-file-size-reached) - (rx/of (msg/error (tr "errors.media-too-large"))) + (rx/of (ntf/error (tr "errors.media-too-large"))) (= (:code error) :media-type-mismatch) - (rx/of (msg/error (tr "errors.media-type-mismatch"))) + (rx/of (ntf/error (tr "errors.media-type-mismatch"))) (= (:code error) :unable-to-optimize) - (rx/of (msg/error (:hint error))) + (rx/of (ntf/error (:hint error))) (fn? on-error) (on-error error) @@ -185,7 +195,7 @@ :else (do (.error js/console "ERROR" error) - (rx/of (msg/error (tr "errors.cannot-upload"))))))) + (rx/of (ntf/error (tr "errors.cannot-upload"))))))) (def ^:private @@ -210,9 +220,9 @@ ptk/WatchEvent (watch [_ _ _] (rx/concat - (rx/of (msg/show {:content (tr "media.loading") - :notification-type :toast - :type :info + (rx/of (ntf/show {:content (tr "media.loading") + :type :toast + :level :info :timeout nil :tag :media-loading})) (->> (if (seq uris) @@ -224,7 +234,7 @@ ;; Every stream has its own sideeffect. We need to ignore the result (rx/ignore) (rx/catch #(handle-media-error % on-error)) - (rx/finalize #(st/emit! (msg/hide-tag :media-loading)))))))) + (rx/finalize #(st/emit! (ntf/hide :tag :media-loading)))))))) ;; Deprecated in components-v2 (defn upload-media-asset @@ -244,8 +254,6 @@ :on-svg #(st/emit! (svg-uploaded % file-id position)))] (process-media-objects params))) - - (defn upload-fill-image [file on-success] (dm/assert! @@ -440,12 +448,22 @@ :id object-id}] (rx/concat - (rx/of (msg/show {:content (tr "media.loading") - :notification-type :toast - :type :info + (rx/of (ntf/show {:content (tr "media.loading") + :type :toast + :level :info :timeout nil :tag :media-loading})) (->> (rp/cmd! :clone-file-media-object params) (rx/tap on-success) (rx/catch on-error) - (rx/finalize #(st/emit! (msg/hide-tag :media-loading))))))))) + (rx/finalize #(st/emit! (ntf/hide :tag :media-loading))))))))) + +(defn create-svg-shape + [id name svg-string position] + (ptk/reify ::create-svg-shape + ptk/WatchEvent + (watch [_ _ _] + (->> (svg->clj [name svg-string]) + (rx/take 1) + (rx/map #(svg/add-svg-shapes id % position {:ignore-selection? true + :change-selection? false})))))) diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index b552bee67acc7ce22a63d28db4c1b06ba6b64c88..773b4f146ffb1ca383c2342b7b8fea3b00bab7d7 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -23,9 +23,9 @@ [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] [app.main.constants :refer [zoom-half-pixel-precision]] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.comments :as-alias dwcm] [app.main.data.workspace.guides :as-alias dwg] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [beicon.v2.core :as rx] @@ -46,9 +46,10 @@ ;; When the interaction is finished (e.g. user releases mouse button), the ;; apply-modifiers event is done, that consolidates all modifiers into the base ;; geometric attributes of the shapes. + (defn- check-delta - "If the shape is a component instance, check its relative position respect the - root of the component, and see if it changes after applying a transformation." + "If the shape is a component instance, check its relative position and rotation respect + the root of the component, and see if it changes after applying a transformation." [shape root transformed-shape transformed-root] (let [shape-delta (when root @@ -65,6 +66,11 @@ (gpt/distance-vector shape-delta transformed-shape-delta) (gpt/point 0 0)) + rotation-delta + (if (and (some? (:rotation shape)) (some? (:rotation shape))) + (- (:rotation transformed-shape) (:rotation shape)) + 0) + selrect (:selrect shape) transformed-selrect (:selrect transformed-shape)] @@ -76,7 +82,8 @@ ;; shape position unchanged. But in this case we do not want to ignore it. (and (and (< (:x distance) 1) (< (:y distance) 1)) (mth/close? (:width selrect) (:width transformed-selrect)) - (mth/close? (:height selrect) (:height transformed-selrect))))) + (mth/close? (:height selrect) (:height transformed-selrect)) + (mth/close? rotation-delta 0)))) (defn calculate-ignore-tree "Retrieves a map with the flag `ignore-geometry?` given a tree of modifiers" @@ -431,28 +438,28 @@ ;; - It consideres the center for everyshape instead of the center of the total selrect ;; - The angle param is the desired final value, not a delta (defn set-delta-rotation-modifiers - ([angle shapes] - (ptk/reify ::set-delta-rotation-modifiers - ptk/UpdateEvent - (update [_ state] - (let [objects (wsh/lookup-page-objects state) - ids - (->> shapes - (remove #(get % :blocked false)) - (filter #(contains? (get editable-attrs (:type %)) :rotation)) - (map :id)) + [angle shapes {:keys [center delta?] :or {center nil delta? false}}] + (ptk/reify ::set-delta-rotation-modifiers + ptk/UpdateEvent + (update [_ state] + (let [objects (wsh/lookup-page-objects state) + ids + (->> shapes + (remove #(get % :blocked false)) + (filter #(contains? (get editable-attrs (:type %)) :rotation)) + (map :id)) - get-modifier - (fn [shape] - (let [delta (- angle (:rotation shape)) - center (gsh/shape->center shape)] - (ctm/rotation-modifiers shape center delta))) + get-modifier + (fn [shape] + (let [delta (if delta? angle (- angle (:rotation shape))) + center (or center (gsh/shape->center shape))] + (ctm/rotation-modifiers shape center delta))) - modif-tree - (-> (build-modif-tree ids objects get-modifier) - (gm/set-objects-modifiers objects))] + modif-tree + (-> (build-modif-tree ids objects get-modifier) + (gm/set-objects-modifiers objects))] - (assoc state :workspace-modifiers modif-tree)))))) + (assoc state :workspace-modifiers modif-tree))))) (defn apply-modifiers ([] @@ -490,9 +497,9 @@ (if undo-transation? (rx/of (dwu/start-undo-transaction undo-id)) (rx/empty)) - (rx/of (ptk/event ::dwg/move-frame-guides ids-with-children) + (rx/of (ptk/event ::dwg/move-frame-guides {:ids ids-with-children :modifiers object-modifiers}) (ptk/event ::dwcm/move-frame-comment-threads ids-with-children) - (dch/update-shapes + (dwsh/update-shapes ids (fn [shape] (let [modif (get-in object-modifiers [(:id shape) :modifiers]) @@ -552,8 +559,10 @@ :layout-grid-rows]}) ;; We've applied the text-modifier so we can dissoc the temporary data (fn [state] - (update state :workspace-text-modifier #(apply dissoc % ids))) - (clear-local-transform)) + (update state :workspace-text-modifier #(apply dissoc % ids)))) + (if (nil? modifiers) + (rx/of (clear-local-transform)) + (rx/empty)) (if undo-transation? (rx/of (dwu/commit-undo-transaction undo-id)) (rx/empty)))))))) diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 4bb3a977232936034c83d294f0f33b99f0d45967..b756579decfd97b7e1f83717bf691fd5d7d603cb 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -11,11 +11,10 @@ [app.common.files.changes :as cpc] [app.common.schema :as sm] [app.common.uuid :as uuid] + [app.main.data.changes :as dch] [app.main.data.common :refer [handle-notification]] [app.main.data.websocket :as dws] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.libraries :as dwl] - [app.main.data.workspace.persistence :as dwp] [app.util.globals :refer [global]] [app.util.mouse :as mse] [app.util.object :as obj] @@ -25,6 +24,8 @@ [clojure.set :as set] [potok.v2.core :as ptk])) +;; FIXME: this ns should be renamed to something different + (declare process-message) (declare handle-presence) (declare handle-pointer-update) @@ -84,7 +85,7 @@ (->> stream (rx/filter mse/pointer-event?) (rx/filter #(= :viewport (mse/get-pointer-source %))) - (rx/pipe (rxs/throttle 100)) + (rx/pipe (rxs/throttle 50)) (rx/map #(handle-pointer-send file-id (:pt %))))) (rx/take-until stopper))] @@ -110,9 +111,15 @@ ptk/WatchEvent (watch [_ state _] (let [page-id (:current-page-id state) + local (:workspace-local state) + message {:type :pointer-update :file-id file-id :page-id page-id + :zoom (:zoom local) + :zoom-inverse (:zoom-inverse local) + :vbox (:vbox local) + :vport (:vport local) :position point}] (rx/of (dws/send message)))))) @@ -174,13 +181,17 @@ (update state :workspace-presence update-presence)))))) (defn handle-pointer-update - [{:keys [page-id session-id position] :as msg}] + [{:keys [page-id session-id position zoom zoom-inverse vbox vport] :as msg}] (ptk/reify ::handle-pointer-update ptk/UpdateEvent (update [_ state] (update-in state [:workspace-presence session-id] (fn [session] (assoc session + :zoom zoom + :zoom-inverse zoom-inverse + :vbox vbox + :vport vport :point position :updated-at (dt/now) :page-id page-id)))))) @@ -197,9 +208,10 @@ [:changes ::cpc/changes]])) (defn handle-file-change - [{:keys [file-id changes] :as msg}] + [{:keys [file-id changes revn] :as msg}] + (dm/assert! - "expected valid arguments" + "expected valid parameters" (sm/check! schema:handle-file-change msg)) (ptk/reify ::handle-file-change @@ -207,45 +219,16 @@ (-deref [_] {:changes changes}) ptk/WatchEvent - (watch [_ state _] - (let [page-id (:current-page-id state) - position-data-operation? - (fn [{:keys [type attr]}] - (and (= :set type) (= attr :position-data))) - - ;;add-origin-session-id - ;;(fn [{:keys [] :as op}] - ;; (cond-> op - ;; (position-data-operation? op) - ;; (update :val with-meta {:session-id (:session-id msg)}))) - - update-position-data - (fn [change] - ;; Remove the position data from remote operations. Will be changed localy, otherwise - ;; creates a strange "out-of-sync" behaviour. - (cond-> change - (and (= page-id (:page-id change)) - (= :mod-obj (:type change))) - (update :operations #(d/removev position-data-operation? %)))) - - process-page-changes - (fn [[page-id changes]] - (dch/update-indices page-id changes)) - - ;; We update `position-data` from the incoming message - changes (->> changes - (mapv update-position-data) - (d/removev (fn [change] - (and (= page-id (:page-id change)) - (:ignore-remote? change))))) - - changes-by-pages (group-by :page-id changes)] - - (rx/merge - (rx/of (dwp/shapes-changes-persisted file-id (assoc msg :changes changes))) - - (when-not (empty? changes-by-pages) - (rx/from (map process-page-changes changes-by-pages)))))))) + (watch [_ _ _] + ;; The commit event is responsible to apply the data localy + ;; and update the persistence internal state with the updated + ;; file-revn + (rx/of (dch/commit {:file-id file-id + :file-revn revn + :save-undo? false + :source :remote + :redo-changes (vec changes) + :undo-changes []}))))) (def ^:private schema:handle-library-change diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs index dd188e72e31bc4a13d742f54d2cf296cc376b6ba..43ad6fdc0f5ccd9a955e93568b8e2fc362098c83 100644 --- a/frontend/src/app/main/data/workspace/path/changes.cljs +++ b/frontend/src/app/main/data/workspace/path/changes.cljs @@ -8,7 +8,7 @@ (:require [app.common.data.macros :as dm] [app.common.files.changes-builder :as pcb] - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dch] [app.main.data.workspace.path.common :refer [check-path-content!]] [app.main.data.workspace.path.helpers :as helpers] [app.main.data.workspace.path.state :as st] diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index af492bc0f9324714028f7c48e81db82a38ce7567..9b562723e258633f69d92a097081c8a25d95055a 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -15,7 +15,6 @@ [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.drawing.common :as dwdc] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.path.changes :as changes] @@ -24,6 +23,7 @@ [app.main.data.workspace.path.state :as st] [app.main.data.workspace.path.streams :as streams] [app.main.data.workspace.path.undo :as undo] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.util.mouse :as mse] [beicon.v2.core :as rx] @@ -139,9 +139,7 @@ (rx/map #(drag-handler position idx prefix %)) (rx/take-until (rx/merge - (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) + (mse/drag-stopper stream) (->> stream (rx/filter helpers/end-path-event?)))))] @@ -166,9 +164,7 @@ ptk/WatchEvent (watch [_ state stream] (let [stopper (rx/merge - (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) + (mse/drag-stopper stream) (->> stream (rx/filter helpers/end-path-event?))) @@ -197,9 +193,7 @@ (gpt/point? down-event)) (let [stopper (rx/merge - (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) + (mse/drag-stopper stream) (->> stream (rx/filter helpers/end-path-event?))) @@ -339,7 +333,7 @@ edit-mode (get-in state [:workspace-local :edit-path id :edit-mode])] (if (= :draw edit-mode) (rx/concat - (rx/of (dch/update-shapes [id] upsp/convert-to-path)) + (rx/of (dwsh/update-shapes [id] upsp/convert-to-path)) (rx/of (handle-drawing id)) (->> stream (rx/filter (ptk/type? ::common/finish-path)) @@ -363,7 +357,8 @@ (common/finish-path) (dwdc/clear-drawing))))))) -(defn change-edit-mode [mode] +(defn change-edit-mode + [mode] (ptk/reify ::change-edit-mode ptk/UpdateEvent (update [_ state] diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 51d688707ea69b962081ca529a809004b38360e7..a91532b0adbcb7ba47430df76618a8e86d25bba2 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -14,7 +14,7 @@ [app.common.svg.path.command :as upc] [app.common.svg.path.shapes-to-path :as upsp] [app.common.svg.path.subpath :as ups] - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dch] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.drawing :as drawing] @@ -23,6 +23,7 @@ [app.main.data.workspace.path.state :as st] [app.main.data.workspace.path.streams :as streams] [app.main.data.workspace.path.undo :as undo] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.streams :as ms] [app.util.mouse :as mse] @@ -114,6 +115,9 @@ (update [_ state] (let [id (st/get-path-id state) content (st/get-path state :content) + to-point (cond-> to-point + (:shift? to-point) (helpers/position-fixed-angle from-point)) + delta (gpt/subtract to-point from-point) modifiers-reducer (partial modify-content-point content delta) @@ -140,7 +144,7 @@ selected? (contains? selected-points position)] (streams/drag-stream (rx/of - (dch/update-shapes [id] upsp/convert-to-path) + (dwsh/update-shapes [id] upsp/convert-to-path) (when-not selected? (selection/select-node position shift?)) (drag-selected-points @ms/mouse-position)) (rx/of (selection/select-node position shift?))))))) @@ -150,9 +154,7 @@ (ptk/reify ::drag-selected-points ptk/WatchEvent (watch [_ state stream] - (let [stopper (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) + (let [stopper (mse/drag-stopper stream) id (dm/get-in state [:workspace-local :edition]) @@ -226,7 +228,7 @@ mov-vec (gpt/multiply (get-displacement direction) scale)] (rx/concat - (rx/of (dch/update-shapes [id] upsp/convert-to-path)) + (rx/of (dwsh/update-shapes [id] upsp/convert-to-path)) (rx/merge (->> move-events (rx/take-until stopper) @@ -264,7 +266,7 @@ (streams/drag-stream (rx/concat - (rx/of (dch/update-shapes [id] upsp/convert-to-path)) + (rx/of (dwsh/update-shapes [id] upsp/convert-to-path)) (->> (streams/move-handler-stream handler point handler opposite points) (rx/map (fn [{:keys [x y alt? shift?]}] @@ -279,9 +281,7 @@ (not alt?))))) (rx/take-until (rx/merge - (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) + (mse/drag-stopper stream) (->> stream (rx/filter streams/finish-edition?))))) @@ -355,5 +355,5 @@ ptk/WatchEvent (watch [_ state _] (let [id (st/get-path-id state)] - (rx/of (dch/update-shapes [id] upsp/convert-to-path) + (rx/of (dwsh/update-shapes [id] upsp/convert-to-path) (split-segments event)))))) diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index 2facaf53a692d773e37937836d74ebbdcf0ffa50..b52ab6e723dba2899b8419d757c75a36586514d9 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -22,6 +22,7 @@ (or (= type ::common/finish-path) (= type :app.main.data.workspace.path.shortcuts/esc-pressed) (= type :app.main.data.workspace.common/clear-edition-mode) + (= type :app.main.data.workspace.edition/clear-edition-mode) (= type :app.main.data.workspace/finalize-page) (= event :interrupt) ;; ESC (and ^boolean (mse/mouse-event? event) diff --git a/frontend/src/app/main/data/workspace/path/selection.cljs b/frontend/src/app/main/data/workspace/path/selection.cljs index 028a6ec3b9d30c8e0dd08b6d0236f49adbeeda2c..b2256b3c90d4d87d7ef63ec32a60d391641d7b53 100644 --- a/frontend/src/app/main/data/workspace/path/selection.cljs +++ b/frontend/src/app/main/data/workspace/path/selection.cljs @@ -10,7 +10,6 @@ [app.common.geom.point :as gpt] [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.path.state :as st] [app.main.streams :as ms] [app.util.mouse :as mse] @@ -119,15 +118,9 @@ (ptk/reify ::handle-area-selection ptk/WatchEvent (watch [_ state stream] - (let [zoom (get-in state [:workspace-local :zoom] 1) - stopper (rx/merge - (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) - (->> stream - (rx/filter dwc/interrupt?))) - - from-p @ms/mouse-position] + (let [zoom (get-in state [:workspace-local :zoom] 1) + stopper (mse/drag-stopper stream) + from-p @ms/mouse-position] (rx/concat (->> ms/mouse-position (rx/map #(grc/points->rect [from-p %])) diff --git a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs index c73ad3dfcad8b3bddd510a1bd68b616ea014c2c2..d6367aefd0065ba27f86d756b35bee1a63539ec1 100644 --- a/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs +++ b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs @@ -10,7 +10,7 @@ [app.common.files.helpers :as cph] [app.common.svg.path.shapes-to-path :as upsp] [app.common.types.container :as ctn] - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs index 653b6659e627bc22b4eb61a53d971386410b79e3..07fb3b1fa39e4a8292340f460918f4dd5c1650fa 100644 --- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -22,13 +22,9 @@ (defn esc-pressed [] (ptk/reify ::esc-pressed ptk/WatchEvent - (watch [_ state _] + (watch [_ _ _] ;; Not interrupt when we're editing a path - (let [edition-id (or (get-in state [:workspace-drawing :object :id]) - (get-in state [:workspace-local :edition])) - path-edit-mode (get-in state [:workspace-local :edit-path edition-id :edit-mode])] - (when-not (= :draw path-edit-mode) - (rx/of :interrupt)))))) + (rx/of :interrupt)))) (def shortcuts {:move-nodes {:tooltip "M" diff --git a/frontend/src/app/main/data/workspace/path/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 9f55e92a0ee9c18047e2d40ff6b9c7d4d6b70645..f860ca586e5aec2d6e986c1d1de4960c53946683 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -53,9 +53,7 @@ start (-> @ms/mouse-position to-pixel-snap) stopper (rx/merge - (->> st/stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) + (mse/drag-stopper st/stream) (->> st/stream (rx/filter finish-edition?))) @@ -103,7 +101,12 @@ (->> ms/mouse-position (rx/map to-pixel-snap) (rx/with-latest-from (snap-toggled-stream)) - (rx/map check-path-snap)))) + (rx/map check-path-snap) + (rx/with-latest-from + (fn [position shift? alt?] + (assoc position :shift? shift? :alt? alt?)) + ms/mouse-position-shift + ms/mouse-position-alt)))) (defn get-angle [node handler opposite] (when (and (some? node) (some? handler) (some? opposite)) diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index e75b53fc3ff5d4c4f06a8f21cb47ad245c822991..0c17182c36a33b681510344429f1f5450d975bb9 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -8,10 +8,11 @@ (:require [app.common.svg.path.shapes-to-path :as upsp] [app.common.svg.path.subpath :as ups] - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dch] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.state :as st] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.util.path.tools :as upt] [beicon.v2.core :as rx] @@ -37,7 +38,7 @@ changes (changes/generate-path-changes it objects page-id shape (:content shape) new-content)] (rx/concat - (rx/of (dch/update-shapes [id] upsp/convert-to-path)) + (rx/of (dwsh/update-shapes [id] upsp/convert-to-path)) (rx/of (dch/commit-changes changes) (when (empty? new-content) (dwe/clear-edition-mode))))))))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs deleted file mode 100644 index c0032c6c8de8e3ee548e70eec9ba5ce62d6d7601..0000000000000000000000000000000000000000 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ /dev/null @@ -1,263 +0,0 @@ -;; 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 app.main.data.workspace.persistence - (:require - [app.common.data.macros :as dm] - [app.common.files.changes :as cpc] - [app.common.logging :as log] - [app.common.types.shape-tree :as ctst] - [app.common.uuid :as uuid] - [app.main.data.workspace.changes :as dch] - [app.main.data.workspace.thumbnails :as dwt] - [app.main.features :as features] - [app.main.repo :as rp] - [app.main.store :as st] - [app.util.time :as dt] - [beicon.v2.core :as rx] - [okulary.core :as l] - [potok.v2.core :as ptk])) - -(log/set-level! :info) - -(declare persist-changes) -(declare persist-synchronous-changes) -(declare shapes-changes-persisted) -(declare shapes-changes-persisted-finished) -(declare update-persistence-status) - -;; --- Persistence - -(defn initialize-file-persistence - [file-id] - (ptk/reify ::initialize-persistence - ptk/WatchEvent - (watch [_ _ stream] - (log/debug :hint "initialize persistence") - (let [stopper (rx/filter (ptk/type? ::initialize-persistence) stream) - commits (l/atom []) - saving? (l/atom false) - - local-file? - #(as-> (:file-id %) event-file-id - (or (nil? event-file-id) - (= event-file-id file-id))) - - library-file? - #(as-> (:file-id %) event-file-id - (and (some? event-file-id) - (not= event-file-id file-id))) - - on-dirty - (fn [] - ;; Enable reload stopper - (swap! st/ongoing-tasks conj :workspace-change) - (st/emit! (update-persistence-status {:status :pending}))) - - on-saving - (fn [] - (reset! saving? true) - (st/emit! (update-persistence-status {:status :saving}))) - - on-saved - (fn [] - ;; Disable reload stopper - (swap! st/ongoing-tasks disj :workspace-change) - (st/emit! (update-persistence-status {:status :saved})) - (reset! saving? false))] - - (rx/merge - (->> stream - (rx/filter dch/commit-changes?) - (rx/map deref) - (rx/filter local-file?) - (rx/tap on-dirty) - (rx/filter (complement empty?)) - (rx/map (fn [commit] - (-> commit - (assoc :id (uuid/next)) - (assoc :file-id file-id)))) - (rx/observe-on :async) - (rx/tap #(swap! commits conj %)) - (rx/take-until (rx/delay 100 stopper)) - (rx/finalize (fn [] - (log/debug :hint "finalize persistence: changes watcher")))) - - (->> (rx/from-atom commits) - (rx/filter (complement empty?)) - (rx/sample-when - (rx/merge - (rx/filter #(= ::force-persist %) stream) - (->> (rx/merge - (rx/interval 5000) - (->> (rx/from-atom commits) - (rx/filter (complement empty?)) - (rx/debounce 2000))) - ;; Not sample while saving so there are no race conditions - (rx/filter #(not @saving?))))) - (rx/tap #(reset! commits [])) - (rx/tap on-saving) - (rx/mapcat (fn [changes] - ;; NOTE: this is needed for don't start the - ;; next persistence before this one is - ;; finished. - (if-let [file-revn (dm/get-in @st/state [:workspace-file :revn])] - (rx/merge - (->> (rx/of (persist-changes file-id file-revn changes commits)) - (rx/observe-on :async)) - (->> stream - ;; We wait for every change to be persisted - (rx/filter (ptk/type? ::shapes-changes-persisted-finished)) - (rx/take 1) - (rx/tap on-saved) - (rx/ignore))) - (rx/empty)))) - (rx/take-until (rx/delay 100 stopper)) - (rx/finalize (fn [] - (log/debug :hint "finalize persistence: save loop")))) - - ;; Synchronous changes - (->> stream - (rx/filter dch/commit-changes?) - (rx/map deref) - (rx/filter library-file?) - (rx/filter (complement #(empty? (:changes %)))) - (rx/map persist-synchronous-changes) - (rx/take-until (rx/delay 100 stopper)) - (rx/finalize (fn [] - (log/debug :hint "finalize persistence: synchronous save loop"))))))))) - -(defn persist-changes - [file-id file-revn changes pending-commits] - (log/debug :hint "persist changes" :changes (count changes)) - (dm/assert! (uuid? file-id)) - (ptk/reify ::persist-changes - ptk/WatchEvent - (watch [_ state _] - (let [sid (:session-id state) - - features (features/get-team-enabled-features state) - params {:id file-id - :revn file-revn - :session-id sid - :changes-with-metadata (into [] changes) - :features features}] - - (->> (rp/cmd! :update-file params) - (rx/mapcat (fn [lagged] - (log/debug :hint "changes persisted" :lagged (count lagged)) - (let [frame-updates - (-> (group-by :page-id changes) - (update-vals #(into #{} (mapcat :frames) %))) - - commits - (->> @pending-commits - (map #(assoc % :revn file-revn)))] - - (rx/concat - (rx/merge - (->> (rx/from frame-updates) - (rx/mapcat (fn [[page-id frames]] - (->> frames (map (fn [frame-id] [file-id page-id frame-id]))))) - (rx/map (fn [data] - (ptk/data-event ::dwt/update data)))) - - (->> (rx/from (concat lagged commits)) - (rx/merge-map - (fn [{:keys [changes] :as entry}] - (rx/merge - (rx/from - (for [[page-id changes] (group-by :page-id changes)] - (dch/update-indices page-id changes))) - (rx/of (shapes-changes-persisted file-id entry))))))) - - (rx/of (shapes-changes-persisted-finished)))))) - (rx/catch (fn [cause] - (if (instance? js/TypeError cause) - (->> (rx/timer 2000) - (rx/map (fn [_] - (persist-changes file-id file-revn changes pending-commits)))) - (rx/throw cause))))))))) - -;; Event to be thrown after the changes have been persisted -(defn shapes-changes-persisted-finished - [] - (ptk/reify ::shapes-changes-persisted-finished)) - -(defn persist-synchronous-changes - [{:keys [file-id changes]}] - (dm/assert! (uuid? file-id)) - (ptk/reify ::persist-synchronous-changes - ptk/WatchEvent - (watch [_ state _] - (let [features (features/get-team-enabled-features state) - - sid (:session-id state) - file (dm/get-in state [:workspace-libraries file-id]) - - params {:id (:id file) - :revn (:revn file) - :session-id sid - :changes changes - :features features}] - - (when (:id params) - (->> (rp/cmd! :update-file params) - (rx/ignore))))))) - -(defn update-persistence-status - [{:keys [status reason]}] - (ptk/reify ::update-persistence-status - ptk/UpdateEvent - (update [_ state] - (update state :workspace-persistence - (fn [local] - (assoc local - :reason reason - :status status - :updated-at (dt/now))))))) - - -(defn shapes-persisted-event? [event] - (= (ptk/type event) ::changes-persisted)) - -(defn shapes-changes-persisted - [file-id {:keys [revn changes] persisted-session-id :session-id}] - (dm/assert! (uuid? file-id)) - (dm/assert! (int? revn)) - (dm/assert! (cpc/check-changes! changes)) - - (ptk/reify ::shapes-changes-persisted - ptk/UpdateEvent - (update [_ state] - ;; NOTE: we don't set the file features context here because - ;; there are no useful context for code that need to be executed - ;; on the frontend side - (let [current-file-id (:current-file-id state) - current-session-id (:session-id state)] - (if (and (some? current-file-id) - ;; If the remote change is from teh current session we skip - (not= persisted-session-id current-session-id)) - (if (= file-id current-file-id) - (let [changes (group-by :page-id changes)] - (-> state - (update-in [:workspace-file :revn] max revn) - (update :workspace-data - (fn [file] - (loop [fdata file - entries (seq changes)] - (if-let [[page-id changes] (first entries)] - (recur (-> fdata - (cpc/process-changes changes) - (cond-> (some? page-id) - (ctst/update-object-indices page-id))) - (rest entries)) - fdata)))))) - (-> state - (update-in [:workspace-libraries file-id :revn] max revn) - (update-in [:workspace-libraries file-id :data] cpc/process-changes changes))) - - state))))) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 15d3d5ab451f3b3ad53110b5d7d4829d5b5e33b0..1f29cbcc3414c1c5152832a6bb53828080ea9224 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -11,23 +11,17 @@ [app.common.files.changes-builder :as pcb] [app.common.files.focus :as cpf] [app.common.files.helpers :as cfh] - [app.common.files.libraries-helpers :as cflh] [app.common.geom.point :as gpt] [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] + [app.common.logic.libraries :as cll] [app.common.record :as cr] [app.common.types.component :as ctk] - [app.common.types.container :as ctn] - [app.common.types.file :as ctf] - [app.common.types.page :as ctp] - [app.common.types.shape-tree :as ctst] - [app.common.types.shape.interactions :as ctsi] - [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] + [app.main.data.changes :as dch] + [app.main.data.events :as ev] [app.main.data.modal :as md] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.collapse :as dwc] - [app.main.data.workspace.libraries-helpers :as dwlh] [app.main.data.workspace.specialized-panel :as-alias dwsp] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] @@ -63,14 +57,8 @@ (ptk/reify ::handle-area-selection ptk/WatchEvent (watch [_ state stream] - (let [zoom (dm/get-in state [:workspace-local :zoom] 1) - stopper (rx/merge - (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) - (->> stream - (rx/filter interrupt?))) - + (let [zoom (dm/get-in state [:workspace-local :zoom] 1) + stopper (mse/drag-stopper stream) init-position @ms/mouse-position init-selrect (grc/make-rect @@ -374,277 +362,6 @@ (rx/of (select-shape (:id selected)))))))) ;; --- Duplicate Shapes -(declare prepare-duplicate-shape-change) -(declare prepare-duplicate-flows) -(declare prepare-duplicate-guides) - -(defn prepare-duplicate-changes - "Prepare objects to duplicate: generate new id, give them unique names, - move to the desired position, and recalculate parents and frames as needed." - ([all-objects page ids delta it libraries library-data file-id] - (let [init-changes - (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/with-objects all-objects))] - (prepare-duplicate-changes all-objects page ids delta it libraries library-data file-id init-changes))) - - ([all-objects page ids delta it libraries library-data file-id init-changes] - (let [shapes (map (d/getf all-objects) ids) - unames (volatile! (cfh/get-used-names (:objects page))) - update-unames! (fn [new-name] (vswap! unames conj new-name)) - all-ids (reduce #(into %1 (cons %2 (cfh/get-children-ids all-objects %2))) (d/ordered-set) ids) - - ;; We need ids-map for remapping the grid layout. But when duplicating the guides - ;; we calculate a new one because the components will have created new shapes. - ids-map (into {} (map #(vector % (uuid/next))) all-ids) - - changes - (->> shapes - (reduce #(prepare-duplicate-shape-change %1 - all-objects - page - unames - update-unames! - ids-map - %2 - delta - libraries - library-data - it - file-id) - init-changes)) - - ;; We need to check the changes to get the ids-map - ids-map - (into {} - (comp - (filter #(= :add-obj (:type %))) - (map #(vector (:old-id %) (-> % :obj :id)))) - (:redo-changes changes))] - - (-> changes - (prepare-duplicate-flows shapes page ids-map) - (prepare-duplicate-guides shapes page ids-map delta))))) - -(defn- prepare-duplicate-component-change - [changes objects page component-root parent-id frame-id delta libraries library-data it] - (let [component-id (:component-id component-root) - file-id (:component-file component-root) - main-component (ctf/get-component libraries file-id component-id) - moved-component (gsh/move component-root delta) - pos (gpt/point (:x moved-component) (:y moved-component)) - - instantiate-component - #(dwlh/generate-instantiate-component changes - objects - file-id - (:component-id component-root) - pos - page - libraries - (:id component-root) - parent-id - frame-id) - - restore-component - #(let [restore (dwlh/prepare-restore-component changes library-data (:component-id component-root) it page delta (:id component-root) parent-id frame-id)] - [(:shape restore) (:changes restore)]) - - [_shape changes] - (if (nil? main-component) - (restore-component) - (instantiate-component))] - changes)) - -;; TODO: move to common.files.shape-helpers -(defn- prepare-duplicate-shape-change - ([changes objects page unames update-unames! ids-map obj delta libraries library-data it file-id] - (prepare-duplicate-shape-change changes objects page unames update-unames! ids-map obj delta libraries library-data it file-id (:frame-id obj) (:parent-id obj) false false)) - - ([changes objects page unames update-unames! ids-map obj delta libraries library-data it file-id frame-id parent-id duplicating-component? child?] - (cond - (nil? obj) - changes - - (ctf/is-known-component? obj libraries) - (prepare-duplicate-component-change changes objects page obj parent-id frame-id delta libraries library-data it) - - :else - (let [frame? (cfh/frame-shape? obj) - group? (cfh/group-shape? obj) - bool? (cfh/bool-shape? obj) - new-id (ids-map (:id obj)) - parent-id (or parent-id frame-id) - parent (get objects parent-id) - name (:name obj) - - is-component-root? (or (:saved-component-root obj) - ;; Backward compatibility - (:saved-component-root? obj) - (ctk/instance-root? obj)) - duplicating-component? (or duplicating-component? (ctk/instance-head? obj)) - is-component-main? (ctk/main-instance? obj) - into-component? (and duplicating-component? - (ctn/in-any-component? objects parent)) - - regenerate-component - (fn [changes shape] - (let [components-v2 (dm/get-in library-data [:options :components-v2]) - [_ changes] (cflh/generate-add-component-changes changes shape objects file-id (:id page) components-v2)] - changes)) - - new-obj - (-> obj - (assoc :id new-id - :name name - :parent-id parent-id - :frame-id frame-id) - - (dissoc :shapes - :main-instance - :use-for-thumbnail) - - (cond-> into-component? - (dissoc :component-root)) - - (cond-> (and (ctk/instance-head? obj) - (not into-component?)) - (assoc :component-root true)) - - (cond-> (or frame? group? bool?) - (assoc :shapes [])) - - (gsh/move delta) - (d/update-when :interactions #(ctsi/remap-interactions % ids-map objects)) - - (cond-> (ctl/grid-layout? obj) - (ctl/remap-grid-cells ids-map))) - - new-obj (cond-> new-obj - (not duplicating-component?) - (ctk/detach-shape)) - - ;; We want the first added object to touch it's parent, but not subsequent children - changes (-> (pcb/add-object changes new-obj {:ignore-touched (and duplicating-component? child?)}) - (pcb/amend-last-change #(assoc % :old-id (:id obj))) - (cond-> (ctl/grid-layout? objects (:parent-id obj)) - (-> (pcb/update-shapes [(:parent-id obj)] ctl/assign-cells {:with-objects? true}) - (pcb/reorder-grid-children [(:parent-id obj)])))) - - changes (cond-> changes - (and is-component-root? is-component-main?) - (regenerate-component new-obj)) - - ;; This is needed for the recursive call to find the new object as parent - page' (ctst/add-shape (:id new-obj) - new-obj - {:objects objects} - (:frame-id new-obj) - (:parent-id new-obj) - nil - true)] - - (reduce (fn [changes child] - (prepare-duplicate-shape-change changes - (:objects page') - page - unames - update-unames! - ids-map - child - delta - libraries - library-data - it - file-id - (if frame? new-id frame-id) - new-id - duplicating-component? - true)) - changes - (map (d/getf objects) (:shapes obj))))))) - -(defn- prepare-duplicate-flows - [changes shapes page ids-map] - (let [flows (-> page :options :flows) - unames (volatile! (into #{} (map :name flows))) - frames-with-flow (->> shapes - (filter #(= (:type %) :frame)) - (filter #(some? (ctp/get-frame-flow flows (:id %)))))] - (if-not (empty? frames-with-flow) - (let [update-flows (fn [flows] - (reduce - (fn [flows frame] - (let [name (cfh/generate-unique-name @unames "Flow 1") - _ (vswap! unames conj name) - new-flow {:id (uuid/next) - :name name - :starting-frame (get ids-map (:id frame))}] - (ctp/add-flow flows new-flow))) - flows - frames-with-flow))] - (pcb/update-page-option changes :flows update-flows)) - changes))) - -(defn- prepare-duplicate-guides - [changes shapes page ids-map delta] - (let [guides (get-in page [:options :guides]) - frames (->> shapes (filter cfh/frame-shape?)) - - new-guides - (reduce - (fn [g frame] - (let [new-id (ids-map (:id frame)) - new-frame (-> frame (gsh/move delta)) - - new-guides - (->> guides - (vals) - (filter #(= (:frame-id %) (:id frame))) - (map #(-> % - (assoc :id (uuid/next)) - (assoc :frame-id new-id) - (assoc :position (if (= (:axis %) :x) - (+ (:position %) (- (:x new-frame) (:x frame))) - (+ (:position %) (- (:y new-frame) (:y frame))))))))] - (cond-> g - (not-empty new-guides) - (conj (into {} (map (juxt :id identity) new-guides)))))) - guides - frames)] - (-> (pcb/with-page changes page) - (pcb/set-page-option :guides new-guides)))) - -(defn duplicate-changes-update-indices - "Updates the changes to correctly set the indexes of the duplicated objects, - depending on the index of the original object respect their parent." - [objects ids changes] - (let [;; index-map is a map that goes from parent-id => vector([id index-in-parent]) - index-map (reduce (fn [index-map id] - (let [parent-id (get-in objects [id :parent-id]) - parent-index (cfh/get-position-on-parent objects id)] - (update index-map parent-id (fnil conj []) [id parent-index]))) - {} - ids) - - inc-indices - (fn [[offset result] [id index]] - [(inc offset) (conj result [id (+ index offset)])]) - - fix-indices - (fn [_ entry] - (->> entry - (sort-by second) - (reduce inc-indices [1 []]) - (second) - (into {}))) - - objects-indices (->> index-map (d/mapm fix-indices) (vals) (reduce merge))] - - (pcb/amend-changes - changes - (fn [change] - (assoc change :index (get objects-indices (:old-id change))))))) (defn clear-memorize-duplicated [] @@ -698,62 +415,77 @@ (gpt/subtract new-pos pt-obj))))) +(defn duplicate-shapes + [ids & {:keys [move-delta? alt-duplication? change-selection? return-ref] + :or {move-delta? false alt-duplication? false change-selection? true return-ref nil}}] + (ptk/reify ::duplicate-shapes + ptk/WatchEvent + (watch [it state _] + (let [page (wsh/lookup-page state) + objects (:objects page) + ids (into #{} + (comp (map (d/getf objects)) + (filter #(ctk/allow-duplicate? objects %)) + (map :id)) + ids)] + (when (seq ids) + (let [obj (get objects (first ids)) + delta (if move-delta? + (calc-duplicate-delta obj state objects) + (gpt/point 0 0)) + + file-id (:current-file-id state) + libraries (wsh/get-libraries state) + library-data (wsh/get-file state file-id) + + changes (-> (pcb/empty-changes it) + (cll/generate-duplicate-changes objects page ids delta libraries library-data file-id) + (cll/generate-duplicate-changes-update-indices objects ids)) + + tags (or (:tags changes) #{}) + + changes (cond-> changes alt-duplication? (assoc :tags (conj tags :alt-duplication))) + + id-original (first ids) + + new-ids (->> changes + :redo-changes + (filter #(= (:type %) :add-obj)) + (filter #(ids (:old-id %))) + (map #(get-in % [:obj :id])) + (into (d/ordered-set))) + + id-duplicated (first new-ids) + + frames (into #{} + (map #(get-in objects [% :frame-id])) + ids) + undo-id (js/Symbol)] + + ;; Warning: This order is important for the focus mode. + (->> (rx/of + (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (when change-selection? + (select-shapes new-ids)) + (ptk/data-event :layout/update {:ids frames}) + (memorize-duplicated id-original id-duplicated) + (dwu/commit-undo-transaction undo-id)) + (rx/tap #(when (some? return-ref) + (reset! return-ref id-duplicated)))))))))) + (defn duplicate-selected ([move-delta?] (duplicate-selected move-delta? false)) ([move-delta? alt-duplication?] (ptk/reify ::duplicate-selected ptk/WatchEvent - (watch [it state _] + (watch [_ state _] (when (or (not move-delta?) (nil? (get-in state [:workspace-local :transform]))) - (let [page (wsh/lookup-page state) - objects (:objects page) - selected (->> (wsh/lookup-selected state) - (map #(get objects %)) - (remove #(ctk/in-component-copy-not-root? %)) ;; We don't want to change the structure of component copies - (map :id) - set)] - (when (seq selected) - (let [obj (get objects (first selected)) - delta (if move-delta? - (calc-duplicate-delta obj state objects) - (gpt/point 0 0)) - - file-id (:current-file-id state) - libraries (wsh/get-libraries state) - library-data (wsh/get-file state file-id) - - changes (->> (prepare-duplicate-changes objects page selected delta it libraries library-data file-id) - (duplicate-changes-update-indices objects selected)) - - tags (or (:tags changes) #{}) - - changes (cond-> changes alt-duplication? (assoc :tags (conj tags :alt-duplication))) - - id-original (first selected) - - new-selected (->> changes - :redo-changes - (filter #(= (:type %) :add-obj)) - (filter #(selected (:old-id %))) - (map #(get-in % [:obj :id])) - (into (d/ordered-set))) - - id-duplicated (first new-selected) - - frames (into #{} - (map #(get-in objects [% :frame-id])) - selected) - undo-id (js/Symbol)] - - ;; Warning: This order is important for the focus mode. - (rx/of - (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (select-shapes new-selected) - (ptk/data-event :layout/update {:ids frames}) - (memorize-duplicated id-original id-duplicated) - (dwu/commit-undo-transaction undo-id)))))))))) + (let [selected (wsh/lookup-selected state)] + (rx/of (duplicate-shapes selected + :move-delta? move-delta? + :alt-duplication? alt-duplication?)))))))) (defn change-hover-state [id value] @@ -782,6 +514,9 @@ (defn toggle-focus-mode [] (ptk/reify ::toggle-focus-mode + ev/Event + (-data [_] {}) + ptk/UpdateEvent (update [_ state] (let [selected (wsh/lookup-selected state)] diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index 37e40cf91a83cdd50e9384d6e3686cf446801313..3f7440c063996537d261bc2dde00e9e06f62c474 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -15,12 +15,13 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes.flex-layout :as flex] [app.common.geom.shapes.grid-layout :as grid] + [app.common.logic.libraries :as cll] [app.common.types.component :as ctc] [app.common.types.modifiers :as ctm] [app.common.types.shape.layout :as ctl] [app.common.uuid :as uuid] + [app.main.data.changes :as dch] [app.main.data.events :as ev] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.colors :as cl] [app.main.data.workspace.grid-layout.editor :as dwge] [app.main.data.workspace.modifiers :as dwm] @@ -72,7 +73,7 @@ :layout-grid-columns []}) (defn get-layout-initializer - [type from-frame?] + [type from-frame? calculate-params?] (let [[initial-layout-data calculate-params] (case type :flex [initial-flex-layout flex/calculate-params] @@ -87,9 +88,11 @@ (cond-> (not from-frame?) (assoc :show-content true :hide-in-viewer true))) - params (calculate-params objects (cfh/get-immediate-children objects (:id shape)) shape)] + params (when calculate-params? + (calculate-params objects (cfh/get-immediate-children objects (:id shape)) shape))] (cond-> (merge shape params) - (= type :grid) (-> (ctl/assign-cells objects) ctl/reorder-grid-children)))))) + (= type :grid) + (-> (ctl/assign-cells objects) ctl/reorder-grid-children)))))) ;; Never call this directly but through the data-event `:layout/update` ;; Otherwise a lot of cycle dependencies could be generated @@ -116,7 +119,14 @@ (->> stream (rx/filter (ptk/type? :layout/update)) (rx/map deref) - (rx/map #(update-layout-positions %)) + ;; We buffer the updates to the layout so if there are many changes at the same time + ;; they are process together. It will get a better performance. + (rx/buffer-time 100) + (rx/filter #(d/not-empty? %)) + (rx/map + (fn [data] + (let [ids (reduce #(into %1 (:ids %2)) #{} data)] + (update-layout-positions {:ids ids})))) (rx/take-until stopper)))))) (defn finalize @@ -124,7 +134,7 @@ (ptk/reify ::finalize)) (defn create-layout-from-id - [id type from-frame?] + [id type & {:keys [from-frame? calculate-params?] :or {from-frame? false calculate-params? true}}] (dm/assert! "expected uuid for `id`" (uuid? id)) @@ -135,11 +145,11 @@ (let [objects (wsh/lookup-page-objects state) parent (get objects id) undo-id (js/Symbol) - layout-initializer (get-layout-initializer type from-frame?)] + layout-initializer (get-layout-initializer type from-frame? calculate-params?)] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes [id] layout-initializer {:with-objects? true}) - (dch/update-shapes (dm/get-prop parent :shapes) #(dissoc % :constraints-h :constraints-v)) + (dwsh/update-shapes [id] layout-initializer {:with-objects? true}) + (dwsh/update-shapes (dm/get-prop parent :shapes) #(dissoc % :constraints-h :constraints-v)) (ptk/data-event :layout/update {:ids [id]}) (dwu/commit-undo-transaction undo-id)))))) @@ -177,9 +187,9 @@ (dwse/select-shapes ordered-ids) (dwsh/create-artboard-from-selection new-shape-id parent-id group-index (:name (first selected-shapes))) (cl/remove-all-fills [new-shape-id] {:color clr/black :opacity 1}) - (create-layout-from-id new-shape-id type false) - (dch/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto)) - (dch/update-shapes selected #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix)) + (create-layout-from-id new-shape-id type) + (dwsh/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto)) + (dwsh/update-shapes selected #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix)) (dwsh/delete-shapes page-id selected) (ptk/data-event :layout/update {:ids [new-shape-id]}) (dwu/commit-undo-transaction undo-id))) @@ -188,9 +198,9 @@ (rx/of (dwsh/create-artboard-from-selection new-shape-id) (cl/remove-all-fills [new-shape-id] {:color clr/black :opacity 1}) - (create-layout-from-id new-shape-id type false) - (dch/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto)) - (dch/update-shapes selected #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix)))) + (create-layout-from-id new-shape-id type) + (dwsh/update-shapes [new-shape-id] #(assoc % :layout-item-h-sizing :auto :layout-item-v-sizing :auto)) + (dwsh/update-shapes selected #(assoc % :layout-item-h-sizing :fix :layout-item-v-sizing :fix)))) (rx/of (ptk/data-event :layout/update {:ids [new-shape-id]}) (dwu/commit-undo-transaction undo-id))))))) @@ -203,7 +213,7 @@ (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes ids #(apply dissoc % layout-keys)) + (dwsh/update-shapes ids #(apply dissoc % layout-keys)) (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))) @@ -227,7 +237,7 @@ (rx/of (dwu/start-undo-transaction undo-id) (if (and single? is-frame?) - (create-layout-from-id (first selected) type true) + (create-layout-from-id (first selected) type :from-frame? true) (create-layout-from-selection type)) (dwu/commit-undo-transaction undo-id)))))) @@ -256,7 +266,7 @@ (watch [_ _ _] (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes ids (d/patch-object changes)) + (dwsh/update-shapes ids (d/patch-object changes)) (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))) @@ -270,7 +280,7 @@ (watch [_ _ _] (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes + (dwsh/update-shapes ids (fn [shape] (case type @@ -303,7 +313,7 @@ (if shapes-to-delete (dwsh/delete-shapes shapes-to-delete) (rx/empty)) - (dch/update-shapes + (dwsh/update-shapes ids (fn [shape objects] (case type @@ -337,8 +347,9 @@ selected (set shapes-by-track) changes - (->> (dwse/prepare-duplicate-changes objects page selected (gpt/point 0 0) it libraries library-data file-id) - (dwse/duplicate-changes-update-indices objects selected)) + (-> (pcb/empty-changes it) + (cll/generate-duplicate-changes objects page selected (gpt/point 0 0) libraries library-data file-id) + (cll/generate-duplicate-changes-update-indices objects selected)) ;; Creates a map with shape-id => duplicated-shape-id ids-map @@ -376,7 +387,7 @@ (watch [_ _ _] (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes + (dwsh/update-shapes ids (fn [shape] (case type @@ -422,7 +433,7 @@ :row :layout-grid-rows :column :layout-grid-columns)] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes + (dwsh/update-shapes ids (fn [shape] (-> shape @@ -514,9 +525,9 @@ parent-ids (->> ids (map #(cfh/get-parent-id objects %))) undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes ids (d/patch-object changes)) - (dch/update-shapes children-ids (partial fix-child-sizing objects changes)) - (dch/update-shapes + (dwsh/update-shapes ids (d/patch-object changes)) + (dwsh/update-shapes children-ids (partial fix-child-sizing objects changes)) + (dwsh/update-shapes parent-ids (fn [parent objects] (-> parent @@ -535,8 +546,7 @@ (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - - (dch/update-shapes + (dwsh/update-shapes [layout-id] (fn [shape] (->> ids @@ -559,7 +569,7 @@ (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes + (dwsh/update-shapes [layout-id] (fn [shape objects] (case mode @@ -625,7 +635,7 @@ (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes + (dwsh/update-shapes [layout-id] (fn [shape objects] (let [cells (->> ids (map #(get-in shape [:layout-grid-cells %]))) @@ -657,7 +667,7 @@ (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes + (dwsh/update-shapes [layout-id] (fn [shape objects] (let [prev-data (-> (dm/get-in shape [:layout-grid-cells cell-id]) diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 7febfef6f457487ea3a3708eb6887b90e4730e44..fecb3f8e02a58f6b637d087c8afb5fca6a67913f 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -11,14 +11,13 @@ [app.common.files.changes-builder :as pcb] [app.common.files.helpers :as cfh] [app.common.files.shapes-helpers :as cfsh] + [app.common.logic.shapes :as cls] [app.common.schema :as sm] [app.common.types.container :as ctn] - [app.common.types.page :as ctp] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] - [app.common.types.shape.interactions :as ctsi] + [app.main.data.changes :as dch] [app.main.data.comments :as dc] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] @@ -27,6 +26,73 @@ [beicon.v2.core :as rx] [potok.v2.core :as ptk])) +(def ^:private update-layout-attr? #{:hidden}) + +(defn- add-undo-group + [changes state] + (let [undo (:workspace-undo state) + items (:items undo) + index (or (:index undo) (dec (count items))) + prev-item (when-not (or (empty? items) (= index -1)) + (get items index)) + undo-group (:undo-group prev-item) + add-undo-group? (and + (not (nil? undo-group)) + (= (get-in changes [:redo-changes 0 :type]) :mod-obj) + (= (get-in prev-item [:redo-changes 0 :type]) :add-obj) + (contains? (:tags prev-item) :alt-duplication))] ;; This is a copy-and-move with mouse+alt + + (cond-> changes add-undo-group? (assoc :undo-group undo-group)))) + +(defn update-shapes + ([ids update-fn] (update-shapes ids update-fn nil)) + ([ids update-fn {:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id ignore-touched undo-group with-objects?] + :or {reg-objects? false save-undo? true stack-undo? false ignore-touched false with-objects? false}}] + + (dm/assert! + "expected a valid coll of uuid's" + (sm/check-coll-of-uuid! ids)) + + (dm/assert! (fn? update-fn)) + + (ptk/reify ::update-shapes + ptk/WatchEvent + (watch [it state _] + (let [page-id (or page-id (:current-page-id state)) + objects (wsh/lookup-page-objects state page-id) + ids (into [] (filter some?) ids) + + update-layout-ids + (->> ids + (map (d/getf objects)) + (filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs :with-objects? with-objects?}))) + (map :id)) + + changes (-> (pcb/empty-changes it page-id) + (pcb/set-save-undo? save-undo?) + (pcb/set-stack-undo? stack-undo?) + (cls/generate-update-shapes ids + update-fn + objects + {:attrs attrs + :ignore-tree ignore-tree + :ignore-touched ignore-touched + :with-objects? with-objects?}) + (cond-> undo-group + (pcb/set-undo-group undo-group))) + + changes (add-undo-group changes state)] + (rx/concat + (if (seq (:redo-changes changes)) + (let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))] + (rx/of (dch/commit-changes changes))) + (rx/empty)) + + ;; Update layouts for properties marked + (if (d/not-empty? update-layout-ids) + (rx/of (ptk/data-event :layout/update {:ids update-layout-ids})) + (rx/empty)))))))) + (defn add-shape ([shape] (add-shape shape {})) @@ -85,7 +151,6 @@ (rx/of (dch/commit-changes changes)) (rx/empty)))))) -(declare real-delete-shapes) (declare update-shape-flags) (defn delete-shapes @@ -104,200 +169,19 @@ file (wsh/get-file state file-id) page (wsh/lookup-page state page-id) objects (wsh/lookup-page-objects state page-id) - components-v2 (features/active-feature? state "components/v2") - - ids (cfh/clean-loops objects ids) - - in-component-copy? - (fn [shape-id] - ;; Look for shapes that are inside a component copy, but are - ;; not the root. In this case, they must not be deleted, - ;; but hidden (to be able to recover them more easily). - ;; Unless we are doing a component swap, in which case we want - ;; to delete the old shape - (let [shape (get objects shape-id)] - (and (ctn/has-any-copy-parent? objects shape) - (not (:component-swap options))))) - - [ids-to-delete ids-to-hide] - (if components-v2 - (loop [ids-seq (seq ids) - ids-to-delete [] - ids-to-hide []] - (let [id (first ids-seq)] - (if (nil? id) - [ids-to-delete ids-to-hide] - (if (in-component-copy? id) - (recur (rest ids-seq) - ids-to-delete - (conj ids-to-hide id)) - (recur (rest ids-seq) - (conj ids-to-delete id) - ids-to-hide))))) - [ids []]) - - undo-id (or (:undo-id options) (js/Symbol))] - - (rx/concat - (rx/of (dwu/start-undo-transaction undo-id) - (update-shape-flags ids-to-hide {:hidden true :undo-group (:undo-group options)})) - (real-delete-shapes file page objects ids-to-delete it {:components-v2 components-v2 - :ignore-touched (:component-swap options) - :undo-group (:undo-group options) - :undo-id undo-id}) - (rx/of (dwu/commit-undo-transaction undo-id)))))))) - -(defn- real-delete-shapes-changes - ([file page objects ids it {:keys [undo-group] :as options}] - (let [changes (-> (pcb/empty-changes it (:id page)) - (pcb/set-undo-group undo-group) - (pcb/with-page page) - (pcb/with-objects objects) - (pcb/with-library-data file))] - (real-delete-shapes-changes changes file page objects ids it options))) - ([changes file page objects ids _it {:keys [components-v2 ignore-touched]}] - (let [lookup (d/getf objects) - groups-to-unmask - (reduce (fn [group-ids id] - ;; When the shape to delete is the mask of a masked group, - ;; the mask condition must be removed, and it must be - ;; converted to a normal group. - (let [obj (lookup id) - parent (lookup (:parent-id obj))] - (if (and (:masked-group parent) - (= id (first (:shapes parent)))) - (conj group-ids (:id parent)) - group-ids))) - #{} - ids) - - interacting-shapes - (filter (fn [shape] - ;; If any of the deleted shapes is the destination of - ;; some interaction, this must be deleted, too. - (let [interactions (:interactions shape)] - (some #(and (ctsi/has-destination %) - (contains? ids (:destination %))) - interactions))) - (vals objects)) - - ids-set (set ids) - guides-to-remove - (->> (dm/get-in page [:options :guides]) - (vals) - (filter #(contains? ids-set (:frame-id %))) - (map :id)) - - guides - (->> guides-to-remove - (reduce dissoc (dm/get-in page [:options :guides]))) - - starting-flows - (filter (fn [flow] - ;; If any of the deleted is a frame that starts a flow, - ;; this must be deleted, too. - (contains? ids (:starting-frame flow))) - (-> page :options :flows)) - - all-parents - (reduce (fn [res id] - ;; All parents of any deleted shape must be resized. - (into res (cfh/get-parent-ids objects id))) - (d/ordered-set) - ids) - - all-children - (->> ids ;; Children of deleted shapes must be also deleted. - (reduce (fn [res id] - (into res (cfh/get-children-ids objects id))) - []) - (reverse) - (into (d/ordered-set))) - - find-all-empty-parents - (fn recursive-find-empty-parents [empty-parents] - (let [all-ids (into empty-parents ids) - contains? (partial contains? all-ids) - xform (comp (map lookup) - (filter #(or (cfh/group-shape? %) (cfh/bool-shape? %))) - (remove #(->> (:shapes %) (remove contains?) seq)) - (map :id)) - parents (into #{} xform all-parents)] - (if (= empty-parents parents) - empty-parents - (recursive-find-empty-parents parents)))) - - empty-parents - ;; Any parent whose children are all deleted, must be deleted too. - (into (d/ordered-set) (find-all-empty-parents #{})) - - components-to-delete - (if components-v2 - (reduce (fn [components id] - (let [shape (get objects id)] - (if (and (= (:component-file shape) (:id file)) ;; Main instances should exist only in local file - (:main-instance shape)) ;; but check anyway - (conj components (:component-id shape)) - components))) - [] - (into ids all-children)) - []) - - changes (-> changes - (pcb/set-page-option :guides guides)) - - changes (reduce (fn [changes component-id] - ;; It's important to delete the component before the main instance, because we - ;; need to store the instance position if we want to restore it later. - (pcb/delete-component changes component-id (:id page))) - changes - components-to-delete) - - changes (-> changes - (pcb/remove-objects all-children {:ignore-touched true}) - (pcb/remove-objects ids {:ignore-touched ignore-touched}) - (pcb/remove-objects empty-parents) - (pcb/resize-parents all-parents) - (pcb/update-shapes groups-to-unmask - (fn [shape] - (assoc shape :masked-group false))) - (pcb/update-shapes (map :id interacting-shapes) - (fn [shape] - (d/update-when shape :interactions - (fn [interactions] - (into [] - (remove #(and (ctsi/has-destination %) - (contains? ids (:destination %)))) - interactions))))) - (cond-> (seq starting-flows) - (pcb/update-page-option :flows (fn [flows] - (->> (map :id starting-flows) - (reduce ctp/remove-flow flows))))))] - [changes all-parents]))) - - -(defn delete-shapes-changes - [changes file page objects ids it components-v2 ignore-touched] - (let [[changes _all-parents] (real-delete-shapes-changes changes - file - page - objects - ids - it - {:components-v2 components-v2 - :ignore-touched ignore-touched})] - changes)) - -(defn- real-delete-shapes - [file page objects ids it options] - (let [[changes all-parents] (real-delete-shapes-changes file page objects ids it options) - undo-id (or (:undo-id options) (js/Symbol))] - (rx/of (dwu/start-undo-transaction undo-id) - (dc/detach-comment-thread ids) - (dch/commit-changes changes) - (ptk/data-event :layout/update {:ids all-parents :undo-group (:undo-group options)}) - (dwu/commit-undo-transaction undo-id)))) + undo-id (or (:undo-id options) (js/Symbol)) + [all-parents changes] (-> (pcb/empty-changes it (:id page)) + (cls/generate-delete-shapes file page objects ids {:components-v2 components-v2 + :ignore-touched (:component-swap options) + :undo-group (:undo-group options) + :undo-id undo-id}))] + + (rx/of (dwu/start-undo-transaction undo-id) + (dc/detach-comment-thread ids) + (dch/commit-changes changes) + (ptk/data-event :layout/update {:ids all-parents :undo-group (:undo-group options)}) + (dwu/commit-undo-transaction undo-id))))))) (defn create-and-add-shape [type frame-x frame-y {:keys [width height] :as attrs}] @@ -353,13 +237,14 @@ (ptk/reify ::create-artboard-from-selection ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - selected (wsh/lookup-selected state) - selected (cfh/clean-loops objects selected) + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + selected (->> (wsh/lookup-selected state) + (cfh/clean-loops objects) + (remove #(ctn/has-any-copy-parent? objects (get objects %)))) - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects)) + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects)) [frame-shape changes] (cfsh/prepare-create-artboard-from-selection changes @@ -386,7 +271,7 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn update-shape-flags - [ids {:keys [blocked hidden transforming undo-group] :as flags}] + [ids {:keys [blocked hidden undo-group] :as flags}] (dm/assert! "expected valid coll of uuids" (every? uuid? ids)) @@ -402,15 +287,14 @@ (fn [obj] (cond-> obj (boolean? blocked) (assoc :blocked blocked) - (boolean? hidden) (assoc :hidden hidden) - (boolean? transforming) (assoc :transforming transforming))) + (boolean? hidden) (assoc :hidden hidden))) objects (wsh/lookup-page-objects state) ;; We have change only the hidden behaviour, to hide only the ;; selected shape, block behaviour remains the same. ids (if (boolean? blocked) (into ids (->> ids (mapcat #(cfh/get-children-ids objects %)))) ids)] - (rx/of (dch/update-shapes ids update-fn {:attrs #{:blocked :hidden :transforming} :undo-group undo-group})))))) + (rx/of (update-shapes ids update-fn {:attrs #{:blocked :hidden} :undo-group undo-group})))))) (defn toggle-visibility-selected [] @@ -418,7 +302,7 @@ ptk/WatchEvent (watch [_ state _] (let [selected (wsh/lookup-selected state)] - (rx/of (dch/update-shapes selected #(update % :hidden not))))))) + (rx/of (update-shapes selected #(update % :hidden not))))))) (defn toggle-lock-selected [] @@ -426,7 +310,7 @@ ptk/WatchEvent (watch [_ state _] (let [selected (wsh/lookup-selected state)] - (rx/of (dch/update-shapes selected #(update % :blocked not))))))) + (rx/of (update-shapes selected #(update % :blocked not))))))) ;; FIXME: this need to be refactored @@ -456,8 +340,8 @@ (map (partial vector id))))))) (d/group-by first second) (map (fn [[page-id frame-ids]] - (dch/update-shapes frame-ids #(dissoc % :use-for-thumbnail) {:page-id page-id}))))) + (update-shapes frame-ids #(dissoc % :use-for-thumbnail) {:page-id page-id}))))) ;; And finally: toggle the flag value on all the selected shapes - (rx/of (dch/update-shapes selected #(update % :use-for-thumbnail not)) + (rx/of (update-shapes selected #(update % :use-for-thumbnail not)) (dwu/commit-undo-transaction undo-id))))))) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 66ad43b6edd06ce36507d98ac5857f0a1e1f67b8..5741087697d86b6c23b4550059eb536ac2da19b9 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -8,12 +8,12 @@ (:require [app.main.data.events :as ev] [app.main.data.exports :as de] + [app.main.data.modal :as modal] [app.main.data.preview :as dp] [app.main.data.shortcuts :as ds] [app.main.data.users :as du] [app.main.data.workspace :as dw] [app.main.data.workspace.colors :as mdc] - [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.layers :as dwly] [app.main.data.workspace.libraries :as dwl] @@ -23,10 +23,12 @@ [app.main.data.workspace.texts :as dwtxt] [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.undo :as dwu] + [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.hooks.resize :as r] - [app.util.dom :as dom])) + [app.util.dom :as dom] + [potok.v2.core :as ptk])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shortcuts @@ -49,12 +51,12 @@ :undo {:tooltip (ds/meta "Z") :command (ds/c-mod "z") :subsections [:edit] - :fn #(emit-when-no-readonly dwc/undo)} + :fn #(emit-when-no-readonly dwu/undo)} :redo {:tooltip (ds/meta "Y") :command [(ds/c-mod "shift+z") (ds/c-mod "y")] :subsections [:edit] - :fn #(emit-when-no-readonly dwc/redo)} + :fn #(emit-when-no-readonly dwu/redo)} :clear-undo {:tooltip (ds/alt "Q") :command "alt+q" @@ -118,22 +120,22 @@ :group {:tooltip (ds/meta "G") :command (ds/c-mod "g") :subsections [:modify-layers] - :fn #(emit-when-no-readonly dw/group-selected)} + :fn #(emit-when-no-readonly (dw/group-selected))} :ungroup {:tooltip (ds/shift "G") :command "shift+g" :subsections [:modify-layers] - :fn #(emit-when-no-readonly dw/ungroup-selected)} + :fn #(emit-when-no-readonly (dw/ungroup-selected))} :mask {:tooltip (ds/meta "M") :command (ds/c-mod "m") :subsections [:modify-layers] - :fn #(emit-when-no-readonly dw/mask-group)} + :fn #(emit-when-no-readonly (dw/mask-group))} :unmask {:tooltip (ds/meta-shift "M") :command (ds/c-mod "shift+m") :subsections [:modify-layers] - :fn #(emit-when-no-readonly dw/unmask-group)} + :fn #(emit-when-no-readonly (dw/unmask-group))} :create-component {:tooltip (ds/meta "K") :command (ds/c-mod "k") @@ -364,19 +366,12 @@ :subsections [:main-menu] :fn #(st/emit! (dw/select-all))} - :toggle-grid {:tooltip (ds/meta "'") + :toggle-guides {:tooltip (ds/meta "'") ;;https://github.com/ccampbell/mousetrap/issues/85 :command [(ds/c-mod "'") (ds/c-mod "219")] :show-command (ds/c-mod "'") :subsections [:main-menu] - :fn #(st/emit! (toggle-layout-flag :display-grid))} - - :toggle-snap-grid {:tooltip (ds/meta-shift "'") - ;;https://github.com/ccampbell/mousetrap/issues/85 - :command [(ds/c-mod "shift+'") (ds/c-mod "shift+219")] - :show-command (ds/c-mod "shift+'") - :subsections [:main-menu] - :fn #(st/emit! (toggle-layout-flag :snap-grid))} + :fn #(st/emit! (toggle-layout-flag :display-guides))} :toggle-alignment {:tooltip (ds/meta "\\") :command (ds/c-mod "\\") @@ -402,12 +397,19 @@ :command (ds/c-mod "shift+e") :subsections [:basics :main-menu] :fn #(st/emit! - (de/show-workspace-export-dialog))} + (de/show-workspace-export-dialog {:origin "workspace:shortcuts"}))} - :toggle-snap-guide {:tooltip (ds/meta-shift "G") - :command (ds/c-mod "shift+g") - :subsections [:main-menu] - :fn #(st/emit! (toggle-layout-flag :snap-guides))} + :toggle-snap-ruler-guide {:tooltip (ds/meta-shift "G") + :command (ds/c-mod "shift+g") + :subsections [:main-menu] + :fn #(st/emit! (toggle-layout-flag :snap-ruler-guides))} + + :toggle-snap-guides {:tooltip (ds/meta-shift "'") + ;;https://github.com/ccampbell/mousetrap/issues/85 + :command [(ds/c-mod "shift+'") (ds/c-mod "shift+219")] + :show-command (ds/c-mod "shift+'") + :subsections [:main-menu] + :fn #(st/emit! (toggle-layout-flag :snap-guides))} :show-shortcuts {:tooltip "?" :command "?" @@ -416,34 +418,36 @@ ;; PANELS - :toggle-layers {:tooltip (ds/alt "L") - :command (ds/a-mod "l") - :subsections [:panels] - :fn #(st/emit! (dw/go-to-layout :layers))} - - :toggle-assets {:tooltip (ds/alt "I") - :command (ds/a-mod "i") - :subsections [:panels] - :fn #(st/emit! (dw/go-to-layout :assets))} - - :toggle-history {:tooltip (ds/alt "H") - :command (ds/a-mod "h") - :subsections [:panels] - :fn #(emit-when-no-readonly (dw/go-to-layout :document-history))} - - :toggle-colorpalette {:tooltip (ds/alt "P") - :command (ds/a-mod "p") - :subsections [:panels] - :fn #(do (r/set-resize-type! :bottom) - (emit-when-no-readonly (dw/remove-layout-flag :textpalette) - (toggle-layout-flag :colorpalette)))} - - :toggle-textpalette {:tooltip (ds/alt "T") - :command (ds/a-mod "t") - :subsections [:panels] - :fn #(do (r/set-resize-type! :bottom) - (emit-when-no-readonly (dw/remove-layout-flag :colorpalette) - (toggle-layout-flag :textpalette)))} + :toggle-layers {:tooltip (ds/alt "L") + :command (ds/a-mod "l") + :subsections [:panels] + :fn #(st/emit! (dw/go-to-layout :layers))} + + :toggle-assets {:tooltip (ds/alt "I") + :command (ds/a-mod "i") + :subsections [:panels] + :fn #(st/emit! (dw/go-to-layout :assets))} + + :toggle-history {:tooltip (ds/alt "H") + :command (ds/a-mod "h") + :subsections [:panels] + :fn #(emit-when-no-readonly (dw/go-to-layout :document-history))} + + :toggle-colorpalette {:tooltip (ds/alt "P") + :command (ds/a-mod "p") + :subsections [:panels] + :fn #(do (r/set-resize-type! :bottom) + (emit-when-no-readonly (dw/remove-layout-flag :hide-palettes) + (dw/remove-layout-flag :textpalette) + (toggle-layout-flag :colorpalette)))} + + :toggle-textpalette {:tooltip (ds/alt "T") + :command (ds/a-mod "t") + :subsections [:panels] + :fn #(do (r/set-resize-type! :bottom) + (emit-when-no-readonly (dw/remove-layout-flag :hide-palettes) + (dw/remove-layout-flag :colorpalette) + (toggle-layout-flag :textpalette)))} :hide-ui {:tooltip "\\" :command "\\" @@ -482,10 +486,10 @@ :subsections [:zoom-workspace] :fn identity} - :zoom-lense-decrease {:tooltip (ds/alt "Z") - :command "alt+z" - :subsections [:zoom-workspace] - :fn identity} + :zoom-lense-decrease {:tooltip (ds/alt "Z") + :command "alt+z" + :subsections [:zoom-workspace] + :fn identity} ;; NAVIGATION @@ -547,17 +551,28 @@ :subsections [:shape] :fn #(emit-when-no-readonly (dw/create-bool :exclude))} - ;; PREVIEW - :preview-frame {:tooltip (ds/meta (ds/alt ds/enter)) - :command (ds/c-mod "alt+enter") - :fn #(emit-when-no-readonly (dp/open-preview-selected))} - ;; THEME - :toggle-theme {:tooltip (ds/alt "M") - :command (ds/a-mod "m") - :subsections [:basics] - :fn #(st/emit! (with-meta (du/toggle-theme) - {::ev/origin "workspace:shortcut"}))}}) + :toggle-theme {:tooltip (ds/alt "M") + :command (ds/a-mod "m") + :subsections [:basics] + :fn #(st/emit! (with-meta (du/toggle-theme) + {::ev/origin "workspace:shortcut"}))} + + + ;; PLUGINS + :plugins {:tooltip (ds/meta (ds/alt "P")) + :command (ds/c-mod "alt+p") + :subsections [:basics] + :fn #(when (features/active-feature? @st/state "plugins/runtime") + (st/emit! + (ptk/event ::ev/event {::ev/name "open-plugins-manager" ::ev/origin "workspace:shortcuts"}) + (modal/show :plugin-management {})))}}) + +(def debug-shortcuts + ;; PREVIEW + {:preview-frame {:tooltip (ds/meta (ds/alt ds/enter)) + :command (ds/c-mod "alt+enter") + :fn #(emit-when-no-readonly (dp/open-preview-selected))}}) (def opacity-shortcuts (into {} (->> @@ -569,7 +584,9 @@ :fn #(emit-when-no-readonly (dwly/pressed-opacity n))}]))))) (def shortcuts - (merge base-shortcuts opacity-shortcuts dwtxts/shortcuts)) + (cond-> (merge base-shortcuts opacity-shortcuts dwtxts/shortcuts) + *assert* + (merge debug-shortcuts))) (defn get-tooltip [shortcut] (assert (contains? shortcuts shortcut) (str shortcut)) diff --git a/frontend/src/app/main/data/workspace/state_helpers.cljs b/frontend/src/app/main/data/workspace/state_helpers.cljs index b04fa88f648906ddebe7a2d36ec2a57e5dc5b400..7249a1f3fd00b7931268fdc16baa3192013628a8 100644 --- a/frontend/src/app/main/data/workspace/state_helpers.cljs +++ b/frontend/src/app/main/data/workspace/state_helpers.cljs @@ -123,6 +123,15 @@ (get state :workspace-data) (dm/get-in state [:workspace-libraries file-id :data]))) +(defn get-file-full + "Get the data content of the given file (it may be the current file + or one library)." + [state file-id] + (if (= file-id (:current-file-id state)) + (-> (get state :workspace-file) + (assoc :data (get state :workspace-data))) + (dm/get-in state [:workspace-libraries file-id :data]))) + (defn get-libraries "Retrieve all libraries, including the local file." [state] diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index fa159cf04cf7a15d903ce0cce295879735b697eb..6f04e7c66833266fae14512ee675199e2171a975 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -13,7 +13,8 @@ [app.common.svg :as csvg] [app.common.svg.shapes-builder :as csvg.shapes-builder] [app.common.types.shape-tree :as ctst] - [app.main.data.workspace.changes :as dch] + [app.common.uuid :as uuid] + [app.main.data.changes :as dch] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] @@ -60,52 +61,64 @@ (rx/reduce conj {}))) (defn add-svg-shapes - [svg-data position] - (ptk/reify ::add-svg-shapes - ptk/WatchEvent - (watch [it state _] - (try - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - frame-id (ctst/top-nested-frame objects position) - selected (wsh/lookup-selected state) - base (cfh/get-base-shape objects selected) - - selected-id (first selected) - selected-frame? (and (= 1 (count selected)) - (= :frame (dm/get-in objects [selected-id :type]))) - - parent-id (if (or selected-frame? (empty? selected)) - frame-id - (:parent-id base)) - - [new-shape new-children] - (csvg.shapes-builder/create-svg-shapes svg-data position objects frame-id parent-id selected true) - - changes (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - (pcb/add-object new-shape)) - - changes (reduce (fn [changes new-child] - (pcb/add-object changes new-child)) - changes - new-children) - - changes (pcb/resize-parents changes - (->> (:redo-changes changes) - (filter #(= :add-obj (:type %))) - (map :id) - (reverse) - (vec))) - undo-id (js/Symbol)] - - (rx/of (dwu/start-undo-transaction undo-id) - (dch/commit-changes changes) - (dws/select-shapes (d/ordered-set (:id new-shape))) - (ptk/data-event :layout/update {:ids [(:id new-shape)]}) - (dwu/commit-undo-transaction undo-id))) - - (catch :default cause - (js/console.log (.-stack cause)) - (rx/throw {:type :svg-parser - :data cause})))))) + ([svg-data position] + (add-svg-shapes nil svg-data position nil)) + + ([id svg-data position {:keys [change-selection? ignore-selection?] + :or {ignore-selection? false change-selection? true}}] + (ptk/reify ::add-svg-shapes + ptk/WatchEvent + (watch [it state _] + (try + (let [id (d/nilv id (uuid/next)) + page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + selected (if ignore-selection? #{} (wsh/lookup-selected state)) + base (cfh/get-base-shape objects selected) + + selected-id (first selected) + selected-frame? (and (= 1 (count selected)) + (= :frame (dm/get-in objects [selected-id :type]))) + + base-id (:parent-id base) + + frame-id (if (or selected-frame? (empty? selected) + (not= :frame (dm/get-in objects [base-id :type]))) + (ctst/top-nested-frame objects position) + base-id) + + parent-id (if (or selected-frame? (empty? selected)) + frame-id + base-id) + + [new-shape new-children] + (csvg.shapes-builder/create-svg-shapes id svg-data position objects frame-id parent-id selected true) + + changes (-> (pcb/empty-changes it page-id) + (pcb/with-objects objects) + (pcb/add-object new-shape)) + + changes (reduce (fn [changes new-child] + (pcb/add-object changes new-child)) + changes + new-children) + + changes (pcb/resize-parents changes + (->> (:redo-changes changes) + (filter #(= :add-obj (:type %))) + (map :id) + (reverse) + (vec))) + undo-id (js/Symbol)] + + (rx/of (dwu/start-undo-transaction undo-id) + (dch/commit-changes changes) + (when change-selection? + (dws/select-shapes (d/ordered-set (:id new-shape)))) + (ptk/data-event :layout/update {:ids [(:id new-shape)]}) + (dwu/commit-undo-transaction undo-id))) + + (catch :default cause + (rx/throw {:type :svg-parser + :data cause}))))))) + diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index c42a65378f2d1f5d5a19878a3c5efdd8ee9c7dbf..4d9785b6725262db348927a840ee4e3011772347 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -17,7 +17,6 @@ [app.common.types.modifiers :as ctm] [app.common.uuid :as uuid] [app.main.data.events :as ev] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.modifiers :as dwm] @@ -93,7 +92,7 @@ (some? (:current-page-id state)) (some? shape)) (rx/of - (dch/update-shapes + (dwsh/update-shapes [id] (fn [shape] (let [{:keys [width height position-data]} modifiers] @@ -206,6 +205,102 @@ ;; --- TEXT EDITION IMPL +(defn count-node-chars + ([node] + (count-node-chars node false)) + ([node last?] + (case (:type node) + ("root" "paragraph-set") + (apply + (concat (map count-node-chars (drop-last (:children node))) + (map #(count-node-chars % true) (take-last 1 (:children node))))) + + "paragraph" + (+ (apply + (map count-node-chars (:children node))) (if last? 0 1)) + + (count (:text node))))) + + +(defn decorate-range-info + "Adds information about ranges inside the metadata of the text nodes" + [content] + (->> (with-meta content {:start 0 :end (count-node-chars content)}) + (txt/transform-nodes + (fn [node] + (d/update-when + node + :children + (fn [children] + (let [start (-> node meta (:start 0))] + (->> children + (reduce (fn [[result start] node] + (let [end (+ start (count-node-chars node))] + [(-> result + (conj (with-meta node {:start start :end end}))) + end])) + [[] start]) + (first))))))))) + +(defn split-content-at + [content position] + (->> content + (txt/transform-nodes + (fn [node] + (and (txt/is-paragraph-node? node) + (< (-> node meta :start) position (-> node meta :end)))) + (fn [node] + (letfn + [(process-node [child] + (let [start (-> child meta :start) + end (-> child meta :end)] + (if (< start position end) + [(-> child + (vary-meta assoc :end position) + (update :text subs 0 (- position start))) + (-> child + (vary-meta assoc :start position) + (update :text subs (- position start)))] + [child])))] + (-> node + (d/update-when :children #(into [] (mapcat process-node) %)))))))) + +(defn update-content-range + [content start end attrs] + (->> content + (txt/transform-nodes + (fn [node] + (and (txt/is-text-node? node) + (and (>= (-> node meta :start) start) + (<= (-> node meta :end) end)))) + #(d/patch-object % attrs)))) + +(defn- update-text-range-attrs + [shape start end attrs] + (let [new-content (-> (:content shape) + (decorate-range-info) + (split-content-at start) + (split-content-at end) + (update-content-range start end attrs))] + (assoc shape :content new-content))) + +(defn update-text-range + [id start end attrs] + (ptk/reify ::update-text-range + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + shape (get objects id) + + update-fn + (fn [shape] + (cond-> shape + (cfh/text-shape? shape) + (update-text-range-attrs start end attrs))) + + shape-ids (cond (cfh/text-shape? shape) [id] + (cfh/group-shape? shape) (cfh/get-children-ids objects id))] + + (rx/of (dwsh/update-shapes shape-ids update-fn)))))) + (defn- update-text-content [shape pred-fn update-fn attrs] (let [update-attrs-fn #(update-fn % attrs) @@ -230,7 +325,7 @@ shape-ids (cond (cfh/text-shape? shape) [id] (cfh/group-shape? shape) (cfh/get-children-ids objects id))] - (rx/of (dch/update-shapes shape-ids update-fn)))))) + (rx/of (dwsh/update-shapes shape-ids update-fn)))))) (defn update-paragraph-attrs [{:keys [id attrs]}] @@ -257,7 +352,7 @@ (cfh/text-shape? shape) [id] (cfh/group-shape? shape) (cfh/get-children-ids objects id))] - (rx/of (dch/update-shapes shape-ids update-fn)))))))) + (rx/of (dwsh/update-shapes shape-ids update-fn)))))))) (defn update-text-attrs [{:keys [id attrs]}] @@ -277,19 +372,28 @@ shape-ids (cond (cfh/text-shape? shape) [id] (cfh/group-shape? shape) (cfh/get-children-ids objects id))] - (rx/of (dch/update-shapes shape-ids #(update-text-content % update-node? d/txt-merge attrs)))))))) - + (rx/of (dwsh/update-shapes shape-ids #(update-text-content % update-node? d/txt-merge attrs)))))))) (defn migrate-node [node] (let [color-attrs (select-keys node [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])] (cond-> node (nil? (:fills node)) - (assoc :fills (:fills txt/default-text-attrs)) + (assoc :fills []) - (and (d/not-empty? color-attrs) (nil? (:fills node))) + ;; Migrate old colors and remove the old fromat + (d/not-empty? color-attrs) (-> (dissoc :fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient) - (assoc :fills [color-attrs]))))) + (update :fills conj color-attrs)) + + ;; We don't have the fills attribute. It's an old text without color + ;; so need to be black + (and (nil? (:fills node)) (empty? color-attrs)) + (update :fills conj txt/default-text-attrs) + + ;; Remove duplicates from the fills + :always + (update :fills (comp vec distinct))))) (defn migrate-content [content] @@ -323,9 +427,11 @@ update-shape (fn [shape] - (d/update-when shape :content update-content))] + (-> shape + (dissoc :fills) + (d/update-when :content update-content)))] - (rx/of (dch/update-shapes shape-ids update-shape))))))) + (rx/of (dwsh/update-shapes shape-ids update-shape))))))) ;; --- RESIZE UTILS @@ -378,10 +484,9 @@ (let [ids (into #{} (filter changed-text?) (keys props))] (rx/of (dwu/start-undo-transaction undo-id) - (dch/update-shapes ids update-fn {:reg-objects? true - :stack-undo? true - :ignore-remote? true - :ignore-touched true}) + (dwsh/update-shapes ids update-fn {:reg-objects? true + :stack-undo? true + :ignore-touched true}) (ptk/data-event :layout/update {:ids ids}) (dwu/commit-undo-transaction undo-id)))))))) @@ -520,12 +625,12 @@ (watch [_ state _] (let [position-data (::update-position-data state)] (rx/concat - (rx/of (dch/update-shapes + (rx/of (dwsh/update-shapes (keys position-data) (fn [shape] (-> shape (assoc :position-data (get position-data (:id shape))))) - {:stack-undo? true :reg-objects? false :ignore-remote? true})) + {:stack-undo? true :reg-objects? false})) (rx/of (fn [state] (dissoc state ::update-position-data-debounce ::update-position-data)))))))) @@ -588,29 +693,32 @@ (rx/map #(update-attrs % attrs))) (rx/of (dwu/commit-undo-transaction undo-id))))))) - (defn apply-typography "A higher level event that has the resposability of to apply the specified typography to the selected shapes." - [typography file-id] - (ptk/reify ::apply-typography - ptk/WatchEvent - (watch [_ state _] - (let [editor-state (:workspace-editor-state state) - selected (wsh/lookup-selected state) - attrs (-> typography - (assoc :typography-ref-file file-id) - (assoc :typography-ref-id (:id typography)) - (dissoc :id :name)) - undo-id (js/Symbol)] - - (rx/concat - (rx/of (dwu/start-undo-transaction undo-id)) - (->> (rx/from (seq selected)) - (rx/map (fn [id] - (let [editor (get editor-state id)] - (update-text-attrs {:id id :editor editor :attrs attrs}))))) - (rx/of (dwu/commit-undo-transaction undo-id))))))) + ([typography file-id] + (apply-typography nil typography file-id)) + + ([ids typography file-id] + (assert (or (nil? ids) (and (set? ids) (every? uuid? ids)))) + (ptk/reify ::apply-typography + ptk/WatchEvent + (watch [_ state _] + (let [editor-state (:workspace-editor-state state) + ids (d/nilv ids (wsh/lookup-selected state)) + attrs (-> typography + (assoc :typography-ref-file file-id) + (assoc :typography-ref-id (:id typography)) + (dissoc :id :name)) + undo-id (js/Symbol)] + + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id)) + (->> (rx/from (seq ids)) + (rx/map (fn [id] + (let [editor (get editor-state id)] + (update-text-attrs {:id id :editor editor :attrs attrs}))))) + (rx/of (dwu/commit-undo-transaction undo-id)))))))) (defn generate-typography-name [{:keys [font-id font-variant-id] :as typography}] @@ -665,4 +773,3 @@ (rx/of (update-attrs (:id shape) {:typography-ref-id typ-id :typography-ref-file file-id})))))))) - diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index e2cb1cacc9fed8fae79bc82ad668c27bc635297c..625c207c62772f60bbc2adee4d42c4f936764203 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -10,74 +10,55 @@ [app.common.files.helpers :as cfh] [app.common.logging :as l] [app.common.thumbnails :as thc] - [app.main.data.workspace.changes :as dch] + [app.common.uuid :as uuid] + [app.main.data.changes :as dch] + [app.main.data.persistence :as-alias dps] [app.main.data.workspace.notifications :as-alias wnt] [app.main.data.workspace.state-helpers :as wsh] [app.main.rasterizer :as thr] [app.main.refs :as refs] [app.main.render :as render] [app.main.repo :as rp] - [app.main.store :as st] - [app.util.http :as http] [app.util.queue :as q] [app.util.time :as tp] [app.util.timers :as tm] [app.util.webapi :as wapi] [beicon.v2.core :as rx] + [cuerdas.core :as str] [potok.v2.core :as ptk])) -(l/set-level! :info) +(l/set-level! :warn) -(declare update-thumbnail) +(defn- find-request + [params item] + (and (= (unchecked-get params "file-id") + (unchecked-get item "file-id")) + (= (unchecked-get params "page-id") + (unchecked-get item "page-id")) + (= (unchecked-get params "shape-id") + (unchecked-get item "shape-id")) + (= (unchecked-get params "tag") + (unchecked-get item "tag")))) -(defn resolve-request - "Resolves the request to generate a thumbnail for the given ids." - [item] - (let [file-id (unchecked-get item "file-id") - page-id (unchecked-get item "page-id") - shape-id (unchecked-get item "shape-id") - tag (unchecked-get item "tag")] - (st/emit! (update-thumbnail file-id page-id shape-id tag)))) - -;; Defines the thumbnail queue -(defonce queue - (q/create resolve-request (/ 1000 30))) - -(defn create-request +(defn- create-request "Creates a request to generate a thumbnail for the given ids." [file-id page-id shape-id tag] - #js {:file-id file-id :page-id page-id :shape-id shape-id :tag tag}) + #js {:file-id file-id + :page-id page-id + :shape-id shape-id + :tag tag}) -(defn find-request - "Returns true if the given item matches the given ids." - [file-id page-id shape-id tag item] - (and (= file-id (unchecked-get item "file-id")) - (= page-id (unchecked-get item "page-id")) - (= shape-id (unchecked-get item "shape-id")) - (= tag (unchecked-get item "tag")))) - -(defn request-thumbnail - "Enqueues a request to generate a thumbnail for the given ids." - ([file-id page-id shape-id tag] - (request-thumbnail file-id page-id shape-id tag "unknown")) - ([file-id page-id shape-id tag requester] - (ptk/reify ::request-thumbnail - ptk/EffectEvent - (effect [_ _ _] - (l/dbg :hint "request thumbnail" :requester requester :file-id file-id :page-id page-id :shape-id shape-id :tag tag) - (q/enqueue-unique - queue - (create-request file-id page-id shape-id tag) - (partial find-request file-id page-id shape-id tag)))))) +;; Defines the thumbnail queue +(defonce queue + (q/create find-request (/ 1000 30))) ;; This function first renders the HTML calling `render/render-frame` that ;; returns HTML as a string, then we send that data to the iframe rasterizer ;; that returns the image as a Blob. Finally we create a URI for that blob. -(defn get-thumbnail +(defn- render-thumbnail "Returns the thumbnail for the given ids" - [state file-id page-id frame-id tag & {:keys [object-id]}] - - (let [object-id (or object-id (thc/fmt-object-id file-id page-id frame-id tag)) + [state file-id page-id frame-id tag] + (let [object-id (thc/fmt-object-id file-id page-id frame-id tag) tp (tp/tpoint-ms) objects (wsh/lookup-objects state file-id page-id) shape (get objects frame-id)] @@ -86,30 +67,47 @@ (rx/take 1) (rx/filter some?) (rx/mapcat thr/render) - (rx/map (fn [blob] (wapi/create-uri blob))) (rx/tap #(l/dbg :hint "thumbnail rendered" :elapsed (dm/str (tp) "ms")))))) +(defn- request-thumbnail + "Enqueues a request to generate a thumbnail for the given ids." + [state file-id page-id shape-id tag] + (let [request (create-request file-id page-id shape-id tag)] + (q/enqueue-unique queue request (partial render-thumbnail state file-id page-id shape-id tag)))) + (defn clear-thumbnail ([file-id page-id frame-id tag] - (clear-thumbnail (thc/fmt-object-id file-id page-id frame-id tag))) - ([object-id] - (let [emit-rpc? (volatile! false)] + (clear-thumbnail file-id (thc/fmt-object-id file-id page-id frame-id tag))) + ([file-id object-id] + (let [pending (volatile! false)] (ptk/reify ::clear-thumbnail cljs.core/IDeref (-deref [_] object-id) ptk/UpdateEvent (update [_ state] - (let [uri (dm/get-in state [:workspace-thumbnails object-id])] - (if (some? uri) - (do - (l/dbg :hint "clear thumbnail" :object-id object-id) - (vreset! emit-rpc? true) - (tm/schedule-on-idle (partial wapi/revoke-uri uri)) - (update state :workspace-thumbnails dissoc object-id)) - - state))))))) + (update state :workspace-thumbnails + (fn [thumbs] + (if-let [uri (get thumbs object-id)] + (do (vreset! pending uri) + (dissoc thumbs object-id)) + thumbs)))) + + ptk/WatchEvent + (watch [_ _ _] + (if-let [uri @pending] + (do + (l/trc :hint "clear-thumbnail" :uri uri) + (when (str/starts-with? uri "blob:") + (tm/schedule-on-idle (partial wapi/revoke-uri uri))) + + (let [params {:file-id file-id + :object-id object-id}] + (->> (rp/cmd! :delete-file-object-thumbnail params) + (rx/catch rx/empty) + (rx/ignore)))) + (rx/empty))))))) (defn- assoc-thumbnail [object-id uri] @@ -141,8 +139,7 @@ (defn update-thumbnail "Updates the thumbnail information for the given `id`" - - [file-id page-id frame-id tag] + [file-id page-id frame-id tag requester] (let [object-id (thc/fmt-object-id file-id page-id frame-id tag)] (ptk/reify ::update-thumbnail cljs.core/IDeref @@ -150,38 +147,40 @@ ptk/WatchEvent (watch [_ state stream] - (l/dbg :hint "update thumbnail" :object-id object-id :tag tag) - ;; Send the update to the back-end - (->> (get-thumbnail state file-id page-id frame-id tag) - (rx/mapcat (fn [uri] - (rx/merge - (rx/of (assoc-thumbnail object-id uri)) - (->> (http/send! {:uri uri :response-type :blob :method :get}) - (rx/map :body) - (rx/mapcat (fn [blob] - ;; Send the data to backend - (let [params {:file-id file-id - :object-id object-id - :media blob - :tag (or tag "frame")}] - (rp/cmd! :create-file-object-thumbnail params)))) - (rx/catch rx/empty) - (rx/ignore))))) - (rx/catch (fn [cause] - (.error js/console cause) - (rx/empty))) - - ;; We cancel all the stream if user starts editing while - ;; thumbnail is generating - (rx/take-until - (->> stream - (rx/filter (ptk/type? ::clear-thumbnail)) - (rx/filter #(= (deref %) object-id))))))))) + (l/dbg :hint "update thumbnail" :requester requester :object-id object-id :tag tag) + (let [tp (tp/tpoint-ms)] + ;; Send the update to the back-end + (->> (request-thumbnail state file-id page-id frame-id tag) + (rx/mapcat (fn [blob] + (let [uri (wapi/create-uri blob) + params {:file-id file-id + :object-id object-id + :media blob + :tag (or tag "frame")}] + + (rx/merge + (rx/of (assoc-thumbnail object-id uri)) + (->> (rp/cmd! :create-file-object-thumbnail params) + (rx/catch rx/empty) + (rx/ignore)))))) + + (rx/catch (fn [cause] + (.error js/console cause) + (rx/empty))) + + (rx/tap #(l/trc :hint "thumbnail updated" :elapsed (dm/str (tp) "ms"))) + + ;; We cancel all the stream if user starts editing while + ;; thumbnail is generating + (rx/take-until + (->> stream + (rx/filter (ptk/type? ::clear-thumbnail)) + (rx/filter #(= (deref %) object-id)))))))))) (defn- extract-root-frame-changes "Process a changes set in a commit to extract the frames that are changing" [page-id [event [old-data new-data]]] - (let [changes (-> event deref :changes) + (let [changes (:changes event) extract-ids (fn [{:keys [page-id type] :as change}] @@ -192,8 +191,8 @@ :mov-objects (->> (:shapes change) (map #(vector page-id %))) [])) - get-frame-id - (fn [[_ id]] + get-frame-ids + (fn get-frame-ids [id] (let [old-objects (wsh/lookup-data-objects old-data page-id) new-objects (wsh/lookup-data-objects new-data page-id) @@ -208,12 +207,21 @@ (conj old-frame-id) (cfh/root-frame? new-objects new-frame-id) - (conj new-frame-id))))] + (conj new-frame-id) + + (and (uuid? (:frame-id old-shape)) + (not= uuid/zero (:frame-id old-shape))) + (into (get-frame-ids (:frame-id old-shape))) + + (and (uuid? (:frame-id new-shape)) + (not= uuid/zero (:frame-id new-shape))) + (into (get-frame-ids (:frame-id new-shape))))))] (into #{} (comp (mapcat extract-ids) (filter (fn [[page-id']] (= page-id page-id'))) - (mapcat get-frame-id)) + (map (fn [[_ id]] id)) + (mapcat get-frame-ids)) changes))) (defn watch-state-changes @@ -239,60 +247,36 @@ (rx/buffer 2 1) (rx/share)) - local-changes-s - (->> stream - (rx/filter dch/commit-changes?) - (rx/with-latest-from workspace-data-s) - (rx/merge-map (partial extract-root-frame-changes page-id)) - (rx/tap #(l/trc :hint "incoming change" :origin "local" :frame-id (dm/str %)))) - - notification-changes-s + ;; All commits stream, indepentendly of the source of the commit + all-commits-s (->> stream - (rx/filter (ptk/type? ::wnt/handle-file-change)) + (rx/filter dch/commit?) + (rx/map deref) (rx/observe-on :async) (rx/with-latest-from workspace-data-s) (rx/merge-map (partial extract-root-frame-changes page-id)) - (rx/tap #(l/trc :hint "incoming change" :origin "notifications" :frame-id (dm/str %)))) - - persistence-changes-s - (->> stream - (rx/filter (ptk/type? ::update)) - (rx/map deref) - (rx/filter (fn [[file-id page-id]] - (and (= file-id file-id) - (= page-id page-id)))) - (rx/map (fn [[_ _ frame-id]] frame-id)) - (rx/tap #(l/trc :hint "incoming change" :origin "persistence" :frame-id (dm/str %)))) - - all-changes-s - (->> (rx/merge - ;; LOCAL CHANGES - local-changes-s - ;; NOTIFICATIONS CHANGES - notification-changes-s - ;; PERSISTENCE CHANGES - persistence-changes-s) - + (rx/tap #(l/trc :hint "inconming change" :origin "all" :frame-id (dm/str %))) (rx/share)) - ;; BUFFER NOTIFIER (window of 5s of inactivity) notifier-s - (->> all-changes-s - (rx/debounce 1000) + (->> stream + (rx/filter (ptk/type? ::dps/commit-persisted)) + (rx/debounce 5000) (rx/tap #(l/trc :hint "buffer initialized")))] (->> (rx/merge ;; Perform instant thumbnail cleaning of affected frames ;; and interrupt any ongoing update-thumbnail process ;; related to current frame-id - (->> all-changes-s - (rx/map #(clear-thumbnail file-id page-id % "frame"))) + (->> all-commits-s + (rx/map (fn [frame-id] + (clear-thumbnail file-id page-id frame-id "frame")))) - ;; Generate thumbnails in batchs, once user becomes - ;; inactive for some instant - (->> all-changes-s + ;; Generate thumbnails in batches, once user becomes + ;; inactive for some instant. + (->> all-commits-s (rx/buffer-until notifier-s) (rx/mapcat #(into #{} %)) - (rx/map #(request-thumbnail file-id page-id % "frame" "watch-state-changes")))) + (rx/map #(update-thumbnail file-id page-id % "frame" "watch-state-changes")))) (rx/take-until stopper-s)))))) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index fc8ee350d68ef5e8a624770881cc7acbd2686da9..c4e2a80642973cb9481726f478785afb5e9923e4 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -18,17 +18,17 @@ [app.common.geom.shapes :as gsh] [app.common.geom.shapes.flex-layout :as gslf] [app.common.geom.shapes.grid-layout :as gslg] + [app.common.logic.shapes :as cls] [app.common.math :as mth] [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] [app.common.types.shape-tree :as ctst] [app.common.types.shape.layout :as ctl] - [app.main.data.workspace.changes :as dch] + [app.main.data.changes :as dch] [app.main.data.workspace.collapse :as dwc] [app.main.data.workspace.modifiers :as dwm] [app.main.data.workspace.selection :as dws] - [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] [app.main.snap :as snap] @@ -257,9 +257,7 @@ (watch [_ state stream] (let [initial-position @ms/mouse-position - stopper (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) + stopper (mse/drag-stopper stream) layout (:workspace-layout state) page-id (:current-page-id state) focus (:workspace-focus-selected state) @@ -293,10 +291,10 @@ ptk/WatchEvent (watch [_ _ stream] (rx/concat - (rx/of (dwsh/update-shape-flags ids {:transforming true})) + (rx/of #(assoc-in % [:workspace-local :transform] :move)) (->> (rx/timer 1000) (rx/map (fn [] - (dwsh/update-shape-flags ids {:transforming false}))) + #(assoc-in % [:workspace-local :transform] nil))) (rx/take-until (rx/filter (ptk/type? ::trigger-bounding-box-cloaking) stream))))))) @@ -370,10 +368,7 @@ ptk/WatchEvent (watch [_ _ stream] - (let [stopper (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) - + (let [stopper (mse/drag-stopper stream) group (gsh/shapes->rect shapes) group-center (grc/rect->center group) initial-angle (gpt/angle @ms/mouse-position group-center) @@ -405,17 +400,18 @@ (defn increase-rotation "Rotate shapes a fixed angle, from a keyboard action." - [ids rotation] - (ptk/reify ::increase-rotation - ptk/WatchEvent - (watch [_ state _] - - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - shapes (->> ids (map #(get objects %)))] - (rx/concat - (rx/of (dwm/set-delta-rotation-modifiers rotation shapes)) - (rx/of (dwm/apply-modifiers))))))) + ([ids rotation] + (increase-rotation ids rotation nil)) + ([ids rotation params] + (ptk/reify ::increase-rotation + ptk/WatchEvent + (watch [_ state _] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + shapes (->> ids (map #(get objects %)))] + (rx/concat + (rx/of (dwm/set-delta-rotation-modifiers rotation shapes params)) + (rx/of (dwm/apply-modifiers)))))))) ;; -- Move ---------------------------------------------------------- @@ -436,10 +432,7 @@ (watch [_ state stream] (let [initial (deref ms/mouse-position) - stopper (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) - + stopper (mse/drag-stopper stream {:interrupt? false}) zoom (get-in state [:workspace-local :zoom] 1) ;; We toggle the selection so we don't have to wait for the event @@ -513,15 +506,14 @@ ids (if (nil? ids) selected ids) shapes (into [] (comp (map (d/getf objects)) - (remove ctk/in-component-copy-not-head?)) + (remove #(let [parent (get objects (:parent-id %))] + (and (ctk/in-component-copy? parent) + (ctl/any-layout? parent))))) ids) duplicate-move-started? (get-in state [:workspace-local :duplicate-move-started?] false) - stopper (->> stream - (rx/filter mse/mouse-event?) - (rx/filter mse/mouse-up-event?)) - + stopper (mse/drag-stopper stream) layout (get state :workspace-layout) zoom (get-in state [:workspace-local :zoom] 1) focus (:workspace-focus-selected state) @@ -594,7 +586,6 @@ :else [move-vector nil])] - (-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector)) (dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data) (dwm/set-modifiers false false {:snap-ignore-axis snap-ignore-axis})))))) @@ -617,11 +608,11 @@ (->> move-stream (rx/last) (rx/mapcat - (fn [[_ target-frame drop-index]] + (fn [[_ target-frame drop-index drop-cell]] (let [undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) - (move-shapes-to-frame ids target-frame drop-index) (dwm/apply-modifiers {:undo-transation? false}) + (move-shapes-to-frame ids target-frame drop-index drop-cell) (finish-transform) (dwu/commit-undo-transaction undo-id)))))))))))))) @@ -842,102 +833,45 @@ :ignore-constraints false :ignore-snap-pixel true})))))) -(defn- move-shapes-to-frame - [ids frame-id drop-index] +(defn- cleanup-invalid-moving-shapes [ids objects frame-id] + (let [lookup (d/getf objects) + frame (get objects frame-id) + layout? (:layout frame) + + shapes (->> ids + set + (cfh/clean-loops objects) + (keep lookup) + ;;remove shapes inside copies, because we can't change the structure of copies + (remove #(ctk/in-component-copy? (get objects (:parent-id %)))) + ;; remove absolute shapes that won't change parent + (remove #(and (ctl/position-absolute? %) (= frame-id (:parent-id %))))) + + shapes + (cond->> shapes + (not layout?) + (remove #(= (:frame-id %) frame-id)) + + layout? + (remove #(and (= (:frame-id %) frame-id) + (not= (:parent-id %) frame-id))))] + (map :id shapes))) + +(defn move-shapes-to-frame + [ids frame-id drop-index cell] (ptk/reify ::move-shapes-to-frame ptk/WatchEvent (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) - lookup (d/getf objects) - frame (get objects frame-id) - layout? (:layout frame) - - shapes (->> ids (cfh/clean-loops objects) (keep lookup)) - - moving-shapes - (cond->> shapes - (not layout?) - (remove #(= (:frame-id %) frame-id)) - - layout? - (remove #(and (= (:frame-id %) frame-id) - (not= (:parent-id %) frame-id)))) - - ordered-indexes (cfh/order-by-indexed-shapes objects (map :id moving-shapes)) - moving-shapes (map (d/getf objects) ordered-indexes) - - all-parents - (reduce (fn [res id] - (into res (cfh/get-parent-ids objects id))) - (d/ordered-set) - ids) - - find-all-empty-parents - (fn recursive-find-empty-parents [empty-parents] - (let [all-ids (into empty-parents ids) - contains? (partial contains? all-ids) - xform (comp (map lookup) - (filter cfh/group-shape?) - (remove #(->> (:shapes %) (remove contains?) seq)) - (map :id)) - parents (into #{} xform all-parents)] - (if (= empty-parents parents) - empty-parents - (recursive-find-empty-parents parents)))) - - empty-parents - ;; Any empty parent whose children are moved to another frame should be deleted - (if (empty? moving-shapes) - #{} - (into (d/ordered-set) (find-all-empty-parents #{}))) - - ;; Not move absolute shapes that won't change parent - moving-shapes - (->> moving-shapes - (remove (fn [shape] - (and (ctl/position-absolute? shape) - (= frame-id (:parent-id shape)))))) - - frame-component - (ctn/get-component-shape objects frame) - - shape-ids-to-detach - (reduce (fn [result shape] - (if (and (some? shape) (ctk/in-component-copy-not-head? shape)) - (let [shape-component (ctn/get-component-shape objects shape)] - (if (= (:id frame-component) (:id shape-component)) - result - (into result (cfh/get-children-ids-with-self objects (:id shape))))) - result)) - #{} - moving-shapes) - - moving-shapes-ids - (map :id moving-shapes) - - changes - (-> (pcb/empty-changes it page-id) - (pcb/with-objects objects) - ;; Remove layout-item properties when moving a shape outside a layout - (cond-> (not (ctl/any-layout? objects frame-id)) - (pcb/update-shapes moving-shapes-ids ctl/remove-layout-item-data)) - ;; Remove component-root property when moving a shape inside a component - (cond-> (ctn/get-instance-root objects frame) - (pcb/update-shapes moving-shapes-ids #(dissoc % :component-root))) - ;; Add component-root property when moving a component outside a component - (cond-> (not (ctn/get-instance-root objects frame)) - (pcb/update-shapes moving-shapes-ids (fn [shape] - (if (ctk/instance-head? shape) - (assoc shape :component-root true) - shape)))) - (pcb/update-shapes moving-shapes-ids #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true))) - (pcb/update-shapes shape-ids-to-detach ctk/detach-shape) - (pcb/change-parent frame-id moving-shapes drop-index) - (cond-> (ctl/grid-layout? objects frame-id) - (-> (pcb/update-shapes [frame-id] ctl/assign-cell-positions {:with-objects? true}) - (pcb/reorder-grid-children [frame-id]))) - (pcb/remove-objects empty-parents))] + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + ids (cleanup-invalid-moving-shapes ids objects frame-id) + changes (cls/generate-relocate (pcb/empty-changes it) + objects + frame-id + page-id + drop-index + ids + :cell cell)] (when (and (some? frame-id) (d/not-empty? changes)) (rx/of (dch/commit-changes changes) @@ -956,26 +890,32 @@ ;; -- Flip ---------------------------------------------------------- -(defn flip-horizontal-selected [] - (ptk/reify ::flip-horizontal-selected - ptk/WatchEvent - (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) - selected (wsh/lookup-selected state {:omit-blocked? true}) - shapes (map #(get objects %) selected) - selrect (gsh/shapes->rect shapes) - center (grc/rect->center selrect) - modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point -1.0 1.0) center))] - (rx/of (dwm/apply-modifiers {:modifiers modifiers})))))) - -(defn flip-vertical-selected [] - (ptk/reify ::flip-vertical-selected - ptk/WatchEvent - (watch [_ state _] - (let [objects (wsh/lookup-page-objects state) - selected (wsh/lookup-selected state {:omit-blocked? true}) - shapes (map #(get objects %) selected) - selrect (gsh/shapes->rect shapes) - center (grc/rect->center selrect) - modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point 1.0 -1.0) center))] - (rx/of (dwm/apply-modifiers {:modifiers modifiers})))))) +(defn flip-horizontal-selected + ([] + (flip-horizontal-selected nil)) + ([ids] + (ptk/reify ::flip-horizontal-selected + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + selected (or ids (wsh/lookup-selected state {:omit-blocked? true})) + shapes (map #(get objects %) selected) + selrect (gsh/shapes->rect shapes) + center (grc/rect->center selrect) + modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point -1.0 1.0) center))] + (rx/of (dwm/apply-modifiers {:modifiers modifiers :ignore-snap-pixel true}))))))) + +(defn flip-vertical-selected + ([] + (flip-vertical-selected nil)) + ([ids] + (ptk/reify ::flip-vertical-selected + ptk/WatchEvent + (watch [_ state _] + (let [objects (wsh/lookup-page-objects state) + selected (or ids (wsh/lookup-selected state {:omit-blocked? true})) + shapes (map #(get objects %) selected) + selrect (gsh/shapes->rect shapes) + center (grc/rect->center selrect) + modifiers (dwm/create-modif-tree selected (ctm/resize-modifiers (gpt/point 1.0 -1.0) center))] + (rx/of (dwm/apply-modifiers {:modifiers modifiers :ignore-snap-pixel true}))))))) diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs index 809c9f6a52cb88c1647cae5371e5f21ceb5e5eff..41f3fe1a1d75eab597457678cd9adc751ef98a23 100644 --- a/frontend/src/app/main/data/workspace/undo.cljs +++ b/frontend/src/app/main/data/workspace/undo.cljs @@ -11,18 +11,18 @@ [app.common.files.changes :as cpc] [app.common.logging :as log] [app.common.schema :as sm] + [app.common.types.shape.layout :as ctl] + [app.main.data.changes :as dch] + [app.main.data.workspace.state-helpers :as wsh] + [app.util.router :as rt] [app.util.time :as dt] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) -(def discard-transaction-time-millis (* 20 1000)) - ;; Change this to :info :debug or :trace to debug this module (log/set-level! :warn) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Undo / Redo -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(def discard-transaction-time-millis (* 20 1000)) (def ^:private schema:undo-entry @@ -44,7 +44,6 @@ (subvec undo (- cnt MAX-UNDO-SIZE)) undo))) -;; TODO: Review the necessity of this method (defn materialize-undo [_changes index] (ptk/reify ::materialize-undo @@ -84,8 +83,7 @@ (-> state (update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %)) (update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes)) - (cond-> - (nil? (get-in state [:workspace-undo :transaction :undo-group])) + (cond-> (nil? (get-in state [:workspace-undo :transaction :undo-group])) (assoc-in [:workspace-undo :transaction :undo-group] undo-group)) (assoc-in [:workspace-undo :transaction :tags] tags))) @@ -182,3 +180,125 @@ (rx/tap #(js/console.warn (dm/str "FORCE COMMIT TRANSACTION AFTER " (second %) "MS"))) (rx/map first) (rx/map commit-undo-transaction)))))) + +(defn undo-to-index + "Repeat undoing or redoing until dest-index is reached." + [dest-index] + (ptk/reify ::undo-to-index + ptk/WatchEvent + (watch [it state _] + (let [objects (wsh/lookup-page-objects state) + edition (get-in state [:workspace-local :edition]) + drawing (get state :workspace-drawing)] + (when-not (and (or (some? edition) (some? (:object drawing))) + (not (ctl/grid-layout? objects edition))) + (let [undo (:workspace-undo state) + items (:items undo) + index (or (:index undo) (dec (count items)))] + (when (and (some? items) + (<= -1 dest-index (dec (count items)))) + (let [changes (vec (apply concat + (cond + (< dest-index index) + (->> (subvec items (inc dest-index) (inc index)) + (reverse) + (map :undo-changes)) + (> dest-index index) + (->> (subvec items (inc index) (inc dest-index)) + (map :redo-changes)) + :else [])))] + (when (seq changes) + (rx/of (materialize-undo changes dest-index) + (dch/commit-changes {:redo-changes changes + :undo-changes [] + :origin it + :save-undo? false}))))))))))) + +(declare ^:private assure-valid-current-page) + +(def undo + (ptk/reify ::undo + ptk/WatchEvent + (watch [it state _] + (let [objects (wsh/lookup-page-objects state) + edition (get-in state [:workspace-local :edition]) + drawing (get state :workspace-drawing)] + + ;; Editors handle their own undo's + (when (or (and (nil? edition) (nil? (:object drawing))) + (ctl/grid-layout? objects edition)) + (let [undo (:workspace-undo state) + items (:items undo) + index (or (:index undo) (dec (count items)))] + (when-not (or (empty? items) (= index -1)) + (let [item (get items index) + changes (:undo-changes item) + undo-group (:undo-group item) + + find-first-group-idx + (fn [index] + (if (= (dm/get-in items [index :undo-group]) undo-group) + (recur (dec index)) + (inc index))) + + undo-group-index + (when undo-group + (find-first-group-idx index))] + + (if undo-group + (rx/of (undo-to-index (dec undo-group-index))) + (rx/of (materialize-undo changes (dec index)) + (dch/commit-changes {:redo-changes changes + :undo-changes [] + :save-undo? false + :origin it}) + (assure-valid-current-page))))))))))) + +(def redo + (ptk/reify ::redo + ptk/WatchEvent + (watch [it state _] + (let [objects (wsh/lookup-page-objects state) + edition (get-in state [:workspace-local :edition]) + drawing (get state :workspace-drawing)] + (when (and (or (nil? edition) (ctl/grid-layout? objects edition)) + (or (empty? drawing) (= :curve (:tool drawing)))) + (let [undo (:workspace-undo state) + items (:items undo) + index (or (:index undo) (dec (count items)))] + (when-not (or (empty? items) (= index (dec (count items)))) + (let [item (get items (inc index)) + changes (:redo-changes item) + undo-group (:undo-group item) + find-last-group-idx (fn flgidx [index] + (let [item (get items index)] + (if (= (:undo-group item) undo-group) + (flgidx (inc index)) + (dec index)))) + + redo-group-index (when undo-group + (find-last-group-idx (inc index)))] + (if undo-group + (rx/of (undo-to-index redo-group-index)) + (rx/of (materialize-undo changes (inc index)) + (dch/commit-changes {:redo-changes changes + :undo-changes [] + :origin it + :save-undo? false}))))))))))) + +(defn- assure-valid-current-page + [] + (ptk/reify ::assure-valid-current-page + ptk/WatchEvent + (watch [_ state _] + (let [current_page (:current-page-id state) + pages (get-in state [:workspace-data :pages]) + exists? (some #(= current_page %) pages) + + project-id (:current-project-id state) + file-id (:current-file-id state) + pparams {:file-id file-id :project-id project-id} + qparams {:page-id (first pages)}] + (if exists? + (rx/empty) + (rx/of (rt/nav :workspace pparams qparams))))))) diff --git a/frontend/src/app/main/data/workspace/zoom.cljs b/frontend/src/app/main/data/workspace/zoom.cljs index 379776ede7df4745be7d33ae853ba45f07140283..6499f93a2f089405737ba724cecc99d3dd516aaf 100644 --- a/frontend/src/app/main/data/workspace/zoom.cljs +++ b/frontend/src/app/main/data/workspace/zoom.cljs @@ -6,6 +6,8 @@ (ns app.main.data.workspace.zoom (:require + [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.geom.align :as gal] [app.common.geom.matrix :as gmt] @@ -54,14 +56,20 @@ #(impl-update-zoom % center (fn [z] (max (/ z 1.3) 0.01))))))))) (defn set-zoom - [center scale] - (ptk/reify ::set-zoom - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local - #(impl-update-zoom % center (fn [z] (-> (* z scale) - (max 0.01) - (min 200)))))))) + ([scale] + (set-zoom nil scale)) + ([center scale] + (ptk/reify ::set-zoom + ptk/UpdateEvent + (update [_ state] + (let [vp (dm/get-in state [:workspace-local :vbox]) + x (+ (:x vp) (/ (:width vp) 2)) + y (+ (:y vp) (/ (:height vp) 2)) + center (d/nilv center (gpt/point x y))] + (update state :workspace-local + #(impl-update-zoom % center (fn [z] (-> (* z scale) + (max 0.01) + (min 200)))))))))) (def reset-zoom (ptk/reify ::reset-zoom @@ -110,6 +118,31 @@ (assoc :zoom-inverse (/ 1 zoom)) (update :vbox merge srect))))))))))) +(defn fit-to-shapes + [ids] + (ptk/reify ::fit-to-shapes + ptk/UpdateEvent + (update [_ state] + (if (empty? ids) + state + (let [page-id (:current-page-id state) + objects (wsh/lookup-page-objects state page-id) + srect (->> ids + (map #(get objects %)) + (gsh/shapes->rect))] + + (update state :workspace-local + (fn [{:keys [vport] :as local}] + (let [srect (gal/adjust-to-viewport + vport srect + {:padding 40}) + zoom (/ (:width vport) + (:width srect))] + (-> local + (assoc :zoom zoom) + (assoc :zoom-inverse (/ 1 zoom)) + (update :vbox merge srect)))))))))) + (defn start-zooming [pt] (ptk/reify ::start-zooming ptk/WatchEvent diff --git a/frontend/src/app/main/errors.cljs b/frontend/src/app/main/errors.cljs index 542b41bce7b6b95be6f6b54baab92a940256c9dd..59891188b89582972f7817daac96dbd0a807aa07 100644 --- a/frontend/src/app/main/errors.cljs +++ b/frontend/src/app/main/errors.cljs @@ -10,14 +10,13 @@ [app.common.exceptions :as ex] [app.common.pprint :as pp] [app.common.schema :as-alias sm] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] [app.main.data.users :as du] [app.main.store :as st] [app.util.globals :as glob] [app.util.i18n :refer [tr]] [app.util.router :as rt] - [app.util.storage :refer [storage]] [app.util.timers :as ts] [cuerdas.core :as str] [potok.v2.core :as ptk])) @@ -57,6 +56,14 @@ (print-explain! cause) (print-trace! cause)))) +(defn exception->error-data + [cause] + (let [data (ex-data cause)] + (-> data + (assoc :hint (or (:hint data) (ex-message cause))) + (assoc ::instance cause) + (assoc ::trace (.-stack cause))))) + (defn print-error! [cause] (cond @@ -67,22 +74,14 @@ (print-cause! (ex-message cause) (ex-data cause)) :else - (let [trace (.-stack cause)] - (print-cause! (ex-message cause) - {:hint (ex-message cause) - ::trace trace - ::instance cause})))) + (print-cause! (ex-message cause) (exception->error-data cause)))) (defn on-error "A general purpose error handler." [error] (if (map? error) (ptk/handle-error error) - (let [data (ex-data error) - data (-> data - (assoc :hint (or (:hint data) (ex-message error))) - (assoc ::instance error) - (assoc ::trace (.-stack error)))] + (let [data (exception->error-data error)] (ptk/handle-error data)))) ;; Set the main potok error handler @@ -96,16 +95,23 @@ (print-trace! error) (print-data! error)))) -;; We receive a explicit authentication error; this explicitly clears +;; We receive a explicit authentication error; +;; If the uri is for workspace, dashboard or view assign the +;; exception for the 'Oops' page. Otherwise this explicitly clears ;; all profile data and redirect the user to the login page. This is ;; here and not in app.main.errors because of circular dependency. (defmethod ptk/handle-error :authentication - [_] - (let [msg (tr "errors.auth.unable-to-login") - uri (. (. js/document -location) -href)] - (st/emit! (du/logout {:capture-redirect true})) - (ts/schedule 500 #(st/emit! (msg/warn msg))) - (ts/schedule 1000 #(swap! storage assoc :redirect-url uri)))) + [e] + (let [msg (tr "errors.auth.unable-to-login") + uri (.-href glob/location) + show-oops? (or (str/includes? uri "workspace") + (str/includes? uri "dashboard") + (str/includes? uri "view"))] + (if show-oops? + (st/async-emit! (rt/assign-exception e)) + (do + (st/emit! (du/logout {:capture-redirect true})) + (ts/schedule 500 #(st/emit! (ntf/warn msg))))))) ;; Error that happens on an active business model validation does not ;; passes an validation (example: profile can't leave a team). From @@ -123,9 +129,9 @@ (= code :invalid-paste-data) (let [message (tr "errors.paste-data-validation")] (st/async-emit! - (msg/show {:content message - :notification-type :toast - :type :error + (ntf/show {:content message + :type :toast + :level :error :timeout 3000}))) :else @@ -138,9 +144,9 @@ (defmethod ptk/handle-error :assertion [error] (ts/schedule - #(st/emit! (msg/show {:content "Internal Assertion Error" - :notification-type :toast - :type :error + #(st/emit! (ntf/show {:content "Internal Assertion Error" + :type :toast + :level :error :timeout 3000}))) (print-group! "Internal Assertion Error" @@ -154,9 +160,9 @@ [error] (ts/schedule #(st/emit! - (msg/show {:content "Something wrong has happened (on worker)." - :notification-type :toast - :type :error + (ntf/show {:content "Something wrong has happened (on worker)." + :type :toast + :level :error :timeout 3000}))) (print-group! "Internal Worker Error" @@ -168,18 +174,18 @@ (defmethod ptk/handle-error :svg-parser [_] (ts/schedule - #(st/emit! (msg/show {:content "SVG is invalid or malformed" - :notification-type :toast - :type :error + #(st/emit! (ntf/show {:content "SVG is invalid or malformed" + :type :toast + :level :error :timeout 3000})))) ;; TODO: should be handled in the event and not as general error handler (defmethod ptk/handle-error :comment-error [_] (ts/schedule - #(st/emit! (msg/show {:content "There was an error with the comment" - :notification-type :toast - :type :error + #(st/emit! (ntf/show {:content "There was an error with the comment" + :type :toast + :level :error :timeout 3000})))) ;; That are special case server-errors that should be treated @@ -279,6 +285,7 @@ (let [message (ex-message cause)] (or (= message "Possible side-effect in debug-evaluate") (= message "Unexpected end of input") + (str/starts-with? message "invalid props on component") (str/starts-with? message "Unexpected token ")))) (on-unhandled-error [event] diff --git a/frontend/src/app/main/features.cljs b/frontend/src/app/main/features.cljs index 51b30ed17795a87e48f65bf04ea775e0b618dc5e..e5a5f7c2b0241b6a2c7e47a14ece97d8f4c94848 100644 --- a/frontend/src/app/main/features.cljs +++ b/frontend/src/app/main/features.cljs @@ -35,7 +35,6 @@ (-> global-enabled-features (set/union (:features/runtime state #{})) (set/intersection cfeat/no-migration-features) - (set/union cfeat/default-enabled-features) (set/union (:features/team state #{})))) (def features-ref diff --git a/frontend/src/app/main/features/pointer_map.cljs b/frontend/src/app/main/features/pointer_map.cljs index 993427e5542e5ec4c7b536de07b3f6ca38de40bf..7055c1188fee212b3e7ce1dbffaab4854d82a087 100644 --- a/frontend/src/app/main/features/pointer_map.cljs +++ b/frontend/src/app/main/features/pointer_map.cljs @@ -16,7 +16,7 @@ (letfn [(resolve-pointer [[key val :as kv]] (if (t/pointer? val) (->> (rp/cmd! :get-file-fragment {:file-id id :fragment-id @val}) - (rx/map #(get % :content)) + (rx/map #(get % :data)) (rx/map #(vector key %))) (rx/of kv))) diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index a1720112455befe87be422c8409e1b99d86fc3cb..5b06f449b4c314a6a90f6b292851d47041d923d6 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -71,6 +71,13 @@ (defn get-font-data [id] (get @fontsdb id)) +(defn find-font-data [data] + (d/seek + (fn [font] + (= (select-keys font (keys data)) + data)) + (vals @fontsdb))) + (defn resolve-variants [id] (get-in @fontsdb [id :variants])) @@ -133,7 +140,10 @@ (defn- fetch-gfont-css [url] (->> (http/send! {:method :get :uri url :mode :cors :response-type :text}) - (rx/map :body))) + (rx/map :body) + (rx/catch (fn [err] + (.warn js/console "Cannot find the font" (obj/get err "message")) + (rx/empty))))) (defmethod load-font :google [{:keys [id ::on-loaded] :as font}] @@ -232,8 +242,8 @@ (defn ready [cb] - (-> (obj/get-in js/document ["fonts" "ready"]) - (p/then cb))) + (let [fonts (obj/get js/document "fonts")] + (p/then (obj/get fonts "ready") cb))) (defn get-default-variant [{:keys [variants]}] @@ -246,6 +256,11 @@ (or (d/seek #(= (:id %) font-variant-id) variants) (get-default-variant font))) +(defn find-variant + [{:keys [variants] :as font} variant-data] + (let [props (keys variant-data)] + (d/seek #(= (select-keys % props) variant-data) variants))) + ;; Font embedding functions (defn get-node-fonts "Extracts the fonts used by some node" diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 969bb43a65244266828ea0cd7d7ebd7d54a723bf..c0f32f6436e5cb652089a9abeb323423f3f15d0c 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -24,9 +24,6 @@ (def router (l/derived :router st/state)) -(def message - (l/derived :message st/state)) - (def profile (l/derived :profile st/state)) @@ -45,6 +42,9 @@ (def export (l/derived :export st/state)) +(def persistence + (l/derived :persistence st/state)) + ;; ---- Dashboard refs (def dashboard-local @@ -236,9 +236,10 @@ =)) (def workspace-recent-colors - (l/derived (fn [data] - (get data :recent-colors [])) - workspace-data)) + (l/derived (fn [state] + (when-let [file-id (:current-file-id state)] + (dm/get-in state [:recent-colors file-id]))) + st/state)) (def workspace-recent-fonts (l/derived (fn [data] diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 60fc6f6be36ef59f866f1cc29005152ba3bdcfd8..a371a67d3def69669d33d25ce28b3132c163a311 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -23,6 +23,7 @@ [app.common.geom.shapes.bounds :as gsb] [app.common.logging :as l] [app.common.math :as mth] + [app.common.types.components-list :as ctkl] [app.common.types.file :as ctf] [app.common.types.modifiers :as ctm] [app.common.types.shape-tree :as ctst] @@ -149,7 +150,7 @@ svg-raw-wrapper (mf/use-memo (mf/deps objects) #(svg-raw-wrapper-factory objects)) bool-wrapper (mf/use-memo (mf/deps objects) #(bool-wrapper-factory objects)) frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))] - (when (and shape (not (:hidden shape))) + (when shape (let [opts #js {:shape shape} svg-raw? (= :svg-raw (:type shape))] (if-not svg-raw? @@ -484,15 +485,18 @@ path (:path component) root-id (or (:main-instance-id component) (:id component)) + orig-root (get (:objects component) root-id) objects (adapt-objects-for-shape (:objects component) root-id) root-shape (get objects root-id) selrect (:selrect root-shape) - main-instance-id (:main-instance-id component) - main-instance-page (:main-instance-page component) - main-instance-x (:main-instance-x component) - main-instance-y (:main-instance-y component) + main-instance-id (:main-instance-id component) + main-instance-page (:main-instance-page component) + main-instance-x (when (:deleted component) (:x orig-root)) + main-instance-y (when (:deleted component) (:y orig-root)) + main-instance-parent (when (:deleted component) (:parent-id orig-root)) + main-instance-frame (when (:deleted component) (:frame-id orig-root)) vbox (format-viewbox @@ -516,7 +520,9 @@ "penpot:main-instance-id" main-instance-id "penpot:main-instance-page" main-instance-page "penpot:main-instance-x" main-instance-x - "penpot:main-instance-y" main-instance-y} + "penpot:main-instance-y" main-instance-y + "penpot:main-instance-parent" main-instance-parent + "penpot:main-instance-frame" main-instance-frame} [:title name] [:> shape-container {:shape root-shape} (case (:type root-shape) @@ -525,8 +531,10 @@ (mf/defc components-svg {::mf/wrap-props false} - [{:keys [data children embed include-metadata source]}] - (let [source (keyword (d/nilv source "components"))] + [{:keys [data children embed include-metadata deleted?]}] + (let [components (if (not deleted?) + (ctkl/components-seq data) + (ctkl/deleted-components-seq data))] [:& (mf/provider embed/context) {:value embed} [:& (mf/provider export/include-metadata-ctx) {:value include-metadata} [:svg {:version "1.1" @@ -536,9 +544,9 @@ :style {:display (when-not (some? children) "none")} :fill "none"} [:defs - (for [[id component] (source data)] + (for [component components] (let [component (ctf/load-component-objects data component)] - [:& component-symbol {:key (dm/str id) :component component}]))] + [:& component-symbol {:key (dm/str (:id component)) :component component}]))] children]]])) @@ -595,10 +603,12 @@ (rds/renderToStaticMarkup elem))))))) (defn render-components - [data source] + [data deleted?] (let [;; Join all components objects into a single map - objects (->> (source data) - (vals) + components (if (not deleted?) + (ctkl/components-seq data) + (ctkl/deleted-components-seq data)) + objects (->> components (map (partial ctf/load-component-objects data)) (map :objects) (reduce conj))] @@ -615,7 +625,7 @@ #js {:data data :embed true :include-metadata true - :source (name source)})] + :deleted? deleted?})] (rds/renderToStaticMarkup elem)))))))) (defn render-frame @@ -625,7 +635,7 @@ (if (some? shape) (let [fonts (ff/shape->fonts shape objects) - bounds (gsb/get-object-bounds objects shape) + bounds (gsb/get-object-bounds objects shape {:ignore-margin? false}) background (when (str/ends-with? object-id "component") (or (:background options) (dom/get-css-variable "--assets-component-background-color") "#fff")) diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index ed71b827a5c98bcbd9aaeb6151b9310769063d93..77d4de0120184a1ff553f7d7d87676c1db35fb88 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -10,13 +10,14 @@ [app.common.transit :as t] [app.common.uri :as u] [app.config :as cf] + [app.main.data.events :as-alias ev] [app.util.http :as http] [app.util.sse :as sse] [beicon.v2.core :as rx] [cuerdas.core :as str])) (defn handle-response - [{:keys [status body] :as response}] + [{:keys [status body headers] :as response}] (cond (= 204 status) ;; We need to send "something" so the streams listening downstream can act @@ -39,6 +40,13 @@ {:type :validation :code :request-body-too-large})) + (and (= status 403) + (or (= "cloudflare" (get headers "server")) + (= "challenge" (get headers "cf-mitigated")))) + (rx/throw (ex-info "http error" + {:type :authorization + :code :challenge-required})) + (and (>= status 400) (map? body)) (rx/throw (ex-info "http error" body)) @@ -47,6 +55,7 @@ (ex-info "http error" {:type :unexpected-error :status status + :headers headers :data body})))) (def default-options @@ -93,11 +102,12 @@ (= query-params :all) :get (str/starts-with? nid "get-") :get :else :post) - request {:method method :uri (u/join cf/public-uri "api/rpc/command/" nid) :credentials "include" - :headers {"accept" "application/transit+json,text/event-stream,*/*"} + :headers {"accept" "application/transit+json,text/event-stream,*/*" + "x-external-session-id" (cf/external-session-id) + "x-event-origin" (::ev/origin (meta params))} :body (when (= method :post) (if form-data? (http/form-data params) @@ -136,6 +146,8 @@ (->> (http/send! {:method :post :uri uri :credentials "include" + :headers {"x-external-session-id" (cf/external-session-id) + "x-event-origin" (::ev/origin (meta params))} :query params}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response)))) @@ -145,6 +157,8 @@ (->> (http/send! {:method :post :uri (u/join cf/public-uri "api/export") :body (http/transit-data (dissoc params :blob?)) + :headers {"x-external-session-id" (cf/external-session-id) + "x-event-origin" (::ev/origin (meta params))} :credentials "include" :response-type (if blob? :blob :text)}) (rx/map http/conditional-decode-transit) @@ -164,6 +178,8 @@ (->> (http/send! {:method :post :uri (u/join cf/public-uri "api/rpc/command/" (name id)) :credentials "include" + :headers {"x-external-session-id" (cf/external-session-id) + "x-event-origin" (::ev/origin (meta params))} :body (http/form-data params)}) (rx/map http/conditional-decode-transit) (rx/mapcat handle-response))) diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs index 5e8f14431391397acc03c8451fb49bdbbc08f3dd..d5ec5a6cfd3db5af1131f1f90d7575da55b9cd33 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -40,14 +40,14 @@ (fn [{:keys [type id frame-id]}] (cond (= type :layout) - (or (not (contains? layout :display-grid)) - (not (contains? layout :snap-grid)) + (or (not (contains? layout :display-guides)) + (not (contains? layout :snap-guides)) (and (d/not-empty? focus) (not (contains? focus id)))) (= type :guide) (or (not (contains? layout :rulers)) - (not (contains? layout :snap-guides)) + (not (contains? layout :snap-ruler-guides)) (and (d/not-empty? focus) (not (contains? focus frame-id)))) diff --git a/frontend/src/app/main/store.cljs b/frontend/src/app/main/store.cljs index 7b02335e76ee02da3dd136bd7e31839d53495951..703b3952de81e6190bf342ca83836455f4625585 100644 --- a/frontend/src/app/main/store.cljs +++ b/frontend/src/app/main/store.cljs @@ -34,8 +34,6 @@ (def debug-exclude-events #{:app.main.data.workspace.notifications/handle-pointer-update :app.main.data.workspace.notifications/handle-pointer-send - :app.main.data.workspace.persistence/update-persistence-status - :app.main.data.workspace.changes/update-indices :app.main.data.websocket/send-message :app.main.data.workspace.selection/change-hover-state}) @@ -65,7 +63,7 @@ :app.util.router/assign-exception}] (->> (rx/merge (->> stream - (rx/filter (ptk/type? :app.main.data.workspace.changes/commit-changes)) + (rx/filter (ptk/type? :app.main.data.changes/commit)) (rx/map #(-> % deref :hint-origin))) (rx/map ptk/type stream)) (rx/filter #(not (contains? omitset %))) diff --git a/frontend/src/app/main/style.clj b/frontend/src/app/main/style.clj index a1870314afad8736f96fc9d461e0a825ac3aecd7..ea9a0242ada9f3017bbf86b0a2bf195de2fc7f91 100644 --- a/frontend/src/app/main/style.clj +++ b/frontend/src/app/main/style.clj @@ -97,7 +97,7 @@ (when cls (cond (true? v) cls - (false? v) nil + (false? v) "" :else `(if ~v ~cls "")))))) (interpose " "))) diff --git a/frontend/src/app/main/ui.cljs b/frontend/src/app/main/ui.cljs index 9c7e3110c3c004b503e75a959784997c89d58232..8a53010de10160f9f81cabb48b82bfc6ddf19264 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -8,19 +8,19 @@ (:require [app.config :as cf] [app.main.refs :as refs] - [app.main.store :as st] [app.main.ui.context :as ctx] - [app.main.ui.cursors :as c] - [app.main.ui.debug.components-preview :as cm] + [app.main.ui.debug.icons-preview :refer [icons-preview]] + [app.main.ui.error-boundary :refer [error-boundary*]] [app.main.ui.frame-preview :as frame-preview] [app.main.ui.icons :as i] - [app.main.ui.messages :as msgs] - [app.main.ui.onboarding :refer [onboarding-modal]] + [app.main.ui.notifications :as notifications] + [app.main.ui.onboarding.newsletter :refer [onboarding-newsletter]] + [app.main.ui.onboarding.questions :refer [questions-modal]] + [app.main.ui.onboarding.team-choice :refer [onboarding-team-modal]] [app.main.ui.releases :refer [release-notes-modal]] [app.main.ui.static :as static] [app.util.dom :as dom] [app.util.i18n :refer [tr]] - [app.util.router :as rt] [rumext.v2 :as mf])) (def auth-page @@ -41,17 +41,33 @@ (def workspace-page (mf/lazy-component app.main.ui.workspace/workspace)) -(mf/defc on-main-error - [{:keys [error] :as props}] - (mf/with-effect - (st/emit! (rt/assign-exception error))) - [:span "Internal application error"]) - (mf/defc main-page - {::mf/wrap [#(mf/catch % {:fallback on-main-error})] - ::mf/props :obj} + {::mf/props :obj} [{:keys [route profile]}] - (let [{:keys [data params]} route] + (let [{:keys [data params]} route + props (get profile :props) + show-question-modal? + (and (contains? cf/flags :onboarding) + (not (:onboarding-viewed props)) + (not (contains? props :onboarding-questions))) + + show-newsletter-modal? + (and (contains? cf/flags :onboarding) + (not (:onboarding-viewed props)) + (not (contains? props :newsletter-updates)) + (contains? props :onboarding-questions)) + + show-team-modal? + (and (contains? cf/flags :onboarding) + (not (:onboarding-viewed props)) + (not (contains? props :onboarding-team-id)) + (contains? props :newsletter-updates)) + + show-release-modal? + (and (contains? cf/flags :onboarding) + (:onboarding-viewed props) + (not= (:release-notes-viewed props) (:main cf/version)) + (not= "0.0" (:main cf/version)))] [:& (mf/provider ctx/current-route) {:value route} (case (:name data) (:auth-login @@ -74,11 +90,7 @@ :debug-icons-preview (when *assert* - [:div.debug-preview - [:h1 "Cursors"] - [:& c/debug-preview] - [:h1 "Icons"] - [:& i/debug-icons-preview]]) + [:& icons-preview]) (:dashboard-search :dashboard-projects @@ -95,28 +107,29 @@ #_[:& app.main.ui.onboarding/onboarding-templates-modal] #_[:& app.main.ui.onboarding/onboarding-modal] #_[:& app.main.ui.onboarding.team-choice/onboarding-team-modal] - (when-let [props (get profile :props)] - (cond - (and (not (:onboarding-viewed props)) - (contains? cf/flags :onboarding)) - [:& onboarding-modal {}] - - (and (contains? cf/flags :onboarding) - (:onboarding-viewed props) - (not= (:release-notes-viewed props) (:main cf/version)) - (not= "0.0" (:main cf/version))) - [:& release-notes-modal {:version (:main cf/version)}])) - [:& dashboard-page {:route route :profile profile}]] + (cond + show-question-modal? + [:& questions-modal] + + show-newsletter-modal? + [:& onboarding-newsletter] + show-team-modal? + [:& onboarding-team-modal {:go-to-team? true}] + + show-release-modal? + [:& release-notes-modal {:version (:main cf/version)}]) + + [:& dashboard-page {:route route :profile profile}]] :viewer (let [{:keys [query-params path-params]} route - {:keys [index share-id section page-id interactions-mode frame-id] + {:keys [index share-id section page-id interactions-mode frame-id share] :or {section :interactions interactions-mode :show-on-click}} query-params {:keys [file-id]} path-params] [:? {} (if (:token query-params) - [:> static/error-container {} + [:> static/error-container* {} [:div.image i/detach] [:div.main-message (tr "viewer.breaking-change.message")] [:div.desc-message (tr "viewer.breaking-change.description")]] @@ -132,7 +145,8 @@ :hide false :show true :show-on-click false) - :frame-id frame-id}])]) + :frame-id frame-id + :share share}])]) :workspace (let [project-id (some-> params :path :project-id uuid) @@ -140,18 +154,26 @@ page-id (some-> params :query :page-id uuid) layout (some-> params :query :layout keyword)] [:? {} + (when (cf/external-feature-flag "onboarding-03" "test") + (cond + show-question-modal? + [:& questions-modal] + + show-newsletter-modal? + [:& onboarding-newsletter] + + show-team-modal? + [:& onboarding-team-modal {:go-to-team? false}] + + show-release-modal? + [:& release-notes-modal {:version (:main cf/version)}])) + [:& workspace-page {:project-id project-id :file-id file-id :page-id page-id :layout-name layout :key file-id}]]) - - :debug-components-preview - [:div.debug-preview - [:h1 "Components preview"] - [:& cm/components-preview]] - :frame-preview [:& frame-preview/frame-preview] @@ -170,8 +192,8 @@ [:& (mf/provider ctx/current-route) {:value route} [:& (mf/provider ctx/current-profile) {:value profile} (if edata - [:& static/exception-page {:data edata}] - [:* - [:& msgs/notifications-hub] + [:> static/exception-page* {:data edata :route route}] + [:> error-boundary* {:fallback static/internal-error*} + [:& notifications/current-notification] (when route [:& main-page {:route route :profile profile}])])]])) diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index fe84732526fe0707fc08716298b162c65ce793fb..1b5fb62b4a55c176bd242b2a33c1dace0f33b0a1 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -7,69 +7,63 @@ (ns app.main.ui.auth (:require-macros [app.main.style :as stl]) (:require - [app.config :as cf] + [app.common.data.macros :as dm] + [app.main.data.users :as du] + [app.main.store :as st] [app.main.ui.auth.login :refer [login-page]] [app.main.ui.auth.recovery :refer [recovery-page]] [app.main.ui.auth.recovery-request :refer [recovery-request-page]] - [app.main.ui.auth.register :refer [register-page register-success-page register-validate-page]] + [app.main.ui.auth.register :refer [register-page register-success-page register-validate-page terms-register]] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) -(mf/defc terms-login - [] - (let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri) - show-terms? (some? cf/terms-of-service-uri) - show-privacy? (some? cf/privacy-policy-uri)] - - (when show-all? - [:div {:class (stl/css :terms-login)} - (when show-terms? - [:a {:href cf/terms-of-service-uri :target "_blank"} (tr "auth.terms-of-service")]) - - (when show-all? - [:span (tr "labels.and")]) - - (when show-privacy? - [:a {:href cf/privacy-policy-uri :target "_blank"} (tr "auth.privacy-policy")])]))) (mf/defc auth - [{:keys [route] :as props}] - (let [section (get-in route [:data :name]) + {::mf/props :obj} + [{:keys [route]}] + (let [section (dm/get-in route [:data :name]) + show-login-icon (and + (not= section :auth-register-validate) + (not= section :auth-register-success)) params (:query-params route) - show-illustration? (contains? cf/flags :login-illustration)] + error (:error params)] - (mf/use-effect - #(dom/set-html-title (tr "title.default"))) + (mf/with-effect [] + (dom/set-html-title (tr "title.default"))) - [:main {:class (stl/css-case :auth-section true - :no-illustration (not show-illustration?))} - (when show-illustration? - [:div {:class (stl/css :login-illustration)} - i/login-illustration]) + (mf/with-effect [error] + (when error + (st/emit! (du/show-redirect-error error)))) + + [:main {:class (stl/css :auth-section)} + (when show-login-icon + [:h1 {:class (stl/css :logo-container)} + [:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]) + [:div {:class (stl/css :login-illustration)} + i/login-illustration] [:section {:class (stl/css :auth-content)} - [:* - [:a {:href "#/" :class (stl/css :logo-btn)} i/logo] - (case section - :auth-register - [:& register-page {:params params}] - :auth-register-validate - [:& register-validate-page {:params params}] + (case section + :auth-register + [:& register-page {:params params}] + + :auth-register-validate + [:& register-validate-page {:params params}] - :auth-register-success - [:& register-success-page {:params params}] + :auth-register-success + [:& register-success-page {:params params}] - :auth-login - [:& login-page {:params params}] + :auth-login + [:& login-page {:params params}] - :auth-recovery-request - [:& recovery-request-page] + :auth-recovery-request + [:& recovery-request-page] - :auth-recovery - [:& recovery-page {:params params}])] + :auth-recovery + [:& recovery-page {:params params}]) - (when (contains? #{:auth-login :auth-register} section) - [:& terms-login])]])) + (when (= section :auth-register) + [:& terms-register])]])) diff --git a/frontend/src/app/main/ui/auth.scss b/frontend/src/app/main/ui/auth.scss index 2e5d45fc7011b06dae02e69b2623e9c9ac842c30..4b3caeefcff4f9acaec36f352c90b26de48fb550 100644 --- a/frontend/src/app/main/ui/auth.scss +++ b/frontend/src/app/main/ui/auth.scss @@ -7,6 +7,7 @@ @use "common/refactor/common-refactor.scss" as *; .auth-section { + position: relative; align-items: center; background: var(--panel-background-color); display: grid; @@ -17,17 +18,23 @@ width: 100%; overflow: auto; - &.no-illustration { - display: flex; - justify-content: center; - } - @media (max-width: 992px) { display: flex; justify-content: center; } } +.logo-container { + position: absolute; + top: $s-20; + left: $s-20; + display: flex; + justify-content: flex-start; + width: $s-120; + height: $s-96; + margin-block-end: $s-52; +} + .login-illustration { display: flex; justify-content: center; @@ -37,7 +44,7 @@ svg { width: 100%; - fill: $df-primary; + fill: var(--color-foreground-primary); height: auto; } @@ -48,44 +55,20 @@ .auth-content { grid-column: 4 / 6; - display: flex; - flex-direction: column; + display: grid; + grid-template-rows: 1fr auto; + gap: $s-24; height: fit-content; max-width: $s-412; - padding-bottom: $s-8; + padding-block-end: $s-8; position: relative; width: 100%; } .logo-btn { - display: flex; - justify-content: flex-start; - margin-bottom: $s-52; - width: $s-120; - svg { width: $s-120; height: $s-40; - fill: $df-primary; - stroke: $df-primary; - } -} - -.terms-login { - font-size: $fs-11; - position: absolute; - bottom: 0; - width: 100%; - display: flex; - gap: $s-4; - justify-content: center; - - a { - font-weight: $fw700; - color: $df-secondary; - } - span { - border-bottom: $s-1 solid transparent; - color: $df-secondary; + fill: var(--main-icon-foreground); } } diff --git a/frontend/src/app/main/ui/auth/common.scss b/frontend/src/app/main/ui/auth/common.scss index 23e2ba11ca4314414cb45c440db225018daa4459..872ce47ddb764668b443cb46e74056279507e12d 100644 --- a/frontend/src/app/main/ui/auth/common.scss +++ b/frontend/src/app/main/ui/auth/common.scss @@ -6,36 +6,45 @@ @use "common/refactor/common-refactor.scss" as *; -.auth-form { +.auth-form-wrapper { width: 100%; - padding-block-end: $s-16; - + padding-block-end: 0; + display: grid; + gap: $s-12; form { display: flex; flex-direction: column; gap: $s-12; - margin-block-end: $s-24; + margin-top: $s-12; } } -.separator { - border-color: $db-quaternary; - margin: $s-24 0; +.auth-title-wrapper { + width: 100%; + padding-block-end: 0; + display: grid; + gap: $s-8; } -.error-wrapper { - padding-block-end: $s-8; +.separator { + border-color: var(--modal-separator-backogrund-color); + margin: 0; } .auth-title { @include bigTitleTipography; - color: $df-primary; + color: var(--title-foreground-color-hover); } .auth-subtitle { - margin-top: $s-24; - font-size: $fs-14; - color: $df-secondary; + @include smallTitleTipography; + color: var(--title-foreground-color); +} + +.auth-tagline { + @include smallTitleTipography; + margin: 0; + color: var(--title-foreground-color); } .form-field { @@ -45,77 +54,103 @@ } .buttons-stack { - display: flex; - flex-direction: column; + display: grid; gap: $s-8; +} - button, - :global(.btn-primary) { - @extend .button-primary; - font-size: $fs-11; - height: $s-40; - text-transform: uppercase; - width: 100%; - } +.login-button, +.login-ldap-button { + @extend .button-primary; + @include uppercaseTitleTipography; + height: $s-40; + width: 100%; } -.link-entry { +.go-back { display: flex; flex-direction: column; gap: $s-12; - padding: $s-24 0; - border-top: $s-1 solid $db-quaternary; + padding: 0; + border-block-start: none; +} - span { - text-align: center; - font-size: $fs-14; - color: $df-secondary; - } - a { - @extend .button-secondary; - height: $s-40; - text-transform: uppercase; - font-size: $fs-11; - } - &.register a { - @extend .button-primary; +.go-back-link { + @extend .button-secondary; + @include uppercaseTitleTipography; + height: $s-40; +} + +.links { + display: grid; + gap: $s-24; +} + +.register, +.account, +.recovery-request, +.demo-account { + display: flex; + justify-content: center; + gap: $s-8; + padding: 0; +} + +.register-text, +.account-text, +.recovery-text, +.demo-account-text { + @include smallTitleTipography; + text-align: right; + color: var(--title-foreground-color); +} + +.register-link, +.account-link, +.recovery-link, +.forgot-pass-link, +.demo-account-link { + @include smallTitleTipography; + text-align: left; + background-color: transparent; + border: none; + display: inline; + color: var(--link-foreground-color); + + &:hover { + text-decoration: underline; } } .forgot-password { display: flex; justify-content: flex-end; - a { - font-size: $fs-14; - color: $df-secondary; - font-weight: $fw400; - } } .submit-btn, .register-btn, .recover-btn { @extend .button-primary; - font-size: $fs-11; + @include uppercaseTitleTipography; height: $s-40; - text-transform: uppercase; width: 100%; } .login-btn { - border-radius: $br-8; - font-size: $fs-14; + @include smallTitleTipography; display: flex; align-items: center; gap: $s-6; width: 100%; - + border-radius: $br-8; + background-color: var(--button-secondary-background-color-rest); + color: var(--button-foreground-color-focus); span { - padding-top: $s-2; + padding-block-start: $s-2; } &:hover { - color: var(--app-white); + color: var(--button-foreground-color-focus); + background-color: var(--button-secondary-background-color-hover); } } @@ -123,39 +158,3 @@ display: flex; gap: $s-8; } - -.btn-google-auth { - color: var(--google-login-foreground); - background-color: var(--google-login-background); - &:hover { - background: var(--google-login-background-hover); - } -} - -.btn-github-auth { - color: var(--github-login-foreground); - background: var(--github-login-background); - &:hover { - background: var(--github-login-background-hover); - } -} - -.btn-oidc-auth { - color: var(--oidc-login-foreground); - background: var(--oidc-login-background); - &:hover { - background: var(--oidc-login-background-hover); - } -} - -.btn-gitlab-auth { - color: var(--gitlab-login-foreground); - background: var(--gitlab-login-background); - &:hover { - background: var(--gitlab-login-background-hover); - } -} - -.banner { - margin: $s-16 0; -} diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index b4d02fb11e6f401ebb5b495bc6666befac9d1710..0d0b587a9573b938be329a92c9f785c0616e1b3b 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -7,11 +7,10 @@ (ns app.main.ui.auth.login (:require-macros [app.main.style :as stl]) (:require - [app.common.data :as d] [app.common.logging :as log] - [app.common.spec :as us] + [app.common.schema :as sm] [app.config :as cf] - [app.main.data.messages :as msg] + [app.main.data.notifications :as ntf] [app.main.data.users :as du] [app.main.repo :as rp] [app.main.store :as st] @@ -25,7 +24,6 @@ [app.util.keyboard :as k] [app.util.router :as rt] [beicon.v2.core :as rx] - [cljs.spec.alpha :as s] [rumext.v2 :as mf])) (def show-alt-login-buttons? @@ -35,6 +33,17 @@ :login-with-gitlab :login-with-oidc])) +(mf/defc demo-warning + {::mf/props :obj} + [] + [:& context-notification + {:level :warning + :content (tr "auth.demo-warning")}]) + +(defn create-demo-profile + [] + (st/emit! (du/create-demo-profile))) + (defn- login-with-oidc [event provider params] (dom/prevent-default event) @@ -44,69 +53,42 @@ (.replace js/location redirect-uri) (log/error :hint "unexpected response from OIDC method" :resp (pr-str rsp)))) - (fn [{:keys [type code] :as error}] - (cond - (and (= type :restriction) - (= code :provider-not-configured)) - (st/emit! (msg/error (tr "errors.auth-provider-not-configured"))) - - :else - (st/emit! (msg/error (tr "errors.generic")))))))) - -(defn- login-with-ldap - [event params] - (dom/prevent-default event) - (dom/stop-propagation event) - (let [{:keys [on-error]} (meta params)] - (->> (rp/cmd! :login-with-ldap params) - (rx/subs! (fn [profile] - (if-let [token (:invitation-token profile)] - (st/emit! (rt/nav :auth-verify-token {} {:token token})) - (st/emit! (du/login-from-token {:profile profile})))) - (fn [{:keys [type code] :as error}] + (fn [cause] + (let [{:keys [type code] :as error} (ex-data cause)] (cond (and (= type :restriction) - (= code :ldap-not-initialized)) - (st/emit! (msg/error (tr "errors.ldap-disabled"))) - - (fn? on-error) - (on-error error) + (= code :provider-not-configured)) + (st/emit! (ntf/error (tr "errors.auth-provider-not-configured"))) :else - (st/emit! (msg/error (tr "errors.generic"))))))))) + (st/emit! (ntf/error (tr "errors.generic"))))))))) -(s/def ::email ::us/email) -(s/def ::password ::us/not-empty-string) -(s/def ::invitation-token ::us/not-empty-string) - -(s/def ::login-form - (s/keys :req-un [::email ::password] - :opt-un [::invitation-token])) - -(defn handle-error-messages - [errors _data] - (d/update-when errors :email - (fn [{:keys [code] :as error}] - (cond-> error - (= code ::us/email) - (assoc :message (tr "errors.email-invalid")))))) +(def ^:private schema:login-form + [:map {:title "LoginForm"} + [:email [::sm/email {:error/code "errors.invalid-email"}]] + [:password [:string {:min 1}]] + [:invitation-token {:optional true} + [:string {:min 1}]]]) (mf/defc login-form - [{:keys [params on-success-callback origin] :as props}] - (let [initial (mf/use-memo (mf/deps params) (constantly params)) + [{:keys [params on-success-callback on-recovery-request origin] :as props}] + (let [initial (mf/with-memo [params] params) error (mf/use-state false) - form (fm/use-form :spec ::login-form - :validators [handle-error-messages] + form (fm/use-form :schema schema:login-form :initial initial) on-error - (fn [err] - (let [cause (ex-data err)] + (fn [cause] + (let [cause (ex-data cause)] (cond (and (= :restriction (:type cause)) (= :profile-blocked (:code cause))) (reset! error (tr "errors.profile-blocked")) + (and (= :restriction (:type cause)) + (= :ldap-not-initialized (:code cause))) + (st/emit! (ntf/error (tr "errors.ldap-disabled"))) + (and (= :restriction (:type cause)) (= :admin-only-profile (:code cause))) (reset! error (tr "errors.profile-blocked")) @@ -123,9 +105,10 @@ (reset! error (tr "errors.generic"))))) on-success-default - (fn [data] - (when-let [token (:invitation-token data)] - (st/emit! (rt/nav :auth-verify-token {} {:token token})))) + (mf/use-fn + (fn [data] + (when-let [token (:invitation-token data)] + (st/emit! (rt/nav :auth-verify-token {} {:token token}))))) on-success (fn [data] @@ -146,31 +129,38 @@ (mf/use-callback (mf/deps form) (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (reset! error nil) - (let [params (:clean-data @form)] - (login-with-ldap event (with-meta params - {:on-error on-error - :on-success on-success}))))) + (let [params (:clean-data @form) + params (with-meta params + {:on-error on-error + :on-success on-success})] + (st/emit! (du/login-with-ldap params))))) - on-recovery-request + default-recovery-req (mf/use-fn - #(st/emit! (rt/nav :auth-recovery-request)))] + #(st/emit! (rt/nav :auth-recovery-request))) + + on-recovery-request (or on-recovery-request + default-recovery-req)] [:* (when-let [message @error] - [:div {:class (stl/css :error-wrapper)} - [:& context-notification - {:type :warning - :content message - :data-test "login-banner" - :role "alert"}]]) - - [:& fm/form {:on-submit on-submit :form form} + [:& context-notification + {:level :error + :content message + :role "alert"}]) + + [:& fm/form {:on-submit on-submit + :class (stl/css :login-form) + :form form} [:div {:class (stl/css :fields-row)} [:& fm/input {:name :email :type "email" - :label (tr "auth.email") + :label (tr "auth.work-email") :class (stl/css :form-field)}]] [:div {:class (stl/css :fields-row)} @@ -185,7 +175,8 @@ (contains? cf/flags :login-with-password))) [:div {:class (stl/css :fields-row :forgot-password)} [:& lk/link {:action on-recovery-request - :data-test "forgot-password"} + :class (stl/css :forgot-pass-link) + :data-testid "forgot-password"} (tr "auth.forgot-password")]]) [:div {:class (stl/css :buttons-stack)} @@ -193,12 +184,13 @@ (contains? cf/flags :login-with-password)) [:> fm/submit-button* {:label (tr "auth.login-submit") - :data-test "login-submit" + :data-testid "login-submit" :class (stl/css :login-button)}]) (when (contains? cf/flags :login-with-ldap) [:> fm/submit-button* {:label (tr "auth.login-with-ldap-submit") + :class (stl/css :login-ldap-button) :on-click on-submit-ldap}])]]])) (mf/defc login-buttons @@ -247,14 +239,14 @@ (when (k/enter? event) (login-oidc event))))] (when (contains? cf/flags :login-with-oidc) - [:div {:class (stl/css :link-entry :link-oidc)} - [:a {:tab-index "0" - :on-key-down handle-key-down - :on-click login-oidc} - (tr "auth.login-with-oidc-submit")]]))) + [:button {:tab-index "0" + :class (stl/css :link-entry :link-oidc) + :on-key-down handle-key-down + :on-click login-oidc} + (tr "auth.login-with-oidc-submit")]))) (mf/defc login-methods - [{:keys [params on-success-callback origin] :as props}] + [{:keys [params on-success-callback on-recovery-request origin] :as props}] [:* (when show-alt-login-buttons? [:* @@ -268,38 +260,34 @@ (when (or (contains? cf/flags :login) (contains? cf/flags :login-with-password) (contains? cf/flags :login-with-ldap)) - [:& login-form {:params params :on-success-callback on-success-callback :origin origin}])]) + [:& login-form {:params params :on-success-callback on-success-callback :on-recovery-request on-recovery-request :origin origin}])]) (mf/defc login-page [{:keys [params] :as props}] (let [go-register (mf/use-fn - #(st/emit! (rt/nav :auth-register {} params))) - - on-create-demo-profile - (mf/use-fn - #(st/emit! (du/create-demo-profile)))] + #(st/emit! (rt/nav :auth-register {} params)))] - [:div {:class (stl/css :auth-form)} + [:div {:class (stl/css :auth-form-wrapper)} [:h1 {:class (stl/css :auth-title) - :data-test "login-title"} (tr "auth.login-title")] + :data-testid "login-title"} (tr "auth.login-account-title")] - [:hr {:class (stl/css :separator)}] + [:p {:class (stl/css :auth-tagline)} + (tr "auth.login-tagline")] + + (when (contains? cf/flags :demo-warning) + [:& demo-warning]) [:& login-methods {:params params}] + [:hr {:class (stl/css :separator)}] + [:div {:class (stl/css :links)} (when (contains? cf/flags :registration) - [:div {:class (stl/css :link-entry :register)} - [:span (tr "auth.register") " "] + [:div {:class (stl/css :register)} + [:span {:class (stl/css :register-text)} + (tr "auth.register") " "] [:& lk/link {:action go-register - :data-test "register-submit"} - (tr "auth.register-submit")]])] - - (when (contains? cf/flags :demo-users) - [:div {:class (stl/css :link-entry :demo-account)} - [:span (tr "auth.create-demo-profile") " "] - [:& lk/link {:action on-create-demo-profile - :data-test "demo-account-link"} - (tr "auth.create-demo-account")]])])) - + :class (stl/css :register-link) + :data-testid "register-submit"} + (tr "auth.register-submit")]])]])) diff --git a/frontend/src/app/main/ui/auth/recovery.cljs b/frontend/src/app/main/ui/auth/recovery.cljs index 1425d9d1e6b3bc9698d5d533550401ec8dfadeef..cc567d310c6ef42ecaf8d2a1d4f7ba3cdf96345c 100644 --- a/frontend/src/app/main/ui/auth/recovery.cljs +++ b/frontend/src/app/main/ui/auth/recovery.cljs @@ -7,43 +7,33 @@ (ns app.main.ui.auth.recovery (:require-macros [app.main.style :as stl]) (:require - [app.common.spec :as us] - [app.main.data.messages :as msg] + [app.common.schema :as sm] + [app.main.data.notifications :as ntf] [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] - [cljs.spec.alpha :as s] [rumext.v2 :as mf])) -(s/def ::password-1 ::us/not-empty-string) -(s/def ::password-2 ::us/not-empty-string) -(s/def ::token ::us/not-empty-string) - -(s/def ::recovery-form - (s/keys :req-un [::password-1 - ::password-2])) - -(defn- password-equality - [errors data] - (let [password-1 (:password-1 data) - password-2 (:password-2 data)] - (cond-> errors - (and password-1 password-2 - (not= password-1 password-2)) - (assoc :password-2 {:message "errors.password-invalid-confirmation"}) - - (and password-1 (> 8 (count password-1))) - (assoc :password-1 {:message "errors.password-too-short"})))) +(def ^:private schema:recovery-form + [:and + [:map {:title "RecoveryForm"} + [:token ::sm/text] + [:password-1 ::sm/password] + [:password-2 ::sm/password]] + [:fn {:error/code "errors.password-invalid-confirmation" + :error/field :password-2} + (fn [{:keys [password-1 password-2]}] + (= password-1 password-2))]]) (defn- on-error [_form _error] - (st/emit! (msg/error (tr "auth.notifications.invalid-token-error")))) + (st/emit! (ntf/error (tr "errors.invalid-recovery-token")))) (defn- on-success [_] - (st/emit! (msg/info (tr "auth.notifications.password-changed-successfully")) + (st/emit! (ntf/info (tr "auth.notifications.password-changed-successfully")) (rt/nav :auth-login))) (defn- on-submit @@ -56,12 +46,13 @@ (mf/defc recovery-form [{:keys [params] :as props}] - (let [form (fm/use-form :spec ::recovery-form - :validators [password-equality - (fm/validate-not-empty :password-1 (tr "auth.password-not-empty")) - (fm/validate-not-empty :password-2 (tr "auth.password-not-empty"))] + (let [form (fm/use-form :schema schema:recovery-form :initial params)] - [:& fm/form {:on-submit on-submit :form form} + + [:& fm/form {:on-submit on-submit + :class (stl/css :recovery-form) + :form form} + [:div {:class (stl/css :fields-row)} [:& fm/input {:type "password" :name :password-1 @@ -84,13 +75,14 @@ (mf/defc recovery-page [{:keys [params] :as props}] - [:div {:class (stl/css :auth-form)} + [:div {:class (stl/css :auth-form-wrapper)} [:h1 {:class (stl/css :auth-title)} "Forgot your password?"] [:div {:class (stl/css :auth-subtitle)} "Please enter your new password"] [:hr {:class (stl/css :separator)}] [:& recovery-form {:params params}] [:div {:class (stl/css :links)} - [:div {:class (stl/css :link-entry)} - [:a {:on-click #(st/emit! (rt/nav :auth-login))} + [:div {:class (stl/css :go-back)} + [:a {:on-click #(st/emit! (rt/nav :auth-login)) + :class (stl/css :go-back-link)} (tr "profile.recovery.go-to-login")]]]]) diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index d1d72ed2dc0b5542a428b2edad1c45ee60bed069..afb2406477fbc38a054f1f9fb2d18a43a74698c0 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -7,9 +7,8 @@ (ns app.main.ui.auth.recovery-request (:require-macros [app.main.style :as stl]) (:require - [app.common.data :as d] - [app.common.spec :as us] - [app.main.data.messages :as msg] + [app.common.schema :as sm] + [app.main.data.notifications :as ntf] [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.components.forms :as fm] @@ -17,30 +16,24 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] [beicon.v2.core :as rx] - [cljs.spec.alpha :as s] [rumext.v2 :as mf])) -(s/def ::email ::us/email) -(s/def ::recovery-request-form (s/keys :req-un [::email])) -(defn handle-error-messages - [errors _data] - (d/update-when errors :email - (fn [{:keys [code] :as error}] - (cond-> error - (= code :missing) - (assoc :message (tr "errors.email-invalid")))))) +(def ^:private schema:recovery-request-form + [:map {:title "RecoverRequestForm"} + [:email ::sm/email]]) (mf/defc recovery-form [{:keys [on-success-callback] :as props}] - (let [form (fm/use-form :spec ::recovery-request-form - :validators [handle-error-messages] + (let [form (fm/use-form :schema schema:recovery-request-form :initial {}) submitted (mf/use-state false) - default-success-finish #(st/emit! (msg/info (tr "auth.notifications.recovery-token-sent"))) + default-success-finish + (mf/use-fn + #(st/emit! (ntf/info (tr "auth.notifications.recovery-token-sent")))) on-success - (mf/use-callback + (mf/use-fn (fn [cdata _] (reset! submitted false) (if (nil? on-success-callback) @@ -48,24 +41,25 @@ (on-success-callback (:email cdata))))) on-error - (mf/use-callback + (mf/use-fn (fn [data cause] (reset! submitted false) (let [code (-> cause ex-data :code)] (case code :profile-not-verified - (rx/of (msg/error (tr "auth.notifications.profile-not-verified"))) + (rx/of (ntf/error (tr "auth.notifications.profile-not-verified"))) :profile-is-muted - (rx/of (msg/error (tr "errors.profile-is-muted"))) + (rx/of (ntf/error (tr "errors.profile-is-muted"))) - :email-has-permanent-bounces - (rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data)))) + (:email-has-permanent-bounces + :email-has-complaints) + (rx/of (ntf/error (tr "errors.email-has-permanent-bounces" (:email data)))) (rx/throw cause))))) on-submit - (mf/use-callback + (mf/use-fn (fn [] (reset! submitted true) (let [cdata (:clean-data @form) @@ -76,16 +70,17 @@ (st/emit! (du/request-profile-recovery params)))))] [:& fm/form {:on-submit on-submit + :class (stl/css :recovery-request-form) :form form} [:div {:class (stl/css :fields-row)} [:& fm/input {:name :email - :label (tr "auth.email") + :label (tr "auth.work-email") :type "text" :class (stl/css :form-field)}]] [:> fm/submit-button* {:label (tr "auth.recovery-request-submit") - :data-test "recovery-resquest-submit" + :data-testid "recovery-resquest-submit" :class (stl/css :recover-btn)}]])) @@ -95,14 +90,28 @@ [{:keys [params on-success-callback go-back-callback] :as props}] (let [default-go-back #(st/emit! (rt/nav :auth-login)) go-back (or go-back-callback default-go-back)] - [:div {:class (stl/css :auth-form)} + [:div {:class (stl/css :auth-form-wrapper)} [:h1 {:class (stl/css :auth-title)} (tr "auth.recovery-request-title")] [:div {:class (stl/css :auth-subtitle)} (tr "auth.recovery-request-subtitle")] [:hr {:class (stl/css :separator)}] [:& recovery-form {:params params :on-success-callback on-success-callback}] - - [:div {:class (stl/css :link-entry)} + [:hr {:class (stl/css :separator)}] + [:div {:class (stl/css :go-back)} [:& lk/link {:action go-back - :data-test "go-back-link"} + :class (stl/css :go-back-link) + :data-testid "go-back-link"} (tr "labels.go-back")]]])) + + +(mf/defc recovery-sent-page + {::mf/props :obj} + [{:keys [email]}] + [:div {:class (stl/css :auth-form-wrapper :register-success)} + [:div {:class (stl/css :auth-title-wrapper)} + [:h2 {:class (stl/css :auth-title)} + (tr "auth.check-mail")] + [:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery")]] + [:div {:class (stl/css :notification-text-email)} email] + [:div {:class (stl/css :notification-text)} (tr "not-found.login.sent-recovery-check")]]) + diff --git a/frontend/src/app/main/ui/auth/recovery_request.scss b/frontend/src/app/main/ui/auth/recovery_request.scss index e78e21b6de7ded5c39ad3858ddc8242181b48215..8b384e59ddda50fc64526e9dca403c3d4b067c96 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.scss +++ b/frontend/src/app/main/ui/auth/recovery_request.scss @@ -10,3 +10,10 @@ .fields-row { margin-bottom: $s-8; } + +.notification-text-email { + @include medTitleTipography; + font-size: $fs-20; + color: var(--register-confirmation-color); + margin-inline: $s-36; +} diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 6789b99dc96ffb15c1c0617f2f76ecafa8d6b486..8c3a8a6da11f025399d6324c16459a4c7dc50065 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -7,10 +7,10 @@ (ns app.main.ui.auth.register (:require-macros [app.main.style :as stl]) (:require - [app.common.data :as d] - [app.common.spec :as us] + [app.common.data.macros :as dm] + [app.common.schema :as sm] [app.config :as cf] - [app.main.data.messages :as msg] + [app.main.data.notifications :as ntf] [app.main.data.users :as du] [app.main.repo :as rp] [app.main.store :as st] @@ -18,106 +18,75 @@ [app.main.ui.components.forms :as fm] [app.main.ui.components.link :as lk] [app.main.ui.icons :as i] - [app.main.ui.notifications.context-notification :refer [context-notification]] - [app.util.i18n :refer [tr tr-html]] + [app.util.i18n :as i18n :refer [tr]] [app.util.router :as rt] + [app.util.storage :as sto] [beicon.v2.core :as rx] - [cljs.spec.alpha :as s] [rumext.v2 :as mf])) -(mf/defc demo-warning - [_] - [:div {:class (stl/css :banner)} - [:& context-notification - {:type :warning - :content (tr "auth.demo-warning")}]]) - ;; --- PAGE: Register -(defn- validate - [errors data] - (let [password (:password data)] - (cond-> errors - (> 8 (count password)) - (assoc :password {:message "errors.password-too-short"}) - :always - (d/update-when :email - (fn [{:keys [code] :as error}] - (cond-> error - (= code ::us/email) - (assoc :message (tr "errors.email-invalid")))))))) - -(s/def ::fullname ::us/not-empty-string) -(s/def ::password ::us/not-empty-string) -(s/def ::email ::us/email) -(s/def ::invitation-token ::us/not-empty-string) -(s/def ::terms-privacy ::us/boolean) - -(s/def ::register-form - (s/keys :req-un [::password ::email] - :opt-un [::invitation-token])) - -(defn- handle-prepare-register-error - [form cause] - (let [{:keys [type code]} (ex-data cause)] - (condp = [type code] - [:restriction :registration-disabled] - (st/emit! (msg/error (tr "errors.registration-disabled"))) - - [:restriction :profile-blocked] - (st/emit! (msg/error (tr "errors.profile-blocked"))) - - [:validation :email-has-permanent-bounces] - (let [email (get @form [:data :email])] - (st/emit! (msg/error (tr "errors.email-has-permanent-bounces" email)))) - - [:validation :email-already-exists] - (swap! form assoc-in [:errors :email] - {:message "errors.email-already-exists"}) - - [:validation :email-as-password] - (swap! form assoc-in [:errors :password] - {:message "errors.email-as-password"}) - - (st/emit! (msg/error (tr "errors.generic")))))) - -(defn- handle-prepare-register-success - [params] - (st/emit! (rt/nav :auth-register-validate {} params))) +(def ^:private schema:register-form + [:map {:title "RegisterForm"} + [:password ::sm/password] + [:email ::sm/email] + [:invitation-token {:optional true} ::sm/text]]) (mf/defc register-form - [{:keys [params on-success-callback] :as props}] + {::mf/props :obj} + [{:keys [params on-success-callback]}] (let [initial (mf/use-memo (mf/deps params) (constantly params)) - form (fm/use-form :spec ::register-form - :validators [validate - (fm/validate-not-empty :password (tr "auth.password-not-empty"))] + form (fm/use-form :schema schema:register-form :initial initial) - submitted? (mf/use-state false) - on-success (fn [p] - (if (nil? on-success-callback) - (handle-prepare-register-success p) - (on-success-callback p))) + submitted? + (mf/use-state false) + + on-error + (mf/use-fn + (fn [form cause] + (let [{:keys [type code] :as edata} (ex-data cause)] + (condp = [type code] + [:restriction :registration-disabled] + (st/emit! (ntf/error (tr "errors.registration-disabled"))) + + [:restriction :email-domain-is-not-allowed] + (st/emit! (ntf/error (tr "errors.email-domain-not-allowed"))) + + [:restriction :email-has-permanent-bounces] + (st/emit! (ntf/error (tr "errors.email-has-permanent-bounces" (:email edata)))) + + [:restriction :email-has-complaints] + (st/emit! (ntf/error (tr "errors.email-has-permanent-bounces" (:email edata)))) + + [:validation :email-as-password] + (swap! form assoc-in [:errors :password] + {:code "errors.email-as-password"}) + + (st/emit! (ntf/error (tr "errors.generic"))))))) on-submit (mf/use-fn + (mf/deps on-success-callback) (fn [form _event] (reset! submitted? true) - (let [cdata (:clean-data @form)] + (let [cdata (:clean-data @form) + on-success (fn [data] + (if (fn? on-success-callback) + (on-success-callback data) + (st/emit! (rt/nav :auth-register-validate {} data))))] + (->> (rp/cmd! :prepare-register-profile cdata) (rx/map #(merge % params)) (rx/finalize #(reset! submitted? false)) - (rx/subs! - on-success - (partial handle-prepare-register-error form))))))] - + (rx/subs! on-success (partial on-error form))))))] [:& fm/form {:on-submit on-submit :form form} [:div {:class (stl/css :fields-row)} [:& fm/input {:type "text" :name :email - :label (tr "auth.email") - :data-test "email-input" + :label (tr "auth.work-email") + :data-testid "email-input" :show-success? true :class (stl/css :form-field)}]] [:div {:class (stl/css :fields-row)} @@ -131,110 +100,132 @@ [:> fm/submit-button* {:label (tr "auth.register-submit") :disabled @submitted? - :data-test "register-form-submit" + :data-testid "register-form-submit" :class (stl/css :register-btn)}]])) - (mf/defc register-methods - [{:keys [params on-success-callback] :as props}] + {::mf/props :obj} + [{:keys [params hide-separator on-success-callback]}] [:* (when login/show-alt-login-buttons? - [:* - [:hr {:class (stl/css :separator)}] - [:& login/login-buttons {:params params}]]) - [:hr {:class (stl/css :separator)}] - [:& register-form {:params params :on-success-callback on-success-callback}]]) + [:& login/login-buttons {:params params}]) + (when (or login/show-alt-login-buttons? (false? hide-separator)) + [:hr {:class (stl/css :separator)}]) + (when (contains? cf/flags :login-with-password) + [:& register-form {:params params :on-success-callback on-success-callback}])]) (mf/defc register-page - [{:keys [params] :as props}] - [:div {:class (stl/css :auth-form)} + {::mf/props :obj} + [{:keys [params]}] + [:div {:class (stl/css :auth-form-wrapper :register-form)} [:h1 {:class (stl/css :auth-title) - :data-test "registration-title"} (tr "auth.register-title")] - [:div {:class (stl/css :auth-subtitle)} (tr "auth.register-subtitle")] + :data-testid "registration-title"} (tr "auth.register-title")] + [:p {:class (stl/css :auth-tagline)} + (tr "auth.register-tagline")] (when (contains? cf/flags :demo-warning) - [:& demo-warning]) + [:& login/demo-warning]) [:& register-methods {:params params}] [:div {:class (stl/css :links)} - [:div {:class (stl/css :link-entry :account)} - [:span (tr "auth.already-have-account") " "] - + [:div {:class (stl/css :account)} + [:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "] [:& lk/link {:action #(st/emit! (rt/nav :auth-login {} params)) - :data-test "login-here-link"} + :class (stl/css :account-link) + :data-testid "login-here-link"} (tr "auth.login-here")]] (when (contains? cf/flags :demo-users) - [:div {:class (stl/css :link-entry :demo-users)} - [:span (tr "auth.create-demo-profile") " "] - [:& lk/link {:action #(st/emit! (du/create-demo-profile))} - (tr "auth.create-demo-account")]])]]) + [:* + [:hr {:class (stl/css :separator)}] + [:div {:class (stl/css :demo-account)} + [:& lk/link {:action login/create-demo-profile + :class (stl/css :demo-account-link)} + (tr "auth.create-demo-account")]]])]]) ;; --- PAGE: register validation -(defn- handle-register-error - [form error] - (case (:code error) - :email-already-exists - (swap! form assoc-in [:errors :email] - {:message "errors.email-already-exists"}) - - (do - (println (:explain error)) - (st/emit! (msg/error (tr "errors.generic")))))) - -(defn- handle-register-success - [data] - (cond - (some? (:invitation-token data)) - (let [token (:invitation-token data)] - (st/emit! (rt/nav :auth-verify-token {} {:token token}))) - - ;; The :is-active flag is true, when insecure-register is enabled - ;; or the user used external auth provider. - (:is-active data) - (st/emit! (du/login-from-register)) - - :else - (st/emit! (rt/nav :auth-register-success {} {:email (:email data)})))) - -(s/def ::accept-terms-and-privacy (s/and ::us/boolean true?)) -(s/def ::accept-newsletter-subscription ::us/boolean) - -(if (contains? cf/flags :terms-and-privacy-checkbox) - (s/def ::register-validate-form - (s/keys :req-un [::token ::fullname ::accept-terms-and-privacy] - :opt-un [::accept-newsletter-subscription])) - (s/def ::register-validate-form - (s/keys :req-un [::token ::fullname] - :opt-un [::accept-terms-and-privacy - ::accept-newsletter-subscription]))) +(mf/defc terms-and-privacy + {::mf/props :obj + ::mf/private true} + [] + (let [terms-label + (mf/html + [:> i18n/tr-html* + {:tag-name "div" + :content (tr "auth.terms-and-privacy-agreement" + cf/terms-of-service-uri + cf/privacy-policy-uri)}])] + + [:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)} + [:& fm/input {:name :accept-terms-and-privacy + :class (stl/css :checkbox-terms-and-privacy) + :type "checkbox" + :default-checked false + :label terms-label}]])) + +(def ^:private schema:register-validate-form + [:map {:title "RegisterValidateForm"} + [:token ::sm/text] + [:fullname [::sm/text {:max 250}]] + [:accept-terms-and-privacy {:optional (not (contains? cf/flags :terms-and-privacy-checkbox))} + [:and :boolean [:= true]]]]) (mf/defc register-validate-form - [{:keys [params on-success-callback] :as props}] - (let [form (fm/use-form :spec ::register-validate-form - :validators [(fm/validate-not-empty :fullname (tr "auth.name.not-all-space")) - (fm/validate-length :fullname fm/max-length-allowed (tr "auth.name.too-long"))] - :initial params) - submitted? (mf/use-state false) - - on-success (fn [p] - (if (nil? on-success-callback) - (handle-register-success p) - (on-success-callback (:email p)))) + {::mf/props :obj + ::mf/private true} + [{:keys [params on-success-callback]}] + (let [form (fm/use-form :schema schema:register-validate-form :initial params) + + submitted? + (mf/use-state false) + + on-success + (mf/use-fn + (mf/deps on-success-callback) + (fn [params] + (if (fn? on-success-callback) + (on-success-callback (:email params)) + + (cond + (some? (:invitation-token params)) + (let [token (:invitation-token params)] + (st/emit! (rt/nav :auth-verify-token {} {:token token}))) + + (:is-active params) + (st/emit! (du/login-from-register)) + + :else + (do + (swap! sto/storage assoc ::email (:email params)) + (st/emit! (rt/nav :auth-register-success))))))) + + on-error + (mf/use-fn + (fn [_] + (st/emit! (ntf/error (tr "errors.generic"))))) on-submit (mf/use-fn - (fn [form _event] + (mf/deps on-success on-error) + (fn [form _] (reset! submitted? true) - (let [params (:clean-data @form)] + (let [create-welcome-file? + (cf/external-feature-flag "onboarding-03" "test") + + params + (cond-> (:clean-data @form) + create-welcome-file? (assoc :create-welcome-file true))] + (->> (rp/cmd! :register-profile params) (rx/finalize #(reset! submitted? false)) - (rx/subs! on-success - (partial handle-register-error form))))))] + (rx/subs! on-success on-error)))))] + + [:& fm/form {:on-submit on-submit + :form form + :class (stl/css :register-validate-form)} - [:& fm/form {:on-submit on-submit :form form} [:div {:class (stl/css :fields-row)} [:& fm/input {:name :fullname :label (tr "auth.fullname") @@ -243,18 +234,7 @@ :class (stl/css :form-field)}]] (when (contains? cf/flags :terms-and-privacy-checkbox) - (let [terms-label - (mf/html - [:& tr-html - {:tag-name "div" - :label "auth.terms-privacy-agreement-md" - :params [cf/terms-of-service-uri cf/privacy-policy-uri]}])] - [:div {:class (stl/css :fields-row :input-visible :accept-terms-and-privacy-wrapper)} - [:& fm/input {:name :accept-terms-and-privacy - :class "check-primary" - :type "checkbox" - :default-checked false - :label terms-label}]])) + [:& terms-and-privacy]) [:> fm/submit-button* {:label (tr "auth.register-submit") @@ -263,27 +243,57 @@ (mf/defc register-validate-page - [{:keys [params] :as props}] - [:div {:class (stl/css :auth-form)} - [:h1 {:class (stl/css :auth-title) - :data-test "register-title"} (tr "auth.register-title")] - [:div {:class (stl/css :auth-subtitle)} (tr "auth.register-subtitle")] - - [:hr {:class (stl/css :separator)}] + {::mf/props :obj} + [{:keys [params]}] + [:div {:class (stl/css :auth-form-wrapper)} + [:h1 {:class (stl/css :logo-container)} + [:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]] + [:div {:class (stl/css :auth-title-wrapper)} + [:h2 {:class (stl/css :auth-title) + :data-testid "register-title"} (tr "auth.register-account-title")] + [:div {:class (stl/css :auth-subtitle)} (tr "auth.register-account-tagline")]] [:& register-validate-form {:params params}] [:div {:class (stl/css :links)} - [:div {:class (stl/css :link-entry :go-back)} - [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} {}))} + [:div {:class (stl/css :go-back)} + [:& lk/link {:action #(st/emit! (rt/nav :auth-register {} {})) + :class (stl/css :go-back-link)} (tr "labels.go-back")]]]]) (mf/defc register-success-page - [{:keys [params] :as props}] - [:div {:class (stl/css :auth-form :register-success)} - [:div {:class (stl/css :notification-icon)} i/icon-verify] - [:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")] - [:div {:class (stl/css :notification-text-email)} (:email params "")] - [:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]]) - + {::mf/props :obj} + [{:keys [params]}] + (let [email (or (:email params) (::email @sto/storage))] + [:div {:class (stl/css :auth-form-wrapper :register-success)} + (when-not (:hide-logo params) + [:h1 {:class (stl/css :logo-container)} + [:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]]) + [:div {:class (stl/css :auth-title-wrapper)} + [:h2 {:class (stl/css :auth-title)} + (tr "auth.check-mail")] + [:div {:class (stl/css :notification-text)} (tr "auth.verification-email-sent")]] + [:div {:class (stl/css :notification-text-email)} email] + [:div {:class (stl/css :notification-text)} (tr "auth.check-your-email")]])) + + +(mf/defc terms-register + [] + (let [show-all? (and cf/terms-of-service-uri cf/privacy-policy-uri) + show-terms? (some? cf/terms-of-service-uri) + show-privacy? (some? cf/privacy-policy-uri)] + + (when show-all? + [:div {:class (stl/css :terms-register)} + (when show-terms? + [:a {:href cf/terms-of-service-uri :target "_blank" :class (stl/css :auth-link)} + (tr "auth.terms-of-service")]) + + (when show-all? + [:span {:class (stl/css :and-text)} + (dm/str " " (tr "labels.and") " ")]) + + (when show-privacy? + [:a {:href cf/privacy-policy-uri :target "_blank" :class (stl/css :auth-link)} + (tr "auth.privacy-policy")])]))) diff --git a/frontend/src/app/main/ui/auth/register.scss b/frontend/src/app/main/ui/auth/register.scss index 9cbc0045740651117f13270c84f230532dfcf8c1..0309cd44ac520174938dc769576205a514610a44 100644 --- a/frontend/src/app/main/ui/auth/register.scss +++ b/frontend/src/app/main/ui/auth/register.scss @@ -8,15 +8,24 @@ @use "./common.scss"; .accept-terms-and-privacy-wrapper { - margin: $s-16 0; :global(a) { - color: $df-secondary; + color: var(--color-foreground-secondary); font-weight: $fw700; } } +.checkbox-terms-and-privacy { + align-items: flex-start; +} +.register-form { + gap: $s-24; +} + .register-success { - padding-bottom: $s-32; + gap: $s-24; + .auth-title { + @include medTitleTipography; + } } .notification-icon { @@ -30,9 +39,50 @@ } } -.notification-text-email, .notification-text { - font-size: $fs-16; - color: var(--notification-foreground-color-default); - margin-bottom: $s-16; + @include bodyMediumTypography; + color: var(--title-foreground-color); +} + +.notification-text-email { + @include medTitleTipography; + font-size: $fs-20; + color: var(--register-confirmation-color); + margin-inline: $s-36; +} + +.logo-btn { + height: $s-40; + svg { + width: $s-120; + height: $s-40; + fill: var(--main-icon-foreground); + } +} + +.logo-container { + display: flex; + justify-content: flex-start; + width: $s-120; + margin-block-end: $s-24; +} + +.terms-register { + @include bodySmallTypography; + display: flex; + gap: $s-4; + justify-content: center; + width: 100%; +} + +.and-text { + border-bottom: $s-1 solid transparent; + color: var(--title-foreground-color); +} + +.auth-link { + color: var(--link-foreground-color); + &:hover { + text-decoration: underline; + } } diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index a914bd46a2d9cc6512429bbd33e956e1a1b95690..9e8bdbbd5ea11ccbbe117c7b55586f18b707e05c 100644 --- a/frontend/src/app/main/ui/auth/verify_token.cljs +++ b/frontend/src/app/main/ui/auth/verify_token.cljs @@ -5,13 +5,12 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.auth.verify-token - (:require-macros [app.main.style :as stl]) (:require - [app.main.data.messages :as msg] + [app.main.data.notifications :as ntf] [app.main.data.users :as du] [app.main.repo :as rp] [app.main.store :as st] - [app.main.ui.icons :as i] + [app.main.ui.ds.product.loader :refer [loader*]] [app.main.ui.static :as static] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -25,13 +24,13 @@ (defmethod handle-token :verify-email [data] (let [msg (tr "dashboard.notifications.email-verified-successfully")] - (ts/schedule 1000 #(st/emit! (msg/success msg))) + (ts/schedule 1000 #(st/emit! (ntf/success msg))) (st/emit! (du/login-from-token data)))) (defmethod handle-token :change-email [_data] (let [msg (tr "dashboard.notifications.email-changed-successfully")] - (ts/schedule 100 #(st/emit! (msg/success msg))) + (ts/schedule 100 #(st/emit! (ntf/success msg))) (st/emit! (rt/nav :settings-profile) (du/fetch-profile)))) @@ -44,7 +43,7 @@ (case (:state tdata) :created (st/emit! - (msg/success (tr "auth.notifications.team-invitation-accepted")) + (ntf/success (tr "auth.notifications.team-invitation-accepted")) (du/fetch-profile) (rt/nav :dashboard-projects {:team-id (:team-id tdata)})) @@ -57,7 +56,7 @@ [_tdata] (st/emit! (rt/nav :auth-login) - (msg/warn (tr "errors.unexpected-token")))) + (ntf/warn (tr "errors.unexpected-token")))) (mf/defc verify-token [{:keys [route] :as props}] @@ -70,29 +69,30 @@ (rx/subs! (fn [tdata] (handle-token tdata)) - (fn [{:keys [type code] :as error}] - (cond - (or (= :validation type) - (= :invalid-token code) - (= :token-expired (:reason error))) - (reset! bad-token true) + (fn [cause] + (let [{:keys [type code] :as error} (ex-data cause)] + (cond + (or (= :validation type) + (= :invalid-token code) + (= :token-expired (:reason error))) + (reset! bad-token true) - (= :email-already-exists code) - (let [msg (tr "errors.email-already-exists")] - (ts/schedule 100 #(st/emit! (msg/error msg))) - (st/emit! (rt/nav :auth-login))) + (= :email-already-exists code) + (let [msg (tr "errors.email-already-exists")] + (ts/schedule 100 #(st/emit! (ntf/error msg))) + (st/emit! (rt/nav :auth-login))) - (= :email-already-validated code) - (let [msg (tr "errors.email-already-validated")] - (ts/schedule 100 #(st/emit! (msg/warn msg))) - (st/emit! (rt/nav :auth-login))) + (= :email-already-validated code) + (let [msg (tr "errors.email-already-validated")] + (ts/schedule 100 #(st/emit! (ntf/warn msg))) + (st/emit! (rt/nav :auth-login))) - :else - (let [msg (tr "errors.generic")] - (ts/schedule 100 #(st/emit! (msg/error msg))) - (st/emit! (rt/nav :auth-login)))))))) + :else + (let [msg (tr "errors.generic")] + (ts/schedule 100 #(st/emit! (ntf/error msg))) + (st/emit! (rt/nav :auth-login))))))))) (if @bad-token [:> static/invalid-token {}] - [:div {:class (stl/css :verify-token)} - i/loader-pencil]))) + [:> loader* {:title (tr "labels.loading") + :overlay true}]))) diff --git a/frontend/src/app/main/ui/comments.cljs b/frontend/src/app/main/ui/comments.cljs index 9cb417b8ff01ff8a65279e72ad37a77b0d2ed55c..5427b29f14a08dc0a5d60448694a76b068cafb00 100644 --- a/frontend/src/app/main/ui/comments.cljs +++ b/frontend/src/app/main/ui/comments.cljs @@ -17,7 +17,6 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -27,6 +26,8 @@ [okulary.core :as l] [rumext.v2 :as mf])) +(def comments-local-options (l/derived :options refs/comments-local)) + (mf/defc resizing-textarea {::mf/wrap-props false} [props] @@ -94,7 +95,7 @@ (let [show-buttons? (mf/use-state false) content (mf/use-state "") - disabled? (or (fm/all-spaces? @content) + disabled? (or (str/blank? @content) (str/empty-or-nil? @content)) on-focus @@ -153,7 +154,7 @@ pos-x (* (:x position) zoom) pos-y (* (:y position) zoom) - disabled? (or (fm/all-spaces? content) + disabled? (or (str/blank? content) (str/empty-or-nil? content)) on-esc @@ -179,6 +180,7 @@ [:* [:div {:class (stl/css :floating-thread-bubble) + :data-testid "floating-thread-bubble" :style {:top (str pos-y "px") :left (str pos-x "px")} :on-click dom/stop-propagation} @@ -222,7 +224,7 @@ (mf/deps @content) (fn [] (on-submit @content))) - disabled? (or (fm/all-spaces? @content) + disabled? (or (str/blank? @content) (str/empty-or-nil? @content))] [:div {:class (stl/css :edit-form)} @@ -248,25 +250,28 @@ [{:keys [comment thread users origin] :as props}] (let [owner (get users (:owner-id comment)) profile (mf/deref refs/profile) - options (mf/use-state false) + options (mf/deref comments-local-options) edition? (mf/use-state false) on-toggle-options (mf/use-fn + (mf/deps options) (fn [event] (dom/stop-propagation event) - (swap! options not))) + (st/emit! (dcm/toggle-comment-options comment)))) on-hide-options (mf/use-fn + (mf/deps options) (fn [event] (dom/stop-propagation event) - (reset! options false))) + (st/emit! (dcm/hide-comment-options)))) on-edit-clicked (mf/use-fn + (mf/deps options) (fn [] - (reset! options false) + (st/emit! (dcm/hide-comment-options)) (reset! edition? true))) on-delete-comment @@ -282,7 +287,6 @@ (dcm/delete-comment-thread-on-viewer thread) (dcm/delete-comment-thread-on-workspace thread)))) - on-delete-thread (mf/use-fn (mf/deps thread) @@ -337,7 +341,7 @@ :on-cancel on-cancel}] [:span {:class (stl/css :text)} (:content comment)])]] - [:& dropdown {:show @options + [:& dropdown {:show (= options (:id comment)) :on-close on-hide-options} [:ul {:class (stl/css :comment-options-dropdown)} [:li {:class (stl/css :context-menu-option) @@ -355,22 +359,42 @@ [thread-id] (l/derived (l/in [:comments thread-id]) st/state)) +(defn- offset-position [position viewport zoom bubble-margin] + (let [viewport (or viewport {:offset-x 0 :offset-y 0 :width 0 :height 0}) + base-x (+ (* (:x position) zoom) (:offset-x viewport)) + base-y (+ (* (:y position) zoom) (:offset-y viewport)) + w (:width viewport) + h (:height viewport) + comment-width 284 ;; TODO: this is the width set via CSS in an outer container… + ;; We should probably do this in a different way. + orientation-left? (>= (+ base-x comment-width (:x bubble-margin)) w) + orientation-top? (>= base-y (/ h 2)) + h-dir (if orientation-left? :left :right) + v-dir (if orientation-top? :top :bottom) + x (:x position) + y (:y position)] + {:x x :y y :h-dir h-dir :v-dir v-dir})) + (mf/defc thread-comments {::mf/wrap [mf/memo]} - [{:keys [thread zoom users origin position-modifier]}] + [{:keys [thread zoom users origin position-modifier viewport]}] (let [ref (mf/use-ref) - - thread-id (:id thread) thread-pos (:position thread) - pos (cond-> thread-pos + base-pos (cond-> thread-pos (some? position-modifier) (gpt/transform position-modifier)) - pos-x (+ (* (:x pos) zoom) 24) - pos-y (- (* (:y pos) zoom) 28) + max-height (when (some? viewport) (int (* (:height viewport) 0.75))) + ;; We should probably look for a better way of doing this. + bubble-margin {:x 24 :y 0} + pos (offset-position base-pos viewport zoom bubble-margin) + margin-x (* (:x bubble-margin) (if (= (:h-dir pos) :left) -1 1)) + margin-y (* (:y bubble-margin) (if (= (:v-dir pos) :top) -1 1)) + pos-x (+ (* (:x pos) zoom) margin-x) + pos-y (- (* (:y pos) zoom) margin-y) comments-ref (mf/with-memo [thread-id] (make-comments-ref thread-id)) @@ -393,9 +417,13 @@ (dom/scroll-into-view-if-needed! node))) (when (some? comment) - [:div {:class (stl/css :thread-content) - :style {:top (str pos-y "px") - :left (str pos-x "px")} + [:div {:class (stl/css-case :thread-content true + :thread-content-left (= (:h-dir pos) :left) + :thread-content-top (= (:v-dir pos) :top)) + :id (str "thread-" thread-id) + :style {:left (str pos-x "px") + :top (str pos-y "px") + :max-height max-height} :on-click dom/stop-propagation} [:div {:class (stl/css :comments)} @@ -407,9 +435,9 @@ [:* {:key (dm/str (:id item))} [:& comment-item {:comment item :users users - :origin origin}]]) - [:div {:ref ref}]] - [:& reply-form {:thread thread}]]))) + :origin origin}]])] + [:& reply-form {:thread thread}] + [:div {:ref ref}]]))) (defn use-buble [zoom {:keys [position frame-id]}] @@ -530,6 +558,7 @@ :on-pointer-move on-pointer-move* :on-click on-click* :on-lost-pointer-capture on-lost-pointer-capture + :data-testid "floating-thread-bubble" :class (stl/css-case :floating-thread-bubble true :resolved (:is-resolved thread) @@ -580,11 +609,13 @@ [{:keys [group users on-thread-click]}] [:div {:class (stl/css :thread-group)} (if (:file-name group) - [:div {:class (stl/css :section-title)} + [:div {:class (stl/css :section-title) + :title (str (:file-name group) ", " (:page-name group))} [:span {:class (stl/css :file-name)} (:file-name group) ", "] [:span {:class (stl/css :page-name)} (:page-name group)]] - [:div {:class (stl/css :section-title)} + [:div {:class (stl/css :section-title) + :title (:page-name group)} [:span {:class (stl/css :icon)} i/document] [:span {:class (stl/css :page-name)} (:page-name group)]]) diff --git a/frontend/src/app/main/ui/comments.scss b/frontend/src/app/main/ui/comments.scss index ddad36a6bdf62baad37a04881b886c187541d6b7..a2d1fdc52899d4ab0013be5c2e7f67b48c584dd1 100644 --- a/frontend/src/app/main/ui/comments.scss +++ b/frontend/src/app/main/ui/comments.scss @@ -19,6 +19,8 @@ } .section-title { + display: grid; + grid-template-columns: auto auto; @include bodySmallTypography; height: $s-32; display: flex; @@ -27,10 +29,12 @@ } .file-name { + @include textEllipsis; color: var(--comment-subtitle-color); } .page-name { + @include textEllipsis; color: var(--comment-subtitle-color); } @@ -98,8 +102,13 @@ } .content { + position: relative; @include bodySmallTypography; color: var(--color-foreground-primary); + word-wrap: break-word; + overflow-wrap: break-word; + hyphens: auto; + white-space: pre-wrap; } .replies { @@ -133,12 +142,19 @@ // thread-content .thread-content { position: absolute; - pointer-events: auto; - user-select: text; + overflow-y: auto; width: $s-284; padding: $s-12; + padding-inline-end: $s-8; + + pointer-events: auto; + user-select: text; border-radius: $br-8; + border: $s-2 solid var(--modal-border-color); background-color: var(--comment-modal-background-color); + --translate-x: 0%; + --translate-y: 0%; + transform: translate(var(--translate-x), var(--translate-y)); .comments { display: flex; flex-direction: column; @@ -146,6 +162,13 @@ } } +.thread-content-left { + --translate-x: -100%; +} +.thread-content-top { + --translate-y: -100%; +} + // comment-item .comment-container { @@ -192,17 +215,12 @@ } } } - .content { - position: relative; - .text { - @include bodySmallTypography; - } - } } .comment-options-dropdown { @extend .dropdown-wrapper; position: absolute; - width: $s-120; + width: fit-content; + max-width: $s-200; right: 0; left: unset; .context-menu-option { @@ -217,14 +235,15 @@ .reply-form { textarea { @extend .input-element; + @include bodySmallTypography; line-height: 1.45; height: 100%; width: 100%; max-width: $s-260; - min-width: $s-260; margin-bottom: $s-8; padding: $s-8; color: var(--input-foreground-color-active); + resize: vertical; &:focus { border: $s-1 solid var(--input-border-color-active); outline: none; diff --git a/frontend/src/app/main/ui/components/button_link.cljs b/frontend/src/app/main/ui/components/button_link.cljs index fadfbd3478308618132768f4733b23fd174317fd..bac6e8e8740c5bac0bcd1ea98ac76d7f28ad6041 100644 --- a/frontend/src/app/main/ui/components/button_link.cljs +++ b/frontend/src/app/main/ui/components/button_link.cljs @@ -5,7 +5,9 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.components.button-link + (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.util.keyboard :as kbd] [rumext.v2 :as mf])) @@ -18,8 +20,8 @@ (when (kbd/enter? event) (when (fn? on-click) (on-click event)))))] - [:a.btn-primary.btn-large.button-link - {:class class + [:a + {:class (dm/str class " " (stl/css :button)) :tab-index "0" :on-click on-click :on-key-down on-key-down} diff --git a/frontend/src/app/main/ui/components/button_link.scss b/frontend/src/app/main/ui/components/button_link.scss new file mode 100644 index 0000000000000000000000000000000000000000..81b0538d9a64a9946ec791953d55e711ff3e90c3 --- /dev/null +++ b/frontend/src/app/main/ui/components/button_link.scss @@ -0,0 +1,28 @@ +// 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 + +@import "refactor/common-refactor.scss"; + +.button { + appearance: none; + align-items: center; + border: none; + cursor: pointer; + display: flex; + font-family: "worksans", "vazirmatn", sans-serif; + justify-content: center; + min-width: 25px; + padding: 0 1rem; + transition: all 0.4s; + text-decoration: none !important; + + height: 40px; + + svg { + height: 20px; + width: 20px; + } +} diff --git a/frontend/src/app/main/ui/components/buttons/simple_button.cljs b/frontend/src/app/main/ui/components/buttons/simple_button.cljs deleted file mode 100644 index fb4bdd9952913cbf1466905024cac980cef1eca3..0000000000000000000000000000000000000000 --- a/frontend/src/app/main/ui/components/buttons/simple_button.cljs +++ /dev/null @@ -1,10 +0,0 @@ -(ns app.main.ui.components.buttons.simple-button - (:require-macros [app.main.style :as stl]) - (:require - [rumext.v2 :as mf])) - -(mf/defc simple-button - {::mf/wrap-props false} - [{:keys [on-click children]}] - [:button {:on-click on-click :class (stl/css :button)} children]) - diff --git a/frontend/src/app/main/ui/components/buttons/simple_button.mdx b/frontend/src/app/main/ui/components/buttons/simple_button.mdx deleted file mode 100644 index 6c93cc3a215647da8406a592de676978f78d2255..0000000000000000000000000000000000000000 --- a/frontend/src/app/main/ui/components/buttons/simple_button.mdx +++ /dev/null @@ -1,16 +0,0 @@ -import { Canvas, Meta } from '@storybook/blocks'; -import * as SimpleButtonStories from "./simple_button.stories" - - - -# Lorem ipsum - -This is an example of **markdown** docs within storybook, for the component ``. - -Here's how we can render a simple button: - - - -Simple buttons can also have **icons**: - - \ No newline at end of file diff --git a/frontend/src/app/main/ui/components/buttons/simple_button.scss b/frontend/src/app/main/ui/components/buttons/simple_button.scss deleted file mode 100644 index e1d162fbc1912adb40932ba84763ead6ee1e6fdc..0000000000000000000000000000000000000000 --- a/frontend/src/app/main/ui/components/buttons/simple_button.scss +++ /dev/null @@ -1,13 +0,0 @@ -.button { - font-family: monospace; - - display: flex; - align-items: center; - column-gap: 0.5rem; - - svg { - width: 16px; - height: 16px; - stroke: #000; - } -} diff --git a/frontend/src/app/main/ui/components/buttons/simple_button.stories.jsx b/frontend/src/app/main/ui/components/buttons/simple_button.stories.jsx deleted file mode 100644 index 33142e12c4d35ebb55579b1a8bd28052fa40dd2d..0000000000000000000000000000000000000000 --- a/frontend/src/app/main/ui/components/buttons/simple_button.stories.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from "react"; - -import Components from "@target/components"; -import Icons from "@target/icons"; - -export default { - title: 'Buttons/Simple Button', - component: Components.SimpleButton, -}; - -export const Default = { - render: () => ( - - - Simple Button - - - ), -}; - -export const WithIcon = { - render: () => ( - - - {Icons.AddRefactor} - Simple Button - - - ), -} diff --git a/frontend/src/app/main/ui/components/buttons/simple_button.test.mjs b/frontend/src/app/main/ui/components/buttons/simple_button.test.mjs deleted file mode 100644 index 9d1c6c9ac2b089a093411cfa629de8f2ecd88137..0000000000000000000000000000000000000000 --- a/frontend/src/app/main/ui/components/buttons/simple_button.test.mjs +++ /dev/null @@ -1,10 +0,0 @@ -import { expect, test } from 'vitest' - -test('use jsdom in this test file', () => { - const element = document.createElement('div') - expect(element).not.toBeNull() -}) - -test('adds 1 + 2 to equal 3', () => { - expect(1 +2).toBe(3) -}); diff --git a/frontend/src/app/main/ui/components/code_block.scss b/frontend/src/app/main/ui/components/code_block.scss index 49d63fda19b6beb4b0b10399f0f9381de83c67c0..7b44c702ae46c7556f1c8a469645aab8921b50d6 100644 --- a/frontend/src/app/main/ui/components/code_block.scss +++ b/frontend/src/app/main/ui/components/code_block.scss @@ -12,5 +12,6 @@ margin-top: $s-8; padding: $s-12; background-color: var(--menu-background-color); + color: var(--input-foreground-color-active); overflow: auto; } diff --git a/frontend/src/app/main/ui/components/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs index 1d4b9aacc7b0ce2bac498e05b188fb52ceab7d99..1a39f1750bf79c07dbafa01cfd1060fee653cab3 100644 --- a/frontend/src/app/main/ui/components/color_bullet.cljs +++ b/frontend/src/app/main/ui/components/color_bullet.cljs @@ -44,6 +44,10 @@ (some? image) (tr "media.image"))))) +(defn- breakable-color-title + [title] + (str/replace title "." ".\u200B")) + (mf/defc color-bullet {::mf/wrap [mf/memo] ::mf/wrap-props false} @@ -76,6 +80,7 @@ :is-transparent (and opacity (> 1 opacity)) :grid-area area :read-only read-only?) + :role "button" :data-readonly (str read-only?) :on-click on-click :title (color-title color)} @@ -111,4 +116,4 @@ :title name :on-click on-click :on-double-click on-double-click} - (or name color (uc/gradient-type->string (:type gradient)))]))) + (breakable-color-title (or name color (uc/gradient-type->string (:type gradient))))]))) diff --git a/frontend/src/app/main/ui/components/color_bullet.scss b/frontend/src/app/main/ui/components/color_bullet.scss index 36afda4b2a1e720ce68f94a003f9b0476061a371..37b733f34d2f058d91b8ec9d7f5d6184f89eba20 100644 --- a/frontend/src/app/main/ui/components/color_bullet.scss +++ b/frontend/src/app/main/ui/components/color_bullet.scss @@ -39,13 +39,13 @@ } } &.is-gradient { - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=") left center; background-color: var(--color-bullet-background-color); transform: rotate(-90deg); } &.is-transparent { - background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=") + background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=") left center; background-color: var(--color-bullet-background-color); } @@ -86,8 +86,8 @@ .big-text { @include inspectValue; @include twoLineTextEllipsis; + line-height: 1; color: var(--palette-text-color); - height: $s-28; text-align: center; } diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.cljs b/frontend/src/app/main/ui/components/context_menu_a11y.cljs index 0787b69e7cd497c43b80284cd63fcef15bbcc69c..ea81d8d5036ec29a0ce6ce8cebf0183d0c80b6cc 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.cljs +++ b/frontend/src/app/main/ui/components/context_menu_a11y.cljs @@ -39,7 +39,7 @@ id (gobj/get props "id") klass (gobj/get props "class") key-index (gobj/get props "key-index") - data-test (gobj/get props "data-test")] + data-testid (gobj/get props "data-testid")] [:li {:id id :class klass :tab-index "0" @@ -47,7 +47,7 @@ :on-click on-click :key key-index :role "menuitem" - :data-test data-test} + :data-testid data-testid} children])) (mf/defc context-menu-a11y' @@ -230,7 +230,7 @@ id (:id option) sub-options (:sub-options option) option-handler (:option-handler option) - data-test (:data-test option)] + data-testid (:data-testid option)] (when option-name (if (= option-name :separator) [:li {:key (dm/str "context-item-" index) @@ -240,7 +240,7 @@ :key id :class (stl/css-case :is-selected (and selected (= option-name selected)) - :selected (and selected (= data-test selected)) + :selected (and selected (= data-testid selected)) :context-menu-item true) :key-index (dm/str "context-item-" index) :tab-index "0" @@ -251,18 +251,18 @@ :on-click #(do (dom/stop-propagation %) (on-close) (option-handler %)) - :data-test data-test} + :data-testid data-testid} (if (and in-dashboard? (= option-name "Default")) (tr "dashboard.default-team-name") option-name) - (when (and selected (= data-test selected)) + (when (and selected (= data-testid selected)) [:span {:class (stl/css :selected-icon)} i/tick])] [:a {:class (stl/css :context-menu-action :submenu) :data-no-close true :on-click (enter-submenu option-name sub-options) - :data-test data-test} + :data-testid data-testid} option-name [:span {:class (stl/css :submenu-icon)} i/arrow]])]))))])])]))) diff --git a/frontend/src/app/main/ui/components/context_menu_a11y.scss b/frontend/src/app/main/ui/components/context_menu_a11y.scss index ed007261c06078f55f9c22dadb2207b8f03d7e8f..ea8a89cc5b7223b8dd63da7c62b06e76c872bc29 100644 --- a/frontend/src/app/main/ui/components/context_menu_a11y.scss +++ b/frontend/src/app/main/ui/components/context_menu_a11y.scss @@ -10,7 +10,7 @@ position: relative; visibility: hidden; opacity: $op-0; - z-index: $z-index-3; + z-index: $z-index-4; &.is-open { position: relative; diff --git a/frontend/src/app/main/ui/components/dropdown_menu.cljs b/frontend/src/app/main/ui/components/dropdown_menu.cljs index 156a1b651f69a4d4e3c504e0faf0ac735f976ca6..8f9daef577cac93478912c512746689264530205 100644 --- a/frontend/src/app/main/ui/components/dropdown_menu.cljs +++ b/frontend/src/app/main/ui/components/dropdown_menu.cljs @@ -96,14 +96,17 @@ [:ul {:class list-class :role "menu"} children])) (mf/defc dropdown-menu - {::mf/wrap-props false} + {::mf/props :obj} [props] (assert (fn? (gobj/get props "on-close")) "missing `on-close` prop") (assert (boolean? (gobj/get props "show")) "missing `show` prop") (let [ids (obj/get props "ids") - ids (d/nilv ids (->> (obj/get props "children") - (keep #(obj/get-in % ["props" "id"]))))] + ids (or ids + (->> (obj/get props "children") + (keep (fn [o] + (let [props (obj/get o "props")] + (obj/get props "id"))))))] (when (gobj/get props "show") (mf/element dropdown-menu' diff --git a/frontend/src/app/main/ui/components/editable_label.scss b/frontend/src/app/main/ui/components/editable_label.scss index 1f72eaf7abe662c15c7671ec3348c94c83cac67b..a8c3991589739f946f99d449514aaaad49f5e810 100644 --- a/frontend/src/app/main/ui/components/editable_label.scss +++ b/frontend/src/app/main/ui/components/editable_label.scss @@ -20,12 +20,8 @@ color: var(--input-foreground-color-active); } -.editable-label { - display: flex; - - &.is-hidden { - display: none; - } +.editable-label.is-hidden { + display: none; } .editable-label-close { diff --git a/frontend/src/app/main/ui/components/file_uploader.cljs b/frontend/src/app/main/ui/components/file_uploader.cljs index 35429e09ee1e2419b60b6e7641f843296b0e7062..8eebdff516042fb84ffdb3366e2b0915a370ad7c 100644 --- a/frontend/src/app/main/ui/components/file_uploader.cljs +++ b/frontend/src/app/main/ui/components/file_uploader.cljs @@ -12,7 +12,7 @@ (mf/defc file-uploader {::mf/forward-ref true} - [{:keys [accept multi label-text label-class input-id on-selected data-test] :as props} input-ref] + [{:keys [accept multi label-text label-class input-id on-selected data-testid] :as props} input-ref] (let [opt-pick-one #(if multi % (first %)) on-files-selected @@ -38,6 +38,6 @@ :type "file" :ref input-ref :on-change on-files-selected - :data-test data-test + :data-testid data-testid :aria-label "uploader"}]])) diff --git a/frontend/src/app/main/ui/components/forms.cljs b/frontend/src/app/main/ui/components/forms.cljs index 7d1ad4a46db95f1df0d049287f6cce90ce2a1ab1..15361c42fa47b43960b9c232bd2dc68d416ada78 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -16,17 +16,18 @@ [app.util.forms :as fm] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] - [app.util.object :as obj] [cljs.core :as c] - [clojure.string] [cuerdas.core :as str] - [rumext.v2 :as mf])) + [rumext.v2 :as mf] + [rumext.v2.util :as mfu])) (def form-ctx (mf/create-context nil)) (def use-form fm/use-form) (mf/defc input - [{:keys [label help-icon disabled form hint trim children data-test on-change-value placeholder show-success?] :as props}] + [{:keys [label help-icon disabled form hint trim children data-testid on-change-value placeholder show-success? show-error] + :or {show-error true} + :as props}] (let [input-type (get props :type "text") input-name (get props :name) more-classes (get props :class) @@ -101,7 +102,7 @@ (cond-> (and value is-checkbox?) (assoc :default-checked value)) (cond-> (and touched? (:message error)) (assoc "aria-invalid" "true" "aria-describedby" (dm/str "error-" input-name))) - (obj/clj->props)) + (mfu/map->props)) checked? (and is-checkbox? (= value true)) show-valid? (and show-success? touched? (not error)) @@ -117,7 +118,7 @@ [:* (cond (some? label) - [:label {:class (stl/css-case :input-with-label (not is-checkbox?) + [:label {:class (stl/css-case :input-with-label-form (not is-checkbox?) :input-label is-text? :radio-label is-radio? :checkbox-label is-checkbox?) @@ -152,11 +153,14 @@ children]) (cond - (and touched? (:message error)) - [:div {:id (dm/str "error-" input-name) - :class (stl/css :error) - :data-test (clojure.string/join [data-test "-error"])} - (tr (:message error))] + (and touched? (:code error) show-error) + (let [code (:code error)] + [:div {:id (dm/str "error-" input-name) + :class (stl/css :error) + :data-testid (dm/str data-testid "-error")} + (if (vector? code) + (tr (nth code 0) (i18n/c (nth code 1))) + (tr code))]) (string? hint) [:div {:class (stl/css :hint)} hint])]])) @@ -201,20 +205,20 @@ :on-blur on-blur ;; :placeholder label :on-change on-change) - (obj/clj->props))] + (mfu/map->props))] [:div {:class (dm/str klass " " (stl/css :textarea-wrapper))} [:label {:class (stl/css :textarea-label)} label] [:> :textarea props] (cond - (and touched? (:message error)) - [:span {:class (stl/css :error)} (tr (:message error))] + (and touched? (:code error)) + [:span {:class (stl/css :error)} (tr (:code error))] (string? hint) [:span {:class (stl/css :hint)} hint])])) (mf/defc select - [{:keys [options disabled form default dropdown-class] :as props + [{:keys [options disabled form default dropdown-class select-class] :as props :or {default ""}}] (let [input-name (get props :name) form (or form (mf/use-ctx form-ctx)) @@ -230,6 +234,7 @@ {:default-value value :disabled disabled :options options + :class select-class :dropdown-class dropdown-class :on-change handle-change}]])) @@ -239,6 +244,7 @@ (let [form (or (unchecked-get props "form") (mf/use-ctx form-ctx)) name (unchecked-get props "name") + image (unchecked-get props "image") current-value (or (dm/get-in @form [:data name] "") (unchecked-get props "value")) @@ -260,7 +266,9 @@ (when (fn? on-change) (on-change name value)))))] - [:div {:class (dm/str class " " (stl/css :custom-radio))} + [:div {:class (if image + class + (dm/str class " " (stl/css :custom-radio)))} (for [{:keys [image icon value label area]} options] (let [image? (some? image) icon? (some? icon) @@ -294,6 +302,71 @@ :value value' :checked checked?}]]))])) +(mf/defc image-radio-buttons + {::mf/wrap-props false} + [props] + (let [form (or (unchecked-get props "form") + (mf/use-ctx form-ctx)) + name (unchecked-get props "name") + image (unchecked-get props "image") + img-height (unchecked-get props "img-height") + img-width (unchecked-get props "img-width") + current-value (or (dm/get-in @form [:data name] "") + (unchecked-get props "value")) + on-change (unchecked-get props "on-change") + options (unchecked-get props "options") + trim? (unchecked-get props "trim") + class (unchecked-get props "class") + encode-fn (d/nilv (unchecked-get props "encode-fn") identity) + decode-fn (d/nilv (unchecked-get props "decode-fn") identity) + + on-change' + (mf/use-fn + (mf/deps on-change form name) + (fn [event] + (let [value (-> event dom/get-target dom/get-value decode-fn)] + (when (some? form) + (swap! form assoc-in [:touched name] true) + (fm/on-input-change form name value trim?)) + + (when (fn? on-change) + (on-change name value)))))] + + [:div {:class (if image + class + (dm/str class " " (stl/css :custom-radio)))} + (for [{:keys [image icon value label area]} options] + (let [icon? (some? icon) + value' (encode-fn value) + checked? (= value current-value) + key (str/ffmt "%-%" (d/name name) (d/name value'))] + + [:label {:for key + :key key + :style {:grid-area area} + :class (stl/css-case :radio-label-image true + :global/checked checked?)} + (cond + icon? + [:span {:class (stl/css :icon-inside) + :style {:height img-height + :width img-width}} icon] + + :else + [:span {:style {:background-image (str/ffmt "url(%)" image) + :height img-height + :width img-width} + :class (stl/css :image-inside)}]) + + [:span {:class (stl/css :image-text)} label] + [:input {:on-change on-change' + :type "radio" + :class (stl/css :radio-input) + :id key + :name name + :value value' + :checked checked?}]]))])) + (mf/defc submit-button* {::mf/wrap-props false} [{:keys [on-click children label form class name disabled] :as props}] @@ -347,7 +420,7 @@ (into [] (distinct) (conj coll item))) (mf/defc multi-input - [{:keys [form label class name trim valid-item-fn caution-item-fn on-submit] :as props}] + [{:keys [form label class name trim valid-item-fn caution-item-fn on-submit invite-email] :as props}] (let [form (or form (mf/use-ctx form-ctx)) input-name (get props :name) touched? (get-in @form [:touched input-name]) @@ -375,6 +448,7 @@ :no-padding (pos? (count @items)) :invalid (and (some? valid-item-fn) touched? + (not (str/empty? @value)) (not (valid-item-fn @value))))) on-focus @@ -454,6 +528,12 @@ values (filterv #(:valid %) values)] (update-form! values))) + (mf/with-effect [] + (when invite-email + (swap! items conj-dedup {:text (str/trim invite-email) + :valid (valid-item-fn invite-email) + :caution (caution-item-fn invite-email)}))) + [:div {:class klass} [:input {:id (name input-name) :class in-klass @@ -480,41 +560,3 @@ [:span {:class (stl/css :text)} (:text item)] [:button {:class (stl/css :icon) :on-click #(remove-item! item)} i/close]]])])])) - -;; --- Validators - -(defn all-spaces? - [value] - (let [trimmed (str/trim value)] - (str/empty? trimmed))) - -(def max-length-allowed 250) -(def max-uri-length-allowed 2048) - -(defn max-length? - [value length] - (> (count value) length)) - -(defn validate-length - [field length errors-msg] - (fn [errors data] - (cond-> errors - (max-length? (get data field) length) - (assoc field {:message errors-msg})))) - -(defn validate-not-empty - [field error-msg] - (fn [errors data] - (cond-> errors - (all-spaces? (get data field)) - (assoc field {:message error-msg})))) - -(defn validate-not-all-spaces - [field error-msg] - (fn [errors data] - (let [value (get data field)] - (cond-> errors - (and - (all-spaces? value) - (> (count value) 0)) - (assoc field {:message error-msg}))))) diff --git a/frontend/src/app/main/ui/components/forms.scss b/frontend/src/app/main/ui/components/forms.scss index 19027a065f3407e6f020763bd9324e7dc9c0dbc6..b31713aad0a5f8cbe97b6d71f207ccaa527e3f2c 100644 --- a/frontend/src/app/main/ui/components/forms.scss +++ b/frontend/src/app/main/ui/components/forms.scss @@ -38,10 +38,9 @@ } } -.input-with-label { +.input-with-label-form { @include flexColumn; gap: $s-8; - @include bodySmallTypography; justify-content: flex-start; align-items: flex-start; height: 100%; @@ -50,12 +49,12 @@ cursor: pointer; color: var(--modal-title-foreground-color); text-transform: uppercase; - margin-bottom: $s-8; input { @extend .input-element; color: var(--input-foreground-color-active); margin-top: 0; width: 100%; + max-width: 100%; height: 100%; padding: 0 $s-8; @@ -65,6 +64,7 @@ border-radius: $br-8; } } + // Input autofill input:-webkit-autofill, input:-webkit-autofill:hover, @@ -93,7 +93,7 @@ top: calc(50% - $s-8); svg { @extend .button-icon-small; - stroke: $df-secondary; + stroke: var(--color-foreground-secondary); width: $s-16; height: $s-16; } @@ -144,8 +144,9 @@ .hint { @include bodySmallTypography; - color: var(--modal-text-foreground-color); width: 99%; + margin-block-start: $s-8; + color: var(--modal-text-foreground-color); } .checkbox { @@ -169,6 +170,10 @@ border-color: var(--input-checkbox-border-color-hover); } } + a { + // Need for terms and conditions links on register checkbox + color: var(--link-foreground-color); + } } } @@ -259,10 +264,10 @@ // SUBMIT-BUTTON .button-submit { @extend .button-primary; -} - -:disabled { - @extend .button-disabled; + &:disabled { + @extend .button-disabled; + min-height: $s-32; + } } // MULTI INPUT @@ -367,7 +372,7 @@ height: fit-content; border-radius: $br-8; padding: $s-8; - color: var(--input-foreground-color); + color: var(--input-foreground-color-rest); border: $s-1 solid transparent; &:focus, &:focus-within { @@ -393,14 +398,12 @@ border-radius: $br-circle; } -.radio-label.with-image { +.radio-label-image { @include smallTitleTipography; display: grid; grid-template-rows: auto auto 0px; justify-items: center; gap: 0; - height: $s-116; - width: $s-92; border-radius: $br-8; margin: 0; border: 1px solid var(--color-background-tertiary); @@ -413,22 +416,29 @@ outline: none; border: $s-1 solid var(--input-border-color-active); } + .image-text { + color: var(--input-foreground-color-rest); + display: grid; + align-self: center; + margin-bottom: $s-16; + padding-inline: $s-8; + text-align: center; + } } .image-inside { - width: $s-60; - height: $s-48; - background-size: $s-48; + margin: $s-16; + background-size: 100%; background-repeat: no-repeat; background-position: center; } .icon-inside { - width: $s-60; - height: $s-48; + margin: $s-16; + @include flexCenter; svg { - width: $s-60; - height: $s-48; + width: 40px; + height: 60px; stroke: var(--icon-foreground); fill: none; } diff --git a/frontend/src/app/main/ui/components/link.cljs b/frontend/src/app/main/ui/components/link.cljs index 4c48681bba156c6886c6d5e02c05352801a41712..e0c1d90fb634846896071916747968d2c7141f94 100644 --- a/frontend/src/app/main/ui/components/link.cljs +++ b/frontend/src/app/main/ui/components/link.cljs @@ -12,7 +12,7 @@ (mf/defc link {::mf/wrap-props false} - [{:keys [action class data-test keyboard-action children]}] + [{:keys [action class data-testid keyboard-action children]}] (let [keyboard-action (d/nilv keyboard-action action)] [:a {:on-click action :class class @@ -20,5 +20,5 @@ (when ^boolean (kbd/enter? event) (keyboard-action event))) :tab-index "0" - :data-test data-test} + :data-testid data-testid} children])) diff --git a/frontend/src/app/main/ui/components/link_button.cljs b/frontend/src/app/main/ui/components/link_button.cljs index eb8c5db8e6d5fea66ffb7675a7c5fea51ae9a330..90da87209c78aa727e7d96fe714957aba74f170e 100644 --- a/frontend/src/app/main/ui/components/link_button.cljs +++ b/frontend/src/app/main/ui/components/link_button.cljs @@ -11,7 +11,7 @@ (mf/defc link-button {::mf/wrap-props false} - [{:keys [on-click class value data-test]}] + [{:keys [on-click class value data-testid]}] (let [on-key-down (mf/use-fn (mf/deps on-click) (fn [event] @@ -24,4 +24,4 @@ :tab-index "0" :on-click on-click :on-key-down on-key-down - :data-test data-test}])) + :data-testid data-testid}])) diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index 308ef8e427096145d0fbf41e1e0e902662b7f364..4a1823868a4994b5f07033207571694e47a558bb 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -36,7 +36,7 @@ title (unchecked-get props "title") default (unchecked-get props "default") nillable? (unchecked-get props "nillable") - class (d/nilv (unchecked-get props "className") "input-text") + class (d/nilv (unchecked-get props "className") "") min-value (d/parse-double min-value) max-value (d/parse-double max-value) @@ -152,7 +152,7 @@ handle-key-down (mf/use-fn - (mf/deps set-delta apply-value update-input) + (mf/deps set-delta apply-value update-input parse-value) (fn [event] (mf/set-ref-val! dirty-ref true) (let [up? (kbd/up-arrow? event) @@ -162,13 +162,14 @@ node (mf/ref-val ref)] (when (or up? down?) (set-delta event up? down?)) + (reset! last-value* (parse-value)) (when enter? (dom/blur! node)) (when esc? (update-input value-str) (dom/blur! node))))) - handle-key-up + handle-change (mf/use-fn (mf/deps parse-value) (fn [] @@ -224,14 +225,13 @@ (obj/unset! "selectOnFocus") (obj/unset! "nillable") (obj/set! "value" mf/undefined) - (obj/set! "onChange" mf/undefined) + (obj/set! "onChange" handle-change) (obj/set! "className" class) (obj/set! "type" "text") (obj/set! "ref" ref) (obj/set! "defaultValue" (fmt/format-number value)) (obj/set! "title" title) (obj/set! "onKeyDown" handle-key-down) - (obj/set! "onKeyUp" handle-key-up) (obj/set! "onBlur" handle-blur) (obj/set! "onFocus" handle-focus))] diff --git a/frontend/src/app/main/ui/components/radio_buttons.cljs b/frontend/src/app/main/ui/components/radio_buttons.cljs index 0d7cce2948fd59cedf4c44944e25248cf13c1482..17a3fe5942d6d198dd0ce29d628c2934ee941ee4 100644 --- a/frontend/src/app/main/ui/components/radio_buttons.cljs +++ b/frontend/src/app/main/ui/components/radio_buttons.cljs @@ -54,11 +54,11 @@ :name name :disabled disabled :value value - :checked checked?}]])) + :default-checked checked?}]])) (mf/defc radio-buttons {::mf/props :obj} - [{:keys [children on-change selected class wide encode-fn decode-fn allow-empty] :as props}] + [{:keys [name children on-change selected class wide encode-fn decode-fn allow-empty] :as props}] (let [encode-fn (d/nilv encode-fn identity) decode-fn (d/nilv decode-fn identity) nitems (if (array? children) @@ -94,5 +94,6 @@ [:& (mf/provider context) {:value context-value} [:div {:class (dm/str class " " (stl/css :radio-btn-wrapper)) - :style {:width width}} + :style {:width width} + :key (dm/str name "-" selected)} children]])) diff --git a/frontend/src/app/main/ui/components/search_bar.cljs b/frontend/src/app/main/ui/components/search_bar.cljs index 0fb73303c86d0f7e2f6753eb25866798bf385ddf..e9c82e391795c6ce3e8cc43c7ec313d408066205 100644 --- a/frontend/src/app/main/ui/components/search_bar.cljs +++ b/frontend/src/app/main/ui/components/search_bar.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.components.search-bar (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.keyboard :as kbd] @@ -23,7 +24,7 @@ icon (unchecked-get props "icon") autofocus (unchecked-get props "auto-focus") id (unchecked-get props "id") - + input-class (unchecked-get props "class") handle-change (mf/use-fn @@ -51,7 +52,7 @@ [:span {:class (stl/css-case :search-box true :has-children (some? children))} children - [:div {:class (stl/css :search-input-wrapper)} + [:div {:class (dm/str input-class " " (stl/css :search-input-wrapper))} icon [:input {:id id :on-change handle-change diff --git a/frontend/src/app/main/ui/components/tab_container.cljs b/frontend/src/app/main/ui/components/tab_container.cljs index 4dae9d52b34f62f3f5340ca0afefaec33db64970..1e3b99079c7229deedd3b74d1037b18b41d911c7 100644 --- a/frontend/src/app/main/ui/components/tab_container.cljs +++ b/frontend/src/app/main/ui/components/tab_container.cljs @@ -54,14 +54,18 @@ (let [props (.-props tab) id (.-id props) title (.-title props) - sid (d/name id)] + sid (d/name id) + tooltip (if (string? title) title nil)] [:div {:key (str/concat "tab-" sid) + :title tooltip :data-id sid + :data-testid sid :on-click on-click :class (stl/css-case :tab-container-tab-title true :current (= selected id))} - title]))]] + [:span {:class (stl/css :content)} + title]]))]] [:div {:class (dm/str content-class " " (stl/css :tab-container-content))} (d/seek #(= selected (-> % .-props .-id)) diff --git a/frontend/src/app/main/ui/components/tab_container.scss b/frontend/src/app/main/ui/components/tab_container.scss index 86ac4c4f3615312917e10869e615f380d74ddcef..c01715bd42feb3bffd6789cfc0757f78676dabdb 100644 --- a/frontend/src/app/main/ui/components/tab_container.scss +++ b/frontend/src/app/main/ui/components/tab_container.scss @@ -21,74 +21,89 @@ cursor: pointer; font-size: $fs-12; height: 100%; - .tab-container-tab-wrapper { - @include flexCenter; - flex-direction: row; - height: 100%; - width: 100%; - .tab-container-tab-title { - @include flexCenter; - @include uppercaseTitleTipography; - height: 100%; - width: 100%; - padding: 0 $s-8; - margin: 0; - border-radius: $br-8; - background-color: transparent; - color: var(--tab-foreground-color); - white-space: nowrap; - border: $s-2 solid var(--tab-border-color); - svg { - @extend .button-icon; - stroke: var(--tab-foreground-color); - } - - &.current, - &.current:hover { - background: var(--tab-background-color-selected); - border-color: var(--tab-border-color-selected); - color: var(--tab-foreground-color-selected); - svg { - stroke: var(--tab-foreground-color-selected); - } - } - &:hover { - color: var(--tab-foreground-color-hover); - svg { - stroke: var(--tab-foreground-color-hover); - } - } +} + +.tab-container-tab-wrapper { + display: grid; + grid-auto-flow: column; + height: 100%; + width: 100%; +} + +.tab-container-tab-title { + @include flexCenter; + height: 100%; + width: 100%; + padding: 0 $s-8; + margin: 0; + border-radius: $br-8; + background-color: transparent; + color: var(--tab-foreground-color); + border: $s-2 solid var(--tab-border-color); + min-width: 0; + + svg { + @extend .button-icon; + stroke: var(--tab-foreground-color); + } + + .content { + @include headlineSmallTypography; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &.current, + &.current:hover { + background: var(--tab-background-color-selected); + border-color: var(--tab-border-color-selected); + color: var(--tab-foreground-color-selected); + + svg { + stroke: var(--tab-foreground-color-selected); } } - .collapse-sidebar { - @include flexCenter; - @include buttonStyle; - height: 100%; - width: $s-24; - min-width: $s-24; - padding: 0 $s-6; - border-radius: $br-5; + &:hover { + color: var(--tab-foreground-color-hover); + svg { - @include flexCenter; - height: $s-16; - width: $s-16; - stroke: var(--icon-foreground); - transform: rotate(180deg); - fill: none; - color: transparent; + stroke: var(--tab-foreground-color-hover); } - &:hover { - svg { - stroke: var(--icon-foreground-hover); - } + } +} + +.collapse-sidebar { + @include flexCenter; + @include buttonStyle; + height: 100%; + width: $s-24; + min-width: $s-24; + padding: 0 $s-6; + border-radius: $br-5; + + svg { + @include flexCenter; + height: $s-16; + width: $s-16; + stroke: var(--icon-foreground); + transform: rotate(180deg); + fill: none; + color: transparent; + } + + &:hover { + svg { + stroke: var(--icon-foreground-hover); } + } - &.collapsed { - svg { - transform: rotate(0deg); - padding: 0 0 0 $s-6; - } + &.collapsed { + svg { + transform: rotate(0deg); + padding: 0 0 0 $s-6; } } } @@ -99,3 +114,10 @@ display: flex; flex-direction: column; } + +//Firefox doesn't respect scrollbar-gutter +@supports (-moz-appearance: none) { + .tab-container-content { + padding-right: $s-8; + } +} diff --git a/frontend/src/app/main/ui/components/title_bar.scss b/frontend/src/app/main/ui/components/title_bar.scss index 2ff458ded16fce95d5dc0e94c64cef609e2a40b5..20e25e233b375d58a337da39142aba2aa174f9fb 100644 --- a/frontend/src/app/main/ui/components/title_bar.scss +++ b/frontend/src/app/main/ui/components/title_bar.scss @@ -14,23 +14,26 @@ width: 100%; min-height: $s-32; background-color: var(--title-background-color); + color: var(--title-foreground-color); } .title, .title-only, .inspect-title { - @include uppercaseTitleTipography; - display: flex; + @include headlineSmallTypography; + display: grid; align-items: center; - flex-grow: 1; + justify-content: flex-start; + grid-auto-flow: column; height: 100%; min-height: $s-32; - color: var(--title-foreground-color); + overflow: hidden; } .title-only { - margin-left: $s-8; + --title-bar-title-margin: #{$s-8}; + margin-inline-start: var(--title-bar-title-margin); } .inspect-title { @@ -66,23 +69,6 @@ } } -.title, -.title-only { - @include uppercaseTitleTipography; - display: flex; - align-items: center; - flex-grow: 1; - height: 100%; - min-height: $s-32; - color: var(--title-foreground-color); - overflow: hidden; -} - -.title-only { - --title-bar-title-margin: #{$s-8}; - margin-inline-start: var(--title-bar-title-margin); -} - .title-only-icon-gap { --title-bar-title-margin: #{$s-12}; } diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs index 66fd00e925796cdfcf38d17e4b4c652a8ffe3128..abb11ea8c72a9e3b89071a1adf92023d00367564 100644 --- a/frontend/src/app/main/ui/confirm.cljs +++ b/frontend/src/app/main/ui/confirm.cljs @@ -11,7 +11,7 @@ [app.main.store :as st] [app.main.ui.icons :as i] [app.util.dom :as dom] - [app.util.i18n :as i18n :refer [tr t]] + [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as k] [goog.events :as events] [rumext.v2 :as mf]) @@ -30,15 +30,13 @@ cancel-label accept-label accept-style] :as props}] - (let [locale (mf/deref i18n/locale) - - on-accept (or on-accept identity) + (let [on-accept (or on-accept identity) on-cancel (or on-cancel identity) - message (or message (t locale "ds.confirm-title")) + message (or message (tr "ds.confirm-title")) cancel-label (or cancel-label (tr "ds.confirm-cancel")) accept-label (or accept-label (tr "ds.confirm-ok")) accept-style (or accept-style :danger) - title (or title (t locale "ds.confirm-title")) + title (or title (tr "ds.confirm-title")) accept-fn (mf/use-callback diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index bfe0fa6ee661e71743bff2db27145db4e076f0ba..1aa4b532be0ca3b411283ab11cb5c7611ed6a85e 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -17,6 +17,7 @@ (def current-page-id (mf/create-context nil)) (def current-file-id (mf/create-context nil)) (def current-vbox (mf/create-context nil)) +(def current-svg-root-id (mf/create-context nil)) (def active-frames (mf/create-context nil)) (def render-thumbnails (mf/create-context nil)) diff --git a/frontend/src/app/main/ui/cursors.cljs b/frontend/src/app/main/ui/cursors.cljs index 8b41528167fed1fc462dd03a80ba15708cd2ca06..ec76bbd0bda4e8a68fb055e8d969c5c2667b30e5 100644 --- a/frontend/src/app/main/ui/cursors.cljs +++ b/frontend/src/app/main/ui/cursors.cljs @@ -5,11 +5,7 @@ ;; Copyright (c) KALEIDOS INC (ns app.main.ui.cursors - (:require-macros [app.main.ui.cursors :refer [cursor-ref cursor-fn collect-cursors]]) - (:require - [app.util.timers :as ts] - [cuerdas.core :as str] - [rumext.v2 :as mf])) + (:require-macros [app.main.ui.cursors :refer [cursor-ref cursor-fn collect-cursors]])) ;; Static cursors (def ^:cursor comments (cursor-ref :comments 0 2 20)) @@ -53,28 +49,3 @@ (def default "A collection of all icons" (collect-cursors)) - -(mf/defc debug-preview - {::mf/wrap-props false} - [] - (let [rotation (mf/use-state 0) - entries (->> (seq (js/Object.entries default)) - (sort-by first))] - - (mf/with-effect [] - (ts/interval 100 #(reset! rotation inc))) - - [:section.debug-icons-preview - (for [[key value] entries] - (let [value (if (fn? value) (value @rotation) value)] - [:div.cursor-item {:key key} - [:div {:style {:width "100px" - :height "100px" - :background-image (-> value (str/replace #"(url\(.*\)).*" "$1")) - :background-size "contain" - :background-repeat "no-repeat" - :background-position "center" - :cursor value}}] - - [:span {:style {:white-space "nowrap" - :margin-right "1rem"}} (pr-str key)]]))])) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 29c41f8e92f081e741dadc4dce7902423821bc86..78c9902a99d8e2f985c3a5acc7a13d925daefd34 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -52,12 +52,15 @@ (assoc :project-id (uuid project-id))))) (mf/defc dashboard-content - [{:keys [team projects project section search-term profile] :as props}] + [{:keys [team projects project section search-term profile invite-email] :as props}] (let [container (mf/use-ref) content-width (mf/use-state 0) project-id (:id project) team-id (:id team) + dashboard-local (mf/deref refs/dashboard-local) + file-menu-open? (:menu-open dashboard-local) + default-project-id (mf/with-memo [projects] (->> (vals projects) @@ -83,6 +86,7 @@ [:div {:class (stl/css :dashboard-content) + :style {:pointer-events (when file-menu-open? "none")} :on-click clear-selected-fn :ref container} (case section :dashboard-projects @@ -125,7 +129,7 @@ [:& libraries-page {:team team}] :dashboard-team-members - [:& team-members-page {:team team :profile profile}] + [:& team-members-page {:team team :profile profile :invite-email invite-email}] :dashboard-team-invitations [:& team-invitations-page {:team team}] @@ -149,6 +153,7 @@ project-id (:project-id params) team-id (:team-id params) search-term (:search-term params) + invite-email (-> route :query-params :invite-email) teams (mf/deref refs/teams) team (get teams team-id) @@ -200,5 +205,6 @@ :project project :section section :search-term search-term - :team team}])])]])) + :team team + :invite-email invite-email}])])]])) diff --git a/frontend/src/app/main/ui/dashboard.scss b/frontend/src/app/main/ui/dashboard.scss index 064694ec97cdd66fae0c6217591c541b06c3feb1..26d4f051a866565d92d7945a2445d6314b451e80 100644 --- a/frontend/src/app/main/ui/dashboard.scss +++ b/frontend/src/app/main/ui/dashboard.scss @@ -7,16 +7,12 @@ @use "refactor/common-refactor.scss" as *; .dashboard { + @extend .new-scrollbar; background-color: var(--app-background); display: grid; grid-template-columns: $s-40 $s-256 1fr; grid-template-rows: $s-52 1fr; height: 100vh; - - :global(svg#loader-pencil) { - fill: $df-secondary; - width: $s-32; - } } .dashboard-content { diff --git a/frontend/src/app/main/ui/dashboard/change_owner.cljs b/frontend/src/app/main/ui/dashboard/change_owner.cljs index 0af37045dc8c5ce0143a7a83b756a78dce9594f0..d87056e00d839dd32efb9d782ba768894ccfe03b 100644 --- a/frontend/src/app/main/ui/dashboard/change_owner.cljs +++ b/frontend/src/app/main/ui/dashboard/change_owner.cljs @@ -7,32 +7,34 @@ (ns app.main.ui.dashboard.change-owner (:require-macros [app.main.style :as stl]) (:require - [app.common.spec :as us] + [app.common.schema :as sm] [app.main.data.modal :as modal] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [tr]] - [cljs.spec.alpha :as s] [rumext.v2 :as mf])) -(s/def ::member-id ::us/uuid) -(s/def ::leave-modal-form - (s/keys :req-un [::member-id])) +(def ^:private schema:leave-modal-form + [:map {:title "LeaveModalForm"} + [:member-id ::sm/uuid]]) (mf/defc leave-and-reassign-modal {::mf/register modal/components ::mf/register-as :leave-and-reassign} [{:keys [profile team accept]}] - (let [form (fm/use-form :spec ::leave-modal-form :initial {}) + (let [form (fm/use-form :schema schema:leave-modal-form :initial {}) members-map (mf/deref refs/dashboard-team-members) members (vals members-map) - options (into [{:value "" - :label (tr "modals.leave-and-reassign.select-member-to-promote")}] - (filter #(not= (:label %) (:fullname profile)) - (map #(hash-map :label (:name %) :value (str (:id %))) members))) + options + (into [{:value "" + :label (tr "modals.leave-and-reassign.select-member-to-promote")}] + (comp + (filter #(not= (:email %) (:email profile))) + (map #(hash-map :label (:name %) :value (str (:id %))))) + members) on-cancel #(st/emit! (modal/hide)) on-accept diff --git a/frontend/src/app/main/ui/dashboard/change_owner.scss b/frontend/src/app/main/ui/dashboard/change_owner.scss index 0b150c1c5e3a4a2fe588f5dfb3894cc8e4ca6aab..0e960020e0e3297b8e47f1e50871f2aaf81d91d4 100644 --- a/frontend/src/app/main/ui/dashboard/change_owner.scss +++ b/frontend/src/app/main/ui/dashboard/change_owner.scss @@ -34,6 +34,7 @@ .input-wrapper { @extend .input-with-label; + @include bodySmallTypography; } .action-buttons { diff --git a/frontend/src/app/main/ui/dashboard/comments.cljs b/frontend/src/app/main/ui/dashboard/comments.cljs index f200d98f9348a8c258cf215294235c2f73d950c1..7a34437cc3bf3ddc25ca172c4718c4beaa1548e8 100644 --- a/frontend/src/app/main/ui/dashboard/comments.cljs +++ b/frontend/src/app/main/ui/dashboard/comments.cljs @@ -54,7 +54,7 @@ [:button {:tab-index "0" :on-click on-show-comments :on-key-down handle-keydown - :data-test "open-comments" + :data-testid "open-comments" :class (stl/css-case :comment-button true :open show? :unread (boolean (seq tgroups)))} diff --git a/frontend/src/app/main/ui/dashboard/comments.scss b/frontend/src/app/main/ui/dashboard/comments.scss index 1be90e32b46f36ca929431b05debc46cca31327f..af55f6dd17cc97f8d42e5a56f528169cb2c0d61f 100644 --- a/frontend/src/app/main/ui/dashboard/comments.scss +++ b/frontend/src/app/main/ui/dashboard/comments.scss @@ -32,7 +32,7 @@ font-size: $fs-12; padding: $s-24; text-align: center; - color: $df-secondary; + color: var(--color-foreground-secondary); } .comments-icon { @@ -57,7 +57,7 @@ } &:hover { - background-color: $db-quaternary; + background-color: var(--color-background-quaternary); --comment-icon-small-foreground-color: var(--icon-foreground-active); } } @@ -69,7 +69,7 @@ .dropdown { @include menuShadow; - background-color: $db-tertiary; + background-color: var(--color-background-tertiary); border-radius: $br-8; border: $s-1 solid transparent; bottom: $s-4; @@ -78,11 +78,11 @@ min-height: $s-200; position: absolute; width: 100%; - z-index: $z-index-3; + z-index: $z-index-4; hr { margin: 0; - border-color: $df-secondary; + border-color: var(--color-foreground-secondary); } } @@ -94,7 +94,7 @@ } .header-title { - color: $df-secondary; + color: var(--color-foreground-secondary); font-size: $fs-11; line-height: 1.28; flex-grow: 1; diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 4be78a0e2b5aa98d14539f2f5465763f70e6b691..8d6e01f7bc1ebb17ddba296916592d6f4d2bae7f 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -9,8 +9,8 @@ [app.main.data.common :as dcm] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] @@ -88,12 +88,12 @@ on-duplicate (fn [_] (apply st/emit! (map dd/duplicate-file files)) - (st/emit! (msg/success (tr "dashboard.success-duplicate-file" (i18n/c (count files)))))) + (st/emit! (ntf/success (tr "dashboard.success-duplicate-file" (i18n/c (count files)))))) on-delete-accept (fn [_] (apply st/emit! (map dd/delete-file files)) - (st/emit! (msg/success (tr "dashboard.success-delete-file" (i18n/c (count files)))) + (st/emit! (ntf/success (tr "dashboard.success-delete-file" (i18n/c (count files)))) (dd/clear-selected-files))) on-delete @@ -126,8 +126,8 @@ on-move-success (fn [team-id project-id] (if multi? - (st/emit! (msg/success (tr "dashboard.success-move-files"))) - (st/emit! (msg/success (tr "dashboard.success-move-file")))) + (st/emit! (ntf/success (tr "dashboard.success-move-files"))) + (st/emit! (ntf/success (tr "dashboard.success-move-file")))) (if (or navigate? (not= team-id current-team-id)) (st/emit! (dd/go-to-files team-id project-id)) (st/emit! (dd/fetch-recent-files team-id) @@ -240,12 +240,12 @@ [{:option-name (tr "dashboard.duplicate-multi" file-count) :id "file-duplicate-multi" :option-handler on-duplicate - :data-test "duplicate-multi"} + :data-testid "duplicate-multi"} (when (or (seq current-projects) (seq other-teams)) {:option-name (tr "dashboard.move-to-multi" file-count) :id "file-move-multi" :sub-options sub-options - :data-test "move-to-multi"}) + :data-testid "move-to-multi"}) {:option-name (tr "dashboard.export-binary-multi" file-count) :id "file-binari-export-multi" :option-handler on-export-binary-files} @@ -256,13 +256,13 @@ {:option-name (tr "labels.unpublish-multi-files" file-count) :id "file-unpublish-multi" :option-handler on-del-shared - :data-test "file-del-shared"}) + :data-testid "file-del-shared"}) (when (not is-lib-page?) {:option-name :separator} {:option-name (tr "labels.delete-multi-files" file-count) :id "file-delete-multi" :option-handler on-delete - :data-test "delete-multi-files"})] + :data-testid "delete-multi-files"})] [{:option-name (tr "dashboard.open-in-new-tab") :id "file-open-new-tab" @@ -271,42 +271,42 @@ {:option-name (tr "labels.rename") :id "file-rename" :option-handler on-edit - :data-test "file-rename"}) + :data-testid "file-rename"}) (when (not is-search-page?) {:option-name (tr "dashboard.duplicate") :id "file-duplicate" :option-handler on-duplicate - :data-test "file-duplicate"}) + :data-testid "file-duplicate"}) (when (and (not is-lib-page?) (not is-search-page?) (or (seq current-projects) (seq other-teams))) {:option-name (tr "dashboard.move-to") :id "file-move-to" :sub-options sub-options - :data-test "file-move-to"}) + :data-testid "file-move-to"}) (when (not is-search-page?) (if (:is-shared file) {:option-name (tr "dashboard.unpublish-shared") :id "file-del-shared" :option-handler on-del-shared - :data-test "file-del-shared"} + :data-testid "file-del-shared"} {:option-name (tr "dashboard.add-shared") :id "file-add-shared" :option-handler on-add-shared - :data-test "file-add-shared"})) + :data-testid "file-add-shared"})) {:option-name :separator} {:option-name (tr "dashboard.download-binary-file") :id "file-download-binary" :option-handler on-export-binary-files - :data-test "download-binary-file"} + :data-testid "download-binary-file"} {:option-name (tr "dashboard.download-standard-file") :id "file-download-standard" :option-handler on-export-standard-files - :data-test "download-standard-file"} + :data-testid "download-standard-file"} (when (and (not is-lib-page?) (not is-search-page?)) {:option-name :separator} {:option-name (tr "labels.delete") :id "file-delete" :option-handler on-delete - :data-test "file-delete"})])] + :data-testid "file-delete"})])] [:& context-menu-a11y {:on-close on-menu-close :show show? diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index afee2564dcdcacd15b5cf9b695628fbc0bf6a6e2..e533a6b8578957fcd8adb836dcf97b9fced2116d 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -66,7 +66,7 @@ (dd/clear-selected-files))))] - [:header {:class (stl/css :dashboard-header)} + [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} (if (:is-default project) [:div#dashboard-drafts-title {:class (stl/css :dashboard-title)} [:h1 (tr "labels.drafts")]] @@ -82,7 +82,7 @@ (swap! local assoc :edition false)))}] [:div {:class (stl/css :dashboard-title)} [:h1 {:on-double-click on-edit - :data-test "project-title" + :data-testid "project-title" :id (:id project)} (:name project)]])) @@ -98,7 +98,7 @@ [:a {:class (stl/css :btn-secondary :btn-small :new-file) :tab-index "0" :on-click on-create-click - :data-test "new-file" + :data-testid "new-file" :on-key-down (fn [event] (when (kbd/enter? event) (on-create-click event)))} diff --git a/frontend/src/app/main/ui/dashboard/files.scss b/frontend/src/app/main/ui/dashboard/files.scss index 98cf1733e60f4dd9dee512ffdf1aee7cf8694453..7c37cd57c48a3eaa52e36d7e3179835d403e7288 100644 --- a/frontend/src/app/main/ui/dashboard/files.scss +++ b/frontend/src/app/main/ui/dashboard/files.scss @@ -12,7 +12,7 @@ margin-right: $s-16; overflow-y: auto; width: 100%; - border-top: $s-1 solid $db-quaternary; + border-top: $s-1 solid var(--color-background-quaternary); &.dashboard-projects { user-select: none; diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index 17aa4a8d4a976963cd20d71185324da6a851f558..519599243dc5af4dc6adbb1c700c24ed3c8a8d19 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -47,7 +47,7 @@ ::mf/private true} [{:keys [section team]}] (use-page-title team section) - [:header {:class (stl/css :dashboard-header)} + [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} [:div#dashboard-fonts-title {:class (stl/css :dashboard-title)} [:h1 (tr "labels.fonts")]]]) @@ -161,13 +161,13 @@ (mf/use-fn (mf/deps fonts) (fn [_] - (run! on-delete (vals fonts))))] + (run! #(swap! fonts* dissoc (:id %)) (vals fonts))))] [:div {:class (stl/css :dashboard-fonts-upload)} [:div {:class (stl/css :dashboard-fonts-hero)} [:div {:class (stl/css :desc)} [:h2 (tr "labels.upload-custom-fonts")] - [:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}] + [:> i18n/tr-html* {:content (tr "dashboard.fonts.hero-text1")}] [:button {:class (stl/css :btn-primary) :on-click on-click @@ -180,12 +180,12 @@ :on-selected on-selected}]] [:& context-notification {:content (tr "dashboard.fonts.hero-text2") - :type :default + :level :default :is-html true}] (when problematic-fonts? [:& context-notification {:content (tr "dashboard.fonts.warning-text") - :type :warning + :level :warning :is-html true}])]] [:* @@ -197,12 +197,12 @@ :btn-primary true :disabled disable-upload-all?) :on-click on-upload-all - :data-test "upload-all" + :data-testid "upload-all" :disabled disable-upload-all?} [:span (tr "dashboard.fonts.upload-all")]] [:button {:class (stl/css :btn-secondary) :on-click on-dismis-all - :data-test "dismiss-all"} + :data-testid "dismiss-all"} [:span (tr "dashboard.fonts.dismiss-all")]]]]) (for [{:keys [id] :as item} (sort-by :font-family font-vals)] diff --git a/frontend/src/app/main/ui/dashboard/fonts.scss b/frontend/src/app/main/ui/dashboard/fonts.scss index e520e01a40b3ac3a81a80eaae467f5bce689d226..fd40fc50dbbd24b174e98f1fbdf3b0db0a489e3e 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.scss +++ b/frontend/src/app/main/ui/dashboard/fonts.scss @@ -8,7 +8,7 @@ @use "common/refactor/common-dashboard"; .dashboard-fonts { - border-top: $s-1 solid $db-quaternary; + border-top: $s-1 solid var(--color-background-quaternary); display: flex; flex-direction: column; padding-left: $s-120; @@ -31,18 +31,18 @@ h3 { font-size: $fs-14; - color: $df-secondary; + color: var(--color-foreground-secondary); margin: $s-4; } .font-item { - color: $db-secondary; + color: var(--color-background-secondary); } } .installed-fonts-header { align-items: center; - color: $df-secondary; + color: var(--color-foreground-secondary); display: flex; font-size: $fs-12; height: $s-40; @@ -65,11 +65,11 @@ justify-content: flex-end; input { - background-color: $db-tertiary; + background-color: var(--color-background-tertiary); border-color: transparent; border-radius: $br-8; border: $s-1 solid transparent; - color: $df-primary; + color: var(--color-foreground-primary); font-size: $fs-14; height: $s-32; margin: 0; @@ -77,19 +77,19 @@ width: $s-152; &:focus { - outline: $s-1 solid $da-primary; + outline: $s-1 solid var(--color-accent-primary); } &::placeholder { - color: $df-secondary; + color: var(--color-foreground-secondary); } } } .font-item { align-items: center; - background-color: $db-tertiary; + background-color: var(--color-background-tertiary); border-radius: $br-4; - color: $df-secondary; + color: var(--color-foreground-secondary); display: flex; font-size: $fs-14; justify-content: space-between; @@ -103,13 +103,13 @@ margin: 0; padding: $s-8; - background-color: $db-tertiary; + background-color: var(--color-background-tertiary); border-radius: $br-8; - color: $df-primary; + color: var(--color-foreground-primary); font-size: $fs-14; &:focus { - outline: $s-1 solid $da-primary; + outline: $s-1 solid var(--color-accent-primary); } } @@ -152,16 +152,16 @@ &:hover { .icon svg { - stroke: $df-secondary; + stroke: var(--color-foreground-secondary); } } } } .table-field { - color: $df-primary; + color: var(--color-foreground-primary); .variant { - background-color: $db-quaternary; + background-color: var(--color-background-quaternary); border-radius: $br-8; margin-right: $s-4; padding-right: $s-4; @@ -189,7 +189,7 @@ svg { width: $s-16; height: $s-16; - stroke: $df-secondary; + stroke: var(--color-foreground-secondary); fill: none; } @@ -204,7 +204,7 @@ background: none; border: none; svg { - stroke: $df-secondary; + stroke: var(--color-foreground-secondary); } } } @@ -242,15 +242,15 @@ display: flex; flex-direction: column; gap: $s-24; - color: $db-secondary; + color: var(--color-background-secondary); width: $s-500; h2 { - color: $df-primary; + color: var(--color-foreground-primary); font-weight: 400; } p { - color: $df-secondary; + color: var(--color-foreground-secondary); font-size: $fs-16; } } @@ -263,7 +263,7 @@ .fonts-placeholder { align-items: center; border-radius: $br-8; - border: $s-1 solid $db-quaternary; + border: $s-1 solid var(--color-background-quaternary); display: flex; flex-direction: column; height: $s-160; @@ -273,14 +273,14 @@ width: 100%; .icon svg { - stroke: $df-secondary; + stroke: var(--color-foreground-secondary); fill: none; width: $s-32; height: $s-32; } .label { - color: $df-secondary; + color: var(--color-foreground-secondary); font-size: $fs-14; } } diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index bb2f44b2b70ae7af05d94cf4295efbe103e36445..15245d39c5123faf56c746aba09744f6b606191d 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -11,13 +11,14 @@ [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.logging :as log] + [app.config :as cf] [app.main.data.dashboard :as dd] - [app.main.data.messages :as msg] + [app.main.data.notifications :as ntf] [app.main.features :as features] [app.main.fonts :as fonts] [app.main.rasterizer :as thr] [app.main.refs :as refs] - [app.main.render :refer [component-svg]] + [app.main.render :as render] [app.main.repo :as rp] [app.main.store :as st] [app.main.ui.components.color-bullet :as bc] @@ -25,6 +26,7 @@ [app.main.ui.dashboard.import :refer [use-import-file]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]] + [app.main.ui.ds.product.loader :refer [loader*]] [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.main.worker :as wrk] @@ -47,10 +49,9 @@ [file-id revn blob] (let [params {:file-id file-id :revn revn :media blob}] (->> (rp/cmd! :create-file-thumbnail params) - (rx/map :uri)))) + (rx/map :id)))) -(defn- ask-for-thumbnail - "Creates some hooks to handle the files thumbnails cache" +(defn render-thumbnail [file-id revn] (->> (wrk/ask! {:cmd :thumbnails/generate-for-file :revn revn @@ -61,21 +62,26 @@ (rx/map (fn [styles] (assoc result :styles styles - :width 252)))))) + :width 252)))))))) + +(defn- ask-for-thumbnail + "Creates some hooks to handle the files thumbnails cache" + [file-id revn] + (->> (render-thumbnail file-id revn) (rx/mapcat thr/render) (rx/mapcat (partial persist-thumbnail file-id revn)))) (mf/defc grid-item-thumbnail {::mf/wrap-props false} - [{:keys [file-id revn thumbnail-uri background-color]}] + [{:keys [file-id revn thumbnail-id background-color]}] (let [container (mf/use-ref) visible? (h/use-visible container :once? true)] - (mf/with-effect [file-id revn visible? thumbnail-uri] - (when (and visible? (not thumbnail-uri)) + (mf/with-effect [file-id revn visible? thumbnail-id] + (when (and visible? (not thumbnail-id)) (->> (ask-for-thumbnail file-id revn) - (rx/subs! (fn [url] - (st/emit! (dd/set-file-thumbnail file-id url))) + (rx/subs! (fn [thumbnail-id] + (st/emit! (dd/set-file-thumbnail file-id thumbnail-id))) (fn [cause] (log/error :hint "unable to render thumbnail" :file-if file-id @@ -86,12 +92,14 @@ :style {:background-color background-color} :ref container} (when visible? - (if thumbnail-uri + (if thumbnail-id [:img {:class (stl/css :grid-item-thumbnail-image) - :src thumbnail-uri + :src (cf/resolve-media thumbnail-id) :loading "lazy" :decoding "async"}] - i/loader-pencil))])) + [:> loader* {:class (stl/css :grid-loader) + :overlay true + :title (tr "labels.loading")}]))])) ;; --- Grid Item Library @@ -109,7 +117,9 @@ [:div {:class (stl/css :grid-item-th :library)} (if (nil? file) - i/loader-pencil + [:> loader* {:class (stl/css :grid-loader) + :overlay true + :title (tr "labels.loading")}] (let [summary (:library-summary file) components (:components summary) colors (:colors summary) @@ -141,8 +151,8 @@ (let [root-id (or (:main-instance-id component) (:id component))] ;; Check for components-v2 in library [:div {:class (stl/css :asset-list-item) :key (str "assets-component-" (:id component))} - [:& component-svg {:root-shape (get-in component [:objects root-id]) - :objects (:objects component)}] ;; Components in the summary come loaded with objects, even in v2 + [:& render/component-svg {:root-shape (get-in component [:objects root-id]) + :objects (:objects component)}] ;; Components in the summary come loaded with objects, even in v2 [:div {:class (stl/css :name-block)} [:span {:class (stl/css :item-name) :title (:name component)} @@ -361,7 +371,7 @@ [:& grid-item-thumbnail {:file-id (:id file) :revn (:revn file) - :thumbnail-uri (:thumbnail-uri file) + :thumbnail-id (:thumbnail-id file) :background-color (dm/get-in file [:data :options :background])}]) (when (and (:is-shared file) (not library-view?)) @@ -388,15 +398,18 @@ (on-menu-click event)))} menu-icon (when (and selected? file-menu-open?) - [:& file-menu {:files (vals selected-files) - :show? (:menu-open dashboard-local) - :left (+ 24 (:x (:menu-pos dashboard-local))) - :top (:y (:menu-pos dashboard-local)) - :navigate? true - :on-edit on-edit - :on-menu-close on-menu-close - :origin origin - :parent-id (str file-id "-action-menu")}])]]]]])) + ;; When the menu is open we disable events in the dashboard. We need to force pointer events + ;; so the menu can be handled + [:div {:style {:pointer-events "all"}} + [:& file-menu {:files (vals selected-files) + :show? (:menu-open dashboard-local) + :left (+ 24 (:x (:menu-pos dashboard-local))) + :top (:y (:menu-pos dashboard-local)) + :navigate? true + :on-edit on-edit + :on-menu-close on-menu-close + :origin origin + :parent-id (str file-id "-action-menu")}]])]]]]])) (mf/defc grid [{:keys [files project origin limit library-view? create-fn] :as props}] @@ -416,8 +429,9 @@ on-drag-enter (mf/use-fn (fn [e] - (when (or (dnd/has-type? e "Files") - (dnd/has-type? e "application/x-moz-file")) + (when (and (not (dnd/has-type? e "penpot/files")) + (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file"))) (dom/prevent-default e) (reset! dragging? true)))) @@ -437,8 +451,9 @@ on-drop (mf/use-fn (fn [e] - (when (or (dnd/has-type? e "Files") - (dnd/has-type? e "application/x-moz-file")) + (when (and (not (dnd/has-type? e "penpot/files")) + (or (dnd/has-type? e "Files") + (dnd/has-type? e "application/x-moz-file"))) (dom/prevent-default e) (reset! dragging? false) (import-files (.-files (.-dataTransfer e))))))] @@ -449,7 +464,6 @@ :on-drag-leave on-drag-leave :on-drop on-drop :ref node-ref} - (cond (nil? files) [:& loading-placeholder] @@ -546,7 +560,7 @@ on-drop-success (fn [] - (st/emit! (msg/success (tr "dashboard.success-move-file")) + (st/emit! (ntf/success (tr "dashboard.success-move-file")) (dd/fetch-recent-files (:id team)) (dd/clear-selected-files))) diff --git a/frontend/src/app/main/ui/dashboard/grid.scss b/frontend/src/app/main/ui/dashboard/grid.scss index 3108fa6567208a6e287b2d0984a70a34f5cc5fdb..fe58c48252fdd42ecd17a4c044f596105e6a1cae 100644 --- a/frontend/src/app/main/ui/dashboard/grid.scss +++ b/frontend/src/app/main/ui/dashboard/grid.scss @@ -6,6 +6,9 @@ @import "refactor/common-refactor.scss"; +// TODO: Legacy sass variables. We should remove them in favor of DS tokens. +$bp-max-1366: "(max-width: 1366px)"; + $thumbnail-default-width: $s-252; // Default width $thumbnail-default-height: $s-168; // Default width @@ -18,7 +21,9 @@ $thumbnail-default-height: $s-168; // Default width } .grid-row { - display: flex; + display: grid; + grid-auto-flow: column; + grid-auto-columns: calc($s-12 + var(--th-width, #{$thumbnail-default-width})); width: 100%; gap: $s-24; } @@ -58,7 +63,7 @@ $thumbnail-default-height: $s-168; // Default width &.dragged { border-radius: $br-4; - outline: $br-4 solid $da-primary; + outline: $br-4 solid var(--color-accent-primary); text-align: initial; width: calc(var(--th-width) + $s-12); height: var(--th-height, #{$thumbnail-default-height}); @@ -66,7 +71,7 @@ $thumbnail-default-height: $s-168; // Default width &.overlay { border-radius: $br-4; - border: $s-2 solid $da-tertiary; + border: $s-2 solid var(--color-accent-tertiary); height: 100%; opacity: 0; pointer-events: none; @@ -96,7 +101,7 @@ $thumbnail-default-height: $s-168; // Default width h3 { border: $s-1 solid transparent; - color: $df-primary; + color: var(--color-foreground-primary); font-size: $fs-16; font-weight: $fw400; height: $s-28; @@ -115,7 +120,7 @@ $thumbnail-default-height: $s-168; // Default width } .date { - color: $df-secondary; + color: var(--color-foreground-secondary); overflow: hidden; text-overflow: ellipsis; width: 100%; @@ -131,7 +136,7 @@ $thumbnail-default-height: $s-168; // Default width } .item-badge { - background-color: $da-primary; + background-color: var(--color-accent-primary); border: none; border-radius: $br-6; position: absolute; @@ -144,7 +149,7 @@ $thumbnail-default-height: $s-168; // Default width justify-content: center; svg { - stroke: $db-secondary; + stroke: var(--color-background-secondary); fill: none; height: $s-16; width: $s-16; @@ -152,18 +157,18 @@ $thumbnail-default-height: $s-168; // Default width } &.add-file { - border: $s-1 dashed $df-secondary; + border: $s-1 dashed var(--color-foreground-secondary); justify-content: center; box-shadow: none; span { - color: $db-primary; + color: var(--color-background-primary); font-size: $fs-14; } &:hover { - background-color: $df-primary; - border: $s-2 solid $da-tertiary; + background-color: var(--color-foreground-primary); + border: $s-2 solid var(--color-accent-tertiary); } } } @@ -174,9 +179,9 @@ $thumbnail-default-height: $s-168; // Default width left: $s-4; width: $s-32; height: $s-32; - background-color: $da-tertiary; + background-color: var(--color-accent-tertiary); border-radius: $br-circle; - color: $db-secondary; + color: var(--color-background-secondary); font-size: $fs-16; display: flex; justify-content: center; @@ -192,7 +197,7 @@ $thumbnail-default-height: $s-168; // Default width &:hover, &:focus, &:focus-within { - background-color: $db-tertiary; + background-color: var(--color-background-tertiary); .project-th-actions { opacity: 1; } @@ -203,7 +208,7 @@ $thumbnail-default-height: $s-168; // Default width .selected { .grid-item-th { - outline: $s-4 solid $da-tertiary; + outline: $s-4 solid var(--color-accent-tertiary); } } } @@ -218,7 +223,7 @@ $thumbnail-default-height: $s-168; // Default width width: $s-32; span { - color: $db-secondary; + color: var(--color-background-secondary); } } @@ -273,16 +278,6 @@ $thumbnail-default-height: $s-168; // Default width height: auto; width: 100%; } - - svg { - height: 100%; - width: 100%; - } - - :global(svg#loader-pencil) { - stroke: $db-quaternary; - width: calc(var(--th-width, #{$thumbnail-default-width}) * 0.25); - } } // LIBRARY VIEW @@ -295,7 +290,7 @@ $thumbnail-default-height: $s-168; // Default width } .grid-item-th.library { - background-color: $db-tertiary; + background-color: var(--color-background-tertiary); flex-direction: column; height: 90%; justify-content: flex-start; @@ -304,7 +299,7 @@ $thumbnail-default-height: $s-168; // Default width .asset-section { font-size: $fs-12; - color: $df-secondary; + color: var(--color-foreground-secondary); &:not(:first-child) { margin-top: $s-16; @@ -317,7 +312,7 @@ $thumbnail-default-height: $s-168; // Default width text-transform: uppercase; .num-assets { - color: $df-secondary; + color: var(--color-foreground-secondary); } } @@ -325,7 +320,7 @@ $thumbnail-default-height: $s-168; // Default width align-items: center; border-radius: $br-4; border: $s-1 solid transparent; - color: $df-primary; + color: var(--color-foreground-primary); display: flex; font-size: $fs-12; margin-top: $s-4; @@ -333,7 +328,7 @@ $thumbnail-default-height: $s-168; // Default width position: relative; .name-block { - color: $df-secondary; + color: var(--color-foreground-secondary); width: calc(100% - $s-24 - $s-8); } @@ -354,11 +349,11 @@ $thumbnail-default-height: $s-168; // Default width } .color-name { - color: $df-primary; + color: var(--color-foreground-primary); } .color-value { - color: $df-secondary; + color: var(--color-foreground-secondary); margin-left: $s-4; text-transform: uppercase; } @@ -376,3 +371,7 @@ $thumbnail-default-height: $s-168; // Default width grid-template-columns: auto 1fr; gap: $s-8; } + +.grid-loader { + --icon-width: calc(var(--th-width, #{$thumbnail-default-width}) * 0.25); +} diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index d51195184d79dee06729f486dc08650efeb30cd4..9acd8005036441ebf15237e8c39a8128cd86cc08 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -12,12 +12,13 @@ [app.common.logging :as log] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] [app.main.errors :as errors] [app.main.features :as features] [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] + [app.main.ui.ds.product.loader :refer [loader*]] [app.main.ui.icons :as i] [app.main.ui.notifications.context-notification :refer [context-notification]] [app.main.worker :as uw] @@ -266,14 +267,17 @@ :editable (and ready? (not editing?)))} [:div {:class (stl/css :file-name)} - [:div {:class (stl/css-case :file-icon true - :icon-fill ready?)} - (cond loading? i/loader-pencil - ready? i/logo-icon - import-warn? i/msg-warning - import-error? i/close - import-finish? i/tick - analyze-error? i/close)] + (if loading? + [:> loader* {:width 16 + :title (tr "labels.loading")}] + [:div {:class (stl/css-case :file-icon true + :icon-fill ready?)} + (cond ready? i/logo-icon + import-warn? i/msg-warning + import-error? i/close + import-finish? i/tick + analyze-error? i/close)]) + (if editing? [:div {:class (stl/css :file-name-edit)} @@ -345,21 +349,24 @@ edition* (mf/use-state nil) edition (deref edition*) + template-finished* (mf/use-state nil) + template-finished (deref template-finished*) + on-template-cloned-success (mf/use-fn (fn [] - (swap! status* (constantly :importing)) - ;; (swap! state assoc :status :importing :importing-templates 0) + (reset! status* :importing) + (reset! template-finished* true) (st/emit! (dd/fetch-recent-files)))) on-template-cloned-error (mf/use-fn (fn [cause] - (swap! status* (constantly :error)) - ;; (swap! state assoc :status :error :importing-templates 0) + (reset! status* :error) + (reset! template-finished* true) (errors/print-error! cause) (rx/of (modal/hide) - (msg/error (tr "dashboard.libraries-and-templates.import-error"))))) + (ntf/error (tr "dashboard.libraries-and-templates.import-error"))))) continue-entries (mf/use-fn @@ -434,15 +441,29 @@ 1 (count (filterv has-status-success? entries))) - errors? (or (some has-status-error? entries) - (zero? (count entries))) - + errors? (if (some? template) + (= status :error) + (or (some has-status-error? entries) + (zero? (count entries)))) pending-analysis? (some has-status-analyzing? entries) - pending-import? (pos? num-importing) + pending-import? (and (or (nil? template) + (not template-finished)) + (pos? num-importing)) + valid-all-entries? (or (some? template) - (not (some has-status-analyze-error? entries)))] + (not (some has-status-analyze-error? entries))) + + template-status + (cond + (and (= :importing status) pending-import?) + :importing + + (and (= :importing status) (not ^boolean pending-import?)) + :import-finish + :else + :ready)] ;; Run analyze operation on component mount (mf/with-effect [] @@ -460,19 +481,19 @@ [:div {:class (stl/css :modal-content)} (when (and (= :analyzing status) errors?) [:& context-notification - {:type :warning + {:level :warning :content (tr "dashboard.import.import-warning")}]) (when (and (= :importing status) (not ^boolean pending-import?)) (cond errors? [:& context-notification - {:type :warning + {:level :warning :content (tr "dashboard.import.import-warning")}] :else [:& context-notification - {:type :success + {:level (if (zero? success-num) :warning :success) :content (tr "dashboard.import.import-message" (i18n/c success-num))}])) (for [entry entries] @@ -486,7 +507,7 @@ :can-be-deleted (> (count entries) 1)}]) (when (some? template) - [:& import-entry {:entry (assoc template :status :ready) + [:& import-entry {:entry (assoc template :status template-status) :can-be-deleted false}])] [:div {:class (stl/css :modal-footer)} diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index a89dda6da13aec8c106b1cd6a8b7e348bccee279..50083f3df551c985e5cb6636738cfd169ab0b706 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -12,6 +12,8 @@ .modal-container { @extend .modal-container-base; + display: flex; + flex-direction: column; } .modal-header { @@ -29,6 +31,9 @@ .modal-content { @include bodySmallTypography; + flex: 1; + overflow-y: auto; + overflow-x: hidden; display: grid; grid-template-columns: 1fr; gap: $s-16; @@ -76,6 +81,7 @@ } .file-name-edit { @extend .input-element; + @include bodySmallTypography; flex-grow: 1; } .file-name-label { @@ -137,13 +143,6 @@ &.loading { .file-name { color: var(--modal-text-foreground-color); - .file-icon { - :global(#loader-pencil) { - color: var(--modal-text-foreground-color); - stroke: var(--modal-text-foreground-color); - fill: var(--modal-text-foreground-color); - } - } } } &.warning { diff --git a/frontend/src/app/main/ui/dashboard/inline_edition.scss b/frontend/src/app/main/ui/dashboard/inline_edition.scss index b2d0276cd508bb918ec65e07ec310ebd682d6dfc..4b4a17eb1c84b11ca50e94a5d26dc787c86f953c 100644 --- a/frontend/src/app/main/ui/dashboard/inline_edition.scss +++ b/frontend/src/app/main/ui/dashboard/inline_edition.scss @@ -17,7 +17,7 @@ input.element-title { background-color: var(--input-background-color-active); border-radius: $br-8; - color: $df-primary; + color: var(--color-foreground-primary); font-size: $fs-16; height: $s-32; margin: 0; @@ -26,7 +26,7 @@ input.element-title { width: 100%; &:focus-visible { - border: $s-1 solid $da-primary; + border: $s-1 solid var(--color-accent-primary); outline: none; } } @@ -39,7 +39,7 @@ input.element-title { right: calc(-1 * $s-8); svg { - fill: $df-secondary; + fill: var(--color-foreground-secondary); height: $s-16; transform: rotate(45deg) translateY(7px); width: $s-16; diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index dd543c154ead03926837bb7bfb75139f9a522b5b..78238721e96b8445f843c4c96f2922db82581cca 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -48,7 +48,7 @@ (dd/clear-selected-files))) [:* - [:header {:class (stl/css :dashboard-header)} + [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} [:div#dashboard-libraries-title {:class (stl/css :dashboard-title)} [:h1 (tr "dashboard.libraries-title")]]] [:section {:class (stl/css :dashboard-container :no-bg :dashboard-shared) :ref rowref} diff --git a/frontend/src/app/main/ui/dashboard/libraries.scss b/frontend/src/app/main/ui/dashboard/libraries.scss index 69660e4f0a4f0c966c028ae159bfd6565b448ac9..5a79d8e33ffc5fdad82169f2f1cb51cac376592e 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.scss +++ b/frontend/src/app/main/ui/dashboard/libraries.scss @@ -12,7 +12,7 @@ margin-right: $s-16; overflow-y: auto; width: 100%; - border-top: $s-1 solid $db-quaternary; + border-top: $s-1 solid var(--color-background-quaternary); &.dashboard-projects { user-select: none; diff --git a/frontend/src/app/main/ui/dashboard/placeholder.cljs b/frontend/src/app/main/ui/dashboard/placeholder.cljs index 8f5daa04e95eda976a8af5f01f56e496b2f56609..261fe3c4fe0ab73f4e9f3b2eca5907ca82150794 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.cljs +++ b/frontend/src/app/main/ui/dashboard/placeholder.cljs @@ -7,12 +7,13 @@ (ns app.main.ui.dashboard.placeholder (:require-macros [app.main.style :as stl]) (:require + [app.main.ui.ds.product.loader :refer [loader*]] [app.main.ui.icons :as i] [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) (mf/defc empty-placeholder - [{:keys [dragging? limit origin create-fn] :as props}] + [{:keys [dragging? limit origin create-fn]}] (let [on-click (mf/use-fn (mf/deps create-fn) @@ -27,9 +28,9 @@ (= :libraries origin) [:div {:class (stl/css :grid-empty-placeholder :libs) - :data-test "empty-placeholder"} + :data-testid "empty-placeholder"} [:div {:class (stl/css :text)} - [:& i18n/tr-html {:label "dashboard.empty-placeholder-drafts"}]]] + [:> i18n/tr-html* {:content (tr "dashboard.empty-placeholder-drafts")}]]] :else [:div @@ -40,6 +41,7 @@ (mf/defc loading-placeholder [] - [:div {:class (stl/css :grid-empty-placeholder :loader)} - [:div {:class (stl/css :icon)} i/loader] - [:div {:class (stl/css :text)} (tr "dashboard.loading-files")]]) + [:> loader* {:width 32 + :title (tr "labels.loading") + :class (stl/css :placeholder-loader)} + [:span {:class (stl/css :placeholder-text)} (tr "dashboard.loading-files")]]) diff --git a/frontend/src/app/main/ui/dashboard/placeholder.scss b/frontend/src/app/main/ui/dashboard/placeholder.scss index 6f05ba0006ad2fe5cf261b8e5b8972e0ca8bf249..a72ebc451def95b433c7d1777f3180338d1bb122 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.scss +++ b/frontend/src/app/main/ui/dashboard/placeholder.scss @@ -12,21 +12,6 @@ display: grid; padding: $s-12 0; - &.loader { - justify-items: center; - } - - .icon { - display: flex; - align-items: center; - justify-content: center; - svg { - width: $s-64; - height: $s-64; - fill: $df-secondary; - } - } - &.libs { background-image: url(/images/ph-left.svg), url(/images/ph-right.svg); background-position: @@ -34,7 +19,7 @@ 85% top; background-repeat: no-repeat; align-items: center; - border: $s-1 solid $db-quaternary; + border: $s-1 solid var(--color-background-quaternary); border-radius: $br-4; display: flex; flex-direction: column; @@ -45,7 +30,7 @@ .text { a { - color: $df-primary; + color: var(--color-foreground-primary); } p { max-width: $s-360; @@ -56,9 +41,9 @@ } .create-new { - background-color: $db-tertiary; + background-color: var(--color-background-tertiary); border-radius: $br-8; - color: $df-primary; + color: var(--color-foreground-primary); cursor: pointer; height: $s-160; margin: $s-8; @@ -70,23 +55,37 @@ svg { width: $s-32; height: $s-32; - stroke: $df-secondary; + stroke: var(--color-foreground-secondary); } &:hover { - border: $s-2 solid $da-tertiary; - background-color: $db-quaternary; - color: $da-primary; + border: $s-2 solid var(--color-accent-tertiary); + background-color: var(--color-background-quaternary); + color: var(--color-accent-primary); svg { - stroke: $da-tertiary; + stroke: var(--color-accent-tertiary); } } } .text { margin-top: $s-12; - color: $df-secondary; + color: var(--color-foreground-secondary); font-size: $fs-16; } } + +.placeholder-loader { + display: flex; + flex-direction: column; + justify-content: center; + row-gap: var(--sp-xxxl); + margin: var(--sp-xxxl) 0 var(--sp-m) 0; +} + +.placeholder-text { + color: var(--color-foreground-secondary); + font-size: $fs-16; + text-align: center; +} diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs index bd725a905aee59dc0310ff270a2e69895a4eae0f..a8eb4621dbc2a567dc319bede239d07ef3577abf 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -7,8 +7,8 @@ (ns app.main.ui.dashboard.project-menu (:require [app.main.data.dashboard :as dd] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]] @@ -30,7 +30,7 @@ on-duplicate-success (fn [new-project] - (st/emit! (msg/success (tr "dashboard.success-duplicate-project")) + (st/emit! (ntf/success (tr "dashboard.success-duplicate-project")) (rt/nav :dashboard-files {:team-id (:team-id new-project) :project-id (:id new-project)}))) @@ -51,12 +51,12 @@ (fn [team-id] (let [data {:id (:id project) :team-id team-id} mdata {:on-success #(on-move-success team-id)}] - #(st/emit! (msg/success (tr "dashboard.success-move-project")) + #(st/emit! (ntf/success (tr "dashboard.success-move-project")) (dd/move-project (with-meta data mdata))))) delete-fn (fn [_] - (st/emit! (msg/success (tr "dashboard.success-delete-project")) + (st/emit! (ntf/success (tr "dashboard.success-delete-project")) (dd/delete-project project) (dd/go-to-projects (:team-id project)))) @@ -85,12 +85,12 @@ {:option-name (tr "labels.rename") :id "project-menu-rename" :option-handler on-edit - :data-test "project-rename"}) + :data-testid "project-rename"}) (when-not (:is-default project) {:option-name (tr "dashboard.duplicate") :id "project-menu-duplicated" :option-handler on-duplicate - :data-test "project-duplicate"}) + :data-testid "project-duplicate"}) (when-not (:is-default project) {:option-name (tr "dashboard.pin-unpin") :id "project-menu-pin" @@ -103,19 +103,19 @@ {:option-name (:name team) :id (:name team) :option-handler (on-move (:id team))}) - :data-test "project-move-to"}) + :data-testid "project-move-to"}) (when (some? on-import) {:option-name (tr "dashboard.import") :id "project-menu-import" :option-handler on-import-files - :data-test "file-import"}) + :data-testid "file-import"}) (when-not (:is-default project) {:option-name :separator}) (when-not (:is-default project) {:option-name (tr "labels.delete") :id "project-menu-delete" :option-handler on-delete - :data-test "project-delete"})]] + :data-testid "project-delete"})]] [:* [:& udi/import-form {:ref file-input diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 82bbe0378d49e558cc9d21f437b5d4e8dabe71da..deacac12a604aa9b844f8cb73da207f3e698d6aa 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -7,15 +7,11 @@ (ns app.main.ui.dashboard.projects (:require-macros [app.main.style :as stl]) (:require - [app.common.data :as d] [app.common.geom.point :as gpt] - [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.data.users :as du] - [app.main.errors :as errors] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.grid :refer [line-grid]] @@ -40,16 +36,22 @@ (def ^:private close-icon (i/icon-xref :close (stl/css :close-icon))) +(def ^:private add-icon + (i/icon-xref :add (stl/css :add-icon))) + +(def ^:private menu-icon + (i/icon-xref :menu (stl/css :menu-icon))) + (mf/defc header {::mf/wrap [mf/memo]} [] (let [on-click (mf/use-fn #(st/emit! (dd/create-project)))] - [:header {:class (stl/css :dashboard-header)} + [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} [:div#dashboard-projects-title {:class (stl/css :dashboard-title)} [:h1 (tr "dashboard.projects-title")]] [:button {:class (stl/css :btn-secondary :btn-small) :on-click on-click - :data-test "new-project-button"} + :data-testid "new-project-button"} (tr "dashboard.new-project")]])) (mf/defc team-hero @@ -94,80 +96,6 @@ (def builtin-templates (l/derived :builtin-templates st/state)) -(mf/defc tutorial-project - [{:keys [close-tutorial default-project-id] :as props}] - (let [state (mf/use-state {:status :waiting - :file nil}) - - templates (mf/deref builtin-templates) - template (d/seek #(= (:id %) "tutorial-for-beginners") templates) - - on-template-cloned-success - (mf/use-fn - (mf/deps default-project-id) - (fn [response] - (swap! state #(assoc % :status :success :file (:first response))) - (st/emit! (dd/go-to-workspace {:id (first response) :project-id default-project-id :name "tutorial"}) - (du/update-profile-props {:viewed-tutorial? true})))) - - on-template-cloned-error - (mf/use-fn - (fn [cause] - (swap! state assoc :status :error) - (errors/print-error! cause) - (st/emit! (msg/error (tr "dashboard.libraries-and-templates.import-error"))))) - - download-tutorial - (mf/use-fn - (mf/deps template default-project-id) - (fn [] - (let [mdata {:on-success on-template-cloned-success - :on-error on-template-cloned-error} - params {:project-id default-project-id - :template-id (:id template)}] - (swap! state #(assoc % :status :importing)) - (st/emit! (with-meta (dd/clone-template (with-meta params mdata)) - {::ev/origin "get-started-hero-block"})))))] - [:article {:class (stl/css :tutorial)} - [:div {:class (stl/css :thumbnail)}] - [:div {:class (stl/css :text)} - [:h2 {:class (stl/css :title)} (tr "dasboard.tutorial-hero.title")] - [:p {:class (stl/css :info)} (tr "dasboard.tutorial-hero.info")] - [:button {:class (stl/css :btn-primary :action) - :on-click download-tutorial} - (case (:status @state) - :waiting (tr "dasboard.tutorial-hero.start") - :importing [:span.loader i/loader-pencil] - :success "")]] - - [:button {:class (stl/css :close) - :on-click close-tutorial - :aria-label (tr "labels.close")} - close-icon]])) - -(mf/defc interface-walkthrough - {::mf/wrap [mf/memo]} - [{:keys [close-walkthrough] :as props}] - (let [handle-walkthrough-link - (fn [] - (st/emit! (ptk/event ::ev/event {::ev/name "show-walkthrough" - ::ev/origin "get-started-hero-block" - :section "dashboard"})))] - [:article {:class (stl/css :walkthrough)} - [:div {:class (stl/css :thumbnail)}] - [:div {:class (stl/css :text)} - [:h2 {:class (stl/css :title)} (tr "dasboard.walkthrough-hero.title")] - [:p {:class (stl/css :info)} (tr "dasboard.walkthrough-hero.info")] - [:a {:class (stl/css :btn-primary :action) - :href " https://design.penpot.app/walkthrough" - :target "_blank" - :on-click handle-walkthrough-link} - (tr "dasboard.walkthrough-hero.start")]] - [:button {:class (stl/css :close) - :on-click close-walkthrough - :aria-label (tr "labels.close")} - close-icon]])) - (mf/defc project-item [{:keys [project first? team files] :as props}] (let [locale (mf/deref i18n/locale) @@ -276,7 +204,8 @@ (fn [event] (when (kbd/enter? event) (dom/stop-propagation event) - (on-menu-click event))))] + (on-menu-click event)))) + title-width (/ 100 limit)] [:article {:class (stl/css-case :dashboard-project-row true :first first?)} [:header {:class (stl/css :project)} @@ -285,46 +214,54 @@ [:& inline-edition {:content (:name project) :on-end on-edit}] [:h2 {:on-click on-nav + :style {:max-width (str title-width "%")} + :class (stl/css :project-name) + :title (if (:is-default project) + (tr "labels.drafts") + (:name project)) :on-context-menu on-menu-click} (if (:is-default project) (tr "labels.drafts") (:name project))]) - [:& project-menu - {:project project - :show? (:menu-open @local) - :left (+ 24 (:x (:menu-pos @local))) - :top (:y (:menu-pos @local)) - :on-edit on-edit-open - :on-menu-close on-menu-close - :on-import on-import}] - - [:span {:class (stl/css :info)} (str (tr "labels.num-of-files" (i18n/c file-count)))] - - (let [time (-> (:modified-at project) - (dt/timeago {:locale locale}))] - [:span {:class (stl/css :recent-files-row-title-info)} (str ", " time)]) - - [:div {:class (stl/css :project-actions)} - (when-not (:is-default project) - [:> pin-button* {:class (stl/css :pin-button) :is-pinned (:is-pinned project) :on-click toggle-pin :tab-index 0}]) - - [:button {:class (stl/css :btn-secondary :btn-small :tooltip :tooltip-bottom) - :on-click on-create-click - :alt (tr "dashboard.new-file") - :aria-label (tr "dashboard.new-file") - :data-test "project-new-file" - :on-key-down handle-create-click} - i/add] - - [:button - {:class (stl/css :btn-secondary :btn-small :tooltip :tooltip-bottom) - :on-click on-menu-click - :alt (tr "dashboard.options") - :aria-label (tr "dashboard.options") - :data-test "project-options" - :on-key-down handle-menu-click} - i/menu]]]] + [:div {:class (stl/css :info-wrapper)} + [:& project-menu + {:project project + :show? (:menu-open @local) + :left (+ 24 (:x (:menu-pos @local))) + :top (:y (:menu-pos @local)) + :on-edit on-edit-open + :on-menu-close on-menu-close + :on-import on-import}] + + ;; We group these two spans under a div to avoid having extra space between them. + [:div + [:span {:class (stl/css :info)} (str (tr "labels.num-of-files" (i18n/c file-count)))] + + (let [time (-> (:modified-at project) + (dt/timeago {:locale locale}))] + [:span {:class (stl/css :recent-files-row-title-info)} (str ", " time)])] + + [:div {:class (stl/css-case :project-actions true + :pinned-project (:is-pinned project))} + (when-not (:is-default project) + [:> pin-button* {:class (stl/css :pin-button) :is-pinned (:is-pinned project) :on-click toggle-pin :tab-index 0}]) + + [:button {:class (stl/css :add-file-btn) + :on-click on-create-click + :title (tr "dashboard.new-file") + :aria-label (tr "dashboard.new-file") + :data-testid "project-new-file" + :on-key-down handle-create-click} + add-icon] + + [:button {:class (stl/css :options-btn) + :on-click on-menu-click + :title (tr "dashboard.options") + :aria-label (tr "dashboard.options") + :data-testid "project-options" + :on-key-down handle-menu-click} + menu-icon]]]]] [:div {:class (stl/css :grid-container) :ref rowref} [:& line-grid @@ -336,14 +273,13 @@ (when (and (> limit 0) (> file-count limit)) - [:button - {:class (stl/css :show-more) - :on-click on-nav - :tab-index "0" - :on-key-down (fn [event] - (when (kbd/enter? event) - (on-nav)))} - [:div {:class (stl/css :placeholder-label)} (tr "dashboard.show-all-files")] + [:button {:class (stl/css :show-more) + :on-click on-nav + :tab-index "0" + :on-key-down (fn [event] + (when (kbd/enter? event) + (on-nav)))} + [:span {:class (stl/css :placeholder-label)} (tr "dashboard.show-all-files")] show-more-icon])])) @@ -351,7 +287,7 @@ (l/derived :dashboard-recent-files st/state)) (mf/defc projects-section - [{:keys [team projects profile default-project-id] :as props}] + [{:keys [team projects profile] :as props}] (let [projects (->> (vals projects) (sort-by :modified-at) (reverse)) @@ -364,8 +300,7 @@ (:team-hero? props true) (not (:is-default team))) - tutorial-viewed? (:viewed-tutorial? props true) - walkthrough-viewed? (:viewed-walkthrough? props true) + is-my-penpot (= (:default-team-id profile) (:id team)) team-id (:id team) @@ -373,24 +308,10 @@ (mf/use-fn (fn [] (st/emit! (du/update-profile-props {:team-hero? false}) - (ptk/event ::ev/event {::ev/name "dont-show-team-up-hero" - ::ev/origin "dashboard"})))) - close-tutorial - (mf/use-fn - (fn [] - (st/emit! (du/update-profile-props {:viewed-tutorial? true}) - (ptk/event ::ev/event {::ev/name "dont-show" - ::ev/origin "get-started-hero-block" - :type "tutorial" - :section "dashboard"})))) - close-walkthrough - (mf/use-fn - (fn [] - (st/emit! (du/update-profile-props {:viewed-walkthrough? true}) - (ptk/event ::ev/event {::ev/name "dont-show" - ::ev/origin "get-started-hero-block" - :type "walkthrough" - :section "dashboard"}))))] + (ptk/data-event ::ev/event {::ev/name "dont-show-team-up-hero" + ::ev/origin "dashboard"})))) + + show-team-hero? (and (not is-my-penpot) team-hero?)] (mf/with-effect [team] (let [tname (if (:is-default team) @@ -405,31 +326,22 @@ (when (seq projects) [:* [:& header] - - (when team-hero? - [:& team-hero {:team team :close-fn close-banner}]) - - (when (and (contains? cf/flags :dashboard-templates-section) - (or (not tutorial-viewed?) - (not walkthrough-viewed?))) - [:div {:class (stl/css :hero-projects)} - (when (and (not tutorial-viewed?) (:is-default team)) - [:& tutorial-project - {:close-tutorial close-tutorial - :default-project-id default-project-id}]) - - (when (and (not walkthrough-viewed?) (:is-default team)) - [:& interface-walkthrough - {:close-walkthrough close-walkthrough}])]) - - [:div {:class (stl/css :dashboard-container :no-bg :dashboard-projects)} - (for [{:keys [id] :as project} projects] - (let [files (when recent-map - (->> (vals recent-map) - (filterv #(= id (:project-id %))) - (sort-by :modified-at #(compare %2 %1))))] - [:& project-item {:project project - :team team - :files files - :first? (= project (first projects)) - :key id}]))]]))) + [:div {:class (stl/css :projects-container)} + [:* + (when team-hero? + [:& team-hero {:team team :close-fn close-banner}]) + + [:div {:class (stl/css-case :dashboard-container true + :no-bg true + :dashboard-projects true + :with-team-hero show-team-hero?)} + (for [{:keys [id] :as project} projects] + (let [files (when recent-map + (->> (vals recent-map) + (filterv #(= id (:project-id %))) + (sort-by :modified-at #(compare %2 %1))))] + [:& project-item {:project project + :team team + :files files + :first? (= project (first projects)) + :key id}]))]]]]))) diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss index 13204f3d5ab7d818cc36016035c5f15555b8fe9a..e544896eec9e81324de315b88326714e1dfed449 100644 --- a/frontend/src/app/main/ui/dashboard/projects.scss +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -9,128 +9,140 @@ .dashboard-container { flex: 1 0 0; + width: 100%; margin-right: $s-16; + border-top: $s-1 solid var(--panel-border-color); overflow-y: auto; - width: 100%; - border-top: $s-1 solid $db-quaternary; +} - &.dashboard-projects { - user-select: none; - } - &.dashboard-shared { - width: calc(100vw - $s-320); - margin-right: $s-52; - } +.dashboard-projects { + user-select: none; + height: calc(100vh - $s-64); +} - &.search { - margin-top: $s-12; - } +.with-team-hero { + height: calc(100vh - $s-280); +} + +.dashboard-shared { + width: calc(100vw - $s-320); + margin-right: $s-52; +} + +.search { + margin-top: $s-12; } .dashboard-project-row { + --actions-opacity: 0; margin-bottom: $s-24; position: relative; - .project { - align-items: center; - border-radius: $br-4; - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - margin-top: $s-16; - padding: $s-8 $s-8 $s-8 $s-16; - width: 99%; - max-height: $s-40; - gap: $s-8; - - .project-name-wrapper { - display: flex; - align-items: center; - justify-content: flex-start; - min-height: $s-32; - margin-left: $s-8; - } + &:hover, + &:focus, + &:focus-within { + --actions-opacity: 1; + } +} - .btn-secondary { - border: none; - padding: $s-8; - } +.pinned-project { + --actions-opacity: 1; +} - h2 { - cursor: pointer; - font-size: $fs-16; - line-height: 0.8; - font-weight: $fw400; - color: $df-primary; - margin-right: $s-4; - margin-right: $s-12; - } +.projects-container { + display: grid; + grid-auto-rows: min-content; +} - .info, - .recent-files-row-title-info { - font-size: $fs-14; - line-height: 1.15; - font-weight: $fw400; - color: $df-secondary; - @media (max-width: 760px) { - display: none; - } - } +.project { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: $s-8; + width: 99%; + max-height: $s-40; + padding: $s-8 $s-8 $s-8 $s-16; + margin-top: $s-16; + border-radius: $br-4; +} - .project-actions { - display: flex; - opacity: 1; - margin-left: $s-32; - - .btn-small:not(.pin-button) { - height: $s-32; - margin: 0 $s-8; - width: $s-32; - - &:not(:hover) { - background: transparent; - } - svg { - fill: $df-primary; - height: $s-16; - width: $s-16; - } - } - } - } +.project-name-wrapper { + display: flex; + align-items: center; + justify-content: flex-start; + width: 100%; + min-height: $s-32; + margin-left: $s-8; +} - .grid-container { - width: 100%; - padding: 0 $s-4; - } +.project-name { + @include bodyLargeTypography; + @include textEllipsis; + width: fit-content; + margin-right: $s-12; + line-height: 0.8; + color: var(--title-foreground-color-hover); + cursor: pointer; + height: $s-16; +} - &:hover, - &:focus, - &:focus-within { - .project-actions { - opacity: 1; - } +.info-wrapper { + display: flex; + align-items: center; + gap: $s-8; +} + +.info, +.recent-files-row-title-info { + @include bodyMediumTypography; + color: var(--title-foreground-color); + @media (max-width: 760px) { + display: none; } } -.show-more { +.project-actions { display: flex; - align-items: center; - column-gap: $s-12; + opacity: var(--actions-opacity); + margin-left: $s-32; +} - color: $df-secondary; - font-size: $fs-14; - justify-content: space-between; - cursor: pointer; - background-color: transparent; - border: none; +.add-file-btn, +.options-btn { + @extend .button-tertiary; + height: $s-32; + width: $s-32; + margin: 0 $s-8; + padding: $s-8; +} + +.add-icon, +.menu-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.grid-container { + width: 100%; + padding: 0 $s-4; +} + +.show-more { + --show-more-color: var(--button-secondary-foreground-color-rest); + @include buttonStyle; + @include bodyMediumTypography; position: absolute; top: $s-8; right: $s-52; + display: flex; + align-items: center; + justify-content: space-between; + column-gap: $s-12; + color: var(--show-more-color); &:hover { - color: $da-tertiary; + --show-more-color: var(--button-secondary-foreground-color-active); } } @@ -138,12 +150,12 @@ height: $s-16; width: $s-16; fill: none; - stroke: currentColor; + stroke: var(--show-more-color); } // Team hero .team-hero { - background-color: $db-tertiary; + background-color: var(--color-background-tertiary); border-radius: $br-8; border: none; display: flex; @@ -173,7 +185,7 @@ .title { font-size: $fs-24; - color: $df-primary; + color: var(--color-foreground-primary); font-weight: $fw400; } @@ -181,11 +193,11 @@ flex: 1; font-size: $fs-16; span { - color: $df-secondary; + color: var(--color-foreground-secondary); display: block; } a { - color: $da-primary; + color: var(--color-accent-primary); } padding: $s-8 0; } @@ -227,88 +239,3 @@ width: 0; } } - -.hero-projects { - display: grid; - grid-template-columns: 1fr 1fr; - grid-gap: $s-32; - margin: 0 $s-16 $s-16 $s-20; - - @media (max-width: 1366px) { - grid-template-columns: 1fr; - } - - .tutorial, - .walkthrough { - display: grid; - grid-template-columns: auto 1fr; - position: relative; - border-radius: $br-8; - min-height: $s-216; - background-color: $db-tertiary; - padding: $s-8; - - .thumbnail { - width: $s-200; - height: $s-200; - border-radius: $br-6; - padding: $s-32; - display: block; - background-color: var(--color-canvas); - } - - img { - border-radius: $br-4; - margin-bottom: 0; - width: $s-232; - } - - .text { - padding: $s-32; - display: flex; - flex-direction: column; - } - - .title { - color: $df-primary; - font-size: $fs-24; - font-weight: $fw400; - margin-bottom: $s-8; - } - .info { - flex: 1; - color: $df-secondary; - margin-bottom: $s-20; - font-size: $fs-16; - } - .invite { - height: $s-32; - } - .action { - width: $s-180; - height: $s-40; - } - } - .walkthrough { - .thumbnail { - background-image: url("/images/walkthrough-cover.png"); - background-position: center; - background-repeat: no-repeat; - background-size: cover; - } - } - .tutorial { - .thumbnail { - background-image: url("/images/hands-on-tutorial.png"); - background-position: center; - background-repeat: no-repeat; - background-size: cover; - } - .loader { - display: flex; - svg#loader-pencil { - width: $s-32; - } - } - } -} diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index 3b4d090996d0aae85b197b1489daad70fcf0f9aa..401d3349445caad043cb55a0b25651b723c3f302 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -37,7 +37,7 @@ (st/emit! (dd/search {:search-term search-term}) (dd/clear-selected-files)))) [:* - [:header {:class (stl/css :dashboard-header)} + [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} [:div#dashboard-search-title {:class (stl/css :dashboard-title)} [:h1 (tr "dashboard.title-search")]]] diff --git a/frontend/src/app/main/ui/dashboard/search.scss b/frontend/src/app/main/ui/dashboard/search.scss index 7c20671c2196b073eb8070ef593459b91e250f6e..f0180ed414f2da5150ae7957c5d9cb23135fe7f1 100644 --- a/frontend/src/app/main/ui/dashboard/search.scss +++ b/frontend/src/app/main/ui/dashboard/search.scss @@ -13,7 +13,7 @@ margin-right: $s-16; overflow-y: auto; width: 100%; - border-top: $s-1 solid $db-quaternary; + border-top: $s-1 solid var(--color-background-quaternary); &.dashboard-projects { user-select: none; @@ -35,14 +35,14 @@ flex-direction: column; height: $s-200; background: transparent; - border: $s-1 solid $db-quaternary; + border: $s-1 solid var(--color-background-quaternary); border-radius: $br-8; .text { - color: $df-primary; + color: var(--color-foreground-primary); } .icon svg { - stroke: $df-secondary; + stroke: var(--color-foreground-secondary); width: $s-32; height: $s-32; } diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 96d4c9df08ad4fdd82274a2a3fd00073a338ec37..0f3565bdbfb2329b34f6806b24120711110720f7 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -13,8 +13,8 @@ [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] @@ -149,7 +149,7 @@ on-drop-success (mf/use-fn (mf/deps (:id item)) - #(st/emit! (msg/success (tr "dashboard.success-move-file")) + #(st/emit! (ntf/success (tr "dashboard.success-move-file")) (dd/go-to-files (:id item)))) on-drop @@ -256,11 +256,13 @@ (if (or @focused? (seq search-term)) [:button {:class (stl/css :search-btn :clear-search-btn) :tab-index "0" + :aria-label "dashboard-clear-search" :on-click on-clear-click :on-key-down handle-clear-search} clear-search-icon] [:button {:class (stl/css :search-btn) + :aria-label "dashboard-search" :on-click on-clear-click} search-icon])])) @@ -360,13 +362,13 @@ (fn [{:keys [code] :as error}] (condp = code :no-enough-members-for-leave - (rx/of (msg/error (tr "errors.team-leave.insufficient-members"))) + (rx/of (ntf/error (tr "errors.team-leave.insufficient-members"))) :member-does-not-exist - (rx/of (msg/error (tr "errors.team-leave.member-does-not-exists"))) + (rx/of (ntf/error (tr "errors.team-leave.member-does-not-exists"))) :owner-cant-leave-team - (rx/of (msg/error (tr "errors.team-leave.owner-cant-leave"))) + (rx/of (ntf/error (tr "errors.team-leave.owner-cant-leave"))) (rx/throw error))) @@ -504,13 +506,13 @@ :on-key-down handle-members :className (stl/css :team-options-item) :id "teams-options-members" - :data-test "team-members"} + :data-testid "team-members"} (tr "labels.members")] [:> dropdown-menu-item* {:on-click go-invitations :on-key-down handle-invitations :className (stl/css :team-options-item) :id "teams-options-invitations" - :data-test "team-invitations"} + :data-testid "team-invitations"} (tr "labels.invitations")] (when (contains? cf/flags :webhooks) @@ -524,7 +526,7 @@ :on-key-down handle-settings :className (stl/css :team-options-item) :id "teams-options-settings" - :data-test "team-settings"} + :data-testid "team-settings"} (tr "labels.settings")] [:hr {:class (stl/css :team-option-separator)}] @@ -533,7 +535,7 @@ :on-key-down handle-rename :id "teams-options-rename" :className (stl/css :team-options-item) - :data-test "rename-team"} + :data-testid "rename-team"} (tr "labels.rename")]) (cond @@ -550,7 +552,7 @@ :on-key-down handle-leave-as-owner-clicked :id "teams-options-leave-team" :className (stl/css :team-options-item) - :data-test "leave-team"} + :data-testid "leave-team"} (tr "dashboard.leave-team")] (> (count members) 1) @@ -565,7 +567,7 @@ :on-key-down handle-on-delete-clicked :id "teams-options-delete-team" :className (stl/css :team-options-item :warning) - :data-test "delete-team"} + :data-testid "delete-team"} (tr "dashboard.delete-team")])])) (mf/defc sidebar-team-switch @@ -654,6 +656,7 @@ (when-not (:is-default team) [:button {:class (stl/css :switch-options) :on-click handle-show-opts-click + :aria-label "team-management" :tab-index "0" :on-key-down handle-show-opts-keydown} menu-icon])] @@ -791,6 +794,7 @@ [:li {:class (stl/css-case :current libs? :sidebar-nav-item true)} [:& link {:action go-libs + :data-testid "libs-link-sidebar" :class (stl/css :sidebar-link) :keyboard-action go-libs-with-key} [:span {:class (stl/css :element-title)} (tr "labels.shared-libraries")]]]]] @@ -803,12 +807,12 @@ [:& link {:action go-fonts :class (stl/css :sidebar-link) :keyboard-action go-fonts-with-key - :data-test "fonts"} + :data-testid "fonts"} [:span {:class (stl/css :element-title)} (tr "labels.fonts")]]]]] [:div {:class (stl/css :sidebar-content-section) - :data-test "pinned-projects"} + :data-testid "pinned-projects"} (if (seq pinned-projects) [:ul {:class (stl/css :sidebar-nav :pinned-projects)} (for [item pinned-projects] @@ -945,11 +949,11 @@ :on-hide-comments handle-hide-comments}]) [:div {:class (stl/css :profile-section)} - [:div {:class (stl/css :profile) - :tab-index "0" - :on-click handle-click - :on-key-down handle-key-down - :data-test "profile-btn"} + [:button {:class (stl/css :profile) + :tab-index "0" + :on-click handle-click + :on-key-down handle-key-down + :data-testid "profile-btn"} [:img {:src photo :class (stl/css :profile-img) :alt (:fullname profile)}] @@ -960,7 +964,7 @@ :class (stl/css :profile-dropdown-item) :on-click handle-set-profile :on-key-down handle-key-down-profile - :data-test "profile-profile-opt"} + :data-testid "profile-profile-opt"} (tr "labels.your-account")] [:li {:class (stl/css :profile-separator)}] @@ -970,7 +974,7 @@ :data-url "https://help.penpot.app" :on-click handle-click-url :on-key-down handle-keydown-url - :data-test "help-center-profile-opt"} + :data-testid "help-center-profile-opt"} (tr "labels.help-center")] [:li {:tab-index (if show "0" "-1") @@ -1000,7 +1004,7 @@ :data-url "https://penpot.app/libraries-templates" :on-click handle-click-url :on-key-down handle-keydown-url - :data-test "libraries-templates-profile-opt"} + :data-testid "libraries-templates-profile-opt"} (tr "labels.libraries-and-templates")] [:li {:tab-index (if show "0" "-1") @@ -1024,14 +1028,14 @@ :tab-index (if show "0" "-1") :on-click handle-feedback-click :on-key-down handle-feedback-keydown - :data-test "feedback-profile-opt"} + :data-testid "feedback-profile-opt"} (tr "labels.give-feedback")]) [:li {:class (stl/css :profile-dropdown-item :item-with-icon) :tab-index (if show "0" "-1") :on-click handle-logout-click :on-key-down handle-logout-keydown - :data-test "logout-profile-opt"} + :data-testid "logout-profile-opt"} exit-icon (tr "labels.logout")]] @@ -1047,7 +1051,7 @@ [props] (let [team (obj/get props "team") profile (obj/get props "profile")] - [:nav {:class (stl/css :dashboard-sidebar)} + [:nav {:class (stl/css :dashboard-sidebar) :data-testid "dashboard-sidebar"} [:> sidebar-content props] [:& profile-section {:profile profile diff --git a/frontend/src/app/main/ui/dashboard/sidebar.scss b/frontend/src/app/main/ui/dashboard/sidebar.scss index c6cea273a4042aede059ccfb4bd9fd6f91c5b6c3..22a0a3c9c6a1018f9c4228b066fc742919eef42f 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -239,6 +239,7 @@ .element-title { @include textEllipsis; + width: $s-256; color: var(--sidebar-element-foreground-color); font-size: $fs-14; } @@ -330,15 +331,17 @@ } .profile { + @include buttonStyle; display: grid; grid-template-columns: auto 1fr; gap: $s-8; cursor: pointer; + text-align: left; } .profile-fullname { @include smallTitleTipography; - @include text-ellipsis; + @include textEllipsis; align-self: center; max-width: $s-160; color: var(--profile-foreground-color); diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index adc63664ce705fdedc6f2315c34f8c83a09ab355..c19f6f495f79e02ef306b355d3a00c9117f42136 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -9,12 +9,13 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.schema :as sm] [app.common.spec :as us] [app.config :as cfg] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] - [app.main.data.messages :as msg] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] @@ -24,6 +25,7 @@ [app.main.ui.dashboard.change-owner] [app.main.ui.dashboard.team-form] [app.main.ui.icons :as i] + [app.main.ui.notifications.badge :refer [badge-notification]] [app.main.ui.notifications.context-notification :refer [context-notification]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] @@ -32,7 +34,6 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) - (def ^:private arrow-icon (i/icon-xref :arrow (stl/css :arrow-icon))) @@ -60,11 +61,11 @@ (mf/defc header {::mf/wrap [mf/memo] ::mf/wrap-props false} - [{:keys [section team]}] - (let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members))) - on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings))) - on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations))) - on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks))) + [{:keys [section team invite-email]}] + (let [on-nav-members (mf/use-fn #(st/emit! (dd/go-to-team-members))) + on-nav-settings (mf/use-fn #(st/emit! (dd/go-to-team-settings))) + on-nav-invitations (mf/use-fn #(st/emit! (dd/go-to-team-invitations))) + on-nav-webhooks (mf/use-fn #(st/emit! (dd/go-to-team-webhooks))) members-section? (= section :dashboard-team-members) settings-section? (= section :dashboard-team-settings) @@ -78,9 +79,14 @@ (fn [] (st/emit! (modal/show {:type :invite-members :team team - :origin :team}))))] + :origin :team + :invite-email invite-email}))))] + + (mf/with-effect [] + (when invite-email + (on-invite-member))) - [:header {:class (stl/css :dashboard-header :team)} + [:header {:class (stl/css :dashboard-header :team) :data-testid "dashboard-header"} [:div {:class (stl/css :dashboard-title)} [:h1 (cond members-section? (tr "labels.members") @@ -104,7 +110,7 @@ [:a {:class (stl/css :btn-secondary :btn-small) :on-click on-invite-member - :data-test "invite-member"} + :data-testid "invite-member"} (tr "dashboard.invite-profile")] [:div {:class (stl/css :blank-space)}])]])) @@ -130,17 +136,28 @@ (s/def ::invite-member-form (s/keys :req-un [::role ::emails ::team-id])) +(def ^:private schema:invite-member-form + [:map {:title "InviteMemberForm"} + [:role :keyword] + [:emails [::sm/set {:kind ::sm/email :min 1}]] + [:team-id ::sm/uuid]]) + (mf/defc invite-members-modal {::mf/register modal/components ::mf/register-as :invite-members ::mf/wrap-props false} - [{:keys [team origin]}] + [{:keys [team origin invite-email]}] (let [members-map (mf/deref refs/dashboard-team-members) perms (:permissions team) - roles (mf/use-memo (mf/deps perms) #(get-available-roles perms)) - initial (mf/use-memo (constantly {:role "editor" :team-id (:id team)})) - form (fm/use-form :spec ::invite-member-form + roles (mf/with-memo [perms] + (get-available-roles perms)) + team-id (:id team) + + initial (mf/with-memo [team-id] + {:role "editor" :team-id team-id}) + + form (fm/use-form :schema schema:invite-member-form :initial initial) error-text (mf/use-state "") @@ -150,27 +167,28 @@ on-success (fn [_form {:keys [total]}] (when (pos? total) - (st/emit! (msg/success (tr "notifications.invitation-email-sent")))) + (st/emit! (ntf/success (tr "notifications.invitation-email-sent")))) (st/emit! (modal/hide) (dd/fetch-team-invitations))) on-error - (fn [{:keys [type code] :as error}] - (cond - (and (= :validation type) - (= :profile-is-muted code)) - (st/emit! (msg/error (tr "errors.profile-is-muted")) - (modal/hide)) - - (and (= :validation type) - (or (= :member-is-muted code) - (= :email-has-permanent-bounces code))) - (swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error))) - - :else - (st/emit! (msg/error (tr "errors.generic")) - (modal/hide)))) + (fn [_form cause] + (let [{:keys [type code] :as error} (ex-data cause)] + (cond + (and (= :validation type) + (= :profile-is-muted code)) + (st/emit! (ntf/error (tr "errors.profile-is-muted")) + (modal/hide)) + + (or (= :member-is-muted code) + (= :email-has-permanent-bounces code) + (= :email-has-complaints code)) + (swap! error-text (tr "errors.email-spam-or-permanent-bounces" (:email error))) + + :else + (st/emit! (ntf/error (tr "errors.generic")) + (modal/hide))))) on-submit (fn [form] @@ -179,7 +197,8 @@ :on-error (partial on-error form)}] (st/emit! (-> (dd/invite-team-members (with-meta params mdata)) (with-meta {::ev/origin origin})) - (dd/fetch-team-invitations))))] + (dd/fetch-team-invitations) + (dd/fetch-team-members (:id team)))))] [:div {:class (stl/css-case :modal-team-container true @@ -190,11 +209,11 @@ (when-not (= "" @error-text) [:& context-notification {:content @error-text - :type :error}]) + :level :error}]) (when (some current-data-emails current-members-emails) [:& context-notification {:content (tr "modals.invite-member.repeated-invitation") - :type :warning}]) + :level :warning}]) [:div {:class (stl/css :role-select)} [:p {:class (stl/css :role-title)} @@ -210,7 +229,8 @@ :valid-item-fn us/parse-email :caution-item-fn current-members-emails :label (tr "modals.invite-member.emails") - :on-submit on-submit}]] + :on-submit on-submit + :invite-email invite-email}]] [:div {:class (stl/css :action-buttons)} [:> fm/submit-button* @@ -355,13 +375,13 @@ (condp = code :no-enough-members-for-leave - (rx/of (msg/error (tr "errors.team-leave.insufficient-members"))) + (rx/of (ntf/error (tr "errors.team-leave.insufficient-members"))) :member-does-not-exist - (rx/of (msg/error (tr "errors.team-leave.member-does-not-exists"))) + (rx/of (ntf/error (tr "errors.team-leave.member-does-not-exists"))) :owner-cant-leave-team - (rx/of (msg/error (tr "errors.team-leave.owner-cant-leave"))) + (rx/of (ntf/error (tr "errors.team-leave.owner-cant-leave"))) (rx/throw error)))) @@ -484,7 +504,7 @@ (mf/defc team-members-page {::mf/wrap-props false} - [{:keys [team profile]}] + [{:keys [team profile invite-email]}] (let [members-map (mf/deref refs/dashboard-team-members)] (mf/with-effect [team] @@ -498,7 +518,7 @@ (st/emit! (dd/fetch-team-members (:id team)))) [:* - [:& header {:section :dashboard-team-members :team team}] + [:& header {:section :dashboard-team-members :team team :invite-email invite-email}] [:section {:class (stl/css :dashboard-container :dashboard-team-members)} [:& team-members {:profile profile @@ -551,19 +571,6 @@ :on-click on-change'} (tr "labels.editor")]]]])) -(mf/defc invitation-status-badge - {::mf/wrap-props false} - [{:keys [status]}] - [:div - {:class (stl/css-case - :status-badge true - :badge-expired (= status :expired) - :badge-pending (= status :pending))} - [:span {:class (stl/css :status-label)} - (if (= status :expired) - (tr "labels.expired-invitation") - (tr "labels.pending-invitation"))]]) - (mf/defc invitation-actions {::mf/wrap-props false} [{:keys [invitation team-id]}] @@ -575,22 +582,24 @@ on-error (mf/use-fn (mf/deps email) - (fn [{:keys [type code] :as error}] - (cond - (and (= :validation type) - (= :profile-is-muted code)) - (rx/of (msg/error (tr "errors.profile-is-muted"))) + (fn [cause] + (let [{:keys [type code] :as error} (ex-data cause)] + (cond + (and (= :validation type) + (= :profile-is-muted code)) + (rx/of (ntf/error (tr "errors.profile-is-muted"))) - (and (= :validation type) - (= :member-is-muted code)) - (rx/of (msg/error (tr "errors.member-is-muted"))) + (and (= :validation type) + (= :member-is-muted code)) + (rx/of (ntf/error (tr "errors.member-is-muted"))) - (and (= :validation type) - (= :email-has-permanent-bounces code)) - (rx/of (msg/error (tr "errors.email-has-permanent-bounces" email))) + (and (= :restriction type) + (or (= :email-has-permanent-bounces code) + (= :email-has-complaints code))) + (rx/of (ntf/error (tr "errors.email-has-permanent-bounces" email))) - :else - (rx/throw error)))) + :else + (rx/throw cause))))) on-delete (mf/use-fn @@ -600,11 +609,10 @@ mdata {:on-success #(st/emit! (dd/fetch-team-invitations))}] (st/emit! (dd/delete-team-invitation (with-meta params mdata)))))) - on-resend-success (mf/use-fn (fn [] - (st/emit! (msg/success (tr "notifications.invitation-email-sent")) + (st/emit! (ntf/success (tr "notifications.invitation-email-sent")) (modal/hide) (dd/fetch-team-invitations)))) @@ -625,7 +633,7 @@ on-copy-success (mf/use-fn (fn [] - (st/emit! (msg/success (tr "notifications.invitation-link-copied")) + (st/emit! (ntf/success (tr "notifications.invitation-link-copied")) (modal/hide)))) on-copy @@ -668,6 +676,10 @@ email (:email invitation) role (:role invitation) status (if expired? :expired :pending) + type (if expired? :warning :default) + badge-content (if (= status :expired) + (tr "labels.expired-invitation") + (tr "labels.pending-invitation")) on-change-role (mf/use-fn @@ -688,7 +700,7 @@ :on-change on-change-role}]] [:div {:class (stl/css :table-field :field-status)} - [:& invitation-status-badge {:status status}]] + [:& badge-notification {:type type :content badge-content}]] [:div {:class (stl/css :table-field :field-actions)} (when can-invite? @@ -701,8 +713,8 @@ [:div {:class (stl/css :empty-invitations)} [:span (tr "labels.no-invitations")] (when can-invite? - [:& i18n/tr-html {:label "labels.no-invitations-hint" - :tag-name "span"}])]) + [:> i18n/tr-html* {:content (tr "labels.no-invitations-hint") + :tag-name "span"}])]) (mf/defc invitation-section [{:keys [team invitations] :as props}] @@ -754,10 +766,11 @@ ;; WEBHOOKS SECTION ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(s/def ::uri ::us/uri) -(s/def ::mtype ::us/not-empty-string) -(s/def ::webhook-form - (s/keys :req-un [::uri ::mtype])) +(def ^:private schema:webhook-form + [:map {:title "WebhookForm"} + [:uri [::sm/uri {:max 4069 :prefix #"^http[s]?://" + :error/code "errors.webhooks.invalid-uri"}]] + [:mtype ::sm/text]]) (def valid-webhook-mtypes [{:label "application/json" :value "application/json"} @@ -771,18 +784,18 @@ {::mf/register modal/components ::mf/register-as :webhook} [{:keys [webhook] :as props}] - ;; FIXME: this is a workaround because input fields do not support rendering hooks - (let [initial (mf/use-memo (fn [] (or (some-> webhook (update :uri str)) - {:is-active false :mtype "application/json"}))) - form (fm/use-form :spec ::webhook-form - :initial initial - :validators [(fm/validate-length :uri fm/max-uri-length-allowed (tr "team.webhooks.max-length"))]) + + (let [initial (mf/with-memo [] + (or (some-> webhook (update :uri str)) + {:is-active false :mtype "application/json"})) + form (fm/use-form :schema schema:webhook-form + :initial initial) on-success (mf/use-fn (fn [_] (let [message (tr "dashboard.webhooks.create.success")] (st/emit! (dd/fetch-team-webhooks) - (msg/success message) + (ntf/success message) (modal/hide))))) on-error @@ -886,8 +899,8 @@ [:div {:class (stl/css :webhooks-hero-container)} [:h2 {:class (stl/css :hero-title)} (tr "labels.webhooks")] - [:& i18n/tr-html {:class (stl/css :hero-desc) - :label "dashboard.webhooks.description"}] + [:> i18n/tr-html* {:class (stl/css :hero-desc) + :content (tr "dashboard.webhooks.description")}] [:button {:class (stl/css :hero-btn) :on-click #(st/emit! (modal/show :webhook {}))} (tr "dashboard.webhooks.create")]]) diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index c5d67a0272679ae2e79bf26717ec9623f60587a0..d914ea773caba4759bf2e33d5d2e4e948d369c0f 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -92,7 +92,7 @@ width: 100%; z-index: $z-index-modal; border-radius: $br-circle; - background-color: $da-primary; + background-color: var(--color-accent-primary); } .image-icon { @@ -322,25 +322,6 @@ align-items: center; } -// STATUS BADGE -.status-badge { - @include flexCenter; - @include headlineSmallTypography; - min-width: $s-76; - width: fit-content; - height: $s-24; - border-radius: $br-8; - color: var(--pill-foreground-color); -} - -.badge-pending { - background-color: var(--status-widget-background-color-warning); -} - -.badge-expired { - background-color: var(--tag-background-color-disabled); -} - .invitations-dropdown { bottom: calc(-1 * $s-112); right: calc(-1 * $s-20); @@ -397,7 +378,7 @@ } .hero-desc { - color: $df-secondary; + color: var(--color-foreground-secondary); margin-bottom: 0; font-size: $fs-16; max-width: $s-512; @@ -559,5 +540,6 @@ .email-input { @extend .input-base; + @include bodySmallTypography; height: auto; } diff --git a/frontend/src/app/main/ui/dashboard/team_form.cljs b/frontend/src/app/main/ui/dashboard/team_form.cljs index 7a37ec9c6c9203e2cf2683066643a506233f7e72..cf8796b75b50e91f4b296b3757e4da425bf726c8 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.cljs +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -7,10 +7,11 @@ (ns app.main.ui.dashboard.team-form (:require-macros [app.main.style :as stl]) (:require - [app.common.spec :as us] + [app.common.schema :as sm] [app.main.data.dashboard :as dd] - [app.main.data.messages :as msg] + [app.main.data.events :as ev] [app.main.data.modal :as modal] + [app.main.data.notifications :as ntf] [app.main.store :as st] [app.main.ui.components.forms :as fm] [app.main.ui.icons :as i] @@ -19,39 +20,39 @@ [app.util.keyboard :as kbd] [app.util.router :as rt] [beicon.v2.core :as rx] - [cljs.spec.alpha :as s] [rumext.v2 :as mf])) -(s/def ::name ::us/not-empty-string) -(s/def ::team-form - (s/keys :req-un [::name])) +(def ^:private schema:team-form + [:map {:title "TeamForm"} + [:name [::sm/text {:max 250}]]]) (defn- on-create-success [_form response] (let [msg "Team created successfully"] - (st/emit! (msg/success msg) + (st/emit! (ntf/success msg) (modal/hide) (rt/nav :dashboard-projects {:team-id (:id response)})))) (defn- on-update-success [_form _response] (let [msg "Team created successfully"] - (st/emit! (msg/success msg) + (st/emit! (ntf/success msg) (modal/hide)))) (defn- on-error [form _response] (let [id (get-in @form [:clean-data :id])] (if id - (rx/of (msg/error "Error on updating team.")) - (rx/of (msg/error "Error on creating team."))))) + (rx/of (ntf/error "Error on updating team.")) + (rx/of (ntf/error "Error on creating team."))))) (defn- on-create-submit [form] (let [mdata {:on-success (partial on-create-success form) :on-error (partial on-error form)} params {:name (get-in @form [:clean-data :name])}] - (st/emit! (dd/create-team (with-meta params mdata))))) + (st/emit! (-> (dd/create-team (with-meta params mdata)) + (with-meta {::ev/origin :dashboard}))))) (defn- on-update-submit [form] @@ -68,24 +69,23 @@ (on-update-submit form) (on-create-submit form)))) -(mf/defc team-form-modal {::mf/register modal/components - ::mf/register-as :team-form} +(mf/defc team-form-modal + {::mf/register modal/components + ::mf/register-as :team-form} [{:keys [team] :as props}] (let [initial (mf/use-memo (fn [] (or team {}))) - form (fm/use-form :spec ::team-form - :validators [(fm/validate-not-empty :name (tr "auth.name.not-all-space")) - (fm/validate-length :name fm/max-length-allowed (tr "auth.name.too-long"))] + form (fm/use-form :schema schema:team-form :initial initial) handle-keydown - (mf/use-callback - (mf/deps) + (mf/use-fn (fn [e] (when (kbd/enter? e) (dom/prevent-default e) (dom/stop-propagation e) (on-submit form e)))) - on-close #(st/emit! (modal/hide))] + on-close + (mf/use-fn #(st/emit! (modal/hide)))] [:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-container)} diff --git a/frontend/src/app/main/ui/dashboard/team_form.scss b/frontend/src/app/main/ui/dashboard/team_form.scss index 2545950f8ed403f91ea14ed6a6bca55b5fe970fe..d57cffb82ecc04b4db35bf2fa8ca35f95e609798 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.scss +++ b/frontend/src/app/main/ui/dashboard/team_form.scss @@ -37,6 +37,8 @@ .group-name-input { @extend .input-element-label; + @include bodySmallTypography; + margin-bottom: $s-8; label { @include flexColumn; @include bodySmallTypography; diff --git a/frontend/src/app/main/ui/dashboard/templates.cljs b/frontend/src/app/main/ui/dashboard/templates.cljs index 7fb943a889d4004e97c5c448da78913a666e4ca3..1d6e079989bb0f7f3798e6faa6fcba2f8a293a16 100644 --- a/frontend/src/app/main/ui/dashboard/templates.cljs +++ b/frontend/src/app/main/ui/dashboard/templates.cljs @@ -8,7 +8,6 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data.macros :as dm] - [app.common.math :as mth] [app.config :as cf] [app.main.data.dashboard :as dd] [app.main.data.events :as ev] @@ -118,11 +117,14 @@ :id id :data-index index :on-click on-click + :on-mouse-down dom/prevent-default :on-key-down on-key-down} [:div {:class (stl/css :template-card)} [:div {:class (stl/css :img-container)} [:img {:src (dm/str thb) - :alt (:name item)}]] + :alt (:name item) + :loading "lazy" + :decoding "async"}]] [:div {:class (stl/css :card-name)} [:span {:class (stl/css :card-text)} (:name item)] download-icon]]])) @@ -153,7 +155,7 @@ [:div {:class (stl/css :img-container)} [:a {:id id :tab-index (if (or (not is-visible) collapsed) "-1" "0") - :href "https://penpot.app/libraries-templates.html" + :href "https://penpot.app/libraries-templates" :target "_blank" :on-click on-click :on-key-down on-key-down} @@ -163,10 +165,12 @@ (mf/defc templates-section {::mf/wrap-props false} - [{:keys [default-project-id profile project-id team-id content-width]}] + [{:keys [default-project-id profile project-id team-id]}] (let [templates (mf/deref builtin-templates) templates (mf/with-memo [templates] - (filterv #(not= (:id %) "tutorial-for-beginners") templates)) + (filterv #(and + (not= (:id %) "welcome") + (not= (:id %) "tutorial-for-beginners")) templates)) route (mf/deref refs/route) route-name (get-in route [:data :name]) @@ -178,63 +182,41 @@ props (:props profile) collapsed (:builtin-templates-collapsed-status props false) - card-offset* (mf/use-state 0) - card-offset (deref card-offset*) + can-move (mf/use-state {:left false :right true}) - card-width 275 total (count templates) - container-size (* (+ 2 total) card-width) ;; We need space for total plus the libraries&templates link - more-cards (> (+ card-offset (* (+ 1 total) card-width)) content-width) - card-count (mth/floor (/ content-width 275)) - left-moves (/ card-offset -275) - first-card left-moves - last-card (+ (- card-count 1) left-moves) content-ref (mf/use-ref) - on-move-left + move-left (fn [] (dom/scroll-by! (mf/ref-val content-ref) -300 0)) + move-right (fn [] (dom/scroll-by! (mf/ref-val content-ref) 300 0)) + + update-can-move + (fn [scroll-left scroll-available client-width] + (reset! can-move {:left (> scroll-left 0) + :right (> scroll-available client-width)})) + + on-scroll (mf/use-fn - (mf/deps card-offset card-width) - (fn [_event] - (when-not (zero? card-offset) - (dom/animate! (mf/ref-val content-ref) - [#js {:left (dm/str card-offset "px")} - #js {:left (dm/str (+ card-offset card-width) "px")}] - #js {:duration 200 :easing "linear"}) - (reset! card-offset* (+ card-offset card-width))))) + (fn [e] + (let [scroll (dom/get-target-scroll e) + scroll-left (:scroll-left scroll) + scroll-available (- (:scroll-width scroll) scroll-left) + client-rect (dom/get-client-size (dom/get-target e))] + (update-can-move scroll-left scroll-available (unchecked-get client-rect "width"))))) + + on-move-left + (mf/use-fn #(move-left)) on-move-left-key-down - (mf/use-fn - (mf/deps on-move-left first-card) - (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (on-move-left event) - (when-let [node (dom/get-element (dm/str "card-container-" first-card))] - (dom/focus! node))))) + (mf/use-fn #(move-left)) on-move-right - (mf/use-fn - (mf/deps more-cards card-offset card-width) - (fn [_event] - (when more-cards - (swap! card-offset* inc) - (dom/animate! (mf/ref-val content-ref) - [#js {:left (dm/str card-offset "px")} - #js {:left (dm/str (- card-offset card-width) "px")}] - #js {:duration 200 :easing "linear"}) - (reset! card-offset* (- card-offset card-width))))) + (mf/use-fn #(move-right)) on-move-right-key-down - (mf/use-fn - (mf/deps on-move-right last-card) - (fn [event] - (when (kbd/enter? event) - (dom/stop-propagation event) - (on-move-right event) - (when-let [node (dom/get-element (dm/str "card-container-" last-card))] - (dom/focus! node))))) + (mf/use-fn #(move-right)) on-import-template (mf/use-fn @@ -242,6 +224,12 @@ (fn [template _event] (import-template! template team-id project-id default-project-id section)))] + (mf/with-effect [content-ref templates] + (let [content (mf/ref-val content-ref)] + (when (and (some? content) (some? templates)) + (dom/scroll-to content #js {:behavior "instant" :left 0 :top 0}) + (.dispatchEvent content (js/Event. "scroll"))))) + (mf/with-effect [profile collapsed] (when (and profile (not collapsed)) (st/emit! (dd/fetch-builtin-templates)))) @@ -251,9 +239,8 @@ [:& title {:collapsed collapsed}] [:div {:class (stl/css :content) - :ref content-ref - :style {:left card-offset - :width (dm/str container-size "px")}} + :on-scroll on-scroll + :ref content-ref} (for [index (range (count templates))] [:& card-item @@ -261,24 +248,23 @@ :item (nth templates index) :index index :key index - :is-visible (and (>= index first-card) - (<= index last-card)) + :is-visible true :collapsed collapsed}]) [:& card-item-link - {:is-visible (and (>= total first-card) (<= total last-card)) + {:is-visible true :collapsed collapsed :section section :total total}]] - (when (< card-offset 0) + (when (:left @can-move) [:button {:class (stl/css :move-button :move-left) :tab-index (if ^boolean collapsed "-1" "0") :on-click on-move-left :on-key-down on-move-left-key-down} arrow-icon]) - (when more-cards + (when (:right @can-move) [:button {:class (stl/css :move-button :move-right) :tab-index (if collapsed "-1" "0") :on-click on-move-right diff --git a/frontend/src/app/main/ui/dashboard/templates.scss b/frontend/src/app/main/ui/dashboard/templates.scss index 2fa5084111607e141d2828ae7c2ed8b0eff10749..909f2cf2d972fe25c2f71f5a4853880676c9860e 100644 --- a/frontend/src/app/main/ui/dashboard/templates.scss +++ b/frontend/src/app/main/ui/dashboard/templates.scss @@ -42,8 +42,8 @@ border-top-right-radius: $br-10; margin-right: $s-32; position: relative; - z-index: 1; - background-color: $db-quaternary; + z-index: $z-index-1; + background-color: var(--color-background-quaternary); } .title-text { @@ -53,7 +53,7 @@ font-size: $fs-16; margin-left: $s-16; margin-right: $s-8; - color: $df-primary; + color: var(--color-foreground-primary); font-weight: $fw400; } @@ -62,7 +62,7 @@ vertical-align: middle; margin-left: $s-16; margin-right: $s-8; - color: $df-primary; + color: var(--color-foreground-primary); margin-left: $s-16; margin-right: $s-16; transform: rotate(90deg); @@ -80,20 +80,20 @@ .move-button { position: absolute; top: $s-136; - border: $s-2 solid $df-secondary; + border: $s-2 solid var(--color-foreground-secondary); border-radius: 50%; text-align: center; width: $s-36; height: $s-36; cursor: pointer; - background-color: $df-primary; + background-color: var(--color-foreground-primary); display: flex; align-items: center; justify-content: center; pointer-events: all; &:hover { - border: $s-2 solid $da-tertiary; + border: $s-2 solid var(--color-accent-tertiary); } } @@ -109,24 +109,29 @@ } .content { + display: grid; + grid-template-columns: repeat(auto-fill, minmax($s-276, $s-276)); + grid-auto-flow: column; pointer-events: all; - width: 200%; height: $s-228; margin-left: $s-6; - position: absolute; border-top-left-radius: $s-8; - background-color: $db-quaternary; + background-color: var(--color-background-quaternary); + overflow: scroll hidden; + scroll-behavior: smooth; + scroll-snap-type: x mandatory; + scroll-snap-stop: always; } .card-container { width: $s-276; margin-top: $s-20; - display: inline-block; text-align: center; vertical-align: top; background-color: transparent; border: none; padding: 0; + scroll-snap-align: start; } .template-card { @@ -134,12 +139,12 @@ width: $s-256; font-size: $fs-16; cursor: pointer; - color: $df-primary; + color: var(--color-foreground-primary); padding: $s-3 $s-6 $s-16 $s-6; border-radius: $br-8; &:hover { - background-color: $db-tertiary; + background-color: var(--color-background-tertiary); } } @@ -183,12 +188,12 @@ .template-link-title { font-size: $fs-14; - color: $df-primary; + color: var(--color-foreground-primary); font-weight: $fw400; } .template-link-text { font-size: $fs-12; margin-top: $s-8; - color: $df-secondary; + color: var(--color-foreground-secondary); } diff --git a/frontend/src/app/main/ui/debug/components_preview.cljs b/frontend/src/app/main/ui/debug/components_preview.cljs deleted file mode 100644 index 9fd0788b77f2174ef783ae1f4856dc4676ca2384..0000000000000000000000000000000000000000 --- a/frontend/src/app/main/ui/debug/components_preview.cljs +++ /dev/null @@ -1,270 +0,0 @@ -;; 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 app.main.ui.debug.components-preview - (:require-macros [app.main.style :as stl]) - (:require - [app.common.data :as d] - [app.main.data.users :as du] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]] - [app.main.ui.components.search-bar :refer [search-bar]] - [app.main.ui.components.tab-container :refer [tab-container tab-element]] - [app.main.ui.components.title-bar :refer [title-bar]] - [app.main.ui.icons :as i] - [app.util.dom :as dom] - [rumext.v2 :as mf])) - -(mf/defc component-wrapper - {::mf/wrap-props false} - [props] - (let [children (unchecked-get props "children") - title (unchecked-get props "title")] - [:div {:class (stl/css :component)} - [:h4 {:class (stl/css :component-name)} title] - children])) - -(mf/defc components-preview - {::mf/wrap-props false} - [] - (let [profile (mf/deref refs/profile) - initial (mf/with-memo [profile] - (update profile :lang #(or % ""))) - initial-theme (:theme initial) - on-change (fn [event] - (let [theme (dom/event->value event) - data (assoc initial :theme theme)] - (st/emit! (du/update-profile data)))) - colors ["var(--color-background-primary)" - "var(--color-background-secondary)" - "var(--color-background-tertiary)" - "var(--color-background-quaternary)" - "var(--color-foreground-primary)" - "var(--color-foreground-secondary)" - "var(--color-accent-primary)" - "var(--color-accent-primary-muted)" - "var(--color-accent-secondary)" - "var(--color-accent-tertiary)"] - - ;; COMPONENTS FNs - state* (mf/use-state {:collapsed? true - :tab-selected :first - :input-value "" - :radio-selected "first"}) - state (deref state*) - - collapsed? (:collapsed? state) - toggle-collapsed - (mf/use-fn #(swap! state* update :collapsed? not)) - - tab-selected (:tab-selected state) - set-tab (mf/use-fn #(swap! state* assoc :tab-selected %)) - - input-value (:input-value state) - radio-selected (:radio-selected state) - - set-radio-selected (mf/use-fn #(swap! state* assoc :radio-selected %)) - - update-search - (mf/use-fn - (fn [value _event] - (swap! state* assoc :input-value value))) - - - on-btn-click (mf/use-fn #(prn "eyy"))] - - [:section.debug-components-preview - [:div {:class (stl/css :themes-row)} - [:h2 "Themes"] - [:select {:label "Select theme color" - :name :theme - :default "default" - :value initial-theme - :on-change on-change} - [:option {:label "Penpot Dark (default)" :value "default"}] - [:option {:label "Penpot Light" :value "light"}]] - [:div {:class (stl/css :wrapper)} - (for [color colors] - [:div {:class (stl/css :color-wrapper)} - [:span (d/name color)] - [:div {:key color - :style {:background color} - :class (stl/css :rect)}]])]] - - [:div {:class (stl/css :components-row)} - [:h2 {:class (stl/css :title)} "Components"] - [:div {:class (stl/css :components-wrapper)} - [:div {:class (stl/css :components-group)} - [:h3 "Titles"] - [:& component-wrapper - {:title "Title"} - [:& title-bar {:collapsable false - :title "Title"}]] - [:& component-wrapper - {:title "Title and action button"} - [:& title-bar {:collapsable false - :title "Title" - :on-btn-click on-btn-click - :btn-children i/add}]] - [:& component-wrapper - {:title "Collapsed title and action button"} - [:& title-bar {:collapsable true - :collapsed collapsed? - :on-collapsed toggle-collapsed - :title "Title" - :on-btn-click on-btn-click - :btn-children i/add}]] - [:& component-wrapper - {:title "Collapsed title and children"} - [:& title-bar {:collapsable true - :collapsed collapsed? - :on-collapsed toggle-collapsed - :title "Title"} - [:& tab-container {:on-change-tab set-tab - :selected tab-selected} - [:& tab-element {:id :first - :title "A tab"}] - [:& tab-element {:id :second - :title "B tab"}]]]]] - - [:div {:class (stl/css :components-group)} - [:h3 "Tabs component"] - [:& component-wrapper - {:title "2 tab component"} - [:& tab-container {:on-change-tab set-tab - :selected tab-selected} - [:& tab-element {:id :first :title "First tab"} - [:div "This is first tab content"]] - - [:& tab-element {:id :second :title "Second tab"} - [:div "This is second tab content"]]]] - [:& component-wrapper - {:title "3 tab component"} - [:& tab-container {:on-change-tab set-tab - :selected tab-selected} - [:& tab-element {:id :first :title "First tab"} - [:div "This is first tab content"]] - - [:& tab-element {:id :second - :title "Second tab"} - [:div "This is second tab content"]] - [:& tab-element {:id :third - :title "Third tab"} - [:div "This is third tab content"]]]]] - - [:div {:class (stl/css :components-group)} - [:h3 "Search bar"] - [:& component-wrapper - {:title "Search bar only"} - [:& search-bar {:on-change update-search - :value input-value - :placeholder "Test value"}]] - [:& component-wrapper - {:title "Search and button"} - [:& search-bar {:on-change update-search - :value input-value - :placeholder "Test value"} - [:button {:class (stl/css :button-secondary) - :on-click on-btn-click} - "X"]]]] - - [:div {:class (stl/css :components-group)} - [:h3 "Radio buttons"] - [:& component-wrapper - {:title "Two radio buttons (toggle)"} - [:& radio-buttons {:selected radio-selected - :on-change set-radio-selected - :name "listing-style"} - [:& radio-button {:icon i/view-as-list - :value "first" - :id :list}] - [:& radio-button {:icon i/flex-grid - :value "second" - :id :grid}]]] - [:& component-wrapper - {:title "Three radio buttons"} - [:& radio-buttons {:selected radio-selected - :on-change set-radio-selected - :name "listing-style"} - [:& radio-button {:icon i/view-as-list - :value "first" - :id :first}] - [:& radio-button {:icon i/flex-grid - :value "second" - :id :second}] - - [:& radio-button {:icon i/add - :value "third" - :id :third}]]] - - [:& component-wrapper - {:title "Four radio buttons"} - [:& radio-buttons {:selected radio-selected - :on-change set-radio-selected - :name "listing-style"} - [:& radio-button {:icon i/view-as-list - :value "first" - :id :first}] - [:& radio-button {:icon i/flex-grid - :value "second" - :id :second}] - - [:& radio-button {:icon i/add - :value "third" - :id :third}] - - [:& radio-button {:icon i/board - :value "forth" - :id :forth}]]]] - [:div {:class (stl/css :components-group)} - [:h3 "Buttons"] - [:& component-wrapper - {:title "Button primary"} - [:button {:class (stl/css :button-primary)} - "Primary"]] - [:& component-wrapper - {:title "Button primary with icon"} - [:button {:class (stl/css :button-primary)} - i/add]] - - [:& component-wrapper - {:title "Button secondary"} - [:button {:class (stl/css :button-secondary)} - "secondary"]] - [:& component-wrapper - {:title "Button secondary with icon"} - [:button {:class (stl/css :button-secondary)} - i/add]] - - [:& component-wrapper - {:title "Button tertiary"} - [:button {:class (stl/css :button-tertiary)} - "tertiary"]] - [:& component-wrapper - {:title "Button tertiary with icon"} - [:button {:class (stl/css :button-tertiary)} - i/add]]] - [:div {:class (stl/css :components-group)} - [:h3 "Inputs"] - [:& component-wrapper - {:title "Only input"} - [:div {:class (stl/css :input-wrapper)} - [:input {:class (stl/css :basic-input) - :placeholder "----"}]]] - [:& component-wrapper - {:title "Input with label"} - [:div {:class (stl/css :input-wrapper)} - [:span {:class (stl/css :input-label)} "label"] - [:input {:class (stl/css :basic-input) - :placeholder "----"}]]] - [:& component-wrapper - {:title "Input with icon"} - [:div {:class (stl/css :input-wrapper)} - [:span {:class (stl/css :input-label)} - i/add] - [:input {:class (stl/css :basic-input) - :placeholder "----"}]]]]]]])) diff --git a/frontend/src/app/main/ui/debug/components_preview.scss b/frontend/src/app/main/ui/debug/components_preview.scss deleted file mode 100644 index eb1d83acd1a72891a76f0c650be3fe908ead0dba..0000000000000000000000000000000000000000 --- a/frontend/src/app/main/ui/debug/components_preview.scss +++ /dev/null @@ -1,98 +0,0 @@ -// 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 - -@import "refactor/common-refactor.scss"; - -.themes-row { - width: 100%; - padding: $s-20; - color: var(--color-foreground-primary); - background: var(--color-background-secondary); - .wrapper { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: $s-40; - background-color: var(--color-background-primary); - width: 100%; - padding: $s-20; - .rect { - display: flex; - justify-content: center; - align-items: center; - border: $s-1 solid var(--color-foreground-primary); - padding: $s-20; - height: $s-96; - min-width: $s-152; - } - } -} -.color-wrapper { - display: grid; - grid-template-rows: auto $s-96; -} - -.components-row { - color: var(--color-foreground-primary); - background: var(--color-background-secondary); - height: 100%; - padding: 0 $s-20; - .title { - padding: $s-20; - } - .components-wrapper { - padding: $s-20; - display: flex; - flex-wrap: wrap; - gap: $s-20; - .components-group { - @include flexCenter; - justify-content: flex-start; - flex-direction: column; - border-radius: $s-8; - h3 { - @include bodySmallTypography; - font-size: $fs-24; - width: 100%; - } - .component { - display: flex; - flex-direction: column; - gap: $s-8; - width: $s-240; - max-height: $s-80; - margin-bottom: $s-16; - .component-name { - @include uppercaseTitleTipography; - font-weight: bold; - } - } - } - .button-primary { - @extend .button-primary; - height: $s-32; - svg { - @extend .button-icon; - } - } - .button-secondary { - @extend .button-secondary; - height: $s-32; - svg { - @extend .button-icon; - } - } - .button-tertiary { - @extend .button-tertiary; - height: $s-32; - svg { - @extend .button-icon; - } - } - .input-wrapper { - @extend .input-element; - } - } -} diff --git a/frontend/src/app/main/ui/debug/icons_preview.cljs b/frontend/src/app/main/ui/debug/icons_preview.cljs new file mode 100644 index 0000000000000000000000000000000000000000..9380dc872679d8983feb779110b453bea65b064a --- /dev/null +++ b/frontend/src/app/main/ui/debug/icons_preview.cljs @@ -0,0 +1,54 @@ +(ns app.main.ui.debug.icons-preview + (:require-macros [app.main.style :as stl]) + (:require + [app.main.ui.cursors :as c] + [app.main.ui.icons :as i] + [app.util.timers :as ts] + [cuerdas.core :as str] + [rumext.v2 :as mf])) + +(mf/defc icons-gallery + {::mf/wrap-props false + ::mf/private true} + [] + (let [entries (->> (seq (js/Object.entries i/default)) + (sort-by first))] + [:section {:class (stl/css :gallery)} + (for [[key val] entries] + [:div {:class (stl/css :gallery-item) + :key key + :title key} + val + [:span key]])])) + +(mf/defc cursors-gallery + {::mf/wrap-props false + ::mf/private true} + [] + (let [rotation (mf/use-state 0) + entries (->> (seq (js/Object.entries c/default)) + (sort-by first))] + + (mf/with-effect [] + (ts/interval 100 #(reset! rotation inc))) + + [:section {:class (stl/css :gallery)} + (for [[key value] entries] + (let [value (if (fn? value) (value @rotation) value)] + [:div {:key key :class (stl/css :gallery-item)} + [:div {:class (stl/css :cursor) + :style {:background-image (-> value (str/replace #"(url\(.*\)).*" "$1")) + :cursor value}}] + + [:span (pr-str key)]]))])) + + +(mf/defc icons-preview + {::mf/wrap-props false} + [] + [:article {:class (stl/css :container)} + [:h2 {:class (stl/css :title)} "Cursors"] + [:& cursors-gallery] + [:h2 {:class (stl/css :title)} "Icons"] + [:& icons-gallery]]) + diff --git a/frontend/src/app/main/ui/debug/icons_preview.scss b/frontend/src/app/main/ui/debug/icons_preview.scss new file mode 100644 index 0000000000000000000000000000000000000000..673c1b0658982bf12070e6adafc1c8b61b596a12 --- /dev/null +++ b/frontend/src/app/main/ui/debug/icons_preview.scss @@ -0,0 +1,51 @@ +@use "common/refactor/common-refactor.scss" as *; + +.container { + display: grid; + row-gap: 1rem; + height: 100vh; + overflow-y: auto; + padding: 1rem; +} + +.title { + @include bigTitleTipography; + color: var(--color-foreground-primary); +} + +.gallery { + display: grid; + grid-template-columns: repeat(auto-fill, 120px); + grid-template-rows: repeat(auto-fill, 120px); + gap: 1rem; + + --cell-size: 64px; +} + +.gallery-item { + display: grid; + place-items: center; + row-gap: 0.5rem; + grid-template-rows: var(--cell-size) 1fr; + padding: 0.5rem; + + color: var(--color-foreground-primary); + word-break: break-word; + @include bodySmallTypography; + + svg { + width: var(--cell-size); + height: var(--cell-size); + fill: none; + color: transparent; + stroke: var(--color-accent-primary); + } +} + +.cursor { + width: var(--cell-size); + height: var(--cell-size); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs new file mode 100644 index 0000000000000000000000000000000000000000..ff1a3ca83b51c99358b21d522cbb0b5b2531f82a --- /dev/null +++ b/frontend/src/app/main/ui/ds.cljs @@ -0,0 +1,48 @@ +;; 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 app.main.ui.ds + (:require + [app.config :as cf] + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.controls.input :refer [input*]] + [app.main.ui.ds.controls.select :refer [select*]] + [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]] + [app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg* raw-svg-list]] + [app.main.ui.ds.foundations.typography :refer [typography-list]] + [app.main.ui.ds.foundations.typography.heading :refer [heading*]] + [app.main.ui.ds.foundations.typography.text :refer [text*]] + [app.main.ui.ds.notifications.toast :refer [toast*]] + [app.main.ui.ds.product.loader :refer [loader*]] + [app.main.ui.ds.storybook :as sb] + [app.main.ui.ds.tab-switcher :refer [tab-switcher*]] + [app.util.i18n :as i18n])) + + +(i18n/init! cf/translations) + +(def default + "A export used for storybook" + #js {:Button button* + :Heading heading* + :Icon icon* + :IconButton icon-button* + :Input input* + :Loader loader* + :RawSvg raw-svg* + :Select select* + :Text text* + :TabSwitcher tab-switcher* + :Toast toast* + ;; meta / misc + :meta #js {:icons (clj->js (sort icon-list)) + :svgs (clj->js (sort raw-svg-list)) + :typography (clj->js typography-list)} + :storybook #js {:StoryGrid sb/story-grid* + :StoryGridCell sb/story-grid-cell* + :StoryGridRow sb/story-grid-row* + :StoryHeader sb/story-header*}}) diff --git a/frontend/src/app/main/ui/ds/_borders.scss b/frontend/src/app/main/ui/ds/_borders.scss new file mode 100644 index 0000000000000000000000000000000000000000..e8a8560748b33114c0c0b8ddfe11c4a2c6e9c07d --- /dev/null +++ b/frontend/src/app/main/ui/ds/_borders.scss @@ -0,0 +1,13 @@ +// 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 + +@use "./utils.scss" as *; + +// TODO: create actual tokens once we have them from design +$br-8: px2rem(8); +$br-circle: 50%; + +$b-1: px2rem(1); diff --git a/frontend/src/app/main/ui/ds/_sizes.scss b/frontend/src/app/main/ui/ds/_sizes.scss new file mode 100644 index 0000000000000000000000000000000000000000..63ad1f93bde8ab1fba7f0e5250cf7aeecdfafdb0 --- /dev/null +++ b/frontend/src/app/main/ui/ds/_sizes.scss @@ -0,0 +1,14 @@ +// 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 + +@use "./utils.scss" as *; + +// TODO: create actual tokens once we have them from design +$sz-16: px2rem(16); +$sz-32: px2rem(32); +$sz-36: px2rem(36); +$sz-224: px2rem(224); +$sz-400: px2rem(400); diff --git a/frontend/src/app/main/ui/loader.scss b/frontend/src/app/main/ui/ds/_utils.scss similarity index 68% rename from frontend/src/app/main/ui/loader.scss rename to frontend/src/app/main/ui/ds/_utils.scss index 71121f51d910a02b6e5239f128c2e1ecca5637ba..248d43d002351ef0e1247d093285571a44d2e5ec 100644 --- a/frontend/src/app/main/ui/loader.scss +++ b/frontend/src/app/main/ui/ds/_utils.scss @@ -4,8 +4,9 @@ // // Copyright (c) KALEIDOS INC -@import "refactor/common-refactor.scss"; +@use "sass:math"; -.loader-content { - @extend .loader-base; +@function px2rem($value) { + $remValue: math.div($value, 16) * 1rem; + @return $remValue; } diff --git a/frontend/src/app/main/ui/ds/buttons/_buttons.scss b/frontend/src/app/main/ui/ds/buttons/_buttons.scss new file mode 100644 index 0000000000000000000000000000000000000000..b489b4df9740dd1c441ee8b78c2456b867aa1a92 --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/_buttons.scss @@ -0,0 +1,132 @@ +// 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 + +@use "../_borders.scss" as *; +@use "../_sizes.scss" as *; +@use "../utils.scss" as *; + +%base-button { + --button-bg-color: initial; + --button-fg-color: initial; + --button-hover-bg-color: initial; + --button-hover-fg-color: initial; + --button-active-bg-color: initial; + --button-disabled-bg-color: initial; + --button-disabled-fg-color: initial; + --button-border-color: var(--button-bg-color); + --button-focus-inner-ring-color: initial; + --button-focus-outer-ring-color: initial; + + appearance: none; + height: $sz-32; + border: none; + border-radius: $br-8; + + background: var(--button-bg-color); + color: var(--button-fg-color); + border: $b-1 solid var(--button-border-color); + + &:hover { + --button-bg-color: var(--button-hover-bg-color); + --button-fg-color: var(--button-hover-fg-color); + } + + &:active { + --button-bg-color: var(--button-active-bg-color); + } + + &:focus-visible { + outline: var(--button-focus-inner-ring-color) solid #{px2rem(2)}; + outline-offset: -#{px2rem(3)}; + --button-border-color: var(--button-focus-outer-ring-color); + --button-fg-color: var(--button-focus-fg-color); + } + + &:disabled { + --button-bg-color: var(--button-disabled-bg-color); + --button-fg-color: var(--button-disabled-fg-color); + } +} + +%base-button-primary { + --button-bg-color: var(--color-accent-primary); + --button-fg-color: var(--color-background-secondary); + + --button-hover-bg-color: var(--color-accent-tertiary); + --button-hover-fg-color: var(--color-background-secondary); + + --button-active-bg-color: var(--color-accent-tertiary); + + --button-disabled-bg-color: var(--color-accent-primary-muted); + --button-disabled-fg-color: var(--color-background-secondary); + + --button-focus-bg-color: var(--color-accent-primary); + --button-focus-fg-color: var(--color-background-secondary); + --button-focus-inner-ring-color: var(--color-background-secondary); + --button-focus-outer-ring-color: var(--color-accent-primary); + + &:active { + box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2); + } +} + +%base-button-secondary { + --button-bg-color: var(--color-background-tertiary); + --button-fg-color: var(--color-foreground-secondary); + + --button-hover-bg-color: var(--color-background-tertiary); + --button-hover-fg-color: var(--color-accent-primary); + + --button-active-bg-color: var(--color-background-quaternary); + + --button-disabled-bg-color: transparent; + --button-disabled-fg-color: var(--color-foreground-secondary); + + --button-focus-bg-color: var(--color-background-tertiary); + --button-focus-fg-color: var(--color-foreground-primary); + --button-focus-inner-ring-color: var(--color-background-secondary); + --button-focus-outer-ring-color: var(--color-accent-primary); +} + +%base-button-ghost { + --button-bg-color: transparent; + --button-fg-color: var(--color-foreground-secondary); + + --button-hover-bg-color: var(--color-background-tertiary); + --button-hover-fg-color: var(--color-accent-primary); + + --button-active-bg-color: var(--color-background-quaternary); + + --button-disabled-bg-color: transparent; + --button-disabled-fg-color: var(--color-accent-primary-muted); + + --button-focus-bg-color: transparent; + --button-focus-fg-color: var(--color-foreground-secondary); + --button-focus-inner-ring-color: transparent; + --button-focus-outer-ring-color: var(--color-accent-primary); +} + +%base-button-destructive { + --button-bg-color: var(--color-accent-error); + --button-fg-color: var(--color-foreground-primary); + + --button-hover-bg-color: var(--color-background-error); + --button-hover-fg-color: var(--color-foreground-primary); + + --button-active-bg-color: var(--color-accent-error); + + --button-disabled-bg-color: var(--color-background-error); + --button-disabled-fg-color: var(--color-accent-error); + + --button-focus-bg-color: var(--color-accent-error); + --button-focus-fg-color: var(--color-foreground-primary); + --button-focus-inner-ring-color: var(--color-background-primary); + --button-focus-outer-ring-color: var(--color-accent-primary); + + &:active { + box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2); + } +} diff --git a/frontend/src/app/main/ui/ds/buttons/button.cljs b/frontend/src/app/main/ui/ds/buttons/button.cljs new file mode 100644 index 0000000000000000000000000000000000000000..9dfb2c9b42d3a7abf2ceb54099f226e5c28648bd --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/button.cljs @@ -0,0 +1,31 @@ +;; 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 app.main.ui.ds.buttons.button + (:require-macros + [app.common.data.macros :as dm] + [app.main.style :as stl]) + (:require + [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]] + [rumext.v2 :as mf])) + +(def button-variants (set '("primary" "secondary" "ghost" "destructive"))) + +(mf/defc button* + {::mf/props :obj} + [{:keys [variant icon children class] :rest props}] + (assert (or (nil? variant) (contains? button-variants variant) "expected valid variant")) + (assert (or (nil? icon) (contains? icon-list icon) "expected valid icon id")) + (let [variant (or variant "primary") + class (dm/str class " " (stl/css-case :button true + :button-primary (= variant "primary") + :button-secondary (= variant "secondary") + :button-ghost (= variant "ghost") + :button-destructive (= variant "destructive"))) + props (mf/spread-props props {:class class})] + [:> "button" props + (when icon [:> icon* {:id icon :size "m"}]) + [:span {:class (stl/css :label-wrapper)} children]])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/ds/buttons/button.scss b/frontend/src/app/main/ui/ds/buttons/button.scss new file mode 100644 index 0000000000000000000000000000000000000000..5e7b2cfe63a9f7e2135bc2c2e56ace9a2b76078e --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/button.scss @@ -0,0 +1,35 @@ +// 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 + +@use "../typography.scss" as *; +@use "./buttons" as *; + +.button { + @extend %base-button; + + @include use-typography("headline-small"); + padding: 0 var(--sp-m); + + display: inline-flex; + align-items: center; + column-gap: var(--sp-xs); +} + +.button-primary { + @extend %base-button-primary; +} + +.button-secondary { + @extend %base-button-secondary; +} + +.button-ghost { + @extend %base-button-ghost; +} + +.button-destructive { + @extend %base-button-destructive; +} diff --git a/frontend/src/app/main/ui/ds/buttons/button.stories.jsx b/frontend/src/app/main/ui/ds/buttons/button.stories.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d41e12c2de5f217689f27d1b68815021b4a8abee --- /dev/null +++ b/frontend/src/app/main/ui/ds/buttons/button.stories.jsx @@ -0,0 +1,68 @@ +// 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 + +import * as React from "react"; +import Components from "@target/components"; + +const { Button } = Components; +const { icons } = Components.meta; + +export default { + title: "Buttons/Button", + component: Components.Button, + argTypes: { + icon: { + options: icons, + control: { type: "select" }, + }, + disabled: { control: "boolean" }, + variant: { + options: ["primary", "secondary", "ghost", "destructive"], + control: { type: "select" }, + }, + }, + args: { + children: "Lorem ipsum", + disabled: false, + variant: undefined, + }, + parameters: { + controls: { exclude: ["children"] }, + }, + render: ({ ...args }) =>