mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
fix: UnraidToaster component and update dialog close button (#1657)
- Introduced a new UnraidToaster component for displaying notifications with customizable positions. - Updated the DialogClose component to use a span element for better semantic structure. - Enhanced CSS for the sonner component to ensure proper layout and styling. These changes improve user feedback through notifications and refine the dialog close button's implementation. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a toaster notifications component with configurable screen position, rich colors, and a close button; programmatic and legacy mounting helpers exposed. * **Style** * Updated toast close-button spacing and min-width behavior. * Simplified dialog close-button rendering and removed redundant style resets. * Reduced SSO provider icon size and added SSO button font-size tokens. * **Tests** * Added unit tests covering component mounting and global exports. * **Chores** * Deployment now performs broader remote cleanup before syncing. * **Chores** * Type declarations and tsconfig updated for global mount/utility typings. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -76,4 +76,21 @@ body {
|
|||||||
button:not(:disabled),
|
button:not(:disabled),
|
||||||
[role='button']:not(:disabled) {
|
[role='button']:not(:disabled) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Font size overrides for SSO button component */
|
||||||
|
unraid-sso-button {
|
||||||
|
--text-xs: 0.75rem;
|
||||||
|
--text-sm: 0.875rem;
|
||||||
|
--text-base: 1rem;
|
||||||
|
--text-lg: 1.125rem;
|
||||||
|
--text-xl: 1.25rem;
|
||||||
|
--text-2xl: 1.5rem;
|
||||||
|
--text-3xl: 1.875rem;
|
||||||
|
--text-4xl: 2.25rem;
|
||||||
|
--text-5xl: 3rem;
|
||||||
|
--text-6xl: 3.75rem;
|
||||||
|
--text-7xl: 4.5rem;
|
||||||
|
--text-8xl: 6rem;
|
||||||
|
--text-9xl: 8rem;
|
||||||
}
|
}
|
||||||
@@ -229,6 +229,8 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
min-width: inherit !important;
|
||||||
|
margin: 0 !important;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -5,32 +5,7 @@ const props = defineProps<DialogCloseProps>();
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<DialogClose v-bind="props">
|
<DialogClose v-bind="props" as="span">
|
||||||
<slot />
|
<slot />
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Reset webgui button styles for dialog close buttons */
|
|
||||||
[role='dialog'] button[type='button'],
|
|
||||||
button[aria-label*='close' i],
|
|
||||||
button[aria-label*='dismiss' i] {
|
|
||||||
/* Reset ALL webgui button styles using !important where needed */
|
|
||||||
all: unset !important;
|
|
||||||
|
|
||||||
/* Re-apply necessary styles after reset */
|
|
||||||
display: inline-flex !important;
|
|
||||||
font-family: inherit !important;
|
|
||||||
font-size: inherit !important;
|
|
||||||
font-weight: inherit !important;
|
|
||||||
line-height: inherit !important;
|
|
||||||
cursor: pointer !important;
|
|
||||||
box-sizing: border-box !important;
|
|
||||||
|
|
||||||
/* Reset any webgui CSS variables */
|
|
||||||
--button-border: none !important;
|
|
||||||
--button-text-color: inherit !important;
|
|
||||||
--button-background: transparent !important;
|
|
||||||
--button-background-size: auto !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
278
web/__test__/components/standalone-mount.test.ts
Normal file
278
web/__test__/components/standalone-mount.test.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import { beforeEach, afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
// Mock all the component imports
|
||||||
|
vi.mock('~/components/Auth.ce.vue', () => ({
|
||||||
|
default: { name: 'MockAuth', template: '<div>Auth</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/ConnectSettings/ConnectSettings.ce.vue', () => ({
|
||||||
|
default: { name: 'MockConnectSettings', template: '<div>ConnectSettings</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/DownloadApiLogs.ce.vue', () => ({
|
||||||
|
default: { name: 'MockDownloadApiLogs', template: '<div>DownloadApiLogs</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/HeaderOsVersion.ce.vue', () => ({
|
||||||
|
default: { name: 'MockHeaderOsVersion', template: '<div>HeaderOsVersion</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/Modals.ce.vue', () => ({
|
||||||
|
default: { name: 'MockModals', template: '<div>Modals</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/UserProfile.ce.vue', () => ({
|
||||||
|
default: { name: 'MockUserProfile', template: '<div>UserProfile</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/UpdateOs.ce.vue', () => ({
|
||||||
|
default: { name: 'MockUpdateOs', template: '<div>UpdateOs</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/DowngradeOs.ce.vue', () => ({
|
||||||
|
default: { name: 'MockDowngradeOs', template: '<div>DowngradeOs</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/Registration.ce.vue', () => ({
|
||||||
|
default: { name: 'MockRegistration', template: '<div>Registration</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/WanIpCheck.ce.vue', () => ({
|
||||||
|
default: { name: 'MockWanIpCheck', template: '<div>WanIpCheck</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/Activation/WelcomeModal.ce.vue', () => ({
|
||||||
|
default: { name: 'MockWelcomeModal', template: '<div>WelcomeModal</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/SsoButton.ce.vue', () => ({
|
||||||
|
default: { name: 'MockSsoButton', template: '<div>SsoButton</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/Logs/LogViewer.ce.vue', () => ({
|
||||||
|
default: { name: 'MockLogViewer', template: '<div>LogViewer</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/ThemeSwitcher.ce.vue', () => ({
|
||||||
|
default: { name: 'MockThemeSwitcher', template: '<div>ThemeSwitcher</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/ApiKeyPage.ce.vue', () => ({
|
||||||
|
default: { name: 'MockApiKeyPage', template: '<div>ApiKeyPage</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/DevModalTest.ce.vue', () => ({
|
||||||
|
default: { name: 'MockDevModalTest', template: '<div>DevModalTest</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/ApiKeyAuthorize.ce.vue', () => ({
|
||||||
|
default: { name: 'MockApiKeyAuthorize', template: '<div>ApiKeyAuthorize</div>' }
|
||||||
|
}));
|
||||||
|
vi.mock('~/components/UnraidToaster.vue', () => ({
|
||||||
|
default: { name: 'MockUnraidToaster', template: '<div>UnraidToaster</div>' }
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock vue-mount-app module
|
||||||
|
const mockAutoMountComponent = vi.fn();
|
||||||
|
const mockMountVueApp = vi.fn();
|
||||||
|
const mockGetMountedApp = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('~/components/Wrapper/vue-mount-app', () => ({
|
||||||
|
autoMountComponent: mockAutoMountComponent,
|
||||||
|
mountVueApp: mockMountVueApp,
|
||||||
|
getMountedApp: mockGetMountedApp,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock theme store
|
||||||
|
const mockSetTheme = vi.fn();
|
||||||
|
const mockSetCssVars = vi.fn();
|
||||||
|
const mockUseThemeStore = vi.fn(() => ({
|
||||||
|
setTheme: mockSetTheme,
|
||||||
|
setCssVars: mockSetCssVars,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('~/store/theme', () => ({
|
||||||
|
useThemeStore: mockUseThemeStore,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock globalPinia
|
||||||
|
vi.mock('~/store/globalPinia', () => ({
|
||||||
|
globalPinia: { state: {} },
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock apollo client
|
||||||
|
const mockApolloClient = {
|
||||||
|
query: vi.fn(),
|
||||||
|
mutate: vi.fn(),
|
||||||
|
};
|
||||||
|
vi.mock('~/helpers/create-apollo-client', () => ({
|
||||||
|
client: mockApolloClient,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock @vue/apollo-composable
|
||||||
|
const mockProvideApolloClient = vi.fn();
|
||||||
|
vi.mock('@vue/apollo-composable', () => ({
|
||||||
|
provideApolloClient: mockProvideApolloClient,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock graphql
|
||||||
|
const mockParse = vi.fn();
|
||||||
|
vi.mock('graphql', () => ({
|
||||||
|
parse: mockParse,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock @unraid/ui
|
||||||
|
const mockEnsureTeleportContainer = vi.fn();
|
||||||
|
vi.mock('@unraid/ui', () => ({
|
||||||
|
ensureTeleportContainer: mockEnsureTeleportContainer,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('standalone-mount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset module cache to ensure fresh imports
|
||||||
|
vi.resetModules();
|
||||||
|
|
||||||
|
// Reset all mocks
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// Use Vitest's unstubAllGlobals to clean up any global stubs from previous tests
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
|
||||||
|
// Mock document methods
|
||||||
|
vi.spyOn(document.head, 'appendChild').mockImplementation(() => document.createElement('style'));
|
||||||
|
vi.spyOn(document, 'addEventListener').mockImplementation(() => {});
|
||||||
|
|
||||||
|
// Clear DOM
|
||||||
|
document.head.innerHTML = '';
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
vi.resetModules();
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initialization', () => {
|
||||||
|
|
||||||
|
it('should set up Apollo client globally', async () => {
|
||||||
|
await import('~/components/standalone-mount');
|
||||||
|
|
||||||
|
expect(window.apolloClient).toBe(mockApolloClient);
|
||||||
|
expect(window.graphqlParse).toBe(mockParse);
|
||||||
|
expect(window.gql).toBe(mockParse);
|
||||||
|
expect(mockProvideApolloClient).toHaveBeenCalledWith(mockApolloClient);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize theme store', async () => {
|
||||||
|
await import('~/components/standalone-mount');
|
||||||
|
|
||||||
|
expect(mockUseThemeStore).toHaveBeenCalled();
|
||||||
|
expect(mockSetTheme).toHaveBeenCalled();
|
||||||
|
expect(mockSetCssVars).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ensure teleport container exists', async () => {
|
||||||
|
await import('~/components/standalone-mount');
|
||||||
|
|
||||||
|
expect(mockEnsureTeleportContainer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('component auto-mounting', () => {
|
||||||
|
it('should auto-mount all defined components', async () => {
|
||||||
|
await import('~/components/standalone-mount');
|
||||||
|
|
||||||
|
// Verify that autoMountComponent was called multiple times
|
||||||
|
expect(mockAutoMountComponent.mock.calls.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
// Verify all calls have the correct structure
|
||||||
|
mockAutoMountComponent.mock.calls.forEach(call => {
|
||||||
|
expect(call[0]).toBeDefined(); // Component
|
||||||
|
expect(call[1]).toBeDefined(); // Selector
|
||||||
|
expect(call[2]).toMatchObject({
|
||||||
|
appId: expect.any(String),
|
||||||
|
useShadowRoot: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract all selectors that were mounted
|
||||||
|
const mountedSelectors = mockAutoMountComponent.mock.calls.map(call => call[1]);
|
||||||
|
|
||||||
|
// Verify critical components are mounted
|
||||||
|
expect(mountedSelectors).toContain('unraid-auth');
|
||||||
|
expect(mountedSelectors).toContain('unraid-modals');
|
||||||
|
expect(mountedSelectors).toContain('unraid-user-profile');
|
||||||
|
expect(mountedSelectors).toContain('uui-toaster');
|
||||||
|
expect(mountedSelectors).toContain('#modals'); // Legacy modal selector
|
||||||
|
|
||||||
|
// Verify no shadow DOM is used
|
||||||
|
const allUseShadowRoot = mockAutoMountComponent.mock.calls.every(
|
||||||
|
call => call[2].useShadowRoot === false
|
||||||
|
);
|
||||||
|
expect(allUseShadowRoot).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('global exports', () => {
|
||||||
|
it('should expose UnraidComponents globally', async () => {
|
||||||
|
await import('~/components/standalone-mount');
|
||||||
|
|
||||||
|
expect(window.UnraidComponents).toBeDefined();
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('Auth');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('ConnectSettings');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('DownloadApiLogs');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('HeaderOsVersion');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('Modals');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('UserProfile');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('UpdateOs');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('DowngradeOs');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('Registration');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('WanIpCheck');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('WelcomeModal');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('SsoButton');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('LogViewer');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('ThemeSwitcher');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('ApiKeyPage');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('DevModalTest');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('ApiKeyAuthorize');
|
||||||
|
expect(window.UnraidComponents).toHaveProperty('UnraidToaster');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expose utility functions globally', async () => {
|
||||||
|
await import('~/components/standalone-mount');
|
||||||
|
|
||||||
|
expect(window.mountVueApp).toBe(mockMountVueApp);
|
||||||
|
expect(window.getMountedApp).toBe(mockGetMountedApp);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create dynamic mount functions for each component', async () => {
|
||||||
|
await import('~/components/standalone-mount');
|
||||||
|
|
||||||
|
// Check for some dynamic mount functions
|
||||||
|
expect(typeof window.mountAuth).toBe('function');
|
||||||
|
expect(typeof window.mountConnectSettings).toBe('function');
|
||||||
|
expect(typeof window.mountUserProfile).toBe('function');
|
||||||
|
expect(typeof window.mountModals).toBe('function');
|
||||||
|
expect(typeof window.mountThemeSwitcher).toBe('function');
|
||||||
|
|
||||||
|
// Test calling a dynamic mount function
|
||||||
|
const customSelector = '#custom-auth';
|
||||||
|
window.mountAuth?.(customSelector);
|
||||||
|
|
||||||
|
expect(mockMountVueApp).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
selector: customSelector,
|
||||||
|
useShadowRoot: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use default selector when no custom selector provided', async () => {
|
||||||
|
await import('~/components/standalone-mount');
|
||||||
|
|
||||||
|
// Call mount function without custom selector
|
||||||
|
window.mountAuth?.();
|
||||||
|
|
||||||
|
expect(mockMountVueApp).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
selector: 'unraid-auth',
|
||||||
|
useShadowRoot: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skip SSR safety test as it's complex to test with module isolation
|
||||||
|
describe.skip('SSR safety', () => {
|
||||||
|
it('should not initialize when window is undefined', async () => {
|
||||||
|
// This test is skipped because the module initialization happens at import time
|
||||||
|
// and it's difficult to properly isolate the window object manipulation
|
||||||
|
// The functionality is simple enough - just checking if window exists before running code
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
11
web/components/UnraidToaster.vue
Normal file
11
web/components/UnraidToaster.vue
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { Toaster } from '@unraid/ui';
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
position: 'top-center' | 'top-right' | 'top-left' | 'bottom-center' | 'bottom-right' | 'bottom-left';
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Toaster rich-colors close-button :position="position" />
|
||||||
|
</template>
|
||||||
@@ -27,7 +27,7 @@ const handleClick = () => {
|
|||||||
<img
|
<img
|
||||||
v-if="props.provider.buttonIcon"
|
v-if="props.provider.buttonIcon"
|
||||||
:src="props.provider.buttonIcon"
|
:src="props.provider.buttonIcon"
|
||||||
class="w-6 h-6 sso-button-icon flex-shrink-0"
|
class="w-4 h-4 sso-button-icon flex-shrink-0"
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// Import all components
|
// Import all components
|
||||||
import type { Component } from 'vue';
|
|
||||||
import Auth from './Auth.ce.vue';
|
import Auth from './Auth.ce.vue';
|
||||||
import ConnectSettings from './ConnectSettings/ConnectSettings.ce.vue';
|
import ConnectSettings from './ConnectSettings/ConnectSettings.ce.vue';
|
||||||
import DownloadApiLogs from './DownloadApiLogs.ce.vue';
|
import DownloadApiLogs from './DownloadApiLogs.ce.vue';
|
||||||
@@ -17,7 +16,7 @@ import ThemeSwitcher from './ThemeSwitcher.ce.vue';
|
|||||||
import ApiKeyPage from './ApiKeyPage.ce.vue';
|
import ApiKeyPage from './ApiKeyPage.ce.vue';
|
||||||
import DevModalTest from './DevModalTest.ce.vue';
|
import DevModalTest from './DevModalTest.ce.vue';
|
||||||
import ApiKeyAuthorize from './ApiKeyAuthorize.ce.vue';
|
import ApiKeyAuthorize from './ApiKeyAuthorize.ce.vue';
|
||||||
|
import UnraidToaster from './UnraidToaster.vue';
|
||||||
// Import utilities
|
// Import utilities
|
||||||
import { autoMountComponent, mountVueApp, getMountedApp } from './Wrapper/vue-mount-app';
|
import { autoMountComponent, mountVueApp, getMountedApp } from './Wrapper/vue-mount-app';
|
||||||
import { useThemeStore } from '~/store/theme';
|
import { useThemeStore } from '~/store/theme';
|
||||||
@@ -27,92 +26,11 @@ import { provideApolloClient } from '@vue/apollo-composable';
|
|||||||
import { parse } from 'graphql';
|
import { parse } from 'graphql';
|
||||||
import { ensureTeleportContainer } from '@unraid/ui';
|
import { ensureTeleportContainer } from '@unraid/ui';
|
||||||
|
|
||||||
// Extend window interface for Apollo client
|
// Window type definitions are automatically included via tsconfig.json
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
apolloClient: typeof apolloClient;
|
|
||||||
gql: typeof parse;
|
|
||||||
graphqlParse: typeof parse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add pre-render CSS to hide components until they're mounted
|
|
||||||
function injectPreRenderCSS() {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.id = 'unraid-prerender-css';
|
|
||||||
style.textContent = `
|
|
||||||
/* Hide unraid components during initial load to prevent FOUC */
|
|
||||||
unraid-auth,
|
|
||||||
unraid-connect-settings,
|
|
||||||
unraid-download-api-logs,
|
|
||||||
unraid-header-os-version,
|
|
||||||
unraid-modals,
|
|
||||||
unraid-user-profile,
|
|
||||||
unraid-update-os,
|
|
||||||
unraid-downgrade-os,
|
|
||||||
unraid-registration,
|
|
||||||
unraid-wan-ip-check,
|
|
||||||
unraid-welcome-modal,
|
|
||||||
unraid-sso-button,
|
|
||||||
unraid-log-viewer,
|
|
||||||
unraid-theme-switcher,
|
|
||||||
unraid-api-key-manager,
|
|
||||||
unraid-dev-modal-test,
|
|
||||||
unraid-api-key-authorize {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Show components once they have the unapi class (mounted) */
|
|
||||||
unraid-auth.unapi,
|
|
||||||
unraid-connect-settings.unapi,
|
|
||||||
unraid-download-api-logs.unapi,
|
|
||||||
unraid-header-os-version.unapi,
|
|
||||||
unraid-modals.unapi,
|
|
||||||
unraid-user-profile.unapi,
|
|
||||||
unraid-update-os.unapi,
|
|
||||||
unraid-downgrade-os.unapi,
|
|
||||||
unraid-registration.unapi,
|
|
||||||
unraid-wan-ip-check.unapi,
|
|
||||||
unraid-welcome-modal.unapi,
|
|
||||||
unraid-sso-button.unapi,
|
|
||||||
unraid-log-viewer.unapi,
|
|
||||||
unraid-theme-switcher.unapi,
|
|
||||||
unraid-api-key-manager.unapi,
|
|
||||||
unraid-dev-modal-test.unapi,
|
|
||||||
unraid-api-key-authorize.unapi {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Font size overrides for SSO button component */
|
|
||||||
unraid-sso-button {
|
|
||||||
--text-xs: 0.75rem;
|
|
||||||
--text-sm: 0.875rem;
|
|
||||||
--text-base: 1rem;
|
|
||||||
--text-lg: 1.125rem;
|
|
||||||
--text-xl: 1.25rem;
|
|
||||||
--text-2xl: 1.5rem;
|
|
||||||
--text-3xl: 1.875rem;
|
|
||||||
--text-4xl: 2.25rem;
|
|
||||||
--text-5xl: 3rem;
|
|
||||||
--text-6xl: 3.75rem;
|
|
||||||
--text-7xl: 4.5rem;
|
|
||||||
--text-8xl: 6rem;
|
|
||||||
--text-9xl: 8rem;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize global Apollo client context
|
// Initialize global Apollo client context
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// Inject pre-render CSS as early as possible
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', injectPreRenderCSS);
|
|
||||||
} else {
|
|
||||||
injectPreRenderCSS();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make Apollo client globally available
|
// Make Apollo client globally available
|
||||||
window.apolloClient = apolloClient;
|
window.apolloClient = apolloClient;
|
||||||
|
|
||||||
@@ -140,6 +58,7 @@ const componentMappings = [
|
|||||||
{ component: DownloadApiLogs, selector: 'unraid-download-api-logs', appId: 'download-api-logs' },
|
{ component: DownloadApiLogs, selector: 'unraid-download-api-logs', appId: 'download-api-logs' },
|
||||||
{ component: HeaderOsVersion, selector: 'unraid-header-os-version', appId: 'header-os-version' },
|
{ component: HeaderOsVersion, selector: 'unraid-header-os-version', appId: 'header-os-version' },
|
||||||
{ component: Modals, selector: 'unraid-modals', appId: 'modals' },
|
{ component: Modals, selector: 'unraid-modals', appId: 'modals' },
|
||||||
|
{ component: Modals, selector: '#modals', appId: 'modals-legacy' }, // Legacy ID selector
|
||||||
{ component: UserProfile, selector: 'unraid-user-profile', appId: 'user-profile' },
|
{ component: UserProfile, selector: 'unraid-user-profile', appId: 'user-profile' },
|
||||||
{ component: UpdateOs, selector: 'unraid-update-os', appId: 'update-os' },
|
{ component: UpdateOs, selector: 'unraid-update-os', appId: 'update-os' },
|
||||||
{ component: DowngradeOs, selector: 'unraid-downgrade-os', appId: 'downgrade-os' },
|
{ component: DowngradeOs, selector: 'unraid-downgrade-os', appId: 'downgrade-os' },
|
||||||
@@ -152,6 +71,8 @@ const componentMappings = [
|
|||||||
{ component: ApiKeyPage, selector: 'unraid-api-key-manager', appId: 'api-key-manager' },
|
{ component: ApiKeyPage, selector: 'unraid-api-key-manager', appId: 'api-key-manager' },
|
||||||
{ component: DevModalTest, selector: 'unraid-dev-modal-test', appId: 'dev-modal-test' },
|
{ component: DevModalTest, selector: 'unraid-dev-modal-test', appId: 'dev-modal-test' },
|
||||||
{ component: ApiKeyAuthorize, selector: 'unraid-api-key-authorize', appId: 'api-key-authorize' },
|
{ component: ApiKeyAuthorize, selector: 'unraid-api-key-authorize', appId: 'api-key-authorize' },
|
||||||
|
{ component: UnraidToaster, selector: 'uui-toaster', appId: 'toaster' },
|
||||||
|
{ component: UnraidToaster, selector: 'unraid-toaster', appId: 'toaster-legacy' }, // Legacy alias
|
||||||
];
|
];
|
||||||
|
|
||||||
// Auto-mount all components
|
// Auto-mount all components
|
||||||
@@ -162,20 +83,7 @@ componentMappings.forEach(({ component, selector, appId }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Special handling for Modals - also mount to #modals
|
// Window interface extensions are defined in ~/types/window.d.ts
|
||||||
autoMountComponent(Modals, '#modals', {
|
|
||||||
appId: 'modals-direct',
|
|
||||||
useShadowRoot: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Expose functions globally for testing and dynamic mounting
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
UnraidComponents: Record<string, Component>;
|
|
||||||
mountVueApp: typeof mountVueApp;
|
|
||||||
getMountedApp: typeof getMountedApp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
// Expose all components
|
// Expose all components
|
||||||
@@ -197,6 +105,7 @@ if (typeof window !== 'undefined') {
|
|||||||
ApiKeyPage,
|
ApiKeyPage,
|
||||||
DevModalTest,
|
DevModalTest,
|
||||||
ApiKeyAuthorize,
|
ApiKeyAuthorize,
|
||||||
|
UnraidToaster,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Expose utility functions
|
// Expose utility functions
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ if [ "$has_standalone" = true ]; then
|
|||||||
# Ensure remote directory exists
|
# Ensure remote directory exists
|
||||||
ssh root@"${server_name}" "mkdir -p /usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/"
|
ssh root@"${server_name}" "mkdir -p /usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/"
|
||||||
# Clear the remote standalone directory before rsyncing
|
# Clear the remote standalone directory before rsyncing
|
||||||
ssh root@"${server_name}" "rm -rf /usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/*"
|
ssh root@"${server_name}" "rm -rf /usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/*"
|
||||||
# Run rsync with proper quoting
|
# Run rsync with proper quoting
|
||||||
rsync -avz --delete -e "ssh" "$standalone_directory" "root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/"
|
rsync -avz --delete -e "ssh" "$standalone_directory" "root@${server_name}:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone/"
|
||||||
standalone_exit_code=$?
|
standalone_exit_code=$?
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
"extends": "./.nuxt/tsconfig.json"
|
"extends": "./.nuxt/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["./types/window"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
web/types/window.d.ts
vendored
Normal file
52
web/types/window.d.ts
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { Component } from 'vue';
|
||||||
|
import type { parse } from 'graphql';
|
||||||
|
import type { client as apolloClient } from '~/helpers/create-apollo-client';
|
||||||
|
import type { mountVueApp, getMountedApp } from '~/components/Wrapper/vue-mount-app';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global Window interface extensions for Unraid components
|
||||||
|
* This file provides type definitions for properties added to the window object
|
||||||
|
* by the standalone-mount.ts module
|
||||||
|
*/
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
// Apollo GraphQL client and utilities
|
||||||
|
apolloClient: typeof apolloClient;
|
||||||
|
gql: typeof parse;
|
||||||
|
graphqlParse: typeof parse;
|
||||||
|
|
||||||
|
// Vue component registry and utilities
|
||||||
|
UnraidComponents: Record<string, Component>;
|
||||||
|
mountVueApp: typeof mountVueApp;
|
||||||
|
getMountedApp: typeof getMountedApp;
|
||||||
|
|
||||||
|
// Dynamic mount functions created at runtime
|
||||||
|
// These are generated for each component in componentMappings
|
||||||
|
mountAuth?: (selector?: string) => unknown;
|
||||||
|
mountConnectSettings?: (selector?: string) => unknown;
|
||||||
|
mountDownloadApiLogs?: (selector?: string) => unknown;
|
||||||
|
mountHeaderOsVersion?: (selector?: string) => unknown;
|
||||||
|
mountModals?: (selector?: string) => unknown;
|
||||||
|
mountModalsLegacy?: (selector?: string) => unknown;
|
||||||
|
mountUserProfile?: (selector?: string) => unknown;
|
||||||
|
mountUpdateOs?: (selector?: string) => unknown;
|
||||||
|
mountDowngradeOs?: (selector?: string) => unknown;
|
||||||
|
mountRegistration?: (selector?: string) => unknown;
|
||||||
|
mountWanIpCheck?: (selector?: string) => unknown;
|
||||||
|
mountWelcomeModal?: (selector?: string) => unknown;
|
||||||
|
mountSsoButton?: (selector?: string) => unknown;
|
||||||
|
mountLogViewer?: (selector?: string) => unknown;
|
||||||
|
mountThemeSwitcher?: (selector?: string) => unknown;
|
||||||
|
mountApiKeyManager?: (selector?: string) => unknown;
|
||||||
|
mountDevModalTest?: (selector?: string) => unknown;
|
||||||
|
mountApiKeyAuthorize?: (selector?: string) => unknown;
|
||||||
|
mountToaster?: (selector?: string) => unknown;
|
||||||
|
mountToasterLegacy?: (selector?: string) => unknown;
|
||||||
|
|
||||||
|
// Index signature for any other dynamic mount functions
|
||||||
|
[key: `mount${string}`]: ((selector?: string) => unknown) | undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export empty object to make this a module and enable augmentation
|
||||||
|
export {};
|
||||||
Reference in New Issue
Block a user