mirror of
https://github.com/penpot/penpot.git
synced 2025-06-29 00:06:59 +02:00
🎉 Add dashboard custom fonts management.
This commit is contained in:
parent
2582e87ffa
commit
e15a212b14
42 changed files with 1329 additions and 208 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -26,6 +26,7 @@ node_modules
|
||||||
/frontend/out/
|
/frontend/out/
|
||||||
/frontend/.shadow-cljs
|
/frontend/.shadow-cljs
|
||||||
/frontend/resources/public/*
|
/frontend/resources/public/*
|
||||||
|
/frontend/resources/fonts/experiments
|
||||||
/exporter/target
|
/exporter/target
|
||||||
/exporter/.shadow-cljs
|
/exporter/.shadow-cljs
|
||||||
/docker/images/bundle*
|
/docker/images/bundle*
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
org.postgresql/postgresql {:mvn/version "42.2.19"}
|
org.postgresql/postgresql {:mvn/version "42.2.19"}
|
||||||
com.zaxxer/HikariCP {:mvn/version "4.0.3"}
|
com.zaxxer/HikariCP {:mvn/version "4.0.3"}
|
||||||
|
|
||||||
funcool/datoteka {:mvn/version "1.2.0"}
|
funcool/datoteka {:mvn/version "2.0.0"}
|
||||||
funcool/promesa {:mvn/version "6.0.0"}
|
funcool/promesa {:mvn/version "6.0.0"}
|
||||||
funcool/cuerdas {:mvn/version "2020.03.26-3"}
|
funcool/cuerdas {:mvn/version "2020.03.26-3"}
|
||||||
|
|
||||||
|
|
|
@ -122,10 +122,15 @@
|
||||||
:app.rlimits/image
|
:app.rlimits/image
|
||||||
(cf/get :rlimits-image)
|
(cf/get :rlimits-image)
|
||||||
|
|
||||||
|
;; RLimit definition for font processing
|
||||||
|
:app.rlimits/font
|
||||||
|
(cf/get :rlimits-font 2)
|
||||||
|
|
||||||
;; A collection of rlimits as hash-map.
|
;; A collection of rlimits as hash-map.
|
||||||
:app.rlimits/all
|
:app.rlimits/all
|
||||||
{:password (ig/ref :app.rlimits/password)
|
{:password (ig/ref :app.rlimits/password)
|
||||||
:image (ig/ref :app.rlimits/image)}
|
:image (ig/ref :app.rlimits/image)
|
||||||
|
:font (ig/ref :app.rlimits/font)}
|
||||||
|
|
||||||
:app.rpc/rpc
|
:app.rpc/rpc
|
||||||
{:pool (ig/ref :app.db/pool)
|
{:pool (ig/ref :app.db/pool)
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
;; Copyright (c) UXBOX Labs SL
|
;; Copyright (c) UXBOX Labs SL
|
||||||
|
|
||||||
(ns app.media
|
(ns app.media
|
||||||
"Media postprocessing."
|
"Media & Font postprocessing."
|
||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.exceptions :as ex]
|
[app.common.exceptions :as ex]
|
||||||
|
@ -13,20 +13,29 @@
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.rlimits :as rlm]
|
[app.rlimits :as rlm]
|
||||||
[app.rpc.queries.svg :as svg]
|
[app.rpc.queries.svg :as svg]
|
||||||
|
[clojure.java.io :as io]
|
||||||
|
[clojure.java.shell :as sh]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[datoteka.core :as fs])
|
[datoteka.core :as fs])
|
||||||
(:import
|
(:import
|
||||||
java.io.ByteArrayInputStream
|
java.io.ByteArrayInputStream
|
||||||
|
java.io.OutputStream
|
||||||
|
org.apache.commons.io.IOUtils
|
||||||
org.im4java.core.ConvertCmd
|
org.im4java.core.ConvertCmd
|
||||||
org.im4java.core.IMOperation
|
org.im4java.core.IMOperation
|
||||||
org.im4java.core.Info))
|
org.im4java.core.Info))
|
||||||
|
|
||||||
;; --- Generic specs
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; --- Utility functions
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(s/def ::image-content-type cm/valid-image-types)
|
||||||
|
(s/def ::font-content-type cm/valid-font-types)
|
||||||
|
|
||||||
(s/def :internal.http.upload/filename ::us/string)
|
(s/def :internal.http.upload/filename ::us/string)
|
||||||
(s/def :internal.http.upload/size ::us/integer)
|
(s/def :internal.http.upload/size ::us/integer)
|
||||||
(s/def :internal.http.upload/content-type cm/valid-media-types)
|
(s/def :internal.http.upload/content-type ::us/string)
|
||||||
(s/def :internal.http.upload/tempfile any?)
|
(s/def :internal.http.upload/tempfile any?)
|
||||||
|
|
||||||
(s/def ::upload
|
(s/def ::upload
|
||||||
|
@ -35,8 +44,44 @@
|
||||||
:internal.http.upload/tempfile
|
:internal.http.upload/tempfile
|
||||||
:internal.http.upload/content-type]))
|
:internal.http.upload/content-type]))
|
||||||
|
|
||||||
|
(defn validate-media-type
|
||||||
|
([mtype] (validate-media-type mtype cm/valid-image-types))
|
||||||
|
([mtype allowed]
|
||||||
|
(when-not (contains? allowed mtype)
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :media-type-not-allowed
|
||||||
|
:hint "Seems like you are uploading an invalid media object"))))
|
||||||
|
|
||||||
|
|
||||||
|
(defmulti process :cmd)
|
||||||
|
(defmulti process-error class)
|
||||||
|
|
||||||
|
(defmethod process :default
|
||||||
|
[{:keys [cmd] :as params}]
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :not-implemented
|
||||||
|
:hint (str/fmt "No impl found for process cmd: %s" cmd)))
|
||||||
|
|
||||||
|
(defmethod process-error :default
|
||||||
|
[error]
|
||||||
|
(ex/raise :type :internal :cause error))
|
||||||
|
|
||||||
|
(defn run
|
||||||
|
[{:keys [rlimits] :as cfg} {:keys [rlimit] :or {rlimit :image} :as params}]
|
||||||
|
(us/assert map? rlimits)
|
||||||
|
(let [rlimit (get rlimits rlimit)]
|
||||||
|
(when-not rlimit
|
||||||
|
(ex/raise :type :internal
|
||||||
|
:code :rlimit-not-configured
|
||||||
|
:hint ":image rlimit not configured"))
|
||||||
|
(try
|
||||||
|
(rlm/execute rlimit (process params))
|
||||||
|
(catch Throwable e
|
||||||
|
(process-error e)))))
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; --- Thumbnails Generation
|
;; --- Thumbnails Generation
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(s/def ::cmd keyword?)
|
(s/def ::cmd keyword?)
|
||||||
|
|
||||||
|
@ -77,8 +122,6 @@
|
||||||
:size (alength ^bytes thumbnail-data)
|
:size (alength ^bytes thumbnail-data)
|
||||||
:data (ByteArrayInputStream. thumbnail-data)))))
|
:data (ByteArrayInputStream. thumbnail-data)))))
|
||||||
|
|
||||||
(defmulti process :cmd)
|
|
||||||
|
|
||||||
(defmethod process :generic-thumbnail
|
(defmethod process :generic-thumbnail
|
||||||
[{:keys [quality width height] :as params}]
|
[{:keys [quality width height] :as params}]
|
||||||
(us/assert ::thumbnail-params params)
|
(us/assert ::thumbnail-params params)
|
||||||
|
@ -161,33 +204,63 @@
|
||||||
:height (.getPageHeight instance)
|
:height (.getPageHeight instance)
|
||||||
:mtype mtype}))))
|
:mtype mtype}))))
|
||||||
|
|
||||||
(defmethod process :default
|
(defmethod process-error org.im4java.core.InfoException
|
||||||
[{:keys [cmd] :as params}]
|
[error]
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :not-implemented
|
|
||||||
:hint (str "No impl found for process cmd:" cmd)))
|
|
||||||
|
|
||||||
(defn run
|
|
||||||
[{:keys [rlimits]} params]
|
|
||||||
(us/assert map? rlimits)
|
|
||||||
(let [rlimit (get rlimits :image)]
|
|
||||||
(when-not rlimit
|
|
||||||
(ex/raise :type :internal
|
|
||||||
:code :rlimit-not-configured
|
|
||||||
:hint ":image rlimit not configured"))
|
|
||||||
(try
|
|
||||||
(rlm/execute rlimit (process params))
|
|
||||||
(catch org.im4java.core.InfoException e
|
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :invalid-image
|
:code :invalid-image
|
||||||
:cause e)))))
|
:cause error))
|
||||||
|
|
||||||
;; --- Utility functions
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; --- Fonts Generation
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn validate-media-type
|
(def all-fotmats #{"font/woff2", "font/woff", "font/otf", "font/ttf"})
|
||||||
([mtype] (validate-media-type mtype cm/valid-media-types))
|
|
||||||
([mtype allowed]
|
(defmethod process :generate-fonts
|
||||||
(when-not (contains? allowed mtype)
|
[{:keys [input] :as params}]
|
||||||
(ex/raise :type :validation
|
(letfn [(ttf->otf [data]
|
||||||
:code :media-type-not-allowed
|
(let [input-file (fs/create-tempfile :prefix "penpot")
|
||||||
:hint "Seems like you are uploading an invalid media object"))))
|
output-file (fs/path (str input-file ".otf"))
|
||||||
|
_ (with-open [out (io/output-stream input-file)]
|
||||||
|
(IOUtils/writeChunked ^bytes data ^OutputStream out)
|
||||||
|
(.flush ^OutputStream out))
|
||||||
|
res (sh/sh "fontforge" "-lang=ff" "-c"
|
||||||
|
(str/fmt "Open('%s'); Generate('%s')"
|
||||||
|
(str input-file)
|
||||||
|
(str output-file)))]
|
||||||
|
(when (zero? (:exit res))
|
||||||
|
(fs/slurp-bytes output-file))))
|
||||||
|
|
||||||
|
|
||||||
|
(ttf-or-otf->woff [data]
|
||||||
|
(let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
|
||||||
|
output-file (fs/path (str input-file ".woff"))
|
||||||
|
_ (with-open [out (io/output-stream input-file)]
|
||||||
|
(IOUtils/writeChunked ^bytes data ^OutputStream out)
|
||||||
|
(.flush ^OutputStream out))
|
||||||
|
res (sh/sh "sfnt2woff" (str input-file))]
|
||||||
|
(when (zero? (:exit res))
|
||||||
|
(fs/slurp-bytes output-file))))
|
||||||
|
|
||||||
|
(ttf-or-otf->woff2 [data]
|
||||||
|
(let [input-file (fs/create-tempfile :prefix "penpot" :suffix "")
|
||||||
|
output-file (fs/path (str input-file ".woff2"))
|
||||||
|
_ (with-open [out (io/output-stream input-file)]
|
||||||
|
(IOUtils/writeChunked ^bytes data ^OutputStream out)
|
||||||
|
(.flush ^OutputStream out))
|
||||||
|
res (sh/sh "woff2_compress" (str input-file))]
|
||||||
|
(when (zero? (:exit res))
|
||||||
|
(fs/slurp-bytes output-file))))]
|
||||||
|
|
||||||
|
(let [current (into #{} (keys input))]
|
||||||
|
(if (contains? current "font/ttf")
|
||||||
|
(-> input
|
||||||
|
(assoc "font/otf" (ttf->otf (get input "font/ttf")))
|
||||||
|
(assoc "font/woff" (ttf-or-otf->woff (get input "font/ttf")))
|
||||||
|
(assoc "font/woff2" (ttf-or-otf->woff2 (get input "font/ttf"))))
|
||||||
|
|
||||||
|
(-> input
|
||||||
|
;; TODO: pending to implement
|
||||||
|
;; (assoc "font/ttf" (otf->ttf (get input "font/ttf")))
|
||||||
|
(assoc "font/woff" (ttf-or-otf->woff (get input "font/otf")))
|
||||||
|
(assoc "font/woff2" (ttf-or-otf->woff2 (get input "font/otf"))))))))
|
||||||
|
|
|
@ -166,6 +166,9 @@
|
||||||
|
|
||||||
{:name "0052-del-legacy-user-and-team"
|
{:name "0052-del-legacy-user-and-team"
|
||||||
:fn (mg/resource "app/migrations/sql/0052-del-legacy-user-and-team.sql")}
|
:fn (mg/resource "app/migrations/sql/0052-del-legacy-user-and-team.sql")}
|
||||||
|
|
||||||
|
{:name "0053-add-team-font-variant-table"
|
||||||
|
:fn (mg/resource "app/migrations/sql/0053-add-team-font-variant-table.sql")}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
CREATE TABLE team_font_variant (
|
||||||
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
|
||||||
|
team_id uuid NOT NULL REFERENCES team(id) ON DELETE CASCADE,
|
||||||
|
profile_id uuid NULL REFERENCES profile(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
modified_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
deleted_at timestamptz NULL DEFAULT NULL,
|
||||||
|
|
||||||
|
font_id text NOT NULL,
|
||||||
|
font_family text NOT NULL,
|
||||||
|
font_weight smallint NOT NULL,
|
||||||
|
font_style text NOT NULL,
|
||||||
|
|
||||||
|
otf_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL,
|
||||||
|
ttf_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL,
|
||||||
|
woff1_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL,
|
||||||
|
woff2_file_id uuid NULL REFERENCES storage_object(id) ON DELETE SET NULL
|
||||||
|
);
|
|
@ -18,6 +18,7 @@
|
||||||
|
|
||||||
(derive ::password ::instance)
|
(derive ::password ::instance)
|
||||||
(derive ::image ::instance)
|
(derive ::image ::instance)
|
||||||
|
(derive ::font ::instance)
|
||||||
|
|
||||||
(defmethod ig/pre-init-spec ::instance [_]
|
(defmethod ig/pre-init-spec ::instance [_]
|
||||||
(s/spec int?))
|
(s/spec int?))
|
||||||
|
|
|
@ -120,6 +120,7 @@
|
||||||
'app.rpc.queries.profile
|
'app.rpc.queries.profile
|
||||||
'app.rpc.queries.recent-files
|
'app.rpc.queries.recent-files
|
||||||
'app.rpc.queries.viewer
|
'app.rpc.queries.viewer
|
||||||
|
'app.rpc.queries.fonts
|
||||||
'app.rpc.queries.svg)
|
'app.rpc.queries.svg)
|
||||||
(map (partial process-method cfg))
|
(map (partial process-method cfg))
|
||||||
(into {}))))
|
(into {}))))
|
||||||
|
@ -143,6 +144,7 @@
|
||||||
'app.rpc.mutations.teams
|
'app.rpc.mutations.teams
|
||||||
'app.rpc.mutations.management
|
'app.rpc.mutations.management
|
||||||
'app.rpc.mutations.ldap
|
'app.rpc.mutations.ldap
|
||||||
|
'app.rpc.mutations.fonts
|
||||||
'app.rpc.mutations.verify-token)
|
'app.rpc.mutations.verify-token)
|
||||||
(map (partial process-method cfg))
|
(map (partial process-method cfg))
|
||||||
(into {}))))
|
(into {}))))
|
||||||
|
|
116
backend/src/app/rpc/mutations/fonts.clj
Normal file
116
backend/src/app/rpc/mutations/fonts.clj
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
;; 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) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.rpc.mutations.fonts
|
||||||
|
(:require
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.media :as media]
|
||||||
|
[app.rpc.queries.teams :as teams]
|
||||||
|
[app.storage :as sto]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[app.worker :as wrk]
|
||||||
|
[clojure.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
|
(declare create-font-variant)
|
||||||
|
|
||||||
|
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
|
||||||
|
(def valid-style #{"normal" "italic"})
|
||||||
|
|
||||||
|
(s/def ::profile-id ::us/uuid)
|
||||||
|
(s/def ::team-id ::us/uuid)
|
||||||
|
(s/def ::name ::us/not-empty-string)
|
||||||
|
(s/def ::weight valid-weight)
|
||||||
|
(s/def ::style valid-style)
|
||||||
|
(s/def ::font-id (s/and ::us/string #(str/starts-with? % "custom-")))
|
||||||
|
(s/def ::content-type ::media/font-content-type)
|
||||||
|
(s/def ::data (s/map-of ::us/string any?))
|
||||||
|
|
||||||
|
(s/def ::create-font-variant
|
||||||
|
(s/keys :req-un [::profile-id ::team-id ::data
|
||||||
|
::font-id ::font-family ::font-weight ::font-style]))
|
||||||
|
|
||||||
|
(sv/defmethod ::create-font-variant
|
||||||
|
[{:keys [pool] :as cfg} {:keys [team-id profile-id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(let [cfg (assoc cfg :conn conn)]
|
||||||
|
(teams/check-edition-permissions! conn profile-id team-id)
|
||||||
|
(create-font-variant cfg params))))
|
||||||
|
|
||||||
|
(defn create-font-variant
|
||||||
|
[{:keys [conn storage] :as cfg} {:keys [data] :as params}]
|
||||||
|
(let [data (media/run cfg {:cmd :generate-fonts :input data :rlimit :font})
|
||||||
|
storage (assoc storage :conn conn)
|
||||||
|
otf (when-let [fdata (get data "font/otf")]
|
||||||
|
(sto/put-object storage {:content (sto/content fdata)
|
||||||
|
:content-type "font/otf"}))
|
||||||
|
|
||||||
|
ttf (when-let [fdata (get data "font/ttf")]
|
||||||
|
(sto/put-object storage {:content (sto/content fdata)
|
||||||
|
:content-type "font/ttf"}))
|
||||||
|
|
||||||
|
woff1 (when-let [fdata (get data "font/woff")]
|
||||||
|
(sto/put-object storage {:content (sto/content fdata)
|
||||||
|
:content-type "font/woff"}))
|
||||||
|
|
||||||
|
woff2 (when-let [fdata (get data "font/woff2")]
|
||||||
|
(sto/put-object storage {:content (sto/content fdata)
|
||||||
|
:content-type "font/woff2"}))]
|
||||||
|
|
||||||
|
(db/insert! conn :team-font-variant
|
||||||
|
{:id (uuid/next)
|
||||||
|
:team-id (:team-id params)
|
||||||
|
:font-id (:font-id params)
|
||||||
|
:font-family (:font-family params)
|
||||||
|
:font-weight (:font-weight params)
|
||||||
|
:font-style (:font-style params)
|
||||||
|
:woff1-file-id (:id woff1)
|
||||||
|
:woff2-file-id (:id woff2)
|
||||||
|
:otf-file-id (:id otf)
|
||||||
|
:ttf-file-id (:id ttf)})))
|
||||||
|
|
||||||
|
;; --- UPDATE FONT VARIANT
|
||||||
|
|
||||||
|
(s/def ::update-font-variant
|
||||||
|
(s/keys :req-un [::profile-id ::team-id ::id ::font-family ::font-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::update-font-variant
|
||||||
|
[{:keys [pool] :as cfg} {:keys [id team-id profile-id font-family font-id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(teams/check-edition-permissions! conn profile-id team-id)
|
||||||
|
(db/update! conn :team-font-variant
|
||||||
|
{:font-family font-family
|
||||||
|
:font-id font-id}
|
||||||
|
{:id id
|
||||||
|
:team-id team-id})
|
||||||
|
nil))
|
||||||
|
|
||||||
|
;; --- DELETE FONT VARIANT
|
||||||
|
|
||||||
|
(s/def ::delete-font-variant
|
||||||
|
(s/keys :req-un [::profile-id ::team-id ::id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::delete-font-variant
|
||||||
|
[{:keys [pool] :as cfg} {:keys [id team-id profile-id font-family font-id] :as params}]
|
||||||
|
(db/with-atomic [conn pool]
|
||||||
|
(teams/check-edition-permissions! conn profile-id team-id)
|
||||||
|
|
||||||
|
;; Schedule object deletion
|
||||||
|
(wrk/submit! {::wrk/task :delete-object
|
||||||
|
::wrk/delay cf/deletion-delay
|
||||||
|
::wrk/conn conn
|
||||||
|
:id id
|
||||||
|
:type :team-font-variant})
|
||||||
|
|
||||||
|
(db/update! conn :team-font-variant
|
||||||
|
{:deleted-at (dt/now)}
|
||||||
|
{:id id
|
||||||
|
:team-id team-id})
|
||||||
|
nil))
|
|
@ -32,12 +32,15 @@
|
||||||
(s/def ::file-id ::us/uuid)
|
(s/def ::file-id ::us/uuid)
|
||||||
(s/def ::team-id ::us/uuid)
|
(s/def ::team-id ::us/uuid)
|
||||||
|
|
||||||
|
|
||||||
;; --- Create File Media object (upload)
|
;; --- Create File Media object (upload)
|
||||||
|
|
||||||
(declare create-file-media-object)
|
(declare create-file-media-object)
|
||||||
(declare select-file)
|
(declare select-file)
|
||||||
|
|
||||||
(s/def ::content ::media/upload)
|
(s/def ::content-type ::media/image-content-type)
|
||||||
|
(s/def ::content (s/and ::media/upload (s/keys :req-un [::content-type])))
|
||||||
|
|
||||||
(s/def ::is-local ::us/boolean)
|
(s/def ::is-local ::us/boolean)
|
||||||
|
|
||||||
(s/def ::upload-file-media-object
|
(s/def ::upload-file-media-object
|
||||||
|
|
|
@ -401,7 +401,9 @@
|
||||||
|
|
||||||
(declare update-profile-photo)
|
(declare update-profile-photo)
|
||||||
|
|
||||||
(s/def ::file ::media/upload)
|
(s/def ::content-type ::media/image-content-type)
|
||||||
|
(s/def ::file (s/and ::media/upload (s/keys :req-un [::content-type])))
|
||||||
|
|
||||||
(s/def ::update-profile-photo
|
(s/def ::update-profile-photo
|
||||||
(s/keys :req-un [::profile-id ::file]))
|
(s/keys :req-un [::profile-id ::file]))
|
||||||
|
|
||||||
|
|
|
@ -249,7 +249,9 @@
|
||||||
|
|
||||||
(declare upload-photo)
|
(declare upload-photo)
|
||||||
|
|
||||||
(s/def ::file ::media/upload)
|
(s/def ::content-type ::media/image-content-type)
|
||||||
|
(s/def ::file (s/and ::media/upload (s/keys :req-un [::content-type])))
|
||||||
|
|
||||||
(s/def ::update-team-photo
|
(s/def ::update-team-photo
|
||||||
(s/keys :req-un [::profile-id ::team-id ::file]))
|
(s/keys :req-un [::profile-id ::team-id ::file]))
|
||||||
|
|
||||||
|
|
29
backend/src/app/rpc/queries/fonts.clj
Normal file
29
backend/src/app/rpc/queries/fonts.clj
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
;; 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) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.rpc.queries.fonts
|
||||||
|
(:require
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.rpc.queries.teams :as teams]
|
||||||
|
[app.util.services :as sv]
|
||||||
|
[clojure.spec.alpha :as s]))
|
||||||
|
|
||||||
|
;; --- Query: Team Font Variants
|
||||||
|
|
||||||
|
(s/def ::team-id ::us/uuid)
|
||||||
|
(s/def ::profile-id ::us/uuid)
|
||||||
|
(s/def ::team-font-variants
|
||||||
|
(s/keys :req-un [::profile-id ::team-id]))
|
||||||
|
|
||||||
|
(sv/defmethod ::team-font-variants
|
||||||
|
[{:keys [pool] :as cfg} {:keys [profile-id team-id] :as params}]
|
||||||
|
(with-open [conn (db/open pool)]
|
||||||
|
(teams/check-read-permissions! conn profile-id team-id)
|
||||||
|
(db/query conn :team-font-variant
|
||||||
|
{:team-id team-id
|
||||||
|
:deleted-at nil})))
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.spec :as us]
|
[app.common.spec :as us]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
|
[app.storage :as sto]
|
||||||
[app.util.logging :as l]
|
[app.util.logging :as l]
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
@ -24,7 +25,8 @@
|
||||||
(fn [{:keys [props] :as task}]
|
(fn [{:keys [props] :as task}]
|
||||||
(us/verify ::props props)
|
(us/verify ::props props)
|
||||||
(db/with-atomic [conn pool]
|
(db/with-atomic [conn pool]
|
||||||
(handle-deletion conn props))))
|
(let [cfg (assoc cfg :conn conn)]
|
||||||
|
(handle-deletion cfg props)))))
|
||||||
|
|
||||||
(s/def ::type ::us/keyword)
|
(s/def ::type ::us/keyword)
|
||||||
(s/def ::id ::us/uuid)
|
(s/def ::id ::us/uuid)
|
||||||
|
@ -34,21 +36,32 @@
|
||||||
(fn [_ props] (:type props)))
|
(fn [_ props] (:type props)))
|
||||||
|
|
||||||
(defmethod handle-deletion :default
|
(defmethod handle-deletion :default
|
||||||
[_conn {:keys [type]}]
|
[_cfg {:keys [type]}]
|
||||||
(l/warn :hint "no handler found"
|
(l/warn :hint "no handler found"
|
||||||
:type (d/name type)))
|
:type (d/name type)))
|
||||||
|
|
||||||
(defmethod handle-deletion :file
|
(defmethod handle-deletion :file
|
||||||
[conn {:keys [id] :as props}]
|
[{:keys [conn]} {:keys [id] :as props}]
|
||||||
(let [sql "delete from file where id=? and deleted_at is not null"]
|
(let [sql "delete from file where id=? and deleted_at is not null"]
|
||||||
(db/exec-one! conn [sql id])))
|
(db/exec-one! conn [sql id])))
|
||||||
|
|
||||||
(defmethod handle-deletion :project
|
(defmethod handle-deletion :project
|
||||||
[conn {:keys [id] :as props}]
|
[{:keys [conn]} {:keys [id] :as props}]
|
||||||
(let [sql "delete from project where id=? and deleted_at is not null"]
|
(let [sql "delete from project where id=? and deleted_at is not null"]
|
||||||
(db/exec-one! conn [sql id])))
|
(db/exec-one! conn [sql id])))
|
||||||
|
|
||||||
(defmethod handle-deletion :team
|
(defmethod handle-deletion :team
|
||||||
[conn {:keys [id] :as props}]
|
[{:keys [conn]} {:keys [id] :as props}]
|
||||||
(let [sql "delete from team where id=? and deleted_at is not null"]
|
(let [sql "delete from team where id=? and deleted_at is not null"]
|
||||||
(db/exec-one! conn [sql id])))
|
(db/exec-one! conn [sql id])))
|
||||||
|
|
||||||
|
(defmethod handle-deletion :team-font-variant
|
||||||
|
[{:keys [conn storage]} {:keys [id] :as props}]
|
||||||
|
(let [font (db/get-by-id conn :team-font-variant id {:uncheked true})
|
||||||
|
storage (assoc storage :conn conn)]
|
||||||
|
(when (:deleted-at font)
|
||||||
|
(db/delete! conn :team-font-variant {:id id})
|
||||||
|
(some->> (:woff1-file-id font) (sto/del-object storage))
|
||||||
|
(some->> (:woff2-file-id font) (sto/del-object storage))
|
||||||
|
(some->> (:otf-file-id font) (sto/del-object storage))
|
||||||
|
(some->> (:ttf-file-id font) (sto/del-object storage)))))
|
||||||
|
|
|
@ -101,7 +101,10 @@
|
||||||
:media-id (:media-id mobj)
|
:media-id (:media-id mobj)
|
||||||
:thumbnail-id (:thumbnail-id mobj))
|
:thumbnail-id (:thumbnail-id mobj))
|
||||||
;; NOTE: deleting the file-media-object in the database
|
;; NOTE: deleting the file-media-object in the database
|
||||||
;; automatically marks as toched the referenced storage objects.
|
;; automatically marks as toched the referenced storage
|
||||||
|
;; objects. The touch mechanism is needed because many files can
|
||||||
|
;; point to the same storage objects and we can't just delete
|
||||||
|
;; them.
|
||||||
(db/delete! conn :file-media-object {:id (:id mobj)}))
|
(db/delete! conn :file-media-object {:id (:id mobj)}))
|
||||||
|
|
||||||
nil))
|
nil))
|
||||||
|
|
|
@ -261,6 +261,19 @@
|
||||||
(recur (reduce-kv assoc! res (first maps))
|
(recur (reduce-kv assoc! res (first maps))
|
||||||
(next maps)))))
|
(next maps)))))
|
||||||
|
|
||||||
|
(defn distinct-xf
|
||||||
|
[f]
|
||||||
|
(fn [rf]
|
||||||
|
(let [seen (volatile! #{})]
|
||||||
|
(fn
|
||||||
|
([] (rf))
|
||||||
|
([result] (rf result))
|
||||||
|
([result input]
|
||||||
|
(let [input* (f input)]
|
||||||
|
(if (contains? @seen input*)
|
||||||
|
result
|
||||||
|
(do (vswap! seen conj input*)
|
||||||
|
(rf result input)))))))))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Data Parsing / Conversion
|
;; Data Parsing / Conversion
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
[clojure.spec.alpha :as s]
|
[clojure.spec.alpha :as s]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(def valid-media-types
|
(def valid-font-types #{"font/ttf" "font/woff", "font/otf"})
|
||||||
#{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
|
(def valid-image-types #{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
|
||||||
|
(def str-image-types (str/join "," valid-image-types))
|
||||||
(def str-media-types (str/join "," valid-media-types))
|
(def str-font-types (str/join "," valid-font-types))
|
||||||
|
|
||||||
(defn format->extension
|
(defn format->extension
|
||||||
[format]
|
[format]
|
||||||
|
@ -65,3 +65,38 @@
|
||||||
::modified-at
|
::modified-at
|
||||||
::uri]))
|
::uri]))
|
||||||
|
|
||||||
|
|
||||||
|
(defn parse-font-weight
|
||||||
|
[variant]
|
||||||
|
(cond
|
||||||
|
(re-seq #"(?i)(?:hairline|thin)" variant) 100
|
||||||
|
(re-seq #"(?i)(?:extra light|ultra light)" variant) 200
|
||||||
|
(re-seq #"(?i)(?:light)" variant) 300
|
||||||
|
(re-seq #"(?i)(?:normal|regular)" variant) 400
|
||||||
|
(re-seq #"(?i)(?:medium)" variant) 500
|
||||||
|
(re-seq #"(?i)(?:semi bold|demi bold)" variant) 600
|
||||||
|
(re-seq #"(?i)(?:bold)" variant) 700
|
||||||
|
(re-seq #"(?i)(?:extra bold|ultra bold)" variant) 800
|
||||||
|
(re-seq #"(?i)(?:black|heavy)" variant) 900
|
||||||
|
(re-seq #"(?i)(?:extra black|ultra black)" variant) 950
|
||||||
|
:else 400))
|
||||||
|
|
||||||
|
(defn parse-font-style
|
||||||
|
[variant]
|
||||||
|
(if (re-seq #"(?i)(?:italic)" variant)
|
||||||
|
"italic"
|
||||||
|
"normal"))
|
||||||
|
|
||||||
|
(defn font-weight->name
|
||||||
|
[weight]
|
||||||
|
(case weight
|
||||||
|
100 "Hairline"
|
||||||
|
200 "Extra Light"
|
||||||
|
300 "Light"
|
||||||
|
400 "Regular"
|
||||||
|
500 "Medium"
|
||||||
|
600 "Semi Bold"
|
||||||
|
700 "Bold"
|
||||||
|
800 "Extra Bold"
|
||||||
|
900 "Black"
|
||||||
|
950 "Extra Black"))
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
(ns app.common.spec
|
(ns app.common.spec
|
||||||
"Data manipulation and query helper functions."
|
"Data manipulation and query helper functions."
|
||||||
(:refer-clojure :exclude [assert])
|
(:refer-clojure :exclude [assert bytes?])
|
||||||
#?(:cljs (:require-macros [app.common.spec :refer [assert]]))
|
#?(:cljs (:require-macros [app.common.spec :refer [assert]]))
|
||||||
(:require
|
(:require
|
||||||
#?(:clj [clojure.spec.alpha :as s]
|
#?(:clj [clojure.spec.alpha :as s]
|
||||||
|
@ -108,6 +108,20 @@
|
||||||
(s/def ::point gpt/point?)
|
(s/def ::point gpt/point?)
|
||||||
(s/def ::id ::uuid)
|
(s/def ::id ::uuid)
|
||||||
|
|
||||||
|
(defn bytes?
|
||||||
|
"Test if a first parameter is a byte
|
||||||
|
array or not."
|
||||||
|
[x]
|
||||||
|
(if (nil? x)
|
||||||
|
false
|
||||||
|
#?(:clj (= (Class/forName "[B")
|
||||||
|
(.getClass ^Object x))
|
||||||
|
:cljs (or (instance? js/Uint8Array x)
|
||||||
|
(instance? js/ArrayBuffer x)))))
|
||||||
|
|
||||||
|
(s/def ::bytes bytes?)
|
||||||
|
|
||||||
|
|
||||||
(s/def ::safe-integer
|
(s/def ::safe-integer
|
||||||
#(and
|
#(and
|
||||||
(int? %)
|
(int? %)
|
||||||
|
|
|
@ -17,6 +17,7 @@ const mkdirp = require("mkdirp");
|
||||||
const rimraf = require("rimraf");
|
const rimraf = require("rimraf");
|
||||||
const sass = require("sass");
|
const sass = require("sass");
|
||||||
const gettext = require("gettext-parser");
|
const gettext = require("gettext-parser");
|
||||||
|
const marked = require("marked");
|
||||||
|
|
||||||
const mapStream = require("map-stream");
|
const mapStream = require("map-stream");
|
||||||
const paths = {};
|
const paths = {};
|
||||||
|
@ -45,17 +46,35 @@ function readLocales() {
|
||||||
|
|
||||||
for (let key of Object.keys(trdata)) {
|
for (let key of Object.keys(trdata)) {
|
||||||
if (key === "") continue;
|
if (key === "") continue;
|
||||||
|
const comments = trdata[key].comments || {};
|
||||||
|
|
||||||
if (l.isNil(result[key])) {
|
if (l.isNil(result[key])) {
|
||||||
result[key] = {};
|
result[key] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const msgstr = trdata[key].msgstr;
|
const isMarkdown = l.includes(comments.flag, "markdown");
|
||||||
if (msgstr.length === 1) {
|
|
||||||
result[key][lang] = msgstr[0];
|
const msgs = trdata[key].msgstr;
|
||||||
} else {
|
if (msgs.length === 1) {
|
||||||
result[key][lang] = msgstr;
|
let message = msgs[0];
|
||||||
|
if (isMarkdown) {
|
||||||
|
message = marked.parseInline(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result[key][lang] = message;
|
||||||
|
} else {
|
||||||
|
result[key][lang] = msgs.map((item) => {
|
||||||
|
if (isMarkdown) {
|
||||||
|
return marked.parseInline(item);
|
||||||
|
} else {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// if (key === "modals.delete-font.title") {
|
||||||
|
// console.dir(trdata[key], {depth:10});
|
||||||
|
// console.dir(result[key], {depth:10});
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,12 +27,13 @@
|
||||||
"gulp-sourcemaps": "^3.0.0",
|
"gulp-sourcemaps": "^3.0.0",
|
||||||
"gulp-svg-sprite": "^1.5.0",
|
"gulp-svg-sprite": "^1.5.0",
|
||||||
"map-stream": "0.0.7",
|
"map-stream": "0.0.7",
|
||||||
|
"marked": "^2.0.3",
|
||||||
"mkdirp": "^1.0.4",
|
"mkdirp": "^1.0.4",
|
||||||
"postcss": "^8.2.7",
|
"postcss": "^8.2.7",
|
||||||
"postcss-clean": "^1.2.2",
|
"postcss-clean": "^1.2.2",
|
||||||
"rimraf": "^3.0.0",
|
"rimraf": "^3.0.0",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.32.8",
|
||||||
"shadow-cljs": "^2.11.20"
|
"shadow-cljs": "2.12.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"date-fns": "^2.21.1",
|
"date-fns": "^2.21.1",
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
"js-beautify": "^1.13.5",
|
"js-beautify": "^1.13.5",
|
||||||
"luxon": "^1.26.0",
|
"luxon": "^1.26.0",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
|
"opentype.js": "^1.3.3",
|
||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
"react": "~17.0.1",
|
"react": "~17.0.1",
|
||||||
"react-dom": "~17.0.1",
|
"react-dom": "~17.0.1",
|
||||||
|
|
|
@ -63,6 +63,7 @@
|
||||||
@import "main/partials/dashboard-sidebar";
|
@import "main/partials/dashboard-sidebar";
|
||||||
@import "main/partials/dashboard-team";
|
@import "main/partials/dashboard-team";
|
||||||
@import "main/partials/dashboard-settings";
|
@import "main/partials/dashboard-settings";
|
||||||
|
@import "main/partials/dashboard-fonts";
|
||||||
@import "main/partials/debug-icons-preview";
|
@import "main/partials/debug-icons-preview";
|
||||||
@import "main/partials/editable-label";
|
@import "main/partials/editable-label";
|
||||||
@import "main/partials/left-toolbar";
|
@import "main/partials/left-toolbar";
|
||||||
|
|
167
frontend/resources/styles/main/partials/dashboard-fonts.scss
Normal file
167
frontend/resources/styles/main/partials/dashboard-fonts.scss
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
.dashboard-fonts {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.dashboard-installed-fonts {
|
||||||
|
max-width: 1000px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
margin-top: $big;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: $fs14;
|
||||||
|
color: $color-gray-30;
|
||||||
|
margin: $x-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-item {
|
||||||
|
color: $color-black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.installed-fonts-header {
|
||||||
|
color: $color-gray-40;
|
||||||
|
display: flex;
|
||||||
|
height: 40px;
|
||||||
|
font-size: $fs12;
|
||||||
|
background-color: $color-white;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0px $big;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
input {
|
||||||
|
font-size: $fs12;
|
||||||
|
border: 1px solid $color-gray-30;
|
||||||
|
border-radius: $br-small;
|
||||||
|
width: 130px;
|
||||||
|
padding: $x-small;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fonts-group {
|
||||||
|
margin-top: $big;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-item {
|
||||||
|
color: $color-gray-40;
|
||||||
|
font-size: $fs14;
|
||||||
|
background-color: $color-white;
|
||||||
|
display: flex;
|
||||||
|
min-width: 1000px;
|
||||||
|
width: 100%;
|
||||||
|
height: 97px;
|
||||||
|
align-items: center;
|
||||||
|
padding: $big;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
border-top: 1px solid $color-gray-10;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: 1px solid $color-gray-30;
|
||||||
|
border-radius: $br-small;
|
||||||
|
margin: 0px;
|
||||||
|
padding: $small;
|
||||||
|
font-size: $fs12;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.variant {
|
||||||
|
font-size: $fs14;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filenames {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
font-size: $fs12;
|
||||||
|
}
|
||||||
|
|
||||||
|
.options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: $big;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
margin-left: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.close {
|
||||||
|
svg {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.dashboard-fonts-upload {
|
||||||
|
max-width: 1000px;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
|
||||||
|
.upload-button {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-fonts-hero {
|
||||||
|
font-size: $fs14;
|
||||||
|
|
||||||
|
padding: $x-big;
|
||||||
|
background-color: $color-white;
|
||||||
|
margin-top: $x-big;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.banner {
|
||||||
|
background-color: unset;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-right: 10px;
|
||||||
|
svg {
|
||||||
|
fill: $color-info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc {
|
||||||
|
h2 {
|
||||||
|
margin-bottom: $medium;
|
||||||
|
color: $color-black;
|
||||||
|
}
|
||||||
|
width: 80%;
|
||||||
|
color: $color-gray-40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -99,8 +99,6 @@
|
||||||
(->> (rp/query :team-stats {:team-id id})
|
(->> (rp/query :team-stats {:team-id id})
|
||||||
(rx/map #(partial fetched %)))))))
|
(rx/map #(partial fetched %)))))))
|
||||||
|
|
||||||
;; --- Fetch Projects
|
|
||||||
|
|
||||||
(defn fetch-projects
|
(defn fetch-projects
|
||||||
[{:keys [team-id] :as params}]
|
[{:keys [team-id] :as params}]
|
||||||
(us/assert ::us/uuid team-id)
|
(us/assert ::us/uuid team-id)
|
||||||
|
@ -123,8 +121,6 @@
|
||||||
(ptk/watch (fetch-projects {:team-id id}) state stream)
|
(ptk/watch (fetch-projects {:team-id id}) state stream)
|
||||||
(ptk/watch (du/fetch-users {:team-id id}) state stream))))))
|
(ptk/watch (du/fetch-users {:team-id id}) state stream))))))
|
||||||
|
|
||||||
;; --- Search Files
|
|
||||||
|
|
||||||
(s/def :internal.event.search-files/team-id ::us/uuid)
|
(s/def :internal.event.search-files/team-id ::us/uuid)
|
||||||
(s/def :internal.event.search-files/search-term (s/nilable ::us/string))
|
(s/def :internal.event.search-files/search-term (s/nilable ::us/string))
|
||||||
|
|
||||||
|
@ -149,8 +145,6 @@
|
||||||
(->> (rp/query :search-files params)
|
(->> (rp/query :search-files params)
|
||||||
(rx/map #(partial fetched %)))))))
|
(rx/map #(partial fetched %)))))))
|
||||||
|
|
||||||
;; --- Fetch Files
|
|
||||||
|
|
||||||
(defn fetch-files
|
(defn fetch-files
|
||||||
[{:keys [project-id] :as params}]
|
[{:keys [project-id] :as params}]
|
||||||
(us/assert ::us/uuid project-id)
|
(us/assert ::us/uuid project-id)
|
||||||
|
@ -162,8 +156,6 @@
|
||||||
(->> (rp/query :files params)
|
(->> (rp/query :files params)
|
||||||
(rx/map #(partial fetched %)))))))
|
(rx/map #(partial fetched %)))))))
|
||||||
|
|
||||||
;; --- Fetch Shared Files
|
|
||||||
|
|
||||||
(defn fetch-shared-files
|
(defn fetch-shared-files
|
||||||
[{:keys [team-id] :as params}]
|
[{:keys [team-id] :as params}]
|
||||||
(us/assert ::us/uuid team-id)
|
(us/assert ::us/uuid team-id)
|
||||||
|
@ -175,8 +167,6 @@
|
||||||
(->> (rp/query :shared-files {:team-id team-id})
|
(->> (rp/query :shared-files {:team-id team-id})
|
||||||
(rx/map #(partial fetched %)))))))
|
(rx/map #(partial fetched %)))))))
|
||||||
|
|
||||||
;; --- Fetch recent files
|
|
||||||
|
|
||||||
(declare recent-files-fetched)
|
(declare recent-files-fetched)
|
||||||
|
|
||||||
(defn fetch-recent-files
|
(defn fetch-recent-files
|
||||||
|
|
94
frontend/src/app/main/data/dashboard/fonts.cljs
Normal file
94
frontend/src/app/main/data/dashboard/fonts.cljs
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
;; 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) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.main.data.dashboard.fonts
|
||||||
|
(:require
|
||||||
|
[app.common.exceptions :as ex]
|
||||||
|
[app.common.data :as d]
|
||||||
|
[app.common.media :as cm]
|
||||||
|
[app.common.spec :as us]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.repo :as rp]
|
||||||
|
[app.util.time :as dt]
|
||||||
|
[app.util.timers :as ts]
|
||||||
|
[app.main.data.messages :as dm]
|
||||||
|
[app.util.webapi :as wa]
|
||||||
|
[app.util.object :as obj]
|
||||||
|
[app.util.transit :as t]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[cljs.spec.alpha :as s]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[potok.core :as ptk]))
|
||||||
|
|
||||||
|
(defn fetch-fonts
|
||||||
|
[{:keys [id] :as team}]
|
||||||
|
(ptk/reify ::fetch-fonts
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state stream]
|
||||||
|
(->> (rp/query! :team-font-variants {:team-id id})
|
||||||
|
(rx/map (fn [items]
|
||||||
|
#(assoc % :dashboard-fonts (d/index-by :id items))))))))
|
||||||
|
|
||||||
|
(defn add-font
|
||||||
|
[font]
|
||||||
|
(ptk/reify ::add-font
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update state :dashboard-fonts assoc (:id font) font))))
|
||||||
|
|
||||||
|
|
||||||
|
(defn update-font
|
||||||
|
[{:keys [id font-family] :as font}]
|
||||||
|
(ptk/reify ::update-font
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(let [font (assoc font :font-id (str "custom-" (str/slug font-family)))]
|
||||||
|
(update state :dashboard-fonts assoc id font)))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state stream]
|
||||||
|
(let [font (get-in state [:dashboard-fonts id])]
|
||||||
|
(->> (rp/mutation! :update-font-variant font)
|
||||||
|
(rx/ignore))))))
|
||||||
|
|
||||||
|
(defn delete-font
|
||||||
|
[{:keys [id] :as font}]
|
||||||
|
(ptk/reify ::delete-font
|
||||||
|
ptk/UpdateEvent
|
||||||
|
(update [_ state]
|
||||||
|
(update state :dashboard-fonts dissoc id))
|
||||||
|
|
||||||
|
ptk/WatchEvent
|
||||||
|
(watch [_ state stream]
|
||||||
|
(let [params (select-keys font [:id :team-id])]
|
||||||
|
(->> (rp/mutation! :delete-font-variant params)
|
||||||
|
(rx/ignore))))))
|
||||||
|
|
||||||
|
;; (defn upload-font
|
||||||
|
;; [{:keys [id] :as font}]
|
||||||
|
;; (ptk/reify ::upload-font
|
||||||
|
;; ptk/WatchEvent
|
||||||
|
;; (watch [_ state stream]
|
||||||
|
;; (let [{:keys [on-success on-error]
|
||||||
|
;; :or {on-success identity
|
||||||
|
;; on-error rx/throw}} (meta params)]
|
||||||
|
;; (->> (rp/mutation! :create-font-variant font)
|
||||||
|
;; (rx/tap on-success)
|
||||||
|
;; (rx/catch on-error))))))
|
||||||
|
|
||||||
|
;; (defn add-font
|
||||||
|
;; "Add fonts to the state in a pending to upload state."
|
||||||
|
;; [font]
|
||||||
|
;; (ptk/reify ::add-font
|
||||||
|
;; ptk/UpdateEvent
|
||||||
|
;; (update [_ state]
|
||||||
|
;; (let [id (uuid/next)
|
||||||
|
;; font (-> font
|
||||||
|
;; (assoc :created-at (dt/now))
|
||||||
|
;; (assoc :id id)
|
||||||
|
;; (assoc :status :draft))]
|
||||||
|
;; (js/console.log (clj->js font))
|
||||||
|
;; (assoc-in state [:dashboard-fonts id] font)))))
|
|
@ -51,7 +51,7 @@
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :media-too-large
|
:code :media-too-large
|
||||||
:hint (str/fmt "media size is large than 5mb (size: %s)" (.-size file))))
|
:hint (str/fmt "media size is large than 5mb (size: %s)" (.-size file))))
|
||||||
(when-not (contains? cm/valid-media-types (.-type file))
|
(when-not (contains? cm/valid-image-types (.-type file))
|
||||||
(ex/raise :type :validation
|
(ex/raise :type :validation
|
||||||
:code :media-type-not-allowed
|
:code :media-type-not-allowed
|
||||||
:hint (str/fmt "media type %s is not supported" (.-type file))))
|
:hint (str/fmt "media type %s is not supported" (.-type file))))
|
||||||
|
|
|
@ -43,6 +43,9 @@
|
||||||
(def dashboard-local
|
(def dashboard-local
|
||||||
(l/derived :dashboard-local st/state))
|
(l/derived :dashboard-local st/state))
|
||||||
|
|
||||||
|
(def dashboard-fonts
|
||||||
|
(l/derived :dashboard-fonts st/state))
|
||||||
|
|
||||||
(def dashboard-selected-project
|
(def dashboard-selected-project
|
||||||
(l/derived (fn [state]
|
(l/derived (fn [state]
|
||||||
(get-in state [:dashboard-local :selected-project]))
|
(get-in state [:dashboard-local :selected-project]))
|
||||||
|
|
|
@ -90,6 +90,8 @@
|
||||||
["/settings" :dashboard-team-settings]
|
["/settings" :dashboard-team-settings]
|
||||||
["/projects" :dashboard-projects]
|
["/projects" :dashboard-projects]
|
||||||
["/search" :dashboard-search]
|
["/search" :dashboard-search]
|
||||||
|
["/fonts" :dashboard-fonts]
|
||||||
|
["/fonts/providers" :dashboard-font-providers]
|
||||||
["/libraries" :dashboard-libraries]
|
["/libraries" :dashboard-libraries]
|
||||||
["/projects/:project-id" :dashboard-files]]
|
["/projects/:project-id" :dashboard-files]]
|
||||||
|
|
||||||
|
@ -135,12 +137,11 @@
|
||||||
:dashboard-projects
|
:dashboard-projects
|
||||||
:dashboard-files
|
:dashboard-files
|
||||||
:dashboard-libraries
|
:dashboard-libraries
|
||||||
|
:dashboard-fonts
|
||||||
|
:dashboard-font-providers
|
||||||
:dashboard-team-members
|
:dashboard-team-members
|
||||||
:dashboard-team-settings)
|
:dashboard-team-settings)
|
||||||
[:*
|
[:& dashboard {:route route}]
|
||||||
#_[:div.modal-wrapper
|
|
||||||
[:& app.main.ui.onboarding/release-notes-modal {:version "1.4"}]]
|
|
||||||
[:& dashboard {:route route}]]
|
|
||||||
|
|
||||||
:viewer
|
:viewer
|
||||||
(let [index (get-in route [:query-params :index])
|
(let [index (get-in route [:query-params :index])
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
[app.main.ui.components.dropdown :refer [dropdown']]
|
[app.main.ui.components.dropdown :refer [dropdown']]
|
||||||
[app.main.ui.icons :as i]
|
[app.main.ui.icons :as i]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.util.data :refer [classnames]]
|
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.object :as obj]))
|
[app.util.object :as obj]))
|
||||||
|
|
||||||
|
@ -81,13 +80,13 @@
|
||||||
|
|
||||||
(when (and open? (some? (:levels @local)))
|
(when (and open? (some? (:levels @local)))
|
||||||
[:> dropdown' props
|
[:> dropdown' props
|
||||||
[:div.context-menu {:class (classnames :is-open open?
|
[:div.context-menu {:class (dom/classnames :is-open open?
|
||||||
:fixed fixed?
|
:fixed fixed?
|
||||||
:is-selectable is-selectable)
|
:is-selectable is-selectable)
|
||||||
:style {:top (+ top (:offset @local))
|
:style {:top (+ top (:offset @local))
|
||||||
:left left}}
|
:left left}}
|
||||||
(let [level (-> @local :levels peek)]
|
(let [level (-> @local :levels peek)]
|
||||||
[:ul.context-menu-items {:class (classnames :min-width min-width?)
|
[:ul.context-menu-items {:class (dom/classnames :min-width min-width?)
|
||||||
:ref check-menu-offscreen}
|
:ref check-menu-offscreen}
|
||||||
(when-let [parent-option (:parent-option level)]
|
(when-let [parent-option (:parent-option level)]
|
||||||
[:*
|
[:*
|
||||||
|
@ -103,8 +102,7 @@
|
||||||
(if (= option-name :separator)
|
(if (= option-name :separator)
|
||||||
[:li.separator]
|
[:li.separator]
|
||||||
[:li.context-menu-item
|
[:li.context-menu-item
|
||||||
{:class (classnames :is-selected (and selected
|
{:class (dom/classnames :is-selected (and selected (= option-name selected)))
|
||||||
(= option-name selected)))
|
|
||||||
:key option-name}
|
:key option-name}
|
||||||
(if-not sub-options
|
(if-not sub-options
|
||||||
[:a.context-menu-action {:on-click option-handler}
|
[:a.context-menu-action {:on-click option-handler}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
[app.main.ui.dashboard.files :refer [files-section]]
|
[app.main.ui.dashboard.files :refer [files-section]]
|
||||||
[app.main.ui.dashboard.libraries :refer [libraries-page]]
|
[app.main.ui.dashboard.libraries :refer [libraries-page]]
|
||||||
[app.main.ui.dashboard.projects :refer [projects-section]]
|
[app.main.ui.dashboard.projects :refer [projects-section]]
|
||||||
|
[app.main.ui.dashboard.fonts :refer [fonts-page font-providers-page]]
|
||||||
[app.main.ui.dashboard.search :refer [search-page]]
|
[app.main.ui.dashboard.search :refer [search-page]]
|
||||||
[app.main.ui.dashboard.sidebar :refer [sidebar]]
|
[app.main.ui.dashboard.sidebar :refer [sidebar]]
|
||||||
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
|
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
|
||||||
|
@ -65,6 +66,12 @@
|
||||||
:dashboard-projects
|
:dashboard-projects
|
||||||
[:& projects-section {:team team :projects projects}]
|
[:& projects-section {:team team :projects projects}]
|
||||||
|
|
||||||
|
:dashboard-fonts
|
||||||
|
[:& fonts-page {:team team}]
|
||||||
|
|
||||||
|
:dashboard-font-providers
|
||||||
|
[:& font-providers-page {:team team}]
|
||||||
|
|
||||||
:dashboard-files
|
:dashboard-files
|
||||||
(when project
|
(when project
|
||||||
[:& files-section {:team team :project project}])
|
[:& files-section {:team team :project project}])
|
||||||
|
@ -121,14 +128,16 @@
|
||||||
[:& (mf/provider ctx/current-page-id) {:value nil}
|
[:& (mf/provider ctx/current-page-id) {:value nil}
|
||||||
|
|
||||||
[:section.dashboard-layout
|
[:section.dashboard-layout
|
||||||
[:& sidebar {:team team
|
[:& sidebar
|
||||||
|
{:team team
|
||||||
:projects projects
|
:projects projects
|
||||||
:project project
|
:project project
|
||||||
:profile profile
|
:profile profile
|
||||||
:section section
|
:section section
|
||||||
:search-term search-term}]
|
:search-term search-term}]
|
||||||
(when (and team (seq projects))
|
(when (and team (seq projects))
|
||||||
[:& dashboard-content {:projects projects
|
[:& dashboard-content
|
||||||
|
{:projects projects
|
||||||
:profile profile
|
:profile profile
|
||||||
:project project
|
:project project
|
||||||
:section section
|
:section section
|
||||||
|
|
353
frontend/src/app/main/ui/dashboard/fonts.cljs
Normal file
353
frontend/src/app/main/ui/dashboard/fonts.cljs
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
;; 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) UXBOX Labs SL
|
||||||
|
|
||||||
|
(ns app.main.ui.dashboard.fonts
|
||||||
|
(:require
|
||||||
|
["opentype.js" :as ot]
|
||||||
|
[app.common.media :as cm]
|
||||||
|
[app.common.uuid :as uuid]
|
||||||
|
[app.main.data.dashboard :as dd]
|
||||||
|
[app.main.data.dashboard.fonts :as df]
|
||||||
|
[app.main.data.modal :as modal]
|
||||||
|
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||||
|
[app.main.ui.components.context-menu :refer [context-menu]]
|
||||||
|
[app.main.store :as st]
|
||||||
|
[app.main.repo :as rp]
|
||||||
|
[app.main.refs :as refs]
|
||||||
|
[app.main.ui.icons :as i]
|
||||||
|
[app.util.dom :as dom]
|
||||||
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
|
[app.util.logging :as log]
|
||||||
|
[app.util.keyboard :as kbd]
|
||||||
|
[app.util.router :as rt]
|
||||||
|
[app.util.webapi :as wa]
|
||||||
|
[cuerdas.core :as str]
|
||||||
|
[beicon.core :as rx]
|
||||||
|
[okulary.core :as l]
|
||||||
|
[rumext.alpha :as mf]))
|
||||||
|
|
||||||
|
(log/set-level! :trace)
|
||||||
|
|
||||||
|
(defn- use-set-page-title
|
||||||
|
[team section]
|
||||||
|
(mf/use-effect
|
||||||
|
(mf/deps team)
|
||||||
|
(fn []
|
||||||
|
(when team
|
||||||
|
(let [tname (if (:is-default team)
|
||||||
|
(tr "dashboard.your-penpot")
|
||||||
|
(:name team))]
|
||||||
|
(case section
|
||||||
|
:fonts (dom/set-html-title (tr "title.dashboard.fonts" tname))
|
||||||
|
:providers (dom/set-html-title (tr "title.dashboard.font-providers" tname))))))))
|
||||||
|
|
||||||
|
(mf/defc header
|
||||||
|
{::mf/wrap [mf/memo]}
|
||||||
|
[{:keys [section team] :as props}]
|
||||||
|
(let [go-fonts
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps team)
|
||||||
|
(st/emitf (rt/nav :dashboard-fonts {:team-id (:id team)})))
|
||||||
|
|
||||||
|
go-providers
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps team)
|
||||||
|
(st/emitf (rt/nav :dashboard-font-providers {:team-id (:id team)})))]
|
||||||
|
|
||||||
|
(use-set-page-title team section)
|
||||||
|
|
||||||
|
[:header.dashboard-header
|
||||||
|
[:div.dashboard-title
|
||||||
|
[:h1 (tr "labels.fonts")]]
|
||||||
|
[:nav
|
||||||
|
[:ul
|
||||||
|
[:li {:class (when (= section :fonts) "active")}
|
||||||
|
[:a {:on-click go-fonts} (tr "labels.custom-fonts")]]
|
||||||
|
[:li {:class (when (= section :providers) "active")}
|
||||||
|
[:a {:on-click go-providers} (tr "labels.font-providers")]]]]
|
||||||
|
|
||||||
|
[:div]]))
|
||||||
|
|
||||||
|
(defn- prepare-fonts
|
||||||
|
[blobs]
|
||||||
|
(letfn [(prepare [{:keys [font type name data] :as params}]
|
||||||
|
(let [family (or (.getEnglishName ^js font "preferredFamily")
|
||||||
|
(.getEnglishName ^js font "fontFamily"))
|
||||||
|
variant (or (.getEnglishName ^js font "preferredSubfamily")
|
||||||
|
(.getEnglishName ^js font "fontSubfamily"))]
|
||||||
|
{:content {:data (js/Uint8Array. data)
|
||||||
|
:name name
|
||||||
|
:type type}
|
||||||
|
:font-id (str "custom-" (str/slug family))
|
||||||
|
:font-family family
|
||||||
|
:font-weight (cm/parse-font-weight variant)
|
||||||
|
:font-style (cm/parse-font-style variant)}))
|
||||||
|
|
||||||
|
(parse-mtype [mtype]
|
||||||
|
(case mtype
|
||||||
|
"application/vnd.oasis.opendocument.formula-template" "font/otf"
|
||||||
|
mtype))
|
||||||
|
|
||||||
|
(parse-font [{:keys [data] :as params}]
|
||||||
|
(try
|
||||||
|
(assoc params :font (ot/parse data))
|
||||||
|
(catch :default e
|
||||||
|
(log/warn :msg (str/fmt "skiping file %s, unsupported format" (:name params)))
|
||||||
|
nil)))
|
||||||
|
|
||||||
|
(read-blob [blob]
|
||||||
|
(->> (wa/read-file-as-array-buffer blob)
|
||||||
|
(rx/map (fn [data]
|
||||||
|
{:data data
|
||||||
|
:name (.-name blob)
|
||||||
|
:type (parse-mtype (.-type blob))}))))]
|
||||||
|
|
||||||
|
(->> (rx/from blobs)
|
||||||
|
(rx/mapcat read-blob)
|
||||||
|
(rx/map parse-font)
|
||||||
|
(rx/filter some?)
|
||||||
|
(rx/map prepare))))
|
||||||
|
|
||||||
|
(mf/defc fonts-upload
|
||||||
|
[{:keys [team] :as props}]
|
||||||
|
(let [fonts (mf/use-state {})
|
||||||
|
input-ref (mf/use-ref)
|
||||||
|
|
||||||
|
uploading (mf/use-state #{})
|
||||||
|
|
||||||
|
on-click
|
||||||
|
(mf/use-callback #(dom/click (mf/ref-val input-ref)))
|
||||||
|
|
||||||
|
font-key-fn
|
||||||
|
(mf/use-callback (juxt :font-family :font-weight :font-style))
|
||||||
|
|
||||||
|
on-selected
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps team)
|
||||||
|
(fn [blobs]
|
||||||
|
(->> (prepare-fonts blobs)
|
||||||
|
(rx/subs (fn [{:keys [content] :as font}]
|
||||||
|
(let [key (font-key-fn font)]
|
||||||
|
(swap! fonts update key
|
||||||
|
(fn [val]
|
||||||
|
(-> (or val font)
|
||||||
|
(assoc :team-id (:id team))
|
||||||
|
(update :id #(or % (uuid/next)))
|
||||||
|
(update :data assoc (:type content) (:data content))
|
||||||
|
(update :names (fnil conj #{}) (:name content))
|
||||||
|
(dissoc :content))))))
|
||||||
|
(fn [error]
|
||||||
|
(js/console.error "error" error))))))
|
||||||
|
|
||||||
|
on-upload
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps team)
|
||||||
|
(fn [item]
|
||||||
|
(let [key (font-key-fn item)]
|
||||||
|
(swap! uploading conj (:id item))
|
||||||
|
(->> (rp/mutation! :create-font-variant item)
|
||||||
|
(rx/delay-at-least 2000)
|
||||||
|
(rx/subs (fn [font]
|
||||||
|
(swap! fonts dissoc key)
|
||||||
|
(swap! uploading disj (:id item))
|
||||||
|
(st/emit! (df/add-font font)))
|
||||||
|
(fn [error]
|
||||||
|
(js/console.log "error" error)))))))
|
||||||
|
|
||||||
|
on-delete
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps team)
|
||||||
|
(fn [item]
|
||||||
|
(swap! fonts dissoc (font-key-fn item))))]
|
||||||
|
|
||||||
|
[:div.dashboard-fonts-upload
|
||||||
|
[:div.dashboard-fonts-hero
|
||||||
|
[:div.desc
|
||||||
|
[:h2 (tr "labels.upload-custom-fonts")]
|
||||||
|
[:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}]
|
||||||
|
|
||||||
|
[:div.banner
|
||||||
|
[:div.icon i/msg-info]
|
||||||
|
[:div.content
|
||||||
|
[:& i18n/tr-html {:tag-name "span"
|
||||||
|
:label "dashboard.fonts.hero-text2"}]]]]
|
||||||
|
|
||||||
|
[:div.btn-primary
|
||||||
|
{:on-click on-click}
|
||||||
|
[:span "Add custom font"]
|
||||||
|
[:& file-uploader {:input-id "font-upload"
|
||||||
|
:accept cm/str-font-types
|
||||||
|
:multi true
|
||||||
|
:input-ref input-ref
|
||||||
|
:on-selected on-selected}]]]
|
||||||
|
|
||||||
|
[:*
|
||||||
|
(for [item (sort-by :font-family (vals @fonts))]
|
||||||
|
(let [uploading? (contains? @uploading (:id item))]
|
||||||
|
[:div.font-item.table-row {:key (:id item)}
|
||||||
|
[:div.table-field.family
|
||||||
|
[:input {:type "text"
|
||||||
|
:default-value (:font-family item)}]]
|
||||||
|
[:div.table-field.variant
|
||||||
|
[:span (cm/font-weight->name (:font-weight item))]
|
||||||
|
(when (not= "normal" (:font-style item))
|
||||||
|
[:span " " (str/capital (:font-style item))])]
|
||||||
|
[:div.table-field.filenames
|
||||||
|
(for [item (:names item)]
|
||||||
|
[:span item])]
|
||||||
|
|
||||||
|
[:div.table-field.options
|
||||||
|
[:button.btn-primary.upload-button
|
||||||
|
{:on-click #(on-upload item)
|
||||||
|
:class (dom/classnames :disabled uploading?)
|
||||||
|
:disabled uploading?}
|
||||||
|
(if uploading?
|
||||||
|
(tr "labels.uploading")
|
||||||
|
(tr "labels.upload"))]
|
||||||
|
[:span.icon.close {:on-click #(on-delete item)} i/close]]]))]]))
|
||||||
|
|
||||||
|
(mf/defc installed-font
|
||||||
|
[{:keys [font] :as props}]
|
||||||
|
(let [open-menu? (mf/use-state false)
|
||||||
|
edit? (mf/use-state false)
|
||||||
|
state (mf/use-var (:font-family font))
|
||||||
|
|
||||||
|
on-change
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps font)
|
||||||
|
(fn [event]
|
||||||
|
(reset! state (dom/get-target-val event))))
|
||||||
|
|
||||||
|
on-save
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps font)
|
||||||
|
(fn [event]
|
||||||
|
(let [font (assoc font :font-family @state)]
|
||||||
|
(st/emit! (df/update-font font))
|
||||||
|
(reset! edit? false))))
|
||||||
|
|
||||||
|
on-key-down
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps font)
|
||||||
|
(fn [event]
|
||||||
|
(when (kbd/enter? event)
|
||||||
|
(on-save event))))
|
||||||
|
|
||||||
|
on-cancel
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps font)
|
||||||
|
(fn [event]
|
||||||
|
(reset! edit? false)
|
||||||
|
(reset! state (:font-family font))))
|
||||||
|
|
||||||
|
delete-fn
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps font)
|
||||||
|
(st/emitf (df/delete-font font)))
|
||||||
|
|
||||||
|
on-delete
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps font)
|
||||||
|
(st/emitf (modal/show
|
||||||
|
{:type :confirm
|
||||||
|
:title (tr "modals.delete-font.title")
|
||||||
|
:message (tr "modals.delete-font.message")
|
||||||
|
:accept-label (tr "labels.delete")
|
||||||
|
:on-accept delete-fn})))]
|
||||||
|
|
||||||
|
|
||||||
|
[:div.font-item.table-row {:key (:id font)}
|
||||||
|
[:div.table-field.family
|
||||||
|
(if @edit?
|
||||||
|
[:input {:type "text"
|
||||||
|
:default-value @state
|
||||||
|
:on-key-down on-key-down
|
||||||
|
:on-change on-change}]
|
||||||
|
[:span (:font-family font)])]
|
||||||
|
|
||||||
|
[:div.table-field.variant
|
||||||
|
[:span (cm/font-weight->name (:font-weight font))]
|
||||||
|
(when (not= "normal" (:font-style font))
|
||||||
|
[:span " " (str/capital (:font-style font))])]
|
||||||
|
|
||||||
|
[:div]
|
||||||
|
|
||||||
|
(if @edit?
|
||||||
|
[:div.table-field.options
|
||||||
|
[:button.btn-primary
|
||||||
|
{:disabled (str/blank? @state)
|
||||||
|
:on-click on-save
|
||||||
|
:class (dom/classnames :btn-disabled (str/blank? @state))}
|
||||||
|
"Save"]
|
||||||
|
[:span.icon.close {:on-click on-cancel} i/close]]
|
||||||
|
|
||||||
|
[:div.table-field.options
|
||||||
|
[:span.icon {:on-click #(reset! open-menu? true)} i/actions]
|
||||||
|
[:& context-menu
|
||||||
|
{:on-close #(reset! open-menu? false)
|
||||||
|
:show @open-menu?
|
||||||
|
:fixed? false
|
||||||
|
:top -15
|
||||||
|
:left -115
|
||||||
|
:options [[(tr "labels.edit") #(reset! edit? true)]
|
||||||
|
[(tr "labels.delete") on-delete]]}]])]))
|
||||||
|
|
||||||
|
|
||||||
|
(mf/defc installed-fonts
|
||||||
|
[{:keys [team fonts] :as props}]
|
||||||
|
(let [sterm (mf/use-state "")
|
||||||
|
|
||||||
|
matches?
|
||||||
|
#(str/includes? (str/lower (:font-family %)) @sterm)
|
||||||
|
|
||||||
|
on-change
|
||||||
|
(mf/use-callback
|
||||||
|
(fn [event]
|
||||||
|
(let [val (dom/get-target-val event)]
|
||||||
|
(reset! sterm val))))]
|
||||||
|
|
||||||
|
[:div.dashboard-installed-fonts
|
||||||
|
[:h3 (tr "labels.installed-fonts")]
|
||||||
|
[:div.installed-fonts-header
|
||||||
|
[:div.table-field.family (tr "labels.font-family")]
|
||||||
|
[:div.table-field.variant (tr "labels.font-variant")]
|
||||||
|
[:div]
|
||||||
|
[:div.table-field.search-input
|
||||||
|
[:input {:placeholder (tr "labels.search-font")
|
||||||
|
:default-value ""
|
||||||
|
:on-change on-change
|
||||||
|
}]]]
|
||||||
|
(for [[font-id fonts] (->> fonts
|
||||||
|
(filter matches?)
|
||||||
|
(group-by :font-id))]
|
||||||
|
[:div.fonts-group
|
||||||
|
(for [font (sort-by (juxt :font-weight :font-style) fonts)]
|
||||||
|
[:& installed-font {:key (:id font) :font font}])])]))
|
||||||
|
|
||||||
|
|
||||||
|
(mf/defc fonts-page
|
||||||
|
[{:keys [team] :as props}]
|
||||||
|
(let [fonts-map (mf/deref refs/dashboard-fonts)
|
||||||
|
fonts (vals fonts-map)]
|
||||||
|
|
||||||
|
(mf/use-effect
|
||||||
|
(mf/deps team)
|
||||||
|
(st/emitf (df/fetch-fonts team)))
|
||||||
|
|
||||||
|
[:*
|
||||||
|
[:& header {:team team :section :fonts}]
|
||||||
|
[:section.dashboard-container.dashboard-fonts
|
||||||
|
[:& fonts-upload {:team team}]
|
||||||
|
|
||||||
|
(when fonts
|
||||||
|
[:& installed-fonts {:team team
|
||||||
|
:fonts fonts}])]]))
|
||||||
|
(mf/defc font-providers-page
|
||||||
|
[{:keys [team] :as props}]
|
||||||
|
[:*
|
||||||
|
[:& header {:team team :section :providers}]
|
||||||
|
[:section.dashboard-container
|
||||||
|
[:span "hello world font providers"]]])
|
|
@ -28,7 +28,7 @@
|
||||||
[app.util.avatars :as avatars]
|
[app.util.avatars :as avatars]
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.dom.dnd :as dnd]
|
[app.util.dom.dnd :as dnd]
|
||||||
[app.util.i18n :as i18n :refer [t tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
[app.util.keyboard :as kbd]
|
[app.util.keyboard :as kbd]
|
||||||
[app.util.object :as obj]
|
[app.util.object :as obj]
|
||||||
[app.util.router :as rt]
|
[app.util.router :as rt]
|
||||||
|
@ -47,7 +47,8 @@
|
||||||
selected-project (:selected-project dstate)
|
selected-project (:selected-project dstate)
|
||||||
edit-id (:project-for-edit dstate)
|
edit-id (:project-for-edit dstate)
|
||||||
|
|
||||||
local (mf/use-state {:menu-open false
|
local (mf/use-state
|
||||||
|
{:menu-open false
|
||||||
:menu-pos nil
|
:menu-pos nil
|
||||||
:edition? (= (:id item) edit-id)
|
:edition? (= (:id item) edit-id)
|
||||||
:dragging? false})
|
:dragging? false})
|
||||||
|
@ -60,10 +61,12 @@
|
||||||
:project-id (:id item)}))))
|
:project-id (:id item)}))))
|
||||||
|
|
||||||
on-menu-click
|
on-menu-click
|
||||||
(mf/use-callback (fn [event]
|
(mf/use-callback
|
||||||
|
(fn [event]
|
||||||
(let [position (dom/get-client-position event)]
|
(let [position (dom/get-client-position event)]
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default event)
|
||||||
(swap! local assoc :menu-open true
|
(swap! local assoc
|
||||||
|
:menu-open true
|
||||||
:menu-pos position))))
|
:menu-pos position))))
|
||||||
|
|
||||||
on-menu-close
|
on-menu-close
|
||||||
|
@ -139,7 +142,7 @@
|
||||||
:on-menu-close on-menu-close}]]))
|
:on-menu-close on-menu-close}]]))
|
||||||
|
|
||||||
(mf/defc sidebar-search
|
(mf/defc sidebar-search
|
||||||
[{:keys [search-term team-id locale] :as props}]
|
[{:keys [search-term team-id] :as props}]
|
||||||
(let [search-term (or search-term "")
|
(let [search-term (or search-term "")
|
||||||
focused? (mf/use-state false)
|
focused? (mf/use-state false)
|
||||||
emit! (mf/use-memo #(f/debounce st/emit! 500))
|
emit! (mf/use-memo #(f/debounce st/emit! 500))
|
||||||
|
@ -183,7 +186,7 @@
|
||||||
{:key :images-search-box
|
{:key :images-search-box
|
||||||
:id "search-input"
|
:id "search-input"
|
||||||
:type "text"
|
:type "text"
|
||||||
:placeholder (t locale "dashboard.search-placeholder")
|
:placeholder (tr "dashboard.search-placeholder")
|
||||||
:default-value search-term
|
:default-value search-term
|
||||||
:auto-complete "off"
|
:auto-complete "off"
|
||||||
:on-focus on-search-focus
|
:on-focus on-search-focus
|
||||||
|
@ -201,7 +204,7 @@
|
||||||
i/search])]))
|
i/search])]))
|
||||||
|
|
||||||
(mf/defc teams-selector-dropdown
|
(mf/defc teams-selector-dropdown
|
||||||
[{:keys [team profile locale] :as props}]
|
[{:keys [team profile] :as props}]
|
||||||
(let [show-dropdown? (mf/use-state false)
|
(let [show-dropdown? (mf/use-state false)
|
||||||
teams (mf/deref refs/teams)
|
teams (mf/deref refs/teams)
|
||||||
|
|
||||||
|
@ -216,11 +219,11 @@
|
||||||
(st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))]
|
(st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))]
|
||||||
|
|
||||||
[:ul.dropdown.teams-dropdown
|
[:ul.dropdown.teams-dropdown
|
||||||
[:li.title (t locale "dashboard.switch-team")]
|
[:li.title (tr "dashboard.switch-team")]
|
||||||
[:hr]
|
[:hr]
|
||||||
[:li.team-name {:on-click (partial team-selected (:default-team-id profile))}
|
[:li.team-name {:on-click (partial team-selected (:default-team-id profile))}
|
||||||
[:span.team-icon i/logo-icon]
|
[:span.team-icon i/logo-icon]
|
||||||
[:span.team-text (t locale "dashboard.your-penpot")]]
|
[:span.team-text (tr "dashboard.your-penpot")]]
|
||||||
|
|
||||||
(for [team (remove :is-default (vals teams))]
|
(for [team (remove :is-default (vals teams))]
|
||||||
[:* {:key (:id team)}
|
[:* {:key (:id team)}
|
||||||
|
@ -231,7 +234,7 @@
|
||||||
|
|
||||||
[:hr]
|
[:hr]
|
||||||
[:li.action {:on-click on-create-clicked}
|
[:li.action {:on-click on-create-clicked}
|
||||||
(t locale "dashboard.create-new-team")]]))
|
(tr "dashboard.create-new-team")]]))
|
||||||
|
|
||||||
(s/def ::member-id ::us/uuid)
|
(s/def ::member-id ::us/uuid)
|
||||||
(s/def ::leave-modal-form
|
(s/def ::leave-modal-form
|
||||||
|
@ -292,7 +295,7 @@
|
||||||
|
|
||||||
|
|
||||||
(mf/defc team-options-dropdown
|
(mf/defc team-options-dropdown
|
||||||
[{:keys [team locale profile] :as props}]
|
[{:keys [team profile] :as props}]
|
||||||
(let [members (mf/use-state [])
|
(let [members (mf/use-state [])
|
||||||
|
|
||||||
go-members
|
go-members
|
||||||
|
@ -341,9 +344,9 @@
|
||||||
(mf/deps team)
|
(mf/deps team)
|
||||||
(st/emitf (modal/show
|
(st/emitf (modal/show
|
||||||
{:type :confirm
|
{:type :confirm
|
||||||
:title (t locale "modals.leave-confirm.title")
|
:title (tr "modals.leave-confirm.title")
|
||||||
:message (t locale "modals.leave-confirm.message")
|
:message (tr "modals.leave-confirm.message")
|
||||||
:accept-label (t locale "modals.leave-confirm.accept")
|
:accept-label (tr "modals.leave-confirm.accept")
|
||||||
:on-accept leave-fn})))
|
:on-accept leave-fn})))
|
||||||
|
|
||||||
on-leave-as-owner-clicked
|
on-leave-as-owner-clicked
|
||||||
|
@ -366,9 +369,9 @@
|
||||||
(mf/deps team)
|
(mf/deps team)
|
||||||
(st/emitf (modal/show
|
(st/emitf (modal/show
|
||||||
{:type :confirm
|
{:type :confirm
|
||||||
:title (t locale "modals.delete-team-confirm.title")
|
:title (tr "modals.delete-team-confirm.title")
|
||||||
:message (t locale "modals.delete-team-confirm.message")
|
:message (tr "modals.delete-team-confirm.message")
|
||||||
:accept-label (t locale "modals.delete-team-confirm.accept")
|
:accept-label (tr "modals.delete-team-confirm.accept")
|
||||||
:on-accept delete-fn})))]
|
:on-accept delete-fn})))]
|
||||||
|
|
||||||
(mf/use-layout-effect
|
(mf/use-layout-effect
|
||||||
|
@ -378,25 +381,25 @@
|
||||||
(rx/subs #(reset! members %)))))
|
(rx/subs #(reset! members %)))))
|
||||||
|
|
||||||
[:ul.dropdown.options-dropdown
|
[:ul.dropdown.options-dropdown
|
||||||
[:li {:on-click go-members} (t locale "labels.members")]
|
[:li {:on-click go-members} (tr "labels.members")]
|
||||||
[:li {:on-click go-settings} (t locale "labels.settings")]
|
[:li {:on-click go-settings} (tr "labels.settings")]
|
||||||
[:hr]
|
[:hr]
|
||||||
[:li {:on-click on-rename-clicked} (t locale "labels.rename")]
|
[:li {:on-click on-rename-clicked} (tr "labels.rename")]
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
(:is-owner team)
|
(:is-owner team)
|
||||||
[:li {:on-click on-leave-as-owner-clicked} (t locale "dashboard.leave-team")]
|
[:li {:on-click on-leave-as-owner-clicked} (tr "dashboard.leave-team")]
|
||||||
|
|
||||||
(> (count @members) 1)
|
(> (count @members) 1)
|
||||||
[:li {:on-click on-leave-clicked} (t locale "dashboard.leave-team")])
|
[:li {:on-click on-leave-clicked} (tr "dashboard.leave-team")])
|
||||||
|
|
||||||
|
|
||||||
(when (:is-owner team)
|
(when (:is-owner team)
|
||||||
[:li {:on-click on-delete-clicked} (t locale "dashboard.delete-team")])]))
|
[:li {:on-click on-delete-clicked} (tr "dashboard.delete-team")])]))
|
||||||
|
|
||||||
|
|
||||||
(mf/defc sidebar-team-switch
|
(mf/defc sidebar-team-switch
|
||||||
[{:keys [team profile locale] :as props}]
|
[{:keys [team profile] :as props}]
|
||||||
(let [show-dropdown? (mf/use-state false)
|
(let [show-dropdown? (mf/use-state false)
|
||||||
|
|
||||||
show-team-opts-ddwn? (mf/use-state false)
|
show-team-opts-ddwn? (mf/use-state false)
|
||||||
|
@ -408,7 +411,7 @@
|
||||||
(if (:is-default team)
|
(if (:is-default team)
|
||||||
[:div.team-name
|
[:div.team-name
|
||||||
[:span.team-icon i/logo-icon]
|
[:span.team-icon i/logo-icon]
|
||||||
[:span.team-text (t locale "dashboard.default-team-name")]]
|
[:span.team-text (tr "dashboard.default-team-name")]]
|
||||||
[:div.team-name
|
[:div.team-name
|
||||||
[:span.team-icon
|
[:span.team-icon
|
||||||
[:img {:src (cfg/resolve-team-photo-url team)}]]
|
[:img {:src (cfg/resolve-team-photo-url team)}]]
|
||||||
|
@ -425,23 +428,22 @@
|
||||||
[:& dropdown {:show @show-teams-ddwn?
|
[:& dropdown {:show @show-teams-ddwn?
|
||||||
:on-close #(reset! show-teams-ddwn? false)}
|
:on-close #(reset! show-teams-ddwn? false)}
|
||||||
[:& teams-selector-dropdown {:team team
|
[:& teams-selector-dropdown {:team team
|
||||||
:profile profile
|
:profile profile}]]
|
||||||
:locale locale}]]
|
|
||||||
|
|
||||||
[:& dropdown {:show @show-team-opts-ddwn?
|
[:& dropdown {:show @show-team-opts-ddwn?
|
||||||
:on-close #(reset! show-team-opts-ddwn? false)}
|
:on-close #(reset! show-team-opts-ddwn? false)}
|
||||||
[:& team-options-dropdown {:team team
|
[:& team-options-dropdown {:team team
|
||||||
:profile profile
|
:profile profile}]]]))
|
||||||
:locale locale}]]]))
|
|
||||||
|
|
||||||
(mf/defc sidebar-content
|
(mf/defc sidebar-content
|
||||||
[{:keys [locale projects profile section team project search-term] :as props}]
|
[{:keys [projects profile section team project search-term] :as props}]
|
||||||
(let [default-project-id
|
(let [default-project-id
|
||||||
(->> (vals projects)
|
(->> (vals projects)
|
||||||
(d/seek :is-default)
|
(d/seek :is-default)
|
||||||
(:id))
|
(:id))
|
||||||
|
|
||||||
projects? (= section :dashboard-projects)
|
projects? (= section :dashboard-projects)
|
||||||
|
fonts? (= section :dashboard-fonts)
|
||||||
libs? (= section :dashboard-libraries)
|
libs? (= section :dashboard-libraries)
|
||||||
drafts? (and (= section :dashboard-files)
|
drafts? (and (= section :dashboard-files)
|
||||||
(= (:id project) default-project-id))
|
(= (:id project) default-project-id))
|
||||||
|
@ -451,6 +453,11 @@
|
||||||
(mf/deps team)
|
(mf/deps team)
|
||||||
(st/emitf (rt/nav :dashboard-projects {:team-id (:id team)})))
|
(st/emitf (rt/nav :dashboard-projects {:team-id (:id team)})))
|
||||||
|
|
||||||
|
go-fonts
|
||||||
|
(mf/use-callback
|
||||||
|
(mf/deps team)
|
||||||
|
(st/emitf (rt/nav :dashboard-fonts {:team-id (:id team)})))
|
||||||
|
|
||||||
go-drafts
|
go-drafts
|
||||||
(mf/use-callback
|
(mf/use-callback
|
||||||
(mf/deps team default-project-id)
|
(mf/deps team default-project-id)
|
||||||
|
@ -469,29 +476,36 @@
|
||||||
(filter :is-pinned))]
|
(filter :is-pinned))]
|
||||||
|
|
||||||
[:div.sidebar-content
|
[:div.sidebar-content
|
||||||
[:& sidebar-team-switch {:team team :profile profile :locale locale}]
|
[:& sidebar-team-switch {:team team :profile profile}]
|
||||||
[:hr]
|
[:hr]
|
||||||
[:& sidebar-search {:search-term search-term
|
[:& sidebar-search {:search-term search-term
|
||||||
:team-id (:id team)
|
:team-id (:id team)}]
|
||||||
:locale locale}]
|
|
||||||
[:div.sidebar-content-section
|
[:div.sidebar-content-section
|
||||||
[:ul.sidebar-nav.no-overflow
|
[:ul.sidebar-nav.no-overflow
|
||||||
[:li.recent-projects
|
[:li.recent-projects
|
||||||
{:on-click go-projects
|
{:on-click go-projects
|
||||||
:class-name (when projects? "current")}
|
:class-name (when projects? "current")}
|
||||||
[:span.element-title (t locale "labels.projects")]]
|
[:span.element-title (tr "labels.projects")]]
|
||||||
|
|
||||||
[:li {:on-click go-drafts
|
[:li {:on-click go-drafts
|
||||||
:class-name (when drafts? "current")}
|
:class-name (when drafts? "current")}
|
||||||
[:span.element-title (t locale "labels.drafts")]]
|
[:span.element-title (tr "labels.drafts")]]
|
||||||
|
|
||||||
|
|
||||||
[:li {:on-click go-libs
|
[:li {:on-click go-libs
|
||||||
:class-name (when libs? "current")}
|
:class-name (when libs? "current")}
|
||||||
[:span.element-title (t locale "labels.shared-libraries")]]]]
|
[:span.element-title (tr "labels.shared-libraries")]]]]
|
||||||
|
|
||||||
[:hr]
|
[:hr]
|
||||||
|
|
||||||
|
[:div.sidebar-content-section
|
||||||
|
[:ul.sidebar-nav.no-overflow
|
||||||
|
[:li.recent-projects
|
||||||
|
{:on-click go-fonts
|
||||||
|
:class-name (when fonts? "current")}
|
||||||
|
[:span.element-title (tr "labels.fonts")]]]]
|
||||||
|
|
||||||
|
[:hr]
|
||||||
[:div.sidebar-content-section
|
[:div.sidebar-content-section
|
||||||
(if (seq pinned-projects)
|
(if (seq pinned-projects)
|
||||||
[:ul.sidebar-nav
|
[:ul.sidebar-nav
|
||||||
|
@ -504,11 +518,11 @@
|
||||||
:selected? (= (:id item) (:id project))}])]
|
:selected? (= (:id item) (:id project))}])]
|
||||||
[:div.sidebar-empty-placeholder
|
[:div.sidebar-empty-placeholder
|
||||||
[:span.icon i/pin]
|
[:span.icon i/pin]
|
||||||
[:span.text (t locale "dashboard.no-projects-placeholder")]])]]))
|
[:span.text (tr "dashboard.no-projects-placeholder")]])]]))
|
||||||
|
|
||||||
|
|
||||||
(mf/defc profile-section
|
(mf/defc profile-section
|
||||||
[{:keys [profile locale team] :as props}]
|
[{:keys [profile team] :as props}]
|
||||||
(let [show (mf/use-state false)
|
(let [show (mf/use-state false)
|
||||||
photo (cfg/resolve-profile-photo-url profile)
|
photo (cfg/resolve-profile-photo-url profile)
|
||||||
|
|
||||||
|
@ -530,18 +544,18 @@
|
||||||
[:ul.dropdown
|
[:ul.dropdown
|
||||||
[:li {:on-click (partial on-click :settings-profile)}
|
[:li {:on-click (partial on-click :settings-profile)}
|
||||||
[:span.icon i/user]
|
[:span.icon i/user]
|
||||||
[:span.text (t locale "labels.profile")]]
|
[:span.text (tr "labels.profile")]]
|
||||||
[:li {:on-click (partial on-click :settings-password)}
|
[:li {:on-click (partial on-click :settings-password)}
|
||||||
[:span.icon i/lock]
|
[:span.icon i/lock]
|
||||||
[:span.text (t locale "labels.password")]]
|
[:span.text (tr "labels.password")]]
|
||||||
[:li {:on-click (partial on-click (da/logout))}
|
[:li {:on-click (partial on-click (da/logout))}
|
||||||
[:span.icon i/exit]
|
[:span.icon i/exit]
|
||||||
[:span.text (t locale "labels.logout")]]
|
[:span.text (tr "labels.logout")]]
|
||||||
|
|
||||||
(when cfg/feedback-enabled
|
(when cfg/feedback-enabled
|
||||||
[:li.feedback {:on-click (partial on-click :settings-feedback)}
|
[:li.feedback {:on-click (partial on-click :settings-feedback)}
|
||||||
[:span.icon i/msg-info]
|
[:span.icon i/msg-info]
|
||||||
[:span.text (t locale "labels.give-feedback")]
|
[:span.text (tr "labels.give-feedback")]
|
||||||
[:span.primary-badge "ALPHA"]])]]]
|
[:span.primary-badge "ALPHA"]])]]]
|
||||||
|
|
||||||
(when (and team profile)
|
(when (and team profile)
|
||||||
|
@ -552,15 +566,11 @@
|
||||||
{::mf/wrap-props false
|
{::mf/wrap-props false
|
||||||
::mf/wrap [mf/memo]}
|
::mf/wrap [mf/memo]}
|
||||||
[props]
|
[props]
|
||||||
(let [locale (mf/deref i18n/locale)
|
(let [team (obj/get props "team")
|
||||||
team (obj/get props "team")
|
profile (obj/get props "profile")]
|
||||||
profile (obj/get props "profile")
|
|
||||||
props (-> (obj/clone props)
|
|
||||||
(obj/set! "locale" locale))]
|
|
||||||
[:div.dashboard-sidebar
|
[:div.dashboard-sidebar
|
||||||
[:div.sidebar-inside
|
[:div.sidebar-inside
|
||||||
[:> sidebar-content props]
|
[:> sidebar-content props]
|
||||||
[:& profile-section
|
[:& profile-section
|
||||||
{:profile profile
|
{:profile profile
|
||||||
:team team
|
:team team}]]]))
|
||||||
:locale locale}]]]))
|
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
[:*
|
[:*
|
||||||
i/image
|
i/image
|
||||||
[:& file-uploader {:input-id "image-upload"
|
[:& file-uploader {:input-id "image-upload"
|
||||||
:accept cm/str-media-types
|
:accept cm/str-image-types
|
||||||
:multi true
|
:multi true
|
||||||
:input-ref ref
|
:input-ref ref
|
||||||
:on-selected on-files-selected}]]]))
|
:on-selected on-files-selected}]]]))
|
||||||
|
|
|
@ -426,7 +426,7 @@
|
||||||
(when local?
|
(when local?
|
||||||
[:div.assets-button {:on-click add-graphic}
|
[:div.assets-button {:on-click add-graphic}
|
||||||
i/plus
|
i/plus
|
||||||
[:& file-uploader {:accept cm/str-media-types
|
[:& file-uploader {:accept cm/str-image-types
|
||||||
:multi true
|
:multi true
|
||||||
:input-ref input-ref
|
:input-ref input-ref
|
||||||
:on-selected on-file-selected}]])]
|
:on-selected on-file-selected}]])]
|
||||||
|
|
|
@ -116,7 +116,6 @@
|
||||||
snap-lines (->> (into (process-snap-lines @state :x)
|
snap-lines (->> (into (process-snap-lines @state :x)
|
||||||
(process-snap-lines @state :y))
|
(process-snap-lines @state :y))
|
||||||
(into #{}))]
|
(into #{}))]
|
||||||
|
|
||||||
(mf/use-effect
|
(mf/use-effect
|
||||||
(fn []
|
(fn []
|
||||||
(let [sub (->> subject
|
(let [sub (->> subject
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.util.globals :as globals]
|
[app.util.globals :as globals]
|
||||||
[app.util.storage :refer [storage]]
|
[app.util.storage :refer [storage]]
|
||||||
|
[app.util.object :as obj]
|
||||||
[app.util.transit :as t]
|
[app.util.transit :as t]
|
||||||
[beicon.core :as rx]
|
[beicon.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
@ -136,6 +137,13 @@
|
||||||
([code] (t @locale code))
|
([code] (t @locale code))
|
||||||
([code & args] (apply t @locale code args)))
|
([code & args] (apply t @locale code args)))
|
||||||
|
|
||||||
|
(mf/defc tr-html
|
||||||
|
{::mf/wrap-props false}
|
||||||
|
[props]
|
||||||
|
(let [label (obj/get props "label")
|
||||||
|
tag-name (obj/get props "tag-name" "p")]
|
||||||
|
[:> tag-name {:dangerouslySetInnerHTML #js {:__html (tr label)}}]))
|
||||||
|
|
||||||
;; DEPRECATED
|
;; DEPRECATED
|
||||||
(defn use-locale
|
(defn use-locale
|
||||||
[]
|
[]
|
||||||
|
|
|
@ -29,6 +29,10 @@
|
||||||
[file]
|
[file]
|
||||||
(file-reader #(.readAsText %1 file)))
|
(file-reader #(.readAsText %1 file)))
|
||||||
|
|
||||||
|
(defn read-file-as-array-buffer
|
||||||
|
[file]
|
||||||
|
(file-reader #(.readAsArrayBuffer %1 file)))
|
||||||
|
|
||||||
(defn read-file-as-data-url
|
(defn read-file-as-data-url
|
||||||
[file]
|
[file]
|
||||||
(file-reader #(.readAsDataURL ^js %1 file)))
|
(file-reader #(.readAsDataURL ^js %1 file)))
|
||||||
|
|
|
@ -1,25 +1,68 @@
|
||||||
# SOME DESCRIPTIVE TITLE.
|
|
||||||
# Copyright (C) YEAR Free Software Foundation, Inc.
|
|
||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
||||||
#
|
|
||||||
#, fuzzy
|
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"PO-Revision-Date: 2021-04-14 13:44+0000\n"
|
||||||
"PO-Revision-Date: 2021-04-22 13:43+0200\n"
|
"Last-Translator: Andrey Antukh <niwi@niwi.nz>\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Language-Team: Spanish "
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"<https://hosted.weblate.org/projects/penpot/frontend/en/>\n"
|
||||||
|
"Language: es\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=iso-8859-1\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
|
"X-Generator: Weblate 4.6-dev\n"
|
||||||
|
|
||||||
|
#: src/app/main/ui/dashboard/fonts.cljs
|
||||||
|
msgid "title.dashboard.fonts"
|
||||||
|
msgstr "Fonts - %s - Penpot"
|
||||||
|
|
||||||
|
#: src/app/main/ui/dashboard/fonts.cljs
|
||||||
|
msgid "title.dashboard.font-providers"
|
||||||
|
msgstr "Font Providers - %s - Penpot"
|
||||||
|
|
||||||
|
msgid "labels.upload"
|
||||||
|
msgstr "Upload"
|
||||||
|
|
||||||
|
msgid "labels.uploading"
|
||||||
|
msgstr "Uploading..."
|
||||||
|
|
||||||
|
msgid "modals.delete-font.title"
|
||||||
|
msgstr "Deleting font"
|
||||||
|
|
||||||
|
msgid "modals.delete-font.message"
|
||||||
|
msgstr "Are you sure you want to delete this font? It will not load if is used in a file."
|
||||||
|
|
||||||
|
msgid "labels.fonts"
|
||||||
|
msgstr "Fonts"
|
||||||
|
|
||||||
|
msgid "labels.installed-fonts"
|
||||||
|
msgstr "Installed fonts"
|
||||||
|
|
||||||
|
msgid "labels.font-family"
|
||||||
|
msgstr "Font Family"
|
||||||
|
|
||||||
|
msgid "labels.font-variant"
|
||||||
|
msgstr "Style"
|
||||||
|
|
||||||
|
msgid "labels.custom-fonts"
|
||||||
|
msgstr "Custom fonts"
|
||||||
|
|
||||||
|
msgid "labels.search-font"
|
||||||
|
msgstr "Search font"
|
||||||
|
|
||||||
|
msgid "labels.font-providers"
|
||||||
|
msgstr "Font providers"
|
||||||
|
|
||||||
|
msgid "labels.upload-custom-fonts"
|
||||||
|
msgstr "Upload custom fonts"
|
||||||
|
|
||||||
|
#, markdown
|
||||||
|
msgid "dashboard.fonts.hero-text1"
|
||||||
|
msgstr "Any web font you upload here will be added to the font family list available at the text properties of the files of this team. Fonts with the same font family name will be grouped as a **single font family**. You can upload fonts with the following formats: **TTF, OTF and WOFF** (only one will be needed)."
|
||||||
|
|
||||||
|
#, markdown
|
||||||
|
msgid "dashboard.fonts.hero-text2"
|
||||||
|
msgstr "You should only upload fonts you own or have license to use in Penpot. Find out more in the Content rights section of [Penpot's Terms of Service](https://penpot.app/terms.html). You also might want to read about [font licensing](2)."
|
||||||
|
|
||||||
# ~ msgid ""
|
|
||||||
# ~ msgstr ""
|
|
||||||
# ~ "Language: en\n"
|
|
||||||
# ~ "MIME-Version: 1.0\n"
|
|
||||||
# ~ "Content-Type: text/plain; charset=utf-8\n"
|
|
||||||
# ~ "Content-Transfer-Encoding: 8bit\n"
|
|
||||||
# ~ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.already-have-account"
|
msgid "auth.already-have-account"
|
||||||
msgstr "Already have an account?"
|
msgstr "Already have an account?"
|
||||||
|
|
|
@ -11,6 +11,68 @@ msgstr ""
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||||
"X-Generator: Weblate 4.6-dev\n"
|
"X-Generator: Weblate 4.6-dev\n"
|
||||||
|
|
||||||
|
#: src/app/main/ui/dashboard/fonts.cljs
|
||||||
|
msgid "title.dashboard.fonts"
|
||||||
|
msgstr "Fuentes - %s - Penpot"
|
||||||
|
|
||||||
|
#: src/app/main/ui/dashboard/fonts.cljs
|
||||||
|
msgid "title.dashboard.font-providers"
|
||||||
|
msgstr "Proveedores de fuentes - %s - Penpot"
|
||||||
|
|
||||||
|
msgid "labels.upload"
|
||||||
|
msgstr "Subir"
|
||||||
|
|
||||||
|
msgid "labels.uploading"
|
||||||
|
msgstr "Subiendo..."
|
||||||
|
|
||||||
|
msgid "modals.delete-font.title"
|
||||||
|
msgstr "Eliminando fuente"
|
||||||
|
|
||||||
|
msgid "modals.delete-font.message"
|
||||||
|
msgstr "Are you sure you want to delete this font? It will not load if is used in a file."
|
||||||
|
msgstr "¿Estas seguro que quieres eliminar esta fuente? La fuente dejara de cargar si es usada en algun fichero."
|
||||||
|
|
||||||
|
msgid "labels.fonts"
|
||||||
|
msgstr "Fuentes"
|
||||||
|
|
||||||
|
msgid "labels.installed-fonts"
|
||||||
|
msgstr "Fuentes instaladas"
|
||||||
|
|
||||||
|
msgid "labels.font-family"
|
||||||
|
msgstr "Familia de fuente"
|
||||||
|
|
||||||
|
msgid "labels.font-variant"
|
||||||
|
msgstr "Estilo"
|
||||||
|
|
||||||
|
msgid "labels.custom-fonts"
|
||||||
|
msgstr "Fuentes personalizadas"
|
||||||
|
|
||||||
|
msgid "labels.search-font"
|
||||||
|
msgstr "Buscar fuente"
|
||||||
|
|
||||||
|
msgid "labels.font-providers"
|
||||||
|
msgstr "Proveedores de fuentes"
|
||||||
|
|
||||||
|
msgid "labels.upload-custom-fonts"
|
||||||
|
msgstr "Subir fuente"
|
||||||
|
|
||||||
|
#, markdown
|
||||||
|
msgid "dashboard.fonts.hero-text1"
|
||||||
|
msgstr "Any web font you upload here will be added to the font family list available at the text properties of the files of this team. Fonts with the same font family name will be grouped as a **single font family**. You can upload fonts with the following formats: **TTF, OTF and WOFF** (only one will be needed)."
|
||||||
|
|
||||||
|
#, markdown
|
||||||
|
msgid "dashboard.fonts.hero-text2"
|
||||||
|
msgstr "You should only upload fonts you own or have license to use in Penpot. Find out more in the Content rights section of [Penpot's Terms of Service](https://penpot.app/terms.html). You also might want to read about [font licensing](2)."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#: src/app/main/ui/auth/register.cljs
|
#: src/app/main/ui/auth/register.cljs
|
||||||
msgid "auth.already-have-account"
|
msgid "auth.already-have-account"
|
||||||
msgstr "¿Tienes ya una cuenta?"
|
msgstr "¿Tienes ya una cuenta?"
|
||||||
|
|
|
@ -3120,6 +3120,11 @@ map-visit@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
object-visit "^1.0.0"
|
object-visit "^1.0.0"
|
||||||
|
|
||||||
|
marked@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/marked/-/marked-2.0.3.tgz#3551c4958c4da36897bda2a16812ef1399c8d6b0"
|
||||||
|
integrity sha512-5otztIIcJfPc2qGTN8cVtOJEjNJZ0jwa46INMagrYfk0EvqtRuEHLsEe0LrFS0/q+ZRKT0+kXK7P2T1AN5lWRA==
|
||||||
|
|
||||||
matchdep@^2.0.0:
|
matchdep@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"
|
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"
|
||||||
|
@ -3644,6 +3649,14 @@ one-time@^1.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fn.name "1.x.x"
|
fn.name "1.x.x"
|
||||||
|
|
||||||
|
opentype.js@^1.3.3:
|
||||||
|
version "1.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-1.3.3.tgz#65b8645b090a1ad444065b784d442fa19d1061f6"
|
||||||
|
integrity sha512-/qIY/+WnKGlPIIPhbeNjynfD2PO15G9lA/xqlX2bDH+4lc3Xz5GCQ68mqxj3DdUv6AJqCeaPvuAoH8mVL0zcuA==
|
||||||
|
dependencies:
|
||||||
|
string.prototype.codepointat "^0.2.1"
|
||||||
|
tiny-inflate "^1.0.3"
|
||||||
|
|
||||||
ordered-read-streams@^1.0.0:
|
ordered-read-streams@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e"
|
resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e"
|
||||||
|
@ -4364,9 +4377,9 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
||||||
inherits "^2.0.1"
|
inherits "^2.0.1"
|
||||||
|
|
||||||
rxjs@~7.0.0-beta.12:
|
rxjs@~7.0.0-beta.12:
|
||||||
version "7.0.0-rc.1"
|
version "7.0.0-rc.2"
|
||||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-rc.1.tgz#11f368e740e2b3cfe805891be127d07391673654"
|
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-rc.2.tgz#bd5b18ff9b60ca28ea4b3a824419035007064fdf"
|
||||||
integrity sha512-FVFOeT+eGdbcPe+uH+cWnEElrU4LiDMrlstNSUpI3MPErICLtVoUCbKrF+n+8DYemHDe7wPqYtuNEYTM3ur3xw==
|
integrity sha512-81+TFxK8hUK3tmJ9TPon07bgun2ASgZ8OXumUuWSAnktSAzTvubw4NCJTr0Tc0lO9IfTThi5z3GDVlmjY3n5ug==
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib "~2.1.0"
|
tslib "~2.1.0"
|
||||||
|
|
||||||
|
@ -4480,7 +4493,7 @@ shadow-cljs-jar@1.3.2:
|
||||||
resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
|
resolved "https://registry.yarnpkg.com/shadow-cljs-jar/-/shadow-cljs-jar-1.3.2.tgz#97273afe1747b6a2311917c1c88d9e243c81957b"
|
||||||
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
|
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
|
||||||
|
|
||||||
shadow-cljs@^2.11.20:
|
shadow-cljs@2.12.5:
|
||||||
version "2.12.5"
|
version "2.12.5"
|
||||||
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4"
|
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4"
|
||||||
integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ==
|
integrity sha512-o3xo3coRgnlkI/iI55ccHjj6AU3F1+ovk3hhK86e3P2JGGOpNTAwsGNxUpMC5JAwS9Nz0v6sSk73hWjEOnm6fQ==
|
||||||
|
@ -4796,6 +4809,11 @@ string-width@^3.0.0, string-width@^3.1.0:
|
||||||
is-fullwidth-code-point "^2.0.0"
|
is-fullwidth-code-point "^2.0.0"
|
||||||
strip-ansi "^5.1.0"
|
strip-ansi "^5.1.0"
|
||||||
|
|
||||||
|
string.prototype.codepointat@^0.2.1:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz#004ad44c8afc727527b108cd462b4d971cd469bc"
|
||||||
|
integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==
|
||||||
|
|
||||||
string.prototype.trimend@^1.0.4:
|
string.prototype.trimend@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
|
resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80"
|
||||||
|
@ -5021,6 +5039,11 @@ timers-ext@^0.1.7:
|
||||||
es5-ext "~0.10.46"
|
es5-ext "~0.10.46"
|
||||||
next-tick "1"
|
next-tick "1"
|
||||||
|
|
||||||
|
tiny-inflate@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz#122715494913a1805166aaf7c93467933eea26c4"
|
||||||
|
integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
|
||||||
|
|
||||||
to-absolute-glob@^2.0.0:
|
to-absolute-glob@^2.0.0:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b"
|
resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue