From f45d687d2a5c5253b409728d045f11fbf41b93d0 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Aug 2019 16:51:56 +0000 Subject: [PATCH 01/46] :books: Update documentation. --- CONTRIBUTING.md | 100 +++++++++++------------------------------------- README.md | 63 +++++++++++++++++++++++------- 2 files changed, 72 insertions(+), 91 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 82e47b7cdb..0fec8a3156 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,92 +44,38 @@ We will use the `easy fix` mark for tag for indicate issues that are easy for begginers. -## Development environment ## +## Commit Message Guidelines ## -### Introduction ### +We have very precise rules over how our git commit messages can be formatted. -The development environment consists in a docker container that mounts your local -copy of the uxbox souce code directory tree and executes a tmux inside the container -in order to facilitate execute multiple processes inside. +The commit message format is: +``` +(): -### System requirements ### +[body] -You should have `docker` installed in your system in order to set up properly -the uxbox development enviroment. - -In debian like linux distributions you can install it executing: - -```bash -sudo apt-get install docker +[footer] ``` +Where type is: -### Start the docker container ### - -**Requires a minimum knowledge of tmux usage in order to use that development -environment.** - -For start it, staying in this repository, execute: - -```bash -./manage.sh run -``` - -This will do the following: - -- Build the image if it is not done before. -- Download all repositories if them are not downloaded previously. -- Start a container with predefined tmux layout. -- Start all needed processes such as gulp and figwheel. - - -### First steps with tmux ### - -Now having the the container running and tmux open inside the container, you are -free to execute any commands and open many shells as you want. - -You can create a new shell just pressing the **Ctr+b c** shortcut. And **Ctrl+b w** -for switch between windows, **Ctrl+b &** for kill the current window. - -### Inside the tmux session ### - -#### UI #### - -The UI related tasks starts automatically so you do not need do anything. The -**window 0** and **window 1** are used for the UI related environment. - - -#### Backend #### - -The backend related environment is located in the **window 2**, and you can go -directly to it using `ctrl+b 2` shortcut. - -By default this tasks are performed: - -- Start postgresql. -- Load initial fixtures into the database. - -The backend is not started automatically, and frontend code by default does not -requires that (because it uses a remote server on default config). - -You can start it just execting the `run.sh` script: - -```bash -./scripts/run.sh -``` - -You also can start an repl and strart the backend inside of them: - -```bash -lein repl -``` - -And use `(start)` to start all the environment, `(stop)` for stoping it and -`(reset)` for restart with code reloading. If some exception is raised when -code is reloaded, just use `(refresh)` in order to finish correctly the -code swaping and later use `(reset)` again. +- `:bug:` a commit that fixes a bug +- `:sparkles:` a commit that adds feature +- `:recycle:` a commit that introduces a refactor +- `:lipstick:` a commit with cosmetic changes +- `:ambulance:` a commit that fixes critical bug +- `:books:` a commit that improves or adds documentation +- `:construction:`: a wip commit +- `:construction_worker:` a commit with CI related stuff +- `:boom:` a commit with breaking changes +- `:wrench:` a commit for config updates +- `:zap:` a commit with performance improvements +- `:whale:` a commit for docker related stuff +- `:rewind:` a commit that reverts changes +- `:paperclip:` a commit with other not relevant changes +More info: https://gist.github.com/parmentf/035de27d6ed1dce0b36a ## Code of conduct ## diff --git a/README.md b/README.md index 0756e6e208..19ae1873bd 100644 --- a/README.md +++ b/README.md @@ -10,36 +10,56 @@ ![UXBOX](https://piweek.com/images/projects/uxbox.jpg) + ## Introduction ## -The open-source solution for design and prototyping. UXBOX is currently at an early development stage but we are working hard to bring you the beta version as soon as possible. Follow the project progress in Twitter or Github and stay tuned! +The open-source solution for design and prototyping. UXBOX is +currently at an early development stage but we are working hard to +bring you the beta version as soon as possible. Follow the project +progress in Twitter or Github and stay tuned! [See SVG specification](https://www.w3.org/Graphics/SVG/) ## SVG based ## -UXBOX works with SVG, a standard format, for all your designs and prototypes . This means that all your stuff in UXBOX is portable and editable in many other vector tools and easy to use on the web. +UXBOX works with SVG, a standard format, for all your designs and +prototypes . This means that all your stuff in UXBOX is portable and +editable in many other vector tools and easy to use on the web. ## Development ## -Most of the main operations can be done through the helper script `manage.sh`. +Most of the main operations can be done through the helper script +`manage.sh`. + +The development requires of UXBOX is done through a single docker +container. Each main service is opened in a different +[tmux](https://github.com/tmux/tmux) sessions. -The development requires of UXBOX is done through a single docker container. Each main service is opened in a different [tmux](https://github.com/tmux/tmux) sessions. ## Docker -Docker is also used to build release images for backend and frontend. Use the helper script `manage.sh` to build the images. -You can run locally UXBOX through a docker-compose or by manually running the containers. +Docker is also used to build release images for backend and +frontend. Use the helper script `manage.sh` to build the images. You +can run locally UXBOX through a docker-compose or by manually running +the containers. -Complementary to the docker images you can build locally from this repository, you can find additionnal flavors for backend and frontend on external repositories: +Complementary to the docker images you can build locally from this +repository, you can find additionnal flavors for backend and frontend +on external repositories: * [Monogramm/docker-uxbox-frontend](https://github.com/Monogramm/docker-uxbox-frontend) * [Monogramm/docker-uxbox-backend](https://github.com/Monogramm/docker-uxbox-backend) ### Persistent data -The UXBOX installation and all data are stored in the database (file uploads, etc). The docker daemon will store that data within the docker directory `/var/lib/docker/volumes/...`. That means your data is saved even if the container crashes, is stopped or deleted. -To make your data persistent to upgrading and get access for backups is using named docker volume or mount a host folder. To achieve this you need one volume for your database container. +The UXBOX installation and all data are stored in the database (file +uploads, etc). The docker daemon will store that data within the +docker directory `/var/lib/docker/volumes/...`. That means your data +is saved even if the container crashes, is stopped or deleted. + +To make your data persistent to upgrading and get access for backups +is using named docker volume or mount a host folder. To achieve this +you need one volume for your database container. Database: - `/var/lib/postgresql/data` PostgreSQL Data @@ -49,8 +69,12 @@ $ docker run -d \ postgresql ``` -You also need to persist the UXBOX backend public resources (media and assets) to not lose images uploaded and allow the frontend to expose assets. +You also need to persist the UXBOX backend public resources (media and +assets) to not lose images uploaded and allow the frontend to expose +assets. + - `/srv/uxbox/resources/public` UXBOX backend public resources + ```console $ docker run -d \ -v db:/srv/uxbox/resources/public \ @@ -59,7 +83,8 @@ $ docker run -d \ ### Auto configuration via environment variables -The following environment variables are also honored for configuring your UXBOX instance: +The following environment variables are also honored for configuring +your UXBOX instance: #### Frontend @@ -103,13 +128,17 @@ Available at runtime: - `-e UXBOX_REGISTRATION_ENABLED=...` (defaults to `true`) - `-e UXBOX_SECRET="..."` (defaults to `"5qjiAndGY3"`) -**Important note:** make sure to use quotation marks for string variables or the backend might try to interpret the values as symbols and have weird issues. +**Important note:** make sure to use quotation marks for string +variables or the backend might try to interpret the values as symbols +and have weird issues. ## Collections import -You can easily import icons and images as global stores with the backend collection importer: +You can easily import icons and images as global stores with the +backend collection importer: * Create a `media` folder with the following sample structure: + ``` media icons @@ -117,8 +146,10 @@ media images my-images-collection ``` + * Add some icons (SVG format) and images to your collection * Create a `config.edn` file with the following content + ```clojure {:icons [{:name "Generic Icons 1" @@ -130,7 +161,9 @@ media :path "./images/my-images-collection/" :regex #"^.*\.(png|jpg|webp)$"}]} ``` + * Then go to the backend directory and import collections: + ```sh clojure -Adev -m uxbox.cli.collimp ../media/config.edn ``` @@ -141,7 +174,9 @@ Take a look at the `sample_media` directory for a sample configuration. **Open to you!** -We love the open source software community. Contributing is our passion and because of this, we'll be glad if you want to participate and improve UXBOX. All your awesome ideas and code are welcome! +We love the open source software community. Contributing is our +passion and because of this, we'll be glad if you want to participate +and improve UXBOX. All your awesome ideas and code are welcome! Please refer to the [Contributing Guide](./CONTRIBUTING.md) From 882102245ad7051713babbee0982102a2169299c Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Aug 2019 20:58:23 +0000 Subject: [PATCH 02/46] :sparkles: Add multicanvas data to sample data. --- backend/src/uxbox/fixtures.clj | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/src/uxbox/fixtures.clj b/backend/src/uxbox/fixtures.clj index b23ea3b994..b8d5eaf472 100644 --- a/backend/src/uxbox/fixtures.clj +++ b/backend/src/uxbox/fixtures.clj @@ -22,8 +22,8 @@ [uxbox.util.transit :as t])) (defn- mk-uuid - [prefix i] - (uuid/v5 uuid/+namespace-oid+ (str prefix i))) + [prefix & args] + (uuid/v5 uuid/+namespace-oid+ (apply str prefix args))) (defn- data-encode [data] @@ -58,7 +58,19 @@ {:id (mk-uuid "page" i) :user (mk-uuid "user" ui) :project (mk-uuid "project" pi) - :data nil + :data {:canvas [{:id (mk-uuid "canvas" i 1) + :x 200 + :y 200 + :width 1024 + :height 768} + {:id (mk-uuid "canvas" i 2) + :x 1324 + :y 200 + :width 1024 + :height 768} + ] + :shapes [] + :shapes-map {}} :metadata {:width 1024 :height 768 :layout "tablet"} From 5b2705e15887c434ab6dc48c8f95df9a70384bfd Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Aug 2019 20:58:53 +0000 Subject: [PATCH 03/46] :arrow_up: Update frontend depedencies. --- frontend/deps.edn | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/deps.edn b/frontend/deps.edn index dea2e51eed..8a79a52f6c 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -2,16 +2,16 @@ org.clojure/clojure {:mvn/version "1.10.1"} com.cognitect/transit-cljs {:mvn/version "0.8.256"} - cljsjs/react-dom-server {:mvn/version "16.8.6-0"} + cljsjs/react-dom-server {:mvn/version "16.9.0-0"} environ/environ {:mvn/version "1.1.0"} metosin/reitit-core {:mvn/version "0.3.9"} - funcool/beicon {:mvn/version "5.1.0-SNAPSHOT"} + funcool/beicon {:mvn/version "5.1.0"} funcool/cuerdas {:mvn/version "2.2.0"} funcool/lentes {:mvn/version "1.3.0-SNAPSHOT"} - funcool/potok {:mvn/version "2.3.0"} - funcool/promesa {:mvn/version "2.0.1"} + funcool/potok {:mvn/version "2.4.0"} + funcool/promesa {:mvn/version "3.0.0-SNAPSHOT"} funcool/rumext {:mvn/version "2.0.0-SNAPSHOT"} } :paths ["src" "vendor" "resources"] From 42337dcd55556951ecab884906b0feff0f05f4d3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Aug 2019 20:59:55 +0000 Subject: [PATCH 04/46] :books: Update contributing guide. --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0fec8a3156..87f44857c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -74,6 +74,7 @@ Where type is: - `:whale:` a commit for docker related stuff - `:rewind:` a commit that reverts changes - `:paperclip:` a commit with other not relevant changes +- `:arrow_up:` a commit with dependencies updates More info: https://gist.github.com/parmentf/035de27d6ed1dce0b36a From ccc6eaf4b04ea2308bce4342a1df3eb4eec4caeb Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Aug 2019 21:00:22 +0000 Subject: [PATCH 05/46] :lipstick: Cosmetic changes on builtin colors ns. --- frontend/src/uxbox/builtins/colors.cljs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/uxbox/builtins/colors.cljs b/frontend/src/uxbox/builtins/colors.cljs index fa4e9ab3eb..b802052075 100644 --- a/frontend/src/uxbox/builtins/colors.cljs +++ b/frontend/src/uxbox/builtins/colors.cljs @@ -6,7 +6,7 @@ (ns uxbox.builtins.colors (:require [uxbox.util.uuid :as uuid] - [uxbox.util.data :refer (index-by)])) + [uxbox.util.data :refer [index-by-id]])) (def collections-list [{:name "UXBOX" @@ -672,4 +672,4 @@ "#3D464D"}}]) (def collections - (index-by collections-list :id)) + (index-by-id collections-list)) From cf7664d446a01fee1f04d6d7e6d1a1bccacbeb29 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Aug 2019 21:03:29 +0000 Subject: [PATCH 06/46] :recycle: Refactor main app component. --- frontend/src/uxbox/main.cljs | 2 +- frontend/src/uxbox/main/ui.cljs | 64 ++++++++++++++++----------------- 2 files changed, 31 insertions(+), 35 deletions(-) diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs index b9bdb3ba06..d43526c91a 100644 --- a/frontend/src/uxbox/main.cljs +++ b/frontend/src/uxbox/main.cljs @@ -69,7 +69,7 @@ (when (:auth storage) (st/emit! (udu/fetch-profile))) - (mf/mount (ui/app) (dom/get-element "app")) + (mf/mount (mf/element ui/app) (dom/get-element "app")) (mf/mount (lightbox) (dom/get-element "lightbox")) (mf/mount (mf/element modal) (dom/get-element "modal")) (mf/mount (loader) (dom/get-element "loader")) diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs index 9e288c09d1..a5e2b949af 100644 --- a/frontend/src/uxbox/main/ui.cljs +++ b/frontend/src/uxbox/main/ui.cljs @@ -11,7 +11,6 @@ [cuerdas.core :as str] [lentes.core :as l] [potok.core :as ptk] - [rumext.core :as mx] [rumext.alpha :as mf] [uxbox.builtins.icons :as i] [uxbox.main.data.auth :refer [logout]] @@ -81,43 +80,40 @@ ;; --- Main App (Component) -(mf/def app - :mixins [mx/reactive] +(def route-iref + (-> (l/key :route) + (l/derive st/state))) - :init - (fn [own props] - (assoc own ::route-ref (l/derive (l/key :route) st/state))) +(mf/defc app + [props] + (let [route (mf/deref route-iref) + route-id (get-in route [:data :name])] + (case route-id + :auth/login (mf/element auth/login-page) + :auth/register (auth/register-page) + :auth/recovery-request (auth/recovery-request-page) - :render - (fn [own props] - (let [route (mx/react (::route-ref own)) - route-id (get-in route [:data :name])] - (case route-id - :auth/login (mf/element auth/login-page) - :auth/register (auth/register-page) - :auth/recovery-request (auth/recovery-request-page) + :auth/recovery + (let [token (get-in route [:params :path :token])] + (auth/recovery-page token)) - :auth/recovery - (let [token (get-in route [:params :path :token])] - (auth/recovery-page token)) + (:settings/profile + :settings/password + :settings/notifications) + (mf/element settings/settings #js {:route route}) - (:settings/profile - :settings/password - :settings/notifications) - (mf/element settings/settings #js {:route route}) + (:dashboard/projects + :dashboard/icons + :dashboard/images + :dashboard/colors) + (mf/element dashboard/dashboard #js {:route route}) - (:dashboard/projects - :dashboard/icons - :dashboard/images - :dashboard/colors) - (mf/element dashboard/dashboard #js {:route route}) + :workspace/page + (let [project-id (uuid (get-in route [:params :path :project])) + page-id (uuid (get-in route [:params :path :page]))] + [:& workspace-page {:project-id project-id + :page-id page-id + :key page-id}]) - :workspace/page - (let [project-id (uuid (get-in route [:params :path :project])) - page-id (uuid (get-in route [:params :path :page]))] - [:& workspace-page {:project-id project-id - :page-id page-id - :key page-id}]) + nil))) - nil - )))) From 41a3f4483f3bd9063afd8c88a82201fdc20b494d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Aug 2019 21:04:41 +0000 Subject: [PATCH 07/46] :sparkles: Improve error reporting. --- frontend/src/uxbox/main/ui.cljs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs index a5e2b949af..b24be3d90a 100644 --- a/frontend/src/uxbox/main/ui.cljs +++ b/frontend/src/uxbox/main/ui.cljs @@ -54,14 +54,17 @@ (defn- on-error "A default error handler." [{:keys [status] :as error}] - (js/console.error "on-error:" (pr-str error)) - (js/console.error (.-stack error)) + (js/console.error "Unhandled Error:" + "\n - message:" (ex-message error) + "\n - data:" (pr-str (ex-data error)) + "\n - stack:" (.-stack error)) (reset! st/loader false) (cond ;; Unauthorized or Auth timeout (and (:status error) (or (= (:status error) 403) (= (:status error) 419))) + (ts/schedule 0 #(st/emit! (rt/nav :auth/login))) ;; Conflict From 247be4a8a49905077ca5af41920eec8d909b6b52 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Aug 2019 21:07:02 +0000 Subject: [PATCH 08/46] :books: Improve contributing guide. --- CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87f44857c5..d70f3101fd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,7 +61,8 @@ The commit message format is: Where type is: - `:bug:` a commit that fixes a bug -- `:sparkles:` a commit that adds feature +- `:sparkles:` a commit that an improvement +- `:tada:` a commit with new feature - `:recycle:` a commit that introduces a refactor - `:lipstick:` a commit with cosmetic changes - `:ambulance:` a commit that fixes critical bug From 176ca590e1520bedb4cc5677f3b45b08e37239b9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Aug 2019 21:09:22 +0000 Subject: [PATCH 09/46] :sparkles: Adapt code to rumext changes. --- frontend/src/uxbox/main/ui/auth/login.cljs | 3 +-- frontend/src/uxbox/main/ui/settings/profile.cljs | 5 ++--- frontend/src/uxbox/main/ui/users.cljs | 3 +-- frontend/src/uxbox/main/ui/workspace.cljs | 4 ++-- frontend/src/uxbox/main/ui/workspace/header.cljs | 3 +-- .../src/uxbox/main/ui/workspace/sidebar/icons.cljs | 13 ++++++------- frontend/src/uxbox/view/ui.cljs | 3 +-- frontend/src/uxbox/view/ui/viewer.cljs | 3 +-- 8 files changed, 15 insertions(+), 22 deletions(-) diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs index 3f4b67fc93..9c1c21a63e 100644 --- a/frontend/src/uxbox/main/ui/auth/login.cljs +++ b/frontend/src/uxbox/main/ui/auth/login.cljs @@ -57,9 +57,8 @@ " the projects will be periodicaly wiped."]]) (mf/defc login-form - {:wrap [mf/wrap-reactive]} [] - (let [data (mf/react form-data) + (let [data (mf/deref form-data) valid? (fm/valid? ::login-form data)] [:form {:on-submit #(on-submit % data)} [:div.login-content diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index 8c62a8e84b..47e7e0b045 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -67,7 +67,7 @@ ;; --- Profile Form (mf/def profile-form - :mixins [mf/memo mf/reactive mf/sync-render (fm/clear-mixin st/store :profile)] + :mixins [mf/memo mf/reactive (fm/clear-mixin st/store :profile)] :render (fn [own props] (let [data (merge {:language @i18n/locale} @@ -113,7 +113,6 @@ ;; --- Profile Photo Form (mf/defc profile-photo-form - {:wrap [mf/wrap-reactive]} [] (letfn [(on-change [event] (let [target (dom/get-target event) @@ -122,7 +121,7 @@ (first))] (st/emit! (udu/update-photo file)) (dom/clean-value! target)))] - (let [{:keys [photo]} (mf/react profile-ref) + (let [{:keys [photo]} (mf/deref profile-ref) photo (if (or (str/empty? photo) (nil? photo)) "images/avatar.jpg" photo)] diff --git a/frontend/src/uxbox/main/ui/users.cljs b/frontend/src/uxbox/main/ui/users.cljs index 0968dbf03b..cd0cf39122 100644 --- a/frontend/src/uxbox/main/ui/users.cljs +++ b/frontend/src/uxbox/main/ui/users.cljs @@ -49,10 +49,9 @@ (l/derive st/state))) (mf/defc user - {:wrap [mf/wrap-reactive]} [_] (let [open (mf/use-state false) - profile (mf/react profile-ref) + profile (mf/deref profile-ref) photo (if (str/empty? (:photo profile "")) "/images/avatar.jpg" (:photo profile))] diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index d1e2d08d10..18a0ee1506 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -83,8 +83,8 @@ :no-tool-bar-left (not left-sidebar?) :scrolling (:viewport-positionig workspace))] - (mf/use-effect {:deps #js [canvas page] - :fn #(subscribe canvas page)}) + (mf/use-effect #(subscribe canvas page) + #js [(:id page)]) [:* (messages-widget) [:& header {:page page diff --git a/frontend/src/uxbox/main/ui/workspace/header.cljs b/frontend/src/uxbox/main/ui/workspace/header.cljs index 4d9d44278b..216e56cd4a 100644 --- a/frontend/src/uxbox/main/ui/workspace/header.cljs +++ b/frontend/src/uxbox/main/ui/workspace/header.cljs @@ -29,9 +29,8 @@ ;; --- Zoom Widget (mf/defc zoom-widget - {:wrap [mf/wrap-reactive]} [props] - (let [zoom (mf/react refs/selected-zoom) + (let [zoom (mf/deref refs/selected-zoom) increase #(st/emit! (dw/increase-zoom)) decrease #(st/emit! (dw/decrease-zoom))] [:ul.options-view diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs index 5a274c0244..a23a434a9d 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs @@ -12,7 +12,6 @@ [uxbox.builtins.icons :as i] [uxbox.main.data.icons :as udi] [uxbox.main.data.workspace :as dw] - [uxbox.main.lenses :as ul] [uxbox.main.store :as st] [uxbox.main.ui.dashboard.icons :as icons] [uxbox.main.ui.shapes.icon :as icon] @@ -22,12 +21,12 @@ ;; --- Refs -(def ^:private drawing-shape-ref - "A focused vision of the drawing property - of the workspace status. This avoids - rerender the whole toolbox on each workspace - change." - (l/derive ul/selected-drawing st/state)) +;; (def ^:private drawing-shape-ref +;; "A focused vision of the drawing property +;; of the workspace status. This avoids +;; rerender the whole toolbox on each workspace +;; change." +;; (l/derive ul/selected-drawing st/state)) (def ^:private icons-toolbox-ref (-> (l/in [:workspace :icons-toolbox]) diff --git a/frontend/src/uxbox/view/ui.cljs b/frontend/src/uxbox/view/ui.cljs index 580719f391..261903f5c8 100644 --- a/frontend/src/uxbox/view/ui.cljs +++ b/frontend/src/uxbox/view/ui.cljs @@ -56,9 +56,8 @@ (l/derive st/state))) (mf/defc app - {:wrap [mf/wrap-reactive]} [] - (let [route (mf/react route-ref)] + (let [route (mf/deref route-ref)] (case (get-in route [:data :name]) :view/notfound (notfound-page) :view/viewer (let [{:keys [token id]} (get-in route [:params :path])] diff --git a/frontend/src/uxbox/view/ui/viewer.cljs b/frontend/src/uxbox/view/ui/viewer.cljs index 76704a6456..57e3950953 100644 --- a/frontend/src/uxbox/view/ui/viewer.cljs +++ b/frontend/src/uxbox/view/ui/viewer.cljs @@ -34,9 +34,8 @@ ;; --- Component (mf/defc viewer-page - {:wrap [mf/wrap-reactive]} [{:keys [token id]}] - (let [{:keys [project pages flags]} (mf/react state-ref)] + (let [{:keys [project pages flags]} (mf/deref state-ref)] (mf/use-effect {:fn #(st/emit! (dv/initialize token))}) (when (seq pages) From 807555d4786b9ff44c22fd0b658929c76acb1077 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 21 Aug 2019 21:09:54 +0000 Subject: [PATCH 10/46] :construction: Initial work on multicanvas feature. --- frontend/src/uxbox/main/data/history.cljs | 8 +- frontend/src/uxbox/main/data/pages.cljs | 78 +- frontend/src/uxbox/main/data/shapes.cljs | 874 +++++++++--------- frontend/src/uxbox/main/data/shapes_impl.cljs | 470 ---------- frontend/src/uxbox/main/data/workspace.cljs | 414 ++++++++- .../src/uxbox/main/data/workspace/ruler.cljs | 128 +-- .../uxbox/main/data/workspace_drawing.cljs | 24 +- frontend/src/uxbox/main/geom.cljs | 6 +- frontend/src/uxbox/main/lenses.cljs | 13 - frontend/src/uxbox/main/refs.cljs | 26 +- frontend/src/uxbox/main/repo/pages.cljs | 9 +- frontend/src/uxbox/main/streams.cljs | 65 -- frontend/src/uxbox/main/ui/shapes.cljs | 1 + frontend/src/uxbox/main/ui/shapes/common.cljs | 5 +- frontend/src/uxbox/main/ui/shapes/text.cljs | 2 +- frontend/src/uxbox/main/ui/workspace.cljs | 4 +- .../src/uxbox/main/ui/workspace/canvas.cljs | 65 +- .../src/uxbox/main/ui/workspace/drawarea.cljs | 123 ++- .../uxbox/main/ui/workspace/selection.cljs | 25 +- .../uxbox/main/ui/workspace/shortcuts.cljs | 23 +- .../src/uxbox/main/ui/workspace/sidebar.cljs | 26 +- .../main/ui/workspace/sidebar/drawtools.cljs | 111 +-- .../main/ui/workspace/sidebar/layers.cljs | 76 +- .../sidebar/options/circle_measures.cljs | 10 +- .../sidebar/options/icon_measures.cljs | 13 +- .../sidebar/options/image_measures.cljs | 15 +- .../sidebar/options/interactions.cljs | 6 +- .../sidebar/options/rect_measures.cljs | 14 +- .../ui/workspace/sidebar/options/text.cljs | 2 +- .../src/uxbox/main/ui/workspace/streams.cljs | 41 +- .../src/uxbox/main/ui/workspace/viewport.cljs | 222 ++++- frontend/src/uxbox/util/data.cljs | 4 +- 32 files changed, 1408 insertions(+), 1495 deletions(-) delete mode 100644 frontend/src/uxbox/main/data/shapes_impl.cljs delete mode 100644 frontend/src/uxbox/main/lenses.cljs delete mode 100644 frontend/src/uxbox/main/streams.cljs diff --git a/frontend/src/uxbox/main/data/history.cljs b/frontend/src/uxbox/main/data/history.cljs index ada0cdde6d..866440d7ca 100644 --- a/frontend/src/uxbox/main/data/history.cljs +++ b/frontend/src/uxbox/main/data/history.cljs @@ -12,6 +12,8 @@ [uxbox.util.data :refer [replace-by-id index-by]])) +;; TODO: this need refactor (completely broken) + ;; --- Initialize History State (declare fetch-history) @@ -52,7 +54,7 @@ (deftype PinnedPageHistoryFetched [items] ptk/UpdateEvent (update [_ state] - (let [items-map (index-by items :version) + (let [items-map (index-by :version items) items-set (into #{} items)] (update-in state [:workspace :history] (fn [history] @@ -164,7 +166,7 @@ (assoc :history true :data (:data item)))] (-> state - (udp/assoc-page page) + (udp/unpack-page page) (assoc-in [:workspace :history :selected] version))))) (defn select-page-history @@ -203,7 +205,7 @@ (set! noop true) state) (let [packed (get-in state [:packed-pages page-id])] - (-> (udp/assoc-page state packed) + (-> (udp/unpack-page state packed) (assoc-in [:workspace :history :deselecting] true) (assoc-in [:workspace :history :selected] nil)))))) diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index 26e123fc0b..d01bac6fd4 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -5,18 +5,17 @@ ;; Copyright (c) 2015-2017 Andrey Antukh (ns uxbox.main.data.pages - (:require [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [beicon.core :as rx] - [lentes.core :as l] - [potok.core :as ptk] - [uxbox.main.store :as st] - [uxbox.main.repo :as rp] - [uxbox.main.lenses :as ul] - [uxbox.util.spec :as us] - [uxbox.util.router :as r] - [uxbox.util.timers :as ts] - [uxbox.util.time :as dt])) + (:require + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [lentes.core :as l] + [potok.core :as ptk] + [uxbox.main.repo :as rp] + [uxbox.main.store :as st] + [uxbox.util.spec :as us] + [uxbox.util.timers :as ts] + [uxbox.util.data :refer [index-by-id]])) ;; --- Specs @@ -77,50 +76,43 @@ ;; --- Helpers -;; TODO: make sure remove all :tmp-* related attrs from shape - -(defn pack-page-shapes - "Create a hash-map of shapes indexed by their id that belongs - to the provided page." - [state page] - (let [lookup-shape-xf (map #(get-in state [:shapes %]))] - (reduce (fn reducer [acc {:keys [id type items] :as shape}] - (let [shape (assoc shape :page (:id page))] - (cond - (= type :group) - (reduce reducer - (assoc acc id shape) - (sequence lookup-shape-xf items)) - - (uuid? id) - (assoc acc id shape) - - :else acc))) - {} - (sequence lookup-shape-xf (:shapes page))))) - (defn pack-page "Return a packed version of page object ready for send to remore storage service." [state id] - (let [page (get-in state [:pages id]) - shapes (pack-page-shapes state page)] - (-> page - (assoc-in [:data :shapes] (vec (:shapes page))) - (assoc-in [:data :shapes-map] shapes) - (dissoc :shapes)))) + (letfn [(get-shape [id] + (get-in state [:shapes id])) + (pack-shapes [ids] + (reduce #(assoc %1 %2 (get-shape %2)) {} ids)) + (pack-canvas [ids] + (mapv #(get-in state [:canvas %]) ids))] + (let [page (get-in state [:pages id]) + data {:canvas (pack-canvas (:canvas page)) + :shapes (vec (:shapes page)) + :shapes-map (pack-shapes (:shapes page))}] + (-> page + (assoc :data data) + (dissoc :shapes :canvas))))) -(defn assoc-page +(defn unpack-page "Unpacks packed page object and assocs it to the provided state." [state {:keys [id data] :as page}] (let [shapes (:shapes data) shapes-map (:shapes-map data) + + canvas-data (:canvas data []) + + canvas (mapv :id canvas-data) + canvas-map (index-by-id canvas-data) + page (-> page (dissoc :data) + (assoc :canvas canvas) (assoc :shapes shapes))] (-> state (update :shapes merge shapes-map) + (update :canvas merge canvas-map) (update :pages assoc id page)))) (defn purge-page @@ -160,7 +152,7 @@ (assoc-in $ [:projects id :pages] page-ids) ;; TODO: this is a workaround (assoc-in $ [:projects id :page-id] (first page-ids)) - (reduce assoc-page $ pages) + (reduce unpack-page $ pages) (reduce assoc-packed-page $ pages))))) (defn pages-fetched @@ -194,7 +186,7 @@ ptk/UpdateEvent (update [_ state] (-> state - (assoc-page data) + (unpack-page data) (assoc-packed-page data))) ptk/WatchEvent diff --git a/frontend/src/uxbox/main/data/shapes.cljs b/frontend/src/uxbox/main/data/shapes.cljs index 9d78f2aa91..3c20c9ca56 100644 --- a/frontend/src/uxbox/main/data/shapes.cljs +++ b/frontend/src/uxbox/main/data/shapes.cljs @@ -2,527 +2,537 @@ ;; 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-2017 Andrey Antukh +;; Copyright (c) 2015-2019 Andrey Antukh (ns uxbox.main.data.shapes (:require [cljs.spec.alpha :as s] - [lentes.core :as l] - [beicon.core :as rx] - [potok.core :as ptk] - [uxbox.main.store :as st] - [uxbox.main.constants :as c] - [uxbox.main.refs :as refs] - [uxbox.main.lenses :as ul] [uxbox.main.geom :as geom] - [uxbox.main.workers :as uwrk] - [uxbox.main.data.pages :as udp] - [uxbox.main.data.shapes-impl :as impl] - [uxbox.main.user-events :as uev] - [uxbox.util.data :refer [dissoc-in]] - [uxbox.util.forms :as sc] - [uxbox.util.spec :as us] - [uxbox.util.geom.point :as gpt] [uxbox.util.geom.matrix :as gmt] - [uxbox.util.router :as r] - [uxbox.util.uuid :as uuid])) + [uxbox.util.uuid :as uuid] + [uxbox.util.data :refer [index-of]])) ;; --- Specs +(s/def ::blocked boolean?) +(s/def ::collapsed boolean?) +(s/def ::content string?) (s/def ::fill-color string?) (s/def ::fill-opacity number?) -(s/def ::line-height number?) -(s/def ::letter-spacing number?) -(s/def ::text-align #{"left" "right" "center" "justify"}) (s/def ::font-family string?) +(s/def ::font-size number?) (s/def ::font-style string?) (s/def ::font-weight string?) -(s/def ::font-size number?) -(s/def ::stroke-style #{:none :solid :dotted :dashed :mixed}) -(s/def ::stroke-width number?) -(s/def ::stroke-color string?) -(s/def ::stroke-opacity number?) -(s/def ::rx number?) -(s/def ::ry number?) +(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 ::collapsed boolean?) -(s/def ::hidden boolean?) -(s/def ::blocked boolean?) -(s/def ::locked 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 ::height number?) (s/def ::x1 number?) -(s/def ::y1 number?) (s/def ::x2 number?) +(s/def ::y1 number?) (s/def ::y2 number?) -(s/def ::id uuid?) -(s/def ::page uuid?) -(s/def ::type #{:rect - :group - :path - :circle - :image - :text}) (s/def ::attributes - (s/keys :opt-un [::fill-color + (s/keys :opt-un [::blocked + ::collapsed + ::conent + ::fill-color ::fill-opacity - ::line-height - ::letter-spacing - ::text-align ::font-family + ::font-size ::font-style ::font-weight - ::font-size - ::stroke-style - ::stroke-width + ::hidden + ::letter-spacing + ::line-height + ::locked + ::proportion + ::proportion-lock + ::rx ::ry ::stroke-color ::stroke-opacity - ::rx ::ry + ::stroke-style + ::stroke-width + ::text-align ::x1 ::x2 - ::y1 ::y2 - ::proportion-lock - ::proportion - ::collapsed - ::hidden - ::blocked - ::locked])) + ::y1 ::y2])) + +(s/def ::minimal-shape + (s/keys ::req-un [::id ::page ::type ::name])) (s/def ::shape - (s/merge (s/keys ::req-un [::id ::page ::type]) ::attributes)) + (s/merge ::minimal-shape ::attributes)) (s/def ::rect-like-shape (s/keys :req-un [::x1 ::y1 ::x2 ::y2 ::type])) -;; --- Shapes CRUD +;; --- Shape Creation -(deftype AddShape [data] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [shape (geom/setup-proportions data) - page-id (get-in state [:workspace :current])] - (impl/assoc-shape-to-page state shape page-id)))) +(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 add-shape - [data] - {:pre [(us/valid? ::shape data)]} - (AddShape. data)) +(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))))) -;; --- Delete Shape +(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)] + (-> state + (update-in [:pages page :shapes] #(into [] (cons shape-id %))) + (assoc-in [:shapes shape-id] shape)))) -(deftype DeleteShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [shape (get-in state [:shapes id])] - (impl/dissoc-shape state shape)))) +(defn duplicate-shapes' + ([state shapes page] + (duplicate-shapes' state shapes page nil)) + ([state shapes page group] + (letfn [(duplicate-shape [state shape page group] + (if (= (:type shape) :group) + (let [id (uuid/random) + items (:items shape) + name (generate-unique-name state (str (:name shape) "-copy")) + shape (assoc shape + :id id + :page page + :items [] + :name name) + state (if (nil? group) + (-> state + (update-in [:pages page :shapes] + #(into [] (cons id %))) + (assoc-in [:shapes id] shape)) + (-> state + (update-in [:shapes group :items] + #(into [] (cons id %))) + (assoc-in [:shapes id] shape)))] + (->> (map #(get-in state [:shapes %]) items) + (reverse) + (reduce #(duplicate-shape %1 %2 page id) state))) + (let [id (uuid/random) + name (generate-unique-name state (str (:name shape) "-copy")) + shape (-> (dissoc shape :group) + (assoc :id id :page page :name name) + (merge (when group {:group group})))] + (if (nil? group) + (-> state + (update-in [:pages page :shapes] #(into [] (cons id %))) + (assoc-in [:shapes id] shape)) + (-> state + (update-in [:shapes group :items] #(into [] (cons id %))) + (assoc-in [:shapes id] shape))))))] + (reduce #(duplicate-shape %1 %2 page group) state shapes)))) -(defn delete-shape - "Remove the shape using its id." - [id] - {:pre [(uuid? id)]} - (DeleteShape. id)) +(defn duplicate-shapes + ([state shapes] + (duplicate-shapes state shapes nil)) + ([state shapes page] + (letfn [(all-toplevel? [coll] + (every? #(nil? (:group %)) coll)) + (all-same-group? [coll] + (let [group (:group (first coll))] + (every? #(= group (:group %)) coll)))] + (let [shapes (reverse (mapv #(get-in state [:shapes %]) shapes))] + (cond + (all-toplevel? shapes) + (let [page (or page (:page (first shapes)))] + (duplicate-shapes' state shapes page)) -;; --- Rename Shape + (all-same-group? shapes) + (let [page (or page (:page (first shapes))) + group (:group (first shapes))] + (duplicate-shapes' state shapes page group)) -(deftype RenameShape [id name] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes id :name] name))) + :else + (let [page (or page (:page (first shapes)))] + (duplicate-shapes' state shapes page))))))) -(defn rename-shape - [id name] - {:pre [(uuid? id) (string? name)]} - (RenameShape. id name)) +;; --- Delete Shapes -;; --- Update Rotation +(defn dissoc-from-index + "A function that dissoc shape from the indexed + data structure of shapes from the state." + [state {:keys [id type] :as shape}] + (if (= :group type) + (let [items (map #(get-in state [:shapes %]) (:items shape))] + (as-> state $ + (update-in $ [:shapes] dissoc id) + (reduce dissoc-from-index $ items))) + (update-in state [:shapes] dissoc id))) -(deftype UpdateShapeRotation [id rotation] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes id :rotation] rotation))) +(defn dissoc-from-page + "Given a shape, try to remove its reference from the + corresponding page." + [state {:keys [id page] :as shape}] + (as-> (get-in state [:pages page :shapes]) $ + (into [] (remove #(= % id) $)) + (assoc-in state [:pages page :shapes] $))) -(defn update-rotation - [id rotation] - {:pre [(uuid? id) - (number? rotation) - (>= rotation 0) - (>= 360 rotation)]} - (UpdateShapeRotation. id rotation)) +(defn dissoc-from-group + "Given a shape, try to remove its reference from the + corresponding group (only if it belongs to one group)." + [state {:keys [id group] :as shape}] + (if-let [group' (get-in state [:shapes group])] + (as-> (:items group') $ + (into [] (remove #(= % id) $)) + (assoc-in state [:shapes group :items] $)) + state)) -;; --- Update Dimensions +(declare dissoc-shape) -(deftype UpdateDimensions [id dimensions] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes id] geom/resize-dim dimensions))) +(defn clear-empty-groups + "Given the shape, try to clean all empty groups + that this shape belongs to. -(s/def ::update-dimensions-opts - (s/keys :opt-un [::width ::height])) + The main purpose of this function is remove the + all empty parent groups of recently removed + shape." + [state {:keys [group] :as shape}] + (if-let [group' (get-in state [:shapes group])] + (if (empty? (:items group')) + (-> (dissoc-shape state group') + (update-in [:workspace :selected] disj (:id group'))) + state) + state)) -(defn update-dimensions - "A helper event just for update the position - of the shape using the width and height attrs - instread final point of coordinates." - [id opts] - {:pre [(uuid? id) (us/valid? ::update-dimensions-opts opts)]} - (UpdateDimensions. id opts)) +(defn dissoc-shape + "Given a shape, removes it from the state." + [state shape] + (as-> state $ + (dissoc-from-page $ shape) + (dissoc-from-group $ shape) + (dissoc-from-index $ shape) + (clear-empty-groups $ shape))) -;; --- Update Shape Position +;; --- Shape Movements -(deftype UpdateShapePosition [id point] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes id] geom/absolute-move point))) +(defn- drop-at-index + [index coll v] + (let [[fst snd] (split-at index coll)] + (into [] (concat fst [v] snd)))) -(defn update-position - "Update the start position coordenate of the shape." - [id point] - {:pre [(uuid? id) (gpt/point? point)]} - (UpdateShapePosition. id point)) +(defn drop-relative + [state loc sid] + {:pre [(not (nil? sid))]} + (let [shape (get-in state [:shapes (first sid)]) + {:keys [page group]} shape + sid (:id shape) -;; --- Update Shape Text + shapes (if group + (get-in state [:shapes group :items]) + (get-in state [:pages page :shapes])) -(deftype UpdateShapeTextContent [id text] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes id :content] text))) + index (case loc + :first 0 + :after (min (- (count shapes) 1) (inc (index-of shapes sid))) + :before (max 0 (- (index-of shapes sid) 1)) + :last (- (count shapes) 1)) -(defn update-text - [id text] - {:pre [(uuid? id) (string? text)]} - (UpdateShapeTextContent. id text)) + state (-> state + (dissoc-from-page shape) + (dissoc-from-group shape)) -;; --- Update Shape Attrs + shapes (if group + (get-in state [:shapes group :items]) + (get-in state [:pages page :shapes])) -(declare UpdateAttrs) -;; TODO: moved -(deftype UpdateAttrs [id attrs] - ptk/WatchEvent - (watch [_ state stream] - (let [{:keys [type] :as shape} (get-in state [:shapes id])] - (if (= type :group) - (rx/from-coll (map #(UpdateAttrs. % attrs) (:items shape))) - (rx/of #(update-in % [:shapes id] merge attrs)))))) + shapes (drop-at-index index shapes sid)] -(defn update-attrs - [id attrs] - {:pre [(uuid? id) (us/valid? ::attributes attrs)]} - (let [atts (us/extract attrs ::attributes)] - (UpdateAttrs. id attrs))) + (if group + (as-> state $ + (assoc-in $ [:shapes group :items] shapes) + (update-in $ [:shapes sid] assoc :group group) + (clear-empty-groups $ shape)) + (as-> state $ + (assoc-in $ [:pages page :shapes] shapes) + (update-in $ [:shapes sid] dissoc :group) + (clear-empty-groups $ shape))))) -;; --- Shape Proportions +(defn drop-aside + [state loc tid sid] + {:pre [(not= tid sid) + (not (nil? tid)) + (not (nil? sid))]} + (let [{:keys [page group]} (get-in state [:shapes tid]) + source (get-in state [:shapes sid]) -(deftype LockShapeProportions [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [[width height] (-> (get-in state [:shapes id]) - (geom/size) - (keep [:width :height])) - proportion (/ width height)] - (update-in state [:shapes id] assoc - :proportion proportion - :proportion-lock true)))) + state (-> state + (dissoc-from-page source) + (dissoc-from-group source)) -(defn lock-proportions - "Mark proportions of the shape locked and save the current - proportion as additional precalculated property." - [id] - {:pre [(uuid? id)]} - (LockShapeProportions. id)) + shapes (if group + (get-in state [:shapes group :items]) + (get-in state [:pages page :shapes])) -(deftype UnlockShapeProportions [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:shapes id :proportion-lock] false))) + index (case loc + :after (inc (index-of shapes tid)) + :before (index-of shapes tid)) -(defn unlock-proportions - [id] - {:pre [(uuid? id)]} - (UnlockShapeProportions. id)) + shapes (drop-at-index index shapes sid)] + (if group + (as-> state $ + (assoc-in $ [:shapes group :items] shapes) + (update-in $ [:shapes sid] assoc :group group) + (clear-empty-groups $ source)) + (as-> state $ + (assoc-in $ [:pages page :shapes] shapes) + (update-in $ [:shapes sid] dissoc :group) + (clear-empty-groups $ source))))) -;; --- Group Collapsing +(def drop-after #(drop-aside %1 :after %2 %3)) +(def drop-before #(drop-aside %1 :before %2 %3)) -(deftype CollapseGroupShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes id] assoc :collapsed true))) - -(defn collapse-shape - [id] - {:pre [(uuid? id)]} - (CollapseGroupShape. id)) - -(deftype UncollapseGroupShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes id] assoc :collapsed false))) - -(defn uncollapse-shape - [id] - {:pre [(uuid? id)]} - (UncollapseGroupShape. id)) - -;; --- Shape Visibility - -(deftype HideShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (letfn [(mark-hidden [state id] - (let [shape (get-in state [:shapes id])] - (if (= :group (:type shape)) - (as-> state $ - (assoc-in $ [:shapes id :hidden] true) - (reduce mark-hidden $ (:items shape))) - (assoc-in state [:shapes id :hidden] true))))] - (mark-hidden state id)))) - -(defn hide-shape - [id] - {:pre [(uuid? id)]} - (HideShape. id)) - -(deftype ShowShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (letfn [(mark-visible [state id] - (let [shape (get-in state [:shapes id])] - (if (= :group (:type shape)) - (as-> state $ - (assoc-in $ [:shapes id :hidden] false) - (reduce mark-visible $ (:items shape))) - (assoc-in state [:shapes id :hidden] false))))] - (mark-visible state id)))) - -(defn show-shape - [id] - {:pre [(uuid? id)]} - (ShowShape. id)) - -;; --- Shape Blocking - -(deftype BlockShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (letfn [(mark-blocked [state id] - (let [shape (get-in state [:shapes id])] - (if (= :group (:type shape)) - (as-> state $ - (assoc-in $ [:shapes id :blocked] true) - (reduce mark-blocked $ (:items shape))) - (assoc-in state [:shapes id :blocked] true))))] - (mark-blocked state id)))) - -(defn block-shape - [id] - {:pre [(uuid? id)]} - (BlockShape. id)) - -(deftype UnblockShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (letfn [(mark-unblocked [state id] - (let [shape (get-in state [:shapes id])] - (if (= :group (:type shape)) - (as-> state $ - (assoc-in $ [:shapes id :blocked] false) - (reduce mark-unblocked $ (:items shape))) - (assoc-in state [:shapes id :blocked] false))))] - (mark-unblocked state id)))) - -(defn unblock-shape - [id] - {:pre [(uuid? id)]} - (UnblockShape. id)) - -;; --- Shape Locking - -(deftype LockShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (letfn [(mark-locked [state id] - (let [shape (get-in state [:shapes id])] - (if (= :group (:type shape)) - (as-> state $ - (assoc-in $ [:shapes id :locked] true) - (reduce mark-locked $ (:items shape))) - (assoc-in state [:shapes id :locked] true))))] - (mark-locked state id)))) - -(defn lock-shape - [id] - {:pre [(uuid? id)]} - (LockShape. id)) - -(deftype UnlockShape [id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (letfn [(mark-unlocked [state id] - (let [shape (get-in state [:shapes id])] - (if (= :group (:type shape)) - (as-> state $ - (assoc-in $ [:shapes id :locked] false) - (reduce mark-unlocked $ (:items shape))) - (assoc-in state [:shapes id :locked] false))))] - (mark-unlocked state id)))) - -(defn unlock-shape - [id] - {:pre [(uuid? id)]} - (UnlockShape. id)) - -;; --- Drop Shape - -(deftype DropShape [sid tid loc] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (impl/drop-shape state sid tid loc))) +(defn drop-inside + [state tid sid] + {:pre [(not= tid sid)]} + (let [source (get-in state [:shapes sid]) + state (-> state + (dissoc-from-page source) + (dissoc-from-group source)) + shapes (get-in state [:shapes tid :items])] + (if (seq shapes) + (as-> state $ + (assoc-in $ [:shapes tid :items] (conj shapes sid)) + (update-in $ [:shapes sid] assoc :group tid)) + state))) (defn drop-shape - "Event used in drag and drop for transfer shape - from one position to an other." - [sid tid loc] - {:pre [(uuid? sid) - (uuid? tid) - (keyword? loc)]} - (DropShape. sid tid loc)) + [state sid tid loc] + (if (= tid sid) + state + (case loc + :inside (drop-inside state tid sid) + :before (drop-before state tid sid) + :after (drop-after state tid sid) + (throw (ex-info "Invalid data" {}))))) -;; --- Update Interaction +(defn move-layer + [state shape loc] + (case loc + :up (drop-relative state :before shape) + :down (drop-relative state :after shape) + :top (drop-relative state :first shape) + :bottom (drop-relative state :last shape) + (throw (ex-info "Invalid data" {})))) -(deftype UpdateInteraction [shape interaction] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [id (or (:id interaction) - (uuid/random)) - data (assoc interaction :id id)] - (assoc-in state [:shapes shape :interactions id] data)))) +;; --- Shape Selection -(defn update-interaction - [shape interaction] - (UpdateInteraction. shape interaction)) +(defn- try-match-shape + [xf selrect acc {:keys [type id items] :as shape}] + (cond + (geom/contained-in? shape selrect) + (conj acc id) -;; --- Delete Interaction + (geom/overlaps? shape selrect) + (conj acc id) -(deftype DeleteInteracton [shape id] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes shape :interactions] dissoc id))) + (:locked shape) + acc -(defn delete-interaction - [shape id] - {:pre [(uuid? id) (uuid? shape)]} - (DeleteInteracton. shape id)) + (= type :group) + (reduce (partial try-match-shape xf selrect) + acc (sequence xf items)) -;; --- Path Modifications + :else + acc)) -(deftype UpdatePath [id index delta] - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes id :segments index] gpt/add delta))) +(defn match-by-selrect + [state page-id selrect] + (let [xf (comp (map #(get-in state [:shapes %])) + (remove :hidden) + (remove :blocked) + (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 update-path - "Update a concrete point in the path shape." - [id index delta] - {:pre [(uuid? id) (number? index) (gpt/point? delta)]} - (UpdatePath. id index delta)) +(defn group-shapes + [state shapes page] + (letfn [(replace-first-item [pred coll replacement] + (into [] + (concat + (take-while #(not (pred %)) coll) + [replacement] + (drop 1 (drop-while #(not (pred %)) coll))))) -(deftype InitialPathPointAlign [id index] - ptk/WatchEvent - (watch [_ state s] - (let [shape (get-in state [:shapes id]) - point (get-in shape [:segments index])] - (->> (uwrk/align-point point) - (rx/map #(update-path id index %)))))) + (move-shapes-to-new-group [state page shapes new-group] + (reduce (fn [state {:keys [id group] :as shape}] + (-> state + (update-in [:shapes group :items] #(remove (set [id]) %)) + (update-in [:pages page :shapes] #(remove (set [id]) %)) + (clear-empty-groups shape) + (assoc-in [:shapes id :group] new-group) + )) + state + shapes)) -(defn initial-path-point-align - "Event responsible of align a specified point of the - shape by `index` with the grid." - [id index] - {:pre [(uuid? id) - (number? index) - (not (neg? index))]} - (InitialPathPointAlign. id index)) + (update-shapes-on-page [state page shapes group] + (as-> (get-in state [:pages page :shapes]) $ + (replace-first-item (set shapes) $ group) + (remove (set shapes) $) + (into [] $) + (assoc-in state [:pages page :shapes] $))) -;; --- Events (implicit) (for selected) + (update-shapes-on-group [state parent-group shapes group] + (as-> (get-in state [:shapes parent-group :items]) $ + (replace-first-item (set shapes) $ group) + (remove (set shapes) $) + (into [] $) + (assoc-in state [:shapes parent-group :items] $))) -;; NOTE: moved to workspace -(deftype DeselectAll [] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace :selected] #{})) + (update-shapes-on-index [state shapes group] + (reduce (fn [state {:keys [id] :as shape}] + (as-> shape $ + (assoc $ :group group) + (assoc-in state [:shapes id] $))) + state + shapes))] + (let [sid (uuid/random) + shapes' (map #(get-in state [:shapes %]) shapes) + distinct-groups (distinct (map :group shapes')) + parent-group (cond + (not= 1 (count distinct-groups)) :multi + (nil? (first distinct-groups)) :page + :else (first distinct-groups)) + name (generate-unique-name state "Group") + group {:type :group + :name name + :items (into [] shapes) + :id sid + :page page}] + (as-> state $ + (update-shapes-on-index $ shapes' sid) + (cond + (= :multi parent-group) + (-> $ + (move-shapes-to-new-group page shapes' sid) + (update-in [:pages page :shapes] #(into [] (cons sid %)))) + (= :page parent-group) + (update-shapes-on-page $ page shapes sid) + :else + (update-shapes-on-group $ parent-group shapes sid)) + (update $ :shapes assoc sid group) + (cond + (= :multi parent-group) $ + (= :page parent-group) $ + :else (assoc-in $ [:shapes sid :group] parent-group)) + (update $ :workspace assoc :selected #{sid}))))) - ptk/WatchEvent - (watch [_ state stream] - (rx/just ::uev/interrupt))) +(defn degroup-shapes + [state shapes page-id] + (letfn [(get-relocation-position [state {id :id parent-id :group}] + (if (nil? parent-id) + (index-of (get-in state [:pages page-id :shapes]) id) + (index-of (get-in state [:shapes parent-id :items]) id))) -(defn deselect-all - "Clear all possible state of drawing, edition - or any similar action taken by the user." - [] - (DeselectAll.)) + (relocate-shape [state shape-id parent-id position] + (if (nil? parent-id) + (-> state + (update-in [:pages page-id :shapes] #(drop-at-index position % shape-id)) + (update-in [:shapes shape-id] dissoc :group)) + (-> state + (update-in [:shapes parent-id :items] #(drop-at-index position % shape-id)) + (assoc-in [:shapes shape-id :group] parent-id)))) -;; --- Group Selected Shapes + (remove-group [state {id :id parent-id :group}] + (let [xform (remove #{id})] + (as-> state $ + (update $ :shapes dissoc id) + (if (nil? parent-id) + (update-in $ [:pages page-id :shapes] #(into [] xform %)) + (update-in $ [:shapes parent-id :items] #(into [] xform %)))))) -(deftype GroupSelectedShapes [] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace :page]) - selected (get-in state [:workspace :selected])] - (assert (not (empty? selected)) "selected set is empty") - (assert (uuid? id) "selected page is not an uuid") - (impl/group-shapes state selected id)))) + (relocate-group-items [state {id :id parent-id :group items :items :as group}] + (let [position (get-relocation-position state group)] + (as-> state $ + (reduce #(relocate-shape %1 %2 parent-id position) $ (reverse items)) + (remove-group $ group)))) -(defn group-selected - [] - (GroupSelectedShapes.)) + (select-degrouped [state groups] + (let [items (into #{} (mapcat :items groups))] + (assoc-in state [:workspace :selected] items))) -;; --- Ungroup Selected Shapes + (remove-from-parent [state id parent-id] + (assert (not (nil? parent-id)) "parent-id should never be nil here") + (update-in state [:shapes parent-id :items] #(into [] (remove #{id}) %))) -(deftype UngroupSelectedShapes [] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace :page]) - selected (get-in state [:workspace :selected])] - (assert (not (empty? selected)) "selected set is empty") - (assert (uuid? id) "selected page is not an uuid") - (impl/degroup-shapes state selected id)))) + (strip-empty-groups [state parent-id] + (if (nil? parent-id) + state + (let [group (get-in state [:shapes parent-id])] + (if (empty? (:items group)) + (-> state + (remove-group group) + (strip-empty-groups (:group group))) + state)))) -(defn ungroup-selected - [] - (UngroupSelectedShapes.)) + (selective-degroup [state [shape & rest :as shapes]] + (let [group (get-in state [:shapes (:group shape)]) + position (get-relocation-position state group) + parent-id (:group group)] + (as-> state $ + (assoc-in $ [:workspace :selected] (into #{} (map :id shapes))) + (reduce (fn [state {shape-id :id}] + (-> state + (relocate-shape shape-id parent-id position) + (remove-from-parent shape-id (:id group)))) + $ (reverse shapes)) + (strip-empty-groups $ (:id group)))))] -;; --- Duplicate Selected + (let [shapes (into #{} (map #(get-in state [:shapes %])) shapes) + groups (into #{} (filter #(= (:type %) :group)) shapes) + parents (into #{} (map :group) shapes)] + (cond + (and (= (count shapes) (count groups)) + (= 1 (count parents)) + (not (empty? groups))) + (as-> state $ + (reduce relocate-group-items $ groups) + (reduce remove-group $ groups) + (select-degrouped $ groups)) -(deftype DuplicateSelected [] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [selected (get-in state [:workspace :selected])] - (impl/duplicate-shapes state selected)))) - -(defn duplicate-selected - [] - (DuplicateSelected.)) + (and (empty? groups) + (= 1 (count parents)) + (not (nil? (first parents)))) + (selective-degroup state shapes) + :else + (throw (ex-info "invalid condition for degrouping" {})))))) +(defn materialize-xfmt + [state id xfmt] + (let [{:keys [type items] :as shape} (get-in state [:shapes id])] + (if (= type :group) + (reduce #(materialize-xfmt %1 %2 xfmt) state items) + (update-in state [:shapes id] geom/transform xfmt)))) diff --git a/frontend/src/uxbox/main/data/shapes_impl.cljs b/frontend/src/uxbox/main/data/shapes_impl.cljs deleted file mode 100644 index 26f718fef7..0000000000 --- a/frontend/src/uxbox/main/data/shapes_impl.cljs +++ /dev/null @@ -1,470 +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-2017 Andrey Antukh - -(ns uxbox.main.data.shapes-impl - (:require [lentes.core :as l] - [uxbox.main.geom :as geom] - [uxbox.main.lenses :as ul] - [uxbox.util.geom.matrix :as gmt] - [uxbox.util.uuid :as uuid] - [uxbox.util.data :refer (index-of)])) - -;; --- Shape Creation - -(defn retrieve-used-names - "Returns a set of already used names by shapes - in the current page." - [{:keys [shapes] :as state}] - (let [page (l/focus ul/selected-page state) - xform (comp (map second) - (filter #(= page (:page %))) - (map :name))] - (into #{} xform 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)] - (-> state - (update-in [:pages page :shapes] #(into [] (cons shape-id %))) - (assoc-in [:shapes shape-id] shape)))) - -(defn duplicate-shapes' - ([state shapes page] - (duplicate-shapes' state shapes page nil)) - ([state shapes page group] - (letfn [(duplicate-shape [state shape page group] - (if (= (:type shape) :group) - (let [id (uuid/random) - items (:items shape) - name (generate-unique-name state (str (:name shape) "-copy")) - shape (assoc shape - :id id - :page page - :items [] - :name name) - state (if (nil? group) - (-> state - (update-in [:pages page :shapes] - #(into [] (cons id %))) - (assoc-in [:shapes id] shape)) - (-> state - (update-in [:shapes group :items] - #(into [] (cons id %))) - (assoc-in [:shapes id] shape)))] - (->> (map #(get-in state [:shapes %]) items) - (reverse) - (reduce #(duplicate-shape %1 %2 page id) state))) - (let [id (uuid/random) - name (generate-unique-name state (str (:name shape) "-copy")) - shape (-> (dissoc shape :group) - (assoc :id id :page page :name name) - (merge (when group {:group group})))] - (if (nil? group) - (-> state - (update-in [:pages page :shapes] #(into [] (cons id %))) - (assoc-in [:shapes id] shape)) - (-> state - (update-in [:shapes group :items] #(into [] (cons id %))) - (assoc-in [:shapes id] shape))))))] - (reduce #(duplicate-shape %1 %2 page group) state shapes)))) - -(defn duplicate-shapes - ([state shapes] - (duplicate-shapes state shapes nil)) - ([state shapes page] - (letfn [(all-toplevel? [coll] - (every? #(nil? (:group %)) coll)) - (all-same-group? [coll] - (let [group (:group (first coll))] - (every? #(= group (:group %)) coll)))] - (let [shapes (reverse (mapv #(get-in state [:shapes %]) shapes))] - (cond - (all-toplevel? shapes) - (let [page (or page (:page (first shapes)))] - (duplicate-shapes' state shapes page)) - - (all-same-group? shapes) - (let [page (or page (:page (first shapes))) - group (:group (first shapes))] - (duplicate-shapes' state shapes page group)) - - :else - (let [page (or page (:page (first shapes)))] - (duplicate-shapes' state shapes page))))))) - -;; --- Delete Shapes - -(defn dissoc-from-index - "A function that dissoc shape from the indexed - data structure of shapes from the state." - [state {:keys [id type] :as shape}] - (if (= :group type) - (let [items (map #(get-in state [:shapes %]) (:items shape))] - (as-> state $ - (update-in $ [:shapes] dissoc id) - (reduce dissoc-from-index $ items))) - (update-in state [:shapes] dissoc id))) - -(defn dissoc-from-page - "Given a shape, try to remove its reference from the - corresponding page." - [state {:keys [id page] :as shape}] - (as-> (get-in state [:pages page :shapes]) $ - (into [] (remove #(= % id) $)) - (assoc-in state [:pages page :shapes] $))) - -(defn dissoc-from-group - "Given a shape, try to remove its reference from the - corresponding group (only if it belongs to one group)." - [state {:keys [id group] :as shape}] - (if-let [group' (get-in state [:shapes group])] - (as-> (:items group') $ - (into [] (remove #(= % id) $)) - (assoc-in state [:shapes group :items] $)) - state)) - -(declare dissoc-shape) - -(defn clear-empty-groups - "Given the shape, try to clean all empty groups - that this shape belongs to. - - The main purpose of this function is remove the - all empty parent groups of recently removed - shape." - [state {:keys [group] :as shape}] - (if-let [group' (get-in state [:shapes group])] - (if (empty? (:items group')) - (-> (dissoc-shape state group') - (update-in [:workspace :selected] disj (:id group'))) - state) - state)) - -(defn dissoc-shape - "Given a shape, removes it from the state." - [state shape] - (as-> state $ - (dissoc-from-page $ shape) - (dissoc-from-group $ shape) - (dissoc-from-index $ shape) - (clear-empty-groups $ shape))) - -;; --- Shape Movements - -(defn- drop-at-index - [index coll v] - (let [[fst snd] (split-at index coll)] - (into [] (concat fst [v] snd)))) - -(defn drop-relative - [state loc sid] - {:pre [(not (nil? sid))]} - (let [shape (get-in state [:shapes (first sid)]) - {:keys [page group]} shape - sid (:id shape) - - shapes (if group - (get-in state [:shapes group :items]) - (get-in state [:pages page :shapes])) - - index (case loc - :first 0 - :after (min (- (count shapes) 1) (inc (index-of shapes sid))) - :before (max 0 (- (index-of shapes sid) 1)) - :last (- (count shapes) 1)) - - state (-> state - (dissoc-from-page shape) - (dissoc-from-group shape)) - - shapes (if group - (get-in state [:shapes group :items]) - (get-in state [:pages page :shapes])) - - shapes (drop-at-index index shapes sid)] - - (if group - (as-> state $ - (assoc-in $ [:shapes group :items] shapes) - (update-in $ [:shapes sid] assoc :group group) - (clear-empty-groups $ shape)) - (as-> state $ - (assoc-in $ [:pages page :shapes] shapes) - (update-in $ [:shapes sid] dissoc :group) - (clear-empty-groups $ shape))))) - -(defn drop-aside - [state loc tid sid] - {:pre [(not= tid sid) - (not (nil? tid)) - (not (nil? sid))]} - (let [{:keys [page group]} (get-in state [:shapes tid]) - source (get-in state [:shapes sid]) - - state (-> state - (dissoc-from-page source) - (dissoc-from-group source)) - - shapes (if group - (get-in state [:shapes group :items]) - (get-in state [:pages page :shapes])) - - index (case loc - :after (inc (index-of shapes tid)) - :before (index-of shapes tid)) - - shapes (drop-at-index index shapes sid)] - (if group - (as-> state $ - (assoc-in $ [:shapes group :items] shapes) - (update-in $ [:shapes sid] assoc :group group) - (clear-empty-groups $ source)) - (as-> state $ - (assoc-in $ [:pages page :shapes] shapes) - (update-in $ [:shapes sid] dissoc :group) - (clear-empty-groups $ source))))) - -(def drop-after #(drop-aside %1 :after %2 %3)) -(def drop-before #(drop-aside %1 :before %2 %3)) - -(defn drop-inside - [state tid sid] - {:pre [(not= tid sid)]} - (let [source (get-in state [:shapes sid]) - state (-> state - (dissoc-from-page source) - (dissoc-from-group source)) - shapes (get-in state [:shapes tid :items])] - (if (seq shapes) - (as-> state $ - (assoc-in $ [:shapes tid :items] (conj shapes sid)) - (update-in $ [:shapes sid] assoc :group tid)) - state))) - -(defn drop-shape - [state sid tid loc] - (if (= tid sid) - state - (case loc - :inside (drop-inside state tid sid) - :before (drop-before state tid sid) - :after (drop-after state tid sid) - (throw (ex-info "Invalid data" {}))))) - -(defn move-layer - [state shape loc] - (case loc - :up (drop-relative state :before shape) - :down (drop-relative state :after shape) - :top (drop-relative state :first shape) - :bottom (drop-relative state :last shape) - (throw (ex-info "Invalid data" {})))) - -;; --- 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) - - (:locked shape) - acc - - (= type :group) - (reduce (partial try-match-shape xf selrect) - acc (sequence xf items)) - - :else - acc)) - -(defn match-by-selrect - [state page-id selrect] - (let [xf (comp (map #(get-in state [:shapes %])) - (remove :hidden) - (remove :blocked) - (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 group-shapes - [state shapes page] - (letfn [(replace-first-item [pred coll replacement] - (into [] - (concat - (take-while #(not (pred %)) coll) - [replacement] - (drop 1 (drop-while #(not (pred %)) coll))))) - - (move-shapes-to-new-group [state page shapes new-group] - (reduce (fn [state {:keys [id group] :as shape}] - (-> state - (update-in [:shapes group :items] #(remove (set [id]) %)) - (update-in [:pages page :shapes] #(remove (set [id]) %)) - (clear-empty-groups shape) - (assoc-in [:shapes id :group] new-group) - )) - state - shapes)) - - (update-shapes-on-page [state page shapes group] - (as-> (get-in state [:pages page :shapes]) $ - (replace-first-item (set shapes) $ group) - (remove (set shapes) $) - (into [] $) - (assoc-in state [:pages page :shapes] $))) - - (update-shapes-on-group [state parent-group shapes group] - (as-> (get-in state [:shapes parent-group :items]) $ - (replace-first-item (set shapes) $ group) - (remove (set shapes) $) - (into [] $) - (assoc-in state [:shapes parent-group :items] $))) - - (update-shapes-on-index [state shapes group] - (reduce (fn [state {:keys [id] :as shape}] - (as-> shape $ - (assoc $ :group group) - (assoc-in state [:shapes id] $))) - state - shapes))] - (let [sid (uuid/random) - shapes' (map #(get-in state [:shapes %]) shapes) - distinct-groups (distinct (map :group shapes')) - parent-group (cond - (not= 1 (count distinct-groups)) :multi - (nil? (first distinct-groups)) :page - :else (first distinct-groups)) - name (generate-unique-name state "Group") - group {:type :group - :name name - :items (into [] shapes) - :id sid - :page page}] - (as-> state $ - (update-shapes-on-index $ shapes' sid) - (cond - (= :multi parent-group) - (-> $ - (move-shapes-to-new-group page shapes' sid) - (update-in [:pages page :shapes] #(into [] (cons sid %)))) - (= :page parent-group) - (update-shapes-on-page $ page shapes sid) - :else - (update-shapes-on-group $ parent-group shapes sid)) - (update $ :shapes assoc sid group) - (cond - (= :multi parent-group) $ - (= :page parent-group) $ - :else (assoc-in $ [:shapes sid :group] parent-group)) - (update $ :workspace assoc :selected #{sid}))))) - -(defn degroup-shapes - [state shapes page-id] - (letfn [(get-relocation-position [state {id :id parent-id :group}] - (if (nil? parent-id) - (index-of (get-in state [:pages page-id :shapes]) id) - (index-of (get-in state [:shapes parent-id :items]) id))) - - (relocate-shape [state shape-id parent-id position] - (if (nil? parent-id) - (-> state - (update-in [:pages page-id :shapes] #(drop-at-index position % shape-id)) - (update-in [:shapes shape-id] dissoc :group)) - (-> state - (update-in [:shapes parent-id :items] #(drop-at-index position % shape-id)) - (assoc-in [:shapes shape-id :group] parent-id)))) - - (remove-group [state {id :id parent-id :group}] - (let [xform (remove #{id})] - (as-> state $ - (update $ :shapes dissoc id) - (if (nil? parent-id) - (update-in $ [:pages page-id :shapes] #(into [] xform %)) - (update-in $ [:shapes parent-id :items] #(into [] xform %)))))) - - (relocate-group-items [state {id :id parent-id :group items :items :as group}] - (let [position (get-relocation-position state group)] - (as-> state $ - (reduce #(relocate-shape %1 %2 parent-id position) $ (reverse items)) - (remove-group $ group)))) - - (select-degrouped [state groups] - (let [items (into #{} (mapcat :items groups))] - (assoc-in state [:workspace :selected] items))) - - (remove-from-parent [state id parent-id] - (assert (not (nil? parent-id)) "parent-id should never be nil here") - (update-in state [:shapes parent-id :items] #(into [] (remove #{id}) %))) - - (strip-empty-groups [state parent-id] - (if (nil? parent-id) - state - (let [group (get-in state [:shapes parent-id])] - (if (empty? (:items group)) - (-> state - (remove-group group) - (strip-empty-groups (:group group))) - state)))) - - (selective-degroup [state [shape & rest :as shapes]] - (let [group (get-in state [:shapes (:group shape)]) - position (get-relocation-position state group) - parent-id (:group group)] - (as-> state $ - (assoc-in $ [:workspace :selected] (into #{} (map :id shapes))) - (reduce (fn [state {shape-id :id}] - (-> state - (relocate-shape shape-id parent-id position) - (remove-from-parent shape-id (:id group)))) - $ (reverse shapes)) - (strip-empty-groups $ (:id group)))))] - (let [shapes (into #{} (map #(get-in state [:shapes %])) shapes) - groups (into #{} (filter #(= (:type %) :group)) shapes) - parents (into #{} (map :group) shapes)] - (cond - (and (= (count shapes) (count groups)) - (= 1 (count parents)) - (not (empty? groups))) - (as-> state $ - (reduce relocate-group-items $ groups) - (reduce remove-group $ groups) - (select-degrouped $ groups)) - - (and (empty? groups) - (= 1 (count parents)) - (not (nil? (first parents)))) - (selective-degroup state shapes) - - :else - (throw (ex-info "invalid condition for degrouping" {})))))) - -(defn materialize-xfmt - [state id xfmt] - (let [{:keys [type items] :as shape} (get-in state [:shapes id])] - (if (= type :group) - (reduce #(materialize-xfmt %1 %2 xfmt) state items) - (update-in state [:shapes id] geom/transform xfmt)))) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 12904f429c..785e3af816 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -7,24 +7,21 @@ (ns uxbox.main.data.workspace (:require [beicon.core :as rx] + ;; [uxbox.main.data.workspace.ruler :as wruler] [cljs.spec.alpha :as s] [potok.core :as ptk] [uxbox.config :as cfg] [uxbox.main.constants :as c] [uxbox.main.data.history :as udh] [uxbox.main.data.icons :as udi] - [uxbox.main.data.lightbox :as udl] [uxbox.main.data.pages :as udp] [uxbox.main.data.projects :as dp] - [uxbox.main.data.shapes :as uds] - [uxbox.main.data.shapes-impl :as simpl] - [uxbox.main.data.workspace.ruler :as wruler] + [uxbox.main.data.shapes :as ds] [uxbox.main.geom :as geom] - [uxbox.main.lenses :as ul] [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.workers :as uwrk] - [uxbox.util.data :refer [dissoc-in index-of]] + [uxbox.util.data :refer [dissoc-in index-of seek]] [uxbox.util.forms :as sc] [uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.point :as gpt] @@ -35,8 +32,10 @@ ;; --- Expose inner functions -(def start-ruler wruler/start-ruler) -(def clear-ruler wruler/clear-ruler) +(def start-ruler nil) +(def clear-ruler nil) + +(defn interrupt? [e] (= e :interrupt)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; General workspace events @@ -258,7 +257,7 @@ (->> (:clipboard state) (filter #(= id (:id %))) (first)))] - (simpl/duplicate-shapes state (:items selected) page-id)))) + (ds/duplicate-shapes state (:items selected) page-id)))) (defn paste-from-clipboard "Copy selected shapes to clipboard." @@ -327,18 +326,51 @@ {:pre [(uuid? id)]} (InitializeAlignment. id)) +;; --- Duplicate Selected + +(def duplicate-selected + (reify + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (let [selected (get-in state [:workspace :selected])] + (ds/duplicate-shapes state selected))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Shapes on Workspace events +;; Shapes events ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +(defn add-shape + [data] + {:pre [(us/valid? ::ds/shape data)]} + (reify + udp/IPageUpdate + 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))))) + +(defn delete-shape + [id] + {:pre [(uuid? id)]} + (reify + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (let [shape (get-in state [:shapes id])] + (ds/dissoc-shape state shape))))) + (defrecord SelectShape [id] ptk/UpdateEvent (update [_ state] - (let [page-id (get-in state [:workspace :current]) - selected (get-in state [:workspace page-id :selected])] + (let [pid (get-in state [:workspace :current]) + selected (get-in state [:workspace pid :selected])] (if (contains? selected id) - (update-in state [:workspace page-id :selected] disj id) - (update-in state [:workspace page-id :selected] conj id)))) + (update-in state [:workspace pid :selected] disj id) + (update-in state [:workspace pid :selected] conj id)))) ptk/WatchEvent (watch [_ state s] @@ -353,9 +385,10 @@ (defrecord DeselectAll [] ptk/UpdateEvent (update [_ state] - (let [page-id (get-in state [:workspace :current])] - (assoc-in state [:workspace page-id :selected] #{}))) - + (let [pid (get-in state [:workspace :current])] + (update-in state [:workspace pid] #(-> % + (assoc :selected #{}) + (dissoc :selected-canvas))))) ptk/WatchEvent (watch [_ state stream] (rx/just :interrupt))) @@ -388,7 +421,7 @@ (update [_ state] (let [pid (get-in state [:workspace :current]) selrect (get-in state [:workspace pid :selrect]) - shapes (simpl/match-by-selrect state pid selrect)] + shapes (ds/match-by-selrect state pid selrect)] (assoc-in state [:workspace pid :selected] shapes))))) ;; --- Update Shape Attrs @@ -400,8 +433,8 @@ (defn update-shape-attrs [id attrs] - {:pre [(uuid? id) (us/valid? ::uds/attributes attrs)]} - (let [atts (us/extract attrs ::uds/attributes)] + {:pre [(uuid? id) (us/valid? ::ds/attributes attrs)]} + (let [atts (us/extract attrs ::ds/attributes)] (UpdateShapeAttrs. id attrs))) ;; --- Update Selected Shapes attrs @@ -416,7 +449,7 @@ (defn update-selected-shapes-attrs [attrs] - {:pre [(us/valid? ::uds/attributes attrs)]} + {:pre [(us/valid? ::ds/attributes attrs)]} (UpdateSelectedShapesAttrs. attrs)) @@ -486,27 +519,49 @@ (update [_ state] (let [id (get-in state [:workspace :current]) selected (get-in state [:workspace id :selected])] - (simpl/move-layer state selected loc)))) + (ds/move-layer state selected loc)))) (defn move-selected-layer [loc] {:pre [(us/valid? ::direction loc)]} (MoveSelectedLayer. loc)) +;; --- Update Shape Position + +(deftype UpdateShapePosition [id point] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (update-in state [:shapes id] geom/absolute-move point))) + +(defn update-position + "Update the start position coordenate of the shape." + [id point] + {:pre [(uuid? id) (gpt/point? point)]} + (UpdateShapePosition. id point)) + ;; --- Delete Selected -(defrecord DeleteSelected [] - ptk/WatchEvent - (watch [_ state stream] - (let [id (get-in state [:workspace :current]) - selected (get-in state [:workspace id :selected])] - (rx/from-coll - (into [(deselect-all)] (map #(uds/delete-shape %) selected)))))) - -(defn delete-selected +(def delete-selected "Deselect all and remove all selected shapes." - [] - (DeleteSelected.)) + (reify + ptk/WatchEvent + (watch [_ state stream] + (let [id (get-in state [:workspace :current]) + selected (get-in state [:workspace id :selected])] + (rx/from-coll + (into [(deselect-all)] (map #(delete-shape %) selected))))))) + +;; --- Rename Shape + +(defn rename-shape + [id name] + {:pre [(uuid? id) (string? name)]} + (reify + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:shapes id :name] name)))) ;; --- Change Shape Order (Ordering) @@ -563,7 +618,7 @@ (let [pid (get-in state [:workspace :current]) displacement (get-in state [:workspace pid :modifiers id :displacement])] (if (gmt/matrix? displacement) - (rx/of #(simpl/materialize-xfmt % id displacement) + (rx/of #(ds/materialize-xfmt % id displacement) #(update-in % [:workspace pid :modifiers id] dissoc :displacement) ::udp/page-update) (rx/empty))))) @@ -595,7 +650,7 @@ (let [pid (get-in state [:workspace :current]) resize (get-in state [:workspace pid :modifiers id :resize])] (if (gmt/matrix? resize) - (rx/of #(simpl/materialize-xfmt % id resize) + (rx/of #(ds/materialize-xfmt % id resize) #(update-in % [:workspace pid :modifiers id] dissoc :resize) ::udp/page-update) (rx/empty))))) @@ -627,19 +682,252 @@ ;; --- Select for Drawing -(defn select-for-drawing - [shape] +(def clear-drawing (reify ptk/UpdateEvent (update [_ state] - (let [pid (get-in state [:workspace :current]) - current (get-in state [:workspace pid :drawing-tool])] - (if (or (nil? shape) - (= shape current)) - (update-in state [:workspace pid] dissoc :drawing :drawing-tool) - (update-in state [:workspace pid] assoc - :drawing shape - :drawing-tool shape)))))) + (let [pid (get-in state [:workspace :current])] + (update-in state [:workspace pid] dissoc :drawing-tool :drawing))))) + +(defn select-for-drawing? + [e] + (= (::type (meta e)) ::select-for-drawing)) + +(defn select-for-drawing + [tool] + (reify + IMeta + (-meta [_] {::type ::select-for-drawing}) + + ptk/UpdateEvent + (update [_ state] + (prn "select-for-drawing" tool) + (let [pid (get-in state [:workspace :current])] + (update-in state [:workspace pid] assoc :drawing-tool tool))))) + +;; --- Shape Proportions + +(deftype LockShapeProportions [id] + ptk/UpdateEvent + (update [_ state] + (let [[width height] (-> (get-in state [:shapes id]) + (geom/size) + (keep [:width :height])) + proportion (/ width height)] + (update-in state [:shapes id] assoc + :proportion proportion + :proportion-lock true)))) + +(defn lock-proportions + "Mark proportions of the shape locked and save the current + proportion as additional precalculated property." + [id] + {:pre [(uuid? id)]} + (LockShapeProportions. id)) + +(deftype UnlockShapeProportions [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:shapes id :proportion-lock] false))) + +(defn unlock-proportions + [id] + {:pre [(uuid? id)]} + (UnlockShapeProportions. id)) + +;; --- Update Dimensions + +(deftype UpdateDimensions [id dimensions] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (update-in state [:shapes id] geom/resize-dim dimensions))) + +(s/def ::update-dimensions-opts + (s/keys :opt-un [::width ::height])) + +(defn update-dimensions + "A helper event just for update the position + of the shape using the width and height attrs + instread final point of coordinates." + [id opts] + {:pre [(uuid? id) (us/valid? ::update-dimensions-opts opts)]} + (UpdateDimensions. id opts)) + +;; --- Update Interaction + +(deftype UpdateInteraction [shape interaction] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (let [id (or (:id interaction) + (uuid/random)) + data (assoc interaction :id id)] + (assoc-in state [:shapes shape :interactions id] data)))) + +(defn update-interaction + [shape interaction] + (UpdateInteraction. shape interaction)) + +;; --- Delete Interaction + +(deftype DeleteInteracton [shape id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (update-in state [:shapes shape :interactions] dissoc id))) + +(defn delete-interaction + [shape id] + {:pre [(uuid? id) (uuid? shape)]} + (DeleteInteracton. shape id)) + +;; --- Path Modifications + +(deftype UpdatePath [id index delta] + ptk/UpdateEvent + (update [_ state] + (update-in state [:shapes id :segments index] gpt/add delta))) + +(defn update-path + "Update a concrete point in the path shape." + [id index delta] + {:pre [(uuid? id) (number? index) (gpt/point? delta)]} + (UpdatePath. id index delta)) + +;; --- Initial Path Point Alignment + +(deftype InitialPathPointAlign [id index] + ptk/WatchEvent + (watch [_ state s] + (let [shape (get-in state [:shapes id]) + point (get-in shape [:segments index])] + (->> (uwrk/align-point point) + (rx/map #(update-path id index %)))))) + +(defn initial-path-point-align + "Event responsible of align a specified point of the + shape by `index` with the grid." + [id index] + {:pre [(uuid? id) + (number? index) + (not (neg? index))]} + (InitialPathPointAlign. id index)) + +;; --- Shape Visibility + +(deftype HideShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-hidden [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :hidden] true) + (reduce mark-hidden $ (:items shape))) + (assoc-in state [:shapes id :hidden] true))))] + (mark-hidden state id)))) + +(defn hide-shape + [id] + {:pre [(uuid? id)]} + (HideShape. id)) + +(deftype ShowShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-visible [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :hidden] false) + (reduce mark-visible $ (:items shape))) + (assoc-in state [:shapes id :hidden] false))))] + (mark-visible state id)))) + +(defn show-shape + [id] + {:pre [(uuid? id)]} + (ShowShape. id)) + +;; --- Shape Blocking + +(deftype BlockShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-blocked [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :blocked] true) + (reduce mark-blocked $ (:items shape))) + (assoc-in state [:shapes id :blocked] true))))] + (mark-blocked state id)))) + +(defn block-shape + [id] + {:pre [(uuid? id)]} + (BlockShape. id)) + +(deftype UnblockShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-unblocked [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :blocked] false) + (reduce mark-unblocked $ (:items shape))) + (assoc-in state [:shapes id :blocked] false))))] + (mark-unblocked state id)))) + +(defn unblock-shape + [id] + {:pre [(uuid? id)]} + (UnblockShape. id)) + +;; --- Shape Locking + +(deftype LockShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-locked [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :locked] true) + (reduce mark-locked $ (:items shape))) + (assoc-in state [:shapes id :locked] true))))] + (mark-locked state id)))) + +(defn lock-shape + [id] + {:pre [(uuid? id)]} + (LockShape. id)) + +(deftype UnlockShape [id] + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (letfn [(mark-unlocked [state id] + (let [shape (get-in state [:shapes id])] + (if (= :group (:type shape)) + (as-> state $ + (assoc-in $ [:shapes id :locked] false) + (reduce mark-unlocked $ (:items shape))) + (assoc-in state [:shapes id :locked] false))))] + (mark-unlocked state id)))) + +(defn unlock-shape + [id] + {:pre [(uuid? id)]} + (UnlockShape. id)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Selection Rect IMPL @@ -662,6 +950,42 @@ :y2 end-y :type :rect))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Canvas Interactions +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; ;; --- Group Collapsing + +;; (deftype CollapseGroupShape [id] +;; udp/IPageUpdate +;; ptk/UpdateEvent +;; (update [_ state] +;; (update-in state [:shapes id] assoc :collapsed true))) + +;; (defn collapse-shape +;; [id] +;; {:pre [(uuid? id)]} +;; (CollapseGroupShape. id)) + +;; (deftype UncollapseGroupShape [id] +;; udp/IPageUpdate +;; ptk/UpdateEvent +;; (update [_ state] +;; (update-in state [:shapes id] assoc :collapsed false))) + +;; (defn uncollapse-shape +;; [id] +;; {:pre [(uuid? id)]} +;; (UncollapseGroupShape. id)) + +(defn select-canvas + [id] + (reify + ptk/UpdateEvent + (update [_ state] + (let [pid (get-in state [:workspace :current])] + (update-in state [:workspace pid] assoc :selected-canvas id))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Server Interactions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/uxbox/main/data/workspace/ruler.cljs b/frontend/src/uxbox/main/data/workspace/ruler.cljs index 9fb062c86c..be34e81fa6 100644 --- a/frontend/src/uxbox/main/data/workspace/ruler.cljs +++ b/frontend/src/uxbox/main/data/workspace/ruler.cljs @@ -7,7 +7,7 @@ (ns uxbox.main.data.workspace.ruler "Workspace ruler related events. Mostly or all events are related to UI logic." - (:require [beicon.core :as rx] + #_(:require [beicon.core :as rx] [potok.core :as ptk] [uxbox.main.refs :as refs] [uxbox.main.streams :as streams] @@ -17,79 +17,79 @@ ;; --- Constants -(declare stop-ruler?) -(declare clear-ruler) -(declare update-ruler) +;; (declare stop-ruler?) +;; (declare clear-ruler) +;; (declare update-ruler) -(def ^:private immanted-zones - (let [transform #(vector (- % 7) (+ % 7) %) - right (map transform (range 0 181 15)) - left (map (comp transform -) (range 0 181 15))] - (vec (concat right left)))) +;; (def ^:private immanted-zones +;; (let [transform #(vector (- % 7) (+ % 7) %) +;; right (map transform (range 0 181 15)) +;; left (map (comp transform -) (range 0 181 15))] +;; (vec (concat right left)))) -(defn- align-position - [pos] - (let [angle (gpt/angle pos)] - (reduce (fn [pos [a1 a2 v]] - (if (< a1 angle a2) - (reduced (gpt/update-angle pos v)) - pos)) - pos - immanted-zones))) +;; (defn- align-position +;; [pos] +;; (let [angle (gpt/angle pos)] +;; (reduce (fn [pos [a1 a2 v]] +;; (if (< a1 angle a2) +;; (reduced (gpt/update-angle pos v)) +;; pos)) +;; pos +;; immanted-zones))) -;; --- Start Ruler +;; ;; --- Start Ruler -(deftype StartRuler [] - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :current]) - pos (get-in state [:workspace :pointer :viewport])] - (assoc-in state [:workspace pid :ruler] {:start pos :end pos}))) +;; (deftype StartRuler [] +;; ptk/UpdateEvent +;; (update [_ state] +;; (let [pid (get-in state [:workspace :current]) +;; pos (get-in state [:workspace :pointer :viewport])] +;; (assoc-in state [:workspace pid :ruler] {:start pos :end pos}))) - ptk/WatchEvent - (watch [_ state stream] - (let [stoper (->> (rx/filter #(= ::uev/interrupt %) stream) - (rx/take 1))] - (->> streams/mouse-position - (rx/take-until stoper) - (rx/map (juxt :viewport :ctrl)) - (rx/map (fn [[pt ctrl?]] - (update-ruler pt ctrl?))))))) +;; ptk/WatchEvent +;; (watch [_ state stream] +;; (let [stoper (->> (rx/filter #(= ::uev/interrupt %) stream) +;; (rx/take 1))] +;; (->> streams/mouse-position +;; (rx/take-until stoper) +;; (rx/map (juxt :viewport :ctrl)) +;; (rx/map (fn [[pt ctrl?]] +;; (update-ruler pt ctrl?))))))) -(defn start-ruler - [] - (StartRuler.)) +;; (defn start-ruler +;; [] +;; (StartRuler.)) -;; --- Update Ruler +;; ;; --- Update Ruler -(deftype UpdateRuler [point ctrl?] - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :current]) - ruler (get-in state [:workspace pid :ruler])] - (if-not ctrl? - (assoc-in state [:workspace pid :ruler :end] point) - (let [start (get-in state [:workspace pid :ruler :start]) - end (-> (gpt/subtract point start) - (align-position) - (gpt/add start))] - (assoc-in state [:workspace pid :ruler :end] end)))))) +;; (deftype UpdateRuler [point ctrl?] +;; ptk/UpdateEvent +;; (update [_ state] +;; (let [pid (get-in state [:workspace :current]) +;; ruler (get-in state [:workspace pid :ruler])] +;; (if-not ctrl? +;; (assoc-in state [:workspace pid :ruler :end] point) +;; (let [start (get-in state [:workspace pid :ruler :start]) +;; end (-> (gpt/subtract point start) +;; (align-position) +;; (gpt/add start))] +;; (assoc-in state [:workspace pid :ruler :end] end)))))) -(defn update-ruler - [point ctrl?] - {:pre [(gpt/point? point) - (boolean? ctrl?)]} - (UpdateRuler. point ctrl?)) +;; (defn update-ruler +;; [point ctrl?] +;; {:pre [(gpt/point? point) +;; (boolean? ctrl?)]} +;; (UpdateRuler. point ctrl?)) -;; --- Clear Ruler +;; ;; --- Clear Ruler -(deftype ClearRuler [] - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid] dissoc :ruler)))) +;; (deftype ClearRuler [] +;; ptk/UpdateEvent +;; (update [_ state] +;; (let [pid (get-in state [:workspace :current])] +;; (update-in state [:workspace pid] dissoc :ruler)))) -(defn clear-ruler - [] - (ClearRuler.)) +;; (defn clear-ruler +;; [] +;; (ClearRuler.)) diff --git a/frontend/src/uxbox/main/data/workspace_drawing.cljs b/frontend/src/uxbox/main/data/workspace_drawing.cljs index 91b1235cb7..ad33037f77 100644 --- a/frontend/src/uxbox/main/data/workspace_drawing.cljs +++ b/frontend/src/uxbox/main/data/workspace_drawing.cljs @@ -6,23 +6,7 @@ ;; TODO: DEPRECTATED, maintained just for temporal documentation, delete on near future -(ns uxbox.main.data.workspace-drawing - "Workspace drawing data events and impl." - (:require [beicon.core :as rx] - [potok.core :as ptk] - [lentes.core :as l] - [uxbox.main.store :as st] - [uxbox.main.constants :as c] - [uxbox.main.refs :as refs] - [uxbox.main.streams :as streams] - [uxbox.main.data.shapes :as uds] - [uxbox.main.data.workspace :as udw] - [uxbox.main.geom :as geom] - [uxbox.main.workers :as uwrk] - [uxbox.main.user-events :as uev] - [uxbox.main.lenses :as ul] - [uxbox.util.geom.path :as pth] - [uxbox.util.geom.point :as gpt])) +(ns uxbox.main.data.workspace-drawing) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Events @@ -234,7 +218,7 @@ ;; (rx/filter uev/mouse-up?) ;; (rx/take 1))) ;; start? (volatile! true) -;; mouse (->> streams/viewport-mouse-position +;; mouse (->> streams/mouse-position ;; (rx/take-until stoper) ;; (rx/mapcat conditional-align) ;; (rx/map translate-to-canvas) @@ -325,7 +309,7 @@ ;; (defn- on-init-draw-free-path ;; [shape stoper] ;; (let [stoper (get-path-stoper-stream stoper true) -;; mouse (->> streams/viewport-mouse-position +;; mouse (->> streams/mouse-position ;; (rx/mapcat conditional-align) ;; (rx/map translate-to-canvas)) @@ -341,7 +325,7 @@ ;; [shape stoper] ;; (let [last-point (volatile! @refs/canvas-mouse-position) ;; stoper (get-path-stoper-stream stoper) -;; mouse (->> (rx/sample 10 streams/viewport-mouse-position) +;; mouse (->> (rx/sample 10 streams/mouse-position) ;; (rx/mapcat conditional-align) ;; (rx/map translate-to-canvas)) ;; points (->> (get-path-point-stream) diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index bd5b01efe8..c19f03dc10 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -26,6 +26,7 @@ :image (move-rect shape dpoint) :rect (move-rect shape dpoint) :text (move-rect shape dpoint) + :curve (move-path shape dpoint) :path (move-path shape dpoint) :circle (move-circle shape dpoint) :group (move-group shape dpoint))) @@ -125,12 +126,12 @@ "Calculate the size of the shape." [shape] (case (:type shape) - :group (assoc shape :width 100 :height 100) :circle (size-circle shape) :text (size-rect shape) :rect (size-rect shape) :icon (size-rect shape) :image (size-rect shape) + :curve (size-path shape) :path (size-path shape))) (defn- size-path @@ -184,6 +185,7 @@ :icon (setup-proportions-image shape) :image (setup-proportions-image shape) :text shape + :curve (setup-proportions-rect shape) :path (setup-proportions-rect shape))) (defn setup-proportions-image @@ -461,6 +463,7 @@ (case type :circle (circle->rect-shape state shape) :path (path->rect-shape state shape) + :curve (path->rect-shape state shape) shape))) (defn shapes->rect-shape @@ -517,6 +520,7 @@ :text (transform-rect shape xfmt) :image (transform-rect shape xfmt) :path (transform-path shape xfmt) + :curve (transform-path shape xfmt) :circle (transform-circle shape xfmt))) (defn- transform-rect diff --git a/frontend/src/uxbox/main/lenses.cljs b/frontend/src/uxbox/main/lenses.cljs deleted file mode 100644 index 2e2d63f2af..0000000000 --- a/frontend/src/uxbox/main/lenses.cljs +++ /dev/null @@ -1,13 +0,0 @@ -(ns uxbox.main.lenses - (:require [lentes.core :as l])) - -;; --- Workspace -;; --- FIXME: remove this ns - -(def workspace (l/key :workspace)) -(def workspace-flags (comp workspace (l/key :flags))) - -(def selected-drawing (comp workspace (l/key :drawing))) -(def selected-shapes (comp workspace (l/key :selected))) -(def selected-page (comp workspace (l/key :page))) -(def selected-project (comp workspace (l/key :project))) diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index 58dde791bc..eaa147ad2e 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -52,6 +52,10 @@ (-> (l/key :selected) (l/derive workspace))) +(def selected-canvas + (-> (l/key :selected-canvas) + (l/derive workspace))) + (def toolboxes (-> (l/key :toolboxes) (l/derive workspace))) @@ -100,28 +104,6 @@ (l/lens alignment-activated?)) (l/derive workspace))) -;; ... - -(def mouse-position - (-> (l/in [:workspace :pointer]) - (l/derive st/state))) - -(def canvas-mouse-position - (-> (l/key :canvas) - (l/derive mouse-position))) - -(def viewport-mouse-position - (-> (l/key :viewport) - (l/derive mouse-position))) - -(def window-mouse-position - (-> (l/key :window) - (l/derive mouse-position))) - -(def workspace-scroll - (-> (l/in [:workspace :scroll]) - (l/derive st/state))) - (def shapes-by-id (-> (l/key :shapes) (l/derive st/state))) diff --git a/frontend/src/uxbox/main/repo/pages.cljs b/frontend/src/uxbox/main/repo/pages.cljs index ddd3b593be..ae6356c1ec 100644 --- a/frontend/src/uxbox/main/repo/pages.cljs +++ b/frontend/src/uxbox/main/repo/pages.cljs @@ -2,14 +2,13 @@ ;; 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 Andrey Antukh +;; Copyright (c) 2016-2019 Andrey Antukh (ns uxbox.main.repo.pages "A main interface for access to remote resources." - (:require [beicon.core :as rx] - [uxbox.config :refer (url)] - [uxbox.main.repo.impl :refer (request send!)] - [uxbox.util.transit :as t])) + (:require + [uxbox.config :refer [url]] + [uxbox.main.repo.impl :refer [request send!]])) (defmethod request :fetch/pages [type data] diff --git a/frontend/src/uxbox/main/streams.cljs b/frontend/src/uxbox/main/streams.cljs deleted file mode 100644 index c7765f0903..0000000000 --- a/frontend/src/uxbox/main/streams.cljs +++ /dev/null @@ -1,65 +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) 2017 Andrey Antukh - -(ns uxbox.main.streams - "A collection of derived streams." - (:require [beicon.core :as rx] - [uxbox.main.store :as st] - [uxbox.main.user-events :as uev] - [uxbox.main.refs :as refs] - [uxbox.main.workers :as uwrk] - [uxbox.util.geom.point :as gpt])) - -;; --- Events - -(defn- user-interaction-event? - [event] - (or (uev/keyboard-event? event) - (uev/mouse-event? event))) - -(defonce events - (rx/filter user-interaction-event? st/stream)) - -;; --- Mouse Position Stream - -(defonce mouse-position - (rx/filter uev/pointer-event? st/stream)) - -(defonce canvas-mouse-position - (->> mouse-position - (rx/map :canvas) - (rx/share))) - -(defonce viewport-mouse-position - (->> mouse-position - (rx/map :viewport) - (rx/share))) - -(defonce window-mouse-position - (->> mouse-position - (rx/map :window) - (rx/share))) - -(defonce mouse-position-ctrl - (->> mouse-position - (rx/map :ctrl) - (rx/share))) - -(defn- coords-delta - [[old new]] - (gpt/subtract new old)) - -(defonce mouse-position-deltas - (->> viewport-mouse-position - (rx/sample 10) - (rx/map #(gpt/divide % @refs/selected-zoom)) - (rx/mapcat (fn [point] - (if @refs/selected-alignment - (uwrk/align-point point) - (rx/of point)))) - (rx/buffer 2 1) - (rx/map coords-delta) - (rx/share))) diff --git a/frontend/src/uxbox/main/ui/shapes.cljs b/frontend/src/uxbox/main/ui/shapes.cljs index 8610a6b1f5..135bd5ae1a 100644 --- a/frontend/src/uxbox/main/ui/shapes.cljs +++ b/frontend/src/uxbox/main/ui/shapes.cljs @@ -20,6 +20,7 @@ [shape] (mf/html (case (:type shape) + :curve [:& path/path-component {:shape shape}] :text [:& text/text-component {:shape shape}] :icon [:& icon/icon-component {:shape shape}] :rect [:& rect/rect-component {:shape shape}] diff --git a/frontend/src/uxbox/main/ui/shapes/common.cljs b/frontend/src/uxbox/main/ui/shapes/common.cljs index 6d972ba25b..3c7b447c25 100644 --- a/frontend/src/uxbox/main/ui/shapes/common.cljs +++ b/frontend/src/uxbox/main/ui/shapes/common.cljs @@ -25,7 +25,7 @@ (watch [_ state stream] (let [pid (get-in state [:workspace :current]) wst (get-in state [:workspace pid]) - stoper (->> ws/interaction-events + stoper (->> stream (rx/filter ws/mouse-up?) (rx/take 1)) stream (->> ws/mouse-position-deltas @@ -58,7 +58,8 @@ (and (not selected?) (empty? selected)) (do (dom/stop-propagation event) - (st/emit! (dw/select-shape id) + (st/emit! (dw/deselect-all) + (dw/select-shape id) (start-move-selected))) (and (not selected?) (not (empty? selected))) diff --git a/frontend/src/uxbox/main/ui/shapes/text.cljs b/frontend/src/uxbox/main/ui/shapes/text.cljs index ea093ab702..fdfb72047a 100644 --- a/frontend/src/uxbox/main/ui/shapes/text.cljs +++ b/frontend/src/uxbox/main/ui/shapes/text.cljs @@ -126,7 +126,7 @@ style (make-style shape) on-input (fn [ev] (let [content (dom/event->inner-text ev)] - (st/emit! (uds/update-text id content))))] + (st/emit! (udw/update-shape-attrs id {:content content}))))] [:foreignObject {:x x1 :y y1 :width width :height height} [:div {:style (normalize-props style) :ref (::container own) diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index 18a0ee1506..296c6d7b7a 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -52,7 +52,7 @@ (let [prev-zoom @refs/selected-zoom dom (mf/ref-node canvas) scroll-position (scroll/get-current-position-absolute dom) - mouse-point @uws/viewport-mouse-position] + mouse-point @uws/mouse-position] (dom/prevent-default event) (dom/stop-propagation event) (if (pos? (.-deltaY event)) @@ -62,7 +62,7 @@ (defn- subscribe [canvas page] - (scroll/scroll-to-page-center (mf/ref-node canvas) page) + ;; (scroll/scroll-to-page-center (mf/ref-node canvas) page) (st/emit! (udp/watch-page-changes (:id page)) (udu/watch-page-changes (:id page))) (let [sub (shortcuts/init)] diff --git a/frontend/src/uxbox/main/ui/workspace/canvas.cljs b/frontend/src/uxbox/main/ui/workspace/canvas.cljs index 2ac61597ef..21606e406c 100644 --- a/frontend/src/uxbox/main/ui/workspace/canvas.cljs +++ b/frontend/src/uxbox/main/ui/workspace/canvas.cljs @@ -8,45 +8,46 @@ (ns uxbox.main.ui.workspace.canvas (:require [rumext.alpha :as mf] + [lentes.core :as l] [uxbox.main.constants :as c] + [uxbox.main.refs :as refs] + [uxbox.main.data.workspace :as dw] [uxbox.main.store :as st] + [uxbox.main.ui.keyboard :as kbd] [uxbox.main.ui.shapes :as uus] [uxbox.main.ui.workspace.drawarea :refer [draw-area]] [uxbox.main.ui.workspace.selection :refer [selection-handlers]] + [uxbox.main.ui.workspace.streams :as uws] + [uxbox.util.data :refer [parse-int]] + [uxbox.util.dom :as dom] [uxbox.util.geom.point :as gpt])) -;; --- Background +(def selected-canvas + (-> (l/key :selected-canvas) + (l/derive refs/workspace))) -(mf/def background - :mixins [mf/memo] - :render - (fn [own {:keys [background] :as metadata}] - [:rect - {:x 0 :y 0 - :width "100%" - :height "100%" - :fill (or background "#ffffff")}])) - -;; --- Canvas +(defn- make-canvas-iref + [id] + (-> (l/in [:canvas id]) + (l/derive st/state))) (mf/defc canvas - [{:keys [page wst] :as props}] - (let [{:keys [metadata id]} page - zoom (:zoom wst 1) ;; NOTE: maybe forward wst to draw-area - width (:width metadata) - height (:height metadata)] - [:svg.page-canvas {:x c/canvas-start-x - :y c/canvas-start-y - :width width - :height height} - [:& background metadata] - #_[:svg.page-layout - [:g.main - (for [id (reverse (:shapes page))] - [:& uus/shape-component {:id id :key id}]) - (when (seq (:selected wst)) - [:& selection-handlers {:wst wst}]) - (when-let [dshape (:drawing wst)] - [:& draw-area {:shape dshape - :zoom (:zoom wst) - :modifiers (:modifiers wst)}])]]])) + [{:keys [id] :as props}] + (letfn [(on-double-click [event] + (dom/prevent-default event) + (st/emit! (dw/select-canvas id)))] + (let [canvas-iref (mf/use-memo #(make-canvas-iref id) #js [id]) + canvas (mf/deref canvas-iref) + selected (mf/deref selected-canvas) + selected? (= id selected)] + [:rect.page-canvas + {:x (:x canvas) + :class (when selected? "selected") + :y (:y canvas) + :fill (:background canvas "#ffffff") + :width (:width canvas) + :height (:height canvas) + :on-double-click on-double-click}]))) + + + diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index ac854aee43..518de36786 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -21,19 +21,27 @@ [uxbox.main.workers :as uwrk] [uxbox.util.math :as mth] [uxbox.util.dom :as dom] + [uxbox.util.data :refer [seek]] [uxbox.util.geom.path :as path] - [uxbox.util.geom.point :as gpt])) + [uxbox.util.geom.point :as gpt] + [uxbox.util.uuid :as uuid])) + +(defn- rxfinalize + [f ob] + (.pipe ob (.finalize js/rxjs.operators f))) ;; --- Events (declare handle-drawing) (declare handle-drawing-generic) (declare handle-drawing-path) -(declare handle-drawing-free-path) +(declare handle-drawing-curve) (declare handle-finish-drawing) +(declare conditional-align) (defn start-drawing - [object] + [type] + {:pre [(keyword? type)]} (let [id (gensym "drawing")] (reify ptk/UpdateEvent @@ -42,35 +50,65 @@ ptk/WatchEvent (watch [_ state stream] - (let [lock (get-in state [:workspace :drawing-lock])] + (let [pid (get-in state [:workspace :current]) + lock (get-in state [:workspace :drawing-lock])] (if (= lock id) - (rx/merge (->> stream - (rx/filter #(= % handle-finish-drawing)) - (rx/take 1) - (rx/map (fn [_] #(update % :workspace dissoc :drawing-lock)))) - (rx/of (handle-drawing object))) + (rx/merge + (->> (rx/filter #(= % handle-finish-drawing) stream) + (rx/take 1) + (rx/map (fn [_] #(update % :workspace dissoc :drawing-lock)))) + (rx/of (handle-drawing type))) (rx/empty))))))) -(defn- conditional-align [point align?] - (if align? - (uwrk/align-point point) - (rx/of point))) +(def ^:private minimal-shapes + [{:type :rect + :name "Rect" + :stroke-color "#000000"} + {:type :circle + :name "Circle"} + {:type :path + :name "Path" + :stroke-style :solid + :stroke-color "#000000" + :stroke-width 2 + :fill-color "#000000" + :fill-opacity 0 + :segments []} + {:type :curve + :name "Path" + :stroke-style :solid + :stroke-color "#000000" + :stroke-width 2 + :fill-color "#000000" + :fill-opacity 0 + :segments []} + {:type :text + :name "Text" + :content "Type your text here"}]) + +(defn- make-minimal-shape + [type] + (let [tool (seek #(= type (:type %)) minimal-shapes)] + (assert tool "unexpected drawing tool") + (assoc tool :id (uuid/random)))) -;; TODO: maybe this should be a simple function (defn handle-drawing - [shape] + [type] (reify + ptk/UpdateEvent + (update [_ state] + (let [pid (get-in state [:workspace :current]) + shape (make-minimal-shape type)] + (assoc-in state [:workspace pid :drawing] shape))) + ptk/WatchEvent (watch [_ state stream] - (rx/of - (if (= :path (:type shape)) - (if (:free shape) - (handle-drawing-free-path shape) - (handle-drawing-path shape)) - (handle-drawing-generic shape)))))) + (case type + :path (rx/of handle-drawing-path) + :curve (rx/of handle-drawing-curve) + (rx/of handle-drawing-generic))))) -(defn- handle-drawing-generic - [shape] +(def handle-drawing-generic (letfn [(initialize-drawing [state point] (let [pid (get-in state [:workspace :current]) shape (get-in state [:workspace pid :drawing]) @@ -114,11 +152,11 @@ stoper (->> (rx/filter #(or (uws/mouse-up? %) (= % :interrupt)) stream) (rx/take 1)) - mouse (->> uws/viewport-mouse-position + mouse (->> uws/mouse-position (rx/mapcat #(conditional-align % align?)) (rx/with-latest vector uws/mouse-position-ctrl))] (rx/concat - (->> uws/viewport-mouse-position + (->> uws/mouse-position (rx/take 1) (rx/mapcat #(conditional-align % align?)) (rx/map (fn [pt] #(initialize-drawing % pt)))) @@ -127,8 +165,7 @@ (rx/take-until stoper)) (rx/of handle-finish-drawing))))))) -(defn handle-drawing-path - [shape] +(def handle-drawing-path (letfn [(stoper-event? [{:keys [type shift] :as event}] (or (= event :interrupt) (and (uws/mouse-event? event) @@ -166,12 +203,12 @@ flags (get-in state [:workspace pid :flags]) align? (refs/alignment-activated? flags) - last-point (volatile! @uws/viewport-mouse-position) + last-point (volatile! @uws/mouse-position) stoper (->> (rx/filter stoper-event? stream) - (rx/take 1)) + (rx/share)) - mouse (->> (rx/sample 10 uws/viewport-mouse-position) + mouse (->> (rx/sample 10 uws/mouse-position) (rx/mapcat #(conditional-align % align?))) points (->> stream @@ -186,7 +223,6 @@ (rx/with-latest vector counter) (rx/map flatten)) - imm-transform #(vector (- % 7) (+ % 7) %) immanted-zones (vec (concat (map imm-transform (range 0 181 15)) @@ -205,8 +241,8 @@ (->> points (rx/take-until stoper) - (rx/map (fn [pt] - #(insert-point-segment % pt)))) + (rx/map (fn [pt]#(insert-point-segment % pt)))) + (rx/concat (->> stream' (rx/map (fn [[point ctrl? index :as xxx]] @@ -221,8 +257,7 @@ (rx/of remove-dangling-segmnet handle-finish-drawing)))))))) -(defn- handle-drawing-free-path - [shape] +(def handle-drawing-curve (letfn [(stoper-event? [{:keys [type shift] :as event}] (or (= event :interrupt) (and (uws/mouse-event? event) (= type :up)))) @@ -249,7 +284,7 @@ stoper (->> (rx/filter stoper-event? stream) (rx/take 1)) - mouse (->> (rx/sample 10 uws/viewport-mouse-position) + mouse (->> (rx/sample 10 uws/mouse-position) (rx/mapcat #(conditional-align % align?)))] (rx/concat (rx/of initialize-drawing) @@ -275,10 +310,11 @@ #(update-in % [:workspace pid :modifiers] dissoc (:id shape)) ;; Unselect the drawing tool - #(update-in % [:workspace pid] dissoc :drawing :drawing-tool) + ;; TODO; maybe a specific event for clear draw-tool + dw/clear-drawing ;; Add & select the cred shape to the workspace - (ds/add-shape shape) + (dw/add-shape (dissoc shape ::initialized?)) (dw/select-first-shape))) (rx/of #(update-in % [:workspace pid] dissoc :drawing :drawing-tool))))))) @@ -296,8 +332,8 @@ (mf/defc draw-area [{:keys [zoom shape modifiers] :as props}] - (if (= (:type shape) :path) - [:& path-draw-area {:shape shape}] + (case (:type shape) + (:path :curve) [:& path-draw-area {:shape shape}] [:& generic-draw-area {:shape (assoc shape :modifiers modifiers) :zoom zoom}])) @@ -328,7 +364,7 @@ (when-let [{:keys [x y] :as segment} (first (:segments shape))] [:g (shapes/render-shape shape) - (when-not (:free shape) + (when (not= :curve (:type shape)) [:circle.close-bezier {:cx x :cy y @@ -336,3 +372,8 @@ :on-click on-click :on-mouse-enter on-mouse-enter :on-mouse-leave on-mouse-leave}])]))) + +(defn- conditional-align [point align?] + (if align? + (uwrk/align-point point) + (rx/of point))) diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index ea2111c4ad..13ed9b9c2b 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -11,9 +11,7 @@ [beicon.core :as rx] [lentes.core :as l] [rumext.alpha :as mf] - [uxbox.main.constants :as c] - [uxbox.main.data.shapes :as uds] - [uxbox.main.data.workspace :as udw] + [uxbox.main.data.workspace :as dw] [uxbox.main.geom :as geom] [uxbox.main.refs :as refs] [uxbox.main.store :as st] @@ -42,11 +40,11 @@ (let [result (geom/resize-shape vid shape point lock?) scale (geom/calculate-scale-ratio shape result) mtx (geom/generate-resize-matrix vid shape scale) - xfm (map #(udw/apply-temporal-resize % mtx))] + xfm (map #(dw/apply-temporal-resize % mtx))] (apply st/emit! (sequence xfm ids)))) (on-end [] - (apply st/emit! (map udw/apply-resize ids))) + (apply st/emit! (map dw/apply-resize ids))) ;; Unifies the instantaneous proportion lock modifier ;; activated by Ctrl key and the shapes own proportion @@ -68,10 +66,10 @@ (let [shape (->> (geom/shape->rect-shape shape) (geom/size)) - stoper (->> ws/interaction-events + stoper (->> st/stream (rx/filter ws/mouse-up?) (rx/take 1)) - stream (->> ws/viewport-mouse-position + stream (->> ws/mouse-position (rx/take-until stoper) (rx/map apply-zoom) (rx/mapcat apply-grid-alignment) @@ -160,22 +158,23 @@ (letfn [(on-mouse-down [event index] (dom/stop-propagation event) - (let [stoper (get-edition-stream-stoper ws/interaction-events) + ;; TODO: this need code ux refactor + (let [stoper (get-edition-stream-stoper) stream (rx/take-until stoper ws/mouse-position-deltas)] (when @refs/selected-alignment - (st/emit! (uds/initial-path-point-align (:id shape) index))) + (st/emit! (dw/initial-path-point-align (:id shape) index))) (rx/subscribe stream #(on-handler-move % index)))) - (get-edition-stream-stoper [stream] + (get-edition-stream-stoper [] (let [stoper? #(and (ws/mouse-event? %) (= (:type %) :up))] (rx/merge - (rx/filter stoper? stream) - (->> stream + (rx/filter stoper? st/stream) + (->> st/stream (rx/filter #(= % :interrupt)) (rx/take 1))))) (on-handler-move [delta index] - (st/emit! (uds/update-path (:id shape) index delta)))] + (st/emit! (dw/update-path (:id shape) index delta)))] (let [displacement (:displacement modifiers) segments (cond->> (:segments shape) diff --git a/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs b/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs index 939ec92953..b8712e6df0 100644 --- a/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs +++ b/frontend/src/uxbox/main/ui/workspace/shortcuts.cljs @@ -12,10 +12,7 @@ [uxbox.main.store :as st] [uxbox.main.data.lightbox :as dl] [uxbox.main.data.workspace :as dw] - [uxbox.main.data.shapes :as uds] - [uxbox.main.data.undo :as udu] - [uxbox.main.data.history :as udh] - [uxbox.main.ui.workspace.sidebar.drawtools :as wsd]) + [uxbox.main.data.undo :as du]) (:import goog.events.EventType goog.events.KeyCodes goog.ui.KeyboardShortcutHandler @@ -27,26 +24,24 @@ (defonce +shortcuts+ {:shift+g #(st/emit! (dw/toggle-flag :grid)) - :ctrl+g #(st/emit! (uds/group-selected)) - :ctrl+shift+g #(st/emit! (uds/ungroup-selected)) :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)) :ctrl+0 #(st/emit! (dw/reset-zoom)) :ctrl+r #(st/emit! (dw/toggle-flag :ruler)) - :ctrl+d #(st/emit! (uds/duplicate-selected)) + :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! (udu/undo)) - :ctrl+shift+z #(st/emit! (udu/redo)) - :ctrl+y #(st/emit! (udu/redo)) - :ctrl+b #(st/emit! (dw/select-for-drawing wsd/+draw-tool-rect+)) - :ctrl+e #(st/emit! (dw/select-for-drawing wsd/+draw-tool-circle+)) - :ctrl+t #(st/emit! (dw/select-for-drawing wsd/+draw-tool-text+)) + :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)) :esc #(st/emit! (dw/deselect-all)) - :delete #(st/emit! (dw/delete-selected)) + :delete #(st/emit! dw/delete-selected) :ctrl+up #(st/emit! (dw/move-selected-layer :up)) :ctrl+down #(st/emit! (dw/move-selected-layer :down)) :ctrl+shift+up #(st/emit! (dw/move-selected-layer :top)) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar.cljs index 6e46c1a5a8..684e1be9b4 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar.cljs @@ -21,17 +21,16 @@ (mf/defc left-sidebar {:wrap [mf/wrap-memo]} [{:keys [flags page] :as props}] - [:aside#settings-bar.settings-bar.settings-bar-left - [:> rdnd/provider {:backend rdnd/html5} - [:div.settings-bar-inside - (when (contains? flags :sitemap) - [:& sitemap-toolbox {:project-id (:project page) - :current-page-id (:id page) - :page page}]) - #_(when (contains? flags :document-history) - (history-toolbox page-id)) - (when (contains? flags :layers) - [:& layers-toolbox {:page page}])]]]) + [:aside.settings-bar.settings-bar-left + [:div.settings-bar-inside + (when (contains? flags :sitemap) + [:& sitemap-toolbox {:project-id (:project page) + :current-page-id (:id page) + :page page}]) + #_(when (contains? flags :document-history) + (history-toolbox page-id)) + (when (contains? flags :layers) + [:& layers-toolbox {:page page}])]]) ;; --- Right Sidebar (Component) @@ -43,6 +42,5 @@ [:& draw-toolbox {:flags flags}]) (when (contains? flags :element-options) [:& options-toolbox {:page page}]) - (when (contains? flags :icons) - #_(icons-toolbox))]]) - + #_(when (contains? flags :icons) + (icons-toolbox))]]) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/drawtools.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/drawtools.cljs index 7d2be8879a..191a0d3903 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/drawtools.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/drawtools.cljs @@ -17,96 +17,69 @@ ;; --- Constants -(def +draw-tool-rect+ - {:type :rect - :id (uuid/random) - :name "Rect" - :stroke-color "#000000"}) - -(def +draw-tool-circle+ - {:type :circle - :id (uuid/random) - :name "Circle"}) - -(def +draw-tool-path+ - {:type :path - :id (uuid/random) - :name "Path" - :stroke-style :solid - :stroke-color "#000000" - :stroke-width 2 - :fill-color "#000000" - :fill-opacity 0 - ;; :close? true - :points []}) - -(def +draw-tool-curve+ - (assoc +draw-tool-path+ - :id (uuid/random) - :free true)) - -(def +draw-tool-text+ - {:type :text - :id (uuid/random) - :name "Text" - :content "Hello world"}) - (def +draw-tools+ [{:icon i/box :help "ds.help.rect" - :shape +draw-tool-rect+ + :type :rect :priority 1} {:icon i/circle :help "ds.help.circle" - :shape +draw-tool-circle+ + :type :circle :priority 2} {:icon i/text :help "ds.help.text" - :shape +draw-tool-text+ + :type :text :priority 4} {:icon i/curve :help "ds.help.path" - :shape +draw-tool-path+ + :type :path :priority 5} {:icon i/pencil :help "ds.help.curve" - :shape +draw-tool-curve+ - :priority 6}]) + :type :curve + :priority 6} + ;; TODO: we need an icon for canvas creation + {:icon i/box + :help "ds.help.canvas" + :type :canvas + :priority 7}]) ;; --- Draw Toolbox (Component) (mf/defc draw-toolbox {:wrap [mf/wrap-memo]} [{:keys [flags] :as props}] - (let [close #(st/emit! (dw/toggle-flag :drawtools)) - dtool (mf/deref refs/selected-drawing-tool) - tools (->> (into [] +draw-tools+) - (sort-by (comp :priority second))) + (letfn [(close [event] + (st/emit! (dw/deactivate-flag :drawtools))) + (select [event tool] + (st/emit! :interrupt + (dw/deactivate-ruler) + (dw/select-for-drawing tool))) + (toggle-ruler [event] + (st/emit! (dw/select-for-drawing nil) + (dw/deselect-all) + (dw/toggle-ruler)))] - select-drawtool #(st/emit! :interrupt - (dw/deactivate-ruler) - (dw/select-for-drawing %)) - toggle-ruler #(st/emit! (dw/select-for-drawing nil) - (dw/deselect-all) - (dw/toggle-ruler))] + (let [selected (mf/deref refs/selected-drawing-tool) + tools (sort-by (comp :priority second) +draw-tools+)] + [:div.tool-window.drawing-tools + [:div.tool-window-bar + [:div.tool-window-icon i/window] + [:span (tr "ds.draw-tools")] + [:div.tool-window-close {:on-click close} i/close]] + [:div.tool-window-content + (for [item tools] + (let [selected? (= (:type item) selected)] + [:div.tool-btn.tooltip.tooltip-hover + {:alt (tr (:help item)) + :class (when selected? "selected") + :key (:type item) + :on-click #(select % (:type item))} + (:icon item)])) - [:div#form-tools.tool-window.drawing-tools - [:div.tool-window-bar - [:div.tool-window-icon i/window] - [:span (tr "ds.draw-tools")] - [:div.tool-window-close {:on-click close} i/close]] - [:div.tool-window-content - (for [[i props] (map-indexed vector tools)] - (let [selected? (= dtool (:shape props))] - [:div.tool-btn.tooltip.tooltip-hover - {:alt (tr (:help props)) - :class (when selected? "selected") - :key i - :on-click (partial select-drawtool (:shape props))} - (:icon props)])) - [:div.tool-btn.tooltip.tooltip-hover - {:alt (tr "ds.help.ruler") - :on-click toggle-ruler - :class (when (contains? flags :ruler) "selected")} - i/ruler-tool]]])) + #_[:div.tool-btn.tooltip.tooltip-hover + {:alt (tr "ds.help.ruler") + :on-click toggle-ruler + :class (when (contains? flags :ruler) "selected")} + i/ruler-tool]]]))) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs index 56631246e8..049d427980 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/layers.cljs @@ -11,8 +11,7 @@ [rumext.alpha :as mf] [uxbox.builtins.icons :as i] [uxbox.main.data.pages :as udp] - [uxbox.main.data.shapes :as uds] - [uxbox.main.data.workspace :as udw] + [uxbox.main.data.workspace :as dw] [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.ui.keyboard :as kbd] @@ -44,9 +43,10 @@ on-blur (fn [event] (let [target (dom/event->target event) parent (.-parentNode target) + parent (.-parentNode parent) name (dom/get-value target)] (set! (.-draggable parent) true) - (st/emit! (uds/rename-shape (:id shape) name)) + (st/emit! (dw/rename-shape (:id shape) name)) (swap! local assoc :edition false))) on-key-down (fn [event] (js/console.log event) @@ -54,7 +54,8 @@ (on-blur event))) on-click (fn [event] (dom/prevent-default event) - (let [parent (.-parentNode (.-target event))] + (let [parent (.-parentNode (.-target event)) + parent (.-parentNode parent)] (set! (.-draggable parent) false)) (swap! local assoc :edition true))] (if (:edition @local) @@ -77,18 +78,18 @@ (let [id (:id shape) blocked? (:blocked shape)] (if blocked? - (st/emit! (uds/unblock-shape id)) - (st/emit! (uds/block-shape id))))) + (st/emit! (dw/unblock-shape id)) + (st/emit! (dw/block-shape id))))) (toggle-visibility [event] (dom/stop-propagation event) (let [id (:id shape) hidden? (:hidden shape)] (if hidden? - (st/emit! (uds/show-shape id)) - (st/emit! (uds/hide-shape id))) + (st/emit! (dw/show-shape id)) + (st/emit! (dw/hide-shape id))) (when (contains? selected id) - (st/emit! (udw/select-shape id))))) + (st/emit! (dw/select-shape id))))) (select-shape [event] (dom/prevent-default event) @@ -99,24 +100,20 @@ nil (.-ctrlKey event) - (st/emit! (udw/select-shape id)) + (st/emit! (dw/select-shape id)) (> (count selected) 1) - (st/emit! (udw/deselect-all) - (udw/select-shape id)) - - (contains? selected id) - (st/emit! (udw/select-shape id)) - + (st/emit! (dw/deselect-all) + (dw/select-shape id)) :else - (st/emit! (udw/deselect-all) - (udw/select-shape id))))) + (st/emit! (dw/deselect-all) + (dw/select-shape id))))) (on-drop [item monitor] (st/emit! (udp/persist-page (:page shape)))) (on-hover [item monitor] - (st/emit! (udw/change-shape-order {:id (:shape-id item) + (st/emit! (dw/change-shape-order {:id (:shape-id item) :index index})))] (let [selected? (contains? selected (:id shape)) [dprops dnd-ref] (use-sortable @@ -132,8 +129,7 @@ :dragging-TODO (:dragging? dprops))} [:div.element-list-body {:class (classnames :selected selected?) :on-click select-shape - :on-double-click #(dom/stop-propagation %) - :draggable true} + :on-double-click #(dom/stop-propagation %)} [:div.element-actions [:div.toggle-element {:class (when-not (:hidden shape) "selected") :on-click toggle-visibility} @@ -144,20 +140,52 @@ [:div.element-icon (element-icon shape)] [:& layer-name {:shape shape}]]]))) + +;; --- Layer Canvas + +;; (mf/defc layer-canvas +;; [{:keys [canvas selected index] :as props}] +;; (letfn [(select-shape [event] +;; (dom/prevent-default event) +;; (st/emit! (dw/select-canvas (:id canvas)))) +;; (let [selected? (contains? selected (:id shape))] +;; [:li {:class (classnames +;; :selected selected?)} +;; [:div.element-list-body {:class (classnames :selected selected?) +;; :on-click select-shape +;; :on-double-click #(dom/stop-propagation %) +;; :draggable true} +;; [:div.element-actions +;; [:div.toggle-element {:class (when-not (:hidden shape) "selected") +;; :on-click toggle-visibility} +;; i/eye] +;; [:div.block-element {:class (when (:blocked shape) "selected") +;; :on-click toggle-blocking} +;; i/lock]] +;; [:div.element-icon (element-icon shape)] +;; [:& layer-name {:shape shape}]]]))) + ;; --- Layers List (def ^:private shapes-iref (-> (l/key :shapes) (l/derive st/state))) +(def ^:private canvas-iref + (-> (l/key :canvas) + (l/derive st/state))) + (mf/defc layers-list [{:keys [shapes selected] :as props}] - (let [shapes-map (mf/deref shapes-iref)] + (let [shapes-map (mf/deref shapes-iref) + canvas-map (mf/deref canvas-iref) + selected-shapes (mf/deref refs/selected-shapes) + selected-canvas (mf/deref refs/selected-canvas)] [:div.tool-window-content [:ul.element-list (for [[index id] (map-indexed vector shapes)] [:& layer-item {:shape (get shapes-map id) - :selected selected + :selected selected-shapes :index index :key id}])]])) @@ -165,7 +193,7 @@ (mf/defc layers-toolbox [{:keys [page selected] :as props}] - (let [on-click #(st/emit! (udw/toggle-flag :layers)) + (let [on-click #(st/emit! (dw/toggle-flag :layers)) selected (mf/deref refs/selected-shapes)] [:div#layers.tool-window [:div.tool-window-bar 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 2467cf1931..d402fe3038 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 @@ -93,14 +93,14 @@ value (parse-int value 0) sid (:id shape) props {attr value}] - (st/emit! (uds/update-dimensions sid props)))) + (st/emit! (udw/update-dimensions sid props)))) (defn- on-rotation-change [event shape] (let [value (dom/event->value event) value (parse-int value 0) sid (:id shape)] - (st/emit! (uds/update-rotation sid value)))) + (st/emit! (udw/update-shape-attrs sid {:rotation value})))) (defn- on-position-change [event shape attr] @@ -108,11 +108,11 @@ value (parse-int value nil) sid (:id shape) point (gpt/point {attr value})] - (st/emit! (uds/update-position sid point)))) + (st/emit! (udw/update-position sid point)))) (defn- on-proportion-lock-change [event shape] (if (:proportion-lock shape) - (st/emit! (uds/unlock-proportions (:id shape))) - (st/emit! (uds/lock-proportions (:id shape))))) + (st/emit! (udw/unlock-proportions (:id shape))) + (st/emit! (udw/lock-proportions (:id shape))))) 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 7e7306f727..6b406b804a 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 @@ -90,14 +90,13 @@ value (parse-int value 0) sid (:id shape) props {attr value}] - (st/emit! (uds/update-dimensions sid props)))) + (st/emit! (udw/update-dimensions sid props)))) (defn- on-rotation-change [event shape] (let [value (dom/event->value event) - value (parse-int value 0) - sid (:id shape)] - (st/emit! (uds/update-rotation sid value)))) + value (parse-int value 0)] + (st/emit! (udw/update-shape-attrs (:id shape) {:rotation value})))) (defn- on-position-change [event shape attr] @@ -105,11 +104,11 @@ value (parse-int value nil) sid (:id shape) point (gpt/point {attr value})] - (st/emit! (uds/update-position sid point)))) + (st/emit! (udw/update-position sid point)))) (defn- on-proportion-lock-change [event shape] (if (:proportion-lock shape) - (st/emit! (uds/unlock-proportions (:id shape))) - (st/emit! (uds/lock-proportions (:id shape))))) + (st/emit! (udw/unlock-proportions (:id shape))) + (st/emit! (udw/lock-proportions (:id shape))))) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/image_measures.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/image_measures.cljs index 6116b18d56..9635718407 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/image_measures.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/image_measures.cljs @@ -9,8 +9,7 @@ (:require [rumext.alpha :as mf] [uxbox.builtins.icons :as i] - [uxbox.main.data.shapes :as uds] - [uxbox.main.data.workspace :as udw] + [uxbox.main.data.workspace :as dw] [uxbox.main.geom :as geom] [uxbox.main.store :as st] [uxbox.util.data :refer (parse-int parse-float read-string)] @@ -106,30 +105,30 @@ (let [value (dom/event->value event) value (parse-int value 0) props {attr value}] - (st/emit! (uds/update-dimensions (:id shape) props)))) + (st/emit! (dw/update-dimensions (:id shape) props)))) (defn- on-rotation-change [event shape] (let [value (dom/event->value event) value (parse-int value 0)] - (st/emit! (uds/update-rotation (:id shape) value)))) + (st/emit! (dw/update-shape-attrs (:id shape) {:rotation value})))) (defn- on-opacity-change [event shape] (let [value (dom/event->value event) value (parse-float value 1) value (/ value 10000)] - (st/emit! (uds/update-attrs (:id shape) {:opacity value})))) + (st/emit! (dw/update-shape-attrs (:id shape) {:opacity value})))) (defn- on-position-change [event shape attr] (let [value (dom/event->value event) value (parse-int value nil) point (gpt/point {attr value})] - (st/emit! (uds/update-position (:id shape) point)))) + (st/emit! (dw/update-position (:id shape) point)))) (defn- on-proportion-lock-change [event shape] (if (:proportion-lock shape) - (st/emit! (uds/unlock-proportions (:id shape))) - (st/emit! (uds/lock-proportions (:id shape))))) + (st/emit! (dw/unlock-proportions (:id shape))) + (st/emit! (dw/lock-proportions (:id shape))))) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs index 6742565049..3ff8dc675c 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/interactions.cljs @@ -10,7 +10,7 @@ [rumext.alpha :as mf] [uxbox.builtins.icons :as i] [uxbox.main.data.lightbox :as udl] - [uxbox.main.data.shapes :as uds] + [uxbox.main.data.workspace :as dw] [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.ui.colorpicker :as cp] @@ -59,7 +59,7 @@ (delete [item] (let [sid (:id shape) id (:id item)] - (st/emit! (uds/delete-interaction sid id)))) + (st/emit! (dw/delete-interaction sid id)))) (on-delete [item event] (dom/prevent-default event) (let [delete (partial delete item)] @@ -455,7 +455,7 @@ (dom/prevent-default event) (let [sid (:id shape) data (deref form)] - (st/emit! (uds/update-interaction sid data)) + (st/emit! (dw/update-interaction sid data)) (reset! form nil))) (on-cancel [event] (dom/prevent-default event) 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 711e11b8ae..f8c47f3c03 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 @@ -87,23 +87,23 @@ [event shape attr] (let [value (-> (dom/event->value event) (parse-int 0))] - (st/emit! (uds/update-dimensions (:id shape) {attr value})))) + (st/emit! (udw/update-dimensions (:id shape) {attr value})))) (defn- on-rotation-change [event shape] - (let [value (-> (dom/event->value event) - (parse-int 0))] - (st/emit! (uds/update-rotation (:id shape) value)))) + (let [value (dom/event->value event) + value (parse-int value 0)] + (st/emit! (udw/update-shape-attrs (:id shape) {:rotation value})))) (defn- on-position-change [event shape attr] (let [value (-> (dom/event->value event) (parse-int nil)) point (gpt/point {attr value})] - (st/emit! (uds/update-position (:id shape) point)))) + (st/emit! (udw/update-position (:id shape) point)))) (defn- on-proportion-lock-change [event shape] (if (:proportion-lock shape) - (st/emit! (uds/unlock-proportions (:id shape))) - (st/emit! (uds/lock-proportions (:id shape))))) + (st/emit! (udw/unlock-proportions (:id shape))) + (st/emit! (udw/lock-proportions (:id shape))))) 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 dfe0ca2838..962b96f56b 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/text.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/text.cljs @@ -30,7 +30,7 @@ {:mixins [mx/static]} [menu {:keys [id] :as shape}] (letfn [(update-attrs [attrs] - (st/emit! (uds/update-attrs id attrs))) + (st/emit! (udw/update-shape-attrs id attrs))) (on-font-family-change [event] (let [value (dom/event->value event) attrs {:font-family (read-string value) diff --git a/frontend/src/uxbox/main/ui/workspace/streams.cljs b/frontend/src/uxbox/main/ui/workspace/streams.cljs index 9654c00e37..88bcb25226 100644 --- a/frontend/src/uxbox/main/ui/workspace/streams.cljs +++ b/frontend/src/uxbox/main/ui/workspace/streams.cljs @@ -52,21 +52,7 @@ (and (mouse-event? v) (= :click (:type v)))) -(defrecord PointerEvent [window - viewport - ctrl - shift]) - -(defn pointer-event - [window viewport ctrl shift] - {:pre [(gpt/point? window) - (gpt/point? viewport) - (boolean? ctrl) - (boolean? shift)]} - (PointerEvent. window - viewport - ctrl - shift)) +(defrecord PointerEvent [source pt ctrl shift]) (defn pointer-event? [v] @@ -90,23 +76,14 @@ ;; --- Derived streams -;; TODO: this shoul be DEPRECATED -(defonce interaction-events - (rx/filter interaction-event? st/stream)) - (defonce mouse-position - (rx/filter pointer-event? st/stream)) - -(defonce viewport-mouse-position - (let [sub (rx/behavior-subject nil)] - (-> (rx/map :viewport mouse-position) - (rx/subscribe-with sub)) - sub)) - -(defonce window-mouse-position - (let [sub (rx/behavior-subject nil)] - (-> (rx/map :window mouse-position) - (rx/subscribe-with sub)) + (let [sub (rx/behavior-subject nil) + ob (->> st/stream + (rx/filter pointer-event?) + (rx/filter #(= :viewport (:source %))) + (rx/map :pt) + )] + (rx/subscribe-with ob sub) sub)) (defonce mouse-position-ctrl @@ -116,7 +93,7 @@ sub)) (defonce mouse-position-deltas - (->> viewport-mouse-position + (->> mouse-position (rx/sample 10) (rx/map #(gpt/divide % @refs/selected-zoom)) (rx/mapcat (fn [point] diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index 299b5c4ac3..b0d6239993 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -37,7 +37,7 @@ (mf/defc coordinates [{:keys [zoom] :as props}] - (let [coords (some-> (use-rxsub uws/viewport-mouse-position) + (let [coords (some-> (use-rxsub uws/mouse-position) (gpt/divide zoom) (gpt/round 0))] [:ul.coordinates @@ -60,16 +60,16 @@ :circle "Drag to draw a Circle" nil)) -(mf/defc cursor-tooltip - {:wrap [mf/wrap-memo]} - [{:keys [tooltip]}] - (let [coords (mf/deref refs/window-mouse-position)] - [:span.cursor-tooltip - {:style - {:position "fixed" - :left (str (+ (:x coords) 5) "px") - :top (str (- (:y coords) 25) "px")}} - tooltip])) +;; (mf/defc cursor-tooltip +;; {:wrap [mf/wrap-memo]} +;; [{:keys [tooltip]}] +;; (let [coords (mf/deref refs/window-mouse-position)] +;; [:span.cursor-tooltip +;; {:style +;; {:position "fixed" +;; :left (str (+ (:x coords) 5) "px") +;; :top (str (- (:y coords) 25) "px")}} +;; tooltip])) ;; --- Selection Rect @@ -89,15 +89,13 @@ (reify ptk/WatchEvent (watch [_ state stream] - (let [stoper (->> (rx/merge (rx/filter #(= % :interrupt) stream) - (rx/filter uws/mouse-up? stream)) - (rx/take 1))] + (let [stoper (rx/filter #(or (dw/interrupt? %) (uws/mouse-up? %)) stream)] (rx/concat - (->> uws/viewport-mouse-position + (rx/of (dw/deselect-all)) + (->> uws/mouse-position (rx/map (fn [pos] #(update-state % pos))) (rx/take-until stoper)) - (rx/of (dw/deselect-all) - dw/select-shapes-by-current-selrect + (rx/of dw/select-shapes-by-current-selrect clear-state))))))) (mf/defc selrect @@ -115,33 +113,187 @@ ;; --- Viewport Positioning (def handle-viewport-positioning - (reify - ptk/WatchEvent - (watch [_ state stream] - (let [stoper (->> (rx/filter #(= ::finish-positioning %) stream) - (rx/take 1)) - reference @uws/viewport-mouse-position - dom (dom/get-element "workspace-viewport")] - (->> uws/viewport-mouse-position - (rx/map (fn [point] - (let [{:keys [x y]} (gpt/subtract point reference) - cx (.-scrollLeft dom) - cy (.-scrollTop dom)] - (set! (.-scrollLeft dom) (- cx x)) - (set! (.-scrollTop dom) (- cy y))))) - (rx/take-until stoper) - (rx/ignore)))))) + (letfn [(on-point [dom reference point] + (let [{:keys [x y]} (gpt/subtract point reference) + cx (.-scrollLeft dom) + cy (.-scrollTop dom)] + (set! (.-scrollLeft dom) (- cx x)) + (set! (.-scrollTop dom) (- cy y))))] + (reify + ptk/EffectEvent + (effect [_ state stream] + (let [stoper (rx/filter #(= ::finish-positioning %) stream) + reference @uws/mouse-position + dom (dom/get-element "workspace-viewport")] + (-> (rx/take-until stoper uws/mouse-position) + (rx/subscribe #(on-point dom reference %)))))))) ;; --- Viewport -(mf/def viewport +(mf/defc viewport + [{:keys [page] :as props}] + (let [{:keys [drawing-tool tooltip zoom flags edition] :as wst} (mf/deref refs/workspace) + 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) + (let [ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + opts {:shift? shift? + :ctrl? ctrl?}] + (st/emit! (uws/mouse-event :down ctrl? shift?))) + (when (not edition) + (if drawing-tool + (st/emit! (start-drawing drawing-tool)) + (st/emit! :interrupt handle-selrect)))) + + (on-context-menu [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (let [ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + opts {:shift? shift? + :ctrl? ctrl?}] + (st/emit! (uws/mouse-event :context-menu ctrl? shift?)))) + + (on-mouse-up [event] + (dom/stop-propagation event) + (let [ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + opts {:shift? shift? + :ctrl? ctrl?}] + (st/emit! (uws/mouse-event :up ctrl? shift?)))) + + (on-click [event] + (dom/stop-propagation event) + (let [ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + opts {:shift? shift? + :ctrl? ctrl?}] + (st/emit! (uws/mouse-event :click ctrl? shift?)))) + + (on-double-click [event] + (dom/stop-propagation event) + (let [ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + opts {:shift? shift? + :ctrl? ctrl?}] + (st/emit! (uws/mouse-event :double-click ctrl? shift?)))) + + (translate-point-to-viewport [pt] + (let [viewport (mf/ref-node viewport-ref) + brect (.getBoundingClientRect viewport) + brect (gpt/point (parse-int (.-left brect)) + (parse-int (.-top brect)))] + (gpt/subtract pt brect))) + + (on-key-down [event] + (let [bevent (.getBrowserEvent event) + key (.-keyCode event) + ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + opts {:key key + :shift? shift? + :ctrl? ctrl?}] + (when-not (.-repeat bevent) + (st/emit! (uws/keyboard-event :down key ctrl? shift?)) + (when (kbd/space? event) + (st/emit! handle-viewport-positioning) + #_(st/emit! (dw/start-viewport-positioning)))))) + + (on-key-up [event] + (let [key (.-keyCode event) + ctrl? (kbd/ctrl? event) + shift? (kbd/shift? event) + opts {:key key + :shift? shift? + :ctrl? ctrl?}] + (when (kbd/space? event) + (st/emit! ::finish-positioning #_(dw/stop-viewport-positioning))) + (st/emit! (uws/keyboard-event :up key ctrl? shift?)))) + + (on-mouse-move [event] + (let [pt (gpt/point (.-clientX event) + (.-clientY event)) + pt (translate-point-to-viewport pt)] + ;; (prn "viewport:on-mouse-move" pt) + (st/emit! (uws/->PointerEvent :viewport pt (kbd/ctrl? event) (kbd/shift? event))))) + + + ;; ;; ctrl? (kbd/ctrl? event) + ;; ;; shift? (kbd/shift? event) + ;; ;; event {:ctrl ctrl? + ;; ;; :shift shift? + ;; ;; :window-coords wpt + ;; ;; :viewport-coords vpt} + ;; ] + ;; #_(st/emit! (uws/pointer-event wpt vpt ctrl? shift?)))) + + (on-mount [] + (prn "viewport.on-mount" (:id page)) + (let [ + ;; key1 (events/listen js/document EventType.MOUSEMOVE on-mousemove) + key2 (events/listen js/document EventType.KEYDOWN on-key-down) + key3 (events/listen js/document EventType.KEYUP on-key-up)] + (fn [] + ;; (events/unlistenByKey key1) + (events/unlistenByKey key2) + (events/unlistenByKey key3))))] + + (mf/use-effect on-mount) + ;; (prn "viewport.render" (:id page)) + + [:* + [:& 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 + :class (when drawing-tool "drawing") + :on-context-menu on-context-menu + :on-click on-click + :on-double-click on-double-click + :on-mouse-move on-mouse-move + :on-mouse-down on-mouse-down + :on-mouse-up on-mouse-up} + [:g.zoom {:transform (str "scale(" zoom ", " zoom ")")} + (when page + [:* + (for [id (:canvas page)] + [:& canvas {:key id :id id}]) + + (for [id (reverse (:shapes page))] + [:& uus/shape-component {:id id :key id}]) + + (when (seq (:selected wst)) + [:& selection-handlers {:wst wst}]) + + (when-let [dshape (:drawing wst)] + [:& draw-area {:shape dshape + :zoom (:zoom wst) + :modifiers (:modifiers wst)}])]) + + + + (if (contains? flags :grid) + [:& grid {:page page}])] + (when (contains? flags :ruler) + [:& ruler {:zoom zoom :ruler (:ruler wst)}]) + [:& selrect {:data (:selrect wst)}]]]))) + + +#_(mf/def viewport :init (fn [own props] (assoc own ::viewport (mf/create-ref))) :did-mount (fn [own] - (letfn [(translate-point-to-viewport [pt] + (letfn [ + (translate-point-to-viewport [pt] (let [viewport (mf/ref-node (::viewport own)) brect (.getBoundingClientRect viewport) brect (gpt/point (parse-int (.-left brect)) diff --git a/frontend/src/uxbox/util/data.cljs b/frontend/src/uxbox/util/data.cljs index b1b3367a18..03e6f29a51 100644 --- a/frontend/src/uxbox/util/data.cljs +++ b/frontend/src/uxbox/util/data.cljs @@ -17,11 +17,11 @@ "Return a indexed map of the collection keyed by the result of executing the getter over each element of the collection." - [coll getter] + [getter coll] (persistent! (reduce #(assoc! %1 (getter %2) %2) (transient {}) coll))) -(def index-by-id #(index-by % :id)) +(def index-by-id #(index-by :id %)) (defn remove-nil-vals "Given a map, return a map removing key-value From d9abe2f475ff4bbce886a07930e5b7c09a98a8c6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 Aug 2019 19:59:44 +0200 Subject: [PATCH 11/46] :sparkles: Improve fixtures initial data structure on fixtures. --- backend/src/uxbox/fixtures.clj | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/backend/src/uxbox/fixtures.clj b/backend/src/uxbox/fixtures.clj index b8d5eaf472..a11764a14c 100644 --- a/backend/src/uxbox/fixtures.clj +++ b/backend/src/uxbox/fixtures.clj @@ -59,16 +59,17 @@ :user (mk-uuid "user" ui) :project (mk-uuid "project" pi) :data {:canvas [{:id (mk-uuid "canvas" i 1) - :x 200 - :y 200 - :width 1024 - :height 768} + :name "Canvas 1" + :x1 200 + :y1 200 + :x2 1224 + :y2 968} {:id (mk-uuid "canvas" i 2) - :x 1324 - :y 200 - :width 1024 - :height 768} - ] + :name "Canvas 2" + :x1 1324 + :y1 200 + :x2 2348 + :y2 968}] :shapes [] :shapes-map {}} :metadata {:width 1024 From 4954bfdf7674c119ebc956aaf1d155ee1c202d88 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 Aug 2019 20:00:33 +0200 Subject: [PATCH 12/46] :recycle: Refactor loader component. --- frontend/src/uxbox/main.cljs | 2 +- frontend/src/uxbox/main/ui/loader.cljs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs index d43526c91a..54b6886511 100644 --- a/frontend/src/uxbox/main.cljs +++ b/frontend/src/uxbox/main.cljs @@ -72,7 +72,7 @@ (mf/mount (mf/element ui/app) (dom/get-element "app")) (mf/mount (lightbox) (dom/get-element "lightbox")) (mf/mount (mf/element modal) (dom/get-element "modal")) - (mf/mount (loader) (dom/get-element "loader")) + (mf/mount (mf/element loader) (dom/get-element "loader")) (on-navigate router cpath))) diff --git a/frontend/src/uxbox/main/ui/loader.cljs b/frontend/src/uxbox/main/ui/loader.cljs index 984004ca73..0a7c9a0319 100644 --- a/frontend/src/uxbox/main/ui/loader.cljs +++ b/frontend/src/uxbox/main/ui/loader.cljs @@ -2,17 +2,17 @@ ;; 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-2017 Andrey Antukh +;; Copyright (c) 2016-2019 Andrey Antukh (ns uxbox.main.ui.loader - (:require [uxbox.main.store :as st] - [uxbox.builtins.icons :as i] - [rumext.core :as mx :include-macros true])) + (:require + [rumext.alpha :as mf] + [uxbox.builtins.icons :as i] + [uxbox.main.store :as st])) ;; --- Component -(mx/defc loader - {:mixins [mx/reactive mx/static]} +(mf/defc loader [] - (when (mx/react st/loader) + (when (mf/deref st/loader) [:div.loader-content i/loader])) From 1fa9faa3144377253ee4ad8db78c58aed97cedf6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 Aug 2019 20:01:05 +0200 Subject: [PATCH 13/46] :lipstick: Cosmetic changes on app component. --- frontend/src/uxbox/main/ui.cljs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs index b24be3d90a..1d23450658 100644 --- a/frontend/src/uxbox/main/ui.cljs +++ b/frontend/src/uxbox/main/ui.cljs @@ -89,9 +89,8 @@ (mf/defc app [props] - (let [route (mf/deref route-iref) - route-id (get-in route [:data :name])] - (case route-id + (let [route (mf/deref route-iref)] + (case (get-in route [:data :name]) :auth/login (mf/element auth/login-page) :auth/register (auth/register-page) :auth/recovery-request (auth/recovery-request-page) From 6483800e499ca49975b25a5ab521d49a6bac84a5 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 Aug 2019 20:02:14 +0200 Subject: [PATCH 14/46] :sparkles: Add more incremental improvements to shapes rendering. That helps for make a good foundation for proper canvas painting and manipulation. --- frontend/src/uxbox/main/data/workspace.cljs | 105 +++++------- frontend/src/uxbox/main/geom.cljs | 7 +- frontend/src/uxbox/main/ui/shapes/circle.cljs | 26 +-- frontend/src/uxbox/main/ui/shapes/common.cljs | 24 ++- frontend/src/uxbox/main/ui/shapes/icon.cljs | 32 ++-- frontend/src/uxbox/main/ui/shapes/image.cljs | 21 +-- frontend/src/uxbox/main/ui/shapes/rect.cljs | 31 ++-- .../src/uxbox/main/ui/workspace/canvas.cljs | 29 ++-- .../src/uxbox/main/ui/workspace/drawarea.cljs | 25 +-- .../src/uxbox/main/ui/workspace/images.cljs | 12 +- .../uxbox/main/ui/workspace/selection.cljs | 4 +- .../src/uxbox/main/ui/workspace/streams.cljs | 12 +- .../src/uxbox/main/ui/workspace/viewport.cljs | 153 ------------------ .../tests/test_main_data_shapes_impl.cljs | 2 +- 14 files changed, 144 insertions(+), 339 deletions(-) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 785e3af816..17a3f00b06 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -479,9 +479,11 @@ :fast (gpt/point (if align? (* 3 gx) 10) (if align? (* 3 gy) 10))})) -(declare apply-temporal-displacement) (declare initial-shape-align) (declare apply-displacement) +(declare assoc-temporal-modifier) +(declare materialize-current-modifier) +(declare apply-temporal-displacement) (defrecord MoveSelected [direction speed] ptk/WatchEvent @@ -500,7 +502,7 @@ (rx/from-coll (map initial-shape-align selected)) (rx/from-coll (map apply-displacement selected)))) (rx/from-coll (map #(apply-temporal-displacement % displacement) selected)) - (rx/from-coll (map apply-displacement selected)))))) + (rx/from-coll (map materialize-current-modifier selected)))))) (s/def ::direction #{:up :down :right :left}) (s/def ::speed #{:std :fast}) @@ -597,69 +599,38 @@ ;; --- Apply Temporal Displacement -(defrecord ApplyTemporalDisplacement [id delta] - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :current]) - prev (get-in state [:workspace pid :modifiers id :displacement] (gmt/matrix)) - curr (gmt/translate prev delta)] - (assoc-in state [:workspace pid :modifiers id :displacement] curr)))) - (defn apply-temporal-displacement - [id pt] - {:pre [(uuid? id) (gpt/point? pt)]} - (ApplyTemporalDisplacement. id pt)) + [id delta] + {:pre [(uuid? id) (gpt/point? delta)]} + (reify + ptk/WatchEvent + (watch [_ state stream] + (let [prev (get-in state [:shapes id :modifier-mtx] (gmt/matrix)) + curr (gmt/translate prev delta)] + (rx/of (assoc-temporal-modifier id curr)))))) -;; --- Apply Displacement +;; --- Modifiers -(defrecord ApplyDisplacement [id] - ptk/WatchEvent - (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - displacement (get-in state [:workspace pid :modifiers id :displacement])] - (if (gmt/matrix? displacement) - (rx/of #(ds/materialize-xfmt % id displacement) - #(update-in % [:workspace pid :modifiers id] dissoc :displacement) - ::udp/page-update) - (rx/empty))))) - -(defn apply-displacement - [id] - {:pre [(uuid? id)]} - (ApplyDisplacement. id)) - -;; --- Apply Temporal Resize Matrix - -(deftype ApplyTemporalResize [sid xfmt] - ptk/UpdateEvent - (update [_ state] - (let [pid (get-in state [:workspace :current])] - (assoc-in state [:workspace pid :modifiers sid :resize] xfmt)))) - -(defn apply-temporal-resize - "Attach temporal resize transformation to the shape." +(defn assoc-temporal-modifier [id xfmt] - {:pre [(gmt/matrix? xfmt) (uuid? id)]} - (ApplyTemporalResize. id xfmt)) + {:pre [(uuid? id) + (gmt/matrix? xfmt)]} + (reify + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:shapes id :modifier-mtx] xfmt)))) -;; --- Apply Resize Matrix - -(deftype ApplyResize [id] - ptk/WatchEvent - (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - resize (get-in state [:workspace pid :modifiers id :resize])] - (if (gmt/matrix? resize) - (rx/of #(ds/materialize-xfmt % id resize) - #(update-in % [:workspace pid :modifiers id] dissoc :resize) - ::udp/page-update) - (rx/empty))))) - -(defn apply-resize - "Apply definitivelly the resize matrix transformation to the shape." +(defn materialize-current-modifier [id] {:pre [(uuid? id)]} - (ApplyResize. id)) + (reify + ptk/WatchEvent + (watch [_ state stream] + (let [xfmt (get-in state [:shapes id :modifier-mtx])] + (when (gmt/matrix? xfmt) + (rx/of #(update-in % [:shapes id] geom/transform xfmt) + #(update-in % [:shapes id] dissoc :modifier-mtx) + ::udp/page-update)))))) ;; --- Start shape "edition mode" @@ -694,16 +665,16 @@ (= (::type (meta e)) ::select-for-drawing)) (defn select-for-drawing - [tool] - (reify - IMeta - (-meta [_] {::type ::select-for-drawing}) + ([tool] (select-for-drawing tool nil)) + ([tool data] + (reify + IMeta + (-meta [_] {::type ::select-for-drawing}) - ptk/UpdateEvent - (update [_ state] - (prn "select-for-drawing" tool) - (let [pid (get-in state [:workspace :current])] - (update-in state [:workspace pid] assoc :drawing-tool tool))))) + ptk/UpdateEvent + (update [_ state] + (let [pid (get-in state [:workspace :current])] + (update-in state [:workspace pid] assoc :drawing-tool tool :drawing data)))))) ;; --- Shape Proportions diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index c19f03dc10..aa6843a8f9 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -127,12 +127,9 @@ [shape] (case (:type shape) :circle (size-circle shape) - :text (size-rect shape) - :rect (size-rect shape) - :icon (size-rect shape) - :image (size-rect shape) :curve (size-path shape) - :path (size-path shape))) + :path (size-path shape) + (size-rect shape))) (defn- size-path [{:keys [segments x1 y1 x2 y2] :as shape}] diff --git a/frontend/src/uxbox/main/ui/shapes/circle.cljs b/frontend/src/uxbox/main/ui/shapes/circle.cljs index fc50940e36..8d05facc07 100644 --- a/frontend/src/uxbox/main/ui/shapes/circle.cljs +++ b/frontend/src/uxbox/main/ui/shapes/circle.cljs @@ -21,37 +21,37 @@ (mf/defc circle-component [{:keys [shape] :as props}] - (let [modifiers (mf/deref (refs/selected-modifiers (:id shape))) - selected (mf/deref refs/selected-shapes) + (let [selected (mf/deref refs/selected-shapes) selected? (contains? selected (:id shape)) on-mouse-down #(common/on-mouse-down % shape selected)] [:g.shape {:class (when selected? "selected") :on-mouse-down on-mouse-down} - [:& circle-shape {:shape shape :modifiers modifiers}]])) + [:& circle-shape {:shape shape}]])) ;; --- Circle Shape (mf/defc circle-shape - [{:keys [shape modifiers] :as props}] - (let [{:keys [id rotation cx cy]} shape - {:keys [resize displacement]} modifiers + [{:keys [shape] :as props}] + (let [{:keys [id rotation cx cy modifier-mtx]} shape - shape (cond-> shape - displacement (geom/transform displacement) - resize (geom/transform resize)) + shape (cond + (gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx) + :else shape) center (gpt/point (:cx shape) (:cy shape)) + rotation (or rotation 0) - moving? (boolean displacement) + moving? (boolean modifier-mtx) - xfmt (-> (gmt/matrix) - (gmt/rotate* rotation center)) + transform (when (pos? rotation) + (str (-> (gmt/matrix) + (gmt/rotate* rotation center)))) props {:id (str "shape-" id) :class (classnames :move-cursor moving?) - :transform (str xfmt)} + :transform transform} attrs (merge props (attrs/extract-style-attrs shape) diff --git a/frontend/src/uxbox/main/ui/shapes/common.cljs b/frontend/src/uxbox/main/ui/shapes/common.cljs index 3c7b447c25..38c6e0a192 100644 --- a/frontend/src/uxbox/main/ui/shapes/common.cljs +++ b/frontend/src/uxbox/main/ui/shapes/common.cljs @@ -12,7 +12,8 @@ [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.ui.keyboard :as kbd] - [uxbox.main.ui.workspace.streams :as ws] + [uxbox.main.ui.workspace.streams :as uws] + [uxbox.util.geom.matrix :as gmt] [uxbox.util.dom :as dom])) ;; --- Shape Movement (by mouse) @@ -24,17 +25,15 @@ ptk/WatchEvent (watch [_ state stream] (let [pid (get-in state [:workspace :current]) - wst (get-in state [:workspace pid]) - stoper (->> stream - (rx/filter ws/mouse-up?) - (rx/take 1)) - stream (->> ws/mouse-position-deltas - (rx/take-until stoper))] - (rx/concat - (when (refs/alignment-activated? (:flags wst)) - (rx/of (dw/initial-shape-align id))) - (rx/map #(dw/apply-temporal-displacement id %) stream) - (rx/of (dw/apply-displacement id))))))) + flags (get-in state [:workspace pid :flags]) + stoper (rx/filter uws/mouse-up? stream)] + (rx/concat + (when (refs/alignment-activated? flags) + (rx/of (dw/initial-shape-align id))) + (->> uws/mouse-position-deltas + (rx/map #(dw/apply-temporal-displacement id %)) + (rx/take-until stoper)) + (rx/of (dw/materialize-current-modifier id))))))) (defn start-move-selected [] @@ -45,7 +44,6 @@ selected (get-in state [:workspace pid :selected])] (rx/from-coll (map start-move selected)))))) - (defn on-mouse-down [event {:keys [id] :as shape} selected] (let [selected? (contains? selected id) diff --git a/frontend/src/uxbox/main/ui/shapes/icon.cljs b/frontend/src/uxbox/main/ui/shapes/icon.cljs index 85d36efbf3..0eb8498dc0 100644 --- a/frontend/src/uxbox/main/ui/shapes/icon.cljs +++ b/frontend/src/uxbox/main/ui/shapes/icon.cljs @@ -23,14 +23,12 @@ (mf/defc icon-component [{:keys [shape] :as props}] - (let [id (:id shape) - modifiers (mf/deref (refs/selected-modifiers id)) - selected (mf/deref refs/selected-shapes) - selected? (contains? selected id) + (let [selected (mf/deref refs/selected-shapes) + selected? (contains? selected (:id shape)) on-mouse-down #(common/on-mouse-down % shape selected)] [:g.shape {:class (when selected? "selected") :on-mouse-down on-mouse-down} - [:& icon-shape {:shape shape :modifiers modifiers}]])) + [:& icon-shape {:shape shape}]])) ;; --- Icon Shape @@ -43,22 +41,20 @@ (gmt/rotate* mt rotation center))) (mf/defc icon-shape - [{:keys [shape modifiers] :as props}] - (let [{:keys [id content metadata rotation x1 y1]} shape - {:keys [resize displacement]} modifiers + [{:keys [shape] :as props}] + (let [{:keys [id content metadata rotation modifier-mtx]} shape - xfmt (cond-> (gmt/matrix) - displacement (gmt/multiply displacement) - resize (gmt/multiply resize)) + shape (cond + (gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx) + :else shape) - {:keys [x1 y1 width height] :as shape} (-> (geom/transform shape xfmt) - (geom/size)) + {:keys [x1 y1 width height] :as shape} (geom/size shape) + + transform (when (pos? rotation) + (str (rotate (gmt/matrix) shape))) view-box (apply str (interpose " " (:view-box metadata))) - xfmt (cond-> (gmt/matrix) - (pos? rotation) (rotate shape)) - - moving? (boolean displacement) + moving? (boolean modifier-mtx) props {:id (str id) :x x1 :y y1 @@ -70,7 +66,7 @@ :dangerouslySetInnerHTML #js {:__html content}} attrs (merge props (attrs/extract-style-attrs shape))] - [:g {:transform (str xfmt)} + [:g {:transform transform} [:> :svg (normalize-props attrs) ]])) ;; --- Icon SVG diff --git a/frontend/src/uxbox/main/ui/shapes/image.cljs b/frontend/src/uxbox/main/ui/shapes/image.cljs index 9e989e37df..c4d308b00a 100644 --- a/frontend/src/uxbox/main/ui/shapes/image.cljs +++ b/frontend/src/uxbox/main/ui/shapes/image.cljs @@ -30,8 +30,7 @@ (mf/defc image-component [{:keys [shape] :as props}] - (let [modifiers (mf/deref (refs/selected-modifiers (:id shape))) - selected (mf/deref refs/selected-shapes) + (let [selected (mf/deref refs/selected-shapes) image (mf/deref (image-ref (:image shape))) selected? (contains? selected (:id shape)) on-mouse-down #(common/on-mouse-down % shape selected)] @@ -42,27 +41,23 @@ [:g.shape {:class (when selected? "selected") :on-mouse-down on-mouse-down} [:& image-shape {:shape shape - :image image - :modifiers modifiers}]]))) + :image image}]]))) ;; --- Image Shape (mf/defc image-shape - [{:keys [shape image modifiers] :as props}] - (let [{:keys [id x1 y1 width height]} (geom/size shape) - {:keys [resize displacement]} modifiers + [{:keys [shape image] :as props}] + (let [{:keys [id x1 y1 width height modifier-mtx]} (geom/size shape) + moving? (boolean modifier-mtx) + transform (when (gmt/matrix? modifier-mtx) + (str modifier-mtx)) - xfmt (cond-> (gmt/matrix) - resize (gmt/multiply resize) - displacement (gmt/multiply displacement)) - - moving? (boolean displacement) props {:x x1 :y y1 :id (str "shape-" id) :preserve-aspect-ratio "none" :class (classnames :move-cursor moving?) :xlink-href (:url image) - :transform (str xfmt) + :transform transform :width width :height height} attrs (merge props (attrs/extract-style-attrs shape))] diff --git a/frontend/src/uxbox/main/ui/shapes/rect.cljs b/frontend/src/uxbox/main/ui/shapes/rect.cljs index b4b5d9a984..891645099e 100644 --- a/frontend/src/uxbox/main/ui/shapes/rect.cljs +++ b/frontend/src/uxbox/main/ui/shapes/rect.cljs @@ -22,16 +22,13 @@ (mf/defc rect-component [{:keys [shape] :as props}] - (let [id (:id shape) - modifiers (mf/deref (refs/selected-modifiers id)) - selected (mf/deref refs/selected-shapes) - selected? (contains? selected id) + (let [selected (mf/deref refs/selected-shapes) + selected? (contains? selected (:id shape)) on-mouse-down #(common/on-mouse-down % shape selected)] ;; shape (assoc shape :modifiers modifiers)] [:g.shape {:class (when selected? "selected") :on-mouse-down on-mouse-down} - [:& rect-shape {:shape shape - :modifiers modifiers}]])) + [:& rect-shape {:shape shape}]])) ;; --- Rect Shape @@ -43,27 +40,25 @@ (gmt/rotate* mt rotation center))) (mf/defc rect-shape - [{:keys [shape modifiers] :as props}] - (let [{:keys [id rotation]} shape - {:keys [displacement resize]} modifiers + [{:keys [shape] :as props}] + (let [{:keys [id rotation modifier-mtx]} shape - xfmt (cond-> (gmt/matrix) - displacement (gmt/multiply displacement) - resize (gmt/multiply resize)) + shape (cond + (gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx) + :else shape) - {:keys [x1 y1 width height] :as shape} (-> (geom/transform shape xfmt) - (geom/size)) + {:keys [x1 y1 width height] :as shape} (geom/size shape) - xfmt (cond-> (gmt/matrix) - (pos? rotation) (rotate shape)) + transform (when (pos? rotation) + (str (rotate (gmt/matrix) shape))) - moving? (boolean displacement) + moving? (boolean modifier-mtx) props {:x x1 :y y1 :id (str "shape-" id) :class-name (classnames :move-cursor moving?) :width width :height height - :transform (str xfmt)} + :transform transform} attrs (merge (attrs/extract-style-attrs shape) props)] [:> :rect (normalize-props attrs)])) diff --git a/frontend/src/uxbox/main/ui/workspace/canvas.cljs b/frontend/src/uxbox/main/ui/workspace/canvas.cljs index 21606e406c..c6f810e716 100644 --- a/frontend/src/uxbox/main/ui/workspace/canvas.cljs +++ b/frontend/src/uxbox/main/ui/workspace/canvas.cljs @@ -7,11 +7,12 @@ (ns uxbox.main.ui.workspace.canvas (:require - [rumext.alpha :as mf] [lentes.core :as l] + [rumext.alpha :as mf] [uxbox.main.constants :as c] - [uxbox.main.refs :as refs] [uxbox.main.data.workspace :as dw] + [uxbox.main.geom :as geom] + [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.ui.keyboard :as kbd] [uxbox.main.ui.shapes :as uus] @@ -33,20 +34,26 @@ (mf/defc canvas [{:keys [id] :as props}] - (letfn [(on-double-click [event] - (dom/prevent-default event) - (st/emit! (dw/select-canvas id)))] - (let [canvas-iref (mf/use-memo #(make-canvas-iref id) #js [id]) - canvas (mf/deref canvas-iref) - selected (mf/deref selected-canvas) - selected? (= id selected)] + (let [canvas-iref (mf/use-memo #(make-canvas-iref id) #js [id]) + canvas (-> (mf/deref canvas-iref) + (geom/size)) + selected (mf/deref selected-canvas) + selected? (= id selected)] + (letfn [(on-double-click [event] + (dom/prevent-default event) + (st/emit! (dw/select-canvas id))) + (on-mouse-down [event] + (when selected? + (dom/stop-propagation event) + #_(st/emit! (start-move id))))] [:rect.page-canvas - {:x (:x canvas) + {:x (:x1 canvas) :class (when selected? "selected") - :y (:y canvas) + :y (:y1 canvas) :fill (:background canvas "#ffffff") :width (:width canvas) :height (:height canvas) + :on-mouse-down on-mouse-down :on-double-click on-double-click}]))) diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index 518de36786..a49660a3bd 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -26,10 +26,6 @@ [uxbox.util.geom.point :as gpt] [uxbox.util.uuid :as uuid])) -(defn- rxfinalize - [f ob] - (.pipe ob (.finalize js/rxjs.operators f))) - ;; --- Events (declare handle-drawing) @@ -64,6 +60,7 @@ [{:type :rect :name "Rect" :stroke-color "#000000"} + {:type :image} {:type :circle :name "Circle"} {:type :path @@ -74,6 +71,9 @@ :fill-color "#000000" :fill-opacity 0 :segments []} + {:type :canvas + :name "Canvas" + :stroke-color "#000000"} {:type :curve :name "Path" :stroke-style :solid @@ -98,8 +98,8 @@ ptk/UpdateEvent (update [_ state] (let [pid (get-in state [:workspace :current]) - shape (make-minimal-shape type)] - (assoc-in state [:workspace pid :drawing] shape))) + data (make-minimal-shape type)] + (update-in state [:workspace pid :drawing] merge data))) ptk/WatchEvent (watch [_ state stream] @@ -118,7 +118,7 @@ :y2 (+ (:y point) 2)})] (assoc-in state [:workspace pid :drawing] (assoc shape ::initialized? true)))) - ;; TODO: this is a new approach for resizing, when all the + ;; NOTE: this is a new approach for resizing, when all the ;; subsystem are migrated to the new resize approach, this ;; function should be moved into uxbox.main.geom ns. (resize-shape [shape point lock?] @@ -331,11 +331,12 @@ (declare path-draw-area) (mf/defc draw-area - [{:keys [zoom shape modifiers] :as props}] - (case (:type shape) - (:path :curve) [:& path-draw-area {:shape shape}] - [:& generic-draw-area {:shape (assoc shape :modifiers modifiers) - :zoom zoom}])) + [{:keys [zoom shape] :as props}] + (when (:id shape) + (case (:type shape) + (:path :curve) [:& path-draw-area {:shape shape}] + [:& generic-draw-area {:shape shape + :zoom zoom}]))) (mf/defc generic-draw-area [{:keys [shape zoom]}] diff --git a/frontend/src/uxbox/main/ui/workspace/images.cljs b/frontend/src/uxbox/main/ui/workspace/images.cljs index e9fcdfdef2..fdab5ec963 100644 --- a/frontend/src/uxbox/main/ui/workspace/images.cljs +++ b/frontend/src/uxbox/main/ui/workspace/images.cljs @@ -48,13 +48,11 @@ (on-uploaded [[image]] (let [{:keys [id name width height]} image - shape {:type :image - :name name - :id (uuid/random) + shape {:name name :metadata {:width width :height height} :image id}] - (st/emit! (dw/select-for-drawing shape)) + (st/emit! (dw/select-for-drawing :image shape)) (modal/hide!))) (on-files-selected [event] @@ -93,13 +91,11 @@ (mf/defc image-item [{:keys [image] :as props}] (letfn [(on-click [event] - (let [shape {:type :image - :name (:name image) - :id (uuid/random) + (let [shape {:name (:name image) :metadata {:width (:width image) :height (:height image)} :image (:id image)}] - (st/emit! (dw/select-for-drawing shape)) + (st/emit! (dw/select-for-drawing :image shape)) (modal/hide!)))] [:div.library-item {:on-click on-click} [:div.library-item-th diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index 13ed9b9c2b..119349b1b4 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -40,11 +40,11 @@ (let [result (geom/resize-shape vid shape point lock?) scale (geom/calculate-scale-ratio shape result) mtx (geom/generate-resize-matrix vid shape scale) - xfm (map #(dw/apply-temporal-resize % mtx))] + xfm (map #(dw/assoc-temporal-modifier % mtx))] (apply st/emit! (sequence xfm ids)))) (on-end [] - (apply st/emit! (map dw/apply-resize ids))) + (apply st/emit! (map dw/materialize-current-modifier ids))) ;; Unifies the instantaneous proportion lock modifier ;; activated by Ctrl key and the shapes own proportion diff --git a/frontend/src/uxbox/main/ui/workspace/streams.cljs b/frontend/src/uxbox/main/ui/workspace/streams.cljs index 88bcb25226..4717bcf3ca 100644 --- a/frontend/src/uxbox/main/ui/workspace/streams.cljs +++ b/frontend/src/uxbox/main/ui/workspace/streams.cljs @@ -81,15 +81,17 @@ ob (->> st/stream (rx/filter pointer-event?) (rx/filter #(= :viewport (:source %))) - (rx/map :pt) - )] + (rx/map :pt))] (rx/subscribe-with ob sub) sub)) (defonce mouse-position-ctrl - (let [sub (rx/behavior-subject nil)] - (-> (rx/map :ctrl mouse-position) - (rx/subscribe-with sub)) + (let [sub (rx/behavior-subject nil) + ob (->> st/stream + (rx/filter pointer-event?) + (rx/map :ctrl) + (rx/dedupe))] + (rx/subscribe-with ob sub) sub)) (defonce mouse-position-deltas diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index b0d6239993..1afa5860a3 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -284,156 +284,3 @@ [:& ruler {:zoom zoom :ruler (:ruler wst)}]) [:& selrect {:data (:selrect wst)}]]]))) - -#_(mf/def viewport - :init - (fn [own props] - (assoc own ::viewport (mf/create-ref))) - - :did-mount - (fn [own] - (letfn [ - (translate-point-to-viewport [pt] - (let [viewport (mf/ref-node (::viewport own)) - brect (.getBoundingClientRect viewport) - brect (gpt/point (parse-int (.-left brect)) - (parse-int (.-top brect)))] - (gpt/subtract pt brect))) - - (on-key-down [event] - (let [bevent (.getBrowserEvent event) - key (.-keyCode event) - ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - opts {:key key - :shift? shift? - :ctrl? ctrl?}] - (when-not (.-repeat bevent) - (st/emit! (uws/keyboard-event :down key ctrl? shift?)) - (when (kbd/space? event) - (st/emit! handle-viewport-positioning) - #_(st/emit! (dw/start-viewport-positioning)))))) - - (on-key-up [event] - (let [key (.-keyCode event) - ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - opts {:key key - :shift? shift? - :ctrl? ctrl?}] - (when (kbd/space? event) - (st/emit! ::finish-positioning #_(dw/stop-viewport-positioning))) - (st/emit! (uws/keyboard-event :up key ctrl? shift?)))) - - (on-mousemove [event] - (let [wpt (gpt/point (.-clientX event) - (.-clientY event)) - vpt (translate-point-to-viewport wpt) - ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - event {:ctrl ctrl? - :shift shift? - :window-coords wpt - :viewport-coords vpt}] - (st/emit! (uws/pointer-event wpt vpt ctrl? shift?))))] - - (let [key1 (events/listen js/document EventType.MOUSEMOVE on-mousemove) - key2 (events/listen js/document EventType.KEYDOWN on-key-down) - key3 (events/listen js/document EventType.KEYUP on-key-up)] - (assoc own - ::key1 key1 - ::key2 key2 - ::key3 key3)))) - - :will-unmount - (fn [own] - (events/unlistenByKey (::key1 own)) - (events/unlistenByKey (::key2 own)) - (events/unlistenByKey (::key3 own)) - (dissoc own ::key1 ::key2 ::key3)) - - :mixins [mf/reactive] - - :render - (fn [own {:keys [page] :as props}] - (let [{:keys [drawing-tool tooltip zoom flags edition] :as wst} (mf/react refs/workspace) - tooltip (or tooltip (get-shape-tooltip drawing-tool)) - zoom (or zoom 1)] - (letfn [(on-mouse-down [event] - (dom/stop-propagation event) - (let [ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - opts {:shift? shift? - :ctrl? ctrl?}] - (st/emit! (uws/mouse-event :down ctrl? shift?))) - (when (not edition) - (if drawing-tool - (st/emit! (start-drawing drawing-tool)) - (st/emit! :interrupt handle-selrect)))) - (on-context-menu [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (let [ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - opts {:shift? shift? - :ctrl? ctrl?}] - (st/emit! (uws/mouse-event :context-menu ctrl? shift?)))) - (on-mouse-up [event] - (dom/stop-propagation event) - (let [ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - opts {:shift? shift? - :ctrl? ctrl?}] - (st/emit! (uws/mouse-event :up ctrl? shift?)))) - (on-click [event] - (dom/stop-propagation event) - (let [ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - opts {:shift? shift? - :ctrl? ctrl?}] - (st/emit! (uws/mouse-event :click ctrl? shift?)))) - (on-double-click [event] - (dom/stop-propagation event) - (let [ctrl? (kbd/ctrl? event) - shift? (kbd/shift? event) - opts {:shift? shift? - :ctrl? ctrl?}] - (st/emit! (uws/mouse-event :double-click ctrl? shift?))))] - [:* - [:& 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 own) - :class (when drawing-tool "drawing") - :on-context-menu on-context-menu - :on-click on-click - :on-double-click on-double-click - :on-mouse-down on-mouse-down - :on-mouse-up on-mouse-up} - [:g.zoom {:transform (str "scale(" zoom ", " zoom ")")} - (when page - [:& canvas {:page page :wst wst}]) - - (when page - [:* - (for [id (reverse (:shapes page))] - [:& uus/shape-component {:id id :key id}]) - - (when (seq (:selected wst)) - [:& selection-handlers {:wst wst}]) - - (when-let [dshape (:drawing wst)] - [:& draw-area {:shape dshape - :zoom (:zoom wst) - :modifiers (:modifiers wst)}])]) - - - - (if (contains? flags :grid) - [:& grid {:page page}])] - (when (contains? flags :ruler) - [:& ruler {:zoom zoom :ruler (:ruler wst)}]) - [:& selrect {:data (:selrect wst)}]]])))) 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 eac8f86658..e5d58caeba 100644 --- a/frontend/test/uxbox/tests/test_main_data_shapes_impl.cljs +++ b/frontend/test/uxbox/tests/test_main_data_shapes_impl.cljs @@ -2,7 +2,7 @@ (:require [cljs.test :as t :include-macros true] [cljs.pprint :refer [pprint]] [uxbox.util.uuid :as uuid] - [uxbox.main.data.shapes-impl :as impl])) + [uxbox.main.data.shapes :as impl])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Duplicate (one shape) From 5a820b4f9e07f4680c31c5916a61ddcf0576a994 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 Aug 2019 20:07:58 +0200 Subject: [PATCH 15/46] :books: Move some deleted docs from CONTRIBUTIN file to README. --- README.md | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 19ae1873bd..dfb7b086fc 100644 --- a/README.md +++ b/README.md @@ -28,15 +28,87 @@ editable in many other vector tools and easy to use on the web. ## Development ## -Most of the main operations can be done through the helper script -`manage.sh`. +### Introduction ### -The development requires of UXBOX is done through a single docker -container. Each main service is opened in a different -[tmux](https://github.com/tmux/tmux) sessions. +The development environment consists in a docker container that mounts +your local copy of the uxbox souce code directory tree and executes a +tmux inside the container in order to facilitate execute multiple +processes inside. -## Docker +### System requirements ### + +You should have `docker` installed in your system in order to set up +properly the uxbox development enviroment. + +In debian like linux distributions you can install it executing: + +```bash +sudo apt-get install docker +``` + + +### Start the docker container ### + +**Requires a minimum knowledge of tmux usage in order to use that +development environment.** + +For start it, staying in this repository, execute: + +```bash +./manage.sh run-devenv +``` + +This will do the following: + +- Build the image if it is not done before. +- Download all repositories if them are not downloaded previously. +- Start a container with predefined tmux layout. +- Start all needed processes such as gulp and figwheel. + + +### First steps with tmux ### + +Now having the the container running and tmux open inside the +container, you are free to execute any commands and open many shells +as you want. + +You can create a new shell just pressing the **Ctr+b c** shortcut. And +**Ctrl+b w** for switch between windows, **Ctrl+b &** for kill the +current window. + + +### Inside the tmux session ### + +#### UI #### + +The UI related tasks starts automatically so you do not need do anything. The +**window 0** and **window 1** are used for the UI related environment. + + +#### Backend #### + +The backend related environment is located in the **window 2**, and you can go +directly to it using `ctrl+b 2` shortcut. + +By default this tasks are performed: + +- Start postgresql. +- Load initial fixtures into the database. + +The repl should be started automatically, if not, you can execute: + +```bash +clojure -Adev:repl +``` + +Then use `(start)` to start all the environment, `(stop)` for stoping +it and `(reset)` for restart with code reloading. If some exception is +raised when code is reloaded, just use `(refresh)` in order to finish +correctly the code swaping and later use `(reset)` again. + + +## Production (Docker) Docker is also used to build release images for backend and frontend. Use the helper script `manage.sh` to build the images. You From f0230c346cd4ef2ee7b501c454faa71b0933f917 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 Aug 2019 20:18:35 +0200 Subject: [PATCH 16/46] :bug: Fix selection handlers reactivity. --- frontend/src/uxbox/main/geom.cljs | 15 ++------- frontend/src/uxbox/main/ui/shapes/path.cljs | 20 ++++++------ .../uxbox/main/ui/workspace/selection.cljs | 32 +++++++------------ 3 files changed, 23 insertions(+), 44 deletions(-) diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index aa6843a8f9..44fa50d6f2 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -565,7 +565,6 @@ ;; --- Outer Rect (declare selection-rect-generic) -(declare selection-rect-group) (defn rotation-matrix "Generate a rotation matrix from shape." @@ -590,25 +589,15 @@ ([shape] (selection-rect @st/state shape)) ([state shape] - (let [{:keys [displacement resize]} (:modifiers shape)] + (let [modifier (:modifier-mtx shape)] (-> (shape->rect-shape shape) (assoc :type :rect :id (:id shape)) - (transform (or resize (gmt/matrix))) - (transform (or displacement (gmt/matrix))) + (transform (or modifier (gmt/matrix))) (rotate-shape) (size))))) ;; --- Helpers -(defn resolve-parent - "Recursively resolve the real shape parent." - ([shape] - (resolve-parent @st/state shape)) - ([state {:keys [group] :as shape}] - (if group - (resolve-parent state (get-in state [:shapes group])) - shape))) - (defn contained-in? "Check if a shape is contained in the provided selection rect." diff --git a/frontend/src/uxbox/main/ui/shapes/path.cljs b/frontend/src/uxbox/main/ui/shapes/path.cljs index 0a20942d4c..07f3fd1526 100644 --- a/frontend/src/uxbox/main/ui/shapes/path.cljs +++ b/frontend/src/uxbox/main/ui/shapes/path.cljs @@ -14,7 +14,8 @@ [uxbox.main.store :as st] [uxbox.main.ui.shapes.attrs :as attrs] [uxbox.main.ui.shapes.common :as common] - [uxbox.util.data :refer [classnames normalize-props]])) + [uxbox.util.data :refer [classnames normalize-props]] + [uxbox.util.geom.matrix :as gmt])) ;; --- Path Component @@ -22,8 +23,7 @@ (mf/defc path-component [{:keys [shape] :as props}] - (let [modifiers (mf/deref (refs/selected-modifiers (:id shape))) - selected (mf/deref refs/selected-shapes) + (let [selected (mf/deref refs/selected-shapes) selected? (contains? selected (:id shape))] (letfn [(on-mouse-down [event] (common/on-mouse-down event shape selected)) @@ -35,7 +35,6 @@ :on-double-click on-double-click :on-mouse-down on-mouse-down} [:& path-shape {:shape shape - :modifiers modifiers :background? true}]]))) ;; --- Path Shape @@ -62,12 +61,13 @@ (recur buffer (inc index))))))) (mf/defc path-shape - [{:keys [shape modifiers background?] :as props}] - (let [{:keys [resize displacement]} modifiers - shape (cond-> shape - displacement (geom/transform displacement) - resize (geom/transform resize)) - moving? (boolean displacement) + [{:keys [shape background?] :as props}] + (let [modifier-mtx (:modifier-mtx shape) + shape (cond + (gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx) + :else shape) + + moving? (boolean modifier-mtx) pdata (render-path shape) props {:id (str (:id shape)) diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index 119349b1b4..87430604b9 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -190,9 +190,8 @@ :style {:cursor "pointer"}}])]))) (mf/defc multiple-selection-handlers - [{:keys [shapes modifiers zoom] :as props}] + [{:keys [shapes zoom] :as props}] (let [shape (->> shapes - (map #(assoc % :modifiers (get modifiers (:id %)))) (map #(geom/selection-rect %)) (geom/shapes->rect-shape) (geom/selection-rect)) @@ -202,18 +201,9 @@ :zoom zoom :on-click on-click}])) -(mf/defc single-selection-handlers - [{:keys [shape zoom modifiers] :as props}] - (let [on-click #(do (dom/stop-propagation %2) - (start-resize %1 #{(:id shape)} shape)) - shape (-> (assoc shape :modifiers modifiers) - (geom/selection-rect))] - [:& controls {:shape shape :zoom zoom :on-click on-click}])) - (mf/defc text-edition-selection-handlers - [{:keys [shape modifiers zoom] :as props}] - (let [{:keys [x1 y1 width height] :as shape} (-> (assoc shape :modifiers modifiers) - (geom/selection-rect))] + [{:keys [shape zoom] :as props}] + (let [{:keys [x1 y1 width height] :as shape} (geom/selection-rect shape)] [:g.controls [:rect.main {:x x1 :y y1 :width width @@ -224,6 +214,13 @@ :stroke-opacity "0.5" :fill "transparent"}}]])) +(mf/defc single-selection-handlers + [{:keys [shape zoom] :as props}] + (let [on-click #(do (dom/stop-propagation %2) + (start-resize %1 #{(:id shape)} shape)) + shape (geom/selection-rect shape)] + [:& controls {:shape shape :zoom zoom :on-click on-click}])) + (def ^:private shapes-map-iref (-> (l/key :shapes) (l/derive st/state))) @@ -233,34 +230,27 @@ (let [shapes-map (mf/deref shapes-map-iref) shapes (map #(get shapes-map %) (:selected wst)) edition? (:edition wst) - modifiers (:modifiers wst) zoom (:zoom wst 1) num (count shapes) {:keys [id type] :as shape} (first shapes)] - (cond (zero? num) nil (> num 1) [:& multiple-selection-handlers {:shapes shapes - :modifiers modifiers :zoom zoom}] (and (= type :text) (= edition? (:id shape))) [:& text-edition-selection-handlers {:shape shape - :modifiers (get modifiers id) :zoom zoom}] (and (= type :path) (= edition? (:id shape))) [:& path-edition-selection-handlers {:shape shape - :zoom zoom - :modifiers (get modifiers id)}] - + :zoom zoom}] :else [:& single-selection-handlers {:shape shape - :modifiers (get modifiers id) :zoom zoom}]))) From 12637bbfe480514a3f5fc2843d49364ad1523fb9 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 Aug 2019 20:27:32 +0200 Subject: [PATCH 17/46] :bug: Fix bug on curve path edition mode. --- frontend/src/uxbox/main/ui/shapes/path.cljs | 6 +++--- frontend/src/uxbox/main/ui/workspace/selection.cljs | 9 +++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frontend/src/uxbox/main/ui/shapes/path.cljs b/frontend/src/uxbox/main/ui/shapes/path.cljs index 07f3fd1526..1cbd9b399d 100644 --- a/frontend/src/uxbox/main/ui/shapes/path.cljs +++ b/frontend/src/uxbox/main/ui/shapes/path.cljs @@ -8,7 +8,7 @@ (:require [cuerdas.core :as str :include-macros true] [rumext.alpha :as mf] - [uxbox.main.data.workspace :as udw] + [uxbox.main.data.workspace :as dw] [uxbox.main.geom :as geom] [uxbox.main.refs :as refs] [uxbox.main.store :as st] @@ -29,8 +29,8 @@ (common/on-mouse-down event shape selected)) (on-double-click [event] (when selected? - (prn "on-double-click") - (st/emit! (udw/start-edition-mode (:id shape)))))] + (prn "path-component$on-double-click") + (st/emit! (dw/start-edition-mode (:id shape)))))] [:g.shape {:class (when selected? "selected") :on-double-click on-double-click :on-mouse-down on-mouse-down} diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index 87430604b9..94e925c428 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -229,7 +229,7 @@ [{:keys [wst] :as props}] (let [shapes-map (mf/deref shapes-map-iref) shapes (map #(get shapes-map %) (:selected wst)) - edition? (:edition wst) + edition (:edition wst) zoom (:zoom wst 1) num (count shapes) {:keys [id type] :as shape} (first shapes)] @@ -243,11 +243,12 @@ :zoom zoom}] (and (= type :text) - (= edition? (:id shape))) + (= edition (:id shape))) [:& text-edition-selection-handlers {:shape shape :zoom zoom}] - (and (= type :path) - (= edition? (:id shape))) + (and (or (= type :path) + (= type :curve)) + (= edition (:id shape))) [:& path-edition-selection-handlers {:shape shape :zoom zoom}] From 363b2db695214d4c7ada56b54273b8cf7d6739a7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 Aug 2019 23:07:01 +0200 Subject: [PATCH 18/46] :sparkles: Improve initial shape drawing impl. --- .../src/uxbox/main/ui/workspace/drawarea.cljs | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index a49660a3bd..d2b24df184 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -118,24 +118,13 @@ :y2 (+ (:y point) 2)})] (assoc-in state [:workspace pid :drawing] (assoc shape ::initialized? true)))) - ;; NOTE: this is a new approach for resizing, when all the - ;; subsystem are migrated to the new resize approach, this - ;; function should be moved into uxbox.main.geom ns. (resize-shape [shape point lock?] - (if (= (:type shape) :circle) - (let [rx (mth/abs (- (:x point) (:cx shape))) - ry (mth/abs (- (:y point) (:cy shape)))] - (if lock? - (assoc shape :rx rx :ry ry) - (assoc shape :rx rx :ry rx))) - (let [width (- (:x point) (:x1 shape)) - height (- (:y point) (:y1 shape)) - proportion (:proportion shape 1)] - (assoc shape - :x2 (+ (:x1 shape) width) - :y2 (if lock? - (+ (:y1 shape) (/ width proportion)) - (+ (:y1 shape) height)))))) + (let [shape (-> (geom/shape->rect-shape shape) + (geom/size)) + result (geom/resize-shape :bottom-right shape point lock?) + scale (geom/calculate-scale-ratio shape result) + mtx (geom/generate-resize-matrix :bottom-right shape scale)] + (assoc shape :modifier-mtx mtx))) (update-drawing [state point lock?] (let [pid (get-in state [:workspace :current])] From 20364f47b482835ac0f06e9822b11e54b338a329 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 Aug 2019 23:47:56 +0200 Subject: [PATCH 19/46] :sparkles: Improve & simplify the drawing end event. --- .../src/uxbox/main/ui/workspace/drawarea.cljs | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index d2b24df184..6bbade8baa 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -22,6 +22,7 @@ [uxbox.util.math :as mth] [uxbox.util.dom :as dom] [uxbox.util.data :refer [seek]] + [uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.path :as path] [uxbox.util.geom.point :as gpt] [uxbox.util.uuid :as uuid])) @@ -289,23 +290,17 @@ (watch [_ state stream] (let [pid (get-in state [:workspace :current]) shape (get-in state [:workspace pid :drawing])] - (if (::initialized? shape) - (let [resize-mtx (get-in state [:workspace pid :modifiers (:id shape) :resize]) - shape (cond-> shape - resize-mtx (geom/transform resize-mtx))] - (rx/of - ;; Remove the stalled modifiers - ;; TODO: maybe a specific event for "clear modifiers" - #(update-in % [:workspace pid :modifiers] dissoc (:id shape)) - - ;; Unselect the drawing tool - ;; TODO; maybe a specific event for clear draw-tool - dw/clear-drawing - + (rx/concat + (rx/of dw/clear-drawing) + (when (::initialized? shape) + (let [modifier-mtx (:modifier-mtx shape) + shape (if (gmt/matrix? modifier-mtx) + (geom/transform shape modifier-mtx) + shape) + shape (dissoc shape ::initialized? :modifier-mtx)] ;; Add & select the cred shape to the workspace - (dw/add-shape (dissoc shape ::initialized?)) - (dw/select-first-shape))) - (rx/of #(update-in % [:workspace pid] dissoc :drawing :drawing-tool))))))) + (rx/of (dw/add-shape shape) + (dw/select-first-shape))))))))) (def close-drawing-path (reify From 844c866642a46fc2e9c9f2c98e314de40b4d1911 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 22 Aug 2019 23:48:35 +0200 Subject: [PATCH 20/46] :recycle: Refactor selection start-resize event. --- .../uxbox/main/ui/workspace/selection.cljs | 45 +++++++++---------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/frontend/src/uxbox/main/ui/workspace/selection.cljs b/frontend/src/uxbox/main/ui/workspace/selection.cljs index 94e925c428..1b55a5eb32 100644 --- a/frontend/src/uxbox/main/ui/workspace/selection.cljs +++ b/frontend/src/uxbox/main/ui/workspace/selection.cljs @@ -10,6 +10,7 @@ (:require [beicon.core :as rx] [lentes.core :as l] + [potok.core :as ptk] [rumext.alpha :as mf] [uxbox.main.data.workspace :as dw] [uxbox.main.geom :as geom] @@ -32,19 +33,13 @@ ;; --- Resize Implementation -;; TODO: this function need to be refactored - (defn- start-resize [vid ids shape] - (letfn [(on-resize [shape [point lock?]] + (letfn [(resize [shape [point lock?]] (let [result (geom/resize-shape vid shape point lock?) scale (geom/calculate-scale-ratio shape result) - mtx (geom/generate-resize-matrix vid shape scale) - xfm (map #(dw/assoc-temporal-modifier % mtx))] - (apply st/emit! (sequence xfm ids)))) - - (on-end [] - (apply st/emit! (map dw/materialize-current-modifier ids))) + mtx (geom/generate-resize-matrix vid shape scale)] + (apply rx/of (map #(dw/assoc-temporal-modifier % mtx) ids)))) ;; Unifies the instantaneous proportion lock modifier ;; activated by Ctrl key and the shapes own proportion @@ -63,19 +58,23 @@ ;; Apply the current zoom factor to the point. (apply-zoom [point] (gpt/divide point @refs/selected-zoom))] + (reify + ptk/WatchEvent + (watch [_ state stream] + (let [shape (->> (geom/shape->rect-shape shape) + (geom/size)) + stoper (rx/filter ws/mouse-up? stream)] + (rx/concat + (->> ws/mouse-position + (rx/map apply-zoom) + (rx/mapcat apply-grid-alignment) + (rx/with-latest vector ws/mouse-position-ctrl) + (rx/map normalize-proportion-lock) + (rx/mapcat (partial resize shape)) + (rx/take-until stoper)) + (rx/from-coll (map dw/materialize-current-modifier ids)))))))) - (let [shape (->> (geom/shape->rect-shape shape) - (geom/size)) - stoper (->> st/stream - (rx/filter ws/mouse-up?) - (rx/take 1)) - stream (->> ws/mouse-position - (rx/take-until stoper) - (rx/map apply-zoom) - (rx/mapcat apply-grid-alignment) - (rx/with-latest vector ws/mouse-position-ctrl) - (rx/map normalize-proportion-lock))] - (rx/subscribe stream (partial on-resize shape) nil on-end)))) + ;; (rx/subscribe stream (partial on-resize shape) nil on-end)))) ;; --- Controls (Component) @@ -196,7 +195,7 @@ (geom/shapes->rect-shape) (geom/selection-rect)) on-click #(do (dom/stop-propagation %2) - (start-resize %1 (map :id shapes) shape))] + (st/emit! (start-resize %1 (mapv :id shapes) shape)))] [:& controls {:shape shape :zoom zoom :on-click on-click}])) @@ -217,7 +216,7 @@ (mf/defc single-selection-handlers [{:keys [shape zoom] :as props}] (let [on-click #(do (dom/stop-propagation %2) - (start-resize %1 #{(:id shape)} shape)) + (st/emit! (start-resize %1 #{(:id shape)} shape))) shape (geom/selection-rect shape)] [:& controls {:shape shape :zoom zoom :on-click on-click}])) From 8f7d22921eb897316248892f06975549009e1bf6 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Fri, 23 Aug 2019 19:12:51 +0200 Subject: [PATCH 21/46] :arrow_up: Update npm (mostly development) dependencies. --- frontend/package-lock.json | 210 ++++++++++++++++++++++++------------- frontend/package.json | 8 +- 2 files changed, 143 insertions(+), 75 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4843128317..061112f28f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -550,9 +550,9 @@ } }, "caniuse-lite": { - "version": "1.0.30000988", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000988.tgz", - "integrity": "sha512-lPj3T8poYrRc/bniW5SQPND3GRtSrQdUM/R4mCYTbZxyi3jQiggLvZH4+BYUuX0t4TXjU+vMM7KFDQg+rSzZUQ==", + "version": "1.0.30000989", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz", + "integrity": "sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw==", "dev": true }, "caseless": { @@ -573,9 +573,9 @@ } }, "chokidar": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz", - "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", + "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, "requires": { "anymatch": "^2.0.0", @@ -973,9 +973,9 @@ } }, "electron-to-chromium": { - "version": "1.3.207", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.207.tgz", - "integrity": "sha512-RIgAnfqbjZNECBLjslfy4cIYvcPl3GAXmnENrcoo0TZ8fGkyEEAealAbO7MoevW4xYUPe+e68cWAj6eP0DmMHw==", + "version": "1.3.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.237.tgz", + "integrity": "sha512-SPAFjDr/7iiVK2kgTluwxela6eaWjjFkS9rO/iYpB/KGXgccUom5YC7OIf19c8m8GGptWxLU0Em8xM64A/N7Fg==", "dev": true }, "end-of-stream": { @@ -1907,6 +1907,17 @@ "inherits": "~2.0.0", "mkdirp": ">=0.5 0", "rimraf": "2" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "function-bind": { @@ -2079,9 +2090,9 @@ } }, "graceful-fs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", - "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", "dev": true }, "gulp": { @@ -2125,15 +2136,15 @@ } }, "gulp-autoprefixer": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/gulp-autoprefixer/-/gulp-autoprefixer-6.1.0.tgz", - "integrity": "sha512-Ti/BUFe+ekhbDJfspZIMiOsOvw51KhI9EncsDfK7NaxjqRm+v4xS9v99kPxEoiDavpWqQWvG8Y6xT1mMlB3aXA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/gulp-autoprefixer/-/gulp-autoprefixer-7.0.0.tgz", + "integrity": "sha512-ZGMA/9iPF7kfZIVhznd3eylivBcyLCgcV32cWtvM7j2IjEQzoRg1XaKgCeO5ytutq8XW3Rip+iM4EsVbNshoZw==", "dev": true, "requires": { - "autoprefixer": "^9.5.1", + "autoprefixer": "^9.6.1", "fancy-log": "^1.3.2", "plugin-error": "^1.0.1", - "postcss": "^7.0.2", + "postcss": "^7.0.17", "through2": "^3.0.1", "vinyl-sourcemaps-apply": "^0.2.1" }, @@ -2187,14 +2198,25 @@ } }, "gulp-if": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-2.0.2.tgz", - "integrity": "sha1-pJe351cwBQQcqivIt92jyARE1ik=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/gulp-if/-/gulp-if-3.0.0.tgz", + "integrity": "sha512-fCUEngzNiEZEK2YuPm+sdMpO6ukb8+/qzbGfJBXyNOXz85bCG7yBI+pPSl+N90d7gnLvMsarthsAImx0qy7BAw==", "dev": true, "requires": { - "gulp-match": "^1.0.3", - "ternary-stream": "^2.0.1", - "through2": "^2.0.1" + "gulp-match": "^1.1.0", + "ternary-stream": "^3.0.0", + "through2": "^3.0.1" + }, + "dependencies": { + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "dev": true, + "requires": { + "readable-stream": "2 || 3" + } + } } }, "gulp-match": { @@ -2207,18 +2229,24 @@ } }, "gulp-mustache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/gulp-mustache/-/gulp-mustache-4.1.1.tgz", - "integrity": "sha512-Cdtzf+owfMF7pD9bwxs+afribmWrCY010uB4GpbnAs9/hpLScnNRTe4I7kKhr/Cn9JJyHDDN/hIsvhAhvP2doA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/gulp-mustache/-/gulp-mustache-4.1.2.tgz", + "integrity": "sha512-4nJKL6akiP77Znbog2my7XbJ94VnS8HmMBwLYvUNoSYmD99I9vCopIok6XXT1nOu0zXztc5HxQ2PndYt06PWAQ==", "dev": true, "requires": { - "escape-string-regexp": "^1.0.5", + "escape-string-regexp": "^2.0.0", "mustache": "^3.0.1", "plugin-error": "^1.0.0", "replace-ext": "^1.0.0", - "through2": "^3.0.0" + "through2": "^3.0.1" }, "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + }, "through2": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", @@ -2452,9 +2480,9 @@ } }, "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", + "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==", "dev": true }, "http-signature": { @@ -2989,13 +3017,10 @@ } }, "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "micromatch": { "version": "3.1.10", @@ -3093,9 +3118,9 @@ "dev": true }, "mustache": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.0.1.tgz", - "integrity": "sha512-jFI/4UVRsRYdUbuDTKT7KzfOp7FiD5WzYmmwNwXyUVypC0xjoTL78Fqc0jHUPIvvGD+6DQSPHIt1NE7D1ArsqA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-3.0.2.tgz", + "integrity": "sha512-64neoEgmozb8e/ecGBOSE+RfnevLSFzCI0UKPcrWmjv953/8fXhYO9+EQFtfbi6hwoFxcTA+Fp5mRiOiI9eTuA==", "dev": true }, "mute-stdout": { @@ -3155,6 +3180,15 @@ "which": "1" }, "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", @@ -3164,9 +3198,9 @@ } }, "node-releases": { - "version": "1.1.26", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.26.tgz", - "integrity": "sha512-fZPsuhhUHMTlfkhDLGtfY80DSJTjOcx+qD1j5pqPkuhUHVS7xHZIg9EE4DHK8O3f0zTxXHX5VIkDG8pu98/wfQ==", + "version": "1.1.28", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.28.tgz", + "integrity": "sha512-AQw4emh6iSXnCpDiFe0phYcThiccmkNWMZnFZ+lDJjAP8J0m2fVd59duvUUyuTirQOhIAajTFkzG6FHCLBO59g==", "dev": true, "requires": { "semver": "^5.3.0" @@ -3612,9 +3646,9 @@ } }, "postcss-value-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.0.tgz", - "integrity": "sha512-ESPktioptiSUchCKgggAkzdmkgzKfmp0EU8jXH+5kbIUB+unr0Y4CY9SRMvibuvYUBjNh1ACLbxqYNpdTQOteQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz", + "integrity": "sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==", "dev": true }, "pretty-hrtime": { @@ -3636,9 +3670,9 @@ "dev": true }, "psl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", - "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz", + "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==", "dev": true }, "pump": { @@ -3856,9 +3890,9 @@ "dev": true }, "resolve": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", - "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz", + "integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==", "dev": true, "requires": { "path-parse": "^1.0.6" @@ -3896,9 +3930,9 @@ "dev": true }, "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==", "dev": true, "requires": { "glob": "^7.1.3" @@ -3959,9 +3993,9 @@ } }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, "semver-greatest-satisfied-range": { @@ -4353,15 +4387,49 @@ } }, "ternary-stream": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-2.1.1.tgz", - "integrity": "sha512-j6ei9hxSoyGlqTmoMjOm+QNvUKDOIY6bNl4Uh1lhBvl6yjPW2iLqxDUYyfDPZknQ4KdRziFl+ec99iT4l7g0cw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ternary-stream/-/ternary-stream-3.0.0.tgz", + "integrity": "sha512-oIzdi+UL/JdktkT+7KU5tSIQjj8pbShj3OASuvDEhm0NT5lppsm7aXWAmAq4/QMaBIyfuEcNLbAQA+HpaISobQ==", "dev": true, "requires": { - "duplexify": "^3.5.0", + "duplexify": "^4.1.1", "fork-stream": "^0.0.4", - "merge-stream": "^1.0.0", - "through2": "^2.0.1" + "merge-stream": "^2.0.0", + "through2": "^3.0.1" + }, + "dependencies": { + "duplexify": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.1.tgz", + "integrity": "sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA==", + "dev": true, + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "through2": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz", + "integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==", + "dev": true, + "requires": { + "readable-stream": "2 || 3" + } + } } }, "through2": { @@ -4500,9 +4568,9 @@ "dev": true }, "type": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/type/-/type-1.0.1.tgz", - "integrity": "sha512-MAM5dBMJCJNKs9E7JXo4CXRAansRfG0nlJxW7Wf6GZzSOvH31zClSaHdIMWLehe/EGMBkqeC55rrkaOr5Oo7Nw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/type/-/type-1.0.3.tgz", + "integrity": "sha512-51IMtNfVcee8+9GJvj0spSuFcZHe9vSib6Xtgsny1Km9ugyz2mbS08I3rsUIRYgJohFRFU1160sgRodYz378Hg==", "dev": true }, "typedarray": { @@ -4636,9 +4704,9 @@ "dev": true }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", "dev": true }, "v8flags": { diff --git a/frontend/package.json b/frontend/package.json index d0f98d036a..067dd45265 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,15 +11,15 @@ "scripts": {}, "devDependencies": { "gulp": "4.0.2", - "gulp-autoprefixer": "^6.1.0", + "gulp-autoprefixer": "^7.0.0", "gulp-clean-css": "^4.2.0", "gulp-gzip": "^1.4.2", - "gulp-if": "^2.0.2", - "gulp-mustache": "^4.1.1", + "gulp-if": "^3.0.0", + "gulp-mustache": "^4.1.2", "gulp-plumber": "^1.2.1", "gulp-rename": "^1.4.0", "gulp-sass": "^4.0.2", - "rimraf": "^2.6.3" + "rimraf": "^3.0.0" }, "dependencies": { "source-map-support": "^0.5.12" From 4df8a6b3b7714b8ee63c4f72a78642896ffa40b3 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 12:39:53 +0200 Subject: [PATCH 22/46] :lipstick: Cosmetic changes on contributing. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d70f3101fd..b58ea99d23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,7 @@ We have very precise rules over how our git commit messages can be formatted. The commit message format is: ``` -(): + [body] From eebd56d738e983bdef07450a6030c17bd3f4e7a2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 12:40:45 +0200 Subject: [PATCH 23/46] :lipstick: Cosmetic changes on initial align shape event. --- frontend/src/uxbox/main/data/workspace.cljs | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 17a3f00b06..befb70eb1f 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -582,20 +582,19 @@ ;; --- Shape Transformations -(defrecord InitialShapeAlign [id] - ptk/WatchEvent - (watch [_ state s] - (let [{:keys [x1 y1] :as shape} (->> (get-in state [:shapes id]) - (geom/shape->rect-shape state)) - point (gpt/point x1 y1)] - (->> (uwrk/align-point point) - (rx/map (fn [{:keys [x y] :as pt}] - (apply-temporal-displacement id (gpt/subtract pt point)))))))) - (defn initial-shape-align [id] {:pre [(uuid? id)]} - (InitialShapeAlign. id)) + (reify + ptk/WatchEvent + (watch [_ state s] + (let [{:keys [x1 y1] :as shape} (->> (get-in state [:shapes id]) + (geom/shape->rect-shape state)) + point (gpt/point x1 y1)] + (->> (uwrk/align-point point) + (rx/map (fn [{:keys [x y] :as pt}] + (apply-temporal-displacement id (gpt/subtract pt point))))))))) + ;; --- Apply Temporal Displacement From 5af263c70ef13e161689467f99a9503e79a3f690 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 12:41:11 +0200 Subject: [PATCH 24/46] :bug: Fix text shape edition mode. --- frontend/src/uxbox/main/ui/shapes/text.cljs | 43 +++++++++------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/frontend/src/uxbox/main/ui/shapes/text.cljs b/frontend/src/uxbox/main/ui/shapes/text.cljs index fdfb72047a..0e4d7a0866 100644 --- a/frontend/src/uxbox/main/ui/shapes/text.cljs +++ b/frontend/src/uxbox/main/ui/shapes/text.cljs @@ -39,30 +39,25 @@ (declare text-shape-wrapper) (declare text-shape-edit) -(mf/def text-component - :mixins [mf/memo mf/reactive] - :render - (fn [own {:keys [shape] :as props}] - (let [{:keys [id x1 y1 content group]} shape - modifiers (mf/react (refs/selected-modifiers id)) - selected (mf/react refs/selected-shapes) - edition? (= (mf/react refs/selected-edition) id) - selected? (and (contains? selected id) - (= (count selected) 1)) - shape (assoc shape :modifiers modifiers)] - (letfn [(on-mouse-down [event] - (handle-mouse-down event shape selected)) - (on-double-click [event] - ;; TODO: handle grouping event propagation - ;; TODO: handle actions locking properly - (dom/stop-propagation event) - (st/emit! (udw/start-edition-mode id)))] - [:g.shape {:class (when selected? "selected") - :on-double-click on-double-click - :on-mouse-down on-mouse-down} - (if edition? - [:& text-shape-edit {:shape shape}] - [:& text-shape-wrapper {:shape shape}])])))) +(mf/defc text-component + [{:keys [shape] :as props}] + (let [{:keys [id x1 y1 content group]} shape + selected (mf/deref refs/selected-shapes) + edition (mf/deref refs/selected-edition) + edition? (= edition id) + selected? (and (contains? selected id) + (= (count selected) 1))] + (letfn [(on-mouse-down [event] + (handle-mouse-down event shape selected)) + (on-double-click [event] + (dom/stop-propagation event) + (st/emit! (udw/start-edition-mode id)))] + [:g.shape {:class (when selected? "selected") + :on-double-click on-double-click + :on-mouse-down on-mouse-down} + (if edition? + [:& text-shape-edit {:shape shape}] + [:& text-shape-wrapper {:shape shape}])]))) ;; --- Text Styles Helpers From afae00f660fae464bf3c34056f3b83c2650fab2b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 12:41:29 +0200 Subject: [PATCH 25/46] :fire: Remove unused and commented code. --- .../src/uxbox/main/ui/workspace/drawarea.cljs | 5 +--- .../src/uxbox/main/ui/workspace/viewport.cljs | 27 +++++-------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index 6bbade8baa..a5d5ccfe04 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -270,10 +270,7 @@ zoom (get-in state [:workspace pid :zoom]) flags (get-in state [:workspace pid :flags]) align? (refs/alignment-activated? flags) - - stoper (->> (rx/filter stoper-event? stream) - (rx/take 1)) - + stoper (rx/filter stoper-event? stream) mouse (->> (rx/sample 10 uws/mouse-position) (rx/mapcat #(conditional-align % align?)))] (rx/concat diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index 1afa5860a3..388db887da 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -217,29 +217,16 @@ (let [pt (gpt/point (.-clientX event) (.-clientY event)) pt (translate-point-to-viewport pt)] - ;; (prn "viewport:on-mouse-move" pt) - (st/emit! (uws/->PointerEvent :viewport pt (kbd/ctrl? event) (kbd/shift? event))))) - - - ;; ;; ctrl? (kbd/ctrl? event) - ;; ;; shift? (kbd/shift? event) - ;; ;; event {:ctrl ctrl? - ;; ;; :shift shift? - ;; ;; :window-coords wpt - ;; ;; :viewport-coords vpt} - ;; ] - ;; #_(st/emit! (uws/pointer-event wpt vpt ctrl? shift?)))) + (st/emit! (uws/->PointerEvent :viewport pt + (kbd/ctrl? event) + (kbd/shift? event))))) (on-mount [] - (prn "viewport.on-mount" (:id page)) - (let [ - ;; key1 (events/listen js/document EventType.MOUSEMOVE on-mousemove) - key2 (events/listen js/document EventType.KEYDOWN on-key-down) - key3 (events/listen js/document EventType.KEYUP on-key-up)] + (let [key1 (events/listen js/document EventType.KEYDOWN on-key-down) + key2 (events/listen js/document EventType.KEYUP on-key-up)] (fn [] - ;; (events/unlistenByKey key1) - (events/unlistenByKey key2) - (events/unlistenByKey key3))))] + (events/unlistenByKey key1) + (events/unlistenByKey key2))))] (mf/use-effect on-mount) ;; (prn "viewport.render" (:id page)) From f99134c70b640cef5d0701c2c24b33698ec335df Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 15:58:20 +0200 Subject: [PATCH 26/46] :sparkles: Make canvas behave like shapes on the data layer. --- backend/src/uxbox/fixtures.clj | 8 ++++---- frontend/src/uxbox/main/data/pages.cljs | 26 +++++++------------------ 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/backend/src/uxbox/fixtures.clj b/backend/src/uxbox/fixtures.clj index a11764a14c..d0827793c2 100644 --- a/backend/src/uxbox/fixtures.clj +++ b/backend/src/uxbox/fixtures.clj @@ -58,20 +58,20 @@ {:id (mk-uuid "page" i) :user (mk-uuid "user" ui) :project (mk-uuid "project" pi) - :data {:canvas [{:id (mk-uuid "canvas" i 1) + :data {:shapes [{:id (mk-uuid "canvas" i 1) :name "Canvas 1" + :type :canvas :x1 200 :y1 200 :x2 1224 :y2 968} {:id (mk-uuid "canvas" i 2) :name "Canvas 2" + :type :canvas :x1 1324 :y1 200 :x2 2348 - :y2 968}] - :shapes [] - :shapes-map {}} + :y2 968}]} :metadata {:width 1024 :height 768 :layout "tablet"} diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index d01bac6fd4..67592c34e4 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -80,39 +80,27 @@ "Return a packed version of page object ready for send to remore storage service." [state id] - (letfn [(get-shape [id] - (get-in state [:shapes id])) - (pack-shapes [ids] - (reduce #(assoc %1 %2 (get-shape %2)) {} ids)) - (pack-canvas [ids] - (mapv #(get-in state [:canvas %]) ids))] + (letfn [(pack-shapes [ids] + (mapv #(get-in state [:shapes %]) ids))] (let [page (get-in state [:pages id]) - data {:canvas (pack-canvas (:canvas page)) - :shapes (vec (:shapes page)) - :shapes-map (pack-shapes (:shapes page))}] + data {:shapes (pack-shapes (:shapes page))}] (-> page (assoc :data data) - (dissoc :shapes :canvas))))) + (dissoc :shapes))))) (defn unpack-page "Unpacks packed page object and assocs it to the provided state." [state {:keys [id data] :as page}] - (let [shapes (:shapes data) - shapes-map (:shapes-map data) - - canvas-data (:canvas data []) - - canvas (mapv :id canvas-data) - canvas-map (index-by-id canvas-data) + (let [shapes-data (:shapes data []) + shapes (mapv :id shapes-data) + shapes-map (index-by-id shapes-data) page (-> page (dissoc :data) - (assoc :canvas canvas) (assoc :shapes shapes))] (-> state (update :shapes merge shapes-map) - (update :canvas merge canvas-map) (update :pages assoc id page)))) (defn purge-page From 902f7c4181879cfc3eac4359aaa19c0a19e14ad8 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 15:58:52 +0200 Subject: [PATCH 27/46] :hankey: Comment some code that needs to be refactored (undo related). --- frontend/src/uxbox/main/data/undo.cljs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/uxbox/main/data/undo.cljs b/frontend/src/uxbox/main/data/undo.cljs index 8e6f316aa6..6b418eb9aa 100644 --- a/frontend/src/uxbox/main/data/undo.cljs +++ b/frontend/src/uxbox/main/data/undo.cljs @@ -21,7 +21,8 @@ (deftype WatchPageChanges [id] ptk/WatchEvent (watch [_ state stream] - (let [stopper (->> stream + nil + #_(let [stopper (->> stream (rx/filter #(= % ::udp/stop-page-watcher)) (rx/take 1))] (->> stream @@ -41,7 +42,8 @@ (defrecord SaveUndoEntry [id] ptk/UpdateEvent (update [_ state] - (let [page (udp/pack-page state id) + state + #_(let [page (udp/pack-page state id) undo {:data (:data page) :metadata (:metadata page)}] (-> state @@ -88,7 +90,7 @@ udp/IPageUpdate ptk/UpdateEvent (update [_ state] - (let [page-id (get-in state [:workspace :page]) + #_(let [page-id (get-in state [:workspace :page]) undo-state (get-in state [:undo page-id]) stack (:stack undo-state) selected (:selected undo-state 0)] @@ -123,7 +125,7 @@ udp/IPageUpdate ptk/UpdateEvent (update [_ state] - (let [page-id (get-in state [:workspace :page]) + #_(let [page-id (get-in state [:workspace :page]) undo-state (get-in state [:undo page-id]) stack (:stack undo-state) selected (:selected undo-state)] From daac4486e5eb6ac7fef2d22bb6e0605ff7d288de Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 16:01:17 +0200 Subject: [PATCH 28/46] :sparkles: Allow canvas movement and resize. --- frontend/src/uxbox/main/data/shapes.cljs | 1 + frontend/src/uxbox/main/geom.cljs | 1 + frontend/src/uxbox/main/ui/shapes.cljs | 2 + frontend/src/uxbox/main/ui/shapes/common.cljs | 18 ++++-- frontend/src/uxbox/main/ui/shapes/rect.cljs | 1 - .../src/uxbox/main/ui/workspace/canvas.cljs | 60 ------------------- .../src/uxbox/main/ui/workspace/viewport.cljs | 6 -- 7 files changed, 16 insertions(+), 73 deletions(-) delete mode 100644 frontend/src/uxbox/main/ui/workspace/canvas.cljs diff --git a/frontend/src/uxbox/main/data/shapes.cljs b/frontend/src/uxbox/main/data/shapes.cljs index 3c20c9ca56..154bdf6c26 100644 --- a/frontend/src/uxbox/main/data/shapes.cljs +++ b/frontend/src/uxbox/main/data/shapes.cljs @@ -372,6 +372,7 @@ (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])] diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index 44fa50d6f2..58ec4c3f78 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -512,6 +512,7 @@ "Apply the matrix transformation to shape." [{:keys [type] :as shape} xfmt] (case type + :canvas (transform-rect shape xfmt) :rect (transform-rect shape xfmt) :icon (transform-rect shape xfmt) :text (transform-rect shape xfmt) diff --git a/frontend/src/uxbox/main/ui/shapes.cljs b/frontend/src/uxbox/main/ui/shapes.cljs index 135bd5ae1a..256c0a78d5 100644 --- a/frontend/src/uxbox/main/ui/shapes.cljs +++ b/frontend/src/uxbox/main/ui/shapes.cljs @@ -14,12 +14,14 @@ [uxbox.main.ui.shapes.image :as image] [uxbox.main.ui.shapes.path :as path] [uxbox.main.ui.shapes.rect :as rect] + [uxbox.main.ui.shapes.canvas :as canvas] [uxbox.main.ui.shapes.text :as text])) (defn render-shape [shape] (mf/html (case (:type shape) + :canvas [:& canvas/canvas-component {:shape shape}] :curve [:& path/path-component {:shape shape}] :text [:& text/text-component {:shape shape}] :icon [:& icon/icon-component {:shape shape}] diff --git a/frontend/src/uxbox/main/ui/shapes/common.cljs b/frontend/src/uxbox/main/ui/shapes/common.cljs index 38c6e0a192..ef76169eca 100644 --- a/frontend/src/uxbox/main/ui/shapes/common.cljs +++ b/frontend/src/uxbox/main/ui/shapes/common.cljs @@ -35,30 +35,36 @@ (rx/take-until stoper)) (rx/of (dw/materialize-current-modifier id))))))) -(defn start-move-selected - [] +(def start-move-selected (reify ptk/WatchEvent (watch [_ state stream] (let [pid (get-in state [:workspace :current]) selected (get-in state [:workspace pid :selected])] + (prn "start-move-selected" selected) (rx/from-coll (map start-move selected)))))) (defn on-mouse-down - [event {:keys [id] :as shape} selected] + [event {:keys [id type] :as shape} selected] (let [selected? (contains? selected id) drawing? @refs/selected-drawing-tool] + (prn "on-mouse-down" id type selected? (= type :canvas)) (when-not (:blocked shape) (cond drawing? nil + (= type :canvas) + (when selected? + (dom/stop-propagation event) + (st/emit! start-move-selected)) + (and (not selected?) (empty? selected)) (do (dom/stop-propagation event) (st/emit! (dw/deselect-all) (dw/select-shape id) - (start-move-selected))) + start-move-selected)) (and (not selected?) (not (empty? selected))) (do @@ -67,8 +73,8 @@ (st/emit! (dw/select-shape id)) (st/emit! (dw/deselect-all) (dw/select-shape id) - (start-move-selected)))) + start-move-selected))) :else (do (dom/stop-propagation event) - (st/emit! (start-move-selected))))))) + (st/emit! start-move-selected)))))) diff --git a/frontend/src/uxbox/main/ui/shapes/rect.cljs b/frontend/src/uxbox/main/ui/shapes/rect.cljs index 891645099e..9bd03450b1 100644 --- a/frontend/src/uxbox/main/ui/shapes/rect.cljs +++ b/frontend/src/uxbox/main/ui/shapes/rect.cljs @@ -25,7 +25,6 @@ (let [selected (mf/deref refs/selected-shapes) selected? (contains? selected (:id shape)) on-mouse-down #(common/on-mouse-down % shape selected)] - ;; shape (assoc shape :modifiers modifiers)] [:g.shape {:class (when selected? "selected") :on-mouse-down on-mouse-down} [:& rect-shape {:shape shape}]])) diff --git a/frontend/src/uxbox/main/ui/workspace/canvas.cljs b/frontend/src/uxbox/main/ui/workspace/canvas.cljs deleted file mode 100644 index c6f810e716..0000000000 --- a/frontend/src/uxbox/main/ui/workspace/canvas.cljs +++ /dev/null @@ -1,60 +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-2016 Andrey Antukh -;; Copyright (c) 2015-2016 Juan de la Cruz - -(ns uxbox.main.ui.workspace.canvas - (:require - [lentes.core :as l] - [rumext.alpha :as mf] - [uxbox.main.constants :as c] - [uxbox.main.data.workspace :as dw] - [uxbox.main.geom :as geom] - [uxbox.main.refs :as refs] - [uxbox.main.store :as st] - [uxbox.main.ui.keyboard :as kbd] - [uxbox.main.ui.shapes :as uus] - [uxbox.main.ui.workspace.drawarea :refer [draw-area]] - [uxbox.main.ui.workspace.selection :refer [selection-handlers]] - [uxbox.main.ui.workspace.streams :as uws] - [uxbox.util.data :refer [parse-int]] - [uxbox.util.dom :as dom] - [uxbox.util.geom.point :as gpt])) - -(def selected-canvas - (-> (l/key :selected-canvas) - (l/derive refs/workspace))) - -(defn- make-canvas-iref - [id] - (-> (l/in [:canvas id]) - (l/derive st/state))) - -(mf/defc canvas - [{:keys [id] :as props}] - (let [canvas-iref (mf/use-memo #(make-canvas-iref id) #js [id]) - canvas (-> (mf/deref canvas-iref) - (geom/size)) - selected (mf/deref selected-canvas) - selected? (= id selected)] - (letfn [(on-double-click [event] - (dom/prevent-default event) - (st/emit! (dw/select-canvas id))) - (on-mouse-down [event] - (when selected? - (dom/stop-propagation event) - #_(st/emit! (start-move id))))] - [:rect.page-canvas - {:x (:x1 canvas) - :class (when selected? "selected") - :y (:y1 canvas) - :fill (:background canvas "#ffffff") - :width (:width canvas) - :height (:height canvas) - :on-mouse-down on-mouse-down - :on-double-click on-double-click}]))) - - - diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index 388db887da..3c7d2ee827 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -17,7 +17,6 @@ [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.ui.keyboard :as kbd] - [uxbox.main.ui.workspace.canvas :refer [canvas]] [uxbox.main.ui.workspace.grid :refer [grid]] [uxbox.main.ui.workspace.ruler :refer [ruler]] [uxbox.main.ui.workspace.streams :as uws] @@ -229,8 +228,6 @@ (events/unlistenByKey key2))))] (mf/use-effect on-mount) - ;; (prn "viewport.render" (:id page)) - [:* [:& coordinates {:zoom zoom}] #_[:div.tooltip-container @@ -249,9 +246,6 @@ [:g.zoom {:transform (str "scale(" zoom ", " zoom ")")} (when page [:* - (for [id (:canvas page)] - [:& canvas {:key id :id id}]) - (for [id (reverse (:shapes page))] [:& uus/shape-component {:id id :key id}]) From 786aefe7d8fa6fe00cbc9dddb26ffed2b6aff431 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 16:12:35 +0200 Subject: [PATCH 29/46] :sparkles: Enable drawing new canvas. --- frontend/src/uxbox/main/geom.cljs | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index 58ec4c3f78..9d1884f00a 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -177,6 +177,7 @@ (defn setup-proportions [shape] (case (:type shape) + :canvas (setup-proportions-rect shape) :rect (setup-proportions-rect shape) :circle (setup-proportions-rect shape) :icon (setup-proportions-image shape) From 89d0b632a4b88052ec89517c8ecc57df83588cf2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 16:25:38 +0200 Subject: [PATCH 30/46] :sparkles: Better handle zoom on drawing. --- .../src/uxbox/main/ui/workspace/drawarea.cljs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index a5d5ccfe04..89d92eeb08 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -135,22 +135,21 @@ ptk/WatchEvent (watch [_ state stream] (let [pid (get-in state [:workspace :current]) - zoom (get-in state [:workspace pid :zoom]) - flags (get-in state [:workspace pid :flags]) + {:keys [zoom flags]} (get-in state [:workspace pid]) align? (refs/alignment-activated? flags) - stoper (->> (rx/filter #(or (uws/mouse-up? %) (= % :interrupt)) stream) - (rx/take 1)) + stoper? #(or (uws/mouse-up? %) (= % :interrupt)) + stoper (rx/filter stoper? stream) mouse (->> uws/mouse-position (rx/mapcat #(conditional-align % align?)) - (rx/with-latest vector uws/mouse-position-ctrl))] + (rx/map #(gpt/divide % zoom)))] (rx/concat - (->> uws/mouse-position + (->> mouse (rx/take 1) - (rx/mapcat #(conditional-align % align?)) (rx/map (fn [pt] #(initialize-drawing % pt)))) (->> mouse + (rx/with-latest vector uws/mouse-position-ctrl) (rx/map (fn [[pt ctrl?]] #(update-drawing % pt ctrl?))) (rx/take-until stoper)) (rx/of handle-finish-drawing))))))) @@ -189,17 +188,17 @@ ptk/WatchEvent (watch [_ state stream] (let [pid (get-in state [:workspace :current]) - zoom (get-in state [:workspace pid :zoom]) - flags (get-in state [:workspace pid :flags]) - align? (refs/alignment-activated? flags) + {:keys [zoom flags]} (get-in state [:workspace pid]) - last-point (volatile! @uws/mouse-position) + align? (refs/alignment-activated? flags) + last-point (volatile! (gpt/divide @uws/mouse-position zoom)) stoper (->> (rx/filter stoper-event? stream) (rx/share)) mouse (->> (rx/sample 10 uws/mouse-position) - (rx/mapcat #(conditional-align % align?))) + (rx/mapcat #(conditional-align % align?)) + (rx/map #(gpt/divide % zoom))) points (->> stream (rx/filter uws/mouse-click?) @@ -267,12 +266,14 @@ ptk/WatchEvent (watch [_ state stream] (let [pid (get-in state [:workspace :current]) - zoom (get-in state [:workspace pid :zoom]) - flags (get-in state [:workspace pid :flags]) + {:keys [zoom flags]} (get-in state [:workspace pid]) + align? (refs/alignment-activated? flags) stoper (rx/filter stoper-event? stream) - mouse (->> (rx/sample 10 uws/mouse-position) - (rx/mapcat #(conditional-align % align?)))] + mouse (->> (rx/sample 10 uws/mouse-position) + (rx/mapcat #(conditional-align % align?)) + (rx/map #(gpt/divide % zoom)))] + (rx/concat (rx/of initialize-drawing) (->> mouse From 9ce650377b674a2bc57af98bf40bf3559bfaf708 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 16:34:35 +0200 Subject: [PATCH 31/46] :sparkles: Load only one canvas on fixtures. --- backend/src/uxbox/fixtures.clj | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/backend/src/uxbox/fixtures.clj b/backend/src/uxbox/fixtures.clj index d0827793c2..fb9017be9f 100644 --- a/backend/src/uxbox/fixtures.clj +++ b/backend/src/uxbox/fixtures.clj @@ -64,14 +64,7 @@ :x1 200 :y1 200 :x2 1224 - :y2 968} - {:id (mk-uuid "canvas" i 2) - :name "Canvas 2" - :type :canvas - :x1 1324 - :y1 200 - :x2 2348 - :y2 968}]} + :y2 968}] :metadata {:width 1024 :height 768 :layout "tablet"} From 33b8d9881266045f0140a8dec0c7ae3fc6ea84d2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 16:35:10 +0200 Subject: [PATCH 32/46] :bug: Fix confirmation dialog on projects and pages deletion. --- .../src/uxbox/main/ui/dashboard/projects.cljs | 44 +++++-------------- .../main/ui/workspace/sidebar/sitemap.cljs | 20 ++++----- 2 files changed, 21 insertions(+), 43 deletions(-) diff --git a/frontend/src/uxbox/main/ui/dashboard/projects.cljs b/frontend/src/uxbox/main/ui/dashboard/projects.cljs index 924bf4269d..af9aaf1b10 100644 --- a/frontend/src/uxbox/main/ui/dashboard/projects.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/projects.cljs @@ -9,20 +9,17 @@ (:require [cuerdas.core :as str] [lentes.core :as l] - [rumext.core :as mx] [rumext.alpha :as mf] [uxbox.builtins.icons :as i] [uxbox.main.constants :as c] - [uxbox.main.data.lightbox :as udl] [uxbox.main.data.projects :as udp] - [uxbox.main.exports :as exports] [uxbox.main.store :as st] - [uxbox.main.ui.dashboard.projects-createform] + [uxbox.main.ui.modal :as modal] [uxbox.main.ui.keyboard :as kbd] - [uxbox.util.blob :as blob] + [uxbox.main.ui.confirm :refer [confirm-dialog]] [uxbox.util.data :refer [read-string]] [uxbox.util.dom :as dom] - [uxbox.util.i18n :as t :refer (tr)] + [uxbox.util.i18n :as t :refer [tr]] [uxbox.util.router :as r] [uxbox.util.time :as dt])) @@ -34,11 +31,11 @@ ;; --- Refs -(def opts-ref +(def opts-iref (-> (l/in [:dashboard :projects]) (l/derive st/state))) -(def projects-ref +(def projects-iref (-> (l/key :projects) (l/derive st/state))) @@ -117,26 +114,9 @@ (mf/defc grid-item-thumbnail [{:keys [project] :as props}] - (let [url (mf/use-state nil)] - #_(mf/use-effect - {:deps #js [(:page-id project)] - :init (fn [] - (when-let [page-id (:page-id project)] - (let [svg (exports/render-page page-id) - uri (some-> svg - (blob/create "image/svg+xml") - (blob/create-uri))] - (reset! url uri) - uri))) - :end #(when % (blob/revoke-uri %))}) - (if @url - [:div.grid-item-th - {:style {:background-image (str "url('" @url "')")}}] - [:div.grid-item-th - [:img.img-th {:src "/images/project-placeholder.svg" - :alt "Project title"}]]))) - - + [:div.grid-item-th + [:img.img-th {:src "/images/project-placeholder.svg" + :alt "Project title"}]]) ;; --- Grid Item @@ -148,7 +128,7 @@ delete #(st/emit! (udp/delete-project project)) on-delete #(do (dom/stop-propagation %) - (udl/open! :confirm {:on-accept delete})) + (modal/show! confirm-dialog {:on-accept delete})) on-blur #(let [target (dom/event->target %) name (dom/get-value target) id (:id project)] @@ -193,13 +173,13 @@ [{:keys [opts] :as props}] (let [order (:order opts :created) filter (:filter opts "") - projects (mf/deref projects-ref) + projects (mf/deref projects-iref) projects (->> (vals projects) (filter-projects-by filter) (sort-projects-by order)) on-click #(do (dom/prevent-default %) - (udl/open! :create-project))] + #_(udl/open! :create-project))] [:section.dashboard-grid [:h2 (tr "ds.project-title")] [:div.dashboard-grid-content @@ -214,7 +194,7 @@ (mf/defc projects-page [_] (mf/use-effect #(st/emit! (udp/initialize))) - (let [opts (mf/deref opts-ref)] + (let [opts (mf/deref opts-iref)] [:section.dashboard-content [:& menu {:opts opts}] [:& grid {:opts opts}]])) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs index a86b7f5536..ae929a78d5 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs @@ -7,24 +7,22 @@ (ns uxbox.main.ui.workspace.sidebar.sitemap (:require + ;; [uxbox.main.data.lightbox :as udl] + ;; [uxbox.main.ui.workspace.sidebar.sitemap-pageform] [cuerdas.core :as str] [lentes.core :as l] [rumext.alpha :as mf] - [rumext.util :as mfu] [uxbox.builtins.icons :as i] - [uxbox.main.data.lightbox :as udl] [uxbox.main.data.pages :as udp] [uxbox.main.data.projects :as dp] [uxbox.main.data.workspace :as dw] - [uxbox.main.refs :as refs] [uxbox.main.store :as st] - [uxbox.main.ui.lightbox :as lbx] - [uxbox.main.ui.workspace.sidebar.sitemap-pageform] + [uxbox.main.ui.confirm :refer [confirm-dialog]] + [uxbox.main.ui.modal :as modal] [uxbox.main.ui.workspace.sortable :refer [use-sortable]] [uxbox.util.data :refer [classnames]] [uxbox.util.dom :as dom] - [uxbox.util.dom.dnd :as dnd] - [uxbox.util.i18n :refer (tr)] + [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as rt])) ;; --- Page Item @@ -32,7 +30,7 @@ (mf/defc page-item [{:keys [page index deletable? selected?] :as props}] (letfn [(on-edit [event] - (udl/open! :page-form {:page page})) + #_(udl/open! :page-form {:page page})) (delete [] (let [next #(st/emit! (dp/go-to (:project page)))] (st/emit! (udp/delete-page (:id page) next)))) @@ -40,7 +38,7 @@ (on-delete [event] (dom/prevent-default event) (dom/stop-propagation event) - (udl/open! :confirm {:on-accept delete})) + (modal/show! confirm-dialog {:on-accept delete})) (on-drop [item monitor] (st/emit! (udp/reorder-pages (:project page)))) (on-hover [item monitor] @@ -101,7 +99,7 @@ :fn #(-> (l/in [:projects project-id]) (l/derive st/state))}) project (mf/deref project-iref) - create #(udl/open! :page-form {:page {:project project-id}}) + ;; create #(udl/open! :page-form {:page {:project project-id}}) close #(st/emit! (dw/toggle-flag :sitemap))] [:div.sitemap.tool-window [:div.tool-window-bar @@ -111,6 +109,6 @@ [:div.tool-window-content [:div.project-title [:span (:name project)] - [:div.add-page {:on-click create} i/close]] + [:div.add-page #_{:on-click create} i/close]] [:& pages-list {:project project :current-page-id current-page-id}]]])) From d147099e92e24310fa7df4da258b47ac3e62c83e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 24 Aug 2019 16:35:53 +0200 Subject: [PATCH 33/46] :tada: Add missing ui.shapes.canvas ns. --- frontend/src/uxbox/main/ui/shapes/canvas.cljs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 frontend/src/uxbox/main/ui/shapes/canvas.cljs diff --git a/frontend/src/uxbox/main/ui/shapes/canvas.cljs b/frontend/src/uxbox/main/ui/shapes/canvas.cljs new file mode 100644 index 0000000000..13c58f5516 --- /dev/null +++ b/frontend/src/uxbox/main/ui/shapes/canvas.cljs @@ -0,0 +1,42 @@ +;; 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-2016 Andrey Antukh +;; Copyright (c) 2015-2016 Juan de la Cruz + +(ns uxbox.main.ui.shapes.canvas + (:require + [lentes.core :as l] + [rumext.alpha :as mf] + [uxbox.main.constants :as c] + [uxbox.main.data.workspace :as dw] + [uxbox.main.geom :as geom] + [uxbox.main.refs :as refs] + [uxbox.main.ui.shapes.common :as common] + [uxbox.main.store :as st] + [uxbox.main.ui.shapes.rect :refer [rect-shape]] + ;; [uxbox.main.ui.workspace.streams :as uws] + [uxbox.util.data :refer [parse-int]] + [uxbox.util.dom :as dom] + [uxbox.util.geom.point :as gpt])) + +(def canvas-default-props + {:fill-color "#ffffff"}) + +(mf/defc canvas-component + [{:keys [shape] :as props}] + (let [selected (mf/deref refs/selected-shapes) + selected? (contains? selected (:id shape)) + on-mouse-down #(common/on-mouse-down % shape selected) + shape (merge canvas-default-props shape)] + (letfn [(on-double-click [event] + (dom/prevent-default event) + (st/emit! (dw/deselect-all) + (dw/select-shape (:id shape))))] + [:g.shape {:class (when selected? "selected") + :on-double-click on-double-click + :on-mouse-down on-mouse-down} + [:& rect-shape {:shape shape}]]))) + + From 263da4cc359fc1dd7e56af021802d12137fe201e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 26 Aug 2019 21:41:13 +0200 Subject: [PATCH 34/46] :constructor: Initial work on forms refactor. --- frontend/deps.edn | 3 +- frontend/src/uxbox/main/data/pages.cljs | 4 +- frontend/src/uxbox/main/data/projects.cljs | 1 + frontend/src/uxbox/main/ui/auth/login.cljs | 58 +++---- .../src/uxbox/main/ui/dashboard/projects.cljs | 2 + .../ui/dashboard/projects_createform.cljs | 151 ------------------ .../main/ui/dashboard/projects_forms.cljs | 97 +++++++++++ .../main/ui/workspace/sidebar/sitemap.cljs | 17 +- .../ui/workspace/sidebar/sitemap_forms.cljs | 103 ++++++++++++ .../workspace/sidebar/sitemap_pageform.cljs | 145 ----------------- frontend/src/uxbox/util/forms.cljs | 101 ++++++++++-- 11 files changed, 327 insertions(+), 355 deletions(-) delete mode 100644 frontend/src/uxbox/main/ui/dashboard/projects_createform.cljs create mode 100644 frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs create mode 100644 frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs delete mode 100644 frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs diff --git a/frontend/deps.edn b/frontend/deps.edn index 8a79a52f6c..6e9fbcabd4 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -7,10 +7,11 @@ environ/environ {:mvn/version "1.1.0"} metosin/reitit-core {:mvn/version "0.3.9"} + funcool/struct {:mvn/version "1.4.0"} funcool/beicon {:mvn/version "5.1.0"} funcool/cuerdas {:mvn/version "2.2.0"} funcool/lentes {:mvn/version "1.3.0-SNAPSHOT"} - funcool/potok {:mvn/version "2.4.0"} + funcool/potok {:mvn/version "2.5.0"} funcool/promesa {:mvn/version "3.0.0-SNAPSHOT"} funcool/rumext {:mvn/version "2.0.0-SNAPSHOT"} } diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index 67592c34e4..fd9e3051dc 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -9,13 +9,13 @@ [beicon.core :as rx] [cljs.spec.alpha :as s] [cuerdas.core :as str] - [lentes.core :as l] [potok.core :as ptk] [uxbox.main.repo :as rp] [uxbox.main.store :as st] + [uxbox.util.data :refer [index-by-id]] [uxbox.util.spec :as us] [uxbox.util.timers :as ts] - [uxbox.util.data :refer [index-by-id]])) + [uxbox.util.uuid :as uuid])) ;; --- Specs diff --git a/frontend/src/uxbox/main/data/projects.cljs b/frontend/src/uxbox/main/data/projects.cljs index df13361593..9e24636cfa 100644 --- a/frontend/src/uxbox/main/data/projects.cljs +++ b/frontend/src/uxbox/main/data/projects.cljs @@ -12,6 +12,7 @@ [uxbox.main.store :as st] [uxbox.main.repo :as rp] [uxbox.main.data.pages :as udp] + [uxbox.util.uuid :as uuid] [uxbox.util.spec :as us] [uxbox.util.time :as dt] [uxbox.util.router :as rt])) diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs index 9c1c21a63e..039bf20447 100644 --- a/frontend/src/uxbox/main/ui/auth/login.cljs +++ b/frontend/src/uxbox/main/ui/auth/login.cljs @@ -7,45 +7,27 @@ (ns uxbox.main.ui.auth.login (:require - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [lentes.core :as l] - [rumext.core :as mx] [rumext.alpha :as mf] [uxbox.builtins.icons :as i] [uxbox.config :as cfg] [uxbox.main.data.auth :as da] [uxbox.main.store :as st] [uxbox.main.ui.messages :refer [messages-widget]] - [uxbox.main.ui.navigation :as nav] [uxbox.util.dom :as dom] [uxbox.util.forms :as fm] - [uxbox.util.i18n :refer (tr)] + [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as rt])) -(def form-data (fm/focus-data :login st/state)) -(def form-errors (fm/focus-errors :login st/state)) - -(def assoc-value (partial fm/assoc-value :login)) -(def assoc-errors (partial fm/assoc-errors :login)) -(def clear-form (partial fm/clear-form :login)) - -(s/def ::username ::fm/non-empty-string) -(s/def ::password ::fm/non-empty-string) - -(s/def ::login-form - (s/keys :req-un [::username ::password])) - -(defn- on-change - [event field] - (let [value (dom/event->value event)] - (st/emit! (assoc-value field value)))) +(def login-form-spec + {:username [fm/required fm/string fm/non-empty-string] + :password [fm/required fm/string fm/non-empty-string]}) (defn- on-submit - [event data] + [event form] (dom/prevent-default event) - (st/emit! (da/login {:username (:username data) - :password (:password data)}))) + (let [{:keys [username password]} (:clean-data form)] + (st/emit! (da/login {:username username + :password password})))) (mf/defc demo-warning [_] @@ -56,33 +38,37 @@ [:strong "DO NOT USE"] " for real work, " [:br] " the projects will be periodicaly wiped."]]) + (mf/defc login-form [] - (let [data (mf/deref form-data) - valid? (fm/valid? ::login-form data)] - [:form {:on-submit #(on-submit % data)} + (let [{:keys [data] :as form} (fm/use-form {:initial {} + :spec login-form-spec})] + [:form {:on-submit #(on-submit % form)} [:div.login-content (when cfg/isdemo [:& demo-warning]) + [:input.input-text - {:name "email" + {:name "username" :tab-index "2" :value (:username data "") - :on-change #(on-change % :username) + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) :placeholder (tr "auth.email-or-username") :type "text"}] [:input.input-text {:name "password" :tab-index "3" :value (:password data "") - :on-change #(on-change % :password) + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) :placeholder (tr "auth.password") :type "password"}] [:input.btn-primary {:name "login" :tab-index "4" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) + :class (when (:errors form) "btn-disabled") + :disabled (boolean (:errors form)) :value (tr "auth.signin") :type "submit"}] [:div.login-links @@ -93,12 +79,8 @@ :tab-index "6"} (tr "auth.no-account")]]]])) - -;; {:mixins [mx/static (fm/clear-mixin st/store :login)]} - (mf/defc login-page [] - (mf/use-effect (constantly #(st/emit! (fm/clear-form :login)))) [:div.login [:div.login-body (messages-widget) diff --git a/frontend/src/uxbox/main/ui/dashboard/projects.cljs b/frontend/src/uxbox/main/ui/dashboard/projects.cljs index af9aaf1b10..0b09c34249 100644 --- a/frontend/src/uxbox/main/ui/dashboard/projects.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/projects.cljs @@ -17,6 +17,7 @@ [uxbox.main.ui.modal :as modal] [uxbox.main.ui.keyboard :as kbd] [uxbox.main.ui.confirm :refer [confirm-dialog]] + [uxbox.main.ui.dashboard.projects-forms :refer [create-project-dialog]] [uxbox.util.data :refer [read-string]] [uxbox.util.dom :as dom] [uxbox.util.i18n :as t :refer [tr]] @@ -179,6 +180,7 @@ (sort-projects-by order)) on-click #(do (dom/prevent-default %) + (modal/show! create-project-dialog {}) #_(udl/open! :create-project))] [:section.dashboard-grid [:h2 (tr "ds.project-title")] diff --git a/frontend/src/uxbox/main/ui/dashboard/projects_createform.cljs b/frontend/src/uxbox/main/ui/dashboard/projects_createform.cljs deleted file mode 100644 index 483dec174c..0000000000 --- a/frontend/src/uxbox/main/ui/dashboard/projects_createform.cljs +++ /dev/null @@ -1,151 +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-2017 Andrey Antukh -;; Copyright (c) 2015-2017 Juan de la Cruz - -(ns uxbox.main.ui.dashboard.projects-createform - (:require - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [lentes.core :as l] - [rumext.core :as mx] - [rumext.alpha :as mf] - [uxbox.builtins.icons :as i] - [uxbox.main.constants :as c] - [uxbox.main.data.lightbox :as udl] - [uxbox.main.data.projects :as udp] - [uxbox.main.store :as st] - [uxbox.main.ui.lightbox :as lbx] - [uxbox.util.data :refer [read-string parse-int]] - [uxbox.util.dom :as dom] - [uxbox.util.forms :as fm] - [uxbox.util.i18n :as t :refer [tr]] - [uxbox.util.router :as r] - [uxbox.util.time :as dt])) - -(def form-data (fm/focus-data :create-project st/state)) -(def form-errors (fm/focus-errors :create-project st/state)) -(def assoc-value (partial fm/assoc-value :create-project)) -(def clear-form (partial fm/clear-form :create-project)) - -(s/def ::name ::fm/non-empty-string) -(s/def ::layout ::fm/non-empty-string) -(s/def ::width number?) -(s/def ::height number?) - -(s/def ::project-form - (s/keys :req-un [::name - ::width - ::height - ::layout])) - -;; --- Create Project Form - -(mx/defc layout-input - [{:keys [::layout-id] :as data}] - (let [layout (get c/page-layouts layout-id)] - [:div - [:input {:type "radio" - :key layout-id - :id layout-id - :name "project-layout" - :value (:name layout) - :checked (= layout-id (:layout data)) - :on-change #(st/emit! (assoc-value :layout layout-id) - (assoc-value :width (:width layout)) - (assoc-value :height (:height layout)))}] - [:label {:value (:name layout) - :for layout-id} - (:name layout)]])) - -(mx/defc layout-selector - [props] - [:div.input-radio.radio-primary - (layout-input (assoc props ::layout-id "mobile")) - (layout-input (assoc props ::layout-id "tablet")) - (layout-input (assoc props ::layout-id "notebook")) - (layout-input (assoc props ::layout-id "desktop"))]) - -(mf/def create-project-form - :mixins #{mf/reactive} - :render - (fn [own props] - (let [data (merge c/project-defaults (mf/react form-data)) - valid? (fm/valid? ::project-form data)] - (letfn [(on-submit [event] - (dom/prevent-default event) - (when valid? - (st/emit! (udp/create-project data) - (udl/close-lightbox)))) - - (update-size [field e] - (let [value (dom/event->value e) - value (parse-int value)] - (st/emit! (assoc-value field value)))) - - (update-name [e] - (let [value (dom/event->value e)] - (st/emit! (assoc-value :name value)))) - (swap-size [] - (st/emit! (assoc-value :width (:height data)) - (assoc-value :height (:width data))))] - [:form {:on-submit on-submit} - [:input#project-name.input-text - {:placeholder "New project name" - :type "text" - :value (:name data) - :auto-focus true - :on-change update-name}] - [:div.project-size - [:div.input-element.pixels - [:span "Width"] - [:input#project-witdh.input-text - {:placeholder "Width" - :type "number" - :min 0 ;;TODO check this value - :max 666666 ;;TODO check this value - :value (str (:width data)) - :on-change (partial update-size :width)}]] - [:a.toggle-layout {:on-click swap-size} i/toggle] - [:div.input-element.pixels - [:span "Height"] - [:input#project-height.input-text - {:placeholder "Height" - :type "number" - :min 0 ;;TODO check this value - :max 666666 ;;TODO check this value - :value (str (:height data)) - :on-change (partial update-size :height)}]]] - - ;; Layout selector - (layout-selector data) - - ;; Submit - [:input#project-btn.btn-primary - {:value "Go go go!" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :type "submit"}]])))) - -;; --- Create Project Lightbox - -(mf/def create-project-lightbox - :will-unmount - (fn [own] - (st/emit! (fm/clear-form :create-project)) - own) - - :render - (fn [own] - [:div.lightbox-body - [:h3 "New project"] - (create-project-form) - [:a.close {:on-click #(st/emit! (udl/close-lightbox))} - i/close]])) - -(defmethod lbx/render-lightbox :create-project - [_] - (create-project-lightbox)) - diff --git a/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs b/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs new file mode 100644 index 0000000000..67e0d1b773 --- /dev/null +++ b/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs @@ -0,0 +1,97 @@ +;; 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 +;; Copyright (c) 2015-2019 Juan de la Cruz + +(ns uxbox.main.ui.dashboard.projects-forms + (:require + [cljs.spec.alpha :as s] + [rumext.alpha :as mf] + [uxbox.builtins.icons :as i] + [uxbox.main.data.projects :as udp] + [uxbox.main.store :as st] + [uxbox.main.ui.modal :as modal] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] + [uxbox.util.i18n :as t :refer [tr]])) + +(def project-form-spec + {:name [fm/required fm/string fm/non-empty-string] + :width [fm/required fm/number-str] + :height [fm/required fm/number-str]}) + +(def defaults + {:name "" + :width "1366" + :height "768"}) + +;; --- Create Project Form + +(defn- on-submit + [event form] + (dom/prevent-default event) + (let [data (:clean-data form)] + (st/emit! (udp/create-project data)) + (modal/hide!))) + +(defn- swap-size + [event {:keys [data] :as form}] + (swap! data assoc + :width (:height data) + :height (:width data))) + +(mf/defc create-project-form + [props] + (let [{:keys [data errors] :as form} (fm/use-form {:initial defaults :spec project-form-spec})] + [:form {:on-submit #(on-submit % form)} + [:input.input-text + {:placeholder "New project name" + :type "text" + :name "name" + :value (:name data) + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :auto-focus true}] + [:div.project-size + [:div.input-element.pixels + [:span "Width"] + [:input#project-witdh.input-text + {:placeholder "Width" + :name "width" + :type "number" + :min 0 + :max 5000 + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:width data)}]] + [:a.toggle-layout {:on-click #(swap-size % form)} i/toggle] + [:div.input-element.pixels + [:span "Height"] + [:input#project-height.input-text + {:placeholder "Height" + :type "number" + :name "height" + :min 0 + :max 5000 + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:height data)}]]] + + ;; Submit + [:input#project-btn.btn-primary + {:value "Go go go!" + :class (when-not (:valid form) "btn-disabled") + :disabled (not (:valid form)) + :type "submit"}]])) + +;; --- Create Project Lightbox + +(mf/defc create-project-dialog + [props] + [:div.lightbox-body + [:h3 "New project"] + [:& create-project-form] + [:a.close {:on-click modal/hide!} i/close]]) + diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs index ae929a78d5..c8aba12056 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs @@ -7,8 +7,8 @@ (ns uxbox.main.ui.workspace.sidebar.sitemap (:require - ;; [uxbox.main.data.lightbox :as udl] - ;; [uxbox.main.ui.workspace.sidebar.sitemap-pageform] + [potok.core :as ptk] + [beicon.core :as rx] [cuerdas.core :as str] [lentes.core :as l] [rumext.alpha :as mf] @@ -19,6 +19,7 @@ [uxbox.main.store :as st] [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.dom :as dom] @@ -30,17 +31,15 @@ (mf/defc page-item [{:keys [page index deletable? selected?] :as props}] (letfn [(on-edit [event] - #_(udl/open! :page-form {:page page})) + (modal/show! page-form-dialog {:page page})) (delete [] - (let [next #(st/emit! (dp/go-to (:project page)))] - (st/emit! (udp/delete-page (:id page) next)))) - + (st/emit! (dw/delete-page (:id page)))) (on-delete [event] (dom/prevent-default event) (dom/stop-propagation event) (modal/show! confirm-dialog {:on-accept delete})) (on-drop [item monitor] - (st/emit! (udp/reorder-pages (:project page)))) + (st/emit! (udp/rehash-pages (:project page)))) (on-hover [item monitor] (st/emit! (udp/move-page {:project-id (:project-id item) :page-id (:page-id item) @@ -99,7 +98,7 @@ :fn #(-> (l/in [:projects project-id]) (l/derive st/state))}) project (mf/deref project-iref) - ;; create #(udl/open! :page-form {:page {:project project-id}}) + create #(modal/show! page-form-dialog {:page {:project project-id}}) close #(st/emit! (dw/toggle-flag :sitemap))] [:div.sitemap.tool-window [:div.tool-window-bar @@ -109,6 +108,6 @@ [:div.tool-window-content [:div.project-title [:span (:name project)] - [:div.add-page #_{:on-click create} i/close]] + [:div.add-page {:on-click create} i/close]] [:& pages-list {:project project :current-page-id current-page-id}]]])) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs new file mode 100644 index 0000000000..d01c3b0b39 --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs @@ -0,0 +1,103 @@ +;; 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-2016 Andrey Antukh +;; Copyright (c) 2015-2016 Juan de la Cruz + +(ns uxbox.main.ui.workspace.sidebar.sitemap-forms + (:require + [rumext.alpha :as mf] + [uxbox.builtins.icons :as i] + [uxbox.main.constants :as c] + [uxbox.main.data.pages :as udp] + [uxbox.main.store :as st] + [uxbox.main.ui.modal :as modal] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] + [uxbox.util.i18n :refer [tr]])) + +(def page-form-spec + {:id [fm/uuid] + :project [fm/uuid] + :name [fm/required fm/string fm/non-empty-string] + :width [fm/required fm/number-str] + :height [fm/required fm/number-str]}) + +(def defaults + {:name "" + :width "1366" + :height "768"}) + +(defn- on-submit + [event form] + (dom/prevent-default event) + (modal/hide!) + (let [data (:clean-data form)] + (if (nil? (:id data)) + (st/emit! (udp/create-page data)) + (st/emit! (udp/persist-page-update-form data))))) + +(defn- swap-size + [event {:keys [data] :as form}] + (swap! data assoc + :width (:height data) + :height (:width data))) + +(defn- initial-data + [page] + (merge {:name "" :width "1366" :height "768"} + (select-keys page [:name :id :project]) + (select-keys (:metadata page) [:width :height]))) + +(mf/defc page-form + [{:keys [page] :as props}] + (let [{:keys [data errors] :as form} (fm/use-form {:initial #(initial-data page) + :spec page-form-spec})] + [:form {:on-submit #(on-submit % form)} + [:input.input-text + {:placeholder "Page name" + :type "text" + :name "name" + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:name data) + :auto-focus true}] + [:div.project-size + [:div.input-element.pixels + [:span "Width"] + [:input#project-witdh.input-text + {:placeholder "Width" + :name "width" + :type "number" + :min 0 + :max 5000 + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:width data)}]] + [:a.toggle-layout {:on-click #(swap-size % form)} i/toggle] + [:div.input-element.pixels + [:span "Height"] + [:input#project-height.input-text + {:placeholder "Height" + :name "height" + :type "number" + :min 0 + :max 5000 + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:height data)}]]] + [:input.btn-primary + {:value "Go go go!" + :type "submit" + :disabled (not (:valid form))}]])) + +(mf/defc page-form-dialog + [{:keys [page] :as props}] + [:div.lightbox-body + (if (nil? (:id page)) + [:h3 "New page"] + [:h3 "Edit page"]) + [:& page-form {:page page}] + [:a.close {:on-click modal/hide!} i/close]]) + diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs deleted file mode 100644 index 362f02f787..0000000000 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_pageform.cljs +++ /dev/null @@ -1,145 +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-2016 Andrey Antukh -;; Copyright (c) 2015-2016 Juan de la Cruz - -(ns uxbox.main.ui.workspace.sidebar.sitemap-pageform - (:require [cljs.spec.alpha :as s :include-macros true] - [lentes.core :as l] - [uxbox.builtins.icons :as i] - [uxbox.main.store :as st] - [uxbox.main.constants :as c] - [uxbox.main.data.pages :as udp] - [uxbox.main.data.lightbox :as udl] - [uxbox.main.ui.lightbox :as lbx] - [uxbox.util.data :refer [parse-int]] - [uxbox.util.dom :as dom] - [uxbox.util.forms :as fm] - [uxbox.util.i18n :refer [tr]] - [uxbox.util.router :as r] - [rumext.core :as mx :include-macros true])) - - -(def form-data (fm/focus-data :workspace-page-form st/state)) -(def form-errors (fm/focus-errors :workspace-page-form st/state)) - -(def assoc-value (partial fm/assoc-value :workspace-page-form)) -(def assoc-error (partial fm/assoc-error :workspace-page-form)) -(def clear-form (partial fm/clear-form :workspace-page-form)) - -;; --- Lightbox - -(s/def ::name ::fm/non-empty-string) -(s/def ::layout ::fm/non-empty-string) -(s/def ::width number?) -(s/def ::height number?) - -(s/def ::page-form - (s/keys :req-un [::name - ::width - ::height - ::layout])) - -(mx/defc layout-input - [data id] - (let [{:keys [id name width height]} (get c/page-layouts id)] - (letfn [(on-change [event] - (st/emit! (assoc-value :layout id) - (assoc-value :width width) - (assoc-value :height height)))] - [:div - [:input {:type "radio" - :id id - :name "project-layout" - :value id - :checked (when (= id (:layout data)) "checked") - :on-change on-change}] - [:label {:value id :for id} name]]))) - -(mx/defc page-form - {:mixins [mx/static mx/reactive]} - [{:keys [metadata id] :as page}] - (let [data (merge c/page-defaults - (select-keys page [:name :id :project]) - (select-keys metadata [:width :height :layout]) - (mx/react form-data)) - valid? (fm/valid? ::page-form data)] - (letfn [(update-size [field e] - (let [value (dom/event->value e) - value (parse-int value)] - (st/emit! (assoc-value field value)))) - (update-name [e] - (let [value (dom/event->value e)] - (st/emit! (assoc-value :name value)))) - (toggle-sizes [] - (let [{:keys [width height]} data] - (st/emit! (assoc-value :width width) - (assoc-value :height height)))) - (on-cancel [e] - (dom/prevent-default e) - (udl/close!)) - (on-save [e] - (dom/prevent-default e) - (udl/close!) - (if (nil? id) - (st/emit! (udp/create-page data)) - (st/emit! (udp/persist-page-update-form id data))))] - [:form - [:input#project-name.input-text - {:placeholder "Page name" - :type "text" - :value (:name data "") - :auto-focus true - :on-change update-name}] - [:div.project-size - [:div.input-element.pixels - [:span "Width"] - [:input#project-witdh.input-text - {:placeholder "Width" - :type "number" - :min 0 - :max 4000 - :value (:width data) - :on-change #(update-size :width %)}]] - [:a.toggle-layout {:on-click toggle-sizes} i/toggle] - [:div.input-element.pixels - [:span "Height"] - [:input#project-height.input-text - {:placeholder "Height" - :type "number" - :min 0 - :max 4000 - :value (:height data) - :on-change #(update-size :height %)}]]] - - [:div.input-radio.radio-primary - (layout-input data "mobile") - (layout-input data "tablet") - (layout-input data "notebook") - (layout-input data "desktop")] - - [:input#project-btn.btn-primary - {:value "Go go go!" - :disabled (not valid?) - :on-click on-save - :type "button"}]]))) - -(mx/defc page-form-lightbox - {:mixins [mx/static (fm/clear-mixin st/store :workspace-page-form)]} - [{:keys [id] :as page}] - (letfn [(on-cancel [event] - (dom/prevent-default event) - (udl/close!))] - (let [creation? (nil? id)] - [:div.lightbox-body - (if creation? - [:h3 "New page"] - [:h3 "Edit page"]) - (page-form page) - [:a.close {:on-click on-cancel} i/close]]))) - -(defmethod lbx/render-lightbox :page-form - [{:keys [page]}] - (page-form-lightbox page)) diff --git a/frontend/src/uxbox/util/forms.cljs b/frontend/src/uxbox/util/forms.cljs index b1a81a61a6..5d2427c661 100644 --- a/frontend/src/uxbox/util/forms.cljs +++ b/frontend/src/uxbox/util/forms.cljs @@ -5,15 +5,97 @@ ;; Copyright (c) 2015-2017 Andrey Antukh (ns uxbox.util.forms + (:refer-clojure :exclude [uuid]) (:require [beicon.core :as rx] [cljs.spec.alpha :as s :include-macros true] [cuerdas.core :as str] [lentes.core :as l] [potok.core :as ptk] - [rumext.core :as mx :include-macros true] + [rumext.alpha :as mf] + [rumext.core :as mx] + [struct.core :as stt] + [uxbox.util.dom :as dom] [uxbox.util.i18n :refer [tr]])) +;; --- Main Api + +(defn validate + [data spec] + (stt/validate data spec)) + +(defn valid? + [data spec] + (stt/valid? data spec)) + +;; --- Handlers Helpers + +(defn- impl-mutator + [v update-fn] + (specify v + IReset + (-reset! [_ new-value] + (update-fn new-value)) + + ISwap + (-swap! + ([self f] (update-fn f)) + ([self f x] (update-fn #(f % x))) + ([self f x y] (update-fn #(f % x y))) + ([self f x y more] (update-fn #(apply f % x y more)))))) + +(defn use-form + [{:keys [initial spec] :as opts}] + (let [[data update-data] (mf/useState initial) + [errors update-errors] (mf/useState nil) + [touched update-touched] (mf/useState {}) + [errors' clean-data] (validate data spec) + + data (impl-mutator data update-data) + errors (-> (merge {} errors' errors) + (impl-mutator update-errors)) + touched (impl-mutator touched update-touched)] + {:clean-data clean-data + :touched touched + :data data + :errors errors + :valid (not (seq errors))})) + +(defn on-input-change + [{:keys [data] :as form}] + (fn [event] + (let [target (dom/get-target event) + field (keyword (.-name target)) + value (dom/get-value target)] + (swap! data assoc field value)))) + +(defn on-input-blur + [{:keys [touched] :as form}] + (fn [event] + (let [target (dom/get-target event) + field (keyword (.-name target))] + (when-not (get touched field) + (swap! touched assoc field true))))) + +;; --- Additional Validators + +(def non-empty-string + {:message "errors.empty-string" + :optional true + :validate #(not (str/empty? %))}) + +(def string (assoc stt/string :message "errors.should-be-string")) +(def number (assoc stt/number :message "errors.should-be-number")) +(def number-str (assoc stt/number-str :message "errors.should-be-number")) +(def integer (assoc stt/integer :message "errors.should-be-integer")) +(def integer-str (assoc stt/integer-str :message "errors.should-be-integer")) +(def required (assoc stt/required :message "errors.required")) +(def email (assoc stt/email :message "errors.should-be-valid-email")) +(def uuid (assoc stt/uuid :message "errors.should-be-uuid")) +(def uuid-str (assoc stt/uuid-str :message "errors.should-be-valid-uuid")) + +;; DEPRECATED + ;; --- Form Validation Api (defn- interpret-problem @@ -30,15 +112,16 @@ :else acc)) -(defn validate - [spec data] - (when-not (s/valid? spec data) - (let [report (s/explain-data spec data)] - (reduce interpret-problem {} (::s/problems report))))) +;; (defn validate +;; [spec data] +;; (when-not (s/valid? spec data) +;; (let [report (s/explain-data spec data)] +;; (reduce interpret-problem {} (::s/problems report))))) + +;; (defn valid? +;; [spec data] +;; (s/valid? spec data)) -(defn valid? - [spec data] - (s/valid? spec data)) ;; --- Form Specs and Conformers From b7c6204b6c5c707d5339106d5110bb8bfb24c74d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 26 Aug 2019 21:41:42 +0200 Subject: [PATCH 35/46] :bug: Fix syntax error on backend fixtures. --- backend/src/uxbox/fixtures.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/uxbox/fixtures.clj b/backend/src/uxbox/fixtures.clj index fb9017be9f..7113d4fd79 100644 --- a/backend/src/uxbox/fixtures.clj +++ b/backend/src/uxbox/fixtures.clj @@ -64,7 +64,7 @@ :x1 200 :y1 200 :x2 1224 - :y2 968}] + :y2 968}]} :metadata {:width 1024 :height 768 :layout "tablet"} From d987e08461fe197610eeb6478ecdd9669894ef6d Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 26 Aug 2019 21:42:12 +0200 Subject: [PATCH 36/46] :recycle: Refactor page deletion. --- frontend/src/uxbox/main/data/pages.cljs | 174 ++++++++++---------- frontend/src/uxbox/main/data/projects.cljs | 90 +++------- frontend/src/uxbox/main/data/workspace.cljs | 19 +++ frontend/tools.clj | 2 +- 4 files changed, 137 insertions(+), 148 deletions(-) diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index fd9e3051dc..1da8b76d64 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -106,16 +106,16 @@ (defn purge-page "Remove page and all related stuff from the state." [state id] - (let [shapes (get state :shapes)] + (let [pid (get-in state [:pages id :project])] (-> state + (update-in [:projects pid :pages] #(filterv (partial not= id) %)) (update :pages dissoc id) (update :packed-pages dissoc id) - (assoc :shapes (reduce-kv (fn [acc k v] - (if (= (:page v) id) - (dissoc acc k) - acc)) - shapes - shapes))))) + (update :shapes (fn [shapes] (->> shapes + (map second) + (filter #(= (:page %) id)) + (map :id) + (apply dissoc shapes))))))) (defn assoc-packed-page [state {:keys [id] :as page}] @@ -125,6 +125,8 @@ [state id] (update state :packed-pages dissoc id)) + + ;; --- Pages Fetched (deftype PagesFetched [id pages] @@ -138,8 +140,6 @@ page-ids (into [] (map :id) pages)] (as-> state $ (assoc-in $ [:projects id :pages] page-ids) - ;; TODO: this is a workaround - (assoc-in $ [:projects id :page-id] (first page-ids)) (reduce unpack-page $ pages) (reduce assoc-packed-page $ pages))))) @@ -168,18 +168,19 @@ ;; --- Page Created -(declare reorder-pages) +(declare rehash-pages) (deftype PageCreated [data] ptk/UpdateEvent (update [_ state] - (-> state - (unpack-page data) - (assoc-packed-page data))) + (let [project-id (:project data)] + (-> (update-in state [:projects project-id :pages] conj (:id data)) + (unpack-page data) + (assoc-packed-page data)))) ptk/WatchEvent (watch [_ state stream] - (rx/of (reorder-pages (:project data))))) + (rx/of (rehash-pages (:project data))))) (s/def ::page-created (s/keys :req-un [::id @@ -192,57 +193,66 @@ {:pre [(us/valid? ::page-created data)]} (PageCreated. data)) +(defn page-created? + [o] + (instance? PageCreated o)) + ;; --- Create Page -(deftype CreatePage [name project width height layout] - ptk/WatchEvent - (watch [this state s] - (let [params {:name name - :project project - :data {} - :metadata {:width width - :height height - :layout layout - :order -100}}] - (->> (rp/req :create/page params) - (rx/map :payload) - (rx/map page-created))))) - -(s/def ::create-page +(s/def ::create-page-params (s/keys :req-un [::name ::project ::width - ::height - ::layout])) + ::height])) (defn create-page [{:keys [name project width height layout] :as data}] - {:pre [(us/valid? ::create-page data)]} - (CreatePage. name project width height layout)) + {:pre [(us/valid? ::create-page-params data)]} + (reify + 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} + params {:name name + :project project + :data {:shapes [canvas]} + :metadata metadata}] + (->> (rp/req :create/page params) + (rx/map :payload) + (rx/map page-created)))))) ;; --- Page Persisted -(deftype PagePersisted [data] - 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] - (instance? PagePersisted event)) - ;; TODO: add page spec (defn page-persisted [data] {:pre [(map? data)]} - (PagePersisted. data)) + (reify + cljs.core/IDeref + (-deref [_] data) + + ptk/EventType + (type [_] ::page-persisted) + + 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 @@ -333,28 +343,27 @@ {:pre [(uuid? id) (us/valid? ::metadata metadata)]} (UpdateMetadata. id metadata)) -;; --- Reorder Pages +;; --- Rehash Pages ;; ;; A post processing event that normalizes the ;; page order numbers after a user sorting ;; operation for a concrete project. -(defn reorder-pages - [project-id] - {:pre [(uuid? project-id)]} +(defn rehash-pages + [id] + {:pre [(uuid? id)]} (reify ptk/UpdateEvent (update [this state] - (let [page-ids (get-in state [:projects project-id :pages])] + (let [page-ids (get-in state [:projects id :pages])] (reduce (fn [state [index id]] (assoc-in state [:pages id :metadata :order] index)) - ;; TODO: this is workaround - (assoc-in state [:projects project-id :page-id] (first page-ids)) + state (map-indexed vector page-ids)))) ptk/WatchEvent (watch [_ state stream] - (let [page-ids (get-in state [:projects project-id :pages])] + (let [page-ids (get-in state [:projects id :pages])] (->> (rx/from-coll page-ids) (rx/map persist-metadata)))))) @@ -376,39 +385,36 @@ ;; A specialized event for persist data ;; from the update page form. -(deftype PersistPageUpdateForm [id name width height layout] - ptk/WatchEvent - (watch [_ state stream] - (let [page (-> (get-in state [:pages id]) - (assoc-in [:name] name) - (assoc-in [:metadata :width] width) - (assoc-in [:metadata :height] height) - (assoc-in [:metadata :layout] layout))] - (rx/of (update-page id page))))) - -(s/def ::persist-page-update-form - (s/keys :req-un [::name ::width ::height ::layout])) +(s/def ::persist-page-update-form-params + (s/keys :req-un [::id ::name ::width ::height])) (defn persist-page-update-form - [id {:keys [name width height layout] :as data}] - {:pre [(uuid? id) (us/valid? ::persist-page-update-form data)]} - (PersistPageUpdateForm. id name width height layout)) + [{:keys [id name width height] :as data}] + {:pre [(us/valid? ::persist-page-update-form-params data)]} + (reify + ptk/WatchEvent + (watch [_ state stream] + (let [page (-> (get-in state [:pages id]) + (assoc-in [:name] name) + (assoc-in [:metadata :width] width) + (assoc-in [:metadata :height] height))] + (rx/of (update-page id page)))))) + ;; --- Delete Page (by id) -(deftype DeletePage [id callback] - ptk/WatchEvent - (watch [_ state s] - (letfn [(on-success [_] - #(purge-page % id))] - (->> (rp/req :delete/page id) - (rx/map on-success) - (rx/tap callback) - (rx/filter identity))))) - (defn delete-page - ([id] (DeletePage. id (constantly nil))) - ([id callback] (DeletePage. id callback))) + [id] + {:pre [(uuid? id)]} + (reify + ptk/UpdateEvent + (update [_ state] + (purge-page state id)) + + ptk/WatchEvent + (watch [_ state s] + (->> (rp/req :delete/page id) + (rx/map (constantly ::delete-completed)))))) ;; --- Watch Page Changes diff --git a/frontend/src/uxbox/main/data/projects.cljs b/frontend/src/uxbox/main/data/projects.cljs index 9e24636cfa..743447167c 100644 --- a/frontend/src/uxbox/main/data/projects.cljs +++ b/frontend/src/uxbox/main/data/projects.cljs @@ -160,74 +160,38 @@ ;; --- Create Project -(defrecord CreateProject [name width height layout] - ptk/WatchEvent - (watch [this state s] - (let [project-data {:name name} - page-data {:name "Page 0" - :data {} - :metadata {:width width - :height height - :layout layout - :order 0}}] - (->> (rp/req :create/project {:name name}) - (rx/map :payload) - (rx/mapcat (fn [{:keys [id] :as project}] - (rp/req :create/page (assoc page-data :project id)))) - (rx/map #(fetch-projects)))))) - -(s/def ::create-project-event - (s/keys :req-un [::name - ::udp/width - ::udp/height - ::udp/layout])) +(s/def ::create-project-params + (s/keys :req-un [::name ::udp/width ::udp/height])) (defn create-project - [data] - {:pre [(us/valid? ::create-project-event data)]} - (map->CreateProject data)) + [{:keys [name] :as params}] + {:pre [(us/valid? ::create-project-params params)]} + (reify + ptk/WatchEvent + (watch [this state stream] + (rx/merge + (->> (rp/req :create/project {:name name}) + (rx/map :payload) + (rx/map (fn [{:keys [id] :as project}] + (udp/create-page (assoc params :project id))))) + (->> stream + (rx/filter udp/page-created?) + (rx/take 1) + (rx/map #(fetch-projects))))))) -;; --- Go To & Go To Page - -(deftype GoToFirstPage [pages] - ptk/WatchEvent - (watch [_ state stream] - (let [[page & rest] (sort-by #(get-in % [:metadata :order]) pages) - params {:project (:project page) - :page (:id page)}] - (rx/of (rt/navigate :workspace/page params))))) - -(defn go-to-first-page - [pages] - (GoToFirstPage. pages)) - -(defrecord GoTo [project-id] - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (get-in state [:projects project-id :page-id])] - (if-not page-id - (rx/empty) - (let [params {:project project-id - :page page-id}] - (rx/of (rt/navigate :workspace/page params))))))) - -(defrecord GoToPage [project-id page-id] - ptk/WatchEvent - (watch [_ state s] - (let [params {:project project-id - :page page-id}] - (rx/of (rt/navigate :workspace/page params))))) +;; --- Go To Project (defn go-to - "A shortcut event that redirects the user to the - first page of the project." - ([project-id] - {:pre [(uuid? project-id)]} - (GoTo. project-id)) - ([project-id page-id] - {:pre [(uuid? project-id) - (uuid? page-id)]} - (GoToPage. project-id page-id))) + [id] + {:pre [(uuid? id)]} + (reify + ptk/WatchEvent + (watch [_ state stream] + (let [[page & rest-pages] (get-in state [:projects id :pages])] + (when page + (let [params {:project id :page page}] + (rx/of (rt/nav :workspace/page params)))))))) + ;; --- Update Opts (Filtering & Ordering) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index befb70eb1f..e21f18582f 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -899,6 +899,25 @@ {:pre [(uuid? id)]} (UnlockShape. id)) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Pages +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn delete-page + [id] + {:pre [(uuid? id)]} + (reify + 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))))))) + + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Selection Rect IMPL ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/tools.clj b/frontend/tools.clj index b2554774a9..9e5a3e84b6 100644 --- a/frontend/tools.clj +++ b/frontend/tools.clj @@ -158,7 +158,7 @@ (def figwheel-options {:open-url false :pprint-config false - :load-warninged-code true + :load-warninged-code false :auto-testing false :css-dirs ["resources/public/css"] :ring-server-options {:port 3449 :host "0.0.0.0"} From ea3e5d14cdc2b767e888be6f5fc74ee3a441d0ca Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 26 Aug 2019 21:42:57 +0200 Subject: [PATCH 37/46] :fire: Remove unused requires. --- frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs index c8aba12056..5b73e6b62f 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap.cljs @@ -7,8 +7,6 @@ (ns uxbox.main.ui.workspace.sidebar.sitemap (:require - [potok.core :as ptk] - [beicon.core :as rx] [cuerdas.core :as str] [lentes.core :as l] [rumext.alpha :as mf] From 88cf5483c81fc9950fde1cf87ffe046df1a2fccd Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 28 Aug 2019 19:00:29 +0200 Subject: [PATCH 38/46] :arrow_up: Update struct dependency. --- backend/deps.edn | 2 +- backend/src/uxbox/http/middleware.clj | 2 +- frontend/deps.edn | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/deps.edn b/backend/deps.edn index e703904fa1..c350c94fd8 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -7,7 +7,7 @@ :sha "80ef2dfdb3248f94f987b476a6bc1a6cfbe5f306"} funcool/datoteka {:mvn/version "1.1.0"} - funcool/struct {:mvn/version "1.4.0"} + funcool/struct {:mvn/version "2.0.0-SNAPSHOT"} ring/ring {:mvn/version "1.7.1"} metosin/reitit-core {:mvn/version "0.3.9"} diff --git a/backend/src/uxbox/http/middleware.clj b/backend/src/uxbox/http/middleware.clj index 5bd2692cac..784b73fdca 100644 --- a/backend/src/uxbox/http/middleware.clj +++ b/backend/src/uxbox/http/middleware.clj @@ -72,7 +72,7 @@ :multipart :multipart-params (throw (ex-info "Not supported key on :parameters" {})))] (assoc acc newkey {:key key - :fn #(st/validate % spec)}))) + :fn #(st/validate spec %)}))) {} parameters)) (validate [request parameters debug] diff --git a/frontend/deps.edn b/frontend/deps.edn index 6e9fbcabd4..ab9b1dfead 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -7,7 +7,7 @@ environ/environ {:mvn/version "1.1.0"} metosin/reitit-core {:mvn/version "0.3.9"} - funcool/struct {:mvn/version "1.4.0"} + funcool/struct {:mvn/version "2.0.0-SNAPSHOT"} funcool/beicon {:mvn/version "5.1.0"} funcool/cuerdas {:mvn/version "2.2.0"} funcool/lentes {:mvn/version "1.3.0-SNAPSHOT"} From 08bd135d55ccf329914c752bd55827b2bbaf8382 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 28 Aug 2019 19:01:11 +0200 Subject: [PATCH 39/46] :recycle: Refactor user profile form. --- frontend/src/uxbox/main/data/users.cljs | 63 ++++---- frontend/src/uxbox/main/data/workspace.cljs | 2 +- .../src/uxbox/main/ui/settings/profile.cljs | 144 +++++++++--------- frontend/src/uxbox/util/forms.cljs | 47 ++++-- 4 files changed, 132 insertions(+), 124 deletions(-) diff --git a/frontend/src/uxbox/main/data/users.cljs b/frontend/src/uxbox/main/data/users.cljs index 639fbebf03..f657212c1d 100644 --- a/frontend/src/uxbox/main/data/users.cljs +++ b/frontend/src/uxbox/main/data/users.cljs @@ -17,21 +17,19 @@ ;; --- Profile Fetched -(deftype ProfileFetched [data] - ptk/UpdateEvent - (update [this state] - (assoc state :profile data)) - - ptk/EffectEvent - (effect [this state stream] - (swap! storage assoc :profile data) - ;; (prn "profile-fetched" data) - (when-let [lang (get-in data [:metadata :language])] - (i18n/set-current-locale! lang)))) - (defn profile-fetched [data] - (ProfileFetched. data)) + (reify + ptk/UpdateEvent + (update [this state] + (assoc state :profile data)) + + ptk/EffectEvent + (effect [this state stream] + (swap! storage assoc :profile data) + ;; (prn "profile-fetched" data) + (when-let [lang (get-in data [:metadata :language])] + (i18n/set-current-locale! lang))))) ;; --- Fetch Profile @@ -60,24 +58,6 @@ ;; --- Update Profile -(deftype UpdateProfile [data on-success on-error] - ptk/WatchEvent - (watch [_ state s] - (letfn [(handle-error [{payload :payload}] - (on-error payload) - (rx/empty))] - (let [data (-> (:profile state) - (assoc :fullname (:fullname data)) - (assoc :email (:email data)) - (assoc :username (:username data)) - (assoc-in [:metadata :language] (:language data)))] - (prn "update-profile" data) - (->> (rp/req :update/profile data) - (rx/map :payload) - (rx/do on-success) - (rx/map profile-updated) - (rx/catch rp/client-error? handle-error)))))) - (s/def ::fullname string?) (s/def ::email us/email?) (s/def ::username string?) @@ -90,11 +70,28 @@ ::username])) (defn update-profile - [data on-success on-error] + [data {:keys [on-success on-error]}] {:pre [(us/valid? ::update-profile data) (fn? on-error) (fn? on-success)]} - (UpdateProfile. data on-success on-error)) + (reify + ptk/WatchEvent + (watch [_ state s] + (letfn [(handle-error [{payload :payload}] + (on-error payload) + (rx/empty))] + (let [data (-> (:profile state) + (assoc :fullname (:fullname data)) + (assoc :email (:email data)) + (assoc :username (:username data)) + (assoc-in [:metadata :language] (:language data)))] + (prn "update-profile" data) + (->> (rp/req :update/profile data) + (rx/map :payload) + (rx/do on-success) + (rx/map profile-updated) + ;; (rx/map profile-fetched) + (rx/catch rp/client-error? handle-error))))))) ;; --- Update Password (Form) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index e21f18582f..c0fa4f1f2e 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -21,7 +21,7 @@ [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.workers :as uwrk] - [uxbox.util.data :refer [dissoc-in index-of seek]] + [uxbox.util.data :refer [dissoc-in index-of]] [uxbox.util.forms :as sc] [uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.point :as gpt] diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index 47e7e0b045..3d7b2fb1ad 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -20,14 +20,6 @@ [uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.interop :refer [iterable->seq]])) - -(def form-data (fm/focus-data :profile st/state)) -(def form-errors (fm/focus-errors :profile st/state)) - -(def assoc-value (partial fm/assoc-value :profile)) -(def assoc-error (partial fm/assoc-error :profile)) -(def clear-form (partial fm/clear-form :profile)) - (defn profile->form [profile] (let [language (get-in profile [:metadata :language])] @@ -35,80 +27,84 @@ (cond-> language (assoc :language language))))) (def profile-ref - (-> (comp (l/key :profile) - (l/lens profile->form)) + (-> (l/key :profile) (l/derive st/state))) -(s/def ::fullname ::fm/non-empty-string) -(s/def ::username ::fm/non-empty-string) -(s/def ::email ::fm/email) -(s/def ::language #{"en" "fr"}) - -(s/def ::profile-form - (s/keys :req-un [::fullname - ::username - ::language - ::email])) +(def profile-form-spec + {:fullname [fm/required fm/string fm/non-empty-string] + :username [fm/required fm/string fm/non-empty-string] + :email [fm/required fm/email] + :language [fm/required fm/string]}) (defn- on-error - [{:keys [code] :as payload}] - (case code - :uxbox.services.users/registration-disabled - (st/emit! (tr "errors.api.form.registration-disabled")) + [error {:keys [errors] :as form}] + (prn "on-error" error form) + (case (:code error) :uxbox.services.users/email-already-exists - (st/emit! (assoc-error :email (tr "errors.api.form.email-already-exists"))) - :uxbox.services.users/username-already-exists - (st/emit! (assoc-error :username (tr "errors.api.form.username-already-exists"))))) + (swap! form assoc-in [:errors :email] "errors.api.form.email-already-exists") -(defn- on-field-change - [event field] - (let [value (dom/event->value event)] - (st/emit! (assoc-value field value)))) + :uxbox.services.users/username-already-exists + (swap! form assoc-in [:errors :username] "errors.api.form.username-already-exists"))) + +(defn- initial-data + [] + (merge {:language @i18n/locale} + (profile->form (deref profile-ref)))) + +(defn- on-submit + [event form] + (dom/prevent-default event) + (let [data (:clean-data form) + opts {:on-success #(prn "On Success" %) + :on-error #(on-error % form)}] + (st/emit! (udu/update-profile data opts)))) ;; --- Profile Form -(mf/def profile-form - :mixins [mf/memo mf/reactive (fm/clear-mixin st/store :profile)] - :render - (fn [own props] - (let [data (merge {:language @i18n/locale} - (mf/react profile-ref) - (mf/react form-data)) - errors (mf/react form-errors) - valid? (fm/valid? ::profile-form data) - on-success #(st/emit! (clear-form)) - on-submit #(st/emit! (udu/update-profile data on-success on-error))] - [:form.profile-form - [:span.user-settings-label (tr "settings.profile.section-basic-data")] - [:input.input-text - {:type "text" - :on-change #(on-field-change % :fullname) - :value (:fullname data "") - :placeholder (tr "settings.profile.your-name")}] - [:input.input-text - {:type "text" - :on-change #(on-field-change % :username) - :value (:username data "") - :placeholder (tr "settings.profile.your-username")}] - (fm/input-error errors :username) - [:input.input-text - {:type "email" - :on-change #(on-field-change % :email) - :value (:email data "") - :placeholder (tr "settings.profile.your-email")}] - (fm/input-error errors :email) +(mf/defc profile-form + [props] + (let [{:keys [data] :as form} (fm/use-form {:initial initial-data + :spec profile-form-spec})] + [:form.profile-form {:on-submit #(on-submit % form)} + [:span.user-settings-label (tr "settings.profile.section-basic-data")] + [:input.input-text + {:type "text" + :name "fullname" + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:fullname data "") + :placeholder (tr "settings.profile.your-name")}] + [:& fm/error-input {:form form :field :fullname}] + [:input.input-text + {:type "text" + :name "username" + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:username data "") + :placeholder (tr "settings.profile.your-username")}] + [:& fm/error-input {:form form :field :username}] - [:span.user-settings-label (tr "settings.profile.section-i18n-data")] - [:select.input-select {:value (:language data) - :on-change #(on-field-change % :language)} - [:option {:value "en"} "English"] - [:option {:value "fr"} "Français"]] + [:input.input-text + {:type "email" + :name "email" + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :value (:email data "") + :placeholder (tr "settings.profile.your-email")}] + [:& fm/error-input {:form form :field :email}] - [:input.btn-primary - {:type "button" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :on-click on-submit - :value (tr "settings.update-settings")}]]))) + [:span.user-settings-label (tr "settings.profile.section-i18n-data")] + [:select.input-select {:value (:language data) + :name "language" + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form)} + [:option {:value "en"} "English"] + [:option {:value "fr"} "Français"]] + + [:input.btn-primary + {:type "submit" + :class (when-not (:valid form) "btn-disabled") + :disabled (not (:valid form)) + :value (tr "settings.update-settings")}]])) ;; --- Profile Photo Form @@ -121,7 +117,7 @@ (first))] (st/emit! (udu/update-photo file)) (dom/clean-value! target)))] - (let [{:keys [photo]} (mf/deref profile-ref) + (let [{:keys [photo] :as profile} (mf/deref profile-ref) photo (if (or (str/empty? photo) (nil? photo)) "images/avatar.jpg" photo)] @@ -139,4 +135,4 @@ [:section.user-settings-content [:span.user-settings-label (tr "settings.profile.your-avatar")] [:& profile-photo-form] - (profile-form)]]) + [:& profile-form]]]) diff --git a/frontend/src/uxbox/util/forms.cljs b/frontend/src/uxbox/util/forms.cljs index 5d2427c661..4034792371 100644 --- a/frontend/src/uxbox/util/forms.cljs +++ b/frontend/src/uxbox/util/forms.cljs @@ -44,22 +44,23 @@ ([self f x y] (update-fn #(f % x y))) ([self f x y more] (update-fn #(apply f % x y more)))))) +(defn- simplify-errors + [errors] + (reduce-kv #(assoc %1 %2 (:message %3)) {} errors)) + (defn use-form [{:keys [initial spec] :as opts}] - (let [[data update-data] (mf/useState initial) - [errors update-errors] (mf/useState nil) - [touched update-touched] (mf/useState {}) - [errors' clean-data] (validate data spec) - - data (impl-mutator data update-data) - errors (-> (merge {} errors' errors) - (impl-mutator update-errors)) - touched (impl-mutator touched update-touched)] - {:clean-data clean-data - :touched touched - :data data - :errors errors - :valid (not (seq errors))})) + (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) + :errors {} + :touched {}}) + [errors' clean-data] (validate spec (:data state)) + errors (merge (reduce-kv #(assoc %1 %2 (:message %3)) {} errors') + (:errors state))] + (-> (assoc state + :errors errors + :clean-data clean-data + :valid (not (seq errors))) + (impl-mutator update-state)))) (defn on-input-change [{:keys [data] :as form}] @@ -67,7 +68,10 @@ (let [target (dom/get-target event) field (keyword (.-name target)) value (dom/get-value target)] - (swap! data assoc field value)))) + (swap! form (fn [state] + (-> state + (assoc-in [:data field] value) + (update :errors dissoc field))))))) (defn on-input-blur [{:keys [touched] :as form}] @@ -75,7 +79,18 @@ (let [target (dom/get-target event) field (keyword (.-name target))] (when-not (get touched field) - (swap! touched assoc field true))))) + (swap! form assoc-in [:touched field] true))))) + +;; --- Helper Components + +(mf/defc error-input + [{:keys [form field] :as props}] + (let [touched? (get-in form [:touched field]) + error? (get-in form [:errors field])] + (when (and touched? error?) + [:ul.form-errors + [:li {:key error?} (tr error?)]]))) + ;; --- Additional Validators From ae2d8330cae2cdfa10f450074f26ad4bac5f07ba Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 28 Aug 2019 19:41:11 +0200 Subject: [PATCH 40/46] :construction: Initial work on password form. --- frontend/src/uxbox/main/data/users.cljs | 19 +- .../src/uxbox/main/ui/settings/password.cljs | 167 +++++++++--------- .../src/uxbox/main/ui/settings/profile.cljs | 6 +- frontend/src/uxbox/util/forms.cljs | 42 +++-- 4 files changed, 129 insertions(+), 105 deletions(-) diff --git a/frontend/src/uxbox/main/data/users.cljs b/frontend/src/uxbox/main/data/users.cljs index f657212c1d..f2840287a2 100644 --- a/frontend/src/uxbox/main/data/users.cljs +++ b/frontend/src/uxbox/main/data/users.cljs @@ -8,6 +8,7 @@ (:require [beicon.core :as rx] [cljs.spec.alpha :as s] + [struct.core :as stt] [potok.core :as ptk] [uxbox.main.repo :as rp] [uxbox.util.i18n :as i18n :refer (tr)] @@ -58,20 +59,15 @@ ;; --- Update Profile -(s/def ::fullname string?) -(s/def ::email us/email?) -(s/def ::username string?) -(s/def ::language string?) - -(s/def ::update-profile - (s/keys :req-un [::fullname - ::email - ::language - ::username])) +(stt/defs update-profile-spec + {:fullname [stt/required stt/string] + :email [stt/required stt/email] + :username [stt/required stt/string] + :language [stt/required stt/string]}) (defn update-profile [data {:keys [on-success on-error]}] - {:pre [(us/valid? ::update-profile data) + {:pre [(stt/valid? update-profile-spec data) (fn? on-error) (fn? on-success)]} (reify @@ -85,7 +81,6 @@ (assoc :email (:email data)) (assoc :username (:username data)) (assoc-in [:metadata :language] (:language data)))] - (prn "update-profile" data) (->> (rp/req :update/profile data) (rx/map :payload) (rx/do on-success) diff --git a/frontend/src/uxbox/main/ui/settings/password.cljs b/frontend/src/uxbox/main/ui/settings/password.cljs index 8eaf2eb06b..f6a014435d 100644 --- a/frontend/src/uxbox/main/ui/settings/password.cljs +++ b/frontend/src/uxbox/main/ui/settings/password.cljs @@ -6,95 +6,102 @@ ;; Copyright (c) 2016-2017 Juan de la Cruz (ns uxbox.main.ui.settings.password - (:require [cljs.spec.alpha :as s :include-macros true] - [lentes.core :as l] - [cuerdas.core :as str] - [potok.core :as ptk] - [uxbox.main.store :as st] - [uxbox.main.data.users :as udu] - [uxbox.builtins.icons :as i] - [uxbox.main.ui.messages :refer [messages-widget]] - [uxbox.main.ui.settings.header :refer [header]] - [uxbox.util.i18n :refer [tr]] - [uxbox.util.forms :as fm] - [uxbox.util.dom :as dom] - [uxbox.util.messages :as um] - [rumext.core :as mx :include-macros true])) + (:require + [cuerdas.core :as str] + [lentes.core :as l] + [rumext.alpha :as mf] + [struct.core :as stt] + [uxbox.builtins.icons :as i] + [uxbox.main.data.users :as udu] + [uxbox.main.store :as st] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] + [uxbox.util.i18n :refer [tr]] + [uxbox.util.messages :as um])) -(def form-data (fm/focus-data :profile-password st/state)) -(def form-errors (fm/focus-errors :profile-password st/state)) +(stt/defs password-form-spec + {:password-1 [stt/required stt/string] + :password-2 [stt/required stt/string] + :password-old [stt/required stt/string]}) -(def assoc-value (partial fm/assoc-value :profile-password)) -(def assoc-error (partial fm/assoc-error :profile-password)) -(def clear-form (partial fm/clear-form :profile-password)) +(defn- on-submit + [event form] + (dom/prevent-default event) + (prn "on-submit" form) + #_(let [data (:clean-data form) + opts {:on-success #(prn "On Success" %) + :on-error #(on-error % form)}] + (st/emit! (udu/update-profile data opts)))) -;; TODO: add better password validation -(s/def ::password-1 ::fm/non-empty-string) -(s/def ::password-2 ::fm/non-empty-string) -(s/def ::password-old ::fm/non-empty-string) -(s/def ::password-form - (s/keys :req-un [::password-1 - ::password-2 - ::password-old])) -(mx/defc password-form - {:mixins [mx/reactive mx/static]} - [] - (let [data (mx/react form-data) - errors (mx/react form-errors) - valid? (fm/valid? ::password-form data)] - (letfn [(on-change [field event] - (let [value (dom/event->value event)] - (st/emit! (assoc-value field value)))) - (on-success [] - (st/emit! (um/info (tr "settings.password.password-saved")))) - (on-error [{:keys [code] :as payload}] - (case code - :uxbox.services.users/old-password-not-match - (st/emit! (assoc-error :password-old (tr "settings.password.wrong-old-password"))) + ;; #_(let [data (mx/deref form-data) + ;; errors (mx/react form-errors) + ;; valid? (fm/valid? ::password-form data)] + ;; (letfn [(on-change [field event] + ;; (let [value (dom/event->value event)] + ;; (st/emit! (assoc-value field value)))) + ;; (on-success [] + ;; (st/emit! (um/info (tr "settings.password.password-saved")))) + ;; (on-error [{:keys [code] :as payload}] + ;; (case code + ;; :uxbox.services.users/old-password-not-match + ;; (st/emit! (assoc-error :password-old (tr "settings.password.wrong-old-password"))) - :else - (throw (ex-info "unexpected" {:error payload})))) - (on-submit [event] - (st/emit! (udu/update-password data - :on-success on-success - :on-error on-error)))] - [:form.password-form - [:span.user-settings-label (tr "settings.password.change-password")] - [:input.input-text - {:type "password" - :class (fm/error-class errors :password-old) - :value (:password-old data "") - :on-change (partial on-change :password-old) - :placeholder (tr "settings.password.old-password")}] - (fm/input-error errors :password-old) - [:input.input-text - {:type "password" - :class (fm/error-class errors :password-1) - :value (:password-1 data "") - :on-change (partial on-change :password-1) - :placeholder (tr "settings.password.new-password")}] - (fm/input-error errors :password-1) - [:input.input-text - {:type "password" - :class (fm/error-class errors :password-2) - :value (:password-2 data "") - :on-change (partial on-change :password-2) - :placeholder (tr "settings.password.confirm-password")}] - (fm/input-error errors :password-2) - [:input.btn-primary - {:type "button" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :on-click on-submit - :value (tr "settings.update-settings")}]]))) + ;; :else + ;; (throw (ex-info "unexpected" {:error payload})))) + ;; (on-submit [event] + ;; (st/emit! (udu/update-password data + ;; :on-success on-success + ;; :on-error on-error)))] + + + +(mf/defc password-form + [props] + (let [{:keys [data] :as form} (fm/use-form {:initial {} :spec password-form-spec})] + (prn "password-form" form) + [:form.password-form {:on-submit #(on-submit % form)} + [:span.user-settings-label (tr "settings.password.change-password")] + [:input.input-text + {:type "password" + :name "password-old" + :class (fm/error-class form :password-old) + :value (:password-old data "") + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :placeholder (tr "settings.password.old-password")}] + [:& fm/error-input {:form form :field :password-old}] + + [:input.input-text + {:type "password" + :name "password-1" + :class (fm/error-class form :password-1) + :value (:password-1 data "") + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :placeholder (tr "settings.password.new-password")}] + [:& fm/error-input {:form form :field :password-1}] + [:input.input-text + {:type "password" + :name "password-2" + :class (fm/error-class form :password-2) + :value (:password-2 data "") + :on-blur (fm/on-input-blur form) + :on-change (fm/on-input-change form) + :placeholder (tr "settings.password.confirm-password")}] + [:& fm/error-input {:form form :field :password-2}] + [:input.btn-primary + {:type "submit" + :class (when-not (:valid form) "btn-disabled") + :disabled (not (:valid form)) + :value (tr "settings.update-settings")}]])) ;; --- Password Page -(mx/defc password-page - [] +(mf/defc password-page + [props] [:section.dashboard-content.user-settings [:section.user-settings-content - (password-form)]]) + [:& password-form]]]) diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index 3d7b2fb1ad..06f83f14b1 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -41,10 +41,12 @@ (prn "on-error" error form) (case (:code error) :uxbox.services.users/email-already-exists - (swap! form assoc-in [:errors :email] "errors.api.form.email-already-exists") + (swap! form assoc-in [:errors :email] + {:message "errors.api.form.email-already-exists"}) :uxbox.services.users/username-already-exists - (swap! form assoc-in [:errors :username] "errors.api.form.username-already-exists"))) + (swap! form assoc-in [:errors :username] + {:message "errors.api.form.username-already-exists"}))) (defn- initial-data [] diff --git a/frontend/src/uxbox/util/forms.cljs b/frontend/src/uxbox/util/forms.cljs index 4034792371..b244ef450a 100644 --- a/frontend/src/uxbox/util/forms.cljs +++ b/frontend/src/uxbox/util/forms.cljs @@ -44,9 +44,29 @@ ([self f x y] (update-fn #(f % x y))) ([self f x y more] (update-fn #(apply f % x y more)))))) -(defn- simplify-errors +(defn- translate-error-type + [type] + (case type + ::stt/string "errors.should-be-string" + ::stt/number "errors.should-be-number" + ::stt/number-str "errors.should-be-number" + ::stt/integer "errors.should-be-integer" + ::stt/integer-str "errors.should-be-integer" + ::stt/required "errors.required" + ::stt/email "errors.should-be-valid-email" + ::stt/uuid "errors.should-be-uuid" + ::stt/uuid-str "errors.should-be-valid-uuid" + "errors.undefined-error")) + +(defn- translate-errors [errors] - (reduce-kv #(assoc %1 %2 (:message %3)) {} errors)) + (reduce-kv (fn [acc key val] + (if (string? (:message val)) + (assoc acc key val) + (->> (translate-error-type (:type val)) + (assoc val :message) + (assoc acc key)))) + {} errors)) (defn use-form [{:keys [initial spec] :as opts}] @@ -54,7 +74,7 @@ :errors {} :touched {}}) [errors' clean-data] (validate spec (:data state)) - errors (merge (reduce-kv #(assoc %1 %2 (:message %3)) {} errors') + errors (merge (translate-errors errors') (:errors state))] (-> (assoc state :errors errors @@ -86,11 +106,16 @@ (mf/defc error-input [{:keys [form field] :as props}] (let [touched? (get-in form [:touched field]) - error? (get-in form [:errors field])] - (when (and touched? error?) + error (get-in form [:errors field])] + (when (and touched? error) [:ul.form-errors - [:li {:key error?} (tr error?)]]))) + [:li {:key (:type error)} (tr (:message error))]]))) +(defn error-class + [form field] + (when (and (get-in form [:errors field]) + (get-in form [:touched field])) + "invalid")) ;; --- Additional Validators @@ -298,11 +323,6 @@ [:ul.form-errors [:li {:key error} (tr error)]])) -(defn error-class - [errors field] - (when (get errors field) - "invalid")) - (defn clear-mixin [store type] {:will-unmount (fn [own] From 04a5038ff40cc845d761275ffce62fdda4d9c23f Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Wed, 28 Aug 2019 20:11:41 +0200 Subject: [PATCH 41/46] :construction: More work on forms refactoring. --- frontend/src/uxbox/main/data/users.cljs | 58 +++++----- frontend/src/uxbox/main/ui/auth/login.cljs | 23 ++-- .../main/ui/dashboard/projects_forms.cljs | 14 +-- .../src/uxbox/main/ui/settings/password.cljs | 84 ++++++-------- .../src/uxbox/main/ui/settings/profile.cljs | 54 +++++---- .../ui/workspace/sidebar/sitemap_forms.cljs | 14 +-- frontend/src/uxbox/util/forms.cljs | 106 +++++++----------- 7 files changed, 161 insertions(+), 192 deletions(-) diff --git a/frontend/src/uxbox/main/data/users.cljs b/frontend/src/uxbox/main/data/users.cljs index f2840287a2..a00abc3554 100644 --- a/frontend/src/uxbox/main/data/users.cljs +++ b/frontend/src/uxbox/main/data/users.cljs @@ -7,11 +7,10 @@ (ns uxbox.main.data.users (:require [beicon.core :as rx] - [cljs.spec.alpha :as s] - [struct.core :as stt] + [struct.core :as s] [potok.core :as ptk] [uxbox.main.repo :as rp] - [uxbox.util.i18n :as i18n :refer (tr)] + [uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.messages :as uum] [uxbox.util.spec :as us] [uxbox.util.storage :refer [storage]])) @@ -59,15 +58,15 @@ ;; --- Update Profile -(stt/defs update-profile-spec - {:fullname [stt/required stt/string] - :email [stt/required stt/email] - :username [stt/required stt/string] - :language [stt/required stt/string]}) +(s/defs update-profile-spec + {:fullname [s/required s/string] + :email [s/required s/email] + :username [s/required s/string] + :language [s/required s/string]}) (defn update-profile [data {:keys [on-success on-error]}] - {:pre [(stt/valid? update-profile-spec data) + {:pre [(s/valid? update-profile-spec data) (fn? on-error) (fn? on-success)]} (reify @@ -90,33 +89,28 @@ ;; --- Update Password (Form) -(deftype UpdatePassword [data on-success on-error] - ptk/WatchEvent - (watch [_ state s] - (let [params {:old-password (:password-old data) - :password (:password-1 data)}] - (->> (rp/req :update/profile-password params) - (rx/catch rp/client-error? (fn [e] - (on-error (:payload e)) - (rx/empty))) - (rx/do on-success) - (rx/ignore))))) - -(s/def ::password-1 string?) -(s/def ::password-2 string?) -(s/def ::password-old string?) - -(s/def ::update-password - (s/keys :req-un [::password-1 - ::password-2 - ::password-old])) +(s/defs update-password-spec + {:password-1 [s/required s/string] + :password-2 [s/required s/string [s/identical-to :password-1]] + :password-old [s/required s/string]}) (defn update-password - [data & {:keys [on-success on-error]}] - {:pre [(us/valid? ::update-password data) + [data {:keys [on-success on-error]}] + {:pre [(s/valid? update-password-spec data) (fn? on-success) (fn? on-error)]} - (UpdatePassword. data on-success on-error)) + (reify + ptk/WatchEvent + (watch [_ state s] + (let [params {:old-password (:password-old data) + :password (:password-1 data)}] + (->> (rp/req :update/profile-password params) + (rx/catch rp/client-error? (fn [e] + (on-error (:payload e)) + (rx/empty))) + (rx/do on-success) + (rx/ignore)))))) + ;; --- Update Photo diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs index 039bf20447..f2dfefafb9 100644 --- a/frontend/src/uxbox/main/ui/auth/login.cljs +++ b/frontend/src/uxbox/main/ui/auth/login.cljs @@ -8,6 +8,7 @@ (ns uxbox.main.ui.auth.login (:require [rumext.alpha :as mf] + [struct.core :as s] [uxbox.builtins.icons :as i] [uxbox.config :as cfg] [uxbox.main.data.auth :as da] @@ -18,9 +19,9 @@ [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as rt])) -(def login-form-spec - {:username [fm/required fm/string fm/non-empty-string] - :password [fm/required fm/string fm/non-empty-string]}) +(s/defs login-form-spec + {:username [fm/required fm/string] + :password [fm/required fm/string]}) (defn- on-submit [event form] @@ -41,8 +42,7 @@ (mf/defc login-form [] - (let [{:keys [data] :as form} (fm/use-form {:initial {} - :spec login-form-spec})] + (let [{:keys [data] :as form} (fm/use-form {:initial {} :spec login-form-spec})] [:form {:on-submit #(on-submit % form)} [:div.login-content (when cfg/isdemo @@ -52,25 +52,26 @@ {:name "username" :tab-index "2" :value (:username data "") - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :on-blur (fm/on-input-blur form :username) + :on-change (fm/on-input-change form :username) :placeholder (tr "auth.email-or-username") :type "text"}] [:input.input-text {:name "password" :tab-index "3" :value (:password data "") - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :on-blur (fm/on-input-blur form :password) + :on-change (fm/on-input-change form :password) :placeholder (tr "auth.password") :type "password"}] [:input.btn-primary {:name "login" :tab-index "4" - :class (when (:errors form) "btn-disabled") - :disabled (boolean (:errors form)) + :class (when-not (:valid form) "btn-disabled") + :disabled (not (:valid form)) :value (tr "auth.signin") :type "submit"}] + [:div.login-links [:a {:on-click #(st/emit! (rt/nav :auth/recovery-request)) :tab-index "5"} diff --git a/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs b/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs index 67e0d1b773..944bb8f18c 100644 --- a/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs @@ -18,7 +18,7 @@ [uxbox.util.i18n :as t :refer [tr]])) (def project-form-spec - {:name [fm/required fm/string fm/non-empty-string] + {:name [fm/required fm/string] :width [fm/required fm/number-str] :height [fm/required fm/number-str]}) @@ -51,8 +51,8 @@ :type "text" :name "name" :value (:name data) - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :on-blur (fm/on-input-blur form :name) + :on-change (fm/on-input-change form :name) :auto-focus true}] [:div.project-size [:div.input-element.pixels @@ -63,8 +63,8 @@ :type "number" :min 0 :max 5000 - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :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 @@ -75,8 +75,8 @@ :name "height" :min 0 :max 5000 - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :on-blur (fm/on-input-blur form :height) + :on-change (fm/on-input-change form :height) :value (:height data)}]]] ;; Submit diff --git a/frontend/src/uxbox/main/ui/settings/password.cljs b/frontend/src/uxbox/main/ui/settings/password.cljs index f6a014435d..090974ebac 100644 --- a/frontend/src/uxbox/main/ui/settings/password.cljs +++ b/frontend/src/uxbox/main/ui/settings/password.cljs @@ -2,15 +2,13 @@ ;; 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-2017 Andrey Antukh -;; Copyright (c) 2016-2017 Juan de la Cruz +;; Copyright (c) 2016-2019 Andrey Antukh +;; Copyright (c) 2016-2019 Juan de la Cruz (ns uxbox.main.ui.settings.password (:require - [cuerdas.core :as str] - [lentes.core :as l] [rumext.alpha :as mf] - [struct.core :as stt] + [struct.core :as s] [uxbox.builtins.icons :as i] [uxbox.main.data.users :as udu] [uxbox.main.store :as st] @@ -19,79 +17,65 @@ [uxbox.util.i18n :refer [tr]] [uxbox.util.messages :as um])) -(stt/defs password-form-spec - {:password-1 [stt/required stt/string] - :password-2 [stt/required stt/string] - :password-old [stt/required stt/string]}) - (defn- on-submit [event form] - (dom/prevent-default event) - (prn "on-submit" form) - #_(let [data (:clean-data form) - opts {:on-success #(prn "On Success" %) - :on-error #(on-error % form)}] - (st/emit! (udu/update-profile data opts)))) + (letfn [(on-error [error] + (case (:code error) + :uxbox.services.users/old-password-not-match + (swap! form assoc-in [:errors :password-old] + {:type ::api :message "settings.password.wrong-old-password"}) + :else (throw (ex-info "unexpected" {:error error})))) + (on-success [_] + (st/emit! (um/info (tr "settings.password.password-saved"))))] + (dom/prevent-default event) + (let [data (:clean-data form) + opts {:on-success on-success + :on-error on-error}] + (st/emit! (udu/update-password data opts))))) - ;; #_(let [data (mx/deref form-data) - ;; errors (mx/react form-errors) - ;; valid? (fm/valid? ::password-form data)] - ;; (letfn [(on-change [field event] - ;; (let [value (dom/event->value event)] - ;; (st/emit! (assoc-value field value)))) - ;; (on-success [] - ;; (st/emit! (um/info (tr "settings.password.password-saved")))) - ;; (on-error [{:keys [code] :as payload}] - ;; (case code - ;; :uxbox.services.users/old-password-not-match - ;; (st/emit! (assoc-error :password-old (tr "settings.password.wrong-old-password"))) - - ;; :else - ;; (throw (ex-info "unexpected" {:error payload})))) - ;; (on-submit [event] - ;; (st/emit! (udu/update-password data - ;; :on-success on-success - ;; :on-error on-error)))] - - +(s/defs password-form-spec + {:password-1 [s/required s/string] + :password-2 [s/required s/string [s/identical-to :password-1]] + :password-old [s/required s/string]}) (mf/defc password-form [props] (let [{:keys [data] :as form} (fm/use-form {:initial {} :spec password-form-spec})] - (prn "password-form" form) [:form.password-form {:on-submit #(on-submit % form)} [:span.user-settings-label (tr "settings.password.change-password")] [:input.input-text {:type "password" :name "password-old" - :class (fm/error-class form :password-old) :value (:password-old data "") - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :class (fm/error-class form :password-old) + :on-blur (fm/on-input-blur form :password-old) + :on-change (fm/on-input-change form :password-old) :placeholder (tr "settings.password.old-password")}] - [:& fm/error-input {:form form :field :password-old}] + [:& fm/field-error {:form form :field :password-old}] [:input.input-text {:type "password" :name "password-1" - :class (fm/error-class form :password-1) :value (:password-1 data "") - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :class (fm/error-class form :password-1) + :on-blur (fm/on-input-blur form :password-1) + :on-change (fm/on-input-change form :password-1) :placeholder (tr "settings.password.new-password")}] - [:& fm/error-input {:form form :field :password-1}] + [:& fm/field-error {:form form :field :password-1}] + [:input.input-text {:type "password" :name "password-2" - :class (fm/error-class form :password-2) :value (:password-2 data "") - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :class (fm/error-class form :password-2) + :on-blur (fm/on-input-blur form :password-2) + :on-change (fm/on-input-change form :password-2) :placeholder (tr "settings.password.confirm-password")}] - [:& fm/error-input {:form form :field :password-2}] + [:& fm/field-error {:form form :field :password-2}] + [:input.btn-primary {:type "submit" :class (when-not (:valid form) "btn-disabled") diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index 06f83f14b1..ed336db08c 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -7,15 +7,15 @@ (ns uxbox.main.ui.settings.profile (:require - [cljs.spec.alpha :as s] [cuerdas.core :as str] [lentes.core :as l] [rumext.alpha :as mf] + [struct.core :as s] [uxbox.builtins.icons :as i] [uxbox.main.data.users :as udu] [uxbox.main.store :as st] - [uxbox.util.dom :as dom] [uxbox.util.data :refer [read-string]] + [uxbox.util.dom :as dom] [uxbox.util.forms :as fm] [uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.interop :refer [iterable->seq]])) @@ -30,23 +30,25 @@ (-> (l/key :profile) (l/derive st/state))) -(def profile-form-spec - {:fullname [fm/required fm/string fm/non-empty-string] - :username [fm/required fm/string fm/non-empty-string] +(s/defs profile-form-spec + {:fullname [fm/required fm/string] + :username [fm/required fm/string] :email [fm/required fm/email] :language [fm/required fm/string]}) (defn- on-error - [error {:keys [errors] :as form}] + [error form] (prn "on-error" error form) (case (:code error) :uxbox.services.users/email-already-exists (swap! form assoc-in [:errors :email] - {:message "errors.api.form.email-already-exists"}) + {:type ::api + :message "errors.api.form.email-already-exists"}) :uxbox.services.users/username-already-exists (swap! form assoc-in [:errors :username] - {:message "errors.api.form.username-already-exists"}))) + {:type ::api + :message "errors.api.form.username-already-exists"}))) (defn- initial-data [] @@ -66,39 +68,53 @@ [props] (let [{:keys [data] :as form} (fm/use-form {:initial initial-data :spec profile-form-spec})] + (prn "profile-form" form) [:form.profile-form {:on-submit #(on-submit % form)} [:span.user-settings-label (tr "settings.profile.section-basic-data")] [:input.input-text {:type "text" :name "fullname" - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :class (fm/error-class form :fullname) + :on-blur (fm/on-input-blur form :fullname) + :on-change (fm/on-input-change form :fullname) :value (:fullname data "") :placeholder (tr "settings.profile.your-name")}] - [:& fm/error-input {:form form :field :fullname}] + + [:& fm/field-error {:form form + :type #{::api} + :field :fullname}] [:input.input-text {:type "text" :name "username" - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :class (fm/error-class form :username) + :on-blur (fm/on-input-blur form :username) + :on-change (fm/on-input-change form :username) :value (:username data "") :placeholder (tr "settings.profile.your-username")}] - [:& fm/error-input {:form form :field :username}] + + [:& fm/field-error {:form form + :type #{::api} + :field :username}] [:input.input-text {:type "email" :name "email" - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :class (fm/error-class form :email) + :on-blur (fm/on-input-blur form :email) + :on-change (fm/on-input-change form :email) :value (:email data "") :placeholder (tr "settings.profile.your-email")}] - [:& fm/error-input {:form form :field :email}] + + [:& fm/field-error {:form form + :type #{::api} + :field :email}] [:span.user-settings-label (tr "settings.profile.section-i18n-data")] [:select.input-select {:value (:language data) :name "language" - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form)} + :class (fm/error-class form :language) + :on-blur (fm/on-input-blur form :language) + :on-change (fm/on-input-change form :language)} [:option {:value "en"} "English"] [:option {:value "fr"} "Français"]] 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 d01c3b0b39..408c58b447 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs @@ -20,7 +20,7 @@ (def page-form-spec {:id [fm/uuid] :project [fm/uuid] - :name [fm/required fm/string fm/non-empty-string] + :name [fm/required fm/string] :width [fm/required fm/number-str] :height [fm/required fm/number-str]}) @@ -59,8 +59,8 @@ {:placeholder "Page name" :type "text" :name "name" - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :on-blur (fm/on-input-blur form :name) + :on-change (fm/on-input-change form :name) :value (:name data) :auto-focus true}] [:div.project-size @@ -72,8 +72,8 @@ :type "number" :min 0 :max 5000 - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :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 @@ -84,8 +84,8 @@ :type "number" :min 0 :max 5000 - :on-blur (fm/on-input-blur form) - :on-change (fm/on-input-change form) + :on-blur (fm/on-input-blur form :height) + :on-change (fm/on-input-change form :height) :value (:height data)}]]] [:input.btn-primary {:value "Go go go!" diff --git a/frontend/src/uxbox/util/forms.cljs b/frontend/src/uxbox/util/forms.cljs index b244ef450a..90d9a4d735 100644 --- a/frontend/src/uxbox/util/forms.cljs +++ b/frontend/src/uxbox/util/forms.cljs @@ -14,7 +14,7 @@ [potok.core :as ptk] [rumext.alpha :as mf] [rumext.core :as mx] - [struct.core :as stt] + [struct.core :as st] [uxbox.util.dom :as dom] [uxbox.util.i18n :refer [tr]])) @@ -22,11 +22,11 @@ (defn validate [data spec] - (stt/validate data spec)) + (st/validate data spec)) (defn valid? [data spec] - (stt/valid? data spec)) + (st/valid? data spec)) ;; --- Handlers Helpers @@ -45,17 +45,16 @@ ([self f x y more] (update-fn #(apply f % x y more)))))) (defn- translate-error-type - [type] - (case type - ::stt/string "errors.should-be-string" - ::stt/number "errors.should-be-number" - ::stt/number-str "errors.should-be-number" - ::stt/integer "errors.should-be-integer" - ::stt/integer-str "errors.should-be-integer" - ::stt/required "errors.required" - ::stt/email "errors.should-be-valid-email" - ::stt/uuid "errors.should-be-uuid" - ::stt/uuid-str "errors.should-be-valid-uuid" + [code] + (case code + ::st/string "errors.form.string" + ::st/number "errors.form.number" + ::st/number-str "errors.form.number" + ::st/integer "errors.form.integer" + ::st/integer-str "errors.form.integer" + ::st/required "errors.form.required" + ::st/email "errors.form.email" + ::st/identical-to "errors.form.does-not-match" "errors.undefined-error")) (defn- translate-errors @@ -63,7 +62,7 @@ (reduce-kv (fn [acc key val] (if (string? (:message val)) (assoc acc key val) - (->> (translate-error-type (:type val)) + (->> (translate-error-type (:code val)) (assoc val :message) (assoc acc key)))) {} errors)) @@ -73,8 +72,8 @@ (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) :errors {} :touched {}}) - [errors' clean-data] (validate spec (:data state)) - errors (merge (translate-errors errors') + [errors clean-data] (validate spec (:data state)) + errors (merge (translate-errors errors) (:errors state))] (-> (assoc state :errors errors @@ -83,10 +82,9 @@ (impl-mutator update-state)))) (defn on-input-change - [{:keys [data] :as form}] + [{:keys [data] :as form} field] (fn [event] (let [target (dom/get-target event) - field (keyword (.-name target)) value (dom/get-value target)] (swap! form (fn [state] (-> state @@ -94,22 +92,28 @@ (update :errors dissoc field))))))) (defn on-input-blur - [{:keys [touched] :as form}] + [{:keys [touched] :as form} field] (fn [event] - (let [target (dom/get-target event) - field (keyword (.-name target))] + (let [target (dom/get-target event)] (when-not (get touched field) (swap! form assoc-in [:touched field] true))))) ;; --- Helper Components -(mf/defc error-input - [{:keys [form field] :as props}] +(mf/defc field-error + [{:keys [form field type] + :or {only (constantly true)} + :as props}] (let [touched? (get-in form [:touched field]) - error (get-in form [:errors field])] - (when (and touched? error) + {:keys [message code] :as error} (get-in form [:errors field])] + (when (and touched? error + (cond + (nil? type) true + (ifn? type) (type (:type error)) + (keyword? type) (= (:type error) type) + :else false)) [:ul.form-errors - [:li {:key (:type error)} (tr (:message error))]]))) + [:li {:key code} (tr message)]]))) (defn error-class [form field] @@ -119,50 +123,20 @@ ;; --- Additional Validators -(def non-empty-string - {:message "errors.empty-string" - :optional true - :validate #(not (str/empty? %))}) - -(def string (assoc stt/string :message "errors.should-be-string")) -(def number (assoc stt/number :message "errors.should-be-number")) -(def number-str (assoc stt/number-str :message "errors.should-be-number")) -(def integer (assoc stt/integer :message "errors.should-be-integer")) -(def integer-str (assoc stt/integer-str :message "errors.should-be-integer")) -(def required (assoc stt/required :message "errors.required")) -(def email (assoc stt/email :message "errors.should-be-valid-email")) -(def uuid (assoc stt/uuid :message "errors.should-be-uuid")) -(def uuid-str (assoc stt/uuid-str :message "errors.should-be-valid-uuid")) +(def string (assoc st/string :message "errors.should-be-string")) +(def number (assoc st/number :message "errors.should-be-number")) +(def number-str (assoc st/number-str :message "errors.should-be-number")) +(def integer (assoc st/integer :message "errors.should-be-integer")) +(def integer-str (assoc st/integer-str :message "errors.should-be-integer")) +(def required (assoc st/required :message "errors.required")) +(def email (assoc st/email :message "errors.should-be-valid-email")) +(def uuid (assoc st/uuid :message "errors.should-be-uuid")) +(def uuid-str (assoc st/uuid-str :message "errors.should-be-valid-uuid")) ;; DEPRECATED ;; --- Form Validation Api -(defn- interpret-problem - [acc {:keys [path pred val via in] :as problem}] - (cond - (and (empty? path) - (= (first pred) 'contains?)) - (let [path (conj path (last pred))] - (update-in acc path assoc :missing)) - - (and (seq path) - (= 1 (count path))) - (update-in acc path assoc :invalid) - - :else acc)) - -;; (defn validate -;; [spec data] -;; (when-not (s/valid? spec data) -;; (let [report (s/explain-data spec data)] -;; (reduce interpret-problem {} (::s/problems report))))) - -;; (defn valid? -;; [spec data] -;; (s/valid? spec data)) - - ;; --- Form Specs and Conformers (def ^:private email-re From 689cc5f3e71993adcd0f24281055d0a917e4ed8e Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 2 Sep 2019 20:49:48 +0200 Subject: [PATCH 42/46] :construction: More work on better forms and data validation. --- frontend/src/uxbox/main/data/auth.cljs | 261 +++++++++--------- frontend/src/uxbox/main/data/pages.cljs | 208 +++++++------- frontend/src/uxbox/main/data/projects.cljs | 57 ++-- frontend/src/uxbox/main/ui.cljs | 10 +- frontend/src/uxbox/main/ui/auth.cljs | 8 +- frontend/src/uxbox/main/ui/auth/login.cljs | 12 +- frontend/src/uxbox/main/ui/auth/recovery.cljs | 104 +++---- .../uxbox/main/ui/auth/recovery_request.cljs | 88 +++--- frontend/src/uxbox/main/ui/auth/register.cljs | 209 +++++++------- frontend/src/uxbox/main/ui/dashboard.cljs | 2 +- .../src/uxbox/main/ui/dashboard/projects.cljs | 1 - .../main/ui/dashboard/projects_forms.cljs | 15 +- frontend/src/uxbox/main/ui/messages.cljs | 21 +- frontend/src/uxbox/main/ui/settings.cljs | 2 +- .../src/uxbox/main/ui/settings/password.cljs | 12 +- .../src/uxbox/main/ui/settings/profile.cljs | 33 ++- frontend/src/uxbox/main/ui/users.cljs | 2 +- frontend/src/uxbox/main/ui/workspace.cljs | 3 +- .../ui/workspace/sidebar/sitemap_forms.cljs | 24 +- frontend/src/uxbox/util/forms.cljs | 75 ++--- frontend/src/uxbox/util/messages.cljs | 112 ++++---- 21 files changed, 641 insertions(+), 618 deletions(-) diff --git a/frontend/src/uxbox/main/data/auth.cljs b/frontend/src/uxbox/main/data/auth.cljs index eb05e6cbf5..1450dd6b99 100644 --- a/frontend/src/uxbox/main/data/auth.cljs +++ b/frontend/src/uxbox/main/data/auth.cljs @@ -5,154 +5,144 @@ ;; Copyright (c) 2015-2016 Andrey Antukh (ns uxbox.main.data.auth - (:require [cljs.spec.alpha :as s] - [beicon.core :as rx] - [potok.core :as ptk] - [uxbox.main.store :as st] - [uxbox.main.repo :as rp] - [uxbox.main.store :refer [initial-state]] - [uxbox.main.data.projects :as udp] - [uxbox.main.data.users :as udu] - [uxbox.util.messages :as uum] - [uxbox.util.router :as rt] - [uxbox.util.spec :as us] - [uxbox.util.i18n :as i18n :refer [tr]] - [uxbox.util.storage :refer [storage]])) - -(s/def ::username string?) -(s/def ::password string?) -(s/def ::fullname string?) -(s/def ::email us/email?) -(s/def ::token string?) + (:require + [struct.alpha :as st] + [beicon.core :as rx] + [potok.core :as ptk] + [uxbox.main.repo :as rp] + [uxbox.main.store :refer [initial-state]] + [uxbox.main.data.users :as du] + [uxbox.util.messages :as um] + [uxbox.util.router :as rt] + [uxbox.util.i18n :as i18n :refer [tr]] + [uxbox.util.storage :refer [storage]])) ;; --- Logged In -(defrecord LoggedIn [data] - ptk/UpdateEvent - (update [this state] - (assoc state :auth data)) - - ptk/WatchEvent - (watch [this state s] - (swap! storage assoc :auth data) - (rx/of (udu/fetch-profile) - (rt/navigate :dashboard/projects)))) - -(defn logged-in? - [v] - (instance? LoggedIn v)) +;; TODO: add spec (defn logged-in [data] - (LoggedIn. data)) + (reify + ptk/EventType + (type [_] ::logged-in) + + ptk/UpdateEvent + (update [this state] + (assoc state :auth data)) + + ptk/WatchEvent + (watch [this state s] + (swap! storage assoc :auth data) + (rx/of (du/fetch-profile) + (rt/navigate :dashboard/projects))))) + +(defn logged-in? + [v] + (= (ptk/type v) ::logged-in)) ;; --- Login -(defrecord Login [username password] - ptk/UpdateEvent - (update [_ state] - (merge state (dissoc initial-state :route :router))) - - ptk/WatchEvent - (watch [this state s] - (let [params {:username username - :password password - :scope "webapp"} - on-error #(rx/of (uum/error (tr "errors.auth.unauthorized")))] - (->> (rp/req :auth/login params) - (rx/map :payload) - (rx/map logged-in) - (rx/catch rp/client-error? on-error))))) - -(s/def ::login-event - (s/keys :req-un [::username ::password])) +(st/defs ::login + (st/dict :username ::st/string + :password ::st/string)) (defn login - [params] - {:pre [(us/valid? ::login-event params)]} - (map->Login params)) + [{:keys [username password] :as data}] + (assert (st/valid? ::login data)) + (reify + ptk/UpdateEvent + (update [_ state] + (merge state (dissoc initial-state :route :router))) + + ptk/WatchEvent + (watch [this state s] + (let [params {:username username + :password password + :scope "webapp"} + on-error #(rx/of (um/error (tr "errors.auth.unauthorized")))] + (->> (rp/req :auth/login params) + (rx/map :payload) + (rx/map logged-in) + (rx/catch rp/client-error? on-error)))))) ;; --- Logout -(defrecord ClearUserData [] - ptk/UpdateEvent - (update [_ state] - (merge state (dissoc initial-state :route :router))) +(def clear-user-data + (reify + ptk/UpdateEvent + (update [_ state] + (merge state (dissoc initial-state :route :router))) - ptk/WatchEvent - (watch [_ state s] - (->> (rp/req :auth/logout) - (rx/ignore))) + ptk/WatchEvent + (watch [_ state s] + (->> (rp/req :auth/logout) + (rx/ignore))) - ptk/EffectEvent - (effect [_ state s] - (reset! storage {}) - (i18n/set-default-locale!))) + ptk/EffectEvent + (effect [_ state s] + (reset! storage {}) + (i18n/set-default-locale!)))) -(defrecord Logout [] - ptk/WatchEvent - (watch [_ state s] - (rx/of (rt/nav :auth/login) - (->ClearUserData)))) - -(defn logout - [] - (->Logout)) +(def logout + (reify + ptk/WatchEvent + (watch [_ state s] + (rx/of (rt/nav :auth/login) + clear-user-data)))) ;; --- Register -;; TODO: clean form on success - -(defrecord Register [data on-error] - ptk/WatchEvent - (watch [_ state stream] - (letfn [(handle-error [{payload :payload}] - (on-error payload) - (rx/empty))] - (rx/merge - (->> (rp/req :auth/register data) - (rx/map :payload) - (rx/map (constantly ::registered)) - (rx/catch rp/client-error? handle-error)) - (->> stream - (rx/filter #(= % ::registered)) - (rx/take 1) - (rx/map #(login data))))))) - -(s/def ::register-event - (s/keys :req-un [::fullname ::username ::email ::password])) +(st/defs ::register + (st/dict :fullname ::st/string + :username ::st/string + :password ::st/string + :email ::st/email)) (defn register "Create a register event instance." [data on-error] - {:pre [(us/valid? ::register-event data) - (fn? on-error)]} - (Register. data on-error)) + (assert (st/valid? ::register data)) + (assert (fn? on-error)) + (reify + ptk/WatchEvent + (watch [_ state stream] + (letfn [(handle-error [{payload :payload}] + (on-error payload) + (rx/empty))] + (rx/merge + (->> (rp/req :auth/register data) + (rx/map :payload) + (rx/map (constantly ::registered)) + (rx/catch rp/client-error? handle-error)) + (->> stream + (rx/filter #(= % ::registered)) + (rx/take 1) + (rx/map #(login data)))))))) ;; --- Recovery Request -(defrecord RecoveryRequest [data] - ptk/WatchEvent - (watch [_ state stream] - (letfn [(on-error [{payload :payload}] - (println "on-error" payload) - (rx/empty))] - (rx/merge - (->> (rp/req :auth/recovery-request data) - (rx/map (constantly ::recovery-requested)) - (rx/catch rp/client-error? on-error)) - (->> stream - (rx/filter #(= % ::recovery-requested)) - (rx/take 1) - (rx/map #(uum/info (tr "auth.message.recovery-token-sent")))))))) - -(s/def ::recovery-request-event - (s/keys :req-un [::username])) +(st/defs ::recovery-request + (st/dict :username ::st/string)) (defn recovery-request [data] - {:pre [(us/valid? ::recovery-request-event data)]} - (RecoveryRequest. data)) + (assert (st/valid? ::recovery-request data)) + (reify + ptk/WatchEvent + (watch [_ state stream] + (letfn [(on-error [{payload :payload}] + (println "on-error" payload) + (rx/empty))] + (rx/merge + (->> (rp/req :auth/recovery-request data) + (rx/map (constantly ::recovery-requested)) + (rx/catch rp/client-error? on-error)) + (->> stream + (rx/filter #(= % ::recovery-requested)) + (rx/take 1) + ;; TODO: this should be moved to the UI part + (rx/map #(um/info (tr "auth.message.recovery-token-sent"))))))))) ;; --- Check Recovery Token @@ -162,7 +152,7 @@ (letfn [(on-error [{payload :payload}] (rx/of (rt/navigate :auth/login) - (uum/error (tr "errors.auth.invalid-recovery-token"))))] + (um/error (tr "errors.auth.invalid-recovery-token"))))] (->> (rp/req :auth/validate-recovery-token token) (rx/ignore) (rx/catch rp/client-error? on-error))))) @@ -174,23 +164,22 @@ ;; --- Recovery (Password) -(defrecord Recovery [token password] - ptk/WatchEvent - (watch [_ state stream] - (letfn [(on-error [{payload :payload}] - (rx/of (uum/error (tr "errors.auth.invalid-recovery-token")))) - (on-success [{payload :payload}] - (rx/of - (rt/navigate :auth/login) - (uum/info (tr "auth.message.password-recovered"))))] - (->> (rp/req :auth/recovery {:token token :password password}) - (rx/mapcat on-success) - (rx/catch rp/client-error? on-error))))) - -(s/def ::recovery-event - (s/keys :req-un [::username ::token])) +(st/defs ::recovery + (st/dict :username ::st/string + :token ::st/string)) (defn recovery [{:keys [token password] :as data}] - {:pre [(us/valid? ::recovery-event data)]} - (Recovery. token password)) + (assert (st/valid? ::recovery data)) + (reify + ptk/WatchEvent + (watch [_ state stream] + (letfn [(on-error [{payload :payload}] + (rx/of (um/error (tr "errors.auth.invalid-recovery-token")))) + (on-success [{payload :payload}] + (rx/of + (rt/navigate :auth/login) + (um/info (tr "auth.message.password-recovered"))))] + (->> (rp/req :auth/recovery {:token token :password password}) + (rx/mapcat on-success) + (rx/catch rp/client-error? on-error)))))) diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index 1da8b76d64..b1edf61c23 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -7,62 +7,65 @@ (ns uxbox.main.data.pages (:require [beicon.core :as rx] - [cljs.spec.alpha :as s] [cuerdas.core :as str] [potok.core :as ptk] + [struct.alpha :as st] [uxbox.main.repo :as rp] - [uxbox.main.store :as st] [uxbox.util.data :refer [index-by-id]] [uxbox.util.spec :as us] [uxbox.util.timers :as ts] [uxbox.util.uuid :as uuid])) -;; --- Specs +;; --- Struct -(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 number?) -(s/def ::grid-alignment boolean?) -(s/def ::width number?) -(s/def ::height number?) -(s/def ::layout string?) +(st/defs ::inst inst?) +(st/defs ::width (st/&& ::st/number ::st/positive)) +(st/defs ::height (st/&& ::st/number ::st/positive)) -(s/def ::metadata - (s/keys :req-un [::width ::height] - :opt-un [::grid-y-axis - ::grid-x-axis - ::grid-color - ::grid-alignment - ::order - ::background - ::background-opacity - ::layout])) +(st/defs ::metadata + (st/dict :width ::width + :height ::height + :grid-y-axis (st/opt ::st/number) + :grid-x-axis (st/opt ::st/number) + :grid-color (st/opt ::st/string) + :order (st/opt ::st/number) + :background (st/opt ::st/string) + :background-opacity (st/opt ::st/number))) -(s/def ::id uuid?) -(s/def ::name string?) -(s/def ::version integer?) -(s/def ::project uuid?) -(s/def ::user uuid?) -(s/def ::created-at inst?) -(s/def ::modified-at inst?) -(s/def ::shapes - (-> (s/coll-of uuid? :kind vector?) - (s/nilable))) +(st/defs ::shapes-list + (st/coll-of ::st/uuid)) -(s/def ::page-entity - (s/keys :req-un [::id - ::name - ::project - ::version - ::created-at - ::modified-at - ::user - ::metadata - ::shapes])) +(st/defs ::page-entity + (st/dict :id ::st/uuid + :name ::st/string + :project ::st/uuid + :created-at ::inst + :modified-at ::inst + :user ::st/uuid + :metadata ::metadata + :shapes ::shapes-list)) -;; TODO: add interactions to spec +(st/defs ::minimal-shape + (st/dict :id ::st/uuid + :type ::st/keyword + :name ::st/string)) + +(st/defs ::server-page-data-sapes + (st/coll-of ::minimal-shape)) + +(st/defs ::server-page-data + (st/dict :shapes ::server-page-data-sapes)) + +(st/defs ::server-page + (st/dict :id ::st/uuid + :name ::st/string + :project ::st/uuid + :version ::st/integer + :created-at ::inst + :modified-at ::inst + :user ::st/uuid + :metadata ::metadata + :data ::server-page-data)) ;; --- Protocols @@ -170,44 +173,39 @@ (declare rehash-pages) -(deftype PageCreated [data] - ptk/UpdateEvent - (update [_ state] - (let [project-id (:project data)] - (-> (update-in state [:projects project-id :pages] conj (:id data)) - (unpack-page data) - (assoc-packed-page data)))) - - ptk/WatchEvent - (watch [_ state stream] - (rx/of (rehash-pages (:project data))))) - -(s/def ::page-created - (s/keys :req-un [::id - ::name - ::project - ::metadata])) +(st/defs ::page-created + (st/dict :id ::st/uuid + :name ::st/string + :project ::st/uuid + :metadata ::metadata)) (defn page-created [data] - {:pre [(us/valid? ::page-created data)]} - (PageCreated. data)) + (assert (st/valid? ::page-created data) "invalid parameters") + (reify + ptk/UpdateEvent + (update [_ state] + (let [pid (:project data)] + (-> state + (update-in [:projects pid :pages] (fnil conj []) (:id data)) + (unpack-page data) + (assoc-packed-page data)))) -(defn page-created? - [o] - (instance? PageCreated o)) + ptk/WatchEvent + (watch [_ state stream] + (rx/of (rehash-pages (:project data)))))) ;; --- Create Page -(s/def ::create-page-params - (s/keys :req-un [::name - ::project - ::width - ::height])) +(st/defs ::create-page + (st/dict :name ::st/string + :project ::st/uuid + :width ::width + :height ::height)) (defn create-page [{:keys [name project width height layout] :as data}] - {:pre [(us/valid? ::create-page-params data)]} + (assert (st/valid? ::create-page data)) (reify ptk/WatchEvent (watch [this state s] @@ -231,11 +229,9 @@ ;; --- Page Persisted -;; TODO: add page spec - (defn page-persisted [data] - {:pre [(map? data)]} + (assert (st/valid? ::server-page data)) (reify cljs.core/IDeref (-deref [_] data) @@ -245,6 +241,7 @@ ptk/UpdateEvent (update [_ state] + (prn "page-persisted" data) (let [{:keys [id version]} data] (-> state (assoc-in [:pages id :version] version) @@ -256,30 +253,30 @@ ;; --- Persist Page -(deftype PersistPage [id on-success] - 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/do #(when (fn? on-success) - (ts/schedule-on-idle on-success))) - (rx/map page-persisted))))))) +(defn persist-page + ([id] (persist-page id identity)) + ([id on-success] + (assert (uuid? id)) + (reify + ptk/EventType + (type [_] ::persist-page) + + ptk/WatchEvent + (watch [this state s] + (prn "persist-page" id) + (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/do #(when (fn? on-success) + (ts/schedule-on-idle on-success))) + (rx/map page-persisted))))))))) (defn persist-page? [v] - (instance? PersistPage v)) - -(defn persist-page - ([id] - {:pre [(uuid? id)]} - (PersistPage. id (constantly nil))) - ([id on-success] - {:pre [(uuid? id)]} - (PersistPage. id on-success))) + (= ::persist-page (ptk/type v))) ;; --- Page Metadata Persisted @@ -288,8 +285,10 @@ (update [_ state] (assoc-in state [:pages id :version] (:version data)))) -(s/def ::metadata-persisted-event - (s/keys :req-un [::id ::version])) +(st/defs ::version integer?) +(st/defs ::metadata-persisted-event + (st/dict :id ::st/uuid + :version ::version)) (defn metadata-persisted? [v] @@ -297,7 +296,7 @@ (defn metadata-persisted [{:keys [id] :as data}] - {:pre [(us/valid? ::metadata-persisted-event data)]} + {:pre [(st/valid? ::metadata-persisted-event data)]} (MetadataPersisted. id data)) ;; --- Persist Page Metadata @@ -327,7 +326,7 @@ (defn update-page [id data] - {:pre [(uuid? id) (us/valid? ::page-entity data)]} + {:pre [(uuid? id) (st/valid? ::page-entity data)]} (UpdatePage. id data)) ;; --- Update Page Metadata @@ -340,7 +339,7 @@ (defn update-metadata [id metadata] - {:pre [(uuid? id) (us/valid? ::metadata metadata)]} + {:pre [(uuid? id) (st/valid? ::metadata metadata)]} (UpdateMetadata. id metadata)) ;; --- Rehash Pages @@ -385,12 +384,15 @@ ;; A specialized event for persist data ;; from the update page form. -(s/def ::persist-page-update-form-params - (s/keys :req-un [::id ::name ::width ::height])) +(st/defs ::persist-page-update-form + (st/dict :id ::st/uuid + :name ::st/string + :width ::width + :height ::height)) (defn persist-page-update-form [{:keys [id name width height] :as data}] - {:pre [(us/valid? ::persist-page-update-form-params data)]} + (assert (st/valid? ::persist-page-update-form data)) (reify ptk/WatchEvent (watch [_ state stream] diff --git a/frontend/src/uxbox/main/data/projects.cljs b/frontend/src/uxbox/main/data/projects.cljs index 743447167c..7c4edfa19c 100644 --- a/frontend/src/uxbox/main/data/projects.cljs +++ b/frontend/src/uxbox/main/data/projects.cljs @@ -5,17 +5,18 @@ ;; Copyright (c) 2015-2017 Andrey Antukh (ns uxbox.main.data.projects - (:require [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [beicon.core :as rx] - [potok.core :as ptk] - [uxbox.main.store :as st] - [uxbox.main.repo :as rp] - [uxbox.main.data.pages :as udp] - [uxbox.util.uuid :as uuid] - [uxbox.util.spec :as us] - [uxbox.util.time :as dt] - [uxbox.util.router :as rt])) + (:require + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [beicon.core :as rx] + [potok.core :as ptk] + [struct.core :as st] + [uxbox.main.repo :as rp] + [uxbox.main.data.pages :as udp] + [uxbox.util.uuid :as uuid] + [uxbox.util.spec :as us] + [uxbox.util.time :as dt] + [uxbox.util.router :as rt])) ;; --- Specs @@ -34,12 +35,21 @@ ::created-at ::modified-at])) +(st/defs project-spec + {:id [st/required st/uuid] + :name [st/required st/string] + :version [st/required st/integer] + :user [st/required st/uuid] + :created-at [st/required inst?] + :modified-at [st/required inst?]}) + ;; --- Helpers (defn assoc-project "A reduce function for assoc the project to the state map." [state {:keys [id] :as project}] - {:pre [(us/valid? ::project-entity project)]} + (assert (st/valid? project-spec project) + "invalid project instance") (update-in state [:projects id] merge project)) (defn dissoc-project @@ -160,24 +170,23 @@ ;; --- Create Project -(s/def ::create-project-params - (s/keys :req-un [::name ::udp/width ::udp/height])) +(st/defs create-project-spec + {:name [st/required st/string] + :width [st/required st/number st/positive] + :height [st/required st/number st/positive]}) (defn create-project [{:keys [name] :as params}] - {:pre [(us/valid? ::create-project-params params)]} + (assert (st/valid? create-project-spec params) + "invalid params for create project event") (reify ptk/WatchEvent (watch [this state stream] - (rx/merge - (->> (rp/req :create/project {:name name}) - (rx/map :payload) - (rx/map (fn [{:keys [id] :as project}] - (udp/create-page (assoc params :project id))))) - (->> stream - (rx/filter udp/page-created?) - (rx/take 1) - (rx/map #(fetch-projects))))))) + (->> (rp/req :create/project {:name name}) + (rx/map :payload) + (rx/mapcat (fn [{:keys [id] :as project}] + (rx/of #(assoc-project % project) + (udp/create-page (assoc params :project id))))))))) ;; --- Go To Project diff --git a/frontend/src/uxbox/main/ui.cljs b/frontend/src/uxbox/main/ui.cljs index 1d23450658..a570faf256 100644 --- a/frontend/src/uxbox/main/ui.cljs +++ b/frontend/src/uxbox/main/ui.cljs @@ -92,12 +92,12 @@ (let [route (mf/deref route-iref)] (case (get-in route [:data :name]) :auth/login (mf/element auth/login-page) - :auth/register (auth/register-page) - :auth/recovery-request (auth/recovery-request-page) + :auth/register (mf/element auth/register-page) + ;; :auth/recovery-request (auth/recovery-request-page) - :auth/recovery - (let [token (get-in route [:params :path :token])] - (auth/recovery-page token)) + ;; :auth/recovery + ;; (let [token (get-in route [:params :path :token])] + ;; (auth/recovery-page token)) (:settings/profile :settings/password diff --git a/frontend/src/uxbox/main/ui/auth.cljs b/frontend/src/uxbox/main/ui/auth.cljs index 8f6c840a3c..72359a9ca7 100644 --- a/frontend/src/uxbox/main/ui/auth.cljs +++ b/frontend/src/uxbox/main/ui/auth.cljs @@ -7,10 +7,10 @@ (ns uxbox.main.ui.auth (:require [uxbox.main.ui.auth.login :as login] [uxbox.main.ui.auth.register :as register] - [uxbox.main.ui.auth.recovery-request :as recovery-request] - [uxbox.main.ui.auth.recovery :as recovery])) + #_[uxbox.main.ui.auth.recovery-request :as recovery-request] + #_[uxbox.main.ui.auth.recovery :as recovery])) (def login-page login/login-page) (def register-page register/register-page) -(def recovery-page recovery/recovery-page) -(def recovery-request-page recovery-request/recovery-request-page) +;; (def recovery-page recovery/recovery-page) +;; (def recovery-request-page recovery-request/recovery-request-page) diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs index f2dfefafb9..94d995fc1a 100644 --- a/frontend/src/uxbox/main/ui/auth/login.cljs +++ b/frontend/src/uxbox/main/ui/auth/login.cljs @@ -8,7 +8,7 @@ (ns uxbox.main.ui.auth.login (:require [rumext.alpha :as mf] - [struct.core :as s] + [struct.alpha :as s] [uxbox.builtins.icons :as i] [uxbox.config :as cfg] [uxbox.main.data.auth :as da] @@ -19,9 +19,9 @@ [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as rt])) -(s/defs login-form-spec - {:username [fm/required fm/string] - :password [fm/required fm/string]}) +(s/defs ::login-form + (s/dict :username (s/&& ::s/string ::fm/not-empty-string) + :password (s/&& ::s/string ::fm/not-empty-string))) (defn- on-submit [event form] @@ -42,7 +42,7 @@ (mf/defc login-form [] - (let [{:keys [data] :as form} (fm/use-form {:initial {} :spec login-form-spec})] + (let [{:keys [data] :as form} (fm/use-form ::login-form {})] [:form {:on-submit #(on-submit % form)} [:div.login-content (when cfg/isdemo @@ -84,6 +84,6 @@ [] [:div.login [:div.login-body - (messages-widget) + [:& messages-widget] [:a i/logo] [:& login-form]]]) diff --git a/frontend/src/uxbox/main/ui/auth/recovery.cljs b/frontend/src/uxbox/main/ui/auth/recovery.cljs index d333b91e5a..c444767bb0 100644 --- a/frontend/src/uxbox/main/ui/auth/recovery.cljs +++ b/frontend/src/uxbox/main/ui/auth/recovery.cljs @@ -21,62 +21,62 @@ [uxbox.util.i18n :refer (tr)] [uxbox.util.router :as rt])) -(def form-data (fm/focus-data :recovery st/state)) -(def form-errors (fm/focus-errors :recovery st/state)) +;; (def form-data (fm/focus-data :recovery st/state)) +;; (def form-errors (fm/focus-errors :recovery st/state)) -(def assoc-value (partial fm/assoc-value :recovery)) -(def assoc-errors (partial fm/assoc-errors :recovery)) -(def clear-form (partial fm/clear-form :recovery)) +;; (def assoc-value (partial fm/assoc-value :recovery)) +;; (def assoc-errors (partial fm/assoc-errors :recovery)) +;; (def clear-form (partial fm/clear-form :recovery)) -;; --- Recovery Form +;; ;; --- Recovery Form -(s/def ::password ::fm/non-empty-string) -(s/def ::recovery-form - (s/keys :req-un [::password])) +;; (s/def ::password ::fm/non-empty-string) +;; (s/def ::recovery-form +;; (s/keys :req-un [::password])) -(mx/defc recovery-form - {:mixins [mx/static mx/reactive]} - [token] - (let [data (merge (mx/react form-data) {:token token}) - valid? (fm/valid? ::recovery-form data)] - (letfn [(on-change [field event] - (let [value (dom/event->value event)] - (st/emit! (assoc-value field value)))) - (on-submit [event] - (dom/prevent-default event) - (st/emit! (uda/recovery data) - (clear-form)))] - [:form {:on-submit on-submit} - [:div.login-content - [:input.input-text - {:name "password" - :value (:password data "") - :on-change (partial on-change :password) - :placeholder (tr "recover.password.placeholder") - :type "password"}] - [:input.btn-primary - {:name "login" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :value (tr "recover.recover-password") - :type "submit"}] - [:div.login-links - [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recover.go-back")]]]]))) +;; (mx/defc recovery-form +;; {:mixins [mx/static mx/reactive]} +;; [token] +;; (let [data (merge (mx/react form-data) {:token token}) +;; valid? (fm/valid? ::recovery-form data)] +;; (letfn [(on-change [field event] +;; (let [value (dom/event->value event)] +;; (st/emit! (assoc-value field value)))) +;; (on-submit [event] +;; (dom/prevent-default event) +;; (st/emit! (uda/recovery data) +;; (clear-form)))] +;; [:form {:on-submit on-submit} +;; [:div.login-content +;; [:input.input-text +;; {:name "password" +;; :value (:password data "") +;; :on-change (partial on-change :password) +;; :placeholder (tr "recover.password.placeholder") +;; :type "password"}] +;; [:input.btn-primary +;; {:name "login" +;; :class (when-not valid? "btn-disabled") +;; :disabled (not valid?) +;; :value (tr "recover.recover-password") +;; :type "submit"}] +;; [:div.login-links +;; [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recover.go-back")]]]]))) -;; --- Recovery Page +;; ;; --- Recovery Page -(defn- recovery-page-init - [own] - (let [[token] (::mx/args own)] - (st/emit! (uda/validate-recovery-token token)) - own)) +;; (defn- recovery-page-init +;; [own] +;; (let [[token] (::mx/args own)] +;; (st/emit! (uda/validate-recovery-token token)) +;; own)) -(mx/defc recovery-page - {:mixins [mx/static (fm/clear-mixin st/store :recovery)] - :init recovery-page-init} - [token] - [:div.login - [:div.login-body - (messages-widget) - [:a i/logo] - (recovery-form token)]]) +;; (mx/defc recovery-page +;; {:mixins [mx/static (fm/clear-mixin st/store :recovery)] +;; :init recovery-page-init} +;; [token] +;; [:div.login +;; [:div.login-body +;; (messages-widget) +;; [:a i/logo] +;; (recovery-form token)]]) diff --git a/frontend/src/uxbox/main/ui/auth/recovery_request.cljs b/frontend/src/uxbox/main/ui/auth/recovery_request.cljs index 66e37c9480..156c7df6b5 100644 --- a/frontend/src/uxbox/main/ui/auth/recovery_request.cljs +++ b/frontend/src/uxbox/main/ui/auth/recovery_request.cljs @@ -20,52 +20,52 @@ [rumext.core :as mx :include-macros true] [uxbox.util.router :as rt])) -(def form-data (fm/focus-data :recovery-request st/state)) -(def form-errors (fm/focus-errors :recovery-request st/state)) +;; (def form-data (fm/focus-data :recovery-request st/state)) +;; (def form-errors (fm/focus-errors :recovery-request st/state)) -(def assoc-value (partial fm/assoc-value :profile-password)) -(def assoc-errors (partial fm/assoc-errors :profile-password)) -(def clear-form (partial fm/clear-form :profile-password)) +;; (def assoc-value (partial fm/assoc-value :profile-password)) +;; (def assoc-errors (partial fm/assoc-errors :profile-password)) +;; (def clear-form (partial fm/clear-form :profile-password)) -(s/def ::username ::fm/non-empty-string) -(s/def ::recovery-request-form (s/keys :req-un [::username])) +;; (s/def ::username ::fm/non-empty-string) +;; (s/def ::recovery-request-form (s/keys :req-un [::username])) -(mx/defc recovery-request-form - {:mixins [mx/static mx/reactive]} - [] - (let [data (mx/react form-data) - valid? (fm/valid? ::recovery-request-form data)] - (letfn [(on-change [event] - (let [value (dom/event->value event)] - (st/emit! (assoc-value :username value)))) - (on-submit [event] - (dom/prevent-default event) - (st/emit! (uda/recovery-request data) - (clear-form)))] - [:form {:on-submit on-submit} - [:div.login-content - [:input.input-text - {:name "username" - :value (:username data "") - :on-change on-change - :placeholder (tr "recovery-request.username-or-email.placeholder") - :type "text"}] - [:input.btn-primary - {:name "login" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :value (tr "recovery-request.recover-password") - :type "submit"}] - [:div.login-links - [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recovery-request.go-back")]]]]))) +;; (mx/defc recovery-request-form +;; {:mixins [mx/static mx/reactive]} +;; [] +;; (let [data (mx/react form-data) +;; valid? (fm/valid? ::recovery-request-form data)] +;; (letfn [(on-change [event] +;; (let [value (dom/event->value event)] +;; (st/emit! (assoc-value :username value)))) +;; (on-submit [event] +;; (dom/prevent-default event) +;; (st/emit! (uda/recovery-request data) +;; (clear-form)))] +;; [:form {:on-submit on-submit} +;; [:div.login-content +;; [:input.input-text +;; {:name "username" +;; :value (:username data "") +;; :on-change on-change +;; :placeholder (tr "recovery-request.username-or-email.placeholder") +;; :type "text"}] +;; [:input.btn-primary +;; {:name "login" +;; :class (when-not valid? "btn-disabled") +;; :disabled (not valid?) +;; :value (tr "recovery-request.recover-password") +;; :type "submit"}] +;; [:div.login-links +;; [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "recovery-request.go-back")]]]]))) -;; --- Recovery Request Page +;; ;; --- Recovery Request Page -(mx/defc recovery-request-page - {:mixins [mx/static (fm/clear-mixin st/store :recovery-request)]} - [] - [:div.login - [:div.login-body - (messages-widget) - [:a i/logo] - (recovery-request-form)]]) +;; (mx/defc recovery-request-page +;; {:mixins [mx/static (fm/clear-mixin st/store :recovery-request)]} +;; [] +;; [:div.login +;; [:div.login-body +;; (messages-widget) +;; [:a i/logo] +;; (recovery-request-form)]]) diff --git a/frontend/src/uxbox/main/ui/auth/register.cljs b/frontend/src/uxbox/main/ui/auth/register.cljs index f02afe659e..261a2a98ae 100644 --- a/frontend/src/uxbox/main/ui/auth/register.cljs +++ b/frontend/src/uxbox/main/ui/auth/register.cljs @@ -6,118 +6,131 @@ ;; Copyright (c) 2015-2017 Juan de la Cruz (ns uxbox.main.ui.auth.register - (:require [cljs.spec.alpha :as s :include-macros true] - [lentes.core :as l] - [cuerdas.core :as str] - [uxbox.builtins.icons :as i] - [uxbox.main.store :as st] - [uxbox.main.data.auth :as uda] - [uxbox.main.ui.messages :refer [messages-widget]] - [uxbox.main.ui.navigation :as nav] - [uxbox.util.i18n :refer (tr)] - [uxbox.util.dom :as dom] - [uxbox.util.forms :as fm] - [rumext.core :as mx :include-macros true] - [uxbox.util.router :as rt])) + (:require + [cuerdas.core :as str] + [lentes.core :as l] + [rumext.alpha :as mf] + [struct.alpha :as s] + [uxbox.builtins.icons :as i] + [uxbox.main.data.auth :as uda] + [uxbox.main.store :as st] + [uxbox.main.ui.messages :refer [messages-widget]] + [uxbox.main.ui.navigation :as nav] + [uxbox.util.dom :as dom] + [uxbox.util.forms :as fm] + [uxbox.util.i18n :refer [tr]] + [uxbox.util.router :as rt])) -(def form-data (fm/focus-data :register st/state)) -(def form-errors (fm/focus-errors :register st/state)) +(s/defs ::register-form + (s/dict :username (s/&& ::s/string ::fm/not-empty-string) + :fullname (s/&& ::s/string ::fm/not-empty-string) + :password (s/&& ::s/string ::fm/not-empty-string) + :email ::s/email)) -(def assoc-value (partial fm/assoc-value :register)) -(def assoc-error (partial fm/assoc-error :register)) -(def clear-form (partial fm/clear-form :register)) +(defn- on-error + [error form] + (case (:code error) + :uxbox.services.users/registration-disabled + (st/emit! (tr "errors.api.form.registration-disabled")) -;; TODO: add better password validation + :uxbox.services.users/email-already-exists + (swap! form assoc-in [:errors :email] + {:type ::api + :message "errors.api.form.email-already-exists"}) -(s/def ::username ::fm/non-empty-string) -(s/def ::fullname ::fm/non-empty-string) -(s/def ::password ::fm/non-empty-string) -(s/def ::email ::fm/email) + :uxbox.services.users/username-already-exists + (swap! form assoc-in [:errors :username] + {:type ::api + :message "errors.api.form.username-already-exists"}))) -(s/def ::register-form - (s/keys :req-un [::username - ::fullname - ::email - ::password])) +(defn- on-submit + [event form] + (dom/prevent-default event) + (let [data (:clean-data form) + on-error #(on-error % form)] + (st/emit! (uda/register data on-error)))) -(mx/defc register-form - {:mixins [mx/static mx/reactive - (fm/clear-mixin st/store :register)]} - [] - (let [data (mx/react form-data) - errors (mx/react form-errors) - valid? (fm/valid? ::register-form data)] - (letfn [(on-change [field event] - (let [value (dom/event->value event)] - (st/emit! (assoc-value field value)))) - (on-error [{:keys [type code] :as payload}] - (case code - :uxbox.services.users/registration-disabled - (st/emit! (tr "errors.api.form.registration-disabled")) - :uxbox.services.users/email-already-exists - (st/emit! (assoc-error :email (tr "errors.api.form.email-already-exists"))) - :uxbox.services.users/username-already-exists - (st/emit! (assoc-error :username (tr "errors.api.form.username-already-exists"))))) - (on-submit [event] - (dom/prevent-default event) - (st/emit! (uda/register data on-error)))] - [:form {:on-submit on-submit} - [:div.login-content - [:input.input-text - {:name "fullname" - :tab-index "2" - :value (:fullname data "") - :on-change (partial on-change :fullname) - :placeholder (tr "register.fullname.placeholder") - :type "text"}] - (fm/input-error errors :fullname) +(mf/defc register-form + [props] + (let [{:keys [data] :as form} (fm/use-form ::register-form {})] + (prn "register-form" form) + [:form {:on-submit #(on-submit % form)} + [:div.login-content + [:input.input-text + {:name "fullname" + :tab-index "1" + :value (:fullname data "") + :class (fm/error-class form :fullname) + :on-blur (fm/on-input-blur form :fullname) + :on-change (fm/on-input-change form :fullname) + :placeholder (tr "register.fullname.placeholder") + :type "text"}] - [:input.input-text - {:name "username" - :tab-index "3" - :value (:username data "") - :on-change (partial on-change :username) - :placeholder (tr "register.username.placeholder") - :type "text"}] - (fm/input-error errors :username) + [:& fm/field-error {:form form + :type #{::api} + :field :fullname}] - [:input.input-text - {:name "email" - :tab-index "4" - :ref "email" - :value (:email data "") - :on-change (partial on-change :email) - :placeholder (tr "register.email.placeholder") - :type "text"}] - (fm/input-error errors :email) + [:input.input-text + {:type "text" + :name "username" + :tab-index "2" + :class (fm/error-class form :username) + :on-blur (fm/on-input-blur form :username) + :on-change (fm/on-input-change form :username) + :value (:username data "") + :placeholder (tr "settings.profile.your-username")}] - [:input.input-text - {:name "password" - :tab-index "5" - :ref "password" - :value (:password data "") - :on-change (partial on-change :password) - :placeholder (tr "register.password.placeholder") - :type "password"}] - (fm/input-error errors :password) + [:& fm/field-error {:form form + :type #{::api} + :field :username}] - [:input.btn-primary - {:name "login" - :tab-index "6" - :class (when-not valid? "btn-disabled") - :disabled (not valid?) - :value (tr "register.get-started") - :type "submit"}] - [:div.login-links - [:a {:on-click #(st/emit! (rt/navigate :auth/login))} (tr "register.already-have-account")]]]]))) + [:input.input-text + {:type "email" + :name "email" + :tab-index "3" + :class (fm/error-class form :email) + :on-blur (fm/on-input-blur form :email) + :on-change (fm/on-input-change form :email) + :value (:email data "") + :placeholder (tr "settings.profile.your-email")}] + + [:& fm/field-error {:form form + :type #{::api} + :field :email}] + + + [:input.input-text + {:name "password" + :tab-index "4" + :value (:password data "") + :class (fm/error-class form :password) + :on-blur (fm/on-input-blur form :password) + :on-change (fm/on-input-change form :password) + :placeholder (tr "register.password.placeholder") + :type "password"}] + + [:& fm/field-error {:form form + :type #{::api} + :field :email}] + + + [:input.btn-primary + {:type "submit" + :tab-index "5" + :class (when-not (:valid form) "btn-disabled") + :disabled (not (:valid form)) + :value (tr "register.get-started")}] + + [:div.login-links + [:a {:on-click #(st/emit! (rt/nav :auth/login))} + (tr "register.already-have-account")]]]])) ;; --- Register Page -(mx/defc register-page - {:mixins [mx/static]} - [own] +(mf/defc register-page + [props] [:div.login [:div.login-body (messages-widget) [:a i/logo] - (register-form)]]) + [:& register-form]]]) diff --git a/frontend/src/uxbox/main/ui/dashboard.cljs b/frontend/src/uxbox/main/ui/dashboard.cljs index 8d86362ea6..a48a821947 100644 --- a/frontend/src/uxbox/main/ui/dashboard.cljs +++ b/frontend/src/uxbox/main/ui/dashboard.cljs @@ -25,7 +25,7 @@ [{:keys [route] :as props}] (let [[section type id] (parse-route route)] [:main.dashboard-main - (messages-widget) + [:& messages-widget] [:& header {:section section}] (case section :dashboard/icons diff --git a/frontend/src/uxbox/main/ui/dashboard/projects.cljs b/frontend/src/uxbox/main/ui/dashboard/projects.cljs index 0b09c34249..f17f5badbb 100644 --- a/frontend/src/uxbox/main/ui/dashboard/projects.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/projects.cljs @@ -40,7 +40,6 @@ (-> (l/key :projects) (l/derive st/state))) - ;; --- Helpers (defn sort-projects-by diff --git a/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs b/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs index 944bb8f18c..119161ccfb 100644 --- a/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs @@ -7,7 +7,7 @@ (ns uxbox.main.ui.dashboard.projects-forms (:require - [cljs.spec.alpha :as s] + [struct.alpha :as s] [rumext.alpha :as mf] [uxbox.builtins.icons :as i] [uxbox.main.data.projects :as udp] @@ -17,10 +17,10 @@ [uxbox.util.forms :as fm] [uxbox.util.i18n :as t :refer [tr]])) -(def project-form-spec - {:name [fm/required fm/string] - :width [fm/required fm/number-str] - :height [fm/required fm/number-str]}) +(s/defs ::project-form + (s/dict :name (s/&& ::s/string ::fm/not-empty-string) + :width ::s/number-str + :height ::s/number-str)) (def defaults {:name "" @@ -44,13 +44,14 @@ (mf/defc create-project-form [props] - (let [{:keys [data errors] :as form} (fm/use-form {:initial defaults :spec project-form-spec})] + (let [{:keys [data] :as form} (fm/use-form ::project-form defaults)] [:form {:on-submit #(on-submit % form)} [:input.input-text {:placeholder "New project name" :type "text" :name "name" :value (:name data) + :class (fm/error-class form :name) :on-blur (fm/on-input-blur form :name) :on-change (fm/on-input-change form :name) :auto-focus true}] @@ -63,6 +64,7 @@ :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)}]] @@ -75,6 +77,7 @@ :name "height" :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)}]]] diff --git a/frontend/src/uxbox/main/ui/messages.cljs b/frontend/src/uxbox/main/ui/messages.cljs index ae72ed2694..6b2e263a8b 100644 --- a/frontend/src/uxbox/main/ui/messages.cljs +++ b/frontend/src/uxbox/main/ui/messages.cljs @@ -1,16 +1,17 @@ (ns uxbox.main.ui.messages - (:require [lentes.core :as l] - [uxbox.main.store :as st] - [uxbox.util.messages :as uum] - [rumext.core :as mx :include-macros true])) + (:require + [lentes.core :as l] + [rumext.alpha :as mf] + [uxbox.main.store :as st] + [uxbox.util.messages :as um])) -(def ^:private message-ref +(def ^:private message-iref (-> (l/key :message) (l/derive st/state))) -(mx/defc messages-widget - {:mixins [mx/static mx/reactive]} +(mf/defc messages-widget [] - (let [message (mx/react message-ref) - on-close #(st/emit! (uum/hide))] - (uum/messages-widget (assoc message :on-close on-close)))) + (let [message (mf/deref message-iref) + on-close #(st/emit! (um/hide))] + [:& um/messages-widget {:message message + :on-close on-close}])) diff --git a/frontend/src/uxbox/main/ui/settings.cljs b/frontend/src/uxbox/main/ui/settings.cljs index 3da1a4f6b5..62b4073d9e 100644 --- a/frontend/src/uxbox/main/ui/settings.cljs +++ b/frontend/src/uxbox/main/ui/settings.cljs @@ -22,7 +22,7 @@ [{:keys [route] :as props}] (let [section (get-in route [:data :name])] [:main.dashboard-main - (messages-widget) + [:& messages-widget] [:& header {:section section}] (case section :settings/profile (mf/element profile/profile-page) diff --git a/frontend/src/uxbox/main/ui/settings/password.cljs b/frontend/src/uxbox/main/ui/settings/password.cljs index 090974ebac..9737f67252 100644 --- a/frontend/src/uxbox/main/ui/settings/password.cljs +++ b/frontend/src/uxbox/main/ui/settings/password.cljs @@ -8,7 +8,7 @@ (ns uxbox.main.ui.settings.password (:require [rumext.alpha :as mf] - [struct.core :as s] + [struct.alpha :as s] [uxbox.builtins.icons :as i] [uxbox.main.data.users :as udu] [uxbox.main.store :as st] @@ -36,14 +36,14 @@ :on-error on-error}] (st/emit! (udu/update-password data opts))))) -(s/defs password-form-spec - {:password-1 [s/required s/string] - :password-2 [s/required s/string [s/identical-to :password-1]] - :password-old [s/required s/string]}) +(s/defs ::password-form + (s/dict :password-1 (s/&& ::s/string ::fm/not-empty-string) + :password-2 (s/&& ::s/string ::fm/not-empty-string) + :password-old (s/&& ::s/string ::fm/not-empty-string))) (mf/defc password-form [props] - (let [{:keys [data] :as form} (fm/use-form {:initial {} :spec password-form-spec})] + (let [{:keys [data] :as form} (fm/use-form ::password-form {})] [:form.password-form {:on-submit #(on-submit % form)} [:span.user-settings-label (tr "settings.password.change-password")] [:input.input-text diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index ed336db08c..814d36e528 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -10,7 +10,7 @@ [cuerdas.core :as str] [lentes.core :as l] [rumext.alpha :as mf] - [struct.core :as s] + [struct.alpha :as s] [uxbox.builtins.icons :as i] [uxbox.main.data.users :as udu] [uxbox.main.store :as st] @@ -18,27 +18,28 @@ [uxbox.util.dom :as dom] [uxbox.util.forms :as fm] [uxbox.util.i18n :as i18n :refer [tr]] - [uxbox.util.interop :refer [iterable->seq]])) + [uxbox.util.interop :refer [iterable->seq]] + [uxbox.util.messages :as um])) -(defn profile->form + +(defn- profile->form [profile] (let [language (get-in profile [:metadata :language])] (-> (select-keys profile [:fullname :username :email]) (cond-> language (assoc :language language))))) -(def profile-ref +(def ^:private profile-ref (-> (l/key :profile) (l/derive st/state))) -(s/defs profile-form-spec - {:fullname [fm/required fm/string] - :username [fm/required fm/string] - :email [fm/required fm/email] - :language [fm/required fm/string]}) +(s/defs ::profile-form + (s/dict :fullname (s/&& ::s/string ::fm/not-empty-string) + :username (s/&& ::s/string ::fm/not-empty-string) + :language (s/&& ::s/string ::fm/not-empty-string) + :email ::s/email)) (defn- on-error [error form] - (prn "on-error" error form) (case (:code error) :uxbox.services.users/email-already-exists (swap! form assoc-in [:errors :email] @@ -57,18 +58,20 @@ (defn- on-submit [event form] + (prn "on-submit" form) (dom/prevent-default event) (let [data (:clean-data form) - opts {:on-success #(prn "On Success" %) - :on-error #(on-error % form)}] + on-success #(st/emit! (um/info (tr "settings.profile.profile-saved"))) + on-error #(on-error % form) + opts {:on-success on-success + :on-error on-error}] (st/emit! (udu/update-profile data opts)))) ;; --- Profile Form + (mf/defc profile-form [props] - (let [{:keys [data] :as form} (fm/use-form {:initial initial-data - :spec profile-form-spec})] - (prn "profile-form" form) + (let [{:keys [data] :as form} (fm/use-form ::profile-form initial-data)] [:form.profile-form {:on-submit #(on-submit % form)} [:span.user-settings-label (tr "settings.profile.section-basic-data")] [:input.input-text diff --git a/frontend/src/uxbox/main/ui/users.cljs b/frontend/src/uxbox/main/ui/users.cljs index cd0cf39122..6283c87069 100644 --- a/frontend/src/uxbox/main/ui/users.cljs +++ b/frontend/src/uxbox/main/ui/users.cljs @@ -38,7 +38,7 @@ [:li {:on-click #(on-click % :settings/notifications)} i/mail [:span (tr "ds.user.notifications")]] - [:li {:on-click #(on-click % (da/logout))} + [:li {:on-click #(on-click % da/logout)} i/exit [:span (tr "ds.user.exit")]]])) diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index 296c6d7b7a..99a22402c5 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -9,7 +9,6 @@ (:require [beicon.core :as rx] [lentes.core :as l] - [rumext.core :as mx] [rumext.alpha :as mf] [uxbox.main.constants :as c] [uxbox.main.data.history :as udh] @@ -86,7 +85,7 @@ (mf/use-effect #(subscribe canvas page) #js [(:id page)]) [:* - (messages-widget) + [:& messages-widget] [:& header {:page page :flags flags :key (:id page)}] 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 408c58b447..8e3c410c4b 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs @@ -2,12 +2,13 @@ ;; 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-2016 Andrey Antukh -;; Copyright (c) 2015-2016 Juan de la Cruz +;; Copyright (c) 2015-2019 Andrey Antukh +;; Copyright (c) 2015-2019 Juan de la Cruz (ns uxbox.main.ui.workspace.sidebar.sitemap-forms (:require [rumext.alpha :as mf] + [struct.alpha :as s] [uxbox.builtins.icons :as i] [uxbox.main.constants :as c] [uxbox.main.data.pages :as udp] @@ -17,12 +18,12 @@ [uxbox.util.forms :as fm] [uxbox.util.i18n :refer [tr]])) -(def page-form-spec - {:id [fm/uuid] - :project [fm/uuid] - :name [fm/required fm/string] - :width [fm/required fm/number-str] - :height [fm/required fm/number-str]}) +(s/defs ::page-form + (s/dict :id (s/opt ::s/uuid) + :project ::s/uuid + :name (s/&& ::s/string ::fm/not-empty-string) + :width ::s/number-str + :height ::s/number-str)) (def defaults {:name "" @@ -52,13 +53,13 @@ (mf/defc page-form [{:keys [page] :as props}] - (let [{:keys [data errors] :as form} (fm/use-form {:initial #(initial-data page) - :spec page-form-spec})] + (let [{:keys [data] :as form} (fm/use-form ::page-form #(initial-data page))] [:form {:on-submit #(on-submit % form)} [:input.input-text {:placeholder "Page name" :type "text" :name "name" + :class (fm/error-class form :name) :on-blur (fm/on-input-blur form :name) :on-change (fm/on-input-change form :name) :value (:name data) @@ -72,6 +73,7 @@ :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)}]] @@ -84,12 +86,14 @@ :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 "Go go go!" :type "submit" + :class (when-not (:valid form) "btn-disabled") :disabled (not (:valid form))}]])) (mf/defc page-form-dialog diff --git a/frontend/src/uxbox/util/forms.cljs b/frontend/src/uxbox/util/forms.cljs index 90d9a4d735..70e5db5435 100644 --- a/frontend/src/uxbox/util/forms.cljs +++ b/frontend/src/uxbox/util/forms.cljs @@ -8,26 +8,16 @@ (:refer-clojure :exclude [uuid]) (:require [beicon.core :as rx] - [cljs.spec.alpha :as s :include-macros true] + [cljs.spec.alpha :as s] [cuerdas.core :as str] [lentes.core :as l] [potok.core :as ptk] [rumext.alpha :as mf] [rumext.core :as mx] - [struct.core :as st] + [struct.alpha :as st] [uxbox.util.dom :as dom] [uxbox.util.i18n :refer [tr]])) -;; --- Main Api - -(defn validate - [data spec] - (st/validate data spec)) - -(defn valid? - [data spec] - (st/valid? data spec)) - ;; --- Handlers Helpers (defn- impl-mutator @@ -45,8 +35,8 @@ ([self f x y more] (update-fn #(apply f % x y more)))))) (defn- translate-error-type - [code] - (case code + [name] + (case name ::st/string "errors.form.string" ::st/number "errors.form.number" ::st/number-str "errors.form.number" @@ -54,31 +44,35 @@ ::st/integer-str "errors.form.integer" ::st/required "errors.form.required" ::st/email "errors.form.email" - ::st/identical-to "errors.form.does-not-match" + ;; ::st/identical-to "errors.form.does-not-match" "errors.undefined-error")) -(defn- translate-errors +(defn- process-errors [errors] - (reduce-kv (fn [acc key val] - (if (string? (:message val)) - (assoc acc key val) - (->> (translate-error-type (:code val)) - (assoc val :message) - (assoc acc key)))) - {} errors)) + (reduce (fn [acc {:keys [path name] :as error}] + (let [message (translate-error-type name)] + (assoc-in acc path + (-> (assoc error :message message) + (dissoc :path))))) + {} errors)) (defn use-form - [{:keys [initial spec] :as opts}] + [spec initial] (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) :errors {} :touched {}}) - [errors clean-data] (validate spec (:data state)) - errors (merge (translate-errors errors) + cdata (st/conform spec (:data state)) + errors' (when (= ::st/invalid cdata) + (st/explain spec (:data state))) + + errors (merge (process-errors errors') (:errors state))] + (-> (assoc state :errors errors - :clean-data clean-data - :valid (not (seq errors))) + :clean-data (when (not= cdata ::st/invalid) cdata) + :valid (and (empty? errors) + (not= cdata ::st/invalid))) (impl-mutator update-state)))) (defn on-input-change @@ -123,15 +117,17 @@ ;; --- Additional Validators -(def string (assoc st/string :message "errors.should-be-string")) -(def number (assoc st/number :message "errors.should-be-number")) -(def number-str (assoc st/number-str :message "errors.should-be-number")) -(def integer (assoc st/integer :message "errors.should-be-integer")) -(def integer-str (assoc st/integer-str :message "errors.should-be-integer")) -(def required (assoc st/required :message "errors.required")) -(def email (assoc st/email :message "errors.should-be-valid-email")) -(def uuid (assoc st/uuid :message "errors.should-be-uuid")) -(def uuid-str (assoc st/uuid-str :message "errors.should-be-valid-uuid")) +(st/defs ::not-empty-string #(not (empty? %))) + +;; (def string (assoc st/string :message "errors.should-be-string")) +;; (def number (assoc st/number :message "errors.should-be-number")) +;; (def number-str (assoc st/number-str :message "errors.should-be-number")) +;; (def integer (assoc st/integer :message "errors.should-be-integer")) +;; (def integer-str (assoc st/integer-str :message "errors.should-be-integer")) +;; (def required (assoc st/required :message "errors.required")) +;; (def email (assoc st/email :message "errors.should-be-valid-email")) +;; (def uuid (assoc st/uuid :message "errors.should-be-uuid")) +;; (def uuid-str (assoc st/uuid-str :message "errors.should-be-valid-uuid")) ;; DEPRECATED @@ -154,6 +150,9 @@ (s/def ::non-empty-string (s/and string? #(not (str/empty? %)))) +(s/def ::not-empty #(not (str/empty? %))) + + (defn- parse-number [v] (cond @@ -167,6 +166,8 @@ (s/def ::color (s/and string? #(boolean (re-matches color-re %)))) + + ;; --- Form State Events ;; --- Assoc Error diff --git a/frontend/src/uxbox/util/messages.cljs b/frontend/src/uxbox/util/messages.cljs index e31239729f..edd1103d4b 100644 --- a/frontend/src/uxbox/util/messages.cljs +++ b/frontend/src/uxbox/util/messages.cljs @@ -6,15 +6,16 @@ (ns uxbox.util.messages "Messages notifications." - (:require [lentes.core :as l] - [cuerdas.core :as str] - [beicon.core :as rx] - [potok.core :as ptk] - [uxbox.builtins.icons :as i] - [uxbox.util.timers :as ts] - [rumext.core :as mx :include-macros true] - [uxbox.util.data :refer [classnames]] - [uxbox.util.dom :as dom])) + (:require + [beicon.core :as rx] + [cuerdas.core :as str] + [lentes.core :as l] + [potok.core :as ptk] + [rumext.alpha :as mf] + [uxbox.builtins.icons :as i] + [uxbox.util.data :refer [classnames]] + [uxbox.util.dom :as dom] + [uxbox.util.timers :as ts])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Events @@ -29,27 +30,28 @@ (declare hide) (declare show?) -(deftype Show [data] - ptk/UpdateEvent - (update [_ state] - (let [message (assoc data :state :visible)] - (assoc state :message message))) - - ptk/WatchEvent - (watch [_ state s] - (let [stoper (->> (rx/filter show? s) - (rx/take 1))] - (->> (rx/of (hide)) - (rx/delay (:timeout data)) - (rx/take-until stoper))))) - (defn show - [message] - (Show. message)) + [data] + (reify + ptk/EventType + (type [_] ::show) + + ptk/UpdateEvent + (update [_ state] + (let [message (assoc data :state :visible)] + (assoc state :message message))) + + ptk/WatchEvent + (watch [_ state s] + (let [stoper (->> (rx/filter show? s) + (rx/take 1))] + (->> (rx/of (hide)) + (rx/delay (:timeout data)) + (rx/take-until stoper)))))) (defn show? [v] - (instance? Show v)) + (= ::show (ptk/type v))) (defn error [message & {:keys [timeout] :or {timeout 3000}}] @@ -73,25 +75,25 @@ ;; --- Hide Message -(deftype Hide [^:mutable canceled?] - ptk/UpdateEvent - (update [_ state] - (update state :message - (fn [v] - (if (nil? v) - (do (set! canceled? true) nil) - (assoc v :state :hide))))) - - ptk/WatchEvent - (watch [_ state stream] - (if canceled? - (rx/empty) - (->> (rx/of #(dissoc state :message)) - (rx/delay +animation-timeout+))))) - (defn hide [] - (Hide. false)) + (let [canceled? (volatile! {})] + (reify + ptk/UpdateEvent + (update [_ state] + (update state :message + (fn [v] + (if (nil? v) + (do (vreset! canceled? true) nil) + (assoc v :state :hide))))) + + ptk/WatchEvent + (watch [_ state stream] + (if @canceled? + (rx/empty) + (->> (rx/of #(dissoc % :message)) + (rx/delay +animation-timeout+))))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; UI Components @@ -99,10 +101,10 @@ ;; --- Notification Component -(mx/defc notification-box - {:mixins [mx/static]} - [{:keys [type on-close] :as message}] - (let [classes (classnames :error (= type :error) +(mf/defc notification-box + [{:keys [message on-close] :as message}] + (let [type (:type message) + classes (classnames :error (= type :error) :info (= type :info) :hide-message (= (:state message) :hide) :quick true)] @@ -113,9 +115,8 @@ ;; --- Dialog Component -(mx/defc dialog-box - {:mixins [mx/static mx/reactive]} - [{:keys [on-accept on-cancel on-close] :as message}] +(mf/defc dialog-box + [{:keys [on-accept on-cancel on-close message] :as props}] (let [classes (classnames :info true :hide-message (= (:state message) :hide))] (letfn [(accept [event] @@ -142,11 +143,10 @@ ;; --- Main Component (entry point) -(mx/defc messages-widget - {:mixins [mx/static mx/reactive]} - [message] +(mf/defc messages-widget + [{:keys [message] :as props}] (case (:type message) - :error (notification-box message) - :info (notification-box message) - :dialog (dialog-box message) + :error (mf/element notification-box props) + :info (mf/element notification-box props) + :dialog (mf/element dialog-box props) nil)) From 2477b289e244376caa52e5612dc617ae2de38f4b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 2 Sep 2019 20:50:38 +0200 Subject: [PATCH 43/46] :fire: Remove useless prn's. --- frontend/src/uxbox/main/data/pages.cljs | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index b1edf61c23..af429d42f5 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -241,7 +241,6 @@ ptk/UpdateEvent (update [_ state] - (prn "page-persisted" data) (let [{:keys [id version]} data] (-> state (assoc-in [:pages id :version] version) @@ -263,7 +262,6 @@ ptk/WatchEvent (watch [this state s] - (prn "persist-page" id) (let [page (get-in state [:pages id])] (if (:history page) (rx/empty) From a009961a58d537f1faa4668fe8e46be13abb3707 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 9 Sep 2019 12:34:31 +0200 Subject: [PATCH 44/46] :construction: More work on data and forms validation. --- frontend/src/uxbox/main/data/auth.cljs | 43 ++-- frontend/src/uxbox/main/data/pages.cljs | 201 +++++++++--------- frontend/src/uxbox/main/data/projects.cljs | 21 +- frontend/src/uxbox/main/data/shapes.cljs | 19 +- frontend/src/uxbox/main/data/users.cljs | 41 ++-- frontend/src/uxbox/main/data/workspace.cljs | 130 ++++++----- frontend/src/uxbox/main/ui/auth/login.cljs | 25 ++- frontend/src/uxbox/main/ui/auth/register.cljs | 1 - .../src/uxbox/main/ui/settings/password.cljs | 51 ++--- .../src/uxbox/main/ui/settings/profile.cljs | 20 +- frontend/src/uxbox/main/ui/shapes/common.cljs | 2 - frontend/src/uxbox/main/ui/shapes/path.cljs | 1 - frontend/src/uxbox/util/forms2.cljs | 145 +++++++++++++ frontend/src/uxbox/util/messages.cljs | 72 +++---- frontend/src/uxbox/util/spec.cljs | 6 + 15 files changed, 464 insertions(+), 314 deletions(-) create mode 100644 frontend/src/uxbox/util/forms2.cljs diff --git a/frontend/src/uxbox/main/data/auth.cljs b/frontend/src/uxbox/main/data/auth.cljs index 1450dd6b99..591d1513db 100644 --- a/frontend/src/uxbox/main/data/auth.cljs +++ b/frontend/src/uxbox/main/data/auth.cljs @@ -6,7 +6,7 @@ (ns uxbox.main.data.auth (:require - [struct.alpha :as st] + [cljs.spec.alpha :as s] [beicon.core :as rx] [potok.core :as ptk] [uxbox.main.repo :as rp] @@ -14,9 +14,15 @@ [uxbox.main.data.users :as du] [uxbox.util.messages :as um] [uxbox.util.router :as rt] + [uxbox.util.spec :as us] [uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.storage :refer [storage]])) +(s/def ::username string?) +(s/def ::password string?) +(s/def ::fullname string?) +(s/def ::email ::us/email) + ;; --- Logged In ;; TODO: add spec @@ -43,13 +49,12 @@ ;; --- Login -(st/defs ::login - (st/dict :username ::st/string - :password ::st/string)) +(s/def ::login-params + (s/keys :req-un [::username ::password])) (defn login [{:keys [username password] :as data}] - (assert (st/valid? ::login data)) + (s/assert ::login-params data) (reify ptk/UpdateEvent (update [_ state] @@ -93,17 +98,17 @@ ;; --- Register -(st/defs ::register - (st/dict :fullname ::st/string - :username ::st/string - :password ::st/string - :email ::st/email)) +(s/def ::register-params + (s/keys :req-un [::fullname + ::username + ::password + ::email])) (defn register "Create a register event instance." [data on-error] - (assert (st/valid? ::register data)) - (assert (fn? on-error)) + (s/assert ::register-params data) + (s/assert ::us/fn on-error) (reify ptk/WatchEvent (watch [_ state stream] @@ -122,12 +127,12 @@ ;; --- Recovery Request -(st/defs ::recovery-request - (st/dict :username ::st/string)) +(s/def ::recovery-request-params + (s/keys :req-un [::username])) (defn recovery-request [data] - (assert (st/valid? ::recovery-request data)) + (s/assert ::recovery-request-params data) (reify ptk/WatchEvent (watch [_ state stream] @@ -164,13 +169,13 @@ ;; --- Recovery (Password) -(st/defs ::recovery - (st/dict :username ::st/string - :token ::st/string)) +(s/def ::token string?) +(s/def ::recovery-params + (s/keys :req-un [::username ::token])) (defn recovery [{:keys [token password] :as data}] - (assert (st/valid? ::recovery data)) + (s/assert ::recovery-params data) (reify ptk/WatchEvent (watch [_ state stream] diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index af429d42f5..8c85f7cf7d 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -6,10 +6,10 @@ (ns uxbox.main.data.pages (:require + [cljs.spec.alpha :as s] [beicon.core :as rx] [cuerdas.core :as str] [potok.core :as ptk] - [struct.alpha :as st] [uxbox.main.repo :as rp] [uxbox.util.data :refer [index-by-id]] [uxbox.util.spec :as us] @@ -18,54 +18,65 @@ ;; --- Struct -(st/defs ::inst inst?) -(st/defs ::width (st/&& ::st/number ::st/positive)) -(st/defs ::height (st/&& ::st/number ::st/positive)) +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::inst ::us/inst) +(s/def ::type ::us/keyword) +(s/def ::project ::us/uuid) +(s/def ::created-at ::us/inst) +(s/def ::modified-at ::us/inst) +(s/def ::version ::us/number) +(s/def ::width (s/and ::us/number ::us/positive)) +(s/def ::height (s/and ::us/number ::us/positive)) +(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 ::background ::us/string) +(s/def ::background-opacity ::us/number) +(s/def ::user ::us/uuid) -(st/defs ::metadata - (st/dict :width ::width - :height ::height - :grid-y-axis (st/opt ::st/number) - :grid-x-axis (st/opt ::st/number) - :grid-color (st/opt ::st/string) - :order (st/opt ::st/number) - :background (st/opt ::st/string) - :background-opacity (st/opt ::st/number))) +(s/def ::metadata + (s/keys :req-un [::width ::height] + :opt-un [::grid-y-axis + ::grid-x-axis + ::grid-color + ::order + ::background + ::background-opacity])) -(st/defs ::shapes-list - (st/coll-of ::st/uuid)) +(s/def ::shapes + (s/coll-of ::us/uuid :kind vector? :into [])) -(st/defs ::page-entity - (st/dict :id ::st/uuid - :name ::st/string - :project ::st/uuid - :created-at ::inst - :modified-at ::inst - :user ::st/uuid - :metadata ::metadata - :shapes ::shapes-list)) +(s/def ::page-entity + (s/keys :req-un [::id + ::name + ::project + ::created-at + ::modified-at + ::user + ::metadata + ::shapes])) -(st/defs ::minimal-shape - (st/dict :id ::st/uuid - :type ::st/keyword - :name ::st/string)) +(s/def ::minimal-shape + (s/keys :req-un [::type ::name] + :opt-un [::id])) -(st/defs ::server-page-data-sapes - (st/coll-of ::minimal-shape)) +(s/def :uxbox.backend/shapes + (s/coll-of ::minimal-shape :kind vector?)) -(st/defs ::server-page-data - (st/dict :shapes ::server-page-data-sapes)) +(s/def :uxbox.backend/data + (s/keys :req-un [:uxbox.backend/shapes])) -(st/defs ::server-page - (st/dict :id ::st/uuid - :name ::st/string - :project ::st/uuid - :version ::st/integer - :created-at ::inst - :modified-at ::inst - :user ::st/uuid - :metadata ::metadata - :data ::server-page-data)) +(s/def ::server-page + (s/keys :req-un [::id ::name + ::project + ::version + ::created-at + ::modified-at + ::user + ::metadata + :uxbox.backend/data])) ;; --- Protocols @@ -173,15 +184,12 @@ (declare rehash-pages) -(st/defs ::page-created - (st/dict :id ::st/uuid - :name ::st/string - :project ::st/uuid - :metadata ::metadata)) +(s/def ::page-created-params + (s/keys :req-un [::id ::name ::project ::metadata])) (defn page-created [data] - (assert (st/valid? ::page-created data) "invalid parameters") + (s/assert ::page-created-event data) (reify ptk/UpdateEvent (update [_ state] @@ -197,15 +205,12 @@ ;; --- Create Page -(st/defs ::create-page - (st/dict :name ::st/string - :project ::st/uuid - :width ::width - :height ::height)) +(s/def ::created-page-params + (s/keys :req-un [::name ::project ::width ::height])) (defn create-page [{:keys [name project width height layout] :as data}] - (assert (st/valid? ::create-page data)) + (s/assert ::created-page-params data) (reify ptk/WatchEvent (watch [this state s] @@ -231,7 +236,7 @@ (defn page-persisted [data] - (assert (st/valid? ::server-page data)) + (s/assert ::server-page data) (reify cljs.core/IDeref (-deref [_] data) @@ -278,24 +283,24 @@ ;; --- Page Metadata Persisted -(deftype MetadataPersisted [id data] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:pages id :version] (:version data)))) - -(st/defs ::version integer?) -(st/defs ::metadata-persisted-event - (st/dict :id ::st/uuid - :version ::version)) - -(defn metadata-persisted? - [v] - (instance? MetadataPersisted v)) +(s/def ::metadata-persisted-params + (s/keys :req-un [::id ::version])) (defn metadata-persisted [{:keys [id] :as data}] - {:pre [(st/valid? ::metadata-persisted-event data)]} - (MetadataPersisted. id data)) + (s/assert ::metadata-persisted-params data) + (reify + ptk/EventType + (type [_] ::metadata-persisted) + + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:pages id :version] (:version data))))) + +(defn metadata-persisted? + [v] + (= ::metadata-persisted (ptk/type v))) + ;; --- Persist Page Metadata @@ -316,29 +321,28 @@ ;; --- Update Page -(deftype UpdatePage [id data] - IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:pages id] merge (dissoc data :id :version)))) - (defn update-page [id data] - {:pre [(uuid? id) (st/valid? ::page-entity data)]} - (UpdatePage. id data)) + (s/assert ::page-entity data) + (s/assert ::id id) + (reify + IPageUpdate + ptk/UpdateEvent + (update [_ state] + (update-in state [:pages id] merge (dissoc data :id :version))))) ;; --- Update Page Metadata -(deftype UpdateMetadata [id metadata] - IMetadataUpdate - ptk/UpdateEvent - (update [this state] - (assoc-in state [:pages id :metadata] metadata))) - (defn update-metadata [id metadata] - {:pre [(uuid? id) (st/valid? ::metadata metadata)]} - (UpdateMetadata. id metadata)) + (s/assert ::id id) + (s/assert ::metadata metadata) + (reify + IMetadataUpdate + ptk/UpdateEvent + (update [this state] + (assoc-in state [:pages id :metadata] metadata)))) + ;; --- Rehash Pages ;; @@ -382,24 +386,21 @@ ;; A specialized event for persist data ;; from the update page form. -(st/defs ::persist-page-update-form - (st/dict :id ::st/uuid - :name ::st/string - :width ::width - :height ::height)) +(s/def ::persist-page-update-form-params + (s/keys :req-un [::id ::name ::width ::height])) (defn persist-page-update-form [{:keys [id name width height] :as data}] - (assert (st/valid? ::persist-page-update-form data)) + (s/assert ::persist-page-update-form-params data) (reify - ptk/WatchEvent - (watch [_ state stream] - (let [page (-> (get-in state [:pages id]) - (assoc-in [:name] name) - (assoc-in [:metadata :width] width) - (assoc-in [:metadata :height] height))] - (rx/of (update-page id 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))))))) ;; --- Delete Page (by id) diff --git a/frontend/src/uxbox/main/data/projects.cljs b/frontend/src/uxbox/main/data/projects.cljs index 7c4edfa19c..9d4bb03a3a 100644 --- a/frontend/src/uxbox/main/data/projects.cljs +++ b/frontend/src/uxbox/main/data/projects.cljs @@ -10,7 +10,6 @@ [cuerdas.core :as str] [beicon.core :as rx] [potok.core :as ptk] - [struct.core :as st] [uxbox.main.repo :as rp] [uxbox.main.data.pages :as udp] [uxbox.util.uuid :as uuid] @@ -35,21 +34,12 @@ ::created-at ::modified-at])) -(st/defs project-spec - {:id [st/required st/uuid] - :name [st/required st/string] - :version [st/required st/integer] - :user [st/required st/uuid] - :created-at [st/required inst?] - :modified-at [st/required inst?]}) - ;; --- Helpers (defn assoc-project "A reduce function for assoc the project to the state map." [state {:keys [id] :as project}] - (assert (st/valid? project-spec project) - "invalid project instance") + (s/assert ::project-entity project) (update-in state [:projects id] merge project)) (defn dissoc-project @@ -170,15 +160,12 @@ ;; --- Create Project -(st/defs create-project-spec - {:name [st/required st/string] - :width [st/required st/number st/positive] - :height [st/required st/number st/positive]}) +(s/def ::create-project-params + (s/keys :req-un [::name ::width ::height])) (defn create-project [{:keys [name] :as params}] - (assert (st/valid? create-project-spec params) - "invalid params for create project event") + (s/assert ::create-project-params params) (reify ptk/WatchEvent (watch [this state stream] diff --git a/frontend/src/uxbox/main/data/shapes.cljs b/frontend/src/uxbox/main/data/shapes.cljs index 154bdf6c26..681ceef1e5 100644 --- a/frontend/src/uxbox/main/data/shapes.cljs +++ b/frontend/src/uxbox/main/data/shapes.cljs @@ -5,14 +5,17 @@ ;; Copyright (c) 2015-2019 Andrey Antukh (ns uxbox.main.data.shapes - (:require [cljs.spec.alpha :as s] - [uxbox.main.geom :as geom] - [uxbox.util.geom.matrix :as gmt] - [uxbox.util.uuid :as uuid] - [uxbox.util.data :refer [index-of]])) + (: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?) @@ -49,7 +52,7 @@ (s/def ::attributes (s/keys :opt-un [::blocked ::collapsed - ::conent + ::content ::fill-color ::fill-opacity ::font-family @@ -72,10 +75,10 @@ ::y1 ::y2])) (s/def ::minimal-shape - (s/keys ::req-un [::id ::page ::type ::name])) + (s/keys :req-un [::id ::page ::type ::name])) (s/def ::shape - (s/merge ::minimal-shape ::attributes)) + (s/and ::minimal-shape ::attributes)) (s/def ::rect-like-shape (s/keys :req-un [::x1 ::y1 ::x2 ::y2 ::type])) diff --git a/frontend/src/uxbox/main/data/users.cljs b/frontend/src/uxbox/main/data/users.cljs index a00abc3554..93919375f3 100644 --- a/frontend/src/uxbox/main/data/users.cljs +++ b/frontend/src/uxbox/main/data/users.cljs @@ -6,8 +6,8 @@ (ns uxbox.main.data.users (:require + [cljs.spec.alpha :as s] [beicon.core :as rx] - [struct.core :as s] [potok.core :as ptk] [uxbox.main.repo :as rp] [uxbox.util.i18n :as i18n :refer [tr]] @@ -58,17 +58,22 @@ ;; --- Update Profile -(s/defs update-profile-spec - {:fullname [s/required s/string] - :email [s/required s/email] - :username [s/required s/string] - :language [s/required s/string]}) +(s/def ::fullname string?) +(s/def ::email ::us/email) +(s/def ::password string?) +(s/def ::language string?) + +(s/def ::update-profile-params + (s/keys :req-un [::fullname + ::email + ::username + ::language])) (defn update-profile [data {:keys [on-success on-error]}] - {:pre [(s/valid? update-profile-spec data) - (fn? on-error) - (fn? on-success)]} + (s/assert ::update-profile-params data) + (s/assert ::us/fn on-error) + (s/assert ::us/fn on-success) (reify ptk/WatchEvent (watch [_ state s] @@ -89,16 +94,20 @@ ;; --- Update Password (Form) -(s/defs update-password-spec - {:password-1 [s/required s/string] - :password-2 [s/required s/string [s/identical-to :password-1]] - :password-old [s/required s/string]}) +(s/def ::password-1 string?) +(s/def ::password-2 string?) +(s/def ::password-old string?) + +(s/def ::update-password-params + (s/keys :req-un [::password-1 + ::password-2 + ::password-old])) (defn update-password [data {:keys [on-success on-error]}] - {:pre [(s/valid? update-password-spec data) - (fn? on-success) - (fn? on-error)]} + (s/assert ::update-password-params data) + (s/assert ::us/fn on-success) + (s/assert ::us/fn on-error) (reify ptk/WatchEvent (watch [_ state s] diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index c0fa4f1f2e..da4a8be357 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -7,7 +7,6 @@ (ns uxbox.main.data.workspace (:require [beicon.core :as rx] - ;; [uxbox.main.data.workspace.ruler :as wruler] [cljs.spec.alpha :as s] [potok.core :as ptk] [uxbox.config :as cfg] @@ -342,7 +341,6 @@ (defn add-shape [data] - {:pre [(us/valid? ::ds/shape data)]} (reify udp/IPageUpdate ptk/UpdateEvent @@ -426,32 +424,27 @@ ;; --- Update Shape Attrs -(deftype UpdateShapeAttrs [id attrs] - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes id] merge attrs))) - (defn update-shape-attrs [id attrs] - {:pre [(uuid? id) (us/valid? ::ds/attributes attrs)]} - (let [atts (us/extract attrs ::ds/attributes)] - (UpdateShapeAttrs. id attrs))) + (s/assert ::us/uuid id) + (s/assert ::ds/attributes attrs) + (let [atts (s/conform ::ds/attributes attrs)] + (reify + ptk/UpdateEvent + (update [_ state] + (update-in state [:shapes id] merge attrs))))) ;; --- Update Selected Shapes attrs - -(deftype UpdateSelectedShapesAttrs [attrs] - ptk/WatchEvent - (watch [_ state stream] - (let [pid (get-in state [:workspace :current]) - selected (get-in state [:workspace pid :selected])] - (rx/from-coll (map #(update-shape-attrs % attrs) selected))))) - (defn update-selected-shapes-attrs [attrs] - {:pre [(us/valid? ::ds/attributes attrs)]} - (UpdateSelectedShapesAttrs. attrs)) - + (s/assert ::ds/attributes attrs) + (reify + ptk/WatchEvent + (watch [_ state stream] + (let [pid (get-in state [:workspace :current]) + selected (get-in state [:workspace pid :selected])] + (rx/from-coll (map #(update-shape-attrs % attrs) selected)))))) ;; --- Move Selected @@ -485,48 +478,44 @@ (declare materialize-current-modifier) (declare apply-temporal-displacement) -(defrecord MoveSelected [direction speed] - 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) - align? (refs/alignment-activated? flags) - metadata (merge c/page-metadata (get-in state [:pages page-id :metadata])) - distance (get-displacement-distance metadata align?) - displacement (get-displacement direction speed distance)] - (rx/concat - (when align? - (rx/concat - (rx/from-coll (map initial-shape-align selected)) - (rx/from-coll (map apply-displacement selected)))) - (rx/from-coll (map #(apply-temporal-displacement % displacement) selected)) - (rx/from-coll (map materialize-current-modifier selected)))))) - (s/def ::direction #{:up :down :right :left}) (s/def ::speed #{:std :fast}) (defn move-selected [direction speed] - {:pre [(us/valid? ::direction direction) - (us/valid? ::speed speed)]} - (MoveSelected. direction speed)) + (s/assert ::direction direction) + (s/assert ::speed speed) + (reify + 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) + align? (refs/alignment-activated? flags) + metadata (merge c/page-metadata (get-in state [:pages page-id :metadata])) + distance (get-displacement-distance metadata align?) + displacement (get-displacement direction speed distance)] + (rx/concat + (when align? + (rx/concat + (rx/from-coll (map initial-shape-align selected)) + (rx/from-coll (map apply-displacement selected)))) + (rx/from-coll (map #(apply-temporal-displacement % displacement) selected)) + (rx/from-coll (map materialize-current-modifier selected))))))) ;; --- Move Selected Layer -(defrecord MoveSelectedLayer [loc] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (let [id (get-in state [:workspace :current]) - selected (get-in state [:workspace id :selected])] - (ds/move-layer state selected loc)))) - (defn move-selected-layer [loc] - {:pre [(us/valid? ::direction loc)]} - (MoveSelectedLayer. loc)) + (assert (s/valid? ::direction loc)) + (reify + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace :current]) + selected (get-in state [:workspace id :selected])] + (ds/move-layer state selected loc))))) ;; --- Update Shape Position @@ -708,22 +697,24 @@ ;; --- Update Dimensions -(deftype UpdateDimensions [id dimensions] - udp/IPageUpdate - ptk/UpdateEvent - (update [_ state] - (update-in state [:shapes id] geom/resize-dim dimensions))) +(s/def ::width (s/and ::us/number ::us/positive)) +(s/def ::height (s/and ::us/number ::us/positive)) -(s/def ::update-dimensions-opts +(s/def ::update-dimensions (s/keys :opt-un [::width ::height])) (defn update-dimensions "A helper event just for update the position of the shape using the width and height attrs instread final point of coordinates." - [id opts] - {:pre [(uuid? id) (us/valid? ::update-dimensions-opts opts)]} - (UpdateDimensions. id opts)) + [id dimensions] + (s/assert ::us/uuid id) + (s/assert ::update-dimensions dimensions) + (reify + udp/IPageUpdate + ptk/UpdateEvent + (update [_ state] + (update-in state [:shapes id] geom/resize-dim dimensions)))) ;; --- Update Interaction @@ -983,16 +974,15 @@ ;; Is a workspace aware wrapper over uxbox.data.pages/UpdateMetadata event. -(defrecord UpdateMetadata [id metadata] - ptk/WatchEvent - (watch [_ state s] - (rx/of (udp/update-metadata id metadata) - (initialize-alignment id)))) - (defn update-metadata [id metadata] - {:pre [(uuid? id) (us/valid? ::udp/metadata metadata)]} - (UpdateMetadata. id metadata)) + (s/assert ::us/uuid id) + (s/assert ::udp/metadata metadata) + (reify + ptk/WatchEvent + (watch [_ state s] + (rx/of (udp/update-metadata id metadata) + (initialize-alignment id))))) (defrecord OpenView [page-id] ptk/WatchEvent diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs index 94d995fc1a..7edd5d571f 100644 --- a/frontend/src/uxbox/main/ui/auth/login.cljs +++ b/frontend/src/uxbox/main/ui/auth/login.cljs @@ -8,20 +8,22 @@ (ns uxbox.main.ui.auth.login (:require [rumext.alpha :as mf] - [struct.alpha :as s] + [cljs.spec.alpha :as s] [uxbox.builtins.icons :as i] [uxbox.config :as cfg] [uxbox.main.data.auth :as da] [uxbox.main.store :as st] [uxbox.main.ui.messages :refer [messages-widget]] [uxbox.util.dom :as dom] - [uxbox.util.forms :as fm] + [uxbox.util.forms2 :as fm2] [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as rt])) -(s/defs ::login-form - (s/dict :username (s/&& ::s/string ::fm/not-empty-string) - :password (s/&& ::s/string ::fm/not-empty-string))) +(s/def ::username ::fm2/not-empty-string) +(s/def ::password ::fm2/not-empty-string) + +(s/def ::login-form + (s/keys :req-un [::username ::password])) (defn- on-submit [event form] @@ -42,7 +44,8 @@ (mf/defc login-form [] - (let [{:keys [data] :as form} (fm/use-form ::login-form {})] + (let [{:keys [data] :as form} (fm2/use-form ::login-form {})] + (prn "login-form" form) [:form {:on-submit #(on-submit % form)} [:div.login-content (when cfg/isdemo @@ -52,16 +55,18 @@ {:name "username" :tab-index "2" :value (:username data "") - :on-blur (fm/on-input-blur form :username) - :on-change (fm/on-input-change form :username) + :class (fm2/error-class form :username) + :on-blur (fm2/on-input-blur form :username) + :on-change (fm2/on-input-change form :username) :placeholder (tr "auth.email-or-username") :type "text"}] [:input.input-text {:name "password" :tab-index "3" :value (:password data "") - :on-blur (fm/on-input-blur form :password) - :on-change (fm/on-input-change form :password) + :class (fm2/error-class form :password) + :on-blur (fm2/on-input-blur form :password) + :on-change (fm2/on-input-change form :password) :placeholder (tr "auth.password") :type "password"}] [:input.btn-primary diff --git a/frontend/src/uxbox/main/ui/auth/register.cljs b/frontend/src/uxbox/main/ui/auth/register.cljs index 261a2a98ae..e9fe977861 100644 --- a/frontend/src/uxbox/main/ui/auth/register.cljs +++ b/frontend/src/uxbox/main/ui/auth/register.cljs @@ -53,7 +53,6 @@ (mf/defc register-form [props] (let [{:keys [data] :as form} (fm/use-form ::register-form {})] - (prn "register-form" form) [:form {:on-submit #(on-submit % form)} [:div.login-content [:input.input-text diff --git a/frontend/src/uxbox/main/ui/settings/password.cljs b/frontend/src/uxbox/main/ui/settings/password.cljs index 9737f67252..fa559aabb2 100644 --- a/frontend/src/uxbox/main/ui/settings/password.cljs +++ b/frontend/src/uxbox/main/ui/settings/password.cljs @@ -8,38 +8,40 @@ (ns uxbox.main.ui.settings.password (:require [rumext.alpha :as mf] - [struct.alpha :as s] + [cljs.spec.alpha :as s] [uxbox.builtins.icons :as i] [uxbox.main.data.users :as udu] [uxbox.main.store :as st] [uxbox.util.dom :as dom] - [uxbox.util.forms :as fm] + [uxbox.util.forms2 :as fm] [uxbox.util.i18n :refer [tr]] [uxbox.util.messages :as um])) +(defn- on-error + [form error] + (case (:code error) + :uxbox.services.users/old-password-not-match + (swap! form assoc-in [:errors :password-old] + {:type ::api :message "settings.password.wrong-old-password"}) + + :else (throw (ex-info "unexpected" {:error error})))) + (defn- on-submit [event form] - (letfn [(on-error [error] - (case (:code error) - :uxbox.services.users/old-password-not-match - (swap! form assoc-in [:errors :password-old] - {:type ::api :message "settings.password.wrong-old-password"}) + (dom/prevent-default event) + (let [data (:clean-data form) + opts {:on-success #(st/emit! (um/info (tr "settings.password.password-saved"))) + :on-error #(on-error form %)}] + (st/emit! (udu/update-password data opts)))) - :else (throw (ex-info "unexpected" {:error error})))) +(s/def ::password-1 ::fm/not-empty-string) +(s/def ::password-2 ::fm/not-empty-string) +(s/def ::password-old ::fm/not-empty-string) - (on-success [_] - (st/emit! (um/info (tr "settings.password.password-saved"))))] - - (dom/prevent-default event) - (let [data (:clean-data form) - opts {:on-success on-success - :on-error on-error}] - (st/emit! (udu/update-password data opts))))) - -(s/defs ::password-form - (s/dict :password-1 (s/&& ::s/string ::fm/not-empty-string) - :password-2 (s/&& ::s/string ::fm/not-empty-string) - :password-old (s/&& ::s/string ::fm/not-empty-string))) +(s/def ::password-form + (s/keys :req-un [::password-1 + ::password-2 + ::password-old])) (mf/defc password-form [props] @@ -54,7 +56,8 @@ :on-blur (fm/on-input-blur form :password-old) :on-change (fm/on-input-change form :password-old) :placeholder (tr "settings.password.old-password")}] - [:& fm/field-error {:form form :field :password-old}] + + [:& fm/field-error {:form form :field :password-old :type ::api}] [:input.input-text {:type "password" @@ -64,7 +67,7 @@ :on-blur (fm/on-input-blur form :password-1) :on-change (fm/on-input-change form :password-1) :placeholder (tr "settings.password.new-password")}] - [:& fm/field-error {:form form :field :password-1}] + ;; [:& fm/field-error {:form form :field :password-1}] [:input.input-text {:type "password" @@ -74,7 +77,7 @@ :on-blur (fm/on-input-blur form :password-2) :on-change (fm/on-input-change form :password-2) :placeholder (tr "settings.password.confirm-password")}] - [:& fm/field-error {:form form :field :password-2}] + ;; [:& fm/field-error {:form form :field :password-2}] [:input.btn-primary {:type "submit" diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index 814d36e528..584935ff35 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -7,16 +7,16 @@ (ns uxbox.main.ui.settings.profile (:require + [cljs.spec.alpha :as s] [cuerdas.core :as str] [lentes.core :as l] [rumext.alpha :as mf] - [struct.alpha :as s] [uxbox.builtins.icons :as i] [uxbox.main.data.users :as udu] [uxbox.main.store :as st] [uxbox.util.data :refer [read-string]] [uxbox.util.dom :as dom] - [uxbox.util.forms :as fm] + [uxbox.util.forms2 :as fm] [uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.interop :refer [iterable->seq]] [uxbox.util.messages :as um])) @@ -32,11 +32,16 @@ (-> (l/key :profile) (l/derive st/state))) -(s/defs ::profile-form - (s/dict :fullname (s/&& ::s/string ::fm/not-empty-string) - :username (s/&& ::s/string ::fm/not-empty-string) - :language (s/&& ::s/string ::fm/not-empty-string) - :email ::s/email)) +(s/def ::fullname ::fm/not-empty-string) +(s/def ::username ::fm/not-empty-string) +(s/def ::language ::fm/not-empty-string) +(s/def ::email ::fm/email) + +(s/def ::profile-form + (s/keys :req-un [::fullname + ::username + ::language + ::email])) (defn- on-error [error form] @@ -58,7 +63,6 @@ (defn- on-submit [event form] - (prn "on-submit" form) (dom/prevent-default event) (let [data (:clean-data form) on-success #(st/emit! (um/info (tr "settings.profile.profile-saved"))) diff --git a/frontend/src/uxbox/main/ui/shapes/common.cljs b/frontend/src/uxbox/main/ui/shapes/common.cljs index ef76169eca..c30f5b0958 100644 --- a/frontend/src/uxbox/main/ui/shapes/common.cljs +++ b/frontend/src/uxbox/main/ui/shapes/common.cljs @@ -41,14 +41,12 @@ (watch [_ state stream] (let [pid (get-in state [:workspace :current]) selected (get-in state [:workspace pid :selected])] - (prn "start-move-selected" selected) (rx/from-coll (map start-move selected)))))) (defn on-mouse-down [event {:keys [id type] :as shape} selected] (let [selected? (contains? selected id) drawing? @refs/selected-drawing-tool] - (prn "on-mouse-down" id type selected? (= type :canvas)) (when-not (:blocked shape) (cond drawing? diff --git a/frontend/src/uxbox/main/ui/shapes/path.cljs b/frontend/src/uxbox/main/ui/shapes/path.cljs index 1cbd9b399d..2889a17424 100644 --- a/frontend/src/uxbox/main/ui/shapes/path.cljs +++ b/frontend/src/uxbox/main/ui/shapes/path.cljs @@ -29,7 +29,6 @@ (common/on-mouse-down event shape selected)) (on-double-click [event] (when selected? - (prn "path-component$on-double-click") (st/emit! (dw/start-edition-mode (:id shape)))))] [:g.shape {:class (when selected? "selected") :on-double-click on-double-click diff --git a/frontend/src/uxbox/util/forms2.cljs b/frontend/src/uxbox/util/forms2.cljs new file mode 100644 index 0000000000..8c3b8735ce --- /dev/null +++ b/frontend/src/uxbox/util/forms2.cljs @@ -0,0 +1,145 @@ +;; 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-2017 Andrey Antukh + +(ns uxbox.util.forms2 + (:refer-clojure :exclude [uuid]) + (:require + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [cuerdas.core :as str] + [lentes.core :as l] + [potok.core :as ptk] + [rumext.alpha :as mf] + [uxbox.util.dom :as dom] + [uxbox.util.i18n :refer [tr]])) + +;; --- Handlers Helpers + +(defn- impl-mutator + [v update-fn] + (specify v + IReset + (-reset! [_ new-value] + (update-fn new-value)) + + ISwap + (-swap! + ([self f] (update-fn f)) + ([self f x] (update-fn #(f % x))) + ([self f x y] (update-fn #(f % x y))) + ([self f x y more] (update-fn #(apply f % x y more)))))) + +(defn- translate-error-type + [name] + "errors.undefined-error") + +(defn- interpret-problem + [acc {:keys [path pred val via in] :as problem}] + ;; (prn "interpret-problem" problem) + (cond + (and (empty? path) + (list? pred) + (= (first (last pred)) 'cljs.core/contains?)) + (let [path (conj path (last (last pred)))] + (assoc-in acc path {:name ::missing :type :builtin})) + + (and (not (empty? path)) + (not (empty? via))) + (assoc-in acc path {:name (last via) :type :builtin}) + + :else acc)) + +(defn use-form + [spec initial] + (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) + :errors {} + :touched {}}) + clean-data (s/conform spec (:data state)) + problems (when (= ::s/invalid clean-data) + (::s/problems (s/explain-data spec (:data state)))) + + + errors (merge (reduce interpret-problem {} problems) + (:errors state))] + (-> (assoc state + :errors errors + :clean-data (when (not= clean-data ::s/invalid) clean-data) + :valid (and (empty? errors) + (not= clean-data ::s/invalid))) + (impl-mutator update-state)))) + +(defn on-input-change + [{:keys [data] :as form} field] + (fn [event] + (let [target (dom/get-target event) + value (dom/get-value target)] + (swap! form (fn [state] + (-> state + (assoc-in [:data field] value) + (update :errors dissoc field))))))) + +(defn on-input-blur + [{:keys [touched] :as form} field] + (fn [event] + (let [target (dom/get-target event)] + (when-not (get touched field) + (swap! form assoc-in [:touched field] true))))) + +;; --- Helper Components + +(mf/defc field-error + [{:keys [form field type] + :or {only (constantly true)} + :as props}] + (let [touched? (get-in form [:touched field]) + {:keys [message code] :as error} (get-in form [:errors field])] + (when (and touched? error + (cond + (nil? type) true + (keyword? type) (= (:type error) type) + (ifn? type) (type (:type error)) + :else false)) + (prn "field-error" error) + [:ul.form-errors + [:li {:key code} (tr message)]]))) + +(defn error-class + [form field] + (when (and (get-in form [:errors field]) + (get-in form [:touched field])) + "invalid")) + +;; --- Form Validation Api + +;; --- Form Specs and Conformers + +(def ^:private email-re + #"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") + +(def ^:private number-re + #"^[-+]?[0-9]*\.?[0-9]+$") + +(def ^:private color-re + #"^#[0-9A-Fa-f]{6}$") + +(s/def ::email + (s/and string? #(boolean (re-matches email-re %)))) + +(s/def ::not-empty-string + (s/and string? #(not (str/empty? %)))) + +(defn- parse-number + [v] + (cond + (re-matches number-re v) (js/parseFloat v) + (number? v) v + :else ::s/invalid)) + +(s/def ::string-number + (s/conformer parse-number str)) + +(s/def ::color + (s/and string? #(boolean (re-matches color-re %)))) diff --git a/frontend/src/uxbox/util/messages.cljs b/frontend/src/uxbox/util/messages.cljs index edd1103d4b..f5d50a9878 100644 --- a/frontend/src/uxbox/util/messages.cljs +++ b/frontend/src/uxbox/util/messages.cljs @@ -25,11 +25,34 @@ (def +animation-timeout+ 600) -;; --- Message Event +;; --- Main API (declare hide) +(declare show) (declare show?) +(defn error + [message & {:keys [timeout] :or {timeout 3000}}] + (show {:content message + :type :error + :timeout timeout})) + +(defn info + [message & {:keys [timeout] :or {timeout 3000}}] + (show {:content message + :type :info + :timeout timeout})) + +(defn dialog + [message & {:keys [on-accept on-cancel]}] + (show {:content message + :on-accept on-accept + :on-cancel on-cancel + :timeout js/Number.MAX_SAFE_INTEGER + :type :dialog})) + +;; --- Show Event + (defn show [data] (reify @@ -53,47 +76,19 @@ [v] (= ::show (ptk/type v))) -(defn error - [message & {:keys [timeout] :or {timeout 3000}}] - (show {:content message - :type :error - :timeout timeout})) - -(defn info - [message & {:keys [timeout] :or {timeout 3000}}] - (show {:content message - :type :info - :timeout timeout})) - -(defn dialog - [message & {:keys [on-accept on-cancel]}] - (show {:content message - :on-accept on-accept - :on-cancel on-cancel - :timeout js/Number.MAX_SAFE_INTEGER - :type :dialog})) - -;; --- Hide Message +;; --- Hide Event (defn hide [] - (let [canceled? (volatile! {})] - (reify - ptk/UpdateEvent - (update [_ state] - (update state :message - (fn [v] - (if (nil? v) - (do (vreset! canceled? true) nil) - (assoc v :state :hide))))) - - ptk/WatchEvent - (watch [_ state stream] - (if @canceled? - (rx/empty) - (->> (rx/of #(dissoc % :message)) - (rx/delay +animation-timeout+))))))) + (reify + ptk/UpdateEvent + (update [_ state] + (update state :message assoc :state :hide)) + ptk/WatchEvent + (watch [_ state stream] + (->> (rx/of #(dissoc % :message)) + (rx/delay +animation-timeout+))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; UI Components @@ -145,6 +140,7 @@ (mf/defc messages-widget [{:keys [message] :as props}] + (prn "messages-widget" props) (case (:type message) :error (mf/element notification-box props) :info (mf/element notification-box props) diff --git a/frontend/src/uxbox/util/spec.cljs b/frontend/src/uxbox/util/spec.cljs index 070824a790..592cbf11a9 100644 --- a/frontend/src/uxbox/util/spec.cljs +++ b/frontend/src/uxbox/util/spec.cljs @@ -42,6 +42,12 @@ (s/def ::uuid uuid?) (s/def ::email email?) (s/def ::color color?) +(s/def ::string string?) +(s/def ::number number?) +(s/def ::positive pos?) +(s/def ::inst inst?) +(s/def ::keyword keyword?) +(s/def ::fn fn?) ;; --- Public Api From faf7877d0079a013f8d29670e7a509a1c461d243 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 9 Sep 2019 23:21:55 +0200 Subject: [PATCH 45/46] :construction: Use cljs.spec everywhere. --- frontend/deps.edn | 1 - frontend/src/uxbox/main/data/dashboard.cljs | 1 - frontend/src/uxbox/main/data/workspace.cljs | 1 - frontend/src/uxbox/main/ui/auth/login.cljs | 2 +- frontend/src/uxbox/main/ui/auth/recovery.cljs | 2 +- frontend/src/uxbox/main/ui/auth/register.cljs | 18 +- .../main/ui/dashboard/projects_forms.cljs | 12 +- .../src/uxbox/main/ui/settings/password.cljs | 2 +- .../src/uxbox/main/ui/settings/profile.cljs | 2 +- .../ui/workspace/sidebar/sitemap_forms.cljs | 21 +- frontend/src/uxbox/util/forms.cljs | 245 +++--------------- frontend/src/uxbox/util/forms2.cljs | 145 ----------- frontend/src/uxbox/util/messages.cljs | 1 - frontend/src/uxbox/util/spec.cljs | 30 ++- frontend/src/uxbox/view/data/viewer.cljs | 1 - 15 files changed, 93 insertions(+), 391 deletions(-) delete mode 100644 frontend/src/uxbox/util/forms2.cljs diff --git a/frontend/deps.edn b/frontend/deps.edn index ab9b1dfead..29f93a12bc 100644 --- a/frontend/deps.edn +++ b/frontend/deps.edn @@ -7,7 +7,6 @@ environ/environ {:mvn/version "1.1.0"} metosin/reitit-core {:mvn/version "0.3.9"} - funcool/struct {:mvn/version "2.0.0-SNAPSHOT"} funcool/beicon {:mvn/version "5.1.0"} funcool/cuerdas {:mvn/version "2.2.0"} funcool/lentes {:mvn/version "1.3.0-SNAPSHOT"} diff --git a/frontend/src/uxbox/main/data/dashboard.cljs b/frontend/src/uxbox/main/data/dashboard.cljs index a56aae4700..ccd0ee4d33 100644 --- a/frontend/src/uxbox/main/data/dashboard.cljs +++ b/frontend/src/uxbox/main/data/dashboard.cljs @@ -10,7 +10,6 @@ [potok.core :as ptk] [uxbox.util.router :as r] [uxbox.main.store :as st] - [uxbox.util.forms :as sc] [uxbox.main.repo :as rp] [uxbox.main.data.projects :as dp] [uxbox.main.data.colors :as dc] diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index da4a8be357..306c454ca2 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -21,7 +21,6 @@ [uxbox.main.store :as st] [uxbox.main.workers :as uwrk] [uxbox.util.data :refer [dissoc-in index-of]] - [uxbox.util.forms :as sc] [uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.point :as gpt] [uxbox.util.math :as mth] diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs index 7edd5d571f..a37c7e6ed6 100644 --- a/frontend/src/uxbox/main/ui/auth/login.cljs +++ b/frontend/src/uxbox/main/ui/auth/login.cljs @@ -15,7 +15,7 @@ [uxbox.main.store :as st] [uxbox.main.ui.messages :refer [messages-widget]] [uxbox.util.dom :as dom] - [uxbox.util.forms2 :as fm2] + [uxbox.util.forms :as fm2] [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as rt])) diff --git a/frontend/src/uxbox/main/ui/auth/recovery.cljs b/frontend/src/uxbox/main/ui/auth/recovery.cljs index c444767bb0..daa9b7a099 100644 --- a/frontend/src/uxbox/main/ui/auth/recovery.cljs +++ b/frontend/src/uxbox/main/ui/auth/recovery.cljs @@ -7,7 +7,7 @@ (ns uxbox.main.ui.auth.recovery (:require - [cljs.spec.alpha :as s :include-macros true] + [cljs.spec.alpha :as s] [cuerdas.core :as str] [lentes.core :as l] [rumext.core :as mx :include-macros true] diff --git a/frontend/src/uxbox/main/ui/auth/register.cljs b/frontend/src/uxbox/main/ui/auth/register.cljs index e9fe977861..db3f2d6488 100644 --- a/frontend/src/uxbox/main/ui/auth/register.cljs +++ b/frontend/src/uxbox/main/ui/auth/register.cljs @@ -7,10 +7,10 @@ (ns uxbox.main.ui.auth.register (:require + [cljs.spec.alpha :as s] [cuerdas.core :as str] [lentes.core :as l] [rumext.alpha :as mf] - [struct.alpha :as s] [uxbox.builtins.icons :as i] [uxbox.main.data.auth :as uda] [uxbox.main.store :as st] @@ -21,11 +21,16 @@ [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as rt])) -(s/defs ::register-form - (s/dict :username (s/&& ::s/string ::fm/not-empty-string) - :fullname (s/&& ::s/string ::fm/not-empty-string) - :password (s/&& ::s/string ::fm/not-empty-string) - :email ::s/email)) +(s/def ::username ::fm/not-empty-string) +(s/def ::fullname ::fm/not-empty-string) +(s/def ::password ::fm/not-empty-string) +(s/def ::email ::fm/email) + +(s/def ::register-form + (s/keys :req-un [::username + ::password + ::fullname + ::email])) (defn- on-error [error form] @@ -112,7 +117,6 @@ :type #{::api} :field :email}] - [:input.btn-primary {:type "submit" :tab-index "5" diff --git a/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs b/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs index 119161ccfb..d6695623b6 100644 --- a/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/projects_forms.cljs @@ -7,7 +7,7 @@ (ns uxbox.main.ui.dashboard.projects-forms (:require - [struct.alpha :as s] + [cljs.spec.alpha :as s] [rumext.alpha :as mf] [uxbox.builtins.icons :as i] [uxbox.main.data.projects :as udp] @@ -17,10 +17,12 @@ [uxbox.util.forms :as fm] [uxbox.util.i18n :as t :refer [tr]])) -(s/defs ::project-form - (s/dict :name (s/&& ::s/string ::fm/not-empty-string) - :width ::s/number-str - :height ::s/number-str)) +(s/def ::name ::fm/not-empty-string) +(s/def ::width ::fm/number-str) +(s/def ::height ::fm/number-str) + +(s/def ::project-form + (s/keys :req-un [::name ::width ::height])) (def defaults {:name "" diff --git a/frontend/src/uxbox/main/ui/settings/password.cljs b/frontend/src/uxbox/main/ui/settings/password.cljs index fa559aabb2..fa286f9c1d 100644 --- a/frontend/src/uxbox/main/ui/settings/password.cljs +++ b/frontend/src/uxbox/main/ui/settings/password.cljs @@ -13,7 +13,7 @@ [uxbox.main.data.users :as udu] [uxbox.main.store :as st] [uxbox.util.dom :as dom] - [uxbox.util.forms2 :as fm] + [uxbox.util.forms :as fm] [uxbox.util.i18n :refer [tr]] [uxbox.util.messages :as um])) diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index 584935ff35..82ec47fe54 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -16,7 +16,7 @@ [uxbox.main.store :as st] [uxbox.util.data :refer [read-string]] [uxbox.util.dom :as dom] - [uxbox.util.forms2 :as fm] + [uxbox.util.forms :as fm] [uxbox.util.i18n :as i18n :refer [tr]] [uxbox.util.interop :refer [iterable->seq]] [uxbox.util.messages :as um])) 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 8e3c410c4b..4ec95b7416 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs @@ -8,22 +8,29 @@ (ns uxbox.main.ui.workspace.sidebar.sitemap-forms (:require [rumext.alpha :as mf] - [struct.alpha :as s] + [cljs.spec.alpha :as s] [uxbox.builtins.icons :as i] [uxbox.main.constants :as c] [uxbox.main.data.pages :as udp] [uxbox.main.store :as st] [uxbox.main.ui.modal :as modal] [uxbox.util.dom :as dom] + [uxbox.util.spec :as us] [uxbox.util.forms :as fm] [uxbox.util.i18n :refer [tr]])) -(s/defs ::page-form - (s/dict :id (s/opt ::s/uuid) - :project ::s/uuid - :name (s/&& ::s/string ::fm/not-empty-string) - :width ::s/number-str - :height ::s/number-str)) +(s/def ::id ::us/uuid) +(s/def ::project ::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 "" diff --git a/frontend/src/uxbox/util/forms.cljs b/frontend/src/uxbox/util/forms.cljs index 70e5db5435..02f91253f4 100644 --- a/frontend/src/uxbox/util/forms.cljs +++ b/frontend/src/uxbox/util/forms.cljs @@ -13,9 +13,8 @@ [lentes.core :as l] [potok.core :as ptk] [rumext.alpha :as mf] - [rumext.core :as mx] - [struct.alpha :as st] [uxbox.util.dom :as dom] + [uxbox.util.spec :as us] [uxbox.util.i18n :refer [tr]])) ;; --- Handlers Helpers @@ -36,43 +35,41 @@ (defn- translate-error-type [name] - (case name - ::st/string "errors.form.string" - ::st/number "errors.form.number" - ::st/number-str "errors.form.number" - ::st/integer "errors.form.integer" - ::st/integer-str "errors.form.integer" - ::st/required "errors.form.required" - ::st/email "errors.form.email" - ;; ::st/identical-to "errors.form.does-not-match" - "errors.undefined-error")) + "errors.undefined-error") -(defn- process-errors - [errors] - (reduce (fn [acc {:keys [path name] :as error}] - (let [message (translate-error-type name)] - (assoc-in acc path - (-> (assoc error :message message) - (dissoc :path))))) - {} errors)) +(defn- interpret-problem + [acc {:keys [path pred val via in] :as problem}] + ;; (prn "interpret-problem" problem) + (cond + (and (empty? path) + (list? pred) + (= (first (last pred)) 'cljs.core/contains?)) + (let [path (conj path (last (last pred)))] + (assoc-in acc path {:name ::missing :type :builtin})) + + (and (not (empty? path)) + (not (empty? via))) + (assoc-in acc path {:name (last via) :type :builtin}) + + :else acc)) (defn use-form [spec initial] (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) :errors {} :touched {}}) - cdata (st/conform spec (:data state)) - errors' (when (= ::st/invalid cdata) - (st/explain spec (:data state))) + clean-data (s/conform spec (:data state)) + problems (when (= ::s/invalid clean-data) + (::s/problems (s/explain-data spec (:data state)))) - errors (merge (process-errors errors') + + errors (merge (reduce interpret-problem {} problems) (:errors state))] - (-> (assoc state :errors errors - :clean-data (when (not= cdata ::st/invalid) cdata) + :clean-data (when (not= clean-data ::s/invalid) clean-data) :valid (and (empty? errors) - (not= cdata ::st/invalid))) + (not= clean-data ::s/invalid))) (impl-mutator update-state)))) (defn on-input-change @@ -103,9 +100,10 @@ (when (and touched? error (cond (nil? type) true - (ifn? type) (type (:type error)) (keyword? type) (= (:type error) type) + (ifn? type) (type (:type error)) :else false)) + (prn "field-error" error) [:ul.form-errors [:li {:key code} (tr message)]]))) @@ -115,191 +113,10 @@ (get-in form [:touched field])) "invalid")) -;; --- Additional Validators - -(st/defs ::not-empty-string #(not (empty? %))) - -;; (def string (assoc st/string :message "errors.should-be-string")) -;; (def number (assoc st/number :message "errors.should-be-number")) -;; (def number-str (assoc st/number-str :message "errors.should-be-number")) -;; (def integer (assoc st/integer :message "errors.should-be-integer")) -;; (def integer-str (assoc st/integer-str :message "errors.should-be-integer")) -;; (def required (assoc st/required :message "errors.required")) -;; (def email (assoc st/email :message "errors.should-be-valid-email")) -;; (def uuid (assoc st/uuid :message "errors.should-be-uuid")) -;; (def uuid-str (assoc st/uuid-str :message "errors.should-be-valid-uuid")) - -;; DEPRECATED - -;; --- Form Validation Api - ;; --- Form Specs and Conformers -(def ^:private email-re - #"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") - -(def ^:private number-re - #"^[-+]?[0-9]*\.?[0-9]+$") - -(def ^:private color-re - #"^#[0-9A-Fa-f]{6}$") - -(s/def ::email - (s/and string? #(boolean (re-matches email-re %)))) - -(s/def ::non-empty-string - (s/and string? #(not (str/empty? %)))) - -(s/def ::not-empty #(not (str/empty? %))) - - -(defn- parse-number - [v] - (cond - (re-matches number-re v) (js/parseFloat v) - (number? v) v - :else ::s/invalid)) - -(s/def ::string-number - (s/conformer parse-number str)) - -(s/def ::color - (s/and string? #(boolean (re-matches color-re %)))) - - - -;; --- Form State Events - -;; --- Assoc Error - -(defrecord AssocError [type field error] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:errors type field] error))) - -(defn assoc-error - ([type field] - (assoc-error type field nil)) - ([type field error] - {:pre [(keyword? type) - (keyword? field) - (any? error)]} - (AssocError. type field error))) - -;; --- Assoc Errors - -(defrecord AssocErrors [type errors] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:errors type] errors))) - -(defn assoc-errors - ([type] - (assoc-errors type nil)) - ([type errors] - {:pre [(keyword? type) - (or (map? errors) - (nil? errors))]} - (AssocErrors. type errors))) - -;; --- Assoc Value - -(declare clear-error) - -(defrecord AssocValue [type field value] - ptk/UpdateEvent - (update [_ state] - (let [form-path (into [:forms type] (if (coll? field) field [field]))] - (assoc-in state form-path value))) - - ptk/WatchEvent - (watch [_ state stream] - (rx/of (clear-error type field)))) - -(defn assoc-value - [type field value] - {:pre [(keyword? type) - (keyword? field) - (any? value)]} - (AssocValue. type field value)) - -;; --- Clear Values - -(defrecord ClearValues [type] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:forms type] nil))) - -(defn clear-values - [type] - {:pre [(keyword? type)]} - (ClearValues. type)) - -;; --- Clear Error - -(deftype ClearError [type field] - ptk/UpdateEvent - (update [_ state] - (let [errors (get-in state [:errors type])] - (if (map? errors) - (assoc-in state [:errors type] (dissoc errors field)) - (update state :errors dissoc type))))) - -(defn clear-error - [type field] - {:pre [(keyword? type) - (keyword? field)]} - (ClearError. type field)) - -;; --- Clear Errors - -(defrecord ClearErrors [type] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:errors type] nil))) - -(defn clear-errors - [type] - {:pre [(keyword? type)]} - (ClearErrors. type)) - -;; --- Clear Form - -(deftype ClearForm [type] - ptk/WatchEvent - (watch [_ state stream] - (rx/of (clear-values type) - (clear-errors type)))) - -(defn clear-form - [type] - {:pre [(keyword? type)]} - (ClearForm. type)) - -;; --- Helpers - -(defn focus-data - [type state] - (-> (l/in [:forms type]) - (l/derive state))) - -(defn focus-errors - [type state] - (-> (l/in [:errors type]) - (l/derive state))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Form UI -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(mx/defc input-error - [errors field] - (when-let [error (get errors field)] - [:ul.form-errors - [:li {:key error} (tr error)]])) - -(defn clear-mixin - [store type] - {:will-unmount (fn [own] - (ptk/emit! store (clear-form type)) - own)}) +;; TODO: migrate to uxbox.util.spec +(s/def ::email ::us/email) +(s/def ::not-empty-string ::us/not-empty-string) +(s/def ::color ::us/color) +(s/def ::number-str ::us/number-str) diff --git a/frontend/src/uxbox/util/forms2.cljs b/frontend/src/uxbox/util/forms2.cljs deleted file mode 100644 index 8c3b8735ce..0000000000 --- a/frontend/src/uxbox/util/forms2.cljs +++ /dev/null @@ -1,145 +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-2017 Andrey Antukh - -(ns uxbox.util.forms2 - (:refer-clojure :exclude [uuid]) - (:require - [beicon.core :as rx] - [cljs.spec.alpha :as s] - [cuerdas.core :as str] - [lentes.core :as l] - [potok.core :as ptk] - [rumext.alpha :as mf] - [uxbox.util.dom :as dom] - [uxbox.util.i18n :refer [tr]])) - -;; --- Handlers Helpers - -(defn- impl-mutator - [v update-fn] - (specify v - IReset - (-reset! [_ new-value] - (update-fn new-value)) - - ISwap - (-swap! - ([self f] (update-fn f)) - ([self f x] (update-fn #(f % x))) - ([self f x y] (update-fn #(f % x y))) - ([self f x y more] (update-fn #(apply f % x y more)))))) - -(defn- translate-error-type - [name] - "errors.undefined-error") - -(defn- interpret-problem - [acc {:keys [path pred val via in] :as problem}] - ;; (prn "interpret-problem" problem) - (cond - (and (empty? path) - (list? pred) - (= (first (last pred)) 'cljs.core/contains?)) - (let [path (conj path (last (last pred)))] - (assoc-in acc path {:name ::missing :type :builtin})) - - (and (not (empty? path)) - (not (empty? via))) - (assoc-in acc path {:name (last via) :type :builtin}) - - :else acc)) - -(defn use-form - [spec initial] - (let [[state update-state] (mf/useState {:data (if (fn? initial) (initial) initial) - :errors {} - :touched {}}) - clean-data (s/conform spec (:data state)) - problems (when (= ::s/invalid clean-data) - (::s/problems (s/explain-data spec (:data state)))) - - - errors (merge (reduce interpret-problem {} problems) - (:errors state))] - (-> (assoc state - :errors errors - :clean-data (when (not= clean-data ::s/invalid) clean-data) - :valid (and (empty? errors) - (not= clean-data ::s/invalid))) - (impl-mutator update-state)))) - -(defn on-input-change - [{:keys [data] :as form} field] - (fn [event] - (let [target (dom/get-target event) - value (dom/get-value target)] - (swap! form (fn [state] - (-> state - (assoc-in [:data field] value) - (update :errors dissoc field))))))) - -(defn on-input-blur - [{:keys [touched] :as form} field] - (fn [event] - (let [target (dom/get-target event)] - (when-not (get touched field) - (swap! form assoc-in [:touched field] true))))) - -;; --- Helper Components - -(mf/defc field-error - [{:keys [form field type] - :or {only (constantly true)} - :as props}] - (let [touched? (get-in form [:touched field]) - {:keys [message code] :as error} (get-in form [:errors field])] - (when (and touched? error - (cond - (nil? type) true - (keyword? type) (= (:type error) type) - (ifn? type) (type (:type error)) - :else false)) - (prn "field-error" error) - [:ul.form-errors - [:li {:key code} (tr message)]]))) - -(defn error-class - [form field] - (when (and (get-in form [:errors field]) - (get-in form [:touched field])) - "invalid")) - -;; --- Form Validation Api - -;; --- Form Specs and Conformers - -(def ^:private email-re - #"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$") - -(def ^:private number-re - #"^[-+]?[0-9]*\.?[0-9]+$") - -(def ^:private color-re - #"^#[0-9A-Fa-f]{6}$") - -(s/def ::email - (s/and string? #(boolean (re-matches email-re %)))) - -(s/def ::not-empty-string - (s/and string? #(not (str/empty? %)))) - -(defn- parse-number - [v] - (cond - (re-matches number-re v) (js/parseFloat v) - (number? v) v - :else ::s/invalid)) - -(s/def ::string-number - (s/conformer parse-number str)) - -(s/def ::color - (s/and string? #(boolean (re-matches color-re %)))) diff --git a/frontend/src/uxbox/util/messages.cljs b/frontend/src/uxbox/util/messages.cljs index f5d50a9878..1847256167 100644 --- a/frontend/src/uxbox/util/messages.cljs +++ b/frontend/src/uxbox/util/messages.cljs @@ -140,7 +140,6 @@ (mf/defc messages-widget [{:keys [message] :as props}] - (prn "messages-widget" props) (case (:type message) :error (mf/element notification-box props) :info (mf/element notification-box props) diff --git a/frontend/src/uxbox/util/spec.cljs b/frontend/src/uxbox/util/spec.cljs index 592cbf11a9..904f0392d6 100644 --- a/frontend/src/uxbox/util/spec.cljs +++ b/frontend/src/uxbox/util/spec.cljs @@ -5,7 +5,9 @@ ;; Copyright (c) 2015-2016 Andrey Antukh (ns uxbox.util.spec - (:require [cljs.spec.alpha :as s])) + (:require [cljs.spec.alpha :as s] + [cuerdas.core :as str])) + ;; --- Constants @@ -15,11 +17,17 @@ (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]+)?$") + +(def ^:private color-re + #"^#[0-9A-Fa-f]{6}$") + ;; --- Predicates (defn email? [v] - (and string? + (and (string? v) (re-matches email-rx v))) (defn color? @@ -31,8 +39,6 @@ [v] (instance? js/File v)) -;; TODO: properly implement - (defn url-str? [v] (string? v)) @@ -49,6 +55,22 @@ (s/def ::keyword keyword?) (s/def ::fn fn?) +(s/def ::not-empty-string + (s/and string? #(not (str/empty? %)))) + + +(defn- conform-number-str + [v] + (cond + (re-matches number-rx v) (js/parseFloat v) + (number? v) v + :else ::s/invalid)) + +(s/def ::number-str + (s/conformer conform-number-str str)) + +(s/def ::color color?) + ;; --- Public Api (defn valid? diff --git a/frontend/src/uxbox/view/data/viewer.cljs b/frontend/src/uxbox/view/data/viewer.cljs index fd8fcddcbc..7ae36890bf 100644 --- a/frontend/src/uxbox/view/data/viewer.cljs +++ b/frontend/src/uxbox/view/data/viewer.cljs @@ -8,7 +8,6 @@ (:require [beicon.core :as rx] [potok.core :as ptk] [uxbox.util.router :as rt] - [uxbox.util.forms :as sc] [uxbox.util.data :refer (parse-int)] [uxbox.main.repo :as rp] [uxbox.main.data.pages :as udpg] From b1459f85cd8d4250ad58573eff65c7f015ae35ef Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Tue, 10 Sep 2019 12:00:52 +0200 Subject: [PATCH 46/46] :construction: More work on data validation. --- frontend/src/uxbox/main.cljs | 2 +- frontend/src/uxbox/main/data/auth.cljs | 4 +- frontend/src/uxbox/main/data/pages.cljs | 158 +++++++++--------- frontend/src/uxbox/main/data/projects.cljs | 2 +- frontend/src/uxbox/main/data/users.cljs | 77 ++++----- frontend/src/uxbox/main/ui/auth/login.cljs | 26 +-- .../src/uxbox/main/ui/settings/profile.cljs | 6 +- frontend/src/uxbox/main/ui/users.cljs | 2 +- .../ui/workspace/sidebar/options/page.cljs | 4 +- .../ui/workspace/sidebar/sitemap_forms.cljs | 4 +- frontend/src/uxbox/util/spec.cljs | 1 + 11 files changed, 136 insertions(+), 150 deletions(-) diff --git a/frontend/src/uxbox/main.cljs b/frontend/src/uxbox/main.cljs index 54b6886511..552c10b8d6 100644 --- a/frontend/src/uxbox/main.cljs +++ b/frontend/src/uxbox/main.cljs @@ -67,7 +67,7 @@ (add-watch html-history/path ::main #(on-navigate router %4)) (when (:auth storage) - (st/emit! (udu/fetch-profile))) + (st/emit! udu/fetch-profile)) (mf/mount (mf/element ui/app) (dom/get-element "app")) (mf/mount (lightbox) (dom/get-element "lightbox")) diff --git a/frontend/src/uxbox/main/data/auth.cljs b/frontend/src/uxbox/main/data/auth.cljs index 591d1513db..a6d80b6c4a 100644 --- a/frontend/src/uxbox/main/data/auth.cljs +++ b/frontend/src/uxbox/main/data/auth.cljs @@ -25,8 +25,6 @@ ;; --- Logged In -;; TODO: add spec - (defn logged-in [data] (reify @@ -40,7 +38,7 @@ ptk/WatchEvent (watch [this state s] (swap! storage assoc :auth data) - (rx/of (du/fetch-profile) + (rx/of du/fetch-profile (rt/navigate :dashboard/projects))))) (defn logged-in? diff --git a/frontend/src/uxbox/main/data/pages.cljs b/frontend/src/uxbox/main/data/pages.cljs index 8c85f7cf7d..2b79fcfea5 100644 --- a/frontend/src/uxbox/main/data/pages.cljs +++ b/frontend/src/uxbox/main/data/pages.cljs @@ -139,46 +139,44 @@ [state id] (update state :packed-pages dissoc id)) - - ;; --- Pages Fetched -(deftype PagesFetched [id pages] - IDeref - (-deref [_] (list id pages)) - - 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))))) - (defn pages-fetched [id pages] - {:pre [(uuid? id) (coll? pages)]} - (PagesFetched. id pages)) + (s/assert ::us/uuid id) + (s/assert ::us/coll pages) + (reify + IDeref + (-deref [_] (list id pages)) + + ptk/EventType + (type [_] ::page-fetched) + + 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)))))) (defn pages-fetched? [v] - (instance? PagesFetched v)) + (= ::page-fetched (ptk/type v))) ;; --- Fetch Pages (by project id) -(deftype FetchPages [id] - ptk/WatchEvent - (watch [_ state s] - (->> (rp/req :fetch/pages-by-project {:project id}) - (rx/map :payload) - (rx/map #(pages-fetched id %))))) - (defn fetch-pages [id] - {:pre [(uuid? id)]} - (FetchPages. id)) + (s/assert ::us/uuid id) + (reify + ptk/WatchEvent + (watch [_ state s] + (->> (rp/req :fetch/pages-by-project {:project id}) + (rx/map :payload) + (rx/map #(pages-fetched id %)))))) ;; --- Page Created @@ -189,7 +187,7 @@ (defn page-created [data] - (s/assert ::page-created-event data) + (s/assert ::page-created-params data) (reify ptk/UpdateEvent (update [_ state] @@ -203,14 +201,14 @@ (watch [_ state stream] (rx/of (rehash-pages (:project data)))))) -;; --- Create Page +;; --- Create Page Form -(s/def ::created-page-params +(s/def ::form-created-page-params (s/keys :req-un [::name ::project ::width ::height])) -(defn create-page +(defn form->create-page [{:keys [name project width height layout] :as data}] - (s/assert ::created-page-params data) + (s/assert ::form-created-page-params data) (reify ptk/WatchEvent (watch [this state s] @@ -232,6 +230,25 @@ (rx/map :payload) (rx/map page-created)))))) +;; --- Update Page Form + +(s/def ::form-update-page-params + (s/keys :req-un [::id ::name ::width ::height])) + +(defn form->update-page + [{:keys [id name width height] :as data}] + (s/assert ::form-update-page-params data) + (reify + IPageUpdate + 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))))))) + ;; --- Page Persisted (defn page-persisted @@ -321,10 +338,9 @@ ;; --- Update Page -(defn update-page - [id data] +(defn update-page-attrs + [{:keys [id] :as data}] (s/assert ::page-entity data) - (s/assert ::id id) (reify IPageUpdate ptk/UpdateEvent @@ -381,27 +397,6 @@ pages (vec (concat before [page-id] after))] (assoc-in state [:projects project-id :pages] pages))))) -;; --- Persist Page Form -;; -;; A specialized event for persist data -;; from the update page form. - -(s/def ::persist-page-update-form-params - (s/keys :req-un [::id ::name ::width ::height])) - -(defn persist-page-update-form - [{:keys [id name width height] :as data}] - (s/assert ::persist-page-update-form-params data) - (reify - 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))))))) - ;; --- Delete Page (by id) (defn delete-page @@ -419,31 +414,28 @@ ;; --- Watch Page Changes -(deftype WatchPageChanges [id] - ptk/WatchEvent - (watch [_ state stream] - (let [stopper (->> stream - (rx/filter #(= % ::stop-page-watcher)) - (rx/take 1))] - (rx/merge - (->> stream - (rx/filter #(or (satisfies? IPageUpdate %) - (= ::page-update %))) - (rx/take-until stopper) - (rx/debounce 1000) - (rx/mapcat #(rx/merge (rx/of (persist-page id)) - (->> (rx/filter page-persisted? stream) - (rx/take 1) - (rx/ignore))))) - (->> stream - (rx/filter #(satisfies? IMetadataUpdate %)) - (rx/take-until stopper) - (rx/debounce 1000) - (rx/mapcat #(rx/merge (rx/of (persist-metadata id)) - (->> (rx/filter metadata-persisted? stream) - (rx/take 1) - (rx/ignore))))))))) - (defn watch-page-changes [id] - (WatchPageChanges. 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/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 9d4bb03a3a..d6cb20b59d 100644 --- a/frontend/src/uxbox/main/data/projects.cljs +++ b/frontend/src/uxbox/main/data/projects.cljs @@ -173,7 +173,7 @@ (rx/map :payload) (rx/mapcat (fn [{:keys [id] :as project}] (rx/of #(assoc-project % project) - (udp/create-page (assoc params :project id))))))))) + (udp/form->create-page (assoc params :project id))))))))) ;; --- Go To Project diff --git a/frontend/src/uxbox/main/data/users.cljs b/frontend/src/uxbox/main/data/users.cljs index 93919375f3..c5290e5552 100644 --- a/frontend/src/uxbox/main/data/users.cljs +++ b/frontend/src/uxbox/main/data/users.cljs @@ -2,7 +2,7 @@ ;; 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 Andrey Antukh +;; Copyright (c) 2016-2019 Andrey Antukh (ns uxbox.main.data.users (:require @@ -15,62 +15,64 @@ [uxbox.util.spec :as us] [uxbox.util.storage :refer [storage]])) +;; --- Common Specs + +(s/def ::id uuid?) +(s/def ::username string?) +(s/def ::fullname string?) +(s/def ::email ::us/email) +(s/def ::password string?) +(s/def ::language string?) +(s/def ::photo string?) +(s/def ::created-at inst?) +(s/def ::password-1 string?) +(s/def ::password-2 string?) +(s/def ::password-old string?) + ;; --- Profile Fetched +(s/def ::profile-fetched-params + (s/keys :req-un [::id + ::username + ::fullname + ::email + ::created-at + ::photo])) + (defn profile-fetched [data] + (s/assert ::profile-fetched-params data) (reify ptk/UpdateEvent - (update [this state] + (update [_ state] (assoc state :profile data)) ptk/EffectEvent - (effect [this state stream] + (effect [_ state stream] (swap! storage assoc :profile data) - ;; (prn "profile-fetched" data) (when-let [lang (get-in data [:metadata :language])] (i18n/set-current-locale! lang))))) ;; --- Fetch Profile -(deftype FetchProfile [] - ptk/WatchEvent - (watch [_ state s] - (->> (rp/req :fetch/profile) - (rx/map :payload) - (rx/map profile-fetched)))) - -(defn fetch-profile - [] - (FetchProfile.)) - -;; --- Profile Updated - -(deftype ProfileUpdated [data] - ptk/WatchEvent - (watch [_ state s] - (rx/of (profile-fetched data) - (uum/info (tr "settings.profile.profile-saved"))))) - -(defn profile-updated - [data] - (ProfileUpdated. data)) +(def fetch-profile + (reify + ptk/WatchEvent + (watch [_ state s] + (->> (rp/req :fetch/profile) + (rx/map :payload) + (rx/map profile-fetched))))) ;; --- Update Profile -(s/def ::fullname string?) -(s/def ::email ::us/email) -(s/def ::password string?) -(s/def ::language string?) - (s/def ::update-profile-params (s/keys :req-un [::fullname ::email ::username ::language])) -(defn update-profile - [data {:keys [on-success on-error]}] +(defn form->update-profile + [data on-success on-error] (s/assert ::update-profile-params data) (s/assert ::us/fn on-error) (s/assert ::us/fn on-success) @@ -88,16 +90,11 @@ (->> (rp/req :update/profile data) (rx/map :payload) (rx/do on-success) - (rx/map profile-updated) - ;; (rx/map profile-fetched) + (rx/map profile-fetched) (rx/catch rp/client-error? handle-error))))))) ;; --- Update Password (Form) -(s/def ::password-1 string?) -(s/def ::password-2 string?) -(s/def ::password-old string?) - (s/def ::update-password-params (s/keys :req-un [::password-1 ::password-2 @@ -128,7 +125,7 @@ (watch [_ state stream] (->> (rp/req :update/profile-photo {:file file}) (rx/do done) - (rx/map fetch-profile)))) + (rx/map (constantly fetch-profile))))) (defn update-photo ([file] (update-photo file (constantly nil))) diff --git a/frontend/src/uxbox/main/ui/auth/login.cljs b/frontend/src/uxbox/main/ui/auth/login.cljs index a37c7e6ed6..5510e02f2f 100644 --- a/frontend/src/uxbox/main/ui/auth/login.cljs +++ b/frontend/src/uxbox/main/ui/auth/login.cljs @@ -7,20 +7,21 @@ (ns uxbox.main.ui.auth.login (:require - [rumext.alpha :as mf] [cljs.spec.alpha :as s] + [rumext.alpha :as mf] [uxbox.builtins.icons :as i] [uxbox.config :as cfg] [uxbox.main.data.auth :as da] [uxbox.main.store :as st] [uxbox.main.ui.messages :refer [messages-widget]] [uxbox.util.dom :as dom] - [uxbox.util.forms :as fm2] + [uxbox.util.forms :as fm] [uxbox.util.i18n :refer [tr]] - [uxbox.util.router :as rt])) + [uxbox.util.router :as rt] + [uxbox.util.spec :as us])) -(s/def ::username ::fm2/not-empty-string) -(s/def ::password ::fm2/not-empty-string) +(s/def ::username ::us/not-empty-string) +(s/def ::password ::us/not-empty-string) (s/def ::login-form (s/keys :req-un [::username ::password])) @@ -44,8 +45,7 @@ (mf/defc login-form [] - (let [{:keys [data] :as form} (fm2/use-form ::login-form {})] - (prn "login-form" form) + (let [{:keys [data] :as form} (fm/use-form ::login-form {})] [:form {:on-submit #(on-submit % form)} [:div.login-content (when cfg/isdemo @@ -55,18 +55,18 @@ {:name "username" :tab-index "2" :value (:username data "") - :class (fm2/error-class form :username) - :on-blur (fm2/on-input-blur form :username) - :on-change (fm2/on-input-change form :username) + :class (fm/error-class form :username) + :on-blur (fm/on-input-blur form :username) + :on-change (fm/on-input-change form :username) :placeholder (tr "auth.email-or-username") :type "text"}] [:input.input-text {:name "password" :tab-index "3" :value (:password data "") - :class (fm2/error-class form :password) - :on-blur (fm2/on-input-blur form :password) - :on-change (fm2/on-input-change form :password) + :class (fm/error-class form :password) + :on-blur (fm/on-input-blur form :password) + :on-change (fm/on-input-change form :password) :placeholder (tr "auth.password") :type "password"}] [:input.btn-primary diff --git a/frontend/src/uxbox/main/ui/settings/profile.cljs b/frontend/src/uxbox/main/ui/settings/profile.cljs index 82ec47fe54..0897eec680 100644 --- a/frontend/src/uxbox/main/ui/settings/profile.cljs +++ b/frontend/src/uxbox/main/ui/settings/profile.cljs @@ -66,10 +66,8 @@ (dom/prevent-default event) (let [data (:clean-data form) on-success #(st/emit! (um/info (tr "settings.profile.profile-saved"))) - on-error #(on-error % form) - opts {:on-success on-success - :on-error on-error}] - (st/emit! (udu/update-profile data opts)))) + on-error #(on-error % form)] + (st/emit! (udu/form->update-profile data on-success on-error)))) ;; --- Profile Form diff --git a/frontend/src/uxbox/main/ui/users.cljs b/frontend/src/uxbox/main/ui/users.cljs index 6283c87069..bf2004f8c9 100644 --- a/frontend/src/uxbox/main/ui/users.cljs +++ b/frontend/src/uxbox/main/ui/users.cljs @@ -49,7 +49,7 @@ (l/derive st/state))) (mf/defc user - [_] + [props] (let [open (mf/use-state false) profile (mf/deref profile-ref) photo (if (str/empty? (:photo profile "")) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/options/page.cljs index 980a6962fb..861f3c3d03 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/options/page.cljs @@ -43,8 +43,8 @@ (on-name-change [event] (let [value (-> (dom/event->value event) (str/trim))] - (st/emit! (->> (assoc page :name value) - (udp/update-page (:id page)))))) + (st/emit! (-> (assoc page :name value) + (udp/update-page-attrs))))) (show-color-picker [event] (let [x (.-clientX event) 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 4ec95b7416..15c16e69e8 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/sitemap_forms.cljs @@ -43,8 +43,8 @@ (modal/hide!) (let [data (:clean-data form)] (if (nil? (:id data)) - (st/emit! (udp/create-page data)) - (st/emit! (udp/persist-page-update-form data))))) + (st/emit! (udp/form->create-page data)) + (st/emit! (udp/form->update-page data))))) (defn- swap-size [event {:keys [data] :as form}] diff --git a/frontend/src/uxbox/util/spec.cljs b/frontend/src/uxbox/util/spec.cljs index 904f0392d6..6d706668d3 100644 --- a/frontend/src/uxbox/util/spec.cljs +++ b/frontend/src/uxbox/util/spec.cljs @@ -54,6 +54,7 @@ (s/def ::inst inst?) (s/def ::keyword keyword?) (s/def ::fn fn?) +(s/def ::coll coll?) (s/def ::not-empty-string (s/and string? #(not (str/empty? %))))