mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 08:03:50 +00:00
feat(frontend): app editor tutorials (#2443)
* feat(frontend): wip * feat(frontend): skeleton done * feat(frontend): background runnables * feat(frontend): fix build * feat(frontend): finish background runnable tuto * feat(frontend): connection output * feat(frontend): add simple app tutorial * feat(frontend): add simple app trigger * feat(frontend): fix wording * feat(frontend): remove duplicate code * feat(frontend): wip tutorial rework * feat(frontend): Tutorial done * feat(frontend): Fix build
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { skipAllTodos } from '$lib/tutorialUtils'
|
||||
import AppTutorial from './tutorials/app/AppTutorial.svelte'
|
||||
import BackgroundRunnablesTutorial from './tutorials/app/BackgroundRunnablesTutorial.svelte'
|
||||
import ConnectionTutorial from './tutorials/app/ConnectionTutorial.svelte'
|
||||
|
||||
let backgroundRunnablesTutorial: BackgroundRunnablesTutorial | undefined = undefined
|
||||
let connectionTutorial: ConnectionTutorial | undefined = undefined
|
||||
let appTutorial: AppTutorial | undefined = undefined
|
||||
|
||||
export function runTutorialById(id: string, options?: { skipStepsCount?: number }) {
|
||||
if (id === 'backgroundrunnables') {
|
||||
backgroundRunnablesTutorial?.runTutorial(options?.skipStepsCount)
|
||||
} else if (id === 'connection') {
|
||||
connectionTutorial?.runTutorial()
|
||||
} else if (id === 'simpleapptutorial') {
|
||||
appTutorial?.runTutorial()
|
||||
}
|
||||
}
|
||||
|
||||
function skipAll() {
|
||||
skipAllTodos()
|
||||
}
|
||||
</script>
|
||||
|
||||
<AppTutorial
|
||||
bind:this={appTutorial}
|
||||
on:error
|
||||
on:skipAll={skipAll}
|
||||
on:reload
|
||||
index={7}
|
||||
name="simpleapptutorial"
|
||||
/>
|
||||
|
||||
<BackgroundRunnablesTutorial
|
||||
bind:this={backgroundRunnablesTutorial}
|
||||
on:error
|
||||
on:skipAll={skipAll}
|
||||
on:reload
|
||||
index={5}
|
||||
name="backgroundrunnables"
|
||||
/>
|
||||
|
||||
<ConnectionTutorial
|
||||
bind:this={connectionTutorial}
|
||||
on:error
|
||||
on:skipAll={skipAll}
|
||||
on:reload
|
||||
index={6}
|
||||
name="connection"
|
||||
/>
|
||||
@@ -396,6 +396,12 @@
|
||||
existingElement.innerHTML = theme
|
||||
}
|
||||
}
|
||||
|
||||
let appEditorHeader: AppEditorHeader | undefined = undefined
|
||||
|
||||
export function triggerTutorial() {
|
||||
appEditorHeader?.toggleTutorial()
|
||||
}
|
||||
</script>
|
||||
|
||||
<DarkModeObserver on:change={onThemeChange} />
|
||||
@@ -404,7 +410,7 @@
|
||||
|
||||
{#if !$userStore?.operator}
|
||||
{#if $appStore}
|
||||
<AppEditorHeader on:restore {versions} {policy} {fromHub} />
|
||||
<AppEditorHeader on:restore {versions} {policy} {fromHub} bind:this={appEditorHeader} />
|
||||
|
||||
{#if $mode === 'preview'}
|
||||
<SplitPanesWrapper>
|
||||
@@ -530,6 +536,7 @@
|
||||
selectedTab = 'insert'
|
||||
}
|
||||
}}
|
||||
id="app-editor-component-library-tab"
|
||||
>
|
||||
<div class="m-1 center-center">
|
||||
<Plus size={18} />
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
import { secondaryMenuLeftStore, secondaryMenuRightStore } from './settingsPanel/secondaryMenu'
|
||||
import ButtonDropdown from '$lib/components/common/button/ButtonDropdown.svelte'
|
||||
import { MenuItem } from '@rgossiaux/svelte-headlessui'
|
||||
import AppEditorTutorial from './AppEditorTutorial.svelte'
|
||||
|
||||
async function hash(message) {
|
||||
try {
|
||||
@@ -432,6 +433,12 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
let appEditorTutorial: AppEditorTutorial | undefined = undefined
|
||||
|
||||
export function toggleTutorial() {
|
||||
appEditorTutorial?.toggleTutorial()
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
@@ -835,6 +842,7 @@
|
||||
<Awareness />
|
||||
{/if}
|
||||
<div class="flex flex-row gap-2 justify-end items-center overflow-visible">
|
||||
<AppEditorTutorial bind:this={appEditorTutorial} />
|
||||
<ButtonDropdown hasPadding={false}>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<Button nonCaptureEvent size="xs" color="light">
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import AppTutorials from '../../AppTutorials.svelte'
|
||||
import { BookOpen } from 'lucide-svelte'
|
||||
|
||||
import ButtonDropdown from '$lib/components/common/button/ButtonDropdown.svelte'
|
||||
import TutorialItem from '$lib/components/tutorials/TutorialItem.svelte'
|
||||
import MenuItem from '$lib/components/common/menu/MenuItem.svelte'
|
||||
import { classNames } from '$lib/utils'
|
||||
import { resetAllTodos, skipAllTodos } from '$lib/tutorialUtils'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import { ignoredTutorials } from '$lib/components/tutorials/ignoredTutorials'
|
||||
import { tutorialsToDo } from '$lib/stores'
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { isAppTainted } from '$lib/components/tutorials/utils'
|
||||
|
||||
let appTutorials: AppTutorials | undefined = undefined
|
||||
let targetTutorial: string | undefined = undefined
|
||||
|
||||
const { app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
onMount(() => {
|
||||
if (!isAppTainted($app) && !$ignoredTutorials.includes(7) && $tutorialsToDo.includes(7)) {
|
||||
appTutorials?.runTutorialById('simpleapptutorial')
|
||||
}
|
||||
})
|
||||
|
||||
export function toggleTutorial() {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const tutorial = urlParams.get('tutorial')
|
||||
|
||||
if (tutorial === 'simpleapptutorial') {
|
||||
appTutorials?.runTutorialById('simpleapptutorial')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ButtonDropdown hasPadding={false}>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<Button nonCaptureEvent size="xs" color="light" variant="border">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<BookOpen size={16} />
|
||||
Tutorials
|
||||
</div>
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="items">
|
||||
<TutorialItem
|
||||
on:click={() => appTutorials?.runTutorialById('simpleapptutorial')}
|
||||
label="App tutorial"
|
||||
index={7}
|
||||
/>
|
||||
<TutorialItem
|
||||
on:click={() => appTutorials?.runTutorialById('backgroundrunnables')}
|
||||
label="Background runnables"
|
||||
index={5}
|
||||
/>
|
||||
<TutorialItem
|
||||
on:click={() => appTutorials?.runTutorialById('connection')}
|
||||
label="Connection"
|
||||
index={6}
|
||||
/>
|
||||
|
||||
<div class="border-t border-surface-hover" />
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
resetAllTodos()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'text-primary flex flex-row items-center text-left gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
Reset tutorials
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem on:click={() => skipAllTodos()}>
|
||||
<div
|
||||
class={classNames(
|
||||
'text-primary flex flex-row items-center text-left gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
)}
|
||||
>
|
||||
Skip tutorials
|
||||
</div>
|
||||
</MenuItem>
|
||||
</svelte:fragment>
|
||||
</ButtonDropdown>
|
||||
|
||||
<AppTutorials
|
||||
bind:this={appTutorials}
|
||||
on:reload
|
||||
on:error={({ detail }) => {
|
||||
targetTutorial = detail.detail
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfirmationModal
|
||||
open={targetTutorial !== undefined}
|
||||
title="Tutorial error"
|
||||
confirmationText="Open new tab"
|
||||
on:canceled={() => {
|
||||
targetTutorial = undefined
|
||||
}}
|
||||
on:confirmed={async () => {
|
||||
window.open(`/apps/add?tutorial=${targetTutorial}&nodraft=true`, '_blank')
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span> This tutorial can only be run on a new app.</span>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<style global>
|
||||
.driver-popover-title {
|
||||
@apply leading-6 text-primary text-base;
|
||||
}
|
||||
|
||||
.driver-popover-description {
|
||||
@apply text-secondary text-sm;
|
||||
}
|
||||
|
||||
.driver-popover {
|
||||
@apply p-6 bg-surface max-w-2xl;
|
||||
}
|
||||
</style>
|
||||
@@ -37,9 +37,10 @@
|
||||
|
||||
{#if connecting}
|
||||
<div class="absolute z-50 left-6 -top-[11px]">
|
||||
<Popup floatingConfig={{ strategy: 'absolute', placement: 'bottom-start' }}>
|
||||
<Popup floatingConfig={{ strategy: 'fixed', placement: 'bottom-start' }}>
|
||||
<svelte:fragment slot="button">
|
||||
<button
|
||||
id={`connect-output-${component.id}`}
|
||||
class="bg-red-500/70 border border-red-600 px-1 py-0.5"
|
||||
title="Outputs"
|
||||
aria-label="Open output"><Plug2 size={12} /></button
|
||||
|
||||
@@ -81,9 +81,9 @@
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
!$focusedGrid && $mode !== 'preview' ? 'border-dashed' : '',
|
||||
!$focusedGrid && $mode !== 'preview' ? 'outline-dashed' : '',
|
||||
'subgrid',
|
||||
'border-[#999999] dark:border-[#aaaaaa] border '
|
||||
'outline-[#999999] dark:outline-[#aaaaaa] outline-dotted outline-offset-2 outline-1'
|
||||
)}
|
||||
style={`transform: scale(${$scale / 100})`}
|
||||
>
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
icon={Eye}
|
||||
tooltip="Preview mode"
|
||||
disabled={loading}
|
||||
id="app-editor-preview-toggle"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
if (!$workspaceStore) return
|
||||
const res = await getGroup($workspaceStore, group.path)
|
||||
|
||||
console.log(res)
|
||||
if (!res) return
|
||||
|
||||
push(history, $app)
|
||||
@@ -108,21 +107,22 @@
|
||||
<ClearableInput bind:value={search} placeholder="Search components..." />
|
||||
</section>
|
||||
|
||||
<div class="relative">
|
||||
{#if componentsFiltered.reduce((acc, { components }) => acc + components.length, 0) === 0}
|
||||
<div class="relative" id="app-editor-component-list">
|
||||
{#if componentsFiltered.reduce((acc, { components, presets }) => acc + components.length + (Array.isArray(presets) ? presets.length : 0), 0) === 0}
|
||||
<div class="absolute left-0 top-0 w-full text-sm text-tertiary text-center py-6 px-2">
|
||||
No components found
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
{#each componentsFiltered as { title, components, presets }, index (index)}
|
||||
{#if components.length}
|
||||
{#if components.length || presets?.length}
|
||||
<div>
|
||||
<ListItem title={`${title} (${components.length})`}>
|
||||
<div class="flex flex-wrap gap-3 py-2">
|
||||
{#each components as item (item)}
|
||||
<div class="w-20">
|
||||
<button
|
||||
id={item}
|
||||
on:click={() => addComponent(item)}
|
||||
title={componentsRecord[item].name}
|
||||
class="transition-all border w-20 shadow-sm h-16 p-2 flex flex-col gap-2 items-center
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
$manuallyOpened[id] = $manuallyOpened[id] != undefined ? !$manuallyOpened[id] : true
|
||||
}
|
||||
}}
|
||||
id={`output-${id}`}
|
||||
>
|
||||
<div class="flex">
|
||||
<button
|
||||
|
||||
@@ -156,8 +156,12 @@
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="flex flex-col px-4 gap-2 text-sm" in:fly={{ duration: 50 }}>
|
||||
<div class="mt-2 flex justify-between gap-4">
|
||||
<div
|
||||
class="flex flex-col px-4 gap-2 text-sm"
|
||||
in:fly={{ duration: 50 }}
|
||||
id="app-editor-empty-runnable"
|
||||
>
|
||||
<div class="mt-2 flex justify-between gap-4" id="app-editor-runnable-header">
|
||||
<div class="font-bold items-baseline truncate">Choose a language</div>
|
||||
<div class="flex gap-2">
|
||||
{#if showScriptPicker}
|
||||
@@ -187,7 +191,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row w-full gap-8">
|
||||
<div class="">
|
||||
<div id="app-editor-backend-runnables">
|
||||
<div class="mb-1 text-sm font-semibold">Backend</div>
|
||||
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
@@ -198,11 +202,12 @@
|
||||
on:click={() => {
|
||||
createInlineScriptByLanguage(lang, name)
|
||||
}}
|
||||
id={`create-${lang}-script`}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="">
|
||||
<div id="app-editor-frontend-runnables">
|
||||
<div class="mb-1 text-sm font-semibold">
|
||||
Frontend
|
||||
<Tooltip
|
||||
|
||||
+14
-1
@@ -7,6 +7,10 @@
|
||||
import { BG_PREFIX, getAllScriptNames } from '../../utils'
|
||||
import PanelSection from '../settingsPanel/common/PanelSection.svelte'
|
||||
import { getAppScripts } from './utils'
|
||||
import AppTutorials from '$lib/components/AppTutorials.svelte'
|
||||
import { tutorialsToDo } from '$lib/stores'
|
||||
import { ignoredTutorials } from '$lib/components/tutorials/ignoredTutorials'
|
||||
import { tutorialInProgress } from '$lib/tutorialUtils'
|
||||
|
||||
const PREFIX = 'script-selector-' as const
|
||||
|
||||
@@ -35,6 +39,10 @@
|
||||
}
|
||||
|
||||
function createBackgroundScript() {
|
||||
if ($tutorialsToDo.includes(5) && !$ignoredTutorials?.includes(5) && !tutorialInProgress()) {
|
||||
appTutorials?.runTutorialById('backgroundrunnables', { skipStepsCount: 2 })
|
||||
}
|
||||
|
||||
for (const [index, script] of $app.hiddenInlineScripts.entries()) {
|
||||
if (script.hidden) {
|
||||
delete script.hidden
|
||||
@@ -68,9 +76,11 @@
|
||||
$app.hiddenInlineScripts = $app.hiddenInlineScripts
|
||||
selectScript(`${BG_PREFIX}${$app.hiddenInlineScripts.length - 1}`)
|
||||
}
|
||||
|
||||
let appTutorials: AppTutorials | undefined = undefined
|
||||
</script>
|
||||
|
||||
<PanelSection title="Runnables">
|
||||
<PanelSection title="Runnables" id="app-editor-runnable-panel">
|
||||
<div class="w-full flex flex-col gap-6 py-1">
|
||||
<div>
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
@@ -178,6 +188,7 @@
|
||||
title="Create a new background runnable"
|
||||
aria-label="Create a new background runnable"
|
||||
on:click={createBackgroundScript}
|
||||
id="create-background-runnable"
|
||||
>
|
||||
<Plus size={14} class="!text-primary" />
|
||||
</Button>
|
||||
@@ -222,6 +233,8 @@
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
<AppTutorials bind:this={appTutorials} on:reload />
|
||||
|
||||
<style lang="postcss">
|
||||
.panel-item {
|
||||
@apply border flex gap-1 truncate font-normal justify-between w-full items-center py-1 px-2 rounded-sm duration-200;
|
||||
|
||||
+9
-1
@@ -12,6 +12,7 @@
|
||||
export let componentInput: AppInput
|
||||
export let disableStatic: boolean = false
|
||||
export let evalV2editor: EvalV2InputEditor | undefined
|
||||
export let id: string
|
||||
|
||||
const { onchange, connectingInput, app } = getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
@@ -90,7 +91,13 @@
|
||||
label="Eval"
|
||||
/>
|
||||
|
||||
<ToggleButton value="runnable" icon={Code} iconOnly={clientWidth < 250} label="Compute" />
|
||||
<ToggleButton
|
||||
value="runnable"
|
||||
icon={Code}
|
||||
iconOnly={clientWidth < 250}
|
||||
label="Compute"
|
||||
id="data-source-compute"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
<div class="flex">
|
||||
<Button
|
||||
@@ -98,6 +105,7 @@
|
||||
variant="border"
|
||||
color="light"
|
||||
title="Connect"
|
||||
id={`plug`}
|
||||
on:click={() => {
|
||||
$connectingInput = {
|
||||
opened: true,
|
||||
|
||||
@@ -192,6 +192,7 @@
|
||||
: hasInteraction
|
||||
? 'Event handler'
|
||||
: 'Data source'}
|
||||
id={'component-input'}
|
||||
>
|
||||
<svelte:fragment slot="action">
|
||||
<span
|
||||
@@ -208,6 +209,7 @@
|
||||
<ComponentInputTypeEditor
|
||||
{evalV2editor}
|
||||
bind:componentInput={componentSettings.item.data.componentInput}
|
||||
id={component.id}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full gap-2 mt-2">
|
||||
@@ -244,8 +246,9 @@
|
||||
{#each componentSettings.item.data?.componentInput.connections as connection (connection.componentId + '-' + connection.id)}
|
||||
<span
|
||||
class="inline-flex items-center rounded-md px-2 py-0.5 text-xs font-medium border"
|
||||
>{connection.componentId + '.' + connection.id}</span
|
||||
>
|
||||
{connection.componentId + '.' + connection.id}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -261,9 +264,9 @@
|
||||
id={component.id}
|
||||
bind:componentInput={componentSettings.item.data.componentInput}
|
||||
/>
|
||||
<a class="text-2xs" on:click={transformToFrontend} href="#"
|
||||
>transform to a frontend script</a
|
||||
>
|
||||
<a class="text-2xs" on:click={transformToFrontend} href="#">
|
||||
transform to a frontend script
|
||||
</a>
|
||||
{:else if componentSettings.item.data.componentInput?.type === 'runnable' && component.componentInput !== undefined}
|
||||
<RunnableInputEditor
|
||||
appComponent={component}
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
onConnect: applyConnection
|
||||
}
|
||||
}}
|
||||
id="schema-plug"
|
||||
>
|
||||
<Plug size={14} />
|
||||
</Button>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let titlePadding: string = ''
|
||||
export let tooltip = ''
|
||||
export let documentationLink: string | undefined = undefined
|
||||
export let id: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -15,6 +16,7 @@
|
||||
'flex flex-col h-full gap-2 items-start',
|
||||
noPadding ? '' : 'p-3'
|
||||
)}
|
||||
{id}
|
||||
>
|
||||
<div class="flex justify-between flex-wrap items-center w-full gap-1">
|
||||
<div class="text-xs inline-flex items-center font-semibold text-primary {titlePadding} gap-1">
|
||||
|
||||
+1
@@ -196,6 +196,7 @@
|
||||
variant="border"
|
||||
startIcon={{ icon: faPlus }}
|
||||
btnClasses="truncate w-full"
|
||||
id="app-editor-create-inline-script"
|
||||
>
|
||||
Create an inline script
|
||||
</Button>
|
||||
|
||||
+4
-1
@@ -13,7 +13,10 @@
|
||||
$: checked = Boolean(appInput.transformer)
|
||||
</script>
|
||||
|
||||
<div class="text-sm font-semibold justify-between flex flex-row items-center">
|
||||
<div
|
||||
class="text-sm font-semibold justify-between flex flex-row items-center"
|
||||
id="app-editor-script-transformer"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
Transformer
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export let style = ''
|
||||
export let selectedClass = ''
|
||||
export let selectedStyle = ''
|
||||
export let id: string | undefined = undefined
|
||||
|
||||
export let disabled: boolean = false
|
||||
|
||||
@@ -45,6 +46,7 @@
|
||||
}}
|
||||
on:pointerdown|stopPropagation
|
||||
{disabled}
|
||||
{id}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
export let iconProps: Record<string, any> = {}
|
||||
export let showTooltipIcon: boolean = false
|
||||
export let documentationLink: string | undefined = undefined
|
||||
export let id: string | undefined = undefined
|
||||
|
||||
const { select, selected } = getContext<ToggleButtonContext>('ToggleButtonGroup')
|
||||
</script>
|
||||
@@ -29,31 +30,35 @@
|
||||
disappearTimeout={0}
|
||||
{documentationLink}
|
||||
>
|
||||
<Tab
|
||||
{disabled}
|
||||
class={twMerge(
|
||||
' rounded-md transition-all text-xs flex gap-1 flex-row items-center',
|
||||
small ? 'px-1 py-0.5' : 'px-2 py-1',
|
||||
$selected === value ? 'bg-surface shadow-md' : 'bg-surface-secondary hover:bg-surface-hover',
|
||||
$$props.class
|
||||
)}
|
||||
on:click={() => select(value)}
|
||||
>
|
||||
{#if icon}
|
||||
<svelte:component
|
||||
this={icon}
|
||||
size={14}
|
||||
color={$selected === value ? selectedColor : '#9CA3AF'}
|
||||
{...iconProps}
|
||||
/>
|
||||
{/if}
|
||||
{#if label && !iconOnly}
|
||||
{label}
|
||||
{/if}
|
||||
{#if showTooltipIcon}
|
||||
<Info size={14} class="text-gray-400" />
|
||||
{/if}
|
||||
</Tab>
|
||||
<div {id} class="flex">
|
||||
<Tab
|
||||
{disabled}
|
||||
class={twMerge(
|
||||
' rounded-md transition-all text-xs flex gap-1 flex-row items-center',
|
||||
small ? 'px-1 py-0.5' : 'px-2 py-1',
|
||||
$selected === value
|
||||
? 'bg-surface shadow-md'
|
||||
: 'bg-surface-secondary hover:bg-surface-hover',
|
||||
$$props.class
|
||||
)}
|
||||
on:click={() => select(value)}
|
||||
>
|
||||
{#if icon}
|
||||
<svelte:component
|
||||
this={icon}
|
||||
size={14}
|
||||
color={$selected === value ? selectedColor : '#9CA3AF'}
|
||||
{...iconProps}
|
||||
/>
|
||||
{/if}
|
||||
{#if label && !iconOnly}
|
||||
{label}
|
||||
{/if}
|
||||
{#if showTooltipIcon}
|
||||
<Info size={14} class="text-gray-400" />
|
||||
{/if}
|
||||
</Tab>
|
||||
</div>
|
||||
<svelte:fragment slot="text">
|
||||
{tooltip}
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
import FlowTutorials from '$lib/components/FlowTutorials.svelte'
|
||||
import { ignoredTutorials } from '$lib/components/tutorials/ignoredTutorials'
|
||||
import { tutorialInProgress } from '$lib/tutorialUtils'
|
||||
|
||||
export let modules: FlowModule[] | undefined
|
||||
export let sidebarSize: number | undefined = undefined
|
||||
@@ -142,13 +143,11 @@
|
||||
getContext<FlowCopilotContext | undefined>('FlowCopilotContext') || {}
|
||||
|
||||
function shouldRunTutorial(tutorialName: string, name: string, index: number) {
|
||||
const svg = document.getElementsByClassName('driver-overlay driver-overlay-animated')
|
||||
|
||||
return (
|
||||
$tutorialsToDo.includes(index) &&
|
||||
name == tutorialName &&
|
||||
svg.length === 0 &&
|
||||
!$ignoredTutorials.includes(index)
|
||||
!$ignoredTutorials.includes(index) &&
|
||||
!tutorialInProgress()
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
| 'powershell'
|
||||
| undefined = undefined
|
||||
export let icon: IconDefinition | undefined = undefined
|
||||
|
||||
export let iconColor: string | undefined = undefined
|
||||
export let id: string | undefined = undefined
|
||||
|
||||
const enterpriseLangs = ['bigquery', 'snowflake']
|
||||
</script>
|
||||
@@ -36,6 +38,7 @@
|
||||
classes: iconColor
|
||||
}}
|
||||
disabled={disabled || (enterpriseLangs.includes(lang || '') && !$enterpriseLicense)}
|
||||
{id}
|
||||
>
|
||||
<div class="flex justify-center flex-col items-center gap-2">
|
||||
{#if lang}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
let tutorial: Tutorial | undefined = undefined
|
||||
|
||||
export function runTutorial(indexToInsertAt?: number | undefined) {
|
||||
tutorial?.runTutorial(indexToInsertAt)
|
||||
tutorial?.runTutorial({ indexToInsertAt })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
name="branchall"
|
||||
on:error
|
||||
on:skipAll
|
||||
getSteps={(driver, indexToInsertAt) => {
|
||||
getSteps={(driver, options) => {
|
||||
const id = nextId($flowStateStore, $flowStore)
|
||||
const index = indexToInsertAt ?? $flowStore.value.modules.length
|
||||
const index = options?.indexToInsertAt ?? $flowStore.value.modules.length
|
||||
const isFirst = id === 'a'
|
||||
|
||||
const steps = [
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
let tutorial: Tutorial | undefined = undefined
|
||||
|
||||
export function runTutorial(indexToInsertAt?: number | undefined) {
|
||||
tutorial?.runTutorial(indexToInsertAt)
|
||||
tutorial?.runTutorial({ indexToInsertAt })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
name="branchone"
|
||||
on:error
|
||||
on:skipAll
|
||||
getSteps={(driver, indexToInsertAt) => {
|
||||
getSteps={(driver, options) => {
|
||||
const id = nextId($flowStateStore, $flowStore)
|
||||
const index = indexToInsertAt ?? $flowStore.value.modules.length
|
||||
const index = options?.indexToInsertAt ?? $flowStore.value.modules.length
|
||||
const isFirst = id === 'a'
|
||||
|
||||
const steps = [
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
let tutorial: Tutorial | undefined = undefined
|
||||
|
||||
export function runTutorial(indexToInsertAt?: number | undefined) {
|
||||
tutorial?.runTutorial(indexToInsertAt)
|
||||
tutorial?.runTutorial({ indexToInsertAt })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
tainted={false}
|
||||
on:error
|
||||
on:skipAll
|
||||
getSteps={(driver, indexToInsertAt) => {
|
||||
getSteps={(driver, options) => {
|
||||
const id = nextId($flowStateStore, $flowStore)
|
||||
const index = indexToInsertAt ?? $flowStore.value.modules.length
|
||||
const index = options?.indexToInsertAt ?? $flowStore.value.modules.length
|
||||
const isFirst = id === 'a'
|
||||
|
||||
let tempId = ''
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
export let name: string = 'action'
|
||||
export let tainted: boolean = false
|
||||
|
||||
export let getSteps: (
|
||||
driver: Driver,
|
||||
indexToInsertAt?: number | undefined
|
||||
) => DriveStep[] = () => []
|
||||
type Options = {
|
||||
indexToInsertAt?: number
|
||||
skipStepsCount?: number
|
||||
}
|
||||
|
||||
export let getSteps: (driver: Driver, options?: Options | undefined) => DriveStep[] = () => []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -27,7 +29,7 @@
|
||||
return button
|
||||
}
|
||||
|
||||
export const runTutorial = (indexToInsertAt?: number | undefined) => {
|
||||
export const runTutorial = (options?: Options | undefined) => {
|
||||
if (tainted) {
|
||||
dispatch('error', { detail: name })
|
||||
return
|
||||
@@ -70,7 +72,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
tutorial.setSteps(getSteps(tutorial, indexToInsertAt))
|
||||
tutorial.setSteps(getSteps(tutorial, options))
|
||||
tutorial.drive()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,12 +7,14 @@
|
||||
|
||||
export let label: string
|
||||
export let index: number
|
||||
export let disabled: boolean = false
|
||||
</script>
|
||||
|
||||
<MenuItem on:click>
|
||||
<MenuItem on:click {disabled}>
|
||||
<div
|
||||
class={classNames(
|
||||
'text-primary flex flex-row items-center text-left px-4 py-2 gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold'
|
||||
'flex flex-row items-center text-left px-4 py-2 gap-2 cursor-pointer hover:bg-surface-hover !text-xs font-semibold',
|
||||
disabled ? 'text-disabled' : 'text-primary'
|
||||
)}
|
||||
>
|
||||
{#if $tutorialsToDo.includes(index)}
|
||||
@@ -20,6 +22,9 @@
|
||||
{:else}
|
||||
<CheckCircle size={16} color="green" />
|
||||
{/if}
|
||||
{label}
|
||||
<div class="flex flex-row justify-between items-center w-full">
|
||||
{label}
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</MenuItem>
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
<script lang="ts">
|
||||
import { insertNewGridItem, appComponentFromType } from '$lib/components/apps/editor/appUtils'
|
||||
import type { AppComponent, TypedComponent } from '$lib/components/apps/editor/component'
|
||||
import type { AppViewerContext, AppEditorContext } from '$lib/components/apps/types'
|
||||
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
|
||||
import { push } from '$lib/history'
|
||||
import { getContext } from 'svelte'
|
||||
import Tutorial from '../Tutorial.svelte'
|
||||
import {
|
||||
clickButtonBySelector,
|
||||
clickFirstButtonBySelector,
|
||||
connectInlineRunnableInputToComponentOutput,
|
||||
isAppTainted,
|
||||
updateInlineRunnableCode
|
||||
} from '../utils'
|
||||
import { updateProgress } from '$lib/tutorialUtils'
|
||||
|
||||
export let name: string
|
||||
export let index: number
|
||||
|
||||
let tutorial: Tutorial | undefined = undefined
|
||||
|
||||
const { app, selectedComponent, focusedGrid, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
const { history } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
export function runTutorial() {
|
||||
tutorial?.runTutorial()
|
||||
}
|
||||
|
||||
function addComponent(appComponentType: TypedComponent['type']): void {
|
||||
push(history, $app)
|
||||
|
||||
$dirtyStore = true
|
||||
|
||||
const id = insertNewGridItem(
|
||||
$app,
|
||||
appComponentFromType(appComponentType) as (id: string) => AppComponent,
|
||||
$focusedGrid
|
||||
)
|
||||
|
||||
$selectedComponent = [id]
|
||||
$app = $app
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tutorial
|
||||
bind:this={tutorial}
|
||||
{index}
|
||||
{name}
|
||||
on:error
|
||||
on:skipAll
|
||||
tainted={isAppTainted($app)}
|
||||
getSteps={(driver) => {
|
||||
const steps = [
|
||||
{
|
||||
popover: {
|
||||
title: 'Welcome to the App editor tutorial !',
|
||||
description:
|
||||
'This tutorial will show you how to use the App editor, add components, background scripts and connect them.',
|
||||
onNextClick: () => {
|
||||
addComponent('textinputcomponent')
|
||||
|
||||
setTimeout(() => {
|
||||
clickButtonBySelector('#app-editor-component-library-tab')
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#app-editor-component-list',
|
||||
popover: {
|
||||
title: 'Components panel',
|
||||
description:
|
||||
'This is the components panel. Here you can add components to your app. Components are the building blocks of your app. You can add as many components as you want.'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#displaycomponent',
|
||||
popover: {
|
||||
title: 'Adding a component',
|
||||
description:
|
||||
'Click on a component to add it to your app. Here we will add a display component.',
|
||||
onNextClick: () => {
|
||||
addComponent('displaycomponent')
|
||||
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '.wm-app-viewer',
|
||||
popover: {
|
||||
title: 'App canvas',
|
||||
description:
|
||||
'In the canvas, you can move components around, resize them, and organize them in grids. In this example, we already added a text input and a display component.',
|
||||
onNextClick: () => {
|
||||
driver.moveNext()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#component-input',
|
||||
popover: {
|
||||
title: 'Component input',
|
||||
description:
|
||||
'They are several ways to set the input of a component. It can be static, the resul of an JS expression, connected to the output of another component, or the result of a inline runnable. Here we will create an inline runnable that will convert the text to uppercase.',
|
||||
onNextClick: () => {
|
||||
clickFirstButtonBySelector('#component-input')
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#data-source-compute',
|
||||
popover: {
|
||||
title: 'Compute',
|
||||
description: 'Click on the compute button to create a new inline runnable.',
|
||||
onNextClick: () => {
|
||||
clickFirstButtonBySelector('#data-source-compute')
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#app-editor-create-inline-script',
|
||||
popover: {
|
||||
title: 'Create an inline script',
|
||||
description: "Let's create an inline script.",
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#app-editor-create-inline-script')
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#app-editor-empty-runnable',
|
||||
popover: {
|
||||
title: 'Choose a language',
|
||||
description:
|
||||
'You can choose the language of your runnable. They are two type of runnables: frontend and backend.'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#app-editor-backend-runnables',
|
||||
popover: {
|
||||
title: 'Backend runnables',
|
||||
description:
|
||||
'Backend runnables are scripts that are executed on the server. They can be used to perform tasks that are not possible to be performed on the client. For example, you can use backend runnables to send emails, perform database operations, etc.'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#app-editor-frontend-runnables',
|
||||
popover: {
|
||||
title: 'Frontend runnables',
|
||||
description:
|
||||
'Frontend scripts are executed in the browser and can manipulate the app context directly. You can also interact with components using component controls.'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#create-deno-script',
|
||||
popover: {
|
||||
title: 'Create a deno script',
|
||||
description:
|
||||
"Let's create a simple deno script. For the sake of this tutorial, we will create a script that converts the text to uppercase.",
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#create-deno-script')
|
||||
setTimeout(() => {
|
||||
if ($selectedComponent?.[0]) {
|
||||
updateInlineRunnableCode(
|
||||
$app,
|
||||
$selectedComponent[0],
|
||||
`export async function main(x: string) {
|
||||
return x?.toLocaleUpperCase();
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#schema-plug',
|
||||
popover: {
|
||||
title: 'Connect the function input',
|
||||
description:
|
||||
"The function we created has an string input 'x'. We can connect the output of the text component to it.",
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#schema-plug')
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#connect-output-a',
|
||||
popover: {
|
||||
title: 'Select the output',
|
||||
description: ' ',
|
||||
onNextClick: () => {
|
||||
$connectingInput.opened = false
|
||||
$connectingInput.input = undefined
|
||||
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
},
|
||||
onPopoverRender: (popover, opts) => {
|
||||
const wrapper = document.createElement('div')
|
||||
wrapper.classList.add('flex', 'flex-col', 'gap-2', 'w-full', 'items-start')
|
||||
|
||||
const p1 = document.createElement('p')
|
||||
p1.innerText =
|
||||
'You can now select the output in the output menu. Click on the little red button to open the menu.'
|
||||
|
||||
const id = document.createElement('div')
|
||||
id.innerHTML = `<button class="bg-red-500/70 border border-red-600 px-1 py-0.5" title="Outputs" aria-label="Open output"><svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide-icon lucide lucide-plug-2 "><path d="M9 2v6"></path><path d="M15 2v6"></path><path d="M12 17v5"></path><path d="M5 8h14"></path><path d="M6 11V8h12v3a6 6 0 1 1-12 0v0Z"></path></svg></button>`
|
||||
|
||||
const p2 = document.createElement('p')
|
||||
p2.innerText =
|
||||
'Once opened, you can select the output you want to connect to. Here we will connect the result output of the text component to the input "x" of the inline runnable.'
|
||||
|
||||
const objectViewer = document.createElement('div')
|
||||
objectViewer.innerHTML = `<div class="rounded-lg shadow-md border p-4 bg-surface"><span class="s-UNyBDXJ1E286"> <ul class="w-full pl-2 border-none s-UNyBDXJ1E286"><li class="s-UNyBDXJ1E286"><button class="whitespace-nowrap s-UNyBDXJ1E286"><span class="key border font-semibold rounded px-1 hover:bg-surface-hover text-2xs text-secondary s-UNyBDXJ1E286">result</span> :</button> <button class="val rounded px-1 hover:bg-blue-100 string s-UNyBDXJ1E286"><span title="" class="text-2xs s-UNyBDXJ1E286">""</span></button></li> </ul> </span> <span class="border border-blue-600 rounded px-1 cursor-pointer hover:bg-gray-200 s-UNyBDXJ1E286 hidden">{...}</span> </div>`
|
||||
|
||||
wrapper.appendChild(p1)
|
||||
wrapper.appendChild(id)
|
||||
wrapper.appendChild(p2)
|
||||
wrapper.appendChild(objectViewer)
|
||||
|
||||
popover.description.appendChild(wrapper)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '.wm-app-viewer',
|
||||
popover: {
|
||||
title: "Let's test out the app !",
|
||||
description:
|
||||
'We can now type in the text input and see the result in the display component',
|
||||
onNextClick: () => {
|
||||
connectInlineRunnableInputToComponentOutput($app, 'b', 'x', 'a', 'result')
|
||||
|
||||
$app = $app
|
||||
|
||||
updateProgress(7)
|
||||
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return steps
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script lang="ts">
|
||||
import { updateProgress } from '$lib/tutorialUtils'
|
||||
import Tutorial from '../Tutorial.svelte'
|
||||
import { clickButtonBySelector } from '../utils'
|
||||
|
||||
export let name: string
|
||||
export let index: number
|
||||
|
||||
let tutorial: Tutorial | undefined = undefined
|
||||
|
||||
export function runTutorial(skipStepsCount: number | undefined = undefined) {
|
||||
tutorial?.runTutorial({ skipStepsCount })
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tutorial
|
||||
bind:this={tutorial}
|
||||
{index}
|
||||
{name}
|
||||
on:error
|
||||
on:skipAll
|
||||
getSteps={(driver, options) => {
|
||||
const steps = [
|
||||
{
|
||||
element: '#app-editor-runnable-panel',
|
||||
popover: {
|
||||
title: 'Runnable panel',
|
||||
description:
|
||||
'This is the runnable panel. Here you can add runnables to your app. Runnables are scripts that can be executed in the background. You can add as many runnables as you want.'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#create-background-runnable',
|
||||
popover: {
|
||||
title: 'Create a runnable',
|
||||
description:
|
||||
'Click here to create a runnable. Runnables are scripts that can be executed in the background. You can add as many runnables as you want.',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#create-background-runnable')
|
||||
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#app-editor-empty-runnable',
|
||||
popover: {
|
||||
title: 'Empty runnable panel',
|
||||
description:
|
||||
'This is the empty runnable panel. Here you can add runnables to your app. Runnables are scripts that can be executed in the background. You can add as many runnables as you want. You can also select a script or a flow from your workspace or the Hub.'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
element: '#app-editor-backend-runnables',
|
||||
popover: {
|
||||
title: 'Backend runnables',
|
||||
description:
|
||||
'Backend runnables are scripts that are executed on the server. They can be used to perform tasks that are not possible to be performed on the client. For example, you can use backend runnables to send emails, perform database operations, etc.'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#app-editor-frontend-runnables',
|
||||
popover: {
|
||||
title: 'Frontend runnables',
|
||||
description:
|
||||
'Frontend scripts are executed in the browser and can manipulate the app context directly. You can also interact with components using component controls.',
|
||||
onNextClick: () => {
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
|
||||
updateProgress(5)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// Remove steps if we want to skip them (excpet the first one)
|
||||
|
||||
if (options?.skipStepsCount) {
|
||||
steps.splice(1, options.skipStepsCount)
|
||||
}
|
||||
|
||||
return steps
|
||||
}}
|
||||
/>
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { insertNewGridItem, appComponentFromType } from '$lib/components/apps/editor/appUtils'
|
||||
import type { AppComponent } from '$lib/components/apps/editor/component'
|
||||
import type { AppViewerContext, AppEditorContext } from '$lib/components/apps/types'
|
||||
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
|
||||
import { push } from '$lib/history'
|
||||
import { getContext } from 'svelte'
|
||||
import Tutorial from '../Tutorial.svelte'
|
||||
import { clickButtonBySelector } from '../utils'
|
||||
import { updateProgress } from '$lib/tutorialUtils'
|
||||
|
||||
export let name: string
|
||||
export let index: number
|
||||
|
||||
let tutorial: Tutorial | undefined = undefined
|
||||
|
||||
const { app, selectedComponent, focusedGrid } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { history } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
export function runTutorial() {
|
||||
tutorial?.runTutorial()
|
||||
}
|
||||
|
||||
function addComponent(): void {
|
||||
push(history, $app)
|
||||
|
||||
$dirtyStore = true
|
||||
|
||||
const id = insertNewGridItem(
|
||||
$app,
|
||||
appComponentFromType('textcomponent') as (id: string) => AppComponent,
|
||||
$focusedGrid
|
||||
)
|
||||
|
||||
$selectedComponent = [id]
|
||||
$app = $app
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tutorial
|
||||
bind:this={tutorial}
|
||||
{index}
|
||||
{name}
|
||||
on:error
|
||||
on:skipAll
|
||||
getSteps={(driver) => [
|
||||
{
|
||||
popover: {
|
||||
title: 'Connection tutorial',
|
||||
description: 'We will connect the input of a text component to an output.',
|
||||
onNextClick: () => {
|
||||
addComponent()
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: `#component-input`,
|
||||
popover: {
|
||||
title: 'Data source',
|
||||
description:
|
||||
'Here we can set the data source of the text component: it can be static, the result of an evaluation or the result of script or flow. We are going to connect the data source to an output.',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#component-input')
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#plug',
|
||||
popover: {
|
||||
title: 'Connect the text component',
|
||||
description: 'Click on the plug icon to connect the text component',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#plug')
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '#output-ctx',
|
||||
popover: {
|
||||
title: 'Select the output',
|
||||
description:
|
||||
"You can now select the output in the output menu. Let's select your email in the app context",
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('#output-ctx')
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.val',
|
||||
popover: {
|
||||
title: 'Click on the output',
|
||||
description: 'Simply click on the output to connect it',
|
||||
onNextClick: () => {
|
||||
clickButtonBySelector('.val')
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
popover: {
|
||||
title: 'Connection done',
|
||||
description: 'You can now see the email output connected to the text component input',
|
||||
onNextClick: () => {
|
||||
updateProgress(6)
|
||||
|
||||
setTimeout(() => {
|
||||
driver.moveNext()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import Tutorial from '../Tutorial.svelte'
|
||||
|
||||
export let name: string
|
||||
export let index: number
|
||||
|
||||
let tutorial: Tutorial | undefined = undefined
|
||||
|
||||
export function runTutorial() {
|
||||
tutorial?.runTutorial()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tutorial
|
||||
bind:this={tutorial}
|
||||
{index}
|
||||
{name}
|
||||
on:error
|
||||
on:skipAll
|
||||
getSteps={(driver) => [
|
||||
{
|
||||
popover: {
|
||||
title: 'Welcome to the Windmil Flow editor',
|
||||
description:
|
||||
'Learn how to build our first branch to be executed on a condition. You can use arrow keys to navigate'
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { Flow, FlowModule } from '$lib/gen'
|
||||
import { findGridItem } from '../apps/editor/appUtils'
|
||||
import type { App } from '../apps/types'
|
||||
|
||||
export function setInputBySelector(selector: string, value: string) {
|
||||
const input = document.querySelector(selector) as HTMLInputElement
|
||||
@@ -17,6 +19,15 @@ export function clickButtonBySelector(selector: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function clickFirstButtonBySelector(selector: string) {
|
||||
const buttons = document.querySelector(selector)
|
||||
const button = buttons?.childNodes[0] as HTMLButtonElement
|
||||
|
||||
if (button) {
|
||||
button.click()
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerAddFlowStep(index: number) {
|
||||
const button = document.querySelector(`#flow-editor-add-step-${index}`) as HTMLButtonElement
|
||||
|
||||
@@ -48,6 +59,10 @@ export function isFlowTainted(flow: Flow) {
|
||||
return flow.value.modules.length > 0 || Object.keys(flow?.schema?.properties).length > 0
|
||||
}
|
||||
|
||||
export function isAppTainted(app: App) {
|
||||
return !(app.grid.length === 0 && app.hiddenInlineScripts.length === 0)
|
||||
}
|
||||
|
||||
export function updateFlowModuleById(
|
||||
flow: Flow,
|
||||
id: string,
|
||||
@@ -74,3 +89,76 @@ export function updateFlowModuleById(
|
||||
|
||||
flow = flow
|
||||
}
|
||||
|
||||
export function updateBackgroundRunnableCode(app: App, index: number, newCode: string) {
|
||||
const script = app.hiddenInlineScripts[index]
|
||||
if (script.type === 'runnableByName' && script.inlineScript) {
|
||||
script.inlineScript.content = newCode
|
||||
}
|
||||
|
||||
app = app
|
||||
}
|
||||
|
||||
export function updateInlineRunnableCode(app: App, componentId: string, newCode: string) {
|
||||
const gridItem = findGridItem(app, componentId)
|
||||
|
||||
if (gridItem?.data.componentInput?.type === 'runnable') {
|
||||
if (
|
||||
gridItem.data.componentInput.runnable?.type === 'runnableByName' &&
|
||||
gridItem.data.componentInput.runnable.inlineScript
|
||||
) {
|
||||
gridItem.data.componentInput.runnable.inlineScript.content = newCode
|
||||
}
|
||||
}
|
||||
|
||||
app = app
|
||||
}
|
||||
|
||||
export function connectComponentSourceToOutput(app: App, componentId: string, targetId: string) {
|
||||
const gridItem = findGridItem(app, componentId)
|
||||
|
||||
if (gridItem) {
|
||||
gridItem.data.componentInput = {
|
||||
type: 'evalv2',
|
||||
fieldType: 'object',
|
||||
|
||||
expr: `${targetId}.result`,
|
||||
connections: [
|
||||
{
|
||||
componentId: targetId,
|
||||
id: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
app = app
|
||||
}
|
||||
|
||||
export function connectInlineRunnableInputToComponentOutput(
|
||||
app: App,
|
||||
sourceComponentId: string,
|
||||
sourceField: string,
|
||||
targetComponentId: string,
|
||||
targetField: string,
|
||||
fieldType: string = 'text'
|
||||
) {
|
||||
const gridItem = findGridItem(app, sourceComponentId)
|
||||
|
||||
if (gridItem?.data.componentInput?.type === 'runnable') {
|
||||
// @ts-ignore
|
||||
gridItem.data.componentInput.fields = {
|
||||
[sourceField]: {
|
||||
type: 'evalv2',
|
||||
expr: `${targetComponentId}.${targetField}`,
|
||||
fieldType: fieldType,
|
||||
connections: [
|
||||
{
|
||||
componentId: targetComponentId,
|
||||
id: targetField
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { get } from 'svelte/store'
|
||||
import { tutorialsToDo } from './stores'
|
||||
import { UserService } from './gen'
|
||||
|
||||
const MAX_TUTORIAL_ID = 6
|
||||
const MAX_TUTORIAL_ID = 7
|
||||
|
||||
export async function updateProgress(id: number) {
|
||||
const bef = get(tutorialsToDo)
|
||||
@@ -49,3 +49,9 @@ export async function syncTutorialsTodos() {
|
||||
}
|
||||
tutorialsToDo.set(todos)
|
||||
}
|
||||
|
||||
export function tutorialInProgress() {
|
||||
const svg = document.getElementsByClassName('driver-overlay driver-overlay-animated')
|
||||
|
||||
return svg.length > 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user