Merge pull request #5088 from penpot/niwinz-bugfix-2

🐛 Fix issues related to invalid colors inserted on shape shadow
This commit is contained in:
Alejandro 2024-09-17 13:58:43 +02:00 committed by GitHub
commit e65c0d9f48
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 449 additions and 222 deletions

View file

@ -6,4 +6,4 @@
(ns app.common.files.defaults) (ns app.common.files.defaults)
(def version 52) (def version 54)

View file

@ -863,11 +863,9 @@
(assoc shadow :color color))) (assoc shadow :color color)))
(update-object [object] (update-object [object]
(d/update-when object :shadow (let [xform (comp (map fix-shadow)
#(into [] (filter valid-shadow?))]
(comp (map fix-shadow) (d/update-when object :shadow #(into [] xform %))))
(filter valid-shadow?))
%)))
(update-container [container] (update-container [container]
(d/update-when container :objects update-vals update-object))] (d/update-when container :objects update-vals update-object))]
@ -1029,6 +1027,25 @@
(update data :pages-index update-vals update-page))) (update data :pages-index update-vals update-page)))
(defn migrate-up-54
"Fixes shapes with invalid colors in shadow: it first tries a non
destructive fix, and if it is not possible, then, shadow is removed"
[data]
(letfn [(fix-shadow [shadow]
(update shadow :color d/without-nils))
(update-shape [shape]
(let [xform (comp (map fix-shadow)
(filter valid-shadow?))]
(d/update-when shape :shadow #(into [] xform %))))
(update-container [container]
(d/update-when container :objects update-vals update-shape))]
(-> data
(update :pages-index update-vals update-container)
(update :components update-vals update-container))))
(def migrations (def migrations
"A vector of all applicable migrations" "A vector of all applicable migrations"
[{:id 2 :migrate-up migrate-up-2} [{:id 2 :migrate-up migrate-up-2}
@ -1072,4 +1089,6 @@
{:id 49 :migrate-up migrate-up-49} {:id 49 :migrate-up migrate-up-49}
{:id 50 :migrate-up migrate-up-50} {:id 50 :migrate-up migrate-up-50}
{:id 51 :migrate-up migrate-up-51} {:id 51 :migrate-up migrate-up-51}
{:id 52 :migrate-up migrate-up-52}]) {:id 52 :migrate-up migrate-up-52}
{:id 53 :migrate-up migrate-up-26}
{:id 54 :migrate-up migrate-up-54}])

View file

@ -80,9 +80,8 @@
[:opacity {:optional true} [:maybe ::sm/safe-number]] [:opacity {:optional true} [:maybe ::sm/safe-number]]
[:offset ::sm/safe-number]]]]]) [:offset ::sm/safe-number]]]]])
(def schema:color (def schema:color-attrs
[:and [:map {:title "ColorAttrs"}
[:map {:title "Color"}
[:id {:optional true} ::sm/uuid] [:id {:optional true} ::sm/uuid]
[:name {:optional true} :string] [:name {:optional true} :string]
[:path {:optional true} [:maybe :string]] [:path {:optional true} [:maybe :string]]
@ -94,7 +93,10 @@
[:ref-file {:optional true} ::sm/uuid] [:ref-file {:optional true} ::sm/uuid]
[:gradient {:optional true} [:maybe schema:gradient]] [:gradient {:optional true} [:maybe schema:gradient]]
[:image {:optional true} [:maybe schema:image-color]] [:image {:optional true} [:maybe schema:image-color]]
[:plugin-data {:optional true} ::ctpg/plugin-data]] [:plugin-data {:optional true} ::ctpg/plugin-data]])
(def schema:color
[:and schema:color-attrs
[::sm/contains-any {:strict true} [:color :gradient :image]]]) [::sm/contains-any {:strict true} [:color :gradient :image]]])
(def schema:recent-color (def schema:recent-color
@ -111,12 +113,13 @@
(sm/register! ::gradient schema:gradient) (sm/register! ::gradient schema:gradient)
(sm/register! ::image-color schema:image-color) (sm/register! ::image-color schema:image-color)
(sm/register! ::recent-color schema:recent-color) (sm/register! ::recent-color schema:recent-color)
(sm/register! ::color-attrs schema:color-attrs)
(def valid-color? (def check-color!
(sm/lazy-validator schema:color)) (sm/check-fn schema:color))
(def valid-recent-color? (def check-recent-color!
(sm/lazy-validator schema:recent-color)) (sm/check-fn schema:recent-color))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS ;; HELPERS

View file

@ -125,6 +125,9 @@
(sm/register! ::stroke schema:stroke) (sm/register! ::stroke schema:stroke)
(def check-stroke!
(sm/check-fn schema:stroke))
(def schema:shape-base-attrs (def schema:shape-base-attrs
[:map {:title "ShapeMinimalRecord"} [:map {:title "ShapeMinimalRecord"}
[:id ::sm/uuid] [:id ::sm/uuid]

View file

@ -27,3 +27,6 @@
[:color ::ctc/color]]) [:color ::ctc/color]])
(sm/register! ::shadow schema:shadow) (sm/register! ::shadow schema:shadow)
(def check-shadow!
(sm/check-fn schema:shadow))

View file

@ -143,4 +143,3 @@
(reinit)))) (reinit))))
(set! (.-stackTraceLimit js/Error) 50) (set! (.-stackTraceLimit js/Error) 50)

View file

@ -171,6 +171,7 @@
(let [team-id (get-current-team-id profile) (let [team-id (get-current-team-id profile)
welcome-file-id (dm/get-in profile [:props :welcome-file-id]) welcome-file-id (dm/get-in profile [:props :welcome-file-id])
redirect-href (:login-redirect @s/session)] redirect-href (:login-redirect @s/session)]
(cond (cond
(some? redirect-href) (some? redirect-href)
(binding [s/*sync* true] (binding [s/*sync* true]

View file

@ -12,6 +12,9 @@
[app.common.files.helpers :as cfh] [app.common.files.helpers :as cfh]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.text :as txt] [app.common.text :as txt]
[app.common.types.color :as ctc]
[app.common.types.shape :refer [check-stroke!]]
[app.common.types.shape.shadow :refer [check-shadow!]]
[app.main.broadcast :as mbc] [app.main.broadcast :as mbc]
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.modal :as md] [app.main.data.modal :as md]
@ -21,7 +24,6 @@
[app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.state-helpers :as wsh]
[app.main.data.workspace.texts :as dwt] [app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]
[app.util.color :as uc]
[app.util.storage :refer [storage]] [app.util.storage :refer [storage]]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cuerdas.core :as str] [cuerdas.core :as str]
@ -165,6 +167,15 @@
(defn add-fill (defn add-fill
[ids color] [ids color]
(dm/assert!
"expected a valid color struct"
(ctc/check-color! color))
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::add-fill (ptk/reify ::add-fill
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
@ -175,6 +186,15 @@
(defn remove-fill (defn remove-fill
[ids color position] [ids color position]
(dm/assert!
"expected a valid color struct"
(ctc/check-color! color))
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::remove-fill (ptk/reify ::remove-fill
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
@ -187,13 +207,21 @@
(defn remove-all-fills (defn remove-all-fills
[ids color] [ids color]
(dm/assert!
"expected a valid color struct"
(ctc/check-color! color))
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::remove-all-fills (ptk/reify ::remove-all-fills
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [remove-all (fn [shape _] (assoc shape :fills []))] (let [remove-all (fn [shape _] (assoc shape :fills []))]
(transform-fill state ids color remove-all))))) (transform-fill state ids color remove-all)))))
(defn change-hide-fill-on-export (defn change-hide-fill-on-export
[ids hide-fill-on-export] [ids hide-fill-on-export]
(ptk/reify ::change-hide-fill-on-export (ptk/reify ::change-hide-fill-on-export
@ -273,16 +301,24 @@
;; multiple shapes) let's use the first stop ;; multiple shapes) let's use the first stop
;; color ;; color
attrs (cond-> attrs attrs (cond-> attrs
(:gradient attrs) (get-in [:gradient :stops 0])) (:gradient attrs)
new-attrs (-> (merge (get-in shape [:shadow index :color]) attrs) (dm/get-in [:gradient :stops 0]))
attrs' (-> (dm/get-in shape [:shadow index :color])
(merge attrs)
(d/without-nils))] (d/without-nils))]
(assoc-in shape [:shadow index :color] new-attrs)))))))) (assoc-in shape [:shadow index :color] attrs'))))))))
(defn add-shadow (defn add-shadow
[ids shadow] [ids shadow]
(dm/assert!
"expected a valid shadow struct"
(check-shadow! shadow))
(dm/assert! (dm/assert!
"expected a valid coll of uuid's" "expected a valid coll of uuid's"
(sm/check-coll-of-uuid! ids)) (every? uuid? ids))
(ptk/reify ::add-shadow (ptk/reify ::add-shadow
ptk/WatchEvent ptk/WatchEvent
@ -293,6 +329,15 @@
(defn add-stroke (defn add-stroke
[ids stroke] [ids stroke]
(dm/assert!
"expected a valid stroke struct"
(check-stroke! stroke))
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::add-stroke (ptk/reify ::add-stroke
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
@ -301,6 +346,11 @@
(defn remove-stroke (defn remove-stroke
[ids position] [ids position]
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::remove-stroke (ptk/reify ::remove-stroke
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
@ -314,6 +364,11 @@
(defn remove-all-strokes (defn remove-all-strokes
[ids] [ids]
(dm/assert!
"expected a valid coll of uuid's"
(every? uuid? ids))
(ptk/reify ::remove-all-strokes (ptk/reify ::remove-all-strokes
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
@ -376,7 +431,7 @@
:on-change handle-change-color} :on-change handle-change-color}
:allow-click-outside true}))))))) :allow-click-outside true})))))))
(defn color-att->text (defn- color-att->text
[color] [color]
{:fill-color (when (:color color) (str/lower (:color color))) {:fill-color (when (:color color) (str/lower (:color color)))
:fill-opacity (:opacity color) :fill-opacity (:opacity color)
@ -395,26 +450,57 @@
(some? has-color?) (some? has-color?)
(assoc-in [:fills index] parsed-new-color)))) (assoc-in [:fills index] parsed-new-color))))
(def ^:private schema:change-color-operation
[:map
[:prop [:enum :fill :stroke :shadow :content]]
[:shape-id ::sm/uuid]
[:index :int]])
(def ^:private schema:change-color-operations
[:vector schema:change-color-operation])
(def ^:private check-change-color-operations!
(sm/check-fn schema:change-color-operations))
(defn change-color-in-selected (defn change-color-in-selected
[new-color shapes-by-color old-color] [operations new-color old-color]
(dm/verify!
"expected valid change color operations"
(check-change-color-operations! operations))
(dm/verify!
"expected a valid color struct for new-color param"
(ctc/check-color! new-color))
(dm/verify!
"expected a valid color struct for old-color param"
(ctc/check-color! old-color))
(ptk/reify ::change-color-in-selected (ptk/reify ::change-color-in-selected
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(let [undo-id (js/Symbol)] (let [undo-id (js/Symbol)]
(rx/concat (rx/concat
(rx/of (dwu/start-undo-transaction undo-id)) (rx/of (dwu/start-undo-transaction undo-id))
(->> (rx/from shapes-by-color) (->> (rx/from operations)
(rx/map (fn [shape] (case (:prop shape) (rx/map (fn [{:keys [shape-id index] :as operation}]
:fill (change-fill [(:shape-id shape)] new-color (:index shape)) (case (:prop operation)
:stroke (change-stroke [(:shape-id shape)] new-color (:index shape)) :fill (change-fill [shape-id] new-color index)
:shadow (change-shadow [(:shape-id shape)] new-color (:index shape)) :stroke (change-stroke [shape-id] new-color index)
:shadow (change-shadow [shape-id] new-color index)
:content (dwt/update-text-with-function :content (dwt/update-text-with-function
(:shape-id shape) shape-id
(partial change-text-color old-color new-color (:index shape))))))) (partial change-text-color old-color new-color index))))))
(rx/of (dwu/commit-undo-transaction undo-id))))))) (rx/of (dwu/commit-undo-transaction undo-id)))))))
(defn apply-color-from-palette (defn apply-color-from-palette
[color stroke?] [color stroke?]
(dm/assert!
"should be a valid color"
(ctc/check-color! color))
(ptk/reify ::apply-color-from-palette (ptk/reify ::apply-color-from-palette
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
@ -437,9 +523,10 @@
result (cond-> result (not group?) (conj cur))] result (cond-> result (not group?) (conj cur))]
(recur (rest pending) result))))] (recur (rest pending) result))))]
(if stroke? (if stroke?
(rx/of (change-stroke ids (merge uc/empty-color color) 0)) (rx/of (change-stroke ids color 0))
(rx/of (change-fill ids (merge uc/empty-color color) 0))))))) (rx/of (change-fill ids color 0)))))))
(declare activate-colorpicker-color) (declare activate-colorpicker-color)
(declare activate-colorpicker-gradient) (declare activate-colorpicker-gradient)
@ -448,15 +535,22 @@
(defn apply-color-from-colorpicker (defn apply-color-from-colorpicker
[color] [color]
(dm/assert!
"expected valid color structure"
(ctc/check-color! color))
(ptk/reify ::apply-color-from-colorpicker (ptk/reify ::apply-color-from-colorpicker
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
;; FIXME: revisit this
(let [gradient-type (dm/get-in color [:gradient :type])]
(rx/of (rx/of
(cond (cond
(:image color) (activate-colorpicker-image) (:image color) (activate-colorpicker-image)
(:color color) (activate-colorpicker-color) (:color color) (activate-colorpicker-color)
(= :linear (get-in color [:gradient :type])) (activate-colorpicker-gradient :linear-gradient) (= :linear gradient-type) (activate-colorpicker-gradient :linear-gradient)
(= :radial (get-in color [:gradient :type])) (activate-colorpicker-gradient :radial-gradient)))))) (= :radial gradient-type) (activate-colorpicker-gradient :radial-gradient)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -596,7 +690,8 @@
(update :current-color merge changes) (update :current-color merge changes)
(update :current-color materialize-color-components) (update :current-color materialize-color-components)
(update :current-color #(if (not= type :image) (dissoc % :image) %)) (update :current-color #(if (not= type :image) (dissoc % :image) %))
;; current color can be a library one I'm changing via colorpicker ;; current color can be a library one
;; I'm changing via colorpicker
(d/dissoc-in [:current-color :id]) (d/dissoc-in [:current-color :id])
(d/dissoc-in [:current-color :file-id]))] (d/dissoc-in [:current-color :file-id]))]
(if-let [stop (:editing-stop state)] (if-let [stop (:editing-stop state)]
@ -614,7 +709,8 @@
:colorpicker :colorpicker
:type) :type)
formated-color (get-color-from-colorpicker-state (:colorpicker state)) formated-color (get-color-from-colorpicker-state (:colorpicker state))
;; Type is set to color on closing the colorpicker, but we can can close it while still uploading an image fill ;; Type is set to color on closing the colorpicker, but we
;; can can close it while still uploading an image fill
ignore-color? (and (= selected-type :color) (nil? (:color formated-color)))] ignore-color? (and (= selected-type :color) (nil? (:color formated-color)))]
(when (and add-recent? (not ignore-color?)) (when (and add-recent? (not ignore-color?))
(rx/of (dwl/add-recent-color formated-color))))))) (rx/of (dwl/add-recent-color formated-color)))))))
@ -686,6 +782,7 @@
(defn select-color (defn select-color
[position add-color] [position add-color]
;; FIXME: revisit
(ptk/reify ::select-color (ptk/reify ::select-color
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]

View file

@ -116,8 +116,13 @@
(update :id #(or % (uuid/next))) (update :id #(or % (uuid/next)))
(assoc :name (or (get-in color [:image :name]) (assoc :name (or (get-in color [:image :name])
(:color color) (:color color)
(uc/gradient-type->string (get-in color [:gradient :type])))))] (uc/gradient-type->string (get-in color [:gradient :type]))))
(dm/assert! ::ctc/color color) (d/without-nils))]
(dm/assert!
"expect valid color structure"
(ctc/check-color! color))
(ptk/reify ::add-color (ptk/reify ::add-color
ev/Event ev/Event
(-data [_] color) (-data [_] color)
@ -135,8 +140,8 @@
[color] [color]
(dm/assert! (dm/assert!
"expected valid recent color map" "expected valid recent color structure"
(ctc/valid-recent-color? color)) (ctc/check-recent-color! color))
(ptk/reify ::add-recent-color (ptk/reify ::add-recent-color
ptk/UpdateEvent ptk/UpdateEvent
@ -155,7 +160,7 @@
(update [_ state] (update [_ state]
(assoc-in state [:workspace-local :color-for-rename] nil)))) (assoc-in state [:workspace-local :color-for-rename] nil))))
(defn- do-update-color (defn- update-color*
[it state color file-id] [it state color file-id]
(let [data (get state :workspace-data) (let [data (get state :workspace-data)
[path name] (cfh/parse-path-name (:name color)) [path name] (cfh/parse-path-name (:name color))
@ -171,10 +176,11 @@
(defn update-color (defn update-color
[color file-id] [color file-id]
(let [color (d/without-nils color)]
(dm/assert! (dm/assert!
"expected valid parameters" "expected valid color data structure"
(ctc/valid-color? color)) (ctc/check-color! color))
(dm/assert! (dm/assert!
"expected file-id" "expected file-id"
@ -183,7 +189,7 @@
(ptk/reify ::update-color (ptk/reify ::update-color
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(do-update-color it state color file-id)))) (update-color* it state color file-id)))))
(defn rename-color (defn rename-color
[file-id id new-name] [file-id id new-name]
@ -198,9 +204,10 @@
(if (str/empty? new-name) (if (str/empty? new-name)
(rx/empty) (rx/empty)
(let [data (get state :workspace-data) (let [data (get state :workspace-data)
object (get-in data [:colors id]) color (get-in data [:colors id])
object (assoc object :name new-name)] color (assoc color :name new-name)
(do-update-color it state object file-id))))))) color (d/without-nils color)]
(update-color* it state color file-id)))))))
(defn delete-color (defn delete-color
[{:keys [id] :as params}] [{:keys [id] :as params}]

View file

@ -42,7 +42,8 @@
(mf/lazy-component app.main.ui.workspace/workspace)) (mf/lazy-component app.main.ui.workspace/workspace))
(mf/defc main-page (mf/defc main-page
{::mf/props :obj} {::mf/props :obj
::mf/private true}
[{:keys [route profile]}] [{:keys [route profile]}]
(let [{:keys [data params]} route (let [{:keys [data params]} route
props (get profile :props) props (get profile :props)
@ -68,6 +69,7 @@
(:onboarding-viewed props) (:onboarding-viewed props)
(not= (:release-notes-viewed props) (:main cf/version)) (not= (:release-notes-viewed props) (:main cf/version))
(not= "0.0" (:main cf/version)))] (not= "0.0" (:main cf/version)))]
[:& (mf/provider ctx/current-route) {:value route} [:& (mf/provider ctx/current-route) {:value route}
(case (:name data) (case (:name data)
(:auth-login (:auth-login

View file

@ -42,9 +42,7 @@
(let [search-term (get-in route [:params :query :search-term]) (let [search-term (get-in route [:params :query :search-term])
team-id (get-in route [:params :path :team-id]) team-id (get-in route [:params :path :team-id])
project-id (get-in route [:params :path :project-id])] project-id (get-in route [:params :path :project-id])]
(cond-> (cond-> {:search-term search-term}
{:search-term search-term}
(uuid-str? team-id) (uuid-str? team-id)
(assoc :team-id (uuid team-id)) (assoc :team-id (uuid team-id))
@ -84,10 +82,10 @@
(mf/use-effect on-resize) (mf/use-effect on-resize)
[:div {:class (stl/css :dashboard-content) [:div {:class (stl/css :dashboard-content)
:style {:pointer-events (when file-menu-open? "none")} :style {:pointer-events (when file-menu-open? "none")}
:on-click clear-selected-fn :ref container} :on-click clear-selected-fn
:ref container}
(case section (case section
:dashboard-projects :dashboard-projects
[:* [:*
@ -146,7 +144,8 @@
(l/derived :current-team-id st/state)) (l/derived :current-team-id st/state))
(mf/defc dashboard (mf/defc dashboard
[{:keys [route profile] :as props}] {::mf/props :obj}
[{:keys [route profile]}]
(let [section (get-in route [:data :name]) (let [section (get-in route [:data :name])
params (parse-params route) params (parse-params route)

View file

@ -260,6 +260,14 @@
(when ^boolean obj (when ^boolean obj
(apply (.-f obj) args))))))) (apply (.-f obj) args)))))))
(defn use-ref-value
"Returns a ref that will be automatically updated when the value is changed"
[v]
(let [ref (mf/use-ref v)]
(mf/with-effect [v]
(mf/set-ref-val! ref v))
ref))
(defn use-equal-memo (defn use-equal-memo
[val] [val]
(let [ref (mf/use-ref nil)] (let [ref (mf/use-ref nil)]

View file

@ -106,9 +106,9 @@
(st/emit! (rt/navigated match)) (st/emit! (rt/navigated match))
:else :else
;; We just recheck with an additional profile request; this avoids ;; We just recheck with an additional profile request; this
;; some race conditions that causes unexpected redirects on ;; avoids some race conditions that causes unexpected redirects
;; invitations workflows (and probably other cases). ;; on invitations workflows (and probably other cases).
(->> (rp/cmd! :get-profile) (->> (rp/cmd! :get-profile)
(rx/subs! (fn [{:keys [id] :as profile}] (rx/subs! (fn [{:keys [id] :as profile}]
(cond (cond

View file

@ -7,19 +7,21 @@
(ns app.main.ui.static (ns app.main.ui.static
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
["rxjs" :as rxjs]
[app.common.data :as d] [app.common.data :as d]
[app.common.pprint :as pp] [app.common.pprint :as pp]
[app.common.uri :as u] [app.common.uri :as u]
[app.main.data.common :as dc] [app.main.data.common :as dc]
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.refs :as refs]
[app.main.repo :as rp] [app.main.repo :as rp]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.auth.login :refer [login-methods]] [app.main.ui.auth.login :refer [login-methods]]
[app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]] [app.main.ui.auth.recovery-request :refer [recovery-request-page recovery-sent-page]]
[app.main.ui.auth.register :refer [register-methods register-validate-form register-success-page terms-register]] [app.main.ui.auth.register :as register]
[app.main.ui.dashboard.sidebar :refer [sidebar]] [app.main.ui.dashboard.sidebar :refer [sidebar]]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.viewer.header :as header] [app.main.ui.viewer.header :as viewer.header]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.router :as rt] [app.util.router :as rt]
@ -29,10 +31,14 @@
[potok.v2.core :as ptk] [potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
;; FIXME: this is a workaround until we export this class on beicon library
(def TimeoutError rxjs/TimeoutError)
(mf/defc error-container* (mf/defc error-container*
{::mf/props :obj} {::mf/props :obj}
[{:keys [children]}] [{:keys [children]}]
(let [profile-id (:profile-id @st/state)] (let [profile-id (:profile-id @st/state)
on-nav-root (mf/use-fn #(st/emit! (rt/nav-root)))]
[:section {:class (stl/css :exception-layout)} [:section {:class (stl/css :exception-layout)}
[:button [:button
{:class (stl/css :exception-header) {:class (stl/css :exception-header)
@ -44,7 +50,7 @@
[:div {:class (stl/css :deco-before)} i/logo-error-screen] [:div {:class (stl/css :deco-before)} i/logo-error-screen]
(when-not profile-id (when-not profile-id
[:button {:class (stl/css :login-header) [:button {:class (stl/css :login-header)
:on-click rt/nav-root} :on-click on-nav-root}
(tr "labels.login")]) (tr "labels.login")])
[:div {:class (stl/css :exception-content)} [:div {:class (stl/css :exception-content)}
@ -85,29 +91,37 @@
#(reset! current-section :login)) #(reset! current-section :login))
success-login success-login
(mf/use-fn
(fn [] (fn []
(reset! show-dialog false) (reset! show-dialog false)
(.reload js/window.location true)) (st/emit! (rt/reload true))))
success-register success-register
(mf/use-fn
(fn [data] (fn [data]
(reset! register-token (:token data)) (reset! register-token (:token data))
(reset! current-section :register-validate)) (reset! current-section :register-validate)))
register-email-sent register-email-sent
(mf/use-fn
(fn [email] (fn [email]
(reset! user-email email) (reset! user-email email)
(reset! current-section :register-email-sent)) (reset! current-section :register-email-sent)))
recovery-email-sent recovery-email-sent
(mf/use-fn
(fn [email] (fn [email]
(reset! user-email email) (reset! user-email email)
(reset! current-section :recovery-email-sent))] (reset! current-section :recovery-email-sent)))
on-nav-root
(mf/use-fn #(st/emit! (rt/nav-root)))]
[:div {:class (stl/css :overlay)} [:div {:class (stl/css :overlay)}
[:div {:class (stl/css :dialog-login)} [:div {:class (stl/css :dialog-login)}
[:div {:class (stl/css :modal-close)} [:div {:class (stl/css :modal-close)}
[:button {:class (stl/css :modal-close-button) :on-click rt/nav-root} [:button {:class (stl/css :modal-close-button)
:on-click on-nav-root}
i/close]] i/close]]
[:div {:class (stl/css :login)} [:div {:class (stl/css :login)}
[:div {:class (stl/css :logo)} i/logo] [:div {:class (stl/css :logo)} i/logo]
@ -125,13 +139,14 @@
(tr "auth.register") (tr "auth.register")
" " " "
[:a {:data-section "register" [:a {:data-section "register"
:on-click set-section} (tr "auth.register-submit")]]] :on-click set-section}
(tr "auth.register-submit")]]]
:register :register
[:* [:*
[:div {:class (stl/css :logo-title)} (tr "not-found.login.signup-free")] [:div {:class (stl/css :logo-title)} (tr "not-found.login.signup-free")]
[:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.start-using")] [:div {:class (stl/css :logo-subtitle)} (tr "not-found.login.start-using")]
[:& register-methods {:on-success-callback success-register :hide-separator true}] [:& register/register-methods {:on-success-callback success-register :hide-separator true}]
#_[:hr {:class (stl/css :separator)}] #_[:hr {:class (stl/css :separator)}]
[:div {:class (stl/css :separator)}] [:div {:class (stl/css :separator)}]
[:div {:class (stl/css :change-section)} [:div {:class (stl/css :change-section)}
@ -141,11 +156,12 @@
:on-click set-section} (tr "auth.login-here")]] :on-click set-section} (tr "auth.login-here")]]
[:div {:class (stl/css :links)} [:div {:class (stl/css :links)}
[:hr {:class (stl/css :separator)}] [:hr {:class (stl/css :separator)}]
[:& terms-register]]] [:& register/terms-register]]]
:register-validate :register-validate
[:div {:class (stl/css :form-container)} [:div {:class (stl/css :form-container)}
[:& register-validate-form {:params {:token @register-token} [:& register/register-validate-form
{:params {:token @register-token}
:on-success-callback register-email-sent}] :on-success-callback register-email-sent}]
[:div {:class (stl/css :links)} [:div {:class (stl/css :links)}
[:div {:class (stl/css :register)} [:div {:class (stl/css :register)}
@ -155,7 +171,7 @@
:register-email-sent :register-email-sent
[:div {:class (stl/css :form-container)} [:div {:class (stl/css :form-container)}
[:& register-success-page {:params {:email @user-email :hide-logo true}}]] [:& register/register-success-page {:params {:email @user-email :hide-logo true}}]]
:recovery-request :recovery-request
[:& recovery-request-page {:go-back-callback set-section-login [:& recovery-request-page {:go-back-callback set-section-login
@ -175,40 +191,49 @@
[:button {:class (stl/css :modal-close-button) :on-click on-close} [:button {:class (stl/css :modal-close-button) :on-click on-close}
i/close]] i/close]]
[:div {:class (stl/css :dialog-title)} title] [:div {:class (stl/css :dialog-title)} title]
(for [txt content] (for [[index content] (d/enumerate content)]
[:div txt]) [:div {:key index} content])
[:div {:class (stl/css :sign-info)} [:div {:class (stl/css :sign-info)}
(when cancel-text (when cancel-text
[:button {:class (stl/css :cancel-button) :on-click on-close} cancel-text]) [:button {:class (stl/css :cancel-button)
:on-click on-close}
cancel-text])
[:button {:on-click on-click} button-text]]]])) [:button {:on-click on-click} button-text]]]]))
(mf/defc request-access (mf/defc request-access
{::mf/props :obj} {::mf/props :obj}
[{:keys [file-id team-id is-default workspace?]}] [{:keys [file-id team-id is-default workspace?]}]
(let [profile (:profile @st/state) (let [profile (mf/deref refs/profile)
requested* (mf/use-state {:sent false :already-requested false}) requested* (mf/use-state {:sent false :already-requested false})
requested (deref requested*) requested (deref requested*)
show-dialog (mf/use-state true) show-dialog (mf/use-state true)
on-close on-close
(mf/use-fn (mf/use-fn
(mf/deps profile) (mf/deps profile)
(fn [] (fn []
(st/emit! (rt/nav :dashboard-projects {:team-id (:default-team-id profile)})))) (st/emit! (rt/nav :dashboard-projects {:team-id (:default-team-id profile)}))))
on-success on-success
(mf/use-fn (mf/use-fn
#(reset! requested* {:sent true :already-requested false})) #(reset! requested* {:sent true :already-requested false}))
on-error on-error
(mf/use-fn (mf/use-fn
#(reset! requested* {:sent true :already-requested true})) #(reset! requested* {:sent true :already-requested true}))
on-request-access on-request-access
(mf/use-fn (mf/use-fn
(mf/deps file-id team-id workspace?) (mf/deps file-id team-id workspace?)
(fn [] (fn []
(let [params (if (some? file-id) {:file-id file-id :is-viewer (not workspace?)} {:team-id team-id}) (let [params (if (some? file-id)
mdata {:on-success on-success :on-error on-error}] {:file-id file-id
(st/emit! (dc/create-team-access-request (with-meta params mdata))))))] :is-viewer (not workspace?)}
{:team-id team-id})
mdata {:on-success on-success
:on-error on-error}]
(st/emit! (dc/create-team-access-request
(with-meta params mdata))))))]
[:* [:*
(if (some? file-id) (if (some? file-id)
@ -220,8 +245,15 @@
[:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")] [:div {:class (stl/css :project-name)} (tr "not-found.no-permission.project-name")]
[:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]] [:div {:class (stl/css :file-name)} (tr "not-found.no-permission.penpot-file")]]]
[:div {:class (stl/css :workspace-right)}]] [:div {:class (stl/css :workspace-right)}]]
[:div {:class (stl/css :viewer)} [:div {:class (stl/css :viewer)}
[:& header/header {:project {:name (tr "not-found.no-permission.project-name")} ;; FIXME: the viewer header was never designed to be reused
;; from other parts of the application, and this code looks
;; like a fast workaround reusing it as-is without a proper
;; component adaptation for be able to use it easily it on
;; viewer context or static error page context
[:& viewer.header/header {:project
{:name (tr "not-found.no-permission.project-name")}
:index 0 :index 0
:file {:name (tr "not-found.no-permission.penpot-file")} :file {:name (tr "not-found.no-permission.penpot-file")}
:page nil :page nil
@ -392,59 +424,87 @@
[:div {:class (stl/css :sign-info)} [:div {:class (stl/css :sign-info)}
[:button {:on-click on-reset} (tr "labels.retry")]]])) [:button {:on-click on-reset} (tr "labels.retry")]]]))
(defn- load-info
"Load exception page info"
[path-params]
(let [default {:loaded true}
stream (cond
(:file-id path-params)
(->> (rp/cmd! :get-file-info {:id (:file-id path-params)})
(rx/map (fn [info]
{:loaded true
:file-id (:id info)})))
(:team-id path-params)
(->> (rp/cmd! :get-team-info {:id (:team-id path-params)})
(rx/map (fn [info]
{:loaded true
:team-id (:id info)
:team-default (:is-default info)})))
:else
(rx/of default))]
(->> stream
(rx/timeout 3000)
(rx/catch (fn [cause]
(if (instance? TimeoutError cause)
(rx/of default)
(rx/throw cause)))))))
(mf/defc exception-page* (mf/defc exception-page*
{::mf/props :obj} {::mf/props :obj}
[{:keys [data route] :as props}] [{:keys [data route] :as props}]
(let [file-info (mf/use-state {:pending true}) (let [type (:type data)
team-info (mf/use-state {:pending true})
type (:type data)
path (:path route) path (:path route)
query-params (:query-params route)
path-params (:path-params route)
workspace? (str/includes? path "workspace") workspace? (str/includes? path "workspace")
dashboard? (str/includes? path "dashboard") dashboard? (str/includes? path "dashboard")
view? (str/includes? path "view") view? (str/includes? path "view")
request-access? (and ;; We stora the request access info int this state
info* (mf/use-state nil)
info (deref info*)
loaded? (get info :loaded false)
request-access?
(and
(or workspace? dashboard? view?) (or workspace? dashboard? view?)
(or (not (str/empty? (:file-id @file-info))) (not (str/empty? (:team-id @team-info))))) (or (:file-id info)
(:team-id info)))]
query-params (u/map->query-string (:query-params route)) (mf/with-effect [type path query-params path-params]
pparams (:path-params route) (let [query-params (u/map->query-string query-params)
on-file-info (mf/use-fn event-params {::ev/name "exception-page"
(fn [info] :type type
(reset! file-info {:file-id (:id info)}))) :path path
on-team-info (mf/use-fn :query-params query-params}]
(fn [info] (st/emit! (ptk/event ::ev/event event-params))))
(reset! team-info {:team-id (:id info) :is-default (:is-default info)})))]
(mf/with-effect [type path query-params pparams @file-info @team-info] (mf/with-effect [path-params info]
(st/emit! (ptk/event ::ev/event {::ev/name "exception-page" :type type :path path :query-params query-params})) (when-not (:loaded info)
(->> (load-info path-params)
(rx/subs! (partial reset! info*)))))
(when (and (:file-id pparams) (:pending @file-info)) (when loaded?
(->> (rp/cmd! :get-file-info {:id (:file-id pparams)}) (if request-access?
(rx/subs! on-file-info))) [:& request-access {:file-id (:file-id info)
:team-id (:team-id info)
(when (and (:team-id pparams) (:pending @team-info)) :is-default (:team-default info)
(->> (rp/cmd! :get-team-info {:id (:team-id pparams)}) :workspace? workspace?}]
(rx/subs! on-team-info))))
(case (:type data) (case (:type data)
:not-found :not-found
(if request-access? [:> not-found* {}]
[:& request-access {:file-id (:file-id @file-info)
:team-id (:team-id @team-info)
:is-default (:is-default @team-info)
:workspace? workspace?}]
[:> not-found* {}])
:authentication :authentication
(if request-access? [:> not-found* {}]
[:& request-access {:file-id (:file-id @file-info)
:team-id (:team-id @team-info)
:is-default (:is-default @team-info)
:workspace? workspace?}]
[:> not-found* {}])
:bad-gateway :bad-gateway
[:> bad-gateway* props] [:> bad-gateway* props]
@ -452,4 +512,4 @@
:service-unavailable :service-unavailable
[:& service-unavailable] [:& service-unavailable]
[:> internal-error* props]))) [:> internal-error* props])))))

View file

@ -42,7 +42,7 @@
(cond-> color (cond-> color
(:value color) (assoc :color (:value color) :opacity 1) (:value color) (assoc :color (:value color) :opacity 1)
(:value color) (dissoc :value) (:value color) (dissoc :value)
true (assoc :file-id file-id))) :always (assoc :file-id file-id)))
color-id (:id color) color-id (:id color)
@ -70,7 +70,7 @@
(fn [event] (fn [event]
(st/emit! (st/emit!
(dwl/add-recent-color color) (dwl/add-recent-color color)
(dc/apply-color-from-palette (merge uc/empty-color color) (kbd/alt? event))))) (dc/apply-color-from-palette color (kbd/alt? event)))))
rename-color rename-color
(mf/use-fn (mf/use-fn

View file

@ -14,6 +14,7 @@
[app.main.data.workspace.selection :as dws] [app.main.data.workspace.selection :as dws]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -21,22 +22,25 @@
(defn- prepare-colors (defn- prepare-colors
[shapes file-id shared-libs] [shapes file-id shared-libs]
(let [data (into [] (remove nil? (ctc/extract-all-colors shapes file-id shared-libs))) (let [data (into [] (remove nil? (ctc/extract-all-colors shapes file-id shared-libs)))
grouped-colors (group-by :attrs data) groups (d/group-by :attrs #(dissoc % :attrs) data)
all-colors (distinct (mapv :attrs data)) all-colors (distinct (mapv :attrs data))
tmp (group-by #(some? (:id %)) all-colors) tmp (group-by #(some? (:id %)) all-colors)
library-colors (get tmp true) library-colors (get tmp true)
colors (get tmp false)] colors (get tmp false)]
{:grouped-colors grouped-colors {:groups groups
:all-colors all-colors :all-colors all-colors
:colors colors :colors colors
:library-colors library-colors})) :library-colors library-colors}))
(def xf:map-shape-id
(map :shape-id))
(mf/defc color-selection-menu (mf/defc color-selection-menu
{::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))] {::mf/wrap [#(mf/memo' % (mf/check-props ["shapes"]))]
::mf/wrap-props false} ::mf/wrap-props false}
[{:keys [shapes file-id shared-libs]}] [{:keys [shapes file-id shared-libs]}]
(let [{:keys [grouped-colors library-colors colors]} (mf/with-memo [shapes file-id shared-libs] (let [{:keys [groups library-colors colors]} (mf/with-memo [shapes file-id shared-libs]
(prepare-colors shapes file-id shared-libs)) (prepare-colors shapes file-id shared-libs))
state* (mf/use-state true) state* (mf/use-state true)
@ -49,54 +53,65 @@
expand-lib-color (mf/use-state false) expand-lib-color (mf/use-state false)
expand-color (mf/use-state false) expand-color (mf/use-state false)
grouped-colors* (mf/use-var nil) groups-ref (h/use-ref-value groups)
prev-colors* (mf/use-var []) prev-colors-ref (mf/use-ref nil)
;; grouped-colors* (mf/use-var nil)
;; prev-colors* (mf/use-var [])
on-change on-change
(mf/use-fn (mf/use-fn
(fn [new-color old-color from-picker?] (fn [new-color old-color from-picker?]
(let [old-color (-> old-color (dissoc :name :path) d/without-nils) (prn "new-color" new-color)
(prn "old-color" old-color)
(let [old-color (-> old-color
(dissoc :name :path)
(d/without-nils))
;; When dragging on the color picker sometimes all ;; When dragging on the color picker sometimes all
;; the shapes hasn't updated the color to the prev ;; the shapes hasn't updated the color to the prev
;; value so we need this extra calculation ;; value so we need this extra calculation
shapes-by-old-color (get @grouped-colors* old-color) groups (mf/ref-val groups-ref)
prev-color (d/seek #(get @grouped-colors* %) @prev-colors*) prev-colors (mf/ref-val prev-colors-ref)
shapes-by-prev-color (get @grouped-colors* prev-color)
shapes-by-color (or shapes-by-prev-color shapes-by-old-color)] prev-color (d/seek (partial get groups) prev-colors)
cops-old (get groups old-color)
cops-prev (get groups prev-colors)
cops (or cops-prev cops-old)
old-color (or prev-color old-color)]
(when from-picker? (when from-picker?
(swap! prev-colors* conj (-> new-color (dissoc :name :path) d/without-nils))) (let [color (-> new-color
(dissoc :name :path)
(d/without-nils))]
(mf/set-ref-val! prev-colors-ref
(conj prev-colors color))))
(st/emit! (dc/change-color-in-selected new-color shapes-by-color (or prev-color old-color)))))) (st/emit! (dc/change-color-in-selected cops new-color old-color)))))
on-open on-open
(mf/use-fn (mf/use-fn #(mf/set-ref-val! prev-colors-ref []))
(fn []
(reset! prev-colors* [])))
on-close on-close
(mf/use-fn (mf/use-fn #(mf/set-ref-val! prev-colors-ref []))
(fn []
(reset! prev-colors* [])))
on-detach on-detach
(mf/use-fn (mf/use-fn
(fn [color] (fn [color]
(let [shapes-by-color (get @grouped-colors* color) (let [groups (mf/ref-val groups-ref)
new-color (assoc color :id nil :file-id nil)] cops (get groups color)
(st/emit! (dc/change-color-in-selected new-color shapes-by-color color))))) color' (dissoc color :id :file-id)]
(st/emit! (dc/change-color-in-selected cops color' color)))))
select-only select-only
(mf/use-fn (mf/use-fn
(fn [color] (fn [color]
(let [shapes-by-color (get @grouped-colors* color) (let [groups (mf/ref-val groups-ref)
ids (into (d/ordered-set) (map :shape-id) shapes-by-color)] cops (get groups color)
ids (into (d/ordered-set) xf:map-shape-id cops)]
(st/emit! (dws/select-shapes ids)))))] (st/emit! (dws/select-shapes ids)))))]
(mf/with-effect [grouped-colors]
(reset! grouped-colors* grouped-colors))
[:div {:class (stl/css :element-set)} [:div {:class (stl/css :element-set)}
[:div {:class (stl/css :element-title)} [:div {:class (stl/css :element-title)}
[:& title-bar {:collapsable has-colors? [:& title-bar {:collapsable has-colors?

View file

@ -113,9 +113,9 @@
handle-value-change handle-value-change
(mf/use-fn (mf/use-fn
(mf/deps color on-change) (mf/deps color on-change)
(fn [new-value] (fn [value]
(let [color (-> color (let [color (-> color
(assoc :color new-value) (assoc :color value)
(dissoc :gradient))] (dissoc :gradient))]
(st/emit! (dwl/add-recent-color color) (st/emit! (dwl/add-recent-color color)
(on-change color))))) (on-change color)))))
@ -146,7 +146,9 @@
:else :else
color) color)
{:keys [x y]} (dom/get-client-position event) cpos (dom/get-client-position event)
x (dm/get-prop cpos :x)
y (dm/get-prop cpos :y)
props {:x x props {:x x
:y y :y y
@ -154,14 +156,14 @@
:disable-opacity disable-opacity :disable-opacity disable-opacity
:disable-image disable-image :disable-image disable-image
;; on-change second parameter means if the source is the color-picker ;; on-change second parameter means if the source is the color-picker
:on-change #(on-change (merge uc/empty-color %) true) :on-change #(on-change % true)
:on-close (fn [value opacity id file-id] :on-close (fn [value opacity id file-id]
(when on-close (when on-close
(on-close value opacity id file-id))) (on-close value opacity id file-id)))
:data color}] :data color}]
(when on-open (when (fn? on-open)
(on-open (merge uc/empty-color color))) (on-open color))
(modal/show! :colorpicker props)))) (modal/show! :colorpicker props))))

View file

@ -80,9 +80,6 @@
(= id :multiple) (= id :multiple)
(= file-id :multiple))) (= file-id :multiple)))
(def empty-color
(into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity :image]))
(defn get-color-name (defn get-color-name
[color] [color]
(or (:color-library-name color) (or (:color-library-name color)

View file

@ -760,8 +760,10 @@
(.back (.-history js/window))) (.back (.-history js/window)))
(defn reload-current-window (defn reload-current-window
[] ([]
(.reload (.-location js/window))) (.reload globals/location))
([force?]
(.reload globals/location force?)))
(defn scroll-by! (defn scroll-by!
([element x y] ([element x y]

View file

@ -148,7 +148,17 @@
(defn nav-root (defn nav-root
"Navigate to the root page." "Navigate to the root page."
[] []
(set! (.-href globals/location) "/")) (ptk/reify ::nav-root
ptk/EffectEvent
(effect [_ _ _]
(set! (.-href globals/location) "/"))))
(defn reload
[force?]
(ptk/reify ::reload
ptk/EffectEvent
(effect [_ _ _]
(ts/asap (partial dom/reload-current-window force?)))))
(defn nav-raw (defn nav-raw
[& {:keys [href uri]}] [& {:keys [href uri]}]