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]