diff --git a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx index 7b8fe49a5..34ad273e1 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/team/page.tsx @@ -13,6 +13,7 @@ import { Clock, Copy, Hash, + Link2, ShieldUser, Trash2, UserPlus, @@ -136,17 +137,6 @@ function getAvatarInitials(member: Membership): string { return "U"; } -function getInviteInitials(invite: Invite): string { - if (invite.name) { - const parts = invite.name.trim().split(/\s+/); - if (parts.length >= 2) { - return (parts[0][0] + parts[1][0]).toUpperCase(); - } - return invite.name.slice(0, 2).toUpperCase(); - } - return "IN"; -} - const PAGE_SIZE = 10; export default function TeamManagementPage() { @@ -247,7 +237,7 @@ export default function TeamManagementPage() { const nonOwnerMembers = useMemo(() => members.filter((m) => !m.is_owner), [members]); const [pageIndex, setPageIndex] = useState(0); - const totalItems = nonOwnerMembers.length + activeInvites.length; + const totalItems = nonOwnerMembers.length; const lastPage = Math.max(0, Math.ceil(totalItems / PAGE_SIZE) - 1); useEffect(() => { @@ -263,14 +253,6 @@ export default function TeamManagementPage() { ); }, [nonOwnerMembers, pageIndex]); - const paginatedInvites = useMemo(() => { - const start = pageIndex * PAGE_SIZE; - const end = start + PAGE_SIZE; - const inviteStart = Math.max(0, start - nonOwnerMembers.length); - const inviteEnd = Math.max(0, end - nonOwnerMembers.length); - return activeInvites.slice(inviteStart, inviteEnd); - }, [activeInvites, nonOwnerMembers.length, pageIndex]); - const displayStart = totalItems > 0 ? pageIndex * PAGE_SIZE + 1 : 0; const displayEnd = Math.min((pageIndex + 1) * PAGE_SIZE, totalItems); const canPrev = pageIndex > 0; @@ -358,7 +340,8 @@ export default function TeamManagementPage() {
{/* Header row: Invite button on left, member count on right */} -
+
+
{canInvite && ( )} - {!canInvite &&
} + {canInvite && activeInvites.length > 0 && ( + + )} +

{members.length} {members.length === 1 ? "member" : "members"}

@@ -424,16 +413,7 @@ export default function TeamManagementPage() { index={owners.length + index} /> ))} - {paginatedInvites.map((invite, index) => ( - - ))} - {members.length === 0 && activeInvites.length === 0 && ( + {members.length === 0 && (
@@ -650,117 +630,6 @@ function MemberRow({ ); } -// ============ Invite Row ============ - -function InviteRow({ - invite, - canRevoke, - onRevokeInvite, - index, -}: { - invite: Invite; - canRevoke: boolean; - onRevokeInvite: (inviteId: number) => Promise; - index: number; -}) { - const initials = getInviteInitials(invite); - const avatarColor = getAvatarColor(invite.invite_code); - const displayName = invite.name || "Unnamed Invite"; - - return ( - - -
-
- {initials} -
-
-

{displayName}

- {invite.role?.name && ( -

- Will join as {invite.role.name} -

- )} -
-
-
- - - Never - - - - {canRevoke ? ( - - - - - e.preventDefault()}> - { - const link = `${window.location.origin}/invite/${invite.invite_code}`; - navigator.clipboard.writeText(link); - toast.success("Invite link copied"); - }} - > - - Copy invite link - - - - - e.preventDefault()} - > - - Revoke invite - - - - - Revoke invite? - - This will permanently delete this invite link. Anyone with this link - will no longer be able to join. - - - - Cancel - onRevokeInvite(invite.id)} - className="bg-destructive text-destructive-foreground hover:bg-destructive/90" - > - Revoke - - - - - - - ) : ( - Invited - )} - -
- ); -} - // ============ Create Invite Dialog ============ function CreateInviteDialog({ @@ -843,8 +712,8 @@ function CreateInviteDialog({ Invite members - - {createdInvite ? ( + e.preventDefault()}> + {createdInvite ? ( <> @@ -989,3 +858,119 @@ function CreateInviteDialog({ ); } + +// ============ All Invites Dialog ============ + +function AllInvitesDialog({ + invites, + onRevokeInvite, +}: { + invites: Invite[]; + onRevokeInvite: (inviteId: number) => Promise; +}) { + const [copiedId, setCopiedId] = useState(null); + + const copyLink = (invite: Invite) => { + const link = `${window.location.origin}/invite/${invite.invite_code}`; + navigator.clipboard.writeText(link); + setCopiedId(invite.id); + toast.success("Invite link copied"); + setTimeout(() => setCopiedId(null), 2000); + }; + + return ( + + + + + + + + Active Invite Links + + + {invites.length} active {invites.length === 1 ? "invite" : "invites"}. Copy a link or revoke access. + + +
+ {invites.map((invite) => ( +
+
+
+

+ {invite.name || "Unnamed invite"} +

+
+ {invite.role?.name && ( + {invite.role.name} + )} + {invite.max_uses != null && ( + + + {invite.uses_count}/{invite.max_uses} + + )} + {invite.expires_at && ( + + + {new Date(invite.expires_at).toLocaleDateString()} + + )} +
+
+ + + + + + + Revoke invite? + + This will permanently delete this invite link. Anyone with this link + will no longer be able to join. + + + + Cancel + onRevokeInvite(invite.id)} + className="bg-destructive text-destructive-foreground hover:bg-destructive/90" + > + Revoke + + + + +
+
+ + {typeof window !== "undefined" + ? `${window.location.origin}/invite/${invite.invite_code}` + : `/invite/${invite.invite_code}`} + + +
+
+ ))} +
+
+
+ ); +} diff --git a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx index aee98a290..4e95c381f 100644 --- a/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx +++ b/surfsense_web/components/layout/ui/sidebar/Sidebar.tsx @@ -93,7 +93,7 @@ export function Sidebar({ return (
- + - + {t("manage_members")} - + {t("search_space_settings")} diff --git a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx index 3ba7bb2a0..7f3a97f93 100644 --- a/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx +++ b/surfsense_web/components/layout/ui/sidebar/SidebarUserProfile.tsx @@ -177,7 +177,7 @@ export function SidebarUserProfile({ {displayName} - +
@@ -191,14 +191,14 @@ export function SidebarUserProfile({ - + {t("user_settings")} {setTheme && ( - + {t("theme")} @@ -216,7 +216,7 @@ export function SidebarUserProfile({ isSelected && "text-primary" )} > - + {t(themeOption.value)} {isSelected && } @@ -229,7 +229,7 @@ export function SidebarUserProfile({ - + {t("language")} @@ -262,7 +262,7 @@ export function SidebarUserProfile({ {isLoggingOut ? ( ) : ( - + )} {isLoggingOut ? t("loggingOut") : t("logout")} @@ -299,7 +299,7 @@ export function SidebarUserProfile({ - +
@@ -313,14 +313,14 @@ export function SidebarUserProfile({ - + {t("user_settings")} {setTheme && ( - + {t("theme")} @@ -338,7 +338,7 @@ export function SidebarUserProfile({ isSelected && "text-primary" )} > - + {t(themeOption.value)} {isSelected && } @@ -351,7 +351,7 @@ export function SidebarUserProfile({ - + {t("language")} @@ -384,7 +384,7 @@ export function SidebarUserProfile({ {isLoggingOut ? ( ) : ( - + )} {isLoggingOut ? t("loggingOut") : t("logout")} diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx index 98911fe38..d894fe252 100644 --- a/surfsense_web/components/new-chat/model-selector.tsx +++ b/surfsense_web/components/new-chat/model-selector.tsx @@ -226,7 +226,7 @@ export function ModelSelector({ size="sm" role="combobox" aria-expanded={open} - className={cn("h-8 gap-2 px-3 text-sm border-border/60", className)} + className={cn("h-8 gap-2 px-3 text-sm border-border/60 select-none", className)} > {isLoading ? ( <> @@ -280,7 +280,7 @@ export function ModelSelector({