mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
Merge branch 'staging' into develop
This commit is contained in:
@@ -67,14 +67,16 @@
|
|||||||
- Fix restoring a variant in another file makes it overlap the existing variant [Taiga #12049](https://tree.taiga.io/project/penpot/issue/12049)
|
- Fix restoring a variant in another file makes it overlap the existing variant [Taiga #12049](https://tree.taiga.io/project/penpot/issue/12049)
|
||||||
- Fix auto-width changes to fixed when switching variants [Taiga #12172](https://tree.taiga.io/project/penpot/issue/12172)
|
- 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 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)
|
||||||
|
|
||||||
|
|
||||||
## 2.10.1 (Unreleased)
|
## 2.10.1
|
||||||
|
|
||||||
### :sparkles: New features & Enhancements
|
### :sparkles: New features & Enhancements
|
||||||
|
|
||||||
- Improve workpace file loading [Github 7366](https://github.com/penpot/penpot/pull/7366)
|
- Improve workpace file loading [Github 7366](https://github.com/penpot/penpot/pull/7366)
|
||||||
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
- Fix regression with text shapes creation with Plugins API [Taiga #12244](https://tree.taiga.io/project/penpot/issue/12244)
|
- Fix regression with text shapes creation with Plugins API [Taiga #12244](https://tree.taiga.io/project/penpot/issue/12244)
|
||||||
|
|||||||
@@ -58,9 +58,9 @@ export JAVA_OPTS="\
|
|||||||
-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
-Dlog4j2.configurationFile=log4j2-devenv.xml \
|
||||||
-Djdk.tracePinnedThreads=full \
|
-Djdk.tracePinnedThreads=full \
|
||||||
-Dim4java.useV7=true \
|
-Dim4java.useV7=true \
|
||||||
|
-XX:+UnlockExperimentalVMOptions \
|
||||||
-XX:+UseShenandoahGC \
|
-XX:+UseShenandoahGC \
|
||||||
-XX:+UseCompactObjectHeaders \
|
-XX:+UseCompactObjectHeaders \
|
||||||
-XX:+UnlockExperimentalVMOptions \
|
|
||||||
-XX:ShenandoahGCMode=generational \
|
-XX:ShenandoahGCMode=generational \
|
||||||
-XX:-OmitStackTraceInFastThrow \
|
-XX:-OmitStackTraceInFastThrow \
|
||||||
--sun-misc-unsafe-memory-access=allow \
|
--sun-misc-unsafe-memory-access=allow \
|
||||||
|
|||||||
@@ -443,13 +443,18 @@
|
|||||||
[:team-id ::sm/uuid]])
|
[:team-id ::sm/uuid]])
|
||||||
|
|
||||||
(def sql:team-invitations
|
(def sql:team-invitations
|
||||||
"select email_to as email, role, (valid_until < now()) as expired
|
"SELECT email_to AS email,
|
||||||
from team_invitation where team_id = ? order by valid_until desc, created_at desc")
|
role,
|
||||||
|
(valid_until < ?::timestamptz) AS expired
|
||||||
|
FROM team_invitation
|
||||||
|
WHERE team_id = ?
|
||||||
|
ORDER BY valid_until DESC, created_at DESC")
|
||||||
|
|
||||||
(defn get-team-invitations
|
(defn get-team-invitations
|
||||||
[conn team-id]
|
[conn team-id]
|
||||||
(->> (db/exec! conn [sql:team-invitations team-id])
|
(let [now (ct/now)]
|
||||||
(mapv #(update % :role keyword))))
|
(->> (db/exec! conn [sql:team-invitations now team-id])
|
||||||
|
(mapv #(update % :role keyword)))))
|
||||||
|
|
||||||
(sv/defmethod ::get-team-invitations
|
(sv/defmethod ::get-team-invitations
|
||||||
{::doc/added "1.17"
|
{::doc/added "1.17"
|
||||||
|
|||||||
@@ -42,45 +42,99 @@
|
|||||||
(assert (sm/check schema:dispatcher cfg)))
|
(assert (sm/check schema:dispatcher cfg)))
|
||||||
|
|
||||||
(def ^:private sql:select-next-tasks
|
(def ^:private sql:select-next-tasks
|
||||||
"select id, queue from task as t
|
"SELECT id, queue, scheduled_at from task AS t
|
||||||
where t.scheduled_at <= now()
|
WHERE t.scheduled_at <= ?::timestamptz
|
||||||
and (t.status = 'new' or t.status = 'retry')
|
AND (t.status = 'new' OR t.status = 'retry')
|
||||||
and queue ~~* ?::text
|
AND queue ~~* ?::text
|
||||||
order by t.priority desc, t.scheduled_at
|
ORDER BY t.priority DESC, t.scheduled_at
|
||||||
limit ?
|
LIMIT ?
|
||||||
for update skip locked")
|
FOR UPDATE
|
||||||
|
SKIP LOCKED")
|
||||||
|
|
||||||
(def ^:private sql:mark-task-scheduled
|
(def ^:private sql:mark-task-scheduled
|
||||||
"UPDATE task SET status = 'scheduled'
|
"UPDATE task SET status = 'scheduled'
|
||||||
WHERE id = ANY(?)")
|
WHERE id = ANY(?)")
|
||||||
|
|
||||||
|
(def ^:private sql:reschedule-lost
|
||||||
|
"UPDATE task
|
||||||
|
SET status='new', scheduled_at=?::timestamptz
|
||||||
|
FROM (SELECT t.id
|
||||||
|
FROM task AS t
|
||||||
|
WHERE status = 'scheduled'
|
||||||
|
AND (?::timestamptz - t.scheduled_at) > '5 min'::interval) AS subquery
|
||||||
|
WHERE task.id=subquery.id
|
||||||
|
RETURNING task.id, task.queue")
|
||||||
|
|
||||||
|
(def ^:private sql:clean-orphan
|
||||||
|
"UPDATE task
|
||||||
|
SET status='failed', modified_at=?::timestamptz,
|
||||||
|
error='orphan with running status'
|
||||||
|
FROM (SELECT t.id
|
||||||
|
FROM task AS t
|
||||||
|
WHERE status = 'running'
|
||||||
|
AND (?::timestamptz - t.modified_at) > '24 hour'::interval) AS subquery
|
||||||
|
WHERE task.id=subquery.id
|
||||||
|
RETURNING task.id, task.queue")
|
||||||
|
|
||||||
(defmethod ig/init-key ::wrk/dispatcher
|
(defmethod ig/init-key ::wrk/dispatcher
|
||||||
[_ {:keys [::db/pool ::wrk/tenant ::batch-size ::timeout] :as cfg}]
|
[_ {:keys [::db/pool ::wrk/tenant ::batch-size ::timeout] :as cfg}]
|
||||||
(letfn [(get-tasks [{:keys [::db/conn]}]
|
(letfn [(reschedule-lost-tasks [{:keys [::db/conn ::timestamp]}]
|
||||||
(let [prefix (str tenant ":%")]
|
(doseq [{:keys [id queue]} (db/exec! conn [sql:reschedule-lost timestamp timestamp]
|
||||||
(not-empty (db/exec! conn [sql:select-next-tasks prefix batch-size]))))
|
{:return-keys true})]
|
||||||
|
(l/wrn :hint "reschedule"
|
||||||
|
:id (str id)
|
||||||
|
:queue queue)))
|
||||||
|
|
||||||
(mark-as-scheduled [{:keys [::db/conn]} ids]
|
(clean-orphan [{:keys [::db/conn ::timestamp]}]
|
||||||
(let [sql [sql:mark-task-scheduled
|
(doseq [{:keys [id queue]} (db/exec! conn [sql:clean-orphan timestamp timestamp]
|
||||||
|
{:return-keys true})]
|
||||||
|
(l/wrn :hint "mark as orphan failed"
|
||||||
|
:id (str id)
|
||||||
|
:queue queue)))
|
||||||
|
|
||||||
|
(get-tasks [{:keys [::db/conn ::timestamp] :as cfg}]
|
||||||
|
(let [prefix (str tenant ":%")
|
||||||
|
result (db/exec! conn [sql:select-next-tasks timestamp prefix batch-size])]
|
||||||
|
(not-empty result)))
|
||||||
|
|
||||||
|
(mark-as-scheduled [{:keys [::db/conn]} items]
|
||||||
|
(let [ids (map :id items)
|
||||||
|
sql [sql:mark-task-scheduled
|
||||||
(db/create-array conn "uuid" ids)]]
|
(db/create-array conn "uuid" ids)]]
|
||||||
(db/exec-one! conn sql)))
|
(db/exec-one! conn sql)))
|
||||||
|
|
||||||
(push-tasks [{:keys [::rds/conn] :as cfg} [queue tasks]]
|
(push-tasks [{:keys [::rds/conn] :as cfg} [queue tasks]]
|
||||||
(let [ids (mapv :id tasks)
|
(let [items (mapv (juxt :id :scheduled-at) tasks)
|
||||||
key (str/ffmt "taskq:%" queue)
|
key (str/ffmt "penpot.worker.queue:%" queue)]
|
||||||
res (rds/rpush conn key (mapv t/encode-str ids))]
|
|
||||||
|
|
||||||
(mark-as-scheduled cfg ids)
|
(rds/rpush conn key (mapv t/encode-str items))
|
||||||
(l/trc :hist "enqueue tasks on redis"
|
(mark-as-scheduled cfg tasks)
|
||||||
:queue queue
|
|
||||||
:tasks (count ids)
|
(doseq [{:keys [id queue]} tasks]
|
||||||
:queued res)))
|
(l/trc :hist "schedule"
|
||||||
|
:id (str id)
|
||||||
|
:queue queue))))
|
||||||
|
|
||||||
(run-batch' [cfg]
|
(run-batch' [cfg]
|
||||||
(if-let [tasks (get-tasks cfg)]
|
(let [cfg (assoc cfg ::timestamp (ct/now))]
|
||||||
(->> (group-by :queue tasks)
|
;; Reschedule lost in transit tasks (can happen when
|
||||||
(run! (partial push-tasks cfg)))
|
;; redis server is restarted just after task is pushed)
|
||||||
::wait))
|
(reschedule-lost-tasks cfg)
|
||||||
|
|
||||||
|
;; Mark as failed all tasks that are still marked as
|
||||||
|
;; running but it's been more than 24 hours since its
|
||||||
|
;; last modification
|
||||||
|
(clean-orphan cfg)
|
||||||
|
|
||||||
|
;; Then, schedule the next tasks in queue
|
||||||
|
(if-let [tasks (get-tasks cfg)]
|
||||||
|
(->> (group-by :queue tasks)
|
||||||
|
(run! (partial push-tasks cfg)))
|
||||||
|
|
||||||
|
;; If no tasks found on this batch run, we signal the
|
||||||
|
;; run-loop to wait for some time before start running
|
||||||
|
;; the next batch interation
|
||||||
|
::wait)))
|
||||||
|
|
||||||
(run-batch []
|
(run-batch []
|
||||||
(let [rconn (rds/connect cfg)]
|
(let [rconn (rds/connect cfg)]
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
[:max-retries :int]
|
[:max-retries :int]
|
||||||
[:retry-num :int]
|
[:retry-num :int]
|
||||||
[:priority :int]
|
[:priority :int]
|
||||||
[:status [:enum "scheduled" "completed" "new" "retry" "failed"]]
|
[:status [:enum "scheduled" "running" "completed" "new" "retry" "failed"]]
|
||||||
[:label {:optional true} :string]
|
[:label {:optional true} :string]
|
||||||
[:props :map]])
|
[:props :map]])
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
(decode-task-row))))
|
(decode-task-row))))
|
||||||
|
|
||||||
(defn- run-task
|
(defn- run-task
|
||||||
[{:keys [::wrk/registry ::id ::queue] :as cfg} task]
|
[{:keys [::db/pool ::wrk/registry ::id ::queue] :as cfg} task]
|
||||||
(try
|
(try
|
||||||
(l/dbg :hint "start"
|
(l/dbg :hint "start"
|
||||||
:name (:name task)
|
:name (:name task)
|
||||||
@@ -78,6 +78,13 @@
|
|||||||
:runner-id id
|
:runner-id id
|
||||||
:retry (:retry-num task))
|
:retry (:retry-num task))
|
||||||
|
|
||||||
|
;; Mark task as running
|
||||||
|
(db/update! pool :task
|
||||||
|
{:status "running"
|
||||||
|
:modified-at (ct/now)}
|
||||||
|
{:id (:id task)}
|
||||||
|
{::db/return-keys false})
|
||||||
|
|
||||||
(let [tpoint (ct/tpoint)
|
(let [tpoint (ct/tpoint)
|
||||||
task-fn (wrk/get-task registry (:name task))
|
task-fn (wrk/get-task registry (:name task))
|
||||||
result (when task-fn (task-fn task))
|
result (when task-fn (task-fn task))
|
||||||
@@ -121,7 +128,7 @@
|
|||||||
{:status "retry" :error cause})))))))
|
{:status "retry" :error cause})))))))
|
||||||
|
|
||||||
(defn- run-task!
|
(defn- run-task!
|
||||||
[{:keys [::id ::timeout] :as cfg} task-id]
|
[{:keys [::id ::timeout] :as cfg} task-id scheduled-at]
|
||||||
(loop [task (get-task cfg task-id)]
|
(loop [task (get-task cfg task-id)]
|
||||||
(cond
|
(cond
|
||||||
(ex/exception? task)
|
(ex/exception? task)
|
||||||
@@ -129,20 +136,26 @@
|
|||||||
(db/serialization-error? task))
|
(db/serialization-error? task))
|
||||||
(do
|
(do
|
||||||
(l/wrn :hint "connection error on retrieving task from database (retrying in some instants)"
|
(l/wrn :hint "connection error on retrieving task from database (retrying in some instants)"
|
||||||
:id id
|
:runner-id id
|
||||||
:cause task)
|
:cause task)
|
||||||
(px/sleep timeout)
|
(px/sleep timeout)
|
||||||
(recur (get-task cfg task-id)))
|
(recur (get-task cfg task-id)))
|
||||||
(do
|
(do
|
||||||
(l/err :hint "unhandled exception on retrieving task from database (retrying in some instants)"
|
(l/err :hint "unhandled exception on retrieving task from database (retrying in some instants)"
|
||||||
:id id
|
:runner-id id
|
||||||
:cause task)
|
:cause task)
|
||||||
(px/sleep timeout)
|
(px/sleep timeout)
|
||||||
(recur (get-task cfg task-id))))
|
(recur (get-task cfg task-id))))
|
||||||
|
|
||||||
|
(not= (inst-ms scheduled-at)
|
||||||
|
(inst-ms (:scheduled-at task)))
|
||||||
|
(l/wrn :hint "skiping task, rescheduled"
|
||||||
|
:task-id task-id
|
||||||
|
:runner-id id)
|
||||||
|
|
||||||
(nil? task)
|
(nil? task)
|
||||||
(l/wrn :hint "no task found on the database"
|
(l/wrn :hint "no task found on the database"
|
||||||
:id id
|
:runner-id id
|
||||||
:task-id task-id)
|
:task-id task-id)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
@@ -185,17 +198,19 @@
|
|||||||
(db/update! pool :task
|
(db/update! pool :task
|
||||||
{:completed-at now
|
{:completed-at now
|
||||||
:modified-at now
|
:modified-at now
|
||||||
|
:error nil
|
||||||
:status "completed"}
|
:status "completed"}
|
||||||
{:id (:id task)})
|
{:id (:id task)})
|
||||||
nil))
|
nil))
|
||||||
|
|
||||||
(decode-payload [payload]
|
(decode-payload [payload]
|
||||||
(try
|
(try
|
||||||
(let [task-id (t/decode-str payload)]
|
(let [[task-id scheduled-at :as payload] (t/decode-str payload)]
|
||||||
(if (uuid? task-id)
|
(if (and (uuid? task-id)
|
||||||
task-id
|
(ct/inst? scheduled-at))
|
||||||
(l/err :hint "received unexpected payload (uuid expected)"
|
payload
|
||||||
:payload task-id)))
|
(l/err :hint "received unexpected payload"
|
||||||
|
:payload payload)))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/err :hint "unable to decode payload"
|
(l/err :hint "unable to decode payload"
|
||||||
:payload payload
|
:payload payload
|
||||||
@@ -211,8 +226,8 @@
|
|||||||
(throw (IllegalArgumentException.
|
(throw (IllegalArgumentException.
|
||||||
(str "invalid status received: " status))))))
|
(str "invalid status received: " status))))))
|
||||||
|
|
||||||
(run-task-loop [task-id]
|
(run-task-loop [[task-id scheduled-at]]
|
||||||
(loop [result (run-task! cfg task-id)]
|
(loop [result (run-task! cfg task-id scheduled-at)]
|
||||||
(when-let [cause (process-result result)]
|
(when-let [cause (process-result result)]
|
||||||
(if (or (db/connection-error? cause)
|
(if (or (db/connection-error? cause)
|
||||||
(db/serialization-error? cause))
|
(db/serialization-error? cause))
|
||||||
@@ -226,7 +241,7 @@
|
|||||||
:cause cause))))))]
|
:cause cause))))))]
|
||||||
|
|
||||||
(try
|
(try
|
||||||
(let [key (str/ffmt "taskq:%" queue)
|
(let [key (str/ffmt "penpot.worker.queue:%" queue)
|
||||||
[_ payload] (rds/blpop conn [key] timeout)]
|
[_ payload] (rds/blpop conn [key] timeout)]
|
||||||
(some-> payload
|
(some-> payload
|
||||||
decode-payload
|
decode-payload
|
||||||
|
|||||||
@@ -1436,74 +1436,6 @@
|
|||||||
(update :pages-index d/update-vals update-container)
|
(update :pages-index d/update-vals update-container)
|
||||||
(d/update-when :components d/update-vals update-container))))
|
(d/update-when :components d/update-vals update-container))))
|
||||||
|
|
||||||
(def ^:private valid-stroke?
|
|
||||||
(sm/lazy-validator cts/schema:stroke))
|
|
||||||
|
|
||||||
(defmethod migrate-data "0007-clear-invalid-strokes-and-fills-v2"
|
|
||||||
[data _]
|
|
||||||
(letfn [(clear-color-image [image]
|
|
||||||
(select-keys image types.color/image-attrs))
|
|
||||||
|
|
||||||
(clear-color-gradient [gradient]
|
|
||||||
(select-keys gradient types.color/gradient-attrs))
|
|
||||||
|
|
||||||
(clear-stroke [stroke]
|
|
||||||
(-> stroke
|
|
||||||
(select-keys cts/stroke-attrs)
|
|
||||||
(d/update-when :stroke-color-gradient clear-color-gradient)
|
|
||||||
(d/update-when :stroke-image clear-color-image)
|
|
||||||
(d/update-when :stroke-style #(if (#{:svg :none} %) :solid %))))
|
|
||||||
|
|
||||||
(fix-strokes [strokes]
|
|
||||||
(->> (map clear-stroke strokes)
|
|
||||||
(filterv valid-stroke?)))
|
|
||||||
|
|
||||||
;; Fixes shapes with nested :fills in the :fills attribute
|
|
||||||
;; introduced in a migration `0006-fix-old-texts-fills` when
|
|
||||||
;; types.text/transform-nodes with identity pred was broken
|
|
||||||
(remove-nested-fills [[fill :as fills]]
|
|
||||||
(if (and (= 1 (count fills))
|
|
||||||
(contains? fill :fills))
|
|
||||||
(:fills fill)
|
|
||||||
fills))
|
|
||||||
|
|
||||||
(clear-fill [fill]
|
|
||||||
(-> fill
|
|
||||||
(select-keys types.fills/fill-attrs)
|
|
||||||
(d/update-when :fill-image clear-color-image)
|
|
||||||
(d/update-when :fill-color-gradient clear-color-gradient)))
|
|
||||||
|
|
||||||
(fix-fills [fills]
|
|
||||||
(->> fills
|
|
||||||
(remove-nested-fills)
|
|
||||||
(map clear-fill)
|
|
||||||
(filterv valid-fill?)))
|
|
||||||
|
|
||||||
(fix-object [object]
|
|
||||||
(-> object
|
|
||||||
(d/update-when :strokes fix-strokes)
|
|
||||||
(d/update-when :fills fix-fills)))
|
|
||||||
|
|
||||||
(fix-text-content [content]
|
|
||||||
(->> content
|
|
||||||
(types.text/transform-nodes types.text/is-content-node? fix-object)
|
|
||||||
(types.text/transform-nodes types.text/is-paragraph-set-node? #(dissoc % :fills))))
|
|
||||||
|
|
||||||
(update-shape [object]
|
|
||||||
(-> object
|
|
||||||
(fix-object)
|
|
||||||
;; The text shape also can has strokes and fils on the
|
|
||||||
;; text fragments so we need to fix them there
|
|
||||||
(cond-> (cfh/text-shape? object)
|
|
||||||
(update :content fix-text-content))))
|
|
||||||
|
|
||||||
(update-container [container]
|
|
||||||
(d/update-when container :objects d/update-vals update-shape))]
|
|
||||||
|
|
||||||
(-> data
|
|
||||||
(update :pages-index d/update-vals update-container)
|
|
||||||
(d/update-when :components d/update-vals update-container))))
|
|
||||||
|
|
||||||
(defmethod migrate-data "0008-fix-library-colors-v4"
|
(defmethod migrate-data "0008-fix-library-colors-v4"
|
||||||
[data _]
|
[data _]
|
||||||
(letfn [(clear-color-opacity [color]
|
(letfn [(clear-color-opacity [color]
|
||||||
@@ -1615,6 +1547,76 @@
|
|||||||
(update component :path #(d/nilv % "")))]
|
(update component :path #(d/nilv % "")))]
|
||||||
(d/update-when data :components d/update-vals update-component)))
|
(d/update-when data :components d/update-vals update-component)))
|
||||||
|
|
||||||
|
(def ^:private valid-stroke?
|
||||||
|
(sm/lazy-validator cts/schema:stroke))
|
||||||
|
|
||||||
|
(defmethod migrate-data "0013-clear-invalid-strokes-and-fills"
|
||||||
|
[data _]
|
||||||
|
(letfn [(clear-color-image [image]
|
||||||
|
(select-keys image types.color/image-attrs))
|
||||||
|
|
||||||
|
(clear-color-gradient [gradient]
|
||||||
|
(select-keys gradient types.color/gradient-attrs))
|
||||||
|
|
||||||
|
(clear-stroke [stroke]
|
||||||
|
(-> stroke
|
||||||
|
(select-keys cts/stroke-attrs)
|
||||||
|
(d/update-when :stroke-color-gradient clear-color-gradient)
|
||||||
|
(d/update-when :stroke-image clear-color-image)
|
||||||
|
(d/update-when :stroke-style #(if (#{:svg :none} %) :solid %))))
|
||||||
|
|
||||||
|
(fix-strokes [strokes]
|
||||||
|
(->> (map clear-stroke strokes)
|
||||||
|
(filterv valid-stroke?)))
|
||||||
|
|
||||||
|
;; Fixes shapes with nested :fills in the :fills attribute
|
||||||
|
;; introduced in a migration `0006-fix-old-texts-fills` when
|
||||||
|
;; types.text/transform-nodes with identity pred was broken
|
||||||
|
(remove-nested-fills [[fill :as fills]]
|
||||||
|
(if (and (= 1 (count fills))
|
||||||
|
(contains? fill :fills))
|
||||||
|
(:fills fill)
|
||||||
|
fills))
|
||||||
|
|
||||||
|
(clear-fill [fill]
|
||||||
|
(-> fill
|
||||||
|
(select-keys types.fills/fill-attrs)
|
||||||
|
(d/update-when :fill-image clear-color-image)
|
||||||
|
(d/update-when :fill-color-gradient clear-color-gradient)))
|
||||||
|
|
||||||
|
(fix-fills [fills]
|
||||||
|
(->> fills
|
||||||
|
(remove-nested-fills)
|
||||||
|
(map clear-fill)
|
||||||
|
(filterv valid-fill?)))
|
||||||
|
|
||||||
|
(fix-object [object]
|
||||||
|
(-> object
|
||||||
|
(d/update-when :strokes fix-strokes)
|
||||||
|
(d/update-when :fills fix-fills)))
|
||||||
|
|
||||||
|
(fix-text-content [content]
|
||||||
|
(->> content
|
||||||
|
(types.text/transform-nodes types.text/is-content-node? fix-object)
|
||||||
|
(types.text/transform-nodes types.text/is-paragraph-set-node? #(dissoc % :fills))))
|
||||||
|
|
||||||
|
(update-shape [object]
|
||||||
|
(-> object
|
||||||
|
(fix-object)
|
||||||
|
(d/update-when :position-data #(mapv fix-object %))
|
||||||
|
|
||||||
|
;; The text shape can also have strokes and fills on
|
||||||
|
;; the text fragments, so we need to fix them there.
|
||||||
|
(cond-> (cfh/text-shape? object)
|
||||||
|
(update :content fix-text-content))))
|
||||||
|
|
||||||
|
(update-container [container]
|
||||||
|
(d/update-when container :objects d/update-vals update-shape))]
|
||||||
|
|
||||||
|
(-> data
|
||||||
|
(update :pages-index d/update-vals update-container)
|
||||||
|
(d/update-when :components d/update-vals update-container))))
|
||||||
|
|
||||||
(defmethod migrate-data "0014-fix-tokens-lib-duplicate-ids"
|
(defmethod migrate-data "0014-fix-tokens-lib-duplicate-ids"
|
||||||
[data _]
|
[data _]
|
||||||
(d/update-when data :tokens-lib types.tokens-lib/fix-duplicate-token-set-ids))
|
(d/update-when data :tokens-lib types.tokens-lib/fix-duplicate-token-set-ids))
|
||||||
@@ -1681,7 +1683,6 @@
|
|||||||
"0004-clean-shadow-color"
|
"0004-clean-shadow-color"
|
||||||
"0005-deprecate-image-type"
|
"0005-deprecate-image-type"
|
||||||
"0006-fix-old-texts-fills"
|
"0006-fix-old-texts-fills"
|
||||||
"0007-clear-invalid-strokes-and-fills-v2"
|
|
||||||
"0008-fix-library-colors-v4"
|
"0008-fix-library-colors-v4"
|
||||||
"0009-clean-library-colors"
|
"0009-clean-library-colors"
|
||||||
"0009-add-partial-text-touched-flags"
|
"0009-add-partial-text-touched-flags"
|
||||||
@@ -1689,4 +1690,5 @@
|
|||||||
"0011-fix-invalid-text-touched-flags"
|
"0011-fix-invalid-text-touched-flags"
|
||||||
"0012-fix-position-data"
|
"0012-fix-position-data"
|
||||||
"0013-fix-component-path"
|
"0013-fix-component-path"
|
||||||
|
"0013-clear-invalid-strokes-and-fills"
|
||||||
"0014-fix-tokens-lib-duplicate-ids"]))
|
"0014-fix-tokens-lib-duplicate-ids"]))
|
||||||
|
|||||||
@@ -420,7 +420,7 @@
|
|||||||
:min 0
|
:min 0
|
||||||
:max 1
|
:max 1
|
||||||
:compile
|
:compile
|
||||||
(fn [{:keys [kind max min] :as props} children _]
|
(fn [{:keys [kind max min ordered] :as props} children _]
|
||||||
(let [kind (or (last children) kind)
|
(let [kind (or (last children) kind)
|
||||||
|
|
||||||
pred
|
pred
|
||||||
@@ -456,18 +456,23 @@
|
|||||||
(fn [value]
|
(fn [value]
|
||||||
(every? pred value)))
|
(every? pred value)))
|
||||||
|
|
||||||
|
empty-set
|
||||||
|
(if ordered
|
||||||
|
(d/ordered-set)
|
||||||
|
#{})
|
||||||
|
|
||||||
decode
|
decode
|
||||||
(fn [v]
|
(fn [v]
|
||||||
(cond
|
(cond
|
||||||
(string? v)
|
(string? v)
|
||||||
(let [v (str/split v #"[\s,]+")]
|
(let [v (str/split v #"[\s,]+")]
|
||||||
(into #{} xf:filter-word-strings v))
|
(into empty-set xf:filter-word-strings v))
|
||||||
|
|
||||||
(set? v)
|
(set? v)
|
||||||
v
|
v
|
||||||
|
|
||||||
(coll? v)
|
(coll? v)
|
||||||
(into #{} v)
|
(into empty-set v)
|
||||||
|
|
||||||
:else
|
:else
|
||||||
v))
|
v))
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
[:version :int]
|
[:version :int]
|
||||||
[:features ::cfeat/features]
|
[:features ::cfeat/features]
|
||||||
[:migrations {:optional true}
|
[:migrations {:optional true}
|
||||||
[::sm/set :string]]])
|
[::sm/set {:ordered true} :string]]])
|
||||||
|
|
||||||
(sm/register! ::data schema:data)
|
(sm/register! ::data schema:data)
|
||||||
(sm/register! ::file schema:file)
|
(sm/register! ::file schema:file)
|
||||||
|
|||||||
131
frontend/playwright/data/design/get-file-12287.json
Normal file
131
frontend/playwright/data/design/get-file-12287.json
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"~:features": {
|
||||||
|
"~#set": [
|
||||||
|
"fdata/path-data",
|
||||||
|
"plugins/runtime",
|
||||||
|
"design-tokens/v1",
|
||||||
|
"variants/v1",
|
||||||
|
"layout/grid",
|
||||||
|
"styles/v2",
|
||||||
|
"fdata/objects-map",
|
||||||
|
"components/v2",
|
||||||
|
"fdata/shape-data-type"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:team-id": "~u3e5ffd68-2819-8084-8006-eb1c740c9873",
|
||||||
|
"~:permissions": {
|
||||||
|
"~:type": "~:membership",
|
||||||
|
"~:is-owner": true,
|
||||||
|
"~:is-admin": true,
|
||||||
|
"~:can-edit": true,
|
||||||
|
"~:can-read": true,
|
||||||
|
"~:is-logged": true
|
||||||
|
},
|
||||||
|
"~:has-media-trimmed": false,
|
||||||
|
"~:comment-thread-seqn": 0,
|
||||||
|
"~:name": "Bug 12287",
|
||||||
|
"~:revn": 1,
|
||||||
|
"~:modified-at": "~m1760447075612",
|
||||||
|
"~:vern": 0,
|
||||||
|
"~:id": "~u4bdef584-e28a-8155-8006-f3f8a71b382e",
|
||||||
|
"~:is-shared": false,
|
||||||
|
"~:migrations": {
|
||||||
|
"~#ordered-set": [
|
||||||
|
"legacy-2",
|
||||||
|
"legacy-3",
|
||||||
|
"legacy-5",
|
||||||
|
"legacy-6",
|
||||||
|
"legacy-7",
|
||||||
|
"legacy-8",
|
||||||
|
"legacy-9",
|
||||||
|
"legacy-10",
|
||||||
|
"legacy-11",
|
||||||
|
"legacy-12",
|
||||||
|
"legacy-13",
|
||||||
|
"legacy-14",
|
||||||
|
"legacy-16",
|
||||||
|
"legacy-17",
|
||||||
|
"legacy-18",
|
||||||
|
"legacy-19",
|
||||||
|
"legacy-25",
|
||||||
|
"legacy-26",
|
||||||
|
"legacy-27",
|
||||||
|
"legacy-28",
|
||||||
|
"legacy-29",
|
||||||
|
"legacy-31",
|
||||||
|
"legacy-32",
|
||||||
|
"legacy-33",
|
||||||
|
"legacy-34",
|
||||||
|
"legacy-36",
|
||||||
|
"legacy-37",
|
||||||
|
"legacy-38",
|
||||||
|
"legacy-39",
|
||||||
|
"legacy-40",
|
||||||
|
"legacy-41",
|
||||||
|
"legacy-42",
|
||||||
|
"legacy-43",
|
||||||
|
"legacy-44",
|
||||||
|
"legacy-45",
|
||||||
|
"legacy-46",
|
||||||
|
"legacy-47",
|
||||||
|
"legacy-48",
|
||||||
|
"legacy-49",
|
||||||
|
"legacy-50",
|
||||||
|
"legacy-51",
|
||||||
|
"legacy-52",
|
||||||
|
"legacy-53",
|
||||||
|
"legacy-54",
|
||||||
|
"legacy-55",
|
||||||
|
"legacy-56",
|
||||||
|
"legacy-57",
|
||||||
|
"legacy-59",
|
||||||
|
"legacy-62",
|
||||||
|
"legacy-65",
|
||||||
|
"legacy-66",
|
||||||
|
"legacy-67",
|
||||||
|
"0001-remove-tokens-from-groups",
|
||||||
|
"0002-normalize-bool-content-v2",
|
||||||
|
"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",
|
||||||
|
"0007-clear-invalid-strokes-and-fills-v2",
|
||||||
|
"0008-fix-library-colors-v4",
|
||||||
|
"0009-clean-library-colors",
|
||||||
|
"0009-add-partial-text-touched-flags",
|
||||||
|
"0010-fix-swap-slots-pointing-non-existent-shapes",
|
||||||
|
"0011-fix-invalid-text-touched-flags",
|
||||||
|
"0012-fix-position-data",
|
||||||
|
"0013-fix-component-path",
|
||||||
|
"0014-fix-tokens-lib-duplicate-ids"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"~:version": 67,
|
||||||
|
"~:project-id": "~u3e5ffd68-2819-8084-8006-eb1c740cecec",
|
||||||
|
"~:created-at": "~m1760447051884",
|
||||||
|
"~:backend": "legacy-db",
|
||||||
|
"~:data": {
|
||||||
|
"~:pages": [
|
||||||
|
"~u4bdef584-e28a-8155-8006-f3f8a71b382f"
|
||||||
|
],
|
||||||
|
"~:pages-index": {
|
||||||
|
"~u4bdef584-e28a-8155-8006-f3f8a71b382f": {
|
||||||
|
"~: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\",[\"~u7dd7d979-39c8-802f-8006-f3f8aa066134\"]]]",
|
||||||
|
"~u7dd7d979-39c8-802f-8006-f3f8aa066134": "[\"~#shape\",[\"^ \",\"~:y\",387,\"~: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\",\"1sho7ukh86n\",\"~:children\",[[\"^ \",\"^7\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"1.2\",\"^;\",\"normal\",\"~:typography-ref-id\",null,\"~:text-transform\",\"none\",\"~:font-id\",\"sourcesanspro\",\"^8\",\"17twz8zxww7\",\"~:font-size\",\"14\",\"~:font-weight\",\"400\",\"~:typography-ref-file\",null,\"~:font-variant-id\",\"regular\",\"~:text-decoration\",\"none\",\"~:letter-spacing\",\"0\",\"~:fills\",[],\"~:font-family\",\"sourcesanspro\",\"~:text\",\"Lorem ipsum\"]],\"^<\",null,\"^=\",\"none\",\"~:text-align\",\"left\",\"^>\",\"sourcesanspro\",\"^8\",\"3nu9h9p6vg\",\"^?\",\"14\",\"^@\",\"400\",\"^A\",null,\"~:text-direction\",\"ltr\",\"^7\",\"paragraph\",\"^B\",\"regular\",\"^C\",\"none\",\"^D\",\"0\",\"^E\",[],\"^F\",\"sourcesanspro\"]]]],\"~:vertical-align\",\"top\"],\"~:hide-in-viewer\",false,\"~:name\",\"Lorem ipsum\",\"~:width\",77,\"^7\",\"^G\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",818,\"~:y\",387]],[\"^O\",[\"^ \",\"~:x\",895,\"~:y\",387]],[\"^O\",[\"^ \",\"~:x\",895,\"~:y\",404]],[\"^O\",[\"^ \",\"~:x\",818,\"~:y\",404]]],\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u7dd7d979-39c8-802f-8006-f3f8aa066134\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:position-data\",[[\"~#rect\",[\"^ \",\"~:y\",404.5,\"^;\",\"normal\",\"^=\",\"none\",\"^?\",\"14px\",\"^@\",\"400\",\"~:y1\",-1,\"^M\",76.5859375,\"^C\",\"none\",\"^D\",\"normal\",\"~:x\",818,\"~:x1\",0,\"~:y2\",17.5,\"^E\",[],\"~:x2\",76.5859375,\"~:direction\",\"ltr\",\"^F\",\"sourcesanspro\",\"~:height\",18.5,\"^G\",\"Lorem ipsum\"]]],\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:x\",818,\"~:selrect\",[\"^T\",[\"^ \",\"~:x\",818,\"~:y\",387,\"^M\",77,\"^Z\",17,\"^V\",818,\"^U\",387,\"^X\",895,\"^W\",404]],\"~:flip-x\",null,\"^Z\",17,\"~:flip-y\",null]]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~u4bdef584-e28a-8155-8006-f3f8a71b382f",
|
||||||
|
"~:name": "Page 1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"~:id": "~u4bdef584-e28a-8155-8006-f3f8a71b382e",
|
||||||
|
"~:options": {
|
||||||
|
"~:components-v2": true,
|
||||||
|
"~:base-font-size": "16px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -283,3 +283,40 @@ test("BUG 11177 - Font size input not showing 'mixed' when needed", async ({
|
|||||||
await expect(fontSizeInput).toHaveValue("");
|
await expect(fontSizeInput).toHaveValue("");
|
||||||
await expect(fontSizeInput).toHaveAttribute("placeholder", "Mixed");
|
await expect(fontSizeInput).toHaveAttribute("placeholder", "Mixed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("BUG 12287 Fix identical text fills not being added/removed", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const workspace = new WorkspacePage(page);
|
||||||
|
await workspace.setupEmptyFile();
|
||||||
|
await workspace.mockRPC(/get\-file\?/, "design/get-file-12287.json");
|
||||||
|
|
||||||
|
await workspace.goToWorkspace({
|
||||||
|
fileId: "4bdef584-e28a-8155-8006-f3f8a71b382e",
|
||||||
|
pageId: "4bdef584-e28a-8155-8006-f3f8a71b382f",
|
||||||
|
});
|
||||||
|
|
||||||
|
await workspace.clickLeafLayer("Lorem ipsum");
|
||||||
|
|
||||||
|
const addFillButton = workspace.page.getByRole("button", {
|
||||||
|
name: "Add fill",
|
||||||
|
});
|
||||||
|
|
||||||
|
await addFillButton.click();
|
||||||
|
await addFillButton.click();
|
||||||
|
await addFillButton.click();
|
||||||
|
await addFillButton.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
workspace.page.getByRole("button", { name: "#B1B2B5" }),
|
||||||
|
).toHaveCount(4);
|
||||||
|
|
||||||
|
await workspace.page
|
||||||
|
.getByRole("button", { name: "Remove color" })
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
workspace.page.getByRole("button", { name: "#B1B2B5" }),
|
||||||
|
).toHaveCount(3);
|
||||||
|
});
|
||||||
|
|||||||
@@ -490,11 +490,7 @@
|
|||||||
;; We don't have the fills attribute. It's an old text without color
|
;; We don't have the fills attribute. It's an old text without color
|
||||||
;; so need to be black
|
;; so need to be black
|
||||||
(and (nil? (:fills node)) (empty? color-attrs))
|
(and (nil? (:fills node)) (empty? color-attrs))
|
||||||
(assoc :fills (txt/get-default-text-fills))
|
(assoc :fills (txt/get-default-text-fills)))))
|
||||||
|
|
||||||
;; Remove duplicates from the fills
|
|
||||||
:always
|
|
||||||
(update :fills types.fills/update distinct))))
|
|
||||||
|
|
||||||
(defn migrate-content
|
(defn migrate-content
|
||||||
[content]
|
[content]
|
||||||
|
|||||||
Reference in New Issue
Block a user