mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-01 01:16:17 +02:00
refactor(teams): team settings surface becomes the team detail page
Align with the house detail-page convention (issueDetail/projectDetail/...): - route: /team/KEY/settings -> /team/KEY (paths.teamDetail), web page moved up a segment, desktop route + TeamDetailRoute renamed - component: TeamSettingsPage -> TeamDetailPage - sidebar: settingsHref prop -> detailHref, drop the /settings-suffix strip - layout: the two-column Linear-style shell collapses to a single max-w-xl column (identity -> members -> goto -> danger) — content is too sparse for an aside Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Jiayuan Zhang
parent
5b91457c10
commit
e7f81d930d
@@ -3,7 +3,7 @@ import {
|
||||
TeamIssuesPage,
|
||||
TeamProjectsPage,
|
||||
TeamAutopilotsPage,
|
||||
TeamSettingsPage,
|
||||
TeamDetailPage,
|
||||
} from "@multica/views/teams";
|
||||
|
||||
// Router wrappers: resolve the :teamKey param and hand it to the shared
|
||||
@@ -27,8 +27,8 @@ export function TeamAutopilotsRoute() {
|
||||
return <TeamAutopilotsPage teamKey={teamKey} />;
|
||||
}
|
||||
|
||||
export function TeamSettingsRoute() {
|
||||
export function TeamDetailRoute() {
|
||||
const { teamKey } = useParams<{ teamKey: string }>();
|
||||
if (!teamKey) return null;
|
||||
return <TeamSettingsPage teamKey={teamKey} />;
|
||||
return <TeamDetailPage teamKey={teamKey} />;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
TeamIssuesRoute,
|
||||
TeamProjectsRoute,
|
||||
TeamAutopilotsRoute,
|
||||
TeamSettingsRoute,
|
||||
TeamDetailRoute,
|
||||
} from "./pages/team-surface-pages";
|
||||
import { IssuesPage } from "@multica/views/issues/components";
|
||||
import { ProjectsPage } from "@multica/views/projects/components";
|
||||
@@ -174,9 +174,9 @@ export const appRoutes: RouteObject[] = [
|
||||
handle: { title: "Team autopilots" },
|
||||
},
|
||||
{
|
||||
path: "team/:teamKey/settings",
|
||||
element: <TeamSettingsRoute />,
|
||||
handle: { title: "Team settings" },
|
||||
path: "team/:teamKey",
|
||||
element: <TeamDetailRoute />,
|
||||
handle: { title: "Team detail" },
|
||||
},
|
||||
{
|
||||
path: "projects/:id",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { use } from "react";
|
||||
import { TeamSettingsPage } from "@multica/views/teams";
|
||||
import { TeamDetailPage } from "@multica/views/teams";
|
||||
|
||||
export default function Page({
|
||||
params,
|
||||
@@ -9,5 +9,5 @@ export default function Page({
|
||||
params: Promise<{ teamKey: string }>;
|
||||
}) {
|
||||
const { teamKey } = use(params);
|
||||
return <TeamSettingsPage teamKey={teamKey} />;
|
||||
return <TeamDetailPage teamKey={teamKey} />;
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ function workspaceScoped(slug: string) {
|
||||
teamIssues: (key: string) => `${ws}/team/${encode(key)}/issues`,
|
||||
teamProjects: (key: string) => `${ws}/team/${encode(key)}/projects`,
|
||||
teamAutopilots: (key: string) => `${ws}/team/${encode(key)}/autopilots`,
|
||||
teamSettings: (key: string) => `${ws}/team/${encode(key)}/settings`,
|
||||
teamDetail: (key: string) => `${ws}/team/${encode(key)}`,
|
||||
projects: () => `${ws}/projects`,
|
||||
projectDetail: (id: string) => `${ws}/projects/${encode(id)}`,
|
||||
autopilots: () => `${ws}/autopilots`,
|
||||
|
||||
@@ -215,13 +215,13 @@ function SortableTeamGroup({
|
||||
team,
|
||||
pathname,
|
||||
buildHref,
|
||||
settingsHref,
|
||||
detailHref,
|
||||
forceCollapsed,
|
||||
}: {
|
||||
team: Team;
|
||||
pathname: string;
|
||||
buildHref: (pathKey: (typeof teamChildNav)[number]["pathKey"], teamKey: string) => string;
|
||||
settingsHref: string;
|
||||
detailHref: string;
|
||||
/** True while any team drag is in flight: every group renders collapsed so
|
||||
all sortable items are the same height — variable-height blocks make
|
||||
dnd-kit's rect math (and thus the whole gesture) feel broken. */
|
||||
@@ -237,11 +237,10 @@ function SortableTeamGroup({
|
||||
useEffect(() => {
|
||||
if (isDragging) wasDragged.current = true;
|
||||
}, [isDragging]);
|
||||
// The row itself is the team's home (overview/settings). It stays
|
||||
// highlighted anywhere inside the team (issues/projects/autopilots too) so
|
||||
// the sidebar always shows which team you're in.
|
||||
const teamBase = settingsHref.replace(/\/settings$/, "");
|
||||
const isTeamActive = isNavActive(pathname, teamBase);
|
||||
// The row itself is the team's home (detail page). It stays highlighted
|
||||
// anywhere inside the team (issues/projects/autopilots too) so the sidebar
|
||||
// always shows which team you're in.
|
||||
const isTeamActive = isNavActive(pathname, detailHref);
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
@@ -257,7 +256,7 @@ function SortableTeamGroup({
|
||||
the row; the 5px activation distance keeps clicks working. */}
|
||||
<div {...attributes} {...listeners}>
|
||||
<SidebarGroupLabel
|
||||
render={<AppLink href={settingsHref} draggable={false} />}
|
||||
render={<AppLink href={detailHref} draggable={false} />}
|
||||
onClick={(event) => {
|
||||
if (wasDragged.current) {
|
||||
wasDragged.current = false;
|
||||
@@ -282,7 +281,7 @@ function SortableTeamGroup({
|
||||
e.stopPropagation();
|
||||
collapse.onOpenChange(!collapse.open);
|
||||
}}
|
||||
className="ml-1 flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:bg-sidebar-accent hover:text-foreground"
|
||||
className="-ml-1 flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-sm text-muted-foreground hover:bg-sidebar-accent hover:text-foreground"
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
@@ -1071,7 +1070,7 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle }
|
||||
team={team}
|
||||
pathname={pathname}
|
||||
buildHref={(pathKey, teamKey) => p[pathKey](teamKey)}
|
||||
settingsHref={p.teamSettings(team.key)}
|
||||
detailHref={p.teamDetail(team.key)}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
|
||||
@@ -2,4 +2,4 @@ export { TeamsPage } from "./teams-page";
|
||||
export { TeamPicker, TeamMultiPicker } from "./team-picker";
|
||||
export { TeamIcon } from "./team-icon";
|
||||
export { TeamIssuesPage, TeamProjectsPage, TeamAutopilotsPage } from "./team-surface-pages";
|
||||
export { TeamSettingsPage } from "./team-settings-page";
|
||||
export { TeamDetailPage } from "./team-detail-page";
|
||||
|
||||
@@ -46,13 +46,13 @@ const underline =
|
||||
"rounded-none border-0 border-b border-input bg-transparent dark:bg-transparent px-0 shadow-none focus-visible:ring-0 focus-visible:border-foreground";
|
||||
|
||||
/**
|
||||
* Team settings — /team/:key/settings, Linear team-page shape. Left column is
|
||||
* the identity itself rendered as page text (icon picker applies on pick,
|
||||
* name and description commit on blur — no save buttons); the right column
|
||||
* holds members (avatar stack → checkbox config; saving an empty set
|
||||
* archives behind a confirm), go-to links, and archive.
|
||||
* Team detail — /team/:key, the sidebar team row's landing page. A single
|
||||
* narrow column: the identity rendered as page text (icon picker applies on
|
||||
* pick, name and description commit on blur — no save buttons), then members
|
||||
* (avatar stack → checkbox config; saving an empty set archives behind a
|
||||
* confirm), go-to links, and archive.
|
||||
*/
|
||||
export function TeamSettingsPage({ teamKey }: { teamKey: string }) {
|
||||
export function TeamDetailPage({ teamKey }: { teamKey: string }) {
|
||||
const { t } = useT("teams");
|
||||
const wsId = useWorkspaceId();
|
||||
// Full list (not active-only): an archived team's settings stay viewable.
|
||||
@@ -77,15 +77,11 @@ export function TeamSettingsPage({ teamKey }: { teamKey: string }) {
|
||||
</PageHeader>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
<div className="mx-auto flex w-full max-w-3xl gap-10 px-6 py-8">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Identity team={team} />
|
||||
</div>
|
||||
<aside className="flex w-52 shrink-0 flex-col gap-6">
|
||||
<MembersSection team={team} />
|
||||
<GotoSection team={team} />
|
||||
<ArchiveSection team={team} />
|
||||
</aside>
|
||||
<div className="mx-auto flex w-full max-w-xl flex-col gap-8 px-6 py-8">
|
||||
<Identity team={team} />
|
||||
<MembersSection team={team} />
|
||||
<GotoSection team={team} />
|
||||
<ArchiveSection team={team} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -299,7 +295,7 @@ function MembersSection({ team }: { team: Team }) {
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{t(($) => $.picker.empty)}
|
||||
{t(($) => $.settings.members_empty)}
|
||||
</span>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
@@ -337,7 +333,7 @@ function MembersSection({ team }: { team: Team }) {
|
||||
))}
|
||||
{filteredMembers.length === 0 && (
|
||||
<div className="px-1 py-4 text-center text-sm text-muted-foreground">
|
||||
{t(($) => $.picker.empty)}
|
||||
{t(($) => $.dialog.member_search_empty)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -47,7 +47,7 @@ export function TeamIssuesPage({ teamKey }: { teamKey: string }) {
|
||||
<PageHeader className="gap-2">
|
||||
{/* Team name links into the team's settings page. */}
|
||||
<AppLink
|
||||
href={p.teamSettings(team.key)}
|
||||
href={p.teamDetail(team.key)}
|
||||
className="flex items-center gap-2 rounded px-1 -mx-1 transition-colors hover:bg-accent/40"
|
||||
>
|
||||
<TeamIcon team={team} />
|
||||
|
||||
@@ -103,7 +103,7 @@ export function TeamsPage() {
|
||||
<ListGridRow
|
||||
key={team.id}
|
||||
className="cursor-pointer"
|
||||
{...rowLink(p.teamSettings(team.key))}
|
||||
{...rowLink(p.teamDetail(team.key))}
|
||||
>
|
||||
<ListGridCell className="gap-2">
|
||||
<TeamIcon team={team} />
|
||||
|
||||
@@ -6,5 +6,5 @@ export {
|
||||
TeamIssuesPage,
|
||||
TeamProjectsPage,
|
||||
TeamAutopilotsPage,
|
||||
TeamSettingsPage,
|
||||
TeamDetailPage,
|
||||
} from "./components";
|
||||
|
||||
Reference in New Issue
Block a user