diff --git a/src/lib/layout.test.ts b/src/lib/layout.test.ts new file mode 100644 index 0000000..83f3075 --- /dev/null +++ b/src/lib/layout.test.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2026 Coffey Labs + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, it, expect } from 'vitest'; +import { isLinkVisible, visibleLayouts } from './layout'; +import { withRegroupedSettings, SETTINGS_OVERVIEW_VIEW } from './settingsLayout'; +import type { Schema } from '@/types/schema'; + +// The shape a server in bootstrap mode serves: every layout present, and an +// administrator who may only read and write x:Bootstrap. +const served = { + objects: { + 'x:Bootstrap': { type: 'singleton', description: '', permissionPrefix: 'sysBootstrap' }, + 'x:Domain': { type: 'object', description: '', permissionPrefix: 'sysDomain' }, + 'x:MtaStageEhlo': { type: 'singleton', description: '', permissionPrefix: 'sysMtaStageEhlo' }, + }, + schemas: {}, + fields: {}, + forms: {}, + lists: {}, + enums: {}, + dashboards: [], + layouts: [ + { + name: 'Management', + icon: 'server', + items: [ + { link: { name: 'Domains', icon: 'globe', viewName: 'x:Domain' } }, + { link: { name: 'Compliance', icon: 'scale', viewName: 'CustomComponent/ComplianceOverview' } }, + { link: { name: 'Coming soon', icon: 'sparkles', viewName: 'CustomComponent/NotInThisConsole' } }, + ], + }, + { + name: 'Settings', + icon: 'sliders-horizontal', + items: [ + { + container: { + name: 'MTA', + icon: 'route', + items: [{ type: 'link', name: 'EHLO Stage', viewName: 'x:MtaStageEhlo' }], + }, + }, + ], + }, + ], +} as unknown as Schema; + +const schema = withRegroupedSettings(served); +const canGetFrom = (perms: string[]) => (prefix: string) => perms.includes(`${prefix}Get`); +const hasPermFrom = (perms: string[]) => (perm: string) => perms.includes(perm); + +describe('layout visibility', () => { + it('leaves the bootstrap administrator no layout, so the wizard shows (inbuxa-server#135)', () => { + const perms = ['sysBootstrapGet', 'sysBootstrapUpdate']; + expect(visibleLayouts(schema, 'enterprise', canGetFrom(perms), hasPermFrom(perms))).toEqual([]); + }); + + it('shows the Settings overview only beside a settings page the account may see', () => { + const none = canGetFrom([]); + const ehlo = canGetFrom(['sysMtaStageEhloGet']); + expect(isLinkVisible(schema, SETTINGS_OVERVIEW_VIEW, 'enterprise', none)).toBe(false); + expect(isLinkVisible(schema, SETTINGS_OVERVIEW_VIEW, 'enterprise', ehlo)).toBe(true); + expect(visibleLayouts(schema, 'enterprise', ehlo).map((l) => l.name)).toEqual(['Settings']); + }); + + it('hides a page this console cannot draw', () => { + const all = () => true; + expect(isLinkVisible(schema, 'CustomComponent/NotInThisConsole', 'enterprise', all)).toBe(false); + }); +}); diff --git a/src/lib/layout.ts b/src/lib/layout.ts index 79c7734..45e1117 100644 --- a/src/lib/layout.ts +++ b/src/lib/layout.ts @@ -9,6 +9,7 @@ import type { Layout, LayoutItem, LayoutSubItem, Schema } from '@/types/schema'; import { resolveObject } from '@/lib/schemaResolver'; +import { SETTINGS_LAYOUT_NAME, SETTINGS_OVERVIEW_VIEW } from '@/lib/settingsLayout'; function findFirstSubLink(items: LayoutSubItem[]): string | null { for (const item of items) { @@ -111,8 +112,11 @@ function checkSpecialLink( return { visible: canGet ? canGet('sysAccountLock') : true, enterprise: false }; } + // INBUXA: a page this console can't draw is hidden. A server newer than the + // console lists pages the console doesn't know yet, and counting them as + // visible hid the first-boot wizard (inbuxa-server#135). if (viewName.startsWith('CustomComponent/')) { - return { visible: true, enterprise: false }; + return { visible: false, enterprise: false }; } // INBUXA: guided jobs are open to whoever may manage what they change. @@ -148,6 +152,20 @@ export function isLinkVisible( canGet: CanGet, hasPerm?: HasPermission, ): boolean { + // INBUXA: the Settings overview is the console's own page, there when any + // settings page is. Visible on its own, it made the Settings layout visible + // to the bootstrap administrator, and the wizard never showed. + if (viewName === SETTINGS_OVERVIEW_VIEW) { + const settings = schema.layouts.find((layout) => layout.name === SETTINGS_LAYOUT_NAME); + return ( + settings?.items.some( + (item) => + 'container' in item && + findFirstVisibleSubLink(schema, item.container.items, edition, canGet, hasPerm) !== null, + ) ?? false + ); + } + const special = checkSpecialLink(viewName, edition, hasPerm, canGet); if (special !== null) return special.visible;