Merge pull request #4223 from penpot/niwinz-staging-bugfix-4

🐛 Several bugfixes and optimizations
This commit is contained in:
Aitor Moreno 2024-03-07 15:40:32 +01:00 committed by GitHub
commit 9012987f7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 712 additions and 521 deletions

View file

@ -37,6 +37,13 @@
<h2>GENERAL NOTES</h2> <h2>GENERAL NOTES</h2>
<h3>HTTP Transport & Methods</h3>
<p>The HTTP is the transport method for accesing this API; all
functions can be called using POST HTTP method; the functions
that starts with <b>get-</b> in the name, can use GET HTTP
method which in many cases benefits from the HTTP cache.</p>
<h3>Authentication</h3> <h3>Authentication</h3>
<p>The penpot backend right now offers two way for authenticate the request: <p>The penpot backend right now offers two way for authenticate the request:
<b>cookies</b> (the same mechanism that we use ourselves on accessing the API from the <b>cookies</b> (the same mechanism that we use ourselves on accessing the API from the

View file

@ -60,8 +60,12 @@
(defmethod handle-error :restriction (defmethod handle-error :restriction
[err _ _] [err _ _]
{::rres/status 400 (let [{:keys [code] :as data} (ex-data err)]
::rres/body (ex-data err)}) (if (= code :method-not-allowed)
{::rres/status 405
::rres/body data}
{::rres/status 400
::rres/body data})))
(defmethod handle-error :rate-limit (defmethod handle-error :rate-limit
[err _ _] [err _ _]

View file

@ -248,6 +248,7 @@
renewal (dt/plus created-at default-renewal-max-age) renewal (dt/plus created-at default-renewal-max-age)
expires (dt/plus created-at max-age) expires (dt/plus created-at max-age)
secure? (contains? cf/flags :secure-session-cookies) secure? (contains? cf/flags :secure-session-cookies)
strict? (contains? cf/flags :strict-session-cookies)
cors? (contains? cf/flags :cors) cors? (contains? cf/flags :cors)
name (cf/get :auth-token-cookie-name default-auth-token-cookie-name) name (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
comment (str "Renewal at: " (dt/format-instant renewal :rfc1123)) comment (str "Renewal at: " (dt/format-instant renewal :rfc1123))
@ -256,7 +257,7 @@
:expires expires :expires expires
:value token :value token
:comment comment :comment comment
:same-site (if cors? :none :lax) :same-site (if cors? :none (if strict? :strict :lax))
:secure secure?}] :secure secure?}]
(update response :cookies assoc name cookie))) (update response :cookies assoc name cookie)))

View file

@ -31,6 +31,7 @@
[app.util.services :as sv] [app.util.services :as sv]
[app.util.time :as dt] [app.util.time :as dt]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[cuerdas.core :as str]
[integrant.core :as ig] [integrant.core :as ig]
[promesa.core :as p] [promesa.core :as p]
[ring.request :as rreq] [ring.request :as rreq]
@ -71,24 +72,31 @@
(defn- rpc-handler (defn- rpc-handler
"Ring handler that dispatches cmd requests and convert between "Ring handler that dispatches cmd requests and convert between
internal async flow into ring async flow." internal async flow into ring async flow."
[methods {:keys [params path-params] :as request}] [methods {:keys [params path-params method] :as request}]
(let [type (keyword (:type path-params)) (let [handler-name (:type path-params)
etag (rreq/get-header request "if-none-match") etag (rreq/get-header request "if-none-match")
profile-id (or (::session/profile-id request) profile-id (or (::session/profile-id request)
(::actoken/profile-id request)) (::actoken/profile-id request))
data (-> params data (-> params
(assoc ::request-at (dt/now)) (assoc ::request-at (dt/now))
(assoc ::session/id (::session/id request)) (assoc ::session/id (::session/id request))
(assoc ::cond/key etag) (assoc ::cond/key etag)
(cond-> (uuid? profile-id) (cond-> (uuid? profile-id)
(assoc ::profile-id profile-id))) (assoc ::profile-id profile-id)))
data (vary-meta data assoc ::http/request request) data (vary-meta data assoc ::http/request request)
method (get methods type default-handler)] handler-fn (get methods (keyword handler-name) default-handler)]
(when (and (or (= method :get)
(= method :head))
(not (str/starts-with? handler-name "get-")))
(ex/raise :type :restriction
:code :method-not-allowed
:hint "method not allowed for this request"))
(binding [cond/*enabled* true] (binding [cond/*enabled* true]
(let [response (method data)] (let [response (handler-fn data)]
(handle-response request response))))) (handle-response request response)))))
(defn- wrap-metrics (defn- wrap-metrics

View file

@ -716,20 +716,19 @@
(defn name (defn name
"Improved version of name that won't fail if the input is not a keyword" "Improved version of name that won't fail if the input is not a keyword"
([maybe-keyword] (name maybe-keyword nil)) [maybe-keyword]
([maybe-keyword default-value] (cond
(cond (nil? maybe-keyword)
(keyword? maybe-keyword) nil
(c/name maybe-keyword)
(string? maybe-keyword) (keyword? maybe-keyword)
maybe-keyword (c/name maybe-keyword)
(nil? maybe-keyword) default-value (string? maybe-keyword)
maybe-keyword
:else :else
(or default-value (str maybe-keyword)))
(str maybe-keyword)))))
(defn prefix-keyword (defn prefix-keyword
"Given a keyword and a prefix will return a new keyword with the prefix attached "Given a keyword and a prefix will return a new keyword with the prefix attached

View file

@ -12,7 +12,6 @@
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.components-list :as ctkl] [app.common.types.components-list :as ctkl]
[app.common.types.pages-list :as ctpl] [app.common.types.pages-list :as ctpl]
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[clojure.set :as set] [clojure.set :as set]
[cuerdas.core :as str])) [cuerdas.core :as str]))
@ -741,22 +740,6 @@
(d/seek root-frame?) (d/seek root-frame?)
:id)) :id))
(defn comparator-layout-z-index
[[idx-a child-a] [idx-b child-b]]
(cond
(> (ctl/layout-z-index child-a) (ctl/layout-z-index child-b)) 1
(< (ctl/layout-z-index child-a) (ctl/layout-z-index child-b)) -1
(< idx-a idx-b) 1
(> idx-a idx-b) -1
:else 0))
(defn sort-layout-children-z-index
[children]
(->> children
(d/enumerate)
(sort comparator-layout-z-index)
(mapv second)))
(defn common-parent-frame (defn common-parent-frame
"Search for the common frame for the selected shapes. Otherwise returns the root frame" "Search for the common frame for the selected shapes. Otherwise returns the root frame"
[objects selected] [objects selected]

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.shapes.grid-layout.areas :as sga] [app.common.geom.shapes.grid-layout.areas :as sga]
[app.common.math :as mth] [app.common.math :as mth]
[app.common.schema :as sm] [app.common.schema :as sm]
@ -47,7 +48,8 @@
#{:flex :grid}) #{:flex :grid})
(def flex-direction-types (def flex-direction-types
#{:row :reverse-row :row-reverse :column :reverse-column :column-reverse}) ;;TODO remove reverse-column and reverse-row after script ;;TODO remove reverse-column and reverse-row after script
#{:row :reverse-row :row-reverse :column :reverse-column :column-reverse})
(def grid-direction-types (def grid-direction-types
#{:row :column}) #{:row :column})
@ -128,7 +130,7 @@
(def grid-cell-justify-self-types (def grid-cell-justify-self-types
#{:auto :start :center :end :stretch}) #{:auto :start :center :end :stretch})
(sm/define! ::grid-cell (sm/def! ::grid-cell
[:map {:title "GridCell"} [:map {:title "GridCell"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:area-name {:optional true} :string] [:area-name {:optional true} :string]
@ -142,7 +144,7 @@
[:shapes [:shapes
[:vector {:gen/max 1} ::sm/uuid]]]) [:vector {:gen/max 1} ::sm/uuid]]])
(sm/define! ::grid-track (sm/def! ::grid-track
[:map {:title "GridTrack"} [:map {:title "GridTrack"}
[:type [::sm/one-of grid-track-types]] [:type [::sm/one-of grid-track-types]]
[:value {:optional true} [:maybe ::sm/safe-number]]]) [:value {:optional true} [:maybe ::sm/safe-number]]])
@ -197,14 +199,14 @@
([objects id] ([objects id]
(flex-layout? (get objects id))) (flex-layout? (get objects id)))
([shape] ([shape]
(and (= :frame (:type shape)) (and (cfh/frame-shape? shape)
(= :flex (:layout shape))))) (= :flex (:layout shape)))))
(defn grid-layout? (defn grid-layout?
([objects id] ([objects id]
(grid-layout? (get objects id))) (grid-layout? (get objects id)))
([shape] ([shape]
(and (= :frame (:type shape)) (and (cfh/frame-shape? shape)
(= :grid (:layout shape))))) (= :grid (:layout shape)))))
(defn any-layout? (defn any-layout?
@ -212,7 +214,10 @@
(any-layout? (get objects id))) (any-layout? (get objects id)))
([shape] ([shape]
(or (flex-layout? shape) (grid-layout? shape)))) (and (cfh/frame-shape? shape)
(let [layout (:layout shape)]
(or (= :flex layout)
(= :grid layout))))))
(defn flex-layout-immediate-child? [objects shape] (defn flex-layout-immediate-child? [objects shape]
(let [parent-id (:parent-id shape) (let [parent-id (:parent-id shape)
@ -262,20 +267,21 @@
(defn inside-layout? (defn inside-layout?
"Check if the shape is inside a layout" "Check if the shape is inside a layout"
[objects shape] [objects shape]
(loop [current-id (dm/get-prop shape :id)]
(loop [current-id (:id shape)] (let [current (get objects current-id)
(let [current (get objects current-id)] parent-id (dm/get-prop current :parent-id)]
(cond (cond
(or (nil? current) (= current-id (:parent-id current))) (or (nil? current) (= current-id parent-id))
false false
(= :frame (:type current)) (cfh/frame-shape? current-id)
(:layout current) (:layout current)
:else :else
(recur (:parent-id current)))))) (recur parent-id)))))
(defn wrap? [{:keys [layout-wrap-type]}] (defn wrap?
[{:keys [layout-wrap-type]}]
(= layout-wrap-type :wrap)) (= layout-wrap-type :wrap))
(defn fill-width? (defn fill-width?
@ -536,6 +542,22 @@
([shape] ([shape]
(or (:layout-item-z-index shape) 0))) (or (:layout-item-z-index shape) 0)))
(defn- comparator-layout-z-index
[[idx-a child-a] [idx-b child-b]]
(cond
(> (layout-z-index child-a) (layout-z-index child-b)) 1
(< (layout-z-index child-a) (layout-z-index child-b)) -1
(< idx-a idx-b) 1
(> idx-a idx-b) -1
:else 0))
(defn sort-layout-children-z-index
[children]
(->> children
(d/enumerate)
(sort comparator-layout-z-index)
(mapv second)))
(defn change-h-sizing? (defn change-h-sizing?
[frame-id objects children-ids] [frame-id objects children-ids]
(and (flex-layout? objects frame-id) (and (flex-layout? objects frame-id)

View file

@ -49,19 +49,20 @@
on-error on-error
(mf/use-callback (mf/use-callback
(fn [data {:keys [code] :as error}] (fn [data cause]
(reset! submitted false) (reset! submitted false)
(case code (let [code (-> cause ex-data :code)]
:profile-not-verified (case code
(rx/of (msg/error (tr "auth.notifications.profile-not-verified"))) :profile-not-verified
(rx/of (msg/error (tr "auth.notifications.profile-not-verified")))
:profile-is-muted :profile-is-muted
(rx/of (msg/error (tr "errors.profile-is-muted"))) (rx/of (msg/error (tr "errors.profile-is-muted")))
:email-has-permanent-bounces :email-has-permanent-bounces
(rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data)))) (rx/of (msg/error (tr "errors.email-has-permanent-bounces" (:email data))))
(rx/throw error)))) (rx/throw cause)))))
on-submit on-submit
(mf/use-callback (mf/use-callback

View file

@ -58,33 +58,33 @@
:opt-un [::invitation-token])) :opt-un [::invitation-token]))
(defn- handle-prepare-register-error (defn- handle-prepare-register-error
[form {:keys [type code] :as cause}] [form cause]
(condp = [type code] (let [{:keys [type code]} (ex-data cause)]
[:restriction :registration-disabled] (condp = [type code]
(st/emit! (msg/error (tr "errors.registration-disabled"))) [:restriction :registration-disabled]
(st/emit! (msg/error (tr "errors.registration-disabled")))
[:restriction :profile-blocked] [:restriction :profile-blocked]
(st/emit! (msg/error (tr "errors.profile-blocked"))) (st/emit! (msg/error (tr "errors.profile-blocked")))
[:validation :email-has-permanent-bounces] [:validation :email-has-permanent-bounces]
(let [email (get @form [:data :email])] (let [email (get @form [:data :email])]
(st/emit! (msg/error (tr "errors.email-has-permanent-bounces" email)))) (st/emit! (msg/error (tr "errors.email-has-permanent-bounces" email))))
[:validation :email-already-exists] [:validation :email-already-exists]
(swap! form assoc-in [:errors :email] (swap! form assoc-in [:errors :email]
{:message "errors.email-already-exists"}) {:message "errors.email-already-exists"})
[:validation :email-as-password] [:validation :email-as-password]
(swap! form assoc-in [:errors :password] (swap! form assoc-in [:errors :password]
{:message "errors.email-as-password"}) {:message "errors.email-as-password"})
(st/emit! (msg/error (tr "errors.generic"))))) (st/emit! (msg/error (tr "errors.generic"))))))
(defn- handle-prepare-register-success (defn- handle-prepare-register-success
[params] [params]
(st/emit! (rt/nav :auth-register-validate {} params))) (st/emit! (rt/nav :auth-register-validate {} params)))
(mf/defc register-form (mf/defc register-form
[{:keys [params on-success-callback] :as props}] [{:keys [params on-success-callback] :as props}]
(let [initial (mf/use-memo (mf/deps params) (constantly params)) (let [initial (mf/use-memo (mf/deps params) (constantly params))
@ -100,7 +100,7 @@
(on-success-callback p))) (on-success-callback p)))
on-submit on-submit
(mf/use-callback (mf/use-fn
(fn [form _event] (fn [form _event]
(reset! submitted? true) (reset! submitted? true)
(let [cdata (:clean-data @form)] (let [cdata (:clean-data @form)]
@ -114,7 +114,7 @@
[:& fm/form {:on-submit on-submit :form form} [:& fm/form {:on-submit on-submit :form form}
[:div {:class (stl/css :fields-row)} [:div {:class (stl/css :fields-row)}
[:& fm/input {:type "email" [:& fm/input {:type "text"
:name :email :name :email
:label (tr "auth.email") :label (tr "auth.email")
:data-test "email-input" :data-test "email-input"
@ -225,7 +225,7 @@
(on-success-callback (:email p)))) (on-success-callback (:email p))))
on-submit on-submit
(mf/use-callback (mf/use-fn
(fn [form _event] (fn [form _event]
(reset! submitted? true) (reset! submitted? true)
(let [params (:clean-data @form)] (let [params (:clean-data @form)]

View file

@ -391,6 +391,7 @@
(mf/with-layout-effect [thread-pos comments-map] (mf/with-layout-effect [thread-pos comments-map]
(when-let [node (mf/ref-val ref)] (when-let [node (mf/ref-val ref)]
(dom/scroll-into-view-if-needed! node))) (dom/scroll-into-view-if-needed! node)))
(when (some? comment) (when (some? comment)
[:div {:class (stl/css :thread-content) [:div {:class (stl/css :thread-content)
:style {:top (str pos-y "px") :style {:top (str pos-y "px")

View file

@ -25,108 +25,143 @@
[cuerdas.core :as str] [cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defn- use-set-page-title (defn- use-page-title
[team section] [team section]
(mf/use-effect (mf/with-effect [team]
(mf/deps team) (when team
(fn [] (let [tname (if (:is-default team)
(when team (tr "dashboard.your-penpot")
(let [tname (if (:is-default team) (:name team))]
(tr "dashboard.your-penpot") (case section
(:name team))] :fonts (dom/set-html-title (tr "title.dashboard.fonts" tname))
(case section :providers (dom/set-html-title (tr "title.dashboard.font-providers" tname)))))))
:fonts (dom/set-html-title (tr "title.dashboard.fonts" tname))
:providers (dom/set-html-title (tr "title.dashboard.font-providers" tname)))))))) (defn- bad-font-family-tmp?
[font]
(and (contains? font :font-family-tmp)
(str/blank? (:font-family-tmp font))))
(mf/defc header (mf/defc header
{::mf/wrap [mf/memo]} {::mf/props :obj
[{:keys [section team] :as props}] ::mf/memo true
(use-set-page-title team section) ::mf/private true}
[{:keys [section team]}]
(use-page-title team section)
[:header {:class (stl/css :dashboard-header)} [:header {:class (stl/css :dashboard-header)}
[:div#dashboard-fonts-title {:class (stl/css :dashboard-title)} [:div#dashboard-fonts-title {:class (stl/css :dashboard-title)}
[:h1 (tr "labels.fonts")]]]) [:h1 (tr "labels.fonts")]]])
(mf/defc font-variant-display-name (mf/defc font-variant-display-name
{::mf/props :obj
::mf/private true}
[{:keys [variant]}] [{:keys [variant]}]
[:* [:*
[:span (cm/font-weight->name (:font-weight variant))] [:span (cm/font-weight->name (:font-weight variant))]
(when (not= "normal" (:font-style variant)) (when (not= "normal" (:font-style variant))
[:span " " (str/capital (:font-style variant))])]) [:span " " (str/capital (:font-style variant))])])
(mf/defc fonts-upload (mf/defc uploaded-fonts
{::mf/props :obj
::mf/private true}
[{:keys [team installed-fonts] :as props}] [{:keys [team installed-fonts] :as props}]
(let [fonts* (mf/use-state {}) (let [fonts* (mf/use-state {})
fonts (deref fonts*) fonts (deref fonts*)
input-ref (mf/use-ref) font-vals (mf/with-memo [fonts]
uploading (mf/use-state #{}) (->> fonts
(into [] (map val))
(not-empty)))
bad-font-family-tmp? team-id (:id team)
(mf/use-fn
(fn [font]
(and (contains? font :font-family-tmp)
(str/blank? (:font-family-tmp font)))))
disable-upload-all? (some bad-font-family-tmp? (vals fonts)) input-ref (mf/use-ref)
handle-click uploading* (mf/use-state #{})
uploading (deref uploading*)
disable-upload-all?
(some bad-font-family-tmp? fonts)
problematic-fonts?
(some :height-warning? (vals fonts))
on-click
(mf/use-fn #(dom/click (mf/ref-val input-ref))) (mf/use-fn #(dom/click (mf/ref-val input-ref)))
handle-selected on-selected
(mf/use-fn (mf/use-fn
(mf/deps team installed-fonts) (mf/deps team-id installed-fonts)
(fn [blobs] (fn [blobs]
(->> (df/process-upload blobs (:id team)) (->> (df/process-upload blobs team-id)
(rx/subs! (fn [result] (rx/subs! (fn [result]
(swap! fonts* df/merge-and-group-fonts installed-fonts result)) (swap! fonts* df/merge-and-group-fonts installed-fonts result))
(fn [error] (fn [error]
(js/console.error "error" error)))))) (js/console.error "error" error))))))
on-upload on-upload*
(mf/use-fn (mf/use-fn
(mf/deps team) (fn [{:keys [id] :as item}]
(fn [item] (swap! uploading* conj id)
(swap! uploading conj (:id item))
(->> (rp/cmd! :create-font-variant item) (->> (rp/cmd! :create-font-variant item)
(rx/delay-at-least 2000) (rx/delay-at-least 2000)
(rx/subs! (fn [font] (rx/subs! (fn [font]
(swap! fonts* dissoc (:id item)) (swap! fonts* dissoc id)
(swap! uploading disj (:id item)) (swap! uploading* disj id)
(st/emit! (df/add-font font))) (st/emit! (df/add-font font)))
(fn [error] (fn [error]
(js/console.log "error" error)))))) (js/console.log "error" error))))))
on-upload-all on-upload
(fn [items] (mf/use-fn
(run! on-upload items)) (mf/deps fonts on-upload*)
(fn [event]
(let [id (-> (dom/get-current-target event)
(dom/get-data "id")
(parse-uuid))
item (get fonts id)]
(on-upload* item))))
on-blur-name on-blur-name
(fn [id event] (mf/use-fn
(let [name (dom/get-target-val event)] (mf/deps installed-fonts)
(when-not (str/blank? name) (fn [event]
(swap! fonts* df/rename-and-regroup id name installed-fonts)))) (let [target (dom/get-current-target event)
id (-> target
(dom/get-data "id")
(parse-uuid))
name (dom/get-value target)]
(when-not (str/blank? name)
(swap! fonts* df/rename-and-regroup id name installed-fonts)))))
on-change-name on-change-name
(fn [id event] (mf/use-fn
(let [name (dom/get-target-val event)] (fn [event]
(swap! fonts* update-in [id] #(assoc % :font-family-tmp name)))) (let [target (dom/get-current-target event)
id (-> target
(dom/get-data "id")
(parse-uuid))
name (dom/get-value target)]
(swap! fonts* update id assoc :font-family-tmp name))))
on-delete on-delete
(mf/use-fn (mf/use-fn
(mf/deps team) (mf/deps team)
(fn [{:keys [id] :as item}] (fn [event]
(swap! fonts* dissoc id))) (let [id (-> (dom/get-current-target event)
(dom/get-data "id")
(parse-uuid))]
(swap! fonts* dissoc id))))
on-dismiss-all on-upload-all
(fn [items] (mf/use-fn
(run! on-delete items)) (mf/deps font-vals)
(fn [_]
(run! on-upload* font-vals)))
problematic-fonts? (some :height-warning? (vals fonts)) on-dismis-all
(mf/use-fn
handle-upload-all (mf/deps fonts)
(mf/use-fn (mf/deps fonts) #(on-upload-all (vals fonts))) (fn [_]
(run! on-delete (vals fonts))))]
handle-dismiss-all
(mf/use-fn (mf/deps fonts) #(on-dismiss-all (vals fonts)))]
[:div {:class (stl/css :dashboard-fonts-upload)} [:div {:class (stl/css :dashboard-fonts-upload)}
[:div {:class (stl/css :dashboard-fonts-hero)} [:div {:class (stl/css :dashboard-fonts-hero)}
@ -135,14 +170,14 @@
[:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}] [:& i18n/tr-html {:label "dashboard.fonts.hero-text1"}]
[:button {:class (stl/css :btn-primary) [:button {:class (stl/css :btn-primary)
:on-click handle-click :on-click on-click
:tab-index "0"} :tab-index "0"}
[:span (tr "labels.add-custom-font")] [:span (tr "labels.add-custom-font")]
[:& file-uploader {:input-id "font-upload" [:& file-uploader {:input-id "font-upload"
:accept cm/str-font-types :accept cm/str-font-types
:multi true :multi true
:ref input-ref :ref input-ref
:on-selected handle-selected}]] :on-selected on-selected}]]
[:& context-notification {:content (tr "dashboard.fonts.hero-text2") [:& context-notification {:content (tr "dashboard.fonts.hero-text2")
:type :default :type :default
@ -154,31 +189,32 @@
:is-html true}])]] :is-html true}])]]
[:* [:*
(when (some? (vals fonts)) (when (seq fonts)
[:div {:class (stl/css :font-item :table-row)} [:div {:class (stl/css :font-item :table-row)}
[:span (tr "dashboard.fonts.fonts-added" (i18n/c (count (vals fonts))))] [:span (tr "dashboard.fonts.fonts-added" (i18n/c (count fonts)))]
[:div {:class (stl/css :table-field :options)} [:div {:class (stl/css :table-field :options)}
[:button {:class (stl/css-case :btn-primary true [:button {:class (stl/css-case
:disabled disable-upload-all?) :btn-primary true
:on-click handle-upload-all :disabled disable-upload-all?)
:on-click on-upload-all
:data-test "upload-all" :data-test "upload-all"
:disabled disable-upload-all?} :disabled disable-upload-all?}
[:span (tr "dashboard.fonts.upload-all")]] [:span (tr "dashboard.fonts.upload-all")]]
[:button {:class (stl/css :btn-secondary) [:button {:class (stl/css :btn-secondary)
:on-click handle-dismiss-all :on-click on-dismis-all
:data-test "dismiss-all"} :data-test "dismiss-all"}
[:span (tr "dashboard.fonts.dismiss-all")]]]]) [:span (tr "dashboard.fonts.dismiss-all")]]]])
(for [item (sort-by :font-family (vals fonts))] (for [{:keys [id] :as item} (sort-by :font-family font-vals)]
(let [uploading? (contains? @uploading (:id item)) (let [uploading? (contains? uploading id)
disable-upload? (or uploading? disable-upload? (or uploading? (bad-font-family-tmp? item))]
(bad-font-family-tmp? item))]
[:div {:class (stl/css :font-item :table-row) [:div {:class (stl/css :font-item :table-row)
:key (:id item)} :key (dm/str id)}
[:div {:class (stl/css :table-field :family)} [:div {:class (stl/css :table-field :family)}
[:input {:type "text" [:input {:type "text"
:on-blur #(on-blur-name (:id item) %) :data-id (dm/str id)
:on-change #(on-change-name (:id item) %) :on-blur on-blur-name
:on-change on-change-name
:default-value (:font-family item)}]] :default-value (:font-family item)}]]
[:div {:class (stl/css :table-field :variants)} [:div {:class (stl/css :table-field :variants)}
[:span {:class (stl/css :label)} [:span {:class (stl/css :label)}
@ -190,115 +226,151 @@
[:div {:class (stl/css :table-field :options)} [:div {:class (stl/css :table-field :options)}
(when (:height-warning? item) (when (:height-warning? item)
[:span {:class (stl/css :icon :failure)} i/msg-neutral-refactor]) [:span {:class (stl/css :icon :failure)}
i/msg-neutral-refactor])
[:button {:on-click #(on-upload item) [:button {:on-click on-upload
:class (stl/css-case :btn-primary true :data-id (dm/str id)
:upload-button true :class (stl/css-case
:disabled disable-upload?) :btn-primary true
:upload-button true
:disabled disable-upload?)
:disabled disable-upload?} :disabled disable-upload?}
(if uploading? (if ^boolean uploading?
(tr "labels.uploading") (tr "labels.uploading")
(tr "labels.upload"))] (tr "labels.upload"))]
[:span {:class (stl/css :icon :close) [:span {:class (stl/css :icon :close)
:on-click #(on-delete item)} i/close-refactor]]]))]])) :data-id (dm/str id)
:on-click on-delete}
i/close-refactor]]]))]]))
(mf/defc installed-font-context-menu
{::mf/props :obj
::mf/private true}
[{:keys [is-open on-close on-edit on-delete]}]
(let [options (mf/with-memo [on-edit on-delete]
[{:option-name (tr "labels.edit")
:id "font-edit"
:option-handler on-edit}
{:option-name (tr "labels.delete")
:id "font-delete"
:option-handler on-delete}])]
[:& context-menu-a11y
{:on-close on-close
:show is-open
:fixed? false
:min-width? true
:top -15
:left -115
:options options
:workspace? false}]))
(mf/defc installed-font (mf/defc installed-font
[{:keys [font-id variants] :as props}] {::mf/props :obj
::mf/private true
::mf/memo true}
[{:keys [font-id variants]}]
(let [font (first variants) (let [font (first variants)
variants (sort-by (fn [item] menu-open* (mf/use-state false)
[(:font-weight item) menu-open? (deref menu-open*)
(if (= "normal" (:font-style item)) 1 2)]) edition* (mf/use-state false)
variants) edition? (deref edition*)
open-menu? (mf/use-state false)
edit? (mf/use-state false)
state* (mf/use-state (:font-family font)) state* (mf/use-state (:font-family font))
font-family (deref state*) font-family (deref state*)
variants
(mf/with-memo [variants]
(sort-by (fn [item]
[(:font-weight item)
(if (= "normal" (:font-style item)) 1 2)])
variants))
on-change on-change
(mf/use-callback (mf/use-fn
(fn [event] (fn [event]
(reset! state* (dom/get-target-val event)))) (reset! state* (dom/get-target-val event))))
on-edit
(mf/use-fn #(reset! edition* true))
on-menu-open
(mf/use-fn #(reset! menu-open* true))
on-menu-close
(mf/use-fn #(reset! menu-open* false))
on-save on-save
(mf/use-callback (mf/use-fn
(mf/deps font-family) (mf/deps font-family)
(fn [_] (fn [_]
(reset! edition* false)
(when-not (str/blank? font-family) (when-not (str/blank? font-family)
(st/emit! (df/update-font {:id font-id :name font-family}))) (st/emit! (df/update-font {:id font-id :name font-family})))))
(reset! edit? false)))
on-key-down on-key-down
(mf/use-callback (mf/use-fn
(mf/deps on-save) (mf/deps on-save)
(fn [event] (fn [event]
(when (kbd/enter? event) (when (kbd/enter? event)
(on-save event)))) (on-save event))))
on-cancel on-cancel
(mf/use-callback (mf/use-fn
(fn [_] (fn [_]
(reset! edit? false) (reset! edition* false)
(reset! state* (:font-family font)))) (reset! state* (:font-family font))))
delete-font-fn on-delete-font
(mf/use-callback (mf/use-fn
(mf/deps font-id) (mf/deps font-id)
(fn [] (fn []
(st/emit! (df/delete-font font-id)))) (let [options {:type :confirm
:title (tr "modals.delete-font.title")
delete-variant-fn :message (tr "modals.delete-font.message")
(mf/use-callback :accept-label (tr "labels.delete")
(fn [id] :on-accept (fn [_props]
(st/emit! (df/delete-font-variant id)))) (st/emit! (df/delete-font font-id)))}]
(st/emit! (modal/show options)))))
on-delete
(mf/use-callback
(mf/deps delete-font-fn)
(fn []
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.delete-font.title")
:message (tr "modals.delete-font.message")
:accept-label (tr "labels.delete")
:on-accept (fn [_props] (delete-font-fn))}))))
on-delete-variant on-delete-variant
(mf/use-callback (mf/use-fn
(mf/deps delete-variant-fn) (fn [event]
(fn [id] (let [id (-> (dom/get-current-target event)
(st/emit! (modal/show (dom/get-data "id")
{:type :confirm (parse-uuid))
:title (tr "modals.delete-font-variant.title") options {:type :confirm
:message (tr "modals.delete-font-variant.message") :title (tr "modals.delete-font-variant.title")
:accept-label (tr "labels.delete") :message (tr "modals.delete-font-variant.message")
:on-accept (fn [_props] :accept-label (tr "labels.delete")
(delete-variant-fn id))}))))] :on-accept (fn [_props]
(st/emit! (df/delete-font-variant id)))}]
(st/emit! (modal/show options)))))]
[:div {:class (stl/css :font-item :table-row)} [:div {:class (stl/css :font-item :table-row)}
[:div {:class (stl/css :table-field :family)} [:div {:class (stl/css :table-field :family)}
(if @edit? (if ^boolean edition?
[:input {:type "text" [:input {:type "text"
:auto-focus true
:default-value font-family :default-value font-family
:on-key-down on-key-down :on-key-down on-key-down
:on-change on-change}] :on-change on-change}]
[:span (:font-family font)])] [:span (:font-family font)])]
[:div {:class (stl/css :table-field :variants)} [:div {:class (stl/css :table-field :variants)}
(for [item variants] (for [{:keys [id] :as item} variants]
[:div {:class (stl/css :variant) [:div {:class (stl/css :variant)
:key (dm/str (:id item) "-variant")} :key (dm/str id)}
[:span {:class (stl/css :label)} [:span {:class (stl/css :label)}
[:& font-variant-display-name {:variant item}]] [:& font-variant-display-name {:variant item}]]
[:span [:span
{:class (stl/css :icon :close) {:class (stl/css :icon :close)
:on-click #(on-delete-variant (:id item))} :data-id (dm/str id)
:on-click on-delete-variant}
i/add-refactor]])] i/add-refactor]])]
(if @edit? (if ^boolean edition?
[:div {:class (stl/css :table-field :options)} [:div {:class (stl/css :table-field :options)}
[:button [:button
{:disabled (str/blank? font-family) {:disabled (str/blank? font-family)
@ -307,27 +379,19 @@
:btn-disabled (str/blank? font-family))} :btn-disabled (str/blank? font-family))}
(tr "labels.save")] (tr "labels.save")]
[:button {:class (stl/css :icon :close) [:button {:class (stl/css :icon :close)
:on-click on-cancel} i/close-refactor]] :on-click on-cancel}
i/close-refactor]]
[:div {:class (stl/css :table-field :options)} [:div {:class (stl/css :table-field :options)}
[:span {:class (stl/css :icon) [:span {:class (stl/css :icon)
:on-click #(reset! open-menu? true)} :on-click on-menu-open}
i/menu-refactor] i/menu-refactor]
[:& context-menu-a11y {:on-close #(reset! open-menu? false) [:& installed-font-context-menu
:show @open-menu? {:on-close on-menu-close
:fixed? false :is-open menu-open?
:min-width? true :on-delete on-delete-font
:top -15 :on-edit on-edit}]])]))
:left -115
:options [{:option-name (tr "labels.edit")
:id "font-edit"
:option-handler #(reset! edit? true)}
{:option-name (tr "labels.delete")
:id "font-delete"
:option-handler on-delete}]
:workspace? false}]])]))
(mf/defc installed-fonts (mf/defc installed-fonts
[{:keys [fonts] :as props}] [{:keys [fonts] :as props}]
@ -377,7 +441,7 @@
[:* [:*
[:& header {:team team :section :fonts}] [:& header {:team team :section :fonts}]
[:section {:class (stl/css :dashboard-container :dashboard-fonts)} [:section {:class (stl/css :dashboard-container :dashboard-fonts)}
[:& fonts-upload {:team team :installed-fonts fonts}] [:& uploaded-fonts {:team team :installed-fonts fonts}]
[:& installed-fonts {:team team :fonts fonts}]]])) [:& installed-fonts {:team team :fonts fonts}]]]))
(mf/defc font-providers-page (mf/defc font-providers-page

View file

@ -36,25 +36,26 @@
(defn use-import-file (defn use-import-file
[project-id on-finish-import] [project-id on-finish-import]
(mf/use-callback (mf/use-fn
(mf/deps project-id on-finish-import) (mf/deps project-id on-finish-import)
(fn [files] (fn [entries]
(when files (let [entries (->> entries
(let [files (->> files (mapv (fn [file]
(mapv {:name (.-name file)
(fn [file] :uri (wapi/create-uri file)}))
{:name (.-name file) (not-empty))]
:uri (wapi/create-uri file)})))] (when entries
(st/emit! (modal/show (st/emit! (modal/show
{:type :import {:type :import
:project-id project-id :project-id project-id
:files files :entries entries
:on-finish-import on-finish-import}))))))) :on-finish-import on-finish-import})))))))
(mf/defc import-form (mf/defc import-form
{::mf/forward-ref true} {::mf/forward-ref true
[{:keys [project-id on-finish-import]} external-ref] ::mf/props :obj}
[{:keys [project-id on-finish-import]} external-ref]
(let [on-file-selected (use-import-file project-id on-finish-import)] (let [on-file-selected (use-import-file project-id on-finish-import)]
[:form.import-file {:aria-hidden "true"} [:form.import-file {:aria-hidden "true"}
[:& file-uploader {:accept ".penpot,.zip" [:& file-uploader {:accept ".penpot,.zip"
@ -62,69 +63,72 @@
:ref external-ref :ref external-ref
:on-selected on-file-selected}]])) :on-selected on-file-selected}]]))
(defn update-file [files file-id new-name] (defn- update-entry-name
(->> files [entries file-id new-name]
(mapv (mapv (fn [entry]
(fn [file]
(let [new-name (str/trim new-name)] (let [new-name (str/trim new-name)]
(cond-> file (cond-> entry
(and (= (:file-id file) file-id) (and (= (:file-id entry) file-id)
(not= "" new-name)) (not= "" new-name))
(assoc :name new-name))))))) (assoc :name new-name))))
entries))
(defn remove-file [files file-id] (defn- remove-entry
(->> files [entries file-id]
(mapv (mapv (fn [entry]
(fn [file] (cond-> entry
(cond-> file (= (:file-id entry) file-id)
(= (:file-id file) file-id) (assoc :deleted true)))
(assoc :deleted? true)))))) entries))
(defn set-analyze-error (defn- update-with-analyze-error
[files uri error] [entries uri error]
(->> files (->> entries
(mapv (fn [file] (mapv (fn [entry]
(cond-> file (cond-> entry
(= uri (:uri file)) (= uri (:uri entry))
(-> (assoc :status :analyze-error) (-> (assoc :status :analyze-error)
(assoc :error error))))))) (assoc :error error)))))))
(defn set-analyze-result [files uri type data] (defn- update-with-analyze-result
(let [existing-files? (into #{} (->> files (map :file-id) (filter some?))) [entries uri type result]
replace-file (let [existing-entries? (into #{} (keep :file-id) entries)
(fn [file] replace-entry
(if (and (= uri (:uri file)) (fn [entry]
(= (:status file) :analyzing)) (if (and (= uri (:uri entry))
(->> (:files data) (= (:status entry) :analyzing))
(remove (comp existing-files? first)) (->> (:files result)
(mapv (fn [[file-id file-data]] (remove (comp existing-entries? first))
(-> file-data (map (fn [[file-id file-data]]
(assoc :file-id file-id (-> file-data
:status :ready (assoc :file-id file-id)
:uri uri (assoc :status :ready)
:type type))))) (assoc :uri uri)
[file]))] (assoc :type type)))))
(into [] (mapcat replace-file) files))) [entry]))]
(into [] (mapcat replace-entry) entries)))
(defn mark-files-importing [files] (defn- mark-entries-importing
(->> files [entries]
(->> entries
(filter #(= :ready (:status %))) (filter #(= :ready (:status %)))
(mapv #(assoc % :status :importing)))) (mapv #(assoc % :status :importing))))
(defn update-status [files file-id status progress errors] (defn- update-entry-status
(->> files [entries file-id status progress errors]
(mapv (fn [file] (mapv (fn [entry]
(cond-> file (cond-> entry
(and (= file-id (:file-id file)) (not= status :import-progress)) (and (= file-id (:file-id entry)) (not= status :import-progress))
(assoc :status status) (assoc :status status)
(and (= file-id (:file-id file)) (= status :import-progress)) (and (= file-id (:file-id entry)) (= status :import-progress))
(assoc :progress progress) (assoc :progress progress)
(= file-id (:file-id file)) (= file-id (:file-id entry))
(assoc :errors errors)))))) (assoc :errors errors)))
entries))
(defn parse-progress-message (defn- parse-progress-message
[message] [message]
(case (:type message) (case (:type message)
:upload-data :upload-data
@ -150,52 +154,116 @@
(str message))) (str message)))
(defn- has-status-importing?
[item]
(= (:status item) :importing))
(defn- has-status-analyzing?
[item]
(= (:status item) :analyzing))
(defn- has-status-analyze-error?
[item]
(= (:status item) :analyzing))
(defn- has-status-success?
[item]
(and (= (:status item) :import-finish)
(empty? (:errors item))))
(defn- has-status-error?
[item]
(and (= (:status item) :import-finish)
(d/not-empty? (:errors item))))
(defn- has-status-ready?
[item]
(and (= :ready (:status item))
(not (:deleted item))))
(defn- analyze-entries
[state entries]
(->> (uw/ask-many!
{:cmd :analyze-import
:files entries
:features @features/features-ref})
(rx/mapcat #(rx/delay emit-delay (rx/of %)))
(rx/filter some?)
(rx/subs!
(fn [{:keys [uri data error type] :as msg}]
(if (some? error)
(swap! state update-with-analyze-error uri error)
(swap! state update-with-analyze-result uri type data))))))
(defn- import-files!
[state project-id entries]
(st/emit! (ptk/data-event ::ev/event {::ev/name "import-files"
:num-files (count entries)}))
(->> (uw/ask-many!
{:cmd :import-files
:project-id project-id
:files entries
:features @features/features-ref})
(rx/subs!
(fn [{:keys [file-id status message errors] :as msg}]
(swap! state update-entry-status file-id status message errors)))))
(mf/defc import-entry (mf/defc import-entry
[{:keys [state file editing? can-be-deleted?]}] {::mf/props :obj
(let [loading? (or (= :analyzing (:status file)) ::mf/memo true
(= :importing (:status file))) ::mf/private true}
analyze-error? (= :analyze-error (:status file)) [{:keys [entries entry edition can-be-deleted on-edit on-change on-delete]}]
import-finish? (= :import-finish (:status file)) (let [status (:status entry)
import-error? (= :import-error (:status file)) loading? (or (= :analyzing status)
import-warn? (d/not-empty? (:errors file)) (= :importing status))
ready? (= :ready (:status file)) analyze-error? (= :analyze-error status)
is-shared? (:shared file) import-finish? (= :import-finish status)
progress (:progress file) import-error? (= :import-error status)
import-warn? (d/not-empty? (:errors entry))
ready? (= :ready status)
is-shared? (:shared entry)
progress (:progress entry)
handle-edit-key-press file-id (:file-id entry)
(mf/use-callback editing? (and (some? file-id) (= edition file-id))
(fn [e]
(when (or (kbd/enter? e) (kbd/esc? e))
(dom/prevent-default e)
(dom/stop-propagation e)
(dom/blur! (dom/get-target e)))))
handle-edit-blur on-edit-key-press
(mf/use-callback (mf/use-fn
(mf/deps file) (fn [event]
(fn [e] (when (or (kbd/enter? event)
(let [value (dom/get-target-val e)] (kbd/esc? event))
(swap! state #(-> (assoc % :editing nil) (dom/prevent-default event)
(update :files update-file (:file-id file) value)))))) (dom/stop-propagation event)
(dom/blur! (dom/get-target event)))))
handle-edit-entry on-edit-blur
(mf/use-callback (mf/use-fn
(mf/deps file) (mf/deps file-id on-change)
(fn [] (fn [event]
(swap! state assoc :editing (:file-id file)))) (let [value (dom/get-target-val event)]
(on-change file-id value event))))
handle-remove-entry on-edit'
(mf/use-callback (mf/use-fn
(mf/deps file) (mf/deps file-id on-change)
(fn [] (fn [event]
(swap! state update :files remove-file (:file-id file))))] (when (fn? on-edit)
(on-edit file-id event))))
[:div {:class (stl/css-case :file-entry true on-delete'
:loading loading? (mf/use-fn
:success (and import-finish? (not import-warn?) (not import-error?)) (mf/deps file-id on-delete)
:warning (and import-finish? import-warn? (not import-error?)) (fn [event]
:error (or import-error? analyze-error?) (when (fn? on-delete)
:editable (and ready? (not editing?)))} (on-delete file-id event))))]
[:div {:class (stl/css-case
:file-entry true
:loading loading?
:success (and import-finish? (not import-warn?) (not import-error?))
:warning (and import-finish? import-warn? (not import-error?))
:error (or import-error? analyze-error?)
:editable (and ready? (not editing?)))}
[:div {:class (stl/css :file-name)} [:div {:class (stl/css :file-name)}
[:div {:class (stl/css-case :file-icon true [:div {:class (stl/css-case :file-icon true
@ -211,26 +279,28 @@
[:div {:class (stl/css :file-name-edit)} [:div {:class (stl/css :file-name-edit)}
[:input {:type "text" [:input {:type "text"
:auto-focus true :auto-focus true
:default-value (:name file) :default-value (:name entry)
:on-key-press handle-edit-key-press :on-key-press on-edit-key-press
:on-blur handle-edit-blur}]] :on-blur on-edit-blur}]]
[:div {:class (stl/css :file-name-label)} [:div {:class (stl/css :file-name-label)}
(:name file) (:name entry)
(when is-shared? (when ^boolean is-shared?
[:span {:class (stl/css :icon)} [:span {:class (stl/css :icon)}
i/library-refactor])]) i/library-refactor])])
[:div {:class (stl/css :edit-entry-buttons)} [:div {:class (stl/css :edit-entry-buttons)}
(when (= "application/zip" (:type file)) (when (and (= "application/zip" (:type entry))
[:button {:on-click handle-edit-entry} i/curve-refactor]) (= status :ready))
(when can-be-deleted? [:button {:on-click on-edit'} i/curve-refactor])
[:button {:on-click handle-remove-entry} i/delete-refactor])]] (when can-be-deleted
[:button {:on-click on-delete'} i/delete-refactor])]]
(cond (cond
analyze-error? analyze-error?
[:div {:class (stl/css :error-message)} [:div {:class (stl/css :error-message)}
(if (some? (:error file)) (if (some? (:error entry))
(tr (:error file)) (tr (:error entry))
(tr "dashboard.import.analyze-error"))] (tr "dashboard.import.analyze-error"))]
import-error? import-error?
@ -241,138 +311,143 @@
[:div {:class (stl/css :progress-message)} (parse-progress-message progress)]) [:div {:class (stl/css :progress-message)} (parse-progress-message progress)])
[:div {:class (stl/css :linked-libraries)} [:div {:class (stl/css :linked-libraries)}
(for [library-id (:libraries file)] (for [library-id (:libraries entry)]
(let [library-data (->> @state :files (d/seek #(= library-id (:file-id %)))) (let [library-data (d/seek #(= library-id (:file-id %)) entries)
error? (or (:deleted? library-data) (:import-error library-data))] error? (or (:deleted library-data)
(:import-error library-data))]
(when (some? library-data) (when (some? library-data)
[:div {:class (stl/css :linked-library)} [:div {:class (stl/css :linked-library)
:key (dm/str library-id)}
(:name library-data) (:name library-data)
[:span {:class (stl/css-case :linked-library-tag true [:span {:class (stl/css-case
:error error?)} i/detach-refactor]])))]])) :linked-library-tag true
:error error?)}
i/detach-refactor]])))]]))
(mf/defc import-dialog (mf/defc import-dialog
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :import} ::mf/register-as :import
[{:keys [project-id files template on-finish-import]}] ::mf/props :obj}
(let [state (mf/use-state
{:status :analyzing
:editing nil
:importing-templates 0
:files (->> files
(mapv #(assoc % :status :analyzing)))})
analyze-import [{:keys [project-id entries template on-finish-import]}]
(mf/use-callback
(fn [files]
(->> (uw/ask-many!
{:cmd :analyze-import
:files files
:features @features/features-ref})
(rx/mapcat #(rx/delay emit-delay (rx/of %)))
(rx/filter some?)
(rx/subs!
(fn [{:keys [uri data error type] :as msg}]
(if (some? error)
(swap! state update :files set-analyze-error uri error)
(swap! state update :files set-analyze-result uri type data)))))))
import-files (mf/with-effect []
(mf/use-callback ;; dispose uris when the component is umount
(fn [project-id files] (fn [] (run! wapi/revoke-uri (map :uri entries))))
(st/emit! (ptk/event ::ev/event {::ev/name "import-files"
:num-files (count files)}))
(->> (uw/ask-many!
{:cmd :import-files
:project-id project-id
:files files
:features @features/features-ref})
(rx/subs!
(fn [{:keys [file-id status message errors] :as msg}]
(swap! state update :files update-status file-id status message errors))))))
handle-cancel (let [entries* (mf/use-state
(mf/use-callback (fn [] (mapv #(assoc % :status :analyzing) entries)))
(mf/deps (:editing @state)) entries (deref entries*)
status* (mf/use-state :analyzing)
status (deref status*)
edition* (mf/use-state nil)
edition (deref edition*)
on-template-cloned-success
(mf/use-fn
(fn []
(swap! status* (constantly :importing))
;; (swap! state assoc :status :importing :importing-templates 0)
(st/emit! (dd/fetch-recent-files))))
on-template-cloned-error
(mf/use-fn
(fn [cause]
(swap! status* (constantly :error))
;; (swap! state assoc :status :error :importing-templates 0)
(errors/print-error! cause)
(rx/of (modal/hide)
(msg/error (tr "dashboard.libraries-and-templates.import-error")))))
continue-entries
(mf/use-fn
(mf/deps entries)
(fn []
(let [entries (filterv has-status-ready? entries)]
(swap! status* (constantly :importing))
(swap! entries* mark-entries-importing)
(import-files! entries* project-id entries))))
continue-template
(mf/use-fn
(mf/deps on-template-cloned-success
on-template-cloned-error
template)
(fn []
(let [mdata {:on-success on-template-cloned-success
:on-error on-template-cloned-error}
params {:project-id project-id :template-id (:id template)}]
(swap! status* (constantly :importing))
(st/emit! (dd/clone-template (with-meta params mdata))))))
on-edit
(mf/use-fn
(fn [file-id _event]
(swap! edition* (constantly file-id))))
on-entry-change
(mf/use-fn
(fn [file-id value]
(swap! edition* (constantly nil))
(swap! entries* update-entry-name file-id value)))
on-entry-delete
(mf/use-fn
(fn [file-id]
(swap! entries* remove-entry file-id)))
on-cancel
(mf/use-fn
(mf/deps edition)
(fn [event] (fn [event]
(when (nil? (:editing @state)) (when (nil? edition)
(dom/prevent-default event) (dom/prevent-default event)
(st/emit! (modal/hide))))) (st/emit! (modal/hide)))))
on-template-cloned-success on-continue
(fn [] (mf/use-fn
(swap! state assoc :status :importing :importing-templates 0) (mf/deps template
(st/emit! (dd/fetch-recent-files))) continue-template
continue-entries)
on-template-cloned-error
(fn [cause]
(swap! state assoc :status :error :importing-templates 0)
(errors/print-error! cause)
(rx/of (modal/hide)
(msg/error (tr "dashboard.libraries-and-templates.import-error"))))
continue-files
(fn []
(let [files (->> @state :files (filterv #(and (= :ready (:status %)) (not (:deleted? %)))))]
(import-files project-id files))
(swap! state
(fn [state]
(-> state
(assoc :status :importing)
(update :files mark-files-importing)))))
continue-template
(fn []
(let [mdata {:on-success on-template-cloned-success
:on-error on-template-cloned-error}
params {:project-id project-id :template-id (:id template)}]
(swap! state
(fn [state]
(-> state
(assoc :status :importing :importing-templates 1))))
(st/emit! (dd/clone-template (with-meta params mdata)))))
handle-continue
(mf/use-callback
(mf/deps project-id (:files @state))
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(if (some? template) (if (some? template)
(continue-template) (continue-template)
(continue-files)))) (continue-entries))))
handle-accept on-accept
(mf/use-callback (mf/use-fn
(mf/deps on-finish-import)
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(st/emit! (modal/hide)) (st/emit! (modal/hide))
(when on-finish-import (on-finish-import)))) (when (fn? on-finish-import)
(on-finish-import))))
files (->> (:files @state) (filterv (comp not :deleted?))) entries (filterv (comp not :deleted) entries)
num-importing (+ (count (filterv has-status-importing? entries))
(if (some? template) 1 0))
num-importing (+ success-num (if (some? template)
(->> files (filter #(= (:status %) :importing)) count) 1
(:importing-templates @state)) (count (filterv has-status-success? entries)))
warning-files (->> files (filter #(and (= (:status %) :import-finish) (d/not-empty? (:errors %)))) count) errors? (or (some has-status-error? entries)
success-files (->> files (filter #(and (= (:status %) :import-finish) (empty? (:errors %)))) count) (zero? (count entries)))
pending-analysis? (> (->> files (filter #(= (:status %) :analyzing)) count) 0)
pending-import? (> num-importing 0)
valid-files? (or (some? template)
(> (+ (->> files (filterv (fn [x] (not= (:status x) :analyze-error))) count)) 0))]
(mf/use-effect pending-analysis? (some has-status-analyzing? entries)
(fn [] pending-import? (pos? num-importing)
(let [sub (analyze-import files)] valid-all-entries? (or (some? template)
#(rx/dispose! sub)))) (not (some has-status-analyze-error? entries)))]
(mf/use-effect
(fn [] ;; Run analyze operation on component mount
;; dispose uris when the component is umount (mf/with-effect []
#(doseq [file files] (let [sub (analyze-entries entries* entries)]
(wapi/revoke-uri (:uri file))))) (partial rx/dispose! sub)))
[:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)} [:div {:class (stl/css :modal-container)}
@ -380,52 +455,58 @@
[:h2 {:class (stl/css :modal-title)} (tr "dashboard.import")] [:h2 {:class (stl/css :modal-title)} (tr "dashboard.import")]
[:button {:class (stl/css :modal-close-btn) [:button {:class (stl/css :modal-close-btn)
:on-click handle-cancel} i/close-refactor]] :on-click on-cancel} i/close-refactor]]
[:div {:class (stl/css :modal-content)} [:div {:class (stl/css :modal-content)}
(when (and (= :analyzing status) errors?)
[:& context-notification
{:type :warning
:content (tr "dashboard.import.import-warning")}])
(when (and (= :importing (:status @state)) (not pending-import?)) (when (and (= :importing status) (not ^boolean pending-import?))
(if (> warning-files 0) (cond
errors?
[:& context-notification [:& context-notification
{:type :warning {:type :warning
:content (tr "dashboard.import.import-warning" warning-files success-files)}] :content (tr "dashboard.import.import-warning")}]
:else
[:& context-notification [:& context-notification
{:type :success {:type :success
:content (tr "dashboard.import.import-message" (i18n/c (if (some? template) 1 success-files)))}])) :content (tr "dashboard.import.import-message" (i18n/c success-num))}]))
(for [file files] (for [entry entries]
(let [editing? (and (some? (:file-id file)) [:& import-entry {:edition edition
(= (:file-id file) (:editing @state)))] :key (dm/str (:uri entry))
[:& import-entry {:state state :entry entry
:key (dm/str (:uri file)) :entries entries
:file file :on-edit on-edit
:editing? editing? :on-change on-entry-change
:can-be-deleted? (> (count files) 1)}])) :on-delete on-entry-delete
:can-be-deleted (> (count entries) 1)}])
(when (some? template) (when (some? template)
[:& import-entry {:state state [:& import-entry {:entry (assoc template :status :ready)
:file (assoc template :status (if (= 1 (:importing-templates @state)) :importing :ready)) :can-be-deleted false}])]
:editing? false
:can-be-deleted? false}])]
[:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)} [:div {:class (stl/css :action-buttons)}
(when (= :analyzing (:status @state)) (when (= :analyzing status)
[:input {:class (stl/css :cancel-button) [:input {:class (stl/css :cancel-button)
:type "button" :type "button"
:value (tr "labels.cancel") :value (tr "labels.cancel")
:on-click handle-cancel}]) :on-click on-cancel}])
(when (= :analyzing (:status @state)) (when (and (= :analyzing status) (not errors?))
[:input {:class (stl/css :accept-btn) [:input {:class (stl/css :accept-btn)
:type "button" :type "button"
:value (tr "labels.continue") :value (tr "labels.continue")
:disabled (or pending-analysis? (not valid-files?)) :disabled (or pending-analysis? (not valid-all-entries?))
:on-click handle-continue}]) :on-click on-continue}])
(when (= :importing (:status @state)) (when (and (= :importing status) (not errors?))
[:input {:class (stl/css :accept-btn) [:input {:class (stl/css :accept-btn)
:type "button" :type "button"
:value (tr "labels.accept") :value (tr "labels.accept")
:disabled (or pending-import? (not valid-files?)) :disabled (or pending-import? (not valid-all-entries?))
:on-click handle-accept}])]]]])) :on-click on-accept}])]]]]))

View file

@ -33,6 +33,7 @@
grid-template-columns: 1fr; grid-template-columns: 1fr;
gap: $s-16; gap: $s-16;
margin-bottom: $s-24; margin-bottom: $s-24;
min-height: 40px;
} }
.action-buttons { .action-buttons {

View file

@ -91,4 +91,4 @@
(defmethod rc/render-release-notes "0.0" (defmethod rc/render-release-notes "0.0"
[params] [params]
(rc/render-release-notes (assoc params :version "2.0"))) (rc/render-release-notes (assoc params :version "1.21")))

View file

@ -11,6 +11,10 @@
[app.main.ui.releases.common :as c] [app.main.ui.releases.common :as c]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defmethod c/render-release-notes "1.21"
[data]
(c/render-release-notes (assoc data :verstion "2.0")))
;; TODO: Review all copies and alt text ;; TODO: Review all copies and alt text
(defmethod c/render-release-notes "2.0" (defmethod c/render-release-notes "2.0"
[{:keys [slide klass next finish navigate version]}] [{:keys [slide klass next finish navigate version]}]

View file

@ -168,7 +168,7 @@
childs (unchecked-get props "childs") childs (unchecked-get props "childs")
childs (cond-> childs childs (cond-> childs
(ctl/any-layout? shape) (ctl/any-layout? shape)
(cfh/sort-layout-children-z-index))] (ctl/sort-layout-children-z-index))]
[:> frame-container props [:> frame-container props
[:g.frame-children {:opacity (:opacity shape)} [:g.frame-children {:opacity (:opacity shape)}

View file

@ -8,6 +8,7 @@
(:require (:require
[app.common.colors :as cc] [app.common.colors :as cc]
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.text.styles :as sts] [app.main.ui.shapes.text.styles :as sts]
@ -169,16 +170,16 @@
[colors color-mapping color-mapping-inverse])) [colors color-mapping color-mapping-inverse]))
(mf/defc text-shape (mf/defc text-shape
{::mf/wrap-props false {::mf/props :obj
::mf/forward-ref true} ::mf/forward-ref true}
[props ref] [{:keys [shape grow-type]} ref]
(let [shape (obj/get props "shape") (let [transform (gsh/transform-str shape)
transform (gsh/transform-str shape) id (dm/get-prop shape :id)
x (dm/get-prop shape :x)
{:keys [id x y width height content]} shape y (dm/get-prop shape :y)
grow-type (obj/get props "grow-type") ;; This is only needed in workspace width (dm/get-prop shape :width)
;; We add 8px to add a padding for the exporter height (dm/get-prop shape :height)
;; width (+ width 8) content (get shape :content)
[colors _color-mapping color-mapping-inverse] (retrieve-colors shape)] [colors _color-mapping color-mapping-inverse] (retrieve-colors shape)]
@ -186,7 +187,7 @@
{:x x {:x x
:y y :y y
:id id :id id
:data-colors (->> colors (str/join ",")) :data-colors (str/join "," colors)
:data-mapping (-> color-mapping-inverse clj->js js/JSON.stringify) :data-mapping (-> color-mapping-inverse clj->js js/JSON.stringify)
:transform transform :transform transform
:width (if (#{:auto-width} grow-type) 100000 width) :width (if (#{:auto-width} grow-type) 100000 width)

View file

@ -27,6 +27,7 @@
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc sidebar-options (mf/defc sidebar-options
{::mf/props :obj}
[{:keys [from-viewer]}] [{:keys [from-viewer]}]
(let [{cmode :mode cshow :show} (mf/deref refs/comments-local) (let [{cmode :mode cshow :show} (mf/deref refs/comments-local)
update-mode update-mode
@ -67,6 +68,7 @@
[:span {:class (stl/css :icon)} i/tick-refactor]]])) [:span {:class (stl/css :icon)} i/tick-refactor]]]))
(mf/defc comments-sidebar (mf/defc comments-sidebar
{::mf/props :obj}
[{:keys [users threads page-id from-viewer]}] [{:keys [users threads page-id from-viewer]}]
(let [threads-map (mf/deref refs/threads-ref) (let [threads-map (mf/deref refs/threads-ref)
profile (mf/deref refs/profile) profile (mf/deref refs/profile)

View file

@ -110,9 +110,10 @@
(let [text (dom/get-value textarea)] (let [text (dom/get-value textarea)]
(when-not (str/blank? text) (when-not (str/blank? text)
(reset! editing* false) (reset! editing* false)
(st/emit! (dw/update-component-annotation component-id text))
(when ^boolean creating? (when ^boolean creating?
(st/emit! (dw/set-annotations-id-for-create nil))) (st/emit! (dw/set-annotations-id-for-create nil))))))))
(dw/update-component-annotation component-id text))))))
on-delete-annotation on-delete-annotation
(mf/use-fn (mf/use-fn

View file

@ -209,12 +209,12 @@
[:div {:class (stl/css :contraints-selects)} [:div {:class (stl/css :contraints-selects)}
[:div {:class (stl/css :horizontal-select)} [:div {:class (stl/css :horizontal-select)}
[:& select [:& select
{:default-value (d/name constraints-h "scale") {:default-value (d/nilv (d/name constraints-h) "scale")
:options options-h :options options-h
:on-change on-constraint-h-select-changed}]] :on-change on-constraint-h-select-changed}]]
[:div {:class (stl/css :vertical-select)} [:div {:class (stl/css :vertical-select)}
[:& select [:& select
{:default-value (d/name constraints-v "scale") {:default-value (d/nilv (d/name constraints-v) "scale")
:options options-v :options options-v
:on-change on-constraint-v-select-changed}]] :on-change on-constraint-v-select-changed}]]
(when first-level? (when first-level?

View file

@ -7,61 +7,72 @@
(ns app.main.ui.workspace.viewport.comments (ns app.main.ui.workspace.viewport.comments
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data.macros :as dm]
[app.main.data.comments :as dcm] [app.main.data.comments :as dcm]
[app.main.data.workspace.comments :as dwcm] [app.main.data.workspace.comments :as dwcm]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.comments :as cmt] [app.main.ui.comments :as cmt]
[cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defn- update-position
[positions {:keys [id] :as thread}]
(if (contains? positions id)
(-> thread
(assoc :position (dm/get-in positions [id :position]))
(assoc :frame-id (dm/get-in positions [id :frame-id])))
thread))
(mf/defc comments-layer (mf/defc comments-layer
{::mf/props :obj}
[{:keys [vbox vport zoom file-id page-id drawing] :as props}] [{:keys [vbox vport zoom file-id page-id drawing] :as props}]
(let [pos-x (* (- (:x vbox)) zoom) (let [vbox-x (dm/get-prop vbox :x)
pos-y (* (- (:y vbox)) zoom) vbox-y (dm/get-prop vbox :y)
vport-w (dm/get-prop vport :width)
vport-h (dm/get-prop vport :height)
profile (mf/deref refs/profile) pos-x (* (- vbox-x) zoom)
users (mf/deref refs/current-file-comments-users) pos-y (* (- vbox-y) zoom)
local (mf/deref refs/comments-local)
threads-position-ref (l/derived (l/in [:workspace-data :pages-index page-id :options :comment-threads-position]) st/state)
threads-position-map (mf/deref threads-position-ref)
threads-map (mf/deref refs/threads-ref)
update-thread-position (fn update-thread-position [thread] profile (mf/deref refs/profile)
(if (contains? threads-position-map (:id thread)) users (mf/deref refs/current-file-comments-users)
(-> thread local (mf/deref refs/comments-local)
(assoc :position (get-in threads-position-map [(:id thread) :position]))
(assoc :frame-id (get-in threads-position-map [(:id thread) :frame-id])))
thread))
threads (->> (vals threads-map) positions-ref
(filter #(= (:page-id %) page-id)) (mf/with-memo [page-id]
(mapv update-thread-position) (-> (l/in [:workspace-data :pages-index page-id :options :comment-threads-position])
(dcm/apply-filters local profile)) (l/derived st/state)))
positions (mf/deref positions-ref)
threads-map (mf/deref refs/threads-ref)
threads
(mf/with-memo [threads-map positions local profile]
(->> (vals threads-map)
(filter #(= (:page-id %) page-id))
(mapv (partial update-position positions))
(dcm/apply-filters local profile)))
on-draft-cancel on-draft-cancel
(mf/use-callback (mf/use-fn #(st/emit! :interrupt))
#(st/emit! :interrupt))
on-draft-submit on-draft-submit
(mf/use-callback (mf/use-fn
(fn [draft] (fn [draft]
(st/emit! (dcm/create-thread-on-workspace draft))))] (st/emit! (dcm/create-thread-on-workspace draft))))]
(mf/use-effect (mf/with-effect [file-id]
(mf/deps file-id) (st/emit! (dwcm/initialize-comments file-id))
(fn [] (fn [] (st/emit! ::dwcm/finalize)))
(st/emit! (dwcm/initialize-comments file-id))
(fn []
(st/emit! ::dwcm/finalize))))
[:div {:class (stl/css :comments-section)} [:div {:class (stl/css :comments-section)}
[:div [:div
{:class (stl/css :workspace-comments-container) {:class (stl/css :workspace-comments-container)
:style {:width (str (:width vport) "px") :style {:width (dm/str vport-w "px")
:height (str (:height vport) "px")}} :height (dm/str vport-h "px")}}
[:div {:class (stl/css :threads) [:div {:class (stl/css :threads)
:style {:transform (str/format "translate(%spx, %spx)" pos-x pos-y)}} :style {:transform (dm/fmt "translate(%px, %px)" pos-x pos-y)}}
(for [item threads] (for [item threads]
[:& cmt/thread-bubble {:thread item [:& cmt/thread-bubble {:thread item
:zoom zoom :zoom zoom
@ -70,7 +81,7 @@
(when-let [id (:open local)] (when-let [id (:open local)]
(when-let [thread (get threads-map id)] (when-let [thread (get threads-map id)]
[:& cmt/thread-comments {:thread (update-thread-position thread) [:& cmt/thread-comments {:thread (update-position positions thread)
:users users :users users
:zoom zoom}])) :zoom zoom}]))