From 752038039ce3b1b5a05c750c897a71c33487ca71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Wed, 5 Aug 2020 13:30:26 +0200 Subject: [PATCH] :tada: Link files to libraries --- .../0017-link-files-to-libraries.sql | 15 + backend/src/uxbox/migrations.clj | 6 +- .../src/uxbox/services/mutations/files.clj | 46 ++- backend/src/uxbox/services/queries/files.clj | 143 ++++++-- common/uxbox/common/data.cljc | 9 + frontend/resources/images/icons/chain.svg | 2 +- frontend/resources/images/icons/libraries.svg | 3 + frontend/resources/locales.json | 347 ++++++++++++------ frontend/resources/styles/main-default.scss | 1 + .../styles/main/layouts/libraries-page.scss | 5 + .../resources/styles/main/partials/modal.scss | 153 ++++++++ .../styles/main/partials/sidebar-assets.scss | 36 +- .../styles/main/partials/sidebar.scss | 16 + frontend/src/uxbox/main/data/colors.cljs | 9 +- frontend/src/uxbox/main/data/workspace.cljs | 5 +- .../main/data/workspace/persistence.cljs | 222 ++++++++--- frontend/src/uxbox/main/refs.cljs | 8 +- frontend/src/uxbox/main/ui/icons.cljs | 1 + frontend/src/uxbox/main/ui/workspace.cljs | 1 - .../uxbox/main/ui/workspace/libraries.cljs | 155 ++++++++ .../main/ui/workspace/sidebar/assets.cljs | 208 ++++++----- frontend/src/uxbox/util/data.cljs | 7 + 22 files changed, 1080 insertions(+), 318 deletions(-) create mode 100644 backend/resources/migrations/0017-link-files-to-libraries.sql create mode 100644 frontend/resources/images/icons/libraries.svg create mode 100644 frontend/resources/styles/main/layouts/libraries-page.scss create mode 100644 frontend/src/uxbox/main/ui/workspace/libraries.cljs diff --git a/backend/resources/migrations/0017-link-files-to-libraries.sql b/backend/resources/migrations/0017-link-files-to-libraries.sql new file mode 100644 index 0000000000..4afc04ca42 --- /dev/null +++ b/backend/resources/migrations/0017-link-files-to-libraries.sql @@ -0,0 +1,15 @@ +CREATE TABLE file_library_rel ( + file_id uuid NOT NULL REFERENCES file(id) ON DELETE CASCADE, + library_file_id uuid NOT NULL REFERENCES file(id) ON DELETE RESTRICT, + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + + PRIMARY KEY (file_id, library_file_id) +); + +COMMENT ON TABLE file_library_rel + IS 'Relation between files and the shared library files they use (NM)'; + +CREATE INDEX file_library_rel__file_id__idx + ON file_library_rel(file_id); + diff --git a/backend/src/uxbox/migrations.clj b/backend/src/uxbox/migrations.clj index 4e70057532..d44b0f158b 100644 --- a/backend/src/uxbox/migrations.clj +++ b/backend/src/uxbox/migrations.clj @@ -79,7 +79,11 @@ {:desc "Truncate & alter tokens tables" :name "0016-truncate-and-alter-tokens-table" - :fn (mg/resource "migrations/0016-truncate-and-alter-tokens-table.sql")}]}) + :fn (mg/resource "migrations/0016-truncate-and-alter-tokens-table.sql")} + + {:desc "Link files to libraries" + :name "0017-link-files-to-libraries" + :fn (mg/resource "migrations/0017-link-files-to-libraries.sql")}]}) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Entry point diff --git a/backend/src/uxbox/services/mutations/files.clj b/backend/src/uxbox/services/mutations/files.clj index d5a1caac5b..3b1e2ee231 100644 --- a/backend/src/uxbox/services/mutations/files.clj +++ b/backend/src/uxbox/services/mutations/files.clj @@ -123,7 +123,7 @@ {:id id})) -;; --- Mutation: Delete Project File +;; --- Mutation: Delete File (declare mark-file-deleted) @@ -149,3 +149,47 @@ {:id id}) nil) + +;; --- Mutation: Link file to library + +(declare link-file-to-library) + +(s/def ::link-file-to-library + (s/keys :req-un [::profile-id ::file-id ::library-id])) + +(sm/defmutation ::link-file-to-library + [{:keys [profile-id file-id library-id] :as params}] + (when (= file-id library-id) + (ex/raise :type :validation + :code :invalid-library + :hint "A file cannot be linked to itself")) + (db/with-atomic [conn db/pool] + (files/check-edition-permissions! conn profile-id file-id) + (link-file-to-library conn params))) + +(defn- link-file-to-library + [conn {:keys [file-id library-id] :as params}] + (db/insert! conn :file-library-rel + {:file-id file-id + :library-file-id library-id})) + + +;; --- Mutation: Unlink file from library + +(declare unlink-file-from-library) + +(s/def ::unlink-file-from-library + (s/keys :req-un [::profile-id ::file-id ::library-id])) + +(sm/defmutation ::unlink-file-from-library + [{:keys [profile-id file-id library-id] :as params}] + (db/with-atomic [conn db/pool] + (files/check-edition-permissions! conn profile-id file-id) + (unlink-file-from-library conn params))) + +(defn- unlink-file-from-library + [conn {:keys [file-id library-id] :as params}] + (db/delete! conn :file-library-rel + {:file-id file-id + :library-file-id library-id})) + diff --git a/backend/src/uxbox/services/queries/files.clj b/backend/src/uxbox/services/queries/files.clj index b9ccd7966b..3434b42940 100644 --- a/backend/src/uxbox/services/queries/files.clj +++ b/backend/src/uxbox/services/queries/files.clj @@ -148,32 +148,6 @@ (mapv decode-row))) -;; --- Query: Shared Files - -(def ^:private sql:shared-files - "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 - left join page as pg on (f.id = pg.file_id) - where is_shared = true - and f.deleted_at is null - and pg.deleted_at is null - window pages_w as (partition by f.id order by pg.ordering - range between unbounded preceding - and unbounded following) - order by f.modified_at desc") - -(s/def ::shared-files - (s/keys :req-un [::profile-id])) - -(sq/defquery ::shared-files - [{:keys [profile-id] :as params}] - (->> (db/exec! db/pool [sql:shared-files]) - (mapv decode-row))) - - ;; --- Query: File Permissions (def ^:private sql:file-permissions @@ -236,6 +210,25 @@ range between unbounded preceding and unbounded following)") +(defn retrieve-file + [conn id] + (let [row (db/exec-one! conn [sql:file id])] + (when-not row + (ex/raise :type :not-found)) + (decode-row row))) + +(s/def ::file + (s/keys :req-un [::profile-id ::id])) + +(sq/defquery ::file + [{:keys [profile-id id] :as params}] + (db/with-atomic [conn db/pool] + (check-edition-permissions! conn profile-id id) + (retrieve-file conn id))) + + +;; --- Query: File users + (def ^:private sql:file-users "select pf.id, pf.fullname, pf.photo from profile as pf @@ -249,13 +242,6 @@ inner join file as f on (p.id = f.project_id) where f.id = ?") -(defn retrieve-file - [conn id] - (let [row (db/exec-one! conn [sql:file id])] - (when-not row - (ex/raise :type :not-found)) - (decode-row row))) - (defn retrieve-file-users [conn id] (->> (db/exec! conn [sql:file-users id id]) @@ -270,14 +256,93 @@ (check-edition-permissions! conn profile-id id) (retrieve-file-users conn id))) -(s/def ::file - (s/keys :req-un [::profile-id ::id])) -(sq/defquery ::file - [{:keys [profile-id id] :as params}] +;; --- Query: Shared Library Files + +(def ^:private sql:shared-files + "select distinct + f.*, + array_agg(pg.id) over pages_w as pages, + first_value(pg.data) over pages_w as data, + (select count(*) from color as c + where c.file_id = f.id + and c.deleted_at is null) as colors_count, + (select count(*) from media_object as m + where m.file_id = f.id + and m.is_local = false + and m.deleted_at is null) as graphics_count + from file as f + left join page as pg on (f.id = pg.file_id) + where is_shared = true + and f.deleted_at is null + and pg.deleted_at is null + window pages_w as (partition by f.id order by pg.ordering + range between unbounded preceding + and unbounded following) + order by f.modified_at desc") + +(s/def ::shared-files + (s/keys :req-un [::profile-id])) + +(sq/defquery ::shared-files + [{:keys [profile-id] :as params}] + (->> (db/exec! db/pool [sql:shared-files]) + (mapv decode-row))) + + +;; --- Query: File Libraries used by a File + +(def ^:private sql:file-libraries + "select fl.*, + array_agg(pg.id) over pages_w as pages, + first_value(pg.data) over pages_w as data + from file as fl + left join page as pg on (fl.id = pg.file_id) + inner join file_library_rel as flr on (flr.library_file_id = fl.id) + where flr.file_id = ? + and fl.deleted_at is null + and pg.deleted_at is null + window pages_w as (partition by fl.id order by pg.ordering + range between unbounded preceding + and unbounded following)") + +(defn retrieve-file-libraries + [conn file-id] + (->> (db/exec! conn [sql:file-libraries file-id]) + (mapv decode-row))) + +(s/def ::file-libraries + (s/keys :req-un [::profile-id ::file-id])) + +(sq/defquery ::file-libraries + [{:keys [profile-id file-id] :as params}] (db/with-atomic [conn db/pool] - (check-edition-permissions! conn profile-id id) - (retrieve-file conn id))) + (check-edition-permissions! conn profile-id file-id) + (retrieve-file-libraries conn file-id))) + + +;; --- Query: Single File Library + +(def ^:private sql:file-library + "select fl.* + from file as fl + where fl.id = ?") + +(defn retrieve-file-library + [conn file-id] + (let [row (db/exec-one! conn [sql:file-library file-id])] + (when-not row + (ex/raise :type :not-found)) + row)) + +(s/def ::file-library + (s/keys :req-un [::profile-id ::file-id])) + +(sq/defquery ::file-library + [{:keys [profile-id file-id] :as params}] + (db/with-atomic [conn db/pool] + (check-edition-permissions! conn profile-id file-id) ;; TODO: this should check read permissions + (retrieve-file-library conn file-id))) ;; --- Helpers diff --git a/common/uxbox/common/data.cljc b/common/uxbox/common/data.cljc index 77236bbeb0..18c374dbd5 100644 --- a/common/uxbox/common/data.cljc +++ b/common/uxbox/common/data.cljc @@ -100,6 +100,15 @@ [coll v] (index-of-pred coll #(= % v))) +(defn replace-by-id + ([value] + (map (fn [item] + (if (= (:id item) (:id value)) + value + item)))) + ([coll value] + (sequence (replace-by-id value) coll))) + (defn remove-nil-vals "Given a map, return a map removing key-value pairs when value is `nil`." diff --git a/frontend/resources/images/icons/chain.svg b/frontend/resources/images/icons/chain.svg index 926f2eb1a3..a34db111ec 100644 --- a/frontend/resources/images/icons/chain.svg +++ b/frontend/resources/images/icons/chain.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/frontend/resources/images/icons/libraries.svg b/frontend/resources/images/icons/libraries.svg new file mode 100644 index 0000000000..3e4e98b759 --- /dev/null +++ b/frontend/resources/images/icons/libraries.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 0b236239b0..04cfddd0cf 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -18,7 +18,7 @@ } }, "auth.create-demo-profile" : { - "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:116", "src/uxbox/main/ui/auth/login.cljs:115" ], + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:116", "src/uxbox/main/ui/auth/login.cljs:121" ], "translations" : { "en" : "Create demo account", "fr" : "Créer un compte de démonstration", @@ -27,7 +27,7 @@ } }, "auth.create-demo-profile-label" : { - "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:113", "src/uxbox/main/ui/auth/login.cljs:112" ], + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:113", "src/uxbox/main/ui/auth/login.cljs:118" ], "translations" : { "en" : "Just wanna try it?", "fr" : "Vous voulez juste essayer?", @@ -45,7 +45,7 @@ } }, "auth.email-label" : { - "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:82", "src/uxbox/main/ui/auth/recovery_request.cljs:46", "src/uxbox/main/ui/auth/login.cljs:73" ], + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:82", "src/uxbox/main/ui/auth/recovery_request.cljs:46", "src/uxbox/main/ui/auth/login.cljs:74" ], "translations" : { "en" : "Email", "fr" : "Adresse email", @@ -54,7 +54,7 @@ } }, "auth.forgot-password" : { - "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:97" ], + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:103" ], "translations" : { "en" : "Forgot your password?", "fr" : "Mot de passe oublié?", @@ -99,7 +99,7 @@ } }, "auth.login-submit-label" : { - "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:81" ], + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:82" ], "translations" : { "en" : "Sign in", "fr" : "Se connecter", @@ -107,17 +107,8 @@ "es" : "Entrar" } }, - "auth.login-with-ldap-submit-label" : { - "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:108" ], - "translations" : { - "en" : "Sign in with LDAP", - "fr" : "Se connecter via LDAP", - "es" : "Entrar con LDAP", - "ru" : "Вход через LDAP" - } - }, "auth.login-subtitle" : { - "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:89" ], + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:95" ], "translations" : { "en" : "Enter your details below", "fr" : "Entrez vos informations ci-dessous", @@ -126,7 +117,7 @@ } }, "auth.login-title" : { - "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:88" ], + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:94" ], "translations" : { "en" : "Great to see you again!", "fr" : "Ravi de vous revoir!", @@ -134,6 +125,15 @@ "es" : "Encantados de volverte a ver" } }, + "auth.login-with-ldap-submit-label" : { + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:86" ], + "translations" : { + "en" : "Sign in with LDAP", + "fr" : "Se connecter via LDAP", + "ru" : "Вход через LDAP", + "es" : "Entrar con LDAP" + } + }, "auth.new-password-label" : { "used-in" : [ "src/uxbox/main/ui/auth/recovery.cljs:73" ], "translations" : { @@ -171,7 +171,7 @@ } }, "auth.password-label" : { - "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:86", "src/uxbox/main/ui/auth/login.cljs:79" ], + "used-in" : [ "src/uxbox/main/ui/auth/register.cljs:86", "src/uxbox/main/ui/auth/login.cljs:80" ], "translations" : { "en" : "Password", "fr" : "Mot de passe", @@ -225,7 +225,7 @@ } }, "auth.register" : { - "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:103" ], + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:109" ], "translations" : { "en" : "Sign up here", "fr" : "Inscrivez-vous ici", @@ -234,7 +234,7 @@ } }, "auth.register-label" : { - "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:100" ], + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:106" ], "translations" : { "en" : "No account yet?", "fr" : "Pas encore de compte?", @@ -504,48 +504,48 @@ "unused" : true }, "dashboard.library.menu.icons" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:89" ], "translations" : { "en" : "Icons", "fr" : "Icônes", "ru" : "Иконки", "es" : "Iconos" - } + }, + "unused" : true }, "dashboard.library.menu.images" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:93" ], "translations" : { "en" : "Images", "fr" : "Images", "ru" : "Изображения", "es" : "Imágenes" - } + }, + "unused" : true }, "dashboard.library.menu.palettes" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:97" ], "translations" : { "en" : "Palettes", "fr" : "Palettes", "ru" : "Палитры", "es" : "Paletas" - } + }, + "unused" : true }, "dashboard.search.no-matches-for" : { "used-in" : [ "src/uxbox/main/ui/dashboard/search.cljs:48" ], "translations" : { - "en" : "No matches found for \"%s\"", - "fr" : "Aucune correspondance pour \"%s\"", - "ru" : "Совпадений для \"%s\" не найдено", - "es" : "No se encuentra \"%s\"" + "en" : "No matches found for “%s“", + "fr" : "Aucune correspondance pour “%s“", + "ru" : "Совпадений для “%s“ не найдено", + "es" : "No se encuentra “%s“" } }, "dashboard.search.searching-for" : { "used-in" : [ "src/uxbox/main/ui/dashboard/search.cljs:44" ], "translations" : { - "en" : "Searching for \"%s\"...", - "fr" : "Recherche de \"%s\"...", - "ru" : "Ищу \"%s\"...", - "es" : "Buscando \"%s\"..." + "en" : "Searching for “%s“...", + "fr" : "Recherche de “%s“...", + "ru" : "Ищу “%s“...", + "es" : "Buscando “%s“..." } }, "dashboard.search.type-something" : { @@ -594,25 +594,25 @@ } }, "ds.button.delete" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:145", "src/uxbox/main/ui/dashboard/library.cljs:210", "src/uxbox/main/ui/dashboard/library.cljs:249", "src/uxbox/main/ui/dashboard/library.cljs:290" ], "translations" : { "en" : "Delete", "fr" : "Supprimer", "ru" : "Удалить", "es" : "Borrar" - } + }, + "unused" : true }, "ds.button.rename" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:142" ], "translations" : { "en" : "Rename", "fr" : "Renommer", "ru" : "Переименовать", "es" : "Renombrar" - } + }, + "unused" : true }, "ds.button.save" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:55", "src/uxbox/main/ui/workspace/sidebar/assets.cljs:69" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:62" ], "translations" : { "en" : "Save", "fr" : "Sauvegarder", @@ -711,7 +711,7 @@ } }, "errors.auth.unauthorized" : { - "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:62" ], + "used-in" : [ "src/uxbox/main/ui/auth/login.cljs:63" ], "translations" : { "en" : "Username or password seems to be wrong.", "fr" : "Le nom d'utilisateur ou le mot de passe semble être faux.", @@ -738,7 +738,7 @@ } }, "errors.generic" : { - "used-in" : [ "src/uxbox/main/ui.cljs:204", "src/uxbox/main/ui/settings/profile.cljs:38", "src/uxbox/main/ui/auth.cljs:91" ], + "used-in" : [ "src/uxbox/main/ui.cljs:183", "src/uxbox/main/ui/settings/profile.cljs:38", "src/uxbox/main/ui/auth.cljs:91" ], "translations" : { "en" : "Something wrong has happened.", "fr" : "Quelque chose c'est mal passé.", @@ -747,7 +747,7 @@ } }, "errors.media-format-unsupported" : { - "used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:406", "src/uxbox/main/data/users.cljs:177", "src/uxbox/main/data/images.cljs:376" ], + "used-in" : [ "src/uxbox/main/data/media.cljs:39" ], "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).", @@ -756,7 +756,7 @@ } }, "errors.media-too-large" : { - "used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:404", "src/uxbox/main/data/users.cljs:175", "src/uxbox/main/data/images.cljs:374" ], + "used-in" : [ "src/uxbox/main/data/media.cljs:37" ], "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).", @@ -765,7 +765,7 @@ } }, "errors.media-type-mismatch" : { - "used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:371", "src/uxbox/main/data/workspace/persistence.cljs:421", "src/uxbox/main/data/users.cljs:191", "src/uxbox/main/data/images.cljs:391" ], + "used-in" : [ "src/uxbox/main/data/media.cljs:62" ], "translations" : { "en" : "Seems that the contents of the image does not match the file extension.", "fr" : "", @@ -774,7 +774,7 @@ } }, "errors.media-type-not-allowed" : { - "used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:368", "src/uxbox/main/data/workspace/persistence.cljs:418", "src/uxbox/main/data/users.cljs:188", "src/uxbox/main/data/images.cljs:388" ], + "used-in" : [ "src/uxbox/main/data/media.cljs:59" ], "translations" : { "en" : "Seems that this is not a valid image.", "fr" : "", @@ -783,7 +783,7 @@ } }, "errors.network" : { - "used-in" : [ "src/uxbox/main/ui.cljs:198" ], + "used-in" : [ "src/uxbox/main/ui.cljs:177" ], "translations" : { "en" : "Unable to connect to backend server.", "fr" : "Impossible de se connecter au serveur principal.", @@ -819,7 +819,7 @@ } }, "errors.unexpected-error" : { - "used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:374", "src/uxbox/main/data/workspace/persistence.cljs:424", "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/media.cljs:65", "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", @@ -864,7 +864,7 @@ } }, "media.loading" : { - "used-in" : [ "src/uxbox/main/data/workspace/persistence.cljs:382", "src/uxbox/main/data/workspace/persistence.cljs:433", "src/uxbox/main/data/users.cljs:201", "src/uxbox/main/data/images.cljs:403" ], + "used-in" : [ "src/uxbox/main/data/media.cljs:44" ], "translations" : { "en" : "Loading image...", "fr" : "Chargement de l'image...", @@ -873,7 +873,7 @@ } }, "modal.create-color.new-color" : { - "used-in" : [ "src/uxbox/main/ui/dashboard/library.cljs:49", "src/uxbox/main/ui/workspace/sidebar/assets.cljs:62" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:55" ], "translations" : { "en" : "New Color", "fr" : "Nouvelle couleur", @@ -1053,7 +1053,7 @@ } }, "settings.multiple" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:132", "src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs:117", "src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs:126", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:124", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:227", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:240" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:137", "src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs:117", "src/uxbox/main/ui/workspace/sidebar/options/rows/color_row.cljs:126", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:124", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:227", "src/uxbox/main/ui/workspace/sidebar/options/text.cljs:240" ], "translations" : { "en" : "Mixed", "fr" : null, @@ -1089,7 +1089,7 @@ } }, "settings.notifications.email-not-verified" : { - "used-in" : [ "src/uxbox/main/ui/dashboard.cljs:118" ], + "used-in" : [ "src/uxbox/main/ui/dashboard.cljs:98" ], "translations" : { "en" : "Your email address has not been verified yet. Please check your inbox at “%s” for a confirmation email.", "fr" : "Votre adresse e-mail n'a pas encore été vérifiée. Veuillez vérifier votre boîte de réception à “%s” pour un e-mail de confirmation.", @@ -1116,7 +1116,7 @@ } }, "settings.notifications.profile-deletion-not-allowed" : { - "used-in" : [ "src/uxbox/main/data/auth.cljs:136" ], + "used-in" : [ "src/uxbox/main/data/auth.cljs:160" ], "translations" : { "en" : "You can't delete you profile. Reasign your teams before proceed.", "fr" : "Vous ne pouvez pas supprimer votre profil. Réassignez vos équipes avant de continuer.", @@ -1449,7 +1449,7 @@ } }, "workspace.assets.assets" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:331" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:328" ], "translations" : { "en" : "Assets", "fr" : "", @@ -1485,7 +1485,7 @@ } }, "workspace.assets.colors" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:247" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:240" ], "translations" : { "en" : "Colors", "fr" : "", @@ -1494,7 +1494,7 @@ } }, "workspace.assets.delete" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:136", "src/uxbox/main/ui/workspace/sidebar/assets.cljs:231" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:129", "src/uxbox/main/ui/workspace/sidebar/assets.cljs:224" ], "translations" : { "en" : "Delete", "fr" : "", @@ -1503,7 +1503,7 @@ } }, "workspace.assets.edit" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:230" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:223" ], "translations" : { "en" : "Edit", "fr" : "", @@ -1512,7 +1512,7 @@ } }, "workspace.assets.file-library" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:272" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:354" ], "translations" : { "en" : "File library", "fr" : "", @@ -1521,7 +1521,7 @@ } }, "workspace.assets.graphics" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:113" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:106" ], "translations" : { "en" : "Graphics", "fr" : "", @@ -1529,8 +1529,17 @@ "es" : "Gráficos" } }, + "workspace.assets.libraries" : { + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:331" ], + "translations" : { + "en" : "Libraries", + "fr" : "", + "ru" : "", + "es" : "Bibliotecas" + } + }, "workspace.assets.not-found" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:287" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:281" ], "translations" : { "en" : "No assets found", "fr" : "", @@ -1539,7 +1548,7 @@ } }, "workspace.assets.rename" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:229" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:222" ], "translations" : { "en" : "Rename", "fr" : "", @@ -1557,7 +1566,7 @@ } }, "workspace.assets.shared" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:274" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/assets.cljs:268" ], "translations" : { "en" : "SHARED", "fr" : "", @@ -1601,6 +1610,15 @@ "es" : "Alinear a la rejilla" } }, + "workspace.header.menu.hide-assets" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:132" ], + "translations" : { + "en" : "Hide assets", + "fr" : "", + "ru" : "", + "es" : "Ocultar recursos" + } + }, "workspace.header.menu.hide-grid" : { "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:104" ], "translations" : { @@ -1619,15 +1637,6 @@ "es" : "Ocultar capas" } }, - "workspace.header.menu.hide-assets" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:132" ], - "translations" : { - "en" : "Hide assets", - "fr" : "", - "ru" : "", - "es" : "Ocultar recursos" - } - }, "workspace.header.menu.hide-palette" : { "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:125" ], "translations" : { @@ -1646,6 +1655,15 @@ "es" : "Ocultar reglas" } }, + "workspace.header.menu.show-assets" : { + "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:133" ], + "translations" : { + "en" : "Show assets", + "fr" : "", + "ru" : "", + "es" : "Mostrar recursos" + } + }, "workspace.header.menu.show-grid" : { "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:105" ], "translations" : { @@ -1664,15 +1682,6 @@ "es" : "Mostrar capas" } }, - "workspace.header.menu.show-assets" : { - "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:133" ], - "translations" : { - "en" : "Show assets", - "fr" : "", - "ru" : "", - "es" : "Mostrar recursos" - } - }, "workspace.header.menu.show-palette" : { "used-in" : [ "src/uxbox/main/ui/workspace/header.cljs:126" ], "translations" : { @@ -1700,59 +1709,167 @@ "es" : "Modo de visualización (Ctrl + P)" } }, + "workspace.libraries.add" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:98" ], + "translations" : { + "en" : "Add", + "fr" : "", + "ru" : "", + "es" : "Añadir" + } + }, + "workspace.libraries.colors" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:55" ], + "translations" : { + "en" : "%s colors", + "fr" : "", + "ru" : "", + "es" : "%s colors" + } + }, + "workspace.libraries.file-library" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:61" ], + "translations" : { + "en" : "File library", + "fr" : "", + "ru" : "", + "es" : "Biblioteca de este archivo" + } + }, + "workspace.libraries.graphics" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:52" ], + "translations" : { + "en" : "%s graphics", + "fr" : "", + "ru" : "", + "es" : "%s gráficos" + } + }, + "workspace.libraries.in-this-file" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:58" ], + "translations" : { + "en" : "LIBRARIES IN THIS FILE", + "fr" : "", + "ru" : "", + "es" : "BIBLIOTECAS EN ESTE ARCHIVO" + } + }, + "workspace.libraries.libraries" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:140" ], + "translations" : { + "en" : "LIBRARIES", + "fr" : "", + "ru" : "", + "es" : "BIBLIOTECAS" + } + }, + "workspace.libraries.no-matches-for" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:104" ], + "translations" : { + "en" : "No matches found for “%s“", + "fr" : "Aucune correspondance pour “%s“", + "ru" : "Совпадений для “%s“ не найдено", + "es" : "No se encuentra “%s“" + } + }, + "workspace.libraries.no-shared-libraries-available" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:103" ], + "translations" : { + "en" : "There are no Shared Libraries available", + "fr" : "", + "ru" : "", + "es" : "No hay bibliotecas compartidas disponibles" + } + }, + "workspace.libraries.remove" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:72" ], + "translations" : { + "en" : "Remove", + "fr" : "", + "ru" : "", + "es" : "Quitar" + } + }, + "workspace.libraries.search-shared-libraries" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:79" ], + "translations" : { + "en" : "Search shared libraries", + "fr" : "", + "ru" : "", + "es" : "Buscar bibliotecas compartidas" + } + }, + "workspace.libraries.shared-libraries" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:76" ], + "translations" : { + "en" : "SHARED LIBRARIES", + "fr" : "", + "ru" : "", + "es" : "BIBLIOTECAS COMPARTIDAS" + } + }, + "workspace.libraries.updates" : { + "used-in" : [ "src/uxbox/main/ui/workspace/libraries.cljs:144" ], + "translations" : { + "en" : "UPDATES", + "fr" : "", + "ru" : "", + "es" : "ACTUALIZACIONES" + } + }, "workspace.library.all" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/libraries.cljs:128" ], "translations" : { "en" : "All libraries", "fr" : "Toutes les librairies", "ru" : "Все библиотеки", "es" : "Todas" - } + }, + "unused" : true }, "workspace.library.icons" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/libraries.cljs:178" ], "translations" : { "en" : "Icons", "fr" : "Icônes", "ru" : "Иконки", "es" : "Iconos" - } + }, + "unused" : true }, "workspace.library.images" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/libraries.cljs:183" ], "translations" : { "en" : "Images", "fr" : "Images", "ru" : "Изображения", "es" : "Imágenes" - } + }, + "unused" : true }, "workspace.library.libraries" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/libraries.cljs:160" ], "translations" : { "en" : "Libraries", "fr" : "Librairies", "ru" : "Библиотеки", "es" : "Bibliotecas" - } + }, + "unused" : true }, "workspace.library.own" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/libraries.cljs:129" ], "translations" : { "en" : "My libraries", "fr" : "Mes librairies", "ru" : "Мои библиотеки", "es" : "Mis bibliotecas" - } + }, + "unused" : true }, "workspace.library.store" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/libraries.cljs:130" ], "translations" : { "en" : "Store libraries", "fr" : "Prédéfinies", "ru" : "Сохраненные библиотеки", "es" : "Predefinidas" - } + }, + "unused" : true }, "workspace.options.canvas-background" : { "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:37" ], @@ -1780,21 +1897,21 @@ } }, "workspace.options.export-object" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:154" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:155" ], "translations" : { "en" : "Export shape", "ru" : "Экспорт фигуры" } }, "workspace.options.exporting-object" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:153" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/exports.cljs:154" ], "translations" : { "en" : "Exporting...", "ru" : "Экспортирую ..." } }, "workspace.options.fill" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:50" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:52" ], "translations" : { "en" : "Fill", "fr" : "Remplissage", @@ -1992,7 +2109,7 @@ } }, "workspace.options.group-fill" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:49" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:51" ], "translations" : { "en" : "Group fill", "fr" : null, @@ -2001,7 +2118,7 @@ } }, "workspace.options.group-stroke" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:65" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:70" ], "translations" : { "en" : "Group stroke", "fr" : null, @@ -2082,7 +2199,7 @@ } }, "workspace.options.selection-fill" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:48" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/fill.cljs:50" ], "translations" : { "en" : "Selection fill", "fr" : null, @@ -2091,7 +2208,7 @@ } }, "workspace.options.selection-stroke" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:64" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:69" ], "translations" : { "en" : "Selection stroke", "fr" : null, @@ -2118,7 +2235,7 @@ } }, "workspace.options.stroke" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:66" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:71" ], "translations" : { "en" : "Stroke", "fr" : "Bordure", @@ -2127,7 +2244,7 @@ } }, "workspace.options.stroke.center" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:139" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:144" ], "translations" : { "en" : "Center", "fr" : "Centre", @@ -2136,7 +2253,7 @@ } }, "workspace.options.stroke.dashed" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:149" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:154" ], "translations" : { "en" : "Dashed", "fr" : "Tiré", @@ -2145,7 +2262,7 @@ } }, "workspace.options.stroke.dotted" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:148" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:153" ], "translations" : { "en" : "Dotted", "fr" : "Pointillé", @@ -2154,7 +2271,7 @@ } }, "workspace.options.stroke.inner" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:140" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:145" ], "translations" : { "en" : "Inside", "fr" : "Intérieur", @@ -2163,7 +2280,7 @@ } }, "workspace.options.stroke.mixed" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:150" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:155" ], "translations" : { "en" : "Mixed", "fr" : "Mixte", @@ -2172,7 +2289,7 @@ } }, "workspace.options.stroke.outer" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:141" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:146" ], "translations" : { "en" : "Outside", "fr" : "Extérieur", @@ -2181,7 +2298,7 @@ } }, "workspace.options.stroke.solid" : { - "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:147" ], + "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:152" ], "translations" : { "en" : "Solid", "fr" : "Solide", @@ -2404,7 +2521,7 @@ } }, "workspace.toolbar.assets" : { - "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:105" ], + "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:99" ], "translations" : { "en" : "Assets (Ctrl + I)", "fr" : "", @@ -2413,7 +2530,7 @@ } }, "workspace.toolbar.circle" : { - "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:64" ], + "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:63" ], "translations" : { "en" : "Circle (E)", "fr" : "Cercle (E)", @@ -2422,7 +2539,7 @@ } }, "workspace.toolbar.color-palette" : { - "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:113" ], + "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:107" ], "translations" : { "en" : "Color Palette (---)", "fr" : "Palette de couleurs (---)", @@ -2431,7 +2548,7 @@ } }, "workspace.toolbar.curve" : { - "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:83" ], + "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:82" ], "translations" : { "en" : "Curve", "fr" : "Courbe", @@ -2440,7 +2557,7 @@ } }, "workspace.toolbar.frame" : { - "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:54" ], + "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:53" ], "translations" : { "en" : "Artboard (A)", "fr" : "Plan de travail (A)", @@ -2449,7 +2566,7 @@ } }, "workspace.toolbar.image" : { - "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:74" ], + "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:73" ], "translations" : { "en" : "Image (I)", "fr" : "Image (I)", @@ -2467,7 +2584,7 @@ "unused" : true }, "workspace.toolbar.path" : { - "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:88" ], + "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:87" ], "translations" : { "en" : "Path", "fr" : "Chemin", @@ -2476,7 +2593,7 @@ } }, "workspace.toolbar.rect" : { - "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:59" ], + "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:58" ], "translations" : { "en" : "Box (B)", "fr" : "Boîte (B)", @@ -2485,7 +2602,7 @@ } }, "workspace.toolbar.text" : { - "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:69" ], + "used-in" : [ "src/uxbox/main/ui/workspace/left_toolbar.cljs:68" ], "translations" : { "en" : "Text (T)", "fr" : "Texte (T)", @@ -2494,7 +2611,7 @@ } }, "workspace.viewport.click-to-close-path" : { - "used-in" : [ "src/uxbox/main/ui/workspace/drawarea.cljs:55" ], + "used-in" : [ "src/uxbox/main/ui/workspace/drawarea.cljs:59" ], "translations" : { "en" : "Click to close the path", "fr" : "Cliquez pour fermer le chemin", diff --git a/frontend/resources/styles/main-default.scss b/frontend/resources/styles/main-default.scss index 1c4b58582d..c5a90280b6 100644 --- a/frontend/resources/styles/main-default.scss +++ b/frontend/resources/styles/main-default.scss @@ -26,6 +26,7 @@ @import 'main/layouts/main-layout'; @import 'main/layouts/login'; @import 'main/layouts/projects-page'; +@import 'main/layouts/libraries-page'; @import 'main/layouts/recent-files-page'; @import "main/layouts/not-found"; @import "main/layouts/viewer"; diff --git a/frontend/resources/styles/main/layouts/libraries-page.scss b/frontend/resources/styles/main/layouts/libraries-page.scss new file mode 100644 index 0000000000..c9e39c846f --- /dev/null +++ b/frontend/resources/styles/main/layouts/libraries-page.scss @@ -0,0 +1,5 @@ +.libraries-page { + padding: 1rem; + height: 100%; + background-color: $color-white; +} diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index 12ded774f5..67eb1aebcf 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -136,3 +136,156 @@ } } +.libraries-dialog { + width: 920px; + height: 664px; + + .modal-content { + display: flex; + flex-grow: 1; + flex-direction: column; + padding: 0; + } + + .libraries-header { + border-bottom: 1px solid $color-gray-20; + padding: 2rem 1rem 0.5rem 1rem; + display: flex; + justify-content: center; + + .header-item { + cursor: pointer; + color: $color-gray-40; + font-size: $fs15; + + &.active { + color: $color-gray-60; + border-bottom: 2px solid $color-primary; + } + + &:not(:first-child) { + margin-left: 3rem; + } + } + } + + .libraries-content { + display: flex; + justify-content: stretch; + align-items: stretch; + flex-grow: 1; + padding: 0 $size-4; + color: $color-gray-40; + + .section { + flex-basis: 0; + flex-grow: 1; + padding: $size-4 0; + display: flex; + flex-direction: column; + + &:not(:first-child) { + border-left: 1px solid $color-gray-20; + } + } + + .section-title { + font-size: $fs15; + padding: 0 $size-4; + } + + .section-list { + flex-basis: 0; + flex-grow: 1; + padding: 0 $size-4; + overflow: auto; + + .section-list-item { + padding: $size-4 0; + border-bottom: 1px solid $color-gray-20; + position: relative; + + .item-name { + color: $color-gray-60; + font-size: $fs14; + } + + .item-contents { + color: $color-gray-40; + font-size: $fs12; + } + + .item-button { + position: absolute; + top: $size-4; + right: 0; + border: 1px solid $color-primary; + border-radius: 2px; + width: 4.5rem; + background: $color-primary; + color: $color-black; + padding: $size-2; + + &:hover { + color: $color-primary; + background: $color-black; + } + } + } + } + + .section-list-empty { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 0 $size-4; + flex-grow: 1; + + & svg { + fill: $color-gray-20; + height: 50px; + width: 50px; + margin-bottom: $size-4; + } + } + + .libraries-search { + border: 1px solid $color-gray-30; + margin: $size-4; + padding: $x-small; + display: flex; + align-items: center; + + &:focus-within { + border-color: $color-primary; + } + + & .search-input { + border: none; + color: $color-gray-60; + font-size: $fs12; + margin: 0; + padding: 0; + flex-grow: 1; + } + + & .search-icon { + display: flex; + align-items: center; + + svg { + fill: $color-gray-30; + height: 16px; + width: 16px; + } + + &.search-close { + transform: rotate(45deg); + cursor: pointer; + } + } + } + } +} + diff --git a/frontend/resources/styles/main/partials/sidebar-assets.scss b/frontend/resources/styles/main/partials/sidebar-assets.scss index 9cf046da02..d401ead10d 100644 --- a/frontend/resources/styles/main/partials/sidebar-assets.scss +++ b/frontend/resources/styles/main/partials/sidebar-assets.scss @@ -13,6 +13,30 @@ color: $color-gray-10; font-size: $fs14; margin: $small $small 0 $small; + display: flex; + align-items: center; + cursor: pointer; + + & .libraries-button { + margin-left: auto; + display: flex; + align-items: center; + + svg { + fill: $color-gray-30; + height: 20px; + width: 20px; + padding-right: $x-small; + } + } + + & .libraries-button:hover { + color: $color-primary; + + & svg { + fill: $color-primary; + } + } } .search-block { @@ -22,14 +46,14 @@ display: flex; align-items: center; - &:focus-within { - border-color: $color-primary !important; - } - &:hover { border-color: $color-gray-20; } + &:focus-within { + border-color: $color-primary; + } + & .search-input { background-color: $color-gray-50; border: none; @@ -50,8 +74,8 @@ svg { fill: $color-gray-30; - height: 15px; - width: 15px; + height: 16px; + width: 16px; } &.close { diff --git a/frontend/resources/styles/main/partials/sidebar.scss b/frontend/resources/styles/main/partials/sidebar.scss index 3bc43c60e7..1dfe95df03 100644 --- a/frontend/resources/styles/main/partials/sidebar.scss +++ b/frontend/resources/styles/main/partials/sidebar.scss @@ -99,6 +99,22 @@ $width-settings-bar: 15rem; margin-left: auto; } + span.tool-link { + margin-left: auto; + + svg { + fill: $color-gray-30; + height: 16px; + width: 16px; + } + + &:hover { + svg { + fill: $color-primary; + } + } + } + .tool-window-icon { margin-right: $small; display: none; diff --git a/frontend/src/uxbox/main/data/colors.cljs b/frontend/src/uxbox/main/data/colors.cljs index bccd286dc1..a4df5b8b7a 100644 --- a/frontend/src/uxbox/main/data/colors.cljs +++ b/frontend/src/uxbox/main/data/colors.cljs @@ -41,7 +41,7 @@ ptk/UpdateEvent (update [_ state] (-> state - (assoc-in [:workspace-colors-library (:id color)] color) + (update-in [:workspace-file :colors] #(conj % color)) (assoc-in [:workspace-local :color-for-rename] (:id color)))))) (def clear-color-for-rename @@ -67,7 +67,7 @@ ptk/UpdateEvent (update [_ state] (-> state - (assoc-in [:workspace-colors-library (:id color)] color))))) + (update-in [:workspace-file :colors] #(d/replace-by-id % color)))))) (declare update-color-result) @@ -86,7 +86,7 @@ ptk/UpdateEvent (update [_ state] (-> state - (assoc-in [:workspace-colors-library (:id color)] color))))) + (update-in [:workspace-file :colors] #(d/replace-by-id % color)))))) (declare delete-color-result) @@ -104,5 +104,6 @@ ptk/UpdateEvent (update [_ state] (-> state - (d/dissoc-in [:workspace-colors-library color-id]))))) + (update-in [:workspace-file :colors] + (fn [colors] (filter #(not= (:id %) color-id) colors))))))) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index ffb7c6cae3..aa3465036b 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -1433,8 +1433,9 @@ ;; Persistence (def set-file-shared dwp/set-file-shared) -(def fetch-media-library dwp/fetch-media-library) -(def fetch-colors-library dwp/fetch-colors-library) +(def fetch-shared-files dwp/fetch-shared-files) +(def link-file-to-library dwp/link-file-to-library) +(def unlink-file-from-library dwp/unlink-file-from-library) (def add-media-object-from-url dwp/add-media-object-from-url) (def upload-media-objects dwp/upload-media-objects) (def delete-media-object dwp/delete-media-object) diff --git a/frontend/src/uxbox/main/data/workspace/persistence.cljs b/frontend/src/uxbox/main/data/workspace/persistence.cljs index defc51b668..8e7b91dece 100644 --- a/frontend/src/uxbox/main/data/workspace/persistence.cljs +++ b/frontend/src/uxbox/main/data/workspace/persistence.cljs @@ -137,6 +137,7 @@ ::ordering ::data])) +(declare fetch-libraries-content) (declare bundle-fetched) (defn- fetch-bundle @@ -147,10 +148,18 @@ (->> (rx/zip (rp/query :file {:id file-id}) (rp/query :file-users {:id file-id}) (rp/query :project-by-id {:project-id project-id}) - (rp/query :pages {:file-id file-id})) + (rp/query :pages {:file-id file-id}) + (rp/query :media-objects {:file-id file-id :is-local false}) + (rp/query :colors {:file-id file-id}) + (rp/query :file-libraries {:file-id file-id})) (rx/first) - (rx/map (fn [[file users project pages]] - (bundle-fetched file users project pages))) + (rx/mapcat + (fn [bundle] + (->> (fetch-libraries-content (get bundle 6)) + (rx/map (fn [[lib-media-objects lib-colors]] + (conj bundle lib-media-objects lib-colors)))))) + (rx/map (fn [bundle] + (apply bundle-fetched bundle))) (rx/catch (fn [{:keys [type code] :as error}] (cond (= :not-found type) @@ -163,27 +172,78 @@ :else (throw error)))))))) +(defn- fetch-libraries-content + [libraries] + (if (empty? libraries) + (rx/of [{} {}]) + (rx/zip + (->> ;; fetch media-objects list of each library, and concatenate in a sequence + (apply rx/zip (for [library libraries] + (->> (rp/query :media-objects {:file-id (:id library) + :is-local false}) + (rx/map (fn [media-objects] + [(:id library) media-objects]))))) + + ;; reorganize the sequence as a map {library-id -> media-objects} + (rx/map (fn [media-list] + (reduce (fn [result, [library-id media-objects]] + (assoc result library-id media-objects)) + {} + media-list)))) + + (->> ;; fetch colorss list of each library, and concatenate in a vector + (apply rx/zip (for [library libraries] + (->> (rp/query :colors {:file-id (:id library)}) + (rx/map (fn [colors] + [(:id library) colors]))))) + + ;; reorganize the sequence as a map {library-id -> colors} + (rx/map (fn [colors-list] + (reduce (fn [result, [library-id colors]] + (assoc result library-id colors)) + {} + colors-list))))))) + (defn- bundle-fetched - [file users project pages] + [file users project pages media-objects colors libraries lib-media-objects lib-colors] (ptk/reify ::bundle-fetched IDeref (-deref [_] {:file file :users users :project project - :pages pages}) + :pages pages + :media-objects media-objects + :colors colors + :libraries libraries}) ptk/UpdateEvent (update [_ state] - (let [assoc-page #(assoc-in %1 [:workspace-pages (:id %2)] %2)] + (let [assoc-page + #(assoc-in %1 [:workspace-pages (:id %2)] %2) + + assoc-media-objects + #(assoc-in %1 [:workspace-libraries %2 :media-objects] + (get lib-media-objects %2)) + + assoc-colors + #(assoc-in %1 [:workspace-libraries %2 :colors] + (get lib-colors %2))] + (as-> state $$ (assoc $$ - :workspace-file file + :workspace-file (assoc file + :media-objects media-objects + :colors colors) :workspace-users (d/index-by :id users) :workspace-pages {} - :workspace-project project) + :workspace-project project + :workspace-libraries (d/index-by :id libraries)) + (reduce assoc-media-objects $$ (keys lib-media-objects)) + (reduce assoc-colors $$ (keys lib-colors)) (reduce assoc-page $$ pages)))))) + ;; --- Set File shared (defn set-file-shared @@ -200,6 +260,79 @@ (->> (rp/mutation :set-file-shared params) (rx/ignore)))))) + +;; --- Fetch Shared Files + +(declare shared-files-fetched) + +(defn fetch-shared-files + [] + (ptk/reify ::fetch-shared-files + ptk/WatchEvent + (watch [_ state stream] + (let [params {}] + (->> (rp/query :shared-files params) + (rx/map shared-files-fetched)))))) + +(defn shared-files-fetched + [files] + (us/verify (s/every ::file) files) + (ptk/reify ::shared-files-fetched + ptk/UpdateEvent + (update [_ state] + (let [state (dissoc state :files)] + (assoc state :workspace-shared-files files))))) + + +;; --- Link and unlink Files + +(declare file-linked) + +(defn link-file-to-library + [file-id library-id] + (ptk/reify ::link-file-to-library + ptk/WatchEvent + (watch [_ state stream] + (let [params {:file-id file-id + :library-id library-id}] + (->> (->> (rp/mutation :link-file-to-library params) + (rx/mapcat + #(rx/zip (rp/query :file-library {:file-id library-id}) + (rp/query :media-objects {:file-id library-id + :is-local false}) + (rp/query :colors {:file-id library-id})))) + (rx/map file-linked)))))) + +(defn file-linked + [[library media-objects colors]] + (ptk/reify ::file-linked + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-libraries (:id library)] + (assoc library + :media-objects media-objects + :colors colors))))) + +(declare file-unlinked) + +(defn unlink-file-from-library + [file-id library-id] + (ptk/reify ::unlink-file-from-library + ptk/WatchEvent + (watch [_ state stream] + (let [params {:file-id file-id + :library-id library-id}] + (->> (rp/mutation :unlink-file-from-library params) + (rx/map #(file-unlinked file-id library-id))))))) + +(defn file-unlinked + [file-id library-id] + (ptk/reify ::file-unlinked + ptk/UpdateEvent + (update [_ state] + (d/dissoc-in state [:workspace-libraries library-id])))) + + ;; --- Fetch Pages (declare page-fetched) @@ -298,46 +431,6 @@ (rx/of go-to-file) (rx/empty)))))))))) -;; --- Fetch Workspace Media library - -(declare media-library-fetched) - -(defn fetch-media-library - [file-id] - (ptk/reify ::fetch-media-library - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :media-objects {:file-id file-id :is-local false}) - (rx/map media-library-fetched))))) - -(defn media-library-fetched - [media-objects] - (ptk/reify ::media-library-fetched - ptk/UpdateEvent - (update [_ state] - (let [media-objects (d/index-by :id media-objects)] - (assoc state :workspace-media-library media-objects))))) - -;; --- Fetch Workspace Colors library - -(declare colors-library-fetched) - -(defn fetch-colors-library - [file-id] - (ptk/reify ::fetch-colors-library - ptk/WatchEvent - (watch [_ state stream] - (->> (rp/query :colors {:file-id file-id}) - (rx/map colors-library-fetched))))) - -(defn colors-library-fetched - [colors] - (ptk/reify ::colors-library-fetched - ptk/UpdateEvent - (update [_ state] - (let [colors (d/index-by :id colors)] - (assoc state :workspace-colors-library colors))))) - ;; --- Upload local media objects @@ -357,6 +450,8 @@ on-error #(do (di/notify-finished-loading) (di/process-error %)) + + is-library (not= file-id (:id (:workspace-file state))) prepare (fn [url] @@ -370,7 +465,7 @@ (rx/map prepare) (rx/mapcat #(rp/mutation! :add-media-object-from-url %)) (rx/do on-success) - (rx/map (partial upload-media-objects-result file-id is-local)) + (rx/map (partial upload-media-objects-result file-id is-local is-library)) (rx/catch on-error))))))) (defn upload-media-objects @@ -389,6 +484,8 @@ on-error #(do (di/notify-finished-loading) (di/process-error %)) + is-library (not= file-id (:id (:workspace-file state))) + prepare (fn [js-file] {:name (.-name js-file) @@ -403,32 +500,43 @@ (rx/map prepare) (rx/mapcat #(rp/mutation! :upload-media-object %)) (rx/do on-success) - (rx/map (partial upload-media-objects-result file-id is-local)) + (rx/map (partial upload-media-objects-result file-id is-local is-library)) (rx/catch on-error))))))) (defn upload-media-objects-result - [file-id is-local media-object] + [file-id is-local is-library media-object] (us/verify ::us/uuid file-id) (us/verify ::us/boolean is-local) (us/verify ::cm/media-object media-object) (ptk/reify ::upload-media-objects-result ptk/UpdateEvent (update [_ state] - (if is-local + (if is-local ;; the media-object is local to the file, not for its library state - (assoc-in state - [:workspace-media-library (:id media-object)] - media-object))))) + (if is-library ;; the file is not the currently editing one, but a linked shared file + (update-in state + [:workspace-libraries file-id :media-objects] + #(conj % media-object)) + (update-in state + [:workspace-file :media-objects] + #(conj % media-object))))))) ;; --- Delete media object (defn delete-media-object - [id] + [file-id id] (ptk/reify ::delete-media-object ptk/UpdateEvent (update [_ state] - (update state :workspace-media-library dissoc id)) + (let [is-library (not= file-id (:id (:workspace-file state)))] + (if is-library + (update-in state + [:workspace-libraries file-id :media-objects] + (fn [media-objects] (filter #(not= (:id %) id) media-objects))) + (update-in state + [:workspace-file :media-objects] + (fn [media-objects] (filter #(not= (:id %) id) media-objects)))))) ptk/WatchEvent (watch [_ state stream] diff --git a/frontend/src/uxbox/main/refs.cljs b/frontend/src/uxbox/main/refs.cljs index 75d501588e..916e887788 100644 --- a/frontend/src/uxbox/main/refs.cljs +++ b/frontend/src/uxbox/main/refs.cljs @@ -57,11 +57,11 @@ (def workspace-project (l/derived :workspace-project st/state)) -(def workspace-media-library - (l/derived :workspace-media-library st/state)) +(def workspace-shared-files + (l/derived :workspace-shared-files st/state)) -(def workspace-colors-library - (l/derived :workspace-colors-library st/state)) +(def workspace-libraries + (l/derived :workspace-libraries st/state)) (def workspace-users (l/derived :workspace-users st/state)) diff --git a/frontend/src/uxbox/main/ui/icons.cljs b/frontend/src/uxbox/main/ui/icons.cljs index 35a2857853..b99a5d7b67 100644 --- a/frontend/src/uxbox/main/ui/icons.cljs +++ b/frontend/src/uxbox/main/ui/icons.cljs @@ -56,6 +56,7 @@ (def layers (icon-xref :layers)) (def letter-spacing (icon-xref :letter-spacing)) (def library (icon-xref :library)) +(def libraries (icon-xref :libraries)) (def line (icon-xref :line)) (def line-height (icon-xref :line-height)) (def loader (icon-xref :loader)) diff --git a/frontend/src/uxbox/main/ui/workspace.cljs b/frontend/src/uxbox/main/ui/workspace.cljs index ca4938e516..5f78278d36 100644 --- a/frontend/src/uxbox/main/ui/workspace.cljs +++ b/frontend/src/uxbox/main/ui/workspace.cljs @@ -18,7 +18,6 @@ [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.streams :as ms] - [uxbox.main.ui.confirm] [uxbox.main.ui.keyboard :as kbd] [uxbox.main.ui.hooks :as hooks] [uxbox.main.ui.workspace.viewport :refer [viewport coordinates]] diff --git a/frontend/src/uxbox/main/ui/workspace/libraries.cljs b/frontend/src/uxbox/main/ui/workspace/libraries.cljs new file mode 100644 index 0000000000..9430f8a1eb --- /dev/null +++ b/frontend/src/uxbox/main/ui/workspace/libraries.cljs @@ -0,0 +1,155 @@ +;; 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) 2016 Andrey Antukh +;; Copyright (c) 2016 Juan de la Cruz + +(ns uxbox.main.ui.workspace.libraries + (:require + [rumext.alpha :as mf] + [cuerdas.core :as str] + [uxbox.util.dom :as dom] + [uxbox.util.i18n :refer (tr)] + [uxbox.util.data :refer [classnames matches-search]] + [uxbox.main.store :as st] + [uxbox.main.refs :as refs] + [uxbox.main.data.workspace :as dw] + [uxbox.main.ui.icons :as i] + [uxbox.main.ui.modal :as modal])) + +(mf/defc libraries-tab + [{:keys [file libraries shared-files] :as props}] + (let [state (mf/use-state {:search-term ""}) + + sorted-libraries (->> (vals libraries) + (sort-by #(str/lower (:name %)))) + + filtered-files (->> shared-files + (filter #(not= (:id %) (:id file))) + (filter #(nil? (get libraries (:id %)))) + (filter #(matches-search (:name %) (:search-term @state))) + (sort-by #(str/lower (:name %)))) + + 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 "")) + + link-library (fn [library-id] + (st/emit! (dw/link-file-to-library (:id file) library-id))) + + unlink-library (fn [library-id] + (st/emit! (dw/unlink-file-from-library (:id file) library-id))) + + contents-str (fn [library graphics-count colors-count] + (str + (str/join " · " + (cond-> [] + (< 0 graphics-count) + (conj (tr "workspace.libraries.graphics" graphics-count)) + + (< 0 colors-count) + (conj (tr "workspace.libraries.colors" colors-count)))) + "\u00A0"))] ;; Include a   so this block has always some content + [:* + [:div.section + [:div.section-title (tr "workspace.libraries.in-this-file")] + [:div.section-list + [:div.section-list-item + [:div.item-name (tr "workspace.libraries.file-library")] + [:div.item-contents (contents-str file + (count (:media-objects file)) + (count (:colors file)))]] + (for [library sorted-libraries] + [:div.section-list-item {:key (:id library)} + [:div.item-name (:name library)] + [:div.item-contents (contents-str library + (count (:media-objects library)) + (count (:colors library)))] + [:input.item-button {:type "button" + :value (tr "workspace.libraries.remove") + :on-click #(unlink-library (:id library))}]]) + ]] + [:div.section + [:div.section-title (tr "workspace.libraries.shared-libraries")] + [:div.libraries-search + [:input.search-input + {:placeholder (tr "workspace.libraries.search-shared-libraries") + :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.search-close + {:on-click on-search-clear-click} + i/close])] + (if (> (count filtered-files) 0) + [:div.section-list + (for [file filtered-files] + [:div.section-list-item {:key (:id file)} + [:div.item-name (:name file)] + [:div.item-contents (contents-str file + (:graphics-count file) + (:colors-count file))] + [:input.item-button {:type "button" + :value (tr "workspace.libraries.add") + :on-click #(link-library (:id file))}]])] + [:div.section-list-empty + i/library + (if (str/empty? (:search-term @state)) + (tr "workspace.libraries.no-shared-libraries-available") + (tr "workspace.libraries.no-matches-for" (:search-term @state)))])]])) + + +(mf/defc updates-tab + [] + [:div]) + + +(mf/defc libraries-dialog + [{:keys [] :as ctx}] + (let [state (mf/use-state {:current-tab :libraries}) + + current-tab (:current-tab @state) + + file (mf/deref refs/workspace-file) + libraries (mf/deref refs/workspace-libraries) + shared-files (mf/deref refs/workspace-shared-files) + + change-tab (fn [tab] + (swap! state assoc :current-tab tab)) + + close (fn [event] + (dom/prevent-default event) + (modal/hide!))] + + (mf/use-effect + #(st/emit! (dw/fetch-shared-files))) + + [:div.modal-overlay + [:div.modal.libraries-dialog + [:a.close {:on-click close} i/close] + [:div.modal-content + [:div.libraries-header + [:div.header-item + {:class (classnames :active (= current-tab :libraries)) + :on-click #(change-tab :libraries)} + (tr "workspace.libraries.libraries")] + [:div.header-item + {:class (classnames :active (= current-tab :updates)) + :on-click #(change-tab :updates)} + (tr "workspace.libraries.updates")]] + [:div.libraries-content + (case current-tab + :libraries + [:& libraries-tab {:file file + :libraries libraries + :shared-files shared-files}] + :updates + [:& updates-tab {}])]]]])) + diff --git a/frontend/src/uxbox/main/ui/workspace/sidebar/assets.cljs b/frontend/src/uxbox/main/ui/workspace/sidebar/assets.cljs index 43d86c2d47..d6ef1b23d2 100644 --- a/frontend/src/uxbox/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/uxbox/main/ui/workspace/sidebar/assets.cljs @@ -29,20 +29,14 @@ [uxbox.util.timers :as timers] [uxbox.common.uuid :as uuid] [uxbox.util.i18n :as i18n :refer [tr]] - [uxbox.util.data :refer [classnames]] + [uxbox.util.data :refer [classnames matches-search]] + [uxbox.util.router :as rt] [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)))) + [uxbox.main.ui.components.context-menu :refer [context-menu]] + [uxbox.main.ui.workspace.libraries :refer [libraries-dialog]])) (mf/defc modal-edit-color [{:keys [color-value on-accept on-cancel] :as ctx}] @@ -72,7 +66,7 @@ [:a.close {:href "#" :on-click cancel} i/close]]]))) (mf/defc graphics-box - [{:keys [file-id media-objects] :as props}] + [{:keys [file-id local-library? media-objects] :as props}] (let [state (mf/use-state {:menu-open false :top nil :left nil @@ -84,7 +78,7 @@ #(dom/click (mf/ref-val file-input)) delete-graphic - #(st/emit! (dw/delete-media-object (:object-id @state))) + #(st/emit! (dw/delete-media-object file-id (:object-id @state))) on-files-selected (fn [js-files] @@ -93,14 +87,15 @@ on-context-menu (fn [object-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 - :object-id object-id)))) + (when local-library? + (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 + :object-id object-id))))) on-drag-start (fn [uri] @@ -112,14 +107,15 @@ [:div.group-title (tr "workspace.assets.graphics") [:span (str "\u00A0(") (count media-objects) ")"] ;; Unicode 00A0 is non-breaking space - [:div.group-button {:on-click add-graphic} - i/plus - [:& file-uploader {:accept cm/str-media-types - :multi true - :input-ref file-input - :on-selected on-files-selected}]]] + (when local-library? + [:div.group-button {:on-click add-graphic} + i/plus + [:& file-uploader {:accept cm/str-media-types + :multi true + :input-ref file-input + :on-selected on-files-selected}]])] [:div.group-grid - (for [object (sort-by :name media-objects)] + (for [object media-objects] [:div.grid-cell {:key (:id object) :draggable true :on-context-menu (on-context-menu (:id object)) @@ -127,17 +123,18 @@ [:img {:src (:thumb-uri object) :draggable false}] ;; Also need to add css pointer-events: none [:div.cell-name (:name object)]]) - [:& 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]]}]]])) + (when local-library? + [:& 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 file-id] :as props}] + [{:keys [color file-id local-library?] :as props}] (let [workspace-local @refs/workspace-local color-for-rename (:color-for-rename workspace-local) @@ -189,14 +186,15 @@ 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)))] + (when local-library? + (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)) @@ -220,18 +218,19 @@ (: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]]}]])) + (when local-library? + [:& 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 [file-id colors] :as props}] + [{:keys [file-id local-library? colors] :as props}] (let [add-color (fn [value opacity] (st/emit! (dcol/create-color file-id value))) @@ -246,15 +245,18 @@ [: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]] + (when local-library? + [:div.group-button {:on-click add-color-clicked} i/plus])] [:div.group-list - (for [color (sort-by :name colors)] + (for [color colors] [:& color-item {:key (:id color) :color color - :file-id file-id}])]])) + :file-id file-id + :local-library? local-library?}])]])) (mf/defc file-library-toolbox - [{:keys [file-id + [{:keys [library + local-library? shared? media-objects colors @@ -262,26 +264,41 @@ search-term box-filter] :as props}] (let [open? (mf/use-state initial-open?) - toggle-open #(swap! open? not)] + toggle-open #(swap! open? not) + router (mf/deref refs/router) + library-url (rt/resolve router :workspace + {:project-id (:project-id library) + :file-id (:id library)} + {:page-id (first (:pages library))})] [: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 shared? - [:span.tool-badge (tr "workspace.assets.shared")])] + (if local-library? + [:* + [:span (tr "workspace.assets.file-library")] + (when shared? + [:span.tool-badge (tr "workspace.assets.shared")])] + [:* + [:span (:name library)] + [:span.tool-link + [:a {:href (str "#" library-url) :target "_blank"} i/chain]]])] (when @open? (let [show-graphics (and (or (= box-filter :all) (= box-filter :graphics)) - (or (> (count media-objects) 0) (str/empty? search-term))) - show-colors (and (or (= box-filter :all) (= box-filter :colors)) - (or (> (count colors) 0) (str/empty? search-term)))] + (or (> (count media-objects) 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 {:file-id file-id :media-objects media-objects}]) + [:& graphics-box {:file-id (:id library) + :local-library? local-library? + :media-objects media-objects}]) (when show-colors - [:& colors-box {:file-id file-id :colors colors}]) + [:& colors-box {:file-id (:id library) + :local-library? local-library? + :colors colors}]) (when (and (not show-graphics) (not show-colors)) [:div.asset-group [:div.group-title (tr "workspace.assets.not-found")]])]))])) @@ -290,19 +307,27 @@ [] (let [team-id (-> refs/workspace-project mf/deref :team-id) file (mf/deref refs/workspace-file) - file-id (:id file) - file-media (mf/deref refs/workspace-media-library) - file-colors (mf/deref refs/workspace-colors-library) + libraries (mf/deref refs/workspace-libraries) + sorted-libraries (->> (vals libraries) + (sort-by #(str/lower (:name %)))) state (mf/use-state {:search-term "" :box-filter :all}) - filtered-media-objects (filter #(matches-search (:name %) (:search-term @state)) - (vals file-media)) + filtered-media-objects (fn [library-id] + (as-> libraries $$ + (assoc $$ (:id file) file) + (get-in $$ [library-id :media-objects]) + (filter #(matches-search (:name %) (:search-term @state)) $$) + (sort-by #(str/lower (:name %)) $$))) - filtered-colors (filter #(or (matches-search (:name %) (:search-term @state)) - (matches-search (:content %) (:search-term @state))) - (vals file-colors)) + filtered-colors (fn [library-id] + (as-> libraries $$ + (assoc $$ (:id file) file) + (get-in $$ [library-id :colors]) + (filter #(or (matches-search (:name %) (:search-term @state)) + (matches-search (:content %) (:search-term @state))) $$) + (sort-by #(str/lower (:name %)) $$))) on-search-term-change (fn [event] (let [value (-> (dom/get-target event) @@ -318,17 +343,15 @@ (d/read-string))] (swap! state assoc :box-filter value)))] - (mf/use-effect - (mf/deps file-id) - #(when file-id - (st/emit! (dw/fetch-media-library file-id)) - (st/emit! (dw/fetch-colors-library file-id)))) - [:div.assets-bar [:div.tool-window [:div.tool-window-content - [:div.assets-bar-title (tr "workspace.assets.assets")] + [:div.assets-bar-title + (tr "workspace.assets.assets") + [:div.libraries-button {:on-click #(modal/show! libraries-dialog {})} + i/libraries + (tr "workspace.assets.libraries")]] [:div.search-block [:input.search-input @@ -347,14 +370,25 @@ :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")]] - ]] + [:option {:value ":colors"} (tr "workspace.assets.box-filter-colors")]]]] - [:& file-library-toolbox {:file-id file-id + [:& file-library-toolbox {:key (:id file) + :library file + :local-library? true :shared? (:is-shared file) - :media-objects filtered-media-objects - :colors filtered-colors + :media-objects (filtered-media-objects (:id file)) + :colors (filtered-colors (:id file)) :initial-open? true :search-term (:search-term @state) - :box-filter (:box-filter @state)}]])) + :box-filter (:box-filter @state)}] + (for [library sorted-libraries] + [:& file-library-toolbox {:key (:id library) + :library library + :local-library? false + :shared? (:is-shared library) + :media-objects (filtered-media-objects (:id library)) + :colors (filtered-colors (:id library)) + :initial-open? false + :search-term (:search-term @state) + :box-filter (:box-filter @state)}])])) diff --git a/frontend/src/uxbox/util/data.cljs b/frontend/src/uxbox/util/data.cljs index 61145306d8..7068c6d96e 100644 --- a/frontend/src/uxbox/util/data.cljs +++ b/frontend/src/uxbox/util/data.cljs @@ -180,6 +180,13 @@ "className" (str/camel (name key)))))) +(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)))) ;; (defn coalesce ;; [^number v ^number n]