diff --git a/backend/src/uxbox/fixtures.clj b/backend/src/uxbox/fixtures.clj index 7cf776b3d..857dc2712 100644 --- a/backend/src/uxbox/fixtures.clj +++ b/backend/src/uxbox/fixtures.clj @@ -72,14 +72,16 @@ (defn create-page [conn [pjid paid uid]] (println "create page" paid "(for project=" pjid ", user=" uid ")") - (let [data {:shapes [{:id (mk-uuid "canvas" 1) - :name "Canvas-1" - :type :canvas - :page (mk-uuid "page" pjid paid uid) - :x1 200 - :y1 200 - :x2 1224 - :y2 968}]} + (let [canvas {:id (mk-uuid "canvas" 1) + :name "Canvas-1" + :type :canvas + :x1 200 + :y1 200 + :x2 1224 + :y2 968} + data {:shapes [] + :canvas [(:id canvas)] + :shapes-by-id {(:id canvas) canvas}} data (blob/encode data) mdata (blob/encode {})] (p/do! diff --git a/backend/src/uxbox/http/handlers.clj b/backend/src/uxbox/http/handlers.clj index 3582bed1d..fcb6371a3 100644 --- a/backend/src/uxbox/http/handlers.clj +++ b/backend/src/uxbox/http/handlers.clj @@ -42,7 +42,7 @@ (let [data (:body-params req) user-agent (get-in req [:headers "user-agent"])] (-> (sm/handle (assoc data ::sm/type :login)) - (p/then #(session/create % user-agent)) + (p/then #(session/create (:id %) user-agent)) (p/then' (fn [token] {:status 204 :cookies {"auth-token" {:value token}} diff --git a/backend/src/uxbox/services/mutations/auth.clj b/backend/src/uxbox/services/mutations/auth.clj index 196340e20..fa26f75b0 100644 --- a/backend/src/uxbox/services/mutations/auth.clj +++ b/backend/src/uxbox/services/mutations/auth.clj @@ -30,8 +30,6 @@ :opt-un [::scope])) (sm/defmutation ::login - {:doc "User login" - :spec ::login-params} [{:keys [username password scope] :as params}] (letfn [(check-password [user password] (hashers/check password (:password user))) diff --git a/backend/src/uxbox/services/mutations/pages.clj b/backend/src/uxbox/services/mutations/pages.clj index 16adcba3c..a70fe7125 100644 --- a/backend/src/uxbox/services/mutations/pages.clj +++ b/backend/src/uxbox/services/mutations/pages.clj @@ -27,6 +27,7 @@ (s/def ::user ::us/uuid) (s/def ::project-id ::us/uuid) (s/def ::metadata any?) +(s/def ::ordering ::us/number) ;; --- Mutation: Create Page @@ -41,14 +42,15 @@ (create-page db/pool params)) (defn create-page - [conn {:keys [id user project-id name data metadata] :as params}] - (let [sql "insert into pages (id, user_id, project_id, name, data, metadata, version) - values ($1, $2, $3, $4, $5, $6, 0) + [conn {:keys [id user project-id name ordering data metadata] :as params}] + (let [sql "insert into pages (id, user_id, project_id, name, + ordering, data, metadata, version) + values ($1, $2, $3, $4, $5, $6, $7, 0) returning *" id (or id (uuid/next)) data (blob/encode data) mdata (blob/encode metadata)] - (-> (db/query-one db/pool [sql id user project-id name data mdata]) + (-> (db/query-one db/pool [sql id user project-id name ordering data mdata]) (p/then' decode-row)))) ;; --- Mutation: Update Page @@ -99,6 +101,21 @@ (update-history conn params) (select-keys params [:id :version]))))))))) +;; --- Mutation: Rename Page + +(s/def ::rename-page + (s/keys :req-un [::id ::name ::user])) + +(sm/defmutation ::rename-page + [{:keys [id name user]}] + (let [sql "update pages + set name = $3 + where id = $1 + and user_id = $2 + and deleted_at is null"] + (-> (db/query-one db/pool [sql id user name]) + (p/then su/constantly-nil)))) + ;; --- Mutation: Update Page Metadata (s/def ::update-page-metadata diff --git a/backend/src/uxbox/services/mutations/projects.clj b/backend/src/uxbox/services/mutations/projects.clj index 08c8f27fa..1a408c6c2 100644 --- a/backend/src/uxbox/services/mutations/projects.clj +++ b/backend/src/uxbox/services/mutations/projects.clj @@ -30,8 +30,6 @@ (s/keys :req-un [::user ::name] :opt-un [::id])) -;; TODO: create role on project creation (maybe in DB?) - (sm/defmutation ::create-project [{:keys [id user name] :as params}] (let [id (or id (uuid/next)) @@ -51,9 +49,7 @@ (s/def ::update-project (s/keys :req-un [::user ::name ::id])) -(sm/defmutation :update-project - {:doc "Update project." - :spec ::update-project} +(sm/defmutation ::update-project [{:keys [id name user] :as params}] (let [sql "update projects set name = $3 @@ -68,9 +64,7 @@ (s/def ::delete-project (s/keys :req-un [::id ::user])) -(sm/defmutation :delete-project - {:doc "Delete project" - :spec ::delete-project} +(sm/defmutation ::delete-project [{:keys [id user] :as params}] (let [sql "update projects set deleted_at = clock_timestamp() diff --git a/frontend/deps.edn b/frontend/deps.edn index fd83e8560..ca09fff0c 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -11,7 +11,7 @@ funcool/beicon {:mvn/version "5.1.0"} funcool/cuerdas {:mvn/version "2.2.0"} funcool/lentes {:mvn/version "1.3.3"} - funcool/potok {:mvn/version "2.6.0"} + funcool/potok {:mvn/version "2.7.0"} funcool/promesa {:mvn/version "4.0.2"} funcool/rumext {:mvn/version "2.0.0-SNAPSHOT"} } diff --git a/frontend/src/uxbox/main/data/history.cljs b/frontend/src/uxbox/main/data/history.cljs index a324f9921..b44e4914e 100644 --- a/frontend/src/uxbox/main/data/history.cljs +++ b/frontend/src/uxbox/main/data/history.cljs @@ -75,7 +75,7 @@ (reify ptk/WatchEvent (watch [_ state stream] - (let [stopper (rx/filter #(= % ::stop-page-watcher) stream)] + #_(let [stopper (rx/filter #(= % ::stop-page-watcher) stream)] (->> stream (rx/filter udp/page-persisted?) (rx/debounce 1000) @@ -186,7 +186,7 @@ (ptk/reify ::select ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current]) + #_(let [pid (get-in state [:workspace :current]) item (get-in state [:workspace pid :history :byver version]) page (-> (get-in state [:pages pid]) (assoc :history true @@ -208,7 +208,7 @@ ptk/WatchEvent (watch [_ state s] - (let [pid (get-in state [:workspace :current])] + #_(let [pid (get-in state [:workspace :current])] (rx/of (udp/persist-page pid)))))) ;; --- Deselect Page History @@ -217,7 +217,7 @@ (ptk/reify ::deselect ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current]) + #_(let [pid (get-in state [:workspace :current]) packed (get-in state [:packed-pages pid])] (-> (udp/unpack-page state packed) (assoc-in [:workspace pid :history :selected] nil)))))) diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index ae68a0ae3..ff3dc4ab9 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -31,22 +31,30 @@ (s/def ::grid-x-axis ::us/number) (s/def ::grid-y-axis ::us/number) (s/def ::grid-color ::us/string) -(s/def ::order ::us/number) +(s/def ::ordering ::us/number) (s/def ::background ::us/string) (s/def ::background-opacity ::us/number) (s/def ::user ::us/uuid) (s/def ::metadata - (s/keys :req-un [::width ::height] - :opt-un [::grid-y-axis + (s/keys :opt-un [::grid-y-axis ::grid-x-axis ::grid-color - ::order ::background ::background-opacity])) -(s/def ::shapes - (s/coll-of ::us/uuid :kind vector? :into [])) +(s/def ::minimal-shape + (s/keys :req-un [::type ::name] + :opt-un [::id])) + +(s/def ::shapes (s/every ::us/uuid :kind vector? :into [])) +(s/def ::canvas (s/every ::us/uuid :kind vector? :into [])) + +(s/def ::shapes-by-id + (s/map-of ::us/uuid ::minimal-shape)) + +(s/def ::data + (s/keys :req-un [::shapes ::canvas ::shapes-by-id])) (s/def ::page-entity (s/keys :req-un [::id @@ -55,28 +63,9 @@ ::created-at ::modified-at ::user-id + ::ordering ::metadata - ::shapes])) - -(s/def ::minimal-shape - (s/keys :req-un [::type ::name] - :opt-un [::id])) - -(s/def :uxbox.backend/shapes - (s/coll-of ::minimal-shape :kind vector?)) - -(s/def :uxbox.backend/data - (s/keys :req-un [:uxbox.backend/shapes])) - -(s/def ::server-page - (s/keys :req-un [::id ::name - ::project-id - ::version - ::created-at - ::modified-at - ::user - ::metadata - :uxbox.backend/data])) + ::data])) ;; --- Protocols @@ -96,62 +85,34 @@ ;; --- Helpers -(defn pack-page - "Return a packed version of page object ready - for send to remore storage service." - [state id] - (letfn [(pack-shapes [ids] - (mapv #(get-in state [:shapes %]) ids))] - (let [page (get-in state [:pages id]) - data {:shapes (pack-shapes (concatv (:canvas page) - (:shapes page)))}] - (-> page - (assoc :data data) - (dissoc :shapes))))) +;; (defn pack-page +;; "Return a packed version of page object ready +;; for send to remore storage service." +;; [state id] +;; (letfn [(pack-shapes [ids] +;; (mapv #(get-in state [:shapes %]) ids))] +;; (let [page (get-in state [:pages id]) +;; data {:shapes (pack-shapes (concatv (:canvas page) +;; (:shapes page)))}] +;; (-> page +;; (assoc :data data) +;; (dissoc :shapes))))) (defn unpack-page - "Unpacks packed page object and assocs it to the - provided state." - [state {:keys [id data] :as page}] - (let [shapes-list (:shapes data []) - - shapes (->> shapes-list - (filter #(not= :canvas (:type %))) - (mapv :id)) - canvas (->> shapes-list - (filter #(= :canvas (:type %))) - (mapv :id)) - - shapes-map (index-by-id shapes-list) - - page (-> page - (dissoc :data) - (assoc :shapes shapes :canvas canvas))] - (-> state - (update :shapes merge shapes-map) - (update :pages assoc id page)))) + [state {:keys [id data metadata] :as page}] + (-> state + (update :pages assoc id (dissoc page :data)) + (update :pages-data assoc id data))) (defn purge-page "Remove page and all related stuff from the state." [state id] - (let [pid (get-in state [:pages id :project])] + (if-let [project-id (get-in state [:pages id :project-id])] (-> state - (update-in [:projects pid :pages] #(filterv (partial not= id) %)) + (update-in [:projects project-id :pages] #(filterv (partial not= id) %)) (update :pages dissoc id) - (update :packed-pages dissoc id) - (update :shapes (fn [shapes] (->> shapes - (map second) - (filter #(= (:page %) id)) - (map :id) - (apply dissoc shapes))))))) - -(defn assoc-packed-page - [state {:keys [id] :as page}] - (assoc-in state [:packed-pages id] page)) - -(defn dissoc-packed-page - [state id] - (update state :packed-pages dissoc id)) + (update :pages-data dissoc id)) + state)) ;; --- Pages Fetched @@ -165,13 +126,7 @@ ptk/UpdateEvent (update [_ state] - (let [get-order #(get-in % [:metadata :order]) - pages (sort-by get-order pages) - page-ids (into [] (map :id) pages)] - (as-> state $ - (assoc-in $ [:projects id :pages] page-ids) - (reduce unpack-page $ pages) - (reduce assoc-packed-page $ pages)))))) + (reduce unpack-page state pages)))) (defn pages-fetched? [v] @@ -192,22 +147,19 @@ (defn page-fetched [data] - (s/assert any? data) ;; TODO: minimal validate + (s/assert ::page-entity data) (ptk/reify ::page-fetched IDeref (-deref [_] data) ptk/UpdateEvent (update [_ state] - (-> state - (unpack-page data) - (assoc-packed-page data))))) + (unpack-page state data)))) (defn page-fetched? [v] (= ::page-fetched (ptk/type v))) - ;; --- Fetch Pages (by project id) (defn fetch-page @@ -222,115 +174,68 @@ ;; --- Page Created -(declare rehash-pages) - -(s/def ::page-created-params - (s/keys :req-un [::id ::name ::project-id ::metadata])) - (defn page-created - [data] - (s/assert ::page-created-params data) - (reify + [{:keys [id project-id] :as page}] + (s/assert ::page-entity page) + (ptk/reify ::page-created + cljs.core/IDeref + (-deref [_] page) + ptk/UpdateEvent (update [_ state] - (let [pid (:project data)] + (let [data (:data page) + page (dissoc page :data)] (-> state - (update-in [:projects pid :pages] (fnil conj []) (:id data)) - (unpack-page data) - (assoc-packed-page data)))) + (update-in [:projects project-id :pages] (fnil conj []) (:id page)) + (update :pages assoc id page) + (update :pages-data assoc id data)))))) - ptk/WatchEvent - (watch [_ state stream] - (rx/of (rehash-pages (:project data)))))) +(defn page-created? + [v] + (= ::page-created (ptk/type v))) ;; --- Create Page Form -(s/def ::form-created-page-params - (s/keys :req-un [::name ::project-id ::width ::height])) +(s/def ::create-page + (s/keys :req-un [::name ::project-id])) -(defn form->create-page - [{:keys [name project width height layout] :as data}] - (s/assert ::form-created-page-params data) - (reify +(defn create-page + [{:keys [project-id name] :as data}] + (s/assert ::create-page data) + (ptk/reify ::create-page ptk/WatchEvent (watch [this state s] - (let [canvas {:id (uuid/random) - :name "Canvas 1" - :type :canvas - :x1 200 - :y1 200 - :x2 (+ 200 width) - :y2 (+ 200 height)} - metadata {:width width - :height height - :order -100} + (let [ordering (count (get-in state [:projects project-id :pages])) params {:name name - :project project - :data {:shapes [canvas]} - :metadata metadata}] - (->> (rp/req :create/page params) - (rx/map :payload) + :project-id project-id + :ordering ordering + :data {:shapes [] + :canvas [] + :shapes-by-id {}} + :metadata {}}] + (->> (rp/mutation :create-page params) (rx/map page-created)))))) ;; --- Update Page Form -(s/def ::form-update-page-params - (s/keys :req-un [::id ::name ::width ::height])) +(declare page-renamed) -(defn form->update-page - [{:keys [id name width height] :as data}] - (s/assert ::form-update-page-params data) - (reify - IPageUpdate +(s/def ::rename-page + (s/keys :req-un [::id ::name])) + +(defn rename-page + [{:keys [id name] :as data}] + (s/assert ::rename-page data) + (ptk/reify ::rename-page ptk/UpdateEvent (update [_ state] - (update-in state [:pages id] - (fn [page] - (-> (assoc page :name name) - (assoc-in [:name] name) - (assoc-in [:metadata :width] width) - (assoc-in [:metadata :height] height))))))) + (update-in state [:pages id] assoc :name name)) -;; --- Page Persisted - -(defn page-persisted - [data] - (s/assert ::server-page data) - (ptk/reify ::page-persisted - cljs.core/IDeref - (-deref [_] data) - - ptk/UpdateEvent - (update [_ state] - (let [{:keys [id version]} data] - (-> state - (assoc-in [:pages id :version] version) - (assoc-packed-page data)))))) - -(defn- page-persisted? - [event] - (= (ptk/type event) ::page-persisted)) - -;; --- Persist Page - -(defn persist-page - [id] - (s/assert ::us/uuid id) - (ptk/reify ::persist-page ptk/WatchEvent - (watch [this state s] - (let [page (get-in state [:pages id])] - (if (:history page) - (rx/empty) - (let [page (pack-page state id)] - (->> (rp/req :update/page page) - (rx/map :payload) - (rx/map page-persisted) - (rx/catch (fn [err] (rx/of ::page-persist-error)))))))))) - -(defn persist-page? - [v] - (= ::persist-page (ptk/type v))) + (watch [_ state stream] + (let [params {:id id :name name}] + (->> (rp/mutation :rename-page params) + (rx/map #(ptk/data-event ::page-renamed data))))))) ;; --- Page Metadata Persisted @@ -349,7 +254,6 @@ [v] (= ::metadata-persisted (ptk/type v))) - ;; --- Persist Page Metadata ;; This is a simplified version of `PersistPage` event @@ -362,7 +266,7 @@ (ptk/reify ::persist-metadata ptk/WatchEvent (watch [_ state stream] - (let [page (get-in state [:pages id])] + #_(let [page (get-in state [:pages id])] (->> (rp/req :update/page-metadata page) (rx/map :payload) (rx/map metadata-persisted)))))) @@ -390,44 +294,6 @@ (update [this state] (assoc-in state [:pages id :metadata] metadata)))) - -;; --- Rehash Pages -;; -;; A post processing event that normalizes the -;; page order numbers after a user sorting -;; operation for a concrete project. - -(defn rehash-pages - [id] - {:pre [(uuid? id)]} - (reify - ptk/UpdateEvent - (update [this state] - (let [page-ids (get-in state [:projects id :pages])] - (reduce (fn [state [index id]] - (assoc-in state [:pages id :metadata :order] index)) - state - (map-indexed vector page-ids)))) - - ptk/WatchEvent - (watch [_ state stream] - (let [page-ids (get-in state [:projects id :pages])] - (->> (rx/from-coll page-ids) - (rx/map persist-metadata)))))) - -;; --- Move Page (Ordering) - -(defn move-page - [{:keys [page-id project-id index] :as params}] - (reify - ptk/UpdateEvent - (update [_ state] - (let [pages (get-in state [:projects project-id :pages]) - pages (into [] (remove #(= % page-id)) pages) - [before after] (split-at index pages) - pages (vec (concat before [page-id] after))] - (assoc-in state [:projects project-id :pages] pages))))) - ;; --- Delete Page (by id) (defn delete-page @@ -440,34 +306,5 @@ ptk/WatchEvent (watch [_ state s] - (->> (rp/req :delete/page id) + (->> (rp/mutation :delete-page {:id id}) (rx/map (constantly ::delete-completed)))))) - -;; --- Watch Page Changes - -(defn watch-page-changes - [id] - (s/assert ::us/uuid id) - (reify - ptk/WatchEvent - (watch [_ state stream] - (let [stopper (rx/filter #(= % ::stop-page-watcher) stream)] - (->> (rx/merge - (->> stream - (rx/filter #(or (satisfies? IPageUpdate %) - (= ::page-update %))) - (rx/debounce 1000) - (rx/mapcat #(rx/merge (rx/of (persist-page id)) - (->> (rx/filter page-persisted? stream) - (rx/timeout 1000 (rx/empty)) - (rx/take 1) - (rx/ignore))))) - (->> stream - (rx/filter #(satisfies? IMetadataUpdate %)) - (rx/debounce 1000) - (rx/mapcat #(rx/merge (rx/of (persist-metadata id)) - (->> (rx/filter metadata-persisted? stream) - (rx/take 1) - (rx/ignore)))))) - (rx/take-until stopper)))))) - diff --git a/frontend/src/uxbox/main/data/projects.cljs b/frontend/src/uxbox/main/data/projects.cljs index 084a964fa..68edb036f 100644 --- a/frontend/src/uxbox/main/data/projects.cljs +++ b/frontend/src/uxbox/main/data/projects.cljs @@ -158,7 +158,7 @@ (reify ptk/WatchEvent (watch [this state stream] - (->> (rp/req :create/project {:name name}) + #_(->> (rp/req :create/project {:name name}) (rx/map :payload) (rx/mapcat (fn [{:keys [id] :as project}] (rx/of #(assoc-project % project) diff --git a/frontend/src/uxbox/main/data/shapes.cljs b/frontend/src/uxbox/main/data/shapes.cljs deleted file mode 100644 index d662f53b2..000000000 --- a/frontend/src/uxbox/main/data/shapes.cljs +++ /dev/null @@ -1,209 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) 2015-2019 Andrey Antukh - -(ns uxbox.main.data.shapes - (:require - [cljs.spec.alpha :as s] - [uxbox.main.geom :as geom] - [uxbox.util.data :refer [index-of]] - [uxbox.util.geom.matrix :as gmt] - [uxbox.util.spec :as us] - [uxbox.util.uuid :as uuid])) - -;; --- Specs - -(s/def ::id ::us/uuid) -(s/def ::blocked boolean?) -(s/def ::collapsed boolean?) -(s/def ::content string?) -(s/def ::fill-color string?) -(s/def ::fill-opacity number?) -(s/def ::font-family string?) -(s/def ::font-size number?) -(s/def ::font-style string?) -(s/def ::font-weight string?) -(s/def ::height number?) -(s/def ::hidden boolean?) -(s/def ::id uuid?) -(s/def ::letter-spacing number?) -(s/def ::line-height number?) -(s/def ::locked boolean?) -(s/def ::name string?) -(s/def ::page uuid?) -(s/def ::proportion number?) -(s/def ::proportion-lock boolean?) -(s/def ::rx number?) -(s/def ::ry number?) -(s/def ::stroke-color string?) -(s/def ::stroke-opacity number?) -(s/def ::stroke-style #{:none :solid :dotted :dashed :mixed}) -(s/def ::stroke-width number?) -(s/def ::text-align #{"left" "right" "center" "justify"}) -(s/def ::type #{:rect :path :circle :image :text}) -(s/def ::width number?) -(s/def ::x1 number?) -(s/def ::x2 number?) -(s/def ::y1 number?) -(s/def ::y2 number?) - -(s/def ::attributes - (s/keys :opt-un [::blocked - ::collapsed - ::content - ::fill-color - ::fill-opacity - ::font-family - ::font-size - ::font-style - ::font-weight - ::hidden - ::letter-spacing - ::line-height - ::locked - ::proportion - ::proportion-lock - ::rx ::ry - ::stroke-color - ::stroke-opacity - ::stroke-style - ::stroke-width - ::text-align - ::x1 ::x2 - ::y1 ::y2])) - -(s/def ::minimal-shape - (s/keys :req-un [::id ::page ::type ::name])) - -(s/def ::shape - (s/and ::minimal-shape ::attributes)) - -(s/def ::rect-like-shape - (s/keys :req-un [::x1 ::y1 ::x2 ::y2 ::type])) - -;; --- Shape Creation - -(defn retrieve-used-names - "Returns a set of already used names by shapes - in the current page." - [{:keys [shapes] :as state}] - (let [pid (get-in state [:workspace :current]) - xf (comp (filter #(= pid (:page %))) - (map :name))] - (into #{} xf (vals shapes)))) - -(defn generate-unique-name - "A unique name generator based on the previous - state of the used names." - [state basename] - (let [used (retrieve-used-names state)] - (loop [counter 1] - (let [candidate (str basename "-" counter)] - (if (contains? used candidate) - (recur (inc counter)) - candidate))))) - -(defn assoc-shape-to-page - [state shape page] - (let [shape-id (uuid/random) - shape-name (generate-unique-name state (:name shape)) - shape (assoc shape - :page page - :id shape-id - :name shape-name)] - (as-> state $ - (if (= :canvas (:type shape)) - (update-in $ [:pages page :canvas] conj shape-id) - (update-in $ [:pages page :shapes] conj shape-id)) - (assoc-in $ [:shapes shape-id] shape)))) - -(defn- duplicate-shape - [state shape page] - (let [id (uuid/random) - name (generate-unique-name state (str (:name shape) "-copy")) - shape (assoc shape :id id :page page :name name)] - (-> state - (update-in [:pages page :shapes] #(into [] (cons id %))) - (assoc-in [:shapes id] shape)))) - -(defn duplicate-shapes - ([state shapes] - (duplicate-shapes state shapes nil)) - ([state shapes page] - (let [shapes (reverse (map #(get-in state [:shapes %]) shapes)) - page (or page (:page (first shapes)))] - (reduce #(duplicate-shape %1 %2 page) state shapes)))) - -;; --- Delete Shapes - -(defn dissoc-from-index - "A function that dissoc shape from the indexed - data structure of shapes from the state." - [state shape] - (update state :shapes dissoc (:id shape))) - -(defn dissoc-from-page - "Given a shape, try to remove its reference from the - corresponding page." - [state {:keys [id page type] :as shape}] - (if (= :canvas type) - (update-in state [:pages page :canvas] - (fn [items] (vec (remove #(= % id) items)))) - (update-in state [:pages page :shapes] - (fn [items] (vec (remove #(= % id) items)))))) - -(defn dissoc-shape - "Given a shape, removes it from the state." - [state shape] - (-> state - (dissoc-from-page shape) - (dissoc-from-index shape))) - -;; --- Shape Vertical Ordering - -(defn order-shape - [state sid opt] - (let [{:keys [page] :as shape} (get-in state [:shapes sid]) - shapes (get-in state [:pages page :shapes]) - index (case opt - :top 0 - :down (min (- (count shapes) 1) (inc (index-of shapes sid))) - :up (max 0 (- (index-of shapes sid) 1)) - :bottom (- (count shapes) 1))] - (update-in state [:pages page :shapes] - (fn [items] - (let [[fst snd] (->> (remove #(= % sid) items) - (split-at index))] - (into [] (concat fst [sid] snd))))))) - -;; --- Shape Selection - -(defn- try-match-shape - [xf selrect acc {:keys [type id items] :as shape}] - (cond - (geom/contained-in? shape selrect) - (conj acc id) - - (geom/overlaps? shape selrect) - (conj acc id) - - :else - acc)) - -(defn match-by-selrect - [state page-id selrect] - (let [xf (comp (map #(get-in state [:shapes %])) - (remove :hidden) - (remove :blocked) - (remove #(= :canvas (:type %))) - (map geom/selection-rect)) - match (partial try-match-shape xf selrect) - shapes (get-in state [:pages page-id :shapes])] - (reduce match #{} (sequence xf shapes)))) - -(defn materialize-xfmt - [state id xfmt] - (let [{:keys [type items] :as shape} (get-in state [:shapes id])] - (update-in state [:shapes id] geom/transform xfmt))) diff --git a/frontend/src/uxbox/main/data/undo.cljs b/frontend/src/uxbox/main/data/undo.cljs index 6e34a025f..0047db0fe 100644 --- a/frontend/src/uxbox/main/data/undo.cljs +++ b/frontend/src/uxbox/main/data/undo.cljs @@ -28,7 +28,7 @@ (ptk/reify ::watch-page-changes ptk/WatchEvent (watch [_ state stream] - (let [stopper (rx/filter #(= % ::udp/stop-page-watcher) stream)] + #_(let [stopper (rx/filter #(= % ::udp/stop-page-watcher) stream)] (->> stream (rx/filter udp/page-update?) (rx/filter #(not (undo? %))) @@ -49,7 +49,7 @@ (ptk/reify ::save-undo-entry ptk/UpdateEvent (update [_ state] - (let [page (udp/pack-page state id) + #_(let [page (udp/pack-page state id) undo {:data (:data page) :metadata (:metadata page)}] (-> state @@ -66,7 +66,7 @@ (ptk/reify ::undo ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current]) + #_(let [pid (get-in state [:workspace :current]) {:keys [stack selected] :as ustate} (get-in state [:undo pid])] (if (>= selected (dec (count stack))) state @@ -95,7 +95,7 @@ (ptk/reify ::redo ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current]) + #_(let [pid (get-in state [:workspace :current]) undo-state (get-in state [:undo pid]) stack (:stack undo-state) selected (:selected undo-state)] diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index c27a5187e..3e1d5f85f 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -14,27 +14,105 @@ [uxbox.main.data.icons :as udi] [uxbox.main.data.pages :as udp] [uxbox.main.data.projects :as dp] - [uxbox.main.data.shapes :as ds] [uxbox.main.geom :as geom] [uxbox.main.refs :as refs] + [uxbox.main.repo.core :as rp] [uxbox.main.store :as st] [uxbox.main.workers :as uwrk] [uxbox.util.data :refer [dissoc-in index-of]] [uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.point :as gpt] [uxbox.util.math :as mth] - [uxbox.util.spec :as us] [uxbox.util.perf :as perf] + [uxbox.util.router :as rt] + [uxbox.util.spec :as us] [uxbox.util.time :as dt] [uxbox.util.uuid :as uuid])) +;; TODO: temporal workaround +(def clear-ruler nil) +(def start-ruler nil) + +;; --- Specs + +(s/def ::id ::us/uuid) +(s/def ::blocked boolean?) +(s/def ::collapsed boolean?) +(s/def ::content string?) +(s/def ::fill-color string?) +(s/def ::fill-opacity number?) +(s/def ::font-family string?) +(s/def ::font-size number?) +(s/def ::font-style string?) +(s/def ::font-weight string?) +(s/def ::height number?) +(s/def ::hidden boolean?) +(s/def ::id uuid?) +(s/def ::letter-spacing number?) +(s/def ::line-height number?) +(s/def ::locked boolean?) +(s/def ::name string?) +(s/def ::page uuid?) +(s/def ::proportion number?) +(s/def ::proportion-lock boolean?) +(s/def ::rx number?) +(s/def ::ry number?) +(s/def ::stroke-color string?) +(s/def ::stroke-opacity number?) +(s/def ::stroke-style #{:none :solid :dotted :dashed :mixed}) +(s/def ::stroke-width number?) +(s/def ::text-align #{"left" "right" "center" "justify"}) +(s/def ::type #{:rect :path :circle :image :text}) +(s/def ::width number?) +(s/def ::x1 number?) +(s/def ::x2 number?) +(s/def ::y1 number?) +(s/def ::y2 number?) + +(s/def ::attributes + (s/keys :opt-un [::blocked + ::collapsed + ::content + ::fill-color + ::fill-opacity + ::font-family + ::font-size + ::font-style + ::font-weight + ::hidden + ::letter-spacing + ::line-height + ::locked + ::proportion + ::proportion-lock + ::rx ::ry + ::stroke-color + ::stroke-opacity + ::stroke-style + ::stroke-width + ::text-align + ::x1 ::x2 + ::y1 ::y2])) + +(s/def ::minimal-shape + (s/keys :req-un [::id ::page ::type ::name])) + +(s/def ::shape + (s/and ::minimal-shape ::attributes)) + +(s/def ::rect-like-shape + (s/keys :req-un [::x1 ::y1 ::x2 ::y2 ::type])) + (s/def ::set-of-uuid (s/every ::us/uuid :kind set?)) -;; --- Expose inner functions +;; --- Protocols -(def start-ruler nil) -(def clear-ruler nil) +(defprotocol IPageDataUpdate + "A marker protocol for mark events that alters the + page and is subject to perform a backend synchronization.") + +;; --- Expose inner functions (defn interrupt? [e] (= e :interrupt)) @@ -46,15 +124,20 @@ (declare initialize-alignment) -(def workspace-default-data - {:initialized true - :zoom 1 +(def default-layout #{:sitemap :drawtools :layers :element-options :rules}) + +(def workspace-default + {:zoom 1 :flags #{:sitemap :drawtools :layers :element-options :rules} :selected #{} :drawing nil :drawing-tool nil :tooltip nil}) +(declare initialized) +(declare watch-page-changes) +(declare watch-events) + (defn initialize "Initialize the workspace state." [project-id page-id] @@ -63,97 +146,100 @@ (ptk/reify ::initialize ptk/UpdateEvent (update [_ state] - (let [data (assoc workspace-default-data - :project-id project-id - :page-id page-id)] - (-> state - (update-in [:workspace page-id] - (fn [wsp] - (if (:initialized wsp) - wsp - (merge wsp data)))) - (assoc-in [:workspace :current] page-id)))) + (-> state + (assoc :workspace-layout default-layout) + ;; (update :workspace-layout + ;; (fn [data] + ;; (if (nil? data) default-layout data))) + (assoc :workspace-local + (assoc workspace-default :id page-id)))) ptk/WatchEvent (watch [_ state stream] - (let [page (get-in state [:pages page-id])] - ;; Activate loaded if page is not fetched. - (when-not page (reset! st/loader true)) - (rx/merge - ;; TODO: the `fetch-pages` should fetch a limited set of attrs? - (rx/of (udp/fetch-page page-id)) - (rx/of (udp/fetch-pages project-id)) - (->> stream - (rx/filter udp/page-fetched?) - (rx/take 1) - (rx/mapcat (fn [event] - (reset! st/loader false) - (rx/of (initialize-alignment page-id)))))))) + #_(when-not (get-in state [:pages page-id]) + (reset! st/loader true)) + + (rx/merge + ;; Stop possible previous watchers and re-fetch the main page + ;; and all project related pages. + (rx/of ::stop-watcher + (udp/fetch-page page-id) + (udp/fetch-pages project-id)) + + ;; When main page is fetched, schedule the main initialization. + (->> (rx/filter udp/page-fetched? stream) + (rx/take 1) + (rx/do #(reset! st/loader false)) + (rx/mapcat #(rx/of (initialized page-id) + #_(initialize-alignment page-id)))) + + ;; When workspace is initialized, run the event watchers. + (->> (rx/filter (ptk/type? ::initialized) stream) + (rx/take 1) + (rx/mapcat #(rx/of watch-page-changes + watch-events))))) ptk/EffectEvent (effect [_ state stream] ;; Optimistic prefetch of projects if them are not already fetched (when-not (seq (:projects state)) (st/emit! (dp/fetch-projects)))))) -;; --- Workspace Tooltips - -(defrecord SetTooltip [text] - ptk/UpdateEvent - (update [_ state] - (let [page-id (get-in state [:workspace :current])] - (assoc-in state [:workspace page-id :tooltip] text)))) - -(defn set-tooltip - [text] - (SetTooltip. text)) +(defn- initialized + [page-id] + (s/assert ::us/uuid page-id) + (ptk/reify ::initialized + ptk/UpdateEvent + (update [_ state] + (let [page (get-in state [:pages page-id]) + data (get-in state [:pages-data page-id])] + (assoc state + :workspace-data data + :workspace-page page))))) ;; --- Workspace Flags -(defrecord ActivateFlag [flag] - ptk/UpdateEvent - (update [_ state] - (let [page-id (get-in state [:workspace :current])] - (update-in state [:workspace page-id :flags] +(defn activate-flag + [flag] + (s/assert keyword? flag) + (ptk/reify ::activate-flag + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-local :flags] (fn [flags] (if (contains? flags flag) flags (conj flags flag))))))) -(defn activate-flag - [flag] - {:pre [(keyword? flag)]} - (ActivateFlag. flag)) - -(defrecord DeactivateFlag [flag] - ptk/UpdateEvent - (update [_ state] - (let [page-id (get-in state [:workspace :current])] - (update-in state [:workspace page-id :flags] disj flag)))) - (defn deactivate-flag [flag] - {:pre [(keyword? flag)]} - (DeactivateFlag. flag)) + (s/assert keyword? flag) + (ptk/reify ::deactivate-flag + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-local :flags] disj flag)))) -(defrecord ToggleFlag [flag] - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (get-in state [:workspace :current]) - flags (get-in state [:workspace page-id :flags])] - (if (contains? flags flag) - (rx/of (deactivate-flag flag)) - (rx/of (activate-flag flag)))))) (defn toggle-flag [flag] - (ToggleFlag. flag)) + (s/assert keyword? flag) + (ptk/reify ::toggle-flag + ptk/WatchEvent + (watch [_ state stream] + (let [flags (get-in state [:workspace-local :flags])] + (if (contains? flags flag) + (rx/of (deactivate-flag flag)) + (rx/of (activate-flag flag))))))) + +(defn set-tooltip + [txt] + ::todo) ;; --- Workspace Ruler (defrecord ActivateRuler [] ptk/WatchEvent (watch [_ state stream] - (rx/of (set-tooltip "Drag to use the ruler") + (rx/of #_(set-tooltip "Drag to use the ruler") (activate-flag :ruler)))) (defn activate-ruler @@ -163,7 +249,7 @@ (defrecord DeactivateRuler [] ptk/WatchEvent (watch [_ state stream] - (rx/of (set-tooltip nil) + (rx/of #_(set-tooltip nil) (deactivate-flag :ruler)))) (defn deactivate-ruler @@ -173,8 +259,7 @@ (defrecord ToggleRuler [] ptk/WatchEvent (watch [_ state stream] - (let [page-id (get-in state [:workspace :current]) - flags (get-in state [:workspace page-id :flags])] + (let [flags (get-in state [:workspace :flags])] (if (contains? flags :ruler) (rx/of (deactivate-ruler)) (rx/of (activate-ruler)))))) @@ -188,8 +273,7 @@ (defrecord SelectIconsToolboxCollection [id] ptk/UpdateEvent (update [_ state] - (let [page-id (get-in state [:workspace :current])] - (assoc-in state [:workspace page-id :icons-toolbox] id))) + (assoc-in state [:workspace :icons-toolbox] id)) ptk/WatchEvent (watch [_ state stream] @@ -233,8 +317,7 @@ (defrecord CopyToClipboard [] ptk/UpdateEvent (update [_ state] - (let [page-id (get-in state [:workspace :current]) - selected (get-in state [:workspace page-id :selected]) + (let [selected (get-in state [:workspace :selected]) item {:id (uuid/random) :created-at (dt/now) :items selected} @@ -255,7 +338,8 @@ udp/IPageUpdate ptk/UpdateEvent (update [_ state] - (let [page-id (get-in state [:workspace :current]) + state + #_(let [page-id (get-in state [:workspace :page :id]) selected (if (nil? id) (first (:clipboard state)) (->> (:clipboard state) @@ -268,6 +352,7 @@ ([] (PasteFromClipboard. nil)) ([id] (PasteFromClipboard. id))) + ;; --- Zoom Management (defrecord IncreaseZoom [] @@ -275,9 +360,8 @@ (update [_ state] (let [increase #(nth c/zoom-levels (+ (index-of c/zoom-levels %) 1) - (last c/zoom-levels)) - page-id (get-in state [:workspace :current])] - (update-in state [:workspace page-id :zoom] (fnil increase 1))))) + (last c/zoom-levels))] + (update-in state [:workspace :zoom] (fnil increase 1))))) (defn increase-zoom [] @@ -288,9 +372,8 @@ (update [_ state] (let [decrease #(nth c/zoom-levels (- (index-of c/zoom-levels %) 1) - (first c/zoom-levels)) - page-id (get-in state [:workspace :current])] - (update-in state [:workspace page-id :zoom] (fnil decrease 1))))) + (first c/zoom-levels))] + (update-in state [:workspace :zoom] (fnil decrease 1))))) (defn decrease-zoom [] @@ -299,8 +382,7 @@ (defrecord ResetZoom [] ptk/UpdateEvent (update [_ state] - (let [page-id (get-in state [:workspace :current])] - (assoc-in state [:workspace page-id :zoom] 1)))) + (assoc-in state [:workspace :zoom] 1))) (defn reset-zoom [] @@ -314,7 +396,7 @@ (ptk/reify ::initialize-alignment ptk/WatchEvent (watch [_ state stream] - (let [metadata (get-in state [:pages id :metadata]) + (let [metadata (get-in state [:workspace-page :metadata]) params {:width c/viewport-width :height c/viewport-height :x-axis (:grid-x-axis metadata c/grid-x-axis) @@ -324,54 +406,103 @@ (->> (uwrk/initialize-alignment params) (rx/map #(activate-flag :grid-indexed)))))))) -;; --- Duplicate Selected - -(def duplicate-selected - (ptk/reify ::duplicate-selected - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :current]) - selected (get-in state [:workspace pid :selected])] - (ds/duplicate-shapes state selected))))) - ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shapes events ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; TODO: add spec +;; --- Add shape to Workspace + +(defn impl-retrieve-used-names + "Returns a set of already used names by shapes + in the current workspace page." + [state] + (let [data (:workspace-data state)] + (into #{} (map :name) (vals (:shapes-by-id data))))) + +(defn impl-generate-unique-name + "A unique name generator based on the current workspace page." + [state basename] + (let [used (impl-retrieve-used-names state)] + (loop [counter 1] + (let [candidate (str basename "-" counter)] + (if (contains? used candidate) + (recur (inc counter)) + candidate))))) + +(defn impl-assoc-shape + [state {:keys [id] :as data}] + (let [name (impl-generate-unique-name state (:name data)) + shape (assoc data :name name)] + (as-> state $ + (if (= :canvas (:type shape)) + (update-in $ [:workspace-data :canvas] conj id) + (update-in $ [:workspace-data :shapes] conj id)) + (assoc-in $ [:workspace-data :shapes-by-id id] shape)))) (defn add-shape [data] (ptk/reify ::add-shape - udp/IPageUpdate + IPageDataUpdate ptk/UpdateEvent (update [_ state] ;; TODO: revisit the `setup-proportions` seems unnecesary - (let [shape (assoc (geom/setup-proportions data) - :id (uuid/random)) - pid (get-in state [:workspace :current])] - (ds/assoc-shape-to-page state shape pid))))) + (let [page-id (get-in state [:workspace-local :id]) + shape (assoc (geom/setup-proportions data) + :id (uuid/random))] + (impl-assoc-shape state shape))))) + +;; --- Duplicate Selected + +(defn impl-duplicate-shape + [state id] + (let [shape (get-in state [:workspace-data :shapes-by-id id])] + (assoc shape :id (uuid/random)))) + +(def duplicate-selected + (ptk/reify ::duplicate-selected + IPageDataUpdate + ptk/UpdateEvent + (update [_ state] + (let [selected (get-in state [:workspace-local :selected]) + duplicate (partial impl-duplicate-shape state) + shapes (map duplicate selected)] + (reduce impl-assoc-shape state shapes))))) + +;; --- Delete shape to Workspace + +(defn impl-dissoc-shape + "Given a shape, removes it from the state." + [state {:keys [id type] :as shape}] + (as-> state $$ + (if (= :canvas type) + (update-in $$ [:workspace-data :canvas] + (fn [items] (vec (remove #(= % id) items)))) + (update-in $$ [:workspace-data :shapes] + (fn [items] (vec (remove #(= % id) items))))) + (update-in $$ [:workspace-data :shapes-by-id] dissoc id))) (defn delete-shape [id] (s/assert ::us/uuid id) (ptk/reify ::delete-shape - udp/IPageUpdate + IPageDataUpdate ptk/UpdateEvent (update [_ state] - (let [shape (get-in state [:shapes id])] - (ds/dissoc-shape state shape))))) + (let [shape (get-in state [:workspace-data :shapes-by-id id])] + (impl-dissoc-shape state shape))))) (defn delete-many-shapes [ids] (s/assert ::us/set ids) (ptk/reify ::delete-many-shapes - udp/IPageUpdate + IPageDataUpdate ptk/UpdateEvent (update [_ state] - (reduce ds/dissoc-shape state - (map #(get-in state [:shapes %]) ids))))) + (reduce impl-dissoc-shape state + (map #(get-in state [:workspace-data :shapes-by-id %]) ids))))) + + +;; --- Toggle shape's selection status (selected or deselected) (defn select-shape "Mark a shape selected for drawing." @@ -380,18 +511,14 @@ (ptk/reify ::select-shape ptk/UpdateEvent (update [_ state] - (prn "select-shape$update" id) - (let [pid (get-in state [:workspace :current]) - selected (get-in state [:workspace pid :selected])] - (update-in state [:workspace pid :selected] - (fn [selected] - (if (contains? selected id) - (disj selected id) - (conj selected id)))))) + (update-in state [:workspace-local :selected] + (fn [selected] + (if (contains? selected id) + (disj selected id) + (conj selected id))))) ptk/WatchEvent (watch [_ state s] - (prn "select-shape$watch" id) (rx/of (activate-flag :element-options))))) (def deselect-all @@ -400,44 +527,67 @@ (ptk/reify ::deselect-all ptk/UpdateEvent (update [_ state] - (prn "deselect-all") - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid] #(-> % - (assoc :selected #{}) - (dissoc :selected-canvas))))))) + (update state :workspace-local #(-> % + (assoc :selected #{}) + (dissoc :selected-canvas)))))) ;; --- Select First Shape +;; TODO: first??? + (def select-first-shape (ptk/reify ::select-first-shape ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current]) - sid (first (get-in state [:pages pid :shapes]))] - (assoc-in state [:workspace pid :selected] #{sid}))))) + (let [pid (get-in state [:workspace-local :id]) + sid (first (get-in state [:workspace-data :shapes]))] + (assoc-in state [:workspace-local :selected] #{sid}))))) ;; --- Select Shapes (By selrect) +(defn- impl-try-match-shape + [xf selrect acc {:keys [type id items] :as shape}] + (cond + (geom/contained-in? shape selrect) + (conj acc id) + + (geom/overlaps? shape selrect) + (conj acc id) + + :else + acc)) + +(defn impl-match-by-selrect + [state selrect] + (let [data (:workspace-data state) + xf (comp (map #(get-in data [:shapes-by-id %])) + (remove :hidden) + (remove :blocked) + (remove #(= :canvas (:type %))) + (map geom/selection-rect)) + match (partial impl-try-match-shape xf selrect) + shapes (:shapes data)] + (reduce match #{} (sequence xf shapes)))) + (def select-shapes-by-current-selrect (ptk/reify ::select-shapes-by-current-selrect ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current]) - selrect (get-in state [:workspace pid :selrect]) - shapes (ds/match-by-selrect state pid selrect)] - (assoc-in state [:workspace pid :selected] shapes))))) + (let [{:keys [selrect id]} (:workspace-local state)] + (->> (impl-match-by-selrect state selrect) + (assoc-in state [:workspace-local :selected])))))) ;; --- Update Shape Attrs (defn update-shape-attrs [id attrs] (s/assert ::us/uuid id) - (s/assert ::ds/attributes attrs) - (let [atts (s/conform ::ds/attributes attrs)] + (s/assert ::attributes attrs) + (let [atts (s/conform ::attributes attrs)] (ptk/reify ::update-shape-attrs ptk/UpdateEvent (update [_ state] - (update-in state [:shapes id] merge attrs))))) + (update-in state [:workspace-data :shapes-by-id id] merge attrs))))) ;; --- Update Selected Shapes attrs @@ -445,12 +595,11 @@ (defn update-selected-shapes-attrs [attrs] - (s/assert ::ds/attributes attrs) + (s/assert ::attributes attrs) (ptk/reify ::update-selected-shapes-attrs ptk/WatchEvent (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - selected (get-in state [:workspace pid :selected])] + (let [selected (get-in state [:workspace-local :selected])] (rx/from-coll (map #(update-shape-attrs % attrs) selected)))))) ;; --- Move Selected @@ -493,36 +642,17 @@ (ptk/reify ::move-selected ptk/WatchEvent (watch [_ state stream] - (let [page-id (get-in state [:workspace :current]) - workspace (get-in state [:workspace page-id]) - selected (:selected workspace) - flags (:flags workspace) + (let [{:keys [selected flags id]} (:workspace-local state) align? (refs/alignment-activated? flags) - metadata (merge c/page-metadata (get-in state [:pages page-id :metadata])) + metadata (merge c/page-metadata + (get-in state [:workspace-page :metadata])) distance (get-displacement-distance metadata align?) displacement (get-displacement direction speed distance)] (rx/concat - (when align? - (rx/of (initial-selection-align selected))) + (when align? (rx/of (initial-selection-align selected))) (rx/of (apply-temporal-displacement-in-bulk selected displacement)) (rx/of (materialize-current-modifier-in-bulk selected))))))) -;; --- Move Selected Layer - -(defn order-selected-shapes - [loc] - (s/assert ::direction loc) - (ptk/reify ::move-selected-layer - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace :current]) - selected (get-in state [:workspace id :selected])] - ;; NOTE: multiple selection ordering not supported - (if (pos? (count selected)) - (ds/order-shape state (first selected) loc) - state))))) - ;; --- Update Shape Position (deftype UpdateShapePosition [id point] @@ -544,8 +674,7 @@ (ptk/reify ::delete-selected ptk/WatchEvent (watch [_ state stream] - (let [id (get-in state [:workspace :current]) - selected (get-in state [:workspace id :selected])] + (let [selected (get-in state [:workspace-local :selected])] (rx/of (delete-many-shapes selected)))))) ;; --- Rename Shape @@ -567,12 +696,41 @@ (ptk/reify ::change-shape-order ptk/UpdateEvent (update [_ state] - (let [page-id (get-in state [:shapes id :page]) - shapes (get-in state [:pages page-id :shapes]) + (let [shapes (get-in state [:workspace-data :shapes]) shapes (into [] (remove #(= % id)) shapes) [before after] (split-at index shapes) shapes (vec (concat before [id] after))] - (assoc-in state [:pages page-id :shapes] shapes))))) + (assoc-in state [:workspace-data :shapes] shapes))))) + +;; --- Shape Vertical Ordering + +(defn impl-order-shape + [state sid opt] + (let [shapes (get-in state [:workspace-data :shapes]) + index (case opt + :top 0 + :down (min (- (count shapes) 1) (inc (index-of shapes sid))) + :up (max 0 (- (index-of shapes sid) 1)) + :bottom (- (count shapes) 1))] + (update-in state [:workspace-data :shapes] + (fn [items] + (let [[fst snd] (->> (remove #(= % sid) items) + (split-at index))] + (into [] (concat fst [sid] snd))))))) + +(defn order-selected-shapes + [loc] + (s/assert ::direction loc) + (ptk/reify ::move-selected-layer + IPageDataUpdate + ptk/UpdateEvent + (update [_ state] + (let [id (first (get-in state [:workspace-local :selected])) + type (get-in state [:workspace-data :shapes-by-id id :type])] + ;; NOTE: multiple selection ordering not supported + (if (and id (not= type :canvas)) + (impl-order-shape state id loc) + state))))) ;; --- Change Canvas Order (D&D Ordering) @@ -583,12 +741,11 @@ (ptk/reify ::change-canvas-order ptk/UpdateEvent (update [_ state] - (let [page-id (get-in state [:shapes id :page]) - canvas (get-in state [:pages page-id :canvas]) - canvas (into [] (remove #(= % id)) canvas) - [before after] (split-at index canvas) - canvas (vec (concat before [id] after))] - (assoc-in state [:pages page-id :canvas] canvas))))) + (let [shapes (get-in state [:workspace-data :canvas]) + shapes (into [] (remove #(= % id)) shapes) + [before after] (split-at index shapes) + shapes (vec (concat before [id] after))] + (assoc-in state [:workspace-data :canvas] shapes))))) ;; --- Shape / Selection Alignment @@ -599,7 +756,7 @@ (ptk/reify ::initialize-shapes-align-in-bulk ptk/WatchEvent (watch [_ state stream] - (let [shapes-by-id (:shapes state) + (let [shapes-by-id (get-in state [:workspace-data :shapes-by-id]) shapes (mapv #(get shapes-by-id %) ids) sshape (geom/shapes->rect-shape shapes) point (gpt/point (:x1 sshape) @@ -617,9 +774,9 @@ (s/assert ::set-of-uuid ids) (s/assert gpt/point? delta) (letfn [(process-shape [state id] - (let [prev (get-in state [:shapes id :modifier-mtx] (gmt/matrix)) + (let [prev (get-in state [:workspace-data :shapes-by-id id :modifier-mtx] (gmt/matrix)) xfmt (gmt/translate prev delta)] - (assoc-in state [:shapes id :modifier-mtx] xfmt)))] + (assoc-in state [:workspace-data :shapes-by-id id :modifier-mtx] xfmt)))] (ptk/reify ::apply-temporal-displacement-in-bulk ptk/UpdateEvent (update [_ state] @@ -634,17 +791,17 @@ (ptk/reify ::assoc-temporal-modifier-in-bulk ptk/UpdateEvent (update [_ state] - (reduce #(assoc-in %1 [:shapes %2 :modifier-mtx] xfmt) state ids)))) + (reduce #(assoc-in %1 [:workspace-data :shapes-by-id %2 :modifier-mtx] xfmt) state ids)))) (defn materialize-current-modifier-in-bulk [ids] (s/assert ::us/set ids) (letfn [(process-shape [state id] - (let [xfmt (get-in state [:shapes id :modifier-mtx])] + (let [xfmt (get-in state [:workspace-data :shapes-by-id id :modifier-mtx])] (if (gmt/matrix? xfmt) (-> state - (update-in [:shapes id] geom/transform xfmt) - (update-in [:shapes id] dissoc :modifier-mtx)) + (update-in [:workspace-data :shapes-by-id id] geom/transform xfmt) + (update-in [:workspace-data :shapes-by-id id] dissoc :modifier-mtx)) state)))] (ptk/reify ::materialize-current-modifier-in-bulk udp/IPageUpdate @@ -664,16 +821,16 @@ (ptk/reify ::rehash-shape-relationship ptk/UpdateEvent (update [_ state] - (let [shape (get-in state [:shapes id]) - xform (comp (map #(get-in state [:shapes %])) + (let [shape (get-in state [:workspace-data :shapes-by-id id]) + xform (comp (map #(get-in state [:workspace-data :shapes-by-id %])) (filter #(overlaps? % shape)) (take 1)) - canvas (->> (get-in state [:pages (:page shape) :canvas]) + canvas (->> (get-in state [:workspace-data :canvas]) (sequence xform) (first))] (if canvas - (update-in state [:shapes id] assoc :canvas (:id canvas)) - (update-in state [:shapes id] assoc :canvas nil))))))) + (update-in state [:workspace-data :shapes-by-id id] assoc :canvas (:id canvas)) + (update-in state [:workspace-data :shapes-by-id id] assoc :canvas nil))))))) ;; --- Start shape "edition mode" @@ -683,16 +840,14 @@ (ptk/reify ::start-edition-mode ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current])] - (assoc-in state [:workspace pid :edition] id))) + (assoc-in state [:workspace-local :edition] id)) ptk/WatchEvent (watch [_ state stream] - (let [pid (get-in state [:workspace :current])] - (->> stream - (rx/filter #(= % :interrupt)) - (rx/take 1) - (rx/map (fn [_] #(dissoc-in % [:workspace pid :edition])))))))) + (->> stream + (rx/filter #(= % :interrupt)) + (rx/take 1) + (rx/map (fn [_] #(dissoc-in % [:workspace-local :edition]))))))) ;; --- Select for Drawing @@ -700,8 +855,7 @@ (ptk/reify ::clear-drawing ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid] dissoc :drawing-tool :drawing))))) + (update state :workspace-local dissoc :drawing-tool :drawing)))) (defn select-for-drawing ([tool] (select-for-drawing tool nil)) @@ -709,11 +863,12 @@ (ptk/reify ::select-for-drawing ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid] assoc :drawing-tool tool :drawing data)))))) + (update state :workspace-local assoc :drawing-tool tool :drawing data))))) ;; --- Shape Proportions +;; TODO: revisit + (deftype LockShapeProportions [id] ptk/UpdateEvent (update [_ state] @@ -732,6 +887,8 @@ {:pre [(uuid? id)]} (LockShapeProportions. id)) +;; TODO: revisit + (deftype UnlockShapeProportions [id] udp/IPageUpdate ptk/UpdateEvent @@ -745,6 +902,8 @@ ;; --- Update Dimensions +;; TODO: revisit + (s/def ::width (s/and ::us/number ::us/positive)) (s/def ::height (s/and ::us/number ::us/positive)) @@ -766,6 +925,7 @@ ;; --- Update Interaction +;; TODO: revisit (deftype UpdateInteraction [shape interaction] udp/IPageUpdate ptk/UpdateEvent @@ -781,6 +941,7 @@ ;; --- Delete Interaction +;; TODO: revisit (deftype DeleteInteracton [shape id] udp/IPageUpdate ptk/UpdateEvent @@ -794,10 +955,11 @@ ;; --- Path Modifications +;; TODO: revisit (deftype UpdatePath [id index delta] ptk/UpdateEvent (update [_ state] - (update-in state [:shapes id :segments index] gpt/add delta))) + (update-in state [:workspace-data :shapes-by-id id :segments index] gpt/add delta))) (defn update-path "Update a concrete point in the path shape." @@ -807,10 +969,11 @@ ;; --- Initial Path Point Alignment +;; TODO: revisit (deftype InitialPathPointAlign [id index] ptk/WatchEvent (watch [_ state s] - (let [shape (get-in state [:shapes id]) + (let [shape (get-in state [:workspace-data :shapes-by-id id]) point (get-in shape [:segments index])] (->> (uwrk/align-point point) (rx/map #(update-path id index %)))))) @@ -826,6 +989,7 @@ ;; --- Shape Visibility +;; TODO: revisit (defn set-hidden-attr [id value] (s/assert ::us/uuid id) @@ -849,6 +1013,7 @@ ;; --- Shape Blocking +;; TODO: revisit (defn set-blocked-attr [id value] (s/assert ::us/uuid id) @@ -872,6 +1037,7 @@ ;; --- Shape Locking +;; TODO: revisit (deftype LockShape [id] udp/IPageUpdate ptk/UpdateEvent @@ -885,6 +1051,7 @@ (assoc-in state [:shapes id :locked] true))))] (mark-locked state id)))) +;; TODO: revisit (defn lock-shape [id] {:pre [(uuid? id)]} @@ -903,29 +1070,30 @@ (assoc-in state [:shapes id :locked] false))))] (mark-unlocked state id)))) +;; TODO: revisit (defn unlock-shape [id] {:pre [(uuid? id)]} (UnlockShape. id)) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Pages -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn delete-page - [id] - {:pre [(uuid? id)]} - (ptk/reify ::delete-page - ptk/WatchEvent - (watch [_ state stream] - (let [pid (get-in state [:pages id :project])] - (rx/merge - (rx/of (udp/delete-page id)) - (->> stream - (rx/filter #(= % ::udp/delete-completed)) - (rx/map #(dp/go-to pid)) - (rx/take 1))))))) +;; --- Recalculate Shapes relations (Shapes <-> Canvas) +(def rehash-shapes-relationships + (letfn [(overlaps? [canvas shape] + (let [shape1 (geom/shape->rect-shape canvas) + shape2 (geom/shape->rect-shape shape)] + (geom/overlaps? shape1 shape2)))] + (ptk/reify ::rehash-shapes-relationships + ptk/UpdateEvent + (update [_ state] + (let [data (:workspace-data state) + canvas (map #(get-in data [:shapes-by-id %]) (:canvas data)) + shapes (map #(get-in data [:shapes-by-id %]) (:shapes data))] + (reduce (fn [state {:keys [id] :as shape}] + (let [canvas (first (filter #(overlaps? % shape) canvas))] + (update-in state [:workspace-data :shapes-by-id id] assoc :canvas (:id canvas)))) + state + shapes)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Selection Rect IMPL @@ -958,11 +1126,10 @@ (ptk/reify ::select-canvas ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid] assoc :selected-canvas id))))) + (update state :workspace-local assoc :selected-canvas id)))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Server Interactions +;; Server Interactions DEPRECATED ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; --- Update Metadata @@ -976,57 +1143,185 @@ (ptk/reify ::update-metadata ptk/WatchEvent (watch [_ state s] - (rx/of (udp/update-metadata id metadata) + #_(rx/of (udp/update-metadata id metadata) (initialize-alignment id))))) -(defrecord OpenView [page-id] - ptk/WatchEvent - (watch [_ state s] - (let [page-id (get-in state [:workspace :page])] - (rx/of (udp/persist-page page-id)))) +;; (defrecord OpenView [page-id] +;; ptk/WatchEvent +;; (watch [_ state s] +;; (let [page-id (get-in state [:workspace :page])] +;; (rx/of (udp/persist-page page-id)))) - ptk/EffectEvent - (effect [_ state s] - (let [rval (rand-int 1000000) - page (get-in state [:pages page-id]) - project (get-in state [:projects (:project page)]) - url (str cfg/viewurl "?v=" rval "#/preview/" (:share-token project) "/" page-id)] - (js/open url "new tab" "")))) +;; ptk/EffectEvent +;; (effect [_ state s] +;; (let [rval (rand-int 1000000) +;; page (get-in state [:pages page-id]) +;; project (get-in state [:projects (:project page)]) +;; url (str cfg/viewurl "?v=" rval "#/preview/" (:share-token project) "/" page-id)] +;; (js/open url "new tab" "")))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Navigation +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn navigate-to-project + [project-id] + (ptk/reify ::navigate-to-project + ptk/WatchEvent + (watch [_ state stream] + (let [page-ids (get-in state [:projects project-id :pages]) + params {:project project-id :page (first page-ids)}] + (rx/of (rt/nav :workspace/page params)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Page Changes Reactions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn start-shapes-watcher +(defn page-update? + [o] + (or (satisfies? IPageDataUpdate o) + (= ::page-data-update o))) + +(defn page-persisted + [{:keys [id] :as page}] + (s/assert ::page-entity page) + (ptk/reify ::page-persisted + cljs.core/IDeref + (-deref [_] page) + + ptk/UpdateEvent + (update [_ state] + (let [data (:data page) + page (dissoc page :data)] + (-> state + (assoc :workspace-data data) + (assoc :workspace-page page) + (update :pages assoc id page) + (update :pages-data assoc id data)))))) + +(defn page-persisted? + [v] + (= ::page-persisted (ptk/type v))) + +;; --- Persist Page + +(def persist-page + (ptk/reify ::persist-page + ptk/WatchEvent + (watch [this state s] + (let [local (:workspace-local state) + page (:workspace-page state) + data (:workspace-data state)] + (if (:history local) + (rx/empty) + (let [page (assoc page :data data)] + (->> (rp/mutation :update-page page) + (rx/map (fn [res] (merge page res))) + (rx/map page-persisted) + (rx/catch (fn [err] (rx/of ::page-persist-error)))))))))) + +;; --- 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))))) + +;; --- Delete Page + +(defn delete-page [id] (s/assert ::us/uuid id) - (letfn [(on-change [[old new]] - (reduce-kv (fn [acc k v] - (if (identical? v (get old k)) - acc - (conj acc k))) - #{} - new)) - (select-shapes [state] - (let [ids (get-in state [:pages id :shapes])] - (select-keys (:shapes state) ids))) - ] - (ptk/reify ::watch-page-changes + (ptk/reify ::delete-page + ptk/WatchEvent + (watch [_ state stream] + (let [project-id (get-in state [:pages id :project-id])] + (rx/merge + (rx/of (udp/delete-page id)) + (->> stream + (rx/filter #(= % ::udp/delete-completed)) + (rx/map #(navigate-to-project project-id)) + (rx/take 1))))))) + +;; -- Page Changes Watcher + +(def watch-page-changes + (ptk/reify ::watch-page-changes + ptk/WatchEvent + (watch [_ state stream] + (let [stopper (rx/filter #(= % ::stop-watcher) stream)] + (->> stream + (rx/filter page-update?) + (rx/debounce 500) + (rx/mapcat #(rx/merge (rx/of rehash-shapes-relationships persist-page) + (->> (rx/filter page-persisted? stream) + (rx/timeout 1000 (rx/empty)) + (rx/take 1) + (rx/ignore)))) + (rx/take-until stopper)))))) + +(def watch-events + (letfn [(on-page-renamed [event] + (rx/of #(update % :workspace-page assoc :name (:name @event))))] + (ptk/reify ::watch-events ptk/WatchEvent (watch [_ state stream] - (let [stoper (rx/filter #(= % ::stop-shapes-watcher) stream) - ids (get-in state [:pages id :shapes]) - local (volatile! nil) - into* (fn [dst srcs] (reduce #(into %1 %2) dst srcs))] - (->> (rx/merge st/store (rx/of state)) - (rx/map select-shapes) - (rx/buffer 2 1) - (rx/map on-change) - (rx/buffer-time 300) - (rx/map #(into* #{} %)) - (rx/filter (complement empty?)) - ;; (rx/tap #(prn "changed" %)) - (rx/mapcat (fn [items] (rx/from-coll - (map rehash-shape-relationship items)))) - (rx/take-until stoper))))))) + (let [stopper (rx/filter #(= % ::stop-watcher) stream) + page-id (get-in state [:workspace-page :id])] + (->> stream + (rx/filter (ptk/type? ::udp/page-renamed)) + (rx/filter #(= (:id (deref %)) page-id)) + (rx/map on-page-renamed) + (rx/filter ptk/event?) + (rx/take-until stopper))))))) + +;; (def watch-shapes-changes +;; (letfn [(look-for-changes [[old new]] +;; (reduce-kv (fn [acc k v] +;; (if (identical? v (get old k)) +;; acc +;; (conj acc k))) +;; #{} +;; new)) +;; (select-shapes [state] +;; (get-in state [:workspace-data :shapes-by-id])) +;; ] +;; (ptk/reify ::watch-page-changes +;; ptk/WatchEvent +;; (watch [_ state stream] +;; (let [stopper (rx/filter #(= % ::stop-page-watcher) stream)] +;; (->> stream +;; (rx/filter page-update?) +;; (rx/debounce 1000) +;; (rx/mapcat #(rx/merge (rx/of persist-page +;; (->> (rx/filter page-persisted? stream) +;; (rx/timeout 1000 (rx/empty)) +;; (rx/take 1) +;; (rx/ignore))))) +;; (rx/take-until stopper)))))) + + + +;; (let [stoper (rx/filter #(= % ::stop-shapes-watcher) stream) +;; into' (fn [dst srcs] (reduce #(into %1 %2) dst srcs))] +;; (->> (rx/merge st/store (rx/of state)) +;; (rx/map #(get-in % [:workspace-data :shapes-by-id])) +;; (rx/buffer 2 1) +;; (rx/map look-for-changes) +;; (rx/buffer-time 300) +;; (rx/map #(into' #{} %)) +;; (rx/filter (complement empty?)) +;; ;; (rx/tap #(prn "changed" %)) +;; ;; (rx/mapcat (fn [items] (rx/from-coll +;; ;; (map rehash-shape-relationship items)))) +;; (rx/ignore) +;; (rx/take-until stoper))))))) diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index 02205392b..2d14e0493 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -12,39 +12,57 @@ [uxbox.main.store :as st])) (def workspace - (letfn [(selector [state] - (let [id (get-in state [:workspace :current])] - (get-in state [:workspace id])))] - (-> (l/lens selector) - (l/derive st/state)))) + (-> (l/key :workspace-local) + (l/derive st/state))) + +(def workspace-local + (-> (l/key :workspace-local) + (l/derive st/state))) + +(def workspace-layout + (-> (l/key :workspace-layout) + (l/derive st/state))) + +(def workspace-page + (-> (l/key :workspace-page) + (l/derive st/state))) + +(def workspace-data + (-> (l/key :workspace-data) + (l/derive st/state))) (def selected-shapes (-> (l/key :selected) - (l/derive workspace))) + (l/derive workspace-local))) (def selected-canvas (-> (l/key :selected-canvas) - (l/derive workspace))) + (l/derive workspace-local))) (def toolboxes (-> (l/key :toolboxes) - (l/derive workspace))) + (l/derive workspace-local))) +;; DEPRECATED (def flags (-> (l/key :flags) - (l/derive workspace))) + (l/derive workspace-local))) + +(def selected-flags + (-> (l/key :flags) + (l/derive workspace-local))) (def selected-zoom (-> (l/key :zoom) - (l/derive workspace))) + (l/derive workspace-local))) (def selected-tooltip (-> (l/key :tooltip) - (l/derive workspace))) + (l/derive workspace-local))) (def selected-drawing-shape (-> (l/key :drawing) - (l/derive workspace))) + (l/derive workspace-local))) (def selected-drawing-tool (-> (l/key :drawing-tool) diff --git a/frontend/src/uxbox/main/repo/core.cljs b/frontend/src/uxbox/main/repo/core.cljs index 02f7f8ab1..bc5086d54 100644 --- a/frontend/src/uxbox/main/repo/core.cljs +++ b/frontend/src/uxbox/main/repo/core.cljs @@ -86,9 +86,9 @@ (defn send-mutation! [id params] (let [url (str url "/w/mutation/" (name id))] - (send! {:method :post - :url url - :body params}))) + (->> (impl-send {:method :post :url url :body params}) + (rx/map conditional-decode) + (rx/mapcat handle-response)))) (defn- dispatch [& args] diff --git a/frontend/src/uxbox/main/store.cljs b/frontend/src/uxbox/main/store.cljs index b26bd4ecb..361f373b3 100644 --- a/frontend/src/uxbox/main/store.cljs +++ b/frontend/src/uxbox/main/store.cljs @@ -20,23 +20,25 @@ (defonce store (ptk/store {:on-error #(*on-error* %)})) (defonce stream (ptk/input-stream store)) -;; (defn repr-event -;; [event] -;; (cond -;; (satisfies? ptk/Event event) -;; (str "typ: " (pr-str (ptk/type event))) +(defn repr-event + [event] + (cond + (satisfies? ptk/Event event) + (str "typ: " (pr-str (ptk/type event))) -;; (and (fn? event) -;; (pos? (count (.-name event)))) -;; (str "fn: " (demunge (.-name event))) + (and (fn? event) + (pos? (count (.-name event)))) + (str "fn: " (demunge (.-name event))) -;; :else -;; (str "unk: " (pr-str event)))) + :else + (str "unk: " (pr-str event)))) -;; (defonce debug (as-> stream $ -;; (rx/filter ptk/event? $) -;; (rx/subscribe $ (fn [event] -;; (println "[stream]: " (repr-event event)))))) +(defonce debug (as-> stream $ + (rx/filter ptk/event? $) + ;; Comment this line if you want full debug. + (rx/ignore $) + (rx/subscribe $ (fn [event] + (println "[stream]: " (repr-event event)))))) (def auth-ref (-> (l/key :auth) @@ -61,15 +63,17 @@ :profile (:profile storage) :clipboard #queue [] :undo {} - :workspace nil + :workspace-layout nil + :workspace-local nil + :workspace-pdata nil :images-collections nil :images nil :icons-collections nil :icons nil :colors-collections colors/collections - :shapes nil :projects nil - :pages nil}) + :pages nil + :pages-data nil}) (defn init "Initialize the state materialization." diff --git a/frontend/src/uxbox/main/ui/shapes/common.cljs b/frontend/src/uxbox/main/ui/shapes/common.cljs index e43629017..06a493d8d 100644 --- a/frontend/src/uxbox/main/ui/shapes/common.cljs +++ b/frontend/src/uxbox/main/ui/shapes/common.cljs @@ -29,9 +29,8 @@ (ptk/reify ::start-move-selected ptk/WatchEvent (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - flags (get-in state [:workspace pid :flags]) - selected (get-in state [:workspace pid :selected]) + (let [flags (get-in state [:workspace-local :flags]) + selected (get-in state [:workspace-local :selected]) stoper (rx/filter uws/mouse-up? stream) position @uws/mouse-position] (rx/concat @@ -40,7 +39,8 @@ (->> (uws/mouse-position-deltas position) (rx/map #(dw/apply-temporal-displacement-in-bulk selected %)) (rx/take-until stoper)) - (rx/of (dw/materialize-current-modifier-in-bulk selected))))))) + (rx/of (dw/materialize-current-modifier-in-bulk selected) + ::dw/page-data-update)))))) (defn on-mouse-down [event {:keys [id type] :as shape} selected] diff --git a/frontend/src/uxbox/main/ui/shapes/text.cljs b/frontend/src/uxbox/main/ui/shapes/text.cljs index 38de6261b..834563b60 100644 --- a/frontend/src/uxbox/main/ui/shapes/text.cljs +++ b/frontend/src/uxbox/main/ui/shapes/text.cljs @@ -11,7 +11,6 @@ [lentes.core :as l] [rumext.core :as mx] [rumext.alpha :as mf] - [uxbox.main.data.shapes :as uds] [uxbox.main.data.workspace :as udw] [uxbox.main.geom :as geom] [uxbox.main.refs :as refs] diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index 43c628182..d217441b8 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -14,7 +14,7 @@ [uxbox.main.data.history :as udh] [uxbox.main.data.pages :as udp] [uxbox.main.data.undo :as udu] - [uxbox.main.data.workspace :as dw] + [uxbox.main.data.workspace :as udw] [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.ui.confirm] @@ -22,9 +22,9 @@ [uxbox.main.ui.messages :refer [messages-widget]] [uxbox.main.ui.workspace.viewport :refer [viewport]] [uxbox.main.ui.workspace.colorpalette :refer [colorpalette]] - [uxbox.main.ui.workspace.download] + ;; [uxbox.main.ui.workspace.download] [uxbox.main.ui.workspace.header :refer [header]] - [uxbox.main.ui.workspace.images] + ;; [uxbox.main.ui.workspace.images] [uxbox.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]] [uxbox.main.ui.workspace.scroll :as scroll] [uxbox.main.ui.workspace.shortcuts :as shortcuts] @@ -55,47 +55,20 @@ (dom/prevent-default event) (dom/stop-propagation event) (if (pos? (.-deltaY event)) - (st/emit! (dw/decrease-zoom)) - (st/emit! (dw/increase-zoom))) + (st/emit! (udw/decrease-zoom)) + (st/emit! (udw/increase-zoom))) (scroll/scroll-to-point dom mouse-point scroll-position)))) -(defn- subscribe - [canvas page] - (st/emit! (udp/watch-page-changes (:id page)) - (udu/watch-page-changes (:id page)) - ;; TODO: temporary commented - ;; (udh/initialize (:id page)) - ;; (udh/watch-page-changes (:id page)) - (dw/start-shapes-watcher (:id page))) - (let [sub (shortcuts/init)] - #(do (st/emit! ::udp/stop-page-watcher - ::udh/stop-page-watcher - ::dw/stop-shapes-watcher) - (rx/cancel! sub)))) - -(mf/defc workspace - [{:keys [page] :as props}] - (let [flags (or (mf/deref refs/flags) #{}) - canvas (mf/use-ref nil) - left-sidebar? (not (empty? (keep flags [:layers :sitemap +(mf/defc workspace-content + [{:keys [layout page] :as params}] + (let [canvas (mf/use-ref nil) + left-sidebar? (not (empty? (keep layout [:layers :sitemap :document-history]))) - right-sidebar? (not (empty? (keep flags [:icons :drawtools + right-sidebar? (not (empty? (keep layout [:icons :drawtools :element-options]))) classes (classnames :no-tool-bar-right (not right-sidebar?) - :no-tool-bar-left (not left-sidebar?) - :scrolling (:viewport-positionig workspace))] - - (mf/use-effect #(subscribe canvas page) - #js [(:id page)]) - [:* - [:& messages-widget] - [:& header {:page page - :flags flags - :key (:id page)}] - - (when (:colorpalette flags) - [:& colorpalette]) + :no-tool-bar-left (not left-sidebar?))] [:main.main-content [:section.workspace-content @@ -106,32 +79,45 @@ [:& history-dialog] ;; Rules - (when (contains? flags :rules) - [:& horizontal-rule]) - - (when (contains? flags :rules) - [:& vertical-rule]) + (when (contains? layout :rules) + [:* + [:& horizontal-rule] + [:& vertical-rule]]) [:section.workspace-viewport {:id "workspace-viewport" :ref canvas} - [:& viewport {:page page :key (:id page)}]]] + [:& viewport {:page page}]]] ;; Aside (when left-sidebar? - [:& left-sidebar {:page page :flags flags}]) + [:& left-sidebar {:page page :layout layout}]) (when right-sidebar? - [:& right-sidebar {:page page :flags flags}])]])) + [:& right-sidebar {:page page :layout layout}])])) + +(mf/defc workspace + [{:keys [page-id] :as props}] + (let [layout (mf/deref refs/workspace-layout) + flags (mf/deref refs/selected-flags) + page (mf/deref refs/workspace-page)] + + [:* + [:& messages-widget] + [:& header {:page page :flags flags}] + + (when (:colorpalette flags) + [:& colorpalette]) + + (when (and layout page) + [:& workspace-content {:layout layout :page page}])])) (mf/defc workspace-page [{:keys [project-id page-id] :as props}] - (let [page-iref (mf/use-memo {:deps #js [project-id page-id] - :fn #(-> (l/in [:pages page-id]) - (l/derive st/state))}) - page (mf/deref page-iref)] - (mf/use-effect - {:deps #js [project-id page-id] - :fn #(st/emit! (dw/initialize project-id page-id))}) + (mf/use-effect + {:deps #js [page-id] + :fn (fn [] + (let [sub (shortcuts/init)] + (st/emit! (udw/initialize project-id page-id)) + #(rx/cancel! sub)))}) - [:> rdnd/provider {:backend rdnd/html5} - (when page - [:& workspace {:page page}])])) + [:> rdnd/provider {:backend rdnd/html5} + [:& workspace {:page-id page-id :key page-id}]]) diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index c76d87ae4..be260ce94 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -12,7 +12,6 @@ [rumext.alpha :as mf] [uxbox.main.constants :as c] [uxbox.main.data.workspace :as dw] - [uxbox.main.data.shapes :as ds] [uxbox.main.geom :as geom] [uxbox.main.refs :as refs] [uxbox.main.store :as st] @@ -73,17 +72,16 @@ (ptk/reify ::start-drawing ptk/UpdateEvent (update [_ state] - (update-in state [:workspace :drawing-lock] #(if (nil? %) id %))) + (update-in state [:workspace-local :drawing-lock] #(if (nil? %) id %))) ptk/WatchEvent (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - lock (get-in state [:workspace :drawing-lock])] + (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 dissoc :drawing-lock)))) + (rx/map (fn [_] #(update % :workspace-local dissoc :drawing-lock)))) (rx/of (handle-drawing type))) (rx/empty))))))) @@ -98,9 +96,8 @@ (ptk/reify ::handle-drawing ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current]) - data (make-minimal-shape type)] - (update-in state [:workspace pid :drawing] merge data))) + (let [data (make-minimal-shape type)] + (update-in state [:workspace-local :drawing] merge data))) ptk/WatchEvent (watch [_ state stream] @@ -111,13 +108,12 @@ (def handle-drawing-generic (letfn [(initialize-drawing [state point] - (let [pid (get-in state [:workspace :current]) - shape (get-in state [:workspace pid :drawing]) + (let [shape (get-in state [:workspace-local :drawing]) shape (geom/setup shape {:x1 (:x point) :y1 (:y point) :x2 (+ (:x point) 2) :y2 (+ (:y point) 2)})] - (assoc-in state [:workspace pid :drawing] (assoc shape ::initialized? true)))) + (assoc-in state [:workspace-local :drawing] (assoc shape ::initialized? true)))) (resize-shape [shape point lock?] (let [shape (-> (geom/shape->rect-shape shape) @@ -128,14 +124,12 @@ (assoc shape :modifier-mtx mtx))) (update-drawing [state point lock?] - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid :drawing] resize-shape point lock?)))] + (update-in state [:workspace-local :drawing] resize-shape point lock?))] (ptk/reify ::handle-drawing-generic ptk/WatchEvent (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - {:keys [zoom flags]} (get-in state [:workspace pid]) + (let [{:keys [zoom flags]} (:workspace-local state) align? (refs/alignment-activated? flags) stoper? #(or (uws/mouse-up? %) (= % :interrupt)) @@ -165,30 +159,26 @@ (= 13 (:key event))))) (initialize-drawing [state point] - (let [pid (get-in state [:workspace :current])] - (-> state - (assoc-in [:workspace pid :drawing :segments] [point point]) - (assoc-in [:workspace pid :drawing ::initialized?] true)))) + (-> state + (assoc-in [:workspace-local :drawing :segments] [point point]) + (assoc-in [:workspace-local :drawing ::initialized?] true))) (insert-point-segment [state point] - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid :drawing :segments] (fnil conj []) point))) + (update-in state [:workspace-local :drawing :segments] (fnil conj []) point)) (update-point-segment [state index point] - (let [pid (get-in state [:workspace :current]) - segments (count (get-in state [:workspace pid :drawing :segments])) + (let [segments (count (get-in state [:workspace-local :drawing :segments])) exists? (< -1 index segments)] (cond-> state - exists? (assoc-in [:workspace pid :drawing :segments index] point)))) + exists? (assoc-in [:workspace-local :drawing :segments index] point)))) (remove-dangling-segmnet [state] - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid :drawing :segments] #(vec (butlast %)))))] + (update-in state [:workspace-local :drawing :segments] #(vec (butlast %))))] + (ptk/reify ::handle-drawing-path ptk/WatchEvent (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - {:keys [zoom flags]} (get-in state [:workspace pid]) + (let [{:keys [zoom flags]} (:workspace-local state) align? (refs/alignment-activated? flags) last-point (volatile! (gpt/divide @uws/mouse-position zoom)) @@ -252,29 +242,23 @@ (and (uws/mouse-event? event) (= type :up)))) (initialize-drawing [state] - (let [pid (get-in state [:workspace :current])] - (assoc-in state [:workspace pid :drawing ::initialized?] true))) + (assoc-in state [:workspace-local :drawing ::initialized?] true)) (insert-point-segment [state point] - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid :drawing :segments] (fnil conj []) point))) + (update-in state [:workspace-local :drawing :segments] (fnil conj []) point)) (simplify-drawing-path [state tolerance] - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid :drawing :segments] path/simplify tolerance)))] + (update-in state [:workspace-local :drawing :segments] path/simplify tolerance))] (ptk/reify ::handle-drawing-curve ptk/WatchEvent (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - {:keys [zoom flags]} (get-in state [:workspace pid]) - + (let [{:keys [zoom flags]} (:workspace-local state) align? (refs/alignment-activated? flags) stoper (rx/filter stoper-event? stream) mouse (->> (rx/sample 10 uws/mouse-position) (rx/mapcat #(conditional-align % align?)) (rx/map #(gpt/divide % zoom)))] - (rx/concat (rx/of initialize-drawing) (->> mouse @@ -287,8 +271,7 @@ (ptk/reify ::handle-finish-drawing ptk/WatchEvent (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - shape (get-in state [:workspace pid :drawing])] + (let [shape (get-in state [:workspace-local :drawing])] (rx/concat (rx/of dw/clear-drawing) (when (::initialized? shape) @@ -305,8 +288,7 @@ (ptk/reify ::close-drawing-path ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current])] - (assoc-in state [:workspace pid :drawing :close?] true))))) + (assoc-in state [:workspace-local :drawing :close?] true)))) ;; --- Components diff --git a/frontend/src/uxbox/main/ui/workspace/header.cljs b/frontend/src/uxbox/main/ui/workspace/header.cljs index be1758205..a00af580b 100644 --- a/frontend/src/uxbox/main/ui/workspace/header.cljs +++ b/frontend/src/uxbox/main/ui/workspace/header.cljs @@ -132,7 +132,7 @@ [:ul.options-btn [:li.tooltip.tooltip-bottom.view-mode {:alt (tr "header.view-mode") - :on-click #(st/emit! (dw/->OpenView (:id page))) + ;; :on-click #(st/emit! (dw/->OpenView (:id page))) } i/play]] [:& zoom-widget]] diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index 7856fefe8..0c7dc157a 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -72,7 +72,8 @@ (rx/map normalize-proportion-lock) (rx/mapcat (partial resize shape)) (rx/take-until stoper)) - (rx/of (dw/materialize-current-modifier-in-bulk ids)))))))) + (rx/of (dw/materialize-current-modifier-in-bulk ids) + ::dw/page-data-update))))))) ;; --- Controls (Component) @@ -221,21 +222,18 @@ [:& controls {:shape shape :zoom zoom :on-click on-click}])) (mf/defc selection-handlers - [{:keys [wst] :as props}] - (let [shapes-map (mf/deref refs/shapes-by-id) - shapes (map #(get shapes-map %) (:selected wst)) - edition (:edition wst) - zoom (:zoom wst 1) + [{:keys [selected edition zoom] :as props}] + (let [data (mf/deref refs/workspace-data) + shapes (map #(get-in data [:shapes-by-id %]) selected) num (count shapes) {:keys [id type] :as shape} (first shapes)] - (cond (zero? num) nil (> num 1) [:& multiple-selection-handlers {:shapes shapes - :selected (:selected wst) + :selected selected :zoom zoom}] (and (= type :text) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar.cljs index 977876d11..831b7a038 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar.cljs @@ -20,27 +20,27 @@ (mf/defc left-sidebar {:wrap [mf/wrap-memo]} - [{:keys [flags page] :as props}] + [{:keys [layout page] :as props}] [:aside.settings-bar.settings-bar-left [:div.settings-bar-inside - (when (contains? flags :sitemap) + (when (contains? layout :sitemap) [:& sitemap-toolbox {:project-id (:project-id page) :current-page-id (:id page) :page page}]) - (when (contains? flags :document-history) + (when (contains? layout :document-history) [:& history-toolbox]) - (when (contains? flags :layers) + (when (contains? layout :layers) [:& layers-toolbox {:page page}])]]) ;; --- Right Sidebar (Component) (mf/defc right-sidebar - [{:keys [flags page] :as props}] + [{:keys [layout page] :as props}] [:aside#settings-bar.settings-bar [:div.settings-bar-inside - (when (contains? flags :drawtools) - [:& draw-toolbox {:flags flags}]) - (when (contains? flags :element-options) + (when (contains? layout :drawtools) + [:& draw-toolbox {:layout layout}]) + (when (contains? layout :element-options) [:& options-toolbox {:page page}]) - #_(when (contains? flags :icons) + #_(when (contains? layout :icons) (icons-toolbox))]]) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs index dba0e52da..0a7e7f863 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -78,6 +78,7 @@ (mf/defc layer-item [{:keys [shape selected index] :as props}] + ;; (prn "layer-item" index (:name shape)) (letfn [(toggle-blocking [event] (dom/stop-propagation event) (let [{:keys [id blocked]} shape] @@ -107,7 +108,7 @@ (dw/select-shape id))))) (on-drop [item monitor] - (st/emit! (udp/persist-page (:page shape)))) + (st/emit! ::dw/page-data-update)) (on-hover [item monitor] (st/emit! (dw/change-shape-order {:id (:shape-id item) @@ -168,10 +169,9 @@ (dw/select-shape id))))) (on-drop [item monitor] - (st/emit! (udp/persist-page (:page canvas)))) + (st/emit! ::dw/page-data-update)) (on-hover [item monitor] - (prn "canvas-item$hover" (:id canvas)) (st/emit! (dw/change-canvas-order {:id (:canvas-id item) :index index})))] (let [selected? (contains? selected (:id canvas)) @@ -236,21 +236,25 @@ ;; --- Layers Toolbox (mf/defc layers-toolbox - [{:keys [page selected] :as props}] + [{:keys [page] :as props}] (let [on-click #(st/emit! (dw/toggle-flag :layers)) + selected (mf/deref refs/selected-shapes) - shapes-by-id (mf/deref shapes-iref) - canvas (->> (:canvas page) + data (mf/deref refs/workspace-data) + + shapes-by-id (:shapes-by-id data) + + canvas (->> (:canvas data) (map #(get shapes-by-id %)) (enumerate)) - all-shapes (->> (:shapes page) - (map #(get shapes-by-id %))) - shapes (->> all-shapes - (filter #(not (:canvas %))) - (enumerate)) + shapes (->> (:shapes data) + (map #(get shapes-by-id %))) - all-shapes (enumerate all-shapes)] + all-shapes (enumerate shapes) + unc-shapes (->> shapes + (filter #(nil? (:canvas %))) + (enumerate))] [:div#layers.tool-window [:div.tool-window-bar @@ -261,5 +265,5 @@ [:& canvas-list {:canvas canvas :shapes all-shapes :selected selected}] - [:& layers-list {:shapes shapes + [:& layers-list {:shapes unc-shapes :selected selected}]]])) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options.cljs index 361ff2dfc..3c5e9b4f6 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options.cljs @@ -11,7 +11,6 @@ [rumext.alpha :as mf] [rumext.core :as mx] [uxbox.builtins.icons :as i] - [uxbox.main.data.shapes :as uds] [uxbox.main.data.workspace :as udw] [uxbox.main.geom :as geom] [uxbox.main.store :as st] diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/circle_measures.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/circle_measures.cljs index 566a2c590..cc95ccf8c 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/circle_measures.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/circle_measures.cljs @@ -9,7 +9,6 @@ (:require [rumext.alpha :as mf] [uxbox.builtins.icons :as i] - [uxbox.main.data.shapes :as uds] [uxbox.main.data.workspace :as udw] [uxbox.main.geom :as geom] [uxbox.main.store :as st] diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs index 08f61bfd8..eb17169b1 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/icon_measures.cljs @@ -9,7 +9,6 @@ (:require [rumext.alpha :as mf] [uxbox.builtins.icons :as i] - [uxbox.main.data.shapes :as uds] [uxbox.main.data.workspace :as udw] [uxbox.main.geom :as geom] [uxbox.main.store :as st] diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect_measures.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect_measures.cljs index 8a691b42f..d4867c085 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect_measures.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/rect_measures.cljs @@ -9,7 +9,6 @@ (:require [rumext.alpha :as mf] [uxbox.builtins.icons :as i] - [uxbox.main.data.shapes :as uds] [uxbox.main.data.workspace :as udw] [uxbox.main.geom :as geom] [uxbox.main.store :as st] diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/text.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/text.cljs index c4aa179b0..b236c4fe4 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/text.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/text.cljs @@ -11,7 +11,6 @@ [uxbox.main.store :as st] [uxbox.main.geom :as geom] [uxbox.main.data.workspace :as udw] - [uxbox.main.data.shapes :as uds] [uxbox.builtins.icons :as i] [uxbox.util.i18n :refer (tr)] [uxbox.util.router :as r] diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs index 8f6cf98dd..b8ebafe2a 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs @@ -15,11 +15,12 @@ [uxbox.main.data.projects :as dp] [uxbox.main.data.workspace :as dw] [uxbox.main.store :as st] + [uxbox.main.refs :as refs] [uxbox.main.ui.confirm :refer [confirm-dialog]] [uxbox.main.ui.modal :as modal] [uxbox.main.ui.workspace.sidebar.sitemap-forms :refer [page-form-dialog]] [uxbox.main.ui.workspace.sortable :refer [use-sortable]] - [uxbox.util.data :refer [classnames]] + [uxbox.util.data :refer [classnames enumerate]] [uxbox.util.dom :as dom] [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as rt])) @@ -37,14 +38,12 @@ (dom/stop-propagation event) (modal/show! confirm-dialog {:on-accept delete})) (on-drop [item monitor] - (st/emit! (udp/rehash-pages (:project-id page)))) + (prn "TODO")) (on-hover [item monitor] - (st/emit! (udp/move-page {:project-id (:project-id item) - :page-id (:page-id item) - :index index})))] + (st/emit! (dw/change-page-order {:id (:id item) + :index index})))] (let [[dprops ref] (use-sortable {:type "page-item" - :data {:page-id (:id page) - :project-id (:project-id page) + :data {:id (:id page) :index index} :on-hover on-hover :on-drop on-drop})] @@ -64,40 +63,52 @@ (when deletable? [:a {:on-click on-delete} i/trash])]]]))) -;; --- Pages List -(defn- make-pages-iref - [{:keys [id pages] :as project}] - (-> (l/lens (fn [s] (into [] (map #(get-in s [:pages %])) pages))) - (l/derive st/state {:equals? =}))) +;; --- Page Item Wrapper -(def ^:private pages-map-iref - (-> (l/key :pages) +(defn- make-page-ref + [page-id] + (-> (l/in [:pages page-id]) (l/derive st/state))) +(mf/defc page-item-wrapper + [{:keys [page-id index deletable? selected?] :as props}] + (let [page-ref (mf/use-memo {:deps #js [page-id] + :fn #(make-page-ref page-id)}) + page (mf/deref page-ref)] + [:& page-item {:page page + :index index + :deletable? deletable? + :selected? selected?}])) + +;; --- Pages List + (mf/defc pages-list [{:keys [project current-page-id] :as props}] - (let [pages-map (mf/deref pages-map-iref) - pages (->> (vals pages-map) - (filter #(= (:project-id %) (:id project)))) + (let [pages (enumerate (:pages project)) deletable? (> (count pages) 1)] [:ul.element-list - (for [[index item] (map-indexed vector pages)] - [:& page-item {:page item - :index index - :deletable? deletable? - :selected? (= (:id item) current-page-id) - :key (:id item)}])])) + (for [[index page-id] pages] + [:& page-item-wrapper + {:page-id page-id + :index index + :deletable? deletable? + :selected? (= page-id current-page-id) + :key page-id}])])) ;; --- Sitemap Toolbox +(def ^:private workspace-project + (letfn [(selector [state] + (let [project-id (get-in state [:workspace-page :project-id])] + (get-in state [:projects project-id])))] + (-> (l/lens selector) + (l/derive st/state)))) + (mf/defc sitemap-toolbox [{:keys [project-id current-page-id] :as props}] - (let [project-iref (mf/use-memo {:deps #js [project-id] - :fn #(-> (l/in [:projects project-id]) - (l/derive st/state))}) - project (mf/deref project-iref) - create #(modal/show! page-form-dialog {:page {:project project-id}}) + (let [project (mf/deref workspace-project) + create #(modal/show! page-form-dialog {:page {:project-id project-id}}) close #(st/emit! (dw/toggle-flag :sitemap))] [:div.sitemap.tool-window [:div.tool-window-bar diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs index 5919e86c0..ab0987553 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs @@ -12,6 +12,7 @@ [uxbox.builtins.icons :as i] [uxbox.main.constants :as c] [uxbox.main.data.pages :as udp] + [uxbox.main.data.workspace :as udw] [uxbox.main.store :as st] [uxbox.main.ui.modal :as modal] [uxbox.util.dom :as dom] @@ -20,22 +21,12 @@ [uxbox.util.i18n :refer [tr]])) (s/def ::id ::us/uuid) -(s/def ::project ::us/uuid) +(s/def ::project-id ::us/uuid) (s/def ::name ::us/not-empty-string) -(s/def ::width ::us/number-str) -(s/def ::height ::us/number-str) (s/def ::page-form - (s/keys :req-un [::id - ::project - ::name - ::width - ::height])) - -(def defaults - {:name "" - :width "1366" - :height "768"}) + (s/keys :req-un [::project-id ::name] + :opt-un [::id])) (defn- on-submit [event form] @@ -43,20 +34,13 @@ (modal/hide!) (let [data (:clean-data form)] (if (nil? (:id data)) - (st/emit! (udp/form->create-page data)) - (st/emit! (udp/form->update-page data))))) - -(defn- swap-size - [event {:keys [data] :as form}] - (swap! data assoc - :width (:height data) - :height (:width data))) + (st/emit! (udp/create-page data)) + (st/emit! (udp/rename-page data))))) (defn- initial-data [page] - (merge {:name "" :width "1366" :height "768"} - (select-keys page [:name :id :project]) - (select-keys (:metadata page) [:width :height]))) + (merge {:name ""} + (select-keys page [:name :id :project-id]))) (mf/defc page-form [{:keys [page] :as props}] @@ -71,32 +55,6 @@ :on-change (fm/on-input-change form :name) :value (:name data) :auto-focus true}] - [:div.project-size - [:div.input-element.pixels - [:span (tr "ds.width")] - [:input#project-witdh.input-text - {:placeholder (tr "ds.width") - :name "width" - :type "number" - :min 0 - :max 5000 - :class (fm/error-class form :width) - :on-blur (fm/on-input-blur form :width) - :on-change (fm/on-input-change form :width) - :value (:width data)}]] - [:a.toggle-layout {:on-click #(swap-size % form)} i/toggle] - [:div.input-element.pixels - [:span (tr "ds.height")] - [:input#project-height.input-text - {:placeholder (tr "ds.height") - :name "height" - :type "number" - :min 0 - :max 5000 - :class (fm/error-class form :height) - :on-blur (fm/on-input-blur form :height) - :on-change (fm/on-input-change form :height) - :value (:height data)}]]] [:input.btn-primary {:value (tr "ds.go") :type "submit" diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index f5ed5a4ba..ac9b909c8 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -22,7 +22,7 @@ [uxbox.main.ui.workspace.streams :as uws] [uxbox.main.ui.workspace.drawarea :refer [start-drawing]] - [uxbox.main.ui.shapes :as uus] + [uxbox.main.ui.shapes :refer [shape-wrapper]] [uxbox.main.ui.workspace.drawarea :refer [draw-area]] [uxbox.main.ui.workspace.selection :refer [selection-handlers]] @@ -74,17 +74,15 @@ (def ^:private handle-selrect (letfn [(update-state [state position] - (let [id (get-in state [:workspace :current]) - selrect (get-in state [:workspace id :selrect])] + (let [selrect (get-in state [:workspace-local :selrect])] (if selrect - (assoc-in state [:workspace id :selrect] + (assoc-in state [:workspace-local :selrect] (dw/selection->rect (assoc selrect :stop position))) - (assoc-in state [:workspace id :selrect] + (assoc-in state [:workspace-local :selrect] (dw/selection->rect {:start position :stop position}))))) (clear-state [state] - (let [id (get-in state [:workspace :current])] - (update-in state [:workspace id] dissoc :selrect)))] + (update state :workspace-local dissoc :selrect))] (ptk/reify ::handle-selrect ptk/WatchEvent (watch [_ state stream] @@ -129,11 +127,28 @@ ;; --- Viewport +(mf/defc canvas-and-shapes + {:wrap [mf/wrap-memo]} + [props] + (let [data (mf/deref refs/workspace-data) + shapes-by-id (:shapes-by-id data) + shapes (map #(get shapes-by-id %) (:shapes data [])) + canvas (map #(get shapes-by-id %) (:canvas data []))] + [:* + (for [item canvas] + [:& shape-wrapper {:shape item :key (:id item)}]) + (for [item (reverse shapes)] + [:& shape-wrapper {:shape item :key (:id item)}])])) + (mf/defc viewport [{:keys [page] :as props}] - (let [{:keys [drawing-tool tooltip zoom flags edition] :as wst} (mf/deref refs/workspace) + (let [{:keys [drawing-tool + zoom + flags + edition + selected] + :as local} (mf/deref refs/workspace-local) viewport-ref (mf/use-ref nil) - tooltip (or tooltip (get-shape-tooltip drawing-tool)) zoom (or zoom 1)] (letfn [(on-mouse-down [event] (dom/stop-propagation event) @@ -230,9 +245,6 @@ ;; (prn "viewport$render") [:* [:& coordinates {:zoom zoom}] - #_[:div.tooltip-container - (when tooltip - [:& cursor-tooltip {:tooltip tooltip}])] [:svg.viewport {:width (* c/viewport-width zoom) :height (* c/viewport-height zoom) :ref viewport-ref @@ -244,23 +256,23 @@ :on-mouse-down on-mouse-down :on-mouse-up on-mouse-up} [:g.zoom {:transform (str "scale(" zoom ", " zoom ")")} - (when page - [:* - [:& uus/canvas-and-shapes {:page page}] - - (when (seq (:selected wst)) - [:& selection-handlers {:wst wst}]) - - (when-let [dshape (:drawing wst)] - [:& draw-area {:shape dshape - :zoom (:zoom wst) - :modifiers (:modifiers wst)}])]) + [:* + [:& canvas-and-shapes] + (when (seq selected) + [:& selection-handlers {:selected selected + :zoom zoom + :edition edition}]) + (when-let [drawing-shape (:drawing local)] + [:& draw-area {:shape drawing-shape + :zoom zoom + :modifiers (:modifiers local)}])] (if (contains? flags :grid) [:& grid {:page page}])] - (when (contains? flags :ruler) - [:& ruler {:zoom zoom :ruler (:ruler wst)}]) - [:& selrect {:data (:selrect wst)}]]]))) + (when (contains? flags :ruler) + [:& ruler {:zoom zoom :ruler (:ruler local)}]) + + [:& selrect {:data (:selrect local)}]]]))) diff --git a/frontend/src/uxbox/util/spec.cljs b/frontend/src/uxbox/util/spec.cljs index 60d35b574..4bbe132f7 100644 --- a/frontend/src/uxbox/util/spec.cljs +++ b/frontend/src/uxbox/util/spec.cljs @@ -65,7 +65,7 @@ [v] (cond (number? v) v - (re-matches number-rx v) (js/parseFloat v) + (and (string? v) (re-matches number-rx v)) (js/parseFloat v) :else ::s/invalid)) (s/def ::number diff --git a/frontend/test/uxbox/tests/test_main_data_shapes_impl.cljs b/frontend/test/uxbox/tests/test_main_data_shapes_impl.cljs index 57042943a..2d634a51b 100644 --- a/frontend/test/uxbox/tests/test_main_data_shapes_impl.cljs +++ b/frontend/test/uxbox/tests/test_main_data_shapes_impl.cljs @@ -1,5 +1,5 @@ (ns uxbox.tests.test-main-data-shapes-impl - (:require [cljs.test :as t :include-macros true] + #_(:require [cljs.test :as t :include-macros true] [cljs.pprint :refer [pprint]] [uxbox.util.uuid :as uuid] [uxbox.main.data.shapes :as impl])) @@ -8,119 +8,119 @@ ;; Duplicate (one shape) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn constantly-inc - [init] - (let [v (atom init)] - (fn [& args] - (let [result @v] - (swap! v inc) - result)))) +;; (defn constantly-inc +;; [init] +;; (let [v (atom init)] +;; (fn [& args] +;; (let [result @v] +;; (swap! v inc) +;; result)))) -;; duplicate shape: duplicate simple shape -(t/deftest duplicate-shapes-test1 - (let [initial {:pages {1 {:id 1 :shapes [1]}} - :shapes {1 {:id 1 :page 1 :name "a"}}} +;; ;; duplicate shape: duplicate simple shape +;; (t/deftest duplicate-shapes-test1 +;; (let [initial {:pages {1 {:id 1 :shapes [1]}} +;; :shapes {1 {:id 1 :page 1 :name "a"}}} - expected (-> initial - (assoc-in [:pages 1 :shapes] [2 1]) - (assoc-in [:shapes 2] {:id 2 :page 1 :name "a-copy-1"}))] +;; expected (-> initial +;; (assoc-in [:pages 1 :shapes] [2 1]) +;; (assoc-in [:shapes 2] {:id 2 :page 1 :name "a-copy-1"}))] - (with-redefs [uxbox.util.uuid/random (constantly 2)] - (let [result (impl/duplicate-shapes initial [1])] - ;; (pprint expected) - ;; (pprint result) - (t/is (= result expected)))))) +;; (with-redefs [uxbox.util.uuid/random (constantly 2)] +;; (let [result (impl/duplicate-shapes initial [1])] +;; ;; (pprint expected) +;; ;; (pprint result) +;; (t/is (= result expected)))))) -;; duplicate shape: duplicate inside group -(t/deftest duplicate-shapes-test2 - (let [initial {:pages {1 {:id 1 :shapes [1]}} - :shapes {1 {:id 1 :name "1" :page 1 - :type :group - :items [2 3]} - 2 {:id 2 :name "2" :page 1 :group 1} - 3 {:id 3 :name "3" :page 1 :group 1}}} +;; ;; duplicate shape: duplicate inside group +;; (t/deftest duplicate-shapes-test2 +;; (let [initial {:pages {1 {:id 1 :shapes [1]}} +;; :shapes {1 {:id 1 :name "1" :page 1 +;; :type :group +;; :items [2 3]} +;; 2 {:id 2 :name "2" :page 1 :group 1} +;; 3 {:id 3 :name "3" :page 1 :group 1}}} - expected (-> initial - (assoc-in [:shapes 1 :items] [5 4 2 3]) - (assoc-in [:shapes 4] {:id 4 :name "3-copy-1" :page 1 :group 1}) - (assoc-in [:shapes 5] {:id 5 :name "2-copy-1" :page 1 :group 1}))] - (with-redefs [uxbox.util.uuid/random (constantly-inc 4)] - (let [result (impl/duplicate-shapes initial [2 3])] - ;; (pprint expected) - ;; (pprint result) - (t/is (= result expected)))))) +;; expected (-> initial +;; (assoc-in [:shapes 1 :items] [5 4 2 3]) +;; (assoc-in [:shapes 4] {:id 4 :name "3-copy-1" :page 1 :group 1}) +;; (assoc-in [:shapes 5] {:id 5 :name "2-copy-1" :page 1 :group 1}))] +;; (with-redefs [uxbox.util.uuid/random (constantly-inc 4)] +;; (let [result (impl/duplicate-shapes initial [2 3])] +;; ;; (pprint expected) +;; ;; (pprint result) +;; (t/is (= result expected)))))) -;; duplicate shape: duplicate mixed bag -(t/deftest duplicate-shapes-test3 - (let [initial {:pages {1 {:id 1 :shapes [1 4]}} - :shapes {1 {:id 1 :name "1" :page 1 - :type :group - :items [2 3]} - 2 {:id 2 :name "2" :page 1 :group 1} - 3 {:id 3 :name "3" :page 1 :group 1} - 4 {:id 4 :name "4" :page 1}}} +;; ;; duplicate shape: duplicate mixed bag +;; (t/deftest duplicate-shapes-test3 +;; (let [initial {:pages {1 {:id 1 :shapes [1 4]}} +;; :shapes {1 {:id 1 :name "1" :page 1 +;; :type :group +;; :items [2 3]} +;; 2 {:id 2 :name "2" :page 1 :group 1} +;; 3 {:id 3 :name "3" :page 1 :group 1} +;; 4 {:id 4 :name "4" :page 1}}} - expected (-> initial - (assoc-in [:pages 1 :shapes] [6 5 1 4]) - (assoc-in [:shapes 5] {:id 5 :name "4-copy-1" :page 1}) - (assoc-in [:shapes 6] {:id 6 :name "3-copy-1" :page 1}))] - (with-redefs [uxbox.util.uuid/random (constantly-inc 5)] - (let [result (impl/duplicate-shapes initial [3 4])] - ;; (pprint expected) - ;; (pprint result) - (t/is (= result expected)))))) +;; expected (-> initial +;; (assoc-in [:pages 1 :shapes] [6 5 1 4]) +;; (assoc-in [:shapes 5] {:id 5 :name "4-copy-1" :page 1}) +;; (assoc-in [:shapes 6] {:id 6 :name "3-copy-1" :page 1}))] +;; (with-redefs [uxbox.util.uuid/random (constantly-inc 5)] +;; (let [result (impl/duplicate-shapes initial [3 4])] +;; ;; (pprint expected) +;; ;; (pprint result) +;; (t/is (= result expected)))))) -;; duplicate shape: duplicate one group -(t/deftest duplicate-shapes-test4 - (let [initial {:pages {1 {:id 1 :shapes [1]}} - :shapes {1 {:id 1 - :name "1" - :page 1 - :type :group - :items [2]} - 2 {:id 2 - :name "2" - :page 1 - :group 1}}} +;; ;; duplicate shape: duplicate one group +;; (t/deftest duplicate-shapes-test4 +;; (let [initial {:pages {1 {:id 1 :shapes [1]}} +;; :shapes {1 {:id 1 +;; :name "1" +;; :page 1 +;; :type :group +;; :items [2]} +;; 2 {:id 2 +;; :name "2" +;; :page 1 +;; :group 1}}} - expected (-> initial - (assoc-in [:pages 1 :shapes] [3 1]) - (assoc-in [:shapes 3] {:id 3 - :name "1-copy-1" - :page 1 - :type :group - :items [4]}) - (assoc-in [:shapes 4] {:id 4 - :name "2-copy-1" - :page 1 - :group 3}))] - (with-redefs [uxbox.util.uuid/random (constantly-inc 3)] - (let [result (impl/duplicate-shapes initial [1])] - ;; (pprint expected) - ;; (pprint result) - (t/is (= result expected)))))) +;; expected (-> initial +;; (assoc-in [:pages 1 :shapes] [3 1]) +;; (assoc-in [:shapes 3] {:id 3 +;; :name "1-copy-1" +;; :page 1 +;; :type :group +;; :items [4]}) +;; (assoc-in [:shapes 4] {:id 4 +;; :name "2-copy-1" +;; :page 1 +;; :group 3}))] +;; (with-redefs [uxbox.util.uuid/random (constantly-inc 3)] +;; (let [result (impl/duplicate-shapes initial [1])] +;; ;; (pprint expected) +;; ;; (pprint result) +;; (t/is (= result expected)))))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Delete Shape -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; ;; Delete Shape +;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; delete shape: delete from page -(t/deftest delete-shape-test1 - (let [initial {:pages {1 {:id 1 :shapes [1 3 4]}} - :shapes {1 {:id 1 :page 1 - :type :group - :items [2]} - 2 {:id 2 :page 1 :group 1} - 3 {:id 3 :page 1} - 4 {:id 4 :page 1}}} +;; ;; delete shape: delete from page +;; (t/deftest delete-shape-test1 +;; (let [initial {:pages {1 {:id 1 :shapes [1 3 4]}} +;; :shapes {1 {:id 1 :page 1 +;; :type :group +;; :items [2]} +;; 2 {:id 2 :page 1 :group 1} +;; 3 {:id 3 :page 1} +;; 4 {:id 4 :page 1}}} - shape (get-in initial [:shapes 4]) - expected {:pages {1 {:id 1 :shapes [1 3]}} - :shapes {1 {:id 1 :page 1 :type :group :items [2]} - 2 {:id 2 :page 1 :group 1} - 3 {:id 3 :page 1}}} +;; shape (get-in initial [:shapes 4]) +;; expected {:pages {1 {:id 1 :shapes [1 3]}} +;; :shapes {1 {:id 1 :page 1 :type :group :items [2]} +;; 2 {:id 2 :page 1 :group 1} +;; 3 {:id 3 :page 1}}} - result (impl/dissoc-shape initial shape)] - ;; (pprint expected) - ;; (pprint result) - (t/is (= result expected)))) +;; result (impl/dissoc-shape initial shape)] +;; ;; (pprint expected) +;; ;; (pprint result) +;; (t/is (= result expected))))