mirror of
https://github.com/penpot/penpot.git
synced 2025-06-07 10:31:38 +02:00
Merge remote-tracking branch 'penpot/develop' into token-studio-develop
This commit is contained in:
commit
dc14933f3a
116 changed files with 7413 additions and 6245 deletions
|
@ -32,42 +32,42 @@ jobs:
|
||||||
- run: clj-kondo --version
|
- run: clj-kondo --version
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: "fmt check backend [clj]"
|
name: "backend fmt check"
|
||||||
working_directory: "./backend"
|
working_directory: "./backend"
|
||||||
command: |
|
command: |
|
||||||
yarn install
|
yarn install
|
||||||
yarn run fmt:clj:check
|
yarn run fmt:clj:check
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: "fmt check exporter [clj]"
|
name: "exporter fmt check"
|
||||||
working_directory: "./exporter"
|
working_directory: "./exporter"
|
||||||
command: |
|
command: |
|
||||||
yarn install
|
yarn install
|
||||||
yarn run fmt:clj:check
|
yarn run fmt:clj:check
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: "fmt check common [clj]"
|
name: "common fmt check"
|
||||||
working_directory: "./common"
|
working_directory: "./common"
|
||||||
command: |
|
command: |
|
||||||
yarn install
|
yarn install
|
||||||
yarn run fmt:clj:check
|
yarn run fmt:clj:check
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: "fmt check frontend [clj]"
|
name: "frontend fmt check"
|
||||||
working_directory: "./frontend"
|
working_directory: "./frontend"
|
||||||
command: |
|
command: |
|
||||||
yarn install
|
yarn install
|
||||||
yarn run fmt:clj:check
|
yarn run fmt:clj:check
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: common lint
|
name: "common linter check"
|
||||||
working_directory: "./common"
|
working_directory: "./common"
|
||||||
command: |
|
command: |
|
||||||
yarn install
|
yarn install
|
||||||
yarn run lint:clj
|
yarn run lint:clj
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: frontend lint
|
name: "frontend linter check"
|
||||||
working_directory: "./frontend"
|
working_directory: "./frontend"
|
||||||
command: |
|
command: |
|
||||||
yarn install
|
yarn install
|
||||||
|
@ -75,14 +75,14 @@ jobs:
|
||||||
yarn run lint:clj
|
yarn run lint:clj
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: backend lint
|
name: "backend linter check"
|
||||||
working_directory: "./backend"
|
working_directory: "./backend"
|
||||||
command: |
|
command: |
|
||||||
yarn install
|
yarn install
|
||||||
yarn run lint:clj
|
yarn run lint:clj
|
||||||
|
|
||||||
- run:
|
- run:
|
||||||
name: exporter lint
|
name: "exporter linter check"
|
||||||
working_directory: "./exporter"
|
working_directory: "./exporter"
|
||||||
command: |
|
command: |
|
||||||
yarn install
|
yarn install
|
||||||
|
@ -108,7 +108,7 @@ jobs:
|
||||||
command: |
|
command: |
|
||||||
yarn install
|
yarn install
|
||||||
yarn run compile
|
yarn run compile
|
||||||
clojure -M:dev:shadow-cljs compile main
|
yarn run compile:cljs
|
||||||
yarn playwright install --with-deps chromium
|
yarn playwright install --with-deps chromium
|
||||||
yarn e2e:test
|
yarn e2e:test
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ jobs:
|
||||||
name: "backend tests"
|
name: "backend tests"
|
||||||
working_directory: "./backend"
|
working_directory: "./backend"
|
||||||
command: |
|
command: |
|
||||||
clojure -X:dev:test :patterns '["backend-tests.*-test"]'
|
clojure -M:dev:test
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
|
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -57,6 +57,7 @@
|
||||||
/frontend/package-lock.json
|
/frontend/package-lock.json
|
||||||
/frontend/resources/fonts/experiments
|
/frontend/resources/fonts/experiments
|
||||||
/frontend/resources/public/*
|
/frontend/resources/public/*
|
||||||
|
/frontend/storybook-static/
|
||||||
/frontend/target/
|
/frontend/target/
|
||||||
/other/
|
/other/
|
||||||
/scripts/
|
/scripts/
|
||||||
|
|
19
CHANGES.md
19
CHANGES.md
|
@ -1,10 +1,25 @@
|
||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## 2.1.0
|
||||||
|
|
||||||
|
### :rocket: Epics and highlights
|
||||||
|
|
||||||
|
### :boom: Breaking changes & Deprecations
|
||||||
|
|
||||||
|
### :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
|
||||||
|
|
||||||
## 2.0.3
|
## 2.0.3
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
- Fix chrome scrollbar styling [Taiga Issue #7852](https://tree.taiga.io/project/penpot/issue/7852)
|
- Fix chrome scrollbar styling [Taiga Issue #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
|
## 2.0.2
|
||||||
|
|
||||||
|
@ -18,12 +33,14 @@
|
||||||
- Fix color palette sorting [Taiga Issue #7458](https://tree.taiga.io/project/penpot/issue/7458)
|
- Fix color palette sorting [Taiga Issue #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)
|
- Fix style scoping problem with imported SVG [Taiga #7671](https://tree.taiga.io/project/penpot/issue/7671)
|
||||||
|
|
||||||
|
|
||||||
## 2.0.1
|
## 2.0.1
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443)
|
- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443)
|
||||||
|
|
||||||
|
|
||||||
## 2.0.0 - I Just Can't Get Enough
|
## 2.0.0 - I Just Can't Get Enough
|
||||||
|
|
||||||
### :rocket: Epics and highlights
|
### :rocket: Epics and highlights
|
||||||
|
@ -117,7 +134,7 @@
|
||||||
- [REDESIGN] Panels visual separations [Taiga #6692](https://tree.taiga.io/project/penpot/us/6692)
|
- [REDESIGN] Panels visual separations [Taiga #6692](https://tree.taiga.io/project/penpot/us/6692)
|
||||||
- [REDESIGN] Onboarding slides [Taiga #6678](https://tree.taiga.io/project/penpot/us/6678)
|
- [REDESIGN] Onboarding slides [Taiga #6678](https://tree.taiga.io/project/penpot/us/6678)
|
||||||
|
|
||||||
### :bug Bugs fixed
|
### :bug: Bugs fixed
|
||||||
- Fix pixelated thumbnails [Github #3681](https://github.com/penpot/penpot/issues/3681), [Github #3661](https://github.com/penpot/penpot/issues/3661)
|
- Fix pixelated thumbnails [Github #3681](https://github.com/penpot/penpot/issues/3681), [Github #3661](https://github.com/penpot/penpot/issues/3661)
|
||||||
- Fix problem with not applying colors to boards [Github #3941](https://github.com/penpot/penpot/issues/3941)
|
- Fix problem with not applying colors to boards [Github #3941](https://github.com/penpot/penpot/issues/3941)
|
||||||
- Fix problem with path editor undoing changes [Github #3998](https://github.com/penpot/penpot/issues/3998)
|
- Fix problem with path editor undoing changes [Github #3998](https://github.com/penpot/penpot/issues/3998)
|
||||||
|
|
|
@ -78,12 +78,9 @@
|
||||||
:ns-default build}
|
:ns-default build}
|
||||||
|
|
||||||
:test
|
:test
|
||||||
{:extra-paths ["test"]
|
{:main-opts ["-m" "kaocha.runner"]
|
||||||
:extra-deps
|
:jvm-opts ["-Dlog4j2.configurationFile=log4j2-devenv-repl.xml"]
|
||||||
{io.github.cognitect-labs/test-runner
|
:extra-deps {lambdaisland/kaocha {:mvn/version "1.88.1376"}}}
|
||||||
{:git/tag "v0.5.1" :git/sha "dfb30dd"}}
|
|
||||||
:main-opts ["-m" "cognitect.test-runner"]
|
|
||||||
:exec-fn cognitect.test-runner.api/test}
|
|
||||||
|
|
||||||
:outdated
|
:outdated
|
||||||
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
|
{:extra-deps {com.github.liquidz/antq {:mvn/version "RELEASE"}}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC",
|
"author": "Kaleidos INC",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "yarn@4.0.2",
|
"packageManager": "yarn@4.2.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/penpot/penpot"
|
"url": "https://github.com/penpot/penpot"
|
||||||
|
|
|
@ -113,8 +113,7 @@
|
||||||
(s/def ::worker-default-parallelism ::us/integer)
|
(s/def ::worker-default-parallelism ::us/integer)
|
||||||
(s/def ::worker-webhook-parallelism ::us/integer)
|
(s/def ::worker-webhook-parallelism ::us/integer)
|
||||||
|
|
||||||
(s/def ::authenticated-cookie-domain ::us/string)
|
(s/def ::auth-data-cookie-domain ::us/string)
|
||||||
(s/def ::authenticated-cookie-name ::us/string)
|
|
||||||
(s/def ::auth-token-cookie-name ::us/string)
|
(s/def ::auth-token-cookie-name ::us/string)
|
||||||
(s/def ::auth-token-cookie-max-age ::dt/duration)
|
(s/def ::auth-token-cookie-max-age ::dt/duration)
|
||||||
|
|
||||||
|
@ -222,7 +221,6 @@
|
||||||
::audit-log-http-handler-concurrency
|
::audit-log-http-handler-concurrency
|
||||||
::auth-token-cookie-name
|
::auth-token-cookie-name
|
||||||
::auth-token-cookie-max-age
|
::auth-token-cookie-max-age
|
||||||
::authenticated-cookie-name
|
|
||||||
::authenticated-cookie-domain
|
::authenticated-cookie-domain
|
||||||
::database-password
|
::database-password
|
||||||
::database-uri
|
::database-uri
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
|
[app.common.uri :as u]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.db.sql :as sql]
|
[app.db.sql :as sql]
|
||||||
|
@ -33,7 +34,7 @@
|
||||||
|
|
||||||
;; A cookie that we can use to check from other sites of the same
|
;; A cookie that we can use to check from other sites of the same
|
||||||
;; domain if a user is authenticated.
|
;; domain if a user is authenticated.
|
||||||
(def default-authenticated-cookie-name "authenticated")
|
(def default-auth-data-cookie-name "auth-data")
|
||||||
|
|
||||||
;; Default value for cookie max-age
|
;; Default value for cookie max-age
|
||||||
(def default-cookie-max-age (dt/duration {:days 7}))
|
(def default-cookie-max-age (dt/duration {:days 7}))
|
||||||
|
@ -133,9 +134,9 @@
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(declare ^:private assign-auth-token-cookie)
|
(declare ^:private assign-auth-token-cookie)
|
||||||
(declare ^:private assign-authenticated-cookie)
|
(declare ^:private assign-auth-data-cookie)
|
||||||
(declare ^:private clear-auth-token-cookie)
|
(declare ^:private clear-auth-token-cookie)
|
||||||
(declare ^:private clear-authenticated-cookie)
|
(declare ^:private clear-auth-data-cookie)
|
||||||
(declare ^:private gen-token)
|
(declare ^:private gen-token)
|
||||||
|
|
||||||
(defn create-fn
|
(defn create-fn
|
||||||
|
@ -153,7 +154,7 @@
|
||||||
(l/trace :hint "create" :profile-id (str profile-id))
|
(l/trace :hint "create" :profile-id (str profile-id))
|
||||||
(-> response
|
(-> response
|
||||||
(assign-auth-token-cookie session)
|
(assign-auth-token-cookie session)
|
||||||
(assign-authenticated-cookie session)))))
|
(assign-auth-data-cookie session)))))
|
||||||
|
|
||||||
(defn delete-fn
|
(defn delete-fn
|
||||||
[{:keys [::manager]}]
|
[{:keys [::manager]}]
|
||||||
|
@ -167,7 +168,7 @@
|
||||||
(assoc :status 204)
|
(assoc :status 204)
|
||||||
(assoc :body nil)
|
(assoc :body nil)
|
||||||
(clear-auth-token-cookie)
|
(clear-auth-token-cookie)
|
||||||
(clear-authenticated-cookie)))))
|
(clear-auth-data-cookie)))))
|
||||||
|
|
||||||
(defn- gen-token
|
(defn- gen-token
|
||||||
[props {:keys [profile-id created-at]}]
|
[props {:keys [profile-id created-at]}]
|
||||||
|
@ -229,7 +230,7 @@
|
||||||
(let [session (update! manager session)]
|
(let [session (update! manager session)]
|
||||||
(-> response
|
(-> response
|
||||||
(assign-auth-token-cookie session)
|
(assign-auth-token-cookie session)
|
||||||
(assign-authenticated-cookie session)))
|
(assign-auth-data-cookie session)))
|
||||||
response))))
|
response))))
|
||||||
|
|
||||||
(def soft-auth
|
(def soft-auth
|
||||||
|
@ -262,11 +263,11 @@
|
||||||
:secure secure?}]
|
:secure secure?}]
|
||||||
(update response :cookies assoc name cookie)))
|
(update response :cookies assoc name cookie)))
|
||||||
|
|
||||||
(defn- assign-authenticated-cookie
|
(defn- assign-auth-data-cookie
|
||||||
[response {updated-at :updated-at}]
|
[response {profile-id :profile-id updated-at :updated-at}]
|
||||||
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
|
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
|
||||||
domain (cf/get :authenticated-cookie-domain)
|
domain (cf/get :auth-data-cookie-domain)
|
||||||
cname (cf/get :authenticated-cookie-name "authenticated")
|
cname default-auth-data-cookie-name
|
||||||
|
|
||||||
created-at (or updated-at (dt/now))
|
created-at (or updated-at (dt/now))
|
||||||
renewal (dt/plus created-at default-renewal-max-age)
|
renewal (dt/plus created-at default-renewal-max-age)
|
||||||
|
@ -274,14 +275,17 @@
|
||||||
|
|
||||||
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
|
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
|
||||||
secure? (contains? cf/flags :secure-session-cookies)
|
secure? (contains? cf/flags :secure-session-cookies)
|
||||||
|
strict? (contains? cf/flags :strict-session-cookies)
|
||||||
|
cors? (contains? cf/flags :cors)
|
||||||
|
|
||||||
cookie {:domain domain
|
cookie {:domain domain
|
||||||
:expires expires
|
:expires expires
|
||||||
:path "/"
|
:path "/"
|
||||||
:comment comment
|
:comment comment
|
||||||
:value true
|
:value (u/map->query-string {:profile-id profile-id})
|
||||||
:same-site :strict
|
:same-site (if cors? :none (if strict? :strict :lax))
|
||||||
:secure secure?}]
|
:secure secure?}]
|
||||||
|
|
||||||
(cond-> response
|
(cond-> response
|
||||||
(string? domain)
|
(string? domain)
|
||||||
(update :cookies assoc cname cookie))))
|
(update :cookies assoc cname cookie))))
|
||||||
|
@ -291,10 +295,10 @@
|
||||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
|
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
|
||||||
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
|
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
|
||||||
|
|
||||||
(defn- clear-authenticated-cookie
|
(defn- clear-auth-data-cookie
|
||||||
[response]
|
[response]
|
||||||
(let [cname (cf/get :authenticated-cookie-name default-authenticated-cookie-name)
|
(let [cname default-auth-data-cookie-name
|
||||||
domain (cf/get :authenticated-cookie-domain)]
|
domain (cf/get :auth-data-cookie-domain)]
|
||||||
(cond-> response
|
(cond-> response
|
||||||
(string? domain)
|
(string? domain)
|
||||||
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0}))))
|
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0}))))
|
||||||
|
|
|
@ -53,8 +53,7 @@
|
||||||
(assoc (->> sk str/kebab (keyword "penpot")) v))))]
|
(assoc (->> sk str/kebab (keyword "penpot")) v))))]
|
||||||
(reduce-kv process-param {} params)))
|
(reduce-kv process-param {} params)))
|
||||||
|
|
||||||
(def ^:private
|
(def profile-props
|
||||||
profile-props
|
|
||||||
[:id
|
[:id
|
||||||
:is-active
|
:is-active
|
||||||
:is-muted
|
:is-muted
|
||||||
|
|
|
@ -349,8 +349,8 @@
|
||||||
:audit-log-archive (ig/ref :app.loggers.audit.archive-task/handler)
|
:audit-log-archive (ig/ref :app.loggers.audit.archive-task/handler)
|
||||||
:audit-log-gc (ig/ref :app.loggers.audit.gc-task/handler)
|
:audit-log-gc (ig/ref :app.loggers.audit.gc-task/handler)
|
||||||
|
|
||||||
:object-update
|
:delete-object
|
||||||
(ig/ref :app.tasks.object-update/handler)
|
(ig/ref :app.tasks.delete-object/handler)
|
||||||
:process-webhook-event
|
:process-webhook-event
|
||||||
(ig/ref ::webhooks/process-event-handler)
|
(ig/ref ::webhooks/process-event-handler)
|
||||||
:run-webhook
|
:run-webhook
|
||||||
|
@ -380,7 +380,7 @@
|
||||||
:app.tasks.orphan-teams-gc/handler
|
:app.tasks.orphan-teams-gc/handler
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
:app.tasks.object-update/handler
|
:app.tasks.delete-object/handler
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)}
|
||||||
|
|
||||||
:app.tasks.file-gc/handler
|
:app.tasks.file-gc/handler
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
[app.util.pointer-map :as pmap]
|
[app.util.pointer-map :as pmap]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
@ -822,7 +823,7 @@
|
||||||
|
|
||||||
(feat.fdata/persist-pointers! cfg file-id))))
|
(feat.fdata/persist-pointers! cfg file-id))))
|
||||||
|
|
||||||
(defn- absorb-library!
|
(defn- absorb-library
|
||||||
"Find all files using a shared library, and absorb all library assets
|
"Find all files using a shared library, and absorb all library assets
|
||||||
into the file local libraries"
|
into the file local libraries"
|
||||||
[cfg {:keys [id] :as library}]
|
[cfg {:keys [id] :as library}]
|
||||||
|
@ -840,7 +841,26 @@
|
||||||
:library-id (str id)
|
:library-id (str id)
|
||||||
:files (str/join "," (map str ids)))
|
:files (str/join "," (map str ids)))
|
||||||
|
|
||||||
(run! (partial absorb-library-by-file! cfg ldata) ids)))
|
(run! (partial absorb-library-by-file! cfg ldata) ids)
|
||||||
|
library))
|
||||||
|
|
||||||
|
(defn absorb-library!
|
||||||
|
[{:keys [::db/conn] :as cfg} id]
|
||||||
|
(let [file (-> (get-file cfg id
|
||||||
|
:lock-for-update? true
|
||||||
|
:include-deleted? true)
|
||||||
|
(check-version!))
|
||||||
|
|
||||||
|
proj (db/get* conn :project {:id (:project-id file)}
|
||||||
|
{::db/remove-deleted false})
|
||||||
|
team (-> (db/get* conn :team {:id (:team-id proj)}
|
||||||
|
{::db/remove-deleted false})
|
||||||
|
(teams/decode-row))]
|
||||||
|
|
||||||
|
(-> (cfeat/get-team-enabled-features cf/flags team)
|
||||||
|
(cfeat/check-file-features! (:features file)))
|
||||||
|
|
||||||
|
(absorb-library cfg file)))
|
||||||
|
|
||||||
(defn- set-file-shared
|
(defn- set-file-shared
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
|
||||||
|
@ -853,25 +873,14 @@
|
||||||
;; file, we need to perform more complex operation,
|
;; file, we need to perform more complex operation,
|
||||||
;; so in this case we retrieve the complete file and
|
;; so in this case we retrieve the complete file and
|
||||||
;; perform all required validations.
|
;; perform all required validations.
|
||||||
(let [file (-> (get-file cfg id :lock-for-update? true)
|
(let [file (-> (absorb-library! cfg id)
|
||||||
(check-version!)
|
(assoc :is-shared false))]
|
||||||
(assoc :is-shared false))
|
|
||||||
team (teams/get-team conn
|
|
||||||
:profile-id profile-id
|
|
||||||
:project-id (:project-id file))]
|
|
||||||
|
|
||||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
|
||||||
(cfeat/check-client-features! (:features params))
|
|
||||||
(cfeat/check-file-features! (:features file)))
|
|
||||||
|
|
||||||
(absorb-library! cfg file)
|
|
||||||
|
|
||||||
(db/delete! conn :file-library-rel {:library-file-id id})
|
(db/delete! conn :file-library-rel {:library-file-id id})
|
||||||
(db/update! conn :file
|
(db/update! conn :file
|
||||||
{:is-shared false
|
{:is-shared false
|
||||||
:modified-at (dt/now)}
|
:modified-at (dt/now)}
|
||||||
{:id id})
|
{:id id})
|
||||||
file)
|
(select-keys file [:id :name :is-shared]))
|
||||||
|
|
||||||
(and (false? (:is-shared file))
|
(and (false? (:is-shared file))
|
||||||
(true? (:is-shared params)))
|
(true? (:is-shared params)))
|
||||||
|
@ -911,12 +920,19 @@
|
||||||
|
|
||||||
;; --- MUTATION COMMAND: delete-file
|
;; --- MUTATION COMMAND: delete-file
|
||||||
|
|
||||||
(defn- mark-file-deleted!
|
(defn- mark-file-deleted
|
||||||
[conn file-id]
|
[conn file-id]
|
||||||
(db/update! conn :file
|
(let [file (db/update! conn :file
|
||||||
{:deleted-at (dt/now)}
|
{:deleted-at (dt/now)}
|
||||||
{:id file-id}
|
{:id file-id}
|
||||||
{::db/return-keys [:id :name :is-shared :project-id :created-at :modified-at]}))
|
{::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})
|
||||||
|
file))
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:delete-file
|
schema:delete-file
|
||||||
|
@ -927,29 +943,7 @@
|
||||||
(defn- delete-file
|
(defn- delete-file
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
|
[{:keys [::db/conn] :as cfg} {:keys [profile-id id] :as params}]
|
||||||
(check-edition-permissions! conn profile-id id)
|
(check-edition-permissions! conn profile-id id)
|
||||||
(let [file (mark-file-deleted! conn id)]
|
(let [file (mark-file-deleted conn id)]
|
||||||
|
|
||||||
;; NOTE: when a file is a shared library, then we proceed to load
|
|
||||||
;; the whole file, proceed with feature checking and properly execute
|
|
||||||
;; the absorb-library procedure
|
|
||||||
(when (:is-shared file)
|
|
||||||
(let [file (-> (get-file cfg id
|
|
||||||
:lock-for-update? true
|
|
||||||
:include-deleted? true)
|
|
||||||
(check-version!))
|
|
||||||
|
|
||||||
team (teams/get-team conn
|
|
||||||
:profile-id profile-id
|
|
||||||
:project-id (:project-id file))]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(-> (cfeat/get-team-enabled-features cf/flags team)
|
|
||||||
(cfeat/check-client-features! (:features params))
|
|
||||||
(cfeat/check-file-features! (:features file)))
|
|
||||||
|
|
||||||
(absorb-library! cfg file)))
|
|
||||||
|
|
||||||
(rph/with-meta (rph/wrap)
|
(rph/with-meta (rph/wrap)
|
||||||
{::audit/props {:project-id (:project-id file)
|
{::audit/props {:project-id (:project-id file)
|
||||||
:name (:name file)
|
:name (:name file)
|
||||||
|
|
|
@ -271,7 +271,7 @@
|
||||||
(when (and (some? th1)
|
(when (and (some? th1)
|
||||||
(not= (:media-id th1)
|
(not= (:media-id th1)
|
||||||
(:media-id th2)))
|
(:media-id th2)))
|
||||||
(sto/touch-object! storage (:media-id th1) :async true))
|
(sto/touch-object! storage (:media-id th1)))
|
||||||
|
|
||||||
th2))
|
th2))
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,6 @@
|
||||||
::sm/params schema:update-profile
|
::sm/params schema:update-profile
|
||||||
::sm/result schema:profile}
|
::sm/result schema:profile}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id fullname lang theme] :as params}]
|
||||||
|
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
;; NOTE: we need to retrieve the profile independently if we use
|
;; NOTE: we need to retrieve the profile independently if we use
|
||||||
;; it or not for explicit locking and avoid concurrent updates of
|
;; it or not for explicit locking and avoid concurrent updates of
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.rpc.commands.projects
|
(ns app.rpc.commands.projects
|
||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.db.sql :as-alias sql]
|
[app.db.sql :as-alias sql]
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
[app.rpc.quotes :as quotes]
|
[app.rpc.quotes :as quotes]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]))
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
(s/def ::id ::us/uuid)
|
(s/def ::id ::us/uuid)
|
||||||
|
@ -244,28 +246,39 @@
|
||||||
|
|
||||||
;; --- MUTATION: Delete Project
|
;; --- MUTATION: Delete Project
|
||||||
|
|
||||||
|
(defn- delete-project
|
||||||
|
[conn project-id]
|
||||||
|
(let [project (db/update! conn :project
|
||||||
|
{:deleted-at (dt/now)}
|
||||||
|
{:id project-id}
|
||||||
|
{::db/return-keys true})]
|
||||||
|
|
||||||
|
(when (:is-default project)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :non-deletable-project
|
||||||
|
:hint "impossible to delete default project"))
|
||||||
|
|
||||||
|
(wrk/submit! {::wrk/task :delete-object
|
||||||
|
::wrk/conn conn
|
||||||
|
:object :project
|
||||||
|
:deleted-at (:deleted-at project)
|
||||||
|
:id project-id})
|
||||||
|
|
||||||
|
project))
|
||||||
|
|
||||||
(s/def ::delete-project
|
(s/def ::delete-project
|
||||||
(s/keys :req [::rpc/profile-id]
|
(s/keys :req [::rpc/profile-id]
|
||||||
:req-un [::id]))
|
:req-un [::id]))
|
||||||
|
|
||||||
;; TODO: right now, we just don't allow delete default projects, in a
|
|
||||||
;; future we need to ensure raise a correct exception signaling that
|
|
||||||
;; this is not allowed.
|
|
||||||
|
|
||||||
(sv/defmethod ::delete-project
|
(sv/defmethod ::delete-project
|
||||||
{::doc/added "1.18"
|
{::doc/added "1.18"
|
||||||
::webhooks/event? true}
|
::webhooks/event? true}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(check-edition-permissions! conn profile-id id)
|
(check-edition-permissions! conn profile-id id)
|
||||||
(let [project (db/update! conn :project
|
(let [project (delete-project conn id)]
|
||||||
{:deleted-at (dt/now)}
|
|
||||||
{:id id :is-default false}
|
|
||||||
{::db/return-keys true})]
|
|
||||||
(rph/with-meta (rph/wrap)
|
(rph/with-meta (rph/wrap)
|
||||||
{::audit/props {:team-id (:team-id project)
|
{::audit/props {:team-id (:team-id project)
|
||||||
:name (:name project)
|
:name (:name project)
|
||||||
:created-at (:created-at project)
|
:created-at (:created-at project)
|
||||||
:modified-at (:modified-at project)}}))))
|
:modified-at (:modified-at project)}}))))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
@ -516,14 +517,32 @@
|
||||||
|
|
||||||
;; --- Mutation: Delete Team
|
;; --- Mutation: Delete Team
|
||||||
|
|
||||||
|
(defn- delete-team
|
||||||
|
"Mark a team for deletion"
|
||||||
|
[conn team-id]
|
||||||
|
|
||||||
|
(let [deleted-at (dt/now)
|
||||||
|
team (db/update! conn :team
|
||||||
|
{:deleted-at deleted-at}
|
||||||
|
{:id team-id}
|
||||||
|
{::db/return-keys true})]
|
||||||
|
|
||||||
|
(when (:is-default team)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :non-deletable-team
|
||||||
|
:hint "impossible to delete default team"))
|
||||||
|
|
||||||
|
(wrk/submit! {::wrk/task :delete-object
|
||||||
|
::wrk/conn conn
|
||||||
|
:object :team
|
||||||
|
:deleted-at deleted-at
|
||||||
|
:id team-id})
|
||||||
|
team))
|
||||||
|
|
||||||
(s/def ::delete-team
|
(s/def ::delete-team
|
||||||
(s/keys :req [::rpc/profile-id]
|
(s/keys :req [::rpc/profile-id]
|
||||||
:req-un [::id]))
|
:req-un [::id]))
|
||||||
|
|
||||||
;; TODO: right now just don't allow delete default team, in future it
|
|
||||||
;; should raise a specific exception for signal that this action is
|
|
||||||
;; not allowed.
|
|
||||||
|
|
||||||
(sv/defmethod ::delete-team
|
(sv/defmethod ::delete-team
|
||||||
{::doc/added "1.17"}
|
{::doc/added "1.17"}
|
||||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}]
|
||||||
|
@ -533,12 +552,9 @@
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :only-owner-can-delete-team))
|
:code :only-owner-can-delete-team))
|
||||||
|
|
||||||
(db/update! conn :team
|
(delete-team conn id)
|
||||||
{:deleted-at (dt/now)}
|
|
||||||
{:id id :is-default false})
|
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
|
|
||||||
;; --- Mutation: Team Update Role
|
;; --- Mutation: Team Update Role
|
||||||
|
|
||||||
(s/def ::team-id ::us/uuid)
|
(s/def ::team-id ::us/uuid)
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.rpc.commands.auth :as cmd.auth]
|
[app.rpc.commands.auth :as cmd.auth]
|
||||||
|
[app.rpc.commands.profile :as cmd.profile]
|
||||||
[app.util.json :as json]
|
[app.util.json :as json]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
@ -37,12 +38,13 @@
|
||||||
:or {is-active true}}]
|
:or {is-active true}}]
|
||||||
(when-let [system (get-current-system)]
|
(when-let [system (get-current-system)]
|
||||||
(db/with-atomic [conn (:app.db/pool system)]
|
(db/with-atomic [conn (:app.db/pool system)]
|
||||||
(let [params {:id (uuid/next)
|
(let [password (cmd.profile/derive-password system password)
|
||||||
:email email
|
params {:id (uuid/next)
|
||||||
:fullname fullname
|
:email email
|
||||||
:is-active is-active
|
:fullname fullname
|
||||||
:password password
|
:is-active is-active
|
||||||
:props {}}]
|
:password password
|
||||||
|
:props {}}]
|
||||||
(->> (cmd.auth/create-profile! conn params)
|
(->> (cmd.auth/create-profile! conn params)
|
||||||
(cmd.auth/create-profile-rels! conn))))))
|
(cmd.auth/create-profile-rels! conn))))))
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,8 @@
|
||||||
[app.rpc.commands.files-snapshot :as fsnap]
|
[app.rpc.commands.files-snapshot :as fsnap]
|
||||||
[app.rpc.commands.management :as mgmt]
|
[app.rpc.commands.management :as mgmt]
|
||||||
[app.rpc.commands.profile :as profile]
|
[app.rpc.commands.profile :as profile]
|
||||||
|
[app.rpc.commands.projects :as projects]
|
||||||
|
[app.rpc.commands.teams :as teams]
|
||||||
[app.srepl.fixes :as fixes]
|
[app.srepl.fixes :as fixes]
|
||||||
[app.srepl.helpers :as h]
|
[app.srepl.helpers :as h]
|
||||||
[app.util.blob :as blob]
|
[app.util.blob :as blob]
|
||||||
|
@ -192,7 +194,6 @@
|
||||||
;; NOTIFICATIONS
|
;; NOTIFICATIONS
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
|
||||||
(defn notify!
|
(defn notify!
|
||||||
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
|
[{:keys [::mbus/msgbus ::db/pool]} & {:keys [dest code message level]
|
||||||
:or {code :generic level :info}
|
:or {code :generic level :info}
|
||||||
|
@ -474,6 +475,110 @@
|
||||||
:rollback rollback?
|
:rollback rollback?
|
||||||
:elapsed elapsed))))))
|
:elapsed elapsed))))))
|
||||||
|
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; DELETE/RESTORE OBJECTS (WITH CASCADE, SOFT)
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn- restore-file*
|
||||||
|
[{:keys [::db/conn]} file-id]
|
||||||
|
(db/update! conn :file
|
||||||
|
{:deleted-at nil
|
||||||
|
:has-media-trimmed false}
|
||||||
|
{:id file-id})
|
||||||
|
|
||||||
|
;; Fragments are not handled here because they
|
||||||
|
;; use the database cascade operation and they
|
||||||
|
;; are not marked for deletion with objects-gc
|
||||||
|
;; task
|
||||||
|
|
||||||
|
(db/update! conn :file-media-object
|
||||||
|
{:deleted-at nil}
|
||||||
|
{:file-id file-id})
|
||||||
|
|
||||||
|
;; Mark thumbnails to be deleted
|
||||||
|
(db/update! conn :file-thumbnail
|
||||||
|
{:deleted-at nil}
|
||||||
|
{:file-id file-id})
|
||||||
|
|
||||||
|
(db/update! conn :file-tagged-object-thumbnail
|
||||||
|
{:deleted-at nil}
|
||||||
|
{:file-id file-id})
|
||||||
|
|
||||||
|
:restored)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defn- restore-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]})]
|
||||||
|
(restore-file* cfg id))
|
||||||
|
|
||||||
|
:restored)
|
||||||
|
|
||||||
|
(defn- restore-team*
|
||||||
|
[{:keys [::db/conn] :as cfg} team-id]
|
||||||
|
(db/update! conn :team
|
||||||
|
{:deleted-at nil}
|
||||||
|
{:id team-id})
|
||||||
|
|
||||||
|
(db/update! conn :team-font-variant
|
||||||
|
{:deleted-at nil}
|
||||||
|
{:team-id team-id})
|
||||||
|
|
||||||
|
(doseq [{:keys [id]} (db/query conn :project
|
||||||
|
{:team-id team-id}
|
||||||
|
{::db/columns [:id]})]
|
||||||
|
(restore-project* cfg id))
|
||||||
|
|
||||||
|
:restored)
|
||||||
|
|
||||||
|
(defn restore-deleted-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)))
|
||||||
|
|
||||||
|
(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)))
|
||||||
|
|
||||||
|
(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-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)))))
|
||||||
|
|
||||||
|
(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)))))
|
||||||
|
|
||||||
|
(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)))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; MISC
|
;; MISC
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
[app.storage.impl :as impl]
|
[app.storage.impl :as impl]
|
||||||
[app.storage.s3 :as ss3]
|
[app.storage.s3 :as ss3]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
[app.worker :as wrk]
|
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[datoteka.fs :as fs]
|
[datoteka.fs :as fs]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
|
@ -171,28 +170,16 @@
|
||||||
(impl/put-object object content))
|
(impl/put-object object content))
|
||||||
object)))
|
object)))
|
||||||
|
|
||||||
(def ^:private default-touch-delay
|
|
||||||
"A default delay for the asynchronous touch operation"
|
|
||||||
(dt/duration "5m"))
|
|
||||||
|
|
||||||
(defn touch-object!
|
(defn touch-object!
|
||||||
"Mark object as touched."
|
"Mark object as touched."
|
||||||
[{:keys [::db/pool-or-conn] :as storage} object-or-id & {:keys [async]}]
|
[{:keys [::db/pool-or-conn] :as storage} object-or-id]
|
||||||
(us/assert! ::storage storage)
|
(us/assert! ::storage storage)
|
||||||
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)]
|
(let [id (if (impl/object? object-or-id) (:id object-or-id) object-or-id)]
|
||||||
(if async
|
(-> (db/update! pool-or-conn :storage-object
|
||||||
(wrk/submit! ::wrk/conn pool-or-conn
|
{:touched-at (dt/now)}
|
||||||
::wrk/task :object-update
|
{:id id})
|
||||||
::wrk/delay default-touch-delay
|
(db/get-update-count)
|
||||||
:object :storage-object
|
(pos?))))
|
||||||
:id id
|
|
||||||
:key :touched-at
|
|
||||||
:val (dt/now))
|
|
||||||
(-> (db/update! pool-or-conn :storage-object
|
|
||||||
{:touched-at (dt/now)}
|
|
||||||
{:id id})
|
|
||||||
(db/get-update-count)
|
|
||||||
(pos?)))))
|
|
||||||
|
|
||||||
(defn get-object-data
|
(defn get-object-data
|
||||||
"Return an input stream instance of the object content."
|
"Return an input stream instance of the object content."
|
||||||
|
|
84
backend/src/app/tasks/delete_object.clj
Normal file
84
backend/src/app/tasks/delete_object.clj
Normal file
|
@ -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 app.tasks.delete-object
|
||||||
|
"A generic task for object deletion cascade handling"
|
||||||
|
(:require
|
||||||
|
[app.common.logging :as l]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.rpc.commands.files :as files]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
|
(def ^:dynamic *team-deletion* false)
|
||||||
|
|
||||||
|
(defmulti delete-object
|
||||||
|
(fn [_ props] (:object props)))
|
||||||
|
|
||||||
|
(defmethod delete-object :file
|
||||||
|
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||||
|
(l/trc :hint "marking for deletion" :rel "file" :id (str id))
|
||||||
|
(when-let [file (db/get* conn :file {:id id} {::db/remove-deleted false})]
|
||||||
|
(when (and (:is-shared file)
|
||||||
|
(not *team-deletion*))
|
||||||
|
;; NOTE: we don't prevent file deletion on absorb operation failure
|
||||||
|
(try
|
||||||
|
(db/tx-run! cfg files/absorb-library! id)
|
||||||
|
(catch Throwable cause
|
||||||
|
(l/warn :hint "error on absorbing library"
|
||||||
|
:file-id id
|
||||||
|
:cause cause))))
|
||||||
|
|
||||||
|
;; Mark file media objects to be deleted
|
||||||
|
(db/update! conn :file-media-object
|
||||||
|
{:deleted-at deleted-at}
|
||||||
|
{:file-id id})
|
||||||
|
|
||||||
|
;; Mark thumbnails to be deleted
|
||||||
|
(db/update! conn :file-thumbnail
|
||||||
|
{:deleted-at deleted-at}
|
||||||
|
{:file-id id})
|
||||||
|
|
||||||
|
(db/update! conn :file-tagged-object-thumbnail
|
||||||
|
{:deleted-at deleted-at}
|
||||||
|
{:file-id id})))
|
||||||
|
|
||||||
|
(defmethod delete-object :project
|
||||||
|
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||||
|
(l/trc :hint "marking for deletion" :rel "project" :id (str id))
|
||||||
|
(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))))
|
||||||
|
|
||||||
|
(defmethod delete-object :team
|
||||||
|
[{:keys [::db/conn] :as cfg} {:keys [id deleted-at]}]
|
||||||
|
(l/trc :hint "marking for deletion" :rel "team" :id (str id))
|
||||||
|
(db/update! conn :team-font-variant
|
||||||
|
{:deleted-at deleted-at}
|
||||||
|
{:team-id id})
|
||||||
|
|
||||||
|
(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)))))
|
||||||
|
|
||||||
|
(defmethod delete-object :default
|
||||||
|
[_cfg props]
|
||||||
|
(l/wrn :hint "not implementation found" :rel (:object props)))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
|
(s/keys :req [::db/pool]))
|
||||||
|
|
||||||
|
(defmethod ig/init-key ::handler
|
||||||
|
[_ cfg]
|
||||||
|
(fn [{:keys [props] :as params}]
|
||||||
|
(db/tx-run! cfg delete-object props)))
|
|
@ -1,32 +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.object-update
|
|
||||||
"A task used for perform simple object properties update
|
|
||||||
in an asynchronous flow."
|
|
||||||
(:require
|
|
||||||
[app.common.data :as d]
|
|
||||||
[app.common.logging :as l]
|
|
||||||
[app.db :as db]
|
|
||||||
[clojure.spec.alpha :as s]
|
|
||||||
[integrant.core :as ig]))
|
|
||||||
|
|
||||||
(defn- update-object
|
|
||||||
[{:keys [::db/conn] :as cfg} {:keys [id object key val] :as props}]
|
|
||||||
(l/trc :hint "update object prop"
|
|
||||||
:id (str id)
|
|
||||||
:object (d/name object)
|
|
||||||
:key (d/name key)
|
|
||||||
:val val)
|
|
||||||
(db/update! conn object {key val} {:id id} {::db/return-keys false}))
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handler [_]
|
|
||||||
(s/keys :req [::db/pool]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
|
||||||
[_ cfg]
|
|
||||||
(fn [{:keys [props] :as params}]
|
|
||||||
(db/tx-run! cfg update-object props)))
|
|
|
@ -17,67 +17,18 @@
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(declare ^:private delete-file-data-fragments!)
|
|
||||||
(declare ^:private delete-file-media-objects!)
|
|
||||||
(declare ^:private delete-file-object-thumbnails!)
|
|
||||||
(declare ^:private delete-file-thumbnails!)
|
|
||||||
(declare ^:private delete-files!)
|
|
||||||
(declare ^:private delete-fonts!)
|
|
||||||
(declare ^:private delete-profiles!)
|
|
||||||
(declare ^:private delete-projects!)
|
|
||||||
(declare ^:private delete-teams!)
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handler [_]
|
|
||||||
(s/keys :req [::db/pool ::sto/storage]))
|
|
||||||
|
|
||||||
(defmethod ig/prep-key ::handler
|
|
||||||
[_ cfg]
|
|
||||||
(assoc cfg ::min-age cf/deletion-delay))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
|
||||||
[_ cfg]
|
|
||||||
(fn [params]
|
|
||||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
|
||||||
;; Disable deletion protection for the current transaction
|
|
||||||
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
|
|
||||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
|
||||||
|
|
||||||
(let [min-age (dt/duration (or (:min-age params) (::min-age cfg)))
|
|
||||||
cfg (-> cfg
|
|
||||||
(assoc ::min-age (db/interval min-age))
|
|
||||||
(update ::sto/storage media/configure-assets-storage conn))
|
|
||||||
|
|
||||||
total (reduce + 0
|
|
||||||
[(delete-profiles! cfg)
|
|
||||||
(delete-teams! cfg)
|
|
||||||
(delete-fonts! cfg)
|
|
||||||
(delete-projects! cfg)
|
|
||||||
(delete-files! cfg)
|
|
||||||
(delete-file-thumbnails! cfg)
|
|
||||||
(delete-file-object-thumbnails! cfg)
|
|
||||||
(delete-file-data-fragments! cfg)
|
|
||||||
(delete-file-media-objects! cfg)])]
|
|
||||||
|
|
||||||
(l/info :hint "task finished"
|
|
||||||
:deleted total
|
|
||||||
:rollback? (boolean (:rollback? params)))
|
|
||||||
|
|
||||||
(when (:rollback? params)
|
|
||||||
(db/rollback! conn))
|
|
||||||
|
|
||||||
{:processed total})))))
|
|
||||||
|
|
||||||
(def ^:private sql:get-profiles
|
(def ^:private sql:get-profiles
|
||||||
"SELECT id, photo_id FROM profile
|
"SELECT id, photo_id FROM profile
|
||||||
WHERE deleted_at IS NOT NULL
|
WHERE deleted_at IS NOT NULL
|
||||||
AND deleted_at < now() - ?::interval
|
AND deleted_at < now() - ?::interval
|
||||||
ORDER BY deleted_at ASC
|
ORDER BY deleted_at ASC
|
||||||
|
LIMIT ?
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
SKIP LOCKED")
|
SKIP LOCKED")
|
||||||
|
|
||||||
(defn- delete-profiles!
|
(defn- delete-profiles!
|
||||||
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
|
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||||
(->> (db/cursor conn [sql:get-profiles min-age])
|
(->> (db/cursor conn [sql:get-profiles min-age chunk-size] {:chunk-size 1})
|
||||||
(reduce (fn [total {:keys [id photo-id]}]
|
(reduce (fn [total {:keys [id photo-id]}]
|
||||||
(l/trc :hint "permanently delete" :rel "profile" :id (str id))
|
(l/trc :hint "permanently delete" :rel "profile" :id (str id))
|
||||||
|
|
||||||
|
@ -99,13 +50,13 @@
|
||||||
WHERE deleted_at IS NOT NULL
|
WHERE deleted_at IS NOT NULL
|
||||||
AND deleted_at < now() - ?::interval
|
AND deleted_at < now() - ?::interval
|
||||||
ORDER BY deleted_at ASC
|
ORDER BY deleted_at ASC
|
||||||
|
LIMIT ?
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
SKIP LOCKED")
|
SKIP LOCKED")
|
||||||
|
|
||||||
(defn- delete-teams!
|
(defn- delete-teams!
|
||||||
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
|
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||||
|
(->> (db/cursor conn [sql:get-teams min-age chunk-size] {:chunk-size 1})
|
||||||
(->> (db/cursor conn [sql:get-teams min-age])
|
|
||||||
(reduce (fn [total {:keys [id photo-id deleted-at]}]
|
(reduce (fn [total {:keys [id photo-id deleted-at]}]
|
||||||
(l/trc :hint "permanently delete"
|
(l/trc :hint "permanently delete"
|
||||||
:rel "team"
|
:rel "team"
|
||||||
|
@ -118,15 +69,6 @@
|
||||||
;; And finally, permanently delete the team.
|
;; And finally, permanently delete the team.
|
||||||
(db/delete! conn :team {:id id})
|
(db/delete! conn :team {:id id})
|
||||||
|
|
||||||
;; Mark for deletion in cascade
|
|
||||||
(db/update! conn :team-font-variant
|
|
||||||
{:deleted-at deleted-at}
|
|
||||||
{:team-id id})
|
|
||||||
|
|
||||||
(db/update! conn :project
|
|
||||||
{:deleted-at deleted-at}
|
|
||||||
{:team-id id})
|
|
||||||
|
|
||||||
(inc total))
|
(inc total))
|
||||||
0)))
|
0)))
|
||||||
|
|
||||||
|
@ -136,12 +78,13 @@
|
||||||
WHERE deleted_at IS NOT NULL
|
WHERE deleted_at IS NOT NULL
|
||||||
AND deleted_at < now() - ?::interval
|
AND deleted_at < now() - ?::interval
|
||||||
ORDER BY deleted_at ASC
|
ORDER BY deleted_at ASC
|
||||||
|
LIMIT ?
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
SKIP LOCKED")
|
SKIP LOCKED")
|
||||||
|
|
||||||
(defn- delete-fonts!
|
(defn- delete-fonts!
|
||||||
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
|
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||||
(->> (db/cursor conn [sql:get-fonts min-age])
|
(->> (db/cursor conn [sql:get-fonts min-age chunk-size] {:chunk-size 1})
|
||||||
(reduce (fn [total {:keys [id team-id deleted-at] :as font}]
|
(reduce (fn [total {:keys [id team-id deleted-at] :as font}]
|
||||||
(l/trc :hint "permanently delete"
|
(l/trc :hint "permanently delete"
|
||||||
:rel "team-font-variant"
|
:rel "team-font-variant"
|
||||||
|
@ -167,12 +110,13 @@
|
||||||
WHERE deleted_at IS NOT NULL
|
WHERE deleted_at IS NOT NULL
|
||||||
AND deleted_at < now() - ?::interval
|
AND deleted_at < now() - ?::interval
|
||||||
ORDER BY deleted_at ASC
|
ORDER BY deleted_at ASC
|
||||||
|
LIMIT ?
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
SKIP LOCKED")
|
SKIP LOCKED")
|
||||||
|
|
||||||
(defn- delete-projects!
|
(defn- delete-projects!
|
||||||
[{:keys [::db/conn ::min-age] :as cfg}]
|
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}]
|
||||||
(->> (db/cursor conn [sql:get-projects min-age])
|
(->> (db/cursor conn [sql:get-projects min-age chunk-size] {:chunk-size 1})
|
||||||
(reduce (fn [total {:keys [id team-id deleted-at]}]
|
(reduce (fn [total {:keys [id team-id deleted-at]}]
|
||||||
(l/trc :hint "permanently delete"
|
(l/trc :hint "permanently delete"
|
||||||
:rel "project"
|
:rel "project"
|
||||||
|
@ -183,11 +127,6 @@
|
||||||
;; And finally, permanently delete the project.
|
;; And finally, permanently delete the project.
|
||||||
(db/delete! conn :project {:id id})
|
(db/delete! conn :project {:id id})
|
||||||
|
|
||||||
;; Mark files to be deleted
|
|
||||||
(db/update! conn :file
|
|
||||||
{:deleted-at deleted-at}
|
|
||||||
{:project-id id})
|
|
||||||
|
|
||||||
(inc total))
|
(inc total))
|
||||||
0)))
|
0)))
|
||||||
|
|
||||||
|
@ -197,12 +136,13 @@
|
||||||
WHERE deleted_at IS NOT NULL
|
WHERE deleted_at IS NOT NULL
|
||||||
AND deleted_at < now() - ?::interval
|
AND deleted_at < now() - ?::interval
|
||||||
ORDER BY deleted_at ASC
|
ORDER BY deleted_at ASC
|
||||||
|
LIMIT ?
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
SKIP LOCKED")
|
SKIP LOCKED")
|
||||||
|
|
||||||
(defn- delete-files!
|
(defn- delete-files!
|
||||||
[{:keys [::db/conn ::min-age] :as cfg}]
|
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}]
|
||||||
(->> (db/cursor conn [sql:get-files min-age])
|
(->> (db/cursor conn [sql:get-files min-age chunk-size] {:chunk-size 1})
|
||||||
(reduce (fn [total {:keys [id deleted-at project-id]}]
|
(reduce (fn [total {:keys [id deleted-at project-id]}]
|
||||||
(l/trc :hint "permanently delete"
|
(l/trc :hint "permanently delete"
|
||||||
:rel "file"
|
:rel "file"
|
||||||
|
@ -210,26 +150,9 @@
|
||||||
:project-id (str project-id)
|
:project-id (str project-id)
|
||||||
:deleted-at (dt/format-instant deleted-at))
|
:deleted-at (dt/format-instant deleted-at))
|
||||||
|
|
||||||
;; NOTE: fragments not handled here because they have
|
|
||||||
;; cascade.
|
|
||||||
|
|
||||||
;; And finally, permanently delete the file.
|
;; And finally, permanently delete the file.
|
||||||
(db/delete! conn :file {:id id})
|
(db/delete! conn :file {:id id})
|
||||||
|
|
||||||
;; Mark file media objects to be deleted
|
|
||||||
(db/update! conn :file-media-object
|
|
||||||
{:deleted-at deleted-at}
|
|
||||||
{:file-id id})
|
|
||||||
|
|
||||||
;; Mark thumbnails to be deleted
|
|
||||||
(db/update! conn :file-thumbnail
|
|
||||||
{:deleted-at deleted-at}
|
|
||||||
{:file-id id})
|
|
||||||
|
|
||||||
(db/update! conn :file-tagged-object-thumbnail
|
|
||||||
{:deleted-at deleted-at}
|
|
||||||
{:file-id id})
|
|
||||||
|
|
||||||
(inc total))
|
(inc total))
|
||||||
0)))
|
0)))
|
||||||
|
|
||||||
|
@ -239,12 +162,13 @@
|
||||||
WHERE deleted_at IS NOT NULL
|
WHERE deleted_at IS NOT NULL
|
||||||
AND deleted_at < now() - ?::interval
|
AND deleted_at < now() - ?::interval
|
||||||
ORDER BY deleted_at ASC
|
ORDER BY deleted_at ASC
|
||||||
|
LIMIT ?
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
SKIP LOCKED")
|
SKIP LOCKED")
|
||||||
|
|
||||||
(defn delete-file-thumbnails!
|
(defn delete-file-thumbnails!
|
||||||
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
|
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||||
(->> (db/cursor conn [sql:get-file-thumbnails min-age])
|
(->> (db/cursor conn [sql:get-file-thumbnails min-age chunk-size] {:chunk-size 1})
|
||||||
(reduce (fn [total {:keys [file-id revn media-id deleted-at]}]
|
(reduce (fn [total {:keys [file-id revn media-id deleted-at]}]
|
||||||
(l/trc :hint "permanently delete"
|
(l/trc :hint "permanently delete"
|
||||||
:rel "file-thumbnail"
|
:rel "file-thumbnail"
|
||||||
|
@ -267,12 +191,13 @@
|
||||||
WHERE deleted_at IS NOT NULL
|
WHERE deleted_at IS NOT NULL
|
||||||
AND deleted_at < now() - ?::interval
|
AND deleted_at < now() - ?::interval
|
||||||
ORDER BY deleted_at ASC
|
ORDER BY deleted_at ASC
|
||||||
|
LIMIT ?
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
SKIP LOCKED")
|
SKIP LOCKED")
|
||||||
|
|
||||||
(defn delete-file-object-thumbnails!
|
(defn delete-file-object-thumbnails!
|
||||||
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
|
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||||
(->> (db/cursor conn [sql:get-file-object-thumbnails min-age])
|
(->> (db/cursor conn [sql:get-file-object-thumbnails min-age chunk-size] {:chunk-size 1})
|
||||||
(reduce (fn [total {:keys [file-id object-id media-id deleted-at]}]
|
(reduce (fn [total {:keys [file-id object-id media-id deleted-at]}]
|
||||||
(l/trc :hint "permanently delete"
|
(l/trc :hint "permanently delete"
|
||||||
:rel "file-tagged-object-thumbnail"
|
:rel "file-tagged-object-thumbnail"
|
||||||
|
@ -295,12 +220,13 @@
|
||||||
WHERE deleted_at IS NOT NULL
|
WHERE deleted_at IS NOT NULL
|
||||||
AND deleted_at < now() - ?::interval
|
AND deleted_at < now() - ?::interval
|
||||||
ORDER BY deleted_at ASC
|
ORDER BY deleted_at ASC
|
||||||
|
LIMIT ?
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
SKIP LOCKED")
|
SKIP LOCKED")
|
||||||
|
|
||||||
(defn- delete-file-data-fragments!
|
(defn- delete-file-data-fragments!
|
||||||
[{:keys [::db/conn ::min-age] :as cfg}]
|
[{:keys [::db/conn ::min-age ::chunk-size] :as cfg}]
|
||||||
(->> (db/cursor conn [sql:get-file-data-fragments min-age])
|
(->> (db/cursor conn [sql:get-file-data-fragments min-age chunk-size] {:chunk-size 1})
|
||||||
(reduce (fn [total {:keys [file-id id deleted-at]}]
|
(reduce (fn [total {:keys [file-id id deleted-at]}]
|
||||||
(l/trc :hint "permanently delete"
|
(l/trc :hint "permanently delete"
|
||||||
:rel "file-data-fragment"
|
:rel "file-data-fragment"
|
||||||
|
@ -319,12 +245,13 @@
|
||||||
WHERE deleted_at IS NOT NULL
|
WHERE deleted_at IS NOT NULL
|
||||||
AND deleted_at < now() - ?::interval
|
AND deleted_at < now() - ?::interval
|
||||||
ORDER BY deleted_at ASC
|
ORDER BY deleted_at ASC
|
||||||
|
LIMIT ?
|
||||||
FOR UPDATE
|
FOR UPDATE
|
||||||
SKIP LOCKED")
|
SKIP LOCKED")
|
||||||
|
|
||||||
(defn- delete-file-media-objects!
|
(defn- delete-file-media-objects!
|
||||||
[{:keys [::db/conn ::min-age ::sto/storage] :as cfg}]
|
[{:keys [::db/conn ::min-age ::chunk-size ::sto/storage] :as cfg}]
|
||||||
(->> (db/cursor conn [sql:get-file-media-objects min-age])
|
(->> (db/cursor conn [sql:get-file-media-objects min-age chunk-size] {:chunk-size 1})
|
||||||
(reduce (fn [total {:keys [id file-id deleted-at] :as fmo}]
|
(reduce (fn [total {:keys [id file-id deleted-at] :as fmo}]
|
||||||
(l/trc :hint "permanently delete"
|
(l/trc :hint "permanently delete"
|
||||||
:rel "file-media-object"
|
:rel "file-media-object"
|
||||||
|
@ -340,3 +267,53 @@
|
||||||
|
|
||||||
(inc total))
|
(inc total))
|
||||||
0)))
|
0)))
|
||||||
|
|
||||||
|
(def ^:private deletion-proc-vars
|
||||||
|
[#'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!])
|
||||||
|
|
||||||
|
(defn- execute-proc!
|
||||||
|
"A generic function that executes the specified proc iterativelly
|
||||||
|
until 0 results is returned"
|
||||||
|
[cfg proc-fn]
|
||||||
|
(loop [total 0]
|
||||||
|
(let [result (db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
||||||
|
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
|
||||||
|
(proc-fn cfg)))]
|
||||||
|
(if (pos? result)
|
||||||
|
(recur (+ total result))
|
||||||
|
total))))
|
||||||
|
|
||||||
|
(defmethod ig/pre-init-spec ::handler [_]
|
||||||
|
(s/keys :req [::db/pool ::sto/storage]))
|
||||||
|
|
||||||
|
(defmethod ig/prep-key ::handler
|
||||||
|
[_ cfg]
|
||||||
|
(assoc cfg
|
||||||
|
::min-age cf/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)))
|
||||||
|
cfg (-> cfg
|
||||||
|
(assoc ::min-age (db/interval min-age))
|
||||||
|
(update ::sto/storage media/configure-assets-storage))]
|
||||||
|
|
||||||
|
(loop [procs (map deref deletion-proc-vars)
|
||||||
|
total 0]
|
||||||
|
(if-let [proc-fn (first procs)]
|
||||||
|
(let [result (execute-proc! cfg proc-fn)]
|
||||||
|
(recur (rest procs)
|
||||||
|
(+ total result)))
|
||||||
|
(do
|
||||||
|
(l/inf :hint "task finished" :deleted total)
|
||||||
|
{:processed total}))))))
|
||||||
|
|
|
@ -10,29 +10,10 @@
|
||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as wrk]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(declare ^:private delete-orphan-teams!)
|
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::handler [_]
|
|
||||||
(s/keys :req [::db/pool]))
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::handler
|
|
||||||
[_ cfg]
|
|
||||||
(fn [params]
|
|
||||||
(db/tx-run! cfg (fn [{:keys [::db/conn] :as cfg}]
|
|
||||||
(l/inf :hint "gc started" :rollback? (boolean (:rollback? params)))
|
|
||||||
(let [total (delete-orphan-teams! cfg)]
|
|
||||||
(l/inf :hint "task finished"
|
|
||||||
:teams total
|
|
||||||
:rollback? (boolean (:rollback? params)))
|
|
||||||
|
|
||||||
(when (:rollback? params)
|
|
||||||
(db/rollback! conn))
|
|
||||||
|
|
||||||
{:processed total})))))
|
|
||||||
|
|
||||||
(def ^:private sql:get-orphan-teams
|
(def ^:private sql:get-orphan-teams
|
||||||
"SELECT t.id
|
"SELECT t.id
|
||||||
FROM team AS t
|
FROM team AS t
|
||||||
|
@ -44,16 +25,43 @@
|
||||||
FOR UPDATE OF t
|
FOR UPDATE OF t
|
||||||
SKIP LOCKED")
|
SKIP LOCKED")
|
||||||
|
|
||||||
(defn- delete-orphan-teams!
|
(defn- delete-orphan-teams
|
||||||
"Find all orphan teams (with no members) and mark them for
|
"Find all orphan teams (with no members) and mark them for
|
||||||
deletion (soft delete)."
|
deletion (soft delete)."
|
||||||
[{:keys [::db/conn] :as cfg}]
|
[{:keys [::db/conn] :as cfg}]
|
||||||
(->> (db/cursor conn sql:get-orphan-teams)
|
(let [deleted-at (dt/now)]
|
||||||
(map :id)
|
(->> (db/cursor conn sql:get-orphan-teams)
|
||||||
(reduce (fn [total team-id]
|
(map :id)
|
||||||
(l/trc :hint "mark orphan team for deletion" :id (str team-id))
|
(reduce (fn [total team-id]
|
||||||
(db/update! conn :team
|
(l/trc :hint "mark orphan team for deletion" :id (str team-id))
|
||||||
{:deleted-at (dt/now)}
|
|
||||||
{:id team-id})
|
(db/update! conn :team
|
||||||
(inc total))
|
{:deleted-at deleted-at}
|
||||||
0)))
|
{: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})))))
|
||||||
|
|
|
@ -35,8 +35,92 @@
|
||||||
[_ item]
|
[_ item]
|
||||||
{:params item})
|
{:params item})
|
||||||
|
|
||||||
|
(defn- get-task
|
||||||
|
[{:keys [::db/pool]} task-id]
|
||||||
|
(ex/try!
|
||||||
|
(some-> (db/get* pool :task {:id task-id})
|
||||||
|
(decode-task-row))))
|
||||||
|
|
||||||
|
(defn- run-task
|
||||||
|
[{:keys [::wrk/registry ::id ::queue] :as cfg} task]
|
||||||
|
(try
|
||||||
|
(l/dbg :hint "start"
|
||||||
|
:name (:name task)
|
||||||
|
:task-id (str (:id task))
|
||||||
|
:queue queue
|
||||||
|
:runner-id id
|
||||||
|
:retry (:retry-num task))
|
||||||
|
(let [tpoint (dt/tpoint)
|
||||||
|
task-fn (get registry (:name task))
|
||||||
|
result (if task-fn
|
||||||
|
(task-fn task)
|
||||||
|
{:status :completed :task task})
|
||||||
|
elapsed (dt/format-duration (tpoint))]
|
||||||
|
|
||||||
|
(when-not task-fn
|
||||||
|
(l/wrn :hint "no task handler found" :name (:name task)))
|
||||||
|
|
||||||
|
(l/dbg :hint "end"
|
||||||
|
:name (:name task)
|
||||||
|
:task-id (str (:id task))
|
||||||
|
:queue queue
|
||||||
|
:runner-id id
|
||||||
|
:retry (:retry-num task)
|
||||||
|
:elapsed elapsed)
|
||||||
|
|
||||||
|
result)
|
||||||
|
|
||||||
|
(catch InterruptedException cause
|
||||||
|
(throw cause))
|
||||||
|
(catch Throwable cause
|
||||||
|
(let [edata (ex-data cause)]
|
||||||
|
(if (and (< (:retry-num task)
|
||||||
|
(:max-retries task))
|
||||||
|
(= ::retry (:type edata)))
|
||||||
|
(cond-> {:status :retry :task task :error cause}
|
||||||
|
(dt/duration? (:delay edata))
|
||||||
|
(assoc :delay (:delay edata))
|
||||||
|
|
||||||
|
(= ::noop (:strategy edata))
|
||||||
|
(assoc :inc-by 0))
|
||||||
|
(do
|
||||||
|
(l/err :hint "unhandled exception on task"
|
||||||
|
::l/context (get-error-context cause task)
|
||||||
|
:cause cause)
|
||||||
|
(if (>= (:retry-num task) (:max-retries task))
|
||||||
|
{:status :failed :task task :error cause}
|
||||||
|
{:status :retry :task task :error cause})))))))
|
||||||
|
|
||||||
|
(defn- run-task!
|
||||||
|
[{:keys [::rds/rconn ::id] :as cfg} task-id]
|
||||||
|
(loop [task (get-task cfg task-id)]
|
||||||
|
(cond
|
||||||
|
(ex/exception? task)
|
||||||
|
(if (or (db/connection-error? task)
|
||||||
|
(db/serialization-error? task))
|
||||||
|
(do
|
||||||
|
(l/wrn :hint "connection error on retrieving task from database (retrying in some instants)"
|
||||||
|
:id id
|
||||||
|
:cause task)
|
||||||
|
(px/sleep (::rds/timeout rconn))
|
||||||
|
(recur (get-task cfg task-id)))
|
||||||
|
(do
|
||||||
|
(l/err :hint "unhandled exception on retrieving task from database (retrying in some instants)"
|
||||||
|
:id id
|
||||||
|
:cause task)
|
||||||
|
(px/sleep (::rds/timeout rconn))
|
||||||
|
(recur (get-task cfg task-id))))
|
||||||
|
|
||||||
|
(nil? task)
|
||||||
|
(l/wrn :hint "no task found on the database"
|
||||||
|
:id id
|
||||||
|
:task-id task-id)
|
||||||
|
|
||||||
|
:else
|
||||||
|
(run-task cfg task))))
|
||||||
|
|
||||||
(defn- run-worker-loop!
|
(defn- run-worker-loop!
|
||||||
[{:keys [::db/pool ::rds/rconn ::wrk/registry ::timeout ::queue ::id]}]
|
[{:keys [::db/pool ::rds/rconn ::timeout ::queue] :as cfg}]
|
||||||
(letfn [(handle-task-retry [{:keys [task error inc-by delay] :or {inc-by 1 delay 1000}}]
|
(letfn [(handle-task-retry [{:keys [task error inc-by delay] :or {inc-by 1 delay 1000}}]
|
||||||
(let [explain (ex-message error)
|
(let [explain (ex-message error)
|
||||||
nretry (+ (:retry-num task) inc-by)
|
nretry (+ (:retry-num task) inc-by)
|
||||||
|
@ -82,88 +166,6 @@
|
||||||
:length (alength payload)
|
:length (alength payload)
|
||||||
:cause cause))))
|
:cause cause))))
|
||||||
|
|
||||||
(handle-task [{:keys [name] :as task}]
|
|
||||||
(let [task-fn (get registry name)]
|
|
||||||
(if task-fn
|
|
||||||
(task-fn task)
|
|
||||||
(l/wrn :hint "no task handler found" :name name))
|
|
||||||
{:status :completed :task task}))
|
|
||||||
|
|
||||||
(handle-task-exception [cause task]
|
|
||||||
(let [edata (ex-data cause)]
|
|
||||||
(if (and (< (:retry-num task)
|
|
||||||
(:max-retries task))
|
|
||||||
(= ::retry (:type edata)))
|
|
||||||
(cond-> {:status :retry :task task :error cause}
|
|
||||||
(dt/duration? (:delay edata))
|
|
||||||
(assoc :delay (:delay edata))
|
|
||||||
|
|
||||||
(= ::noop (:strategy edata))
|
|
||||||
(assoc :inc-by 0))
|
|
||||||
(do
|
|
||||||
(l/err :hint "unhandled exception on task"
|
|
||||||
::l/context (get-error-context cause task)
|
|
||||||
:cause cause)
|
|
||||||
(if (>= (:retry-num task) (:max-retries task))
|
|
||||||
{:status :failed :task task :error cause}
|
|
||||||
{:status :retry :task task :error cause})))))
|
|
||||||
|
|
||||||
(get-task [task-id]
|
|
||||||
(ex/try!
|
|
||||||
(some-> (db/get* pool :task {:id task-id})
|
|
||||||
(decode-task-row))))
|
|
||||||
|
|
||||||
(run-task [task-id]
|
|
||||||
(loop [task (get-task task-id)]
|
|
||||||
(cond
|
|
||||||
(ex/exception? task)
|
|
||||||
(if (or (db/connection-error? task)
|
|
||||||
(db/serialization-error? task))
|
|
||||||
(do
|
|
||||||
(l/wrn :hint "connection error on retrieving task from database (retrying in some instants)"
|
|
||||||
:id id
|
|
||||||
:cause task)
|
|
||||||
(px/sleep (::rds/timeout rconn))
|
|
||||||
(recur (get-task task-id)))
|
|
||||||
(do
|
|
||||||
(l/err :hint "unhandled exception on retrieving task from database (retrying in some instants)"
|
|
||||||
:id id
|
|
||||||
:cause task)
|
|
||||||
(px/sleep (::rds/timeout rconn))
|
|
||||||
(recur (get-task task-id))))
|
|
||||||
|
|
||||||
(nil? task)
|
|
||||||
(l/wrn :hint "no task found on the database"
|
|
||||||
:id id
|
|
||||||
:task-id task-id)
|
|
||||||
|
|
||||||
:else
|
|
||||||
(try
|
|
||||||
(l/dbg :hint "start"
|
|
||||||
:name (:name task)
|
|
||||||
:task-id (str task-id)
|
|
||||||
:queue queue
|
|
||||||
:runner-id id
|
|
||||||
:retry (:retry-num task))
|
|
||||||
(let [tpoint (dt/tpoint)
|
|
||||||
result (handle-task task)
|
|
||||||
elapsed (dt/format-duration (tpoint))]
|
|
||||||
|
|
||||||
(l/dbg :hint "end"
|
|
||||||
:name (:name task)
|
|
||||||
:task-id (str task-id)
|
|
||||||
:queue queue
|
|
||||||
:runner-id id
|
|
||||||
:retry (:retry-num task)
|
|
||||||
:elapsed elapsed)
|
|
||||||
|
|
||||||
result)
|
|
||||||
|
|
||||||
(catch InterruptedException cause
|
|
||||||
(throw cause))
|
|
||||||
(catch Throwable cause
|
|
||||||
(handle-task-exception cause task))))))
|
|
||||||
|
|
||||||
(process-result [{:keys [status] :as result}]
|
(process-result [{:keys [status] :as result}]
|
||||||
(ex/try!
|
(ex/try!
|
||||||
(case status
|
(case status
|
||||||
|
@ -173,7 +175,7 @@
|
||||||
nil)))
|
nil)))
|
||||||
|
|
||||||
(run-task-loop [task-id]
|
(run-task-loop [task-id]
|
||||||
(loop [result (run-task task-id)]
|
(loop [result (run-task! cfg task-id)]
|
||||||
(when-let [cause (process-result result)]
|
(when-let [cause (process-result result)]
|
||||||
(if (or (db/connection-error? cause)
|
(if (or (db/connection-error? cause)
|
||||||
(db/serialization-error? cause))
|
(db/serialization-error? cause))
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
[app.util.blob :as blob]
|
[app.util.blob :as blob]
|
||||||
[app.util.services :as sv]
|
[app.util.services :as sv]
|
||||||
[app.util.time :as dt]
|
[app.util.time :as dt]
|
||||||
|
[app.worker.runner]
|
||||||
[clojure.java.io :as io]
|
[clojure.java.io :as io]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[clojure.test :as t]
|
[clojure.test :as t]
|
||||||
|
@ -77,47 +78,6 @@
|
||||||
:enable-feature-components-v2
|
:enable-feature-components-v2
|
||||||
:disable-file-validation])
|
:disable-file-validation])
|
||||||
|
|
||||||
(def test-init-sql
|
|
||||||
["alter table project_profile_rel set unlogged;\n"
|
|
||||||
"alter table file_profile_rel set unlogged;\n"
|
|
||||||
"alter table presence set unlogged;\n"
|
|
||||||
"alter table presence set unlogged;\n"
|
|
||||||
"alter table http_session set unlogged;\n"
|
|
||||||
"alter table team_profile_rel set unlogged;\n"
|
|
||||||
"alter table team_project_profile_rel set unlogged;\n"
|
|
||||||
"alter table comment_thread_status set unlogged;\n"
|
|
||||||
"alter table comment set unlogged;\n"
|
|
||||||
"alter table comment_thread set unlogged;\n"
|
|
||||||
"alter table profile_complaint_report set unlogged;\n"
|
|
||||||
"alter table file_change set unlogged;\n"
|
|
||||||
"alter table team_font_variant set unlogged;\n"
|
|
||||||
"alter table share_link set unlogged;\n"
|
|
||||||
"alter table usage_quote set unlogged;\n"
|
|
||||||
"alter table access_token set unlogged;\n"
|
|
||||||
"alter table profile set unlogged;\n"
|
|
||||||
"alter table file_library_rel set unlogged;\n"
|
|
||||||
"alter table file_thumbnail set unlogged;\n"
|
|
||||||
"alter table file_object_thumbnail set unlogged;\n"
|
|
||||||
"alter table file_tagged_object_thumbnail set unlogged;\n"
|
|
||||||
"alter table file_media_object set unlogged;\n"
|
|
||||||
"alter table file_data_fragment set unlogged;\n"
|
|
||||||
"alter table file set unlogged;\n"
|
|
||||||
"alter table project set unlogged;\n"
|
|
||||||
"alter table team_invitation set unlogged;\n"
|
|
||||||
"alter table webhook_delivery set unlogged;\n"
|
|
||||||
"alter table webhook set unlogged;\n"
|
|
||||||
"alter table team set unlogged;\n"
|
|
||||||
;; For some reason, modifying the task realted tables is very very
|
|
||||||
;; slow (5s); so we just don't alter them
|
|
||||||
;; "alter table task set unlogged;\n"
|
|
||||||
;; "alter table task_default set unlogged;\n"
|
|
||||||
;; "alter table task_completed set unlogged;\n"
|
|
||||||
"alter table audit_log set unlogged ;\n"
|
|
||||||
"alter table storage_object set unlogged;\n"
|
|
||||||
"alter table server_error_report set unlogged;\n"
|
|
||||||
"alter table server_prop set unlogged;\n"
|
|
||||||
"alter table global_complaint_report set unlogged;\n"])
|
|
||||||
|
|
||||||
(defn state-init
|
(defn state-init
|
||||||
[next]
|
[next]
|
||||||
(with-redefs [app.config/flags (flags/parse flags/default default-flags)
|
(with-redefs [app.config/flags (flags/parse flags/default default-flags)
|
||||||
|
@ -164,9 +124,6 @@
|
||||||
(try
|
(try
|
||||||
(binding [*system* system
|
(binding [*system* system
|
||||||
*pool* (:app.db/pool system)]
|
*pool* (:app.db/pool system)]
|
||||||
(db/with-atomic [conn *pool*]
|
|
||||||
(doseq [sql test-init-sql]
|
|
||||||
(db/exec! conn [sql])))
|
|
||||||
(next))
|
(next))
|
||||||
(finally
|
(finally
|
||||||
(ig/halt! system))))))
|
(ig/halt! system))))))
|
||||||
|
@ -181,8 +138,7 @@
|
||||||
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
(db/exec-one! conn ["SET CONSTRAINTS ALL DEFERRED"])
|
||||||
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
|
(db/exec-one! conn ["SET LOCAL rules.deletion_protection TO off"])
|
||||||
(let [result (->> (db/exec! conn [sql])
|
(let [result (->> (db/exec! conn [sql])
|
||||||
(map :table-name)
|
(map :table-name))]
|
||||||
(remove #(= "task" %)))]
|
|
||||||
(doseq [table result]
|
(doseq [table result]
|
||||||
(db/exec! conn [(str "delete from " table ";")]))))
|
(db/exec! conn [(str "delete from " table ";")]))))
|
||||||
|
|
||||||
|
@ -263,7 +219,7 @@
|
||||||
([params]
|
([params]
|
||||||
(mark-file-deleted* *system* params))
|
(mark-file-deleted* *system* params))
|
||||||
([conn {:keys [id] :as params}]
|
([conn {:keys [id] :as params}]
|
||||||
(#'files/mark-file-deleted! conn id)))
|
(#'files/mark-file-deleted conn id)))
|
||||||
|
|
||||||
(defn create-team*
|
(defn create-team*
|
||||||
([i params] (create-team* *system* i params))
|
([i params] (create-team* *system* i params))
|
||||||
|
@ -425,6 +381,18 @@
|
||||||
(let [task-fn (get tasks (d/name name))]
|
(let [task-fn (get tasks (d/name name))]
|
||||||
(task-fn params)))))
|
(task-fn params)))))
|
||||||
|
|
||||||
|
(def sql:pending-tasks
|
||||||
|
"select t.* from task as t
|
||||||
|
where t.status = 'new'
|
||||||
|
order by t.priority desc, t.scheduled_at")
|
||||||
|
|
||||||
|
(defn run-pending-tasks!
|
||||||
|
[]
|
||||||
|
(db/tx-run! *system* (fn [{:keys [::db/conn] :as cfg}]
|
||||||
|
(let [tasks (->> (db/exec! conn [sql:pending-tasks])
|
||||||
|
(map #'app.worker.runner/decode-task-row))]
|
||||||
|
(run! (partial #'app.worker.runner/run-task cfg) tasks)))))
|
||||||
|
|
||||||
;; --- UTILS
|
;; --- UTILS
|
||||||
|
|
||||||
(defn print-error!
|
(defn print-error!
|
||||||
|
|
|
@ -1189,6 +1189,7 @@
|
||||||
(t/is (nil? error))
|
(t/is (nil? error))
|
||||||
(t/is (map? result)))
|
(t/is (map? result)))
|
||||||
|
|
||||||
|
;; insert another thumbnail with different revn
|
||||||
(let [data {::th/type :create-file-thumbnail
|
(let [data {::th/type :create-file-thumbnail
|
||||||
::rpc/profile-id (:id prof)
|
::rpc/profile-id (:id prof)
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
|
@ -1207,8 +1208,6 @@
|
||||||
(t/is (= 2 (count rows)))))
|
(t/is (= 2 (count rows)))))
|
||||||
|
|
||||||
(t/testing "gc task"
|
(t/testing "gc task"
|
||||||
;; make the file eligible for GC waiting 300ms (configured
|
|
||||||
;; timeout for testing)
|
|
||||||
(let [res (th/run-task! :file-gc {:min-age 0})]
|
(let [res (th/run-task! :file-gc {:min-age 0})]
|
||||||
(t/is (= 1 (:processed res))))
|
(t/is (= 1 (:processed res))))
|
||||||
|
|
||||||
|
|
|
@ -346,13 +346,5 @@
|
||||||
(assoc :size 312043))))
|
(assoc :size 312043))))
|
||||||
out (th/command! data)]
|
out (th/command! data)]
|
||||||
(t/is (nil? (:error out)))
|
(t/is (nil? (:error out)))
|
||||||
(t/is (map? (:result out))))
|
(t/is (map? (:result out))))))
|
||||||
|
|
||||||
(let [[row1 :as rows]
|
|
||||||
(->> (th/db-query :task {:name "object-update"})
|
|
||||||
(map #(update % :props db/decode-transit-pgobject)))]
|
|
||||||
|
|
||||||
;; (app.common.pprint/pprint rows)
|
|
||||||
(t/is (= 1 (count rows)))
|
|
||||||
(t/is (> (inst-ms (dt/diff (:created-at row1) (:scheduled-at row1)))
|
|
||||||
(inst-ms (dt/duration "4m")))))))
|
|
||||||
|
|
|
@ -391,6 +391,8 @@
|
||||||
(t/is (= 1 (count result)))
|
(t/is (= 1 (count result)))
|
||||||
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
|
(t/is (= (:default-team-id profile1) (get-in result [0 :id])))))
|
||||||
|
|
||||||
|
(th/run-pending-tasks!)
|
||||||
|
|
||||||
;; run permanent deletion (should be noop)
|
;; run permanent deletion (should be noop)
|
||||||
(let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})]
|
(let [result (th/run-task! :objects-gc {:min-age (dt/duration {:minutes 1})})]
|
||||||
(t/is (= 0 (:processed result))))
|
(t/is (= 0 (:processed result))))
|
||||||
|
@ -457,6 +459,8 @@
|
||||||
#_(th/print-result! out)
|
#_(th/print-result! out)
|
||||||
(t/is (nil? (:error out))))
|
(t/is (nil? (:error out))))
|
||||||
|
|
||||||
|
(th/run-pending-tasks!)
|
||||||
|
|
||||||
(let [rows (th/db-exec! ["select * from team where id = ?" (:id team)])]
|
(let [rows (th/db-exec! ["select * from team where id = ?" (:id team)])]
|
||||||
(t/is (= 1 (count rows)))
|
(t/is (= 1 (count rows)))
|
||||||
(t/is (dt/instant? (:deleted-at (first rows)))))
|
(t/is (dt/instant? (:deleted-at (first rows)))))
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
{:tests
|
{:tests
|
||||||
[{:id :unit
|
[{:id :unit
|
||||||
:test-paths ["test" "src"]
|
:test-paths ["test" "src"]
|
||||||
:ns-patterns [".*-test$"]}]}
|
:ns-patterns [".*-test$"]
|
||||||
|
:kaocha/reporter [kaocha.report/dots]}]}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,7 +5,7 @@
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC",
|
"author": "Kaleidos INC",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "yarn@4.0.2",
|
"packageManager": "yarn@4.2.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/penpot/penpot"
|
"url": "https://github.com/penpot/penpot"
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
(and add-container? (nil? component-id))
|
(and add-container? (nil? component-id))
|
||||||
(assoc :page-id (:current-page-id file)
|
(assoc :page-id (:current-page-id file)
|
||||||
:frame-id (:current-frame-id file)))
|
:frame-id (:current-frame-id file)))
|
||||||
|
|
||||||
valid? (ch/check-change! change)]
|
valid? (ch/check-change! change)]
|
||||||
|
|
||||||
(when-not valid?
|
(when-not valid?
|
||||||
|
@ -135,13 +136,8 @@
|
||||||
(create-file (uuid/next) name))
|
(create-file (uuid/next) name))
|
||||||
|
|
||||||
([id name]
|
([id name]
|
||||||
{:id id
|
(-> (ctf/make-file {:id id :name name :create-page false})
|
||||||
:name name
|
(assoc :changes [])))) ;; We keep the changes so we can send them to the backend
|
||||||
:data (-> ctf/empty-file-data
|
|
||||||
(assoc :id id))
|
|
||||||
|
|
||||||
;; We keep the changes so we can send them to the backend
|
|
||||||
:changes []}))
|
|
||||||
|
|
||||||
(defn add-page
|
(defn add-page
|
||||||
[file data]
|
[file data]
|
||||||
|
@ -511,9 +507,12 @@
|
||||||
{:type :del-media
|
{:type :del-media
|
||||||
:id id}))))
|
:id id}))))
|
||||||
|
|
||||||
|
|
||||||
(defn start-component
|
(defn start-component
|
||||||
([file data] (start-component file data :group))
|
([file data]
|
||||||
|
(let [components-v2 (dm/get-in file [:data :options :components-v2])
|
||||||
|
root-type (if components-v2 :frame :group)]
|
||||||
|
(start-component file data root-type)))
|
||||||
|
|
||||||
([file data root-type]
|
([file data root-type]
|
||||||
;; FIXME: data probably can be a shape instance, then we can use gsh/shape->rect
|
;; 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 [selrect (or (grc/make-rect (:x data) (:y data) (:width data) (:height data))
|
||||||
|
@ -566,9 +565,11 @@
|
||||||
|
|
||||||
file
|
file
|
||||||
(cond
|
(cond
|
||||||
;; Components-v2 component we skip this step
|
;; In components-v2 components haven't any shape inside them.
|
||||||
(and component-data (:main-instance-id component-data))
|
(and component-data (:main-instance-id component-data))
|
||||||
file
|
(update file :data
|
||||||
|
(fn [data]
|
||||||
|
(ctkl/update-component data component-id dissoc :objects)))
|
||||||
|
|
||||||
(empty? children)
|
(empty? children)
|
||||||
(commit-change
|
(commit-change
|
||||||
|
|
|
@ -6,4 +6,4 @@
|
||||||
|
|
||||||
(ns app.common.files.defaults)
|
(ns app.common.files.defaults)
|
||||||
|
|
||||||
(def version 46)
|
(def version 47)
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.svg :as csvg]
|
[app.common.svg :as csvg]
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
|
[app.common.types.component :as ctk]
|
||||||
|
[app.common.types.file :as ctf]
|
||||||
[app.common.types.shape :as cts]
|
[app.common.types.shape :as cts]
|
||||||
[app.common.types.shape.shadow :as ctss]
|
[app.common.types.shape.shadow :as ctss]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
@ -898,6 +900,29 @@
|
||||||
(update :pages-index update-vals update-container)
|
(update :pages-index update-vals update-container)
|
||||||
(update :components update-vals update-container))))
|
(update :components update-vals update-container))))
|
||||||
|
|
||||||
|
(defn migrate-up-47
|
||||||
|
[data]
|
||||||
|
(letfn [(fix-shape [page shape]
|
||||||
|
(let [file {:id (:id data) :data data}
|
||||||
|
component-file (:component-file shape)
|
||||||
|
;; On cloning a file, the component-file of the shapes point to the old file id
|
||||||
|
;; this is a workaround to be able to found the components in that case
|
||||||
|
libraries {component-file {:id component-file :data data}}
|
||||||
|
ref-shape (ctf/find-ref-shape file page libraries shape {:include-deleted? true :with-context? true})
|
||||||
|
ref-parent (get (:objects (:container (meta ref-shape))) (:parent-id ref-shape))
|
||||||
|
shape-swap-slot (ctk/get-swap-slot shape)
|
||||||
|
ref-swap-slot (ctk/get-swap-slot ref-shape)]
|
||||||
|
(if (and (some? shape-swap-slot)
|
||||||
|
(= shape-swap-slot ref-swap-slot)
|
||||||
|
(ctk/main-instance? ref-parent))
|
||||||
|
(ctk/remove-swap-slot shape)
|
||||||
|
shape)))
|
||||||
|
|
||||||
|
(update-page [page]
|
||||||
|
(d/update-when page :objects update-vals (partial fix-shape page)))]
|
||||||
|
(-> data
|
||||||
|
(update :pages-index update-vals update-page))))
|
||||||
|
|
||||||
(def migrations
|
(def migrations
|
||||||
"A vector of all applicable migrations"
|
"A vector of all applicable migrations"
|
||||||
[{:id 2 :migrate-up migrate-up-2}
|
[{:id 2 :migrate-up migrate-up-2}
|
||||||
|
@ -935,4 +960,5 @@
|
||||||
{:id 43 :migrate-up migrate-up-43}
|
{:id 43 :migrate-up migrate-up-43}
|
||||||
{:id 44 :migrate-up migrate-up-44}
|
{:id 44 :migrate-up migrate-up-44}
|
||||||
{:id 45 :migrate-up migrate-up-45}
|
{:id 45 :migrate-up migrate-up-45}
|
||||||
{:id 46 :migrate-up migrate-up-46}])
|
{:id 46 :migrate-up migrate-up-46}
|
||||||
|
{:id 47 :migrate-up migrate-up-47}])
|
||||||
|
|
|
@ -460,6 +460,21 @@
|
||||||
(pcb/with-library-data file-data)
|
(pcb/with-library-data file-data)
|
||||||
(pcb/update-component (:id shape) repair-component))))
|
(pcb/update-component (:id shape) repair-component))))
|
||||||
|
|
||||||
|
(defmethod repair-error :missing-slot
|
||||||
|
[_ {:keys [shape page-id args] :as error} file-data _]
|
||||||
|
(let [repair-shape
|
||||||
|
(fn [shape]
|
||||||
|
;; Set the desired swap slot
|
||||||
|
(let [slot (:swap-slot args)]
|
||||||
|
(when (some? slot)
|
||||||
|
(log/debug :hint (str " -> set swap-slot to " slot))
|
||||||
|
(update shape :touched cfh/set-touched-group (ctk/build-swap-slot-group slot)))))]
|
||||||
|
|
||||||
|
(log/dbg :hint "repairing shape :missing-slot" :id (:id shape) :name (:name shape) :page-id page-id)
|
||||||
|
(-> (pcb/empty-changes nil page-id)
|
||||||
|
(pcb/with-file-data file-data)
|
||||||
|
(pcb/update-shapes [(:id shape)] repair-shape))))
|
||||||
|
|
||||||
(defmethod repair-error :default
|
(defmethod repair-error :default
|
||||||
[_ error file _]
|
[_ error file _]
|
||||||
(log/error :hint "Unknown error code, don't know how to repair" :code (:code error))
|
(log/error :hint "Unknown error code, don't know how to repair" :code (:code error))
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
(ns app.common.files.validate
|
(ns app.common.files.validate
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
|
@ -50,7 +51,8 @@
|
||||||
:not-head-copy-not-allowed
|
:not-head-copy-not-allowed
|
||||||
:not-component-not-allowed
|
:not-component-not-allowed
|
||||||
:component-nil-objects-not-allowed
|
:component-nil-objects-not-allowed
|
||||||
:instance-head-not-frame})
|
:instance-head-not-frame
|
||||||
|
:missing-slot})
|
||||||
|
|
||||||
(def ^:private
|
(def ^:private
|
||||||
schema:error
|
schema:error
|
||||||
|
@ -454,6 +456,8 @@
|
||||||
;; PUBLIC API: VALIDATION FUNCTIONS
|
;; PUBLIC API: VALIDATION FUNCTIONS
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(declare check-swap-slots)
|
||||||
|
|
||||||
(defn validate-file
|
(defn validate-file
|
||||||
"Validate full referential integrity and semantic coherence on file data.
|
"Validate full referential integrity and semantic coherence on file data.
|
||||||
|
|
||||||
|
@ -464,6 +468,8 @@
|
||||||
|
|
||||||
(doseq [page (filter :id (ctpl/pages-seq data))]
|
(doseq [page (filter :id (ctpl/pages-seq data))]
|
||||||
(check-shape uuid/zero file page libraries)
|
(check-shape uuid/zero file page libraries)
|
||||||
|
(when (str/includes? (:name file) "check-swap-slot")
|
||||||
|
(check-swap-slots uuid/zero file page libraries))
|
||||||
(->> (get-orphan-shapes page)
|
(->> (get-orphan-shapes page)
|
||||||
(run! #(check-shape % file page libraries))))
|
(run! #(check-shape % file page libraries))))
|
||||||
|
|
||||||
|
@ -517,3 +523,41 @@
|
||||||
:hint "error on validating file referential integrity"
|
:hint "error on validating file referential integrity"
|
||||||
:file-id (:id file)
|
:file-id (:id file)
|
||||||
:details errors)))
|
:details errors)))
|
||||||
|
|
||||||
|
|
||||||
|
(declare compare-slots)
|
||||||
|
|
||||||
|
;; Optional check to look for missing swap slots.
|
||||||
|
;; Search for copies that do not point the shape-ref to the near component but don't have swap slot
|
||||||
|
;; (looking for position relative to the parent, in the copy and the main).
|
||||||
|
;;
|
||||||
|
;; This check cannot be generally enabled, because files that have been migrated from components v1
|
||||||
|
;; may have copies with shapes that do not match by position, but have not been swapped. So we enable
|
||||||
|
;; it for specific files only. To activate the check, you need to add the string "check-swap-slot" to
|
||||||
|
;; the name of the file.
|
||||||
|
(defn- check-swap-slots
|
||||||
|
[shape-id file page libraries]
|
||||||
|
(let [shape (ctst/get-shape page shape-id)]
|
||||||
|
(if (and (ctk/instance-root? shape) (ctk/in-component-copy? shape))
|
||||||
|
(let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true :with-context? true)
|
||||||
|
container (:container (meta ref-shape))]
|
||||||
|
(when (some? ref-shape)
|
||||||
|
(compare-slots shape ref-shape file page container)))
|
||||||
|
(doall (for [child-id (:shapes shape)]
|
||||||
|
(check-swap-slots child-id file page libraries))))))
|
||||||
|
|
||||||
|
(defn- compare-slots
|
||||||
|
[shape-copy shape-main file container-copy container-main]
|
||||||
|
(if (and (not= (:shape-ref shape-copy) (:id shape-main))
|
||||||
|
(nil? (ctk/get-swap-slot shape-copy)))
|
||||||
|
(report-error :missing-slot
|
||||||
|
"Shape has been swapped, should have swap slot"
|
||||||
|
shape-copy file container-copy
|
||||||
|
:swap-slot (or (ctk/get-swap-slot shape-main) (:id shape-main)))
|
||||||
|
(when (nil? (ctk/get-swap-slot shape-copy))
|
||||||
|
(let [children-id-pairs (d/zip-all (:shapes shape-copy) (:shapes shape-main))]
|
||||||
|
(doall (for [[child-copy-id child-main-id] children-id-pairs]
|
||||||
|
(let [child-copy (ctst/get-shape container-copy child-copy-id)
|
||||||
|
child-main (ctst/get-shape container-main child-main-id)]
|
||||||
|
(when (and (some? child-copy) (some? child-main))
|
||||||
|
(compare-slots child-copy child-main file container-copy container-main)))))))))
|
||||||
|
|
|
@ -122,12 +122,12 @@
|
||||||
#(ctst/add-shape (:id shape)
|
#(ctst/add-shape (:id shape)
|
||||||
shape
|
shape
|
||||||
%
|
%
|
||||||
(:parent-id shape)
|
|
||||||
(:frame-id shape)
|
(:frame-id shape)
|
||||||
|
(:parent-id shape)
|
||||||
nil
|
nil
|
||||||
true)))
|
true)))
|
||||||
$
|
$
|
||||||
(remove #(= (:id %) (:did copy-root')) copy-shapes)))))]
|
(remove #(= (:id %) (:id copy-root')) copy-shapes)))))]
|
||||||
(when children-labels
|
(when children-labels
|
||||||
(dotimes [idx (count children-labels)]
|
(dotimes [idx (count children-labels)]
|
||||||
(set-child-label file' copy-root-label idx (nth children-labels idx))))
|
(set-child-label file' copy-root-label idx (nth children-labels idx))))
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.files.changes-builder :as pcb]
|
[app.common.files.changes-builder :as pcb]
|
||||||
|
[app.common.geom.point :as gpt]
|
||||||
[app.common.logic.libraries :as cll]
|
[app.common.logic.libraries :as cll]
|
||||||
[app.common.logic.shapes :as cls]
|
[app.common.logic.shapes :as cls]
|
||||||
[app.common.test-helpers.components :as thc]
|
[app.common.test-helpers.components :as thc]
|
||||||
|
@ -20,7 +21,7 @@
|
||||||
(defn add-rect
|
(defn add-rect
|
||||||
[file rect-label & {:keys [] :as params}]
|
[file rect-label & {:keys [] :as params}]
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; :rect-label [:type :rect :name: Rect1]
|
;; :rect-label [:type :rect :name Rect1]
|
||||||
(ths/add-sample-shape file rect-label
|
(ths/add-sample-shape file rect-label
|
||||||
(merge {:type :rect
|
(merge {:type :rect
|
||||||
:name "Rect1"}
|
:name "Rect1"}
|
||||||
|
@ -29,17 +30,26 @@
|
||||||
(defn add-frame
|
(defn add-frame
|
||||||
[file frame-label & {:keys [] :as params}]
|
[file frame-label & {:keys [] :as params}]
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; :frame-label [:type :frame :name: Frame1]
|
;; :frame-label [:type :frame :name Frame1]
|
||||||
(ths/add-sample-shape file frame-label
|
(ths/add-sample-shape file frame-label
|
||||||
(merge {:type :frame
|
(merge {:type :frame
|
||||||
:name "Frame1"}
|
:name "Frame1"}
|
||||||
params)))
|
params)))
|
||||||
|
|
||||||
|
(defn add-group
|
||||||
|
[file group-label & {:keys [] :as params}]
|
||||||
|
;; Generated shape tree:
|
||||||
|
;; :group-label [:type :group :name Group1]
|
||||||
|
(ths/add-sample-shape file group-label
|
||||||
|
(merge {:type :group
|
||||||
|
:name "Group1"}
|
||||||
|
params)))
|
||||||
|
|
||||||
(defn add-frame-with-child
|
(defn add-frame-with-child
|
||||||
[file frame-label child-label & {:keys [frame-params child-params]}]
|
[file frame-label child-label & {:keys [frame-params child-params]}]
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; :frame-label [:name: Frame1]
|
;; :frame-label [:name Frame1]
|
||||||
;; :child-label [:name: Rect1]
|
;; :child-label [:name Rect1]
|
||||||
(-> file
|
(-> file
|
||||||
(add-frame frame-label frame-params)
|
(add-frame frame-label frame-params)
|
||||||
(ths/add-sample-shape child-label
|
(ths/add-sample-shape child-label
|
||||||
|
@ -52,8 +62,8 @@
|
||||||
[file component-label root-label child-label
|
[file component-label root-label child-label
|
||||||
& {:keys [component-params root-params child-params]}]
|
& {:keys [component-params root-params child-params]}]
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; {:root-label} [:name: Frame1] # [Component :component-label]
|
;; {:root-label} [:name Frame1] # [Component :component-label]
|
||||||
;; :child-label [:name: Rect1]
|
;; :child-label [:name Rect1]
|
||||||
(-> file
|
(-> file
|
||||||
(add-frame-with-child root-label child-label :frame-params root-params :child-params child-params)
|
(add-frame-with-child root-label child-label :frame-params root-params :child-params child-params)
|
||||||
(thc/make-component component-label root-label component-params)))
|
(thc/make-component component-label root-label component-params)))
|
||||||
|
@ -62,11 +72,11 @@
|
||||||
[file component-label main-root-label main-child-label copy-root-label
|
[file component-label main-root-label main-child-label copy-root-label
|
||||||
& {:keys [component-params main-root-params main-child-params copy-root-params]}]
|
& {:keys [component-params main-root-params main-child-params copy-root-params]}]
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; {:main-root-label} [:name: Frame1] # [Component :component-label]
|
;; {:main-root-label} [:name Frame1] # [Component :component-label]
|
||||||
;; :main-child-label [:name: Rect1]
|
;; :main-child-label [:name Rect1]
|
||||||
;;
|
;;
|
||||||
;; :copy-root-label [:name: Frame1] #--> [Component :component-label] :main-root-label
|
;; :copy-root-label [:name Frame1] #--> [Component :component-label] :main-root-label
|
||||||
;; <no-label> [:name: Rect1] ---> :main-child-label
|
;; <no-label> [:name Rect1] ---> :main-child-label
|
||||||
(-> file
|
(-> file
|
||||||
(add-simple-component component-label
|
(add-simple-component component-label
|
||||||
main-root-label
|
main-root-label
|
||||||
|
@ -80,10 +90,10 @@
|
||||||
[file component-label root-label child-labels
|
[file component-label root-label child-labels
|
||||||
& {:keys [component-params root-params child-params-list]}]
|
& {:keys [component-params root-params child-params-list]}]
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; {:root-label} [:name: Frame1] # [Component :component-label]
|
;; {:root-label} [:name Frame1] # [Component :component-label]
|
||||||
;; :child1-label [:name: Rect1]
|
;; :child1-label [:name Rect1]
|
||||||
;; :child2-label [:name: Rect2]
|
;; :child2-label [:name Rect2]
|
||||||
;; :child3-label [:name: Rect3]
|
;; :child3-label [:name Rect3]
|
||||||
(as-> file $
|
(as-> file $
|
||||||
(add-frame $ root-label root-params)
|
(add-frame $ root-label root-params)
|
||||||
(reduce (fn [file [index [label params]]]
|
(reduce (fn [file [index [label params]]]
|
||||||
|
@ -101,15 +111,15 @@
|
||||||
[file component-label main-root-label main-child-labels copy-root-label
|
[file component-label main-root-label main-child-labels copy-root-label
|
||||||
& {:keys [component-params main-root-params main-child-params-list copy-root-params]}]
|
& {:keys [component-params main-root-params main-child-params-list copy-root-params]}]
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; {:root-label} [:name: Frame1] # [Component :component-label]
|
;; {:root-label} [:name Frame1] # [Component :component-label]
|
||||||
;; :child1-label [:name: Rect1]
|
;; :child1-label [:name Rect1]
|
||||||
;; :child2-label [:name: Rect2]
|
;; :child2-label [:name Rect2]
|
||||||
;; :child3-label [:name: Rect3]
|
;; :child3-label [:name Rect3]
|
||||||
;;
|
;;
|
||||||
;; :copy-root-label [:name: Frame1] #--> [Component :component-label] :root-label
|
;; :copy-root-label [:name Frame1] #--> [Component :component-label] :root-label
|
||||||
;; <no-label> [:name: Rect1] ---> :child1-label
|
;; <no-label> [:name Rect1] ---> :child1-label
|
||||||
;; <no-label> [:name: Rect2] ---> :child2-label
|
;; <no-label> [:name Rect2] ---> :child2-label
|
||||||
;; <no-label> [:name: Rect3] ---> :child3-label
|
;; <no-label> [:name Rect3] ---> :child3-label
|
||||||
(-> file
|
(-> file
|
||||||
(add-component-with-many-children component-label
|
(add-component-with-many-children component-label
|
||||||
main-root-label
|
main-root-label
|
||||||
|
@ -123,12 +133,12 @@
|
||||||
[file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label
|
[file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label
|
||||||
& {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params]}]
|
& {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params]}]
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; {:main1-root-label} [:name: Frame1] # [Component :component1-label]
|
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
||||||
;; :main1-child-label [:name: Rect1]
|
;; :main1-child-label [:name Rect1]
|
||||||
;;
|
;;
|
||||||
;; {:main2-root-label} [:name: Frame2] # [Component :component2-label]
|
;; {:main2-root-label} [:name Frame2] # [Component :component2-label]
|
||||||
;; :nested-head-label [:name: Frame1] @--> [Component :component1-label] :main1-root-label
|
;; :nested-head-label [:name Frame1] @--> [Component :component1-label] :main1-root-label
|
||||||
;; <no-label> [:name: Rect1] ---> :main1-child-label
|
;; <no-label> [:name Rect1] ---> :main1-child-label
|
||||||
(-> file
|
(-> file
|
||||||
(add-simple-component component1-label
|
(add-simple-component component1-label
|
||||||
main1-root-label
|
main1-root-label
|
||||||
|
@ -150,16 +160,16 @@
|
||||||
[file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label copy2-root-label
|
[file component1-label main1-root-label main1-child-label component2-label main2-root-label nested-head-label copy2-root-label
|
||||||
& {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params copy2-root-params]}]
|
& {:keys [component1-params root1-params main1-child-params component2-params main2-root-params nested-head-params copy2-root-params]}]
|
||||||
;; Generated shape tree:
|
;; Generated shape tree:
|
||||||
;; {:main1-root-label} [:name: Frame1] # [Component :component1-label]
|
;; {:main1-root-label} [:name Frame1] # [Component :component1-label]
|
||||||
;; :main1-child-label [:name: Rect1]
|
;; :main1-child-label [:name Rect1]
|
||||||
;;
|
;;
|
||||||
;; {:main2-root-label} [:name: Frame2] # [Component :component2-label]
|
;; {:main2-root-label} [:name Frame2] # [Component :component2-label]
|
||||||
;; :nested-head-label [:name: Frame1] @--> [Component :component1-label] :main1-root-label
|
;; :nested-head-label [:name Frame1] @--> [Component :component1-label] :main1-root-label
|
||||||
;; <no-label> [:name: Rect1] ---> :main1-child-label
|
;; <no-label> [:name Rect1] ---> :main1-child-label
|
||||||
;;
|
;;
|
||||||
;; :copy2-label [:name: Frame2] #--> [Component :component2-label] :main2-root-label
|
;; :copy2-label [:name Frame2] #--> [Component :component2-label] :main2-root-label
|
||||||
;; <no-label> [:name: Frame1] @--> [Component :component1-label] :nested-head-label
|
;; <no-label> [:name Frame1] @--> [Component :component1-label] :nested-head-label
|
||||||
;; <no-label> [:name: Rect1] ---> <no-label>
|
;; <no-label> [:name Rect1] ---> <no-label>
|
||||||
(-> file
|
(-> file
|
||||||
(add-nested-component component1-label
|
(add-nested-component component1-label
|
||||||
main1-root-label
|
main1-root-label
|
||||||
|
@ -334,3 +344,25 @@
|
||||||
(if propagate-fn
|
(if propagate-fn
|
||||||
(propagate-fn file')
|
(propagate-fn file')
|
||||||
file')))
|
file')))
|
||||||
|
|
||||||
|
(defn duplicate-shape [file shape-tag & {:keys [page-label propagate-fn]}]
|
||||||
|
(let [page (if page-label
|
||||||
|
(thf/get-page file page-label)
|
||||||
|
(thf/current-page file))
|
||||||
|
shape (ths/get-shape file shape-tag :page-label page-label)
|
||||||
|
changes
|
||||||
|
(-> (pcb/empty-changes nil)
|
||||||
|
(cll/generate-duplicate-changes (:objects page) ;; objects
|
||||||
|
page ;; page
|
||||||
|
#{(:id shape)} ;; ids
|
||||||
|
(gpt/point 0 0) ;; delta
|
||||||
|
{(:id file) file} ;; libraries
|
||||||
|
(:data file) ;; library-data
|
||||||
|
(:id file)) ;; file-id
|
||||||
|
(cll/generate-duplicate-changes-update-indices (:objects page) ;; objects
|
||||||
|
#{(:id shape)}))
|
||||||
|
file' (thf/apply-changes file changes)]
|
||||||
|
(if propagate-fn
|
||||||
|
(propagate-fn file')
|
||||||
|
file')))
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,11 @@
|
||||||
;; ---- Helpers to manage ids as known identifiers
|
;; ---- Helpers to manage ids as known identifiers
|
||||||
|
|
||||||
(def ^:private idmap (atom {}))
|
(def ^:private idmap (atom {}))
|
||||||
|
(def ^:private next-uuid-val (atom 1))
|
||||||
|
|
||||||
(defn reset-idmap! []
|
(defn reset-idmap! []
|
||||||
(reset! idmap {}))
|
(reset! idmap {})
|
||||||
|
(reset! next-uuid-val 1))
|
||||||
|
|
||||||
(defn set-id!
|
(defn set-id!
|
||||||
[label id]
|
[label id]
|
||||||
|
@ -41,3 +43,8 @@
|
||||||
(map key)
|
(map key)
|
||||||
(first))
|
(first))
|
||||||
(str "<no-label #" (subs (str id) (- (count (str id)) 6)) ">")))
|
(str "<no-label #" (subs (str id) (- (count (str id)) 6)) ">")))
|
||||||
|
|
||||||
|
(defn next-uuid []
|
||||||
|
(let [current (uuid/custom @next-uuid-val)]
|
||||||
|
(swap! next-uuid-val inc)
|
||||||
|
current))
|
||||||
|
|
|
@ -386,8 +386,7 @@
|
||||||
(fn [new-shape original-shape]
|
(fn [new-shape original-shape]
|
||||||
(let [new-name (:name new-shape)
|
(let [new-name (:name new-shape)
|
||||||
root? (or (ctk/instance-root? original-shape) ; If shape is inside a component (not components-v2)
|
root? (or (ctk/instance-root? original-shape) ; If shape is inside a component (not components-v2)
|
||||||
(nil? (:parent-id original-shape))) ; we detect it by having no parent)
|
(nil? (:parent-id original-shape)))] ; we detect it by having no parent)
|
||||||
swap-slot (ctk/get-swap-slot original-shape)]
|
|
||||||
|
|
||||||
(when root?
|
(when root?
|
||||||
(vswap! unames conj new-name))
|
(vswap! unames conj new-name))
|
||||||
|
@ -399,9 +398,6 @@
|
||||||
(-> (gsh/move delta)
|
(-> (gsh/move delta)
|
||||||
(dissoc :touched))
|
(dissoc :touched))
|
||||||
|
|
||||||
(some? swap-slot)
|
|
||||||
(assoc :touched #{(ctk/build-swap-slot-group swap-slot)})
|
|
||||||
|
|
||||||
(and main-instance? root?)
|
(and main-instance? root?)
|
||||||
(assoc :main-instance true)
|
(assoc :main-instance true)
|
||||||
|
|
||||||
|
|
BIN
common/test/cases/copying-and-duplicating.penpot
Normal file
BIN
common/test/cases/copying-and-duplicating.penpot
Normal file
Binary file not shown.
122
common/test/common_tests/logic/copying_and_duplicating_test.cljc
Normal file
122
common/test/common_tests/logic/copying_and_duplicating_test.cljc
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns common-tests.logic.copying-and-duplicating-test
|
||||||
|
(:require
|
||||||
|
[app.common.files.changes :as ch]
|
||||||
|
[app.common.files.changes-builder :as pcb]
|
||||||
|
[app.common.logic.libraries :as cll]
|
||||||
|
[app.common.logic.shapes :as cls]
|
||||||
|
[app.common.pprint :as pp]
|
||||||
|
[app.common.test-helpers.components :as thc]
|
||||||
|
[app.common.test-helpers.compositions :as tho]
|
||||||
|
[app.common.test-helpers.files :as thf]
|
||||||
|
[app.common.test-helpers.ids-map :as thi]
|
||||||
|
[app.common.test-helpers.shapes :as ths]
|
||||||
|
[app.common.types.component :as ctk]
|
||||||
|
[app.common.types.container :as ctn]
|
||||||
|
[app.common.types.file :as ctf]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[clojure.test :as t]))
|
||||||
|
|
||||||
|
(t/use-fixtures :each thi/test-fixture)
|
||||||
|
|
||||||
|
(defn- setup []
|
||||||
|
(-> (thf/sample-file :file1)
|
||||||
|
(tho/add-simple-component :simple-1 :frame-simple-1 :rect-simple-1
|
||||||
|
:child-params {:type :rect :fills (ths/sample-fills-color :fill-color "#2152e5") :name "rect-simple-1"})
|
||||||
|
|
||||||
|
(tho/add-frame :frame-composed-1 :name "frame-composed-1")
|
||||||
|
(thc/instantiate-component :simple-1 :copy-simple-1 :parent-label :frame-composed-1 :children-labels [:composed-1-simple-1])
|
||||||
|
(ths/add-sample-shape :rect-composed-1 :parent-label :frame-composed-1 :fills (ths/sample-fills-color :fill-color "#B1B2B5"))
|
||||||
|
(thc/make-component :composed-1 :frame-composed-1)
|
||||||
|
|
||||||
|
(tho/add-frame :frame-composed-2 :name "frame-composed-2")
|
||||||
|
(thc/instantiate-component :composed-1 :copy-composed-1-composed-2 :parent-label :frame-composed-2 :children-labels [:composed-1-composed-2])
|
||||||
|
(thc/make-component :composed-2 :frame-composed-2)
|
||||||
|
|
||||||
|
(thc/instantiate-component :composed-2 :copy-composed-2)
|
||||||
|
|
||||||
|
(tho/add-frame :frame-composed-3 :name "frame-composed-3")
|
||||||
|
(tho/add-group :group-3 :parent-label :frame-composed-3)
|
||||||
|
(thc/instantiate-component :composed-2 :copy-composed-1-composed-3 :parent-label :group-3 :children-labels [:composed-1-composed-2])
|
||||||
|
(ths/add-sample-shape :circle-composed-3 :parent-label :group-3 :fills (ths/sample-fills-color :fill-color "#B1B2B5"))
|
||||||
|
(thc/make-component :composed-3 :frame-composed-3)
|
||||||
|
|
||||||
|
(thc/instantiate-component :composed-3 :copy-composed-3 :children-labels [:composed-2-composed-3])))
|
||||||
|
|
||||||
|
(defn- propagate-all-component-changes [file]
|
||||||
|
(-> file
|
||||||
|
(tho/propagate-component-changes :simple-1)
|
||||||
|
(tho/propagate-component-changes :composed-1)
|
||||||
|
(tho/propagate-component-changes :composed-2)
|
||||||
|
(tho/propagate-component-changes :composed-3)))
|
||||||
|
|
||||||
|
(defn- count-shapes [file name color]
|
||||||
|
(let [page (thf/current-page file)]
|
||||||
|
(->> (vals (:objects page))
|
||||||
|
(filter #(and
|
||||||
|
(= (:name %) name)
|
||||||
|
(-> (ths/get-shape-by-id file (:id %))
|
||||||
|
:fills
|
||||||
|
first
|
||||||
|
:fill-color
|
||||||
|
(= color))))
|
||||||
|
(count))))
|
||||||
|
|
||||||
|
(defn- validate [file validator]
|
||||||
|
(validator file)
|
||||||
|
file)
|
||||||
|
|
||||||
|
;; Related .penpot file: common/test/cases/copying-and-duplicating.penpot
|
||||||
|
(t/deftest main-and-first-level-copy
|
||||||
|
(-> (setup)
|
||||||
|
;; For each main and first level copy:
|
||||||
|
;; - Duplicate it two times.
|
||||||
|
(tho/duplicate-shape :frame-simple-1)
|
||||||
|
(tho/duplicate-shape :frame-simple-1)
|
||||||
|
(tho/duplicate-shape :frame-composed-1)
|
||||||
|
(tho/duplicate-shape :frame-composed-1)
|
||||||
|
(tho/duplicate-shape :frame-composed-2)
|
||||||
|
(tho/duplicate-shape :frame-composed-2)
|
||||||
|
(tho/duplicate-shape :frame-composed-3)
|
||||||
|
(tho/duplicate-shape :frame-composed-3)
|
||||||
|
(tho/duplicate-shape :copy-composed-2)
|
||||||
|
(tho/duplicate-shape :copy-composed-2)
|
||||||
|
(tho/duplicate-shape :copy-composed-3)
|
||||||
|
(tho/duplicate-shape :copy-composed-3)
|
||||||
|
|
||||||
|
;; - Change color of Simple1 and check propagation to all copies.
|
||||||
|
(tho/update-bottom-color :frame-simple-1 "#111111" :propagate-fn propagate-all-component-changes)
|
||||||
|
(validate #(t/is (= (count-shapes % "rect-simple-1" "#111111") 18)))
|
||||||
|
;; - Change color of the nearest main and check propagation to duplicated.
|
||||||
|
(tho/update-bottom-color :frame-composed-1 "#222222" :propagate-fn propagate-all-component-changes)
|
||||||
|
(validate #(t/is (= (count-shapes % "rect-simple-1" "#222222") 15)))
|
||||||
|
(tho/update-bottom-color :frame-composed-2 "#333333" :propagate-fn propagate-all-component-changes)
|
||||||
|
(validate #(t/is (= (count-shapes % "rect-simple-1" "#333333") 12)))
|
||||||
|
(tho/update-bottom-color :frame-composed-3 "#444444" :propagate-fn propagate-all-component-changes)
|
||||||
|
(validate #(t/is (= (count-shapes % "rect-simple-1" "#444444") 6)))))
|
||||||
|
|
||||||
|
(t/deftest copy-nested-in-main
|
||||||
|
(-> (setup)
|
||||||
|
;; For each copy of Simple1 nested in a main, and the group inside Composed3 main:
|
||||||
|
;; - Duplicate it two times, keeping the duplicated inside the same main.
|
||||||
|
(tho/duplicate-shape :copy-simple-1)
|
||||||
|
(tho/duplicate-shape :copy-simple-1)
|
||||||
|
(tho/duplicate-shape :group-3)
|
||||||
|
(tho/duplicate-shape :group-3)
|
||||||
|
|
||||||
|
;; - Change color of Simple1 and check propagation to all copies.
|
||||||
|
(tho/update-bottom-color :frame-simple-1 "#111111" :propagate-fn propagate-all-component-changes)
|
||||||
|
(validate #(t/is (= (count-shapes % "rect-simple-1" "#111111") 28)))
|
||||||
|
|
||||||
|
;; - Change color of the nearest main and check propagation to duplicated.
|
||||||
|
(tho/update-bottom-color :frame-composed-1 "#222222" :propagate-fn propagate-all-component-changes)
|
||||||
|
(validate #(t/is (= (count-shapes % "rect-simple-1" "#222222") 9)))
|
||||||
|
|
||||||
|
;; - Change color of the copy you duplicated from, and check that it's NOT PROPAGATED.
|
||||||
|
(tho/update-bottom-color :group-3 "#333333" :propagate-fn propagate-all-component-changes)
|
||||||
|
(validate #(t/is (= (count-shapes % "rect-simple-1" "#333333") 2)))))
|
|
@ -59,7 +59,7 @@
|
||||||
(validator file)
|
(validator file)
|
||||||
file)
|
file)
|
||||||
|
|
||||||
;; Related .penpot file: common/test/cases/xxxxxx
|
;; Related .penpot file: common/test/cases/swap-as-override.penpot
|
||||||
(t/deftest swap-main-then-copy
|
(t/deftest swap-main-then-copy
|
||||||
(-> (setup)
|
(-> (setup)
|
||||||
;; Swap icon in icon+text main. Check that it propagates to copies.
|
;; Swap icon in icon+text main. Check that it propagates to copies.
|
||||||
|
|
699
common/yarn.lock
699
common/yarn.lock
File diff suppressed because it is too large
Load diff
|
@ -3,8 +3,8 @@ LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
|
||||||
|
|
||||||
ARG DEBIAN_FRONTEND=noninteractive
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
ENV NODE_VERSION=v20.13.1 \
|
ENV NODE_VERSION=v20.11.1 \
|
||||||
CLOJURE_VERSION=1.11.3.1463 \
|
CLOJURE_VERSION=1.11.1.1435 \
|
||||||
CLJKONDO_VERSION=2024.03.13 \
|
CLJKONDO_VERSION=2024.03.13 \
|
||||||
BABASHKA_VERSION=1.3.189 \
|
BABASHKA_VERSION=1.3.189 \
|
||||||
CLJFMT_VERSION=0.12.0 \
|
CLJFMT_VERSION=0.12.0 \
|
||||||
|
@ -105,12 +105,12 @@ RUN set -eux; \
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
ARCH="$(dpkg --print-architecture)"; \
|
||||||
case "${ARCH}" in \
|
case "${ARCH}" in \
|
||||||
aarch64|arm64) \
|
aarch64|arm64) \
|
||||||
ESUM='7d3ab0e8eba95bd682cfda8041c6cb6fa21e09d0d9131316fd7c96c78969de31'; \
|
ESUM='3ce6a2b357e2ef45fd6b53d6587aa05bfec7771e7fb982f2c964f6b771b7526a'; \
|
||||||
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'; \
|
BINARY_URL='https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jdk_aarch64_linux_hotspot_21.0.2_13.tar.gz'; \
|
||||||
;; \
|
;; \
|
||||||
amd64|x86_64) \
|
amd64|x86_64) \
|
||||||
ESUM='fffa52c22d797b715a962e6c8d11ec7d79b90dd819b5bc51d62137ea4b22a340'; \
|
ESUM='454bebb2c9fe48d981341461ffb6bf1017c7b7c6e15c6b0c29b959194ba3aaa5'; \
|
||||||
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'; \
|
BINARY_URL='https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.2%2B13/OpenJDK21U-jdk_x64_linux_hotspot_21.0.2_13.tar.gz'; \
|
||||||
;; \
|
;; \
|
||||||
*) \
|
*) \
|
||||||
echo "Unsupported arch: ${ARCH}"; \
|
echo "Unsupported arch: ${ARCH}"; \
|
||||||
|
@ -159,6 +159,8 @@ RUN set -eux; \
|
||||||
tar -xf /tmp/nodejs.tar.gz --strip-components=1; \
|
tar -xf /tmp/nodejs.tar.gz --strip-components=1; \
|
||||||
chown -R root /usr/local/nodejs; \
|
chown -R root /usr/local/nodejs; \
|
||||||
corepack enable; \
|
corepack enable; \
|
||||||
|
corepack install -g yarn@4.2.2; \
|
||||||
|
npx playwright install --with-deps chromium; \
|
||||||
rm -rf /tmp/nodejs.tar.gz;
|
rm -rf /tmp/nodejs.tar.gz;
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
version: "3.5"
|
version: "3.8"
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
penpot:
|
penpot:
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC",
|
"author": "Kaleidos INC",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "yarn@4.0.2",
|
"packageManager": "yarn@4.2.2",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/penpot/penpot"
|
"url": "https://github.com/penpot/penpot"
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
|
|
||||||
(def ^:private defaults
|
(def ^:private defaults
|
||||||
{:public-uri "http://localhost:3449"
|
{:public-uri "http://localhost:3449"
|
||||||
:tenant "default"
|
:tenant "dev"
|
||||||
:host "localhost"
|
:host "localhost"
|
||||||
:http-server-port 6061
|
:http-server-port 6061
|
||||||
:http-server-host "0.0.0.0"
|
:http-server-host "0.0.0.0"
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,7 +4,7 @@
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC",
|
"author": "Kaleidos INC",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "yarn@4.0.2",
|
"packageManager": "yarn@4.2.2",
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"defaults"
|
"defaults"
|
||||||
],
|
],
|
||||||
|
@ -29,15 +29,16 @@
|
||||||
"translations:validate": "node ./scripts/validate-translations.js",
|
"translations:validate": "node ./scripts/validate-translations.js",
|
||||||
"translations:find-unused": "node ./scripts/find-unused-translations.js",
|
"translations:find-unused": "node ./scripts/find-unused-translations.js",
|
||||||
"compile": "node ./scripts/compile.js",
|
"compile": "node ./scripts/compile.js",
|
||||||
|
"compile:cljs": "clojure -M:dev:shadow-cljs compile main",
|
||||||
"watch": "node ./scripts/watch.js",
|
"watch": "node ./scripts/watch.js",
|
||||||
"e2e:server": "node ./scripts/e2e-server.js",
|
"e2e:server": "node ./scripts/e2e-server.js",
|
||||||
"e2e:test": "playwright test",
|
"e2e:test": "playwright test --project default",
|
||||||
"storybook:compile": "gulp template:storybook && clojure -M:dev:shadow-cljs compile storybook",
|
"storybook:compile": "yarn run compile && clojure -M:dev:shadow-cljs compile storybook",
|
||||||
"storybook:watch": "npm run storybook:compile && concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"storybook dev -p 6006\"",
|
"storybook:watch": "yarn run storybook:compile && concurrently \"clojure -M:dev:shadow-cljs watch storybook\" \"storybook dev -p 6006\" \"yarn run watch\"",
|
||||||
"storybook:build": "npm run storybook:compile && storybook build"
|
"storybook:build": "yarn run storybook:compile && storybook build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.42.1",
|
"@playwright/test": "1.42.1",
|
||||||
"@storybook/addon-essentials": "^7.6.17",
|
"@storybook/addon-essentials": "^7.6.17",
|
||||||
"@storybook/addon-interactions": "^7.6.17",
|
"@storybook/addon-interactions": "^7.6.17",
|
||||||
"@storybook/addon-links": "^7.6.17",
|
"@storybook/addon-links": "^7.6.17",
|
||||||
|
@ -50,7 +51,7 @@
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.17",
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"draft-js": "git+https://github.com/penpot/draft-js.git",
|
"draft-js": "git+https://github.com/penpot/draft-js.git#commit=4a99b2a6020b2af97f6dc5fa1b4275ec16b559a0",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"fancy-log": "^2.0.0",
|
"fancy-log": "^2.0.0",
|
||||||
"gettext-parser": "^8.0.0",
|
"gettext-parser": "^8.0.0",
|
||||||
|
@ -99,8 +100,8 @@
|
||||||
"opentype.js": "^1.3.4",
|
"opentype.js": "^1.3.4",
|
||||||
"postcss-modules": "^6.0.0",
|
"postcss-modules": "^6.0.0",
|
||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
"react": "^18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"rxjs": "8.0.0-alpha.14",
|
"rxjs": "8.0.0-alpha.14",
|
||||||
"sax": "^1.3.0",
|
"sax": "^1.3.0",
|
||||||
|
|
|
@ -19,6 +19,10 @@ export default defineConfig({
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
/* Opt out of parallel tests by default; can be overriden with --workers */
|
/* Opt out of parallel tests by default; can be overriden with --workers */
|
||||||
workers: 1,
|
workers: 1,
|
||||||
|
/* Timeout for expects (longer in CI) */
|
||||||
|
expect: {
|
||||||
|
timeout: process.env.CI ? 20000 : 5000,
|
||||||
|
},
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: "html",
|
reporter: "html",
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
@ -35,8 +39,17 @@ export default defineConfig({
|
||||||
/* Configure projects for major browsers */
|
/* Configure projects for major browsers */
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: "chromium",
|
name: "default",
|
||||||
use: { ...devices["Desktop Chrome"] },
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
testDir: "./playwright/ui/specs",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ds",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
testDir: "./playwright/ui/visual-specs",
|
||||||
|
expect: {
|
||||||
|
toHaveScreenshot: { maxDiffPixelRatio: 0.01 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||||
this.viewport = page.getByTestId("viewport");
|
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.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" });
|
this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" });
|
||||||
|
this.colorpicker = page.getByTestId("colorpicker");
|
||||||
}
|
}
|
||||||
|
|
||||||
async goToWorkspace() {
|
async goToWorkspace() {
|
||||||
|
|
22
frontend/playwright/ui/specs/colorpicker.spec.js
Normal file
22
frontend/playwright/ui/specs/colorpicker.spec.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await WorkspacePage.init(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fix for https://tree.taiga.io/project/penpot/issue/7549
|
||||||
|
test("Bug 7549 - User clicks on color swatch to display the color picker next to it", async ({ page }) => {
|
||||||
|
const workspacePage = new WorkspacePage(page);
|
||||||
|
await workspacePage.setupEmptyFile(page);
|
||||||
|
|
||||||
|
await workspacePage.goToWorkspace();
|
||||||
|
const swatch = workspacePage.page.getByRole("button", { name: "E8E9EA" });
|
||||||
|
const swatchBox = await swatch.boundingBox();
|
||||||
|
await swatch.click();
|
||||||
|
|
||||||
|
await expect(workspacePage.colorpicker).toBeVisible();
|
||||||
|
const pickerBox = await workspacePage.colorpicker.boundingBox();
|
||||||
|
const distance = swatchBox.x - (pickerBox.x + pickerBox.width);
|
||||||
|
expect(distance).toBeLessThan(60);
|
||||||
|
});
|
10
frontend/playwright/ui/visual-specs/example.spec.js
Normal file
10
frontend/playwright/ui/visual-specs/example.spec.js
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
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();
|
||||||
|
});
|
Binary file not shown.
After Width: | Height: | Size: 152 KiB |
1
frontend/resources/images/icons/external-link.svg
Normal file
1
frontend/resources/images/icons/external-link.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg width="42" xmlns="http://www.w3.org/2000/svg" height="42" stroke-width="3.5"><path d="M35 23.333v14A4.666 4.666 0 0 1 30.333 42H4.667A4.666 4.666 0 0 1 0 37.333V11.667A4.666 4.666 0 0 1 4.667 7h14"/><path d="M28 0h14v14"/><path d="M16.333 25.667 42 0"/></svg>
|
After Width: | Height: | Size: 265 B |
1
frontend/resources/images/icons/rocket.svg
Normal file
1
frontend/resources/images/icons/rocket.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="m10.165 3.902.117 8.274c.018 1.266-1.134 2.005-2.182 2.02-1.102.015-2.218-.59-2.238-1.957l-.116-8.275L7.9.031s1.823 2.992 2.265 3.871Z"/><circle cx="8" cy="5.604" r=".753"/><path d="M6.109 8.064c-3.276 2.163-3.18 4.351-3.18 7.936l3.121-3.934"/><path d="M9.891 8.064c3.276 2.163 3.18 4.351 3.18 7.936L9.95 12.066"/></svg>
|
After Width: | Height: | Size: 393 B |
File diff suppressed because it is too large
Load diff
|
@ -593,6 +593,9 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: $z-index-modal;
|
z-index: $z-index-modal;
|
||||||
background-color: var(--overlay-color);
|
background-color: var(--overlay-color);
|
||||||
|
&.onboarding-a-b-test {
|
||||||
|
background-color: var(--overlay-color-onboarding-a-b-test);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container-base {
|
.modal-container-base {
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
// Dark background
|
// Dark background
|
||||||
--db-primary: #18181a;
|
--db-primary: #18181a;
|
||||||
--db-primary-60: #{color.change(#18181a, $alpha: 0.6)};
|
--db-primary-60: #{color.change(#18181a, $alpha: 0.6)};
|
||||||
|
--db-primary-90: #{color.change(#18181a, $alpha: 0.9)};
|
||||||
--db-secondary: #000000;
|
--db-secondary: #000000;
|
||||||
--db-secondary-30: #{color.change(#000000, $alpha: 0.3)};
|
--db-secondary-30: #{color.change(#000000, $alpha: 0.3)};
|
||||||
--db-secondary-80: #{color.change(#000000, $alpha: 0.8)};
|
--db-secondary-80: #{color.change(#000000, $alpha: 0.8)};
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
// Light background
|
// Light background
|
||||||
--lb-primary: #ffffff;
|
--lb-primary: #ffffff;
|
||||||
--lb-primary-60: #{color.change(#ffffff, $alpha: 0.6)};
|
--lb-primary-60: #{color.change(#ffffff, $alpha: 0.6)};
|
||||||
|
--lb-primary-90: #{color.change(#ffffff, $alpha: 0.9)};
|
||||||
--lb-secondary: #e8eaee;
|
--lb-secondary: #e8eaee;
|
||||||
--lb-secondary-30: #{color.change(#e8eaee, $alpha: 0.3)};
|
--lb-secondary-30: #{color.change(#e8eaee, $alpha: 0.3)};
|
||||||
--lb-secondary-80: #{color.change(#e8eaee, $alpha: 0.8)};
|
--lb-secondary-80: #{color.change(#e8eaee, $alpha: 0.8)};
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
--color-info-foreground: var(--status-color-info-500);
|
--color-info-foreground: var(--status-color-info-500);
|
||||||
|
|
||||||
--overlay-color: var(--db-primary-60);
|
--overlay-color: var(--db-primary-60);
|
||||||
|
--overlay-color-onboarding-a-b-test: var(--db-primary-90);
|
||||||
|
|
||||||
--shadow-color: var(--db-secondary-30);
|
--shadow-color: var(--db-secondary-30);
|
||||||
--radio-button-box-shadow: 0 0 0 1px var(--db-secondary-30) inset;
|
--radio-button-box-shadow: 0 0 0 1px var(--db-secondary-30) inset;
|
||||||
|
|
|
@ -37,6 +37,8 @@
|
||||||
--color-info-foreground: var(--status-color-info-500);
|
--color-info-foreground: var(--status-color-info-500);
|
||||||
|
|
||||||
--overlay-color: var(--lb-primary-60);
|
--overlay-color: var(--lb-primary-60);
|
||||||
|
--overlay-color-onboarding-a-b-test: var(--lb-primary-90);
|
||||||
|
|
||||||
--shadow-color: var(--lf-secondary-40);
|
--shadow-color: var(--lf-secondary-40);
|
||||||
--radio-button-box-shadow: 0 0 0 1px var(--lb-secondary) inset;
|
--radio-button-box-shadow: 0 0 0 1px var(--lb-secondary) inset;
|
||||||
|
|
||||||
|
|
|
@ -130,6 +130,10 @@
|
||||||
(def worker-uri
|
(def worker-uri
|
||||||
(obj/get global "penpotWorkerURI" "/js/worker.js"))
|
(obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||||
|
|
||||||
|
(defn external-feature-flag [flag value]
|
||||||
|
(when-let [fn (obj/get global "externalFeatureFlag")]
|
||||||
|
(fn flag value)))
|
||||||
|
|
||||||
;; --- Helper Functions
|
;; --- Helper Functions
|
||||||
|
|
||||||
(defn ^boolean check-browser? [candidate]
|
(defn ^boolean check-browser? [candidate]
|
||||||
|
|
|
@ -249,9 +249,18 @@
|
||||||
(deleteObject [_ id]
|
(deleteObject [_ id]
|
||||||
(set! file (fb/delete-object file (uuid/uuid id))))
|
(set! file (fb/delete-object file (uuid/uuid id))))
|
||||||
|
|
||||||
|
(getId [_]
|
||||||
|
(:id file))
|
||||||
|
|
||||||
|
(getCurrentPageId [_]
|
||||||
|
(:current-page-id file))
|
||||||
|
|
||||||
(asMap [_]
|
(asMap [_]
|
||||||
(clj->js file))
|
(clj->js file))
|
||||||
|
|
||||||
|
(newId [_]
|
||||||
|
(uuid/next))
|
||||||
|
|
||||||
(export [_]
|
(export [_]
|
||||||
(->> (export-file file)
|
(->> (export-file file)
|
||||||
(rx/subs!
|
(rx/subs!
|
||||||
|
@ -261,7 +270,8 @@
|
||||||
(dom/trigger-download (:name file) export-blob))))))))
|
(dom/trigger-download (:name file) export-blob))))))))
|
||||||
|
|
||||||
(defn create-file-export [^string name]
|
(defn create-file-export [^string name]
|
||||||
(File. (fb/create-file name)))
|
(binding [cfeat/*current* cfeat/default-features]
|
||||||
|
(File. (fb/create-file name))))
|
||||||
|
|
||||||
(defn exports []
|
(defn exports []
|
||||||
#js {:createFile create-file-export})
|
#js {:createFile create-file-export})
|
||||||
|
|
|
@ -343,9 +343,9 @@
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [on-success (:on-success opts identity)
|
(let [on-success (:on-success opts identity)
|
||||||
on-error (:on-error opts rx/throw)
|
on-error (:on-error opts rx/throw)
|
||||||
profile (:profile state)]
|
profile (:profile state)
|
||||||
|
params (select-keys profile [:fullname :lang :theme])]
|
||||||
(->> (rp/cmd! :update-profile (dissoc profile :props))
|
(->> (rp/cmd! :update-profile params)
|
||||||
(rx/tap on-success)
|
(rx/tap on-success)
|
||||||
(rx/catch on-error))))))
|
(rx/catch on-error))))))
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,14 @@
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? :layout/update))
|
(rx/filter (ptk/type? :layout/update))
|
||||||
(rx/map deref)
|
(rx/map deref)
|
||||||
(rx/map #(update-layout-positions %))
|
;; We buffer the updates to the layout so if there are many changes at the same time
|
||||||
|
;; they are process together. It will get a better performance.
|
||||||
|
(rx/buffer-time 100)
|
||||||
|
(rx/filter #(d/not-empty? %))
|
||||||
|
(rx/map
|
||||||
|
(fn [data]
|
||||||
|
(let [ids (reduce #(into %1 (:ids %2)) #{} data)]
|
||||||
|
(update-layout-positions {:ids ids}))))
|
||||||
(rx/take-until stopper))))))
|
(rx/take-until stopper))))))
|
||||||
|
|
||||||
(defn finalize
|
(defn finalize
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
:is-transparent (and opacity (> 1 opacity))
|
:is-transparent (and opacity (> 1 opacity))
|
||||||
:grid-area area
|
:grid-area area
|
||||||
:read-only read-only?)
|
:read-only read-only?)
|
||||||
|
:role "button"
|
||||||
:data-readonly (str read-only?)
|
:data-readonly (str read-only?)
|
||||||
:on-click on-click
|
:on-click on-click
|
||||||
:title (color-title color)}
|
:title (color-title color)}
|
||||||
|
|
|
@ -144,6 +144,7 @@
|
||||||
(def ^:icon img (icon-xref :img))
|
(def ^:icon img (icon-xref :img))
|
||||||
(def ^:icon interaction (icon-xref :interaction))
|
(def ^:icon interaction (icon-xref :interaction))
|
||||||
(def ^:icon join-nodes (icon-xref :join-nodes))
|
(def ^:icon join-nodes (icon-xref :join-nodes))
|
||||||
|
(def ^:icon external-link (icon-xref :external-link))
|
||||||
(def ^:icon justify-content-column-around (icon-xref :justify-content-column-around))
|
(def ^:icon justify-content-column-around (icon-xref :justify-content-column-around))
|
||||||
(def ^:icon justify-content-column-between (icon-xref :justify-content-column-between))
|
(def ^:icon justify-content-column-between (icon-xref :justify-content-column-between))
|
||||||
(def ^:icon justify-content-column-center (icon-xref :justify-content-column-center))
|
(def ^:icon justify-content-column-center (icon-xref :justify-content-column-center))
|
||||||
|
@ -160,13 +161,13 @@
|
||||||
(def ^:icon library (icon-xref :library))
|
(def ^:icon library (icon-xref :library))
|
||||||
(def ^:icon locate (icon-xref :locate))
|
(def ^:icon locate (icon-xref :locate))
|
||||||
(def ^:icon lock (icon-xref :lock))
|
(def ^:icon lock (icon-xref :lock))
|
||||||
|
(def ^:icon margin (icon-xref :margin))
|
||||||
(def ^:icon margin-bottom (icon-xref :margin-bottom))
|
(def ^:icon margin-bottom (icon-xref :margin-bottom))
|
||||||
(def ^:icon margin-left (icon-xref :margin-left))
|
(def ^:icon margin-left (icon-xref :margin-left))
|
||||||
(def ^:icon margin-left-right (icon-xref :margin-left-right))
|
(def ^:icon margin-left-right (icon-xref :margin-left-right))
|
||||||
(def ^:icon margin-right (icon-xref :margin-right))
|
(def ^:icon margin-right (icon-xref :margin-right))
|
||||||
(def ^:icon margin-top-bottom (icon-xref :margin-top-bottom))
|
|
||||||
(def ^:icon margin-top (icon-xref :margin-top))
|
(def ^:icon margin-top (icon-xref :margin-top))
|
||||||
(def ^:icon margin (icon-xref :margin))
|
(def ^:icon margin-top-bottom (icon-xref :margin-top-bottom))
|
||||||
(def ^:icon mask (icon-xref :mask))
|
(def ^:icon mask (icon-xref :mask))
|
||||||
(def ^:icon masked (icon-xref :masked))
|
(def ^:icon masked (icon-xref :masked))
|
||||||
(def ^:icon menu (icon-xref :menu))
|
(def ^:icon menu (icon-xref :menu))
|
||||||
|
@ -179,11 +180,11 @@
|
||||||
(def ^:icon open-link (icon-xref :open-link))
|
(def ^:icon open-link (icon-xref :open-link))
|
||||||
(def ^:icon padding-bottom (icon-xref :padding-bottom))
|
(def ^:icon padding-bottom (icon-xref :padding-bottom))
|
||||||
(def ^:icon padding-extended (icon-xref :padding-extended))
|
(def ^:icon padding-extended (icon-xref :padding-extended))
|
||||||
(def ^:icon padding-left-right (icon-xref :padding-left-right))
|
|
||||||
(def ^:icon padding-left (icon-xref :padding-left))
|
(def ^:icon padding-left (icon-xref :padding-left))
|
||||||
|
(def ^:icon padding-left-right (icon-xref :padding-left-right))
|
||||||
(def ^:icon padding-right (icon-xref :padding-right))
|
(def ^:icon padding-right (icon-xref :padding-right))
|
||||||
(def ^:icon padding-top-bottom (icon-xref :padding-top-bottom))
|
|
||||||
(def ^:icon padding-top (icon-xref :padding-top))
|
(def ^:icon padding-top (icon-xref :padding-top))
|
||||||
|
(def ^:icon padding-top-bottom (icon-xref :padding-top-bottom))
|
||||||
(def ^:icon path (icon-xref :path))
|
(def ^:icon path (icon-xref :path))
|
||||||
(def ^:icon pentool (icon-xref :pentool))
|
(def ^:icon pentool (icon-xref :pentool))
|
||||||
(def ^:icon picker (icon-xref :picker))
|
(def ^:icon picker (icon-xref :picker))
|
||||||
|
@ -192,11 +193,12 @@
|
||||||
(def ^:icon rectangle (icon-xref :rectangle))
|
(def ^:icon rectangle (icon-xref :rectangle))
|
||||||
(def ^:icon reload (icon-xref :reload))
|
(def ^:icon reload (icon-xref :reload))
|
||||||
(def ^:icon remove-icon (icon-xref :remove))
|
(def ^:icon remove-icon (icon-xref :remove))
|
||||||
(def ^:icon rgba-complementary (icon-xref :rgba-complementary))
|
|
||||||
(def ^:icon rgba (icon-xref :rgba))
|
(def ^:icon rgba (icon-xref :rgba))
|
||||||
|
(def ^:icon rgba-complementary (icon-xref :rgba-complementary))
|
||||||
|
(def ^:icon rocket (icon-xref :rocket))
|
||||||
(def ^:icon rotation (icon-xref :rotation))
|
(def ^:icon rotation (icon-xref :rotation))
|
||||||
(def ^:icon row-reverse (icon-xref :row-reverse))
|
|
||||||
(def ^:icon row (icon-xref :row))
|
(def ^:icon row (icon-xref :row))
|
||||||
|
(def ^:icon row-reverse (icon-xref :row-reverse))
|
||||||
(def ^:icon search (icon-xref :search))
|
(def ^:icon search (icon-xref :search))
|
||||||
(def ^:icon separate-nodes (icon-xref :separate-nodes))
|
(def ^:icon separate-nodes (icon-xref :separate-nodes))
|
||||||
(def ^:icon shown (icon-xref :shown))
|
(def ^:icon shown (icon-xref :shown))
|
||||||
|
@ -218,6 +220,7 @@
|
||||||
(def ^:icon svg (icon-xref :svg))
|
(def ^:icon svg (icon-xref :svg))
|
||||||
(def ^:icon swatches (icon-xref :swatches))
|
(def ^:icon swatches (icon-xref :swatches))
|
||||||
(def ^:icon switch (icon-xref :switch))
|
(def ^:icon switch (icon-xref :switch))
|
||||||
|
(def ^:icon text (icon-xref :text))
|
||||||
(def ^:icon text-align-center (icon-xref :text-align-center))
|
(def ^:icon text-align-center (icon-xref :text-align-center))
|
||||||
(def ^:icon text-align-left (icon-xref :text-align-left))
|
(def ^:icon text-align-left (icon-xref :text-align-left))
|
||||||
(def ^:icon text-align-right (icon-xref :text-align-right))
|
(def ^:icon text-align-right (icon-xref :text-align-right))
|
||||||
|
@ -239,7 +242,6 @@
|
||||||
(def ^:icon text-top (icon-xref :text-top))
|
(def ^:icon text-top (icon-xref :text-top))
|
||||||
(def ^:icon text-underlined (icon-xref :text-underlined))
|
(def ^:icon text-underlined (icon-xref :text-underlined))
|
||||||
(def ^:icon text-uppercase (icon-xref :text-uppercase))
|
(def ^:icon text-uppercase (icon-xref :text-uppercase))
|
||||||
(def ^:icon text (icon-xref :text))
|
|
||||||
(def ^:icon thumbnail (icon-xref :thumbnail))
|
(def ^:icon thumbnail (icon-xref :thumbnail))
|
||||||
(def ^:icon tick (icon-xref :tick))
|
(def ^:icon tick (icon-xref :tick))
|
||||||
(def ^:icon to-corner (icon-xref :to-corner))
|
(def ^:icon to-corner (icon-xref :to-corner))
|
||||||
|
@ -258,7 +260,6 @@
|
||||||
(def ^:icon view-as-list (icon-xref :view-as-list))
|
(def ^:icon view-as-list (icon-xref :view-as-list))
|
||||||
(def ^:icon wrap (icon-xref :wrap))
|
(def ^:icon wrap (icon-xref :wrap))
|
||||||
|
|
||||||
|
|
||||||
(def ^:icon loader-pencil
|
(def ^:icon loader-pencil
|
||||||
(mf/html
|
(mf/html
|
||||||
[:svg
|
[:svg
|
||||||
|
|
|
@ -142,7 +142,9 @@
|
||||||
(modal/show! {:type :onboarding-newsletter})
|
(modal/show! {:type :onboarding-newsletter})
|
||||||
|
|
||||||
(contains? cf/flags :onboarding-team)
|
(contains? cf/flags :onboarding-team)
|
||||||
(modal/show! {:type :onboarding-team}))))]
|
(modal/show! {:type :onboarding-team}))))
|
||||||
|
|
||||||
|
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||||
|
|
||||||
(mf/with-effect [@slide]
|
(mf/with-effect [@slide]
|
||||||
(when (not= :start @slide)
|
(when (not= :start @slide)
|
||||||
|
@ -151,8 +153,8 @@
|
||||||
(fn []
|
(fn []
|
||||||
(reset! klass nil)
|
(reset! klass nil)
|
||||||
(tm/dispose! sem))))
|
(tm/dispose! sem))))
|
||||||
|
[:div {:class (stl/css-case :modal-overlay true
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||||
[:div.animated {:class (dm/str @klass " " (stl/css :animated))}
|
[:div.animated {:class (dm/str @klass " " (stl/css :animated))}
|
||||||
(case @slide
|
(case @slide
|
||||||
:start [:& onboarding-welcome {:next #(navigate :opensource)}]
|
:start [:& onboarding-welcome {:next #(navigate :opensource)}]
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.main.ui.onboarding.newsletter
|
(ns app.main.ui.onboarding.newsletter
|
||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
|
[app.config :as cf]
|
||||||
[app.main.data.messages :as msg]
|
[app.main.data.messages :as msg]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
[app.main.data.users :as du]
|
[app.main.data.users :as du]
|
||||||
|
@ -35,9 +36,11 @@
|
||||||
(st/emit! (when (or @newsletter-updates @newsletter-news)
|
(st/emit! (when (or @newsletter-updates @newsletter-news)
|
||||||
(msg/success message))
|
(msg/success message))
|
||||||
(modal/show {:type :onboarding-team})
|
(modal/show {:type :onboarding-team})
|
||||||
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))]
|
(du/update-profile-props {:newsletter-updates @newsletter-updates :newsletter-news @newsletter-news}))))
|
||||||
|
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css-case :modal-overlay true
|
||||||
|
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||||
[:div.animated.fadeInDown {:class (stl/css :modal-container)}
|
[:div.animated.fadeInDown {:class (stl/css :modal-container)}
|
||||||
[:div {:class (stl/css :modal-left)}
|
[:div {:class (stl/css :modal-left)}
|
||||||
[:img {:src "images/deco-newsletter.png"
|
[:img {:src "images/deco-newsletter.png"
|
||||||
|
|
|
@ -287,9 +287,11 @@
|
||||||
(modal/show! {:type :onboarding-team})
|
(modal/show! {:type :onboarding-team})
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(modal/hide!)))))]
|
(modal/hide!)))))
|
||||||
|
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css-case :modal-overlay true
|
||||||
|
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||||
[:div {:class (stl/css :modal-container)
|
[:div {:class (stl/css :modal-container)
|
||||||
:ref container}
|
:ref container}
|
||||||
(case @step
|
(case @step
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dmc]
|
[app.common.data.macros :as dmc]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
|
[app.config :as cf]
|
||||||
[app.main.data.dashboard :as dd]
|
[app.main.data.dashboard :as dd]
|
||||||
[app.main.data.events :as ev]
|
[app.main.data.events :as ev]
|
||||||
[app.main.data.messages :as msg]
|
[app.main.data.messages :as msg]
|
||||||
|
@ -84,14 +85,16 @@
|
||||||
::ev/origin "onboarding"
|
::ev/origin "onboarding"
|
||||||
:step 1}))))
|
:step 1}))))
|
||||||
|
|
||||||
teams (mf/deref refs/teams)]
|
teams (mf/deref refs/teams)
|
||||||
|
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||||
|
|
||||||
(mf/with-effect [teams]
|
(mf/with-effect [teams]
|
||||||
(when (> (count teams) 1)
|
(when (> (count teams) 1)
|
||||||
(st/emit! (modal/hide))))
|
(st/emit! (modal/hide))))
|
||||||
|
|
||||||
(when (< (count teams) 2)
|
(when (< (count teams) 2)
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css-case :modal-overlay true
|
||||||
|
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||||
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
||||||
[:& team-modal-left]
|
[:& team-modal-left]
|
||||||
[:div {:class (stl/css :separator)}]
|
[:div {:class (stl/css :separator)}]
|
||||||
|
@ -212,9 +215,11 @@
|
||||||
(if (> (count emails) 0)
|
(if (> (count emails) 0)
|
||||||
(on-invite-now form)
|
(on-invite-now form)
|
||||||
(on-invite-later form))
|
(on-invite-later form))
|
||||||
(modal/hide!))))]
|
(modal/hide!))))
|
||||||
|
onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css-case :modal-overlay true
|
||||||
|
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||||
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
[:div.animated.fadeIn {:class (stl/css :modal-container)}
|
||||||
[:& team-modal-left]
|
[:& team-modal-left]
|
||||||
|
|
||||||
|
|
|
@ -8,196 +8,203 @@
|
||||||
(:require-macros [app.main.style :as stl])
|
(:require-macros [app.main.style :as stl])
|
||||||
(:require
|
(:require
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.config :as cf]
|
||||||
[app.main.ui.releases.common :as c]
|
[app.main.ui.releases.common :as c]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
;; TODO: Review all copies and alt text
|
;; TODO: Review all copies and alt text
|
||||||
(defmethod c/render-release-notes "2.0"
|
(defmethod c/render-release-notes "2.0"
|
||||||
[{:keys [slide klass next finish navigate version]}]
|
[{:keys [slide klass next finish navigate version]}]
|
||||||
(mf/html
|
(let [onboarding-a-b-test? (cf/external-feature-flag "signup-background" "test")]
|
||||||
(case slide
|
(mf/html
|
||||||
:start
|
(case slide
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
:start
|
||||||
[:div.animated {:class klass}
|
[:div {:class (stl/css-case :modal-overlay true
|
||||||
[:div {:class (stl/css :modal-container)}
|
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||||
[:img {:src "images/features/2.0-intro-image.png"
|
[:div.animated {:class klass}
|
||||||
:class (stl/css :start-image)
|
[:div {:class (stl/css :modal-container)}
|
||||||
:border "0"
|
[:img {:src "images/features/2.0-intro-image.png"
|
||||||
:alt "A graphic illustration with Penpot style"}]
|
:class (stl/css :start-image)
|
||||||
|
:border "0"
|
||||||
|
:alt "A graphic illustration with Penpot style"}]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
[:div {:class (stl/css :modal-header)}
|
[:div {:class (stl/css :modal-header)}
|
||||||
[:h1 {:class (stl/css :modal-title)}
|
[:h1 {:class (stl/css :modal-title)}
|
||||||
"Welcome to Penpot 2.0! "]
|
"Welcome to Penpot 2.0! "]
|
||||||
|
|
||||||
[:div {:class (stl/css :version-tag)}
|
[:div {:class (stl/css :version-tag)}
|
||||||
(dm/str "Version " version)]]
|
(dm/str "Version " version)]]
|
||||||
|
|
||||||
[:div {:class (stl/css :features-block)}
|
[:div {:class (stl/css :features-block)}
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
[:spam {:class (stl/css :feature-title)}
|
[:spam {:class (stl/css :feature-title)}
|
||||||
"CSS Grid Layout: "]
|
"CSS Grid Layout: "]
|
||||||
"Bring your designs to life, knowing that what you create is what developers code."]
|
"Bring your designs to life, knowing that what you create is what developers code."]
|
||||||
|
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
[:spam {:class (stl/css :feature-title)}
|
[:spam {:class (stl/css :feature-title)}
|
||||||
"Sleeker UI: "]
|
"Sleeker UI: "]
|
||||||
"We’ve polished Penpot to make your experience smoother and more enjoyable."]
|
"We’ve polished Penpot to make your experience smoother and more enjoyable."]
|
||||||
|
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
[:spam {:class (stl/css :feature-title)}
|
[:spam {:class (stl/css :feature-title)}
|
||||||
"New Components System: "]
|
"New Components System: "]
|
||||||
"Managing and using your design components got a whole lot better."]
|
"Managing and using your design components got a whole lot better."]
|
||||||
|
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
"And that’s not all - we’ve fined tuned performance and "
|
"And that’s not all - we’ve fined tuned performance and "
|
||||||
"accessibility to give you a better and more fluid design experience."]
|
"accessibility to give you a better and more fluid design experience."]
|
||||||
|
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
" Ready to dive in? Let 's get started!"]]
|
" Ready to dive in? Let 's get started!"]]
|
||||||
|
|
||||||
[:div {:class (stl/css :navigation)}
|
[:div {:class (stl/css :navigation)}
|
||||||
[:button {:class (stl/css :next-btn)
|
[:button {:class (stl/css :next-btn)
|
||||||
:on-click next} "Continue"]]]]]]
|
:on-click next} "Continue"]]]]]]
|
||||||
|
|
||||||
0
|
0
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css-case :modal-overlay true
|
||||||
[:div.animated {:class klass}
|
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||||
[:div {:class (stl/css :modal-container)}
|
[:div.animated {:class klass}
|
||||||
[:img {:src "images/features/2.0-css-grid.gif"
|
[:div {:class (stl/css :modal-container)}
|
||||||
:class (stl/css :start-image)
|
[:img {:src "images/features/2.0-css-grid.gif"
|
||||||
:border "0"
|
:class (stl/css :start-image)
|
||||||
:alt "Penpot's CSS Grid Layout"}]
|
:border "0"
|
||||||
|
:alt "Penpot's CSS Grid Layout"}]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
[:div {:class (stl/css :modal-header)}
|
[:div {:class (stl/css :modal-header)}
|
||||||
[:h1 {:class (stl/css :modal-title)}
|
[:h1 {:class (stl/css :modal-title)}
|
||||||
"CSS Grid Layout - Design Meets Development"]]
|
"CSS Grid Layout - Design Meets Development"]]
|
||||||
|
|
||||||
[:div {:class (stl/css :feature)}
|
[:div {:class (stl/css :feature)}
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
"The much-awaited Grid Layout introduces 2-dimensional"
|
"The much-awaited Grid Layout introduces 2-dimensional"
|
||||||
" layout capabilities to Penpot, allowing for the creation"
|
" layout capabilities to Penpot, allowing for the creation"
|
||||||
" of adaptive layouts by leveraging the power of CSS properties."]
|
" of adaptive layouts by leveraging the power of CSS properties."]
|
||||||
|
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
"It’s a host of new features, including columns and"
|
"It’s a host of new features, including columns and"
|
||||||
" rows management, flexible units such as FR (fractions),"
|
" rows management, flexible units such as FR (fractions),"
|
||||||
" the ability to create and name areas, and tons of new "
|
" the ability to create and name areas, and tons of new "
|
||||||
"and unique possibilities within a design tool."]
|
"and unique possibilities within a design tool."]
|
||||||
|
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
"Designers will learn CSS basics while working, "
|
"Designers will learn CSS basics while working, "
|
||||||
"and as always with Penpot, developers can pick"
|
"and as always with Penpot, developers can pick"
|
||||||
" up the design as code to take it from there."]]
|
" up the design as code to take it from there."]]
|
||||||
|
|
||||||
[:div {:class (stl/css :navigation)}
|
[:div {:class (stl/css :navigation)}
|
||||||
[:& c/navigation-bullets
|
[:& c/navigation-bullets
|
||||||
{:slide slide
|
{:slide slide
|
||||||
:navigate navigate
|
:navigate navigate
|
||||||
:total 4}]
|
:total 4}]
|
||||||
|
|
||||||
[:button {:on-click next
|
[:button {:on-click next
|
||||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||||
|
|
||||||
1
|
1
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css-case :modal-overlay true
|
||||||
[:div.animated {:class klass}
|
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||||
[:div {:class (stl/css :modal-container)}
|
[:div.animated {:class klass}
|
||||||
[:img {:src "images/features/2.0-new-ui.gif"
|
[:div {:class (stl/css :modal-container)}
|
||||||
:class (stl/css :start-image)
|
[:img {:src "images/features/2.0-new-ui.gif"
|
||||||
:border "0"
|
:class (stl/css :start-image)
|
||||||
:alt "Penpot's UI Makeover"}]
|
:border "0"
|
||||||
|
:alt "Penpot's UI Makeover"}]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
[:div {:class (stl/css :modal-header)}
|
[:div {:class (stl/css :modal-header)}
|
||||||
[:h1 {:class (stl/css :modal-title)}
|
[:h1 {:class (stl/css :modal-title)}
|
||||||
"UI Makeover - Smoother, Sharper, and Simply More Fun"]]
|
"UI Makeover - Smoother, Sharper, and Simply More Fun"]]
|
||||||
|
|
||||||
[:div {:class (stl/css :feature)}
|
[:div {:class (stl/css :feature)}
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
"We've completely overhauled Penpot's user interface. "
|
"We've completely overhauled Penpot's user interface. "
|
||||||
"The improvements in consistency, the introduction of "
|
"The improvements in consistency, the introduction of "
|
||||||
"new microinteractions, and attention to countless details"
|
"new microinteractions, and attention to countless details"
|
||||||
" will significantly enhance the productivity and enjoyment of using Penpot."]
|
" will significantly enhance the productivity and enjoyment of using Penpot."]
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
"Furthermore, we’ve made several accessibility improvements, "
|
"Furthermore, we’ve made several accessibility improvements, "
|
||||||
"with better color contrast, keyboard navigation,"
|
"with better color contrast, keyboard navigation,"
|
||||||
" and adherence to other best practices."]]
|
" and adherence to other best practices."]]
|
||||||
|
|
||||||
[:div {:class (stl/css :navigation)}
|
[:div {:class (stl/css :navigation)}
|
||||||
[:& c/navigation-bullets
|
[:& c/navigation-bullets
|
||||||
{:slide slide
|
{:slide slide
|
||||||
:navigate navigate
|
:navigate navigate
|
||||||
:total 4}]
|
:total 4}]
|
||||||
|
|
||||||
[:button {:on-click next
|
[:button {:on-click next
|
||||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||||
|
|
||||||
2
|
2
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css-case :modal-overlay true
|
||||||
[:div.animated {:class klass}
|
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||||
[:div {:class (stl/css :modal-container)}
|
[:div.animated {:class klass}
|
||||||
[:img {:src "images/features/2.0-components.gif"
|
[:div {:class (stl/css :modal-container)}
|
||||||
:class (stl/css :start-image)
|
[:img {:src "images/features/2.0-components.gif"
|
||||||
:border "0"
|
:class (stl/css :start-image)
|
||||||
:alt "Penpot's new components system"}]
|
:border "0"
|
||||||
|
:alt "Penpot's new components system"}]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
[:div {:class (stl/css :modal-header)}
|
[:div {:class (stl/css :modal-header)}
|
||||||
[:h1 {:class (stl/css :modal-title)}
|
[:h1 {:class (stl/css :modal-title)}
|
||||||
"New Components System"]]
|
"New Components System"]]
|
||||||
[:div {:class (stl/css :feature)}
|
[:div {:class (stl/css :feature)}
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
"The new Penpot components system improves"
|
"The new Penpot components system improves"
|
||||||
" control over instances, including their "
|
" control over instances, including their "
|
||||||
"inheritances and properties overrides. "
|
"inheritances and properties overrides. "
|
||||||
"Main components are now accessible as design"
|
"Main components are now accessible as design"
|
||||||
" elements, allowing a better updating "
|
" elements, allowing a better updating "
|
||||||
"workflow through instant changes synchronization."]
|
"workflow through instant changes synchronization."]
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
"And that’s not all, there are new capabilities "
|
"And that’s not all, there are new capabilities "
|
||||||
"such as component swapping and annotations "
|
"such as component swapping and annotations "
|
||||||
"that will help you to better manage your design systems."]]
|
"that will help you to better manage your design systems."]]
|
||||||
|
|
||||||
[:div {:class (stl/css :navigation)}
|
[:div {:class (stl/css :navigation)}
|
||||||
[:& c/navigation-bullets
|
[:& c/navigation-bullets
|
||||||
{:slide slide
|
{:slide slide
|
||||||
:navigate navigate
|
:navigate navigate
|
||||||
:total 4}]
|
:total 4}]
|
||||||
|
|
||||||
[:button {:on-click next
|
[:button {:on-click next
|
||||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||||
|
|
||||||
3
|
3
|
||||||
[:div {:class (stl/css :modal-overlay)}
|
[:div {:class (stl/css-case :modal-overlay true
|
||||||
[:div.animated {:class klass}
|
:onboarding-a-b-test onboarding-a-b-test?)}
|
||||||
[:div {:class (stl/css :modal-container)}
|
[:div.animated {:class klass}
|
||||||
[:img {:src "images/features/2.0-html.gif"
|
[:div {:class (stl/css :modal-container)}
|
||||||
:class (stl/css :start-image)
|
[:img {:src "images/features/2.0-html.gif"
|
||||||
:border "0"
|
:class (stl/css :start-image)
|
||||||
:alt " Penpot's HTML code generator"}]
|
:border "0"
|
||||||
|
:alt " Penpot's HTML code generator"}]
|
||||||
|
|
||||||
[:div {:class (stl/css :modal-content)}
|
[:div {:class (stl/css :modal-content)}
|
||||||
[:div {:class (stl/css :modal-header)}
|
[:div {:class (stl/css :modal-header)}
|
||||||
[:h1 {:class (stl/css :modal-title)}
|
[:h1 {:class (stl/css :modal-title)}
|
||||||
"And much more"]]
|
"And much more"]]
|
||||||
[:div {:class (stl/css :feature)}
|
[:div {:class (stl/css :feature)}
|
||||||
[:p {:class (stl/css :feature-content)}
|
[:p {:class (stl/css :feature-content)}
|
||||||
"In addition to all of this, we’ve included several other requested improvements:"]
|
"In addition to all of this, we’ve included several other requested improvements:"]
|
||||||
[:ul {:class (stl/css :feature-list)}
|
[:ul {:class (stl/css :feature-list)}
|
||||||
[:li "Access HTML markup code directly in inspect mode"]
|
[:li "Access HTML markup code directly in inspect mode"]
|
||||||
[:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"]
|
[:li "Images are now treated as element fills, maintaining their aspect ratio on resize, ideal for flexible designs"]
|
||||||
[:li "Enjoy new color themes with options for both dark and light modes"]
|
[:li "Enjoy new color themes with options for both dark and light modes"]
|
||||||
[:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]]
|
[:li "Feel the speed boost! Enjoy a smoother experience with a bunch of performance improvements"]]]
|
||||||
|
|
||||||
[:div {:class (stl/css :navigation)}
|
[:div {:class (stl/css :navigation)}
|
||||||
|
|
||||||
[:& c/navigation-bullets
|
[:& c/navigation-bullets
|
||||||
{:slide slide
|
{:slide slide
|
||||||
:navigate navigate
|
:navigate navigate
|
||||||
:total 4}]
|
:total 4}]
|
||||||
|
|
||||||
[:button {:on-click finish
|
[:button {:on-click finish
|
||||||
:class (stl/css :next-btn)} "Let's go"]]]]]])))
|
:class (stl/css :next-btn)} "Let's go"]]]]]]))))
|
||||||
|
|
||||||
|
|
|
@ -372,15 +372,14 @@
|
||||||
(defn calculate-position
|
(defn calculate-position
|
||||||
"Calculates the style properties for the given coordinates and position"
|
"Calculates the style properties for the given coordinates and position"
|
||||||
[{vh :height} position x y]
|
[{vh :height} position x y]
|
||||||
(let [;; picker height in pixels
|
(let [;; picker size in pixels
|
||||||
h 510
|
h 510
|
||||||
|
w 284
|
||||||
;; Checks for overflow outside the viewport height
|
;; Checks for overflow outside the viewport height
|
||||||
max-y (- vh h)
|
max-y (- vh h)
|
||||||
rulers? (mf/deref refs/rulers?)
|
rulers? (mf/deref refs/rulers?)
|
||||||
left-offset (if rulers? 40 18)
|
left-offset (if rulers? 40 18)
|
||||||
|
right-offset (+ w 40)]
|
||||||
x-pos 400]
|
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
(or (nil? x) (nil? y))
|
(or (nil? x) (nil? y))
|
||||||
|
@ -388,9 +387,9 @@
|
||||||
|
|
||||||
(= position :left)
|
(= position :left)
|
||||||
(if (> y max-y)
|
(if (> y max-y)
|
||||||
#js {:left (dm/str (- x x-pos) "px")
|
#js {:left (dm/str (- x right-offset) "px")
|
||||||
:bottom "1rem"}
|
:bottom "1rem"}
|
||||||
#js {:left (dm/str (- x x-pos) "px")
|
#js {:left (dm/str (- x right-offset) "px")
|
||||||
:top (dm/str (- y 70) "px")})
|
:top (dm/str (- y 70) "px")})
|
||||||
|
|
||||||
(= position :right)
|
(= position :right)
|
||||||
|
@ -440,6 +439,7 @@
|
||||||
(on-close @last-change)))
|
(on-close @last-change)))
|
||||||
|
|
||||||
[:div {:class (stl/css :colorpicker-tooltip)
|
[:div {:class (stl/css :colorpicker-tooltip)
|
||||||
|
:data-testid "colorpicker"
|
||||||
:style style}
|
:style style}
|
||||||
|
|
||||||
[:& colorpicker {:data data
|
[:& colorpicker {:data data
|
||||||
|
|
|
@ -629,13 +629,13 @@
|
||||||
(when (d/not-empty? plugins)
|
(when (d/not-empty? plugins)
|
||||||
[:div {:class (stl/css :separator)}])
|
[:div {:class (stl/css :separator)}])
|
||||||
|
|
||||||
(for [[idx {:keys [name url]}] (d/enumerate plugins)]
|
(for [[idx {:keys [name] :as manifest}] (d/enumerate plugins)]
|
||||||
[:> dropdown-menu-item* {:key (dm/str "plugins-menu-" idx)
|
[:> dropdown-menu-item* {:key (dm/str "plugins-menu-" idx)
|
||||||
:on-click #(uwp/open-plugin! url)
|
:on-click #(uwp/open-plugin! manifest)
|
||||||
:class (stl/css :submenu-item)
|
:class (stl/css :submenu-item)
|
||||||
:on-key-down (fn [event]
|
:on-key-down (fn [event]
|
||||||
(when (kbd/enter? event)
|
(when (kbd/enter? event)
|
||||||
#(uwp/open-plugin! url)))}
|
#(uwp/open-plugin! manifest)))}
|
||||||
[:span {:class (stl/css :item-name)} name]])])))
|
[:span {:class (stl/css :item-name)} name]])])))
|
||||||
|
|
||||||
(mf/defc menu
|
(mf/defc menu
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
[app.main.ui.components.search-bar :refer [search-bar]]
|
[app.main.ui.components.search-bar :refer [search-bar]]
|
||||||
[app.main.ui.components.title-bar :refer [title-bar]]
|
[app.main.ui.components.title-bar :refer [title-bar]]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
|
[app.util.avatars :as avatars]
|
||||||
[app.util.http :as http]
|
[app.util.http :as http]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
|
@ -24,26 +25,30 @@
|
||||||
|
|
||||||
|
|
||||||
(mf/defc plugin-entry
|
(mf/defc plugin-entry
|
||||||
[{:keys [index _icon url name description on-open-plugin on-remove-plugin]}]
|
[{:keys [index manifest on-open-plugin on-remove-plugin]}]
|
||||||
|
|
||||||
(let [handle-open-click
|
(let [{:keys [host icon name description]} manifest
|
||||||
|
handle-open-click
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps index url on-open-plugin)
|
(mf/deps index manifest on-open-plugin)
|
||||||
(fn []
|
(fn []
|
||||||
(when on-open-plugin
|
(when on-open-plugin
|
||||||
(on-open-plugin index url))))
|
(on-open-plugin manifest))))
|
||||||
|
|
||||||
handle-delete-click
|
handle-delete-click
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps index url on-remove-plugin)
|
(mf/deps index on-remove-plugin)
|
||||||
(fn []
|
(fn []
|
||||||
(when on-remove-plugin
|
(when on-remove-plugin
|
||||||
(on-remove-plugin index url))))]
|
(on-remove-plugin index))))]
|
||||||
[:div {:class (stl/css :plugins-list-element)}
|
[:div {:class (stl/css :plugins-list-element)}
|
||||||
[:div {:class (stl/css :plugin-icon)} ""]
|
[:div {:class (stl/css :plugin-icon)}
|
||||||
|
[:img {:src (if (some? icon)
|
||||||
|
(dm/str host icon)
|
||||||
|
(avatars/generate {:name name}))}]]
|
||||||
[:div {:class (stl/css :plugin-description)}
|
[:div {:class (stl/css :plugin-description)}
|
||||||
[:div {:class (stl/css :plugin-title)} name]
|
[:div {:class (stl/css :plugin-title)} name]
|
||||||
[:div {:class (stl/css :plugin-summary)} description]]
|
[:div {:class (stl/css :plugin-summary)} (d/nilv description "")]]
|
||||||
[:button {:class (stl/css :open-button)
|
[:button {:class (stl/css :open-button)
|
||||||
:on-click handle-open-click} (tr "workspace.plugins.button-open")]
|
:on-click handle-open-click} (tr "workspace.plugins.button-open")]
|
||||||
[:button {:class (stl/css :trash-button)
|
[:button {:class (stl/css :trash-button)
|
||||||
|
@ -65,8 +70,15 @@
|
||||||
(.setItem ls "plugins" plugins-val)))
|
(.setItem ls "plugins" plugins-val)))
|
||||||
|
|
||||||
(defn open-plugin!
|
(defn open-plugin!
|
||||||
[url]
|
[{:keys [name description host code icon permissions]}]
|
||||||
(.ɵloadPlugin js/window #js {:manifest url}))
|
(.ɵloadPlugin
|
||||||
|
js/window #js
|
||||||
|
{:name name
|
||||||
|
:description description
|
||||||
|
:host host
|
||||||
|
:code code
|
||||||
|
:icon icon
|
||||||
|
:permissions (apply array permissions)}))
|
||||||
|
|
||||||
(mf/defc plugin-management-dialog
|
(mf/defc plugin-management-dialog
|
||||||
{::mf/register modal/components
|
{::mf/register modal/components
|
||||||
|
@ -107,7 +119,20 @@
|
||||||
(rx/subs!
|
(rx/subs!
|
||||||
(fn [body]
|
(fn [body]
|
||||||
(let [name (obj/get body "name")
|
(let [name (obj/get body "name")
|
||||||
new-state (conj plugins-state {:name name :url plugin-url})]
|
desc (obj/get body "description")
|
||||||
|
code (obj/get body "code")
|
||||||
|
icon (obj/get body "icon")
|
||||||
|
permissions (obj/get body "permissions")
|
||||||
|
origin (obj/get (js/URL. plugin-url) "origin")
|
||||||
|
|
||||||
|
new-state
|
||||||
|
(conj plugins-state
|
||||||
|
{:name name
|
||||||
|
:description desc
|
||||||
|
:host origin
|
||||||
|
:code code
|
||||||
|
:icon icon
|
||||||
|
:permissions (->> permissions (mapv str))})]
|
||||||
(reset! input-status* :success)
|
(reset! input-status* :success)
|
||||||
(reset! plugin-url* "")
|
(reset! plugin-url* "")
|
||||||
(reset! plugins-state* new-state)
|
(reset! plugins-state* new-state)
|
||||||
|
@ -117,18 +142,18 @@
|
||||||
|
|
||||||
handle-open-plugin
|
handle-open-plugin
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(fn [_ url]
|
(fn [manifest]
|
||||||
(open-plugin! url)
|
(open-plugin! manifest)
|
||||||
(modal/hide!)))
|
(modal/hide!)))
|
||||||
|
|
||||||
handle-remove-plugin
|
handle-remove-plugin
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps plugins-state)
|
(mf/deps plugins-state)
|
||||||
(fn [rm-idx _]
|
(fn [plugin-index]
|
||||||
(let [new-state
|
(let [new-state
|
||||||
(into []
|
(into []
|
||||||
(keep-indexed (fn [idx item]
|
(keep-indexed (fn [idx item]
|
||||||
(when (not= idx rm-idx) item)))
|
(when (not= idx plugin-index) item)))
|
||||||
plugins-state)]
|
plugins-state)]
|
||||||
|
|
||||||
(reset! plugins-state* new-state)
|
(reset! plugins-state* new-state)
|
||||||
|
@ -160,22 +185,22 @@
|
||||||
|
|
||||||
[:hr]
|
[:hr]
|
||||||
|
|
||||||
[:& title-bar {:collapsable false
|
|
||||||
:title (tr "workspace.plugins.installed-plugins")}]
|
|
||||||
|
|
||||||
(if (empty? plugins-state)
|
(if (empty? plugins-state)
|
||||||
[:div {:class (stl/css :plugins-empty)}
|
[:div {:class (stl/css :plugins-empty)}
|
||||||
[:div {:class (stl/css :plugins-empty-logo)} i/logo-icon]
|
[:div {:class (stl/css :plugins-empty-logo)} i/rocket]
|
||||||
[:div {:class (stl/css :plugins-empty-text)} (tr "workspace.plugins.empty-plugins")]]
|
[:div {:class (stl/css :plugins-empty-text)} (tr "workspace.plugins.empty-plugins")]
|
||||||
|
[:a {:class (stl/css :plugins-link) :href "#"}
|
||||||
|
(tr "workspace.plugins.plugin-list-link") i/external-link]]
|
||||||
|
|
||||||
[:div {:class (stl/css :plugins-list)}
|
[:*
|
||||||
|
[:& title-bar {:collapsable false
|
||||||
|
:title (tr "workspace.plugins.installed-plugins")}]
|
||||||
|
|
||||||
(for [[idx {:keys [name url]}] (d/enumerate plugins-state)]
|
[:div {:class (stl/css :plugins-list)}
|
||||||
[:& plugin-entry {:key (dm/str "plugin-" idx)
|
|
||||||
:name name
|
(for [[idx manifest] (d/enumerate plugins-state)]
|
||||||
:url url
|
[:& plugin-entry {:key (dm/str "plugin-" idx)
|
||||||
:index idx
|
:index idx
|
||||||
:icon nil
|
:manifest manifest
|
||||||
:description "Nullam ullamcorper ligula ac felis commodo pulvinar."
|
:on-open-plugin handle-open-plugin
|
||||||
:on-open-plugin handle-open-plugin
|
:on-remove-plugin handle-remove-plugin}])]])]]]))
|
||||||
:on-remove-plugin handle-remove-plugin}])])]]]))
|
|
||||||
|
|
|
@ -18,6 +18,10 @@
|
||||||
max-height: $s-472;
|
max-height: $s-472;
|
||||||
width: $s-472;
|
width: $s-472;
|
||||||
max-width: $s-472;
|
max-width: $s-472;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-color: $db-tertiary;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-btn {
|
.close-btn {
|
||||||
|
@ -31,7 +35,7 @@
|
||||||
|
|
||||||
.modal-title {
|
.modal-title {
|
||||||
@include headlineMediumTypography;
|
@include headlineMediumTypography;
|
||||||
margin-block-end: $s-16;
|
margin-block-end: $s-32;
|
||||||
color: var(--modal-title-foreground-color);
|
color: var(--modal-title-foreground-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,6 +43,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: $s-380;
|
height: $s-380;
|
||||||
|
padding-bottom: $s-16;
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-button {
|
.primary-button {
|
||||||
|
@ -88,7 +93,7 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: $s-12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugins-list-element {
|
.plugins-list-element {
|
||||||
|
@ -101,56 +106,61 @@
|
||||||
min-height: $s-32;
|
min-height: $s-32;
|
||||||
width: $s-32;
|
width: $s-32;
|
||||||
height: $s-32;
|
height: $s-32;
|
||||||
background: #b1b2b5;
|
background: var(--button-secondary-background-color-rest);
|
||||||
|
padding: $s-2;
|
||||||
|
border-radius: $s-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-description {
|
.plugin-description {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $s-8;
|
gap: $s-8;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-title {
|
.plugin-title {
|
||||||
@include bodyMediumTypography;
|
@include bodyMediumTypography;
|
||||||
color: #ffffff;
|
color: $df-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugin-summary {
|
.plugin-summary {
|
||||||
@include bodySmallTypography;
|
@include bodySmallTypography;
|
||||||
color: #8f9da3;
|
color: $df-secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugins-empty {
|
.plugins-empty {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20px;
|
gap: $s-20;
|
||||||
margin-top: 3rem;
|
margin-top: $s-16;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugins-empty-logo {
|
.plugins-empty-logo {
|
||||||
width: 44px;
|
width: $s-44;
|
||||||
height: 44px;
|
height: $s-44;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #212426;
|
background: $db-tertiary;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
width: 16px;
|
width: $s-16;
|
||||||
height: 16px;
|
height: $s-16;
|
||||||
fill: #8f9da3;
|
fill: none;
|
||||||
|
stroke: $df-secondary;
|
||||||
|
stroke-width: 0.8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.plugins-empty-text {
|
.plugins-empty-text {
|
||||||
@include bodySmallTypography;
|
@include bodySmallTypography;
|
||||||
color: white;
|
color: $df-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.input-error {
|
div.input-error {
|
||||||
border: 1px solid var(--input-border-color-error);
|
border: $s-1 solid var(--input-border-color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
@ -165,3 +175,19 @@ div.input-error {
|
||||||
color: var(--input-border-color-success);
|
color: var(--input-border-color-success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plugins-link {
|
||||||
|
color: $da-primary;
|
||||||
|
font-size: $fs-12;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $s-4;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-top: calc(-1 * var($s-2));
|
||||||
|
width: $s-12;
|
||||||
|
height: $s-12;
|
||||||
|
stroke: $da-primary;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -11,11 +11,20 @@
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.plugins.api :as api]
|
[app.plugins.api :as api]
|
||||||
[app.util.globals :refer [global]]
|
[app.util.globals :refer [global]]
|
||||||
[app.util.object :as obj]))
|
[app.util.object :as obj]
|
||||||
|
[beicon.v2.core :as rx]
|
||||||
|
[potok.v2.core :as ptk]))
|
||||||
|
|
||||||
(defn init!
|
(defn init!
|
||||||
[]
|
[]
|
||||||
(when (features/active-feature? @st/state "plugins/runtime")
|
(->> st/stream
|
||||||
(when-let [init-runtime (obj/get global "initPluginsRuntime")]
|
(rx/filter (ptk/type? ::features/initialize))
|
||||||
(let [context (api/create-context)]
|
(rx/take 1)
|
||||||
(init-runtime context)))))
|
;; We need to wait to the init event to finish
|
||||||
|
(rx/observe-on :async)
|
||||||
|
(rx/subs!
|
||||||
|
(fn []
|
||||||
|
(when (features/active-feature? @st/state "plugins/runtime")
|
||||||
|
(when-let [init-runtime (obj/get global "initPluginsRuntime")]
|
||||||
|
(let [context (api/create-context)]
|
||||||
|
(init-runtime context))))))))
|
||||||
|
|
|
@ -52,6 +52,10 @@
|
||||||
[_ type callback]
|
[_ type callback]
|
||||||
(events/add-listener type callback))
|
(events/add-listener type callback))
|
||||||
|
|
||||||
|
(removeListener
|
||||||
|
[_ listener-id]
|
||||||
|
(events/remove-listener listener-id))
|
||||||
|
|
||||||
(getViewport
|
(getViewport
|
||||||
[_]
|
[_]
|
||||||
(viewport/create-proxy))
|
(viewport/create-proxy))
|
||||||
|
|
|
@ -14,6 +14,14 @@
|
||||||
|
|
||||||
(defmulti handle-state-change (fn [type _] type))
|
(defmulti handle-state-change (fn [type _] type))
|
||||||
|
|
||||||
|
(defmethod handle-state-change "finish"
|
||||||
|
[_ old-val new-val]
|
||||||
|
(let [old-file-id (:current-file-id old-val)
|
||||||
|
new-file-id (:current-file-id new-val)]
|
||||||
|
(if (and (some? old-file-id) (nil? new-file-id))
|
||||||
|
(str old-file-id)
|
||||||
|
::not-changed)))
|
||||||
|
|
||||||
(defmethod handle-state-change "filechange"
|
(defmethod handle-state-change "filechange"
|
||||||
[_ old-val new-val]
|
[_ old-val new-val]
|
||||||
(let [old-file (:workspace-file old-val)
|
(let [old-file (:workspace-file old-val)
|
||||||
|
@ -72,3 +80,6 @@
|
||||||
;; return the generated key
|
;; return the generated key
|
||||||
key))
|
key))
|
||||||
|
|
||||||
|
(defn remove-listener
|
||||||
|
[key]
|
||||||
|
(remove-watch st/state key))
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
(ns app.plugins.library
|
(ns app.plugins.library
|
||||||
"RPC for plugins runtime."
|
"RPC for plugins runtime."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.colors :as cc]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.record :as cr]
|
[app.common.record :as cr]
|
||||||
|
[app.main.data.workspace.libraries :as dwl]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.plugins.utils :as u]))
|
[app.plugins.utils :as u]))
|
||||||
|
|
||||||
|
@ -53,13 +55,32 @@
|
||||||
{:name "id" :get (fn [_] (dm/str id))}
|
{:name "id" :get (fn [_] (dm/str id))}
|
||||||
|
|
||||||
{:name "name"
|
{:name "name"
|
||||||
:get #(-> % u/proxy->library-color :name)}
|
:get #(-> % u/proxy->library-color :name)
|
||||||
|
:set
|
||||||
|
(fn [_ value]
|
||||||
|
(if (and (some? value) (string? value))
|
||||||
|
(st/emit! (dwl/rename-color file-id id value))
|
||||||
|
(u/display-not-valid :library-color-name value)))}
|
||||||
|
|
||||||
{:name "color"
|
{:name "color"
|
||||||
:get #(-> % u/proxy->library-color :color)}
|
:get #(-> % u/proxy->library-color :color)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(if (and (some? value) (string? value) (cc/valid-hex-color? value))
|
||||||
|
(let [color (-> (u/proxy->library-color self)
|
||||||
|
(assoc :color value))]
|
||||||
|
(st/emit! (dwl/update-color color file-id)))
|
||||||
|
(u/display-not-valid :library-color-color value)))}
|
||||||
|
|
||||||
{:name "opacity"
|
{:name "opacity"
|
||||||
:get #(-> % u/proxy->library-color :opacity)}
|
:get #(-> % u/proxy->library-color :opacity)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(if (and (some? value) (number? value) (>= value 0) (<= value 1))
|
||||||
|
(let [color (-> (u/proxy->library-color self)
|
||||||
|
(assoc :opacity value))]
|
||||||
|
(st/emit! (dwl/update-color color file-id)))
|
||||||
|
(u/display-not-valid :library-color-color value)))}
|
||||||
|
|
||||||
{:name "gradient"
|
{:name "gradient"
|
||||||
:get #(-> % u/proxy->library-color :gradient u/to-js)}
|
:get #(-> % u/proxy->library-color :gradient u/to-js)}
|
||||||
|
@ -96,8 +117,7 @@
|
||||||
{:name "$id" :enumerable false :get (constantly id)}
|
{:name "$id" :enumerable false :get (constantly id)}
|
||||||
{:name "$file" :enumerable false :get (constantly file-id)}
|
{:name "$file" :enumerable false :get (constantly file-id)}
|
||||||
{:name "id" :get (fn [_] (dm/str id))}
|
{:name "id" :get (fn [_] (dm/str id))}
|
||||||
{:name "name"
|
{:name "name" :get #(-> % u/proxy->library-component :name)}))
|
||||||
:get #(-> % u/proxy->library-component :name)}))
|
|
||||||
|
|
||||||
(deftype Library [$id]
|
(deftype Library [$id]
|
||||||
Object)
|
Object)
|
||||||
|
|
|
@ -28,8 +28,9 @@
|
||||||
(findShapes
|
(findShapes
|
||||||
[_]
|
[_]
|
||||||
;; Returns a lazy (iterable) of all available shapes
|
;; Returns a lazy (iterable) of all available shapes
|
||||||
(let [page (locate-page $file $id)]
|
(when (and (some? $file) (some? $id))
|
||||||
(apply array (sequence (map shape/shape-proxy) (keys (:objects page)))))))
|
(let [page (locate-page $file $id)]
|
||||||
|
(apply array (sequence (map shape/shape-proxy) (keys (:objects page))))))))
|
||||||
|
|
||||||
(crc/define-properties!
|
(crc/define-properties!
|
||||||
PageProxy
|
PageProxy
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
(ns app.plugins.shape
|
(ns app.plugins.shape
|
||||||
"RPC for plugins runtime."
|
"RPC for plugins runtime."
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.colors :as clr]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.files.helpers :as cfh]
|
[app.common.files.helpers :as cfh]
|
||||||
[app.common.record :as crc]
|
[app.common.record :as crc]
|
||||||
|
@ -14,19 +15,30 @@
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
[app.common.types.shape :as cts]
|
[app.common.types.shape :as cts]
|
||||||
[app.common.types.shape.layout :as ctl]
|
[app.common.types.shape.layout :as ctl]
|
||||||
|
[app.common.types.shape.radius :as ctsr]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
[app.main.data.workspace :as udw]
|
[app.main.data.workspace :as udw]
|
||||||
[app.main.data.workspace.changes :as dwc]
|
[app.main.data.workspace.changes :as dwc]
|
||||||
[app.main.data.workspace.selection :as dws]
|
[app.main.data.workspace.selection :as dws]
|
||||||
[app.main.data.workspace.shape-layout :as dwsl]
|
[app.main.data.workspace.shape-layout :as dwsl]
|
||||||
[app.main.data.workspace.shapes :as dwsh]
|
[app.main.data.workspace.shapes :as dwsh]
|
||||||
|
[app.main.data.workspace.texts :as dwt]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.plugins.flex :as flex]
|
[app.plugins.flex :as flex]
|
||||||
[app.plugins.grid :as grid]
|
[app.plugins.grid :as grid]
|
||||||
[app.plugins.utils :as utils :refer [locate-objects locate-shape proxy->shape array-to-js]]
|
[app.plugins.utils :as utils :refer [locate-objects locate-shape proxy->shape array-to-js]]
|
||||||
[app.util.object :as obj]))
|
[app.util.object :as obj]
|
||||||
|
[app.util.text-editor :as ted]))
|
||||||
|
|
||||||
(declare shape-proxy)
|
(declare shape-proxy)
|
||||||
|
|
||||||
|
(defn text-props
|
||||||
|
[shape]
|
||||||
|
(d/merge
|
||||||
|
(dwt/current-root-values {:shape shape :attrs txt/root-attrs})
|
||||||
|
(dwt/current-paragraph-values {:shape shape :attrs txt/paragraph-attrs})
|
||||||
|
(dwt/current-text-values {:shape shape :attrs txt/text-node-attrs})))
|
||||||
|
|
||||||
(deftype ShapeProxy [$file $page $id]
|
(deftype ShapeProxy [$file $page $id]
|
||||||
Object
|
Object
|
||||||
(resize
|
(resize
|
||||||
|
@ -145,37 +157,52 @@
|
||||||
{:name "borderRadius"
|
{:name "borderRadius"
|
||||||
:get #(-> % proxy->shape :rx)
|
:get #(-> % proxy->shape :rx)
|
||||||
:set (fn [self value]
|
:set (fn [self value]
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")
|
||||||
|
shape (proxy->shape self)]
|
||||||
(when (us/safe-int? value)
|
(when (us/safe-int? value)
|
||||||
(st/emit! (dwc/update-shapes [id] #(assoc % :rx value :ry value))))))}
|
(when (or (not (ctsr/has-radius? shape)) (ctsr/radius-4? shape))
|
||||||
|
(st/emit! (dwc/update-shapes [id] ctsr/switch-to-radius-1)))
|
||||||
|
(st/emit! (dwc/update-shapes [id] #(ctsr/set-radius-1 % value))))))}
|
||||||
|
|
||||||
{:name "borderRadiusTopLeft"
|
{:name "borderRadiusTopLeft"
|
||||||
:get #(-> % proxy->shape :r1)
|
:get #(-> % proxy->shape :r1)
|
||||||
:set (fn [self value]
|
:set (fn [self value]
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")
|
||||||
|
shape (proxy->shape self)]
|
||||||
(when (us/safe-int? value)
|
(when (us/safe-int? value)
|
||||||
(st/emit! (dwc/update-shapes [id] #(assoc % :r1 value))))))}
|
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
|
||||||
|
(st/emit! (dwc/update-shapes [id] ctsr/switch-to-radius-4)))
|
||||||
|
(st/emit! (dwc/update-shapes [id] #(ctsr/set-radius-4 % :r1 value))))))}
|
||||||
|
|
||||||
{:name "borderRadiusTopRight"
|
{:name "borderRadiusTopRight"
|
||||||
:get #(-> % proxy->shape :r2)
|
:get #(-> % proxy->shape :r2)
|
||||||
:set (fn [self value]
|
:set (fn [self value]
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")
|
||||||
|
shape (proxy->shape self)]
|
||||||
(when (us/safe-int? value)
|
(when (us/safe-int? value)
|
||||||
(st/emit! (dwc/update-shapes [id] #(assoc % :r2 value))))))}
|
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
|
||||||
|
(st/emit! (dwc/update-shapes [id] ctsr/switch-to-radius-4)))
|
||||||
|
(st/emit! (dwc/update-shapes [id] #(ctsr/set-radius-4 % :r2 value))))))}
|
||||||
|
|
||||||
{:name "borderRadiusBottomRight"
|
{:name "borderRadiusBottomRight"
|
||||||
:get #(-> % proxy->shape :r3)
|
:get #(-> % proxy->shape :r3)
|
||||||
:set (fn [self value]
|
:set (fn [self value]
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")
|
||||||
|
shape (proxy->shape self)]
|
||||||
(when (us/safe-int? value)
|
(when (us/safe-int? value)
|
||||||
(st/emit! (dwc/update-shapes [id] #(assoc % :r3 value))))))}
|
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
|
||||||
|
(st/emit! (dwc/update-shapes [id] ctsr/switch-to-radius-4)))
|
||||||
|
(st/emit! (dwc/update-shapes [id] #(ctsr/set-radius-4 % :r3 value))))))}
|
||||||
|
|
||||||
{:name "borderRadiusBottomLeft"
|
{:name "borderRadiusBottomLeft"
|
||||||
:get #(-> % proxy->shape :r4)
|
:get #(-> % proxy->shape :r4)
|
||||||
:set (fn [self value]
|
:set (fn [self value]
|
||||||
(let [id (obj/get self "$id")]
|
(let [id (obj/get self "$id")
|
||||||
|
shape (proxy->shape self)]
|
||||||
(when (us/safe-int? value)
|
(when (us/safe-int? value)
|
||||||
(st/emit! (dwc/update-shapes [id] #(assoc % :r4 value))))))}
|
(when (or (not (ctsr/has-radius? shape)) (not (ctsr/radius-4? shape)))
|
||||||
|
(st/emit! (dwc/update-shapes [id] ctsr/switch-to-radius-4)))
|
||||||
|
(st/emit! (dwc/update-shapes [id] #(ctsr/set-radius-4 % :r4 value))))))}
|
||||||
|
|
||||||
{:name "opacity"
|
{:name "opacity"
|
||||||
:get #(-> % proxy->shape :opacity)
|
:get #(-> % proxy->shape :opacity)
|
||||||
|
@ -196,15 +223,35 @@
|
||||||
:get #(-> % proxy->shape :shadow array-to-js)
|
:get #(-> % proxy->shape :shadow array-to-js)
|
||||||
:set (fn [self value]
|
:set (fn [self value]
|
||||||
(let [id (obj/get self "$id")
|
(let [id (obj/get self "$id")
|
||||||
value (mapv #(utils/from-js %) value)]
|
value (mapv (fn [val]
|
||||||
(st/emit! (dwc/update-shapes [id] #(assoc % :shadows value)))))}
|
;; Merge default shadow properties
|
||||||
|
(d/patch-object
|
||||||
|
{:id (uuid/next)
|
||||||
|
:style :drop-shadow
|
||||||
|
:color {:color clr/black :opacity 0.2}
|
||||||
|
:offset-x 4
|
||||||
|
:offset-y 4
|
||||||
|
:blur 4
|
||||||
|
:spread 0
|
||||||
|
:hidden false}
|
||||||
|
(utils/from-js val #{:style :type})))
|
||||||
|
value)]
|
||||||
|
(st/emit! (dwc/update-shapes [id] #(assoc % :shadow value)))))}
|
||||||
|
|
||||||
{:name "blur"
|
{:name "blur"
|
||||||
:get #(-> % proxy->shape :blur utils/to-js)
|
:get #(-> % proxy->shape :blur utils/to-js)
|
||||||
:set (fn [self value]
|
:set (fn [self value]
|
||||||
(let [id (obj/get self "$id")
|
(if (nil? value)
|
||||||
value (utils/from-js value)]
|
(st/emit! (dwc/update-shapes [id] #(dissoc % :blur)))
|
||||||
(st/emit! (dwc/update-shapes [id] #(assoc % :blur value)))))}
|
(let [id (obj/get self "$id")
|
||||||
|
value
|
||||||
|
(d/patch-object
|
||||||
|
{:id (uuid/next)
|
||||||
|
:type :layer-blur
|
||||||
|
:value 4
|
||||||
|
:hidden false}
|
||||||
|
(utils/from-js value))]
|
||||||
|
(st/emit! (dwc/update-shapes [id] #(assoc % :blur value))))))}
|
||||||
|
|
||||||
{:name "exports"
|
{:name "exports"
|
||||||
:get #(-> % proxy->shape :exports array-to-js)
|
:get #(-> % proxy->shape :exports array-to-js)
|
||||||
|
@ -301,7 +348,9 @@
|
||||||
|
|
||||||
;; Strokes and fills
|
;; Strokes and fills
|
||||||
{:name "fills"
|
{:name "fills"
|
||||||
:get #(-> % proxy->shape :fills array-to-js)
|
:get #(if (cfh/text-shape? data)
|
||||||
|
(-> % proxy->shape text-props :fills array-to-js)
|
||||||
|
(-> % proxy->shape :fills array-to-js))
|
||||||
:set (fn [self value]
|
:set (fn [self value]
|
||||||
(let [id (obj/get self "$id")
|
(let [id (obj/get self "$id")
|
||||||
value (mapv #(utils/from-js %) value)]
|
value (mapv #(utils/from-js %) value)]
|
||||||
|
@ -311,7 +360,7 @@
|
||||||
:get #(-> % proxy->shape :strokes array-to-js)
|
:get #(-> % proxy->shape :strokes array-to-js)
|
||||||
:set (fn [self value]
|
:set (fn [self value]
|
||||||
(let [id (obj/get self "$id")
|
(let [id (obj/get self "$id")
|
||||||
value (mapv #(utils/from-js %) value)]
|
value (mapv #(utils/from-js % #{:stroke-style :stroke-alignment}) value)]
|
||||||
(st/emit! (dwc/update-shapes [id] #(assoc % :strokes value)))))}
|
(st/emit! (dwc/update-shapes [id] #(assoc % :strokes value)))))}
|
||||||
|
|
||||||
{:name "layoutChild"
|
{:name "layoutChild"
|
||||||
|
@ -397,18 +446,93 @@
|
||||||
(obj/unset! "addFlexLayout")))
|
(obj/unset! "addFlexLayout")))
|
||||||
|
|
||||||
(cond-> (cfh/text-shape? data)
|
(cond-> (cfh/text-shape? data)
|
||||||
(-> (crc/add-properties!
|
(crc/add-properties!
|
||||||
{:name "characters"
|
{:name "characters"
|
||||||
:get #(-> % proxy->shape :content txt/content->text)
|
:get #(-> % proxy->shape :content txt/content->text)
|
||||||
:set (fn [self value]
|
:set
|
||||||
(let [id (obj/get self "$id")]
|
(fn [self value]
|
||||||
(st/emit! (dwc/update-shapes [id] #(txt/change-text % value)))))})
|
(let [id (obj/get self "$id")]
|
||||||
|
;; The user is currently editing the text. We need to update the
|
||||||
|
;; editor as well
|
||||||
|
(when (contains? (:workspace-editor-state @st/state) id)
|
||||||
|
(let [shape (proxy->shape self)
|
||||||
|
editor
|
||||||
|
(-> shape
|
||||||
|
(txt/change-text value)
|
||||||
|
:content
|
||||||
|
ted/import-content
|
||||||
|
ted/create-editor-state)]
|
||||||
|
(st/emit! (dwt/update-editor-state shape editor))))
|
||||||
|
(st/emit! (dwc/update-shapes [id] #(txt/change-text % value)))))}
|
||||||
|
|
||||||
(crc/add-properties!
|
{:name "growType"
|
||||||
{:name "growType"
|
:get #(-> % proxy->shape :grow-type d/name)
|
||||||
:get #(-> % proxy->shape :grow-type d/name)
|
:set
|
||||||
:set (fn [self value]
|
(fn [self value]
|
||||||
(let [id (obj/get self "$id")
|
(let [id (obj/get self "$id")
|
||||||
value (keyword value)]
|
value (keyword value)]
|
||||||
(when (contains? #{:auto-width :auto-height :fixed} value)
|
(when (contains? #{:auto-width :auto-height :fixed} value)
|
||||||
(st/emit! (dwc/update-shapes [id] #(assoc % :grow-type value))))))})))))))
|
(st/emit! (dwc/update-shapes [id] #(assoc % :grow-type value))))))}
|
||||||
|
|
||||||
|
{:name "fontId"
|
||||||
|
:get #(-> % proxy->shape text-props :font-id)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(let [id (obj/get self "$id")]
|
||||||
|
(st/emit! (dwt/update-attrs id {:font-id value}))))}
|
||||||
|
|
||||||
|
{:name "fontFamily"
|
||||||
|
:get #(-> % proxy->shape text-props :font-family)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(let [id (obj/get self "$id")]
|
||||||
|
(st/emit! (dwt/update-attrs id {:font-id value}))))}
|
||||||
|
|
||||||
|
{:name "fontVariantId"
|
||||||
|
:get #(-> % proxy->shape text-props :font-variant-id)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(let [id (obj/get self "$id")]
|
||||||
|
(st/emit! (dwt/update-attrs id {:font-id value}))))}
|
||||||
|
|
||||||
|
{:name "fontSize"
|
||||||
|
:get #(-> % proxy->shape text-props :font-size)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(let [id (obj/get self "$id")]
|
||||||
|
(st/emit! (dwt/update-attrs id {:font-id value}))))}
|
||||||
|
|
||||||
|
{:name "fontWeight"
|
||||||
|
:get #(-> % proxy->shape text-props :font-weight)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(let [id (obj/get self "$id")]
|
||||||
|
(st/emit! (dwt/update-attrs id {:font-id value}))))}
|
||||||
|
|
||||||
|
{:name "fontStyle"
|
||||||
|
:get #(-> % proxy->shape text-props :font-style)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(let [id (obj/get self "$id")]
|
||||||
|
(st/emit! (dwt/update-attrs id {:font-style value}))))}
|
||||||
|
|
||||||
|
{:name "lineHeight"
|
||||||
|
:get #(-> % proxy->shape text-props :line-height)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(let [id (obj/get self "$id")]
|
||||||
|
(st/emit! (dwt/update-attrs id {:line-height value}))))}
|
||||||
|
|
||||||
|
{:name "letterSpacing"
|
||||||
|
:get #(-> % proxy->shape text-props :letter-spacing)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(let [id (obj/get self "$id")]
|
||||||
|
(st/emit! (dwt/update-attrs id {:letter-spacing value}))))}
|
||||||
|
|
||||||
|
{:name "textTransform"
|
||||||
|
:get #(-> % proxy->shape text-props :text-transform)
|
||||||
|
:set
|
||||||
|
(fn [self value]
|
||||||
|
(let [id (obj/get self "$id")]
|
||||||
|
(st/emit! (dwt/update-attrs id {:text-transform value}))))}))))))
|
||||||
|
|
|
@ -56,38 +56,44 @@
|
||||||
(defn proxy->file
|
(defn proxy->file
|
||||||
[proxy]
|
[proxy]
|
||||||
(let [id (obj/get proxy "$id")]
|
(let [id (obj/get proxy "$id")]
|
||||||
(locate-file id)))
|
(when (some? id)
|
||||||
|
(locate-file id))))
|
||||||
|
|
||||||
(defn proxy->page
|
(defn proxy->page
|
||||||
[proxy]
|
[proxy]
|
||||||
(let [file-id (obj/get proxy "$file")
|
(let [file-id (obj/get proxy "$file")
|
||||||
id (obj/get proxy "$id")]
|
id (obj/get proxy "$id")]
|
||||||
(locate-page file-id id)))
|
(when (and (some? file-id) (some? id))
|
||||||
|
(locate-page file-id id))))
|
||||||
|
|
||||||
(defn proxy->shape
|
(defn proxy->shape
|
||||||
[proxy]
|
[proxy]
|
||||||
(let [file-id (obj/get proxy "$file")
|
(let [file-id (obj/get proxy "$file")
|
||||||
page-id (obj/get proxy "$page")
|
page-id (obj/get proxy "$page")
|
||||||
id (obj/get proxy "$id")]
|
id (obj/get proxy "$id")]
|
||||||
(locate-shape file-id page-id id)))
|
(when (and (some? file-id) (some? page-id) (some? id))
|
||||||
|
(locate-shape file-id page-id id))))
|
||||||
|
|
||||||
(defn proxy->library-color
|
(defn proxy->library-color
|
||||||
[proxy]
|
[proxy]
|
||||||
(let [file-id (obj/get proxy "$file")
|
(let [file-id (obj/get proxy "$file")
|
||||||
id (obj/get proxy "$id")]
|
id (obj/get proxy "$id")]
|
||||||
(locate-library-color file-id id)))
|
(when (and (some? file-id) (some? id))
|
||||||
|
(locate-library-color file-id id))))
|
||||||
|
|
||||||
(defn proxy->library-typography
|
(defn proxy->library-typography
|
||||||
[proxy]
|
[proxy]
|
||||||
(let [file-id (obj/get proxy "$file")
|
(let [file-id (obj/get proxy "$file")
|
||||||
id (obj/get proxy "$id")]
|
id (obj/get proxy "$id")]
|
||||||
(locate-library-color file-id id)))
|
(when (and (some? file-id) (some? id))
|
||||||
|
(locate-library-color file-id id))))
|
||||||
|
|
||||||
(defn proxy->library-component
|
(defn proxy->library-component
|
||||||
[proxy]
|
[proxy]
|
||||||
(let [file-id (obj/get proxy "$file")
|
(let [file-id (obj/get proxy "$file")
|
||||||
id (obj/get proxy "$id")]
|
id (obj/get proxy "$id")]
|
||||||
(locate-library-color file-id id)))
|
(when (and (some? file-id) (some? id))
|
||||||
|
(locate-library-color file-id id))))
|
||||||
|
|
||||||
(defn get-data
|
(defn get-data
|
||||||
([self attr]
|
([self attr]
|
||||||
|
@ -118,30 +124,32 @@
|
||||||
|
|
||||||
(defn from-js
|
(defn from-js
|
||||||
"Converts the object back to js"
|
"Converts the object back to js"
|
||||||
[obj]
|
([obj]
|
||||||
(when (some? obj)
|
(from-js obj #{:type}))
|
||||||
(let [process-node
|
([obj keyword-keys]
|
||||||
(fn process-node [node]
|
(when (some? obj)
|
||||||
(reduce-kv
|
(let [process-node
|
||||||
(fn [m k v]
|
(fn process-node [node]
|
||||||
(let [k (keyword (str/kebab k))
|
(reduce-kv
|
||||||
v (cond (map? v)
|
(fn [m k v]
|
||||||
(process-node v)
|
(let [k (keyword (str/kebab k))
|
||||||
|
v (cond (map? v)
|
||||||
|
(process-node v)
|
||||||
|
|
||||||
(vector? v)
|
(vector? v)
|
||||||
(mapv process-node v)
|
(mapv process-node v)
|
||||||
|
|
||||||
(and (string? v) (re-matches us/uuid-rx v))
|
(and (string? v) (re-matches us/uuid-rx v))
|
||||||
(uuid/uuid v)
|
(uuid/uuid v)
|
||||||
|
|
||||||
(= k :type)
|
(contains? keyword-keys k)
|
||||||
(keyword v)
|
(keyword v)
|
||||||
|
|
||||||
:else v)]
|
:else v)]
|
||||||
(assoc m k v)))
|
(assoc m k v)))
|
||||||
{}
|
{}
|
||||||
node))]
|
node))]
|
||||||
(process-node (js->clj obj)))))
|
(process-node (js->clj obj))))))
|
||||||
|
|
||||||
(defn to-js
|
(defn to-js
|
||||||
"Converts to javascript an camelize the keys"
|
"Converts to javascript an camelize the keys"
|
||||||
|
@ -180,3 +188,7 @@
|
||||||
(remove-watch ret-v ::watcher)
|
(remove-watch ret-v ::watcher)
|
||||||
(resolve value)))))]
|
(resolve value)))))]
|
||||||
[ret-v ret-p]))
|
[ret-v ret-p]))
|
||||||
|
|
||||||
|
(defn display-not-valid
|
||||||
|
[code value]
|
||||||
|
(.error js/console (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)))
|
||||||
|
|
|
@ -19,6 +19,9 @@
|
||||||
[frontend-tests.helpers.pages :as thp]
|
[frontend-tests.helpers.pages :as thp]
|
||||||
[frontend-tests.helpers.state :as ths]))
|
[frontend-tests.helpers.state :as ths]))
|
||||||
|
|
||||||
|
(t/use-fixtures :each
|
||||||
|
{:before thp/reset-idmap!})
|
||||||
|
|
||||||
;; Related .penpot file: common/test/cases/remove-swap-slots.penpot
|
;; Related .penpot file: common/test/cases/remove-swap-slots.penpot
|
||||||
(defn- setup-file
|
(defn- setup-file
|
||||||
[]
|
[]
|
||||||
|
@ -813,3 +816,57 @@
|
||||||
;; copied-blue1 has swap-id
|
;; copied-blue1 has swap-id
|
||||||
(t/is (some? copied-blue2'))
|
(t/is (some? copied-blue2'))
|
||||||
(t/is (some? (ctk/get-swap-slot copied-blue2')))))))))
|
(t/is (some? (ctk/get-swap-slot copied-blue2')))))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest test-remove-swap-slot-copy-paste-swapped-main
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(let [;; ==== Setup
|
||||||
|
;; {:frame-red} [:name frame-blue] # [Component :red]
|
||||||
|
;; {:frame-blue} [:name frame-blue] #[Component :blue]
|
||||||
|
;; {:frame-green} [:name frame-green] #[Component :green]
|
||||||
|
;; :blue1 [:name frame-blue, :swap-slot-label :red-copy-green] @--> frame-blue
|
||||||
|
|
||||||
|
file (-> (cthf/sample-file :file1)
|
||||||
|
(ctho/add-frame :frame-red :name "frame-blue")
|
||||||
|
(cthc/make-component :red :frame-red)
|
||||||
|
(ctho/add-frame :frame-blue :name "frame-blue")
|
||||||
|
(cthc/make-component :blue :frame-blue)
|
||||||
|
(ctho/add-frame :frame-green :name "frame-green")
|
||||||
|
(cthc/make-component :green :frame-green)
|
||||||
|
(cthc/instantiate-component :red :red-copy-green :parent-label :frame-green)
|
||||||
|
(cthc/component-swap :red-copy-green :blue :blue1))
|
||||||
|
store (ths/setup-store file)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
page (cthf/current-page file)
|
||||||
|
green (cths/get-shape file :frame-green)
|
||||||
|
features #{"components/v2"}
|
||||||
|
version 47
|
||||||
|
|
||||||
|
pdata (thp/simulate-copy-shape #{(:id green)} (:objects page) {(:id file) file} page file features version)
|
||||||
|
|
||||||
|
events
|
||||||
|
[(dws/select-shape uuid/zero)
|
||||||
|
(dw/paste-shapes pdata)]]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [;; ==== Get
|
||||||
|
file' (ths/get-file-from-store new-state)
|
||||||
|
page' (cthf/current-page file')
|
||||||
|
green' (cths/get-shape file' :frame-green)
|
||||||
|
blue1' (cths/get-shape file' :blue1)
|
||||||
|
copied-green' (find-copied-shape green' page' uuid/zero)
|
||||||
|
copied-blue1' (find-copied-shape blue1' page' (:id copied-green'))]
|
||||||
|
|
||||||
|
;; ==== Check
|
||||||
|
;; blue1 has swap-id
|
||||||
|
(t/is (some? (ctk/get-swap-slot blue1')))
|
||||||
|
|
||||||
|
;; copied-blue1 has not swap-id
|
||||||
|
(t/is (some? copied-blue1'))
|
||||||
|
(t/is (nil? (ctk/get-swap-slot copied-blue1')))))))))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,396 @@
|
||||||
|
;; 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 frontend-tests.logic.copying-and-duplicating-test
|
||||||
|
(:require
|
||||||
|
[app.common.test-helpers.components :as cthc]
|
||||||
|
[app.common.test-helpers.compositions :as ctho]
|
||||||
|
[app.common.test-helpers.files :as cthf]
|
||||||
|
[app.common.test-helpers.ids-map :as cthi]
|
||||||
|
[app.common.test-helpers.shapes :as cths]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.data.workspace :as dw]
|
||||||
|
[app.main.data.workspace.colors :as dc]
|
||||||
|
[app.main.data.workspace.libraries :as dwl]
|
||||||
|
[app.main.data.workspace.selection :as dws]
|
||||||
|
[cljs.test :as t :include-macros true]
|
||||||
|
[frontend-tests.helpers.pages :as thp]
|
||||||
|
[frontend-tests.helpers.state :as ths]))
|
||||||
|
|
||||||
|
(t/use-fixtures :each
|
||||||
|
{:before thp/reset-idmap!})
|
||||||
|
|
||||||
|
;; Related .penpot file: common/test/cases/copying-and-duplicating.penpot
|
||||||
|
(defn- setup-file []
|
||||||
|
(-> (cthf/sample-file :file1 :page-label :page-1)
|
||||||
|
(ctho/add-simple-component :simple-1 :frame-simple-1 :rect-simple-1 :child-params {:type :rect :fills (cths/sample-fills-color :fill-color "#2152e5") :name "rect-simple-1"})
|
||||||
|
|
||||||
|
(ctho/add-frame :frame-composed-1 :name "frame-composed-1")
|
||||||
|
(cthc/instantiate-component :simple-1 :copy-simple-1 :parent-label :frame-composed-1 :children-labels [:composed-1-simple-1])
|
||||||
|
(cths/add-sample-shape :rect-composed-1 :parent-label :frame-composed-1 :fills (cths/sample-fills-color :fill-color "#B1B2B5"))
|
||||||
|
(cthc/make-component :composed-1 :frame-composed-1)
|
||||||
|
|
||||||
|
(ctho/add-frame :frame-composed-2 :name "frame-composed-2")
|
||||||
|
(cthc/instantiate-component :composed-1 :copy-composed-1-composed-2 :parent-label :frame-composed-2 :children-labels [:composed-1-composed-2])
|
||||||
|
(cthc/make-component :composed-2 :frame-composed-2)
|
||||||
|
|
||||||
|
(cthc/instantiate-component :composed-2 :copy-composed-2)
|
||||||
|
|
||||||
|
(ctho/add-frame :frame-composed-3 :name "frame-composed-3")
|
||||||
|
(ctho/add-group :group-3 :parent-label :frame-composed-3)
|
||||||
|
(cthc/instantiate-component :composed-2 :copy-composed-1-composed-3 :parent-label :group-3 :children-labels [:composed-1-composed-2])
|
||||||
|
(cths/add-sample-shape :circle-composed-3 :parent-label :group-3 :fills (cths/sample-fills-color :fill-color "#B1B2B5"))
|
||||||
|
(cthc/make-component :composed-3 :frame-composed-3)
|
||||||
|
|
||||||
|
(cthc/instantiate-component :composed-3 :copy-composed-3 :children-labels [:composed-2-composed-3])
|
||||||
|
(cthf/add-sample-page :page-2)
|
||||||
|
(cthf/switch-to-page :page-1)))
|
||||||
|
|
||||||
|
|
||||||
|
(defn- copy-paste-shape
|
||||||
|
[id file & {:keys [target-page-label target-container-id]}]
|
||||||
|
(let [features #{"components/v2"}
|
||||||
|
version 46
|
||||||
|
page (cthf/current-page file)
|
||||||
|
target-page-id (cthi/id target-page-label)
|
||||||
|
shape (if (keyword? id)
|
||||||
|
(cths/get-shape file id)
|
||||||
|
(cths/get-shape-by-id file id))
|
||||||
|
pdata (thp/simulate-copy-shape #{(:id shape)} (:objects page) {(:id file) file} page file features version)
|
||||||
|
target-container-id (or target-container-id (:parent-id shape))]
|
||||||
|
|
||||||
|
(filter some?
|
||||||
|
[(when target-page-id (dw/initialize-page target-page-id))
|
||||||
|
(dws/select-shape target-container-id)
|
||||||
|
(dw/paste-shapes pdata)
|
||||||
|
(when target-page-id (dw/initialize-page (:id page)))])))
|
||||||
|
|
||||||
|
(defn- sync-file [file]
|
||||||
|
(map (fn [component-tag]
|
||||||
|
(->> component-tag
|
||||||
|
(cthc/get-component file)
|
||||||
|
:component-id
|
||||||
|
(dwl/sync-file (:id file) (:id file) :components)))
|
||||||
|
[:simple-1 :composed-1 :composed-2 :composed-3]))
|
||||||
|
|
||||||
|
(defn- set-color-bottom-shape [label file color]
|
||||||
|
(let [shape (ctho/bottom-shape file label)]
|
||||||
|
(concat
|
||||||
|
[(dws/select-shape (:id shape))
|
||||||
|
(dc/apply-color-from-palette color false)]
|
||||||
|
(sync-file file))))
|
||||||
|
|
||||||
|
(defn- count-shapes [file name color]
|
||||||
|
(let [page (cthf/current-page file)]
|
||||||
|
(->> (vals (:objects page))
|
||||||
|
(filter #(and
|
||||||
|
(= (:name %) name)
|
||||||
|
(-> (cths/get-shape-by-id file (:id %))
|
||||||
|
:fills
|
||||||
|
first
|
||||||
|
:fill-color
|
||||||
|
(= color))))
|
||||||
|
(count))))
|
||||||
|
|
||||||
|
(defn- duplicate-each-main-and-first-level-copy [file]
|
||||||
|
(concat (copy-paste-shape :frame-simple-1 file)
|
||||||
|
(copy-paste-shape :frame-simple-1 file)
|
||||||
|
(copy-paste-shape :frame-composed-1 file)
|
||||||
|
(copy-paste-shape :frame-composed-1 file)
|
||||||
|
(copy-paste-shape :frame-composed-2 file)
|
||||||
|
(copy-paste-shape :frame-composed-2 file)
|
||||||
|
(copy-paste-shape :frame-composed-3 file)
|
||||||
|
(copy-paste-shape :frame-composed-3 file)
|
||||||
|
(copy-paste-shape :copy-composed-2 file)
|
||||||
|
(copy-paste-shape :copy-composed-2 file)
|
||||||
|
(copy-paste-shape :copy-composed-3 file)
|
||||||
|
(copy-paste-shape :copy-composed-3 file)))
|
||||||
|
|
||||||
|
(defn- duplicate-simple-nested-in-main-and-group [file]
|
||||||
|
(concat (copy-paste-shape :copy-simple-1 file)
|
||||||
|
(copy-paste-shape :copy-simple-1 file)
|
||||||
|
(copy-paste-shape :group-3 file)
|
||||||
|
(copy-paste-shape :group-3 file)))
|
||||||
|
|
||||||
|
(defn- duplicate-copy-nested-and-group-out-of-the-main
|
||||||
|
[file & {:keys [target-page-label]}]
|
||||||
|
(let [page (cthf/current-page file)
|
||||||
|
frame-1-instance-ids (->> (vals (:objects page))
|
||||||
|
(filter #(and
|
||||||
|
(or
|
||||||
|
(= (:name %) "Frame1")
|
||||||
|
(= (:name %) "Group1"))
|
||||||
|
(not (:component-root %))))
|
||||||
|
(map :id))]
|
||||||
|
(concat
|
||||||
|
(apply concat (mapv #(copy-paste-shape % file :target-page-label target-page-label :target-container-id uuid/zero) frame-1-instance-ids))
|
||||||
|
(apply concat (mapv #(copy-paste-shape % file :target-page-label target-page-label :target-container-id uuid/zero) frame-1-instance-ids)))))
|
||||||
|
|
||||||
|
(t/deftest main-and-first-level-copy-1
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(with-redefs [uuid/next cthi/next-uuid]
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (setup-file)
|
||||||
|
store (ths/setup-store file)
|
||||||
|
;; ==== Action
|
||||||
|
|
||||||
|
|
||||||
|
;; For each main and first level copy:
|
||||||
|
;; - Duplicate it two times with copy-paste.
|
||||||
|
events
|
||||||
|
(concat
|
||||||
|
(duplicate-each-main-and-first-level-copy file)
|
||||||
|
;; - Change color of Simple1
|
||||||
|
(set-color-bottom-shape :frame-simple-1 file {:color "#111111"}))]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-store new-state)]
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#111111") 18)))))))))
|
||||||
|
|
||||||
|
(t/deftest main-and-first-level-copy-2
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(with-redefs [uuid/next cthi/next-uuid]
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (setup-file)
|
||||||
|
store (ths/setup-store file)
|
||||||
|
;; ==== Action
|
||||||
|
|
||||||
|
|
||||||
|
;; For each main and first level copy:
|
||||||
|
;; - Duplicate it two times with copy-paste.
|
||||||
|
events
|
||||||
|
(concat
|
||||||
|
(duplicate-each-main-and-first-level-copy file)
|
||||||
|
;; - Change color of Simple1
|
||||||
|
(set-color-bottom-shape :frame-simple-1 file {:color "#111111"})
|
||||||
|
;; - Change color of the nearest main and check propagation to duplicated.
|
||||||
|
(set-color-bottom-shape :frame-composed-1 file {:color "#222222"}))]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-store new-state)]
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#222222") 15)))))))))
|
||||||
|
|
||||||
|
(t/deftest main-and-first-level-copy-3
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(with-redefs [uuid/next cthi/next-uuid]
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (setup-file)
|
||||||
|
store (ths/setup-store file)
|
||||||
|
;; ==== Action
|
||||||
|
|
||||||
|
|
||||||
|
;; For each main and first level copy:
|
||||||
|
;; - Duplicate it two times with copy-paste.
|
||||||
|
events
|
||||||
|
(concat
|
||||||
|
(duplicate-each-main-and-first-level-copy file)
|
||||||
|
;; - Change color of Simple1
|
||||||
|
(set-color-bottom-shape :frame-simple-1 file {:color "#111111"})
|
||||||
|
;; - Change color of the nearest main and check propagation to duplicated.
|
||||||
|
(set-color-bottom-shape :frame-composed-1 file {:color "#222222"})
|
||||||
|
(set-color-bottom-shape :frame-composed-2 file {:color "#333333"}))]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-store new-state)]
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#333333") 12)))))))))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest main-and-first-level-copy-4
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(with-redefs [uuid/next cthi/next-uuid]
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (setup-file)
|
||||||
|
store (ths/setup-store file)
|
||||||
|
;; ==== Action
|
||||||
|
|
||||||
|
|
||||||
|
;; For each main and first level copy:
|
||||||
|
;; - Duplicate it two times with copy-paste.
|
||||||
|
events
|
||||||
|
(concat
|
||||||
|
(duplicate-each-main-and-first-level-copy file)
|
||||||
|
;; - Change color of Simple1
|
||||||
|
(set-color-bottom-shape :frame-simple-1 file {:color "#111111"})
|
||||||
|
;; - Change color of the nearest main and check propagation to duplicated.
|
||||||
|
(set-color-bottom-shape :frame-composed-1 file {:color "#222222"})
|
||||||
|
(set-color-bottom-shape :frame-composed-2 file {:color "#333333"})
|
||||||
|
(set-color-bottom-shape :frame-composed-3 file {:color "#444444"}))]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-store new-state)]
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#444444") 6)))))))))
|
||||||
|
|
||||||
|
(t/deftest copy-nested-in-main-1
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(with-redefs [uuid/next cthi/next-uuid]
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (setup-file)
|
||||||
|
store (ths/setup-store file)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
;; For each copy of Simple1 nested in a main, and the group inside Composed3 main:
|
||||||
|
;; - Duplicate it two times, keeping the duplicated inside the same main.
|
||||||
|
events
|
||||||
|
(concat
|
||||||
|
(duplicate-simple-nested-in-main-and-group file)
|
||||||
|
;; - Change color of Simple1
|
||||||
|
(set-color-bottom-shape :frame-simple-1 file {:color "#111111"}))]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-store new-state)]
|
||||||
|
;; Check propagation to all copies.
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#111111") 28)))))))))
|
||||||
|
|
||||||
|
(t/deftest copy-nested-in-main-2
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(with-redefs [uuid/next cthi/next-uuid]
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (setup-file)
|
||||||
|
store (ths/setup-store file)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
;; For each copy of Simple1 nested in a main, and the group inside Composed3 main:
|
||||||
|
;; - Duplicate it two times, keeping the duplicated inside the same main.
|
||||||
|
events
|
||||||
|
(concat
|
||||||
|
(duplicate-simple-nested-in-main-and-group file)
|
||||||
|
;; - Change color of the nearest main
|
||||||
|
(set-color-bottom-shape :frame-composed-1 file {:color "#222222"}))]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-store new-state)]
|
||||||
|
;; Check propagation to duplicated.
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#222222") 9)))))))))
|
||||||
|
|
||||||
|
(t/deftest copy-nested-in-main-3
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(with-redefs [uuid/next cthi/next-uuid]
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (setup-file)
|
||||||
|
store (ths/setup-store file)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
;; For each copy of Simple1 nested in a main, and the group inside Composed3 main:
|
||||||
|
;; - Duplicate it two times, keeping the duplicated inside the same main.
|
||||||
|
events
|
||||||
|
(concat
|
||||||
|
(duplicate-simple-nested-in-main-and-group file)
|
||||||
|
;; - Change color of the copy you duplicated from.
|
||||||
|
(set-color-bottom-shape :group-3 file {:color "#333333"}))]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-store new-state)]
|
||||||
|
;; Check that it's NOT PROPAGATED.
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#333333") 2)))))))))
|
||||||
|
|
||||||
|
(t/deftest copy-nested-1
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(with-redefs [uuid/next cthi/next-uuid]
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (setup-file)
|
||||||
|
store (ths/setup-store file)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3
|
||||||
|
;; main and copy:
|
||||||
|
;; - Duplicate it two times, moving the duplicates out of the main.
|
||||||
|
events
|
||||||
|
(concat
|
||||||
|
(duplicate-copy-nested-and-group-out-of-the-main file)
|
||||||
|
;; - Change color of Simple1
|
||||||
|
(set-color-bottom-shape :frame-simple-1 file {:color "#111111"}))]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-store new-state)]
|
||||||
|
;; Check propagation to all copies.
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#111111") 20)))))))))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest copy-nested-2
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(with-redefs [uuid/next cthi/next-uuid]
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (setup-file)
|
||||||
|
store (ths/setup-store file)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3
|
||||||
|
;; main and copy:
|
||||||
|
;; - Duplicate it two times, moving the duplicates out of the main.
|
||||||
|
events
|
||||||
|
(concat
|
||||||
|
(duplicate-copy-nested-and-group-out-of-the-main file)
|
||||||
|
;; - Change color of Simple1
|
||||||
|
(set-color-bottom-shape :frame-simple-1 file {:color "#111111"})
|
||||||
|
;; - Change color of the previous main
|
||||||
|
(set-color-bottom-shape :frame-composed-1 file {:color "#222222"})
|
||||||
|
(set-color-bottom-shape :group-3 file {:color "#333333"}))]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (ths/get-file-from-store new-state)]
|
||||||
|
;; Check that it's NOT PROPAGATED.
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#111111") 11))
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#222222") 7))
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#333333") 2)))))))))
|
||||||
|
|
||||||
|
|
||||||
|
(t/deftest copy-nested-3
|
||||||
|
(t/async
|
||||||
|
done
|
||||||
|
(with-redefs [uuid/next cthi/next-uuid]
|
||||||
|
(let [;; ==== Setup
|
||||||
|
file (setup-file)
|
||||||
|
store (ths/setup-store file)
|
||||||
|
|
||||||
|
;; ==== Action
|
||||||
|
;; For each copy of Simple1 nested in a main or other copy, and the group inside Composed3
|
||||||
|
;; main and copy:
|
||||||
|
;; - Duplicate it two times, moving the duplicates to another page
|
||||||
|
events
|
||||||
|
(concat
|
||||||
|
(duplicate-copy-nested-and-group-out-of-the-main file :target-page-label :page-2)
|
||||||
|
;; - Change color of Simple1
|
||||||
|
(set-color-bottom-shape :frame-simple-1 file {:color "#111111"})
|
||||||
|
;; - Change color of the previous main
|
||||||
|
(set-color-bottom-shape :frame-composed-1 file {:color "#222222"})
|
||||||
|
(set-color-bottom-shape :group-3 file {:color "#333333"}))]
|
||||||
|
|
||||||
|
(ths/run-store
|
||||||
|
store done events
|
||||||
|
(fn [new-state]
|
||||||
|
(let [file' (-> (ths/get-file-from-store new-state)
|
||||||
|
(cthf/switch-to-page :page-2))]
|
||||||
|
;; Check that it's NOT PROPAGATED.
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#111111") 10))
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#222222") 4))
|
||||||
|
(t/is (= (count-shapes file' "rect-simple-1" "#333333") 0)))))))))
|
|
@ -392,7 +392,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Lêers wat by biblioteke gevoeg is, sal hier verskyn. Probeer om jou lêers "
|
"Lêers wat by biblioteke gevoeg is, sal hier verskyn. Probeer om jou lêers "
|
||||||
"te deel of voeg by vanaf ons [Biblioteke en "
|
"te deel of voeg by vanaf ons [Biblioteke en "
|
||||||
"sjablone](https://penpot.app/libraries-templates.html)."
|
"sjablone](https://penpot.app/libraries-templates)."
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Laai %s Penpot lêers (.penpot) af"
|
msgstr "Laai %s Penpot lêers (.penpot) af"
|
||||||
|
|
|
@ -301,7 +301,7 @@ msgstr "تكرير %s الملفات"
|
||||||
msgid "dashboard.empty-placeholder-drafts"
|
msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"أوه لا! ليس لديك ملفات بعد! إذا كنت تريد تجربة بعض القوالب ، فانتقل إلى "
|
"أوه لا! ليس لديك ملفات بعد! إذا كنت تريد تجربة بعض القوالب ، فانتقل إلى "
|
||||||
"[المكتبات والقوالب] (https://penpot.app/libraries-templates.html)"
|
"[المكتبات والقوالب] (https://penpot.app/libraries-templates)"
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "تنزيل ملفات ٪s Penpot (.penpot)"
|
msgstr "تنزيل ملفات ٪s Penpot (.penpot)"
|
||||||
|
|
|
@ -307,7 +307,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Encara no hi ha fitxers. Si voleu provar algunes plantilles, podeu anar a "
|
"Encara no hi ha fitxers. Si voleu provar algunes plantilles, podeu anar a "
|
||||||
"la secció [Biblioteques i "
|
"la secció [Biblioteques i "
|
||||||
"plantilles](https://penpot.app/libraries-templates.html)"
|
"plantilles](https://penpot.app/libraries-templates)"
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Baixa %s fitxers Penpot (.penpot)"
|
msgstr "Baixa %s fitxers Penpot (.penpot)"
|
||||||
|
|
|
@ -406,7 +406,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Zde se zobrazí soubory přidané do knihoven. Zkuste své soubory sdílet nebo "
|
"Zde se zobrazí soubory přidané do knihoven. Zkuste své soubory sdílet nebo "
|
||||||
"je přidat z našich [Libraries & "
|
"je přidat z našich [Libraries & "
|
||||||
"templates](https://penpot.app/libraries-templates.html)."
|
"templates](https://penpot.app/libraries-templates)."
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Stáhnout soubory %s Penpot (.penpot)"
|
msgstr "Stáhnout soubory %s Penpot (.penpot)"
|
||||||
|
|
|
@ -408,7 +408,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Files added to Libraries will appear here. Try sharing your files or add "
|
"Files added to Libraries will appear here. Try sharing your files or add "
|
||||||
"from our [Libraries & "
|
"from our [Libraries & "
|
||||||
"templates](https://penpot.app/libraries-templates.html)."
|
"templates](https://penpot.app/libraries-templates)."
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Download %s Penpot files (.penpot)"
|
msgstr "Download %s Penpot files (.penpot)"
|
||||||
|
@ -5205,3 +5205,6 @@ msgstr "Plugins"
|
||||||
|
|
||||||
msgid "workspace.plugins.menu.plugins-manager"
|
msgid "workspace.plugins.menu.plugins-manager"
|
||||||
msgstr "Plugins manager"
|
msgstr "Plugins manager"
|
||||||
|
|
||||||
|
msgid "workspace.plugins.plugin-list-link"
|
||||||
|
msgstr "Plugins List"
|
||||||
|
|
|
@ -416,7 +416,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Los archivos agregados a las bibliotecas aparecerán aquí. Si quieres probar "
|
"Los archivos agregados a las bibliotecas aparecerán aquí. Si quieres probar "
|
||||||
"con alguna plantilla ve a [Bibliotecas y "
|
"con alguna plantilla ve a [Bibliotecas y "
|
||||||
"plantillas](https://penpot.app/libraries-templates.html)."
|
"plantillas](https://penpot.app/libraries-templates)."
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Descargar %s archivos Penpot (.penpot)"
|
msgstr "Descargar %s archivos Penpot (.penpot)"
|
||||||
|
@ -5332,3 +5332,5 @@ msgstr "Extensiones"
|
||||||
msgid "workspace.plugins.menu.plugins-manager"
|
msgid "workspace.plugins.menu.plugins-manager"
|
||||||
msgstr "Gestor de extensiones"
|
msgstr "Gestor de extensiones"
|
||||||
|
|
||||||
|
msgid "workspace.plugins.plugin-list-link"
|
||||||
|
msgstr "Lista de extensiones"
|
||||||
|
|
|
@ -404,7 +404,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Los archivos agregados a las Bibliotecas aparecerán aquí. Intente compartir "
|
"Los archivos agregados a las Bibliotecas aparecerán aquí. Intente compartir "
|
||||||
"sus archivos o agréguelos desde nuestras [Libraries & "
|
"sus archivos o agréguelos desde nuestras [Libraries & "
|
||||||
"templates](https://penpot.app/libraries-templates.html)."
|
"templates](https://penpot.app/libraries-templates)."
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Descargar %s archivos Penpot (.penpot)"
|
msgstr "Descargar %s archivos Penpot (.penpot)"
|
||||||
|
|
|
@ -305,7 +305,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Oh ez! Oraindik ez duzu fitxategirik! Txantiloi batekin proba egin nahi "
|
"Oh ez! Oraindik ez duzu fitxategirik! Txantiloi batekin proba egin nahi "
|
||||||
"baduzu joan [Liburutegi eta "
|
"baduzu joan [Liburutegi eta "
|
||||||
"txantiloiak](https://penpot.app/libraries-templates.html) atalera."
|
"txantiloiak](https://penpot.app/libraries-templates) atalera."
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Deskargatu %s Penpot fitxategi (.penpot)"
|
msgstr "Deskargatu %s Penpot fitxategi (.penpot)"
|
||||||
|
|
|
@ -304,7 +304,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"وای نه! شما هنوز هیچ فایلی ندارید! اگر میخواهید چند الگو را امتحان کنید، "
|
"وای نه! شما هنوز هیچ فایلی ندارید! اگر میخواهید چند الگو را امتحان کنید، "
|
||||||
"به [کتابخانهها و الگوها] بروید "
|
"به [کتابخانهها و الگوها] بروید "
|
||||||
"(https://penpot.app/libraries-templates.html)"
|
"(https://penpot.app/libraries-templates)"
|
||||||
|
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
|
|
|
@ -294,7 +294,7 @@ msgstr "Tvítak %s fílur"
|
||||||
msgid "dashboard.empty-placeholder-drafts"
|
msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Áh nei! Tú hevur ongar fílur enn! Um tú vilt royna við nøkrum skapilónum, "
|
"Áh nei! Tú hevur ongar fílur enn! Um tú vilt royna við nøkrum skapilónum, "
|
||||||
"vitja [Libraries & templates](https://penpot.app/libraries-templates.html)"
|
"vitja [Libraries & templates](https://penpot.app/libraries-templates)"
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Heinta %s Penpot fílur (.penpot)"
|
msgstr "Heinta %s Penpot fílur (.penpot)"
|
||||||
|
|
|
@ -397,7 +397,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Oh non ! Vous n'avez pas encore de fichiers ! Si vous voulez essayer avec "
|
"Oh non ! Vous n'avez pas encore de fichiers ! Si vous voulez essayer avec "
|
||||||
"des modèles, allez sur [Bibliothèques et modèles] "
|
"des modèles, allez sur [Bibliothèques et modèles] "
|
||||||
"(https://penpot.app/libraries-templates.html)."
|
"(https://penpot.app/libraries-templates)."
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Télécharger %s fichiers Penpot (.penpot)"
|
msgstr "Télécharger %s fichiers Penpot (.penpot)"
|
||||||
|
|
|
@ -302,7 +302,7 @@ msgstr "Duplicar % ficheiros"
|
||||||
msgid "dashboard.empty-placeholder-drafts"
|
msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Ai non! Ainda non tes ficheiros! Se queres facer a proba con algún modelo "
|
"Ai non! Ainda non tes ficheiros! Se queres facer a proba con algún modelo "
|
||||||
"vai a [Bibliotecas e modelos] (https://penpot.app/libraries-templates.html)"
|
"vai a [Bibliotecas e modelos] (https://penpot.app/libraries-templates)"
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Descargar %s ficheiros Penpot (.penpot)"
|
msgstr "Descargar %s ficheiros Penpot (.penpot)"
|
||||||
|
|
|
@ -402,7 +402,7 @@ msgstr "שכפול %s קבצים"
|
||||||
msgid "dashboard.empty-placeholder-drafts"
|
msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"קבצים שנוספו לספריות יתווספו לכאן. כדאי לנסות לשתף את הקבצים שלך או להוסיף "
|
"קבצים שנוספו לספריות יתווספו לכאן. כדאי לנסות לשתף את הקבצים שלך או להוסיף "
|
||||||
"אותם מ[הספריות והתבניות](https://penpot.app/libraries-templates.html)."
|
"אותם מ[הספריות והתבניות](https://penpot.app/libraries-templates)."
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "הורדת %s קובצי Penpot (.penpot)"
|
msgstr "הורדת %s קובצי Penpot (.penpot)"
|
||||||
|
|
|
@ -304,7 +304,7 @@ msgstr "Kopiraj %s datoteka"
|
||||||
msgid "dashboard.empty-placeholder-drafts"
|
msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"O ne! Još nemaš datoteka! Ako želiš isprobati neke predloške, idi na "
|
"O ne! Još nemaš datoteka! Ako želiš isprobati neke predloške, idi na "
|
||||||
"[Biblioteke i predlošci](https://penpot.app/libraries-templates.html)"
|
"[Biblioteke i predlošci](https://penpot.app/libraries-templates)"
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Preuzmi %s Penpot datoteke (.penpot)"
|
msgstr "Preuzmi %s Penpot datoteke (.penpot)"
|
||||||
|
|
|
@ -404,7 +404,7 @@ msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Berkas yang ditambahkan ke Pustaka akan muncul di sini. Coba membagikan "
|
"Berkas yang ditambahkan ke Pustaka akan muncul di sini. Coba membagikan "
|
||||||
"berkas Anda atau menambahkan dari [Pustaka & "
|
"berkas Anda atau menambahkan dari [Pustaka & "
|
||||||
"templat](https://penpot.app/libraries-templates.html) kami."
|
"templat](https://penpot.app/libraries-templates) kami."
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Unduh %s berkas Penpot (.penpot)"
|
msgstr "Unduh %s berkas Penpot (.penpot)"
|
||||||
|
|
|
@ -300,7 +300,7 @@ msgstr "Duplicare %s file"
|
||||||
msgid "dashboard.empty-placeholder-drafts"
|
msgid "dashboard.empty-placeholder-drafts"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Oh no! Non hai ancora nessun file! Se desideri provare alcuni template vai "
|
"Oh no! Non hai ancora nessun file! Se desideri provare alcuni template vai "
|
||||||
"su [Librerie e template](https://penpot.app/libraries-templates.html)"
|
"su [Librerie e template](https://penpot.app/libraries-templates)"
|
||||||
|
|
||||||
msgid "dashboard.export-binary-multi"
|
msgid "dashboard.export-binary-multi"
|
||||||
msgstr "Scarica %s file Penpot (.penpot)"
|
msgstr "Scarica %s file Penpot (.penpot)"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue