Skip to content
Open
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
29 changes: 29 additions & 0 deletions internal/webapp/frontend/e2e/browse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,35 @@ test("project settings lists this project's public links and revokes them", asyn
await expect(page.locator(".admin-empty", { hasText: "No public links." })).toBeVisible();
});

// "No public links." before the request lands is a confident no to "is anything
// of ours public right now?" — the one wrong answer this panel must never give.
test("public links show a loading row, never a premature 'no', while shares load", async ({
page,
}) => {
await login(page);
const pid = await wikiId(page);
const made = await (
await page.request.post(`/api/p/${pid}/shares`, { data: { path: "notes/readme.md" } })
).json();

await page.route("**/api/p/*/shares", async (route) => {
await new Promise((r) => setTimeout(r, 2000));
await route.continue();
});

await page.goto(`/${pid}/settings`);
await expect(page.locator(".admin-empty", { hasText: "Loading…" })).toBeVisible();
await expect(page.locator(".admin-empty", { hasText: "No public links." })).toHaveCount(0);

await expect(page.locator(".admin-item", { hasText: "notes/readme.md" })).toBeVisible({
timeout: 10_000,
});
await expect(page.locator(".admin-empty", { hasText: "Loading…" })).toHaveCount(0);

await page.unroute("**/api/p/*/shares");
await page.request.delete(`/api/shares/${made.token}`);
});

test("a read-only member sees the public-link banner but cannot revoke", async ({ page }) => {
await login(page);
const pid = await wikiId(page);
Expand Down
9 changes: 7 additions & 2 deletions internal/webapp/frontend/src/components/OrgAdmin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function OrgAdmin({
enabled: owner,
select: (d) => d.invites || [],
});
const { data: shares } = useQuery({
const { data: shares, isLoading: sharesLoading } = useQuery({
queryKey: ["orgShares", org.id],
queryFn: () => getJSON<{ shares: ShareInfo[] }>(`/api/orgs/${org.id}/shares`),
enabled: owner,
Expand Down Expand Up @@ -207,7 +207,12 @@ export function OrgAdmin({
Every live link across this organization's projects. A project's own links are on its
Settings page, and on the file itself.
</p>
<SharesTable shares={shares || []} onChanged={refreshShares} showProject />
<SharesTable
shares={shares || []}
loading={sharesLoading}
onChanged={refreshShares}
showProject
/>
</>
)}
</div>
Expand Down
3 changes: 2 additions & 1 deletion internal/webapp/frontend/src/components/ProjectSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ export function ProjectSettings({
// there to find one project's links.
function PublicLinks({ project }: { project: Project }) {
const qc = useQueryClient();
const { data: shares, error } = useShares(project.id);
const { data: shares, error, isLoading } = useShares(project.id);
if (error) return null; // sharing is off on this server, or single-volume mode
return (
<Card>
Expand All @@ -310,6 +310,7 @@ function PublicLinks({ project }: { project: Project }) {
<CardContent>
<SharesTable
shares={shares || []}
loading={isLoading}
canRevoke={atLeast(project.perm, "write")}
onChanged={() => qc.invalidateQueries({ queryKey: ["shares", project.id] })}
empty="No public links."
Expand Down
13 changes: 13 additions & 0 deletions internal/webapp/frontend/src/components/SharesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,16 @@ export function SharesTable({
showProject = false,
canRevoke = true,
empty = "No public shares.",
loading = false,
}: {
shares: ShareInfo[];
onChanged: () => void;
showProject?: boolean;
canRevoke?: boolean;
empty?: string;
// An empty array is not a settled answer while the request is in flight, and
// "nothing is public" is the one wrong answer nobody should read for a frame.
loading?: boolean;
}) {
const [sorting, setSorting] = useState<SortingState>([]);
const col = useMemo(() => createColumnHelper<ShareInfo>(), []);
Expand Down Expand Up @@ -99,6 +103,15 @@ export function SharesTable({
getSortedRowModel: getSortedRowModel(),
});

// Same shell and metrics as the empty state, so the section is one row tall
// either way and doesn't jump when the data lands.
if (loading)
return (
<div className="admin-list">
<div className="admin-empty">Loading…</div>
</div>
);

if (shares.length === 0)
return (
<div className="admin-list">
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion internal/webapp/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BearDrive</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
<script type="module" crossorigin src="/assets/index-ClP6ITud.js"></script>
<script type="module" crossorigin src="/assets/index-ClY_vPIC.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C52IQv2y.css">
</head>
<body>
Expand Down
Loading