Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/web/src/components/settings/ProjectsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { EnvironmentId } from "@t3tools/contracts";
import { ProjectSettingsPanel } from "./ProjectSettingsPanel";
import { useSettingsScope } from "./SettingsScopeContext";
import { SettingsScopeNotice } from "./SettingsScopeNotice";
import { SettingsPageContainer } from "./settingsLayout";

/** Project identity and checkout management for the selected project. */
export function ProjectsSettings() {
Expand All @@ -22,7 +23,9 @@ export function ProjectsSettings() {
checkoutKey={value.checkout ?? null}
/>
) : scope.kind === "unavailable" ? (
<p className="p-8 text-sm text-muted-foreground">{scope.message}</p>
<SettingsPageContainer>
<p className="text-sm text-muted-foreground">{scope.message}</p>
</SettingsPageContainer>
) : (
<SettingsScopeNotice target="project">
Choose a project to manage its name, icon, checkouts and actions.
Expand Down
198 changes: 3 additions & 195 deletions apps/web/src/components/settings/SettingsBreadcrumb.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,9 @@
import { resolveEnvironmentMachineKind } from "@t3tools/contracts";
import { LayersIcon } from "lucide-react";
import type { ReactNode } from "react";

import type { SidebarProjectSnapshot } from "../../sidebarProjectGrouping";
import type { EnvironmentPresentation } from "../../state/environments";
import { EnvironmentMachineIcon } from "../EnvironmentMachineIcon";
import { ProjectFavicon } from "../ProjectFavicon";
import { InlineButton } from "../ui/button";
import {
Menu,
MenuPopup,
MenuRadioGroup,
MenuRadioItem,
MenuRadioItemIndicator,
MenuSeparator,
MenuTrigger,
} from "../ui/menu";
import {
WorkspaceBreadcrumb,
WorkspaceBreadcrumbItem,
WorkspaceBreadcrumbSeparator,
} from "../WorkspaceBreadcrumb";
import { SETTINGS_SECTION_LABELS } from "./settingsSearch";
import { resolveSettingsScope, type SettingsScopeSearch } from "./settingsScope";
import {
ALL_ENVIRONMENTS_VALUE,
ALL_PROJECTS_VALUE,
environmentAxisValue,
projectAxisValue,
selectEnvironmentAxis,
selectProjectAxis,
settingsScopeEnvironmentLabel,
} from "./settingsScopeAxis";

const SETTINGS_BREADCRUMB_LABELS: Readonly<Record<string, string>> = {
...SETTINGS_SECTION_LABELS,
Expand All @@ -44,27 +16,11 @@ function settingsBreadcrumbLabel(pathname: string): string | null {
return SETTINGS_BREADCRUMB_LABELS[normalizedPathname] ?? null;
}

export interface SettingsScopeBreadcrumbProps {
readonly value: SettingsScopeSearch;
readonly groups: readonly SidebarProjectSnapshot[];
readonly environments: readonly EnvironmentPresentation[];
readonly onChange: (next: SettingsScopeSearch) => void;
}

/**
* `Settings / Section / Environment / Project`. The last two crumbs are the
* targets a change applies to and read like the usage page's filter: muted at
* "all", foreground once narrowed. A project is the same project on every
* environment, so the environment crumb alone decides where a project
* override is written.
* `Settings / Section`. The scope a change applies to lives at the top of the
* page content, see `SettingsScopeSentence`.
*/
export function SettingsBreadcrumb({
pathname,
scope,
}: {
pathname: string;
scope?: SettingsScopeBreadcrumbProps | undefined;
}) {
export function SettingsBreadcrumb({ pathname }: { pathname: string }) {
const sectionLabel = settingsBreadcrumbLabel(pathname);

return (
Expand All @@ -78,154 +34,6 @@ export function SettingsBreadcrumb({
<WorkspaceBreadcrumbItem current className="truncate">
{sectionLabel ?? "Settings"}
</WorkspaceBreadcrumbItem>
{scope ? (
<>
<WorkspaceBreadcrumbSeparator />
<WorkspaceBreadcrumbItem className="min-w-0 shrink">
<EnvironmentScopeMenu {...scope} />
</WorkspaceBreadcrumbItem>
<WorkspaceBreadcrumbSeparator />
<WorkspaceBreadcrumbItem className="min-w-0 shrink">
<ProjectScopeMenu {...scope} />
</WorkspaceBreadcrumbItem>
</>
) : null}
</WorkspaceBreadcrumb>
);
}

function ScopeMenu({
ariaLabel,
icon,
label,
narrowed,
children,
}: {
ariaLabel: string;
icon: ReactNode;
label: string;
narrowed: boolean;
children: ReactNode;
}) {
return (
<Menu>
<MenuTrigger
aria-label={ariaLabel}
render={<InlineButton tone={narrowed ? "default" : "muted"} />}
className="min-w-0 max-w-56 gap-1.5"
>
{icon}
<span className="min-w-0 truncate">{label}</span>
</MenuTrigger>
<MenuPopup align="start">{children}</MenuPopup>
</Menu>
);
}

function EnvironmentScopeMenu({
value,
groups,
environments,
onChange,
}: SettingsScopeBreadcrumbProps) {
const resolved = resolveSettingsScope(value, groups, environments);
const environmentValue = environmentAxisValue(
value,
resolved.kind === "checkout" ? resolved.environmentId : null,
);
const selected = environments.find(
(environment) => environment.environmentId === environmentValue,
);
return (
<ScopeMenu
ariaLabel="Environment scope"
narrowed={environmentValue !== ALL_ENVIRONMENTS_VALUE}
icon={
selected ? (
<EnvironmentMachineIcon
aria-hidden
kind={resolveEnvironmentMachineKind(selected.serverConfig)}
className="size-3.5 shrink-0"
/>
) : null
}
label={
selected
? settingsScopeEnvironmentLabel(selected, environments)
: environmentValue !== ALL_ENVIRONMENTS_VALUE
? "Unavailable environment"
: "All environments"
}
>
<MenuRadioGroup
value={environmentValue}
onValueChange={(next) => {
if (typeof next === "string") onChange(selectEnvironmentAxis(value, next));
}}
>
<MenuRadioItem value={ALL_ENVIRONMENTS_VALUE}>
<span className="flex min-w-0 items-center gap-2">
<LayersIcon aria-hidden className="size-3.5" />
<span className="min-w-0 flex-1 truncate">All environments</span>
<MenuRadioItemIndicator />
</span>
</MenuRadioItem>
<MenuSeparator />
{environments.map((environment) => (
<MenuRadioItem key={environment.environmentId} value={environment.environmentId}>
<span className="flex min-w-0 items-center gap-2">
<EnvironmentMachineIcon
aria-hidden
kind={resolveEnvironmentMachineKind(environment.serverConfig)}
className="size-3.5"
/>
<span className="min-w-0 flex-1 truncate">
{settingsScopeEnvironmentLabel(environment, environments)}
</span>
{environment.connection.phase === "connected" ? null : (
<span className="shrink-0 text-xs text-muted-foreground">Offline</span>
)}
<MenuRadioItemIndicator />
</span>
</MenuRadioItem>
))}
</MenuRadioGroup>
</ScopeMenu>
);
}

function ProjectScopeMenu({ value, groups, onChange }: SettingsScopeBreadcrumbProps) {
const selected = groups.find((group) => group.projectKey === value.project);
return (
<ScopeMenu
ariaLabel="Project scope"
narrowed={value.project !== undefined}
icon={selected ? <ProjectFavicon project={selected} className="size-3.5 shrink-0" /> : null}
label={selected?.displayName ?? (value.project ? "Unavailable project" : "All projects")}
>
<MenuRadioGroup
value={projectAxisValue(value)}
onValueChange={(next) => {
if (typeof next === "string") onChange(selectProjectAxis(value, next));
}}
>
<MenuRadioItem value={ALL_PROJECTS_VALUE}>
<span className="flex min-w-0 items-center gap-2">
<span className="min-w-0 flex-1 truncate">All projects</span>
<MenuRadioItemIndicator />
</span>
</MenuRadioItem>
<MenuSeparator />
{groups.map((group) => (
<MenuRadioItem key={group.projectKey} value={group.projectKey}>
<span className="flex min-w-0 items-center gap-2">
<ProjectFavicon project={group} className="size-3.5" />
<span className="min-w-0 flex-1 truncate">{group.displayName}</span>
<MenuRadioItemIndicator />
</span>
</MenuRadioItem>
))}
</MenuRadioGroup>
</ScopeMenu>
);
}
Loading
Loading