0
Fork 0
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:
Andrey Antukh 2021-04-29 13:04:19 +02:00 committed by Andrés Moya
parent 2582e87ffa
commit e15a212b14
42 changed files with 1329 additions and 208 deletions

1
.gitignore vendored
View file

@ -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*

View file

@ -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"}

View file

@ -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)

View file

@ -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"))))))))

View file

@ -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")}
])

View file

@ -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
);

View file

@ -18,6 +18,7 @@
(derive ::password ::instance)
(derive ::image ::instance)
(derive ::font ::instance)
(defmethod ig/pre-init-spec ::instance [_]
(s/spec int?))

View file

@ -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 {}))))

View 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))

View file

@ -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

View file

@ -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]))

View 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]))

View 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})))

View file

@ -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))

View file

@ -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)))))

View file

@ -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))

View file

@ -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

View file

@ -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"))

View file

@ -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? %)

View file

@ -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});
// }
}
}

View file

@ -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",

View file

@ -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";

View 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;
}
}
}

View file

@ -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

View 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)))))

View file

@ -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))))

View 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]))

View file

@ -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)))

View file

@ -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])

View file

@ -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}

View file

@ -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}])]]]]]))

View 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"]]])

View file

@ -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}]]]))

View file

@ -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
[]

View file

@ -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}]]]))

View file

@ -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}]])]

View file

@ -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

View file

@ -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
[]

View file

@ -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)))

View 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?"

View file

@ -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?"

View file

@ -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"