mirror of
https://github.com/penpot/penpot.git
synced 2025-03-18 10:41:29 -05:00
commit
a38430fb80
39 changed files with 1512 additions and 459 deletions
|
@ -0,0 +1,29 @@
|
|||
-- ALTER TABLE color_library
|
||||
-- DROP COLUMN team_id,
|
||||
-- DROP COLUMN name,
|
||||
-- ADD COLUMN file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE;
|
||||
--
|
||||
-- CREATE INDEX color_library__file_id__idx
|
||||
-- ON color_library(file_id);
|
||||
|
||||
TRUNCATE TABLE color;
|
||||
TRUNCATE TABLE color_library CASCADE;
|
||||
TRUNCATE TABLE image;
|
||||
TRUNCATE TABLE image_library CASCADE;
|
||||
TRUNCATE TABLE icon;
|
||||
TRUNCATE TABLE icon_library CASCADE;
|
||||
|
||||
ALTER TABLE color
|
||||
DROP COLUMN library_id,
|
||||
ADD COLUMN file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE;
|
||||
|
||||
CREATE INDEX color__file_id__idx
|
||||
ON color(file_id);
|
||||
|
||||
ALTER TABLE image
|
||||
DROP COLUMN library_id,
|
||||
ADD COLUMN file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE;
|
||||
|
||||
CREATE INDEX image__file_id__idx
|
||||
ON image(file_id);
|
||||
|
2
backend/scripts/psql.sh
Executable file
2
backend/scripts/psql.sh
Executable file
|
@ -0,0 +1,2 @@
|
|||
#!/usr/bin/env bash
|
||||
PGPASSWORD=$UXBOX_DATABASE_PASSWORD psql $UXBOX_DATABASE_URI -U $UXBOX_DATABASE_USERNAME
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
(s/def ::width integer?)
|
||||
(s/def ::height integer?)
|
||||
(s/def ::format #{:jpeg :webp :png})
|
||||
(s/def ::format #{:jpeg :webp :png :svg})
|
||||
(s/def ::quality #(< 0 % 101))
|
||||
|
||||
(s/def ::thumbnail-params
|
||||
|
@ -62,21 +62,24 @@
|
|||
(case format
|
||||
:png ".png"
|
||||
:jpeg ".jpg"
|
||||
:webp ".webp"))
|
||||
:webp ".webp"
|
||||
:svg ".svg"))
|
||||
|
||||
(defn format->mtype
|
||||
[format]
|
||||
(case format
|
||||
:png "image/png"
|
||||
:jpeg "image/jpeg"
|
||||
:webp "image/webp"))
|
||||
:webp "image/webp"
|
||||
:svg "image/svg+xml"))
|
||||
|
||||
(defn mtype->format
|
||||
[mtype]
|
||||
(case mtype
|
||||
"image/jpeg" :jpeg
|
||||
"image/webp" :webp
|
||||
"image/png" :png
|
||||
"image/png" :png
|
||||
"image/jpeg" :jpeg
|
||||
"image/webp" :webp
|
||||
"image/svg+xml" :svg
|
||||
nil))
|
||||
|
||||
(defn- generic-process
|
||||
|
@ -127,18 +130,21 @@
|
|||
(defmethod process :info
|
||||
[{:keys [input] :as params}]
|
||||
(us/assert ::input input)
|
||||
(let [{:keys [path mtype]} input
|
||||
instance (Info. (str path))
|
||||
mtype' (.getProperty instance "Mime type")]
|
||||
|
||||
(when (and (string? mtype)
|
||||
(not= mtype mtype'))
|
||||
(ex/raise :type :validation
|
||||
:code :image-type-mismatch
|
||||
:hint "Seems like you are uploading a file whose content does not match the extension."))
|
||||
{:width (.getImageWidth instance)
|
||||
:height (.getImageHeight instance)
|
||||
:mtype mtype'}))
|
||||
(let [{:keys [path mtype]} input]
|
||||
(if (= mtype "image/svg+xml")
|
||||
{:width 100
|
||||
:height 100
|
||||
:mtype mtype}
|
||||
(let [instance (Info. (str path))
|
||||
mtype' (.getProperty instance "Mime type")]
|
||||
(when (and (string? mtype)
|
||||
(not= mtype mtype'))
|
||||
(ex/raise :type :validation
|
||||
:code :image-type-mismatch
|
||||
:hint "Seems like you are uploading a file whose content does not match the extension."))
|
||||
{:width (.getImageWidth instance)
|
||||
:height (.getImageHeight instance)
|
||||
:mtype mtype'}))))
|
||||
|
||||
(defmethod process :default
|
||||
[{:keys [cmd] :as params}]
|
||||
|
@ -164,13 +170,15 @@
|
|||
(defn resolve-urls
|
||||
[row src dst]
|
||||
(s/assert map? row)
|
||||
(let [src (if (vector? src) src [src])
|
||||
dst (if (vector? dst) dst [dst])
|
||||
value (get-in row src)]
|
||||
(if (empty? value)
|
||||
row
|
||||
(let [url (ust/public-uri media/media-storage value)]
|
||||
(assoc-in row dst (str url))))))
|
||||
(if (and src dst)
|
||||
(let [src (if (vector? src) src [src])
|
||||
dst (if (vector? dst) dst [dst])
|
||||
value (get-in row src)]
|
||||
(if (empty? value)
|
||||
row
|
||||
(let [url (ust/public-uri media/media-storage value)]
|
||||
(assoc-in row dst (str url)))))
|
||||
row))
|
||||
|
||||
(defn- resolve-uri
|
||||
[storage row src dst]
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
[uxbox.util.blob :as blob]
|
||||
[uxbox.common.uuid :as uuid]
|
||||
[uxbox.util.data :as data]
|
||||
[uxbox.services.mutations.projects :as projects]
|
||||
[uxbox.services.mutations.files :as files]
|
||||
[uxbox.services.mutations.colors :as colors]
|
||||
[uxbox.services.mutations.icons :as icons]
|
||||
[uxbox.services.mutations.images :as images]
|
||||
|
@ -49,120 +51,243 @@
|
|||
(s/def ::path ::us/string)
|
||||
(s/def ::regex #(instance? java.util.regex.Pattern %))
|
||||
|
||||
(s/def ::colors
|
||||
;; (s/def ::colors
|
||||
;; (s/* (s/cat :name ::us/string :color ::us/color)))
|
||||
;;
|
||||
;; (s/def ::import-item-media
|
||||
;; (s/keys :req-un [::name ::path ::regex]))
|
||||
;;
|
||||
;; (s/def ::import-item-color
|
||||
;; (s/keys :req-un [::name ::id ::colors]))
|
||||
|
||||
(s/def ::import-images
|
||||
(s/keys :req-un [::path ::regex]))
|
||||
|
||||
(s/def ::import-color
|
||||
(s/* (s/cat :name ::us/string :color ::us/color)))
|
||||
|
||||
(s/def ::import-item-media
|
||||
(s/keys :req-un [::name ::path ::regex]))
|
||||
(s/def ::import-colors (s/coll-of ::import-color))
|
||||
|
||||
(s/def ::import-item-color
|
||||
(s/keys :req-un [::name ::id ::colors]))
|
||||
(s/def ::import-library
|
||||
(s/keys :req-un [::name]
|
||||
:opt-un [::import-images ::import-colors]))
|
||||
|
||||
(defn exit!
|
||||
([] (exit! 0))
|
||||
([code]
|
||||
(System/exit code)))
|
||||
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ;; Icons Libraries Importer
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; (defn- icon-library-exists?
|
||||
;; [conn id]
|
||||
;; (s/assert ::us/uuid id)
|
||||
;; (let [row (db/get-by-id conn :icon-library id)]
|
||||
;; (if row true false)))
|
||||
;;
|
||||
;; (defn- create-icons-library
|
||||
;; [conn {:keys [name] :as item}]
|
||||
;; (let [id (uuid/namespaced +icons-uuid-ns+ name)]
|
||||
;; (log/info "Creating icons library:" name)
|
||||
;; (icons/create-library conn {:team-id uuid/zero
|
||||
;; :id id
|
||||
;; :name name})))
|
||||
;;
|
||||
;; (defn- create-icons-library-if-not-exists
|
||||
;; [conn {:keys [name] :as item}]
|
||||
;; (let [id (uuid/namespaced +icons-uuid-ns+ name)]
|
||||
;; (when-not (icon-library-exists? conn id)
|
||||
;; (create-icons-library conn item))
|
||||
;; id))
|
||||
;;
|
||||
;; (defn- create-icon
|
||||
;; [conn library-id icon-id localpath]
|
||||
;; (s/assert fs/path? localpath)
|
||||
;; (s/assert ::us/uuid library-id)
|
||||
;; (s/assert ::us/uuid icon-id)
|
||||
;; (let [filename (fs/name localpath)
|
||||
;; extension (second (fs/split-ext filename))
|
||||
;; data (svg/parse localpath)
|
||||
;; mdata (select-keys data [:width :height :view-box])]
|
||||
;;
|
||||
;; (log/info "Creating or updating icon" filename icon-id)
|
||||
;; (icons/create-icon conn {:id icon-id
|
||||
;; :library-id library-id
|
||||
;; :name (:name data filename)
|
||||
;; :content (:content data)
|
||||
;; :metadata mdata})))
|
||||
;;
|
||||
;; (defn- icon-exists?
|
||||
;; [conn id]
|
||||
;; (s/assert ::us/uuid id)
|
||||
;; (let [row (db/get-by-id conn :icon id)]
|
||||
;; (if row true false)))
|
||||
;;
|
||||
;; (defn- import-icon-if-not-exists
|
||||
;; [conn library-id fpath]
|
||||
;; (s/assert ::us/uuid library-id)
|
||||
;; (s/assert fs/path? fpath)
|
||||
;; (let [icon-id (uuid/namespaced +icons-uuid-ns+ (str library-id (fs/name fpath)))]
|
||||
;; (when-not (icon-exists? conn icon-id)
|
||||
;; (create-icon conn library-id icon-id fpath))
|
||||
;; icon-id))
|
||||
;;
|
||||
;; (defn- import-icons
|
||||
;; [conn library-id {:keys [path regex] :as item}]
|
||||
;; (run! (fn [fpath]
|
||||
;; (when (re-matches regex (str fpath))
|
||||
;; (import-icon-if-not-exists conn library-id fpath)))
|
||||
;; (->> (fs/list-dir path)
|
||||
;; (filter fs/regular-file?))))
|
||||
;;
|
||||
;; (defn- process-icons-library
|
||||
;; [conn basedir {:keys [path regex] :as item}]
|
||||
;; (s/assert ::import-item-media item)
|
||||
;; (let [library-id (create-icons-library-if-not-exists conn item)]
|
||||
;; (->> (assoc item :path (fs/join basedir path))
|
||||
;; (import-icons conn library-id))))
|
||||
;;
|
||||
;;
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ;; --- Images Libraries Importer
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; (defn- image-library-exists?
|
||||
;; [conn id]
|
||||
;; (s/assert ::us/uuid id)
|
||||
;; (let [row (db/get-by-id conn :image-library id)]
|
||||
;; (if row true false)))
|
||||
;;
|
||||
;; (defn- create-images-library
|
||||
;; [conn {:keys [name] :as item}]
|
||||
;; (let [id (uuid/namespaced +images-uuid-ns+ name)]
|
||||
;; (log/info "Creating image library:" name)
|
||||
;; (images/create-library conn {:id id
|
||||
;; :team-id uuid/zero
|
||||
;; :name name})))
|
||||
;;
|
||||
;; (defn- create-images-library-if-not-exists
|
||||
;; [conn {:keys [name] :as item}]
|
||||
;; (let [id (uuid/namespaced +images-uuid-ns+ name)]
|
||||
;; (when-not (image-library-exists? conn id)
|
||||
;; (create-images-library conn item)
|
||||
;; id)))
|
||||
;;
|
||||
;; (defn- create-image
|
||||
;; [conn library-id image-id localpath]
|
||||
;; (s/assert fs/path? localpath)
|
||||
;; (s/assert ::us/uuid library-id)
|
||||
;; (s/assert ::us/uuid image-id)
|
||||
;; (let [filename (fs/name localpath)
|
||||
;; extension (second (fs/split-ext filename))
|
||||
;; file (io/as-file localpath)
|
||||
;; mtype (case extension
|
||||
;; ".jpg" "image/jpeg"
|
||||
;; ".png" "image/png"
|
||||
;; ".webp" "image/webp")]
|
||||
;; (log/info "Creating image" filename image-id)
|
||||
;; (images/create-image conn {:content {:tempfile localpath
|
||||
;; :filename filename
|
||||
;; :content-type mtype
|
||||
;; :size (.length file)}
|
||||
;; :id image-id
|
||||
;; :library-id library-id
|
||||
;; :user uuid/zero
|
||||
;; :name filename})))
|
||||
;;
|
||||
;; (defn- image-exists?
|
||||
;; [conn id]
|
||||
;; (s/assert ::us/uuid id)
|
||||
;; (let [row (db/get-by-id conn :image id)]
|
||||
;; (if row true false)))
|
||||
;;
|
||||
;; (defn- import-image-if-not-exists
|
||||
;; [conn library-id fpath]
|
||||
;; (s/assert ::us/uuid library-id)
|
||||
;; (s/assert fs/path? fpath)
|
||||
;; (let [image-id (uuid/namespaced +images-uuid-ns+ (str library-id (fs/name fpath)))]
|
||||
;; (when-not (image-exists? conn image-id)
|
||||
;; (create-image conn library-id image-id fpath))
|
||||
;; image-id))
|
||||
;;
|
||||
;; (defn- import-images
|
||||
;; [conn library-id {:keys [path regex] :as item}]
|
||||
;; (run! (fn [fpath]
|
||||
;; (when (re-matches regex (str fpath))
|
||||
;; (import-image-if-not-exists conn library-id fpath)))
|
||||
;; (->> (fs/list-dir path)
|
||||
;; (filter fs/regular-file?))))
|
||||
;;
|
||||
;; (defn- process-images-library
|
||||
;; [conn basedir {:keys [path regex] :as item}]
|
||||
;; (s/assert ::import-item-media item)
|
||||
;; (let [library-id (create-images-library-if-not-exists conn item)]
|
||||
;; (->> (assoc item :path (fs/join basedir path))
|
||||
;; (import-images conn library-id))))
|
||||
;;
|
||||
;;
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; ;; Colors Libraries Importer
|
||||
;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;
|
||||
;; (defn- color-library-exists?
|
||||
;; [conn id]
|
||||
;; (s/assert ::us/uuid id)
|
||||
;; (let [row (db/get-by-id conn :file id)]
|
||||
;; (if row true false)))
|
||||
;;
|
||||
;; (defn- create-colors-library
|
||||
;; [conn {:keys [name] :as item}]
|
||||
;; (let [id (uuid/namespaced +colors-uuid-ns+ name)]
|
||||
;; (log/info "Creating color library:" name)
|
||||
;; (colors/create-library conn {:id id
|
||||
;; :team-id uuid/zero
|
||||
;; :name name})))
|
||||
;;
|
||||
;;
|
||||
;; (defn- create-colors-library-if-not-exists
|
||||
;; [conn {:keys [name] :as item}]
|
||||
;; (let [id (uuid/namespaced +colors-uuid-ns+ name)]
|
||||
;; (when-not (color-library-exists? conn id)
|
||||
;; (create-colors-library conn item))
|
||||
;; id))
|
||||
;;
|
||||
;; (defn- create-color
|
||||
;; [conn library-id name content]
|
||||
;; (s/assert ::us/uuid library-id)
|
||||
;; (s/assert ::us/color content)
|
||||
;; (let [color-id (uuid/namespaced +colors-uuid-ns+ (str library-id content))]
|
||||
;; (log/info "Creating color" color-id "-" name content)
|
||||
;; (colors/create-color conn {:id color-id
|
||||
;; :library-id library-id
|
||||
;; :name name
|
||||
;; :content content})
|
||||
;; color-id))
|
||||
;;
|
||||
;; (defn- import-colors
|
||||
;; [conn library-id {:keys [colors] :as item}]
|
||||
;; (db/delete! conn :color {:library-id library-id})
|
||||
;; (run! (fn [[name content]]
|
||||
;; (create-color conn library-id name content))
|
||||
;; (partition-all 2 colors)))
|
||||
;;
|
||||
;; (defn- process-colors-library
|
||||
;; [conn {:keys [name id colors] :as item}]
|
||||
;; (us/verify ::import-item-color item)
|
||||
;; (let [library-id (create-colors-library-if-not-exists conn item)]
|
||||
;; (import-colors conn library-id item)))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Icons Libraries Importer
|
||||
;; Images Importer
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- icon-library-exists?
|
||||
[conn id]
|
||||
(s/assert ::us/uuid id)
|
||||
(let [row (db/get-by-id conn :icon-library id)]
|
||||
(if row true false)))
|
||||
|
||||
(defn- create-icons-library
|
||||
[conn {:keys [name] :as item}]
|
||||
(let [id (uuid/namespaced +icons-uuid-ns+ name)]
|
||||
(log/info "Creating icons library:" name)
|
||||
(icons/create-library conn {:team-id uuid/zero
|
||||
:id id
|
||||
:name name})))
|
||||
|
||||
(defn- create-icons-library-if-not-exists
|
||||
[conn {:keys [name] :as item}]
|
||||
(let [id (uuid/namespaced +icons-uuid-ns+ name)]
|
||||
(when-not (icon-library-exists? conn id)
|
||||
(create-icons-library conn item))
|
||||
id))
|
||||
|
||||
(defn- create-icon
|
||||
[conn library-id icon-id localpath]
|
||||
(s/assert fs/path? localpath)
|
||||
(s/assert ::us/uuid library-id)
|
||||
(s/assert ::us/uuid icon-id)
|
||||
(let [filename (fs/name localpath)
|
||||
extension (second (fs/split-ext filename))
|
||||
data (svg/parse localpath)
|
||||
mdata (select-keys data [:width :height :view-box])]
|
||||
|
||||
(log/info "Creating or updating icon" filename icon-id)
|
||||
(icons/create-icon conn {:id icon-id
|
||||
:library-id library-id
|
||||
:name (:name data filename)
|
||||
:content (:content data)
|
||||
:metadata mdata})))
|
||||
|
||||
(defn- icon-exists?
|
||||
[conn id]
|
||||
(s/assert ::us/uuid id)
|
||||
(let [row (db/get-by-id conn :icon id)]
|
||||
(if row true false)))
|
||||
|
||||
(defn- import-icon-if-not-exists
|
||||
[conn library-id fpath]
|
||||
(s/assert ::us/uuid library-id)
|
||||
(s/assert fs/path? fpath)
|
||||
(let [icon-id (uuid/namespaced +icons-uuid-ns+ (str library-id (fs/name fpath)))]
|
||||
(when-not (icon-exists? conn icon-id)
|
||||
(create-icon conn library-id icon-id fpath))
|
||||
icon-id))
|
||||
|
||||
(defn- import-icons
|
||||
[conn library-id {:keys [path regex] :as item}]
|
||||
(run! (fn [fpath]
|
||||
(when (re-matches regex (str fpath))
|
||||
(import-icon-if-not-exists conn library-id fpath)))
|
||||
(->> (fs/list-dir path)
|
||||
(filter fs/regular-file?))))
|
||||
|
||||
(defn- process-icons-library
|
||||
[conn basedir {:keys [path regex] :as item}]
|
||||
(s/assert ::import-item-media item)
|
||||
(let [library-id (create-icons-library-if-not-exists conn item)]
|
||||
(->> (assoc item :path (fs/join basedir path))
|
||||
(import-icons conn library-id))))
|
||||
|
||||
|
||||
;; --- Images Libraries Importer
|
||||
|
||||
(defn- image-library-exists?
|
||||
[conn id]
|
||||
(s/assert ::us/uuid id)
|
||||
(let [row (db/get-by-id conn :image-library id)]
|
||||
(if row true false)))
|
||||
|
||||
(defn- create-images-library
|
||||
[conn {:keys [name] :as item}]
|
||||
(let [id (uuid/namespaced +images-uuid-ns+ name)]
|
||||
(log/info "Creating image library:" name)
|
||||
(images/create-library conn {:id id
|
||||
:team-id uuid/zero
|
||||
:name name})))
|
||||
|
||||
(defn- create-images-library-if-not-exists
|
||||
[conn {:keys [name] :as item}]
|
||||
(let [id (uuid/namespaced +images-uuid-ns+ name)]
|
||||
(when-not (image-library-exists? conn id)
|
||||
(create-images-library conn item)
|
||||
id)))
|
||||
|
||||
(defn- create-image
|
||||
[conn library-id image-id localpath]
|
||||
[conn file-id image-id localpath]
|
||||
(s/assert fs/path? localpath)
|
||||
(s/assert ::us/uuid library-id)
|
||||
(s/assert ::us/uuid file-id)
|
||||
(s/assert ::us/uuid image-id)
|
||||
(let [filename (fs/name localpath)
|
||||
extension (second (fs/split-ext filename))
|
||||
|
@ -170,14 +295,15 @@
|
|||
mtype (case extension
|
||||
".jpg" "image/jpeg"
|
||||
".png" "image/png"
|
||||
".webp" "image/webp")]
|
||||
".webp" "image/webp"
|
||||
".svg" "image/svg+xml")]
|
||||
(log/info "Creating image" filename image-id)
|
||||
(images/create-image conn {:content {:tempfile localpath
|
||||
:filename filename
|
||||
:content-type mtype
|
||||
:size (.length file)}
|
||||
:id image-id
|
||||
:library-id library-id
|
||||
:file-id file-id
|
||||
:user uuid/zero
|
||||
:name filename})))
|
||||
|
||||
|
@ -188,85 +314,100 @@
|
|||
(if row true false)))
|
||||
|
||||
(defn- import-image-if-not-exists
|
||||
[conn library-id fpath]
|
||||
(s/assert ::us/uuid library-id)
|
||||
[conn file-id fpath]
|
||||
(s/assert ::us/uuid file-id)
|
||||
(s/assert fs/path? fpath)
|
||||
(let [image-id (uuid/namespaced +images-uuid-ns+ (str library-id (fs/name fpath)))]
|
||||
(let [image-id (uuid/namespaced +images-uuid-ns+ (str file-id (fs/name fpath)))]
|
||||
(when-not (image-exists? conn image-id)
|
||||
(create-image conn library-id image-id fpath))
|
||||
(create-image conn file-id image-id fpath))
|
||||
image-id))
|
||||
|
||||
(defn- import-images
|
||||
[conn library-id {:keys [path regex] :as item}]
|
||||
[conn file-id {:keys [path regex] :as images}]
|
||||
(run! (fn [fpath]
|
||||
(when (re-matches regex (str fpath))
|
||||
(import-image-if-not-exists conn library-id fpath)))
|
||||
(import-image-if-not-exists conn file-id fpath)))
|
||||
(->> (fs/list-dir path)
|
||||
(filter fs/regular-file?))))
|
||||
|
||||
(defn- process-images-library
|
||||
[conn basedir {:keys [path regex] :as item}]
|
||||
(s/assert ::import-item-media item)
|
||||
(let [library-id (create-images-library-if-not-exists conn item)]
|
||||
(->> (assoc item :path (fs/join basedir path))
|
||||
(import-images conn library-id))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Colors Libraries Importer
|
||||
;; Colors Importer
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- color-library-exists?
|
||||
[conn id]
|
||||
(s/assert ::us/uuid id)
|
||||
(let [row (db/get-by-id conn :color-library id)]
|
||||
(if row true false)))
|
||||
|
||||
(defn- create-colors-library
|
||||
[conn {:keys [name] :as item}]
|
||||
(let [id (uuid/namespaced +colors-uuid-ns+ name)]
|
||||
(log/info "Creating color library:" name)
|
||||
(colors/create-library conn {:id id
|
||||
:team-id uuid/zero
|
||||
:name name})))
|
||||
|
||||
|
||||
(defn- create-colors-library-if-not-exists
|
||||
[conn {:keys [name] :as item}]
|
||||
(let [id (uuid/namespaced +colors-uuid-ns+ name)]
|
||||
(when-not (color-library-exists? conn id)
|
||||
(create-colors-library conn item))
|
||||
id))
|
||||
|
||||
(defn- create-color
|
||||
[conn library-id name content]
|
||||
(s/assert ::us/uuid library-id)
|
||||
[conn file-id name content]
|
||||
(s/assert ::us/uuid file-id)
|
||||
(s/assert ::us/color content)
|
||||
(let [color-id (uuid/namespaced +colors-uuid-ns+ (str library-id content))]
|
||||
(let [color-id (uuid/namespaced +colors-uuid-ns+ (str file-id content))]
|
||||
(log/info "Creating color" color-id "-" name content)
|
||||
(colors/create-color conn {:id color-id
|
||||
:library-id library-id
|
||||
:file-id file-id
|
||||
:name name
|
||||
:content content})
|
||||
color-id))
|
||||
|
||||
(defn- import-colors
|
||||
[conn library-id {:keys [colors] :as item}]
|
||||
(db/delete! conn :color {:library-id library-id})
|
||||
[conn file-id colors]
|
||||
(db/delete! conn :color {:file-id file-id})
|
||||
(run! (fn [[name content]]
|
||||
(create-color conn library-id name content))
|
||||
(create-color conn file-id name content))
|
||||
(partition-all 2 colors)))
|
||||
|
||||
(defn- process-colors-library
|
||||
[conn {:keys [name id colors] :as item}]
|
||||
(us/verify ::import-item-color item)
|
||||
(let [library-id (create-colors-library-if-not-exists conn item)]
|
||||
(import-colors conn library-id item)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Libraries Importer
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- library-file-exists?
|
||||
[conn id]
|
||||
(s/assert ::us/uuid id)
|
||||
(let [row (db/get-by-id conn :file id)]
|
||||
(if row true false)))
|
||||
|
||||
(defn- create-library-file-if-not-exists
|
||||
[conn project-id {:keys [name] :as library-file}]
|
||||
(let [id (uuid/namespaced +colors-uuid-ns+ name)]
|
||||
(when-not (library-file-exists? conn id)
|
||||
(log/info "Creating library-file:" name)
|
||||
(files/create-file conn {:id id
|
||||
:profile-id uuid/zero
|
||||
:project-id project-id
|
||||
:name name})
|
||||
(files/create-page conn {:file-id id}))
|
||||
id))
|
||||
|
||||
(defn- process-library
|
||||
[conn basedir project-id {:keys [name images colors] :as library}]
|
||||
(us/verify ::import-library library)
|
||||
(let [library-file-id (create-library-file-if-not-exists conn project-id library)]
|
||||
(when images
|
||||
(->> (assoc images :path (fs/join basedir (:path images)))
|
||||
(import-images conn library-file-id)))
|
||||
(when colors
|
||||
(import-colors conn library-file-id colors))))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Entry Point
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- project-exists?
|
||||
[conn id]
|
||||
(s/assert ::us/uuid id)
|
||||
(let [row (db/get-by-id conn :project id)]
|
||||
(if row true false)))
|
||||
|
||||
(defn- create-project-if-not-exists
|
||||
[conn {:keys [name] :as project}]
|
||||
(let [id (uuid/namespaced +colors-uuid-ns+ name)]
|
||||
(when-not (project-exists? conn id)
|
||||
(log/info "Creating project" name)
|
||||
(projects/create-project conn {:id id
|
||||
:team-id uuid/zero
|
||||
:name name
|
||||
:default? false}))
|
||||
id))
|
||||
|
||||
(defn- validate-path
|
||||
[path]
|
||||
(when-not path
|
||||
|
@ -295,20 +436,21 @@
|
|||
[]
|
||||
(mount/stop))
|
||||
|
||||
(defn- importer
|
||||
[conn basedir data]
|
||||
(let [images (:images data)
|
||||
icons (:icons data)
|
||||
colors (:colors data)]
|
||||
(run! #(process-images-library conn basedir %) images)
|
||||
(run! #(process-icons-library conn basedir %) icons)
|
||||
(run! #(process-colors-library conn %) colors)))
|
||||
;; (defn- importer
|
||||
;; [conn basedir data]
|
||||
;; (let [images (:images data)
|
||||
;; icons (:icons data)
|
||||
;; colors (:colors data)]
|
||||
;; (run! #(process-images-library conn basedir %) images)
|
||||
;; (run! #(process-icons-library conn basedir %) icons)
|
||||
;; (run! #(process-colors-library conn %) colors)))
|
||||
|
||||
(defn run
|
||||
[path]
|
||||
(let [[basedir data] (read-file path)]
|
||||
(let [[basedir libraries] (read-file path)]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(importer conn basedir data))))
|
||||
(let [project-id (create-project-if-not-exists conn {:name "System libraries"})]
|
||||
(run! #(process-library conn basedir project-id %) libraries)))))
|
||||
|
||||
(defn -main
|
||||
[& [path]]
|
||||
|
|
|
@ -59,7 +59,11 @@
|
|||
|
||||
{:desc "Add session_id field to page_change table"
|
||||
:name "0011-add-session-id-field-to-page-change-table"
|
||||
:fn (mg/resource "migrations/0011-add-session-id-field-to-page-change-table.sql")}]})
|
||||
:fn (mg/resource "migrations/0011-add-session-id-field-to-page-change-table.sql")}
|
||||
|
||||
{:desc "Make libraries linked to a file"
|
||||
:name "0012-make-libraries-linked-to-a-file"
|
||||
:fn (mg/resource "migrations/0012-make-libraries-linked-to-a-file.sql")}]})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Entry point
|
||||
|
|
|
@ -119,26 +119,27 @@
|
|||
(declare create-color)
|
||||
|
||||
(s/def ::create-color
|
||||
(s/keys :req-un [::profile-id ::name ::content ::library-id]
|
||||
(s/keys :req-un [::profile-id ::name ::content ::file-id]
|
||||
:opt-un [::id]))
|
||||
|
||||
(sm/defmutation ::create-color
|
||||
[{:keys [profile-id library-id] :as params}]
|
||||
[{:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [lib (select-library-for-update conn library-id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id lib))
|
||||
(create-color conn params))))
|
||||
(create-color conn params)))
|
||||
;; (let [lib (select-library-for-update conn library-id)]
|
||||
;; (teams/check-edition-permissions! conn profile-id (:team-id lib))
|
||||
;; (create-color conn params))))
|
||||
|
||||
(def ^:private sql:create-color
|
||||
"insert into color (id, name, library_id, content)
|
||||
"insert into color (id, name, file_id, content)
|
||||
values ($1, $2, $3, $4) returning *")
|
||||
|
||||
(defn create-color
|
||||
[conn {:keys [id name library-id content]}]
|
||||
[conn {:keys [id name file-id content]}]
|
||||
(let [id (or id (uuid/next))]
|
||||
(db/insert! conn :color {:id id
|
||||
:name name
|
||||
:library-id library-id
|
||||
:file-id file-id
|
||||
:content content})))
|
||||
|
||||
|
||||
|
@ -160,9 +161,10 @@
|
|||
|
||||
(def ^:private sql:select-color-for-update
|
||||
"select c.*,
|
||||
lib.team_id as team_id
|
||||
p.team_id as team_id
|
||||
from color as c
|
||||
inner join color_library as lib on (lib.id = c.library_id)
|
||||
inner join file as f on f.id = c.file_id
|
||||
inner join project as p on p.id = f.project_id
|
||||
where c.id = ?
|
||||
for update of c")
|
||||
|
||||
|
@ -174,6 +176,26 @@
|
|||
row))
|
||||
|
||||
|
||||
;; --- Mutation: Update Color
|
||||
|
||||
(s/def ::update-color
|
||||
(s/keys :req-un [::profile-id ::id ::content]))
|
||||
|
||||
(sm/defmutation ::update-color
|
||||
[{:keys [profile-id id content] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [clr (select-color-for-update conn id)
|
||||
;; IMPORTANT: if the previous name was equal to the hex content,
|
||||
;; we must rename it in addition to changing the value.
|
||||
new-name (if (= (:name clr) (:content clr))
|
||||
content
|
||||
(:name clr))]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id clr))
|
||||
(db/update! conn :color
|
||||
{:name new-name
|
||||
:content content}
|
||||
{:id id}))))
|
||||
|
||||
;; --- Delete Color
|
||||
|
||||
(declare delete-color)
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
:is-admin true
|
||||
:can-edit true}))
|
||||
|
||||
(defn- create-file
|
||||
(defn create-file
|
||||
[conn {:keys [id profile-id name project-id] :as params}]
|
||||
(let [id (or id (uuid/next))
|
||||
file (db/insert! conn :file {:id id :project-id project-id :name name})]
|
||||
|
@ -70,7 +70,7 @@
|
|||
(create-file-profile conn))
|
||||
file))
|
||||
|
||||
(defn- create-page
|
||||
(defn create-page
|
||||
[conn {:keys [file-id] :as params}]
|
||||
(let [id (uuid/next)]
|
||||
(db/insert! conn :page
|
||||
|
@ -133,6 +133,7 @@
|
|||
(declare create-file-image)
|
||||
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::image-id ::us/uuid)
|
||||
(s/def ::content ::imgs/upload)
|
||||
|
||||
(s/def ::add-file-image-from-url
|
||||
|
@ -171,7 +172,11 @@
|
|||
opts (assoc imgs/thumbnail-options
|
||||
:input {:mtype (:mtype info)
|
||||
:path path})
|
||||
thumb (imgs/persist-image-thumbnail-on-fs opts)]
|
||||
thumb (if-not (= (:mtype info) "image/svg+xml")
|
||||
(imgs/persist-image-thumbnail-on-fs opts)
|
||||
(assoc info
|
||||
:path path
|
||||
:quality 0))]
|
||||
|
||||
(-> (db/insert! conn :file-image
|
||||
{:file-id file-id
|
||||
|
@ -189,6 +194,33 @@
|
|||
(images/resolve-urls :thumb-path :thumb-uri))))
|
||||
|
||||
|
||||
;; --- Mutation: Delete File Image
|
||||
|
||||
(declare mark-file-image-deleted)
|
||||
|
||||
(s/def ::delete-file-image
|
||||
(s/keys :req-un [::file-id ::image-id ::profile-id]))
|
||||
|
||||
(sm/defmutation ::delete-file-image
|
||||
[{:keys [file-id image-id profile-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(files/check-edition-permissions! conn profile-id file-id)
|
||||
|
||||
;; Schedule object deletion
|
||||
(tasks/submit! conn {:name "delete-object"
|
||||
:delay cfg/default-deletion-delay
|
||||
:props {:id image-id :type :file-image}})
|
||||
|
||||
(mark-file-image-deleted conn params)))
|
||||
|
||||
(defn mark-file-image-deleted
|
||||
[conn {:keys [image-id] :as params}]
|
||||
(db/update! conn :file-image
|
||||
{:deleted-at (dt/now)}
|
||||
{:id image-id})
|
||||
nil)
|
||||
|
||||
|
||||
;; --- Mutation: Import from collection
|
||||
|
||||
(declare copy-image)
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
(s/def ::id ::us/uuid)
|
||||
(s/def ::name ::us/string)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::library-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::url ::us/url)
|
||||
|
||||
|
@ -62,7 +62,7 @@
|
|||
|
||||
;; --- Rename Library
|
||||
|
||||
(declare select-library-for-update)
|
||||
(declare select-file-for-update)
|
||||
|
||||
(s/def ::rename-image-library
|
||||
(s/keys :req-un [::id ::profile-id ::name]))
|
||||
|
@ -70,15 +70,26 @@
|
|||
(sm/defmutation ::rename-image-library
|
||||
[{:keys [profile-id id name] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [lib (select-library-for-update conn id)]
|
||||
(let [lib (select-file-for-update conn id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id lib))
|
||||
(db/update! conn :image-library
|
||||
{:name name}
|
||||
{:id id}))))
|
||||
|
||||
(defn- select-library-for-update
|
||||
(def ^:private sql:select-file-for-update
|
||||
"select file.*,
|
||||
project.team_id as team_id
|
||||
from file
|
||||
inner join project on (project.id = file.project_id)
|
||||
where file.id = ?
|
||||
for update of file")
|
||||
|
||||
(defn- select-file-for-update
|
||||
[conn id]
|
||||
(db/get-by-id conn :image-library id {:for-update true}))
|
||||
(let [row (db/exec-one! conn [sql:select-file-for-update id])]
|
||||
(when-not row
|
||||
(ex/raise :type :not-found))
|
||||
row))
|
||||
|
||||
|
||||
;; --- Delete Library
|
||||
|
@ -91,7 +102,7 @@
|
|||
(sm/defmutation ::delete-image-library
|
||||
[{:keys [id profile-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [lib (select-library-for-update conn id)]
|
||||
(let [lib (select-file-for-update conn id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id lib))
|
||||
|
||||
;; Schedule object deletion
|
||||
|
@ -112,7 +123,7 @@
|
|||
(declare persist-image-thumbnail-on-fs)
|
||||
|
||||
(def valid-image-types?
|
||||
#{"image/jpeg", "image/png", "image/webp"})
|
||||
#{"image/jpeg", "image/png", "image/webp", "image/svg+xml"})
|
||||
|
||||
(s/def :uxbox$upload/filename ::us/string)
|
||||
(s/def :uxbox$upload/size ::us/integer)
|
||||
|
@ -128,32 +139,32 @@
|
|||
(s/def ::content ::upload)
|
||||
|
||||
(s/def ::add-image-from-url
|
||||
(s/keys :req-un [::profile-id ::library-id ::url]
|
||||
(s/keys :req-un [::profile-id ::file-id ::url]
|
||||
:opt-un [::id]))
|
||||
|
||||
(s/def ::upload-image
|
||||
(s/keys :req-un [::profile-id ::library-id ::name ::content]
|
||||
(s/keys :req-un [::profile-id ::file-id ::name ::content]
|
||||
:opt-un [::id]))
|
||||
|
||||
(sm/defmutation ::add-image-from-url
|
||||
[{:keys [profile-id library-id url] :as params}]
|
||||
[{:keys [profile-id file-id url] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [lib (select-library-for-update conn library-id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id lib))
|
||||
(let [file (select-file-for-update conn file-id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id file))
|
||||
(let [content (images/download-image url)
|
||||
params' (merge params {:content content
|
||||
:name (:filename content)})]
|
||||
(create-image conn params')))))
|
||||
|
||||
(sm/defmutation ::upload-image
|
||||
[{:keys [profile-id library-id] :as params}]
|
||||
[{:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [lib (select-library-for-update conn library-id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id lib))
|
||||
(let [file (select-file-for-update conn file-id)]
|
||||
(teams/check-edition-permissions! conn profile-id (:team-id file))
|
||||
(create-image conn params))))
|
||||
|
||||
(defn create-image
|
||||
[conn {:keys [id content library-id name]}]
|
||||
[conn {:keys [id content file-id name]}]
|
||||
(when-not (valid-image-types? (:content-type content))
|
||||
(ex/raise :type :validation
|
||||
:code :image-type-not-allowed
|
||||
|
@ -165,11 +176,15 @@
|
|||
opts (assoc thumbnail-options
|
||||
:input {:mtype (:mtype info)
|
||||
:path path})
|
||||
thumb (persist-image-thumbnail-on-fs opts)]
|
||||
thumb (if-not (= (:mtype info) "image/svg+xml")
|
||||
(persist-image-thumbnail-on-fs opts)
|
||||
(assoc info
|
||||
:path path
|
||||
:quality 0))]
|
||||
|
||||
(-> (db/insert! conn :image
|
||||
{:id (or id (uuid/next))
|
||||
:library-id library-id
|
||||
:file-id file-id
|
||||
:name name
|
||||
:path (str path)
|
||||
:width (:width info)
|
||||
|
@ -236,8 +251,6 @@
|
|||
(ex/raise :type :not-found))
|
||||
row))
|
||||
|
||||
|
||||
|
||||
;; --- Delete Image
|
||||
|
||||
(s/def ::delete-image
|
||||
|
|
|
@ -71,7 +71,8 @@
|
|||
(defn check-edition-permissions!
|
||||
[conn profile-id team-id]
|
||||
(let [row (db/exec-one! conn [sql:team-permissions profile-id team-id])]
|
||||
(when-not (or (:can-edit row)
|
||||
(when-not (or (= team-id uuid/zero)
|
||||
(:can-edit row)
|
||||
(:is-admin row)
|
||||
(:is-owner row))
|
||||
(ex/raise :type :validation
|
||||
|
|
|
@ -28,9 +28,10 @@
|
|||
(s/def ::id ::us/uuid)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
(s/def ::library-id (s/nilable ::us/uuid))
|
||||
|
||||
;; --- Query: Colors Librarys
|
||||
;; --- Query: Colors Libraries
|
||||
|
||||
(def ^:private sql:libraries
|
||||
"select lib.*,
|
||||
|
@ -78,31 +79,31 @@
|
|||
(ex/raise :type :not-found))
|
||||
row))
|
||||
|
||||
;; --- Query: Colors (by library)
|
||||
;; --- Query: Colors (by file)
|
||||
|
||||
(declare retrieve-colors)
|
||||
|
||||
(s/def ::colors
|
||||
(s/keys :req-un [::profile-id ::library-id]))
|
||||
(s/keys :req-un [::profile-id ::file-id]))
|
||||
|
||||
(sq/defquery ::colors
|
||||
[{:keys [profile-id library-id] :as params}]
|
||||
[{:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [lib (retrieve-library conn library-id)]
|
||||
(teams/check-read-permissions! conn profile-id (:team-id lib))
|
||||
(retrieve-colors conn library-id))))
|
||||
(retrieve-colors conn file-id)))
|
||||
;; (let [lib (retrieve-library conn library-id)]
|
||||
;; (teams/check-read-permissions! conn profile-id (:team-id lib))
|
||||
;; (retrieve-colors conn library-id))))
|
||||
|
||||
(def ^:private sql:colors
|
||||
"select color.*
|
||||
from color as color
|
||||
inner join color_library as lib on (lib.id = color.library_id)
|
||||
"select *
|
||||
from color
|
||||
where color.deleted_at is null
|
||||
and color.library_id = ?
|
||||
and color.file_id = ?
|
||||
order by created_at desc")
|
||||
|
||||
(defn- retrieve-colors
|
||||
[conn library-id]
|
||||
(db/exec! conn [sql:colors library-id]))
|
||||
[conn file-id]
|
||||
(db/exec! conn [sql:colors file-id]))
|
||||
|
||||
|
||||
;; --- Query: Color (by ID)
|
||||
|
|
|
@ -53,6 +53,11 @@
|
|||
and (ppr.is_admin = true or
|
||||
ppr.is_owner = true or
|
||||
ppr.can_edit = true)
|
||||
union
|
||||
select p.*
|
||||
from project as p
|
||||
where p.team_id = uuid_nil()
|
||||
and p.deleted_at is null
|
||||
)
|
||||
select distinct
|
||||
file.*,
|
||||
|
@ -80,23 +85,52 @@
|
|||
(mapv decode-row rows)))
|
||||
|
||||
|
||||
;; --- Query: Draft Files
|
||||
;; --- Query: Project Files
|
||||
|
||||
(def ^:private sql:files
|
||||
"select distinct
|
||||
"with projects as (
|
||||
select p.*
|
||||
from project as p
|
||||
inner join team_profile_rel as tpr on (tpr.team_id = p.team_id)
|
||||
where tpr.profile_id = ?
|
||||
and p.deleted_at is null
|
||||
and (tpr.is_admin = true or
|
||||
tpr.is_owner = true or
|
||||
tpr.can_edit = true)
|
||||
union
|
||||
select p.*
|
||||
from project as p
|
||||
inner join project_profile_rel as ppr on (ppr.project_id = p.id)
|
||||
where ppr.profile_id = ?
|
||||
and p.deleted_at is null
|
||||
and (ppr.is_admin = true or
|
||||
ppr.is_owner = true or
|
||||
ppr.can_edit = true)
|
||||
union
|
||||
select p.*
|
||||
from project as p
|
||||
where p.team_id = uuid_nil()
|
||||
and p.deleted_at is null
|
||||
)
|
||||
select distinct
|
||||
f.*,
|
||||
array_agg(pg.id) over pages_w as pages,
|
||||
first_value(pg.data) over pages_w as data
|
||||
from file as f
|
||||
inner join file_profile_rel as fp_r on (fp_r.file_id = f.id)
|
||||
left join page as pg on (f.id = pg.file_id)
|
||||
where fp_r.profile_id = ?
|
||||
and f.project_id = ?
|
||||
where f.project_id = ?
|
||||
and (exists (select *
|
||||
from file_profile_rel as fp_r
|
||||
where fp_r.profile_id = ?
|
||||
and fp_r.file_id = f.id
|
||||
and (fp_r.is_admin = true or
|
||||
fp_r.is_owner = true or
|
||||
fp_r.can_edit = true))
|
||||
or exists (select *
|
||||
from projects as p
|
||||
where p.id = f.project_id))
|
||||
and f.deleted_at is null
|
||||
and pg.deleted_at is null
|
||||
and (fp_r.is_admin = true or
|
||||
fp_r.is_owner = true or
|
||||
fp_r.can_edit = true)
|
||||
window pages_w as (partition by f.id order by pg.ordering
|
||||
range between unbounded preceding
|
||||
and unbounded following)
|
||||
|
@ -108,7 +142,9 @@
|
|||
|
||||
(sq/defquery ::files
|
||||
[{:keys [profile-id project-id] :as params}]
|
||||
(->> (db/exec! db/pool [sql:files profile-id project-id])
|
||||
(->> (db/exec! db/pool [sql:files
|
||||
profile-id profile-id
|
||||
project-id profile-id])
|
||||
(mapv decode-row)))
|
||||
|
||||
;; --- Query: File Permissions
|
||||
|
@ -136,7 +172,12 @@
|
|||
from project_profile_rel as ppr
|
||||
inner join file as f on (f.project_id = ppr.project_id)
|
||||
where f.id = ?
|
||||
and ppr.profile_id = ?;")
|
||||
and ppr.profile_id = ?
|
||||
union all
|
||||
select true, true, true
|
||||
from file as f
|
||||
inner join project as p on (f.project_id = p.id)
|
||||
and p.team_id = uuid_nil();")
|
||||
|
||||
(defn check-edition-permissions!
|
||||
[conn profile-id file-id]
|
||||
|
@ -169,7 +210,8 @@
|
|||
(def ^:private sql:file-images
|
||||
"select fi.*
|
||||
from file_image as fi
|
||||
where fi.file_id = ?")
|
||||
where fi.file_id = ?
|
||||
and fi.deleted_at is null")
|
||||
|
||||
(defn retrieve-file-images
|
||||
[conn {:keys [file-id] :as params}]
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
(s/def ::name ::us/string)
|
||||
(s/def ::profile-id ::us/uuid)
|
||||
(s/def ::team-id ::us/uuid)
|
||||
(s/def ::library-id ::us/uuid)
|
||||
(s/def ::file-id ::us/uuid)
|
||||
|
||||
;; --- Query: Image Librarys
|
||||
|
||||
|
@ -77,32 +77,34 @@
|
|||
(declare retrieve-images)
|
||||
|
||||
(s/def ::images
|
||||
(s/keys :req-un [::profile-id ::library-id]))
|
||||
(s/keys :req-un [::profile-id ::file-id]))
|
||||
|
||||
;; TODO: check if we can resolve url with transducer for reduce
|
||||
;; garbage generation for each request
|
||||
|
||||
(sq/defquery ::images
|
||||
[{:keys [profile-id library-id] :as params}]
|
||||
[{:keys [profile-id file-id] :as params}]
|
||||
(db/with-atomic [conn db/pool]
|
||||
(let [lib (retrieve-library conn library-id)]
|
||||
(teams/check-read-permissions! conn profile-id (:team-id lib))
|
||||
(->> (retrieve-images conn library-id)
|
||||
(mapv #(images/resolve-urls % :path :uri))
|
||||
(mapv #(images/resolve-urls % :thumb-path :thumb-uri))))))
|
||||
(->> (retrieve-images conn file-id)
|
||||
(mapv #(images/resolve-urls % :path :uri))
|
||||
(mapv #(images/resolve-urls % :thumb-path :thumb-uri)))))
|
||||
;; (let [lib (retrieve-library conn file-id)]
|
||||
;; (teams/check-read-permissions! conn profile-id (:team-id lib))
|
||||
;; (->> (retrieve-images conn file-id)
|
||||
;; (mapv #(images/resolve-urls % :path :uri))
|
||||
;; (mapv #(images/resolve-urls % :thumb-path :thumb-uri))))))
|
||||
|
||||
|
||||
(def ^:private sql:images
|
||||
"select img.*
|
||||
"select *
|
||||
from image as img
|
||||
inner join image_library as lib on (lib.id = img.library_id)
|
||||
where img.deleted_at is null
|
||||
and img.library_id = ?
|
||||
and img.file_id = ?
|
||||
order by created_at desc")
|
||||
|
||||
(defn- retrieve-images
|
||||
[conn library-id]
|
||||
(db/exec! conn [sql:images library-id]))
|
||||
[conn file-id]
|
||||
(db/exec! conn [sql:images file-id]))
|
||||
|
||||
|
||||
|
||||
|
@ -125,9 +127,9 @@
|
|||
|
||||
(def ^:private sql:single-image
|
||||
"select img.*,
|
||||
lib.team_id as team_id
|
||||
file.team_id as team_id
|
||||
from image as img
|
||||
inner join image_library as lib on (lib.id = img.library_id)
|
||||
inner join file on (file.id = img.file_id)
|
||||
where img.deleted_at is null
|
||||
and img.id = ?
|
||||
order by created_at desc")
|
||||
|
|
|
@ -48,10 +48,19 @@
|
|||
and (ppr.is_admin = true or
|
||||
ppr.is_owner = true or
|
||||
ppr.can_edit = true)
|
||||
union
|
||||
select p.*,
|
||||
(select count(*) from file as f
|
||||
where f.project_id = p.id
|
||||
and deleted_at is null)
|
||||
from project as p
|
||||
where p.team_id = uuid_nil()
|
||||
and p.deleted_at is null
|
||||
)
|
||||
select *
|
||||
from projects
|
||||
where team_id = ?
|
||||
or team_id = uuid_nil()
|
||||
order by modified_at desc")
|
||||
|
||||
(def ^:private sql:project-by-id
|
||||
|
|
|
@ -30,7 +30,8 @@
|
|||
(defn check-edition-permissions!
|
||||
[conn profile-id team-id]
|
||||
(let [row (db/exec-one! conn [sql:team-permissions profile-id team-id])]
|
||||
(when-not (or (:can-edit row)
|
||||
(when-not (or (= team-id uuid/zero) ;; We can write global-project owned items
|
||||
(:can-edit row)
|
||||
(:is-admin row)
|
||||
(:is-owner row))
|
||||
(ex/raise :type :validation
|
||||
|
@ -39,10 +40,9 @@
|
|||
(defn check-read-permissions!
|
||||
[conn profile-id team-id]
|
||||
(let [row (db/exec-one! conn [sql:team-permissions profile-id team-id])]
|
||||
(when-not (or (:can-edit row)
|
||||
(when-not (or (= team-id uuid/zero) ;; We can read global-project owned items
|
||||
(:can-edit row)
|
||||
(:is-admin row)
|
||||
(:is-owner row)
|
||||
;; We can read global-project owned items
|
||||
(= team-id #uuid "00000000-0000-0000-0000-000000000000"))
|
||||
(:is-owner row))
|
||||
(ex/raise :type :validation
|
||||
:code :not-authorized))))
|
||||
|
|
|
@ -522,7 +522,7 @@
|
|||
}
|
||||
},
|
||||
"ds.button.save" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:55" ],
|
||||
"used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:55", "src/uxbox/main/ui/workspace/sidebar/assets.cljs:69" ],
|
||||
"translations" : {
|
||||
"en" : "Save",
|
||||
"fr" : "Sauvegarder",
|
||||
|
@ -657,7 +657,7 @@
|
|||
}
|
||||
},
|
||||
"errors.image-format-unsupported" : {
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:370", "src/uxbox/main/data/users.cljs:177", "src/uxbox/main/data/images.cljs:376" ],
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:390", "src/uxbox/main/data/users.cljs:177", "src/uxbox/main/data/images.cljs:376" ],
|
||||
"translations" : {
|
||||
"en" : "The image format is not supported (must be svg, jpg or png).",
|
||||
"fr" : "Le format d'image n'est pas supporté (doit être svg, jpg ou png).",
|
||||
|
@ -666,7 +666,7 @@
|
|||
}
|
||||
},
|
||||
"errors.image-too-large" : {
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:368", "src/uxbox/main/data/users.cljs:175", "src/uxbox/main/data/images.cljs:374" ],
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:388", "src/uxbox/main/data/users.cljs:175", "src/uxbox/main/data/images.cljs:374" ],
|
||||
"translations" : {
|
||||
"en" : "The image is too large to be inserted (must be under 5mb).",
|
||||
"fr" : "L'image est trop grande (doit être inférieure à 5 Mo).",
|
||||
|
@ -675,7 +675,7 @@
|
|||
}
|
||||
},
|
||||
"errors.image-type-mismatch" : {
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:335", "src/uxbox/main/data/workspace/persistence.cljs:385", "src/uxbox/main/data/users.cljs:191", "src/uxbox/main/data/images.cljs:391" ],
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:355", "src/uxbox/main/data/workspace/persistence.cljs:405", "src/uxbox/main/data/users.cljs:191", "src/uxbox/main/data/images.cljs:391" ],
|
||||
"translations" : {
|
||||
"en" : "Seems that the contents of the image does not match the file extension.",
|
||||
"fr" : "",
|
||||
|
@ -684,7 +684,7 @@
|
|||
}
|
||||
},
|
||||
"errors.image-type-not-allowed" : {
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:332", "src/uxbox/main/data/workspace/persistence.cljs:382", "src/uxbox/main/data/users.cljs:188", "src/uxbox/main/data/images.cljs:388" ],
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:352", "src/uxbox/main/data/workspace/persistence.cljs:402", "src/uxbox/main/data/users.cljs:188", "src/uxbox/main/data/images.cljs:388" ],
|
||||
"translations" : {
|
||||
"en" : "Seems that this is not a valid image.",
|
||||
"fr" : "",
|
||||
|
@ -729,7 +729,7 @@
|
|||
}
|
||||
},
|
||||
"errors.unexpected-error" : {
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:338", "src/uxbox/main/data/workspace/persistence.cljs:388", "src/uxbox/main/data/users.cljs:194", "src/uxbox/main/data/images.cljs:394", "src/uxbox/main/ui/settings/change_email.cljs:51", "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:65", "src/uxbox/main/ui/auth/register.cljs:54" ],
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:358", "src/uxbox/main/data/workspace/persistence.cljs:408", "src/uxbox/main/data/users.cljs:194", "src/uxbox/main/data/images.cljs:394", "src/uxbox/main/ui/settings/change_email.cljs:51", "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:65", "src/uxbox/main/ui/auth/register.cljs:54" ],
|
||||
"translations" : {
|
||||
"en" : "An unexpected error occurred.",
|
||||
"fr" : "Une erreur inattendue c'est produite",
|
||||
|
@ -774,7 +774,7 @@
|
|||
}
|
||||
},
|
||||
"image.loading" : {
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:346", "src/uxbox/main/data/workspace/persistence.cljs:397", "src/uxbox/main/data/users.cljs:201", "src/uxbox/main/data/images.cljs:403" ],
|
||||
"used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:366", "src/uxbox/main/data/workspace/persistence.cljs:417", "src/uxbox/main/data/users.cljs:201", "src/uxbox/main/data/images.cljs:403" ],
|
||||
"translations" : {
|
||||
"en" : "Loading image...",
|
||||
"fr" : "Chargement de l'image...",
|
||||
|
@ -783,7 +783,7 @@
|
|||
}
|
||||
},
|
||||
"modal.create-color.new-color" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:49" ],
|
||||
"used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:49", "src/uxbox/main/ui/workspace/sidebar/assets.cljs:62" ],
|
||||
"translations" : {
|
||||
"en" : "New Color",
|
||||
"fr" : "Nouvelle couleur",
|
||||
|
@ -1358,6 +1358,114 @@
|
|||
"es" : "Alinear arriba"
|
||||
}
|
||||
},
|
||||
"workspace.assets.assets" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:325" ],
|
||||
"translations" : {
|
||||
"en" : "Assets",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Recursos"
|
||||
}
|
||||
},
|
||||
"workspace.assets.box-filter-all" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:342" ],
|
||||
"translations" : {
|
||||
"en" : "All assets",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Todos"
|
||||
}
|
||||
},
|
||||
"workspace.assets.box-filter-colors" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:344" ],
|
||||
"translations" : {
|
||||
"en" : "Colors",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Colores"
|
||||
}
|
||||
},
|
||||
"workspace.assets.box-filter-graphics" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:343" ],
|
||||
"translations" : {
|
||||
"en" : "Graphics",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Gráficos"
|
||||
}
|
||||
},
|
||||
"workspace.assets.colors" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:247" ],
|
||||
"translations" : {
|
||||
"en" : "Colors",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Colores"
|
||||
}
|
||||
},
|
||||
"workspace.assets.delete" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:136", "src/uxbox/main/ui/workspace/sidebar/assets.cljs:231" ],
|
||||
"translations" : {
|
||||
"en" : "Delete",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Borrar"
|
||||
}
|
||||
},
|
||||
"workspace.assets.edit" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:230" ],
|
||||
"translations" : {
|
||||
"en" : "Edit",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Editar"
|
||||
}
|
||||
},
|
||||
"workspace.assets.file-library" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:271" ],
|
||||
"translations" : {
|
||||
"en" : "File library",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Bilioteca del archivo"
|
||||
}
|
||||
},
|
||||
"workspace.assets.graphics" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:113" ],
|
||||
"translations" : {
|
||||
"en" : "Graphics",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Gráficos"
|
||||
}
|
||||
},
|
||||
"workspace.assets.not-found" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:282" ],
|
||||
"translations" : {
|
||||
"en" : "No assets found",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "No se encontraron recursos"
|
||||
}
|
||||
},
|
||||
"workspace.assets.rename" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:229" ],
|
||||
"translations" : {
|
||||
"en" : "Rename",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Renombrar"
|
||||
}
|
||||
},
|
||||
"workspace.assets.search" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:329" ],
|
||||
"translations" : {
|
||||
"en" : "Search assets",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Buscar recursos"
|
||||
}
|
||||
},
|
||||
"workspace.header.menu.disable-dynamic-alignment" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:117" ],
|
||||
"translations" : {
|
||||
|
@ -2179,13 +2287,13 @@
|
|||
}
|
||||
},
|
||||
"workspace.sidebar.icons" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/icons.cljs:89" ],
|
||||
"translations" : {
|
||||
"en" : "Icons",
|
||||
"fr" : "Icône",
|
||||
"ru" : "Иконки",
|
||||
"es" : "Iconos"
|
||||
}
|
||||
},
|
||||
"unused" : true
|
||||
},
|
||||
"workspace.sidebar.sitemap" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/sitemap.cljs:150" ],
|
||||
|
@ -2196,6 +2304,15 @@
|
|||
"es" : "Páginas"
|
||||
}
|
||||
},
|
||||
"workspace.toolbar.assets" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:105" ],
|
||||
"translations" : {
|
||||
"en" : "Assets (Ctrl + I)",
|
||||
"fr" : "",
|
||||
"ru" : "",
|
||||
"es" : "Recursos (Ctrl + I)"
|
||||
}
|
||||
},
|
||||
"workspace.toolbar.circle" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:64" ],
|
||||
"translations" : {
|
||||
|
@ -2206,7 +2323,7 @@
|
|||
}
|
||||
},
|
||||
"workspace.toolbar.color-palette" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:108" ],
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:113" ],
|
||||
"translations" : {
|
||||
"en" : "Color Palette (---)",
|
||||
"fr" : "Palette de couleurs (---)",
|
||||
|
@ -2242,13 +2359,13 @@
|
|||
}
|
||||
},
|
||||
"workspace.toolbar.libraries" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:100" ],
|
||||
"translations" : {
|
||||
"en" : "Libraries (Ctrl + Shift + L)",
|
||||
"fr" : "Librairies (Ctrl + Shift + L)",
|
||||
"ru" : "Библиотеки (Ctrl + Shift + L)",
|
||||
"es" : "Bibliotecas (Ctrl + Mays + L)"
|
||||
}
|
||||
},
|
||||
"unused" : true
|
||||
},
|
||||
"workspace.toolbar.path" : {
|
||||
"used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:88" ],
|
||||
|
|
|
@ -72,6 +72,7 @@
|
|||
@import 'main/partials/sidebar-layers';
|
||||
@import 'main/partials/sidebar-sitemap';
|
||||
@import 'main/partials/sidebar-tools';
|
||||
@import 'main/partials/sidebar-assets';
|
||||
@import 'main/partials/tab-container';
|
||||
@import 'main/partials/tool-bar';
|
||||
@import 'main/partials/user-settings';
|
||||
|
|
199
frontend/resources/styles/main/partials/sidebar-assets.scss
Normal file
199
frontend/resources/styles/main/partials/sidebar-assets.scss
Normal file
|
@ -0,0 +1,199 @@
|
|||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) 2015-2016 Andrey Antukh <niwi@niwi.nz>
|
||||
// Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
|
||||
|
||||
.assets-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.assets-bar-title {
|
||||
color: $color-gray-10;
|
||||
font-size: $fs14;
|
||||
margin: $small $small 0 $small;
|
||||
}
|
||||
|
||||
.search-block {
|
||||
border: 1px solid $color-gray-30;
|
||||
margin: $small $small 0 $small;
|
||||
padding: $x-small;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:focus-within {
|
||||
border-color: $color-primary !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: $color-gray-20;
|
||||
}
|
||||
|
||||
& .search-input {
|
||||
background-color: $color-gray-50;
|
||||
border: none;
|
||||
color: $color-gray-10;
|
||||
font-size: $fs12;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex-grow: 1;
|
||||
|
||||
&:focus {
|
||||
color: lighten($color-gray-10, 8%);
|
||||
}
|
||||
}
|
||||
|
||||
& .search-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
svg {
|
||||
fill: $color-gray-30;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
&.close {
|
||||
transform: rotate(45deg);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-select {
|
||||
background-color: $color-gray-50;
|
||||
color: $color-gray-10;
|
||||
border: 1px solid transparent;
|
||||
border-bottom-color: $color-gray-40;
|
||||
padding: $x-small $x-small 0 $x-small;
|
||||
margin: $small $small $medium $small;
|
||||
|
||||
&:focus {
|
||||
color: lighten($color-gray-10, 8%);
|
||||
}
|
||||
|
||||
option {
|
||||
color: $color-gray-60;
|
||||
background: $color-white;
|
||||
font-size: $fs11;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse-library {
|
||||
margin-right: $small;
|
||||
cursor: pointer;
|
||||
|
||||
&.open svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.asset-group {
|
||||
background-color: $color-gray-60;
|
||||
padding: $small;
|
||||
font-size: $fs11;
|
||||
color: $color-gray-20;
|
||||
|
||||
.group-title {
|
||||
display: flex;
|
||||
|
||||
& span {
|
||||
color: $color-gray-30;
|
||||
}
|
||||
}
|
||||
|
||||
.group-button {
|
||||
margin-left: auto;
|
||||
cursor: pointer;
|
||||
|
||||
& svg {
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
fill: #F0F0F0;
|
||||
}
|
||||
}
|
||||
|
||||
.group-grid {
|
||||
margin-top: $small;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-auto-rows: 7vh;
|
||||
column-gap: 0.5rem;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
background-color: $color-white;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
& img {
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
width: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-name {
|
||||
background-color: $color-gray-60;
|
||||
font-size: $fs9;
|
||||
display: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.grid-cell:hover {
|
||||
border: 1px solid $color-primary;
|
||||
|
||||
& .cell-name {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.group-list {
|
||||
max-height: 30rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.group-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: $x-small;
|
||||
font-size: $fs11;
|
||||
color: $color-white;
|
||||
cursor: pointer;
|
||||
|
||||
& .color-block {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 10px;
|
||||
margin-right: $x-small;
|
||||
}
|
||||
|
||||
& span {
|
||||
margin-left: $x-small;
|
||||
color: $color-gray-30;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ $width-settings-bar: 15rem;
|
|||
}
|
||||
|
||||
.settings-bar-inside {
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
grid-template-columns: 100%;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[beicon.core :as rx]
|
||||
[clojure.set :as set]
|
||||
[potok.core :as ptk]
|
||||
[uxbox.common.data :as d]
|
||||
[uxbox.common.spec :as us]
|
||||
[uxbox.main.repo :as rp]
|
||||
[uxbox.main.store :as st]
|
||||
|
@ -249,21 +250,85 @@
|
|||
(declare create-color-result)
|
||||
|
||||
(defn create-color
|
||||
[library-id color]
|
||||
(s/assert (s/nilable uuid?) library-id)
|
||||
[file-id color]
|
||||
(s/assert (s/nilable uuid?) file-id)
|
||||
(ptk/reify ::create-color
|
||||
ptk/WatchEvent
|
||||
(watch [_ state s]
|
||||
|
||||
(->> (rp/mutation! :create-color {:library-id library-id
|
||||
(->> (rp/mutation! :create-color {:file-id file-id
|
||||
:content color
|
||||
:name color})
|
||||
(rx/map (partial create-color-result library-id))))))
|
||||
(rx/map (partial create-color-result file-id))))))
|
||||
|
||||
(defn create-color-result
|
||||
[library-id item]
|
||||
[file-id color]
|
||||
(ptk/reify ::create-color-result
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update-in [:library-items :palettes library-id] #(into [item] %) )))))
|
||||
(assoc-in [:workspace-colors (:id color)] color)
|
||||
(assoc-in [:workspace-local :color-for-rename] (:id color))))))
|
||||
|
||||
(def clear-color-for-rename
|
||||
(ptk/reify ::clear-color-for-rename
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc-in state [:workspace-local :color-for-rename] nil))))
|
||||
|
||||
(declare rename-color-result)
|
||||
|
||||
(defn rename-color
|
||||
[file-id color-id name]
|
||||
(ptk/reify ::rename-color
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/mutation! :rename-color {:id color-id
|
||||
:name name})
|
||||
(rx/map (partial rename-color-result file-id))))))
|
||||
|
||||
(defn rename-color-result
|
||||
[file-id color]
|
||||
(ptk/reify ::rename-color-result
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-colors (:id color)] color)))))
|
||||
|
||||
(declare update-color-result)
|
||||
|
||||
(defn update-color
|
||||
[file-id color-id content]
|
||||
(ptk/reify ::update-color
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/mutation! :update-color {:id color-id
|
||||
:content content})
|
||||
(rx/map (partial update-color-result file-id))))))
|
||||
|
||||
(defn update-color-result
|
||||
[file-id color]
|
||||
(ptk/reify ::update-color-result
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(assoc-in [:workspace-colors (:id color)] color)))))
|
||||
|
||||
(declare delete-color-result)
|
||||
|
||||
(defn delete-color
|
||||
[file-id color-id]
|
||||
(ptk/reify ::delete-color
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/mutation! :delete-color {:id color-id})
|
||||
(rx/map #(delete-color-result file-id color-id))))))
|
||||
|
||||
(defn delete-color-result
|
||||
[file-id color-id]
|
||||
(ptk/reify ::delete-color-result
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(d/dissoc-in [:workspace-colors color-id])))))
|
||||
|
||||
|
|
|
@ -353,7 +353,7 @@
|
|||
|
||||
;; --- Create Image
|
||||
(declare create-images-result)
|
||||
(def allowed-file-types #{"image/jpeg" "image/png" "image/webp"})
|
||||
(def allowed-file-types #{"image/jpeg" "image/png" "image/webp" "image/svg+xml"})
|
||||
(def max-file-size (* 5 1024 1024))
|
||||
|
||||
;; TODO: unify with upload-image at main/data/workspace/persistence.cljs
|
||||
|
@ -361,9 +361,9 @@
|
|||
;; https://tree.taiga.io/project/uxboxproject/us/440
|
||||
|
||||
(defn create-images
|
||||
([library-id files] (create-images library-id files identity))
|
||||
([library-id files on-uploaded]
|
||||
(us/verify (s/nilable ::us/uuid) library-id)
|
||||
([file-id files] (create-images file-id files identity))
|
||||
([file-id files on-uploaded]
|
||||
(us/verify (s/nilable ::us/uuid) file-id)
|
||||
(us/verify fn? on-uploaded)
|
||||
(ptk/reify ::create-images
|
||||
ptk/WatchEvent
|
||||
|
@ -397,7 +397,7 @@
|
|||
prepare
|
||||
(fn [file]
|
||||
{:name (.-name file)
|
||||
:library-id library-id
|
||||
:file-id file-id
|
||||
:content file})]
|
||||
|
||||
(st/emit! (dm/show {:content (tr "image.loading")
|
||||
|
@ -411,17 +411,17 @@
|
|||
(rx/reduce conj [])
|
||||
(rx/do on-success)
|
||||
(rx/mapcat identity)
|
||||
(rx/map (partial create-images-result library-id))
|
||||
(rx/map (partial create-images-result file-id))
|
||||
(rx/catch on-error)))))))
|
||||
|
||||
;; --- Image Created
|
||||
|
||||
(defn create-images-result
|
||||
[library-id item]
|
||||
#_(us/verify ::image item)
|
||||
[file-id image]
|
||||
#_(us/verify ::image image)
|
||||
(ptk/reify ::create-images-result
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update-in [:library-items :images library-id] #(into [item] %))))))
|
||||
(assoc-in [:workspace-images (:id image)] image)))))
|
||||
|
||||
|
|
|
@ -54,23 +54,39 @@
|
|||
;; Workspace Initialization
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(declare initialized)
|
||||
(declare file-initialized)
|
||||
|
||||
;; --- Initialize Workspace
|
||||
|
||||
(s/def ::layout-flag
|
||||
#{:sitemap
|
||||
:sitemap-pages
|
||||
:layers
|
||||
:libraries
|
||||
:assets
|
||||
:document-history
|
||||
:colorpalette
|
||||
:element-options
|
||||
:rules
|
||||
:display-grid
|
||||
:snap-grid
|
||||
:dynamic-alignment})
|
||||
|
||||
(s/def ::layout-flags (s/coll-of ::layout-flag))
|
||||
|
||||
(def default-layout
|
||||
#{:sitemap
|
||||
:sitemap-pages
|
||||
:layers
|
||||
:element-options
|
||||
:rules
|
||||
:dynamic-alignment
|
||||
:display-grid
|
||||
:snap-grid})
|
||||
:snap-grid
|
||||
:dynamic-alignment})
|
||||
|
||||
(s/def ::options-mode #{:design :prototype})
|
||||
|
||||
(def workspace-default
|
||||
(def workspace-local-default
|
||||
{:zoom 1
|
||||
:flags #{}
|
||||
:selected (d/ordered-set)
|
||||
|
@ -81,7 +97,8 @@
|
|||
:options-mode :design
|
||||
:draw-interaction-to nil
|
||||
:left-sidebar? true
|
||||
:right-sidebar? true})
|
||||
:right-sidebar? true
|
||||
:color-for-rename nil})
|
||||
|
||||
(def initialize-layout
|
||||
(ptk/reify ::initialize-layout
|
||||
|
@ -89,12 +106,12 @@
|
|||
(update [_ state]
|
||||
(assoc state :workspace-layout default-layout))))
|
||||
|
||||
(defn initialize
|
||||
(defn initialize-file
|
||||
[project-id file-id]
|
||||
(us/verify ::us/uuid project-id)
|
||||
(us/verify ::us/uuid file-id)
|
||||
|
||||
(ptk/reify ::initialize
|
||||
(ptk/reify ::initialize-file
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :workspace-presence {}))
|
||||
|
@ -118,9 +135,9 @@
|
|||
(->> stream
|
||||
(rx/filter #(= ::dwc/index-initialized %))
|
||||
(rx/map (constantly
|
||||
(initialized project-id file-id))))))))
|
||||
(file-initialized project-id file-id))))))))
|
||||
|
||||
(defn- initialized
|
||||
(defn- file-initialized
|
||||
[project-id file-id]
|
||||
(ptk/reify ::initialized
|
||||
ptk/UpdateEvent
|
||||
|
@ -131,7 +148,7 @@
|
|||
(assoc file :initialized true)
|
||||
file))))))
|
||||
|
||||
(defn finalize
|
||||
(defn finalize-file
|
||||
[project-id file-id]
|
||||
(ptk/reify ::finalize
|
||||
ptk/UpdateEvent
|
||||
|
@ -149,7 +166,7 @@
|
|||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [page (get-in state [:workspace-pages page-id])
|
||||
local (get-in state [:workspace-cache page-id] workspace-default)]
|
||||
local (get-in state [:workspace-cache page-id] workspace-local-default)]
|
||||
(-> state
|
||||
(assoc :current-page-id page-id ; mainly used by events
|
||||
:workspace-local local
|
||||
|
@ -276,6 +293,7 @@
|
|||
|
||||
(defn- toggle-layout-flag
|
||||
[state flag]
|
||||
(us/assert ::layout-flag flag)
|
||||
(update state :workspace-layout
|
||||
(fn [flags]
|
||||
(if (contains? flags flag)
|
||||
|
@ -288,20 +306,36 @@
|
|||
left-sidebar? (not (empty? (keep layout [:layers
|
||||
:sitemap
|
||||
:document-history
|
||||
:libraries])))
|
||||
right-sidebar? (not (empty? (keep layout [:icons
|
||||
:element-options])))]
|
||||
:libraries
|
||||
:assets])))
|
||||
right-sidebar? (not (empty? (keep layout [:element-options])))]
|
||||
(update-in state [:workspace-local]
|
||||
assoc :left-sidebar? left-sidebar?
|
||||
:right-sidebar? right-sidebar?)))
|
||||
|
||||
(defn- check-auto-flags
|
||||
[state flags-to-toggle]
|
||||
(update state :workspace-layout
|
||||
(fn [flags]
|
||||
(cond
|
||||
(contains? (set flags-to-toggle) :assets)
|
||||
(disj flags :sitemap :layers)
|
||||
|
||||
(contains? (set flags-to-toggle) :sitemap)
|
||||
(disj flags :assets)
|
||||
|
||||
:else
|
||||
flags))))
|
||||
|
||||
(defn toggle-layout-flags
|
||||
[& flags]
|
||||
(us/assert ::layout-flags flags)
|
||||
(ptk/reify ::toggle-layout-flags
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> (reduce toggle-layout-flag state flags)
|
||||
(check-sidebars)))))
|
||||
(check-sidebars)
|
||||
(check-auto-flags flags)))))
|
||||
|
||||
;; --- Set element options mode
|
||||
|
||||
|
@ -1402,8 +1436,11 @@
|
|||
|
||||
;; Persistence
|
||||
|
||||
(def fetch-images dwp/fetch-images)
|
||||
(def add-image-from-url dwp/add-image-from-url)
|
||||
(def upload-image dwp/upload-image)
|
||||
(def delete-file-image dwp/delete-file-image)
|
||||
(def fetch-colors dwp/fetch-colors)
|
||||
(def rename-page dwp/rename-page)
|
||||
(def delete-page dwp/delete-page)
|
||||
(def create-empty-page dwp/create-empty-page)
|
||||
|
|
|
@ -289,7 +289,7 @@
|
|||
(ptk/reify ::fetch-images
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/query :file-images {:file-id file-id})
|
||||
(->> (rp/query :images {:file-id file-id})
|
||||
(rx/map images-fetched)))))
|
||||
|
||||
(defn images-fetched
|
||||
|
@ -300,11 +300,31 @@
|
|||
(let [images (d/index-by :id images)]
|
||||
(assoc state :workspace-images images)))))
|
||||
|
||||
;; --- Fetch Workspace Colors
|
||||
|
||||
(declare colors-fetched)
|
||||
|
||||
(defn fetch-colors
|
||||
[file-id]
|
||||
(ptk/reify ::fetch-colors
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(->> (rp/query :colors {:file-id file-id})
|
||||
(rx/map colors-fetched)))))
|
||||
|
||||
(defn colors-fetched
|
||||
[colors]
|
||||
(ptk/reify ::colors-fetched
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [colors (d/index-by :id colors)]
|
||||
(assoc state :workspace-colors colors)))))
|
||||
|
||||
|
||||
;; --- Upload Image
|
||||
|
||||
(declare image-uploaded)
|
||||
(def allowed-file-types #{"image/jpeg" "image/png" "image/webp"})
|
||||
(def allowed-file-types #{"image/jpeg" "image/png" "image/webp" "image/svg+xml"})
|
||||
(def max-file-size (* 5 1024 1024))
|
||||
|
||||
;; TODO: unify with create-images at main/data/images.cljs
|
||||
|
@ -428,7 +448,25 @@
|
|||
(ptk/reify ::image-created
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-images assoc (:id item) item))))
|
||||
state)))
|
||||
;; (update state :workspace-images assoc (:id item) item))))
|
||||
|
||||
|
||||
;; --- Delete image
|
||||
|
||||
(defn delete-file-image
|
||||
[file-id image-id]
|
||||
(ptk/reify ::delete-file-image
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update state :workspace-images dissoc image-id))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state stream]
|
||||
(let [params {:file-id file-id
|
||||
:image-id image-id}]
|
||||
(rp/mutation :delete-file-image params)))))
|
||||
|
||||
|
||||
;; --- Helpers
|
||||
|
||||
|
|
|
@ -60,6 +60,9 @@
|
|||
(def workspace-images
|
||||
(l/derived :workspace-images st/state))
|
||||
|
||||
(def workspace-colors
|
||||
(l/derived :workspace-colors st/state))
|
||||
|
||||
(def workspace-users
|
||||
(l/derived :workspace-users st/state))
|
||||
|
||||
|
|
|
@ -25,11 +25,15 @@
|
|||
(let [open? (gobj/get props "show")
|
||||
options (gobj/get props "options")
|
||||
is-selectable (gobj/get props "selectable")
|
||||
selected (gobj/get props "selected")]
|
||||
selected (gobj/get props "selected")
|
||||
top (gobj/get props "top")
|
||||
left (gobj/get props "left")]
|
||||
(when open?
|
||||
[:> dropdown' props
|
||||
[:div.context-menu {:class (classnames :is-open open?
|
||||
:is-selectable is-selectable)}
|
||||
:is-selectable is-selectable)
|
||||
:style {:top top
|
||||
:left left}}
|
||||
[:ul.context-menu-items
|
||||
(for [[action-name action-handler] options]
|
||||
[:li.context-menu-item {:class (classnames :is-selected (and selected (= action-name selected)))
|
||||
|
|
|
@ -79,6 +79,7 @@
|
|||
(def picker (icon-xref :picker))
|
||||
(def pin (icon-xref :pin))
|
||||
(def play (icon-xref :play))
|
||||
(def plus (icon-xref :plus))
|
||||
(def radius (icon-xref :radius))
|
||||
(def recent (icon-xref :recent))
|
||||
(def redo (icon-xref :redo))
|
||||
|
|
|
@ -106,8 +106,8 @@
|
|||
(mf/use-effect
|
||||
(mf/deps project-id file-id)
|
||||
(fn []
|
||||
(st/emit! (dw/initialize project-id file-id))
|
||||
#(st/emit! (dw/finalize project-id file-id))))
|
||||
(st/emit! (dw/initialize-file project-id file-id))
|
||||
#(st/emit! (dw/finalize-file project-id file-id))))
|
||||
|
||||
(hooks/use-shortcuts dw/shortcuts)
|
||||
|
||||
|
|
|
@ -96,10 +96,15 @@
|
|||
:class (when (contains? layout :layers) "selected")
|
||||
:on-click #(st/emit! (dw/toggle-layout-flags :sitemap :layers))}
|
||||
i/layers]
|
||||
;; [:li.tooltip.tooltip-right
|
||||
;; {:alt (t locale "workspace.toolbar.libraries")
|
||||
;; :class (when (contains? layout :libraries) "selected")
|
||||
;; :on-click #(st/emit! (dw/toggle-layout-flags :libraries))}
|
||||
;; i/icon-set]
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt (t locale "workspace.toolbar.libraries")
|
||||
:class (when (contains? layout :libraries) "selected")
|
||||
:on-click #(st/emit! (dw/toggle-layout-flags :libraries))}
|
||||
{:alt (t locale "workspace.toolbar.assets")
|
||||
:class (when (contains? layout :assets) "selected")
|
||||
:on-click #(st/emit! (dw/toggle-layout-flags :assets))}
|
||||
i/icon-set]
|
||||
[:li.tooltip.tooltip-right
|
||||
{:alt "History"}
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
[uxbox.main.ui.workspace.sidebar.layers :refer [layers-toolbox]]
|
||||
[uxbox.main.ui.workspace.sidebar.options :refer [options-toolbox]]
|
||||
[uxbox.main.ui.workspace.sidebar.sitemap :refer [sitemap-toolbox]]
|
||||
[uxbox.main.ui.workspace.sidebar.libraries :refer [libraries-toolbox]]))
|
||||
[uxbox.main.ui.workspace.sidebar.libraries :refer [libraries-toolbox]]
|
||||
[uxbox.main.ui.workspace.sidebar.assets :refer [assets-toolbox]]))
|
||||
|
||||
;; --- Left Sidebar (Component)
|
||||
|
||||
|
@ -34,7 +35,9 @@
|
|||
(when (contains? layout :layers)
|
||||
[:& layers-toolbox {:page page}])
|
||||
(when (contains? layout :libraries)
|
||||
[:& libraries-toolbox])]])
|
||||
[:& libraries-toolbox])
|
||||
(when (contains? layout :assets)
|
||||
[:& assets-toolbox])]])
|
||||
|
||||
;; --- Right Sidebar (Component)
|
||||
|
||||
|
|
355
frontend/src/uxbox/main/ui/workspace/sidebar/assets.cljs
Normal file
355
frontend/src/uxbox/main/ui/workspace/sidebar/assets.cljs
Normal file
|
@ -0,0 +1,355 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns uxbox.main.ui.workspace.sidebar.assets
|
||||
(:require
|
||||
[okulary.core :as l]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.common.data :as d]
|
||||
[uxbox.common.pages :as cp]
|
||||
[uxbox.common.geom.shapes :as geom]
|
||||
[uxbox.common.geom.point :as gpt]
|
||||
[uxbox.main.ui.icons :as i]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.data.images :as di]
|
||||
[uxbox.main.data.colors :as dcol]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.keyboard :as kbd]
|
||||
[uxbox.main.ui.shapes.icon :as icon]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.dom.dnd :as dnd]
|
||||
[uxbox.util.timers :as timers]
|
||||
[uxbox.common.uuid :as uuid]
|
||||
[uxbox.util.i18n :as i18n :refer [tr]]
|
||||
[uxbox.util.data :refer [classnames]]
|
||||
[uxbox.main.data.library :as dlib]
|
||||
[uxbox.main.ui.modal :as modal]
|
||||
[uxbox.main.ui.colorpicker :refer [colorpicker most-used-colors]]
|
||||
[uxbox.main.ui.components.tab-container :refer [tab-container tab-element]]
|
||||
[uxbox.main.ui.components.file-uploader :refer [file-uploader]]
|
||||
[uxbox.main.ui.components.context-menu :refer [context-menu]]))
|
||||
|
||||
(defn matches-search
|
||||
[name search-term]
|
||||
(if (str/empty? search-term)
|
||||
true
|
||||
(let [st (str/trim (str/lower search-term))
|
||||
nm (str/trim (str/lower name))]
|
||||
(str/includes? nm st))))
|
||||
|
||||
(mf/defc modal-edit-color
|
||||
[{:keys [color-value on-accept on-cancel] :as ctx}]
|
||||
(let [state (mf/use-state {:current-color color-value})]
|
||||
(letfn [(accept [event]
|
||||
(dom/prevent-default event)
|
||||
(modal/hide!)
|
||||
(when on-accept (on-accept (:current-color @state))))
|
||||
|
||||
(cancel [event]
|
||||
(dom/prevent-default event)
|
||||
(modal/hide!)
|
||||
(when on-cancel (on-cancel)))]
|
||||
|
||||
[:div.modal-create-color
|
||||
[:h3.modal-create-color-title (tr "modal.create-color.new-color")]
|
||||
[:& colorpicker {:value (:current-color @state)
|
||||
:colors (into-array @most-used-colors)
|
||||
:disable-opacity true
|
||||
:on-change #(swap! state assoc :current-color %)}]
|
||||
|
||||
[:input.btn-primary {:type "button"
|
||||
:value (tr "ds.button.save")
|
||||
:on-click accept}]
|
||||
|
||||
[:a.close {:href "#" :on-click cancel} i/close]])))
|
||||
|
||||
(mf/defc graphics-box
|
||||
[{:keys [library-id images] :as props}]
|
||||
(let [state (mf/use-state {:menu-open false
|
||||
:top nil
|
||||
:left nil
|
||||
:image-id nil})
|
||||
|
||||
file-input (mf/use-ref nil)
|
||||
|
||||
add-graphic
|
||||
#(dom/click (mf/ref-val file-input))
|
||||
|
||||
delete-graphic
|
||||
#(st/emit! (dw/delete-file-image library-id (:image-id @state)))
|
||||
|
||||
on-files-selected
|
||||
(fn [files]
|
||||
(st/emit! (di/create-images library-id files)))
|
||||
|
||||
on-context-menu
|
||||
(fn [image-id]
|
||||
(fn [event]
|
||||
(let [pos (dom/get-client-position event)
|
||||
top (:y pos)
|
||||
left (- (:x pos) 20)]
|
||||
(dom/prevent-default event)
|
||||
(swap! state assoc :menu-open true
|
||||
:top top
|
||||
:left left
|
||||
:image-id image-id))))
|
||||
|
||||
on-drag-start
|
||||
(fn [uri]
|
||||
(fn [event]
|
||||
(dnd/set-data! event "text/uri-list" uri)
|
||||
(dnd/set-allowed-effect! event "move")))]
|
||||
|
||||
[:div.asset-group
|
||||
[:div.group-title
|
||||
(tr "workspace.assets.graphics")
|
||||
[:span (str "\u00A0(") (count images) ")"] ;; Unicode 00A0 is non-breaking space
|
||||
[:div.group-button {:on-click add-graphic}
|
||||
i/plus
|
||||
[:& file-uploader {:accept "image/jpeg,image/png,image/webp,image/svg+xml"
|
||||
:multi true
|
||||
:input-ref file-input
|
||||
:on-selected on-files-selected}]]]
|
||||
[:div.group-grid
|
||||
(for [image (sort-by :name images)]
|
||||
[:div.grid-cell {:key (:id image)
|
||||
:draggable true
|
||||
:on-context-menu (on-context-menu (:id image))
|
||||
:on-drag-start (on-drag-start (:uri image))}
|
||||
[:img {:src (:thumb-uri image)
|
||||
:draggable false}] ;; Also need to add css pointer-events: none
|
||||
[:div.cell-name (:name image)]])
|
||||
[:& context-menu
|
||||
{:selectable false
|
||||
:show (:menu-open @state)
|
||||
:on-close #(swap! state assoc :menu-open false)
|
||||
:top (:top @state)
|
||||
:left (:left @state)
|
||||
:options [[(tr "workspace.assets.delete") delete-graphic]]}]]]))
|
||||
|
||||
|
||||
(mf/defc color-item
|
||||
[{:keys [color library-id] :as props}]
|
||||
(let [workspace-local @refs/workspace-local
|
||||
color-for-rename (:color-for-rename workspace-local)
|
||||
|
||||
edit-input-ref (mf/use-ref)
|
||||
|
||||
state (mf/use-state {:menu-open false
|
||||
:top nil
|
||||
:left nil
|
||||
:editing (= color-for-rename (:id color))})
|
||||
|
||||
rename-color
|
||||
(fn [name]
|
||||
(st/emit! (dcol/rename-color library-id (:id color) name)))
|
||||
|
||||
edit-color
|
||||
(fn [value opacity]
|
||||
(st/emit! (dcol/update-color library-id (:id color) value)))
|
||||
|
||||
delete-color
|
||||
(fn []
|
||||
(st/emit! (dcol/delete-color library-id (:id color))))
|
||||
|
||||
rename-color-clicked
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(swap! state assoc :editing true))
|
||||
|
||||
input-blur
|
||||
(fn [event]
|
||||
(let [target (dom/event->target event)
|
||||
name (dom/get-value target)]
|
||||
(rename-color name)
|
||||
(st/emit! dcol/clear-color-for-rename)
|
||||
(swap! state assoc :editing false)))
|
||||
|
||||
input-key-down
|
||||
(fn [event]
|
||||
(when (kbd/esc? event)
|
||||
(st/emit! dcol/clear-color-for-rename)
|
||||
(swap! state assoc :editing false))
|
||||
(when (kbd/enter? event)
|
||||
(input-blur event)))
|
||||
|
||||
edit-color-clicked
|
||||
(fn [event]
|
||||
(modal/show! modal-edit-color
|
||||
{:color-value (:content color)
|
||||
:on-accept edit-color}))
|
||||
|
||||
on-context-menu
|
||||
(fn [event]
|
||||
(let [pos (dom/get-client-position event)
|
||||
top (:y pos)
|
||||
left (- (:x pos) 20)]
|
||||
(dom/prevent-default event)
|
||||
(swap! state assoc
|
||||
:menu-open true
|
||||
:top top
|
||||
:left left)))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps (:editing @state))
|
||||
#(when (:editing @state)
|
||||
(let [edit-input (mf/ref-val edit-input-ref)]
|
||||
(dom/select-text! edit-input))
|
||||
nil))
|
||||
|
||||
[:div.group-list-item {:on-context-menu on-context-menu}
|
||||
[:div.color-block {:style {:background-color (:content color)}}]
|
||||
(if (:editing @state)
|
||||
[:input.element-name
|
||||
{:type "text"
|
||||
:ref edit-input-ref
|
||||
:on-blur input-blur
|
||||
:on-key-down input-key-down
|
||||
:auto-focus true
|
||||
:default-value (:name color "")}]
|
||||
[:div.name-block
|
||||
{:on-double-click rename-color-clicked}
|
||||
(:name color)
|
||||
(when-not (= (:name color) (:content color))
|
||||
[:span (:content color)])])
|
||||
[:& context-menu
|
||||
{:selectable false
|
||||
:show (:menu-open @state)
|
||||
:on-close #(swap! state assoc :menu-open false)
|
||||
:top (:top @state)
|
||||
:left (:left @state)
|
||||
:options [[(tr "workspace.assets.rename") rename-color-clicked]
|
||||
[(tr "workspace.assets.edit") edit-color-clicked]
|
||||
[(tr "workspace.assets.delete") delete-color]]}]]))
|
||||
|
||||
(mf/defc colors-box
|
||||
[{:keys [library-id colors] :as props}]
|
||||
(let [add-color
|
||||
(fn [value opacity]
|
||||
(st/emit! (dcol/create-color library-id value)))
|
||||
|
||||
add-color-clicked
|
||||
(fn [event]
|
||||
(modal/show! modal-edit-color
|
||||
{:color-value "#406280"
|
||||
:on-accept add-color}))]
|
||||
|
||||
[:div.asset-group
|
||||
[:div.group-title
|
||||
(tr "workspace.assets.colors")
|
||||
[:span (str "\u00A0(") (count colors) ")"] ;; Unicode 00A0 is non-breaking space
|
||||
[:div.group-button {:on-click add-color-clicked} i/plus]]
|
||||
[:div.group-list
|
||||
(for [color (sort-by :name colors)]
|
||||
[:& color-item {:key (:id color)
|
||||
:color color
|
||||
:library-id library-id}])]]))
|
||||
|
||||
(mf/defc library-toolbox
|
||||
[{:keys [library-id
|
||||
images
|
||||
colors
|
||||
initial-open?
|
||||
search-term
|
||||
box-filter] :as props}]
|
||||
(let [open? (mf/use-state initial-open?)
|
||||
toggle-open #(swap! open? not)]
|
||||
[:div.tool-window
|
||||
[:div.tool-window-bar
|
||||
[:div.collapse-library
|
||||
{:class (classnames :open @open?)
|
||||
:on-click toggle-open}
|
||||
i/arrow-slide]
|
||||
[:span (tr "workspace.assets.file-library")]]
|
||||
(when @open?
|
||||
(let [show-graphics (and (or (= box-filter :all) (= box-filter :graphics))
|
||||
(or (> (count images) 0) (str/empty? search-term)))
|
||||
show-colors (and (or (= box-filter :all) (= box-filter :colors))
|
||||
(or (> (count colors) 0) (str/empty? search-term)))]
|
||||
[:div.tool-window-content
|
||||
(when show-graphics
|
||||
[:& graphics-box {:library-id library-id :images images}])
|
||||
(when show-colors
|
||||
[:& colors-box {:library-id library-id :colors colors}])
|
||||
(when (and (not show-graphics) (not show-colors))
|
||||
[:div.asset-group
|
||||
[:div.group-title (tr "workspace.assets.not-found")]])]))]))
|
||||
|
||||
(mf/defc assets-toolbox
|
||||
[]
|
||||
(let [team-id (-> refs/workspace-project mf/deref :team-id)
|
||||
file-id (-> refs/workspace-file mf/deref :id)
|
||||
file-images (mf/deref refs/workspace-images)
|
||||
file-colors (mf/deref refs/workspace-colors)
|
||||
|
||||
state (mf/use-state {:search-term ""
|
||||
:box-filter :all})
|
||||
|
||||
filtered-images (filter #(matches-search (:name %) (:search-term @state))
|
||||
(vals file-images))
|
||||
|
||||
filtered-colors (filter #(or (matches-search (:name %) (:search-term @state))
|
||||
(matches-search (:content %) (:search-term @state)))
|
||||
(vals file-colors))
|
||||
|
||||
on-search-term-change (fn [event]
|
||||
(let [value (-> (dom/get-target event)
|
||||
(dom/get-value))]
|
||||
(swap! state assoc :search-term value)))
|
||||
|
||||
on-search-clear-click (fn [event]
|
||||
(swap! state assoc :search-term ""))
|
||||
|
||||
on-box-filter-change (fn [event]
|
||||
(let [value (-> (dom/get-target event)
|
||||
(dom/get-value)
|
||||
(d/read-string))]
|
||||
(swap! state assoc :box-filter value)))]
|
||||
|
||||
(mf/use-effect
|
||||
(mf/deps file-id)
|
||||
#(when file-id
|
||||
(st/emit! (dw/fetch-images file-id))
|
||||
(st/emit! (dw/fetch-colors file-id))))
|
||||
|
||||
[:div.assets-bar
|
||||
|
||||
[:div.tool-window
|
||||
[:div.tool-window-content
|
||||
[:div.assets-bar-title (tr "workspace.assets.assets")]
|
||||
|
||||
[:div.search-block
|
||||
[:input.search-input
|
||||
{:placeholder (tr "workspace.assets.search")
|
||||
:type "text"
|
||||
:value (:search-term @state)
|
||||
:on-change on-search-term-change}]
|
||||
(if (str/empty? (:search-term @state))
|
||||
[:div.search-icon
|
||||
i/search]
|
||||
[:div.search-icon.close
|
||||
{:on-click on-search-clear-click}
|
||||
i/close])]
|
||||
|
||||
[:select.input-select {:value (:box-filter @state)
|
||||
:on-change on-box-filter-change}
|
||||
[:option {:value ":all"} (tr "workspace.assets.box-filter-all")]
|
||||
[:option {:value ":graphics"} (tr "workspace.assets.box-filter-graphics")]
|
||||
[:option {:value ":colors"} (tr "workspace.assets.box-filter-colors")]]
|
||||
]]
|
||||
|
||||
[:& library-toolbox {:library-id file-id
|
||||
:images filtered-images
|
||||
:colors filtered-colors
|
||||
:initial-open? true
|
||||
:search-term (:search-term @state)
|
||||
:box-filter (:box-filter @state)}]]))
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
;; defined by the Mozilla Public License, v. 2.0.
|
||||
;;
|
||||
;; Copyright (c) 2020 UXBOX Labs SL
|
||||
|
||||
(ns uxbox.main.ui.workspace.sidebar.icons
|
||||
(:require
|
||||
#_[uxbox.main.ui.dashboard.icons :as icons]
|
||||
[rumext.alpha :as mf]
|
||||
[uxbox.common.data :as d]
|
||||
[uxbox.main.data.icons :as di]
|
||||
[uxbox.main.data.workspace :as dw]
|
||||
[uxbox.main.refs :as refs]
|
||||
[uxbox.main.store :as st]
|
||||
[uxbox.main.ui.icons :as i]
|
||||
[uxbox.main.ui.shapes.icon :as icon]
|
||||
[uxbox.util.data :refer [read-string]]
|
||||
[uxbox.util.dom :as dom]
|
||||
[uxbox.util.i18n :as i18n :refer [t]]
|
||||
[uxbox.util.router :as r]))
|
||||
|
||||
(mf/defc icons-collections
|
||||
[{:keys [collections value on-change] :as props}]
|
||||
[:div.figures-catalog
|
||||
;; extract component: set selector
|
||||
[:select.input-select.small
|
||||
{:on-change (fn [event]
|
||||
(let [val (-> (dom/get-target event)
|
||||
(dom/get-value))]
|
||||
(on-change (d/read-string val))))
|
||||
:value (pr-str value)}
|
||||
[:option {:value (pr-str nil)} "Storage"]
|
||||
(for [coll collections]
|
||||
[:option {:key (str "icon-coll" (:id coll))
|
||||
:value (pr-str (:id coll))}
|
||||
(:name coll)])]])
|
||||
|
||||
(mf/defc icons-list
|
||||
[{:keys [collection-id] :as props}]
|
||||
(let [icons [] #_(mf/deref icons/icons-iref) ;; TODO: Fix this
|
||||
|
||||
on-select
|
||||
(fn [event data]
|
||||
(st/emit! (dw/select-for-drawing :icon data)))]
|
||||
|
||||
#_(mf/use-effect
|
||||
{:fn #(st/emit! (di/fetch-icons collection-id))
|
||||
:deps (mf/deps collection-id)})
|
||||
|
||||
(for [icon icons
|
||||
:let [selected? (= nil #_(:drawing local) icon)]]
|
||||
[:div.figure-btn {:key (str (:id icon))
|
||||
:class (when selected? "selected")
|
||||
:on-click #(on-select % icon)
|
||||
}
|
||||
[:& icon/icon-svg {:shape icon}]])))
|
||||
|
||||
;; --- Icons (Component)
|
||||
|
||||
(mf/defc icons-toolbox
|
||||
[props]
|
||||
(let [locale (i18n/use-locale)
|
||||
selected (mf/use-state nil)
|
||||
|
||||
local (mf/deref refs/workspace-local)
|
||||
|
||||
collections (vals [] #_(mf/deref icons/collections-iref)) ;; TODO: FIX THIS
|
||||
collection (first collections)
|
||||
|
||||
on-close
|
||||
(fn [event]
|
||||
(st/emit! (dw/toggle-layout-flags :icons)))
|
||||
|
||||
on-change
|
||||
(fn [val]
|
||||
(st/emit! (dw/select-for-drawing nil))
|
||||
(reset! selected val))]
|
||||
|
||||
#_(mf/use-effect
|
||||
{:fn #(st/emit! di/fetch-collections)})
|
||||
|
||||
[:div#form-figures.tool-window
|
||||
[:div.tool-window-bar
|
||||
[:div.tool-window-icon i/icon-set]
|
||||
[:span (t locale "workspace.sidebar.icons")]
|
||||
[:div.tool-window-close {:on-click on-close} i/close]]
|
||||
[:div.tool-window-content
|
||||
[:& icons-collections {:collections collections
|
||||
:value @selected
|
||||
:on-change on-change
|
||||
}]
|
||||
[:& icons-list {:collection-id @selected}]]]))
|
|
@ -113,7 +113,7 @@
|
|||
(if (= section :icons)
|
||||
[:svg {:view-box (->> item :metadata :view-box (str/join " "))
|
||||
:width (-> item :metadata :width)
|
||||
:height (-> item :metadat :height)
|
||||
:height (-> item :metadata :height)
|
||||
:dangerouslySetInnerHTML {:__html (:content item)}}]
|
||||
[:img {:draggable false
|
||||
:src (:thumb-uri item)}])
|
||||
|
|
|
@ -58,7 +58,10 @@
|
|||
(set-data! e "uxbox/data" data))
|
||||
([e data-type data]
|
||||
(let [dt (.-dataTransfer e)]
|
||||
(.setData dt data-type (t/encode data))
|
||||
(if (or (str/starts-with? data-type "application")
|
||||
(str/starts-with? data-type "uxbox"))
|
||||
(.setData dt data-type (t/encode data))
|
||||
(.setData dt data-type data))
|
||||
e)))
|
||||
|
||||
(defn set-drag-image!
|
||||
|
|
|
@ -1,54 +1,68 @@
|
|||
{:icons
|
||||
[{:name "Material Design (Action)"
|
||||
:path "./icons/material-action"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
[
|
||||
;; Icons
|
||||
{:name "Material Design (Action)"
|
||||
:images {:path "./icons/material-action"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Alert)"
|
||||
:path "./icons/material-alert"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-alert"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Av)"
|
||||
:path "./icons/material-av"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-av"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Content)"
|
||||
:path "./icons/material-content"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-content"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Device)"
|
||||
:path "./icons/material-device"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-device"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Editor)"
|
||||
:path "./icons/material-editor"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-editor"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (File)"
|
||||
:path "./icons/material-file"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-file"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Hardware)"
|
||||
:path "./icons/material-hardware"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-hardware"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Image)"
|
||||
:path "./icons/material-image"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-image"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Maps)"
|
||||
:path "./icons/material-maps"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-maps"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Navigation)"
|
||||
:path "./icons/material-navigation"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-navigation"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Notification)"
|
||||
:path "./icons/material-notification"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-notification"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Places)"
|
||||
:path "./icons/material-places"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-places"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Social)"
|
||||
:path "./icons/material-social"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
:images {:path "./icons/material-social"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
{:name "Material Design (Toggle)"
|
||||
:path "./icons/material-toggle"
|
||||
:regex #"^.*_48px\.svg$"}
|
||||
]
|
||||
:images {:path "./icons/material-toggle"
|
||||
:regex #"^.*_48px\.svg$"}}
|
||||
|
||||
:colors
|
||||
[{:name "Flat design"
|
||||
:id #uuid "00000000-0000-0000-0000-00000000001"
|
||||
;; Images
|
||||
{:name "Unsplash"
|
||||
:images {:path "./images/unsplash"
|
||||
:regex #"^.*\.jpg$"}
|
||||
:colors ["Chateau Green" "#419860"
|
||||
"Toast" "#987567"
|
||||
"Confetti" "#EAC75B"
|
||||
"Thunderbird" "#C23D1F"
|
||||
"Coffee Bean" "#331113"
|
||||
"Lavender Magenta" "#EF88DF"
|
||||
"Persian Rose" "#F536A6"
|
||||
"Royal Blue" "#5C4AEE"
|
||||
"Biscay" "#202362"
|
||||
"Olivine" "#98C277"]}
|
||||
|
||||
;; Colors
|
||||
{:name "Flat design"
|
||||
:colors ["turquoise-50" "#e8f8f5"
|
||||
"turquoise-100" "#d1f2eb"
|
||||
"turquoise-200" "#a3e4d7"
|
||||
|
@ -250,9 +264,7 @@
|
|||
"asbestos-800" "#515a5a"
|
||||
"asbestos-900" "#424949"]}
|
||||
|
||||
|
||||
{:name "Material design"
|
||||
:id #uuid "00000000-0000-0000-0000-000000000020"
|
||||
:colors ["red-50" "#ffebee"
|
||||
"red-100" "#ffcdd2"
|
||||
"red-200" "#ef9a9a"
|
||||
|
@ -509,5 +521,4 @@
|
|||
"blue-grey-900" "#263238"
|
||||
"white" "#ffffff"
|
||||
"black" "#000000"]}
|
||||
]}
|
||||
|
||||
]
|
||||
|
|
BIN
sample_media/images/unsplash/anna-pelzer.jpg
Normal file
BIN
sample_media/images/unsplash/anna-pelzer.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 120 KiB |
BIN
sample_media/images/unsplash/bruna-branco.jpg
Normal file
BIN
sample_media/images/unsplash/bruna-branco.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 156 KiB |
BIN
sample_media/images/unsplash/cayla1.jpg
Normal file
BIN
sample_media/images/unsplash/cayla1.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 75 KiB |
BIN
sample_media/images/unsplash/charles-deluvio.jpg
Normal file
BIN
sample_media/images/unsplash/charles-deluvio.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 45 KiB |
BIN
sample_media/images/unsplash/dan-gold.jpg
Normal file
BIN
sample_media/images/unsplash/dan-gold.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 185 KiB |
BIN
sample_media/images/unsplash/dose-juice.jpg
Normal file
BIN
sample_media/images/unsplash/dose-juice.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
Loading…
Add table
Reference in a new issue