Files
windmill/frontend/src/lib/tutorialUtils.ts
T
Tristan TR e96da54001 fix: New tutorials (#7427)
* Put back banner a new tutorial is available for a user that completed all his tutorials and never skipped all

* Create onboarding tutorial for operators in tutorial config file

* Add router and steps for onboarding tutorial for operators

* Improve onboarding tutorial for operators

* Improve the tutorial UX

* Refactor

* Remove cursor from last step of operator onboarding tutorial

* Improve filtering per role

* Add Runs page tutorial

* Improve Runs page tutorial

* Add failed run

* Simplify Runs tutorial with job clicks into one unique step

* Finish overall structure of Runs tutorial

* Improve wordings

* Prevent breaking animations by clicking on Next or Previous

* Add success and failure logo to step title

* Improve wording

* Create util function for moving cursor

* Nits

* Improve wordings

* Differentiate successfull and failed jobs steps

* Remove delete flows if operator to prevent permission errors

* Add comment
2025-12-23 18:19:30 +00:00

225 lines
6.3 KiB
TypeScript

import { get } from 'svelte/store'
import { tutorialsToDo, skippedAll } from './stores'
import { UserService } from './gen'
import { TUTORIALS_CONFIG } from './tutorials/config'
/**
* LocalStorage key for tracking if the tutorial banner has been dismissed.
* Shared between tutorialUtils and TutorialBanner component.
*/
export const TUTORIAL_BANNER_DISMISSED_KEY = 'tutorial_banner_dismissed'
/**
* Get the maximum tutorial index from the config.
* This ensures we don't hardcode the max ID and it automatically updates when tutorials are added.
*/
function getMaxTutorialId(): number {
let maxId = 0
for (const tab of Object.values(TUTORIALS_CONFIG)) {
for (const tutorial of tab.tutorials) {
if (tutorial.index !== undefined && tutorial.index > maxId) {
maxId = tutorial.index
}
}
}
return maxId
}
const MAX_TUTORIAL_ID = getMaxTutorialId()
/**
* Helper function to calculate tutorial progress for a given set of tutorial indexes.
* Returns total count. For completed count, use in component with reactive store access.
*/
export function getTutorialProgressTotal(tutorialIndexes: Record<string, number>): number {
return Object.values(tutorialIndexes).length
}
/**
* Helper function to calculate completed tutorials count.
* Must be called with current tutorialsToDo array.
*/
export function getTutorialProgressCompleted(
tutorialIndexes: Record<string, number>,
tutorialsToDoArray: number[]
): number {
return Object.values(tutorialIndexes).filter((index) => !tutorialsToDoArray.includes(index))
.length
}
export async function updateProgress(id: number) {
const bef = get(tutorialsToDo)
const aft = bef.filter((x) => x != id)
tutorialsToDo.set(aft)
skippedAll.set(false) // Mark as not skipped when completing a tutorial
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, skipped_all: false } })
}
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([])
skippedAll.set(true)
await UserService.updateTutorialProgress({ requestBody: { progress: bits, skipped_all: true } })
}
export async function resetAllTodos() {
let todos: number[] = []
for (let i = 0; i <= MAX_TUTORIAL_ID; i++) {
todos.push(i)
}
tutorialsToDo.set(todos)
skippedAll.set(false)
await UserService.updateTutorialProgress({ requestBody: { progress: 0, skipped_all: false } })
}
/**
* Skip (mark as complete) all tutorials in a specific set of indexes
*/
export async function skipTutorialsByIndexes(tutorialIndexes: number[]) {
const currentTodos = get(tutorialsToDo)
const aft = currentTodos.filter((x) => !tutorialIndexes.includes(x))
tutorialsToDo.set(aft)
// Get current progress bits
const currentResponse = await UserService.getTutorialProgress()
let bits: number = currentResponse.progress ?? 0
// Set bits for the specified indexes
for (const index of tutorialIndexes) {
const mask = 1 << index
bits = bits | mask
}
// Only set skipped_all to true if ALL tutorials are now complete
const allComplete = aft.length === 0
await UserService.updateTutorialProgress({
requestBody: {
progress: bits,
skipped_all: allComplete
}
})
}
/**
* Reset (mark as incomplete) all tutorials in a specific set of indexes
*/
export async function resetTutorialsByIndexes(tutorialIndexes: number[]) {
const currentTodos = get(tutorialsToDo)
const aft = [...new Set([...currentTodos, ...tutorialIndexes])]
tutorialsToDo.set(aft)
skippedAll.set(false)
// Get current progress bits
const currentResponse = await UserService.getTutorialProgress()
let bits: number = currentResponse.progress ?? 0
// Clear bits for the specified indexes
for (const index of tutorialIndexes) {
const mask = 1 << index
bits = bits & ~mask
}
await UserService.updateTutorialProgress({
requestBody: {
progress: bits,
skipped_all: false
}
})
}
/**
* Update a single tutorial's completion status by index
*/
async function updateTutorialStatusByIndex(tutorialIndex: number, completed: boolean) {
const currentTodos = get(tutorialsToDo)
const isInTodos = currentTodos.includes(tutorialIndex)
// Only update if the status needs to change
// isInTodos = true means NOT completed, isInTodos = false means completed
// So if completed === !isInTodos, we're already in the desired state
if (completed === !isInTodos) {
return // Already in the desired state
}
// Update todos list
const aft = completed
? currentTodos.filter((x) => x !== tutorialIndex)
: [...currentTodos, tutorialIndex]
tutorialsToDo.set(aft)
skippedAll.set(false)
// Get current progress bits
const currentResponse = await UserService.getTutorialProgress()
let bits: number = currentResponse.progress ?? 0
// Update bit for this tutorial index
const mask = 1 << tutorialIndex
bits = completed ? bits | mask : bits & ~mask
await UserService.updateTutorialProgress({
requestBody: {
progress: bits,
skipped_all: false
}
})
}
/**
* Reset (mark as incomplete) a single tutorial by index
*/
export async function resetTutorialByIndex(tutorialIndex: number) {
await updateTutorialStatusByIndex(tutorialIndex, false)
}
/**
* Mark a single tutorial as completed by index
*/
export async function completeTutorialByIndex(tutorialIndex: number) {
await updateTutorialStatusByIndex(tutorialIndex, true)
}
export async function syncTutorialsTodos() {
const response = await UserService.getTutorialProgress()
const bits: number = response.progress!
const skipped: boolean = response.skipped_all ?? false
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)
skippedAll.set(skipped)
}
export function tutorialInProgress() {
const svg = document.getElementsByClassName('driver-overlay driver-overlay-animated')
return svg.length > 0
}
/**
* Check if tutorials should be hidden from the main menu.
* Returns true if all tutorials are completed OR user skipped all.
*/
export function shouldHideTutorialsFromMainMenu(): boolean {
const todos = get(tutorialsToDo)
const skipped = get(skippedAll)
// Hide if all tutorials are completed OR user skipped all
return todos.length === 0 || skipped
}