fix(settings): derive the sidebar preview from the real sidebar declaration

SidebarPreview kept its own hand-maintained 6-item array with only two gates
wired (analytics, userManagement), so toggling e.g. Workflows changed nothing
in the preview even though it does add a real sidebar entry once saved.

Export AdminSidebar's `navigation` as `adminNavigation` (2 lines) and derive
the preview from it, so every gate -- transfers, messaging, analytics,
userManagement, clients incl. its featureFlagsAny set, accounting, workflows --
is covered and the two can't drift again.

Note the report's item list was partly wrong: Quotes, Contracts, Invoices,
Hours, Projects, Calendar and the CRM dev tools have no top-level sidebar
entries at all -- they are sub-nav inside /admin/clients and surface in the
preview through the CRM entry's featureFlagsAny.

Permission filtering is deliberately not applied (unchanged): the preview
answers "what do these flags do to the sidebar", not "what can this admin see".

Refs testplan REPORT.md #20 (Part 3, J.14).
This commit is contained in:
Paul Nothaft
2026-09-01 16:29:33 +02:00
parent 3e16b81be8
commit c6cb01865e
3 changed files with 82 additions and 23 deletions
@@ -1,22 +1,14 @@
import React, { useMemo } from 'react';
import clsx from 'clsx';
import {
LayoutDashboard,
Calendar,
Archive,
BarChart3,
Settings,
Users,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { Card } from '../../../components/common';
import { useTranslation } from 'react-i18next';
import { adminNavigation } from '../../../components/admin/AdminSidebar';
import type { FeatureFlags } from '../../../contexts/FeatureFlagsContext';
interface PreviewItem {
key: string;
label: string;
icon: LucideIcon;
icon: React.ComponentType<{ className?: string }>;
featureDriven: boolean;
}
@@ -28,21 +20,25 @@ interface SidebarPreviewProps {
* Renders the live shape of the main admin sidebar based on the user's
* staged (unsaved) feature flags. Items in green (primary tint) are
* controlled by toggles above; greyscale items are unconditional.
*
* Derived from AdminSidebar's own `adminNavigation` declaration so every
* feature-gated entry is covered automatically — the previous hand-kept
* copy had drifted to 2 of the gates (QA J.14). Permissions are
* deliberately NOT applied here: the preview answers "what do these flags
* do to the sidebar", not "what can this particular admin see".
*/
export const SidebarPreview: React.FC<SidebarPreviewProps> = ({ staged }) => {
const { t } = useTranslation();
const items = useMemo<PreviewItem[]>(() => {
const all: Array<PreviewItem & { gate?: keyof FeatureFlags }> = [
{ key: 'dashboard', label: t('navigation.dashboard'), icon: LayoutDashboard, featureDriven: false },
{ key: 'events', label: t('navigation.events'), icon: Calendar, featureDriven: false },
{ key: 'archives', label: t('navigation.archives'), icon: Archive, featureDriven: false },
{ key: 'analytics', label: t('admin.analytics', 'Analytics'), icon: BarChart3, featureDriven: true, gate: 'analytics' },
{ key: 'settings', label: t('navigation.settings'), icon: Settings, featureDriven: false },
{ key: 'users', label: t('navigation.users'), icon: Users, featureDriven: true, gate: 'userManagement' },
];
return all.filter((it) => !it.gate || staged[it.gate]);
}, [staged, t]);
const items = useMemo<PreviewItem[]>(() => adminNavigation
.filter((it) => (!it.featureFlag || staged[it.featureFlag])
&& (!it.featureFlagsAny?.length || it.featureFlagsAny.some((k) => staged[k])))
.map((it) => ({
key: it.nameKey,
label: t(it.nameKey),
icon: it.icon,
featureDriven: Boolean(it.featureFlag || it.featureFlagsAny?.length),
})), [staged, t]);
return (
<Card padding="md">