feat(app-switcher): централизовать ссылки приложений в portal settings
Публичный GET и admin PUT/UI /admin/apps; каталог и chrome читают URL из store. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { z } from 'zod'
|
||||
import { APP_IDS, APPS, appIdSchema, type AppId, type AppMeta } from './auth.js'
|
||||
|
||||
export const appSwitcherIconSchema = z.enum([
|
||||
'server',
|
||||
'cloud',
|
||||
'globe',
|
||||
'dashboard',
|
||||
'chart',
|
||||
])
|
||||
|
||||
export const appSwitcherEntrySchema = z.object({
|
||||
id: appIdSchema,
|
||||
name: z.string().min(1),
|
||||
subtitle: z.string().optional(),
|
||||
url: z.string().url(),
|
||||
icon: appSwitcherIconSchema,
|
||||
shortcut: z.string().optional(),
|
||||
enabled: z.boolean(),
|
||||
sort: z.number().int().optional(),
|
||||
})
|
||||
|
||||
export const appSwitcherConfigSchema = z.object({
|
||||
menuLabel: z.string().min(1),
|
||||
apps: z.array(appSwitcherEntrySchema).min(1),
|
||||
})
|
||||
|
||||
export type AppSwitcherIconName = z.infer<typeof appSwitcherIconSchema>
|
||||
export type AppSwitcherEntry = z.infer<typeof appSwitcherEntrySchema>
|
||||
export type AppSwitcherConfig = z.infer<typeof appSwitcherConfigSchema>
|
||||
|
||||
const DEFAULT_ICONS: Record<AppId, AppSwitcherIconName> = {
|
||||
cfdm: 'cloud',
|
||||
vps: 'server',
|
||||
bgp: 'globe',
|
||||
}
|
||||
|
||||
/** Seed / fallback when DB is empty. */
|
||||
export function defaultAppSwitcherConfig(): AppSwitcherConfig {
|
||||
return {
|
||||
menuLabel: 'Приложения',
|
||||
apps: APPS.map((app, index) => ({
|
||||
id: app.id,
|
||||
name: app.title,
|
||||
subtitle: app.description,
|
||||
url: app.url,
|
||||
icon: DEFAULT_ICONS[app.id],
|
||||
enabled: true,
|
||||
sort: index,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAppSwitcherConfig(raw: unknown): AppSwitcherConfig {
|
||||
const parsed = appSwitcherConfigSchema.safeParse(raw)
|
||||
if (!parsed.success) return defaultAppSwitcherConfig()
|
||||
return normalizeAppSwitcherConfig(parsed.data)
|
||||
}
|
||||
|
||||
/** Ensure all APP_IDS present; sort; drop unknown. */
|
||||
export function normalizeAppSwitcherConfig(
|
||||
config: AppSwitcherConfig,
|
||||
): AppSwitcherConfig {
|
||||
const byId = new Map(config.apps.map((a) => [a.id, a]))
|
||||
const defaults = defaultAppSwitcherConfig()
|
||||
const apps = APP_IDS.map((id, index) => {
|
||||
const existing = byId.get(id)
|
||||
const fallback = defaults.apps.find((a) => a.id === id)!
|
||||
return {
|
||||
...fallback,
|
||||
...existing,
|
||||
id,
|
||||
sort: existing?.sort ?? index,
|
||||
enabled: existing?.enabled ?? true,
|
||||
icon: existing?.icon ?? fallback.icon,
|
||||
}
|
||||
}).sort((a, b) => (a.sort ?? 0) - (b.sort ?? 0))
|
||||
|
||||
return {
|
||||
menuLabel: config.menuLabel || 'Приложения',
|
||||
apps,
|
||||
}
|
||||
}
|
||||
|
||||
/** AppMeta list with URLs from switcher store (for /apps + catalog). */
|
||||
export function appsMetaFromSwitcher(config: AppSwitcherConfig): AppMeta[] {
|
||||
const normalized = normalizeAppSwitcherConfig(config)
|
||||
return normalized.apps
|
||||
.filter((a) => a.enabled !== false)
|
||||
.map((a) => ({
|
||||
id: a.id,
|
||||
title: a.name,
|
||||
description: a.subtitle ?? APPS.find((x) => x.id === a.id)?.description ?? '',
|
||||
url: a.url,
|
||||
}))
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export * from './contracts/auth.js'
|
||||
export * from './contracts/app-switcher.js'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user