Refactored tours. Tours will end if a TourWrap is removed from screen.

This commit is contained in:
Dean 2024-02-15 15:23:13 +00:00
parent 5c5dc4c155
commit b0cd3d4d03
8 changed files with 109 additions and 52 deletions

View File

@ -1,6 +1,6 @@
<script> <script>
import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui" import { Popover, Layout, Heading, Body, Button, Link } from "@budibase/bbui"
import { TOURS } from "./tours.js" import { TOURS, getCurrentStepIdx } from "./tours.js"
import { goto, layout, isActive } from "@roxi/routify" import { goto, layout, isActive } from "@roxi/routify"
import { builderStore } from "stores/builder" import { builderStore } from "stores/builder"
@ -20,6 +20,12 @@
const updateTourStep = (targetStepKey, tourKey) => { const updateTourStep = (targetStepKey, tourKey) => {
if (!tourKey) { if (!tourKey) {
tourSteps = null
tourStepIdx = null
lastStep = null
tourStep = null
popoverAnchor = null
popover = null
return return
} }
if (!tourSteps?.length) { if (!tourSteps?.length) {
@ -78,16 +84,6 @@
} }
} }
} }
const getCurrentStepIdx = (steps, tourStepKey) => {
if (!steps?.length) {
return
}
if (steps?.length && !tourStepKey) {
return 0
}
return steps.findIndex(step => step.id === tourStepKey)
}
</script> </script>
{#if tourKey} {#if tourKey}
@ -100,6 +96,7 @@
dismissible={false} dismissible={false}
offset={15} offset={15}
handlePostionUpdate={tourStep?.positionHandler} handlePostionUpdate={tourStep?.positionHandler}
customZindex={3}
> >
<div class="tour-content"> <div class="tour-content">
<Layout noPadding gap="M"> <Layout noPadding gap="M">

View File

@ -1,44 +1,62 @@
<script> <script>
import { tourHandler } from "./tourHandler" import { tourHandler } from "./tourHandler"
import { TOURSBYSTEP } from "./tours" import { TOURSBYSTEP, TOURS, getCurrentStepIdx } from "./tours"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { builderStore } from "stores/builder" import { builderStore } from "stores/builder"
export let stepKeys = [] export let stepKeys = []
let ready = false let ready = false
let handler let registered = {}
let registered = []
const registerTourNode = (tourKey, stepKey) => { const registerTourNode = (tourKey, stepKey) => {
const step = TOURSBYSTEP[stepKey] const step = TOURSBYSTEP[stepKey]
if ( if (ready && step && !registered[stepKey] && step?.tour === tourKey) {
ready &&
step &&
!registered.includes(stepKey) &&
step?.tour === tourKey
) {
const elem = document.querySelector(step.query) const elem = document.querySelector(step.query)
handler = tourHandler(elem, stepKey) registered[stepKey] = tourHandler(elem, stepKey)
registered.push(stepKey)
} }
} }
$: tourKeyWatch = $builderStore.tourKey $: tourKeyWatch = $builderStore.tourKey
$: tourStepKeyWatch = $builderStore.tourStepKey
$: if (tourKeyWatch || stepKeys || ready) { $: if (tourKeyWatch || stepKeys || ready) {
stepKeys.forEach(tourStepKey => { stepKeys.forEach(tourStepKey => {
registerTourNode(tourKeyWatch, tourStepKey) registerTourNode(tourKeyWatch, tourStepKey)
}) })
} }
$: if (tourKeyWatch || tourStepKeyWatch) {
let tourStepIdx = getCurrentStepIdx(
TOURS[tourKeyWatch]?.steps,
tourStepKeyWatch
)
let currentStep = TOURS[tourKeyWatch]?.steps?.[tourStepIdx]
if (currentStep?.scrollIntoView) {
let currentNode = $builderStore.tourNodes?.[currentStep.id]
if (currentNode) {
currentNode.scrollIntoView({ behavior: "smooth", block: "center" })
}
}
}
onMount(() => { onMount(() => {
ready = true ready = true
}) })
onDestroy(() => { onDestroy(() => {
if (handler) { Object.entries(registered).forEach(entry => {
const handler = entry[1]
const stepKey = entry[0]
// Run step destroy, de-register nodes in the builderStore and local cache
handler.destroy() handler.destroy()
} delete registered[stepKey]
// Check if the step is part of an active tour. End the tour if that is the case
const step = TOURSBYSTEP[stepKey]
if (step.tour === tourKeyWatch) {
builderStore.setTour()
}
})
}) })
</script> </script>

View File

@ -32,14 +32,18 @@ export const TOUR_KEYS = {
BUILDER_FORM_VIEW_UPDATE: "builder-form-view-update", BUILDER_FORM_VIEW_UPDATE: "builder-form-view-update",
} }
export const getCurrentStepIdx = (steps, tourStepKey) => {
if (!steps?.length) {
return
}
if (steps?.length && !tourStepKey) {
return 0
}
return steps.findIndex(step => step.id === tourStepKey)
}
const resetTourState = () => { const resetTourState = () => {
builderStore.update(state => ({ builderStore.setTour()
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
} }
const endUserOnboarding = async ({ skipped = false } = {}) => { const endUserOnboarding = async ({ skipped = false } = {}) => {
@ -58,6 +62,7 @@ const endUserOnboarding = async ({ skipped = false } = {}) => {
// Update the cached user // Update the cached user
await auth.getSelf() await auth.getSelf()
builderStore.endBuilderOnboarding()
resetTourState() resetTourState()
} catch (e) { } catch (e) {
console.error("Onboarding failed", e) console.error("Onboarding failed", e)
@ -222,6 +227,7 @@ const getTours = () => {
}, },
positionHandler: customPositionHandler, positionHandler: customPositionHandler,
align: "left-outside", align: "left-outside",
scrollIntoView: true,
}, },
], ],
onSkip: async () => { onSkip: async () => {

View File

@ -95,7 +95,7 @@
const release_date = new Date("2023-03-01T00:00:00.000Z") const release_date = new Date("2023-03-01T00:00:00.000Z")
const onboarded = new Date($auth.user?.onboardedAt) const onboarded = new Date($auth.user?.onboardedAt)
if (onboarded < release_date) { if (onboarded < release_date) {
builderStore.startTour(TOUR_KEYS.FEATURE_ONBOARDING) builderStore.setTour(TOUR_KEYS.FEATURE_ONBOARDING)
} }
} }
} }

View File

@ -155,7 +155,7 @@
// Handler for Datasource Screen Creation // Handler for Datasource Screen Creation
const completeDatasourceScreenCreation = async () => { const completeDatasourceScreenCreation = async () => {
templates = rowListScreen(selectedDatasources) templates = rowListScreen(selectedDatasources, mode)
const screens = templates.map(template => { const screens = templates.map(template => {
let screenTemplate = template.create() let screenTemplate = template.create()
@ -192,10 +192,17 @@
} }
const loadNewScreen = createdScreens => { const loadNewScreen = createdScreens => {
const lastScreen = createdScreens.slice(-1) const lastScreen = createdScreens.slice(-1)[0]
// Go to new screen // Go to new screen
$goto(`./${lastScreen._id}`) if (lastScreen?.props?._children.length) {
// Focus on the main component for the streen type
const mainComponent = lastScreen?.props?._children?.[0]._id
$goto(`./${lastScreen._id}/${mainComponent}`)
} else {
$goto(`./${lastScreen._id}`)
}
screenStore.select(lastScreen._id) screenStore.select(lastScreen._id)
} }
@ -206,8 +213,6 @@
return screenTemplate return screenTemplate
}) })
const createdScreens = await createScreens({ screens, screenAccessRole }) const createdScreens = await createScreens({ screens, screenAccessRole })
const lastScreen = createdScreens?.slice(-1)?.pop()
const mainComponent = lastScreen?.props?._children?.[0]._id
if (formType === "Update" || formType === "Create") { if (formType === "Update" || formType === "Create") {
const associatedTour = const associatedTour =
@ -217,18 +222,12 @@
const tourRequired = !$auth?.user?.tours?.[associatedTour] const tourRequired = !$auth?.user?.tours?.[associatedTour]
if (tourRequired) { if (tourRequired) {
builderStore.update(state => ({ builderStore.setTour(associatedTour)
...state,
tourStepKey: null,
tourNodes: null,
tourKey: associatedTour,
}))
} }
} }
// Go to new screen // Go to new screen
$goto(`./${lastScreen._id}/${mainComponent}`) loadNewScreen(createdScreens)
screenStore.select(lastScreen._id)
} }
// Submit screen config for creation. // Submit screen config for creation.

View File

@ -4,7 +4,7 @@
import blankImage from "./images/blank.png" import blankImage from "./images/blank.png"
import tableImage from "./images/table.png" import tableImage from "./images/table.png"
import gridImage from "./images/grid.png" import gridImage from "./images/grid.png"
import formImage from "./images/form.png" //optimized example import formImage from "./images/form.png"
import CreateScreenModal from "./CreateScreenModal.svelte" import CreateScreenModal from "./CreateScreenModal.svelte"
import { screenStore } from "stores/builder" import { screenStore } from "stores/builder"

View File

@ -7,7 +7,7 @@ import { TOUR_KEYS } from "components/portal/onboarding/tours.js"
export const INITIAL_BUILDER_STATE = { export const INITIAL_BUILDER_STATE = {
previousTopNavPath: {}, previousTopNavPath: {},
highlightedSettingKey: null, highlightedSetting: null,
propertyFocus: null, propertyFocus: null,
builderSidePanel: false, builderSidePanel: false,
onboarding: false, onboarding: false,
@ -61,7 +61,7 @@ export class BuilderStore extends BudiStore {
highlightSetting(key, type) { highlightSetting(key, type) {
this.update(state => ({ this.update(state => ({
...state, ...state,
highlightedSetting: { key, type: type || "info" }, highlightedSetting: key ? { key, type: type || "info" } : null,
})) }))
} }
@ -135,9 +135,18 @@ export class BuilderStore extends BudiStore {
})) }))
} }
startTour(tourKey) { endBuilderOnboarding() {
this.update(state => ({ this.update(state => ({
...state, ...state,
onboarding: false,
}))
}
setTour(tourKey) {
this.update(state => ({
...state,
tourStepKey: null,
tourNodes: null,
tourKey: tourKey, tourKey: tourKey,
})) }))
} }

View File

@ -88,14 +88,42 @@ describe("Builder store", () => {
) )
}) })
it("Sync a highlighted setting key to state", ctx => { it("Sync a highlighted setting key to state. Default to info type", ctx => {
expect(ctx.test.store.highlightedSettingKey).toBeNull() expect(ctx.test.store.highlightedSetting).toBeNull()
ctx.test.builderStore.highlightSetting("testing") ctx.test.builderStore.highlightSetting("testing")
expect(ctx.test.store).toStrictEqual({ expect(ctx.test.store).toStrictEqual({
...INITIAL_BUILDER_STATE, ...INITIAL_BUILDER_STATE,
highlightedSettingKey: "testing", highlightedSetting: {
key: "testing",
type: "info",
},
})
})
it("Sync a highlighted setting key to state. Use provided type", ctx => {
expect(ctx.test.store.highlightedSetting).toBeNull()
ctx.test.builderStore.highlightSetting("testing", "error")
expect(ctx.test.store).toStrictEqual({
...INITIAL_BUILDER_STATE,
highlightedSetting: {
key: "testing",
type: "error",
},
})
})
it("Sync a highlighted setting key to state. Unset when no value is passed", ctx => {
expect(ctx.test.store.highlightedSetting).toBeNull()
ctx.test.builderStore.highlightSetting("testing", "error")
ctx.test.builderStore.highlightSetting()
expect(ctx.test.store).toStrictEqual({
...INITIAL_BUILDER_STATE,
}) })
}) })