mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(webapp): Public links says "Loading…", not a premature "no" (BEA-78) (#127)
The public-links panel is where a member answers "is anything of ours public right now?". SharesTable read an empty array as a settled answer, and both callers hand it `shares || []` while the request is still in flight — so the panel printed "No public links." as a confident NO, then swapped to a table listing an active, never-expiring link. Fix at the choke point: an optional `loading` prop honored above the empty branch, in the same .admin-list/.admin-empty shell so the section is one row tall either way and doesn't jump when the data lands. Wired at both call sites — project Settings and the org-wide cross-project audit, which had the identical bug. `isLoading`, not `isPending`: OrgAdmin's shares query is `enabled: owner`, and TanStack reports isPending true forever for a disabled query. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7dd5d097fe
commit
7a20849631
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -313,6 +313,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."
|
||||
|
||||
@@ -63,12 +63,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>(), []);
|
||||
@@ -121,6 +125,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">
|
||||
|
||||
+14
-14
File diff suppressed because one or more lines are too long
@@ -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-xRtrzl4p.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BeL6RCAd.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-C62PcDae.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user