Refactored tours. Tours will end if a TourWrap is removed from screen.
This commit is contained in:
parent
5c5dc4c155
commit
b0cd3d4d03
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue