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>
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 { builderStore } from "stores/builder"
@ -20,6 +20,12 @@
const updateTourStep = (targetStepKey, tourKey) => {
if (!tourKey) {
tourSteps = null
tourStepIdx = null
lastStep = null
tourStep = null
popoverAnchor = null
popover = null
return
}
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>
{#if tourKey}
@ -100,6 +96,7 @@
dismissible={false}
offset={15}
handlePostionUpdate={tourStep?.positionHandler}
customZindex={3}
>
<div class="tour-content">
<Layout noPadding gap="M">

View File

@ -1,44 +1,62 @@
<script>
import { tourHandler } from "./tourHandler"
import { TOURSBYSTEP } from "./tours"
import { TOURSBYSTEP, TOURS, getCurrentStepIdx } from "./tours"
import { onMount, onDestroy } from "svelte"
import { builderStore } from "stores/builder"
export let stepKeys = []
let ready = false
let handler
let registered = []
let registered = {}
const registerTourNode = (tourKey, stepKey) => {
const step = TOURSBYSTEP[stepKey]
if (
ready &&
step &&
!registered.includes(stepKey) &&
step?.tour === tourKey
) {
if (ready && step && !registered[stepKey] && step?.tour === tourKey) {
const elem = document.querySelector(step.query)
handler = tourHandler(elem, stepKey)
registered.push(stepKey)
registered[stepKey] = tourHandler(elem, stepKey)
}
}
$: tourKeyWatch = $builderStore.tourKey
$: tourStepKeyWatch = $builderStore.tourStepKey
$: if (tourKeyWatch || stepKeys || ready) {
stepKeys.forEach(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(() => {
ready = true
})
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()
}
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>

View File

@ -32,14 +32,18 @@ export const TOUR_KEYS = {
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 = () => {
builderStore.update(state => ({
...state,
tourNodes: undefined,
tourKey: undefined,
tourKeyStep: undefined,
onboarding: false,
}))
builderStore.setTour()
}
const endUserOnboarding = async ({ skipped = false } = {}) => {
@ -58,6 +62,7 @@ const endUserOnboarding = async ({ skipped = false } = {}) => {
// Update the cached user
await auth.getSelf()
builderStore.endBuilderOnboarding()
resetTourState()
} catch (e) {
console.error("Onboarding failed", e)
@ -222,6 +227,7 @@ const getTours = () => {
},
positionHandler: customPositionHandler,
align: "left-outside",
scrollIntoView: true,
},
],
onSkip: async () => {

View File

@ -95,7 +95,7 @@
const release_date = new Date("2023-03-01T00:00:00.000Z")
const onboarded = new Date($auth.user?.onboardedAt)
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
const completeDatasourceScreenCreation = async () => {
templates = rowListScreen(selectedDatasources)
templates = rowListScreen(selectedDatasources, mode)
const screens = templates.map(template => {
let screenTemplate = template.create()
@ -192,10 +192,17 @@
}
const loadNewScreen = createdScreens => {
const lastScreen = createdScreens.slice(-1)
const lastScreen = createdScreens.slice(-1)[0]
// 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)
}
@ -206,8 +213,6 @@
return screenTemplate
})
const createdScreens = await createScreens({ screens, screenAccessRole })
const lastScreen = createdScreens?.slice(-1)?.pop()
const mainComponent = lastScreen?.props?._children?.[0]._id
if (formType === "Update" || formType === "Create") {
const associatedTour =
@ -217,18 +222,12 @@
const tourRequired = !$auth?.user?.tours?.[associatedTour]
if (tourRequired) {
builderStore.update(state => ({
...state,
tourStepKey: null,
tourNodes: null,
tourKey: associatedTour,
}))
builderStore.setTour(associatedTour)
}
}
// Go to new screen
$goto(`./${lastScreen._id}/${mainComponent}`)
screenStore.select(lastScreen._id)
loadNewScreen(createdScreens)
}
// Submit screen config for creation.

View File

@ -4,7 +4,7 @@
import blankImage from "./images/blank.png"
import tableImage from "./images/table.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 { screenStore } from "stores/builder"

View File

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

View File

@ -88,14 +88,42 @@ describe("Builder store", () => {
)
})
it("Sync a highlighted setting key to state", ctx => {
expect(ctx.test.store.highlightedSettingKey).toBeNull()
it("Sync a highlighted setting key to state. Default to info type", ctx => {
expect(ctx.test.store.highlightedSetting).toBeNull()
ctx.test.builderStore.highlightSetting("testing")
expect(ctx.test.store).toStrictEqual({
...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,
})
})