Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
67 changes: 67 additions & 0 deletions apps/desktop/src/renderer/desktop/desktop-home.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,73 @@ function button(page: HTMLElement, text: string): HTMLButtonElement {
return found;
}

/** What each region still being read says to a reader, in the order they stand. */
function waits(page: HTMLElement): string[] {
return [...page.querySelectorAll('[role="status"][aria-busy="true"]')].map(
(region) => region.textContent ?? "",
);
}

test("while the task list is unread, Home stands in its own shape: New task, and placeholder rows under the Inbox and Recent, saying no greeting", () => {
const { page } = mount({ listRead: false });

assert.ok(button(page, "New task"));
assert.deepEqual(waits(page), ["Reading your inbox…", "Reading your tasks…"]);
assert.ok(page.querySelectorAll(".luke-list-row .luke-skeleton").length > 0, "rows of blocks");
assert.doesNotMatch(page.textContent ?? "", /Welcome back|Nothing needs you/u);
assert.equal(page.querySelector("h1"), null);
});

test("an account whose task list is unread is neither welcomed as new nor greeted back until the list says which it is", () => {
const { page } = mount({
listRead: false,
tasks: [],
listed: { tasks: [], archived: [], inbox: [], inboxTasks: [] },
inbox: [],
inboxTasks: [],
});

assert.equal(page.querySelector("[aria-label='Welcome']"), null, "no welcome stands");
assert.doesNotMatch(page.textContent ?? "", /Start your first task|Welcome back/u);
assert.deepEqual(waits(page), ["Reading your inbox…", "Reading your tasks…"]);
});

test("once the task list lands, the page replaces its placeholder rows with the greeting, what waits, and the plans saved latest", async () => {
const { page } = mount({ listRead: false });
assert.equal(waits(page).length, 2, "the page stands in its own shape first");

const tasks = tasksControl({ tasks: TASKS, listed: { tasks: TASKS, archived: [], ...HOLDING } });
act(() =>
root?.render(
onRegistry(
AtomRegistry.make(),
createElement(DesktopHome, { tasks, accountName: "Dean Stratakos" }),
),
),
);
await settle();

assert.deepEqual(waits(page), []);
assert.equal(page.querySelector("h1")?.textContent, "Welcome back, Dean");
assert.equal(rows(page, "home-inbox").length, 2);
assert.equal(rows(page, "home-recent").length, 6);
});

test("while what waits is read, Home's Inbox stands a few placeholder rows under its name, and no ring", () => {
const { page } = mount();

const section = page.querySelector<HTMLElement>('section[aria-labelledby="home-inbox"]');
assert.ok(section, "Home's Inbox stands");
assert.equal(section.querySelector("#home-inbox")?.textContent, "Inbox");
assert.deepEqual(waits(section), ["Reading your inbox…"]);
assert.equal(section.querySelectorAll("li").length, 3, "fewer rows than a whole Inbox's five");
assert.equal(
section.querySelector(".luke-spinner"),
null,
"no ring stands at the section's edge",
);
});

test("the home page greets the account, lists what needs it with each item's plan, and the plans saved latest with their repositories", async () => {
const { page } = mount();
await settle();
Expand Down
109 changes: 102 additions & 7 deletions apps/desktop/src/renderer/desktop/desktop-home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import {
FolderIcon,
List,
ListRow,
SKELETON_SHAPE,
Skeleton,
SkeletonRegion,
TEXT_ELEMENT,
TEXT_TONE,
TEXT_VARIANT,
Expand All @@ -21,8 +24,7 @@ import { ACTOR_SIZE, ActorMark, CreatorHeard } from "../actor-ref";
import { MemberDirectory, useMemberDirectory } from "../member-directory";
import { RepositoryPicture } from "../repository-picture";
import { Shortcut } from "../shortcut";
import { SPINNER_SIZE, Spinner } from "../spinner";
import { InboxItemRow } from "../tasks/inbox-list";
import { InboxItemRow, InboxListSkeleton } from "../tasks/inbox-list";
import {
holderName,
INBOX_HOLDER,
Expand Down Expand Up @@ -53,14 +55,42 @@ import { useRowKeys } from "./use-row-keys";
* ↑ and ↓ move through its rows, a launcher's way, from the window itself
* as well as from a row, and Return opens the row reached, so a launch is a
* key or two from a task; a key pressed anywhere else, the sidebar or a
* field, is left to it. Until the task list is read the page draws nothing,
* since what it says hangs on the list; a read refused says so under Recent,
* field, is left to it. Until the task list is read the page stands in its
* own shape: New task, which the welcome offers too, and blocks where the
* greeting, the sections' headings, and their rows will stand. It says no
* word that hangs on the list, so a new account's welcome replaces it
* without having claimed otherwise. A read refused says so under Recent,
* with Retry, over whatever an earlier read listed.
*/

/** How many tasks Recent lists. */
const RECENT_COUNT = 6;

/** How many placeholder rows Home's Inbox stands while its items are read, fewer than a whole Inbox's. */
const INBOX_SKELETON_ROWS = 3;

/** What Recent's placeholder rows say to a reader while the task list is read. */
const READING_TASKS = "Reading your tasks…";

/** How wide the greeting's block stands, as wide as "Welcome back" and a first name in the title's type. */
const SKELETON_GREETING = "9em";

/** How wide each section heading's block stands, as wide as its word in the heading's type. */
const SKELETON_HEADING = { INBOX: "3em", RECENT: "3.5em" } as const;

/** How wide each of Recent's placeholder rows' names and repositories stand, one row for each task Recent lists, so the rows read as tasks of their own. */
const SKELETON_RECENT = [
{ name: "46%", repository: "5em" },
{ name: "34%", repository: "6em" },
{ name: "52%", repository: "5em" },
{ name: "40%", repository: "7em" },
{ name: "30%", repository: "5em" },
{ name: "44%", repository: "6em" },
] as const;

/** How wide a placeholder row's time stands, as wide as a short "2h". */
const SKELETON_TIME = "20px";
Comment thread
luke-agent[bot] marked this conversation as resolved.

/** What Recent says where the plan list could not be read, as the sidebar says it. */
const UNREAD_LINE = "Your tasks could not be read.";

Expand Down Expand Up @@ -186,7 +216,7 @@ function HomeInbox({ tasks, now }: { tasks: TasksControl; now: number }): React.
<ErrorState onRetry={waiting.onRetry}>Some items could not be read.</ErrorState>
</div>
) : empty && waiting.reading ? (
<Spinner label="Loading inbox" size={SPINNER_SIZE.INLINE} />
<InboxListSkeleton rows={INBOX_SKELETON_ROWS} context />
) : empty ? (
<div className="home-note">
<Text tone={TEXT_TONE.MUTED}>{NEEDS_YOU.NONE}</Text>
Expand Down Expand Up @@ -248,6 +278,71 @@ function Welcome({ tasks }: { tasks: TasksControl }): React.JSX.Element {
);
}

/** A section heading's block, in the heading's own type, where its word will stand. */
function HeadingSkeleton({ width }: { width: string }): React.JSX.Element {
return (
<div className="home-section-title">
<Skeleton width={width} />
</div>
);
}

/** Recent's rows while the task list is read: blocks where each row's picture, name, repository, and time will stand. */
function RecentSkeleton(): React.JSX.Element {
return (
<SkeletonRegion label={READING_TASKS}>
<List label="Recent">
{SKELETON_RECENT.map(({ name, repository }) => (
<ListRow
key={name}
leading={
<span className="home-row-picture">
<Skeleton shape={SKELETON_SHAPE.BLOCK} />
</span>
}
title={<Skeleton width={name} />}
meta={
<span className="home-row-meta">
<Skeleton width={repository} />
<Skeleton width={SKELETON_TIME} />
</span>
}
/>
))}
</List>
</SkeletonRegion>
);
}

/**
* The page while the task list is first read, in the shape of the page an
* account with tasks lands on: the greeting's block beside New task, then
* the Inbox's and Recent's heading blocks over rows of blocks, so the page
* lands where the blocks stood.
*/
function HomeSkeleton({ tasks }: { tasks: TasksControl }): React.JSX.Element {
return (
<div className="home-scroll scroll-fade-top">
<div className="home-column">
<header className="home-head">
<Text as={TEXT_ELEMENT.DIV} variant={TEXT_VARIANT.TITLE}>
<Skeleton width={SKELETON_GREETING} />
</Text>
<NewTaskButton tasks={tasks} />
</header>
<div className="home-section">
<HeadingSkeleton width={SKELETON_HEADING.INBOX} />
<InboxListSkeleton rows={INBOX_SKELETON_ROWS} context />
</div>
<div className="home-section">
<HeadingSkeleton width={SKELETON_HEADING.RECENT} />
<RecentSkeleton />
</div>
</div>
</div>
);
}

/** The home page, in the task's place while no task is open. */
export function DesktopHome({
tasks,
Expand All @@ -256,11 +351,11 @@ export function DesktopHome({
tasks: TasksControl;
/** The signed-in account's name, which greets it. */
accountName: string | undefined;
}): React.JSX.Element | null {
}): React.JSX.Element {
const now = useMinute();
const [column, setColumn] = useState<HTMLDivElement | null>(null);
useRowKeys(column, tasks.shown);
if (!tasks.listRead && !tasks.listFailed) return null;
if (!tasks.listRead && !tasks.listFailed) return <HomeSkeleton tasks={tasks} />;
// Only an account with no task in any workspace, and nothing an agent with
// no task waits on, is new; a workspace of its own with none still says
// what needs it elsewhere.
Expand Down
31 changes: 27 additions & 4 deletions apps/desktop/src/renderer/tasks/inbox-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ const SKELETON_ROWS = [
/** How wide a placeholder row's time stands, as wide as a short "2h". */
const SKELETON_TIME = "20px";

/** How wide a placeholder row's context stands, as wide as a short plan's name in a row's caption type. */
const SKELETON_CONTEXT = "7em";

/** How wide a placeholder row's mark stands, an agent's tile at its inline size (`.actor-mark`). */
const SKELETON_MARK = "16px";

Expand Down Expand Up @@ -534,19 +537,39 @@ export function NeedsYouEmpty({
* block where its agent's tile, its title, its time, and the line under
* them will stand, so the
* items land where the blocks stood rather than a lone ring in the list's
* corner. The rows open nothing and the region says what is being read.
* corner. The rows open nothing and the region says what is being read. A
* list whose rows name what each item is about, as Home's do, asks for a
* block before each time where that name will stand, and a short list asks
* for fewer rows than the five a whole Inbox stands.
*/
export function InboxListSkeleton(): React.JSX.Element {
export function InboxListSkeleton({
rows = SKELETON_ROWS.length,
context = false,
}: {
/** How many placeholder rows stand, at most five. */
rows?: number;
/** Whether each row holds a block for its context before its time, as an item row given a `context` draws it. */
context?: boolean;
}): React.JSX.Element {
return (
<SkeletonRegion label={READING_ITEMS}>
<ul className="sidebar-tasks">
{SKELETON_ROWS.map(({ title, preview }) => (
{SKELETON_ROWS.slice(0, rows).map(({ title, preview }) => (
<ListRow
key={title}
leading={<Skeleton shape={SKELETON_SHAPE.CIRCLE} width={SKELETON_MARK} />}
title={<Skeleton width={title} />}
description={<Skeleton width={preview} />}
meta={<Skeleton width={SKELETON_TIME} />}
meta={
context ? (
<span className="home-row-meta">
<Skeleton width={SKELETON_CONTEXT} />
<Skeleton width={SKELETON_TIME} />
</span>
) : (
<Skeleton width={SKELETON_TIME} />
)
}
metaAt={LIST_ROW_META.TITLE}
/>
))}
Expand Down
Loading