Compare commits
35 Commits
2.11.0-RC5
...
eva-bugfix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e2bd5c38c | ||
|
|
24264e7d8a | ||
|
|
5b77df997b | ||
|
|
968274096d | ||
|
|
392e3ac34e | ||
|
|
e8336a401e | ||
|
|
18048a4b2e | ||
|
|
e28d4eaff1 | ||
|
|
fae574796f | ||
|
|
db59209b21 | ||
|
|
a62f1fb46f | ||
|
|
ffd2aa03a9 | ||
|
|
f1ebcaf635 | ||
|
|
6a4d0f05bc | ||
|
|
4d751c5acd | ||
|
|
9a5efe8671 | ||
|
|
1f65e2f560 | ||
|
|
bf6874a96d | ||
|
|
3c05067c99 | ||
|
|
bbb78904fb | ||
|
|
12e91751c3 | ||
|
|
0f0c8466be | ||
|
|
204d0dfb9d | ||
|
|
e4a3fc3940 | ||
|
|
29dc99deae | ||
|
|
025f0d2fdb | ||
|
|
045aa7c788 | ||
|
|
0f3ca67773 | ||
|
|
1c06c87acf | ||
|
|
d532558bab | ||
|
|
71ed845307 | ||
|
|
dd35c82824 | ||
|
|
253605f6cc | ||
|
|
2548bec651 | ||
|
|
da5da00bd4 |
4
.github/workflows/build-bundle.yml
vendored
@@ -57,6 +57,7 @@ jobs:
|
||||
id: vars
|
||||
run: |
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
echo "bundle_version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build bundle
|
||||
env:
|
||||
@@ -76,7 +77,7 @@ jobs:
|
||||
|
||||
- name: Upload Penpot bundle to S3
|
||||
run: |
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip
|
||||
aws s3 cp zips/penpot.zip s3://${{ secrets.S3_BUCKET }}/penpot-${{ steps.vars.outputs.gh_ref }}.zip --metadata bundle-version=${{ steps.vars.outputs.bundle_version }}
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
@@ -86,4 +87,5 @@ jobs:
|
||||
TEXT: |
|
||||
❌ *[PENPOT] Error during the execution of the job*
|
||||
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
|
||||
Bundle version: `${{ steps.vars.outputs.bundle_version }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
27
.github/workflows/build-docker.yml
vendored
@@ -46,7 +46,6 @@ jobs:
|
||||
mv penpot/backend bundle-backend
|
||||
mv penpot/frontend bundle-frontend
|
||||
mv penpot/exporter bundle-exporter
|
||||
mv penpot/storybook bundle-storybook
|
||||
popd
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
@@ -100,29 +99,3 @@ jobs:
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
|
||||
- name: Build and push Storybook Docker image
|
||||
uses: docker/build-push-action@v6
|
||||
env:
|
||||
DOCKER_IMAGE: 'storybook'
|
||||
BUNDLE_PATH: './bundle-storybook'
|
||||
with:
|
||||
context: ./docker/images/
|
||||
file: ./docker/images/Dockerfile.storybook
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:${{ steps.vars.outputs.gh_ref }}
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_REGISTRY }}/${{ env.DOCKER_IMAGE }}:buildcache,mode=max
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
❌ 🐳 *[PENPOT] Error building penpot docker images.*
|
||||
📄 Triggered from ref: `${{ steps.vars.outputs.gh_ref }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
|
||||
43
CHANGES.md
@@ -1,5 +1,20 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 2.12.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix pan cursor not disabling viewport guides [Github #6985](https://github.com/penpot/penpot/issues/6985)
|
||||
- Fix viewport resize on locked shapes [Taiga #11974](https://tree.taiga.io/project/penpot/issue/11974)
|
||||
|
||||
## 2.11.0 (Unreleased)
|
||||
|
||||
### :boom: Breaking changes & Deprecations
|
||||
@@ -41,7 +56,6 @@
|
||||
- Alternative ways of creating variants - Button Viewport [Taiga #11931](https://tree.taiga.io/project/penpot/us/11931)
|
||||
- Reorder properties for a component [Taiga #10225](https://tree.taiga.io/project/penpot/us/10225)
|
||||
- File Data storage layout refactor [Github #7345](https://github.com/penpot/penpot/pull/7345)
|
||||
- Make several queries optimization on comment threads [Github #7506](https://github.com/penpot/penpot/pull/7506)
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
@@ -56,27 +70,7 @@
|
||||
- Fix auto-width changes to fixed when switching variants [Taiga #12172](https://tree.taiga.io/project/penpot/issue/12172)
|
||||
- Fix component number has no singular translation string [Taiga #12106](https://tree.taiga.io/project/penpot/issue/12106)
|
||||
- Fix adding/removing identical text fills [Taiga #12287](https://tree.taiga.io/project/penpot/issue/12287)
|
||||
- Fix scroll on the inspect tab [Taiga #12293](https://tree.taiga.io/project/penpot/issue/12293)
|
||||
- Fix lock proportion tooltip [Taiga #12326](https://tree.taiga.io/project/penpot/issue/12326)
|
||||
- Fix internal Error when selecting a set by name in the token theme editor [Taiga #12310](https://tree.taiga.io/project/penpot/issue/12310)
|
||||
- Fix drag & drop functionality is swapping instead or reordering [Taiga #12254](https://tree.taiga.io/project/penpot/issue/12254)
|
||||
- Fix variants not syncronizing tokens on switch [Taiga #12290](https://tree.taiga.io/project/penpot/issue/12290)
|
||||
- Fix incorrect behavior of Alt + Drag for variants [Taiga #12309](https://tree.taiga.io/project/penpot/issue/12309)
|
||||
- Fix text override is lost after switch [Taiga #12269](https://tree.taiga.io/project/penpot/issue/12269)
|
||||
- Fix exporting a board crashing the app [Taiga #12384](https://tree.taiga.io/project/penpot/issue/12384)
|
||||
- Fix nested variant in a component doesn't keep inherited overrides [Taiga #12299](https://tree.taiga.io/project/penpot/issue/12299)
|
||||
- Fix selected colors not showing colors from children shapes in multiple selection [Taiga #12384](https://tree.taiga.io/project/penpot/issue/12385)
|
||||
- Fix scrollbar issue in design tab [Taiga #12367](https://tree.taiga.io/project/penpot/issue/12367)
|
||||
- Fix library update notificacions showing when they should not [Taiga #12397](https://tree.taiga.io/project/penpot/issue/12397)
|
||||
- Fix remove flex button doesn’t work within variant [Taiga #12314](https://tree.taiga.io/project/penpot/issue/12314)
|
||||
- Fix an error translation [Taiga #12402](https://tree.taiga.io/project/penpot/issue/12402)
|
||||
- Fix problem with certain text input in some editable labels (pages, components, tokens...) being in conflict with the drag/drop functionality [Taiga #12316](https://tree.taiga.io/project/penpot/issue/12316)
|
||||
- Fix not controlled theme renaming [Taiga #12411](https://tree.taiga.io/project/penpot/issue/12411)
|
||||
- Fix paste without selection sends the new element in the back [Taiga #12382](https://tree.taiga.io/project/penpot/issue/12382)
|
||||
- Fix options button does not work for comments created in the lower part of the screen [Taiga #12422](https://tree.taiga.io/project/penpot/issue/12422)
|
||||
- Fix problem when checking usage with removed teams [Taiga #12442](https://tree.taiga.io/project/penpot/issue/12442)
|
||||
- Fix focus mode persisting across page/file navigation [Taiga #12469](https://tree.taiga.io/project/penpot/issue/12469)
|
||||
- Fix shadow color validation [Github #7705](https://github.com/penpot/penpot/pull/7705)
|
||||
|
||||
|
||||
## 2.10.1
|
||||
|
||||
@@ -84,10 +78,12 @@
|
||||
|
||||
- Improve workpace file loading [Github 7366](https://github.com/penpot/penpot/pull/7366)
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Fix regression with text shapes creation with Plugins API [Taiga #12244](https://tree.taiga.io/project/penpot/issue/12244)
|
||||
|
||||
|
||||
## 2.10.0
|
||||
|
||||
### :rocket: Epics and highlights
|
||||
@@ -103,7 +99,7 @@
|
||||
- Add efficiency enhancements to right sidebar [Github #7182](https://github.com/penpot/penpot/pull/7182)
|
||||
- Add defaults for artboard drawing [Taiga #494](https://tree.taiga.io/project/penpot/us/494?milestone=465047)
|
||||
- Continuous display of distances between elements when moving a layer with the keyboard [Taiga #1780](https://tree.taiga.io/project/penpot/us/1780)
|
||||
- New Number token - unitless values [Taiga #10936](https://tree.taiga.io/project/penpot/us/10936)
|
||||
- New Number token - unitless values [Taiga #10936](https://tree.taiga.io/project/penpot/us/10936)
|
||||
- New font-family token [Taiga #10937](https://tree.taiga.io/project/penpot/us/10937)
|
||||
- New text case token [Taiga #10942](https://tree.taiga.io/project/penpot/us/10942)
|
||||
- New text-decoration token [Taiga #10941](https://tree.taiga.io/project/penpot/us/10941)
|
||||
@@ -184,6 +180,7 @@
|
||||
- Add info to apply-token event [Taiga #11710](https://tree.taiga.io/project/penpot/task/11710)
|
||||
- Fix double click on set name input [Taiga #11747](https://tree.taiga.io/project/penpot/issue/11747)
|
||||
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Copying font size does not copy the unit [Taiga #11143](https://tree.taiga.io/project/penpot/issue/11143)
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
com.google.guava/guava {:mvn/version "33.4.8-jre"}
|
||||
|
||||
funcool/yetti
|
||||
{:git/tag "v11.8"
|
||||
:git/sha "1d1b33f"
|
||||
{:git/tag "v11.6"
|
||||
:git/sha "94dc017"
|
||||
:git/url "https://github.com/funcool/yetti.git"
|
||||
:exclusions [org.slf4j/slf4j-api]}
|
||||
|
||||
|
||||
@@ -550,7 +550,7 @@
|
||||
[cfg data file-id]
|
||||
(let [library-ids (get-libraries cfg [file-id])]
|
||||
(reduce (fn [data library-id]
|
||||
(if-let [library (get-file cfg library-id :include-deleted? true)]
|
||||
(if-let [library (get-file cfg library-id)]
|
||||
(ctf/absorb-assets data (:data library))
|
||||
data))
|
||||
data
|
||||
@@ -749,7 +749,7 @@
|
||||
l.version
|
||||
FROM libs AS l
|
||||
INNER JOIN project AS p ON (p.id = l.project_id)
|
||||
WHERE l.deleted_at IS NULL;")
|
||||
WHERE l.deleted_at IS NULL OR l.deleted_at > now();")
|
||||
|
||||
(defn get-file-libraries
|
||||
[conn file-id]
|
||||
|
||||
@@ -228,7 +228,6 @@
|
||||
(db/tx-run! cfg (fn [cfg]
|
||||
(cond-> (bfc/get-file cfg file-id
|
||||
{:realize? true
|
||||
:include-deleted? true
|
||||
:lock-for-update? true})
|
||||
detach?
|
||||
(-> (ctf/detach-external-references file-id)
|
||||
@@ -286,12 +285,14 @@
|
||||
|
||||
(let [file (cond-> (select-keys file bfc/file-attrs)
|
||||
(:options data)
|
||||
(assoc :options (:options data)))
|
||||
(assoc :options (:options data))
|
||||
|
||||
file (-> file
|
||||
(dissoc :data)
|
||||
(dissoc :deleted-at)
|
||||
(encode-file))
|
||||
:always
|
||||
(dissoc :data))
|
||||
|
||||
file (cond-> file
|
||||
:always
|
||||
(encode-file))
|
||||
|
||||
path (str "files/" file-id ".json")]
|
||||
(write-entry! output path file))
|
||||
|
||||
@@ -319,9 +319,5 @@
|
||||
([key default]
|
||||
(c/get config key default)))
|
||||
|
||||
(defn logging-context
|
||||
[]
|
||||
{:version/backend (:full version)})
|
||||
|
||||
;; Set value for all new threads bindings.
|
||||
(alter-var-root #'*assert* (constantly (contains? flags :backend-asserts)))
|
||||
|
||||
@@ -25,14 +25,15 @@
|
||||
(let [claims (-> {}
|
||||
(into (::session/token-claims request))
|
||||
(into (::actoken/token-claims request)))]
|
||||
(-> (cf/logging-context)
|
||||
(assoc :request/path (:path request))
|
||||
(assoc :request/method (:method request))
|
||||
(assoc :request/params (:params request))
|
||||
(assoc :request/user-agent (yreq/get-header request "user-agent"))
|
||||
(assoc :request/ip-addr (inet/parse-request request))
|
||||
(assoc :request/profile-id (:uid claims))
|
||||
(assoc :version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")))))
|
||||
{:request/path (:path request)
|
||||
:request/method (:method request)
|
||||
:request/params (:params request)
|
||||
:request/user-agent (yreq/get-header request "user-agent")
|
||||
:request/ip-addr (inet/parse-request request)
|
||||
:request/profile-id (:uid claims)
|
||||
:version/frontend (or (yreq/get-header request "x-frontend-version") "unknown")
|
||||
:version/backend (:full cf/version)}))
|
||||
|
||||
|
||||
(defmulti handle-error
|
||||
(fn [cause _ _]
|
||||
|
||||
@@ -64,13 +64,9 @@
|
||||
(let [mdata (meta result)
|
||||
response (if (fn? result)
|
||||
(result request)
|
||||
(let [result (rph/unwrap result)
|
||||
status (::http/status mdata 200)
|
||||
headers (cond-> (::http/headers mdata {})
|
||||
(yres/stream-body? result)
|
||||
(assoc "content-type" "application/octet-stream"))]
|
||||
{::yres/status status
|
||||
::yres/headers headers
|
||||
(let [result (rph/unwrap result)]
|
||||
{::yres/status (::http/status mdata 200)
|
||||
::yres/headers (::http/headers mdata {})
|
||||
::yres/body result}))]
|
||||
(-> response
|
||||
(handle-response-transformation request mdata)
|
||||
|
||||
@@ -25,10 +25,10 @@
|
||||
[app.rpc.commands.projects :as projects]
|
||||
[app.rpc.commands.teams :as teams]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.tasks.file-gc]
|
||||
[app.util.services :as sv]
|
||||
[app.worker :as-alias wrk]))
|
||||
[app.worker :as-alias wrk]
|
||||
[yetti.response :as yres]))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
(defn stream-export-v1
|
||||
[cfg {:keys [file-id include-libraries embed-assets] :as params}]
|
||||
(rph/stream
|
||||
(yres/stream-body
|
||||
(fn [_ output-stream]
|
||||
(try
|
||||
(-> cfg
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
(defn stream-export-v3
|
||||
[cfg {:keys [file-id include-libraries embed-assets] :as params}]
|
||||
(rph/stream
|
||||
(yres/stream-body
|
||||
(fn [_ output-stream]
|
||||
(try
|
||||
(-> cfg
|
||||
@@ -79,11 +79,16 @@
|
||||
::sm/params schema:export-binfile}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id version file-id] :as params}]
|
||||
(files/check-read-permissions! pool profile-id file-id)
|
||||
(let [version (or version 1)]
|
||||
(case (int version)
|
||||
1 (stream-export-v1 cfg params)
|
||||
2 (throw (ex-info "not-implemented" {}))
|
||||
3 (stream-export-v3 cfg params))))
|
||||
(fn [_]
|
||||
(let [version (or version 1)
|
||||
body (case (int version)
|
||||
1 (stream-export-v1 cfg params)
|
||||
2 (throw (ex-info "not-implemented" {}))
|
||||
3 (stream-export-v3 cfg params))]
|
||||
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "application/octet-stream"}
|
||||
::yres/body body})))
|
||||
|
||||
;; --- Command: import-binfile
|
||||
|
||||
|
||||
@@ -234,39 +234,36 @@
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(get-comment-threads conn profile-id file-id))))
|
||||
|
||||
(defn- get-comment-threads-sql
|
||||
[where]
|
||||
(str/ffmt
|
||||
"SELECT DISTINCT ON (ct.id)
|
||||
ct.*,
|
||||
pf.fullname AS owner_fullname,
|
||||
pf.email AS owner_email,
|
||||
pf.photo_id AS owner_photo_id,
|
||||
p.team_id AS team_id,
|
||||
f.name AS file_name,
|
||||
f.project_id AS project_id,
|
||||
first_value(c.content) OVER w AS content,
|
||||
(SELECT count(1)
|
||||
FROM comment AS c
|
||||
WHERE c.thread_id = ct.id) AS count_comments,
|
||||
(SELECT count(1)
|
||||
FROM comment AS c
|
||||
WHERE c.thread_id = ct.id
|
||||
AND c.created_at >= coalesce(cts.modified_at, ct.created_at)) AS count_unread_comments
|
||||
FROM comment_thread AS ct
|
||||
INNER JOIN comment AS c ON (c.thread_id = ct.id)
|
||||
INNER JOIN file AS f ON (f.id = ct.file_id)
|
||||
INNER JOIN project AS p ON (p.id = f.project_id)
|
||||
LEFT JOIN comment_thread_status AS cts ON (cts.thread_id = ct.id AND cts.profile_id = ?)
|
||||
LEFT JOIN profile AS pf ON (ct.owner_id = pf.id)
|
||||
WHERE f.deleted_at IS NULL
|
||||
AND p.deleted_at IS NULL
|
||||
%1
|
||||
WINDOW w AS (PARTITION BY c.thread_id ORDER BY c.created_at ASC)"
|
||||
where))
|
||||
(def ^:private sql:comment-threads
|
||||
"SELECT DISTINCT ON (ct.id)
|
||||
ct.*,
|
||||
pf.fullname AS owner_fullname,
|
||||
pf.email AS owner_email,
|
||||
pf.photo_id AS owner_photo_id,
|
||||
p.team_id AS team_id,
|
||||
f.name AS file_name,
|
||||
f.project_id AS project_id,
|
||||
first_value(c.content) OVER w AS content,
|
||||
(SELECT count(1)
|
||||
FROM comment AS c
|
||||
WHERE c.thread_id = ct.id) AS count_comments,
|
||||
(SELECT count(1)
|
||||
FROM comment AS c
|
||||
WHERE c.thread_id = ct.id
|
||||
AND c.created_at >= coalesce(cts.modified_at, ct.created_at)) AS count_unread_comments
|
||||
FROM comment_thread AS ct
|
||||
INNER JOIN comment AS c ON (c.thread_id = ct.id)
|
||||
INNER JOIN file AS f ON (f.id = ct.file_id)
|
||||
INNER JOIN project AS p ON (p.id = f.project_id)
|
||||
LEFT JOIN comment_thread_status AS cts ON (cts.thread_id = ct.id AND cts.profile_id = ?)
|
||||
LEFT JOIN profile AS pf ON (ct.owner_id = pf.id)
|
||||
WHERE f.deleted_at IS NULL
|
||||
AND p.deleted_at IS NULL
|
||||
WINDOW w AS (PARTITION BY c.thread_id ORDER BY c.created_at ASC)")
|
||||
|
||||
(def ^:private sql:comment-threads-by-file-id
|
||||
(get-comment-threads-sql "AND ct.file_id = ?"))
|
||||
(str "WITH threads AS (" sql:comment-threads ")"
|
||||
"SELECT * FROM threads WHERE file_id = ?"))
|
||||
|
||||
(defn- get-comment-threads
|
||||
[conn profile-id file-id]
|
||||
@@ -276,29 +273,34 @@
|
||||
;; --- COMMAND: Get Unread Comment Threads
|
||||
|
||||
(def ^:private sql:unread-all-comment-threads-by-team
|
||||
(str "WITH threads AS ("
|
||||
(get-comment-threads-sql "AND p.team_id = ?")
|
||||
")"
|
||||
"SELECT t.* FROM threads AS t
|
||||
WHERE t.count_unread_comments > 0"))
|
||||
(str "WITH threads AS (" sql:comment-threads ")"
|
||||
"SELECT * FROM threads WHERE count_unread_comments > 0 AND team_id = ?"))
|
||||
|
||||
;; The partial configuration will retrieve only comments created by the user and
|
||||
;; threads that have a mention to the user.
|
||||
(def ^:private sql:unread-partial-comment-threads-by-team
|
||||
(str "WITH threads AS ("
|
||||
(get-comment-threads-sql "AND p.team_id = ? AND (ct.owner_id = ? OR ? = ANY(ct.mentions))")
|
||||
")"
|
||||
"SELECT t.* FROM threads AS t
|
||||
WHERE t.count_unread_comments > 0"))
|
||||
(str "WITH threads AS (" sql:comment-threads ")"
|
||||
"SELECT * FROM threads
|
||||
WHERE count_unread_comments > 0
|
||||
AND team_id = ?
|
||||
AND (owner_id = ? OR ? = ANY(mentions))"))
|
||||
|
||||
(defn- get-unread-comment-threads
|
||||
[cfg profile-id team-id]
|
||||
(let [profile (-> (db/get cfg :profile {:id profile-id} ::db/remove-deleted false)
|
||||
(let [profile (-> (db/get cfg :profile {:id profile-id})
|
||||
(profile/decode-row))
|
||||
notify (or (-> profile :props :notifications :dashboard-comments) :all)
|
||||
result (case notify
|
||||
:all (db/exec! cfg [sql:unread-all-comment-threads-by-team profile-id team-id])
|
||||
:partial (db/exec! cfg [sql:unread-partial-comment-threads-by-team profile-id team-id profile-id profile-id])
|
||||
[])]
|
||||
(into [] xf-decode-row result)))
|
||||
notify (or (-> profile :props :notifications :dashboard-comments) :all)]
|
||||
|
||||
(case notify
|
||||
:all
|
||||
(->> (db/exec! cfg [sql:unread-all-comment-threads-by-team profile-id team-id])
|
||||
(into [] xf-decode-row))
|
||||
|
||||
:partial
|
||||
(->> (db/exec! cfg [sql:unread-partial-comment-threads-by-team profile-id team-id profile-id profile-id])
|
||||
(into [] xf-decode-row))
|
||||
|
||||
[])))
|
||||
|
||||
(def ^:private
|
||||
schema:get-unread-comment-threads
|
||||
@@ -321,17 +323,16 @@
|
||||
[:id ::sm/uuid]
|
||||
[:share-id {:optional true} [:maybe ::sm/uuid]]])
|
||||
|
||||
(def ^:private sql:get-comment-thread
|
||||
(get-comment-threads-sql "AND ct.file_id = ? AND ct.id = ?"))
|
||||
|
||||
(sv/defmethod ::get-comment-thread
|
||||
{::doc/added "1.15"
|
||||
::sm/params schema:get-comment-thread}
|
||||
[cfg {:keys [::rpc/profile-id file-id id share-id] :as params}]
|
||||
(db/run! cfg (fn [{:keys [::db/conn]}]
|
||||
(files/check-comment-permissions! conn profile-id file-id share-id)
|
||||
(some-> (db/exec-one! conn [sql:get-comment-thread profile-id file-id id])
|
||||
(decode-row)))))
|
||||
(let [sql (str "WITH threads AS (" sql:comment-threads ")"
|
||||
"SELECT * FROM threads WHERE id = ? AND file_id = ?")]
|
||||
(-> (db/exec-one! conn [sql profile-id id file-id])
|
||||
(decode-row))))))
|
||||
|
||||
;; --- COMMAND: Retrieve Comments
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
params {:email email
|
||||
:fullname fullname
|
||||
:is-active true
|
||||
:is-demo true
|
||||
:deleted-at (ct/in-future (cf/get-deletion-delay))
|
||||
:password (derive-password password)
|
||||
:props {}}
|
||||
|
||||
@@ -107,9 +107,7 @@
|
||||
(defn get-profile
|
||||
"Get profile by id. Throws not-found exception if no profile found."
|
||||
[conn id & {:as opts}]
|
||||
;; NOTE: We need to set ::db/remove-deleted to false because demo profiles
|
||||
;; are created with a set deleted-at value
|
||||
(-> (db/get-by-id conn :profile id (assoc opts ::db/remove-deleted false))
|
||||
(-> (db/get-by-id conn :profile id opts)
|
||||
(decode-row)))
|
||||
|
||||
;; --- MUTATION: Update Profile (own)
|
||||
@@ -475,16 +473,13 @@
|
||||
p.fullname AS name,
|
||||
p.email AS email
|
||||
FROM team_profile_rel AS tpr1
|
||||
JOIN team as t
|
||||
ON tpr1.team_id = t.id
|
||||
JOIN team_profile_rel AS tpr2
|
||||
ON (tpr1.team_id = tpr2.team_id)
|
||||
JOIN profile AS p
|
||||
ON (tpr2.profile_id = p.id)
|
||||
WHERE tpr1.profile_id = ?
|
||||
AND tpr1.is_owner IS true
|
||||
AND tpr2.can_edit IS true
|
||||
AND t.deleted_at IS NULL")
|
||||
AND tpr2.can_edit IS true")
|
||||
|
||||
(sv/defmethod ::get-subscription-usage
|
||||
{::doc/added "2.9"}
|
||||
|
||||
@@ -37,14 +37,14 @@
|
||||
;; --- Helpers & Specs
|
||||
|
||||
(def ^:private sql:team-permissions
|
||||
"SELECT tpr.is_owner,
|
||||
"select tpr.is_owner,
|
||||
tpr.is_admin,
|
||||
tpr.can_edit
|
||||
FROM team_profile_rel AS tpr
|
||||
JOIN team AS t ON (t.id = tpr.team_id)
|
||||
WHERE tpr.profile_id = ?
|
||||
AND tpr.team_id = ?
|
||||
AND t.deleted_at IS NULL")
|
||||
from team_profile_rel as tpr
|
||||
join team as t on (t.id = tpr.team_id)
|
||||
where tpr.profile_id = ?
|
||||
and tpr.team_id = ?
|
||||
and t.deleted_at is null")
|
||||
|
||||
(defn get-permissions
|
||||
[conn profile-id team-id]
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.http :as-alias http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[yetti.response :as yres]))
|
||||
[yetti.response :as-alias yres]))
|
||||
|
||||
;; A utilty wrapper object for wrap service responses that does not
|
||||
;; implements the IObj interface that make possible attach metadata to
|
||||
@@ -78,8 +78,3 @@
|
||||
(let [exp (if (integer? max-age) max-age (inst-ms max-age))
|
||||
val (dm/fmt "max-age=%" (int (/ exp 1000.0)))]
|
||||
(update response ::yres/headers assoc "cache-control" val)))))
|
||||
|
||||
(defn stream
|
||||
"A convenience allias for yetti.response/stream-body"
|
||||
[f]
|
||||
(yres/stream-body f))
|
||||
|
||||
@@ -218,9 +218,6 @@
|
||||
(when (or (nil? revn) (= revn (:revn file)))
|
||||
file)))
|
||||
|
||||
;; FIXME: we should skip files that does not match the revn on the
|
||||
;; props and add proper schema for this task props
|
||||
|
||||
(defn- process-file!
|
||||
[cfg {:keys [file-id] :as props}]
|
||||
(if-let [file (get-file cfg props)]
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"A maintenance task that is responsible of properly scheduling the
|
||||
file-gc task for all files that matches the eligibility threshold."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
@@ -22,24 +21,25 @@
|
||||
f.modified_at
|
||||
FROM file AS f
|
||||
WHERE f.has_media_trimmed IS false
|
||||
AND f.modified_at < ?
|
||||
AND f.modified_at < now() - ?::interval
|
||||
AND f.deleted_at IS NULL
|
||||
ORDER BY f.modified_at DESC
|
||||
FOR UPDATE OF f
|
||||
SKIP LOCKED")
|
||||
|
||||
(defn- get-candidates
|
||||
[{:keys [::db/conn ::min-age] :as cfg}]
|
||||
(let [min-age (db/interval min-age)]
|
||||
(db/plan conn [sql:get-candidates min-age] {:fetch-size 10})))
|
||||
|
||||
(defn- schedule!
|
||||
[{:keys [::db/conn] :as cfg} threshold]
|
||||
[cfg]
|
||||
(let [total (reduce (fn [total {:keys [id modified-at revn]}]
|
||||
(let [params {:file-id id :revn revn}]
|
||||
(l/trc :hint "schedule"
|
||||
:file-id (str id)
|
||||
:revn revn
|
||||
:modified-at (ct/format-inst modified-at))
|
||||
(let [params {:file-id id :modified-at modified-at :revn revn}]
|
||||
(wrk/submit! (assoc cfg ::wrk/params params))
|
||||
(inc total)))
|
||||
0
|
||||
(db/plan conn [sql:get-candidates threshold] {:fetch-size 10}))]
|
||||
(get-candidates cfg))]
|
||||
{:processed total}))
|
||||
|
||||
(defmethod ig/assert-key ::handler
|
||||
@@ -53,12 +53,12 @@
|
||||
(defmethod ig/init-key ::handler
|
||||
[_ cfg]
|
||||
(fn [{:keys [props] :as task}]
|
||||
(let [threshold (-> (ct/duration (or (:min-age props) (::min-age cfg)))
|
||||
(ct/in-past))]
|
||||
(let [min-age (ct/duration (or (:min-age props) (::min-age cfg)))]
|
||||
(-> cfg
|
||||
(assoc ::db/rollback (:rollback? props))
|
||||
(assoc ::min-age min-age)
|
||||
(assoc ::wrk/task :file-gc)
|
||||
(assoc ::wrk/priority 10)
|
||||
(assoc ::wrk/mark-retries 0)
|
||||
(assoc ::wrk/delay 10000)
|
||||
(db/tx-run! schedule! threshold)))))
|
||||
(assoc ::wrk/delay 1000)
|
||||
(db/tx-run! schedule!)))))
|
||||
|
||||
@@ -77,8 +77,8 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private sql:insert-new-task
|
||||
"insert into task (id, name, props, queue, label, priority, max_retries, created_at, modified_at, scheduled_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"insert into task (id, name, props, queue, label, priority, max_retries, scheduled_at)
|
||||
values (?, ?, ?, ?, ?, ?, ?, now() + ?)
|
||||
returning id")
|
||||
|
||||
(def ^:private
|
||||
@@ -88,7 +88,7 @@
|
||||
AND queue=?
|
||||
AND label=?
|
||||
AND status = 'new'
|
||||
AND scheduled_at > ?")
|
||||
AND scheduled_at > now()")
|
||||
|
||||
(def ^:private schema:options
|
||||
[:map {:title "submit-options"}
|
||||
@@ -111,19 +111,17 @@
|
||||
|
||||
(check-options! options)
|
||||
|
||||
(let [delay (ct/duration delay)
|
||||
now (ct/now)
|
||||
scheduled-at (-> (ct/plus now delay)
|
||||
(ct/truncate :millisecond))
|
||||
props (db/tjson params)
|
||||
id (uuid/next)
|
||||
tenant (cf/get :tenant)
|
||||
task (d/name task)
|
||||
queue (str/ffmt "%:%" tenant (d/name queue))
|
||||
conn (db/get-connectable options)
|
||||
deleted (when dedupe
|
||||
(-> (db/exec-one! conn [sql:remove-not-started-tasks task queue label now])
|
||||
(db/get-update-count)))]
|
||||
(let [duration (ct/duration delay)
|
||||
interval (db/interval duration)
|
||||
props (db/tjson params)
|
||||
id (uuid/next)
|
||||
tenant (cf/get :tenant)
|
||||
task (d/name task)
|
||||
queue (str/ffmt "%:%" tenant (d/name queue))
|
||||
conn (db/get-connectable options)
|
||||
deleted (when dedupe
|
||||
(-> (db/exec-one! conn [sql:remove-not-started-tasks task queue label])
|
||||
:next.jdbc/update-count))]
|
||||
|
||||
(l/trc :hint "submit task"
|
||||
:name task
|
||||
@@ -131,13 +129,11 @@
|
||||
:queue queue
|
||||
:label label
|
||||
:dedupe (boolean dedupe)
|
||||
:delay (ct/format-duration delay)
|
||||
:delay (ct/format-duration duration)
|
||||
:replace (or deleted 0))
|
||||
|
||||
(db/exec-one! conn [sql:insert-new-task id task props queue
|
||||
label priority max-retries
|
||||
now now scheduled-at])
|
||||
|
||||
label priority max-retries interval])
|
||||
id))
|
||||
|
||||
(defn invoke!
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
[app.common.transit :as t]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.metrics :as mtx]
|
||||
[app.redis :as rds]
|
||||
@@ -61,8 +60,7 @@
|
||||
|
||||
(defn get-error-context
|
||||
[_ item]
|
||||
(-> (cf/logging-context)
|
||||
(assoc :params item)))
|
||||
{:params item})
|
||||
|
||||
(defn- get-task
|
||||
[{:keys [::db/pool]} task-id]
|
||||
@@ -133,11 +131,6 @@
|
||||
[{:keys [::id ::timeout] :as cfg} task-id scheduled-at]
|
||||
(loop [task (get-task cfg task-id)]
|
||||
(cond
|
||||
(nil? task)
|
||||
(l/wrn :hint "no task found on the database"
|
||||
:runner-id id
|
||||
:task-id task-id)
|
||||
|
||||
(ex/exception? task)
|
||||
(if (or (db/connection-error? task)
|
||||
(db/serialization-error? task))
|
||||
@@ -158,9 +151,12 @@
|
||||
(inst-ms (:scheduled-at task)))
|
||||
(l/wrn :hint "skiping task, rescheduled"
|
||||
:task-id task-id
|
||||
:runner-id id)
|
||||
|
||||
(nil? task)
|
||||
(l/wrn :hint "no task found on the database"
|
||||
:runner-id id
|
||||
:scheduled-at (ct/format-inst (:scheduled-at task))
|
||||
:expected-scheduled-at (ct/format-inst scheduled-at))
|
||||
:task-id task-id)
|
||||
|
||||
:else
|
||||
(let [result (run-task cfg task)]
|
||||
@@ -181,8 +177,7 @@
|
||||
{:error explain
|
||||
:status "retry"
|
||||
:modified-at now
|
||||
:scheduled-at (-> (ct/plus now delay)
|
||||
(ct/truncate :millisecond))
|
||||
:scheduled-at (ct/plus now delay)
|
||||
:retry-num nretry}
|
||||
{:id (:id task)})
|
||||
nil))
|
||||
@@ -218,7 +213,6 @@
|
||||
:payload payload)))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "unable to decode payload"
|
||||
::l/context (cf/logging-context)
|
||||
:payload payload
|
||||
:length (alength ^String/1 payload)
|
||||
:cause cause))))
|
||||
@@ -230,11 +224,11 @@
|
||||
"failed" (handle-task-failure result)
|
||||
"completed" (handle-task-completion result)
|
||||
(throw (IllegalArgumentException.
|
||||
(str "invalid status received: '" status "'"))))))
|
||||
(str "invalid status received: " status))))))
|
||||
|
||||
(run-task-loop [[task-id scheduled-at]]
|
||||
(loop [result (run-task! cfg task-id scheduled-at)]
|
||||
(when-let [cause (some-> result process-result)]
|
||||
(when-let [cause (process-result result)]
|
||||
(if (or (db/connection-error? cause)
|
||||
(db/serialization-error? cause))
|
||||
(do
|
||||
@@ -242,9 +236,9 @@
|
||||
:cause cause)
|
||||
(px/sleep timeout)
|
||||
(recur result))
|
||||
(l/err :hint "unhandled exception on processing task result"
|
||||
::l/context (cf/logging-context)
|
||||
:cause cause)))))]
|
||||
(do
|
||||
(l/err :hint "unhandled exception on processing task result"
|
||||
:cause cause))))))]
|
||||
|
||||
(try
|
||||
(let [key (str/ffmt "penpot.worker.queue:%" queue)
|
||||
@@ -260,14 +254,11 @@
|
||||
(if (rds/timeout-exception? cause)
|
||||
(do
|
||||
(l/err :hint "redis pop operation timeout, consider increasing redis timeout (will retry in some instants)"
|
||||
::l/context (cf/logging-context)
|
||||
:timeout timeout
|
||||
:cause cause)
|
||||
(px/sleep timeout))
|
||||
|
||||
(l/err :hint "unhandled exception"
|
||||
::l/context (cf/logging-context)
|
||||
:cause cause))))))
|
||||
(l/err :hint "unhandled exception" :cause cause))))))
|
||||
|
||||
(defn- start-thread!
|
||||
[{:keys [::id ::queue ::wrk/tenant] :as cfg}]
|
||||
@@ -293,7 +284,6 @@
|
||||
:queue queue))
|
||||
(catch Throwable cause
|
||||
(l/err :hint "unexpected exception"
|
||||
::l/context (cf/logging-context)
|
||||
:id id
|
||||
:queue queue
|
||||
:cause cause))
|
||||
|
||||
@@ -549,44 +549,6 @@
|
||||
(io/copy r sw)
|
||||
(.toString sw))))
|
||||
|
||||
(defn parse-sse
|
||||
[content]
|
||||
(let [state
|
||||
(reduce (fn [{:keys [events data event id] :as state} line]
|
||||
(cond
|
||||
;; empty line → dispatch event if we have data
|
||||
(str/blank? line)
|
||||
(if (seq data)
|
||||
(-> state
|
||||
(update :events conj {:event (or event "message")
|
||||
:data (-> (str/join "\n" data))})
|
||||
(assoc :data [] :event nil))
|
||||
state)
|
||||
|
||||
;; comment line (starts with :)
|
||||
(str/starts-with? line ":")
|
||||
state
|
||||
|
||||
:else
|
||||
(let [[field raw-value] (str/split line #":" 2)
|
||||
value (some-> raw-value (str/replace #"^ " ""))]
|
||||
(case field
|
||||
"data" (update state :data conj (or value ""))
|
||||
"event" (assoc state :event value)
|
||||
;; ignore retry and unknown fields
|
||||
state))))
|
||||
{:events [] :data [] :event nil}
|
||||
(str/split content #"\r?\n"))
|
||||
|
||||
;; handle unterminated last event (no trailing blank line)
|
||||
state (if (seq (:data state))
|
||||
(update state :events conj
|
||||
{:event (or (:event state) "message")
|
||||
:data (str/join "\n" (:data state))})
|
||||
state)]
|
||||
|
||||
(:events state)))
|
||||
|
||||
(defn consume-sse
|
||||
[callback]
|
||||
(let [{:keys [::yres/status ::yres/body ::yres/headers] :as response} (callback {})
|
||||
@@ -596,9 +558,12 @@
|
||||
(try
|
||||
(px/exec! :virtual #(rcp/write-body-to-stream body nil output))
|
||||
(into []
|
||||
(map (fn [{:keys [event data]}]
|
||||
[(keyword event)
|
||||
(tr/decode-str data)]))
|
||||
(parse-sse (slurp' input)))
|
||||
(map (fn [event]
|
||||
(let [[item1 item2] (re-seq #"(.*): (.*)\n?" event)]
|
||||
|
||||
[(keyword (nth item1 2))
|
||||
(tr/decode-str (nth item2 2))])))
|
||||
(-> (slurp' input)
|
||||
(str/split "\n\n")))
|
||||
(finally
|
||||
(.close input)))))
|
||||
|
||||
@@ -1024,29 +1024,6 @@
|
||||
:clj
|
||||
(sort comp-fn items))))
|
||||
|
||||
(defn reorder
|
||||
"Reorder a vector by moving one of their items from some position to some space between positions.
|
||||
It clamps the position numbers to a valid range."
|
||||
[v from-pos to-space-between-pos]
|
||||
(let [max-space-pos (count v)
|
||||
max-prop-pos (dec max-space-pos)
|
||||
|
||||
from-pos (max 0 (min max-prop-pos from-pos))
|
||||
to-space-between-pos (max 0 (min max-space-pos to-space-between-pos))]
|
||||
|
||||
(if (= from-pos to-space-between-pos)
|
||||
v
|
||||
(let [elem (nth v from-pos)
|
||||
without-elem (-> []
|
||||
(into (subvec v 0 from-pos))
|
||||
(into (subvec v (inc from-pos))))
|
||||
insert-pos (if (< from-pos to-space-between-pos)
|
||||
(dec to-space-between-pos)
|
||||
to-space-between-pos)]
|
||||
(-> []
|
||||
(into (subvec without-elem 0 insert-pos))
|
||||
(into [elem])
|
||||
(into (subvec without-elem insert-pos)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; String Functions
|
||||
|
||||
@@ -1357,6 +1357,38 @@
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0004-clean-shadow-color"
|
||||
[data _]
|
||||
(let [decode-color (sm/decoder types.color/schema:color sm/json-transformer)
|
||||
|
||||
clean-shadow-color
|
||||
(fn [color]
|
||||
(let [ref-id (get color :id)
|
||||
ref-file (get color :file-id)]
|
||||
(-> (d/without-qualified color)
|
||||
(select-keys [:opacity :color :gradient :image :ref-id :ref-file])
|
||||
(cond-> ref-id
|
||||
(assoc :ref-id ref-id))
|
||||
(cond-> ref-file
|
||||
(assoc :ref-file ref-file))
|
||||
(decode-color))))
|
||||
|
||||
clean-shadow
|
||||
(fn [shadow]
|
||||
(update shadow :color clean-shadow-color))
|
||||
|
||||
update-object
|
||||
(fn [object]
|
||||
(d/update-when object :shadow #(mapv clean-shadow %)))
|
||||
|
||||
update-container
|
||||
(fn [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(defmethod migrate-data "0005-deprecate-image-type"
|
||||
[data _]
|
||||
(letfn [(update-object [object]
|
||||
@@ -1598,45 +1630,6 @@
|
||||
;; as value; this migration fixes it.
|
||||
(d/update-when data :components d/update-vals d/without-nils))
|
||||
|
||||
(defmethod migrate-data "0015-clean-shadow-color"
|
||||
[data _]
|
||||
(let [decode-shadow-color
|
||||
(sm/decoder ctss/schema:color sm/json-transformer)
|
||||
|
||||
clean-shadow-color
|
||||
(fn [color]
|
||||
(let [ref-id (get color :id)
|
||||
ref-file (get color :file-id)]
|
||||
(-> (d/without-qualified color)
|
||||
(select-keys ctss/color-attrs)
|
||||
(cond-> ref-id
|
||||
(assoc :ref-id ref-id))
|
||||
(cond-> ref-file
|
||||
(assoc :ref-file ref-file))
|
||||
(decode-shadow-color)
|
||||
(d/without-nils))))
|
||||
|
||||
clean-shadow
|
||||
(fn [shadow]
|
||||
(update shadow :color clean-shadow-color))
|
||||
|
||||
clean-xform
|
||||
(comp
|
||||
(keep clean-shadow)
|
||||
(filter ctss/valid-shadow?))
|
||||
|
||||
update-object
|
||||
(fn [object]
|
||||
(d/update-when object :shadow #(into [] clean-xform %)))
|
||||
|
||||
update-container
|
||||
(fn [container]
|
||||
(d/update-when container :objects d/update-vals update-object))]
|
||||
|
||||
(-> data
|
||||
(update :pages-index d/update-vals update-container)
|
||||
(d/update-when :components d/update-vals update-container))))
|
||||
|
||||
(def available-migrations
|
||||
(into (d/ordered-set)
|
||||
["legacy-2"
|
||||
@@ -1696,6 +1689,7 @@
|
||||
"0002-clean-shape-interactions"
|
||||
"0003-fix-root-shape"
|
||||
"0003-convert-path-content-v2"
|
||||
"0004-clean-shadow-color"
|
||||
"0005-deprecate-image-type"
|
||||
"0006-fix-old-texts-fills"
|
||||
"0008-fix-library-colors-v4"
|
||||
@@ -1707,5 +1701,4 @@
|
||||
"0013-fix-component-path"
|
||||
"0013-clear-invalid-strokes-and-fills"
|
||||
"0014-fix-tokens-lib-duplicate-ids"
|
||||
"0014-clear-components-nil-objects"
|
||||
"0015-clean-shadow-color"]))
|
||||
"0014-clear-components-nil-objects"]))
|
||||
|
||||
@@ -466,20 +466,19 @@
|
||||
children (map #(ctst/get-shape page %) shapes)
|
||||
prop-names (cfv/extract-properties-names (first children) (:data file))]
|
||||
(doseq [child children]
|
||||
(when child
|
||||
(if (not (ctk/is-variant? child))
|
||||
(report-error :not-a-variant
|
||||
(str/ffmt "Shape % should be a variant" (:id child))
|
||||
child file page)
|
||||
(do
|
||||
(when (not= (:variant-id child) shape-id)
|
||||
(report-error :invalid-variant-id
|
||||
(str/ffmt "Variant % has invalid variant-id %" (:id child) (:variant-id child))
|
||||
child file page))
|
||||
(when (not= prop-names (cfv/extract-properties-names child (:data file)))
|
||||
(report-error :invalid-variant-properties
|
||||
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
|
||||
child file page))))))))
|
||||
(if (not (ctk/is-variant? child))
|
||||
(report-error :not-a-variant
|
||||
(str/ffmt "Shape % should be a variant" (:id child))
|
||||
child file page)
|
||||
(do
|
||||
(when (not= (:variant-id child) shape-id)
|
||||
(report-error :invalid-variant-id
|
||||
(str/ffmt "Variant % has invalid variant-id %" (:id child) (:variant-id child))
|
||||
child file page))
|
||||
(when (not= prop-names (cfv/extract-properties-names child (:data file)))
|
||||
(report-error :invalid-variant-properties
|
||||
(str/ffmt "Variant % has invalid properties %" (:id child) (vec prop-names))
|
||||
child file page)))))))
|
||||
|
||||
(defn- check-variant
|
||||
"Shape is a variant, so
|
||||
@@ -628,8 +627,7 @@
|
||||
main-component (if (:deleted component)
|
||||
(dm/get-in component [:objects (:main-instance-id component)])
|
||||
(ctst/get-shape component-page (:main-instance-id component)))]
|
||||
(when (and main-component
|
||||
(not (ctk/is-variant? main-component)))
|
||||
(when-not (ctk/is-variant? main-component)
|
||||
(report-error :not-a-variant
|
||||
(str/ffmt "Shape % should be a variant" (:id main-component))
|
||||
main-component file component-page))))
|
||||
|
||||
@@ -1642,8 +1642,7 @@
|
||||
(pcb/apply-changes-local)))))
|
||||
|
||||
(defn- generate-update-tokens
|
||||
[changes container dest-shape origin-shape touched omit-touched? valid-attrs]
|
||||
;; valid-attrs is a set of attrs to consider on the update. If it is nil, it will consider all the attrs
|
||||
[changes container dest-shape origin-shape touched omit-touched?]
|
||||
(let [attrs (->> (seq (keys ctk/sync-attrs))
|
||||
;; We don't update the flex-child attrs
|
||||
(remove #(= :layout-grid-cells %)))
|
||||
@@ -1651,8 +1650,8 @@
|
||||
applied-tokens (reduce (fn [applied-tokens attr]
|
||||
(let [attr-group (get ctk/sync-attrs attr)
|
||||
token-attrs (cto/shape-attr->token-attrs attr)]
|
||||
(if (and (or (not omit-touched?) (not (touched attr-group)))
|
||||
(or (empty? valid-attrs) (contains? valid-attrs attr)))
|
||||
(if (not (and (touched attr-group)
|
||||
omit-touched?))
|
||||
(into applied-tokens token-attrs)
|
||||
applied-tokens)))
|
||||
#{}
|
||||
@@ -1809,7 +1808,7 @@
|
||||
:always
|
||||
(check-detached-main dest-shape origin-shape)
|
||||
:always
|
||||
(generate-update-tokens container dest-shape origin-shape touched omit-touched? nil))
|
||||
(generate-update-tokens container dest-shape origin-shape touched omit-touched?))
|
||||
|
||||
(let [attr-group (get ctk/sync-attrs attr)
|
||||
;; position-data is a special case because can be affected by
|
||||
@@ -2083,14 +2082,12 @@
|
||||
(recur (next attrs)
|
||||
roperations'
|
||||
uoperations'))
|
||||
(cond-> changes
|
||||
(> (count roperations) 1)
|
||||
(add-update-attr-changes current-shape container roperations uoperations)
|
||||
|
||||
(let [updated-attrs (into #{} (comp (filter #(= :set (:type %)))
|
||||
(map :attr))
|
||||
roperations)]
|
||||
(cond-> changes
|
||||
(> (count roperations) 1)
|
||||
(-> (add-update-attr-changes current-shape container roperations uoperations)
|
||||
(generate-update-tokens container current-shape previous-shape touched false updated-attrs))))))))
|
||||
:always
|
||||
(generate-update-tokens container current-shape previous-shape touched false))))))
|
||||
|
||||
(defn- propagate-attrs
|
||||
"Helper that puts the origin attributes (attrs) into dest but only if
|
||||
@@ -2801,7 +2798,7 @@
|
||||
(defn generate-duplicate-changes
|
||||
"Prepare objects to duplicate: generate new id, give them unique names,
|
||||
move to the desired position, and recalculate parents and frames as needed."
|
||||
[changes all-objects page ids delta libraries library-data file-id & {:keys [variant-props alt-duplication?]}]
|
||||
[changes all-objects page ids delta libraries library-data file-id & {:keys [variant-props]}]
|
||||
(let [shapes (map (d/getf all-objects) ids)
|
||||
unames (volatile! (cfh/get-used-names (:objects page)))
|
||||
update-unames! (fn [new-name] (vswap! unames conj new-name))
|
||||
@@ -2811,22 +2808,10 @@
|
||||
;; we calculate a new one because the components will have created new shapes.
|
||||
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
|
||||
|
||||
|
||||
;; If there is an alt-duplication of a variant, change its parent to root
|
||||
;; so the copy is made as a child of root
|
||||
;; This is because inside a variant-container can't be a copy
|
||||
shapes (map (fn [shape]
|
||||
(if (and alt-duplication? (ctk/is-variant? shape))
|
||||
(assoc shape :parent-id uuid/zero :frame-id nil)
|
||||
shape))
|
||||
shapes)
|
||||
|
||||
|
||||
changes (-> changes
|
||||
(pcb/with-page page)
|
||||
(pcb/with-objects all-objects)
|
||||
(pcb/with-library-data library-data))
|
||||
|
||||
changes
|
||||
(->> shapes
|
||||
(reduce #(generate-duplicate-shape-change %1
|
||||
|
||||
@@ -28,7 +28,11 @@
|
||||
(pcb/update-component
|
||||
changes (:id component)
|
||||
(fn [component]
|
||||
(d/update-in-when component [:variant-properties pos] #(assoc % :name new-name)))
|
||||
(d/update-in-when component [:variant-properties pos]
|
||||
(fn [property]
|
||||
(-> property
|
||||
(assoc :name new-name)
|
||||
(with-meta nil)))))
|
||||
{:apply-changes-local-library? true}))
|
||||
changes
|
||||
related-components)))
|
||||
@@ -84,7 +88,7 @@
|
||||
related-components (cfv/find-variant-components data objects variant-id)]
|
||||
(reduce (fn [changes component]
|
||||
(let [props (:variant-properties component)
|
||||
props (d/reorder props from-pos to-space-between-pos)
|
||||
props (ctv/reorder-by-moving-to-position props from-pos to-space-between-pos)
|
||||
main-id (:main-instance-id component)
|
||||
name (ctv/properties-to-name props)]
|
||||
(-> changes
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
[app.common.types.container :as ctn]
|
||||
[app.common.types.file :as ctf]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.common.uuid :as uuid]
|
||||
[clojure.set :as set]))
|
||||
[app.common.uuid :as uuid]))
|
||||
|
||||
(defn generate-add-new-variant
|
||||
[changes shape variant-id new-component-id new-shape-id prop-num]
|
||||
@@ -68,6 +67,7 @@
|
||||
[[] {}]
|
||||
shapes))))
|
||||
|
||||
|
||||
(defn- keep-swapped-item
|
||||
"As part of the keep-touched process on a switch, given a child on the original
|
||||
copy that was swapped (orig-swapped-child), and its related shape on the new copy
|
||||
@@ -88,6 +88,7 @@
|
||||
current-parent (get objects (:parent-id related-shape-in-new))
|
||||
pos (d/index-of (:shapes current-parent) (:id related-shape-in-new))]
|
||||
|
||||
|
||||
(-> (pcb/concat-changes before-changes changes)
|
||||
|
||||
;; Move the previous shape to the new parent
|
||||
@@ -121,44 +122,6 @@
|
||||
(subvec (vec ancestors) 1 (dec num-ancestors)))]
|
||||
(some ctk/get-swap-slot ancestors)))
|
||||
|
||||
(defn- find-shape-ref-child-of
|
||||
"Get the shape referenced by the shape-ref of the near main of the shape,
|
||||
recursively repeated until find a shape-ref with parent-id as ancestor.
|
||||
It will return the shape or nil if it doesn't found any"
|
||||
[container libraries shape parent-id]
|
||||
(let [ref-shape (ctf/find-ref-shape nil container libraries shape
|
||||
:with-context? true)
|
||||
|
||||
ref-shape-container (when ref-shape (:container (meta ref-shape)))
|
||||
ref-shape-parents-set (when ref-shape
|
||||
(->> (cfh/get-parents (:objects ref-shape-container) (:id ref-shape))
|
||||
(into #{} d/xf:map-id)))]
|
||||
|
||||
(if (or (nil? ref-shape) (contains? ref-shape-parents-set parent-id))
|
||||
ref-shape
|
||||
(find-shape-ref-child-of ref-shape-container libraries ref-shape parent-id))))
|
||||
|
||||
(defn- get-ref-chain
|
||||
"Returns a vector with the shape ref chain including itself"
|
||||
[container libraries shape]
|
||||
(loop [chain [shape]
|
||||
current shape]
|
||||
(if-let [ref (ctf/find-ref-shape nil container libraries current :with-context? true)]
|
||||
(recur (conj chain ref) ref)
|
||||
chain)))
|
||||
|
||||
(defn- add-touched-from-ref-chain
|
||||
"Adds to the :touched attr of a shape the content of
|
||||
the :touched of all its chain of ref shapes"
|
||||
[container libraries shape]
|
||||
(let [chain (get-ref-chain container libraries shape)
|
||||
more-touched (->> chain
|
||||
(map :touched)
|
||||
(remove nil?)
|
||||
(apply set/union)
|
||||
(remove ctk/swap-slot?)
|
||||
set)]
|
||||
(update shape :touched #(set/union (or % #{}) more-touched))))
|
||||
|
||||
(defn generate-keep-touched
|
||||
"This is used as part of the switch process, when you switch from
|
||||
@@ -178,10 +141,7 @@
|
||||
;; Ignore children of swapped items, because
|
||||
;; they will be moved without change when
|
||||
;; managing their swapped ancestor
|
||||
orig-touched (->> original-shapes
|
||||
;; Add to each shape also the touched of its ref chain
|
||||
(map #(add-touched-from-ref-chain container libraries %))
|
||||
(filter (comp seq :touched))
|
||||
orig-touched (->> (filter (comp seq :touched) original-shapes)
|
||||
(remove
|
||||
#(child-of-swapped? %
|
||||
page-objects
|
||||
@@ -198,7 +158,7 @@
|
||||
|
||||
;; The original-shape is in a copy. For the relation rules, we need the referenced
|
||||
;; shape on the main component
|
||||
orig-ref-shape (ctf/find-remote-shape container libraries original-shape {:with-context? true})
|
||||
orig-ref-shape (ctf/find-ref-shape nil container libraries original-shape {:with-context? true})
|
||||
orig-ref-objects (:objects (:container (meta orig-ref-shape)))
|
||||
|
||||
;; Adds a :shape-path attribute to the children of the orig-ref-shape,
|
||||
@@ -211,6 +171,7 @@
|
||||
;; Creates a map to quickly find a child of the orig-ref-shape by its shape-path
|
||||
o-ref-shapes-p-map (into {} (map (juxt :id :shape-path)) o-ref-shapes-wp)
|
||||
|
||||
|
||||
;; Process each touched children of the original-shape
|
||||
[changes parents-of-swapped]
|
||||
(reduce
|
||||
@@ -221,7 +182,8 @@
|
||||
;; orig-child-touched is in a copy. Get the referenced shape on the main component
|
||||
;; If there is a swap slot, we will get the referenced shape in another way
|
||||
orig-ref-shape (when-not swap-slot
|
||||
(find-shape-ref-child-of container libraries orig-child-touched (:id orig-ref-shape)))
|
||||
;; TODO Maybe just get it from o-ref-shapes-wp
|
||||
(ctf/find-ref-shape nil container libraries orig-child-touched))
|
||||
|
||||
orig-ref-id (if swap-slot
|
||||
;; If there is a swap slot, find the referenced shape id
|
||||
@@ -234,7 +196,6 @@
|
||||
;; Get its related shape in the children of new-shape: the one that
|
||||
;; has the same shape-path
|
||||
related-shape-in-new (get new-shapes-map shape-path)
|
||||
|
||||
parents-of-swapped (if related-shape-in-new
|
||||
(conj parent-of-swapped (:parent-id related-shape-in-new))
|
||||
parent-of-swapped)
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
(defn type
|
||||
[s]
|
||||
(m/type s default-options))
|
||||
(m/-type s))
|
||||
|
||||
(defn properties
|
||||
[s]
|
||||
@@ -46,10 +46,6 @@
|
||||
[s]
|
||||
(m/type-properties s))
|
||||
|
||||
(defn children
|
||||
[s]
|
||||
(m/children s default-options))
|
||||
|
||||
(defn schema
|
||||
[s]
|
||||
(if (schema? s)
|
||||
@@ -131,19 +127,9 @@
|
||||
|
||||
(defn keys
|
||||
"Given a map schema, return all keys as set"
|
||||
[schema']
|
||||
(let [schema' (m/schema schema' default-options)]
|
||||
(case (m/type schema')
|
||||
:map
|
||||
(->> (entries schema')
|
||||
(into #{} xf:map-key))
|
||||
|
||||
:merge
|
||||
(->> (m/children schema')
|
||||
(mapcat m/entries)
|
||||
(into #{} xf:map-key))
|
||||
|
||||
(throw (ex-info "not supported schema type" {:type (m/type schema')})))))
|
||||
[schema]
|
||||
(->> (entries schema)
|
||||
(into #{} xf:map-key)))
|
||||
|
||||
(defn update-properties
|
||||
[s f & args]
|
||||
|
||||
@@ -286,7 +286,7 @@
|
||||
(fn [touched]
|
||||
(into #{} (remove #(str/starts-with? (name %) "swap-slot-") touched)))))
|
||||
|
||||
(defn get-deleted-component-root
|
||||
(defn get-component-root
|
||||
[component]
|
||||
(if (some? (:main-instance-id component))
|
||||
(get-in component [:objects (:main-instance-id component)])
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
(-> file-data
|
||||
(get-component-page component)
|
||||
(ctn/get-shape (:main-instance-id component)))
|
||||
(ctk/get-deleted-component-root component)))
|
||||
(ctk/get-component-root component)))
|
||||
|
||||
(defn get-component-shape
|
||||
"Retrieve one shape in the component by id. If with-context? is true, add the
|
||||
@@ -355,7 +355,7 @@
|
||||
|
||||
(defn find-remote-shape
|
||||
"Recursively go back by the :shape-ref of the shape until find the correct shape of the original component"
|
||||
[container libraries shape & {:keys [with-context?] :or {with-context? false}}]
|
||||
[container libraries shape]
|
||||
(let [top-instance (ctn/get-component-shape (:objects container) shape)
|
||||
component-file (get-in libraries [(:component-file top-instance) :data])
|
||||
component (ctkl/get-component component-file (:component-id top-instance) true)
|
||||
@@ -375,12 +375,8 @@
|
||||
(if (nil? remote-shape)
|
||||
nil
|
||||
(if (nil? (:shape-ref remote-shape))
|
||||
(cond-> remote-shape
|
||||
(and remote-shape with-context?)
|
||||
(with-meta {:file {:id (:id file-data)
|
||||
:data file-data}
|
||||
:container component-container}))
|
||||
(find-remote-shape component-container libraries remote-shape :with-context? with-context?)))))
|
||||
remote-shape
|
||||
(find-remote-shape component-container libraries remote-shape)))))
|
||||
|
||||
(defn direct-copy?
|
||||
"Check if the shape is in a direct copy of the component (i.e. the shape-ref points to shapes inside
|
||||
@@ -905,7 +901,7 @@
|
||||
(println))
|
||||
|
||||
(when (seq (:objects component))
|
||||
(let [root (ctk/get-deleted-component-root component)]
|
||||
(let [root (ctk/get-component-root component)]
|
||||
(dump-shape (:id root)
|
||||
1
|
||||
(:objects component)
|
||||
|
||||
@@ -11,14 +11,6 @@
|
||||
|
||||
(def styles #{:drop-shadow :inner-shadow})
|
||||
|
||||
(def schema:color
|
||||
[:merge {:title "ShadowColor"}
|
||||
ctc/schema:color-attrs
|
||||
ctc/schema:plain-color])
|
||||
|
||||
(def color-attrs
|
||||
(sm/keys schema:color))
|
||||
|
||||
(def schema:shadow
|
||||
[:map {:title "Shadow"}
|
||||
[:id [:maybe ::sm/uuid]]
|
||||
@@ -28,7 +20,7 @@
|
||||
[:blur ::sm/safe-number]
|
||||
[:spread ::sm/safe-number]
|
||||
[:hidden :boolean]
|
||||
[:color schema:color]])
|
||||
[:color ctc/schema:color]])
|
||||
|
||||
(def check-shadow
|
||||
(sm/check-fn schema:shadow))
|
||||
|
||||
@@ -249,16 +249,12 @@
|
||||
(defn equal-attrs?
|
||||
"Given a text structure, and a map of attrs, check that all the internal attrs in
|
||||
paragraphs and sentences have the same attrs"
|
||||
([item attrs]
|
||||
;; Ignore the root attrs of the content. We only want to check paragraphs and sentences
|
||||
(equal-attrs? item attrs true))
|
||||
([item attrs ignore?]
|
||||
(let [item-attrs (dissoc item :text :type :key :children)]
|
||||
(and
|
||||
(or ignore?
|
||||
(empty? item-attrs)
|
||||
(= attrs (dissoc item :text :type :key :children)))
|
||||
(every? #(equal-attrs? % attrs false) (:children item))))))
|
||||
[item attrs]
|
||||
(let [item-attrs (dissoc item :text :type :key :children)]
|
||||
(and
|
||||
(or (empty? item-attrs)
|
||||
(= attrs (dissoc item :text :type :key :children)))
|
||||
(every? #(equal-attrs? % attrs) (:children item)))))
|
||||
|
||||
(defn get-first-paragraph-text-attrs
|
||||
"Given a content text structure, extract it's first paragraph
|
||||
|
||||
@@ -1991,19 +1991,13 @@ Will return a value that matches this schema:
|
||||
|
||||
#?(:clj
|
||||
(defn- migrate-to-v1-4
|
||||
"Migrate the TokensLib data structure internals to v1.4 version; it
|
||||
"Migrate the TokensLib data structure internals to v1.2 version; it
|
||||
expects input from v1.3 version"
|
||||
[params]
|
||||
(let [migrate-set-node
|
||||
(fn recurse [node]
|
||||
(cond
|
||||
(token-set-legacy? node)
|
||||
(if (token-set-legacy? node)
|
||||
(make-token-set node)
|
||||
|
||||
(token-set? node)
|
||||
node
|
||||
|
||||
:else
|
||||
(d/update-vals node recurse)))]
|
||||
|
||||
(update params :sets d/update-vals migrate-set-node))))
|
||||
|
||||
@@ -310,3 +310,27 @@
|
||||
the real name of the shape joined by the properties values separated by '/'"
|
||||
[variant]
|
||||
(cpn/merge-path-item (:name variant) (str/replace (:variant-name variant) #", " " / ")))
|
||||
|
||||
(defn reorder-by-moving-to-position
|
||||
"Reorder a vector by moving one of their items from some position to some space between positions.
|
||||
It clamps the position numbers to a valid range."
|
||||
[props from-pos to-space-between-pos]
|
||||
(let [max-space-pos (count props)
|
||||
max-prop-pos (dec max-space-pos)
|
||||
|
||||
from-pos (max 0 (min max-prop-pos from-pos))
|
||||
to-space-between-pos (max 0 (min max-space-pos to-space-between-pos))]
|
||||
|
||||
(if (= from-pos to-space-between-pos)
|
||||
props
|
||||
(let [elem (nth props from-pos)
|
||||
without-elem (-> []
|
||||
(into (subvec props 0 from-pos))
|
||||
(into (subvec props (inc from-pos))))
|
||||
insert-pos (if (< from-pos to-space-between-pos)
|
||||
(dec to-space-between-pos)
|
||||
to-space-between-pos)]
|
||||
(-> []
|
||||
(into (subvec without-elem 0 insert-pos))
|
||||
(into [elem])
|
||||
(into (subvec without-elem insert-pos)))))))
|
||||
|
||||
@@ -102,14 +102,3 @@
|
||||
(t/is (= (d/insert-at-index [:a :b :c :d] 1 [:a])
|
||||
[:a :b :c :d])))
|
||||
|
||||
(t/deftest reorder
|
||||
(let [v ["a" "b" "c" "d"]]
|
||||
(t/is (= (d/reorder v 0 2) ["b" "a" "c" "d"]))
|
||||
(t/is (= (d/reorder v 0 3) ["b" "c" "a" "d"]))
|
||||
(t/is (= (d/reorder v 0 4) ["b" "c" "d" "a"]))
|
||||
(t/is (= (d/reorder v 3 0) ["d" "a" "b" "c"]))
|
||||
(t/is (= (d/reorder v 3 2) ["a" "b" "d" "c"]))
|
||||
(t/is (= (d/reorder v 0 5) ["b" "c" "d" "a"]))
|
||||
(t/is (= (d/reorder v 3 -1) ["d" "a" "b" "c"]))
|
||||
(t/is (= (d/reorder v 5 -1) ["d" "a" "b" "c"]))
|
||||
(t/is (= (d/reorder v -1 5) ["b" "c" "d" "a"]))))
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
(pcb/with-library-data (:data file))
|
||||
(clt/generate-toggle-token-set (tht/get-tokens-lib file) "foo/bar"))
|
||||
|
||||
_ (prn "changes" changes)
|
||||
redo (thf/apply-changes file changes)
|
||||
redo-lib (tht/get-tokens-lib redo)
|
||||
undo (thf/apply-undo-changes redo changes)
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
;; The rect has width 15 after the switch
|
||||
(t/is (= (:width rect02') 15))))
|
||||
|
||||
|
||||
(t/deftest test-switch-with-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@@ -124,10 +125,12 @@
|
||||
;; The rect has width 15 after the switch
|
||||
(t/is (= (:width rect02') 15))))
|
||||
|
||||
|
||||
(def font-size-path-paragraph [:content :children 0 :children 0 :font-size])
|
||||
(def font-size-path-0 [:content :children 0 :children 0 :children 0 :font-size])
|
||||
(def font-size-path-1 [:content :children 0 :children 0 :children 1 :font-size])
|
||||
|
||||
|
||||
(def text-path-0 [:content :children 0 :children 0 :children 0 :text])
|
||||
(def text-path-1 [:content :children 0 :children 0 :children 1 :text])
|
||||
(def text-lines-path [:content :children 0 :children 0 :children])
|
||||
@@ -185,8 +188,6 @@
|
||||
|
||||
|
||||
;; The copy clean has no overrides
|
||||
|
||||
|
||||
copy-clean (ths/get-shape file :copy-clean)
|
||||
copy-clean-t (ths/get-shape file :copy-clean-t)
|
||||
|
||||
@@ -208,8 +209,6 @@
|
||||
|
||||
|
||||
;; ==== Action: Switch all the copies
|
||||
|
||||
|
||||
file' (-> file
|
||||
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
||||
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
||||
@@ -235,8 +234,6 @@
|
||||
;; Before the switch:
|
||||
;; * font size 14
|
||||
;; * text "hello world"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
|
||||
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
|
||||
|
||||
@@ -251,8 +248,6 @@
|
||||
;; Before the switch:
|
||||
;; * font size 25
|
||||
;; * text "hello world"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
|
||||
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
|
||||
|
||||
@@ -311,8 +306,6 @@
|
||||
|
||||
|
||||
;; The copy clean has no overrides
|
||||
|
||||
|
||||
copy-clean (ths/get-shape file :copy-clean)
|
||||
copy-clean-t (ths/get-shape file :copy-clean-t)
|
||||
|
||||
@@ -334,8 +327,6 @@
|
||||
|
||||
|
||||
;; ==== Action: Switch all the copies
|
||||
|
||||
|
||||
file' (-> file
|
||||
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
||||
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
||||
@@ -361,8 +352,6 @@
|
||||
;; Before the switch:
|
||||
;; * font size 14
|
||||
;; * text "hello world"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
|
||||
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
|
||||
|
||||
@@ -377,8 +366,6 @@
|
||||
;; Before the switch:
|
||||
;; * font size 25
|
||||
;; * text "hello world"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
|
||||
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
|
||||
|
||||
@@ -414,6 +401,7 @@
|
||||
(t/is (= (get-in copy-both-t' font-size-path-0) "50"))
|
||||
(t/is (= (get-in copy-both-t' text-path-0) "text overriden"))))
|
||||
|
||||
|
||||
(t/deftest test-switch-with-different-text-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@@ -435,8 +423,6 @@
|
||||
|
||||
|
||||
;; The copy clean has no overrides
|
||||
|
||||
|
||||
copy-clean (ths/get-shape file :copy-clean)
|
||||
copy-clean-t (ths/get-shape file :copy-clean-t)
|
||||
|
||||
@@ -458,8 +444,6 @@
|
||||
|
||||
|
||||
;; ==== Action: Switch all the copies
|
||||
|
||||
|
||||
file' (-> file
|
||||
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
||||
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
||||
@@ -485,8 +469,6 @@
|
||||
;; Before the switch:
|
||||
;; * font size 14
|
||||
;; * text "hello world"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
|
||||
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
|
||||
|
||||
@@ -501,8 +483,6 @@
|
||||
;; Before the switch:
|
||||
;; * font size 25
|
||||
;; * text "hello world"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
|
||||
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
|
||||
|
||||
@@ -538,6 +518,7 @@
|
||||
(t/is (= (get-in copy-both-t' font-size-path-0) "25"))
|
||||
(t/is (= (get-in copy-both-t' text-path-0) "bye"))))
|
||||
|
||||
|
||||
(t/deftest test-switch-with-different-text-and-prop-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@@ -561,8 +542,6 @@
|
||||
|
||||
|
||||
;; The copy clean has no overrides
|
||||
|
||||
|
||||
copy-clean (ths/get-shape file :copy-clean)
|
||||
copy-clean-t (ths/get-shape file :copy-clean-t)
|
||||
|
||||
@@ -584,8 +563,6 @@
|
||||
|
||||
|
||||
;; ==== Action: Switch all the copies
|
||||
|
||||
|
||||
file' (-> file
|
||||
(tho/swap-component copy-clean :c02 {:new-shape-label :copy-clean-2 :keep-touched? true})
|
||||
(tho/swap-component copy-font-size :c02 {:new-shape-label :copy-font-size-2 :keep-touched? true})
|
||||
@@ -611,8 +588,6 @@
|
||||
;; Before the switch:
|
||||
;; * font size 14
|
||||
;; * text "hello world"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-clean-t font-size-path-0) "14"))
|
||||
(t/is (= (get-in copy-clean-t text-path-0) "hello world"))
|
||||
|
||||
@@ -627,8 +602,6 @@
|
||||
;; Before the switch:
|
||||
;; * font size 25
|
||||
;; * text "hello world"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-font-size-t font-size-path-0) "25"))
|
||||
(t/is (= (get-in copy-font-size-t text-path-0) "hello world"))
|
||||
|
||||
@@ -664,6 +637,7 @@
|
||||
(t/is (= (get-in copy-both-t' font-size-path-0) "50"))
|
||||
(t/is (= (get-in copy-both-t' text-path-0) "bye"))))
|
||||
|
||||
|
||||
(t/deftest test-switch-with-identical-structure-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@@ -683,8 +657,6 @@
|
||||
|
||||
|
||||
;; Duplicate a text line in copy-structure-clean
|
||||
|
||||
|
||||
file (change-structure file :copy-structure-clean-t)
|
||||
copy-structure-clean (ths/get-shape file :copy-structure-clean)
|
||||
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
||||
@@ -706,8 +678,6 @@
|
||||
|
||||
|
||||
;; ==== Action: Switch all the copies
|
||||
|
||||
|
||||
file' (-> file
|
||||
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
||||
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
||||
@@ -793,6 +763,7 @@
|
||||
(t/is (= (get-in copy-structure-mixed-t' font-size-path-1) "40"))
|
||||
(t/is (= (get-in copy-structure-mixed-t' text-path-1) "new line 2"))))
|
||||
|
||||
|
||||
(t/deftest test-switch-with-different-prop-structure-text-override
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@@ -813,8 +784,6 @@
|
||||
|
||||
|
||||
;; Duplicate a text line in copy-structure-clean
|
||||
|
||||
|
||||
file (change-structure file :copy-structure-clean-t)
|
||||
copy-structure-clean (ths/get-shape file :copy-structure-clean)
|
||||
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
||||
@@ -836,8 +805,6 @@
|
||||
|
||||
|
||||
;; ==== Action: Switch all the copies
|
||||
|
||||
|
||||
file' (-> file
|
||||
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
||||
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
||||
@@ -939,8 +906,6 @@
|
||||
|
||||
|
||||
;; Duplicate a text line in copy-structure-clean
|
||||
|
||||
|
||||
file (change-structure file :copy-structure-clean-t)
|
||||
copy-structure-clean (ths/get-shape file :copy-structure-clean)
|
||||
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
||||
@@ -962,8 +927,6 @@
|
||||
|
||||
|
||||
;; ==== Action: Switch all the copies
|
||||
|
||||
|
||||
file' (-> file
|
||||
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
||||
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
||||
@@ -1008,8 +971,6 @@
|
||||
;; Second line:
|
||||
;; * font size 25
|
||||
;; * text "new line 2"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-structure-unif-t font-size-path-0) "25"))
|
||||
(t/is (= (get-in copy-structure-unif-t text-path-0) "new line 1"))
|
||||
(t/is (= (get-in copy-structure-unif-t font-size-path-1) "25"))
|
||||
@@ -1031,8 +992,6 @@
|
||||
;; Before the switch, second line:
|
||||
;; * font size 40
|
||||
;; * text "new line 2"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-structure-mixed-t font-size-path-0) "35"))
|
||||
(t/is (= (get-in copy-structure-mixed-t text-path-0) "new line 1"))
|
||||
(t/is (= (get-in copy-structure-mixed-t font-size-path-1) "40"))
|
||||
@@ -1066,8 +1025,6 @@
|
||||
|
||||
|
||||
;; Duplicate a text line in copy-structure-clean
|
||||
|
||||
|
||||
file (change-structure file :copy-structure-clean-t)
|
||||
copy-structure-clean (ths/get-shape file :copy-structure-clean)
|
||||
copy-structure-clean-t (ths/get-shape file :copy-structure-clean-t)
|
||||
@@ -1089,8 +1046,6 @@
|
||||
|
||||
|
||||
;; ==== Action: Switch all the copies
|
||||
|
||||
|
||||
file' (-> file
|
||||
(tho/swap-component copy-structure-clean :c02 {:new-shape-label :copy-structure-clean-2 :keep-touched? true})
|
||||
(tho/swap-component copy-structure-unif :c02 {:new-shape-label :copy-structure-unif-2 :keep-touched? true})
|
||||
@@ -1135,8 +1090,6 @@
|
||||
;; Second line:
|
||||
;; * font size 25
|
||||
;; * text "new line 2"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-structure-unif-t font-size-path-0) "25"))
|
||||
(t/is (= (get-in copy-structure-unif-t text-path-0) "new line 1"))
|
||||
(t/is (= (get-in copy-structure-unif-t font-size-path-1) "25"))
|
||||
@@ -1158,8 +1111,6 @@
|
||||
;; Before the switch, second line:
|
||||
;; * font size 40
|
||||
;; * text "new line 2"
|
||||
|
||||
|
||||
(t/is (= (get-in copy-structure-mixed-t font-size-path-0) "35"))
|
||||
(t/is (= (get-in copy-structure-mixed-t text-path-0) "new line 1"))
|
||||
(t/is (= (get-in copy-structure-mixed-t font-size-path-1) "40"))
|
||||
@@ -1173,6 +1124,7 @@
|
||||
(t/is (= (get-in copy-structure-mixed-t' text-path-0) "bye"))
|
||||
(t/is (nil? (get-in copy-structure-mixed-t' font-size-path-1)))))
|
||||
|
||||
|
||||
(t/deftest test-switch-variant-for-other-with-same-nested-component
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@@ -1192,8 +1144,6 @@
|
||||
|
||||
|
||||
;; On :copy-cp01, change the width of the rect
|
||||
|
||||
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{copy-cp01-rect-id}
|
||||
(fn [shape]
|
||||
@@ -1216,6 +1166,8 @@
|
||||
;; The width of copy-cp02-rect' is 25 (change is preserved)
|
||||
(t/is (= (:width copy-cp02-rect') 25))))
|
||||
|
||||
|
||||
|
||||
(t/deftest test-switch-variant-that-has-swaped-copy
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@@ -1241,6 +1193,7 @@
|
||||
;; Switch :c01 for :c02
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
copy-cp02' (ths/get-shape file' :copy-cp02)]
|
||||
(thf/dump-file file')
|
||||
@@ -1254,6 +1207,7 @@
|
||||
;;copy-02' had copy-cp02' as child
|
||||
(t/is (= (-> copy02' :shapes first) (:id copy-cp02')))))
|
||||
|
||||
|
||||
(t/deftest test-switch-variant-that-has-swaped-copy-with-changed-attr
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
@@ -1290,6 +1244,7 @@
|
||||
;; Switch :c01 for :c02
|
||||
file' (tho/swap-component file copy01 :c02 {:new-shape-label :copy02 :keep-touched? true})
|
||||
|
||||
|
||||
copy02' (ths/get-shape file' :copy02)
|
||||
copy-cp02' (ths/get-shape file' :copy-cp02)
|
||||
copy-cp02-rect' (ths/get-shape-by-id file' (-> copy-cp02' :shapes first))]
|
||||
@@ -1307,58 +1262,3 @@
|
||||
(t/is (= (-> copy02' :shapes first) (:id copy-cp02')))
|
||||
;; The width of copy-cp02-rect' is 25 (change is preserved)
|
||||
(t/is (= (:width copy-cp02-rect') 25))))
|
||||
|
||||
(t/deftest test-switch-variant-without-touched-but-touched-parent
|
||||
(let [;; ==== Setup
|
||||
file (-> (thf/sample-file :file1)
|
||||
(thv/add-variant-with-child
|
||||
:v01 :c01 :m01 :c02 :m02 :r01 :r02
|
||||
{:child1-params {:width 5}
|
||||
:child2-params {:width 5}})
|
||||
(tho/add-simple-component :external01 :external01-root :external01-child)
|
||||
|
||||
(thc/instantiate-component :c01
|
||||
:c01-in-root
|
||||
:children-labels [:r01-in-c01-in-root]
|
||||
:parent-label :external01-root))
|
||||
|
||||
;; Make a change on r01-in-c01-in-root so it is touched
|
||||
page (thf/current-page file)
|
||||
r01-in-c01-in-root (ths/get-shape file :r01-in-c01-in-root)
|
||||
|
||||
changes (cls/generate-update-shapes (pcb/empty-changes nil (:id page))
|
||||
#{(:id r01-in-c01-in-root)}
|
||||
(fn [shape]
|
||||
(assoc shape :width 25))
|
||||
(:objects page)
|
||||
{})
|
||||
|
||||
file (thf/apply-changes file changes)
|
||||
|
||||
|
||||
;; Instantiate the component :external01
|
||||
|
||||
|
||||
file (thc/instantiate-component file
|
||||
:external01
|
||||
:external-copy01
|
||||
:children-labels [:external-copy01-rect :c01-in-copy])
|
||||
page (thf/current-page file)
|
||||
c01-in-copy (ths/get-shape file :c01-in-copy)
|
||||
rect01 (get-in page [:objects (-> c01-in-copy :shapes first)])
|
||||
|
||||
|
||||
;; ==== Action
|
||||
|
||||
|
||||
file' (tho/swap-component file c01-in-copy :c02 {:new-shape-label :c02-in-copy :keep-touched? true})
|
||||
|
||||
page' (thf/current-page file')
|
||||
c02-in-copy' (ths/get-shape file' :c02-in-copy)
|
||||
rect02' (get-in page' [:objects (-> c02-in-copy' :shapes first)])]
|
||||
|
||||
(thf/dump-file file :keys [:width :touched])
|
||||
;; The rect had width 25 before the switch
|
||||
(t/is (= (:width rect01) 25))
|
||||
;; The rect still has width 25 after the switch
|
||||
(t/is (= (:width rect02') 25))))
|
||||
|
||||
@@ -159,3 +159,48 @@
|
||||
|
||||
(t/testing "update-number-in-repeated-prop-names"
|
||||
(t/is (= (ctv/update-number-in-repeated-prop-names props) numbered-props)))))
|
||||
|
||||
|
||||
(t/deftest reorder-by-moving-to-position
|
||||
(let [props [{:name "border" :value "no"}
|
||||
{:name "color" :value "blue"}
|
||||
{:name "shadow" :value "yes"}
|
||||
{:name "background" :value "none"}]]
|
||||
|
||||
(t/testing "reorder-by-moving-to-position"
|
||||
(t/is (= (ctv/reorder-by-moving-to-position props 0 2) [{:name "color" :value "blue"}
|
||||
{:name "border" :value "no"}
|
||||
{:name "shadow" :value "yes"}
|
||||
{:name "background" :value "none"}]))
|
||||
(t/is (= (ctv/reorder-by-moving-to-position props 0 3) [{:name "color" :value "blue"}
|
||||
{:name "shadow" :value "yes"}
|
||||
{:name "border" :value "no"}
|
||||
{:name "background" :value "none"}]))
|
||||
(t/is (= (ctv/reorder-by-moving-to-position props 0 4) [{:name "color" :value "blue"}
|
||||
{:name "shadow" :value "yes"}
|
||||
{:name "background" :value "none"}
|
||||
{:name "border" :value "no"}]))
|
||||
(t/is (= (ctv/reorder-by-moving-to-position props 3 0) [{:name "background" :value "none"}
|
||||
{:name "border" :value "no"}
|
||||
{:name "color" :value "blue"}
|
||||
{:name "shadow" :value "yes"}]))
|
||||
(t/is (= (ctv/reorder-by-moving-to-position props 3 2) [{:name "border" :value "no"}
|
||||
{:name "color" :value "blue"}
|
||||
{:name "background" :value "none"}
|
||||
{:name "shadow" :value "yes"}]))
|
||||
(t/is (= (ctv/reorder-by-moving-to-position props 0 5) [{:name "color" :value "blue"}
|
||||
{:name "shadow" :value "yes"}
|
||||
{:name "background" :value "none"}
|
||||
{:name "border" :value "no"}]))
|
||||
(t/is (= (ctv/reorder-by-moving-to-position props 3 -1) [{:name "background" :value "none"}
|
||||
{:name "border" :value "no"}
|
||||
{:name "color" :value "blue"}
|
||||
{:name "shadow" :value "yes"}]))
|
||||
(t/is (= (ctv/reorder-by-moving-to-position props 5 -1) [{:name "background" :value "none"}
|
||||
{:name "border" :value "no"}
|
||||
{:name "color" :value "blue"}
|
||||
{:name "shadow" :value "yes"}]))
|
||||
(t/is (= (ctv/reorder-by-moving-to-position props -1 5) [{:name "color" :value "blue"}
|
||||
{:name "shadow" :value "yes"}
|
||||
{:name "background" :value "none"}
|
||||
{:name "border" :value "no"}])))))
|
||||
|
||||
@@ -73,7 +73,7 @@ RUN set -eux; \
|
||||
|
||||
FROM base AS setup-node
|
||||
|
||||
ENV NODE_VERSION=v22.21.1 \
|
||||
ENV NODE_VERSION=v22.19.0 \
|
||||
PATH=/opt/node/bin:$PATH
|
||||
|
||||
RUN set -eux; \
|
||||
@@ -113,12 +113,12 @@ RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
case "${ARCH}" in \
|
||||
aarch64|arm64) \
|
||||
ESUM='8c5321f16d9f1d8149f83e4e9ff8ca5d9e94320b92d205e6db42a604de3d1140'; \
|
||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_aarch64.tar.gz'; \
|
||||
ESUM='b60eb9d54c97ba4159547834a98cc5d016281dd2b3e60e7475cba4911324bcb4'; \
|
||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_aarch64.tar.gz'; \
|
||||
;; \
|
||||
amd64|x86_64) \
|
||||
ESUM='471b3e62bdffaed27e37005d842d8639f10d244ccce1c7cdebf7abce06c8313e'; \
|
||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_x64.tar.gz'; \
|
||||
ESUM='164d901e5a240b8c18516f5ab55bc11fc9689ab6e829045aea8467356dcdb340'; \
|
||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.28.85-ca-jdk25.0.0-linux_x64.tar.gz'; \
|
||||
;; \
|
||||
*) \
|
||||
echo "Unsupported arch: ${ARCH}"; \
|
||||
@@ -149,24 +149,18 @@ FROM base AS setup-rust
|
||||
ENV PATH=/opt/cargo/bin:$PATH \
|
||||
RUSTUP_HOME=/opt/rustup \
|
||||
CARGO_HOME=/opt/cargo \
|
||||
RUSTUP_VERSION=1.28.2 \
|
||||
RUST_VERSION=1.91.0 \
|
||||
RUSTUP_VERSION=1.27.1 \
|
||||
RUST_VERSION=1.85.0 \
|
||||
EMSCRIPTEN_VERSION=4.0.6
|
||||
|
||||
WORKDIR /opt
|
||||
|
||||
RUN set -eux; \
|
||||
# Same steps as in Rust official Docker image https://github.com/rust-lang/docker-rust/blob/9f287282d513a84cb7c7f38f197838f15d37b6a9/1.81.0/bookworm/Dockerfile
|
||||
arch="$(dpkg --print-architecture)"; \
|
||||
case "$arch" in \
|
||||
'amd64') \
|
||||
rustArch='x86_64-unknown-linux-gnu'; \
|
||||
rustupSha256='20a06e644b0d9bd2fbdbfd52d42540bdde820ea7df86e92e533c073da0cdd43c'; \
|
||||
;; \
|
||||
'arm64') \
|
||||
rustArch='aarch64-unknown-linux-gnu'; \
|
||||
rustupSha256='e3853c5a252fca15252d07cb23a1bdd9377a8c6f3efa01531109281ae47f841c'; \
|
||||
;; \
|
||||
dpkgArch="$(dpkg --print-architecture)"; \
|
||||
case "${dpkgArch##*-}" in \
|
||||
amd64) rustArch='x86_64-unknown-linux-gnu'; rustupSha256='6aeece6993e902708983b209d04c0d1dbb14ebb405ddb87def578d41f920f56d' ;; \
|
||||
arm64) rustArch='aarch64-unknown-linux-gnu'; rustupSha256='1cffbf51e63e634c746f741de50649bbbcbd9dbe1de363c9ecef64e278dba2b2' ;; \
|
||||
*) echo >&2 "unsupported architecture: ${dpkgArch}"; exit 1 ;; \
|
||||
esac; \
|
||||
wget "https://static.rust-lang.org/rustup/archive/${RUSTUP_VERSION}/${rustArch}/rustup-init"; \
|
||||
|
||||
@@ -5,7 +5,7 @@ ENV LANG='C.UTF-8' \
|
||||
LC_ALL='C.UTF-8' \
|
||||
JAVA_HOME="/opt/jdk" \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
NODE_VERSION=v22.21.1 \
|
||||
NODE_VERSION=v22.19.0 \
|
||||
TZ=Etc/UTC
|
||||
|
||||
RUN set -ex; \
|
||||
@@ -46,12 +46,12 @@ RUN set -eux; \
|
||||
ARCH="$(dpkg --print-architecture)"; \
|
||||
case "${ARCH}" in \
|
||||
aarch64|arm64) \
|
||||
ESUM='8c5321f16d9f1d8149f83e4e9ff8ca5d9e94320b92d205e6db42a604de3d1140'; \
|
||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_aarch64.tar.gz'; \
|
||||
ESUM='6f8725d186d05c627176db9c46c732a6ef3ba41d9e9b3775c4727fc8ac642bb2'; \
|
||||
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_aarch64_linux_hotspot_24.0.2_12.tar.gz'; \
|
||||
;; \
|
||||
amd64|x86_64) \
|
||||
ESUM='471b3e62bdffaed27e37005d842d8639f10d244ccce1c7cdebf7abce06c8313e'; \
|
||||
BINARY_URL='https://cdn.azul.com/zulu/bin/zulu25.30.17-ca-jdk25.0.1-linux_x64.tar.gz'; \
|
||||
ESUM='aea1cc55e51cf651c85f2f00ad021603fe269c4bb6493fa97a321ad770c9b096'; \
|
||||
BINARY_URL='https://github.com/adoptium/temurin24-binaries/releases/download/jdk-24.0.2%2B12/OpenJDK24U-jdk_x64_linux_hotspot_24.0.2_12.tar.gz'; \
|
||||
;; \
|
||||
*) \
|
||||
echo "Unsupported arch: ${ARCH}"; \
|
||||
@@ -128,7 +128,7 @@ COPY --from=build /opt/node /opt/node
|
||||
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
|
||||
|
||||
ARG BUNDLE_PATH="./bundle-backend/"
|
||||
ADD --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/backend/
|
||||
COPY --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/backend/
|
||||
|
||||
USER penpot:penpot
|
||||
WORKDIR /opt/penpot/backend
|
||||
|
||||
@@ -3,7 +3,7 @@ LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
ENV LANG=en_US.UTF-8 \
|
||||
LC_ALL=en_US.UTF-8 \
|
||||
NODE_VERSION=v22.21.1 \
|
||||
NODE_VERSION=v22.19.0 \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
PATH=/opt/node/bin:$PATH
|
||||
|
||||
@@ -90,7 +90,7 @@ RUN set -eux; \
|
||||
chown -R penpot:penpot /opt/penpot;
|
||||
|
||||
ARG BUNDLE_PATH="./bundle-exporter/"
|
||||
ADD --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/exporter/
|
||||
COPY --chown=penpot:penpot $BUNDLE_PATH /opt/penpot/exporter/
|
||||
|
||||
WORKDIR /opt/penpot/exporter
|
||||
USER penpot:penpot
|
||||
|
||||
@@ -12,13 +12,13 @@ RUN set -ex; \
|
||||
mkdir -p /etc/nginx/overrides/location.d/;
|
||||
|
||||
ARG BUNDLE_PATH="./bundle-frontend/"
|
||||
ADD $BUNDLE_PATH /var/www/app/
|
||||
ADD ./files/config.js /var/www/app/js/config.js
|
||||
ADD ./files/nginx.conf.template /tmp/nginx.conf.template
|
||||
ADD ./files/nginx-resolvers.conf.template /tmp/resolvers.conf.template
|
||||
ADD ./files/nginx-mime.types /etc/nginx/mime.types
|
||||
ADD ./files/nginx-external-locations.conf /etc/nginx/overrides/location.d/external-locations.conf
|
||||
ADD ./files/nginx-entrypoint.sh /entrypoint.sh
|
||||
COPY $BUNDLE_PATH /var/www/app/
|
||||
COPY ./files/config.js /var/www/app/js/config.js
|
||||
COPY ./files/nginx.conf.template /tmp/nginx.conf.template
|
||||
COPY ./files/nginx-resolvers.conf.template /tmp/resolvers.conf.template
|
||||
COPY ./files/nginx-mime.types /etc/nginx/mime.types
|
||||
COPY ./files/nginx-external-locations.conf /etc/nginx/overrides/location.d/external-locations.conf
|
||||
COPY ./files/nginx-entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chown -R 1001:0 /var/cache/nginx; \
|
||||
chmod -R g+w /var/cache/nginx; \
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
FROM nginxinc/nginx-unprivileged:1.29.1
|
||||
LABEL maintainer="Penpot <docker@penpot.app>"
|
||||
|
||||
USER root
|
||||
|
||||
RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot;
|
||||
|
||||
ARG BUNDLE_PATH="./bundle-storybook/"
|
||||
COPY $BUNDLE_PATH /var/www/
|
||||
COPY ./files/nginx.storybook.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
RUN chown -R 1001:0 /var/cache/nginx; \
|
||||
chmod -R g+w /var/cache/nginx; \
|
||||
chown -R 1001:0 /etc/nginx; \
|
||||
chmod -R g+w /etc/nginx; \
|
||||
chown -R 1001:0 /var/www; \
|
||||
chmod -R g+w /var/www;
|
||||
|
||||
USER penpot:penpot
|
||||
@@ -247,11 +247,6 @@ services:
|
||||
networks:
|
||||
- penpot
|
||||
|
||||
environment:
|
||||
# You can increase the max memory size if you have sufficient resources,
|
||||
# although this should not be necessary.
|
||||
- VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu
|
||||
|
||||
## A mailcatch service, used as temporal SMTP server. You can access via HTTP to the
|
||||
## port 1080 for read all emails the penpot platform has sent. Should be only used as a
|
||||
## temporal solution while no real SMTP provider is configured.
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
server {
|
||||
listen 8080 default_server;
|
||||
server_name _;
|
||||
|
||||
charset utf-8;
|
||||
etag off;
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip_types text/plain text/css application/javascript application/json application/vnd.api+json application/xml application/x-javascript text/xml image/svg+xml;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_vary on;
|
||||
|
||||
error_log /dev/stderr;
|
||||
access_log /dev/stdout;
|
||||
|
||||
root /var/www;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
server {
|
||||
listen 8080 default_server;
|
||||
server_name _;
|
||||
|
||||
charset utf-8;
|
||||
etag off;
|
||||
|
||||
gzip on;
|
||||
gzip_static on;
|
||||
gzip_types text/plain text/css application/javascript application/json application/vnd.api+json application/xml application/x-javascript text/xml image/svg+xml;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_vary on;
|
||||
|
||||
error_log /dev/stderr;
|
||||
access_log /dev/stdout;
|
||||
|
||||
root /var/www;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 35 KiB |
@@ -23,7 +23,7 @@ Flags and evironment variables are also used together; for example:
|
||||
|
||||
```bash
|
||||
# This flag enables the use of SMTP email
|
||||
PENPOT_FLAGS: [...] enable-smtp
|
||||
PENPOT_FLAGS: enable-smtp
|
||||
|
||||
# These environment variables configure the specific SMPT service
|
||||
# Backend
|
||||
@@ -36,7 +36,7 @@ the exporter, or all of them; on the other hand, **environment variables** are c
|
||||
each specific service. For example:
|
||||
|
||||
```bash
|
||||
PENPOT_FLAGS: [...] enable-login-with-google
|
||||
PENPOT_FLAGS: enable-login-with-google
|
||||
|
||||
# Backend
|
||||
PENPOT_GOOGLE_CLIENT_ID: <client-id>
|
||||
@@ -56,7 +56,7 @@ Penpot uses anonymous telemetries from the self-hosted instances to improve the
|
||||
Consider sharing these anonymous telemetries enabling the corresponding flag:
|
||||
|
||||
```bash
|
||||
PENPOT_FLAGS: [...] enable-telemetries
|
||||
PENPOT_FLAGS: enable-telemetries
|
||||
```
|
||||
|
||||
## Registration and authentication
|
||||
@@ -402,7 +402,7 @@ This is implemented as specific locations in the penpot-front Nginx. If your org
|
||||
in a 100% air-gapped environment, you can use the following configuration:
|
||||
|
||||
```bash
|
||||
PENPOT_FLAGS: [...] enable-air-gapped-conf
|
||||
PENPOT_FLAGS: enable-air-gapped-conf
|
||||
```
|
||||
|
||||
When Penpot starts, it will leave out the Nginx configuration related to external requests. This means that,
|
||||
@@ -459,15 +459,11 @@ POSTGRES_PASSWORD: penpot
|
||||
|
||||
### Storage
|
||||
|
||||
Storage refers to storing the user uploaded different objects in Penpot (assets, file data,...).
|
||||
Storage refers to storing the user uploaded assets.
|
||||
|
||||
Objects storage is implemented using "plugable" backends. Currently there are two
|
||||
Assets storage is implemented using "plugable" backends. Currently there are two
|
||||
backends available: <code class="language-bash">fs</code> and <code class="language-bash">s3</code> (for AWS S3).
|
||||
|
||||
__Since version 2.11.0__
|
||||
The configuration variables related to storage has been renamed, `PENPOT_STORAGE_ASSETS_*` are now `PENPOT_OBJECTS_STORAGE_*`.
|
||||
`PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its values now are `fs` and `s3` instead of `assets-fs` or `assets-s3`.
|
||||
|
||||
#### FS Backend (default)
|
||||
|
||||
This is the default backend when you use the official docker images and the default
|
||||
@@ -475,8 +471,8 @@ configuration looks like this:
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
PENPOT_OBJECTS_STORAGE_BACKEND: fs
|
||||
PENPOT_OBJECTS_STORAGE_FS_DIRECTORY: /opt/data/objects
|
||||
PENPOT_ASSETS_STORAGE_BACKEND: assets-fs
|
||||
PENPOT_STORAGE_ASSETS_FS_DIRECTORY: /opt/data/assets
|
||||
```
|
||||
|
||||
The main downside of this backend is the hard dependency on nginx approach to serve files
|
||||
@@ -489,7 +485,7 @@ configuration file][4] used in the docker images.
|
||||
|
||||
#### AWS S3 Backend
|
||||
|
||||
This backend uses AWS S3 bucket for store the user uploaded objects. For use it you should
|
||||
This backend uses AWS S3 bucket for store the user uploaded assets. For use it you should
|
||||
have an appropriate account on AWS cloud and have the credentials, region and the bucket.
|
||||
|
||||
This is how configuration looks for S3 backend:
|
||||
@@ -498,36 +494,18 @@ This is how configuration looks for S3 backend:
|
||||
# Backend
|
||||
AWS_ACCESS_KEY_ID: <you-access-key-id-here>
|
||||
AWS_SECRET_ACCESS_KEY: <your-secret-access-key-here>
|
||||
PENPOT_OBJECTS_STORAGE_BACKEND: s3
|
||||
PENPOT_OBJECTS_STORAGE_S3_REGION: <aws-region>
|
||||
PENPOT_OBJECTS_STORAGE_S3_BUCKET: <bucket-name>
|
||||
PENPOT_ASSETS_STORAGE_BACKEND: assets-s3
|
||||
PENPOT_STORAGE_ASSETS_S3_REGION: <aws-region>
|
||||
PENPOT_STORAGE_ASSETS_S3_BUCKET: <bucket-name>
|
||||
|
||||
# Optional if you want to use it with non AWS, S3 compatible service:
|
||||
PENPOT_OBJECTS_STORAGE_S3_ENDPOINT: <endpoint-uri>
|
||||
PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <endpoint-uri>
|
||||
```
|
||||
|
||||
<p class="advice">
|
||||
These settings are equally useful if you have a Minio storage system.
|
||||
</p>
|
||||
|
||||
### File Data Storage
|
||||
|
||||
__Since version 2.11.0__
|
||||
|
||||
You can change the default file data storage backend with `PENPOT_FILE_DATA_BACKEND` environment variable. Possible values are:
|
||||
|
||||
- `legacy-db`: the current default backend, continues storing the file data of files and snapshots in the same location as previous versions of Penpot (< 2.11.0), this is a conservative default behaviour and will be changed to `db` in next versions.
|
||||
- `db`: stores the file data on an specific table (the future default backend).
|
||||
- `storage`: stores the file data using the objects storage system (S3 or FS, depending on which one is configured)
|
||||
|
||||
This also comes with an additional feature that allows offload the "inactive" files on file storage backend and leaves the database only for the active files. To enable it, you should use the `enable-tiered-file-data-storage` flag and `db` as file data storage backend.
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
PENPOT_FLAGS: [...] enable-tiered-file-data-storage
|
||||
PENPOT_FILE_DATA_BACKEND: db
|
||||
```
|
||||
|
||||
### Autosave
|
||||
|
||||
By default, Penpot stores manually saved versions indefinitely; these can be found in the History tab and can be renamed, restored, deleted, etc. Additionally, the default behavior of on-premise instances is to not keep automatic version history. This automatic behavior can be modified and adapted to each on-premise installation with the corresponding configuration.
|
||||
@@ -539,7 +517,7 @@ You need to be very careful when configuring automatic versioning, as it can sig
|
||||
This is how configuration looks for auto-file-snapshot
|
||||
|
||||
```bash
|
||||
PENPOT_FLAGS: [...] enable-auto-file-snapshot # Enable automatic version saving
|
||||
PENPOT_FLAGS: enable-auto-file-snapshot # Enable automatic version saving
|
||||
|
||||
# Backend
|
||||
PENPOT_AUTO_FILE_SNAPSHOT_EVERY: 5 # How many save operations trigger the auto-save-version?
|
||||
|
||||
@@ -217,7 +217,7 @@ repository:
|
||||
|
||||
```bash
|
||||
# cd <repo>/frontend
|
||||
yarn run validate-translations
|
||||
yarn run translations
|
||||
```
|
||||
|
||||
At Penpot core team we maintain manually the english and spanish .po files. All
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: 1.1 Recommended settings
|
||||
title: 1.1 Recommended storage
|
||||
desc: Learn recommended self-hosting settings, Docker & Kubernetes installs, configuration, and troubleshooting tips in Penpot's technical guide.
|
||||
---
|
||||
|
||||
@@ -10,33 +10,3 @@ Disk requirements depend on your usage, with the primary factors being database
|
||||
As a rule of thumb, start with a **minimum** database size of **50GB** to **100GB** with elastic sizing capability — this configuration should adequately support up to 10 editors. For environments with **more than 10 users**, we recommend adding approximately **5GB** of capacity per additional editor.
|
||||
|
||||
Keep in mind that database size doesn't grow strictly proportionally with user count, as it depends heavily on how Penpot is used and the complexity of files created. Most organizations begin with this baseline and elastic sizing approach, then monitor usage patterns monthly until resource requirements stabilize.
|
||||
|
||||
|
||||
# About Valkey / Redis requirements
|
||||
|
||||
"Valkey is mainly used for coordinating websocket notifications and, since Penpot 2.11, as a cache. Therefore, disk storage will not be necessary as it will use the instance's RAM.
|
||||
|
||||
To prevent the cache from hogging all the system's RAM usage, it is recommended to use two configuration parameters which, both in the docker-compose.yaml provided by Penpot and in the official Helm Chart, come with default parameters that should be sufficient for most deployments:
|
||||
|
||||
```bash
|
||||
## Recommended values for most Penpot instances.
|
||||
## You can modify this value to follow your policies.
|
||||
|
||||
# Set maximum memory Valkey/Redis will use.
|
||||
# Accepted units: b, k, kb, m, mb, g, gb
|
||||
maxmemory 128mb
|
||||
|
||||
# Choose an eviction policy (see Valkey docs:
|
||||
# https://valkey.io/topics/memory-optimization/ or for Redis
|
||||
# https://redis.io/docs/latest/develop/reference/eviction/
|
||||
# Common choices:
|
||||
# noeviction, allkeys-lru, volatile-lru, allkeys-random, volatile-random,
|
||||
# volatile-ttl, volatile-lfu, allkeys-lfu
|
||||
#
|
||||
# For Penpot, volatile-lfu is recommended
|
||||
maxmemory-policy volatile-lfu
|
||||
```
|
||||
|
||||
The `maxmemory` configuration directive specifies the maximum amount of memory to use for the cache data. If you are using a dedicated instance to host Valkey/Redis, we do not recommend using more than 60% of the available RAM.
|
||||
|
||||
With `maxmemory-policy` configuration directive, you can select the eviction policy you want to use when the limit set by `maxmemory` is reached. Penpot works fine with `volatile-lfu`, which evicts the least frequently used keys that have been marked as expired.
|
||||
|
||||
@@ -423,40 +423,6 @@ ExtraBold Italic
|
||||
<p>This token can be applied directly to a text element or be used as a reference in a Typography Composite Token.</p>
|
||||
|
||||
|
||||
<h3 id="typography-composite-tokens">Typography composite token</h3>
|
||||
<p><strong>Typography tokens</strong> are composite entities that group several text properties into a single token definition. They allow you to define and reuse complete text styles in a consistent way.</p>
|
||||
<p>Each property within a typography token can either reference an existing <a href="#design-tokens-typography">individual typography token</a> (for example, <em>font-size</em> or <em>font-family</em>) or use a hardcoded value. The behavior and syntax of individual typography tokens are described in the previous section of this guide.</p>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/36-tokens-composite-typography.webp" alt="Typography composite token" />
|
||||
</figure>
|
||||
|
||||
<h4 id="reference-composite-token">Reference another Typography Composite Token</h4>
|
||||
<p>You can also reference another existing <strong>Typography Composite Token</strong> instead of defining each property manually. When doing so, Penpot resolves all individual properties from the referenced token.</p>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/34-tokens-composite-typography-alias.webp" alt="Typography composite token" />
|
||||
</figure>
|
||||
|
||||
<h4 id="line-height-property">Line height property</h4>
|
||||
<p>The <strong>Typography Token</strong> includes a <em>line-height</em> property, which is not available as an individual token. This is because line-height depends on the font size to be calculated properly. Make sure the <em>font-size</em> property is defined before setting <em>line-height</em>.</p>
|
||||
<figure>
|
||||
<img src="/img/design-tokens/35-tokens-composite-typography-lineheight.webp" alt="Typography composite token" />
|
||||
</figure>
|
||||
|
||||
<p>Accepted values for the line-height input:</p>
|
||||
<ul>
|
||||
<li><strong>Unitless number:</strong> interpreted as a multiplier of the font size. This is Penpot’s default behavior.</li>
|
||||
<li><strong>Percentage (%):</strong> converted internally to a multiplier.</li>
|
||||
<li><strong>Pixel (px) or rem value:</strong> if using rem, Penpot calculates the proportion relative to the font size and converts it to a multiplier.</li>
|
||||
<li><strong>References:</strong> you can also reference <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> tokens.</li>
|
||||
</ul>
|
||||
|
||||
<h4 id="apply-typography-token">Apply a Typography token</h4>
|
||||
<p>A <strong>Typography composite token</strong> can be applied to a full text layer to set all typography properties at once. This lets you manage complete text styles using a single token instead of combining multiple individual ones.</p>
|
||||
<p>When applying a Typography composite token to a layer, any previously applied <em>Typography composite token</em> or <em>style</em> will be detached. The same happens in reverse. Only one of them can be active at a time.</p>
|
||||
|
||||
|
||||
|
||||
|
||||
<h2 id="design-tokens-sets">Token Sets</h2>
|
||||
<p>Token Sets allow you to split your tokens up into multiple files in order to create organized groups or collections of tokens. It enables efficient management and customization within design files. For example you can group all your color sets, sizing sets or platform-specific sets. The purpose of tokens sets is to organize them in a way that matches your needs.</p>
|
||||
<figure>
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "Bug 12384",
|
||||
"~:revn": 4,
|
||||
"~:modified-at": "~m1761124840773",
|
||||
"~:name": "Text fills",
|
||||
"~:revn": 26,
|
||||
"~:modified-at": "~m1760450987132",
|
||||
"~:vern": 0,
|
||||
"~:id": "~ufa6ce865-34dd-80ac-8006-fe0dab5539a7",
|
||||
"~:id": "~ub1ff3fdf-b491-812b-8006-f2ce3d29333a",
|
||||
"~:is-shared": false,
|
||||
"~:migrations": {
|
||||
"~#ordered-set": [
|
||||
@@ -92,6 +92,7 @@
|
||||
"0004-clean-shadow-color",
|
||||
"0005-deprecate-image-type",
|
||||
"0006-fix-old-texts-fills",
|
||||
"0007-clear-invalid-strokes-and-fills-v2",
|
||||
"0008-fix-library-colors-v4",
|
||||
"0009-clean-library-colors",
|
||||
"0009-add-partial-text-touched-flags",
|
||||
@@ -99,33 +100,30 @@
|
||||
"0011-fix-invalid-text-touched-flags",
|
||||
"0012-fix-position-data",
|
||||
"0013-fix-component-path",
|
||||
"0013-clear-invalid-strokes-and-fills",
|
||||
"0014-fix-tokens-lib-duplicate-ids",
|
||||
"0014-clear-components-nil-objects"
|
||||
"0014-fix-tokens-lib-duplicate-ids"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~u3e5ffd68-2819-8084-8006-eb1c616e69bf",
|
||||
"~:created-at": "~m1761123649876",
|
||||
"~:created-at": "~m1760368824484",
|
||||
"~:backend": "legacy-db",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~ufa6ce865-34dd-80ac-8006-fe0dab5539a8"
|
||||
"~ub1ff3fdf-b491-812b-8006-f2ce3d29333b"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~ufa6ce865-34dd-80ac-8006-fe0dab5539a8": {
|
||||
"~ub1ff3fdf-b491-812b-8006-f2ce3d29333b": {
|
||||
"~:objects": {
|
||||
"~#penpot/objects-map/v2": {
|
||||
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u3fc80ad6-7d08-8031-8006-fe0dba3fddf7\"]]]",
|
||||
"~u3fc80ad6-7d08-8031-8006-fe0dba3fddf7": "[\"~#shape\",[\"^ \",\"~:y\",250,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Board\",\"~:width\",265,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",616,\"~:y\",250]],[\"^=\",[\"^ \",\"~:x\",881,\"~:y\",250]],[\"^=\",[\"^ \",\"~:x\",881,\"~:y\",494]],[\"^=\",[\"^ \",\"~:x\",616,\"~:y\",494]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:exports\",[[\"^ \",\"^:\",\"~:png\",\"~:suffix\",\"\",\"~:scale\",1]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u3fc80ad6-7d08-8031-8006-fe0dba3fddf7\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",616,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",616,\"~:y\",250,\"^9\",265,\"~:height\",244,\"~:x1\",616,\"~:y1\",250,\"~:x2\",881,\"~:y2\",494]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^O\",244,\"~:flip-y\",null,\"~:shapes\",[\"~u20a28a94-4ab0-801b-8006-fe0e8cee02c3\"]]]",
|
||||
"~u20a28a94-4ab0-801b-8006-fe0e8cee02c3": "[\"~#shape\",[\"^ \",\"~:y\",297.00000381469727,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",65,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",644.0000085830688,\"~:y\",297.00000381469727]],[\"^<\",[\"^ \",\"~:x\",709.0000085830688,\"~:y\",297.00000381469727]],[\"^<\",[\"^ \",\"~:x\",709.0000085830688,\"~:y\",362.00000381469727]],[\"^<\",[\"^ \",\"~:x\",644.0000085830688,\"~:y\",362.00000381469727]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:r1\",0,\"~:id\",\"~u20a28a94-4ab0-801b-8006-fe0e8cee02c3\",\"~:parent-id\",\"~u3fc80ad6-7d08-8031-8006-fe0dba3fddf7\",\"~:frame-id\",\"~u3fc80ad6-7d08-8031-8006-fe0dba3fddf7\",\"~:strokes\",[],\"~:x\",644.0000085830688,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",644.0000085830688,\"~:y\",297.00000381469727,\"^8\",65,\"~:height\",65,\"~:x1\",644.0000085830688,\"~:y1\",297.00000381469727,\"~:x2\",709.0000085830688,\"~:y2\",362.00000381469727]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^N\",65,\"~:flip-y\",null]]"
|
||||
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u23b2a9cc-5d10-800a-8006-f2ce6354e75d\"]]]",
|
||||
"~u23b2a9cc-5d10-800a-8006-f2ce6354e75d": "[\"~#shape\",[\"^ \",\"~:y\",540.9999842227844,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:auto-width\",\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:key\",\"1tx250q8puw\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"\",\"^;\",\"normal\",\"~:typography-ref-id\",null,\"~:text-transform\",\"none\",\"~:font-id\",\"sourcesanspro\",\"^8\",\"1c1skqx1y2p\",\"~:font-size\",\"36\",\"~:font-weight\",\"700\",\"~:typography-ref-file\",null,\"~:font-variant-id\",\"bold\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-opacity\",1],[\"^ \",\"^F\",\"#00ffc3\",\"^G\",1],[\"^ \",\"^F\",\"#d9ff00\",\"^G\",1],[\"^ \",\"^F\",\"#192b60\",\"^G\",1],[\"^ \",\"^F\",\"#7f9fff\",\"^G\",1],[\"^ \",\"^F\",\"#ff00ca\",\"^G\",1],[\"^ \",\"^F\",\"#003fff\",\"^G\",1]],\"~:font-family\",\"sourcesanspro\",\"~:text\",\"Lorem ipsum\"]],\"^<\",null,\"^=\",\"none\",\"~:text-align\",\"left\",\"^>\",\"sourcesanspro\",\"^8\",\"12a6vwimj9j\",\"^?\",\"0\",\"^@\",\"700\",\"^A\",null,\"~:text-direction\",\"ltr\",\"^7\",\"paragraph\",\"^B\",\"bold\",\"^C\",\"none\",\"^D\",\"0\",\"^E\",[[\"^ \",\"^F\",\"#000000\",\"^G\",1],[\"^ \",\"^F\",\"#00ffc3\",\"^G\",1],[\"^ \",\"^F\",\"#d9ff00\",\"^G\",1],[\"^ \",\"^F\",\"#192b60\",\"^G\",1],[\"^ \",\"^F\",\"#7f9fff\",\"^G\",1],[\"^ \",\"^F\",\"#ff00ca\",\"^G\",1],[\"^ \",\"^F\",\"#003fff\",\"^G\",1]],\"^H\",\"sourcesanspro\"]]]],\"~:vertical-align\",\"top\"],\"~:hide-in-viewer\",false,\"~:name\",\"Lorem ipsum\",\"~:width\",207.000004835394,\"^7\",\"^I\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",321.9999624852649,\"~:y\",540.9999842227845]],[\"^Q\",[\"^ \",\"~:x\",528.9999673206589,\"~:y\",540.9999842227845]],[\"^Q\",[\"^ \",\"~:x\",528.9999673206589,\"~:y\",583.9999821366218]],[\"^Q\",[\"^ \",\"~:x\",321.9999624852649,\"~:y\",583.9999821366218]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u23b2a9cc-5d10-800a-8006-f2ce6354e75d\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:x\",321.99996248526486,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",321.99996248526486,\"~:y\",540.9999842227844,\"^O\",207.000004835394,\"~:height\",42.99999791383732,\"~:x1\",321.99996248526486,\"~:y1\",540.9999842227844,\"~:x2\",528.9999673206589,\"~:y2\",583.9999821366217]],\"~:flip-x\",null,\"^X\",42.99999791383732,\"~:flip-y\",null]]"
|
||||
}
|
||||
},
|
||||
"~:id": "~ufa6ce865-34dd-80ac-8006-fe0dab5539a8",
|
||||
"~:id": "~ub1ff3fdf-b491-812b-8006-f2ce3d29333b",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~ufa6ce865-34dd-80ac-8006-fe0dab5539a7",
|
||||
"~:id": "~ub1ff3fdf-b491-812b-8006-f2ce3d29333a",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
@@ -1,31 +0,0 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import DashboardPage from "../pages/DashboardPage";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await DashboardPage.init(page);
|
||||
await DashboardPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"logged-in-user/get-profile-logged-in-no-onboarding.json",
|
||||
);
|
||||
});
|
||||
|
||||
test("BUG 12359 - Selected invitations count is not pluralized", async ({
|
||||
page,
|
||||
}) => {
|
||||
const dashboardPage = new DashboardPage(page);
|
||||
await dashboardPage.setupDashboardFull();
|
||||
await dashboardPage.setupTeamInvitations();
|
||||
|
||||
await dashboardPage.goToSecondTeamInvitationsSection();
|
||||
|
||||
await expect(page.getByText("test1@mail.com")).toBeVisible();
|
||||
|
||||
// NOTE: we cannot use check() or getByLabel() because the checkbox
|
||||
// is hidden inside the label.
|
||||
await page.getByText("test1@mail.com").click();
|
||||
await expect(page.getByText("1 invitation selected")).toBeVisible();
|
||||
|
||||
await page.getByText("test2@mail.com").check();
|
||||
await expect(page.getByText("2 invitations selected")).toBeVisible();
|
||||
});
|
||||
@@ -89,6 +89,38 @@ test.describe("Shape attributes", () => {
|
||||
|
||||
await expect(workspace.page.getByTestId("add-fill")).toBeDisabled();
|
||||
});
|
||||
|
||||
test("Cannot add a new text fill when the limit has been reached", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page);
|
||||
await workspace.mockConfigFlags(["enable-feature-render-wasm"]);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockRPC(
|
||||
/get\-file\?/,
|
||||
"design/get-file-text-fills-limit.json",
|
||||
);
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
fileId: "b1ff3fdf-b491-812b-8006-f2ce3d29333a",
|
||||
pageId: "b1ff3fdf-b491-812b-8006-f2ce3d29333b",
|
||||
});
|
||||
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
|
||||
await expect(
|
||||
workspace.page.getByRole("button", { name: "Remove color" }),
|
||||
).toHaveCount(7);
|
||||
|
||||
await workspace.page.getByRole("button", { name: "Add fill" }).click();
|
||||
await expect(
|
||||
workspace.page.getByRole("button", { name: "Remove color" }),
|
||||
).toHaveCount(8);
|
||||
|
||||
await expect(
|
||||
workspace.page.getByRole("button", { name: "Add fill" }),
|
||||
).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Multiple shapes attributes", () => {
|
||||
@@ -288,50 +320,3 @@ test("BUG 12287 Fix identical text fills not being added/removed", async ({
|
||||
workspace.page.getByRole("button", { name: "#B1B2B5" }),
|
||||
).toHaveCount(3);
|
||||
});
|
||||
|
||||
test("BUG 12384 - Export crashing when exporting a board", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockRPC(/get\-file\?/, "design/get-file-12384.json");
|
||||
|
||||
let hasExportRequestBeenIntercepted = false;
|
||||
await workspace.page.route("**/api/export", (route) => {
|
||||
if (hasExportRequestBeenIntercepted) {
|
||||
route.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
hasExportRequestBeenIntercepted = true;
|
||||
const payload = route.request().postData();
|
||||
const parsedPayload = JSON.parse(payload);
|
||||
|
||||
expect(parsedPayload["~:exports"]).toHaveLength(1);
|
||||
expect(parsedPayload["~:exports"][0]["~:file-id"]).toBe(
|
||||
"~ufa6ce865-34dd-80ac-8006-fe0dab5539a7",
|
||||
);
|
||||
expect(parsedPayload["~:exports"][0]["~:page-id"]).toBe(
|
||||
"~ufa6ce865-34dd-80ac-8006-fe0dab5539a8",
|
||||
);
|
||||
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
response: {},
|
||||
});
|
||||
});
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
fileId: "fa6ce865-34dd-80ac-8006-fe0dab5539a7",
|
||||
pageId: "fa6ce865-34dd-80ac-8006-fe0dab5539a8",
|
||||
});
|
||||
|
||||
await workspace.clickLeafLayer("Board");
|
||||
|
||||
let exportRequest = workspace.page.waitForRequest("**/api/export");
|
||||
|
||||
await workspace.rightSidebar
|
||||
.getByRole("button", { name: "Export 1 element" })
|
||||
.click();
|
||||
|
||||
await exportRequest;
|
||||
});
|
||||
|
||||
@@ -95,24 +95,6 @@ const setupTypographyTokensFile = async (page, options = {}) => {
|
||||
});
|
||||
};
|
||||
|
||||
const checkInputFieldWithError = async (tokenThemeUpdateCreateModal, inputLocator) => {
|
||||
await expect(inputLocator).toHaveAttribute("aria-invalid", "true");
|
||||
|
||||
const errorMessageId = await inputLocator.getAttribute("aria-describedby");
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.locator(`#${errorMessageId}`),
|
||||
).toBeVisible();
|
||||
};
|
||||
|
||||
const checkInputFieldWithoutError = async (tokenThemeUpdateCreateModal, inputLocator) => {
|
||||
expect(
|
||||
await inputLocator.getAttribute("aria-invalid")
|
||||
).toBeNull();
|
||||
expect(
|
||||
await inputLocator.getAttribute("aria-describedby")
|
||||
).toBeNull();
|
||||
};
|
||||
|
||||
test.describe("Tokens: Tokens Tab", () => {
|
||||
test("Clicking tokens tab button opens tokens sidebar tab", async ({
|
||||
page,
|
||||
@@ -824,25 +806,18 @@ test.describe("Tokens: Themes modal", () => {
|
||||
})
|
||||
.click();
|
||||
|
||||
const groupInput = tokenThemeUpdateCreateModal.getByLabel("Group");
|
||||
const nameInput = tokenThemeUpdateCreateModal.getByLabel("Theme");
|
||||
const saveButton = tokenThemeUpdateCreateModal.getByRole("button", {
|
||||
name: "Save theme",
|
||||
});
|
||||
|
||||
await groupInput.fill("Core"); // Invalid because "Core / Light" theme already exists
|
||||
await nameInput.fill("Light");
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByLabel("Group")
|
||||
.fill("New Group name");
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByLabel("Theme")
|
||||
.fill("New Theme name");
|
||||
|
||||
await checkInputFieldWithError(tokenThemeUpdateCreateModal, nameInput);
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
await groupInput.fill("New Group name");
|
||||
await nameInput.fill("New Theme name");
|
||||
|
||||
await checkInputFieldWithoutError(tokenThemeUpdateCreateModal, nameInput);
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
|
||||
await saveButton.click();
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByRole("button", {
|
||||
name: "Save theme",
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByText("New Theme name"),
|
||||
@@ -870,36 +845,12 @@ test.describe("Tokens: Themes modal", () => {
|
||||
.first()
|
||||
.click();
|
||||
|
||||
const groupInput = tokenThemeUpdateCreateModal.getByLabel("Group");
|
||||
const nameInput = tokenThemeUpdateCreateModal.getByLabel("Theme");
|
||||
const saveButton = tokenThemeUpdateCreateModal.getByRole("button", {
|
||||
name: "Save theme",
|
||||
});
|
||||
|
||||
await groupInput.fill("Core"); // Invalid because "Core / Dark" theme already exists
|
||||
await nameInput.fill("Dark");
|
||||
|
||||
await checkInputFieldWithError(tokenThemeUpdateCreateModal, nameInput);
|
||||
await expect(saveButton).toBeDisabled();
|
||||
|
||||
await groupInput.fill("Core"); // Valid because "Core / Light" theme already exists
|
||||
await nameInput.fill("Light"); // but it's the same theme we are editing
|
||||
|
||||
await checkInputFieldWithoutError(tokenThemeUpdateCreateModal, nameInput);
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
|
||||
await nameInput.fill("Changed Theme name"); // New names should be also valid
|
||||
await groupInput.fill("Changed Group name");
|
||||
|
||||
await checkInputFieldWithoutError(tokenThemeUpdateCreateModal, nameInput);
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
|
||||
expect(
|
||||
await nameInput.getAttribute("aria-invalid")
|
||||
).toBeNull();
|
||||
expect(
|
||||
await nameInput.getAttribute("aria-describedby")
|
||||
).toBeNull();
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByLabel("Theme")
|
||||
.fill("Changed Theme name");
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByLabel("Group")
|
||||
.fill("Changed Group name");
|
||||
|
||||
const checkboxes = await tokenThemeUpdateCreateModal
|
||||
.locator('[role="checkbox"]')
|
||||
@@ -913,15 +864,11 @@ test.describe("Tokens: Themes modal", () => {
|
||||
}
|
||||
}
|
||||
|
||||
const firstButton = await tokenThemeUpdateCreateModal
|
||||
.getByTestId('tokens-set-item')
|
||||
.first();
|
||||
|
||||
await firstButton.click();
|
||||
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
|
||||
await saveButton.click();
|
||||
await tokenThemeUpdateCreateModal
|
||||
.getByRole("button", {
|
||||
name: "Save theme",
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
tokenThemeUpdateCreateModal.getByText("Changed Theme name"),
|
||||
|
||||
@@ -150,8 +150,8 @@ test("User copy paste a variant container", async ({ page }) => {
|
||||
await workspacePage.clickAt(500, 500);
|
||||
await workspacePage.page.keyboard.press("Control+v");
|
||||
|
||||
const variant_original = await findVariant(workspacePage, 1);
|
||||
const variant_duplicate = await findVariant(workspacePage, 0);
|
||||
const variant_original = await findVariant(workspacePage, 0);
|
||||
const variant_duplicate = await findVariant(workspacePage, 1);
|
||||
|
||||
// Expand the layers
|
||||
await variant_duplicate.container.getByRole("button").first().click();
|
||||
|
||||
@@ -28,13 +28,29 @@ const setupFileWithAssets = async (workspace) => {
|
||||
return { fileId, pageId };
|
||||
};
|
||||
|
||||
test("Shows the workspace correctly for a blank file", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
test.describe("Viewport", () => {
|
||||
test("Shows the workspace correctly for a blank file", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await expect(workspace.page).toHaveScreenshot();
|
||||
await expect(workspace.page).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("User creates a rectangle and locks it", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page);
|
||||
await workspace.setupEmptyFile(page);
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await workspace.rectShapeButton.click();
|
||||
await workspace.clickWithDragViewportAt(128, 128, 200, 100);
|
||||
await workspace.clickLeafLayer("Rectangle");
|
||||
|
||||
await page.keyboard.press("Shift+Control+L");
|
||||
|
||||
await expect(workspace.page).toHaveScreenshot();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Design tab", () => {
|
||||
@@ -145,4 +161,4 @@ test.describe("Palette", () => {
|
||||
workspace.palette.getByRole("button", { name: "#7798ff" }),
|
||||
).toBeVisible();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Before Width: | Height: | Size: 109 KiB |
|
Before Width: | Height: | Size: 239 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 100 KiB |
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# NOTE: this script should be called from the parent directory to
|
||||
# properly work.
|
||||
|
||||
export CURRENT_VERSION=$1;
|
||||
export BUILD_DATE=$(date -R);
|
||||
export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
|
||||
export TS=$(date +%s);
|
||||
|
||||
export NODE_ENV=production;
|
||||
|
||||
echo "Current path:"
|
||||
echo $PATH
|
||||
|
||||
set -ex
|
||||
|
||||
corepack enable;
|
||||
corepack install || exit 1;
|
||||
yarn install || exit 1;
|
||||
|
||||
yarn run build:storybook || exit 1;
|
||||
@@ -139,9 +139,9 @@
|
||||
(fn [data]
|
||||
(assoc file :data (d/removem (comp t/pointer? val) data))))))
|
||||
|
||||
(defn- check-libraries-synchronization
|
||||
(defn- check-libraries-synchronozation
|
||||
[file-id libraries]
|
||||
(ptk/reify ::check-libraries-synchronization
|
||||
(ptk/reify ::check-libraries-synchronozation
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file (dsh/lookup-file state file-id)
|
||||
@@ -154,7 +154,7 @@
|
||||
libraries)]
|
||||
|
||||
(when needs-check?
|
||||
(->> (rx/of (dwl/notify-sync-file))
|
||||
(->> (rx/of (dwl/notify-sync-file file-id))
|
||||
(rx/delay 1000)))))))
|
||||
|
||||
(defn- library-resolved
|
||||
@@ -168,32 +168,30 @@
|
||||
[file-id features]
|
||||
(ptk/reify ::fetch-libries
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
|
||||
(->> (rx/concat
|
||||
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
|
||||
(rx/mapcat
|
||||
(fn [libraries]
|
||||
(rx/concat
|
||||
(rx/of (dwl/libraries-fetched file-id libraries))
|
||||
(rx/merge
|
||||
(->> (rx/from libraries)
|
||||
(rx/merge-map
|
||||
(fn [{:keys [id synced-at]}]
|
||||
(->> (rp/cmd! :get-file {:id id :features features})
|
||||
(rx/map #(assoc % :synced-at synced-at :library-of file-id)))))
|
||||
(rx/mapcat resolve-file)
|
||||
(rx/map library-resolved))
|
||||
(->> (rx/from libraries)
|
||||
(rx/map :id)
|
||||
(rx/mapcat (fn [file-id]
|
||||
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
|
||||
(rx/map dwl/library-thumbnails-fetched)))
|
||||
(rx/of (check-libraries-synchronization file-id libraries))))))
|
||||
(watch [_ _ _]
|
||||
(rx/concat
|
||||
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
|
||||
(rx/mapcat
|
||||
(fn [libraries]
|
||||
(rx/concat
|
||||
(rx/of (dwl/libraries-fetched file-id libraries))
|
||||
(rx/merge
|
||||
(->> (rx/from libraries)
|
||||
(rx/merge-map
|
||||
(fn [{:keys [id synced-at]}]
|
||||
(->> (rp/cmd! :get-file {:id id :features features})
|
||||
(rx/map #(assoc % :synced-at synced-at :library-of file-id)))))
|
||||
(rx/mapcat resolve-file)
|
||||
(rx/map library-resolved))
|
||||
(->> (rx/from libraries)
|
||||
(rx/map :id)
|
||||
(rx/mapcat (fn [file-id]
|
||||
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
|
||||
(rx/map dwl/library-thumbnails-fetched)))
|
||||
(rx/of (check-libraries-synchronozation file-id libraries))))))
|
||||
|
||||
;; This events marks that all the libraries have been resolved
|
||||
(rx/of (ptk/data-event ::all-libraries-resolved)))
|
||||
(rx/take-until stopper-s))))))
|
||||
;; This events marks that all the libraries have been resolved
|
||||
(rx/of (ptk/data-event ::all-libraries-resolved))))))
|
||||
|
||||
(defn- workspace-initialized
|
||||
[file-id]
|
||||
|
||||
@@ -848,10 +848,6 @@
|
||||
index
|
||||
0)
|
||||
|
||||
index (if index
|
||||
index
|
||||
(dec (count (dm/get-in page-objects [parent-id :shapes]))))
|
||||
|
||||
selected (if (and (ctl/flex-layout? page-objects parent-id) (not (ctl/reverse? page-objects parent-id)))
|
||||
(into (d/ordered-set) (reverse selected))
|
||||
selected)
|
||||
|
||||
@@ -154,8 +154,23 @@
|
||||
|
||||
(transform-fill* state ids transform-attrs options))))
|
||||
|
||||
(defn swap-attrs [shape attr index new-index]
|
||||
(let [first (get-in shape [attr index])
|
||||
second (get-in shape [attr new-index])]
|
||||
(-> shape
|
||||
(assoc-in [attr index] second)
|
||||
(assoc-in [attr new-index] first))))
|
||||
|
||||
(defn- swap-fills-index
|
||||
[fills index new-index]
|
||||
(let [first (get fills index)
|
||||
second (get fills new-index)]
|
||||
(-> fills
|
||||
(assoc index second)
|
||||
(assoc new-index first))))
|
||||
|
||||
(defn reorder-fills
|
||||
[ids from-pos to-space-between-pos]
|
||||
[ids index new-index]
|
||||
(ptk/reify ::reorder-fills
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -167,7 +182,7 @@
|
||||
|
||||
transform-attrs
|
||||
(fn [object]
|
||||
(update object :fills types.fills/update d/reorder from-pos to-space-between-pos))]
|
||||
(update object :fills types.fills/update swap-fills-index index new-index))]
|
||||
|
||||
(rx/concat
|
||||
(rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids))
|
||||
@@ -500,22 +515,22 @@
|
||||
{:attrs [:strokes]}))))))
|
||||
|
||||
(defn reorder-shadows
|
||||
[ids from-pos to-space-between-pos]
|
||||
[ids index new-index]
|
||||
(ptk/reify ::reorder-shadow
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (dwsh/update-shapes
|
||||
ids
|
||||
#(update % :shadow d/reorder from-pos to-space-between-pos))))))
|
||||
#(swap-attrs % :shadow index new-index))))))
|
||||
|
||||
(defn reorder-strokes
|
||||
[ids from-pos to-space-between-pos]
|
||||
[ids index new-index]
|
||||
(ptk/reify ::reorder-strokes
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of (dwsh/update-shapes
|
||||
ids
|
||||
#(update % :strokes d/reorder from-pos to-space-between-pos)
|
||||
#(swap-attrs % :strokes index new-index)
|
||||
{:attrs [:strokes]})))))
|
||||
|
||||
(defn picker-for-selected-shape
|
||||
@@ -1122,16 +1137,20 @@
|
||||
ref-id (:stroke-color-ref-id stroke)
|
||||
|
||||
colors (-> libraries
|
||||
(get ref-file)
|
||||
(get ref-id)
|
||||
(get :data)
|
||||
(ctl/get-colors))
|
||||
shared? (contains? colors ref-id)
|
||||
has-color? (:stroke-color stroke)
|
||||
|
||||
is-shared? (contains? colors ref-id)
|
||||
has-color? (or (:stroke-color stroke)
|
||||
(:stroke-color-gradient stroke))
|
||||
attrs (cond-> (clr/stroke->color stroke)
|
||||
(not (or is-shared? (= ref-file file-id)))
|
||||
(dissoc :ref-id :ref-file))]
|
||||
base-attrs (cond-> (clr/stroke->color stroke)
|
||||
(not (or shared? (= ref-file file-id)))
|
||||
(dissoc :ref-file :ref-id))
|
||||
|
||||
attrs (cond-> base-attrs
|
||||
(:has-token-applied stroke)
|
||||
(assoc :has-token-applied true
|
||||
:token-name (:token-name stroke)))]
|
||||
|
||||
(when has-color?
|
||||
{:attrs attrs
|
||||
@@ -1139,7 +1158,25 @@
|
||||
:shape-id (:shape-id stroke)
|
||||
:index (:index stroke)})))
|
||||
|
||||
(defn- shadow->color-att
|
||||
(defn- shadow->color-attr
|
||||
"Given a stroke map enriched with :shape-id, :index, and optionally
|
||||
:has-token-applied / :token-name, returns a color attribute map.
|
||||
|
||||
If :has-token-applied is true, adds token metadata to :attrs:
|
||||
{:has-token-applied true
|
||||
:token-name <token-name>}
|
||||
|
||||
Args:
|
||||
- stroke: map with stroke info, including :shape-id and :index
|
||||
- file-id: current file UUID
|
||||
- libraries: map of shared color libraries
|
||||
|
||||
Returns:
|
||||
A map like:
|
||||
{:attrs {...color data...}
|
||||
:prop :stroke
|
||||
:shape-id <uuid>
|
||||
:index <int>}"
|
||||
[shadow file-id libraries]
|
||||
(let [color (get shadow :color)
|
||||
ref-file (get color :ref-file)
|
||||
@@ -1187,6 +1224,24 @@
|
||||
(map #(text->color-att % file-id libraries)))))
|
||||
|
||||
(defn- fill->color-att
|
||||
"Given a fill map enriched with :shape-id, :index, and optionally
|
||||
:has-token-applied / :token-name, returns a color attribute map.
|
||||
|
||||
If :has-token-applied is true, adds token metadata to :attrs:
|
||||
{:has-token-applied true
|
||||
:token-name <token-name>}
|
||||
|
||||
Args:
|
||||
- fill: map with fill info, including :shape-id and :index
|
||||
- file-id: current file UUID
|
||||
- libraries: map of shared color libraries
|
||||
|
||||
Returns:
|
||||
A map like:
|
||||
{:attrs {...color data...}
|
||||
:prop :fill
|
||||
:shape-id <uuid>
|
||||
:index <int>}"
|
||||
[fill file-id libraries]
|
||||
(let [ref-file (:fill-color-ref-file fill)
|
||||
ref-id (:fill-color-ref-id fill)
|
||||
@@ -1198,9 +1253,15 @@
|
||||
shared? (contains? colors ref-id)
|
||||
has-color? (or (:fill-color fill)
|
||||
(:fill-color-gradient fill))
|
||||
attrs (cond-> (types.fills/fill->color fill)
|
||||
|
||||
base-attrs (cond-> (types.fills/fill->color fill)
|
||||
(not (or shared? (= ref-file file-id)))
|
||||
(dissoc :ref-file :ref-id))]
|
||||
(dissoc :ref-file :ref-id))
|
||||
|
||||
attrs (cond-> base-attrs
|
||||
(:has-token-applied fill)
|
||||
(assoc :has-token-applied true
|
||||
:token-name (:token-name fill)))]
|
||||
|
||||
(when has-color?
|
||||
{:attrs attrs
|
||||
@@ -1209,21 +1270,55 @@
|
||||
:index (:index fill)})))
|
||||
|
||||
(defn extract-all-colors
|
||||
"Extracts color information from a list of shapes, including fills, strokes, and shadows.
|
||||
If a shape has applied tokens of type :fill or :stroke-color, the first fill or stroke
|
||||
will include extra attributes in its :attrs map:
|
||||
{:has-token-applied true
|
||||
:token-name <token-name>}
|
||||
|
||||
Args:
|
||||
- shapes: vector of shape maps
|
||||
- file-id: current file UUID
|
||||
- libraries: map of shared color libraries
|
||||
|
||||
Returns:
|
||||
A vector of color attribute maps with metadata for each shape."
|
||||
[shapes file-id libraries]
|
||||
(reduce
|
||||
(fn [result shape]
|
||||
(let [fill-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:fills shape))
|
||||
stroke-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:strokes shape))
|
||||
shadow-obj (map-indexed #(assoc %2 :shape-id (:id shape) :index %1) (:shadow shape))]
|
||||
(let [applied-tokens (:applied-tokens shape)
|
||||
applied-fill (get applied-tokens :fill)
|
||||
applied-stroke (get applied-tokens :stroke-color)
|
||||
fills (:fills shape)
|
||||
strokes (:strokes shape)
|
||||
shadows (:shadow shape)
|
||||
shape-id (:id shape)
|
||||
|
||||
fills* (map-indexed
|
||||
(fn [index fill]
|
||||
(cond-> (assoc fill :shape-id shape-id :index index)
|
||||
(and (zero? index) applied-fill)
|
||||
(assoc :has-token-applied true
|
||||
:token-name applied-fill)))
|
||||
fills)
|
||||
|
||||
strokes* (map-indexed
|
||||
(fn [index stroke]
|
||||
(cond-> (assoc stroke :shape-id shape-id :index index)
|
||||
(and (zero? index) applied-stroke)
|
||||
(assoc :has-token-applied true
|
||||
:token-name applied-stroke)))
|
||||
strokes)
|
||||
|
||||
shadows* (map-indexed #(assoc %2 :shape-id shape-id :index %1) shadows)]
|
||||
(if (= :text (:type shape))
|
||||
(-> result
|
||||
(into (keep #(stroke->color-att % file-id libraries)) stroke-obj)
|
||||
(into (map #(shadow->color-att % file-id libraries)) shadow-obj)
|
||||
(into (keep #(stroke->color-att % file-id libraries)) strokes*)
|
||||
(into (map #(shadow->color-attr % file-id libraries)) shadows*)
|
||||
(into (extract-text-colors shape file-id libraries)))
|
||||
|
||||
(-> result
|
||||
(into (keep #(fill->color-att % file-id libraries)) fill-obj)
|
||||
(into (keep #(stroke->color-att % file-id libraries)) stroke-obj)
|
||||
(into (map #(shadow->color-att % file-id libraries)) shadow-obj)))))
|
||||
(into (keep #(fill->color-att % file-id libraries)) fills*)
|
||||
(into (keep #(stroke->color-att % file-id libraries)) strokes*)
|
||||
(into (map #(shadow->color-attr % file-id libraries)) shadows*)))))
|
||||
[]
|
||||
shapes))
|
||||
|
||||
@@ -1193,22 +1193,19 @@
|
||||
(ctf/used-assets-changed-since file-data library sync-date))))))
|
||||
|
||||
(defn notify-sync-file
|
||||
"Notify the user that there are updates in the libraries used by the
|
||||
current file, and ask if he wants to update them now."
|
||||
[]
|
||||
;; file-id is the id of the modified library
|
||||
[file-id]
|
||||
(dm/assert! (uuid? file-id))
|
||||
(ptk/reify ::notify-sync-file
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (:current-file-id state)
|
||||
file (dsh/lookup-file state file-id)
|
||||
(let [file (dsh/lookup-file state (:current-file-id state))
|
||||
file-data (get file :data)
|
||||
ignore-until (get file :ignore-sync-until)
|
||||
|
||||
libraries-need-sync
|
||||
(->> (vals (get state :files))
|
||||
(filter #(= (:library-of %) file-id))
|
||||
(filter #(seq (assets-need-sync % file-data ignore-until))))
|
||||
|
||||
(filter #(seq (assets-need-sync % file-data ignore-until))
|
||||
(vals (get state :files)))
|
||||
do-more-info
|
||||
#(modal/show! :libraries-dialog {:starting-tab "updates" :file-id file-id})
|
||||
|
||||
@@ -1278,7 +1275,7 @@
|
||||
(watch [_ _ stream]
|
||||
(let [stopper-s
|
||||
(->> stream
|
||||
(rx/filter #(or (= ::dwpg/finalize-page (ptk/type %))
|
||||
(rx/filter #(or (= ::dw/finalize-page (ptk/type %))
|
||||
(= ::watch-component-changes (ptk/type %)))))
|
||||
|
||||
workspace-data-s
|
||||
|
||||
@@ -191,6 +191,30 @@
|
||||
(d/not-empty? position-data)
|
||||
(assoc :position-data position-data))))
|
||||
|
||||
(defn update-grow-type
|
||||
[shape old-shape]
|
||||
(let [auto-width? (= :auto-width (:grow-type shape))
|
||||
auto-height? (= :auto-height (:grow-type shape))
|
||||
|
||||
changed-width? (> (mth/abs (- (:width shape) (:width old-shape))) 0.1)
|
||||
changed-height? (> (mth/abs (- (:height shape) (:height old-shape))) 0.1)
|
||||
|
||||
;; Check if the shape is in a flex layout context that might cause layout-driven changes
|
||||
;; We should be more conservative about converting auto-width to fixed when the shape
|
||||
;; is part of a layout system that could cause automatic resizing
|
||||
has-layout-item-sizing? (or (:layout-item-h-sizing shape) (:layout-item-v-sizing shape))
|
||||
|
||||
;; Only convert auto-width to fixed if:
|
||||
;; 1. For auto-width: both width AND height changed (indicating user manipulation, not layout)
|
||||
;; 2. For auto-height: only height changed
|
||||
;; 3. The shape is not in a layout context where automatic sizing changes are expected
|
||||
change-to-fixed? (and (not has-layout-item-sizing?)
|
||||
(or (and auto-width? changed-width? changed-height?)
|
||||
(and auto-height? changed-height?)))]
|
||||
(cond-> shape
|
||||
change-to-fixed?
|
||||
(assoc :grow-type :fixed))))
|
||||
|
||||
(defn- set-wasm-props!
|
||||
[objects prev-wasm-props wasm-props]
|
||||
(let [;; Set old value for previous properties
|
||||
@@ -786,7 +810,9 @@
|
||||
(-> shape
|
||||
(gsh/transform-shape modifiers)
|
||||
(cond-> (d/not-empty? pos-data)
|
||||
(assoc-position-data pos-data shape)))))]
|
||||
(assoc-position-data pos-data shape))
|
||||
(cond-> text-shape?
|
||||
(update-grow-type shape)))))]
|
||||
|
||||
(rx/of (ptk/event ::dwg/move-frame-guides {:ids ids-with-children :modifiers object-modifiers})
|
||||
(ptk/event ::dwcm/move-frame-comment-threads ids-with-children)
|
||||
@@ -831,20 +857,23 @@
|
||||
(rx/empty))))))))
|
||||
|
||||
;; Pure function to determine next grow-type for text layers
|
||||
(defn next-grow-type
|
||||
[current-grow-type scalev]
|
||||
(defn next-grow-type [current-grow-type resize-direction]
|
||||
(cond
|
||||
(= current-grow-type :fixed)
|
||||
:fixed
|
||||
|
||||
(and (not (mth/close? (:y scalev) 1.0))
|
||||
(and (= resize-direction :horizontal)
|
||||
(= current-grow-type :auto-width))
|
||||
:auto-height
|
||||
|
||||
(and (= resize-direction :horizontal)
|
||||
(= current-grow-type :auto-height))
|
||||
:auto-height
|
||||
|
||||
(and (= resize-direction :vertical)
|
||||
(or (= current-grow-type :auto-width)
|
||||
(= current-grow-type :auto-height)))
|
||||
:fixed
|
||||
|
||||
(and (not (mth/close? (:x scalev) 1.0))
|
||||
(= current-grow-type :auto-width))
|
||||
:auto-height
|
||||
|
||||
:else
|
||||
current-grow-type))
|
||||
|
||||
@@ -331,4 +331,4 @@
|
||||
(watch [_ state _]
|
||||
(when (contains? (:files state) file-id)
|
||||
(rx/of (dwl/ext-library-changed file-id modified-at revn changes)
|
||||
(dwl/notify-sync-file))))))
|
||||
(dwl/notify-sync-file file-id))))))
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.drawing.common :as dwdc]
|
||||
[app.main.data.workspace.edition :as dwe]
|
||||
[app.main.data.workspace.pages :as-alias dwpg]
|
||||
[app.main.data.workspace.path.changes :as changes]
|
||||
[app.main.data.workspace.path.common :as common]
|
||||
[app.main.data.workspace.path.helpers :as helpers]
|
||||
@@ -44,7 +43,7 @@
|
||||
(= type :app.main.data.workspace.path.shortcuts/esc-pressed)
|
||||
(= type :app.main.data.workspace.common/clear-edition-mode)
|
||||
(= type :app.main.data.workspace.edition/clear-edition-mode)
|
||||
(= type ::dwpg/finalize-page)
|
||||
(= type :app.main.data.workspace/finalize-page)
|
||||
(= event :interrupt) ;; ESC
|
||||
(and ^boolean (mse/mouse-event? event)
|
||||
^boolean (mse/mouse-double-click-event? event)))))
|
||||
|
||||
@@ -10,8 +10,6 @@
|
||||
[app.common.data.undo-stack :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.workspace.common :as dwc]
|
||||
[app.main.data.workspace.edition :as-alias dwe]
|
||||
[app.main.data.workspace.pages :as-alias dwpg]
|
||||
[app.main.data.workspace.path.changes :as changes]
|
||||
[app.main.data.workspace.path.common :as common]
|
||||
[app.main.data.workspace.path.state :as st]
|
||||
@@ -135,8 +133,8 @@
|
||||
|
||||
(defn- stop-undo? [event]
|
||||
(let [type (ptk/type event)]
|
||||
(or (= ::dwe/clear-edition-mode type)
|
||||
(= ::dwpg/finalize-page type))))
|
||||
(or (= :app.main.data.workspace.edition/clear-edition-mode type)
|
||||
(= :app.main.data.workspace/finalize-page type))))
|
||||
|
||||
(def path-content-ref
|
||||
(letfn [(selector [state]
|
||||
|
||||
@@ -24,7 +24,6 @@
|
||||
[app.main.data.modal :as md]
|
||||
[app.main.data.workspace.collapse :as dwc]
|
||||
[app.main.data.workspace.edition :as dwe]
|
||||
[app.main.data.workspace.pages :as-alias dwpg]
|
||||
[app.main.data.workspace.specialized-panel :as-alias dwsp]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.zoom :as dwz]
|
||||
@@ -464,7 +463,7 @@
|
||||
library-data (dsh/lookup-file-data state file-id)
|
||||
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(cll/generate-duplicate-changes objects page ids delta libraries library-data file-id {:alt-duplication? alt-duplication?})
|
||||
(cll/generate-duplicate-changes objects page ids delta libraries library-data file-id)
|
||||
(cll/generate-duplicate-changes-update-indices objects ids))
|
||||
|
||||
tags (or (:tags changes) #{})
|
||||
@@ -597,21 +596,21 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [stopper (rx/filter #(or (= ::toggle-focus-mode (ptk/type %))
|
||||
(= ::dwpg/finalize-page (ptk/type %))) stream)]
|
||||
(= :app.main.data.workspace/finalize-page (ptk/type %))) stream)]
|
||||
(when (d/not-empty? (:workspace-focus-selected state))
|
||||
(->> (rx/merge
|
||||
(rx/of dwz/zoom-to-selected-shape
|
||||
(deselect-all))
|
||||
(->> (rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
||||
(rx/map (comp set keys))
|
||||
(rx/buffer 2 1)
|
||||
(rx/merge-map
|
||||
;; While focus is active, update it with any new and deleted shapes
|
||||
(fn [[old-keys new-keys]]
|
||||
(let [removed (set/difference old-keys new-keys)
|
||||
added (set/difference new-keys old-keys)]
|
||||
(rx/merge
|
||||
(rx/of dwz/zoom-to-selected-shape
|
||||
(deselect-all))
|
||||
(->> (rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
||||
(rx/take-until stopper)
|
||||
(rx/map (comp set keys))
|
||||
(rx/buffer 2 1)
|
||||
(rx/merge-map
|
||||
;; While focus is active, update it with any new and deleted shapes
|
||||
(fn [[old-keys new-keys]]
|
||||
(let [removed (set/difference old-keys new-keys)
|
||||
added (set/difference new-keys old-keys)]
|
||||
|
||||
(if (or (d/not-empty? added) (d/not-empty? removed))
|
||||
(rx/of (update-focus-shapes added removed))
|
||||
(rx/empty)))))))
|
||||
(rx/take-until stopper)))))))
|
||||
(if (or (d/not-empty? added) (d/not-empty? removed))
|
||||
(rx/of (update-focus-shapes added removed))
|
||||
(rx/empty))))))))))))
|
||||
|
||||
@@ -235,8 +235,13 @@
|
||||
[ids]
|
||||
(ptk/reify ::remove-shape-layout
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [undo-id (js/Symbol)]
|
||||
(watch [_ state _]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
ids (->> ids
|
||||
(remove #(->> %
|
||||
(get objects)
|
||||
(ctc/is-variant?))))
|
||||
undo-id (js/Symbol)]
|
||||
(rx/of
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes ids #(apply dissoc % layout-keys))
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.fills :as types.fills]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.event :as ev]
|
||||
@@ -582,17 +581,12 @@
|
||||
|
||||
shape
|
||||
(cond-> shape
|
||||
(and (or (not (ctl/any-layout-immediate-child? objects shape))
|
||||
(not (ctl/fill-width? shape)))
|
||||
(not-changed? shape-width new-width)
|
||||
(= grow-type :auto-width))
|
||||
(and (not-changed? shape-width new-width) (= grow-type :auto-width))
|
||||
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :width new-width {:ignore-lock? true})))
|
||||
|
||||
shape
|
||||
(cond-> shape
|
||||
(and (or (not (ctl/any-layout-immediate-child? objects shape))
|
||||
(not (ctl/fill-height? shape)))
|
||||
(not-changed? shape-height new-height)
|
||||
(and (not-changed? shape-height new-height)
|
||||
(or (= grow-type :auto-height) (= grow-type :auto-width)))
|
||||
(gsh/transform-shape (ctm/change-dimensions-modifiers shape :height new-height {:ignore-lock? true})))]
|
||||
|
||||
@@ -600,8 +594,7 @@
|
||||
|
||||
(let [ids (into #{} (filter changed-text?) (keys props))]
|
||||
(rx/of (dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes ids update-fn {:with-objects? true
|
||||
:reg-objects? true
|
||||
(dwsh/update-shapes ids update-fn {:reg-objects? true
|
||||
:stack-undo? true
|
||||
:ignore-touched true})
|
||||
(ptk/data-event :layout/update {:ids ids})
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.persistence :as-alias dps]
|
||||
[app.main.data.workspace.notifications :as-alias wnt]
|
||||
[app.main.data.workspace.pages :as-alias dwpg]
|
||||
[app.main.rasterizer :as thr]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.render :as render]
|
||||
@@ -255,7 +254,7 @@
|
||||
(let [stopper-s (rx/filter
|
||||
(fn [event]
|
||||
(as-> (ptk/type event) type
|
||||
(or (= ::dwpg/finalize-page type)
|
||||
(or (= :app.main.data.workspace/finalize-page type)
|
||||
(= ::watch-state-changes type))))
|
||||
stream)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
:error/fn #(tr "workspace.tokens.invalid-json")}
|
||||
|
||||
:error.import/invalid-token-name
|
||||
{:error/code :error.import/invalid-token-name
|
||||
{:error/code :error.import/invalid-json-data
|
||||
:error/fn #(tr "workspace.tokens.invalid-json-token-name")
|
||||
:error/detail #(tr "workspace.tokens.invalid-json-token-name-detail" %)}
|
||||
|
||||
|
||||
@@ -218,10 +218,15 @@
|
||||
(gpt/add resize-origin displacement)
|
||||
resize-origin)
|
||||
|
||||
;; Determine resize direction for grow-type logic
|
||||
resize-direction (cond
|
||||
(or (= handler :left) (= handler :right)) :horizontal
|
||||
(or (= handler :top) (= handler :bottom)) :vertical
|
||||
:else nil)
|
||||
|
||||
;; Calculate new grow-type for text layers
|
||||
new-grow-type
|
||||
(when (cfh/text-shape? shape)
|
||||
(dwm/next-grow-type (dm/get-prop shape :grow-type) scalev))
|
||||
new-grow-type (when (cfh/text-shape? shape)
|
||||
(dwm/next-grow-type (dm/get-prop shape :grow-type) resize-direction))
|
||||
|
||||
;; When the horizontal/vertical scale a flex children with auto/fill
|
||||
;; we change it too fixed
|
||||
@@ -268,76 +273,82 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [initial-position @ms/mouse-position
|
||||
(if (:blocked shape)
|
||||
(rx/empty)
|
||||
(let [initial-position @ms/mouse-position
|
||||
|
||||
stopper (mse/drag-stopper stream)
|
||||
layout (:workspace-layout state)
|
||||
page-id (:current-page-id state)
|
||||
focus (:workspace-focus-selected state)
|
||||
zoom (dm/get-in state [:workspace-local :zoom] 1)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
shapes (map (d/getf objects) ids)
|
||||
stopper (mse/drag-stopper stream)
|
||||
layout (:workspace-layout state)
|
||||
page-id (:current-page-id state)
|
||||
focus (:workspace-focus-selected state)
|
||||
zoom (dm/get-in state [:workspace-local :zoom] 1)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
shape-ids (filterv (comp not :blocked (d/getf objects)) ids)]
|
||||
|
||||
resize-events-stream
|
||||
(->> ms/mouse-position
|
||||
(rx/filter some?)
|
||||
(rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt)
|
||||
(rx/map normalize-proportion-lock)
|
||||
(rx/switch-map
|
||||
(fn [[point _ _ :as current]]
|
||||
(->> (snap/closest-snap-point page-id shapes objects layout zoom focus point)
|
||||
(rx/map #(conj current %)))))
|
||||
(rx/map #(resize shape initial-position layout %))
|
||||
(rx/share))
|
||||
(if (empty? shape-ids)
|
||||
(rx/empty)
|
||||
(let [shapes (map (d/getf objects) shape-ids)
|
||||
|
||||
modifiers-stream
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
(rx/merge
|
||||
(->> resize-events-stream
|
||||
(rx/mapcat
|
||||
(fn [modifiers]
|
||||
(let [modif-tree (dwm/create-modif-tree ids modifiers)]
|
||||
(rx/of
|
||||
(dwm/set-wasm-modifiers
|
||||
modif-tree
|
||||
:ignore-constraints (contains? layout :scale-text))))))
|
||||
(rx/take-until stopper))
|
||||
resize-events-stream
|
||||
(->> ms/mouse-position
|
||||
(rx/filter some?)
|
||||
(rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt)
|
||||
(rx/map normalize-proportion-lock)
|
||||
(rx/switch-map
|
||||
(fn [[point _ _ :as current]]
|
||||
(->> (snap/closest-snap-point page-id shapes objects layout zoom focus point)
|
||||
(rx/map #(conj current %)))))
|
||||
(rx/map #(resize shape initial-position layout %))
|
||||
(rx/share))
|
||||
|
||||
modifiers-stream
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
(rx/merge
|
||||
(->> resize-events-stream
|
||||
(rx/mapcat
|
||||
(fn [modifiers]
|
||||
(let [modif-tree (dwm/create-modif-tree shape-ids modifiers)]
|
||||
(rx/of
|
||||
(dwm/set-wasm-modifiers
|
||||
modif-tree
|
||||
:ignore-constraints (contains? layout :scale-text))))))
|
||||
(rx/take-until stopper))
|
||||
|
||||
;; The last event we need to use the old method so the elements are correctly positioned until
|
||||
;; all the logic is implemented in wasm
|
||||
(->> resize-events-stream
|
||||
(rx/take-until stopper)
|
||||
(rx/last)
|
||||
(rx/map
|
||||
#(dwm/apply-wasm-modifiers
|
||||
(dwm/create-modif-tree ids %)
|
||||
:ignore-constraints (contains? layout :scale-text)))))
|
||||
(->> resize-events-stream
|
||||
(rx/take-until stopper)
|
||||
(rx/last)
|
||||
(rx/map
|
||||
#(dwm/apply-wasm-modifiers
|
||||
(dwm/create-modif-tree shape-ids %)
|
||||
:ignore-constraints (contains? layout :scale-text)))))
|
||||
|
||||
(->> resize-events-stream
|
||||
(rx/mapcat
|
||||
(fn [modifiers]
|
||||
(let [modif-tree (dwm/create-modif-tree ids modifiers)]
|
||||
(rx/of (dwm/set-modifiers modif-tree (contains? layout :scale-text))))))
|
||||
(rx/take-until stopper)))]
|
||||
(->> resize-events-stream
|
||||
(rx/mapcat
|
||||
(fn [modifiers]
|
||||
(let [modif-tree (dwm/create-modif-tree shape-ids modifiers)]
|
||||
(rx/of (dwm/set-modifiers modif-tree (contains? layout :scale-text))))))
|
||||
(rx/take-until stopper)))]
|
||||
|
||||
(rx/concat
|
||||
(rx/concat
|
||||
;; This initial stream waits for some pixels to be move before making the resize
|
||||
;; if you make a click in the border will not make a resize
|
||||
(->> ms/mouse-position
|
||||
(rx/map #(gpt/to-vec initial-position %))
|
||||
(rx/map #(gpt/length %))
|
||||
(rx/filter #(> % (/ 10 zoom)))
|
||||
(rx/take 1)
|
||||
(rx/take-until stopper)
|
||||
(rx/mapcat (fn [] modifiers-stream)))
|
||||
(->> ms/mouse-position
|
||||
(rx/map #(gpt/to-vec initial-position %))
|
||||
(rx/map #(gpt/length %))
|
||||
(rx/filter #(> % (/ 10 zoom)))
|
||||
(rx/take 1)
|
||||
(rx/take-until stopper)
|
||||
(rx/mapcat (fn [] modifiers-stream)))
|
||||
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
(rx/of
|
||||
(finish-transform))
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
(rx/of
|
||||
(finish-transform))
|
||||
|
||||
(rx/of
|
||||
(dwm/apply-modifiers)
|
||||
(finish-transform)))))))))
|
||||
(rx/of
|
||||
(dwm/apply-modifiers)
|
||||
(finish-transform))))))))))))
|
||||
|
||||
(defn trigger-bounding-box-cloaking
|
||||
"Trigger the bounding box cloaking (with default timer of 1sec)
|
||||
@@ -382,19 +393,7 @@
|
||||
|
||||
get-modifier
|
||||
(fn [shape]
|
||||
(let [modifiers (ctm/change-dimensions-modifiers shape attr value)]
|
||||
;; For text shapes, also update grow-type based on the resize
|
||||
(if (cfh/text-shape? shape)
|
||||
(let [{sr-width :width sr-height :height} (:selrect shape)
|
||||
new-width (if (= attr :width) value sr-width)
|
||||
new-height (if (= attr :height) value sr-height)
|
||||
scalev (gpt/point (/ new-width sr-width) (/ new-height sr-height))
|
||||
current-grow-type (dm/get-prop shape :grow-type)
|
||||
new-grow-type (dwm/next-grow-type current-grow-type scalev)]
|
||||
(cond-> modifiers
|
||||
(not= new-grow-type current-grow-type)
|
||||
(ctm/change-property :grow-type new-grow-type)))
|
||||
modifiers)))
|
||||
(ctm/change-dimensions-modifiers shape attr value))
|
||||
|
||||
modif-tree
|
||||
(-> (dwm/build-modif-tree ids objects get-modifier)
|
||||
|
||||
@@ -99,25 +99,9 @@
|
||||
|
||||
(defn update-property-name
|
||||
"Update the variant property name on the position pos
|
||||
in all the components with this variant-id and remove the focus"
|
||||
in all the components with this variant-id"
|
||||
[variant-id pos new-name {:keys [trigger]}]
|
||||
(ptk/reify ::update-property-name
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [file-id (:current-file-id state)
|
||||
data (dsh/lookup-file-data state)
|
||||
objects (dsh/lookup-page-objects state)
|
||||
|
||||
related-components (cfv/find-variant-components data objects variant-id)]
|
||||
|
||||
(reduce
|
||||
(fn [s related-component]
|
||||
(update-in s
|
||||
[:files file-id :data :components (:id related-component) :variant-properties]
|
||||
(fn [props] (mapv #(with-meta % nil) props))))
|
||||
state
|
||||
related-components)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
|
||||
@@ -84,9 +84,6 @@
|
||||
(l/derived :shared-files st/state))
|
||||
|
||||
(defn select-libraries
|
||||
"Find between all the given files, those who are libraries of the file-id.
|
||||
Also include the file-id file itself.
|
||||
Return a map of id -> library."
|
||||
[files file-id]
|
||||
(persistent!
|
||||
(reduce-kv (fn [result id file]
|
||||
|
||||
@@ -666,12 +666,11 @@
|
||||
|
||||
[:div {:class (stl/css :form-buttons-wrapper)}
|
||||
[:> mentions-button*]
|
||||
(when (some? on-cancel)
|
||||
[:> button* {:variant "ghost"
|
||||
:type "button"
|
||||
:on-key-down handle-cancel
|
||||
:on-click on-cancel}
|
||||
(tr "ds.confirm-cancel")])
|
||||
[:> button* {:variant "ghost"
|
||||
:type "button"
|
||||
:on-key-down handle-cancel
|
||||
:on-click on-cancel}
|
||||
(tr "ds.confirm-cancel")]
|
||||
[:> button* {:variant "primary"
|
||||
:type "button"
|
||||
:on-key-down handle-submit
|
||||
@@ -687,39 +686,52 @@
|
||||
{::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [on-submit]}]
|
||||
(let [content (mf/use-state "")
|
||||
(let [show-buttons? (mf/use-state false)
|
||||
content (mf/use-state "")
|
||||
|
||||
disabled? (or (blank-content? @content)
|
||||
(exceeds-length? @content))
|
||||
|
||||
on-cancel
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
#(st/emit! :interrupt))
|
||||
#(reset! show-buttons? true))
|
||||
|
||||
on-blur
|
||||
(mf/use-fn
|
||||
#(reset! show-buttons? false))
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
#(reset! content %))
|
||||
|
||||
on-cancel
|
||||
(mf/use-fn
|
||||
#(do (reset! content "")
|
||||
(reset! show-buttons? false)))
|
||||
|
||||
on-submit*
|
||||
(mf/use-fn
|
||||
(mf/deps @content)
|
||||
(fn []
|
||||
(on-submit @content)
|
||||
(reset! content "")))]
|
||||
(on-cancel)))]
|
||||
|
||||
[:div {:class (stl/css :form)}
|
||||
[:> comment-input*
|
||||
{:value @content
|
||||
:placeholder (tr "labels.reply.thread")
|
||||
:autofocus true
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus
|
||||
:on-ctrl-enter on-submit*
|
||||
:on-change on-change}]
|
||||
(when (exceeds-length? @content)
|
||||
[:div {:class (stl/css :error-text)}
|
||||
(tr "errors.character-limit-exceeded")])
|
||||
[:> comment-form-buttons* {:on-submit on-submit*
|
||||
:on-cancel on-cancel
|
||||
:is-disabled disabled?}]]))
|
||||
(when (or @show-buttons? (seq @content))
|
||||
[:> comment-form-buttons* {:on-submit on-submit*
|
||||
:on-cancel on-cancel
|
||||
:is-disabled disabled?}])]))
|
||||
|
||||
(mf/defc comment-edit-form*
|
||||
{::mf/private true}
|
||||
|
||||
@@ -1057,17 +1057,16 @@
|
||||
{:profile profile
|
||||
:on-show-comments handle-show-comments}])]
|
||||
|
||||
(when show-profile-menu?
|
||||
(case sub-menu
|
||||
:help-learning
|
||||
[:> help-learning-menu* {:on-close close-sub-menu :on-click on-click}]
|
||||
(case sub-menu
|
||||
:help-learning
|
||||
[:> help-learning-menu* {:on-close close-sub-menu :on-click on-click}]
|
||||
|
||||
:community-contributions
|
||||
[:> community-contributions-menu* {:on-close close-sub-menu}]
|
||||
:community-contributions
|
||||
[:> community-contributions-menu* {:on-close close-sub-menu}]
|
||||
|
||||
:about-penpot
|
||||
[:> about-penpot-menu* {:on-close close-sub-menu}]
|
||||
nil))]))
|
||||
:about-penpot
|
||||
[:> about-penpot-menu* {:on-close close-sub-menu}]
|
||||
nil)]))
|
||||
|
||||
(mf/defc sidebar*
|
||||
{::mf/props :obj
|
||||
|
||||
@@ -690,18 +690,18 @@
|
||||
[:div {:class (stl/css :table-row :table-row-invitations)}
|
||||
[:div {:class (stl/css :table-field :field-email)}
|
||||
[:div {:class (stl/css :input-wrapper)}
|
||||
[:label
|
||||
[:label {:for (str "email-" email)}
|
||||
[:span {:class (stl/css-case :input-checkbox true
|
||||
:global/checked (is-selected? email))}
|
||||
deprecated-icon/status-tick]
|
||||
|
||||
[:input {:type "checkbox"
|
||||
:id (dm/str "email-" email)
|
||||
:id (str "email-" email)
|
||||
:data-attr email
|
||||
:value email
|
||||
:checked (is-selected? email)
|
||||
:on-change on-change}]
|
||||
email]]]
|
||||
:on-change on-change}]]]
|
||||
email]
|
||||
|
||||
[:div {:class (stl/css :table-field :field-roles)}
|
||||
[:> invitation-role-selector*
|
||||
@@ -930,7 +930,7 @@
|
||||
[:*
|
||||
[:div {:class (stl/css :invitations-actions)}
|
||||
[:div
|
||||
(tr "team.invitations-selected" (i18n/c (count @selected)))]
|
||||
(str (count @selected) " invitations selected")]
|
||||
[:div
|
||||
[:> button* {:variant "secondary"
|
||||
:type "button"
|
||||
|
||||
@@ -780,19 +780,28 @@
|
||||
@extend .input-base;
|
||||
height: auto;
|
||||
}
|
||||
// FIXME: This does not conform to our CSS Guidelines. Need to unnest and to use
|
||||
// custom properties to handle state changes.
|
||||
// TODO: Fix this nested classes.
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include t.use-typography("body-large");
|
||||
|
||||
label {
|
||||
@include t.use-typography("body-small");
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: px2rem(6);
|
||||
cursor: pointer;
|
||||
color: var(--color-foreground-primary);
|
||||
color: var(--color-foreground-secondary);
|
||||
span {
|
||||
@extend .checkbox-icon;
|
||||
}
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
&:hover {
|
||||
span {
|
||||
border-color: var(--color-accent-primary-muted);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:focus-within {
|
||||
@@ -800,22 +809,6 @@
|
||||
border-color: var(--color-accent-primary);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
span {
|
||||
border-color: var(--color-accent-primary-muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
@extend .checkbox-icon;
|
||||
@include t.use-typography("body-small");
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
input {
|
||||
margin: 0;
|
||||
@include t.use-typography("body-small");
|
||||
color: var(--color-foreground-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ $sz-36: px2rem(36);
|
||||
$sz-40: px2rem(40);
|
||||
$sz-48: px2rem(48);
|
||||
$sz-88: px2rem(88);
|
||||
$sz-96: px2rem(96);
|
||||
$sz-120: px2rem(120);
|
||||
$sz-154: px2rem(154);
|
||||
$sz-160: px2rem(160);
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[app.util.simple-math :as smt]
|
||||
[app.util.timers :as ts]
|
||||
[cuerdas.core :as str]
|
||||
[goog.events :as events]
|
||||
[rumext.v2 :as mf]
|
||||
@@ -382,7 +383,11 @@
|
||||
(reset! focused-id* nil)
|
||||
(reset! is-open* false)
|
||||
(reset! token-applied* name)
|
||||
(apply-token value name)))
|
||||
(apply-token value name)
|
||||
(ts/schedule-on-idle
|
||||
(fn []
|
||||
(when token-wrapper-ref
|
||||
(dom/focus! (mf/ref-val token-wrapper-ref)))))))
|
||||
|
||||
on-option-click
|
||||
(mf/use-fn
|
||||
@@ -416,14 +421,16 @@
|
||||
(fn [event]
|
||||
(let [target (dom/get-related-target event)
|
||||
self-node (mf/ref-val wrapper-ref)]
|
||||
|
||||
(when-not (dom/is-child? self-node target)
|
||||
(reset! filter-id* "")
|
||||
(reset! focused-id* nil)
|
||||
(reset! is-open* false)))
|
||||
|
||||
(when (mf/ref-val dirty-ref)
|
||||
(apply-value (mf/ref-val raw-value*))
|
||||
(when (fn? on-blur)
|
||||
(on-blur event)))))
|
||||
(apply-value (mf/ref-val raw-value*)))
|
||||
(when (fn? on-blur)
|
||||
(on-blur event))))
|
||||
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
@@ -560,9 +567,11 @@
|
||||
(reset! token-applied* nil)
|
||||
(reset! selected-id* nil)
|
||||
(reset! focused-id* nil)
|
||||
(dom/focus! (mf/ref-val ref))
|
||||
(when on-detach
|
||||
(on-detach token))))))
|
||||
(on-detach token))
|
||||
(ts/schedule-on-idle
|
||||
(fn []
|
||||
(dom/focus! (mf/ref-val ref))))))))
|
||||
|
||||
on-token-key-down
|
||||
(mf/use-fn
|
||||
@@ -656,6 +665,7 @@
|
||||
:label label
|
||||
:value token-value
|
||||
:on-click open-dropdown-token
|
||||
:on-focus on-focus
|
||||
:on-token-key-down on-token-key-down
|
||||
:disabled disabled
|
||||
:on-blur on-blur
|
||||
|
||||
@@ -27,13 +27,14 @@
|
||||
[:on-click {:optional true} fn?]
|
||||
[:on-token-key-down fn?]
|
||||
[:on-blur {:optional true} fn?]
|
||||
[:on-focus {:optional true} fn?]
|
||||
[:detach-token fn?]])
|
||||
|
||||
(mf/defc token-field*
|
||||
{::mf/schema schema:token-field}
|
||||
[{:keys [id label value slot-start disabled
|
||||
on-click on-token-key-down on-blur detach-token
|
||||
token-wrapper-ref token-detach-btn-ref]}]
|
||||
token-wrapper-ref token-detach-btn-ref on-focus]}]
|
||||
(let [set-active? (some? id)
|
||||
content (if set-active?
|
||||
label
|
||||
@@ -60,6 +61,7 @@
|
||||
:on-key-down on-token-key-down
|
||||
:ref token-wrapper-ref
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus
|
||||
:tab-index (if disabled -1 0)}
|
||||
|
||||
(when (some? slot-start) slot-start)
|
||||
|
||||
@@ -93,6 +93,8 @@
|
||||
image (:image background)
|
||||
format (if id? "rounded" "square")
|
||||
element-id (mf/use-id)
|
||||
has-opacity? (and (some? (:color background))
|
||||
(< (:opacity background) 1))
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(mf/deps background on-click)
|
||||
@@ -124,19 +126,20 @@
|
||||
[:> element-type props
|
||||
(cond
|
||||
(some? gradient-type)
|
||||
[:span {:class (stl/css :swatch-gradient)
|
||||
:style {:background-image (str (uc/gradient->css gradient-data) ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
|
||||
[:div {:class (stl/css :swatch-gradient)
|
||||
:style {:background-image (str (uc/gradient->css gradient-data) ", repeating-conic-gradient(lightgray 0% 25%, white 0% 50%)")}}]
|
||||
|
||||
(some? image)
|
||||
(let [uri (cfg/resolve-file-media image)]
|
||||
[:span {:class (stl/css :swatch-image)
|
||||
:style {:background-image (str/ffmt "url(%)" uri)}}])
|
||||
[:div {:class (stl/css :swatch-image)
|
||||
:style {:background-image (str/ffmt "url(%)" uri)}}])
|
||||
has-errors
|
||||
[:span {:class (stl/css :swatch-error)}]
|
||||
|
||||
[:div {:class (stl/css :swatch-error)}]
|
||||
:else
|
||||
[:span {:class (stl/css :swatch-opacity)}
|
||||
[:span {:class (stl/css :swatch-solid-side)
|
||||
:style {:background (uc/color->background (assoc background :opacity 1))}}]
|
||||
[:span {:class (stl/css :swatch-opacity-side)
|
||||
:style {:background (uc/color->background background)}}]])]]))
|
||||
[:div {:class (stl/css :swatch-opacity)}
|
||||
[:div {:class (stl/css :swatch-solid-side)
|
||||
:style {:background (uc/color->background (assoc background :opacity 1))}}]
|
||||
[:div {:class (stl/css-case :swatch-opacity-side true
|
||||
:swatch-opacity-side-transparency has-opacity?
|
||||
:swatch-opacity-side-solid-color (not has-opacity?))
|
||||
:style {"--solid-color-overlay" (str (uc/color->background background))}}]])]]))
|
||||
|
||||
@@ -8,8 +8,14 @@
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/colors.scss" as *;
|
||||
|
||||
@property --solid-color-overlay {
|
||||
syntax: "<color>";
|
||||
inherits: false;
|
||||
initial-value: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.swatch {
|
||||
--border-color: var(--color-accent-primary-muted);
|
||||
--border-color: var(--color-background-quaternary);
|
||||
--border-radius: #{$br-4};
|
||||
--border-color-active: var(--color-foreground-primary);
|
||||
--border-color-active-inset: var(--color-background-primary);
|
||||
@@ -73,7 +79,8 @@
|
||||
background: none;
|
||||
|
||||
&:hover {
|
||||
border: 2px solid var(--border-color);
|
||||
--border-color: var(--color-accent-primary-muted);
|
||||
border-width: $b-2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,9 +105,25 @@
|
||||
}
|
||||
|
||||
.swatch-opacity {
|
||||
background: var(--checkerboard-background);
|
||||
background-size: var(--checkerboard-size);
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto;
|
||||
}
|
||||
|
||||
.swatch-opacity-side-transparency {
|
||||
background-image:
|
||||
/* solid‑colour overlay */
|
||||
/* checkerboard pattern */
|
||||
linear-gradient(var(--solid-color-overlay), var(--solid-color-overlay)), var(--checkerboard-background);
|
||||
|
||||
background-size: cover, var(--checkerboard-size);
|
||||
background-position: center, center;
|
||||
background-repeat: no-repeat, repeat;
|
||||
clip-path: inset(0 0 0 0 round 0 #{$br-4} #{$br-4} 0);
|
||||
}
|
||||
|
||||
.swatch-opacity-side-solid-color {
|
||||
background: var(--solid-color-overlay);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.swatch-solid-side,
|
||||
|
||||
@@ -13,29 +13,18 @@
|
||||
[app.common.types.fills :as types.fills]
|
||||
[app.common.types.text :as types.text]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.copy-button :refer [copy-button*]]
|
||||
[app.main.ui.components.title-bar :refer [inspect-title-bar*]]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.main.ui.inspect.attributes.common :refer [color-row]]
|
||||
[app.main.ui.inspect.common.typography :as ict]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- has-text? [shape]
|
||||
(:content shape))
|
||||
|
||||
(def ^:private file-typographies-ref
|
||||
(l/derived (l/in [:viewer :file :data :typographies]) st/state))
|
||||
|
||||
(defn- make-typographies-library-ref [file-id]
|
||||
(let [get-library
|
||||
(fn [state]
|
||||
(get-in state [:viewer-libraries file-id :data :typographies]))]
|
||||
#(l/derived get-library st/state)))
|
||||
|
||||
(defn- copy-style-data
|
||||
[style & properties]
|
||||
(->> properties
|
||||
@@ -44,24 +33,10 @@
|
||||
|
||||
(mf/defc typography-block
|
||||
[{:keys [text style]}]
|
||||
(let [typography-library-ref
|
||||
(mf/use-memo
|
||||
(mf/deps (:typography-ref-file style))
|
||||
(make-typographies-library-ref (:typography-ref-file style)))
|
||||
|
||||
typography-library (mf/deref typography-library-ref)
|
||||
|
||||
;; FIXME: too many duplicate operations
|
||||
file-typographies-viewer (mf/deref file-typographies-ref)
|
||||
file-typographies-workspace (mf/deref refs/workspace-file-typography)
|
||||
|
||||
file-library-workspace (get (mf/deref refs/files) (:typography-ref-file style))
|
||||
typography-external-lib (get-in file-library-workspace [:data :typographies (:typography-ref-id style)])
|
||||
|
||||
color-format* (mf/use-state :hex)
|
||||
(let [color-format* (mf/use-state :hex)
|
||||
color-format (deref color-format*)
|
||||
|
||||
typography (or (get (or typography-library file-typographies-viewer file-typographies-workspace) (:typography-ref-id style)) typography-external-lib)]
|
||||
typography (ict/get-typography style)]
|
||||
|
||||
[:div {:class (stl/css :attributes-content)}
|
||||
(when (:fills style)
|
||||
|
||||
38
frontend/src/app/main/ui/inspect/common/typography.cljs
Normal file
@@ -0,0 +1,38 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.inspect.common.typography
|
||||
(:require
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- make-typographies-library-ref
|
||||
[file-id]
|
||||
(let [get-library
|
||||
(fn [state]
|
||||
(get-in state [:viewer-libraries file-id :data :typographies]))]
|
||||
#(l/derived get-library st/state)))
|
||||
|
||||
(def ^:private file-typographies-ref
|
||||
(l/derived (l/in [:viewer :file :data :typographies]) st/state))
|
||||
|
||||
(defn get-typography
|
||||
[style]
|
||||
(let [typography-library-ref
|
||||
(mf/use-memo
|
||||
(mf/deps (:typography-ref-file style))
|
||||
(make-typographies-library-ref (:typography-ref-file style)))
|
||||
|
||||
; FIXME: too many duplicate operations
|
||||
typography-library (mf/deref typography-library-ref)
|
||||
file-typographies-viewer (mf/deref file-typographies-ref)
|
||||
file-typographies-workspace (mf/deref refs/workspace-file-typography)
|
||||
file-library-workspace (get (mf/deref refs/files) (:typography-ref-file style))
|
||||
typography-external-lib (get-in file-library-workspace [:data :typographies (:typography-ref-id style)])
|
||||
typography (or (get (or typography-library file-typographies-viewer file-typographies-workspace) (:typography-ref-id style)) typography-external-lib)]
|
||||
typography))
|
||||
@@ -146,12 +146,9 @@
|
||||
.viewer-tab-switcher {
|
||||
--tabs-nav-padding-inline-start: 0;
|
||||
--tabs-nav-padding-inline-end: var(--sp-m);
|
||||
--max-inspect-tab-height: calc(100vh - 12rem);
|
||||
|
||||
/* same height as .element-options in workspace/sidebar/options.scss */
|
||||
/* which is one of the parents of this component */
|
||||
--max-inspect-tab-height: var(--sidebar-element-options-height);
|
||||
max-block-size: var(--max-inspect-tab-height);
|
||||
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
[app.main.ui.inspect.styles.panels.layout-element :refer [layout-element-panel*]]
|
||||
[app.main.ui.inspect.styles.panels.stroke :refer [stroke-panel*]]
|
||||
[app.main.ui.inspect.styles.panels.svg :refer [svg-panel*]]
|
||||
[app.main.ui.inspect.styles.panels.text :refer [text-panel*]]
|
||||
[app.main.ui.inspect.styles.panels.tokens-panel :refer [tokens-panel*]]
|
||||
[app.main.ui.inspect.styles.panels.variants-panel :refer [variants-panel*]]
|
||||
[app.main.ui.inspect.styles.panels.visibility :refer [visibility-panel*]]
|
||||
@@ -71,6 +72,9 @@
|
||||
(defn- has-blur? [shape]
|
||||
(:blur shape))
|
||||
|
||||
(defn- has-text? [shape]
|
||||
(:content shape))
|
||||
|
||||
(defn- get-shape-type
|
||||
[shapes first-shape first-component]
|
||||
(if (= (count shapes) 1)
|
||||
@@ -195,6 +199,14 @@
|
||||
[:> style-box* {:panel :blur}
|
||||
[:> blur-panel* {:shapes shapes
|
||||
:objects objects}]]))
|
||||
:text
|
||||
(let [shapes (filter has-text? shapes)]
|
||||
(when (seq shapes)
|
||||
[:> style-box* {:panel :text}
|
||||
[:> text-panel* {:shapes shapes
|
||||
:color-space color-space
|
||||
:objects objects
|
||||
:resolved-tokens resolved-active-tokens}]]))
|
||||
;; DEFAULT WIP
|
||||
[:> style-box* {:panel panel}
|
||||
[:div color-space]])])]))
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
|
||||
;; Current token implementation on fills only supports one token per shape and has to be the first fill
|
||||
;; This must be improved in the future
|
||||
(defn- has-token?
|
||||
(defn- has-color-token?
|
||||
"Returns true if the resolved token matches the color and is the first fill (idx = 0)."
|
||||
[resolved-token stroke-type idx]
|
||||
(and (= (:resolved-value resolved-token) (:color stroke-type))
|
||||
@@ -66,17 +66,17 @@
|
||||
property-name (cmm/get-css-rule-humanized property)
|
||||
property-value (css/get-css-property objects stroke property)
|
||||
resolved-token (get-resolved-token property shape resolved-tokens)
|
||||
has-token (has-token? resolved-token stroke-type idx)]
|
||||
has-color-token (has-color-token? resolved-token stroke-type idx)]
|
||||
(if (= property :border-color)
|
||||
[:> color-properties-row* {:key (str idx property)
|
||||
:term property-name
|
||||
:color stroke-type
|
||||
:token (when has-token resolved-token)
|
||||
:token (when has-color-token resolved-token)
|
||||
:format color-space
|
||||
:copiable true}]
|
||||
[:> properties-row* {:key (str idx property)
|
||||
:term (d/name property-name)
|
||||
:detail (dm/str value)
|
||||
:token (when has-token resolved-token)
|
||||
:token resolved-token
|
||||
:property property-value
|
||||
:copiable true}]))))])])
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
:term (d/name sub-attr-key)
|
||||
:detail (dm/str sub-attr-value)
|
||||
:property property-value
|
||||
:copiable false}]))
|
||||
:copiable true}]))
|
||||
[:> properties-row* {:key (dm/str "svg-property-" (d/name attr-key))
|
||||
:term (d/name attr-key)
|
||||
:detail (dm/str attr-value)
|
||||
:property (dm/str attr-key ": " attr-value ";")
|
||||
:copiable false}]))]])
|
||||
:copiable true}]))]])
|
||||
|
||||
184
frontend/src/app/main/ui/inspect/styles/panels/text.cljs
Normal file
@@ -0,0 +1,184 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.inspect.styles.panels.text
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.types.fills :as types.fills]
|
||||
[app.common.types.text :as txt]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.main.ui.inspect.common.typography :as ict]
|
||||
[app.main.ui.inspect.styles.property-detail-copiable :refer [property-detail-copiable*]]
|
||||
[app.main.ui.inspect.styles.rows.color-properties-row :refer [color-properties-row*]]
|
||||
[app.main.ui.inspect.styles.rows.properties-row :refer [properties-row*]]
|
||||
[app.util.timers :as tm]
|
||||
[app.util.webapi :as wapi]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- get-applied-tokens-in-shape
|
||||
[shape-tokens property]
|
||||
(get shape-tokens property))
|
||||
|
||||
(defn- get-resolved-token
|
||||
[property shape resolved-tokens]
|
||||
(let [shape-tokens (:applied-tokens shape)
|
||||
applied-tokens-in-shape (get-applied-tokens-in-shape shape-tokens property)
|
||||
token (get resolved-tokens applied-tokens-in-shape)]
|
||||
token))
|
||||
|
||||
(defn- get-style-text
|
||||
[shape]
|
||||
(->> (:content shape)
|
||||
(txt/content->text+styles)
|
||||
(remove (fn [[_ text]] (str/empty? (str/trim text))))
|
||||
(mapv (fn [[style text]] (vector (merge (txt/get-default-text-attrs) style) text)))))
|
||||
|
||||
(mf/defc typography-name-block*
|
||||
[{:keys [style]}]
|
||||
(let [typography (ict/get-typography style)
|
||||
property-value (:name typography)]
|
||||
(when typography
|
||||
[:> properties-row* {:term "Typography"
|
||||
:detail (:name typography)
|
||||
:property property-value
|
||||
:copiable true}])))
|
||||
|
||||
(mf/defc typography-color-row*
|
||||
[{:keys [fill shape resolved-tokens color-space]}]
|
||||
(let [color (types.fills/fill->color fill)
|
||||
resolved-token (get-resolved-token :fill shape resolved-tokens)]
|
||||
[:> color-properties-row* {:term "Font Color"
|
||||
:color color
|
||||
:token resolved-token
|
||||
:format color-space
|
||||
:copiable true}]))
|
||||
|
||||
(mf/defc text-panel*
|
||||
[{:keys [shapes _ resolved-tokens color-space]}]
|
||||
[:div {:class (stl/css :text-panel)}
|
||||
(for [shape shapes]
|
||||
(let [style-text-blocks (get-style-text shape)
|
||||
composite-typography-token (get-resolved-token :typography shape resolved-tokens)]
|
||||
|
||||
[:div {:key (:id shape) :class "text-shape"}
|
||||
(for [[style text] style-text-blocks]
|
||||
|
||||
[:div {:key (:id shape) :class "text-properties"}
|
||||
|
||||
(when (:fills style)
|
||||
(for [[idx fill] (map-indexed vector (:fills style))]
|
||||
[:> typography-color-row* {:key idx
|
||||
:fill fill
|
||||
:shape shape
|
||||
:resolved-tokens resolved-tokens
|
||||
:color-space color-space}]))
|
||||
|
||||
;; Typography style
|
||||
(when (and (not composite-typography-token)
|
||||
(:typography-ref-id style))
|
||||
[:> typography-name-block* {:style style}])
|
||||
|
||||
;; Composite Typography token
|
||||
(when (and (not (:typography-ref-id style))
|
||||
composite-typography-token)
|
||||
[:> properties-row* {:term "Typography"
|
||||
:detail (:name composite-typography-token)
|
||||
:token composite-typography-token
|
||||
:property (:name composite-typography-token)
|
||||
:copiable true}])
|
||||
|
||||
(when (:font-id style)
|
||||
(let [name (get (fonts/get-font-data (:font-id style)) :name)
|
||||
resolved-token (get-resolved-token :font-family shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Font Family"
|
||||
:detail name
|
||||
:token resolved-token
|
||||
:property (str "font-family: \"" name "\";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:font-style style)
|
||||
[:> properties-row* {:term "Font Style"
|
||||
:detail (:font-style style)
|
||||
:property (str "font-style: " (:font-style style) ";")
|
||||
:copiable true}])
|
||||
|
||||
(when (:font-size style)
|
||||
(let [font-size (fmt/format-pixels (:font-size style))
|
||||
resolved-token (get-resolved-token :font-size shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Font Size"
|
||||
:detail font-size
|
||||
:token resolved-token
|
||||
:property (str "font-size: " font-size ";")
|
||||
:copiable true}]))
|
||||
(when (:font-weight style)
|
||||
(let [resolved-token (get-resolved-token :font-weight shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Font Weight"
|
||||
:detail (:font-weight style)
|
||||
:token resolved-token
|
||||
:property (str "font-weight: " (:font-weight style) ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:line-height style)
|
||||
(let [line-height (:line-height style)
|
||||
resolved-token (get-resolved-token :line-height shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Line Height"
|
||||
:detail (str line-height)
|
||||
:token resolved-token
|
||||
:property (str "line-height: " line-height ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:letter-spacing style)
|
||||
(let [letter-spacing (fmt/format-pixels (:letter-spacing style))
|
||||
resolved-token (get-resolved-token :letter-spacing shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Letter Spacing"
|
||||
:detail letter-spacing
|
||||
:token resolved-token
|
||||
:property (str "letter-spacing: " letter-spacing ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:text-decoration style)
|
||||
(let [resolved-token (get-resolved-token :text-decoration shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Text Decoration"
|
||||
:detail (:text-decoration style)
|
||||
:token resolved-token
|
||||
:property (str "text-decoration: " (:text-decoration style) ";")
|
||||
:copiable true}]))
|
||||
|
||||
(when (:text-transform style)
|
||||
(let [resolved-token (get-resolved-token :text-case shape resolved-tokens)]
|
||||
[:> properties-row* {:term "Text Transform"
|
||||
:detail (:text-transform style)
|
||||
:token resolved-token
|
||||
:property (str "text-transform: " (:text-transform style) ";")
|
||||
:copiable true}]))
|
||||
(when text
|
||||
(let [copied* (mf/use-state false)
|
||||
copied (deref copied*)
|
||||
|
||||
text (str/trim text)
|
||||
|
||||
copy-text
|
||||
(mf/use-fn
|
||||
(mf/deps copied)
|
||||
(fn []
|
||||
(let [formatted-text (if (= (:text-transform style) "uppercase")
|
||||
(.toUpperCase text)
|
||||
text)]
|
||||
(reset! copied* true)
|
||||
(wapi/write-to-clipboard formatted-text)
|
||||
(tm/schedule 1000 #(reset! copied* false)))))]
|
||||
[:div {:class (stl/css :text-content-wrapper)}
|
||||
[:> property-detail-copiable* {:copied copied
|
||||
:on-click copy-text}
|
||||
[:span {:class (stl/css :text-content)
|
||||
:style {:font-family (:font-family style)
|
||||
:font-weight (:font-weight style)
|
||||
:text-transform (:text-transform style)
|
||||
:letter-spacing (fmt/format-pixels (:letter-spacing style))
|
||||
:font-style (:font-style style)}}
|
||||
text]]]))])]))])
|
||||
20
frontend/src/app/main/ui/inspect/styles/panels/text.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "ds/_borders.scss" as *;
|
||||
|
||||
.text-content-wrapper {
|
||||
--border-color: var(--color-background-quaternary);
|
||||
--border-radius: ${$br-8};
|
||||
|
||||
border: $b-1 solid var(--border-color);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.text-content {
|
||||
--detail-color: var(--color-foreground-secondary);
|
||||
color: var(--detail-color);
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
(def ^:private schema:property-detail-copiable
|
||||
[:map
|
||||
[:detail :string]
|
||||
[:color {:optional true} :any] ;; color object with :color, :gradient or :image
|
||||
[:token {:optional true} :any] ;; resolved token object
|
||||
[:copied :boolean]
|
||||
@@ -24,7 +23,7 @@
|
||||
|
||||
(mf/defc property-detail-copiable*
|
||||
{::mf/schema schema:property-detail-copiable}
|
||||
[{:keys [detail color token copied on-click]}]
|
||||
[{:keys [color token copied on-click children]}]
|
||||
[:button {:class (stl/css-case :property-detail-copiable true
|
||||
:property-detail-copied copied
|
||||
:property-detail-copiable-color (some? color))
|
||||
@@ -45,7 +44,7 @@
|
||||
color-library-name (get-in (or colors-library file-colors) [(:ref-id color) :name])
|
||||
color (assoc color :name color-library-name)]
|
||||
[:span {:class (stl/css :property-detail-text)} (:name color)])
|
||||
[:span {:class (stl/css :property-detail-text)} detail]))
|
||||
[:span {:class (stl/css :property-detail-text)} children]))
|
||||
[:> icon* {:class (stl/css :property-detail-icon)
|
||||
:icon-id (if copied i/tick i/clipboard)
|
||||
:size "s"
|
||||
|
||||
@@ -12,23 +12,6 @@
|
||||
--detail-color: var(--color-foreground-primary);
|
||||
--button-min-inline-size: #{$sz-154};
|
||||
--button-min-block-size: #{$sz-36};
|
||||
}
|
||||
|
||||
.property-detail-text {
|
||||
color: var(--detail-color);
|
||||
}
|
||||
|
||||
.property-detail-text-token {
|
||||
@include use-typography("code-font");
|
||||
--detail-color: var(--color-token-foreground);
|
||||
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.property-detail-copiable {
|
||||
--button-border-radius: #{$br-4};
|
||||
--button-background: none;
|
||||
|
||||
@@ -69,3 +52,17 @@
|
||||
.property-detail-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.property-detail-text {
|
||||
color: var(--detail-color);
|
||||
}
|
||||
|
||||
.property-detail-text-token {
|
||||
@include use-typography("code-font");
|
||||
--detail-color: var(--color-token-foreground);
|
||||
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -101,18 +101,16 @@
|
||||
[:div {:class (stl/css :tooltip-token-title)}
|
||||
(tr "inspect.tabs.styles.token.resolved-value")]
|
||||
[:div {:class (stl/css :tooltip-token-value)}
|
||||
(:value token)]])}
|
||||
[:> property-detail-copiable* {:detail formatted-color-value
|
||||
:color color
|
||||
(:resolved-value token)]])}
|
||||
[:> property-detail-copiable* {:color color
|
||||
:token token
|
||||
:copied copied
|
||||
:on-click copy-attr}]]
|
||||
:on-click copy-attr} formatted-color-value]]
|
||||
|
||||
|
||||
[:> property-detail-copiable* {:detail formatted-color-value
|
||||
:color color
|
||||
[:> property-detail-copiable* {:color color
|
||||
:copied copied
|
||||
:on-click copy-attr}])]]
|
||||
:on-click copy-attr} formatted-color-value])]]
|
||||
(when (:image color)
|
||||
[:div {:class (stl/css :color-image-preview)}
|
||||
[:div {:class (stl/css :color-image-preview-wrapper)}
|
||||
|
||||