mirror of
https://github.com/penpot/penpot.git
synced 2025-05-11 06:26:38 +02:00
🎉 Add dashboard custom fonts management.
This commit is contained in:
parent
2582e87ffa
commit
e15a212b14
42 changed files with 1329 additions and 208 deletions
|
@ -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)))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue