diff --git a/backend/bin/start b/backend/bin/start-dev similarity index 75% rename from backend/bin/start rename to backend/bin/start-dev index 178a4bac3..1ea95d260 100755 --- a/backend/bin/start +++ b/backend/bin/start-dev @@ -8,7 +8,6 @@ if [ ! -e ~/local/.fixtures-loaded ]; then touch ~/local/.fixtures-loaded fi -clojure -J-XX:-OmitStackTraceInFastThrow -m uxbox.main - +clojure ${CLOJURE_OPTS} -m uxbox.main diff --git a/backend/bin/start-prod b/backend/bin/start-prod new file mode 100755 index 000000000..3ea781c07 --- /dev/null +++ b/backend/bin/start-prod @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +clojure ${CLOJURE_OPTIONS} -m uxbox.main diff --git a/backend/deps.edn b/backend/deps.edn index 422baadde..69eeb6fdf 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -61,15 +61,17 @@ :test {:extra-deps {lambdaisland/kaocha {:mvn/version "0.0-554"}}} - :nrepl - {:extra-deps {nrepl/nrepl {:mvn/version "0.6.0"}} - :main-opts ["-m" "nrepl.cmdline"]} - :ancient {:main-opts ["-m" "deps-ancient.deps-ancient"] :extra-deps {deps-ancient {:mvn/version "RELEASE"}}} + :jmx-remote + {:jvm-opts ["-Dcom.sun.management.jmxremote" + "-Dcom.sun.management.jmxremote.port=9090" + "-Dcom.sun.management.jmxremote.rmi.port=9090" + "-Dcom.sun.management.jmxremote.local.only=false" + "-Dcom.sun.management.jmxremote.authenticate=false" + "-Dcom.sun.management.jmxremote.ssl=false" + "-Djava.rmi.server.hostname=localhost"]} + }} - - - diff --git a/backend/src/uxbox/http.clj b/backend/src/uxbox/http.clj index e7277a27c..7c78ecc17 100644 --- a/backend/src/uxbox/http.clj +++ b/backend/src/uxbox/http.clj @@ -16,6 +16,7 @@ [uxbox.http.session :as session] [uxbox.http.handlers :as handlers] [uxbox.http.debug :as debug] + [uxbox.http.ws :as ws] [vertx.core :as vc] [vertx.http :as vh] [vertx.web :as vw] @@ -43,7 +44,12 @@ interceptors/format-response-body (vxi/errors errors/handle)] - routes [["/api" {:interceptors interceptors} + routes [["/sub/:page-id" {:interceptors [(vxi/cookies) + (vxi/cors cors-opts) + (session/auth)] + :get ws/handler}] + + ["/api" {:interceptors interceptors} ["/echo" {:all handlers/echo-handler}] ["/login" {:post handlers/login-handler}] ["/logout" {:post handlers/logout-handler}] diff --git a/backend/src/uxbox/http/handlers.clj b/backend/src/uxbox/http/handlers.clj index c21d3d1d9..ba7103876 100644 --- a/backend/src/uxbox/http/handlers.clj +++ b/backend/src/uxbox/http/handlers.clj @@ -12,7 +12,9 @@ [uxbox.services.init] [uxbox.services.mutations :as sm] [uxbox.services.queries :as sq] - [uxbox.util.uuid :as uuid])) + [uxbox.util.uuid :as uuid] + [vertx.web :as vw] + [vertx.eventbus :as ve])) (defn query-handler [req] @@ -45,7 +47,7 @@ (p/then #(session/create (:id %) user-agent)) (p/then' (fn [token] {:status 204 - :cookies {"auth-token" {:value token}} + :cookies {"auth-token" {:value token :path "/"}} :body ""}))))) (defn logout-handler diff --git a/backend/src/uxbox/http/session.clj b/backend/src/uxbox/http/session.clj index f85b12867..7dad11bdb 100644 --- a/backend/src/uxbox/http/session.clj +++ b/backend/src/uxbox/http/session.clj @@ -56,7 +56,7 @@ (spx/terminate (assoc data ::unauthorized true))))) (vc/handle-on-context)))) :leave (fn [data] - (if (::unauthorized data) + (if (and (::unauthorized data) (:response data)) (update data :response assoc :status 403 :body {:type :authentication :code :unauthorized}) diff --git a/backend/src/uxbox/http/ws.clj b/backend/src/uxbox/http/ws.clj new file mode 100644 index 000000000..321278fa9 --- /dev/null +++ b/backend/src/uxbox/http/ws.clj @@ -0,0 +1,105 @@ +;; 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) 2019 Andrey Antukh + +(ns uxbox.http.ws + "Web Socket handlers" + (:require + [promesa.core :as p] + [uxbox.emails :as emails] + [uxbox.http.session :as session] + [uxbox.services.init] + [uxbox.services.mutations :as sm] + [uxbox.services.queries :as sq] + [uxbox.util.uuid :as uuid] + [uxbox.util.blob :as blob] + [vertx.http :as vh] + [vertx.web :as vw] + [vertx.util :as vu] + [vertx.eventbus :as ve]) + (:import + io.vertx.core.Future + io.vertx.core.Promise + io.vertx.core.Handler + io.vertx.core.Vertx + io.vertx.core.buffer.Buffer + io.vertx.core.http.HttpServerRequest + io.vertx.core.http.HttpServerResponse + io.vertx.core.http.ServerWebSocket)) + +(declare ws-websocket) +(declare ws-send!) +(declare ws-on-message!) +(declare ws-on-close!) + +;; --- Public API + +(declare on-message) +(declare on-close) +(declare on-eventbus-message) + +(def state (atom {})) + +(defn handler + [{:keys [user] :as req}] + (letfn [(on-init [ws] + (let [vsm (::vw/execution-context req) + tpc "test.foobar" + pid (get-in req [:path-params :page-id]) + sem (ve/consumer vsm tpc #(on-eventbus-message ws %2))] + (swap! state update pid (fnil conj #{}) user) + (assoc ws ::sem sem))) + + (on-message [ws message] + (let [pid (get-in req [:path-params :page-id])] + (ws-send! ws (str (::counter ws 0))) + (update ws ::counter (fnil inc 0)))) + + (on-close [ws] + (let [pid (get-in req [:path-params :page-id])] + (swap! state update pid disj user) + (.unregister (::sem ws))))] + + ;; (ws-websocket :on-init on-init + ;; :on-message on-message + ;; :on-close on-close))) + + (-> (ws-websocket) + (assoc :on-init on-init + :on-message on-message + :on-close on-close)))) + +(defn- on-eventbus-message + [ws {:keys [body] :as message}] + (ws-send! ws body)) + +;; --- Internal (vertx api) (experimental) + +(defrecord WebSocket [on-init on-message on-close] + vh/IAsyncResponse + (-handle-response [this ctx] + (let [^HttpServerRequest req (::vh/request ctx) + ^ServerWebSocket ws (.upgrade req) + local (volatile! (assoc this :ws ws))] + (-> (p/do! (on-init @local)) + (p/then (fn [data] + (vreset! local data) + (.textMessageHandler ws (vu/fn->handler + (fn [msg] + (-> (p/do! (on-message @local msg)) + (p/then (fn [data] + (when (instance? WebSocket data) + (vreset! local data)) + (.fetch ws 1))))))) + (.closeHandler ws (vu/fn->handler (fn [& args] (on-close @local)))))))))) + +(defn ws-websocket + [] + (->WebSocket nil nil nil)) + +(defn ws-send! + [ws msg] + (.writeTextMessage ^ServerWebSocket (:ws ws) + ^String msg)) diff --git a/backend/src/uxbox/services/mutations/project_pages.clj b/backend/src/uxbox/services/mutations/project_pages.clj index 191f654ca..fe81faa7f 100644 --- a/backend/src/uxbox/services/mutations/project_pages.clj +++ b/backend/src/uxbox/services/mutations/project_pages.clj @@ -14,21 +14,21 @@ [uxbox.services.queries.project-pages :refer [decode-row]] [uxbox.services.util :as su] [uxbox.common.pages :as cp] - [uxbox.common.spec :as cs] [uxbox.util.exceptions :as ex] [uxbox.util.blob :as blob] [uxbox.util.sql :as sql] + [uxbox.util.spec :as us] [uxbox.util.uuid :as uuid])) ;; --- Helpers & Specs -(s/def ::id ::cs/uuid) -(s/def ::name ::cs/string) +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) (s/def ::data ::cp/data) -(s/def ::user ::cs/uuid) -(s/def ::project-id ::cs/uuid) +(s/def ::user ::us/uuid) +(s/def ::project-id ::us/uuid) (s/def ::metadata ::cp/metadata) -(s/def ::ordering ::cs/number) +(s/def ::ordering ::us/number) ;; --- Mutation: Create Page @@ -157,21 +157,22 @@ :stored-version (:version page)})) (let [ops (:operations params) data (-> (:data page) - (blob/decode) - (cp/process-ops ops) - (blob/encode)) + (blob/decode) + (cp/process-ops ops) + (blob/encode)) + page (assoc page :user-id (:user params) :data data :version (inc (:version page)) :operations (blob/encode ops))] + (-> (update-page-data conn page) (p/then (fn [_] (insert-page-snapshot conn page))) (p/then (fn [s] (retrieve-lagged-operations conn s params)))))) (su/defstr sql:lagged-snapshots - "select s.id, s.page_id, s.version, s.operations, - s.created_at, s.modified_at, s.user_id + "select s.id, s.operations from project_page_snapshots as s where s.page_id = $1 and s.version > $2") @@ -179,8 +180,14 @@ (defn- retrieve-lagged-operations [conn snapshot params] (let [sql sql:lagged-snapshots] - (-> (db/query conn [sql (:id params) (:version params)]) - (p/then (partial mapv decode-row))))) + (-> (db/query conn [sql (:id params) (:version params) #_(:id snapshot)]) + (p/then (fn [rows] + {:id (:id params) + :version (:version snapshot) + :operations (into [] (comp (map decode-row) + (map :operations) + (mapcat identity)) + rows)}))))) ;; --- Mutation: Delete Page @@ -219,7 +226,7 @@ ;; (some-> (db/fetch-one conn sqlv) ;; (decode-row)))) -;; (s/def ::label ::cs/string) +;; (s/def ::label ::us/string) ;; (s/def ::update-page-history ;; (s/keys :req-un [::user ::id ::pinned ::label])) diff --git a/backend/src/uxbox/services/queries/project_pages.clj b/backend/src/uxbox/services/queries/project_pages.clj index 2411edb98..decf5b74e 100644 --- a/backend/src/uxbox/services/queries/project_pages.clj +++ b/backend/src/uxbox/services/queries/project_pages.clj @@ -111,13 +111,13 @@ (s/def ::pinned ::us/boolean) (s/def ::since ::us/integer) -(s/def ::page-history +(s/def ::project-page-snapshots (s/keys :req-un [::page-id ::user] :opt-un [::max ::pinned ::since])) -(defn retrieve-page-history - [{:keys [page-id user since max pinned] :or {since Long/MAX_VALUE max 10}}] - (let [sql (-> (sql/from ["pages_history" "ph"]) +(defn retrieve-page-snapshots + [conn {:keys [page-id user since max pinned] :or {since Long/MAX_VALUE max 10}}] + (let [sql (-> (sql/from ["project_page_snapshots" "ph"]) (sql/select "ph.*") (sql/where ["ph.user_id = ?" user] ["ph.page_id = ?" page-id] @@ -126,15 +126,14 @@ ["ph.pinned = ?" true])) (sql/order "ph.version desc") (sql/limit max))] - (-> (db/query db/pool (sql/fmt sql)) + (-> (db/query conn (sql/fmt sql)) (p/then (partial mapv decode-row))))) -(sq/defquery ::page-history +(sq/defquery ::project-page-snapshots [{:keys [page-id user] :as params}] (db/with-atomic [conn db/pool] (p/do! (retrieve-page conn {:id page-id :user user}) - (retrieve-page-history conn params)))) - + (retrieve-page-snapshots conn params)))) ;; --- Helpers diff --git a/backend/src/uxbox/util/exceptions.clj b/backend/src/uxbox/util/exceptions.clj index c32522f17..b39a9e8ce 100644 --- a/backend/src/uxbox/util/exceptions.clj +++ b/backend/src/uxbox/util/exceptions.clj @@ -8,8 +8,6 @@ "A helpers for work with exceptions." (:require [clojure.spec.alpha :as s])) -;; TODO: moved to uxbox.common.exceptions - (s/def ::type keyword?) (s/def ::code keyword?) (s/def ::mesage string?) diff --git a/backend/src/uxbox/util/spec.clj b/backend/src/uxbox/util/spec.clj index 2cc26f221..efd8561d9 100644 --- a/backend/src/uxbox/util/spec.clj +++ b/backend/src/uxbox/util/spec.clj @@ -10,6 +10,7 @@ [clojure.spec.alpha :as s] [cuerdas.core :as str] [datoteka.core :as fs] + [expound.alpha :as expound] [uxbox.util.exceptions :as ex]) (:import java.time.Instant)) @@ -23,23 +24,18 @@ (def uuid-rx #"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") -(def number-rx - #"^[+-]?([0-9]*\.?[0-9]+|[0-9]+\.?[0-9]*)([eE][+-]?[0-9]+)?$") - ;; --- Public Api (defn conform [spec data] (let [result (s/conform spec data)] - (if (= result ::s/invalid) - [nil (s/explain-data spec data)] - [result nil]))) - -(defn valid? - [spec data] - (if (s/valid? spec data) - true - (s/explain spec data))) + (when (= result ::s/invalid) + (throw (ex/error :type :validation + :code :spec-validation + :explain (with-out-str + (expound/printer data)) + :data (::s/problems data)))) + result)) ;; --- Predicates @@ -113,7 +109,7 @@ [v] (cond (number? v) v - (re-matches number-rx v) (Double/parseDouble v) + (str/numeric? v) (Double/parseDouble v) :else ::s/invalid)) diff --git a/backend/src/vertx/http.clj b/backend/src/vertx/http.clj index 86ee729d0..da6a27640 100644 --- a/backend/src/vertx/http.clj +++ b/backend/src/vertx/http.clj @@ -23,7 +23,8 @@ io.vertx.core.http.HttpServer io.vertx.core.http.HttpServerRequest io.vertx.core.http.HttpServerResponse - io.vertx.core.http.HttpServerOptions)) + io.vertx.core.http.HttpServerOptions + io.vertx.core.http.ServerWebSocket)) (declare opts->http-server-options) (declare resolve-handler) @@ -126,7 +127,14 @@ (let [body (:body data) res (::response ctx)] (assign-status-and-headers! res data) - (-handle-body body res)))) + (-handle-body body res))) + + ;; ServerWebSocket + ;; (-handle-response [sws ctx] + ;; (.accept ^ServerWebSocket sws)) + + nil + (-handle-response [sws ctx])) (extend-protocol IAsyncBody (Class/forName "[B") diff --git a/backend/src/vertx/web.clj b/backend/src/vertx/web.clj index eafa876f5..4172ddb26 100644 --- a/backend/src/vertx/web.clj +++ b/backend/src/vertx/web.clj @@ -26,6 +26,7 @@ io.vertx.core.http.HttpServerOptions io.vertx.core.http.HttpServerRequest io.vertx.core.http.HttpServerResponse + io.vertx.core.http.ServerWebSocket io.vertx.ext.web.Route io.vertx.ext.web.Router io.vertx.ext.web.RoutingContext @@ -140,7 +141,6 @@ (.setDeleteUploadedFilesOnEnd delete-uploads?) (.setUploadsDirectory upload-dir))) - (.handler (reify Handler (handle [_ rc] diff --git a/backend/test/uxbox/tests/test_services_project_pages.clj b/backend/test/uxbox/tests/test_services_project_pages.clj index 2c8036a7d..06bff02a7 100644 --- a/backend/test/uxbox/tests/test_services_project_pages.clj +++ b/backend/test/uxbox/tests/test_services_project_pages.clj @@ -117,13 +117,42 @@ ;; (th/print-result! out) (t/is (nil? (:error out))) - (t/is (= 1 (count (:result out)))) - (t/is (= (:id data) (get-in out [:result 0 :page-id]))) - (t/is (= 1 (count (get-in out [:result 0 :operations])))) - (t/is (= :add-shape (get-in out [:result 0 :operations 0 0]))) - (t/is (= sid (get-in out [:result 0 :operations 0 1]))) + (t/is (= 0 (count (:result out)))) + ;; (t/is (= 1 (count (:result out)))) + ;; (t/is (= (:id data) (get-in out [:result 0 :page-id]))) + ;; (t/is (= 1 (count (get-in out [:result 0 :operations])))) + ;; (t/is (= :add-shape (get-in out [:result 0 :operations 0 0]))) + ;; (t/is (= sid (get-in out [:result 0 :operations 0 1]))) )) +(t/deftest mutation-update-project-page-3 + (let [user @(th/create-user db/pool 1) + proj @(th/create-project db/pool (:id user) 1) + file @(th/create-project-file db/pool (:id user) (:id proj) 1) + page @(th/create-project-page db/pool (:id user) (:id file) 1) + + sid (uuid/next) + data {::sm/type :update-project-page + :id (:id page) + :version 0 + :user (:id user) + :operations [[:add-shape sid {:id sid :type :rect}]]} + + out1 (th/try-on! (sm/handle data)) + out2 (th/try-on! (sm/handle data))] + + ;; (th/print-result! out1) + ;; (th/print-result! out2) + + (t/is (nil? (:error out1))) + (t/is (nil? (:error out2))) + (t/is (= 0 (count (:result out1)))) + (t/is (= 1 (count (:result out2)))) + (t/is (= (:id data) (get-in out2 [:result 0 :page-id]))) + (t/is (= 1 (count (get-in out2 [:result 0 :operations])))) + (t/is (= :add-shape (get-in out2 [:result 0 :operations 0 0]))) + (t/is (= sid (get-in out2 [:result 0 :operations 0 1]))) + )) (t/deftest mutation-delete-project-page (let [user @(th/create-user db/pool 1) diff --git a/common/uxbox/common/data.cljc b/common/uxbox/common/data.cljc new file mode 100644 index 000000000..1c2ee5c28 --- /dev/null +++ b/common/uxbox/common/data.cljc @@ -0,0 +1,50 @@ +;; 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) 2016-2019 Andrey Antukh + +(ns uxbox.common.data + "Data manipulation and query helper functions." + (:refer-clojure :exclude [concat]) + (:require [clojure.set :as set])) + +(defn concat + [& colls] + (loop [result (first colls) + colls (rest colls)] + (if (seq colls) + (recur (reduce conj result (first colls)) + (rest colls)) + result))) + +(defn seek + ([pred coll] + (seek pred coll nil)) + ([pred coll not-found] + (reduce (fn [_ x] + (if (pred x) + (reduced x) + not-found)) + not-found coll))) + +(defn diff-maps + [ma mb] + (let [ma-keys (set (keys ma)) + mb-keys (set (keys mb)) + added (set/difference mb-keys ma-keys) + removed (set/difference ma-keys mb-keys) + both (set/intersection ma-keys mb-keys)] + (concat + (mapv #(vector :add % (get mb %)) added) + (mapv #(vector :del % nil) removed) + (loop [k (first both) + r (rest both) + rs []] + (if k + (let [vma (get ma k) + vmb (get mb k)] + (if (= vma vmb) + (recur (first r) (rest r) rs) + (recur (first r) (rest r) (conj rs [:mod k vmb])))) + rs))))) diff --git a/common/uxbox/common/exceptions.cljc b/common/uxbox/common/exceptions.cljc deleted file mode 100644 index 5d1ca920a..000000000 --- a/common/uxbox/common/exceptions.cljc +++ /dev/null @@ -1,32 +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) 2016-2019 Andrey Antukh - -(ns uxbox.common.exceptions - "A helpers for work with exceptions." - (:require [clojure.spec.alpha :as s])) - -(s/def ::type keyword?) -(s/def ::code keyword?) -(s/def ::mesage string?) -(s/def ::hint string?) - -(s/def ::error - (s/keys :req-un [::type] - :opt-un [::code - ::hint - ::mesage])) - -(defn error - [& {:keys [type code message hint cause] :as params}] - (s/assert ::error params) - (let [message (or message hint "") - payload (dissoc params :cause :message)] - (ex-info message payload cause))) - -#?(:clj - (defmacro raise - [& args] - `(throw (error ~@args)))) diff --git a/common/uxbox/common/pages.cljc b/common/uxbox/common/pages.cljc index 6776f71c7..437b572d5 100644 --- a/common/uxbox/common/pages.cljc +++ b/common/uxbox/common/pages.cljc @@ -1,37 +1,36 @@ (ns uxbox.common.pages "A common (clj/cljs) functions and specs for pages." (:require - [clojure.spec.alpha :as s] - [uxbox.common.spec :as cs])) + [clojure.spec.alpha :as s])) ;; --- Specs -(s/def ::id ::cs/uuid) +(s/def ::id uuid?) (s/def ::name string?) (s/def ::type keyword?) ;; Metadata related -(s/def ::grid-x-axis ::cs/number) -(s/def ::grid-y-axis ::cs/number) +(s/def ::grid-x-axis number?) +(s/def ::grid-y-axis number?) (s/def ::grid-color string?) (s/def ::background string?) -(s/def ::background-opacity ::cs/number) +(s/def ::background-opacity number?) ;; Page related -(s/def ::file-id ::cs/uuid) -(s/def ::user ::cs/uuid) -(s/def ::created-at ::cs/inst) -(s/def ::modified-at ::cs/inst) -(s/def ::version ::cs/number) -(s/def ::ordering ::cs/number) +(s/def ::file-id uuid?) +(s/def ::user uuid?) +(s/def ::created-at inst?) +(s/def ::modified-at inst?) +(s/def ::version number?) +(s/def ::ordering number?) ;; Page Data related (s/def ::shape (s/keys :req-un [::type ::name] :opt-un [::id])) -(s/def ::shapes (s/coll-of ::cs/uuid :kind vector?)) -(s/def ::canvas (s/coll-of ::cs/uuid :kind vector?)) +(s/def ::shapes (s/coll-of uuid? :kind vector?)) +(s/def ::canvas (s/coll-of uuid? :kind vector?)) (s/def ::shapes-by-id (s/map-of uuid? ::shape)) @@ -49,11 +48,13 @@ ::background ::background-opacity])) +(s/def ::shape-change + (s/tuple #{:add :mod :del} keyword? any?)) + (s/def ::operation (s/or :mod-shape (s/cat :name #(= % :mod-shape) :id uuid? - :attr keyword? - :value any?) + :changes (s/* ::shape-change)) :add-shape (s/cat :name #(= % :add-shape) :id uuid? :data any?) @@ -68,7 +69,6 @@ (s/def ::operations (s/coll-of ::operation :kind vector?)) - ;; --- Operations Processing Impl (declare process-operation) @@ -80,42 +80,55 @@ (defn process-ops [data operations] - (->> (cs/conform ::operations operations) + (->> (s/assert ::operations operations) (reduce process-operation data))) (defn- process-operation - [data operation] - (case (first operation) - :mod-shape (process-mod-shape data operation) - :add-shape (process-add-shape data operation) - :del-shape (process-del-shape data operation) - :add-canvas (process-add-canvas data operation) - :del-canvas (process-del-canvas data operation))) + [data [op & rest]] + (case op + :mod-shape (process-mod-shape data rest) + :add-shape (process-add-shape data rest) + :del-shape (process-del-shape data rest) + :add-canvas (process-add-canvas data rest) + :del-canvas (process-del-canvas data rest))) (defn- process-mod-shape - [data {:keys [id attr value]}] - (update-in data [:shapes-by-id id] assoc attr value)) + [data [id & changes]] + (if (get-in data [:shapes-by-id id]) + (update-in data [:shapes-by-id id] + #(reduce (fn [shape [op att val]] + (if (= op :del) + (dissoc shape att) + (assoc shape att val))) + % changes)) + data)) (defn- process-add-shape - [data {:keys [id data]}] + [data [id sdata]] (-> data - (update :shapes conj id) - (update :shapes-by-id assoc id data))) + (update :shapes (fn [shapes] + (if (some #{id} shapes) + shapes + (conj shapes id)))) + (update :shapes-by-id assoc id sdata))) (defn- process-del-shape - [data {:keys [id attr value]}] + [data [id]] (-> data (update :shapes (fn [s] (filterv #(not= % id) s))) (update :shapes-by-id dissoc id))) (defn- process-add-canvas - [data {:keys [id data]}] + [data [id sdata]] (-> data - (update :canvas conj id) - (update :shapes-by-id assoc id data))) + (update :canvas (fn [shapes] + (if (some #{id} shapes) + shapes + (conj shapes id)))) + (update :shapes-by-id assoc id sdata))) (defn- process-del-canvas - [data {:keys [id attr value]}] + [data [id]] (-> data (update :canvas (fn [s] (filterv #(not= % id) s))) (update :shapes-by-id dissoc id))) diff --git a/common/uxbox/common/spec.cljc b/common/uxbox/common/spec.cljc deleted file mode 100644 index 250ffbb16..000000000 --- a/common/uxbox/common/spec.cljc +++ /dev/null @@ -1,132 +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) 2016-2019 Andrey Antukh - -(ns uxbox.common.spec - (:require - #?(:clj [datoteka.core :as fs]) - [clojure.spec.alpha :as s] - [cuerdas.core :as str] - [expound.alpha :as expound] - [uxbox.common.exceptions :as ex])) - -(s/check-asserts true) - -;; --- Constants - -(def email-rx - #"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") - -(def uuid-rx - #"^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") - -;; --- Public API - -(defn conform - [spec data] - (let [result (s/conform spec data)] - (when (= result ::s/invalid) - (ex/raise :type :validation - :code :spec-validation - :explain (with-out-str - (expound/printer data)) - :data (::s/problems data))) - result)) - -;; --- Predicates - -(defn email? - [v] - (and (string? v) - (re-matches email-rx v))) - -;; --- Conformers - -(defn- uuid-conformer - [v] - (cond - (uuid? v) v - (string? v) - (cond - (re-matches uuid-rx v) - #?(:clj (java.util.UUID/fromString v) - :cljs (uuid v)) - - (str/empty? v) - nil - - :else - ::s/invalid) - :else ::s/invalid)) - -(defn- integer-conformer - [v] - (cond - (integer? v) v - (string? v) - (if (re-matches #"^[-+]?\d+$" v) - (Long/parseLong v) - ::s/invalid) - :else ::s/invalid)) - -(defn boolean-conformer - [v] - (cond - (boolean? v) v - (string? v) - (if (re-matches #"^(?:t|true|false|f|0|1)$" v) - (contains? #{"t" "true" "1"} v) - ::s/invalid) - :else ::s/invalid)) - -(defn boolean-unformer - [v] - (if v "true" "false")) - -#?(:clj - (defn path-conformer - [v] - (cond - (string? v) (fs/path v) - (fs/path? v) v - :else ::s/invalid))) - -(defn- number-conformer - [v] - (cond - (number? v) v - - (str/numeric? v) - #?(:clj (Double/parseDouble v) - :cljs (js/parseFloat v)) - - :else ::s/invalid)) - -;; --- Default Specs - -(s/def ::string string?) -(s/def ::integer (s/conformer integer-conformer str)) -(s/def ::uuid (s/conformer uuid-conformer str)) -(s/def ::boolean (s/conformer boolean-conformer boolean-unformer)) -(s/def ::number (s/conformer number-conformer str)) - -(s/def ::inst inst?) -(s/def ::positive pos?) -(s/def ::negative neg?) -(s/def ::uploaded-file any?) -(s/def ::email email?) -(s/def ::file any?) - -;; Clojure Specific -#?(:clj - (do - (s/def ::bytes bytes?) - (s/def ::name ::string) - (s/def ::size ::integer) - (s/def ::mtype ::string) - (s/def ::path (s/conformer path-conformer str)) - (s/def ::upload - (s/keys :req-un [::name ::path ::size ::mtype])))) - diff --git a/docker/backend/Dockerfile b/docker/backend/Dockerfile index 619c8b3c6..e028cbb13 100644 --- a/docker/backend/Dockerfile +++ b/docker/backend/Dockerfile @@ -1,7 +1,7 @@ FROM azul/zulu-openjdk:12 LABEL maintainer="Andrey Antukh " -ENV CLOJURE_VERSION=1.10.1.469 \ +ENV CLOJURE_VERSION=1.10.1.492 \ LANG=en_US.UTF-8 \ LC_ALL=C.UTF-8 @@ -33,4 +33,4 @@ WORKDIR /srv/uxbox/ EXPOSE 6060 ENTRYPOINT ["bash", "/entrypoint.sh"] -CMD ["clojure", "-m", "uxbox.main"] +CMD ["/srv/uxbox/bin/start-prod"] diff --git a/docker/devenv/Dockerfile b/docker/devenv/Dockerfile index 1ab929998..95830b09c 100644 --- a/docker/devenv/Dockerfile +++ b/docker/devenv/Dockerfile @@ -5,7 +5,7 @@ ARG EXTERNAL_UID=1000 ARG DEBIAN_FRONTEND=noninteractive ENV NODE_VERSION=v10.16.3 \ - CLOJURE_VERSION=1.10.1.469 \ + CLOJURE_VERSION=1.10.1.492 \ LANG=en_US.UTF-8 \ LC_ALL=C.UTF-8 @@ -86,10 +86,10 @@ RUN set -ex; \ COPY files/bashrc /home/uxbox/.bashrc COPY files/zshrc /home/uxbox/.zshrc COPY files/vimrc /home/uxbox/.vimrc -COPY files/start.sh /home/uxbox/start.sh +COPY files/start-tmux.sh /home/uxbox/start-tmux.sh COPY files/tmux.conf /home/uxbox/.tmux.conf COPY files/entrypoint.sh /home/uxbox/ COPY files/init.sh /home/uxbox/ ENTRYPOINT ["zsh", "/home/uxbox/entrypoint.sh"] -CMD ["/home/uxbox/start.sh"] +CMD ["/home/uxbox/init.sh"] diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml index f6b434ecf..603ccb7fa 100644 --- a/docker/devenv/docker-compose.yaml +++ b/docker/devenv/docker-compose.yaml @@ -35,6 +35,7 @@ services: - 9090:9090 environment: + - CLOJURE_OPTS="-J-XX:-OmitStackTraceInFastThrow" - UXBOX_DATABASE_URI="postgresql://postgres/uxbox" - UXBOX_DATABASE_USERNAME="uxbox" - UXBOX_DATABASE_PASSWORD="uxbox" diff --git a/docker/devenv/files/entrypoint.sh b/docker/devenv/files/entrypoint.sh index e2f98cf75..1cd2e1298 100644 --- a/docker/devenv/files/entrypoint.sh +++ b/docker/devenv/files/entrypoint.sh @@ -1,3 +1,4 @@ #!/usr/bin/env zsh + set -ex exec "$@" diff --git a/docker/devenv/files/start.sh b/docker/devenv/files/start-tmux.sh similarity index 93% rename from docker/devenv/files/start.sh rename to docker/devenv/files/start-tmux.sh index 118065e1a..1bc6585f5 100755 --- a/docker/devenv/files/start.sh +++ b/docker/devenv/files/start-tmux.sh @@ -10,7 +10,7 @@ tmux send-keys -t uxbox 'clojure -Adev tools.clj figwheel' enter tmux new-window -t uxbox:2 -n 'backend' tmux select-window -t uxbox:2 tmux send-keys -t uxbox 'cd uxbox/backend' enter C-l -tmux send-keys -t uxbox './bin/start' enter +tmux send-keys -t uxbox './bin/start-dev' enter tmux rename-window -t uxbox:0 'gulp' tmux select-window -t uxbox:0 diff --git a/frontend/deps.edn b/frontend/deps.edn index 65a259917..5670a4d05 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -14,11 +14,11 @@ 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.7.0"} + funcool/potok {:mvn/version "2.8.0-SNAPSHOT"} funcool/promesa {:mvn/version "4.0.2"} funcool/rumext {:mvn/version "2.0.0-SNAPSHOT"} } - :paths ["src" "vendor" "resources"] + :paths ["src" "vendor" "resources" "../common"] :aliases {:dev {:extra-deps diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index f323b70d3..fc7093981 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -82,8 +82,8 @@ "A marker protocol for mark events that alters the page and is subject to perform a backend synchronization.") -(defprotocol IPageOps - (-ops [_] "Get a list of ops for the event.")) +(defprotocol IPagePersistentOps + (-persistent-ops [o] "Get a list of ops for the event.")) (defn page-update? [o] diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 1cc725105..14d3654db 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -10,6 +10,8 @@ [cljs.spec.alpha :as s] [potok.core :as ptk] [uxbox.config :as cfg] + [uxbox.common.data :as d] + [uxbox.common.pages :as cp] [uxbox.main.constants :as c] [uxbox.main.data.icons :as udi] [uxbox.main.data.pages :as udp] @@ -33,6 +35,8 @@ (def clear-ruler nil) (def start-ruler nil) +(declare shapes-overlaps?) + ;; --- Specs (s/def ::id ::us/uuid) @@ -196,6 +200,21 @@ :workspace-data data :workspace-page page))))) +;; --- Toggle layout flag + +(defn toggle-layout-flag + [flag] + (s/assert keyword? flag) + (ptk/reify ::toggle-layout-flag + ptk/UpdateEvent + (update [_ state] + (update state :workspace-layout + (fn [flags] + (if (contains? flags flag) + (disj flags flag) + (conj flags flag))))))) + + ;; --- Workspace Flags (defn activate-flag @@ -234,18 +253,6 @@ [txt] ::todo) -(defn toggle-layout-flag - [flag] - (s/assert keyword? flag) - (ptk/reify ::toggle-layout-flag - ptk/UpdateEvent - (update [_ state] - (update state :workspace-layout - (fn [flags] - (if (contains? flags flag) - (disj flags flag) - (conj flags flag))))))) - ;; --- Workspace Ruler (defrecord ActivateRuler [] @@ -450,17 +457,24 @@ (update-in $ [:workspace-data :shapes] conj id)) (assoc-in $ [:workspace-data :shapes-by-id id] shape)))) +(declare commit-shapes-changes) +(declare recalculate-shape-canvas-relation) + (defn add-shape [data] - (ptk/reify ::add-shape - udp/IPageDataUpdate - ptk/UpdateEvent - (update [_ state] - ;; TODO: revisit the `setup-proportions` seems unnecesary - (let [page-id (get-in state [:workspace-local :id]) - shape (assoc (geom/setup-proportions data) - :id (uuid/random))] - (impl-assoc-shape state shape))))) + (let [id (uuid/random)] + (ptk/reify ::add-shape + ptk/UpdateEvent + (update [_ state] + (let [shape (-> (geom/setup-proportions data) + (assoc :id id)) + shape (recalculate-shape-canvas-relation state shape)] + (impl-assoc-shape state shape))) + + ptk/WatchEvent + (watch [_ state stream] + (let [shape (get-in state [:workspace-data :shapes-by-id id])] + (rx/of (commit-shapes-changes [[:add-shape id shape]]))))))) ;; --- Duplicate Selected @@ -471,47 +485,14 @@ (def duplicate-selected (ptk/reify ::duplicate-selected - udp/IPageDataUpdate - ptk/UpdateEvent - (update [_ state] + ptk/WatchEvent + (watch [_ state stream] (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/IPageDataUpdate - ptk/UpdateEvent - (update [_ state] - (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/IPageDataUpdate - ptk/UpdateEvent - (update [_ state] - (reduce impl-dissoc-shape state - (map #(get-in state [:workspace-data :shapes-by-id %]) ids))))) - + (rx/merge + (rx/from-coll (map (fn [s] #(impl-assoc-shape % s)) shapes)) + (rx/of (commit-shapes-changes (mapv #(vector :add-shape (:id %) %) shapes)))))))) ;; --- Toggle shape's selection status (selected or deselected) @@ -593,12 +574,30 @@ (defn update-shape-attrs [id attrs] (s/assert ::us/uuid id) - (s/assert ::attributes attrs) (let [atts (s/conform ::attributes attrs)] (ptk/reify ::update-shape-attrs ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-data :shapes-by-id id] merge attrs))))) + (if (map? attrs) + (update-in state [:workspace-data :shapes-by-id id] merge attrs) + state))))) + +(defn update-shape + [id & attrs] + (let [attrs' (->> (apply hash-map attrs) + (s/conform ::attributes))] + (ptk/reify ::update-shape + udp/IPagePersistentOps + (-persistent-ops [_] + (->> (partition-all 2 attrs) + (mapv (fn [[key val]] [:mod-shape id key val])))) + + ptk/UpdateEvent + (update [_ state] + (cond-> state + (not= attrs' ::s/invalid) + (update-in [:workspace-data :shapes-by-id id] merge attrs')))))) + ;; --- Update Selected Shapes attrs @@ -615,8 +614,9 @@ ;; --- Move Selected -;; Event used for apply displacement transformation -;; to the selected shapes throught the keyboard shortcuts. +(declare initial-selection-align) +(declare apply-temporal-displacement-in-bulk) +(declare materialize-temporal-modifier-in-bulk) (defn- get-displacement "Retrieve the correct displacement delta point for the @@ -639,13 +639,13 @@ :fast (gpt/point (if align? (* 3 gx) 10) (if align? (* 3 gy) 10))})) -(declare initial-selection-align) -(declare materialize-current-modifier-in-bulk) -(declare apply-temporal-displacement-in-bulk) (s/def ::direction #{:up :down :right :left}) (s/def ::speed #{:std :fast}) +;; Event used for apply displacement transformation +;; to the selected shapes throught the keyboard shortcuts. + (defn move-selected [direction speed] (s/assert ::direction direction) @@ -662,7 +662,7 @@ (rx/concat (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))))))) + (rx/of (materialize-temporal-modifier-in-bulk selected))))))) ;; --- Update Shape Position @@ -679,13 +679,30 @@ ;; --- Delete Selected +(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))) + (def delete-selected "Deselect all and remove all selected shapes." (ptk/reify ::delete-selected + ptk/UpdateEvent + (update [_ state] + (let [selected (get-in state [:workspace-local :selected])] + (reduce impl-dissoc-shape state + (map #(get-in state [:workspace-data :shapes-by-id %]) selected)))) + ptk/WatchEvent (watch [_ state stream] (let [selected (get-in state [:workspace-local :selected])] - (rx/of (delete-many-shapes selected)))))) + (rx/of (commit-shapes-changes (mapv #(vector :del-shape %) selected))))))) ;; --- Rename Shape @@ -695,24 +712,30 @@ (ptk/reify ::rename-shape ptk/UpdateEvent (update [_ state] - (assoc-in state [:shapes id :name] name)))) + (assoc-in state [:shapes id :name] name)) -;; --- Change Shape Order (D&D Ordering) - -(defn change-shape-order - [{:keys [id index] :as params}] - {:pre [(uuid? id) (number? index)]} - (ptk/reify ::change-shape-order - ptk/UpdateEvent - (update [_ state] - (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 [:workspace-data :shapes] shapes))))) + ptk/WatchEvent + (watch [_ state stream] + (rx/of (commit-shapes-changes [[:mod-shape id [:mod :name name]]]))))) ;; --- Shape Vertical Ordering +(declare impl-order-shape) + +(defn order-selected-shapes + [loc] + (s/assert ::direction loc) + (ptk/reify ::move-selected-layer + udp/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))))) + (defn impl-order-shape [state sid opt] (let [shapes (get-in state [:workspace-data :shapes]) @@ -727,19 +750,19 @@ (split-at index))] (into [] (concat fst [sid] snd))))))) -(defn order-selected-shapes - [loc] - (s/assert ::direction loc) - (ptk/reify ::move-selected-layer - udp/IPageDataUpdate +;; --- Change Shape Order (D&D Ordering) + +(defn change-shape-order + [{:keys [id index] :as params}] + {:pre [(uuid? id) (number? index)]} + (ptk/reify ::change-shape-order 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))))) + (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 [:workspace-data :shapes] shapes))))) ;; --- Change Canvas Order (D&D Ordering) @@ -776,6 +799,15 @@ ;; --- Temportal displacement for Shape / Selection +(defn assoc-temporal-modifier-in-bulk + [ids xfmt] + (s/assert ::set-of-uuid ids) + (s/assert gmt/matrix? xfmt) + (ptk/reify ::assoc-temporal-modifier-in-bulk + ptk/UpdateEvent + (update [_ state] + (reduce #(assoc-in %1 [:workspace-data :shapes-by-id %2 :modifier-mtx] xfmt) state ids)))) + (defn apply-temporal-displacement-in-bulk "Apply the same displacement delta to all shapes identified by the set if ids." @@ -787,39 +819,91 @@ xfmt (gmt/translate prev delta)] (assoc-in state [:workspace-data :shapes-by-id id :modifier-mtx] xfmt)))] (ptk/reify ::apply-temporal-displacement-in-bulk - ;; udp/IPageOps - ;; (-ops [_] - ;; (mapv #(vec :udp/shape id :move delta) ids)) - ptk/UpdateEvent (update [_ state] (reduce process-shape state ids))))) -;; --- Modifiers +(defn- recalculate-shape-canvas-relation + [state shape] + (let [xfmt (comp (map #(get-in state [:workspace-data :shapes-by-id %])) + (map geom/shape->rect-shape) + (filter #(geom/overlaps? % shape)) + (map :id)) -(defn assoc-temporal-modifier-in-bulk - [ids xfmt] - (s/assert ::set-of-uuid ids) - (s/assert gmt/matrix? xfmt) - (ptk/reify ::assoc-temporal-modifier-in-bulk + id (->> (get-in state [:workspace-data :canvas]) + (into [] xfmt) + (first))] + (assoc shape :canvas id))) + +(defn materialize-temporal-modifier-in-bulk + [ids] + (letfn [(process-shape [state id] + (let [shape (get-in state [:workspace-data :shapes-by-id id]) + xfmt (or (:modifier-mtx shape) (gmt/matrix)) + shape-old (dissoc shape :modifier-mtx) + shape-new (geom/transform shape-old xfmt) + shape-new (recalculate-shape-canvas-relation state shape-new) + diff (d/diff-maps shape-old shape-new)] + (-> state + (assoc-in [:workspace-data :shapes-by-id id] shape-new) + (update ::tmp-changes (fnil conj []) (into [:mod-shape id] diff)))))] + + (ptk/reify ::materialize-temporal-modifier-in-bulk + ptk/UpdateEvent + (update [_ state] + (reduce process-shape state ids)) + + ptk/WatchEvent + (watch [_ state stream] + (let [changes (::tmp-changes state)] + (rx/of (commit-shapes-changes changes) + #(dissoc state ::tmp-changes))))))) + +(declare shapes-changes-commited) + +(defn commit-shapes-changes + [operations] + (s/assert ::cp/operations operations) + (ptk/reify ::commit-shapes-changes + ;; Commits the just performed changes to root pages-data ptk/UpdateEvent (update [_ state] - (reduce #(assoc-in %1 [:workspace-data :shapes-by-id %2 :modifier-mtx] xfmt) state ids)))) + (let [pid (get-in state [:workspace-local :page-id]) + data (get-in state [:pages-data pid])] + (update-in state [:pages-data pid] cp/process-ops operations))) -(defn materialize-current-modifier-in-bulk - [ids] - (s/assert ::us/set ids) - (letfn [(process-shape [state id] - (let [xfmt (get-in state [:workspace-data :shapes-by-id id :modifier-mtx])] - (if (gmt/matrix? xfmt) - (-> state - (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 - ptk/UpdateEvent - (update [_ state] - (reduce process-shape state ids))))) + ptk/WatchEvent + (watch [_ state stream] + (let [page (:workspace-page state) + params {:id (:id page) + :version (:version page) + :operations operations}] + (prn "commit-shapes-changes" params) + (->> (rp/mutation :update-project-page params) + ;; (rx/tap #(prn "foobar" %)) + (rx/map shapes-changes-commited)))) + + ;; ptk/EffectEvent + ;; (effect [_ state stream] + ;; (let [data {:shapes [] + ;; :shapes-by-id {}}] + ;; (prn "commit-shapes-changes$effect" (cp/process-ops data operations)))) + )) + +(s/def ::shapes-changes-commited + (s/keys :req-un [::id ::version ::cp/operations])) + +(defn shapes-changes-commited + [{:keys [id version operations] :as params}] + (s/assert ::shapes-changes-commited params) + (ptk/reify ::shapes-changes-commited + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-page :version] version) + (assoc-in [:pages id :version] version) + (update-in [:pages-data id] cp/process-ops operations) + (update :workspace-data cp/process-ops operations))))) ;; --- Start shape "edition mode" @@ -1226,3 +1310,10 @@ ;; (rx/ignore) ;; (rx/take-until stoper))))))) +(defn shapes-overlaps? + [canvas shape] + (let [shape1 (geom/shape->rect-shape canvas) + shape2 (geom/shape->rect-shape shape)] + (geom/overlaps? shape1 shape2))) + + diff --git a/frontend/src/uxbox/main/ui/shapes/common.cljs b/frontend/src/uxbox/main/ui/shapes/common.cljs index 06a493d8d..7ac4cc732 100644 --- a/frontend/src/uxbox/main/ui/shapes/common.cljs +++ b/frontend/src/uxbox/main/ui/shapes/common.cljs @@ -39,7 +39,7 @@ (->> (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-temporal-modifier-in-bulk selected) ::dw/page-data-update)))))) (defn on-mouse-down diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index 0c7dc157a..6e2819827 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -72,7 +72,7 @@ (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-temporal-modifier-in-bulk ids) ::dw/page-data-update))))))) ;; --- Controls (Component) diff --git a/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs b/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs index befba6acd..ec94ee7ef 100644 --- a/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs +++ b/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs @@ -22,20 +22,21 @@ ;; --- Shortcuts (defonce +shortcuts+ - {:shift+g #(st/emit! (dw/toggle-flag :grid)) - :ctrl+shift+m #(st/emit! (dw/toggle-flag :sitemap)) - :ctrl+shift+f #(st/emit! (dw/toggle-flag :drawtools)) - :ctrl+shift+i #(st/emit! (dw/toggle-flag :icons)) - :ctrl+shift+l #(st/emit! (dw/toggle-flag :layers)) + { + ;; :shift+g #(st/emit! (dw/toggle-flag :grid)) + :ctrl+shift+m #(st/emit! (dw/toggle-layout-flag :sitemap)) + :ctrl+shift+f #(st/emit! (dw/toggle-layout-flag :drawtools)) + :ctrl+shift+i #(st/emit! (dw/toggle-layout-flag :icons)) + :ctrl+shift+l #(st/emit! (dw/toggle-layout-flag :layers)) :ctrl+0 #(st/emit! (dw/reset-zoom)) - :ctrl+r #(st/emit! (dw/toggle-flag :ruler)) + ;; :ctrl+r #(st/emit! (dw/toggle-flag :ruler)) :ctrl+d #(st/emit! dw/duplicate-selected) - :ctrl+c #(st/emit! (dw/copy-to-clipboard)) - :ctrl+v #(st/emit! (dw/paste-from-clipboard)) - :ctrl+shift+v #(dl/open! :clipboard) - :ctrl+z #(st/emit! du/undo) - :ctrl+shift+z #(st/emit! du/redo) - :ctrl+y #(st/emit! du/redo) + ;; :ctrl+c #(st/emit! (dw/copy-to-clipboard)) + ;; :ctrl+v #(st/emit! (dw/paste-from-clipboard)) + ;; :ctrl+shift+v #(dl/open! :clipboard) + ;; :ctrl+z #(st/emit! du/undo) + ;; :ctrl+shift+z #(st/emit! du/redo) + ;; :ctrl+y #(st/emit! du/redo) :ctrl+b #(st/emit! (dw/select-for-drawing :rect)) :ctrl+e #(st/emit! (dw/select-for-drawing :circle)) :ctrl+t #(st/emit! (dw/select-for-drawing :text)) diff --git a/frontend/src/uxbox/main/ui/workspace/streams.cljs b/frontend/src/uxbox/main/ui/workspace/streams.cljs index 0f016a160..687a7484f 100644 --- a/frontend/src/uxbox/main/ui/workspace/streams.cljs +++ b/frontend/src/uxbox/main/ui/workspace/streams.cljs @@ -94,7 +94,6 @@ (rx/subscribe-with ob sub) sub)) - (defn mouse-position-deltas [current] (->> (rx/concat (rx/of current) diff --git a/frontend/tools.clj b/frontend/tools.clj index 2196a286c..2a542bd10 100644 --- a/frontend/tools.clj +++ b/frontend/tools.clj @@ -197,9 +197,10 @@ :pprint-config false :load-warninged-code false :auto-testing false + :reload-clj-files true :css-dirs ["resources/public/css"] :ring-server-options {:port 3449 :host "0.0.0.0"} - :watch-dirs ["src" "test"]}) + :watch-dirs ["src" "test" "../common"]}) (defmethod task "figwheel" [& args] diff --git a/manage.sh b/manage.sh index 73fbf5574..903264f80 100755 --- a/manage.sh +++ b/manage.sh @@ -37,7 +37,7 @@ function run-devenv { start-devenv fi - docker exec -ti uxboxdev-main /home/uxbox/start.sh; + docker exec -ti uxboxdev-main /home/uxbox/start-tmux.sh } function run-all-tests {