diff --git a/.circleci/config.yml b/.circleci/config.yml index 70ba3b801..09cc658e4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -22,10 +22,10 @@ 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 @@ -58,6 +58,7 @@ jobs: command: | yarn install yarn run fmt:clj:check + yarn run fmt:js:check - run: name: "common linter check" @@ -107,8 +108,8 @@ jobs: working_directory: "./frontend" command: | yarn install - yarn run compile - yarn run compile:cljs + yarn run build:app:assets + clojure -M:dev:shadow-cljs release main yarn playwright install --with-deps chromium yarn e2e:test @@ -125,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 fe1d14d90..5675b9d5d 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 a209cf018..50e617de5 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 38cfeb89b..02c567b2e 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 caf638f38..b0b2074d8 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/*/ @@ -68,7 +70,6 @@ /web clj-profiler/ node_modules -frontend/.storybook/preview-body.html /test-results/ /playwright-report/ /blob-report/ diff --git a/CHANGES.md b/CHANGES.md index 17f333222..ea160e12e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,6 @@ # CHANGELOG -## 2.1.0 +## 2.2.0 ### :rocket: Epics and highlights @@ -9,18 +9,87 @@ ### :heart: Community contributions (Thank you!) ### :sparkles: New features -- Improve auth process [Taiga #Change Auth Process](https://tree.taiga.io/project/penpot/us/7094) ### :bug: Bugs fixed +- Fix components are not dragged from the group to the assets tab [Taiga #8273](https://tree.taiga.io/project/penpot/issue/8273) + +## 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 Issue #7852](https://tree.taiga.io/project/penpot/issue/7852) +- 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 @@ -30,7 +99,7 @@ ### :bug: Bugs fixed -- Fix color palette sorting [Taiga Issue #7458](https://tree.taiga.io/project/penpot/issue/7458) +- 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) @@ -175,7 +244,7 @@ - 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 Issue #7421](https://tree.taiga.io/project/penpot/issue/7421) +- Fix long text on tab breaks UI [Taiga #7421](https://tree.taiga.io/project/penpot/issue/7421) ## 1.19.5 diff --git a/README.md b/README.md index 52171b8c9..848b3efd1 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,7 @@ Penpot’s latest [huge release 2.0](https://penpot.app/dev-diaries), takes the - [Why Penpot](#why-penpot) - [Getting Started](#getting-started) - [Community](#community) +- [Contributing](#contributing) - [Resources](#resources) - [License](#license) diff --git a/THANKYOU.md b/THANKYOU.md index 1a27aa8eb..8c075112b 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 df9fd4708..a88402829 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-alpha9"} + 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"} @@ -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.925"} - metosin/reitit-core {:mvn/version "0.6.0"} - nrepl/nrepl {:mvn/version "1.1.1"} - cider/cider-nrepl {:mvn/version "0.47.1"} + 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.3"} - org.xerial/sqlite-jdbc {:mvn/version "3.45.2.0"} + org.xerial/sqlite-jdbc {:mvn/version "3.46.0.0"} com.zaxxer/HikariCP {:mvn/version "5.1.0"} @@ -58,7 +58,7 @@ ;; 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,13 +74,13 @@ :build {:extra-deps - {io.github.clojure/tools.build {:git/tag "v0.10.0" :git/sha "3a2c484"}} + {io.github.clojure/tools.build {:git/tag "v0.10.3" :git/sha "15ead66"}} :ns-default build} :test {:main-opts ["-m" "kaocha.runner"] :jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"] - :extra-deps {lambdaisland/kaocha {:mvn/version "1.88.1376"}}} + :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}} :outdated {:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}} diff --git a/backend/package.json b/backend/package.json index 9efdad02c..0855bf4eb 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.2.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 ad95fa641..d63efa72f 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 0a688cb0d..09d6e84a5 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 478a3cc3c..6de9cda62 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 881af47f4..93763c106 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 ea85c084f..55e61d8e2 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/password-recovery/en.html b/backend/resources/app/email/password-recovery/en.html index 14fe1a5f2..ed18ef12c 100644 --- a/backend/resources/app/email/password-recovery/en.html +++ b/backend/resources/app/email/password-recovery/en.html @@ -168,7 +168,7 @@
-
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 }}”.
@@ -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 ad314b41d..3bac8f815 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 4a425b69f..3f058b184 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 41a9bd8d9..c38454ccd 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/onboarding.edn b/backend/resources/app/onboarding.edn index 3a94d29ed..a6449f5fd 100644 --- a/backend/resources/app/onboarding.edn +++ b/backend/resources/app/onboarding.edn @@ -1,4 +1,16 @@ -[{:id "tutorial-for-beginners" +[{: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 "lucide-icons" @@ -7,12 +19,6 @@ {:id "font-awesome" :name "Font Awesome" :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Font-Awesome.penpot"} - {:id "plants-app" - :name "Plants app" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/Plants-app.penpot"} - {:id "wireframing-kit" - :name "Wireframing Kit" - :file-uri "https://github.com/penpot/penpot-files/raw/binary-files/wireframing-kit.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"} diff --git a/backend/resources/app/templates/api-doc-entry.tmpl b/backend/resources/app/templates/api-doc-entry.tmpl index 31c48deeb..9123233a8 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/scripts/repl b/backend/scripts/repl index 057018a11..0debeece2 100755 --- a/backend/scripts/repl +++ b/backend/scripts/repl @@ -24,6 +24,7 @@ export PENPOT_FLAGS="\ enable-rpc-climit \ enable-rpc-rlimit \ enable-soft-rpc-rlimit \ + enable-file-snapshot \ enable-webhooks \ enable-access-tokens \ enable-file-validation \ diff --git a/backend/scripts/start-dev b/backend/scripts/start-dev index 55f5e835a..564151bef 100755 --- a/backend/scripts/start-dev +++ b/backend/scripts/start-dev @@ -17,6 +17,7 @@ export PENPOT_FLAGS="\ disable-secure-session-cookies \ enable-rpc-climit \ enable-smtp \ + enable-file-snapshot \ enable-access-tokens \ enable-file-validation \ enable-file-schema-validation"; diff --git a/backend/src/app/auth.clj b/backend/src/app/auth.clj index 5bde8aa79..fc6d25481 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 5100abff9..c430a794d 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 3caeb9877..824f8a937 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.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,6 +35,7 @@ [clojure.spec.alpha :as s] [cuerdas.core :as str] [integrant.core :as ig] + [ring.request :as rreq] [ring.response :as-alias rres])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -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") @@ -322,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] @@ -368,9 +380,9 @@ (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))) + (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))} @@ -378,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 @@ -418,12 +430,6 @@ (defn- get-info [{:keys [::provider ::setup/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))) - (let [state (get params :state) code (get params :code) state (tokens/verify props {:token state :iss :oauth}) @@ -436,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) @@ -469,6 +475,9 @@ (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)) @@ -553,24 +562,32 @@ (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)}))] + (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 "command" + (audit/submit! cfg {::audit/type "action" ::audit/name "login-with-oidc" ::audit/profile-id (:id profile) - ::audit/ip-addr (audit/parse-client-ip request) - ::audit/props (audit/profile->props profile)}) + ::audit/ip-addr (inet/parse-request request) + ::audit/props props + ::audit/context context}) (->> (redirect-to-verify-token token) (sxf request)))) - (not (auth/email-domain-in-whitelist? (:email info))) + (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 @@ -579,26 +596,50 @@ (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 (::setup/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)] - (process-callback 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/err :hint "error on oauth process" :cause 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 @@ -614,17 +655,17 @@ :provider provider :hint "provider not configured"))))))}) -(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) +(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 ::provider (s/keys :req-un [::client-id diff --git a/backend/src/app/binfile/common.clj b/backend/src/app/binfile/common.clj index bfbfe6186..d8c381174 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] diff --git a/backend/src/app/binfile/v1.clj b/backend/src/app/binfile/v1.clj index 5bad01f6d..3e1c93aa0 100644 --- a/backend/src/app/binfile/v1.clj +++ b/backend/src/app/binfile/v1.clj @@ -130,7 +130,6 @@ (.writeLong output (long data)) (swap! *position* + 8)) - (defn read-long! [^DataInputStream input] (let [v (.readLong input)] diff --git a/backend/src/app/config.clj b/backend/src/app/config.clj index 5e490b676..d1315d48b 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" + :file-snapshot-total 10 + :file-snapshot-every 5 + :file-snapshot-timeout "3h" :public-uri "http://localhost:3449" :host "localhost" @@ -87,249 +75,148 @@ :ldap-attrs-fullname "cn" ;; a server prop key where initial project is stored. - :initial-project-skey "initial-project"}) + :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) + ;; time to avoid email sending after profile modification + :email-verify-threshold "15m"}) -(s/def ::media-max-file-size ::us/integer) +(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] -(s/def ::flags ::us/vector-of-keywords) -(s/def ::telemetry-enabled ::us/boolean) + [:tenant {:optional false} :string] + [:public-uri {:optional false} :string] + [:host {:optional false} :string] -(s/def ::audit-log-archive-uri ::us/string) -(s/def ::audit-log-http-handler-concurrency ::us/integer) + [:http-server-port {:optional true} :int] + [:http-server-host {:optional true} :string] + [:http-server-max-body-size {:optional true} :int] + [:http-server-max-multipart-body-size {:optional true} :int] + [:http-server-io-threads {:optional true} :int] + [:http-server-worker-threads {:optional true} :int] -(s/def ::deletion-delay ::dt/duration) + [:telemetry-uri {:optional true} :string] + [:telemetry-with-taiga {:optional true} :boolean] ;; DELETE -(s/def ::admins ::us/set-of-valid-emails) -(s/def ::file-change-snapshot-every ::us/integer) -(s/def ::file-change-snapshot-timeout ::dt/duration) + [:file-snapshot-total {:optional true} :int] + [:file-snapshot-every {:optional true} :int] + [:file-snapshot-timeout {:optional true} ::dt/duration] -(s/def ::default-executor-parallelism ::us/integer) -(s/def ::scheduled-executor-parallelism ::us/integer) + [:media-max-file-size {:optional true} :int] + [:deletion-delay {:optional true} ::dt/duration] ;; REVIEW + [:telemetry-enabled {:optional true} :boolean] + [:default-blob-version {:optional true} :int] + [:allow-demo-users {:optional true} :boolean] + [:error-report-webhook {:optional true} :string] + [:user-feedback-destination {:optional true} :string] -(s/def ::worker-default-parallelism ::us/integer) -(s/def ::worker-webhook-parallelism ::us/integer) + [:default-rpc-rlimit {:optional true} [::sm/vec :string]] + [:rpc-rlimit-config {:optional true} ::fs/path] + [:rpc-climit-config {:optional true} ::fs/path] -(s/def ::auth-data-cookie-domain ::us/string) -(s/def ::auth-token-cookie-name ::us/string) -(s/def ::auth-token-cookie-max-age ::dt/duration) + [:audit-log-archive-uri {:optional true} :string] + [:audit-log-http-handler-concurrency {:optional true} :int] -(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) + [:default-executor-parallelism {:optional true} :int] ;; REVIEW + [:scheduled-executor-parallelism {:optional true} :int] ;; REVIEW + [:worker-default-parallelism {:optional true} :int] + [:worker-webhook-parallelism {:optional true} :int] -(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) + [:database-password {:optional true} [:maybe :string]] + [:database-uri {:optional true} :string] + [:database-username {:optional true} [:maybe :string]] + [:database-readonly {:optional true} :boolean] + [:database-min-pool-size {:optional true} :int] + [:database-max-pool-size {:optional true} :int] -(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) + [:quotes-teams-per-profile {:optional true} :int] + [:quotes-access-tokens-per-profile {:optional true} :int] + [:quotes-projects-per-team {:optional true} :int] + [:quotes-invitations-per-team {:optional true} :int] + [:quotes-profiles-per-team {:optional true} :int] + [:quotes-files-per-project {:optional true} :int] + [:quotes-files-per-team {:optional true} :int] + [:quotes-font-variants-per-team {:optional true} :int] + [:quotes-comment-threads-per-file {:optional true} :int] + [:quotes-comments-per-file {:optional true} :int] -(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) + [:auth-data-cookie-domain {:optional true} :string] + [:auth-token-cookie-name {:optional true} :string] + [:auth-token-cookie-max-age {:optional true} ::dt/duration] -(s/def ::config - (s/keys :opt-un [::secret-key - ::flags - ::admins - ::deletion-delay - ::allow-demo-users - ::audit-log-archive-uri - ::audit-log-http-handler-concurrency - ::auth-token-cookie-name - ::auth-token-cookie-max-age - ::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 + [:registration-domain-whitelist {:optional true} [::sm/set :string]] + [:email-verify-threshold {:optional true} ::dt/duration] - ::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 + [: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] - ::redis-uri - ::registration-domain-whitelist - ::rpc-rlimit-config - ::rpc-climit-config + [: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} :int] + [:ldap-ssl {:optional true} :boolean] + [:ldap-starttls {:optional true} :boolean] + [:ldap-user-query {:optional true} :string] - ::semaphore-process-font - ::semaphore-process-image - ::semaphore-update-file - ::semaphore-auth + [:profile-bounce-max-age {:optional true} ::dt/duration] + [:profile-bounce-threshold {:optional true} :int] + [:profile-complaint-max-age {:optional true} ::dt/duration] + [:profile-complaint-threshold {:optional true} :int] - ::smtp-default-from - ::smtp-default-reply-to - ::smtp-host - ::smtp-password - ::smtp-port - ::smtp-ssl - ::smtp-tls - ::smtp-username + [:redis-uri {:optional true} :string] - ::urepl-host - ::urepl-port - ::prepl-host - ::prepl-port + [:email-domain-blacklist {:optional true} ::fs/path] + [:email-domain-whitelist {:optional true} ::fs/path] - ::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])) + [: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} :int] + [:smtp-ssl {:optional true} :boolean] + [:smtp-tls {:optional true} :boolean] + [:smtp-username {:optional true} [:maybe :string]] + + [:urepl-host {:optional true} :string] + [:urepl-port {:optional true} :int] + [:prepl-host {:optional true} :string] + [:prepl-port {:optional true} :int] + + [:assets-storage-backend {:optional true} :keyword] + [:media-directory {:optional true} :string] ;; REVIEW + [:media-uri {:optional true} :string] + [:assets-path {:optional true} :string] + + [: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} :int]])) (def default-flags [:enable-backend-api-doc @@ -357,20 +244,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/default-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") @@ -378,10 +267,28 @@ (str/trim)) "%version%"))) -(defonce ^:dynamic config (read-config)) +(defonce ^:dynamic config (read-config :default default)) (defonce ^:dynamic flags (parse-flags config)) -(def deletion-delay +(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}))) diff --git a/backend/src/app/email.clj b/backend/src/app/email.clj index 2cc47a37a..03228e45b 100644 --- a/backend/src/app/email.clj +++ b/backend/src/app/email.clj @@ -7,9 +7,11 @@ (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] @@ -149,9 +151,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} :boolean] + [::ssl {:optional true} :boolean] + [::host {:optional true} :string] + [::port {:optional true} :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 +282,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] @@ -449,3 +446,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 000000000..ca80afb6c --- /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 000000000..85c137bfb --- /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/http.clj b/backend/src/app/http.clj index a696d5477..672d1ec60 100644 --- a/backend/src/app/http.clj +++ b/backend/src/app/http.clj @@ -114,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") @@ -151,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/awsns.clj b/backend/src/app/http/awsns.clj index 88060bb20..77ae6c5d6 100644 --- a/backend/src/app/http/awsns.clj +++ b/backend/src/app/http/awsns.clj @@ -9,6 +9,7 @@ (: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] @@ -16,10 +17,10 @@ [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])) @@ -136,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)})) + (catch Throwable cause + (l/warn :hint "unable to persist profile complaint" + :cause cause))) - (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)}) - - ;; 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 9ef4cc4b2..4494a1bb0 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)] + (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)] + (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/errors.clj b/backend/src/app/http/errors.clj index 14f4cb223..8101db116 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))) diff --git a/backend/src/app/http/middleware.clj b/backend/src/app/http/middleware.clj index 4ea815f07..f70e102ad 100644 --- a/backend/src/app/http/middleware.clj +++ b/backend/src/app/http/middleware.clj @@ -10,16 +10,14 @@ [app.common.logging :as l] [app.common.transit :as t] [app.config :as cf] - [app.util.json :as json] + [app.http.errors :as errors] + [clojure.data.json :as json] [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 +32,22 @@ {: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- read-json-key + [k] + (-> k str/kebab keyword)) + +(defn- write-json-key + [k] + (if (or (keyword? k) (symbol? k)) + (str/camel k) + (str k))) (defn wrap-parse-request [handler] @@ -53,8 +62,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 read-json-key)] (-> request (assoc :body-params params) (update :params merge params)))) @@ -62,35 +71,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 @@ -128,7 +135,8 @@ (-write-body-to-stream [_ _ output-stream] (try (with-open [^OutputStream bos (buffered-output-stream output-stream buffer-size)] - (json/write! bos data json-mapper)) + (with-open [^java.io.OutputStreamWriter writer (java.io.OutputStreamWriter. bos)] + (json/write data writer :key-fn write-json-key))) (catch java.io.IOException _) (catch Throwable cause diff --git a/backend/src/app/http/sse.clj b/backend/src/app/http/sse.clj index 868801091..3da84322c 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 b9fa191f5..c0ca61da9 100644 --- a/backend/src/app/loggers/audit.clj +++ b/backend/src/app/loggers/audit.clj @@ -21,24 +21,18 @@ [app.rpc :as-alias rpc] [app.rpc.retry :as rtry] [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] - [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." @@ -86,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 @@ -140,25 +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 (-> (::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))] + (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 @@ -180,15 +191,33 @@ (::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) @@ -197,12 +226,8 @@ ;; this case we just retry the operation. (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)))] + (append-audit-entry! cfg params))) (when (and (or (contains? cf/flags :telemetry) (cf/get :telemetry-enabled)) @@ -214,12 +239,10 @@ ;; NOTE: this is only executed when general audit log is disabled (let [params (-> params (assoc :created-at tnow) - (assoc :tracked-at tnow) - (assoc :props (db/tjson {})) - (assoc :context (db/tjson {})) - (assoc :ip-addr (db/inet "0.0.0.0")) - (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)) @@ -232,25 +255,23 @@ :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 :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) @@ -259,3 +280,18 @@ (rtry/invoke! cfg db/tx-run! handle-event! event)) (catch Throwable cause (l/error :hint "unexpected error processing event" :cause cause)))) + +(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/webhooks.clj b/backend/src/app/loggers/webhooks.clj index 5f13bc55b..cd6385429 100644 --- a/backend/src/app/loggers/webhooks.clj +++ b/backend/src/app/loggers/webhooks.clj @@ -64,22 +64,22 @@ (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)] + (let [event (:event props)] (l/dbg :hint "process webhook event" :name (:name event)) (when-let [items (lookup-webhooks cfg event)] (l/trc :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))))))) + (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 event + :config item})))))))))) ;; --- RUN @@ -128,8 +128,8 @@ :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/write-str event json-write-opts) diff --git a/backend/src/app/main.clj b/backend/src/app/main.clj index 023783828..ee58a21b5 100644 --- a/backend/src/app/main.clj +++ b/backend/src/app/main.clj @@ -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" @@ -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)} @@ -267,7 +267,9 @@ :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) @@ -322,7 +324,10 @@ ::rpc/climit (ig/ref ::rpc/climit) ::rpc/rlimit (ig/ref ::rpc/rlimit) ::setup/templates (ig/ref ::setup/templates) - ::setup/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)} @@ -338,7 +343,6 @@ ::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-xlog-gc (ig/ref :app.tasks.file-xlog-gc/handler) :tasks-gc (ig/ref :app.tasks.tasks-gc/handler) @@ -356,6 +360,12 @@ :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) @@ -377,9 +387,6 @@ {::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)} @@ -468,9 +475,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} @@ -520,6 +524,7 @@ (defn start [] + (cf/validate!) (ig/load-namespaces (merge system-config worker-config)) (alter-var-root #'system (fn [sys] (when sys (ig/halt! sys)) diff --git a/backend/src/app/media.clj b/backend/src/app/media.clj index 8d2315352..9e1a120fe 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,19 +46,7 @@ (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] diff --git a/backend/src/app/rpc.clj b/backend/src/app/rpc.clj index 89eee548d..09fff7b89 100644 --- a/backend/src/app/rpc.clj +++ b/backend/src/app/rpc.clj @@ -29,6 +29,7 @@ [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] @@ -70,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." @@ -79,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) @@ -188,10 +213,10 @@ (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) diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj index 06a6e516c..e8d9675f9 100644 --- a/backend/src/app/rpc/commands/access_token.clj +++ b/backend/src/app/rpc/commands/access_token.clj @@ -6,7 +6,7 @@ (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] @@ -16,8 +16,7 @@ [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] @@ -44,7 +43,7 @@ :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)] @@ -53,16 +52,14 @@ 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)] @@ -72,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 5db758b46..f43195dd7 100644 --- a/backend/src/app/rpc/commands/audit.clj +++ b/backend/src/app/rpc/commands/audit.clj @@ -14,11 +14,12 @@ [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.inet :as inet] [app.util.services :as sv] [app.util.time :as dt])) @@ -61,7 +62,7 @@ (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 (fn [event] @@ -77,10 +78,19 @@ (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 586c60d1c..ff8bfdb8f 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,6 +16,8 @@ [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.rpc :as-alias rpc] @@ -37,13 +38,11 @@ (def schema:token [::sm/word-string {:max 6000}]) -(def ^:private default-verify-threshold - (dt/duration "15m")) - (defn- elapsed-verify-threshold? [profile] - (let [elapsed (dt/diff (:modified-at profile) (dt/now))] - (pos? (compare elapsed default-verify-threshold)))) + (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 @@ -129,12 +128,21 @@ ;; ---- 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 @@ -186,8 +194,14 @@ :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)) + + (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 @@ -195,7 +209,19 @@ (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")) + + (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")) + + (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}] @@ -272,14 +298,17 @@ (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}] @@ -326,7 +355,7 @@ profile (if-let [profile-id (:profile-id claims)] (profile/get-profile conn profile-id) - (let [is-active (or (boolean (:is-active params)) + (let [is-active (or (boolean (:is-active claims)) (not (contains? cf/flags :email-verification))) params (-> params (assoc :is-active is-active) @@ -334,6 +363,9 @@ (->> (create-profile! conn params) (create-profile-rels! conn)))) + ;; When no profile-id comes on claims means a new register + created? (not (:profile-id claims)) + invitation (when-let [token (:invitation-token params)] (tokens/verify (::setup/props cfg) {:token token :iss :team-invitation})) @@ -371,8 +403,8 @@ ;; 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 - (not (:profile-id claims)) - (if (:is-active claims) + created? + (if (:is-active profile) (-> (profile/strip-private-attrs profile) (rph/with-transform (session/create-fn cfg (:id profile))) (rph/with-meta @@ -381,20 +413,22 @@ ::audit/profile-id (:id profile)})) (do - (send-email-verification! cfg profile) + (when-not (eml/has-reports? conn (:email profile)) + (send-email-verification! cfg profile)) + (rph/with-meta {:email (:email profile)} {::audit/replace-props props ::audit/context {:action "email-verification"} ::audit/profile-id (:id profile)}))) :else - (let [elapsed? (elapsed-verify-threshold? profile) - bounce? (eml/has-bounce-reports? conn (:email profile)) - action (if bounce? - "ignore-because-bounce" - (if elapsed? - "resend-email-verification" - "ignore"))] + (let [elapsed? (elapsed-verify-threshold? profile) + complaints? (eml/has-reports? conn (:email profile)) + action (if complaints? + "ignore-because-complaints" + (if elapsed? + "resend-email-verification" + "ignore"))] (l/wrn :hint "repeated registry detected" :profile-id (str (:id profile)) @@ -423,15 +457,13 @@ ::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}] + [{:keys [::db/conn] :as cfg} {:keys [email] :as params}] (letfn [(create-recovery-token [{:keys [id] :as profile}] (let [token (tokens/generate (::setup/props cfg) {:iss :password-recovery @@ -453,39 +485,42 @@ :extra-data ptoken}) nil))] - (db/with-atomic [conn pool] - (let [profile (->> (profile/clean-email email) - (profile/get-profile-by-email 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) + (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)) + (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-bounce-reports? conn (:email profile)) + (l/wrn :hint "attempt of profile recovery: email has bounces" + :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)) + (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)))))))) + :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 @@ -497,6 +532,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 d6759eb42..6b2b69c90 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." @@ -78,11 +77,10 @@ (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 4949f1a43..41645a8be 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 3dabb96fb..059548f22 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 7d2ab1c88..29b79a87b 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 dc48abd6e..6c9ac43c8 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] @@ -36,7 +35,6 @@ [app.util.services :as sv] [app.util.time :as dt] [app.worker :as wrk] - [clojure.spec.alpha :as s] [cuerdas.core :as str])) ;; --- FEATURES @@ -46,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 @@ -191,7 +177,7 @@ [:features ::cfeat/features] [:has-media-trimmed :boolean] [:comment-thread-seqn {:min 0} :int] - [:name :string] + [:name [:string {:max 250}]] [:revn {:min 0} :int] [:modified-at ::dt/instant] [:is-shared :boolean] @@ -671,7 +657,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) @@ -690,10 +676,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 @@ -761,19 +745,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]]} @@ -927,11 +911,11 @@ {:id file-id} {::db/return-keys [:id :name :is-shared :deleted-at :project-id :created-at :modified-at]})] - (wrk/submit! {::wrk/task :delete-object - ::wrk/conn conn - :object :file - :deleted-at (:deleted-at file) - :id file-id}) + (wrk/submit! {::db/conn conn + ::wrk/task :delete-object + ::wrk/params {:object :file + :deleted-at (:deleted-at file) + :id file-id}}) file)) (def ^:private @@ -1047,14 +1031,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/duration]]) ;; 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 ab386eca0..b65efa3bf 100644 --- a/backend/src/app/rpc/commands/files_create.clj +++ b/backend/src/app/rpc/commands/files_create.clj @@ -88,7 +88,7 @@ (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] diff --git a/backend/src/app/rpc/commands/files_share.clj b/backend/src/app/rpc/commands/files_share.clj index bf761b5bf..98132e06e 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_temp.clj b/backend/src/app/rpc/commands/files_temp.clj index 4eef26214..250026076 100644 --- a/backend/src/app/rpc/commands/files_temp.clj +++ b/backend/src/app/rpc/commands/files_temp.clj @@ -35,7 +35,7 @@ (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] diff --git a/backend/src/app/rpc/commands/files_thumbnails.clj b/backend/src/app/rpc/commands/files_thumbnails.clj index d766acd3c..446de5378 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) @@ -279,9 +275,9 @@ 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" @@ -317,25 +313,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 media/configure-assets-storage conn) + (delete-file-object-thumbnail! file-id object-id)) + nil))) ;; --- MUTATION COMMAND: create-file-thumbnail @@ -413,4 +407,5 @@ (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))}))))) + {: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 cf9e9b590..76b621b3c 100644 --- a/backend/src/app/rpc/commands/files_update.clj +++ b/backend/src/app/rpc/commands/files_update.clj @@ -51,7 +51,7 @@ [:vector [:map [:changes [:vector ::cpc/change]] [:hint-origin {:optional true} :keyword] - [:hint-events {:optional true} [:vector :string]]]]] + [:hint-events {:optional true} [:vector [:string {:max 250}]]]]]] [:skip-validate {:optional true} :boolean]]) (def ^:private @@ -123,12 +123,13 @@ (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?) +(declare ^:private delete-old-snapshots!) +(declare ^:private get-lagged-changes) +(declare ^:private send-notifications!) +(declare ^:private take-snapshot?) +(declare ^:private update-file) +(declare ^:private update-file*) +(declare ^:private update-file-data) ;; If features are specified from params and the final feature ;; set is different than the persisted one, update it on the @@ -238,12 +239,15 @@ :created-at created-at :file-id (:id file) :revn (:revn file) + :label (::snapshot-label file) + :data (::snapshot-data file) :features (db/create-array conn "text" (:features file)) - :data (when (take-snapshot? file) - (:data file)) :changes (blob/encode changes)} {::db/return-keys false}) + (when (::snapshot-data file) + (delete-old-snapshots! cfg file)) + (db/update! conn :file {:revn (:revn file) :data (:data file) @@ -262,8 +266,8 @@ ;; Send asynchronous notifications (send-notifications! cfg params) - ;; Retrieve and return lagged data - (get-lagged-changes conn params)))) + {:revn (:revn file) + :lagged (get-lagged-changes conn params)}))) (defn- soft-validate-file-schema! [file] @@ -286,7 +290,6 @@ (-> 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 @@ -322,21 +325,42 @@ file (-> (files/check-version! file) (update :revn inc) (update :data cpc/process-changes changes) - (update :data d/without-nils))] + (update :data d/without-nils)) - (when (contains? cf/flags :soft-file-validation) - (soft-validate-file! file libs)) + 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))] - (when (contains? cf/flags :soft-file-schema-validation) - (soft-validate-file-schema! file)) + (l/trc :hint "take snapshot" + :file-id (str (:id file)) + :revn (:revn file) + :label label + :elapsed (dt/format-duration elapsed)) - (when (and (contains? cf/flags :file-validation) - (not skip-validate)) - (val/validate-file! file libs)) + (-> file + (assoc ::snapshot-data snapshot) + (assoc ::snapshot-label label))) + file)] - (when (and (contains? cf/flags :file-schema-validation) - (not skip-validate)) - (val/validate-file-schema! file)) + (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 (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))) (cond-> file (contains? cfeat/*current* "fdata/objects-map") @@ -351,13 +375,42 @@ (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))))) + (when (contains? cf/flags :file-snapshot) + (let [freq (or (cf/get :file-snapshot-every) 20) + timeout (or (cf/get :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 IS NOT NULL + 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 :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 diff --git a/backend/src/app/rpc/commands/ldap.clj b/backend/src/app/rpc/commands/ldap.clj index dff521500..0829987d4 100644 --- a/backend/src/app/rpc/commands/ldap.clj +++ b/backend/src/app/rpc/commands/ldap.clj @@ -8,7 +8,7 @@ (: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] @@ -19,27 +19,25 @@ [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} + ::doc/module :auth + ::sm/params schema:login-with-ldap} [{:keys [::setup/props ::ldap/provider] :as cfg} params] (when-not provider (ex/raise :type :restriction diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 5d01d9ec6..680541184 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" @@ -397,17 +398,30 @@ ;; --- 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}] + [cfg {:keys [project-id ::rpc/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/name "create-file") + (assoc ::audit/props props))] + (audit/submit! cfg event)))) + + result)))) (def ^:private schema:clone-template @@ -425,16 +439,14 @@ [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id project-id template-id] :as params}] (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))] + template (tmpl/get-template-stream cfg template-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 08232e899..992c5d1da 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,25 +38,21 @@ :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 :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}] @@ -176,14 +171,17 @@ (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 :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) @@ -255,12 +253,15 @@ (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 :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 4064f0dd6..40b8b8a43 100644 --- a/backend/src/app/rpc/commands/profile.clj +++ b/backend/src/app/rpc/commands/profile.clj @@ -28,7 +28,7 @@ [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])) @@ -276,19 +276,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)) @@ -299,7 +299,7 @@ {:changed true}) (defn- request-email-change! - [{:keys [::conn] :as cfg} {:keys [profile email] :as params}] + [{: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") @@ -319,9 +319,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 @@ -366,13 +385,13 @@ ;; --- 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 @@ -384,37 +403,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") + 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-with-participants +(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 a8236008e..4901a6efd 100644 --- a/backend/src/app/rpc/commands/projects.clj +++ b/backend/src/app/rpc/commands/projects.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.db :as db] [app.db.sql :as-alias sql] [app.loggers.audit :as-alias audit] @@ -21,11 +21,7 @@ [app.rpc.quotes :as quotes] [app.util.services :as sv] [app.util.time :as dt] - [app.worker :as wrk] - [clojure.spec.alpha :as s])) - -(s/def ::id ::us/uuid) -(s/def ::name ::us/string) + [app.worker :as wrk])) ;; --- Check Project Permissions @@ -75,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) @@ -112,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))) @@ -154,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)] @@ -170,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) @@ -205,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 :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} @@ -226,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] @@ -258,20 +261,22 @@ :code :non-deletable-project :hint "impossible to delete default project")) - (wrk/submit! {::wrk/task :delete-object - ::wrk/conn conn - :object :project - :deleted-at (:deleted-at project) - :id project-id}) + (wrk/submit! {::db/conn conn + ::wrk/task :delete-object + ::wrk/params {:object :project + :deleted-at (:deleted-at project) + :id project-id}}) project)) -(s/def ::delete-project - (s/keys :req [::rpc/profile-id] - :req-un [::id])) + +(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] diff --git a/backend/src/app/rpc/commands/search.clj b/backend/src/app/rpc/commands/search.clj index 571015645..1a25a6dcf 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 4cc75de5a..74918de97 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -12,7 +12,6 @@ [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] @@ -32,16 +31,10 @@ [app.util.services :as sv] [app.util.time :as dt] [app.worker :as wrk] - [clojure.spec.alpha :as s] [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, @@ -351,7 +344,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]]) @@ -364,10 +357,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 @@ -438,12 +433,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) @@ -503,14 +500,14 @@ 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)))) @@ -532,19 +529,20 @@ :code :non-deletable-team :hint "impossible to delete default team")) - (wrk/submit! {::wrk/task :delete-object - ::wrk/conn conn - :object :team - :deleted-at deleted-at - :id team-id}) + (wrk/submit! {::db/conn conn + ::wrk/task :delete-object + ::wrk/params {:object :team + :deleted-at deleted-at + :id team-id}}) team)) -(s/def ::delete-team - (s/keys :req [::rpc/profile-id] - :req-un [::id])) +(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)] @@ -557,10 +555,6 @@ ;; --- 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 @@ -624,25 +618,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)] @@ -665,13 +663,14 @@ (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"}) @@ -735,12 +734,19 @@ :email email :hint "the profile has reported repeatedly as spam or has bounces")) - ;; Secondly check if the invited member email is part of the global spam/bounce report. + ;; Secondly check if the invited member email is part of the global bounce report. (when (eml/has-bounce-reports? conn email) - (ex/raise :type :validation + (ex/raise :type :restriction :code :email-has-permanent-bounces :email email - :hint "the email you invite has been repeatedly reported as spam or bounce")) + :hint "the email you invite has been repeatedly reported as bounce")) + + ;; Secondly check if the invited member email is part of the global complain report. + (when (eml/has-complaint-reports? conn email) + (ex/raise :type :restriction + :code :email-has-complaints + :email email + :hint "the email you invite has been repeatedly reported as spam")) ;; When we have email verification disabled and invitation user is ;; already present in the database, we proceed to add it to the @@ -766,6 +772,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 @@ -786,14 +793,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 @@ -809,7 +818,7 @@ (def ^:private schema:create-team-invitations [:map {:title "create-team-invitations"} [:team-id ::sm/uuid] - [:role [::sm/one-of #{:owner :admin :editor}]] + [:role schema:role] [:emails ::sm/set-of-emails]]) (sv/defmethod ::create-team-invitations @@ -853,28 +862,22 @@ ;; 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)] (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] @@ -883,59 +886,62 @@ (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] + [cfg {:keys [::rpc/profile-id emails role name] :as params}] - (let [features (-> (cfeat/get-enabled-features cf/flags) - (cfeat/check-client-features! (:features params))) - params (assoc params - :profile-id profile-id - :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)] + (db/tx-run! cfg + (fn [{:keys [::db/conn] :as cfg}] + (let [features (-> (cfeat/get-enabled-features cf/flags) + (cfeat/check-client-features! (:features params))) - ;; Create invitations for all provided emails. - (->> emails - (map (fn [email] - {:team team - :profile profile - :email email - :role role})) - (run! (partial create-invitation cfg))) + params (-> params + (assoc :profile-id profile-id) + (assoc :features features)) - (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)})) + 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)] - (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)}}) + (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)) - (vary-meta team assoc ::audit/props {:invitations (count emails)})))) + ;; 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) @@ -956,12 +962,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)] @@ -977,12 +986,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)] diff --git a/backend/src/app/rpc/commands/verify_token.clj b/backend/src/app/rpc/commands/verify_token.clj index e2641df23..0e4f3c89f 100644 --- a/backend/src/app/rpc/commands/verify_token.clj +++ b/backend/src/app/rpc/commands/verify_token.clj @@ -7,7 +7,7 @@ (ns app.rpc.commands.verify-token (:require [app.common.exceptions :as ex] - [app.common.spec :as us] + [app.common.schema :as sm] [app.db :as db] [app.db.sql :as-alias sql] [app.http.session :as session] @@ -23,21 +23,19 @@ [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 (::setup/props cfg) {:token token}) @@ -131,26 +129,28 @@ (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) + (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}) @@ -169,13 +169,16 @@ ;; 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 diff --git a/backend/src/app/rpc/commands/viewer.clj b/backend/src/app/rpc/commands/viewer.clj index 70d5193c1..9d15b3e8f 100644 --- a/backend/src/app/rpc/commands/viewer.clj +++ b/backend/src/app/rpc/commands/viewer.clj @@ -98,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 13a5d0210..2649a73a4 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 :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/permissions.clj b/backend/src/app/rpc/permissions.clj index d2f1719ee..ef1d71072 100644 --- a/backend/src/app/rpc/permissions.clj +++ b/backend/src/app/rpc/permissions.clj @@ -12,7 +12,7 @@ [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] diff --git a/backend/src/app/rpc/quotes.clj b/backend/src/app/rpc/quotes.clj index 3244bd03f..87f9bf7f7 100644 --- a/backend/src/app/rpc/quotes.clj +++ b/backend/src/app/rpc/quotes.clj @@ -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/rlimit.clj b/backend/src/app/rpc/rlimit.clj index 0c0868f93..4e0924490 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/srepl/fixes.clj b/backend/src/app/srepl/fixes.clj index ee40421df..5e80516b8 100644 --- a/backend/src/app/srepl/fixes.clj +++ b/backend/src/app/srepl/fixes.clj @@ -184,10 +184,7 @@ (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) - #(update % :touched - cfh/set-touched-group - (ctk/build-swap-slot-group slot)))) + (ctn/update-shape container (:id child) #(ctk/set-swap-slot % slot))) container)] (recur (process-copy-head container child) (rest children) @@ -237,3 +234,44 @@ 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/main.clj b/backend/src/app/srepl/main.clj index 193f72d1c..a5f002b8d 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] @@ -38,10 +40,12 @@ [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])) @@ -59,32 +63,27 @@ ([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] @@ -195,6 +194,12 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (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}] @@ -202,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 @@ -231,6 +232,9 @@ (resolve-dest [dest] (cond + (= :all dest) + [uuid/zero] + (uuid? dest) [dest] @@ -246,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) @@ -480,6 +485,27 @@ ;; 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 @@ -507,22 +533,105 @@ :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} - {::db/columns [: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 @@ -535,49 +644,167 @@ (doseq [{:keys [id]} (db/query conn :project {:team-id team-id} - {::db/columns [:id]})] + {::sql/columns [:id]})] (restore-project* cfg id)) :restored) -(defn restore-deleted-team! +(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 restore-team* 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)}) -(defn restore-deleted-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 restore-project* project-id))) + (restore-team* system team-id)))))) -(defn restore-deleted-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 restore-file* file-id))) +(defn delete-profile! + "Mark a profile for deletion." + [profile-id] + (let [profile-id (h/parse-uuid profile-id) + tnow (dt/now)] -(defn delete-team! - "Mark a team for deletion" - [team-id] - (let [team-id (h/parse-uuid team-id)] - (db/tx-run! main/system (fn [{:keys [::db/conn]}] - (#'teams/delete-team conn team-id))))) + (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}) -(defn delete-project! - "Mark a project for deletion" - [project-id] - (let [project-id (h/parse-uuid project-id)] - (db/tx-run! main/system (fn [{:keys [::db/conn]}] - (#'projects/delete-project conn project-id))))) + (wrk/invoke! (-> main/system + (assoc ::wrk/task :delete-object) + (assoc ::wrk/params {:object :profile + :deleted-at tnow + :id profile-id}))) + :deleted)) -(defn delete-file! - "Mark a project for deletion" - [file-id] - (let [file-id (h/parse-uuid file-id)] - (db/tx-run! main/system (fn [{:keys [::db/conn]}] - (#'files/mark-file-deleted conn file-id))))) +(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 (db/get* system :profile + {:email (str/lower email)} + {::db/remove-deleted false})] + (do + (audit/insert! system + {::audit/name "delete-profile" + ::audit/type "action" + ::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/gc_deleted.clj b/backend/src/app/storage/gc_deleted.clj index 8d1d0e5ad..52cdce4b1 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)] diff --git a/backend/src/app/tasks/delete_object.clj b/backend/src/app/tasks/delete_object.clj index 557e35b59..9c48d2309 100644 --- a/backend/src/app/tasks/delete_object.clj +++ b/backend/src/app/tasks/delete_object.clj @@ -10,6 +10,8 @@ [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])) @@ -20,8 +22,15 @@ (defmethod delete-object :file [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] - (l/trc :hint "marking for deletion" :rel "file" :id (str id)) (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 @@ -48,28 +57,57 @@ (defmethod delete-object :project [{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}] - (l/trc :hint "marking for deletion" :rel "project" :id (str id)) - (doseq [file (db/update! conn :file - {:deleted-at deleted-at} - {:project-id id} - {::db/return-keys [:id :deleted-at] - ::db/many true})] - (delete-object cfg (assoc file :object :file)))) + (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)) + (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}) + {:team-id id} + {::db/return-keys false}) (binding [*team-deletion* true] - (doseq [project (db/update! conn :project - {:deleted-at deleted-at} - {:team-id id} - {::db/return-keys [:id :deleted-at] - ::db/many true})] - (delete-object cfg (assoc project :object :project))))) + (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] @@ -80,5 +118,5 @@ (defmethod ig/init-key ::handler [_ cfg] - (fn [{:keys [props] :as params}] + (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 88f1a74b4..79f5ff8b9 100644 --- a/backend/src/app/tasks/file_gc.clj +++ b/backend/src/app/tasks/file_gc.clj @@ -295,17 +295,17 @@ (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 [_ cfg] - (fn [{:keys [file-id] :as params}] + (fn [{:keys [props] :as task}] (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}] - (let [min-age (dt/duration (or (:min-age params) (::min-age cfg))) + (let [min-age (dt/duration (or (:min-age props) (::min-age cfg))) cfg (-> cfg (update ::sto/storage media/configure-assets-storage conn) - (assoc ::file-id file-id) + (assoc ::file-id (:file-id props)) (assoc ::min-age min-age)) total (reduce (fn [total file] @@ -314,12 +314,12 @@ 0 (get-candidates cfg))] - (l/inf :hint "task finished" + (l/inf :hint "finished" :min-age (dt/format-duration min-age) :processed total) ;; Allow optional rollback passed by params - (when (:rollback? params) + (when (:rollback? props) (db/rollback! conn)) {:processed total}))))) diff --git a/backend/src/app/tasks/file_xlog_gc.clj b/backend/src/app/tasks/file_xlog_gc.clj index c88f42a84..4e240d7f7 100644 --- a/backend/src/app/tasks/file_xlog_gc.clj +++ b/backend/src/app/tasks/file_xlog_gc.clj @@ -29,8 +29,8 @@ (defmethod ig/init-key ::handler [_ {:keys [::db/pool] :as cfg}] - (fn [params] - (let [min-age (or (:min-age params) (::min-age cfg))] + (fn [{:keys [props] :as task}] + (let [min-age (or (:min-age props) (::min-age cfg))] (db/with-atomic [conn pool] (let [interval (db/interval min-age) result (db/exec-one! conn [sql:delete-files-xlog interval]) @@ -38,7 +38,7 @@ (l/info :hint "task finished" :min-age (dt/format-duration min-age) :total result) - (when (:rollback? params) + (when (:rollback? props) (db/rollback! conn)) result))))) diff --git a/backend/src/app/tasks/objects_gc.clj b/backend/src/app/tasks/objects_gc.clj index da9e1232f..9858585cc 100644 --- a/backend/src/app/tasks/objects_gc.clj +++ b/backend/src/app/tasks/objects_gc.clj @@ -35,11 +35,6 @@ ;; 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)) @@ -269,15 +264,15 @@ 0))) (def ^:private deletion-proc-vars - [#'delete-file-media-objects! + [#'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! - #'delete-profiles!]) + #'delete-teams!]) (defn- execute-proc! "A generic function that executes the specified proc iterativelly @@ -297,13 +292,13 @@ (defmethod ig/prep-key ::handler [_ cfg] (assoc cfg - ::min-age cf/deletion-delay + ::min-age (cf/get-deletion-delay) ::chunk-size 10)) (defmethod ig/init-key ::handler [_ cfg] - (fn [params] - (let [min-age (dt/duration (or (:min-age params) (::min-age cfg))) + (fn [{:keys [props] :as task}] + (let [min-age (dt/duration (or (:min-age props) (::min-age cfg))) cfg (-> cfg (assoc ::min-age (db/interval min-age)) (update ::sto/storage media/configure-assets-storage))] 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 8869c72cc..000000000 --- a/backend/src/app/tasks/orphan_teams_gc.clj +++ /dev/null @@ -1,67 +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] - [app.worker :as wrk] - [clojure.spec.alpha :as s] - [integrant.core :as ig])) - -(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}] - (let [deleted-at (dt/now)] - (->> (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 deleted-at} - {:id team-id}) - - (wrk/submit! {::wrk/task :delete-object - ::wrk/conn conn - :object :team - :deleted-at deleted-at - :id team-id}) - - (inc total)) - 0)))) - -(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}))))) diff --git a/backend/src/app/tasks/tasks_gc.clj b/backend/src/app/tasks/tasks_gc.clj index 77f1f92fa..0e93ea0d0 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 ec07c67b3..410595f72 100644 --- a/backend/src/app/tasks/telemetry.clj +++ b/backend/src/app/tasks/telemetry.clj @@ -206,14 +206,16 @@ (defmethod ig/init-key ::handler [_ {:keys [::db/pool ::setup/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? + (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)}] diff --git a/backend/src/app/tokens.clj b/backend/src/app/tokens.clj index 30ca32b3b..60b0d50b2 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 000000000..9e3fca606 --- /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 19a7bdea6..c7e4f42eb 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 000000000..8f8842718 --- /dev/null +++ b/backend/src/app/util/overrides.clj @@ -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 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) + ::oapi/type "string" + ::oapi/format "unix-path" + ::oapi/decode fs/path}}) diff --git a/backend/src/app/util/pointer_map.clj b/backend/src/app/util/pointer_map.clj index 16ce73bb0..ba84d3d4b 100644 --- a/backend/src/app/util/pointer_map.clj +++ b/backend/src/app/util/pointer_map.clj @@ -40,7 +40,8 @@ [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 @@ -75,6 +76,14 @@ ^: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! [_] (when-not *load-fn* diff --git a/backend/src/app/util/time.clj b/backend/src/app/util/time.clj index 778596624..4c8f6d40e 100644 --- a/backend/src/app/util/time.clj +++ b/backend/src/app/util/time.clj @@ -368,7 +368,7 @@ (let [p1 (System/nanoTime)] #(duration {:nanos (- (System/nanoTime) p1)}))) -(sm/def! ::instant +(sm/register! ::instant {:type ::instant :pred instant? :type-properties @@ -379,7 +379,7 @@ ::oapi/type "string" ::oapi/format "iso"}}) -(sm/def! ::duration +(sm/register! ::duration {:type :durations :pred duration? :type-properties diff --git a/backend/src/app/util/websocket.clj b/backend/src/app/util/websocket.clj index 70d8eb406..b468c0e28 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 1da2e8de0..d5a2a8551 100644 --- a/backend/src/app/worker.clj +++ b/backend/src/app/worker.clj @@ -8,6 +8,7 @@ "Async tasks abstraction (impl)." (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.logging :as l] [app.common.spec :as us] [app.common.uuid :as uuid] @@ -58,17 +59,6 @@ ;; 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() + ?) @@ -87,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 @@ -102,21 +91,23 @@ 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) @@ -126,7 +117,13 @@ :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/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index cb399e70f..8380ea13e 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -34,6 +34,7 @@ [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] @@ -57,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 @@ -76,6 +76,7 @@ :enable-feature-fdata-pointer-map :enable-feature-fdata-objets-map :enable-feature-components-v2 + :enable-file-snapshot :disable-file-validation]) (defn state-init @@ -87,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" @@ -103,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 @@ -377,9 +380,9 @@ ([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 @@ -523,7 +526,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 ab3a4e82e..d0a8e7475 100644 --- a/backend/test/backend_tests/loggers_webhooks_test.clj +++ b/backend/test/backend_tests/loggers_webhooks_test.clj @@ -21,11 +21,10 @@ (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)}}}})] + {:event + {: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 +34,10 @@ (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)}}}})] + {:event + {: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 +50,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 +72,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 +90,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 78d0e4d41..14bff7ea6 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 35d76231f..5d1fe1824 100644 --- a/backend/test/backend_tests/rpc_file_test.clj +++ b/backend/test/backend_tests/rpc_file_test.clj @@ -166,6 +166,10 @@ :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)))) @@ -176,11 +180,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,7 +207,7 @@ ;; 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})] @@ -211,13 +215,13 @@ ;; 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; (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)}) @@ -233,11 +237,11 @@ ;; 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))))))) @@ -338,7 +342,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)})] @@ -367,7 +371,7 @@ (t/is (= 1 (:processed res)))) (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 @@ -494,11 +498,11 @@ (t/is (= 1 (:processed res)))) (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)})] @@ -535,11 +539,11 @@ (t/is (= 1 (:processed res)))) (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 @@ -702,7 +706,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 @@ -732,7 +736,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})] @@ -1127,9 +1131,9 @@ (t/is (= 1 (:processed res)))) ;; 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 +1152,30 @@ (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)))) + ;; 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 :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 @@ -1220,7 +1232,3 @@ (let [rows (th/db-query :file-thumbnail {:file-id (:id file)})] (t/is (= 1 (count rows))))))) - - - - diff --git a/backend/test/backend_tests/rpc_file_thumbnails_test.clj b/backend/test/backend_tests/rpc_file_thumbnails_test.clj index e5cd918b1..c73941aff 100644 --- a/backend/test/backend_tests/rpc_file_thumbnails_test.clj +++ b/backend/test/backend_tests/rpc_file_thumbnails_test.clj @@ -118,7 +118,7 @@ (t/is (= 1 (:processed result)))) (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 diff --git a/backend/test/backend_tests/rpc_profile_test.clj b/backend/test/backend_tests/rpc_profile_test.clj index 95a275874..7a90c9a81 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] @@ -126,7 +127,7 @@ ;; (th/print-result! out) (t/is (nil? (:error out))))))) -(t/deftest profile-deletion-simple +(t/deftest profile-deletion-1 (let [prof (th/create-profile* 1) file (th/create-file* 1 {:profile-id (:id prof) :project-id (:default-project-id prof) @@ -152,23 +153,22 @@ (t/is (nil? (:error out))) (t/is (= 1 (count (:result out))))) - ;; execute permanent deletion task - (let [result (th/run-task! :objects-gc {:min-age 0})] - (t/is (= 1 (:processed result)))) - - (let [row (th/db-get :team - {:id (:default-team-id prof)} - {::db/remove-deleted false})] - (t/is (nil? (:deleted-at row)))) - - (let [result (th/run-task! :orphan-teams-gc {:min-age 0})] - (t/is (= 1 (:processed result)))) + (th/run-pending-tasks!) (let [row (th/db-get :team {:id (:default-team-id prof)} {::db/remove-deleted false})] (t/is (dt/instant? (:deleted-at row)))) + ;; execute permanent deletion task + (let [result (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 4 (:processed result)))) + + (let [row (th/db-get :team + {:id (:default-team-id prof)} + {::db/remove-deleted false})] + (t/is (nil? row))) + ;; query profile after delete (let [params {::th/type :get-profile ::rpc/profile-id (:id prof)} @@ -177,14 +177,187 @@ (let [result (:result out)] (t/is (= uuid/zero (:id result))))))) -(t/deftest registration-domain-whitelist - (let [whitelist #{"gmail.com" "hey.com" "ya.ru"}] - (t/testing "allowed email domain" - (t/is (true? (auth/email-domain-in-whitelist? whitelist "username@ya.ru"))) - (t/is (true? (auth/email-domain-in-whitelist? #{} "username@somedomain.com")))) +(t/deftest profile-deletion-2 + (let [prof1 (th/create-profile* 1) + prof2 (th/create-profile* 2) + file1 (th/create-file* 1 {:profile-id (:id prof1) + :project-id (:default-project-id prof1) + :is-shared false}) + team1 (th/create-team* 1 {:profile-id (:id prof1)}) - (t/testing "not allowed email domain" - (t/is (false? (auth/email-domain-in-whitelist? whitelist "username@somedomain.com")))))) + role1 (th/create-team-role* {:team-id (:id team1) + :profile-id (:id prof2) + + :role :editor})] + ;; Assert all roles for team + (let [roles (th/db-query :team-profile-rel {:team-id (:id team1)})] + (t/is (= 2 (count roles)))) + + ;; Request profile to be deleted + (let [params {::th/type :delete-profile + ::rpc/profile-id (:id prof1)} + out (th/command! params)] + ;; (th/print-result! out) + + (let [error (:error out) + edata (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type edata) :validation)) + (t/is (= (:code edata) :owner-teams-with-people)))))) + +(t/deftest profile-deletion-3 + (let [prof1 (th/create-profile* 1) + prof2 (th/create-profile* 2) + prof3 (th/create-profile* 3) + file1 (th/create-file* 1 {:profile-id (:id prof1) + :project-id (:default-project-id prof1) + :is-shared false}) + team1 (th/create-team* 1 {:profile-id (:id prof1)}) + + role1 (th/create-team-role* {:team-id (:id team1) + :profile-id (:id prof2) + :role :editor}) + role2 (th/create-team-role* {:team-id (:id team1) + :profile-id (:id prof3) + :role :editor})] + + ;; Assert all roles for team + (let [roles (th/db-query :team-profile-rel {:team-id (:id team1)})] + (t/is (= 3 (count roles)))) + + ;; Request profile to be deleted (it should fail) + (let [params {::th/type :delete-profile + ::rpc/profile-id (:id prof1)} + out (th/command! params)] + ;; (th/print-result! out) + + (let [error (:error out) + edata (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type edata) :validation)) + (t/is (= (:code edata) :owner-teams-with-people)))) + + ;; Leave team by role 1 + (let [params {::th/type :leave-team + ::rpc/profile-id (:id prof2) + :id (:id team1)} + out (th/command! params)] + + ;; (th/print-result! out) + (t/is (nil? (:result out))) + (t/is (nil? (:error out)))) + + ;; Request profile to be deleted (it should fail) + (let [params {::th/type :delete-profile + ::rpc/profile-id (:id prof1)} + out (th/command! params)] + ;; (th/print-result! out) + (let [error (:error out) + edata (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type edata) :validation)) + (t/is (= (:code edata) :owner-teams-with-people)))) + + ;; Leave team by role 0 (the default) and reassing owner to role 3 + ;; without reassinging it (should fail) + (let [params {::th/type :leave-team + ::rpc/profile-id (:id prof1) + ;; :reassign-to (:id prof3) + :id (:id team1)} + out (th/command! params)] + + ;; (th/print-result! out) + + (let [error (:error out) + edata (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type edata) :validation)) + (t/is (= (:code edata) :owner-cant-leave-team)))) + + ;; Leave team by role 0 (the default) and reassing owner to role 3 + (let [params {::th/type :leave-team + ::rpc/profile-id (:id prof1) + :reassign-to (:id prof3) + :id (:id team1)} + out (th/command! params)] + + ;; (th/print-result! out) + (t/is (nil? (:result out))) + (t/is (nil? (:error out)))) + + ;; Request profile to be deleted + (let [params {::th/type :delete-profile + ::rpc/profile-id (:id prof1)} + out (th/command! params)] + ;; (th/print-result! out) + + (t/is (= {} (:result out))) + (t/is (nil? (:error out)))) + + ;; query files after profile soft deletion + (let [params {::th/type :get-project-files + ::rpc/profile-id (:id prof1) + :project-id (:default-project-id prof1)} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= 1 (count (:result out))))) + + (th/run-pending-tasks!) + + ;; execute permanent deletion task + (let [result (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 4 (:processed result)))) + + (let [row (th/db-get :team + {:id (:default-team-id prof1)} + {::db/remove-deleted false})] + (t/is (nil? row))) + + ;; query profile after delete + (let [params {::th/type :get-profile + ::rpc/profile-id (:id prof1)} + out (th/command! params)] + ;; (th/print-result! out) + (let [result (:result out)] + (t/is (= uuid/zero (:id result))))))) + + +(t/deftest profile-deletion-4 + (let [prof1 (th/create-profile* 1) + file1 (th/create-file* 1 {:profile-id (:id prof1) + :project-id (:default-project-id prof1) + :is-shared false}) + team1 (th/create-team* 1 {:profile-id (:id prof1)}) + team2 (th/create-team* 2 {:profile-id (:id prof1)})] + + ;; Request profile to be deleted + (let [params {::th/type :delete-profile + ::rpc/profile-id (:id prof1)} + out (th/command! params)] + ;; (th/print-result! out) + (t/is (= {} (:result out))) + (t/is (nil? (:error out)))) + + (th/run-pending-tasks!) + + (let [rows (th/db-exec! ["select id,name,deleted_at from team where deleted_at is not null"])] + (t/is (= 3 (count rows)))) + + ;; execute permanent deletion task + (let [result (th/run-task! :objects-gc {:min-age 0})] + (t/is (= 8 (:processed result)))))) + + +(t/deftest email-blacklist-1 + (t/is (false? (email.blacklist/enabled? th/*system*))) + (t/is (true? (email.blacklist/enabled? (assoc th/*system* :app.email/blacklist [])))) + (t/is (true? (email.blacklist/contains? (assoc th/*system* :app.email/blacklist #{"foo.com"}) "AA@FOO.COM")))) + +(t/deftest email-whitelist-1 + (t/is (false? (email.whitelist/enabled? th/*system*))) + (t/is (true? (email.whitelist/enabled? (assoc th/*system* :app.email/whitelist [])))) + (t/is (true? (email.whitelist/contains? (assoc th/*system* :app.email/whitelist #{"foo.com"}) "AA@FOO.COM")))) (t/deftest prepare-register-and-register-profile-1 (let [data {::th/type :prepare-register-profile @@ -417,9 +590,10 @@ (th/create-global-complaint-for pool {:type :bounce :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-permanent-bounces (:code edata))))))) (t/deftest register-profile-with-complained-email (let [pool (:app.db/pool th/*system*) @@ -430,9 +604,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 @@ -463,20 +639,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 @@ -541,7 +723,7 @@ out (th/command! data)] ;; (th/print-result! out) (t/is (nil? (:result out))) - (t/is (= 2 (:call-count @mock)))) + (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)}) @@ -550,7 +732,7 @@ (t/is (nil? (:result out))) (t/is (nil? (:error out))) ;; (th/print-result! out) - (t/is (= 2 (:call-count @mock)))))))) + (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 3bd6ac3b9..8b4ccda3f 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 diff --git a/backend/test/backend_tests/rpc_webhooks_test.clj b/backend/test/backend_tests/rpc_webhooks_test.clj index 76c3de763..f47472a73 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)) diff --git a/backend/yarn.lock b/backend/yarn.lock index 8962bedea..7e062d727 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -118,11 +118,11 @@ __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 @@ -573,7 +573,7 @@ __metadata: 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: 10c0/02e26a0b039c11fd5b75e1d734c8f0332c95510f6a514a9a0991023e43fb233884da02d7f966823ffb230632a733fc86d4a4b1e63c3fbe00058b8ee0f8c728af @@ -745,9 +745,9 @@ __metadata: languageName: node linkType: hard -"nodemon@npm:^3.0.1": - version: 3.1.0 - resolution: "nodemon@npm:3.1.0" +"nodemon@npm:^3.1.2": + version: 3.1.2 + resolution: "nodemon@npm:3.1.2" dependencies: chokidar: "npm:^3.5.2" debug: "npm:^4" @@ -761,7 +761,7 @@ __metadata: undefsafe: "npm:^2.0.5" bin: nodemon: bin/nodemon.js - checksum: 10c0/3aeb50105ecae31ce4d0a5cd464011d4aa0dc15419e39ac0fd203d784e38940e1436f4ed96adbaa0f9614ee0644f91e3cf38f2afae8d3918ae7afc51c7e2116b + checksum: 10c0/7a091067d766768fb6660b796194b01748bba5dc3f1e3ed3dd5f804bfa305e207d24635755078ee5e7cc53848cea35204901e0a6e51ac64483bb8e9ecb237c95 languageName: node linkType: hard @@ -870,10 +870,10 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.2.4": - version: 1.3.0 - resolution: "sax@npm:1.3.0" - checksum: 10c0/599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea +"sax@npm:^1.4.1": + version: 1.4.1 + resolution: "sax@npm:1.4.1" + checksum: 10c0/6bf86318a254c5d898ede6bd3ded15daf68ae08a5495a2739564eb265cd13bcc64a07ab466fb204f67ce472bb534eb8612dac587435515169593f4fffa11de7c languageName: node linkType: hard @@ -1129,7 +1129,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.13.0": +"ws@npm:^8.17.0": version: 8.17.0 resolution: "ws@npm:8.17.0" peerDependencies: diff --git a/common/deps.edn b/common/deps.edn index 704021288..57379fc35 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -1,10 +1,10 @@ {:deps {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.23.1"} @@ -12,14 +12,14 @@ 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.12"} + 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.3"} + 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"} @@ -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,12 +72,12 @@ :build {:extra-deps - {io.github.clojure/tools.build {:git/tag "v0.10.0" :git/sha "3a2c484"}} + {io.github.clojure/tools.build {:git/tag "v0.10.3" :git/sha "15ead66"}} :ns-default build} :test {:main-opts ["-m" "kaocha.runner"] - :extra-deps {lambdaisland/kaocha {:mvn/version "1.88.1376"}}} + :extra-deps {lambdaisland/kaocha {:mvn/version "1.91.1392"}}} :shadow-cljs {:main-opts ["-m" "shadow.cljs.devtools.cli"]} diff --git a/common/package.json b/common/package.json index 5fc9cc0f4..93a456d1d 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.2.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/data.cljc b/common/src/app/common/data.cljc index 736803631..ecf55b115 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])) @@ -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`." @@ -642,6 +641,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 f0c35b26b..6e5562096 100644 --- a/common/src/app/common/features.cljc +++ b/common/src/app/common/features.cljc @@ -84,7 +84,7 @@ "plugins/runtime"} (into frontend-only-features))) -(sm/def! ::features +(sm/register! ::features [:schema {:title "FileFeatures" ::smdj/inline true diff --git a/common/src/app/common/files/builder.cljc b/common/src/app/common/files/builder.cljc index 811f372b5..988164c20 100644 --- a/common/src/app/common/files/builder.cljc +++ b/common/src/app/common/files/builder.cljc @@ -38,18 +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)))) + (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? (nil? component-id)) - (assoc :page-id (:current-page-id file) - :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? (ch/check-change! change)] + 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)] @@ -61,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] @@ -181,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) @@ -515,12 +520,18 @@ ([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)) @@ -542,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) @@ -619,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] @@ -666,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 dbf872bca..9505175cb 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -35,25 +35,24 @@ (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]]]]])) + [: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 +(sm/register! ::change [:schema [:multi {:dispatch :type :title "Change" ::smd/simplified true} [:set-option @@ -135,6 +134,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]] @@ -252,7 +263,7 @@ [:type [:= :del-token]] [:id ::sm/uuid]]]]]) -(sm/define! ::changes +(sm/register! ::changes [:sequential {:gen/max 2} ::change]) (def check-change! @@ -604,6 +615,36 @@ [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)) @@ -702,52 +743,14 @@ (ctol/delete-token data id)) ;; === Operations - (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)))) + (ctn/set-shape-attr shape + (:attr op) + (:val op) + :on-changed on-changed + :ignore-touched (:ignore-touched op) + :ignore-geometry (:ignore-geometry op))) (defmethod process-operation :set-touched [_ shape op] diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 831e47433..35f4a32b7 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?] @@ -201,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 @@ -794,15 +825,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 721adab70..6ef70b5ea 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 47) +(def version 51) diff --git a/common/src/app/common/files/helpers.cljc b/common/src/app/common/files/helpers.cljc index 3856bc327..508bea799 100644 --- a/common/src/app/common/files/helpers.cljc +++ b/common/src/app/common/files/helpers.cljc @@ -357,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)) diff --git a/common/src/app/common/files/migrations.cljc b/common/src/app/common/files/migrations.cljc index 363311564..111d05072 100644 --- a/common/src/app/common/files/migrations.cljc +++ b/common/src/app/common/files/migrations.cljc @@ -22,6 +22,7 @@ [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] @@ -923,6 +924,99 @@ (-> 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} @@ -961,4 +1055,8 @@ {:id 44 :migrate-up migrate-up-44} {:id 45 :migrate-up migrate-up-45} {:id 46 :migrate-up migrate-up-46} - {:id 47 :migrate-up migrate-up-47}]) + {: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 98e1642a9..67f90dafe 100644 --- a/common/src/app/common/files/repair.cljc +++ b/common/src/app/common/files/repair.cljc @@ -460,6 +460,72 @@ (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 @@ -468,7 +534,7 @@ (let [slot (:swap-slot args)] (when (some? slot) (log/debug :hint (str " -> set swap-slot to " slot)) - (update shape :touched cfh/set-touched-group (ctk/build-swap-slot-group 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) diff --git a/common/src/app/common/files/validate.cljc b/common/src/app/common/files/validate.cljc index 7959c5f31..5eb708ab3 100644 --- a/common/src/app/common/files/validate.cljc +++ b/common/src/app/common/files/validate.cljc @@ -31,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 @@ -52,6 +54,7 @@ :not-component-not-allowed :component-nil-objects-not-allowed :instance-head-not-frame + :misplaced-slot :missing-slot}) (def ^:private @@ -63,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 @@ -287,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: @@ -298,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 @@ -309,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 @@ -323,6 +353,8 @@ (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 @@ -345,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 @@ -353,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 @@ -362,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 @@ -438,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}] diff --git a/common/src/app/common/geom/matrix.cljc b/common/src/app/common/geom/matrix.cljc index d435d861c..7c090a2d6 100644 --- a/common/src/app/common/geom/matrix.cljc +++ b/common/src/app/common/geom/matrix.cljc @@ -90,7 +90,7 @@ (sm/lazy-validator [:and [:fn matrix?] schema:matrix-attrs])) -(sm/def! ::matrix +(sm/register! ::matrix (letfn [(decode [o] (if (map? o) (map->Matrix o) diff --git a/common/src/app/common/geom/modifiers.cljc b/common/src/app/common/geom/modifiers.cljc index 813fd784a..ec4646f66 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 0a04fa747..560f30a5b 100644 --- a/common/src/app/common/geom/point.cljc +++ b/common/src/app/common/geom/point.cljc @@ -61,7 +61,7 @@ (sm/lazy-validator [:and [:fn point?] schema:point-attrs])) -(sm/def! ::point +(sm/register! ::point (letfn [(decode [p] (if (map? p) (map->Point p) diff --git a/common/src/app/common/geom/rect.cljc b/common/src/app/common/geom/rect.cljc index 48d620adf..c23f9942b 100644 --- a/common/src/app/common/geom/rect.cljc +++ b/common/src/app/common/geom/rect.cljc @@ -80,7 +80,7 @@ [:x2 ::sm/safe-number] [:y2 ::sm/safe-number]]) -(sm/define! ::rect +(sm/register! ::rect [:and {:gen/gen (->> (sg/tuple (sg/small-double) (sg/small-double) diff --git a/common/src/app/common/logging.cljc b/common/src/app/common/logging.cljc index d7780ef70..f1eaee79d 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)) - (str/join ", " result)))) + result + v) + + :else + (recur (next props) + result + body)) + + (let [message (str/join ", " result)] + (if (string? body) + (str message "\n" body) + message))))) (defn build-stack-trace [cause] diff --git a/common/src/app/common/logic/libraries.cljc b/common/src/app/common/logic/libraries.cljc index ccc2f5d34..85382be2c 100644 --- a/common/src/app/common/logic/libraries.cljc +++ b/common/src/app/common/logic/libraries.cljc @@ -266,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 @@ -280,9 +284,27 @@ (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 @@ -1190,7 +1212,7 @@ :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'))) @@ -1345,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'))) @@ -1381,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'))) @@ -1842,12 +1864,11 @@ ;; 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?) - (update :touched cfh/set-touched-group (-> (ctf/find-swap-slot shape - page - {:id (:id file) - :data file} - libraries) - (ctk/build-swap-slot-group))))] + (ctk/set-swap-slot (ctf/find-swap-slot shape + page + {:id (:id file) + :data file} + libraries)))] [new-shape (-> changes ;; Restore the properties diff --git a/common/src/app/common/logic/shapes.cljc b/common/src/app/common/logic/shapes.cljc index 29d58dad6..f5d38f0c2 100644 --- a/common/src/app/common/logic/shapes.cljc +++ b/common/src/app/common/logic/shapes.cljc @@ -204,15 +204,35 @@ (reduce ctp/remove-flow flows))))))] [all-parents changes])) -(defn generate-relocate-shapes [changes objects parents parent-id page-id to-index ids] - (let [groups-to-delete + +(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 + ;; Base case, no next element result (let [group (get objects current-id)] @@ -220,14 +240,14 @@ (not= current-id parent-id) (empty? (remove removed-id? (:shapes group)))) - ;; Adds group to the remove and check its parent + ;; 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 + ;; otherwise recur (recur (first to-check) (rest to-check) removed-id? @@ -248,7 +268,6 @@ #{} ids) - ;; TODO: Probably implementing this using loop/recur will ;; be more efficient than using reduce and continuous data ;; desturcturing. @@ -283,17 +302,7 @@ (->> ids (mapcat #(ctn/get-child-heads objects %)) (map :id))) - - shapes-to-unconstraint ids - - ordered-indexes (cfh/order-by-indexed-shapes objects ids) - shapes (map (d/getf objects) ordered-indexes) - parent (get objects parent-id) - component-main-parent (ctn/find-component-main objects parent false) - child-heads - (->> ordered-indexes - (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) @@ -301,18 +310,23 @@ ;; 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)) + (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 ordered-indexes #(cond-> % (cfh/frame-shape? %) (assoc :hide-in-viewer true)))) + (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)))) + (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)) @@ -345,7 +359,7 @@ (assoc shape :component-root true))) ;; Reset constraints depending on the new parent - (pcb/update-shapes shapes-to-unconstraint + (pcb/update-shapes ids (fn [shape] (let [frame-id (if (= (:type parent) :frame) (:id parent) @@ -370,150 +384,39 @@ (assoc :layout-item-v-sizing :fix)) parent))) - ;; Update grid layout + ;; Change the grid cell in a 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) + (-> (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 ordered-indexes #(assoc % :blocked true))) + (pcb/update-shapes ids #(assoc % :blocked true))) ;; Resize parent containers that need to (pcb/resize-parents parents)))) -(defn generate-move-shapes-to-frame - [changes ids frame-id page-id objects drop-index [row column :as cell]] - (let [lookup (d/getf objects) - frame (get objects frame-id) - layout? (:layout frame) - component-main-frame (ctn/find-component-main objects frame false) - shapes (->> ids - (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 %))))) +(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))) - 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) - - moving-shapes-children-ids - (->> moving-shapes-ids - (mapcat #(cfh/get-children-ids-with-self objects %))) - - child-heads - (->> moving-shapes-ids - (mapcat #(ctn/get-child-heads objects %)) - (map :id))] - (-> 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? objects frame-id)) - (pcb/update-shapes moving-shapes-ids ctl/remove-layout-item-data)) - - ;; Remove the swap slots if it is moving to a different component - (pcb/update-shapes - child-heads - (fn [shape] - (cond-> shape - (not= component-main-frame (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 frame) - (pcb/update-shapes moving-shapes-children-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 child-heads #(assoc % :component-root true))) - - (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) - - ;; Change the grid cell in a grid layout - (cond-> (ctl/grid-layout? objects frame-id) - (-> (pcb/update-shapes - [frame-id] - (fn [frame objects] - (-> frame - ;; Assign the cell when pushing into a specific grid cell - (cond-> (some? cell) - (-> (ctl/free-cell-shapes moving-shapes-ids) - (ctl/push-into-cell moving-shapes-ids row column) - (ctl/assign-cells objects))) - (ctl/assign-cell-positions objects))) - {:with-objects? true}) - (pcb/reorder-grid-children [frame-id]))) - (pcb/remove-objects empty-parents)))) +(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/schema.cljc b/common/src/app/common/schema.cljc index b1e743f64..570cfa062 100644 --- a/common/src/app/common/schema.cljc +++ b/common/src/app/common/schema.cljc @@ -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,9 +105,14 @@ (m/deref s)) (defn error-values + "Get error values form explain data structure" [exp] (malli.error/error-value exp {:malli.error/mask-valid-values '...})) +(defn optional-keys + [schema] + (mu/optional-keys schema default-options)) + (def default-transformer (let [default-decoder {:compile (fn [s _registry] @@ -125,6 +142,20 @@ :decoders coders :encoders coders}))) +(defn encode + ([s val transformer] + (m/encode s val default-options transformer)) + ([s val options transformer] + (m/encode s val options transformer))) + +(defn decode + ([s val] + (m/decode s val default-options default-transformer)) + ([s val transformer] + (m/decode s val default-options transformer)) + ([s val options transformer] + (m/decode s val options transformer))) + (defn validator [s] (if (lazy-schema? s) @@ -137,18 +168,6 @@ (-get-explainer s) (-> s schema m/explainer))) -(defn encode - ([s val transformer] - (m/encode s val default-options transformer)) - ([s val options transformer] - (m/encode s val options transformer))) - -(defn decode - ([s val transformer] - (m/decode s val default-options transformer)) - ([s val options transformer] - (m/decode s val options transformer))) - (defn encoder ([s] (if (lazy-schema? s) @@ -180,11 +199,13 @@ (fn [v] (@vfn v)))) (defn lazy-decoder - [s transformer] - (let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))] - (fn [v] (@vfn v)))) + ([s] (lazy-decoder s default-transformer)) + ([s transformer] + (let [vfn (delay (decoder (if (delay? s) (deref s) s) transformer))] + (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 +218,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] @@ -287,7 +324,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 +344,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") @@ -316,15 +354,8 @@ (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) + (swap! sr/registry assoc type s) + nil)) (defn define "Create ans instance of ILazySchema" @@ -398,8 +429,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 +441,7 @@ (some->> (re-matches uuid-rx s) uuid/uuid) s)) -(define! ::uuid +(register! ::uuid {:type ::uuid :pred uuid? :type-properties @@ -428,23 +459,33 @@ [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) ::oapi/type "string" ::oapi/format "email" - ::oapi/decode parse-email}}) + ::oapi/decode + (fn [v] + (or (parse-email v) v))}}) (def non-empty-strings-xf (comp @@ -452,7 +493,122 @@ (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 [coerce kind max min] :as props} children _] + (let [xform (if coerce + (comp non-empty-strings-xf (map coerce)) + non-empty-strings-xf) + 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)))] + + {: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) + ::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 #{} xform v)))}}))}) + + +(register! ::vec + {:type :vector + :min 0 + :max 1 + :compile + (fn [{:keys [coerce kind max min] :as props} children _] + (let [xform (if coerce + (comp non-empty-strings-xf (map coerce)) + non-empty-strings-xf) + + 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)))] + + {: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) + ::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 [] xform v)))}}))}) + + +(register! ::set-of-strings {:type ::set-of-strings :pred #(and (set? %) (every? string? %)) :type-properties @@ -468,7 +624,7 @@ (let [v (if (string? v) (str/split v #"[\s,]+") v)] (into #{} non-empty-strings-xf v)))}}) -(define! ::set-of-keywords +(register! ::set-of-keywords {:type ::set-of-keywords :pred #(and (set? %) (every? keyword? %)) :type-properties @@ -484,7 +640,7 @@ (let [v (if (string? v) (str/split v #"[\s,]+") v)] (into #{} (comp non-empty-strings-xf (map keyword)) v)))}}) -(define! ::set-of-emails +(register! ::set-of-emails {:type ::set-of-emails :pred #(and (set? %) (every? string? %)) :type-properties @@ -500,7 +656,7 @@ (let [v (if (string? v) (str/split v #"[\s,]+") v)] (into #{} (keep parse-email) v)))}}) -(define! ::set-of-uuid +(register! ::set-of-uuid {:type ::set-of-uuid :pred #(and (set? %) (every? uuid? %)) :type-properties @@ -516,7 +672,7 @@ (let [v (if (string? v) (str/split v #"[\s,]+") v)] (into #{} (keep parse-uuid) v)))}}) -(define! ::coll-of-uuid +(register! ::coll-of-uuid {:type ::set-of-uuid :pred (partial every? uuid?) :type-properties @@ -532,7 +688,7 @@ (let [v (if (string? v) (str/split v #"[\s,]+") v)] (into [] (keep parse-uuid) v)))}}) -(define! ::one-of +(register! ::one-of {:type ::one-of :min 1 :max 1 @@ -555,7 +711,7 @@ ;; Integer/MIN_VALUE (def min-safe-int -2147483648) -(define! ::safe-int +(register! ::safe-int {:type ::safe-int :pred #(and (int? %) (>= max-safe-int %) (>= % min-safe-int)) :type-properties @@ -570,7 +726,7 @@ (parse-long s) s))}}) -(define! ::safe-number +(register! ::safe-number {:type ::safe-number :pred #(and (number? %) (>= max-safe-int %) (>= % min-safe-int)) :type-properties @@ -586,7 +742,7 @@ (parse-double s) s))}}) -(define! ::safe-double +(register! ::safe-double {:type ::safe-double :pred #(and (double? %) (>= max-safe-int %) (>= % min-safe-int)) :type-properties @@ -601,7 +757,7 @@ (parse-double s) s))}}) -(define! ::contains-any +(register! ::contains-any {:type ::contains-any :min 1 :max 1 @@ -619,7 +775,7 @@ {:title "contains" :description "contains predicate"}}))}) -(define! ::inst +(register! ::inst {:type ::inst :pred inst? :type-properties @@ -631,10 +787,12 @@ ::oapi/type "number" ::oapi/format "int64"}}) -(define! ::fn +(register! ::fn [:schema fn?]) -(define! ::word-string +;; FIXME: deprecated, replace with ::text + +(register! ::word-string {:type ::word-string :pred #(and (string? %) (not (str/blank? %))) :property-pred (m/-min-max-pred count) @@ -646,17 +804,106 @@ ::oapi/type "string" ::oapi/format "string"}}) -(define! ::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) ::oapi/type "string" ::oapi/format "uri" - ::oapi/decode (comp u/uri str/trim)}}) + ::oapi/decode + (fn [val] + (if (u/uri? val) + val + (-> val str/trim u/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 diff --git a/common/src/app/common/schema/generators.cljc b/common/src/app/common/schema/generators.cljc index 83e00bfd8..081e1d5ca 100644 --- a/common/src/app/common/schema/generators.cljc +++ b/common/src/app/common/schema/generators.cljc @@ -77,10 +77,23 @@ (defn word-string [] - (->> (tg/such-that #(re-matches #"\w+" %) - tg/string-alphanumeric - 50) - (tg/such-that (complement str/blank?)))) + (as-> tg/string-alphanumeric $$ + (tg/such-that (fn [v] (re-matches #"\w+" v)) $$ 50) + (tg/such-that (fn [v] + (and (not (str/blank? v)) + (not (re-matches #"^\d+.*" v)))) + $$ + 50))) + + +(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 [] diff --git a/common/src/app/common/svg.cljc b/common/src/app/common/svg.cljc index 1b50a9dde..4b9f71572 100644 --- a/common/src/app/common/svg.cljc +++ b/common/src/app/common/svg.cljc @@ -1046,7 +1046,6 @@ (str/includes? data "]*>" ""))) - (defn parse [text] #?(:cljs (tubax/xml->clj text) diff --git a/common/src/app/common/test_helpers/components.cljc b/common/src/app/common/test_helpers/components.cljc index dadd2feac..150bbeeb4 100644 --- a/common/src/app/common/test_helpers/components.cljc +++ b/common/src/app/common/test_helpers/components.cljc @@ -6,6 +6,7 @@ (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] @@ -64,13 +65,12 @@ [file id] (ctkl/get-component (:data file) id)) -(defn set-child-label - [file shape-label child-idx label] - (let [id (-> (ths/get-shape file shape-label) - :shapes - (nth child-idx))] - (when id - (thi/set-id! label 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}] @@ -103,6 +103,7 @@ (and (some? parent) (ctn/in-any-component? (:objects page) parent)) (dissoc :component-root)) + file' (ctf/update-file-data file (fn [file-data] @@ -128,14 +129,14 @@ true))) $ (remove #(= (:id %) (:id copy-root')) copy-shapes)))))] + (when children-labels - (dotimes [idx (count children-labels)] - (set-child-label file' copy-root-label idx (nth children-labels idx)))) + (set-children-labels! file' copy-root-label children-labels)) file')) (defn component-swap - [file shape-label new-component-label new-shape-label & {:keys [library] :as params}] + [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} @@ -147,10 +148,15 @@ ;; 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))] + (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)) - (thf/apply-changes file changes))) + + (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 index 6d89fa475..82ebf5c58 100644 --- a/common/src/app/common/test_helpers/compositions.cljc +++ b/common/src/app/common/test_helpers/compositions.cljc @@ -58,6 +58,28 @@ :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]}] diff --git a/common/src/app/common/test_helpers/shapes.cljc b/common/src/app/common/test_helpers/shapes.cljc index 28e8c5d2c..408e1233e 100644 --- a/common/src/app/common/test_helpers/shapes.cljc +++ b/common/src/app/common/test_helpers/shapes.cljc @@ -12,10 +12,12 @@ [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])) @@ -68,6 +70,19 @@ (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)))) @@ -99,3 +114,19 @@ [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 8ee125e8a..c5d14f549 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,28 @@ (-> (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] @@ -406,6 +428,8 @@ [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)) @@ -425,10 +449,12 @@ :children [(merge {:text pt} text-style)]})))) new-content - {:type "root" - :children - [{:type "paragraph-set" - :children paragraphs}]}] + (d/patch-object + {:type "root" + :children + [{:type "paragraph-set" + :children paragraphs}]} + root-styles)] (assoc shape :content new-content))) diff --git a/common/src/app/common/types/color.cljc b/common/src/app/common/types/color.cljc index 111343d58..5ab2dc635 100644 --- a/common/src/app/common/types/color.cljc +++ b/common/src/app/common/types/color.cljc @@ -7,14 +7,17 @@ (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.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] + [app.common.types.plugins :as ctpg] [app.common.uuid :as uuid] - [clojure.test.check.generators :as tgen])) + [clojure.test.check.generators :as tgen] + [cuerdas.core :as str])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SCHEMAS @@ -35,7 +38,7 @@ (.. g (toString 16) (padStart 2 "0")) (.. b (toString 16) (padStart 2 "0")))))) -(sm/define! ::rgb-color +(sm/register! ::rgb-color {:type ::rgb-color :pred #(and (string? %) (some? (re-matches rgb-color-re %))) :type-properties @@ -47,7 +50,7 @@ ::oapi/type "integer" ::oapi/format "int64"}}) -(sm/define! ::image-color +(sm/register! ::image-color [:map {:title "ImageColor"} [:name {:optional true} :string] [:width :int] @@ -56,7 +59,7 @@ [:id ::sm/uuid] [:keep-aspect-ratio {:optional true} :boolean]]) -(sm/define! ::gradient +(sm/register! ::gradient [:map {:title "Gradient"} [:type [::sm/one-of #{:linear :radial}]] [:start-x ::sm/safe-number] @@ -71,7 +74,7 @@ [:opacity {:optional true} [:maybe ::sm/safe-number]] [:offset ::sm/safe-number]]]]]) -(sm/define! ::color +(sm/register! ::color [:and [:map {:title "Color"} [:id {:optional true} ::sm/uuid] @@ -84,10 +87,12 @@ [:ref-id {:optional true} ::sm/uuid] [:ref-file {:optional true} ::sm/uuid] [:gradient {:optional true} [:maybe ::gradient]] - [:image {:optional true} [:maybe ::image-color]]] + [:image {:optional true} [:maybe ::image-color]] + [:plugin-data {:optional true} + [:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]] [::sm/contains-any {:strict true} [:color :gradient :image]]]) -(sm/define! ::recent-color +(sm/register! ::recent-color [:and [:map {:title "RecentColor"} [:opacity {:optional true} [:maybe ::sm/safe-number]] @@ -381,3 +386,121 @@ (and (some? (:color c1)) (some? (:color c2)) (= (:color c1) (:color c2))))) + + +(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 7c48e7f30..cc064be50 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)) @@ -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] @@ -227,7 +263,6 @@ :shape-ref :touched)) - (defn- extract-ids [shape] (if (map? shape) (let [current-id (:id shape) @@ -256,3 +291,16 @@ ;; 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/container.cljc b/common/src/app/common/types/container.cljc index b50c5058a..3a7f88c12 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,9 @@ [: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} + [:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]]) (def check-container! (sm/check-fn ::container)) @@ -375,8 +378,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}) @@ -495,7 +501,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) @@ -531,3 +537,48 @@ (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 + [shape attr val & {:keys [on-changed ignore-touched ignore-geometry]}] + (let [group (get ctk/sync-attrs attr) + shape-val (get shape attr) + ignore (or ignore-touched (= attr :position-data)) ;; position-data is a derived attribute and + is-geometry? (and (or (= group :geometry-group) ;; never triggers touched by itself + (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 on-changed 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 ctk/set-touched-group group) + (dissoc :remote-synced)) + + (nil? val) + (dissoc attr) + + (some? val) + (assoc attr val)))) diff --git a/common/src/app/common/types/file.cljc b/common/src/app/common/types/file.cljc index ab00f1de3..fefef7b75 100644 --- a/common/src/app/common/types/file.cljc +++ b/common/src/app/common/types/file.cljc @@ -24,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.token :as cto] [app.common.types.typographies-list :as ctyl] @@ -35,7 +36,7 @@ ;; SCHEMA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(sm/define! ::media-object +(sm/register! ::media-object [:map {:title "FileMediaObject"} [:id ::sm/uuid] [:name :string] @@ -44,7 +45,7 @@ [:mtype :string] [:path {:optional true} [:maybe :string]]]) -(sm/define! ::data +(sm/register! ::data [:map {:title "FileData"} [:pages [:vector ::sm/uuid]] [:pages-index @@ -60,7 +61,9 @@ [:tokens {:optional true} [:map-of {:gen/max 100} ::sm/uuid ::cto/token]] [: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} + [:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]]) (def check-file-data! (sm/check-fn ::data)) @@ -636,19 +639,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)] + (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. - (cond-> file-data - (d/not-empty? used-components) - (absorb-components used-components library-data) + 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)) - (d/not-empty? used-colors) - (absorb-colors used-colors) - - (d/not-empty? used-typographies) - (absorb-typographies used-typographies)))) + 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 29e90af4c..72a7ceac6 100644 --- a/common/src/app/common/types/grid.cljc +++ b/common/src/app/common/types/grid.cljc @@ -13,12 +13,12 @@ ;; SCHEMA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(sm/def! ::grid-color +(sm/register! ::grid-color [:map {:title "PageGridColor"} [:color ::ctc/rgb-color] [:opacity ::sm/safe-number]]) -(sm/def! ::column-params +(sm/register! ::column-params [:map [:color ::grid-color] [:type {:optional true} [::sm/one-of #{:stretch :left :center :right}]] @@ -27,12 +27,12 @@ [:item-length {:optional true} [:maybe ::sm/safe-number]] [:gutter {:optional true} [:maybe ::sm/safe-number]]]) -(sm/def! ::square-params +(sm/register! ::square-params [:map [:size {:optional true} [:maybe ::sm/safe-number]] [:color ::grid-color]]) -(sm/def! ::grid +(sm/register! ::grid [:multi {:dispatch :type} [:column [:map @@ -52,7 +52,7 @@ [:display :boolean] [:params ::square-params]]]]) -(sm/def! ::saved-grids +(sm/register! ::saved-grids [:map {:title "PageGrid"} [:square {:optional true} ::square-params] [:row {:optional true} ::column-params] diff --git a/common/src/app/common/types/page.cljc b/common/src/app/common/types/page.cljc index 0b2038928..3b00643ce 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 +(sm/register! ::flow [:map {:title "PageFlow"} [:id ::sm/uuid] [:name :string] [:starting-frame ::sm/uuid]]) -(sm/define! ::guide +(sm/register! ::guide [:map {:title "PageGuide"} [:id ::sm/uuid] [:axis [::sm/one-of #{:x :y}]] [:position ::sm/safe-number] [:frame-id {:optional true} [:maybe ::sm/uuid]]]) -(sm/define! ::page +(sm/register! ::page [:map {:title "FilePage"} [:id ::sm/uuid] [:name :string] @@ -43,7 +44,9 @@ [:flows {:optional true} [:vector {:gen/max 2} ::flow]] [:guides {:optional true} - [:map-of {:gen/max 2} ::sm/uuid ::guide]]]]]) + [:map-of {:gen/max 2} ::sm/uuid ::guide]] + [:plugin-data {:optional true} + [:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]]]]) (def check-page-guide! (sm/check-fn ::guide)) diff --git a/common/src/app/common/types/plugins.cljc b/common/src/app/common/types/plugins.cljc new file mode 100644 index 000000000..49d31bf2d --- /dev/null +++ b/common/src/app/common/types/plugins.cljc @@ -0,0 +1,16 @@ +;; 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])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; SCHEMAS +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(sm/register! ::plugin-data + [:map-of {:gen/max 5} :string :string]) diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index d46214c6e..50e27a0af 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] @@ -80,10 +81,16 @@ (def text-align-types #{"left" "right" "center" "justify"}) -(sm/define! ::points +(def bool-types + #{:union + :difference + :exclude + :intersection}) + +(sm/register! ::points [:vector {:gen/max 4 :gen/min 4} ::gpt/point]) -(sm/define! ::fill +(sm/register! ::fill [:map {:title "Fill"} [:fill-color {:optional true} ::ctc/rgb-color] [:fill-opacity {:optional true} ::sm/safe-number] @@ -92,7 +99,7 @@ [:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]] [:fill-image {:optional true} ::ctc/image-color]]) -(sm/define! ::stroke +(sm/register! ::stroke [:map {:title "Stroke"} [:stroke-color {:optional true} :string] [:stroke-color-ref-file {:optional true} ::sm/uuid] @@ -110,7 +117,7 @@ [:stroke-color-gradient {:optional true} ::ctc/gradient] [:stroke-image {:optional true} ::ctc/image-color]]) -(sm/define! ::shape-base-attrs +(sm/register! ::shape-base-attrs [:map {:title "ShapeMinimalRecord"} [:id ::sm/uuid] [:name :string] @@ -122,14 +129,14 @@ [:parent-id ::sm/uuid] [:frame-id ::sm/uuid]]) -(sm/define! ::shape-geom-attrs +(sm/register! ::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 +(sm/register! ::shape-attrs [:map {:title "ShapeAttrs"} [:name {:optional true} :string] [:component-id {:optional true} ::sm/uuid] @@ -181,15 +188,17 @@ [:vector {:gen/max 1} ::ctss/shadow]] [:blur {:optional true} ::ctsb/blur] [:grow-type {:optional true} - [::sm/one-of #{:auto-width :auto-height :fixed}]] - [:applied-tokens {:optional true} ::cto/applied-tokens]]) + [::sm/one-of #{:auto-width :auto-height :fixed}]]]) + [:applied-tokens {:optional true} ::cto/applied-tokens] + [:plugin-data {:optional true} + [:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]] -(sm/define! ::group-attrs +(sm/register! ::group-attrs [:map {:title "GroupAttrs"} [:type [:= :group]] [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]]]) -(sm/define! ::frame-attrs +(sm/register! ::frame-attrs [:map {:title "FrameAttrs"} [:type [:= :frame]] [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] @@ -197,13 +206,15 @@ [:show-content {:optional true} :boolean] [:hide-in-viewer {:optional true} :boolean]]) -(sm/define! ::bool-attrs +(sm/register! ::bool-attrs [:map {:title "BoolAttrs"} [:type [:= :bool]] [:shapes [:vector {:gen/max 10 :gen/min 1} ::sm/uuid]] - ;; FIXME: improve this schema [:bool-type :keyword] + ;; FIXME: This should be the spec but we need to create a migration + ;; to make this transition safely + ;; [:bool-type [::sm/one-of bool-types]] [:bool-content [:vector {:gen/max 2} @@ -215,19 +226,19 @@ [:maybe [:map-of {:gen/max 5} :keyword ::sm/safe-number]]]]]]]) -(sm/define! ::rect-attrs +(sm/register! ::rect-attrs [:map {:title "RectAttrs"} [:type [:= :rect]]]) -(sm/define! ::circle-attrs +(sm/register! ::circle-attrs [:map {:title "CircleAttrs"} [:type [:= :circle]]]) -(sm/define! ::svg-raw-attrs +(sm/register! ::svg-raw-attrs [:map {:title "SvgRawAttrs"} [:type [:= :svg-raw]]]) -(sm/define! ::image-attrs +(sm/register! ::image-attrs [:map {:title "ImageAttrs"} [:type [:= :image]] [:metadata @@ -237,17 +248,17 @@ [:mtype {:optional true} [:maybe :string]] [:id ::sm/uuid]]]]) -(sm/define! ::path-attrs +(sm/register! ::path-attrs [:map {:title "PathAttrs"} [:type [:= :path]] [:content ::ctsp/content]]) -(sm/define! ::text-attrs +(sm/register! ::text-attrs [:map {:title "TextAttrs"} [:type [:= :text]] [:content {:optional true} [:maybe ::ctsx/content]]]) -(sm/define! ::shape-map +(sm/register! ::shape-map [:multi {:dispatch :type :title "Shape"} [:group [:and {:title "GroupShape"} @@ -319,7 +330,7 @@ ::text-attrs ::ctsl/layout-child-attrs]]]) -(sm/define! ::shape +(sm/register! ::shape [:and {:title "Shape" :gen/gen (->> (sg/generator ::shape-base-attrs) @@ -465,9 +476,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)))) @@ -490,8 +506,8 @@ (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 2c4ce5ab4..796c0d170 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 6d7953a88..7adbf7574 100644 --- a/common/src/app/common/types/shape/export.cljc +++ b/common/src/app/common/types/shape/export.cljc @@ -8,8 +8,10 @@ (:require [app.common.schema :as sm])) -(sm/def! ::export +(def export-types #{:png :jpeg :svg :pdf}) + +(sm/register! ::export [:map {:title "ShapeExport"} - [:type :keyword] + [:type [::sm/one-of export-types]] [:scale ::sm/safe-number] [:suffix :string]]) diff --git a/common/src/app/common/types/shape/interactions.cljc b/common/src/app/common/types/shape/interactions.cljc index 05724ebe8..647e6cf26 100644 --- a/common/src/app/common/types/shape/interactions.cljc +++ b/common/src/app/common/types/shape/interactions.cljc @@ -71,7 +71,7 @@ (def animation-types #{:dissolve :slide :push}) -(sm/define! ::animation +(sm/register! ::animation [:multi {:dispatch :animation-type :title "Animation"} [:dissolve [:map {:title "AnimationDisolve"} @@ -96,7 +96,7 @@ (def check-animation! (sm/check-fn ::animation)) -(sm/define! ::interaction +(sm/register! ::interaction [:multi {:dispatch :action-type} [:navigate [:map diff --git a/common/src/app/common/types/shape/layout.cljc b/common/src/app/common/types/shape/layout.cljc index 7f5e6e83a..a999145cb 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? @@ -604,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] @@ -1469,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)])))) diff --git a/common/src/app/common/types/shape/path.cljc b/common/src/app/common/types/shape/path.cljc index d633bb85c..f6002a293 100644 --- a/common/src/app/common/types/shape/path.cljc +++ b/common/src/app/common/types/shape/path.cljc @@ -12,7 +12,7 @@ ;; SCHEMA ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(sm/define! ::segment +(sm/register! ::segment [:multi {:title "PathSegment" :dispatch :command} [:line-to [:map @@ -43,5 +43,5 @@ [:c2x ::sm/safe-number] [:c2y ::sm/safe-number]]]]]]) -(sm/define! ::content +(sm/register! ::content [:vector ::segment]) diff --git a/common/src/app/common/types/shape/shadow.cljc b/common/src/app/common/types/shape/shadow.cljc index cc2fd81c3..62bdc2691 100644 --- a/common/src/app/common/types/shape/shadow.cljc +++ b/common/src/app/common/types/shape/shadow.cljc @@ -11,7 +11,7 @@ (def styles #{:drop-shadow :inner-shadow}) -(sm/def! ::shadow +(sm/register! ::shadow [:map {:title "Shadow"} [:id [:maybe ::sm/uuid]] [:style [::sm/one-of styles]] diff --git a/common/src/app/common/types/shape/text.cljc b/common/src/app/common/types/shape/text.cljc index dff875956..99d3a55b5 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/typography.cljc b/common/src/app/common/types/typography.cljc index 6e216020a..e143a2b8b 100644 --- a/common/src/app/common/types/typography.cljc +++ b/common/src/app/common/types/typography.cljc @@ -9,13 +9,14 @@ [app.common.data :as d] [app.common.schema :as sm] [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] @@ -29,7 +30,9 @@ [: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} + [:map-of {:gen/max 5} :keyword ::ctpg/plugin-data]]]) (def check-typography! (sm/check-fn ::typography)) diff --git a/common/test/cases/detach-with-nested.penpot b/common/test/cases/detach-with-nested.penpot new file mode 100644 index 000000000..2ff274b6d Binary files /dev/null and b/common/test/cases/detach-with-nested.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 000000000..0de71803b Binary files /dev/null and b/common/test/cases/remove-swap-slots.penpot differ 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 000000000..d7b999db3 --- /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 index 5077b6fde..0decac57c 100644 --- a/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc +++ b/common/test/common_tests/logic/comp_remove_swap_slots_test.cljc @@ -61,41 +61,12 @@ blue1 (ths/get-shape file :blue1) ;; ==== Action - changes (cls/generate-relocate-shapes (pcb/empty-changes nil) - (:objects page) - #{(:parent-id blue1)} ;; parents - 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-move-blue1-to-root - (let [;; ==== Setup - file (setup-file) - page (thf/current-page file) - blue1 (ths/get-shape file :blue1) - - ;; ==== Action - changes (cls/generate-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id blue1)} ;; ids - uuid/zero ;; frame-id - (:id page) ;; page-id - (:objects page) ;; objects - 0 ;; drop-index - nil) ;; cell + 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) @@ -111,7 +82,6 @@ (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) @@ -121,43 +91,12 @@ ;; ==== Action - changes (cls/generate-relocate-shapes (pcb/empty-changes nil) - (:objects page) - #{(:parent-id blue1)} ;; parents - (: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-move-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-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id blue1)} ;; ids - (:id b2) ;; frame-id - (:id page) ;; page-id - (:objects page) ;; objects - 0 ;; drop-index - nil) ;; cell + 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) @@ -182,26 +121,26 @@ ;; ==== Action ;; Move blue1 into yellow - changes (cls/generate-relocate-shapes (pcb/empty-changes nil) - (:objects page) - #{(:parent-id blue1)} ;; parents - (:id yellow) ;; parent-id - (:id page) ;; page-id - 0 ;; to-index - #{(:id blue1)}) ;; ids + 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-shapes (pcb/empty-changes nil) - (:objects page') - #{(:parent-id yellow')} ;; parents - uuid/zero ;; parent-id - (:id page') ;; page-id - 0 ;; to-index - #{(:id yellow')}) ;; ids + 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 @@ -216,50 +155,6 @@ (t/is (some? blue1'')) (t/is (nil? (ctk/get-swap-slot blue1''))))) -(t/deftest test-remove-swap-slot-move-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-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id blue1)} ;; ids - (:id yellow) ;; frame-id - (:id page) ;; page-id - (:objects page) ;; objects - 0 ;; drop-index - nil) ;; cell - - 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-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id yellow')} ;; ids - uuid/zero ;; frame-id - (:id page') ;; page-id - (:objects page') ;; objects - 0 ;; drop-index - nil) ;; cell - 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) @@ -269,13 +164,13 @@ ;; ==== Action ;; Move blue1 into yellow - changes (cls/generate-relocate-shapes (pcb/empty-changes nil) - (:objects page) - #{(:parent-id blue1)} ;; parents - (:id yellow) ;; parent-id - (:id page) ;; page-id - 0 ;; to-index - #{(:id blue1)}) ;; ids + 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') @@ -283,57 +178,12 @@ b2' (ths/get-shape file' :frame-b2) ;; Move yellow into b2 - changes' (cls/generate-relocate-shapes (pcb/empty-changes nil) - (:objects page') - #{(:parent-id yellow')} ;; parents - (: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''))))) - -(t/deftest test-remove-swap-slot-move-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-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id blue1)} ;; ids - (:id yellow) ;; frame-id - (:id page) ;; page-id - (:objects page) ;; objects - 0 ;; drop-index - nil) ;; cell - - 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-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id yellow')} ;; ids - (:id b2') ;; frame-id - (:id page') ;; page-id - (:objects page') ;; objects - 0 ;; drop-index - nil) ;; cell + 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') @@ -404,13 +254,12 @@ ;; ==== Action ;; Move blue1 into yellow - changes (cls/generate-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id blue1)} ;; ids - (:id yellow) ;; frame-id - (:id page) ;; page-id - (:objects page) ;; objects - 0 ;; drop-index - nil) ;; cell + 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') @@ -459,13 +308,13 @@ blue1 (ths/get-shape file :blue1) ;; ==== Action - changes (cls/generate-relocate-shapes (pcb/empty-changes nil) - (:objects page) - #{(:parent-id blue1)} ;; parents - (:parent-id blue1) ;; parent-id - (:id page) ;; page-id - 2 ;; to-index - #{(:id blue1)}) ;; ids + 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 @@ -489,13 +338,12 @@ ;; ==== Action ;; Move blue1 into yellow - changes (cls/generate-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id blue1)} ;; ids - (:id yellow) ;; frame-id - (:id page) ;; page-id - (:objects page) ;; objects - 0 ;; drop-index - nil) ;; cell + 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) @@ -503,13 +351,12 @@ page' (thf/current-page file') blue1' (ths/get-shape file' :blue1) b1' (ths/get-shape file' :frame-b1) - changes' (cls/generate-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id blue1')} ;; ids - (:id b1') ;; frame-id - (:id page') ;; page-id - (:objects page') ;; objects - 0 ;; drop-index - nil) ;; cell + 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') @@ -535,13 +382,13 @@ ;; ==== Action ;; Relocate blue1 into yellow - changes (cls/generate-relocate-shapes (pcb/empty-changes nil) - (:objects page) - #{(:parent-id blue1)} ;; parents - (:id yellow) ;; parent-id - (:id page) ;; page-id - 0 ;; to-index - #{(:id blue1)}) ;; ids + 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) @@ -549,13 +396,13 @@ page' (thf/current-page file') blue1' (ths/get-shape file' :blue1) b1' (ths/get-shape file' :frame-b1) - changes' (cls/generate-relocate-shapes (pcb/empty-changes nil) - (:objects page') - #{(:parent-id blue1')} ;; parents - (:id b1') ;; parent-id - (:id page') ;; page-id - 0 ;; to-index - #{(:id blue1')}) ;; ids + 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') @@ -581,43 +428,12 @@ green-copy (ths/get-shape file :green-copy) ;; ==== Action - changes (cls/generate-relocate-shapes (pcb/empty-changes nil) - (:objects page) - #{(:parent-id green-copy)} ;; parents - 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-moving-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-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id green-copy)} ;; ids - uuid/zero ;; frame-id - (:id page) ;; page-id - (:objects page) ;; objects - 0 ;; drop-index - nil) ;; cell + 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) @@ -633,7 +449,6 @@ (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) @@ -644,44 +459,12 @@ b2 (ths/get-shape file :frame-b2) ;; ==== Action - changes (cls/generate-relocate-shapes (pcb/empty-changes nil) - (:objects page) - #{(:parent-id green-copy)} ;; parents - (: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-moving-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-move-shapes-to-frame (pcb/empty-changes nil) - #{(:id green-copy)} ;; ids - (:id b2) ;; frame-id - (:id page) ;; page-id - (:objects page) ;; objects - 0 ;; drop-index - nil) ;; cell + 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) @@ -697,7 +480,6 @@ (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) @@ -743,10 +525,6 @@ (t/deftest test-swap-outside-component-doesnt-have-swap-slot (let [;; ==== Setup file (setup-file) - - page (thf/current-page file) - blue1 (ths/get-shape file :blue1) - ;; ==== Action file' (-> file @@ -761,3 +539,31 @@ ;; 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 index d7f441ed9..1c663917e 100644 --- a/common/test/common_tests/logic/comp_reset_test.cljc +++ b/common/test/common_tests/logic/comp_reset_test.cljc @@ -136,13 +136,13 @@ ;; 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-shapes (pcb/empty-changes) - (:objects page) - #{(:parent-id copy-root)} ; parents - (thi/id :copy-root) ; parent-id - (:id page) ; page-id - 0 ; to-index - #{(thi/id :free-shape)}) ; ids + 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) @@ -231,13 +231,13 @@ ;; 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-shapes (pcb/empty-changes) - (:objects page) - #{(:parent-id copy-child1)} ; parents - (thi/id :copy-root) ; parent-id - (:id page) ; page-id - 2 ; to-index - #{(:id copy-child1)}) ; ids + 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) diff --git a/common/test/common_tests/logic/comp_sync_test.cljc b/common/test/common_tests/logic/comp_sync_test.cljc index 75abacea3..94f093ff3 100644 --- a/common/test/common_tests/logic/comp_sync_test.cljc +++ b/common/test/common_tests/logic/comp_sync_test.cljc @@ -196,13 +196,15 @@ main-root (ths/get-shape file :main-root) ;; ==== Action - changes1 (cls/generate-relocate-shapes (pcb/empty-changes) - (:objects page) - #{(:parent-id main-root)} ; parents - (thi/id :main-root) ; parent-id - (:id page) ; page-id - 0 ; to-index - #{(thi/id :free-shape)}) ; ids + 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) @@ -294,13 +296,13 @@ main-child1 (ths/get-shape file :main-child1) ;; ==== Action - changes1 (cls/generate-relocate-shapes (pcb/empty-changes) - (:objects page) - #{(:parent-id main-child1)} ; parents - (thi/id :main-root) ; parent-id - (:id page) ; page-id - 2 ; to-index - #{(:id main-child1)}) ; ids + 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) diff --git a/common/test/common_tests/logic/comp_touched_test.cljc b/common/test/common_tests/logic/comp_touched_test.cljc index a0907e37c..1f16a2107 100644 --- a/common/test/common_tests/logic/comp_touched_test.cljc +++ b/common/test/common_tests/logic/comp_touched_test.cljc @@ -112,13 +112,13 @@ ;; 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-shapes (pcb/empty-changes) - (:objects page) - #{(:parent-id copy-root)} ; parents - (thi/id :copy-root) ; parent-id - (:id page) ; page-id - 0 ; to-index - #{(thi/id :free-shape)}) ; ids + 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) @@ -187,13 +187,13 @@ ;; 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-shapes (pcb/empty-changes) - (:objects page) - #{(:parent-id copy-child1)} ; parents - (thi/id :copy-root) ; parent-id - (:id page) ; page-id - 2 ; to-index - #{(:id copy-child1)}) ; ids + 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) 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 000000000..051a4732e --- /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 000000000..f85a72cbe --- /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/types/types_component_test.cljc b/common/test/common_tests/types/types_component_test.cljc new file mode 100644 index 000000000..cff174329 --- /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/yarn.lock b/common/yarn.lock index a4d30e0be..94f9b89aa 100644 --- a/common/yarn.lock +++ b/common/yarn.lock @@ -352,11 +352,11 @@ __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 @@ -913,7 +913,7 @@ __metadata: 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: 10c0/02e26a0b039c11fd5b75e1d734c8f0332c95510f6a514a9a0991023e43fb233884da02d7f966823ffb230632a733fc86d4a4b1e63c3fbe00058b8ee0f8c728af @@ -1420,10 +1420,10 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.2.4": - version: 1.3.0 - resolution: "sax@npm:1.3.0" - checksum: 10c0/599dbe0ba9d8bd55e92d920239b21d101823a6cedff71e542589303fa0fa8f3ece6cf608baca0c51be846a2e88365fac94a9101a9c341d94b98e30c4deea5bea +"sax@npm:^1.4.1": + version: 1.4.1 + resolution: "sax@npm:1.4.1" + checksum: 10c0/6bf86318a254c5d898ede6bd3ded15daf68ae08a5495a2739564eb265cd13bcc64a07ab466fb204f67ce472bb534eb8612dac587435515169593f4fffa11de7c languageName: node linkType: hard @@ -1476,9 +1476,9 @@ __metadata: 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" @@ -1488,7 +1488,7 @@ __metadata: ws: "npm:^7.4.6" bin: shadow-cljs: cli/runner.js - checksum: 10c0/bae23e71df9c2b2979259a0cde8747c923ee295f58ab4637c9d6b103d82542b40ef39172d4be2dbb94af2e6458a177d1ec96c1eb1e73b1d8f3a4ddb5eaaba7d4 + checksum: 10c0/c5c77d524ee8f44e4ae2ddc196af170d02405cc8731ea71f852c7b220fc1ba8aaf5cf33753fd8a7566c8749bb75d360f903dfb0d131bcdc6c2c33f44404bd6a3 languageName: node linkType: hard @@ -1852,7 +1852,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.13.0": +"ws@npm:^8.17.0": version: 8.17.0 resolution: "ws@npm:8.17.0" peerDependencies: diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index c67d3cc82..7182905a6 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:22.04 +FROM debian:bookworm LABEL maintainer="Andrey Antukh " ARG DEBIAN_FRONTEND=noninteractive @@ -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,10 +132,11 @@ 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; \ @@ -244,12 +244,6 @@ 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 diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index bd1a3167b..0d6aa068f 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 diff --git a/docker/devenv/files/nginx.conf b/docker/devenv/files/nginx.conf index 60d025284..961b3a4fb 100644 --- a/docker/devenv/files/nginx.conf +++ b/docker/devenv/files/nginx.conf @@ -68,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; @@ -146,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; diff --git a/docker/images/docker-compose.yaml b/docker/images/docker-compose.yaml index fd7cd976f..d16402ce5 100644 --- a/docker/images/docker-compose.yaml +++ b/docker/images/docker-compose.yaml @@ -1,6 +1,3 @@ ---- -version: "3.8" - 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/exporter/deps.edn b/exporter/deps.edn index 547e131f6..0bcd1ec3b 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.28.3"}}} + {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 0a5b65356..49da8e876 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.2.2", + "packageManager": "yarn@4.3.1", "repository": { "type": "git", "url": "https://github.com/penpot/penpot" }, "dependencies": { - "archiver": "^6.0.2", + "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.43.0", + "playwright": "^1.44.1", "raw-body": "^2.5.2", "xml-js": "^1.6.11", "xregexp": "^5.1.1" }, "devDependencies": { - "shadow-cljs": "2.28.3", + "shadow-cljs": "2.28.11", "source-map-support": "^0.5.21" }, "scripts": { diff --git a/exporter/src/app/config.cljs b/exporter/src/app/config.cljs index ae590e53f..4c0088077 100644 --- a/exporter/src/app/config.cljs +++ b/exporter/src/app/config.cljs @@ -17,7 +17,7 @@ (def ^:private defaults {:public-uri "http://localhost:3449" - :tenant "dev" + :tenant "default" :host "localhost" :http-server-port 6061 :http-server-host "0.0.0.0" diff --git a/exporter/yarn.lock b/exporter/yarn.lock index 1cc17aa2e..cd19abac0 100644 --- a/exporter/yarn.lock +++ b/exporter/yarn.lock @@ -72,6 +72,15 @@ __metadata: languageName: node linkType: hard +"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" @@ -121,32 +130,33 @@ __metadata: 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: 10c0/fc646fe1f8e3650383b6f79384e1c8f69caf7685c705221e23393a674ee1d67331e246250a72b03ec2fbdb2cfe30adc2d4287f6357684d6843d604738bf2c870 + readable-stream: "npm:^4.0.0" + checksum: 10c0/3782c5fa9922186aa1a8e41ed0c2867569faa5f15c8e5e6418ea4c1b730b476e21bd68270b3ea457daf459ae23aaea070b2b9f90cf90a59def8dc79b9e4ef538 languageName: node linkType: hard -"archiver@npm:^6.0.2": - version: 6.0.2 - resolution: "archiver@npm:6.0.2" +"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: 10c0/23a470d468c01cd40fc13b6bd3dbc6d04c4f7b770785dcc7e1e4af256c3d79c4ffd7f7e0e84ae320437e5b8d0a2117aecfca0586b8c0fbd6edc3e04977c438cc + zip-stream: "npm:^6.0.1" + checksum: 10c0/02afd87ca16f6184f752db8e26884e6eff911c476812a0e7f7b26c4beb09f06119807f388a8e26ed2558aa8ba9db28646ebd147a4f99e46813b8b43158e1438e languageName: node linkType: hard @@ -199,7 +209,7 @@ __metadata: 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: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf @@ -310,10 +320,10 @@ __metadata: languageName: node linkType: hard -"buffer-crc32@npm:^0.2.1": - version: 0.2.13 - resolution: "buffer-crc32@npm:0.2.13" - checksum: 10c0/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 @@ -342,6 +352,16 @@ __metadata: 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" @@ -436,15 +456,16 @@ __metadata: languageName: node linkType: hard -"compress-commons@npm:^5.0.1": - version: 5.0.3 - resolution: "compress-commons@npm:5.0.3" +"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: 10c0/ca7fe7ec4feb2854876df928192fc9b2bece15690e171d771a23a8e54a97ef78c057791d0fadc5c6c6703831687facd1f2428bb0dff3187caa2d631d92be69fc + readable-stream: "npm:^4.0.0" + checksum: 10c0/2347031b7c92c8ed5011b07b93ec53b298fa2cd1800897532ac4d4d1aeae06567883f481b6e35f13b65fc31b190c751df6635434d525562f0203fde76f1f0814 languageName: node linkType: hard @@ -495,13 +516,13 @@ __metadata: languageName: node linkType: hard -"crc32-stream@npm:^5.0.0": - version: 5.0.1 - resolution: "crc32-stream@npm:5.0.1" +"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: 10c0/32fdffdd6e80f08ffef03a120a23fad7fdd04bd9c386dd8b9c8d27f58b32b78f6a1f43a327812858a0237aec72d55b77e33f5229cbbc0ee4856a71ea010c6aa8 + readable-stream: "npm:^4.0.0" + checksum: 10c0/bf9c84571ede2d119c2b4f3a9ef5eeb9ff94b588493c0d3862259af86d3679dcce1c8569dd2b0a6eff2f35f5e2081cc1263b846d2538d4054da78cf34f262a3d languageName: node linkType: hard @@ -723,7 +744,14 @@ __metadata: languageName: node linkType: hard -"events@npm:^3.0.0": +"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.3.0": version: 3.3.0 resolution: "events@npm:3.3.0" checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 @@ -752,15 +780,15 @@ __metadata: version: 0.0.0-use.local resolution: "exporter@workspace:." dependencies: - archiver: "npm:^6.0.2" + 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.43.0" + playwright: "npm:^1.44.1" raw-body: "npm:^2.5.2" - shadow-cljs: "npm:2.28.3" + shadow-cljs: "npm:2.28.11" source-map-support: "npm:^0.5.21" xml-js: "npm:^1.6.11" xregexp: "npm:^5.1.1" @@ -802,13 +830,6 @@ __metadata: languageName: node linkType: hard -"fs.realpath@npm:^1.0.0": - version: 1.0.0 - resolution: "fs.realpath@npm:1.0.0" - checksum: 10c0/444cf1291d997165dfd4c0d58b69f0e4782bfd9149fd72faa4fe299e68e0e93d6db941660b37dd29153bf7186672ececa3b50b7e7249477b03fdf850f287c948 - languageName: node - linkType: hard - "fsevents@npm:2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" @@ -855,6 +876,21 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.0.0": + version: 10.4.1 + resolution: "glob@npm:10.4.1" + dependencies: + foreground-child: "npm:^3.1.0" + 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: 10c0/77f2900ed98b9cc2a0e1901ee5e476d664dae3cd0f1b662b8bfd4ccf00d0edc31a11595807706a274ca10e1e251411bbf2e8e976c82bed0d879a9b89343ed379 + languageName: node + linkType: hard + "glob@npm:^10.2.2, glob@npm:^10.3.10": version: 10.3.16 resolution: "glob@npm:10.3.16" @@ -870,19 +906,6 @@ __metadata: languageName: node linkType: hard -"glob@npm:^8.0.0": - version: 8.1.0 - resolution: "glob@npm:8.1.0" - 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: 10c0/cb0b5cab17a59c57299376abe5646c7070f8acb89df5595b492dba3bfb43d301a46c01e5695f01154e6553168207cb60d4eaf07d3be4bc3eb9b0457c5c561d0f - languageName: node - linkType: hard - "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -1038,7 +1061,7 @@ __metadata: 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: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb @@ -1066,23 +1089,6 @@ __metadata: 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: 10c0/7faca22584600a9dc5b9fca2cd5feb7135ac8c935449837b315676b4c90aa4f391ec4f42240178244b5a34e8bede1948627fda392ca3191522fc46b34e985ab2 - 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": - version: 2.0.4 - resolution: "inherits@npm:2.0.4" - checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 - languageName: node - linkType: hard - "inherits@npm:2.0.3": version: 2.0.3 resolution: "inherits@npm:2.0.3" @@ -1090,7 +1096,14 @@ __metadata: languageName: node linkType: hard -"ioredis@npm:^5.3.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": + version: 2.0.4 + resolution: "inherits@npm:2.0.4" + checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2 + languageName: node + linkType: hard + +"ioredis@npm:^5.4.1": version: 5.4.1 resolution: "ioredis@npm:5.4.1" dependencies: @@ -1131,6 +1144,13 @@ __metadata: 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" @@ -1282,7 +1302,7 @@ __metadata: 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: @@ -1291,7 +1311,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^9.0.1": +"minimatch@npm:^9.0.1, minimatch@npm:^9.0.4": version: 9.0.4 resolution: "minimatch@npm:9.0.4" dependencies: @@ -1374,6 +1394,13 @@ __metadata: 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 + "minizlib@npm:^2.1.1, minizlib@npm:^2.1.2": version: 2.1.2 resolution: "minizlib@npm:2.1.2" @@ -1502,15 +1529,6 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0": - version: 1.4.0 - resolution: "once@npm:1.4.0" - dependencies: - wrappy: "npm:1" - checksum: 10c0/5d48aca287dfefabd756621c5dfce5c91a549a93e9fdb7b8246bc4c4790aa2ec17b34a260530474635147aeb631a2dcc8b32c613df0675f96041cbb8244517d0 - languageName: node - linkType: hard - "os-browserify@npm:^0.3.0": version: 0.3.0 resolution: "os-browserify@npm:0.3.0" @@ -1562,7 +1580,7 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.11.0": +"path-scurry@npm:^1.11.0, path-scurry@npm:^1.11.1": version: 1.11.1 resolution: "path-scurry@npm:1.11.1" dependencies: @@ -1585,27 +1603,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.44.0": - version: 1.44.0 - resolution: "playwright-core@npm:1.44.0" +"playwright-core@npm:1.44.1": + version: 1.44.1 + resolution: "playwright-core@npm:1.44.1" bin: playwright-core: cli.js - checksum: 10c0/e1220371a76cdf145f6aaefb2dd6c5194531d1c1e2b67712c56dbc1d589dffb66fd4fc0168be60cd2115aca40660aa13c572e14be47674c0542bc879705b9fb3 + checksum: 10c0/6ffa3a04822b3df86d7f47a97e4f20318c0c50868ba4311820e6626ecadaab1424fbd0a3d01f0b4228adc0c781115e44b801742a4970b88739f804d82f142d68 languageName: node linkType: hard -"playwright@npm:^1.43.0": - version: 1.44.0 - resolution: "playwright@npm:1.44.0" +"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.44.0" + playwright-core: "npm:1.44.1" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/dcbee9022623dd9e219e9867983789262e80339f0c3601219930883e5a304ce75e1397715c0f378a2bab0a799cf88a73ea4b58fe595cfd9058bd7a82f5d8e3b6 + checksum: 10c0/de827d17746b18ae2ec67d510a640d8ceebf8ee8e3d8399bccffa83b76a967498ca377777e4e6a1daaef4b3c86cb2c44c7468de53d2d915acc61b3b89c032738 languageName: node linkType: hard @@ -1737,7 +1755,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -1748,6 +1766,19 @@ __metadata: 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 + "readdir-glob@npm:^1.1.2": version: 1.1.3 resolution: "readdir-glob@npm:1.1.3" @@ -1888,9 +1919,9 @@ __metadata: languageName: node linkType: hard -"shadow-cljs@npm:2.28.3": - version: 2.28.3 - resolution: "shadow-cljs@npm:2.28.3" +"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" @@ -1900,7 +1931,7 @@ __metadata: ws: "npm:^7.4.6" bin: shadow-cljs: cli/runner.js - checksum: 10c0/623b536a0d95d7696dd465c09ab3cb5d921c867a577a33463ad58dbc40f51f5d0424ba2791a8803f33a94f5c877198de91c3c7f7616618a6b4ae90e80d5d213e + checksum: 10c0/c5c77d524ee8f44e4ae2ddc196af170d02405cc8731ea71f852c7b220fc1ba8aaf5cf33753fd8a7566c8749bb75d360f903dfb0d131bcdc6c2c33f44404bd6a3 languageName: node linkType: hard @@ -2089,7 +2120,7 @@ __metadata: 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: @@ -2309,13 +2340,6 @@ __metadata: languageName: node linkType: hard -"wrappy@npm:1": - version: 1.0.2 - resolution: "wrappy@npm:1.0.2" - checksum: 10c0/56fece1a4018c6a6c8e28fbc88c87e0fbf4ea8fd64fc6c63b18f4acc4bd13e0ad2515189786dd2c30d3eec9663d70f4ecf699330002f8ccb547e4a18231fc9f0 - languageName: node - linkType: hard - "ws@npm:^7.4.6": version: 7.5.9 resolution: "ws@npm:7.5.9" @@ -2365,13 +2389,13 @@ __metadata: languageName: node linkType: hard -"zip-stream@npm:^5.0.1": - version: 5.0.2 - resolution: "zip-stream@npm:5.0.2" +"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: 10c0/cb5c4b57771a03429188ae73f90744f6996aa98c885852970de1c8bed3351c8a931cce0cf74cf37b9fa3727a07119236def871ec6d05c9becbc80746f52dd795 + 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 d69ed5d6f..dd3776ebd 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -10,3 +10,4 @@ node_modules/ /playwright-report/ /blob-report/ /playwright/.cache/ +/playwright/**/visual-specs/**/*.png diff --git a/frontend/.prettierrc b/frontend/.prettierrc deleted file mode 100644 index eb8da3b69..000000000 --- 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 000000000..f6769669c --- /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 a1b422ae6..cd48f83bc 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 4c273a63f..000000000 --- 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 de50ecbf7..d15d79b78 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 3cd46d85a..938c794a9 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.1" :git/sha "84c97b9" - :git/url "https://github.com/funcool/potok.git"} + :git/url "https://github.com/funcool/potok.git" + :exclusions [funcool/beicon2]} funcool/beicon2 - {:git/tag "v2.0" - :git/sha "e7135e0" + {:git/tag "v2.1" + :git/sha "7d648e1" :git/url "https://github.com/funcool/beicon.git"} funcool/rumext - {:git/tag "v2.11.3" - :git/sha "b1f6ce4" + {:git/tag "v2.12" + :git/sha "ab819f5" :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 a56170864..0dd9e4661 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,10 +4,8 @@ "license": "MPL-2.0", "author": "Kaleidos INC", "private": true, - "packageManager": "yarn@4.2.2", - "browserslist": [ - "defaults" - ], + "packageManager": "yarn@4.3.1", + "browserslist": ["defaults"], "type": "module", "repository": { "type": "git", @@ -17,48 +15,48 @@ "@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/", + "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", "token-test:compile": "clojure -M:dev:shadow-cljs compile test-esm --config-merge '{:autorun false}'", "token-test:run": "bun target/tests-esm.cjs", "token-test:watch": "clojure -M:dev:shadow-cljs watch test-esm", "token-test:nodemon": "nodemon --watch ./target/tests-esm.cjs --exec 'bun run token-test:run'", "token-test": "yarn run token-test:compile && yarn run token-test:run", - "translations:validate": "node ./scripts/validate-translations.js", - "translations:find-unused": "node ./scripts/find-unused-translations.js", - "compile": "node ./scripts/compile.js", - "compile:cljs": "clojure -M:dev:shadow-cljs compile main", - "watch": "node ./scripts/watch.js", - "e2e:server": "node ./scripts/e2e-server.js", - "e2e:test": "playwright test --project default", - "storybook:compile": "yarn run compile && clojure -M:dev:shadow-cljs compile storybook", - "storybook:watch": "yarn run storybook:compile && concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"storybook dev -p 6006\" \"yarn run watch\"", - "storybook:build": "yarn 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": { - "@playwright/test": "1.42.1", - "@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#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", @@ -69,34 +67,35 @@ "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", "mustache": "^4.2.0", - "nodemon": "^3.1.0", + "nodemon": "^3.1.2", "npm-run-all": "^4.1.5", "p-limit": "^5.0.0", - "postcss": "^8.4.35", + "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", - "sass-embedded": "^1.71.1", - "shadow-cljs": "2.27.4", - "storybook": "^7.6.17", - "svg-sprite": "^2.0.2", - "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", - "watcher": "^2.3.0", - "workerpool": "^9.1.0" + "watcher": "^2.3.1", + "workerpool": "^9.1.1" }, "dependencies": { "@tokens-studio/sd-transforms": "^0.16.1", - "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", @@ -106,15 +105,15 @@ "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-virtualized": "^9.22.5", "rxjs": "8.0.0-alpha.14", - "sax": "^1.3.0", + "sax": "^1.4.1", "source-map-support": "^0.5.21", "style-dictionary": "patch:style-dictionary@npm%3A4.0.0-prerelease.36#~/.yarn/patches/style-dictionary-npm-4.0.0-prerelease.36-55c0fc33bd.patch", "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 index 646795fd2..6196826df 100644 --- a/frontend/playwright.config.js +++ b/frontend/playwright.config.js @@ -48,7 +48,7 @@ export default defineConfig({ use: { ...devices["Desktop Chrome"] }, testDir: "./playwright/ui/visual-specs", expect: { - toHaveScreenshot: { maxDiffPixelRatio: 0.01 }, + toHaveScreenshot: { maxDiffPixelRatio: 0.005 }, }, }, ], 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 000000000..4f8cfb630 --- /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", + "~:content": { + "~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" + } + } +} \ No newline at end of file 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 000000000..99e01ce34 --- /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", + "~:content": { + "~: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" + } +} \ No newline at end of file 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 000000000..29758d1a0 --- /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 000000000..395e5a1a9 --- /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/get-access-tokens-empty.json b/frontend/playwright/data/dashboard/get-access-tokens-empty.json new file mode 100644 index 000000000..fe51488c7 --- /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 000000000..ec296ea8a --- /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 000000000..6a16ec574 --- /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-projects-full.json b/frontend/playwright/data/dashboard/get-projects-full.json new file mode 100644 index 000000000..491351e86 --- /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-shared-files-empty.json b/frontend/playwright/data/dashboard/get-shared-files-empty.json new file mode 100644 index 000000000..fe51488c7 --- /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 000000000..3fffa07f4 --- /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 000000000..fe51488c7 --- /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 000000000..f7ac77543 --- /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 000000000..a869d5e34 --- /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 000000000..920bb2df0 --- /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 000000000..c984f1021 --- /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 000000000..fe51488c7 --- /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 000000000..3849e3608 --- /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 000000000..e69de29bb diff --git a/frontend/playwright/data/dashboard/search-files.json b/frontend/playwright/data/dashboard/search-files.json new file mode 100644 index 000000000..920bb2df0 --- /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 000000000..1a055d7d1 --- /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", + "~:content": { + "~: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" + } +} \ No newline at end of file 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 000000000..c0a67da95 --- /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 000000000..76edd0baf --- /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 000000000..0637a088a --- /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 000000000..f3a0d6d27 --- /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/logged-in-user/get-teams-complete.json b/frontend/playwright/data/logged-in-user/get-teams-complete.json new file mode 100644 index 000000000..910e1543f --- /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/viewer/get-file-fragment-empty-file.json b/frontend/playwright/data/viewer/get-file-fragment-empty-file.json new file mode 100644 index 000000000..544c559f7 --- /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", + "~:content": { + "~: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" + } +} \ No newline at end of file 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 000000000..8c1e62a15 --- /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", + "~:content": { + "~: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" + } +} \ No newline at end of file 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 000000000..ef001224a --- /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 000000000..9284de685 --- /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 000000000..9e26dfeeb --- /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-not-empty.json b/frontend/playwright/data/workspace/get-comment-threads-not-empty.json new file mode 100644 index 000000000..503f1069f --- /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 000000000..ff33a7a94 --- /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-fragment-7760.json b/frontend/playwright/data/workspace/get-file-fragment-7760.json new file mode 100644 index 000000000..0c8011553 --- /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", + "~:content": { + "~: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-library.json b/frontend/playwright/data/workspace/get-file-library.json new file mode 100644 index 000000000..de4775427 --- /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 000000000..27a91a25b --- /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-team-shared-libraries-non-empty.json b/frontend/playwright/data/workspace/get-team-shared-libraries-non-empty.json new file mode 100644 index 000000000..05a5c8c3c --- /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 000000000..b3822d1eb --- /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 000000000..0967ef424 --- /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 000000000..0967ef424 --- /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 000000000..9e26dfeeb --- /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/helpers/MockWebSocketHelper.js b/frontend/playwright/helpers/MockWebSocketHelper.js index 42d06d5eb..8cf63f973 100644 --- a/frontend/playwright/helpers/MockWebSocketHelper.js +++ b/frontend/playwright/helpers/MockWebSocketHelper.js @@ -14,12 +14,17 @@ export class MockWebSocketHelper extends EventTarget { } 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.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" }); } diff --git a/frontend/playwright/scripts/MockWebSocket.js b/frontend/playwright/scripts/MockWebSocket.js index aa54067da..a401e6552 100644 --- a/frontend/playwright/scripts/MockWebSocket.js +++ b/frontend/playwright/scripts/MockWebSocket.js @@ -188,13 +188,18 @@ window.WebSocket = class MockWebSocket extends EventTarget { mockClose(code, reason) { this.#readyState = MockWebSocket.CLOSED; - this.dispatchEvent(new CloseEvent("close", { code: code || 1000, reason: reason || "" })); + 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"); + throw new DOMException( + "InvalidStateError", + "MockWebSocket is not connected", + ); } if (this.#spyMessage) { @@ -203,7 +208,12 @@ window.WebSocket = class MockWebSocket extends EventTarget { } close(code, reason) { - if (code && !Number.isInteger(code) && code !== 1000 && (code < 3000 || code > 4999)) { + if ( + code && + !Number.isInteger(code) && + code !== 1000 && + (code < 3000 || code > 4999) + ) { throw new DOMException("InvalidAccessError", "Invalid code"); } @@ -214,7 +224,9 @@ window.WebSocket = class MockWebSocket extends EventTarget { } } - if ([MockWebSocket.CLOSED, MockWebSocket.CLOSING].includes(this.#readyState)) { + if ( + [MockWebSocket.CLOSED, MockWebSocket.CLOSING].includes(this.#readyState) + ) { return; } diff --git a/frontend/playwright/ui/pages/BasePage.js b/frontend/playwright/ui/pages/BasePage.js index 076bf13f6..b233be060 100644 --- a/frontend/playwright/ui/pages/BasePage.js +++ b/frontend/playwright/ui/pages/BasePage.js @@ -4,7 +4,9 @@ export class BasePage { 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."); + throw new TypeError( + "Invalid path argument. Must be a string or a RegExp.", + ); } const url = typeof path === "string" ? `**/api/rpc/command/${path}` : path; diff --git a/frontend/playwright/ui/pages/DashboardPage.js b/frontend/playwright/ui/pages/DashboardPage.js index 285e47d95..6d340c62e 100644 --- a/frontend/playwright/ui/pages/DashboardPage.js +++ b/frontend/playwright/ui/pages/DashboardPage.js @@ -1,3 +1,4 @@ +import { expect } from "@playwright/test"; import { BaseWebSocketPage } from "./BaseWebSocketPage"; export class DashboardPage extends BaseWebSocketPage { @@ -6,10 +7,9 @@ export class DashboardPage extends BaseWebSocketPage { await BaseWebSocketPage.mockRPC( page, - "get-profile", - "logged-in-user/get-profile-logged-in-no-onboarding.json", + "get-teams", + "logged-in-user/get-teams-default.json", ); - await BaseWebSocketPage.mockRPC(page, "get-teams", "logged-in-user/get-teams-default.json"); await BaseWebSocketPage.mockRPC( page, "get-font-variants?team-id=*", @@ -54,39 +54,215 @@ export class DashboardPage extends BaseWebSocketPage { } 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.titleLabel = page.getByRole("heading", { name: "Projects" }); - this.addProjectBtn = page.getByRole("button", { name: "+ NEW PROJECT" }); + + 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.draftTitle = page.getByRole("heading", { name: "Drafts" }); - this.draftLink = page.getByTestId("drafts-link-sidebar"); - this.draftsFile = page.getByText(/New File 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"); + 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"); + 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"); + await this.mockRPC("create-project", "dashboard/create-project.json", { + method: "POST", + }); + await this.mockRPC( + "get-projects?team-id=*", + "dashboard/get-projects-new.json", + ); } - async goToWorkspace() { - await this.page.goto(`#/dashboard/team/${DashboardPage.anyTeamId}/projects`); + + 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(); } } diff --git a/frontend/playwright/ui/pages/LoginPage.js b/frontend/playwright/ui/pages/LoginPage.js index 5e94c10ca..0ee5f863c 100644 --- a/frontend/playwright/ui/pages/LoginPage.js +++ b/frontend/playwright/ui/pages/LoginPage.js @@ -1,18 +1,18 @@ import { BasePage } from "./BasePage"; export class LoginPage extends BasePage { - static async initWithLoggedOutUser(page) { - await BasePage.mockRPC(page, "get-profile", "get-profile-anonymous.json"); - } - 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.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" }); + this.initialHeading = page.getByRole("heading", { + name: "Log into my account", + }); } async fillEmailAndPasswordInputs(email, password) { @@ -24,18 +24,40 @@ export class LoginPage extends BasePage { 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-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-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-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", @@ -43,11 +65,18 @@ export class LoginPage extends BasePage { } async setupLoginSuccess() { - await this.mockRPC("login-with-password", "logged-in-user/login-with-password-success.json"); + 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 }); + await this.mockRPC( + "login-with-password", + "login-with-password-error.json", + { status: 400 }, + ); } } diff --git a/frontend/playwright/ui/pages/OnboardingPage.js b/frontend/playwright/ui/pages/OnboardingPage.js new file mode 100644 index 000000000..81e199588 --- /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 000000000..41fd45a23 --- /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 index 5bcfa5f9f..7e5bf6b36 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -11,7 +11,11 @@ export class WorkspacePage extends BaseWebSocketPage { 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-profile", + "logged-in-user/get-profile-logged-in.json", + ); await BaseWebSocketPage.mockRPC( page, "get-team-users?file-id=*", @@ -22,8 +26,16 @@ export class WorkspacePage extends BaseWebSocketPage { "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-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=*", @@ -40,16 +52,37 @@ export class WorkspacePage extends BaseWebSocketPage { constructor(page) { super(page); this.pageName = page.getByTestId("page-name"); - this.presentUserListItems = page.getByTestId("active-users-list").getByAltText("Princesa Leia"); + 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.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() { + async goToWorkspace({ + fileId = WorkspacePage.anyFileId, + pageId = WorkspacePage.anyPageId, + } = {}) { await this.page.goto( - `/#/workspace/${WorkspacePage.anyProjectId}/${WorkspacePage.anyFileId}?page-id=${WorkspacePage.anyPageId}`, + `/#/workspace/${WorkspacePage.anyProjectId}/${fileId}?page-id=${pageId}`, ); this.#ws = await this.waitForNotificationsWebSocket(); @@ -71,10 +104,22 @@ export class WorkspacePage extends BaseWebSocketPage { } 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-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=*", @@ -85,9 +130,18 @@ export class WorkspacePage extends BaseWebSocketPage { "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"); + 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) { @@ -97,4 +151,87 @@ export class WorkspacePage extends BaseWebSocketPage { 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 index 1793f7ade..0ea20f52b 100644 --- a/frontend/playwright/ui/specs/colorpicker.spec.js +++ b/frontend/playwright/ui/specs/colorpicker.spec.js @@ -6,7 +6,9 @@ test.beforeEach(async ({ 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 }) => { +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); diff --git a/frontend/playwright/ui/specs/dashboard.spec.js b/frontend/playwright/ui/specs/dashboard.spec.js index 145c1321a..b11e1a326 100644 --- a/frontend/playwright/ui/specs/dashboard.spec.js +++ b/frontend/playwright/ui/specs/dashboard.spec.js @@ -3,23 +3,28 @@ 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.goToWorkspace(); + await dashboardPage.goToDashboard(); await expect(dashboardPage.page).toHaveURL(/dashboard/); - await expect(dashboardPage.titleLabel).toBeVisible(); + await expect(dashboardPage.mainHeading).toBeVisible(); }); test("User can create a new project", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.setupNewProject(); - await dashboardPage.goToWorkspace(); - await dashboardPage.addProjectBtn.click(); + await dashboardPage.goToDashboard(); + await dashboardPage.addProjectButton.click(); await expect(dashboardPage.projectName).toBeVisible(); }); @@ -28,17 +33,22 @@ test("User goes to draft page", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.setupDraftsEmpty(); - await dashboardPage.goToWorkspace(); - await dashboardPage.draftLink.click(); + await dashboardPage.goToDashboard(); + await dashboardPage.draftsLink.click(); - await expect(dashboardPage.draftTitle).toBeVisible(); + await expect(dashboardPage.mainHeading).toHaveText("Drafts"); }); -test("User loads the draft page", async ({ page }) => { +test("Lists files in the drafts page", async ({ page }) => { const dashboardPage = new DashboardPage(page); await dashboardPage.setupDrafts(); await dashboardPage.goToDrafts(); - await expect(dashboardPage.draftsFile).toBeVisible(); + 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 000000000..3b28bb534 --- /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/login.spec.js b/frontend/playwright/ui/specs/login.spec.js index dd259cf77..4a2604f4b 100644 --- a/frontend/playwright/ui/specs/login.spec.js +++ b/frontend/playwright/ui/specs/login.spec.js @@ -2,11 +2,15 @@ import { test, expect } from "@playwright/test"; import { LoginPage } from "../pages/LoginPage"; test.beforeEach(async ({ page }) => { - await LoginPage.initWithLoggedOutUser(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 }) => { +test("User is redirected to the login page when logged out", async ({ + page, +}) => { const loginPage = new LoginPage(page); await loginPage.setupLoggedInUser(); @@ -28,7 +32,9 @@ test.describe("Login form", () => { await expect(loginPage.page).toHaveURL(/dashboard/); }); - test("User gets error message when submitting an bad formatted email ", async ({ page }) => { + test("User gets error message when submitting an bad formatted email ", async ({ + page, + }) => { const loginPage = new LoginPage(page); await loginPage.setupLoginSuccess(); @@ -37,11 +43,16 @@ test.describe("Login form", () => { await expect(loginPage.invalidEmailError).toBeVisible(); }); - test("User gets error message when submitting wrong credentials", async ({ page }) => { + 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.fillEmailAndPasswordInputs( + "test@example.com", + "loremipsum", + ); await loginPage.clickLoginButton(); await expect(loginPage.invalidCredentialsError).toBeVisible(); diff --git a/frontend/playwright/ui/specs/onboarding.spec.js b/frontend/playwright/ui/specs/onboarding.spec.js new file mode 100644 index 000000000..968c88825 --- /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 000000000..b97301889 --- /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 000000000..4ed32135a --- /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 000000000..805d31c66 --- /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 index 58e9e5697..066486f50 100644 --- a/frontend/playwright/ui/specs/workspace.spec.js +++ b/frontend/playwright/ui/specs/workspace.spec.js @@ -15,20 +15,27 @@ test("User loads worskpace with empty file", async ({ page }) => { await expect(workspacePage.pageName).toHaveText("Page 1"); }); -test("User receives presence notifications updates in the workspace", async ({ page }) => { +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); + 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.mockRPC( + "update-file?id=*", + "workspace/update-file-create-rect.json", + ); await workspacePage.goToWorkspace(); await workspacePage.rectShapeButton.click(); @@ -38,3 +45,135 @@ test("User draws a rect", async ({ page }) => { 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/example.spec.js b/frontend/playwright/ui/visual-specs/example.spec.js deleted file mode 100644 index e4c344eec..000000000 --- a/frontend/playwright/ui/visual-specs/example.spec.js +++ /dev/null @@ -1,10 +0,0 @@ -import { test, expect } from "@playwright/test"; -import { LoginPage } from "../pages/LoginPage"; - -test("Shows login form correctly", async ({ page }) => { - await LoginPage.initWithLoggedOutUser(page); - const loginPage = new LoginPage(page); - await page.goto("/#/auth/login"); - - await expect(page).toHaveScreenshot(); -}); diff --git a/frontend/playwright/ui/visual-specs/example.spec.js-snapshots/Shows-login-form-correctly-1-ds-linux.png b/frontend/playwright/ui/visual-specs/example.spec.js-snapshots/Shows-login-form-correctly-1-ds-linux.png deleted file mode 100644 index bbe1dc402..000000000 Binary files a/frontend/playwright/ui/visual-specs/example.spec.js-snapshots/Shows-login-form-correctly-1-ds-linux.png and /dev/null differ 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 000000000..44bfb9abb --- /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 000000000..b3b63a0c5 --- /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 000000000..a3eeddc82 --- /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.getByTestId("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 000000000..e594ea343 --- /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 000000000..22595c499 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 000000000..9a827989b 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 000000000..cfb34953e --- /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 000000000..591427ec6 --- /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 000000000..eb61aab34 --- /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 000000000..28dd05ed8 --- /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 000000000..ee1d9b96b --- /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 000000000..6e6b7394a --- /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 000000000..7e71215fa --- /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 000000000..7ce38ce15 --- /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 000000000..68ea4e6d6 --- /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 000000000..ce2d299f8 --- /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 000000000..4b5e97d4a --- /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 000000000..06adb5d44 --- /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 000000000..6439292bd --- /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/form/Design.png b/frontend/resources/images/form/Design.png new file mode 100644 index 000000000..ae1e28ab7 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 000000000..508d43e1d 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 000000000..e1817a6a9 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 000000000..b66bddc79 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 000000000..3c77a7a0f Binary files /dev/null and b/frontend/resources/images/form/templates.png differ diff --git a/frontend/resources/images/icons/character-a.svg b/frontend/resources/images/icons/character-a.svg new file mode 100644 index 000000000..3a740083a --- /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 000000000..39fe59dd5 --- /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 000000000..73347384d --- /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 000000000..d585f275b --- /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 000000000..eb7ac8837 --- /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 000000000..c6ddd2c4d --- /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 000000000..fd87e7fc0 --- /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 000000000..082571f40 --- /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 000000000..567b9f471 --- /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 000000000..b90b0bca6 --- /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 000000000..dacc93917 --- /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 000000000..9b4f0b17d --- /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 000000000..771d684a0 --- /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 000000000..ec006c85a --- /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 000000000..fded9d9ba --- /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 000000000..3d01ad2f7 --- /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 000000000..1e272df2c --- /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 000000000..6ead103be --- /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 000000000..120e254e2 --- /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 000000000..796a64e13 --- /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 000000000..1c8b6ba10 --- /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 000000000..d07aef54e --- /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 000000000..d28b777f0 --- /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 000000000..8ecae0bf5 --- /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 000000000..2253d0d17 --- /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 000000000..add5b34b8 --- /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 000000000..4df568995 --- /dev/null +++ b/frontend/resources/images/icons/character-z.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/images/icons/oauth-1.svg b/frontend/resources/images/icons/oauth-1.svg new file mode 100644 index 000000000..49a0dec9b --- /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 000000000..06c59a185 --- /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 000000000..db38820bc --- /dev/null +++ b/frontend/resources/images/icons/oauth-3.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/images/icons/percentage.svg b/frontend/resources/images/icons/percentage.svg new file mode 100644 index 000000000..faa338037 --- /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 000000000..500638984 --- /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/rocket.svg b/frontend/resources/images/icons/rocket.svg deleted file mode 100644 index 4bc138edd..000000000 --- a/frontend/resources/images/icons/rocket.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/resources/images/icons/row.svg b/frontend/resources/images/icons/row.svg index 8475819d4..6dbcfe31d 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/thumbnails/template-prototype-examples.jpg b/frontend/resources/images/thumbnails/template-prototype-examples.jpg new file mode 100644 index 000000000..5e5d4dab4 Binary files /dev/null and b/frontend/resources/images/thumbnails/template-prototype-examples.jpg differ diff --git a/frontend/resources/plugins-runtime/index.js b/frontend/resources/plugins-runtime/index.js index 33e0bd06b..bd841634d 100644 --- a/frontend/resources/plugins-runtime/index.js +++ b/frontend/resources/plugins-runtime/index.js @@ -1,509 +1,593 @@ -var An = (t, e, r) => { +var Hn = (t, e, r) => { if (!e.has(t)) throw TypeError("Cannot " + r); }; -var Se = (t, e, r) => (An(t, e, "read from private field"), r ? r.call(t) : e.get(t)), Fr = (t, e, r) => { +var Ee = (t, e, r) => (Hn(t, e, "read from private field"), r ? r.call(t) : e.get(t)), Gr = (t, e, r) => { if (e.has(t)) throw TypeError("Cannot add the same private member more than once"); e instanceof WeakSet ? e.add(t) : e.set(t, r); -}, Dr = (t, e, r, n) => (An(t, e, "write to private field"), n ? n.call(t, r) : e.set(t, r), r); -const x = globalThis, { - Array: Ps, - Date: ks, - FinalizationRegistry: bt, - Float32Array: Ts, - JSON: Is, - Map: Ce, - Math: As, - Number: io, - Object: ln, - Promise: Cs, - Proxy: xr, - Reflect: $s, - RegExp: Be, - Set: Pt, - String: ie, - Symbol: Ot, - WeakMap: Te, - WeakSet: kt +}, Br = (t, e, r, n) => (Hn(t, e, "write to private field"), n ? n.call(t, r) : e.set(t, r), r); +const k = globalThis, { + Array: Bs, + Date: Hs, + FinalizationRegistry: kt, + Float32Array: Vs, + JSON: Ws, + Map: Pe, + Math: qs, + Number: So, + Object: _n, + Promise: Ks, + Proxy: Cr, + Reflect: Ys, + RegExp: We, + Set: Ct, + String: pe, + Symbol: St, + WeakMap: Me, + WeakSet: $t } = 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: le, - RangeError: Ns, - ReferenceError: ot, - SyntaxError: Kt, - TypeError: v + Error: ue, + RangeError: Js, + ReferenceError: lt, + SyntaxError: tr, + TypeError: v, + AggregateError: Hr } = globalThis, { - assign: Pr, - create: H, + assign: $r, + create: Z, defineProperties: F, - entries: te, - freeze: g, - getOwnPropertyDescriptor: de, - getOwnPropertyDescriptors: Je, - getOwnPropertyNames: Mt, - getPrototypeOf: B, - is: kr, - isFrozen: ol, - isSealed: sl, - isExtensible: al, - keys: co, - prototype: lo, - seal: il, - preventExtensions: Os, - setPrototypeOf: uo, - values: fo, - fromEntries: Tt -} = ln, { - species: Cn, - toStringTag: He, - iterator: Yt, - matchAll: po, - unscopables: Rs, - keyFor: Ms, - for: cl -} = Ot, { isInteger: Ls } = io, { stringify: mo } = Is, { defineProperty: Fs } = ln, L = (t, e, r) => { - const n = Fs(t, e, r); + entries: re, + freeze: y, + getOwnPropertyDescriptor: J, + getOwnPropertyDescriptors: Ze, + getOwnPropertyNames: Dt, + getPrototypeOf: j, + is: Nr, + isFrozen: jl, + isSealed: Zl, + isExtensible: zl, + keys: Eo, + prototype: bn, + seal: Gl, + preventExtensions: Xs, + setPrototypeOf: xo, + values: ko, + fromEntries: mt +} = _n, { + species: Vr, + toStringTag: qe, + iterator: rr, + matchAll: Po, + unscopables: Qs, + keyFor: ea, + for: ta +} = St, { isInteger: ra } = So, { stringify: To } = Ws, { defineProperty: na } = _n, M = (t, e, r) => { + const n = na(t, e, r); if (n !== t) throw v( - `Please report that the original defineProperty silently failed to set ${mo( - ie(e) + `Please report that the original defineProperty silently failed to set ${To( + pe(e) )}. (SES_DEFINE_PROPERTY_FAILED_SILENTLY)` ); return n; }, { - apply: oe, - construct: lr, - get: Ds, - getOwnPropertyDescriptor: Us, - has: ho, - isExtensible: js, - ownKeys: it, - preventExtensions: Zs, - set: yo -} = $s, { isArray: vt, prototype: Ie } = Ps, { prototype: It } = Ce, { prototype: Tr } = RegExp, { prototype: Jt } = Pt, { prototype: Re } = ie, { prototype: Ir } = Te, { prototype: go } = kt, { prototype: un } = Function, { prototype: vo } = Cs, zs = B(Uint8Array.prototype), { bind: $n } = un, k = $n.bind($n.call), se = k(lo.hasOwnProperty), Ve = k(Ie.filter), st = k(Ie.forEach), Ar = k(Ie.includes), At = k(Ie.join), fe = ( + apply: ne, + construct: mr, + get: oa, + getOwnPropertyDescriptor: sa, + has: Ao, + isExtensible: aa, + ownKeys: De, + preventExtensions: ia, + set: Io +} = Ys, { isArray: Et, prototype: _e } = Bs, { prototype: Nt } = Pe, { prototype: Rr } = RegExp, { prototype: nr } = Ct, { prototype: Le } = pe, { prototype: Or } = Me, { prototype: Co } = $t, { prototype: wn } = Function, { prototype: $o } = Ks, { prototype: No } = j( + // eslint-disable-next-line no-empty-function, func-names + function* () { + } +), ca = j(Uint8Array.prototype), { bind: tn } = wn, P = tn.bind(tn.call), oe = P(bn.hasOwnProperty), Ke = P(_e.filter), ut = P(_e.forEach), Mr = P(_e.includes), Rt = P(_e.join), se = ( /** @type {any} */ - k(Ie.map) -), Hr = k(Ie.pop), ae = k(Ie.push), Gs = k(Ie.slice), Bs = k(Ie.some), _o = k(Ie.sort), Hs = k(Ie[Yt]), $e = k(It.set), De = k(It.get), Cr = k(It.has), Vs = k(It.delete), Ws = k(It.entries), qs = k(It[Yt]), $r = k(Jt.add); -k(Jt.delete); -const Nn = k(Jt.forEach), dn = k(Jt.has), Ks = k(Jt[Yt]), fn = k(Tr.test), pn = k(Tr.exec), Ys = k(Tr[po]), bo = k(Re.endsWith), Js = k(Re.includes), Xs = k(Re.indexOf); -k(Re.match); -const ur = ( + P(_e.map) +), Ro = ( /** @type {any} */ - k(Re.replace) -), Qs = k(Re.search), mn = k(Re.slice), wo = k(Re.split), So = k(Re.startsWith), ea = k(Re[Yt]), ta = k(Ir.delete), M = k(Ir.get), hn = k(Ir.has), ee = k(Ir.set), Nr = k(go.add), Xt = k(go.has), ra = k(un.toString), na = k(vo.catch), yn = ( + P(_e.flatMap) +), gr = P(_e.pop), X = P(_e.push), la = P(_e.slice), ua = P(_e.some), Oo = P(_e.sort), da = P(_e[rr]), $e = P(Nt.set), Ue = P(Nt.get), Lr = P(Nt.has), fa = P(Nt.delete), pa = P(Nt.entries), ha = P(Nt[rr]), Sn = P(nr.add); +P(nr.delete); +const Vn = P(nr.forEach), En = P(nr.has), ma = P(nr[rr]), xn = P(Rr.test), kn = P(Rr.exec), ga = P(Rr[Po]), Mo = P(Le.endsWith), Lo = P(Le.includes), ya = P(Le.indexOf); +P(Le.match); +const yr = P(No.next), Fo = P(No.throw), vr = ( /** @type {any} */ - k(vo.then) -), oa = bt && k(bt.prototype.register); -bt && k(bt.prototype.unregister); -const gn = g(H(null)), We = (t) => ln(t) === t, vn = (t) => t instanceof le, Eo = eval, ve = Function, sa = () => { + P(Le.replace) +), va = P(Le.search), Pn = P(Le.slice), Tn = P(Le.split), Do = P(Le.startsWith), _a = P(Le[rr]), ba = P(Or.delete), L = P(Or.get), An = P(Or.has), ie = P(Or.set), Fr = P(Co.add), or = P(Co.has), wa = P(wn.toString), Sa = P(tn); +P($o.catch); +const Uo = ( + /** @type {any} */ + P($o.then) +), Ea = kt && P(kt.prototype.register); +kt && P(kt.prototype.unregister); +const In = y(Z(null)), Ye = (t) => _n(t) === t, Dr = (t) => t instanceof ue, jo = eval, ve = Function, xa = () => { throw v('Cannot eval with evalTaming set to "noEval" (SES_NO_EVAL)'); -}; -function aa() { +}, He = J(Error("er1"), "stack"), Wr = J(v("er2"), "stack"); +let Zo, zo; +if (He && Wr && He.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 He.get == "function" && He.get === Wr.get && typeof He.set == "function" && He.set === Wr.set + ) + Zo = y(He.get), zo = y(He.set); + else + throw v( + "Unexpected Error own stack accessor functions (SES_UNEXPECTED_ERROR_OWN_STACK_ACCESSOR)" + ); +const qr = Zo, ka = zo; +function Pa() { return this; } -if (aa()) +if (Pa()) throw v("SES failed to initialize, sloppy mode (SES_NO_SLOPPY)"); -const { freeze: tt } = Object, { apply: ia } = Reflect, _n = (t) => (e, ...r) => ia(t, e, r), ca = _n(Array.prototype.push), On = _n(Array.prototype.includes), la = _n(String.prototype.split), Qe = JSON.stringify, tr = (t, ...e) => { +const { freeze: at } = Object, { apply: Ta } = Reflect, Cn = (t) => (e, ...r) => Ta(t, e, r), Aa = Cn(Array.prototype.push), Wn = Cn(Array.prototype.includes), Ia = Cn(String.prototype.split), nt = JSON.stringify, ir = (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); -}, xo = (t, e = !1) => { - const r = [], n = (c, u, l = void 0) => { - typeof c == "string" || tr`Environment option name ${Qe(c)} must be a string.`, typeof u == "string" || tr`Environment option default setting ${Qe( - u +}, Go = (t, e = !1) => { + const r = [], n = (c, l, u = void 0) => { + typeof c == "string" || ir`Environment option name ${nt(c)} must be a string.`, typeof l == "string" || ir`Environment option default setting ${nt( + l )} must be a string.`; - let d = u; - const f = t.process || void 0, m = typeof f == "object" && f.env || void 0; - if (typeof m == "object" && c in m) { - e || ca(r, c); - const p = m[c]; - typeof p == "string" || tr`Environment option named ${Qe( + 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 || Aa(r, c); + const p = h[c]; + typeof p == "string" || ir`Environment option named ${nt( c - )}, if present, must have a corresponding string value, got ${Qe( + )}, if present, must have a corresponding string value, got ${nt( p )}`, d = p; } - return l === void 0 || d === u || On(l, d) || tr`Unrecognized ${Qe(c)} value ${Qe( + return u === void 0 || d === l || Wn(u, d) || ir`Unrecognized ${nt(c)} value ${nt( d - )}. Expected one of ${Qe([u, ...l])}`, d; + )}. Expected one of ${nt([l, ...u])}`, d; }; - tt(n); - const a = (c) => { - const u = n(c, ""); - return tt(u === "" ? [] : la(u, ",")); + at(n); + const o = (c) => { + const l = n(c, ""); + return at(l === "" ? [] : Ia(l, ",")); }; - tt(a); - const s = (c, u) => On(a(c), u), i = () => tt([...r]); - return tt(i), tt({ + at(o); + const a = (c, l) => Wn(o(c), l), i = () => at([...r]); + return at(i), at({ getEnvironmentOption: n, - getEnvironmentOptionsList: a, - environmentOptionsListHas: s, + getEnvironmentOptionsList: o, + environmentOptionsListHas: a, getCapturedEnvironmentOptionNames: i }); }; -tt(xo); +at(Go); const { - getEnvironmentOption: he, - getEnvironmentOptionsList: ll, - environmentOptionsListHas: ul -} = xo(globalThis, !0), dr = (t) => (t = `${t}`, t.length >= 1 && Js("aeiouAEIOU", t[0]) ? `an ${t}` : `a ${t}`); -g(dr); -const Po = (t, e = void 0) => { - const r = new Pt(), n = (a, s) => { - switch (typeof s) { + getEnvironmentOption: le, + getEnvironmentOptionsList: Bl, + environmentOptionsListHas: Hl +} = Go(globalThis, !0), _r = (t) => (t = `${t}`, t.length >= 1 && Lo("aeiouAEIOU", t[0]) ? `an ${t}` : `a ${t}`); +y(_r); +const Bo = (t, e = void 0) => { + const r = new Ct(), n = (o, a) => { + switch (typeof a) { case "object": { - if (s === null) + if (a === null) return null; - if (dn(r, s)) + if (En(r, a)) return "[Seen]"; - if ($r(r, s), vn(s)) - return `[${s.name}: ${s.message}]`; - if (He in s) - return `[${s[He]}]`; - if (vt(s)) - return s; - const i = co(s); + if (Sn(r, a), Dr(a)) + return `[${a.name}: ${a.message}]`; + if (qe in a) + return `[${a[qe]}]`; + if (Et(a)) + return a; + const i = Eo(a); if (i.length < 2) - return s; + return a; let c = !0; - for (let l = 1; l < i.length; l += 1) - if (i[l - 1] >= i[l]) { + for (let u = 1; u < i.length; u += 1) + if (i[u - 1] >= i[u]) { c = !1; break; } if (c) - return s; - _o(i); - const u = fe(i, (l) => [l, s[l]]); - return Tt(u); + return a; + Oo(i); + const l = se(i, (u) => [u, a[u]]); + return mt(l); } case "function": - return `[Function ${s.name || ""}]`; + return `[Function ${a.name || ""}]`; case "string": - return So(s, "[") ? `[${s}]` : s; + return Do(a, "[") ? `[${a}]` : a; case "undefined": case "symbol": - return `[${ie(s)}]`; + return `[${pe(a)}]`; case "bigint": - return `[${s}n]`; + return `[${a}n]`; case "number": - return kr(s, NaN) ? "[NaN]" : s === 1 / 0 ? "[Infinity]" : s === -1 / 0 ? "[-Infinity]" : s; + return Nr(a, NaN) ? "[NaN]" : a === 1 / 0 ? "[Infinity]" : a === -1 / 0 ? "[-Infinity]" : a; default: - return s; + return a; } }; try { - return mo(t, n, e); + return To(t, n, e); } catch { return "[Something that failed to stringify]"; } }; -g(Po); -const { isSafeInteger: ua } = Number, { freeze: yt } = Object, { toStringTag: da } = Symbol, Rn = (t) => { +y(Bo); +const { isSafeInteger: Ca } = Number, { freeze: vt } = Object, { toStringTag: $a } = Symbol, qn = (t) => { const r = { next: void 0, prev: void 0, data: t }; return r.next = r, r.prev = r, r; -}, Mn = (t, e) => { +}, Kn = (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; -}, Ur = (t) => { +}, Kr = (t) => { const { prev: e, next: r } = t; e.next = r, r.prev = e, t.prev = t, t.next = t; -}, ko = (t) => { - if (!ua(t) || t < 0) +}, Ho = (t) => { + if (!Ca(t) || t < 0) throw TypeError("keysBudget must be a safe non-negative integer number"); const e = /* @__PURE__ */ new WeakMap(); let r = 0; - const n = Rn(void 0), a = (d) => { + const n = qn(void 0), o = (d) => { const f = e.get(d); if (!(f === void 0 || f.data === void 0)) - return Ur(f), Mn(n, f), f; - }, s = (d) => a(d) !== void 0; - yt(s); + return Kr(f), Kn(n, f), f; + }, a = (d) => o(d) !== void 0; + vt(a); const i = (d) => { - const f = a(d); + const f = o(d); return f && f.data && f.data.get(d); }; - yt(i); + vt(i); const c = (d, f) => { if (t < 1) - return l; - let m = a(d); - if (m === void 0 && (m = Rn(void 0), Mn(n, m)), !m.data) - for (r += 1, m.data = /* @__PURE__ */ new WeakMap(), e.set(d, m); r > t; ) { + return u; + let h = o(d); + if (h === void 0 && (h = qn(void 0), Kn(n, h)), !h.data) + for (r += 1, h.data = /* @__PURE__ */ new WeakMap(), e.set(d, h); r > t; ) { const p = n.prev; - Ur(p), p.data = void 0, r -= 1; + Kr(p), p.data = void 0, r -= 1; } - return m.data.set(d, f), l; + return h.data.set(d, f), u; }; - yt(c); - const u = (d) => { + vt(c); + const l = (d) => { const f = e.get(d); - return f === void 0 || (Ur(f), e.delete(d), f.data === void 0) ? !1 : (f.data = void 0, r -= 1, !0); + return f === void 0 || (Kr(f), e.delete(d), f.data === void 0) ? !1 : (f.data = void 0, r -= 1, !0); }; - yt(u); - const l = yt({ - has: s, + vt(l); + const u = vt({ + has: a, get: i, set: c, - delete: u, + delete: l, // eslint-disable-next-line jsdoc/check-types [ /** @type {typeof Symbol.toStringTag} */ - da + $a ]: "LRUCacheMap" }); - return l; + return u; }; -yt(ko); -const { freeze: ar } = Object, { isSafeInteger: fa } = Number, pa = 1e3, ma = 100, To = (t = pa, e = ma) => { - if (!fa(e) || e < 1) +vt(Ho); +const { freeze: pr } = Object, { isSafeInteger: Na } = Number, Ra = 1e3, Oa = 100, Vo = (t = Ra, e = Oa) => { + if (!Na(e) || e < 1) throw TypeError( "argsPerErrorBudget must be a safe positive integer number" ); - const r = ko(t), n = (s, i) => { - const c = r.get(s); - c !== void 0 ? (c.length >= e && c.shift(), c.push(i)) : r.set(s, [i]); + const r = Ho(t), n = (a, i) => { + const c = r.get(a); + c !== void 0 ? (c.length >= e && c.shift(), c.push(i)) : r.set(a, [i]); }; - ar(n); - const a = (s) => { - const i = r.get(s); - return r.delete(s), i; + pr(n); + const o = (a) => { + const i = r.get(a); + return r.delete(a), i; }; - return ar(a), ar({ + return pr(o), pr({ addLogArgs: n, - takeLogArgsArray: a + takeLogArgsArray: o }); }; -ar(To); -const wt = new Te(), ct = (t, e = void 0) => { - const r = g({ - toString: g(() => Po(t, e)) +pr(Vo); +const Pt = new Me(), Je = (t, e = void 0) => { + const r = y({ + toString: y(() => Bo(t, e)) }); - return ee(wt, r, t), r; + return ie(Pt, r, t), r; }; -g(ct); -const ha = g(/^[\w:-]( ?[\w:-])*$/), Vr = (t, e = void 0) => { - if (typeof t != "string" || !fn(ha, t)) - return ct(t, e); - const r = g({ - toString: g(() => t) +y(Je); +const Ma = y(/^[\w:-]( ?[\w:-])*$/), rn = (t, e = void 0) => { + if (typeof t != "string" || !xn(Ma, t)) + return Je(t, e); + const r = y({ + toString: y(() => t) }); - return ee(wt, r, t), r; + return ie(Pt, r, t), r; }; -g(Vr); -const Or = new Te(), Io = ({ template: t, args: e }) => { +y(rn); +const Ur = new Me(), Wo = ({ template: t, args: e }) => { const r = [t[0]]; for (let n = 0; n < e.length; n += 1) { - const a = e[n]; - let s; - hn(wt, a) ? s = `${a}` : vn(a) ? s = `(${dr(a.name)})` : s = `(${dr(typeof a)})`, ae(r, s, t[n + 1]); + const o = e[n]; + let a; + An(Pt, o) ? a = `${o}` : Dr(o) ? a = `(${_r(o.name)})` : a = `(${_r(typeof o)})`, X(r, a, t[n + 1]); } - return At(r, ""); -}, Ao = g({ + return Rt(r, ""); +}, qo = y({ toString() { - const t = M(Or, this); - return t === void 0 ? "[Not a DetailsToken]" : Io(t); + const t = L(Ur, this); + return t === void 0 ? "[Not a DetailsToken]" : Wo(t); } }); -g(Ao.toString); -const St = (t, ...e) => { - const r = g({ __proto__: Ao }); - return ee(Or, r, { template: t, args: e }), r; +y(qo.toString); +const ft = (t, ...e) => { + const r = y({ __proto__: qo }); + return ie(Ur, r, { template: t, args: e }), /** @type {DetailsToken} */ + /** @type {unknown} */ + r; }; -g(St); -const Co = (t, ...e) => (e = fe( +y(ft); +const Ko = (t, ...e) => (e = se( e, - (r) => hn(wt, r) ? r : ct(r) -), St(t, ...e)); -g(Co); -const $o = ({ template: t, args: e }) => { + (r) => An(Pt, r) ? r : Je(r) +), ft(t, ...e)); +y(Ko); +const Yo = ({ template: t, args: e }) => { const r = [t[0]]; for (let n = 0; n < e.length; n += 1) { - let a = e[n]; - hn(wt, a) && (a = M(wt, a)); - const s = ur(Hr(r) || "", / $/, ""); - s !== "" && ae(r, s); - const i = ur(t[n + 1], /^ /, ""); - ae(r, a, i); + let o = e[n]; + An(Pt, o) && (o = L(Pt, o)); + const a = vr(gr(r) || "", / $/, ""); + a !== "" && X(r, a); + const i = vr(t[n + 1], /^ /, ""); + X(r, o, i); } - return r[r.length - 1] === "" && Hr(r), r; -}, ir = new Te(); -let Wr = 0; -const Ln = new Te(), No = (t, e = t.name) => { - let r = M(Ln, t); - return r !== void 0 || (Wr += 1, r = `${e}#${Wr}`, ee(Ln, t, r)), r; -}, qr = (t = St`Assert failed`, e = x.Error, { errorName: r = void 0 } = {}) => { - typeof t == "string" && (t = St([t])); - const n = M(Or, t); - if (n === void 0) - throw v(`unrecognized details ${ct(t)}`); - const a = Io(n), s = new e(a); - return ee(ir, s, $o(n)), r !== void 0 && No(s, r), s; + return r[r.length - 1] === "" && gr(r), r; +}, hr = new Me(); +let nn = 0; +const Yn = new Me(), Jo = (t, e = t.name) => { + let r = L(Yn, t); + return r !== void 0 || (nn += 1, r = `${e}#${nn}`, ie(Yn, t, r)), r; +}, La = (t) => { + const e = Ze(t), { + name: r, + message: n, + errors: o = void 0, + cause: a = void 0, + stack: i = void 0, + ...c + } = e, l = De(c); + if (l.length >= 1) { + for (const d of l) + delete t[d]; + const u = Z(bn, c); + $n( + t, + ft`originally with properties ${Je(u)}` + ); + } + for (const u of De(t)) { + const d = e[u]; + d && oe(d, "get") && M(t, u, { + value: t[u] + // invoke the getter to convert to data property + }); + } + y(t); +}, on = (t = ft`Assert failed`, e = k.Error, { + errorName: r = void 0, + cause: n = void 0, + errors: o = void 0, + sanitize: a = !0 +} = {}) => { + typeof t == "string" && (t = ft([t])); + const i = L(Ur, t); + if (i === void 0) + throw v(`unrecognized details ${Je(t)}`); + const c = Wo(i), l = n && { cause: n }; + let u; + return typeof Hr < "u" && e === Hr ? u = Hr(o || [], c, l) : (u = /** @type {ErrorConstructor} */ + e( + c, + l + ), o !== void 0 && M(u, "errors", { + value: o, + writable: !0, + enumerable: !1, + configurable: !0 + })), ie(hr, u, Yo(i)), r !== void 0 && Jo(u, r), a && La(u), u; }; -g(qr); -const { addLogArgs: ya, takeLogArgsArray: ga } = To(), Kr = new Te(), Oo = (t, e) => { - typeof e == "string" && (e = St([e])); - const r = M(Or, e); +y(on); +const { addLogArgs: Fa, takeLogArgsArray: Da } = Vo(), sn = new Me(), $n = (t, e) => { + typeof e == "string" && (e = ft([e])); + const r = L(Ur, e); if (r === void 0) - throw v(`unrecognized details ${ct(e)}`); - const n = $o(r), a = M(Kr, t); - if (a !== void 0) - for (const s of a) - s(t, n); + throw v(`unrecognized details ${Je(e)}`); + const n = Yo(r), o = L(sn, t); + if (o !== void 0) + for (const a of o) + a(t, n); else - ya(t, n); + Fa(t, n); }; -g(Oo); -const va = (t) => { +y($n); +const Ua = (t) => { if (!("stack" in t)) return ""; - const e = `${t.stack}`, r = Xs(e, ` + const e = `${t.stack}`, r = ya(e, ` `); - return So(e, " ") || r === -1 ? e : mn(e, r + 1); -}, Yr = { - getStackString: x.getStackString || va, - tagError: (t) => No(t), + return Do(e, " ") || r === -1 ? e : Pn(e, r + 1); +}, br = { + getStackString: k.getStackString || Ua, + tagError: (t) => Jo(t), resetErrorTagNum: () => { - Wr = 0; + nn = 0; }, - getMessageLogArgs: (t) => M(ir, t), + getMessageLogArgs: (t) => L(hr, t), takeMessageLogArgs: (t) => { - const e = M(ir, t); - return ta(ir, t), e; + const e = L(hr, t); + return ba(hr, t), e; }, takeNoteLogArgsArray: (t, e) => { - const r = ga(t); + const r = Da(t); if (e !== void 0) { - const n = M(Kr, t); - n ? ae(n, e) : ee(Kr, t, [e]); + const n = L(sn, t); + n ? X(n, e) : ie(sn, t, [e]); } return r || []; } }; -g(Yr); -const Rr = (t = void 0, e = !1) => { - const r = e ? Co : St, n = r`Check failed`, a = (f = n, m = x.Error) => { - const p = qr(f, m); - throw t !== void 0 && t(p), p; +y(br); +const jr = (t = void 0, e = !1) => { + const r = e ? Ko : ft, n = r`Check failed`, o = (f = n, h = void 0, p = void 0) => { + const m = on(f, h, p); + throw t !== void 0 && t(m), m; }; - g(a); - const s = (f, ...m) => a(r(f, ...m)); - function i(f, m = void 0, p = void 0) { - f || a(m, p); + 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, m, p = void 0, h = void 0) => { - kr(f, m) || a( - p || r`Expected ${f} is same as ${m}`, - h || Ns + const c = (f, h, p = void 0, m = void 0, _ = void 0) => { + Nr(f, h) || o( + p || r`Expected ${f} is same as ${h}`, + m || Js, + _ ); }; - g(c); - const u = (f, m, p) => { - if (typeof f !== m) { - if (typeof m == "string" || s`${ct(m)} must be a string`, p === void 0) { - const h = dr(m); - p = r`${f} must be ${Vr(h)}`; + y(c); + const l = (f, h, p) => { + if (typeof f !== h) { + if (typeof h == "string" || a`${Je(h)} must be a string`, p === void 0) { + const m = _r(h); + p = r`${f} must be ${rn(m)}`; } - a(p, v); + o(p, v); } }; - g(u); - const d = Pr(i, { - error: qr, - fail: a, + y(l); + const d = $r(i, { + error: on, + fail: o, equal: c, - typeof: u, - string: (f, m = void 0) => u(f, "string", m), - note: Oo, + typeof: l, + string: (f, h = void 0) => l(f, "string", h), + note: $n, details: r, - Fail: s, - quote: ct, - bare: Vr, - makeAssert: Rr + Fail: a, + quote: Je, + bare: rn, + makeAssert: jr }); - return g(d); + return y(d); }; -g(Rr); -const Z = Rr(), Ro = de( - zs, - He +y(jr); +const z = jr(), Xo = J( + ca, + qe ); -Z(Ro); -const Mo = Ro.get; -Z(Mo); -const _a = (t) => oe(Mo, t, []) !== void 0, ba = (t) => { - const e = +ie(t); - return Ls(e) && ie(e) === t; -}, wa = (t) => { - Os(t), st(it(t), (e) => { - const r = de(t, e); - Z(r), ba(e) || L(t, e, { +z(Xo); +const Qo = Xo.get; +z(Qo); +const ja = (t) => ne(Qo, t, []) !== void 0, Za = (t) => { + const e = +pe(t); + return ra(e) && pe(e) === t; +}, za = (t) => { + Xs(t), ut(De(t), (e) => { + const r = J(t, e); + z(r), Za(e) || M(t, e, { ...r, writable: !1, configurable: !1 }); }); -}, Sa = () => { - if (typeof x.harden == "function") - return x.harden; - const t = new kt(), { harden: e } = { +}, Ga = () => { + if (typeof k.harden == "function") + return k.harden; + const t = new $t(), { harden: e } = { /** * @template T * @param {T} root * @returns {T} */ harden(r) { - const n = new Pt(), a = new Te(); - function s(d, f = void 0) { - if (!We(d)) + const n = new Ct(); + function o(d) { + if (!Ye(d)) return; - const m = typeof d; - if (m !== "object" && m !== "function") - throw v(`Unexpected typeof: ${m}`); - Xt(t, d) || dn(n, d) || ($r(n, d), ee(a, d, f)); + const f = typeof d; + if (f !== "object" && f !== "function") + throw v(`Unexpected typeof: ${f}`); + or(t, d) || En(n, d) || Sn(n, d); } - function i(d) { - _a(d) ? wa(d) : g(d); - const f = M(a, d) || "unknown", m = Je(d), p = B(d); - s(p, `${f}.__proto__`), st(it(m), (h) => { - const _ = `${f}.${ie(h)}`, w = m[ + const a = (d) => { + ja(d) ? za(d) : y(d); + const f = Ze(d), h = j(d); + o(h), ut(De(f), (p) => { + const m = f[ /** @type {string} */ - h + p ]; - se(w, "value") ? s(w.value, `${_}`) : (s(w.get, `${_}(get)`), s(w.set, `${_}(set)`)); + oe(m, "value") ? o(m.value) : (o(m.get), o(m.set)); }); - } - function c() { - Nn(n, i); - } - function u(d) { - Nr(t, d); - } - function l() { - Nn(n, u); - } - return s(r), c(), l(), r; + }, i = qr === void 0 && ka === void 0 ? ( + // On platforms without v8's error own stack accessor problem, + // don't pay for any extra overhead. + a + ) : (d) => { + if (Dr(d)) { + const f = J(d, "stack"); + f && f.get === qr && f.configurable && M(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: ne(qr, d, []) + }); + } + return a(d); + }, c = () => { + Vn(n, i); + }, l = (d) => { + Fr(t, d); + }, u = () => { + Vn(n, l); + }; + return o(r), c(), u(), r; } }; return e; -}, Lo = { +}, es = { // *** Value Properties of the Global Object Infinity: 1 / 0, NaN: NaN, undefined: void 0 -}, Fo = { +}, ts = { // *** Function Properties of the Global Object isFinite: "isFinite", isNaN: "isNaN", @@ -551,6 +635,8 @@ const _a = (t) => oe(Mo, t, []) !== void 0, ba = (t) => { 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", @@ -562,7 +648,7 @@ const _a = (t) => oe(Mo, t, []) !== void 0, ba = (t) => { harden: "harden", HandledPromise: "HandledPromise" // TODO: Until Promise.delegate (see below). -}, Fn = { +}, Jn = { // *** Constructor Properties of the Global Object Date: "%InitialDate%", Error: "%InitialError%", @@ -581,7 +667,7 @@ const _a = (t) => oe(Mo, t, []) !== void 0, ba = (t) => { // TODO https://github.com/Agoric/SES-shim/issues/551 // Need initial WeakRef and FinalizationGroup in // start compartment only. -}, Do = { +}, rs = { // *** Constructor Properties of the Global Object Date: "%SharedDate%", Error: "%SharedError%", @@ -589,32 +675,38 @@ const _a = (t) => oe(Mo, t, []) !== void 0, ba = (t) => { Symbol: "%SharedSymbol%", // *** Other Properties of the Global Object Math: "%SharedMath%" -}, Ea = [ +}, ns = [ EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError -], Jr = { + // https://github.com/endojs/endo/issues/550 + // Commented out to accommodate platforms prior to AggregateError. + // Instead, conditional push below. + // AggregateError, +]; +typeof AggregateError < "u" && X(ns, AggregateError); +const an = { "[[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. -}, xa = { +}, Ba = { // This property is not mentioned in ECMA 262, but is present in V8 and // necessary for lockdown to succeed. "[[Proto]]": "%AsyncFunctionPrototype%" -}, o = Jr, Dn = xa, O = { - get: o, +}, s = an, Xn = Ba, R = { + get: s, set: "undefined" -}, Ae = { - get: o, - set: o -}, Un = (t) => t === O || t === Ae; -function pt(t) { +}, Ie = { + get: s, + set: s +}, Qn = (t) => t === R || t === Ie; +function ot(t) { return { // Properties of the NativeError Constructors "[[Proto]]": "%SharedError%", @@ -622,7 +714,7 @@ function pt(t) { prototype: t }; } -function mt(t) { +function st(t) { return { // Properties of the NativeError Prototype Objects "[[Proto]]": "%ErrorPrototype%", @@ -636,7 +728,7 @@ function mt(t) { cause: !1 }; } -function ye(t) { +function ge(t) { return { // Properties of the TypedArray Constructors "[[Proto]]": "%TypedArray%", @@ -644,7 +736,7 @@ function ye(t) { prototype: t }; } -function ge(t) { +function ye(t) { return { // Properties of the TypedArray Prototype Objects "[[Proto]]": "%TypedArrayPrototype%", @@ -652,7 +744,7 @@ function ge(t) { constructor: t }; } -const jn = { +const eo = { E: "number", LN10: "number", LN2: "number", @@ -662,40 +754,40 @@ const jn = { SQRT1_2: "number", SQRT2: "number", "@@toStringTag": "string", - abs: o, - acos: o, - acosh: o, - asin: o, - asinh: o, - atan: o, - atanh: o, - atan2: o, - cbrt: o, - ceil: o, - clz32: o, - cos: o, - cosh: o, - exp: o, - expm1: o, - floor: o, - fround: o, - hypot: o, - imul: o, - log: o, - log1p: o, - log10: o, - log2: o, - max: o, - min: o, - pow: o, - round: o, - sign: o, - sin: o, - sinh: o, - sqrt: o, - tan: o, - tanh: o, - trunc: o, + 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 @@ -710,12 +802,12 @@ const jn = { mod: !1, // See https://github.com/Moddable-OpenSource/moddable/issues/523#issuecomment-1942904505 irandom: !1 -}, fr = { +}, wr = { // ECMA https://tc39.es/ecma262 // The intrinsics object has no prototype to avoid conflicts. "[[Proto]]": null, // %ThrowTypeError% - "%ThrowTypeError%": o, + "%ThrowTypeError%": s, // *** The Global Object // *** Value Properties of the Global Object Infinity: "number", @@ -723,44 +815,44 @@ const jn = { undefined: "undefined", // *** Function Properties of the Global Object // eval - "%UniqueEval%": o, - isFinite: o, - isNaN: o, - parseFloat: o, - parseInt: o, - decodeURI: o, - decodeURIComponent: o, - encodeURI: o, - encodeURIComponent: o, + "%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: o, - create: o, - defineProperties: o, - defineProperty: o, - entries: o, - freeze: o, - fromEntries: o, - getOwnPropertyDescriptor: o, - getOwnPropertyDescriptors: o, - getOwnPropertyNames: o, - getOwnPropertySymbols: o, - getPrototypeOf: o, - hasOwn: o, - is: o, - isExtensible: o, - isFrozen: o, - isSealed: o, - keys: o, - preventExtensions: o, + 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: o, - setPrototypeOf: o, - values: o, + seal: s, + setPrototypeOf: s, + values: s, // https://github.com/tc39/proposal-array-grouping - groupBy: o, + groupBy: s, // Seen on QuickJS __getClass: !1 }, @@ -768,20 +860,20 @@ const jn = { // Properties of the Object Prototype Object "[[Proto]]": null, constructor: "Object", - hasOwnProperty: o, - isPrototypeOf: o, - propertyIsEnumerable: o, - toLocaleString: o, - toString: o, - valueOf: o, + 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--": Ae, - __defineGetter__: o, - __defineSetter__: o, - __lookupGetter__: o, - __lookupSetter__: o + "--proto--": Ie, + __defineGetter__: s, + __defineSetter__: s, + __lookupGetter__: s, + __lookupSetter__: s }, "%UniqueFunction%": { // Properties of the Function Constructor @@ -793,12 +885,12 @@ const jn = { prototype: "%FunctionPrototype%" }, "%FunctionPrototype%": { - apply: o, - bind: o, - call: o, + apply: s, + bind: s, + call: s, constructor: "%InertFunction%", - toString: o, - "@@hasInstance": o, + 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 @@ -815,8 +907,8 @@ const jn = { }, "%BooleanPrototype%": { constructor: "Boolean", - toString: o, - valueOf: o + toString: s, + valueOf: s }, "%SharedSymbol%": { // Properties of the Symbol Constructor @@ -824,11 +916,11 @@ const jn = { asyncDispose: "symbol", asyncIterator: "symbol", dispose: "symbol", - for: o, + for: s, hasInstance: "symbol", isConcatSpreadable: "symbol", iterator: "symbol", - keyFor: o, + keyFor: s, match: "symbol", matchAll: "symbol", prototype: "%SymbolPrototype%", @@ -849,10 +941,10 @@ const jn = { "%SymbolPrototype%": { // Properties of the Symbol Prototype Object constructor: "%SharedSymbol%", - description: O, - toString: o, - valueOf: o, - "@@toPrimitive": o, + description: R, + toString: s, + valueOf: s, + "@@toPrimitive": s, "@@toStringTag": "string" }, "%InitialError%": { @@ -860,85 +952,89 @@ const jn = { "[[Proto]]": "%FunctionPrototype%", prototype: "%ErrorPrototype%", // Non standard, v8 only, used by tap - captureStackTrace: o, + captureStackTrace: s, // Non standard, v8 only, used by tap, tamed to accessor - stackTraceLimit: Ae, + stackTraceLimit: Ie, // Non standard, v8 only, used by several, tamed to accessor - prepareStackTrace: Ae + prepareStackTrace: Ie }, "%SharedError%": { // Properties of the Error Constructor "[[Proto]]": "%FunctionPrototype%", prototype: "%ErrorPrototype%", // Non standard, v8 only, used by tap - captureStackTrace: o, + captureStackTrace: s, // Non standard, v8 only, used by tap, tamed to accessor - stackTraceLimit: Ae, + stackTraceLimit: Ie, // Non standard, v8 only, used by several, tamed to accessor - prepareStackTrace: Ae + prepareStackTrace: Ie }, "%ErrorPrototype%": { constructor: "%SharedError%", message: "string", name: "string", - toString: o, + toString: s, // proposed de-facto, assumed TODO // Seen on FF Nightly 88.0a1 at: !1, // Seen on FF and XS - stack: Ae, + stack: Ie, // 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: pt("%EvalErrorPrototype%"), - RangeError: pt("%RangeErrorPrototype%"), - ReferenceError: pt("%ReferenceErrorPrototype%"), - SyntaxError: pt("%SyntaxErrorPrototype%"), - TypeError: pt("%TypeErrorPrototype%"), - URIError: pt("%URIErrorPrototype%"), - "%EvalErrorPrototype%": mt("EvalError"), - "%RangeErrorPrototype%": mt("RangeError"), - "%ReferenceErrorPrototype%": mt("ReferenceError"), - "%SyntaxErrorPrototype%": mt("SyntaxError"), - "%TypeErrorPrototype%": mt("TypeError"), - "%URIErrorPrototype%": mt("URIError"), + EvalError: ot("%EvalErrorPrototype%"), + RangeError: ot("%RangeErrorPrototype%"), + ReferenceError: ot("%ReferenceErrorPrototype%"), + SyntaxError: ot("%SyntaxErrorPrototype%"), + TypeError: ot("%TypeErrorPrototype%"), + URIError: ot("%URIErrorPrototype%"), + // https://github.com/endojs/endo/issues/550 + AggregateError: ot("%AggregateErrorPrototype%"), + "%EvalErrorPrototype%": st("EvalError"), + "%RangeErrorPrototype%": st("RangeError"), + "%ReferenceErrorPrototype%": st("ReferenceError"), + "%SyntaxErrorPrototype%": st("SyntaxError"), + "%TypeErrorPrototype%": st("TypeError"), + "%URIErrorPrototype%": st("URIError"), + // https://github.com/endojs/endo/issues/550 + "%AggregateErrorPrototype%": st("AggregateError"), // *** Numbers and Dates Number: { // Properties of the Number Constructor "[[Proto]]": "%FunctionPrototype%", EPSILON: "number", - isFinite: o, - isInteger: o, - isNaN: o, - isSafeInteger: o, + 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: o, - parseInt: o, + parseFloat: s, + parseInt: s, POSITIVE_INFINITY: "number", prototype: "%NumberPrototype%" }, "%NumberPrototype%": { // Properties of the Number Prototype Object constructor: "Number", - toExponential: o, - toFixed: o, - toLocaleString: o, - toPrecision: o, - toString: o, - valueOf: o + toExponential: s, + toFixed: s, + toLocaleString: s, + toPrecision: s, + toString: s, + valueOf: s }, BigInt: { // Properties of the BigInt Constructor "[[Proto]]": "%FunctionPrototype%", - asIntN: o, - asUintN: o, + asIntN: s, + asUintN: s, prototype: "%BigIntPrototype%", // See https://github.com/Moddable-OpenSource/moddable/issues/523 bitLength: !1, @@ -971,174 +1067,174 @@ const jn = { }, "%BigIntPrototype%": { constructor: "BigInt", - toLocaleString: o, - toString: o, - valueOf: o, + toLocaleString: s, + toString: s, + valueOf: s, "@@toStringTag": "string" }, "%InitialMath%": { - ...jn, + ...eo, // `%InitialMath%.random()` has the standard unsafe behavior - random: o + random: s }, "%SharedMath%": { - ...jn, + ...eo, // `%SharedMath%.random()` is tamed to always throw - random: o + random: s }, "%InitialDate%": { // Properties of the Date Constructor "[[Proto]]": "%FunctionPrototype%", - now: o, - parse: o, + now: s, + parse: s, prototype: "%DatePrototype%", - UTC: o + UTC: s }, "%SharedDate%": { // Properties of the Date Constructor "[[Proto]]": "%FunctionPrototype%", // `%SharedDate%.now()` is tamed to always throw - now: o, - parse: o, + now: s, + parse: s, prototype: "%DatePrototype%", - UTC: o + UTC: s }, "%DatePrototype%": { constructor: "%SharedDate%", - getDate: o, - getDay: o, - getFullYear: o, - getHours: o, - getMilliseconds: o, - getMinutes: o, - getMonth: o, - getSeconds: o, - getTime: o, - getTimezoneOffset: o, - getUTCDate: o, - getUTCDay: o, - getUTCFullYear: o, - getUTCHours: o, - getUTCMilliseconds: o, - getUTCMinutes: o, - getUTCMonth: o, - getUTCSeconds: o, - setDate: o, - setFullYear: o, - setHours: o, - setMilliseconds: o, - setMinutes: o, - setMonth: o, - setSeconds: o, - setTime: o, - setUTCDate: o, - setUTCFullYear: o, - setUTCHours: o, - setUTCMilliseconds: o, - setUTCMinutes: o, - setUTCMonth: o, - setUTCSeconds: o, - toDateString: o, - toISOString: o, - toJSON: o, - toLocaleDateString: o, - toLocaleString: o, - toLocaleTimeString: o, - toString: o, - toTimeString: o, - toUTCString: o, - valueOf: o, - "@@toPrimitive": o, + 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: o, - setYear: o, - toGMTString: o + getYear: s, + setYear: s, + toGMTString: s }, // Text Processing String: { // Properties of the String Constructor "[[Proto]]": "%FunctionPrototype%", - fromCharCode: o, - fromCodePoint: o, + fromCharCode: s, + fromCodePoint: s, prototype: "%StringPrototype%", - raw: o, + raw: s, // See https://github.com/Moddable-OpenSource/moddable/issues/523 fromArrayBuffer: !1 }, "%StringPrototype%": { // Properties of the String Prototype Object length: "number", - at: o, - charAt: o, - charCodeAt: o, - codePointAt: o, - concat: o, + at: s, + charAt: s, + charCodeAt: s, + codePointAt: s, + concat: s, constructor: "String", - endsWith: o, - includes: o, - indexOf: o, - lastIndexOf: o, - localeCompare: o, - match: o, - matchAll: o, - normalize: o, - padEnd: o, - padStart: o, - repeat: o, - replace: o, - replaceAll: o, + 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: o, - slice: o, - split: o, - startsWith: o, - substring: o, - toLocaleLowerCase: o, - toLocaleUpperCase: o, - toLowerCase: o, - toString: o, - toUpperCase: o, - trim: o, - trimEnd: o, - trimStart: o, - valueOf: o, - "@@iterator": o, + 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: o, - anchor: o, - big: o, - blink: o, - bold: o, - fixed: o, - fontcolor: o, - fontsize: o, - italics: o, - link: o, - small: o, - strike: o, - sub: o, - sup: o, - trimLeft: o, - trimRight: o, + 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: o, - toWellFormed: o, - unicodeSets: o, + isWellFormed: s, + toWellFormed: s, + unicodeSets: s, // Seen on QuickJS __quote: !1 }, "%StringIteratorPrototype%": { "[[Proto]]": "%IteratorPrototype%", - next: o, + next: s, "@@toStringTag": "string" }, "%InitialRegExp%": { // Properties of the RegExp Constructor "[[Proto]]": "%FunctionPrototype%", prototype: "%RegExpPrototype%", - "@@species": O, + "@@species": R, // The https://github.com/tc39/proposal-regexp-legacy-features // are all optional, unsafe, and omitted input: !1, @@ -1165,29 +1261,29 @@ const jn = { // Properties of the RegExp Constructor "[[Proto]]": "%FunctionPrototype%", prototype: "%RegExpPrototype%", - "@@species": O + "@@species": R }, "%RegExpPrototype%": { // Properties of the RegExp Prototype Object constructor: "%SharedRegExp%", - exec: o, - dotAll: O, - flags: O, - global: O, - hasIndices: O, - ignoreCase: O, - "@@match": o, - "@@matchAll": o, - multiline: O, - "@@replace": o, - "@@search": o, - source: O, - "@@split": o, - sticky: O, - test: o, - toString: o, - unicode: O, - unicodeSets: O, + exec: s, + dotAll: R, + flags: R, + global: R, + hasIndices: R, + ignoreCase: R, + "@@match": s, + "@@matchAll": s, + multiline: R, + "@@replace": s, + "@@search": s, + source: R, + "@@split": s, + sticky: R, + test: s, + toString: s, + unicode: R, + unicodeSets: R, // Annex B: Additional Properties of the RegExp.prototype Object compile: !1 // UNSAFE and suppressed. @@ -1195,61 +1291,61 @@ const jn = { "%RegExpStringIteratorPrototype%": { // The %RegExpStringIteratorPrototype% Object "[[Proto]]": "%IteratorPrototype%", - next: o, + next: s, "@@toStringTag": "string" }, // Indexed Collections Array: { // Properties of the Array Constructor "[[Proto]]": "%FunctionPrototype%", - from: o, - isArray: o, - of: o, + from: s, + isArray: s, + of: s, prototype: "%ArrayPrototype%", - "@@species": O, + "@@species": R, // Stage 3: // https://tc39.es/proposal-relative-indexing-method/ - at: o, + at: s, // https://tc39.es/proposal-array-from-async/ - fromAsync: o + fromAsync: s }, "%ArrayPrototype%": { // Properties of the Array Prototype Object - at: o, + at: s, length: "number", - concat: o, + concat: s, constructor: "Array", - copyWithin: o, - entries: o, - every: o, - fill: o, - filter: o, - find: o, - findIndex: o, - flat: o, - flatMap: o, - forEach: o, - includes: o, - indexOf: o, - join: o, - keys: o, - lastIndexOf: o, - map: o, - pop: o, - push: o, - reduce: o, - reduceRight: o, - reverse: o, - shift: o, - slice: o, - some: o, - sort: o, - splice: o, - toLocaleString: o, - toString: o, - unshift: o, - values: o, - "@@iterator": o, + 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", @@ -1279,174 +1375,174 @@ const jn = { groupBy: "boolean" }, // See https://github.com/tc39/proposal-array-find-from-last - findLast: o, - findLastIndex: o, + findLast: s, + findLastIndex: s, // https://github.com/tc39/proposal-change-array-by-copy - toReversed: o, - toSorted: o, - toSpliced: o, - with: o, + toReversed: s, + toSorted: s, + toSpliced: s, + with: s, // https://github.com/tc39/proposal-array-grouping - group: o, + group: s, // Not in proposal? Where? - groupToMap: o, + groupToMap: s, // Not in proposal? Where? - groupBy: o + groupBy: s }, "%ArrayIteratorPrototype%": { // The %ArrayIteratorPrototype% Object "[[Proto]]": "%IteratorPrototype%", - next: o, + next: s, "@@toStringTag": "string" }, // *** TypedArray Objects "%TypedArray%": { // Properties of the %TypedArray% Intrinsic Object "[[Proto]]": "%FunctionPrototype%", - from: o, - of: o, + from: s, + of: s, prototype: "%TypedArrayPrototype%", - "@@species": O + "@@species": R }, "%TypedArrayPrototype%": { - at: o, - buffer: O, - byteLength: O, - byteOffset: O, + at: s, + buffer: R, + byteLength: R, + byteOffset: R, constructor: "%TypedArray%", - copyWithin: o, - entries: o, - every: o, - fill: o, - filter: o, - find: o, - findIndex: o, - forEach: o, - includes: o, - indexOf: o, - join: o, - keys: o, - lastIndexOf: o, - length: O, - map: o, - reduce: o, - reduceRight: o, - reverse: o, - set: o, - slice: o, - some: o, - sort: o, - subarray: o, - toLocaleString: o, - toString: o, - values: o, - "@@iterator": o, - "@@toStringTag": O, + 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: R, + 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": R, // See https://github.com/tc39/proposal-array-find-from-last - findLast: o, - findLastIndex: o, + findLast: s, + findLastIndex: s, // https://github.com/tc39/proposal-change-array-by-copy - toReversed: o, - toSorted: o, - with: o + toReversed: s, + toSorted: s, + with: s }, // The TypedArray Constructors - BigInt64Array: ye("%BigInt64ArrayPrototype%"), - BigUint64Array: ye("%BigUint64ArrayPrototype%"), + BigInt64Array: ge("%BigInt64ArrayPrototype%"), + BigUint64Array: ge("%BigUint64ArrayPrototype%"), // https://github.com/tc39/proposal-float16array - Float16Array: ye("%Float16ArrayPrototype%"), - Float32Array: ye("%Float32ArrayPrototype%"), - Float64Array: ye("%Float64ArrayPrototype%"), - Int16Array: ye("%Int16ArrayPrototype%"), - Int32Array: ye("%Int32ArrayPrototype%"), - Int8Array: ye("%Int8ArrayPrototype%"), - Uint16Array: ye("%Uint16ArrayPrototype%"), - Uint32Array: ye("%Uint32ArrayPrototype%"), - Uint8Array: ye("%Uint8ArrayPrototype%"), - Uint8ClampedArray: ye("%Uint8ClampedArrayPrototype%"), - "%BigInt64ArrayPrototype%": ge("BigInt64Array"), - "%BigUint64ArrayPrototype%": ge("BigUint64Array"), + Float16Array: ge("%Float16ArrayPrototype%"), + Float32Array: ge("%Float32ArrayPrototype%"), + Float64Array: ge("%Float64ArrayPrototype%"), + Int16Array: ge("%Int16ArrayPrototype%"), + Int32Array: ge("%Int32ArrayPrototype%"), + Int8Array: ge("%Int8ArrayPrototype%"), + Uint16Array: ge("%Uint16ArrayPrototype%"), + Uint32Array: ge("%Uint32ArrayPrototype%"), + Uint8Array: ge("%Uint8ArrayPrototype%"), + Uint8ClampedArray: ge("%Uint8ClampedArrayPrototype%"), + "%BigInt64ArrayPrototype%": ye("BigInt64Array"), + "%BigUint64ArrayPrototype%": ye("BigUint64Array"), // https://github.com/tc39/proposal-float16array - "%Float16ArrayPrototype%": ge("Float16Array"), - "%Float32ArrayPrototype%": ge("Float32Array"), - "%Float64ArrayPrototype%": ge("Float64Array"), - "%Int16ArrayPrototype%": ge("Int16Array"), - "%Int32ArrayPrototype%": ge("Int32Array"), - "%Int8ArrayPrototype%": ge("Int8Array"), - "%Uint16ArrayPrototype%": ge("Uint16Array"), - "%Uint32ArrayPrototype%": ge("Uint32Array"), - "%Uint8ArrayPrototype%": ge("Uint8Array"), - "%Uint8ClampedArrayPrototype%": ge("Uint8ClampedArray"), + "%Float16ArrayPrototype%": ye("Float16Array"), + "%Float32ArrayPrototype%": ye("Float32Array"), + "%Float64ArrayPrototype%": ye("Float64Array"), + "%Int16ArrayPrototype%": ye("Int16Array"), + "%Int32ArrayPrototype%": ye("Int32Array"), + "%Int8ArrayPrototype%": ye("Int8Array"), + "%Uint16ArrayPrototype%": ye("Uint16Array"), + "%Uint32ArrayPrototype%": ye("Uint32Array"), + "%Uint8ArrayPrototype%": ye("Uint8Array"), + "%Uint8ClampedArrayPrototype%": ye("Uint8ClampedArray"), // *** Keyed Collections Map: { // Properties of the Map Constructor "[[Proto]]": "%FunctionPrototype%", - "@@species": O, + "@@species": R, prototype: "%MapPrototype%", // https://github.com/tc39/proposal-array-grouping - groupBy: o + groupBy: s }, "%MapPrototype%": { - clear: o, + clear: s, constructor: "Map", - delete: o, - entries: o, - forEach: o, - get: o, - has: o, - keys: o, - set: o, - size: O, - values: o, - "@@iterator": o, + delete: s, + entries: s, + forEach: s, + get: s, + has: s, + keys: s, + set: s, + size: R, + values: s, + "@@iterator": s, "@@toStringTag": "string" }, "%MapIteratorPrototype%": { // The %MapIteratorPrototype% Object "[[Proto]]": "%IteratorPrototype%", - next: o, + next: s, "@@toStringTag": "string" }, Set: { // Properties of the Set Constructor "[[Proto]]": "%FunctionPrototype%", prototype: "%SetPrototype%", - "@@species": O, + "@@species": R, // Seen on QuickJS groupBy: !1 }, "%SetPrototype%": { - add: o, - clear: o, + add: s, + clear: s, constructor: "Set", - delete: o, - entries: o, - forEach: o, - has: o, - keys: o, - size: O, - values: o, - "@@iterator": o, + delete: s, + entries: s, + forEach: s, + has: s, + keys: s, + size: R, + values: s, + "@@iterator": s, "@@toStringTag": "string", // See https://github.com/tc39/proposal-set-methods - intersection: o, + intersection: s, // See https://github.com/tc39/proposal-set-methods - union: o, + union: s, // See https://github.com/tc39/proposal-set-methods - difference: o, + difference: s, // See https://github.com/tc39/proposal-set-methods - symmetricDifference: o, + symmetricDifference: s, // See https://github.com/tc39/proposal-set-methods - isSubsetOf: o, + isSubsetOf: s, // See https://github.com/tc39/proposal-set-methods - isSupersetOf: o, + isSupersetOf: s, // See https://github.com/tc39/proposal-set-methods - isDisjointFrom: o + isDisjointFrom: s }, "%SetIteratorPrototype%": { // The %SetIteratorPrototype% Object "[[Proto]]": "%IteratorPrototype%", - next: o, + next: s, "@@toStringTag": "string" }, WeakMap: { @@ -1456,10 +1552,10 @@ const jn = { }, "%WeakMapPrototype%": { constructor: "WeakMap", - delete: o, - get: o, - has: o, - set: o, + delete: s, + get: s, + has: s, + set: s, "@@toStringTag": "string" }, WeakSet: { @@ -1468,39 +1564,39 @@ const jn = { prototype: "%WeakSetPrototype%" }, "%WeakSetPrototype%": { - add: o, + add: s, constructor: "WeakSet", - delete: o, - has: o, + delete: s, + has: s, "@@toStringTag": "string" }, // *** Structured Data ArrayBuffer: { // Properties of the ArrayBuffer Constructor "[[Proto]]": "%FunctionPrototype%", - isView: o, + isView: s, prototype: "%ArrayBufferPrototype%", - "@@species": O, + "@@species": R, // See https://github.com/Moddable-OpenSource/moddable/issues/523 fromString: !1, // See https://github.com/Moddable-OpenSource/moddable/issues/523 fromBigInt: !1 }, "%ArrayBufferPrototype%": { - byteLength: O, + byteLength: R, constructor: "ArrayBuffer", - slice: o, + slice: s, "@@toStringTag": "string", // See https://github.com/Moddable-OpenSource/moddable/issues/523 concat: !1, // See https://github.com/tc39/proposal-resizablearraybuffer - transfer: o, - resize: o, - resizable: O, - maxByteLength: O, + transfer: s, + resize: s, + resizable: R, + maxByteLength: R, // https://github.com/tc39/proposal-arraybuffer-transfer - transferToFixedLength: o, - detached: O + transferToFixedLength: s, + detached: R }, // SharedArrayBuffer Objects SharedArrayBuffer: !1, @@ -1515,46 +1611,46 @@ const jn = { prototype: "%DataViewPrototype%" }, "%DataViewPrototype%": { - buffer: O, - byteLength: O, - byteOffset: O, + buffer: R, + byteLength: R, + byteOffset: R, constructor: "DataView", - getBigInt64: o, - getBigUint64: o, + getBigInt64: s, + getBigUint64: s, // https://github.com/tc39/proposal-float16array - getFloat16: o, - getFloat32: o, - getFloat64: o, - getInt8: o, - getInt16: o, - getInt32: o, - getUint8: o, - getUint16: o, - getUint32: o, - setBigInt64: o, - setBigUint64: o, + 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: o, - setFloat32: o, - setFloat64: o, - setInt8: o, - setInt16: o, - setInt32: o, - setUint8: o, - setUint16: o, - setUint32: o, + 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: o, - stringify: o, + parse: s, + stringify: s, "@@toStringTag": "string", // https://github.com/tc39/proposal-json-parse-with-source/ - rawJSON: o, - isRawJSON: o + rawJSON: s, + isRawJSON: s }, // *** Control Abstraction Objects // https://github.com/tc39/proposal-iterator-helpers @@ -1562,41 +1658,41 @@ const jn = { // Properties of the Iterator Constructor "[[Proto]]": "%FunctionPrototype%", prototype: "%IteratorPrototype%", - from: o + from: s }, "%IteratorPrototype%": { // The %IteratorPrototype% Object - "@@iterator": o, + "@@iterator": s, // https://github.com/tc39/proposal-iterator-helpers constructor: "Iterator", - map: o, - filter: o, - take: o, - drop: o, - flatMap: o, - reduce: o, - toArray: o, - forEach: o, - some: o, - every: o, - find: o, + 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: o, + 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: o, - return: o + next: s, + return: s }, // https://github.com/tc39/proposal-iterator-helpers "%IteratorHelperPrototype%": { "[[Proto]]": "%IteratorPrototype%", - next: o, - return: o, + next: s, + return: s, "@@toStringTag": "string" }, // https://github.com/tc39/proposal-async-iterator-helpers @@ -1604,24 +1700,24 @@ const jn = { // Properties of the Iterator Constructor "[[Proto]]": "%FunctionPrototype%", prototype: "%AsyncIteratorPrototype%", - from: o + from: s }, "%AsyncIteratorPrototype%": { // The %AsyncIteratorPrototype% Object - "@@asyncIterator": o, + "@@asyncIterator": s, // https://github.com/tc39/proposal-async-iterator-helpers constructor: "AsyncIterator", - map: o, - filter: o, - take: o, - drop: o, - flatMap: o, - reduce: o, - toArray: o, - forEach: o, - some: o, - every: o, - find: o, + 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 @@ -1629,14 +1725,14 @@ const jn = { // https://github.com/tc39/proposal-async-iterator-helpers "%WrapForValidAsyncIteratorPrototype%": { "[[Proto]]": "%AsyncIteratorPrototype%", - next: o, - return: o + next: s, + return: s }, // https://github.com/tc39/proposal-async-iterator-helpers "%AsyncIteratorHelperPrototype%": { "[[Proto]]": "%AsyncIteratorPrototype%", - next: o, - return: o, + next: s, + return: s, "@@toStringTag": "string" }, "%InertGeneratorFunction%": { @@ -1671,18 +1767,18 @@ const jn = { // Properties of the Generator Prototype Object "[[Proto]]": "%IteratorPrototype%", constructor: "%Generator%", - next: o, - return: o, - throw: o, + next: s, + return: s, + throw: s, "@@toStringTag": "string" }, "%AsyncGeneratorPrototype%": { // Properties of the AsyncGenerator Prototype Object "[[Proto]]": "%AsyncIteratorPrototype%", constructor: "%AsyncGenerator%", - next: o, - return: o, - throw: o, + next: s, + return: s, + throw: s, "@@toStringTag": "string" }, // TODO: To be replaced with Promise.delegate @@ -1696,43 +1792,41 @@ const jn = { // another whitelist change to update to the current proposed standard. HandledPromise: { "[[Proto]]": "Promise", - applyFunction: o, - applyFunctionSendOnly: o, - applyMethod: o, - applyMethodSendOnly: o, - get: o, - getSendOnly: o, + applyFunction: s, + applyFunctionSendOnly: s, + applyMethod: s, + applyMethodSendOnly: s, + get: s, + getSendOnly: s, prototype: "%PromisePrototype%", - resolve: o + resolve: s }, Promise: { // Properties of the Promise Constructor "[[Proto]]": "%FunctionPrototype%", - all: o, - allSettled: o, - // To transition from `false` to `fn` once we also have `AggregateError` - // TODO https://github.com/Agoric/SES-shim/issues/550 - any: !1, - // ES2021 + all: s, + allSettled: s, + // https://github.com/Agoric/SES-shim/issues/550 + any: s, prototype: "%PromisePrototype%", - race: o, - reject: o, - resolve: o, + race: s, + reject: s, + resolve: s, // https://github.com/tc39/proposal-promise-with-resolvers - withResolvers: o, - "@@species": O + withResolvers: s, + "@@species": R }, "%PromisePrototype%": { // Properties of the Promise Prototype Object - catch: o, + catch: s, constructor: "Promise", - finally: o, - then: o, + finally: s, + then: s, "@@toStringTag": "string", // Non-standard, used in node to prevent async_hooks from breaking - "UniqueSymbol(async_id_symbol)": Ae, - "UniqueSymbol(trigger_async_id_symbol)": Ae, - "UniqueSymbol(destroyed)": Ae + "UniqueSymbol(async_id_symbol)": Ie, + "UniqueSymbol(trigger_async_id_symbol)": Ie, + "UniqueSymbol(destroyed)": Ie }, "%InertAsyncFunction%": { // Properties of the AsyncFunction Constructor @@ -1753,95 +1847,95 @@ const jn = { Reflect: { // The Reflect Object // Not a function object. - apply: o, - construct: o, - defineProperty: o, - deleteProperty: o, - get: o, - getOwnPropertyDescriptor: o, - getPrototypeOf: o, - has: o, - isExtensible: o, - ownKeys: o, - preventExtensions: o, - set: o, - setPrototypeOf: o, + 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: o + revocable: s }, // Appendix B // Annex B: Additional Properties of the Global Object - escape: o, - unescape: o, + escape: s, + unescape: s, // Proposed "%UniqueCompartment%": { "[[Proto]]": "%FunctionPrototype%", prototype: "%CompartmentPrototype%", - toString: o + toString: s }, "%InertCompartment%": { "[[Proto]]": "%FunctionPrototype%", prototype: "%CompartmentPrototype%", - toString: o + toString: s }, "%CompartmentPrototype%": { constructor: "%InertCompartment%", - evaluate: o, - globalThis: O, - name: O, - import: Dn, - load: Dn, - importNow: o, - module: o, + evaluate: s, + globalThis: R, + name: R, + import: Xn, + load: Xn, + importNow: s, + module: s, "@@toStringTag": "string" }, - lockdown: o, - harden: { ...o, isFake: "boolean" }, - "%InitialGetStackString%": o -}, Pa = (t) => typeof t == "function"; -function ka(t, e, r) { - if (se(t, e)) { - const n = de(t, e); - if (!n || !kr(n.value, r.value) || n.get !== r.get || n.set !== r.set || n.writable !== r.writable || n.enumerable !== r.enumerable || n.configurable !== r.configurable) + lockdown: s, + harden: { ...s, isFake: "boolean" }, + "%InitialGetStackString%": s +}, Ha = (t) => typeof t == "function"; +function Va(t, e, r) { + if (oe(t, e)) { + const n = J(t, e); + if (!n || !Nr(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 v(`Conflicting definitions of ${e}`); } - L(t, e, r); + M(t, e, r); } -function Ta(t, e) { - for (const [r, n] of te(e)) - ka(t, r, n); +function Wa(t, e) { + for (const [r, n] of re(e)) + Va(t, r, n); } -function Uo(t, e) { +function os(t, e) { const r = { __proto__: null }; - for (const [n, a] of te(e)) - se(t, n) && (r[a] = t[n]); + for (const [n, o] of re(e)) + oe(t, n) && (r[o] = t[n]); return r; } -const jo = () => { - const t = H(null); +const ss = () => { + const t = Z(null); let e; const r = (c) => { - Ta(t, Je(c)); + Wa(t, Ze(c)); }; - g(r); + y(r); const n = () => { - for (const [c, u] of te(t)) { - if (!We(u) || !se(u, "prototype")) + for (const [c, l] of re(t)) { + if (!Ye(l) || !oe(l, "prototype")) continue; - const l = fr[c]; - if (typeof l != "object") + const u = wr[c]; + if (typeof u != "object") throw v(`Expected permit object at whitelist.${c}`); - const d = l.prototype; + const d = u.prototype; if (!d) throw v(`${c}.prototype property not whitelisted`); - if (typeof d != "string" || !se(fr, d)) + if (typeof d != "string" || !oe(wr, d)) throw v(`Unrecognized ${c}.prototype whitelist entry`); - const f = u.prototype; - if (se(t, d)) { + const f = l.prototype; + if (oe(t, d)) { if (t[d] !== f) throw v(`Conflicting bindings of ${d}`); continue; @@ -1849,157 +1943,157 @@ const jo = () => { t[d] = f; } }; - g(n); - const a = () => (g(t), e = new kt(Ve(fo(t), Pa)), t); - g(a); - const s = (c) => { + y(n); + const o = () => (y(t), e = new $t(Ke(ko(t), Ha)), t); + y(o); + const a = (c) => { if (!e) throw v( "isPseudoNative can only be called after finalIntrinsics" ); - return Xt(e, c); + return or(e, c); }; - g(s); + y(a); const i = { addIntrinsics: r, completePrototypes: n, - finalIntrinsics: a, - isPseudoNative: s + finalIntrinsics: o, + isPseudoNative: a }; - return g(i), r(Lo), r(Uo(x, Fo)), i; -}, Ia = (t) => { - const { addIntrinsics: e, finalIntrinsics: r } = jo(); - return e(Uo(t, Do)), r(); + return y(i), r(es), r(os(k, ts)), i; +}, qa = (t) => { + const { addIntrinsics: e, finalIntrinsics: r } = ss(); + return e(os(t, rs)), r(); }; -function Aa(t, e) { +function Ka(t, e) { let r = !1; - const n = (m, ...p) => (r || (console.groupCollapsed("Removing unpermitted intrinsics"), r = !0), console[m](...p)), a = ["undefined", "boolean", "number", "string", "symbol"], s = new Ce( - Ot ? fe( - Ve( - te(fr["%SharedSymbol%"]), - ([m, p]) => p === "symbol" && typeof Ot[m] == "symbol" + const n = (h, ...p) => (r || (console.groupCollapsed("Removing unpermitted intrinsics"), r = !0), console[h](...p)), o = ["undefined", "boolean", "number", "string", "symbol"], a = new Pe( + St ? se( + Ke( + re(wr["%SharedSymbol%"]), + ([h, p]) => p === "symbol" && typeof St[h] == "symbol" ), - ([m]) => [Ot[m], `@@${m}`] + ([h]) => [St[h], `@@${h}`] ) : [] ); - function i(m, p) { + function i(h, p) { if (typeof p == "string") return p; - const h = De(s, p); + const m = Ue(a, p); if (typeof p == "symbol") { - if (h) - return h; + if (m) + return m; { - const _ = Ms(p); - return _ !== void 0 ? `RegisteredSymbol(${_})` : `Unique${ie(p)}`; + const _ = ea(p); + return _ !== void 0 ? `RegisteredSymbol(${_})` : `Unique${pe(p)}`; } } - throw v(`Unexpected property name type ${m} ${p}`); + throw v(`Unexpected property name type ${h} ${p}`); } - function c(m, p, h) { - if (!We(p)) - throw v(`Object expected: ${m}, ${p}, ${h}`); - const _ = B(p); - if (!(_ === null && h === null)) { - if (h !== void 0 && typeof h != "string") - throw v(`Malformed whitelist permit ${m}.__proto__`); - if (_ !== t[h || "%ObjectPrototype%"]) - throw v(`Unexpected intrinsic ${m}.__proto__ at ${h}`); + function c(h, p, m) { + if (!Ye(p)) + throw v(`Object expected: ${h}, ${p}, ${m}`); + const _ = j(p); + if (!(_ === null && m === null)) { + if (m !== void 0 && typeof m != "string") + throw v(`Malformed whitelist permit ${h}.__proto__`); + if (_ !== t[m || "%ObjectPrototype%"]) + throw v(`Unexpected intrinsic ${h}.__proto__ at ${m}`); } } - function u(m, p, h, _) { + function l(h, p, m, _) { if (typeof _ == "object") - return f(m, p, _), !0; + return f(h, p, _), !0; if (_ === !1) return !1; if (typeof _ == "string") { - if (h === "prototype" || h === "constructor") { - if (se(t, _)) { + if (m === "prototype" || m === "constructor") { + if (oe(t, _)) { if (p !== t[_]) - throw v(`Does not match whitelist ${m}`); + throw v(`Does not match whitelist ${h}`); return !0; } - } else if (Ar(a, _)) { + } else if (Mr(o, _)) { if (typeof p !== _) throw v( - `At ${m} expected ${_} not ${typeof p}` + `At ${h} expected ${_} not ${typeof p}` ); return !0; } } - throw v(`Unexpected whitelist permit ${_} at ${m}`); + throw v(`Unexpected whitelist permit ${_} at ${h}`); } - function l(m, p, h, _) { - const w = de(p, h); - if (!w) - throw v(`Property ${h} not found at ${m}`); - if (se(w, "value")) { - if (Un(_)) - throw v(`Accessor expected at ${m}`); - return u(m, w.value, h, _); + function u(h, p, m, _) { + const S = J(p, m); + if (!S) + throw v(`Property ${m} not found at ${h}`); + if (oe(S, "value")) { + if (Qn(_)) + throw v(`Accessor expected at ${h}`); + return l(h, S.value, m, _); } - if (!Un(_)) - throw v(`Accessor not expected at ${m}`); - return u(`${m}`, w.get, h, _.get) && u(`${m}`, w.set, h, _.set); + if (!Qn(_)) + throw v(`Accessor not expected at ${h}`); + return l(`${h}`, S.get, m, _.get) && l(`${h}`, S.set, m, _.set); } - function d(m, p, h) { - const _ = h === "__proto__" ? "--proto--" : h; - if (se(p, _)) + function d(h, p, m) { + const _ = m === "__proto__" ? "--proto--" : m; + if (oe(p, _)) return p[_]; - if (typeof m == "function" && se(Jr, _)) - return Jr[_]; + if (typeof h == "function" && oe(an, _)) + return an[_]; } - function f(m, p, h) { + function f(h, p, m) { if (p == null) return; - const _ = h["[[Proto]]"]; - c(m, p, _), typeof p == "function" && e(p); - for (const w of it(p)) { - const I = i(m, w), $ = `${m}.${I}`, T = d(p, h, I); - if (!T || !l($, p, w, T)) { - T !== !1 && n("warn", `Removing ${$}`); + const _ = m["[[Proto]]"]; + c(h, p, _), typeof p == "function" && e(p); + for (const S of De(p)) { + const T = i(h, S), N = `${h}.${T}`, x = d(p, m, T); + if (!x || !u(N, p, S, x)) { + x !== !1 && n("warn", `Removing ${N}`); try { - delete p[w]; + delete p[S]; } catch (D) { - if (w in p) { - if (typeof p == "function" && w === "prototype" && (p.prototype = void 0, p.prototype === void 0)) { + if (S in p) { + if (typeof p == "function" && S === "prototype" && (p.prototype = void 0, p.prototype === void 0)) { n( "warn", - `Tolerating undeletable ${$} === undefined` + `Tolerating undeletable ${N} === undefined` ); continue; } - n("error", `failed to delete ${$}`, D); + n("error", `failed to delete ${N}`, D); } else - n("error", `deleting ${$} threw`, D); + n("error", `deleting ${N} threw`, D); throw D; } } } } try { - f("intrinsics", t, fr); + f("intrinsics", t, wr); } finally { r && console.groupEnd(); } } -function Ca() { +function Ya() { try { ve.prototype.constructor("return 1"); } catch { - return g({}); + return y({}); } const t = {}; - function e(r, n, a) { - let s; + function e(r, n, o) { + let a; try { - s = (0, eval)(a); - } catch (u) { - if (u instanceof Kt) + a = (0, eval)(o); + } catch (l) { + if (l instanceof tr) return; - throw u; + throw l; } - const i = B(s), c = function() { + const i = j(a), c = function() { throw v( "Function.prototype.constructor is not a valid constructor." ); @@ -2014,7 +2108,7 @@ function Ca() { } }), F(i, { constructor: { value: c } - }), c !== ve.prototype.constructor && uo(c, ve.prototype.constructor), t[n] = c; + }), c !== ve.prototype.constructor && xo(c, ve.prototype.constructor), t[n] = c; } return e("Function", "%InertFunction%", "(function(){})"), e( "GeneratorFunction", @@ -2030,10 +2124,10 @@ function Ca() { "(async function*(){})" ), t; } -function $a(t = "safe") { +function Ja(t = "safe") { if (t !== "safe" && t !== "unsafe") throw v(`unrecognized dateTaming ${t}`); - const e = ks, r = e.prototype, n = { + const e = Hs, r = e.prototype, n = { /** * `%SharedDate%.now()` throw a `TypeError` starting with "secure mode". * See https://github.com/endojs/endo/issues/910#issuecomment-1581855420 @@ -2041,11 +2135,11 @@ function $a(t = "safe") { now() { throw v("secure mode Calling %SharedDate%.now() throws"); } - }, a = ({ powers: c = "none" } = {}) => { - let u; - return c === "original" ? u = function(...d) { - return new.target === void 0 ? oe(e, void 0, d) : lr(e, d, new.target); - } : u = function(...d) { + }, o = ({ powers: c = "none" } = {}) => { + let l; + return c === "original" ? l = function(...d) { + return new.target === void 0 ? ne(e, void 0, d) : mr(e, d, new.target); + } : l = function(...d) { if (new.target === void 0) throw v( "secure mode Calling %SharedDate% constructor as a function throws" @@ -2054,8 +2148,8 @@ function $a(t = "safe") { throw v( "secure mode Calling new %SharedDate%() with no arguments throws" ); - return lr(e, d, new.target); - }, F(u, { + return mr(e, d, new.target); + }, F(l, { length: { value: 7 }, prototype: { value: r, @@ -2075,9 +2169,9 @@ function $a(t = "safe") { enumerable: !1, configurable: !0 } - }), u; - }, s = a({ powers: "original" }), i = a({ powers: "none" }); - return F(s, { + }), l; + }, a = o({ powers: "original" }), i = o({ powers: "none" }); + return F(a, { now: { value: e.now, writable: !0, @@ -2094,15 +2188,15 @@ function $a(t = "safe") { }), F(r, { constructor: { value: i } }), { - "%InitialDate%": s, + "%InitialDate%": a, "%SharedDate%": i }; } -function Na(t = "safe") { +function Xa(t = "safe") { if (t !== "safe" && t !== "unsafe") throw v(`unrecognized mathTaming ${t}`); - const e = As, r = e, { random: n, ...a } = Je(e), i = H(lo, { - ...a, + const e = qs, r = e, { random: n, ...o } = Ze(e), i = Z(bn, { + ...o, random: { value: { /** @@ -2123,34 +2217,42 @@ function Na(t = "safe") { "%SharedMath%": i }; } -function Oa(t = "safe") { +function Qa(t = "safe") { if (t !== "safe" && t !== "unsafe") throw v(`unrecognized regExpTaming ${t}`); - const e = Be.prototype, r = (s = {}) => { + const e = We.prototype, r = (a = {}) => { const i = function(...l) { - return new.target === void 0 ? Be(...l) : lr(Be, l, new.target); - }, c = de(Be, Cn); - if (!c) - throw v("no RegExp[Symbol.species] descriptor"); - return F(i, { + return new.target === void 0 ? We(...l) : mr(We, l, new.target); + }; + if (F(i, { length: { value: 2 }, prototype: { value: e, writable: !1, enumerable: !1, configurable: !1 - }, - [Cn]: c - }), i; - }, n = r(), a = r(); + } + }), Vr) { + const c = J( + We, + Vr + ); + if (!c) + throw v("no RegExp[Symbol.species] descriptor"); + F(i, { + [Vr]: c + }); + } + return i; + }, n = r(), o = r(); return t !== "unsafe" && delete e.compile, F(e, { - constructor: { value: a } + constructor: { value: o } }), { "%InitialRegExp%": n, - "%SharedRegExp%": a + "%SharedRegExp%": o }; } -const Ra = { +const ei = { "%ObjectPrototype%": { toString: !0 }, @@ -2167,9 +2269,9 @@ const Ra = { // https://github.com/tc39/proposal-iterator-helpers constructor: !0, // https://github.com/tc39/proposal-iterator-helpers - [He]: !0 + [qe]: !0 } -}, Zo = { +}, as = { "%ObjectPrototype%": { toString: !0, valueOf: !0 @@ -2180,7 +2282,7 @@ const Ra = { // set by "Google Analytics" concat: !0, // set by mobx generated code (old TS compiler?) - [Yt]: !0 + [rr]: !0 // set by mobx generated code (old TS compiler?) }, // Function.prototype has no 'prototype' property to enable. @@ -2242,6 +2344,13 @@ const Ra = { 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" @@ -2258,10 +2367,10 @@ const Ra = { // https://github.com/tc39/proposal-iterator-helpers constructor: !0, // https://github.com/tc39/proposal-iterator-helpers - [He]: !0 + [qe]: !0 } -}, Ma = { - ...Zo, +}, ti = { + ...as, /** * Rollup (as used at least by vega) and webpack * (as used at least by regenerator) both turn exports into assignments @@ -2314,24 +2423,24 @@ const Ra = { */ "%SetPrototype%": "*" }; -function La(t, e, r = []) { - const n = new Pt(r); - function a(l, d, f, m) { - if ("value" in m && m.configurable) { - const { value: p } = m, h = dn(n, f), { get: _, set: w } = de( +function ri(t, e, r = []) { + const n = new Ct(r); + function o(u, d, f, h) { + if ("value" in h && h.configurable) { + const { value: p } = h, m = En(n, f), { get: _, set: S } = J( { get [f]() { return p; }, - set [f](I) { + set [f](T) { if (d === this) throw v( - `Cannot assign to read only property '${ie( + `Cannot assign to read only property '${pe( f - )}' of '${l}'` + )}' of '${u}'` ); - se(this, f) ? this[f] = I : (h && console.error(v(`Override property ${f}`)), L(this, f, { - value: I, + oe(this, f) ? this[f] = T : (m && console.error(v(`Override property ${f}`)), M(this, f, { + value: T, writable: !0, enumerable: !0, configurable: !0 @@ -2340,63 +2449,63 @@ function La(t, e, r = []) { }, f ); - L(_, "originalValue", { + M(_, "originalValue", { value: p, writable: !1, enumerable: !1, configurable: !1 - }), L(d, f, { + }), M(d, f, { get: _, - set: w, - enumerable: m.enumerable, - configurable: m.configurable + set: S, + enumerable: h.enumerable, + configurable: h.configurable }); } } - function s(l, d, f) { - const m = de(d, f); - m && a(l, d, f, m); + function a(u, d, f) { + const h = J(d, f); + h && o(u, d, f, h); } - function i(l, d) { - const f = Je(d); - f && st(it(f), (m) => a(l, d, m, f[m])); + function i(u, d) { + const f = Ze(d); + f && ut(De(f), (h) => o(u, d, h, f[h])); } - function c(l, d, f) { - for (const m of it(f)) { - const p = de(d, m); + function c(u, d, f) { + for (const h of De(f)) { + const p = J(d, h); if (!p || p.get || p.set) continue; - const h = `${l}.${ie(m)}`, _ = f[m]; + const m = `${u}.${pe(h)}`, _ = f[h]; if (_ === !0) - s(h, d, m); + a(m, d, h); else if (_ === "*") - i(h, p.value); - else if (We(_)) - c(h, p.value, _); + i(m, p.value); + else if (Ye(_)) + c(m, p.value, _); else - throw v(`Unexpected override enablement plan ${h}`); + throw v(`Unexpected override enablement plan ${m}`); } } - let u; + let l; switch (e) { case "min": { - u = Ra; + l = ei; break; } case "moderate": { - u = Zo; + l = as; break; } case "severe": { - u = Ma; + l = ti; break; } default: throw v(`unrecognized overrideTaming ${e}`); } - c("root", t, u); + c("root", t, l); } -const { Fail: Xr, quote: pr } = Z, Fa = /^(\w*[a-z])Locale([A-Z]\w*)$/, zo = { +const { Fail: cn, quote: Sr } = z, ni = /^(\w*[a-z])Locale([A-Z]\w*)$/, is = { // See https://tc39.es/ecma262/#sec-string.prototype.localecompare localeCompare(t) { if (this === null || this === void 0) @@ -2404,47 +2513,47 @@ const { Fail: Xr, quote: pr } = Z, Fa = /^(\w*[a-z])Locale([A-Z]\w*)$/, zo = { 'Cannot localeCompare with null or undefined "this" value' ); const e = `${this}`, r = `${t}`; - return e < r ? -1 : e > r ? 1 : (e === r || Xr`expected ${pr(e)} and ${pr(r)} to compare`, 0); + return e < r ? -1 : e > r ? 1 : (e === r || cn`expected ${Sr(e)} and ${Sr(r)} to compare`, 0); }, toString() { return `${this}`; } -}, Da = zo.localeCompare, Ua = zo.toString; -function ja(t, e = "safe") { +}, oi = is.localeCompare, si = is.toString; +function ai(t, e = "safe") { if (e !== "safe" && e !== "unsafe") throw v(`unrecognized localeTaming ${e}`); if (e !== "unsafe") { - L(ie.prototype, "localeCompare", { - value: Da + M(pe.prototype, "localeCompare", { + value: oi }); - for (const r of Mt(t)) { + for (const r of Dt(t)) { const n = t[r]; - if (We(n)) - for (const a of Mt(n)) { - const s = pn(Fa, a); - if (s) { - typeof n[a] == "function" || Xr`expected ${pr(a)} to be a function`; - const i = `${s[1]}${s[2]}`, c = n[i]; - typeof c == "function" || Xr`function ${pr(i)} not found`, L(n, a, { value: c }); + if (Ye(n)) + for (const o of Dt(n)) { + const a = kn(ni, o); + if (a) { + typeof n[o] == "function" || cn`expected ${Sr(o)} to be a function`; + const i = `${a[1]}${a[2]}`, c = n[i]; + typeof c == "function" || cn`function ${Sr(i)} not found`, M(n, o, { value: c }); } } } - L(io.prototype, "toLocaleString", { - value: Ua + M(So.prototype, "toLocaleString", { + value: si }); } } -const Za = (t) => ({ +const ii = (t) => ({ eval(r) { return typeof r != "string" ? r : t(r); } -}).eval, { Fail: Zn } = Z, za = (t) => { +}).eval, { Fail: to } = z, ci = (t) => { const e = function(n) { - const a = `${Hr(arguments) || ""}`, s = `${At(arguments, ",")}`; - new ve(s, ""), new ve(a); - const i = `(function anonymous(${s} + const o = `${gr(arguments) || ""}`, a = `${Rt(arguments, ",")}`; + new ve(a, ""), new ve(o); + const i = `(function anonymous(${a} ) { -${a} +${o} })`; return t(i); }; @@ -2457,14 +2566,14 @@ ${a} enumerable: !1, configurable: !1 } - }), B(ve) === ve.prototype || Zn`Function prototype is the same accross compartments`, B(e) === ve.prototype || Zn`Function constructor prototype is the same accross compartments`, e; -}, Ga = (t) => { - L( + }), j(ve) === ve.prototype || to`Function prototype is the same accross compartments`, j(e) === ve.prototype || to`Function constructor prototype is the same accross compartments`, e; +}, li = (t) => { + M( t, - Rs, - g( - Pr(H(null), { - set: g(() => { + Qs, + y( + $r(Z(null), { + set: y(() => { throw v( "Cannot set Symbol.unscopables of a Compartment's globalThis" ); @@ -2474,55 +2583,55 @@ ${a} }) ) ); -}, Go = (t) => { - for (const [e, r] of te(Lo)) - L(t, e, { +}, cs = (t) => { + for (const [e, r] of re(es)) + M(t, e, { value: r, writable: !1, enumerable: !1, configurable: !1 }); -}, Bo = (t, { +}, ls = (t, { intrinsics: e, newGlobalPropertyNames: r, makeCompartmentConstructor: n, - markVirtualizedNativeFunction: a + markVirtualizedNativeFunction: o }) => { - for (const [i, c] of te(Fo)) - se(e, c) && L(t, i, { + for (const [i, c] of re(ts)) + oe(e, c) && M(t, i, { value: e[c], writable: !0, enumerable: !1, configurable: !0 }); - for (const [i, c] of te(r)) - se(e, c) && L(t, i, { + for (const [i, c] of re(r)) + oe(e, c) && M(t, i, { value: e[c], writable: !0, enumerable: !1, configurable: !0 }); - const s = { + const a = { globalThis: t }; - s.Compartment = g( + a.Compartment = y( n( n, e, - a + o ) ); - for (const [i, c] of te(s)) - L(t, i, { + for (const [i, c] of re(a)) + M(t, i, { value: c, writable: !0, enumerable: !1, configurable: !0 - }), typeof c == "function" && a(c); -}, Qr = (t, e, r) => { + }), typeof c == "function" && o(c); +}, ln = (t, e, r) => { { - const n = g(Za(e)); - r(n), L(t, "eval", { + const n = y(ii(e)); + r(n), M(t, "eval", { value: n, writable: !0, enumerable: !1, @@ -2530,29 +2639,29 @@ ${a} }); } { - const n = g(za(e)); - r(n), L(t, "Function", { + const n = y(ci(e)); + r(n), M(t, "Function", { value: n, writable: !0, enumerable: !1, configurable: !0 }); } -}, { Fail: Ba, quote: Ho } = Z, Vo = new xr( - gn, - g({ +}, { Fail: ui, quote: us } = z, ds = new Cr( + In, + y({ get(t, e) { - Ba`Please report unexpected scope handler trap: ${Ho(ie(e))}`; + ui`Please report unexpected scope handler trap: ${us(pe(e))}`; } }) -), Ha = { +), di = { get(t, e) { }, set(t, e, r) { - throw ot(`${ie(e)} is not defined`); + throw lt(`${pe(e)} is not defined`); }, has(t, e) { - return e in x; + return e in k; }, // note: this is likely a bug of safari // https://bugs.webkit.org/show_bug.cgi?id=195534 @@ -2562,7 +2671,7 @@ ${a} // 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 = Ho(ie(e)); + const r = us(pe(e)); console.warn( `getOwnPropertyDescriptor trap on scopeTerminatorHandler for ${r}`, v().stack @@ -2573,43 +2682,43 @@ ${a} ownKeys(t) { return []; } -}, Wo = g( - H( - Vo, - Je(Ha) +}, fs = y( + Z( + ds, + Ze(di) ) -), Va = new xr( - gn, - Wo -), qo = (t) => { +), fi = new Cr( + In, + fs +), ps = (t) => { const e = { // inherit scopeTerminator behavior - ...Wo, + ...fs, // Redirect set properties to the globalObject. - set(a, s, i) { - return yo(t, s, i); + 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(a, s) { + has(o, a) { return !0; } - }, r = g( - H( - Vo, - Je(e) + }, r = y( + Z( + ds, + Ze(e) ) ); - return new xr( - gn, + return new Cr( + In, r ); }; -g(qo); -const { Fail: Wa } = Z, qa = () => { - const t = H(null), e = g({ +y(ps); +const { Fail: pi } = z, hi = () => { + const t = Z(null), e = y({ eval: { get() { - return delete t.eval, Eo; + return delete t.eval, jo; }, enumerable: !1, configurable: !0 @@ -2618,78 +2727,78 @@ const { Fail: Wa } = Z, qa = () => { evalScope: t, allowNextEvalToBeUnsafe() { const { revoked: n } = r; - n !== null && Wa`a handler did not reset allowNextEvalToBeUnsafe ${n.err}`, F(t, e); + n !== null && pi`a handler did not reset allowNextEvalToBeUnsafe ${n.err}`, F(t, e); }, /** @type {null | { err: any }} */ revoked: null }; return r; -}, zn = "\\s*[@#]\\s*([a-zA-Z][a-zA-Z0-9]*)\\s*=\\s*([^\\s\\*]*)", Ka = new Be( - `(?:\\s*//${zn}|/\\*${zn}\\s*\\*/)\\s*$` -), bn = (t) => { +}, ro = "\\s*[@#]\\s*([a-zA-Z][a-zA-Z0-9]*)\\s*=\\s*([^\\s\\*]*)", mi = new We( + `(?:\\s*//${ro}|/\\*${ro}\\s*\\*/)\\s*$` +), Nn = (t) => { let e = ""; for (; t.length > 0; ) { - const r = pn(Ka, t); + const r = kn(mi, t); if (r === null) break; - t = mn(t, 0, t.length - r[0].length), r[3] === "sourceURL" ? e = r[4] : r[1] === "sourceURL" && (e = r[2]); + t = Pn(t, 0, t.length - r[0].length), r[3] === "sourceURL" ? e = r[4] : r[1] === "sourceURL" && (e = r[2]); } return e; }; -function wn(t, e) { - const r = Qs(t, e); +function Rn(t, e) { + const r = va(t, e); if (r < 0) return -1; const n = t[r] === ` ` ? 1 : 0; - return wo(mn(t, 0, r), ` + return Tn(Pn(t, 0, r), ` `).length + n; } -const Ko = new Be("(?:)", "g"), Yo = (t) => { - const e = wn(t, Ko); +const hs = new We("(?:)", "g"), ms = (t) => { + const e = Rn(t, hs); if (e < 0) return t; - const r = bn(t); - throw Kt( + const r = Nn(t); + throw tr( `Possible HTML comment rejected at ${r}:${e}. (SES_HTML_COMMENT_REJECTED)` ); -}, Jo = (t) => ur(t, Ko, (r) => r[0] === "<" ? "< ! --" : "-- >"), Xo = new Be( +}, gs = (t) => vr(t, hs, (r) => r[0] === "<" ? "< ! --" : "-- >"), ys = new We( "(^|[^.]|\\.\\.\\.)\\bimport(\\s*(?:\\(|/[/*]))", "g" -), Qo = (t) => { - const e = wn(t, Xo); +), vs = (t) => { + const e = Rn(t, ys); if (e < 0) return t; - const r = bn(t); - throw Kt( + const r = Nn(t); + throw tr( `Possible import expression rejected at ${r}:${e}. (SES_IMPORT_REJECTED)` ); -}, es = (t) => ur(t, Xo, (r, n, a) => `${n}__import__${a}`), Ya = new Be( +}, _s = (t) => vr(t, ys, (r, n, o) => `${n}__import__${o}`), gi = new We( "(^|[^.])\\beval(\\s*\\()", "g" -), ts = (t) => { - const e = wn(t, Ya); +), bs = (t) => { + const e = Rn(t, gi); if (e < 0) return t; - const r = bn(t); - throw Kt( + const r = Nn(t); + throw tr( `Possible direct eval expression rejected at ${r}:${e}. (SES_EVAL_REJECTED)` ); -}, rs = (t) => (t = Yo(t), t = Qo(t), t), ns = (t, e) => { +}, ws = (t) => (t = ms(t), t = vs(t), t), Ss = (t, e) => { for (const r of e) t = r(t); return t; }; -g({ - rejectHtmlComments: g(Yo), - evadeHtmlCommentTest: g(Jo), - rejectImportExpressions: g(Qo), - evadeImportExpressionTest: g(es), - rejectSomeDirectEvalExpressions: g(ts), - mandatoryTransforms: g(rs), - applyTransforms: g(ns) +y({ + rejectHtmlComments: y(ms), + evadeHtmlCommentTest: y(gs), + rejectImportExpressions: y(vs), + evadeImportExpressionTest: y(_s), + rejectSomeDirectEvalExpressions: y(bs), + mandatoryTransforms: y(ws), + applyTransforms: y(Ss) }); -const Ja = [ +const yi = [ // 11.6.2.1 Keywords "await", "break", @@ -2744,9 +2853,9 @@ const Ja = [ "false", "this", "arguments" -], Xa = /^[a-zA-Z_$][\w$]*$/, Gn = (t) => t !== "eval" && !Ar(Ja, t) && fn(Xa, t); -function Bn(t, e) { - const r = de(t, e); +], vi = /^[a-zA-Z_$][\w$]*$/, no = (t) => t !== "eval" && !Mr(yi, t) && xn(vi, t); +function oo(t, e) { + const r = J(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 @@ -2760,45 +2869,45 @@ function Bn(t, e) { // 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. - se(r, "value"); + oe(r, "value"); } -const Qa = (t, e = {}) => { - const r = Mt(t), n = Mt(e), a = Ve( +const _i = (t, e = {}) => { + const r = Dt(t), n = Dt(e), o = Ke( n, - (i) => Gn(i) && Bn(e, i) + (i) => no(i) && oo(e, i) ); return { - globalObjectConstants: Ve( + globalObjectConstants: Ke( r, (i) => ( // Can't define a constant: it would prevent a // lookup on the endowments. - !Ar(n, i) && Gn(i) && Bn(t, i) + !Mr(n, i) && no(i) && oo(t, i) ) ), - moduleLexicalConstants: a + moduleLexicalConstants: o }; }; -function Hn(t, e) { - return t.length === 0 ? "" : `const {${At(t, ",")}} = this.${e};`; +function so(t, e) { + return t.length === 0 ? "" : `const {${Rt(t, ",")}} = this.${e};`; } -const ei = (t) => { - const { globalObjectConstants: e, moduleLexicalConstants: r } = Qa( +const bi = (t) => { + const { globalObjectConstants: e, moduleLexicalConstants: r } = _i( t.globalObject, t.moduleLexicals - ), n = Hn( + ), n = so( e, "globalObject" - ), a = Hn( + ), o = so( r, "moduleLexicals" - ), s = ve(` + ), a = ve(` with (this.scopeTerminator) { with (this.globalObject) { with (this.moduleLexicals) { with (this.evalScope) { ${n} - ${a} + ${o} return function() { 'use strict'; return eval(arguments[0]); @@ -2808,71 +2917,71 @@ const ei = (t) => { } } `); - return oe(s, t, []); -}, { Fail: ti } = Z, Sn = ({ + return ne(a, t, []); +}, { Fail: wi } = z, On = ({ globalObject: t, moduleLexicals: e = {}, globalTransforms: r = [], sloppyGlobalsMode: n = !1 }) => { - const a = n ? qo(t) : Va, s = qa(), { evalScope: i } = s, c = g({ + const o = n ? ps(t) : fi, a = hi(), { evalScope: i } = a, c = y({ evalScope: i, moduleLexicals: e, globalObject: t, - scopeTerminator: a + scopeTerminator: o }); - let u; - const l = () => { - u || (u = ei(c)); + let l; + const u = () => { + l || (l = bi(c)); }; - return { safeEvaluate: (f, m) => { - const { localTransforms: p = [] } = m || {}; - l(), f = ns(f, [ + return { safeEvaluate: (f, h) => { + const { localTransforms: p = [] } = h || {}; + u(), f = Ss(f, [ ...p, ...r, - rs + ws ]); - let h; + let m; try { - return s.allowNextEvalToBeUnsafe(), oe(u, t, [f]); + return a.allowNextEvalToBeUnsafe(), ne(l, t, [f]); } catch (_) { - throw h = _, _; + throw m = _, _; } finally { const _ = "eval" in i; - delete i.eval, _ && (s.revoked = { err: h }, ti`handler did not reset allowNextEvalToBeUnsafe ${h}`); + delete i.eval, _ && (a.revoked = { err: m }, wi`handler did not reset allowNextEvalToBeUnsafe ${m}`); } } }; -}, ri = ") { [native code] }"; -let jr; -const os = () => { - if (jr === void 0) { - const t = new kt(); - L(un, "toString", { +}, Si = ") { [native code] }"; +let Yr; +const Es = () => { + if (Yr === void 0) { + const t = new $t(); + M(wn, "toString", { value: { toString() { - const r = ra(this); - return bo(r, ri) || !Xt(t, this) ? r : `function ${this.name}() { [native code] }`; + const r = wa(this); + return Mo(r, Si) || !or(t, this) ? r : `function ${this.name}() { [native code] }`; } }.toString - }), jr = g( - (r) => Nr(t, r) + }), Yr = y( + (r) => Fr(t, r) ); } - return jr; + return Yr; }; -function ni(t = "safe") { +function Ei(t = "safe") { if (t !== "safe" && t !== "unsafe") throw v(`unrecognized domainTaming ${t}`); if (t === "unsafe") return; - const e = x.process || void 0; + const e = k.process || void 0; if (typeof e == "object") { - const r = de(e, "domain"); + const r = J(e, "domain"); if (r !== void 0 && r.get !== void 0) throw v( "SES failed to lockdown, Node.js domains have been initialized (SES_NO_DOMAINS)" ); - L(e, "domain", { + M(e, "domain", { value: null, configurable: !1, writable: !1, @@ -2880,7 +2989,7 @@ function ni(t = "safe") { }); } } -const ss = g([ +const Mn = y([ ["debug", "debug"], // (fmt?, ...args) verbose level on Chrome ["log", "log"], @@ -2894,12 +3003,12 @@ const ss = g([ ["trace", "log"], // (fmt?, ...args) ["dirxml", "log"], - // (fmt?, ...args) + // (fmt?, ...args) but TS typed (...data) ["group", "log"], - // (fmt?, ...args) + // (fmt?, ...args) but TS typed (...label) ["groupCollapsed", "log"] - // (fmt?, ...args) -]), as = g([ + // (fmt?, ...args) but TS typed (...label) +]), Ln = y([ ["assert", "error"], // (value, fmt?, ...args) ["timeLog", "log"], @@ -2931,209 +3040,243 @@ const ss = g([ // (label?) ["timeStamp", void 0] // (label?) -]), is = g([ - ...ss, - ...as -]), oi = (t, { shouldResetForDebugging: e = !1 } = {}) => { +]), xs = y([ + ...Mn, + ...Ln +]), xi = (t, { shouldResetForDebugging: e = !1 } = {}) => { e && t.resetErrorTagNum(); let r = []; - const n = Tt( - fe(is, ([i, c]) => { - const u = (...l) => { - ae(r, [i, ...l]); + const n = mt( + se(xs, ([i, c]) => { + const l = (...u) => { + X(r, [i, ...u]); }; - return L(u, "name", { value: i }), [i, g(u)]; + return M(l, "name", { value: i }), [i, y(l)]; }) ); - g(n); - const a = () => { - const i = g(r); + y(n); + const o = () => { + const i = y(r); return r = [], i; }; - return g(a), g({ loggingConsole: ( + return y(o), y({ loggingConsole: ( /** @type {VirtualConsole} */ n - ), takeLog: a }); + ), takeLog: o }); }; -g(oi); -const $t = { +y(xi); +const it = { NOTE: "ERROR_NOTE:", - MESSAGE: "ERROR_MESSAGE:" + MESSAGE: "ERROR_MESSAGE:", + CAUSE: "cause:", + ERRORS: "errors:" }; -g($t); -const cs = (t, e) => { +y(it); +const Fn = (t, e) => { if (!t) return; - const { getStackString: r, tagError: n, takeMessageLogArgs: a, takeNoteLogArgsArray: s } = e, i = (w, I) => fe(w, (T) => vn(T) ? (ae(I, T), `(${n(T)})`) : T), c = (w, I, $, T, D) => { - const j = n(I), q = $ === $t.MESSAGE ? `${j}:` : `${j} ${$}`, K = i(T, D); - t[w](q, ...K); - }, u = (w, I, $ = void 0) => { - if (I.length === 0) + const { getStackString: r, tagError: n, takeMessageLogArgs: o, takeNoteLogArgsArray: a } = e, i = (S, T) => se(S, (x) => Dr(x) ? (X(T, x), `(${n(x)})`) : x), c = (S, T, N, x, D) => { + const G = n(T), B = N === it.MESSAGE ? `${G}:` : `${G} ${N}`, K = i(x, D); + t[S](B, ...K); + }, l = (S, T, N = void 0) => { + if (T.length === 0) return; - if (I.length === 1 && $ === void 0) { - f(w, I[0]); + if (T.length === 1 && N === void 0) { + f(S, T[0]); return; } - let T; - I.length === 1 ? T = "Nested error" : T = `Nested ${I.length} errors`, $ !== void 0 && (T = `${T} under ${$}`), t.group(T); + let x; + T.length === 1 ? x = "Nested error" : x = `Nested ${T.length} errors`, N !== void 0 && (x = `${x} under ${N}`), t.group(x); try { - for (const D of I) - f(w, D); + for (const D of T) + f(S, D); } finally { t.groupEnd(); } - }, l = new kt(), d = (w) => (I, $) => { - const T = []; - c(w, I, $t.NOTE, $, T), u(w, T, n(I)); - }, f = (w, I) => { - if (Xt(l, I)) + }, u = new $t(), d = (S) => (T, N) => { + const x = []; + c(S, T, it.NOTE, N, x), l(S, x, n(T)); + }, f = (S, T) => { + if (or(u, T)) return; - const $ = n(I); - Nr(l, I); - const T = [], D = a(I), j = s( - I, - d(w) + const N = n(T); + Fr(u, T); + const x = [], D = o(T), G = a( + T, + d(S) ); - D === void 0 ? t[w](`${$}:`, I.message) : c( - w, - I, - $t.MESSAGE, + D === void 0 ? t[S](`${N}:`, T.message) : c( + S, + T, + it.MESSAGE, D, - T + x ); - let q = r(I); - typeof q == "string" && q.length >= 1 && !bo(q, ` -`) && (q += ` -`), t[w](q); - for (const K of j) - c(w, I, $t.NOTE, K, T); - u(w, T, $); - }, m = fe(ss, ([w, I]) => { - const $ = (...T) => { - const D = [], j = i(T, D); - t[w](...j), u(w, D); + let B = r(T); + typeof B == "string" && B.length >= 1 && !Mo(B, ` +`) && (B += ` +`), t[S](B), T.cause && c(S, T, it.CAUSE, [T.cause], x), T.errors && c(S, T, it.ERRORS, T.errors, x); + for (const K of G) + c(S, T, it.NOTE, K, x); + l(S, x, N); + }, h = se(Mn, ([S, T]) => { + const N = (...x) => { + const D = [], G = i(x, D); + t[S](...G), l(S, D); }; - return L($, "name", { value: w }), [w, g($)]; - }), p = Ve( - as, - ([w, I]) => w in t - ), h = fe(p, ([w, I]) => { - const $ = (...T) => { - t[w](...T); + return M(N, "name", { value: S }), [S, y(N)]; + }), p = Ke( + Ln, + ([S, T]) => S in t + ), m = se(p, ([S, T]) => { + const N = (...x) => { + t[S](...x); }; - return L($, "name", { value: w }), [w, g($)]; - }), _ = Tt([...m, ...h]); + return M(N, "name", { value: S }), [S, y(N)]; + }), _ = mt([...h, ...m]); return ( /** @type {VirtualConsole} */ - g(_) + y(_) ); }; -g(cs); -const si = (t, e, r = void 0) => { - const n = Ve( - is, +y(Fn); +const ki = (t, e, r) => { + const [n, ...o] = Tn(t, e), a = Ro(o, (i) => [e, ...r, i]); + return ["", n, ...a]; +}, ks = (t) => y((r) => { + const n = [], o = (...l) => (n.length > 0 && (l = Ro( + l, + (u) => typeof u == "string" && Lo(u, ` +`) ? ki(u, ` +`, n) : [u] + ), l = [...n, ...l]), r(...l)), a = (l, u) => ({ [l]: (...d) => u(...d) })[l], i = mt([ + ...se(Mn, ([l]) => [ + l, + a(l, o) + ]), + ...se(Ln, ([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), X(n, " "); + })); + return i.groupEnd && (i.groupEnd = a("groupEnd", (...l) => { + gr(n); + })), harden(i), Fn( + /** @type {VirtualConsole} */ + i, + t + ); +}); +y(ks); +const Pi = (t, e, r = void 0) => { + const n = Ke( + xs, ([i, c]) => i in t - ), a = fe(n, ([i, c]) => [i, g((...l) => { - (c === void 0 || e.canLog(c)) && t[i](...l); - })]), s = Tt(a); + ), o = se(n, ([i, c]) => [i, y((...u) => { + (c === void 0 || e.canLog(c)) && t[i](...u); + })]), a = mt(o); return ( /** @type {VirtualConsole} */ - g(s) + y(a) ); }; -g(si); -const Vn = (t) => { - if (bt === void 0) +y(Pi); +const ao = (t) => { + if (kt === void 0) return; let e = 0; - const r = new Ce(), n = (d) => { - Vs(r, d); - }, a = new Te(), s = (d) => { - if (Cr(r, d)) { - const f = De(r, d); + const r = new Pe(), n = (d) => { + fa(r, d); + }, o = new Me(), a = (d) => { + if (Lr(r, d)) { + const f = Ue(r, d); n(d), t(f); } - }, i = new bt(s); + }, i = new kt(a); return { rejectionHandledHandler: (d) => { - const f = M(a, d); + const f = L(o, d); n(f); }, unhandledRejectionHandler: (d, f) => { e += 1; - const m = e; - $e(r, m, d), ee(a, f, m), oa(i, f, m, f); + const h = e; + $e(r, h, d), ie(o, f, h), Ea(i, f, h, f); }, processTerminationHandler: () => { - for (const [d, f] of Ws(r)) + for (const [d, f] of pa(r)) n(d), t(f); } }; -}, Zr = (t) => { +}, Jr = (t) => { throw v(t); -}, Wn = (t, e) => g((...r) => oe(t, e, r)), ai = (t = "safe", e = "platform", r = "report", n = void 0) => { - t === "safe" || t === "unsafe" || Zr(`unrecognized consoleTaming ${t}`); - let a; - n === void 0 ? a = Yr : a = { - ...Yr, +}, io = (t, e) => y((...r) => ne(t, e, r)), Ti = (t = "safe", e = "platform", r = "report", n = void 0) => { + t === "safe" || t === "unsafe" || Jr(`unrecognized consoleTaming ${t}`); + let o; + n === void 0 ? o = br : o = { + ...br, getStackString: n }; - const s = ( + const a = ( /** @type {VirtualConsole} */ // eslint-disable-next-line no-nested-ternary - typeof x.console < "u" ? x.console : typeof x.print == "function" ? ( + typeof k.console < "u" ? k.console : typeof k.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 - ((l) => g({ debug: l, log: l, info: l, warn: l, error: l }))( + ((u) => y({ debug: u, log: u, info: u, warn: u, error: u }))( // eslint-disable-next-line no-undef - Wn(x.print) + io(k.print) ) ) : void 0 ); - if (s && s.log) - for (const l of ["warn", "error"]) - s[l] || L(s, l, { - value: Wn(s.log, s) + if (a && a.log) + for (const u of ["warn", "error"]) + a[u] || M(a, u, { + value: io(a.log, a) }); const i = ( /** @type {VirtualConsole} */ - t === "unsafe" ? s : cs(s, a) - ), c = x.process || void 0; + t === "unsafe" ? a : Fn(a, o) + ), c = k.process || void 0; if (e !== "none" && typeof c == "object" && typeof c.on == "function") { - let l; + let u; if (e === "platform" || e === "exit") { const { exit: d } = c; - typeof d == "function" || Zr("missing process.exit"), l = () => d(c.exitCode || -1); + typeof d == "function" || Jr("missing process.exit"), u = () => d(c.exitCode || -1); } else - e === "abort" && (l = c.abort, typeof l == "function" || Zr("missing process.abort")); + e === "abort" && (u = c.abort, typeof u == "function" || Jr("missing process.abort")); c.on("uncaughtException", (d) => { - i.error(d), l && l(); + i.error(d), u && u(); }); } if (r !== "none" && typeof c == "object" && typeof c.on == "function") { - const d = Vn((f) => { + const d = ao((f) => { i.error("SES_UNHANDLED_REJECTION:", f); }); d && (c.on("unhandledRejection", d.unhandledRejectionHandler), c.on("rejectionHandled", d.rejectionHandledHandler), c.on("exit", d.processTerminationHandler)); } - const u = x.window || void 0; - if (e !== "none" && typeof u == "object" && typeof u.addEventListener == "function" && u.addEventListener("error", (l) => { - l.preventDefault(), i.error(l.error), (e === "exit" || e === "abort") && (u.location.href = "about:blank"); - }), r !== "none" && typeof u == "object" && typeof u.addEventListener == "function") { - const d = Vn((f) => { + const l = k.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 = ao((f) => { i.error("SES_UNHANDLED_REJECTION:", f); }); - d && (u.addEventListener("unhandledrejection", (f) => { + d && (l.addEventListener("unhandledrejection", (f) => { f.preventDefault(), d.unhandledRejectionHandler(f.reason, f.promise); - }), u.addEventListener("rejectionhandled", (f) => { + }), l.addEventListener("rejectionhandled", (f) => { f.preventDefault(), d.rejectionHandledHandler(f.promise); - }), u.addEventListener("beforeunload", (f) => { + }), l.addEventListener("beforeunload", (f) => { d.processTerminationHandler(); })); } return { console: i }; -}, ii = [ +}, Ai = [ // suppress 'getThis' definitely "getTypeName", // suppress 'getFunction' definitely @@ -3156,88 +3299,88 @@ const Vn = (t) => { "getScriptNameOrSourceURL", "toString" // TODO replace to use only whitelisted info -], ci = (t) => { - const r = Tt(fe(ii, (n) => { - const a = t[n]; - return [n, () => oe(a, t, [])]; +], Ii = (t) => { + const r = mt(se(Ai, (n) => { + const o = t[n]; + return [n, () => ne(o, t, [])]; })); - return H(r, {}); -}, li = (t) => fe(t, ci), ui = /\/node_modules\//, di = /^(?:node:)?internal\//, fi = /\/packages\/ses\/src\/error\/assert.js$/, pi = /\/packages\/eventual-send\/src\//, mi = [ - ui, - di, - fi, - pi -], hi = (t) => { + return Z(r, {}); +}, Ci = (t) => se(t, Ii), $i = /\/node_modules\//, Ni = /^(?:node:)?internal\//, Ri = /\/packages\/ses\/src\/error\/assert.js$/, Oi = /\/packages\/eventual-send\/src\//, Mi = [ + $i, + Ni, + Ri, + Oi +], Li = (t) => { if (!t) return !0; - for (const e of mi) - if (fn(e, t)) + for (const e of Mi) + if (xn(e, t)) return !1; return !0; -}, yi = /^((?:.*[( ])?)[:/\w_-]*\/\.\.\.\/(.+)$/, gi = /^((?:.*[( ])?)[:/\w_-]*\/(packages\/.+)$/, vi = [ - yi, - gi -], _i = (t) => { - for (const e of vi) { - const r = pn(e, t); +}, Fi = /^((?:.*[( ])?)[:/\w_-]*\/\.\.\.\/(.+)$/, Di = /^((?:.*[( ])?)[:/\w_-]*\/(packages\/.+)$/, Ui = [ + Fi, + Di +], ji = (t) => { + for (const e of Ui) { + const r = kn(e, t); if (r) - return At(Gs(r, 1), ""); + return Rt(la(r, 1), ""); } return t; -}, bi = (t, e, r, n) => { - const a = t.captureStackTrace, s = (p) => n === "verbose" ? !0 : hi(p.getFileName()), i = (p) => { - let h = `${p}`; - return n === "concise" && (h = _i(h)), ` - at ${h}`; - }, c = (p, h) => At( - fe(Ve(h, s), i), +}, Zi = (t, e, r, n) => { + const o = t.captureStackTrace, a = (p) => n === "verbose" ? !0 : Li(p.getFileName()), i = (p) => { + let m = `${p}`; + return n === "concise" && (m = ji(m)), ` + at ${m}`; + }, c = (p, m) => Rt( + se(Ke(m, a), i), "" - ), u = new Te(), l = { + ), l = new Me(), 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, h = l.captureStackTrace) { - if (typeof a == "function") { - oe(a, t, [p, h]); + captureStackTrace(p, m = u.captureStackTrace) { + if (typeof o == "function") { + ne(o, t, [p, m]); return; } - yo(p, "stack", ""); + 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 h = M(u, p); - if (h === void 0 && (p.stack, h = M(u, p), h || (h = { stackString: "" }, ee(u, p, h))), h.stackString !== void 0) - return h.stackString; - const _ = c(p, h.callSites); - return ee(u, p, { stackString: _ }), _; + let m = L(l, p); + if (m === void 0 && (p.stack, m = L(l, p), m || (m = { stackString: "" }, ie(l, p, m))), m.stackString !== void 0) + return m.stackString; + const _ = c(p, m.callSites); + return ie(l, p, { stackString: _ }), _; }, - prepareStackTrace(p, h) { + prepareStackTrace(p, m) { if (r === "unsafe") { - const _ = c(p, h); - return ee(u, p, { stackString: _ }), `${p}${_}`; + const _ = c(p, m); + return ie(l, p, { stackString: _ }), `${p}${_}`; } else - return ee(u, p, { callSites: h }), ""; + return ie(l, p, { callSites: m }), ""; } - }, d = l.prepareStackTrace; + }, d = u.prepareStackTrace; t.prepareStackTrace = d; - const f = new kt([d]), m = (p) => { - if (Xt(f, p)) + const f = new $t([d]), h = (p) => { + if (or(f, p)) return p; - const h = { - prepareStackTrace(_, w) { - return ee(u, _, { callSites: w }), p(_, li(w)); + const m = { + prepareStackTrace(_, S) { + return ie(l, _, { callSites: S }), p(_, Ci(S)); } }; - return Nr(f, h.prepareStackTrace), h.prepareStackTrace; + return Fr(f, m.prepareStackTrace), m.prepareStackTrace; }; return F(e, { captureStackTrace: { - value: l.captureStackTrace, + value: u.captureStackTrace, writable: !0, enumerable: !1, configurable: !0 @@ -3248,29 +3391,29 @@ const Vn = (t) => { }, set(p) { if (typeof p == "function") { - const h = m(p); - t.prepareStackTrace = h; + const m = h(p); + t.prepareStackTrace = m; } else t.prepareStackTrace = d; }, enumerable: !1, configurable: !0 } - }), l.getStackString; -}, qn = de(le.prototype, "stack"), Kn = qn && qn.get, wi = { + }), u.getStackString; +}, co = J(ue.prototype, "stack"), lo = co && co.get, zi = { getStackString(t) { - return typeof Kn == "function" ? oe(Kn, t, []) : "stack" in t ? `${t.stack}` : ""; + return typeof lo == "function" ? ne(lo, t, []) : "stack" in t ? `${t.stack}` : ""; } }; -function Si(t = "safe", e = "concise") { +function Gi(t = "safe", e = "concise") { if (t !== "safe" && t !== "unsafe") throw v(`unrecognized errorTaming ${t}`); if (e !== "concise" && e !== "verbose") throw v(`unrecognized stackFiltering ${e}`); - const r = le.prototype, n = typeof le.captureStackTrace == "function" ? "v8" : "unknown", { captureStackTrace: a } = le, s = (l = {}) => { - const d = function(...m) { + const r = ue.prototype, n = typeof ue.captureStackTrace == "function" ? "v8" : "unknown", { captureStackTrace: o } = ue, a = (u = {}) => { + const d = function(...h) { let p; - return new.target === void 0 ? p = oe(le, this, m) : p = lr(le, m, new.target), n === "v8" && oe(a, le, [p, d]), p; + return new.target === void 0 ? p = ne(ue, this, h) : p = mr(ue, h, new.target), n === "v8" && ne(o, ue, [p, d]), p; }; return F(d, { length: { value: 1 }, @@ -3281,21 +3424,21 @@ function Si(t = "safe", e = "concise") { configurable: !1 } }), d; - }, i = s({ powers: "original" }), c = s({ powers: "none" }); + }, i = a({ powers: "original" }), c = a({ powers: "none" }); F(r, { constructor: { value: c } }); - for (const l of Ea) - uo(l, c); + for (const u of ns) + xo(u, c); F(i, { stackTraceLimit: { get() { - if (typeof le.stackTraceLimit == "number") - return le.stackTraceLimit; + if (typeof ue.stackTraceLimit == "number") + return ue.stackTraceLimit; }, - set(l) { - if (typeof l == "number" && typeof le.stackTraceLimit == "number") { - le.stackTraceLimit = l; + set(u) { + if (typeof u == "number" && typeof ue.stackTraceLimit == "number") { + ue.stackTraceLimit = u; return; } }, @@ -3307,7 +3450,7 @@ function Si(t = "safe", e = "concise") { stackTraceLimit: { get() { }, - set(l) { + set(u) { }, enumerable: !1, configurable: !0 @@ -3317,14 +3460,14 @@ function Si(t = "safe", e = "concise") { get() { return () => ""; }, - set(l) { + set(u) { }, enumerable: !1, configurable: !0 }, captureStackTrace: { - value: (l, d) => { - L(l, "stack", { + value: (u, d) => { + M(u, "stack", { value: "" }); }, @@ -3333,21 +3476,21 @@ function Si(t = "safe", e = "concise") { configurable: !0 } }); - let u = wi.getStackString; - return n === "v8" ? u = bi( - le, + let l = zi.getStackString; + return n === "v8" ? l = Zi( + ue, i, t, e ) : t === "unsafe" ? F(r, { stack: { get() { - return u(this); + return l(this); }, - set(l) { + set(u) { F(this, { stack: { - value: l, + value: u, writable: !0, enumerable: !0, configurable: !0 @@ -3360,10 +3503,10 @@ function Si(t = "safe", e = "concise") { get() { return `${this}`; }, - set(l) { + set(u) { F(this, { stack: { - value: l, + value: u, writable: !0, enumerable: !0, configurable: !0 @@ -3372,244 +3515,307 @@ function Si(t = "safe", e = "concise") { } } }), { - "%InitialGetStackString%": u, + "%InitialGetStackString%": l, "%InitialError%": i, "%SharedError%": c }; } -const { Fail: Ei, details: en, quote: Le } = Z, ls = () => { -}, xi = (t, e) => g({ +const { Fail: Bi, details: un, quote: xe } = z, Hi = () => { +}; +async function Vi(t, e, r) { + const n = t(...e); + let o = yr(n); + for (; !o.done; ) + try { + const a = await o.value; + o = yr(n, a); + } catch (a) { + o = Fo(n, r(a)); + } + return o.value; +} +function Wi(t, e) { + const r = t(...e); + let n = yr(r); + for (; !n.done; ) + try { + n = yr(r, n.value); + } catch (o) { + n = Fo(r, o); + } + return n.value; +} +const qi = (t, e) => y({ compartment: t, specifier: e -}), Pi = (t, e, r) => { - const n = H(null); - for (const a of t) { - const s = e(a, r); - n[a] = s; +}), Ki = (t, e, r) => { + const n = Z(null); + for (const o of t) { + const a = e(o, r); + n[o] = a; } - return g(n); -}, Yn = (t, e, r, n, a, s, i, c, u) => { - const { resolveHook: l, moduleRecords: d } = M( + return y(n); +}, uo = (t, e, r, n, o, a, i, c, l) => { + const { resolveHook: u, moduleRecords: d } = L( t, r - ), f = Pi( - a.imports, - l, + ), f = Ki( + o.imports, + u, n - ), m = g({ + ), h = y({ compartment: r, - staticModuleRecord: a, + staticModuleRecord: o, moduleSpecifier: n, resolvedImports: f, - importMeta: u + importMeta: l }); - for (const p of fo(f)) { - const h = mr( + for (const p of ko(f)) + a(Ut, [ t, e, r, p, - s, + a, i, c - ); - $r( - s, - yn(h, ls, (_) => { - ae(c, _); - }) - ); - } - return $e(d, n, m), m; -}, ki = async (t, e, r, n, a, s, i) => { - const { importHook: c, moduleMap: u, moduleMapHook: l, moduleRecords: d } = M( - t, - r - ); - let f = u[n]; - if (f === void 0 && l !== void 0 && (f = l(n)), typeof f == "string") - Z.fail( - en`Cannot map module ${Le(n)} to ${Le( - f + ]); + return $e(d, n, h), h; +}; +function* Yi(t, e, r, n, o, a, i) { + const { importHook: c, importNowHook: l, moduleMap: u, moduleMapHook: d, moduleRecords: f } = L(t, r); + let h = u[n]; + if (h === void 0 && d !== void 0 && (h = d(n)), typeof h == "string") + z.fail( + un`Cannot map module ${xe(n)} to ${xe( + h )} in parent compartment, not yet implemented`, v ); - else if (f !== void 0) { - const p = M(e, f); - p === void 0 && Z.fail( - en`Cannot map module ${Le( + else if (h !== void 0) { + const m = L(e, h); + m === void 0 && z.fail( + un`Cannot map module ${xe( n )} because the value is not a module exports namespace, or is from another realm`, - ot + lt ); - const h = await mr( + const _ = yield Ut( t, e, - p.compartment, - p.specifier, + m.compartment, + m.specifier, + o, a, - s, i ); - return $e(d, n, h), h; + return $e(f, n, _), _; } - if (Cr(d, n)) - return De(d, n); - const m = await c(n); - if ((m === null || typeof m != "object") && Ei`importHook must return a promise for an object, for module ${Le( + if (Lr(f, n)) + return Ue(f, n); + const p = yield a( + c, + l + )(n); + if ((p === null || typeof p != "object") && Bi`importHook must return a promise for an object, for module ${xe( n - )} in compartment ${Le(r.name)}`, m.specifier !== void 0) { - if (m.record !== void 0) { - if (m.compartment !== void 0) + )} in compartment ${xe(r.name)}`, p.specifier !== void 0) { + if (p.record !== void 0) { + if (p.compartment !== void 0) throw v( "Cannot redirect to an explicit record with a specified compartment" ); const { - compartment: p = r, - specifier: h = n, - record: _, - importMeta: w - } = m, I = Yn( + compartment: m = r, + specifier: _ = n, + record: S, + importMeta: T + } = p, N = uo( t, e, - p, - h, + m, _, + S, + o, a, - s, i, - w + T ); - return $e(d, n, I), I; + return $e(f, n, N), N; } - if (m.compartment !== void 0) { - if (m.importMeta !== void 0) + if (p.compartment !== void 0) { + if (p.importMeta !== void 0) throw v( "Cannot redirect to an implicit record with a specified importMeta" ); - const p = await mr( + const m = yield Ut( t, e, - m.compartment, - m.specifier, + p.compartment, + p.specifier, + o, a, - s, i ); - return $e(d, n, p), p; + return $e(f, n, m), m; } throw v("Unnexpected RedirectStaticModuleInterface record shape"); } - return Yn( + return uo( t, e, r, n, - m, + p, + o, a, - s, i ); -}, mr = async (t, e, r, n, a, s, i) => { - const { name: c } = M( +} +const Ut = (t, e, r, n, o, a, i) => { + const { name: c } = L( t, r ); - let u = De(s, r); - u === void 0 && (u = new Ce(), $e(s, r, u)); - let l = De(u, n); - return l !== void 0 || (l = na( - ki( + let l = Ue(i, r); + l === void 0 && (l = new Pe(), $e(i, r, l)); + let u = Ue(l, n); + return u !== void 0 || (u = a(Vi, Wi)( + Yi, + [ t, e, r, n, + o, a, - s, i - ), + ], (d) => { - throw Z.note( + throw z.note( d, - en`${d.message}, loading ${Le(n)} in compartment ${Le( + un`${d.message}, loading ${xe(n)} in compartment ${xe( c )}` ), d; } - ), $e(u, n, l)), l; -}, Jn = async (t, e, r, n) => { - const { name: a } = M( + ), $e(l, n, u)), u; +}; +function Ji() { + const t = new Ct(), e = []; + return { enqueueJob: (o, a) => { + Sn( + t, + Uo(o(...a), Hi, (i) => { + X(e, i); + }) + ); + }, drainQueue: async () => { + for (const o of t) + await o; + return e; + } }; +} +function Ps({ errors: t, errorPrefix: e }) { + if (t.length > 0) { + const r = le("COMPARTMENT_LOAD_ERRORS", "", ["verbose"]) === "verbose"; + throw v( + `${e} (${t.length} underlying failures: ${Rt( + se(t, (n) => n.message + (r ? n.stack : "")), + ", " + )}` + ); + } +} +const Xi = (t, e) => e, Qi = (t, e) => t, fo = async (t, e, r, n) => { + const { name: o } = L( t, r - ), s = new Pt(), i = new Ce(), c = [], u = mr( + ), a = new Pe(), { enqueueJob: i, drainQueue: c } = Ji(); + i(Ut, [ t, e, r, n, - s, i, - c - ); - $r( - s, - yn(u, ls, (l) => { - ae(c, l); - }) - ); - for (const l of s) - await l; - if (c.length > 0) - throw v( - `Failed to load module ${Le(n)} in package ${Le( - a - )} (${c.length} underlying failures: ${At( - fe(c, (l) => l.message), - ", " - )}` - ); -}, { quote: ht } = Z, Ti = () => { + Qi, + a + ]); + const l = await c(); + Ps({ + errors: l, + errorPrefix: `Failed to load module ${xe(n)} in package ${xe( + o + )}` + }); +}, ec = (t, e, r, n) => { + const { name: o } = L( + t, + r + ), a = new Pe(), i = [], c = (l, u) => { + try { + l(...u); + } catch (d) { + X(i, d); + } + }; + c(Ut, [ + t, + e, + r, + n, + c, + Xi, + a + ]), Ps({ + errors: i, + errorPrefix: `Failed to load module ${xe(n)} in package ${xe( + o + )}` + }); +}, { quote: yt } = z, tc = () => { let t = !1; - const e = H(null, { + const e = Z(null, { // Make this appear like an ESM module namespace object. - [He]: { + [qe]: { value: "Module", writable: !1, enumerable: !1, configurable: !1 } }); - return g({ + return y({ activate() { t = !0; }, exportsTarget: e, - exportsProxy: new xr(e, { - get(r, n, a) { + exportsProxy: new Cr(e, { + get(r, n, o) { if (!t) throw v( - `Cannot get property ${ht( + `Cannot get property ${yt( n )} of module exports namespace, the module has not yet begun to execute` ); - return Ds(e, n, a); + return oa(e, n, o); }, - set(r, n, a) { + set(r, n, o) { throw v( - `Cannot set property ${ht(n)} of module exports namespace` + `Cannot set property ${yt(n)} of module exports namespace` ); }, has(r, n) { if (!t) throw v( - `Cannot check property ${ht( + `Cannot check property ${yt( n )}, the module has not yet begun to execute` ); - return ho(e, n); + return Ao(e, n); }, deleteProperty(r, n) { throw v( - `Cannot delete property ${ht(n)}s of module exports namespace` + `Cannot delete property ${yt(n)}s of module exports namespace` ); }, ownKeys(r) { @@ -3617,30 +3823,30 @@ const { Fail: Ei, details: en, quote: Le } = Z, ls = () => { throw v( "Cannot enumerate keys, the module has not yet begun to execute" ); - return it(e); + return De(e); }, getOwnPropertyDescriptor(r, n) { if (!t) throw v( - `Cannot get own property descriptor ${ht( + `Cannot get own property descriptor ${yt( n )}, the module has not yet begun to execute` ); - return Us(e, n); + return sa(e, n); }, preventExtensions(r) { if (!t) throw v( "Cannot prevent extensions of module exports namespace, the module has not yet begun to execute" ); - return Zs(e); + return ia(e); }, isExtensible() { if (!t) throw v( "Cannot check extensibility of module exports namespace, the module has not yet begun to execute" ); - return js(e); + return aa(e); }, getPrototypeOf(r) { return null; @@ -3648,12 +3854,12 @@ const { Fail: Ei, details: en, quote: Le } = Z, ls = () => { setPrototypeOf(r, n) { throw v("Cannot set prototype of module exports namespace"); }, - defineProperty(r, n, a) { + defineProperty(r, n, o) { throw v( - `Cannot define property ${ht(n)} of module exports namespace` + `Cannot define property ${yt(n)} of module exports namespace` ); }, - apply(r, n, a) { + apply(r, n, o) { throw v( "Cannot call module exports namespace, it is not a function" ); @@ -3665,102 +3871,102 @@ const { Fail: Ei, details: en, quote: Le } = Z, ls = () => { } }) }); -}, En = (t, e, r, n) => { - const { deferredExports: a } = e; - if (!Cr(a, n)) { - const s = Ti(); - ee( +}, Dn = (t, e, r, n) => { + const { deferredExports: o } = e; + if (!Lr(o, n)) { + const a = tc(); + ie( r, - s.exportsProxy, - xi(t, n) - ), $e(a, n, s); + a.exportsProxy, + qi(t, n) + ), $e(o, n, a); } - return De(a, n); -}, Ii = (t, e) => { + return Ue(o, n); +}, rc = (t, e) => { const { sloppyGlobalsMode: r = !1, __moduleShimLexicals__: n = void 0 } = e; - let a; + let o; if (n === void 0 && !r) - ({ safeEvaluate: a } = t); + ({ safeEvaluate: o } = t); else { - let { globalTransforms: s } = t; + let { globalTransforms: a } = t; const { globalObject: i } = t; let c; - n !== void 0 && (s = void 0, c = H( + n !== void 0 && (a = void 0, c = Z( null, - Je(n) - )), { safeEvaluate: a } = Sn({ + Ze(n) + )), { safeEvaluate: o } = On({ globalObject: i, moduleLexicals: c, - globalTransforms: s, + globalTransforms: a, sloppyGlobalsMode: r }); } - return { safeEvaluate: a }; -}, us = (t, e, r) => { + return { safeEvaluate: o }; +}, Ts = (t, e, r) => { if (typeof e != "string") throw v("first argument of evaluate() must be a string"); const { transforms: n = [], - __evadeHtmlCommentTest__: a = !1, - __evadeImportExpressionTest__: s = !1, + __evadeHtmlCommentTest__: o = !1, + __evadeImportExpressionTest__: a = !1, __rejectSomeDirectEvalExpressions__: i = !0 // Note default on } = r, c = [...n]; - a === !0 && ae(c, Jo), s === !0 && ae(c, es), i === !0 && ae(c, ts); - const { safeEvaluate: u } = Ii( + o === !0 && X(c, gs), a === !0 && X(c, _s), i === !0 && X(c, bs); + const { safeEvaluate: l } = rc( t, r ); - return u(e, { + return l(e, { localTransforms: c }); -}, { quote: rr } = Z, Ai = (t, e, r, n, a, s) => { - const { exportsProxy: i, exportsTarget: c, activate: u } = En( +}, { quote: cr } = z, nc = (t, e, r, n, o, a) => { + const { exportsProxy: i, exportsTarget: c, activate: l } = Dn( r, - M(t, r), + L(t, r), n, - a - ), l = H(null); + o + ), u = Z(null); if (e.exports) { - if (!vt(e.exports) || Bs(e.exports, (f) => typeof f != "string")) + if (!Et(e.exports) || ua(e.exports, (f) => typeof f != "string")) throw v( - `SES third-party static module record "exports" property must be an array of strings for module ${a}` + `SES third-party static module record "exports" property must be an array of strings for module ${o}` ); - st(e.exports, (f) => { - let m = c[f]; + ut(e.exports, (f) => { + let h = c[f]; const p = []; - L(c, f, { - get: () => m, - set: (w) => { - m = w; - for (const I of p) - I(w); + M(c, f, { + get: () => h, + set: (S) => { + h = S; + for (const T of p) + T(S); }, enumerable: !0, configurable: !1 - }), l[f] = (w) => { - ae(p, w), w(m); + }), u[f] = (S) => { + X(p, S), S(h); }; - }), l["*"] = (f) => { + }), u["*"] = (f) => { f(c); }; } const d = { activated: !1 }; - return g({ - notifiers: l, + return y({ + notifiers: u, exportsProxy: i, execute() { - if (ho(d, "errorFromExecute")) + if (Ao(d, "errorFromExecute")) throw d.errorFromExecute; if (!d.activated) { - u(), d.activated = !0; + l(), d.activated = !0; try { e.execute( c, r, - s + a ); } catch (f) { throw d.errorFromExecute = f, f; @@ -3768,294 +3974,294 @@ const { Fail: Ei, details: en, quote: Le } = Z, ls = () => { } } }); -}, Ci = (t, e, r, n) => { +}, oc = (t, e, r, n) => { const { - compartment: a, - moduleSpecifier: s, + compartment: o, + moduleSpecifier: a, staticModuleRecord: i, importMeta: c } = r, { - reexports: u = [], - __syncModuleProgram__: l, + reexports: l = [], + __syncModuleProgram__: u, __fixedExportMap__: d = {}, __liveExportMap__: f = {}, - __reexportMap__: m = {}, + __reexportMap__: h = {}, __needsImportMeta__: p = !1, - __syncModuleFunctor__: h - } = i, _ = M(t, a), { __shimTransforms__: w, importMetaHook: I } = _, { exportsProxy: $, exportsTarget: T, activate: D } = En( - a, + __syncModuleFunctor__: m + } = i, _ = L(t, o), { __shimTransforms__: S, importMetaHook: T } = _, { exportsProxy: N, exportsTarget: x, activate: D } = Dn( + o, _, e, - s - ), j = H(null), q = H(null), K = H(null), je = H(null), pe = H(null); - c && Pr(pe, c), p && I && I(s, pe); - const Ze = H(null), Xe = H(null); - st(te(d), ([me, [z]]) => { - let G = Ze[z]; - if (!G) { - let X, Q = !0, ce = []; + a + ), G = Z(null), B = Z(null), K = Z(null), ze = Z(null), he = Z(null); + c && $r(he, c), p && T && T(a, he); + const Ge = Z(null), rt = Z(null); + ut(re(d), ([me, [H]]) => { + let V = Ge[H]; + if (!V) { + let ee, te = !0, ce = []; const Y = () => { - if (Q) - throw ot(`binding ${rr(z)} not yet initialized`); - return X; - }, _e = g((be) => { - if (!Q) + if (te) + throw lt(`binding ${cr(H)} not yet initialized`); + return ee; + }, be = y((we) => { + if (!te) throw v( - `Internal: binding ${rr(z)} already initialized` + `Internal: binding ${cr(H)} already initialized` ); - X = be; - const In = ce; - ce = null, Q = !1; - for (const we of In || []) - we(be); - return be; + ee = we; + const Bn = ce; + ce = null, te = !1; + for (const Se of Bn || []) + Se(we); + return we; }); - G = { + V = { get: Y, - notify: (be) => { - be !== _e && (Q ? ae(ce || [], be) : be(X)); + notify: (we) => { + we !== be && (te ? X(ce || [], we) : we(ee)); } - }, Ze[z] = G, K[z] = _e; + }, Ge[H] = V, K[H] = be; } - j[me] = { - get: G.get, + G[me] = { + get: V.get, set: void 0, enumerable: !0, configurable: !1 - }, Xe[me] = G.notify; - }), st( - te(f), - ([me, [z, G]]) => { - let X = Ze[z]; - if (!X) { - let Q, ce = !0; - const Y = [], _e = () => { + }, rt[me] = V.notify; + }), ut( + re(f), + ([me, [H, V]]) => { + let ee = Ge[H]; + if (!ee) { + let te, ce = !0; + const Y = [], be = () => { if (ce) - throw ot( - `binding ${rr(me)} not yet initialized` + throw lt( + `binding ${cr(me)} not yet initialized` ); - return Q; - }, ft = g((we) => { - Q = we, ce = !1; - for (const Lr of Y) - Lr(we); - }), be = (we) => { + return te; + }, gt = y((Se) => { + te = Se, ce = !1; + for (const zr of Y) + zr(Se); + }), we = (Se) => { if (ce) - throw ot(`binding ${rr(z)} not yet initialized`); - Q = we; - for (const Lr of Y) - Lr(we); + throw lt(`binding ${cr(H)} not yet initialized`); + te = Se; + for (const zr of Y) + zr(Se); }; - X = { - get: _e, - notify: (we) => { - we !== ft && (ae(Y, we), ce || we(Q)); + ee = { + get: be, + notify: (Se) => { + Se !== gt && (X(Y, Se), ce || Se(te)); } - }, Ze[z] = X, G && L(q, z, { - get: _e, - set: be, + }, Ge[H] = ee, V && M(B, H, { + get: be, + set: we, enumerable: !0, configurable: !1 - }), je[z] = ft; + }), ze[H] = gt; } - j[me] = { - get: X.get, + G[me] = { + get: ee.get, set: void 0, enumerable: !0, configurable: !1 - }, Xe[me] = X.notify; + }, rt[me] = ee.notify; } ); - const ze = (me) => { - me(T); + const Be = (me) => { + me(x); }; - Xe["*"] = ze; - function er(me) { - const z = H(null); - z.default = !1; - for (const [G, X] of me) { - const Q = De(n, G); - Q.execute(); - const { notifiers: ce } = Q; - for (const [Y, _e] of X) { - const ft = ce[Y]; - if (!ft) - throw Kt( - `The requested module '${G}' does not provide an export named '${Y}'` + rt["*"] = Be; + function ar(me) { + const H = Z(null); + H.default = !1; + for (const [V, ee] of me) { + const te = Ue(n, V); + te.execute(); + const { notifiers: ce } = te; + for (const [Y, be] of ee) { + const gt = ce[Y]; + if (!gt) + throw tr( + `The requested module '${V}' does not provide an export named '${Y}'` ); - for (const be of _e) - ft(be); + for (const we of be) + gt(we); } - if (Ar(u, G)) - for (const [Y, _e] of te( + if (Mr(l, V)) + for (const [Y, be] of re( ce )) - z[Y] === void 0 ? z[Y] = _e : z[Y] = !1; - if (m[G]) - for (const [Y, _e] of m[G]) - z[_e] = ce[Y]; + H[Y] === void 0 ? H[Y] = be : H[Y] = !1; + if (h[V]) + for (const [Y, be] of h[V]) + H[be] = ce[Y]; } - for (const [G, X] of te(z)) - if (!Xe[G] && X !== !1) { - Xe[G] = X; - let Q; - X((Y) => Q = Y), j[G] = { + for (const [V, ee] of re(H)) + if (!rt[V] && ee !== !1) { + rt[V] = ee; + let te; + ee((Y) => te = Y), G[V] = { get() { - return Q; + return te; }, set: void 0, enumerable: !0, configurable: !1 }; } - st( - _o(co(j)), - (G) => L(T, G, j[G]) - ), g(T), D(); + ut( + Oo(Eo(G)), + (V) => M(x, V, G[V]) + ), y(x), D(); } - let Ct; - h !== void 0 ? Ct = h : Ct = us(_, l, { - globalObject: a.globalThis, - transforms: w, - __moduleShimLexicals__: q + let Ot; + m !== void 0 ? Ot = m : Ot = Ts(_, u, { + globalObject: o.globalThis, + transforms: S, + __moduleShimLexicals__: B }); - let kn = !1, Tn; - function xs() { - if (Ct) { - const me = Ct; - Ct = null; + let zn = !1, Gn; + function Gs() { + if (Ot) { + const me = Ot; + Ot = null; try { me( - g({ - imports: g(er), - onceVar: g(K), - liveVar: g(je), - importMeta: pe + y({ + imports: y(ar), + onceVar: y(K), + liveVar: y(ze), + importMeta: he }) ); - } catch (z) { - kn = !0, Tn = z; + } catch (H) { + zn = !0, Gn = H; } } - if (kn) - throw Tn; + if (zn) + throw Gn; } - return g({ - notifiers: Xe, - exportsProxy: $, - execute: xs + return y({ + notifiers: rt, + exportsProxy: N, + execute: Gs }); -}, { Fail: nt, quote: W } = Z, ds = (t, e, r, n) => { - const { name: a, moduleRecords: s } = M( +}, { Fail: ct, quote: q } = z, As = (t, e, r, n) => { + const { name: o, moduleRecords: a } = L( t, r - ), i = De(s, n); + ), i = Ue(a, n); if (i === void 0) - throw ot( - `Missing link to module ${W(n)} from compartment ${W( - a + throw lt( + `Missing link to module ${q(n)} from compartment ${q( + o )}` ); - return Li(t, e, i); + return uc(t, e, i); }; -function $i(t) { +function sc(t) { return typeof t.__syncModuleProgram__ == "string"; } -function Ni(t, e) { +function ac(t, e) { const { __fixedExportMap__: r, __liveExportMap__: n } = t; - We(r) || nt`Property '__fixedExportMap__' of a precompiled module record must be an object, got ${W( + Ye(r) || ct`Property '__fixedExportMap__' of a precompiled module record must be an object, got ${q( r - )}, for module ${W(e)}`, We(n) || nt`Property '__liveExportMap__' of a precompiled module record must be an object, got ${W( + )}, for module ${q(e)}`, Ye(n) || ct`Property '__liveExportMap__' of a precompiled module record must be an object, got ${q( n - )}, for module ${W(e)}`; + )}, for module ${q(e)}`; } -function Oi(t) { +function ic(t) { return typeof t.execute == "function"; } -function Ri(t, e) { +function cc(t, e) { const { exports: r } = t; - vt(r) || nt`Property 'exports' of a third-party static module record must be an array, got ${W( + Et(r) || ct`Property 'exports' of a third-party static module record must be an array, got ${q( r - )}, for module ${W(e)}`; + )}, for module ${q(e)}`; } -function Mi(t, e) { - We(t) || nt`Static module records must be of type object, got ${W( +function lc(t, e) { + Ye(t) || ct`Static module records must be of type object, got ${q( t - )}, for module ${W(e)}`; - const { imports: r, exports: n, reexports: a = [] } = t; - vt(r) || nt`Property 'imports' of a static module record must be an array, got ${W( + )}, for module ${q(e)}`; + const { imports: r, exports: n, reexports: o = [] } = t; + Et(r) || ct`Property 'imports' of a static module record must be an array, got ${q( r - )}, for module ${W(e)}`, vt(n) || nt`Property 'exports' of a precompiled module record must be an array, got ${W( + )}, for module ${q(e)}`, Et(n) || ct`Property 'exports' of a precompiled module record must be an array, got ${q( n - )}, for module ${W(e)}`, vt(a) || nt`Property 'reexports' of a precompiled module record must be an array if present, got ${W( - a - )}, for module ${W(e)}`; + )}, for module ${q(e)}`, Et(o) || ct`Property 'reexports' of a precompiled module record must be an array if present, got ${q( + o + )}, for module ${q(e)}`; } -const Li = (t, e, r) => { - const { compartment: n, moduleSpecifier: a, resolvedImports: s, staticModuleRecord: i } = r, { instances: c } = M(t, n); - if (Cr(c, a)) - return De(c, a); - Mi(i, a); - const u = new Ce(); - let l; - if ($i(i)) - Ni(i, a), l = Ci( +const uc = (t, e, r) => { + const { compartment: n, moduleSpecifier: o, resolvedImports: a, staticModuleRecord: i } = r, { instances: c } = L(t, n); + if (Lr(c, o)) + return Ue(c, o); + lc(i, o); + const l = new Pe(); + let u; + if (sc(i)) + ac(i, o), u = oc( t, e, r, - u + l ); - else if (Oi(i)) - Ri(i, a), l = Ai( + else if (ic(i)) + cc(i, o), u = nc( t, i, n, e, - a, - s + o, + a ); else throw v( - `importHook must return a static module record, got ${W( + `importHook must return a static module record, got ${q( i )}` ); - $e(c, a, l); - for (const [d, f] of te(s)) { - const m = ds( + $e(c, o, u); + for (const [d, f] of re(a)) { + const h = As( t, e, n, f ); - $e(u, d, m); + $e(l, d, h); } - return l; -}, { quote: zr } = Z, Rt = new Te(), Me = new Te(), nr = (t) => { - const { importHook: e, resolveHook: r } = M(Me, t); + return u; +}, { quote: Xr } = z, bt = new Me(), Ce = new Me(), lr = (t) => { + const { importHook: e, resolveHook: r } = L(Ce, t); if (typeof e != "function" || typeof r != "function") throw v( "Compartment must be constructed with an importHook and a resolveHook for it to be able to load modules" ); -}, xn = function(e = {}, r = {}, n = {}) { +}, Un = function(e = {}, r = {}, n = {}) { throw v( "Compartment.prototype.constructor is not a valid constructor." ); -}, Xn = (t, e) => { - const { execute: r, exportsProxy: n } = ds( - Me, - Rt, +}, po = (t, e) => { + const { execute: r, exportsProxy: n } = As( + Ce, + bt, t, e ); return r(), n; -}, Pn = { - constructor: xn, +}, jn = { + constructor: Un, get globalThis() { - return M(Me, this).globalObject; + return L(Ce, this).globalObject; }, get name() { - return M(Me, this).name; + return L(Ce, this).name; }, /** * @param {string} source is a JavaScript program grammar construction. @@ -4068,17 +4274,17 @@ const Li = (t, e, r) => { * @param {boolean} [options.__rejectSomeDirectEvalExpressions__] */ evaluate(t, e = {}) { - const r = M(Me, this); - return us(r, t, e); + const r = L(Ce, this); + return Ts(r, t, e); }, module(t) { if (typeof t != "string") throw v("first argument of module() must be a string"); - nr(this); - const { exportsProxy: e } = En( + lr(this); + const { exportsProxy: e } = Dn( this, - M(Me, this), - Rt, + L(Ce, this), + bt, t ); return e; @@ -4086,9 +4292,9 @@ const Li = (t, e, r) => { async import(t) { if (typeof t != "string") throw v("first argument of import() must be a string"); - return nr(this), yn( - Jn(Me, Rt, this, t), - () => ({ namespace: Xn( + return lr(this), Uo( + fo(Ce, bt, this, t), + () => ({ namespace: po( /** @type {Compartment} */ this, t @@ -4098,212 +4304,210 @@ const Li = (t, e, r) => { async load(t) { if (typeof t != "string") throw v("first argument of load() must be a string"); - return nr(this), Jn(Me, Rt, this, t); + return lr(this), fo(Ce, bt, this, t); }, importNow(t) { if (typeof t != "string") throw v("first argument of importNow() must be a string"); - return nr(this), Xn( + return lr(this), ec(Ce, bt, this, t), po( /** @type {Compartment} */ this, t ); } }; -F(Pn, { - [He]: { +F(jn, { + [qe]: { value: "Compartment", writable: !1, enumerable: !1, configurable: !0 } }); -F(xn, { - prototype: { value: Pn } +F(Un, { + prototype: { value: jn } }); -const tn = (t, e, r) => { - function n(a = {}, s = {}, i = {}) { +const dn = (t, e, r) => { + function n(o = {}, a = {}, i = {}) { if (new.target === void 0) throw v( "Class constructor Compartment cannot be invoked without 'new'" ); const { name: c = "", - transforms: u = [], - __shimTransforms__: l = [], + transforms: l = [], + __shimTransforms__: u = [], resolveHook: d, importHook: f, - moduleMapHook: m, - importMetaHook: p - } = i, h = [...u, ...l], _ = new Ce(), w = new Ce(), I = new Ce(); - for (const [D, j] of te(s || {})) { - if (typeof j == "string") + importNowHook: h, + moduleMapHook: p, + importMetaHook: m + } = i, _ = [...l, ...u], S = new Pe(), T = new Pe(), N = new Pe(); + for (const [G, B] of re(a || {})) { + if (typeof B == "string") throw v( - `Cannot map module ${zr(D)} to ${zr( - j + `Cannot map module ${Xr(G)} to ${Xr( + B )} in parent compartment` ); - if (M(Rt, j) === void 0) - throw ot( - `Cannot map module ${zr( - D + if (L(bt, B) === void 0) + throw lt( + `Cannot map module ${Xr( + G )} because it has no known compartment in this realm` ); } - const $ = {}; - Ga($), Go($); - const { safeEvaluate: T } = Sn({ - globalObject: $, - globalTransforms: h, + const x = {}; + li(x), cs(x); + const { safeEvaluate: D } = On({ + globalObject: x, + globalTransforms: _, sloppyGlobalsMode: !1 }); - Bo($, { + ls(x, { intrinsics: e, - newGlobalPropertyNames: Do, + newGlobalPropertyNames: rs, makeCompartmentConstructor: t, markVirtualizedNativeFunction: r - }), Qr( - $, - T, + }), ln( + x, + D, r - ), Pr($, a), ee(Me, this, { + ), $r(x, o), ie(Ce, this, { name: `${c}`, - globalTransforms: h, - globalObject: $, - safeEvaluate: T, + globalTransforms: _, + globalObject: x, + safeEvaluate: D, resolveHook: d, importHook: f, - moduleMap: s, - moduleMapHook: m, - importMetaHook: p, - moduleRecords: _, - __shimTransforms__: l, - deferredExports: I, - instances: w + importNowHook: h, + moduleMap: a, + moduleMapHook: p, + importMetaHook: m, + moduleRecords: S, + __shimTransforms__: u, + deferredExports: N, + instances: T }); } - return n.prototype = Pn, n; + return n.prototype = jn, n; }; -function Gr(t) { - return B(t).constructor; +function Qr(t) { + return j(t).constructor; } -function Fi() { +function dc() { return arguments; } -const Di = () => { - const t = ve.prototype.constructor, e = de(Fi(), "callee"), r = e && e.get, n = ea(new ie()), a = B(n), s = Tr[po] && Ys(/./), i = s && B(s), c = Hs([]), u = B(c), l = B(Ts), d = qs(new Ce()), f = B(d), m = Ks(new Pt()), p = B(m), h = B(u); +const fc = () => { + const t = ve.prototype.constructor, e = J(dc(), "callee"), r = e && e.get, n = _a(new pe()), o = j(n), a = Rr[Po] && ga(/./), i = a && j(a), c = da([]), l = j(c), u = j(Vs), d = ha(new Pe()), f = j(d), h = ma(new Ct()), p = j(h), m = j(l); function* _() { } - const w = Gr(_), I = w.prototype; - async function* $() { + const S = Qr(_), T = S.prototype; + async function* N() { } - const T = Gr( - $ - ), D = T.prototype, j = D.prototype, q = B(j); + const x = Qr( + N + ), D = x.prototype, G = D.prototype, B = j(G); async function K() { } - const je = Gr(K), pe = { + const ze = Qr(K), he = { "%InertFunction%": t, - "%ArrayIteratorPrototype%": u, - "%InertAsyncFunction%": je, + "%ArrayIteratorPrototype%": l, + "%InertAsyncFunction%": ze, "%AsyncGenerator%": D, - "%InertAsyncGeneratorFunction%": T, - "%AsyncGeneratorPrototype%": j, - "%AsyncIteratorPrototype%": q, - "%Generator%": I, - "%InertGeneratorFunction%": w, - "%IteratorPrototype%": h, + "%InertAsyncGeneratorFunction%": x, + "%AsyncGeneratorPrototype%": G, + "%AsyncIteratorPrototype%": B, + "%Generator%": T, + "%InertGeneratorFunction%": S, + "%IteratorPrototype%": m, "%MapIteratorPrototype%": f, "%RegExpStringIteratorPrototype%": i, "%SetIteratorPrototype%": p, - "%StringIteratorPrototype%": a, + "%StringIteratorPrototype%": o, "%ThrowTypeError%": r, - "%TypedArray%": l, - "%InertCompartment%": xn + "%TypedArray%": u, + "%InertCompartment%": Un }; - return x.Iterator && (pe["%IteratorHelperPrototype%"] = B( + return k.Iterator && (he["%IteratorHelperPrototype%"] = j( // eslint-disable-next-line @endo/no-polymorphic-call - x.Iterator.from([]).take(0) - ), pe["%WrapForValidIteratorPrototype%"] = B( + k.Iterator.from([]).take(0) + ), he["%WrapForValidIteratorPrototype%"] = j( // eslint-disable-next-line @endo/no-polymorphic-call - x.Iterator.from({ next() { + k.Iterator.from({ next() { } }) - )), x.AsyncIterator && (pe["%AsyncIteratorHelperPrototype%"] = B( + )), k.AsyncIterator && (he["%AsyncIteratorHelperPrototype%"] = j( // eslint-disable-next-line @endo/no-polymorphic-call - x.AsyncIterator.from([]).take(0) - ), pe["%WrapForValidAsyncIteratorPrototype%"] = B( + k.AsyncIterator.from([]).take(0) + ), he["%WrapForValidAsyncIteratorPrototype%"] = j( // eslint-disable-next-line @endo/no-polymorphic-call - x.AsyncIterator.from({ next() { + k.AsyncIterator.from({ next() { } }) - )), pe; -}, fs = (t, e) => { + )), he; +}, Is = (t, e) => { if (e !== "safe" && e !== "unsafe") throw v(`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, g(r); + return r.isFake = !0, y(r); }; -g(fs); -const Ui = () => { - const t = Ot, e = t.prototype, r = { - Symbol(s) { - return t(s); - } - }.Symbol; +y(Is); +const pc = () => { + const t = St, e = t.prototype, r = Sa(St, void 0); F(e, { constructor: { value: r // leave other `constructor` attributes as is } }); - const n = te( - Je(t) - ), a = Tt( - fe(n, ([s, i]) => [ - s, + const n = re( + Ze(t) + ), o = mt( + se(n, ([a, i]) => [ + a, { ...i, configurable: !0 } ]) ); - return F(r, a), { "%SharedSymbol%": r }; -}, ji = (t) => { + return F(r, o), { "%SharedSymbol%": r }; +}, hc = (t) => { try { return t(), !1; } catch { return !0; } -}, Qn = (t, e, r) => { +}, ho = (t, e, r) => { if (t === void 0) return !1; - const n = de(t, e); + const n = J(t, e); if (!n || "value" in n) return !1; - const { get: a, set: s } = n; - if (typeof a != "function" || typeof s != "function" || a() !== r || oe(a, t, []) !== r) + const { get: o, set: a } = n; + if (typeof o != "function" || typeof a != "function" || o() !== r || ne(o, t, []) !== r) return !1; const i = "Seems to be a setter", c = { __proto__: null }; - if (oe(s, c, [i]), c[e] !== i) + if (ne(a, c, [i]), c[e] !== i) return !1; - const u = { __proto__: t }; - return oe(s, u, [i]), u[e] !== i || !ji(() => oe(s, t, [r])) || "originalValue" in a || n.configurable === !1 ? !1 : (L(t, e, { + const l = { __proto__: t }; + return ne(a, l, [i]), l[e] !== i || !hc(() => ne(a, t, [r])) || "originalValue" in o || n.configurable === !1 ? !1 : (M(t, e, { value: r, writable: !0, enumerable: n.enumerable, configurable: !0 }), !0); -}, Zi = (t) => { - Qn( +}, mc = (t) => { + ho( t["%IteratorPrototype%"], "constructor", t.Iterator - ), Qn( + ), ho( t["%IteratorPrototype%"], - He, + qe, "Iterator" ); -}, { Fail: eo, details: to, quote: ro } = Z; -let or, sr; -const zi = Sa(), Gi = () => { +}, { Fail: mo, details: go, quote: yo } = z; +let ur, dr; +const gc = Ga(), yc = () => { let t = !1; try { t = ve( @@ -4312,7 +4516,7 @@ const zi = Sa(), Gi = () => { ` eval("SES_changed = true"); return SES_changed; ` - )(Eo, !1), t || delete x.SES_changed; + )(jo, !1), t || delete k.SES_changed; } catch { t = !0; } @@ -4320,185 +4524,195 @@ const zi = Sa(), Gi = () => { throw v( "SES cannot initialize unless 'eval' is the original intrinsic 'eval', suitable for direct-eval (dynamically scoped eval) (SES_DIRECT_EVAL)" ); -}, ps = (t = {}) => { +}, Cs = (t = {}) => { const { - errorTaming: e = he("LOCKDOWN_ERROR_TAMING", "safe"), + errorTaming: e = le("LOCKDOWN_ERROR_TAMING", "safe"), errorTrapping: r = ( /** @type {"platform" | "none" | "report" | "abort" | "exit" | undefined} */ - he("LOCKDOWN_ERROR_TRAPPING", "platform") + le("LOCKDOWN_ERROR_TRAPPING", "platform") ), unhandledRejectionTrapping: n = ( /** @type {"none" | "report" | undefined} */ - he("LOCKDOWN_UNHANDLED_REJECTION_TRAPPING", "report") + le("LOCKDOWN_UNHANDLED_REJECTION_TRAPPING", "report") ), - regExpTaming: a = he("LOCKDOWN_REGEXP_TAMING", "safe"), - localeTaming: s = he("LOCKDOWN_LOCALE_TAMING", "safe"), + regExpTaming: o = le("LOCKDOWN_REGEXP_TAMING", "safe"), + localeTaming: a = le("LOCKDOWN_LOCALE_TAMING", "safe"), consoleTaming: i = ( /** @type {'unsafe' | 'safe' | undefined} */ - he("LOCKDOWN_CONSOLE_TAMING", "safe") + le("LOCKDOWN_CONSOLE_TAMING", "safe") ), - overrideTaming: c = he("LOCKDOWN_OVERRIDE_TAMING", "moderate"), - stackFiltering: u = he("LOCKDOWN_STACK_FILTERING", "concise"), - domainTaming: l = he("LOCKDOWN_DOMAIN_TAMING", "safe"), - evalTaming: d = he("LOCKDOWN_EVAL_TAMING", "safeEval"), - overrideDebug: f = Ve( - wo(he("LOCKDOWN_OVERRIDE_DEBUG", ""), ","), + overrideTaming: c = le("LOCKDOWN_OVERRIDE_TAMING", "moderate"), + stackFiltering: l = le("LOCKDOWN_STACK_FILTERING", "concise"), + domainTaming: u = le("LOCKDOWN_DOMAIN_TAMING", "safe"), + evalTaming: d = le("LOCKDOWN_EVAL_TAMING", "safeEval"), + overrideDebug: f = Ke( + Tn(le("LOCKDOWN_OVERRIDE_DEBUG", ""), ","), /** @param {string} debugName */ - (ze) => ze !== "" + (Be) => Be !== "" ), - __hardenTaming__: m = he("LOCKDOWN_HARDEN_TAMING", "safe"), + __hardenTaming__: h = le("LOCKDOWN_HARDEN_TAMING", "safe"), dateTaming: p = "safe", // deprecated - mathTaming: h = "safe", + mathTaming: m = "safe", // deprecated ..._ } = t; - d === "unsafeEval" || d === "safeEval" || d === "noEval" || eo`lockdown(): non supported option evalTaming: ${ro(d)}`; - const w = it(_); - if (w.length === 0 || eo`lockdown(): non supported option ${ro(w)}`, or === void 0 || // eslint-disable-next-line @endo/no-polymorphic-call - Z.fail( - to`Already locked down at ${or} (SES_ALREADY_LOCKED_DOWN)`, + d === "unsafeEval" || d === "safeEval" || d === "noEval" || mo`lockdown(): non supported option evalTaming: ${yo(d)}`; + const S = De(_); + if (S.length === 0 || mo`lockdown(): non supported option ${yo(S)}`, ur === void 0 || // eslint-disable-next-line @endo/no-polymorphic-call + z.fail( + go`Already locked down at ${ur} (SES_ALREADY_LOCKED_DOWN)`, v - ), or = v("Prior lockdown (SES_ALREADY_LOCKED_DOWN)"), or.stack, Gi(), x.Function.prototype.constructor !== x.Function && // @ts-ignore harden is absent on globalThis type def. - typeof x.harden == "function" && // @ts-ignore lockdown is absent on globalThis type def. - typeof x.lockdown == "function" && x.Date.prototype.constructor !== x.Date && typeof x.Date.now == "function" && // @ts-ignore does not recognize that Date constructor is a special + ), ur = v("Prior lockdown (SES_ALREADY_LOCKED_DOWN)"), ur.stack, yc(), k.Function.prototype.constructor !== k.Function && // @ts-ignore harden is absent on globalThis type def. + typeof k.harden == "function" && // @ts-ignore lockdown is absent on globalThis type def. + typeof k.lockdown == "function" && k.Date.prototype.constructor !== k.Date && typeof k.Date.now == "function" && // @ts-ignore does not recognize that Date constructor is a special // Function. // eslint-disable-next-line @endo/no-polymorphic-call - kr(x.Date.prototype.constructor.now(), NaN)) + Nr(k.Date.prototype.constructor.now(), NaN)) throw v( "Already locked down but not by this SES instance (SES_MULTIPLE_INSTANCES)" ); - ni(l); - const $ = os(), { addIntrinsics: T, completePrototypes: D, finalIntrinsics: j } = jo(), q = fs(zi, m); - T({ harden: q }), T(Ca()), T($a(p)), T(Si(e, u)), T(Na(h)), T(Oa(a)), T(Ui()), T(Di()), D(); - const K = j(), je = { __proto__: null }; - typeof x.Buffer == "function" && (je.Buffer = x.Buffer); - let pe; - e !== "unsafe" && (pe = K["%InitialGetStackString%"]); - const Ze = ai( + Ei(u); + const N = Es(), { addIntrinsics: x, completePrototypes: D, finalIntrinsics: G } = ss(), B = Is(gc, h); + x({ harden: B }), x(Ya()), x(Ja(p)), x(Gi(e, l)), x(Xa(m)), x(Qa(o)), x(pc()), x(fc()), D(); + const K = G(), ze = { __proto__: null }; + typeof k.Buffer == "function" && (ze.Buffer = k.Buffer); + let he; + e !== "unsafe" && (he = K["%InitialGetStackString%"]); + const Ge = Ti( i, r, n, - pe + he ); - if (x.console = /** @type {Console} */ - Ze.console, typeof /** @type {any} */ - Ze.console._times == "object" && (je.SafeMap = B( + if (k.console = /** @type {Console} */ + Ge.console, typeof /** @type {any} */ + Ge.console._times == "object" && (ze.SafeMap = j( // eslint-disable-next-line no-underscore-dangle /** @type {any} */ - Ze.console._times - )), e === "unsafe" && x.assert === Z && (x.assert = Rr(void 0, !0)), ja(K, s), Zi(K), Aa(K, $), Go(x), Bo(x, { + Ge.console._times + )), e === "unsafe" && k.assert === z && (k.assert = jr(void 0, !0)), ai(K, a), mc(K), Ka(K, N), cs(k), ls(k, { intrinsics: K, - newGlobalPropertyNames: Fn, - makeCompartmentConstructor: tn, - markVirtualizedNativeFunction: $ + newGlobalPropertyNames: Jn, + makeCompartmentConstructor: dn, + markVirtualizedNativeFunction: N }), d === "noEval") - Qr( - x, - sa, - $ + ln( + k, + xa, + N ); else if (d === "safeEval") { - const { safeEvaluate: ze } = Sn({ globalObject: x }); - Qr( - x, - ze, - $ + const { safeEvaluate: Be } = On({ globalObject: k }); + ln( + k, + Be, + N ); } return () => { - sr === void 0 || // eslint-disable-next-line @endo/no-polymorphic-call - Z.fail( - to`Already locked down at ${sr} (SES_ALREADY_LOCKED_DOWN)`, + dr === void 0 || // eslint-disable-next-line @endo/no-polymorphic-call + z.fail( + go`Already locked down at ${dr} (SES_ALREADY_LOCKED_DOWN)`, v - ), sr = v( + ), dr = v( "Prior lockdown (SES_ALREADY_LOCKED_DOWN)" - ), sr.stack, La(K, c, f); - const ze = { + ), dr.stack, ri(K, c, f); + const Be = { intrinsics: K, - hostIntrinsics: je, + hostIntrinsics: ze, globals: { // Harden evaluators - Function: x.Function, - eval: x.eval, + Function: k.Function, + eval: k.eval, // @ts-ignore Compartment does exist on globalThis - Compartment: x.Compartment, + Compartment: k.Compartment, // Harden Symbol - Symbol: x.Symbol + Symbol: k.Symbol } }; - for (const er of Mt(Fn)) - ze.globals[er] = x[er]; - return q(ze), q; + for (const ar of Dt(Jn)) + Be.globals[ar] = k[ar]; + return B(Be), B; }; }; -x.lockdown = (t) => { - const e = ps(t); - x.harden = e(); +k.lockdown = (t) => { + const e = Cs(t); + k.harden = e(); }; -x.repairIntrinsics = (t) => { - const e = ps(t); - x.hardenIntrinsics = () => { - x.harden = e(); +k.repairIntrinsics = (t) => { + const e = Cs(t); + k.hardenIntrinsics = () => { + k.harden = e(); }; }; -const Bi = os(); -x.Compartment = tn( - tn, - Ia(x), - Bi +const vc = Es(); +k.Compartment = dn( + dn, + qa(k), + vc ); -x.assert = Z; -const Hi = (t) => { - let e = { x: 0, y: 0 }, r = { x: 0, y: 0 }, n = { x: 0, y: 0 }; - const a = (c) => { - const { clientX: u, clientY: l } = c, d = u - n.x + r.x, f = l - n.y + r.y; - e = { x: d, y: f }, t.style.transform = `translate(${d}px, ${f}px)`; - }, s = () => { - document.removeEventListener("mousemove", a), document.removeEventListener("mouseup", s); - }, i = (c) => { - n = { x: c.clientX, y: c.clientY }, r = { x: e.x, y: e.y }, document.addEventListener("mousemove", a), document.addEventListener("mouseup", s); +k.assert = z; +const _c = ks(br), bc = ta( + "MAKE_CAUSAL_CONSOLE_FROM_LOGGER_KEY_FOR_SES_AVA" +); +k[bc] = _c; +const wc = (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", i), s; -}, Vi = ":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{display:flex;flex-direction:column;position:fixed;inset-block-start:40px;inset-inline-end:320px;z-index:1000;padding:25px;border-radius:15px;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%}", Wi = ` + return t.addEventListener("mousedown", c), i; +}, Sc = ":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%}", Ec = ` `; -var ue, qt; -class qi extends HTMLElement { +var de, er; +class xc extends HTMLElement { constructor() { super(); - Fr(this, ue, null); - Fr(this, qt, null); + Gr(this, de, null); + Gr(this, er, null); this.attachShadow({ mode: "open" }); } setTheme(r) { - Se(this, ue) && Se(this, ue).setAttribute("data-theme", r); + Ee(this, de) && Ee(this, de).setAttribute("data-theme", r); } disconnectedCallback() { var r; - (r = Se(this, qt)) == null || r.call(this); + (r = Ee(this, er)) == 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"), a = Number(this.getAttribute("width") || "300"), s = Number(this.getAttribute("height") || "400"); + 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"); - Dr(this, ue, document.createElement("div")), Se(this, ue).classList.add("wrapper"), Se(this, ue).style.inlineSize = `${a}px`, Se(this, ue).style.blockSize = `${s}px`, Dr(this, qt, Hi(Se(this, ue))); + Br(this, de, document.createElement("div")), Ee(this, de).classList.add("wrapper"), Ee(this, de).style.inlineSize = `${o}px`, Ee(this, de).style.blockSize = `${a}px`, Br(this, er, wc(Ee(this, de), () => { + this.calculateZIndex(); + })); const i = document.createElement("div"); i.classList.add("header"); const c = document.createElement("h1"); c.textContent = r, i.appendChild(c); - const u = document.createElement("button"); - u.setAttribute("type", "button"), u.innerHTML = `
${Wi}
`, u.addEventListener("click", () => { + const l = document.createElement("button"); + l.setAttribute("type", "button"), l.innerHTML = `
${Ec}
`, l.addEventListener("click", () => { this.shadowRoot && this.shadowRoot.dispatchEvent( new CustomEvent("close", { composed: !0, bubbles: !0 }) ); - }), i.appendChild(u); - const l = document.createElement("iframe"); - l.src = n, l.allow = "", l.sandbox.add( + }), i.appendChild(l); + const u = document.createElement("iframe"); + u.src = n, u.allow = "", u.sandbox.add( "allow-scripts", "allow-forms", "allow-modals", @@ -4506,59 +4720,59 @@ class qi extends HTMLElement { "allow-popups-to-escape-sandbox", "allow-storage-access-by-user-activation" ), this.addEventListener("message", (f) => { - l.contentWindow && l.contentWindow.postMessage(f.detail, "*"); - }), this.shadowRoot.appendChild(Se(this, ue)), Se(this, ue).appendChild(i), Se(this, ue).appendChild(l); + u.contentWindow && u.contentWindow.postMessage(f.detail, "*"); + }), this.shadowRoot.appendChild(Ee(this, de)), Ee(this, de).appendChild(i), Ee(this, de).appendChild(u); const d = document.createElement("style"); - d.textContent = Vi, this.shadowRoot.appendChild(d); + d.textContent = Sc, this.shadowRoot.appendChild(d), this.calculateZIndex(); } } -ue = new WeakMap(), qt = new WeakMap(); -customElements.define("plugin-modal", qi); -var R; +de = new WeakMap(), er = new WeakMap(); +customElements.define("plugin-modal", xc); +var O; (function(t) { - t.assertEqual = (a) => a; - function e(a) { + t.assertEqual = (o) => o; + function e(o) { } t.assertIs = e; - function r(a) { + function r(o) { throw new Error(); } - t.assertNever = r, t.arrayToEnum = (a) => { - const s = {}; - for (const i of a) - s[i] = i; - return s; - }, t.getValidEnumValues = (a) => { - const s = t.objectKeys(a).filter((c) => typeof a[a[c]] != "number"), i = {}; - for (const c of s) - i[c] = a[c]; + 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 = (a) => t.objectKeys(a).map(function(s) { - return a[s]; - }), t.objectKeys = typeof Object.keys == "function" ? (a) => Object.keys(a) : (a) => { - const s = []; - for (const i in a) - Object.prototype.hasOwnProperty.call(a, i) && s.push(i); - return s; - }, t.find = (a, s) => { - for (const i of a) - if (s(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" ? (a) => Number.isInteger(a) : (a) => typeof a == "number" && isFinite(a) && Math.floor(a) === a; - function n(a, s = " | ") { - return a.map((i) => typeof i == "string" ? `'${i}'` : i).join(s); + }, 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 = (a, s) => typeof s == "bigint" ? s.toString() : s; -})(R || (R = {})); -var rn; + t.joinValues = n, t.jsonStringifyReplacer = (o, a) => typeof a == "bigint" ? a.toString() : a; +})(O || (O = {})); +var fn; (function(t) { t.mergeShapes = (e, r) => ({ ...e, ...r // second overwrites first }); -})(rn || (rn = {})); -const b = R.arrayToEnum([ +})(fn || (fn = {})); +const w = O.arrayToEnum([ "string", "nan", "number", @@ -4579,28 +4793,28 @@ const b = R.arrayToEnum([ "never", "map", "set" -]), Ge = (t) => { +]), Ve = (t) => { switch (typeof t) { case "undefined": - return b.undefined; + return w.undefined; case "string": - return b.string; + return w.string; case "number": - return isNaN(t) ? b.nan : b.number; + return isNaN(t) ? w.nan : w.number; case "boolean": - return b.boolean; + return w.boolean; case "function": - return b.function; + return w.function; case "bigint": - return b.bigint; + return w.bigint; case "symbol": - return b.symbol; + return w.symbol; case "object": - return Array.isArray(t) ? b.array : t === null ? b.null : t.then && typeof t.then == "function" && t.catch && typeof t.catch == "function" ? b.promise : typeof Map < "u" && t instanceof Map ? b.map : typeof Set < "u" && t instanceof Set ? b.set : typeof Date < "u" && t instanceof Date ? b.date : b.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 b.unknown; + return w.unknown; } -}, y = R.arrayToEnum([ +}, g = O.arrayToEnum([ "invalid_type", "invalid_literal", "custom", @@ -4617,8 +4831,8 @@ const b = R.arrayToEnum([ "invalid_intersection_types", "not_multiple_of", "not_finite" -]), Ki = (t) => JSON.stringify(t, null, 2).replace(/"([^"]+)":/g, "$1:"); -class xe extends Error { +]), kc = (t) => JSON.stringify(t, null, 2).replace(/"([^"]+)":/g, "$1:"); +class fe extends Error { constructor(e) { super(), this.issues = [], this.addIssue = (n) => { this.issues = [...this.issues, n]; @@ -4632,142 +4846,152 @@ class xe extends Error { return this.issues; } format(e) { - const r = e || function(s) { - return s.message; - }, n = { _errors: [] }, a = (s) => { - for (const i of s.issues) + 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(a); + i.unionErrors.map(o); else if (i.code === "invalid_return_type") - a(i.returnTypeError); + o(i.returnTypeError); else if (i.code === "invalid_arguments") - a(i.argumentsError); + o(i.argumentsError); else if (i.path.length === 0) n._errors.push(r(i)); else { - let c = n, u = 0; - for (; u < i.path.length; ) { - const l = i.path[u]; - u === i.path.length - 1 ? (c[l] = c[l] || { _errors: [] }, c[l]._errors.push(r(i))) : c[l] = c[l] || { _errors: [] }, c = c[l], u++; + 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 a(this), n; + return o(this), n; + } + static assert(e) { + if (!(e instanceof fe)) + throw new Error(`Not a ZodError: ${e}`); } toString() { return this.message; } get message() { - return JSON.stringify(this.issues, R.jsonStringifyReplacer, 2); + return JSON.stringify(this.issues, O.jsonStringifyReplacer, 2); } get isEmpty() { return this.issues.length === 0; } flatten(e = (r) => r.message) { const r = {}, n = []; - for (const a of this.issues) - a.path.length > 0 ? (r[a.path[0]] = r[a.path[0]] || [], r[a.path[0]].push(e(a))) : n.push(e(a)); + 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(); } } -xe.create = (t) => new xe(t); -const Lt = (t, e) => { +fe.create = (t) => new fe(t); +const Tt = (t, e) => { let r; switch (t.code) { - case y.invalid_type: - t.received === b.undefined ? r = "Required" : r = `Expected ${t.expected}, received ${t.received}`; + case g.invalid_type: + t.received === w.undefined ? r = "Required" : r = `Expected ${t.expected}, received ${t.received}`; break; - case y.invalid_literal: - r = `Invalid literal value, expected ${JSON.stringify(t.expected, R.jsonStringifyReplacer)}`; + case g.invalid_literal: + r = `Invalid literal value, expected ${JSON.stringify(t.expected, O.jsonStringifyReplacer)}`; break; - case y.unrecognized_keys: - r = `Unrecognized key(s) in object: ${R.joinValues(t.keys, ", ")}`; + case g.unrecognized_keys: + r = `Unrecognized key(s) in object: ${O.joinValues(t.keys, ", ")}`; break; - case y.invalid_union: + case g.invalid_union: r = "Invalid input"; break; - case y.invalid_union_discriminator: - r = `Invalid discriminator value. Expected ${R.joinValues(t.options)}`; + case g.invalid_union_discriminator: + r = `Invalid discriminator value. Expected ${O.joinValues(t.options)}`; break; - case y.invalid_enum_value: - r = `Invalid enum value. Expected ${R.joinValues(t.options)}, received '${t.received}'`; + case g.invalid_enum_value: + r = `Invalid enum value. Expected ${O.joinValues(t.options)}, received '${t.received}'`; break; - case y.invalid_arguments: + case g.invalid_arguments: r = "Invalid function arguments"; break; - case y.invalid_return_type: + case g.invalid_return_type: r = "Invalid function return type"; break; - case y.invalid_date: + case g.invalid_date: r = "Invalid date"; break; - case y.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}"` : R.assertNever(t.validation) : t.validation !== "regex" ? r = `Invalid ${t.validation}` : r = "Invalid"; + 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}"` : O.assertNever(t.validation) : t.validation !== "regex" ? r = `Invalid ${t.validation}` : r = "Invalid"; break; - case y.too_small: + 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 y.too_big: + 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 y.custom: + case g.custom: r = "Invalid input"; break; - case y.invalid_intersection_types: + case g.invalid_intersection_types: r = "Intersection results could not be merged"; break; - case y.not_multiple_of: + case g.not_multiple_of: r = `Number must be a multiple of ${t.multipleOf}`; break; - case y.not_finite: + case g.not_finite: r = "Number must be finite"; break; default: - r = e.defaultError, R.assertNever(t); + r = e.defaultError, O.assertNever(t); } return { message: r }; }; -let ms = Lt; -function Yi(t) { - ms = t; +let $s = Tt; +function Pc(t) { + $s = t; } -function hr() { - return ms; +function Er() { + return $s; } -const yr = (t) => { - const { data: e, path: r, errorMaps: n, issueData: a } = t, s = [...r, ...a.path || []], i = { - ...a, - path: s +const xr = (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 u = n.filter((l) => !!l).slice().reverse(); - for (const l of u) - c = l(i, { data: e, defaultError: c }).message; + const l = n.filter((u) => !!u).slice().reverse(); + for (const u of l) + c = u(i, { data: e, defaultError: c }).message; return { - ...a, - path: s, - message: a.message || c + ...o, + path: a, + message: c }; -}, Ji = []; -function S(t, e) { - const r = yr({ +}, Tc = []; +function b(t, e) { + const r = Er(), n = xr({ issueData: e, data: t.data, path: t.path, errorMaps: [ t.common.contextualErrorMap, t.schemaErrorMap, - hr(), - Lt + r, + r === Tt ? void 0 : Tt // then global default map - ].filter((n) => !!n) + ].filter((o) => !!o) }); - t.common.issues.push(r); + t.common.issues.push(n); } -class J { +class Q { constructor() { this.value = "valid"; } @@ -4779,50 +5003,63 @@ class J { } static mergeArray(e, r) { const n = []; - for (const a of r) { - if (a.status === "aborted") - return A; - a.status === "dirty" && e.dirty(), n.push(a.value); + for (const o of r) { + if (o.status === "aborted") + return I; + o.status === "dirty" && e.dirty(), n.push(o.value); } return { status: e.value, value: n }; } static async mergeObjectAsync(e, r) { const n = []; - for (const a of r) + for (const o of r) { + const a = await o.key, i = await o.value; n.push({ - key: await a.key, - value: await a.value + key: a, + value: i }); - return J.mergeObjectSync(e, n); + } + return Q.mergeObjectSync(e, n); } static mergeObjectSync(e, r) { const n = {}; - for (const a of r) { - const { key: s, value: i } = a; - if (s.status === "aborted" || i.status === "aborted") - return A; - s.status === "dirty" && e.dirty(), i.status === "dirty" && e.dirty(), s.value !== "__proto__" && (typeof i.value < "u" || a.alwaysSet) && (n[s.value] = i.value); + for (const o of r) { + const { key: a, value: i } = o; + if (a.status === "aborted" || i.status === "aborted") + return I; + 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 A = Object.freeze({ +const I = Object.freeze({ status: "aborted" -}), hs = (t) => ({ status: "dirty", value: t }), re = (t) => ({ status: "valid", value: t }), nn = (t) => t.status === "aborted", on = (t) => t.status === "dirty", Ft = (t) => t.status === "valid", gr = (t) => typeof Promise < "u" && t instanceof Promise; +}), wt = (t) => ({ status: "dirty", value: t }), ae = (t) => ({ status: "valid", value: t }), pn = (t) => t.status === "aborted", hn = (t) => t.status === "dirty", jt = (t) => t.status === "valid", Zt = (t) => typeof Promise < "u" && t instanceof Promise; +function kr(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 Ns(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 E; (function(t) { t.errToObj = (e) => typeof e == "string" ? { message: e } : e || {}, t.toString = (e) => typeof e == "string" ? e : e == null ? void 0 : e.message; })(E || (E = {})); -class Ne { - constructor(e, r, n, a) { - this._cachedPath = [], this.parent = e, this.data = r, this._path = n, this._key = a; +var Lt, Ft; +class Re { + 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 no = (t, e) => { - if (Ft(e)) +const vo = (t, e) => { + if (jt(e)) return { success: !0, data: e.value }; if (!t.common.issues.length) throw new Error("Validation failed but no issues detected."); @@ -4831,7 +5068,7 @@ const no = (t, e) => { get error() { if (this._error) return this._error; - const r = new xe(t.common.issues); + const r = new fe(t.common.issues); return this._error = r, this._error; } }; @@ -4839,12 +5076,16 @@ const no = (t, e) => { function C(t) { if (!t) return {}; - const { errorMap: e, invalid_type_error: r, required_error: n, description: a } = t; + 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: a } : { errorMap: (i, c) => i.code !== "invalid_type" ? { message: c.defaultError } : typeof c.data > "u" ? { message: n ?? c.defaultError } : { message: r ?? c.defaultError }, description: a }; + 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 N { +class $ { 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); } @@ -4852,13 +5093,13 @@ class N { return this._def.description; } _getType(e) { - return Ge(e.data); + return Ve(e.data); } _getOrReturnCtx(e, r) { return r || { common: e.parent.common, data: e.data, - parsedType: Ge(e.data), + parsedType: Ve(e.data), schemaErrorMap: this._def.errorMap, path: e.path, parent: e.parent @@ -4866,11 +5107,11 @@ class N { } _processInputParams(e) { return { - status: new J(), + status: new Q(), ctx: { common: e.parent.common, data: e.data, - parsedType: Ge(e.data), + parsedType: Ve(e.data), schemaErrorMap: this._def.errorMap, path: e.path, parent: e.parent @@ -4879,7 +5120,7 @@ class N { } _parseSync(e) { const r = this._parse(e); - if (gr(r)) + if (Zt(r)) throw new Error("Synchronous parse encountered promise."); return r; } @@ -4895,7 +5136,7 @@ class N { } safeParse(e, r) { var n; - const a = { + const o = { common: { issues: [], async: (n = r == null ? void 0 : r.async) !== null && n !== void 0 ? n : !1, @@ -4905,9 +5146,9 @@ class N { schemaErrorMap: this._def.errorMap, parent: null, data: e, - parsedType: Ge(e) - }, s = this._parseSync({ data: e, path: a.path, parent: a }); - return no(a, s); + parsedType: Ve(e) + }, a = this._parseSync({ data: e, path: o.path, parent: o }); + return vo(o, a); } async parseAsync(e, r) { const n = await this.safeParseAsync(e, r); @@ -4926,27 +5167,27 @@ class N { schemaErrorMap: this._def.errorMap, parent: null, data: e, - parsedType: Ge(e) - }, a = this._parse({ data: e, path: n.path, parent: n }), s = await (gr(a) ? a : Promise.resolve(a)); - return no(n, s); + parsedType: Ve(e) + }, o = this._parse({ data: e, path: n.path, parent: n }), a = await (Zt(o) ? o : Promise.resolve(o)); + return vo(n, a); } refine(e, r) { - const n = (a) => typeof r == "string" || typeof r > "u" ? { message: r } : typeof r == "function" ? r(a) : r; - return this._refinement((a, s) => { - const i = e(a), c = () => s.addIssue({ - code: y.custom, - ...n(a) + 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((u) => u ? !0 : (c(), !1)) : i ? !0 : (c(), !1); + 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, a) => e(n) ? !0 : (a.addIssue(typeof r == "function" ? r(n, a) : r), !1)); + return this._refinement((n, o) => e(n) ? !0 : (o.addIssue(typeof r == "function" ? r(n, o) : r), !1)); } _refinement(e) { - return new ke({ + return new Ae({ schema: this, - typeName: P.ZodEffects, + typeName: A.ZodEffects, effect: { type: "refinement", refinement: e } }); } @@ -4954,57 +5195,57 @@ class N { return this._refinement(e); } optional() { - return Fe.create(this, this._def); + return Ne.create(this, this._def); } nullable() { - return dt.create(this, this._def); + return tt.create(this, this._def); } nullish() { return this.nullable().optional(); } array() { - return Pe.create(this, this._def); + return Te.create(this, this._def); } promise() { - return xt.create(this, this._def); + return It.create(this, this._def); } or(e) { - return Zt.create([this, e], this._def); + return Ht.create([this, e], this._def); } and(e) { - return zt.create(this, e, this._def); + return Vt.create(this, e, this._def); } transform(e) { - return new ke({ + return new Ae({ ...C(this._def), schema: this, - typeName: P.ZodEffects, + typeName: A.ZodEffects, effect: { type: "transform", transform: e } }); } default(e) { const r = typeof e == "function" ? e : () => e; - return new Wt({ + return new Jt({ ...C(this._def), innerType: this, defaultValue: r, - typeName: P.ZodDefault + typeName: A.ZodDefault }); } brand() { - return new gs({ - typeName: P.ZodBranded, + return new Zn({ + typeName: A.ZodBranded, type: this, ...C(this._def) }); } catch(e) { const r = typeof e == "function" ? e : () => e; - return new wr({ + return new Xt({ ...C(this._def), innerType: this, catchValue: r, - typeName: P.ZodCatch + typeName: A.ZodCatch }); } describe(e) { @@ -5015,10 +5256,10 @@ class N { }); } pipe(e) { - return Qt.create(this, e); + return sr.create(this, e); } readonly() { - return Er.create(this); + return Qt.create(this); } isOptional() { return this.safeParse(void 0).success; @@ -5027,147 +5268,177 @@ class N { return this.safeParse(null).success; } } -const Xi = /^c[^\s-]{8,}$/i, Qi = /^[a-z][a-z0-9]*$/, ec = /^[0-9A-HJKMNP-TV-Z]{26}$/, tc = /^[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, rc = /^(?!\.)(?!.*\.\.)([A-Z0-9_+-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i, nc = "^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$"; -let Br; -const oc = /^(((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}))$/, sc = /^(([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})))$/, ac = (t) => t.precision ? t.offset ? new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{${t.precision}}(([+-]\\d{2}(:?\\d{2})?)|Z)$`) : new RegExp(`^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{${t.precision}}Z$`) : t.precision === 0 ? t.offset ? new RegExp("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(([+-]\\d{2}(:?\\d{2})?)|Z)$") : new RegExp("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$") : t.offset ? new RegExp("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?(([+-]\\d{2}(:?\\d{2})?)|Z)$") : new RegExp("^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$"); -function ic(t, e) { - return !!((e === "v4" || !e) && oc.test(t) || (e === "v6" || !e) && sc.test(t)); +const Ac = /^c[^\s-]{8,}$/i, Ic = /^[0-9a-z]+$/, Cc = /^[0-9A-HJKMNP-TV-Z]{26}$/, $c = /^[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, Nc = /^[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)?)??$/, Oc = /^(?!\.)(?!.*\.\.)([A-Z0-9_'+\-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i, Mc = "^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$"; +let en; +const Lc = /^(?:(?: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])$/, Fc = /^(([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})))$/, Dc = /^([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])))", Uc = new RegExp(`^${Rs}$`); +function Os(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; } -class Ee extends N { +function jc(t) { + return new RegExp(`^${Os(t)}$`); +} +function Ms(t) { + let e = `${Rs}T${Os(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 Zc(t, e) { + return !!((e === "v4" || !e) && Lc.test(t) || (e === "v6" || !e) && Fc.test(t)); +} +class ke extends $ { _parse(e) { - if (this._def.coerce && (e.data = String(e.data)), this._getType(e) !== b.string) { - const s = this._getOrReturnCtx(e); - return S( - s, - { - code: y.invalid_type, - expected: b.string, - received: s.parsedType - } - // - ), A; + if (this._def.coerce && (e.data = String(e.data)), this._getType(e) !== w.string) { + const a = this._getOrReturnCtx(e); + return b(a, { + code: g.invalid_type, + expected: w.string, + received: a.parsedType + }), I; } - const n = new J(); - let a; - for (const s of this._def.checks) - if (s.kind === "min") - e.data.length < s.value && (a = this._getOrReturnCtx(e, a), S(a, { - code: y.too_small, - minimum: s.value, + const n = new Q(); + let o; + for (const a of this._def.checks) + if (a.kind === "min") + e.data.length < a.value && (o = this._getOrReturnCtx(e, o), b(o, { + code: g.too_small, + minimum: a.value, type: "string", inclusive: !0, exact: !1, - message: s.message + message: a.message }), n.dirty()); - else if (s.kind === "max") - e.data.length > s.value && (a = this._getOrReturnCtx(e, a), S(a, { - code: y.too_big, - maximum: s.value, + else if (a.kind === "max") + e.data.length > a.value && (o = this._getOrReturnCtx(e, o), b(o, { + code: g.too_big, + maximum: a.value, type: "string", inclusive: !0, exact: !1, - message: s.message + message: a.message }), n.dirty()); - else if (s.kind === "length") { - const i = e.data.length > s.value, c = e.data.length < s.value; - (i || c) && (a = this._getOrReturnCtx(e, a), i ? S(a, { - code: y.too_big, - maximum: s.value, + 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 ? b(o, { + code: g.too_big, + maximum: a.value, type: "string", inclusive: !0, exact: !0, - message: s.message - }) : c && S(a, { - code: y.too_small, - minimum: s.value, + message: a.message + }) : c && b(o, { + code: g.too_small, + minimum: a.value, type: "string", inclusive: !0, exact: !0, - message: s.message + message: a.message }), n.dirty()); - } else if (s.kind === "email") - rc.test(e.data) || (a = this._getOrReturnCtx(e, a), S(a, { + } else if (a.kind === "email") + Oc.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { validation: "email", - code: y.invalid_string, - message: s.message + code: g.invalid_string, + message: a.message }), n.dirty()); - else if (s.kind === "emoji") - Br || (Br = new RegExp(nc, "u")), Br.test(e.data) || (a = this._getOrReturnCtx(e, a), S(a, { + else if (a.kind === "emoji") + en || (en = new RegExp(Mc, "u")), en.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { validation: "emoji", - code: y.invalid_string, - message: s.message + code: g.invalid_string, + message: a.message }), n.dirty()); - else if (s.kind === "uuid") - tc.test(e.data) || (a = this._getOrReturnCtx(e, a), S(a, { + else if (a.kind === "uuid") + $c.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { validation: "uuid", - code: y.invalid_string, - message: s.message + code: g.invalid_string, + message: a.message }), n.dirty()); - else if (s.kind === "cuid") - Xi.test(e.data) || (a = this._getOrReturnCtx(e, a), S(a, { + else if (a.kind === "nanoid") + Nc.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { + validation: "nanoid", + code: g.invalid_string, + message: a.message + }), n.dirty()); + else if (a.kind === "cuid") + Ac.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { validation: "cuid", - code: y.invalid_string, - message: s.message + code: g.invalid_string, + message: a.message }), n.dirty()); - else if (s.kind === "cuid2") - Qi.test(e.data) || (a = this._getOrReturnCtx(e, a), S(a, { + else if (a.kind === "cuid2") + Ic.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { validation: "cuid2", - code: y.invalid_string, - message: s.message + code: g.invalid_string, + message: a.message }), n.dirty()); - else if (s.kind === "ulid") - ec.test(e.data) || (a = this._getOrReturnCtx(e, a), S(a, { + else if (a.kind === "ulid") + Cc.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { validation: "ulid", - code: y.invalid_string, - message: s.message + code: g.invalid_string, + message: a.message }), n.dirty()); - else if (s.kind === "url") + else if (a.kind === "url") try { new URL(e.data); } catch { - a = this._getOrReturnCtx(e, a), S(a, { + o = this._getOrReturnCtx(e, o), b(o, { validation: "url", - code: y.invalid_string, - message: s.message + code: g.invalid_string, + message: a.message }), n.dirty(); } else - s.kind === "regex" ? (s.regex.lastIndex = 0, s.regex.test(e.data) || (a = this._getOrReturnCtx(e, a), S(a, { + a.kind === "regex" ? (a.regex.lastIndex = 0, a.regex.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { validation: "regex", - code: y.invalid_string, - message: s.message - }), n.dirty())) : s.kind === "trim" ? e.data = e.data.trim() : s.kind === "includes" ? e.data.includes(s.value, s.position) || (a = this._getOrReturnCtx(e, a), S(a, { - code: y.invalid_string, - validation: { includes: s.value, position: s.position }, - message: s.message - }), n.dirty()) : s.kind === "toLowerCase" ? e.data = e.data.toLowerCase() : s.kind === "toUpperCase" ? e.data = e.data.toUpperCase() : s.kind === "startsWith" ? e.data.startsWith(s.value) || (a = this._getOrReturnCtx(e, a), S(a, { - code: y.invalid_string, - validation: { startsWith: s.value }, - message: s.message - }), n.dirty()) : s.kind === "endsWith" ? e.data.endsWith(s.value) || (a = this._getOrReturnCtx(e, a), S(a, { - code: y.invalid_string, - validation: { endsWith: s.value }, - message: s.message - }), n.dirty()) : s.kind === "datetime" ? ac(s).test(e.data) || (a = this._getOrReturnCtx(e, a), S(a, { - code: y.invalid_string, + 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), b(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), b(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), b(o, { + code: g.invalid_string, + validation: { endsWith: a.value }, + message: a.message + }), n.dirty()) : a.kind === "datetime" ? Ms(a).test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { + code: g.invalid_string, validation: "datetime", - message: s.message - }), n.dirty()) : s.kind === "ip" ? ic(e.data, s.version) || (a = this._getOrReturnCtx(e, a), S(a, { + message: a.message + }), n.dirty()) : a.kind === "date" ? Uc.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { + code: g.invalid_string, + validation: "date", + message: a.message + }), n.dirty()) : a.kind === "time" ? jc(a).test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { + code: g.invalid_string, + validation: "time", + message: a.message + }), n.dirty()) : a.kind === "duration" ? Rc.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { + validation: "duration", + code: g.invalid_string, + message: a.message + }), n.dirty()) : a.kind === "ip" ? Zc(e.data, a.version) || (o = this._getOrReturnCtx(e, o), b(o, { validation: "ip", - code: y.invalid_string, - message: s.message - }), n.dirty()) : R.assertNever(s); + code: g.invalid_string, + message: a.message + }), n.dirty()) : a.kind === "base64" ? Dc.test(e.data) || (o = this._getOrReturnCtx(e, o), b(o, { + validation: "base64", + code: g.invalid_string, + message: a.message + }), n.dirty()) : O.assertNever(a); return { status: n.value, value: e.data }; } _regex(e, r, n) { - return this.refinement((a) => e.test(a), { + return this.refinement((o) => e.test(o), { validation: r, - code: y.invalid_string, + code: g.invalid_string, ...E.errToObj(n) }); } _addCheck(e) { - return new Ee({ + return new ke({ ...this._def, checks: [...this._def.checks, e] }); @@ -5184,6 +5455,9 @@ class Ee extends N { uuid(e) { return this._addCheck({ kind: "uuid", ...E.errToObj(e) }); } + nanoid(e) { + return this._addCheck({ kind: "nanoid", ...E.errToObj(e) }); + } cuid(e) { return this._addCheck({ kind: "cuid", ...E.errToObj(e) }); } @@ -5193,23 +5467,45 @@ class Ee extends N { ulid(e) { return this._addCheck({ kind: "ulid", ...E.errToObj(e) }); } + base64(e) { + return this._addCheck({ kind: "base64", ...E.errToObj(e) }); + } ip(e) { return this._addCheck({ kind: "ip", ...E.errToObj(e) }); } datetime(e) { - var r; + 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, ...E.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, + ...E.errToObj(e == null ? void 0 : e.message) + }); + } + duration(e) { + return this._addCheck({ kind: "duration", ...E.errToObj(e) }); + } regex(e, r) { return this._addCheck({ kind: "regex", @@ -5268,19 +5564,19 @@ class Ee extends N { return this.min(1, E.errToObj(e)); } trim() { - return new Ee({ + return new ke({ ...this._def, checks: [...this._def.checks, { kind: "trim" }] }); } toLowerCase() { - return new Ee({ + return new ke({ ...this._def, checks: [...this._def.checks, { kind: "toLowerCase" }] }); } toUpperCase() { - return new Ee({ + return new ke({ ...this._def, checks: [...this._def.checks, { kind: "toUpperCase" }] }); @@ -5288,6 +5584,15 @@ class Ee extends N { 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"); } @@ -5300,6 +5605,9 @@ class Ee extends N { 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"); } @@ -5312,6 +5620,9 @@ class Ee extends N { 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) @@ -5325,63 +5636,63 @@ class Ee extends N { return e; } } -Ee.create = (t) => { +ke.create = (t) => { var e; - return new Ee({ + return new ke({ checks: [], - typeName: P.ZodString, + typeName: A.ZodString, coerce: (e = t == null ? void 0 : t.coerce) !== null && e !== void 0 ? e : !1, ...C(t) }); }; -function cc(t, e) { - const r = (t.toString().split(".")[1] || "").length, n = (e.toString().split(".")[1] || "").length, a = r > n ? r : n, s = parseInt(t.toFixed(a).replace(".", "")), i = parseInt(e.toFixed(a).replace(".", "")); - return s % i / Math.pow(10, a); +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 qe extends N { +class Xe extends $ { 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) !== b.number) { - const s = this._getOrReturnCtx(e); - return S(s, { - code: y.invalid_type, - expected: b.number, - received: s.parsedType - }), A; + if (this._def.coerce && (e.data = Number(e.data)), this._getType(e) !== w.number) { + const a = this._getOrReturnCtx(e); + return b(a, { + code: g.invalid_type, + expected: w.number, + received: a.parsedType + }), I; } let n; - const a = new J(); - for (const s of this._def.checks) - s.kind === "int" ? R.isInteger(e.data) || (n = this._getOrReturnCtx(e, n), S(n, { - code: y.invalid_type, + const o = new Q(); + for (const a of this._def.checks) + a.kind === "int" ? O.isInteger(e.data) || (n = this._getOrReturnCtx(e, n), b(n, { + code: g.invalid_type, expected: "integer", received: "float", - message: s.message - }), a.dirty()) : s.kind === "min" ? (s.inclusive ? e.data < s.value : e.data <= s.value) && (n = this._getOrReturnCtx(e, n), S(n, { - code: y.too_small, - minimum: s.value, + message: a.message + }), o.dirty()) : a.kind === "min" ? (a.inclusive ? e.data < a.value : e.data <= a.value) && (n = this._getOrReturnCtx(e, n), b(n, { + code: g.too_small, + minimum: a.value, type: "number", - inclusive: s.inclusive, + inclusive: a.inclusive, exact: !1, - message: s.message - }), a.dirty()) : s.kind === "max" ? (s.inclusive ? e.data > s.value : e.data >= s.value) && (n = this._getOrReturnCtx(e, n), S(n, { - code: y.too_big, - maximum: s.value, + message: a.message + }), o.dirty()) : a.kind === "max" ? (a.inclusive ? e.data > a.value : e.data >= a.value) && (n = this._getOrReturnCtx(e, n), b(n, { + code: g.too_big, + maximum: a.value, type: "number", - inclusive: s.inclusive, + inclusive: a.inclusive, exact: !1, - message: s.message - }), a.dirty()) : s.kind === "multipleOf" ? cc(e.data, s.value) !== 0 && (n = this._getOrReturnCtx(e, n), S(n, { - code: y.not_multiple_of, - multipleOf: s.value, - message: s.message - }), a.dirty()) : s.kind === "finite" ? Number.isFinite(e.data) || (n = this._getOrReturnCtx(e, n), S(n, { - code: y.not_finite, - message: s.message - }), a.dirty()) : R.assertNever(s); - return { status: a.value, value: e.data }; + message: a.message + }), o.dirty()) : a.kind === "multipleOf" ? zc(e.data, a.value) !== 0 && (n = this._getOrReturnCtx(e, n), b(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), b(n, { + code: g.not_finite, + message: a.message + }), o.dirty()) : O.assertNever(a); + return { status: o.value, value: e.data }; } gte(e, r) { return this.setLimit("min", e, !0, E.toString(r)); @@ -5395,8 +5706,8 @@ class qe extends N { lt(e, r) { return this.setLimit("max", e, !1, E.toString(r)); } - setLimit(e, r, n, a) { - return new qe({ + setLimit(e, r, n, o) { + return new Xe({ ...this._def, checks: [ ...this._def.checks, @@ -5404,13 +5715,13 @@ class qe extends N { kind: e, value: r, inclusive: n, - message: E.toString(a) + message: E.toString(o) } ] }); } _addCheck(e) { - return new qe({ + return new Xe({ ...this._def, checks: [...this._def.checks, e] }); @@ -5492,7 +5803,7 @@ class qe extends N { return e; } get isInt() { - return !!this._def.checks.find((e) => e.kind === "int" || e.kind === "multipleOf" && R.isInteger(e.value)); + return !!this._def.checks.find((e) => e.kind === "int" || e.kind === "multipleOf" && O.isInteger(e.value)); } get isFinite() { let e = null, r = null; @@ -5504,46 +5815,46 @@ class qe extends N { return Number.isFinite(r) && Number.isFinite(e); } } -qe.create = (t) => new qe({ +Xe.create = (t) => new Xe({ checks: [], - typeName: P.ZodNumber, + typeName: A.ZodNumber, coerce: (t == null ? void 0 : t.coerce) || !1, ...C(t) }); -class Ke extends N { +class Qe extends $ { 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) !== b.bigint) { - const s = this._getOrReturnCtx(e); - return S(s, { - code: y.invalid_type, - expected: b.bigint, - received: s.parsedType - }), A; + if (this._def.coerce && (e.data = BigInt(e.data)), this._getType(e) !== w.bigint) { + const a = this._getOrReturnCtx(e); + return b(a, { + code: g.invalid_type, + expected: w.bigint, + received: a.parsedType + }), I; } let n; - const a = new J(); - for (const s of this._def.checks) - s.kind === "min" ? (s.inclusive ? e.data < s.value : e.data <= s.value) && (n = this._getOrReturnCtx(e, n), S(n, { - code: y.too_small, + const o = new Q(); + 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), b(n, { + code: g.too_small, type: "bigint", - minimum: s.value, - inclusive: s.inclusive, - message: s.message - }), a.dirty()) : s.kind === "max" ? (s.inclusive ? e.data > s.value : e.data >= s.value) && (n = this._getOrReturnCtx(e, n), S(n, { - code: y.too_big, + 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), b(n, { + code: g.too_big, type: "bigint", - maximum: s.value, - inclusive: s.inclusive, - message: s.message - }), a.dirty()) : s.kind === "multipleOf" ? e.data % s.value !== BigInt(0) && (n = this._getOrReturnCtx(e, n), S(n, { - code: y.not_multiple_of, - multipleOf: s.value, - message: s.message - }), a.dirty()) : R.assertNever(s); - return { status: a.value, value: e.data }; + 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), b(n, { + code: g.not_multiple_of, + multipleOf: a.value, + message: a.message + }), o.dirty()) : O.assertNever(a); + return { status: o.value, value: e.data }; } gte(e, r) { return this.setLimit("min", e, !0, E.toString(r)); @@ -5557,8 +5868,8 @@ class Ke extends N { lt(e, r) { return this.setLimit("max", e, !1, E.toString(r)); } - setLimit(e, r, n, a) { - return new Ke({ + setLimit(e, r, n, o) { + return new Qe({ ...this._def, checks: [ ...this._def.checks, @@ -5566,13 +5877,13 @@ class Ke extends N { kind: e, value: r, inclusive: n, - message: E.toString(a) + message: E.toString(o) } ] }); } _addCheck(e) { - return new Ke({ + return new Qe({ ...this._def, checks: [...this._def.checks, e] }); @@ -5629,74 +5940,74 @@ class Ke extends N { return e; } } -Ke.create = (t) => { +Qe.create = (t) => { var e; - return new Ke({ + return new Qe({ checks: [], - typeName: P.ZodBigInt, + typeName: A.ZodBigInt, coerce: (e = t == null ? void 0 : t.coerce) !== null && e !== void 0 ? e : !1, ...C(t) }); }; -class Dt extends N { +class zt extends $ { _parse(e) { - if (this._def.coerce && (e.data = !!e.data), this._getType(e) !== b.boolean) { + if (this._def.coerce && (e.data = !!e.data), this._getType(e) !== w.boolean) { const n = this._getOrReturnCtx(e); - return S(n, { - code: y.invalid_type, - expected: b.boolean, + return b(n, { + code: g.invalid_type, + expected: w.boolean, received: n.parsedType - }), A; + }), I; } - return re(e.data); + return ae(e.data); } } -Dt.create = (t) => new Dt({ - typeName: P.ZodBoolean, +zt.create = (t) => new zt({ + typeName: A.ZodBoolean, coerce: (t == null ? void 0 : t.coerce) || !1, ...C(t) }); -class lt extends N { +class pt extends $ { _parse(e) { - if (this._def.coerce && (e.data = new Date(e.data)), this._getType(e) !== b.date) { - const s = this._getOrReturnCtx(e); - return S(s, { - code: y.invalid_type, - expected: b.date, - received: s.parsedType - }), A; + if (this._def.coerce && (e.data = new Date(e.data)), this._getType(e) !== w.date) { + const a = this._getOrReturnCtx(e); + return b(a, { + code: g.invalid_type, + expected: w.date, + received: a.parsedType + }), I; } if (isNaN(e.data.getTime())) { - const s = this._getOrReturnCtx(e); - return S(s, { - code: y.invalid_date - }), A; + const a = this._getOrReturnCtx(e); + return b(a, { + code: g.invalid_date + }), I; } - const n = new J(); - let a; - for (const s of this._def.checks) - s.kind === "min" ? e.data.getTime() < s.value && (a = this._getOrReturnCtx(e, a), S(a, { - code: y.too_small, - message: s.message, + const n = new Q(); + let o; + for (const a of this._def.checks) + a.kind === "min" ? e.data.getTime() < a.value && (o = this._getOrReturnCtx(e, o), b(o, { + code: g.too_small, + message: a.message, inclusive: !0, exact: !1, - minimum: s.value, + minimum: a.value, type: "date" - }), n.dirty()) : s.kind === "max" ? e.data.getTime() > s.value && (a = this._getOrReturnCtx(e, a), S(a, { - code: y.too_big, - message: s.message, + }), n.dirty()) : a.kind === "max" ? e.data.getTime() > a.value && (o = this._getOrReturnCtx(e, o), b(o, { + code: g.too_big, + message: a.message, inclusive: !0, exact: !1, - maximum: s.value, + maximum: a.value, type: "date" - }), n.dirty()) : R.assertNever(s); + }), n.dirty()) : O.assertNever(a); return { status: n.value, value: new Date(e.data.getTime()) }; } _addCheck(e) { - return new lt({ + return new pt({ ...this._def, checks: [...this._def.checks, e] }); @@ -5728,175 +6039,175 @@ class lt extends N { return e != null ? new Date(e) : null; } } -lt.create = (t) => new lt({ +pt.create = (t) => new pt({ checks: [], coerce: (t == null ? void 0 : t.coerce) || !1, - typeName: P.ZodDate, + typeName: A.ZodDate, ...C(t) }); -class vr extends N { +class Pr extends $ { _parse(e) { - if (this._getType(e) !== b.symbol) { + if (this._getType(e) !== w.symbol) { const n = this._getOrReturnCtx(e); - return S(n, { - code: y.invalid_type, - expected: b.symbol, + return b(n, { + code: g.invalid_type, + expected: w.symbol, received: n.parsedType - }), A; + }), I; } - return re(e.data); + return ae(e.data); } } -vr.create = (t) => new vr({ - typeName: P.ZodSymbol, +Pr.create = (t) => new Pr({ + typeName: A.ZodSymbol, ...C(t) }); -class Ut extends N { +class Gt extends $ { _parse(e) { - if (this._getType(e) !== b.undefined) { + if (this._getType(e) !== w.undefined) { const n = this._getOrReturnCtx(e); - return S(n, { - code: y.invalid_type, - expected: b.undefined, + return b(n, { + code: g.invalid_type, + expected: w.undefined, received: n.parsedType - }), A; + }), I; } - return re(e.data); + return ae(e.data); } } -Ut.create = (t) => new Ut({ - typeName: P.ZodUndefined, +Gt.create = (t) => new Gt({ + typeName: A.ZodUndefined, ...C(t) }); -class jt extends N { +class Bt extends $ { _parse(e) { - if (this._getType(e) !== b.null) { + if (this._getType(e) !== w.null) { const n = this._getOrReturnCtx(e); - return S(n, { - code: y.invalid_type, - expected: b.null, + return b(n, { + code: g.invalid_type, + expected: w.null, received: n.parsedType - }), A; + }), I; } - return re(e.data); + return ae(e.data); } } -jt.create = (t) => new jt({ - typeName: P.ZodNull, +Bt.create = (t) => new Bt({ + typeName: A.ZodNull, ...C(t) }); -class Et extends N { +class At extends $ { constructor() { super(...arguments), this._any = !0; } _parse(e) { - return re(e.data); + return ae(e.data); } } -Et.create = (t) => new Et({ - typeName: P.ZodAny, +At.create = (t) => new At({ + typeName: A.ZodAny, ...C(t) }); -class at extends N { +class dt extends $ { constructor() { super(...arguments), this._unknown = !0; } _parse(e) { - return re(e.data); + return ae(e.data); } } -at.create = (t) => new at({ - typeName: P.ZodUnknown, +dt.create = (t) => new dt({ + typeName: A.ZodUnknown, ...C(t) }); -class Ue extends N { +class je extends $ { _parse(e) { const r = this._getOrReturnCtx(e); - return S(r, { - code: y.invalid_type, - expected: b.never, + return b(r, { + code: g.invalid_type, + expected: w.never, received: r.parsedType - }), A; + }), I; } } -Ue.create = (t) => new Ue({ - typeName: P.ZodNever, +je.create = (t) => new je({ + typeName: A.ZodNever, ...C(t) }); -class _r extends N { +class Tr extends $ { _parse(e) { - if (this._getType(e) !== b.undefined) { + if (this._getType(e) !== w.undefined) { const n = this._getOrReturnCtx(e); - return S(n, { - code: y.invalid_type, - expected: b.void, + return b(n, { + code: g.invalid_type, + expected: w.void, received: n.parsedType - }), A; + }), I; } - return re(e.data); + return ae(e.data); } } -_r.create = (t) => new _r({ - typeName: P.ZodVoid, +Tr.create = (t) => new Tr({ + typeName: A.ZodVoid, ...C(t) }); -class Pe extends N { +class Te extends $ { _parse(e) { - const { ctx: r, status: n } = this._processInputParams(e), a = this._def; - if (r.parsedType !== b.array) - return S(r, { - code: y.invalid_type, - expected: b.array, + const { ctx: r, status: n } = this._processInputParams(e), o = this._def; + if (r.parsedType !== w.array) + return b(r, { + code: g.invalid_type, + expected: w.array, received: r.parsedType - }), A; - if (a.exactLength !== null) { - const i = r.data.length > a.exactLength.value, c = r.data.length < a.exactLength.value; - (i || c) && (S(r, { - code: i ? y.too_big : y.too_small, - minimum: c ? a.exactLength.value : void 0, - maximum: i ? a.exactLength.value : void 0, + }), I; + if (o.exactLength !== null) { + const i = r.data.length > o.exactLength.value, c = r.data.length < o.exactLength.value; + (i || c) && (b(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: a.exactLength.message + message: o.exactLength.message }), n.dirty()); } - if (a.minLength !== null && r.data.length < a.minLength.value && (S(r, { - code: y.too_small, - minimum: a.minLength.value, + if (o.minLength !== null && r.data.length < o.minLength.value && (b(r, { + code: g.too_small, + minimum: o.minLength.value, type: "array", inclusive: !0, exact: !1, - message: a.minLength.message - }), n.dirty()), a.maxLength !== null && r.data.length > a.maxLength.value && (S(r, { - code: y.too_big, - maximum: a.maxLength.value, + message: o.minLength.message + }), n.dirty()), o.maxLength !== null && r.data.length > o.maxLength.value && (b(r, { + code: g.too_big, + maximum: o.maxLength.value, type: "array", inclusive: !0, exact: !1, - message: a.maxLength.message + message: o.maxLength.message }), n.dirty()), r.common.async) - return Promise.all([...r.data].map((i, c) => a.type._parseAsync(new Ne(r, i, r.path, c)))).then((i) => J.mergeArray(n, i)); - const s = [...r.data].map((i, c) => a.type._parseSync(new Ne(r, i, r.path, c))); - return J.mergeArray(n, s); + return Promise.all([...r.data].map((i, c) => o.type._parseAsync(new Re(r, i, r.path, c)))).then((i) => Q.mergeArray(n, i)); + const a = [...r.data].map((i, c) => o.type._parseSync(new Re(r, i, r.path, c))); + return Q.mergeArray(n, a); } get element() { return this._def.type; } min(e, r) { - return new Pe({ + return new Te({ ...this._def, minLength: { value: e, message: E.toString(r) } }); } max(e, r) { - return new Pe({ + return new Te({ ...this._def, maxLength: { value: e, message: E.toString(r) } }); } length(e, r) { - return new Pe({ + return new Te({ ...this._def, exactLength: { value: e, message: E.toString(r) } }); @@ -5905,104 +6216,104 @@ class Pe extends N { return this.min(1, e); } } -Pe.create = (t, e) => new Pe({ +Te.create = (t, e) => new Te({ type: t, minLength: null, maxLength: null, exactLength: null, - typeName: P.ZodArray, + typeName: A.ZodArray, ...C(e) }); -function gt(t) { +function _t(t) { if (t instanceof U) { const e = {}; for (const r in t.shape) { const n = t.shape[r]; - e[r] = Fe.create(gt(n)); + e[r] = Ne.create(_t(n)); } return new U({ ...t._def, shape: () => e }); } else - return t instanceof Pe ? new Pe({ + return t instanceof Te ? new Te({ ...t._def, - type: gt(t.element) - }) : t instanceof Fe ? Fe.create(gt(t.unwrap())) : t instanceof dt ? dt.create(gt(t.unwrap())) : t instanceof Oe ? Oe.create(t.items.map((e) => gt(e))) : t; + type: _t(t.element) + }) : t instanceof Ne ? Ne.create(_t(t.unwrap())) : t instanceof tt ? tt.create(_t(t.unwrap())) : t instanceof Oe ? Oe.create(t.items.map((e) => _t(e))) : t; } -class U extends N { +class U extends $ { 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 = R.objectKeys(e); + const e = this._def.shape(), r = O.objectKeys(e); return this._cached = { shape: e, keys: r }; } _parse(e) { - if (this._getType(e) !== b.object) { - const l = this._getOrReturnCtx(e); - return S(l, { - code: y.invalid_type, - expected: b.object, - received: l.parsedType - }), A; + if (this._getType(e) !== w.object) { + const u = this._getOrReturnCtx(e); + return b(u, { + code: g.invalid_type, + expected: w.object, + received: u.parsedType + }), I; } - const { status: n, ctx: a } = this._processInputParams(e), { shape: s, keys: i } = this._getCached(), c = []; - if (!(this._def.catchall instanceof Ue && this._def.unknownKeys === "strip")) - for (const l in a.data) - i.includes(l) || c.push(l); - const u = []; - for (const l of i) { - const d = s[l], f = a.data[l]; - u.push({ - key: { status: "valid", value: l }, - value: d._parse(new Ne(a, f, a.path, l)), - alwaysSet: l in a.data + const { status: n, ctx: o } = this._processInputParams(e), { shape: a, keys: i } = this._getCached(), c = []; + if (!(this._def.catchall instanceof je && 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 Re(o, f, o.path, u)), + alwaysSet: u in o.data }); } - if (this._def.catchall instanceof Ue) { - const l = this._def.unknownKeys; - if (l === "passthrough") + if (this._def.catchall instanceof je) { + const u = this._def.unknownKeys; + if (u === "passthrough") for (const d of c) - u.push({ + l.push({ key: { status: "valid", value: d }, - value: { status: "valid", value: a.data[d] } + value: { status: "valid", value: o.data[d] } }); - else if (l === "strict") - c.length > 0 && (S(a, { - code: y.unrecognized_keys, + else if (u === "strict") + c.length > 0 && (b(o, { + code: g.unrecognized_keys, keys: c }), n.dirty()); - else if (l !== "strip") + else if (u !== "strip") throw new Error("Internal ZodObject error: invalid unknownKeys value."); } else { - const l = this._def.catchall; + const u = this._def.catchall; for (const d of c) { - const f = a.data[d]; - u.push({ + const f = o.data[d]; + l.push({ key: { status: "valid", value: d }, - value: l._parse( - new Ne(a, f, a.path, d) + value: u._parse( + new Re(o, f, o.path, d) //, ctx.child(key), value, getParsedType(value) ), - alwaysSet: d in a.data + alwaysSet: d in o.data }); } } - return a.common.async ? Promise.resolve().then(async () => { - const l = []; - for (const d of u) { - const f = await d.key; - l.push({ + 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: await d.value, + value: h, alwaysSet: d.alwaysSet }); } - return l; - }).then((l) => J.mergeObjectSync(n, l)) : J.mergeObjectSync(n, u); + return u; + }).then((u) => Q.mergeObjectSync(n, u)) : Q.mergeObjectSync(n, l); } get shape() { return this._def.shape(); @@ -6013,12 +6324,12 @@ class U extends N { unknownKeys: "strict", ...e !== void 0 ? { errorMap: (r, n) => { - var a, s, i, c; - const u = (i = (s = (a = this._def).errorMap) === null || s === void 0 ? void 0 : s.call(a, r, n).message) !== null && i !== void 0 ? i : n.defaultError; + 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 = E.errToObj(e).message) !== null && c !== void 0 ? c : u + message: (c = E.errToObj(e).message) !== null && c !== void 0 ? c : l } : { - message: u + message: l }; } } : {} @@ -6075,7 +6386,7 @@ class U extends N { ...this._def.shape(), ...e._def.shape() }), - typeName: P.ZodObject + typeName: A.ZodObject }); } // merge< @@ -6145,7 +6456,7 @@ class U extends N { } pick(e) { const r = {}; - return R.objectKeys(e).forEach((n) => { + return O.objectKeys(e).forEach((n) => { e[n] && this.shape[n] && (r[n] = this.shape[n]); }), new U({ ...this._def, @@ -6154,7 +6465,7 @@ class U extends N { } omit(e) { const r = {}; - return R.objectKeys(this.shape).forEach((n) => { + return O.objectKeys(this.shape).forEach((n) => { e[n] || (r[n] = this.shape[n]); }), new U({ ...this._def, @@ -6165,13 +6476,13 @@ class U extends N { * @deprecated */ deepPartial() { - return gt(this); + return _t(this); } partial(e) { const r = {}; - return R.objectKeys(this.shape).forEach((n) => { - const a = this.shape[n]; - e && !e[n] ? r[n] = a : r[n] = a.optional(); + return O.objectKeys(this.shape).forEach((n) => { + const o = this.shape[n]; + e && !e[n] ? r[n] = o : r[n] = o.optional(); }), new U({ ...this._def, shape: () => r @@ -6179,14 +6490,14 @@ class U extends N { } required(e) { const r = {}; - return R.objectKeys(this.shape).forEach((n) => { + return O.objectKeys(this.shape).forEach((n) => { if (e && !e[n]) r[n] = this.shape[n]; else { - let s = this.shape[n]; - for (; s instanceof Fe; ) - s = s._def.innerType; - r[n] = s; + let a = this.shape[n]; + for (; a instanceof Ne; ) + a = a._def.innerType; + r[n] = a; } }), new U({ ...this._def, @@ -6194,48 +6505,48 @@ class U extends N { }); } keyof() { - return ys(R.objectKeys(this.shape)); + return Ls(O.objectKeys(this.shape)); } } U.create = (t, e) => new U({ shape: () => t, unknownKeys: "strip", - catchall: Ue.create(), - typeName: P.ZodObject, + catchall: je.create(), + typeName: A.ZodObject, ...C(e) }); U.strictCreate = (t, e) => new U({ shape: () => t, unknownKeys: "strict", - catchall: Ue.create(), - typeName: P.ZodObject, + catchall: je.create(), + typeName: A.ZodObject, ...C(e) }); U.lazycreate = (t, e) => new U({ shape: t, unknownKeys: "strip", - catchall: Ue.create(), - typeName: P.ZodObject, + catchall: je.create(), + typeName: A.ZodObject, ...C(e) }); -class Zt extends N { +class Ht extends $ { _parse(e) { const { ctx: r } = this._processInputParams(e), n = this._def.options; - function a(s) { - for (const c of s) + function o(a) { + for (const c of a) if (c.result.status === "valid") return c.result; - for (const c of s) + for (const c of a) if (c.result.status === "dirty") return r.common.issues.push(...c.ctx.common.issues), c.result; - const i = s.map((c) => new xe(c.ctx.common.issues)); - return S(r, { - code: y.invalid_union, + const i = a.map((c) => new fe(c.ctx.common.issues)); + return b(r, { + code: g.invalid_union, unionErrors: i - }), A; + }), I; } if (r.common.async) - return Promise.all(n.map(async (s) => { + return Promise.all(n.map(async (a) => { const i = { ...r, common: { @@ -6245,76 +6556,76 @@ class Zt extends N { parent: null }; return { - result: await s._parseAsync({ + result: await a._parseAsync({ data: r.data, path: r.path, parent: i }), ctx: i }; - })).then(a); + })).then(o); { - let s; + let a; const i = []; - for (const u of n) { - const l = { + for (const l of n) { + const u = { ...r, common: { ...r.common, issues: [] }, parent: null - }, d = u._parseSync({ + }, d = l._parseSync({ data: r.data, path: r.path, - parent: l + parent: u }); if (d.status === "valid") return d; - d.status === "dirty" && !s && (s = { result: d, ctx: l }), l.common.issues.length && i.push(l.common.issues); + d.status === "dirty" && !a && (a = { result: d, ctx: u }), u.common.issues.length && i.push(u.common.issues); } - if (s) - return r.common.issues.push(...s.ctx.common.issues), s.result; - const c = i.map((u) => new xe(u)); - return S(r, { - code: y.invalid_union, + if (a) + return r.common.issues.push(...a.ctx.common.issues), a.result; + const c = i.map((l) => new fe(l)); + return b(r, { + code: g.invalid_union, unionErrors: c - }), A; + }), I; } } get options() { return this._def.options; } } -Zt.create = (t, e) => new Zt({ +Ht.create = (t, e) => new Ht({ options: t, - typeName: P.ZodUnion, + typeName: A.ZodUnion, ...C(e) }); -const cr = (t) => t instanceof Bt ? cr(t.schema) : t instanceof ke ? cr(t.innerType()) : t instanceof Ht ? [t.value] : t instanceof Ye ? t.options : t instanceof Vt ? Object.keys(t.enum) : t instanceof Wt ? cr(t._def.innerType) : t instanceof Ut ? [void 0] : t instanceof jt ? [null] : null; -class Mr extends N { +const Fe = (t) => t instanceof qt ? Fe(t.schema) : t instanceof Ae ? Fe(t.innerType()) : t instanceof Kt ? [t.value] : t instanceof et ? t.options : t instanceof Yt ? O.objectValues(t.enum) : t instanceof Jt ? Fe(t._def.innerType) : t instanceof Gt ? [void 0] : t instanceof Bt ? [null] : t instanceof Ne ? [void 0, ...Fe(t.unwrap())] : t instanceof tt ? [null, ...Fe(t.unwrap())] : t instanceof Zn || t instanceof Qt ? Fe(t.unwrap()) : t instanceof Xt ? Fe(t._def.innerType) : []; +class Zr extends $ { _parse(e) { const { ctx: r } = this._processInputParams(e); - if (r.parsedType !== b.object) - return S(r, { - code: y.invalid_type, - expected: b.object, + if (r.parsedType !== w.object) + return b(r, { + code: g.invalid_type, + expected: w.object, received: r.parsedType - }), A; - const n = this.discriminator, a = r.data[n], s = this.optionsMap.get(a); - return s ? r.common.async ? s._parseAsync({ + }), I; + 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 - }) : s._parseSync({ + }) : a._parseSync({ data: r.data, path: r.path, parent: r - }) : (S(r, { - code: y.invalid_union_discriminator, + }) : (b(r, { + code: g.invalid_union_discriminator, options: Array.from(this.optionsMap.keys()), path: [n] - }), A); + }), I); } get discriminator() { return this._def.discriminator; @@ -6334,62 +6645,62 @@ class Mr extends N { * @param params */ static create(e, r, n) { - const a = /* @__PURE__ */ new Map(); - for (const s of r) { - const i = cr(s.shape[e]); - if (!i) + const o = /* @__PURE__ */ new Map(); + for (const a of r) { + const i = Fe(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 (a.has(c)) + if (o.has(c)) throw new Error(`Discriminator property ${String(e)} has duplicate value ${String(c)}`); - a.set(c, s); + o.set(c, a); } } - return new Mr({ - typeName: P.ZodDiscriminatedUnion, + return new Zr({ + typeName: A.ZodDiscriminatedUnion, discriminator: e, options: r, - optionsMap: a, + optionsMap: o, ...C(n) }); } } -function sn(t, e) { - const r = Ge(t), n = Ge(e); +function mn(t, e) { + const r = Ve(t), n = Ve(e); if (t === e) return { valid: !0, data: t }; - if (r === b.object && n === b.object) { - const a = R.objectKeys(e), s = R.objectKeys(t).filter((c) => a.indexOf(c) !== -1), i = { ...t, ...e }; - for (const c of s) { - const u = sn(t[c], e[c]); - if (!u.valid) + if (r === w.object && n === w.object) { + const o = O.objectKeys(e), a = O.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] = u.data; + i[c] = l.data; } return { valid: !0, data: i }; - } else if (r === b.array && n === b.array) { + } else if (r === w.array && n === w.array) { if (t.length !== e.length) return { valid: !1 }; - const a = []; - for (let s = 0; s < t.length; s++) { - const i = t[s], c = e[s], u = sn(i, c); - if (!u.valid) + 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 }; - a.push(u.data); + o.push(l.data); } - return { valid: !0, data: a }; + return { valid: !0, data: o }; } else - return r === b.date && n === b.date && +t == +e ? { valid: !0, data: t } : { valid: !1 }; + return r === w.date && n === w.date && +t == +e ? { valid: !0, data: t } : { valid: !1 }; } -class zt extends N { +class Vt extends $ { _parse(e) { - const { status: r, ctx: n } = this._processInputParams(e), a = (s, i) => { - if (nn(s) || nn(i)) - return A; - const c = sn(s.value, i.value); - return c.valid ? ((on(s) || on(i)) && r.dirty(), { status: r.value, value: c.data }) : (S(n, { - code: y.invalid_intersection_types - }), A); + const { status: r, ctx: n } = this._processInputParams(e), o = (a, i) => { + if (pn(a) || pn(i)) + return I; + const c = mn(a.value, i.value); + return c.valid ? ((hn(a) || hn(i)) && r.dirty(), { status: r.value, value: c.data }) : (b(n, { + code: g.invalid_intersection_types + }), I); }; return n.common.async ? Promise.all([ this._def.left._parseAsync({ @@ -6402,7 +6713,7 @@ class zt extends N { path: n.path, parent: n }) - ]).then(([s, i]) => a(s, i)) : a(this._def.left._parseSync({ + ]).then(([a, i]) => o(a, i)) : o(this._def.left._parseSync({ data: n.data, path: n.path, parent: n @@ -6413,41 +6724,41 @@ class zt extends N { })); } } -zt.create = (t, e, r) => new zt({ +Vt.create = (t, e, r) => new Vt({ left: t, right: e, - typeName: P.ZodIntersection, + typeName: A.ZodIntersection, ...C(r) }); -class Oe extends N { +class Oe extends $ { _parse(e) { const { status: r, ctx: n } = this._processInputParams(e); - if (n.parsedType !== b.array) - return S(n, { - code: y.invalid_type, - expected: b.array, + if (n.parsedType !== w.array) + return b(n, { + code: g.invalid_type, + expected: w.array, received: n.parsedType - }), A; + }), I; if (n.data.length < this._def.items.length) - return S(n, { - code: y.too_small, + return b(n, { + code: g.too_small, minimum: this._def.items.length, inclusive: !0, exact: !1, type: "array" - }), A; - !this._def.rest && n.data.length > this._def.items.length && (S(n, { - code: y.too_big, + }), I; + !this._def.rest && n.data.length > this._def.items.length && (b(n, { + code: g.too_big, maximum: this._def.items.length, inclusive: !0, exact: !1, type: "array" }), r.dirty()); - const s = [...n.data].map((i, c) => { - const u = this._def.items[c] || this._def.rest; - return u ? u._parse(new Ne(n, i, n.path, c)) : null; + const a = [...n.data].map((i, c) => { + const l = this._def.items[c] || this._def.rest; + return l ? l._parse(new Re(n, i, n.path, c)) : null; }).filter((i) => !!i); - return n.common.async ? Promise.all(s).then((i) => J.mergeArray(r, i)) : J.mergeArray(r, s); + return n.common.async ? Promise.all(a).then((i) => Q.mergeArray(r, i)) : Q.mergeArray(r, a); } get items() { return this._def.items; @@ -6464,12 +6775,12 @@ Oe.create = (t, e) => { throw new Error("You must pass an array of schemas to z.tuple([ ... ])"); return new Oe({ items: t, - typeName: P.ZodTuple, + typeName: A.ZodTuple, rest: null, ...C(e) }); }; -class Gt extends N { +class Wt extends $ { get keySchema() { return this._def.keyType; } @@ -6478,38 +6789,39 @@ class Gt extends N { } _parse(e) { const { status: r, ctx: n } = this._processInputParams(e); - if (n.parsedType !== b.object) - return S(n, { - code: y.invalid_type, - expected: b.object, + if (n.parsedType !== w.object) + return b(n, { + code: g.invalid_type, + expected: w.object, received: n.parsedType - }), A; - const a = [], s = this._def.keyType, i = this._def.valueType; + }), I; + const o = [], a = this._def.keyType, i = this._def.valueType; for (const c in n.data) - a.push({ - key: s._parse(new Ne(n, c, n.path, c)), - value: i._parse(new Ne(n, n.data[c], n.path, c)) + o.push({ + key: a._parse(new Re(n, c, n.path, c)), + value: i._parse(new Re(n, n.data[c], n.path, c)), + alwaysSet: c in n.data }); - return n.common.async ? J.mergeObjectAsync(r, a) : J.mergeObjectSync(r, a); + return n.common.async ? Q.mergeObjectAsync(r, o) : Q.mergeObjectSync(r, o); } get element() { return this._def.valueType; } static create(e, r, n) { - return r instanceof N ? new Gt({ + return r instanceof $ ? new Wt({ keyType: e, valueType: r, - typeName: P.ZodRecord, + typeName: A.ZodRecord, ...C(n) - }) : new Gt({ - keyType: Ee.create(), + }) : new Wt({ + keyType: ke.create(), valueType: e, - typeName: P.ZodRecord, + typeName: A.ZodRecord, ...C(r) }); } } -class br extends N { +class Ar extends $ { get keySchema() { return this._def.keyType; } @@ -6518,91 +6830,91 @@ class br extends N { } _parse(e) { const { status: r, ctx: n } = this._processInputParams(e); - if (n.parsedType !== b.map) - return S(n, { - code: y.invalid_type, - expected: b.map, + if (n.parsedType !== w.map) + return b(n, { + code: g.invalid_type, + expected: w.map, received: n.parsedType - }), A; - const a = this._def.keyType, s = this._def.valueType, i = [...n.data.entries()].map(([c, u], l) => ({ - key: a._parse(new Ne(n, c, n.path, [l, "key"])), - value: s._parse(new Ne(n, u, n.path, [l, "value"])) + }), I; + const o = this._def.keyType, a = this._def.valueType, i = [...n.data.entries()].map(([c, l], u) => ({ + key: o._parse(new Re(n, c, n.path, [u, "key"])), + value: a._parse(new Re(n, l, n.path, [u, "value"])) })); if (n.common.async) { const c = /* @__PURE__ */ new Map(); return Promise.resolve().then(async () => { - for (const u of i) { - const l = await u.key, d = await u.value; - if (l.status === "aborted" || d.status === "aborted") - return A; - (l.status === "dirty" || d.status === "dirty") && r.dirty(), c.set(l.value, d.value); + for (const l of i) { + const u = await l.key, d = await l.value; + if (u.status === "aborted" || d.status === "aborted") + return I; + (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 u of i) { - const l = u.key, d = u.value; - if (l.status === "aborted" || d.status === "aborted") - return A; - (l.status === "dirty" || d.status === "dirty") && r.dirty(), c.set(l.value, d.value); + for (const l of i) { + const u = l.key, d = l.value; + if (u.status === "aborted" || d.status === "aborted") + return I; + (u.status === "dirty" || d.status === "dirty") && r.dirty(), c.set(u.value, d.value); } return { status: r.value, value: c }; } } } -br.create = (t, e, r) => new br({ +Ar.create = (t, e, r) => new Ar({ valueType: e, keyType: t, - typeName: P.ZodMap, + typeName: A.ZodMap, ...C(r) }); -class ut extends N { +class ht extends $ { _parse(e) { const { status: r, ctx: n } = this._processInputParams(e); - if (n.parsedType !== b.set) - return S(n, { - code: y.invalid_type, - expected: b.set, + if (n.parsedType !== w.set) + return b(n, { + code: g.invalid_type, + expected: w.set, received: n.parsedType - }), A; - const a = this._def; - a.minSize !== null && n.data.size < a.minSize.value && (S(n, { - code: y.too_small, - minimum: a.minSize.value, + }), I; + const o = this._def; + o.minSize !== null && n.data.size < o.minSize.value && (b(n, { + code: g.too_small, + minimum: o.minSize.value, type: "set", inclusive: !0, exact: !1, - message: a.minSize.message - }), r.dirty()), a.maxSize !== null && n.data.size > a.maxSize.value && (S(n, { - code: y.too_big, - maximum: a.maxSize.value, + message: o.minSize.message + }), r.dirty()), o.maxSize !== null && n.data.size > o.maxSize.value && (b(n, { + code: g.too_big, + maximum: o.maxSize.value, type: "set", inclusive: !0, exact: !1, - message: a.maxSize.message + message: o.maxSize.message }), r.dirty()); - const s = this._def.valueType; - function i(u) { - const l = /* @__PURE__ */ new Set(); - for (const d of u) { + const a = this._def.valueType; + function i(l) { + const u = /* @__PURE__ */ new Set(); + for (const d of l) { if (d.status === "aborted") - return A; - d.status === "dirty" && r.dirty(), l.add(d.value); + return I; + d.status === "dirty" && r.dirty(), u.add(d.value); } - return { status: r.value, value: l }; + return { status: r.value, value: u }; } - const c = [...n.data.values()].map((u, l) => s._parse(new Ne(n, u, n.path, l))); - return n.common.async ? Promise.all(c).then((u) => i(u)) : i(c); + const c = [...n.data.values()].map((l, u) => a._parse(new Re(n, l, n.path, u))); + return n.common.async ? Promise.all(c).then((l) => i(l)) : i(c); } min(e, r) { - return new ut({ + return new ht({ ...this._def, minSize: { value: e, message: E.toString(r) } }); } max(e, r) { - return new ut({ + return new ht({ ...this._def, maxSize: { value: e, message: E.toString(r) } }); @@ -6614,77 +6926,77 @@ class ut extends N { return this.min(1, e); } } -ut.create = (t, e) => new ut({ +ht.create = (t, e) => new ht({ valueType: t, minSize: null, maxSize: null, - typeName: P.ZodSet, + typeName: A.ZodSet, ...C(e) }); -class _t extends N { +class xt extends $ { constructor() { super(...arguments), this.validate = this.implement; } _parse(e) { const { ctx: r } = this._processInputParams(e); - if (r.parsedType !== b.function) - return S(r, { - code: y.invalid_type, - expected: b.function, + if (r.parsedType !== w.function) + return b(r, { + code: g.invalid_type, + expected: w.function, received: r.parsedType - }), A; - function n(c, u) { - return yr({ + }), I; + function n(c, l) { + return xr({ data: c, path: r.path, errorMaps: [ r.common.contextualErrorMap, r.schemaErrorMap, - hr(), - Lt - ].filter((l) => !!l), + Er(), + Tt + ].filter((u) => !!u), issueData: { - code: y.invalid_arguments, - argumentsError: u + code: g.invalid_arguments, + argumentsError: l } }); } - function a(c, u) { - return yr({ + function o(c, l) { + return xr({ data: c, path: r.path, errorMaps: [ r.common.contextualErrorMap, r.schemaErrorMap, - hr(), - Lt - ].filter((l) => !!l), + Er(), + Tt + ].filter((u) => !!u), issueData: { - code: y.invalid_return_type, - returnTypeError: u + code: g.invalid_return_type, + returnTypeError: l } }); } - const s = { errorMap: r.common.contextualErrorMap }, i = r.data; - if (this._def.returns instanceof xt) { + const a = { errorMap: r.common.contextualErrorMap }, i = r.data; + if (this._def.returns instanceof It) { const c = this; - return re(async function(...u) { - const l = new xe([]), d = await c._def.args.parseAsync(u, s).catch((p) => { - throw l.addIssue(n(u, p)), l; + return ae(async function(...l) { + const u = new fe([]), 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, s).catch((p) => { - throw l.addIssue(a(f, p)), l; + return await c._def.returns._def.type.parseAsync(f, a).catch((p) => { + throw u.addIssue(o(f, p)), u; }); }); } else { const c = this; - return re(function(...u) { - const l = c._def.args.safeParse(u, s); - if (!l.success) - throw new xe([n(u, l.error)]); - const d = Reflect.apply(i, this, l.data), f = c._def.returns.safeParse(d, s); + return ae(function(...l) { + const u = c._def.args.safeParse(l, a); + if (!u.success) + throw new fe([n(l, u.error)]); + const d = Reflect.apply(i, this, u.data), f = c._def.returns.safeParse(d, a); if (!f.success) - throw new xe([a(d, f.error)]); + throw new fe([o(d, f.error)]); return f.data; }); } @@ -6696,13 +7008,13 @@ class _t extends N { return this._def.returns; } args(...e) { - return new _t({ + return new xt({ ...this._def, - args: Oe.create(e).rest(at.create()) + args: Oe.create(e).rest(dt.create()) }); } returns(e) { - return new _t({ + return new xt({ ...this._def, returns: e }); @@ -6714,15 +7026,15 @@ class _t extends N { return this.parse(e); } static create(e, r, n) { - return new _t({ - args: e || Oe.create([]).rest(at.create()), - returns: r || at.create(), - typeName: P.ZodFunction, + return new xt({ + args: e || Oe.create([]).rest(dt.create()), + returns: r || dt.create(), + typeName: A.ZodFunction, ...C(n) }); } } -class Bt extends N { +class qt extends $ { get schema() { return this._def.getter(); } @@ -6731,20 +7043,20 @@ class Bt extends N { return this._def.getter()._parse({ data: r.data, path: r.path, parent: r }); } } -Bt.create = (t, e) => new Bt({ +qt.create = (t, e) => new qt({ getter: t, - typeName: P.ZodLazy, + typeName: A.ZodLazy, ...C(e) }); -class Ht extends N { +class Kt extends $ { _parse(e) { if (e.data !== this._def.value) { const r = this._getOrReturnCtx(e); - return S(r, { + return b(r, { received: r.data, - code: y.invalid_literal, + code: g.invalid_literal, expected: this._def.value - }), A; + }), I; } return { status: "valid", value: e.data }; } @@ -6752,37 +7064,40 @@ class Ht extends N { return this._def.value; } } -Ht.create = (t, e) => new Ht({ +Kt.create = (t, e) => new Kt({ value: t, - typeName: P.ZodLiteral, + typeName: A.ZodLiteral, ...C(e) }); -function ys(t, e) { - return new Ye({ +function Ls(t, e) { + return new et({ values: t, - typeName: P.ZodEnum, + typeName: A.ZodEnum, ...C(e) }); } -class Ye extends N { +class et extends $ { + constructor() { + super(...arguments), Lt.set(this, void 0); + } _parse(e) { if (typeof e.data != "string") { const r = this._getOrReturnCtx(e), n = this._def.values; - return S(r, { - expected: R.joinValues(n), + return b(r, { + expected: O.joinValues(n), received: r.parsedType, - code: y.invalid_type - }), A; + code: g.invalid_type + }), I; } - if (this._def.values.indexOf(e.data) === -1) { + if (kr(this, Lt) || Ns(this, Lt, new Set(this._def.values)), !kr(this, Lt).has(e.data)) { const r = this._getOrReturnCtx(e), n = this._def.values; - return S(r, { + return b(r, { received: r.data, - code: y.invalid_enum_value, + code: g.invalid_enum_value, options: n - }), A; + }), I; } - return re(e.data); + return ae(e.data); } get options() { return this._def.values; @@ -6805,105 +7120,125 @@ class Ye extends N { e[r] = r; return e; } - extract(e) { - return Ye.create(e); + extract(e, r = this._def) { + return et.create(e, { + ...this._def, + ...r + }); } - exclude(e) { - return Ye.create(this.options.filter((r) => !e.includes(r))); + exclude(e, r = this._def) { + return et.create(this.options.filter((n) => !e.includes(n)), { + ...this._def, + ...r + }); } } -Ye.create = ys; -class Vt extends N { +Lt = /* @__PURE__ */ new WeakMap(); +et.create = Ls; +class Yt extends $ { + constructor() { + super(...arguments), Ft.set(this, void 0); + } _parse(e) { - const r = R.getValidEnumValues(this._def.values), n = this._getOrReturnCtx(e); - if (n.parsedType !== b.string && n.parsedType !== b.number) { - const a = R.objectValues(r); - return S(n, { - expected: R.joinValues(a), + const r = O.getValidEnumValues(this._def.values), n = this._getOrReturnCtx(e); + if (n.parsedType !== w.string && n.parsedType !== w.number) { + const o = O.objectValues(r); + return b(n, { + expected: O.joinValues(o), received: n.parsedType, - code: y.invalid_type - }), A; + code: g.invalid_type + }), I; } - if (r.indexOf(e.data) === -1) { - const a = R.objectValues(r); - return S(n, { + if (kr(this, Ft) || Ns(this, Ft, new Set(O.getValidEnumValues(this._def.values))), !kr(this, Ft).has(e.data)) { + const o = O.objectValues(r); + return b(n, { received: n.data, - code: y.invalid_enum_value, - options: a - }), A; + code: g.invalid_enum_value, + options: o + }), I; } - return re(e.data); + return ae(e.data); } get enum() { return this._def.values; } } -Vt.create = (t, e) => new Vt({ +Ft = /* @__PURE__ */ new WeakMap(); +Yt.create = (t, e) => new Yt({ values: t, - typeName: P.ZodNativeEnum, + typeName: A.ZodNativeEnum, ...C(e) }); -class xt extends N { +class It extends $ { unwrap() { return this._def.type; } _parse(e) { const { ctx: r } = this._processInputParams(e); - if (r.parsedType !== b.promise && r.common.async === !1) - return S(r, { - code: y.invalid_type, - expected: b.promise, + if (r.parsedType !== w.promise && r.common.async === !1) + return b(r, { + code: g.invalid_type, + expected: w.promise, received: r.parsedType - }), A; - const n = r.parsedType === b.promise ? r.data : Promise.resolve(r.data); - return re(n.then((a) => this._def.type.parseAsync(a, { + }), I; + const n = r.parsedType === w.promise ? r.data : Promise.resolve(r.data); + return ae(n.then((o) => this._def.type.parseAsync(o, { path: r.path, errorMap: r.common.contextualErrorMap }))); } } -xt.create = (t, e) => new xt({ +It.create = (t, e) => new It({ type: t, - typeName: P.ZodPromise, + typeName: A.ZodPromise, ...C(e) }); -class ke extends N { +class Ae extends $ { innerType() { return this._def.schema; } sourceType() { - return this._def.schema._def.typeName === P.ZodEffects ? this._def.schema.sourceType() : this._def.schema; + return this._def.schema._def.typeName === A.ZodEffects ? this._def.schema.sourceType() : this._def.schema; } _parse(e) { - const { status: r, ctx: n } = this._processInputParams(e), a = this._def.effect || null, s = { + const { status: r, ctx: n } = this._processInputParams(e), o = this._def.effect || null, a = { addIssue: (i) => { - S(n, i), i.fatal ? r.abort() : r.dirty(); + b(n, i), i.fatal ? r.abort() : r.dirty(); }, get path() { return n.path; } }; - if (s.addIssue = s.addIssue.bind(s), a.type === "preprocess") { - const i = a.transform(n.data, s); - return n.common.issues.length ? { - status: "dirty", - value: n.data - } : n.common.async ? Promise.resolve(i).then((c) => this._def.schema._parseAsync({ - data: c, - path: n.path, - parent: n - })) : this._def.schema._parseSync({ - data: i, - path: n.path, - parent: n - }); + 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 I; + const l = await this._def.schema._parseAsync({ + data: c, + path: n.path, + parent: n + }); + return l.status === "aborted" ? I : l.status === "dirty" || r.value === "dirty" ? wt(l.value) : l; + }); + { + if (r.value === "aborted") + return I; + const c = this._def.schema._parseSync({ + data: i, + path: n.path, + parent: n + }); + return c.status === "aborted" ? I : c.status === "dirty" || r.value === "dirty" ? wt(c.value) : c; + } } - if (a.type === "refinement") { + if (o.type === "refinement") { const i = (c) => { - const u = a.refinement(c, s); + const l = o.refinement(c, a); if (n.common.async) - return Promise.resolve(u); - if (u instanceof Promise) + return Promise.resolve(l); + if (l instanceof Promise) throw new Error("Async refinement encountered during synchronous parse operation. Use .parseAsync instead."); return c; }; @@ -6913,71 +7248,71 @@ class ke extends N { path: n.path, parent: n }); - return c.status === "aborted" ? A : (c.status === "dirty" && r.dirty(), i(c.value), { status: r.value, value: c.value }); + return c.status === "aborted" ? I : (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" ? A : (c.status === "dirty" && r.dirty(), i(c.value).then(() => ({ status: r.value, value: c.value })))); + return this._def.schema._parseAsync({ data: n.data, path: n.path, parent: n }).then((c) => c.status === "aborted" ? I : (c.status === "dirty" && r.dirty(), i(c.value).then(() => ({ status: r.value, value: c.value })))); } - if (a.type === "transform") + if (o.type === "transform") if (n.common.async === !1) { const i = this._def.schema._parseSync({ data: n.data, path: n.path, parent: n }); - if (!Ft(i)) + if (!jt(i)) return i; - const c = a.transform(i.value, s); + 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) => Ft(i) ? Promise.resolve(a.transform(i.value, s)).then((c) => ({ status: r.value, value: c })) : i); - R.assertNever(a); + return this._def.schema._parseAsync({ data: n.data, path: n.path, parent: n }).then((i) => jt(i) ? Promise.resolve(o.transform(i.value, a)).then((c) => ({ status: r.value, value: c })) : i); + O.assertNever(o); } } -ke.create = (t, e, r) => new ke({ +Ae.create = (t, e, r) => new Ae({ schema: t, - typeName: P.ZodEffects, + typeName: A.ZodEffects, effect: e, ...C(r) }); -ke.createWithPreprocess = (t, e, r) => new ke({ +Ae.createWithPreprocess = (t, e, r) => new Ae({ schema: e, effect: { type: "preprocess", transform: t }, - typeName: P.ZodEffects, + typeName: A.ZodEffects, ...C(r) }); -class Fe extends N { +class Ne extends $ { _parse(e) { - return this._getType(e) === b.undefined ? re(void 0) : this._def.innerType._parse(e); + return this._getType(e) === w.undefined ? ae(void 0) : this._def.innerType._parse(e); } unwrap() { return this._def.innerType; } } -Fe.create = (t, e) => new Fe({ +Ne.create = (t, e) => new Ne({ innerType: t, - typeName: P.ZodOptional, + typeName: A.ZodOptional, ...C(e) }); -class dt extends N { +class tt extends $ { _parse(e) { - return this._getType(e) === b.null ? re(null) : this._def.innerType._parse(e); + return this._getType(e) === w.null ? ae(null) : this._def.innerType._parse(e); } unwrap() { return this._def.innerType; } } -dt.create = (t, e) => new dt({ +tt.create = (t, e) => new tt({ innerType: t, - typeName: P.ZodNullable, + typeName: A.ZodNullable, ...C(e) }); -class Wt extends N { +class Jt extends $ { _parse(e) { const { ctx: r } = this._processInputParams(e); let n = r.data; - return r.parsedType === b.undefined && (n = this._def.defaultValue()), this._def.innerType._parse({ + return r.parsedType === w.undefined && (n = this._def.defaultValue()), this._def.innerType._parse({ data: n, path: r.path, parent: r @@ -6987,13 +7322,13 @@ class Wt extends N { return this._def.innerType; } } -Wt.create = (t, e) => new Wt({ +Jt.create = (t, e) => new Jt({ innerType: t, - typeName: P.ZodDefault, + typeName: A.ZodDefault, defaultValue: typeof e.default == "function" ? e.default : () => e.default, ...C(e) }); -class wr extends N { +class Xt extends $ { _parse(e) { const { ctx: r } = this._processInputParams(e), n = { ...r, @@ -7001,26 +7336,26 @@ class wr extends N { ...r.common, issues: [] } - }, a = this._def.innerType._parse({ + }, o = this._def.innerType._parse({ data: n.data, path: n.path, parent: { ...n } }); - return gr(a) ? a.then((s) => ({ + return Zt(o) ? o.then((a) => ({ status: "valid", - value: s.status === "valid" ? s.value : this._def.catchValue({ + value: a.status === "valid" ? a.value : this._def.catchValue({ get error() { - return new xe(n.common.issues); + return new fe(n.common.issues); }, input: n.data }) })) : { status: "valid", - value: a.status === "valid" ? a.value : this._def.catchValue({ + value: o.status === "valid" ? o.value : this._def.catchValue({ get error() { - return new xe(n.common.issues); + return new fe(n.common.issues); }, input: n.data }) @@ -7030,31 +7365,31 @@ class wr extends N { return this._def.innerType; } } -wr.create = (t, e) => new wr({ +Xt.create = (t, e) => new Xt({ innerType: t, - typeName: P.ZodCatch, + typeName: A.ZodCatch, catchValue: typeof e.catch == "function" ? e.catch : () => e.catch, ...C(e) }); -class Sr extends N { +class Ir extends $ { _parse(e) { - if (this._getType(e) !== b.nan) { + if (this._getType(e) !== w.nan) { const n = this._getOrReturnCtx(e); - return S(n, { - code: y.invalid_type, - expected: b.nan, + return b(n, { + code: g.invalid_type, + expected: w.nan, received: n.parsedType - }), A; + }), I; } return { status: "valid", value: e.data }; } } -Sr.create = (t) => new Sr({ - typeName: P.ZodNaN, +Ir.create = (t) => new Ir({ + typeName: A.ZodNaN, ...C(t) }); -const lc = Symbol("zod_brand"); -class gs extends N { +const Gc = Symbol("zod_brand"); +class Zn extends $ { _parse(e) { const { ctx: r } = this._processInputParams(e), n = r.data; return this._def.type._parse({ @@ -7067,405 +7402,521 @@ class gs extends N { return this._def.type; } } -class Qt extends N { +class sr extends $ { _parse(e) { const { status: r, ctx: n } = this._processInputParams(e); if (n.common.async) return (async () => { - const s = await this._def.in._parseAsync({ + const a = await this._def.in._parseAsync({ data: n.data, path: n.path, parent: n }); - return s.status === "aborted" ? A : s.status === "dirty" ? (r.dirty(), hs(s.value)) : this._def.out._parseAsync({ - data: s.value, + return a.status === "aborted" ? I : a.status === "dirty" ? (r.dirty(), wt(a.value)) : this._def.out._parseAsync({ + data: a.value, path: n.path, parent: n }); })(); { - const a = this._def.in._parseSync({ + const o = this._def.in._parseSync({ data: n.data, path: n.path, parent: n }); - return a.status === "aborted" ? A : a.status === "dirty" ? (r.dirty(), { + return o.status === "aborted" ? I : o.status === "dirty" ? (r.dirty(), { status: "dirty", - value: a.value + value: o.value }) : this._def.out._parseSync({ - data: a.value, + data: o.value, path: n.path, parent: n }); } } static create(e, r) { - return new Qt({ + return new sr({ in: e, out: r, - typeName: P.ZodPipeline + typeName: A.ZodPipeline }); } } -class Er extends N { +class Qt extends $ { _parse(e) { - const r = this._def.innerType._parse(e); - return Ft(r) && (r.value = Object.freeze(r.value)), r; + const r = this._def.innerType._parse(e), n = (o) => (jt(o) && (o.value = Object.freeze(o.value)), o); + return Zt(r) ? r.then((o) => n(o)) : n(r); + } + unwrap() { + return this._def.innerType; } } -Er.create = (t, e) => new Er({ +Qt.create = (t, e) => new Qt({ innerType: t, - typeName: P.ZodReadonly, + typeName: A.ZodReadonly, ...C(e) }); -const vs = (t, e = {}, r) => t ? Et.create().superRefine((n, a) => { - var s, i; - if (!t(n)) { - const c = typeof e == "function" ? e(n) : typeof e == "string" ? { message: e } : e, u = (i = (s = c.fatal) !== null && s !== void 0 ? s : r) !== null && i !== void 0 ? i : !0, l = typeof c == "string" ? { message: c } : c; - a.addIssue({ code: "custom", ...l, fatal: u }); - } -}) : Et.create(), uc = { +function Fs(t, e = {}, r) { + return t ? At.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 }); + } + }) : At.create(); +} +const Bc = { object: U.lazycreate }; -var P; +var A; (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"; -})(P || (P = {})); -const dc = (t, e = { +})(A || (A = {})); +const Hc = (t, e = { message: `Input not instance of ${t.name}` -}) => vs((r) => r instanceof t, e), _s = Ee.create, bs = qe.create, fc = Sr.create, pc = Ke.create, ws = Dt.create, mc = lt.create, hc = vr.create, yc = Ut.create, gc = jt.create, vc = Et.create, _c = at.create, bc = Ue.create, wc = _r.create, Sc = Pe.create, Ec = U.create, xc = U.strictCreate, Pc = Zt.create, kc = Mr.create, Tc = zt.create, Ic = Oe.create, Ac = Gt.create, Cc = br.create, $c = ut.create, Nc = _t.create, Oc = Bt.create, Rc = Ht.create, Mc = Ye.create, Lc = Vt.create, Fc = xt.create, oo = ke.create, Dc = Fe.create, Uc = dt.create, jc = ke.createWithPreprocess, Zc = Qt.create, zc = () => _s().optional(), Gc = () => bs().optional(), Bc = () => ws().optional(), Hc = { - string: (t) => Ee.create({ ...t, coerce: !0 }), - number: (t) => qe.create({ ...t, coerce: !0 }), - boolean: (t) => Dt.create({ +}) => Fs((r) => r instanceof t, e), Ds = ke.create, Us = Xe.create, Vc = Ir.create, Wc = Qe.create, js = zt.create, qc = pt.create, Kc = Pr.create, Yc = Gt.create, Jc = Bt.create, Xc = At.create, Qc = dt.create, el = je.create, tl = Tr.create, rl = Te.create, nl = U.create, ol = U.strictCreate, sl = Ht.create, al = Zr.create, il = Vt.create, cl = Oe.create, ll = Wt.create, ul = Ar.create, dl = ht.create, fl = xt.create, pl = qt.create, hl = Kt.create, ml = et.create, gl = Yt.create, yl = It.create, _o = Ae.create, vl = Ne.create, _l = tt.create, bl = Ae.createWithPreprocess, wl = sr.create, Sl = () => Ds().optional(), El = () => Us().optional(), xl = () => js().optional(), kl = { + string: (t) => ke.create({ ...t, coerce: !0 }), + number: (t) => Xe.create({ ...t, coerce: !0 }), + boolean: (t) => zt.create({ ...t, coerce: !0 }), - bigint: (t) => Ke.create({ ...t, coerce: !0 }), - date: (t) => lt.create({ ...t, coerce: !0 }) -}, Vc = A; -var V = /* @__PURE__ */ Object.freeze({ + bigint: (t) => Qe.create({ ...t, coerce: !0 }), + date: (t) => pt.create({ ...t, coerce: !0 }) +}, Pl = I; +var W = /* @__PURE__ */ Object.freeze({ __proto__: null, - defaultErrorMap: Lt, - setErrorMap: Yi, - getErrorMap: hr, - makeIssue: yr, - EMPTY_PATH: Ji, - addIssueToContext: S, - ParseStatus: J, - INVALID: A, - DIRTY: hs, - OK: re, - isAborted: nn, - isDirty: on, - isValid: Ft, - isAsync: gr, + defaultErrorMap: Tt, + setErrorMap: Pc, + getErrorMap: Er, + makeIssue: xr, + EMPTY_PATH: Tc, + addIssueToContext: b, + ParseStatus: Q, + INVALID: I, + DIRTY: wt, + OK: ae, + isAborted: pn, + isDirty: hn, + isValid: jt, + isAsync: Zt, get util() { - return R; + return O; }, get objectUtil() { - return rn; + return fn; }, - ZodParsedType: b, - getParsedType: Ge, - ZodType: N, - ZodString: Ee, - ZodNumber: qe, - ZodBigInt: Ke, - ZodBoolean: Dt, - ZodDate: lt, - ZodSymbol: vr, - ZodUndefined: Ut, - ZodNull: jt, - ZodAny: Et, - ZodUnknown: at, - ZodNever: Ue, - ZodVoid: _r, - ZodArray: Pe, + ZodParsedType: w, + getParsedType: Ve, + ZodType: $, + datetimeRegex: Ms, + ZodString: ke, + ZodNumber: Xe, + ZodBigInt: Qe, + ZodBoolean: zt, + ZodDate: pt, + ZodSymbol: Pr, + ZodUndefined: Gt, + ZodNull: Bt, + ZodAny: At, + ZodUnknown: dt, + ZodNever: je, + ZodVoid: Tr, + ZodArray: Te, ZodObject: U, - ZodUnion: Zt, - ZodDiscriminatedUnion: Mr, - ZodIntersection: zt, + ZodUnion: Ht, + ZodDiscriminatedUnion: Zr, + ZodIntersection: Vt, ZodTuple: Oe, - ZodRecord: Gt, - ZodMap: br, - ZodSet: ut, - ZodFunction: _t, - ZodLazy: Bt, - ZodLiteral: Ht, - ZodEnum: Ye, - ZodNativeEnum: Vt, - ZodPromise: xt, - ZodEffects: ke, - ZodTransformer: ke, - ZodOptional: Fe, - ZodNullable: dt, - ZodDefault: Wt, - ZodCatch: wr, - ZodNaN: Sr, - BRAND: lc, - ZodBranded: gs, - ZodPipeline: Qt, - ZodReadonly: Er, - custom: vs, - Schema: N, - ZodSchema: N, - late: uc, + ZodRecord: Wt, + ZodMap: Ar, + ZodSet: ht, + ZodFunction: xt, + ZodLazy: qt, + ZodLiteral: Kt, + ZodEnum: et, + ZodNativeEnum: Yt, + ZodPromise: It, + ZodEffects: Ae, + ZodTransformer: Ae, + ZodOptional: Ne, + ZodNullable: tt, + ZodDefault: Jt, + ZodCatch: Xt, + ZodNaN: Ir, + BRAND: Gc, + ZodBranded: Zn, + ZodPipeline: sr, + ZodReadonly: Qt, + custom: Fs, + Schema: $, + ZodSchema: $, + late: Bc, get ZodFirstPartyTypeKind() { - return P; + return A; }, - coerce: Hc, - any: vc, - array: Sc, - bigint: pc, - boolean: ws, - date: mc, - discriminatedUnion: kc, - effect: oo, - enum: Mc, - function: Nc, - instanceof: dc, - intersection: Tc, - lazy: Oc, - literal: Rc, - map: Cc, - nan: fc, - nativeEnum: Lc, - never: bc, - null: gc, - nullable: Uc, - number: bs, - object: Ec, - oboolean: Bc, - onumber: Gc, - optional: Dc, - ostring: zc, - pipeline: Zc, - preprocess: jc, - promise: Fc, - record: Ac, - set: $c, - strictObject: xc, - string: _s, - symbol: hc, - transformer: oo, - tuple: Ic, - undefined: yc, - union: Pc, - unknown: _c, - void: wc, - NEVER: Vc, - ZodIssueCode: y, - quotelessJson: Ki, - ZodError: xe + coerce: kl, + any: Xc, + array: rl, + bigint: Wc, + boolean: js, + date: qc, + discriminatedUnion: al, + effect: _o, + enum: ml, + function: fl, + instanceof: Hc, + intersection: il, + lazy: pl, + literal: hl, + map: ul, + nan: Vc, + nativeEnum: gl, + never: el, + null: Jc, + nullable: _l, + number: Us, + object: nl, + oboolean: xl, + onumber: El, + optional: vl, + ostring: Sl, + pipeline: wl, + preprocess: bl, + promise: yl, + record: ll, + set: dl, + strictObject: ol, + string: Ds, + symbol: Kc, + transformer: _o, + tuple: cl, + undefined: Yc, + union: sl, + unknown: Qc, + void: tl, + NEVER: Pl, + ZodIssueCode: g, + quotelessJson: kc, + ZodError: fe }); -const Wc = V.object({ - width: V.number().positive(), - height: V.number().positive() +const Tl = W.object({ + width: W.number().positive(), + height: W.number().positive() }); -function qc(t, e, r, n) { - const a = document.createElement("plugin-modal"); - return a.setTheme(r), a.setAttribute("title", t), a.setAttribute("iframe-src", e), a.setAttribute("width", String((n == null ? void 0 : n.width) || 285)), a.setAttribute("height", String((n == null ? void 0 : n.height) || 540)), document.body.appendChild(a), a; +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 Kc = V.function().args( - V.string(), - V.string(), - V.enum(["dark", "light"]), - Wc.optional() -).implement((t, e, r, n) => qc(t, e, r, n)), Yc = V.object({ - name: V.string(), - host: V.string().url(), - code: V.string(), - icon: V.string().optional(), - description: V.string().max(200).optional(), - permissions: V.array( - V.enum([ - "page:read", - "page:write", - "file:read", - "file:write", - "selection:read" +const Il = W.function().args( + W.string(), + W.string(), + W.enum(["dark", "light"]), + Tl.optional() +).implement((t, e, r, n) => Al(t, e, r, n)), Cl = W.object({ + pluginId: W.string(), + name: W.string(), + host: W.string().url(), + code: W.string(), + icon: W.string().optional(), + description: W.string().max(200).optional(), + permissions: W.array( + W.enum([ + "content:read", + "content:write", + "library:read", + "library:write", + "user:read" ]) ) }); -function Ss(t, e) { +function Zs(t, e) { return new URL(e, t).toString(); } -function Jc(t) { +function $l(t) { return fetch(t).then((e) => e.json()).then((e) => { - if (!Yc.safeParse(e).success) + if (!Cl.safeParse(e).success) throw new Error("Invalid plugin manifest"); return e; }).catch((e) => { throw console.error(e), e; }); } -function Xc(t) { - return fetch(Ss(t.host, t.code)).then((e) => { +function Nl(t) { + return fetch(Zs(t.host, t.code)).then((e) => { if (e.ok) return e.text(); throw new Error("Failed to load plugin code"); }); } -const an = [ +const Rl = [ "finish", "pagechange", "filechange", "selectionchange", - "themechange" + "themechange", + "shapechange", + "contentsave" ]; -let cn = [], ne = null; -const Nt = /* @__PURE__ */ new Map(); +let gn = [], yn = /* @__PURE__ */ new Set([]), Mt = {}; window.addEventListener("message", (t) => { - for (const e of cn) - e(t.data); + try { + for (const e of gn) + e(t.data); + } catch (e) { + console.error(e); + } }); -function Qc(t, e) { - t === "themechange" && ne && ne.setTheme(e), (Nt.get(t) || []).forEach((n) => n(e)); +function Ol(t) { + yn.forEach((e) => { + e.setTheme(t); + }); } -function el(t, e) { - const r = () => { - ne == null || ne.removeEventListener("close", r), ne && ne.remove(), cn = [], ne = null; - }, n = (s) => { - if (!e.permissions.includes(s)) - throw new Error(`Permission ${s} is not granted`); +function Ml(t, e) { + let r = null; + const n = () => { + Object.entries(Mt).forEach(([, i]) => { + i.forEach((c) => { + t.removeListener(c); + }); + }), r && (yn.delete(r), r.removeEventListener("close", n), r.remove()), gn = [], r = null; + }, o = (i) => { + if (!e.permissions.includes(i)) + throw new Error(`Permission ${i} is not granted`); }; return { ui: { - open: (s, i, c) => { + open: (i, c, l) => { const u = t.getTheme(); - ne = Kc( - s, - Ss(e.host, i), + r = Il( + i, + Zs(e.host, c), u, - c - ), ne.setTheme(u), ne.addEventListener("close", r, { + l + ), r.setTheme(u), r.addEventListener("close", n, { once: !0 - }); + }), yn.add(r); }, - sendMessage(s) { - const i = new CustomEvent("message", { - detail: s + sendMessage(i) { + const c = new CustomEvent("message", { + detail: i }); - ne == null || ne.dispatchEvent(i); + r == null || r.dispatchEvent(c); }, - onMessage: (s) => { - V.function().parse(s), cn.push(s); + onMessage: (i) => { + W.function().parse(i), gn.push(i); } }, utils: { + geometry: { + center(i) { + return window.app.plugins.public_utils.centerShapes(i); + } + }, types: { - isText(s) { - return s.type === "text"; + isFrame(i) { + return i.type === "frame"; }, - isRectangle(s) { - return s.type === "rect"; + isGroup(i) { + return i.type === "group"; }, - isFrame(s) { - return s.type === "frame"; + isMask(i) { + return i.type === "group" && i.isMask(); + }, + isBool(i) { + return i.type === "bool"; + }, + isRectangle(i) { + return i.type === "rect"; + }, + isPath(i) { + return i.type === "path"; + }, + isText(i) { + return i.type === "text"; + }, + isEllipse(i) { + return i.type === "circle"; + }, + isSVG(i) { + return i.type === "svg-raw"; } } }, - closePlugin: r, - on(s, i) { - V.enum(an).parse(s), V.function().parse(i), s === "pagechange" ? n("page:read") : s === "filechange" ? n("file:read") : s === "selectionchange" && n("selection:read"); - const c = Nt.get(s) || []; - c.push(i), Nt.set(s, c); + closePlugin: n, + on(i, c, l) { + W.enum(Rl).parse(i), W.function().parse(c), o("content:read"); + const u = t.addListener(i, c, l); + return Mt[i] || (Mt[i] = /* @__PURE__ */ new Map()), Mt[i].set(c, u), u; }, - off(s, i) { - V.enum(an).parse(s), V.function().parse(i); - const c = Nt.get(s) || []; - Nt.set( - s, - c.filter((u) => u !== i) - ); + off(i, c) { + let l; + typeof i == "symbol" ? l = i : c && (l = Mt[i].get(c)), l && t.removeListener(l); }, // Penpot State API get root() { - return n("page:read"), t.root; + return o("content:read"), t.root; }, get currentPage() { - return n("page:read"), t.currentPage; + return o("content:read"), t.currentPage; }, get selection() { - return n("selection:read"), t.selection; + return o("content:read"), t.selection; + }, + set selection(i) { + o("content:read"), t.selection = i; }, get viewport() { return t.viewport; }, + get history() { + return t.history; + }, get library() { - return t.library; + return o("library:read"), t.library; + }, + get fonts() { + return o("content:read"), t.fonts; + }, + get currentUser() { + return o("user:read"), t.currentUser; + }, + get activeUsers() { + return o("user:read"), t.activeUsers; }, getFile() { - return n("file:read"), t.getFile(); + return o("content:read"), t.getFile(); }, getPage() { - return n("page:read"), t.getPage(); + return o("content:read"), t.getPage(); }, getSelected() { - return n("selection:read"), t.getSelected(); + return o("content:read"), t.getSelected(); }, getSelectedShapes() { - return n("selection:read"), t.getSelectedShapes(); + return o("content:read"), t.getSelectedShapes(); + }, + shapesColors(i) { + return o("content:read"), t.shapesColors(i); + }, + replaceColor(i, c, l) { + return o("content:write"), t.replaceColor(i, c, l); }, getTheme() { return t.getTheme(); }, createFrame() { - return t.createFrame(); + return o("content:write"), t.createFrame(); }, createRectangle() { - return t.createRectangle(); + return o("content:write"), t.createRectangle(); }, - createText(s) { - return t.createText(s); + createEllipse() { + return o("content:write"), t.createEllipse(); }, - createShapeFromSvg(s) { - return t.createShapeFromSvg(s); + createText(i) { + return o("content:write"), t.createText(i); }, - group(s) { - return t.group(s); + createPath() { + return o("content:write"), t.createPath(); }, - ungroup(s, ...i) { - t.ungroup(s, ...i); + createBoolean(i, c) { + return o("content:write"), t.createBoolean(i, c); }, - uploadMediaUrl(s, i) { - return t.uploadMediaUrl(s, i); + createShapeFromSvg(i) { + return o("content:write"), t.createShapeFromSvg(i); + }, + group(i) { + return o("content:write"), t.group(i); + }, + ungroup(i, ...c) { + o("content:write"), t.ungroup(i, ...c); + }, + uploadMediaUrl(i, c) { + return o("content:write"), t.uploadMediaUrl(i, c); + }, + uploadMediaData(i, c, l) { + return o("content:write"), t.uploadMediaData(i, c, l); + }, + generateMarkup(i, c) { + return o("content:read"), t.generateMarkup(i, c); + }, + generateStyle(i, c) { + return o("content:read"), t.generateStyle(i, c); + }, + openViewer() { + o("content:read"), t.openViewer(); + }, + createPage() { + return o("content:write"), t.createPage(); + }, + openPage(i) { + o("content:read"), t.openPage(i); } }; } -let so = !1, et, rt = null; -function tl(t) { - rt = t; +let bo = !1, fr = []; +const Ll = !1; +let vn = null; +function Fl(t) { + vn = t; } -const Es = async function(t) { +const zs = async function(t) { try { - const e = await Xc(t); - if (so || (so = !0, hardenIntrinsics()), et && et.closePlugin(), rt) { - et = el(rt, t), new Compartment({ - penpot: harden(et), - fetch: window.fetch.bind(window), - console: harden(window.console), - Math: harden(Math), - setTimeout: harden( - (...[a, s]) => setTimeout(() => { - a(); - }, s) - ), - clearTimeout: harden((a) => { - clearTimeout(a); - }) - }).evaluate(e); - const n = rt.addListener("finish", () => { - et == null || et.closePlugin(), rt == null || rt.removeListener(n); - }); - } else - console.error("Cannot find Penpot Context"); + const e = () => { + fr.forEach((c) => { + c.closePlugin(); + }), fr = []; + }, r = vn && vn(t.pluginId); + if (!r) + return; + r.addListener("themechange", (c) => Ol(c)); + const n = await Nl(t); + bo || (bo = !0, hardenIntrinsics()), fr && !Ll && e(); + const o = Ml(r, t); + fr.push(o), new Compartment({ + penpot: harden(o), + fetch: harden((...c) => { + const l = { + ...c[1], + credentials: "omit" + }; + return fetch(c[0], l); + }), + console: harden(window.console), + Math: harden(Math), + setTimeout: harden( + (...[c, l]) => setTimeout(() => { + c(); + }, l) + ), + clearTimeout: harden((c) => { + clearTimeout(c); + }) + }).evaluate(n); + const i = r.addListener("finish", () => { + e(), r == null || r.removeListener(i); + }); } catch (e) { console.error(e); } -}, rl = async function(t) { - const e = await Jc(t); - Es(e); +}, Dl = async function(t) { + const e = await $l(t); + zs(e); }; console.log("%c[PLUGINS] Loading plugin system", "color: #008d7c"); repairIntrinsics({ @@ -7474,12 +7925,12 @@ repairIntrinsics({ errorTaming: "unsafe", consoleTaming: "unsafe" }); -const ao = globalThis; -ao.initPluginsRuntime = (t) => { - if (t) { - console.log("%c[PLUGINS] Initialize context", "color: #008d7c"), ao.ɵcontext = t, globalThis.ɵloadPlugin = Es, globalThis.ɵloadPluginByUrl = rl, tl(t); - for (const e of an) - t.addListener(e, Qc.bind(null, e)); +const wo = globalThis; +wo.initPluginsRuntime = (t) => { + try { + console.log("%c[PLUGINS] Initialize runtime", "color: #008d7c"), Fl(t), wo.ɵ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 8ca76c9ca..078593de0 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; @@ -29,27 +39,15 @@ body { * { box-sizing: border-box; scrollbar-width: thin; - // transition: all .4s ease; } +// Firefox-only hack @-moz-document url-prefix() { * { scrollbar-width: auto; } } -.global-zeroclipboard-container { - transition: none; - - #global-zeroclipboard-flash-bridge { - transition: none; - } - - object { - transition: none; - } -} - img { height: auto; width: 100%; @@ -72,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 { @@ -149,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; @@ -270,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 8b9a0fb03..ea30c21e1 100644 --- a/frontend/resources/styles/common/dependencies/animations.scss +++ b/frontend/resources/styles/common/dependencies/animations.scss @@ -6,23 +6,6 @@ * 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; @@ -30,1228 +13,6 @@ 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; @@ -1310,2125 +71,19 @@ 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; - } -} - -@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; - } -} - -.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% { - 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); - } -} - -@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); - } -} - -.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); - } -} - -.slideOutRight { - -webkit-animation-name: slideOutRight; - animation-name: slideOutRight; -} - -@-webkit-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); - } -} - -@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); - } -} - -@keyframes pen3 { - 60% { - transform: translateY(0px); - } - 75% { - transform: translateY(-10px); - } - 90% { - transform: translateY(0px); - } -} - @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 34bd2dccb..000000000 --- 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 74cd11499..7882eb7d3 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 005357e6a..000000000 --- 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 000000000..9d53084cb --- /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 8d8fbd6f9..000000000 --- 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 e8ec11d40..000000000 --- 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 6092943ca..39e198d8d 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 a90e22dbd..000000000 --- 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: 40; - - 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 858838d05..5096ecd6a 100644 --- a/frontend/resources/styles/common/refactor/basic-rules.scss +++ b/frontend/resources/styles/common/refactor/basic-rules.scss @@ -291,9 +291,7 @@ // INPUTS .input-base { @include removeInputStyle; - @include bodySmallTypography; @include textEllipsis; - // @include focusInput; height: $s-28; width: 100%; flex-grow: 1; @@ -326,7 +324,6 @@ } .input-element { - @include bodySmallTypography; @include focusInput; display: flex; align-items: center; @@ -593,9 +590,6 @@ width: 100%; z-index: $z-index-modal; background-color: var(--overlay-color); - &.onboarding-a-b-test { - background-color: var(--overlay-color-onboarding-a-b-test); - } } .modal-container-base { @@ -665,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; diff --git a/frontend/resources/styles/common/refactor/color-defs.scss b/frontend/resources/styles/common/refactor/color-defs.scss index c7048003d..ade8d8991 100644 --- a/frontend/resources/styles/common/refactor/color-defs.scss +++ b/frontend/resources/styles/common/refactor/color-defs.scss @@ -9,84 +9,49 @@ :root { // DARK // Dark background - --db-primary: #18181a; - --db-primary-60: #{color.change(#18181a, $alpha: 0.6)}; - --db-primary-90: #{color.change(#18181a, $alpha: 0.9)}; - --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-primary-90: #{color.change(#ffffff, $alpha: 0.9)}; - --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 + --status-color-info-500: #0e9be9; // used on pixel grid and status widget //GENERIC - --color-canvas: #e8e9ea; + --color-canvas: #e8e9ea; // Not defined on DS // 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 79ffa2542..ed30f20a2 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,13 +95,13 @@ cursor: pointer; svg { - stroke: $df-secondary; + stroke: var(--color-foreground-secondary); fill: none; width: $s-16; height: $s-16; &:hover { - stroke: $da-tertiary; + stroke: var(--color-accent-tertiary); fill: none; } } @@ -122,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); } } } @@ -146,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 b06f8723e..952beb6cc 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 72435730b..4ca89d408 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); @@ -238,10 +238,11 @@ --assets-item-background-color-drag: transparent; --assets-item-border-color-drag: var(--color-accent-primary-muted); --assets-component-background-color: var(--color-canvas); - --assets-component-background-color-disabled: var(--df-secondary;); + --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); @@ -323,25 +324,25 @@ --alert-icon-foreground-color-default: var(--color-foreground-primary); --alert-border-color-default: var(--color-background-quaternary); - --alert-background-color-success: var(--color-success-background); + --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-text-foreground-color-focus: var(--color-accent-primary); --alert-border-color-focus: var(--color-accent-primary); @@ -354,7 +355,7 @@ // 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); @@ -372,6 +373,7 @@ --pill-foreground-color: 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); diff --git a/frontend/resources/styles/common/refactor/fonts.scss b/frontend/resources/styles/common/refactor/fonts.scss index 81a8b5f67..015555225 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 1a9c06ab8..f2d734ccf 100644 --- a/frontend/resources/styles/common/refactor/mixins.scss +++ b/frontend/resources/styles/common/refactor/mixins.scss @@ -4,18 +4,6 @@ // // Copyright (c) KALEIDOS INC -@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 flexCenter { display: flex; justify-content: center; @@ -47,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; @@ -55,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; @@ -84,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; @@ -92,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; @@ -100,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; diff --git a/frontend/resources/styles/common/refactor/themes/default-theme.scss b/frontend/resources/styles/common/refactor/themes/default-theme.scss index 2b5feb06a..f7d092338 100644 --- a/frontend/resources/styles/common/refactor/themes/default-theme.scss +++ b/frontend/resources/styles/common/refactor/themes/default-theme.scss @@ -7,40 +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); - --overlay-color-onboarding-a-b-test: var(--db-primary-90); - - --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 01e98c6cb..8baec1aa9 100644 --- a/frontend/resources/styles/common/refactor/themes/light-theme.scss +++ b/frontend/resources/styles/common/refactor/themes/light-theme.scss @@ -7,40 +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); - --overlay-color-onboarding-a-b-test: var(--lb-primary-90); - - --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/main-default.scss b/frontend/resources/styles/main-default.scss index 0f4f8e621..dfd83571d 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/dependencies/animations"; -@import "common/dependencies/z-index"; -@import "common/dependencies/highlightjs-theme"; - -@import "animate"; @import "common/refactor/color-defs.scss"; +@import "common/dependencies/fonts"; +@import "common/dependencies/animations"; +@import "common/dependencies/highlight.scss"; + @import "common/refactor/themes.scss"; @import "common/refactor/design-tokens.scss"; @@ -33,20 +22,11 @@ //################################################# @import "common/base"; -@import "main/layouts/main-layout"; -@import "main/layouts/not-found"; //################################################# // Commons //################################################# -@import "common/framework"; +// TODO: remove this stylesheet once the new text editor is in place +// https: //tree.taiga.io/project/penpot/us/8165 @import "main/partials/texts"; - -//################################################# -// Partials -//################################################# - -@import "main/partials/debug-icons-preview"; -@import "main/partials/loader"; -@import "main/partials/workspace"; diff --git a/frontend/resources/styles/main/layouts/main-layout.scss b/frontend/resources/styles/main/layouts/main-layout.scss deleted file mode 100644 index fbe768447..000000000 --- a/frontend/resources/styles/main/layouts/main-layout.scss +++ /dev/null @@ -1,52 +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 - -.main-content { - display: flex; - height: 100%; - position: relative; -} - -.dashboard-layout { - background-color: $color-white; - display: grid; - grid-template-rows: 50px 1fr; - grid-template-columns: 40px 256px 1fr; - height: 100vh; - - .dashboard-sidebar { - grid-row: 1 / span 2; - grid-column: 1 / span 2; - padding: 1rem; - } - - .dashboard-content { - grid-row: 1 / span 2; - padding: 1rem 1rem 0 0; - } -} - -.dashboard-content { - display: flex; - flex-direction: column; - position: relative; -} - -.verify-token { - display: flex; - justify-content: center; - align-items: center; - height: 100vh; - - svg#loader-pencil { - fill: $color-gray-50; - } -} - -#screenshot { - display: flex; - flex-direction: column; -} 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 d9cda8242..000000000 --- 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/partials/debug-icons-preview.scss b/frontend/resources/styles/main/partials/debug-icons-preview.scss deleted file mode 100644 index 4b72bd0a7..000000000 --- a/frontend/resources/styles/main/partials/debug-icons-preview.scss +++ /dev/null @@ -1,70 +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 { - stroke: #aab5ba; - } - - .cursor-item { - height: auto; - } -} diff --git a/frontend/resources/styles/main/partials/loader.scss b/frontend/resources/styles/main/partials/loader.scss deleted file mode 100644 index ce2542d2d..000000000 --- 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: 40; -} - -// 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/texts.scss b/frontend/resources/styles/main/partials/texts.scss index ad53796b6..aab38a496 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; diff --git a/frontend/resources/styles/main/partials/workspace.scss b/frontend/resources/styles/main/partials/workspace.scss deleted file mode 100644 index 088add928..000000000 --- a/frontend/resources/styles/main/partials/workspace.scss +++ /dev/null @@ -1,363 +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-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; - overflow: hidden; - pointer-events: none; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - z-index: 10; - - .pixel-overlay { - left: 0; - pointer-events: initial; - position: absolute; - top: 0; - right: 0; - bottom: 0; - 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/index.mustache b/frontend/resources/templates/index.mustache index b5967f94d..7846bb111 100644 --- a/frontend/resources/templates/index.mustache +++ b/frontend/resources/templates/index.mustache @@ -41,6 +41,7 @@ {{> ../public/images/sprites/symbol/icons.svg }} {{> ../public/images/sprites/symbol/cursors.svg }} + {{> ../public/images/sprites/assets.svg }}
diff --git a/frontend/resources/templates/preview-body.mustache b/frontend/resources/templates/preview-body.mustache index fc2683716..4ade10b08 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 000000000..fdbcabf55 --- /dev/null +++ b/frontend/resources/templates/preview-head.mustache @@ -0,0 +1,10 @@ + + diff --git a/frontend/scripts/_helpers.js b/frontend/scripts/_helpers.js index 0e284111d..b8fbd0de5 100644 --- a/frontend/scripts/_helpers.js +++ b/frontend/scripts/_helpers.js @@ -27,6 +27,8 @@ export function startWorker() { }); } +export const isDebug = process.env.NODE_ENV !== "production"; + async function findFiles(basePath, predicate, options = {}) { predicate = predicate ?? @@ -34,7 +36,9 @@ async function findFiles(basePath, predicate, options = {}) { return true; }; - let files = await fs.readdir(basePath, { recursive: options.recursive ?? false }); + let files = await fs.readdir(basePath, { + recursive: options.recursive ?? false, + }); files = files.map((path) => ph.join(basePath, path)); return files; @@ -73,28 +77,28 @@ export async function compileSass(worker, path, options) { return worker.exec("compileSass", [path, options]); } -export async function compileSassAll(worker) { +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 = "src"; + const sourceDir = ph.join("src", "app", "main", "ui", "ds"); - let files = await fs.readdir(sourceDir, { recursive: true }); - files = files.filter((path) => path.endsWith(".scss")); - files = files.map((path) => ph.join(sourceDir, path)); + 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", {})]; - const procs = [ - compileSass(worker, "resources/styles/main-default.scss", {}), - compileSass(worker, "resources/styles/debug.scss", {}), - ]; - - for (let path of files) { + 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, index) => { + (acc, item) => { acc.index[item.outputPath] = item.css; acc.items.push(item.outputPath); return acc; @@ -103,14 +107,42 @@ export async function compileSassAll(worker) { ); } -function compare(a, b) { - if (a < b) { - return -1; - } else if (a > b) { - return 1; - } else { - return 0; +export async function compileSassAll(worker) { + const limitFn = pLimit(4); + const sourceDir = "src"; + + const isDesignSystemFile = (path) => { + return path.startsWith("app/main/ui/ds/"); + }; + + let files = (await fs.readdir(sourceDir, { recursive: true })).filter( + isSassFile, + ); + + const appFiles = files + .filter((path) => !isDesignSystemFile(path)) + .map((path) => ph.join(sourceDir, path)); + const dsFiles = files + .filter(isDesignSystemFile) + .map((path) => ph.join(sourceDir, path)); + + const procs = [compileSass(worker, "resources/styles/main-default.scss", {})]; + + for (let path of [...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) { @@ -173,7 +205,7 @@ async function renderTemplate(path, context = {}, partials = {}) { context = Object.assign({}, context, { ts: ts, - isDebug: process.env.NODE_ENV !== "production", + isDebug, }); return mustache.render(content, context, partials); @@ -232,7 +264,9 @@ async function readTranslations() { lang = lang[0]; } - const content = await fs.readFile(`./translations/${filename}`, { encoding: "utf-8" }); + const content = await fs.readFile(`./translations/${filename}`, { + encoding: "utf-8", + }); lang = lang.toLowerCase(); @@ -291,15 +325,30 @@ async function generateSvgSprite(files, prefix) { } async function generateSvgSprites() { - await fs.mkdir("resources/public/images/sprites/symbol/", { recursive: true }); + 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); + await fs.writeFile( + "resources/public/images/sprites/symbol/icons.svg", + iconsSprite, + ); const cursors = await findFiles("resources/images/cursors/", isSvgFile); - const cursorsSprite = await generateSvgSprite(icons, "cursor-"); - await fs.writeFile("resources/public/images/sprites/symbol/cursors.svg", cursorsSprite); + 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() { @@ -310,15 +359,28 @@ async function generateTemplates() { 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 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"; + process.env.PENPOT_PLUGIN_DEV === "true" + ? "http://localhost:4200" + : "./plugins-runtime"; content = await renderTemplate( "resources/templates/index.mustache", @@ -333,13 +395,24 @@ async function generateTemplates() { await fs.writeFile("./resources/public/index.html", content); - content = await renderTemplate("resources/templates/preview-body.mustache", { - manifest: manifest, - translations: JSON.stringify(translations), - }); - + 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, + }, + partials, + ); + await fs.writeFile("./.storybook/preview-head.html", content); + content = await renderTemplate("resources/templates/render.mustache", { manifest: manifest, translations: JSON.stringify(translations), @@ -355,6 +428,22 @@ async function generateTemplates() { 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(); @@ -366,6 +455,11 @@ export async function compileStyles() { 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(); @@ -411,7 +505,10 @@ export async function copyAssets() { await syncDirs("resources/images/", "resources/public/images/"); await syncDirs("resources/fonts/", "resources/public/fonts/"); - await syncDirs("resources/plugins-runtime/", "resources/public/plugins-runtime/"); + 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 index eab272fbf..47e333664 100644 --- a/frontend/scripts/_worker.js +++ b/frontend/scripts/_worker.js @@ -17,18 +17,21 @@ async function compileFile(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 + loadPaths: [ + "node_modules/animate.css", + "resources/styles/common/", + "resources/styles", + ], + sourceMap: false, }); // console.dir(result); resolve({ inputPath: path, outputPath: dest, - css: result.css + css: result.css, }); } catch (cause) { // console.error(cause); @@ -56,7 +59,7 @@ function configureModulesProcessor(options) { }); } -function configureProcessor(options={}) { +function configureProcessor(options = {}) { const processors = []; if (options.modules) { @@ -78,7 +81,7 @@ async function postProcessFile(data, options) { }); return Object.assign(data, { - css: result.css + css: result.css, }); } @@ -87,11 +90,14 @@ async function compile(path, options) { return await postProcessFile(result, options); } -wpool.worker({ - compileSass: compile -}, { - onTerminate: async (code) => { - // log.info("worker: terminate"); - await compiler.dispose(); - } -}); +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 2b462db6a..97199d20d 100755 --- a/frontend/scripts/build +++ b/frontend/scripts/build @@ -4,10 +4,13 @@ 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) @@ -17,11 +20,18 @@ yarn install || exit 1; rm -rf resources/public; rm -rf target/dist; -clojure -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}\"}" $EXTRA_PARAMS || exit 1 +clojure -M:dev:shadow-cljs release main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS || exit 1 + +yarn run build:app:assets || exit 1; -yarn run compile || 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 000000000..902f4c39e --- /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 000000000..c0eb37a36 --- /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/compile.js b/frontend/scripts/compile.js deleted file mode 100644 index e04d07001..000000000 --- a/frontend/scripts/compile.js +++ /dev/null @@ -1,10 +0,0 @@ -import fs from "node:fs/promises"; -import ppt from "pretty-time"; -import log from "fancy-log"; -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/e2e-server.js b/frontend/scripts/e2e-server.js index 4c441d21c..77be5fcca 100644 --- a/frontend/scripts/e2e-server.js +++ b/frontend/scripts/e2e-server.js @@ -1,11 +1,18 @@ import express from "express"; +import compression from "compression"; + import { fileURLToPath } from "url"; import path from "path"; const app = express(); const port = 3000; -const staticPath = path.join(fileURLToPath(import.meta.url), "../../resources/public"); +app.use(compression()); + +const staticPath = path.join( + fileURLToPath(import.meta.url), + "../../resources/public", +); app.use(express.static(staticPath)); app.listen(port, () => { diff --git a/frontend/scripts/find-unused-translations.js b/frontend/scripts/find-unused-translations.js index 3369e4dce..7770031f5 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); +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 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/translations.js b/frontend/scripts/translations.js new file mode 100755 index 000000000..a781a45d8 --- /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 f6ec245ca..000000000 --- a/frontend/scripts/validate-translations.js +++ /dev/null @@ -1,31 +0,0 @@ -import {promises as fs} from 'fs'; -import gt from 'gettext-parser'; -import l from 'lodash'; -import path from '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 000000000..a82a66932 --- /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 index 6d7924752..99ced7b70 100644 --- a/frontend/scripts/watch.js +++ b/frontend/scripts/watch.js @@ -12,13 +12,11 @@ let sass = null; async function compileSassAll() { const start = process.hrtime(); log.info("init: compile styles"); - try { - sass = await h.compileSassAll(worker); - let output = await h.concatSass(sass); - await fs.writeFile("./resources/public/css/main.css", output); - } catch (error) { - log.error("Error during compileSassAll: ", error); - } + + 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)})`); } @@ -26,20 +24,18 @@ async function compileSassAll() { async function compileSass(path) { const start = process.hrtime(); log.info("changed:", path); - try { - const result = await h.compileSass(worker, path, { modules: true }); - sass.index[result.outputPath] = result.css; + const result = await h.compileSass(worker, path, { modules: true }); + sass.index[result.outputPath] = result.css; - const output = h.concatSass(sass); + const output = h.concatSass(sass); + + await fs.writeFile("./resources/public/css/main.css", output); - await fs.writeFile("./resources/public/css/main.css", output); - } catch (error) { - log.error("Error during compileSass: ", error); - } 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(); @@ -68,14 +64,22 @@ h.watch("resources/templates", null, async function (path) { await h.compileTemplates(); }); -log.info("watch: assets (~)"); -h.watch(["resources/images", "resources/fonts", "resources/plugins-runtime"], null, async function (path) { +log.info("watch: translations (~)"); +h.watch("translations", null, async function (path) { log.info("changed:", path); - await h.compileSvgSprites(); - await h.copyAssets(); await h.compileTemplates(); }); -process.on("exit", () => { - worker.terminate(); -}); +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 813e319c1..ef001a93d 100644 --- a/frontend/shadow-cljs.edn +++ b/frontend/shadow-cljs.edn @@ -92,12 +92,8 @@ {: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 8f93adf39..908ae3ed5 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/technical-guide/plugins/getting-started/#examples")) (defn- normalize-uri [uri-str] @@ -130,9 +132,16 @@ (def worker-uri (obj/get global "penpotWorkerURI" "/js/worker.js")) -(defn external-feature-flag [flag value] - (when-let [fn (obj/get global "externalFeatureFlag")] - (fn flag value))) +(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 @@ -158,6 +167,10 @@ (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)) diff --git a/frontend/src/app/libs/file_builder.cljs b/frontend/src/app/libs/file_builder.cljs index 63df0eb87..1d8e2815f 100644 --- a/frontend/src/app/libs/file_builder.cljs +++ b/frontend/src/app/libs/file_builder.cljs @@ -12,13 +12,13 @@ [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 $ @@ -262,12 +262,16 @@ (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] (binding [cfeat/*current* cfeat/default-features] diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 522715331..3cbbe0d70 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -105,8 +105,7 @@ (rx/map deref) (rx/filter du/is-authenticated?) (rx/take 1) - (rx/map #(ws/initialize)) - (rx/tap #(plugins/init!))))))) + (rx/map #(ws/initialize))))))) (defn ^:export init [] @@ -116,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 000000000..5f8229eb1 --- /dev/null +++ b/frontend/src/app/main/data/changes.cljs @@ -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.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/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/common.cljs b/frontend/src/app/main/data/common.cljs index 4bab615e9..839dd5c29 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -13,6 +13,7 @@ [app.main.data.modal :as modal] [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! msg/hide)) + (defn handle-notification [{:keys [message code level] :as params}] (ptk/reify ::show-notification @@ -75,6 +80,15 @@ :actions [{:label "Refresh" :callback force-reload!}] :tag :notification))) + :maintenance + (rx/of (msg/dialog + :content (tr "notifications.by-code.maintenance") + :controls :inline-actions + :type level + :actions [{:label (tr "labels.accept") + :callback hide-notifications!}] + :tag :notification)) + (rx/of (msg/dialog :content message :controls :close diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 267e299e2..7a0e3297a 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,7 +422,7 @@ [{: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) @@ -430,7 +431,7 @@ :emails emails :role role :features features}] - (->> (rp/cmd! :create-team-with-invitations params) + (->> (rp/cmd! :create-team-with-invitations (with-meta params (meta it))) (rx/tap on-success) (rx/map team-created) (rx/catch on-error)))))) @@ -553,12 +554,12 @@ :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)))))) @@ -897,8 +898,7 @@ (-> 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 @@ -908,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] @@ -916,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 diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/events.cljs index ec217339c..1e0cc623f 100644 --- a/frontend/src/app/main/data/events.cljs +++ b/frontend/src/app/main/data/events.cljs @@ -168,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) @@ -213,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 9894691a2..d77a4a021 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] diff --git a/frontend/src/app/main/data/messages.cljs b/frontend/src/app/main/data/messages.cljs index 024fec415..b02eb7d75 100644 --- a/frontend/src/app/main/data/messages.cljs +++ b/frontend/src/app/main/data/messages.cljs @@ -15,42 +15,42 @@ (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:message + [: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 valid-message? + (sm/validator schema:message)) (defn show [data] (dm/assert! "expected valid message map" - (sm/check! schema:message data)) + (valid-message? data)) (ptk/reify ::show ptk/UpdateEvent @@ -76,14 +76,7 @@ (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)))))) + (dissoc state :message)))) (defn hide-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 000000000..9a17eda3f --- /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/users.cljs b/frontend/src/app/main/data/users.cljs index e49925038..0d6461979 100644 --- a/frontend/src/app/main/data/users.cljs +++ b/frontend/src/app/main/data/users.cljs @@ -317,8 +317,7 @@ (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))))) + (set-current-team! nil))))) (defn logout ([] (logout {})) @@ -328,11 +327,15 @@ (-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 @@ -565,10 +568,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 @@ -693,15 +695,20 @@ (ptk/reify ::show-redirect-error ptk/WatchEvent (watch [_ _ _] - (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") - :else - (tr "errors.generic"))] + (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 (msg/warn hint)))))) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index a45e75939..456413018 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -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 ea52674f5..fd898824d 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -19,6 +19,7 @@ [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] @@ -34,14 +35,15 @@ [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.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] @@ -59,7 +61,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] @@ -87,6 +88,7 @@ [potok.v2.core :as ptk])) (def default-workspace-local {:zoom 1}) +(log/set-level! :debug) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Workspace Initialization @@ -341,15 +343,32 @@ :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 msg/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 @@ -460,8 +479,8 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (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 ev/Event (-data [_] @@ -549,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) @@ -671,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" @@ -808,15 +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) - changes (cls/generate-relocate-shapes (pcb/empty-changes it) - objects - parents - parent-id - page-id - to-index - 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) @@ -982,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 [] @@ -996,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 @@ -1077,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 @@ -1258,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 @@ -1271,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 @@ -1280,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})))))) @@ -1997,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 ac9e06dee..013a2dbe6 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 %)))] + ([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))] - (rx/of (dch/commit-changes changes) - (dws/select-shapes (d/ordered-set shape-id))))))))) + (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 56fbb2411..000000000 --- a/frontend/src/app/main/data/workspace/changes.cljs +++ /dev/null @@ -1,264 +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.logic.shapes :as cls] - [app.common.schema :as sm] - [app.common.types.shape-tree :as ctst] - [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 (-> (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)) - 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 5e9028424..dc0a44d4a 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 @@ -288,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] @@ -296,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] @@ -309,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] @@ -317,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)))))) @@ -333,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)))))) @@ -377,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) @@ -590,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) @@ -605,12 +606,17 @@ (-> state (dissoc :gradient :stops :editing-stop) - (cond-> (not= :image (:type state)) + (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 4718b252c..69e2a77eb 100644 --- a/frontend/src/app/main/data/workspace/comments.cljs +++ b/frontend/src/app/main/data/workspace/comments.cljs @@ -12,9 +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.events :as ev] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwco] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.state-helpers :as wsh] diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index d140bdb6b..587995105 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,136 +27,11 @@ [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 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/workspace/fix_bool_contents.cljs b/frontend/src/app/main/data/workspace/fix_bool_contents.cljs index 8d0ac516e..5cb1c493a 100644 --- a/frontend/src/app/main/data/workspace/fix_bool_contents.cljs +++ b/frontend/src/app/main/data/workspace/fix_bool_contents.cljs @@ -8,7 +8,8 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] - [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])) @@ -82,9 +83,9 @@ :objects (-> component migrate-component :objects)})) components)] - (rx/of (dch/update-shapes ids #(update-shape % objects) {:reg-objects? false - :save-undo? false - :ignore-tree true})) + (rx/of (dwsh/update-shapes ids #(update-shape % objects) {:reg-objects? false + :save-undo? false + :ignore-tree true})) (if (empty? component-changes) (rx/empty) 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 c110de09b..ae19af68a 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 31ec4176e..f79db6867 100644 --- a/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs +++ b/frontend/src/app/main/data/workspace/fix_deleted_fonts.cljs @@ -9,7 +9,8 @@ [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.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] [app.main.fonts :as fonts] [beicon.v2.core :as rx] @@ -111,19 +112,19 @@ typographies)] (rx/concat - (rx/of (dch/update-shapes ids #(fix-deleted-font-shape %) {:reg-objects? false - :save-undo? false - :ignore-tree true})) + (rx/of (dwsh/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 + (rx/of (dwc/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 + (rx/of (dwc/commit-changes {:origin it :redo-changes typography-changes :undo-changes [] :save-undo? false})))))))) diff --git a/frontend/src/app/main/data/workspace/grid.cljs b/frontend/src/app/main/data/workspace/grid.cljs index 50a235107..beaff9e61 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 8d76c38ca..a0b71b509 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 43bed0489..04beaa553 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -16,7 +16,7 @@ [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.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] @@ -198,12 +198,13 @@ (dws/select-shapes (d/ordered-set (:id group)))) (ptk/data-event :layout/update {:ids parents})))))))) -(def group-selected +(defn group-selected + [] (ptk/reify ::group-selected ptk/WatchEvent (watch [_ state _] (let [selected (wsh/lookup-selected state)] - (rx/of (group-shapes nil selected)))))) + (rx/of (group-shapes nil selected :change-selection? true)))))) (defn ungroup-shapes [ids & {:keys [change-selection?] :or {change-selection? false}}] @@ -258,76 +259,84 @@ (when change-selection? (dws/select-shapes child-ids)))))))) -(def ungroup-selected +(defn ungroup-selected + [] (ptk/reify ::ungroup-selected ptk/WatchEvent (watch [_ state _] (let [selected (wsh/lookup-selected state)] (rx/of (ungroup-shapes selected :change-selection? true)))))) -(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 (pcb/empty-changes it) (uuid/next) objects page-id shapes "Mask" 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)] + 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)))))))) + (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 - ptk/WatchEvent - (watch [it state _] - (let [page-id (:current-page-id state) - objects (wsh/lookup-page-objects state page-id) +(defn unmask-group + ([] + (unmask-group nil)) - masked-groups (->> (wsh/lookup-selected state) - (map #(get objects %)) - (filter #(or (= :bool (:type %)) (= :group (:type %))))) + ([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) - 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)] + masked-groups (->> (d/nilv ids (wsh/lookup-selected state)) + (map #(get objects %)) + (filter #(or (= :bool (:type %)) (= :group (:type %))))) - (rx/of (dch/commit-changes changes)))))) + 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 2c7c6c2c0..4e2895bb2 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -11,8 +11,8 @@ [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.common.types.page :as ctp] + [app.main.data.changes :as dwc] [app.main.data.events :as ev] - [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])) @@ -42,7 +42,7 @@ (-> (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] @@ -66,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] @@ -79,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 1aad31f2f..2fb10ada8 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) @@ -260,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) @@ -364,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 afde3c03a..3425a16a4 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 c4a6b5985..85a10efff 100644 --- a/frontend/src/app/main/data/workspace/layout.cljs +++ b/frontend/src/app/main/data/workspace/layout.cljs @@ -20,6 +20,7 @@ :comments :assets :document-history + :hide-palettes :colorpalette :element-options :rulers @@ -138,7 +139,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 585ab58a5..6d7aafc56 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -24,15 +24,17 @@ [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.workspace :as-alias dw] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.notifications :as-alias dwn] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.specialized-panel :as dwsp] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] @@ -54,7 +56,6 @@ ;; 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)) @@ -106,24 +107,28 @@ (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 - ev/Event - (-data [_] color) + ([color] + (add-color color nil)) - 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 {: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] @@ -336,49 +341,56 @@ (defn- add-component2 "This is the second step of the component creation." - [selected components-v2] - (ptk/reify ::add-component2 - ev/Event - (-data [_] - {::ev/name "add-component" - :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] - (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}))))))))) + 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 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." @@ -441,7 +453,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." @@ -531,7 +543,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)) @@ -554,6 +566,10 @@ 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)]}) @@ -599,7 +615,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) @@ -611,7 +626,7 @@ changes (when can-detach? (reduce (fn [changes id] - (cll/generate-detach-instance changes container libraries id)) + (cll/generate-detach-component changes id file page-id libraries)) (pcb/empty-changes it) selected))] @@ -799,7 +814,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)) @@ -1027,6 +1042,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." @@ -1148,14 +1166,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))) @@ -1165,7 +1184,7 @@ (if (d/not-empty? changed-components) (if save-undo? - (do (log/info :msg "DETECTED COMPONENTS CHANGED" + (do (log/info :hint "detected component changes" :ids (map str changed-components) :undo-group undo-group) @@ -1174,7 +1193,8 @@ ;; 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/map touch-component))) + (rx/empty))))) changes-s @@ -1188,7 +1208,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 @@ -1266,18 +1286,20 @@ 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] diff --git a/frontend/src/app/main/data/workspace/media.cljs b/frontend/src/app/main/data/workspace/media.cljs index 50105e9b0..b3f5d48ec 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.workspace.libraries :as dwl] [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.state-helpers :as wsh] @@ -131,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) @@ -467,4 +467,5 @@ (watch [_ _ _] (->> (svg->clj [name svg-string]) (rx/take 1) - (rx/map #(svg/add-svg-shapes id % position {:change-selection? false})))))) + (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 d4f8799f7..773b4f146 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] @@ -438,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 ([] @@ -497,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]) @@ -559,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 4bb3a9772..932e9ccfa 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] @@ -84,7 +83,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 +109,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 +179,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 +206,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 +217,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 dd188e72e..43ad6fdc0 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 3841c3f76..9b562723e 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] @@ -333,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)) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 164e37acb..a91532b0a 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?))))))) @@ -224,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) @@ -262,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?]}] @@ -351,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 2facaf53a..b52ab6e72 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/shapes_to_path.cljs b/frontend/src/app/main/data/workspace/path/shapes_to_path.cljs index c73ad3dfc..d6367aefd 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/streams.cljs b/frontend/src/app/main/data/workspace/path/streams.cljs index 38d0efd50..f860ca586 100644 --- a/frontend/src/app/main/data/workspace/path/streams.cljs +++ b/frontend/src/app/main/data/workspace/path/streams.cljs @@ -101,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 e75b53fc3..0c17182c3 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 c0032c6c8..000000000 --- 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 f05cf5d5e..1f29cbcc3 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -18,9 +18,9 @@ [app.common.record :as cr] [app.common.types.component :as ctk] [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.specialized-panel :as-alias dwsp] [app.main.data.workspace.state-helpers :as wsh] diff --git a/frontend/src/app/main/data/workspace/shape_layout.cljs b/frontend/src/app/main/data/workspace/shape_layout.cljs index 76a346c00..3f7440c06 100644 --- a/frontend/src/app/main/data/workspace/shape_layout.cljs +++ b/frontend/src/app/main/data/workspace/shape_layout.cljs @@ -20,8 +20,8 @@ [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] @@ -148,8 +148,8 @@ 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)))))) @@ -188,8 +188,8 @@ (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) - (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)) + (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))) @@ -199,8 +199,8 @@ (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) - (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)))) + (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))))))) @@ -213,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)))))) @@ -266,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)))))) @@ -280,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 @@ -313,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 @@ -387,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 @@ -433,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 @@ -525,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 @@ -546,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 @@ -570,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 @@ -636,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 %]))) @@ -668,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 1918aa0f3..fecb3f8e0 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -16,8 +16,8 @@ [app.common.types.container :as ctn] [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] + [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] @@ -26,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 {})) @@ -227,7 +294,7 @@ 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} :undo-group undo-group})))))) + (rx/of (update-shapes ids update-fn {:attrs #{:blocked :hidden} :undo-group undo-group})))))) (defn toggle-visibility-selected [] @@ -235,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 [] @@ -243,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 @@ -273,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 6ca11f33e..87346de67 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -14,7 +14,6 @@ [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] @@ -28,7 +27,8 @@ [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 @@ -51,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" @@ -120,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") @@ -437,14 +437,16 @@ :command (ds/a-mod "p") :subsections [:panels] :fn #(do (r/set-resize-type! :bottom) - (emit-when-no-readonly (dw/remove-layout-flag :textpalette) + (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 :colorpalette) + (emit-when-no-readonly (dw/remove-layout-flag :hide-palettes) + (dw/remove-layout-flag :colorpalette) (toggle-layout-flag :textpalette)))} :hide-ui {:tooltip "\\" @@ -562,7 +564,9 @@ :command (ds/c-mod "alt+p") :subsections [:basics] :fn #(when (features/active-feature? @st/state "plugins/runtime") - (st/emit! (modal/show :plugin-management {})))}}) + (st/emit! + (ptk/event ::ev/event {::ev/name "open-plugins-manager" ::ev/origin "workspace:shortcuts"}) + (modal/show :plugin-management {})))}}) (def debug-shortcuts ;; PREVIEW diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index d81d8ecb3..169e2dd3e 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -14,7 +14,7 @@ [app.common.svg.shapes-builder :as csvg.shapes-builder] [app.common.types.shape-tree :as ctst] [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.state-helpers :as wsh] [app.main.data.workspace.undo :as dwu] @@ -64,7 +64,8 @@ ([svg-data position] (add-svg-shapes nil svg-data position nil)) - ([id svg-data position {:keys [change-selection?] :or {change-selection? false}}] + ([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 _] @@ -73,7 +74,7 @@ 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) + selected (if ignore-selection? #{} (wsh/lookup-selected state)) base (cfh/get-base-shape objects selected) selected-id (first selected) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index bf67c549f..4d9785b67 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,8 +372,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-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] @@ -337,7 +431,7 @@ (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 @@ -390,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)))))))) @@ -532,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)))))))) @@ -600,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)] + ([typography file-id] + (apply-typography nil typography file-id)) - (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))))))) + ([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}] @@ -677,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 e2cb1cacc..625c207c6 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)))) +(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}) ;; Defines the thumbnail queue (defonce queue - (q/create resolve-request (/ 1000 30))) - -(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}) - -(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)))))) + (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)) + (update state :workspace-thumbnails + (fn [thumbs] + (if-let [uri (get thumbs object-id)] + (do (vreset! pending uri) + (dissoc thumbs object-id)) + thumbs)))) - state))))))) + 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))) + (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")}] - ;; 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))))))))) + (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 + ;; All commits stream, indepentendly of the source of the commit + all-commits-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 - (->> 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 e381b4bee..c4e2a8064 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -25,7 +25,7 @@ [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] @@ -400,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 ---------------------------------------------------------- @@ -431,7 +432,7 @@ (watch [_ state stream] (let [initial (deref ms/mouse-position) - stopper (mse/drag-stopper stream) + 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 @@ -832,6 +833,30 @@ :ignore-constraints false :ignore-snap-pixel true})))))) +(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 @@ -839,7 +864,14 @@ (watch [it state _] (let [page-id (:current-page-id state) objects (wsh/lookup-page-objects state page-id) - changes (cls/generate-move-shapes-to-frame (pcb/empty-changes it) ids frame-id page-id objects drop-index cell)] + 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) @@ -858,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 :ignore-snap-pixel true})))))) +(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 [] - (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 :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 809c9f6a5..41f3fe1a1 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/fonts.cljs b/frontend/src/app/main/fonts.cljs index 31949e7fe..d563da84e 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])) @@ -249,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 d69c319c2..65efa9f93 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -45,6 +45,9 @@ (def export (l/derived :export st/state)) +(def persistence + (l/derived :persistence st/state)) + ;; ---- Dashboard refs (def dashboard-local diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 16c961b5d..a371a67d3 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 diff --git a/frontend/src/app/main/repo.cljs b/frontend/src/app/main/repo.cljs index ed71b827a..b19edf933 100644 --- a/frontend/src/app/main/repo.cljs +++ b/frontend/src/app/main/repo.cljs @@ -10,6 +10,7 @@ [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] @@ -93,11 +94,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 +138,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 +149,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 +170,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/store.cljs b/frontend/src/app/main/store.cljs index 7b02335e7..703b3952d 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 a1870314a..ea9a0242a 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 7b2fb0296..60803b2ee 100644 --- a/frontend/src/app/main/ui.cljs +++ b/frontend/src/app/main/ui.cljs @@ -10,12 +10,14 @@ [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.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.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] @@ -74,11 +76,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 @@ -96,19 +94,43 @@ #_[:& 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 {}] + (let [show-question-modal? + (and (contains? cf/flags :onboarding) + (not (:onboarding-viewed props)) + (not (contains? props :onboarding-questions))) - (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)}])) + 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)))] + + (cond + show-question-modal? + [:& questions-modal] + + show-newsletter-modal? + [:& onboarding-newsletter] + + show-team-modal? + [:& onboarding-team-modal] + + 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] diff --git a/frontend/src/app/main/ui/auth.cljs b/frontend/src/app/main/ui/auth.cljs index b8408856e..c22ec0902 100644 --- a/frontend/src/app/main/ui/auth.cljs +++ b/frontend/src/app/main/ui/auth.cljs @@ -44,6 +44,9 @@ {::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) error (:error params)] @@ -55,8 +58,9 @@ (st/emit! (du/show-redirect-error error)))) [:main {:class (stl/css :auth-section)} - [:h1 {:class (stl/css :logo-container)} - [:a {:href "#/" :title "Penpot" :class (stl/css :logo-btn)} i/logo]] + (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] diff --git a/frontend/src/app/main/ui/auth.scss b/frontend/src/app/main/ui/auth.scss index f2d41c34d..569fa7b9c 100644 --- a/frontend/src/app/main/ui/auth.scss +++ b/frontend/src/app/main/ui/auth.scss @@ -31,6 +31,7 @@ display: flex; justify-content: flex-start; width: $s-120; + height: $s-96; margin-block-end: $s-52; } @@ -43,7 +44,7 @@ svg { width: 100%; - fill: $df-primary; + fill: var(--color-foreground-primary); height: auto; } diff --git a/frontend/src/app/main/ui/auth/common.scss b/frontend/src/app/main/ui/auth/common.scss index 0a018fb77..872ce47dd 100644 --- a/frontend/src/app/main/ui/auth/common.scss +++ b/frontend/src/app/main/ui/auth/common.scss @@ -10,14 +10,22 @@ width: 100%; padding-block-end: 0; display: grid; - gap: $s-24; + gap: $s-12; form { display: flex; flex-direction: column; gap: $s-12; + margin-top: $s-12; } } +.auth-title-wrapper { + width: 100%; + padding-block-end: 0; + display: grid; + gap: $s-8; +} + .separator { border-color: var(--modal-separator-backogrund-color); margin: 0; diff --git a/frontend/src/app/main/ui/auth/login.cljs b/frontend/src/app/main/ui/auth/login.cljs index ee852b852..27add1c2e 100644 --- a/frontend/src/app/main/ui/auth/login.cljs +++ b/frontend/src/app/main/ui/auth/login.cljs @@ -7,9 +7,8 @@ (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.users :as du] @@ -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? @@ -64,28 +62,18 @@ :else (st/emit! (msg/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)) + (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 @@ -100,7 +88,6 @@ (= :ldap-not-initialized (:code cause))) (st/emit! (msg/error (tr "errors.ldap-disabled"))) - (and (= :restriction (:type cause)) (= :admin-only-profile (:code cause))) (reset! error (tr "errors.profile-blocked")) @@ -160,7 +147,7 @@ [:& context-notification {:type :error :content message - :data-test "login-banner" + :data-testid "login-banner" :role "alert"}]) [:& fm/form {:on-submit on-submit @@ -170,7 +157,7 @@ [:& 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)} @@ -186,7 +173,7 @@ [:div {:class (stl/css :fields-row :forgot-password)} [:& lk/link {:action on-recovery-request :class (stl/css :forgot-pass-link) - :data-test "forgot-password"} + :data-testid "forgot-password"} (tr "auth.forgot-password")]]) [:div {:class (stl/css :buttons-stack)} @@ -194,7 +181,7 @@ (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) @@ -280,7 +267,7 @@ [:div {:class (stl/css :auth-form-wrapper)} [:h1 {:class (stl/css :auth-title) - :data-test "login-title"} (tr "auth.login-account-title")] + :data-testid "login-title"} (tr "auth.login-account-title")] [:p {:class (stl/css :auth-tagline)} (tr "auth.login-tagline")] @@ -299,14 +286,5 @@ (tr "auth.register") " "] [:& lk/link {:action go-register :class (stl/css :register-link) - :data-test "register-submit"} - (tr "auth.register-submit")]]) - - (when (contains? cf/flags :demo-users) - [:div {:class (stl/css :demo-account)} - [:span {:class (stl/css :demo-account-text)} - (tr "auth.create-demo-profile") " "] - [:& lk/link {:action create-demo-profile - :class (stl/css :demo-account-link) - :data-test "demo-account-link"} - (tr "auth.create-demo-account")]])]])) + :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 85657eef6..6ec730c5b 100644 --- a/frontend/src/app/main/ui/auth/recovery.cljs +++ b/frontend/src/app/main/ui/auth/recovery.cljs @@ -7,39 +7,29 @@ (ns app.main.ui.auth.recovery (:require-macros [app.main.style :as stl]) (:require - [app.common.spec :as us] + [app.common.schema :as sm] [app.main.data.messages :as msg] [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! (msg/error (tr "errors.invalid-recovery-token")))) (defn- on-success [_] @@ -56,14 +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 :class (stl/css :recovery-form) :form form} + [:div {:class (stl/css :fields-row)} [:& fm/input {:type "password" :name :password-1 diff --git a/frontend/src/app/main/ui/auth/recovery_request.cljs b/frontend/src/app/main/ui/auth/recovery_request.cljs index b2d116daf..c409a318c 100644 --- a/frontend/src/app/main/ui/auth/recovery_request.cljs +++ b/frontend/src/app/main/ui/auth/recovery_request.cljs @@ -7,8 +7,7 @@ (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.common.schema :as sm] [app.main.data.messages :as msg] [app.main.data.users :as du] [app.main.store :as st] @@ -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! (msg/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,7 +41,7 @@ (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)] @@ -59,13 +52,14 @@ :profile-is-muted (rx/of (msg/error (tr "errors.profile-is-muted"))) - :email-has-permanent-bounces + (:email-has-permanent-bounces + :email-has-complaints) (rx/of (msg/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) @@ -80,13 +74,13 @@ :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)}]])) @@ -106,5 +100,5 @@ [:div {:class (stl/css :go-back)} [:& lk/link {:action go-back :class (stl/css :go-back-link) - :data-test "go-back-link"} + :data-testid "go-back-link"} (tr "labels.go-back")]]])) diff --git a/frontend/src/app/main/ui/auth/register.cljs b/frontend/src/app/main/ui/auth/register.cljs index 61066fb81..e85e3def9 100644 --- a/frontend/src/app/main/ui/auth/register.cljs +++ b/frontend/src/app/main/ui/auth/register.cljs @@ -7,8 +7,7 @@ (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.schema :as sm] [app.config :as cf] [app.main.data.messages :as msg] [app.main.data.users :as du] @@ -18,67 +17,52 @@ [app.main.ui.components.forms :as fm] [app.main.ui.components.link :as lk] [app.main.ui.icons :as i] - [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])) ;; --- PAGE: Register -(defn- validate-password-length - [errors data] - (let [password (:password data)] - (cond-> errors - (> 8 (count password)) - (assoc :password {:message "errors.password-too-short"})))) - -(defn- validate-email - [errors _] - (d/update-when errors :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- on-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"))) - - [:validation :email-as-password] - (swap! form assoc-in [:errors :password] - {:message "errors.email-as-password"}) - - (st/emit! (msg/error (tr "errors.generic")))))) - -(defn- on-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 + {::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-password-length - validate-email - (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-error + (mf/use-fn + (fn [form cause] + (let [{:keys [type code] :as edata} (ex-data cause)] + (condp = [type code] + [:restriction :registration-disabled] + (st/emit! (msg/error (tr "errors.registration-disabled"))) + + [:restriction :email-domain-is-not-allowed] + (st/emit! (msg/error (tr "errors.email-domain-not-allowed"))) + + [:restriction :email-has-permanent-bounces] + (st/emit! (msg/error (tr "errors.email-has-permanent-bounces" (:email edata)))) + + [:restriction :email-has-complaints] + (st/emit! (msg/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! (msg/error (tr "errors.generic"))))))) + on-submit (mf/use-fn (mf/deps on-success-callback) @@ -86,23 +70,21 @@ (reset! submitted? true) (let [cdata (:clean-data @form) on-success (fn [data] - (if (nil? on-success-callback) - (on-prepare-register-success data) - (on-success-callback data))) - on-error (fn [data] - (on-prepare-register-error form 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 on-error)))))] + (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)} @@ -116,7 +98,7 @@ [:> 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 @@ -131,11 +113,11 @@ (mf/defc register-page {::mf/props :obj} [{:keys [params]}] - [:div {:class (stl/css :auth-form-wrapper)} + [:div {:class (stl/css :auth-form-wrapper :register-form)} [:h1 {:class (stl/css :auth-title) - :data-test "registration-title"} (tr "auth.register-title")] + :data-testid "registration-title"} (tr "auth.register-title")] [:p {:class (stl/css :auth-tagline)} - (tr "auth.login-tagline")] + (tr "auth.register-tagline")] (when (contains? cf/flags :demo-warning) [:& login/demo-warning]) @@ -147,7 +129,7 @@ [:span {:class (stl/css :account-text)} (tr "auth.already-have-account") " "] [:& lk/link {:action #(st/emit! (rt/nav :auth-login {} params)) :class (stl/css :account-link) - :data-test "login-here-link"} + :data-testid "login-here-link"} (tr "auth.login-here")]] (when (contains? cf/flags :demo-users) @@ -160,60 +142,78 @@ ;; --- PAGE: register validation -(defn- handle-register-error - [_form _data] - (st/emit! (msg/error (tr "errors.generic")))) +(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)}])] -(defn- handle-register-success - [data] - (cond - (some? (:invitation-token data)) - (let [token (:invitation-token data)] - (st/emit! (rt/nav :auth-verify-token {} {:token token}))) + [: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}]])) - (: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]))) +(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 + {::mf/props :obj + ::mf/private true} [{:keys [params on-success-callback]}] - (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) + (let [form (fm/use-form :schema schema:register-validate-form :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)))) + 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! (msg/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)] (->> (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 + [:& fm/form {:on-submit on-submit + :form form :class (stl/css :register-validate-form)} + [:div {:class (stl/css :fields-row)} [:& fm/input {:name :fullname :label (tr "auth.fullname") @@ -222,18 +222,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") @@ -242,13 +231,15 @@ (mf/defc register-validate-page + {::mf/props :obj} [{:keys [params]}] [:div {:class (stl/css :auth-form-wrapper)} - [: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)}] + [: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}] @@ -259,9 +250,15 @@ (tr "labels.go-back")]]]]) (mf/defc register-success-page - [{:keys [params]}] - [:div {:class (stl/css :auth-form-wrapper :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} + [] + (let [email (::email @sto/storage)] + [:div {:class (stl/css :auth-form-wrapper :register-success)} + [: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")]])) diff --git a/frontend/src/app/main/ui/auth/register.scss b/frontend/src/app/main/ui/auth/register.scss index 9cbc00457..0f0497442 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,30 @@ } } -.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; } diff --git a/frontend/src/app/main/ui/auth/verify_token.cljs b/frontend/src/app/main/ui/auth/verify_token.cljs index a914bd46a..81d92ede5 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.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]] @@ -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! (msg/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! (msg/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! (msg/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 31cfd9a60..5427b29f1 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]] @@ -96,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 @@ -155,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 @@ -181,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} @@ -224,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)} @@ -435,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]}] @@ -558,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) diff --git a/frontend/src/app/main/ui/comments.scss b/frontend/src/app/main/ui/comments.scss index 3c6e569ea..a2d1fdc52 100644 --- a/frontend/src/app/main/ui/comments.scss +++ b/frontend/src/app/main/ui/comments.scss @@ -142,11 +142,10 @@ // thread-content .thread-content { position: absolute; - overflow-y: scroll; - scrollbar-gutter: stable; + overflow-y: auto; width: $s-284; padding: $s-12; - padding-inline-end: 0; + padding-inline-end: $s-8; pointer-events: auto; user-select: text; @@ -236,6 +235,7 @@ .reply-form { textarea { @extend .input-element; + @include bodySmallTypography; line-height: 1.45; height: 100%; width: 100%; diff --git a/frontend/src/app/main/ui/components.cljs b/frontend/src/app/main/ui/components.cljs deleted file mode 100644 index 6119114fc..000000000 --- a/frontend/src/app/main/ui/components.cljs +++ /dev/null @@ -1,20 +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.components - (:require - [app.main.ui.components.buttons.simple-button :as sb] - [rumext.v2 :as mf])) - -(mf/defc story-wrapper - {::mf/wrap-props false} - [{:keys [children]}] - [:.default children]) - -(def default - "A export used for storybook" - #js {:SimpleButton sb/simple-button - :StoryWrapper story-wrapper}) diff --git a/frontend/src/app/main/ui/components/button_link.cljs b/frontend/src/app/main/ui/components/button_link.cljs index fadfbd347..bac6e8e87 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 000000000..81b0538d9 --- /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 fb4bdd995..000000000 --- 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 6c93cc3a2..000000000 --- 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 e1d162fbc..000000000 --- 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 33142e12c..000000000 --- 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 9d1c6c9ac..000000000 --- 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/color_bullet.cljs b/frontend/src/app/main/ui/components/color_bullet.cljs index 0c857cc3d..1a39f1750 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} @@ -112,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 3a8e0e202..37b733f34 100644 --- a/frontend/src/app/main/ui/components/color_bullet.scss +++ b/frontend/src/app/main/ui/components/color_bullet.scss @@ -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 0787b69e7..ea81d8d50 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/file_uploader.cljs b/frontend/src/app/main/ui/components/file_uploader.cljs index 35429e09e..8eebdff51 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 9aec3f302..eef34a8cf 100644 --- a/frontend/src/app/main/ui/components/forms.cljs +++ b/frontend/src/app/main/ui/components/forms.cljs @@ -18,7 +18,6 @@ [app.util.keyboard :as kbd] [app.util.object :as obj] [cljs.core :as c] - [clojure.string] [cuerdas.core :as str] [rumext.v2 :as mf])) @@ -26,7 +25,9 @@ (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)) + (obj/map->obj obj/prop-key-fn)) 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))] + (obj/map->obj obj/prop-key-fn))] [: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}]])) @@ -297,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}] @@ -378,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 @@ -483,41 +554,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 d29571542..b31713aad 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%; @@ -55,6 +54,7 @@ color: var(--input-foreground-color-active); margin-top: 0; width: 100%; + max-width: 100%; height: 100%; padding: 0 $s-8; @@ -64,6 +64,7 @@ border-radius: $br-8; } } + // Input autofill input:-webkit-autofill, input:-webkit-autofill:hover, @@ -92,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; } @@ -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,11 +264,10 @@ // SUBMIT-BUTTON .button-submit { @extend .button-primary; -} - -:disabled { - @extend .button-disabled; - min-height: $s-32; + &:disabled { + @extend .button-disabled; + min-height: $s-32; + } } // MULTI INPUT @@ -368,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 { @@ -394,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); @@ -414,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 6ceee146b..e0c1d90fb 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 data-testid]}] + [{:keys [action class data-testid keyboard-action children]}] (let [keyboard-action (d/nilv keyboard-action action)] [:a {:on-click action :class class @@ -20,6 +20,5 @@ (when ^boolean (kbd/enter? event) (keyboard-action event))) :tab-index "0" - :data-testid data-testid - :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 eb8c5db8e..90da87209 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 134a01440..4a1823868 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) diff --git a/frontend/src/app/main/ui/components/radio_buttons.cljs b/frontend/src/app/main/ui/components/radio_buttons.cljs index 0d7cce294..17a3fe594 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/tab_container.cljs b/frontend/src/app/main/ui/components/tab_container.cljs index 20c79a417..1e3b99079 100644 --- a/frontend/src/app/main/ui/components/tab_container.cljs +++ b/frontend/src/app/main/ui/components/tab_container.cljs @@ -59,6 +59,7 @@ [: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 diff --git a/frontend/src/app/main/ui/confirm.cljs b/frontend/src/app/main/ui/confirm.cljs index 66fd00e92..abb11ea8c 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/cursors.cljs b/frontend/src/app/main/ui/cursors.cljs index 8b4152816..ec76bbd0b 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.scss b/frontend/src/app/main/ui/dashboard.scss index bad41ea11..26d4f051a 100644 --- a/frontend/src/app/main/ui/dashboard.scss +++ b/frontend/src/app/main/ui/dashboard.scss @@ -13,11 +13,6 @@ 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 b3a8e04d2..d87056e00 100644 --- a/frontend/src/app/main/ui/dashboard/change_owner.cljs +++ b/frontend/src/app/main/ui/dashboard/change_owner.cljs @@ -7,25 +7,24 @@ (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) diff --git a/frontend/src/app/main/ui/dashboard/change_owner.scss b/frontend/src/app/main/ui/dashboard/change_owner.scss index 0b150c1c5..0e960020e 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 f200d98f9..7a34437cc 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 b6e872e52..af55f6dd1 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; @@ -82,7 +82,7 @@ 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 4be78a0e2..f270e1efb 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -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 afee2564d..e533a6b85 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 98cf1733e..7c37cd57c 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 be6cd908f..514be108d 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")]]]) @@ -167,7 +167,7 @@ [: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 @@ -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 e520e01a4..fd40fc50d 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 dfffc9c66..46b4cdefd 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -11,6 +11,7 @@ [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.features :as features] @@ -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,7 +49,7 @@ [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 render-thumbnail [file-id revn] @@ -71,15 +73,15 @@ (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 @@ -90,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 @@ -113,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) @@ -365,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?)) @@ -458,7 +464,6 @@ :on-drag-leave on-drag-leave :on-drop on-drop :ref node-ref} - (cond (nil? files) [:& loading-placeholder] diff --git a/frontend/src/app/main/ui/dashboard/grid.scss b/frontend/src/app/main/ui/dashboard/grid.scss index 72f95e4ec..fe58c4825 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 @@ -60,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}); @@ -68,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; @@ -98,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; @@ -117,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%; @@ -133,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; @@ -146,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; @@ -154,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); } } } @@ -176,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; @@ -194,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; } @@ -205,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); } } } @@ -220,7 +223,7 @@ $thumbnail-default-height: $s-168; // Default width width: $s-32; span { - color: $db-secondary; + color: var(--color-background-secondary); } } @@ -275,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 @@ -297,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; @@ -306,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; @@ -319,7 +312,7 @@ $thumbnail-default-height: $s-168; // Default width text-transform: uppercase; .num-assets { - color: $df-secondary; + color: var(--color-foreground-secondary); } } @@ -327,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; @@ -335,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); } @@ -356,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; } @@ -378,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 14f0318e3..bc72e3f29 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -18,6 +18,7 @@ [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)} @@ -489,7 +493,7 @@ :else [:& context-notification - {:type :success + {:type (if (zero? success-num) :warning :success) :content (tr "dashboard.import.import-message" (i18n/c success-num))}])) (for [entry entries] diff --git a/frontend/src/app/main/ui/dashboard/import.scss b/frontend/src/app/main/ui/dashboard/import.scss index 7708be6ef..50083f3df 100644 --- a/frontend/src/app/main/ui/dashboard/import.scss +++ b/frontend/src/app/main/ui/dashboard/import.scss @@ -81,6 +81,7 @@ } .file-name-edit { @extend .input-element; + @include bodySmallTypography; flex-grow: 1; } .file-name-label { @@ -142,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 b2d0276cd..4b4a17eb1 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 dd543c154..78238721e 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 69660e4f0..5a79d8e33 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 8f5daa04e..261fe3c4f 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 f2a37fbf0..a72ebc451 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.scss +++ b/frontend/src/app/main/ui/dashboard/placeholder.scss @@ -12,22 +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; - stroke: $df-secondary; - fill: none; - } - } - &.libs { background-image: url(/images/ph-left.svg), url(/images/ph-right.svg); background-position: @@ -35,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; @@ -46,7 +30,7 @@ .text { a { - color: $df-primary; + color: var(--color-foreground-primary); } p { max-width: $s-360; @@ -57,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; @@ -71,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 bd725a905..2f886686f 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -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 11ab89f5d..deacac12a 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -46,12 +46,12 @@ {::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 @@ -251,7 +251,7 @@ :on-click on-create-click :title (tr "dashboard.new-file") :aria-label (tr "dashboard.new-file") - :data-test "project-new-file" + :data-testid "project-new-file" :on-key-down handle-create-click} add-icon] @@ -259,7 +259,7 @@ :on-click on-menu-click :title (tr "dashboard.options") :aria-label (tr "dashboard.options") - :data-test "project-options" + :data-testid "project-options" :on-key-down handle-menu-click} menu-icon]]]]] diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss index 62b1e1adb..e544896ee 100644 --- a/frontend/src/app/main/ui/dashboard/projects.scss +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -155,7 +155,7 @@ // Team hero .team-hero { - background-color: $db-tertiary; + background-color: var(--color-background-tertiary); border-radius: $br-8; border: none; display: flex; @@ -185,7 +185,7 @@ .title { font-size: $fs-24; - color: $df-primary; + color: var(--color-foreground-primary); font-weight: $fw400; } @@ -193,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; } diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index 3b4d09099..401d33494 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 7c20671c2..f0180ed41 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 10cfd2f5c..245145f44 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -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])])) @@ -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])] @@ -783,7 +786,6 @@ [:li {:class (stl/css-case :current drafts? :sidebar-nav-item true)} [:& link {:action go-drafts - :data-testid "drafts-link-sidebar" :class (stl/css :sidebar-link) :keyboard-action go-drafts-with-key} [:span {:class (stl/css :element-title)} (tr "labels.drafts")]]] @@ -792,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")]]]]] @@ -804,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] @@ -946,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)}] @@ -961,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)}] @@ -971,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") @@ -1001,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") @@ -1025,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")]] @@ -1048,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 f939c8d44..22a0a3c9c 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.scss +++ b/frontend/src/app/main/ui/dashboard/sidebar.scss @@ -331,10 +331,12 @@ } .profile { + @include buttonStyle; display: grid; grid-template-columns: auto 1fr; gap: $s-8; cursor: pointer; + text-align: left; } .profile-fullname { diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 76652e98a..85c7d67ed 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -9,6 +9,7 @@ (: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] @@ -33,7 +34,6 @@ [cuerdas.core :as str] [rumext.v2 :as mf])) - (def ^:private arrow-icon (i/icon-xref :arrow (stl/css :arrow-icon))) @@ -62,10 +62,10 @@ {::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))) + (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) @@ -81,7 +81,7 @@ :team team :origin :team}))))] - [: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") @@ -105,7 +105,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)}])]])) @@ -131,6 +131,12 @@ (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 @@ -139,9 +145,14 @@ (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 "") @@ -157,21 +168,22 @@ (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)) + (fn [_form cause] + (let [{:keys [type code] :as error} (ex-data cause)] + (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))) + (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! (msg/error (tr "errors.generic")) - (modal/hide)))) + :else + (st/emit! (msg/error (tr "errors.generic")) + (modal/hide))))) on-submit (fn [form] @@ -563,22 +575,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 (msg/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 (msg/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 (msg/error (tr "errors.email-has-permanent-bounces" email))) - :else - (rx/throw error)))) + :else + (rx/throw cause))))) on-delete (mf/use-fn @@ -588,7 +602,6 @@ 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 [] @@ -693,8 +706,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}] @@ -746,10 +759,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"} @@ -763,12 +777,12 @@ {::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 [_] @@ -878,8 +892,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 e79a69f63..d914ea773 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 { @@ -378,7 +378,7 @@ } .hero-desc { - color: $df-secondary; + color: var(--color-foreground-secondary); margin-bottom: 0; font-size: $fs-16; max-width: $s-512; @@ -540,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 7a37ec9c6..cc0f37c9f 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.cljs +++ b/frontend/src/app/main/ui/dashboard/team_form.cljs @@ -7,8 +7,9 @@ (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.events :as ev] [app.main.data.messages :as msg] [app.main.data.modal :as modal] [app.main.store :as st] @@ -19,12 +20,11 @@ [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] @@ -51,7 +51,8 @@ (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 d94cb4c28..d57cffb82 100644 --- a/frontend/src/app/main/ui/dashboard/team_form.scss +++ b/frontend/src/app/main/ui/dashboard/team_form.scss @@ -37,6 +37,7 @@ .group-name-input { @extend .input-element-label; + @include bodySmallTypography; margin-bottom: $s-8; label { @include flexColumn; diff --git a/frontend/src/app/main/ui/dashboard/templates.scss b/frontend/src/app/main/ui/dashboard/templates.scss index 76cf2f455..909f2cf2d 100644 --- a/frontend/src/app/main/ui/dashboard/templates.scss +++ b/frontend/src/app/main/ui/dashboard/templates.scss @@ -43,7 +43,7 @@ margin-right: $s-32; position: relative; z-index: $z-index-1; - background-color: $db-quaternary; + 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); } } @@ -116,7 +116,7 @@ height: $s-228; margin-left: $s-6; 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; @@ -139,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); } } @@ -188,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.scss b/frontend/src/app/main/ui/debug/components_preview.scss index eb1d83acd..8a087c9ee 100644 --- a/frontend/src/app/main/ui/debug/components_preview.scss +++ b/frontend/src/app/main/ui/debug/components_preview.scss @@ -93,6 +93,7 @@ } .input-wrapper { @extend .input-element; + @include bodySmallTypography; } } } 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 000000000..9380dc872 --- /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 000000000..673c1b065 --- /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 000000000..85268cf8d --- /dev/null +++ b/frontend/src/app/main/ui/ds.cljs @@ -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.main.ui.ds + (:require + [app.main.ui.ds.buttons.button :refer [button*]] + [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] + [app.main.ui.ds.forms.input :refer [input*]] + [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.product.loader :refer [loader*]] + [app.main.ui.ds.storybook :as sb])) + +(def default + "A export used for storybook" + #js {:Button button* + :Heading heading* + :Icon icon* + :IconButton icon-button* + :Input input* + :Loader loader* + :RawSvg raw-svg* + :Text text* + ;; 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 000000000..a424603d1 --- /dev/null +++ b/frontend/src/app/main/ui/ds/_borders.scss @@ -0,0 +1,12 @@ +// 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); + +$b-1: px2rem(1); diff --git a/frontend/src/app/main/ui/loader.scss b/frontend/src/app/main/ui/ds/_sizes.scss similarity index 69% rename from frontend/src/app/main/ui/loader.scss rename to frontend/src/app/main/ui/ds/_sizes.scss index 71121f51d..f27838b6a 100644 --- a/frontend/src/app/main/ui/loader.scss +++ b/frontend/src/app/main/ui/ds/_sizes.scss @@ -4,8 +4,7 @@ // // Copyright (c) KALEIDOS INC -@import "refactor/common-refactor.scss"; +@use "./utils.scss" as *; -.loader-content { - @extend .loader-base; -} +// TODO: create actual tokens once we have them from design +$sz-32: px2rem(32); diff --git a/frontend/resources/styles/common/dependencies/z-index.scss b/frontend/src/app/main/ui/ds/_utils.scss similarity index 68% rename from frontend/resources/styles/common/dependencies/z-index.scss rename to frontend/src/app/main/ui/ds/_utils.scss index 0275fbe3e..248d43d00 100644 --- a/frontend/resources/styles/common/dependencies/z-index.scss +++ b/frontend/src/app/main/ui/ds/_utils.scss @@ -4,7 +4,9 @@ // // Copyright (c) KALEIDOS INC -$autocomplete: 30000; -$index-lightbox-shadow: 60000; -$index-lightbox: 60001; -$index-lightbox-close-x: 200; +@use "sass:math"; + +@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 000000000..b489b4df9 --- /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 000000000..9dfb2c9b4 --- /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 000000000..5e7b2cfe6 --- /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 000000000..d41e12c2d --- /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 }) =>
-
Hello {{name}}!
+
Hello {{name|abbreviate:25}}!