Store some profile props on browser global storage

This commit is contained in:
Andrey Antukh 2024-09-10 17:04:38 +02:00 committed by Alonso Torres
parent c8caca77a3
commit cdcff62232
3 changed files with 81 additions and 60 deletions

View file

@ -10,6 +10,7 @@
[app.common.data :as d] [app.common.data :as d]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.plugins :refer [schema:plugin-data]]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.db :as db] [app.db :as db]
@ -40,22 +41,19 @@
(declare strip-private-attrs) (declare strip-private-attrs)
(declare verify-password) (declare verify-password)
(defn clean-email (def schema:props
"Clean and normalizes email address string" [:map {:title "ProfileProps"}
[email] [:plugins {:optional true} schema:plugin-data]
(let [email (str/lower email) [:newsletter-updates {:optional true} ::sm/boolean]
email (if (str/starts-with? email "mailto:") [:newsletter-news {:optional true} ::sm/boolean]
(subs email 7) [:onboarding-team-id {:optional true} ::sm/uuid]
email) [:onboarding-viewed {:optional true} ::sm/boolean]
email (if (or (str/starts-with? email "<") [:v2-info-shown {:optional true} ::sm/boolean]
(str/ends-with? email ">")) [:welcome-file-id {:optional true} [:maybe ::sm/boolean]]
(str/trim email "<>") [:release-notes-viewed {:optional true}
email)] [::sm/text {:max 100}]]])
email))
(def ^:private (def schema:profile
schema:profile
(sm/define
[:map {:title "Profile"} [:map {:title "Profile"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:fullname [::sm/word-string {:max 250}]] [:fullname [::sm/word-string {:max 250}]]
@ -68,8 +66,20 @@
[:modified-at {:optional true} ::sm/inst] [:modified-at {:optional true} ::sm/inst]
[:default-project-id {:optional true} ::sm/uuid] [:default-project-id {:optional true} ::sm/uuid]
[:default-team-id {:optional true} ::sm/uuid] [:default-team-id {:optional true} ::sm/uuid]
[:props {:optional true} [:props {:optional true} schema:props]])
[:map-of {:title "ProfileProps"} :keyword :any]]]))
(defn clean-email
"Clean and normalizes email address string"
[email]
(let [email (str/lower email)
email (if (str/starts-with? email "mailto:")
(subs email 7)
email)
email (if (or (str/starts-with? email "<")
(str/ends-with? email ">"))
(str/trim email "<>")
email)]
email))
;; --- QUERY: Get profile (own) ;; --- QUERY: Get profile (own)
@ -351,14 +361,12 @@
:extra-data ptoken}) :extra-data ptoken})
nil)) nil))
;; --- MUTATION: Update Profile Props ;; --- MUTATION: Update Profile Props
(def ^:private (def ^:private
schema:update-profile-props schema:update-profile-props
(sm/define
[:map {:title "update-profile-props"} [:map {:title "update-profile-props"}
[:props [:map-of :keyword :any]]])) [:props schema:props]])
(defn update-profile-props (defn update-profile-props
[{:keys [::db/conn] :as cfg} profile-id props] [{:keys [::db/conn] :as cfg} profile-id props]

View file

@ -11,7 +11,6 @@
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.dashboard.grid :refer [line-grid]] [app.main.ui.dashboard.grid :refer [line-grid]]
@ -24,6 +23,7 @@
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.storage :as storage]
[app.util.time :as dt] [app.util.time :as dt]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
@ -54,24 +54,25 @@
:data-testid "new-project-button"} :data-testid "new-project-button"}
(tr "dashboard.new-project")]])) (tr "dashboard.new-project")]]))
(mf/defc team-hero (mf/defc team-hero*
{::mf/wrap [mf/memo]} {::mf/wrap [mf/memo]
[{:keys [team close-fn] :as props}] ::mf/props :obj}
[{:keys [team on-close]}]
(let [on-nav-members-click (mf/use-fn #(st/emit! (dd/go-to-team-members))) (let [on-nav-members-click (mf/use-fn #(st/emit! (dd/go-to-team-members)))
on-invite-click on-invite
(mf/use-fn (mf/use-fn
(mf/deps team) (mf/deps team)
(fn [] (fn []
(st/emit! (modal/show {:type :invite-members (st/emit! (modal/show {:type :invite-members
:team team :team team
:origin :hero})))) :origin :hero}))))
on-close-click on-close'
(mf/use-fn (mf/use-fn
(mf/deps close-fn) (mf/deps on-close)
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(close-fn)))] (on-close event)))]
[:div {:class (stl/css :team-hero)} [:div {:class (stl/css :team-hero)}
[:div {:class (stl/css :img-wrapper)} [:div {:class (stl/css :img-wrapper)}
@ -85,11 +86,11 @@
[:a {:on-click on-nav-members-click} (tr "dasboard.team-hero.management")]] [:a {:on-click on-nav-members-click} (tr "dasboard.team-hero.management")]]
[:button [:button
{:class (stl/css :btn-primary :invite) {:class (stl/css :btn-primary :invite)
:on-click on-invite-click} :on-click on-invite}
(tr "onboarding.choice.team-up.invite-members")]] (tr "onboarding.choice.team-up.invite-members")]]
[:button {:class (stl/css :close) [:button {:class (stl/css :close)
:on-click on-close-click :on-click on-close'
:aria-label (tr "labels.close")} :aria-label (tr "labels.close")}
close-icon]])) close-icon]]))
@ -292,26 +293,27 @@
(sort-by :modified-at) (sort-by :modified-at)
(reverse)) (reverse))
recent-map (mf/deref recent-files-ref) recent-map (mf/deref recent-files-ref)
props (some-> profile (get :props {}))
you-owner? (get-in team [:permissions :is-owner]) you-owner? (get-in team [:permissions :is-owner])
you-admin? (get-in team [:permissions :is-admin]) you-admin? (get-in team [:permissions :is-admin])
can-invite? (or you-owner? you-admin?) can-invite? (or you-owner? you-admin?)
team-hero? (and can-invite?
(:team-hero? props true) show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true))
(not (:is-default team))) show-team-hero? (deref show-team-hero*)
is-my-penpot (= (:default-team-id profile) (:id team)) is-my-penpot (= (:default-team-id profile) (:id team))
is-defalt-team? (:is-default team)
team-id (:id team) team-id (:id team)
close-banner on-close
(mf/use-fn (mf/use-fn
(fn [] (fn []
(st/emit! (du/update-profile-props {:team-hero? false}) (reset! show-team-hero* false)
(ptk/data-event ::ev/event {::ev/name "dont-show-team-up-hero" (st/emit! (ptk/data-event ::ev/event {::ev/name "dont-show-team-up-hero"
::ev/origin "dashboard"})))) ::ev/origin "dashboard"}))))]
show-team-hero? (and (not is-my-penpot) team-hero?)] (mf/with-effect [show-team-hero?]
(swap! storage/global assoc ::show-team-hero show-team-hero?))
(mf/with-effect [team] (mf/with-effect [team]
(let [tname (if (:is-default team) (let [tname (if (:is-default team)
@ -328,13 +330,18 @@
[:& header] [:& header]
[:div {:class (stl/css :projects-container)} [:div {:class (stl/css :projects-container)}
[:* [:*
(when team-hero? (when (and show-team-hero?
[:& team-hero {:team team :close-fn close-banner}]) can-invite?
(not is-defalt-team?))
[:> team-hero* {:team team :on-close on-close}])
[:div {:class (stl/css-case :dashboard-container true [:div {:class (stl/css-case :dashboard-container true
:no-bg true :no-bg true
:dashboard-projects true :dashboard-projects true
:with-team-hero show-team-hero?)} :with-team-hero (and (not is-my-penpot)
(not is-defalt-team?)
show-team-hero?
can-invite?))}
(for [{:keys [id] :as project} projects] (for [{:keys [id] :as project} projects]
(let [files (when recent-map (let [files (when recent-map
(->> (vals recent-map) (->> (vals recent-map)

View file

@ -12,7 +12,6 @@
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.users :as du]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
@ -20,6 +19,7 @@
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.router :as rt] [app.util.router :as rt]
[app.util.storage :as storage]
[okulary.core :as l] [okulary.core :as l]
[potok.v2.core :as ptk] [potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@ -60,17 +60,11 @@
:template template :template template
:on-finish-import on-finish})))) :on-finish-import on-finish}))))
(mf/defc title (mf/defc title*
{::mf/wrap-props false} {::mf/props :obj
[{:keys [collapsed]}] ::mf/private true}
(let [on-click [{:keys [on-click is-collapsed]}]
(mf/use-fn (let [on-key-down
(mf/deps collapsed)
(fn [_event]
(let [props {:builtin-templates-collapsed-status (not collapsed)}]
(st/emit! (du/update-profile-props props)))))
on-key-down
(mf/use-fn (mf/use-fn
(mf/deps on-click) (mf/deps on-click)
(fn [event] (fn [event]
@ -86,7 +80,7 @@
:on-key-down on-key-down} :on-key-down on-key-down}
[:span {:class (stl/css :title-text)} [:span {:class (stl/css :title-text)}
(tr "dashboard.libraries-and-templates")] (tr "dashboard.libraries-and-templates")]
(if ^boolean collapsed (if ^boolean is-collapsed
[:span {:class (stl/css :title-icon :title-icon-collapsed)} [:span {:class (stl/css :title-icon :title-icon-collapsed)}
arrow-icon] arrow-icon]
[:span {:class (stl/css :title-icon)} [:span {:class (stl/css :title-icon)}
@ -180,8 +174,12 @@
"dashboard-project") "dashboard-project")
(name route-name)) (name route-name))
props (:props profile) collapsed* (mf/use-state
collapsed (:builtin-templates-collapsed-status props false) #(get storage/global ::collapsed))
collapsed (deref collapsed*)
can-move (mf/use-state {:left false :right true}) can-move (mf/use-state {:left false :right true})
total (count templates) total (count templates)
@ -192,6 +190,11 @@
move-left (fn [] (dom/scroll-by! (mf/ref-val content-ref) -300 0)) move-left (fn [] (dom/scroll-by! (mf/ref-val content-ref) -300 0))
move-right (fn [] (dom/scroll-by! (mf/ref-val content-ref) 300 0)) move-right (fn [] (dom/scroll-by! (mf/ref-val content-ref) 300 0))
on-toggle-collapse
(mf/use-fn
(fn [_event]
(swap! collapsed* not)))
update-can-move update-can-move
(fn [scroll-left scroll-available client-width] (fn [scroll-left scroll-available client-width]
(reset! can-move {:left (> scroll-left 0) (reset! can-move {:left (> scroll-left 0)
@ -231,12 +234,15 @@
(.dispatchEvent content (js/Event. "scroll"))))) (.dispatchEvent content (js/Event. "scroll")))))
(mf/with-effect [profile collapsed] (mf/with-effect [profile collapsed]
(swap! storage/global assoc ::collapsed collapsed)
(when (and profile (not collapsed)) (when (and profile (not collapsed))
(st/emit! (dd/fetch-builtin-templates)))) (st/emit! (dd/fetch-builtin-templates))))
[:div {:class (stl/css-case :dashboard-templates-section true [:div {:class (stl/css-case :dashboard-templates-section true
:collapsed collapsed)} :collapsed collapsed)}
[:& title {:collapsed collapsed}] [:> title* {:on-click on-toggle-collapse
:is-collapsed collapsed}]
[:div {:class (stl/css :content) [:div {:class (stl/css :content)
:on-scroll on-scroll :on-scroll on-scroll