mirror of
https://github.com/penpot/penpot.git
synced 2025-03-20 19:51:23 -05: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/.shadow-cljs
|
||||
/frontend/resources/public/*
|
||||
/frontend/resources/fonts/experiments
|
||||
/exporter/target
|
||||
/exporter/.shadow-cljs
|
||||
/docker/images/bundle*
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
org.postgresql/postgresql {:mvn/version "42.2.19"}
|
||||
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/cuerdas {:mvn/version "2020.03.26-3"}
|
||||
|
||||
|
|
|
@ -122,10 +122,15 @@
|
|||
:app.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.
|
||||
:app.rlimits/all
|
||||
{: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
|
||||
{:pool (ig/ref :app.db/pool)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.media
|
||||
"Media postprocessing."
|
||||
"Media & Font postprocessing."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.exceptions :as ex]
|
||||
|
@ -13,20 +13,29 @@
|
|||
[app.common.spec :as us]
|
||||
[app.rlimits :as rlm]
|
||||
[app.rpc.queries.svg :as svg]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.java.shell :as sh]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]
|
||||
[datoteka.core :as fs])
|
||||
(:import
|
||||
java.io.ByteArrayInputStream
|
||||
java.io.OutputStream
|
||||
org.apache.commons.io.IOUtils
|
||||
org.im4java.core.ConvertCmd
|
||||
org.im4java.core.IMOperation
|
||||
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/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 ::upload
|
||||
|
@ -35,8 +44,44 @@
|
|||
:internal.http.upload/tempfile
|
||||
: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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(s/def ::cmd keyword?)
|
||||
|
||||
|
@ -77,8 +122,6 @@
|
|||
:size (alength ^bytes thumbnail-data)
|
||||
:data (ByteArrayInputStream. thumbnail-data)))))
|
||||
|
||||
(defmulti process :cmd)
|
||||
|
||||
(defmethod process :generic-thumbnail
|
||||
[{:keys [quality width height] :as params}]
|
||||
(us/assert ::thumbnail-params params)
|
||||
|
@ -161,33 +204,63 @@
|
|||
:height (.getPageHeight instance)
|
||||
:mtype mtype}))))
|
||||
|
||||
(defmethod process :default
|
||||
[{:keys [cmd] :as params}]
|
||||
(ex/raise :type :internal
|
||||
:code :not-implemented
|
||||
:hint (str "No impl found for process cmd:" cmd)))
|
||||
(defmethod process-error org.im4java.core.InfoException
|
||||
[error]
|
||||
(ex/raise :type :validation
|
||||
:code :invalid-image
|
||||
:cause error))
|
||||
|
||||
(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
|
||||
:code :invalid-image
|
||||
:cause e)))))
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; --- Fonts Generation
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
;; --- Utility functions
|
||||
(def all-fotmats #{"font/woff2", "font/woff", "font/otf", "font/ttf"})
|
||||
|
||||
(defn validate-media-type
|
||||
([mtype] (validate-media-type mtype cm/valid-media-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"))))
|
||||
(defmethod process :generate-fonts
|
||||
[{:keys [input] :as params}]
|
||||
(letfn [(ttf->otf [data]
|
||||
(let [input-file (fs/create-tempfile :prefix "penpot")
|
||||
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"
|
||||
: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 ::image ::instance)
|
||||
(derive ::font ::instance)
|
||||
|
||||
(defmethod ig/pre-init-spec ::instance [_]
|
||||
(s/spec int?))
|
||||
|
|
|
@ -120,6 +120,7 @@
|
|||
'app.rpc.queries.profile
|
||||
'app.rpc.queries.recent-files
|
||||
'app.rpc.queries.viewer
|
||||
'app.rpc.queries.fonts
|
||||
'app.rpc.queries.svg)
|
||||
(map (partial process-method cfg))
|
||||
(into {}))))
|
||||
|
@ -143,6 +144,7 @@
|
|||
'app.rpc.mutations.teams
|
||||
'app.rpc.mutations.management
|
||||
'app.rpc.mutations.ldap
|
||||
'app.rpc.mutations.fonts
|
||||
'app.rpc.mutations.verify-token)
|
||||
(map (partial process-method cfg))
|
||||
(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 ::team-id ::us/uuid)
|
||||
|
||||
|
||||
;; --- Create File Media object (upload)
|
||||
|
||||
(declare create-file-media-object)
|
||||
(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 ::upload-file-media-object
|
||||
|
|
|
@ -401,7 +401,9 @@
|
|||
|
||||
(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/keys :req-un [::profile-id ::file]))
|
||||
|
||||
|
|
|
@ -249,7 +249,9 @@
|
|||
|
||||
(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/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})))
|
||||
|
|
@ -145,8 +145,8 @@
|
|||
(make-output-stream [_ opts]
|
||||
(throw (UnsupportedOperationException. "not implemented")))
|
||||
|
||||
clojure.lang.Counted
|
||||
(count [_] size)))
|
||||
clojure.lang.Counted
|
||||
(count [_] size)))
|
||||
|
||||
(defn content
|
||||
([data] (content data nil))
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[app.common.data :as d]
|
||||
[app.common.spec :as us]
|
||||
[app.db :as db]
|
||||
[app.storage :as sto]
|
||||
[app.util.logging :as l]
|
||||
[clojure.spec.alpha :as s]
|
||||
[integrant.core :as ig]))
|
||||
|
@ -24,7 +25,8 @@
|
|||
(fn [{:keys [props] :as task}]
|
||||
(us/verify ::props props)
|
||||
(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 ::id ::us/uuid)
|
||||
|
@ -34,21 +36,32 @@
|
|||
(fn [_ props] (:type props)))
|
||||
|
||||
(defmethod handle-deletion :default
|
||||
[_conn {:keys [type]}]
|
||||
[_cfg {:keys [type]}]
|
||||
(l/warn :hint "no handler found"
|
||||
:type (d/name type)))
|
||||
|
||||
(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"]
|
||||
(db/exec-one! conn [sql id])))
|
||||
|
||||
(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"]
|
||||
(db/exec-one! conn [sql id])))
|
||||
|
||||
(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"]
|
||||
(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)
|
||||
:thumbnail-id (:thumbnail-id mobj))
|
||||
;; 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)}))
|
||||
|
||||
nil))
|
||||
|
|
|
@ -261,6 +261,19 @@
|
|||
(recur (reduce-kv assoc! res (first 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
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
[clojure.spec.alpha :as s]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(def valid-media-types
|
||||
#{"image/jpeg", "image/png", "image/webp", "image/gif", "image/svg+xml"})
|
||||
|
||||
(def str-media-types (str/join "," valid-media-types))
|
||||
(def valid-font-types #{"font/ttf" "font/woff", "font/otf"})
|
||||
(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-font-types (str/join "," valid-font-types))
|
||||
|
||||
(defn format->extension
|
||||
[format]
|
||||
|
@ -65,3 +65,38 @@
|
|||
::modified-at
|
||||
::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
|
||||
"Data manipulation and query helper functions."
|
||||
(:refer-clojure :exclude [assert])
|
||||
(:refer-clojure :exclude [assert bytes?])
|
||||
#?(:cljs (:require-macros [app.common.spec :refer [assert]]))
|
||||
(:require
|
||||
#?(:clj [clojure.spec.alpha :as s]
|
||||
|
@ -108,6 +108,20 @@
|
|||
(s/def ::point gpt/point?)
|
||||
(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
|
||||
#(and
|
||||
(int? %)
|
||||
|
|
|
@ -17,6 +17,7 @@ const mkdirp = require("mkdirp");
|
|||
const rimraf = require("rimraf");
|
||||
const sass = require("sass");
|
||||
const gettext = require("gettext-parser");
|
||||
const marked = require("marked");
|
||||
|
||||
const mapStream = require("map-stream");
|
||||
const paths = {};
|
||||
|
@ -45,17 +46,35 @@ function readLocales() {
|
|||
|
||||
for (let key of Object.keys(trdata)) {
|
||||
if (key === "") continue;
|
||||
const comments = trdata[key].comments || {};
|
||||
|
||||
if (l.isNil(result[key])) {
|
||||
result[key] = {};
|
||||
}
|
||||
|
||||
const msgstr = trdata[key].msgstr;
|
||||
if (msgstr.length === 1) {
|
||||
result[key][lang] = msgstr[0];
|
||||
const isMarkdown = l.includes(comments.flag, "markdown");
|
||||
|
||||
const msgs = trdata[key].msgstr;
|
||||
if (msgs.length === 1) {
|
||||
let message = msgs[0];
|
||||
if (isMarkdown) {
|
||||
message = marked.parseInline(message);
|
||||
}
|
||||
|
||||
result[key][lang] = message;
|
||||
} else {
|
||||
result[key][lang] = msgstr;
|
||||
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-svg-sprite": "^1.5.0",
|
||||
"map-stream": "0.0.7",
|
||||
"marked": "^2.0.3",
|
||||
"mkdirp": "^1.0.4",
|
||||
"postcss": "^8.2.7",
|
||||
"postcss-clean": "^1.2.2",
|
||||
"rimraf": "^3.0.0",
|
||||
"sass": "^1.32.8",
|
||||
"shadow-cljs": "^2.11.20"
|
||||
"shadow-cljs": "2.12.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "^2.21.1",
|
||||
|
@ -41,6 +42,7 @@
|
|||
"js-beautify": "^1.13.5",
|
||||
"luxon": "^1.26.0",
|
||||
"mousetrap": "^1.6.5",
|
||||
"opentype.js": "^1.3.3",
|
||||
"randomcolor": "^0.6.2",
|
||||
"react": "~17.0.1",
|
||||
"react-dom": "~17.0.1",
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
@import "main/partials/dashboard-sidebar";
|
||||
@import "main/partials/dashboard-team";
|
||||
@import "main/partials/dashboard-settings";
|
||||
@import "main/partials/dashboard-fonts";
|
||||
@import "main/partials/debug-icons-preview";
|
||||
@import "main/partials/editable-label";
|
||||
@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})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
;; --- Fetch Projects
|
||||
|
||||
(defn fetch-projects
|
||||
[{:keys [team-id] :as params}]
|
||||
(us/assert ::us/uuid team-id)
|
||||
|
@ -123,8 +121,6 @@
|
|||
(ptk/watch (fetch-projects {: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/search-term (s/nilable ::us/string))
|
||||
|
||||
|
@ -149,8 +145,6 @@
|
|||
(->> (rp/query :search-files params)
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
;; --- Fetch Files
|
||||
|
||||
(defn fetch-files
|
||||
[{:keys [project-id] :as params}]
|
||||
(us/assert ::us/uuid project-id)
|
||||
|
@ -162,8 +156,6 @@
|
|||
(->> (rp/query :files params)
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
;; --- Fetch Shared Files
|
||||
|
||||
(defn fetch-shared-files
|
||||
[{:keys [team-id] :as params}]
|
||||
(us/assert ::us/uuid team-id)
|
||||
|
@ -175,8 +167,6 @@
|
|||
(->> (rp/query :shared-files {:team-id team-id})
|
||||
(rx/map #(partial fetched %)))))))
|
||||
|
||||
;; --- Fetch recent files
|
||||
|
||||
(declare recent-files-fetched)
|
||||
|
||||
(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
|
||||
:code :media-too-large
|
||||
: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
|
||||
:code :media-type-not-allowed
|
||||
:hint (str/fmt "media type %s is not supported" (.-type file))))
|
||||
|
|
|
@ -43,6 +43,9 @@
|
|||
(def dashboard-local
|
||||
(l/derived :dashboard-local st/state))
|
||||
|
||||
(def dashboard-fonts
|
||||
(l/derived :dashboard-fonts st/state))
|
||||
|
||||
(def dashboard-selected-project
|
||||
(l/derived (fn [state]
|
||||
(get-in state [:dashboard-local :selected-project]))
|
||||
|
|
|
@ -284,7 +284,7 @@
|
|||
([matches other]
|
||||
(let [merge-coord
|
||||
(fn [matches other]
|
||||
|
||||
|
||||
(let [matches (into {} matches)
|
||||
other (into {} other)
|
||||
keys (set/union (keys matches) (keys other))]
|
||||
|
@ -305,7 +305,7 @@
|
|||
(if (< (mth/abs cur-val) (mth/abs other-val))
|
||||
current
|
||||
other))
|
||||
|
||||
|
||||
min-match-coord
|
||||
(fn [matches]
|
||||
(if (and (seq matches) (not (empty? matches)))
|
||||
|
|
|
@ -90,6 +90,8 @@
|
|||
["/settings" :dashboard-team-settings]
|
||||
["/projects" :dashboard-projects]
|
||||
["/search" :dashboard-search]
|
||||
["/fonts" :dashboard-fonts]
|
||||
["/fonts/providers" :dashboard-font-providers]
|
||||
["/libraries" :dashboard-libraries]
|
||||
["/projects/:project-id" :dashboard-files]]
|
||||
|
||||
|
@ -135,12 +137,11 @@
|
|||
:dashboard-projects
|
||||
:dashboard-files
|
||||
:dashboard-libraries
|
||||
:dashboard-fonts
|
||||
:dashboard-font-providers
|
||||
:dashboard-team-members
|
||||
:dashboard-team-settings)
|
||||
[:*
|
||||
#_[:div.modal-wrapper
|
||||
[:& app.main.ui.onboarding/release-notes-modal {:version "1.4"}]]
|
||||
[:& dashboard {:route route}]]
|
||||
[:& dashboard {:route route}]
|
||||
|
||||
:viewer
|
||||
(let [index (get-in route [:query-params :index])
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
[app.main.ui.components.dropdown :refer [dropdown']]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.util.data :refer [classnames]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]))
|
||||
|
||||
|
@ -22,18 +21,18 @@
|
|||
(assert (boolean? (gobj/get props "show")) "missing `show` prop")
|
||||
(assert (vector? (gobj/get props "options")) "missing `options` prop")
|
||||
|
||||
(let [open? (gobj/get props "show")
|
||||
on-close (gobj/get props "on-close")
|
||||
options (gobj/get props "options")
|
||||
(let [open? (gobj/get props "show")
|
||||
on-close (gobj/get props "on-close")
|
||||
options (gobj/get props "options")
|
||||
is-selectable (gobj/get props "selectable")
|
||||
selected (gobj/get props "selected")
|
||||
top (gobj/get props "top" 0)
|
||||
left (gobj/get props "left" 0)
|
||||
fixed? (gobj/get props "fixed?" false)
|
||||
min-width? (gobj/get props "min-width?" false)
|
||||
selected (gobj/get props "selected")
|
||||
top (gobj/get props "top" 0)
|
||||
left (gobj/get props "left" 0)
|
||||
fixed? (gobj/get props "fixed?" false)
|
||||
min-width? (gobj/get props "min-width?" false)
|
||||
|
||||
local (mf/use-state {:offset 0
|
||||
:levels nil})
|
||||
local (mf/use-state {:offset 0
|
||||
:levels nil})
|
||||
|
||||
on-local-close
|
||||
(mf/use-callback
|
||||
|
@ -81,13 +80,13 @@
|
|||
|
||||
(when (and open? (some? (:levels @local)))
|
||||
[:> dropdown' props
|
||||
[:div.context-menu {:class (classnames :is-open open?
|
||||
:fixed fixed?
|
||||
:is-selectable is-selectable)
|
||||
[:div.context-menu {:class (dom/classnames :is-open open?
|
||||
:fixed fixed?
|
||||
:is-selectable is-selectable)
|
||||
:style {:top (+ top (:offset @local))
|
||||
:left left}}
|
||||
(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}
|
||||
(when-let [parent-option (:parent-option level)]
|
||||
[:*
|
||||
|
@ -103,8 +102,7 @@
|
|||
(if (= option-name :separator)
|
||||
[:li.separator]
|
||||
[:li.context-menu-item
|
||||
{:class (classnames :is-selected (and selected
|
||||
(= option-name selected)))
|
||||
{:class (dom/classnames :is-selected (and selected (= option-name selected)))
|
||||
:key option-name}
|
||||
(if-not sub-options
|
||||
[:a.context-menu-action {:on-click option-handler}
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
[app.main.ui.dashboard.files :refer [files-section]]
|
||||
[app.main.ui.dashboard.libraries :refer [libraries-page]]
|
||||
[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.sidebar :refer [sidebar]]
|
||||
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page]]
|
||||
|
@ -65,6 +66,12 @@
|
|||
:dashboard-projects
|
||||
[:& projects-section {:team team :projects projects}]
|
||||
|
||||
:dashboard-fonts
|
||||
[:& fonts-page {:team team}]
|
||||
|
||||
:dashboard-font-providers
|
||||
[:& font-providers-page {:team team}]
|
||||
|
||||
:dashboard-files
|
||||
(when project
|
||||
[:& files-section {:team team :project project}])
|
||||
|
@ -121,17 +128,19 @@
|
|||
[:& (mf/provider ctx/current-page-id) {:value nil}
|
||||
|
||||
[:section.dashboard-layout
|
||||
[:& sidebar {:team team
|
||||
:projects projects
|
||||
:project project
|
||||
:profile profile
|
||||
:section section
|
||||
:search-term search-term}]
|
||||
[:& sidebar
|
||||
{:team team
|
||||
:projects projects
|
||||
:project project
|
||||
:profile profile
|
||||
:section section
|
||||
:search-term search-term}]
|
||||
(when (and team (seq projects))
|
||||
[:& dashboard-content {:projects projects
|
||||
:profile profile
|
||||
:project project
|
||||
:section section
|
||||
:search-term search-term
|
||||
:team team}])]]]]]))
|
||||
[:& dashboard-content
|
||||
{:projects projects
|
||||
:profile profile
|
||||
:project project
|
||||
:section section
|
||||
:search-term search-term
|
||||
:team team}])]]]]]))
|
||||
|
||||
|
|
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.dom :as dom]
|
||||
[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.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
|
@ -47,10 +47,11 @@
|
|||
selected-project (:selected-project dstate)
|
||||
edit-id (:project-for-edit dstate)
|
||||
|
||||
local (mf/use-state {:menu-open false
|
||||
:menu-pos nil
|
||||
:edition? (= (:id item) edit-id)
|
||||
:dragging? false})
|
||||
local (mf/use-state
|
||||
{:menu-open false
|
||||
:menu-pos nil
|
||||
:edition? (= (:id item) edit-id)
|
||||
:dragging? false})
|
||||
|
||||
on-click
|
||||
(mf/use-callback
|
||||
|
@ -60,11 +61,13 @@
|
|||
:project-id (:id item)}))))
|
||||
|
||||
on-menu-click
|
||||
(mf/use-callback (fn [event]
|
||||
(let [position (dom/get-client-position event)]
|
||||
(dom/prevent-default event)
|
||||
(swap! local assoc :menu-open true
|
||||
:menu-pos position))))
|
||||
(mf/use-callback
|
||||
(fn [event]
|
||||
(let [position (dom/get-client-position event)]
|
||||
(dom/prevent-default event)
|
||||
(swap! local assoc
|
||||
:menu-open true
|
||||
:menu-pos position))))
|
||||
|
||||
on-menu-close
|
||||
(mf/use-callback #(swap! local assoc :menu-open false))
|
||||
|
@ -139,7 +142,7 @@
|
|||
:on-menu-close on-menu-close}]]))
|
||||
|
||||
(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 "")
|
||||
focused? (mf/use-state false)
|
||||
emit! (mf/use-memo #(f/debounce st/emit! 500))
|
||||
|
@ -183,7 +186,7 @@
|
|||
{:key :images-search-box
|
||||
:id "search-input"
|
||||
:type "text"
|
||||
:placeholder (t locale "dashboard.search-placeholder")
|
||||
:placeholder (tr "dashboard.search-placeholder")
|
||||
:default-value search-term
|
||||
:auto-complete "off"
|
||||
:on-focus on-search-focus
|
||||
|
@ -201,7 +204,7 @@
|
|||
i/search])]))
|
||||
|
||||
(mf/defc teams-selector-dropdown
|
||||
[{:keys [team profile locale] :as props}]
|
||||
[{:keys [team profile] :as props}]
|
||||
(let [show-dropdown? (mf/use-state false)
|
||||
teams (mf/deref refs/teams)
|
||||
|
||||
|
@ -216,11 +219,11 @@
|
|||
(st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))]
|
||||
|
||||
[:ul.dropdown.teams-dropdown
|
||||
[:li.title (t locale "dashboard.switch-team")]
|
||||
[:li.title (tr "dashboard.switch-team")]
|
||||
[:hr]
|
||||
[:li.team-name {:on-click (partial team-selected (:default-team-id profile))}
|
||||
[: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))]
|
||||
[:* {:key (:id team)}
|
||||
|
@ -231,7 +234,7 @@
|
|||
|
||||
[:hr]
|
||||
[: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 ::leave-modal-form
|
||||
|
@ -292,7 +295,7 @@
|
|||
|
||||
|
||||
(mf/defc team-options-dropdown
|
||||
[{:keys [team locale profile] :as props}]
|
||||
[{:keys [team profile] :as props}]
|
||||
(let [members (mf/use-state [])
|
||||
|
||||
go-members
|
||||
|
@ -341,9 +344,9 @@
|
|||
(mf/deps team)
|
||||
(st/emitf (modal/show
|
||||
{:type :confirm
|
||||
:title (t locale "modals.leave-confirm.title")
|
||||
:message (t locale "modals.leave-confirm.message")
|
||||
:accept-label (t locale "modals.leave-confirm.accept")
|
||||
:title (tr "modals.leave-confirm.title")
|
||||
:message (tr "modals.leave-confirm.message")
|
||||
:accept-label (tr "modals.leave-confirm.accept")
|
||||
:on-accept leave-fn})))
|
||||
|
||||
on-leave-as-owner-clicked
|
||||
|
@ -366,9 +369,9 @@
|
|||
(mf/deps team)
|
||||
(st/emitf (modal/show
|
||||
{:type :confirm
|
||||
:title (t locale "modals.delete-team-confirm.title")
|
||||
:message (t locale "modals.delete-team-confirm.message")
|
||||
:accept-label (t locale "modals.delete-team-confirm.accept")
|
||||
:title (tr "modals.delete-team-confirm.title")
|
||||
:message (tr "modals.delete-team-confirm.message")
|
||||
:accept-label (tr "modals.delete-team-confirm.accept")
|
||||
:on-accept delete-fn})))]
|
||||
|
||||
(mf/use-layout-effect
|
||||
|
@ -378,25 +381,25 @@
|
|||
(rx/subs #(reset! members %)))))
|
||||
|
||||
[:ul.dropdown.options-dropdown
|
||||
[:li {:on-click go-members} (t locale "labels.members")]
|
||||
[:li {:on-click go-settings} (t locale "labels.settings")]
|
||||
[:li {:on-click go-members} (tr "labels.members")]
|
||||
[:li {:on-click go-settings} (tr "labels.settings")]
|
||||
[:hr]
|
||||
[:li {:on-click on-rename-clicked} (t locale "labels.rename")]
|
||||
[:li {:on-click on-rename-clicked} (tr "labels.rename")]
|
||||
|
||||
(cond
|
||||
(: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)
|
||||
[: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)
|
||||
[: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
|
||||
[{:keys [team profile locale] :as props}]
|
||||
[{:keys [team profile] :as props}]
|
||||
(let [show-dropdown? (mf/use-state false)
|
||||
|
||||
show-team-opts-ddwn? (mf/use-state false)
|
||||
|
@ -408,7 +411,7 @@
|
|||
(if (:is-default team)
|
||||
[:div.team-name
|
||||
[: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
|
||||
[:span.team-icon
|
||||
[:img {:src (cfg/resolve-team-photo-url team)}]]
|
||||
|
@ -425,23 +428,22 @@
|
|||
[:& dropdown {:show @show-teams-ddwn?
|
||||
:on-close #(reset! show-teams-ddwn? false)}
|
||||
[:& teams-selector-dropdown {:team team
|
||||
:profile profile
|
||||
:locale locale}]]
|
||||
:profile profile}]]
|
||||
|
||||
[:& dropdown {:show @show-team-opts-ddwn?
|
||||
:on-close #(reset! show-team-opts-ddwn? false)}
|
||||
[:& team-options-dropdown {:team team
|
||||
:profile profile
|
||||
:locale locale}]]]))
|
||||
:profile profile}]]]))
|
||||
|
||||
(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
|
||||
(->> (vals projects)
|
||||
(d/seek :is-default)
|
||||
(:id))
|
||||
|
||||
projects? (= section :dashboard-projects)
|
||||
fonts? (= section :dashboard-fonts)
|
||||
libs? (= section :dashboard-libraries)
|
||||
drafts? (and (= section :dashboard-files)
|
||||
(= (:id project) default-project-id))
|
||||
|
@ -451,6 +453,11 @@
|
|||
(mf/deps 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
|
||||
(mf/use-callback
|
||||
(mf/deps team default-project-id)
|
||||
|
@ -469,29 +476,36 @@
|
|||
(filter :is-pinned))]
|
||||
|
||||
[:div.sidebar-content
|
||||
[:& sidebar-team-switch {:team team :profile profile :locale locale}]
|
||||
[:& sidebar-team-switch {:team team :profile profile}]
|
||||
[:hr]
|
||||
[:& sidebar-search {:search-term search-term
|
||||
:team-id (:id team)
|
||||
:locale locale}]
|
||||
:team-id (:id team)}]
|
||||
[:div.sidebar-content-section
|
||||
[:ul.sidebar-nav.no-overflow
|
||||
[:li.recent-projects
|
||||
{:on-click go-projects
|
||||
:class-name (when projects? "current")}
|
||||
[:span.element-title (t locale "labels.projects")]]
|
||||
[:span.element-title (tr "labels.projects")]]
|
||||
|
||||
[:li {:on-click go-drafts
|
||||
:class-name (when drafts? "current")}
|
||||
[:span.element-title (t locale "labels.drafts")]]
|
||||
[:span.element-title (tr "labels.drafts")]]
|
||||
|
||||
|
||||
[:li {:on-click go-libs
|
||||
:class-name (when libs? "current")}
|
||||
[:span.element-title (t locale "labels.shared-libraries")]]]]
|
||||
[:span.element-title (tr "labels.shared-libraries")]]]]
|
||||
|
||||
[: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
|
||||
(if (seq pinned-projects)
|
||||
[:ul.sidebar-nav
|
||||
|
@ -504,11 +518,11 @@
|
|||
:selected? (= (:id item) (:id project))}])]
|
||||
[:div.sidebar-empty-placeholder
|
||||
[:span.icon i/pin]
|
||||
[:span.text (t locale "dashboard.no-projects-placeholder")]])]]))
|
||||
[:span.text (tr "dashboard.no-projects-placeholder")]])]]))
|
||||
|
||||
|
||||
(mf/defc profile-section
|
||||
[{:keys [profile locale team] :as props}]
|
||||
[{:keys [profile team] :as props}]
|
||||
(let [show (mf/use-state false)
|
||||
photo (cfg/resolve-profile-photo-url profile)
|
||||
|
||||
|
@ -530,18 +544,18 @@
|
|||
[:ul.dropdown
|
||||
[:li {:on-click (partial on-click :settings-profile)}
|
||||
[:span.icon i/user]
|
||||
[:span.text (t locale "labels.profile")]]
|
||||
[:span.text (tr "labels.profile")]]
|
||||
[:li {:on-click (partial on-click :settings-password)}
|
||||
[:span.icon i/lock]
|
||||
[:span.text (t locale "labels.password")]]
|
||||
[:span.text (tr "labels.password")]]
|
||||
[:li {:on-click (partial on-click (da/logout))}
|
||||
[:span.icon i/exit]
|
||||
[:span.text (t locale "labels.logout")]]
|
||||
[:span.text (tr "labels.logout")]]
|
||||
|
||||
(when cfg/feedback-enabled
|
||||
[:li.feedback {:on-click (partial on-click :settings-feedback)}
|
||||
[:span.icon i/msg-info]
|
||||
[:span.text (t locale "labels.give-feedback")]
|
||||
[:span.text (tr "labels.give-feedback")]
|
||||
[:span.primary-badge "ALPHA"]])]]]
|
||||
|
||||
(when (and team profile)
|
||||
|
@ -552,15 +566,11 @@
|
|||
{::mf/wrap-props false
|
||||
::mf/wrap [mf/memo]}
|
||||
[props]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
team (obj/get props "team")
|
||||
profile (obj/get props "profile")
|
||||
props (-> (obj/clone props)
|
||||
(obj/set! "locale" locale))]
|
||||
(let [team (obj/get props "team")
|
||||
profile (obj/get props "profile")]
|
||||
[:div.dashboard-sidebar
|
||||
[:div.sidebar-inside
|
||||
[:> sidebar-content props]
|
||||
[:& profile-section
|
||||
{:profile profile
|
||||
:team team
|
||||
:locale locale}]]]))
|
||||
:team team}]]]))
|
||||
|
|
|
@ -18,33 +18,33 @@
|
|||
(mf/defc banner
|
||||
[{:keys [type position status controls content actions on-close] :as props}]
|
||||
[:div.banner {:class (dom/classnames
|
||||
:warning (= type :warning)
|
||||
:error (= type :error)
|
||||
:success (= type :success)
|
||||
:info (= type :info)
|
||||
:fixed (= position :fixed)
|
||||
:floating (= position :floating)
|
||||
:inline (= position :inline)
|
||||
:hide (= status :hide))}
|
||||
:warning (= type :warning)
|
||||
:error (= type :error)
|
||||
:success (= type :success)
|
||||
:info (= type :info)
|
||||
:fixed (= position :fixed)
|
||||
:floating (= position :floating)
|
||||
:inline (= position :inline)
|
||||
:hide (= status :hide))}
|
||||
[:div.wrapper
|
||||
[:div.icon (case type
|
||||
:warning i/msg-warning
|
||||
:error i/msg-error
|
||||
:success i/msg-success
|
||||
:info i/msg-info
|
||||
i/msg-error)]
|
||||
[:div.content {:class (dom/classnames
|
||||
:inline-actions (= controls :inline-actions)
|
||||
:bottom-actions (= controls :bottom-actions))}
|
||||
content
|
||||
(when (or (= controls :bottom-actions) (= controls :inline-actions))
|
||||
[:div.actions
|
||||
(for [action actions]
|
||||
[:div.btn-secondary.btn-small {:key (uuid/next)
|
||||
:on-click (:callback action)}
|
||||
(:label action)])])]
|
||||
(when (= controls :close)
|
||||
[:div.btn-close {:on-click on-close} i/close])]])
|
||||
[:div.icon (case type
|
||||
:warning i/msg-warning
|
||||
:error i/msg-error
|
||||
:success i/msg-success
|
||||
:info i/msg-info
|
||||
i/msg-error)]
|
||||
[:div.content {:class (dom/classnames
|
||||
:inline-actions (= controls :inline-actions)
|
||||
:bottom-actions (= controls :bottom-actions))}
|
||||
content
|
||||
(when (or (= controls :bottom-actions) (= controls :inline-actions))
|
||||
[:div.actions
|
||||
(for [action actions]
|
||||
[:div.btn-secondary.btn-small {:key (uuid/next)
|
||||
:on-click (:callback action)}
|
||||
(:label action)])])]
|
||||
(when (= controls :close)
|
||||
[:div.btn-close {:on-click on-close} i/close])]])
|
||||
|
||||
(mf/defc notifications
|
||||
[]
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
[:*
|
||||
i/image
|
||||
[:& file-uploader {:input-id "image-upload"
|
||||
:accept cm/str-media-types
|
||||
:accept cm/str-image-types
|
||||
:multi true
|
||||
:input-ref ref
|
||||
:on-selected on-files-selected}]]]))
|
||||
|
|
|
@ -212,7 +212,7 @@
|
|||
(fn [path]
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! state update :folded-groups
|
||||
(swap! state update :folded-groups
|
||||
toggle-folded-group path))))
|
||||
|
||||
on-group
|
||||
|
@ -400,7 +400,7 @@
|
|||
(fn [path]
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! state update :folded-groups
|
||||
(swap! state update :folded-groups
|
||||
toggle-folded-group path))))
|
||||
|
||||
on-group
|
||||
|
@ -426,7 +426,7 @@
|
|||
(when local?
|
||||
[:div.assets-button {:on-click add-graphic}
|
||||
i/plus
|
||||
[:& file-uploader {:accept cm/str-media-types
|
||||
[:& file-uploader {:accept cm/str-image-types
|
||||
:multi true
|
||||
:input-ref input-ref
|
||||
:on-selected on-file-selected}]])]
|
||||
|
|
|
@ -116,7 +116,6 @@
|
|||
snap-lines (->> (into (process-snap-lines @state :x)
|
||||
(process-snap-lines @state :y))
|
||||
(into #{}))]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [sub (->> subject
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
[app.config :as cfg]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.storage :refer [storage]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.transit :as t]
|
||||
[beicon.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
|
@ -136,6 +137,13 @@
|
|||
([code] (t @locale code))
|
||||
([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
|
||||
(defn use-locale
|
||||
[]
|
||||
|
|
|
@ -29,6 +29,10 @@
|
|||
[file]
|
||||
(file-reader #(.readAsText %1 file)))
|
||||
|
||||
(defn read-file-as-array-buffer
|
||||
[file]
|
||||
(file-reader #(.readAsArrayBuffer %1 file)))
|
||||
|
||||
(defn read-file-as-data-url
|
||||
[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 ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"PO-Revision-Date: 2021-04-22 13:43+0200\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"PO-Revision-Date: 2021-04-14 13:44+0000\n"
|
||||
"Last-Translator: Andrey Antukh <niwi@niwi.nz>\n"
|
||||
"Language-Team: Spanish "
|
||||
"<https://hosted.weblate.org/projects/penpot/frontend/en/>\n"
|
||||
"Language: es\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"
|
||||
"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
|
||||
msgid "auth.already-have-account"
|
||||
msgstr "Already have an account?"
|
||||
|
|
|
@ -11,6 +11,68 @@ msgstr ""
|
|||
"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 "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
|
||||
msgid "auth.already-have-account"
|
||||
msgstr "¿Tienes ya una cuenta?"
|
||||
|
|
|
@ -3120,6 +3120,11 @@ map-visit@^1.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/matchdep/-/matchdep-2.0.0.tgz#c6f34834a0d8dbc3b37c27ee8bbcb27c7775582e"
|
||||
|
@ -3644,6 +3649,14 @@ one-time@^1.0.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
|
||||
rxjs@~7.0.0-beta.12:
|
||||
version "7.0.0-rc.1"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-rc.1.tgz#11f368e740e2b3cfe805891be127d07391673654"
|
||||
integrity sha512-FVFOeT+eGdbcPe+uH+cWnEElrU4LiDMrlstNSUpI3MPErICLtVoUCbKrF+n+8DYemHDe7wPqYtuNEYTM3ur3xw==
|
||||
version "7.0.0-rc.2"
|
||||
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.0.0-rc.2.tgz#bd5b18ff9b60ca28ea4b3a824419035007064fdf"
|
||||
integrity sha512-81+TFxK8hUK3tmJ9TPon07bgun2ASgZ8OXumUuWSAnktSAzTvubw4NCJTr0Tc0lO9IfTThi5z3GDVlmjY3n5ug==
|
||||
dependencies:
|
||||
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"
|
||||
integrity sha512-XmeffAZHv8z7451kzeq9oKh8fh278Ak+UIOGGrapyqrFBB773xN8vMQ3O7J7TYLnb9BUwcqadKkmgaq7q6fhZg==
|
||||
|
||||
shadow-cljs@^2.11.20:
|
||||
shadow-cljs@2.12.5:
|
||||
version "2.12.5"
|
||||
resolved "https://registry.yarnpkg.com/shadow-cljs/-/shadow-cljs-2.12.5.tgz#d3cf29fc1f1e02dd875939549419979e0feadbf4"
|
||||
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"
|
||||
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:
|
||||
version "1.0.4"
|
||||
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"
|
||||
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:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b"
|
||||
|
|
Loading…
Add table
Reference in a new issue