Compare commits

...

11 Commits

Author SHA1 Message Date
tristantr
bd1bedf69e nit 2025-12-09 12:38:00 +01:00
tristantr
048368b1f8 Change banner wordings if new tutorial available 2025-12-09 12:37:38 +01:00
tristantr
c92ab37930 Put back banner when new tutorial is available for a user who completed all tutotorials, and never skipped all 2025-12-09 12:10:10 +01:00
tristantr
0bcacc1f10 Create shared Constants for TUTORIAL_DISMISSED_KEY 2025-12-09 11:34:36 +01:00
tristantr
635bb207af Add comments 2025-12-09 11:29:55 +01:00
tristantr
b5029f9a61 Add error handling 2025-12-09 11:27:12 +01:00
tristantr
2033ded4b4 Use derived instead of function for Svelte 5 good practices 2025-12-09 11:24:44 +01:00
tristantr
05099b891a Remove banner if all tutorials marked as completed, by role 2025-12-09 11:09:29 +01:00
tristantr
8ab0ffe2d1 Remove tutorials banner if all tutorials marked as completed 2025-12-09 11:03:31 +01:00
tristantr
3383eaf060 Remove banner if user has skipped all tutorials 2025-12-09 10:50:53 +01:00
tristantr
4be122c6fc Allow new operator so see the tutorial banner 2025-12-09 10:42:34 +01:00
3 changed files with 96 additions and 13 deletions

View File

@@ -5,26 +5,95 @@
import { goto } from '$app/navigation'
import { sendUserToast, type ToastAction } from '$lib/toast'
import { getLocalSetting, storeLocalSetting } from '$lib/utils'
import { skipAllTodos, syncTutorialsTodos } from '$lib/tutorialUtils'
import {
skipAllTodos,
syncTutorialsTodos,
TUTORIAL_BANNER_DISMISSED_KEY
} from '$lib/tutorialUtils'
import { tutorialsToDo, userStore, skippedAll } from '$lib/stores'
import { TUTORIALS_CONFIG } from '$lib/tutorials/config'
import { hasRoleAccess } from '$lib/tutorials/roleUtils'
import { onMount } from 'svelte'
const DISMISSED_KEY = 'tutorial_banner_dismissed'
let isDismissed = $state(false)
let hasCompletedAny = $state(false)
onMount(() => {
// Check if banner has been dismissed
isDismissed = getLocalSetting(DISMISSED_KEY) === 'true'
/**
* Get all tutorial indexes that are accessible to the current user based on their role.
* Automatically recomputes when $userStore changes.
*/
const accessibleTutorialIndexes = $derived.by(() => {
const indexes = new Set<number>()
const user = $userStore
for (const tab of Object.values(TUTORIALS_CONFIG)) {
// Check if user has access to this tab category
if (!hasRoleAccess(user, tab.roles)) {
continue
}
for (const tutorial of tab.tutorials) {
// Check if tutorial has an index and user has access to it
if (tutorial.index !== undefined && hasRoleAccess(user, tutorial.roles)) {
indexes.add(tutorial.index)
}
}
}
return indexes
})
onMount(async () => {
try {
// Sync tutorial progress from backend first
await syncTutorialsTodos()
// Check if banner has been manually dismissed via X button (soft dismiss, per-device)
const manuallyDismissed = getLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY) === 'true'
if (manuallyDismissed) {
isDismissed = true
return
}
// Check if user deliberately skipped all tutorials (permanent dismiss, from backend)
if ($skippedAll) {
isDismissed = true
return
}
// Safe to check tutorialsToDo here since we awaited syncTutorialsTodos() above
// Filter tutorialsToDo to only include tutorials accessible to the user's role
const remainingAccessibleTutorials = $tutorialsToDo.filter((index) =>
accessibleTutorialIndexes.has(index)
)
// Calculate if user has completed at least one tutorial (for banner wording)
hasCompletedAny = remainingAccessibleTutorials.length < accessibleTutorialIndexes.size
// Hide banner if all accessible tutorials are completed (but can reappear with new tutorials)
if (remainingAccessibleTutorials.length === 0) {
isDismissed = true
return
}
// Show banner - user has accessible tutorials to complete
isDismissed = false
} catch (error) {
console.error('Failed to sync tutorial progress:', error)
// Fallback to manual dismissal check only if API call fails
isDismissed = getLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY) === 'true'
}
})
async function handleSkipAllTutorials() {
// Skip all tutorials and set skipped_all flag in backend (permanent)
await skipAllTodos()
await syncTutorialsTodos()
storeLocalSetting(DISMISSED_KEY, 'true')
// No need to set localStorage - backend skipped_all flag is the source of truth
isDismissed = true
}
function dismissBanner() {
storeLocalSetting(DISMISSED_KEY, 'true')
storeLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY, 'true')
isDismissed = true
const actions: ToastAction[] = [
@@ -57,16 +126,24 @@
<GraduationCap size={20} class="text-accent-primary flex-shrink-0" />
<div class="flex-1 min-w-0">
<div class="text-emphasis flex-wrap text-left text-xs font-semibold">
Learn with interactive tutorials
{#if hasCompletedAny}
New tutorial available!
{:else}
Learn with interactive tutorials
{/if}
</div>
<div class="text-hint text-3xs truncate text-left font-normal">
Get started quickly with step-by-step guides on building flows, scripts, and more.
{#if hasCompletedAny}
Continue your learning journey and master new Windmill skills.
{:else}
Get started quickly with step-by-step guides on building flows, scripts, and more.
{/if}
</div>
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<Button size="xs" variant="accent" onclick={goToTutorials} startIcon={{ icon: GraduationCap }}>
View tutorials
View tutorials
</Button>
<button
onclick={dismissBanner}

View File

@@ -3,6 +3,12 @@ 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: string = '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.
@@ -64,6 +70,8 @@ export async function skipAllTodos() {
}
tutorialsToDo.set([])
skippedAll.set(true)
// Set skipped_all flag in backend - this is the permanent source of truth
await UserService.updateTutorialProgress({ requestBody: { progress: bits, skipped_all: true } })
}

View File

@@ -274,9 +274,7 @@
{/if}
</PageHeader>
{#if !$userStore?.operator}
<TutorialBanner />
{/if}
<TutorialBanner />
{#if !$userStore?.operator}
<div class="w-full overflow-auto scrollbar-hidden pb-2">