Cap rail bot and workspace lists at five with see-all summary
This commit is contained in:
@@ -5,6 +5,7 @@ import { twMerge } from "tailwind-merge";
|
||||
import profileImageUrls from "@/src/utils/profile-image-urls";
|
||||
|
||||
const WorkspaceAvatarSizes = {
|
||||
xs: { box: "size-[18px] rounded-[4px]", icon: 12 },
|
||||
sm: { box: "size-8 rounded-default-sm", icon: 16 },
|
||||
lg: { box: "size-14 rounded-default", icon: 24 },
|
||||
} as const;
|
||||
|
||||
@@ -252,11 +252,16 @@ export const AppData = {
|
||||
* Worker bots the rail lists, most recently active first, before
|
||||
* "See all bots".
|
||||
*/
|
||||
RailRecentBots: 8,
|
||||
RailRecentBots: 5,
|
||||
/**
|
||||
* Stones shown beside "See all bots".
|
||||
* Workspaces the rail lists, most recently active first, before
|
||||
* "See all workspaces".
|
||||
*/
|
||||
RailSeeAllStones: 4,
|
||||
RailRecentWorkspaces: 5,
|
||||
/**
|
||||
* Stones shown beside "See all bots" and "See all workspaces".
|
||||
*/
|
||||
RailSeeAllStones: 3,
|
||||
/**
|
||||
* Recent sessions the rail lists under the open bot, before "All
|
||||
* sessions".
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState } from "react";
|
||||
import Input from "@moduletrace/twui/form/Input";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import type { WorkspaceWithActivity } from "@/src/types";
|
||||
import AddModalShell from "./add-modal-shell";
|
||||
import AllWorkspacesRow from "./all-workspaces-row";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
workspaces: WorkspaceWithActivity[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Every workspace, latest activity first, with a search by name or
|
||||
* description. A workspace opens its page.
|
||||
*/
|
||||
export default function AllWorkspacesModal({ open, setOpen, workspaces }: Props) {
|
||||
const [query, setQuery] = useState("");
|
||||
const search = query.trim().toLowerCase();
|
||||
const found = search
|
||||
? workspaces.filter((workspace) =>
|
||||
`${workspace.name} ${workspace.description || ""}`.toLowerCase().includes(search),
|
||||
)
|
||||
: workspaces;
|
||||
|
||||
return (
|
||||
<AddModalShell open={open} setOpen={setOpen} title="Workspaces" description={`${workspaces.length} in all.`}>
|
||||
<Input
|
||||
id="all-workspaces-search"
|
||||
title="Search workspaces"
|
||||
placeholder="Search by name or description"
|
||||
autoComplete="off"
|
||||
defaultValue={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
wrapperWrapperProps={{ className: "mt-4" }}
|
||||
/>
|
||||
<Stack className="w-full mt-2 max-h-[60vh] overflow-y-auto gap-0! items-stretch">
|
||||
{found.map((workspace) => (
|
||||
<AllWorkspacesRow key={workspace.id} workspace={workspace} />
|
||||
))}
|
||||
{found.length ? null : (
|
||||
<Span className="px-3 py-6 text-[13px] leading-[18px] text-ink-secondary">
|
||||
No workspace matches "{query.trim()}".
|
||||
</Span>
|
||||
)}
|
||||
</Stack>
|
||||
</AddModalShell>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Link from "@moduletrace/twui/layout/Link";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import Stack from "@moduletrace/twui/layout/Stack";
|
||||
import WorkspaceAvatar from "@/src/components/general/workspace-avatar";
|
||||
import type { WorkspaceWithActivity } from "@/src/types";
|
||||
import relativeTime from "@/src/utils/relative-time";
|
||||
|
||||
type Props = {
|
||||
workspace: WorkspaceWithActivity;
|
||||
};
|
||||
|
||||
/**
|
||||
* One workspace in the all-workspaces modal: tile, name, description, last
|
||||
* activity.
|
||||
*/
|
||||
export default function AllWorkspacesRow({ workspace }: Props) {
|
||||
return (
|
||||
<Link
|
||||
href={`/admin/workspaces/${workspace.id}`}
|
||||
title={workspace.name}
|
||||
className="flex h-14 items-center gap-3 rounded-default px-3 border-0 leading-normal hover:opacity-100 hover:bg-sunken transition-colors duration-[var(--m-fast)] focus-visible:ring-2 focus-visible:ring-ink"
|
||||
>
|
||||
<WorkspaceAvatar image_media_id={workspace.image_media_id} />
|
||||
<Stack className="grow min-w-0 gap-0!">
|
||||
<Span className="text-sm font-bold leading-5 text-ink truncate w-full">{workspace.name}</Span>
|
||||
<Span className="text-xs leading-4 text-ink-secondary truncate w-full">
|
||||
{workspace.unread ? "New messages" : workspace.description || ""}
|
||||
</Span>
|
||||
</Stack>
|
||||
{workspace.last_activity_at ? (
|
||||
<Span className="shrink-0 text-[11px] leading-4 text-ink-secondary">
|
||||
{relativeTime({ at: Number(workspace.last_activity_at) })}
|
||||
</Span>
|
||||
) : null}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import AllBotsModal from "./all-bots-modal";
|
||||
import RailBotEntry from "./rail-bot-entry";
|
||||
import RailSectionLabel from "./rail-section-label";
|
||||
import RailSeeAllRow from "./rail-see-all-row";
|
||||
import RailSeeAllStone from "./rail-see-all-stone";
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
@@ -55,7 +56,9 @@ export default function RailBotsSection({ title, workers, selected_id, workspace
|
||||
)}
|
||||
{hidden.length ? (
|
||||
<RailSeeAllRow
|
||||
hidden={hidden}
|
||||
stones={hidden.slice(0, AppData.RailSeeAllStones).map((bot) => (
|
||||
<RailSeeAllStone key={bot.id} bot={bot} />
|
||||
))}
|
||||
label={`See all ${workers.length} bots`}
|
||||
onOpen={() => setOpen(true)}
|
||||
/>
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Button from "@moduletrace/twui/layout/Button";
|
||||
import Row from "@moduletrace/twui/layout/Row";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { Ellipsis } from "lucide-react";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { BotWithLastMessage } from "@/src/types";
|
||||
import RailSeeAllStone from "./rail-see-all-stone";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
* The bots the rail doesn't list.
|
||||
* A few of the records the rail doesn't list, e.g. their stones.
|
||||
*/
|
||||
hidden: BotWithLastMessage[];
|
||||
stones?: ReactNode;
|
||||
label: string;
|
||||
onOpen: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Under the listed bots: a few stones of the ones that aren't listed and
|
||||
* "See all bots", which opens them all. An ellipsis when the rail
|
||||
* Under the listed records: a few stones of the ones that aren't listed and
|
||||
* "See all", which opens every one in a modal. An ellipsis when the rail
|
||||
* collapses.
|
||||
*/
|
||||
export default function RailSeeAllRow({ hidden, label, onOpen }: Props) {
|
||||
export default function RailSeeAllRow({ stones, label, onOpen }: Props) {
|
||||
return (
|
||||
<Button
|
||||
title={label}
|
||||
@@ -31,12 +29,10 @@ export default function RailSeeAllRow({ hidden, label, onOpen }: Props) {
|
||||
className="flex shrink-0 h-10 w-full items-center justify-center @rail:justify-start gap-3! rounded-default px-1! @rail:px-3! text-ink-secondary dark:text-ink-secondary hover:text-ink hover:dark:text-ink hover:bg-sunken hover:dark:bg-sunken transition-colors duration-[var(--m-fast)] focus-visible:ring-2 focus-visible:ring-ink"
|
||||
>
|
||||
<Ellipsis size={18} aria-hidden className="@rail:hidden" />
|
||||
<Row className="hidden @rail:flex shrink-0 gap-1! flex-nowrap items-center">
|
||||
{hidden.slice(0, AppData.RailSeeAllStones).map((bot) => (
|
||||
<RailSeeAllStone key={bot.id} bot={bot} />
|
||||
))}
|
||||
</Row>
|
||||
<Span className="hidden @rail:inline text-[13px] font-bold leading-4">{label}</Span>
|
||||
{stones ? (
|
||||
<Row className="hidden @rail:flex shrink-0 gap-1! flex-nowrap items-center">{stones}</Row>
|
||||
) : null}
|
||||
<Span className="hidden @rail:inline text-[13px] font-bold leading-4 truncate">{label}</Span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import WorkspaceAvatar from "@/src/components/general/workspace-avatar";
|
||||
import type { WorkspaceWithActivity } from "@/src/types";
|
||||
|
||||
type Props = {
|
||||
workspace: WorkspaceWithActivity;
|
||||
};
|
||||
|
||||
/**
|
||||
* One small workspace tile beside "See all workspaces".
|
||||
*/
|
||||
export default function RailSeeAllWorkspace({ workspace }: Props) {
|
||||
return <WorkspaceAvatar image_media_id={workspace.image_media_id} size="xs" />;
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
import { useContext } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import Span from "@moduletrace/twui/layout/Span";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import type { WorkspaceWithActivity } from "@/src/types";
|
||||
import { AddFlowContext } from "../(data)/add-flow-context";
|
||||
import AllWorkspacesModal from "./all-workspaces-modal";
|
||||
import RailSectionAddButton from "./rail-section-add-button";
|
||||
import RailSectionLabel from "./rail-section-label";
|
||||
import RailSeeAllRow from "./rail-see-all-row";
|
||||
import RailSeeAllWorkspace from "./rail-see-all-workspace";
|
||||
import WorkspaceRow from "./workspace-row";
|
||||
|
||||
type Props = {
|
||||
@@ -12,10 +16,15 @@ type Props = {
|
||||
};
|
||||
|
||||
/**
|
||||
* The user's workspaces, with a "+" to create one.
|
||||
* The user's workspaces, with a "+" to create one: the latest
|
||||
* `AppData.RailRecentWorkspaces`, then "See all workspaces" for the rest,
|
||||
* which lists them all in a modal.
|
||||
*/
|
||||
export default function RailWorkspacesSection({ workspaces, selected_id }: Props) {
|
||||
const { openForm } = useContext(AddFlowContext);
|
||||
const [open, setOpen] = useState(false);
|
||||
const shown = workspaces.slice(0, AppData.RailRecentWorkspaces);
|
||||
const hidden = workspaces.slice(AppData.RailRecentWorkspaces);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -28,7 +37,7 @@ export default function RailWorkspacesSection({ workspaces, selected_id }: Props
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{workspaces.map((workspace) => (
|
||||
{shown.map((workspace) => (
|
||||
<WorkspaceRow key={workspace.id} workspace={workspace} selected={workspace.id == selected_id} />
|
||||
))}
|
||||
{workspaces.length ? null : (
|
||||
@@ -36,6 +45,16 @@ export default function RailWorkspacesSection({ workspaces, selected_id }: Props
|
||||
No workspaces yet. A workspace keeps a project's context, bots and computers together.
|
||||
</Span>
|
||||
)}
|
||||
{hidden.length ? (
|
||||
<RailSeeAllRow
|
||||
stones={hidden.slice(0, AppData.RailSeeAllStones).map((workspace) => (
|
||||
<RailSeeAllWorkspace key={workspace.id} workspace={workspace} />
|
||||
))}
|
||||
label={`See all ${workspaces.length} workspaces`}
|
||||
onOpen={() => setOpen(true)}
|
||||
/>
|
||||
) : null}
|
||||
<AllWorkspacesModal open={open} setOpen={setOpen} workspaces={workspaces} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user