-
Notifications
You must be signed in to change notification settings - Fork 11
fix: enhance dark mode support in theme handling #1808
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Added PHP logic to determine if the current theme is dark and set a CSS variable accordingly. - Introduced a new function to retrieve the dark mode state from the CSS variable in JavaScript. - Updated the theme store to initialize dark mode based on the CSS variable, ensuring consistent theme application across the application. This improves user experience by ensuring the correct theme is applied based on user preferences.
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughBackend emits theme-related CSS variables and conditional banner gradient vars; frontend bootstraps theme from those CSS vars, exposes computed getters and a lazy Changes
Sequence Diagram(s)sequenceDiagram
participant PHP as Backend (PHP)
participant RootCSS as :root CSS vars
participant Frontend as Frontend (store + components)
participant DOM as Document / .unapi
participant GraphQL as Apollo (lazy)
participant API as Server GraphQL
participant FS as Dynamix config file
PHP->>RootCSS: emit `--theme-name`, `--theme-dark-mode` and (cond.) banner gradient vars
RootCSS-->>Frontend: read via getComputedStyle(:root)
Frontend->>DOM: apply/remove 'dark' on documentElement, body, `.unapi` and teleported containers
Frontend->>GraphQL: fetchTheme() (lazy) -> GraphQL returns theme payload
Frontend->>Frontend: mapPublicTheme / merge with CSS-derived values
Frontend->>API: GraphQL mutation setTheme(theme)
API->>FS: write theme into dynamix config file
API->>API: reload dynamix config from disk and update in-memory config
API-->>Frontend: updated Theme result
Frontend->>RootCSS: write `--theme-dark-mode` when toggled
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this 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
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php(1 hunks)web/src/store/theme.ts(2 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*
📄 CodeRabbit inference engine (.cursor/rules/default.mdc)
Never add comments unless they are needed for clarity of function
Files:
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.phpweb/src/store/theme.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with.jsextensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks
Files:
web/src/store/theme.ts
web/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
Always run
pnpm codegenfor GraphQL code generation in the web directory
Files:
web/src/store/theme.ts
web/src/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)
Files:
web/src/store/theme.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never use theanytype. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start
Files:
web/src/store/theme.ts
**/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
Ensure Vue reactivity imports like
computed,ref, andwatchEffectare added to store files even with Nuxt auto-imports enabled
Files:
web/src/store/theme.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Applied to files:
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.phpweb/src/store/theme.ts
📚 Learning: 2025-02-21T18:40:10.810Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
Applied to files:
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.phpweb/src/store/theme.ts
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.
Applied to files:
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.phpweb/src/store/theme.ts
📚 Learning: 2025-02-20T15:52:56.733Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
Applied to files:
web/src/store/theme.ts
🧬 Code graph analysis (1)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php (2)
api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
theme(58-60)api/src/unraid-api/graph/resolvers/vars/vars.resolver.ts (1)
vars(18-23)
🔇 Additional comments (3)
web/src/store/theme.ts (2)
53-60: LGTM!The function correctly reads the CSS variable set by PHP and converts it to a boolean, with appropriate SSR handling.
67-67: LGTM!Setting the CSS variable alongside the dark class creates a bidirectional sync, allowing the variable to stay in sync with runtime theme changes.
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php (1)
212-214: Dark theme lists are synchronized.Verification confirms both the PHP (
web-components-extractor.php, line 212) and TypeScript (web/src/store/theme.ts, line 13) dark theme lists contain identical values['gray', 'black']. The lists are currently in sync, and dark mode detection is consistent between backend and frontend.
- Removed the previous test for applying dark mode classes to .unapi elements. - Added a new test to verify that dark mode is initialized based on a CSS variable when the store is created. - Mocked getComputedStyle to simulate dark mode and checked that the appropriate classes are added to document elements. This enhances the test coverage for theme handling in the application.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1808 +/- ##
==========================================
- Coverage 52.04% 51.85% -0.19%
==========================================
Files 874 875 +1
Lines 50372 50699 +327
Branches 5017 5049 +32
==========================================
+ Hits 26214 26290 +76
- Misses 24083 24334 +251
Partials 75 75 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
- Removed the unnecessary assignment of the store variable in the theme store test. - This change streamlines the test setup while maintaining the verification of dark mode class application. This update enhances the clarity and efficiency of the test code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
web/__test__/store/theme.test.ts (1)
200-225: Dark‑mode init test looks solid; consider simplifying mock cleanup and commentsThe new test correctly verifies initialization from
--theme-dark-modeand the resultingdarkclasses ondocumentElementandbody. Two small cleanups:
- Since
afterEachalready callsvi.restoreAllMocks(), the explicitvi.restoreAllMocks()inside this test is redundant and can unexpectedly reset other spies (likerequestAnimationFrame) mid‑test. Relying on the suite‑level cleanup keeps the lifecycle clearer.- The inline comments here add little beyond what the code already conveys and slightly conflict with the “no unnecessary comments” guideline; you can safely drop them.
Example minimal version:
- it('should initialize dark mode from CSS variable on store creation', () => { - // Mock getComputedStyle to return dark mode + it('should initialize dark mode from CSS variable on store creation', () => { const originalGetComputedStyle = window.getComputedStyle; vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => { const style = originalGetComputedStyle(el); if (el === document.documentElement) { return { ...style, getPropertyValue: (prop: string) => { if (prop === '--theme-dark-mode') { return '1'; } return style.getPropertyValue(prop); }, } as CSSStyleDeclaration; } return style; }); createStore(); - // Should have added dark class to documentElement expect(document.documentElement.classList.add).toHaveBeenCalledWith('dark'); expect(document.body.classList.add).toHaveBeenCalledWith('dark'); - - vi.restoreAllMocks(); });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
web/__test__/store/theme.test.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with.jsextensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks
Files:
web/__test__/store/theme.test.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use.rejects.toThrow()without arguments to test that functions throw errors, not exact error message strings
Files:
web/__test__/store/theme.test.ts
web/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
Always run
pnpm codegenfor GraphQL code generation in the web directory
Files:
web/__test__/store/theme.test.ts
web/__test__/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.test.{ts,tsx}: Test component behavior and output, not implementation details
Usemountfrom Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Find elements with semantic queries likefind('button')rather than data-test IDs
UsecreatePinia()andsetActivePiniawhen testing Store files, only usecreateTestingPiniaif you specifically need its testing features
Files:
web/__test__/store/theme.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never use theanytype. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start
Files:
web/__test__/store/theme.test.ts
**/*
📄 CodeRabbit inference engine (.cursor/rules/default.mdc)
Never add comments unless they are needed for clarity of function
Files:
web/__test__/store/theme.test.ts
**/*.test.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/*.test.ts: Use.rejects.toThrow()without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Usevi.mock()for module-level mocks
Specify return values for component methods withvi.spyOn()
Reset mocks between tests withvi.clearAllMocks()
Always await async operations before making assertions
Files:
web/__test__/store/theme.test.ts
**/__test__/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/store/**/*.ts: UsecreatePiniaandsetActivePiniawhen testing Pinia store files
UsecreatePinia()instead ofcreateTestingPinia()for most Pinia store tests
Only usecreateTestingPiniaif you specifically need its testing features for Pinia stores
Let stores initialize with their natural default state instead of forcing initial state
Do not mock the store being tested in the test file when usingcreatePinia()
Place all mock declarations at the top level in Pinia store tests
Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Test Pinia action side effects and state changes
Verify Pinia actions are called with correct parameters
Mock external dependencies appropriately in Pinia store tests
Test computed properties in Pinia stores by accessing them directly
Verify state changes after Pinia store actions
Test Pinia store getter dependencies are properly mocked
Test Pinia store interactions with other stores
Verify proper error handling in Pinia store tests
Test async operations completely in Pinia store tests
Override specific Pinia action implementations when needed in tests
Set initial state for focused Pinia store testing
Files:
web/__test__/store/theme.test.ts
**/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
Ensure Vue reactivity imports like
computed,ref, andwatchEffectare added to store files even with Nuxt auto-imports enabled
Files:
web/__test__/store/theme.test.ts
🧠 Learnings (18)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store getter dependencies are properly mocked
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Place all mock declarations at the top level in Pinia store tests
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia action side effects and state changes
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store interactions with other stores
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store being tested in the test file when using `createPinia()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `createPinia()` and `setActivePinia` when testing Store files, only use `createTestingPinia` if you specifically need its testing features
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify state changes after Pinia store actions
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Specify return values for component methods with `vi.spyOn()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Reset mocks between tests with `vi.clearAllMocks()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Use `vi.mock()` for module-level mocks
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify element existence with `expect(element.exists()).toBe(true)`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `createTestingPinia()` for mocking stores in Vue components
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Set initial state for focused Pinia store testing
Applied to files:
web/__test__/store/theme.test.ts
- Updated the WebComponentsExtractor to set the theme name and conditionally apply gradient variables based on banner visibility. - Improved the useTeleport and mount-engine functions to inherit dark mode from the document, ensuring consistency across components. - Refactored the theme store to initialize dark mode based on CSS variables and streamline theme management, including lazy loading of theme queries. These changes improve the user experience by ensuring that the correct theme and dark mode settings are applied consistently throughout the application.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this 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.
ℹ️ 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".
| // Lazy query - only executes when explicitly called | ||
| const { load, onResult, onError } = useLazyQuery<GetThemeQuery>(GET_THEME_QUERY, null, { | ||
| fetchPolicy: 'cache-and-network', | ||
| nextFetchPolicy: 'cache-first', | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trigger theme query after switching to useLazyQuery
useThemeStore now instantiates useLazyQuery for GET_THEME_QUERY, but load()/fetchTheme() are never called anywhere (rg "fetchTheme" web only returns this definition), so the theme query no longer runs on startup. This regresses the previous eager fetch and leaves server-provided theme values (colors, banner flags, etc.) at defaults unless some other code happens to reload the page, meaning users’ configured theme is never applied in a single-page session.
Useful? React with 👍 / 👎.
There was a problem hiding this 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 (4)
unraid-ui/src/composables/useTeleport.ts (1)
12-19: Dark class is applied at container creation but won't update dynamically.The virtual container is a singleton created once. If the theme changes after the container is created (e.g., via dev tools), the dark class won't be updated. This may be acceptable if theme changes always trigger a page refresh in production, but could cause visual inconsistencies during development.
Consider adding a MutationObserver or exposing a method to sync the dark class if dynamic theme switching without refresh is needed.
web/src/store/theme.ts (1)
243-247: Consider extracting DOM initialization to a named function.The inline initialization logic works, but extracting it to a named function (similar to
bootstrapDarkClass) would improve readability and make the initialization sequence clearer.+ const initializeThemeFromDom = () => { + const domThemeName = themeName.value; + if (domThemeName && domThemeName !== DEFAULT_THEME.name) { + theme.value.name = domThemeName; + } + }; + // Initialize dark mode from CSS variable set by PHP or any pre-applied .dark class bootstrapDarkClass(); + initializeThemeFromDom(); - // Initialize theme from DOM on store creation - const domThemeName = themeName.value; - if (domThemeName && domThemeName !== DEFAULT_THEME.name) { - theme.value.name = domThemeName; - }web/__test__/store/theme.test.ts (2)
21-27: MockonResultshould accept and store a callback.The store calls
onResult(callback)to register a result handler. The current mock returnsvi.fn()which accepts any args but doesn't do anything with them. For tests that need to simulate query results, you'll need a more sophisticated mock.If you need to test query result handling in the future:
useLazyQuery: () => { const callbacks: Array<(result: { data: unknown }) => void> = []; return { load: vi.fn(), result: ref(null), loading: ref(false), onResult: vi.fn((cb) => callbacks.push(cb)), onError: vi.fn(), // Helper for tests to trigger result callbacks _triggerResult: (data: unknown) => callbacks.forEach(cb => cb({ data })), }; },
174-196: Test passes for the wrong reason - tests flag check, not CSS variable behavior.This test expects
undefinedwhen the CSS variable is empty, but it actually returnsundefinedbecausetheme.value.bannerandtheme.value.bannerGradientare bothfalseby default. The CSS variable mock is never reached.Consider clarifying the test name or adjusting to actually test the CSS variable fallback behavior.
- it('should return undefined when bannerGradient CSS variable is not set', () => { + it('should return undefined when banner flags are disabled', () => { + const store = createStore(); + // Default theme has banner: false, bannerGradient: false + expect(store.bannerGradient).toBeUndefined(); + }); + + it('should return undefined when CSS variable is not set but banner flags are enabled', () => { const originalGetComputedStyle = window.getComputedStyle; vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => { // ... existing mock that returns empty string }); const store = createStore(); + store.setTheme({ banner: true, bannerGradient: true }); expect(store.bannerGradient).toBeUndefined(); vi.restoreAllMocks(); });
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php(2 hunks)unraid-ui/src/composables/useTeleport.ts(1 hunks)web/__test__/store/theme.test.ts(3 hunks)web/src/components/Wrapper/mount-engine.ts(1 hunks)web/src/store/theme.ts(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with.jsextensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks
Files:
web/src/components/Wrapper/mount-engine.tsunraid-ui/src/composables/useTeleport.tsweb/__test__/store/theme.test.tsweb/src/store/theme.ts
web/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
Always run
pnpm codegenfor GraphQL code generation in the web directory
Files:
web/src/components/Wrapper/mount-engine.tsweb/__test__/store/theme.test.tsweb/src/store/theme.ts
web/src/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)
Files:
web/src/components/Wrapper/mount-engine.tsweb/src/store/theme.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never use theanytype. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start
Files:
web/src/components/Wrapper/mount-engine.tsunraid-ui/src/composables/useTeleport.tsweb/__test__/store/theme.test.tsweb/src/store/theme.ts
**/*
📄 CodeRabbit inference engine (.cursor/rules/default.mdc)
Never add comments unless they are needed for clarity of function
Files:
web/src/components/Wrapper/mount-engine.tsunraid-ui/src/composables/useTeleport.tsweb/__test__/store/theme.test.tsweb/src/store/theme.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use.rejects.toThrow()without arguments to test that functions throw errors, not exact error message strings
Files:
web/__test__/store/theme.test.ts
web/__test__/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.test.{ts,tsx}: Test component behavior and output, not implementation details
Usemountfrom Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Find elements with semantic queries likefind('button')rather than data-test IDs
UsecreatePinia()andsetActivePiniawhen testing Store files, only usecreateTestingPiniaif you specifically need its testing features
Files:
web/__test__/store/theme.test.ts
**/*.test.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/*.test.ts: Use.rejects.toThrow()without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Usevi.mock()for module-level mocks
Specify return values for component methods withvi.spyOn()
Reset mocks between tests withvi.clearAllMocks()
Always await async operations before making assertions
Files:
web/__test__/store/theme.test.ts
**/__test__/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/store/**/*.ts: UsecreatePiniaandsetActivePiniawhen testing Pinia store files
UsecreatePinia()instead ofcreateTestingPinia()for most Pinia store tests
Only usecreateTestingPiniaif you specifically need its testing features for Pinia stores
Let stores initialize with their natural default state instead of forcing initial state
Do not mock the store being tested in the test file when usingcreatePinia()
Place all mock declarations at the top level in Pinia store tests
Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Test Pinia action side effects and state changes
Verify Pinia actions are called with correct parameters
Mock external dependencies appropriately in Pinia store tests
Test computed properties in Pinia stores by accessing them directly
Verify state changes after Pinia store actions
Test Pinia store getter dependencies are properly mocked
Test Pinia store interactions with other stores
Verify proper error handling in Pinia store tests
Test async operations completely in Pinia store tests
Override specific Pinia action implementations when needed in tests
Set initial state for focused Pinia store testing
Files:
web/__test__/store/theme.test.ts
**/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
Ensure Vue reactivity imports like
computed,ref, andwatchEffectare added to store files even with Nuxt auto-imports enabled
Files:
web/__test__/store/theme.test.tsweb/src/store/theme.ts
🧠 Learnings (36)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `mount` from Vue Test Utils for component testing
Applied to files:
web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `mount` from Vue Test Utils for component testing
Applied to files:
web/src/components/Wrapper/mount-engine.ts
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Applied to files:
unraid-ui/src/composables/useTeleport.tsweb/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store getter dependencies are properly mocked
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Place all mock declarations at the top level in Pinia store tests
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Test component behavior and output, not implementation details
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia action side effects and state changes
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `createPinia()` and `setActivePinia` when testing Store files, only use `createTestingPinia` if you specifically need its testing features
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store interactions with other stores
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store being tested in the test file when using `createPinia()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `createTestingPinia()` for mocking stores in Vue components
Applied to files:
web/__test__/store/theme.test.tsweb/src/store/theme.ts
📚 Learning: 2025-02-20T15:52:58.297Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:161-172
Timestamp: 2025-02-20T15:52:58.297Z
Learning: The banner gradient implementation in web/store/theme.ts doesn't require explicit error handling for hexToRgba as CSS gracefully handles invalid values by ignoring them.
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test computed properties in Pinia stores by accessing them directly
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify state changes after Pinia store actions
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-02-20T15:52:56.733Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Specify return values for component methods with `vi.spyOn()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Reset mocks between tests with `vi.clearAllMocks()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Use `vi.mock()` for module-level mocks
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify element existence with `expect(element.exists()).toBe(true)`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Set initial state for focused Pinia store testing
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-02-21T18:40:10.810Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-06-13T17:14:21.739Z
Learnt from: pujitm
Repo: unraid/api PR: 1417
File: web/components/ConnectSettings/ConnectSettings.ce.vue:11-18
Timestamp: 2025-06-13T17:14:21.739Z
Learning: The project’s build tooling auto-imports common Vue/Pinia helpers such as `storeToRefs`, so explicit import statements for them are not required.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Nuxt is set to auto import so some Vue files may need `computed` or `ref` imported explicitly in store files
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to maintain proper isolation and encapsulation.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to function correctly.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/store/**/*.ts : Ensure Vue reactivity imports like `computed`, `ref`, and `watchEffect` are added to store files even with Nuxt auto-imports enabled
Applied to files:
web/src/store/theme.ts
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
Repo: unraid/api PR: 975
File: web/components/Notifications/TabList.vue:1-4
Timestamp: 2024-12-09T15:45:46.492Z
Learning: In our Nuxt.js setup for the `web` project, it's not necessary to explicitly import `computed` from `vue` in Vue components, as it's globally available.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)
Applied to files:
web/src/store/theme.ts
🧬 Code graph analysis (1)
web/__test__/store/theme.test.ts (1)
api/src/store/index.ts (1)
store(10-16)
🪛 GitHub Actions: CI - Main (API)
web/__test__/store/theme.test.ts
[error] 150-170: Assertion failed: expected bannerGradient to have a gradient string, but received undefined in theme store tests.
🪛 GitHub Check: Test API
web/src/store/theme.ts
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > enables gradient automatically when banner is enabled
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when switches change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when color inputs change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when theme selection changes
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > renders all form elements correctly
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
🔇 Additional comments (4)
web/src/store/theme.ts (2)
180-196: Computed correctly guards CSS variable access with theme flags.The logic properly checks
bannerandbannerGradientflags before reading the CSS variable. The pipeline failure in tests is due to tests not setting these flags totruebefore asserting the gradient value.
232-241: Good:immediate: falseresolves the timing issue.The watch no longer triggers immediately on initialization, which prevents the race condition flagged in the past review. The
bootstrapDarkClass()call at line 114 handles the initial dark mode setup separately.web/src/components/Wrapper/mount-engine.ts (1)
182-189: LGTM - Dark mode class propagation is correctly applied to mounted elements.The detection logic correctly checks both the DOM class and CSS variable, consistent with the patterns in
theme.ts. This ensures mounted web components inherit the dark mode styling.web/__test__/store/theme.test.ts (1)
37-38: Variablestoreis correctly used throughout tests.The past review comment about an unused variable appears to be outdated or a false positive. The
storevariable is properly used for cleanup inafterEachand populated viacreateStore().
| const { load, onResult, onError } = useLazyQuery<GetThemeQuery>(GET_THEME_QUERY, null, { | ||
| fetchPolicy: 'cache-and-network', | ||
| nextFetchPolicy: 'cache-first', | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update existing Apollo mocks to include useLazyQuery.
The static analysis shows test failures in ColorSwitcher.test.ts because the Apollo mock doesn't export useLazyQuery. Any test file that mocks @vue/apollo-composable needs to include both useQuery and useLazyQuery.
The affected test file needs to update its mock:
vi.mock('@vue/apollo-composable', () => ({
useQuery: () => ({
result: ref(null),
loading: ref(false),
onResult: vi.fn(),
onError: vi.fn(),
}),
+ useLazyQuery: () => ({
+ load: vi.fn(),
+ result: ref(null),
+ loading: ref(false),
+ onResult: vi.fn(),
+ onError: vi.fn(),
+ }),
}));Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 GitHub Check: Test API
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > enables gradient automatically when banner is enabled
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when switches change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when color inputs change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when theme selection changes
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > renders all form elements correctly
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
🤖 Prompt for AI Agents
In web/src/store/theme.ts around lines 117 to 120 the code uses useLazyQuery
from @vue/apollo-composable but the test mocks for that module only export
useQuery; update the test mocks that stub @vue/apollo-composable (e.g.,
ColorSwitcher.test.ts or the shared test mock file) to export both useQuery and
useLazyQuery with equivalent mock implementations and return shapes (including
load, onResult, onError, and reactive result/error/loading properties) so tests
that import useLazyQuery behave like the real composable.
There was a problem hiding this 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
♻️ Duplicate comments (2)
unraid-ui/src/composables/useTeleport.ts (1)
12-19: Inconsistent dark mode detection across files.This detection logic differs from
web/src/components/Wrapper/mount-engine.ts(lines 184-186), which checks the--theme-dark-modeCSS variable in addition to thedocumentElementclass. The--theme-dark-modevariable is set by the theme store and serves as the source of truth for dark mode state.Not checking the CSS variable here could cause the teleported container to miss dark mode in scenarios where the variable is set but the DOM classes haven't been applied yet.
Update the detection logic to align with mount-engine.ts:
- const isDark = - document.documentElement.classList.contains('dark') || - document.body?.classList.contains('dark') || - Boolean(document.querySelector('.unapi.dark')); + const isDark = + document.documentElement.classList.contains('dark') || + getComputedStyle(document.documentElement).getPropertyValue('--theme-dark-mode').trim() === '1';web/src/store/theme.ts (1)
116-120: Critical: Theme query never executes.The switch to
useLazyQueryrequires an explicit call toload()(exposed asfetchTheme), butfetchThemeis never invoked anywhere in the store initialization or by external code. This means the server theme is never fetched, and users will only see the default white theme regardless of their configured preferences.Call
fetchTheme()after the store initialization completes:// Initialize theme from DOM on store creation const domThemeName = themeName.value; if (domThemeName && domThemeName !== DEFAULT_THEME.name) { theme.value.name = domThemeName; } + + // Fetch server theme to sync with backend + fetchTheme(); return {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php(2 hunks)unraid-ui/src/composables/useTeleport.ts(1 hunks)web/__test__/store/theme.test.ts(3 hunks)web/src/components/Wrapper/mount-engine.ts(1 hunks)web/src/store/theme.ts(5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/web-components-extractor.php
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with.jsextensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks
Files:
web/src/components/Wrapper/mount-engine.tsunraid-ui/src/composables/useTeleport.tsweb/src/store/theme.tsweb/__test__/store/theme.test.ts
web/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
Always run
pnpm codegenfor GraphQL code generation in the web directory
Files:
web/src/components/Wrapper/mount-engine.tsweb/src/store/theme.tsweb/__test__/store/theme.test.ts
web/src/**/*.ts
📄 CodeRabbit inference engine (CLAUDE.md)
Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)
Files:
web/src/components/Wrapper/mount-engine.tsweb/src/store/theme.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never use theanytype. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start
Files:
web/src/components/Wrapper/mount-engine.tsunraid-ui/src/composables/useTeleport.tsweb/src/store/theme.tsweb/__test__/store/theme.test.ts
**/*
📄 CodeRabbit inference engine (.cursor/rules/default.mdc)
Never add comments unless they are needed for clarity of function
Files:
web/src/components/Wrapper/mount-engine.tsunraid-ui/src/composables/useTeleport.tsweb/src/store/theme.tsweb/__test__/store/theme.test.ts
**/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
Ensure Vue reactivity imports like
computed,ref, andwatchEffectare added to store files even with Nuxt auto-imports enabled
Files:
web/src/store/theme.tsweb/__test__/store/theme.test.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use.rejects.toThrow()without arguments to test that functions throw errors, not exact error message strings
Files:
web/__test__/store/theme.test.ts
web/__test__/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.test.{ts,tsx}: Test component behavior and output, not implementation details
Usemountfrom Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Find elements with semantic queries likefind('button')rather than data-test IDs
UsecreatePinia()andsetActivePiniawhen testing Store files, only usecreateTestingPiniaif you specifically need its testing features
Files:
web/__test__/store/theme.test.ts
**/*.test.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/*.test.ts: Use.rejects.toThrow()without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Usevi.mock()for module-level mocks
Specify return values for component methods withvi.spyOn()
Reset mocks between tests withvi.clearAllMocks()
Always await async operations before making assertions
Files:
web/__test__/store/theme.test.ts
**/__test__/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/store/**/*.ts: UsecreatePiniaandsetActivePiniawhen testing Pinia store files
UsecreatePinia()instead ofcreateTestingPinia()for most Pinia store tests
Only usecreateTestingPiniaif you specifically need its testing features for Pinia stores
Let stores initialize with their natural default state instead of forcing initial state
Do not mock the store being tested in the test file when usingcreatePinia()
Place all mock declarations at the top level in Pinia store tests
Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Test Pinia action side effects and state changes
Verify Pinia actions are called with correct parameters
Mock external dependencies appropriately in Pinia store tests
Test computed properties in Pinia stores by accessing them directly
Verify state changes after Pinia store actions
Test Pinia store getter dependencies are properly mocked
Test Pinia store interactions with other stores
Verify proper error handling in Pinia store tests
Test async operations completely in Pinia store tests
Override specific Pinia action implementations when needed in tests
Set initial state for focused Pinia store testing
Files:
web/__test__/store/theme.test.ts
🧠 Learnings (34)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:161-172
Timestamp: 2025-02-20T15:52:58.297Z
Learning: The banner gradient implementation in web/store/theme.ts doesn't require explicit error handling for hexToRgba as CSS gracefully handles invalid values by ignoring them.
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Applied to files:
unraid-ui/src/composables/useTeleport.tsweb/src/store/theme.ts
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-02-21T18:40:10.810Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-06-13T17:14:21.739Z
Learnt from: pujitm
Repo: unraid/api PR: 1417
File: web/components/ConnectSettings/ConnectSettings.ce.vue:11-18
Timestamp: 2025-06-13T17:14:21.739Z
Learning: The project’s build tooling auto-imports common Vue/Pinia helpers such as `storeToRefs`, so explicit import statements for them are not required.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Nuxt is set to auto import so some Vue files may need `computed` or `ref` imported explicitly in store files
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `createTestingPinia()` for mocking stores in Vue components
Applied to files:
web/src/store/theme.tsweb/__test__/store/theme.test.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. The `setActivePinia(createPinia())` call at the module level in store files is intentional and ensures all web components share a single Pinia store instance, which is the desired behavior. This shared state approach is critical for the application's architecture to function correctly.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to maintain proper isolation and encapsulation.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-03-27T23:33:13.215Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:33:13.215Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files would break the build by causing all web components to share a singular Pinia store instance. Each web component needs its own Pinia store instance to function correctly.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/store/**/*.ts : Ensure Vue reactivity imports like `computed`, `ref`, and `watchEffect` are added to store files even with Nuxt auto-imports enabled
Applied to files:
web/src/store/theme.ts
📚 Learning: 2024-12-09T15:45:46.492Z
Learnt from: pujitm
Repo: unraid/api PR: 975
File: web/components/Notifications/TabList.vue:1-4
Timestamp: 2024-12-09T15:45:46.492Z
Learning: In our Nuxt.js setup for the `web` project, it's not necessary to explicitly import `computed` from `vue` in Vue components, as it's globally available.
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)
Applied to files:
web/src/store/theme.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store getter dependencies are properly mocked
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Place all mock declarations at the top level in Pinia store tests
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store being tested in the test file when using `createPinia()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in Vue component tests
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store interactions with other stores
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia action side effects and state changes
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Test component behavior and output, not implementation details
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-02-20T15:52:58.297Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:161-172
Timestamp: 2025-02-20T15:52:58.297Z
Learning: The banner gradient implementation in web/store/theme.ts doesn't require explicit error handling for hexToRgba as CSS gracefully handles invalid values by ignoring them.
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test computed properties in Pinia stores by accessing them directly
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify state changes after Pinia store actions
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-02-20T15:52:56.733Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Specify return values for component methods with `vi.spyOn()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Reset mocks between tests with `vi.clearAllMocks()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Use `vi.mock()` for module-level mocks
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify element existence with `expect(element.exists()).toBe(true)`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Set initial state for focused Pinia store testing
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components
Applied to files:
web/__test__/store/theme.test.ts
🪛 GitHub Check: Test API
web/src/store/theme.ts
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > enables gradient automatically when banner is enabled
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when switches change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when color inputs change
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > updates theme store when theme selection changes
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
[failure] 117-117: test/components/ColorSwitcher.test.ts > ColorSwitcher > renders all form elements correctly
Error: [vitest] No "useLazyQuery" export is defined on the "@vue/apollo-composable" mock. Did you forget to return it from "vi.mock"?
If you need to partially mock a module, you can use "importOriginal" helper inside:
vi.mock(import("@vue/apollo-composable"), async (importOriginal) => {
const actual = await importOriginal()
return {
...actual,
// your mocked methods
}
})
❯ src/store/theme.ts:117:39
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:98
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:88
❯ EffectScope.run ../node_modules/.pnpm/@VUE[email protected]/node_modules/@vue/reactivity/dist/reactivity.cjs.js:85:16
❯ ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:54
❯ fallbackRunWithContext ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1156:40
❯ createSetupStore ../node_modules/.pnpm/[email protected][email protected][email protected][email protected]_/node_modules/pinia/dist/pinia.mjs:1464:24
🔇 Additional comments (7)
web/src/store/theme.ts (6)
41-85: Well-structured helper functions.The DOM availability checks and CSS variable reading helpers are well-organized. The
syncDarkClassfunction correctly propagates the dark class todocumentElement,body, and all.unapielements, ensuring consistency across the application.
113-114: Good initialization pattern.Calling
bootstrapDarkClass()during store initialization ensures that dark mode is properly applied based on CSS variables or existing DOM state when the store is created.
158-168: CSS-variable-driven getters align with PR objectives.The
themeNameanddarkModecomputed properties correctly read from CSS variables set by PHP (--theme-nameand--theme-dark-mode), with appropriate fallbacks. This ensures consistent theme state between server-rendered HTML and client-side JavaScript.
180-196: Banner gradient logic correctly respects dark mode.The implementation correctly returns
undefinedwhendarkModeis active, aligning with the learned behavior that banners should not show gradients in dark mode. The CSS variable reading includes proper normalization to handle empty, null, or undefined values.Based on learnings, the header design intentionally differs between light and dark modes.
234-241: Watch withimmediate: falseprevents initialization conflict.Setting
immediate: falsecorrectly prevents the watcher from overriding the dark class applied bybootstrapDarkClass()during initialization. The watcher now only applies the dark class when the theme name actually changes (e.g., via dev tools), which is the intended behavior per the comment.
254-257: Computed theme wrapper ensures CSS variable takes precedence.Returning a computed object that overrides
namewiththemeName.valueensures the theme name always reflects the CSS variable set by PHP, even if the internalthemeref has a different value. This maintains the CSS variable as the source of truth for theme name.web/__test__/store/theme.test.ts (1)
100-294: Comprehensive test coverage for CSS-variable-driven theme.The new tests effectively validate the CSS-variable-driven theme initialization and computed properties:
- Dark mode detection from
--theme-dark-mode(both '1' and '0' values)- Banner gradient reading from
--banner-gradient- Proper handling of missing/empty CSS variables
- Bootstrap dark class application on store creation
The mocking strategy using
getComputedStyleis appropriate and aligns with the implementation.
| useLazyQuery: () => ({ | ||
| load: vi.fn(), | ||
| result: ref(null), | ||
| loading: ref(false), | ||
| onResult: vi.fn(), | ||
| onError: vi.fn(), | ||
| }), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update other test files to include useLazyQuery mock.
The useLazyQuery mock added here is correct, but static analysis reveals that other test files (e.g., __test__/components/ColorSwitcher.test.ts) are failing because their @vue/apollo-composable mocks don't export useLazyQuery.
Update all test files that mock @vue/apollo-composable to include useLazyQuery. Run the following script to identify files that need updating:
#!/bin/bash
# Find all test files that mock @vue/apollo-composable but don't include useLazyQuery
rg -l "vi\.mock.*@vue/apollo-composable" web/__test__ | while read -r file; do
if ! grep -q "useLazyQuery" "$file"; then
echo "Missing useLazyQuery mock: $file"
fi
doneFor each file found, add useLazyQuery to the mock:
vi.mock('@vue/apollo-composable', () => ({
useQuery: () => ({
result: ref(null),
loading: ref(false),
onResult: vi.fn(),
onError: vi.fn(),
}),
+ useLazyQuery: () => ({
+ load: vi.fn(),
+ result: ref(null),
+ loading: ref(false),
+ onResult: vi.fn(),
+ onError: vi.fn(),
+ }),
}));🤖 Prompt for AI Agents
In web/__test__/store/theme.test.ts around lines 21 to 27 the mock for
@vue/apollo-composable includes useLazyQuery but other test files that mock
@vue/apollo-composable are missing it; run the provided grep script to locate
all such test files, and for each file add a useLazyQuery export to the mock
that matches the shape used here (return an object with load: vi.fn(), result:
ref(null), loading: ref(false), onResult: vi.fn(), onError: vi.fn()); update
imports if needed and run tests to confirm no missing-export errors remain.
| // Apply dark mode class if active | ||
| // Check both documentElement class and CSS variable (set by PHP) | ||
| const isDarkMode = | ||
| document.documentElement.classList.contains('dark') || | ||
| getComputedStyle(document.documentElement).getPropertyValue('--theme-dark-mode').trim() === '1'; | ||
| if (isDarkMode) { | ||
| element.classList.add('dark'); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent dark mode detection across files.
This implementation checks documentElement.classList and the CSS variable --theme-dark-mode, but unraid-ui/src/composables/useTeleport.ts (lines 13-16) uses a different detection strategy: it checks documentElement.classList, body.classList, and the presence of .unapi.dark elements, but does not check the --theme-dark-mode CSS variable.
This inconsistency could result in the teleported container and mounted elements having different dark mode states under certain conditions.
Consider consolidating the detection logic into a shared utility function that both files can use. For example:
// helpers/dark-mode.ts
export const isDarkModeActive = (): boolean => {
if (typeof document === 'undefined') return false;
return (
document.documentElement.classList.contains('dark') ||
getComputedStyle(document.documentElement).getPropertyValue('--theme-dark-mode').trim() === '1'
);
};Then use it in both files:
+import { isDarkModeActive } from '~/helpers/dark-mode';
- const isDarkMode =
- document.documentElement.classList.contains('dark') ||
- getComputedStyle(document.documentElement).getPropertyValue('--theme-dark-mode').trim() === '1';
- if (isDarkMode) {
+ if (isDarkModeActive()) {
element.classList.add('dark');
}- Updated the WebComponentsExtractor to utilize a CSS variable for the banner gradient stop, allowing for responsive adjustments. - Modified theme store tests to reflect the new gradient format, ensuring consistency in gradient rendering. - Added CSS rules to define the banner gradient stop, with a media query for different screen sizes. These changes improve the flexibility and responsiveness of the banner gradient in the application.
- Introduced a new test to verify the correct application of the banner gradient when the theme is set. - This enhancement ensures that the theme store's functionality for banner gradient customization is properly validated. These changes improve the robustness of the theme store tests.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
1 similar comment
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
- Refactored UserProfile tests to replace direct theme property manipulations with the setTheme method, ensuring consistency in theme state management. - Mocked getComputedStyle to simulate CSS variables for banner gradient and dark mode, enhancing test reliability. - Improved test coverage for theme-related functionality, particularly around banner visibility and description rendering. These changes enhance the robustness and maintainability of the UserProfile component tests.
- Updated ColorSwitcher and UserProfile tests to set CSS variables directly on the document element, improving test reliability and consistency. - Refactored cleanup logic in theme store tests to handle unmounting more gracefully, ensuring robust test teardown. These changes enhance the maintainability and clarity of the theme-related tests.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
web/__test__/store/theme.test.ts (1)
106-128: Consider extracting repeatedgetComputedStylemock to a helper.The mock pattern for
getComputedStyleis repeated across multiple tests with slight variations. A helper function could reduce duplication:const mockCssVariables = (variables: Record<string, string>) => { const originalGetComputedStyle = window.getComputedStyle; vi.spyOn(window, 'getComputedStyle').mockImplementation((el) => { const style = originalGetComputedStyle(el); if (el === document.documentElement) { return { ...style, getPropertyValue: (prop: string) => variables[prop] ?? style.getPropertyValue(prop), } as CSSStyleDeclaration; } return style; }); };Usage:
mockCssVariables({ '--theme-dark-mode': '1' })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
web/__test__/components/ColorSwitcher.test.ts(3 hunks)web/__test__/components/UserProfile.test.ts(3 hunks)web/__test__/store/theme.test.ts(4 hunks)
🧰 Additional context used
📓 Path-based instructions (10)
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with.jsextensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks
Files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
**/*.test.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.test.{ts,tsx,js,jsx}: Use VITEST for test suite, not jest
Use.rejects.toThrow()without arguments to test that functions throw errors, not exact error message strings
Files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
web/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
Always run
pnpm codegenfor GraphQL code generation in the web directory
Files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
web/__test__/**/*.test.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
web/__test__/**/*.test.{ts,tsx}: Test component behavior and output, not implementation details
Usemountfrom Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Find elements with semantic queries likefind('button')rather than data-test IDs
UsecreatePinia()andsetActivePiniawhen testing Store files, only usecreateTestingPiniaif you specifically need its testing features
Files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never use theanytype. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start
Files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
**/*
📄 CodeRabbit inference engine (.cursor/rules/default.mdc)
Never add comments unless they are needed for clarity of function
Files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
**/*.test.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/*.test.ts: Use.rejects.toThrow()without arguments to test that functions throw errors. Don't test exact error message strings unless the message format is specifically what you're testing
Test what the code does, not implementation details like exact error message wording
Mock external services and API calls
Usevi.mock()for module-level mocks
Specify return values for component methods withvi.spyOn()
Reset mocks between tests withvi.clearAllMocks()
Always await async operations before making assertions
Files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
**/__test__/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/store/**/*.ts: UsecreatePiniaandsetActivePiniawhen testing Pinia store files
UsecreatePinia()instead ofcreateTestingPinia()for most Pinia store tests
Only usecreateTestingPiniaif you specifically need its testing features for Pinia stores
Let stores initialize with their natural default state instead of forcing initial state
Do not mock the store being tested in the test file when usingcreatePinia()
Place all mock declarations at the top level in Pinia store tests
Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Test Pinia action side effects and state changes
Verify Pinia actions are called with correct parameters
Mock external dependencies appropriately in Pinia store tests
Test computed properties in Pinia stores by accessing them directly
Verify state changes after Pinia store actions
Test Pinia store getter dependencies are properly mocked
Test Pinia store interactions with other stores
Verify proper error handling in Pinia store tests
Test async operations completely in Pinia store tests
Override specific Pinia action implementations when needed in tests
Set initial state for focused Pinia store testing
Files:
web/__test__/store/theme.test.ts
**/store/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
Ensure Vue reactivity imports like
computed,ref, andwatchEffectare added to store files even with Nuxt auto-imports enabled
Files:
web/__test__/store/theme.test.ts
**/__test__/components/**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/web-testing-rules.mdc)
**/__test__/components/**/*.ts: Usemountfrom Vue Test Utils for component testing
Stub complex child components that aren't the focus of the test
Mock external dependencies and services in Vue component tests
Test component behavior and output, not implementation details
Verify that the expected elements are rendered in Vue components
Test component interactions such as clicks and inputs
Check for expected prop handling and event emissions in Vue components
UsecreateTestingPinia()for mocking stores in Vue components
Use semantic queries likefind('button')orfind('[data-test="id"]')but prefer not to use data test ID's when finding elements
Find Vue components withfindComponent(ComponentName)
UsefindAllto check for multiple elements in Vue components
Assert on rendered text content withwrapper.text()
Assert on element attributes withelement.attributes()
Verify element existence withexpect(element.exists()).toBe(true)
Check component state through rendered output
Trigger events withawait element.trigger('click')
Set input values withawait input.setValue('value')
Test emitted events withwrapper.emitted()
Useawait nextTick()for DOM updates in Vue component tests
UseflushPromises()for complex promise chains in Vue component tests
Files:
web/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
🧠 Learnings (37)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Mock external dependencies appropriately in Pinia store tests
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use factory functions for module mocks in Pinia store tests to avoid hoisting issues
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store getter dependencies are properly mocked
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Place all mock declarations at the top level in Pinia store tests
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Do not mock the store being tested in the test file when using `createPinia()`
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia store interactions with other stores
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `createPinia()` and `setActivePinia` when testing Store files, only use `createTestingPinia` if you specifically need its testing features
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Mock external dependencies and services in Vue component tests
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test Pinia action side effects and state changes
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-02-20T15:52:58.297Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:161-172
Timestamp: 2025-02-20T15:52:58.297Z
Learning: The banner gradient implementation in web/store/theme.ts doesn't require explicit error handling for hexToRgba as CSS gracefully handles invalid values by ignoring them.
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify proper error handling in Pinia store tests
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Test computed properties in Pinia stores by accessing them directly
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Verify state changes after Pinia store actions
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `await nextTick()` for DOM updates in Vue component tests
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Use `vi.mock()` for module-level mocks
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `createTestingPinia()` for mocking stores in Vue components
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check for expected prop handling and event emissions in Vue components
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify that the expected elements are rendered in Vue components
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Test component behavior and output, not implementation details
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-02-20T15:52:56.733Z
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Specify return values for component methods with `vi.spyOn()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/*.test.ts : Reset mocks between tests with `vi.clearAllMocks()`
Applied to files:
web/__test__/store/theme.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Verify element existence with `expect(element.exists()).toBe(true)`
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Set initial state for focused Pinia store testing
Applied to files:
web/__test__/store/theme.test.tsweb/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use `createPinia` and `setActivePinia` when testing Pinia store files
Applied to files:
web/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Use `createPinia()` instead of `createTestingPinia()` for most Pinia store tests
Applied to files:
web/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Override specific Pinia action implementations when needed in tests
Applied to files:
web/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Assert on rendered text content with `wrapper.text()`
Applied to files:
web/__test__/components/UserProfile.test.tsweb/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Check component state through rendered output
Applied to files:
web/__test__/components/UserProfile.test.ts
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/__test__/**/*.test.{ts,tsx} : Use `mount` from Vue Test Utils for component testing
Applied to files:
web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Use `mount` from Vue Test Utils for component testing
Applied to files:
web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component interactions such as clicks and inputs
Applied to files:
web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-08-09T01:03:29.676Z
Learnt from: elibosley
Repo: unraid/api PR: 1575
File: packages/unraid-shared/src/services/socket-config.service.spec.ts:10-13
Timestamp: 2025-08-09T01:03:29.676Z
Learning: Vitest is used for all testing across all repositories in the unraid organization, not Jest. Always use `vi` for mocking utilities, not `jest`.
Applied to files:
web/__test__/components/ColorSwitcher.test.ts
📚 Learning: 2025-11-24T17:52:26.908Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.908Z
Learning: Applies to **/__test__/store/**/*.ts : Only use `createTestingPinia` if you specifically need its testing features for Pinia stores
Applied to files:
web/__test__/components/ColorSwitcher.test.ts
🧬 Code graph analysis (2)
web/__test__/store/theme.test.ts (1)
api/src/store/index.ts (1)
store(10-16)
web/__test__/components/UserProfile.test.ts (1)
web/src/store/theme.ts (1)
useThemeStore(105-263)
🔇 Additional comments (12)
web/__test__/components/UserProfile.test.ts (3)
191-210: LGTM! Well-structured theme initialization.The CSS variable setup and
setThemeinitialization properly align with the new CSS-variable-driven theme resolution. This approach ensures tests reflect production behavior where theme state is derived from CSS variables set by PHP.
336-357: Clean refactor to use public API.The spread pattern
{ ...themeStore.theme, descriptionShow: ... }correctly preserves existing theme state while updating specific properties, replacing direct state mutations with propersetThemecalls.
368-398: LGTM!Banner tests correctly use
setThemeto toggle flags and verifybannerGradientcomputed behavior with CSS variables.web/__test__/components/ColorSwitcher.test.ts (3)
25-31: Good addition ofuseLazyQuerymock.This addresses the missing mock issue flagged in past reviews. The mock shape correctly mirrors the
useLazyQueryreturn type withload,result,loading,onResult, andonError.
64-67: CSS variable setup enables proper theme store initialization.Setting
--theme-dark-modeand--banner-gradientbefore creating the store ensures the CSS-variable-driven computed properties initialize correctly during tests.
84-89: Improved cleanup robustness.The guards prevent potential errors during teardown when
modalDivis already removed orconsoleWarnSpywasn't created.web/__test__/store/theme.test.ts (6)
21-27:useLazyQuerymock added correctly.This aligns with the mock added in
ColorSwitcher.test.tsand addresses the past review comment about consistency across test files.
65-71: Defensive cleanup pattern.The try-catch handles the case where the app was never mounted, preventing test failures during teardown.
106-152: Thorough CSS variable-based darkMode tests.These tests properly verify the
darkModecomputed property reads from--theme-dark-modeCSS variable and returns the correct boolean value.
154-186: Good test for bannerGradient with CSS variable.The test correctly sets
banner: trueandbannerGradient: trueviasetThemebefore asserting the computed value, addressing the issue flagged in past reviews.
188-210: Correctly tests undefined gradient when CSS variable is empty.This validates the edge case where
--banner-gradientis not set or empty.
279-308: LGTM! Validates dark mode initialization from CSS variable.This test confirms that
bootstrapDarkClasscorrectly applies the dark class todocumentElementandbodywhen the CSS variable--theme-dark-modeis set to'1'at store creation time.
- Introduced CustomizationMutations to handle theme updates via GraphQL. - Implemented setTheme mutation to update the UI theme and persist changes in the configuration. - Updated the generated GraphQL schema to include the new mutations and theme enum. - Enhanced the CustomizationService to manage theme persistence and in-memory updates. - Updated the frontend to support theme selection and synchronization with the server. These changes improve the user experience by allowing dynamic theme management and ensuring consistent application of user preferences.
|
🚀 Storybook has been deployed to staging: https://unraid-ui-storybook-staging.unraid-workers.workers.dev |
|
This plugin has been deployed to Cloudflare R2 and is available for testing. |
There was a problem hiding this 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 (8)
web/src/components/DevThemeSwitcher.standalone.vue (3)
66-77: Remove unnecessaryasynckeyword.This function doesn't contain any
awaitexpressions, so theasyncmodifier is unnecessary overhead.-const resolveInitialTheme = async (): Promise<ThemeName> => { +const resolveInitialTheme = (): ThemeName => { const candidates = [readCssTheme(), readCookieTheme(), readLocalStorageTheme(), theme.value?.name];If this is intentionally async for future extensibility, consider adding a comment to clarify.
96-102: Redundant watch for query loading state.The
queryLoadingref fromuseQuerycan be used directly instead of copying it to another ref via a watcher.Remove the redundant watch and
isQueryLoadingref:-const isQueryLoading = ref(false); ... -watch( - () => queryLoading.value, - (loading) => { - isQueryLoading.value = loading; - }, - { immediate: true } -);Then in the template, use
queryLoadingdirectly:- :disabled="isSaving || isQueryLoading" + :disabled="isSaving || queryLoading"
191-216: Clarify thepersistparameter semantics.The
persistparameter name is misleading—local persistence to cookie/localStorage always happens (line 198), whilepersistonly controls the GraphQL server mutation.Consider renaming for clarity:
const applyThemeSelection = async ( themeName: string | null | undefined, - { persist = false, skipStore = false }: { persist?: boolean; skipStore?: boolean } = {} + { persistToServer = false, skipStore = false }: { persistToServer?: boolean; skipStore?: boolean } = {} ) => { // ... - if (persist) { + if (persistToServer) {web/test-pages/partials/styles.njk (1)
2-5: Dev theme bootstrap matches CSS-var/dark-mode flow; confirm azure dark-mode mappingThe dev-only logic looks good overall:
activeThemeprecedence (resolvedTheme→query.theme→'white') and the explicit allowlist guard prevent invalid theme names from leaking into CSS vars.- The script keeps the
<link id="dev-theme-css-link">href in sync with the normalized theme.--theme-name,--theme-dark-mode(as'1'/'0'), anddata-themeondocumentElementalign with how the theme store expects to bootstrap state. Based on learnings, this should integrate cleanly with existing CSS-variable-driven theming.syncDarkClassupdatinghtml,body, and.unapimirrors how the app propagates thedarkclass to containers.Two small follow-ups:
isDarkonly treats'black'and'gray'as dark:var isDark = theme === 'black' || theme === 'gray';Please double-check whether
azureshould also be considered dark for your design system. If yes, add it here so--theme-dark-modeand thedarkclass stay consistent.The theme allowlist,
themeCssMapkeys, andisDarkpredicate all repeat the same set of theme names. If you find yourself tweaking themes often, consider centralizing this into a single map/object and deriving the allowlist andisDarkfrom it to avoid drift.Also applies to: 26-66
web/vite-plugin-serve-static.ts (4)
10-39: TightennormalizeThemetyping to avoid unnecessary castThe runtime logic is fine, but casting
(theme ?? '').toLowerCase()directly toThemeNameis a bit loose for TS and goes against the “exact theme types, minimal casting” pattern used elsewhere in the theme system (see previous learnings). You can keep the same behavior while letting TypeScript infer the union fromALLOWED_THEMES:-const normalizeTheme = (theme?: string | null): ThemeName => { - const normalized = (theme ?? '').toLowerCase() as ThemeName; - return ALLOWED_THEMES.includes(normalized) ? normalized : 'white'; -}; +const normalizeTheme = (theme?: string | null): ThemeName => { + const normalized = (theme ?? '').toLowerCase(); + return ALLOWED_THEMES.find((t) => t === normalized) ?? 'white'; +};This preserves guarantees that only whitelisted values flow out, without relying on a broad cast. Based on learnings, this matches the preference for precise theme typings.
40-58: HardenparseCookiesagainst malformed encodingsThe implementation is solid for normal cookies, but
decodeURIComponentwill throw if a cookie value contains invalid percent-encoding, which would currently take down the dev request.If you want this helper to be more defensive (even in dev), consider wrapping the decode in a try/catch and skipping values that can’t be decoded:
- acc[trimmedName] = decodeURIComponent(rest.join('=').trim()); + const rawValue = rest.join('=').trim(); + try { + acc[trimmedName] = decodeURIComponent(rawValue); + } catch { + // Ignore malformed cookie values + }That way a single bad cookie header doesn’t break
/test-pagesrendering.
70-109: INI parsing and theme read behavior are minimal but adequate
parseIniSectionis intentionally minimal (single-linekey=value, no multi-line or advanced INI features) andreadThemeFromConfiggracefully falls back tonullon any error, which is appropriate for a dev helper.One behavioral note: if
[display]exists but has notheme,normalizeTheme(displaySection.theme)effectively defaults to'white'. That’s probably fine for dev, but it does mean a misconfigured file silently falls back rather than surfacing an error. If you ever want stricter behavior, you could explicitly check forObject.hasOwn(displaySection, 'theme')before normalizing.
307-318: Theme resolution precedence is clear; confirm it matches intended UXThe resolution chain:
const cookies = parseCookies(req.headers.cookie); const cookieTheme = cookies['unraid_dev_theme']; const queryTheme = requestUrl.searchParams.get('theme'); const cfgTheme = readThemeFromConfig(); const resolvedTheme = normalizeTheme(cfgTheme || queryTheme || cookieTheme);gives precedence
config > query param > cookie, then defaults vianormalizeTheme. That’s a reasonable choice for dev, but it does mean a remembered config will override ad‑hoc query testing unless config is cleared/changed.If that’s the intended behavior for local testing, this looks good. If you expect query params to always win during manual testing, you may want to reorder to
query > cookie > config.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (11)
api/dev/configs/api.json(1 hunks)api/generated-schema.graphql(2 hunks)api/src/unraid-api/graph/resolvers/customization/customization.module.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts(1 hunks)api/src/unraid-api/graph/resolvers/customization/customization.service.ts(2 hunks)api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts(2 hunks)api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts(2 hunks)web/.prettierignore(1 hunks)web/src/components/DevThemeSwitcher.standalone.vue(3 hunks)web/test-pages/partials/styles.njk(2 hunks)web/vite-plugin-serve-static.ts(4 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
api/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
Prefer adding new files to the NestJS repo located at
api/src/unraid-api/instead of the legacy code
Files:
api/dev/configs/api.jsonapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.service.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsapi/generated-schema.graphql
**/*
📄 CodeRabbit inference engine (.cursor/rules/default.mdc)
Never add comments unless they are needed for clarity of function
Files:
api/dev/configs/api.jsonapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.service.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsweb/src/components/DevThemeSwitcher.standalone.vueweb/vite-plugin-serve-static.tsweb/test-pages/partials/styles.njkapi/generated-schema.graphql
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: Always use TypeScript imports with.jsextensions for ESM compatibility
Never add comments unless they are needed for clarity of function
Never add comments for obvious things, and avoid commenting when starting and ending code blocks
Files:
api/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.service.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsweb/vite-plugin-serve-static.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: Never use theanytype. Always prefer proper typing
Avoid using casting whenever possible, prefer proper typing from the start
Files:
api/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.service.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.tsweb/vite-plugin-serve-static.ts
api/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
cache-manager v7 expects TTL values in milliseconds, not seconds (e.g., 600000 for 10 minutes, not 600)
Files:
api/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.service.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.tsapi/src/unraid-api/graph/resolvers/customization/customization.module.ts
web/**/*
📄 CodeRabbit inference engine (CLAUDE.md)
Always run
pnpm codegenfor GraphQL code generation in the web directory
Files:
web/src/components/DevThemeSwitcher.standalone.vueweb/vite-plugin-serve-static.tsweb/test-pages/partials/styles.njk
🧠 Learnings (18)
📓 Common learnings
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Learnt from: elibosley
Repo: unraid/api PR: 1155
File: web/store/theme.ts:49-50
Timestamp: 2025-02-20T15:52:56.733Z
Learning: CSS variable names in the theme store should be concise and follow established patterns. For example, prefer '--gradient-start' over '--color-customgradient-start' to maintain consistency with other variable names.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/src/**/*.ts : Ensure Vue reactivity imports are added to store files (computed, ref, watchEffect)
Applied to files:
web/.prettierignore
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/**/* : Always run `pnpm codegen` for GraphQL code generation in the web directory
Applied to files:
web/.prettierignore
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/**/*.query.ts : GraphQL queries must be placed in `.query.ts` files
Applied to files:
web/.prettierignore
📚 Learning: 2025-05-23T21:59:29.632Z
Learnt from: elibosley
Repo: unraid/api PR: 1408
File: web/components/ApiKey/PermissionCounter.vue:6-6
Timestamp: 2025-05-23T21:59:29.632Z
Learning: This codebase uses ESM (ECMAScript Modules) and requires .js extensions in import statements, even when importing from TypeScript files, as the imports refer to the compiled JavaScript output.
Applied to files:
web/.prettierignore
📚 Learning: 2025-11-24T17:52:00.572Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-graphql.mdc:0-0
Timestamp: 2025-11-24T17:52:00.572Z
Learning: Applies to web/**/*.{query.ts,mutation.ts} : All GraphQL under `web/` must follow the `.query.ts` and `.mutation.ts` naming convention
Applied to files:
web/.prettierignore
📚 Learning: 2025-11-24T17:51:37.915Z
Learnt from: CR
Repo: unraid/api PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-24T17:51:37.915Z
Learning: Applies to web/**/*.mutation.ts : GraphQL mutations must be placed in `.mutation.ts` files
Applied to files:
web/.prettierignoreapi/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2025-03-12T13:48:14.850Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: web/composables/gql/gql.ts:17-18
Timestamp: 2025-03-12T13:48:14.850Z
Learning: In the Unraid API project, the duplicate GraphQL query and mutation strings in gql.ts files are intentionally generated by GraphQL CodeGen tool and are necessary for the type system to function properly.
Applied to files:
web/.prettierignoreapi/src/unraid-api/graph/resolvers/mutation/mutation.model.ts
📚 Learning: 2025-11-24T17:52:26.907Z
Learnt from: CR
Repo: unraid/api PR: 0
File: .cursor/rules/web-testing-rules.mdc:0-0
Timestamp: 2025-11-24T17:52:26.907Z
Learning: Applies to **/__test__/components/**/*.ts : Test component behavior and output, not implementation details
Applied to files:
web/.prettierignore
📚 Learning: 2025-03-14T16:10:38.562Z
Learnt from: pujitm
Repo: unraid/api PR: 1211
File: api/src/graphql/schema/types/connect/connect.graphql:142-146
Timestamp: 2025-03-14T16:10:38.562Z
Learning: The updateApiSettings mutation in api/src/unraid-api/graph/connect/connect.resolver.ts is protected with the UsePermissions decorator that requires UPDATE permission on the CONFIG resource.
Applied to files:
api/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2024-11-06T20:59:25.809Z
Learnt from: mdatelle
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:111-113
Timestamp: 2024-11-06T20:59:25.809Z
Learning: In the Unraid API project, error handling for mutations is handled at the service level rather than in the GraphQL resolvers. Specifically, in `api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts`, methods like `removeRoleFromApiKey` rely on service-level error handling.
Applied to files:
api/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2024-11-05T14:48:56.949Z
Learnt from: elibosley
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:41-47
Timestamp: 2024-11-05T14:48:56.949Z
Learning: Input types for GraphQL mutations can be pulled from the GraphQL codegen types, as they are generated into the API files. Therefore, explicit typing in the resolver files may be unnecessary.
Applied to files:
api/src/unraid-api/graph/resolvers/mutation/mutation.model.tsapi/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.tsapi/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts
📚 Learning: 2024-11-06T20:59:06.096Z
Learnt from: mdatelle
Repo: unraid/api PR: 942
File: api/src/unraid-api/graph/resolvers/auth/auth.resolver.ts:87-89
Timestamp: 2024-11-06T20:59:06.096Z
Learning: Error handling in mutations like `addRoleForUser` in `auth.resolver.ts` is performed at the service level, so additional error handling in the resolver is unnecessary.
Applied to files:
api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts
📚 Learning: 2024-12-17T14:59:32.458Z
Learnt from: elibosley
Repo: unraid/api PR: 972
File: web/store/theme.ts:46-49
Timestamp: 2024-12-17T14:59:32.458Z
Learning: In the `web/store/theme.ts` file of the Unraid web application, the header is intentionally designed to have a light background with dark text in dark mode, and a dark background with light text in light mode.
Applied to files:
api/src/unraid-api/graph/resolvers/customization/customization.service.tsweb/src/components/DevThemeSwitcher.standalone.vueweb/vite-plugin-serve-static.tsweb/test-pages/partials/styles.njk
📚 Learning: 2025-04-23T20:19:42.542Z
Learnt from: pujitm
Repo: unraid/api PR: 1367
File: packages/unraid-api-plugin-connect/src/pubsub/user.service.ts:44-52
Timestamp: 2025-04-23T20:19:42.542Z
Learning: The project uses a custom or extended implementation of NestJS ConfigService that includes a `set()` method for runtime configuration mutation, unlike the standard nestjs/config package which only provides getter methods.
Applied to files:
api/src/unraid-api/graph/resolvers/customization/customization.module.ts
📚 Learning: 2025-02-24T14:51:21.328Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:0-0
Timestamp: 2025-02-24T14:51:21.328Z
Learning: In the Unraid API project's theme system, exact TypeScript type definitions are preferred over index signatures for theme variables to ensure better type safety.
Applied to files:
web/src/components/DevThemeSwitcher.standalone.vueweb/vite-plugin-serve-static.ts
📚 Learning: 2025-03-27T23:52:57.888Z
Learnt from: zackspear
Repo: unraid/api PR: 0
File: :0-0
Timestamp: 2025-03-27T23:52:57.888Z
Learning: In the unraid/api project, Vue components are compiled into web components. Using `setActivePinia(createPinia())` in store files ensures that all web components share a single Pinia store instance, which is the desired behavior. Without this initialization, each web component would have its own isolated store, breaking the intended architecture.
Applied to files:
web/src/components/DevThemeSwitcher.standalone.vue
📚 Learning: 2025-02-21T18:40:10.810Z
Learnt from: elibosley
Repo: unraid/api PR: 1181
File: web/store/theme.ts:210-216
Timestamp: 2025-02-21T18:40:10.810Z
Learning: When updating theme-related CSS variables via `cssText`, preserve existing non-theme styles by filtering out only theme-related rules (those starting with '--') and combining them with the new theme styles.
Applied to files:
web/test-pages/partials/styles.njk
🧬 Code graph analysis (2)
api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts (3)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
Resolver(14-50)packages/unraid-shared/src/use-permissions.directive.ts (1)
UsePermissions(81-128)api/src/unraid-api/graph/resolvers/customization/customization.resolver.ts (1)
theme(58-60)
api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
web/composables/gql/graphql.ts (1)
Mutation(1199-1239)
🔇 Additional comments (19)
web/.prettierignore (1)
5-5: Addingdist/to Prettier ignore list is sensible.The change appropriately excludes the compiled/build output directory from Prettier formatting, which is a best practice to prevent formatting of generated files. The entry follows the existing naming convention with other directory-based ignores in the file.
web/src/components/DevThemeSwitcher.standalone.vue (7)
1-27: LGTM!Good use of the generated
ThemeNameenum for type-safe theme options, aligning with the project's preference for exact TypeScript type definitions. Based on learnings, this approach ensures better type safety.
34-64: LGTM!The helper functions are SSR-safe with appropriate guards for
documentandwindow. Cookie parsing correctly handles values containing=by usingrest.join('=').
142-150: LGTM!Cookie persistence with reasonable expiration and
SameSite=Lax. The omission of theSecureflag is acceptable for a dev-only component that may run on localhost.
152-163: LGTM!The CSS variable names
--theme-nameand--theme-dark-modefollow the concise naming pattern as per established conventions. The dark class synchronization across root, body, and.unapielements ensures consistent theming. Based on learnings, this aligns with the project's approach to CSS variable naming.
227-244: LGTM!The lifecycle management correctly:
- Sets dev override on mount before applying theme
- Watches store changes with
skipStore: trueto prevent circular updates- Separates user-initiated changes (with server persistence) from external sync
247-286: LGTM!Template correctly binds to
currentThemeand disables the select during saving/loading operations. The scoped styles appropriately handle the disabled state.
29-32: No case mismatch exists — review comment is incorrect.The
ThemeNameenum values are lowercase strings ('azure','black','gray','white'), not uppercase. On line 27,themeValuesis populated fromthemeOptions.map((option) => option.value), where eachoption.valueis aThemeNameenum member with a lowercase string value. ThenormalizeThemefunction correctly converts input to lowercase and checks membership againstthemeValues, which contains lowercase strings. The logic functions as intended—no fix is needed.api/dev/configs/api.json (1)
2-2: Version bump looks consistentOnly the dev API config version was updated; structure and other fields are unchanged. As long as
4.27.2matches the API build you’re shipping, this is good to go.api/src/unraid-api/graph/resolvers/mutation/mutation.model.ts (1)
27-30: CustomizationMutations container wiring matches existing mutation patternDefining
CustomizationMutationsand exposing it viaRootMutations.customizationmirrors the existing nested mutation containers (array/docker/vm/apiKey/parityCheck/rclone). Naming and descriptions are consistent.Also applies to: 62-63
api/src/unraid-api/graph/resolvers/customization/customization.mutations.resolver.ts (1)
1-25: setTheme mutation resolver is well-scoped and permissionedThe resolver cleanly delegates to
CustomizationService.setTheme, usesThemeNameas the argument type and returnsTheme, and appliesUsePermissions({ action: AuthAction.UPDATE_ANY, resource: Resource.CUSTOMIZATIONS })in line with existing mutation patterns. No additional error handling is needed here since it’s handled in the service.api/src/unraid-api/graph/resolvers/mutation/mutation.resolver.ts (1)
6-6: RootMutations.customization entry correctly exposes customization mutationsThe new
@Mutation(() => CustomizationMutations, { name: 'customization' })method matches the existing container-style mutations and cleanly returns a newCustomizationMutationsinstance. Import updates are correct.Also applies to: 41-44
api/src/unraid-api/graph/resolvers/customization/customization.module.ts (1)
3-3: CustomizationModule now cleanly wires mutations resolver and exports the serviceAdding
CustomizationMutationsResolvertoprovidersand exportingCustomizationServicealigns this module with how other resolver/service modules are structured. No issues.Also applies to: 8-9
api/generated-schema.graphql (1)
947-963: Schema additions for customization/theming align with resolvers; ensure dependent codegen is updatedThe generated SDL updates look internally consistent:
type CustomizationMutationswithsetTheme(theme: ThemeName!): Theme!matchesCustomizationMutationsResolver.setTheme.enum ThemeName { azure black gray white }aligns with the service logic that mapsdynamix.display.themetoThemeName.Mutation.customization: CustomizationMutations!matches the newRootMutationsResolver.customization()entry.Since this file is generated, the main follow-up is to ensure all dependent codegen is refreshed (e.g., web GraphQL TypeScript types via
pnpm codegeninweb/) so clients seeThemeName,CustomizationMutations, and thecustomizationmutation root.Also applies to: 2461-2461
api/src/unraid-api/graph/resolvers/customization/customization.service.ts (1)
12-15: Argument shape is correct;loadDynamixConfigFromDiskSyncreceives the right data structureThe review comment's concern about the loader argument is based on a misunderstanding. The code is correct as-is:
- Line 50 in
onModuleInit:this.configFile = paths['dynamix-config']?.[1]extracts a single file path string for use withupdateCfgFile.- Line 478 in
setTheme:loadDynamixConfigFromDiskSync(paths['dynamix-config'])correctly passes the full array to the loader.The function signature requires
configPaths: readonly (string | undefined | null)[](an array), andpaths['dynamix-config']is exactly that. This matches the existing pattern inapi/src/store/index.tsline 32, which also passes the full array:loadDynamixConfigFromDiskSync(configPaths)whereconfigPaths = store.getState().paths['dynamix-config'] ?? [].Passing
this.configFile(a single string) would violate the type contract and break the function.Optional: add error handling for reload/dispatch resilience
If the disk read or dispatch fails after
updateCfgFilesucceeds, the mutation currently rejects even though the config on disk is updated. Wrapping the reload/dispatch in atry-catchthat logs but proceeds togetTheme()would make the endpoint more resilient, though this is optional.web/vite-plugin-serve-static.ts (4)
60-69: Config path resolution order looks reasonable for devThe candidate resolution favors explicit env vars (
DEV_DYNAMIX_CFG,PATHS_DYNAMIX_CONFIG) before falling back to repo-relative paths, which is a sane precedence for a dev-only tool. TheexistsSyncprobe infindDynamixConfigPathis simple and should behave predictably across the small candidate list.No changes needed from my side; just confirm this order matches how you expect to override local configs during development.
111-152: Config write logic is correct; consider concurrency edge cases only if neededThe
writeThemeToConfigread–modify–write flow correctly:
- Preserves existing sections.
- Updates an existing
theme=in[display]if present.- Otherwise inserts
theme=into an existing[display]or creates the section at the end.For a Vite dev server this is plenty. The only theoretical gap is concurrent writes (multiple
/dev/themePOSTs) racing on the same file, but that’s unlikely and probably not worth guarding against here.No required changes; just be aware of the race window in case this ever gets reused outside dev tooling.
153-162: Request body reader is fine for dev; no size limits
readRequestBodyis straightforward and adequate for the small JSON payload expected on/dev/theme. If this ever grows beyond a tiny dev interface, you might want to:
- Impose a max body size and reject larger payloads.
- Handle
req.abortedexplicitly.For now, I’d keep it as-is.
230-276:/dev/themeendpoint behavior looks correct and robustThe middleware cleanly handles:
GET→ returns{ theme: ThemeName | null }JSON from config.POST→ reads JSON body, validatesthemeas a string, normalizes againstALLOWED_THEMES, persists viawriteThemeToConfig, and returns the result plustheme.- Other methods → 405 with an
Allowheader.- Malformed JSON → 400 with a structured error.
This aligns well with the PR’s goal of a small dev-only surface for theme persistence, and keeps responses consistently JSON-typed for the main flows.
No behavior changes to suggest here.
| type SetThemeMutationResult = { | ||
| customization: { | ||
| setTheme: { | ||
| name: ThemeName; | ||
| showBannerImage: boolean; | ||
| showBannerGradient: boolean; | ||
| headerBackgroundColor?: string | null; | ||
| showHeaderDescription: boolean; | ||
| headerPrimaryTextColor?: string | null; | ||
| headerSecondaryTextColor?: string | null; | ||
| }; | ||
| }; | ||
| }; | ||
| type SetThemeMutationVariables = { | ||
| theme: ThemeName; | ||
| }; | ||
| const SET_THEME_MUTATION = gql` | ||
| mutation setTheme($theme: ThemeName!) { | ||
| customization { | ||
| setTheme(theme: $theme) { | ||
| name | ||
| showBannerImage | ||
| showBannerGradient | ||
| headerBackgroundColor | ||
| showHeaderDescription | ||
| headerPrimaryTextColor | ||
| headerSecondaryTextColor | ||
| } | ||
| } | ||
| } | ||
| `; | ||
| const { mutate: setThemeMutation } = useMutation<SetThemeMutationResult, SetThemeMutationVariables>( | ||
| SET_THEME_MUTATION | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Script executed:
# Check if SetThemeMutation types exist in generated graphql files
rg -n "SetThemeMutation|SetTheme" --type ts web/composables/gql/Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 1
</shell_result>
Script executed:
# Explore .standalone.vue files to understand the pattern
fd -e "standalone.vue" web/Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 1
stderr:
[fd error]: The search pattern 'web/' contains a path-separation character ('/') and will not lead to any search results.
If you want to search for all files inside the 'web/' directory, use a match-all pattern:
fd . 'web/'
Instead, if you want your pattern to match the full file path, use:
fd --full-path 'web/'
</shell_result>
Script executed:
# Check the current DevThemeSwitcher.standalone.vue file content
wc -l web/src/components/DevThemeSwitcher.standalone.vueRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
286 web/src/components/DevThemeSwitcher.standalone.vue
</shell_result>
Script executed:
# Find .standalone.vue files correctly
fd -e standalone.vue . web/Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
web/src/components/Activation/WelcomeModal.standalone.vue
web/src/components/ApiKeyAuthorize.standalone.vue
web/src/components/ApiKeyPage.standalone.vue
web/src/components/ApiStatus/ApiStatus.standalone.vue
web/src/components/Auth.standalone.vue
web/src/components/CallbackHandler.standalone.vue
web/src/components/ColorSwitcher.standalone.vue
web/src/components/ConnectSettings/ConnectSettings.standalone.vue
web/src/components/DevModalTest.standalone.vue
web/src/components/DevThemeSwitcher.standalone.vue
web/src/components/DowngradeOs.standalone.vue
web/src/components/HeaderOsVersion.standalone.vue
web/src/components/LayoutViews/Detail/DetailTest.standalone.vue
web/src/components/Logs/LogViewer.standalone.vue
web/src/components/Modals.standalone.vue
web/src/components/Registration.standalone.vue
web/src/components/SsoButton.standalone.vue
web/src/components/TestThemeSwitcher.standalone.vue
web/src/components/ThemeSwitcher.standalone.vue
web/src/components/UpdateOs/TestUpdateModal.standalone.vue
web/src/components/UpdateOs.standalone.vue
web/src/components/UserProfile.standalone.vue
web/src/components/WanIpCheck.standalone.vue
</shell_result>
Script executed:
# Check what files exist in web/composables/gql
ls -la web/composables/gql/ 2>/dev/null || echo "Directory not found"Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
total 196
drwxr-xr-x 2 jailuser jailuser 120 Nov 26 21:30 .
drwxr-xr-x 3 jailuser jailuser 60 Nov 26 21:30 ..
-rw-r--r-- 1 jailuser jailuser 3934 Nov 26 21:30 fragment-masking.ts
-rw-r--r-- 1 jailuser jailuser 42524 Nov 26 21:30 gql.ts
-rw-r--r-- 1 jailuser jailuser 145290 Nov 26 21:30 graphql.ts
-rw-r--r-- 1 jailuser jailuser 59 Nov 26 21:30 index.ts
</shell_result>
Script executed:
# Read lines 104-140 of the file
sed -n '104,140p' web/src/components/DevThemeSwitcher.standalone.vueRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
type SetThemeMutationResult = {
customization: {
setTheme: {
name: ThemeName;
showBannerImage: boolean;
showBannerGradient: boolean;
headerBackgroundColor?: string | null;
showHeaderDescription: boolean;
headerPrimaryTextColor?: string | null;
headerSecondaryTextColor?: string | null;
};
};
};
type SetThemeMutationVariables = {
theme: ThemeName;
};
const SET_THEME_MUTATION = gql`
mutation setTheme($theme: ThemeName!) {
customization {
setTheme(theme: $theme) {
name
showBannerImage
showBannerGradient
headerBackgroundColor
showHeaderDescription
headerPrimaryTextColor
headerSecondaryTextColor
}
}
}
`;
const { mutate: setThemeMutation } = useMutation<SetThemeMutationResult, SetThemeMutationVariables>(
SET_THEME_MUTATION
);
</shell_result>
Script executed:
# Search for SetThemeMutation or SetTheme in the generated graphql types
rg -n "SetTheme" web/composables/gql/graphql.ts | head -20Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
</shell_result>
Script executed:
# Check ColorSwitcher.standalone.vue to see if it uses generated types or inline
grep -n "Mutation" web/src/components/ColorSwitcher.standalone.vue | head -20Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
</shell_result>
Script executed:
# Check TestThemeSwitcher.standalone.vue for comparison
grep -n "Mutation" web/src/components/TestThemeSwitcher.standalone.vue | head -20Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
</shell_result>
Script executed:
# Look for mutation type definitions in another standalone component
rg -n "type.*Mutation" web/src/components/ColorSwitcher.standalone.vueRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 1
</shell_result>
Script executed:
# Search for setTheme (lowercase) in generated files
rg -n "setTheme" web/composables/gql/graphql.ts | head -10Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
</shell_result>
Script executed:
# Check the structure of graphql.ts to understand if SetThemeMutation should exist
head -100 web/composables/gql/graphql.tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
/* eslint-disable */
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends keyof T> = { [_ in K]?: never };
export type Incremental<T> = T | { [P in keyof T]?: P extends ' $fragmentName' | '__typename' ? T[P] : never };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: { input: string; output: string; }
String: { input: string; output: string; }
Boolean: { input: boolean; output: boolean; }
Int: { input: number; output: number; }
Float: { input: number; output: number; }
/** The `BigInt` scalar type represents non-fractional signed whole numeric values. */
BigInt: { input: any; output: any; }
/** A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. */
DateTime: { input: string; output: string; }
/** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
JSON: { input: any; output: any; }
/** A field whose value is a valid TCP port within the range of 0 to 65535: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports */
Port: { input: number; output: number; }
/**
*
* ### Description:
*
* ID scalar type that prefixes the underlying ID with the server identifier on output and strips it on input.
*
* We use this scalar type to ensure that the ID is unique across all servers, allowing the same underlying resource ID to be used across different server instances.
*
* #### Input Behavior:
*
* When providing an ID as input (e.g., in arguments or input objects), the server identifier prefix ('<serverId>:') is optional.
*
* - If the prefix is present (e.g., '123:456'), it will be automatically stripped, and only the underlying ID ('456') will be used internally.
* - If the prefix is absent (e.g., '456'), the ID will be used as-is.
*
* This makes it flexible for clients, as they don't strictly need to know or provide the server ID.
*
* #### Output Behavior:
*
* When an ID is returned in the response (output), it will *always* be prefixed with the current server's unique identifier (e.g., '123:456').
*
* #### Example:
*
* Note: The server identifier is '123' in this example.
*
* ##### Input (Prefix Optional):
* ```graphql
* # Both of these are valid inputs resolving to internal ID '456'
* {
* someQuery(id: "123:456") { ... }
* anotherQuery(id: "456") { ... }
* }
* ```
*
* ##### Output (Prefix Always Added):
* ```graphql
* # Assuming internal ID is '456'
* {
* "data": {
* "someResource": {
* "id": "123:456"
* }
* }
* }
* ```
*
*/
PrefixedID: { input: string; output: string; }
/** A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. */
URL: { input: URL; output: URL; }
};
export type AccessUrl = {
__typename?: 'AccessUrl';
ipv4?: Maybe<Scalars['URL']['output']>;
ipv6?: Maybe<Scalars['URL']['output']>;
name?: Maybe<Scalars['String']['output']>;
type: UrlType;
};
export type AccessUrlInput = {
ipv4?: InputMaybe<Scalars['URL']['input']>;
ipv6?: InputMaybe<Scalars['URL']['input']>;
name?: InputMaybe<Scalars['String']['input']>;
type: UrlType;
};
export type AccessUrlObject = {
__typename?: 'AccessUrlObject';
ipv4?: Maybe<Scalars['String']['output']>;
ipv6?: Maybe<Scalars['String']['output']>;
name?: Maybe<Scalars['String']['output']>;
type: UrlType;
};
export type AccessUrlObjectInput = {
</shell_result>
Script executed:
# Look at ColorSwitcher.standalone.vue content
cat web/src/components/ColorSwitcher.standalone.vueRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
<script lang="ts" setup>
import { reactive, watch } from 'vue';
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
Input,
Label,
Select,
Switch,
} from '@unraid/ui';
import { defaultColors } from '~/themes/default';
import type { Theme } from '~/themes/types';
import { useThemeStore } from '~/store/theme';
const themeStore = useThemeStore();
// Form state
const form = reactive({
selectedTheme: 'white',
gradient: false,
description: true,
banner: true,
textPrimary: '',
textSecondary: '',
bgColor: '',
});
// Watch for changes and update theme
watch([form], () => {
// Enable gradient if banner is enabled
if (form.banner && !form.gradient) {
form.gradient = true;
}
const themeToSet: Theme = {
banner: form.banner,
bannerGradient: form.gradient,
descriptionShow: form.description,
textColor: form.textPrimary ?? defaultColors[form.selectedTheme]['--header-text-primary']!,
metaColor: form.textSecondary ?? defaultColors[form.selectedTheme]['--header-text-secondary']!,
bgColor: form.bgColor ?? defaultColors[form.selectedTheme]['--header-background-color']!,
name: form.selectedTheme,
};
themeStore.setTheme(themeToSet);
});
const items = [
{ value: 'white', label: 'Light' },
{ value: 'black', label: 'Dark' },
{ value: 'azure', label: 'Azure' },
{ value: 'gray', label: 'Gray' },
];
</script>
<template>
<Accordion>
<AccordionItem value="color-theme-customization">
<AccordionTrigger>Color Theme Customization</AccordionTrigger>
<AccordionContent>
<div class="border-muted flex flex-col gap-2 border-2 border-solid p-2">
<h1 class="text-lg">Color Theme Customization</h1>
<Label for="theme-select">Theme</Label>
<Select v-model="form.selectedTheme" :items="items" placeholder="Select a theme" />
<Label for="primary-text-color">Header Primary Text Color</Label>
<Input id="primary-text-color" v-model="form.textPrimary" />
<Label for="secondary-text-color">Header Secondary Text Color</Label>
<Input id="secondary-text-color" v-model="form.textSecondary" />
<Label for="background-color">Header Background Color</Label>
<Input id="background-color" v-model="form.bgColor" />
<Label for="gradient">Gradient</Label>
<Switch id="gradient" v-model:checked="form.gradient" />
<Label for="description">Description</Label>
<Switch id="description" v-model:checked="form.description" />
<Label for="banner">Banner</Label>
<Switch id="banner" v-model:checked="form.banner" />
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
</template>
</shell_result>
Script executed:
# Search for mutation definitions in graphql.ts
rg -n "mutation|Mutation" web/composables/gql/graphql.ts | head -30Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
163:/** API Key related mutations */
164:export type ApiKeyMutations = {
165: __typename?: 'ApiKeyMutations';
179:/** API Key related mutations */
180:export type ApiKeyMutationsAddRoleArgs = {
185:/** API Key related mutations */
186:export type ApiKeyMutationsCreateArgs = {
191:/** API Key related mutations */
192:export type ApiKeyMutationsDeleteArgs = {
197:/** API Key related mutations */
198:export type ApiKeyMutationsRemoveRoleArgs = {
203:/** API Key related mutations */
204:export type ApiKeyMutationsUpdateArgs = {
304:export type ArrayMutations = {
305: __typename?: 'ArrayMutations';
321:export type ArrayMutationsAddDiskToArrayArgs = {
326:export type ArrayMutationsClearArrayDiskStatisticsArgs = {
331:export type ArrayMutationsMountArrayDiskArgs = {
336:export type ArrayMutationsRemoveDiskFromArrayArgs = {
341:export type ArrayMutationsSetStateArgs = {
346:export type ArrayMutationsUnmountArrayDiskArgs = {
713:export type DockerMutations = {
714: __typename?: 'DockerMutations';
722:export type DockerMutationsStartArgs = {
727:export type DockerMutationsStopArgs = {
1199:export type Mutation = {
1200: __typename?: 'Mutation';
1203: apiKey: ApiKeyMutations;
1208: array: ArrayMutations;
1219: docker: DockerMutations;
</shell_result>
Script executed:
# Search for setTheme in gql.ts
rg -n "setTheme" web/composables/gql/gql.tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 1
</shell_result>
Script executed:
# Check if non-standalone ThemeSwitcher exists
ls -la web/src/components/ThemeSwitcher.vue 2>/dev/null || echo "Not found"Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
Not found
</shell_result>
Script executed:
# Check the full DevThemeSwitcher.standalone.vue to understand context
head -50 web/src/components/DevThemeSwitcher.standalone.vueRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useMutation, useQuery } from '@vue/apollo-composable';
import gql from 'graphql-tag';
import type { GetThemeQuery } from '~/composables/gql/graphql';
import { ThemeName } from '~/composables/gql/graphql';
import { DARK_UI_THEMES, GET_THEME_QUERY, useThemeStore } from '~/store/theme';
const themeStore = useThemeStore();
const themeOptions: Array<{ value: ThemeName; label: string }> = [
{ value: ThemeName.WHITE, label: 'White' },
{ value: ThemeName.BLACK, label: 'Black' },
{ value: ThemeName.GRAY, label: 'Gray' },
{ value: ThemeName.AZURE, label: 'Azure' },
];
const STORAGE_KEY_THEME = 'unraid:test:theme';
const THEME_COOKIE_KEY = 'unraid_dev_theme';
const { theme } = storeToRefs(themeStore);
const themeValues = new Set<ThemeName>(themeOptions.map((option) => option.value));
const normalizeTheme = (value?: string | ThemeName | null): ThemeName | null => {
const normalized = (value ?? '').toString().toLowerCase();
return themeValues.has(normalized as ThemeName) ? (normalized as ThemeName) : null;
};
const readCookieTheme = (): string | null => {
if (typeof document === 'undefined') {
return null;
}
const cookies = document.cookie?.split(';') ?? [];
for (const cookie of cookies) {
const [name, ...rest] = cookie.split('=');
if (name?.trim() === THEME_COOKIE_KEY) {
return decodeURIComponent(rest.join('=').trim());
}
}
return null;
};
const readLocalStorageTheme = (): string | null => {
</shell_result>
Script executed:
# Search for SetThemeMutation export in graphql.ts
rg -n "export type SetTheme" web/composables/gql/graphql.tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 1
</shell_result>
Script executed:
# Check ~/store/theme.ts
cat web/store/theme.ts | head -100Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stderr:
cat: web/store/theme.ts: No such file or directory
</shell_result>
Script executed:
# Search for setTheme string definition in gql.ts (the query/mutation definitions)
rg -A5 "setTheme" web/composables/gql/gql.tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 1
</shell_result>
Script executed:
# Find theme.ts file
fd -e "theme.ts" web/Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 1
stderr:
[fd error]: The search pattern 'web/' contains a path-separation character ('/') and will not lead to any search results.
If you want to search for all files inside the 'web/' directory, use a match-all pattern:
fd . 'web/'
Instead, if you want your pattern to match the full file path, use:
fd --full-path 'web/'
</shell_result>
Script executed:
# Search for GET_THEME_QUERY to find where it's defined
rg -n "GET_THEME_QUERY" web/Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
web/src/components/DevThemeSwitcher.standalone.vue:11:import { DARK_UI_THEMES, GET_THEME_QUERY, useThemeStore } from '~/store/theme';
web/src/components/DevThemeSwitcher.standalone.vue:84: GET_THEME_QUERY,
web/src/store/theme.ts:15:export const GET_THEME_QUERY = graphql(`
web/src/store/theme.ts:117: const { load, onResult, onError } = useLazyQuery<GetThemeQuery>(GET_THEME_QUERY, null, {
</shell_result>
Script executed:
# Search for customization mutations in graphql.ts
rg -n "customization|setTheme" web/composables/gql/graphql.ts | head -20Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
1648: customization?: Maybe<Customization>;
2567:export type ActivationCodeQuery = { __typename?: 'Query', vars: { __typename?: 'Vars', regState?: RegistrationState | null }, customization?: { __typename?: 'Customization', activationCode?: { __typename?: 'ActivationCode', code?: string | null, partnerName?: string | null, serverName?: string | null, sysModel?: string | null, comment?: string | null, header?: string | null, headermetacolor?: string | null, background?: string | null, showBannerGradient?: boolean | null, theme?: string | null } | null, partnerInfo?: { __typename?: 'PublicPartnerInfo', hasPartnerLogo: boolean, partnerName?: string | null, partnerUrl?: string | null, partnerLogoUrl?: string | null } | null } | null };
2830:export const ActivationCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ActivationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regState"}}]}},{"kind":"Field","name":{"kind":"Name","value":"customization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activationCode"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"serverName"}},{"kind":"Field","name":{"kind":"Name","value":"sysModel"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"header"}},{"kind":"Field","name":{"kind":"Name","value":"headermetacolor"}},{"kind":"Field","name":{"kind":"Name","value":"background"}},{"kind":"Field","name":{"kind":"Name","value":"showBannerGradient"}},{"kind":"Field","name":{"kind":"Name","value":"theme"}}]}},{"kind":"Field","name":{"kind":"Name","value":"partnerInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasPartnerLogo"}},{"kind":"Field","name":{"kind":"Name","value":"partnerName"}},{"kind":"Field","name":{"kind":"Name","value":"partnerUrl"}},{"kind":"Field","name":{"kind":"Name","value":"partnerLogoUrl"}}]}}]}}]}}]} as unknown as DocumentNode<ActivationCodeQuery, ActivationCodeQueryVariables>;
</shell_result>
Script executed:
# List what's exported from gql.ts
rg -n "^export" web/composables/gql/gql.ts | head -40Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
119:export function graphql(source: string): unknown;
124:export function graphql(source: "\n query PartnerInfo {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n"): (typeof documents)["\n query PartnerInfo {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n"];
128:export function graphql(source: "\n query PublicWelcomeData {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n isInitialSetup\n }\n"): (typeof documents)["\n query PublicWelcomeData {\n publicPartnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n isInitialSetup\n }\n"];
132:export function graphql(source: "\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n"): (typeof documents)["\n query ActivationCode {\n vars {\n regState\n }\n customization {\n activationCode {\n code\n partnerName\n serverName\n sysModel\n comment\n header\n headermetacolor\n background\n showBannerGradient\n theme\n }\n partnerInfo {\n hasPartnerLogo\n partnerName\n partnerUrl\n partnerLogoUrl\n }\n }\n }\n"];
136:export function graphql(source: "\n query GetApiKeyCreationFormSchema {\n getApiKeyCreationFormSchema {\n id\n dataSchema\n uiSchema\n values\n }\n }\n"): (typeof documents)["\n query GetApiKeyCreationFormSchema {\n getApiKeyCreationFormSchema {\n id\n dataSchema\n uiSchema\n values\n }\n }\n"];
140:export function graphql(source: "\n fragment ApiKey on ApiKey {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n"): (typeof documents)["\n fragment ApiKey on ApiKey {\n id\n key\n name\n description\n createdAt\n roles\n permissions {\n resource\n actions\n }\n }\n"];
144:export function graphql(source: "\n query ApiKeys {\n apiKeys {\n ...ApiKey\n }\n }\n"): (typeof documents)["\n query ApiKeys {\n apiKeys {\n ...ApiKey\n }\n }\n"];
148:export function graphql(source: "\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n ...ApiKey\n }\n }\n }\n"): (typeof documents)["\n mutation CreateApiKey($input: CreateApiKeyInput!) {\n apiKey {\n create(input: $input) {\n ...ApiKey\n }\n }\n }\n"];
152:export function graphql(source: "\n mutation UpdateApiKey($input: UpdateApiKeyInput!) {\n apiKey {\n update(input: $input) {\n ...ApiKey\n }\n }\n }\n"): (typeof documents)["\n mutation UpdateApiKey($input: UpdateApiKeyInput!) {\n apiKey {\n update(input: $input) {\n ...ApiKey\n }\n }\n }\n"];
156:export function graphql(source: "\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n"): (typeof documents)["\n mutation DeleteApiKey($input: DeleteApiKeyInput!) {\n apiKey {\n delete(input: $input)\n }\n }\n"];
160:export function graphql(source: "\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n"): (typeof documents)["\n query ApiKeyMeta {\n apiKeyPossibleRoles\n apiKeyPossiblePermissions {\n resource\n actions\n }\n }\n"];
164:export function graphql(source: "\n query PreviewEffectivePermissions($roles: [Role!], $permissions: [AddPermissionInput!]) {\n previewEffectivePermissions(roles: $roles, permissions: $permissions) {\n resource\n actions\n }\n }\n"): (typeof documents)["\n query PreviewEffectivePermissions($roles: [Role!], $permissions: [AddPermissionInput!]) {\n previewEffectivePermissions(roles: $roles, permissions: $permissions) {\n resource\n actions\n }\n }\n"];
168:export function graphql(source: "\n query GetPermissionsForRoles($roles: [Role!]!) {\n getPermissionsForRoles(roles: $roles) {\n resource\n actions\n }\n }\n"): (typeof documents)["\n query GetPermissionsForRoles($roles: [Role!]!) {\n getPermissionsForRoles(roles: $roles) {\n resource\n actions\n }\n }\n"];
172:export function graphql(source: "\n query Unified {\n settings {\n unified {\n id\n dataSchema\n uiSchema\n values\n }\n }\n }\n"): (typeof documents)["\n query Unified {\n settings {\n unified {\n id\n dataSchema\n uiSchema\n values\n }\n }\n }\n"];
176:export function graphql(source: "\n mutation UpdateConnectSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n"): (typeof documents)["\n mutation UpdateConnectSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n"];
180:export function graphql(source: "\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n"): (typeof documents)["\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n"];
184:export function graphql(source: "\n query LogFileContent($path: String!, $lines: Int, $startLine: Int) {\n logFile(path: $path, lines: $lines, startLine: $startLine) {\n path\n content\n totalLines\n startLine\n }\n }\n"): (typeof documents)["\n query LogFileContent($path: String!, $lines: Int, $startLine: Int) {\n logFile(path: $path, lines: $lines, startLine: $startLine) {\n path\n content\n totalLines\n startLine\n }\n }\n"];
188:export function graphql(source: "\n subscription LogFileSubscription($path: String!) {\n logFile(path: $path) {\n path\n content\n totalLines\n }\n }\n"): (typeof documents)["\n subscription LogFileSubscription($path: String!) {\n logFile(path: $path) {\n path\n content\n totalLines\n }\n }\n"];
192:export function graphql(source: "\n fragment NotificationFragment on Notification {\n id\n title\n subject\n description\n importance\n link\n type\n timestamp\n formattedTimestamp\n }\n"): (typeof documents)["\n fragment NotificationFragment on Notification {\n id\n title\n subject\n description\n importance\n link\n type\n timestamp\n formattedTimestamp\n }\n"];
196:export function graphql(source: "\n fragment NotificationCountFragment on NotificationCounts {\n total\n info\n warning\n alert\n }\n"): (typeof documents)["\n fragment NotificationCountFragment on NotificationCounts {\n total\n info\n warning\n alert\n }\n"];
200:export function graphql(source: "\n query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n"): (typeof documents)["\n query Notifications($filter: NotificationFilter!) {\n notifications {\n id\n list(filter: $filter) {\n ...NotificationFragment\n }\n }\n }\n"];
204:export function graphql(source: "\n mutation ArchiveNotification($id: PrefixedID!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n"): (typeof documents)["\n mutation ArchiveNotification($id: PrefixedID!) {\n archiveNotification(id: $id) {\n ...NotificationFragment\n }\n }\n"];
208:export function graphql(source: "\n mutation ArchiveAllNotifications {\n archiveAll {\n unread {\n total\n }\n archive {\n info\n warning\n alert\n total\n }\n }\n }\n"): (typeof documents)["\n mutation ArchiveAllNotifications {\n archiveAll {\n unread {\n total\n }\n archive {\n info\n warning\n alert\n total\n }\n }\n }\n"];
212:export function graphql(source: "\n mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n"): (typeof documents)["\n mutation DeleteNotification($id: PrefixedID!, $type: NotificationType!) {\n deleteNotification(id: $id, type: $type) {\n archive {\n total\n }\n }\n }\n"];
216:export function graphql(source: "\n mutation DeleteAllNotifications {\n deleteArchivedNotifications {\n archive {\n total\n }\n unread {\n total\n }\n }\n }\n"): (typeof documents)["\n mutation DeleteAllNotifications {\n deleteArchivedNotifications {\n archive {\n total\n }\n unread {\n total\n }\n }\n }\n"];
220:export function graphql(source: "\n query Overview {\n notifications {\n id\n overview {\n unread {\n info\n warning\n alert\n total\n }\n archive {\n total\n }\n }\n }\n }\n"): (typeof documents)["\n query Overview {\n notifications {\n id\n overview {\n unread {\n info\n warning\n alert\n total\n }\n archive {\n total\n }\n }\n }\n }\n"];
224:export function graphql(source: "\n mutation RecomputeOverview {\n recalculateOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n"): (typeof documents)["\n mutation RecomputeOverview {\n recalculateOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n"];
228:export function graphql(source: "\n subscription NotificationAddedSub {\n notificationAdded {\n ...NotificationFragment\n }\n }\n"): (typeof documents)["\n subscription NotificationAddedSub {\n notificationAdded {\n ...NotificationFragment\n }\n }\n"];
232:export function graphql(source: "\n subscription NotificationOverviewSub {\n notificationsOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n"): (typeof documents)["\n subscription NotificationOverviewSub {\n notificationsOverview {\n archive {\n ...NotificationCountFragment\n }\n unread {\n ...NotificationCountFragment\n }\n }\n }\n"];
236:export function graphql(source: "\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n rclone {\n createRCloneRemote(input: $input) {\n name\n type\n parameters\n }\n }\n }\n"): (typeof documents)["\n mutation CreateRCloneRemote($input: CreateRCloneRemoteInput!) {\n rclone {\n createRCloneRemote(input: $input) {\n name\n type\n parameters\n }\n }\n }\n"];
240:export function graphql(source: "\n mutation DeleteRCloneRemote($input: DeleteRCloneRemoteInput!) {\n rclone {\n deleteRCloneRemote(input: $input)\n }\n }\n"): (typeof documents)["\n mutation DeleteRCloneRemote($input: DeleteRCloneRemoteInput!) {\n rclone {\n deleteRCloneRemote(input: $input)\n }\n }\n"];
244:export function graphql(source: "\n query GetRCloneConfigForm($formOptions: RCloneConfigFormInput) {\n rclone {\n configForm(formOptions: $formOptions) {\n id\n dataSchema\n uiSchema\n }\n }\n }\n"): (typeof documents)["\n query GetRCloneConfigForm($formOptions: RCloneConfigFormInput) {\n rclone {\n configForm(formOptions: $formOptions) {\n id\n dataSchema\n uiSchema\n }\n }\n }\n"];
248:export function graphql(source: "\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n"): (typeof documents)["\n query ListRCloneRemotes {\n rclone {\n remotes {\n name\n type\n parameters\n config\n }\n }\n }\n"];
252:export function graphql(source: "\n query InfoVersions {\n info {\n id\n os {\n id\n hostname\n }\n versions {\n id\n core {\n unraid\n api\n }\n }\n }\n }\n"): (typeof documents)["\n query InfoVersions {\n info {\n id\n os {\n id\n hostname\n }\n versions {\n id\n core {\n unraid\n api\n }\n }\n }\n }\n"];
256:export function graphql(source: "\n query OidcProviders {\n settings {\n sso {\n oidcProviders {\n id\n name\n clientId\n issuer\n authorizationEndpoint\n tokenEndpoint\n jwksUri\n scopes\n authorizationRules {\n claim\n operator\n value\n }\n authorizationRuleMode\n buttonText\n buttonIcon\n }\n }\n }\n }\n"): (typeof documents)["\n query OidcProviders {\n settings {\n sso {\n oidcProviders {\n id\n name\n clientId\n issuer\n authorizationEndpoint\n tokenEndpoint\n jwksUri\n scopes\n authorizationRules {\n claim\n operator\n value\n }\n authorizationRuleMode\n buttonText\n buttonIcon\n }\n }\n }\n }\n"];
260:export function graphql(source: "\n query PublicOidcProviders {\n publicOidcProviders {\n id\n name\n buttonText\n buttonIcon\n buttonVariant\n buttonStyle\n }\n }\n"): (typeof documents)["\n query PublicOidcProviders {\n publicOidcProviders {\n id\n name\n buttonText\n buttonIcon\n buttonVariant\n buttonStyle\n }\n }\n"];
264:export function graphql(source: "\n query serverInfo {\n info {\n os {\n hostname\n }\n }\n vars {\n comment\n }\n }\n"): (typeof documents)["\n query serverInfo {\n info {\n os {\n hostname\n }\n }\n vars {\n comment\n }\n }\n"];
268:export function graphql(source: "\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n"): (typeof documents)["\n mutation ConnectSignIn($input: ConnectSignInInput!) {\n connectSignIn(input: $input)\n }\n"];
272:export function graphql(source: "\n mutation SignOut {\n connectSignOut\n }\n"): (typeof documents)["\n mutation SignOut {\n connectSignOut\n }\n"];
276:export function graphql(source: "\n query IsSSOEnabled {\n isSSOEnabled\n }\n"): (typeof documents)["\n query IsSSOEnabled {\n isSSOEnabled\n }\n"];
</shell_result>
Script executed:
# Search for setTheme in gql.ts more broadly
grep -n "setTheme" web/composables/gql/gql.tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 1
</shell_result>
Script executed:
# Check theme.ts file content
cat web/src/store/theme.tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { useLazyQuery } from '@vue/apollo-composable';
import { defaultColors } from '~/themes/default';
import type { GetThemeQuery } from '~/composables/gql/graphql';
import type { Theme, ThemeVariables } from '~/themes/types';
import { graphql } from '~/composables/gql/gql';
// Themes that should apply the .dark class (dark UI themes)
export const DARK_UI_THEMES = ['gray', 'black'] as const;
export const GET_THEME_QUERY = graphql(`
query getTheme {
publicTheme {
name
showBannerImage
showBannerGradient
headerBackgroundColor
showHeaderDescription
headerPrimaryTextColor
headerSecondaryTextColor
}
}
`);
const DEFAULT_THEME: Theme = {
name: 'white',
banner: false,
bannerGradient: false,
bgColor: '',
descriptionShow: false,
metaColor: '',
textColor: '',
};
type ThemeSource = 'local' | 'server';
const isDomAvailable = () => typeof document !== 'undefined';
const getCssVar = (name: string): string => {
if (!isDomAvailable()) return '';
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
};
const readDomThemeName = () => getCssVar('--theme-name');
const readDomDarkFlag = (): boolean | null => {
if (!isDomAvailable()) return null;
const cssVar = getCssVar('--theme-dark-mode');
if (cssVar === '1') return true;
if (cssVar === '0') return false;
return null;
};
const hasDarkClassApplied = (): boolean => {
if (!isDomAvailable()) return false;
if (document.documentElement.classList.contains('dark')) return true;
if (document.body?.classList.contains('dark')) return true;
return Boolean(document.querySelector('.unapi.dark'));
};
const syncDarkClass = (method: 'add' | 'remove') => {
if (!isDomAvailable()) return;
document.documentElement.classList[method]('dark');
document.body?.classList[method]('dark');
document.querySelectorAll('.unapi').forEach((el) => el.classList[method]('dark'));
};
const applyDarkClass = (isDark: boolean) => {
if (!isDomAvailable()) return;
const method: 'add' | 'remove' = isDark ? 'add' : 'remove';
syncDarkClass(method);
document.documentElement.style.setProperty('--theme-dark-mode', isDark ? '1' : '0');
};
const bootstrapDarkClass = () => {
const domFlag = readDomDarkFlag();
const shouldBeDark = domFlag ?? hasDarkClassApplied();
if (shouldBeDark) {
applyDarkClass(true);
}
};
const sanitizeTheme = (data: Partial<Theme> | null | undefined): Theme | null => {
if (!data || typeof data !== 'object') {
return null;
}
return {
name: typeof data.name === 'string' ? data.name : DEFAULT_THEME.name,
banner: typeof data.banner === 'boolean' ? data.banner : DEFAULT_THEME.banner,
bannerGradient:
typeof data.bannerGradient === 'boolean' ? data.bannerGradient : DEFAULT_THEME.bannerGradient,
bgColor: typeof data.bgColor === 'string' ? data.bgColor : DEFAULT_THEME.bgColor,
descriptionShow:
typeof data.descriptionShow === 'boolean' ? data.descriptionShow : DEFAULT_THEME.descriptionShow,
metaColor: typeof data.metaColor === 'string' ? data.metaColor : DEFAULT_THEME.metaColor,
textColor: typeof data.textColor === 'string' ? data.textColor : DEFAULT_THEME.textColor,
};
};
export const useThemeStore = defineStore('theme', () => {
// State
const theme = ref<Theme>({ ...DEFAULT_THEME });
const activeColorVariables = ref<ThemeVariables>(defaultColors.white);
const hasServerTheme = ref(false);
const devOverride = ref(false);
// Initialize dark mode from CSS variable set by PHP or any pre-applied .dark class
bootstrapDarkClass();
// Lazy query - only executes when explicitly called
const { load, onResult, onError } = useLazyQuery<GetThemeQuery>(GET_THEME_QUERY, null, {
fetchPolicy: 'cache-and-network',
nextFetchPolicy: 'cache-first',
});
const mapPublicTheme = (publicTheme?: GetThemeQuery['publicTheme'] | null): Theme | null => {
if (!publicTheme) {
return null;
}
return sanitizeTheme({
name: publicTheme.name?.toLowerCase(),
banner: publicTheme.showBannerImage,
bannerGradient: publicTheme.showBannerGradient,
bgColor: publicTheme.headerBackgroundColor ?? undefined,
descriptionShow: publicTheme.showHeaderDescription,
metaColor: publicTheme.headerSecondaryTextColor ?? undefined,
textColor: publicTheme.headerPrimaryTextColor ?? undefined,
});
};
const applyThemeFromQuery = (publicTheme?: GetThemeQuery['publicTheme'] | null) => {
const sanitized = mapPublicTheme(publicTheme);
if (!sanitized) {
return;
}
setTheme(sanitized, { source: 'server' });
};
onResult(({ data }) => {
if (data?.publicTheme) {
applyThemeFromQuery(data.publicTheme);
}
});
onError((err) => {
console.warn('Failed to load theme from server, keeping existing theme:', err);
});
// Getters - read from DOM CSS variables set by PHP
const themeName = computed<string>(() => {
if (!isDomAvailable()) return DEFAULT_THEME.name;
const name = readDomThemeName() || theme.value.name;
return name || DEFAULT_THEME.name;
});
const darkMode = computed<boolean>(() => {
if (!isDomAvailable()) return false;
const flag = readDomDarkFlag();
return flag ?? hasDarkClassApplied();
});
const readBannerGradientVar = (): string => {
const raw = getCssVar('--banner-gradient');
if (!raw) return '';
const normalized = raw.trim().toLowerCase();
if (!normalized || normalized === 'null' || normalized === 'none' || normalized === 'undefined') {
return '';
}
return raw;
};
const bannerGradient = computed(() => {
if (darkMode.value) {
return undefined;
}
const { banner, bannerGradient } = theme.value;
if (!banner || !bannerGradient) {
return undefined;
}
const gradient = readBannerGradientVar();
// Only return gradient if CSS variable is set and not empty
// CSS variable is only set by PHP when both banner and gradient are enabled
if (gradient) {
return `background-image: ${gradient};`;
}
// No fallback - only use CSS variable set by PHP
return undefined;
});
// Actions
function setTheme(data?: Partial<Theme>, options: { source?: ThemeSource } = {}) {
if (data) {
const { source = 'local' } = options;
if (source === 'server') {
hasServerTheme.value = true;
} else if (hasServerTheme.value && !devOverride.value) {
return;
}
const sanitized = sanitizeTheme({
...theme.value,
...data,
});
if (sanitized) {
theme.value = sanitized;
const fallbackTheme = defaultColors[sanitized.name as keyof typeof defaultColors];
activeColorVariables.value = {
...(fallbackTheme ?? defaultColors.white),
};
}
}
}
const setDevOverride = (enabled: boolean) => {
devOverride.value = enabled;
};
const fetchTheme = () => {
load();
};
// Only apply dark class when theme changes (for dev tools that don't refresh)
// In production, PHP sets the dark class and page refreshes on theme change
watch(
() => theme.value.name,
(themeName) => {
const isDark = DARK_UI_THEMES.includes(themeName as (typeof DARK_UI_THEMES)[number]);
applyDarkClass(isDark);
},
{ immediate: false }
);
// Initialize theme from DOM on store creation
const domThemeName = themeName.value;
if (domThemeName && domThemeName !== DEFAULT_THEME.name) {
theme.value.name = domThemeName;
}
return {
// state
activeColorVariables,
bannerGradient,
darkMode,
theme: computed(() => ({
...theme.value,
name: themeName.value,
})),
// actions
setTheme,
setDevOverride,
fetchTheme,
};
});
</shell_result>
Script executed:
# Search in graphql.ts for SetTheme or customization mutations
grep -n "SetTheme\|CustomizationMutations\|customization.*mutation" web/composables/gql/graphql.ts | head -20Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
</shell_result>
Script executed:
# Search for Customization in graphql.ts to find setTheme mutation
rg -n "Customization" web/composables/gql/graphql.ts | head -20Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
584:export type Customization = {
585: __typename?: 'Customization';
1648: customization?: Maybe<Customization>;
2567:export type ActivationCodeQuery = { __typename?: 'Query', vars: { __typename?: 'Vars', regState?: RegistrationState | null }, customization?: { __typename?: 'Customization', activationCode?: { __typename?: 'ActivationCode', code?: string | null, partnerName?: string | null, serverName?: string | null, sysModel?: string | null, comment?: string | null, header?: string | null, headermetacolor?: string | null, background?: string | null, showBannerGradient?: boolean | null, theme?: string | null } | null, partnerInfo?: { __typename?: 'PublicPartnerInfo', hasPartnerLogo: boolean, partnerName?: string | null, partnerUrl?: string | null, partnerLogoUrl?: string | null } | null } | null };
</shell_result>
Script executed:
# Look for mutation definitions in the schema or codegen config
find . -name "*.graphql" -o -name "codegen.ts" -o -name "codegen.yml" | head -10Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
./api/codegen.ts
./api/legacy/generated-schema-legacy.graphql
./api/generated-schema.graphql
./packages/unraid-api-plugin-connect/codegen.ts
./web/codegen.ts
</shell_result>
Script executed:
# Check if there are any .mutation.ts files in web directory
fd -e mutation.ts web/Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 1
stderr:
[fd error]: The search pattern 'web/' contains a path-separation character ('/') and will not lead to any search results.
If you want to search for all files inside the 'web/' directory, use a match-all pattern:
fd . 'web/'
Instead, if you want your pattern to match the full file path, use:
fd --full-path 'web/'
</shell_result>
Script executed:
# Get the full Customization type definition
rg -A 30 "^export type Customization = {" web/composables/gql/graphql.tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 2
stderr:
regex parse error:
^export type Customization = {
^
error: unclosed counted repetition
</shell_result>
Script executed:
# Check web/codegen.ts
cat web/codegen.tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
overwrite: true,
documents: ['./**/**/*.ts'],
ignoreNoDocuments: false,
config: {
namingConvention: {
enumValues: 'change-case-all#upperCase',
transformUnderscore: true,
},
scalars: {
DateTime: 'string',
Long: 'number',
JSON: 'any',
URL: 'URL',
Port: 'number',
UUID: 'string',
PrefixedID: 'string',
BigInt: 'number',
},
},
generates: {
'src/composables/gql/': {
preset: 'client',
config: {
useTypeImports: true,
},
schema: '../api/generated-schema.graphql',
plugins: [
{
add: {
content: '/* eslint-disable */',
},
},
],
},
},
};
export default config;
</shell_result>
Script executed:
# Find .mutation.ts files correctly
fd --full-path ".*mutation\.ts" web/Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
web/src/components/ApiKey/apikey.mutation.ts
</shell_result>
Script executed:
# Check the GraphQL schema for setTheme mutation
grep -n "setTheme" api/generated-schema.graphqlRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
950: setTheme(
</shell_result>
Script executed:
# Check the apikey.mutation.ts file to understand the pattern
cat web/src/components/ApiKey/apikey.mutation.tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
import { graphql } from '~/composables/gql/gql';
export const CREATE_API_KEY = graphql(/* GraphQL */ `
mutation CreateApiKey($input: CreateApiKeyInput!) {
apiKey {
create(input: $input) {
...ApiKey
}
}
}
`);
export const UPDATE_API_KEY = graphql(/* GraphQL */ `
mutation UpdateApiKey($input: UpdateApiKeyInput!) {
apiKey {
update(input: $input) {
...ApiKey
}
}
}
`);
export const DELETE_API_KEY = graphql(/* GraphQL */ `
mutation DeleteApiKey($input: DeleteApiKeyInput!) {
apiKey {
delete(input: $input)
}
}
`);
</shell_result>
Script executed:
# Check if DevThemeSwitcher has a .mutation.ts file
ls -la web/src/components/DevThemeSwitcher.mutation.ts 2>/dev/null || echo "Not found"Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
Not found
</shell_result>
Script executed:
# Look at the setTheme definition in the GraphQL schema
grep -B 5 -A 10 "setTheme" api/generated-schema.graphqlRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
}
"""Customization related mutations"""
type CustomizationMutations {
"""Update the UI theme (writes dynamix.cfg)"""
setTheme(
"""Theme to apply"""
theme: ThemeName!
): Theme!
}
"""The theme name"""
enum ThemeName {
azure
black
gray
</shell_result>
Script executed:
# Check what the full Customization type looks like in the schema
grep -B 5 -A 30 "type Customization" api/generated-schema.graphqlRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
roles: [Role!]
permissions: [AddPermissionInput!]
}
"""Customization related mutations"""
type CustomizationMutations {
"""Update the UI theme (writes dynamix.cfg)"""
setTheme(
"""Theme to apply"""
theme: ThemeName!
): Theme!
}
"""The theme name"""
enum ThemeName {
azure
black
gray
white
}
"""
Parity check related mutations, WIP, response types and functionaliy will change
"""
type ParityCheckMutations {
"""Start a parity check"""
start(correct: Boolean!): JSON!
"""Pause a parity check"""
pause: JSON!
"""Resume a parity check"""
resume: JSON!
"""Cancel a parity check"""
cancel: JSON!
--
background: String
showBannerGradient: Boolean
theme: String
}
type Customization {
activationCode: ActivationCode
partnerInfo: PublicPartnerInfo
theme: Theme!
}
type Theme {
"""The theme name"""
name: ThemeName!
"""Whether to show the header banner image"""
showBannerImage: Boolean!
"""Whether to show the banner gradient"""
showBannerGradient: Boolean!
"""Whether to show the description in the header"""
showHeaderDescription: Boolean!
"""The background color of the header"""
headerBackgroundColor: String
"""The text color of the header"""
headerPrimaryTextColor: String
"""The secondary text color of the header"""
headerSecondaryTextColor: String
}
type ExplicitStatusItem {
name: String!
</shell_result>
Script executed:
# Check the entire mutation definition section in DevThemeSwitcher.standalone.vue (lines 104-140)
sed -n '100,145p' web/src/components/DevThemeSwitcher.standalone.vueRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
},
{ immediate: true }
);
type SetThemeMutationResult = {
customization: {
setTheme: {
name: ThemeName;
showBannerImage: boolean;
showBannerGradient: boolean;
headerBackgroundColor?: string | null;
showHeaderDescription: boolean;
headerPrimaryTextColor?: string | null;
headerSecondaryTextColor?: string | null;
};
};
};
type SetThemeMutationVariables = {
theme: ThemeName;
};
const SET_THEME_MUTATION = gql`
mutation setTheme($theme: ThemeName!) {
customization {
setTheme(theme: $theme) {
name
showBannerImage
showBannerGradient
headerBackgroundColor
showHeaderDescription
headerPrimaryTextColor
headerSecondaryTextColor
}
}
}
`;
const { mutate: setThemeMutation } = useMutation<SetThemeMutationResult, SetThemeMutationVariables>(
SET_THEME_MUTATION
);
const persistThemePreference = (themeName: ThemeName) => {
const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();
document.cookie = `${THEME_COOKIE_KEY}=${encodeURIComponent(themeName)}; path=/; SameSite=Lax; expires=${expires}`;
try {
</shell_result>
Script executed:
# Search for any other theme mutation definitions in the codebase
rg -n "setTheme" web/ --type tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
web/src/store/themeInitializer.ts:32: await themeStore.setTheme();
web/src/store/themeInitializer.ts:58:export function resetThemeInitialization(): void {
web/src/store/themeInitializer.ts:67: reset: resetThemeInitialization,
web/src/store/theme.ts:144: setTheme(sanitized, { source: 'server' });
web/src/store/theme.ts:199: function setTheme(data?: Partial<Theme>, options: { source?: ThemeSource } = {}) {
web/src/store/theme.ts:259: setTheme,
web/src/store/server.ts:152: themeStore.setTheme(newVal);
web/__test__/store/server.test.ts:275: setTheme: vi.fn(),
web/__test__/store/theme.test.ts:176: store.setTheme({ banner: true, bannerGradient: true });
web/__test__/store/theme.test.ts:227: store.setTheme(newTheme);
web/__test__/store/theme.test.ts:234: store.setTheme({ ...store.theme, name: 'black' });
web/__test__/store/theme.test.ts:240: store.setTheme({ ...store.theme, name: 'white' });
web/__test__/store/theme.test.ts:250: store.setTheme({
web/__test__/store/theme.test.ts:268: store.setTheme({
web/__test__/components/ColorSwitcher.test.ts:147: expect(themeStore.setTheme).toHaveBeenCalledTimes(2);
web/__test__/components/ColorSwitcher.test.ts:149: expect(themeStore.setTheme).toHaveBeenLastCalledWith({
web/__test__/components/ColorSwitcher.test.ts:189: expect(themeStore.setTheme).toHaveBeenCalledTimes(2);
web/__test__/components/ColorSwitcher.test.ts:190: expect(themeStore.setTheme).toHaveBeenCalledWith(expect.objectContaining({ textColor: '#ff0000' }));
web/__test__/components/ColorSwitcher.test.ts:196: expect(themeStore.setTheme).toHaveBeenCalledTimes(3);
web/__test__/components/ColorSwitcher.test.ts:197: expect(themeStore.setTheme).toHaveBeenCalledWith(expect.objectContaining({ metaColor: '#00ff00' }));
web/__test__/components/ColorSwitcher.test.ts:203: expect(themeStore.setTheme).toHaveBeenCalledTimes(4);
web/__test__/components/ColorSwitcher.test.ts:204: expect(themeStore.setTheme).toHaveBeenCalledWith(expect.objectContaining({ bgColor: '#0000ff' }));
web/__test__/components/ColorSwitcher.test.ts:206: expect(themeStore.setTheme).toHaveBeenLastCalledWith({
web/__test__/components/ColorSwitcher.test.ts:242: expect(themeStore.setTheme).toHaveBeenLastCalledWith(
web/__test__/components/ColorSwitcher.test.ts:249: expect(themeStore.setTheme).toHaveBeenLastCalledWith(
web/__test__/components/ColorSwitcher.test.ts:256: expect(themeStore.setTheme).toHaveBeenLastCalledWith(
web/__test__/components/ColorSwitcher.test.ts:293: expect(themeStore.setTheme).toHaveBeenLastCalledWith(
web/__test__/components/Activation/WelcomeModal.test.ts:63: setTheme: vi.fn(),
web/__test__/components/Activation/WelcomeModal.test.ts:203: it('calls setTheme on mount', () => {
web/__test__/components/Activation/WelcomeModal.test.ts:206: expect(mockThemeStore.setTheme).toHaveBeenCalled();
web/__test__/components/Activation/WelcomeModal.test.ts:214: mockThemeStore.setTheme.mockRejectedValueOnce(new Error('Theme error'));
web/__test__/components/component-registry.test.ts:70: resetThemeInitialization: vi.fn(),
web/__test__/components/UserProfile.test.ts:201: // Set the theme using setTheme method
web/__test__/components/UserProfile.test.ts:202: themeStore.setTheme({
web/__test__/components/UserProfile.test.ts:336: themeStore.setTheme({ ...themeStore.theme, descriptionShow: true });
web/__test__/components/UserProfile.test.ts:344: themeStore.setTheme({ ...themeStore.theme, descriptionShow: false });
web/__test__/components/UserProfile.test.ts:351: themeStore.setTheme({ ...themeStore.theme, descriptionShow: true });
web/__test__/components/UserProfile.test.ts:371: themeStore.setTheme({
web/__test__/components/UserProfile.test.ts:381: themeStore.setTheme({
web/__test__/components/UserProfile.test.ts:390: themeStore.setTheme({
web/__test__/components/ThemeSwitcher.test.ts:184: expect(consoleDebugSpy).toHaveBeenCalledWith('[ThemeSwitcher.setTheme] Theme is already set');
web/__test__/components/ThemeSwitcher.test.ts:208: expect(consoleDebugSpy).toHaveBeenCalledWith('[ThemeSwitcher.setTheme] Submitting form');
web/__test__/components/ThemeSwitcher.test.ts:222: expect(consoleLogSpy).toHaveBeenCalledWith('[ThemeSwitcher.setTheme] Theme updated, reloading…');
web/__test__/components/ThemeSwitcher.test.ts:233: const componentThrownErrorMsg = '[ThemeSwitcher.setTheme] Failed to update theme';
web/__test__/components/ThemeSwitcher.test.ts:260: '[ThemeSwitcher.setTheme] Failed to update theme',
</shell_result>
Script executed:
# Check if DevThemeSwitcher has related files
ls -la web/src/components/DevThemeSwitcher*Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
-rw-r--r-- 1 jailuser jailuser 7724 Nov 26 21:30 web/src/components/DevThemeSwitcher.standalone.vue
</shell_result>
Script executed:
# Run codegen to see what would be generated if we had the mutation in a .mutation.ts file
# First, check the current codegen output for any mutation-related exports
rg "SetTheme" web/composables/gql/Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 1
</shell_result>
Script executed:
# Check if the inline mutation matches the GraphQL schema definition
# Compare the structure of SET_THEME_MUTATION with the schema
cat api/generated-schema.graphql | grep -A 20 "setTheme"Repository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
setTheme(
"""Theme to apply"""
theme: ThemeName!
): Theme!
}
"""The theme name"""
enum ThemeName {
azure
black
gray
white
}
"""
Parity check related mutations, WIP, response types and functionaliy will change
"""
type ParityCheckMutations {
"""Start a parity check"""
start(correct: Boolean!): JSON!
</shell_result>
Script executed:
# Look at how other components handle mutations - check ThemeSwitcher.test.ts
grep -B 5 -A 10 "setTheme.*Theme updated" web/__test__/components/ThemeSwitcher.test.tsRepository: unraid/api
<shell_result>
Repository: unraid/api
Exit code: 0
stdout:
expect(mockFormUrlPostRes).toHaveBeenCalledTimes(1);
expect(select.attributes('disabled')).toBeDefined();
expect(mockLocation.reload).not.toHaveBeenCalled();
expect(mockResCallback).toHaveBeenCalledTimes(1);
expect(consoleLogSpy).toHaveBeenCalledWith('[ThemeSwitcher.setTheme] Theme updated, reloading…');
await vi.advanceTimersByTimeAsync(1000);
expect(mockLocation.reload).toHaveBeenCalledTimes(1);
});
it('handles error during WebguiUpdate call', async () => {
const currentTheme = 'azure';
const newTheme = 'black';
const updateError = new Error('Network Error');
</shell_result>
Extract the mutation definition to a separate .mutation.ts file and use generated types.
The inline mutation type definitions violate the .mutation.ts file pattern required in this codebase. Per coding guidelines, GraphQL mutations must be placed in dedicated .mutation.ts files.
Create web/src/components/DevThemeSwitcher.mutation.ts:
+import { graphql } from '~/composables/gql/gql';
+
+export const SET_THEME_MUTATION = graphql(`
+ mutation setTheme($theme: ThemeName!) {
+ customization {
+ setTheme(theme: $theme) {
+ name
+ showBannerImage
+ showBannerGradient
+ headerBackgroundColor
+ showHeaderDescription
+ headerPrimaryTextColor
+ headerSecondaryTextColor
+ }
+ }
+ }
+`);Then in DevThemeSwitcher.standalone.vue, import the generated types and mutation:
-type SetThemeMutationResult = {
- customization: {
- setTheme: {
- name: ThemeName;
- showBannerImage: boolean;
- showBannerGradient: boolean;
- headerBackgroundColor?: string | null;
- showHeaderDescription: boolean;
- headerPrimaryTextColor?: string | null;
- headerSecondaryTextColor?: string | null;
- };
- };
-};
-
-type SetThemeMutationVariables = {
- theme: ThemeName;
-};
-
-const SET_THEME_MUTATION = gql`
- mutation setTheme($theme: ThemeName!) {
- customization {
- setTheme(theme: $theme) {
- name
- showBannerImage
- showBannerGradient
- headerBackgroundColor
- showHeaderDescription
- headerPrimaryTextColor
- headerSecondaryTextColor
- }
- }
- }
-`;
+import type { SetThemeMutation, SetThemeMutationVariables } from '~/composables/gql/graphql';
+import { SET_THEME_MUTATION } from './DevThemeSwitcher.mutation';After creating the .mutation.ts file, run pnpm codegen to generate the types automatically.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In web/src/components/DevThemeSwitcher.standalone.vue around lines 104 to 140
you have inline GraphQL mutation and local TypeScript types; move the GraphQL
mutation string into a new file web/src/components/DevThemeSwitcher.mutation.ts,
export the gql mutation from that file, remove the inline type defs and mutation
from the .vue file, run pnpm codegen to generate the operation types, then
import the generated types and the exported mutation into
DevThemeSwitcher.standalone.vue and call useMutation with those generated types
and the imported mutation.
This improves user experience by ensuring the correct theme is applied based on user preferences.
Summary by CodeRabbit
New Features
Bug Fixes
Improvements
Tests
✏️ Tip: You can customize this high-level summary in your review settings.