Compare commits

...

25 Commits
2.0.0 ... 2.0.2

Author SHA1 Message Date
Alejandro Alonso
380c77a704 Merge remote-tracking branch 'origin/staging' 2024-04-16 12:41:24 +02:00
Andrey Antukh
caaf695352 📚 Update changelog 2024-04-16 12:39:13 +02:00
Alejandro
56f4348586 Merge pull request #4452 from penpot/niwinz-staging-bugfix-1
 Reduce lock contention on uploading file object thumbnail
2024-04-16 11:52:24 +02:00
Andrey Antukh
56ba32b66d Reduce lock contention on uploading file object thumbnail 2024-04-16 11:37:35 +02:00
Alejandro
4dacba6836 Merge pull request #4450 from penpot/niwinz-staging-bugfix-1
 Make cron task schedule sync more lock resilent
2024-04-16 09:28:03 +02:00
Jordi Sala Morales
ddfe5fbcb8 Avoid non existent function warning 2024-04-16 08:47:35 +02:00
Andrey Antukh
7948f565e3 Make cron task schedule sync more lock resilent 2024-04-16 08:39:04 +02:00
Alejandro Alonso
2bca2b005e Merge remote-tracking branch 'origin/staging' 2024-04-15 20:57:17 +02:00
Alejandro
4cb57c9748 Merge pull request #4446 from penpot/superalex-update-changes-2
📚 Update CHANGES for 2.0.1
2024-04-15 20:57:02 +02:00
Alejandro Alonso
bb76700c18 📚 Update CHANGES for 2.0.1 2024-04-15 20:55:51 +02:00
Andrey Antukh
33bdf5e83f Merge remote-tracking branch 'origin/staging' 2024-04-15 20:27:29 +02:00
Alejandro Alonso
f0eff95e18 🐛 Fix v2 components migration script 2024-04-15 20:26:51 +02:00
Alejandro Alonso
2a6b9f06b3 Merge remote-tracking branch 'origin/staging' 2024-04-15 16:46:54 +02:00
Alejandro
f531a5c323 Merge pull request #4442 from penpot/niwinz-staging-bugfixes-14
🐛 Bugfixes
2024-04-15 16:24:11 +02:00
Andrey Antukh
36e66c4dd9 Merge remote-tracking branch 'origin/staging' 2024-04-15 14:27:46 +02:00
Andrey Antukh
8c2038e43b 🐛 Fix incorrect name on audit event 2024-04-15 14:27:24 +02:00
Andrey Antukh
0135b477ca Add improved traceability of climit module 2024-04-15 14:27:24 +02:00
Alejandro
8bf1b9c28e Merge pull request #4427 from penpot/palba-bugfixing-007
🐛 Bugfixing
2024-04-15 12:48:28 +02:00
Alejandro
002772ff0e Merge pull request #4429 from penpot/alotor-bugfix-44
Alotor bugfix 44
2024-04-15 12:38:56 +02:00
alonso.torres
4838571ec2 🐛 Fix problem with position-data overriding in copies 2024-04-15 10:13:01 +02:00
alonso.torres
8e71d219ca 🐛 Fix editor when several colors are in a single word 2024-04-15 10:13:01 +02:00
alonso.torres
cbac4587cf 🐛 Fix crash when removing multiple text fills 2024-04-15 10:13:01 +02:00
alonso.torres
e636bdd0b0 🐛 Fix problem copy/paste svg text 2024-04-15 10:13:01 +02:00
Pablo Alba
a7a3344030 🐛 Inverted highlight constraint for vertical and horizontal constraints 2024-04-12 12:53:03 +02:00
Pablo Alba
137e576e63 🐛 Fix scrollbar appears on top of UI buttons 2024-04-12 12:39:48 +02:00
16 changed files with 212 additions and 147 deletions

View File

@@ -1,5 +1,19 @@
# CHANGELOG
## 2.0.2
### :sparkles: Enhancements
- Fix locking contention on cron subsystem (causes backend start blocking)
- Fix locking contention on file object thumbails backend RPC calls
## 2.0.1
### :bug: Bugs fixed
- Fix different issues related to components v2 migrations including [Github #4443](https://github.com/penpot/penpot/issues/4443)
## 2.0.0 - I Just Can't Get Enough
### :rocket: Epics and highlights

View File

@@ -18,7 +18,7 @@
row_number() OVER (ORDER BY created_at DESC) AS rown
FROM team
WHERE deleted_at IS NULL
AND (features <@ '{components/v2}' OR features IS NULL)
AND (not (features @> '{components/v2}') OR features IS NULL)
ORDER BY created_at DESC")
(defn- get-teams
@@ -37,7 +37,7 @@
;; Run teams migration
(run! (fn [{:keys [id rown]}]
(try
(-> (assoc system ::db/rollback true)
(-> (assoc system ::db/rollback false)
(feat/migrate-team! id
:rown rown
:label "v2-migration"

View File

@@ -20,6 +20,7 @@
[app.util.time :as dt]
[app.worker :as-alias wrk]
[clojure.edn :as edn]
[clojure.set :as set]
[clojure.spec.alpha :as s]
[datoteka.fs :as fs]
[integrant.core :as ig]
@@ -91,67 +92,77 @@
:timeout (:timeout config)
:type :semaphore))
(defmacro ^:private measure-and-log!
[metrics mlabels stats id action limit-id limit-label profile-id elapsed]
`(let [mpermits# (:max-permits ~stats)
mqueue# (:max-queue ~stats)
permits# (:permits ~stats)
queue# (:queue ~stats)
queue# (- queue# mpermits#)
queue# (if (neg? queue#) 0 queue#)
level# (if (pos? queue#) :warn :trace)]
(mtx/run! ~metrics
:id :rpc-climit-queue
:val queue#
:labels ~mlabels)
(defn measure!
[metrics mlabels stats elapsed]
(let [mpermits (:max-permits stats)
permits (:permits stats)
queue (:queue stats)
queue (- queue mpermits)
queue (if (neg? queue) 0 queue)]
(mtx/run! ~metrics
:id :rpc-climit-permits
:val permits#
:labels ~mlabels)
(mtx/run! metrics
:id :rpc-climit-queue
:val queue
:labels mlabels)
(l/log level#
:hint ~action
:req ~id
:id ~limit-id
:label ~limit-label
:profile-id (str ~profile-id)
:permits permits#
:queue queue#
:max-permits mpermits#
:max-queue mqueue#
~@(if (some? elapsed)
[:elapsed `(dt/format-duration ~elapsed)]
[]))))
(mtx/run! metrics
:id :rpc-climit-permits
:val permits
:labels mlabels)
(when elapsed
(mtx/run! metrics
:id :rpc-climit-timing
:val (inst-ms elapsed)
:labels mlabels))))
(defn log!
[action req-id stats limit-id limit-label params elapsed]
(let [mpermits (:max-permits stats)
queue (:queue stats)
queue (- queue mpermits)
queue (if (neg? queue) 0 queue)
level (if (pos? queue) :warn :trace)]
(l/log level
:hint action
:req req-id
:id limit-id
:label limit-label
:queue queue
:elapsed (some-> elapsed dt/format-duration)
:params (-> (select-keys params [::rpc/profile-id :file-id :profile-id])
(set/rename-keys {::rpc/profile-id :profile-id})
(update-vals str)))))
(def ^:private idseq (AtomicLong. 0))
(defn- invoke
[limiter metrics limit-id limit-key limit-label profile-id f params]
[limiter metrics limit-id limit-key limit-label handler params]
(let [tpoint (dt/tpoint)
mlabels (into-array String [(id->str limit-id)])
limit-id (id->str limit-id limit-key)
stats (pbh/get-stats limiter)
id (.incrementAndGet ^AtomicLong idseq)]
req-id (.incrementAndGet ^AtomicLong idseq)]
(try
(measure-and-log! metrics mlabels stats id "enqueued" limit-id limit-label profile-id nil)
(measure! metrics mlabels stats nil)
(log! "enqueued" req-id stats limit-id limit-label params nil)
(px/invoke! limiter (fn []
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(measure-and-log! metrics mlabels stats id "acquired" limit-id limit-label profile-id elapsed)
(mtx/run! metrics
:id :rpc-climit-timing
:val (inst-ms elapsed)
:labels mlabels)
(apply f params))))
(measure! metrics mlabels stats elapsed)
(log! "acquired" req-id stats limit-id limit-label params elapsed)
(handler params))))
(catch ExceptionInfo cause
(let [{:keys [type code]} (ex-data cause)]
(if (= :bulkhead-error type)
(let [elapsed (tpoint)]
(measure-and-log! metrics mlabels stats id "reject" limit-id limit-label profile-id elapsed)
(log! "rejected" req-id stats limit-id limit-label params elapsed)
(ex/raise :type :concurrency-limit
:code code
:hint "concurrency limit reached"
@@ -161,7 +172,9 @@
(finally
(let [elapsed (tpoint)
stats (pbh/get-stats limiter)]
(measure-and-log! metrics mlabels stats id "finished" limit-id limit-label profile-id elapsed))))))
(measure! metrics mlabels stats nil)
(log! "finished" req-id stats limit-id limit-label params elapsed))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MIDDLEWARE
@@ -219,10 +232,8 @@
(let [limit-key (key-fn params)
cache-key [limit-id limit-key]
limiter (cache/get cache cache-key (partial create-limiter config))
profile-id (if (= key-fn ::rpc/profile-id)
limit-key
(get params ::rpc/profile-id))]
(invoke limiter metrics limit-id limit-key label profile-id handler [cfg params])))))
handler (partial handler cfg)]
(invoke limiter metrics limit-id limit-key label handler params)))))
(do
(l/wrn :hint "no config found for specified queue" :id (id->str limit-id))
@@ -237,15 +248,15 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- build-exec-chain
[{:keys [::label ::profile-id ::rpc/climit ::mtx/metrics] :as cfg} f]
[{:keys [::label ::rpc/climit ::mtx/metrics] :as cfg} f]
(let [config (get climit ::config)
cache (get climit ::cache)]
(reduce (fn [handler [limit-id limit-key :as ckey]]
(if-let [config (get config limit-id)]
(fn [& params]
(let [limiter (cache/get cache ckey (partial create-limiter config))]
(invoke limiter metrics limit-id limit-key label profile-id handler params)))
(fn [cfg params]
(let [limiter (cache/get cache ckey (partial create-limiter config))
handler (partial handler cfg)]
(invoke limiter metrics limit-id limit-key label handler params)))
(do
(l/wrn :hint "config not found" :label label :id limit-id)
f)))
@@ -255,9 +266,9 @@
(defn invoke!
"Run a function in context of climit.
Intended to be used in virtual threads."
[{:keys [::executor] :as cfg} f & params]
[{:keys [::executor] :as cfg} f params]
(let [f (if (some? executor)
(fn [& params] (px/await! (px/submit! executor (fn [] (apply f params)))))
(fn [cfg params] (px/await! (px/submit! executor (fn [] (f cfg params)))))
f)
f (build-exec-chain cfg f)]
(apply f params)))
(f cfg params)))

View File

@@ -228,51 +228,52 @@
;; MUTATION COMMANDS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMAND: create-file-object-thumbnail
(def sql:get-file-object-thumbnail
"SELECT * FROM file_tagged_object_thumbnail
WHERE file_id = ? AND object_id = ? AND tag = ?
FOR UPDATE")
(defn- create-file-object-thumbnail!
[{:keys [::db/conn ::sto/storage]} file-id object-id media tag]
(def sql:create-file-object-thumbnail
"INSERT INTO file_tagged_object_thumbnail (file_id, object_id, tag, media_id)
VALUES (?, ?, ?, ?)
ON CONFLICT (file_id, object_id, tag)
DO UPDATE SET updated_at=?, media_id=?, deleted_at=null
RETURNING *")
(let [thumb (db/get* conn :file-tagged-object-thumbnail
{:file-id file-id
:object-id object-id
:tag tag}
{::db/remove-deleted false
::sql/for-update true})
path (:path media)
(defn- persist-thumbnail!
[storage media created-at]
(let [path (:path media)
mtype (:mtype media)
hash (sto/calculate-hash path)
data (-> (sto/content path)
(sto/wrap-with-hash hash))
tnow (dt/now)
(sto/wrap-with-hash hash))]
media (sto/put-object! storage
{::sto/content data
::sto/deduplicate? true
::sto/touched-at tnow
:content-type mtype
:bucket "file-object-thumbnail"})]
(sto/put-object! storage
{::sto/content data
::sto/deduplicate? true
::sto/touched-at created-at
:content-type mtype
:bucket "file-object-thumbnail"})))
(if (some? thumb)
(do
;; We mark the old media id as touched if it does not matches
(when (not= (:id media) (:media-id thumb))
(sto/touch-object! storage (:media-id thumb)))
(db/update! conn :file-tagged-object-thumbnail
{:media-id (:id media)
:deleted-at nil
:updated-at tnow}
{:file-id file-id
:object-id object-id
:tag tag}))
(db/insert! conn :file-tagged-object-thumbnail
{:file-id file-id
:object-id object-id
:created-at tnow
:updated-at tnow
:tag tag
:media-id (:id media)}))))
(defn- create-file-object-thumbnail!
[{:keys [::sto/storage] :as cfg} file-id object-id media tag]
(let [tsnow (dt/now)
media (persist-thumbnail! storage media tsnow)
[th1 th2] (db/tx-run! cfg (fn [{:keys [::db/conn]}]
(let [th1 (db/exec-one! conn [sql:get-file-object-thumbnail file-id object-id tag])
th2 (db/exec-one! conn [sql:create-file-object-thumbnail
file-id object-id tag (:id media)
tsnow (:id media)])]
[th1 th2])))]
(when (and (some? th1)
(not= (:media-id th1)
(:media-id th2)))
(sto/touch-object! storage (:media-id th1)))
th2))
(def ^:private
schema:create-file-object-thumbnail
@@ -296,16 +297,10 @@
(media/validate-media-type! media)
(media/validate-media-size! media)
(db/tx-run! cfg
(fn [{:keys [::db/conn] :as cfg}]
(files/check-edition-permissions! conn profile-id file-id)
(when-not (db/read-only? conn)
(let [cfg (-> cfg
(update ::sto/storage media/configure-assets-storage)
(assoc ::rtry/when rtry/conflict-exception?)
(assoc ::rtry/max-retries 5)
(assoc ::rtry/label "create-file-object-thumbnail"))]
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame")))))))
(db/run! cfg files/check-edition-permissions! profile-id file-id)
(let [cfg (update cfg ::sto/storage media/configure-assets-storage)]
(create-file-object-thumbnail! cfg file-id object-id media (or tag "frame"))))
;; --- MUTATION COMMAND: delete-file-object-thumbnail

View File

@@ -243,12 +243,13 @@
;; NOTE: we use the climit here in a dynamic invocation because we
;; don't want saturate the process-image limit with IO (download
;; of external image)
(-> cfg
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
[:process-image/global]])
(assoc ::climit/profile-id (:profile-id params))
(assoc ::climit/label "create-file-media-object-from-url")
(climit/invoke! db/run! cfg create-file-media-object params))))
(climit/invoke! #(db/run! %1 create-file-media-object %2) params))))
;; --- Clone File Media object (Upload and create from url)

View File

@@ -233,7 +233,7 @@
:file-mtype (:mtype file)}}))))
(defn- generate-thumbnail!
[file]
[_ file]
(let [input (media/run {:cmd :info :input file})
thumb (media/run {:cmd :profile-thumbnail
:format :jpeg
@@ -250,15 +250,15 @@
:content-type (:mtype thumb)}))
(defn upload-photo
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file]}]
[{:keys [::sto/storage ::wrk/executor] :as cfg} {:keys [file] :as params}]
(let [params (-> cfg
(assoc ::climit/id :process-image/global)
(assoc ::climit/id [[:process-image/by-profile (:profile-id params)]
[:process-image/global]])
(assoc ::climit/label "upload-photo")
(assoc ::climit/executor executor)
(climit/invoke! generate-thumbnail! file))]
(sto/put-object! storage params)))
;; --- MUTATION: Request Email Change
(declare ^:private request-email-change!)

View File

@@ -27,14 +27,15 @@
"insert into scheduled_task (id, cron_expr)
values (?, ?)
on conflict (id)
do update set cron_expr=?")
do nothing")
(defn- synchronize-cron-entries!
[{:keys [::db/pool ::entries]}]
(db/with-atomic [conn pool]
(doseq [{:keys [id cron]} entries]
(l/trc :hint "register cron task" :id id :cron (str cron))
(db/exec-one! conn [sql:upsert-cron-task id (str cron) (str cron)]))))
[{:keys [::db/conn ::entries]}]
(doseq [{:keys [id cron]} entries]
(let [result (db/exec-one! conn [sql:upsert-cron-task id (str cron)])
updated? (pos? (db/get-update-count result))]
(l/dbg :hint "register task" :id id :cron (str cron)
:status (if updated? "created" "exists")))))
(defn- lock-scheduled-task!
[conn id]
@@ -45,7 +46,7 @@
(declare ^:private schedule-cron-task)
(defn- execute-cron-task
[cfg {:keys [id] :as task}]
[cfg {:keys [id cron] :as task}]
(px/thread
{:name (str "penpot/cron-task/" id)}
(let [tpoint (dt/tpoint)]
@@ -54,20 +55,25 @@
(db/exec-one! conn ["SET LOCAL statement_timeout=0;"])
(db/exec-one! conn ["SET LOCAL idle_in_transaction_session_timeout=0;"])
(when (lock-scheduled-task! conn id)
(l/dbg :hint "start task" :task-id id)
(db/update! conn :scheduled-task
{:cron-expr (str cron)
:modified-at (dt/now)}
{:id id}
{::db/return-keys false})
(l/dbg :hint "start" :id id)
((:fn task) task)
(let [elapsed (dt/format-duration (tpoint))]
(l/dbg :hint "end task" :task-id id :elapsed elapsed)))))
(l/dbg :hint "end" :id id :elapsed elapsed)))))
(catch InterruptedException _
(let [elapsed (dt/format-duration (tpoint))]
(l/debug :hint "task interrupted" :task-id id :elapsed elapsed)))
(l/debug :hint "task interrupted" :id id :elapsed elapsed)))
(catch Throwable cause
(let [elapsed (dt/format-duration (tpoint))]
(binding [l/*context* (get-error-context cause task)]
(l/err :hint "unhandled exception on running task"
:task-id id
:id id
:elapsed elapsed
:cause cause))))
(finally
@@ -86,7 +92,7 @@
(let [ts (ms-until-valid cron)
ft (px/schedule! ts (partial execute-cron-task cfg task))]
(l/dbg :hint "schedule task" :task-id id
(l/dbg :hint "schedule task" :id id
:ts (dt/format-duration ts)
:at (dt/format-instant (dt/in-future ts)))
@@ -135,7 +141,8 @@
cfg (assoc cfg ::entries entries ::running running)]
(l/inf :hint "started" :tasks (count entries))
(synchronize-cron-entries! cfg)
(db/tx-run! cfg synchronize-cron-entries!)
(->> (filter some? entries)
(run! (partial schedule-cron-task cfg)))

View File

@@ -1158,7 +1158,7 @@
;; check that the unknown frame thumbnail is deleted
(let [rows (th/db-query :file-tagged-object-thumbnail {:file-id (:id file)})]
(t/is (= 2 (count rows)))
(t/is (= 1 (count (remove (comp some? :deleted-at) rows)))))
(t/is (= 1 (count (remove :deleted-at rows)))))
(let [res (th/run-task! :objects-gc {:min-age 0})]
(t/is (= 3 (:processed res))))

View File

@@ -21,6 +21,10 @@
flex-direction: column;
}
.public-DraftStyleDefault-block {
white-space: pre;
}
&.align-top {
.DraftEditor-root {
justify-content: flex-start;

View File

@@ -253,7 +253,7 @@
(export [_]
(->> (export-file file)
(rx/subs
(rx/subs!
(fn [value]
(when (not (contains? value :type))
(let [[file export-blob] value]

View File

@@ -163,7 +163,7 @@
(ptk/reify ::logged-in
ev/Event
(-data [_]
{::ev/name "signing"
{::ev/name "signin"
::ev/type "identify"
:email (:email profile)
:auth-backend (:auth-backend profile)

View File

@@ -1711,8 +1711,14 @@
(process-entry [[type data]]
(case type
:text
(if (str/empty? data)
(cond
(str/empty? data)
(rx/empty)
(re-find #"<svg\s" data)
(rx/of (paste-svg-text data))
:else
(rx/of (paste-text data)))
:transit
@@ -1757,8 +1763,7 @@
text-data (some-> pdata wapi/extract-text)
transit-data (ex/ignoring (some-> text-data t/decode-str))]
(cond
(and (string? text-data)
(str/includes? text-data "<svg "))
(and (string? text-data) (re-find #"<svg\s" text-data))
(rx/of (paste-svg-text text-data))
(seq image-data)

View File

@@ -1491,9 +1491,22 @@
container
{:type :reg-objects
:shapes all-parents})]))))
(let [roperation {:type :set
(let [;; position-data is a special case because can be affected by :geometry-group and :content-group
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
;; so it's calculated again
reset-pos-data?
(and (cfh/text-shape? origin-shape)
(= attr :position-data)
(not= (get origin-shape attr) (get dest-shape attr))
(touched :geometry-group))
roperation {:type :set
:attr attr
:val (get origin-shape attr)
:val (cond
;; If position data changes and the geometry group is touched
;; we need to put to nil so we can regenerate it
reset-pos-data? nil
:else (get origin-shape attr))
:ignore-touched true}
uoperation {:type :set
:attr attr

View File

@@ -46,6 +46,7 @@
@extend .button-icon;
stroke: var(--tab-foreground-color);
}
.content {
@include headlineSmallTypography;
text-align: center;
@@ -53,17 +54,21 @@
overflow: hidden;
text-overflow: ellipsis;
}
&.current,
&.current:hover {
background: var(--tab-background-color-selected);
border-color: var(--tab-border-color-selected);
color: var(--tab-foreground-color-selected);
svg {
stroke: var(--tab-foreground-color-selected);
}
}
&:hover {
color: var(--tab-foreground-color-hover);
svg {
stroke: var(--tab-foreground-color-hover);
}
@@ -78,6 +83,7 @@
min-width: $s-24;
padding: 0 $s-6;
border-radius: $br-5;
svg {
@include flexCenter;
height: $s-16;
@@ -87,6 +93,7 @@
fill: none;
color: transparent;
}
&:hover {
svg {
stroke: var(--icon-foreground-hover);
@@ -107,3 +114,10 @@
display: flex;
flex-direction: column;
}
//Firefox doesn't respect scrollbar-gutter
@supports (-moz-appearance: none) {
.tab-container-content {
padding-right: $s-8;
}
}

View File

@@ -181,14 +181,14 @@
[:span {:class (stl/css :resalted-area)}]]]
[:div {:class (stl/css :constraints-center)}
[:button {:class (stl/css-case :constraint-btn true
:active (= constraints-h :center))
:data-value "centerh"
:active (= constraints-v :center))
:data-value "centerv"
:on-click on-constraint-button-clicked}
[:span {:class (stl/css :resalted-area)}]]
[:button {:class (stl/css-case :constraint-btn-special true
:constraint-btn-rotated true
:active (= constraints-v :center))
:data-value "centerv"
:active (= constraints-h :center))
:data-value "centerh"
:on-click on-constraint-button-clicked}
[:span {:class (stl/css :resalted-area)}]]]
[:div {:class (stl/css :constraints-right)}

View File

@@ -69,33 +69,34 @@
on-add
(mf/use-fn
(mf/deps ids)
(mf/deps ids fills)
(fn [_]
(st/emit! (dc/add-fill ids {:color default-color
:opacity 1}))
(when (not (some? (seq fills))) (open-content))))
(when (or (= :multiple fills)
(not (some? (seq fills))))
(open-content))))
on-change
(mf/use-fn
(mf/deps ids)
(fn [index]
(fn [color]
(st/emit! (dc/change-fill ids color index)))))
(fn [index]
(fn [color]
(st/emit! (dc/change-fill ids color index))))
on-reorder
(mf/use-fn
(mf/deps ids)
(fn [new-index]
(fn [index]
(st/emit! (dc/reorder-fills ids index new-index)))))
(fn [new-index]
(fn [index]
(st/emit! (dc/reorder-fills ids index new-index))))
on-remove
(fn [index]
(fn []
(st/emit! (dc/remove-fill ids {:color default-color
:opacity 1} index))
(when (= 1 (count (seq fills))) (close-content))))
(when (or (= :multiple fills)
(= 1 (count (seq fills))))
(close-content))))
on-remove-all
(fn [_]
(st/emit! (dc/remove-all-fills ids {:color clr/black