mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-14 22:52:15 +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} />
|
<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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -159,9 +159,7 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
|
||||||
{t("creating")}
|
{t("creating")}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>{t("create_button")}</>
|
||||||
{t("create_button")}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue