From d8d4236ee24ae059d53e3b7ebbd4a8c5379cd43f Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Fri, 29 May 2026 15:25:49 -0400 Subject: [PATCH] Make TeamPage settings tabs and admin-access extensible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract two small extension points out of TeamPage into @/extensions/teamSettings: - useTeamSettingsTabs(baseTabs): transforms the settings tab/section list before render (default: identity). - useTeamAdminAccess(): whether the current user can administer the team, gating the team-name edit affordance (default: always allowed). TeamPage builds its base tabs and passes them through the transform, and reads admin access via the hook. No behavior change — the defaults preserve current behavior. This makes the team-settings page composable and testable without editing the page itself. No resolution plumbing needed: @/extensions/* already resolves to src/extensions/* via the existing tsconfig/jest/next config. --- packages/app/src/TeamPage.tsx | 23 +++++------ packages/app/src/extensions/teamSettings.tsx | 42 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 packages/app/src/extensions/teamSettings.tsx diff --git a/packages/app/src/TeamPage.tsx b/packages/app/src/TeamPage.tsx index 4a9c2bffd4..411f229359 100644 --- a/packages/app/src/TeamPage.tsx +++ b/packages/app/src/TeamPage.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import Head from 'next/head'; import { useRouter } from 'next/router'; import { SubmitHandler, useForm } from 'react-hook-form'; @@ -16,6 +16,12 @@ import { import { notifications } from '@mantine/notifications'; import { IconPencil } from '@tabler/icons-react'; +import { + type TeamTab, + useTeamAdminAccess, + useTeamSettingsTabs, +} from '@/extensions/teamSettings'; + import { PageHeader } from './components/PageHeader'; import ApiKeysSection from './components/TeamSettings/ApiKeysSection'; import ConnectionsSection from './components/TeamSettings/ConnectionsSection'; @@ -28,15 +34,6 @@ import { useBrandDisplayName } from './theme/ThemeProvider'; import api from './api'; import { withAppNav } from './layout'; -type TeamTab = { - value: string; - label: string; - sections: { - id: string; - content: ReactNode; - }[]; -}; - function TeamTabContent({ sections }: { sections: TeamTab['sections'] }) { return ( @@ -57,7 +54,7 @@ export default function TeamPage() { const allowedAuthMethods = team?.allowedAuthMethods ?? []; const hasAllowedAuthMethods = allowedAuthMethods.length > 0; - const hasAdminAccess = true; + const hasAdminAccess = useTeamAdminAccess(); const [isEditingTeamName, setIsEditingTeamName] = useState(false); const form = useForm<{ name: string }>({ defaultValues: { name: team?.name }, @@ -88,7 +85,7 @@ export default function TeamPage() { [refetchTeam, setTeamName], ); - const tabs: TeamTab[] = [ + const baseTabs: TeamTab[] = [ { value: 'data', label: 'Data', @@ -157,6 +154,8 @@ export default function TeamPage() { }, ]; + const tabs = useTeamSettingsTabs(baseTabs); + const queryTab = typeof router.query.tab === 'string' ? router.query.tab : null; const activeTab = tabs.some(tab => tab.value === queryTab) diff --git a/packages/app/src/extensions/teamSettings.tsx b/packages/app/src/extensions/teamSettings.tsx new file mode 100644 index 0000000000..9c3d68f83a --- /dev/null +++ b/packages/app/src/extensions/teamSettings.tsx @@ -0,0 +1,42 @@ +import { ReactNode } from 'react'; + +/** + * Team settings extension points. + * + * `TeamPage` builds its tabs from a declarative list and renders them + * generically. The two hooks below are the extension seam: builds that ship + * additional team-settings surfaces can supply their own implementations of + * this module (resolved via the `@/extensions` import) to contribute or + * rearrange tabs and to gate administrative affordances, without editing + * `TeamPage` itself. The defaults here preserve the standard behavior. + */ + +export type TeamTab = { + value: string; + label: string; + sections: { + id: string; + content: ReactNode; + }[]; +}; + +/** + * Transform the team-settings tabs before they are rendered. Receives the + * base tabs and returns the tabs to display. Default: unchanged. + * + * This is a hook so implementations may read state (current team, feature + * flags, etc.) when deciding which tabs and sections to show. + */ +// eslint-disable-next-line @eslint-react/no-unnecessary-use-prefix -- hook by contract: override implementations call hooks +export function useTeamSettingsTabs(tabs: TeamTab[]): TeamTab[] { + return tabs; +} + +/** + * Whether the current user may administer the team (controls the team-name + * edit affordance). Default: always allowed. + */ +// eslint-disable-next-line @eslint-react/no-unnecessary-use-prefix -- hook by contract: override implementations call hooks +export function useTeamAdminAccess(): boolean { + return true; +}