Skip to content

refactor(onboarding): migrate to Nuxt UI + browser history support#1949

Merged
Ajit-Mehrotra merged 36 commits intomainfrom
codex/onboarding-ui-refactor
Mar 25, 2026
Merged

refactor(onboarding): migrate to Nuxt UI + browser history support#1949
Ajit-Mehrotra merged 36 commits intomainfrom
codex/onboarding-ui-refactor

Conversation

@Ajit-Mehrotra
Copy link
Contributor

@Ajit-Mehrotra Ajit-Mehrotra commented Mar 23, 2026

Summary

  • Standardize the onboarding flow on Nuxt UI primitives across all steps (Core Settings, Plugins, Internal Boot, License, Summary, Next Steps)
  • Replace HeadlessUI Disclosure components with @unraid/ui Accordion (enhanced with v-model and open state slot props)
  • Integrate browser back/forward navigation support and "No Updates Needed" messaging (from feat: onboarding use history #1964)
  • Polish UX: disabled button states, alert styling, skip button behavior, radio button boot mode selection

What Changed

HeadlessUI → Nuxt UI Migration

  • All onboarding steps now use Nuxt UI components (USelectMenu, UInput, UCheckbox, UAlert, UModal, UButton, URadioGroup) instead of HeadlessUI or native HTML elements
  • All @headlessui/vue imports removed from onboarding
  • Custom <transition> wrappers removed — Accordion uses Reka UI's built-in animations

Accordion Enhancement (@unraid/ui)

  • Extended Accordion component with optional v-model support, itemClass prop, and { open } boolean in scoped slots
  • Changes are additive — existing accordion usage is unaffected (no global style changes)

UX Polish

  • Radio buttons for boot mode selection (USB vs Storage)
  • Disabled state on BrandButton via aria-disabled with pointer-events-none
  • cursor-not-allowed on disabled Back/Skip navigation buttons
  • Neutral-colored UAlerts for info panels and warnings
  • Solid green UAlert for initialization ready state
  • Outline variant for Skip button in license step
  • Placeholder text in empty device select menus
  • Guard against slotCount NaN from undefined USelectMenu values
  • :disabled on reboot modal buttons to prevent double-click

Browser History + No-Changes Messaging (from #1964)

  • Browser back/forward navigation support within onboarding flows
  • "No Updates Needed" messaging when no configuration changes are applied
  • Enhanced session handling with improved distinction between automatic and manual onboarding modes

Test Updates

  • Replaced HeadlessUI Disclosure mocks with Accordion stubs
  • Fixed auto-import bypass: tests use VM state for USelectMenu interaction (Nuxt UI auto-imports bypass global.stubs)
  • Updated "Setup Applied" → "No Updates Needed" assertions for no-changes path
  • All 618 web tests pass, all 1962 API tests pass

Why

  • The onboarding flow mixed native controls, HeadlessUI components, and older modal patterns — making the UI feel uneven
  • HeadlessUI was an unnecessary dependency when @unraid/ui (backed by Reka UI) provides equivalent components
  • Disabled states were not communicated visually to users

Includes

Verification

pnpm --dir web test -- --run
pnpm --dir web lint
pnpm --dir web type-check
pnpm --dir api test -- --run
pnpm --dir api lint
pnpm --dir api type-check

Smoke Test

  1. Walk the full onboarding flow — confirm all controls use consistent Nuxt UI styling
  2. Setup Boot: verify radio buttons for USB/Storage selection, disabled states on Back/Skip during load
  3. Internal Boot: expand/collapse eligibility details, verify device selects have placeholder text
  4. License: test Skip dialog with solid info alert and outline Skip button
  5. Summary: expand/collapse plugins accordion, verify "No Updates Needed" when no changes apply
  6. Next Steps: verify reboot confirmation modal buttons disable during submission
  7. Browser back/forward: navigate between steps using browser history buttons

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replaces Headless UI / @unraid/ui primitives with Nuxt UI components across onboarding UIs and tests, moves module-level test mocks to per-mount Vue Test Utils stubs, updates TypeScript declaration import paths, and adds controlled modelValue support to the Accordion component.

Changes

Cohort / File(s) Summary
Test stub migration
web/__test__/components/Onboarding/OnboardingCoreSettingsStep.test.ts, web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts, web/__test__/components/Onboarding/OnboardingLicenseStep.test.ts, web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts, web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts, web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts
Removed module-level vi.mock UI mocks; register per-mount global.stubs (e.g., USelectMenu, USwitch, UModal, UButton, UAlert); add document.body reset in beforeEach; updated queries/assertions to use stubs and VM helpers.
Onboarding components (UI swaps & wiring)
web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue, web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue, web/src/components/Onboarding/steps/OnboardingLicenseStep.vue, web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue, web/src/components/Onboarding/steps/OnboardingPluginsStep.vue, web/src/components/Onboarding/steps/OnboardingSummaryStep.vue
Replaced Select/Switch/Dialog/HeadlessUI usage with USelectMenu/USwitch/UModal/UButton/UAlert/Accordion; rewired props/events to model-value/update:model-value or v-model; removed custom dialog/blockquote markup and unused icon imports; adjusted local types, handlers, and disabled/lock logic.
Type declaration path updates
web/auto-imports.d.ts, web/components.d.ts
Adjusted module specifier paths for auto-imports and GlobalComponents so Nuxt UI/composable types resolve from ../../api/node_modules/... instead of ../node_modules/....
Accordion controlled state
unraid-ui/src/components/common/accordion/Accordion.vue
Added optional `modelValue?: string
Brand button variants
unraid-ui/src/components/brand/brand-button.variants.ts
Extended base class to apply visual disabled styles when aria-disabled is present (mirrors disabled:* utility behavior).
Small UI styling tweaks
web/src/components/Onboarding/steps/OnboardingOverviewStep.vue
Added Tailwind disabled-state styling classes (disabled:cursor-not-allowed, disabled:opacity-50) to Back and Skip buttons without changing behavior.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I swapped mocks for stubs with care,
Menus and modals now take their place,
Types found new paths and accordions share,
Little hops of change across the space,
A nibble, a tweak — a tidy code embrace.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main refactoring objective: migrating the onboarding components to use Nuxt UI primitives and adding browser history support.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/onboarding-ui-refactor

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link

codecov bot commented Mar 23, 2026

Codecov Report

❌ Patch coverage is 86.80445% with 83 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.02%. Comparing base (9e0e89e) to head (fddfd10).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
web/src/components/Onboarding/OnboardingModal.vue 74.00% 38 Missing and 1 partial ⚠️
...d-ui/src/components/common/accordion/Accordion.vue 0.00% 24 Missing ⚠️
...g/standalone/OnboardingInternalBoot.standalone.vue 90.27% 14 Missing ⚠️
...ts/Onboarding/steps/OnboardingInternalBootStep.vue 95.41% 5 Missing ⚠️
...d-ui/src/components/brand/brand-button.variants.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1949      +/-   ##
==========================================
+ Coverage   51.99%   52.02%   +0.03%     
==========================================
  Files        1030     1030              
  Lines       71171    71349     +178     
  Branches     7959     8053      +94     
==========================================
+ Hits        37002    37121     +119     
- Misses      34046    34104      +58     
- Partials      123      124       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts (1)

89-126: ⚠️ Potential issue | 🔴 Critical

Stub template missing role="switch" attribute and data-state attribute required by test queries.

The USwitch stub template renders an <input type="checkbox"> without role or data-state attributes, but the tests at lines 119 and 151 query for [role="switch"] and check .attributes('data-state'). These mismatches will cause the findAll('[role="switch"]') queries to return an empty array and fail assertions at lines 120, 121-123, 152, and 153-155.

🐛 Proposed fix to add missing attributes to stub
 USwitch: {
   props: ['modelValue', 'disabled'],
   emits: ['update:modelValue'],
   template: `
     <input
       data-testid="plugin-switch"
       type="checkbox"
+      role="switch"
+      :data-state="modelValue ? 'checked' : 'unchecked'"
       :checked="modelValue"
       :disabled="disabled"
       `@change`="$emit('update:modelValue', $event.target.checked)"
     />
   `,
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts` around
lines 89 - 126, The USwitch stub template is missing the role="switch" and
data-state attributes the tests expect; update the USwitch stub (the component
object named USwitch in the stubs) so its template includes role="switch" and
sets data-state based on the modelValue prop (e.g., 'checked' when modelValue
truthy, otherwise 'unchecked'), while still reflecting the disabled prop and
emitting update:modelValue on change so the existing test queries for
[role="switch"] and assertions on .attributes('data-state') succeed.
🧹 Nitpick comments (2)
web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts (1)

386-391: Timer advancement change looks intentional.

The change from vi.runAllTimersAsync() to vi.advanceTimersByTimeAsync(2500) provides more precise control over timer simulation. Ensure this 2500ms value aligns with any debounce/delay logic in the component (e.g., confirmation dialog transitions).

Consider extracting the 2500 constant to a named variable (e.g., const DIALOG_TRANSITION_MS = 2500) to document its purpose and make future adjustments easier.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts` around
lines 386 - 391, The test now advances timers with
vi.advanceTimersByTimeAsync(2500) which should match the component's
debounce/transition delay; replace the magic number 2500 with a descriptive
constant (e.g., const DIALOG_TRANSITION_MS = 2500) used where
vi.advanceTimersByTimeAsync is called and ensure the value matches the
component's delay/debounce used in the OnboardingSummaryStep (verify any timeout
used in the component code that controls the confirmation dialog/transition).
Update references in this test around wrapper and clickButtonByText so the
intent of the wait is clear and maintainable.
web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts (1)

20-22: Accessing internal VM methods is a pragmatic workaround for stubbed components.

The InternalBootVm type and getDeviceSelectItems() calls test internal computed values rather than rendered output. While this slightly deviates from "test behavior, not implementation details" guidance, it's a reasonable trade-off when USelectMenu is stubbed and the actual options rendering is masked.

Consider adding a comment explaining why VM access is necessary here, or alternatively, enhance the stub to render option labels in a queryable format.

Also applies to: 294-302

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts` around
lines 20 - 22, The test accesses the component VM via the InternalBootVm type
and calls getDeviceSelectItems() to inspect computed option values because
USelectMenu is stubbed and doesn't render option labels; add a brief inline
comment next to the InternalBootVm type and each getDeviceSelectItems() usage
explaining that VM access is intentional (pragmatic workaround for the stubbed
USelectMenu) so future readers know this is not accidental coupling to
implementation, or alternatively update the USelectMenu test stub to render
option labels (so tests can query DOM text) and replace getDeviceSelectItems()
calls with DOM queries if you prefer behavior-level testing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/auto-imports.d.ts`:
- Around line 9-37: The auto-imports.d.ts contains hardcoded
../../api/node_modules paths (seen in symbols like avatarGroupInjectionKey,
defineLocale, useLocale, useToast, etc.) which break in different checkout
layouts; update the auto-import generation configuration (Nuxt/Vite/auto-imports
plugin config used to produce web/auto-imports.d.ts) to emit
checkout-independent module paths (use paths relative to web/, e.g.
../api/node_modules or better ../node_modules when deps are consolidated) and
then regenerate the declarations (run the type-check/generation command such as
pnpm --dir web type-check or your auto-imports regenerate task) so the produced
file no longer contains ../../api/node_modules references.

---

Outside diff comments:
In `@web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts`:
- Around line 89-126: The USwitch stub template is missing the role="switch" and
data-state attributes the tests expect; update the USwitch stub (the component
object named USwitch in the stubs) so its template includes role="switch" and
sets data-state based on the modelValue prop (e.g., 'checked' when modelValue
truthy, otherwise 'unchecked'), while still reflecting the disabled prop and
emitting update:modelValue on change so the existing test queries for
[role="switch"] and assertions on .attributes('data-state') succeed.

---

Nitpick comments:
In `@web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts`:
- Around line 20-22: The test accesses the component VM via the InternalBootVm
type and calls getDeviceSelectItems() to inspect computed option values because
USelectMenu is stubbed and doesn't render option labels; add a brief inline
comment next to the InternalBootVm type and each getDeviceSelectItems() usage
explaining that VM access is intentional (pragmatic workaround for the stubbed
USelectMenu) so future readers know this is not accidental coupling to
implementation, or alternatively update the USelectMenu test stub to render
option labels (so tests can query DOM text) and replace getDeviceSelectItems()
calls with DOM queries if you prefer behavior-level testing.

In `@web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts`:
- Around line 386-391: The test now advances timers with
vi.advanceTimersByTimeAsync(2500) which should match the component's
debounce/transition delay; replace the magic number 2500 with a descriptive
constant (e.g., const DIALOG_TRANSITION_MS = 2500) used where
vi.advanceTimersByTimeAsync is called and ensure the value matches the
component's delay/debounce used in the OnboardingSummaryStep (verify any timeout
used in the component code that controls the confirmation dialog/transition).
Update references in this test around wrapper and clickButtonByText so the
intent of the wait is clear and maintainable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0981f0f9-fcc1-4a4d-ad9a-6ac2eba76860

📥 Commits

Reviewing files that changed from the base of the PR and between 8f4ea98 and ad9ff32.

📒 Files selected for processing (14)
  • web/__test__/components/Onboarding/OnboardingCoreSettingsStep.test.ts
  • web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts
  • web/__test__/components/Onboarding/OnboardingLicenseStep.test.ts
  • web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts
  • web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts
  • web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts
  • web/auto-imports.d.ts
  • web/components.d.ts
  • web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue
  • web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
  • web/src/components/Onboarding/steps/OnboardingLicenseStep.vue
  • web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue
  • web/src/components/Onboarding/steps/OnboardingPluginsStep.vue
  • web/src/components/Onboarding/steps/OnboardingSummaryStep.vue

@Ajit-Mehrotra Ajit-Mehrotra force-pushed the codex/onboarding-ui-refactor branch from ad9ff32 to 2075ce9 Compare March 24, 2026 20:19
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts (1)

68-71: Minor: Accordion mock passes open as string.

The mock template uses open="false" (string) rather than :open="false" (boolean). This doesn't affect test behavior since the slots only need to render, but for consistency with the actual component's boolean open slot-scope:

Optional fix for boolean binding
   Accordion: {
     props: ['items', 'type', 'collapsible', 'class'],
-    template: `<div data-testid="accordion"><template v-for="item in items" :key="item.value"><slot name="trigger" :item="item" :open="false" /><slot name="content" :item="item" :open="false" /></template></div>`,
+    template: `<div data-testid="accordion"><template v-for="item in items" :key="item.value"><slot name="trigger" :item="item" :open="false" /><slot name="content" :item="item" :open="false" /></template></div>`,
   },

Actually, looking again - :open="false" in template strings already works correctly since it's a JavaScript expression evaluated as boolean. No change needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts` around
lines 68 - 71, The Accordion test mock currently sets the slot prop as
open="false" (a string) in the template; update the template in the Accordion
mock (the component definition with props ['items','type','collapsible','class']
and its template string) to bind a boolean by changing open="false" to
:open="false" for both trigger and content slots so the slot-scope receives a
boolean open value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts`:
- Around line 109-112: The UAlert test stub currently renders a plain <div>
without the alert role so the test assertion looking for [role="alert"] (in
OnboardingNextStepsStep.test.ts) no longer matches; update the UAlert stub (the
component defined as UAlert with props ['description']) so its root element
includes role="alert" (e.g., render <div role="alert"> or equivalent) and still
exposes the description prop and named slot so the error banner path exercises
the alert markup.

In `@web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts`:
- Around line 265-278: Tests currently synthesize modal text by reaching into
wrapper.vm (the wrapper.text override using SummaryVm and properties like
showBootDriveWarningDialog, showApplyResultDialog, applyResultTitle,
applyResultMessage) which masks missing DOM rendering and calls VM handlers
indirectly; revert this by removing the wrapper.text override and instead assert
modals and their content from the rendered DOM (use wrapper.find/findAll to
locate the dialog elements, text(), and footer buttons) and simulate user
interactions via DOM events (click on the actual footer button elements) so you
test output/behavior not internal refs or methods (do not call VM handlers like
handleApplyResultConfirm or read local refs declared in
OnboardingSummaryStep.vue).

---

Nitpick comments:
In `@web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts`:
- Around line 68-71: The Accordion test mock currently sets the slot prop as
open="false" (a string) in the template; update the template in the Accordion
mock (the component definition with props ['items','type','collapsible','class']
and its template string) to bind a boolean by changing open="false" to
:open="false" for both trigger and content slots so the slot-scope receives a
boolean open value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c53a7606-7757-4069-94a7-b98e7a377f05

📥 Commits

Reviewing files that changed from the base of the PR and between ad9ff32 and 2075ce9.

📒 Files selected for processing (15)
  • unraid-ui/src/components/common/accordion/Accordion.vue
  • web/__test__/components/Onboarding/OnboardingCoreSettingsStep.test.ts
  • web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts
  • web/__test__/components/Onboarding/OnboardingLicenseStep.test.ts
  • web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts
  • web/__test__/components/Onboarding/OnboardingPluginsStep.test.ts
  • web/__test__/components/Onboarding/OnboardingSummaryStep.test.ts
  • web/auto-imports.d.ts
  • web/components.d.ts
  • web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue
  • web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
  • web/src/components/Onboarding/steps/OnboardingLicenseStep.vue
  • web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue
  • web/src/components/Onboarding/steps/OnboardingPluginsStep.vue
  • web/src/components/Onboarding/steps/OnboardingSummaryStep.vue
✅ Files skipped from review due to trivial changes (3)
  • web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue
  • web/auto-imports.d.ts
  • web/src/components/Onboarding/steps/OnboardingSummaryStep.vue
🚧 Files skipped from review as they are similar to previous changes (6)
  • web/test/components/Onboarding/OnboardingPluginsStep.test.ts
  • web/test/components/Onboarding/OnboardingLicenseStep.test.ts
  • web/src/components/Onboarding/steps/OnboardingLicenseStep.vue
  • web/src/components/Onboarding/steps/OnboardingPluginsStep.vue
  • web/src/components/Onboarding/steps/OnboardingNextStepsStep.vue
  • web/components.d.ts

Comment on lines +109 to +112
UAlert: {
props: ['description'],
template: '<div>{{ description }}<slot name="description" /></div>',
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Preserve the alert role in this UAlert stub.

Line 199 still asserts [role="alert"], but this stub now renders a plain <div>. That makes the completion-failure path stop exercising the alert markup and will likely fail once the error banner is shown.

🔧 Minimal fix
          UAlert: {
            props: ['description'],
-            template: '<div>{{ description }}<slot name="description" /></div>',
+            template: '<div role="alert">{{ description }}<slot name="description" /></div>',
          },
As per coding guidelines "Verify that the expected elements are rendered in Vue components".
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
UAlert: {
props: ['description'],
template: '<div>{{ description }}<slot name="description" /></div>',
},
UAlert: {
props: ['description'],
template: '<div role="alert">{{ description }}<slot name="description" /></div>',
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/__test__/components/Onboarding/OnboardingNextStepsStep.test.ts` around
lines 109 - 112, The UAlert test stub currently renders a plain <div> without
the alert role so the test assertion looking for [role="alert"] (in
OnboardingNextStepsStep.test.ts) no longer matches; update the UAlert stub (the
component defined as UAlert with props ['description']) so its root element
includes role="alert" (e.g., render <div role="alert"> or equivalent) and still
exposes the description prop and named slot so the error banner path exercises
the alert markup.

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

7 similar comments
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
web/src/components/Onboarding/steps/OnboardingSummaryStep.vue (1)

1351-1385: Modal state may persist across open/close cycles with :open vs v-if.

The previous Dialog implementation used v-if which destroyed the component on close, guaranteeing fresh state on each open. With UModal using :open binding, the modal content remains mounted but hidden. For this specific modal, this should be fine since there's no internal form state to reset, but be aware of this behavioral difference.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue` around lines
1351 - 1385, The UModal is bound with :open="showBootDriveWarningDialog" which
keeps the modal mounted when closed (unlike the prior v-if behavior) causing
potential persisted internal state; either revert to conditional mounting by
replacing :open with v-if="showBootDriveWarningDialog" on the UModal to destroy
and recreate the component, or explicitly reset any internal state when closing
by calling reset logic from
handleBootDriveWarningCancel/handleBootDriveWarningConfirm (and ensure
selectedBootDevices is refreshed before opening) so the modal has fresh state
each time.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue`:
- Around line 1351-1385: The UModal is bound with
:open="showBootDriveWarningDialog" which keeps the modal mounted when closed
(unlike the prior v-if behavior) causing potential persisted internal state;
either revert to conditional mounting by replacing :open with
v-if="showBootDriveWarningDialog" on the UModal to destroy and recreate the
component, or explicitly reset any internal state when closing by calling reset
logic from handleBootDriveWarningCancel/handleBootDriveWarningConfirm (and
ensure selectedBootDevices is refreshed before opening) so the modal has fresh
state each time.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2f091bc6-773a-4626-a266-b629345a87fa

📥 Commits

Reviewing files that changed from the base of the PR and between 156c1d5 and 61ad61f.

📒 Files selected for processing (6)
  • unraid-ui/src/components/brand/brand-button.variants.ts
  • web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue
  • web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
  • web/src/components/Onboarding/steps/OnboardingOverviewStep.vue
  • web/src/components/Onboarding/steps/OnboardingPluginsStep.vue
  • web/src/components/Onboarding/steps/OnboardingSummaryStep.vue
✅ Files skipped from review due to trivial changes (1)
  • web/src/components/Onboarding/steps/OnboardingOverviewStep.vue

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

4 similar comments
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue`:
- Around line 1380-1387: The UModal instance bound to showApplyResultDialog
allows overlay/ESC dismissal which can bypass the completion flow; update the
UModal usage (the component with props :open="showApplyResultDialog" and events
`@update`:open) to include :dismissible="false" so the modal cannot be closed via
overlay or ESC, ensuring closure only goes through handleApplyResultConfirm
which triggers props.onComplete().

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 592a4d16-d963-4040-9cf3-dd08d154f6fc

📥 Commits

Reviewing files that changed from the base of the PR and between 61ad61f and 3fb762a.

📒 Files selected for processing (3)
  • unraid-ui/src/components/ui/accordion/AccordionTrigger.vue
  • web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
  • web/src/components/Onboarding/steps/OnboardingSummaryStep.vue
✅ Files skipped from review due to trivial changes (1)
  • unraid-ui/src/components/ui/accordion/AccordionTrigger.vue

Comment on lines +1380 to 1387
<UModal
:open="showApplyResultDialog"
:portal="false"
:title="applyResultTitle"
:description="applyResultMessage"
:ui="{ footer: 'justify-end', overlay: 'z-50', content: 'z-50 w-[calc(100vw-2rem)] max-w-3xl' }"
@update:open="showApplyResultDialog = $event"
>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Nuxt UI's UModal (the version used in this repo), does the modal close by default on overlay click or Escape key when bound with :open and @update:open? What is the recommended prop/config to make it non-dismissible?

💡 Result:

In Nuxt UI's UModal (current version as of 2026), when bound with :open and @update:open (v-model:open), the modal closes by default on overlay click or Escape key, as the dismissible prop defaults to true. The recommended prop to make it non-dismissible is :dismissible="false". When set to false, attempts to close via overlay or Escape emit a close:prevent event (handle with @close:prevent if needed).

Citations:


🏁 Script executed:

# Find the OnboardingSummaryStep.vue file
fd -t f "OnboardingSummaryStep.vue"

Repository: unraid/api

Length of output: 115


🏁 Script executed:

# Check the file size first
wc -l web/src/components/Onboarding/steps/OnboardingSummaryStep.vue

Repository: unraid/api

Length of output: 120


🏁 Script executed:

# Read the modal code at lines 1380-1387
sed -n '1375,1395p' web/src/components/Onboarding/steps/OnboardingSummaryStep.vue

Repository: unraid/api

Length of output: 889


🏁 Script executed:

# Search for handleApplyResultConfirm method
sed -n '1040,1050p' web/src/components/Onboarding/steps/OnboardingSummaryStep.vue

Repository: unraid/api

Length of output: 246


🏁 Script executed:

# Look for the modal footer content with OK button
sed -n '1380,1420p' web/src/components/Onboarding/steps/OnboardingSummaryStep.vue

Repository: unraid/api

Length of output: 1778


🏁 Script executed:

# Check if dismissible prop is set anywhere for this modal
sed -n '1375,1410p' web/src/components/Onboarding/steps/OnboardingSummaryStep.vue | grep -i dismissible

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Check all UModal instances in this file to see if any use dismissible prop
rg -n "UModal" web/src/components/Onboarding/steps/OnboardingSummaryStep.vue -A 8

Repository: unraid/api

Length of output: 1163


🏁 Script executed:

# Check if dismissible prop is used elsewhere in this component or similar components
rg -n "dismissible" web/src/components/Onboarding/ -B 2 -A 2

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Check the boot drive warning dialog handler to see if it follows the same pattern
rg -n "showBootDriveWarningDialog" web/src/components/Onboarding/steps/OnboardingSummaryStep.vue

Repository: unraid/api

Length of output: 363


🏁 Script executed:

# Check what props.onComplete is and how critical it is
rg -n "onComplete" web/src/components/Onboarding/steps/OnboardingSummaryStep.vue -B 2 -A 2

Repository: unraid/api

Length of output: 299


🏁 Script executed:

# Find where OnboardingSummaryStep is used to understand the context of onComplete
rg -n "OnboardingSummaryStep" web/src/components/ --type vue -B 2 -A 2

Repository: unraid/api

Length of output: 81


🏁 Script executed:

# Find where OnboardingSummaryStep is used
rg -n "OnboardingSummaryStep" web/src/components/ -B 2 -A 2

Repository: unraid/api

Length of output: 4624


🏁 Script executed:

# Check if there are any event handlers or side effects triggered by showApplyResultDialog changes
rg -n "showApplyResultDialog" web/src/components/Onboarding/steps/OnboardingSummaryStep.vue -B 1 -A 1

Repository: unraid/api

Length of output: 878


🏁 Script executed:

# Check if there's a watcher on showApplyResultDialog that might prevent early closing
rg -n "watch.*showApplyResultDialog" web/src/components/Onboarding/steps/OnboardingSummaryStep.vue -B 2 -A 5

Repository: unraid/api

Length of output: 36


Prevent accidental modal dismissal from bypassing completion flow.

Line 1386 directly mirrors @update:open into state. Without :dismissible="false", users can close this modal via overlay click or ESC, which skips handleApplyResultConfirm (Line 1043) and therefore skips props.onComplete() (Line 1045).

Add :dismissible="false" to disable overlay/ESC dismissal:

Proposed fix
      <UModal
        :open="showApplyResultDialog"
        :portal="false"
+       :dismissible="false"
        :title="applyResultTitle"
        :description="applyResultMessage"
        :ui="{ footer: 'justify-end', overlay: 'z-50', content: 'z-50 w-[calc(100vw-2rem)] max-w-3xl' }"
        `@update`:open="showApplyResultDialog = $event"
      >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<UModal
:open="showApplyResultDialog"
:portal="false"
:title="applyResultTitle"
:description="applyResultMessage"
:ui="{ footer: 'justify-end', overlay: 'z-50', content: 'z-50 w-[calc(100vw-2rem)] max-w-3xl' }"
@update:open="showApplyResultDialog = $event"
>
<UModal
:open="showApplyResultDialog"
:portal="false"
:dismissible="false"
:title="applyResultTitle"
:description="applyResultMessage"
:ui="{ footer: 'justify-end', overlay: 'z-50', content: 'z-50 w-[calc(100vw-2rem)] max-w-3xl' }"
`@update`:open="showApplyResultDialog = $event"
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/Onboarding/steps/OnboardingSummaryStep.vue` around lines
1380 - 1387, The UModal instance bound to showApplyResultDialog allows
overlay/ESC dismissal which can bypass the completion flow; update the UModal
usage (the component with props :open="showApplyResultDialog" and events
`@update`:open) to include :dismissible="false" so the modal cannot be closed via
overlay or ESC, ensuring closure only goes through handleApplyResultConfirm
which triggers props.onComplete().

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

6 similar comments
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Purpose: replace remaining mixed onboarding form primitives and modal actions with Nuxt UI components so the flow feels visually and behaviorally consistent end to end.
- Before: onboarding still mixed native inputs, bespoke Headless UI switches, custom callouts, and older dialog/button patterns across Core Settings, Plugins, Internal Boot, License, Summary, and Next Steps.
- Problem: the flow looked uneven, dark-mode surfaces were inconsistent, and the test suite still assumed pre-refactor modal behavior.
- Now: onboarding uses Nuxt UI switches, inputs, select menus, checkboxes, alerts, modals, and dialog buttons throughout the targeted steps while keeping the intentional native footer CTA buttons unchanged.
- How: migrated the step components, regenerated Nuxt auto-import typings, and updated the onboarding Vitest specs to assert the new modal/result flows and storage-boot confirmation behavior.
- Purpose: enable consumers to react to accordion open/close state
- Before: the Accordion wrapper only passed `item` to #trigger and
  #content slots, with no way to know if a given item was expanded
- Problem: onboarding Disclosure components rely on `v-slot="{ open }"`
  to toggle label text ("Show Details" / "Hide Details") and rotate
  chevron icons — the Accordion wrapper could not replace them
- Change: track open items via v-model/modelValue on AccordionRoot,
  compute per-item `open` boolean, and pass it to both #trigger and
  #content scoped slots as `{ item, open }`
- How it works:
  - internal `openValue` ref mirrors modelValue or defaultValue
  - `isItemOpen(value)` checks whether a value is in the active set
  - `handleUpdate` syncs internal state and emits update:modelValue
  - slots receive `open` alongside `item` for conditional rendering
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Purpose: soften the initialization ready alert on the summary page
- Before: UAlert used variant="solid" producing a bold green block
- Problem: solid green was visually heavy against the page background
- Change: switch to variant="subtle" for a lighter, more subdued look
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Purpose: fix the Setup Applied dialog showing a large empty body
- Before: #body slot always rendered (even when empty), and the
  dialog used max-w-3xl regardless of whether logs were present
- Problem: empty #body slot produced a tall blank white area in the
  success case; dialog was also unnecessarily wide
- Change: conditionally render #body slot only when diagnostic logs
  exist; use max-w-md for the compact success case and max-w-3xl
  only when the wider log console is shown
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Purpose: fix misleading log ordering in the summary step
- Before: "Finalizing setup..." appeared between SSH and identity
  update, making it look like a separate intermediate step
- Problem: confusing to users — "Finalizing" should come after all
  substantive mutations are complete
- Change: move the "Finalizing setup" log to after the server
  identity update so the sequence is SSH → Identity → Finalizing
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Purpose: fix two edge cases found during adversarial review
- Reboot modal: restore :disabled="isCompleting" on Cancel and
  Confirm buttons so double-clicking can't fire duplicate reboot
  requests while the mutation is in-flight
- SlotCount: guard Number() conversion with Number.isFinite() and
  minimum check so if USelectMenu ever emits undefined, slotCount
  stays at its current value instead of becoming NaN (which would
  break the device selector loop)
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

elibosley and others added 2 commits March 25, 2026 15:54
- Purpose: merge feat/onboarding-use-history into our Nuxt UI branch
- Brings in browser back/forward navigation support for onboarding
- Adds "No Updates Needed" messaging when no config changes apply
- Resolves 1 merge conflict in OnboardingSummaryStep.test.ts:
  kept our VM-based assertion approach (showApplyResultDialog) and
  adopted PR 1964's updated "No Updates Needed" text
- Fixed additional test assertion for the wider dialog test case
  that also expected the old "Setup Applied" title
- All 618 web tests pass after merge
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@Ajit-Mehrotra Ajit-Mehrotra changed the title Refactor onboarding flow to use Nuxt UI primitives refactor(onboarding): migrate to Nuxt UI + browser history support Mar 25, 2026
- Purpose: fix TypeScript errors introduced by merging PR 1964
- sessionId extraction: cast to string after typeof guard so TS
  narrows from string | {} to string
- historySessionId.value: add ?? '' fallback since ref is string | null
  but callers guard non-null before calling buildHistoryState
- handleClose onClick: wrap in arrow function to prevent Vue from
  passing MouseEvent to the options parameter
- Remove dead code: hasCoreSettingChanges and hasAnyChangesToApply
  computeds replaced by inline hasUpdatesToApply in handleComplete
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Purpose: fix the same accordion styling issues as the plugins
  accordion on the storage boot eligibility details panel
- Before: default AccordionTrigger added a bottom border, hover
  underline, and a second chevron icon alongside the custom one
- Change: add item-class="border-none" to remove the bottom border,
  trigger-class with hover:no-underline and [&>svg]:hidden to remove
  the hover underline and hide the duplicate default chevron
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Purpose: align eligibility accordion with plugins accordion style
- Before: custom ChevronDownIcon in the trigger slot plus the default
  AccordionTrigger chevron resulted in two chevrons; the default was
  hidden via [&>svg]:hidden
- Change: remove the custom ChevronDownIcon entirely and use the
  default AccordionTrigger chevron colored with [&>svg]:text-primary,
  matching the plugins accordion implementation
- Also removes unused ChevronDownIcon import
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

- Purpose: fix chevron color and alignment on eligibility panel
- Before: chevron was text-primary (orange) and misaligned below
  the "View details" text due to items-start on the trigger div
- Change: remove text-primary override so chevron uses default color,
  switch trigger div from items-start to items-center so the chevron
  aligns inline with the "View details" text
@github-actions
Copy link
Contributor

🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev

@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1949/dynamix.unraid.net.plg

@Ajit-Mehrotra Ajit-Mehrotra marked this pull request as ready for review March 25, 2026 21:36
@Ajit-Mehrotra Ajit-Mehrotra merged commit afe1ae6 into main Mar 25, 2026
14 checks passed
@Ajit-Mehrotra Ajit-Mehrotra deleted the codex/onboarding-ui-refactor branch March 25, 2026 21:38
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fddfd10192

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +460 to +462
if (
availableSteps.value.includes(nextHistoryState.stepId) &&
currentStepId.value !== nextHistoryState.stepId

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle hidden-step history entries on popstate

When a step disappears mid-session (for example ACTIVATE_LICENSE after registration state changes), browser back can land on a same-session history entry whose stepId is no longer in availableSteps. This branch returns without changing currentStepId, so the UI appears stuck on the current step and users need extra back presses to move. In practice this breaks the new browser-history navigation whenever step availability changes during onboarding.

Useful? React with 👍 / 👎.

Comment on lines +522 to +523
historyPosition.value += 1;
window.history.pushState(buildHistoryState(stepId, historyPosition.value), '', window.location.href);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent automatic sessions from polluting browser history

Step changes always call pushState, including automatic onboarding sessions. But automatic close paths do not rewind history (rewind only happens for manual sessions), so closing onboarding leaves one inert history entry per step. After onboarding closes, pressing browser back walks through these stale entries with no visible state change, which is a regression in normal page navigation behavior.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants