1
0
Fork 0
mirror of synced 2024-08-27 16:01:28 +12:00

Merge pull request #11255 from Budibase/feature/skippable-tours

Skippable tours
This commit is contained in:
deanhannigan 2023-07-19 16:40:19 +01:00 committed by GitHub
commit fbdcba8457
4 changed files with 129 additions and 138 deletions

View file

@ -1,5 +1,5 @@
<script> <script>
import { Popover, Layout, Heading, Body, Button } from "@budibase/bbui" import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui"
import { store } from "builderStore" import { store } from "builderStore"
import { TOURS } from "./tours.js" import { TOURS } from "./tours.js"
import { goto, layout, isActive } from "@roxi/routify" import { goto, layout, isActive } from "@roxi/routify"
@ -10,17 +10,20 @@
let tourStep let tourStep
let tourStepIdx let tourStepIdx
let lastStep let lastStep
let skipping = false
$: tourNodes = { ...$store.tourNodes } $: tourNodes = { ...$store.tourNodes }
$: tourKey = $store.tourKey $: tourKey = $store.tourKey
$: tourStepKey = $store.tourStepKey $: tourStepKey = $store.tourStepKey
$: tour = TOURS[tourKey]
$: tourOnSkip = tour?.onSkip
const updateTourStep = (targetStepKey, tourKey) => { const updateTourStep = (targetStepKey, tourKey) => {
if (!tourKey) { if (!tourKey) {
return return
} }
if (!tourSteps?.length) { if (!tourSteps?.length) {
tourSteps = [...TOURS[tourKey]] tourSteps = [...tour.steps]
} }
tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey) tourStepIdx = getCurrentStepIdx(tourSteps, targetStepKey)
lastStep = tourStepIdx + 1 == tourSteps.length lastStep = tourStepIdx + 1 == tourSteps.length
@ -71,23 +74,8 @@
tourStep.onComplete() tourStep.onComplete()
} }
popover.hide() popover.hide()
if (tourStep.endRoute) { if (tour.endRoute) {
$goto(tourStep.endRoute) $goto(tour.endRoute)
}
}
}
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")
} }
} }
} }
@ -132,16 +120,23 @@
</Body> </Body>
<div class="tour-footer"> <div class="tour-footer">
<div class="tour-navigation"> <div class="tour-navigation">
{#if tourStepIdx > 0} {#if typeof tourOnSkip === "function"}
<Button <Link
secondary secondary
on:click={previousStep} quiet
disabled={tourStepIdx == 0} on:click={() => {
skipping = true
tourOnSkip()
if (tour.endRoute) {
$goto(tour.endRoute)
}
}}
disabled={skipping}
> >
<div>Back</div> Skip
</Button> </Link>
{/if} {/if}
<Button cta on:click={nextStep}> <Button cta on:click={nextStep} disabled={skipping}>
<div>{lastStep ? "Finish" : "Next"}</div> <div>{lastStep ? "Finish" : "Next"}</div>
</Button> </Button>
</div> </div>
@ -157,9 +152,10 @@
padding: var(--spacing-xl); padding: var(--spacing-xl);
} }
.tour-navigation { .tour-navigation {
grid-gap: var(--spectrum-alias-grid-baseline); grid-gap: var(--spacing-xl);
display: flex; display: flex;
justify-content: end; justify-content: end;
align-items: center;
} }
.tour-body :global(.feature-list) { .tour-body :global(.feature-list) {
margin-bottom: 0px; margin-bottom: 0px;

View file

@ -13,7 +13,7 @@
const registerTourNode = (tourKey, stepKey) => { const registerTourNode = (tourKey, stepKey) => {
if (ready && !registered && tourKey) { if (ready && !registered && tourKey) {
currentTourStep = TOURS[tourKey].find(step => step.id === stepKey) currentTourStep = TOURS[tourKey].steps.find(step => step.id === stepKey)
if (!currentTourStep) { if (!currentTourStep) {
return return
} }

View file

@ -4,6 +4,7 @@ import { auth } from "stores/portal"
import analytics from "analytics" import analytics from "analytics"
import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps" import { OnboardingData, OnboardingDesign, OnboardingPublish } from "./steps"
import { API } from "api" import { API } from "api"
const ONBOARDING_EVENT_PREFIX = "onboarding" const ONBOARDING_EVENT_PREFIX = "onboarding"
export const TOUR_STEP_KEYS = { export const TOUR_STEP_KEYS = {
@ -20,6 +21,37 @@ export const TOUR_KEYS = {
FEATURE_ONBOARDING: "feature-onboarding", FEATURE_ONBOARDING: "feature-onboarding",
} }
const endUserOnboarding = async ({ skipped = false } = {}) => {
// Mark the users onboarding as complete
// Clear all tour related state
if (get(auth).user) {
try {
await API.updateSelf({
onboardedAt: new Date().toISOString(),
})
if (skipped) {
tourEvent("skipped")
}
// Update the cached user
await auth.getSelf()
store.update(state => ({
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
} catch (e) {
console.log("Onboarding failed", e)
return false
}
return true
}
}
const tourEvent = eventKey => { const tourEvent = eventKey => {
analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, { analytics.captureEvent(`${ONBOARDING_EVENT_PREFIX}:${eventKey}`, {
eventSource: EventSource.PORTAL, eventSource: EventSource.PORTAL,
@ -28,7 +60,8 @@ const tourEvent = eventKey => {
const getTours = () => { const getTours = () => {
return { return {
[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: [ [TOUR_KEYS.TOUR_BUILDER_ONBOARDING]: {
steps: [
{ {
id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION, id: TOUR_STEP_KEYS.BUILDER_DATA_SECTION,
title: "Data", title: "Data",
@ -76,34 +109,20 @@ const getTours = () => {
title: "Publish", title: "Publish",
layout: OnboardingPublish, layout: OnboardingPublish,
route: "/builder/app/:application/design", route: "/builder/app/:application/design",
endRoute: "/builder/app/:application/data",
query: ".toprightnav #builder-app-publish-button", query: ".toprightnav #builder-app-publish-button",
onLoad: () => { onLoad: () => {
tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH) tourEvent(TOUR_STEP_KEYS.BUILDER_APP_PUBLISH)
}, },
onComplete: async () => { onComplete: endUserOnboarding,
// Mark the users onboarding as complete
// Clear all tour related state
if (get(auth).user) {
await API.updateSelf({
onboardedAt: new Date().toISOString(),
})
// Update the cached user
await auth.getSelf()
store.update(state => ({
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
}
},
}, },
], ],
[TOUR_KEYS.FEATURE_ONBOARDING]: [ onSkip: async () => {
await endUserOnboarding({ skipped: true })
},
endRoute: "/builder/app/:application/data",
},
[TOUR_KEYS.FEATURE_ONBOARDING]: {
steps: [
{ {
id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT, id: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT,
title: "Users", title: "Users",
@ -112,27 +131,10 @@ const getTours = () => {
onLoad: () => { onLoad: () => {
tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT) tourEvent(TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT)
}, },
onComplete: async () => { onComplete: endUserOnboarding,
// Push the onboarding forward
if (get(auth).user) {
await API.updateSelf({
onboardedAt: new Date().toISOString(),
})
// Update the cached user
await auth.getSelf()
store.update(state => ({
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
}
},
}, },
], ],
},
} }
} }

View file

@ -27,7 +27,7 @@
import TourPopover from "components/portal/onboarding/TourPopover.svelte" import TourPopover from "components/portal/onboarding/TourPopover.svelte"
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte" import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
import { UserAvatars } from "@budibase/frontend-core" import { UserAvatars } from "@budibase/frontend-core"
import { TOUR_KEYS, TOURS } from "components/portal/onboarding/tours.js" import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
import PreviewOverlay from "./_components/PreviewOverlay.svelte" import PreviewOverlay from "./_components/PreviewOverlay.svelte"
export let application export let application
@ -87,17 +87,10 @@
// Check if onboarding is enabled. // Check if onboarding is enabled.
if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) { if (isEnabled(TENANT_FEATURE_FLAGS.ONBOARDING_TOUR)) {
if (!$auth.user?.onboardedAt) { if (!$auth.user?.onboardedAt) {
// Determine the correct step
const activeNav = $layout.children.find(c => $isActive(c.path))
const onboardingTour = TOURS[TOUR_KEYS.TOUR_BUILDER_ONBOARDING]
const targetStep = activeNav
? onboardingTour.find(step => step.route === activeNav?.path)
: null
await store.update(state => ({ await store.update(state => ({
...state, ...state,
onboarding: true, onboarding: true,
tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING, tourKey: TOUR_KEYS.TOUR_BUILDER_ONBOARDING,
tourStepKey: targetStep?.id,
})) }))
} else { } else {
// Feature tour date // Feature tour date