mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-06 20:15:17 +02:00
chore: linting
This commit is contained in:
parent
ac0027e2d2
commit
aeb2613b2b
11 changed files with 266 additions and 310 deletions
|
|
@ -627,30 +627,28 @@ export function DocumentsTableShell({
|
|||
<DocumentTypeChip type={doc.document_type} />
|
||||
</TableCell>
|
||||
)}
|
||||
{columnVisibility.created_by && (
|
||||
<TableCell className="w-36 py-2.5 text-sm text-foreground truncate border-r border-border/40">
|
||||
{doc.created_by_name ? (
|
||||
doc.created_by_email ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="cursor-default truncate block">
|
||||
{doc.created_by_name}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start">
|
||||
{doc.created_by_email}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{columnVisibility.created_by && (
|
||||
<TableCell className="w-36 py-2.5 text-sm text-foreground truncate border-r border-border/40">
|
||||
{doc.created_by_name ? (
|
||||
doc.created_by_email ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="cursor-default truncate block">
|
||||
{doc.created_by_name}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start">
|
||||
{doc.created_by_email}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span className="truncate block">{doc.created_by_name}</span>
|
||||
)
|
||||
) : (
|
||||
<span className="truncate block">{doc.created_by_name}</span>
|
||||
)
|
||||
) : (
|
||||
<span className="truncate block">
|
||||
{doc.created_by_email || "—"}
|
||||
</span>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
<span className="truncate block">{doc.created_by_email || "—"}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
)}
|
||||
{columnVisibility.created_at && (
|
||||
<TableCell className="w-32 py-2.5 text-sm text-foreground border-r border-border/40">
|
||||
<Tooltip>
|
||||
|
|
@ -784,20 +782,20 @@ export function DocumentsTableShell({
|
|||
|
||||
{/* Document Content Viewer - lazy loads content on-demand */}
|
||||
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] flex flex-col overflow-hidden pb-0">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>{viewingDoc?.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 overflow-y-auto flex-1 min-h-0 px-6 select-text">
|
||||
{viewingLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spinner size="lg" className="text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownViewer content={viewingContent} />
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
<DialogContent className="max-w-4xl max-h-[80vh] flex flex-col overflow-hidden pb-0">
|
||||
<DialogHeader className="flex-shrink-0">
|
||||
<DialogTitle>{viewingDoc?.title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="mt-4 overflow-y-auto flex-1 min-h-0 px-6 select-text">
|
||||
{viewingLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Spinner size="lg" className="text-muted-foreground" />
|
||||
</div>
|
||||
) : (
|
||||
<MarkdownViewer content={viewingContent} />
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</motion.div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -306,9 +306,7 @@ function SettingsContent({
|
|||
{activeSection === "public-links" && (
|
||||
<PublicChatSnapshotsManager searchSpaceId={searchSpaceId} />
|
||||
)}
|
||||
{activeSection === "team-roles" && (
|
||||
<RolesManager searchSpaceId={searchSpaceId} />
|
||||
)}
|
||||
{activeSection === "team-roles" && <RolesManager searchSpaceId={searchSpaceId} />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
|
@ -328,7 +326,8 @@ export default function SettingsPage() {
|
|||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
const sectionParam = searchParams.get("section");
|
||||
const activeSection = sectionParam && VALID_SECTIONS.has(sectionParam) ? sectionParam : DEFAULT_SECTION;
|
||||
const activeSection =
|
||||
sectionParam && VALID_SECTIONS.has(sectionParam) ? sectionParam : DEFAULT_SECTION;
|
||||
|
||||
const handleSectionChange = useCallback(
|
||||
(section: string) => {
|
||||
|
|
|
|||
|
|
@ -154,10 +154,7 @@ export default function TeamManagementPage() {
|
|||
[access]
|
||||
);
|
||||
|
||||
const {
|
||||
data: members = [],
|
||||
isLoading: membersLoading,
|
||||
} = useAtomValue(membersAtom);
|
||||
const { data: members = [], isLoading: membersLoading } = useAtomValue(membersAtom);
|
||||
|
||||
const { mutateAsync: updateMember } = useAtomValue(updateMemberMutationAtom);
|
||||
const { mutateAsync: deleteMember } = useAtomValue(deleteMemberMutationAtom);
|
||||
|
|
@ -211,17 +208,13 @@ export default function TeamManagementPage() {
|
|||
[deleteMember, searchSpaceId]
|
||||
);
|
||||
|
||||
const {
|
||||
data: roles = [],
|
||||
} = useQuery({
|
||||
const { data: roles = [] } = useQuery({
|
||||
queryKey: cacheKeys.roles.all(searchSpaceId.toString()),
|
||||
queryFn: () => rolesApiService.getRoles({ search_space_id: searchSpaceId }),
|
||||
enabled: !!searchSpaceId,
|
||||
});
|
||||
|
||||
const {
|
||||
data: invites = [],
|
||||
} = useQuery({
|
||||
const { data: invites = [] } = useQuery({
|
||||
queryKey: cacheKeys.invites.all(searchSpaceId.toString()),
|
||||
queryFn: () => invitesApiService.getInvites({ search_space_id: searchSpaceId }),
|
||||
staleTime: 5 * 60 * 1000,
|
||||
|
|
@ -271,12 +264,12 @@ export default function TeamManagementPage() {
|
|||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="bg-background select-none"
|
||||
>
|
||||
<div className="container max-w-5xl mx-auto p-4 md:p-6 lg:p-8 pt-20 md:pt-24 lg:pt-28">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-9 w-36 rounded-md" />
|
||||
className="bg-background select-none"
|
||||
>
|
||||
<div className="container max-w-5xl mx-auto p-4 md:p-6 lg:p-8 pt-20 md:pt-24 lg:pt-28">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-9 w-36 rounded-md" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
<div className="rounded-lg border border-border/40 bg-background overflow-hidden">
|
||||
|
|
@ -335,96 +328,93 @@ export default function TeamManagementPage() {
|
|||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="bg-background select-none"
|
||||
>
|
||||
<div className="container max-w-5xl mx-auto p-4 md:p-6 lg:p-8 pt-20 md:pt-24 lg:pt-28">
|
||||
<div className="space-y-6">
|
||||
{/* Header row: Invite button on left, member count on right */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{canInvite && (
|
||||
<CreateInviteDialog
|
||||
roles={roles}
|
||||
onCreateInvite={handleCreateInvite}
|
||||
searchSpaceId={searchSpaceId}
|
||||
/>
|
||||
)}
|
||||
{canInvite && activeInvites.length > 0 && (
|
||||
<AllInvitesDialog
|
||||
invites={activeInvites}
|
||||
onRevokeInvite={handleRevokeInvite}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p className="hidden md:block text-sm text-muted-foreground">
|
||||
{members.length} {members.length === 1 ? "member" : "members"}
|
||||
</p>
|
||||
className="bg-background select-none"
|
||||
>
|
||||
<div className="container max-w-5xl mx-auto p-4 md:p-6 lg:p-8 pt-20 md:pt-24 lg:pt-28">
|
||||
<div className="space-y-6">
|
||||
{/* Header row: Invite button on left, member count on right */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{canInvite && (
|
||||
<CreateInviteDialog
|
||||
roles={roles}
|
||||
onCreateInvite={handleCreateInvite}
|
||||
searchSpaceId={searchSpaceId}
|
||||
/>
|
||||
)}
|
||||
{canInvite && activeInvites.length > 0 && (
|
||||
<AllInvitesDialog invites={activeInvites} onRevokeInvite={handleRevokeInvite} />
|
||||
)}
|
||||
</div>
|
||||
<p className="hidden md:block text-sm text-muted-foreground">
|
||||
{members.length} {members.length === 1 ? "member" : "members"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Members & Invites Table */}
|
||||
<div className="rounded-lg border border-border/40 bg-background overflow-hidden">
|
||||
<Table className="table-fixed w-full">
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent border-b border-border/40">
|
||||
<TableHead className="w-[45%] px-4 md:px-6 border-r border-border/40">
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground/70">
|
||||
<User size={14} className="opacity-60 text-muted-foreground" />
|
||||
Name
|
||||
</span>
|
||||
</TableHead>
|
||||
<TableHead className="hidden md:table-cell w-[25%] border-r border-border/40">
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground/70">
|
||||
<Clock size={14} className="opacity-60 text-muted-foreground" />
|
||||
Last logged in
|
||||
</span>
|
||||
</TableHead>
|
||||
<TableHead className="w-[30%] px-4 md:px-6">
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground/70 justify-end">
|
||||
<ShieldUser size={14} className="opacity-60 text-muted-foreground" />
|
||||
Role
|
||||
</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{owners.map((member, index) => (
|
||||
<MemberRow
|
||||
key={`member-${member.id}`}
|
||||
member={member}
|
||||
roles={roles}
|
||||
canManageRoles={canManageRoles}
|
||||
canRemove={canRemove}
|
||||
onUpdateRole={handleUpdateMember}
|
||||
onRemoveMember={handleRemoveMember}
|
||||
searchSpaceId={searchSpaceId}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
{paginatedMembers.map((member, index) => (
|
||||
<MemberRow
|
||||
key={`member-${member.id}`}
|
||||
member={member}
|
||||
roles={roles}
|
||||
canManageRoles={canManageRoles}
|
||||
canRemove={canRemove}
|
||||
onUpdateRole={handleUpdateMember}
|
||||
onRemoveMember={handleRemoveMember}
|
||||
searchSpaceId={searchSpaceId}
|
||||
index={owners.length + index}
|
||||
/>
|
||||
))}
|
||||
{members.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center py-12">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Users className="h-8 w-8 text-muted-foreground/50" />
|
||||
<p className="text-muted-foreground">No members yet</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
<Table className="table-fixed w-full">
|
||||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent border-b border-border/40">
|
||||
<TableHead className="w-[45%] px-4 md:px-6 border-r border-border/40">
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground/70">
|
||||
<User size={14} className="opacity-60 text-muted-foreground" />
|
||||
Name
|
||||
</span>
|
||||
</TableHead>
|
||||
<TableHead className="hidden md:table-cell w-[25%] border-r border-border/40">
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground/70">
|
||||
<Clock size={14} className="opacity-60 text-muted-foreground" />
|
||||
Last logged in
|
||||
</span>
|
||||
</TableHead>
|
||||
<TableHead className="w-[30%] px-4 md:px-6">
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium text-muted-foreground/70 justify-end">
|
||||
<ShieldUser size={14} className="opacity-60 text-muted-foreground" />
|
||||
Role
|
||||
</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{owners.map((member, index) => (
|
||||
<MemberRow
|
||||
key={`member-${member.id}`}
|
||||
member={member}
|
||||
roles={roles}
|
||||
canManageRoles={canManageRoles}
|
||||
canRemove={canRemove}
|
||||
onUpdateRole={handleUpdateMember}
|
||||
onRemoveMember={handleRemoveMember}
|
||||
searchSpaceId={searchSpaceId}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
{paginatedMembers.map((member, index) => (
|
||||
<MemberRow
|
||||
key={`member-${member.id}`}
|
||||
member={member}
|
||||
roles={roles}
|
||||
canManageRoles={canManageRoles}
|
||||
canRemove={canRemove}
|
||||
onUpdateRole={handleUpdateMember}
|
||||
onRemoveMember={handleRemoveMember}
|
||||
searchSpaceId={searchSpaceId}
|
||||
index={owners.length + index}
|
||||
/>
|
||||
))}
|
||||
{members.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center py-12">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Users className="h-8 w-8 text-muted-foreground/50" />
|
||||
<p className="text-muted-foreground">No members yet</p>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
|
|
@ -513,7 +503,7 @@ function MemberRow({
|
|||
const initials = getAvatarInitials(member);
|
||||
const avatarColor = getAvatarColor(member.user_id);
|
||||
const displayName = member.user_display_name || member.user_email || "Unknown";
|
||||
const roleName = member.is_owner ? "Owner" : (member.role?.name || "No role");
|
||||
const roleName = member.is_owner ? "Owner" : member.role?.name || "No role";
|
||||
const showActions = !member.is_owner && (canManageRoles || canRemove);
|
||||
|
||||
return (
|
||||
|
|
@ -547,7 +537,9 @@ function MemberRow({
|
|||
<div className="min-w-0">
|
||||
<p className="font-medium text-sm truncate select-text">{displayName}</p>
|
||||
{member.user_display_name && member.user_email && (
|
||||
<p className="text-xs text-muted-foreground truncate select-text">{member.user_email}</p>
|
||||
<p className="text-xs text-muted-foreground truncate select-text">
|
||||
{member.user_email}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -569,7 +561,11 @@ function MemberRow({
|
|||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" onCloseAutoFocus={(e) => e.preventDefault()} className="min-w-[120px] bg-muted dark:border dark:border-neutral-700">
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
className="min-w-[120px] bg-muted dark:border dark:border-neutral-700"
|
||||
>
|
||||
{canManageRoles &&
|
||||
roles
|
||||
.filter((r) => r.name !== "Owner")
|
||||
|
|
@ -590,40 +586,39 @@ function MemberRow({
|
|||
>
|
||||
Remove
|
||||
</DropdownMenuItem>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove member?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will remove{" "}
|
||||
<span className="font-medium">{member.user_email}</span>{" "}
|
||||
from this search space. They will lose access to all resources.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => onRemoveMember(member.id)}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Remove
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Remove member?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will remove <span className="font-medium">{member.user_email}</span>{" "}
|
||||
from this search space. They will lose access to all resources.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => onRemoveMember(member.id)}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Remove
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
<DropdownMenuSeparator className="dark:bg-neutral-700" />
|
||||
<DropdownMenuItem
|
||||
onClick={() => router.push(`/dashboard/${searchSpaceId}/settings?section=team-roles`)}
|
||||
onClick={() =>
|
||||
router.push(`/dashboard/${searchSpaceId}/settings?section=team-roles`)
|
||||
}
|
||||
>
|
||||
Manage Roles
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<span className="text-sm text-foreground">
|
||||
{roleName}
|
||||
</span>
|
||||
<span className="text-sm text-foreground">{roleName}</span>
|
||||
)}
|
||||
</TableCell>
|
||||
</motion.tr>
|
||||
|
|
@ -671,9 +666,7 @@ function CreateInviteDialog({
|
|||
const invite = await onCreateInvite(data);
|
||||
setCreatedInvite(invite);
|
||||
|
||||
const roleName = roleId
|
||||
? roles.find((r) => r.id.toString() === roleId)?.name
|
||||
: undefined;
|
||||
const roleName = roleId ? roles.find((r) => r.id.toString() === roleId)?.name : undefined;
|
||||
trackSearchSpaceInviteSent(searchSpaceId, {
|
||||
roleName,
|
||||
hasExpiry: !!expiresAt,
|
||||
|
|
@ -707,13 +700,19 @@ function CreateInviteDialog({
|
|||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => (v ? setOpen(true) : handleClose())}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="gap-2 bg-black text-white dark:bg-white dark:text-black hover:bg-black/90 dark:hover:bg-white/90">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 bg-black text-white dark:bg-white dark:text-black hover:bg-black/90 dark:hover:bg-white/90"
|
||||
>
|
||||
<UserPlus className="h-4 w-4" />
|
||||
Invite members
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6 select-none" onOpenAutoFocus={(e) => e.preventDefault()}>
|
||||
{createdInvite ? (
|
||||
<DialogContent
|
||||
className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6 select-none"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
{createdInvite ? (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
|
|
@ -730,11 +729,7 @@ function CreateInviteDialog({
|
|||
{window.location.origin}/invite/{createdInvite.invite_code}
|
||||
</code>
|
||||
<Button variant="outline" size="sm" onClick={copyLink} className="shrink-0">
|
||||
{copiedLink ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
{copiedLink ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground">
|
||||
|
|
@ -891,24 +886,18 @@ function AllInvitesDialog({
|
|||
</DialogTrigger>
|
||||
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-lg p-4 md:p-6 select-none">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
Active Invite Links
|
||||
</DialogTitle>
|
||||
<DialogTitle className="flex items-center gap-2">Active Invite Links</DialogTitle>
|
||||
<DialogDescription>
|
||||
{invites.length} active {invites.length === 1 ? "invite" : "invites"}. Copy a link or revoke access.
|
||||
{invites.length} active {invites.length === 1 ? "invite" : "invites"}. Copy a link or
|
||||
revoke access.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="max-h-[320px] overflow-y-auto -mx-1 px-1 space-y-3 py-2">
|
||||
{invites.map((invite) => (
|
||||
<div
|
||||
key={invite.id}
|
||||
className="rounded-lg border border-border/40 p-3 space-y-2.5"
|
||||
>
|
||||
<div key={invite.id} className="rounded-lg border border-border/40 p-3 space-y-2.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<p className="text-sm font-medium truncate">
|
||||
{invite.name || "Unnamed invite"}
|
||||
</p>
|
||||
<p className="text-sm font-medium truncate">{invite.name || "Unnamed invite"}</p>
|
||||
<div className="flex flex-wrap gap-x-2 text-xs text-muted-foreground shrink-0">
|
||||
{invite.role?.name && (
|
||||
<span className="rounded bg-muted px-1.5 py-0.5">{invite.role.name}</span>
|
||||
|
|
@ -929,7 +918,11 @@ function AllInvitesDialog({
|
|||
</div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0 text-muted-foreground hover:text-destructive">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 shrink-0 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
|
|
@ -937,8 +930,8 @@ function AllInvitesDialog({
|
|||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Revoke invite?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete this invite link. Anyone with this link
|
||||
will no longer be able to join.
|
||||
This will permanently delete this invite link. Anyone with this link will no
|
||||
longer be able to join.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
|
|
@ -961,7 +954,12 @@ function AllInvitesDialog({
|
|||
: `/invite/${invite.invite_code}`}
|
||||
</code>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="shrink-0" onClick={() => copyLink(invite)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
onClick={() => copyLink(invite)}
|
||||
>
|
||||
{copiedId === invite.id ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -747,10 +747,14 @@ const ComposerAction: FC<ComposerActionProps> = ({
|
|||
<div className="flex items-center gap-1">
|
||||
<TooltipIconButton
|
||||
tooltip={
|
||||
isUploadingDocs ? "Uploading documents..." : (
|
||||
isUploadingDocs ? (
|
||||
"Uploading documents..."
|
||||
) : (
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="font-medium">Upload and mention files</span>
|
||||
<span className="text-xs text-muted-foreground flex items-center">Max 10 files <Dot className="size-3" /> 50 MB each</span>
|
||||
<span className="text-xs text-muted-foreground flex items-center">
|
||||
Max 10 files <Dot className="size-3" /> 50 MB each
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">Total upload limit: 200 MB</span>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -159,9 +159,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
|
|||
{t("creating")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t("create_button")}
|
||||
</>
|
||||
<>{t("create_button")}</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
|
|
|||
|
|
@ -409,19 +409,19 @@ export function AllPrivateChatsSidebar({
|
|||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||
{!thread.archived && (
|
||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||
{!thread.archived && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleStartRename(thread.id, thread.title || "New Chat")}
|
||||
>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
<span>{t("rename") || "Rename"}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleStartRename(thread.id, thread.title || "New Chat")}
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
<span>{t("rename") || "Rename"}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
>
|
||||
{thread.archived ? (
|
||||
<>
|
||||
<RotateCcwIcon className="mr-2 h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -409,20 +409,20 @@ export function AllSharedChatsSidebar({
|
|||
<span className="sr-only">{t("more_options") || "More options"}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||
{!thread.archived && (
|
||||
<DropdownMenuContent align="end" className="w-40 z-80">
|
||||
{!thread.archived && (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleStartRename(thread.id, thread.title || "New Chat")}
|
||||
>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
<span>{t("rename") || "Rename"}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleStartRename(thread.id, thread.title || "New Chat")}
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
>
|
||||
<PenLine className="mr-2 h-4 w-4" />
|
||||
<span>{t("rename") || "Rename"}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleToggleArchive(thread.id, thread.archived)}
|
||||
disabled={isArchiving}
|
||||
>
|
||||
{thread.archived ? (
|
||||
{thread.archived ? (
|
||||
<>
|
||||
<RotateCcwIcon className="mr-2 h-4 w-4" />
|
||||
<span>{t("unarchive") || "Restore"}</span>
|
||||
|
|
|
|||
|
|
@ -381,11 +381,7 @@ export function SidebarUserProfile({
|
|||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}>
|
||||
{isLoggingOut ? (
|
||||
<Spinner size="sm" className="mr-2" />
|
||||
) : (
|
||||
<LogOut className="h-4 w-4" />
|
||||
)}
|
||||
{isLoggingOut ? <Spinner size="sm" className="mr-2" /> : <LogOut className="h-4 w-4" />}
|
||||
{isLoggingOut ? t("loggingOut") : t("logout")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
|
|||
|
|
@ -144,7 +144,9 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
|
|||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push(`/dashboard/${params.search_space_id}/settings?section=public-links`)}
|
||||
onClick={() =>
|
||||
router.push(`/dashboard/${params.search_space_id}/settings?section=public-links`)
|
||||
}
|
||||
className="flex items-center justify-center h-8 w-8 rounded-md bg-muted/50 hover:bg-muted transition-colors"
|
||||
>
|
||||
<Earth className="h-4 w-4 text-muted-foreground" />
|
||||
|
|
|
|||
|
|
@ -364,11 +364,7 @@ function ReportPanelContent({
|
|||
{versions.length > 1 && (
|
||||
<DropdownMenu modal={insideDrawer ? false : undefined}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 px-3.5 py-4 text-[15px] gap-1.5"
|
||||
>
|
||||
<Button variant="outline" size="sm" className="h-8 px-3.5 py-4 text-[15px] gap-1.5">
|
||||
v{activeVersionIndex + 1}
|
||||
<ChevronDownIcon className="size-3" />
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -156,7 +156,6 @@ const ACTION_LABELS: Record<string, string> = {
|
|||
manage_roles: "Manage Roles",
|
||||
};
|
||||
|
||||
|
||||
const ROLE_PRESETS = {
|
||||
editor: {
|
||||
name: "Editor",
|
||||
|
|
@ -241,13 +240,9 @@ export function RolesManager({ searchSpaceId }: { searchSpaceId: number }) {
|
|||
[access]
|
||||
);
|
||||
|
||||
const {
|
||||
data: roles = [],
|
||||
isLoading: rolesLoading,
|
||||
} = useQuery({
|
||||
const { data: roles = [], isLoading: rolesLoading } = useQuery({
|
||||
queryKey: cacheKeys.roles.all(searchSpaceId.toString()),
|
||||
queryFn: () =>
|
||||
rolesApiService.getRoles({ search_space_id: searchSpaceId }),
|
||||
queryFn: () => rolesApiService.getRoles({ search_space_id: searchSpaceId }),
|
||||
enabled: !!searchSpaceId,
|
||||
});
|
||||
|
||||
|
|
@ -358,14 +353,10 @@ function RolePermissionsDialog({
|
|||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
{children}
|
||||
</DialogTrigger>
|
||||
<DialogTrigger asChild>{children}</DialogTrigger>
|
||||
<DialogContent className="w-[92vw] max-w-md p-0 gap-0">
|
||||
<DialogHeader className="p-4 md:p-5">
|
||||
<DialogTitle className="text-base">
|
||||
{roleName} — Permissions
|
||||
</DialogTitle>
|
||||
<DialogTitle className="text-base">{roleName} — Permissions</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
{isFullAccess
|
||||
? "This role has unrestricted access to all resources"
|
||||
|
|
@ -379,7 +370,9 @@ function RolePermissionsDialog({
|
|||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Full access</p>
|
||||
<p className="text-xs text-muted-foreground">All permissions granted across every category</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
All permissions granted across every category
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -399,9 +392,7 @@ function RolePermissionsDialog({
|
|||
>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<IconComponent className="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{config.label}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">{config.label}</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-end gap-1">
|
||||
{actions.map((action) => (
|
||||
|
|
@ -409,8 +400,7 @@ function RolePermissionsDialog({
|
|||
key={action}
|
||||
className="px-1.5 py-0.5 rounded bg-muted text-muted-foreground text-[11px] font-medium"
|
||||
>
|
||||
{ACTION_LABELS[action] ||
|
||||
action.replace(/_/g, " ")}
|
||||
{ACTION_LABELS[action] || action.replace(/_/g, " ")}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -435,7 +425,9 @@ function PermissionsBadge({ permissions }: { permissions: string[] }) {
|
|||
}
|
||||
return (
|
||||
<div className="px-2.5 py-1 rounded-md border border-border/60 bg-muted/50 text-muted-foreground">
|
||||
<span className="text-xs font-medium whitespace-nowrap">{permissions.length} permissions</span>
|
||||
<span className="text-xs font-medium whitespace-nowrap">
|
||||
{permissions.length} permissions
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -514,15 +506,17 @@ function RolesContent({
|
|||
{editingRole && (
|
||||
<EditRoleDialog
|
||||
open={!!editingRole}
|
||||
onOpenChange={(open) => { if (!open) setEditingRoleId(null); }}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setEditingRoleId(null);
|
||||
}}
|
||||
role={editingRole}
|
||||
groupedPermissions={groupedPermissions}
|
||||
onUpdateRole={onUpdateRole}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{roles.map((role, index) => (
|
||||
<div className="space-y-3">
|
||||
{roles.map((role, index) => (
|
||||
<motion.div
|
||||
key={role.id}
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
|
|
@ -560,17 +554,19 @@ function RolesContent({
|
|||
</div>
|
||||
|
||||
{!role.is_system_role && (
|
||||
<div className="shrink-0" role="none" onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
|
||||
<div
|
||||
className="shrink-0"
|
||||
role="none"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<DropdownMenuContent align="end" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||
{canUpdate && (
|
||||
<DropdownMenuItem onClick={() => setEditingRoleId(role.id)}>
|
||||
<Edit2 className="h-4 w-4 mr-2" />
|
||||
|
|
@ -649,18 +645,14 @@ function PermissionsEditor({
|
|||
|
||||
const toggleCategoryExpanded = useCallback((category: string) => {
|
||||
setExpandedCategories((prev) =>
|
||||
prev.includes(category)
|
||||
? prev.filter((c) => c !== category)
|
||||
: [...prev, category]
|
||||
prev.includes(category) ? prev.filter((c) => c !== category) : [...prev, category]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const getCategoryStats = useCallback(
|
||||
(category: string) => {
|
||||
const perms = groupedPermissions[category] || [];
|
||||
const selected = perms.filter((p) =>
|
||||
selectedPermissions.includes(p.value)
|
||||
).length;
|
||||
const selected = perms.filter((p) => selectedPermissions.includes(p.value)).length;
|
||||
return {
|
||||
selected,
|
||||
total: perms.length,
|
||||
|
|
@ -683,15 +675,11 @@ function PermissionsEditor({
|
|||
className="text-xs h-7"
|
||||
onClick={() =>
|
||||
setExpandedCategories(
|
||||
expandedCategories.length === sortedCategories.length
|
||||
? []
|
||||
: sortedCategories
|
||||
expandedCategories.length === sortedCategories.length ? [] : sortedCategories
|
||||
)
|
||||
}
|
||||
>
|
||||
{expandedCategories.length === sortedCategories.length
|
||||
? "Collapse All"
|
||||
: "Expand All"}
|
||||
{expandedCategories.length === sortedCategories.length ? "Collapse All" : "Expand All"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
|
@ -709,10 +697,7 @@ function PermissionsEditor({
|
|||
const perms = groupedPermissions[category] || [];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={category}
|
||||
className="rounded-lg border border-border/60 overflow-hidden"
|
||||
>
|
||||
<div key={category} className="rounded-lg border border-border/60 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center justify-between px-3 py-2.5 cursor-pointer hover:bg-muted/40 transition-colors"
|
||||
|
|
@ -775,9 +760,7 @@ function PermissionsEditor({
|
|||
type="button"
|
||||
className={cn(
|
||||
"w-full flex items-center justify-between gap-3 px-2.5 py-2 rounded-md cursor-pointer transition-colors",
|
||||
isSelected
|
||||
? "bg-muted/60 hover:bg-muted/80"
|
||||
: "hover:bg-muted/40"
|
||||
isSelected ? "bg-muted/60 hover:bg-muted/80" : "hover:bg-muted/40"
|
||||
)}
|
||||
onClick={() => onTogglePermission(perm.value)}
|
||||
>
|
||||
|
|
@ -858,28 +841,19 @@ function CreateRoleDialog({
|
|||
|
||||
const togglePermission = useCallback((perm: string) => {
|
||||
setSelectedPermissions((prev) =>
|
||||
prev.includes(perm)
|
||||
? prev.filter((p) => p !== perm)
|
||||
: [...prev, perm]
|
||||
prev.includes(perm) ? prev.filter((p) => p !== perm) : [...prev, perm]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const toggleCategory = useCallback(
|
||||
(category: string) => {
|
||||
const categoryPerms =
|
||||
groupedPermissions[category]?.map((p) => p.value) || [];
|
||||
const allSelected = categoryPerms.every((p) =>
|
||||
selectedPermissions.includes(p)
|
||||
);
|
||||
const categoryPerms = groupedPermissions[category]?.map((p) => p.value) || [];
|
||||
const allSelected = categoryPerms.every((p) => selectedPermissions.includes(p));
|
||||
|
||||
if (allSelected) {
|
||||
setSelectedPermissions((prev) =>
|
||||
prev.filter((p) => !categoryPerms.includes(p))
|
||||
);
|
||||
setSelectedPermissions((prev) => prev.filter((p) => !categoryPerms.includes(p)));
|
||||
} else {
|
||||
setSelectedPermissions((prev) => [
|
||||
...new Set([...prev, ...categoryPerms]),
|
||||
]);
|
||||
setSelectedPermissions((prev) => [...new Set([...prev, ...categoryPerms])]);
|
||||
}
|
||||
},
|
||||
[groupedPermissions, selectedPermissions]
|
||||
|
|
@ -1063,28 +1037,19 @@ function EditRoleDialog({
|
|||
|
||||
const togglePermission = useCallback((perm: string) => {
|
||||
setSelectedPermissions((prev) =>
|
||||
prev.includes(perm)
|
||||
? prev.filter((p) => p !== perm)
|
||||
: [...prev, perm]
|
||||
prev.includes(perm) ? prev.filter((p) => p !== perm) : [...prev, perm]
|
||||
);
|
||||
}, []);
|
||||
|
||||
const toggleCategory = useCallback(
|
||||
(category: string) => {
|
||||
const categoryPerms =
|
||||
groupedPermissions[category]?.map((p) => p.value) || [];
|
||||
const allSelected = categoryPerms.every((p) =>
|
||||
selectedPermissions.includes(p)
|
||||
);
|
||||
const categoryPerms = groupedPermissions[category]?.map((p) => p.value) || [];
|
||||
const allSelected = categoryPerms.every((p) => selectedPermissions.includes(p));
|
||||
|
||||
if (allSelected) {
|
||||
setSelectedPermissions((prev) =>
|
||||
prev.filter((p) => !categoryPerms.includes(p))
|
||||
);
|
||||
setSelectedPermissions((prev) => prev.filter((p) => !categoryPerms.includes(p)));
|
||||
} else {
|
||||
setSelectedPermissions((prev) => [
|
||||
...new Set([...prev, ...categoryPerms]),
|
||||
]);
|
||||
setSelectedPermissions((prev) => [...new Set([...prev, ...categoryPerms])]);
|
||||
}
|
||||
},
|
||||
[groupedPermissions, selectedPermissions]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue