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} />
</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>
);

View file

@ -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) => {

View file

@ -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" />
) : (

View file

@ -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>
)

View file

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

View file

@ -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" />

View file

@ -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>

View file

@ -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>

View file

@ -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" />

View file

@ -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>

View file

@ -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]