Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Web: Work's list says which repository it is empty for, and offers New only to who may

Filtered to a repository alone, an empty list said nothing matched the filters, since the repository counts as one: it names the repository again. New work item showed for anyone signed in once ?repo= was set; it shows for members, and for others only when they may take part in that repository, as the New work item page decides.

G1T-Session:
ses_01m56vafhge7ttrgs5288dpxvb
syntaqxcommitted Parent8557d88Browse files
3 files+39−130/3 viewed
+11−0
55
66 import {
77 filterCount,
8+ emptyListTitle,
89 filtered,
910 movedWorkPath,
1011 newItemPath,
109110 assert.equal(movedWorkPath("/acme/web/pulls"), null);
110111 assert.equal(movedWorkPath("/acme/-/issues"), null);
111112 });
113+
114+test("an empty Work list names the repository it is filtered to, when that is all it is filtered by", () => {
115+ const query = (text: string) => workFilters(new URLSearchParams(text));
116+ assert.equal(emptyListTitle(query("repo=web"), "web"), "No open work items linked to web");
117+ assert.equal(emptyListTitle(query("repo=web&state=closed&role=target"), "web"), "No closed work items linked to web");
118+ assert.equal(emptyListTitle(query("repo=web&label=bug"), "web"), "No open work items match these filters");
119+ assert.equal(emptyListTitle(query("label=bug"), null), "No open work items match these filters");
120+ assert.equal(emptyListTitle(query(""), null), "No open work items");
121+ assert.equal(emptyListTitle(query("state=closed"), null), "No closed work items");
122+});
+11−0
135135 return LINK_ROLES.find((role) => role === value) ?? null;
136136 }
137137
138+/**
139+ * What an empty Work list says: the repository it is filtered to, when
140+ * that is its only filter; that nothing matches, for any other; else that
141+ * there are none in this state.
142+ */
143+export function emptyListTitle(filters: WorkFilters, repoName: string | null): string {
144+ if (repoName && !filters.label && !filters.milestone && !filters.assignee) return `No ${filters.state} work items linked to ${repoName}`;
145+ if (filtered(filters)) return `No ${filters.state} work items match these filters`;
146+ return filters.state === "open" ? "No open work items" : "No closed work items";
147+}
148+
138149 /** Whether any filter but the state is set. */
139150 export function filtered(filters: WorkFilters): boolean {
140151 return Boolean(filters.label || filters.milestone || filters.assignee || filters.repo);
+17−13
1111 import { Unavailable } from "../../../components/boundary";
1212 import { colorsOf } from "../../../lib/labels";
1313 import { isFailed, part } from "../../../lib/partial";
14−import { identity, work } from "../../../lib/services.server";
14+import { identity, repos, work } from "../../../lib/services.server";
1515 import { getViewer, roleIn, unwrap } from "../../../lib/session.server";
16−import { filtered, newItemPath, repoFilter, shortPath, workFilters, workPath, workQuery } from "../../../lib/work-items";
16+import { emptyListTitle, filtered, newItemPath, repoFilter, shortPath, workFilters, workPath, workQuery } from "../../../lib/work-items";
17+import { canItem } from "../../../lib/work-access";
1718 import { workspaceProjects } from "../../../lib/workspace-projects.server";
1819 import { workPages } from "../../../lib/repo-live";
1920 import { useRefreshWhile } from "../../../lib/refresh";
5556 work.listWorkspaceSessions(workspace, viewer, { shown: "live", sessionsOnly: true, repo: filters.repo ? shortPath(filters.repo, workspace) : undefined, limit: 30 }).catch(() => null),
5657 ]);
5758 const live = cooking?.ok ? splitLive(cooking.value.sessions) : null;
59+ // Who may open one here, as New work item says (routes/workspace/work/new.tsx): a member; anyone else
60+ // only from a repository of the workspace (`?repo=web`) they may take part in.
61+ const short = filters.repo ? shortPath(filters.repo, workspace) : null;
62+ let canOpen = viewer != null && member;
63+ if (viewer && !member && short && !short.includes("/")) {
64+ const found = await repos.get({ namespace: workspace, name: short }, viewer).catch(() => null);
65+ canOpen = Boolean(
66+ found?.ok && canItem(viewer, workspace, { id: found.value.id, namespace: found.value.namespace, isPrivate: found.value.isPrivate }, "participate"),
67+ );
68+ }
5869 return {
70+ canOpen,
5971 workspace,
6072 filters,
6173 member,
7284 }
7385
7486 export default function WorkList({ loaderData }: Route.ComponentProps) {
75− const { workspace, filters, items, member, signedIn } = loaderData;
87+ const { workspace, filters, items, member } = loaderData;
7688 const { search } = useLocation();
7789 const colors = colorsOf(loaderData.labels ?? []);
7890 const repoName = filters.repo ? shortPath(filters.repo, workspace) : null;
94106 state={filters.state}
95107 query={kept.toString()}
96108 action={
97− signedIn && (member || repoName) ? (
109+ loaderData.canOpen ? (
98110 <ButtonLink to={newItemPath(workspace, repoName)} className="max-sm:h-10">
99111 <Plus size={15} />
100112 New work item
135147 <div className="mt-4">
136148 {items.length === 0 ? (
137149 <EmptyState
138− title={
139− filtered(filters)
140− ? `No ${filters.state} work items match these filters`
141− : repoName
142− ? `No ${filters.state} work items linked to ${repoName}`
143− : filters.state === "open"
144− ? "No open work items"
145− : "No closed work items"
146− }
150+ title={emptyListTitle(filters, repoName)}
147151 >
148152 A work item says what should change: a feature, a bug, a chore. It belongs to the workspace and lands in the repositories it
149153 names; people and their coding agents open pull requests for it.

Comments

Sign in to comment.