0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-06 12:01:19 -05:00

Merge branch 'develop' into refactor-ui-integration

This commit is contained in:
Andrey Antukh 2019-12-18 10:57:24 +01:00
commit 9c1c613c90
32 changed files with 551 additions and 402 deletions

View file

@ -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

5
backend/bin/start-prod Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
set -e
clojure ${CLOJURE_OPTIONS} -m uxbox.main

View file

@ -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"]}
}}

View file

@ -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}]

View file

@ -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

View file

@ -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})

View file

@ -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 <niwi@niwi.nz>
(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))

View file

@ -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]))

View file

@ -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

View file

@ -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?)

View file

@ -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))

View file

@ -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")

View file

@ -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]

View file

@ -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)

View file

@ -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 <niwi@niwi.nz>
(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)))))

View file

@ -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 <niwi@niwi.nz>
(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))))

View file

@ -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)))

View file

@ -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 <niwi@niwi.nz>
(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]))))

View file

@ -1,7 +1,7 @@
FROM azul/zulu-openjdk:12
LABEL maintainer="Andrey Antukh <niwi@niwi.nz>"
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"]

View file

@ -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"]

View file

@ -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"

View file

@ -1,3 +1,4 @@
#!/usr/bin/env zsh
set -ex
exec "$@"

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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)))

View file

@ -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

View file

@ -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)

View file

@ -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))

View file

@ -94,7 +94,6 @@
(rx/subscribe-with ob sub)
sub))
(defn mouse-position-deltas
[current]
(->> (rx/concat (rx/of current)

View file

@ -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]

View file

@ -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 {