♻️ Refactor file persistence layer.

This commit is contained in:
Andrey Antukh 2020-09-07 10:56:42 +02:00 committed by Alonso Torres
parent 182afedc54
commit 4e694ff194
86 changed files with 3205 additions and 3313 deletions

View file

@ -201,26 +201,17 @@
;; --- Fetch Files
(declare files-fetched)
(defn fetch-files
[project-id]
(ptk/reify ::fetch-files
ptk/WatchEvent
(watch [_ state stream]
(let [params {:project-id project-id}]
(->> (rp/query :files params)
(rx/map files-fetched))))))
(defn files-fetched
[files]
(us/verify (s/every ::file) files)
(ptk/reify ::files-fetched
ptk/UpdateEvent
(update [_ state]
(let [state (dissoc state :files)
files (d/index-by :id files)]
(assoc state :files files)))))
(us/assert ::us/uuid project-id)
(letfn [(on-fetched [files state]
(assoc state :files (d/index-by :id files)))]
(ptk/reify ::fetch-files
ptk/WatchEvent
(watch [_ state stream]
(let [params {:project-id project-id}]
(->> (rp/query :files params)
(rx/map #(partial on-fetched %))))))))
;; --- Fetch Shared Files
@ -241,14 +232,13 @@
(defn fetch-recent-files
[team-id]
(us/assert ::us/uuid team-id)
(ptk/reify ::fetch-recent-files
ptk/WatchEvent
(watch [_ state stream]
(let [params {:team-id team-id}]
(->> (rp/query :recent-files params)
(rx/map recent-files-fetched)
(rx/catch (fn [e]
(rx/of (rt/nav' :auth-login)))))))))
(rx/map recent-files-fetched))))))
(defn recent-files-fetched
[recent-files]
@ -415,9 +405,10 @@
ptk/WatchEvent
(watch [_ state stream]
(rx/of (rt/nav :workspace {:project-id (:project-id data)
:file-id (:id data)}
{:page-id (first (:pages data))})))))
(let [pparams {:project-id (:project-id data)
:file-id (:id data)}
qparams {:page-id (get-in data [:data :pages 0])}]
(rx/of (rt/nav :workspace pparams qparams))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -29,7 +29,7 @@
(s/def ::project (s/keys ::req-un [::id ::name]))
(s/def ::file (s/keys :req-un [::id ::name]))
(s/def ::page (s/keys :req-un [::id ::name ::cp/data]))
(s/def ::page ::cp/page)
(s/def ::interactions-mode #{:hide :show :show-on-click})
@ -43,37 +43,38 @@
(declare bundle-fetched)
(defn initialize
[page-id share-token]
[{:keys [page-id file-id] :as params}]
(ptk/reify ::initialize
ptk/UpdateEvent
(update [_ state]
(assoc state :viewer-local {:zoom 1
:page-id page-id
:file-id file-id
:interactions-mode :hide
:show-interactions? false}))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (fetch-bundle page-id share-token)))))
(rx/of (fetch-bundle params)))))
;; --- Data Fetching
(defn fetch-bundle
[page-id share-token]
[{:keys [page-id file-id token]}]
(ptk/reify ::fetch-file
ptk/WatchEvent
(watch [_ state stream]
(let [params (cond-> {:page-id page-id}
(string? share-token) (assoc :share-token share-token))]
(let [params (cond-> {:page-id page-id
:file-id file-id}
(string? token) (assoc :share-token token))]
(->> (rp/query :viewer-bundle params)
(rx/map bundle-fetched)
(rx/catch (fn [error-data]
#_(rx/catch (fn [error-data]
(rx/of (rt/nav :not-found)))))))))
(defn- extract-frames
[page]
(let [objects (get-in page [:data :objects])
root (get objects uuid/zero)]
[objects]
(let [root (get objects uuid/zero)]
(->> (:shapes root)
(map #(get objects %))
(filter #(= :frame (:type %)))
@ -81,37 +82,41 @@
(vec))))
(defn bundle-fetched
[{:keys [project file page images] :as bundle}]
[{:keys [project file page] :as bundle}]
(us/verify ::bundle bundle)
(ptk/reify ::file-fetched
ptk/UpdateEvent
(update [_ state]
(let [frames (extract-frames page)
objects (get-in page [:data :objects])]
(let [objects (:objects page)
frames (extract-frames objects)]
(assoc state :viewer-data {:project project
:objects objects
:file file
:page page
:images images
:frames frames})))))
(def create-share-link
(ptk/reify ::create-share-link
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-in state [:viewer-local :page-id])]
(->> (rp/mutation :generate-page-share-token {:id id})
(rx/map (fn [{:keys [share-token]}]
#(assoc-in % [:viewer-data :page :share-token] share-token))))))))
(let [file-id (get-in state [:viewer-local :file-id])
page-id (get-in state [:viewer-local :page-id])]
(->> (rp/mutation :create-file-share-token {:file-id file-id
:page-id page-id})
(rx/map (fn [{:keys [token]}]
#(assoc-in % [:viewer-data :share-token] token))))))))
(def delete-share-link
(ptk/reify ::delete-share-link
ptk/WatchEvent
(watch [_ state stream]
(let [id (get-in state [:viewer-local :page-id])]
(->> (rp/mutation :clear-page-share-token {:id id})
(rx/map (fn [_]
#(assoc-in % [:viewer-data :page :share-token] nil))))))))
(let [file-id (get-in state [:viewer-local :file-id])
page-id (get-in state [:viewer-local :page-id])
token (get-in state [:viewer-data :share-token])]
(->> (rp/mutation :delete-file-share-token {:file-id file-id
:page-id page-id
:token token})
(rx/map (fn [_] #(update % :viewer-data dissoc :share-token))))))))
;; --- Zoom Management
@ -226,9 +231,10 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (get-in state [:viewer-local :page-id])
frames (get-in state [:viewer-data :frames])
index (d/index-of-pred frames #(= (:id %) frame-id))]
(rx/of (rt/nav :viewer {:page-id page-id} {:index index}))))))
file-id (get-in state [:viewer-local :file-id])
frames (get-in state [:viewer-data :frames])
index (d/index-of-pred frames #(= (:id %) frame-id))]
(rx/of (rt/nav :viewer {:page-id page-id :file-id file-id} {:index index}))))))
;; --- Shortcuts

View file

@ -9,12 +9,12 @@
(ns app.main.data.workspace
(:require
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[clojure.set :as set]
[potok.core :as ptk]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.common.math :as mth]
[app.common.pages :as cp]
[app.common.pages-helpers :as cph]
[app.common.spec :as us]
@ -24,21 +24,21 @@
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.persistence :as dwp]
[app.main.data.workspace.selection :as dws]
[app.main.data.workspace.texts :as dwtxt]
[app.main.data.workspace.transforms :as dwt]
[app.main.data.workspace.selection :as dws]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.streams :as ms]
[app.main.worker :as uw]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.common.math :as mth]
[app.util.timers :as ts]
[app.util.router :as rt]
[app.util.timers :as ts]
[app.util.transit :as t]
[app.util.webapi :as wapi]))
[app.util.webapi :as wapi]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[clojure.set :as set]
[potok.core :as ptk]))
;; --- Specs
@ -85,13 +85,16 @@
(s/def ::options-mode #{:design :prototype})
(def workspace-file-local-default
{:left-sidebar? true
:right-sidebar? true
:color-for-rename nil})
(def workspace-local-default
{:zoom 1
:flags #{}
:selected (d/ordered-set)
:expanded {}
:drawing nil
:drawing-tool nil
:tooltip nil
:options-mode :design
:draw-interaction-to nil
@ -113,32 +116,41 @@
(ptk/reify ::initialize-file
ptk/UpdateEvent
(update [_ state]
(assoc state :workspace-presence {}))
(assoc state
:workspace-presence {}
:workspace-file-local workspace-file-local-default))
ptk/WatchEvent
(watch [_ state stream]
(rx/merge
(rx/of (dwp/fetch-bundle project-id file-id))
;; Initialize notifications (websocket connection) and the file persistence
(->> stream
(rx/filter (ptk/type? ::dwp/bundle-fetched))
(rx/mapcat (fn [_] (rx/of (dwn/initialize file-id))))
(rx/first))
(rx/first)
(rx/mapcat #(rx/of (dwn/initialize file-id)
(dwp/initialize-file-persistence file-id))))
;; Initialize Indexes (webworker)
(->> stream
(rx/filter (ptk/type? ::dwp/bundle-fetched))
(rx/map deref)
(rx/map dwc/setup-selection-index)
(rx/map dwc/initialize-indices)
(rx/first))
;; Mark file initialized when indexes are ready
(->> stream
(rx/filter #(= ::dwc/index-initialized %))
(rx/map (constantly
(file-initialized project-id file-id))))))))
(file-initialized project-id file-id))))
))))
(defn- file-initialized
[project-id file-id]
(ptk/reify ::initialized
(ptk/reify ::file-initialized
ptk/UpdateEvent
(update [_ state]
(update state :workspace-file
@ -152,11 +164,12 @@
(ptk/reify ::finalize
ptk/UpdateEvent
(update [_ state]
(dissoc state :workspace-file :workspace-project))
(dissoc state :workspace-file :workspace-project :workspace-media-objects :workspace-users))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (dwn/finalize file-id)))))
(rx/of (dwn/finalize file-id)
::dwp/finalize))))
(defn initialize-page
@ -164,17 +177,14 @@
(ptk/reify ::initialize-page
ptk/UpdateEvent
(update [_ state]
(let [page (get-in state [:workspace-pages page-id])
;; TODO: looks workspace-page is unused
(let [page (get-in state [:workspace-data :pages-index page-id])
local (get-in state [:workspace-cache page-id] workspace-local-default)]
(-> state
(assoc :current-page-id page-id ; mainly used by events
:workspace-local local
:workspace-page (dissoc page :data))
(assoc-in [:workspace-data page-id] (:data page)))))
ptk/WatchEvent
(watch [_ state stream]
(rx/of (dwp/initialize-page-persistence page-id)))))
(assoc state
:current-page-id page-id ; mainly used by events
:workspace-page page
:workspace-local local
)))))
(defn finalize-page
[page-id]
@ -185,11 +195,67 @@
(let [local (:workspace-local state)]
(-> state
(assoc-in [:workspace-cache page-id] local)
(update :workspace-data dissoc page-id))))
(dissoc :current-page-id :workspace-page))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Workspace Page CRUD
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def create-empty-page
(ptk/reify ::create-empty-page
ptk/WatchEvent
(watch [this state stream]
(let [id (uuid/next)
pages (get-in state [:workspace-data :pages-index])
unames (dwc/retrieve-used-names pages)
name (dwc/generate-unique-name unames "Page")
rchange {:type :add-page
:id id
:name name}
uchange {:type :del-page
:id id}]
(rx/of (dwc/commit-changes [rchange] [uchange] {:commit-local? true}))))))
(s/def ::rename-page
(s/keys :req-un [::id ::name]))
(defn rename-page
[id name]
(us/verify ::us/uuid id)
(us/verify string? name)
(ptk/reify ::rename-page
ptk/WatchEvent
(watch [_ state stream]
(rx/of ::dwp/finalize))))
(let [page (get-in state [:workspace-data :pages-index id])
rchg {:type :mod-page
:id id
:name name}
uchg {:type :mod-page
:id id
:name (:name page)}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(declare purge-page)
(declare go-to-file)
;; TODO: properly handle positioning on undo.
(defn delete-page
[id]
(ptk/reify ::delete-page
ptk/WatchEvent
(watch [_ state s]
(let [page (get-in state [:workspace-data :pages-index id])
rchg {:type :del-page
:id id}
uchg {:type :add-page
:page page}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})
(when (= id (:current-page-id state))
go-to-file))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Workspace State Manipulation
@ -212,8 +278,8 @@
(update :height #(/ % hprop))))))))
(initialize [state local]
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
shapes (cph/select-toplevel-shapes objects {:include-frames? true})
srect (geom/selection-rect shapes)
local (assoc local :vport size)]
@ -397,8 +463,8 @@
(ptk/reify ::zoom-to-fit-all
ptk/UpdateEvent
(update [_ state]
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
shapes (cph/select-toplevel-shapes objects {:include-frames? true})
srect (geom/selection-rect shapes)]
@ -420,11 +486,11 @@
(let [selected (get-in state [:workspace-local :selected])]
(if (empty? selected)
state
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])
srect (->> selected
(map #(get objects %))
(geom/selection-rect))]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
srect (->> selected
(map #(get objects %))
(geom/selection-rect))]
(update state :workspace-local
(fn [{:keys [vbox vport] :as local}]
(let [srect (geom/adjust-to-viewport vport srect {:padding 40})
@ -433,31 +499,8 @@
(assoc :zoom zoom)
(update :vbox merge srect)))))))))))
;; --- Add shape to Workspace
(defn- retrieve-used-names
[objects]
(into #{} (map :name) (vals objects)))
(defn- extract-numeric-suffix
[basename]
(if-let [[match p1 p2] (re-find #"(.*)-([0-9]+)$" basename)]
[p1 (+ 1 (d/parse-integer p2))]
[basename 1]))
(defn- generate-unique-name
"A unique name generator"
[used basename]
(s/assert ::set-of-string used)
(s/assert ::us/string basename)
(let [[prefix initial] (extract-numeric-suffix basename)]
(loop [counter initial]
(let [candidate (str prefix "-" counter)]
(if (contains? used candidate)
(recur (inc counter))
candidate)))))
(declare start-edition-mode)
(defn add-shape
@ -466,14 +509,14 @@
(ptk/reify ::add-shape
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
id (uuid/next)
shape (geom/setup-proportions attrs)
unames (retrieve-used-names objects)
name (generate-unique-name unames (:name shape))
unames (dwc/retrieve-used-names objects)
name (dwc/generate-unique-name unames (:name shape))
frames (cph/select-frames objects)
@ -492,9 +535,11 @@
rchange {:type :add-obj
:id id
:page-id page-id
:frame-id frame-id
:obj shape}
uchange {:type :del-obj
:page-id page-id
:id id}]
(rx/concat
@ -614,9 +659,9 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
objects (dwc/lookup-page-objects state page-id)
del-change #(array-map :type :del-obj :id %)
del-change #(array-map :type :del-obj :page-id page-id :id %)
get-empty-parents
(fn get-empty-parents [parents]
@ -637,7 +682,9 @@
(map del-change (reverse children))
[(del-change id)]
(map del-change (get-empty-parents parents))
[{:type :reg-objects :shapes (vec parents)}])))
[{:type :reg-objects
:page-id page-id
:shapes (vec parents)}])))
[]
ids)
@ -649,6 +696,7 @@
(let [item (get objects id)]
{:type :add-obj
:id (:id item)
:page-id page-id
:index (cph/position-on-parent id objects)
:frame-id (:frame-id item)
:parent-id (:parent-id item)
@ -657,7 +705,9 @@
(map add-chg (reverse (get-empty-parents parents)))
[(add-chg id)]
(map add-chg children)
[{:type :reg-objects :shapes (vec parents)}])))
[{:type :reg-objects
:page-id page-id
:shapes (vec parents)}])))
[]
ids)
]
@ -673,16 +723,10 @@
(ptk/reify ::delete-selected
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
lookup #(get-in state [:workspace-data page-id :objects %])
selected (get-in state [:workspace-local :selected])
shapes (map lookup selected)
shape? #(not= (:type %) :frame)]
(let [selected (get-in state [:workspace-local :selected])]
(rx/of (delete-shapes selected)
dws/deselect-all)))))
;; --- Shape Vertical Ordering
(s/def ::loc #{:up :down :bottom :top})
@ -694,7 +738,7 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
rchanges (mapv (fn [id]
(let [obj (get objects id)
@ -709,6 +753,7 @@
{:type :mov-objects
:parent-id (:parent-id obj)
:frame-id (:frame-id obj)
:page-id page-id
:index nindex
:shapes [id]}))
selected)
@ -718,9 +763,11 @@
{:type :mov-objects
:parent-id (:parent-id obj)
:frame-id (:frame-id obj)
:page-id page-id
:shapes [id]
:index (cph/position-on-parent id objects)}))
selected)]
selected)]
;; TODO: maybe missing the :reg-objects event?
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))))))
@ -736,8 +783,7 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
objects (dwc/lookup-page-objects state page-id)
parents (loop [res #{parent-id}
ids (seq ids)]
(if (nil? ids)
@ -748,9 +794,11 @@
rchanges [{:type :mov-objects
:parent-id parent-id
:page-id page-id
:index to-index
:shapes (vec (reverse ids))}
{:type :reg-objects
:page-id page-id
:shapes parents}]
uchanges
@ -759,11 +807,13 @@
(conj res
{:type :mov-objects
:parent-id (:parent-id obj)
:page-id page-id
:index (cph/position-on-parent id objects)
:shapes [id]})))
[] (reverse ids))
uchanges (conj uchanges
{:type :reg-objects
:page-id page-id
:shapes parents})]
;; (println "================ rchanges")
@ -787,23 +837,17 @@
(defn relocate-page
[id index]
(ptk/reify ::relocate-pages
ptk/UpdateEvent
(update [_ state]
(let [pages (get-in state [:workspace-file :pages])
[before after] (split-at index pages)
p? (partial = id)
pages' (d/concat []
(remove p? before)
[id]
(remove p? after))]
(assoc-in state [:workspace-file :pages] pages')))
ptk/WatchEvent
(watch [_ state stream]
(let [file (:workspace-file state)]
(->> (rp/mutation! :reorder-pages {:page-ids (:pages file)
:file-id (:id file)})
(rx/ignore))))))
(let [cidx (-> (get-in state [:workspace-data :pages])
(d/index-of id))
rchg {:type :mov-page
:id id
:index index}
uchg {:type :mov-page
:id id
:index cidx}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
;; --- Shape / Selection Alignment and Distribution
@ -817,7 +861,7 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
moved (if (= 1 (count selected))
(align-object-to-frame objects (first selected) axis)
@ -838,9 +882,11 @@
ops2 (dwc/generate-operations curr prev)]
(recur (next moved)
(conj rchanges {:type :mod-obj
:page-id page-id
:operations ops1
:id (:id curr)})
(conj uchanges {:type :mod-obj
:page-id page-id
:operations ops2
:id (:id curr)})))))))))
@ -863,9 +909,8 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
moved (-> (map #(get objects %) selected)
(geom/distribute-space axis objects))]
(loop [moved (seq moved)
@ -884,9 +929,11 @@
ops2 (dwc/generate-operations curr prev)]
(recur (next moved)
(conj rchanges {:type :mod-obj
:page-id page-id
:operations ops1
:id (:id curr)})
(conj uchanges {:type :mod-obj
:page-id page-id
:operations ops2
:id (:id curr)})))))))))
@ -921,7 +968,7 @@
(ptk/reify ::clear-drawing
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local dissoc :drawing-tool :drawing))))
(update state :workspace-drawing dissoc :tool :object))))
(defn select-for-drawing
([tool] (select-for-drawing tool nil))
@ -929,7 +976,7 @@
(ptk/reify ::select-for-drawing
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local assoc :drawing-tool tool :drawing data))
(update state :workspace-drawing assoc :tool tool :object data))
ptk/WatchEvent
(watch [_ state stream]
@ -963,7 +1010,8 @@
(defn set-shape-proportion-lock
[id lock]
(ptk/reify ::set-shape-proportion-lock
(js/alert "TODO: broken")
#_(ptk/reify ::set-shape-proportion-lock
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
@ -988,11 +1036,13 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
shape (get-in state [:workspace-data page-id :objects id])
current-position (gpt/point (:x shape) (:y shape))
position (gpt/point (or (:x position) (:x shape)) (or (:y position) (:y shape)))
displacement (gmt/translate-matrix (gpt/subtract position current-position))]
(rx/of (dwt/set-modifiers [id] {:displacement displacement})
objects (dwc/lookup-page-objects state page-id)
shape (get objects id)
cpos (gpt/point (:x shape) (:y shape))
pos (gpt/point (or (:x position) (:x shape))
(or (:y position) (:y shape)))
displ (gmt/translate-matrix (gpt/subtract pos cpos))]
(rx/of (dwt/set-modifiers [id] {:displacement displ})
(dwt/apply-modifiers [id]))))))
;; --- Path Modifications
@ -1003,7 +1053,8 @@
(us/verify ::us/uuid id)
(us/verify ::us/integer index)
(us/verify gpt/point? delta)
(ptk/reify ::update-path
(js/alert "TODO: broken")
#_(ptk/reify ::update-path
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)]
@ -1047,24 +1098,21 @@
ptk/WatchEvent
(watch [_ state stream]
(let [project-id (get-in state [:workspace-project :id])
file-id (get-in state [:workspace-page :file-id])
path-params {:file-id file-id :project-id project-id}
query-params {:page-id page-id}]
(rx/of (rt/nav :workspace path-params query-params))))))
file-id (get-in state [:workspace-file :id])
pparams {:file-id file-id :project-id project-id}
qparams {:page-id page-id}]
(rx/of (rt/nav :workspace pparams qparams))))))
(def go-to-file
(ptk/reify ::go-to-file
ptk/WatchEvent
(watch [_ state stream]
(let [file (:workspace-file state)
file-id (:id file)
project-id (:project-id file)
page-ids (:pages file)
path-params {:project-id project-id :file-id file-id}
query-params {:page-id (first page-ids)}]
(rx/of (rt/nav :workspace path-params query-params))))))
(let [{:keys [id project-id data] :as file} (:workspace-file state)
page-id (get-in data [:pages 0])
pparams {:project-id project-id :file-id id}
qparams {:page-id page-id}]
(rx/of (rt/nav :workspace pparams qparams))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Context Menu
@ -1128,8 +1176,7 @@
(ptk/reify ::copy-selected
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
(let [objects (dwc/lookup-page-objects state)
selected (get-in state [:workspace-local :selected])
cdata (prepare-selected objects selected)]
(->> (t/encode cdata)
@ -1144,18 +1191,18 @@
ptk/WatchEvent
(watch [_ state stream]
(let [selected-objs (map #(get objects %) selected)
wrapper (geom/selection-rect selected-objs)
orig-pos (gpt/point (:x1 wrapper) (:y1 wrapper))
wrapper (geom/selection-rect selected-objs)
orig-pos (gpt/point (:x1 wrapper) (:y1 wrapper))
mouse-pos @ms/mouse-position
delta (gpt/subtract mouse-pos orig-pos)
delta (gpt/subtract mouse-pos orig-pos)
page-id (:current-page-id state)
unames (-> (get-in state [:workspace-data page-id :objects])
(retrieve-used-names))
page-id (:current-page-id state)
unames (-> (dwc/lookup-page-objects state page-id)
(dwc/retrieve-used-names))
rchanges (dws/prepare-duplicate-changes objects unames selected delta)
uchanges (mapv #(array-map :type :del-obj :id (:id %))
(reverse rchanges))
rchanges (dws/prepare-duplicate-changes objects page-id unames selected delta)
uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %))
(reverse rchanges))
selected (->> rchanges
(filter #(selected (:old-id %)))
@ -1203,22 +1250,6 @@
(js/console.error "Clipboard error:" err)
(rx/empty)))))))
;; --- Change Page Order (D&D Ordering)
(defn change-page-order
[{:keys [id index] :as params}]
{:pre [(uuid? id) (number? index)]}
(ptk/reify ::change-page-order
ptk/UpdateEvent
(update [_ state]
(let [page (get-in state [:pages id])
pages (get-in state [:projects (:project-id page) :pages])
pages (into [] (remove #(= % id)) pages)
[before after] (split-at index pages)
pages (vec (concat before [id] after))]
(assoc-in state [:projects (:project-id page) :pages] pages)))))
(defn update-shape-flags
[id {:keys [blocked hidden] :as flags}]
(s/assert ::us/uuid id)
@ -1252,10 +1283,10 @@
(ptk/reify ::group-selected
ptk/WatchEvent
(watch [_ state stream]
(let [id (uuid/next)
page-id (get-in state [:workspace-page :id])
(let [id (uuid/next)
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
objects (get-in state [:workspace-data page-id :objects])
items (->> selected
(map #(get objects %))
(filter #(not= :frame (:type %)))
@ -1273,11 +1304,13 @@
rchanges [{:type :add-obj
:id id
:page-id page-id
:frame-id frame-id
:parent-id parent-id
:obj group
:index index}
{:type :mov-objects
:page-id page-id
:parent-id id
:shapes (->> items
(map :id)
@ -1287,13 +1320,14 @@
uchanges
(reduce (fn [res obj]
(conj res {:type :mov-objects
:page-id page-id
:parent-id (:parent-id obj)
:index (::index obj)
:shapes [(:id obj)]}))
[]
items)
uchanges (conj uchanges {:type :del-obj :id id})]
uchanges (conj uchanges {:type :del-obj :id id :page-id page-id})]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})
(dws/select-shapes (d/ordered-set id)))))))))
@ -1303,7 +1337,7 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
group-id (first selected)
group (get objects group-id)]
@ -1317,17 +1351,21 @@
(filter #(#{group-id} (second %)))
(ffirst))
rchanges [{:type :mov-objects
:page-id page-id
:parent-id parent-id
:shapes shapes
:index index-in-parent}]
uchanges [{:type :add-obj
:page-id page-id
:id group-id
:frame-id (:frame-id group)
:obj (assoc group :shapes [])}
{:type :mov-objects
:page-id page-id
:parent-id group-id
:shapes shapes}
{:type :mov-objects
:page-id page-id
:parent-id parent-id
:shapes [group-id]
:index index-in-parent}]]
@ -1361,12 +1399,12 @@
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
objects (dwc/lookup-page-objects state page-id)
selected-shape-id (-> state (get-in [:workspace-local :selected]) first)
selected-shape (get objects selected-shape-id)
selected-shape-frame-id (:frame-id selected-shape)
start-frame (get objects selected-shape-frame-id)
end-frame (dwc/get-frame-at-point objects position)]
end-frame (dwc/get-frame-at-point objects position)]
(cond-> state
(not= position initial-pos) (assoc-in [:workspace-local :draw-interaction-to] position)
(not= start-frame end-frame) (assoc-in [:workspace-local :draw-interaction-to-frame] end-frame))))))
@ -1383,12 +1421,12 @@
ptk/WatchEvent
(watch [_ state stream]
(let [position @ms/mouse-position
page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
frame (dwc/get-frame-at-point objects position)
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
frame (dwc/get-frame-at-point objects position)
shape-id (first (get-in state [:workspace-local :selected]))
shape (get objects shape-id)]
shape (get objects shape-id)]
(when-not (= position initial-pos)
(if (and frame shape-id
@ -1410,15 +1448,17 @@
(ptk/reify ::change-canvas-color
ptk/WatchEvent
(watch [_ state stream]
(let [pid (get state :current-page-id)
current-color (get-in state [:workspace-data pid :options :background])]
(let [page-id (get state :current-page-id)
options (dwc/lookup-page-options state page-id)
ccolor (:background options)]
(rx/of (dwc/commit-changes
[{:type :set-option
:page-id page-id
:option :background
:value color}]
[{:type :set-option
:option :background
:value current-color}]
:value ccolor}]
{:commit-local? true}))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1444,9 +1484,6 @@
(def unlink-file-from-library dwp/unlink-file-from-library)
(def upload-media-objects dwp/upload-media-objects)
(def delete-media-object dwp/delete-media-object)
(def rename-page dwp/rename-page)
(def delete-page dwp/delete-page)
(def create-empty-page dwp/create-empty-page)
;; Selection

View file

@ -25,10 +25,27 @@
;; --- Protocols
(declare setup-selection-index)
(declare update-page-indices)
(declare update-indices)
(declare reset-undo)
(declare append-undo)
;; --- Helpers
(defn lookup-page-objects
([state]
(lookup-page-objects state (:current-page-id state)))
([state page-id]
(get-in state [:workspace-data :pages-index page-id :objects])))
(defn lookup-page-options
([state]
(lookup-page-options state (:current-page-id state)))
([state page-id]
(get-in state [:workspace-data :pages-index page-id :options])))
;; --- Changes Handling
(defn commit-changes
@ -41,24 +58,23 @@
:as opts}]
(us/verify ::cp/changes changes)
(us/verify ::cp/changes undo-changes)
(ptk/reify ::commit-changes
cljs.core/IDeref
(-deref [_] changes)
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
state (update-in state [:workspace-pages page-id :data] cp/process-changes changes)]
(let [state (update-in state [:workspace-file :data] cp/process-changes changes)]
(cond-> state
commit-local? (update-in [:workspace-data page-id] cp/process-changes changes))))
commit-local? (update :workspace-data cp/process-changes changes))))
ptk/WatchEvent
(watch [_ state stream]
(let [page (:workspace-page state)
uidx (get-in state [:workspace-local :undo-index] ::not-found)]
(let [page-id (:current-page-id state)
uidx (get-in state [:workspace-undo :index] ::not-found)]
(rx/concat
(rx/of (update-page-indices (:id page)))
(when (some :page-id changes)
(rx/of (update-indices page-id)))
(when (and save-undo? (not= uidx ::not-found))
(rx/of (reset-undo uidx)))
@ -93,7 +109,7 @@
result)))))
(defn generate-changes
[objects1 objects2]
[page-id objects1 objects2]
(letfn [(impl-diff [res id]
(let [obj1 (get objects1 id)
obj2 (get objects2 id)
@ -102,6 +118,7 @@
(if (empty? ops)
res
(conj res {:type :mod-obj
:page-id page-id
:operations ops
:id id}))))]
(reduce impl-diff [] (set/union (set (keys objects1))
@ -109,25 +126,23 @@
;; --- Selection Index Handling
(defn- setup-selection-index
[{:keys [file pages] :as bundle}]
(defn initialize-indices
[{:keys [file] :as bundle}]
(ptk/reify ::setup-selection-index
ptk/WatchEvent
(watch [_ state stream]
(let [msg {:cmd :create-page-indices
(let [msg {:cmd :initialize-indices
:file-id (:id file)
:pages pages}]
:data (:data file)}]
(->> (uw/ask! msg)
(rx/map (constantly ::index-initialized)))))))
(defn update-page-indices
(defn update-indices
[page-id]
(ptk/reify ::update-page-indices
(ptk/reify ::update-indices
ptk/EffectEvent
(effect [_ state stream]
(let [objects (get-in state [:workspace-pages page-id :data :objects])
lookup #(get objects %)]
(let [objects (lookup-page-objects state page-id)]
(uw/ask! {:cmd :update-page-indices
:page-id page-id
:objects objects})))))
@ -143,7 +158,7 @@
(or (:id frame) uuid/zero)))
(defn- calculate-shape-to-frame-relationship-changes
[frames shapes]
[page-id frames shapes]
(loop [shape (first shapes)
shapes (rest shapes)
rch []
@ -155,9 +170,11 @@
(recur (first shapes)
(rest shapes)
(conj rch {:type :mov-objects
:page-id page-id
:parent-id fid
:shapes [(:id shape)]})
(conj uch {:type :mov-objects
:page-id page-id
:parent-id (:frame-id shape)
:shapes [(:id shape)]}))
(recur (first shapes)
@ -171,12 +188,12 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])
objects (lookup-page-objects state page-id)
shapes (cph/select-toplevel-shapes objects)
frames (cph/select-frames objects)
[rch uch] (calculate-shape-to-frame-relationship-changes frames shapes)]
[rch uch] (calculate-shape-to-frame-relationship-changes page-id frames shapes)]
(when-not (empty? rch)
(rx/of (commit-changes rch uch {:commit-local? true})))))))
@ -184,11 +201,34 @@
(defn get-frame-at-point
[objects point]
(let [frames (cph/select-frames objects)]
(loop [frame (first frames)
rest (rest frames)]
(d/seek #(geom/has-point? % point) frames))))
(d/seek #(geom/has-point? % point) frames)))
(defn- extract-numeric-suffix
[basename]
(if-let [[match p1 p2] (re-find #"(.*)-([0-9]+)$" basename)]
[p1 (+ 1 (d/parse-integer p2))]
[basename 1]))
(defn retrieve-used-names
[objects]
(into #{} (map :name) (vals objects)))
(s/def ::set-of-string
(s/every string? :kind set?))
(defn generate-unique-name
"A unique name generator"
[used basename]
(s/assert ::set-of-string used)
(s/assert ::us/string basename)
(let [[prefix initial] (extract-numeric-suffix basename)]
(loop [counter initial]
(let [candidate (str prefix "-" counter)]
(if (contains? used candidate)
(recur (inc counter))
candidate)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Undo / Redo
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -212,10 +252,9 @@
(ptk/reify ::materialize-undo
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)]
(-> state
(update-in [:workspace-data page-id] cp/process-changes changes)
(assoc-in [:workspace-local :undo-index] index))))))
(-> state
(update :workspace-data cp/process-changes changes)
(assoc-in [:workspace-undo :index] index)))))
(defn- reset-undo
[index]
@ -223,10 +262,8 @@
ptk/UpdateEvent
(update [_ state]
(-> state
(update :workspace-local dissoc :undo-index)
(update-in [:workspace-local :undo]
(fn [queue]
(into [] (take (inc index) queue))))))))
(update :workspace-undo dissoc :undo-index)
(update-in [:workspace-undo :items] (fn [queue] (into [] (take (inc index) queue))))))))
(defn- append-undo
[entry]
@ -234,18 +271,17 @@
(ptk/reify ::append-undo
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :undo] (fnil conj-undo-entry []) entry))))
(update-in state [:workspace-undo :items] (fnil conj-undo-entry []) entry))))
(def undo
(ptk/reify ::undo
ptk/WatchEvent
(watch [_ state stream]
(let [local (:workspace-local state)
undo (:undo local [])
index (or (:undo-index local)
(dec (count undo)))]
(when-not (or (empty? undo) (= index -1))
(let [changes (get-in undo [index :undo-changes])]
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))]
(when-not (or (empty? items) (= index -1))
(let [changes (get-in items [index :undo-changes])]
(rx/of (materialize-undo changes (dec index))
(commit-changes changes [] {:save-undo? false}))))))))
@ -253,12 +289,11 @@
(ptk/reify ::redo
ptk/WatchEvent
(watch [_ state stream]
(let [local (:workspace-local state)
undo (:undo local [])
index (or (:undo-index local)
(dec (count undo)))]
(when-not (or (empty? undo) (= index (dec (count undo))))
(let [changes (get-in undo [(inc index) :redo-changes])]
(let [undo (:workspace-undo state)
items (:items undo)
index (or (:index undo) (dec (count items)))]
(when-not (or (empty? items) (= index (dec items)))
(let [changes (get-in items [(inc index) :redo-changes])]
(rx/of (materialize-undo changes (inc index))
(commit-changes changes [] {:save-undo? false}))))))))
@ -266,7 +301,13 @@
(ptk/reify ::reset-undo
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local dissoc :undo-index :undo))))
(assoc state :workspace-undo {}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shapes
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn expand-all-parents
@ -301,23 +342,25 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])]
objects (lookup-page-objects state page-id)]
(loop [ids (seq ids)
rch []
uch []]
(if (nil? ids)
(rx/of (commit-changes
(cond-> rch reg-objects? (conj {:type :reg-objects :shapes (vec ids)}))
(cond-> uch reg-objects? (conj {:type :reg-objects :shapes (vec ids)}))
(cond-> rch reg-objects? (conj {:type :reg-objects :page-id page-id :shapes (vec ids)}))
(cond-> uch reg-objects? (conj {:type :reg-objects :page-id page-id :shapes (vec ids)}))
{:commit-local? true}))
(let [id (first ids)
obj1 (get objects id)
obj2 (f obj1)
rchg {:type :mod-obj
:page-id page-id
:operations (generate-operations obj1 obj2)
:id id}
uchg {:type :mod-obj
:page-id page-id
:operations (generate-operations obj2 obj1)
:id id}]
(recur (next ids)
@ -332,7 +375,7 @@
(letfn [(impl-get-children [objects id]
(cons id (cph/get-children id objects)))
(impl-gen-changes [objects ids]
(impl-gen-changes [objects page-id ids]
(loop [sids (seq ids)
cids (seq (impl-get-children objects (first sids)))
rchanges []
@ -354,9 +397,11 @@
rops (generate-operations obj1 obj2)
uops (generate-operations obj2 obj1)
rchg {:type :mod-obj
:page-id page-id
:operations rops
:id id}
uchg {:type :mod-obj
:page-id page-id
:operations uops
:id id}]
(recur sids
@ -366,10 +411,7 @@
(ptk/reify ::update-shapes-recursive
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])
[rchanges uchanges] (impl-gen-changes objects (seq ids))]
(let [page-id (:current-page-id state)
objects (lookup-page-objects state page-id)
[rchanges uchanges] (impl-gen-changes objects page-id (seq ids))]
(rx/of (commit-changes rchanges uchanges {:commit-local? true})))))))

View file

@ -15,9 +15,11 @@
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.common.pages :as cp]
[app.common.uuid :as uuid]
[app.common.pages-helpers :as cph]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.data.workspace.common :as dwc]
[app.main.snap :as snap]
[app.main.streams :as ms]
[app.util.geom.path :as path]))
@ -29,25 +31,27 @@
(declare handle-finish-drawing)
(declare conditional-align)
;; NOTE/TODO: when an exception is raised in some point of drawing the
;; draw lock is not released so the user need to refresh in order to
;; be able draw again. THIS NEED TO BE REVISITED
(defn start-drawing
[type]
{:pre [(keyword? type)]}
(let [id (gensym "drawing")]
(let [id (uuid/next)]
(ptk/reify ::start-drawing
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :drawing-lock] #(if (nil? %) id %)))
(update-in state [:workspace-drawing :lock] #(if (nil? %) id %)))
ptk/WatchEvent
(watch [_ state stream]
(let [lock (get-in state [:workspace-local :drawing-lock])]
(if (= lock id)
(rx/merge
(->> (rx/filter #(= % handle-finish-drawing) stream)
(rx/take 1)
(rx/map (fn [_] #(update % :workspace-local dissoc :drawing-lock))))
(rx/of (handle-drawing type)))
(rx/empty)))))))
(let [lock (get-in state [:workspace-drawing :lock])]
(when (= lock id)
(rx/merge (->> (rx/filter #(= % handle-finish-drawing) stream)
(rx/take 1)
(rx/map (fn [_] #(update % :workspace-drawing dissoc :lock))))
(rx/of (handle-drawing type)))))))))
(defn handle-drawing
[type]
@ -55,7 +59,7 @@
ptk/UpdateEvent
(update [_ state]
(let [data (cp/make-minimal-shape type)]
(update-in state [:workspace-local :drawing] merge data)))
(update-in state [:workspace-drawing :object] merge data)))
ptk/WatchEvent
(watch [_ state stream]
@ -81,7 +85,7 @@
(assoc-in [:modifiers :resize-rotation] 0))))
(update-drawing [state point lock? point-snap]
(update-in state [:workspace-local :drawing] resize-shape point lock? point-snap))]
(update-in state [:workspace-drawing :object] resize-shape point lock? point-snap))]
(ptk/reify ::handle-drawing-generic
ptk/WatchEvent
@ -92,8 +96,9 @@
stoper (rx/filter stoper? stream)
initial @ms/mouse-position
page-id (get state :current-page-id)
objects (get-in state [:workspace-data page-id :objects])
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
layout (get state :workspace-layout)
frames (cph/select-frames objects)
@ -104,18 +109,18 @@
uuid/zero)
shape (-> state
(get-in [:workspace-local :drawing])
(get-in [:workspace-drawing :object])
(geom/setup {:x (:x initial) :y (:y initial) :width 1 :height 1})
(assoc :frame-id fid)
(assoc ::initialized? true))]
(rx/concat
;; Add shape to drawing state
(rx/of #(assoc-in state [:workspace-local :drawing] shape))
(rx/of #(assoc-in state [:workspace-drawing :object] shape))
;; Initial SNAP
(->> (snap/closest-snap-point page-id [shape] layout initial)
(rx/map (fn [{:keys [x y]}]
#(update-in % [:workspace-local :drawing] assoc :x x :y y))))
#(update-in % [:workspace-drawing :object] assoc :x x :y y))))
(->> ms/mouse-position
(rx/with-latest vector ms/mouse-position-ctrl)
@ -143,22 +148,22 @@
(initialize-drawing [state point]
(-> state
(assoc-in [:workspace-local :drawing :segments] [point point])
(assoc-in [:workspace-local :drawing ::initialized?] true)))
(assoc-in [:workspace-drawing :object :segments] [point point])
(assoc-in [:workspace-drawing :object ::initialized?] true)))
(insert-point-segment [state point]
(-> state
(update-in [:workspace-local :drawing :segments] (fnil conj []) point)))
(update-in [:workspace-drawing :object :segments] (fnil conj []) point)))
(update-point-segment [state index point]
(let [segments (count (get-in state [:workspace-local :drawing :segments]))
(let [segments (count (get-in state [:workspace-drawing :object :segments]))
exists? (< -1 index segments)]
(cond-> state
exists? (assoc-in [:workspace-local :drawing :segments index] point))))
exists? (assoc-in [:workspace-drawing :object :segments index] point))))
(finish-drawing-path [state]
(update-in
state [:workspace-local :drawing]
state [:workspace-drawing :object]
(fn [shape] (-> shape
(update :segments #(vec (butlast %)))
(geom/update-path-selrect)))))]
@ -229,14 +234,14 @@
(ms/mouse-event? event) (= type :up))
(initialize-drawing [state]
(assoc-in state [:workspace-local :drawing ::initialized?] true))
(assoc-in state [:workspace-drawing :object ::initialized?] true))
(insert-point-segment [state point]
(update-in state [:workspace-local :drawing :segments] (fnil conj []) point))
(update-in state [:workspace-drawing :object :segments] (fnil conj []) point))
(finish-drawing-curve [state]
(update-in
state [:workspace-local :drawing]
state [:workspace-drawing :object]
(fn [shape]
(-> shape
(update :segments #(path/simplify % simplify-tolerance))
@ -260,7 +265,7 @@
(ptk/reify ::handle-finish-drawing
ptk/WatchEvent
(watch [_ state stream]
(let [shape (get-in state [:workspace-local :drawing])]
(let [shape (get-in state [:workspace-drawing :object])]
(rx/concat
(rx/of dw/clear-drawing)
(when (::initialized? shape)
@ -281,5 +286,5 @@
(ptk/reify ::close-drawing-path
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :drawing :close?] true))))
(assoc-in state [:workspace-drawing :object :close?] true))))

View file

@ -0,0 +1,99 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.data.workspace.libraries
(:require
[app.common.data :as d]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.main.data.workspace.common :as dwc]
[app.common.pages :as cp]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.color :as color]
[app.util.i18n :refer [tr]]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
(defn add-color
[color]
(us/assert ::us/string color)
(ptk/reify ::add-color
ptk/WatchEvent
(watch [_ state s]
(let [id (uuid/next)
rchg {:type :add-color
:color {:id id
:name color
:value color}}
uchg {:type :del-color
:id id}]
(rx/of #(assoc-in % [:workspace-local :color-for-rename] id)
(dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(def clear-color-for-rename
(ptk/reify ::clear-color-for-rename
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :color-for-rename] nil))))
(defn update-color
[{:keys [id] :as color}]
(us/assert ::cp/color color)
(ptk/reify ::update-color
ptk/WatchEvent
(watch [_ state stream]
(let [prev (get-in state [:workspace-data :colors id])
rchg {:type :mod-color
:color color}
uchg {:type :mod-color
:color prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(defn delete-color
[{:keys [id] :as color}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-color
ptk/WatchEvent
(watch [_ state stream]
(let [prev (get-in state [:workspace-data :colors id])
rchg {:type :del-color
:id id}
uchg {:type :add-color
:color prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(defn add-media
[{:keys [id] :as media}]
(us/assert ::cp/media-object media)
(ptk/reify ::add-media
ptk/WatchEvent
(watch [_ state stream]
(let [rchg {:type :add-media
:object media}
uchg {:type :del-media
:id id}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))
(defn delete-media
[{:keys [id] :as media}]
(us/assert ::us/uuid id)
(ptk/reify ::delete-media
ptk/WatchEvent
(watch [_ state stream]
(let [prev (get-in state [:workspace-data :media id])
rchg {:type :del-media
:id id}
uchg {:type :add-media
:object prev}]
(rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))

View file

@ -9,26 +9,28 @@
(ns app.main.data.workspace.notifications
(:require
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[clojure.set :as set]
[potok.core :as ptk]
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.persistence :as dwp]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.streams :as ms]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.persistence :as dwp]
[app.util.avatars :as avatars]
[app.common.geom.point :as gpt]
[app.util.time :as dt]
[app.util.transit :as t]
[app.util.websockets :as ws]))
[app.util.websockets :as ws]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[clojure.set :as set]
[potok.core :as ptk]))
;; TODO: this module need to be revisited.
(declare handle-presence)
(declare handle-pointer-update)
(declare handle-page-change)
(declare handle-file-change)
(declare handle-pointer-send)
(declare send-keepalive)
@ -62,7 +64,7 @@
(case type
:presence (handle-presence msg)
:pointer-update (handle-pointer-update msg)
:page-change (handle-page-change msg)
:file-change (handle-file-change msg)
::unknown))))
(->> stream
@ -120,22 +122,29 @@
avail (set/difference presence-palette used)
color (or (first avail) "#000000")]
(assoc session :color color))))
(assign-session [sessions {:keys [id profile]}]
(let [session {:id id
:fullname (:fullname profile)
:updated-at (dt/now)
:photo-uri (or (:photo-uri profile)
(avatars/generate {:name (:fullname profile)}))}
session (assign-color sessions session)]
(assoc sessions id session)))
(update-sessions [previous profiles]
(reduce (fn [current [session-id profile-id]]
(let [profile (get profiles profile-id)
session {:id session-id
:fullname (:fullname profile)
:photo-uri (or (:photo-uri profile)
(avatars/generate {:name (:fullname profile)}))}
session (assign-color current session)]
(assoc current session-id session)))
(select-keys previous (map first sessions))
(filter (fn [[sid]] (not (contains? previous sid))) sessions)))]
(let [previous (select-keys previous (map first sessions)) ; Initial clearing
pending (->> sessions
(filter #(not (contains? previous (first %))))
(map (fn [[session-id profile-id]]
{:id session-id
:profile (get profiles profile-id)})))]
(reduce assign-session previous pending)))]
(ptk/reify ::handle-presence
ptk/UpdateEvent
(update [_ state]
(let [profiles (:workspace-users state)]
(let [profiles (:workspace-users state)]
(update state :workspace-presence update-sessions profiles))))))
(defn handle-pointer-update
@ -143,13 +152,12 @@
(ptk/reify ::handle-pointer-update
ptk/UpdateEvent
(update [_ state]
(let [profile (get-in state [:workspace-users profile-id])]
(update-in state [:workspace-presence session-id]
(fn [session]
(assoc session
:point (gpt/point x y)
:updated-at (dt/now)
:page-id page-id)))))))
(update-in state [:workspace-presence session-id]
(fn [session]
(assoc session
:point (gpt/point x y)
:updated-at (dt/now)
:page-id page-id))))))
(defn handle-pointer-send
[file-id point]
@ -158,19 +166,24 @@
(effect [_ state stream]
(let [ws (get-in state [:ws file-id])
sid (:session-id state)
pid (get-in state [:workspace-page :id])
pid (:current-page-id state)
msg {:type :pointer-update
:page-id pid
:x (:x point)
:y (:y point)}]
(ws/-send ws (t/encode msg))))))
(defn handle-page-change
[msg]
(ptk/reify ::handle-page-change
;; TODO: add specs
(defn handle-file-change
[{:keys [file-id changes] :as msg}]
(ptk/reify ::handle-file-change
ptk/WatchEvent
(watch [_ state stream]
(rx/of (dwp/shapes-changes-persisted msg)
(dwc/update-page-indices (:page-id msg))))))
(let [page-ids (into #{} (comp (map :page-id)
(filter identity))
changes)]
(rx/merge
(rx/of (dwp/shapes-changes-persisted file-id msg))
(when (seq page-ids)
(rx/from (map dwc/update-indices page-ids))))))))

View file

@ -9,17 +9,15 @@
(ns app.main.data.workspace.persistence
(:require
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]
[app.common.data :as d]
[app.common.media :as cm]
[app.common.geom.point :as gpt]
[app.common.media :as cm]
[app.common.pages :as cp]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.main.data.dashboard :as dd]
[app.main.data.messages :as dm]
[app.main.data.media :as di]
[app.main.data.messages :as dm]
[app.main.data.workspace.common :as dwc]
[app.main.repo :as rp]
[app.main.store :as st]
@ -27,25 +25,23 @@
[app.util.object :as obj]
[app.util.router :as rt]
[app.util.time :as dt]
[app.util.transit :as t]))
[app.util.transit :as t]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
(declare persist-changes)
(declare update-selection-index)
(declare shapes-changes-persisted)
;; --- Persistence
(defn initialize-page-persistence
[page-id]
(defn initialize-file-persistence
[file-id]
(letfn [(enable-reload-stoper []
(obj/set! js/window "onbeforeunload" (constantly false)))
(disable-reload-stoper []
(obj/set! js/window "onbeforeunload" nil))]
(ptk/reify ::initialize-persistence
ptk/UpdateEvent
(update [_ state]
(assoc state :current-page-id page-id))
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter #(= ::finalize %) stream)
@ -61,7 +57,7 @@
(rx/buffer-until notifier)
(rx/map vec)
(rx/filter (complement empty?))
(rx/map #(persist-changes page-id %))
(rx/map #(persist-changes file-id %))
(rx/take-until (rx/delay 100 stoper)))
(->> stream
(rx/filter (ptk/type? ::changes-persisted))
@ -70,40 +66,44 @@
(rx/take-until stoper))))))))
(defn persist-changes
[page-id changes]
[file-id changes]
(ptk/reify ::persist-changes
ptk/WatchEvent
(watch [_ state stream]
(let [sid (:session-id state)
page (get-in state [:workspace-pages page-id])
changes (into [] (mapcat identity) changes)
params {:id (:id page)
:revn (:revn page)
:session-id sid
:changes changes}]
(->> (rp/mutation :update-page params)
(rx/map (fn [lagged]
(if (= #{sid} (into #{} (map :session-id) lagged))
(map #(assoc % :changes []) lagged)
lagged)))
(rx/mapcat seq)
(rx/map shapes-changes-persisted))))))
file (:workspace-file state)]
(when (= (:id file) file-id)
(let [changes (into [] (mapcat identity) changes)
params {:id (:id file)
:revn (:revn file)
:session-id sid
:changes changes}]
(->> (rp/mutation :update-file params)
(rx/map (fn [lagged]
(if (= #{sid} (into #{} (map :session-id) lagged))
(map #(assoc % :changes []) lagged)
lagged)))
(rx/mapcat seq)
(rx/map #(shapes-changes-persisted file-id %)))))))))
(s/def ::shapes-changes-persisted
(s/keys :req-un [::page-id ::revn ::cp/changes]))
(s/keys :req-un [::revn ::cp/changes]))
(defn shapes-changes-persisted
[{:keys [page-id revn changes] :as params}]
[file-id {:keys [revn changes] :as params}]
(us/verify ::us/uuid file-id)
(us/verify ::shapes-changes-persisted params)
(ptk/reify ::changes-persisted
ptk/UpdateEvent
(update [_ state]
(let [sid (:session-id state)
page (get-in state [:workspace-pages page-id])
state (update-in state [:workspace-pages page-id :revn] #(max % revn))]
(-> state
(update-in [:workspace-data page-id] cp/process-changes changes)
(update-in [:workspace-pages page-id :data] cp/process-changes changes))))))
file (:workspace-file state)]
(if (= file-id (:id file))
(let [state (update-in state [:workspace-file :revn] #(max % revn))]
(-> state
(update :workspace-data cp/process-changes changes)
(update-in [:workspace-file :data] cp/process-changes changes)))
state)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -148,18 +148,9 @@
(->> (rx/zip (rp/query :file {:id file-id})
(rp/query :file-users {:id file-id})
(rp/query :project-by-id {:project-id project-id})
(rp/query :pages {:file-id file-id})
(rp/query :media-objects {:file-id file-id :is-local false})
(rp/query :colors {:file-id file-id})
(rp/query :file-libraries {:file-id file-id}))
(rx/first)
(rx/mapcat
(fn [bundle]
(->> (fetch-libraries-content (get bundle 6))
(rx/map (fn [[lib-media-objects lib-colors]]
(conj bundle lib-media-objects lib-colors))))))
(rx/map (fn [bundle]
(apply bundle-fetched bundle)))
(rx/map (fn [bundle] (apply bundle-fetched bundle)))
(rx/catch (fn [{:keys [type code] :as error}]
(cond
(= :not-found type)
@ -172,76 +163,25 @@
:else
(throw error))))))))
(defn- fetch-libraries-content
[libraries]
(if (empty? libraries)
(rx/of [{} {}])
(rx/zip
(->> ;; fetch media-objects list of each library, and concatenate in a sequence
(apply rx/zip (for [library libraries]
(->> (rp/query :media-objects {:file-id (:id library)
:is-local false})
(rx/map (fn [media-objects]
[(:id library) media-objects])))))
;; reorganize the sequence as a map {library-id -> media-objects}
(rx/map (fn [media-list]
(reduce (fn [result, [library-id media-objects]]
(assoc result library-id media-objects))
{}
media-list))))
(->> ;; fetch colorss list of each library, and concatenate in a vector
(apply rx/zip (for [library libraries]
(->> (rp/query :colors {:file-id (:id library)})
(rx/map (fn [colors]
[(:id library) colors])))))
;; reorganize the sequence as a map {library-id -> colors}
(rx/map (fn [colors-list]
(reduce (fn [result, [library-id colors]]
(assoc result library-id colors))
{}
colors-list)))))))
(defn- bundle-fetched
[file users project pages media-objects colors libraries lib-media-objects lib-colors]
[file users project libraries]
(ptk/reify ::bundle-fetched
IDeref
(-deref [_]
{:file file
:users users
:project project
:pages pages
:media-objects media-objects
:colors colors
:libraries libraries})
ptk/UpdateEvent
(update [_ state]
(let [assoc-page
#(assoc-in %1 [:workspace-pages (:id %2)] %2)
assoc-media-objects
#(assoc-in %1 [:workspace-libraries %2 :media-objects]
(get lib-media-objects %2))
assoc-colors
#(assoc-in %1 [:workspace-libraries %2 :colors]
(get lib-colors %2))]
(as-> state $$
(assoc $$
:workspace-file (assoc file
:media-objects media-objects
:colors colors)
:workspace-users (d/index-by :id users)
:workspace-pages {}
:workspace-project project
:workspace-libraries (d/index-by :id libraries))
(reduce assoc-media-objects $$ (keys lib-media-objects))
(reduce assoc-colors $$ (keys lib-colors))
(reduce assoc-page $$ pages))))))
(assoc state
:workspace-undo {}
:workspace-project project
:workspace-file file
:workspace-data (:data file)
:workspace-users (d/index-by :id users)
:workspace-libraries (d/index-by :id libraries)))))
;; --- Set File shared
@ -358,80 +298,6 @@
(assoc-in state [:workspace-pages id] page))))
;; --- Page Crud
(declare page-created)
(def create-empty-page
(ptk/reify ::create-empty-page
ptk/WatchEvent
(watch [this state stream]
(let [file-id (get-in state [:workspace-file :id])
name (name (gensym "Page "))
ordering (count (get-in state [:workspace-file :pages]))
params {:name name
:file-id file-id
:ordering ordering
:data cp/default-page-data}]
(->> (rp/mutation :create-page params)
(rx/map page-created))))))
(defn page-created
[{:keys [id file-id] :as page}]
(us/verify ::page page)
(ptk/reify ::page-created
cljs.core/IDeref
(-deref [_] page)
ptk/UpdateEvent
(update [_ state]
(-> state
(update-in [:workspace-file :pages] (fnil conj []) id)
(assoc-in [:workspace-pages id] page)))))
(s/def ::rename-page
(s/keys :req-un [::id ::name]))
(defn rename-page
[id name]
(us/verify ::us/uuid id)
(us/verify string? name)
(ptk/reify ::rename-page
ptk/UpdateEvent
(update [_ state]
(let [pid (get-in state [:workspace-page :id])
state (assoc-in state [:workspace-pages id :name] name)]
(cond-> state
(= pid id) (assoc-in [:workspace-page :name] name))))
ptk/WatchEvent
(watch [_ state stream]
(let [params {:id id :name name}]
(->> (rp/mutation :rename-page params)
(rx/map #(ptk/data-event ::page-renamed params)))))))
(declare purge-page)
(declare go-to-file)
(defn delete-page
[id]
{:pre [(uuid? id)]}
(reify
ptk/UpdateEvent
(update [_ state]
(purge-page state id))
ptk/WatchEvent
(watch [_ state s]
(let [page (:workspace-page state)]
(rx/merge
(->> (rp/mutation :delete-page {:id id})
(rx/flat-map (fn [_]
(if (= id (:id page))
(rx/of go-to-file)
(rx/empty))))))))))
;; --- Upload local media objects
(s/def ::local? ::us/boolean)
@ -462,23 +328,7 @@
(fn [uri]
{:file-id file-id
:is-local local?
:url uri})
assoc-to-library
(fn [media-object state]
(cond
(true? local?)
state
(true? is-library)
(update-in state
[:workspace-libraries file-id :media-objects]
#(conj % media-object))
:else
(update-in state
[:workspace-file :media-objects]
#(conj % media-object))))]
:url uri})]
(rx/concat
(rx/of (dm/show {:content (tr "media.loading")
@ -493,7 +343,6 @@
(rx/map prepare-js-file)
(rx/mapcat #(rp/mutation! :upload-media-object %))))
(rx/do on-success)
(rx/map (fn [mobj] (partial assoc-to-library mobj)))
(rx/catch (fn [error]
(cond
(= (:code error) :media-type-not-allowed)
@ -518,17 +367,6 @@
(defn delete-media-object
[file-id id]
(ptk/reify ::delete-media-object
ptk/UpdateEvent
(update [_ state]
(let [is-library (not= file-id (:id (:workspace-file state)))]
(if is-library
(update-in state
[:workspace-libraries file-id :media-objects]
(fn [media-objects] (filter #(not= (:id %) id) media-objects)))
(update-in state
[:workspace-file :media-objects]
(fn [media-objects] (filter #(not= (:id %) id) media-objects))))))
ptk/WatchEvent
(watch [_ state stream]
(let [params {:id id}]

View file

@ -125,8 +125,8 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)]
(rx/of (dwc/expand-all-parents [id] objects)))))))
(defn select-shapes
@ -139,8 +139,8 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)]
(rx/of (dwc/expand-all-parents ids objects))))))
(def deselect-all
@ -159,7 +159,7 @@
(ptk/reify ::select-shapes-by-current-selrect
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (get-in state [:workspace-page :id])
(let [page-id (:current-page-id state)
selrect (get-in state [:workspace-local :selrect])]
(rx/merge
(rx/of (update-selrect nil))
@ -174,17 +174,19 @@
(ptk/reify ::select-inside-group
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
group (get objects group-id)
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
group (get objects group-id)
children (map #(get objects %) (:shapes group))
;; TODO: consider using d/seek instead of filter+first
selected (->> children (filter #(geom/has-point? % position)) first)]
(when selected
(rx/of deselect-all (select-shape (:id selected))))))))
;; --- Duplicate Shapes
(declare prepare-duplicate-changes)
;; (declare prepare-duplicate-changes)
(declare prepare-duplicate-change)
(declare prepare-duplicate-frame-change)
(declare prepare-duplicate-shape-change)
@ -195,13 +197,13 @@
"Prepare objects to paste: generate new id, give them unique names,
move to the position of mouse pointer, and find in what frame they
fit."
[objects names ids delta]
[objects page-id names ids delta]
(loop [names names
ids (seq ids)
chgs []]
(if ids
(let [id (first ids)
result (prepare-duplicate-change objects names id delta)
result (prepare-duplicate-change objects page-id names id delta)
result (if (vector? result) result [result])]
(recur
(into names (map change->name) result)
@ -210,24 +212,24 @@
chgs)))
(defn- prepare-duplicate-change
[objects names id delta]
[objects page-id names id delta]
(let [obj (get objects id)]
(if (= :frame (:type obj))
(prepare-duplicate-frame-change objects names obj delta)
(prepare-duplicate-shape-change objects names obj delta (:frame-id obj) (:parent-id obj)))))
(prepare-duplicate-frame-change objects page-id names obj delta)
(prepare-duplicate-shape-change objects page-id names obj delta (:frame-id obj) (:parent-id obj)))))
(defn- prepare-duplicate-shape-change
[objects names obj delta frame-id parent-id]
(let [id (uuid/next)
name (generate-unique-name names (:name obj))
[objects page-id names obj delta frame-id parent-id]
(let [id (uuid/next)
name (generate-unique-name names (:name obj))
renamed-obj (assoc obj :id id :name name)
moved-obj (geom/move renamed-obj delta)
frames (cph/select-frames objects)
frame-id (if frame-id
frame-id
(dwc/calculate-frame-overlap frames moved-obj))
moved-obj (geom/move renamed-obj delta)
frames (cph/select-frames objects)
frame-id (if frame-id
frame-id
(dwc/calculate-frame-overlap frames moved-obj))
parent-id (or parent-id frame-id)
parent-id (or parent-id frame-id)
children-changes
(loop [names names
@ -237,7 +239,7 @@
(if (nil? cid)
result
(let [obj (get objects cid)
changes (prepare-duplicate-shape-change objects names obj delta frame-id id)]
changes (prepare-duplicate-shape-change objects page-id names obj delta frame-id id)]
(recur
(into names (map change->name changes))
(into result changes)
@ -249,6 +251,7 @@
(dissoc :shapes))]
(into [{:type :add-obj
:id id
:page-id page-id
:old-id (:id obj)
:frame-id frame-id
:parent-id parent-id
@ -256,25 +259,25 @@
children-changes)))
(defn- prepare-duplicate-frame-change
[objects names obj delta]
[objects page-id names obj delta]
(let [frame-id (uuid/next)
frame-name (generate-unique-name names (:name obj))
sch (->> (map #(get objects %) (:shapes obj))
(mapcat #(prepare-duplicate-shape-change objects names % delta frame-id frame-id)))
sch (->> (map #(get objects %) (:shapes obj))
(mapcat #(prepare-duplicate-shape-change objects page-id names % delta frame-id frame-id)))
renamed-frame (-> obj
(assoc :id frame-id)
(assoc :name frame-name)
(assoc :frame-id uuid/zero)
(dissoc :shapes))
moved-frame (geom/move renamed-frame delta)
frame (-> obj
(assoc :id frame-id)
(assoc :name frame-name)
(assoc :frame-id uuid/zero)
(dissoc :shapes)
(geom/move delta))
fch {:type :add-obj
:old-id (:id obj)
:page-id page-id
:id frame-id
:frame-id uuid/zero
:obj moved-frame}]
:obj frame}]
(into [fch] sch)))
@ -283,13 +286,14 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
selected (get-in state [:workspace-local :selected])
objects (get-in state [:workspace-data page-id :objects])
delta (gpt/point 0 0)
unames (retrieve-used-names objects)
rchanges (prepare-duplicate-changes objects unames selected delta)
uchanges (mapv #(array-map :type :del-obj :id (:id %))
rchanges (prepare-duplicate-changes objects page-id unames selected delta)
uchanges (mapv #(array-map :type :del-obj :page-id page-id :id (:id %))
(reverse rchanges))
selected (->> rchanges

View file

@ -10,23 +10,23 @@
(ns app.main.data.workspace.transforms
"Events related with shapes transformations"
(:require
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[beicon.core :as rx]
[potok.core :as ptk]
[app.common.data :as d]
[app.common.spec :as us]
[app.common.pages :as cp]
[app.common.pages-helpers :as cph]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.selection :as dws]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.streams :as ms]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.main.snap :as snap]))
[app.common.pages :as cp]
[app.common.pages-helpers :as cph]
[app.common.spec :as us]
[app.main.data.workspace.common :as dwc]
[app.main.data.workspace.selection :as dws]
[app.main.refs :as refs]
[app.main.snap :as snap]
[app.main.store :as st]
[app.main.streams :as ms]
[beicon.core :as rx]
[beicon.core :as rx]
[cljs.spec.alpha :as s]
[potok.core :as ptk]))
;; -- Declarations
@ -135,10 +135,11 @@
(watch [_ state stream]
(let [current-pointer @ms/mouse-position
initial-position (merge current-pointer initial)
stoper (rx/filter ms/mouse-up? stream)
page-id (get state :current-page-id)
resizing-shapes (map #(get-in state [:workspace-data page-id :objects %]) ids)
layout (get state :workspace-layout)]
stoper (rx/filter ms/mouse-up? stream)
layout (:workspace-layout state)
page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
resizing-shapes (map #(get objects %) ids)]
(rx/concat
(->> ms/mouse-position
(rx/with-latest vector ms/mouse-position-shift)
@ -148,12 +149,10 @@
(rx/map #(conj current %)))))
(rx/mapcat (partial resize shape initial-position resizing-shapes))
(rx/take-until stoper))
#_(rx/empty)
(rx/of (apply-modifiers ids)
finish-transform)))))))
;; -- ROTATE
(defn start-rotate
[shapes]
(ptk/reify ::start-rotate
@ -164,10 +163,10 @@
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter ms/mouse-up? stream)
group (gsh/selection-rect shapes)
group-center (gsh/center group)
initial-angle (gpt/angle @ms/mouse-position group-center)
(let [stoper (rx/filter ms/mouse-up? stream)
group (gsh/selection-rect shapes)
group-center (gsh/center group)
initial-angle (gpt/angle @ms/mouse-position group-center)
calculate-angle (fn [pos ctrl?]
(let [angle (- (gpt/angle pos group-center) initial-angle)
angle (if (neg? angle) (+ 360 angle) angle)
@ -201,9 +200,9 @@
(ptk/reify ::start-move-selected
ptk/WatchEvent
(watch [_ state stream]
(let [initial @ms/mouse-position
(let [initial (deref ms/mouse-position)
selected (get-in state [:workspace-local :selected])
stopper (rx/filter ms/mouse-up? stream)]
stopper (rx/filter ms/mouse-up? stream)]
(->> ms/mouse-position
(rx/take-until stopper)
(rx/map #(gpt/to-vec initial %))
@ -240,8 +239,8 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (get state :current-page-id)
objects (get-in state [:workspace-data page-id :objects])
(let [page-id (:current-page-id state)
objects (dwc/lookup-page-objects state page-id)
ids (if (nil? ids) (get-in state [:workspace-local :selected]) ids)
shapes (mapv #(get objects %) ids)
stopper (rx/filter ms/mouse-up? stream)
@ -342,22 +341,24 @@
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)
objects (get-in state [:workspace-data page-id :objects])
not-frame-id? (fn [shape-id]
(let [shape (get objects shape-id)]
(or recurse-frames? (not (= :frame (:type shape))))))
objects (dwc/lookup-page-objects state page-id)
not-frame-id?
(fn [shape-id]
(let [shape (get objects shape-id)]
(or recurse-frames? (not (= :frame (:type shape))))))
;; For each shape updates the modifiers given as arguments
update-shape
(fn [objects shape-id]
(update-in objects [shape-id :modifiers] #(merge % modifiers)))
;; ID's + Children but remove frame children if the flag is set to false
ids-with-children (concat ids (mapcat #(cph/get-children % objects)
(filter not-frame-id? ids)))
(filter not-frame-id? ids)))]
;; For each shape updates the modifiers given as arguments
update-shape (fn [state shape-id]
(update-in
state
[:workspace-data page-id :objects shape-id :modifiers]
#(merge % modifiers)))]
(reduce update-shape state ids-with-children))))))
(d/update-in-when state [:workspace-data :pages-index page-id :objects]
#(reduce update-shape % ids-with-children)))))))
(defn rotation-modifiers [center shape angle]
(let [displacement (let [shape-center (gsh/center shape)]
@ -376,25 +377,23 @@
(set-rotation delta-rotation shapes (-> shapes gsh/selection-rect gsh/center)))
([delta-rotation shapes center]
(ptk/reify ::set-rotation
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)]
(letfn [(rotate-shape [state angle shape center]
(let [objects (get-in state [:workspace-data page-id :objects])
path [:workspace-data page-id :objects (:id shape) :modifiers]
modifiers (rotation-modifiers center shape angle)]
(-> state
(update-in path merge modifiers))))
(letfn [(rotate-shape [objects angle shape center]
(update-in objects [(:id shape) :modifiers] merge (rotation-modifiers center shape angle)))
(rotate-around-center [state angle center shapes]
(reduce #(rotate-shape %1 angle %2 center) state shapes))]
(rotate-around-center [objects angle center shapes]
(reduce #(rotate-shape %1 angle %2 center) objects shapes))
(let [objects (get-in state [:workspace-data page-id :objects])
id->obj #(get objects %)
get-children (fn [shape] (map id->obj (cph/get-children (:id shape) objects)))
shapes (concat shapes (mapcat get-children shapes))]
(rotate-around-center state delta-rotation center shapes))))))))
(set-rotation [objects]
(let [id->obj #(get objects %)
get-children (fn [shape] (map id->obj (cph/get-children (:id shape) objects)))
shapes (concat shapes (mapcat get-children shapes))]
(rotate-around-center objects delta-rotation center shapes)))]
(ptk/reify ::set-rotation
ptk/UpdateEvent
(update [_ state]
(let [page-id (:current-page-id state)]
(d/update-in-when state [:workspace-data :pages-index page-id :objects] set-rotation)))))))
(defn apply-modifiers
[ids]
@ -403,8 +402,9 @@
ptk/WatchEvent
(watch [_ state stream]
(let [page-id (:current-page-id state)
objects0 (get-in state [:workspace-pages page-id :data :objects])
objects1 (get-in state [:workspace-data page-id :objects])
objects0 (get-in state [:workspace-file :data :pages-index page-id :objects])
objects1 (get-in state [:workspace-data :pages-index page-id :objects])
;; ID's + Children ID's
ids-with-children (d/concat [] (mapcat #(cph/get-children % objects1) ids) ids)
@ -413,7 +413,9 @@
update-shape #(update %1 %2 gsh/transform-shape)
objects2 (reduce update-shape objects1 ids-with-children)
regchg {:type :reg-objects :shapes (vec ids)}
regchg {:type :reg-objects
:page-id page-id
:shapes (vec ids)}
;; we need to generate redo chages from current
;; state (with current temporal values) to new state but
@ -421,8 +423,8 @@
;; state (without temporal values in it, for this reason
;; we have 3 different objects references).
rchanges (conj (dwc/generate-changes objects1 objects2) regchg)
uchanges (conj (dwc/generate-changes objects2 objects0) regchg)
rchanges (conj (dwc/generate-changes page-id objects1 objects2) regchg)
uchanges (conj (dwc/generate-changes page-id objects2 objects0) regchg)
]
(rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})

View file

@ -73,7 +73,8 @@
[objects]
(mf/fnc shape-wrapper
[{:keys [frame shape] :as props}]
(let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects))]
(let [group-wrapper (mf/use-memo (mf/deps objects) #(group-wrapper-factory objects))
frame-wrapper (mf/use-memo (mf/deps objects) #(frame-wrapper-factory objects))]
(when (and shape (not (:hidden shape)))
(let [shape (geom/transform-shape frame shape)
opts #js {:shape shape}]
@ -85,6 +86,7 @@
:path [:> path/path-shape opts]
:image [:> image/image-shape opts]
:circle [:> circle/circle-shape opts]
:frame [:> frame-wrapper {:shape shape}]
:group [:> group-wrapper {:shape shape :frame frame}]
nil))))))

View file

@ -39,20 +39,65 @@
;; ---- Workspace refs
;; (def workspace-local
;; (l/derived :workspace-local st/state))
(def workspace-drawing
(l/derived :workspace-drawing st/state))
(def workspace-local
(l/derived :workspace-local st/state))
(l/derived (fn [state]
(merge (:workspace-local state)
(:workspace-file-local state)))
st/state =))
(def selected-shapes
(l/derived :selected workspace-local))
(def selected-zoom
(l/derived :zoom workspace-local))
(def selected-drawing-tool
(l/derived :tool workspace-drawing))
(def current-drawing-shape
(l/derived :object workspace-drawing))
(def selected-edition
(l/derived :edition workspace-local))
(def current-transform
(l/derived :transform workspace-local))
(def options-mode
(l/derived :options-mode workspace-local))
(def vbox
(l/derived :vbox workspace-local))
(def current-hover
(l/derived :hover workspace-local))
(def workspace-layout
(l/derived :workspace-layout st/state))
(def workspace-page
(l/derived :workspace-page st/state))
(def workspace-page-id
(l/derived :id workspace-page))
(def workspace-file
(l/derived :workspace-file st/state))
(l/derived (fn [state]
(when-let [file (:workspace-file state)]
(-> file
(dissoc :data)
(assoc :pages (get-in file [:data :pages])))))
st/state =))
(def workspace-file-colors
(l/derived (fn [state]
(when-let [file (:workspace-file state)]
(get-in file [:data :colors])))
st/state))
(def workspace-project
(l/derived :workspace-project st/state))
@ -72,110 +117,85 @@
(def workspace-snap-data
(l/derived :workspace-snap-data st/state))
;; TODO: BROKEN & TO BE REMOVED
(def workspace-data
(-> #(let [page-id (get-in % [:workspace-page :id])]
(get-in % [:workspace-data page-id]))
(l/derived st/state)))
(def workspace-page-options
(l/derived :options workspace-data))
(def workspace-page
(l/derived (fn [state]
(let [page-id (:current-page-id state)
data (:workspace-data state)]
(get-in data [:pages-index page-id])))
st/state))
(def workspace-page-objects
(l/derived :objects workspace-page))
(def workspace-page-options
(l/derived :options workspace-page))
;; TODO: revisit
(def workspace-saved-grids
(l/derived :saved-grids workspace-page-options))
(def workspace-objects
(l/derived :objects workspace-data))
(def workspace-frames
(l/derived cph/select-frames workspace-objects))
(l/derived cph/select-frames workspace-page-objects))
(defn object-by-id
[id]
(letfn [(selector [state]
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])]
(get objects id)))]
(l/derived selector st/state =)))
(l/derived #(get % id) workspace-page-objects))
(defn objects-by-id
[ids]
(letfn [(selector [state]
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])]
(->> (set ids)
(map #(get objects %))
(filter identity)
(vec))))]
(l/derived selector st/state =)))
(l/derived (fn [objects]
(into [] (comp (map #(get objects %))
(filter identity))
(set ids)))
workspace-page-objects =))
(defn is-child-selected?
[id]
(letfn [(selector [state]
(let [page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])
(let [page-id :current-page-id
objects (get-in state [:workspace-data :pages-index page-id :objects])
selected (get-in state [:workspace-local :selected])
shape (get objects id)
children (cph/get-children id objects)]
(some selected children)))]
(l/derived selector st/state)))
(def selected-shapes
(l/derived :selected workspace-local))
;; TODO: can be replaced by objects-by-id
(def selected-objects
(letfn [(selector [state]
(let [selected (get-in state [:workspace-local :selected])
page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])]
(mapv #(get objects %) selected)))]
(let [selected (get-in state [:workspace-local :selected])
page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data :pages-index page-id :objects])]
(mapv #(get objects %) selected)))]
(l/derived selector st/state =)))
(def selected-shapes-with-children
(letfn [(selector [state]
(let [selected (get-in state [:workspace-local :selected])
page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])
page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data :pages-index page-id :objects])
children (mapcat #(cph/get-children % objects) selected)]
(into selected children)))]
(l/derived selector st/state)))
(l/derived selector st/state =)))
;; TODO: looks very inneficient access method, revisit the usage of this ref
(def selected-objects-with-children
(letfn [(selector [state]
(let [selected (get-in state [:workspace-local :selected])
page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data page-id :objects])
page-id (get-in state [:workspace-page :id])
objects (get-in state [:workspace-data :pages-index page-id :objects])
children (mapcat #(cph/get-children % objects) selected)
accumulated (into selected children)]
(mapv #(get objects %) accumulated)))]
(l/derived selector st/state)))
(defn make-selected
[id]
(l/derived #(contains? % id) selected-shapes))
(def selected-zoom
(l/derived :zoom workspace-local))
(def selected-drawing-tool
(l/derived :drawing-tool workspace-local))
(def current-drawing-shape
(l/derived :drawing workspace-local))
(def selected-edition
(l/derived :edition workspace-local))
(def current-transform
(l/derived :transform workspace-local))
(def options-mode
(l/derived :options-mode workspace-local))
(def vbox
(l/derived :vbox workspace-local))
(def current-hover
(l/derived :hover workspace-local))
shapes (into selected children)]
(mapv #(get objects %) shapes)))]
(l/derived selector st/state =)))
;; ---- Viewer refs

View file

@ -9,6 +9,7 @@
(ns app.main.ui
(:require
[expound.alpha :as expound]
[beicon.core :as rx]
[cuerdas.core :as str]
[potok.core :as ptk]
@ -49,7 +50,7 @@
["/password" :settings-password]
["/options" :settings-options]]
["/view/:page-id" :viewer]
["/view/:file-id/:page-id" :viewer]
["/not-found" :not-found]
["/not-authorized" :not-authorized]
@ -57,7 +58,7 @@
["/debug/icons-preview" :debug-icons-preview])
;; Used for export
["/render-object/:page-id/:object-id" :render-object]
["/render-object/:file-id/:page-id/:object-id" :render-object]
["/dashboard"
["/team/:team-id"
@ -112,16 +113,20 @@
:viewer
(let [index (d/parse-integer (get-in route [:params :query :index]))
token (get-in route [:params :query :token])
file-id (uuid (get-in route [:params :path :file-id]))
page-id (uuid (get-in route [:params :path :page-id]))]
[:& viewer-page {:page-id page-id
:file-id file-id
:index index
:token token}])
:render-object
(do
(let [page-id (uuid (get-in route [:params :path :page-id]))
object-id (uuid (get-in route [:params :path :object-id]))]
[:& render/render-object {:page-id page-id
(let [file-id (uuid (get-in route [:params :path :file-id]))
page-id (uuid (get-in route [:params :path :page-id]))
object-id (uuid (get-in route [:params :path :object-id]))]
[:& render/render-object {:file-id file-id
:page-id page-id
:object-id object-id}]))
:workspace
@ -163,9 +168,18 @@
[error]
(ts/schedule 0 #(st/emit! logout)))
(defmethod ptk/handle-error :assertion
[{:keys [data stack] :as error}]
(js/console.error stack)
(js/console.error (with-out-str
(expound/printer data))))
(defmethod ptk/handle-error :default
[error]
(js/console.error (if (map? error) (pr-str error) error))
(ts/schedule 100 #(st/emit! (dm/show {:content "Something wrong has happened."
:type :error
:timeout 5000}))))
(if (instance? ExceptionInfo error)
(ptk/handle-error (ex-data error))
(do
(js/console.error (if (map? error) (pr-str error) error))
(ts/schedule 100 #(st/emit! (dm/show {:content "Something wrong has happened."
:type :error
:timeout 5000}))))))

View file

@ -1,22 +1,34 @@
;; 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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.dashboard.grid
(:require
[cuerdas.core :as str]
[beicon.core :as rx]
[rumext.alpha :as mf]
[app.main.ui.icons :as i]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.dashboard :as dsh]
[app.main.store :as st]
[app.main.ui.modal :as modal]
[app.main.ui.keyboard :as kbd]
[app.main.ui.confirm :refer [confirm-dialog]]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.worker :as wrk]
[app.main.fonts :as fonts]
[app.main.store :as st]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.confirm :refer [confirm-dialog]]
[app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd]
[app.main.ui.modal :as modal]
[app.main.worker :as wrk]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]]
[app.util.router :as rt]
[app.util.time :as dt]
[app.util.timers :as ts]
[app.util.time :as dt]))
[beicon.core :as rx]
[cuerdas.core :as str]
[lambdaisland.uri :as uri]
[rumext.alpha :as mf]))
;; --- Grid Item Thumbnail
@ -25,15 +37,15 @@
[{:keys [file] :as props}]
(let [container (mf/use-ref)]
(mf/use-effect
(mf/deps file)
(mf/deps (:id file))
(fn []
(-> (wrk/ask! {:cmd :thumbnails/generate
:id (first (:pages file))
})
(rx/subscribe (fn [{:keys [svg fonts]}]
(run! fonts/ensure-loaded! fonts)
(when-let [node (mf/ref-val container)]
(set! (.-innerHTML ^js node) svg)))))))
(->> (wrk/ask! {:cmd :thumbnails/generate
:file-id (:id file)
:page-id (get-in file [:data :pages 0])})
(rx/subs (fn [{:keys [svg fonts]}]
(run! fonts/ensure-loaded! fonts)
(when-let [node (mf/ref-val container)]
(set! (.-innerHTML ^js node) svg)))))))
[:div.grid-item-th {:style {:background-color (get-in file [:data :options :background])}
:ref container}]))
@ -41,61 +53,90 @@
(mf/defc grid-item-metadata
[{:keys [modified-at]}]
(let [locale (i18n/use-locale)
time (dt/timeago modified-at {:locale locale})]
(let [locale (mf/deref i18n/locale)
time (dt/timeago modified-at {:locale locale})]
(str (t locale "ds.updated-at" time))))
(mf/defc grid-item
{:wrap [mf/memo]}
[{:keys [file] :as props}]
(let [local (mf/use-state {:menu-open false
:edition false})
locale (i18n/use-locale)
on-navigate #(st/emit! (rt/nav :workspace
{:project-id (:project-id file)
:file-id (:id file)}
{:page-id (first (:pages file))}))
delete-fn #(st/emit! nil (dsh/delete-file (:id file)))
on-delete #(do
(dom/stop-propagation %)
(modal/show! confirm-dialog {:on-accept delete-fn}))
[{:keys [id file] :as props}]
(let [local (mf/use-state {:menu-open false :edition false})
locale (mf/deref i18n/locale)
delete (mf/use-callback (mf/deps id) #(st/emit! nil (dsh/delete-file id)))
add-shared (mf/use-callback (mf/deps id) #(st/emit! (dsh/set-file-shared id true)))
del-shared (mf/use-callback (mf/deps id) #(st/emit! (dsh/set-file-shared id false)))
on-close (mf/use-callback #(swap! local assoc :menu-open false))
on-delete
(mf/use-callback
(mf/deps id)
(fn [event]
(dom/stop-propagation event)
(modal/show! confirm-dialog {:on-accept delete})))
on-navigate
(mf/use-callback
(mf/deps id)
(fn []
(let [pparams {:project-id (:project-id file)
:file-id (:id file)}
qparams {:page-id (first (get-in file [:data :pages]))}]
(st/emit! (rt/nav :workspace pparams qparams)))))
add-shared-fn #(st/emit! nil (dsh/set-file-shared (:id file) true))
on-add-shared
#(do
(dom/stop-propagation %)
(mf/use-callback
(mf/deps id)
(fn [event]
(dom/stop-propagation event)
(modal/show! confirm-dialog
{:message (t locale "dashboard.grid.add-shared-message" (:name file))
:hint (t locale "dashboard.grid.add-shared-hint")
:accept-text (t locale "dashboard.grid.add-shared-accept")
:not-danger? true
:on-accept add-shared-fn}))
:on-accept add-shared})))
remove-shared-fn #(st/emit! nil (dsh/set-file-shared (:id file) false))
on-remove-shared
#(do
(dom/stop-propagation %)
on-edit
(mf/use-callback
(mf/deps id)
(fn [event]
(dom/stop-propagation event)
(swap! local assoc :edition true)))
on-del-shared
(mf/use-callback
(mf/deps id)
(fn [event]
(dom/stop-propagation event)
(modal/show! confirm-dialog
{:message (t locale "dashboard.grid.remove-shared-message" (:name file))
:hint (t locale "dashboard.grid.remove-shared-hint")
:accept-text (t locale "dashboard.grid.remove-shared-accept")
:not-danger? false
:on-accept remove-shared-fn}))
:on-accept del-shared})))
on-blur #(let [name (-> % dom/get-target dom/get-value)]
(st/emit! (dsh/rename-file (:id file) name))
(swap! local assoc :edition false))
on-menu-click
(mf/use-callback
(mf/deps id)
(fn [event]
(dom/stop-propagation event)
(swap! local assoc :menu-open true)))
on-key-down #(cond
(kbd/enter? %) (on-blur %)
(kbd/esc? %) (swap! local assoc :edition false))
on-menu-click #(do
(dom/stop-propagation %)
(swap! local assoc :menu-open true))
on-menu-close #(swap! local assoc :menu-open false)
on-edit #(do
(dom/stop-propagation %)
(swap! local assoc :edition true))]
on-blur
(mf/use-callback
(mf/deps id)
(fn [event]
(let [name (-> event dom/get-target dom/get-value)]
(st/emit! (dsh/rename-file id name))
(swap! local assoc :edition false))))
on-key-down
(mf/use-callback
#(cond
(kbd/enter? %) (on-blur %)
(kbd/esc? %) (swap! local assoc :edition false)))
]
[:div.grid-item.project-th {:on-click on-navigate}
[:div.overlay]
[:& grid-item-thumbnail {:file file}]
@ -113,42 +154,39 @@
[:& grid-item-metadata {:modified-at (:modified-at file)}]]
[:div.project-th-actions {:class (dom/classnames
:force-display (:menu-open @local))}
;; [:div.project-th-icon.pages
;; i/page
;; #_[:span (:total-pages project)]]
;; [:div.project-th-icon.comments
;; i/chat
;; [:span "0"]]
[:div.project-th-icon.menu
{:on-click on-menu-click}
i/actions]
[:& context-menu {:on-close on-menu-close
[:& context-menu {:on-close on-close
:show (:menu-open @local)
:options [[(t locale "dashboard.grid.rename") on-edit]
[(t locale "dashboard.grid.delete") on-delete]
(if (:is-shared file)
[(t locale "dashboard.grid.remove-shared") on-remove-shared]
[(t locale "dashboard.grid.remove-shared") on-del-shared]
[(t locale "dashboard.grid.add-shared") on-add-shared])]}]]]))
;; --- Grid
(mf/defc grid
[{:keys [id opts files hide-new?] :as props}]
(let [locale (i18n/use-locale)
order (:order opts :modified)
filter (:filter opts "")
on-click #(do
(dom/prevent-default %)
(st/emit! (dsh/create-file id)))]
(let [locale (mf/deref i18n/locale)
click #(st/emit! (dsh/create-file id))]
[:section.dashboard-grid
(if (> (count files) 0)
[:div.dashboard-grid-row
(when (not hide-new?)
[:div.grid-item.add-file {:on-click on-click}
[:span (tr "ds.new-file")]])
(for [item files]
[:& grid-item {:file item :key (:id item)}])]
[:div.grid-files-empty
[:div.grid-files-desc (t locale "dashboard.grid.empty-files")]
[:div.grid-files-link
[:a.btn-secondary.btn-small {:on-click on-click} (t locale "ds.new-file")]]])]))
(cond
(pos? (count files))
[:div.dashboard-grid-row
(when (not hide-new?)
[:div.grid-item.add-file {:on-click click}
[:span (t locale "ds.new-file")]])
(for [item files]
[:& grid-item
{:id (:id item)
:file item
:key (:id item)}])]
(zero? (count files))
[:div.grid-files-empty
[:div.grid-files-desc (t locale "dashboard.grid.empty-files")]
[:div.grid-files-link
[:a.btn-secondary.btn-small {:on-click click} (t locale "ds.new-file")]]])]))

View file

@ -65,6 +65,7 @@
(mf/deps objects)
#(exports/shape-wrapper-factory objects))
]
[:svg {:id "screenshot"
:view-box vbox
:width width
@ -77,17 +78,35 @@
:group [:& group-wrapper {:shape object}]
[:& shape-wrapper {:shape object}])]))
(defn- adapt-root-frame
[objects object-id]
(if (uuid/zero? object-id)
(let [object (get objects object-id)
shapes (cph/select-toplevel-shapes objects {:include-frames? true})
srect (geom/selection-rect shapes)
object (merge object (select-keys srect [:x :y :width :height]))
object (geom/transform-shape object)
object (assoc object :fill-color "#f0f0f0")]
(assoc objects (:id object) object))
objects))
;; NOTE: for now, it is ok download the entire file for render only
;; single page but in a future we need consider to add a specific
;; backend entry point for download only the data of single page.
(mf/defc render-object
[{:keys [page-id object-id] :as props}]
(let [data (mf/use-state nil)]
[{:keys [file-id page-id object-id] :as props}]
(let [objects (mf/use-state nil)]
(mf/use-effect
(fn []
(let [subs (->> (repo/query! :page {:id page-id})
(rx/subs (fn [result]
(reset! data (:data result)))))]
#(rx/dispose! subs))))
(when @data
[:& object-svg {:objects (:objects @data)
#(let [subs (->> (repo/query! :file {:id file-id})
(rx/subs (fn [{:keys [data]}]
(let [objs (get-in data [:pages-index page-id :objects])
objs (adapt-root-frame objs object-id)]
(reset! objects objs)))))]
(fn [] (rx/dispose! subs))))
(when @objects
[:& object-svg {:objects @objects
:object-id object-id
:zoom 1}])))

View file

@ -9,11 +9,11 @@
(ns app.main.ui.shapes.frame
(:require
[rumext.alpha :as mf]
[app.common.data :as d]
[app.main.ui.shapes.attrs :as attrs]
[app.common.geom.shapes :as geom]
[app.util.object :as obj]))
[app.main.ui.shapes.attrs :as attrs]
[app.util.object :as obj]
[rumext.alpha :as mf]))
(def frame-default-props {:fill-color "#ffffff"})
@ -22,8 +22,8 @@
(mf/fnc frame-shape
{::mf/wrap-props false}
[props]
(let [childs (unchecked-get props "childs")
shape (unchecked-get props "shape")
(let [childs (unchecked-get props "childs")
shape (unchecked-get props "shape")
{:keys [id x y width height]} shape
props (-> (merge frame-default-props shape)

View file

@ -9,25 +9,24 @@
(ns app.main.ui.viewer
(:require
[beicon.core :as rx]
[goog.events :as events]
[goog.object :as gobj]
[okulary.core :as l]
[rumext.alpha :as mf]
[app.main.ui.icons :as i]
[app.common.exceptions :as ex]
[app.main.data.viewer :as dv]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd]
[app.main.ui.viewer.header :refer [header]]
[app.main.ui.viewer.thumbnails :refer [thumbnails-panel]]
[app.main.ui.viewer.shapes :refer [frame-svg]]
[app.main.ui.viewer.thumbnails :refer [thumbnails-panel]]
[app.util.data :refer [classnames]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t tr]])
[app.util.i18n :as i18n :refer [t tr]]
[beicon.core :as rx]
[goog.events :as events]
[okulary.core :as l]
[rumext.alpha :as mf])
(:import goog.events.EventType))
(mf/defc main-panel
@ -106,10 +105,11 @@
;; --- Component: Viewer Page
(mf/defc viewer-page
[{:keys [page-id index token] :as props}]
[{:keys [file-id page-id index token] :as props}]
(mf/use-effect
(mf/deps page-id token)
#(st/emit! (dv/initialize page-id token)))
(mf/deps file-id page-id token)
(fn []
(st/emit! (dv/initialize props))))
(let [data (mf/deref refs/viewer-data)
local (mf/deref refs/viewer-local)]

View file

@ -75,12 +75,10 @@
(t locale "viewer.header.show-interactions-on-click")]]]]]))
(mf/defc share-link
[{:keys [page] :as props}]
[{:keys [page token] :as props}]
(let [show-dropdown? (mf/use-state false)
dropdown-ref (mf/use-ref)
token (:share-token page)
locale (i18n/use-locale)
dropdown-ref (mf/use-ref)
locale (mf/deref i18n/locale)
create #(st/emit! dv/create-share-link)
delete #(st/emit! dv/delete-share-link)
@ -158,8 +156,11 @@
[:div.options-zone
[:& interactions-menu {:interactions-mode interactions-mode}]
(when-not anonymous?
[:& share-link {:page (:page data)}])
[:& share-link {:token (:share-token data)
:page (:page data)}])
(when-not anonymous?
[:a.btn-text-basic.btn-small {:on-click on-edit}
(t locale "viewer.header.edit-page")])

View file

@ -9,44 +9,44 @@
(ns app.main.ui.workspace
(:require
[beicon.core :as rx]
[rumext.alpha :as mf]
[app.main.ui.icons :as i]
[app.common.geom.point :as gpt]
[app.main.constants :as c]
[app.main.data.history :as udh]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.streams :as ms]
[app.main.ui.keyboard :as kbd]
[app.main.ui.hooks :as hooks]
[app.main.ui.workspace.viewport :refer [viewport coordinates]]
[app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd]
[app.main.ui.workspace.colorpalette :refer [colorpalette]]
[app.main.ui.workspace.context-menu :refer [context-menu]]
[app.main.ui.workspace.header :refer [header]]
[app.main.ui.workspace.left-toolbar :refer [left-toolbar]]
[app.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]]
[app.main.ui.workspace.scroll :as scroll]
[app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
[app.main.ui.workspace.sidebar.history :refer [history-dialog]]
[app.main.ui.workspace.left-toolbar :refer [left-toolbar]]
[app.util.data :refer [classnames]]
[app.main.ui.workspace.viewport :refer [viewport coordinates]]
[app.util.dom :as dom]
[app.common.geom.point :as gpt]))
[beicon.core :as rx]
[okulary.core :as l]
[rumext.alpha :as mf]))
;; --- Workspace
(mf/defc workspace-content
[{:keys [page file layout project] :as params}]
(let [local (mf/deref refs/workspace-local)
left-sidebar? (:left-sidebar? local)
[{:keys [page-id file layout project] :as params}]
(let [local (mf/deref refs/workspace-local)
left-sidebar? (:left-sidebar? local)
right-sidebar? (:right-sidebar? local)
classes (classnames
:no-tool-bar-right (not right-sidebar?)
:no-tool-bar-left (not left-sidebar?))]
classes (dom/classnames
:no-tool-bar-right (not right-sidebar?)
:no-tool-bar-left (not left-sidebar?))]
[:*
(when (:colorpalette layout)
[:& colorpalette {:left-sidebar? left-sidebar?
:project project}])
:team-id (:team-id project)}])
[:section.workspace-content {:class classes}
[:& history-dialog]
@ -63,36 +63,52 @@
:vport (:vport local)}]
[:& coordinates]])
[:& viewport {:page page
:key (:id page)
[:& viewport {:page-id page-id
:key (str page-id)
:file file
:local local
:layout layout}]]]
[:& left-toolbar {:page page :layout layout}]
[:& left-toolbar {:layout layout}]
;; Aside
(when left-sidebar?
[:& left-sidebar {:file file :page page :layout layout}])
[:& left-sidebar
{:file file
:page-id page-id
:project project
:layout layout}])
(when right-sidebar?
[:& right-sidebar {:page page
:local local
:layout layout}])]))
[:& right-sidebar
{:page-id page-id
:file-id (:id file)
:local local
:layout layout}])]))
(defn trimmed-page-ref
[id]
(l/derived (fn [state]
(let [page-id (:current-page-id state)
data (:workspace-data state)]
(select-keys (get-in data [:pages-index page-id]) [:id :name])))
st/state =))
(mf/defc workspace-page
[{:keys [project file layout page-id] :as props}]
(mf/use-effect
(mf/deps page-id)
(fn []
(st/emit! (dw/initialize-page page-id))
#(st/emit! (dw/finalize-page page-id))))
(when-let [page (mf/deref refs/workspace-page)]
[:& workspace-content {:page page
:project project
:file file
:layout layout}]))
(let [page-ref (mf/use-memo (mf/deps page-id) #(trimmed-page-ref page-id))
page (mf/deref page-ref)]
(when page
[:& workspace-content {:page page
:page-id (:id page)
:project project
:file file
:layout layout}])))
(mf/defc workspace-loader
[]
@ -102,6 +118,7 @@
(mf/defc workspace
[{:keys [project-id file-id page-id] :as props}]
(mf/use-effect #(st/emit! dw/initialize-layout))
(mf/use-effect
(mf/deps project-id file-id)
(fn []
@ -110,11 +127,13 @@
(hooks/use-shortcuts dw/shortcuts)
(let [file (mf/deref refs/workspace-file)
(let [file (mf/deref refs/workspace-file)
project (mf/deref refs/workspace-project)
layout (mf/deref refs/workspace-layout)]
layout (mf/deref refs/workspace-layout)]
[:section#workspace
[:& header {:file file
:page-id page-id
:project project
:layout layout}]
@ -122,8 +141,9 @@
(if (and (and file project)
(:initialized file))
[:& workspace-page {:file file
[:& workspace-page {:page-id page-id
:project project
:layout layout
:page-id page-id}]
:file file
:layout layout}]
[:& workspace-loader])]))

View file

@ -155,9 +155,8 @@
[:span.right-arrow {:on-click on-right-arrow-click} i/arrow-slide]]))
(mf/defc colorpalette
[{:keys [left-sidebar? project] :as props}]
(let [team-id (:team-id project)
palettes (->> (mf/deref palettes-ref)
[{:keys [left-sidebar? team-id] :as props}]
(let [palettes (->> (mf/deref palettes-ref)
(vals)
(mapcat identity))
selected (or (mf/deref selected-palette-ref)

View file

@ -5,8 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2017 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2017 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.header
(:require
@ -150,15 +149,14 @@
;; --- Header Component
(mf/defc header
[{:keys [file layout project] :as props}]
[{:keys [file layout project page-id] :as props}]
(let [locale (i18n/use-locale)
team-id (:team-id project)
go-back #(st/emit! (rt/nav :dashboard-team {:team-id team-id}))
zoom (mf/deref refs/selected-zoom)
page (mf/deref refs/workspace-page)
locale (i18n/use-locale)
router (mf/deref refs/router)
view-url (rt/resolve router :viewer {:page-id (:id page)} {:index 0})]
view-url (rt/resolve router :viewer {:page-id page-id :file-id (:id file)} {:index 0})]
[:header.workspace-header
[:div.main-icon
[:a {:on-click go-back} i/logo-icon]]

View file

@ -5,8 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.left-toolbar
(:require
@ -23,7 +22,7 @@
;; --- Component: Left toolbar
(mf/defc left-toolbar
[{:keys [page layout] :as props}]
[{:keys [layout] :as props}]
(let [file-input (mf/use-ref nil)
selected-drawtool (mf/deref refs/selected-drawing-tool)
select-drawtool #(st/emit! :interrupt

View file

@ -10,8 +10,11 @@
(ns app.main.ui.workspace.presence
(:require
[rumext.alpha :as mf]
[beicon.core :as rx]
[app.main.refs :as refs]
[app.main.store :as st]
[app.util.time :as dt]
[app.util.timers :as tm]
[app.util.router :as rt]))
(def pointer-icon-path
@ -52,12 +55,21 @@
(mf/defc active-cursors
{::mf/wrap [mf/memo]}
[{:keys [page] :as props}]
(let [sessions (mf/deref refs/workspace-presence)
[{:keys [page-id] :as props}]
(let [counter (mf/use-state 0)
sessions (mf/deref refs/workspace-presence)
sessions (->> (vals sessions)
(filter #(= (:id page) (:page-id %))))]
(filter #(= page-id (:page-id %)))
(filter #(>= 3000 (- (inst-ms (dt/now)) (inst-ms (:updated-at %))))))]
(mf/use-effect
nil
(fn []
(let [sem (tm/schedule 1000 #(swap! counter inc))]
(fn [] (rx/dispose! sem)))))
(for [session sessions]
[:& session-cursor {:session session :key (:id session)}])))
(when (:point session)
[:& session-cursor {:session session :key (:id session)}]))))
(mf/defc session-widget
[{:keys [session self?] :as props}]
@ -72,10 +84,13 @@
(mf/defc active-sessions
{::mf/wrap [mf/memo]}
[]
(let [profile (mf/deref refs/profile)
(let [profile (mf/deref refs/profile)
sessions (mf/deref refs/workspace-presence)]
[:ul.active-users
(for [session (vals sessions)]
[:& session-widget {:session session :key (:id session)}])]))
[:& session-widget
{:session session
:self? (= (:id session) (:id profile))
:key (:id session)}])]))

View file

@ -9,6 +9,7 @@
(ns app.main.ui.workspace.shapes.frame
(:require
[okulary.core :as l]
[rumext.alpha :as mf]
[app.common.data :as d]
[app.main.constants :as c]
@ -43,6 +44,10 @@
(recur (first ids) (rest ids))
false))))))
(defn make-selected-ref
[id]
(l/derived #(contains? % id) refs/selected-shapes))
(defn frame-wrapper-factory
[shape-wrapper]
(let [frame-shape (frame/frame-shape shape-wrapper)]
@ -55,7 +60,7 @@
objects (unchecked-get props "objects")
selected-iref (mf/use-memo (mf/deps (:id shape))
#(refs/make-selected (:id shape)))
#(make-selected-ref (:id shape)))
selected? (mf/deref selected-iref)
zoom (mf/deref refs/selected-zoom)

View file

@ -14,34 +14,38 @@
[app.main.ui.workspace.sidebar.history :refer [history-toolbox]]
[app.main.ui.workspace.sidebar.layers :refer [layers-toolbox]]
[app.main.ui.workspace.sidebar.options :refer [options-toolbox]]
[app.main.ui.workspace.sidebar.sitemap :refer [sitemap-toolbox]]
[app.main.ui.workspace.sidebar.sitemap :refer [sitemap]]
[app.main.ui.workspace.sidebar.assets :refer [assets-toolbox]]))
;; --- Left Sidebar (Component)
(mf/defc left-sidebar
{:wrap [mf/memo]}
[{:keys [layout page file] :as props}]
[{:keys [layout page-id file project] :as props}]
[:aside.settings-bar.settings-bar-left
[:div.settings-bar-inside
{:data-layout (str/join "," layout)}
(when (contains? layout :sitemap)
[:& sitemap-toolbox {:file file
:page page
:layout layout}])
(when (contains? layout :document-history)
[:& history-toolbox])
[:& sitemap {:file file
:page-id page-id
:layout layout}])
#_(when (contains? layout :document-history)
[:& history-toolbox])
(when (contains? layout :layers)
[:& layers-toolbox {:page page}])
[:& layers-toolbox])
(when (contains? layout :assets)
[:& assets-toolbox])]])
[:& assets-toolbox {:team-id (:team-id project)
:file file}])]])
;; --- Right Sidebar (Component)
;; TODO: revisit page prop
(mf/defc right-sidebar
[{:keys [layout page local] :as props}]
[{:keys [layout page-id file-id local] :as props}]
[:aside#settings-bar.settings-bar
[:div.settings-bar-inside
(when (contains? layout :element-options)
[:& options-toolbox {:page page
:local local}])]])
[:& options-toolbox {:page-id page-id
:file-id file-id
:local local}])]])

View file

@ -9,35 +9,35 @@
(ns app.main.ui.workspace.sidebar.assets
(:require
[okulary.core :as l]
[cuerdas.core :as str]
[rumext.alpha :as mf]
[app.config :as cfg]
[app.common.data :as d]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as geom]
[app.common.media :as cm]
[app.common.pages :as cp]
[app.common.geom.shapes :as geom]
[app.common.geom.point :as gpt]
[app.main.ui.icons :as i]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.workspace :as dw]
[app.main.data.colors :as dcol]
[app.main.data.workspace.libraries :as dwl]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.colorpicker :refer [colorpicker most-used-colors]]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd]
[app.main.ui.modal :as modal]
[app.main.ui.shapes.icon :as icon]
[app.main.ui.workspace.libraries :refer [libraries-dialog]]
[app.util.data :refer [matches-search]]
[app.util.dom :as dom]
[app.util.dom.dnd :as dnd]
[app.util.timers :as timers]
[app.common.uuid :as uuid]
[app.util.i18n :as i18n :refer [tr]]
[app.util.data :refer [classnames matches-search]]
[app.util.i18n :as i18n :refer [tr t]]
[app.util.router :as rt]
[app.main.ui.modal :as modal]
[app.main.ui.colorpicker :refer [colorpicker most-used-colors]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.context-menu :refer [context-menu]]
[app.main.ui.workspace.libraries :refer [libraries-dialog]]))
[app.util.timers :as timers]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.alpha :as mf]))
(mf/defc modal-edit-color
[{:keys [color-value on-accept on-cancel] :as ctx}]
@ -67,58 +67,73 @@
[:a.close {:href "#" :on-click cancel} i/close]]])))
(mf/defc graphics-box
[{:keys [file-id local-library? media-objects] :as props}]
(let [state (mf/use-state {:menu-open false
:top nil
:left nil
:object-id nil})
file-input (mf/use-ref nil)
[{:keys [file-id local? objects] :as props}]
(let [input-ref (mf/use-ref nil)
state (mf/use-state {:menu-open false
:top nil
:left nil
:object-id nil})
add-graphic
#(dom/click (mf/ref-val file-input))
(mf/use-callback
(fn [] (dom/click (mf/ref-val input-ref))))
delete-graphic
#(st/emit! (dw/delete-media-object file-id (:object-id @state)))
on-media-uploaded
(mf/use-callback
(mf/deps file-id)
(fn [data]
(st/emit! (dwl/add-media data))))
on-files-selected
(fn [js-files]
(let [params {:file-id file-id
:local? false
:js-files js-files}]
(st/emit! (dw/upload-media-objects params))))
on-selected
(mf/use-callback
(mf/deps file-id)
(fn [js-files]
(let [params (with-meta {:file-id file-id
:local? false
:js-files js-files}
{:on-success on-media-uploaded})]
(st/emit! (dw/upload-media-objects params)))))
on-delete
(mf/use-callback
(mf/deps state)
(fn []
(let [params {:id (:object-id @state)}]
(st/emit! (dwl/delete-media params)))))
on-context-menu
(fn [object-id]
(fn [event]
(when local-library?
(let [pos (dom/get-client-position event)
top (:y pos)
left (- (:x pos) 20)]
(dom/prevent-default event)
(swap! state assoc :menu-open true
:top top
:left left
:object-id object-id)))))
(mf/use-callback
(fn [object-id]
(fn [event]
(when local?
(let [pos (dom/get-client-position event)
top (:y pos)
left (- (:x pos) 20)]
(dom/prevent-default event)
(swap! state assoc :menu-open true
:top top
:left left
:object-id object-id))))))
on-drag-start
(fn [path event]
(dnd/set-data! event "text/uri-list" (cfg/resolve-media-path path))
(dnd/set-allowed-effect! event "move"))]
(mf/use-callback
(fn [path event]
(dnd/set-data! event "text/uri-list" (cfg/resolve-media-path path))
(dnd/set-allowed-effect! event "move")))]
[:div.asset-group
[:div.group-title
(tr "workspace.assets.graphics")
[:span (str "\u00A0(") (count media-objects) ")"] ;; Unicode 00A0 is non-breaking space
(when local-library?
[:span (str "\u00A0(") (count objects) ")"] ;; Unicode 00A0 is non-breaking space
(when local?
[:div.group-button {:on-click add-graphic}
i/plus
[:& file-uploader {:accept cm/str-media-types
:multi true
:input-ref file-input
:on-selected on-files-selected}]])]
:input-ref input-ref
:on-selected on-selected}]])]
[:div.group-grid
(for [object media-objects]
(for [object objects]
[:div.grid-cell {:key (:id object)
:draggable true
:on-context-menu (on-context-menu (:id object))
@ -127,39 +142,37 @@
:draggable false}] ;; Also need to add css pointer-events: none
[:div.cell-name (:name object)]])
(when local-library?
(when local?
[:& context-menu
{:selectable false
:show (:menu-open @state)
:on-close #(swap! state assoc :menu-open false)
:top (:top @state)
:left (:left @state)
:options [[(tr "workspace.assets.delete") delete-graphic]]}])]]))
:options [[(tr "workspace.assets.delete") on-delete]]}])]]))
(mf/defc color-item
[{:keys [color file-id local-library?] :as props}]
(let [workspace-local @refs/workspace-local
color-for-rename (:color-for-rename workspace-local)
edit-input-ref (mf/use-ref)
state (mf/use-state {:menu-open false
:top nil
:left nil
:editing (= color-for-rename (:id color))})
[{:keys [color local? locale] :as props}]
(let [rename? (= (:color-for-rename @refs/workspace-local) (:id color))
id (:id color)
input-ref (mf/use-ref)
state (mf/use-state {:menu-open false
:top nil
:left nil
:editing rename?})
rename-color
(fn [name]
(st/emit! (dcol/rename-color file-id (:id color) name)))
(st/emit! (dwl/update-color (assoc color :name name))))
edit-color
(fn [value opacity]
(st/emit! (dcol/update-color file-id (:id color) value)))
(st/emit! (dwl/update-color (assoc color :value name))))
delete-color
(fn []
(st/emit! (dcol/delete-color file-id (:id color))))
(st/emit! (dwl/delete-color color)))
rename-color-clicked
(fn [event]
@ -171,13 +184,13 @@
(let [target (dom/event->target event)
name (dom/get-value target)]
(rename-color name)
(st/emit! dcol/clear-color-for-rename)
(st/emit! dwl/clear-color-for-rename)
(swap! state assoc :editing false)))
input-key-down
(fn [event]
(when (kbd/esc? event)
(st/emit! dcol/clear-color-for-rename)
(st/emit! dwl/clear-color-for-rename)
(swap! state assoc :editing false))
(when (kbd/enter? event)
(input-blur event)))
@ -185,12 +198,12 @@
edit-color-clicked
(fn [event]
(modal/show! modal-edit-color
{:color-value (:content color)
{:color-value (:value color)
:on-accept edit-color}))
on-context-menu
(fn [event]
(when local-library?
(when local?
(let [pos (dom/get-client-position event)
top (:y pos)
left (- (:x pos) 20)]
@ -203,16 +216,16 @@
(mf/use-effect
(mf/deps (:editing @state))
#(when (:editing @state)
(let [edit-input (mf/ref-val edit-input-ref)]
(dom/select-text! edit-input))
(let [input (mf/ref-val input-ref)]
(dom/select-text! input))
nil))
[:div.group-list-item {:on-context-menu on-context-menu}
[:div.color-block {:style {:background-color (:content color)}}]
[:div.color-block {:style {:background-color (:value color)}}]
(if (:editing @state)
[:input.element-name
{:type "text"
:ref edit-input-ref
:ref input-ref
:on-blur input-blur
:on-key-down input-key-down
:auto-focus true
@ -220,179 +233,201 @@
[:div.name-block
{:on-double-click rename-color-clicked}
(:name color)
(when-not (= (:name color) (:content color))
[:span (:content color)])])
(when local-library?
(when-not (= (:name color) (:value color))
[:span (:value color)])])
(when local?
[:& context-menu
{:selectable false
:show (:menu-open @state)
:on-close #(swap! state assoc :menu-open false)
:top (:top @state)
:left (:left @state)
:options [[(tr "workspace.assets.rename") rename-color-clicked]
[(tr "workspace.assets.edit") edit-color-clicked]
[(tr "workspace.assets.delete") delete-color]]}])]))
:options [[(t locale "workspace.assets.rename") rename-color-clicked]
[(t locale "workspace.assets.edit") edit-color-clicked]
[(t locale "workspace.assets.delete") delete-color]]}])]))
(mf/defc colors-box
[{:keys [file-id local-library? colors] :as props}]
[{:keys [file-id local? colors locale] :as props}]
(let [add-color
(fn [value opacity]
(st/emit! (dcol/create-color file-id value)))
(mf/use-callback
(mf/deps file-id)
(fn [value opacity]
(st/emit! (dwl/add-color value))))
add-color-clicked
(fn [event]
(modal/show! modal-edit-color
{:color-value "#406280"
:on-accept add-color}))]
(mf/use-callback
(mf/deps file-id)
(fn [event]
(modal/show! modal-edit-color
{:color-value "#406280"
:on-accept add-color})))]
[:div.asset-group
[:div.group-title
(tr "workspace.assets.colors")
[:span (str "\u00A0(") (count colors) ")"] ;; Unicode 00A0 is non-breaking space
(when local-library?
[:div.group-button {:on-click add-color-clicked} i/plus])]
(t locale "workspace.assets.colors")
[:span (str "\u00A0(") (count colors) ")"] ;; Unicode 00A0 is non-breaking space
(when local?
[:div.group-button {:on-click add-color-clicked} i/plus])]
[:div.group-list
(for [color colors]
[:& color-item {:key (:id color)
:color color
:file-id file-id
:local-library? local-library?}])]]))
:local? local?}])]]))
(mf/defc file-library-toolbox
[{:keys [library
local-library?
shared?
media-objects
colors
initial-open?
search-term
box-filter] :as props}]
(let [open? (mf/use-state initial-open?)
(defn file-colors-ref
[id]
(l/derived (fn [state]
(let [wfile (:workspace-file state)]
(if (= (:id wfile) id)
(vals (get-in wfile [:data :colors]))
(vals (get-in state [:workspace-libraries id :data :colors])))))
st/state =))
(defn file-media-ref
[id]
(l/derived (fn [state]
(let [wfile (:workspace-file state)]
(if (= (:id wfile) id)
(vals (get-in wfile [:data :media]))
(vals (get-in state [:workspace-libraries id :data :media])))))
st/state =))
(defn apply-filters
[coll filters]
(filter (fn [item]
(or (matches-search (:name item "!$!") (:term filters))
(matches-search (:value item "!$!") (:term filters))))
coll))
(mf/defc file-library
[{:keys [file local? open? filters locale] :as props}]
(let [open? (mf/use-state open?)
shared? (:is-shared file)
router (mf/deref refs/router)
toggle-open #(swap! open? not)
router (mf/deref refs/router)
library-url (rt/resolve router :workspace
{:project-id (:project-id library)
:file-id (:id library)}
{:page-id (first (:pages library))})]
url (rt/resolve router :workspace
{:project-id (:project-id file)
:file-id (:id file)}
{:page-id (get-in file [:data :pages 0])})
colors-ref (mf/use-memo (mf/deps (:id file)) #(file-colors-ref (:id file)))
colors (apply-filters (mf/deref colors-ref) filters)
media-ref (mf/use-memo (mf/deps (:id file)) #(file-media-ref (:id file)))
media (apply-filters (mf/deref media-ref) filters)]
[:div.tool-window
[:div.tool-window-bar
[:div.collapse-library
{:class (classnames :open @open?)
{:class (dom/classnames :open @open?)
:on-click toggle-open}
i/arrow-slide]
(if local-library?
(if local?
[:*
[:span (tr "workspace.assets.file-library")]
[:span (t locale "workspace.assets.file-library")]
(when shared?
[:span.tool-badge (tr "workspace.assets.shared")])]
[:span.tool-badge (t locale "workspace.assets.shared")])]
[:*
[:span (:name library)]
[:span (:name file)]
[:span.tool-link
[:a {:href (str "#" library-url) :target "_blank"} i/chain]]])]
[:a {:href (str "#" url) :target "_blank"} i/chain]]])]
(when @open?
(let [show-graphics (and (or (= box-filter :all) (= box-filter :graphics))
(or (> (count media-objects) 0) (str/empty? search-term)))
show-colors (and (or (= box-filter :all) (= box-filter :colors))
(or (> (count colors) 0) (str/empty? search-term)))]
(let [show-graphics? (and (or (= (:box filters) :all)
(= (:box filters) :graphics))
(or (> (count media) 0)
(str/empty? (:term filters))))
show-colors? (and (or (= (:box filters) :all)
(= (:box filters) :colors))
(or (> (count colors) 0)
(str/empty? (:term filters))))]
[:div.tool-window-content
(when show-graphics
[:& graphics-box {:file-id (:id library)
:local-library? local-library?
:media-objects media-objects}])
(when show-colors
[:& colors-box {:file-id (:id library)
:local-library? local-library?
(when show-graphics?
[:& graphics-box {:file-id (:id file)
:local? local?
:objects media}])
(when show-colors?
[:& colors-box {:file-id (:id file)
:local? local?
:locale locale
:colors colors}])
(when (and (not show-graphics) (not show-colors))
(when (and (not show-graphics?) (not show-colors?))
[:div.asset-group
[:div.group-title (tr "workspace.assets.not-found")]])]))]))
[:div.group-title (t locale "workspace.assets.not-found")]])]))]))
(mf/defc assets-toolbox
[]
(let [team-id (-> refs/workspace-project mf/deref :team-id)
file (mf/deref refs/workspace-file)
libraries (mf/deref refs/workspace-libraries)
sorted-libraries (->> (vals libraries)
(sort-by #(str/lower (:name %))))
[{:keys [team-id file] :as props}]
(let [libraries (mf/deref refs/workspace-libraries)
locale (mf/deref i18n/locale)
filters (mf/use-state {:term "" :box :all})
state (mf/use-state {:search-term ""
:box-filter :all})
on-search-term-change
(mf/use-callback
(mf/deps team-id)
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value))]
(swap! filters assoc :term value))))
filtered-media-objects (fn [library-id]
(as-> libraries $$
(assoc $$ (:id file) file)
(get-in $$ [library-id :media-objects])
(filter #(matches-search (:name %) (:search-term @state)) $$)
(sort-by #(str/lower (:name %)) $$)))
on-search-clear-click
(mf/use-callback
(mf/deps team-id)
(fn [event]
(swap! filters assoc :term "")))
filtered-colors (fn [library-id]
(as-> libraries $$
(assoc $$ (:id file) file)
(get-in $$ [library-id :colors])
(filter #(or (matches-search (:name %) (:search-term @state))
(matches-search (:content %) (:search-term @state))) $$)
(sort-by #(str/lower (:name %)) $$)))
on-search-term-change (fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value))]
(swap! state assoc :search-term value)))
on-search-clear-click (fn [event]
(swap! state assoc :search-term ""))
on-box-filter-change (fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/read-string))]
(swap! state assoc :box-filter value)))]
on-box-filter-change
(mf/use-callback
(mf/deps team-id)
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/read-string))]
(swap! filters assoc :box value))))]
[:div.assets-bar
[:div.tool-window
[:div.tool-window-content
[:div.assets-bar-title
(tr "workspace.assets.assets")
(t locale "workspace.assets.assets")
[:div.libraries-button {:on-click #(modal/show! libraries-dialog {})}
i/libraries
(tr "workspace.assets.libraries")]]
(t locale "workspace.assets.libraries")]]
[:div.search-block
[:input.search-input
{:placeholder (tr "workspace.assets.search")
:type "text"
:value (:search-term @state)
:value (:term @filters)
:on-change on-search-term-change}]
(if (str/empty? (:search-term @state))
(if (str/empty? (:term @filters))
[:div.search-icon
i/search]
[:div.search-icon.close
{:on-click on-search-clear-click}
i/close])]
[:select.input-select {:value (:box-filter @state)
[:select.input-select {:value (:box @filters)
:on-change on-box-filter-change}
[:option {:value ":all"} (tr "workspace.assets.box-filter-all")]
[:option {:value ":graphics"} (tr "workspace.assets.box-filter-graphics")]
[:option {:value ":colors"} (tr "workspace.assets.box-filter-colors")]]]]
[:option {:value ":all"} (t locale "workspace.assets.box-filter-all")]
[:option {:value ":graphics"} (t locale "workspace.assets.box-filter-graphics")]
[:option {:value ":colors"} (t locale "workspace.assets.box-filter-colors")]]]]
[:& file-library-toolbox {:key (:id file)
:library file
:local-library? true
:shared? (:is-shared file)
:media-objects (filtered-media-objects (:id file))
:colors (filtered-colors (:id file))
:initial-open? true
:search-term (:search-term @state)
:box-filter (:box-filter @state)}]
(for [library sorted-libraries]
[:& file-library-toolbox {:key (:id library)
:library library
:local-library? false
:shared? (:is-shared library)
:media-objects (filtered-media-objects (:id library))
:colors (filtered-colors (:id library))
:initial-open? false
:search-term (:search-term @state)
:box-filter (:box-filter @state)}])]))
[:& file-library
{:file file
:locale locale
:local? true
:open? true
:filters @filters}]
(for [file (->> (vals libraries)
(sort-by #(str/lower (:name %))))]
[:& file-library
{:key (:id file)
:file file
:local? false
:open? false
:filters @filters}])]))

View file

@ -5,30 +5,29 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.sidebar.layers
(:require
[okulary.core :as l]
[rumext.alpha :as mf]
[beicon.core :as rx]
[app.main.ui.icons :as i]
[app.common.data :as d]
[app.common.uuid :as uuid]
[app.common.pages :as cp]
[app.common.pages-helpers :as cph]
[app.common.uuid :as uuid]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd]
[app.main.ui.shapes.icon :as icon]
[app.util.object :as obj]
[app.util.dom :as dom]
[app.util.timers :as ts]
[app.util.i18n :as i18n :refer [t]]
[app.util.perf :as perf]))
[app.util.object :as obj]
[app.util.perf :as perf]
[app.util.timers :as ts]
[beicon.core :as rx]
[okulary.core :as l]
[rumext.alpha :as mf]))
;; --- Helpers
@ -305,15 +304,13 @@
(mf/defc layers-toolbox
{:wrap [mf/memo]}
[{:keys [page] :as props}]
[]
(let [locale (mf/deref i18n/locale)
data (mf/deref refs/workspace-data)
on-click #(st/emit! (dw/toggle-layout-flags :layers))]
page (mf/deref refs/workspace-page)]
[:div#layers.tool-window
[:div.tool-window-bar
[:div.tool-window-icon i/layers]
[:span (:name page)]
#_[:div.tool-window-close {:on-click on-click} i/close]]
[:span (:name page)]]
[:div.tool-window-content
[:& layers-tree-wrapper {:key (:id page)
:objects (:objects data)}]]]))
:objects (:objects page)}]]]))

View file

@ -37,7 +37,7 @@
(mf/defc shape-options
{::mf/wrap [#(mf/throttle % 60)]}
[{:keys [shape shapes-with-children page] :as props}]
[{:keys [shape shapes-with-children page-id file-id]}]
[:*
(case (:type shape)
:frame [:& frame/options {:shape shape}]
@ -50,12 +50,15 @@
:curve [:& path/options {:shape shape}]
:image [:& image/options {:shape shape}]
nil)
[:& exports-menu {:shape shape :page page}]])
[:& exports-menu
{:shape shape
:page-id page-id
:file-id file-id}]])
(mf/defc options-content
{::mf/wrap [mf/memo]}
[{:keys [section shapes shapes-with-children page] :as props}]
[{:keys [selected section shapes shapes-with-children page-id file-id]}]
(let [locale (mf/deref i18n/locale)]
[:div.tool-window
[:div.tool-window-content
@ -65,10 +68,11 @@
:title (t locale "workspace.options.design")}
[:div.element-options
[:& align-options]
(case (count shapes)
0 [:& page/options {:page page}]
(case (count selected)
0 [:& page/options {:page-id page-id}]
1 [:& shape-options {:shape (first shapes)
:page page
:page-id page-id
:file-id file-id
:shapes-with-children shapes-with-children}]
[:& multiple/options {:shapes shapes-with-children}])]]
@ -78,14 +82,20 @@
[:& interactions-menu {:shape (first shapes)}]]]]]]))
;; TODO: this need optimizations, selected-objects and
;; selected-objects-with-children are derefed always but they only
;; need on multiple selection in majority of cases
(mf/defc options-toolbox
{::mf/wrap [mf/memo]}
[{:keys [page local] :as props}]
[{:keys [page-id file-id local] :as props}]
(let [section (:options-mode local)
shapes (mf/deref refs/selected-objects)
shapes-with-children (mf/deref refs/selected-objects-with-children)]
[:& options-content {:shapes shapes
:selected (:selected local)
:shapes-with-children shapes-with-children
:page page
:file-id file-id
:page-id page-id
:section section}]))

View file

@ -29,6 +29,7 @@
:response-type :blob
:auth true
:body {:page-id (:page-id shape)
:file-id (:file-id shape)
:object-id (:id shape)
:name (:name shape)
:exports exports}}))
@ -45,7 +46,7 @@
(.remove link)))
(mf/defc exports-menu
[{:keys [shape page] :as props}]
[{:keys [shape page-id file-id] :as props}]
(let [locale (mf/deref i18n/locale)
exports (:exports shape [])
loading? (mf/use-state false)
@ -56,7 +57,7 @@
(fn [event]
(dom/prevent-default event)
(swap! loading? not)
(->> (request-export (assoc shape :page-id (:id page)) exports)
(->> (request-export (assoc shape :page-id page-id :file-id file-id) exports)
(rx/subs
(fn [{:keys [status body] :as response}]
(js/console.log status body)

View file

@ -23,7 +23,7 @@
(mf/defc interactions-menu
[{:keys [shape] :as props}]
(let [locale (mf/deref i18n/locale)
objects (deref refs/workspace-objects)
objects (deref refs/workspace-page-objects)
interaction (first (:interactions shape)) ; TODO: in the
; future we may
; have several

View file

@ -21,18 +21,17 @@
(def options-iref
(l/derived :options refs/workspace-data))
(defn use-change-color [page]
(defn use-change-color [page-id]
(mf/use-callback
(mf/deps page)
(mf/deps page-id)
(fn [value]
(st/emit! (dw/change-canvas-color value)))))
(mf/defc options
[{:keys [page] :as props}]
[{:keys [page-id] :as props}]
(let [locale (i18n/use-locale)
options (mf/deref refs/workspace-page-options)
handle-change-color (use-change-color page)]
handle-change-color (use-change-color page-id)]
[:div.element-set
[:div.element-set-title (t locale "workspace.options.canvas-background")]
[:div.element-set-content

View file

@ -2,96 +2,104 @@
;; 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) 2015-2019 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.main.ui.workspace.sidebar.sitemap
(:require
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.alpha :as mf]
[app.common.data :as d]
[app.main.ui.icons :as i]
[app.main.data.workspace :as dw]
[app.main.store :as st]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.confirm :refer [confirm-dialog]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.main.ui.keyboard :as kbd]
[app.main.ui.modal :as modal]
[app.main.ui.hooks :as hooks]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [t]]
[app.util.router :as rt]))
[app.util.router :as rt]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.alpha :as mf]))
;; --- Page Item
(mf/defc page-item
[{:keys [page index deletable? selected?] :as props}]
(let [local (mf/use-state {})
edit-input-ref (mf/use-ref)
(let [local (mf/use-state {})
input-ref (mf/use-ref)
id (:id page)
delete-fn (mf/use-callback (mf/deps id) #(st/emit! (dw/delete-page id)))
on-delete (mf/use-callback (mf/deps id) #(modal/show! confirm-dialog {:on-accept delete-fn}))
navigate-fn (mf/use-callback (mf/deps id) #(st/emit! (dw/go-to-page id)))
on-double-click
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(swap! local assoc :edition true))
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(swap! local assoc :edition true)))
on-blur
(mf/use-callback
(fn [event]
(let [target (dom/event->target event)
name (dom/get-value target)]
(st/emit! (dw/rename-page id name))
(swap! local assoc :edition false))))
on-key-down
(mf/use-callback
(fn [event]
(let [target (dom/event->target event)
name (dom/get-value target)]
(st/emit! (dw/rename-page (:id page) name))
(swap! local assoc :edition false)))
(cond
(kbd/enter? event)
(on-blur event)
on-key-down (fn [event]
(cond
(kbd/enter? event)
(on-blur event)
(kbd/esc? event)
(swap! local assoc :edition false)))
delete-fn #(st/emit! (dw/delete-page (:id page)))
on-delete #(do
(dom/prevent-default %)
(dom/stop-propagation %)
(modal/show! confirm-dialog {:on-accept delete-fn}))
navigate-fn #(st/emit! (dw/go-to-page (:id page)))
(kbd/esc? event)
(swap! local assoc :edition false))))
on-drop
(fn [side {:keys [id name] :as data}]
(let [index (if (= :bot side) (inc index) index)]
(st/emit! (dw/relocate-page id index))))
(mf/use-callback
(mf/deps id)
(fn [side {:keys [id name] :as data}]
(let [index (if (= :bot side) (inc index) index)]
(st/emit! (dw/relocate-page id index)))))
[dprops dref] (hooks/use-sortable
:data-type "app/page"
:on-drop on-drop
:data {:id (:id page)
:index index
:name (:name page)})]
[dprops dref]
(hooks/use-sortable
:data-type "app/page"
:on-drop on-drop
:data {:id id
:index index
:name (:name page)})]
(mf/use-effect
(mf/deps (:edition @local))
#(when (:edition @local)
(let [edit-input (mf/ref-val edit-input-ref)]
(mf/use-layout-effect
(mf/deps (:edition @local))
(fn []
(when (:edition @local)
(let [edit-input (mf/ref-val input-ref)]
(dom/select-text! edit-input))
nil))
nil)))
[:li {:class (dom/classnames
:selected selected?
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))
:ref dref}
[:div.element-list-body {:class (dom/classnames
:selected selected?)
:on-click navigate-fn
:on-double-click on-double-click}
[:div.element-list-body
{:class (dom/classnames
:selected selected?)
:on-click navigate-fn
:on-double-click on-double-click}
[:div.page-icon i/file-html]
(if (:edition @local)
[:*
[:input.element-name {:type "text"
:ref edit-input-ref
:ref input-ref
:on-blur on-blur
:on-key-down on-key-down
:auto-focus true
@ -105,18 +113,17 @@
;; --- Page Item Wrapper
(defn- make-page-iref
[id]
#(l/derived (fn [state]
(let [page (get-in state [:workspace-pages id])]
(select-keys page [:id :name :ordering])))
(defn- make-page-ref
[page-id]
(l/derived (fn [state]
(let [page (get-in state [:workspace-file :data :pages-index page-id])]
(select-keys page [:id :name])))
st/state =))
(mf/defc page-item-wrapper
[{:keys [page-id index deletable? selected?] :as props}]
(let [page-iref (mf/use-memo (mf/deps page-id)
(make-page-iref page-id))
page (mf/deref page-iref)]
[{:keys [file-id page-id index deletable? selected?] :as props}]
(let [page-ref (mf/use-memo (mf/deps page-id) #(make-page-ref page-id))
page (mf/deref page-ref)]
[:& page-item {:page page
:index index
:deletable? deletable?
@ -125,33 +132,35 @@
;; --- Pages List
(mf/defc pages-list
[{:keys [file current-page] :as props}]
(let [pages (d/enumerate (:pages file))
[{:keys [file current-page-id] :as props}]
(let [pages (:pages file)
deletable? (> (count pages) 1)]
[:ul.element-list
[:& hooks/sortable-container {}
(for [[index page-id] pages]
[:& page-item-wrapper
{:page-id page-id
:index index
:deletable? deletable?
:selected? (= page-id (:id current-page))
:key page-id}])]]))
(for [[index page-id] (d/enumerate pages)]
[:& page-item-wrapper
{:page-id page-id
:index index
:deletable? deletable?
:selected? (= page-id current-page-id)
:key page-id}])]]))
;; --- Sitemap Toolbox
(mf/defc sitemap-toolbox
[{:keys [file page layout] :as props}]
(let [on-create-click #(st/emit! dw/create-empty-page)
toggle-layout #(st/emit! (dw/toggle-layout-flags %))
locale (i18n/use-locale)]
(mf/defc sitemap
[{:keys [file page-id layout] :as props}]
(let [create (mf/use-callback #(st/emit! dw/create-empty-page))
collapse (mf/use-callback #(st/emit! (dw/toggle-layout-flags :sitemap-pages)))
locale (mf/deref i18n/locale)]
[:div.sitemap.tool-window
[:div.tool-window-bar
[:span (t locale "workspace.sidebar.sitemap")]
[:div.add-page {:on-click on-create-click} i/close]
[:div.collapse-pages {:on-click #(st/emit! (dw/toggle-layout-flags :sitemap-pages))}
i/arrow-slide]]
[:div.add-page {:on-click create} i/close]
[:div.collapse-pages {:on-click collapse} i/arrow-slide]]
(when (contains? layout :sitemap-pages)
[:div.tool-window-content
[:& pages-list {:file file :current-page page}]])]))
[:& pages-list
{:file file
:key (:id file)
:current-page-id page-id}]])]))

View file

@ -132,7 +132,7 @@
:frame-id (:id frame)
:rect (gsh/pad-selrec (areas side))})
(rx/map #(set/difference % selected))
(rx/map #(->> % (map (partial get @refs/workspace-objects))))))]
(rx/map #(->> % (map (partial get @refs/workspace-page-objects))))))]
(->> (query-side lt-side)
(rx/combine-latest vector (query-side gt-side)))))
@ -213,29 +213,25 @@
:coord coord
:zoom zoom}])))
(mf/defc snap-distances [{:keys [layout]}]
(let [page-id (mf/deref refs/workspace-page-id)
selected (mf/deref refs/selected-shapes)
shapes (->> (refs/objects-by-id selected)
(mf/deref)
(map gsh/transform-shape))
selrect (gsh/selection-rect shapes)
frame-id (-> shapes first :frame-id)
frame (mf/deref (refs/object-by-id frame-id))
zoom (mf/deref refs/selected-zoom)
current-transform (mf/deref refs/current-transform)
key (->> selected (map str) (str/join "-"))]
(when (and (contains? layout :dynamic-alignment)
(= current-transform :move)
(not (empty? selected)))
[:g.distance
(for [coord [:x :y]]
[:& shape-distance
{:key (str key (name coord))
:selrect selrect
:page-id page-id
:frame frame
:zoom zoom
:coord coord
:selected selected}])])))
(mf/defc snap-distances
[{:keys [layout page-id zoom selected transform]}]
(when (and (contains? layout :dynamic-alignment)
(= transform :move)
(not (empty? selected)))
(let [shapes (->> (refs/objects-by-id selected)
(mf/deref)
(map gsh/transform-shape))
selrect (gsh/selection-rect shapes)
frame-id (-> shapes first :frame-id)
frame (mf/deref (refs/object-by-id frame-id))
key (->> selected (map str) (str/join "-"))]
[:g.distance
(for [coord [:x :y]]
[:& shape-distance
{:key (str key (name coord))
:selrect selrect
:page-id page-id
:frame frame
:zoom zoom
:coord coord
:selected selected}])])))

View file

@ -116,6 +116,8 @@
(declare remote-user-cursors)
;; TODO: revisit the refs usage (vs props)
(mf/defc shape-outlines
{::mf/wrap-props false}
[props]
@ -135,13 +137,13 @@
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[props]
(let [data (mf/deref refs/workspace-data)
(let [data (mf/deref refs/workspace-page)
hover (unchecked-get props "hover")
selected (unchecked-get props "selected")
objects (:objects data)
root (get objects uuid/zero)
shapes (->> (:shapes root)
(map #(get objects %)))]
objects (:objects data)
root (get objects uuid/zero)
shapes (->> (:shapes root)
(map #(get objects %)))]
[:*
[:g.shapes
(for [item shapes]
@ -157,9 +159,8 @@
:hover hover}]]))
(mf/defc viewport
[{:keys [page local layout] :as props}]
(let [{:keys [drawing-tool
options-mode
[{:keys [page-id page local layout] :as props}]
(let [{:keys [options-mode
zoom
flags
vport
@ -169,9 +170,12 @@
selected
panning]} local
file (mf/deref refs/workspace-file)
viewport-ref (mf/use-ref nil)
file (mf/deref refs/workspace-file)
viewport-ref (mf/use-ref nil)
last-position (mf/use-var nil)
drawing (mf/deref refs/workspace-drawing)
drawing-tool (:tool drawing)
drawing-obj (:object drawing)
zoom (or zoom 1)
@ -462,7 +466,7 @@
:on-drop on-drop}
[:g
[:& frames {:key (:id page)
[:& frames {:key page-id
:hover (:hover local)
:selected (:selected selected)}]
@ -471,8 +475,8 @@
:zoom zoom
:edition edition}])
(when-let [drawing-shape (:drawing local)]
[:& draw-area {:shape drawing-shape
(when drawing-obj
[:& draw-area {:shape drawing-obj
:zoom zoom
:modifiers (:modifiers local)}])
@ -481,17 +485,21 @@
[:& snap-points {:layout layout
:transform (:transform local)
:drawing (:drawing local)
:drawing drawing-obj
:zoom zoom
:page-id (:id page)
:page-id page-id
:selected selected}]
[:& snap-distances {:layout layout}]
[:& snap-distances {:layout layout
:zoom zoom
:transform (:transform local)
:selected selected
:page-id page-id}]
(when tooltip
[:& cursor-tooltip {:zoom zoom :tooltip tooltip}])]
[:& presence/active-cursors {:page page}]
[:& presence/active-cursors {:page-id page-id}]
[:& selection-rect {:data (:selrect local)}]
(when (= options-mode :prototype)
[:& interactions {:selected selected}])]))

View file

@ -20,7 +20,8 @@
(when-not (nil? obj)
(unchecked-get obj k)))
([obj k default]
(or (get obj k) default)))
(let [result (get obj k)]
(if (undefined? result) default result))))
(defn get-in
[obj keys]

View file

@ -23,12 +23,12 @@
[message]
message)
(defmethod handler :create-page-indices
(defmethod handler :initialize-indices
[message]
(handler (-> message
(assoc :cmd :selection/create-index)))
(assoc :cmd :selection/initialize-index)))
(handler (-> message
(assoc :cmd :snaps/create-index))))
(assoc :cmd :snaps/initialize-index))))
(defmethod handler :update-page-indices
[message]

View file

@ -26,16 +26,15 @@
(declare index-object)
(declare create-index)
(defmethod impl/handler :selection/create-index
[{:keys [file-id pages] :as message}]
(defmethod impl/handler :selection/initialize-index
[{:keys [file-id data] :as message}]
(letfn [(index-page [state page]
(let [id (:id page)
objects (get-in page [:data :objects])]
(let [id (:id page)
objects (:objects page)]
(assoc state id (create-index objects))))
(update-state [state]
(reduce index-page state pages))]
(reduce index-page state (vals (:pages-index data))))]
(swap! state update-state)
nil))

View file

@ -65,19 +65,17 @@
(assoc state page-id snap-data)))
;; Public API
(defmethod impl/handler :snaps/create-index
[{:keys [file-id pages] :as message}]
(defmethod impl/handler :snaps/initialize-index
[{:keys [file-id data] :as message}]
;; Create the index
(letfn [(process-page [state page]
(let [id (:id page)
objects (get-in page [:data :objects])]
(let [id (:id page)
objects (:objects page)]
(index-page state id objects)))]
(swap! state #(reduce process-page % pages)))
;; (log-state)
;; Return nil so the worker will not answer anything back
nil)
(swap! state #(reduce process-page % (vals (:pages-index data))))
;; (log-state)
;; Return nil so the worker will not answer anything back
nil))
(defmethod impl/handler :snaps/update-index
[{:keys [page-id objects] :as message}]

View file

@ -32,23 +32,23 @@
:code (:error response)})))
(defn- request-page
[id]
[file-id page-id]
(let [uri "/api/w/query/page"]
(p/create
(fn [resolve reject]
(->> (http/send! {:uri uri
:query {:id id}
:query {:file-id file-id :id page-id}
:method :get})
(rx/mapcat handle-response)
(rx/subs (fn [body]
(resolve (:data body)))
(resolve body))
(fn [error]
(reject error))))))))
(defmethod impl/handler :thumbnails/generate
[{:keys [id] :as message}]
[{:keys [file-id page-id] :as message}]
(p/then
(request-page id)
(request-page file-id page-id)
(fn [data]
(let [elem (mf/element exports/page-svg #js {:data data
:width "290"