From 65cdcff28c6680bc342cb3cb7cc372f5b3c99d2a Mon Sep 17 00:00:00 2001 From: Diego Imbert <70353967+diegoimbert@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:09:30 +0200 Subject: [PATCH] Fix app tutorials (#6728) * Fix tutorial basic * fix other tutorials * nit fix bug with button shrinking * tutorial works backwards * nit delete field on prev * remove empty app duplication and magic code * fix norefreshbar auto binding to false, making app dirty * fix and improve app tutorial * fix background runnable tutorial scroll * fix connection tutorial * mistake * isCurrentlyInTutorial global state * disable component navigation when in tutorial * ci --- .../components/apps/editor/GridEditor.svelte | 8 +- .../lib/components/apps/editor/appUtils.ts | 187 +++++++++++------- .../component/ComponentNavigation.svelte | 4 +- .../contextPanel/ComponentOutputViewer.svelte | 2 +- .../EmptyInlineScript.svelte | 10 +- .../settingsPanel/InputsSpecEditor.svelte | 1 + .../common/button/ConnectionButton.svelte | 3 +- .../lib/components/tutorials/Tutorial.svelte | 5 + .../tutorials/app/AppTutorial.svelte | 99 ++++------ .../app/BackgroundRunnablesTutorial.svelte | 8 +- .../tutorials/app/ConnectionTutorial.svelte | 4 +- .../src/lib/components/tutorials/utils.ts | 162 +-------------- frontend/src/lib/stores.ts | 5 +- .../(root)/(logged)/apps/add/+page.svelte | 42 +--- 14 files changed, 195 insertions(+), 345 deletions(-) diff --git a/frontend/src/lib/components/apps/editor/GridEditor.svelte b/frontend/src/lib/components/apps/editor/GridEditor.svelte index 403660a189..015990c2e4 100644 --- a/frontend/src/lib/components/apps/editor/GridEditor.svelte +++ b/frontend/src/lib/components/apps/editor/GridEditor.svelte @@ -166,7 +166,13 @@
Hide bar on view - + $app.norefreshbar ?? false, + (v) => ($app.norefreshbar !== undefined || v) && ($app.norefreshbar = v) + } + />
{policy.on_behalf_of ? `Author ${policy.on_behalf_of_email}` : ''} diff --git a/frontend/src/lib/components/apps/editor/appUtils.ts b/frontend/src/lib/components/apps/editor/appUtils.ts index d4e2a2e995..5276ff301f 100644 --- a/frontend/src/lib/components/apps/editor/appUtils.ts +++ b/frontend/src/lib/components/apps/editor/appUtils.ts @@ -13,6 +13,8 @@ import { ccomponents, components, getRecommendedDimensionsByComponent, + presets, + processDimension, type AppComponent, type BaseComponent, type InitialAppComponent, @@ -36,17 +38,18 @@ import { sendUserToast } from '$lib/toast' import { getNextId } from '$lib/components/flows/idUtils' import { enterpriseLicense } from '$lib/stores' import gridHelp from '../svelte-grid/utils/helper' +import { DEFAULT_THEME } from './componentsPanel/themeUtils' type GridItemLocation = | { - type: 'grid' - gridItemIndex: number - } + type: 'grid' + gridItemIndex: number + } | { - type: 'subgrid' - subgridItemIndex: number - subgridKey: string - } + type: 'subgrid' + subgridItemIndex: number + subgridKey: string + } interface GridItemWithLocation { location: GridItemLocation item: GridItem @@ -187,7 +190,7 @@ export function selectId( selectedComponent: Writable, app: App ) { - ; (document?.activeElement as HTMLElement)?.blur() + ;(document?.activeElement as HTMLElement)?.blur() if (e.shiftKey) { selectedComponent.update((old) => { if (old && old?.[0]) { @@ -492,11 +495,11 @@ export function appComponentFromType( xData: type === 'plotlycomponentv2' || type === 'chartjscomponentv2' ? { - type: 'evalv2', - fieldType: 'array', - expr: '[1, 2, 3, 4]', - connections: [] - } + type: 'evalv2', + fieldType: 'array', + expr: '[1, 2, 3, 4]', + connections: [] + } : undefined, ...(extra ?? {}) } @@ -845,33 +848,33 @@ export type InitConfig< | EvalAppInput | EvalV2AppInput | { - type: 'oneOf' - selected: string - configuration: Record< - string, - Record - > - } + type: 'oneOf' + selected: string + configuration: Record< + string, + Record + > + } > > = { - [Property in keyof T]: T[Property] extends StaticAppInput + [Property in keyof T]: T[Property] extends StaticAppInput ? T[Property]['value'] | undefined : T[Property] extends { type: 'oneOf' } - ? { - type: 'oneOf' - selected: keyof T[Property]['configuration'] - configuration: { - [Choice in keyof T[Property]['configuration']]: { - [IT in keyof T[Property]['configuration'][Choice]]: T[Property]['configuration'][Choice][IT] extends StaticAppInput - ? T[Property]['configuration'][Choice][IT] extends StaticAppInputOnDemand - ? () => Promise - : T[Property]['configuration'][Choice][IT]['value'] | undefined - : undefined + ? { + type: 'oneOf' + selected: keyof T[Property]['configuration'] + configuration: { + [Choice in keyof T[Property]['configuration']]: { + [IT in keyof T[Property]['configuration'][Choice]]: T[Property]['configuration'][Choice][IT] extends StaticAppInput + ? T[Property]['configuration'][Choice][IT] extends StaticAppInputOnDemand + ? () => Promise + : T[Property]['configuration'][Choice][IT]['value'] | undefined + : undefined + } + } } - } - } - : undefined - } + : undefined +} export function initConfig< T extends Record< @@ -880,13 +883,13 @@ export function initConfig< | EvalAppInput | EvalV2AppInput | { - type: 'oneOf' - selected: string - configuration: Record< - string, - Record - > - } + type: 'oneOf' + selected: string + configuration: Record< + string, + Record + > + } > >( r: T, @@ -894,13 +897,13 @@ export function initConfig< string, | StaticAppInput | { - type: 'oneOf' - selected: string - configuration: Record< - string, - Record - > - } + type: 'oneOf' + selected: string + configuration: Record< + string, + Record + > + } | any > ): InitConfig { @@ -910,31 +913,31 @@ export function initConfig< Object.entries(r).map(([key, value]) => value.type == 'static' ? [ - key, - configuration?.[key]?.type == 'static' ? configuration?.[key]?.['value'] : undefined - ] + key, + configuration?.[key]?.type == 'static' ? configuration?.[key]?.['value'] : undefined + ] : value.type == 'oneOf' ? [ - key, - { - selected: value.selected, - type: 'oneOf', - configuration: Object.fromEntries( - Object.entries(value.configuration).map(([choice, config]) => { - const conf = initConfig( - config, - configuration?.[key]?.configuration?.[choice] - ) - Object.entries(config).forEach(([innerKey, innerValue]) => { - if (innerValue.type === 'static' && !(innerKey in conf)) { - conf[innerKey] = innerValue.value - } + key, + { + selected: value.selected, + type: 'oneOf', + configuration: Object.fromEntries( + Object.entries(value.configuration).map(([choice, config]) => { + const conf = initConfig( + config, + configuration?.[key]?.configuration?.[choice] + ) + Object.entries(config).forEach(([innerKey, innerValue]) => { + if (innerValue.type === 'static' && !(innerKey in conf)) { + conf[innerKey] = innerValue.value + } + }) + return [choice, conf] }) - return [choice, conf] - }) - ) - } - ] + ) + } + ] : [key, undefined] ) ) as any @@ -1395,3 +1398,45 @@ export function animateTo(start: number, end: number, onUpdate: (newValue: numbe function easeInOut(t: number) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t } + +export function emptyApp(): App { + let value: App = { + grid: [], + fullscreen: false, + unusedInlineScripts: [], + hiddenInlineScripts: [], + theme: { + type: 'path', + path: DEFAULT_THEME + } + } + const preset = presets['topbarcomponent'] + + const id = insertNewGridItem( + value, + appComponentFromType(preset.targetComponent, preset.configuration, undefined, { + customCss: { + container: { + class: '!p-0' as any, + style: '' + } + } + }) as (id: string) => AppComponent, + undefined, + undefined, + 'topbar', + { x: 0, y: 0 }, + { + 3: processDimension(preset.dims, 3), + 12: processDimension(preset.dims, 12) + }, + true, + true + ) + + setUpTopBarComponentContent(id, value) + + value.hideLegacyTopBar = true + value.mobileViewOnSmallerScreens = false + return value +} diff --git a/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte b/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte index aa81b7f5cc..2ac045feaa 100644 --- a/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte +++ b/frontend/src/lib/components/apps/editor/component/ComponentNavigation.svelte @@ -11,6 +11,7 @@ left } from './componentCallbacks.svelte' import type { AppEditorContext, AppViewerContext } from '../../types' + import { isCurrentlyInTutorial } from '$lib/stores' const { history, movingcomponents, jobsDrawerOpen, runnableJobEditorPanel } = getContext('AppEditorContext') as AppEditorContext @@ -33,7 +34,8 @@ if ( (typeof classes === 'string' && classes.includes('inputarea')) || ['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName!) || - $runnableJobEditorPanel.focused + $runnableJobEditorPanel.focused || + isCurrentlyInTutorial.val ) { return } diff --git a/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte b/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte index ffc9e7f750..9baad1f998 100644 --- a/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte +++ b/frontend/src/lib/components/apps/editor/contextPanel/ComponentOutputViewer.svelte @@ -52,7 +52,7 @@ {#if render && object != undefined && Object.keys(object).length > 0} {#if $hasResult[componentId] || $search == ''} -
+
diff --git a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte index 127abc8311..7d73f0a0d2 100644 --- a/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte +++ b/frontend/src/lib/components/apps/editor/settingsPanel/InputsSpecEditor.svelte @@ -193,6 +193,7 @@ {openConnection} isOpen={!!$connectingInput.opened} btnWrapperClasses={'h-6 w-8 opacity-0 group-hover:opacity-100 transition-opacity'} + id="schema-plug-{key}" /> void export let closeConnection: () => void export let btnWrapperClasses = '' + export let id: string | undefined = undefined let selected = false @@ -83,7 +84,7 @@ color="light" title="Connect" on:click={() => handleConnect(true)} - id="schema-plug" + {id} wrapperClasses={twMerge(btnWrapperClasses, selected ? 'opacity-100' : '')} btnClasses="p-0" > diff --git a/frontend/src/lib/components/tutorials/Tutorial.svelte b/frontend/src/lib/components/tutorials/Tutorial.svelte index 94c92420b1..1ef9e2b53c 100644 --- a/frontend/src/lib/components/tutorials/Tutorial.svelte +++ b/frontend/src/lib/components/tutorials/Tutorial.svelte @@ -6,10 +6,12 @@ import SkipTutorials from './SkipTutorials.svelte' import TutorialControls from './TutorialControls.svelte' import TutorialInner from './TutorialInner.svelte' + import { isCurrentlyInTutorial } from '$lib/stores' export let index: number = 0 export let name: string = 'action' export let tainted: boolean = false + export let onDestroyed: (() => void) | undefined = undefined type Options = { indexToInsertAt?: number @@ -111,6 +113,7 @@ dispatch('error', { detail: name }) return } + isCurrentlyInTutorial.val = true tutorial = driver({ allowClose: true, @@ -122,9 +125,11 @@ renderControls({ config, state }) }, onDestroyed: () => { + onDestroyed?.() if (!tutorial?.hasNextStep()) { $ignoredTutorials = Array.from(new Set([...$ignoredTutorials, index])) } + isCurrentlyInTutorial.val = false } }) diff --git a/frontend/src/lib/components/tutorials/app/AppTutorial.svelte b/frontend/src/lib/components/tutorials/app/AppTutorial.svelte index 97039e4af4..1b52905525 100644 --- a/frontend/src/lib/components/tutorials/app/AppTutorial.svelte +++ b/frontend/src/lib/components/tutorials/app/AppTutorial.svelte @@ -13,14 +13,15 @@ updateInlineRunnableCode } from '../utils' import { updateProgress } from '$lib/tutorialUtils' + import { type DriveStep } from 'driver.js' + import { wait } from '$lib/utils' export let name: string export let index: number let tutorial: Tutorial | undefined = undefined - const { app, selectedComponent, focusedGrid, connectingInput } = - getContext('AppViewerContext') + const { app, selectedComponent, focusedGrid } = getContext('AppViewerContext') const { history } = getContext('AppEditorContext') export function runTutorial() { @@ -49,7 +50,7 @@ on:skipAll tainted={isAppTainted($app)} getSteps={(driver) => { - const steps = [ + const steps: DriveStep[] = [ { popover: { title: 'App editor tutorial', @@ -112,7 +113,7 @@ popover: { title: 'Component input', description: - 'There are several ways to set the input of a component. It can be static, the result of a 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.', + 'There are several ways to set the input of a component. It can be static, the result of a JS expression, connected to the output of another component, or the result of an inline runnable. Here we will create an inline runnable that will convert the text to uppercase.', onNextClick: () => { clickFirstButtonBySelector('#component-input') setTimeout(() => { @@ -143,9 +144,7 @@ description: "Let's create an inline script.", onNextClick: () => { clickButtonBySelector('#app-editor-create-inline-script') - setTimeout(() => { - driver.moveNext() - }) + setTimeout(() => driver.moveNext()) } } }, @@ -155,7 +154,7 @@ popover: { title: 'Choose a language', description: - 'You can choose the language of your runnable. They are two type of runnables: frontend and backend.' + 'You can choose the language of your runnable. There are two type of runnables: frontend and backend.' } }, @@ -177,87 +176,71 @@ }, { element: '#create-deno-script', + onHighlighted: () => { + document.querySelector('#schema-plug-x')?.parentElement?.classList.remove('opacity-0') + }, 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: () => { + onNextClick: async () => { clickButtonBySelector('#create-deno-script') - setTimeout(() => { - if ($selectedComponent?.[0]) { - updateInlineRunnableCode( - $app, - $selectedComponent[0], - `export async function main(x: string) { - return x?.toLocaleUpperCase(); -} -` - ) - } + await wait(50) + if ($selectedComponent?.[0]) { + updateInlineRunnableCode( + $app, + $selectedComponent[0], + 'export function main(x: string) {\n return x?.toLocaleUpperCase();\n}' + ) + } - driver.moveNext() - }) + driver.moveNext() } } }, { - element: '#schema-plug', + element: '#schema-plug-x', + onHighlighted: () => { + document.querySelector('#schema-plug-x')?.parentElement?.classList.remove('opacity-0') + }, 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') + clickButtonBySelector('#schema-plug-x') setTimeout(() => { driver.moveNext() }) } } }, - { - element: '#connect-output-d', + element: '#connect-output-a', popover: { title: 'Select the output', - description: ' ', + description: 'Open the output selector of the text input component.', onNextClick: () => { - $connectingInput.opened = false - $connectingInput.input = undefined - + clickButtonBySelector('#connect-output-a') 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 = `` - - 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 = `
` - - wrapper.appendChild(p1) - wrapper.appendChild(id) - wrapper.appendChild(p2) - wrapper.appendChild(objectViewer) - - popover.description.appendChild(wrapper) - - tutorial?.renderControls(opts) } } }, - + { + element: '.component-output-viewer-a li *:has(> button[title="result"])', + popover: { + title: 'Select the output', + description: "Let's select the result of the text input component.", + onNextClick: () => { + setTimeout(async () => { + clickButtonBySelector('.component-output-viewer-a li button[title="result"]') + driver.moveNext() + }) + } + } + }, { element: '.wm-app-viewer', popover: { diff --git a/frontend/src/lib/components/tutorials/app/BackgroundRunnablesTutorial.svelte b/frontend/src/lib/components/tutorials/app/BackgroundRunnablesTutorial.svelte index 7671ac9db1..16524af0b5 100644 --- a/frontend/src/lib/components/tutorials/app/BackgroundRunnablesTutorial.svelte +++ b/frontend/src/lib/components/tutorials/app/BackgroundRunnablesTutorial.svelte @@ -1,5 +1,6 @@