Files
windmill/frontend/src/lib/components/tutorials/Tutorial.svelte
T
Diego ImbertandClaude Opus 4.6 5d79f33590 Final Svelte 5 migration (#8211)
* Remove $$props.field usage

* Rename slots to ensure no hyphen

* _props

* _trigger

* OnSelectedIteration type correct capitalization

* rename _content

* Remove afterUpdate

* Migrate everything to svelte 5

* array bind

* Fix popover

* type never

* nit fixes

* Fixed many trivial errors

* onClick

* Fix errors

* use let:

* nit typing

* fix: wrap state_referenced_locally vars with untrack()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add untrack import

* Fix all syntax errors due to untrack migration

* Fix undefined errors

* Fix more undefined errors

* untrack(() => initialOpen)

* svelte-ignore

* Fix state_descriptors_fixed error in Chart.svelte

Use $state.snapshot() to pass plain copies of data/options to Chart.js
instead of $state proxies. Chart.js's listenArrayEvents tries to define
property descriptors on data arrays, which Svelte 5 proxies reject.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* nit typing

* Merge issue

* Fix "path is not set" error in resource picker / editor

* Fix InputTransformForm error when rerunning some flows

* fix npm run check

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-05 18:11:40 +01:00

158 lines
3.6 KiB
Svelte

<script lang="ts">
import { driver, type Driver, type DriveStep } from 'driver.js'
import { createEventDispatcher, mount } from 'svelte'
import { updateProgress } from '$lib/tutorialUtils'
import { ignoredTutorials } from './ignoredTutorials'
import SkipTutorials from './SkipTutorials.svelte'
import TutorialControls from './TutorialControls.svelte'
import TutorialInner from './TutorialInner.svelte'
import { isCurrentlyInTutorial } from '$lib/stores'
type Options = {
indexToInsertAt?: number
skipStepsCount?: number
}
interface Props {
index?: number;
name?: string;
tainted?: boolean;
onDestroyed?: (() => void) | undefined;
getSteps?: (driver: Driver, options?: Options | undefined) => DriveStep[];
}
let {
index = 0,
name = 'action',
tainted = false,
onDestroyed = undefined,
getSteps = () => []
}: Props = $props();
let totalSteps = 0
let tutorial: Driver | undefined = $state(undefined)
const dispatch = createEventDispatcher()
// Render controls needs to be exposed so steps that have a custom render can call it
export function renderControls({ config, state }) {
const popoverContent = document.querySelector('#driver-popover-content')
popoverContent?.addEventListener('pointerdown', (event) => {
event.stopPropagation()
})
const popoverDescription = document.querySelector('#driver-popover-description')
if (!tutorial) {
return
}
if (state.activeIndex == 0) {
const div = document.createElement('div')
mount(SkipTutorials, {
target: div,
events: {
skipAll: () => {
dispatch('skipAll')
tutorial?.destroy()
},
skipThis: () => {
updateProgress(index)
tutorial?.destroy()
}
}
})
if (popoverDescription) {
popoverDescription.appendChild(div)
}
}
const controls = document.createElement('div')
mount(TutorialControls, {
target: controls,
props: {
activeIndex: state.activeIndex,
totalSteps
},
events: {
next: () => {
const step = tutorial?.getActiveStep()
if (step) {
if (tutorial?.getActiveStep()?.popover?.onNextClick) {
const activeElement = tutorial?.getActiveElement()
tutorial?.getActiveStep()?.popover?.onNextClick?.(activeElement, step, {
config,
state,
driver: tutorial
})
} else {
tutorial?.moveNext()
}
}
},
previous: () => {
const step = tutorial?.getActiveStep()
if (step) {
if (tutorial?.getActiveStep()?.popover?.onPrevClick) {
const activeElement = tutorial?.getActiveElement()
tutorial?.getActiveStep()?.popover?.onPrevClick?.(activeElement, step, {
config,
state,
driver: tutorial
})
} else {
tutorial?.movePrevious()
}
}
}
}
})
if (popoverDescription) {
popoverDescription.appendChild(controls)
}
}
export const runTutorial = (options?: Options | undefined) => {
if (tainted) {
dispatch('error', { detail: name })
return
}
isCurrentlyInTutorial.val = true
tutorial = driver({
allowClose: true,
disableActiveInteraction: true,
showButtons: ['close'],
showProgress: false,
overlayColor: 'rgba(0, 0, 0, 0.8)',
onPopoverRender: (popover, { config, state }) => {
renderControls({ config, state })
},
onDestroyed: () => {
onDestroyed?.()
if (!tutorial?.hasNextStep()) {
$ignoredTutorials = Array.from(new Set([...$ignoredTutorials, index]))
}
isCurrentlyInTutorial.val = false
}
})
const steps = getSteps(tutorial, options)
totalSteps = steps.length
tutorial.setSteps(steps)
tutorial.drive()
}
</script>
{#if tutorial}
<TutorialInner />
{/if}