mirror of
https://github.com/unraid/api.git
synced 2026-01-06 08:39:54 -06:00
Compare commits
2 Commits
4.23.0-bui
...
4.23.1-bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8b166e4b6 | ||
|
|
8b862ecef5 |
@@ -1 +1 @@
|
||||
{".":"4.23.0"}
|
||||
{".":"4.23.1"}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## [4.23.1](https://github.com/unraid/api/compare/v4.23.0...v4.23.1) (2025-09-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cleanup ini parser logic with better fallbacks ([#1713](https://github.com/unraid/api/issues/1713)) ([1691362](https://github.com/unraid/api/commit/16913627de9497a5d2f71edb710cec6e2eb9f890))
|
||||
|
||||
## [4.23.0](https://github.com/unraid/api/compare/v4.22.2...v4.23.0) (2025-09-16)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/api",
|
||||
"version": "4.23.0",
|
||||
"version": "4.23.1",
|
||||
"main": "src/cli/index.ts",
|
||||
"type": "module",
|
||||
"corepack": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "unraid-monorepo",
|
||||
"private": true,
|
||||
"version": "4.23.0",
|
||||
"version": "4.23.1",
|
||||
"scripts": {
|
||||
"build": "pnpm -r build",
|
||||
"build:watch": "pnpm -r --parallel --filter '!@unraid/ui' build:watch",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/connect-plugin",
|
||||
"version": "4.23.0",
|
||||
"version": "4.23.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"commander": "14.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/ui",
|
||||
"version": "4.23.0",
|
||||
"version": "4.23.1",
|
||||
"private": true,
|
||||
"license": "GPL-2.0-or-later",
|
||||
"type": "module",
|
||||
|
||||
@@ -105,12 +105,7 @@ describe('mount-engine', () => {
|
||||
vi.restoreAllMocks();
|
||||
document.body.innerHTML = '';
|
||||
// Clean up global references
|
||||
if (window.__unifiedApp) {
|
||||
delete window.__unifiedApp;
|
||||
}
|
||||
if (window.__mountedComponents) {
|
||||
delete window.__mountedComponents;
|
||||
}
|
||||
// Clean up any window references if needed
|
||||
});
|
||||
|
||||
describe('mountUnifiedApp', () => {
|
||||
@@ -438,29 +433,6 @@ describe('mount-engine', () => {
|
||||
});
|
||||
|
||||
describe('global exposure', () => {
|
||||
it('should expose unified app globally', () => {
|
||||
const app = mountUnifiedApp();
|
||||
expect(window.__unifiedApp).toBe(app);
|
||||
});
|
||||
|
||||
it('should expose mounted components globally', () => {
|
||||
const element = document.createElement('div');
|
||||
element.id = 'global-app';
|
||||
document.body.appendChild(element);
|
||||
|
||||
mockComponentMappings.push({
|
||||
selector: '#global-app',
|
||||
appId: 'global-app',
|
||||
component: TestComponent,
|
||||
});
|
||||
|
||||
mountUnifiedApp();
|
||||
|
||||
expect(window.__mountedComponents).toBeDefined();
|
||||
expect(Array.isArray(window.__mountedComponents)).toBe(true);
|
||||
expect(window.__mountedComponents!.length).toBe(1);
|
||||
});
|
||||
|
||||
it('should expose globalPinia globally', () => {
|
||||
expect(window.globalPinia).toBeDefined();
|
||||
expect(window.globalPinia).toBe(mockGlobalPinia);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/web",
|
||||
"version": "4.23.0",
|
||||
"version": "4.23.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "GPL-2.0-or-later",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/* Import theme and utilities only - no global preflight */
|
||||
@import "tailwindcss/theme.css" layer(theme);
|
||||
@import "tailwindcss/utilities.css" layer(utilities);
|
||||
@import "@nuxt/ui";
|
||||
/* @import "@nuxt/ui"; temporarily disabled */
|
||||
@import 'tw-animate-css';
|
||||
@import '../../../@tailwind-shared/index.css';
|
||||
|
||||
|
||||
@@ -23,9 +23,6 @@ function initializeGlobalDependencies() {
|
||||
});
|
||||
|
||||
// Expose utility functions on window for debugging/external use
|
||||
// With unified app, these are no longer needed
|
||||
// Access the unified app via window.__unifiedApp instead
|
||||
|
||||
// Expose Apollo client on window for global access
|
||||
window.apolloClient = apolloClient;
|
||||
|
||||
|
||||
@@ -10,8 +10,6 @@ import { client } from '~/helpers/create-apollo-client';
|
||||
import { createHtmlEntityDecoder } from '~/helpers/i18n-utils';
|
||||
import en_US from '~/locales/en_US.json';
|
||||
|
||||
import type { App as VueApp } from 'vue';
|
||||
|
||||
// Import Pinia for use in Vue apps
|
||||
import { globalPinia } from '~/store/globalPinia';
|
||||
|
||||
@@ -22,7 +20,7 @@ const apolloClient = (typeof window !== 'undefined' && window.apolloClient) || c
|
||||
declare global {
|
||||
interface Window {
|
||||
globalPinia: typeof globalPinia;
|
||||
__unifiedApp?: VueApp;
|
||||
LOCALE_DATA?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +36,7 @@ function setupI18n() {
|
||||
|
||||
// Check for window locale data
|
||||
if (typeof window !== 'undefined') {
|
||||
const windowLocaleData = (window as unknown as { LOCALE_DATA?: string }).LOCALE_DATA || null;
|
||||
const windowLocaleData = window.LOCALE_DATA || null;
|
||||
if (windowLocaleData) {
|
||||
try {
|
||||
parsedMessages = JSON.parse(decodeURIComponent(windowLocaleData));
|
||||
@@ -64,19 +62,26 @@ function setupI18n() {
|
||||
|
||||
// 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;
|
||||
const value = attr.value;
|
||||
|
||||
// Skip Vue internal attributes and common HTML attributes
|
||||
if (name.startsWith('data-v-') || name === 'class' || name === 'id' || name === 'style') {
|
||||
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 (value.startsWith('{') || value.startsWith('[')) {
|
||||
if (first === '{' || first === '[') {
|
||||
try {
|
||||
// Decode HTML entities first
|
||||
const decoded = value
|
||||
@@ -126,75 +131,95 @@ export function mountUnifiedApp() {
|
||||
// Now render components to their locations using the shared context
|
||||
const mountedComponents: Array<{ element: HTMLElement; unmount: () => void }> = [];
|
||||
|
||||
// Components are already in priority order in component-registry
|
||||
// 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 { selector, appId } = mapping;
|
||||
const selectors = Array.isArray(selector) ? selector : [selector];
|
||||
const selectors = Array.isArray(mapping.selector) ? mapping.selector : [mapping.selector];
|
||||
selectors.forEach((sel) => selectorToMapping.set(sel, mapping));
|
||||
});
|
||||
|
||||
// Find first matching element
|
||||
for (const sel of selectors) {
|
||||
const element = document.querySelector(sel) as HTMLElement;
|
||||
if (element && !element.hasAttribute('data-vue-mounted')) {
|
||||
// Get the async component from mapping
|
||||
const component = mapping.component;
|
||||
// Query all selectors at once
|
||||
const allSelectors = Array.from(selectorToMapping.keys()).join(',');
|
||||
|
||||
// Skip if no component is defined
|
||||
if (!component) {
|
||||
console.error(`[UnifiedMount] No component defined for ${appId}`);
|
||||
continue;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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.innerHTML = '';
|
||||
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),
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Store reference for debugging
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__unifiedApp = app;
|
||||
window.__mountedComponents = mountedComponents;
|
||||
}
|
||||
// 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 function autoMountAllComponents() {
|
||||
mountUnifiedApp();
|
||||
return mountUnifiedApp();
|
||||
}
|
||||
|
||||
4
web/types/window.d.ts
vendored
4
web/types/window.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import type { ApolloClient } from '@apollo/client/core';
|
||||
import type { autoMountComponent, getMountedApp, mountVueApp } from '~/components/Wrapper/mount-engine';
|
||||
import type { client as apolloClient } from '~/helpers/create-apollo-client';
|
||||
import type { parse } from 'graphql';
|
||||
import type { Component } from 'vue';
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { Component } from 'vue';
|
||||
declare global {
|
||||
interface Window {
|
||||
// Apollo GraphQL client and utilities
|
||||
apolloClient: typeof apolloClient;
|
||||
apolloClient?: ApolloClient<unknown>;
|
||||
gql: typeof parse;
|
||||
graphqlParse: typeof parse;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user