diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 544296d3c3..b57f98ab18 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,6 +24,7 @@ "d3-zoom": "^3.0.0", "date-fns": "^2.30.0", "diff": "^5.1.0", + "driver.js": "^1.3.0", "esm-env": "^1.0.0", "fast-equals": "^5.0.1", "graphql": "^16.7.1", @@ -3502,6 +3503,11 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/driver.js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.3.0.tgz", + "integrity": "sha512-ilUkVc5iMIYfMd8FdWy8n5Wv//gsJuRP+lo8QfWpwP9c0UGOgD7P9nVQMZwcdW84aqAZHHUHrV7GgiopAN6HUQ==" + }, "node_modules/earcut": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", @@ -12517,6 +12523,11 @@ "domhandler": "^5.0.3" } }, + "driver.js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.3.0.tgz", + "integrity": "sha512-ilUkVc5iMIYfMd8FdWy8n5Wv//gsJuRP+lo8QfWpwP9c0UGOgD7P9nVQMZwcdW84aqAZHHUHrV7GgiopAN6HUQ==" + }, "earcut": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8b5c06e25d..11796d7be1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -101,6 +101,7 @@ "d3-zoom": "^3.0.0", "date-fns": "^2.30.0", "diff": "^5.1.0", + "driver.js": "^1.3.0", "esm-env": "^1.0.0", "fast-equals": "^5.0.1", "graphql": "^16.7.1", diff --git a/frontend/src/lib/components/ArgInput.svelte b/frontend/src/lib/components/ArgInput.svelte index 6dd2ed4554..44cc8d0cb1 100644 --- a/frontend/src/lib/components/ArgInput.svelte +++ b/frontend/src/lib/components/ArgInput.svelte @@ -289,7 +289,7 @@ {#if i < itemsLimit}
{#if itemsType?.type == 'number'} - + {:else if itemsType?.type == 'string' && itemsType?.contentEncoding == 'base64'} Add item diff --git a/frontend/src/lib/components/ArrayTypeNarrowing.svelte b/frontend/src/lib/components/ArrayTypeNarrowing.svelte index 4795463043..aa6478d125 100644 --- a/frontend/src/lib/components/ArrayTypeNarrowing.svelte +++ b/frontend/src/lib/components/ArrayTypeNarrowing.svelte @@ -34,6 +34,7 @@ itemsType = undefined } }} + id="array-type-narrowing" > diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 49105258f1..52a2ff3e32 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -54,6 +54,7 @@ import { fade } from 'svelte/transition' import { loadFlowModuleState } from './flows/flowStateUtils' import FlowCopilotInputsModal from './copilot/FlowCopilotInputsModal.svelte' + import FlowBuilderTutorials from './FlowBuilderTutorials.svelte' export let initialPath: string = '' export let selectedId: string | undefined @@ -844,141 +845,150 @@ } $: $copilotCurrentStepStore === undefined && blurCopilot() + + let renderCount = 0 -{#if !$userStore?.operator} - - { - applyCopilotFlowInputs() - finishStepGen() - }} - on:canceled={async () => { - clearFlowInputsFromStep($copilotModulesStore[0]?.id) - finishStepGen() - }} - bind:open={openCopilotInputsModal} - inputs={Object.keys(copilotFlowInputs)} - /> - +{#key renderCount} + {#if !$userStore?.operator} + + { + applyCopilotFlowInputs() + finishStepGen() + }} + on:canceled={async () => { + clearFlowInputsFromStep($copilotModulesStore[0]?.id) + finishStepGen() + }} + bind:open={openCopilotInputsModal} + inputs={Object.keys(copilotFlowInputs)} + /> + -
- -
- {#if $copilotCurrentStepStore !== undefined} -
- {/if} -
-
- + +
+ {#if $copilotCurrentStepStore !== undefined} +
+ {/if} +
+
+ +
+ { + $flowStore = undo(history, $flowStore) + $selectedIdStore = 'Input' + }} + on:redo={() => { + $flowStore = redo(history) + }} />
- { - $flowStore = undo(history, $flowStore) - $selectedIdStore = 'Input' - }} - on:redo={() => { - $flowStore = redo(history) - }} - /> -
- +
+ {#if $enterpriseLicense && initialPath != ''} + + {/if} + { + renderCount += 1 }} /> + + + + + + + +
-
- {#if $enterpriseLicense && initialPath != ''} - - {/if} - - - - - - - -
+ + {#if $flowStateStore} + + {:else} + Loading... + {/if}
- - - {#if $flowStateStore} - - {:else} - Loading... - {/if} -
-{:else} - Flow Builder not available to operators -{/if} + {:else} + Flow Builder not available to operators + {/if} +{/key} diff --git a/frontend/src/lib/components/FlowBuilderTutorials.svelte b/frontend/src/lib/components/FlowBuilderTutorials.svelte new file mode 100644 index 0000000000..a5086ac4db --- /dev/null +++ b/frontend/src/lib/components/FlowBuilderTutorials.svelte @@ -0,0 +1,79 @@ + + + + + + + + + +
+ { + resetAllTodos() + }} + > +
+ Reset tutorials +
+
+ skipAllTodos()}> +
+ Skip tutorials +
+
+ + + + + diff --git a/frontend/src/lib/components/FlowPreviewContent.svelte b/frontend/src/lib/components/FlowPreviewContent.svelte index eb6f300fcf..9ef30af973 100644 --- a/frontend/src/lib/components/FlowPreviewContent.svelte +++ b/frontend/src/lib/components/FlowPreviewContent.svelte @@ -115,7 +115,10 @@ -
+
+ → +
{/if} diff --git a/frontend/src/lib/components/SchemaEditor.svelte b/frontend/src/lib/components/SchemaEditor.svelte index 9a734342b9..1626cc88b5 100644 --- a/frontend/src/lib/components/SchemaEditor.svelte +++ b/frontend/src/lib/components/SchemaEditor.svelte @@ -272,6 +272,7 @@ on:click={() => { schemaModal.openDrawer(Object.assign({}, DEFAULT_PROPERTY)) }} + id="flow-editor-add-property" > Add Property diff --git a/frontend/src/lib/components/SchemaModal.svelte b/frontend/src/lib/components/SchemaModal.svelte index bc798b6a06..4ea92d734a 100644 --- a/frontend/src/lib/components/SchemaModal.svelte +++ b/frontend/src/lib/components/SchemaModal.svelte @@ -131,6 +131,7 @@
{ - property.selectedType = argType; - property.format = undefined; - property.contentEncoding = undefined; - property.enum_ = undefined; - property.pattern = undefined; - property.default = undefined; + property.selectedType = argType + property.format = undefined + property.contentEncoding = undefined + property.enum_ = undefined + property.pattern = undefined + property.default = undefined if (argType == 'array') { property.items = { type: 'string' } } else { - property.items = undefined; + property.items = undefined } }} + id={`schema-modal-type-${argType}`} > {argType} @@ -185,12 +187,11 @@ color={!property.selectedType ? 'blue' : 'light'} btnClasses={!property.selectedType ? '!border-2' : 'm-[1px]'} on:click={() => { - property.selectedType = undefined; + property.selectedType = undefined }} > any -
@@ -260,6 +261,7 @@ on:click={() => { dispatch('save', property) }} + id="schema-modal-save" > Save diff --git a/frontend/src/lib/components/common/button/ButtonDropdown.svelte b/frontend/src/lib/components/common/button/ButtonDropdown.svelte index e250c9f63a..bda2410cff 100644 --- a/frontend/src/lib/components/common/button/ButtonDropdown.svelte +++ b/frontend/src/lib/components/common/button/ButtonDropdown.svelte @@ -22,6 +22,12 @@ } ] } + + let menuRef: Menu | undefined = undefined + + export function toggleOpen() { + menuRef?.openMenu() + } @@ -29,8 +35,12 @@ - - + {#if $$slots.buttonReplacement} + + {:else} + + + {/if} diff --git a/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte b/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte index 3845bc47dd..f1e7962105 100644 --- a/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte +++ b/frontend/src/lib/components/copilot/FlowCopilotStatus.svelte @@ -34,7 +34,7 @@
+
{branch.expr}
+ +
{/if} diff --git a/frontend/src/lib/components/flows/content/FlowInputs.svelte b/frontend/src/lib/components/flows/content/FlowInputs.svelte index bf837cbc63..fa1a3b044c 100644 --- a/frontend/src/lib/components/flows/content/FlowInputs.svelte +++ b/frontend/src/lib/components/flows/content/FlowInputs.svelte @@ -30,7 +30,7 @@ let filter = '' -
+
{#if summary == 'Terminate flow'} This is an identity step with an early stop that has 'true' for expression
-
+
{#if mod.value.iterator.type == 'javascript'} -
+
@@ -291,7 +293,7 @@
{#if selected === 'inputs' && (flowModule.value.type == 'rawscript' || flowModule.value.type == 'script' || flowModule.value.type == 'flow')} -
+
{:else if advancedSelected === 'concurrency'}
-

+

Concurrency Limits Allowed concurrency within a given timeframe

{#if flowModule.value.type == 'rawscript'}
-
Max number of executions within the time window
+
+ Max number of executions within the time window +
+ Set shared directory in the flow settings +
{:else if advancedSelected === 's3'}

S3 snippets - Pull, push and aggregate snippets for S3, particularly useful for ETL - processes. + + Pull, push and aggregate snippets for S3, particularly useful for ETL + processes. +

@@ -424,7 +428,8 @@ size="xs" on:click={() => editor.setCode(s3Scripts[flowModule.value['language']][s3Kind])} - >Apply snippet + > + Apply snippet
Test flow diff --git a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte index ca1151277f..1c378a19aa 100644 --- a/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte +++ b/frontend/src/lib/components/flows/map/FlowModuleSchemaMap.svelte @@ -22,10 +22,18 @@ import { getDependentComponents } from '../flowExplorer' import type { FlowCopilotContext } from '$lib/components/copilot/flow' import { fade } from 'svelte/transition' + import { tutorialsToDo } from '$lib/stores' + import FlowBuilderTutorialsForLoop from '$lib/components/tutorials/FlowBuilderTutorialsForLoop.svelte' + import FlowBranchOne from '$lib/components/tutorials/FlowBranchOne.svelte' + import FlowBranchAll from '$lib/components/tutorials/FlowBranchAll.svelte' export let modules: FlowModule[] | undefined export let sidebarSize: number | undefined = undefined + let flowBuilderTutorialsForLoop: FlowBuilderTutorialsForLoop | undefined = undefined + let flowBranchOne: FlowBranchOne | undefined = undefined + let flowBranchAll: FlowBranchAll | undefined = undefined + const { selectedId, moving, history, flowStateStore, flowStore } = getContext('FlowEditorContext') @@ -206,20 +214,28 @@ } }} on:insert={async ({ detail }) => { - if (detail.modules) { - await tick() - if ($moving) { - push(history, $flowStore) - let indexToRemove = $moving.modules.findIndex((m) => $moving?.module?.id == m.id) - $moving.modules.splice(indexToRemove, 1) - detail.modules.splice(detail.index, 0, $moving.module) - $selectedId = $moving.module.id - $moving = undefined - } else { - await insertNewModuleAtIndex(detail.modules, detail.index ?? 0, detail.detail) - $selectedId = detail.modules[detail.index ?? 0].id + if ($tutorialsToDo.includes(1) && detail.detail == 'forloop') { + flowBuilderTutorialsForLoop?.runTutorial() + } else if ($tutorialsToDo.includes(2) && detail.detail == 'branchone') { + flowBranchOne?.runTutorial() + } else if ($tutorialsToDo.includes(3) && detail.detail == 'branchall') { + flowBranchAll?.runTutorial() + } else { + if (detail.modules) { + await tick() + if ($moving) { + push(history, $flowStore) + let indexToRemove = $moving.modules.findIndex((m) => $moving?.module?.id == m.id) + $moving.modules.splice(indexToRemove, 1) + detail.modules.splice(detail.index, 0, $moving.module) + $selectedId = $moving.module.id + $moving = undefined + } else { + await insertNewModuleAtIndex(detail.modules, detail.index ?? 0, detail.detail) + $selectedId = detail.modules[detail.index ?? 0].id + } + $flowStore = $flowStore } - $flowStore = $flowStore } }} on:newBranch={async ({ detail }) => { @@ -253,3 +269,7 @@
+ + + + diff --git a/frontend/src/lib/components/flows/map/InsertModuleButton.svelte b/frontend/src/lib/components/flows/map/InsertModuleButton.svelte index 66aa25525b..3b66d9549f 100644 --- a/frontend/src/lib/components/flows/map/InsertModuleButton.svelte +++ b/frontend/src/lib/components/flows/map/InsertModuleButton.svelte @@ -35,115 +35,118 @@ - - {#if funcDesc.length === 0} -
- - {#if trigger} +
+ + {#if funcDesc.length === 0} +
- {/if} - - - - - - - - - {#if stop} + {#if trigger} + + {/if} + - {/if} -
- {/if} + + + + + + + {#if stop} + + {/if} +
+ {/if} +
diff --git a/frontend/src/lib/components/flows/map/MapItem.svelte b/frontend/src/lib/components/flows/map/MapItem.svelte index 330a458c7f..44c7da87f4 100644 --- a/frontend/src/lib/components/flows/map/MapItem.svelte +++ b/frontend/src/lib/components/flows/map/MapItem.svelte @@ -229,6 +229,7 @@ dispatch('newBranch', { module: mod }) }} type="button" + id="add-branch-button" class=" text-primary bg-surface border mx-0.5 rotate-180 border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 font-medium rounded-full text-sm w-6 h-6 flex items-center justify-center" > diff --git a/frontend/src/lib/components/flows/map/VirtualItem.svelte b/frontend/src/lib/components/flows/map/VirtualItem.svelte index 110cb49db2..ede45e366c 100644 --- a/frontend/src/lib/components/flows/map/VirtualItem.svelte +++ b/frontend/src/lib/components/flows/map/VirtualItem.svelte @@ -77,6 +77,7 @@ } } }} + id={`flow-editor-virtual-${label}`} >
+ import { driver } from 'driver.js' + import 'driver.js/dist/driver.css' + import { createEventDispatcher, getContext, tick } from 'svelte' + import type { FlowEditorContext } from '../flows/types' + import TutorialItem from './TutorialItem.svelte' + import { clickButtonBySelector, triggerAddFlowStep, selectFlowStepKind } from './utils' + import { updateProgress } from '$lib/tutorialUtils' + import { RawScript } from '$lib/gen' + + export let shouldRenderButton: boolean = true + + const { flowStore } = getContext('FlowEditorContext') + + const dispatch = createEventDispatcher() + + export function runTutorial() { + const branchAllTutorial = driver({ + showProgress: true, + allowClose: true, + onPopoverRender: (popover, { config, state }) => { + if (state.activeIndex == 0) { + const skipThisButton = document.createElement('button') + skipThisButton.innerText = 'Skip this' + skipThisButton.addEventListener('click', () => { + updateProgress(3) + + branchAllTutorial.destroy() + }) + skipThisButton.setAttribute( + 'class', + 'border px-2 py-1 !text-xs font-normal rounded-md hover:bg-surface-hover transition-all flex items-center' + ) + + const skipAllButton = document.createElement('button') + skipAllButton.innerText = 'Skip all' + skipAllButton.addEventListener('click', () => { + dispatch('skipAll') + branchAllTutorial.destroy() + }) + + skipAllButton.setAttribute( + 'class', + 'border px-2 py-1 !text-xs font-normal rounded-md hover:bg-surface-hover transition-all flex items-center' + ) + + const popoverDescription = document.querySelector('#driver-popover-description') + + const div = document.createElement('div') + + div.setAttribute('class', 'flex flex-row gap-2') + + if (popoverDescription) { + div.appendChild(skipThisButton) + div.appendChild(skipAllButton) + + popoverDescription.appendChild(div) + } + } + }, + steps: [ + { + popover: { + title: 'Welcome in the Windmil Flow editor', + description: 'Learn how to build our first branch to be executed on a condition' + } + }, + { + element: '#flow-editor-add-step-0', + popover: { + title: 'Add a step', + description: 'Click here to add a step to your flow', + onNextClick: () => { + triggerAddFlowStep(0) + + setTimeout(() => { + branchAllTutorial.moveNext() + }) + } + } + }, + + { + popover: { + title: 'Steps kind', + description: "Choose the kind of step you want to add. Let's start with a simple action" + }, + element: '#flow-editor-insert-module' + }, + + { + popover: { + title: 'Insert Branch all', + description: "Let's pick branch all", + onNextClick: () => { + selectFlowStepKind(6) + setTimeout(() => { + branchAllTutorial.moveNext() + }) + } + }, + element: '#flow-editor-insert-module > div > button:nth-child(6)' + }, + + { + element: '#add-branch-button', + popover: { + title: 'Add branch', + description: 'Click here to add a branch', + onNextClick: () => { + clickButtonBySelector('#add-branch-button') + + setTimeout(() => { + branchAllTutorial.moveNext() + }) + } + } + }, + + { + element: '#flow-editor-step-input', + popover: { + title: 'Branch all', + description: + 'All branches will be executed, and the result will be gathered in an array', + + onNextClick: () => { + if ($flowStore.value.modules[0].value.type === 'branchall') { + $flowStore.value.modules[0].value = { + type: 'branchall', + branches: [ + { + modules: [ + { + id: 'b', + value: { + type: 'rawscript', + content: + '// import * as wmill from "npm:windmill-client@1"\n\nexport async function main(x: string) {\n return "Hello"\n}\n', + language: RawScript.language.DENO, + input_transforms: { + x: { + type: 'static' + } + } + } + } + ], + skip_failure: false + }, + { + summary: '', + modules: [ + { + id: 'c', + value: { + type: 'rawscript', + content: + 'package inner\n\nfunc main() (interface{}, error) {\n\treturn "World", nil\n}\n', + language: RawScript.language.GO, + input_transforms: {} + } + } + ], + skip_failure: false + } + ], + parallel: false + } + } + + $flowStore = $flowStore + dispatch('reload') + + tick().then(() => { + branchAllTutorial.moveNext() + }) + } + } + }, + + { + element: '#flow-editor-test-flow', + popover: { + title: 'Test your flow', + description: 'We can now test our flow', + onNextClick: () => { + clickButtonBySelector('#flow-editor-test-flow') + + setTimeout(() => { + branchAllTutorial.moveNext() + }) + } + } + }, + + { + element: '#flow-editor-test-flow-drawer', + popover: { + title: 'Test your flow', + description: 'Finally we can test our flow, and view the results!', + onNextClick: () => { + clickButtonBySelector('#flow-editor-test-flow-drawer') + + setTimeout(() => { + branchAllTutorial.moveNext() + updateProgress(3) + }) + } + } + } + ] + }) + branchAllTutorial.drive() + } + + +{#if shouldRenderButton} + runTutorial()} + label="Branch all tutorial" + index={3} + id="flow-builder-tutorial-branchall" + /> +{/if} diff --git a/frontend/src/lib/components/tutorials/FlowBranchOne.svelte b/frontend/src/lib/components/tutorials/FlowBranchOne.svelte new file mode 100644 index 0000000000..1b2017ad38 --- /dev/null +++ b/frontend/src/lib/components/tutorials/FlowBranchOne.svelte @@ -0,0 +1,318 @@ + + +{#if shouldRenderButton} + runTutorial()} + label="Branch one tutorial" + index={2} + id="flow-builder-tutorial-branchone" + /> +{/if} diff --git a/frontend/src/lib/components/tutorials/FlowBuilderTutorialSimpleFlow.svelte b/frontend/src/lib/components/tutorials/FlowBuilderTutorialSimpleFlow.svelte new file mode 100644 index 0000000000..3a6d6df151 --- /dev/null +++ b/frontend/src/lib/components/tutorials/FlowBuilderTutorialSimpleFlow.svelte @@ -0,0 +1,317 @@ + + + simpleFlowTutorial.drive()} + label="Simple flow tutorial" + index={0} + id="flow-builder-tutorial-action" +/> diff --git a/frontend/src/lib/components/tutorials/FlowBuilderTutorialsForLoop.svelte b/frontend/src/lib/components/tutorials/FlowBuilderTutorialsForLoop.svelte new file mode 100644 index 0000000000..549f6ac714 --- /dev/null +++ b/frontend/src/lib/components/tutorials/FlowBuilderTutorialsForLoop.svelte @@ -0,0 +1,415 @@ + + +{#if shouldRenderButton} + runTutorial()} + label="For loops tutorial" + index={1} + id="flow-builder-tutorial-forloops" + /> +{/if} diff --git a/frontend/src/lib/components/tutorials/TutorialItem.svelte b/frontend/src/lib/components/tutorials/TutorialItem.svelte new file mode 100644 index 0000000000..895b80553d --- /dev/null +++ b/frontend/src/lib/components/tutorials/TutorialItem.svelte @@ -0,0 +1,27 @@ + + + +
+ {#if $tutorialsToDo.includes(index)} + + {:else} + + {/if} + {label} +
+
diff --git a/frontend/src/lib/components/tutorials/utils.ts b/frontend/src/lib/components/tutorials/utils.ts new file mode 100644 index 0000000000..6ab20cb7f2 --- /dev/null +++ b/frontend/src/lib/components/tutorials/utils.ts @@ -0,0 +1,43 @@ +export function setInputBySelector(selector: string, value: string) { + const input = document.querySelector(selector) as HTMLInputElement + + if (input) { + input.value = value + input.dispatchEvent(new Event('input', { bubbles: true })) + } +} + +export function clickButtonBySelector(selector: string) { + const button = document.querySelector(selector) as HTMLButtonElement + + if (button) { + button.click() + } +} + +export function triggerAddFlowStep(index: number) { + const button = document.querySelector(`#flow-editor-add-step-${index}`) as HTMLButtonElement + + if (button) { + button.parentElement?.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })) + } +} + +export function selectFlowStepKind(index: number) { + const button = document.querySelector( + `#flow-editor-insert-module > div > button:nth-child(${index})` + ) as HTMLButtonElement + + if (button) { + button?.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })) + } +} + +export function selectOptionsBySelector(selector: string, value: string) { + const select = document.querySelector(selector) as HTMLSelectElement + + if (select) { + select.value = value + select.dispatchEvent(new Event('change', { bubbles: true })) + } +} diff --git a/frontend/src/lib/tutorialUtils.ts b/frontend/src/lib/tutorialUtils.ts index 080cee8504..32f3f6c5c0 100644 --- a/frontend/src/lib/tutorialUtils.ts +++ b/frontend/src/lib/tutorialUtils.ts @@ -1,52 +1,51 @@ -import { get } from "svelte/store"; -import { tutorialsToDo } from "./stores"; -import { UserService } from "./gen"; +import { get } from 'svelte/store' +import { tutorialsToDo } from './stores' +import { UserService } from './gen' const MAX_TUTORIAL_ID = 6 export async function updateProgress(id: number) { - const bef = get(tutorialsToDo) - const aft = bef.filter((x) => x != id) - tutorialsToDo.set(aft) - let bits = 0 - for (let i = 0; i <= MAX_TUTORIAL_ID; i++) { - let mask = 1 << i; - if (!aft.includes(i)) { - bits = bits | mask - } - } - await UserService.updateTutorialProgress({requestBody: { progress: bits}}) - + const bef = get(tutorialsToDo) + const aft = bef.filter((x) => x != id) + tutorialsToDo.set(aft) + let bits = 0 + for (let i = 0; i <= MAX_TUTORIAL_ID; i++) { + let mask = 1 << i + if (!aft.includes(i)) { + bits = bits | mask + } + } + await UserService.updateTutorialProgress({ requestBody: { progress: bits } }) } export async function skipAllTodos() { - let bits = 0 - for (let i = 0; i <= MAX_TUTORIAL_ID; i++) { - let mask = 1 << i; - bits = bits | mask - } - tutorialsToDo.set([]) - await UserService.updateTutorialProgress({requestBody: { progress: bits}}) - + let bits = 0 + for (let i = 0; i <= MAX_TUTORIAL_ID; i++) { + let mask = 1 << i + bits = bits | mask + } + tutorialsToDo.set([]) + await UserService.updateTutorialProgress({ requestBody: { progress: bits } }) } export async function resetAllTodos() { - let todos: number[] = [] - for (let i = 0; i <= MAX_TUTORIAL_ID; i++) { - todos.push(i) - } - tutorialsToDo.set(todos) - await UserService.updateTutorialProgress({requestBody: { progress: 0 }}) + let todos: number[] = [] + for (let i = 0; i <= MAX_TUTORIAL_ID; i++) { + todos.push(i) + } + tutorialsToDo.set(todos) + + await UserService.updateTutorialProgress({ requestBody: { progress: 0 } }) } export async function syncTutorialsTodos() { - const bits: number = (await UserService.getTutorialProgress()).progress! - const todos: number[] = [] - for (let i = 0; i <= MAX_TUTORIAL_ID; i++) { - let mask = 1 << i; - if ((bits & mask) == 0) { - todos.push(i) - } - } - tutorialsToDo.set(todos) -} \ No newline at end of file + const bits: number = (await UserService.getTutorialProgress()).progress! + const todos: number[] = [] + for (let i = 0; i <= MAX_TUTORIAL_ID; i++) { + let mask = 1 << i + if ((bits & mask) == 0) { + todos.push(i) + } + } + tutorialsToDo.set(todos) +}