diff --git a/backend/resources/migrations/0012-make-libraries-linked-to-a-file.sql b/backend/resources/migrations/0012-make-libraries-linked-to-a-file.sql new file mode 100644 index 000000000..398ca880c --- /dev/null +++ b/backend/resources/migrations/0012-make-libraries-linked-to-a-file.sql @@ -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); + diff --git a/backend/scripts/psql.sh b/backend/scripts/psql.sh new file mode 100755 index 000000000..5edf959d8 --- /dev/null +++ b/backend/scripts/psql.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +PGPASSWORD=$UXBOX_DATABASE_PASSWORD psql $UXBOX_DATABASE_URI -U $UXBOX_DATABASE_USERNAME diff --git a/backend/src/uxbox/images.clj b/backend/src/uxbox/images.clj index 3ae3fd03c..7b211debf 100644 --- a/backend/src/uxbox/images.clj +++ b/backend/src/uxbox/images.clj @@ -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] diff --git a/backend/src/uxbox/media_loader.clj b/backend/src/uxbox/media_loader.clj index 988bffb66..fe791cdf9 100644 --- a/backend/src/uxbox/media_loader.clj +++ b/backend/src/uxbox/media_loader.clj @@ -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]] diff --git a/backend/src/uxbox/migrations.clj b/backend/src/uxbox/migrations.clj index 1fdf5c5fa..781b27db4 100644 --- a/backend/src/uxbox/migrations.clj +++ b/backend/src/uxbox/migrations.clj @@ -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 diff --git a/backend/src/uxbox/services/mutations/colors.clj b/backend/src/uxbox/services/mutations/colors.clj index 82428854f..ee6143027 100644 --- a/backend/src/uxbox/services/mutations/colors.clj +++ b/backend/src/uxbox/services/mutations/colors.clj @@ -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) diff --git a/backend/src/uxbox/services/mutations/files.clj b/backend/src/uxbox/services/mutations/files.clj index d128d46f6..8cd7d9e44 100644 --- a/backend/src/uxbox/services/mutations/files.clj +++ b/backend/src/uxbox/services/mutations/files.clj @@ -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) diff --git a/backend/src/uxbox/services/mutations/images.clj b/backend/src/uxbox/services/mutations/images.clj index af5941190..08ba7ccdd 100644 --- a/backend/src/uxbox/services/mutations/images.clj +++ b/backend/src/uxbox/services/mutations/images.clj @@ -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 diff --git a/backend/src/uxbox/services/mutations/teams.clj b/backend/src/uxbox/services/mutations/teams.clj index 62c977a76..5867f90ee 100644 --- a/backend/src/uxbox/services/mutations/teams.clj +++ b/backend/src/uxbox/services/mutations/teams.clj @@ -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 diff --git a/backend/src/uxbox/services/queries/colors.clj b/backend/src/uxbox/services/queries/colors.clj index e03c3276c..965e091b2 100644 --- a/backend/src/uxbox/services/queries/colors.clj +++ b/backend/src/uxbox/services/queries/colors.clj @@ -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) diff --git a/backend/src/uxbox/services/queries/files.clj b/backend/src/uxbox/services/queries/files.clj index 66ca104f2..3991dc4c6 100644 --- a/backend/src/uxbox/services/queries/files.clj +++ b/backend/src/uxbox/services/queries/files.clj @@ -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}] diff --git a/backend/src/uxbox/services/queries/images.clj b/backend/src/uxbox/services/queries/images.clj index 7563138d9..4653819d5 100644 --- a/backend/src/uxbox/services/queries/images.clj +++ b/backend/src/uxbox/services/queries/images.clj @@ -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") diff --git a/backend/src/uxbox/services/queries/projects.clj b/backend/src/uxbox/services/queries/projects.clj index 6636c4b92..d7b254cde 100644 --- a/backend/src/uxbox/services/queries/projects.clj +++ b/backend/src/uxbox/services/queries/projects.clj @@ -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 diff --git a/backend/src/uxbox/services/queries/teams.clj b/backend/src/uxbox/services/queries/teams.clj index 43a2d4844..031b12e0f 100644 --- a/backend/src/uxbox/services/queries/teams.clj +++ b/backend/src/uxbox/services/queries/teams.clj @@ -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)))) diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index ebc7095b7..7fc274e08 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -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" ], diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index 122715b6a..e613cbd47 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -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'; diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss new file mode 100644 index 000000000..713720ccd --- /dev/null +++ b/frontend/resources/styles/main/partials/sidebar-assets.scss @@ -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 +// Copyright (c) 2015-2016 Juan de la Cruz + +.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; + } + } +} diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index 27d80a3d7..3bd89a0f9 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -25,7 +25,7 @@ $width-settings-bar: 15rem; } .settings-bar-inside { - align-items: center; + align-items: flex-start; display: grid; grid-template-columns: 100%; diff --git a/frontend/src/uxbox/main/data/colors.cljs b/frontend/src/uxbox/main/data/colors.cljs index 0f80684df..1a34fc97d 100644 --- a/frontend/src/uxbox/main/data/colors.cljs +++ b/frontend/src/uxbox/main/data/colors.cljs @@ -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]))))) + diff --git a/frontend/src/uxbox/main/data/images.cljs b/frontend/src/uxbox/main/data/images.cljs index 58c8a2f6e..33e00d05a 100644 --- a/frontend/src/uxbox/main/data/images.cljs +++ b/frontend/src/uxbox/main/data/images.cljs @@ -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))))) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index e2f1b9225..84416ca49 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -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) diff --git a/frontend/src/uxbox/main/data/workspace/persistence.cljs b/frontend/src/uxbox/main/data/workspace/persistence.cljs index 73470f82c..f0a72e8ed 100644 --- a/frontend/src/uxbox/main/data/workspace/persistence.cljs +++ b/frontend/src/uxbox/main/data/workspace/persistence.cljs @@ -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 diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index 23a9394f2..edff25f5b 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -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)) diff --git a/frontend/src/uxbox/main/ui/components/context_menu.cljs b/frontend/src/uxbox/main/ui/components/context_menu.cljs index df6fa4f69..e07788b15 100644 --- a/frontend/src/uxbox/main/ui/components/context_menu.cljs +++ b/frontend/src/uxbox/main/ui/components/context_menu.cljs @@ -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))) diff --git a/frontend/src/uxbox/main/ui/icons.cljs b/frontend/src/uxbox/main/ui/icons.cljs index e2a12b1d9..731ba3bbe 100644 --- a/frontend/src/uxbox/main/ui/icons.cljs +++ b/frontend/src/uxbox/main/ui/icons.cljs @@ -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)) diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index a3bf31006..ca4938e51 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -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) diff --git a/frontend/src/uxbox/main/ui/workspace/left_toolbar.cljs b/frontend/src/uxbox/main/ui/workspace/left_toolbar.cljs index d2aaaa355..a97a059d4 100644 --- a/frontend/src/uxbox/main/ui/workspace/left_toolbar.cljs +++ b/frontend/src/uxbox/main/ui/workspace/left_toolbar.cljs @@ -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"} diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar.cljs index f78465bea..7734c749a 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar.cljs @@ -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) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/assets.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/assets.cljs new file mode 100644 index 000000000..1cf18c409 --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/assets.cljs @@ -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)}]])) + diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs deleted file mode 100644 index 557d16cb3..000000000 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/icons.cljs +++ /dev/null @@ -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}]]])) diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/libraries.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/libraries.cljs index 8a410d504..4f6f607d0 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/libraries.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/libraries.cljs @@ -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)}]) diff --git a/frontend/src/uxbox/util/dom/dnd.cljs b/frontend/src/uxbox/util/dom/dnd.cljs index ed840b88c..5f0a0a4cf 100644 --- a/frontend/src/uxbox/util/dom/dnd.cljs +++ b/frontend/src/uxbox/util/dom/dnd.cljs @@ -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! diff --git a/sample_media/config.edn b/sample_media/config.edn index 082c93749..3a68db24c 100644 --- a/sample_media/config.edn +++ b/sample_media/config.edn @@ -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"]} - ]} - +] diff --git a/sample_media/images/unsplash/anna-pelzer.jpg b/sample_media/images/unsplash/anna-pelzer.jpg new file mode 100644 index 000000000..77a9a0eef Binary files /dev/null and b/sample_media/images/unsplash/anna-pelzer.jpg differ diff --git a/sample_media/images/unsplash/bruna-branco.jpg b/sample_media/images/unsplash/bruna-branco.jpg new file mode 100644 index 000000000..08fb25864 Binary files /dev/null and b/sample_media/images/unsplash/bruna-branco.jpg differ diff --git a/sample_media/images/unsplash/cayla1.jpg b/sample_media/images/unsplash/cayla1.jpg new file mode 100644 index 000000000..9a84f0b98 Binary files /dev/null and b/sample_media/images/unsplash/cayla1.jpg differ diff --git a/sample_media/images/unsplash/charles-deluvio.jpg b/sample_media/images/unsplash/charles-deluvio.jpg new file mode 100644 index 000000000..e763158d8 Binary files /dev/null and b/sample_media/images/unsplash/charles-deluvio.jpg differ diff --git a/sample_media/images/unsplash/dan-gold.jpg b/sample_media/images/unsplash/dan-gold.jpg new file mode 100644 index 000000000..0ad02f178 Binary files /dev/null and b/sample_media/images/unsplash/dan-gold.jpg differ diff --git a/sample_media/images/unsplash/dose-juice.jpg b/sample_media/images/unsplash/dose-juice.jpg new file mode 100644 index 000000000..baf50fbc8 Binary files /dev/null and b/sample_media/images/unsplash/dose-juice.jpg differ