From 2cebbbc2f8c18cb9f39c242d6ce6c982385fc22b Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Mon, 3 Feb 2020 22:29:59 +0100 Subject: [PATCH] :recycle: Refactor images storage. --- .../resources/migrations/0003.projects.sql | 22 +- backend/resources/migrations/0005.images.sql | 14 +- backend/src/uxbox/images.clj | 95 ++-- backend/src/uxbox/media.clj | 72 +-- backend/src/uxbox/media_loader.clj | 76 ++- .../src/uxbox/services/mutations/images.clj | 236 +++++---- .../src/uxbox/services/mutations/profile.clj | 68 +-- .../services/mutations/project_files.clj | 105 +++- backend/src/uxbox/services/queries/images.clj | 62 +-- .../src/uxbox/services/queries/profile.clj | 18 +- .../uxbox/services/queries/project_files.clj | 172 ++++--- backend/src/uxbox/util/storage.clj | 43 +- backend/tests.edn | 2 +- backend/tests/user.clj | 4 +- backend/tests/uxbox/tests/helpers.clj | 24 +- backend/tests/uxbox/tests/test_images.clj | 312 ++++++------ .../tests/uxbox/tests/test_services_auth.clj | 2 +- .../tests/test_services_project_files.clj | 86 +++- .../tests/test_services_project_pages.clj | 43 +- .../tests/uxbox/tests/test_services_users.clj | 29 +- frontend/src/uxbox/main/data/icons.cljs | 415 ++++++++-------- frontend/src/uxbox/main/data/images.cljs | 449 +++++++++-------- frontend/src/uxbox/main/data/workspace.cljs | 93 +++- frontend/src/uxbox/main/refs.cljs | 7 + frontend/src/uxbox/main/repo.cljs | 10 +- .../src/uxbox/main/ui/dashboard/icons.cljs | 451 +++++++++--------- .../src/uxbox/main/ui/dashboard/images.cljs | 429 +++++++++-------- frontend/src/uxbox/main/ui/shapes/image.cljs | 31 +- .../src/uxbox/main/ui/workspace/images.cljs | 208 ++++---- .../main/ui/workspace/sidebar/icons.cljs | 5 +- frontend/src/uxbox/util/blob.cljs | 2 + frontend/src/uxbox/util/dom.cljs | 7 +- frontend/src/uxbox/util/files.cljs | 2 + frontend/src/uxbox/util/webapi.cljs | 68 +++ 34 files changed, 2032 insertions(+), 1630 deletions(-) create mode 100644 frontend/src/uxbox/util/webapi.cljs diff --git a/backend/resources/migrations/0003.projects.sql b/backend/resources/migrations/0003.projects.sql index 6ed2717f1..5be4d3bb6 100644 --- a/backend/resources/migrations/0003.projects.sql +++ b/backend/resources/migrations/0003.projects.sql @@ -50,18 +50,30 @@ CREATE INDEX project_files__user_id__idx CREATE INDEX project_files__project_id__idx ON project_files(project_id); -CREATE TABLE project_file_media ( +CREATE TABLE project_file_images ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE, + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + name text NOT NULL, - type text NOT NULL, path text NOT NULL, + width int NOT NULL, + height int NOT NULL, + mtype text NOT NULL, - metadata bytea NULL DEFAULT NULL + thumb_path text NOT NULL, + thumb_width int NOT NULL, + thumb_height int NOT NULL, + thumb_quality int NOT NULL, + thumb_mtype text NOT NULL ); -CREATE INDEX project_file_media__file_id__idx - ON project_file_media(file_id); +CREATE INDEX project_file_images__file_id__idx + ON project_file_images(file_id); + +CREATE INDEX project_file_images__user_id__idx + ON project_file_images(user_id); CREATE TABLE project_file_users ( file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE, diff --git a/backend/resources/migrations/0005.images.sql b/backend/resources/migrations/0005.images.sql index b3237fb3b..246741742 100644 --- a/backend/resources/migrations/0005.images.sql +++ b/backend/resources/migrations/0005.images.sql @@ -15,18 +15,24 @@ CREATE INDEX image_collections__user_id__idx CREATE TABLE images ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, - collection_id uuid REFERENCES image_collections(id) ON DELETE CASCADE, + collection_id uuid NOT NULL REFERENCES image_collections(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), deleted_at timestamptz DEFAULT NULL, + name text NOT NULL, + + path text NOT NULL, width int NOT NULL, height int NOT NULL, - mimetype text NOT NULL, + mtype text NOT NULL, - name text NOT NULL, - path text NOT NULL + thumb_path text NOT NULL, + thumb_width int NOT NULL, + thumb_height int NOT NULL, + thumb_quality int NOT NULL, + thumb_mtype text NOT NULL ); CREATE INDEX images__user_id__idx diff --git a/backend/src/uxbox/images.clj b/backend/src/uxbox/images.clj index b10991de9..158d04aa5 100644 --- a/backend/src/uxbox/images.clj +++ b/backend/src/uxbox/images.clj @@ -10,25 +10,37 @@ [clojure.java.io :as io] [clojure.spec.alpha :as s] [datoteka.core :as fs] - [datoteka.proto :as pt] - [datoteka.storages :as st] [uxbox.common.data :as d] [uxbox.common.spec :as us] + [uxbox.util.storage :as ust] [uxbox.media :as media]) (:import java.io.ByteArrayInputStream java.io.InputStream org.im4java.core.ConvertCmd + org.im4java.core.Info org.im4java.core.IMOperation)) -;; TODO: make this module non-blocking +;; --- Helpers + +(defn format->extension + [format] + (case format + "jpeg" ".jpg" + "webp" ".webp")) + +(defn format->mtype + [format] + (case format + "jpeg" "image/jpeg" + "webp" "image/webp")) ;; --- Thumbnails Generation (s/def ::width integer?) (s/def ::height integer?) (s/def ::quality #(< 0 % 101)) -(s/def ::format #{"jpg" "webp"}) +(s/def ::format #{"jpeg" "webp"}) (s/def ::thumbnail-opts (s/keys :opt-un [::format ::quality ::width ::height])) @@ -37,20 +49,30 @@ (defn generate-thumbnail ([input] (generate-thumbnail input nil)) - ([input {:keys [size quality format width height] - :or {format "jpg" + ([input {:keys [quality format width height] + :or {format "jpeg" quality 92 width 200 height 200} :as opts}] - (us/verify ::thumbnail-opts opts) + ;; (us/verify ::thumbnail-opts opts) (us/verify fs/path? input) - (let [tmp (fs/create-tempfile :suffix (str "." format)) + (let [ext (format->extension format) + tmp (fs/create-tempfile :suffix ext) opr (doto (IMOperation.) (.addImage) + (.autoOrient) - (.resize (int width) (int height) "^") + (.strip) + (.thumbnail (int width) (int height) ">") (.quality (double quality)) + + ;; (.autoOrient) + ;; (.strip) + ;; (.thumbnail (int width) (int height) "^") + ;; (.gravity "center") + ;; (.extent (int width) (int height)) + ;; (.quality (double quality)) (.addImage))] (doto (ConvertCmd.) (.run opr (into-array (map str [input tmp])))) @@ -58,50 +80,19 @@ (fs/delete tmp) (ByteArrayInputStream. thumbnail-data))))) -(defn make-thumbnail - [input {:keys [width height format quality] :as opts}] - (us/verify ::thumbnail-opts opts) - (let [[filename ext] (fs/split-ext (fs/name input)) - suffix (->> [width height quality format] - (interpose ".") - (apply str)) - thumbnail-path (fs/path input (str "thumb-" suffix)) - images-storage media/images-storage - thumbs-storage media/thumbnails-storage] - (if @(st/exists? thumbs-storage thumbnail-path) - (str (st/public-url thumbs-storage thumbnail-path)) - (if @(st/exists? images-storage input) - (let [datapath @(st/lookup images-storage input) - thumbnail (generate-thumbnail datapath opts) - path @(st/save thumbs-storage thumbnail-path thumbnail)] - (str (st/public-url thumbs-storage path))) - nil)))) +(defn info + [path] + (let [instance (Info. (str path))] + {:width (.getImageWidth instance) + :height (.getImageHeight instance)})) -(defn populate-thumbnail - [entry {:keys [src dst] :as opts}] - (assert (map? entry)) +(defn resolve-urls + [row src dst] + (s/assert map? row) (let [src (if (vector? src) src [src]) dst (if (vector? dst) dst [dst]) - src (get-in entry src)] - (if (empty? src) - entry - (assoc-in entry dst (make-thumbnail src opts))))) - -(defn populate-thumbnails - [entry & settings] - (reduce populate-thumbnail entry settings)) - -(defn populate-urls - [entry storage src dst] - (assert (map? entry)) - (assert (st/storage? storage)) - (let [src (if (vector? src) src [src]) - dst (if (vector? dst) dst [dst]) - value (get-in entry src)] + value (get-in row src)] (if (empty? value) - entry - (let [url (str (st/public-url storage value))] - (-> entry - (d/dissoc-in src) - (assoc-in dst url)))))) - + row + (let [url (ust/public-uri media/media-storage value)] + (assoc-in row dst (str url)))))) diff --git a/backend/src/uxbox/media.clj b/backend/src/uxbox/media.clj index 64a365b76..8a6b3e630 100644 --- a/backend/src/uxbox/media.clj +++ b/backend/src/uxbox/media.clj @@ -2,71 +2,35 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2017 Andrey Antukh +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2017-2020 Andrey Antukh (ns uxbox.media "A media storage impl for uxbox." - (:require [mount.core :refer [defstate]] - [clojure.java.io :as io] - [cuerdas.core :as str] - [datoteka.core :as fs] - [datoteka.proto :as stp] - [datoteka.storages :as st] - [datoteka.storages.local :refer [localfs]] - [datoteka.storages.misc :refer [hashed scoped]] - [uxbox.config :refer [config]])) - -;; --- Backends - -(defn- normalize-filename - [path] - (let [parent (or (fs/parent path) "") - [name ext] (fs/split-ext (fs/name path))] - (fs/path parent (str (str/uslug name) ext)))) - -(defrecord FilenameSlugifiedBackend [storage] - stp/IPublicStorage - (-public-uri [_ path] - (stp/-public-uri storage path)) - - stp/IStorage - (-save [_ path content] - (let [^Path path (normalize-filename path)] - (stp/-save storage path content))) - - (-delete [_ path] - (stp/-delete storage path)) - - (-exists? [this path] - (stp/-exists? storage path)) - - (-lookup [_ path] - (stp/-lookup storage path))) + (:require + [mount.core :refer [defstate]] + [clojure.java.io :as io] + [cuerdas.core :as str] + [datoteka.core :as fs] + [uxbox.util.storage :as ust] + [uxbox.config :refer [config]])) ;; --- State (defstate assets-storage - :start (localfs {:basedir (:assets-directory config) - :baseuri (:assets-uri config) - :transform-filename str/uslug})) + :start (ust/create {:base-path (:assets-directory config) + :base-uri (:assets-uri config)})) (defstate media-storage - :start (localfs {:basedir (:media-directory config) - :baseuri (:media-uri config) - :transform-filename str/uslug})) - -(defstate images-storage - :start (-> media-storage - (scoped "images") - (hashed) - (->FilenameSlugifiedBackend))) - -(defstate thumbnails-storage - :start (-> media-storage - (scoped "thumbs"))) + :start (ust/create {:base-path (:media-directory config) + :base-uri (:media-uri config) + :xf (comp ust/random-path + ust/slugify-filename)})) ;; --- Public Api (defn resolve-asset [path] - (str (st/public-url assets-storage path))) + (str (ust/public-uri assets-storage path))) diff --git a/backend/src/uxbox/media_loader.clj b/backend/src/uxbox/media_loader.clj index 896afa814..998051b25 100644 --- a/backend/src/uxbox/media_loader.clj +++ b/backend/src/uxbox/media_loader.clj @@ -14,9 +14,8 @@ [clojure.edn :as edn] [promesa.core :as p] [mount.core :as mount] - [cuerdas.core :as str] - [datoteka.storages :as st] [datoteka.core :as fs] + [cuerdas.core :as str] [uxbox.config] [uxbox.common.spec :as us] [uxbox.db :as db] @@ -27,7 +26,9 @@ [uxbox.util.transit :as t] [uxbox.util.blob :as blob] [uxbox.util.uuid :as uuid] - [uxbox.util.data :as data]) + [uxbox.util.data :as data] + [uxbox.services.mutations.images :as images] + [uxbox.util.storage :as ust]) (:import java.io.Reader java.io.PushbackReader @@ -65,7 +66,7 @@ (-> (db/query-one conn [sql id name]) (p/then' (constantly id))))) -(def create-icon-sql +(def sql:create-icon "insert into icons (user_id, id, collection_id, name, metadata, content) values ('00000000-0000-0000-0000-000000000000'::uuid, $1, $2, $3, $4, $5) on conflict (id) @@ -85,7 +86,9 @@ extension (second (fs/split-ext filename)) data (svg/parse localpath) mdata (select-keys data [:width :height :view-box])] - (db/query-one conn [create-icon-sql icon-id id + (db/query-one conn [sql:create-icon + icon-id + id (:name data filename) (blob/encode mdata) (:content data)]))) @@ -123,56 +126,43 @@ [conn {:keys [name] :as item}] (log/info "Creating or updating image collection:" name) (let [id (uuid/namespaced +images-uuid-ns+ name) + user uuid/zero sql "insert into image_collections (id, user_id, name) - values ($1, '00000000-0000-0000-0000-000000000000'::uuid, $2) - on conflict (id) - do update set name = $2 - returning *;" - sqlv [sql id name]] - (-> (db/query-one conn [sql id name]) - (p/then' (constantly id))))) - -(defn- retrieve-image-size - [path] - (let [info (Info. (str path) true)] - [(.getImageWidth info) (.getImageHeight info)])) + values ($1, $2, $3) + on conflict (id) do nothing + returning *;"] + (-> (db/query-one db/pool [sql id user name]) + (p/then (constantly id))))) (defn- image-exists? [conn id] (s/assert ::us/uuid id) - (let [sql "select id - from images as i - where i.id = $1 - and i.user_id = '00000000-0000-0000-0000-000000000000'::uuid"] + (let [sql "select id from images as i + where i.id = $1 and i.user_id = '00000000-0000-0000-0000-000000000000'::uuid"] (-> (db/query-one conn [sql id]) (p/then (fn [row] (if row true false)))))) -(def create-image-sql - "insert into images (user_id, id, collection_id, name, path, width, height, mimetype) - values ('00000000-0000-0000-0000-000000000000'::uuid, $1, $2, $3, $4, $5, $6, $7) - returning *;") - (defn- create-image [conn id image-id localpath] (s/assert fs/path? localpath) (s/assert ::us/uuid id) (s/assert ::us/uuid image-id) - (let [storage media/images-storage - filename (fs/name localpath) - [width height] (retrieve-image-size localpath) + (let [filename (fs/name localpath) extension (second (fs/split-ext filename)) - mimetype (case extension - ".jpg" "image/jpeg" - ".png" "image/png")] - (-> (st/save storage filename localpath) - (p/then (fn [path] - (db/query-one conn [create-image-sql image-id id - filename - (str path) - width - height - mimetype]))) - (p/then (constantly nil))))) + file (io/as-file localpath) + mtype (case extension + ".jpg" "image/jpeg" + ".png" "image/png" + ".webp" "image/webp")] + + (images/create-image conn {:content {:path localpath + :name filename + :mtype mtype + :size (.length file)} + :id image-id + :collection-id id + :user uuid/zero + :name filename}))) (defn- import-image [conn id fpath] @@ -218,7 +208,7 @@ (exit! -1)) (fs/path path)) -(defn- read-import-file +(defn- read-file [path] (let [path (validate-path path) reader (java.io.PushbackReader. (io/reader path))] @@ -244,7 +234,7 @@ (defn -main [& [path]] - (let [[basedir data] (read-import-file path)] + (let [[basedir data] (read-file path)] (start-system) (-> (db/with-atomic [conn db/pool] (importer conn basedir data)) diff --git a/backend/src/uxbox/services/mutations/images.clj b/backend/src/uxbox/services/mutations/images.clj index f898c20ea..a8febfcab 100644 --- a/backend/src/uxbox/services/mutations/images.clj +++ b/backend/src/uxbox/services/mutations/images.clj @@ -21,135 +21,184 @@ [uxbox.util.blob :as blob] [uxbox.util.data :as data] [uxbox.util.uuid :as uuid] + [uxbox.util.storage :as ust] [vertx.core :as vc])) -(def +thumbnail-options+ - {:src :path - :dst :thumbnail - :width 300 - :height 100 - :quality 92 +(def thumbnail-options + {:width 800 + :height 800 + :quality 80 :format "webp"}) -(defn- populate-thumbnail - [row] - (let [opts +thumbnail-options+] - (-> (px/submit! #(images/populate-thumbnails row opts)) - (su/handle-on-context)))) - -(defn- populate-thumbnails - [rows] - (if (empty? rows) - rows - (p/all (map populate-thumbnail rows)))) - -(defn- populate-urls - [row] - (images/populate-urls row media/images-storage :path :url)) - (s/def ::id ::us/uuid) (s/def ::name ::us/string) (s/def ::user ::us/uuid) -(s/def ::collection-id (s/nilable ::us/uuid)) ;; --- Create Collection -(s/def ::create-image-collection +(declare create-images-collection) + +(s/def ::create-images-collection (s/keys :req-un [::user ::us/name] :opt-un [::id])) -(sm/defmutation ::create-image-collection +(sm/defmutation ::create-images-collection [{:keys [id user name] :as params}] - (let [sql "insert into image_collections (id, user_id, name) - values ($1, $2, $3) returning *;"] - (db/query-one db/pool [sql (or id (uuid/next)) user name]))) + (db/with-atomic [conn db/pool] + (create-images-collection conn params))) + +(defn create-images-collection + [conn {:keys [id user name] :as params}] + (let [id (or id (uuid/next)) + sql "insert into image_collections (id, user_id, name) + values ($1, $2, $3) + on conflict (id) do nothing + returning *;"] + (db/query-one db/pool [sql id user name]))) ;; --- Update Collection -(s/def ::update-images-collection +(def ^:private + sql:rename-images-collection + "update image_collections + set name = $3 + where id = $1 + and user_id = $2 + returning *;") + +(s/def ::rename-images-collection (s/keys :req-un [::id ::user ::us/name])) -(sm/defmutation ::update-images-collection +(sm/defmutation ::rename-images-collection [{:keys [id user name] :as params}] - (let [sql "update image_collections - set name = $3 - where id = $1 - and user_id = $2 - returning *;"] - (db/query-one db/pool [sql id user name]))) + (db/with-atomic [conn db/pool] + (db/query-one conn [sql:rename-images-collection id user name]))) ;; --- Delete Collection (s/def ::delete-images-collection (s/keys :req-un [::user ::id])) +(def ^:private + sql:delete-images-collection + "update image_collections + set deleted_at = clock_timestamp() + where id = $1 + and user_id = $2 + returning id") + (sm/defmutation ::delete-images-collection [{:keys [id user] :as params}] - (let [sql "update image_collections - set deleted_at = clock_timestamp() - where id = $1 - and user_id = $2 - returning id"] - (-> (db/query-one db/pool [sql id user]) - (p/then' su/raise-not-found-if-nil)))) + (-> (db/query-one db/pool [sql:delete-images-collection id user]) + (p/then' su/raise-not-found-if-nil))) ;; --- Create Image (Upload) -(defn- store-image-in-fs - [{:keys [name path] :as upload}] - (let [filename (fs/name name) - storage media/images-storage] - (-> (ds/save storage filename (fs/path path)) - (su/handle-on-context)))) - -(def sql:create-image - "insert into images (user_id, name, collection_id, path, width, height, mimetype) - values ($1, $2, $3, $4, $5, $6, $7) returning *") - -(defn- store-image-in-db - [conn {:keys [id user name path collection-id height width mimetype]}] - (let [sqlv [sql:create-image user name collection-id - path width height mimetype]] - (-> (db/query-one conn sqlv) - (p/then populate-thumbnail) - (p/then populate-urls)))) +(declare select-collection-for-update) +(declare create-image) +(declare persist-image-on-fs) +(declare persist-image-thumbnail-on-fs) (def valid-image-types? #{"image/jpeg", "image/png", "image/webp"}) (s/def :uxbox$upload/name ::us/string) (s/def :uxbox$upload/size ::us/integer) -(s/def :uxbox$upload/mtype ::us/string) +(s/def :uxbox$upload/mtype valid-image-types?) +(s/def :uxbox$upload/path ::us/string) + (s/def ::upload (s/keys :req-un [:uxbox$upload/name :uxbox$upload/size + :uxbox$upload/path :uxbox$upload/mtype])) -(s/def ::file ::upload) -(s/def ::width ::us/integer) -(s/def ::height ::us/integer) -(s/def ::mimetype valid-image-types?) +(s/def ::collection-id ::us/uuid) +(s/def ::content ::upload) -(s/def ::create-image - (s/keys :req-un [::user ::name ::file ::width ::height ::mimetype] - :opt-un [::id ::collection-id])) +(s/def ::upload-image + (s/keys :req-un [::user ::name ::content ::collection-id] + :opt-un [::id])) -(sm/defmutation ::create-image - [{:keys [file] :as params}] - (when-not (valid-image-types? (:mtype file)) +(sm/defmutation ::upload-image + [{:keys [collection-id user] :as params}] + (db/with-atomic [conn db/pool] + (p/let [coll (select-collection-for-update conn collection-id)] + (when (not= (:user-id coll) user) + (ex/raise :type :validation + :code :not-authorized)) + (create-image conn params)))) + +(def ^:private sql:insert-image + "insert into images + (id, collection_id, user_id, name, path, width, height, mtype, + thumb_path, thumb_width, thumb_height, thumb_quality, thumb_mtype) + values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + returning *") + +(defn create-image + [conn {:keys [id content collection-id user name] :as params}] + (when-not (valid-image-types? (:mtype content)) (ex/raise :type :validation :code :image-type-not-allowed :hint "Seems like you are uploading an invalid image.")) - (-> (store-image-in-fs file) - (p/then (fn [path] - (store-image-in-db db/pool (assoc params :path (str path))))))) + (p/let [image-opts (vc/blocking (images/info (:path content))) + image-path (persist-image-on-fs content) + thumb-opts thumbnail-options + thumb-path (persist-image-thumbnail-on-fs thumb-opts image-path) + id (or id (uuid/next)) + + sqlv [sql:insert-image + id + collection-id + user + name + (str image-path) + (:width image-opts) + (:height image-opts) + (:mtype content) + (str thumb-path) + (:width thumb-opts) + (:height thumb-opts) + (:quality thumb-opts) + (images/format->mtype (:format thumb-opts))]] + + (-> (db/query-one conn sqlv) + (p/then' #(images/resolve-urls % :path :uri)) + (p/then' #(images/resolve-urls % :thumb-path :thumb-uri))))) + +(defn- select-collection-for-update + [conn id] + (let [sql "select c.id, c.user_id + from image_collections as c + where c.id = $1 + and c.deleted_at is null + for update;"] + (-> (db/query-one conn [sql id]) + (p/then' su/raise-not-found-if-nil)))) + +(defn persist-image-on-fs + [{:keys [name path] :as upload}] + (vc/blocking + (let [filename (fs/name name)] + (ust/save! media/media-storage filename path)))) + +(defn persist-image-thumbnail-on-fs + [thumb-opts input-path] + (vc/blocking + (let [input-path (ust/lookup media/media-storage input-path) + thumb-data (images/generate-thumbnail input-path thumb-opts) + [filename ext] (fs/split-ext (fs/name input-path)) + thumb-name (->> (images/format->extension (:format thumb-opts)) + (str "thumbnail-" filename))] + (ust/save! media/media-storage thumb-name thumb-data)))) ;; --- Update Image (s/def ::update-image (s/keys :req-un [::id ::user ::name ::collection-id])) -(def ^:private update-image-sql +(def ^:private sql:update-image "update images set name = $3, collection_id = $2 @@ -159,31 +208,30 @@ (sm/defmutation ::update-image [{:keys [id name user collection-id] :as params}] - (let [sql update-image-sql] - (db/query-one db/pool [sql id collection-id name user]))) + (db/query-one db/pool [sql:update-image id collection-id name user])) ;; --- Copy Image (declare retrieve-image) -(s/def ::copy-image - (s/keys :req-un [::id ::collection-id ::user])) +;; (s/def ::copy-image +;; (s/keys :req-un [::id ::collection-id ::user])) -(sm/defmutation ::copy-image - [{:keys [user id collection-id] :as params}] - (letfn [(copy-image [conn {:keys [path] :as image}] - (-> (ds/lookup media/images-storage (:path image)) - (p/then (fn [path] (ds/save media/images-storage (fs/name path) path))) - (p/then (fn [path] - (-> image - (assoc :path (str path) :collection-id collection-id) - (dissoc :id)))) - (p/then (partial store-image-in-db conn))))] +;; (sm/defmutation ::copy-image +;; [{:keys [user id collection-id] :as params}] +;; (letfn [(copy-image [conn {:keys [path] :as image}] +;; (-> (ds/lookup media/images-storage (:path image)) +;; (p/then (fn [path] (ds/save media/images-storage (fs/name path) path))) +;; (p/then (fn [path] +;; (-> image +;; (assoc :path (str path) :collection-id collection-id) +;; (dissoc :id)))) +;; (p/then (partial store-image-in-db conn))))] - (db/with-atomic [conn db/pool] - (-> (retrieve-image conn {:id id :user user}) - (p/then su/raise-not-found-if-nil) - (p/then (partial copy-image conn)))))) +;; (db/with-atomic [conn db/pool] +;; (-> (retrieve-image conn {:id id :user user}) +;; (p/then su/raise-not-found-if-nil) +;; (p/then (partial copy-image conn)))))) ;; --- Delete Image diff --git a/backend/src/uxbox/services/mutations/profile.clj b/backend/src/uxbox/services/mutations/profile.clj index 2154d6a3d..bdfc8179c 100644 --- a/backend/src/uxbox/services/mutations/profile.clj +++ b/backend/src/uxbox/services/mutations/profile.clj @@ -178,46 +178,46 @@ ;; --- Mutation: Update Photo -(s/def :uxbox$upload/name ::us/string) -(s/def :uxbox$upload/size ::us/integer) -(s/def :uxbox$upload/mtype ::us/string) -(s/def ::upload - (s/keys :req-un [:uxbox$upload/name - :uxbox$upload/size - :uxbox$upload/mtype])) +;; (s/def :uxbox$upload/name ::us/string) +;; (s/def :uxbox$upload/size ::us/integer) +;; (s/def :uxbox$upload/mtype ::us/string) +;; (s/def ::upload +;; (s/keys :req-un [:uxbox$upload/name +;; :uxbox$upload/size +;; :uxbox$upload/mtype])) -(s/def ::file ::upload) -(s/def ::update-profile-photo - (s/keys :req-un [::user ::file])) +;; (s/def ::file ::upload) +;; (s/def ::update-profile-photo +;; (s/keys :req-un [::user ::file])) -(def valid-image-types? - #{"image/jpeg", "image/png", "image/webp"}) +;; (def valid-image-types? +;; #{"image/jpeg", "image/png", "image/webp"}) -(sm/defmutation ::update-profile-photo - [{:keys [user file] :as params}] - (letfn [(store-photo [{:keys [name path] :as upload}] - (let [filename (fs/name name) - storage media/images-storage] - (-> (ds/save storage filename path) - #_(su/handle-on-context)))) +;; (sm/defmutation ::update-profile-photo +;; [{:keys [user file] :as params}] +;; (letfn [(store-photo [{:keys [name path] :as upload}] +;; (let [filename (fs/name name) +;; storage media/media-storage] +;; (-> (ds/save storage filename path) +;; #_(su/handle-on-context)))) - (update-user-photo [path] - (let [sql "update users - set photo = $1 - where id = $2 - and deleted_at is null - returning id, photo"] - (-> (db/query-one db/pool [sql (str path) user]) - (p/then' su/raise-not-found-if-nil) - (p/then profile/resolve-thumbnail))))] +;; (update-user-photo [path] +;; (let [sql "update users +;; set photo = $1 +;; where id = $2 +;; and deleted_at is null +;; returning id, photo"] +;; (-> (db/query-one db/pool [sql (str path) user]) +;; (p/then' su/raise-not-found-if-nil) +;; (p/then profile/resolve-thumbnail))))] - (when-not (valid-image-types? (:mtype file)) - (ex/raise :type :validation - :code :image-type-not-allowed - :hint "Seems like you are uploading an invalid image.")) +;; (when-not (valid-image-types? (:mtype file)) +;; (ex/raise :type :validation +;; :code :image-type-not-allowed +;; :hint "Seems like you are uploading an invalid image.")) - (-> (store-photo file) - (p/then update-user-photo)))) +;; (-> (store-photo file) +;; (p/then update-user-photo)))) ;; --- Mutation: Register Profile diff --git a/backend/src/uxbox/services/mutations/project_files.clj b/backend/src/uxbox/services/mutations/project_files.clj index e6b7b14ca..e707bf542 100644 --- a/backend/src/uxbox/services/mutations/project_files.clj +++ b/backend/src/uxbox/services/mutations/project_files.clj @@ -11,15 +11,21 @@ (:require [clojure.spec.alpha :as s] [promesa.core :as p] + [datoteka.core :as fs] [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.images :as images] [uxbox.common.exceptions :as ex] [uxbox.common.spec :as us] [uxbox.common.pages :as cp] [uxbox.services.mutations :as sm] [uxbox.services.mutations.projects :as proj] + [uxbox.services.mutations.images :as imgs] [uxbox.services.util :as su] [uxbox.util.blob :as blob] - [uxbox.util.uuid :as uuid])) + [uxbox.util.uuid :as uuid] + [uxbox.util.storage :as ust] + [vertx.core :as vc])) ;; --- Helpers & Specs @@ -123,7 +129,8 @@ (-> (db/query-one conn [sql id name]) (p/then' su/constantly-nil)))) -;; --- Mutation: Delete Project + +;; --- Mutation: Delete Project File (declare delete-file) @@ -147,3 +154,97 @@ (let [sql sql:delete-file] (-> (db/query-one conn [sql id]) (p/then' su/constantly-nil)))) + +;; --- Mutation: Upload File Image + +(s/def ::file-id ::us/uuid) +(s/def ::content ::imgs/upload) + +(s/def ::upload-project-file-image + (s/keys :req-un [::user ::file-id ::name ::content] + :opt-un [::id])) + +(declare create-file-image) + +(sm/defmutation ::upload-project-file-image + [{:keys [user file-id] :as params}] + (db/with-atomic [conn db/pool] + (check-edition-permissions! conn user file-id) + (create-file-image conn params))) + +(def ^:private + sql:insert-file-image + "insert into project_file_images + (file_id, user_id, name, path, width, height, mtype, + thumb_path, thumb_width, thumb_height, thumb_quality, thumb_mtype) + values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + returning *") + +(defn- create-file-image + [conn {:keys [content file-id user name] :as params}] + (when-not (imgs/valid-image-types? (:mtype content)) + (ex/raise :type :validation + :code :image-type-not-allowed + :hint "Seems like you are uploading an invalid image.")) + + (p/let [image-opts (vc/blocking (images/info (:path content))) + image-path (imgs/persist-image-on-fs content) + thumb-opts imgs/thumbnail-options + thumb-path (imgs/persist-image-thumbnail-on-fs thumb-opts image-path) + + sqlv [sql:insert-file-image + file-id + user + name + (str image-path) + (:width image-opts) + (:height image-opts) + (:mtype content) + (str thumb-path) + (:width thumb-opts) + (:height thumb-opts) + (:quality thumb-opts) + (images/format->mtype (:format thumb-opts))]] + (-> (db/query-one db/pool sqlv) + (p/then' #(images/resolve-urls % :path :uri)) + (p/then' #(images/resolve-urls % :thumb-path :thumb-uri))))) + +;; --- Mutation: Import from collection + +(declare copy-image!) + +(s/def ::import-image-to-file + (s/keys :req-un [::image-id ::file-id ::user])) + +(def ^:private sql:select-image-by-id + "select img.* from images as img where id=$1") + +(sm/defmutation ::import-image-to-file + [{:keys [image-id file-id user]}] + (db/with-atomic [conn db/pool] + (p/let [image (-> (db/query-one conn [sql:select-image-by-id image-id]) + (p/then' su/raise-not-found-if-nil)) + image-path (copy-image! (:path image)) + thumb-path (copy-image! (:thumb-path image)) + sqlv [sql:insert-file-image + file-id + user + (:name image) + (str image-path) + (:width image) + (:height image) + (:mtype image) + (str thumb-path) + (:thumb-width image) + (:thumb-height image) + (:thumb-quality image) + (:thumb-mtype image)]] + (-> (db/query-one db/pool sqlv) + (p/then' #(images/resolve-urls % :path :uri)) + (p/then' #(images/resolve-urls % :thumb-path :thumb-uri)))))) + +(defn- copy-image! + [path] + (vc/blocking + (let [image-path (ust/lookup media/media-storage path)] + (ust/save! media/media-storage (fs/name image-path) image-path)))) diff --git a/backend/src/uxbox/services/queries/images.clj b/backend/src/uxbox/services/queries/images.clj index 735835322..5b5f60a95 100644 --- a/backend/src/uxbox/services/queries/images.clj +++ b/backend/src/uxbox/services/queries/images.clj @@ -21,38 +21,14 @@ [uxbox.util.uuid :as uuid] [vertx.core :as vc])) -(def +thumbnail-options+ - {:src :path - :dst :thumbnail - :width 300 - :height 100 - :quality 92 - :format "webp"}) - -(defn populate-thumbnail - [row] - (let [opts +thumbnail-options+] - (-> (p/promise row) - (p/then (vc/wrap-blocking #(images/populate-thumbnail % opts)))))) - -(defn populate-thumbnails - [rows] - (if (empty? rows) - rows - (vc/blocking - (mapv (fn [row] - (images/populate-thumbnail row +thumbnail-options+)) rows)))) - -(defn populate-urls - [row] - (images/populate-urls row media/images-storage :path :url)) - (s/def ::id ::us/uuid) (s/def ::name ::us/string) (s/def ::user ::us/uuid) (s/def ::collection-id (s/nilable ::us/uuid)) -(def ^:private images-collections-sql +;; --- Query: Images Collections + +(def ^:private sql:collections "select *, (select count(*) from images where collection_id = ic.id) as num_images from image_collections as ic @@ -66,9 +42,10 @@ (sq/defquery ::images-collections [{:keys [user] :as params}] - (db/query db/pool [images-collections-sql user])) + (db/query db/pool [sql:collections user])) -;; --- Retrieve Image + +;; --- Query: Image by ID (defn retrieve-image [conn id] @@ -84,10 +61,10 @@ (sq/defquery ::image-by-id [params] (-> (retrieve-image db/pool (:id params)) - (p/then populate-thumbnail) - (p/then populate-urls))) + (p/then' #(images/resolve-urls % :path :uri)) + (p/then' #(images/resolve-urls % :thumb-path :thumb-uri)))) -;; --- Query Images by Collection (id) +;; --- Query: Images by collection ID (def sql:images-by-collection "select * from images @@ -96,12 +73,7 @@ and deleted_at is null order by created_at desc") -(def sql:images-by-collection1 - (str "with images as (" sql:images-by-collection ") - select im.* from images as im - where im.collection_id is null")) - -(def sql:images-by-collection2 +(def sql:images-by-collection (str "with images as (" sql:images-by-collection ") select im.* from images as im where im.collection_id = $2")) @@ -110,12 +82,14 @@ (s/keys :req-un [::user] :opt-un [::collection-id])) +;; TODO: check if we can resolve url with transducer for reduce +;; garbage generation for each request + (sq/defquery ::images-by-collection [{:keys [user collection-id] :as params}] - (let [sqlv (if (nil? collection-id) - [sql:images-by-collection1 user] - [sql:images-by-collection2 user collection-id])] + (let [sqlv [sql:images-by-collection user collection-id]] (-> (db/query db/pool sqlv) - (p/then populate-thumbnails) - (p/then #(mapv populate-urls %))))) - + (p/then' (fn [rows] + (->> rows + (mapv #(images/resolve-urls % :path :uri)) + (mapv #(images/resolve-urls % :thumb-path :thumb-uri)))))))) diff --git a/backend/src/uxbox/services/queries/profile.clj b/backend/src/uxbox/services/queries/profile.clj index c8dbdf7e5..4142c1858 100644 --- a/backend/src/uxbox/services/queries/profile.clj +++ b/backend/src/uxbox/services/queries/profile.clj @@ -31,15 +31,15 @@ ;; --- Query: Profile (own) -(defn resolve-thumbnail - [user] - (let [opts {:src :photo - :dst :photo - :size [100 100] - :quality 90 - :format "jpg"}] - (-> (px/submit! #(images/populate-thumbnails user opts)) - (su/handle-on-context)))) +;; (defn resolve-thumbnail +;; [user] +;; (let [opts {:src :photo +;; :dst :photo +;; :size [100 100] +;; :quality 90 +;; :format "jpg"}] +;; (-> (px/submit! #(images/populate-thumbnails user opts)) +;; (su/handle-on-context)))) (defn retrieve-profile [conn id] diff --git a/backend/src/uxbox/services/queries/project_files.clj b/backend/src/uxbox/services/queries/project_files.clj index 24ef0fd3d..f65c9859b 100644 --- a/backend/src/uxbox/services/queries/project_files.clj +++ b/backend/src/uxbox/services/queries/project_files.clj @@ -13,6 +13,7 @@ [promesa.core :as p] [uxbox.common.spec :as us] [uxbox.db :as db] + [uxbox.images :as images] [uxbox.services.queries :as sq] [uxbox.services.util :as su] [uxbox.util.blob :as blob])) @@ -27,24 +28,6 @@ (s/def ::file-id ::us/uuid) (s/def ::user ::us/uuid) -(def sql:generic-project-files - "select distinct on (pf.id, pf.created_at) - pf.*, - p.name as project_name, - array_agg(pp.id) over pages_w as pages, - first_value(pp.data) over pages_w as data - from project_files as pf - inner join projects as p on (pf.project_id = p.id) - inner join project_users as pu on (p.id = pu.project_id) - left join project_pages as pp on (pf.id = pp.file_id) - where pu.user_id = $1 - and pu.can_edit = true - and pf.deleted_at is null - and pp.deleted_at is null - window pages_w as (partition by pf.id order by pp.created_at - range BETWEEN UNBOUNDED PRECEDING - AND UNBOUNDED FOLLOWING)") - ;; --- Query: Project Files (declare retrieve-recent-files) @@ -60,33 +43,77 @@ (retrieve-recent-files db/pool params) (retrieve-project-files db/pool params))) -(def sql:project-files - (str "with files as (" sql:generic-project-files ") - select * from files where project_id = $2 - order by created_at asc")) +(def ^:private sql:generic-project-files + "select distinct + pf.*, + array_agg(pp.id) over pages_w as pages, + first_value(pp.data) over pages_w as data, + p.name as project_name + from project_users as pu + inner join project_files as pf on (pf.project_id = pu.project_id) + inner join projects as p on (p.id = pf.project_id) + left join project_pages as pp on (pf.id = pp.file_id) + where pu.user_id = $1 + and pu.can_edit = true + window pages_w as (partition by pf.id order by pp.created_at + range between unbounded preceding + and unbounded following) + order by pf.created_at") -(def sql:recent-files - (str "with files as (" sql:generic-project-files ") - select * from files - order by modified_at desc - limit $2")) +(def ^:private sql:project-files + (str "with files as (" sql:generic-project-files ") " + "select * from files where project_id = $2")) (defn retrieve-project-files [conn {:keys [user project-id]}] (-> (db/query conn [sql:project-files user project-id]) (p/then' (partial mapv decode-row)))) +(def ^:private sql:recent-files + "with project_files as ( + (select pf.*, + array_agg(pp.id) over pages_w as pages, + first_value(pp.data) over pages_w as data, + p.name as project_name + from project_users as pu + inner join project_files as pf on (pf.project_id = pu.project_id) + inner join projects as p on (p.id = pf.project_id) + left join project_pages as pp on (pf.id = pp.file_id) + where pu.user_id = $1 + and pu.can_edit = true + window pages_w as (partition by pf.id order by pp.created_at + range between unbounded preceding + and unbounded following)) + union + (select pf.*, + array_agg(pp.id) over pages_w as pages, + first_value(pp.data) over pages_w as data, + p.name as project_name + from project_file_users as pfu + inner join project_files as pf on (pfu.file_id = pf.id) + inner join projects as p on (p.id = pf.project_id) + left join project_pages as pp on (pf.id = pp.file_id) + where pfu.user_id = $1 + and pfu.can_edit = true + window pages_w as (partition by pf.id order by pp.created_at + range between unbounded preceding + and unbounded following)) + ) select pf1.* + from project_files as pf1 + order by pf1.modified_at desc + limit $2;") + + (defn retrieve-recent-files [conn {:keys [user]}] (-> (db/query conn [sql:recent-files user 20]) (p/then' (partial mapv decode-row)))) - ;; --- Query: Project File (By ID) -(def sql:project-file - (str "with files as (" sql:generic-project-files ") - select * from files where id = $2")) +(def ^:private sql:project-file + (str "with files as (" sql:generic-project-files ") " + "select * from files where id = $2")) (s/def ::project-file (s/keys :req-un [::user ::id])) @@ -96,36 +123,10 @@ (-> (db/query-one db/pool [sql:project-file user id]) (p/then' decode-row))) - ;; --- Query: Users of the File -(def sql:file-users - "select u.id, u.fullname, u.photo - from users as u - join project_file_users as pfu on (pfu.user_id = u.id) - where pfu.file_id = $1 - union all - select u.id, u.fullname, u.photo - from users as u - join project_users as pu on (pu.user_id = u.id) - where pu.project_id = $2") - -(def sql:file-users - "select u.id, u.fullname, u.photo - from users as u - join project_file_users as pfu on (pfu.user_id = u.id) - where pfu.file_id = $1 - union all - select u.id, u.fullname, u.photo - from users as u - join project_users as pu on (pu.user_id = u.id) - where pu.project_id = $2") - (declare retrieve-minimal-file) - -(def sql:minimal-file - (str "with files as (" sql:generic-project-files ") - select id, project_id from files where id = $2")) +(declare retrieve-file-users) (s/def ::project-file-users (s/keys :req-un [::user ::file-id])) @@ -134,20 +135,65 @@ [{:keys [user file-id] :as params}] (db/with-atomic [conn db/pool] (-> (retrieve-minimal-file conn user file-id) - (p/then (fn [{:keys [id project-id]}] - (db/query conn [sql:file-users id project-id])))))) + (p/then #(retrieve-file-users conn %))))) + +(def ^:private sql:minimal-file + (str "with files as (" sql:generic-project-files ") " + "select id, project_id from files where id = $2")) (defn- retrieve-minimal-file [conn user-id file-id] (-> (db/query-one conn [sql:minimal-file user-id file-id]) (p/then' su/raise-not-found-if-nil))) +(def ^:private sql:file-users + "select u.id, u.fullname, u.photo + from users as u + join project_file_users as pfu on (pfu.user_id = u.id) + where pfu.file_id = $1 + union all + select u.id, u.fullname, u.photo + from users as u + join project_users as pu on (pu.user_id = u.id) + where pu.project_id = $2") + +(defn- retrieve-file-users + [conn {:keys [id project-id] :as file}] + (let [sqlv [sql:file-users id project-id]] + (db/query conn sqlv))) + + +;; --- Query: Images of the File + +(declare retrieve-file-images) + +(s/def ::project-file-images + (s/keys :req-un [::user ::file-id])) + +(sq/defquery ::project-file-images + [{:keys [user file-id] :as params}] + (db/with-atomic [conn db/pool] + (-> (retrieve-minimal-file conn user file-id) + (p/then #(retrieve-file-images conn %))))) + +(def ^:private sql:file-images + "select pfi.* + from project_file_images as pfi + where pfi.file_id = $1") + +(defn retrieve-file-images + [conn {:keys [id] :as file}] + (let [sqlv [sql:file-images id] + xf (comp (map #(images/resolve-urls % :path :uri)) + (map #(images/resolve-urls % :thumb-path :thumb-uri)))] + (-> (db/query conn sqlv) + (p/then' #(into [] xf %))))) + ;; --- Helpers (defn decode-row - [{:keys [metadata pages data] :as row}] + [{:keys [pages data] :as row}] (when row (cond-> row data (assoc :data (blob/decode data)) - pages (assoc :pages (vec (remove nil? pages))) - metadata (assoc :metadata (blob/decode metadata))))) + pages (assoc :pages (vec (remove nil? pages)))))) diff --git a/backend/src/uxbox/util/storage.clj b/backend/src/uxbox/util/storage.clj index 97e3164fc..58808ff36 100644 --- a/backend/src/uxbox/util/storage.clj +++ b/backend/src/uxbox/util/storage.clj @@ -164,20 +164,31 @@ (doto (java.security.SecureRandom/getInstance "SHA1PRNG") (.setSeed ^bytes (sodi.prng/random-bytes 64))))) -(defn random-path - [^Path path] - (let [name (str (.getFileName path)) - hash (-> (sodi.prng/random-bytes @prng 10) - (sodi.util/bytes->b64s)) - tokens (re-seq #"[\w\d\-\_]{2}" hash) - path-tokens (take 3 tokens) - rest-tokens (drop 3 tokens) - path (fs/path path-tokens) - frest (apply str rest-tokens)] - (fs/path (list path frest name)))) +(defn with-xf + [storage xfm] + (let [xf (::xf storage)] + (if (nil? xf) + (assoc storage ::xf xfm) + (assoc storage ::xf (comp xf xfm))))) -(defn slugify-filename - [path] - (let [parent (or (fs/parent path) "") - [name ext] (fs/split-ext (fs/name path))] - (fs/path parent (str (str/uslug name) ext)))) +(def random-path + (map (fn [^Path path] + (let [name (str (.getFileName path)) + hash (-> (sodi.prng/random-bytes @prng 10) + (sodi.util/bytes->b64s)) + tokens (re-seq #"[\w\d\-\_]{2}" hash) + path-tokens (take 3 tokens) + rest-tokens (drop 3 tokens) + path (fs/path path-tokens) + frest (apply str rest-tokens)] + (fs/path (list path frest name)))))) + +(def slugify-filename + (map (fn [path] + (let [parent (or (fs/parent path) "") + [name ext] (fs/split-ext (fs/name path))] + (fs/path parent (str (str/uslug name) ext)))))) + +(defn prefix-path + [prefix] + (map (fn [^Path path] (fs/join (fs/path prefix) path)))) diff --git a/backend/tests.edn b/backend/tests.edn index bb837d2b7..5b277459d 100644 --- a/backend/tests.edn +++ b/backend/tests.edn @@ -1,5 +1,5 @@ #kaocha/v1 {:tests [{:id :unit - :test-paths ["test" "src"] + :test-paths ["tests" "src"] :ns-patterns ["test-.*"]}]} diff --git a/backend/tests/user.clj b/backend/tests/user.clj index 617867e15..0f03107c1 100644 --- a/backend/tests/user.clj +++ b/backend/tests/user.clj @@ -19,6 +19,8 @@ [criterium.core :refer [quick-bench bench with-progress-reporting]] [promesa.core :as p] [promesa.exec :as pe] + [uxbox.migrations] + [uxbox.util.storage :as st] [mount.core :as mount])) ;; --- Benchmarking Tools @@ -58,7 +60,7 @@ (defn- run-tests ([] (run-tests #"^uxbox.tests.*")) ([o] - ;; (repl/refresh) + (repl/refresh) (cond (instance? java.util.regex.Pattern o) (test/run-all-tests o) diff --git a/backend/tests/uxbox/tests/helpers.clj b/backend/tests/uxbox/tests/helpers.clj index d2ad5d5a1..49c46bada 100644 --- a/backend/tests/uxbox/tests/helpers.clj +++ b/backend/tests/uxbox/tests/helpers.clj @@ -5,17 +5,18 @@ [cuerdas.core :as str] [mount.core :as mount] [environ.core :refer [env]] - [datoteka.storages :as st] [uxbox.services.mutations.profile :as profile] [uxbox.services.mutations.projects :as projects] [uxbox.services.mutations.project-files :as files] [uxbox.services.mutations.project-pages :as pages] + [uxbox.services.mutations.images :as images] [uxbox.fixtures :as fixtures] [uxbox.migrations] [uxbox.media] [uxbox.db :as db] [uxbox.util.blob :as blob] [uxbox.util.uuid :as uuid] + [uxbox.util.storage :as ust] [uxbox.config :as cfg])) (defn state-init @@ -28,9 +29,7 @@ #'uxbox.services.init/mutation-services #'uxbox.migrations/migrations #'uxbox.media/assets-storage - #'uxbox.media/media-storage - #'uxbox.media/images-storage - #'uxbox.media/thumbnails-storage}) + #'uxbox.media/media-storage}) (mount/swap {#'uxbox.config/config config}) (mount/start)) (try @@ -55,8 +54,8 @@ (try (next) (finally - (st/clear! uxbox.media/media-storage) - (st/clear! uxbox.media/assets-storage)))) + (ust/clear! uxbox.media/media-storage) + (ust/clear! uxbox.media/assets-storage)))) (defn mk-uuid [prefix & args] @@ -96,10 +95,17 @@ :file-id file-id :name (str "page" i) :ordering i - :data {:shapes [] + :data {:version 1 + :shapes [] + :options {} :canvas [] - :shapes-by-id {}} - :metadata {}})) + :shapes-by-id {}}})) + +(defn create-images-collection + [conn user-id i] + (images/create-images-collection conn {:id (mk-uuid "imgcoll" i) + :user user-id + :name (str "image collection " i)})) (defn handle-error [err] diff --git a/backend/tests/uxbox/tests/test_images.clj b/backend/tests/uxbox/tests/test_images.clj index c98b9f8a2..f465cc984 100644 --- a/backend/tests/uxbox/tests/test_images.clj +++ b/backend/tests/uxbox/tests/test_images.clj @@ -1,176 +1,158 @@ (ns uxbox.tests.test-images - #_(:require [clojure.test :as t] - [promesa.core :as p] - [suricatta.core :as sc] - [clojure.java.io :as io] - [datoteka.storages :as st] - [uxbox.db :as db] - [uxbox.sql :as sql] - [uxbox.media :as media] - [uxbox.http :as http] - [uxbox.services.images :as images] - [uxbox.services :as usv] - [uxbox.tests.helpers :as th])) + (:require + [clojure.test :as t] + [promesa.core :as p] + [datoteka.core :as fs] + [clojure.java.io :as io] + [uxbox.db :as db] + [uxbox.core :refer [system]] + [uxbox.services.mutations :as sm] + [uxbox.services.queries :as sq] + [uxbox.util.storage :as ust] + [uxbox.util.uuid :as uuid] + [uxbox.tests.helpers :as th] + [vertx.core :as vc])) -;; (t/use-fixtures :once th/state-init) -;; (t/use-fixtures :each th/database-reset) +(t/use-fixtures :once th/state-init) +(t/use-fixtures :each th/database-reset) -;; (t/deftest test-http-list-image-collections -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "coll1"} -;; coll (images/create-collection conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/image-collections") -;; [status data] (th/http-get user uri)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 200 status)) -;; (t/is (= 1 (count data)))))))) +(t/deftest images-collections-crud + (let [id (uuid/next) + user @(th/create-user db/pool 2)] -;; (t/deftest test-http-create-image-collection -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/image-collections") -;; data {:user (:id user) -;; :name "coll1"} -;; params {:body data} -;; [status data] (th/http-post user uri params)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 201 status)) -;; (t/is (= (:user data) (:id user))) -;; (t/is (= (:name data) "coll1"))))))) + (t/testing "create collection" + (let [data {::sm/type :create-images-collection + :name "sample collection" + :user (:id user) + :id id} + out (th/try-on! (sm/handle data))] -;; (t/deftest test-http-update-image-collection -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "coll1"} -;; coll (images/create-collection conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/image-collections/" (:id coll)) -;; params {:body (assoc coll :name "coll2")} -;; [status data] (th/http-put user uri params)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 200 status)) -;; (t/is (= (:user data) (:id user))) -;; (t/is (= (:name data) "coll2"))))))) + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= (:id user) (get-in out [:result :user-id]))) + (t/is (= (:name data) (get-in out [:result :name]))))) -;; (t/deftest test-http-image-collection-delete -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "coll1" -;; :data #{1}} -;; coll (images/create-collection conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/image-collections/" (:id coll)) -;; [status data] (th/http-delete user uri)] -;; (t/is (= 204 status)) -;; (let [sqlv (sql/get-image-collections {:user (:id user)}) -;; result (sc/fetch conn sqlv)] -;; (t/is (empty? result)))))))) + (t/testing "update collection" + (let [data {::sm/type :rename-images-collection + :name "sample collection renamed" + :user (:id user) + :id id} + out (th/try-on! (sm/handle data))] -;; (t/deftest test-http-create-image -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/images") -;; parts [{:name "sample.jpg" -;; :part-name "file" -;; :content (io/input-stream -;; (io/resource "uxbox/tests/_files/sample.jpg"))} -;; {:part-name "user" :content (str (:id user))} -;; {:part-name "width" :content "100"} -;; {:part-name "height" :content "100"} -;; {:part-name "mimetype" :content "image/png"}] -;; [status data] (th/http-multipart user uri parts)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 201 status)) -;; (t/is (= (:user data) (:id user))) -;; (t/is (= (:name data) "sample.jpg"))))))) + ;; (th/print-result! out) + (t/is (nil? (:error out))) -;; (t/deftest test-http-update-image -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "test.png" -;; :path "some/path" -;; :width 100 -;; :height 100 -;; :mimetype "image/png" -;; :collection nil} -;; img (images/create-image conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/images/" (:id img)) -;; params {:body (assoc img :name "my stuff")} -;; [status data] (th/http-put user uri params)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 200 status)) -;; (t/is (= (:user data) (:id user))) -;; (t/is (= (:name data) "my stuff"))))))) + (t/is (= id (get-in out [:result :id]))) + (t/is (= (:id user) (get-in out [:result :user-id]))) + (t/is (= (:name data) (get-in out [:result :name]))))) -;; (t/deftest test-http-copy-image -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; storage media/images-storage -;; filename "sample.jpg" -;; rcs (io/resource "uxbox/tests/_files/sample.jpg") -;; path @(st/save storage filename rcs) -;; data {:user (:id user) -;; :name filename -;; :path (str path) -;; :width 100 -;; :height 100 -;; :mimetype "image/jpg" -;; :collection nil} -;; img (images/create-image conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/images/" (:id img) "/copy") -;; body {:id (:id img) -;; :collection nil} -;; params {:body body} -;; [status data] (th/http-put user uri params)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 200 status)) -;; (let [sqlv (sql/get-images {:user (:id user) :collection nil}) -;; result (sc/fetch conn sqlv)] -;; (t/is (= 2 (count result))))))))) + (t/testing "query collections" + (let [data {::sq/type :images-collections + :user (:id user)} + out (th/try-on! (sq/handle data))] -;; (t/deftest test-http-delete-image -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "test.png" -;; :path "some/path" -;; :width 100 -;; :height 100 -;; :mimetype "image/png" -;; :collection nil} -;; img (images/create-image conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/images/" (:id img)) -;; [status data] (th/http-delete user uri)] -;; (t/is (= 204 status)) -;; (let [sqlv (sql/get-images {:user (:id user) :collection nil}) -;; result (sc/fetch conn sqlv)] -;; (t/is (empty? result)))))))) + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (t/is (= 1 (count (:result out)))) + (t/is (= (:id user) (get-in out [:result 0 :user-id]))) + (t/is (= id (get-in out [:result 0 :id]))))) + + (t/testing "delete collection" + (let [data {::sm/type :delete-images-collection + :user (:id user) + :id id} + + out (th/try-on! (sm/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= id (get-in out [:result :id]))))) + + (t/testing "query collections after delete" + (let [data {::sq/type :images-collections + :user (:id user)} + out (th/try-on! (sq/handle data))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + (t/is (= 0 (count (:result out)))))) + )) + +(t/deftest images-crud + (let [user @(th/create-user db/pool 1) + coll @(th/create-images-collection db/pool (:id user) 1) + image-id (uuid/next)] + + (t/testing "upload image to collection" + (let [content {:name "sample.jpg" + :path "tests/uxbox/tests/_files/sample.jpg" + :mtype "image/jpeg" + :size 312043} + data {::sm/type :upload-image + :id image-id + :user (:id user) + :collection-id (:id coll) + :name "testfile" + :content content} + out (th/try-on! (sm/handle data))] + ;; out (with-redefs [vc/*context* (vc/get-or-create-context system)] + ;; (th/try-on! (sm/handle data)))] + + ;; (th/print-result! out) + (t/is (nil? (:error out))) + + (t/is (= image-id (get-in out [:result :id]))) + (t/is (= "testfile" (get-in out [:result :name]))) + (t/is (= "image/jpeg" (get-in out [:result :mtype]))) + (t/is (= "image/webp" (get-in out [:result :thumb-mtype]))) + (t/is (= 800 (get-in out [:result :width]))) + (t/is (= 800 (get-in out [:result :height]))) + + (t/is (string? (get-in out [:result :path]))) + (t/is (string? (get-in out [:result :thumb-path]))) + (t/is (string? (get-in out [:result :uri]))) + (t/is (string? (get-in out [:result :thumb-uri]))))) + + + (t/testing "list images by collection" + (let [data {::sq/type :images-by-collection + :user (:id user) + :collection-id (:id coll)} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + + (t/is (= image-id (get-in out [:result 0 :id]))) + (t/is (= "testfile" (get-in out [:result 0 :name]))) + (t/is (= "image/jpeg" (get-in out [:result 0 :mtype]))) + (t/is (= "image/webp" (get-in out [:result 0 :thumb-mtype]))) + (t/is (= 800 (get-in out [:result 0 :width]))) + (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 :thumb-path]))) + (t/is (string? (get-in out [:result 0 :uri]))) + (t/is (string? (get-in out [:result 0 :thumb-uri]))))) + + (t/testing "get image by id" + (let [data {::sq/type :image-by-id + :user (:id user) + :id image-id} + out (th/try-on! (sq/handle data))] + ;; (th/print-result! out) + + (t/is (= image-id (get-in out [:result :id]))) + (t/is (= "testfile" (get-in out [:result :name]))) + (t/is (= "image/jpeg" (get-in out [:result :mtype]))) + (t/is (= "image/webp" (get-in out [:result :thumb-mtype]))) + (t/is (= 800 (get-in out [:result :width]))) + (t/is (= 800 (get-in out [:result :height]))) + + (t/is (string? (get-in out [:result :path]))) + (t/is (string? (get-in out [:result :thumb-path]))) + (t/is (string? (get-in out [:result :uri]))) + (t/is (string? (get-in out [:result :thumb-uri]))))) + )) + +;; TODO: (soft) delete image -;; (t/deftest test-http-list-images -;; (with-open [conn (db/connection)] -;; (let [user (th/create-user conn 1) -;; data {:user (:id user) -;; :name "test.png" -;; :path "some/path" -;; :width 100 -;; :height 100 -;; :mimetype "image/png" -;; :collection nil} -;; img (images/create-image conn data)] -;; (th/with-server {:handler @http/app} -;; (let [uri (str th/+base-url+ "/api/library/images") -;; [status data] (th/http-get user uri)] -;; ;; (println "RESPONSE:" status data) -;; (t/is (= 200 status)) -;; (t/is (= 1 (count data)))))))) diff --git a/backend/tests/uxbox/tests/test_services_auth.clj b/backend/tests/uxbox/tests/test_services_auth.clj index 35ac36b80..660f799c5 100644 --- a/backend/tests/uxbox/tests/test_services_auth.clj +++ b/backend/tests/uxbox/tests/test_services_auth.clj @@ -31,7 +31,7 @@ (let [error (ex-cause (:error out))] (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :validation)) - (t/is (th/ex-of-code? error :uxbox.services.mutations.auth/wrong-credentials))))) + (t/is (th/ex-of-code? error :uxbox.services.mutations.profile/wrong-credentials))))) (t/deftest success-auth (let [user @(th/create-user db/pool 1) diff --git a/backend/tests/uxbox/tests/test_services_project_files.clj b/backend/tests/uxbox/tests/test_services_project_files.clj index a06a1388d..a5afdcf95 100644 --- a/backend/tests/uxbox/tests/test_services_project_files.clj +++ b/backend/tests/uxbox/tests/test_services_project_files.clj @@ -2,11 +2,17 @@ (:require [clojure.test :as t] [promesa.core :as p] + [datoteka.core :as fs] [uxbox.db :as db] + [uxbox.media :as media] + [uxbox.core :refer [system]] [uxbox.http :as http] [uxbox.services.mutations :as sm] [uxbox.services.queries :as sq] - [uxbox.tests.helpers :as th])) + [uxbox.tests.helpers :as th] + [uxbox.util.storage :as ust] + [uxbox.util.uuid :as uuid] + [vertx.core :as vc])) (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) @@ -40,7 +46,7 @@ ;; (th/print-result! out) (t/is (nil? (:error out))) (t/is (= (:name data) (get-in out [:result :name]))) - #_(t/is (= (:project-id data) (get-in out [:result :project-id]))))) + (t/is (= (:project-id data) (get-in out [:result :project-id]))))) (t/deftest mutation-rename-project-file (let [user @(th/create-user db/pool 1) @@ -73,4 +79,78 @@ res @(db/query db/pool [sql (:id proj)])] (t/is (empty? res))))) -;; ;; TODO: add permisions related tests +(t/deftest mutation-upload-file-image + (let [user @(th/create-user db/pool 1) + proj @(th/create-project db/pool (:id user) 1) + pf @(th/create-project-file db/pool (:id user) (:id proj) 1) + + content {:name "sample.jpg" + :path "tests/uxbox/tests/_files/sample.jpg" + :mtype "image/jpeg" + :size 312043} + data {::sm/type :upload-project-file-image + :user (:id user) + :file-id (:id pf) + :name "testfile" + :content content + :width 800 + :height 800} + + out (with-redefs [vc/*context* (vc/get-or-create-context system)] + (th/try-on! (sm/handle data)))] + + ;; (th/print-result! out) + + (t/is (= (:id pf) (get-in out [:result :file-id]))) + (t/is (= (:name data) (get-in out [:result :name]))) + (t/is (= (:width data) (get-in out [:result :width]))) + (t/is (= (:height data) (get-in out [:result :height]))) + (t/is (= (:mimetype data) (get-in out [:result :mimetype]))) + + (t/is (string? (get-in out [:result :path]))) + (t/is (string? (get-in out [:result :thumb-path]))) + (t/is (string? (get-in out [:result :uri]))) + (t/is (string? (get-in out [:result :thumb-uri]))))) + +(t/deftest mutation-import-image-file-from-collection + (let [user @(th/create-user db/pool 1) + proj @(th/create-project db/pool (:id user) 1) + pf @(th/create-project-file db/pool (:id user) (:id proj) 1) + coll @(th/create-images-collection db/pool (:id user) 1) + image-id (uuid/next) + + content {:name "sample.jpg" + :path "tests/uxbox/tests/_files/sample.jpg" + :mtype "image/jpeg" + :size 312043} + + data {::sm/type :upload-image + :id image-id + :user (:id user) + :collection-id (:id coll) + :name "testfile" + :content content} + out1 (th/try-on! (sm/handle data))] + + ;; (th/print-result! out1) + (t/is (nil? (:error out1))) + (t/is (= image-id (get-in out1 [:result :id]))) + (t/is (= "testfile" (get-in out1 [:result :name]))) + (t/is (= "image/jpeg" (get-in out1 [:result :mtype]))) + (t/is (= "image/webp" (get-in out1 [:result :thumb-mtype]))) + + (let [data2 {::sm/type :import-image-to-file + :image-id image-id + :file-id (:id pf) + :user (:id user)} + out2 (th/try-on! (sm/handle data2))] + + ;; (th/print-result! out2) + (t/is (nil? (:error out2))) + (t/is (not= (get-in out2 [:result :path]) + (get-in out1 [:result :path]))) + (t/is (not= (get-in out2 [:result :thumb-path]) + (get-in out1 [:result :thumb-path])))))) + + + diff --git a/backend/tests/uxbox/tests/test_services_project_pages.clj b/backend/tests/uxbox/tests/test_services_project_pages.clj index 06bff02a7..cc0e3c2d9 100644 --- a/backend/tests/uxbox/tests/test_services_project_pages.clj +++ b/backend/tests/uxbox/tests/test_services_project_pages.clj @@ -36,9 +36,9 @@ data {::sm/type :create-project-page :data {:canvas [] + :options {} :shapes [] :shapes-by-id {}} - :metadata {} :file-id (:id pf) :ordering 1 :name "test page" @@ -50,7 +50,6 @@ (t/is (= (:user data) (get-in out [:result :user-id]))) (t/is (= (:name data) (get-in out [:result :name]))) (t/is (= (:data data) (get-in out [:result :data]))) - (t/is (= (:metadata data) (get-in out [:result :metadata]))) (t/is (= 0 (get-in out [:result :version]))))) (t/deftest mutation-update-project-page-data @@ -61,6 +60,7 @@ data {::sm/type :update-project-page-data :id (:id page) :data {:shapes [(uuid/next)] + :options {} :canvas [] :shapes-by-id {}} :file-id (:id file) @@ -85,7 +85,7 @@ :id (:id page) :version 99 :user (:id user) - :operations []} + :changes []} out (th/try-on! (sm/handle data))] @@ -111,18 +111,21 @@ :id (:id page) :version 0 :user (:id user) - :operations [[:add-shape sid {:id sid :type :rect}]]} + :changes [{:type :add-shape + :id sid + :session-id (uuid/next) + :shape {:id sid + :name "Rect" + :type :rect}}]} out (th/try-on! (sm/handle data))] ;; (th/print-result! out) (t/is (nil? (:error out))) - (t/is (= 0 (count (:result out)))) - ;; (t/is (= 1 (count (:result out)))) - ;; (t/is (= (:id data) (get-in out [:result 0 :page-id]))) - ;; (t/is (= 1 (count (get-in out [:result 0 :operations])))) - ;; (t/is (= :add-shape (get-in out [:result 0 :operations 0 0]))) - ;; (t/is (= sid (get-in out [:result 0 :operations 0 1]))) + + (t/is (= 1 (get-in out [:result :version]))) + (t/is (= (:id page) (get-in out [:result :page-id]))) + (t/is (= :add-shape (get-in out [:result :changes 0 :type]))) )) (t/deftest mutation-update-project-page-3 @@ -132,11 +135,17 @@ page @(th/create-project-page db/pool (:id user) (:id file) 1) sid (uuid/next) + data {::sm/type :update-project-page :id (:id page) :version 0 :user (:id user) - :operations [[:add-shape sid {:id sid :type :rect}]]} + :changes [{:type :add-shape + :id sid + :session-id (uuid/next) + :shape {:id sid + :name "Rect" + :type :rect}}]} out1 (th/try-on! (sm/handle data)) out2 (th/try-on! (sm/handle data))] @@ -146,12 +155,12 @@ (t/is (nil? (:error out1))) (t/is (nil? (:error out2))) - (t/is (= 0 (count (:result out1)))) - (t/is (= 1 (count (:result out2)))) - (t/is (= (:id data) (get-in out2 [:result 0 :page-id]))) - (t/is (= 1 (count (get-in out2 [:result 0 :operations])))) - (t/is (= :add-shape (get-in out2 [:result 0 :operations 0 0]))) - (t/is (= sid (get-in out2 [:result 0 :operations 0 1]))) + + (t/is (= 1 (count (get-in out1 [:result :changes])))) + (t/is (= 2 (count (get-in out2 [:result :changes])))) + + (t/is (= (:id data) (get-in out1 [:result :page-id]))) + (t/is (= (:id data) (get-in out2 [:result :page-id]))) )) (t/deftest mutation-delete-project-page diff --git a/backend/tests/uxbox/tests/test_services_users.clj b/backend/tests/uxbox/tests/test_services_users.clj index ab64fb38d..aefff8d12 100644 --- a/backend/tests/uxbox/tests/test_services_users.clj +++ b/backend/tests/uxbox/tests/test_services_users.clj @@ -32,8 +32,7 @@ ::sm/type :update-profile :fullname "Full Name" :username "user222" - :metadata {:foo "bar"} - :email "user222@uxbox.io") + :lang "en") out (th/try-on! (sm/handle data))] ;; (th/print-result! out) (t/is (nil? (:error out))) @@ -43,20 +42,20 @@ (t/is (= (:metadata data) (get-in out [:result :metadata]))) (t/is (not (contains? (:result out) :password))))) -(t/deftest test-mutation-update-profile-photo - (let [user @(th/create-user db/pool 1) - data {::sm/type :update-profile-photo - :user (:id user) - :file {:name "sample.jpg" - :path (fs/path "test/uxbox/tests/_files/sample.jpg") - :size 123123 - :mtype "image/jpeg"}} +;; (t/deftest test-mutation-update-profile-photo +;; (let [user @(th/create-user db/pool 1) +;; data {::sm/type :update-profile-photo +;; :user (:id user) +;; :file {:name "sample.jpg" +;; :path (fs/path "test/uxbox/tests/_files/sample.jpg") +;; :size 123123 +;; :mtype "image/jpeg"}} - out (th/try-on! (sm/handle data))] - ;; (th/print-result! out) - (t/is (nil? (:error out))) - (t/is (= (:id user) (get-in out [:result :id]))) - (t/is (str/starts-with? (get-in out [:result :photo]) "http")))) +;; out (th/try-on! (sm/handle data))] +;; ;; (th/print-result! out) +;; (t/is (nil? (:error out))) +;; (t/is (= (:id user) (get-in out [:result :id]))) +;; (t/is (str/starts-with? (get-in out [:result :photo]) "http")))) ;; (t/deftest test-mutation-register-profile ;; (let[data {:fullname "Full Name" diff --git a/frontend/src/uxbox/main/data/icons.cljs b/frontend/src/uxbox/main/data/icons.cljs index bba1c27ae..6c3ccc60d 100644 --- a/frontend/src/uxbox/main/data/icons.cljs +++ b/frontend/src/uxbox/main/data/icons.cljs @@ -10,12 +10,13 @@ [beicon.core :as rx] [cuerdas.core :as str] [potok.core :as ptk] + [uxbox.common.spec :as us] + [uxbox.common.data :as d] [uxbox.main.repo :as rp] [uxbox.main.store :as st] - [uxbox.util.data :refer (jscoll->vec)] [uxbox.util.dom :as dom] - [uxbox.util.files :as files] - [uxbox.util.i18n :refer [tr]] + [uxbox.util.webapi :as wapi] + [uxbox.util.i18n :as i18n :refer [t tr]] [uxbox.util.router :as r] [uxbox.util.uuid :as uuid])) @@ -24,14 +25,7 @@ (s/def ::created-at inst?) (s/def ::modified-at inst?) (s/def ::user-id uuid?) - -;; (s/def ::collection-id (s/nilable ::us/uuid)) - -;; (s/def ::mimetype string?) -;; (s/def ::thumbnail us/url-str?) -;; (s/def ::width number?) -;; (s/def ::height number?) -;; (s/def ::url us/url-str?) +(s/def ::collection-id ::us/uuid) (s/def ::collection (s/keys :req-un [::id @@ -40,6 +34,32 @@ ::modified-at ::user-id])) + +(declare fetch-icons) + +(defn initialize + [collection-id] + (s/assert ::us/uuid collection-id) + (ptk/reify ::initialize + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:dashboard-icons :selected] #{})) + + ptk/WatchEvent + (watch [_ state stream] + (rx/of (fetch-icons collection-id))))) + +;; --- Fetch Collections + +(declare collections-fetched) + +(def fetch-collections + (ptk/reify ::fetch-collections + ptk/WatchEvent + (watch [_ state s] + (->> (rp/query! :icons-collections) + (rx/map collections-fetched))))) + ;; --- Collections Fetched (defn collections-fetched @@ -58,14 +78,20 @@ state items)))) -;; --- Fetch Collections -(def fetch-collections - (ptk/reify ::fetch-collections +;; --- Create Collection + +(declare collection-created) + +(def create-collection + (ptk/reify ::create-collection ptk/WatchEvent (watch [_ state s] - (->> (rp/query! :icons-collections) - (rx/map collections-fetched))))) + (let [name (tr "ds.default-library-title" (gensym "c")) + data {:name name}] + (->> (rp/mutation! :create-icons-collection data) + (rx/map collection-created)))))) + ;; --- Collection Created @@ -78,70 +104,35 @@ (let [{:keys [id] :as item} (assoc item :type :own)] (update state :icons-collections assoc id item))))) -;; --- Create Collection - -(def create-collection - (ptk/reify ::create-collection - ptk/WatchEvent - (watch [_ state s] - (let [name (tr "ds.default-library-title" (gensym "c")) - data {:name name}] - (->> (rp/mutation! :create-icons-collection data) - (rx/map collection-created)))))) - -;; --- Collection Updated - -(defn collection-updated - [item] - (ptk/reify ::collection-updated - ptk/UpdateEvent - (update [_ state] - (update-in state [:icons-collections (:id item)] merge item)))) - -;; --- Update Collection - -(defrecord UpdateCollection [id] - ptk/WatchEvent - (watch [_ state s] - (let [data (get-in state [:icons-collections id])] - (->> (rp/mutation! :update-icons-collection data) - (rx/map collection-updated))))) - -(defn update-collection - [id] - (UpdateCollection. id)) - ;; --- Rename Collection -(defrecord RenameCollection [id name] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:icons-collections id :name] name)) - - ptk/WatchEvent - (watch [_ state s] - (rx/of (update-collection id)))) - (defn rename-collection [id name] - (RenameCollection. id name)) + (ptk/reify ::rename-collection + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:icons-collections id :name] name)) + + ptk/WatchEvent + (watch [_ state s] + (let [params {:id id :name name}] + (->> (rp/mutation! :rename-icons-collection params) + (rx/ignore)))))) ;; --- Delete Collection -(defrecord DeleteCollection [id] - ptk/UpdateEvent - (update [_ state] - (update state :icons-collections dissoc id)) - - ptk/WatchEvent - (watch [_ state s] - (let [type (get-in state [:dashboard :icons :type])] - (->> (rp/mutation! :delete-icons-collection {:id id}) - (rx/map #(r/nav :dashboard-icons {:type type})))))) - (defn delete-collection - [id] - (DeleteCollection. id)) + [id on-success] + (ptk/reify ::delete-collection + ptk/UpdateEvent + (update [_ state] + (update state :icons-collections dissoc id)) + + ptk/WatchEvent + (watch [_ state s] + (->> (rp/mutation! :delete-icons-collection {:id id}) + (rx/tap on-success) + (rx/ignore))))) ;; --- Icon Created @@ -157,9 +148,11 @@ ;; --- Create Icon +(declare icon-created) + (defn- parse-svg [data] - {:pre [(string? data)]} + (s/assert ::us/string data) (let [valid-tags #{"defs" "path" "circle" "rect" "metadata" "g" "radialGradient" "stop"} div (dom/create-element "div") @@ -194,7 +187,7 @@ ptk/WatchEvent (watch [_ state s] (letfn [(parse [file] - (->> (files/read-as-text file) + (->> (wapi/read-file-as-text file) (rx/map parse-svg))) (allowed? [file] (= (.-type file) "image/svg+xml")) @@ -207,7 +200,7 @@ :metadata metadata})] (->> (rx/from files) (rx/filter allowed?) - (rx/flat-map parse) + (rx/merge-map parse) (rx/map prepare) (rx/flat-map #(rp/mutation! :create-icon %)) (rx/map icon-created)))))) @@ -226,184 +219,158 @@ ;; --- Persist Icon -(defrecord PersistIcon [id] - ptk/WatchEvent - (watch [_ state stream] - (let [data (get-in state [:icons id])] - (->> (rp/mutation! :update-icon data) - (rx/map icon-persisted))))) - (defn persist-icon [id] - {:pre [(uuid? id)]} - (PersistIcon. id)) - -;; --- Icons Fetched - -(defrecord IconsFetched [items] - ptk/UpdateEvent - (update [_ state] - (reduce (fn [state {:keys [id] :as icon}] - (let [icon (assoc icon :type :icon)] - (assoc-in state [:icons id] icon))) - state - items))) - -(defn icons-fetched - [items] - (IconsFetched. items)) + (s/assert ::us/uuid id) + (ptk/reify ::persist-icon + ptk/WatchEvent + (watch [_ state stream] + (let [data (get-in state [:icons id])] + (->> (rp/mutation! :update-icon data) + (rx/ignore)))))) ;; --- Load Icons -(defrecord FetchIcons [id] - ptk/WatchEvent - (watch [_ state s] - (let [params (cond-> {} id (assoc :collection-id id))] - (->> (rp/query! :icons-by-collection params) - (rx/map icons-fetched))))) +(declare icons-fetched) (defn fetch-icons [id] - {:pre [(or (uuid? id) (nil? id))]} - (FetchIcons. id)) + (ptk/reify ::fetch-icons + ptk/WatchEvent + (watch [_ state s] + (let [params (cond-> {} id (assoc :collection-id id))] + (->> (rp/query! :icons-by-collection params) + (rx/map icons-fetched)))))) -;; --- Delete Icons +;; --- Icons Fetched -(defrecord DeleteIcon [id] - ptk/UpdateEvent - (update [_ state] - (-> state - (update :icons dissoc id) - (update-in [:dashboard :icons :selected] disj id))) - - ptk/WatchEvent - (watch [_ state s] - (->> (rp/mutation! :delete-icon {:id id}) - (rx/ignore)))) - -(defn delete-icon - [id] - {:pre [(uuid? id)]} - (DeleteIcon. id)) +(defn icons-fetched + [items] + ;; TODO: specs + (ptk/reify ::icons-fetched + ptk/UpdateEvent + (update [_ state] + (let [icons (d/index-by :id items)] + (assoc state :icons icons))))) ;; --- Rename Icon -(defrecord RenameIcon [id name] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:icons id :name] name)) - - ptk/WatchEvent - (watch [_ state stream] - (rx/of (persist-icon id)))) - (defn rename-icon [id name] - {:pre [(uuid? id) (string? name)]} - (RenameIcon. id name)) + (s/assert ::us/uuid id) + (s/assert ::us/string name) + (ptk/reify ::rename-icon + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:icons id :name] name)) -;; --- Select icon + ptk/WatchEvent + (watch [_ state stream] + (rx/of (persist-icon id))))) -(defrecord SelectIcon [id] - ptk/UpdateEvent - (update [_ state] - (update-in state [:dashboard :icons :selected] conj id))) +;; --- Icon Selection -(defrecord DeselectIcon [id] - ptk/UpdateEvent - (update [_ state] - (update-in state [:dashboard :icons :selected] disj id))) - -(defrecord ToggleIconSelection [id] - ptk/WatchEvent - (watch [_ state stream] - (let [selected (get-in state [:dashboard :icons :selected])] - (rx/of - (if (selected id) - (DeselectIcon. id) - (SelectIcon. id)))))) +(defn select-icon + [id] + (ptk/reify ::select-icon + ptk/UpdateEvent + (update [_ state] + (update-in state [:dashboard-icons :selected] (fnil conj #{}) id)))) (defn deselect-icon [id] - {:pre [(uuid? id)]} - (DeselectIcon. id)) + (ptk/reify ::deselect-icon + ptk/UpdateEvent + (update [_ state] + (update-in state [:dashboard-icons :selected] (fnil disj #{}) id)))) -(defn toggle-icon-selection +(def deselect-all-icons + (ptk/reify ::deselect-all-icons + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:dashboard-icons :selected] #{})))) + +;; --- Delete Icons + +(defn delete-icon [id] - (ToggleIconSelection. id)) + (ptk/reify ::delete-icon + ptk/UpdateEvent + (update [_ state] + (update state :icons dissoc id)) -;; --- Copy Selected Icon - -(defrecord CopySelected [id] - ptk/WatchEvent - (watch [_ state stream] - (let [selected (get-in state [:dashboard :icons :selected])] + ptk/WatchEvent + (watch [_ state s] (rx/merge - (->> (rx/from selected) - (rx/map #(get-in state [:icons %])) - (rx/map #(dissoc % :id)) - (rx/map #(assoc % :collection-id id)) - (rx/flat-map #(rp/mutation :create-icon %)) - (rx/map :payload) - (rx/map icon-created)) - (->> (rx/from selected) - (rx/map deselect-icon)))))) - -(defn copy-selected - [id] - {:pre [(or (uuid? id) (nil? id))]} - (CopySelected. id)) - -;; --- Move Selected Icon - -(defrecord MoveSelected [id] - ptk/UpdateEvent - (update [_ state] - (let [selected (get-in state [:dashboard :icons :selected])] - (reduce (fn [state icon] - (assoc-in state [:icons icon :collection] id)) - state - selected))) - - ptk/WatchEvent - (watch [_ state stream] - (let [selected (get-in state [:dashboard :icons :selected])] - (rx/merge - (->> (rx/from selected) - (rx/map persist-icon)) - (->> (rx/from selected) - (rx/map deselect-icon)))))) - -(defn move-selected - [id] - {:pre [(or (uuid? id) (nil? id))]} - (MoveSelected. id)) + (rx/of deselect-all-icons) + (->> (rp/mutation! :delete-icon {:id id}) + (rx/ignore)))))) ;; --- Delete Selected -(defrecord DeleteSelected [] - ptk/WatchEvent - (watch [_ state stream] - (let [selected (get-in state [:dashboard :icons :selected])] - (->> (rx/from selected) - (rx/map delete-icon))))) - -(defn delete-selected - [] - (DeleteSelected.)) - +(def delete-selected + (ptk/reify ::delete-selected + ptk/WatchEvent + (watch [_ state stream] + (let [selected (get-in state [:dashboard-icons :selected])] + (->> (rx/from selected) + (rx/map delete-icon)))))) ;; --- Update Opts (Filtering & Ordering) -(defrecord UpdateOpts [order filter edition] - ptk/UpdateEvent - (update [_ state] - (update-in state [:dashboard :icons] merge - {:edition edition} - (when order {:order order}) - (when filter {:filter filter})))) - (defn update-opts [& {:keys [order filter edition] - :or {edition false} - :as opts}] - (UpdateOpts. order filter edition)) + :or {edition false}}] + (ptk/reify ::update-opts + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-icons merge + {:edition edition} + (when order {:order order}) + (when filter {:filter filter}))))) + +;; --- Copy Selected Icon + +;; (defrecord CopySelected [id] +;; ptk/WatchEvent +;; (watch [_ state stream] +;; (let [selected (get-in state [:dashboard :icons :selected])] +;; (rx/merge +;; (->> (rx/from selected) +;; (rx/map #(get-in state [:icons %])) +;; (rx/map #(dissoc % :id)) +;; (rx/map #(assoc % :collection-id id)) +;; (rx/flat-map #(rp/mutation :create-icon %)) +;; (rx/map :payload) +;; (rx/map icon-created)) +;; (->> (rx/from selected) +;; (rx/map deselect-icon)))))) + +;; (defn copy-selected +;; [id] +;; {:pre [(or (uuid? id) (nil? id))]} +;; (CopySelected. id)) + +;; --- Move Selected Icon + +;; (defrecord MoveSelected [id] +;; ptk/UpdateEvent +;; (update [_ state] +;; (let [selected (get-in state [:dashboard :icons :selected])] +;; (reduce (fn [state icon] +;; (assoc-in state [:icons icon :collection] id)) +;; state +;; selected))) + +;; ptk/WatchEvent +;; (watch [_ state stream] +;; (let [selected (get-in state [:dashboard :icons :selected])] +;; (rx/merge +;; (->> (rx/from selected) +;; (rx/map persist-icon)) +;; (->> (rx/from selected) +;; (rx/map deselect-icon)))))) + +;; (defn move-selected +;; [id] +;; {:pre [(or (uuid? id) (nil? id))]} +;; (MoveSelected. id)) diff --git a/frontend/src/uxbox/main/data/images.cljs b/frontend/src/uxbox/main/data/images.cljs index da84ae26f..1c6e6280e 100644 --- a/frontend/src/uxbox/main/data/images.cljs +++ b/frontend/src/uxbox/main/data/images.cljs @@ -11,11 +11,11 @@ [beicon.core :as rx] [potok.core :as ptk] [uxbox.common.spec :as us] + [uxbox.common.data :as d] [uxbox.main.store :as st] [uxbox.main.repo :as rp] [uxbox.util.i18n :refer [tr]] [uxbox.util.router :as rt] - [uxbox.util.data :refer (jscoll->vec)] [uxbox.util.uuid :as uuid] [uxbox.util.time :as ts] [uxbox.util.router :as r] @@ -28,38 +28,66 @@ (s/def ::height number?) (s/def ::modified-at inst?) (s/def ::created-at inst?) -(s/def ::mimetype string?) +(s/def ::mtype string?) (s/def ::thumbnail string?) (s/def ::id uuid?) (s/def ::url string?) -(s/def ::collection-id (s/nilable uuid?)) +(s/def ::collection-id uuid?) (s/def ::user-id uuid?) -(s/def ::collection-entity +(s/def ::collection (s/keys :req-un [::id ::name ::created-at ::modified-at ::user-id])) -(s/def ::image-entity - (s/keys :opt-un [::collection-id] - :req-un [::id +(s/def ::image + (s/keys :req-un [::id ::name ::width ::height + ::mtype + ::collection-id ::created-at ::modified-at - ::mimetype - ::thumbnail - ::url + ::uri + ::thumb-uri ::user-id])) +;; --- Initialize Collection Page + +(declare fetch-images) + +(defn initialize + [collection-id] + (us/verify ::us/uuid collection-id) + (ptk/reify ::initialize + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:dashboard-images :selected] #{})) + + ptk/WatchEvent + (watch [_ state stream] + (rx/of (fetch-images collection-id))))) + +;; --- Fetch Collections + +(declare collections-fetched) + +(def fetch-collections + (ptk/reify ::fetch-collections + ptk/WatchEvent + (watch [_ state s] + (->> (rp/query! :images-collections) + (rx/map collections-fetched))))) + + ;; --- Collections Fetched (defn collections-fetched [items] - (us/verify (s/every ::collection-entity) items) + (us/verify (s/every ::collection) items) (ptk/reify ::collections-fetched ptk/UpdateEvent (update [_ state] @@ -70,105 +98,63 @@ state items)))) -;; --- Fetch Color Collections - -(def fetch-collections - (ptk/reify ::fetch-collections - ptk/WatchEvent - (watch [_ state s] - (->> (rp/query! :images-collections) - (rx/map collections-fetched))))) - -;; --- Collection Created - -(defn collection-created - [item] - (us/verify ::collection-entity item) - (ptk/reify ::collection-created - ptk/UpdateEvent - (update [_ state] - (let [{:keys [id] :as item} (assoc item :type :own)] - (update state :images-collections assoc id item))))) ;; --- Create Collection +(declare collection-created) + (def create-collection (ptk/reify ::create-collection ptk/WatchEvent (watch [_ state s] (let [data {:name (tr "ds.default-library-title" (gensym "c"))}] - (->> (rp/mutation! :create-image-collection data) + (->> (rp/mutation! :create-images-collection data) (rx/map collection-created)))))) -;; --- Collection Updated +;; --- Collection Created -(defrecord CollectionUpdated [item] - ptk/UpdateEvent - (update [_ state] - (update-in state [:images-collections (:id item)] merge item))) - -(defn collection-updated +(defn collection-created [item] - (us/verify ::collection-entity item) - (CollectionUpdated. item)) - -;; --- Update Collection - -(defrecord UpdateCollection [id] - ptk/WatchEvent - (watch [_ state s] - (let [item (get-in state [:images-collections id])] - (->> (rp/mutation! :update-images-collection item) - (rx/map collection-updated))))) - -(defn update-collection - [id] - (UpdateCollection. id)) + (us/verify ::collection item) + (ptk/reify ::collection-created + ptk/UpdateEvent + (update [_ state] + (let [{:keys [id] :as item} (assoc item :type :own)] + (update state :images-collections assoc id item))))) ;; --- Rename Collection -(defrecord RenameCollection [id name] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:images-collections id :name] name)) - - ptk/WatchEvent - (watch [_ state s] - (rx/of (update-collection id)))) - (defn rename-collection [id name] - (RenameCollection. id name)) + (ptk/reify ::rename-collection + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:images-collections id :name] name)) + + ptk/WatchEvent + (watch [_ state s] + (let [params {:id id :name name}] + (->> (rp/mutation! :rename-images-collection params) + (rx/ignore)))))) ;; --- Delete Collection -(defrecord DeleteCollection [id] - ptk/UpdateEvent - (update [_ state] - (update state :images-collections dissoc id)) - - ptk/WatchEvent - (watch [_ state s] - (let [type (get-in state [:dashboard :images :type])] - (->> (rp/mutation! :delete-images-collection {:id id}) - (rx/map #(rt/nav :dashboard/images nil {:type type})))))) - (defn delete-collection - [id] - (DeleteCollection. id)) - -;; --- Image Created - -(defn image-created - [item] - (us/verify ::image-entity item) - (ptk/reify ::image-created + [id on-success] + (ptk/reify ::delete-collection ptk/UpdateEvent (update [_ state] - (update state :images assoc (:id item) item)))) + (update state :images-collections dissoc id)) + + ptk/WatchEvent + (watch [_ state s] + (->> (rp/mutation! :delete-images-collection {:id id}) + (rx/tap on-success) + (rx/ignore))))) ;; --- Create Image +(declare image-created) (def allowed-file-types #{"image/jpeg" "image/png"}) (defn create-images @@ -179,41 +165,49 @@ (ptk/reify ::create-images ptk/UpdateEvent (update [_ state] - (assoc-in state [:dashboard :images :uploading] true)) + (assoc-in state [:dashboard-images :uploading] true)) ptk/WatchEvent (watch [_ state stream] - (letfn [(image-size [file] - (->> (files/get-image-size file) - (rx/map (partial vector file)))) - (allowed-file? [file] + (letfn [(allowed-file? [file] (contains? allowed-file-types (.-type file))) (finalize-upload [state] - (assoc-in state [:dashboard :images :uploading] false)) - (prepare [[file [width height]]] - (cond-> {:name (.-name file) - :mimetype (.-type file) - :id (uuid/next) - :file file - :width width - :height height} - id (assoc :collection-id id)))] + (assoc-in state [:dashboard-images :uploading] false)) + (on-success [_] + (st/emit! finalize-upload) + (on-uploaded)) + (on-error [e] + (st/emit! finalize-upload) + (rx/throw e)) + (prepare [file] + {:name (.-name file) + :collection-id id + :content file})] (->> (rx/from files) (rx/filter allowed-file?) - (rx/mapcat image-size) (rx/map prepare) - (rx/mapcat #(rp/mutation! :create-image %)) + (rx/mapcat #(rp/mutation! :upload-image %)) (rx/reduce conj []) - (rx/do #(st/emit! finalize-upload)) - (rx/do on-uploaded) + (rx/do on-success) (rx/mapcat identity) - (rx/map image-created))))))) + (rx/map image-created) + (rx/catch on-error))))))) + +;; --- Image Created + +(defn image-created + [item] + (us/verify ::image item) + (ptk/reify ::image-created + ptk/UpdateEvent + (update [_ state] + (update state :images assoc (:id item) item)))) ;; --- Update Image (defn persist-image [id] - {:pre [(uuid? id)]} + (us/verify ::us/uuid id) (ptk/reify ::persist-image ptk/WatchEvent (watch [_ state stream] @@ -221,31 +215,34 @@ (->> (rp/mutation! :update-image data) (rx/ignore)))))) -;; --- Images Fetched - -(defn images-fetched - [items] - (us/verify (s/every ::image-entity) items) - (ptk/reify ::images-fetched - ptk/UpdateEvent - (update [_ state] - (reduce (fn [state {:keys [id] :as image}] - (assoc-in state [:images id] image)) - state - items)))) - ;; --- Fetch Images +(declare images-fetched) + (defn fetch-images "Fetch a list of images of the selected collection" [id] - (us/verify (s/nilable ::us/uuid) id) + (us/verify ::us/uuid id) (ptk/reify ::fetch-images ptk/WatchEvent (watch [_ state s] - (let [params (cond-> {} id (assoc :collection-id id))] + (let [params {:collection-id id}] (->> (rp/query! :images-by-collection params) - (rx/map images-fetched)))))) + (rx/map (partial images-fetched id))))))) + +;; --- Images Fetched + +(s/def ::images (s/every ::image)) + +(defn images-fetched + [collection-id items] + (us/verify ::us/uuid collection-id) + (us/verify ::images items) + (ptk/reify ::images-fetched + ptk/UpdateEvent + (update [_ state] + (let [images (d/index-by :id items)] + (assoc state :images images))))) ;; --- Fetch Image @@ -281,139 +278,123 @@ {:pre [(map? image)]} (ImageFetched. image)) -;; --- Delete Images - -(defrecord DeleteImage [id] - ptk/UpdateEvent - (update [_ state] - (-> state - (update :images dissoc id) - (update-in [:dashboard :images :selected] disj id))) - - ptk/WatchEvent - (watch [_ state s] - (->> (rp/mutation! :delete-image {:id id}) - (rx/ignore)))) - -(defn delete-image - [id] - {:pre [(uuid? id)]} - (DeleteImage. id)) - ;; --- Rename Image -(defrecord RenameImage [id name] - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:images id :name] name)) - - ptk/WatchEvent - (watch [_ state stream] - (rx/of (persist-image id)))) - (defn rename-image [id name] - {:pre [(uuid? id) (string? name)]} - (RenameImage. id name)) + (us/verify ::us/uuid id) + (us/verify ::us/string name) + (ptk/reify ::rename-image + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:images id :name] name)) -;; --- Select image + ptk/WatchEvent + (watch [_ state stream] + (rx/of (persist-image id))))) -(defrecord SelectImage [id] - ptk/UpdateEvent - (update [_ state] - (update-in state [:dashboard :images :selected] conj id))) +;; --- Image Selection -(defrecord DeselectImage [id] - ptk/UpdateEvent - (update [_ state] - (update-in state [:dashboard :images :selected] disj id))) - -(defrecord ToggleImageSelection [id] - ptk/WatchEvent - (watch [_ state stream] - (let [selected (get-in state [:dashboard :images :selected])] - (rx/of - (if (selected id) - (DeselectImage. id) - (SelectImage. id)))))) +(defn select-image + [id] + (ptk/reify ::select-image + ptk/UpdateEvent + (update [_ state] + (update-in state [:dashboard-images :selected] (fnil conj #{}) id)))) (defn deselect-image [id] - {:pre [(uuid? id)]} - (DeselectImage. id)) + (ptk/reify ::deselect-image + ptk/UpdateEvent + (update [_ state] + (update-in state [:dashboard-images :selected] (fnil disj #{}) id)))) -(defn toggle-image-selection +(def deselect-all-images + (ptk/reify ::deselect-all-images + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:dashboard-images :selected] #{})))) + +;; --- Delete Images + +(defn delete-image [id] - (ToggleImageSelection. id)) + (us/verify ::us/uuid id) + (ptk/reify ::delete-image + ptk/UpdateEvent + (update [_ state] + (update state :images dissoc id)) -;; --- Copy Selected Image - -(defrecord CopySelected [id] - ptk/WatchEvent - (watch [_ state stream] - (let [selected (get-in state [:dashboard :images :selected])] + ptk/WatchEvent + (watch [_ state s] (rx/merge - (->> (rx/from selected) - (rx/flat-map #(rp/mutation! :copy-image {:id % :collection-id id})) - (rx/map image-created)) - (->> (rx/from selected) - (rx/map deselect-image)))))) - -(defn copy-selected - [id] - {:pre [(or (uuid? id) (nil? id))]} - (CopySelected. id)) - -;; --- Move Selected Image - -(defrecord MoveSelected [id] - ptk/UpdateEvent - (update [_ state] - (let [selected (get-in state [:dashboard :images :selected])] - (reduce (fn [state image] - (assoc-in state [:images image :collection] id)) - state - selected))) - - ptk/WatchEvent - (watch [_ state stream] - (let [selected (get-in state [:dashboard :images :selected])] - (rx/merge - (->> (rx/from selected) - (rx/map persist-image)) - (->> (rx/from selected) - (rx/map deselect-image)))))) - -(defn move-selected - [id] - {:pre [(or (uuid? id) (nil? id))]} - (MoveSelected. id)) + (rx/of deselect-all-images) + (->> (rp/mutation! :delete-image {:id id}) + (rx/ignore)))))) ;; --- Delete Selected -(defrecord DeleteSelected [] - ptk/WatchEvent - (watch [_ state stream] - (let [selected (get-in state [:dashboard :images :selected])] - (->> (rx/from selected) - (rx/map delete-image))))) - -(defn delete-selected - [] - (DeleteSelected.)) +(def delete-selected + (ptk/reify ::delete-selected + ptk/WatchEvent + (watch [_ state stream] + (let [selected (get-in state [:dashboard-images :selected])] + (->> (rx/from selected) + (rx/map delete-image)))))) ;; --- Update Opts (Filtering & Ordering) -(defrecord UpdateOpts [order filter edition] - ptk/UpdateEvent - (update [_ state] - (update-in state [:dashboard :images] merge - {:edition edition} - (when order {:order order}) - (when filter {:filter filter})))) - (defn update-opts [& {:keys [order filter edition] - :or {edition false} - :as opts}] - (UpdateOpts. order filter edition)) + :or {edition false}}] + (ptk/reify ::update-opts + ptk/UpdateEvent + (update [_ state] + (update state :dashboard-images merge + {:edition edition} + (when order {:order order}) + (when filter {:filter filter}))))) + +;; --- Copy Selected Image + +;; (defrecord CopySelected [id] +;; ptk/WatchEvent +;; (watch [_ state stream] +;; (let [selected (get-in state [:dashboard-images :selected])] +;; (rx/merge +;; (->> (rx/from selected) +;; (rx/flat-map #(rp/mutation! :copy-image {:id % :collection-id id})) +;; (rx/map image-created)) +;; (->> (rx/from selected) +;; (rx/map deselect-image)))))) + +;; (defn copy-selected +;; [id] +;; {:pre [(or (uuid? id) (nil? id))]} +;; (CopySelected. id)) + +;; --- Move Selected Image + +;; (defrecord MoveSelected [id] +;; ptk/UpdateEvent +;; (update [_ state] +;; (let [selected (get-in state [:dashboard-images :selected])] +;; (reduce (fn [state image] +;; (assoc-in state [:images image :collection] id)) +;; state +;; selected))) + +;; ptk/WatchEvent +;; (watch [_ state stream] +;; (let [selected (get-in state [:dashboard-images :selected])] +;; (rx/merge +;; (->> (rx/from selected) +;; (rx/map persist-image)) +;; (->> (rx/from selected) +;; (rx/map deselect-image)))))) + +;; (defn move-selected +;; [id] +;; {:pre [(or (uuid? id) (nil? id))]} +;; (MoveSelected. id)) + diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 94f220bf9..a8224d776 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -54,6 +54,7 @@ ;; --- Declarations (declare fetch-users) +(declare fetch-images) (declare handle-who) (declare handle-pointer-update) (declare handle-pointer-send) @@ -289,7 +290,8 @@ (rx/of (dp/fetch-file file-id) (dp/fetch-pages file-id) (initialize-layout file-id) - (fetch-users file-id)) + (fetch-users file-id) + (fetch-images file-id)) (->> (rx/zip (rx/filter (ptk/type? ::dp/pages-fetched) stream) (rx/filter (ptk/type? ::dp/files-fetched) stream)) (rx/take 1) @@ -399,6 +401,7 @@ state users)))) + ;; --- Toggle layout flag (defn toggle-layout-flag @@ -1303,6 +1306,94 @@ query-params {:page-id page-id}] (rx/of (rt/nav :workspace path-params query-params)))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Workspace Images +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- Fetch Workspace Images + +(declare images-fetched) + +(defn fetch-images + [file-id] + (ptk/reify ::fetch-images + ptk/WatchEvent + (watch [_ state stream] + (->> (rp/query :project-file-images {:file-id file-id}) + (rx/map images-fetched))))) + +(defn images-fetched + [images] + (ptk/reify ::images-fetched + ptk/UpdateEvent + (update [_ state] + (let [images (d/index-by :id images)] + (assoc state :workspace-images images))))) + + +;; --- Upload Image + +(declare image-uploaded) +(def allowed-file-types #{"image/jpeg" "image/png"}) + +(defn upload-image + ([file] (upload-image file identity)) + ([file on-uploaded] + (us/verify fn? on-uploaded) + (ptk/reify ::upload-image + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :uploading] true)) + + ptk/WatchEvent + (watch [_ state stream] + (let [allowed-file? #(contains? allowed-file-types (.-type %)) + finalize-upload #(assoc-in % [:workspace-local :uploading] false) + file-id (get-in state [:workspace-page :file-id]) + + on-success #(do (st/emit! finalize-upload) + (on-uploaded %)) + on-error #(do (st/emit! finalize-upload) + (rx/throw %)) + + prepare + (fn [file] + {:name (.-name file) + :file-id file-id + :content file})] + (->> (rx/of file) + (rx/filter allowed-file?) + (rx/map prepare) + (rx/mapcat #(rp/mutation! :upload-project-file-image %)) + (rx/do on-success) + (rx/map image-uploaded) + (rx/catch on-error))))))) + +(s/def ::id ::us/uuid) +(s/def ::name ::us/string) +(s/def ::width ::us/number) +(s/def ::height ::us/number) +(s/def ::mtype ::us/string) +(s/def ::uri ::us/string) +(s/def ::thumb-uri ::us/string) + +(s/def ::image + (s/keys :req-un [::id + ::name + ::width + ::height + ::uri + ::thumb-uri])) + +(defn image-uploaded + [item] + (us/verify ::image item) + (ptk/reify ::image-created + ptk/UpdateEvent + (update [_ state] + (update state :workspace-images assoc (:id item) item)))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Page Changes Reactions ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index b59cd57ec..10664b66f 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -2,6 +2,9 @@ ;; 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) 2017-2019 Andrey Antukh (ns uxbox.main.refs @@ -35,6 +38,10 @@ (-> (l/key :workspace-file) (l/derive st/state))) +(def workspace-images + (-> (l/key :workspace-images) + (l/derive st/state))) + (def workspace-users (-> (l/key :workspace-users) (l/derive st/state))) diff --git a/frontend/src/uxbox/main/repo.cljs b/frontend/src/uxbox/main/repo.cljs index 8cd70e253..0057822d3 100644 --- a/frontend/src/uxbox/main/repo.cljs +++ b/frontend/src/uxbox/main/repo.cljs @@ -112,7 +112,15 @@ ([id] (mutation id {})) ([id params] (mutation id params))) -(defmethod mutation :create-image +(defmethod mutation :upload-image + [id params] + (let [form (js/FormData.)] + (run! (fn [[key val]] + (.append form (name key) val)) + (seq params)) + (send-mutation! id form))) + +(defmethod mutation :upload-project-file-image [id params] (let [form (js/FormData.)] (run! (fn [[key val]] diff --git a/frontend/src/uxbox/main/ui/dashboard/icons.cljs b/frontend/src/uxbox/main/ui/dashboard/icons.cljs index 39419fb07..b1de2dbf7 100644 --- a/frontend/src/uxbox/main/ui/dashboard/icons.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/icons.cljs @@ -7,9 +7,12 @@ (ns uxbox.main.ui.dashboard.icons (:require + [cljs.spec.alpha :as s] [cuerdas.core :as str] [lentes.core :as l] [rumext.alpha :as mf] + [uxbox.common.data :as d] + [uxbox.common.spec :as us] [uxbox.builtins.icons :as i] [uxbox.main.data.icons :as di] [uxbox.main.store :as st] @@ -21,7 +24,7 @@ [uxbox.util.components :refer [chunked-list]] [uxbox.util.data :refer [read-string jscoll->vec seek]] [uxbox.util.dom :as dom] - [uxbox.util.i18n :as t :refer [tr]] + [uxbox.util.i18n :as i18n :refer [tr t]] [uxbox.util.router :as rt] [uxbox.util.time :as dt])) @@ -51,111 +54,200 @@ icons (filter #(contains-term? (:name %) term) icons))) -;; --- Refs - -(def collections-iref - (-> (l/key :icons-collections) - (l/derive st/state))) - -(def opts-iref - (-> (l/in [:dashboard :icons]) - (l/derive st/state))) - ;; --- Component: Grid Header (mf/defc grid-header - [{:keys [coll] :as props}] - (letfn [(on-change [name] - (st/emit! (di/rename-collection (:id coll) name))) - (delete [] - (st/emit! - (di/delete-collection (:id coll)) - (rt/nav :dashboard/icons nil {:type (:type coll)}))) - (on-delete [] - (modal/show! confirm-dialog {:on-accept delete}))] - [:& common/grid-header {:value (:name coll) + [{:keys [collection] :as props}] + (let [{:keys [id type]} collection + on-change #(st/emit! (di/rename-collection id %)) + on-deleted #(st/emit! (rt/nav :dashboard-icons nil {:type type})) + delete #(st/emit! (di/delete-collection id on-deleted)) + on-delete #(modal/show! confirm-dialog {:on-accept delete})] + [:& common/grid-header {:value (:name collection) :on-change on-change :on-delete on-delete}])) ;; --- Nav (mf/defc nav-item - [{:keys [coll selected?] :as props}] + [{:keys [collection selected?] :as props}] (let [local (mf/use-state {}) - {:keys [id type name]} coll - editable? (= type :own)] - (letfn [(on-click [event] - (let [type (or type :own)] - (st/emit! (rt/nav :dashboard-icons {} {:type type :id id})))) - (on-input-change [event] - (-> (dom/get-target event) - (dom/get-value) - (swap! local assoc :name))) - (on-cancel [event] - (swap! local dissoc :name :edit)) - (on-double-click [event] - (when editable? - (swap! local assoc :edit true))) - (on-input-keyup [event] - (when (kbd/enter? event) - (let [value (-> (dom/get-target event) (dom/get-value))] - (st/emit! (di/rename-collection id (str/trim (:name @local)))) - (swap! local assoc :edit false))))] - [:li {:on-click on-click - :on-double-click on-double-click - :class-name (when selected? "current")} - (if (:edit @local) - [:div - [:input.element-title {:value (if (:name @local) - (:name @local) - (if id name "Storage")) - :on-change on-input-change - :on-key-down on-input-keyup}] - [:span.close {:on-click on-cancel} i/close]] - [:span.element-title (if id name "Storage")])]))) + {:keys [id type name]} collection + editable? (= type :own) + + on-click + (fn [event] + (let [type (or type :own)] + (st/emit! (rt/nav :dashboard-icons {} {:type type :id id})))) + + + on-input-change + (fn [event] + (-> (dom/get-target event) + (dom/get-value) + (swap! local assoc :name))) + + on-cancel #(swap! local dissoc :name :edit) + on-double-click #(when editable? (swap! local assoc :edit true)) + + on-input-keyup + (fn [event] + (when (kbd/enter? event) + (let [value (-> (dom/get-target event) (dom/get-value))] + (st/emit! (di/rename-collection id (str/trim (:name @local)))) + (swap! local assoc :edit false))))] + + [:li {:on-click on-click + :on-double-click on-double-click + :class-name (when selected? "current")} + (if (:edit @local) + [:div + [:input.element-title {:value (or (:name @local) name) + :on-change on-input-change + :on-key-down on-input-keyup}] + [:span.close {:on-click on-cancel} i/close]] + [:span.element-title name])])) (mf/defc nav - [{:keys [id type colls selected-coll] :as props}] - (let [own? (= type :own) + [{:keys [id type collections] :as props}] + (let [locale (i18n/use-locale) + own? (= type :own) builtin? (= type :builtin) - select-tab #(st/emit! (rt/nav :dashboard-icons nil {:type %}))] + create-collection #(st/emit! di/create-collection) + select-own-tab #(st/emit! (rt/nav :dashboard-icons nil {:type :own})) + select-buitin-tab #(st/emit! (rt/nav :dashboard-icons nil {:type :builtin}))] + [:div.library-bar [:div.library-bar-inside + ;; Tabs [:ul.library-tabs - [:li {:class-name (when own? "current") - :on-click (partial select-tab :own)} - (tr "ds.your-icons-title")] - [:li {:class-name (when builtin? "current") - :on-click (partial select-tab :builtin)} - (tr "ds.store-icons-title")]] + [:li {:class (when own? "current") + :on-click select-own-tab} + (t locale "ds.your-icons-title")] + [:li {:class (when builtin? "current") + :on-click select-buitin-tab} + (t locale "ds.store-icons-title")]] + + + ;; Collections List [:ul.library-elements (when own? [:li [:a.btn-primary {:on-click #(st/emit! di/create-collection)} (tr "ds.icons-collection.new")]]) - (when own? - [:& nav-item {:selected? (nil? id)}]) - (for [item colls] - [:& nav-item {:coll item + (for [item collections] + [:& nav-item {:collection item :selected? (= (:id item) id) :key (:id item)}])]]])) -;; --- Grid +;; (mf/def grid-options-tooltip +;; :mixins [mf/reactive mf/memo] + +;; :render +;; (fn [own {:keys [selected on-select title]}] +;; {:pre [(uuid? selected) +;; (fn? on-select) +;; (string? title)]} +;; (let [colls (mf/react collections-iref) +;; colls (->> (vals colls) +;; (filter #(= :own (:type %))) +;; (remove #(= selected (:id %))) +;; (sort-by :name colls)) +;; on-select (fn [event id] +;; (dom/prevent-default event) +;; (dom/stop-propagation event) +;; (on-select id))] +;; [:ul.move-list +;; [:li.title title] +;; [:li +;; [:a {:href "#" :on-click #(on-select % nil)} "Storage"]] +;; (for [{:keys [id name] :as coll} colls] +;; [:li {:key (pr-str id)} +;; [:a {:on-click #(on-select % id)} name]])]))) + +(mf/defc grid-options + [{:keys [id type selected] :as props}] + (let [local (mf/use-state {}) + delete #(st/emit! di/delete-selected) + on-delete #(modal/show! confirm-dialog {:on-accept delete}) + + ;; (on-toggle-copy [event] + ;; (swap! local update :show-copy-tooltip not)) + ;; (on-toggle-move [event] + ;; (swap! local update :show-move-tooltip not)) + ;; (on-copy [selected] + ;; (swap! local assoc + ;; :show-move-tooltip false + ;; :show-copy-tooltip false) + ;; (st/emit! (di/copy-selected selected))) + ;; (on-move [selected] + ;; (swap! local assoc + ;; :show-move-tooltip false + ;; :show-copy-tooltip false) + ;; (st/emit! (di/move-selected selected))) + ;; (on-rename [event] + ;; (let [selected (first selected)] + ;; (st/emit! (di/update-opts :edition selected)))) + ] + ;; MULTISELECT OPTIONS BAR + [:div.multiselect-bar + (when (= type :own) + ;; If editable + [:div.multiselect-nav + ;; [:span.move-item.tooltip.tooltip-top + ;; {:alt (tr "ds.multiselect-bar.copy") + ;; :on-click on-toggle-copy} + ;; (when (:show-copy-tooltip @local) + ;; [:& grid-options-tooltip {:selected id + ;; :title (tr "ds.multiselect-bar.copy-to-library") + ;; :on-select on-copy}]) + ;; i/copy] + ;; [:span.move-item.tooltip.tooltip-top + ;; {:alt (tr "ds.multiselect-bar.move") + ;; :on-click on-toggle-move} + ;; (when (:show-move-tooltip @local) + ;; [:& grid-options-tooltip {:selected id + ;; :title (tr "ds.multiselect-bar.move-to-library") + ;; :on-select on-move}]) + ;; i/move] + ;; (when (= 1 (count selected)) + ;; [:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.rename") + ;; :on-click on-rename} + ;; i/pencil]) + [:span.delete.tooltip.tooltip-top + {:alt (tr "ds.multiselect-bar.delete") + :on-click on-delete} + i/trash]] + + ;; If not editable + ;; [:div.multiselect-nav + ;; [:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.copy") + ;; :on-click on-toggle-copy} + ;; (when (:show-copy-tooltip @local) + ;; [:& grid-options-tooltip {:selected id + ;; :title (tr "ds.multiselect-bar.copy-to-library") + ;; :on-select on-copy}]) + ;; i/organize]] + )])) + +;; --- Grid Form (mf/defc grid-form [{:keys [id type uploading?] :as props}] - (let [input (mf/use-ref nil) + (let [locale (i18n/use-locale) + input (mf/use-ref nil) on-click #(dom/click (mf/ref-node input)) - on-select #(st/emit! (->> (dom/get-event-files %) - (jscoll->vec) + on-select #(st/emit! (->> (dom/get-target %) + (dom/get-files) + (array-seq) (di/create-icons id)))] [:div.grid-item.add-project {:on-click on-click} (if uploading? [:div i/loader-pencil] - [:span (tr "ds.icon-new")]) + [:span (t locale "ds.icon-new")]) [:input.upload-icon-input {:style {:display "none"} :multiple true @@ -165,122 +257,36 @@ :type "file" :on-change on-select}]])) -(mf/def grid-options-tooltip - :mixins [mf/reactive mf/memo] - - :render - (fn [own {:keys [selected on-select title]}] - {:pre [(uuid? selected) - (fn? on-select) - (string? title)]} - (let [colls (mf/react collections-iref) - colls (->> (vals colls) - (filter #(= :own (:type %))) - (remove #(= selected (:id %))) - (sort-by :name colls)) - on-select (fn [event id] - (dom/prevent-default event) - (dom/stop-propagation event) - (on-select id))] - [:ul.move-list - [:li.title title] - [:li - [:a {:href "#" :on-click #(on-select % nil)} "Storage"]] - (for [{:keys [id name] :as coll} colls] - [:li {:key (pr-str id)} - [:a {:on-click #(on-select % id)} name]])]))) - -;; (mf/def grid-options -;; :mixins [(mf/local) mf/memo] - -;; :render -;; (fn [{:keys [::mf/local] :as own} -;; {:keys [id type selected] :as props}] -;; (letfn [(delete [] -;; (st/emit! (di/delete-selected))) -;; (on-delete [event] -;; (modal/show! confirm-dialog {:on-accept delete})) -;; (on-toggle-copy [event] -;; (swap! local update :show-copy-tooltip not)) -;; (on-toggle-move [event] -;; (swap! local update :show-move-tooltip not)) -;; (on-copy [selected] -;; (swap! local assoc -;; :show-move-tooltip false -;; :show-copy-tooltip false) -;; (st/emit! (di/copy-selected selected))) -;; (on-move [selected] -;; (swap! local assoc -;; :show-move-tooltip false -;; :show-copy-tooltip false) -;; (st/emit! (di/move-selected selected))) -;; (on-rename [event] -;; (let [selected (first selected)] -;; (st/emit! (di/update-opts :edition selected))))] - -;; ;; MULTISELECT OPTIONS BAR -;; [:div.multiselect-bar -;; (if (or (= type :own) (nil? id)) -;; ;; if editable -;; [:div.multiselect-nav {} -;; [:span.move-item.tooltip.tooltip-top -;; {:alt (tr "ds.multiselect-bar.copy") -;; :on-click on-toggle-copy} -;; (when (:show-copy-tooltip @local) -;; (grid-options-tooltip {:selected id -;; :title (tr "ds.multiselect-bar.copy-to-library") -;; :on-select on-copy})) -;; i/copy] -;; [:span.move-item.tooltip.tooltip-top -;; {:alt (tr "ds.multiselect-bar.move") -;; :on-click on-toggle-move} -;; (when (:show-move-tooltip @local) -;; (grid-options-tooltip {:selected id -;; :title (tr "ds.multiselect-bar.move-to-library") -;; :on-select on-move})) -;; i/move] -;; (when (= 1 (count selected)) -;; [:span.move-item.tooltip.tooltip-top -;; {:alt (tr "ds.multiselect-bar.rename") -;; :on-click on-rename} -;; i/pencil]) -;; [:span.delete.tooltip.tooltip-top -;; {:alt (tr "ds.multiselect-bar.delete") -;; :on-click on-delete} -;; i/trash]] - -;; ;; if not editable -;; [:div.multiselect-nav -;; [:span.move-item.tooltip.tooltip-top -;; {:alt (tr "ds.multiselect-bar.copy") -;; :on-click on-toggle-copy} -;; (when (:show-copy-tooltip @local) -;; (grid-options-tooltip {:selected id -;; :title (tr "ds.multiselect-bar.copy-to-library") -;; :on-select on-copy})) -;; i/organize]])]))) - ;; --- Grid Item (mf/defc grid-item [{:keys [icon selected? edition?] :as props}] - (letfn [(toggle-selection [event] - (st/emit! (di/toggle-icon-selection (:id icon)))) - (on-key-down [event] - (when (kbd/enter? event) - (on-blur event))) - (on-blur [event] - (let [target (dom/event->target event) - name (dom/get-value target)] - (st/emit! (di/update-opts :edition false) - (di/rename-icon (:id icon) name)))) - (ignore-click [event] - (dom/stop-propagation event) - (dom/prevent-default event)) - (on-edit [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (di/update-opts :edition (:id icon))))] + (let [toggle-selection #(st/emit! (if selected? + (di/deselect-icon (:id icon)) + (di/select-icon (:id icon)))) + on-blur + (fn [event] + (let [target (dom/get-target event) + name (dom/get-value target)] + (st/emit! (di/update-opts :edition false) + (di/rename-icon (:id icon) name)))) + + on-key-down + (fn [event] + (when (kbd/enter? event) + (on-blur event))) + + ignore-click + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event)) + + on-edit + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! (di/update-opts :edition (:id icon))))] + [:div.grid-item.small-item.project-th [:div.input-checkbox.check-primary [:input {:type "checkbox" @@ -304,18 +310,13 @@ ;; --- Grid -(defn make-icons-iref - [id] - (-> (comp (l/key :icons) - (l/lens (fn [icons] - (->> (vals icons) - (filter #(= id (:collection-id %))))))) +(def icons-iref + (-> (comp (l/key :icons) (l/lens vals)) (l/derive st/state))) (mf/defc grid - [{:keys [id type coll opts] :as props}] - (let [editable? (or (= type :own) (nil? id)) - icons-iref (mf/use-memo #(make-icons-iref id) #js [id]) + [{:keys [id type collection opts] :as props}] + (let [editable? (= type :own) icons (->> (mf/deref icons-iref) (filter-icons-by (:filter opts "")) (sort-icons-by (:order opts :name)))] @@ -336,44 +337,48 @@ ;; --- Content +(def opts-iref + (-> (l/key :dashboard-icons) + (l/derive st/state))) + (mf/defc content - [{:keys [id type coll] :as props}] - (let [opts (mf/deref opts-iref)] - [:* - [:section.dashboard-grid.library - (when coll - [:& grid-header {:coll coll}]) - [:& grid {:id id - :key [id type] - :type type - :coll coll - :opts opts}] - (when (seq (:selected opts)) - #_[:& grid-options {:id id :type type :selected (:selected opts)}])]])) + [{:keys [id type collection] :as props}] + (let [{:keys [selected] :as opts} (mf/deref opts-iref)] + [:section.dashboard-grid.library + (when collection + [:& grid-header {:collection collection}]) + (if collection + [:& grid {:id id :type type :collection collection :opts opts}] + [:span "EMPTY STATE TODO"]) + (when-not (empty? selected) + #_[:& grid-options {:id id :type type :selected (:selected opts)}])])) ;; --- Icons Page +(def collections-iref + (-> (l/key :icons-collections) + (l/derive st/state))) + (mf/defc icons-page [{:keys [id type] :as props}] (let [type (or type :own) - colls (mf/deref collections-iref) - colls (cond->> (vals colls) - (= type :own) (filter #(= :own (:type %))) - (= type :builtin) (filter #(= :builtin (:type %))) - true (sort-by :created-at)) - selected-coll (cond - (and (= type :own) (nil? id)) nil - (uuid? id) (seek #(= id (:id %)) colls) - :else (first colls)) - id (:id selected-coll)] + collections (mf/deref collections-iref) + collections (cond->> (vals collections) + (= type :own) (filter #(= :own (:type %))) + (= type :builtin) (filter #(= :builtin (:type %))) + true (sort-by :created-at)) + + collection (cond + (uuid? id) (seek #(= id (:id %)) collections) + :else (first collections)) + + id (:id collection)] + (mf/use-effect #(st/emit! di/fetch-collections)) - (mf/use-effect {:fn #(st/emit! (di/fetch-icons id)) - :deps #js [(str id)]}) + (mf/use-effect + {:fn #(when id (st/emit! (di/initialize id))) + :deps (mf/deps id)}) [:section.dashboard-content - [:& nav {:type type - :id id - :colls colls}] - [:& content {:type type - :id id - :coll selected-coll}]])) + [:& nav {:type type :id id :collections collections}] + [:& content {:type type :id id :collection collection}]])) diff --git a/frontend/src/uxbox/main/ui/dashboard/images.cljs b/frontend/src/uxbox/main/ui/dashboard/images.cljs index d4cdd3996..3825ff26c 100644 --- a/frontend/src/uxbox/main/ui/dashboard/images.cljs +++ b/frontend/src/uxbox/main/ui/dashboard/images.cljs @@ -2,55 +2,42 @@ ;; License, v. 2.0. If a copy of the MPL was not distributed with this ;; file, You can obtain one at http://mozilla.org/MPL/2.0/. ;; -;; Copyright (c) 2015-2016 Andrey Antukh -;; Copyright (c) 2015-2016 Juan de la Cruz +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2015-2020 Andrey Antukh +;; Copyright (c) 2015-2020 Juan de la Cruz (ns uxbox.main.ui.dashboard.images (:require + [cljs.spec.alpha :as s] [cuerdas.core :as str] [lentes.core :as l] - [rumext.core :as mx] [rumext.alpha :as mf] [uxbox.builtins.icons :as i] + [uxbox.common.data :as d] + [uxbox.common.spec :as us] [uxbox.main.data.images :as di] - [uxbox.main.data.lightbox :as udl] [uxbox.main.store :as st] + [uxbox.main.ui.confirm :refer [confirm-dialog]] [uxbox.main.ui.dashboard.common :as common] [uxbox.main.ui.keyboard :as kbd] - [uxbox.main.ui.lightbox :as lbx] - [uxbox.main.ui.confirm :refer [confirm-dialog]] [uxbox.main.ui.modal :as modal] - [uxbox.util.data :refer [read-string jscoll->vec seek]] [uxbox.util.dom :as dom] - [uxbox.util.i18n :as t :refer [tr]] + [uxbox.util.i18n :as i18n :refer [t tr]] [uxbox.util.router :as rt] [uxbox.util.time :as dt])) -;; --- Refs - -(def collections-iref - (-> (l/key :images-collections) - (l/derive st/state))) - -(def opts-iref - (-> (l/in [:dashboard :images]) - (l/derive st/state))) - ;; --- Page Title (mf/defc grid-header - [{:keys [coll] :as props}] - (letfn [(on-change [name] - (st/emit! (di/rename-collection (:id coll) name))) - - (delete [] - (st/emit! - (di/delete-collection (:id coll)) - (rt/nav :dashboard/images nil {:type (:type coll)}))) - - (on-delete [] - (modal/show! confirm-dialog {:on-accept delete}))] - [:& common/grid-header {:value (:name coll) + [{:keys [collection] :as props}] + (let [{:keys [id type]} collection + on-change #(st/emit! (di/rename-collection id %)) + on-deleted #(st/emit! (rt/nav :dashboard-images nil {:type type})) + delete #(st/emit! (di/delete-collection id on-deleted)) + on-delete #(modal/show! confirm-dialog {:on-accept delete})] + [:& common/grid-header {:value (:name collection) :on-change on-change :on-delete on-delete}])) @@ -61,6 +48,7 @@ (let [local (mf/use-state {}) {:keys [id type name num-images]} coll editable? (= type :own) + on-click (fn [event] (let [type (or type :own)] @@ -68,6 +56,7 @@ on-cancel-edition #(swap! local dissoc :edit) on-double-click #(when editable? (swap! local assoc :edit true)) + on-input-keyup (fn [event] (when (kbd/enter? event) @@ -76,6 +65,7 @@ (str/trim))] (st/emit! (di/rename-collection id value)) (swap! local assoc :edit false))))] + [:li {:on-click on-click :on-double-click on-double-click :class-name (when selected? "current")} @@ -87,155 +77,128 @@ [:span.element-title (if id name "Storage")])])) (mf/defc nav - [{:keys [id type colls] :as props}] - (let [own? (= type :own) + [{:keys [id type collections] :as props}] + (let [locale (i18n/use-locale) + own? (= type :own) builtin? (= type :builtin) - select-tab #(st/emit! (rt/nav :dashboard-images nil {:type %}))] + create-collection #(st/emit! di/create-collection) + select-own-tab #(st/emit! (rt/nav :dashboard-images nil {:type :own})) + select-buitin-tab #(st/emit! (rt/nav :dashboard-images nil {:type :builtin}))] [:div.library-bar [:div.library-bar-inside - [:ul.library-tabs - [:li {:class-name (when own? "current") - :on-click (partial select-tab :own)} - (tr "ds.your-images-title")] - [:li {:class-name (when builtin? "current") - :on-click (partial select-tab :builtin)} - (tr "ds.store-images-title")]] + ;; Tabs + [:ul.library-tabs + [:li {:class (when own? "current") + :on-click select-own-tab} + (t locale "ds.your-images-title")] + + [:li {:class (when builtin? "current") + :on-click select-buitin-tab} + (t locale "ds.store-images-title")]] + + ;; Collections List [:ul.library-elements (when own? [:li - [:a.btn-primary {:on-click #(st/emit! di/create-collection)} - (tr "ds.images-collection.new")]]) - (when own? - [:& nav-item {:selected? (nil? id)}]) - (for [item colls] + [:a.btn-primary {:on-click create-collection} + (t locale "ds.images-collection.new")]]) + + (for [item collections] [:& nav-item {:coll item :selected? (= (:id item) id) :key (:id item)}])]]])) ;; --- Grid -(mf/defc grid-options-tooltip - [{:keys [selected on-select title] :as props}] - {:pre [(uuid? selected) - (fn? on-select) - (string? title)]} - (let [colls (mf/deref collections-iref) - colls (->> (vals colls) - (filter #(= :own (:type %))) - (remove #(= selected (:id %))) - #_(sort-by :name colls)) - on-select (fn [event id] - (dom/prevent-default event) - (dom/stop-propagation event) - (on-select id))] - [:ul.move-list - [:li.title title] - [:li - (when (not (nil? selected)) - [:a {:href "#" :on-click #(on-select % nil)} "Storage"])] - (for [{:keys [id name] :as coll} colls] - [:li {:key (pr-str id)} - [:a {:on-click #(on-select % id)} name]])])) +;; (mf/defc grid-options-tooltip +;; [{:keys [selected on-select title] :as props}] +;; {:pre [(uuid? selected) +;; (fn? on-select) +;; (string? title)]} +;; (let [colls (mf/deref collections-iref) +;; colls (->> (vals colls) +;; (filter #(= :own (:type %))) +;; (remove #(= selected (:id %))) +;; #_(sort-by :name colls)) +;; on-select (fn [event id] +;; (dom/prevent-default event) +;; (dom/stop-propagation event) +;; (on-select id))] +;; [:ul.move-list +;; [:li.title title] +;; [:li +;; (when (not (nil? selected)) +;; [:a {:href "#" :on-click #(on-select % nil)} "Storage"])] +;; (for [{:keys [id name] :as coll} colls] +;; [:li {:key (pr-str id)} +;; [:a {:on-click #(on-select % id)} name]])])) (mf/defc grid-options [{:keys [id type selected] :as props}] - (let [local (mf/use-state {})] - (letfn [(delete [] - (st/emit! (di/delete-selected))) - (on-delete [event] - (modal/show! confirm-dialog {:on-accept delete})) - (on-toggle-copy [event] - (swap! local update :show-copy-tooltip not)) - (on-toggle-move [event] - (swap! local update :show-move-tooltip not)) - (on-copy [selected] - (swap! local assoc - :show-move-tooltip false - :show-copy-tooltip false) - (st/emit! (di/copy-selected selected))) - (on-move [selected] - (swap! local assoc - :show-move-tooltip false - :show-copy-tooltip false) - (st/emit! (di/move-selected selected))) - (on-rename [event] - (let [selected (first selected)] - (st/emit! (di/update-opts :edition selected))))] - ;; MULTISELECT OPTIONS BAR - [:div.multiselect-bar - (if (or (= type :own) (nil? id)) - ;; If editable - [:div.multiselect-nav - [:span.move-item.tooltip.tooltip-top - {:alt (tr "ds.multiselect-bar.copy") - :on-click on-toggle-copy} - (when (:show-copy-tooltip @local) - [:& grid-options-tooltip {:selected id - :title (tr "ds.multiselect-bar.copy-to-library") - :on-select on-copy}]) - i/copy] - [:span.move-item.tooltip.tooltip-top - {:alt (tr "ds.multiselect-bar.move") - :on-click on-toggle-move} - (when (:show-move-tooltip @local) - [:& grid-options-tooltip {:selected id - :title (tr "ds.multiselect-bar.move-to-library") - :on-select on-move}]) - i/move] - (when (= 1 (count selected)) - [:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.rename") - :on-click on-rename} - i/pencil]) - [:span.delete.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.delete") - :on-click on-delete} - i/trash]] + (let [local (mf/use-state {}) + delete #(st/emit! di/delete-selected) + on-delete #(modal/show! confirm-dialog {:on-accept delete}) - ;; If not editable - [:div.multiselect-nav - [:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.copy") - :on-click on-toggle-copy} - (when (:show-copy-tooltip @local) - [:& grid-options-tooltip {:selected id - :title (tr "ds.multiselect-bar.copy-to-library") - :on-select on-copy}]) - i/organize]])]))) + ;; (on-toggle-copy [event] + ;; (swap! local update :show-copy-tooltip not)) + ;; (on-toggle-move [event] + ;; (swap! local update :show-move-tooltip not)) + ;; (on-copy [selected] + ;; (swap! local assoc + ;; :show-move-tooltip false + ;; :show-copy-tooltip false) + ;; (st/emit! (di/copy-selected selected))) + ;; (on-move [selected] + ;; (swap! local assoc + ;; :show-move-tooltip false + ;; :show-copy-tooltip false) + ;; (st/emit! (di/move-selected selected))) + ;; (on-rename [event] + ;; (let [selected (first selected)] + ;; (st/emit! (di/update-opts :edition selected)))) + ] + ;; MULTISELECT OPTIONS BAR + [:div.multiselect-bar + (when (= type :own) + ;; If editable + [:div.multiselect-nav + ;; [:span.move-item.tooltip.tooltip-top + ;; {:alt (tr "ds.multiselect-bar.copy") + ;; :on-click on-toggle-copy} + ;; (when (:show-copy-tooltip @local) + ;; [:& grid-options-tooltip {:selected id + ;; :title (tr "ds.multiselect-bar.copy-to-library") + ;; :on-select on-copy}]) + ;; i/copy] + ;; [:span.move-item.tooltip.tooltip-top + ;; {:alt (tr "ds.multiselect-bar.move") + ;; :on-click on-toggle-move} + ;; (when (:show-move-tooltip @local) + ;; [:& grid-options-tooltip {:selected id + ;; :title (tr "ds.multiselect-bar.move-to-library") + ;; :on-select on-move}]) + ;; i/move] + ;; (when (= 1 (count selected)) + ;; [:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.rename") + ;; :on-click on-rename} + ;; i/pencil]) + [:span.delete.tooltip.tooltip-top + {:alt (tr "ds.multiselect-bar.delete") + :on-click on-delete} + i/trash]] + + ;; If not editable + ;; [:div.multiselect-nav + ;; [:span.move-item.tooltip.tooltip-top {:alt (tr "ds.multiselect-bar.copy") + ;; :on-click on-toggle-copy} + ;; (when (:show-copy-tooltip @local) + ;; [:& grid-options-tooltip {:selected id + ;; :title (tr "ds.multiselect-bar.copy-to-library") + ;; :on-select on-copy}]) + ;; i/organize]] + )])) -(mf/defc grid-item - [{:keys [image selected? edition?] :as props}] - (letfn [(toggle-selection [event] - (st/emit! (di/toggle-image-selection (:id image)))) - (on-key-down [event] - (when (kbd/enter? event) - (on-blur event))) - (on-blur [event] - (let [target (dom/event->target event) - name (dom/get-value target)] - (st/emit! (di/update-opts :edition false) - (di/rename-image (:id image) name)))) - (on-edit [event] - (dom/stop-propagation event) - (dom/prevent-default event) - (st/emit! (di/update-opts :edition (:id image))))] - [:div.grid-item.images-th - [:div.grid-item-th {:style {:background-image (str "url('" (:thumbnail image) "')")}} - [:div.input-checkbox.check-primary - [:input {:type "checkbox" - :id (:id image) - :on-change toggle-selection - :checked selected?}] - [:label {:for (:id image)}]]] - [:div.item-info - (if edition? - [:input.element-name {:type "text" - :auto-focus true - :on-key-down on-key-down - :on-blur on-blur - :on-click on-edit - :default-value (:name image)}] - [:h3 {:on-double-click on-edit} (:name image)]) - [:span.date (str (tr "ds.uploaded-at" - (dt/format (:created-at image) "dd/MM/yyyy")))]]])) ;; --- Grid Form @@ -243,8 +206,9 @@ [{:keys [id type uploading?] :as props}] (let [input (mf/use-ref nil) on-click #(dom/click (mf/ref-node input)) - on-select #(st/emit! (->> (dom/get-event-files %) - (jscoll->vec) + on-select #(st/emit! (->> (dom/get-target %) + (dom/get-files) + (array-seq) (di/create-images id)))] [:div.grid-item.add-project {:on-click on-click} (if uploading? @@ -255,26 +219,76 @@ :multiple true :ref input :value "" - :accept "image/jpeg,image/png" + :accept "image/jpeg,image/png,image/webp" :type "file" :on-change on-select}]])) +;; --- Grid Item + +(mf/defc grid-item + [{:keys [image selected? edition?] :as props}] + (let [toggle-selection #(st/emit! (if selected? + (di/deselect-image (:id image)) + (di/select-image (:id image)))) + on-blur + (fn [event] + (let [target (dom/get-target event) + name (dom/get-value target)] + (st/emit! (di/update-opts :edition false) + (di/rename-image (:id image) name)))) + + on-key-down + (fn [event] + (when (kbd/enter? event) + (on-blur event))) + + on-edit + (fn [event] + (dom/stop-propagation event) + (dom/prevent-default event) + (st/emit! (di/update-opts :edition (:id image)))) + + background (str "url('" (:thumb-uri image) "')")] + + [:div.grid-item.images-th + [:div.grid-item-th {:style {:background-image background}} + [:div.input-checkbox.check-primary + [:input {:type "checkbox" + :id (:id image) + :on-change toggle-selection + :checked selected?}] + [:label {:for (:id image)}]]] + + [:div.item-info + (if edition? + [:input.element-name {:type "text" + :auto-focus true + :on-key-down on-key-down + :on-blur on-blur + :on-click on-edit + :default-value (:name image)}] + [:h3 {:on-double-click on-edit} (:name image)]) + [:span.date (tr "ds.uploaded-at" (dt/format (:created-at image) "dd/MM/yyyy"))]]])) + ;; --- Grid -(defn- make-images-iref - [id type] - (letfn [(selector-fn [state] - (let [images (vals (:images state))] - (filterv #(= id (:collection-id %)) images)))] - (-> (l/lens selector-fn) - (l/derive st/state)))) +;; (defn- make-images-iref +;; [collection-id] +;; (letfn [(selector [state] +;; (->> (vals (:images state)) +;; (filterv #(= (:collection-id %) collection-id))))] +;; (-> (l/lens selector) +;; (l/derive st/state)))) + +(def images-iref + (-> (comp (l/key :images) (l/lens vals)) + (l/derive st/state))) (mf/defc grid - [{:keys [id type coll opts] :as props}] - (let [editable? (or (= type :own) (nil? id)) - images-iref (mf/use-memo - {:fn #(make-images-iref id type) - :deps (mf/deps id type)}) + [{:keys [id type collection opts] :as props}] + (let [editable? (= type :own) + ;; images-iref (mf/use-memo {:fn #(make-images-iref id) + ;; :deps (mf/deps id)}) images (->> (mf/deref images-iref) (sort-by :created-at))] [:div.dashboard-grid-content @@ -328,45 +342,46 @@ ;; :value filtering}] ;; [:div.clear-search {:on-click on-clear} i/close]]]]))) -(mf/defc content - [{:keys [id type coll] :as props}] - (let [opts (mf/deref opts-iref)] - [:* - [:section.dashboard-grid.library - (when coll - [:& grid-header {:coll coll}]) +(def opts-iref + (-> (l/key :dashboard-images) + (l/derive st/state))) - [:& grid {:id id - :type type - :coll coll - :opts opts}] - (when (seq (:selected opts)) - [:& grid-options {:id id :type type :selected (:selected opts)}])]])) +(mf/defc content + [{:keys [id type collection] :as props}] + (let [{:keys [selected] :as opts} (mf/deref opts-iref)] + [:section.dashboard-grid.library + (when collection + [:& grid-header {:collection collection}]) + (if collection + [:& grid {:id id :type type :collection collection :opts opts}] + [:span "EMPTY STATE TODO"]) + (when-not (empty? selected) + [:& grid-options {:id id :type type :selected selected}])])) ;; --- Images Page +(def collections-iref + (-> (l/key :images-collections) + (l/derive st/state))) + (mf/defc images-page [{:keys [id type] :as props}] - (let [colls (mf/deref collections-iref) - colls (cond->> (vals colls) - (= type :own) (filter #(= :own (:type %))) - (= type :builtin) (filter #(= :builtin (:type %))) - true (sort-by :created-at)) + (let [collections (mf/deref collections-iref) + collections (cond->> (vals collections) + (= type :own) (filter #(= :own (:type %))) + (= type :builtin) (filter #(= :builtin (:type %))) + true (sort-by :created-at)) - coll (cond - (and (= type :own) (nil? id)) nil - (uuid? id) (seek #(= id (:id %)) colls) - :else (first colls)) - id (:id coll)] + collection (cond + (uuid? id) (d/seek #(= id (:id %)) collections) + :else (first collections)) + id (:id collection)] (mf/use-effect #(st/emit! di/fetch-collections)) - (mf/use-effect {:fn #(st/emit! (di/fetch-images (:id coll))) - :deps #js [(str (:id coll))]}) + (mf/use-effect + {:fn #(when id (st/emit! (di/initialize id))) + :deps (mf/deps id)}) [:section.dashboard-content - [:& nav {:type type - :id id - :colls colls}] - [:& content {:type type - :id id - :coll coll}]])) + [:& nav {:type type :id id :collections collections}] + [:& content {:type type :id id :collection collection}]])) diff --git a/frontend/src/uxbox/main/ui/shapes/image.cljs b/frontend/src/uxbox/main/ui/shapes/image.cljs index fd61bbbea..1bc3a7524 100644 --- a/frontend/src/uxbox/main/ui/shapes/image.cljs +++ b/frontend/src/uxbox/main/ui/shapes/image.cljs @@ -6,7 +6,6 @@ (ns uxbox.main.ui.shapes.image (:require - [lentes.core :as l] [rumext.alpha :as mf] [cuerdas.core :as str] [uxbox.main.data.images :as udi] @@ -17,13 +16,6 @@ [uxbox.main.ui.shapes.common :as common] [uxbox.util.geom.matrix :as gmt])) -;; --- Refs - -(defn image-ref - [id] - (-> (l/in [:images id]) - (l/derive st/state))) - ;; --- Image Wrapper (declare image-shape) @@ -31,23 +23,17 @@ (mf/defc image-wrapper [{:keys [shape] :as props}] (let [selected (mf/deref refs/selected-shapes) - image (mf/deref (image-ref (:image shape))) selected? (contains? selected (:id shape)) on-mouse-down #(common/on-mouse-down % shape selected)] - - (mf/use-effect #(st/emit! (udi/fetch-image (:image shape)))) - - (when image - [:g.shape {:class (when selected? "selected") - :on-mouse-down on-mouse-down} - [:& image-shape {:shape shape - :image image}]]))) + [:g.shape {:class (when selected? "selected") + :on-mouse-down on-mouse-down} + [:& image-shape {:shape shape}]])) ;; --- Image Shape (mf/defc image-shape - [{:keys [shape image] :as props}] - (let [{:keys [id rotation modifier-mtx]} shape + [{:keys [shape] :as props}] + (let [{:keys [id rotation modifier-mtx metadata]} shape shape (cond (gmt/matrix? modifier-mtx) (geom/transform shape modifier-mtx) @@ -60,13 +46,18 @@ rotation (+ x (/ width 2)) (+ y (/ height 2)))) + uri (if (or (> (:thumb-width metadata) width) + (> (:thumb-height metadata) height)) + (:thumb-uri metadata) + (:uri metadata)) + props (-> (attrs/extract-style-attrs shape) (assoc :x x :y y :id (str "shape-" id) :preserveAspectRatio "none" - :xlinkHref (:url image) + :xlinkHref uri :transform transform :width width :height height))] diff --git a/frontend/src/uxbox/main/ui/workspace/images.cljs b/frontend/src/uxbox/main/ui/workspace/images.cljs index 0091eaf0c..d2cc8b460 100644 --- a/frontend/src/uxbox/main/ui/workspace/images.cljs +++ b/frontend/src/uxbox/main/ui/workspace/images.cljs @@ -11,13 +11,14 @@ [rumext.alpha :as mf] [rumext.core :as mx] [uxbox.builtins.icons :as i] + [uxbox.common.data :as d] [uxbox.main.data.images :as udi] [uxbox.main.data.workspace :as dw] [uxbox.main.store :as st] + [uxbox.main.refs :as refs] [uxbox.main.ui.modal :as modal] - [uxbox.util.data :refer [read-string jscoll->vec]] [uxbox.util.dom :as dom] - [uxbox.util.i18n :as t :refer [tr]] + [uxbox.util.i18n :as i18n :refer [tr t]] [uxbox.util.uuid :as uuid])) ;; --- Refs @@ -30,8 +31,13 @@ (-> (l/key :images) (l/derive st/state))) +(def ^:private workspace-images-iref + (-> (comp (l/key :workspace-images) + (l/lens vals)) + (l/derive st/state))) + (def ^:private uploading-iref - (-> (l/in [:dashboard :images :uploading]) + (-> (l/in [:workspace-local :uploading]) (l/derive st/state))) ;; --- Import Image Modal @@ -41,116 +47,152 @@ (mf/defc import-image-modal [props] (let [input (mf/use-ref nil) - uploading? (mf/deref uploading-iref)] - (letfn [(on-upload-click [event] - (let [input-el (mf/ref-node input)] - (dom/click input-el))) + uploading? (mf/deref uploading-iref) - (on-uploaded [[image]] - (let [{:keys [id name width height]} image - shape {:name name - :metadata {:width width - :height height} - :image id}] - (st/emit! (dw/select-for-drawing :image shape)) - (modal/hide!))) + on-upload-click #(dom/click (mf/ref-node input)) - (on-files-selected [event] - (let [files (dom/get-event-files event) - files (jscoll->vec files)] - (st/emit! (udi/create-images nil files on-uploaded)))) + on-uploaded + (fn [{:keys [id name] :as image}] + (let [shape {:name name + :metadata {:width (:width image) + :height (:height image) + :uri (:uri image) + :thumb-width (:thumb-width image) + :thumb-height (:thumb-height image) + :thumb-uri (:thumb-uri image)}}] + (st/emit! (dw/select-for-drawing :image shape)) + (modal/hide!))) - (on-select-from-library [event] - (dom/prevent-default event) - (modal/show! import-image-from-coll-modal {})) + on-files-selected + (fn [event] + (st/emit! (-> (dom/get-target event) + (dom/get-files) + (array-seq) + (first) + (dw/upload-image on-uploaded)))) - (on-close [event] - (dom/prevent-default event) - (modal/hide!))] - [:div.lightbox-body - [:h3 (tr "image.new")] - [:div.row-flex - [:div.lightbox-big-btn {:on-click on-select-from-library} - [:span.big-svg i/image] - [:span.text (tr "image.select")]] - [:div.lightbox-big-btn {:on-click on-upload-click} - (if uploading? - [:span.big-svg.upload i/loader-pencil] - [:span.big-svg.upload i/exit]) - [:span.text (tr "image.upload")] - [:input.upload-image-input - {:style {:display "none"} - :accept "image/jpeg,image/png" - :type "file" - :ref input - :on-change on-files-selected}]]] - [:a.close {:on-click on-close} i/close]]))) + on-select-from-library + (fn [event] + (dom/prevent-default event) + (modal/show! import-image-from-coll-modal {})) + + on-close + (fn [event] + (dom/prevent-default event) + (modal/hide!))] + [:div.lightbox-body + [:h3 (tr "image.new")] + [:div.row-flex + + ;; Select from collections + [:div.lightbox-big-btn {:on-click on-select-from-library} + [:span.big-svg i/image] + [:span.text (tr "image.select")]] + + ;; Select from workspace + [:div.lightbox-big-btn {:on-click on-select-from-library} + [:span.big-svg i/image] + [:span.text (tr "image.select")]] + + ;; Direct image upload + [:div.lightbox-big-btn {:on-click on-upload-click} + (if uploading? + [:span.big-svg.upload i/loader-pencil] + [:span.big-svg.upload i/exit]) + [:span.text (tr "image.upload")] + [:input.upload-image-input + {:style {:display "none"} + :multiple false + :accept "image/jpeg,image/png,image/webp" + :type "file" + :ref input + :on-change on-files-selected}]]] + [:a.close {:on-click on-close} i/close]])) ;; --- Import Image from Collection Modal (mf/defc image-item [{:keys [image] :as props}] (letfn [(on-click [event] + ;; TODO: deduplicate this code... (let [shape {:name (:name image) :metadata {:width (:width image) - :height (:height image)} - :image (:id image)}] + :height (:height image) + :uri (:uri image) + :thumb-width (:thumb-width image) + :thumb-height (:thumb-height image) + :thumb-uri (:thumb-uri image)}}] (st/emit! (dw/select-for-drawing :image shape)) (modal/hide!)))] [:div.library-item {:on-click on-click} [:div.library-item-th - {:style {:background-image (str "url('" (:thumbnail image) "')")}}] + {:style {:background-image (str "url('" (:thumb-uri image) "')")}}] [:span (:name image)]])) -(mf/defc image-collection - [{:keys [images] :as props}] - [:div.library-content - (for [image images] - [:& image-item {:image image :key (:id image)}])]) - (mf/defc import-image-from-coll-modal [props] - (let [local (mf/use-state {:id nil :type :own}) - id (:id @local) - type (:type @local) - own? (= type :own) - builtin? (= type :builtin) - colls (mf/deref collections-iref) - colls (->> (vals colls) - (filter #(= type (:type %))) - (sort-by :name)) + (let [locale (i18n/use-locale) + local (mf/use-state {:collection-id nil :tab :file}) + + collections (mf/deref collections-iref) + collections (->> (vals collections) + (sort-by :name)) + + select-tab #(swap! local assoc :tab %) + + collection-id (or (:collection-id @local) + (:id (first collections))) + + tab (:tab @local) + + images (mf/deref images-iref) images (->> (vals images) - (filter #(= id (:collection %)))) + (filter #(= collection-id (:collection-id %)))) + + workspace-images (mf/deref workspace-images-iref) + on-close #(do (dom/prevent-default %) (modal/hide!)) - select-type #(swap! local assoc :type %) - on-change #(-> (dom/event->value %) - (read-string) - (swap! local assoc :id))] + + on-change #(->> (dom/get-target %) + (dom/get-value) + (d/read-string) + (swap! local assoc :collection-id))] (mf/use-effect #(st/emit! udi/fetch-collections)) - (mf/use-effect {:deps #js [(str id)] - :fn #(st/emit! (udi/fetch-images id))}) + (mf/use-effect + {:deps (mf/deps collection-id) + :fn #(when collection-id + (st/emit! (udi/fetch-images collection-id)))}) [:div.lightbox-body.big-lightbox [:h3 (tr "image.import-library")] [:div.import-img-library [:div.library-actions - [:ul.toggle-library - [:li.your-images {:class (when own? "current") - :on-click #(select-type :own)} - (tr "ds.your-images-title")] - [:li.standard {:class (when builtin? "current") - :on-click #(select-type :builtin)} - (tr "ds.store-images-title")]] - [:select.input-select {:on-change on-change} - (when own? - [:option {:value (pr-str nil)} "Storage"]) - (for [coll colls] - (let [id (:id coll) - name (:name coll)] - [:option {:key (str id) :value (pr-str id)} name]))]] - [:& image-collection {:images images}]] + ;; Tabs + [:ul.toggle-library + [:li.your-images {:class (when (= tab :file) "current") + :on-click #(select-tab :file)} + (t locale "ds.your-images-title")] + [:li.standard {:class (when (not= tab :file) "current") + :on-click #(select-tab :collection)} + (t locale "ds.store-images-title")]] + + ;; Collections dropdown + (when (= tab :collection) + [:select.input-select {:on-change on-change} + (for [coll collections] + (let [id (:id coll) + name (:name coll)] + [:option {:key (str id) :value (pr-str id)} name]))])] + + (if (= tab :collection) + [:div.library-content + (for [image images] + [:& image-item {:image image :key (:id image)}])] + [:div.library-content + (for [image workspace-images] + [:& image-item {:image image :key (:id image)}])])] [:a.close {:href "#" :on-click on-close} i/close]])) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs index 99e2c1222..1b9b474a2 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs @@ -40,10 +40,7 @@ (mf/defc icons-list [{:keys [collection-id] :as props}] - (let [icons-iref (mf/use-memo - {:fn #(icons/make-icons-iref collection-id) - :deps (mf/deps collection-id)}) - icons (mf/deref icons-iref) + (let [icons (mf/deref icons/icons-iref) on-select (fn [event data] diff --git a/frontend/src/uxbox/util/blob.cljs b/frontend/src/uxbox/util/blob.cljs index 2ae1866b4..ad555e70c 100644 --- a/frontend/src/uxbox/util/blob.cljs +++ b/frontend/src/uxbox/util/blob.cljs @@ -7,6 +7,8 @@ (ns uxbox.util.blob "Helpers for work with HTML5 Blob objects.") +;; TODO: DEPRECATED + (defn ^boolean blob? [v] (instance? js/Blob v)) diff --git a/frontend/src/uxbox/util/dom.cljs b/frontend/src/uxbox/util/dom.cljs index 76737df23..4d9100302 100644 --- a/frontend/src/uxbox/util/dom.cljs +++ b/frontend/src/uxbox/util/dom.cljs @@ -11,7 +11,10 @@ (ns uxbox.util.dom (:require [goog.dom :as dom] - [cuerdas.core :as str])) + [cuerdas.core :as str] + [beicon.core :as rx] + [cuerdas.core :as str] + [uxbox.util.blob :as blob])) ;; --- Deprecated methods @@ -131,3 +134,5 @@ (defn query [el query] (.querySelector el query)) + + diff --git a/frontend/src/uxbox/util/files.cljs b/frontend/src/uxbox/util/files.cljs index 4f17bc1d8..767fdd7af 100644 --- a/frontend/src/uxbox/util/files.cljs +++ b/frontend/src/uxbox/util/files.cljs @@ -10,6 +10,8 @@ [cuerdas.core :as str] [uxbox.util.blob :as blob])) +;; TODO: DEPRECATED + (defn read-as-text [file] (rx/create diff --git a/frontend/src/uxbox/util/webapi.cljs b/frontend/src/uxbox/util/webapi.cljs new file mode 100644 index 000000000..e079e75e6 --- /dev/null +++ b/frontend/src/uxbox/util/webapi.cljs @@ -0,0 +1,68 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) 2020 Andrey Antukh + +(ns uxbox.util.webapi + "HTML5 web api helpers." + (:require + [beicon.core :as rx] + [cuerdas.core :as str])) + +(defn read-file-as-text + [file] + (rx/create + (fn [sink] + (let [fr (js/FileReader.)] + (aset fr "onload" #(sink (rx/end (.-result fr)))) + (.readAsText fr file) + (constantly nil))))) + +(defn read-file-as-dataurl + [file] + (rx/create + (fn [sick] + (let [fr (js/FileReader.)] + (aset fr "onload" #(sick (rx/end (.-result fr)))) + (.readAsDataURL fr file)) + (constantly nil)))) + +(defn ^boolean blob? + [v] + (instance? js/Blob v)) + +(defn create-blob + "Create a blob from content." + ([content] + (create-blob content "application/octet-stream")) + ([content mtype] + (js/Blob. #js [content] #js {:type mtype}))) + +(defn revoke-uri + [url] + (assert (string? url) "invalid arguments") + (js/URL.revokeObjectURL url)) + +(defn create-uri + "Create a url from blob." + [b] + (assert (blob? b) "invalid arguments") + (js/URL.createObjectURL b)) + + +;; (defn get-image-size +;; [file] +;; (letfn [(on-load [sink img] +;; (let [size [(.-width img) (.-height img)]] +;; (sink (rx/end size)))) +;; (on-subscribe [sink] +;; (let [img (js/Image.) +;; uri (blob/create-uri file)] +;; (set! (.-onload img) (partial on-load sink img)) +;; (set! (.-src img) uri) +;; #(blob/revoke-uri uri)))] +;; (rx/create on-subscribe))) + + +