♻️ Refactor file persistence layer.

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

View file

@ -0,0 +1,48 @@
ALTER TABLE file
ADD COLUMN revn bigint NOT NULL DEFAULT 0,
ADD COLUMN data bytea NULL;
CREATE TABLE file_change (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
session_id uuid NULL DEFAULT NULL,
revn bigint NOT NULL DEFAULT 0,
data bytea NOT NULL,
changes bytea NULL DEFAULT NULL
);
CREATE TABLE file_share_token (
file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE,
page_id uuid NOT NULL,
token text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (file_id, token)
);
CREATE INDEX page_change_file_id_idx
ON file_change(file_id);
CREATE FUNCTION handle_file_update()
RETURNS TRIGGER AS $pagechange$
DECLARE
current_dt timestamptz := clock_timestamp();
BEGIN
NEW.modified_at := current_dt;
--- Update projects modified_at attribute when a
--- page of that project is modified.
UPDATE project
SET modified_at = current_dt
WHERE id = OLD.project_id;
RETURN NEW;
END;
$pagechange$ LANGUAGE plpgsql;
CREATE TRIGGER file_on_update_tgr
BEFORE UPDATE ON file
FOR EACH ROW EXECUTE PROCEDURE handle_file_update();

View file

@ -1,2 +1,2 @@
#!/usr/bin/env bash #!/usr/bin/env bash
PGPASSWORD=$app_DATABASE_PASSWORD psql $app_DATABASE_URI -U $app_DATABASE_USERNAME PGPASSWORD=$APP_DATABASE_PASSWORD psql $APP_DATABASE_URI -U $APP_DATABASE_USERNAME

View file

@ -36,9 +36,7 @@
:num-profiles-per-team 5 :num-profiles-per-team 5
:num-projects-per-team 5 :num-projects-per-team 5
:num-files-per-project 5 :num-files-per-project 5
:num-pages-per-file 3 :num-draft-files-per-profile 10})
:num-draft-files-per-profile 10
:num-draft-pages-per-file 3})
(defn- rng-ids (defn- rng-ids
[rng n max] [rng n max]
@ -75,27 +73,30 @@
(defn impl-run (defn impl-run
[opts] [opts]
(let [rng (java.util.Random. 1) (let [rng (java.util.Random. 1)]
(letfn [(create-profile [conn index]
(let [id (mk-uuid "profile" index)
_ (log/info "create profile" id)
create-profile prof (register-profile conn
(fn [conn index]
(let [id (mk-uuid "profile" index)]
(log/info "create profile" id)
(register-profile conn
{:id id {:id id
:fullname (str "Profile " index) :fullname (str "Profile " index)
:password "123123" :password "123123"
:demo? true :demo? true
:email (str "profile" index ".test@uxbox.io")}))) :email (str "profile" index ".test@uxbox.io")})
team-id (:default-team-id prof)
owner-id id]
(let [project-ids (collect (partial create-project conn team-id owner-id)
(range (:num-projects-per-team opts)))]
(run! (partial create-files conn owner-id) project-ids))
prof))
create-profiles (create-profiles [conn]
(fn [conn]
(log/info "create profiles") (log/info "create profiles")
(collect (partial create-profile conn) (collect (partial create-profile conn)
(range (:num-profiles opts)))) (range (:num-profiles opts))))
create-team (create-team [conn index]
(fn [conn index]
(let [id (mk-uuid "team" index) (let [id (mk-uuid "team" index)
name (str "Team" index)] name (str "Team" index)]
(log/info "create team" id) (log/info "create team" id)
@ -104,41 +105,19 @@
:photo ""}) :photo ""})
id)) id))
create-teams (create-teams [conn]
(fn [conn]
(log/info "create teams") (log/info "create teams")
(collect (partial create-team conn) (collect (partial create-team conn)
(range (:num-teams opts)))) (range (:num-teams opts))))
create-page (create-file [conn owner-id project-id index]
(fn [conn owner-id project-id file-id index]
(let [id (mk-uuid "page" project-id file-id index)
data cp/default-page-data
name (str "page " index)
version 0
ordering index
data (blob/encode data)]
(log/info "create page" id)
(db/insert! conn :page
{:id id
:file-id file-id
:name name
:ordering ordering
:data data})))
create-pages
(fn [conn owner-id project-id file-id]
(log/info "create pages")
(run! (partial create-page conn owner-id project-id file-id)
(range (:num-pages-per-file opts))))
create-file
(fn [conn owner-id project-id index]
(let [id (mk-uuid "file" project-id index) (let [id (mk-uuid "file" project-id index)
name (str "file" index)] name (str "file" index)
data (cp/make-file-data)]
(log/info "create file" id) (log/info "create file" id)
(db/insert! conn :file (db/insert! conn :file
{:id id {:id id
:data (blob/encode data)
:project-id project-id :project-id project-id
:name name}) :name name})
(db/insert! conn :file-profile-rel (db/insert! conn :file-profile-rel
@ -149,15 +128,12 @@
:can-edit true}) :can-edit true})
id)) id))
create-files (create-files [conn owner-id project-id]
(fn [conn owner-id project-id]
(log/info "create files") (log/info "create files")
(let [file-ids (collect (partial create-file conn owner-id project-id) (run! (partial create-file conn owner-id project-id)
(range (:num-files-per-project opts)))] (range (:num-files-per-project opts))))
(run! (partial create-pages conn owner-id project-id) file-ids)))
create-project (create-project [conn team-id owner-id index]
(fn [conn team-id owner-id index]
(let [id (mk-uuid "project" team-id index) (let [id (mk-uuid "project" team-id index)
name (str "project " index)] name (str "project " index)]
(log/info "create project" id) (log/info "create project" id)
@ -173,16 +149,14 @@
:can-edit true}) :can-edit true})
id)) id))
create-projects (create-projects [conn team-id profile-ids]
(fn [conn team-id profile-ids]
(log/info "create projects") (log/info "create projects")
(let [owner-id (rng-nth rng profile-ids) (let [owner-id (rng-nth rng profile-ids)
project-ids (collect (partial create-project conn team-id owner-id) project-ids (collect (partial create-project conn team-id owner-id)
(range (:num-projects-per-team opts)))] (range (:num-projects-per-team opts)))]
(run! (partial create-files conn owner-id) project-ids))) (run! (partial create-files conn owner-id) project-ids)))
assign-profile-to-team (assign-profile-to-team [conn team-id owner? profile-id]
(fn [conn team-id owner? profile-id]
(db/insert! conn :team-profile-rel (db/insert! conn :team-profile-rel
{:team-id team-id {:team-id team-id
:profile-id profile-id :profile-id profile-id
@ -190,16 +164,14 @@
:is-admin true :is-admin true
:can-edit true})) :can-edit true}))
setup-team (setup-team [conn team-id profile-ids]
(fn [conn team-id profile-ids]
(log/info "setup team" team-id profile-ids) (log/info "setup team" team-id profile-ids)
(assign-profile-to-team conn team-id true (first profile-ids)) (assign-profile-to-team conn team-id true (first profile-ids))
(run! (partial assign-profile-to-team conn team-id false) (run! (partial assign-profile-to-team conn team-id false)
(rest profile-ids)) (rest profile-ids))
(create-projects conn team-id profile-ids)) (create-projects conn team-id profile-ids))
assign-teams-and-profiles (assign-teams-and-profiles [conn teams profiles]
(fn [conn teams profiles]
(log/info "assign teams and profiles") (log/info "assign teams and profiles")
(loop [team-id (first teams) (loop [team-id (first teams)
teams (rest teams)] teams (rest teams)]
@ -210,22 +182,17 @@
(recur (first teams) (recur (first teams)
(rest teams)))))) (rest teams))))))
(create-draft-file [conn owner index]
create-draft-pages
(fn [conn owner-id file-id]
(log/info "create draft pages")
(run! (partial create-page conn owner-id nil file-id)
(range (:num-draft-pages-per-file opts))))
create-draft-file
(fn [conn owner index]
(let [owner-id (:id owner) (let [owner-id (:id owner)
id (mk-uuid "file" "draft" owner-id index) id (mk-uuid "file" "draft" owner-id index)
name (str "file" index) name (str "file" index)
project-id (:default-project-id owner)] project-id (:default-project-id owner)
data (cp/make-file-data)]
(log/info "create draft file" id) (log/info "create draft file" id)
(db/insert! conn :file (db/insert! conn :file
{:id id {:id id
:data (blob/encode data)
:project-id project-id :project-id project-id
:name name}) :name name})
(db/insert! conn :file-profile-rel (db/insert! conn :file-profile-rel
@ -236,18 +203,15 @@
:can-edit true}) :can-edit true})
id)) id))
create-draft-files (create-draft-files [conn profile]
(fn [conn profile] (run! (partial create-draft-file conn profile)
(let [file-ids (collect (partial create-draft-file conn profile) (range (:num-draft-files-per-profile opts))))
(range (:num-draft-files-per-profile opts)))]
(run! (partial create-draft-pages conn (:id profile)) file-ids)))
] ]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(let [profiles (create-profiles conn) (let [profiles (create-profiles conn)
teams (create-teams conn)] teams (create-teams conn)]
(assign-teams-and-profiles conn teams (map :id profiles)) (assign-teams-and-profiles conn teams (map :id profiles))
(run! (partial create-draft-files conn) profiles))))) (run! (partial create-draft-files conn) profiles))))))
(defn run* (defn run*
[preset] [preset]

View file

@ -31,203 +31,203 @@
;; --- Constants & Helpers ;; --- Constants & Helpers
(def ^:const +graphics-uuid-ns+ #uuid "3642a582-565f-4070-beba-af797ab27a6a") ;; (def ^:const +graphics-uuid-ns+ #uuid "3642a582-565f-4070-beba-af797ab27a6a")
(def ^:const +colors-uuid-ns+ #uuid "3642a582-565f-4070-beba-af797ab27a6c") ;; (def ^:const +colors-uuid-ns+ #uuid "3642a582-565f-4070-beba-af797ab27a6c")
(s/def ::id ::us/uuid) ;; (s/def ::id ::us/uuid)
(s/def ::name ::us/string) ;; (s/def ::name ::us/string)
(s/def ::path ::us/string) ;; (s/def ::path ::us/string)
(s/def ::regex #(instance? java.util.regex.Pattern %)) ;; (s/def ::regex #(instance? java.util.regex.Pattern %))
(s/def ::import-graphics ;; (s/def ::import-graphics
(s/keys :req-un [::path ::regex])) ;; (s/keys :req-un [::path ::regex]))
(s/def ::import-color ;; (s/def ::import-color
(s/* (s/cat :name ::us/string :color ::us/color))) ;; (s/* (s/cat :name ::us/string :color ::us/color)))
(s/def ::import-colors (s/coll-of ::import-color)) ;; (s/def ::import-colors (s/coll-of ::import-color))
(s/def ::import-library ;; (s/def ::import-library
(s/keys :req-un [::name] ;; (s/keys :req-un [::name]
:opt-un [::import-graphics ::import-colors])) ;; :opt-un [::import-graphics ::import-colors]))
(defn exit! ;; (defn exit!
([] (exit! 0)) ;; ([] (exit! 0))
([code] ;; ([code]
(System/exit code))) ;; (System/exit code)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Graphics Importer ;; ;; Graphics Importer
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- create-media-object ;; (defn- create-media-object
[conn file-id media-object-id localpath] ;; [conn file-id media-object-id localpath]
(s/assert fs/path? localpath) ;; (s/assert fs/path? localpath)
(s/assert ::us/uuid file-id) ;; (s/assert ::us/uuid file-id)
(s/assert ::us/uuid media-object-id) ;; (s/assert ::us/uuid media-object-id)
(let [filename (fs/name localpath) ;; (let [filename (fs/name localpath)
extension (second (fs/split-ext filename)) ;; extension (second (fs/split-ext filename))
file (io/as-file localpath) ;; file (io/as-file localpath)
mtype (case extension ;; mtype (case extension
".jpg" "image/jpeg" ;; ".jpg" "image/jpeg"
".png" "image/png" ;; ".png" "image/png"
".webp" "image/webp" ;; ".webp" "image/webp"
".svg" "image/svg+xml")] ;; ".svg" "image/svg+xml")]
(log/info "Creating image" filename media-object-id) ;; (log/info "Creating image" filename media-object-id)
(media/create-media-object conn {:content {:tempfile localpath ;; (media/create-media-object conn {:content {:tempfile localpath
:filename filename ;; :filename filename
:content-type mtype ;; :content-type mtype
:size (.length file)} ;; :size (.length file)}
:id media-object-id ;; :id media-object-id
:file-id file-id ;; :file-id file-id
:name filename ;; :name filename
:is-local false}))) ;; :is-local false})))
(defn- media-object-exists? ;; (defn- media-object-exists?
[conn id] ;; [conn id]
(s/assert ::us/uuid id) ;; (s/assert ::us/uuid id)
(let [row (db/get-by-id conn :media-object id)] ;; (let [row (db/get-by-id conn :media-object id)]
(if row true false))) ;; (if row true false)))
(defn- import-media-object-if-not-exists ;; (defn- import-media-object-if-not-exists
[conn file-id fpath] ;; [conn file-id fpath]
(s/assert ::us/uuid file-id) ;; (s/assert ::us/uuid file-id)
(s/assert fs/path? fpath) ;; (s/assert fs/path? fpath)
(let [media-object-id (uuid/namespaced +graphics-uuid-ns+ (str file-id (fs/name fpath)))] ;; (let [media-object-id (uuid/namespaced +graphics-uuid-ns+ (str file-id (fs/name fpath)))]
(when-not (media-object-exists? conn media-object-id) ;; (when-not (media-object-exists? conn media-object-id)
(create-media-object conn file-id media-object-id fpath)) ;; (create-media-object conn file-id media-object-id fpath))
media-object-id)) ;; media-object-id))
(defn- import-graphics ;; (defn- import-graphics
[conn file-id {:keys [path regex]}] ;; [conn file-id {:keys [path regex]}]
(run! (fn [fpath] ;; (run! (fn [fpath]
(when (re-matches regex (str fpath)) ;; (when (re-matches regex (str fpath))
(import-media-object-if-not-exists conn file-id fpath))) ;; (import-media-object-if-not-exists conn file-id fpath)))
(->> (fs/list-dir path) ;; (->> (fs/list-dir path)
(filter fs/regular-file?)))) ;; (filter fs/regular-file?))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Colors Importer ;; ;; Colors Importer
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- create-color ;; (defn- create-color
[conn file-id name content] ;; [conn file-id name content]
(s/assert ::us/uuid file-id) ;; (s/assert ::us/uuid file-id)
(s/assert ::us/color content) ;; (s/assert ::us/color content)
(let [color-id (uuid/namespaced +colors-uuid-ns+ (str file-id content))] ;; (let [color-id (uuid/namespaced +colors-uuid-ns+ (str file-id content))]
(log/info "Creating color" color-id "-" name content) ;; (log/info "Creating color" color-id "-" name content)
(colors/create-color conn {:id color-id ;; (colors/create-color conn {:id color-id
:file-id file-id ;; :file-id file-id
:name name ;; :name name
:content content}) ;; :content content})
color-id)) ;; color-id))
(defn- import-colors ;; (defn- import-colors
[conn file-id colors] ;; [conn file-id colors]
(db/delete! conn :color {:file-id file-id}) ;; (db/delete! conn :color {:file-id file-id})
(run! (fn [[name content]] ;; (run! (fn [[name content]]
(create-color conn file-id name content)) ;; (create-color conn file-id name content))
(partition-all 2 colors))) ;; (partition-all 2 colors)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Library files Importer ;; ;; Library files Importer
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- library-file-exists? ;; (defn- library-file-exists?
[conn id] ;; [conn id]
(s/assert ::us/uuid id) ;; (s/assert ::us/uuid id)
(let [row (db/get-by-id conn :file id)] ;; (let [row (db/get-by-id conn :file id)]
(if row true false))) ;; (if row true false)))
(defn- create-library-file-if-not-exists ;; (defn- create-library-file-if-not-exists
[conn project-id {:keys [name]}] ;; [conn project-id {:keys [name]}]
(let [id (uuid/namespaced +colors-uuid-ns+ name)] ;; (let [id (uuid/namespaced +colors-uuid-ns+ name)]
(when-not (library-file-exists? conn id) ;; (when-not (library-file-exists? conn id)
(log/info "Creating library-file:" name) ;; (log/info "Creating library-file:" name)
(files/create-file conn {:id id ;; (files/create-file conn {:id id
:profile-id uuid/zero ;; :profile-id uuid/zero
:project-id project-id ;; :project-id project-id
:name name ;; :name name
:is-shared true}) ;; :is-shared true})
(files/create-page conn {:file-id id})) ;; (files/create-page conn {:file-id id}))
id)) ;; id))
(defn- process-library ;; (defn- process-library
[conn basedir project-id {:keys [graphics colors] :as library}] ;; [conn basedir project-id {:keys [graphics colors] :as library}]
(us/verify ::import-library library) ;; (us/verify ::import-library library)
(let [library-file-id (create-library-file-if-not-exists conn project-id library)] ;; (let [library-file-id (create-library-file-if-not-exists conn project-id library)]
(when graphics ;; (when graphics
(->> (assoc graphics :path (fs/join basedir (:path graphics))) ;; (->> (assoc graphics :path (fs/join basedir (:path graphics)))
(import-graphics conn library-file-id))) ;; (import-graphics conn library-file-id)))
(when colors ;; (when colors
(import-colors conn library-file-id colors)))) ;; (import-colors conn library-file-id colors))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Entry Point ;; ;; Entry Point
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- project-exists? ;; (defn- project-exists?
[conn id] ;; [conn id]
(s/assert ::us/uuid id) ;; (s/assert ::us/uuid id)
(let [row (db/get-by-id conn :project id)] ;; (let [row (db/get-by-id conn :project id)]
(if row true false))) ;; (if row true false)))
(defn- create-project-if-not-exists ;; (defn- create-project-if-not-exists
[conn {:keys [name] :as project}] ;; [conn {:keys [name] :as project}]
(let [id (uuid/namespaced +colors-uuid-ns+ name)] ;; (let [id (uuid/namespaced +colors-uuid-ns+ name)]
(when-not (project-exists? conn id) ;; (when-not (project-exists? conn id)
(log/info "Creating project" name) ;; (log/info "Creating project" name)
(projects/create-project conn {:id id ;; (projects/create-project conn {:id id
:team-id uuid/zero ;; :team-id uuid/zero
:name name ;; :name name
:default? false})) ;; :default? false}))
id)) ;; id))
(defn- validate-path ;; (defn- validate-path
[path] ;; [path]
(let [path (if (symbol? path) (str path) path)] ;; (let [path (if (symbol? path) (str path) path)]
(log/infof "Trying to load config from '%s'." path) ;; (log/infof "Trying to load config from '%s'." path)
(when-not path ;; (when-not path
(log/error "No path is provided") ;; (log/error "No path is provided")
(exit! -1)) ;; (exit! -1))
(when-not (fs/exists? path) ;; (when-not (fs/exists? path)
(log/error "Path does not exists.") ;; (log/error "Path does not exists.")
(exit! -1)) ;; (exit! -1))
(when (fs/directory? path) ;; (when (fs/directory? path)
(log/error "The provided path is a directory.") ;; (log/error "The provided path is a directory.")
(exit! -1)) ;; (exit! -1))
(fs/path path))) ;; (fs/path path)))
(defn- read-file ;; (defn- read-file
[path] ;; [path]
(let [reader (PushbackReader. (io/reader path))] ;; (let [reader (PushbackReader. (io/reader path))]
[(fs/parent path) ;; [(fs/parent path)
(read reader)])) ;; (read reader)]))
(defn run* ;; (defn run*
[path] ;; [path]
(let [[basedir libraries] (read-file path)] ;; (let [[basedir libraries] (read-file path)]
(db/with-atomic [conn db/pool] ;; (db/with-atomic [conn db/pool]
(let [project-id (create-project-if-not-exists conn {:name "System libraries"})] ;; (let [project-id (create-project-if-not-exists conn {:name "System libraries"})]
(run! #(process-library conn basedir project-id %) libraries))))) ;; (run! #(process-library conn basedir project-id %) libraries)))))
(defn run ;; (defn run
[{:keys [path] :as params}] ;; [{:keys [path] :as params}]
(log/infof "Starting media loader.") ;; (log/infof "Starting media loader.")
(let [path (validate-path path)] ;; (let [path (validate-path path)]
(try ;; (try
(-> (mount/only #{#'app.config/config ;; (-> (mount/only #{#'app.config/config
#'app.db/pool ;; #'app.db/pool
#'app.migrations/migrations ;; #'app.migrations/migrations
#'app.media/semaphore ;; #'app.media/semaphore
#'app.media-storage/media-storage}) ;; #'app.media-storage/media-storage})
(mount/start)) ;; (mount/start))
(run* path) ;; (run* path)
(catch Exception e ;; (catch Exception e
(log/errorf e "Unhandled exception.")) ;; (log/errorf e "Unhandled exception."))
(finally ;; (finally
(mount/stop))))) ;; (mount/stop)))))

View file

@ -12,6 +12,7 @@
[mount.core :as mount :refer [defstate]] [mount.core :as mount :refer [defstate]]
[app.db :as db] [app.db :as db]
[app.config :as cfg] [app.config :as cfg]
[app.migrations.migration-0023 :as mg0023]
[app.util.migrations :as mg])) [app.util.migrations :as mg]))
(def +migrations+ (def +migrations+
@ -100,6 +101,15 @@
{:desc "Improve http session tables" {:desc "Improve http session tables"
:name "0021-http-session-improvements" :name "0021-http-session-improvements"
:fn (mg/resource "migrations/0021-http-session-improvements.sql")} :fn (mg/resource "migrations/0021-http-session-improvements.sql")}
{:desc "Refactor pages and files"
:name "0022-page-file-refactor"
:fn (mg/resource "migrations/0022-page-file-refactor.sql")}
{:desc "Adapt old pages and files to new format"
:name "0023-adapt-old-pages-and-files"
:fn mg0023/migrate}
]}) ]})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -0,0 +1,64 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.migrations.migration-0023
(:require
[app.db :as db]
[app.util.blob :as blob]))
(defn decode-row
[{:keys [data] :as row}]
(when row
(cond-> row
data (assoc :data (blob/decode data)))))
(defn retrieve-files
[conn]
(->> (db/exec! conn ["select * from file;"])
(map decode-row)))
(defn retrieve-pages
[conn file-id]
(->> (db/query conn :page {:file-id file-id})
(map decode-row)
(sort-by :ordering)))
(def empty-file-data
{:version 1
:pages []
:pages-index {}})
(defn pages->data
[pages]
(reduce (fn [acc {:keys [id data name] :as page}]
(let [data (-> data
(dissoc :version)
(assoc :id id :name name))]
(-> acc
(update :pages (fnil conj []) id)
(update :pages-index assoc id data))))
empty-file-data
pages))
(defn migrate-file
[conn {:keys [id] :as file}]
(let [pages (retrieve-pages conn (:id file))
data (pages->data pages)]
(db/update! conn :file
{:data (blob/encode data)}
{:id id})))
(defn migrate
[conn]
(let [files (retrieve-files conn)]
(doseq [file files]
(when (nil? (:data file))
(migrate-file conn file)))
(db/exec-one! conn ["drop table page cascade;"])))

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.init (ns app.services.init
"A initialization of services." "A initialization of services."
@ -18,7 +18,6 @@
(require 'app.services.queries.colors) (require 'app.services.queries.colors)
(require 'app.services.queries.projects) (require 'app.services.queries.projects)
(require 'app.services.queries.files) (require 'app.services.queries.files)
(require 'app.services.queries.pages)
(require 'app.services.queries.profile) (require 'app.services.queries.profile)
(require 'app.services.queries.recent-files) (require 'app.services.queries.recent-files)
(require 'app.services.queries.viewer)) (require 'app.services.queries.viewer))
@ -30,7 +29,6 @@
(require 'app.services.mutations.colors) (require 'app.services.mutations.colors)
(require 'app.services.mutations.projects) (require 'app.services.mutations.projects)
(require 'app.services.mutations.files) (require 'app.services.mutations.files)
(require 'app.services.mutations.pages)
(require 'app.services.mutations.profile)) (require 'app.services.mutations.profile))
(defstate query-services (defstate query-services

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.mutations.colors (ns app.services.mutations.colors
(:require (:require

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2016-2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.mutations.demo (ns app.services.mutations.demo
"A demo specific mutations." "A demo specific mutations."

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.mutations.files (ns app.services.mutations.files
(:require (:require
@ -14,16 +14,19 @@
[promesa.core :as p] [promesa.core :as p]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.common.pages-migrations :as pmg]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cfg] [app.config :as cfg]
[app.db :as db] [app.db :as db]
[app.redis :as redis]
[app.services.mutations :as sm] [app.services.mutations :as sm]
[app.services.mutations.projects :as proj] [app.services.mutations.projects :as proj]
[app.services.queries.files :as files] [app.services.queries.files :as files]
[app.tasks :as tasks] [app.tasks :as tasks]
[app.util.blob :as blob] [app.util.blob :as blob]
[app.util.storage :as ust] [app.util.storage :as ust]
[app.util.transit :as t]
[app.util.time :as dt])) [app.util.time :as dt]))
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -37,7 +40,6 @@
;; --- Mutation: Create File ;; --- Mutation: Create File
(declare create-file) (declare create-file)
(declare create-page)
(s/def ::is-shared ::us/boolean) (s/def ::is-shared ::us/boolean)
(s/def ::create-file (s/def ::create-file
@ -47,9 +49,7 @@
(sm/defmutation ::create-file (sm/defmutation ::create-file
[{:keys [profile-id project-id] :as params}] [{:keys [profile-id project-id] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(let [file (create-file conn params) (create-file conn params)))
page (create-page conn (assoc params :file-id (:id file)))]
(assoc file :pages [(:id page)]))))
(defn- create-file-profile (defn- create-file-profile
[conn {:keys [profile-id file-id] :as params}] [conn {:keys [profile-id file-id] :as params}]
@ -65,24 +65,16 @@
:or {is-shared false} :or {is-shared false}
:as params}] :as params}]
(let [id (or id (uuid/next)) (let [id (or id (uuid/next))
data (cp/make-file-data)
file (db/insert! conn :file file (db/insert! conn :file
{:id id {:id id
:project-id project-id :project-id project-id
:name name :name name
:is-shared is-shared})] :is-shared is-shared
:data (blob/encode data)})]
(->> (assoc params :file-id id) (->> (assoc params :file-id id)
(create-file-profile conn)) (create-file-profile conn))
file)) (assoc file :data data)))
(defn create-page
[conn {:keys [file-id] :as params}]
(let [id (uuid/next)]
(db/insert! conn :page
{:id id
:file-id file-id
:name "Page 1"
:ordering 1
:data (blob/encode cp/default-page-data)})))
;; --- Mutation: Rename File ;; --- Mutation: Rename File
@ -195,3 +187,93 @@
{:file-id file-id {:file-id file-id
:library-file-id library-id})) :library-file-id library-id}))
;; A generic, Changes based (granular) file update method.
(s/def ::changes
(s/coll-of map? :kind vector?))
(s/def ::session-id ::us/uuid)
(s/def ::revn ::us/integer)
(s/def ::update-file
(s/keys :req-un [::id ::session-id ::profile-id ::revn ::changes]))
(declare update-file)
(declare retrieve-lagged-changes)
(declare insert-change)
(sm/defmutation ::update-file
[{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool]
(let [{:keys [id] :as file} (db/get-by-id conn :file id {:for-update true})]
(files/check-edition-permissions! conn profile-id id)
(update-file conn file params))))
(defn- update-file
[conn file params]
(when (> (:revn params)
(:revn file))
(ex/raise :type :validation
:code :revn-conflict
:hint "The incoming revision number is greater that stored version."
:context {:incoming-revn (:revn params)
:stored-revn (:revn file)}))
(let [sid (:session-id params)
changes (:changes params)
file (-> file
(update :data blob/decode)
(update :data pmg/migrate-data)
(update :data cp/process-changes changes)
(update :data blob/encode)
(update :revn inc)
(assoc :changes (blob/encode changes)
:session-id sid))
chng (insert-change conn file)
msg {:type :file-change
:profile-id (:profile-id params)
:file-id (:id file)
:session-id sid
:revn (:revn file)
:changes changes}]
@(redis/run! :publish {:channel (str (:id file))
:message (t/encode-str msg)})
(db/update! conn :file
{:revn (:revn file)
:data (:data file)}
{:id (:id file)})
(retrieve-lagged-changes conn chng params)))
(defn- insert-change
[conn {:keys [revn data changes session-id] :as file}]
(let [id (uuid/next)
file-id (:id file)]
(db/insert! conn :file-change
{:id id
:session-id session-id
:file-id file-id
:revn revn
:data data
:changes changes})))
(def ^:private
sql:lagged-changes
"select s.id, s.revn, s.file_id,
s.session_id, s.changes
from file_change as s
where s.file_id = ?
and s.revn > ?
order by s.created_at asc")
(defn- retrieve-lagged-changes
[conn snapshot params]
(->> (db/exec! conn [sql:lagged-changes (:id params) (:revn params)])
(mapv files/decode-row)))

View file

@ -1,255 +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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
(ns app.services.mutations.pages
(:require
[clojure.spec.alpha :as s]
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.pages :as cp]
[app.common.pages-migrations :as pmg]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.services.mutations :as sm]
[app.services.queries.files :as files]
[app.services.queries.pages :refer [decode-row]]
[app.tasks :as tasks]
[app.redis :as redis]
[app.util.blob :as blob]
[app.util.time :as dt]
[app.util.transit :as t]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::name ::us/string)
(s/def ::data ::cp/data)
(s/def ::profile-id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::ordering ::us/number)
(s/def ::file-id ::us/uuid)
;; --- Mutation: Create Page
(declare create-page)
(s/def ::create-page
(s/keys :req-un [::profile-id ::file-id ::name ::ordering ::data]
:opt-un [::id]))
(sm/defmutation ::create-page
[{:keys [profile-id file-id] :as params}]
(db/with-atomic [conn db/pool]
(files/check-edition-permissions! conn profile-id file-id)
(create-page conn params)))
(defn- create-page
[conn {:keys [id file-id name ordering data] :as params}]
(let [id (or id (uuid/next))
data (blob/encode data)]
(-> (db/insert! conn :page
{:id id
:file-id file-id
:name name
:ordering ordering
:data data})
(decode-row))))
;; --- Mutation: Rename Page
(declare rename-page)
(declare select-page-for-update)
(s/def ::rename-page
(s/keys :req-un [::id ::name ::profile-id]))
(sm/defmutation ::rename-page
[{:keys [id name profile-id]}]
(db/with-atomic [conn db/pool]
(let [page (select-page-for-update conn id)]
(files/check-edition-permissions! conn profile-id (:file-id page))
(rename-page conn (assoc page :name name)))))
(defn- select-page-for-update
[conn id]
(db/get-by-id conn :page id {:for-update true}))
(defn- rename-page
[conn {:keys [id name] :as params}]
(db/update! conn :page
{:name name}
{:id id}))
;; --- Mutation: Sort Pages
(s/def ::page-ids (s/every ::us/uuid :kind vector?))
(s/def ::reorder-pages
(s/keys :req-un [::profile-id ::file-id ::page-ids]))
(declare update-page-ordering)
(sm/defmutation ::reorder-pages
[{:keys [profile-id file-id page-ids]}]
(db/with-atomic [conn db/pool]
(run! #(update-page-ordering conn file-id %)
(d/enumerate page-ids))
nil))
(defn- update-page-ordering
[conn file-id [ordering page-id]]
(db/update! conn :page
{:ordering ordering}
{:file-id file-id
:id page-id}))
;; --- Mutation: Generate Share Token
(declare assign-page-share-token)
(s/def ::generate-page-share-token
(s/keys :req-un [::id]))
(sm/defmutation ::generate-page-share-token
[{:keys [id] :as params}]
(let [token (-> (sodi.prng/random-bytes 16)
(sodi.util/bytes->b64s))]
(db/with-atomic [conn db/pool]
(db/update! conn :page
{:share-token token}
{:id id}))))
;; --- Mutation: Clear Share Token
(s/def ::clear-page-share-token
(s/keys :req-un [::id]))
(sm/defmutation ::clear-page-share-token
[{:keys [id] :as params}]
(db/with-atomic [conn db/pool]
(db/update! conn :page
{:share-token nil}
{:id id})))
;; --- Mutation: Update Page
;; A generic, Changes based (granular) page update method.
(s/def ::changes
(s/coll-of map? :kind vector?))
(s/def ::session-id ::us/uuid)
(s/def ::revn ::us/integer)
(s/def ::update-page
(s/keys :req-un [::id ::session-id ::profile-id ::revn ::changes]))
(declare update-page)
(declare retrieve-lagged-changes)
(declare insert-page-change!)
(sm/defmutation ::update-page
[{:keys [id profile-id] :as params}]
(db/with-atomic [conn db/pool]
(let [{:keys [file-id] :as page} (select-page-for-update conn id)]
(files/check-edition-permissions! conn profile-id file-id)
(update-page conn page params))))
(defn- update-page
[conn page params]
(when (> (:revn params)
(:revn page))
(ex/raise :type :validation
:code :revn-conflict
:hint "The incoming revision number is greater that stored version."
:context {:incoming-revn (:revn params)
:stored-revn (:revn page)}))
(let [sid (:session-id params)
changes (:changes params)
page (-> page
(update :data blob/decode)
(update :data pmg/migrate-data)
(update :data cp/process-changes changes)
(update :data blob/encode)
(update :revn inc)
(assoc :changes (blob/encode changes)
:session-id sid))
chng (insert-page-change! conn page)
msg {:type :page-change
:profile-id (:profile-id params)
:page-id (:id page)
:session-id sid
:revn (:revn page)
:changes changes}]
@(redis/run! :publish {:channel (str (:file-id page))
:message (t/encode-str msg)})
(db/update! conn :page
{:revn (:revn page)
:data (:data page)}
{:id (:id page)})
(retrieve-lagged-changes conn chng params)))
(defn- insert-page-change!
[conn {:keys [revn data changes session-id] :as page}]
(let [id (uuid/next)
page-id (:id page)]
(db/insert! conn :page-change
{:id id
:session-id session-id
:page-id page-id
:revn revn
:data data
:changes changes})))
(def ^:private
sql:lagged-changes
"select s.id, s.revn, s.page_id,
s.session_id, s.changes
from page_change as s
where s.page_id = ?
and s.revn > ?
order by s.created_at asc")
(defn- retrieve-lagged-changes
[conn snapshot params]
(->> (db/exec! conn [sql:lagged-changes (:id params) (:revn params)])
(mapv decode-row)))
;; --- Mutation: Delete Page
(declare mark-page-deleted)
(s/def ::delete-page
(s/keys :req-un [::profile-id ::id]))
(sm/defmutation ::delete-page
[{:keys [id profile-id]}]
(db/with-atomic [conn db/pool]
(let [page (select-page-for-update conn id)]
(files/check-edition-permissions! conn profile-id (:file-id page))
;; Schedule object deletion
(tasks/submit! conn {:name "delete-object"
:delay cfg/default-deletion-delay
:props {:id id :type :page}})
(db/update! conn :page
{:deleted-at (dt/now)}
{:id id})
nil)))

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.services.mutations.teams (ns app.services.mutations.teams
(:require (:require
@ -57,23 +57,3 @@
:is-owner true :is-owner true
:is-admin true :is-admin true
:can-edit true})) :can-edit true}))
;; --- Mutation: Team Edition Permissions
(def ^:private sql:team-permissions
"select tpr.is_owner,
tpr.is_admin,
tpr.can_edit
from team_profile_rel as tpr
where tpr.profile_id = ?
and tpr.team_id = ?")
(defn check-edition-permissions!
[conn profile-id team-id]
(let [row (db/exec-one! conn [sql:team-permissions profile-id team-id])]
(when-not (or (= team-id uuid/zero)
(:can-edit row)
(:is-admin row)
(:is-owner row))
(ex/raise :type :validation
:code :not-authorized))))

View file

@ -0,0 +1,65 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
(ns app.services.mutations.viewer
(:require
[app.common.exceptions :as ex]
[app.common.pages :as cp]
[app.common.pages-migrations :as pmg]
[app.common.spec :as us]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.db :as db]
[app.redis :as redis]
[app.services.mutations :as sm]
[app.services.mutations.projects :as proj]
[app.services.queries.files :as files]
[app.tasks :as tasks]
[app.util.blob :as blob]
[app.util.storage :as ust]
[app.util.time :as dt]
[app.util.transit :as t]
[clojure.spec.alpha :as s]
[datoteka.core :as fs]
[promesa.core :as p]
[sodi.prng]
[sodi.util]))
(s/def ::profile-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::page-id ::us/uuid)
(s/def ::create-file-share-token
(s/keys :req-un [::profile-id ::file-id ::page-id]))
(sm/defmutation ::create-file-share-token
[{:keys [profile-id file-id page-id] :as params}]
(db/with-atomic [conn db/pool]
(files/check-edition-permissions! conn profile-id file-id)
(let [token (-> (sodi.prng/random-bytes 16)
(sodi.util/bytes->b64s))]
(db/insert! conn :file-share-token
{:file-id file-id
:page-id page-id
:token token})
{:token token})))
(s/def ::token ::us/not-empty-string)
(s/def ::delete-file-share-token
(s/keys :req-un [::profile-id ::file-id ::token]))
(sm/defmutation ::delete-file-share-token
[{:keys [profile-id file-id token]}]
(db/with-atomic [conn db/pool]
(files/check-edition-permissions! conn profile-id file-id)
(db/delete! conn :file-share-token
{:file-id file-id
:token token})
nil))

View file

@ -10,17 +10,17 @@
(ns app.services.notifications (ns app.services.notifications
"A websocket based notifications mechanism." "A websocket based notifications mechanism."
(:require (:require
[clojure.core.async :as a :refer [>! <!]]
[clojure.tools.logging :as log]
[promesa.core :as p]
[ring.adapter.jetty9 :as jetty]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.redis :as redis]
[app.metrics :as mtx] [app.metrics :as mtx]
[app.redis :as redis]
[app.util.time :as dt] [app.util.time :as dt]
[app.util.transit :as t])) [app.util.transit :as t]
[clojure.core.async :as a :refer [>! <!]]
[clojure.tools.logging :as log]
[promesa.core :as p]
[ring.adapter.jetty9 :as jetty]))
(defmacro go-try (defmacro go-try
[& body] [& body]
@ -44,8 +44,6 @@
(catch Throwable e# (catch Throwable e#
e#)))) e#))))
;; --- Redis Interactions
(defn- publish (defn- publish
[channel message] [channel message]
(go-try (go-try
@ -187,14 +185,6 @@
(defrecord WebSocket [conn in out sub]) (defrecord WebSocket [conn in out sub])
(defn- start-rcv-loop!
[{:keys [conn out] :as ws}]
(a/go-loop []
(let [val (a/<! out)]
(when-not (nil? val)
(jetty/send! conn (t/encode-str val))
(recur)))))
(defonce metrics-active-connections (defonce metrics-active-connections
(mtx/gauge {:id "notificatons__active_connections" (mtx/gauge {:id "notificatons__active_connections"
:help "Active connections to the notifications service."})) :help "Active connections to the notifications service."}))
@ -207,30 +197,42 @@
[{:keys [file-id profile-id] :as params}] [{:keys [file-id profile-id] :as params}]
(let [in (a/chan 32) (let [in (a/chan 32)
out (a/chan 32)] out (a/chan 32)]
{:on-connect (fn [conn] {:on-connect
(fn [conn]
(metrics-active-connections :inc) (metrics-active-connections :inc)
(let [xf (map t/decode-str) (let [xf (map t/decode-str)
sub (redis/subscribe (str file-id) xf) sub (redis/subscribe (str file-id) xf)
ws (WebSocket. conn in out sub nil params)] ws (WebSocket. conn in out sub nil params)]
(start-rcv-loop! ws)
;; RCV LOOP
(a/go-loop []
(let [val (a/<! out)]
(when-not (nil? val)
(jetty/send! conn (t/encode-str val))
(recur))))
(a/go (a/go
(a/<! (on-subscribed ws)) (a/<! (on-subscribed ws))
(a/close! sub)))) (a/close! sub))))
:on-error (fn [conn e] :on-error
(fn [conn e]
(a/close! out) (a/close! out)
(a/close! in)) (a/close! in))
:on-close (fn [conn status-code reason] :on-close
(fn [conn status-code reason]
(metrics-active-connections :dec) (metrics-active-connections :dec)
(a/close! out) (a/close! out)
(a/close! in)) (a/close! in))
:on-text (fn [ws message] :on-text
(fn [ws message]
(metrics-message-counter :inc) (metrics-message-counter :inc)
(let [message (t/decode-str message)] (let [message (t/decode-str message)]
(a/>!! in message))) (a/>!! in message)))
:on-bytes (constantly nil)})) :on-bytes
(constantly nil)}))

View file

@ -11,14 +11,17 @@
(:require (:require
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[promesa.core :as p] [promesa.core :as p]
[app.common.pages-migrations :as pmg]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.db :as db] [app.db :as db]
[app.media :as media] [app.media :as media]
[app.services.queries :as sq] [app.services.queries :as sq]
[app.services.queries.projects :as projects]
[app.util.blob :as blob])) [app.util.blob :as blob]))
(declare decode-row) (declare decode-row)
(declare decode-row-xf)
;; --- Helpers & Specs ;; --- Helpers & Specs
@ -32,6 +35,8 @@
;; --- Query: Files search ;; --- Query: Files search
;; TODO: this query need to a good refactor
(def ^:private sql:search-files (def ^:private sql:search-files
"with projects as ( "with projects as (
select p.* select p.*
@ -82,58 +87,16 @@
profile-id team-id profile-id team-id
profile-id team-id profile-id team-id
search-term])] search-term])]
(mapv decode-row rows))) (into [] decode-row-xf rows)))
;; --- Query: Project Files ;; --- Query: Project Files
(def ^:private sql:files (def ^:private sql:files
"with projects as ( "select f.*
select p.*
from project as p
inner join team_profile_rel as tpr on (tpr.team_id = p.team_id)
where tpr.profile_id = ?
and p.deleted_at is null
and (tpr.is_admin = true or
tpr.is_owner = true or
tpr.can_edit = true)
union
select p.*
from project as p
inner join project_profile_rel as ppr on (ppr.project_id = p.id)
where ppr.profile_id = ?
and p.deleted_at is null
and (ppr.is_admin = true or
ppr.is_owner = true or
ppr.can_edit = true)
union
select p.*
from project as p
where p.team_id = uuid_nil()
and p.deleted_at is null
)
select distinct
f.*,
array_agg(pg.id) over pages_w as pages,
first_value(pg.data) over pages_w as data
from file as f from file as f
left join page as pg on (f.id = pg.file_id)
where f.project_id = ? where f.project_id = ?
and (exists (select *
from file_profile_rel as fp_r
where fp_r.profile_id = ?
and fp_r.file_id = f.id
and (fp_r.is_admin = true or
fp_r.is_owner = true or
fp_r.can_edit = true))
or exists (select *
from projects as p
where p.id = f.project_id))
and f.deleted_at is null and f.deleted_at is null
and pg.deleted_at is null
window pages_w as (partition by f.id order by pg.ordering
range between unbounded preceding
and unbounded following)
order by f.modified_at desc") order by f.modified_at desc")
(s/def ::project-id ::us/uuid) (s/def ::project-id ::us/uuid)
@ -142,10 +105,10 @@
(sq/defquery ::files (sq/defquery ::files
[{:keys [profile-id project-id] :as params}] [{:keys [profile-id project-id] :as params}]
(->> (db/exec! db/pool [sql:files (with-open [conn (db/open)]
profile-id profile-id (let [project (db/get-by-id conn :project project-id)]
project-id profile-id]) (projects/check-edition-permissions! conn profile-id project)
(mapv decode-row))) (into [] decode-row-xf (db/exec! conn [sql:files project-id])))))
;; --- Query: File Permissions ;; --- Query: File Permissions
@ -173,12 +136,7 @@
from project_profile_rel as ppr from project_profile_rel as ppr
inner join file as f on (f.project_id = ppr.project_id) inner join file as f on (f.project_id = ppr.project_id)
where f.id = ? where f.id = ?
and ppr.profile_id = ? and ppr.profile_id = ?")
union all
select true, true, true
from file as f
inner join project as p on (f.project_id = p.id)
and p.team_id = uuid_nil();")
(defn check-edition-permissions! (defn check-edition-permissions!
[conn profile-id file-id] [conn profile-id file-id]
@ -198,24 +156,11 @@
;; --- Query: File (By ID) ;; --- Query: File (By ID)
(def ^:private sql:file
"select f.*,
array_agg(pg.id) over pages_w as pages
from file as f
left join page as pg on (f.id = pg.file_id)
where f.id = ?
and f.deleted_at is null
and pg.deleted_at is null
window pages_w as (partition by f.id order by pg.ordering
range between unbounded preceding
and unbounded following)")
(defn retrieve-file (defn retrieve-file
[conn id] [conn id]
(let [row (db/exec-one! conn [sql:file id])] (let [file (db/get-by-id conn :file id)]
(when-not row (-> (decode-row file)
(ex/raise :type :not-found)) (pmg/migrate-file))))
(decode-row row)))
(s/def ::file (s/def ::file
(s/keys :req-un [::profile-id ::id])) (s/keys :req-un [::profile-id ::id]))
@ -226,6 +171,15 @@
(check-edition-permissions! conn profile-id id) (check-edition-permissions! conn profile-id id)
(retrieve-file conn id))) (retrieve-file conn id)))
(s/def ::page
(s/keys :req-un [::profile-id ::id ::file-id]))
(sq/defquery ::page
[{:keys [profile-id file-id id]}]
(db/with-atomic [conn db/pool]
(check-edition-permissions! conn profile-id file-id)
(let [file (retrieve-file conn file-id)]
(get-in file [:data :pages-index id]))))
;; --- Query: File users ;; --- Query: File users
@ -256,14 +210,12 @@
(check-edition-permissions! conn profile-id id) (check-edition-permissions! conn profile-id id)
(retrieve-file-users conn id))) (retrieve-file-users conn id)))
;; --- Query: Shared Library Files ;; --- Query: Shared Library Files
;; TODO: remove the counts, because they are no longer needed.
(def ^:private sql:shared-files (def ^:private sql:shared-files
"select distinct "select f.*,
f.*,
array_agg(pg.id) over pages_w as pages,
first_value(pg.data) over pages_w as data,
(select count(*) from color as c (select count(*) from color as c
where c.file_id = f.id where c.file_id = f.id
and c.deleted_at is null) as colors_count, and c.deleted_at is null) as colors_count,
@ -272,16 +224,11 @@
and m.is_local = false and m.is_local = false
and m.deleted_at is null) as graphics_count and m.deleted_at is null) as graphics_count
from file as f from file as f
left join page as pg on (f.id = pg.file_id)
inner join project as p on (p.id = f.project_id) inner join project as p on (p.id = f.project_id)
where f.is_shared = true where f.is_shared = true
and f.deleted_at is null and f.deleted_at is null
and pg.deleted_at is null
and p.deleted_at is null and p.deleted_at is null
and p.team_id = ? and p.team_id = ?
window pages_w as (partition by f.id order by pg.ordering
range between unbounded preceding
and unbounded following)
order by f.modified_at desc") order by f.modified_at desc")
(s/def ::shared-files (s/def ::shared-files
@ -289,30 +236,21 @@
(sq/defquery ::shared-files (sq/defquery ::shared-files
[{:keys [profile-id team-id] :as params}] [{:keys [profile-id team-id] :as params}]
(->> (db/exec! db/pool [sql:shared-files team-id]) (into [] decode-row-xf (db/exec! db/pool [sql:shared-files team-id])))
(mapv decode-row)))
;; --- Query: File Libraries used by a File ;; --- Query: File Libraries used by a File
(def ^:private sql:file-libraries (def ^:private sql:file-libraries
"select fl.*, "select fl.*
array_agg(pg.id) over pages_w as pages,
first_value(pg.data) over pages_w as data
from file as fl from file as fl
left join page as pg on (fl.id = pg.file_id)
inner join file_library_rel as flr on (flr.library_file_id = fl.id) inner join file_library_rel as flr on (flr.library_file_id = fl.id)
where flr.file_id = ? where flr.file_id = ?
and fl.deleted_at is null and fl.deleted_at is null")
and pg.deleted_at is null
window pages_w as (partition by fl.id order by pg.ordering
range between unbounded preceding
and unbounded following)")
(defn retrieve-file-libraries (defn retrieve-file-libraries
[conn file-id] [conn file-id]
(->> (db/exec! conn [sql:file-libraries file-id]) (into [] decode-row-xf (db/exec! conn [sql:file-libraries file-id])))
(mapv decode-row)))
(s/def ::file-libraries (s/def ::file-libraries
(s/keys :req-un [::profile-id ::file-id])) (s/keys :req-un [::profile-id ::file-id]))
@ -326,6 +264,8 @@
;; --- Query: Single File Library ;; --- Query: Single File Library
;; TODO: this looks like is duplicate of `::file`
(def ^:private sql:file-library (def ^:private sql:file-library
"select fl.* "select fl.*
from file as fl from file as fl
@ -333,10 +273,10 @@
(defn retrieve-file-library (defn retrieve-file-library
[conn file-id] [conn file-id]
(let [row (db/exec-one! conn [sql:file-library file-id])] (let [rows (db/exec! conn [sql:file-library file-id])]
(when-not row (when-not (seq rows)
(ex/raise :type :not-found)) (ex/raise :type :not-found))
row)) (first (sequence decode-row-xf rows))))
(s/def ::file-library (s/def ::file-library
(s/keys :req-un [::profile-id ::file-id])) (s/keys :req-un [::profile-id ::file-id]))
@ -351,8 +291,13 @@
;; --- Helpers ;; --- Helpers
(defn decode-row (defn decode-row
[{:keys [pages data] :as row}] [{:keys [pages data changes] :as row}]
(when row (when row
(cond-> row (cond-> row
changes (assoc :changes (blob/decode changes))
data (assoc :data (blob/decode data)) data (assoc :data (blob/decode data))
pages (assoc :pages (vec (.getArray pages)))))) pages (assoc :pages (vec (.getArray pages))))))
(def decode-row-xf
(comp (map decode-row)
(map pmg/migrate-file)))

View file

@ -1,122 +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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz>
(ns app.services.queries.pages
(:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[app.common.spec :as us]
[app.common.exceptions :as ex]
[app.common.pages-migrations :as pmg]
[app.db :as db]
[app.services.queries :as sq]
[app.services.queries.files :as files]
[app.util.blob :as blob]))
;; --- Helpers & Specs
(declare decode-row)
(s/def ::id ::us/uuid)
(s/def ::profile-id ::us/uuid)
(s/def ::project-id ::us/uuid)
(s/def ::file-id ::us/uuid)
;; --- Query: Pages (By File ID)
(declare retrieve-pages)
(s/def ::pages
(s/keys :req-un [::profile-id ::file-id]))
(sq/defquery ::pages
[{:keys [profile-id file-id] :as params}]
(db/with-atomic [conn db/pool]
(files/check-edition-permissions! conn profile-id file-id)
(->> (retrieve-pages conn params)
(mapv #(update % :data pmg/migrate-data)))))
(def ^:private sql:pages
"select p.*
from page as p
where p.file_id = ?
and p.deleted_at is null
order by p.created_at asc")
(defn- retrieve-pages
[conn {:keys [profile-id file-id] :as params}]
(->> (db/exec! conn [sql:pages file-id])
(mapv decode-row)))
;; --- Query: Single Page (By ID)
(declare retrieve-page)
(s/def ::page
(s/keys :req-un [::profile-id ::id]))
(sq/defquery ::page
[{:keys [profile-id id] :as params}]
(with-open [conn (db/open)]
(let [page (retrieve-page conn id)]
(files/check-edition-permissions! conn profile-id (:file-id page))
(-> page
(update :data pmg/migrate-data)))))
(def ^:private sql:page
"select p.* from page as p where id=?")
(defn retrieve-page
[conn id]
(let [row (db/exec-one! conn [sql:page id])]
(when-not row
(ex/raise :type :not-found))
(decode-row row)))
;; --- Query: Page Changes
(def ^:private
sql:page-changes
"select pc.id,
pc.created_at,
pc.changes,
pc.revn
from page_change as pc
where pc.page_id=?
order by pc.revn asc
limit ?
offset ?")
(s/def ::skip ::us/integer)
(s/def ::limit ::us/integer)
(s/def ::page-changes
(s/keys :req-un [::profile-id ::id ::skip ::limit]))
(defn retrieve-page-changes
[conn id skip limit]
(->> (db/exec! conn [sql:page-changes id limit skip])
(mapv decode-row)))
(sq/defquery ::page-changes
[{:keys [profile-id id skip limit]}]
(when *assert*
(-> (db/exec! db/pool [sql:page-changes id limit skip])
(mapv decode-row))))
;; --- Helpers
(defn decode-row
[{:keys [data metadata changes] :as row}]
(when row
(cond-> row
data (assoc :data (blob/decode data))
changes (assoc :changes (blob/decode changes)))))

View file

@ -76,8 +76,9 @@
where f.project_id = p.id where f.project_id = p.id
and deleted_at is null) and deleted_at is null)
from project as p from project as p
where team_id = ? where p.team_id = ?
order by modified_at desc") and p.deleted_at is null
order by p.modified_at desc")
(defn retrieve-projects (defn retrieve-projects
[conn team-id] [conn team-id]

View file

@ -16,21 +16,13 @@
[app.services.queries :as sq] [app.services.queries :as sq]
[app.services.queries.teams :as teams] [app.services.queries.teams :as teams]
[app.services.queries.projects :as projects :refer [retrieve-projects]] [app.services.queries.projects :as projects :refer [retrieve-projects]]
[app.services.queries.files :refer [decode-row]])) [app.services.queries.files :refer [decode-row-xf]]))
(def sql:project-recent-files (def sql:project-recent-files
"select distinct "select f.*
f.*,
array_agg(pg.id) over pages_w as pages,
first_value(pg.data) over pages_w as data
from file as f from file as f
left join page as pg on (f.id = pg.file_id)
where f.project_id = ? where f.project_id = ?
and f.deleted_at is null and f.deleted_at is null
and pg.deleted_at is null
window pages_w as (partition by f.id order by pg.ordering
range between unbounded preceding
and unbounded following)
order by f.modified_at desc order by f.modified_at desc
limit 5") limit 5")
@ -38,8 +30,7 @@
[conn profile-id project] [conn profile-id project]
(let [project-id (:id project)] (let [project-id (:id project)]
(projects/check-edition-permissions! conn profile-id project) (projects/check-edition-permissions! conn profile-id project)
(->> (db/exec! conn [sql:project-recent-files project-id]) (into [] decode-row-xf (db/exec! conn [sql:project-recent-files project-id]))))
(map decode-row))))
(s/def ::team-id ::us/uuid) (s/def ::team-id ::us/uuid)
(s/def ::profile-id ::us/uuid) (s/def ::profile-id ::us/uuid)

View file

@ -9,27 +9,18 @@
(ns app.services.queries.viewer (ns app.services.queries.viewer
(:require (:require
[clojure.spec.alpha :as s]
[promesa.core :as p]
[promesa.exec :as px]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.spec :as us] [app.common.spec :as us]
[app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.services.queries :as sq] [app.services.queries :as sq]
[app.services.queries.files :as files] [app.services.queries.files :as files]
[app.services.queries.media :as media-queries] [clojure.spec.alpha :as s]))
[app.services.queries.pages :as pages]
[app.util.blob :as blob]
[app.util.data :as data]))
;; --- Helpers & Specs
(s/def ::id ::us/uuid)
(s/def ::page-id ::us/uuid)
;; --- Query: Viewer Bundle (by Page ID) ;; --- Query: Viewer Bundle (by Page ID)
(declare check-shared-token!)
(declare retrieve-shared-token)
(def ^:private (def ^:private
sql:project sql:project
"select p.id, p.name "select p.id, p.name
@ -41,24 +32,45 @@
[conn id] [conn id]
(db/exec-one! conn [sql:project id])) (db/exec-one! conn [sql:project id]))
(s/def ::id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::page-id ::us/uuid)
(s/def ::share-token ::us/string) (s/def ::share-token ::us/string)
(s/def ::viewer-bundle (s/def ::viewer-bundle
(s/keys :req-un [::page-id] (s/keys :req-un [::file-id ::page-id]
:opt-un [::profile-id ::share-token])) :opt-un [::profile-id ::share-token]))
(sq/defquery ::viewer-bundle (sq/defquery ::viewer-bundle
[{:keys [profile-id page-id share-token] :as params}] [{:keys [profile-id file-id page-id share-token] :as params}]
(db/with-atomic [conn db/pool] (db/with-atomic [conn db/pool]
(let [page (pages/retrieve-page conn page-id) (let [file (files/retrieve-file conn file-id)
file (files/retrieve-file conn (:file-id page))
images (media-queries/retrieve-media-objects conn (:file-id page) true) project (retrieve-project conn (:project-id file))
project (retrieve-project conn (:project-id file))] page (get-in file [:data :pages-index page-id])
bundle {:file (dissoc file :data)
:page (get-in file [:data :pages-index page-id])
:project project}]
(if (string? share-token) (if (string? share-token)
(when (not= share-token (:share-token page)) (do
(check-shared-token! conn file-id page-id share-token)
(assoc bundle :share-token share-token))
(let [token (retrieve-shared-token conn file-id page-id)]
(files/check-edition-permissions! conn profile-id file-id)
(assoc bundle :share-token token))))))
(defn check-shared-token!
[conn file-id page-id token]
(let [sql "select exists(select 1 from file_share_token where file_id=? and page_id=? and token=?) as exists"]
(when-not (:exists (db/exec-one! conn [sql file-id page-id token]))
(ex/raise :type :validation (ex/raise :type :validation
:code :not-authorized)) :code :not-authorized))))
(files/check-edition-permissions! conn profile-id (:file-id page)))
{:page page (defn retrieve-shared-token
:file file [conn file-id page-id]
:images images (let [sql "select * from file_share_token where file_id=? and page_id=?"]
:project project}))) (db/exec-one! conn [sql file-id page-id])))

View file

@ -1,3 +1,12 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.tests.helpers (ns app.tests.helpers
(:require (:require
[clojure.java.io :as io] [clojure.java.io :as io]
@ -13,7 +22,6 @@
[app.services.mutations.projects :as projects] [app.services.mutations.projects :as projects]
[app.services.mutations.teams :as teams] [app.services.mutations.teams :as teams]
[app.services.mutations.files :as files] [app.services.mutations.files :as files]
[app.services.mutations.pages :as pages]
[app.services.mutations.colors :as colors] [app.services.mutations.colors :as colors]
[app.migrations] [app.migrations]
[app.media] [app.media]
@ -90,9 +98,17 @@
(defn create-team (defn create-team
[conn profile-id i] [conn profile-id i]
(#'teams/create-team conn {:id (mk-uuid "team" i) (let [id (mk-uuid "team" i)
team (#'teams/create-team conn {:id id
:profile-id profile-id :profile-id profile-id
:name (str "team" i)})) :name (str "team" i)})]
(#'teams/create-team-profile conn
{:team-id id
:profile-id profile-id
:is-owner true
:is-admin true
:can-edit true})
team))
(defn create-project (defn create-project
[conn profile-id team-id i] [conn profile-id team-id i]
@ -109,15 +125,6 @@
:is-shared is-shared :is-shared is-shared
:name (str "file" i)})) :name (str "file" i)}))
(defn create-page
[conn profile-id file-id i]
(#'pages/create-page conn {:id (mk-uuid "page" i)
:profile-id profile-id
:file-id file-id
:name (str "page" i)
:ordering i
:data cp/default-page-data}))
(defn handle-error (defn handle-error
[^Throwable err] [^Throwable err]
(if (instance? java.util.concurrent.ExecutionException err) (if (instance? java.util.concurrent.ExecutionException err)

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2020 app Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.tests.test-common-pages (ns app.tests.test-common-pages
(:require (:require
@ -18,64 +18,80 @@
[app.tests.helpers :as th])) [app.tests.helpers :as th]))
(t/deftest process-change-set-option (t/deftest process-change-set-option
(let [data cp/default-page-data] (let [page-id (uuid/custom 1 1)
data (cp/make-file-data page-id)]
(t/testing "Sets option single" (t/testing "Sets option single"
(let [chg {:type :set-option (let [chg {:type :set-option
:page-id page-id
:option :test :option :test
:value "test"} :value "test"}
res (cp/process-changes data [chg])] res (cp/process-changes data [chg])]
(t/is (= "test" (get-in res [:options :test]))))) (t/is (= "test" (get-in res [:pages-index page-id :options :test])))))
(t/testing "Sets option nested" (t/testing "Sets option nested"
(let [chgs [{:type :set-option (let [chgs [{:type :set-option
:page-id page-id
:option [:values :test :a] :option [:values :test :a]
:value "a"} :value "a"}
{:type :set-option {:type :set-option
:page-id page-id
:option [:values :test :b] :option [:values :test :b]
:value "b"}] :value "b"}]
res (cp/process-changes data chgs)] res (cp/process-changes data chgs)]
(t/is (= {:a "a" :b "b"} (get-in res [:options :values :test]))))) (t/is (= {:a "a" :b "b"}
(get-in res [:pages-index page-id :options :values :test])))))
(t/testing "Remove option single" (t/testing "Remove option single"
(let [chg {:type :set-option (let [chg {:type :set-option
:page-id page-id
:option :test :option :test
:value nil} :value nil}
res (cp/process-changes data [chg])] res (cp/process-changes data [chg])]
(t/is (empty? (keys (get res :options)))))) (t/is (empty? (keys (get-in res [:pages-index page-id :options]))))))
(t/testing "Remove option nested 1" (t/testing "Remove option nested 1"
(let [chgs [{:type :set-option (let [chgs [{:type :set-option
:page-id page-id
:option [:values :test :a] :option [:values :test :a]
:value "a"} :value "a"}
{:type :set-option {:type :set-option
:page-id page-id
:option [:values :test :b] :option [:values :test :b]
:value "b"} :value "b"}
{:type :set-option {:type :set-option
:page-id page-id
:option [:values :test] :option [:values :test]
:value nil}] :value nil}]
res (cp/process-changes data chgs)] res (cp/process-changes data chgs)]
(t/is (empty? (keys (get res :options)))))) (t/is (empty? (keys (get-in res [:pages-index page-id :options]))))))
(t/testing "Remove option nested 2" (t/testing "Remove option nested 2"
(let [chgs [{:type :set-option (let [chgs [{:type :set-option
:option [:values :test1 :a] :option [:values :test1 :a]
:page-id page-id
:value "a"} :value "a"}
{:type :set-option {:type :set-option
:option [:values :test2 :b] :option [:values :test2 :b]
:page-id page-id
:value "b"} :value "b"}
{:type :set-option {:type :set-option
:page-id page-id
:option [:values :test2] :option [:values :test2]
:value nil}] :value nil}]
res (cp/process-changes data chgs)] res (cp/process-changes data chgs)]
(t/is (= [:test1] (keys (get-in res [:options :values])))))))) (t/is (= [:test1] (keys (get-in res [:pages-index page-id :options :values]))))))
))
(t/deftest process-change-add-obj (t/deftest process-change-add-obj
(let [data cp/default-page-data (let [page-id (uuid/custom 1 1)
id-a (uuid/next) data (cp/make-file-data page-id)
id-b (uuid/next) id-a (uuid/custom 2 1)
id-c (uuid/next)] id-b (uuid/custom 2 2)
id-c (uuid/custom 2 3)]
(t/testing "Adds single object" (t/testing "Adds single object"
(let [chg {:type :add-obj (let [chg {:type :add-obj
:page-id page-id
:id id-a :id id-a
:parent-id uuid/zero :parent-id uuid/zero
:frame-id uuid/zero :frame-id uuid/zero
@ -88,16 +104,16 @@
;; (clojure.pprint/pprint data) ;; (clojure.pprint/pprint data)
;; (clojure.pprint/pprint res) ;; (clojure.pprint/pprint res)
(let [objects (get-in res [:pages-index page-id :objects])]
(t/is (= 2 (count (:objects res)))) (t/is (= 2 (count objects)))
(t/is (= (:obj chg) (get-in res [:objects id-a]))) (t/is (= (:obj chg) (get objects id-a)))
(t/is (= [id-a] (get-in res [:objects uuid/zero :shapes]))))) (t/is (= [id-a] (get-in objects [uuid/zero :shapes]))))))
(t/testing "Adds several objects with different indexes" (t/testing "Adds several objects with different indexes"
(let [data cp/default-page-data (let [chg (fn [id index]
{:type :add-obj
chg (fn [id index] {:type :add-obj :page-id page-id
:id id :id id
:frame-id uuid/zero :frame-id uuid/zero
:index index :index index
@ -109,77 +125,93 @@
(chg id-b 0) (chg id-b 0)
(chg id-c 1)])] (chg id-c 1)])]
(t/is (= 4 (count (:objects res)))) ;; (clojure.pprint/pprint data)
(t/is (not (nil? (get-in res [:objects id-a])))) ;; (clojure.pprint/pprint res)
(t/is (not (nil? (get-in res [:objects id-b])))) (let [objects (get-in res [:pages-index page-id :objects])]
(t/is (not (nil? (get-in res [:objects id-c])))) (t/is (= 4 (count objects)))
(t/is (= [id-b id-c id-a] (get-in res [:objects uuid/zero :shapes]))))))) (t/is (not (nil? (get objects id-a))))
(t/is (not (nil? (get objects id-b))))
(t/is (not (nil? (get objects id-c))))
(t/is (= [id-b id-c id-a] (get-in objects [uuid/zero :shapes]))))))
))
(t/deftest process-change-mod-obj (t/deftest process-change-mod-obj
(let [page-id (uuid/custom 1 1)
data (cp/make-file-data page-id)]
(t/testing "simple mod-obj" (t/testing "simple mod-obj"
(let [data cp/default-page-data (let [chg {:type :mod-obj
chg {:type :mod-obj :page-id page-id
:id uuid/zero :id uuid/zero
:operations [{:type :set :operations [{:type :set
:attr :name :attr :name
:val "foobar"}]} :val "foobar"}]}
res (cp/process-changes data [chg])] res (cp/process-changes data [chg])]
(t/is (= "foobar" (get-in res [:objects uuid/zero :name]))))) (let [objects (get-in res [:pages-index page-id :objects])]
(t/is (= "foobar" (get-in objects [uuid/zero :name]))))))
(t/testing "mod-obj for not existing shape" (t/testing "mod-obj for not existing shape"
(let [data cp/default-page-data (let [chg {:type :mod-obj
chg {:type :mod-obj :page-id page-id
:id (uuid/next) :id (uuid/next)
:operations [{:type :set :operations [{:type :set
:attr :name :attr :name
:val "foobar"}]} :val "foobar"}]}
res (cp/process-changes data [chg])] res (cp/process-changes data [chg])]
(t/is (= res cp/default-page-data))))) (t/is (= res data))))))
(t/deftest process-change-del-obj-1 (t/deftest process-change-del-obj
(let [id (uuid/next) (let [page-id (uuid/custom 1 1)
data (-> cp/default-page-data id (uuid/custom 2 1)
(assoc-in [:objects uuid/zero :shapes] [id]) data (cp/make-file-data page-id)
(assoc-in [:objects id] {:id id data (-> data
(assoc-in [:pages-index page-id :objects uuid/zero :shapes] [id])
(assoc-in [:pages-index page-id :objects id]
{:id id
:frame-id uuid/zero :frame-id uuid/zero
:type :rect :type :rect
:name "rect"})) :name "rect"}))]
chg {:type :del-obj (t/testing "delete"
(let [chg {:type :del-obj
:page-id page-id
:id id} :id id}
res (cp/process-changes data [chg])] res (cp/process-changes data [chg])]
(t/is (= 1 (count (:objects res)))) (let [objects (get-in res [:pages-index page-id :objects])]
(t/is (= [] (get-in res [:objects uuid/zero :shapes]))))) (t/is (= 1 (count objects)))
(t/is (= [] (get-in objects [uuid/zero :shapes]))))))
(t/testing "delete idempotency"
(let [chg {:type :del-obj
:page-id page-id
:id id}
res1 (cp/process-changes data [chg])
res2 (cp/process-changes res1 [chg])]
(t/is (= res1 res2))
(let [objects (get-in res1 [:pages-index page-id :objects])]
(t/is (= 1 (count objects)))
(t/is (= [] (get-in objects [uuid/zero :shapes]))))))))
(t/deftest process-change-del-obj-2
(let [id (uuid/next)
data (-> cp/default-page-data
(assoc-in [:objects uuid/zero :shapes] [id])
(assoc-in [:objects id] {:id id
:frame-id uuid/zero
:type :rect
:name "rect"}))
chg {:type :del-obj
:id uuid/zero}
res (cp/process-changes data [chg])]
(t/is (= 0 (count (:objects res))))))
(t/deftest process-change-move-objects (t/deftest process-change-move-objects
(let [frame-a-id (uuid/custom 1) (let [frame-a-id (uuid/custom 0 1)
frame-b-id (uuid/custom 2) frame-b-id (uuid/custom 0 2)
group-a-id (uuid/custom 3) group-a-id (uuid/custom 0 3)
group-b-id (uuid/custom 4) group-b-id (uuid/custom 0 4)
rect-a-id (uuid/custom 5) rect-a-id (uuid/custom 0 5)
rect-b-id (uuid/custom 6) rect-b-id (uuid/custom 0 6)
rect-c-id (uuid/custom 7) rect-c-id (uuid/custom 0 7)
rect-d-id (uuid/custom 8) rect-d-id (uuid/custom 0 8)
rect-e-id (uuid/custom 9) rect-e-id (uuid/custom 0 9)
data page-id (uuid/custom 1 1)
(-> cp/default-page-data data (cp/make-file-data page-id)
(assoc-in [:objects uuid/zero :shapes] [frame-a-id frame-b-id])
(assoc-in [:objects frame-a-id] data (update-in data [:pages-index page-id :objects]
#(-> %
(assoc-in [uuid/zero :shapes] [frame-a-id frame-b-id])
(assoc-in [frame-a-id]
{:id frame-a-id {:id frame-a-id
:parent-id uuid/zero :parent-id uuid/zero
:frame-id uuid/zero :frame-id uuid/zero
@ -187,7 +219,7 @@
:shapes [group-a-id group-b-id rect-e-id] :shapes [group-a-id group-b-id rect-e-id]
:type :frame}) :type :frame})
(assoc-in [:objects frame-b-id] (assoc-in [frame-b-id]
{:id frame-b-id {:id frame-b-id
:parent-id uuid/zero :parent-id uuid/zero
:frame-id uuid/zero :frame-id uuid/zero
@ -196,14 +228,14 @@
:type :frame}) :type :frame})
;; Groups ;; Groups
(assoc-in [:objects group-a-id] (assoc-in [group-a-id]
{:id group-a-id {:id group-a-id
:name "Group A" :name "Group A"
:type :group :type :group
:parent-id frame-a-id :parent-id frame-a-id
:frame-id frame-a-id :frame-id frame-a-id
:shapes [rect-a-id rect-b-id rect-c-id]}) :shapes [rect-a-id rect-b-id rect-c-id]})
(assoc-in [:objects group-b-id] (assoc-in [group-b-id]
{:id group-b-id {:id group-b-id
:name "Group B" :name "Group B"
:type :group :type :group
@ -212,44 +244,45 @@
:shapes [rect-d-id]}) :shapes [rect-d-id]})
;; Shapes ;; Shapes
(assoc-in [:objects rect-a-id] (assoc-in [rect-a-id]
{:id rect-a-id {:id rect-a-id
:name "Rect A" :name "Rect A"
:type :rect :type :rect
:parent-id group-a-id :parent-id group-a-id
:frame-id frame-a-id}) :frame-id frame-a-id})
(assoc-in [:objects rect-b-id] (assoc-in [rect-b-id]
{:id rect-b-id {:id rect-b-id
:name "Rect B" :name "Rect B"
:type :rect :type :rect
:parent-id group-a-id :parent-id group-a-id
:frame-id frame-a-id}) :frame-id frame-a-id})
(assoc-in [:objects rect-c-id] (assoc-in [rect-c-id]
{:id rect-c-id {:id rect-c-id
:name "Rect C" :name "Rect C"
:type :rect :type :rect
:parent-id group-a-id :parent-id group-a-id
:frame-id frame-a-id}) :frame-id frame-a-id})
(assoc-in [:objects rect-d-id] (assoc-in [rect-d-id]
{:id rect-d-id {:id rect-d-id
:name "Rect D" :name "Rect D"
:parent-id group-b-id :parent-id group-b-id
:type :rect :type :rect
:frame-id frame-a-id}) :frame-id frame-a-id})
(assoc-in [:objects rect-e-id] (assoc-in [rect-e-id]
{:id rect-e-id {:id rect-e-id
:name "Rect E" :name "Rect E"
:type :rect :type :rect
:parent-id frame-a-id :parent-id frame-a-id
:frame-id frame-a-id}))] :frame-id frame-a-id})))]
(t/testing "Create new group an add objects from the same group" (t/testing "Create new group an add objects from the same group"
(let [new-group-id (uuid/next) (let [new-group-id (uuid/next)
changes [{:type :add-obj changes [{:type :add-obj
:page-id page-id
:id new-group-id :id new-group-id
:frame-id frame-a-id :frame-id frame-a-id
:obj {:id new-group-id :obj {:id new-group-id
@ -257,6 +290,7 @@
:frame-id frame-a-id :frame-id frame-a-id
:name "Group C"}} :name "Group C"}}
{:type :mov-objects {:type :mov-objects
:page-id page-id
:parent-id new-group-id :parent-id new-group-id
:shapes [rect-b-id rect-c-id]}] :shapes [rect-b-id rect-c-id]}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
@ -265,94 +299,112 @@
;; (println "===============") ;; (println "===============")
;; (clojure.pprint/pprint res) ;; (clojure.pprint/pprint res)
(let [objects (get-in res [:pages-index page-id :objects])]
(t/is (= [group-a-id group-b-id rect-e-id new-group-id] (t/is (= [group-a-id group-b-id rect-e-id new-group-id]
(get-in res [:objects frame-a-id :shapes]))) (get-in objects [frame-a-id :shapes])))
(t/is (= [rect-b-id rect-c-id] (t/is (= [rect-b-id rect-c-id]
(get-in res [:objects new-group-id :shapes]))) (get-in objects [new-group-id :shapes])))
(t/is (= [rect-a-id] (t/is (= [rect-a-id]
(get-in res [:objects group-a-id :shapes]))))) (get-in objects [group-a-id :shapes]))))))
(t/testing "Move elements to an existing group at index" (t/testing "Move elements to an existing group at index"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id group-b-id :parent-id group-b-id
:index 0 :index 0
:shapes [rect-a-id rect-c-id]}] :shapes [rect-a-id rect-c-id]}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
(let [objects (get-in res [:pages-index page-id :objects])]
(t/is (= [group-a-id group-b-id rect-e-id] (t/is (= [group-a-id group-b-id rect-e-id]
(get-in res [:objects frame-a-id :shapes]))) (get-in objects [frame-a-id :shapes])))
(t/is (= [rect-b-id] (t/is (= [rect-b-id]
(get-in res [:objects group-a-id :shapes]))) (get-in objects [group-a-id :shapes])))
(t/is (= [rect-a-id rect-c-id rect-d-id] (t/is (= [rect-a-id rect-c-id rect-d-id]
(get-in res [:objects group-b-id :shapes]))))) (get-in objects [group-b-id :shapes]))))))
(t/testing "Move elements from group and frame to an existing group at index" (t/testing "Move elements from group and frame to an existing group at index"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id group-b-id :parent-id group-b-id
:index 0 :index 0
:shapes [rect-a-id rect-e-id]}] :shapes [rect-a-id rect-e-id]}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
(let [objects (get-in res [:pages-index page-id :objects])]
(t/is (= [group-a-id group-b-id] (t/is (= [group-a-id group-b-id]
(get-in res [:objects frame-a-id :shapes]))) (get-in objects [frame-a-id :shapes])))
(t/is (= [rect-b-id rect-c-id] (t/is (= [rect-b-id rect-c-id]
(get-in res [:objects group-a-id :shapes]))) (get-in objects [group-a-id :shapes])))
(t/is (= [rect-a-id rect-e-id rect-d-id] (t/is (= [rect-a-id rect-e-id rect-d-id]
(get-in res [:objects group-b-id :shapes]))))) (get-in objects [group-b-id :shapes]))))))
(t/testing "Move elements from several groups" (t/testing "Move elements from several groups"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id group-b-id :parent-id group-b-id
:index 0 :index 0
:shapes [rect-a-id rect-e-id]}] :shapes [rect-a-id rect-e-id]}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
(let [objects (get-in res [:pages-index page-id :objects])]
(t/is (= [group-a-id group-b-id] (t/is (= [group-a-id group-b-id]
(get-in res [:objects frame-a-id :shapes]))) (get-in objects [frame-a-id :shapes])))
(t/is (= [rect-b-id rect-c-id] (t/is (= [rect-b-id rect-c-id]
(get-in res [:objects group-a-id :shapes]))) (get-in objects [group-a-id :shapes])))
(t/is (= [rect-a-id rect-e-id rect-d-id] (t/is (= [rect-a-id rect-e-id rect-d-id]
(get-in res [:objects group-b-id :shapes]))))) (get-in objects [group-b-id :shapes]))))))
(t/testing "Move elements and delete the empty group" (t/testing "Move elements and delete the empty group"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id group-a-id :parent-id group-a-id
:shapes [rect-d-id]}] :shapes [rect-d-id]}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
(let [objects (get-in res [:pages-index page-id :objects])]
(t/is (= [group-a-id rect-e-id] (t/is (= [group-a-id rect-e-id]
(get-in res [:objects frame-a-id :shapes]))) (get-in objects [frame-a-id :shapes])))
(t/is (nil? (get-in res [:objects group-b-id]))))) (t/is (nil? (get-in objects [group-b-id]))))))
(t/testing "Move elements to a group with different frame" (t/testing "Move elements to a group with different frame"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id frame-b-id :parent-id frame-b-id
:shapes [group-a-id]}] :shapes [group-a-id]}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
(t/is (= [group-b-id rect-e-id] (get-in res [:objects frame-a-id :shapes]))) ;; (pprint (get-in data [:pages-index page-id :objects]))
(t/is (= [group-a-id] (get-in res [:objects frame-b-id :shapes]))) ;; (println "==========")
(t/is (= frame-b-id (get-in res [:objects group-a-id :frame-id]))) ;; (pprint (get-in res [:pages-index page-id :objects]))
(t/is (= frame-b-id (get-in res [:objects rect-a-id :frame-id])))
(t/is (= frame-b-id (get-in res [:objects rect-b-id :frame-id]))) (let [objects (get-in res [:pages-index page-id :objects])]
(t/is (= frame-b-id (get-in res [:objects rect-c-id :frame-id]))))) (t/is (= [group-b-id rect-e-id] (get-in objects [frame-a-id :shapes])))
(t/is (= [group-a-id] (get-in objects [frame-b-id :shapes])))
(t/is (= frame-b-id (get-in objects [group-a-id :frame-id])))
(t/is (= frame-b-id (get-in objects [rect-a-id :frame-id])))
(t/is (= frame-b-id (get-in objects [rect-b-id :frame-id])))
(t/is (= frame-b-id (get-in objects [rect-c-id :frame-id]))))))
(t/testing "Move elements to frame zero" (t/testing "Move elements to frame zero"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id uuid/zero :parent-id uuid/zero
:shapes [group-a-id] :shapes [group-a-id]
:index 0}] :index 0}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
(let [objects (get-in res [:pages-index page-id :objects])]
;; (pprint (get-in data [:objects uuid/zero])) ;; (pprint (get-in data [:objects uuid/zero]))
;; (println "==========") ;; (println "==========")
;; (pprint (get-in res [:objects uuid/zero])) ;; (pprint (get-in objects [uuid/zero]))
(t/is (= [group-a-id frame-a-id frame-b-id] (t/is (= [group-a-id frame-a-id frame-b-id]
(get-in res [:objects cp/root :shapes]))))) (get-in objects [cp/root :shapes]))))))
(t/testing "Don't allow to move inside self" (t/testing "Don't allow to move inside self"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id group-a-id :parent-id group-a-id
:shapes [group-a-id]}] :shapes [group-a-id]}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
@ -365,19 +417,24 @@
shape-2-id (uuid/custom 2 2) shape-2-id (uuid/custom 2 2)
shape-3-id (uuid/custom 2 3) shape-3-id (uuid/custom 2 3)
frame-id (uuid/custom 1 1) frame-id (uuid/custom 1 1)
page-id (uuid/custom 0 1)
changes [{:type :add-obj changes [{:type :add-obj
:id frame-id :id frame-id
:page-id page-id
:parent-id uuid/zero :parent-id uuid/zero
:frame-id uuid/zero :frame-id uuid/zero
:obj {:type :frame :obj {:type :frame
:name "Frame"}} :name "Frame"}}
{:type :add-obj {:type :add-obj
:page-id page-id
:frame-id frame-id :frame-id frame-id
:parent-id frame-id :parent-id frame-id
:id shape-1-id :id shape-1-id
:obj {:type :shape :obj {:type :shape
:name "Shape 1"}} :name "Shape 1"}}
{:type :add-obj {:type :add-obj
:page-id page-id
:id shape-2-id :id shape-2-id
:parent-id uuid/zero :parent-id uuid/zero
:frame-id uuid/zero :frame-id uuid/zero
@ -385,16 +442,19 @@
:name "Shape 2"}} :name "Shape 2"}}
{:type :add-obj {:type :add-obj
:page-id page-id
:id shape-3-id :id shape-3-id
:parent-id uuid/zero :parent-id uuid/zero
:frame-id uuid/zero :frame-id uuid/zero
:obj {:type :rect :obj {:type :rect
:name "Shape 3"}} :name "Shape 3"}}
] ]
data (cp/process-changes cp/default-page-data changes)] data (cp/make-file-data page-id)
data (cp/process-changes data changes)]
(t/testing "preserve order on multiple shape mov 1" (t/testing "preserve order on multiple shape mov 1"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:shapes [shape-2-id shape-3-id] :shapes [shape-2-id shape-3-id]
:parent-id uuid/zero :parent-id uuid/zero
:index 0}] :index 0}]
@ -406,12 +466,13 @@
;; (pprint (get-in res [:objects])) ;; (pprint (get-in res [:objects]))
(t/is (= [frame-id shape-2-id shape-3-id] (t/is (= [frame-id shape-2-id shape-3-id]
(get-in data [:objects uuid/zero :shapes]))) (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
(t/is (= [shape-2-id shape-3-id frame-id] (t/is (= [shape-2-id shape-3-id frame-id]
(get-in res [:objects uuid/zero :shapes]))))) (get-in res [:pages-index page-id :objects uuid/zero :shapes])))))
(t/testing "preserve order on multiple shape mov 1" (t/testing "preserve order on multiple shape mov 1"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:shapes [shape-3-id shape-2-id] :shapes [shape-3-id shape-2-id]
:parent-id uuid/zero :parent-id uuid/zero
:index 0}] :index 0}]
@ -423,23 +484,25 @@
;; (pprint (get-in res [:objects])) ;; (pprint (get-in res [:objects]))
(t/is (= [frame-id shape-2-id shape-3-id] (t/is (= [frame-id shape-2-id shape-3-id]
(get-in data [:objects uuid/zero :shapes]))) (get-in data [:pages-index page-id :objects uuid/zero :shapes])))
(t/is (= [shape-3-id shape-2-id frame-id] (t/is (= [shape-3-id shape-2-id frame-id]
(get-in res [:objects uuid/zero :shapes]))))) (get-in res [:pages-index page-id :objects uuid/zero :shapes])))))
(t/testing "move inside->outside-inside" (t/testing "move inside->outside-inside"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:shapes [shape-2-id] :shapes [shape-2-id]
:parent-id frame-id} :parent-id frame-id}
{:type :mov-objects {:type :mov-objects
:page-id page-id
:shapes [shape-2-id] :shapes [shape-2-id]
:parent-id uuid/zero}] :parent-id uuid/zero}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
(t/is (= (get-in res [:objects shape-1-id :frame-id]) (t/is (= (get-in res [:pages-index page-id :objects shape-1-id :frame-id])
(get-in data [:objects shape-1-id :frame-id]))) (get-in data [:pages-index page-id :objects shape-1-id :frame-id])))
(t/is (= (get-in res [:objects shape-2-id :frame-id]) (t/is (= (get-in res [:pages-index page-id :objects shape-2-id :frame-id])
(get-in data [:objects shape-2-id :frame-id]))))) (get-in data [:pages-index page-id :objects shape-2-id :frame-id])))))
)) ))
@ -450,43 +513,54 @@
shape-3-id (uuid/custom 1 3) shape-3-id (uuid/custom 1 3)
shape-4-id (uuid/custom 1 4) shape-4-id (uuid/custom 1 4)
group-1-id (uuid/custom 1 5) group-1-id (uuid/custom 1 5)
page-id (uuid/custom 0 1)
changes [{:type :add-obj changes [{:type :add-obj
:page-id page-id
:id shape-1-id :id shape-1-id
:frame-id cp/root :frame-id cp/root
:obj {:id shape-1-id :obj {:id shape-1-id
:type :rect :type :rect
:name "Shape a"}} :name "Shape a"}}
{:type :add-obj {:type :add-obj
:page-id page-id
:id shape-2-id :id shape-2-id
:frame-id cp/root :frame-id cp/root
:obj {:id shape-2-id :obj {:id shape-2-id
:type :rect :type :rect
:name "Shape b"}} :name "Shape b"}}
{:type :add-obj {:type :add-obj
:page-id page-id
:id shape-3-id :id shape-3-id
:frame-id cp/root :frame-id cp/root
:obj {:id shape-3-id :obj {:id shape-3-id
:type :rect :type :rect
:name "Shape c"}} :name "Shape c"}}
{:type :add-obj {:type :add-obj
:page-id page-id
:id shape-4-id :id shape-4-id
:frame-id cp/root :frame-id cp/root
:obj {:id shape-4-id :obj {:id shape-4-id
:type :rect :type :rect
:name "Shape d"}} :name "Shape d"}}
{:type :add-obj {:type :add-obj
:page-id page-id
:id group-1-id :id group-1-id
:frame-id cp/root :frame-id cp/root
:obj {:id group-1-id :obj {:id group-1-id
:type :group :type :group
:name "Group"}} :name "Group"}}
{:type :mov-objects {:type :mov-objects
:page-id page-id
:parent-id group-1-id :parent-id group-1-id
:shapes [shape-1-id shape-2-id]}] :shapes [shape-1-id shape-2-id]}]
data (cp/process-changes cp/default-page-data changes)]
data (cp/make-file-data page-id)
data (cp/process-changes data changes)]
(t/testing "case 1" (t/testing "case 1"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id cp/root :parent-id cp/root
:index 2 :index 2
:shapes [shape-3-id]}] :shapes [shape-3-id]}]
@ -495,19 +569,20 @@
;; Before ;; Before
(t/is (= [shape-3-id shape-4-id group-1-id] (t/is (= [shape-3-id shape-4-id group-1-id]
(get-in data [:objects cp/root :shapes]))) (get-in data [:pages-index page-id :objects cp/root :shapes])))
;; After ;; After
(t/is (= [shape-4-id shape-3-id group-1-id] (t/is (= [shape-4-id shape-3-id group-1-id]
(get-in res [:objects cp/root :shapes]))) (get-in res [:pages-index page-id :objects cp/root :shapes])))
;; (pprint (get-in data [:objects cp/root])) ;; (pprint (get-in data [:pages-index page-id :objects cp/root]))
;; (pprint (get-in res [:objects cp/root])) ;; (pprint (get-in res [:pages-index page-id :objects cp/root]))
)) ))
(t/testing "case 2" (t/testing "case 2"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id group-1-id :parent-id group-1-id
:index 2 :index 2
:shapes [shape-3-id]}] :shapes [shape-3-id]}]
@ -516,25 +591,26 @@
;; Before ;; Before
(t/is (= [shape-3-id shape-4-id group-1-id] (t/is (= [shape-3-id shape-4-id group-1-id]
(get-in data [:objects cp/root :shapes]))) (get-in data [:pages-index page-id :objects cp/root :shapes])))
(t/is (= [shape-1-id shape-2-id] (t/is (= [shape-1-id shape-2-id]
(get-in data [:objects group-1-id :shapes]))) (get-in data [:pages-index page-id :objects group-1-id :shapes])))
;; After: ;; After:
(t/is (= [shape-4-id group-1-id] (t/is (= [shape-4-id group-1-id]
(get-in res [:objects cp/root :shapes]))) (get-in res [:pages-index page-id :objects cp/root :shapes])))
(t/is (= [shape-1-id shape-2-id shape-3-id] (t/is (= [shape-1-id shape-2-id shape-3-id]
(get-in res [:objects group-1-id :shapes]))) (get-in res [:pages-index page-id :objects group-1-id :shapes])))
;; (pprint (get-in data [:objects group-1-id])) ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
;; (pprint (get-in res [:objects group-1-id])) ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
)) ))
(t/testing "case 3" (t/testing "case 3"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id group-1-id :parent-id group-1-id
:index 1 :index 1
:shapes [shape-3-id]}] :shapes [shape-3-id]}]
@ -543,25 +619,26 @@
;; Before ;; Before
(t/is (= [shape-3-id shape-4-id group-1-id] (t/is (= [shape-3-id shape-4-id group-1-id]
(get-in data [:objects cp/root :shapes]))) (get-in data [:pages-index page-id :objects cp/root :shapes])))
(t/is (= [shape-1-id shape-2-id] (t/is (= [shape-1-id shape-2-id]
(get-in data [:objects group-1-id :shapes]))) (get-in data [:pages-index page-id :objects group-1-id :shapes])))
;; After ;; After
(t/is (= [shape-4-id group-1-id] (t/is (= [shape-4-id group-1-id]
(get-in res [:objects cp/root :shapes]))) (get-in res [:pages-index page-id :objects cp/root :shapes])))
(t/is (= [shape-1-id shape-3-id shape-2-id] (t/is (= [shape-1-id shape-3-id shape-2-id]
(get-in res [:objects group-1-id :shapes]))) (get-in res [:pages-index page-id :objects group-1-id :shapes])))
;; (pprint (get-in data [:objects group-1-id])) ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
;; (pprint (get-in res [:objects group-1-id])) ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
)) ))
(t/testing "case 4" (t/testing "case 4"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id group-1-id :parent-id group-1-id
:index 0 :index 0
:shapes [shape-3-id]}] :shapes [shape-3-id]}]
@ -570,135 +647,85 @@
;; Before ;; Before
(t/is (= [shape-3-id shape-4-id group-1-id] (t/is (= [shape-3-id shape-4-id group-1-id]
(get-in data [:objects cp/root :shapes]))) (get-in data [:pages-index page-id :objects cp/root :shapes])))
(t/is (= [shape-1-id shape-2-id] (t/is (= [shape-1-id shape-2-id]
(get-in data [:objects group-1-id :shapes]))) (get-in data [:pages-index page-id :objects group-1-id :shapes])))
;; After ;; After
(t/is (= [shape-4-id group-1-id] (t/is (= [shape-4-id group-1-id]
(get-in res [:objects cp/root :shapes]))) (get-in res [:pages-index page-id :objects cp/root :shapes])))
(t/is (= [shape-3-id shape-1-id shape-2-id] (t/is (= [shape-3-id shape-1-id shape-2-id]
(get-in res [:objects group-1-id :shapes]))) (get-in res [:pages-index page-id :objects group-1-id :shapes])))
;; (pprint (get-in data [:objects group-1-id])) ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
;; (pprint (get-in res [:objects group-1-id])) ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
)) ))
(t/testing "case 5" (t/testing "case 5"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id cp/root :parent-id cp/root
:index 0 :index 0
:shapes [shape-2-id]}] :shapes [shape-2-id]}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
;; (pprint (get-in data [:objects cp/root])) ;; (pprint (get-in data [:pages-index page-id :objects cp/root]))
;; (pprint (get-in res [:objects cp/root])) ;; (pprint (get-in res [:pages-index page-id :objects cp/root]))
;; (pprint (get-in data [:objects group-1-id])) ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
;; (pprint (get-in res [:objects group-1-id])) ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
;; Before ;; Before
(t/is (= [shape-3-id shape-4-id group-1-id] (t/is (= [shape-3-id shape-4-id group-1-id]
(get-in data [:objects cp/root :shapes]))) (get-in data [:pages-index page-id :objects cp/root :shapes])))
(t/is (= [shape-1-id shape-2-id] (t/is (= [shape-1-id shape-2-id]
(get-in data [:objects group-1-id :shapes]))) (get-in data [:pages-index page-id :objects group-1-id :shapes])))
;; After ;; After
(t/is (= [shape-2-id shape-3-id shape-4-id group-1-id] (t/is (= [shape-2-id shape-3-id shape-4-id group-1-id]
(get-in res [:objects cp/root :shapes]))) (get-in res [:pages-index page-id :objects cp/root :shapes])))
(t/is (= [shape-1-id] (t/is (= [shape-1-id]
(get-in res [:objects group-1-id :shapes]))) (get-in res [:pages-index page-id :objects group-1-id :shapes])))
)) ))
(t/testing "case 6" (t/testing "case 6"
(let [changes [{:type :mov-objects (let [changes [{:type :mov-objects
:page-id page-id
:parent-id cp/root :parent-id cp/root
:index 0 :index 0
:shapes [shape-2-id shape-1-id]}] :shapes [shape-2-id shape-1-id]}]
res (cp/process-changes data changes)] res (cp/process-changes data changes)]
;; (pprint (get-in data [:objects cp/root])) ;; (pprint (get-in data [:pages-index page-id :objects cp/root]))
;; (pprint (get-in res [:objects cp/root])) ;; (pprint (get-in res [:pages-index page-id :objects cp/root]))
;; (pprint (get-in data [:objects group-1-id])) ;; (pprint (get-in data [:pages-index page-id :objects group-1-id]))
;; (pprint (get-in res [:objects group-1-id])) ;; (pprint (get-in res [:pages-index page-id :objects group-1-id]))
;; Before ;; Before
(t/is (= [shape-3-id shape-4-id group-1-id] (t/is (= [shape-3-id shape-4-id group-1-id]
(get-in data [:objects cp/root :shapes]))) (get-in data [:pages-index page-id :objects cp/root :shapes])))
(t/is (= [shape-1-id shape-2-id] (t/is (= [shape-1-id shape-2-id]
(get-in data [:objects group-1-id :shapes]))) (get-in data [:pages-index page-id :objects group-1-id :shapes])))
;; After ;; After
(t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id] (t/is (= [shape-2-id shape-1-id shape-3-id shape-4-id]
(get-in res [:objects cp/root :shapes]))) (get-in res [:pages-index page-id :objects cp/root :shapes])))
(t/is (= nil (t/is (= nil
(get-in res [:objects group-1-id]))) (get-in res [:pages-index page-id :objects group-1-id])))
)) ))
)) ))
(t/deftest idenpotency-regression-1
(let [data {:version 5
:objects
{#uuid "00000000-0000-0000-0000-000000000000"
{:id #uuid "00000000-0000-0000-0000-000000000000",
:type :frame,
:name "root",
:shapes
[#uuid "f5d51910-ab23-11ea-ac38-e1abed64181a"
#uuid "f6a36590-ab23-11ea-ac38-e1abed64181a"]},
#uuid "f5d51910-ab23-11ea-ac38-e1abed64181a"
{:name "Rect-1",
:type :rect,
:id #uuid "f5d51910-ab23-11ea-ac38-e1abed64181a",
:parent-id #uuid "00000000-0000-0000-0000-000000000000",
:frame-id #uuid "00000000-0000-0000-0000-000000000000"}
#uuid "f6a36590-ab23-11ea-ac38-e1abed64181a"
{:name "Rect-2",
:type :rect,
:id #uuid "f6a36590-ab23-11ea-ac38-e1abed64181a",
:parent-id #uuid "00000000-0000-0000-0000-000000000000",
:frame-id #uuid "00000000-0000-0000-0000-000000000000"}}}
chgs [{:type :add-obj,
:id #uuid "3375ec40-ab24-11ea-b512-b945e8edccf5",
:frame-id #uuid "00000000-0000-0000-0000-000000000000",
:index 0
:obj {:name "Group-1",
:type :group,
:id #uuid "3375ec40-ab24-11ea-b512-b945e8edccf5",
:frame-id #uuid "00000000-0000-0000-0000-000000000000"}}
{:type :mov-objects,
:parent-id #uuid "3375ec40-ab24-11ea-b512-b945e8edccf5",
:shapes
[#uuid "f5d51910-ab23-11ea-ac38-e1abed64181a"
#uuid "f6a36590-ab23-11ea-ac38-e1abed64181a"]}]
res1 (cp/process-changes data chgs)
res2 (cp/process-changes res1 chgs)]
;; (clojure.pprint/pprint data)
;; (println "==============")
;; (clojure.pprint/pprint res2)
(t/is (= [#uuid "f5d51910-ab23-11ea-ac38-e1abed64181a"
#uuid "f6a36590-ab23-11ea-ac38-e1abed64181a"]
(get-in data [:objects uuid/zero :shapes])))
(t/is (= [#uuid "3375ec40-ab24-11ea-b512-b945e8edccf5"]
(get-in res2 [:objects uuid/zero :shapes])))
(t/is (= [#uuid "3375ec40-ab24-11ea-b512-b945e8edccf5"]
(get-in res1 [:objects uuid/zero :shapes])))
))

View file

@ -2,7 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; 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/. ;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; ;;
;; Copyright (c) 2019 Andrey Antukh <niwi@niwi.nz> ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.tests.test-emails (ns app.tests.test-emails
(:require (:require

View file

@ -1,102 +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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 app Labs SL
(ns app.tests.test-services-colors
(:require
[clojure.test :as t]
[datoteka.core :as fs]
[clojure.java.io :as io]
[app.db :as db]
[app.services.mutations :as sm]
[app.services.queries :as sq]
[app.util.storage :as ust]
[app.common.uuid :as uuid]
[app.tests.helpers :as th]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest colors-crud
(let [prof (th/create-profile db/pool 1)
team-id (:default-team-id prof)
proj (th/create-project db/pool (:id prof) team-id 1)
file (th/create-file db/pool (:id prof) (:id proj) true 1)
color-id (uuid/next)]
(t/testing "upload color to library file"
(let [data {::sm/type :create-color
:id color-id
:profile-id (:id prof)
:file-id (:id file)
:name "testfile"
:content "#222222"}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (= (:id data) (:id result)))
(t/is (= (:name data) (:name result)))
(t/is (= (:content data) (:content result))))))
(t/testing "list colors by library file"
(let [data {::sq/type :colors
:profile-id (:id prof)
:file-id (:id file)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (= color-id (get-in out [:result 0 :id])))
(t/is (= "testfile" (get-in out [:result 0 :name])))))
(t/testing "get single color"
(let [data {::sq/type :color
:profile-id (:id prof)
:id color-id}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (= color-id (get-in out [:result :id])))
(t/is (= "testfile" (get-in out [:result :name])))))
(t/testing "delete colors"
(let [data {::sm/type :delete-color
:profile-id (:id prof)
:id color-id}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (get-in out [:result])))))
(t/testing "query color after delete"
(let [data {::sq/type :color
:profile-id (:id prof)
:id color-id}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(let [error (:error out)]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :service-error)))
(let [error (ex-cause (:error out))]
(t/is (th/ex-info? error))
(t/is (th/ex-of-type? error :not-found)))))
(t/testing "query colors after delete"
(let [data {::sq/type :colors
:profile-id (:id prof)
:file-id (:id file)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(let [result (:result out)]
(t/is (= 0 (count result))))))
))

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2020 app Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.tests.test-services-files (ns app.tests.test-services-files
(:require (:require
@ -70,7 +70,7 @@
(t/is (= 1 (count result))) (t/is (= 1 (count result)))
(t/is (= file-id (get-in result [0 :id]))) (t/is (= file-id (get-in result [0 :id])))
(t/is (= "new name" (get-in result [0 :name]))) (t/is (= "new name" (get-in result [0 :name])))
(t/is (= 1 (count (get-in result [0 :pages]))))))) (t/is (= 1 (count (get-in result [0 :data :pages])))))))
(t/testing "query single file without users" (t/testing "query single file without users"
(let [data {::sq/type :file (let [data {::sq/type :file
@ -84,8 +84,7 @@
(let [result (:result out)] (let [result (:result out)]
(t/is (= file-id (:id result))) (t/is (= file-id (:id result)))
(t/is (= "new name" (:name result))) (t/is (= "new name" (:name result)))
(t/is (vector? (:pages result))) (t/is (= 1 (count (get-in result [:data :pages]))))
(t/is (= 1 (count (:pages result))))
(t/is (nil? (:users result)))))) (t/is (nil? (:users result))))))
(t/testing "delete file" (t/testing "delete file"
@ -128,5 +127,3 @@
(let [result (:result out)] (let [result (:result out)]
(t/is (= 0 (count result)))))) (t/is (= 0 (count result))))))
)) ))
;; TODO: delete file image

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2020 app Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.tests.test-services-media (ns app.tests.test-services-media
(:require (:require
@ -30,7 +30,7 @@
object-id-2 (uuid/next)] object-id-2 (uuid/next)]
(t/testing "create media object from url to file" (t/testing "create media object from url to file"
(let [url "https://raw.githubusercontent.com/app/app/develop/frontend/resources/images/penpot-login.jpg" (let [url "https://raw.githubusercontent.com/uxbox/uxbox/develop/sample_media/images/unsplash/anna-pelzer.jpg"
data {::sm/type :add-media-object-from-url data {::sm/type :add-media-object-from-url
:id object-id-1 :id object-id-1
:profile-id (:id prof) :profile-id (:id prof)
@ -45,12 +45,12 @@
(t/is (= object-id-1 (get-in out [:result :id]))) (t/is (= object-id-1 (get-in out [:result :id])))
(t/is (not (nil? (get-in out [:result :name])))) (t/is (not (nil? (get-in out [:result :name]))))
(t/is (= "image/jpeg" (get-in out [:result :mtype]))) (t/is (= "image/jpeg" (get-in out [:result :mtype])))
(t/is (= 787 (get-in out [:result :width]))) (t/is (= 1024 (get-in out [:result :width])))
(t/is (= 2000 (get-in out [:result :height]))) (t/is (= 683 (get-in out [:result :height])))
(t/is (string? (get-in out [:result :path]))) (t/is (string? (get-in out [:result :path])))
(t/is (string? (get-in out [:result :uri]))) (t/is (string? (get-in out [:result :thumb-path])))
(t/is (string? (get-in out [:result :thumb-uri]))))) ))
(t/testing "upload media object to file" (t/testing "upload media object to file"
(let [content {:filename "sample.jpg" (let [content {:filename "sample.jpg"
@ -76,8 +76,7 @@
(t/is (= 800 (get-in out [:result :height]))) (t/is (= 800 (get-in out [:result :height])))
(t/is (string? (get-in out [:result :path]))) (t/is (string? (get-in out [:result :path])))
(t/is (string? (get-in out [:result :uri]))) (t/is (string? (get-in out [:result :thumb-path])))))
(t/is (string? (get-in out [:result :thumb-uri])))))
(t/testing "list media objects by file" (t/testing "list media objects by file"
(let [data {::sq/type :media-objects (let [data {::sq/type :media-objects
@ -95,8 +94,7 @@
(t/is (= 800 (get-in out [:result 0 :height]))) (t/is (= 800 (get-in out [:result 0 :height])))
(t/is (string? (get-in out [:result 0 :path]))) (t/is (string? (get-in out [:result 0 :path])))
(t/is (string? (get-in out [:result 0 :uri]))) (t/is (string? (get-in out [:result 0 :thumb-path])))))
(t/is (string? (get-in out [:result 0 :thumb-uri])))))
(t/testing "single media object" (t/testing "single media object"
(let [data {::sq/type :media-object (let [data {::sq/type :media-object
@ -111,8 +109,7 @@
(t/is (= 800 (get-in out [:result :width]))) (t/is (= 800 (get-in out [:result :width])))
(t/is (= 800 (get-in out [:result :height]))) (t/is (= 800 (get-in out [:result :height])))
(t/is (string? (get-in out [:result :path]))) (t/is (string? (get-in out [:result :path])))))
(t/is (string? (get-in out [:result :uri])))))
(t/testing "delete media objects" (t/testing "delete media objects"
(let [data {::sm/type :delete-media-object (let [data {::sm/type :delete-media-object

View file

@ -1,221 +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/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 app Labs SL
(ns app.tests.test-services-pages
(:require
[clojure.spec.alpha :as s]
[clojure.test :as t]
[promesa.core :as p]
[app.common.pages :as cp]
[app.db :as db]
[app.http :as http]
[app.services.mutations :as sm]
[app.services.queries :as sq]
[app.common.uuid :as uuid]
[app.tests.helpers :as th]))
(t/use-fixtures :once th/state-init)
(t/use-fixtures :each th/database-reset)
(t/deftest pages-crud
(let [prof (th/create-profile db/pool 1)
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
file (th/create-file db/pool (:id prof) proj-id false 1)
page-id (uuid/next)]
(t/testing "create page"
(let [data {::sm/type :create-page
:data cp/default-page-data
:file-id (:id file)
:id page-id
:ordering 1
:name "test page"
:profile-id (:id prof)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (uuid? (:id result)))
(t/is (= (:id data) (:id result)))
(t/is (= (:name data) (:name result)))
(t/is (= (:data data) (:data result)))
(t/is (nil? (:share-token result)))
(t/is (= 0 (:revn result))))))
(t/testing "generate share token"
(let [data {::sm/type :generate-page-share-token
:id page-id}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (string? (:share-token result))))))
(t/testing "query pages"
(let [data {::sq/type :pages
:file-id (:id file)
:profile-id (:id prof)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (vector? result))
(t/is (= 1 (count result)))
(t/is (= page-id (get-in result [0 :id])))
(t/is (= "test page" (get-in result [0 :name])))
(t/is (string? (get-in result [0 :share-token])))
(t/is (:id file) (get-in result [0 :file-id])))))
(t/testing "delete page"
(let [data {::sm/type :delete-page
:id page-id
:profile-id (:id prof)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(t/is (nil? (:result out)))))
(t/testing "query pages after delete"
(let [data {::sq/type :pages
:file-id (:id file)
:profile-id (:id prof)}
out (th/try-on! (sq/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (vector? result))
(t/is (= 0 (count result))))))
))
(t/deftest update-page-data
(let [prof (th/create-profile db/pool 1)
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
file (th/create-file db/pool (:id prof) proj-id false 1)
page-id (uuid/next)]
(t/testing "create empty page"
(let [data {::sm/type :create-page
:data cp/default-page-data
:file-id (:id file)
:id page-id
:ordering 1
:name "test page"
:profile-id (:id prof)}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (uuid? (:id result)))
(t/is (= (:id data) (:id result))))))
(t/testing "successfully update data"
(let [sid (uuid/next)
data {::sm/type :update-page
:id page-id
:revn 0
:session-id uuid/zero
:profile-id (:id prof)
:changes [{:type :add-obj
:frame-id uuid/zero
:id sid
:obj {:id sid
:name "Rect"
:frame-id uuid/zero
:type :rect}}]}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)
result1 (first result)]
(t/is (= 1 (count result)))
(t/is (= 1 (:revn result1)))
(t/is (= (:id data) (:page-id result1)))
(t/is (vector (:changes result1)))
(t/is (= 1 (count (:changes result1))))
(t/is (= :add-obj (get-in result1 [:changes 0 :type]))))))
(t/testing "conflict error"
(let [data {::sm/type :update-page
:session-id uuid/zero
:id page-id
:revn 99
:profile-id (:id prof)
:changes []}
out (th/try-on! (sm/handle data))]
;; (th/print-result! out)
(let [error (:error out)
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :service-error))
(t/is (= (:name error-data) :app.services.mutations.pages/update-page)))
(let [error (ex-cause (:error out))
error-data (ex-data error)]
(t/is (th/ex-info? error))
(t/is (= (:type error-data) :validation))
(t/is (= (:code error-data) :revn-conflict)))))
))
(t/deftest update-page-data-2
(let [prof (th/create-profile db/pool 1)
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
file (th/create-file db/pool (:id prof) proj-id false 1)
page (th/create-page db/pool (:id prof) (:id file) 1)]
(t/testing "lagging changes"
(let [sid (uuid/next)
data {::sm/type :update-page
:id (:id page)
:revn 0
:session-id uuid/zero
:profile-id (:id prof)
:changes [{:type :add-obj
:id sid
:frame-id uuid/zero
:obj {:id sid
:name "Rect"
:frame-id uuid/zero
:type :rect}}]}
out1 (th/try-on! (sm/handle data))
out2 (th/try-on! (sm/handle data))
]
;; (th/print-result! out1)
;; (th/print-result! out2)
(t/is (nil? (:error out1)))
(t/is (nil? (:error out2)))
(t/is (= 1 (count (get-in out1 [:result 0 :changes]))))
(t/is (= 1 (count (get-in out2 [:result 0 :changes]))))
(t/is (= 2 (count (:result out2))))
(t/is (= (:id data) (get-in out1 [:result 0 :page-id])))
(t/is (= (:id data) (get-in out2 [:result 0 :page-id])))
))))

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2019-2020 Andrey Antukh <niwi@niwi.nz> ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.tests.test-services-profile (ns app.tests.test-services-profile
(:require (:require

View file

@ -1,3 +1,12 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.tests.test-services-projects (ns app.tests.test-services-projects
(:require (:require
[clojure.test :as t] [clojure.test :as t]

View file

@ -1,65 +0,0 @@
(ns app.tests.test-services-user-attrs
(:require
[clojure.spec.alpha :as s]
[clojure.test :as t]
[promesa.core :as p]
[app.db :as db]
[app.http :as http]
[app.services.mutations :as sm]
[app.services.queries :as sq]
[app.tests.helpers :as th]))
;; (t/use-fixtures :once th/state-init)
;; (t/use-fixtures :each th/database-reset)
;; (t/deftest test-user-attrs
;; (let [{:keys [id] :as user} @(th/create-user db/pool 1)]
;; (let [out (th/try-on! (sq/handle {::sq/type :user-attr
;; :key "foobar"
;; :user id}))]
;; (t/is (nil? (:result out)))
;; (let [error (:error out)]
;; (t/is (th/ex-info? error))
;; (t/is (th/ex-of-type? error :service-error)))
;; (let [error (ex-cause (:error out))]
;; (t/is (th/ex-info? error))
;; (t/is (th/ex-of-type? error :not-found))))
;; (let [out (th/try-on! (sm/handle {::sm/type :upsert-user-attr
;; :user id
;; :key "foobar"
;; :val {:some #{:value}}}))]
;; ;; (th/print-result! out)
;; (t/is (nil? (:error out)))
;; (t/is (nil? (:result out))))
;; (let [out (th/try-on! (sq/handle {::sq/type :user-attr
;; :key "foobar"
;; :user id}))]
;; ;; (th/print-result! out)
;; (t/is (nil? (:error out)))
;; (t/is (= {:some #{:value}} (get-in out [:result :val])))
;; (t/is (= "foobar" (get-in out [:result :key]))))
;; (let [out (th/try-on! (sm/handle {::sm/type :delete-user-attr
;; :user id
;; :key "foobar"}))]
;; ;; (th/print-result! out)
;; (t/is (nil? (:error out)))
;; (t/is (nil? (:result out))))
;; (let [out (th/try-on! (sq/handle {::sq/type :user-attr
;; :key "foobar"
;; :user id}))]
;; ;; (th/print-result! out)
;; (t/is (nil? (:result out)))
;; (let [error (:error out)]
;; (t/is (th/ex-info? error))
;; (t/is (th/ex-of-type? error :service-error)))
;; (let [error (ex-cause (:error out))]
;; (t/is (th/ex-info? error))
;; (t/is (th/ex-of-type? error :not-found))))))

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0. ;; defined by the Mozilla Public License, v. 2.0.
;; ;;
;; Copyright (c) 2020 app Labs SL ;; Copyright (c) 2020 UXBOX Labs SL
(ns app.tests.test-services-viewer (ns app.tests.test-services-viewer
(:require (:require
@ -29,14 +29,13 @@
proj-id (:default-project-id prof) proj-id (:default-project-id prof)
file (th/create-file db/pool (:id prof) proj-id false 1) file (th/create-file db/pool (:id prof) proj-id false 1)
page (th/create-page db/pool (:id prof) (:id file) 1)
token (atom nil)] token (atom nil)]
(t/testing "authenticated with page-id" (t/testing "authenticated with page-id"
(let [data {::sq/type :viewer-bundle (let [data {::sq/type :viewer-bundle
:profile-id (:id prof) :profile-id (:id prof)
:page-id (:id page)} :file-id (:id file)
:page-id (get-in file [:data :pages 0])}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
@ -44,29 +43,32 @@
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (contains? result :share-token))
(t/is (contains? result :page)) (t/is (contains? result :page))
(t/is (contains? result :file)) (t/is (contains? result :file))
(t/is (contains? result :project))))) (t/is (contains? result :project)))))
(t/testing "generate share token" (t/testing "generate share token"
(let [data {::sm/type :generate-page-share-token (let [data {::sm/type :create-file-share-token
:id (:id page)} :profile-id (:id prof)
:file-id (:id file)
:page-id (get-in file [:data :pages 0])}
out (th/try-on! (sm/handle data))] out (th/try-on! (sm/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [result (:result out)] (let [result (:result out)]
(t/is (string? (:share-token result))) (t/is (string? (:token result)))
(reset! token (:share-token result))))) (reset! token (:token result)))))
(t/testing "authenticated with page-id" (t/testing "not authenticated with page-id"
(let [data {::sq/type :viewer-bundle (let [data {::sq/type :viewer-bundle
:profile-id (:id prof2) :profile-id (:id prof2)
:page-id (:id page)} :file-id (:id file)
:page-id (get-in file [:data :pages 0])}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
(let [error (:error out) (let [error (:error out)
error-data (ex-data error)] error-data (ex-data error)]
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
@ -78,11 +80,12 @@
(t/is (th/ex-info? error)) (t/is (th/ex-info? error))
(t/is (= (:type error-data) :not-found))))) (t/is (= (:type error-data) :not-found)))))
(t/testing "authenticated with page-id and token" (t/testing "authenticated with token & profile"
(let [data {::sq/type :viewer-bundle (let [data {::sq/type :viewer-bundle
:profile-id (:id prof2) :profile-id (:id prof2)
:page-id (:id page) :share-token @token
:share-token @token} :file-id (:id file)
:page-id (get-in file [:data :pages 0])}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)
@ -92,10 +95,11 @@
(t/is (contains? result :file)) (t/is (contains? result :file))
(t/is (contains? result :project))))) (t/is (contains? result :project)))))
(t/testing "not authenticated with page-id and token" (t/testing "authenticated with token"
(let [data {::sq/type :viewer-bundle (let [data {::sq/type :viewer-bundle
:page-id (:id page) :share-token @token
:share-token @token} :file-id (:id file)
:page-id (get-in file [:data :pages 0])}
out (th/try-on! (sq/handle data))] out (th/try-on! (sq/handle data))]
;; (th/print-result! out) ;; (th/print-result! out)

View file

@ -1,3 +1,12 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.tests.test-util-svg (ns app.tests.test-util-svg
(:require (:require
[clojure.test :as t] [clojure.test :as t]

View file

@ -163,6 +163,25 @@
:else (recur (rest col1) col2 join-fn :else (recur (rest col1) col2 join-fn
(core/concat acc (map (partial join-fn (first col1)) col2)))))) (core/concat acc (map (partial join-fn (first col1)) col2))))))
(def sentinel
#?(:clj (Object.)
:cljs (js/Object.)))
(defn update-in-when
[m key-seq f & args]
(let [found (get-in m key-seq sentinel)]
(if-not (identical? sentinel found)
(assoc-in m key-seq (apply f found args))
m)))
(defn update-when
[m key f & args]
(let [found (get m key sentinel)]
(if-not (identical? sentinel found)
(assoc m key (apply f found args))
m)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Parsing / Conversion ;; Data Parsing / Conversion
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -21,6 +21,7 @@
[app.common.uuid :as uuid])) [app.common.uuid :as uuid]))
(def page-version 5) (def page-version 5)
(def file-version 1)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Page Transformation Changes ;; Page Transformation Changes
@ -28,123 +29,139 @@
;; --- Specs ;; --- Specs
(s/def ::frame-id uuid?)
(s/def ::id uuid?) (s/def ::id uuid?)
(s/def ::shape-id uuid?) (s/def ::integer integer?)
(s/def ::session-id uuid?)
(s/def ::name string?) (s/def ::name string?)
(s/def ::page-id uuid?)
(s/def ::parent-id uuid?) (s/def ::parent-id uuid?)
(s/def ::string string?)
(s/def ::type keyword?)
(s/def ::uuid uuid?)
;; Page Options ;; Page Options
(s/def ::grid-x number?)
(s/def ::grid-y number?)
(s/def ::grid-color string?)
(s/def ::options
(s/keys :opt-un [::grid-y ;; TODO: missing specs for :saved-grids
::grid-x
::grid-color])) (s/def :internal.page.options/background string?)
(s/def :internal.page/options
(s/keys :opt-un [:internal.page.options/background]))
;; Interactions ;; Interactions
(s/def ::event-type #{:click}) ; In the future we will have more options (s/def :internal.shape.interaction/event-type #{:click}) ; In the future we will have more options
(s/def ::action-type #{:navigate}) (s/def :internal.shape.interaction/action-type #{:navigate})
(s/def ::destination uuid?) (s/def :internal.shape.interaction/destination ::uuid)
(s/def ::interaction (s/def :internal.shape/interaction
(s/keys :req-un [::event-type (s/keys :req-un [:internal.shape.interaction/event-type
::action-type :internal.shape.interaction/action-type
::destination])) :internal.shape.interaction/destination]))
(s/def ::interactions (s/coll-of ::interaction :kind vector?)) (s/def :internal.shape/interactions
(s/coll-of :internal.shape/interaction :kind vector?))
;; Page Data related ;; Page Data related
(s/def ::blocked boolean?) (s/def :internal.shape/blocked boolean?)
(s/def ::collapsed boolean?) (s/def :internal.shape/collapsed boolean?)
(s/def ::content any?) (s/def :internal.shape/content any?)
(s/def ::fill-color string?) (s/def :internal.shape/fill-color string?)
(s/def ::fill-opacity number?) (s/def :internal.shape/fill-opacity number?)
(s/def ::font-family string?) (s/def :internal.shape/font-family string?)
(s/def ::font-size number?) (s/def :internal.shape/font-size number?)
(s/def ::font-style string?) (s/def :internal.shape/font-style string?)
(s/def ::font-weight string?) (s/def :internal.shape/font-weight string?)
(s/def ::hidden boolean?) (s/def :internal.shape/hidden boolean?)
(s/def ::letter-spacing number?) (s/def :internal.shape/letter-spacing number?)
(s/def ::line-height number?) (s/def :internal.shape/line-height number?)
(s/def ::locked boolean?) (s/def :internal.shape/locked boolean?)
(s/def ::page-id uuid?) (s/def :internal.shape/page-id uuid?)
(s/def ::proportion number?) (s/def :internal.shape/proportion number?)
(s/def ::proportion-lock boolean?) (s/def :internal.shape/proportion-lock boolean?)
(s/def ::rx number?) (s/def :internal.shape/rx number?)
(s/def ::ry number?) (s/def :internal.shape/ry number?)
(s/def ::stroke-color string?) (s/def :internal.shape/stroke-color string?)
(s/def ::stroke-opacity number?) (s/def :internal.shape/stroke-opacity number?)
(s/def ::stroke-style #{:solid :dotted :dashed :mixed :none}) (s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none})
(s/def ::stroke-width number?) (s/def :internal.shape/stroke-width number?)
(s/def ::stroke-alignment #{:center :inner :outer}) (s/def :internal.shape/stroke-alignment #{:center :inner :outer})
(s/def ::text-align #{"left" "right" "center" "justify"}) (s/def :internal.shape/text-align #{"left" "right" "center" "justify"})
(s/def ::type keyword?) (s/def :internal.shape/x number?)
(s/def ::x number?) (s/def :internal.shape/y number?)
(s/def ::y number?) (s/def :internal.shape/cx number?)
(s/def ::cx number?) (s/def :internal.shape/cy number?)
(s/def ::cy number?) (s/def :internal.shape/width number?)
(s/def ::width number?) (s/def :internal.shape/height number?)
(s/def ::height number?) (s/def :internal.shape/index integer?)
(s/def ::index integer?)
(s/def ::x1 number?)
(s/def ::y1 number?)
(s/def ::x2 number?)
(s/def ::y2 number?)
(s/def ::suffix string?) (s/def :internal.shape/x1 number?)
(s/def ::scale number?) (s/def :internal.shape/y1 number?)
(s/def ::export (s/def :internal.shape/x2 number?)
(s/keys :req-un [::type ::suffix ::scale])) (s/def :internal.shape/y2 number?)
(s/def ::exports (s/coll-of ::export :kind vector?)) (s/def :internal.shape.export/suffix string?)
(s/def :internal.shape.export/scale number?)
(s/def :internal.shape/export
(s/keys :req-un [::type
:internal.shape.export/suffix
:internal.shape.export/scale]))
(s/def :internal.shape/exports
(s/coll-of :internal.shape/export :kind vector?))
(s/def ::selrect (s/keys :req-un [::x (s/def :internal.shape/selrect
::y (s/keys :req-un [:internal.shape/x
::x1 :internal.shape/y
::y1 :internal.shape/x1
::x2 :internal.shape/y1
::y2 :internal.shape/x2
::width :internal.shape/y2
::height])) :internal.shape/width
:internal.shape/height]))
(s/def ::point (s/keys :req-un [::x ::y])) (s/def :internal.shape/point
(s/def ::points (s/coll-of ::point :kind vector?)) (s/keys :req-un [:internal.shape/x :internal.shape/y]))
(s/def :internal.shape/points
(s/coll-of :internal.shape/point :kind vector?))
(s/def ::shape-attrs (s/def ::shape-attrs
(s/keys :opt-un [::blocked (s/keys :opt-un [:internal.shape/blocked
::collapsed :internal.shape/collapsed
::content :internal.shape/content
::fill-color :internal.shape/fill-color
::fill-opacity :internal.shape/fill-opacity
::font-family :internal.shape/font-family
::font-size :internal.shape/font-size
::font-style :internal.shape/font-style
::font-weight :internal.shape/font-weight
::hidden :internal.shape/hidden
::letter-spacing :internal.shape/letter-spacing
::line-height :internal.shape/line-height
::locked :internal.shape/locked
::proportion :internal.shape/proportion
::proportion-lock :internal.shape/proportion-lock
::rx ::ry :internal.shape/rx
::cx ::cy :internal.shape/ry
::x ::y :internal.shape/cx
::exports :internal.shape/cy
::stroke-color :internal.shape/x
::stroke-opacity :internal.shape/y
::stroke-style :internal.shape/exports
::stroke-width :internal.shape/stroke-color
::stroke-alignment :internal.shape/stroke-opacity
::text-align :internal.shape/stroke-style
::width ::height :internal.shape/stroke-width
::interactions :internal.shape/stroke-alignment
::selrect :internal.shape/text-align
::points])) :internal.shape/width
:internal.shape/height
:internal.shape/interactions
:internal.shape/selrect
:internal.shape/points]))
(s/def ::minimal-shape (s/def ::minimal-shape
(s/keys :req-un [::type ::name] (s/keys :req-un [::type ::name]
@ -154,73 +171,157 @@
(s/and ::minimal-shape ::shape-attrs (s/and ::minimal-shape ::shape-attrs
(s/keys :opt-un [::id]))) (s/keys :opt-un [::id])))
(s/def ::shapes (s/coll-of uuid? :kind vector?)) (s/def :internal.page/objects (s/map-of uuid? ::shape))
(s/def ::canvas (s/coll-of uuid? :kind vector?))
(s/def ::objects (s/def ::page
(s/map-of uuid? ::shape)) (s/keys :req-un [::id
::name
:internal.page/options
:internal.page/objects]))
(s/def :internal.color/name ::string)
(s/def :internal.color/value ::string)
(s/def ::color
(s/keys :req-un [::id
:internal.color/name
:internal.color/value]))
(s/def :internal.media-object/name ::string)
(s/def :internal.media-object/path ::string)
(s/def :internal.media-object/width ::integer)
(s/def :internal.media-object/height ::integer)
(s/def :internal.media-object/mtype ::string)
(s/def :internal.media-object/thumb-path ::string)
(s/def :internal.media-object/thumb-width ::integer)
(s/def :internal.media-object/thumb-height ::integer)
(s/def :internal.media-object/thumb-mtype ::string)
(s/def ::media-object
(s/keys :req-un [::id ::name
:internal.media-object/name
:internal.media-object/path
:internal.media-object/width
:internal.media-object/height
:internal.media-object/mtype
:internal.media-object/thumb-path]))
(s/def :internal.file/colors
(s/map-of ::uuid ::color))
(s/def :internal.file/pages
(s/coll-of ::uuid :kind vector?))
(s/def :internal.file/media
(s/map-of ::uuid ::media-object))
(s/def :internal.file/pages-index
(s/map-of ::uuid ::page))
(s/def ::data (s/def ::data
(s/keys :req-un [::version (s/keys :req-un [:internal.file/pages-index
::options :internal.file/pages]
::objects])) :opt-un [:internal.file/colors
:internal.file/media]))
(s/def ::ids (s/coll-of ::us/uuid)) (defmulti operation-spec :type)
(s/def ::attr keyword?)
(s/def ::val any?)
(s/def ::frame-id uuid?)
(defmulti operation-spec-impl :type) (s/def :internal.operations.set/attr keyword?)
(s/def :internal.operations.set/val any?)
(defmethod operation-spec-impl :set [_] (defmethod operation-spec :set [_]
(s/keys :req-un [::attr ::val])) (s/keys :req-un [:internal.operations.set/attr
:internal.operations.set/val]))
(s/def ::operation (s/multi-spec operation-spec-impl :type)) (defmulti change-spec :type)
(s/def ::operations (s/coll-of ::operation))
(defmulti change-spec-impl :type) (s/def :internal.changes.set-option/option any?)
(s/def :internal.changes.set-option/value any?)
(s/def :set-option/option any? #_(s/or keyword? (s/coll-of keyword?))) (defmethod change-spec :set-option [_]
(s/def :set-option/value any?) (s/keys :req-un [:internal.changes.set-option/option
:internal.changes.set-option/value]))
(defmethod change-spec-impl :set-option [_] (s/def :internal.changes.add-obj/obj ::shape)
(s/keys :req-un [:set-option/option :set-option/value]))
(defmethod change-spec-impl :add-obj [_] (defmethod change-spec :add-obj [_]
(s/keys :req-un [::id ::frame-id ::obj] (s/keys :req-un [::id ::page-id ::frame-id
:internal.changes.add-obj/obj]
:opt-un [::parent-id])) :opt-un [::parent-id]))
(defmethod change-spec-impl :mod-obj [_] (s/def ::operation (s/multi-spec operation-spec :type))
(s/keys :req-un [::id ::operations])) (s/def ::operations (s/coll-of ::operation))
(defmethod change-spec-impl :del-obj [_] (defmethod change-spec :mod-obj [_]
(s/keys :req-un [::id])) (s/keys :req-un [::id ::page-id ::operations]))
(defmethod change-spec-impl :reg-objects [_] (defmethod change-spec :del-obj [_]
(s/keys :req-un [::shapes])) (s/keys :req-un [::id ::page-id]))
(defmethod change-spec-impl :mov-objects [_] (s/def :internal.changes.reg-objects/shapes
(s/keys :req-un [::parent-id ::shapes] (s/coll-of uuid? :kind vector?))
(defmethod change-spec :reg-objects [_]
(s/keys :req-un [::page-id :internal.changes.reg-objects/shapes]))
(defmethod change-spec :mov-objects [_]
(s/keys :req-un [::page-id ::parent-id ::shapes]
:opt-un [::index])) :opt-un [::index]))
(s/def ::change (s/multi-spec change-spec-impl :type)) (defmethod change-spec :add-page [_]
(s/or :empty (s/keys :req-un [::id ::name])
:complete (s/keys :req-un [::page])))
(defmethod change-spec :mod-page [_]
(s/keys :req-un [::id ::name]))
(defmethod change-spec :del-page [_]
(s/keys :req-un [::id]))
(defmethod change-spec :mov-page [_]
(s/keys :req-un [::id ::index]))
(defmethod change-spec :add-color [_]
(s/keys :req-un [::color]))
(defmethod change-spec :mod-color [_]
(s/keys :req-un [::color]))
(defmethod change-spec :del-color [_]
(s/keys :req-un [::id]))
(s/def :internal.changes.media/object ::media-object)
(defmethod change-spec :add-media [_]
(s/keys :req-un [:internal.changes.media/object]))
(defmethod change-spec :mod-media [_]
(s/keys :req-un [:internal.changes.media/object]))
(defmethod change-spec :del-media [_]
(s/keys :req-un [::id]))
(s/def ::change (s/multi-spec change-spec :type))
(s/def ::changes (s/coll-of ::change)) (s/def ::changes (s/coll-of ::change))
(def root uuid/zero) (def root uuid/zero)
(def default-page-data (def empty-page-data
"A reference value of the empty page data." {:options {}
{:version page-version :name "Page"
:options {}
:objects :objects
{root {root
{:id root {:id root
:type :frame :type :frame
:name "root" :name "Root Frame"}}})
:shapes []}}})
(def empty-file-data
{:version file-version
:pages []
:pages-index {}})
(def default-color "#b1b2b5") ;; $color-gray-20 (def default-color "#b1b2b5") ;; $color-gray-20
(def default-shape-attrs (def default-shape-attrs
{:fill-color default-color {:fill-color default-color
:fill-opacity 1}) :fill-opacity 1})
@ -297,7 +398,10 @@
(defn make-minimal-shape (defn make-minimal-shape
[type] [type]
(let [shape (d/seek #(= type (:type %)) minimal-shapes)] (let [shape (d/seek #(= type (:type %)) minimal-shapes)]
(assert shape "unexpected shape type") (when-not shape
(ex/raise :type :assertion
:code :shape-type-not-implemented
:context {:type type}))
(assoc shape (assoc shape
:id (uuid/next) :id (uuid/next)
:x 0 :x 0
@ -315,13 +419,21 @@
:points [] :points []
:segments []))) :segments [])))
(defn make-file-data
([] (make-file-data (uuid/next)))
([id]
(let [
pd (assoc empty-page-data
:id id
:name "Page-1")]
(-> empty-file-data
(update :pages conj id)
(update :pages-index assoc id pd)))))
;; --- Changes Processing Impl ;; --- Changes Processing Impl
(defmulti process-change (defmulti process-change (fn [data change] (:type change)))
(fn [data change] (:type change))) (defmulti process-operation (fn [_ op] (:type op)))
(defmulti process-operation
(fn [_ op] (:type op)))
(defn process-changes (defn process-changes
[data items] [data items]
@ -332,14 +444,18 @@
data))) data)))
(defmethod process-change :set-option (defmethod process-change :set-option
[data {:keys [option value]}] [data {:keys [page-id option value]}]
(d/update-in-when data [:pages-index page-id]
(fn [data]
(let [path (if (seqable? option) option [option])] (let [path (if (seqable? option) option [option])]
(if value (if value
(assoc-in data (into [:options] path) value) (assoc-in data (into [:options] path) value)
(assoc data :options (d/dissoc-in (:options data) path))))) (assoc data :options (d/dissoc-in (:options data) path)))))))
(defmethod process-change :add-obj (defmethod process-change :add-obj
[data {:keys [id obj frame-id parent-id index] :as change}] [data {:keys [id obj page-id frame-id parent-id index] :as change}]
(d/update-in-when data [:pages-index page-id]
(fn [data]
(let [parent-id (or parent-id frame-id) (let [parent-id (or parent-id frame-id)
objects (:objects data)] objects (:objects data)]
(when (and (contains? objects parent-id) (when (and (contains? objects parent-id)
@ -356,34 +472,37 @@
(cond (cond
(some #{id} shapes) shapes (some #{id} shapes) shapes
(nil? index) (conj shapes id) (nil? index) (conj shapes id)
:else (cph/insert-at-index shapes index [id])))))))))) :else (cph/insert-at-index shapes index [id]))))))))))))
(defmethod process-change :mod-obj (defmethod process-change :mod-obj
[data {:keys [id operations] :as change}] [data {:keys [id page-id operations] :as change}]
(update data :objects (d/update-in-when data [:pages-index page-id :objects]
(fn [objects] (fn [objects]
(if-let [obj (get objects id)] (if-let [obj (get objects id)]
(assoc objects id (reduce process-operation obj operations)) (assoc objects id (reduce process-operation obj operations))
objects)))) objects))))
(defmethod process-change :del-obj (defmethod process-change :del-obj
[data {:keys [id] :as change}] [data {:keys [page-id id] :as change}]
(when-let [{:keys [frame-id shapes] :as obj} (get-in data [:objects id])] (letfn [(delete-object [objects id]
(let [objects (:objects data) (if-let [target (get objects id)]
parent-id (cph/get-parent id objects) (let [parent-id (cph/get-parent id objects)
frame-id (:frame-id target)
parent (get objects parent-id) parent (get objects parent-id)
data (update data :objects dissoc id)] objects (dissoc objects id)]
(cond-> data (cond-> objects
(and (not= parent-id frame-id) (and (not= parent-id frame-id)
(= :group (:type parent))) (= :group (:type parent)))
(update-in [:objects parent-id :shapes] (fn [s] (filterv #(not= % id) s))) (update-in [parent-id :shapes] (fn [s] (filterv #(not= % id) s)))
(contains? objects frame-id) (contains? objects frame-id)
(update-in [:objects frame-id :shapes] (fn [s] (filterv #(not= % id) s))) (update-in [frame-id :shapes] (fn [s] (filterv #(not= % id) s)))
(seq shapes) ; Recursive delete all dependend objects
(as-> $ (reduce #(or (process-change %1 {:type :del-obj :id %2}) %1) $ shapes))))))
(seq (:shapes target)) ; Recursive delete all
; dependend objects
(as-> $ (reduce delete-object $ (:shapes target)))))
objects))]
(d/update-in-when data [:pages-index page-id :objects] delete-object id)))
(defn rotation-modifiers (defn rotation-modifiers
[center shape angle] [center shape angle]
@ -395,29 +514,24 @@
:displacement displacement})) :displacement displacement}))
(defmethod process-change :reg-objects (defmethod process-change :reg-objects
[data {:keys [shapes]}] [data {:keys [page-id shapes]}]
(let [objects (:objects data) (letfn [(reg-objects [objects]
xfm (comp (reduce #(update %1 %2 update-group %1) objects
(sequence (comp
(mapcat #(cons % (cph/get-parents % objects))) (mapcat #(cons % (cph/get-parents % objects)))
(map #(get objects %)) (map #(get objects %))
(filter #(= (:type %) :group)) (filter #(= (:type %) :group))
(map :id) (map :id)
(distinct)) (distinct))
shapes)))
ids (into [] xfm shapes) (update-group [group objects]
(let [gcenter (geom/center group)
update-group
(fn [group data]
(let [objects (:objects data)
gcenter (geom/center group)
gxfm (comp gxfm (comp
(map #(get objects %)) (map #(get objects %))
(map #(-> % (map #(-> %
(assoc :modifiers (assoc :modifiers
(rotation-modifiers gcenter % (- (:rotation group 0)))) (rotation-modifiers gcenter % (- (:rotation group 0))))
(geom/transform-shape)))) (geom/transform-shape))))
selrect (-> (into [] gxfm (:shapes group)) selrect (-> (into [] gxfm (:shapes group))
(geom/selection-rect))] (geom/selection-rect))]
@ -429,25 +543,17 @@
(assoc-in [:modifiers :rotation] (:rotation group)) (assoc-in [:modifiers :rotation] (:rotation group))
(geom/transform-shape))))] (geom/transform-shape))))]
(reduce #(update-in %1 [:objects %2] update-group %1) data ids))) (d/update-in-when data [:pages-index page-id :objects] reg-objects)))
(defmethod process-change :mov-objects (defmethod process-change :mov-objects
[data {:keys [parent-id shapes index] :as change}] [data {:keys [parent-id shapes index page-id] :as change}]
(let [ (letfn [(is-valid-move? [objects shape-id]
;; Check if the move from shape-id -> parent-id is valid (let [invalid-targets (cph/calculate-invalid-targets shape-id objects)]
is-valid-move
(fn [shape-id]
(let [invalid-targets (cph/calculate-invalid-targets shape-id (:objects data))]
(and (not (invalid-targets parent-id)) (and (not (invalid-targets parent-id))
(cph/valid-frame-target shape-id parent-id (:objects data))))) (cph/valid-frame-target shape-id parent-id objects))))
valid? (every? is-valid-move shapes) (insert-items [prev-shapes index shapes]
;; Add items into the :shapes property of the target parent-id
insert-items
(fn [prev-shapes]
(let [prev-shapes (or prev-shapes [])] (let [prev-shapes (or prev-shapes [])]
(if index (if index
(cph/insert-at-index prev-shapes index shapes) (cph/insert-at-index prev-shapes index shapes)
@ -458,63 +564,123 @@
prev-shapes prev-shapes
shapes)))) shapes))))
strip-id (strip-id [coll id]
(fn [id] (filterv #(not= % id) coll))
(fn [coll] (filterv #(not= % id) coll)))
cpindex (remove-from-old-parent [cpindex objects shape-id]
(reduce
(fn [index id]
(let [obj (get-in data [:objects id])]
(assoc index id (:parent-id obj))))
{} (keys (:objects data)))
remove-from-old-parent
(fn remove-from-old-parent [data shape-id]
(let [prev-parent-id (get cpindex shape-id)] (let [prev-parent-id (get cpindex shape-id)]
;; Do nothing if the parent id of the shape is the same as ;; Do nothing if the parent id of the shape is the same as
;; the new destination target parent id. ;; the new destination target parent id.
(if (= prev-parent-id parent-id) (if (= prev-parent-id parent-id)
data objects
(loop [sid shape-id (loop [sid shape-id
pid prev-parent-id pid prev-parent-id
data data] objects objects]
(let [obj (get-in data [:objects pid])] (let [obj (get objects pid)]
(if (and (= 1 (count (:shapes obj))) (if (and (= 1 (count (:shapes obj)))
(= sid (first (:shapes obj))) (= sid (first (:shapes obj)))
(= :group (:type obj))) (= :group (:type obj)))
(recur pid (recur pid
(:parent-id obj) (:parent-id obj)
(update data :objects dissoc pid)) (dissoc objects pid))
(update-in data [:objects pid :shapes] (strip-id sid)))))))) (update-in objects [pid :shapes] strip-id sid)))))))
parent (get-in data [:objects parent-id])
frame (if (= :frame (:type parent))
parent
(get-in data [:objects (:frame-id parent)]))
frame-id (:id frame) (update-parent-id [objects id]
(update objects id assoc :parent-id parent-id))
;; Update parent-id references.
update-parent-id
(fn [data id]
(update-in data [:objects id] assoc :parent-id parent-id))
;; Updates the frame-id references that might be outdated ;; Updates the frame-id references that might be outdated
update-frame-ids (update-frame-ids [frame-id objects id]
(fn update-frame-ids [data id] (let [objects (assoc-in objects [id :frame-id] frame-id)
(let [data (assoc-in data [:objects id :frame-id] frame-id) obj (get objects id)]
obj (get-in data [:objects id])] (cond-> objects
(cond-> data
(not= :frame (:type obj)) (not= :frame (:type obj))
(as-> $$ (reduce update-frame-ids $$ (:shapes obj))))))] (as-> $$ (reduce (partial update-frame-ids frame-id) $$ (:shapes obj))))))
(when valid? (move-objects [objects]
(as-> data $ (let [valid? (every? (partial is-valid-move? objects) shapes)
(update-in $ [:objects parent-id :shapes] insert-items) cpindex (reduce (fn [index id]
(let [obj (get objects id)]
(assoc! index id (:parent-id obj))))
(transient {})
(keys objects))
cpindex (persistent! cpindex)
parent (get-in data [:objects parent-id])
parent (get objects parent-id)
frame (if (= :frame (:type parent))
parent
(get objects (:frame-id parent)))
frm-id (:id frame)]
(if valid?
(as-> objects $
(update-in $ [parent-id :shapes] insert-items index shapes)
(reduce update-parent-id $ shapes) (reduce update-parent-id $ shapes)
(reduce remove-from-old-parent $ shapes) (reduce (partial remove-from-old-parent cpindex) $ shapes)
(reduce update-frame-ids $ (get-in $ [:objects parent-id :shapes])))))) (reduce (partial update-frame-ids frm-id) $ (get-in $ [parent-id :shapes])))
objects)))]
(d/update-in-when data [:pages-index page-id :objects] move-objects)))
(defmethod process-change :add-page
[data {:keys [id name page]}]
(cond
(and (string? name) (uuid? id))
(let [page (assoc empty-page-data
:id id
:name name)]
(-> data
(update :pages conj id)
(update :pages-index assoc id page)))
(map? page)
(->> data
(update :pages conj (:id page)
(update :pages-index assoc (:id page) page)))
:else
(ex/raise :type :conflict
:hint "name or page should be provided, never both")))
(defmethod process-change :mod-page
[data {:keys [id name]}]
(d/update-in-when data [:pages-index id] assoc :name name))
(defmethod process-change :del-page
[data {:keys [id]}]
(-> data
(update :pages (fn [pages] (filterv #(not= % id) pages)))
(update :pages-index dissoc id)))
(defmethod process-change :mov-page
[data {:keys [id index]}]
(update data :pages cph/insert-at-index index [id]))
(defmethod process-change :add-color
[data {:keys [color]}]
(update data :colors assoc (:id color) color))
(defmethod process-change :mod-color
[data {:keys [color]}]
(d/update-in-when data [:colors (:id color)] merge color))
(defmethod process-change :del-color
[data {:keys [id]}]
(update data :colors dissoc id))
(defmethod process-change :add-media
[data {:keys [object]}]
(update data :media assoc (:id object) object))
(defmethod process-change :mod-media
[data {:keys [object]}]
(d/update-in-when data [:media (:id object)] merge object))
(defmethod process-change :del-media
[data {:keys [id]}]
(update data :media dissoc id))
(defmethod process-operation :set (defmethod process-operation :set
[shape op] [shape op]
@ -526,5 +692,6 @@
(defmethod process-operation :default (defmethod process-operation :default
[shape op] [shape op]
(ex/raise :type :operation-not-implemented (ex/raise :type :not-implemented
:code :operation-not-implemented
:context {:type (:type op)})) :context {:type (:type op)}))

View file

@ -68,8 +68,8 @@
(d/index-of (:shapes prt) id))) (d/index-of (:shapes prt) id)))
(defn insert-at-index (defn insert-at-index
[shapes index ids] [objects index ids]
(let [[before after] (split-at index shapes) (let [[before after] (split-at index objects)
p? (set ids)] p? (set ids)]
(d/concat [] (d/concat []
(remove p? before) (remove p? before)

View file

@ -8,23 +8,27 @@
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.common.data :as d])) [app.common.data :as d]))
;; TODO: revisit this ;; TODO: revisit this and rename to file-migrations
(defmulti migrate :version) (defmulti migrate :version)
(defn migrate-data (defn migrate-data
([data] ([data]
(if (= (:version data) cp/page-version) (if (= (:version data) cp/file-version)
data data
(reduce #(migrate-data %1 %2 (inc %2)) (reduce #(migrate-data %1 %2 (inc %2))
data data
(range (:version data 0) cp/page-version)))) (range (:version data 0) cp/file-version))))
([data from-version to-version] ([data from-version to-version]
(-> data (-> data
(assoc :version to-version) (assoc :version to-version)
(migrate)))) (migrate))))
(defn migrate-file
[file]
(update file :data migrate-data))
;; Default handler, noop ;; Default handler, noop
(defmethod migrate :default [data] data) (defmethod migrate :default [data] data)
@ -37,49 +41,15 @@
(into index (map #(vector % id) (:shapes obj [])))) (into index (map #(vector % id) (:shapes obj []))))
{} objects)) {} objects))
(defmethod migrate 5 ;; (defmethod migrate 5
[data] ;; [data]
(update data :objects ;; (update data :objects
(fn [objects] ;; (fn [objects]
(let [index (generate-child-parent-index objects)] ;; (let [index (generate-child-parent-index objects)]
(d/mapm ;; (d/mapm
(fn [id obj] ;; (fn [id obj]
(let [parent-id (get index id)] ;; (let [parent-id (get index id)]
(assoc obj :parent-id parent-id))) ;; (assoc obj :parent-id parent-id)))
objects))))) ;; objects)))))
;; We changed the internal model of the shapes so they have their
;; selection rect and the vertices
(defmethod migrate 4
[data]
(letfn [;; Creates a new property `points` that stores the
;; transformed points inside the shape this will be used for
;; the snaps and the selection rect
(calculate-shape-points [objects]
(->> objects
(d/mapm
(fn [id shape]
(if (= (:id shape) uuid/zero)
shape
(assoc shape :points (gsh/shape->points shape)))))))
;; Creates a new property `selrect` that stores the
;; selection rect for the shape
(calculate-shape-selrects [objects]
(->> objects
(d/mapm
(fn [id shape]
(if (= (:id shape) uuid/zero)
shape
(assoc shape :selrect (gsh/points->selrect (:points shape))))))))]
(-> data
;; Adds vertices to shapes
(update :objects calculate-shape-points)
;; Creates selection rects for shapes
(update :objects calculate-shape-selrects))))

View file

@ -2,7 +2,10 @@
;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; 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/. ;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;; ;;
;; Copyright (c) 2016-2019 Andrey Antukh <niwi@niwi.nz> ;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.common.spec (ns app.common.spec
"Data manipulation and query helper functions." "Data manipulation and query helper functions."
@ -103,7 +106,11 @@
(defn spec-assert (defn spec-assert
[spec x] [spec x]
(s/assert* spec x)) (if (s/valid? spec x)
x
(ex/raise :type :assertion
:data (s/explain-data spec x)
#?@(:cljs [:stack (.-stack (ex-info "assertion" {}))]))))
(defmacro assert (defmacro assert
"Development only assertion macro." "Development only assertion macro."

View file

@ -49,7 +49,7 @@ services:
smtp: smtp:
container_name: "uxbox-devenv-smtp" container_name: "uxbox-devenv-smtp"
image: mwader/postfix-relay image: mwader/postfix-relay:latest
restart: always restart: always
environment: environment:
- POSTFIX_myhostname=smtp.uxbox.io - POSTFIX_myhostname=smtp.uxbox.io
@ -75,7 +75,7 @@ services:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
redis: redis:
image: redis:6.0.6 image: redis:6
hostname: "uxbox-devenv-redis" hostname: "uxbox-devenv-redis"
container_name: "uxbox-devenv-redis" container_name: "uxbox-devenv-redis"
restart: always restart: always

View file

@ -62,15 +62,8 @@ http {
location / { location / {
root /home/uxbox/uxbox/frontend/resources/public; root /home/uxbox/uxbox/frontend/resources/public;
try_files $uri /index.html; try_files $uri /index.html;
location ~* \.(js|css).*$ {
add_header Cache-Control "max-age=86400" always; # 24 hours
}
location = /index.html {
add_header Cache-Control "no-cache, max-age=0"; add_header Cache-Control "no-cache, max-age=0";
} }
}
location /api { location /api {
proxy_pass http://127.0.0.1:6060/api; proxy_pass http://127.0.0.1:6060/api;

View file

@ -1,13 +1,15 @@
(ns app.http (ns app.http
(:require (:require
[app.http.export :refer [export-handler]] [app.http.export :refer [export-handler]]
[app.http.thumbnail :refer [thumbnail-handler]]
[app.http.impl :as impl] [app.http.impl :as impl]
[lambdaisland.glogi :as log] [lambdaisland.glogi :as log]
[promesa.core :as p] [promesa.core :as p]
[reitit.core :as r])) [reitit.core :as r]))
(def routes (def routes
[["/export" {:handler export-handler}]]) [["/export/thumbnail" {:handler thumbnail-handler}]
["/export" {:handler export-handler}]])
(defn start! (defn start!
[extra] [extra]

View file

@ -1,3 +1,12 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.http.export (ns app.http.export
(:require (:require
[app.http.export-bitmap :as bitmap] [app.http.export-bitmap :as bitmap]
@ -12,6 +21,7 @@
(s/def ::name ::us/string) (s/def ::name ::us/string)
(s/def ::page-id ::us/uuid) (s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::object-id ::us/uuid) (s/def ::object-id ::us/uuid)
(s/def ::scale ::us/number) (s/def ::scale ::us/number)
(s/def ::suffix ::us/string) (s/def ::suffix ::us/string)
@ -23,7 +33,7 @@
(s/def ::exports (s/coll-of ::export :kind vector?)) (s/def ::exports (s/coll-of ::export :kind vector?))
(s/def ::handler-params (s/def ::handler-params
(s/keys :req-un [::page-id ::object-id ::name ::exports])) (s/keys :req-un [::page-id ::file-id ::object-id ::name ::exports]))
(declare handle-single-export) (declare handle-single-export)
(declare handle-multiple-export) (declare handle-multiple-export)
@ -32,7 +42,7 @@
(defn export-handler (defn export-handler
[{:keys [params browser cookies] :as request}] [{:keys [params browser cookies] :as request}]
(let [{:keys [exports page-id object-id name]} (us/conform ::handler-params params) (let [{:keys [exports page-id file-id object-id name]} (us/conform ::handler-params params)
token (.get ^js cookies "auth-token")] token (.get ^js cookies "auth-token")]
(case (count exports) (case (count exports)
0 (exc/raise :type :validation :code :missing-exports) 0 (exc/raise :type :validation :code :missing-exports)
@ -41,6 +51,7 @@
(assoc (first exports) (assoc (first exports)
:name name :name name
:token token :token token
:file-id file-id
:page-id page-id :page-id page-id
:object-id object-id)) :object-id object-id))
(handle-multiple-export (handle-multiple-export
@ -49,6 +60,7 @@
(assoc item (assoc item
:name name :name name
:token token :token token
:file-id file-id
:page-id page-id :page-id page-id
:object-id object-id)) exports))))) :object-id object-id)) exports)))))

View file

@ -13,10 +13,10 @@
(:import (:import
goog.Uri)) goog.Uri))
(defn- screenshot-object (defn screenshot-object
[browser {:keys [page-id object-id token scale suffix type]}] [browser {:keys [file-id page-id object-id token scale type]}]
(letfn [(handle [page] (letfn [(handle [page]
(let [path (str "/render-object/" page-id "/" object-id) (let [path (str "/render-object/" file-id "/" page-id "/" object-id)
uri (doto (Uri. (:public-uri cfg/config)) uri (doto (Uri. (:public-uri cfg/config))
(.setPath "/") (.setPath "/")
(.setFragment path)) (.setFragment path))
@ -46,13 +46,14 @@
(s/def ::suffix ::us/string) (s/def ::suffix ::us/string)
(s/def ::type #{:jpeg :png}) (s/def ::type #{:jpeg :png})
(s/def ::page-id ::us/uuid) (s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::object-id ::us/uuid) (s/def ::object-id ::us/uuid)
(s/def ::scale ::us/number) (s/def ::scale ::us/number)
(s/def ::token ::us/string) (s/def ::token ::us/string)
(s/def ::filename ::us/string) (s/def ::filename ::us/string)
(s/def ::export-params (s/def ::export-params
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token] (s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token ::file-id]
:opt-un [::filename])) :opt-un [::filename]))
(defn export (defn export

View file

@ -249,13 +249,14 @@
(s/def ::suffix ::us/string) (s/def ::suffix ::us/string)
(s/def ::type #{:svg}) (s/def ::type #{:svg})
(s/def ::page-id ::us/uuid) (s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::object-id ::us/uuid) (s/def ::object-id ::us/uuid)
(s/def ::scale ::us/number) (s/def ::scale ::us/number)
(s/def ::token ::us/string) (s/def ::token ::us/string)
(s/def ::filename ::us/string) (s/def ::filename ::us/string)
(s/def ::export-params (s/def ::export-params
(s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::scale ::token] (s/keys :req-un [::name ::suffix ::type ::object-id ::page-id ::file-id ::scale ::token]
:opt-un [::filename])) :opt-un [::filename]))
(defn export (defn export

View file

@ -0,0 +1,46 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.http.thumbnail
(:require
[app.common.exceptions :as exc :include-macros true]
[app.common.spec :as us]
[app.http.export-bitmap :as bitmap]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[lambdaisland.glogi :as log]
[promesa.core :as p]))
(s/def ::page-id ::us/uuid)
(s/def ::file-id ::us/uuid)
(s/def ::object-id ::us/uuid)
(s/def ::scale ::us/number)
(s/def ::handler-params
(s/keys :req-un [::page-id ::file-id ::object-id]))
(declare handle-single-export)
(declare handle-multiple-export)
(declare perform-export)
(declare attach-filename)
(defn thumbnail-handler
[{:keys [params browser cookies] :as request}]
(let [{:keys [page-id file-id object-id]} (us/conform ::handler-params params)
params {:token (.get ^js cookies "auth-token")
:file-id file-id
:page-id page-id
:object-id object-id
:scale 0.3
:type :jpeg}]
(p/let [content (bitmap/screenshot-object browser params)]
{:status 200
:body content
:headers {"content-type" "image/jpeg"
"content-length" (alength content)}})))

View file

@ -59,14 +59,15 @@ argparse@^1.0.7:
dependencies: dependencies:
sprintf-js "~1.0.2" sprintf-js "~1.0.2"
asn1.js@^4.0.0: asn1.js@^5.2.0:
version "4.10.1" version "5.4.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07"
integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==
dependencies: dependencies:
bn.js "^4.0.0" bn.js "^4.0.0"
inherits "^2.0.1" inherits "^2.0.1"
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.0"
safer-buffer "^2.1.0"
assert@^1.1.1: assert@^1.1.1:
version "1.5.0" version "1.5.0"
@ -106,9 +107,9 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
bn.js@^5.1.1: bn.js@^5.1.1:
version "5.1.2" version "5.1.3"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA== integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
boolbase@^1.0.0, boolbase@~1.0.0: boolbase@^1.0.0, boolbase@~1.0.0:
version "1.0.0" version "1.0.0"
@ -168,15 +169,15 @@ browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
randombytes "^2.0.1" randombytes "^2.0.1"
browserify-sign@^4.0.0: browserify-sign@^4.0.0:
version "4.2.0" version "4.2.1"
resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.0.tgz#545d0b1b07e6b2c99211082bf1b12cce7a0b0e11" resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.1.tgz#eaf4add46dd54be3bb3b36c0cf15abbeba7956c3"
integrity sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA== integrity sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==
dependencies: dependencies:
bn.js "^5.1.1" bn.js "^5.1.1"
browserify-rsa "^4.0.1" browserify-rsa "^4.0.1"
create-hash "^1.2.0" create-hash "^1.2.0"
create-hmac "^1.1.7" create-hmac "^1.1.7"
elliptic "^6.5.2" elliptic "^6.5.3"
inherits "^2.0.4" inherits "^2.0.4"
parse-asn1 "^5.1.5" parse-asn1 "^5.1.5"
readable-stream "^3.6.0" readable-stream "^3.6.0"
@ -333,12 +334,12 @@ core-util-is@~1.0.0:
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=
create-ecdh@^4.0.0: create-ecdh@^4.0.0:
version "4.0.3" version "4.0.4"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e"
integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw== integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==
dependencies: dependencies:
bn.js "^4.1.0" bn.js "^4.1.0"
elliptic "^6.0.0" elliptic "^6.5.3"
create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0:
version "1.2.0" version "1.2.0"
@ -522,7 +523,7 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
elliptic@^6.0.0, elliptic@^6.5.2: elliptic@^6.5.3:
version "6.5.3" version "6.5.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
@ -594,9 +595,9 @@ esprima@^4.0.0:
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
events@^3.0.0: events@^3.0.0:
version "3.1.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.1.0.tgz#84279af1b34cb75aa88bf5ff291f6d0bd9b31a59" resolved "https://registry.yarnpkg.com/events/-/events-3.2.0.tgz#93b87c18f8efcd4202a461aec4dfc0556b639379"
integrity sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg== integrity sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==
evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
version "1.0.3" version "1.0.3"
@ -1126,13 +1127,12 @@ pako@~1.0.2, pako@~1.0.5:
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
parse-asn1@^5.0.0, parse-asn1@^5.1.5: parse-asn1@^5.0.0, parse-asn1@^5.1.5:
version "5.1.5" version "5.1.6"
resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4"
integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==
dependencies: dependencies:
asn1.js "^4.0.0" asn1.js "^5.2.0"
browserify-aes "^1.0.0" browserify-aes "^1.0.0"
create-hash "^1.1.0"
evp_bytestokey "^1.0.0" evp_bytestokey "^1.0.0"
pbkdf2 "^3.0.3" pbkdf2 "^3.0.3"
safe-buffer "^5.1.1" safe-buffer "^5.1.1"
@ -1339,7 +1339,7 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2,
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
"safer-buffer@>= 2.1.2 < 3": "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0:
version "2.1.2" version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@ -1383,9 +1383,9 @@ shadow-cljs-jar@1.3.2:
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
shadow-cljs@^2.10.19: shadow-cljs@^2.10.19:
version "2.10.19" version "2.11.1"
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.10.19.tgz#907bbad10bb3af38f6a728452e3cd9c34f1166d1" resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.11.1.tgz#1658278e2fdc7e0239f9573c505d3fbcfd741a31"
integrity sha512-Dzzn+Ll5okjFze5x1AYqO2qNJOalA1/NBu5pehfyO75HqYzsTK+C4+xufKto6qaMb52iM94p2sbzP+Oh8M3VIw== integrity sha512-3V+mtrGQwFJcb7DIreKwmCtwLKi/a7r8++mdmSTq2z1HRmcQV9DqIY4y+TLS6HkF/GNSIH7+hyHSH8uLdvsPlQ==
dependencies: dependencies:
node-libs-browser "^2.0.0" node-libs-browser "^2.0.0"
readline-sync "^1.4.7" readline-sync "^1.4.7"

View file

@ -37,7 +37,7 @@
funcool/datoteka {:mvn/version "1.2.0"} funcool/datoteka {:mvn/version "1.2.0"}
binaryage/devtools {:mvn/version "RELEASE"} binaryage/devtools {:mvn/version "RELEASE"}
thheller/shadow-cljs {:mvn/version "2.10.19"} thheller/shadow-cljs {:mvn/version "2.11.0"}
;; i18n parsing ;; i18n parsing
carocad/parcera {:mvn/version "0.11.0"} carocad/parcera {:mvn/version "0.11.0"}

View file

@ -25,7 +25,7 @@
"postcss": "^7.0.32", "postcss": "^7.0.32",
"rimraf": "^3.0.0", "rimraf": "^3.0.0",
"sass": "^1.26.10", "sass": "^1.26.10",
"shadow-cljs": "^2.10.19" "shadow-cljs": "^2.11.0"
}, },
"dependencies": { "dependencies": {
"date-fns": "^2.15.0", "date-fns": "^2.15.0",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,11 +9,11 @@
(ns app.main.ui.shapes.frame (ns app.main.ui.shapes.frame
(:require (:require
[rumext.alpha :as mf]
[app.common.data :as d] [app.common.data :as d]
[app.main.ui.shapes.attrs :as attrs]
[app.common.geom.shapes :as geom] [app.common.geom.shapes :as geom]
[app.util.object :as obj])) [app.main.ui.shapes.attrs :as attrs]
[app.util.object :as obj]
[rumext.alpha :as mf]))
(def frame-default-props {:fill-color "#ffffff"}) (def frame-default-props {:fill-color "#ffffff"})

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3768,10 +3768,10 @@ shadow-cljs-jar@1.3.2:
resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b" resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg== integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
shadow-cljs@^2.10.19: shadow-cljs@^2.11.0:
version "2.10.19" version "2.11.0"
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.10.19.tgz#907bbad10bb3af38f6a728452e3cd9c34f1166d1" resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.11.0.tgz#6ffdfaad420b4444ba4bf8f6f21a88efe709d75f"
integrity sha512-Dzzn+Ll5okjFze5x1AYqO2qNJOalA1/NBu5pehfyO75HqYzsTK+C4+xufKto6qaMb52iM94p2sbzP+Oh8M3VIw== integrity sha512-Cu05hL632tQ6UrpTwglIOHm3E/X5Fu8UXnTDUX9nEadcAc608Ojwk1YoVFM4f0Slt8oFZPUNjKQBgy2Sr/r6qw==
dependencies: dependencies:
node-libs-browser "^2.0.0" node-libs-browser "^2.0.0"
readline-sync "^1.4.7" readline-sync "^1.4.7"