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 _ _]
(let [{:keys [code] :as data} (ex-data err)]
(if (= code :method-not-allowed)
{::rres/status 405
::rres/body data}
{::rres/status 400 {::rres/status 400
::rres/body (ex-data err)}) ::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,8 +72,8 @@
(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))
@ -85,10 +86,17 @@
(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)
nil
(keyword? maybe-keyword) (keyword? maybe-keyword)
(c/name maybe-keyword) (c/name maybe-keyword)
(string? maybe-keyword) (string? maybe-keyword)
maybe-keyword maybe-keyword
(nil? maybe-keyword) default-value
: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,8 +49,9 @@
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)
(let [code (-> cause ex-data :code)]
(case code (case code
:profile-not-verified :profile-not-verified
(rx/of (msg/error (tr "auth.notifications.profile-not-verified"))) (rx/of (msg/error (tr "auth.notifications.profile-not-verified")))
@ -61,7 +62,7 @@
: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,7 +58,8 @@
: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]
(let [{:keys [type code]} (ex-data cause)]
(condp = [type code] (condp = [type code]
[:restriction :registration-disabled] [:restriction :registration-disabled]
(st/emit! (msg/error (tr "errors.registration-disabled"))) (st/emit! (msg/error (tr "errors.registration-disabled")))
@ -78,13 +79,12 @@
(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)
(fn []
(when team (when team
(let [tname (if (:is-default team) (let [tname (if (:is-default team)
(tr "dashboard.your-penpot") (tr "dashboard.your-penpot")
(:name team))] (:name team))]
(case section (case section
:fonts (dom/set-html-title (tr "title.dashboard.fonts" tname)) :fonts (dom/set-html-title (tr "title.dashboard.fonts" tname))
:providers (dom/set-html-title (tr "title.dashboard.font-providers" 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*)
font-vals (mf/with-memo [fonts]
(->> fonts
(into [] (map val))
(not-empty)))
team-id (:id team)
input-ref (mf/use-ref) input-ref (mf/use-ref)
uploading (mf/use-state #{})
bad-font-family-tmp? uploading* (mf/use-state #{})
(mf/use-fn uploading (deref uploading*)
(fn [font]
(and (contains? font :font-family-tmp)
(str/blank? (:font-family-tmp font)))))
disable-upload-all? (some bad-font-family-tmp? (vals fonts)) disable-upload-all?
(some bad-font-family-tmp? fonts)
handle-click 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)
(fn [event]
(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) (when-not (str/blank? name)
(swap! fonts* df/rename-and-regroup id name installed-fonts)))) (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
:btn-primary true
:disabled disable-upload-all?) :disabled disable-upload-all?)
:on-click handle-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)
:class (stl/css-case
:btn-primary true
:upload-button true :upload-button true
:disabled disable-upload?) :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
delete-variant-fn
(mf/use-callback
(fn [id]
(st/emit! (df/delete-font-variant id))))
on-delete
(mf/use-callback
(mf/deps delete-font-fn)
(fn []
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.delete-font.title") :title (tr "modals.delete-font.title")
:message (tr "modals.delete-font.message") :message (tr "modals.delete-font.message")
:accept-label (tr "labels.delete") :accept-label (tr "labels.delete")
:on-accept (fn [_props] (delete-font-fn))})))) :on-accept (fn [_props]
(st/emit! (df/delete-font font-id)))}]
(st/emit! (modal/show options)))))
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))
options {:type :confirm
:title (tr "modals.delete-font-variant.title") :title (tr "modals.delete-font-variant.title")
:message (tr "modals.delete-font-variant.message") :message (tr "modals.delete-font-variant.message")
:accept-label (tr "labels.delete") :accept-label (tr "labels.delete")
:on-accept (fn [_props] :on-accept (fn [_props]
(delete-variant-fn id))}))))] (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
(fn [file]
{:name (.-name file) {:name (.-name file)
:uri (wapi/create-uri file)})))] :uri (wapi/create-uri file)}))
(not-empty))]
(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))
(map (fn [[file-id file-data]]
(-> file-data (-> file-data
(assoc :file-id file-id (assoc :file-id file-id)
:status :ready (assoc :status :ready)
:uri uri (assoc :uri uri)
:type type))))) (assoc :type type)))))
[file]))] [entry]))]
(into [] (mapcat replace-file) files))) (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,47 +154,111 @@
(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'
(mf/use-fn
(mf/deps file-id on-delete)
(fn [event]
(when (fn? on-delete)
(on-delete file-id event))))]
[:div {:class (stl/css-case
:file-entry true
:loading loading? :loading loading?
:success (and import-finish? (not import-warn?) (not import-error?)) :success (and import-finish? (not import-warn?) (not import-error?))
:warning (and import-finish? import-warn? (not import-error?)) :warning (and import-finish? import-warn? (not import-error?))
@ -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*)
(fn [event]
(when (nil? (:editing @state)) status* (mf/use-state :analyzing)
(dom/prevent-default event) status (deref status*)
(st/emit! (modal/hide)))))
edition* (mf/use-state nil)
edition (deref edition*)
on-template-cloned-success on-template-cloned-success
(mf/use-fn
(fn [] (fn []
(swap! state assoc :status :importing :importing-templates 0) (swap! status* (constantly :importing))
(st/emit! (dd/fetch-recent-files))) ;; (swap! state assoc :status :importing :importing-templates 0)
(st/emit! (dd/fetch-recent-files))))
on-template-cloned-error on-template-cloned-error
(mf/use-fn
(fn [cause] (fn [cause]
(swap! state assoc :status :error :importing-templates 0) (swap! status* (constantly :error))
;; (swap! state assoc :status :error :importing-templates 0)
(errors/print-error! cause) (errors/print-error! cause)
(rx/of (modal/hide) (rx/of (modal/hide)
(msg/error (tr "dashboard.libraries-and-templates.import-error")))) (msg/error (tr "dashboard.libraries-and-templates.import-error")))))
continue-files continue-entries
(mf/use-fn
(mf/deps entries)
(fn [] (fn []
(let [files (->> @state :files (filterv #(and (= :ready (:status %)) (not (:deleted? %)))))] (let [entries (filterv has-status-ready? entries)]
(import-files project-id files)) (swap! status* (constantly :importing))
(swap! entries* mark-entries-importing)
(swap! state (import-files! entries* project-id entries))))
(fn [state]
(-> state
(assoc :status :importing)
(update :files mark-files-importing)))))
continue-template continue-template
(mf/use-fn
(mf/deps on-template-cloned-success
on-template-cloned-error
template)
(fn [] (fn []
(let [mdata {:on-success on-template-cloned-success (let [mdata {:on-success on-template-cloned-success
:on-error on-template-cloned-error} :on-error on-template-cloned-error}
params {:project-id project-id :template-id (:id template)}] params {:project-id project-id :template-id (:id template)}]
(swap! state (swap! status* (constantly :importing))
(fn [state] (st/emit! (dd/clone-template (with-meta params mdata))))))
(-> state
(assoc :status :importing :importing-templates 1))))
(st/emit! (dd/clone-template (with-meta params mdata)))))
on-edit
(mf/use-fn
(fn [file-id _event]
(swap! edition* (constantly file-id))))
handle-continue on-entry-change
(mf/use-callback (mf/use-fn
(mf/deps project-id (:files @state)) (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]
(when (nil? edition)
(dom/prevent-default event)
(st/emit! (modal/hide)))))
on-continue
(mf/use-fn
(mf/deps template
continue-template
continue-entries)
(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?)
(when (and (= :importing (:status @state)) (not pending-import?))
(if (> warning-files 0)
[:& context-notification [:& context-notification
{:type :warning {:type :warning
:content (tr "dashboard.import.import-warning" warning-files success-files)}] :content (tr "dashboard.import.import-warning")}])
(when (and (= :importing status) (not ^boolean pending-import?))
(cond
errors?
[:& context-notification
{:type :warning
: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)
pos-x (* (- vbox-x) zoom)
pos-y (* (- vbox-y) zoom)
profile (mf/deref refs/profile) profile (mf/deref refs/profile)
users (mf/deref refs/current-file-comments-users) users (mf/deref refs/current-file-comments-users)
local (mf/deref refs/comments-local) 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) positions-ref
(mf/with-memo [page-id]
(-> (l/in [:workspace-data :pages-index page-id :options :comment-threads-position])
(l/derived st/state)))
positions (mf/deref positions-ref)
threads-map (mf/deref refs/threads-ref) threads-map (mf/deref refs/threads-ref)
update-thread-position (fn update-thread-position [thread] threads
(if (contains? threads-position-map (:id thread)) (mf/with-memo [threads-map positions local profile]
(-> thread (->> (vals threads-map)
(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)
(filter #(= (:page-id %) page-id)) (filter #(= (:page-id %) page-id))
(mapv update-thread-position) (mapv (partial update-position positions))
(dcm/apply-filters local profile)) (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)
(fn []
(st/emit! (dwcm/initialize-comments file-id)) (st/emit! (dwcm/initialize-comments file-id))
(fn [] (fn [] (st/emit! ::dwcm/finalize)))
(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}]))