mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
The quick-create field toggles used to live inside the quick create popover; per feedback on MUL-4873 they belong in the Settings page. Adds a My Account 'Issue' tab with one group per create mode backed by a new per-workspace issue-create-settings store, extends field visibility to the manual dialog (status/priority/assignee/labels/ project/due date/start date with value-reveal and overflow re-entry), and routes both dialogs' 'Customize fields' items to the tab. Co-authored-by: Lambda <lambda@multica.ai> Co-authored-by: multica-agent <github@multica.ai>
234 lines
8.4 KiB
TypeScript
234 lines
8.4 KiB
TypeScript
"use client";
|
|
|
|
import React from "react";
|
|
import {
|
|
User,
|
|
SlidersHorizontal,
|
|
Key,
|
|
Settings,
|
|
Users,
|
|
FolderGit2,
|
|
FlaskConical,
|
|
Bell,
|
|
Plug,
|
|
MessageCircle,
|
|
Tags,
|
|
Keyboard,
|
|
ListTodo,
|
|
} from "lucide-react";
|
|
import { GitHubMark } from "./github-mark";
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@multica/ui/components/ui/tabs";
|
|
import { useIsMobile } from "@multica/ui/hooks/use-mobile";
|
|
import { useCurrentWorkspace } from "@multica/core/paths";
|
|
import { useNavigation } from "../../navigation";
|
|
import { AccountTab } from "./account-tab";
|
|
import { PreferencesTab } from "./preferences-tab";
|
|
import { ChatTab } from "./chat-tab";
|
|
import { IssueTab } from "./issue-tab";
|
|
import { TokensTab } from "./tokens-tab";
|
|
import { WorkspaceTab } from "./workspace-tab";
|
|
import { MembersTab } from "./members-tab";
|
|
import { RepositoriesTab } from "./repositories-tab";
|
|
import { GitHubTab } from "./github-tab";
|
|
import { IntegrationsTab } from "./integrations-tab";
|
|
import { LabsTab } from "./labs-tab";
|
|
import { NotificationsTab } from "./notifications-tab";
|
|
import { LabelsTab } from "./labels-tab";
|
|
import { PropertiesTab } from "./properties-tab";
|
|
import { KeyboardShortcutsTab } from "./keyboard-shortcuts-tab";
|
|
import { useT } from "../../i18n";
|
|
|
|
const ACCOUNT_TAB_KEYS = ["profile", "preferences", "shortcuts", "issue", "chat", "notifications", "tokens"] as const;
|
|
const ACCOUNT_TAB_ICONS = {
|
|
profile: User,
|
|
preferences: SlidersHorizontal,
|
|
shortcuts: Keyboard,
|
|
issue: ListTodo,
|
|
chat: MessageCircle,
|
|
notifications: Bell,
|
|
tokens: Key,
|
|
} as const;
|
|
|
|
const WORKSPACE_TAB_KEYS = [
|
|
"general",
|
|
"repositories",
|
|
"github",
|
|
"integrations",
|
|
"labs",
|
|
"members",
|
|
"labels",
|
|
"properties",
|
|
] as const;
|
|
const WORKSPACE_TAB_VALUES = {
|
|
general: "workspace",
|
|
repositories: "repositories",
|
|
github: "github",
|
|
integrations: "integrations",
|
|
labs: "labs",
|
|
members: "members",
|
|
labels: "labels",
|
|
properties: "properties",
|
|
} as const;
|
|
const WORKSPACE_TAB_ICONS = {
|
|
general: Settings,
|
|
repositories: FolderGit2,
|
|
github: GitHubMark,
|
|
integrations: Plug,
|
|
labs: FlaskConical,
|
|
members: Users,
|
|
labels: Tags,
|
|
properties: SlidersHorizontal,
|
|
} as const;
|
|
|
|
const DEFAULT_TAB = "profile";
|
|
const TAB_QUERY_KEY = "tab";
|
|
|
|
// Legacy `?tab=…` values that have been collapsed into another tab. Old
|
|
// bookmarks still land on the correct surface without us preserving a
|
|
// dead TabsContent entry. Lark used to be its own top-level workspace
|
|
// tab; it now lives inside Integrations.
|
|
const LEGACY_WORKSPACE_TAB_REDIRECTS: Record<string, string> = {
|
|
lark: "integrations",
|
|
};
|
|
|
|
const SETTINGS_TAB_TRIGGER_CLASS =
|
|
"h-8 shrink-0 px-2.5 hover:bg-surface-hover data-active:!bg-surface-selected data-active:!text-surface-selected-foreground data-active:hover:!bg-surface-selected md:!w-full md:px-2 md:after:hidden";
|
|
|
|
export interface ExtraSettingsTab {
|
|
value: string;
|
|
label: string;
|
|
icon: React.ComponentType<{ className?: string }>;
|
|
content: React.ReactNode;
|
|
}
|
|
|
|
interface SettingsPageProps {
|
|
/** Additional tabs injected by platform (e.g. desktop daemon settings) */
|
|
extraAccountTabs?: ExtraSettingsTab[];
|
|
}
|
|
|
|
export function SettingsPage({ extraAccountTabs }: SettingsPageProps = {}) {
|
|
const { t } = useT("settings");
|
|
const workspaceName = useCurrentWorkspace()?.name;
|
|
const navigation = useNavigation();
|
|
const isMobile = useIsMobile();
|
|
|
|
// Whitelist of valid tab values; unknown ?tab=… values silently fall back to
|
|
// the default. Whitelisting also blocks junk like ?tab=<script> from
|
|
// surfacing in the DOM via Radix Tabs internals.
|
|
const validTabs = React.useMemo(
|
|
() =>
|
|
new Set<string>([
|
|
...ACCOUNT_TAB_KEYS,
|
|
...Object.values(WORKSPACE_TAB_VALUES),
|
|
...(extraAccountTabs?.map((tab) => tab.value) ?? []),
|
|
]),
|
|
[extraAccountTabs],
|
|
);
|
|
|
|
const tabFromUrl = navigation.searchParams.get(TAB_QUERY_KEY);
|
|
const candidateTab = tabFromUrl
|
|
? LEGACY_WORKSPACE_TAB_REDIRECTS[tabFromUrl] ?? tabFromUrl
|
|
: null;
|
|
const activeTab =
|
|
candidateTab && validTabs.has(candidateTab) ? candidateTab : DEFAULT_TAB;
|
|
|
|
// replace (not push) so settings tab switches don't pollute browser history.
|
|
// Preserve any other query params the page may carry.
|
|
const handleTabChange = (next: string) => {
|
|
const params = new URLSearchParams(navigation.searchParams);
|
|
params.set(TAB_QUERY_KEY, next);
|
|
navigation.replace(`${navigation.pathname}?${params.toString()}`);
|
|
};
|
|
|
|
return (
|
|
<Tabs
|
|
value={activeTab}
|
|
onValueChange={handleTabChange}
|
|
orientation={isMobile ? "horizontal" : "vertical"}
|
|
className="flex flex-1 min-h-0 flex-col gap-0 overflow-y-auto md:flex-row md:overflow-hidden"
|
|
>
|
|
{/* Structural navigation; bounded setting groups remain in the content surface.
|
|
Stays on the content surface color (no shell tint): the desktop's active
|
|
tab merges into the card top, and a tinted panel under the first tabs
|
|
breaks that seam (MUL-4439). Zoning comes from the divider instead. */}
|
|
<div className="shrink-0 overflow-x-auto border-b border-surface-border p-2 md:w-56 md:overflow-y-auto md:border-b-0 md:border-r md:p-4">
|
|
<h1 className="sr-only text-sm font-semibold md:not-sr-only md:mb-4 md:px-2">{t(($) => $.page.title)}</h1>
|
|
<TabsList
|
|
variant="line"
|
|
className="flex w-max min-w-full flex-row items-center gap-1 p-0 md:w-full md:flex-col md:items-stretch"
|
|
>
|
|
{/* My Account group */}
|
|
<span className="hidden px-2 pb-1 pt-2 text-xs font-medium text-muted-foreground md:block">
|
|
{t(($) => $.page.my_account)}
|
|
</span>
|
|
{ACCOUNT_TAB_KEYS.map((key) => {
|
|
const Icon = ACCOUNT_TAB_ICONS[key];
|
|
return (
|
|
<TabsTrigger
|
|
key={key}
|
|
value={key}
|
|
className={SETTINGS_TAB_TRIGGER_CLASS}
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
{t(($) => $.page.tabs[key])}
|
|
</TabsTrigger>
|
|
);
|
|
})}
|
|
{extraAccountTabs?.map((tab) => (
|
|
<TabsTrigger
|
|
key={tab.value}
|
|
value={tab.value}
|
|
className={SETTINGS_TAB_TRIGGER_CLASS}
|
|
>
|
|
<tab.icon className="h-4 w-4" />
|
|
{tab.label}
|
|
</TabsTrigger>
|
|
))}
|
|
|
|
{/* Workspace group */}
|
|
<span className="hidden truncate px-2 pb-1 pt-4 text-xs font-medium text-muted-foreground md:block">
|
|
{workspaceName ?? t(($) => $.page.workspace_fallback)}
|
|
</span>
|
|
{WORKSPACE_TAB_KEYS.map((key) => {
|
|
const Icon = WORKSPACE_TAB_ICONS[key];
|
|
return (
|
|
<TabsTrigger
|
|
key={key}
|
|
value={WORKSPACE_TAB_VALUES[key]}
|
|
className={SETTINGS_TAB_TRIGGER_CLASS}
|
|
>
|
|
<Icon className="h-4 w-4" />
|
|
{t(($) => $.page.tabs[key])}
|
|
</TabsTrigger>
|
|
);
|
|
})}
|
|
</TabsList>
|
|
</div>
|
|
|
|
{/* Right content */}
|
|
<div className="min-w-0 flex-1 md:overflow-y-auto">
|
|
<div className={`mx-auto w-full p-4 sm:p-6 md:p-8 ${activeTab === "labels" || activeTab === "properties" ? "max-w-5xl" : "max-w-3xl"}`}>
|
|
<TabsContent value="profile"><AccountTab /></TabsContent>
|
|
<TabsContent value="preferences"><PreferencesTab /></TabsContent>
|
|
<TabsContent value="shortcuts"><KeyboardShortcutsTab /></TabsContent>
|
|
<TabsContent value="issue"><IssueTab /></TabsContent>
|
|
<TabsContent value="chat"><ChatTab /></TabsContent>
|
|
<TabsContent value="notifications"><NotificationsTab /></TabsContent>
|
|
<TabsContent value="tokens"><TokensTab /></TabsContent>
|
|
<TabsContent value="workspace"><WorkspaceTab /></TabsContent>
|
|
<TabsContent value="repositories"><RepositoriesTab /></TabsContent>
|
|
<TabsContent value="github"><GitHubTab /></TabsContent>
|
|
<TabsContent value="integrations"><IntegrationsTab /></TabsContent>
|
|
<TabsContent value="labs"><LabsTab /></TabsContent>
|
|
<TabsContent value="members"><MembersTab /></TabsContent>
|
|
<TabsContent value="labels"><LabelsTab /></TabsContent>
|
|
<TabsContent value="properties"><PropertiesTab /></TabsContent>
|
|
{extraAccountTabs?.map((tab) => (
|
|
<TabsContent key={tab.value} value={tab.value}>{tab.content}</TabsContent>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</Tabs>
|
|
);
|
|
}
|