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
3 files+39−130/3 viewed
| 5 | 5 | ||
| 6 | 6 | import { | |
| 7 | 7 | filterCount, | |
| 8 | + | emptyListTitle, | |
| 8 | 9 | filtered, | |
| 9 | 10 | movedWorkPath, | |
| 10 | 11 | newItemPath, | |
| ⋯ | |||
| 109 | 110 | assert.equal(movedWorkPath("/acme/web/pulls"), null); | |
| 110 | 111 | assert.equal(movedWorkPath("/acme/-/issues"), null); | |
| 111 | 112 | }); | |
| 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 | + | }); | |
| 135 | 135 | return LINK_ROLES.find((role) => role === value) ?? null; | |
| 136 | 136 | } | |
| 137 | 137 | ||
| 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 | + | ||
| 138 | 149 | /** Whether any filter but the state is set. */ | |
| 139 | 150 | export function filtered(filters: WorkFilters): boolean { | |
| 140 | 151 | return Boolean(filters.label || filters.milestone || filters.assignee || filters.repo); |
| 11 | 11 | import { Unavailable } from "../../../components/boundary"; | |
| 12 | 12 | import { colorsOf } from "../../../lib/labels"; | |
| 13 | 13 | import { isFailed, part } from "../../../lib/partial"; | |
| 14 | − | import { identity, work } from "../../../lib/services.server"; | |
| 14 | + | import { identity, repos, work } from "../../../lib/services.server"; | |
| 15 | 15 | 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"; | |
| 17 | 18 | import { workspaceProjects } from "../../../lib/workspace-projects.server"; | |
| 18 | 19 | import { workPages } from "../../../lib/repo-live"; | |
| 19 | 20 | import { useRefreshWhile } from "../../../lib/refresh"; | |
| ⋯ | |||
| 55 | 56 | work.listWorkspaceSessions(workspace, viewer, { shown: "live", sessionsOnly: true, repo: filters.repo ? shortPath(filters.repo, workspace) : undefined, limit: 30 }).catch(() => null), | |
| 56 | 57 | ]); | |
| 57 | 58 | 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 | + | } | |
| 58 | 69 | return { | |
| 70 | + | canOpen, | |
| 59 | 71 | workspace, | |
| 60 | 72 | filters, | |
| 61 | 73 | member, | |
| ⋯ | |||
| 72 | 84 | } | |
| 73 | 85 | ||
| 74 | 86 | export default function WorkList({ loaderData }: Route.ComponentProps) { | |
| 75 | − | const { workspace, filters, items, member, signedIn } = loaderData; | |
| 87 | + | const { workspace, filters, items, member } = loaderData; | |
| 76 | 88 | const { search } = useLocation(); | |
| 77 | 89 | const colors = colorsOf(loaderData.labels ?? []); | |
| 78 | 90 | const repoName = filters.repo ? shortPath(filters.repo, workspace) : null; | |
| ⋯ | |||
| 94 | 106 | state={filters.state} | |
| 95 | 107 | query={kept.toString()} | |
| 96 | 108 | action={ | |
| 97 | − | signedIn && (member || repoName) ? ( | |
| 109 | + | loaderData.canOpen ? ( | |
| 98 | 110 | <ButtonLink to={newItemPath(workspace, repoName)} className="max-sm:h-10"> | |
| 99 | 111 | <Plus size={15} /> | |
| 100 | 112 | New work item | |
| ⋯ | |||
| 135 | 147 | <div className="mt-4"> | |
| 136 | 148 | {items.length === 0 ? ( | |
| 137 | 149 | <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)} | |
| 147 | 151 | > | |
| 148 | 152 | A work item says what should change: a feature, a bug, a chore. It belongs to the workspace and lands in the repositories it | |
| 149 | 153 | names; people and their coding agents open pull requests for it. | |
Comments
Sign in to comment.