mirror of
https://github.com/penpot/penpot.git
synced 2025-04-04 19:11:20 -05:00
♻️ Refactor images storage.
This commit is contained in:
parent
b98d8519d4
commit
2cebbbc2f8
34 changed files with 2032 additions and 1630 deletions
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))))))
|
||||
|
|
|
@ -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 <niwi@niwi.nz>
|
||||
;; 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 <niwi@niwi.nz>
|
||||
|
||||
(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)))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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))))))))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))))))
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#kaocha/v1
|
||||
{:tests
|
||||
[{:id :unit
|
||||
:test-paths ["test" "src"]
|
||||
:test-paths ["tests" "src"]
|
||||
:ns-patterns ["test-.*"]}]}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))))))))
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]))))))
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
|
|
@ -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 <niwi@niwi.nz>
|
||||
|
||||
(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)))
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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}]]))
|
||||
|
|
|
@ -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 <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
;; 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 <niwi@niwi.nz>
|
||||
;; Copyright (c) 2015-2020 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
(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}]]))
|
||||
|
|
|
@ -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))]
|
||||
|
|
|
@ -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]]))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
[cuerdas.core :as str]
|
||||
[uxbox.util.blob :as blob]))
|
||||
|
||||
;; TODO: DEPRECATED
|
||||
|
||||
(defn read-as-text
|
||||
[file]
|
||||
(rx/create
|
||||
|
|
68
frontend/src/uxbox/util/webapi.cljs
Normal file
68
frontend/src/uxbox/util/webapi.cljs
Normal file
|
@ -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 <niwi@niwi.nz>
|
||||
|
||||
(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)))
|
||||
|
||||
|
||||
|
Loading…
Add table
Reference in a new issue