mirror of
https://github.com/penpot/penpot.git
synced 2025-02-24 07:46:13 -05:00
✨ Improve page options handling.
This commit is contained in:
parent
9c68877328
commit
c8298c72ea
8 changed files with 269 additions and 283 deletions
|
@ -81,7 +81,7 @@ CREATE TABLE IF NOT EXISTS project_page_snapshots (
|
||||||
label text NOT NULL DEFAULT '',
|
label text NOT NULL DEFAULT '',
|
||||||
|
|
||||||
data bytea NOT NULL,
|
data bytea NOT NULL,
|
||||||
operations bytea NULL DEFAULT NULL
|
changes bytea NULL DEFAULT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Indexes
|
-- Indexes
|
||||||
|
|
|
@ -28,7 +28,6 @@
|
||||||
(s/def ::data ::cp/data)
|
(s/def ::data ::cp/data)
|
||||||
(s/def ::user ::us/uuid)
|
(s/def ::user ::us/uuid)
|
||||||
(s/def ::project-id ::us/uuid)
|
(s/def ::project-id ::us/uuid)
|
||||||
(s/def ::metadata ::cp/metadata)
|
|
||||||
(s/def ::ordering ::us/number)
|
(s/def ::ordering ::us/number)
|
||||||
|
|
||||||
;; --- Mutation: Create Page
|
;; --- Mutation: Create Page
|
||||||
|
@ -36,7 +35,7 @@
|
||||||
(declare create-page)
|
(declare create-page)
|
||||||
|
|
||||||
(s/def ::create-project-page
|
(s/def ::create-project-page
|
||||||
(s/keys :req-un [::user ::file-id ::name ::ordering ::metadata ::data]
|
(s/keys :req-un [::user ::file-id ::name ::ordering ::data]
|
||||||
:opt-un [::id]))
|
:opt-un [::id]))
|
||||||
|
|
||||||
(sm/defmutation ::create-project-page
|
(sm/defmutation ::create-project-page
|
||||||
|
@ -46,15 +45,14 @@
|
||||||
(create-page conn params)))
|
(create-page conn params)))
|
||||||
|
|
||||||
(defn create-page
|
(defn create-page
|
||||||
[conn {:keys [id user file-id name ordering data metadata] :as params}]
|
[conn {:keys [id user file-id name ordering data] :as params}]
|
||||||
(let [sql "insert into project_pages (id, user_id, file_id, name,
|
(let [sql "insert into project_pages (id, user_id, file_id, name,
|
||||||
ordering, data, metadata, version)
|
ordering, data, version)
|
||||||
values ($1, $2, $3, $4, $5, $6, $7, 0)
|
values ($1, $2, $3, $4, $5, $6, 0)
|
||||||
returning *"
|
returning *"
|
||||||
id (or id (uuid/next))
|
id (or id (uuid/next))
|
||||||
data (blob/encode data)
|
data (blob/encode data)]
|
||||||
mdata (blob/encode metadata)]
|
(-> (db/query-one conn [sql id user file-id name ordering data])
|
||||||
(-> (db/query-one conn [sql id user file-id name ordering data mdata])
|
|
||||||
(p/then' decode-row))))
|
(p/then' decode-row))))
|
||||||
|
|
||||||
;; --- Mutation: Update Page Data
|
;; --- Mutation: Update Page Data
|
||||||
|
@ -98,11 +96,11 @@
|
||||||
(p/then' su/constantly-nil))))
|
(p/then' su/constantly-nil))))
|
||||||
|
|
||||||
(defn- insert-page-snapshot
|
(defn- insert-page-snapshot
|
||||||
[conn {:keys [user-id id version data operations]}]
|
[conn {:keys [user-id id version data changes]}]
|
||||||
(let [sql "insert into project_page_snapshots (user_id, page_id, version, data, operations)
|
(let [sql "insert into project_page_snapshots (user_id, page_id, version, data, changes)
|
||||||
values ($1, $2, $3, $4, $5)
|
values ($1, $2, $3, $4, $5)
|
||||||
returning id, page_id, user_id, version, operations"]
|
returning id, page_id, user_id, version, changes"]
|
||||||
(db/query-one conn [sql user-id id version data operations])))
|
(db/query-one conn [sql user-id id version data changes])))
|
||||||
|
|
||||||
;; --- Mutation: Rename Page
|
;; --- Mutation: Rename Page
|
||||||
|
|
||||||
|
@ -129,16 +127,16 @@
|
||||||
|
|
||||||
;; --- Mutation: Update Page
|
;; --- Mutation: Update Page
|
||||||
|
|
||||||
;; A generic, Ops based (granular) page update method.
|
;; A generic, Changes based (granular) page update method.
|
||||||
|
|
||||||
(s/def ::operations
|
(s/def ::changes
|
||||||
(s/coll-of vector? :kind vector?))
|
(s/coll-of vector? :kind vector?))
|
||||||
|
|
||||||
(s/def ::update-project-page
|
(s/def ::update-project-page
|
||||||
(s/keys :opt-un [::id ::user ::version ::operations]))
|
(s/keys :opt-un [::id ::user ::version ::changes]))
|
||||||
|
|
||||||
(declare update-project-page)
|
(declare update-project-page)
|
||||||
(declare retrieve-lagged-operations)
|
(declare retrieve-lagged-changes)
|
||||||
|
|
||||||
(sm/defmutation ::update-project-page
|
(sm/defmutation ::update-project-page
|
||||||
[{:keys [id user] :as params}]
|
[{:keys [id user] :as params}]
|
||||||
|
@ -156,17 +154,17 @@
|
||||||
:hint "The incoming version is greater that stored version."
|
:hint "The incoming version is greater that stored version."
|
||||||
:context {:incoming-version (:version params)
|
:context {:incoming-version (:version params)
|
||||||
:stored-version (:version page)}))
|
:stored-version (:version page)}))
|
||||||
(let [ops (:operations params)
|
(let [changes (:changes params)
|
||||||
data (-> (:data page)
|
data (-> (:data page)
|
||||||
(blob/decode)
|
(blob/decode)
|
||||||
(cp/process-ops ops)
|
(cp/process-changes changes)
|
||||||
(blob/encode))
|
(blob/encode))
|
||||||
|
|
||||||
page (assoc page
|
page (assoc page
|
||||||
:user-id (:user params)
|
:user-id (:user params)
|
||||||
:data data
|
:data data
|
||||||
:version (inc (:version page))
|
:version (inc (:version page))
|
||||||
:operations (blob/encode ops))]
|
:changes (blob/encode changes))]
|
||||||
|
|
||||||
(-> (update-page-data conn page)
|
(-> (update-page-data conn page)
|
||||||
(p/then (fn [_] (insert-page-snapshot conn page)))
|
(p/then (fn [_] (insert-page-snapshot conn page)))
|
||||||
|
@ -176,24 +174,24 @@
|
||||||
:user-id (:user-id s)
|
:user-id (:user-id s)
|
||||||
:page-id (:page-id s)
|
:page-id (:page-id s)
|
||||||
:version (:version s)
|
:version (:version s)
|
||||||
:operations ops})
|
:changes changes})
|
||||||
(retrieve-lagged-operations conn s params))))))))
|
(retrieve-lagged-changes conn s params))))))))
|
||||||
|
|
||||||
(su/defstr sql:lagged-snapshots
|
(su/defstr sql:lagged-snapshots
|
||||||
"select s.id, s.operations
|
"select s.id, s.changes
|
||||||
from project_page_snapshots as s
|
from project_page_snapshots as s
|
||||||
where s.page_id = $1
|
where s.page_id = $1
|
||||||
and s.version > $2")
|
and s.version > $2")
|
||||||
|
|
||||||
(defn- retrieve-lagged-operations
|
(defn- retrieve-lagged-changes
|
||||||
[conn snapshot params]
|
[conn snapshot params]
|
||||||
(let [sql sql:lagged-snapshots]
|
(let [sql sql:lagged-snapshots]
|
||||||
(-> (db/query conn [sql (:id params) (:version params) #_(:id snapshot)])
|
(-> (db/query conn [sql (:id params) (:version params) #_(:id snapshot)])
|
||||||
(p/then (fn [rows]
|
(p/then (fn [rows]
|
||||||
{:page-id (:id params)
|
{:page-id (:id params)
|
||||||
:version (:version snapshot)
|
:version (:version snapshot)
|
||||||
:operations (into [] (comp (map decode-row)
|
:changes (into [] (comp (map decode-row)
|
||||||
(map :operations)
|
(map :changes)
|
||||||
(mapcat identity))
|
(mapcat identity))
|
||||||
rows)})))))
|
rows)})))))
|
||||||
|
|
||||||
|
|
|
@ -120,9 +120,9 @@
|
||||||
;; --- Helpers
|
;; --- Helpers
|
||||||
|
|
||||||
(defn decode-row
|
(defn decode-row
|
||||||
[{:keys [data metadata operations] :as row}]
|
[{:keys [data metadata changes] :as row}]
|
||||||
(when row
|
(when row
|
||||||
(cond-> row
|
(cond-> row
|
||||||
data (assoc :data (blob/decode data))
|
data (assoc :data (blob/decode data))
|
||||||
metadata (assoc :metadata (blob/decode metadata))
|
metadata (assoc :metadata (blob/decode metadata))
|
||||||
operations (assoc :operations (blob/decode operations)))))
|
changes (assoc :changes (blob/decode changes)))))
|
||||||
|
|
|
@ -10,25 +10,83 @@
|
||||||
(s/def ::name string?)
|
(s/def ::name string?)
|
||||||
(s/def ::type keyword?)
|
(s/def ::type keyword?)
|
||||||
|
|
||||||
;; Metadata related
|
;; Page Options
|
||||||
(s/def ::grid-x-axis number?)
|
(s/def ::grid-x number?)
|
||||||
(s/def ::grid-y-axis number?)
|
(s/def ::grid-y number?)
|
||||||
(s/def ::grid-color string?)
|
(s/def ::grid-color string?)
|
||||||
(s/def ::background string?)
|
|
||||||
(s/def ::background-opacity number?)
|
|
||||||
|
|
||||||
(s/def ::metadata
|
(s/def ::options
|
||||||
(s/keys :opt-un [::grid-y-axis
|
(s/keys :opt-un [::grid-y
|
||||||
::grid-x-axis
|
::grid-x
|
||||||
::grid-color
|
::grid-color]))
|
||||||
::background
|
|
||||||
::background-opacity]))
|
|
||||||
|
|
||||||
;; Page Data related
|
;; Page Data related
|
||||||
(s/def ::shape
|
(s/def ::blocked boolean?)
|
||||||
|
(s/def ::collapsed boolean?)
|
||||||
|
(s/def ::content string?)
|
||||||
|
(s/def ::fill-color string?)
|
||||||
|
(s/def ::fill-opacity number?)
|
||||||
|
(s/def ::font-family string?)
|
||||||
|
(s/def ::font-size number?)
|
||||||
|
(s/def ::font-style string?)
|
||||||
|
(s/def ::font-weight string?)
|
||||||
|
(s/def ::hidden boolean?)
|
||||||
|
(s/def ::letter-spacing number?)
|
||||||
|
(s/def ::line-height number?)
|
||||||
|
(s/def ::locked boolean?)
|
||||||
|
(s/def ::page-id uuid?)
|
||||||
|
(s/def ::proportion number?)
|
||||||
|
(s/def ::proportion-lock boolean?)
|
||||||
|
(s/def ::rx number?)
|
||||||
|
(s/def ::ry number?)
|
||||||
|
(s/def ::stroke-color string?)
|
||||||
|
(s/def ::stroke-opacity number?)
|
||||||
|
(s/def ::stroke-style #{:none :solid :dotted :dashed :mixed})
|
||||||
|
(s/def ::stroke-width number?)
|
||||||
|
(s/def ::text-align #{"left" "right" "center" "justify"})
|
||||||
|
(s/def ::type #{:rect :path :circle :image :text :canvas})
|
||||||
|
(s/def ::x number?)
|
||||||
|
(s/def ::y number?)
|
||||||
|
(s/def ::cx number?)
|
||||||
|
(s/def ::cy number?)
|
||||||
|
(s/def ::width number?)
|
||||||
|
(s/def ::height number?)
|
||||||
|
|
||||||
|
(s/def ::shape-attrs
|
||||||
|
(s/keys :opt-un [::blocked
|
||||||
|
::collapsed
|
||||||
|
::content
|
||||||
|
::fill-color
|
||||||
|
::fill-opacity
|
||||||
|
::font-family
|
||||||
|
::font-size
|
||||||
|
::font-style
|
||||||
|
::font-weight
|
||||||
|
::hidden
|
||||||
|
;; ::page-id ??
|
||||||
|
::letter-spacing
|
||||||
|
::line-height
|
||||||
|
::locked
|
||||||
|
::proportion
|
||||||
|
::proportion-lock
|
||||||
|
::rx ::ry
|
||||||
|
::cx ::cy
|
||||||
|
::x ::y
|
||||||
|
::stroke-color
|
||||||
|
::stroke-opacity
|
||||||
|
::stroke-style
|
||||||
|
::stroke-width
|
||||||
|
::text-align
|
||||||
|
::width ::height]))
|
||||||
|
|
||||||
|
(s/def ::minimal-shape
|
||||||
(s/keys :req-un [::type ::name]
|
(s/keys :req-un [::type ::name]
|
||||||
:opt-un [::id]))
|
:opt-un [::id]))
|
||||||
|
|
||||||
|
(s/def ::shape
|
||||||
|
(s/and ::minimal-shape ::shape-attrs
|
||||||
|
(s/keys :opt-un [::id])))
|
||||||
|
|
||||||
(s/def ::shapes (s/coll-of uuid? :kind vector?))
|
(s/def ::shapes (s/coll-of uuid? :kind vector?))
|
||||||
(s/def ::canvas (s/coll-of uuid? :kind vector?))
|
(s/def ::canvas (s/coll-of uuid? :kind vector?))
|
||||||
|
|
||||||
|
@ -36,12 +94,16 @@
|
||||||
(s/map-of uuid? ::shape))
|
(s/map-of uuid? ::shape))
|
||||||
|
|
||||||
(s/def ::data
|
(s/def ::data
|
||||||
(s/keys :req-un [::shapes ::canvas ::shapes-by-id]))
|
(s/keys :req-un [::shapes
|
||||||
|
::canvas
|
||||||
|
::options
|
||||||
|
::shapes-by-id]))
|
||||||
|
|
||||||
|
;; Changes related
|
||||||
(s/def ::attr-change
|
(s/def ::attr-change
|
||||||
(s/tuple #{:set} keyword? any?))
|
(s/tuple #{:set} keyword? any?))
|
||||||
|
|
||||||
(s/def ::operation
|
(s/def ::change
|
||||||
(s/or :mod-shape (s/cat :name #(= % :mod-shape)
|
(s/or :mod-shape (s/cat :name #(= % :mod-shape)
|
||||||
:id uuid?
|
:id uuid?
|
||||||
:changes (s/* ::attr-change))
|
:changes (s/* ::attr-change))
|
||||||
|
@ -64,12 +126,12 @@
|
||||||
:del-canvas (s/cat :name #(= % :del-canvas)
|
:del-canvas (s/cat :name #(= % :del-canvas)
|
||||||
:id uuid?)))
|
:id uuid?)))
|
||||||
|
|
||||||
(s/def ::operations
|
(s/def ::changes
|
||||||
(s/coll-of ::operation :kind vector?))
|
(s/coll-of ::change :kind vector?))
|
||||||
|
|
||||||
;; --- Operations Processing Impl
|
;; --- Changes Processing Impl
|
||||||
|
|
||||||
(declare process-operation)
|
(declare process-change)
|
||||||
(declare process-mod-shape)
|
(declare process-mod-shape)
|
||||||
(declare process-mod-opts)
|
(declare process-mod-opts)
|
||||||
(declare process-mov-shape)
|
(declare process-mov-shape)
|
||||||
|
@ -78,12 +140,12 @@
|
||||||
(declare process-del-shape)
|
(declare process-del-shape)
|
||||||
(declare process-del-canvas)
|
(declare process-del-canvas)
|
||||||
|
|
||||||
(defn process-ops
|
(defn process-changes
|
||||||
[data operations]
|
[data items]
|
||||||
(->> (s/assert ::operations operations)
|
(->> (s/assert ::changes items)
|
||||||
(reduce process-operation data)))
|
(reduce process-change data)))
|
||||||
|
|
||||||
(defn- process-operation
|
(defn- process-change
|
||||||
[data [op & rest]]
|
[data [op & rest]]
|
||||||
(case op
|
(case op
|
||||||
:mod-shape (process-mod-shape data rest)
|
:mod-shape (process-mod-shape data rest)
|
||||||
|
|
|
@ -471,26 +471,3 @@
|
||||||
(assoc :workspace-page page)
|
(assoc :workspace-page page)
|
||||||
(update :pages assoc id page)
|
(update :pages assoc id page)
|
||||||
(update :pages-data assoc id data))))))
|
(update :pages-data assoc id data))))))
|
||||||
|
|
||||||
;; --- Update Page
|
|
||||||
|
|
||||||
;; TODO: deprecated, need refactor (this is used on page options)
|
|
||||||
(defn update-page-attrs
|
|
||||||
[{:keys [id] :as data}]
|
|
||||||
(s/assert ::page data)
|
|
||||||
(ptk/reify ::update-page-attrs
|
|
||||||
ptk/UpdateEvent
|
|
||||||
(update [_ state]
|
|
||||||
(update state :workspace-page merge (dissoc data :id :version)))))
|
|
||||||
|
|
||||||
;; --- Update Page Metadata
|
|
||||||
|
|
||||||
;; TODO: deprecated, need refactor (this is used on page options)
|
|
||||||
(defn update-metadata
|
|
||||||
[id metadata]
|
|
||||||
(s/assert ::id id)
|
|
||||||
(s/assert ::metadata metadata)
|
|
||||||
(reify
|
|
||||||
ptk/UpdateEvent
|
|
||||||
(update [this state]
|
|
||||||
(assoc-in state [:pages id :metadata] metadata))))
|
|
||||||
|
|
|
@ -34,89 +34,24 @@
|
||||||
[uxbox.util.uuid :as uuid]
|
[uxbox.util.uuid :as uuid]
|
||||||
[vendor.randomcolor]))
|
[vendor.randomcolor]))
|
||||||
|
|
||||||
|
|
||||||
;; TODO: temporal workaround
|
;; TODO: temporal workaround
|
||||||
(def clear-ruler nil)
|
(def clear-ruler nil)
|
||||||
(def start-ruler nil)
|
(def start-ruler nil)
|
||||||
|
|
||||||
;; --- Specs
|
;; --- Specs
|
||||||
|
|
||||||
(s/def ::id ::us/uuid)
|
(s/def ::shape-attrs ::cp/shape-attrs)
|
||||||
(s/def ::blocked boolean?)
|
|
||||||
(s/def ::collapsed boolean?)
|
|
||||||
(s/def ::content string?)
|
|
||||||
(s/def ::fill-color string?)
|
|
||||||
(s/def ::fill-opacity number?)
|
|
||||||
(s/def ::font-family string?)
|
|
||||||
(s/def ::font-size number?)
|
|
||||||
(s/def ::font-style string?)
|
|
||||||
(s/def ::font-weight string?)
|
|
||||||
(s/def ::hidden boolean?)
|
|
||||||
(s/def ::id uuid?)
|
|
||||||
(s/def ::letter-spacing number?)
|
|
||||||
(s/def ::line-height number?)
|
|
||||||
(s/def ::locked boolean?)
|
|
||||||
(s/def ::name string?)
|
|
||||||
(s/def ::page uuid?)
|
|
||||||
(s/def ::proportion number?)
|
|
||||||
(s/def ::proportion-lock boolean?)
|
|
||||||
(s/def ::rx number?)
|
|
||||||
(s/def ::ry number?)
|
|
||||||
(s/def ::stroke-color string?)
|
|
||||||
(s/def ::stroke-opacity number?)
|
|
||||||
(s/def ::stroke-style #{:none :solid :dotted :dashed :mixed})
|
|
||||||
(s/def ::stroke-width number?)
|
|
||||||
(s/def ::text-align #{"left" "right" "center" "justify"})
|
|
||||||
(s/def ::type #{:rect :path :circle :image :text :canvas})
|
|
||||||
(s/def ::x number?)
|
|
||||||
(s/def ::y number?)
|
|
||||||
(s/def ::cx number?)
|
|
||||||
(s/def ::cy number?)
|
|
||||||
(s/def ::width number?)
|
|
||||||
(s/def ::height number?)
|
|
||||||
|
|
||||||
(s/def ::attributes
|
|
||||||
(s/keys :opt-un [::blocked
|
|
||||||
::collapsed
|
|
||||||
::content
|
|
||||||
::fill-color
|
|
||||||
::fill-opacity
|
|
||||||
::font-family
|
|
||||||
::font-size
|
|
||||||
::font-style
|
|
||||||
::font-weight
|
|
||||||
::hidden
|
|
||||||
::letter-spacing
|
|
||||||
::line-height
|
|
||||||
::locked
|
|
||||||
::proportion
|
|
||||||
::proportion-lock
|
|
||||||
::rx ::ry
|
|
||||||
::cx ::cy
|
|
||||||
::x ::y
|
|
||||||
::stroke-color
|
|
||||||
::stroke-opacity
|
|
||||||
::stroke-style
|
|
||||||
::stroke-width
|
|
||||||
::text-align
|
|
||||||
::width ::height]))
|
|
||||||
|
|
||||||
(s/def ::minimal-shape
|
|
||||||
(s/keys :req-un [::id ::page ::type ::name]))
|
|
||||||
|
|
||||||
(s/def ::shape
|
|
||||||
(s/and ::minimal-shape ::attributes))
|
|
||||||
|
|
||||||
(s/def ::rect-like-shape
|
|
||||||
(s/keys :req-un [::x1 ::y1 ::x2 ::y2 ::type]))
|
|
||||||
|
|
||||||
(s/def ::set-of-uuid
|
(s/def ::set-of-uuid
|
||||||
(s/every ::us/uuid :kind set?))
|
(s/every uuid? :kind set?))
|
||||||
|
|
||||||
;; --- Expose inner functions
|
;; --- Expose inner functions
|
||||||
|
|
||||||
(defn interrupt? [e] (= e :interrupt))
|
(defn interrupt? [e] (= e :interrupt))
|
||||||
|
|
||||||
|
;; --- Protocols
|
||||||
|
|
||||||
|
(defprotocol IAsyncChange)
|
||||||
|
|
||||||
;; --- Declarations
|
;; --- Declarations
|
||||||
|
|
||||||
(declare fetch-users)
|
(declare fetch-users)
|
||||||
|
@ -125,8 +60,8 @@
|
||||||
(declare handle-pointer-send)
|
(declare handle-pointer-send)
|
||||||
(declare handle-page-snapshot)
|
(declare handle-page-snapshot)
|
||||||
(declare shapes-changes-commited)
|
(declare shapes-changes-commited)
|
||||||
(declare commit-shapes-changes)
|
(declare commit-changes)
|
||||||
(declare async-commit-shapes-changes)
|
(declare commit-async-changes)
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Websockets Events
|
;; Websockets Events
|
||||||
|
@ -320,19 +255,14 @@
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [stoper (rx/filter #(or (ptk/type? ::finalize %)
|
(let [stoper (rx/filter #(or (ptk/type? ::finalize %)
|
||||||
(ptk/type? ::initialize-page %))
|
(ptk/type? ::initialize-page %))
|
||||||
stream)
|
stream)]
|
||||||
notifier (->> stream
|
|
||||||
(rx/filter (ptk/type? ::async-commit-shapes-changes))
|
|
||||||
(rx/debounce 500))]
|
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::async-commit-shapes-changes))
|
(rx/filter #(satisfies? IAsyncChange %))
|
||||||
(rx/map deref)
|
(rx/debounce 500)
|
||||||
(rx/mapcat identity)
|
(rx/map (constantly commit-async-changes))
|
||||||
(rx/buffer-until notifier)
|
(rx/finalize #(prn "FINALIZE" %))
|
||||||
(rx/map vec)
|
(rx/take-until stoper))))))
|
||||||
(rx/map commit-shapes-changes)
|
|
||||||
(rx/take-until stoper)
|
|
||||||
(rx/finalize #(prn "FINALIZE" %)))))))
|
|
||||||
|
|
||||||
;; --- Fetch Workspace Users
|
;; --- Fetch Workspace Users
|
||||||
|
|
||||||
|
@ -612,7 +542,7 @@
|
||||||
|
|
||||||
(defn add-shape
|
(defn add-shape
|
||||||
[data]
|
[data]
|
||||||
(s/assert ::attributes data)
|
(s/assert ::shape-attrs data)
|
||||||
(let [id (uuid/random)]
|
(let [id (uuid/random)]
|
||||||
(ptk/reify ::add-shape
|
(ptk/reify ::add-shape
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
|
@ -626,7 +556,7 @@
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [shape (get-in state [:workspace-data :shapes-by-id id])]
|
(let [shape (get-in state [:workspace-data :shapes-by-id id])]
|
||||||
(rx/of (commit-shapes-changes [[:add-shape id shape]])
|
(rx/of (commit-changes [[:add-shape id shape]])
|
||||||
(select-shape id)))))))
|
(select-shape id)))))))
|
||||||
|
|
||||||
(def canvas-default-attrs
|
(def canvas-default-attrs
|
||||||
|
@ -637,7 +567,7 @@
|
||||||
|
|
||||||
(defn add-canvas
|
(defn add-canvas
|
||||||
[data]
|
[data]
|
||||||
(s/assert ::attributes data)
|
(s/assert ::shape-attrs data)
|
||||||
(let [id (uuid/random)]
|
(let [id (uuid/random)]
|
||||||
(ptk/reify ::add-canvas
|
(ptk/reify ::add-canvas
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
|
@ -651,7 +581,7 @@
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [shape (get-in state [:workspace-data :shapes-by-id id])]
|
(let [shape (get-in state [:workspace-data :shapes-by-id id])]
|
||||||
(rx/of (commit-shapes-changes [[:add-canvas id shape]])
|
(rx/of (commit-changes [[:add-canvas id shape]])
|
||||||
(select-shape id)))))))
|
(select-shape id)))))))
|
||||||
|
|
||||||
|
|
||||||
|
@ -671,7 +601,7 @@
|
||||||
shapes (map duplicate selected)]
|
shapes (map duplicate selected)]
|
||||||
(rx/merge
|
(rx/merge
|
||||||
(rx/from (map (fn [s] #(impl-assoc-shape % s)) shapes))
|
(rx/from (map (fn [s] #(impl-assoc-shape % s)) shapes))
|
||||||
(rx/of (commit-shapes-changes (mapv #(vector :add-shape (:id %) %) shapes))))))))
|
(rx/of (commit-changes (mapv #(vector :add-shape (:id %) %) shapes))))))))
|
||||||
|
|
||||||
;; --- Toggle shape's selection status (selected or deselected)
|
;; --- Toggle shape's selection status (selected or deselected)
|
||||||
|
|
||||||
|
@ -740,8 +670,9 @@
|
||||||
(defn update-shape
|
(defn update-shape
|
||||||
[id attrs]
|
[id attrs]
|
||||||
(s/assert ::us/uuid id)
|
(s/assert ::us/uuid id)
|
||||||
(s/assert ::attributes attrs)
|
(s/assert ::shape-attrs attrs)
|
||||||
(ptk/reify ::update-shape
|
(ptk/reify ::update-shape
|
||||||
|
IAsyncChange
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [shape-old (get-in state [:workspace-data :shapes-by-id id])
|
(let [shape-old (get-in state [:workspace-data :shapes-by-id id])
|
||||||
|
@ -749,13 +680,23 @@
|
||||||
diff (d/diff-maps shape-old shape-new)]
|
diff (d/diff-maps shape-old shape-new)]
|
||||||
(-> state
|
(-> state
|
||||||
(assoc-in [:workspace-data :shapes-by-id id] shape-new)
|
(assoc-in [:workspace-data :shapes-by-id id] shape-new)
|
||||||
(assoc ::tmp-change (into [:mod-shape id] diff)))))
|
(update ::async-changes (fnil conj []) (into [:mod-shape id] diff)))))))
|
||||||
|
|
||||||
ptk/WatchEvent
|
;; --- Update Page Options
|
||||||
(watch [_ state stream]
|
|
||||||
(let [change (::tmp-change state)]
|
(defn update-options
|
||||||
(rx/of (async-commit-shapes-changes [change])
|
[opts]
|
||||||
#(dissoc state ::tmp-change))))))
|
(s/assert ::cp/options opts)
|
||||||
|
(ptk/reify ::update-options
|
||||||
|
IAsyncChange
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [opts-old (get-in state [:workspace-data :options])
|
||||||
|
opts-new (merge opts-old opts)
|
||||||
|
diff (d/diff-maps opts-old opts-new)]
|
||||||
|
(-> state
|
||||||
|
(assoc-in [:workspace-data :options] opts-new)
|
||||||
|
(update ::async-changes (fnil conj []) (into [:mod-opts] diff)))))))
|
||||||
|
|
||||||
;; --- Update Selected Shapes attrs
|
;; --- Update Selected Shapes attrs
|
||||||
|
|
||||||
|
@ -844,7 +785,7 @@
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [selected (get-in state [:workspace-local :selected])]
|
(let [selected (get-in state [:workspace-local :selected])]
|
||||||
(rx/of (commit-shapes-changes (mapv #(vector :del-shape %) selected)))))))
|
(rx/of (commit-changes (mapv #(vector :del-shape %) selected)))))))
|
||||||
|
|
||||||
;; --- Rename Shape
|
;; --- Rename Shape
|
||||||
|
|
||||||
|
@ -858,7 +799,7 @@
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(rx/of (commit-shapes-changes [[:mod-shape id [:mod :name name]]])))))
|
(rx/of (commit-changes [[:mod-shape id [:mod :name name]]])))))
|
||||||
|
|
||||||
;; --- Shape Vertical Ordering
|
;; --- Shape Vertical Ordering
|
||||||
|
|
||||||
|
@ -897,7 +838,6 @@
|
||||||
[id index]
|
[id index]
|
||||||
(s/assert ::us/uuid id)
|
(s/assert ::us/uuid id)
|
||||||
(s/assert number? index)
|
(s/assert number? index)
|
||||||
{:pre [(uuid? id) (number? index)]}
|
|
||||||
(ptk/reify ::change-shape-order
|
(ptk/reify ::change-shape-order
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
|
@ -905,18 +845,18 @@
|
||||||
shapes (into [] (remove #(= % id)) shapes)
|
shapes (into [] (remove #(= % id)) shapes)
|
||||||
[before after] (split-at index shapes)
|
[before after] (split-at index shapes)
|
||||||
shapes (d/concat [] before [id] after)
|
shapes (d/concat [] before [id] after)
|
||||||
operation [:mov-shape id :after (last before)]]
|
change [:mov-shape id :after (last before)]]
|
||||||
(-> state
|
(-> state
|
||||||
(assoc-in [:workspace-data :shapes] shapes)
|
(assoc-in [:workspace-data :shapes] shapes)
|
||||||
(assoc ::tmp-changes [operation]))))))
|
(assoc ::tmp-shape-order-change change))))))
|
||||||
|
|
||||||
(def commit-shape-order-change
|
(def commit-shape-order-change
|
||||||
(ptk/reify ::commit-shape-order-change
|
(ptk/reify ::commit-shape-order-change
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [changes (::tmp-changes state)]
|
(let [change (::tmp-shape-order-change state)]
|
||||||
(rx/of (commit-shapes-changes changes)
|
(rx/of #(dissoc state ::tmp-changes)
|
||||||
#(dissoc state ::tmp-changes))))))
|
(commit-changes [change]))))))
|
||||||
|
|
||||||
;; --- Change Canvas Order (D&D Ordering)
|
;; --- Change Canvas Order (D&D Ordering)
|
||||||
|
|
||||||
|
@ -1001,50 +941,46 @@
|
||||||
diff (d/diff-maps shape-old shape-new)]
|
diff (d/diff-maps shape-old shape-new)]
|
||||||
(-> state
|
(-> state
|
||||||
(assoc-in [:workspace-data :shapes-by-id id] shape-new)
|
(assoc-in [:workspace-data :shapes-by-id id] shape-new)
|
||||||
(update ::tmp-changes (fnil conj []) (into [:mod-shape id] diff)))))]
|
(update ::async-changes (fnil conj []) (into [:mod-shape id] diff)))))]
|
||||||
|
|
||||||
(ptk/reify ::materialize-temporal-modifier-in-bulk
|
(ptk/reify ::materialize-temporal-modifier-in-bulk
|
||||||
|
IAsyncChange
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(reduce process-shape state ids))
|
(reduce process-shape state ids)))))
|
||||||
|
|
||||||
ptk/WatchEvent
|
(defn commit-changes
|
||||||
(watch [_ state stream]
|
[changes]
|
||||||
(let [changes (::tmp-changes state)]
|
(s/assert ::cp/changes changes)
|
||||||
(rx/of (commit-shapes-changes changes)
|
(ptk/reify ::commit-changes
|
||||||
#(dissoc state ::tmp-changes)))))))
|
|
||||||
|
|
||||||
(defn commit-shapes-changes
|
|
||||||
[operations]
|
|
||||||
(s/assert ::cp/operations operations)
|
|
||||||
(ptk/reify ::commit-shapes-changes
|
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [pid (get-in state [:workspace-local :page-id])
|
(let [pid (get-in state [:workspace-local :page-id])
|
||||||
data (get-in state [:pages-data pid])]
|
data (get-in state [:pages-data pid])]
|
||||||
(update-in state [:pages-data pid] cp/process-ops operations)))
|
(update-in state [:pages-data pid] cp/process-changes changes)))
|
||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state stream]
|
(watch [_ state stream]
|
||||||
(let [page (:workspace-page state)
|
(let [page (:workspace-page state)
|
||||||
params {:id (:id page)
|
params {:id (:id page)
|
||||||
:version (:version page)
|
:version (:version page)
|
||||||
:operations operations}]
|
:changes changes}]
|
||||||
(->> (rp/mutation :update-project-page params)
|
(->> (rp/mutation :update-project-page params)
|
||||||
(rx/map shapes-changes-commited))))))
|
(rx/map shapes-changes-commited))))))
|
||||||
|
|
||||||
(defn async-commit-shapes-changes
|
(def commit-async-changes
|
||||||
[operations]
|
(ptk/reify ::commit-async-changes
|
||||||
(s/assert ::cp/operations operations)
|
ptk/WatchEvent
|
||||||
(ptk/reify ::async-commit-shapes-changes
|
(watch [_ state stream]
|
||||||
cljs.core/IDeref
|
(let [changes (::async-changes state)]
|
||||||
(-deref [_] operations)))
|
(rx/of #(dissoc % ::async-changes)
|
||||||
|
(commit-changes changes))))))
|
||||||
|
|
||||||
(s/def ::shapes-changes-commited
|
(s/def ::shapes-changes-commited
|
||||||
(s/keys :req-un [::page-id ::version ::cp/operations]))
|
(s/keys :req-un [::page-id ::version ::cp/changes]))
|
||||||
|
|
||||||
(defn shapes-changes-commited
|
(defn shapes-changes-commited
|
||||||
[{:keys [page-id version operations] :as params}]
|
[{:keys [page-id version changes] :as params}]
|
||||||
(s/assert ::shapes-changes-commited params)
|
(s/assert ::shapes-changes-commited params)
|
||||||
(ptk/reify ::shapes-changes-commited
|
(ptk/reify ::shapes-changes-commited
|
||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
|
@ -1052,8 +988,8 @@
|
||||||
(-> state
|
(-> state
|
||||||
(assoc-in [:workspace-page :version] version)
|
(assoc-in [:workspace-page :version] version)
|
||||||
(assoc-in [:pages page-id :version] version)
|
(assoc-in [:pages page-id :version] version)
|
||||||
(update-in [:pages-data page-id] cp/process-ops operations)
|
(update-in [:pages-data page-id] cp/process-changes changes)
|
||||||
(update :workspace-data cp/process-ops operations)))))
|
(update :workspace-data cp/process-changes changes)))))
|
||||||
|
|
||||||
;; --- Start shape "edition mode"
|
;; --- Start shape "edition mode"
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
(:require
|
(:require
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[rumext.alpha :as mf]
|
[rumext.alpha :as mf]
|
||||||
|
[lentes.core :as l]
|
||||||
|
[uxbox.common.data :as d]
|
||||||
[uxbox.builtins.icons :as i]
|
[uxbox.builtins.icons :as i]
|
||||||
[uxbox.main.constants :as c]
|
[uxbox.main.constants :as c]
|
||||||
[uxbox.main.data.workspace :as udw]
|
[uxbox.main.data.workspace :as udw]
|
||||||
|
@ -22,65 +24,83 @@
|
||||||
[uxbox.util.i18n :refer [tr]]
|
[uxbox.util.i18n :refer [tr]]
|
||||||
[uxbox.util.spec :refer [color?]]))
|
[uxbox.util.spec :refer [color?]]))
|
||||||
|
|
||||||
(mf/defc metadata-options
|
;; (mf/defc metadata-options
|
||||||
[{:keys [page] :as props}]
|
;; [{:keys [page] :as props}]
|
||||||
(let [metadata (:metadata page)
|
;; (let [metadata (:metadata page)
|
||||||
|
;; change-color
|
||||||
|
;; (fn [color]
|
||||||
|
;; #_(st/emit! (->> (assoc metadata :background color)
|
||||||
|
;; (udp/update-metadata (:id page)))))
|
||||||
|
;; on-color-change
|
||||||
|
;; (fn [event]
|
||||||
|
;; (let [value (dom/event->value event)]
|
||||||
|
;; (change-color value)))
|
||||||
|
|
||||||
|
;; show-color-picker
|
||||||
|
;; (fn [event]
|
||||||
|
;; (let [x (.-clientX event)
|
||||||
|
;; y (.-clientY event)
|
||||||
|
;; props {:x x :y y
|
||||||
|
;; :default "#ffffff"
|
||||||
|
;; :value (:background metadata)
|
||||||
|
;; :transparent? true
|
||||||
|
;; :on-change change-color}]
|
||||||
|
;; (modal/show! colorpicker-modal props)))]
|
||||||
|
|
||||||
|
;; [:div.element-set
|
||||||
|
;; [:div.element-set-title (tr "workspace.options.page-measures")]
|
||||||
|
;; [:div.element-set-content
|
||||||
|
;; [:span (tr "workspace.options.background-color")]
|
||||||
|
;; [:div.row-flex.color-data
|
||||||
|
;; [:span.color-th
|
||||||
|
;; {:style {:background-color (:background metadata "#ffffff")}
|
||||||
|
;; :on-click show-color-picker}]
|
||||||
|
;; [:div.color-info
|
||||||
|
;; [:input
|
||||||
|
;; {:on-change on-color-change
|
||||||
|
;; :value (:background metadata "#ffffff")}]]]]]))
|
||||||
|
|
||||||
|
(def default-options
|
||||||
|
"Default data for page metadata."
|
||||||
|
{:grid-x 10
|
||||||
|
:grid-y 10
|
||||||
|
:grid-color "#cccccc"})
|
||||||
|
|
||||||
|
(def options-iref
|
||||||
|
(-> (l/key :options)
|
||||||
|
(l/derive refs/workspace-data)))
|
||||||
|
|
||||||
|
(mf/defc grid-options
|
||||||
|
{:wrap [mf/wrap-memo]}
|
||||||
|
[props]
|
||||||
|
(let [options (->> (mf/deref options-iref)
|
||||||
|
(merge default-options))
|
||||||
|
on-x-change
|
||||||
|
(fn [event]
|
||||||
|
(let [value (-> (dom/get-target event)
|
||||||
|
(dom/get-value)
|
||||||
|
(d/parse-integer 0))]
|
||||||
|
(st/emit! (udw/update-options {:grid-x value}))))
|
||||||
|
|
||||||
|
on-y-change
|
||||||
|
(fn [event]
|
||||||
|
(let [value (-> (dom/get-target event)
|
||||||
|
(dom/get-value)
|
||||||
|
(d/parse-integer 0))]
|
||||||
|
(st/emit! (udw/update-options {:grid-y value}))))
|
||||||
|
|
||||||
change-color
|
change-color
|
||||||
(fn [color]
|
(fn [color]
|
||||||
#_(st/emit! (->> (assoc metadata :background color)
|
(st/emit! (udw/update-options {:grid-color color})))
|
||||||
(udp/update-metadata (:id page)))))
|
|
||||||
on-color-change
|
on-color-change
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [value (dom/event->value event)]
|
(let [value (-> (dom/get-target event)
|
||||||
|
(dom/get-value))]
|
||||||
(change-color value)))
|
(change-color value)))
|
||||||
|
|
||||||
show-color-picker
|
show-color-picker
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [x (.-clientX event)
|
|
||||||
y (.-clientY event)
|
|
||||||
props {:x x :y y
|
|
||||||
:default "#ffffff"
|
|
||||||
:value (:background metadata)
|
|
||||||
:transparent? true
|
|
||||||
:on-change change-color}]
|
|
||||||
(modal/show! colorpicker-modal props)))]
|
|
||||||
|
|
||||||
[:div.element-set
|
|
||||||
[:div.element-set-title (tr "workspace.options.page-measures")]
|
|
||||||
[:div.element-set-content
|
|
||||||
[:span (tr "workspace.options.background-color")]
|
|
||||||
[:div.row-flex.color-data
|
|
||||||
[:span.color-th
|
|
||||||
{:style {:background-color (:background metadata "#ffffff")}
|
|
||||||
:on-click show-color-picker}]
|
|
||||||
[:div.color-info
|
|
||||||
[:input
|
|
||||||
{:on-change on-color-change
|
|
||||||
:value (:background metadata "#ffffff")}]]]]]))
|
|
||||||
|
|
||||||
(mf/defc grid-options
|
|
||||||
[{:keys [page] :as props}]
|
|
||||||
(let [metadata (:metadata page)
|
|
||||||
metadata (merge c/page-metadata metadata)]
|
|
||||||
(letfn [(on-x-change [event]
|
|
||||||
#_(let [value (-> (dom/event->value event)
|
|
||||||
(parse-int nil))]
|
|
||||||
(st/emit! (->> (assoc metadata :grid-x-axis value)
|
|
||||||
(udp/update-metadata (:id page))))))
|
|
||||||
(on-y-change [event]
|
|
||||||
#_(let [value (-> (dom/event->value event)
|
|
||||||
(parse-int nil))]
|
|
||||||
(st/emit! (->> (assoc metadata :grid-y-axis value)
|
|
||||||
(udp/update-metadata (:id page))))))
|
|
||||||
|
|
||||||
(change-color [color]
|
|
||||||
#_(st/emit! (->> (assoc metadata :grid-color color)
|
|
||||||
(udp/update-metadata (:id page)))))
|
|
||||||
(on-color-change [event]
|
|
||||||
(let [value (dom/event->value event)]
|
|
||||||
(change-color value)))
|
|
||||||
|
|
||||||
(show-color-picker [event]
|
|
||||||
(let [x (.-clientX event)
|
(let [x (.-clientX event)
|
||||||
y (.-clientY event)
|
y (.-clientY event)
|
||||||
props {:x x :y y
|
props {:x x :y y
|
||||||
|
@ -95,30 +115,23 @@
|
||||||
[:span (tr "workspace.options.size")]
|
[:span (tr "workspace.options.size")]
|
||||||
[:div.row-flex
|
[:div.row-flex
|
||||||
[:div.input-element.pixels
|
[:div.input-element.pixels
|
||||||
[:input.input-text
|
[:input.input-text {:type "number"
|
||||||
{:type "number"
|
:value (:grid-x options)
|
||||||
:value (:grid-x-axis metadata)
|
:on-change on-x-change}]]
|
||||||
:on-change on-x-change
|
|
||||||
:placeholder "x"}]]
|
|
||||||
[:div.input-element.pixels
|
[:div.input-element.pixels
|
||||||
[:input.input-text
|
[:input.input-text {:type "number"
|
||||||
{:type "number"
|
:value (:grid-y options)
|
||||||
:value (:grid-y-axis metadata)
|
:on-change on-y-change}]]]
|
||||||
:on-change on-y-change
|
|
||||||
:placeholder "y"}]]]
|
|
||||||
[:span (tr "workspace.options.color")]
|
[:span (tr "workspace.options.color")]
|
||||||
[:div.row-flex.color-data
|
[:div.row-flex.color-data
|
||||||
[:span.color-th
|
[:span.color-th {:style {:background-color (:grid-color options)}
|
||||||
{:style {:background-color (:grid-color metadata)}
|
|
||||||
:on-click show-color-picker}]
|
:on-click show-color-picker}]
|
||||||
[:div.color-info
|
[:div.color-info
|
||||||
[:input
|
[:input {:on-change on-color-change
|
||||||
{:on-change on-color-change
|
:value (:grid-color options)}]]]]]))
|
||||||
:value (:grid-color metadata "#cccccc")}]]]]])))
|
|
||||||
|
|
||||||
(mf/defc options
|
(mf/defc options
|
||||||
[{:keys [page] :as props}]
|
[{:keys [page] :as props}]
|
||||||
[:div
|
[:div
|
||||||
#_[:& metadata-options {:page page}]
|
|
||||||
[:& grid-options {:page page}]])
|
[:& grid-options {:page page}]])
|
||||||
|
|
||||||
|
|
|
@ -150,7 +150,7 @@
|
||||||
shapes-by-id (:shapes-by-id data)
|
shapes-by-id (:shapes-by-id data)
|
||||||
shapes (map #(get shapes-by-id %) (:shapes data []))
|
shapes (map #(get shapes-by-id %) (:shapes data []))
|
||||||
canvas (map #(get shapes-by-id %) (:canvas data []))]
|
canvas (map #(get shapes-by-id %) (:canvas data []))]
|
||||||
[:*
|
[:g.shapes
|
||||||
(for [item canvas]
|
(for [item canvas]
|
||||||
[:& shape-wrapper {:shape item :key (:id item)}])
|
[:& shape-wrapper {:shape item :key (:id item)}])
|
||||||
(for [item shapes]
|
(for [item shapes]
|
||||||
|
@ -285,7 +285,7 @@
|
||||||
:modifiers (:modifiers local)}])]
|
:modifiers (:modifiers local)}])]
|
||||||
|
|
||||||
(if (contains? flags :grid)
|
(if (contains? flags :grid)
|
||||||
[:& grid {:page page}])]
|
[:& grid])]
|
||||||
|
|
||||||
(when (contains? flags :ruler)
|
(when (contains? flags :ruler)
|
||||||
[:& ruler {:zoom zoom :ruler (:ruler local)}])
|
[:& ruler {:zoom zoom :ruler (:ruler local)}])
|
||||||
|
|
Loading…
Add table
Reference in a new issue