mirror of
https://github.com/unraid/api.git
synced 2026-01-03 23:19:54 -06:00
- Removed translation function calls from the UI components for reboot
type text, replacing them with direct references to the computed
properties.
- Enhanced ineligible update messages by integrating localization for
various conditions, ensuring clearer user feedback regarding update
eligibility.
- Added new localization strings for ineligible update scenarios in the
English locale file.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added new localization keys for OS update eligibility, reboot labels,
changelog link, and expanded uptime/trial expiry messages.
* **Bug Fixes**
* Restored translated strings and added locale-aware release date
formatting for update/ineligible messaging and badges.
* **Theme & UI**
* Streamlined theme initialization and server-driven theme application;
removed legacy CSS-variable persistence and adjusted dark/banner
behavior.
* **Tests**
* Added i18n and date/locale formatting tests and improved
local-storage-like test mocks.
* **Chores**
* Removed an auto-registered global component and strengthened
script/theme initialization and CSS-variable validation.
<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
198 lines
6.0 KiB
TypeScript
198 lines
6.0 KiB
TypeScript
import { createApp, createVNode, h, render } from 'vue';
|
|
import { DefaultApolloClient } from '@vue/apollo-composable';
|
|
import UApp from '@nuxt/ui/components/App.vue';
|
|
import ui from '@nuxt/ui/vue-plugin';
|
|
|
|
// Import component registry (only imported here to avoid ordering issues)
|
|
import { componentMappings } from '@/components/Wrapper/component-registry';
|
|
import { client } from '~/helpers/create-apollo-client';
|
|
import { createI18nInstance, ensureLocale, getWindowLocale } from '~/helpers/i18n-loader';
|
|
|
|
// Import Pinia for use in Vue apps
|
|
import { globalPinia } from '~/store/globalPinia';
|
|
|
|
// Ensure Apollo client is singleton
|
|
const apolloClient = (typeof window !== 'undefined' && window.apolloClient) || client;
|
|
|
|
// Expose globally for debugging
|
|
declare global {
|
|
interface Window {
|
|
globalPinia: typeof globalPinia;
|
|
LOCALE?: string;
|
|
}
|
|
}
|
|
|
|
if (typeof window !== 'undefined') {
|
|
window.globalPinia = globalPinia;
|
|
}
|
|
|
|
async function setupI18n() {
|
|
const i18n = createI18nInstance();
|
|
await ensureLocale(i18n, getWindowLocale());
|
|
return i18n;
|
|
}
|
|
|
|
// Helper function to parse props from HTML attributes
|
|
function parsePropsFromElement(element: Element): Record<string, unknown> {
|
|
// Early exit if no attributes
|
|
if (!element.hasAttributes()) return {};
|
|
|
|
const props: Record<string, unknown> = {};
|
|
// Pre-compile attribute skip list into a Set for O(1) lookup
|
|
const skipAttrs = new Set(['class', 'id', 'style', 'data-vue-mounted']);
|
|
|
|
for (const attr of element.attributes) {
|
|
const name = attr.name;
|
|
|
|
// Skip Vue internal attributes and common HTML attributes
|
|
if (skipAttrs.has(name) || name.startsWith('data-v-')) {
|
|
continue;
|
|
}
|
|
|
|
const value = attr.value;
|
|
const first = value.trimStart()[0];
|
|
|
|
// Try to parse JSON values (handles HTML-encoded JSON)
|
|
if (first === '{' || first === '[') {
|
|
try {
|
|
// Decode HTML entities first
|
|
const decoded = value
|
|
.replace(/"/g, '"')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/'/g, "'");
|
|
props[name] = JSON.parse(decoded);
|
|
} catch (_e) {
|
|
// If JSON parsing fails, use as string
|
|
props[name] = value;
|
|
}
|
|
} else {
|
|
props[name] = value;
|
|
}
|
|
}
|
|
|
|
return props;
|
|
}
|
|
|
|
// Create and mount unified app with shared context
|
|
export async function mountUnifiedApp() {
|
|
// Create a minimal app just for context sharing
|
|
const app = createApp({
|
|
name: 'UnifiedContextApp',
|
|
render: () => h('div', 'Context Provider'),
|
|
});
|
|
|
|
// Setup everything once
|
|
const i18n = await setupI18n();
|
|
app.use(i18n);
|
|
app.use(globalPinia);
|
|
app.use(ui);
|
|
app.provide(DefaultApolloClient, apolloClient);
|
|
|
|
// Mount the app to establish context
|
|
let rootElement = document.getElementById('unraid-unified-root');
|
|
if (!rootElement) {
|
|
rootElement = document.createElement('div');
|
|
rootElement.id = 'unraid-unified-root';
|
|
rootElement.style.display = 'none';
|
|
document.body.appendChild(rootElement);
|
|
}
|
|
app.mount(rootElement);
|
|
|
|
// Now render components to their locations using the shared context
|
|
const mountedComponents: Array<{ element: HTMLElement; unmount: () => void }> = [];
|
|
|
|
// Batch all selector queries first to identify which components are needed
|
|
const componentsToMount: Array<{ mapping: (typeof componentMappings)[0]; element: HTMLElement }> = [];
|
|
|
|
// Build a map of all selectors to their mappings for quick lookup
|
|
const selectorToMapping = new Map<string, (typeof componentMappings)[0]>();
|
|
componentMappings.forEach((mapping) => {
|
|
const selectors = Array.isArray(mapping.selector) ? mapping.selector : [mapping.selector];
|
|
selectors.forEach((sel) => selectorToMapping.set(sel, mapping));
|
|
});
|
|
|
|
// Query all selectors at once
|
|
const allSelectors = Array.from(selectorToMapping.keys()).join(',');
|
|
|
|
// Early exit if no selectors to query
|
|
if (!allSelectors) {
|
|
console.debug('[UnifiedMount] Mounted 0 components');
|
|
return app;
|
|
}
|
|
|
|
const foundElements = document.querySelectorAll(allSelectors);
|
|
const processedMappings = new Set<(typeof componentMappings)[0]>();
|
|
|
|
foundElements.forEach((element) => {
|
|
if (!element.hasAttribute('data-vue-mounted')) {
|
|
// Find which mapping this element belongs to
|
|
for (const [selector, mapping] of selectorToMapping) {
|
|
if (element.matches(selector) && !processedMappings.has(mapping)) {
|
|
componentsToMount.push({ mapping, element: element as HTMLElement });
|
|
processedMappings.add(mapping);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Now mount only the components that exist
|
|
componentsToMount.forEach(({ mapping, element }) => {
|
|
const { appId } = mapping;
|
|
const component = mapping.component;
|
|
|
|
// Skip if no component is defined
|
|
if (!component) {
|
|
console.error(`[UnifiedMount] No component defined for ${appId}`);
|
|
return;
|
|
}
|
|
|
|
// Parse props from element
|
|
const props = parsePropsFromElement(element);
|
|
|
|
// Wrap component in UApp for Nuxt UI support
|
|
const wrappedComponent = {
|
|
name: `${appId}-wrapper`,
|
|
setup() {
|
|
return () =>
|
|
h(
|
|
UApp,
|
|
{},
|
|
{
|
|
default: () => h(component, props),
|
|
}
|
|
);
|
|
},
|
|
};
|
|
|
|
// Create vnode with shared app context
|
|
const vnode = createVNode(wrappedComponent);
|
|
vnode.appContext = app._context; // Share the app context
|
|
|
|
// Clear the element and render the component into it
|
|
element.replaceChildren();
|
|
render(vnode, element);
|
|
|
|
// Mark as mounted
|
|
element.setAttribute('data-vue-mounted', 'true');
|
|
element.classList.add('unapi');
|
|
|
|
// Store for cleanup
|
|
mountedComponents.push({
|
|
element,
|
|
unmount: () => render(null, element),
|
|
});
|
|
});
|
|
|
|
console.debug(`[UnifiedMount] Mounted ${mountedComponents.length} components`);
|
|
|
|
return app;
|
|
}
|
|
|
|
// Replace the old autoMountAllComponents with the new unified approach
|
|
export async function autoMountAllComponents() {
|
|
return await mountUnifiedApp();
|
|
}
|