- );
+ return ;
}
diff --git a/internal/webapp/frontend/src/components/ProjectSettings.tsx b/internal/webapp/frontend/src/components/ProjectSettings.tsx
index 458863d..3d84079 100644
--- a/internal/webapp/frontend/src/components/ProjectSettings.tsx
+++ b/internal/webapp/frontend/src/components/ProjectSettings.tsx
@@ -6,8 +6,9 @@ import { useQueryClient } from "@tanstack/react-query";
import { api } from "../api/http";
import { modalConfirm, modalPrompt } from "../modal";
import { toast } from "../toast";
-import { useHubRefresh, usePermissions } from "../hooks/useHub";
+import { useHubRefresh, usePermissions, useShares } from "../hooks/useHub";
import { PROJECT_ICONS, ProjectIcon } from "./shell";
+import { SharesTable } from "./SharesTable";
import { projColor } from "./ProjectNav";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
@@ -247,6 +248,8 @@ export function ProjectSettings({
+
+
{/* Admin-only, and only as UX: handleProjectDelete enforces it too. */}
{mayEdit && (
@@ -288,6 +291,34 @@ export function ProjectSettings({
);
}
+// Public links: this project's live share URLs, where its own settings are.
+// The org panel keeps the cross-project audit; nobody should have to go
+// there to find one project's links.
+function PublicLinks({ project }: { project: Project }) {
+ const qc = useQueryClient();
+ const { data: shares, error } = useShares(project.id);
+ if (error) return null; // sharing is off on this server, or single-volume mode
+ return (
+
+
+ Public links
+
+ Files in this project that anyone with the URL can read — no account needed.
+
+
+
+
+ qc.invalidateQueries({ queryKey: ["shares", project.id] })}
+ empty="No public links."
+ />
+
+
+ );
+}
+
const LEVELS: Array<{ value: PermLevel; label: string }> = [
{ value: "admin", label: "Admin" },
{ value: "write", label: "Write" },
diff --git a/internal/webapp/frontend/src/components/ShareBanner.tsx b/internal/webapp/frontend/src/components/ShareBanner.tsx
new file mode 100644
index 0000000..c7543a3
--- /dev/null
+++ b/internal/webapp/frontend/src/components/ShareBanner.tsx
@@ -0,0 +1,72 @@
+import { Button } from "@/components/ui/button";
+import type { ShareInfo } from "../api/types";
+import { copyText } from "../util";
+import { toast } from "../toast";
+import { Icon } from "./shell";
+import { revokeShare, shareDetail } from "./SharesTable";
+
+/* A file that is publicly reachable says so while you are reading it. The
+ Share dialog used to be the only place the link — and its Revoke button —
+ existed, so once it closed the undo lived three clicks away in the org
+ panel while the action was one click away here.
+
+ Anyone with read sees the banner (a member should know the folder they
+ rely on is exposed); Revoke is offered where the Share button already is,
+ and the server checks again anyway. */
+export function ShareBanner({
+ shares,
+ canRevoke,
+ onChanged,
+}: {
+ shares: ShareInfo[];
+ canRevoke: boolean;
+ onChanged: () => void;
+}) {
+ if (shares.length === 0) return null;
+ return (
+
+ );
+}
diff --git a/internal/webapp/frontend/src/components/ShareDialog.tsx b/internal/webapp/frontend/src/components/ShareDialog.tsx
index 811d6cf..38550cb 100644
--- a/internal/webapp/frontend/src/components/ShareDialog.tsx
+++ b/internal/webapp/frontend/src/components/ShareDialog.tsx
@@ -5,7 +5,9 @@ import { toast } from "../toast";
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
/* A clear, explicitly-public share confirmation: warns that anyone with the
- link can view, and offers copy / open / revoke. */
+ link can view, and offers copy / open / revoke. The title says "Public
+ link", not "created": ShareDB.Create hands back the file's existing live
+ link when there is one, so a second Share click is not a second link. */
export function ShareDialog({
url,
copied,
@@ -20,7 +22,7 @@ export function ShareDialog({