chore: linting

This commit is contained in:
Anish Sarkar 2026-02-22 04:41:56 +05:30
parent ac0027e2d2
commit aeb2613b2b
11 changed files with 266 additions and 310 deletions

View file

@ -627,30 +627,28 @@ export function DocumentsTableShell({
<DocumentTypeChip type={doc.document_type} /> <DocumentTypeChip type={doc.document_type} />
</TableCell> </TableCell>
)} )}
{columnVisibility.created_by && ( {columnVisibility.created_by && (
<TableCell className="w-36 py-2.5 text-sm text-foreground truncate border-r border-border/40"> <TableCell className="w-36 py-2.5 text-sm text-foreground truncate border-r border-border/40">
{doc.created_by_name ? ( {doc.created_by_name ? (
doc.created_by_email ? ( doc.created_by_email ? (
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="cursor-default truncate block"> <span className="cursor-default truncate block">
{doc.created_by_name} {doc.created_by_name}
</span> </span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top" align="start"> <TooltipContent side="top" align="start">
{doc.created_by_email} {doc.created_by_email}
</TooltipContent> </TooltipContent>
</Tooltip> </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 && ( {columnVisibility.created_at && (
<TableCell className="w-32 py-2.5 text-sm text-foreground border-r border-border/40"> <TableCell className="w-32 py-2.5 text-sm text-foreground border-r border-border/40">
<Tooltip> <Tooltip>
@ -784,20 +782,20 @@ export function DocumentsTableShell({
{/* Document Content Viewer - lazy loads content on-demand */} {/* Document Content Viewer - lazy loads content on-demand */}
<Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}> <Dialog open={!!viewingDoc} onOpenChange={(open) => !open && handleCloseViewer()}>
<DialogContent className="max-w-4xl max-h-[80vh] flex flex-col overflow-hidden pb-0"> <DialogContent className="max-w-4xl max-h-[80vh] flex flex-col overflow-hidden pb-0">
<DialogHeader className="flex-shrink-0"> <DialogHeader className="flex-shrink-0">
<DialogTitle>{viewingDoc?.title}</DialogTitle> <DialogTitle>{viewingDoc?.title}</DialogTitle>
</DialogHeader> </DialogHeader>
<div className="mt-4 overflow-y-auto flex-1 min-h-0 px-6 select-text"> <div className="mt-4 overflow-y-auto flex-1 min-h-0 px-6 select-text">
{viewingLoading ? ( {viewingLoading ? (
<div className="flex items-center justify-center py-12"> <div className="flex items-center justify-center py-12">
<Spinner size="lg" className="text-muted-foreground" /> <Spinner size="lg" className="text-muted-foreground" />
</div> </div>
) : ( ) : (
<MarkdownViewer content={viewingContent} /> <MarkdownViewer content={viewingContent} />
)} )}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</motion.div> </motion.div>
); );

View file

@ -306,9 +306,7 @@ function SettingsContent({
{activeSection === "public-links" && ( {activeSection === "public-links" && (
<PublicChatSnapshotsManager searchSpaceId={searchSpaceId} /> <PublicChatSnapshotsManager searchSpaceId={searchSpaceId} />
)} )}
{activeSection === "team-roles" && ( {activeSection === "team-roles" && <RolesManager searchSpaceId={searchSpaceId} />}
<RolesManager searchSpaceId={searchSpaceId} />
)}
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
</div> </div>
@ -328,7 +326,8 @@ export default function SettingsPage() {
const [isSidebarOpen, setIsSidebarOpen] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const sectionParam = searchParams.get("section"); 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( const handleSectionChange = useCallback(
(section: string) => { (section: string) => {

View file

@ -154,10 +154,7 @@ export default function TeamManagementPage() {
[access] [access]
); );
const { const { data: members = [], isLoading: membersLoading } = useAtomValue(membersAtom);
data: members = [],
isLoading: membersLoading,
} = useAtomValue(membersAtom);
const { mutateAsync: updateMember } = useAtomValue(updateMemberMutationAtom); const { mutateAsync: updateMember } = useAtomValue(updateMemberMutationAtom);
const { mutateAsync: deleteMember } = useAtomValue(deleteMemberMutationAtom); const { mutateAsync: deleteMember } = useAtomValue(deleteMemberMutationAtom);
@ -211,17 +208,13 @@ export default function TeamManagementPage() {
[deleteMember, searchSpaceId] [deleteMember, searchSpaceId]
); );
const { const { data: roles = [] } = useQuery({
data: roles = [],
} = useQuery({
queryKey: cacheKeys.roles.all(searchSpaceId.toString()), queryKey: cacheKeys.roles.all(searchSpaceId.toString()),
queryFn: () => rolesApiService.getRoles({ search_space_id: searchSpaceId }), queryFn: () => rolesApiService.getRoles({ search_space_id: searchSpaceId }),
enabled: !!searchSpaceId, enabled: !!searchSpaceId,
}); });
const { const { data: invites = [] } = useQuery({
data: invites = [],
} = useQuery({
queryKey: cacheKeys.invites.all(searchSpaceId.toString()), queryKey: cacheKeys.invites.all(searchSpaceId.toString()),
queryFn: () => invitesApiService.getInvites({ search_space_id: searchSpaceId }), queryFn: () => invitesApiService.getInvites({ search_space_id: searchSpaceId }),
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
@ -271,12 +264,12 @@ export default function TeamManagementPage() {
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="bg-background select-none" 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="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="space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Skeleton className="h-9 w-36 rounded-md" /> <Skeleton className="h-9 w-36 rounded-md" />
<Skeleton className="h-4 w-20" /> <Skeleton className="h-4 w-20" />
</div> </div>
<div className="rounded-lg border border-border/40 bg-background overflow-hidden"> <div className="rounded-lg border border-border/40 bg-background overflow-hidden">
@ -335,96 +328,93 @@ export default function TeamManagementPage() {
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="bg-background select-none" 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="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="space-y-6">
{/* Header row: Invite button on left, member count on right */} {/* Header row: Invite button on left, member count on right */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{canInvite && ( {canInvite && (
<CreateInviteDialog <CreateInviteDialog
roles={roles} roles={roles}
onCreateInvite={handleCreateInvite} onCreateInvite={handleCreateInvite}
searchSpaceId={searchSpaceId} searchSpaceId={searchSpaceId}
/> />
)} )}
{canInvite && activeInvites.length > 0 && ( {canInvite && activeInvites.length > 0 && (
<AllInvitesDialog <AllInvitesDialog invites={activeInvites} onRevokeInvite={handleRevokeInvite} />
invites={activeInvites} )}
onRevokeInvite={handleRevokeInvite} </div>
/> <p className="hidden md:block text-sm text-muted-foreground">
)} {members.length} {members.length === 1 ? "member" : "members"}
</div> </p>
<p className="hidden md:block text-sm text-muted-foreground">
{members.length} {members.length === 1 ? "member" : "members"}
</p>
</div> </div>
{/* Members & Invites Table */} {/* Members & Invites Table */}
<div className="rounded-lg border border-border/40 bg-background overflow-hidden"> <div className="rounded-lg border border-border/40 bg-background overflow-hidden">
<Table className="table-fixed w-full"> <Table className="table-fixed w-full">
<TableHeader> <TableHeader>
<TableRow className="hover:bg-transparent border-b border-border/40"> <TableRow className="hover:bg-transparent border-b border-border/40">
<TableHead className="w-[45%] px-4 md:px-6 border-r 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"> <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" /> <User size={14} className="opacity-60 text-muted-foreground" />
Name Name
</span> </span>
</TableHead> </TableHead>
<TableHead className="hidden md:table-cell w-[25%] border-r border-border/40"> <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"> <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" /> <Clock size={14} className="opacity-60 text-muted-foreground" />
Last logged in Last logged in
</span> </span>
</TableHead> </TableHead>
<TableHead className="w-[30%] px-4 md:px-6"> <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"> <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" /> <ShieldUser size={14} className="opacity-60 text-muted-foreground" />
Role Role
</span> </span>
</TableHead> </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>
</TableRow> </TableRow>
)} </TableHeader>
</TableBody> <TableBody>
</Table> {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> </div>
{/* Pagination */} {/* Pagination */}
@ -513,7 +503,7 @@ function MemberRow({
const initials = getAvatarInitials(member); const initials = getAvatarInitials(member);
const avatarColor = getAvatarColor(member.user_id); const avatarColor = getAvatarColor(member.user_id);
const displayName = member.user_display_name || member.user_email || "Unknown"; 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); const showActions = !member.is_owner && (canManageRoles || canRemove);
return ( return (
@ -547,7 +537,9 @@ function MemberRow({
<div className="min-w-0"> <div className="min-w-0">
<p className="font-medium text-sm truncate select-text">{displayName}</p> <p className="font-medium text-sm truncate select-text">{displayName}</p>
{member.user_display_name && member.user_email && ( {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>
</div> </div>
@ -569,7 +561,11 @@ function MemberRow({
<ChevronDown className="h-4 w-4" /> <ChevronDown className="h-4 w-4" />
</button> </button>
</DropdownMenuTrigger> </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 && {canManageRoles &&
roles roles
.filter((r) => r.name !== "Owner") .filter((r) => r.name !== "Owner")
@ -590,40 +586,39 @@ function MemberRow({
> >
Remove Remove
</DropdownMenuItem> </DropdownMenuItem>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Remove member?</AlertDialogTitle> <AlertDialogTitle>Remove member?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This will remove{" "} This will remove <span className="font-medium">{member.user_email}</span>{" "}
<span className="font-medium">{member.user_email}</span>{" "} from this search space. They will lose access to all resources.
from this search space. They will lose access to all resources. </AlertDialogDescription>
</AlertDialogDescription> </AlertDialogHeader>
</AlertDialogHeader> <AlertDialogFooter>
<AlertDialogFooter> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogAction
<AlertDialogAction onClick={() => onRemoveMember(member.id)}
onClick={() => onRemoveMember(member.id)} className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
className="bg-destructive text-destructive-foreground hover:bg-destructive/90" >
> Remove
Remove </AlertDialogAction>
</AlertDialogAction> </AlertDialogFooter>
</AlertDialogFooter> </AlertDialogContent>
</AlertDialogContent> </AlertDialog>
</AlertDialog>
)} )}
<DropdownMenuSeparator className="dark:bg-neutral-700" /> <DropdownMenuSeparator className="dark:bg-neutral-700" />
<DropdownMenuItem <DropdownMenuItem
onClick={() => router.push(`/dashboard/${searchSpaceId}/settings?section=team-roles`)} onClick={() =>
router.push(`/dashboard/${searchSpaceId}/settings?section=team-roles`)
}
> >
Manage Roles Manage Roles
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
) : ( ) : (
<span className="text-sm text-foreground"> <span className="text-sm text-foreground">{roleName}</span>
{roleName}
</span>
)} )}
</TableCell> </TableCell>
</motion.tr> </motion.tr>
@ -671,9 +666,7 @@ function CreateInviteDialog({
const invite = await onCreateInvite(data); const invite = await onCreateInvite(data);
setCreatedInvite(invite); setCreatedInvite(invite);
const roleName = roleId const roleName = roleId ? roles.find((r) => r.id.toString() === roleId)?.name : undefined;
? roles.find((r) => r.id.toString() === roleId)?.name
: undefined;
trackSearchSpaceInviteSent(searchSpaceId, { trackSearchSpaceInviteSent(searchSpaceId, {
roleName, roleName,
hasExpiry: !!expiresAt, hasExpiry: !!expiresAt,
@ -707,13 +700,19 @@ function CreateInviteDialog({
return ( return (
<Dialog open={open} onOpenChange={(v) => (v ? setOpen(true) : handleClose())}> <Dialog open={open} onOpenChange={(v) => (v ? setOpen(true) : handleClose())}>
<DialogTrigger asChild> <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" /> <UserPlus className="h-4 w-4" />
Invite members Invite members
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6 select-none" onOpenAutoFocus={(e) => e.preventDefault()}> <DialogContent
{createdInvite ? ( className="w-[92vw] max-w-[92vw] sm:max-w-md p-4 md:p-6 select-none"
onOpenAutoFocus={(e) => e.preventDefault()}
>
{createdInvite ? (
<> <>
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
@ -730,11 +729,7 @@ function CreateInviteDialog({
{window.location.origin}/invite/{createdInvite.invite_code} {window.location.origin}/invite/{createdInvite.invite_code}
</code> </code>
<Button variant="outline" size="sm" onClick={copyLink} className="shrink-0"> <Button variant="outline" size="sm" onClick={copyLink} className="shrink-0">
{copiedLink ? ( {copiedLink ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</Button> </Button>
</div> </div>
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground"> <div className="flex flex-wrap gap-2 text-sm text-muted-foreground">
@ -891,24 +886,18 @@ function AllInvitesDialog({
</DialogTrigger> </DialogTrigger>
<DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-lg p-4 md:p-6 select-none"> <DialogContent className="w-[92vw] max-w-[92vw] sm:max-w-lg p-4 md:p-6 select-none">
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">Active Invite Links</DialogTitle>
Active Invite Links
</DialogTitle>
<DialogDescription> <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> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="max-h-[320px] overflow-y-auto -mx-1 px-1 space-y-3 py-2"> <div className="max-h-[320px] overflow-y-auto -mx-1 px-1 space-y-3 py-2">
{invites.map((invite) => ( {invites.map((invite) => (
<div <div key={invite.id} className="rounded-lg border border-border/40 p-3 space-y-2.5">
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 justify-between gap-2">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<p className="text-sm font-medium truncate"> <p className="text-sm font-medium truncate">{invite.name || "Unnamed invite"}</p>
{invite.name || "Unnamed invite"}
</p>
<div className="flex flex-wrap gap-x-2 text-xs text-muted-foreground shrink-0"> <div className="flex flex-wrap gap-x-2 text-xs text-muted-foreground shrink-0">
{invite.role?.name && ( {invite.role?.name && (
<span className="rounded bg-muted px-1.5 py-0.5">{invite.role.name}</span> <span className="rounded bg-muted px-1.5 py-0.5">{invite.role.name}</span>
@ -929,7 +918,11 @@ function AllInvitesDialog({
</div> </div>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <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" /> <Trash2 className="h-3.5 w-3.5" />
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
@ -937,8 +930,8 @@ function AllInvitesDialog({
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Revoke invite?</AlertDialogTitle> <AlertDialogTitle>Revoke invite?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This will permanently delete this invite link. Anyone with this link This will permanently delete this invite link. Anyone with this link will no
will no longer be able to join. longer be able to join.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
@ -961,7 +954,12 @@ function AllInvitesDialog({
: `/invite/${invite.invite_code}`} : `/invite/${invite.invite_code}`}
</code> </code>
</div> </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 ? ( {copiedId === invite.id ? (
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
) : ( ) : (

View file

@ -747,10 +747,14 @@ const ComposerAction: FC<ComposerActionProps> = ({
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<TooltipIconButton <TooltipIconButton
tooltip={ tooltip={
isUploadingDocs ? "Uploading documents..." : ( isUploadingDocs ? (
"Uploading documents..."
) : (
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<span className="font-medium">Upload and mention files</span> <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> <span className="text-xs text-muted-foreground">Total upload limit: 200 MB</span>
</div> </div>
) )

View file

@ -159,9 +159,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
{t("creating")} {t("creating")}
</> </>
) : ( ) : (
<> <>{t("create_button")}</>
{t("create_button")}
</>
)} )}
</Button> </Button>
</DialogFooter> </DialogFooter>

View file

@ -409,19 +409,19 @@ export function AllPrivateChatsSidebar({
<span className="sr-only">{t("more_options") || "More options"}</span> <span className="sr-only">{t("more_options") || "More options"}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40 z-80"> <DropdownMenuContent align="end" className="w-40 z-80">
{!thread.archived && ( {!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 <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" /> <RotateCcwIcon className="mr-2 h-4 w-4" />

View file

@ -409,20 +409,20 @@ export function AllSharedChatsSidebar({
<span className="sr-only">{t("more_options") || "More options"}</span> <span className="sr-only">{t("more_options") || "More options"}</span>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-40 z-80"> <DropdownMenuContent align="end" className="w-40 z-80">
{!thread.archived && ( {!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 <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" /> {thread.archived ? (
<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" /> <RotateCcwIcon className="mr-2 h-4 w-4" />
<span>{t("unarchive") || "Restore"}</span> <span>{t("unarchive") || "Restore"}</span>

View file

@ -381,11 +381,7 @@ export function SidebarUserProfile({
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}> <DropdownMenuItem onClick={handleLogout} disabled={isLoggingOut}>
{isLoggingOut ? ( {isLoggingOut ? <Spinner size="sm" className="mr-2" /> : <LogOut className="h-4 w-4" />}
<Spinner size="sm" className="mr-2" />
) : (
<LogOut className="h-4 w-4" />
)}
{isLoggingOut ? t("loggingOut") : t("logout")} {isLoggingOut ? t("loggingOut") : t("logout")}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>

View file

@ -144,7 +144,9 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="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" 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" /> <Earth className="h-4 w-4 text-muted-foreground" />

View file

@ -364,11 +364,7 @@ function ReportPanelContent({
{versions.length > 1 && ( {versions.length > 1 && (
<DropdownMenu modal={insideDrawer ? false : undefined}> <DropdownMenu modal={insideDrawer ? false : undefined}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button variant="outline" size="sm" className="h-8 px-3.5 py-4 text-[15px] gap-1.5">
variant="outline"
size="sm"
className="h-8 px-3.5 py-4 text-[15px] gap-1.5"
>
v{activeVersionIndex + 1} v{activeVersionIndex + 1}
<ChevronDownIcon className="size-3" /> <ChevronDownIcon className="size-3" />
</Button> </Button>

View file

@ -156,7 +156,6 @@ const ACTION_LABELS: Record<string, string> = {
manage_roles: "Manage Roles", manage_roles: "Manage Roles",
}; };
const ROLE_PRESETS = { const ROLE_PRESETS = {
editor: { editor: {
name: "Editor", name: "Editor",
@ -241,13 +240,9 @@ export function RolesManager({ searchSpaceId }: { searchSpaceId: number }) {
[access] [access]
); );
const { const { data: roles = [], isLoading: rolesLoading } = useQuery({
data: roles = [],
isLoading: rolesLoading,
} = useQuery({
queryKey: cacheKeys.roles.all(searchSpaceId.toString()), queryKey: cacheKeys.roles.all(searchSpaceId.toString()),
queryFn: () => queryFn: () => rolesApiService.getRoles({ search_space_id: searchSpaceId }),
rolesApiService.getRoles({ search_space_id: searchSpaceId }),
enabled: !!searchSpaceId, enabled: !!searchSpaceId,
}); });
@ -358,14 +353,10 @@ function RolePermissionsDialog({
return ( return (
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>{children}</DialogTrigger>
{children}
</DialogTrigger>
<DialogContent className="w-[92vw] max-w-md p-0 gap-0"> <DialogContent className="w-[92vw] max-w-md p-0 gap-0">
<DialogHeader className="p-4 md:p-5"> <DialogHeader className="p-4 md:p-5">
<DialogTitle className="text-base"> <DialogTitle className="text-base">{roleName} Permissions</DialogTitle>
{roleName} Permissions
</DialogTitle>
<DialogDescription className="text-xs"> <DialogDescription className="text-xs">
{isFullAccess {isFullAccess
? "This role has unrestricted access to all resources" ? "This role has unrestricted access to all resources"
@ -379,7 +370,9 @@ function RolePermissionsDialog({
</div> </div>
<div> <div>
<p className="text-sm font-medium">Full access</p> <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>
</div> </div>
) : ( ) : (
@ -399,9 +392,7 @@ function RolePermissionsDialog({
> >
<div className="flex items-center gap-2 shrink-0"> <div className="flex items-center gap-2 shrink-0">
<IconComponent className="h-3.5 w-3.5 text-muted-foreground" /> <IconComponent className="h-3.5 w-3.5 text-muted-foreground" />
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">{config.label}</span>
{config.label}
</span>
</div> </div>
<div className="flex flex-wrap justify-end gap-1"> <div className="flex flex-wrap justify-end gap-1">
{actions.map((action) => ( {actions.map((action) => (
@ -409,8 +400,7 @@ function RolePermissionsDialog({
key={action} key={action}
className="px-1.5 py-0.5 rounded bg-muted text-muted-foreground text-[11px] font-medium" className="px-1.5 py-0.5 rounded bg-muted text-muted-foreground text-[11px] font-medium"
> >
{ACTION_LABELS[action] || {ACTION_LABELS[action] || action.replace(/_/g, " ")}
action.replace(/_/g, " ")}
</span> </span>
))} ))}
</div> </div>
@ -435,7 +425,9 @@ function PermissionsBadge({ permissions }: { permissions: string[] }) {
} }
return ( return (
<div className="px-2.5 py-1 rounded-md border border-border/60 bg-muted/50 text-muted-foreground"> <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> </div>
); );
} }
@ -514,15 +506,17 @@ function RolesContent({
{editingRole && ( {editingRole && (
<EditRoleDialog <EditRoleDialog
open={!!editingRole} open={!!editingRole}
onOpenChange={(open) => { if (!open) setEditingRoleId(null); }} onOpenChange={(open) => {
if (!open) setEditingRoleId(null);
}}
role={editingRole} role={editingRole}
groupedPermissions={groupedPermissions} groupedPermissions={groupedPermissions}
onUpdateRole={onUpdateRole} onUpdateRole={onUpdateRole}
/> />
)} )}
<div className="space-y-3"> <div className="space-y-3">
{roles.map((role, index) => ( {roles.map((role, index) => (
<motion.div <motion.div
key={role.id} key={role.id}
initial={{ opacity: 0, y: 6 }} initial={{ opacity: 0, y: 6 }}
@ -560,17 +554,19 @@ function RolesContent({
</div> </div>
{!role.is_system_role && ( {!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> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8"> <Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" /> <MoreHorizontal className="h-4 w-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent align="end" onCloseAutoFocus={(e) => e.preventDefault()}>
align="end"
onCloseAutoFocus={(e) => e.preventDefault()}
>
{canUpdate && ( {canUpdate && (
<DropdownMenuItem onClick={() => setEditingRoleId(role.id)}> <DropdownMenuItem onClick={() => setEditingRoleId(role.id)}>
<Edit2 className="h-4 w-4 mr-2" /> <Edit2 className="h-4 w-4 mr-2" />
@ -649,18 +645,14 @@ function PermissionsEditor({
const toggleCategoryExpanded = useCallback((category: string) => { const toggleCategoryExpanded = useCallback((category: string) => {
setExpandedCategories((prev) => setExpandedCategories((prev) =>
prev.includes(category) prev.includes(category) ? prev.filter((c) => c !== category) : [...prev, category]
? prev.filter((c) => c !== category)
: [...prev, category]
); );
}, []); }, []);
const getCategoryStats = useCallback( const getCategoryStats = useCallback(
(category: string) => { (category: string) => {
const perms = groupedPermissions[category] || []; const perms = groupedPermissions[category] || [];
const selected = perms.filter((p) => const selected = perms.filter((p) => selectedPermissions.includes(p.value)).length;
selectedPermissions.includes(p.value)
).length;
return { return {
selected, selected,
total: perms.length, total: perms.length,
@ -683,15 +675,11 @@ function PermissionsEditor({
className="text-xs h-7" className="text-xs h-7"
onClick={() => onClick={() =>
setExpandedCategories( setExpandedCategories(
expandedCategories.length === sortedCategories.length expandedCategories.length === sortedCategories.length ? [] : sortedCategories
? []
: sortedCategories
) )
} }
> >
{expandedCategories.length === sortedCategories.length {expandedCategories.length === sortedCategories.length ? "Collapse All" : "Expand All"}
? "Collapse All"
: "Expand All"}
</Button> </Button>
</div> </div>
@ -709,10 +697,7 @@ function PermissionsEditor({
const perms = groupedPermissions[category] || []; const perms = groupedPermissions[category] || [];
return ( return (
<div <div key={category} className="rounded-lg border border-border/60 overflow-hidden">
key={category}
className="rounded-lg border border-border/60 overflow-hidden"
>
<button <button
type="button" type="button"
className="w-full flex items-center justify-between px-3 py-2.5 cursor-pointer hover:bg-muted/40 transition-colors" 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" type="button"
className={cn( className={cn(
"w-full flex items-center justify-between gap-3 px-2.5 py-2 rounded-md cursor-pointer transition-colors", "w-full flex items-center justify-between gap-3 px-2.5 py-2 rounded-md cursor-pointer transition-colors",
isSelected isSelected ? "bg-muted/60 hover:bg-muted/80" : "hover:bg-muted/40"
? "bg-muted/60 hover:bg-muted/80"
: "hover:bg-muted/40"
)} )}
onClick={() => onTogglePermission(perm.value)} onClick={() => onTogglePermission(perm.value)}
> >
@ -858,28 +841,19 @@ function CreateRoleDialog({
const togglePermission = useCallback((perm: string) => { const togglePermission = useCallback((perm: string) => {
setSelectedPermissions((prev) => setSelectedPermissions((prev) =>
prev.includes(perm) prev.includes(perm) ? prev.filter((p) => p !== perm) : [...prev, perm]
? prev.filter((p) => p !== perm)
: [...prev, perm]
); );
}, []); }, []);
const toggleCategory = useCallback( const toggleCategory = useCallback(
(category: string) => { (category: string) => {
const categoryPerms = const categoryPerms = groupedPermissions[category]?.map((p) => p.value) || [];
groupedPermissions[category]?.map((p) => p.value) || []; const allSelected = categoryPerms.every((p) => selectedPermissions.includes(p));
const allSelected = categoryPerms.every((p) =>
selectedPermissions.includes(p)
);
if (allSelected) { if (allSelected) {
setSelectedPermissions((prev) => setSelectedPermissions((prev) => prev.filter((p) => !categoryPerms.includes(p)));
prev.filter((p) => !categoryPerms.includes(p))
);
} else { } else {
setSelectedPermissions((prev) => [ setSelectedPermissions((prev) => [...new Set([...prev, ...categoryPerms])]);
...new Set([...prev, ...categoryPerms]),
]);
} }
}, },
[groupedPermissions, selectedPermissions] [groupedPermissions, selectedPermissions]
@ -1063,28 +1037,19 @@ function EditRoleDialog({
const togglePermission = useCallback((perm: string) => { const togglePermission = useCallback((perm: string) => {
setSelectedPermissions((prev) => setSelectedPermissions((prev) =>
prev.includes(perm) prev.includes(perm) ? prev.filter((p) => p !== perm) : [...prev, perm]
? prev.filter((p) => p !== perm)
: [...prev, perm]
); );
}, []); }, []);
const toggleCategory = useCallback( const toggleCategory = useCallback(
(category: string) => { (category: string) => {
const categoryPerms = const categoryPerms = groupedPermissions[category]?.map((p) => p.value) || [];
groupedPermissions[category]?.map((p) => p.value) || []; const allSelected = categoryPerms.every((p) => selectedPermissions.includes(p));
const allSelected = categoryPerms.every((p) =>
selectedPermissions.includes(p)
);
if (allSelected) { if (allSelected) {
setSelectedPermissions((prev) => setSelectedPermissions((prev) => prev.filter((p) => !categoryPerms.includes(p)));
prev.filter((p) => !categoryPerms.includes(p))
);
} else { } else {
setSelectedPermissions((prev) => [ setSelectedPermissions((prev) => [...new Set([...prev, ...categoryPerms])]);
...new Set([...prev, ...categoryPerms]),
]);
} }
}, },
[groupedPermissions, selectedPermissions] [groupedPermissions, selectedPermissions]