Files
breadcrumb-the-shire/web/js/app-init.js

286 lines
9.8 KiB
JavaScript
Raw Normal View History

2026-03-06 00:44:52 +01:00
import './core/app-telemetry.js';
import { createComponentRuntime } from './core/app-component-runtime.js';
import { warnOnce } from './core/app-dom.js';
import { initFlashAutoDismiss } from './components/app-flash-auto-dismiss.js';
import { initPasswordHints } from './components/app-password-hints.js';
import { initBadgeCopy } from './components/app-copy-badge.js';
import { initMultiSelectComponent } from './components/app-multiselect-init.js';
import { initSidebarToggle } from './components/app-toggle-sidebar.js';
import { initAsidePanels } from './components/app-toggle-aside-panels.js';
import { initDetailsAsideToggle } from './components/app-toggle-details-aside.js';
import { initGlobalSearch } from './components/app-global-search.js';
import { initThemeControls } from './components/app-theme-toggle.js';
import { initContrastToggle } from './components/app-contrast-toggle.js';
import { initConfirmActions } from './components/app-confirm-actions.js';
import { initDetailsState } from './components/app-details-state.js';
import { initTenantSwitcher } from './components/app-tenant-switcher.js';
import { initColorDefaultToggle } from './components/app-color-default-toggle.js';
import { initCustomFieldOptionsToggle } from './components/app-custom-field-options-toggle.js';
import { initTenantSsoToggle } from './components/app-tenant-sso-toggle.js';
import { initTenantLdapToggle } from './components/app-tenant-ldap-toggle.js';
import { initTelemetrySettings } from './components/app-settings-telemetry.js';
import { initStandardDetailPages } from './components/app-standard-detail-page.js';
import { initAutoSubmit } from './components/app-auto-submit.js';
import { initTabs } from './components/app-tabs.js';
import { initSessionWarning } from './components/app-session-warning.js';
import { initFsLightboxRefresh } from './components/app-fslightbox-refresh.js';
import { initFileUpload } from './components/app-file-upload.js';
import { initLookupFields } from './components/app-lookup-field.js';
const runtime = createComponentRuntime({
root: document,
configId: 'app-components',
});
const phaseRank = {
early: 10,
default: 50,
late: 90,
};
const normalizePhase = (raw) => {
const phase = String(raw || '').trim().toLowerCase();
if (phase === 'early' || phase === 'default' || phase === 'late') {
return phase;
}
return 'late';
};
const normalizeModuleRuntimeComponents = (value) => {
if (!Array.isArray(value)) {
return [];
}
return value
.filter((item) => item && typeof item === 'object')
.map((item) => {
const key = String(item.key || '').trim();
const moduleId = String(item.moduleId || '').trim();
const componentName = String(item.componentName || '').trim() || (moduleId && key ? `${moduleId}.${key}` : '');
const script = String(item.script || '').trim();
const exportName = String(item.export || '').trim();
const selector = String(item.selector || '').trim();
const scope = String(item.scope || '').trim().toLowerCase();
const configPath = String(item.configPath || '').trim();
const phase = normalizePhase(item.phase);
const order = Number.isFinite(Number(item.order)) ? Number(item.order) : 100;
return {
key,
moduleId,
componentName,
script,
exportName,
selector,
scope,
configPath,
phase,
order,
};
})
.filter((item) => item.componentName !== '' && item.script !== '')
.sort((a, b) => {
const phaseCmp = (phaseRank[a.phase] || phaseRank.late) - (phaseRank[b.phase] || phaseRank.late);
if (phaseCmp !== 0) {
return phaseCmp;
}
if (a.order !== b.order) {
return a.order - b.order;
}
return a.componentName.localeCompare(b.componentName);
});
};
const registerRuntimeComponent = async (definition) => {
const moduleContext = {
module: 'app-init',
runtimeComponent: definition.componentName,
};
const scriptUrl = (() => {
try {
return new URL(definition.script, document.baseURI).href;
} catch {
return '';
}
})();
if (!scriptUrl) {
warnOnce('UI_COMPONENT_INVALID', `Invalid runtime.component script URL for "${definition.componentName}"`, moduleContext);
return;
}
try {
const loaded = await import(scriptUrl);
const exportName = definition.exportName || 'default';
const init = loaded?.[exportName];
if (typeof init !== 'function') {
warnOnce(
'UI_COMPONENT_INVALID',
`runtime.component "${definition.componentName}" export "${exportName}" is not a function`,
moduleContext
);
return;
}
const componentOptions = {};
if (definition.scope !== '') {
componentOptions.scope = definition.scope;
}
if (definition.selector !== '') {
componentOptions.selector = definition.selector;
}
if (definition.configPath !== '') {
componentOptions.configPath = definition.configPath;
}
runtime.register(definition.componentName, init, componentOptions);
} catch (error) {
warnOnce('UI_COMPONENT_IMPORT_FAILED', `Failed to import runtime.component "${definition.componentName}"`, {
...moduleContext,
error,
});
}
};
const mountModuleComponentsByPhase = async (phase) => {
const pageConfig = runtime.getPageConfig();
const runtimeComponents = normalizeModuleRuntimeComponents(pageConfig?.moduleRuntimeComponents);
const phaseComponents = runtimeComponents.filter((item) => item.phase === phase);
for (const definition of phaseComponents) {
// eslint-disable-next-line no-await-in-loop
await registerRuntimeComponent(definition);
}
};
const bootRuntime = async () => {
// Phase A: low risk and global utilities.
runtime.register('confirm-actions', initConfirmActions, {
scope: 'global',
configPath: 'components.confirmActions',
});
runtime.register('flash-auto-dismiss', initFlashAutoDismiss, {
selector: '[data-app-component="flash-auto-dismiss"]',
configPath: 'components.flashAutoDismiss',
defaultConfig: {
selector: '.notice[data-flash-timeout]',
},
});
runtime.register('password-hints', initPasswordHints, {
scope: 'global',
configPath: 'components.passwordHints',
});
runtime.register('copy-badge', initBadgeCopy, {
scope: 'global',
configPath: 'components.copyBadge',
});
runtime.register('multiselect', initMultiSelectComponent, {
scope: 'global',
configPath: 'components.multiSelect',
});
runtime.register('details-state', initDetailsState, {
scope: 'global',
configPath: 'components.detailsState',
});
runtime.register('settings-telemetry', initTelemetrySettings, {
scope: 'global',
configPath: 'components.settingsTelemetry',
});
runtime.register('auto-submit', initAutoSubmit, {
scope: 'global',
configPath: 'components.autoSubmit',
});
runtime.register('file-upload', initFileUpload, {
selector: '[data-app-component="file-upload"]',
configPath: 'components.fileUpload',
});
runtime.register('lookup-field', initLookupFields, {
scope: 'global',
configPath: 'components.lookupField',
defaultConfig: {
selector: '[data-app-lookup]',
},
});
await mountModuleComponentsByPhase('early');
// Phase B: structure-defining components.
runtime.register('sidebar-toggle', initSidebarToggle, {
scope: 'global',
configPath: 'components.sidebarToggle',
});
runtime.register('aside-panels', initAsidePanels, {
scope: 'global',
configPath: 'components.asidePanels',
});
runtime.register('details-aside-toggle', initDetailsAsideToggle, {
scope: 'global',
configPath: 'components.detailsAsideToggle',
});
runtime.register('theme-controls', initThemeControls, {
selector: '[data-app-component="theme-controls"]',
configPath: 'components.themeControls',
});
runtime.register('contrast-toggle', initContrastToggle, {
selector: '[data-app-component="contrast-toggle"]',
configPath: 'components.contrastToggle',
});
runtime.register('tenant-switcher', initTenantSwitcher, {
selector: '[data-app-component="tenant-switcher"]',
configPath: 'components.tenantSwitcher',
});
runtime.register('color-default-toggle', initColorDefaultToggle, {
scope: 'global',
configPath: 'components.colorDefaultToggle',
});
runtime.register('custom-field-options-toggle', initCustomFieldOptionsToggle, {
scope: 'global',
configPath: 'components.customFieldOptionsToggle',
});
runtime.register('tenant-sso-toggle', initTenantSsoToggle, {
scope: 'global',
configPath: 'components.tenantSsoToggle',
});
runtime.register('tenant-ldap-toggle', initTenantLdapToggle, {
scope: 'global',
configPath: 'components.tenantLdapToggle',
});
runtime.register('standard-detail-page', initStandardDetailPages, {
scope: 'global',
configPath: 'components.standardDetailPage',
});
await mountModuleComponentsByPhase('default');
// Phase C: high-risk (search, side panel interactions).
runtime.register('global-search', initGlobalSearch, {
2026-04-01 16:31:59 +02:00
selector: '[data-app-search-dialog]',
configPath: 'components.globalSearch',
});
runtime.register('tabs', initTabs, {
selector: '[data-app-component="tabs"]',
configPath: 'components.tabs',
defaultConfig: {
selector: '[data-app-component="tabs"]',
storageNamespace: 'app-ui',
storageVersion: 'v1',
},
});
runtime.register('session-warning', initSessionWarning, {
selector: '[data-app-component="session-warning"]',
configPath: 'components.sessionWarning',
defaultConfig: {
selector: '[data-app-session-warning-dialog]',
enabled: true,
},
});
runtime.register('fslightbox-refresh', initFsLightboxRefresh, {
scope: 'global',
configPath: 'components.fslightboxRefresh',
});
await mountModuleComponentsByPhase('late');
runtime.mountAll();
};
void bootRuntime();