Skip to content

fix(onboarding): replace internal boot native selects#1942

Merged
Ajit-Mehrotra merged 4 commits intomainfrom
codex/onboarding-color-fixes
Mar 20, 2026
Merged

fix(onboarding): replace internal boot native selects#1942
Ajit-Mehrotra merged 4 commits intomainfrom
codex/onboarding-color-fixes

Conversation

@Ajit-Mehrotra
Copy link
Contributor

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

Summary

This PR replaces the onboarding flow's remaining native dropdowns with the shared Select component from @unraid/ui, fixes the resulting select handler typing, and aligns the onboarding select trigger styling with the rest of the form in dark mode.

The work is targeted at the black-theme report where dropdowns in onboarding were either unreadable for some users or visually inconsistent with adjacent inputs.

Work Intent

Onboarding color fixes.

Problem

The onboarding flow still had a few browser-native <select> elements in the internal boot step. That left the open dropdown menu styling up to the browser and operating system, which can produce theme mismatches that are hard to reproduce locally.

After moving those controls to the shared Select component, another issue became obvious: the shared trigger defaults looked darker than the surrounding onboarding inputs in dark mode, so the controls still felt visually off even though the popup rendering problem was addressed.

What Changed

  • replaced the native selects in OnboardingInternalBootStep with @unraid/ui Select
  • converted slot-count, device, and boot-size options into typed SelectItemType[] item lists
  • preserved existing step behavior by wiring explicit update handlers back into the same refs and validation flow
  • normalized emitted select values through a shared helper so the onboarding step stays type-safe with the shared component event contract
  • updated the internal boot step test to stub the shared Select component without depending on portal rendering
  • applied onboarding-specific Select trigger styling in OnboardingInternalBootStep so internal boot dropdowns match nearby inputs in dark mode
  • applied the same onboarding-specific Select trigger styling in OnboardingCoreSettingsStep so timezone, language, and theme selectors match the rest of the onboarding form

Why This Approach

Using the same shared Select component already used elsewhere in onboarding gives us control over menu rendering and theme tokens instead of relying on browser-native popup behavior.

Keeping the dark-mode styling override local to onboarding lets us make these selects visually match the onboarding form without changing the shared Select component globally.

Verification

Passed in this worktree:

  • pnpm --dir web type-check
  • pnpm --dir api type-check
  • pnpm --dir web lint
  • pnpm --filter ./api lint
  • pnpm --dir web test
  • pnpm --filter ./api test
  • pnpm --dir web test OnboardingInternalBootStep.test.ts
  • pnpm --dir web test OnboardingCoreSettingsStep.test.ts

Notes

  • dependencies were installed in this worktree so hooks and checks could run normally
  • the first commit on this branch was created before install and used --no-verify because lint-staged was unavailable in the fresh worktree at that point
  • no production app or dev server was started or restarted

Summary by CodeRabbit

  • Refactor

    • Onboarding boot, device, and preset selections now use a consistent Select component for improved UI consistency and interaction handling.
    • Select styling consolidated for time zone, language, and theme controls for uniform appearance and focus behavior.
  • Tests

    • Test harness updated with a functional mock Select that supports item rendering and selection change propagation.

- Purpose: replace the internal boot step's native dropdowns with the shared Select component used elsewhere in onboarding.
- Before: the step rendered browser-native select popups for slot count, device selection, and boot size presets.
- Problem: native dropdown rendering can pick OS/browser popup colors that mismatch the black theme, which made option text effectively invisible for some users while remaining hard to reproduce locally.
- Change: swap those controls to @unraid/ui Select, build typed item lists for each control, and preserve the step's existing selection and validation flow through explicit update handlers.
- Tests: update the internal boot step test to stub the shared Select component so the existing step behavior stays covered without depending on portal rendering.
- Verification: automated test execution is still blocked in this new worktree because dependencies are not installed yet.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Walkthrough

Replaced native selects with @unraid/ui Select in onboarding components, added computed Select item lists and explicit model-value handlers/normalization, and introduced a test mock Select to emulate selection behavior in tests.

Changes

Cohort / File(s) Summary
Onboarding Internal Boot Step
web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue
Replaced three native <select> elements with Select from @unraid/ui. Added computed SelectItemType[] lists for slot count, device options, and boot-size presets (includes 'custom'). Switched to :model-value/@update:model-value, normalizing emitted values to strings and validating/assigning slot count only when finite and within 1–2.
Onboarding Core Settings styling
web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue
Introduced shared onboardingSelectClasses and applied via :class to existing Select components (replaced prior class="w-full" usage) to unify select styling.
Test Infrastructure / Mock
web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts
Added a mock Select component for @unraid/ui in tests. The stub accepts modelValue, items, disabled, and placeholder; emits update:modelValue on change; renders a <select> with optional placeholder and one <option> per items entry (uses item.value and respects item.disabled).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through dropdowns, made them one bright click,
Options lined up tidy, values trimmed and quick,
Tests got a tiny friend to mimic every pick,
A nibble of code, a celebratory flick. 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: replacing native elements with the shared Select component in the OnboardingInternalBootStep component.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ 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-color-fixes

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 20, 2026

Codecov Report

❌ Patch coverage is 70.66667% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 51.53%. Comparing base (f0241a8) to head (2912373).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...ts/Onboarding/steps/OnboardingInternalBootStep.vue 68.57% 22 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1942      +/-   ##
==========================================
+ Coverage   51.51%   51.53%   +0.01%     
==========================================
  Files        1025     1025              
  Lines       70460    70506      +46     
  Branches     7793     7803      +10     
==========================================
+ Hits        36297    36332      +35     
- Misses      34040    34051      +11     
  Partials      123      123              

☔ 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

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

64-85: Mock emits string values while component may expect numbers.

The native <select> always emits string values via $event.target.value. For slotCountItems which uses numeric values (e.g., value: 1), the mock will emit "1" instead of 1. The handleSlotCountChange handler already handles this conversion, so tests will pass, but be aware of this type coercion behavior.

Additionally, the mock assumes all items are objects with item.value. Per SelectItemType definition, items can be primitives. Consider adding robustness for future-proofing:

♻️ Optional: Handle primitive items in mock
   Select: {
     props: ['modelValue', 'items', 'disabled', 'placeholder'],
     emits: ['update:modelValue'],
     template: `
       <select
         data-testid="select"
         :disabled="disabled"
         :value="modelValue ?? ''"
         `@change`="$emit('update:modelValue', $event.target.value)"
       >
         <option v-if="placeholder" value="">{{ placeholder }}</option>
         <option
           v-for="item in items"
-          :key="item.value"
-          :value="item.value"
-          :disabled="item.disabled"
+          :key="typeof item === 'object' ? item.value : item"
+          :value="typeof item === 'object' ? item.value : item"
+          :disabled="typeof item === 'object' ? item.disabled : false"
         >
-          {{ item.label }}
+          {{ typeof item === 'object' ? item.label : item }}
         </option>
       </select>
     `,
   },

Based on learnings: "Stub complex child components that aren't the focus of the test" - the current mock is sufficient for the component's usage patterns.

🤖 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 64 - 85, The Select mock emits string values from $event.target.value
which can misrepresent numeric item values used by slotCountItems and consumed
by handleSlotCountChange; update the Select mock (component name Select) to emit
a parsed value (e.g., coerce numeric-looking strings back to numbers before
emitting to 'update:modelValue') and make the item iteration resilient to
primitive items by using item.value ?? item when reading the option value/label
so the mock supports both object and primitive SelectItemType shapes.
🤖 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/OnboardingInternalBootStep.vue`:
- Around line 526-531: The handlers handleSlotCountChange,
handleDeviceSelection, and handleBootSizePresetChange have too-narrow parameter
types (currently string | number) and must accept the Select component's
AcceptableValue union; update each handler signature to accept AcceptableValue
(import the type from Radix Vue) or a safe unknown/any, then perform type-guards
inside the function: detect and coerce number/strings via
Number.parseInt/Number.isFinite before assigning to slotCount.value, handle
boolean/object cases appropriately (ignore or map to a valid value), and ensure
device/boot-preset handlers validate the value type before setting the related
reactive state. Ensure imports/reference to AcceptableValue and use explicit
guards so the handlers match the `@update`:model-value event shape.

---

Nitpick comments:
In `@web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts`:
- Around line 64-85: The Select mock emits string values from
$event.target.value which can misrepresent numeric item values used by
slotCountItems and consumed by handleSlotCountChange; update the Select mock
(component name Select) to emit a parsed value (e.g., coerce numeric-looking
strings back to numbers before emitting to 'update:modelValue') and make the
item iteration resilient to primitive items by using item.value ?? item when
reading the option value/label so the mock supports both object and primitive
SelectItemType shapes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 420b9a36-a8f8-4a6a-a8bb-a01e4fcdf5ab

📥 Commits

Reviewing files that changed from the base of the PR and between f0241a8 and 562e594.

📒 Files selected for processing (2)
  • web/__test__/components/Onboarding/OnboardingInternalBootStep.test.ts
  • web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue

- Purpose: make the internal boot step's shared Select handlers compatible with the value shape emitted by the component.
- Before: the handlers assumed narrower string/number inputs than the Select component's emitted event type.
- Problem: web type-check failed on the new onboarding select wiring because the handler signatures were not assignable to the component event contract.
- Change: accept unknown values at the handler boundary, normalize them through a single typed helper, and keep the slot-count and preset logic unchanged after conversion.
- Verification: pnpm --dir web type-check, pnpm --dir api type-check, pnpm --dir web lint, pnpm --filter ./api lint, pnpm --dir web test, and pnpm --filter ./api test all pass in this worktree.
- Purpose: make the internal boot step's shared Select controls visually match the rest of the onboarding form in dark mode.
- Before: the internal boot dropdown triggers used the shared Select defaults, which rendered darker than the neighboring onboarding inputs.
- Problem: the controls looked visually out of place and made the onboarding dark theme feel inconsistent even after moving off native browser selects.
- Change: add onboarding-specific Select trigger classes for border, background, text, placeholder, and focus-ring treatment within the internal boot step.
- Scope: applies to slot count, device selection, and boot reserved size only.
- Verification: pnpm --dir web type-check and pnpm --dir web test OnboardingInternalBootStep.test.ts passed before commit.
- Purpose: make the remaining onboarding Select controls match the form surfaces in dark mode.
- Before: core settings dropdowns still used the shared Select defaults, so timezone, language, and theme looked darker than adjacent onboarding inputs.
- Problem: the onboarding flow still felt visually inconsistent after fixing the internal boot step alone.
- Change: apply the same onboarding-specific Select trigger classes in the core settings step for timezone, language, and theme selectors.
- Scope: styling only; no behavior or data-flow changes.
- Verification: pnpm --dir web type-check and pnpm --dir web test OnboardingCoreSettingsStep.test.ts passed before commit.
@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/PR1942/dynamix.unraid.net.plg

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/OnboardingCoreSettingsStep.vue (1)

359-360: Consider extracting duplicated styling constant to a shared location.

This onboardingSelectClasses constant is identically defined in OnboardingInternalBootStep.vue (lines 558-560). Extracting it to a shared module (e.g., onboarding/constants.ts or a composable) would prevent future divergence if the styling needs adjustment.

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

In `@web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue` around
lines 359 - 360, The duplicated CSS class string assigned to
onboardingSelectClasses (found in OnboardingCoreSettingsStep.vue and identically
in OnboardingInternalBootStep.vue) should be extracted to a shared
constant/export so both components import the same value; create a new module
(e.g., onboarding/constants.ts or a composable) that exports a named constant
like ONBOARDING_SELECT_CLASSES, replace the inline onboardingSelectClasses
declarations in both components with an import of that constant, and update
usages to reference the imported symbol to prevent future drift.
🤖 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/OnboardingCoreSettingsStep.vue`:
- Around line 359-360: The duplicated CSS class string assigned to
onboardingSelectClasses (found in OnboardingCoreSettingsStep.vue and identically
in OnboardingInternalBootStep.vue) should be extracted to a shared
constant/export so both components import the same value; create a new module
(e.g., onboarding/constants.ts or a composable) that exports a named constant
like ONBOARDING_SELECT_CLASSES, replace the inline onboardingSelectClasses
declarations in both components with an import of that constant, and update
usages to reference the imported symbol to prevent future drift.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 51ac4834-6896-4c59-9985-c6c13b25d049

📥 Commits

Reviewing files that changed from the base of the PR and between 6a1d374 and 2912373.

📒 Files selected for processing (2)
  • web/src/components/Onboarding/steps/OnboardingCoreSettingsStep.vue
  • web/src/components/Onboarding/steps/OnboardingInternalBootStep.vue

@Ajit-Mehrotra Ajit-Mehrotra marked this pull request as ready for review March 20, 2026 21:08
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: 2912373271

ℹ️ 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 +519 to +523
const getDeviceSelectItems = (index: number): SelectItemType[] =>
deviceOptions.value.map((option) => ({
value: option.value,
label: option.label,
disabled: isDeviceDisabled(option.value, index),

Choose a reason for hiding this comment

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

P2 Badge Restore an explicit empty device option

Mapping deviceOptions directly into items removes the previously selectable empty value, and placeholder only affects display state. Because duplicate device choices are disabled via isDeviceDisabled, users with two selected slots can no longer swap assignments (for example A/B to B/A) since there is no way to temporarily clear one slot. Please add an explicit empty item (equivalent to the old <option value="">) so a slot can be unselected and reassigned.

Useful? React with 👍 / 👎.

@Ajit-Mehrotra Ajit-Mehrotra requested a review from elibosley March 20, 2026 21:14
Copy link
Member

@elibosley elibosley left a comment

Choose a reason for hiding this comment

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

Great change

@Ajit-Mehrotra Ajit-Mehrotra merged commit d6ea032 into main Mar 20, 2026
11 checks passed
@Ajit-Mehrotra Ajit-Mehrotra deleted the codex/onboarding-color-fixes branch March 20, 2026 21:18
elibosley pushed a commit that referenced this pull request Mar 23, 2026
🤖 I have created a release *beep* *boop*
---


## [4.31.0](v4.30.1...v4.31.0)
(2026-03-23)


### Features

* **api:** support encrypted array start inputs
([#1944](#1944))
([018a8d5](018a8d5))
* **onboarding:** add shared loading states
([#1945](#1945))
([776c8cc](776c8cc))
* Serverside state for onboarding display
([#1936](#1936))
([682d51c](682d51c))


### Bug Fixes

* **api:** reconcile emhttp state without spinning disks
([#1946](#1946))
([d3e0b95](d3e0b95))
* **onboarding:** auto-open incomplete onboarding on 7.3+
([#1940](#1940))
([f0241a8](f0241a8))
* **onboarding:** replace internal boot native selects
([#1942](#1942))
([d6ea032](d6ea032))
* preserve onboarding resume state on reload
([#1941](#1941))
([91f7fe9](91f7fe9))
* recover VM availability after reconnect
([#1947](#1947))
([e064de7](e064de7))
* Unify callback server payloads
([#1938](#1938))
([f58fcc0](f58fcc0))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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