From 14da8c0c9e0bf789e42aec1bc5e897cc88890dd1 Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 23 Jan 2023 00:32:01 +0000 Subject: [PATCH] Added new onboarding tour flow for builder --- .../src/events/publishers/user.ts | 9 +- .../bbui/src/Actions/position_dropdown.js | 17 +- packages/bbui/src/Button/Button.svelte | 2 + packages/bbui/src/Popover/Popover.svelte | 11 +- packages/bbui/src/Tabs/Tab.svelte | 7 +- .../src/builderStore/store/frontend.js | 4 + .../src/components/deploy/DeployModal.svelte | 8 +- .../portal/onboarding/TourPopover.svelte | 164 ++++++++++++++++++ .../portal/onboarding/TourWrap.svelte | 27 +++ .../onboarding/steps/OnboardingData.svelte | 10 ++ .../onboarding/steps/OnboardingDesign.svelte | 10 ++ .../onboarding/steps/OnboardingPublish.svelte | 7 + .../portal/onboarding/steps/index.js | 3 + .../portal/onboarding/tourHandler.js | 47 +++++ .../src/components/portal/onboarding/tours.js | 90 ++++++++++ .../builder/app/[application]/_layout.svelte | 39 ++++- packages/types/src/documents/global/user.ts | 1 + packages/types/src/sdk/events/event.ts | 3 + packages/types/src/sdk/events/user.ts | 5 + packages/worker/src/sdk/users/events.ts | 8 + 20 files changed, 449 insertions(+), 23 deletions(-) create mode 100644 packages/builder/src/components/portal/onboarding/TourPopover.svelte create mode 100644 packages/builder/src/components/portal/onboarding/TourWrap.svelte create mode 100644 packages/builder/src/components/portal/onboarding/steps/OnboardingData.svelte create mode 100644 packages/builder/src/components/portal/onboarding/steps/OnboardingDesign.svelte create mode 100644 packages/builder/src/components/portal/onboarding/steps/OnboardingPublish.svelte create mode 100644 packages/builder/src/components/portal/onboarding/steps/index.js create mode 100644 packages/builder/src/components/portal/onboarding/tourHandler.js create mode 100644 packages/builder/src/components/portal/onboarding/tours.js diff --git a/packages/backend-core/src/events/publishers/user.ts b/packages/backend-core/src/events/publishers/user.ts index 018d90e906..1fe50149b5 100644 --- a/packages/backend-core/src/events/publishers/user.ts +++ b/packages/backend-core/src/events/publishers/user.ts @@ -44,14 +44,6 @@ export async function onboardingComplete(user: User) { await publishEvent(Event.USER_ONBOARDING_COMPLETE, properties) } -export async function onboardingProgress(user: User, step?: string) { - const properties: UserOnboardingEvent = { - userId: user._id as string, - step, - } - await publishEvent(Event.USER_ONBOARDING_STEP, properties) -} - // PERMISSIONS async function permissionAdminAssigned(user: User, timestamp?: number) { @@ -142,6 +134,7 @@ export default { permissionAdminRemoved, permissionBuilderAssigned, permissionBuilderRemoved, + onboardingComplete, invited, inviteAccepted, passwordForceReset, diff --git a/packages/bbui/src/Actions/position_dropdown.js b/packages/bbui/src/Actions/position_dropdown.js index 463b69169f..d04159476c 100644 --- a/packages/bbui/src/Actions/position_dropdown.js +++ b/packages/bbui/src/Actions/position_dropdown.js @@ -1,6 +1,6 @@ export default function positionDropdown( element, - { anchor, align, maxWidth, useAnchorWidth } + { anchor, align, maxWidth, useAnchorWidth, showTip } ) { const update = () => { const anchorBounds = anchor.getBoundingClientRect() @@ -13,6 +13,9 @@ export default function positionDropdown( top: null, } + let popoverLeftPad = 20 + let tipOffset = showTip ? 12.5 : 0 + // Determine vertical styles if (window.innerHeight - anchorBounds.bottom < 100) { styles.top = anchorBounds.top - elementBounds.height - 5 @@ -29,7 +32,16 @@ export default function positionDropdown( styles.minWidth = anchorBounds.width } if (align === "right") { - styles.left = anchorBounds.left + anchorBounds.width - elementBounds.width + let left = + anchorBounds.left + + anchorBounds.width / 2 - + elementBounds.width + + tipOffset + // Accommodate margin on popover: 1.25rem; ~20px + if (left + elementBounds.width + popoverLeftPad > window.innerWidth) { + left -= 20 + } + styles.left = left } else if (align === "right-side") { styles.left = anchorBounds.left + anchorBounds.width } else { @@ -56,6 +68,7 @@ export default function positionDropdown( }) resizeObserver.observe(anchor) resizeObserver.observe(element) + resizeObserver.observe(document.body) document.addEventListener("scroll", update, true) diff --git a/packages/bbui/src/Button/Button.svelte b/packages/bbui/src/Button/Button.svelte index 979ec6a728..b8ffe9f7e6 100644 --- a/packages/bbui/src/Button/Button.svelte +++ b/packages/bbui/src/Button/Button.svelte @@ -15,11 +15,13 @@ export let tooltip = undefined export let dataCy export let newStyles = true + export let id let showTooltip = false + + + + import { Popover, Layout, Heading, Body, Button } from "@budibase/bbui" + import { store } from "builderStore" + import { TOURS } from "./tours.js" + import { goto, layout, isActive } from "@roxi/routify" + + let popoverAnchor + let popover + let tourSteps = null + let tourStep + let tourStepIdx + let lastStep + + $: tourNodes = { ...$store.tourNodes } + $: tourKey = $store.tourKey + $: tourStepKey = $store.tourStepKey + + const initTour = targetKey => { + tourSteps = [...TOURS[targetKey]] + tourStepIdx = 0 + tourStep = { ...tourSteps[tourStepIdx] } + } + + $: initTour(tourKey) + + const updateTourStep = targetStepKey => { + tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey) + lastStep = tourStepIdx + 1 == tourSteps.length + tourStep = { ...tourSteps[tourStepIdx] } + tourStep.onLoad() + } + + $: updateTourStep(tourStepKey) + + const showPopover = (tourStep, tourNodes, popover) => { + popoverAnchor = tourNodes[tourStep.id] + popover?.show() + } + + $: showPopover(tourStep, tourNodes, popover) + + const navigateStep = step => { + if (step.route) { + const activeNav = $layout.children.find(c => $isActive(c.path)) + if (activeNav) { + store.update(state => { + if (!state.previousTopNavPath) state.previousTopNavPath = {} + state.previousTopNavPath[activeNav.path] = window.location.pathname + $goto(state.previousTopNavPath[step.route] || step.route) + return state + }) + } + } + } + + const nextStep = async () => { + if (!lastStep === true) { + let target = tourSteps[tourStepIdx + 1] + if (target) { + store.update(state => ({ + ...state, + tourStepKey: target.id, + })) + navigateStep(target) + } else { + console.log("Could not retrieve step") + } + } else { + if (typeof tourStep.onComplete === "function") { + tourStep.onComplete() + } + popover.hide() + } + } + + const previousStep = async () => { + if (tourStepIdx > 0) { + let target = tourSteps[tourStepIdx - 1] + if (target) { + store.update(state => ({ + ...state, + tourStepKey: target.id, + })) + navigateStep(target) + } else { + console.log("Could not retrieve step") + } + } + } + + const getCurrentStepIdx = (steps, tourStepKey) => { + if (!steps?.length) { + return + } + if (steps?.length && !tourStepKey) { + return 0 + } + return steps.findIndex(step => step.id === tourStepKey) + } + + +{#key tourStepKey} + + +
+ {tourStep?.title || "-"} +
{`${tourStepIdx + 1}/${tourSteps?.length}`}
+
+ + + {#if tourStep.layout} + + {:else} + {tourStep?.body || ""} + {/if} + + + +
+
+{/key} + + diff --git a/packages/builder/src/components/portal/onboarding/TourWrap.svelte b/packages/builder/src/components/portal/onboarding/TourWrap.svelte new file mode 100644 index 0000000000..5761247ba3 --- /dev/null +++ b/packages/builder/src/components/portal/onboarding/TourWrap.svelte @@ -0,0 +1,27 @@ + + + diff --git a/packages/builder/src/components/portal/onboarding/steps/OnboardingData.svelte b/packages/builder/src/components/portal/onboarding/steps/OnboardingData.svelte new file mode 100644 index 0000000000..674d5c14ab --- /dev/null +++ b/packages/builder/src/components/portal/onboarding/steps/OnboardingData.svelte @@ -0,0 +1,10 @@ +
+ In this section you can mange the data for your app: +
    +
  • Connect data sources
  • +
  • Edit data
  • +
  • Manage read & write access
  • +
  • Create views
  • +
  • Add bindings
  • +
+
diff --git a/packages/builder/src/components/portal/onboarding/steps/OnboardingDesign.svelte b/packages/builder/src/components/portal/onboarding/steps/OnboardingDesign.svelte new file mode 100644 index 0000000000..84d84777f5 --- /dev/null +++ b/packages/builder/src/components/portal/onboarding/steps/OnboardingDesign.svelte @@ -0,0 +1,10 @@ +
+ After setting up your data, Design is where you build the screens for your + app: +
    +
  • Add screens
  • +
  • Add components
  • +
  • Choose your theme
  • +
  • Edit navigation
  • +
+
diff --git a/packages/builder/src/components/portal/onboarding/steps/OnboardingPublish.svelte b/packages/builder/src/components/portal/onboarding/steps/OnboardingPublish.svelte new file mode 100644 index 0000000000..8913d77482 --- /dev/null +++ b/packages/builder/src/components/portal/onboarding/steps/OnboardingPublish.svelte @@ -0,0 +1,7 @@ +
+ Once you’re happy with your app you can publish it to production! +

+ After publishing, any changes you make will not take affect until you next + publish. +

+
diff --git a/packages/builder/src/components/portal/onboarding/steps/index.js b/packages/builder/src/components/portal/onboarding/steps/index.js new file mode 100644 index 0000000000..8e27748f36 --- /dev/null +++ b/packages/builder/src/components/portal/onboarding/steps/index.js @@ -0,0 +1,3 @@ +export { default as OnboardingData } from "./OnboardingData.svelte" +export { default as OnboardingDesign } from "./OnboardingDesign.svelte" +export { default as OnboardingPublish } from "./OnboardingPublish.svelte" diff --git a/packages/builder/src/components/portal/onboarding/tourHandler.js b/packages/builder/src/components/portal/onboarding/tourHandler.js new file mode 100644 index 0000000000..d4a564f23a --- /dev/null +++ b/packages/builder/src/components/portal/onboarding/tourHandler.js @@ -0,0 +1,47 @@ +import { store } from "builderStore/index" +import { get } from "svelte/store" + +const registerNode = async (node, tourStepKey) => { + if (!node) { + console.log("Tour Handler - an anchor node is required") + } + + if (!get(store).tourKey) { + console.log("Tour Handler - No active tour ", tourStepKey, node) + return + } + + store.update(state => { + const update = { + ...state, + tourNodes: { + ...state.tourNodes, + [tourStepKey]: node, + }, + } + return update + }) +} + +export function tourHandler(node, tourStepKey) { + if (node && tourStepKey) { + registerNode(node, tourStepKey) + } + return { + destroy: () => { + const updatedTourNodes = get(store).tourNodes + if (updatedTourNodes && updatedTourNodes[tourStepKey]) { + delete updatedTourNodes[tourStepKey] + store.update(state => { + const update = { + ...state, + tourNodes: { + ...updatedTourNodes, + }, + } + return update + }) + } + }, + } +} diff --git a/packages/builder/src/components/portal/onboarding/tours.js b/packages/builder/src/components/portal/onboarding/tours.js new file mode 100644 index 0000000000..59f4c8daf9 --- /dev/null +++ b/packages/builder/src/components/portal/onboarding/tours.js @@ -0,0 +1,90 @@ +import { get } from "svelte/store" +import { store } from "builderStore" +import { users, auth } from "stores/portal" +import analytics from "analytics" +import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps" +const ONBOARDING_EVENT_PREFIX = "onboarding" + +export const TOUR_STEP_KEYS = { + BUILDER_APP_PUBLISH: "builder-app-publish", + BUILDER_DATA_SECTION: "builder-data-section", + BUILDER_DESIGN_SECTION: "builder-design-section", + BUILDER_AUTOMATE_SECTION: "builder-automate-section", +} + +export const TOUR_KEYS = { + TOUR_BUILDER_ONBOARDING: "builder-onboarding", +} + +const tourEvent = eventKey => { + console.log("Emitting EVENT ", eventKey) + analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, { + eventSource: EventSource.PORTAL, + }) +} + +const getTours = () => { + return { + [TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: [ + { + id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION, + title: "Data", + route: "/builder/app/:application/data", + layout: OnboardingData, + query: ".topcenternav .spectrum-Tabs-item#builder-data-tab", + onLoad: async () => { + tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION) + }, + }, + { + id: TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION, + title: "Design", + route: "/builder/app/:application/design", + layout: OnboardingDesign, + query: ".topcenternav .spectrum-Tabs-item#builder-design-tab", + onLoad: () => { + tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION) + }, + }, + { + id: TOUR_STEP_KEYS.BUILDER_AUTOMATE_SECTION, + title: "Automations", + route: "/builder/app/:application/automate", + query: ".topcenternav .spectrum-Tabs-item#builder-automate-tab", + body: "Once you have your app screens made, you can set up automations to fit in with your current workflow", + onLoad: () => { + tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATE_SECTION) + }, + }, + { + id: TOUR_STEP_KEYS.BUILDER_APP_PUBLISH, + title: "Publish", + layout: OnboardingPublish, + query: ".toprightnav #builder-app-publish-button", + onLoad: () => { + tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH) + }, + onComplete: async () => { + // Mark the users onboarding as complete + // Clear all tour related state + if (get(auth).user) { + await users.save({ + ...get(auth).user, + onboardedAt: new Date().toISOString(), + }) + + store.update(state => ({ + ...state, + tourNodes: null, + tourKey: null, + tourKeyStep: null, + onboarding: false, + })) + } + }, + }, + ], + } +} + +export const TOURS = getTours() diff --git a/packages/builder/src/pages/builder/app/[application]/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/_layout.svelte index 06ae57fa85..c99776320f 100644 --- a/packages/builder/src/pages/builder/app/[application]/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/_layout.svelte @@ -1,6 +1,7 @@