import { objectModels, evaluateVariable } from '../database/ObjectModels' import { getSidebarMenuSections } from '../database/Sidebars' import { buildSvgIconUrlMap, getSvgIconUrl } from './desktopIcons' import config from '../config' import packageJson from '../../package.json' const FUNCTION_KEYS = new Set([ 'value', 'visible', 'readOnly', 'required', 'disabled' ]) const SKIP_KEYS = new Set(['content', 'icon', 'pages']) const isPlainObject = (value) => value != null && typeof value === 'object' && !Array.isArray(value) && Object.prototype.toString.call(value) === '[object Object]' const isReactish = (value) => { if (value == null) return false if (typeof value === 'object') { if (value.$$typeof) return true if (typeof value._init === 'function' && value._payload) return true } if (typeof value === 'function') { const name = value.displayName || value.name || '' if (name.endsWith('Icon') || name === 'Icon') return true } return false } const functionToIconKey = (fn) => { if (typeof fn !== 'function') return null const name = fn.displayName || fn.name || '' if (!name.endsWith('Icon')) return null const base = name.slice(0, -4) if (!base) return null return base.charAt(0).toLowerCase() + base.slice(1) } const serializeJsFunction = (fn) => { try { return { __js: Function.prototype.toString.call(fn) } } catch { return undefined } } const serializeValue = (value, key) => { if (value == null) return value if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { return value } if (typeof value === 'bigint') { return value.toString() } if (value instanceof Date) { return value.toISOString() } if (typeof value === 'function') { if (isReactish(value) || SKIP_KEYS.has(key)) { return undefined } if (FUNCTION_KEYS.has(key) || key == null) { return serializeJsFunction(value) } return serializeJsFunction(value) } if (Array.isArray(value)) { return value .map((item) => serializeValue(item, key)) .filter((item) => item !== undefined) } if (isReactish(value)) { return undefined } if (!isPlainObject(value)) { return undefined } const result = {} for (const [childKey, childValue] of Object.entries(value)) { if (SKIP_KEYS.has(childKey) && childKey !== 'pages') { if (childKey === 'icon' && typeof childValue === 'function') { const iconKey = functionToIconKey(childValue) if (iconKey) { result.iconKey = iconKey } } continue } if (childKey === 'pages' && Array.isArray(childValue)) { result.pages = childValue .map((page) => { if (!page || typeof page !== 'object') return null return { name: page.name } }) .filter((page) => page?.name) continue } const serialized = serializeValue(childValue, childKey) if (serialized !== undefined) { result[childKey] = serialized } } return result } const serializeModel = (model) => { if (!model || typeof model !== 'object') return null const iconKey = model.iconKey || functionToIconKey(model.icon) || model.name || null const serialized = serializeValue(model, null) || {} serialized.name = model.name serialized.label = model.label serialized.labelPlural = model.labelPlural serialized.url = typeof model.url === 'string' ? model.url : undefined serialized.prefix = model.prefix serialized.endpoint = model.endpoint || `${String(model.name || '').toLowerCase()}s` serialized.iconKey = iconKey serialized.columns = Array.isArray(model.columns) ? model.columns : [] serialized.filters = Array.isArray(model.filters) ? model.filters : [] serialized.sorters = Array.isArray(model.sorters) ? model.sorters : [] if (Array.isArray(model.actions)) { serialized.actions = model.actions .map((action) => { if (!action || typeof action !== 'object') return null if (action.type === 'divider') { return { type: 'divider' } } const next = serializeValue(action, null) || {} if (typeof action.icon === 'function') { const actionIconKey = functionToIconKey(action.icon) if (actionIconKey) next.iconKey = actionIconKey } delete next.content delete next.children return next }) .filter(Boolean) } if (Array.isArray(model.properties)) { serialized.properties = serializeValue(model.properties, 'properties') } return serialized } const attachModelsToSidebarItems = (items, modelsByUrl) => { if (!Array.isArray(items)) return [] return items .map((item) => { if (!item || item.type === 'divider') { return { type: 'divider' } } const next = { key: item.key, label: item.label, iconKey: item.iconKey, path: item.path } if (item.path && modelsByUrl.has(item.path)) { next.model = modelsByUrl.get(item.path) } if (Array.isArray(item.children) && item.children.length > 0) { next.children = attachModelsToSidebarItems(item.children, modelsByUrl) if ( !next.model && !next.children.some((child) => child.model || child.children) ) { return null } } if (!next.model && !next.children) { return null } return next }) .filter(Boolean) } export function serializeDesktopBundle() { const models = serializeDesktopModels() const modelsByUrl = new Map( models.filter((model) => model.url).map((model) => [model.url, model.name]) ) const sidebars = getSidebarMenuSections({ includeDev: false }).map( (section) => ({ key: section.key, label: section.label, iconKey: section.iconKey, items: attachModelsToSidebarItems(section.items, modelsByUrl) }) ) const icons = buildSvgIconUrlMap() const registerIconKey = (iconKey) => { if (!iconKey) return if (icons[iconKey] || icons[iconKey.toLowerCase()]) return const urls = getSvgIconUrl(iconKey, icons) if (urls) { icons[iconKey] = urls } } const registerSidebarIcons = (items) => { if (!Array.isArray(items)) return for (const item of items) { registerIconKey(item?.iconKey) registerSidebarIcons(item?.children) } } for (const model of models) { registerIconKey(model.iconKey) if (Array.isArray(model.actions)) { for (const action of model.actions) { registerIconKey(action?.iconKey) } } } for (const sidebar of sidebars) { registerIconKey(sidebar.iconKey) registerSidebarIcons(sidebar.items) } return { version: packageJson.version, webAppUrl: config.webAppUrl, apiUrl: config.backendUrl, wsUrl: config.apiServerUrl, printServerUrl: config.printServerUrl, auth: { loginPath: '/auth/web/login', tokenPath: '/auth/web/token', refreshPath: '/auth/refresh', userPath: '/auth/user', logoutPath: '/auth/logout' }, helpers: { evaluateVariable: { __js: evaluateVariable.toString() } }, icons, sidebars, models: models.map((model) => ({ name: model.name, url: `/desktop/models/${model.name}.json` })) } } export function serializeDesktopModels() { return objectModels.map(serializeModel).filter(Boolean) }