mirror of
https://github.com/penpot/penpot.git
synced 2025-05-23 21:46:12 +02:00
🎉 Add new onboarding flow.
This commit is contained in:
parent
898ae64a57
commit
fced22bc60
28 changed files with 743 additions and 209 deletions
|
@ -300,6 +300,28 @@
|
|||
(rx/map team-created)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- EVENT: create-team-with-invitations
|
||||
|
||||
;; NOTE: right now, it only handles a single email, in a near future
|
||||
;; this will be changed to the ability to specify multiple emails.
|
||||
|
||||
(defn create-team-with-invitations
|
||||
[{:keys [name email role] :as params}]
|
||||
(us/assert string? name)
|
||||
(ptk/reify ::create-team-with-invitations
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(let [{:keys [on-success on-error]
|
||||
:or {on-success identity
|
||||
on-error rx/throw}} (meta params)
|
||||
params {:name name
|
||||
:emails #{email}
|
||||
:role role}]
|
||||
(->> (rp/mutation! :create-team-and-invite-members params)
|
||||
(rx/tap on-success)
|
||||
(rx/map team-created)
|
||||
(rx/catch on-error))))))
|
||||
|
||||
;; --- EVENT: update-team
|
||||
|
||||
(defn update-team
|
||||
|
|
|
@ -61,12 +61,6 @@
|
|||
(def dashboard-search-result
|
||||
(l/derived :dashboard-search-result st/state))
|
||||
|
||||
(def dashboard-team
|
||||
(l/derived (fn [state]
|
||||
(let [team-id (:current-team-id state)]
|
||||
(get-in state [:teams team-id])))
|
||||
st/state))
|
||||
|
||||
(def dashboard-team-stats
|
||||
(l/derived :dashboard-team-stats st/state))
|
||||
|
||||
|
|
|
@ -72,7 +72,10 @@
|
|||
:dashboard-team-settings)
|
||||
[:*
|
||||
#_[:div.modal-wrapper
|
||||
[:& app.main.ui.onboarding/release-notes-modal {:version "1.8"}]]
|
||||
#_[:& app.main.ui.onboarding/onboarding-templates-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-modal]
|
||||
#_[:& app.main.ui.onboarding/onboarding-team-modal]
|
||||
]
|
||||
[:& dashboard {:route route}]]
|
||||
|
||||
:viewer
|
||||
|
|
|
@ -104,11 +104,11 @@
|
|||
(when (and (:onboarding-viewed props)
|
||||
(not= version (:main @cf/version))
|
||||
(not= "0.0" (:main @cf/version)))
|
||||
(tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes :version (:main @cf/version)})))))))
|
||||
(tm/schedule 1000 #(st/emit! (modal/show {:type :release-notes
|
||||
:version (:main @cf/version)})))))))
|
||||
|
||||
[:& (mf/provider ctx/current-team-id) {:value team-id}
|
||||
[:& (mf/provider ctx/current-project-id) {:value project-id}
|
||||
|
||||
;; NOTE: dashboard events and other related functions assumes
|
||||
;; that the team is a implicit context variable that is
|
||||
;; available using react context or accessing
|
||||
|
|
|
@ -120,6 +120,6 @@
|
|||
[:*
|
||||
[:& header {:team team :project project}]
|
||||
[:section.dashboard-container
|
||||
[:& grid {:project-id (:id project)
|
||||
[:& grid {:project project
|
||||
:files files}]]]))
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
[app.main.ui.dashboard.file-menu :refer [file-menu]]
|
||||
[app.main.ui.dashboard.import :refer [use-import-file]]
|
||||
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
|
||||
[app.main.ui.dashboard.placeholder :refer [empty-placeholder loading-placeholder]]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.worker :as wrk]
|
||||
[app.util.dom :as dom]
|
||||
|
@ -195,24 +196,10 @@
|
|||
:on-edit on-edit
|
||||
:on-menu-close on-menu-close}])]]]))
|
||||
|
||||
(mf/defc empty-placeholder
|
||||
[{:keys [dragging?] :as props}]
|
||||
(if-not dragging?
|
||||
[:div.grid-empty-placeholder
|
||||
[:div.icon i/file-html]
|
||||
[:div.text (tr "dashboard.empty-files")]]
|
||||
[:div.grid-row.no-wrap
|
||||
[:div.grid-item]]))
|
||||
|
||||
(mf/defc loading-placeholder
|
||||
[]
|
||||
[:div.grid-empty-placeholder
|
||||
[:div.icon i/loader]
|
||||
[:div.text (tr "dashboard.loading-files")]])
|
||||
|
||||
(mf/defc grid
|
||||
[{:keys [files project-id] :as props}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
[{:keys [files project] :as props}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
project-id (:id project)
|
||||
|
||||
on-finish-import
|
||||
(mf/use-callback
|
||||
|
@ -272,7 +259,7 @@
|
|||
:navigate? true}])]
|
||||
|
||||
:else
|
||||
[:& empty-placeholder])]))
|
||||
[:& empty-placeholder {:default? (:is-default project)}])]))
|
||||
|
||||
(mf/defc line-grid-row
|
||||
[{:keys [files selected-files on-load-more dragging?] :as props}]
|
||||
|
@ -330,8 +317,11 @@
|
|||
(tr "dashboard.show-all-files")]])]))
|
||||
|
||||
(mf/defc line-grid
|
||||
[{:keys [project-id team-id files on-load-more] :as props}]
|
||||
[{:keys [project team files on-load-more] :as props}]
|
||||
(let [dragging? (mf/use-state false)
|
||||
project-id (:id project)
|
||||
team-id (:id team)
|
||||
|
||||
selected-files (mf/deref refs/dashboard-selected-files)
|
||||
selected-project (mf/deref refs/dashboard-selected-project)
|
||||
|
||||
|
@ -413,5 +403,6 @@
|
|||
:dragging? @dragging?}]
|
||||
|
||||
:else
|
||||
[:& empty-placeholder {:dragging? @dragging?}])]))
|
||||
[:& empty-placeholder {:dragging? @dragging?
|
||||
:default? (:is-default project)}])]))
|
||||
|
||||
|
|
34
frontend/src/app/main/ui/dashboard/placeholder.cljs
Normal file
34
frontend/src/app/main/ui/dashboard/placeholder.cljs
Normal file
|
@ -0,0 +1,34 @@
|
|||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) UXBOX Labs SL
|
||||
|
||||
(ns app.main.ui.dashboard.placeholder
|
||||
(:require
|
||||
[app.main.ui.icons :as i]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
(mf/defc empty-placeholder
|
||||
[{:keys [dragging? default?] :as props}]
|
||||
(cond
|
||||
(true? dragging?)
|
||||
[:div.grid-row.no-wrap
|
||||
[:div.grid-item]]
|
||||
|
||||
(true? default?)
|
||||
[:div.grid-empty-placeholder.drafts
|
||||
[:div.text
|
||||
[:& i18n/tr-html {:label "dashboard.empty-placeholder-drafts"}]]]
|
||||
|
||||
:else
|
||||
[:div.grid-empty-placeholder
|
||||
[:img.ph-files {:src "images/ph-file.svg"}]]))
|
||||
|
||||
(mf/defc loading-placeholder
|
||||
[]
|
||||
[:div.grid-empty-placeholder
|
||||
[:div.icon i/loader]
|
||||
[:div.text (tr "dashboard.loading-files")]])
|
||||
|
|
@ -73,7 +73,6 @@
|
|||
:accept-label (tr "modals.delete-project-confirm.accept")
|
||||
:on-accept delete-fn}))
|
||||
|
||||
|
||||
file-input (mf/use-ref nil)
|
||||
|
||||
on-import-files
|
||||
|
|
|
@ -33,10 +33,8 @@
|
|||
(tr "dashboard.new-project")]]))
|
||||
|
||||
(mf/defc project-item
|
||||
[{:keys [project first? files] :as props}]
|
||||
[{:keys [project first? team files] :as props}]
|
||||
(let [locale (mf/deref i18n/locale)
|
||||
|
||||
team-id (:team-id project)
|
||||
file-count (or (:count project) 0)
|
||||
|
||||
dstate (mf/deref refs/dashboard-local)
|
||||
|
@ -145,9 +143,8 @@
|
|||
i/actions]]
|
||||
|
||||
[:& line-grid
|
||||
{:project-id (:id project)
|
||||
:project project
|
||||
:team-id team-id
|
||||
{:project project
|
||||
:team team
|
||||
:on-load-more on-nav
|
||||
:files files}]]))
|
||||
|
||||
|
@ -186,7 +183,8 @@
|
|||
(filterv #(= id (:project-id %)))
|
||||
(sort-by :modified-at #(compare %2 %1))))]
|
||||
[:& project-item {:project project
|
||||
:files files
|
||||
:team team
|
||||
:files files
|
||||
:first? (= project (first projects))
|
||||
:key (:id project)}]))]])))
|
||||
|
||||
|
|
|
@ -72,8 +72,9 @@
|
|||
#(doseq [key keys]
|
||||
(events/unlistenByKey key)))))
|
||||
|
||||
[:div.modal-wrapper {:ref wrapper-ref}
|
||||
(mf/element (get components (:type data)) (:props data))]))
|
||||
(when-let [component (get components (:type data))]
|
||||
[:div.modal-wrapper {:ref wrapper-ref}
|
||||
(mf/element component (:props data))])))
|
||||
|
||||
|
||||
(def modal-ref
|
||||
|
|
|
@ -12,8 +12,10 @@
|
|||
[app.main.data.messages :as dm]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.users :as du]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.forms :as fm]
|
||||
[app.main.ui.icons :as i]
|
||||
[app.main.ui.releases.common :as rc]
|
||||
[app.main.ui.releases.v1-4]
|
||||
[app.main.ui.releases.v1-5]
|
||||
|
@ -21,10 +23,13 @@
|
|||
[app.main.ui.releases.v1-7]
|
||||
[app.main.ui.releases.v1-8]
|
||||
[app.main.ui.releases.v1-9]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.http :as http]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.object :as obj]
|
||||
[app.util.router :as rt]
|
||||
[app.util.timers :as tm]
|
||||
[beicon.core :as rx]
|
||||
[cljs.spec.alpha :as s]
|
||||
[rumext.alpha :as mf]))
|
||||
|
||||
|
@ -159,7 +164,7 @@
|
|||
skip
|
||||
(mf/use-callback
|
||||
(st/emitf (modal/hide)
|
||||
(modal/show {:type :onboarding-team})
|
||||
(modal/show {:type :onboarding-choice})
|
||||
(du/mark-onboarding-as-viewed)))]
|
||||
|
||||
(mf/use-layout-effect
|
||||
|
@ -187,57 +192,232 @@
|
|||
(s/def ::team-form
|
||||
(s/keys :req-un [::name]))
|
||||
|
||||
(mf/defc onboarding-choice-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-choice}
|
||||
[]
|
||||
(let [;; When user choices the option of `fly solo`, we proceed to show
|
||||
;; the onboarding templates modal.
|
||||
on-fly-solo
|
||||
(fn []
|
||||
(tm/schedule 400 #(st/emit! (modal/show {:type :onboarding-templates}))))
|
||||
|
||||
;; When user choices the option of `team up`, we proceed to show
|
||||
;; the team creation modal.
|
||||
on-team-up
|
||||
(fn []
|
||||
(st/emit! (modal/show {:type :onboarding-team})))
|
||||
]
|
||||
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.onboarding.final.animated.fadeInUp
|
||||
[:div.modal-top
|
||||
[:h1 (tr "onboarding.welcome.title")]
|
||||
[:p (tr "onboarding.welcome.desc3")]]
|
||||
[:div.modal-columns
|
||||
[:div.modal-left
|
||||
[:div.content-button {:on-click on-fly-solo}
|
||||
[:h2 (tr "onboarding.choice.fly-solo")]
|
||||
[:p (tr "onboarding.choice.fly-solo-desc")]]]
|
||||
[:div.modal-right
|
||||
[:div.content-button {:on-click on-team-up}
|
||||
[:h2 (tr "onboarding.choice.team-up")]
|
||||
[:p (tr "onboarding.choice.team-up-desc")]]]]
|
||||
[:img.deco {:src "images/deco-left.png" :border "0"}]
|
||||
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
|
||||
|
||||
(mf/defc onboarding-team-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-team}
|
||||
[]
|
||||
(let [close (mf/use-fn (st/emitf (modal/hide)))
|
||||
form (fm/use-form :spec ::team-form
|
||||
(let [form (fm/use-form :spec ::team-form
|
||||
:initial {})
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(fn [form _]
|
||||
(let [tname (get-in @form [:clean-data :name])]
|
||||
(st/emit! (modal/show {:type :onboarding-team-invitations :name tname})))))]
|
||||
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.onboarding-team
|
||||
[:div.title
|
||||
[:h2 (tr "onboarding.choice.team-up")]
|
||||
[:p (tr "onboarding.choice.team-up-desc")]]
|
||||
|
||||
[:& fm/form {:form form
|
||||
:on-submit on-submit}
|
||||
|
||||
[:div.team-row
|
||||
[:& fm/input {:type "text"
|
||||
:name :name
|
||||
:label (tr "onboarding.team-input-placeholder")}]]
|
||||
|
||||
[:div.buttons
|
||||
[:button.btn-secondary.btn-large
|
||||
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
|
||||
(tr "labels.cancel")]
|
||||
[:& fm/submit-button
|
||||
{:label (tr "labels.next")}]]]
|
||||
|
||||
[:img.deco {:src "images/deco-left.png" :border "0"}]
|
||||
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
|
||||
|
||||
(defn get-available-roles
|
||||
[]
|
||||
[{:value "editor" :label (tr "labels.editor")}
|
||||
{:value "admin" :label (tr "labels.admin")}])
|
||||
|
||||
(s/def ::email ::us/email)
|
||||
(s/def ::role ::us/keyword)
|
||||
(s/def ::invite-form
|
||||
(s/keys :req-un [::role ::email]))
|
||||
|
||||
;; This is the final step of team creation, consists in provide a
|
||||
;; shortcut for invite users.
|
||||
|
||||
(mf/defc onboarding-team-invitations-modal
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :onboarding-team-invitations}
|
||||
[{:keys [name] :as props}]
|
||||
(let [initial (mf/use-memo (constantly
|
||||
{:role "editor"
|
||||
:name name}))
|
||||
form (fm/use-form :spec ::invite-form
|
||||
:initial initial)
|
||||
|
||||
roles (mf/use-memo #(get-available-roles))
|
||||
|
||||
on-success
|
||||
(mf/use-callback
|
||||
(fn [_form response]
|
||||
(st/emit! (modal/hide)
|
||||
(rt/nav :dashboard-projects {:team-id (:id response)}))))
|
||||
(let [project-id (:default-project-id response)
|
||||
team-id (:id response)]
|
||||
(st/emit!
|
||||
(modal/hide)
|
||||
(rt/nav :dashboard-projects {:team-id team-id}))
|
||||
(tm/schedule 400 #(st/emit!
|
||||
(modal/show {:type :onboarding-templates
|
||||
:project-id project-id}))))))
|
||||
|
||||
on-error
|
||||
(mf/use-callback
|
||||
(fn [_form _response]
|
||||
(st/emit! (dm/error "Error on creating team."))))
|
||||
|
||||
on-submit
|
||||
;; The SKIP branch only creates the team, without invitations
|
||||
on-skip
|
||||
(mf/use-callback
|
||||
(fn [form _event]
|
||||
(fn [_]
|
||||
(let [mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}
|
||||
params {:name (get-in @form [:clean-data :name])}]
|
||||
(st/emit! (dd/create-team (with-meta params mdata))))))]
|
||||
params {:name name}]
|
||||
(st/emit! (dd/create-team (with-meta params mdata))))))
|
||||
|
||||
;; The SUBMIT branch creates the team with the invitations
|
||||
on-submit
|
||||
(mf/use-callback
|
||||
(fn [form _]
|
||||
(let [mdata {:on-success (partial on-success form)
|
||||
:on-error (partial on-error form)}
|
||||
params (:clean-data @form)]
|
||||
(st/emit! (dd/create-team-with-invitations (with-meta params mdata))))))]
|
||||
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.onboarding.final.animated.fadeInUp
|
||||
[:div.modal-left
|
||||
[:img {:src "images/onboarding-team.jpg" :border "0" :alt (tr "onboarding.team.create.title")}]
|
||||
[:h2 (tr "onboarding.team.create.title")]
|
||||
[:p (tr "onboarding.team.create.desc1")]
|
||||
[:div.modal-container.onboarding-team
|
||||
[:div.title
|
||||
[:h2 (tr "onboarding.choice.team-up")]
|
||||
[:p (tr "onboarding.choice.team-up-desc")]]
|
||||
|
||||
[:& fm/form {:form form
|
||||
:on-submit on-submit}
|
||||
[:& fm/input {:type "text"
|
||||
:name :name
|
||||
:label (tr "onboarding.team.create.input-placeholder")}]
|
||||
[:& fm/form {:form form
|
||||
:on-submit on-submit}
|
||||
|
||||
[:div.invite-row
|
||||
[:& fm/input {:name :email
|
||||
:label (tr "labels.email")}]
|
||||
[:& fm/select {:name :role
|
||||
:options roles}]]
|
||||
|
||||
[:div.buttons
|
||||
[:button.btn-secondary.btn-large
|
||||
{:on-click #(st/emit! (modal/show {:type :onboarding-choice}))}
|
||||
(tr "labels.cancel")]
|
||||
[:& fm/submit-button
|
||||
{:label (tr "onboarding.team.create.button")}]]]
|
||||
|
||||
[:div.modal-right
|
||||
[:img {:src "images/onboarding-start.jpg" :border "0" :alt (tr "onboarding.team.start.title")}]
|
||||
[:h2 (tr "onboarding.team.start.title")]
|
||||
[:p (tr "onboarding.team.start.desc1")]
|
||||
[:button.btn-primary.btn-large {:on-click close} (tr "onboarding.team.start.button")]]
|
||||
|
||||
|
||||
{:label (tr "labels.create")}]]
|
||||
[:div.skip-action
|
||||
{:on-click on-skip}
|
||||
[:div.action "Skip and invite later"]]]
|
||||
[:img.deco {:src "images/deco-left.png" :border "0"}]
|
||||
[:img.deco.right {:src "images/deco-right.png" :border "0"}]]]))
|
||||
|
||||
(mf/defc template-item
|
||||
[{:keys [name path image project-id]}]
|
||||
(let [downloading? (mf/use-state false)
|
||||
link (str (assoc cf/public-uri :path path))
|
||||
|
||||
on-finish-import
|
||||
(fn []
|
||||
(st/emit! (dd/fetch-recent-files)))
|
||||
|
||||
open-import-modal
|
||||
(fn [file]
|
||||
(st/emit! (modal/show
|
||||
{:type :import
|
||||
:project-id project-id
|
||||
:files [file]
|
||||
:on-finish-import on-finish-import})))
|
||||
on-click
|
||||
(fn []
|
||||
(reset! downloading? true)
|
||||
(->> (http/send! {:method :get :uri link :response-type :blob :mode :no-cors})
|
||||
(rx/subs (fn [{:keys [body] :as response}]
|
||||
(open-import-modal {:name name :uri (dom/create-uri body)}))
|
||||
(fn [error]
|
||||
(js/console.log "error" error))
|
||||
(fn []
|
||||
(reset! downloading? false)))))
|
||||
]
|
||||
|
||||
[:div.template-item
|
||||
[:div.template-item-content
|
||||
[:img {:src image}]]
|
||||
[:div.template-item-title
|
||||
[:div.label name]
|
||||
(if @downloading?
|
||||
[:div.action "Fetching..."]
|
||||
[:div.action {:on-click on-click} "+ Add to drafts"])]]))
|
||||
|
||||
(mf/defc onboarding-templates-modal
|
||||
{::mf/wrap-props false
|
||||
::mf/register modal/components
|
||||
::mf/register-as :onboarding-templates}
|
||||
;; NOTE: the project usually comes empty, it only comes fullfilled
|
||||
;; when a user creates a new team just after signup.
|
||||
[{:keys [project-id] :as props}]
|
||||
(let [close-fn (mf/use-callback #(st/emit! (modal/hide)))
|
||||
profile (mf/deref refs/profile)
|
||||
project-id (or project-id (:default-project-id profile))]
|
||||
[:div.modal-overlay
|
||||
[:div.modal-container.onboarding-templates
|
||||
[:div.modal-header
|
||||
[:div.modal-close-button
|
||||
{:on-click close-fn} i/close]]
|
||||
|
||||
[:div.modal-content
|
||||
[:h3 (tr "onboarding.templates.title")]
|
||||
[:p (tr "onboarding.templates.subtitle")]
|
||||
|
||||
[:div.templates
|
||||
[:& template-item
|
||||
{:path "/github/penpot-files/Penpot-Design-system.penpot"
|
||||
:image "https://penpot.app/images/libraries/cover-ds-penpot.jpg"
|
||||
:name "Penpot Design System"
|
||||
:project-id project-id}]
|
||||
[:& template-item
|
||||
{:path "/github/penpot-files/Material-Design-Kit.penpot"
|
||||
:image "https://penpot.app/images/libraries/cover-material.jpg"
|
||||
:name "Material Design Kit"
|
||||
:project-id project-id}]]]]]))
|
||||
|
||||
|
||||
;;; --- RELEASE NOTES MODAL
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@
|
|||
(rx/tap #(do (swap! current inc)
|
||||
(progress! context
|
||||
:upload-data @current total))))))
|
||||
|
||||
|
||||
(rx/map first)
|
||||
(rx/tap #(reset! revn (:revn %)))
|
||||
(rx/ignore))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue