Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 42 additions & 13 deletions my-app/app/admin/groups/groups-table-columns.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { MoreVertical, Trash2, Pencil } from "lucide-react";
import { MoreVertical, Trash2, Edit } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { SortingIcon } from "@/components/table-components";
Expand All @@ -18,13 +18,19 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Group } from "@/lib/types";
import { SortingState } from "@tanstack/react-table";
import { formatRelativeTime, formatDateTime } from "@/lib/utils";

interface GroupsTableCoreProps {
groups: Group[];
searchTerm: string;
onGroupAction: (groupName: string, action: "delete" | "edit") => void;
onGroupAction: (groupName: string, action: "delete" | "rename") => void;
selectedGroups: string[];
onSelectGroup: (groupName: string, checked: boolean) => void;
onSelectAll: (checked: boolean) => void;
Expand Down Expand Up @@ -87,7 +93,7 @@ export function GroupsTableCore({
disabled={allSelectableGroups.length === 0}
/>
</TableHead>
<TableHead className="w-[300px]">
<TableHead>
<div className="flex items-center justify-between pr-2 group">
<Button
variant="ghost"
Expand All @@ -100,7 +106,7 @@ export function GroupsTableCore({
<SortingIcon sortDirection={getSortDirection("name")} />
</div>
</TableHead>
<TableHead className="w-[175px]">
<TableHead>
<div className="flex items-center justify-between pr-2 group">
<Button
variant="ghost"
Expand All @@ -113,12 +119,20 @@ export function GroupsTableCore({
<SortingIcon sortDirection={getSortDirection("user_count")} />
</div>
</TableHead>
<TableHead className="min-w-[200px]">
<div>
<span className="font-medium">Comment</span>
<TableHead>
<div className="flex items-center justify-between pr-2 group">
<Button
variant="ghost"
size="sm"
onClick={() => handleSortingChange("created_at")}
className="-ml-3 flex-1 justify-start hover:bg-muted transition-colors"
>
<span className="font-medium">Created At</span>
</Button>
<SortingIcon sortDirection={getSortDirection("created_at")} />
</div>
</TableHead>
<TableHead className="w-[20px]">
<TableHead className="w-[50px]">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" disabled={selectedGroups.length === 0}>
Expand Down Expand Up @@ -169,8 +183,21 @@ export function GroupsTableCore({
<TableCell className="text-muted-foreground">
{group.user_count ?? 0}
</TableCell>
<TableCell className="text-muted-foreground max-w-[300px]">
<div className="truncate">{group.comment || ""}</div>
<TableCell>
{group.created_at ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="text-sm text-muted-foreground cursor-help">
{formatRelativeTime(group.created_at)}
</span>
</TooltipTrigger>
<TooltipContent>
<p>{formatDateTime(group.created_at)}</p>
</TooltipContent>
</Tooltip>
) : (
<span className="text-sm text-muted-foreground">N/A</span>
)}
</TableCell>
<TableCell>
<DropdownMenu>
Expand All @@ -181,16 +208,18 @@ export function GroupsTableCore({
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => onGroupAction(group.name, "edit")}
onClick={() => onGroupAction(group.name, "rename")}
className="cursor-pointer"
disabled={!group.can_modify}
>
<Pencil className="mr-2 h-4 w-4" />
Edit
<Edit className="mr-2 h-4 w-4" />
Rename
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => onGroupAction(group.name, "delete")}
className="cursor-pointer text-destructive focus:text-destructive"
disabled={!group.can_modify}
>
<Trash2 className="mr-2 h-4 w-4" />
Delete
Expand Down
2 changes: 1 addition & 1 deletion my-app/app/admin/groups/groups-table-core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface GroupsTableCoreWrapperProps {
groups: Group[];
sorting: SortingState;
onSortingChange: (sorting: SortingState) => void;
onGroupAction: (groupName: string, action: "delete" | "edit") => void;
onGroupAction: (groupName: string, action: "delete" | "rename") => void;
searchTerm: string;
selectedGroups: string[];
onSelectGroup: (groupName: string, checked: boolean) => void;
Expand Down
14 changes: 8 additions & 6 deletions my-app/app/admin/groups/groups-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { GroupsTablePagination } from "./groups-table-pagination";
import { SortingState } from "@tanstack/react-table";

interface GroupsTableProps {
onGroupAction: (groupName: string, action: "delete" | "edit") => void;
onGroupAction: (groupName: string, action: "delete" | "rename") => void;
onBulkDeleteRequest?: (groupNames: string[]) => void;
}

Expand All @@ -31,9 +31,9 @@ export function GroupsTable({
// Pagination state
const [currentPage, setCurrentPage] = React.useState(1);
const [itemsPerPage, setItemsPerPage] = React.useState(25);
// Sorting state with default sort by name
// Sorting state with default sort by created_at (recent to old)
const [sorting, setSorting] = React.useState<SortingState>([
{ id: "name", desc: false },
{ id: "created_at", desc: false },
]);
// Multi-select state
const [selectedGroups, setSelectedGroups] = React.useState<string[]>([]);
Expand Down Expand Up @@ -80,9 +80,11 @@ export function GroupsTable({
} else if (sort.id === "user_count") {
aValue = a.user_count || 0;
bValue = b.user_count || 0;
} else if (sort.id === "comment") {
aValue = (a.comment || "").toLowerCase();
bValue = (b.comment || "").toLowerCase();
} else if (sort.id === "created_at") {
// Sort by date (most recent first by default)
const aDate = a.created_at ? new Date(a.created_at).getTime() : 0;
const bDate = b.created_at ? new Date(b.created_at).getTime() : 0;
return sort.desc ? aDate - bDate : bDate - aDate;
} else {
return 0;
}
Expand Down
8 changes: 4 additions & 4 deletions my-app/app/admin/groups/manage-group-members-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,10 @@ export function ManageGroupMembersDialog({
// Filter based on operation
if (operation === "add") {
// Show users NOT in the selected group
return !user.groups.includes(selectedGroup);
return !user.groups.some((group) => group.name === selectedGroup);
} else {
// Show users IN the selected group
return user.groups.includes(selectedGroup);
return user.groups.some((group) => group.name === selectedGroup);
}
});

Expand Down Expand Up @@ -379,11 +379,11 @@ export function ManageGroupMembersDialog({
<div className="flex gap-2 flex-wrap justify-end">
{user.groups.slice(0, 3).map((group) => (
<Badge
key={group}
key={group.name}
variant="outline"
className="text-muted-foreground"
>
{group}
{group.name}
</Badge>
))}
{user.groups.length > 3 && (
Expand Down
90 changes: 89 additions & 1 deletion my-app/app/admin/groups/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { AuthGuard } from "@/components/auth-guard";
import { PageLayout } from "@/app/admin/admin-page-layout";
import { GroupsTable } from "@/app/admin/groups/groups-table";
import { handleDeleteGroups } from "@/lib/admin-operations";
import { renameGroup } from "@/lib/api";
import { toast } from "sonner";
import {
AlertDialog,
AlertDialogAction,
Expand All @@ -14,16 +16,30 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Group } from "@/lib/types";

const breadcrumbs = [{ label: "Groups", href: "/admin/groups" }];

export default function AdminGroupsPage() {
const [alertOpen, setAlertOpen] = useState(false);
const [renameDialogOpen, setRenameDialogOpen] = useState(false);
const [bulkDeleteDialogOpen, setBulkDeleteDialogOpen] = useState(false);
const [bulkDeleteGroups, setBulkDeleteGroups] = useState<string[]>([]);
const [selectedGroup, setSelectedGroup] = useState<Group | null>(null);
const [newGroupName, setNewGroupName] = useState("");
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
const [isRenaming, setIsRenaming] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);

const handleRefresh = async () => {
Expand All @@ -32,11 +48,18 @@ export default function AdminGroupsPage() {
await new Promise((resolve) => setTimeout(resolve, 500));
};

const handleGroupAction = (groupName: string, action: "delete" | "edit") => {
const handleGroupAction = (
groupName: string,
action: "delete" | "rename",
) => {
const group = { name: groupName } as Group;
if (action === "delete") {
setSelectedGroup(group);
setAlertOpen(true);
} else if (action === "rename") {
setSelectedGroup(group);
setNewGroupName(groupName);
setRenameDialogOpen(true);
}
};

Expand Down Expand Up @@ -79,6 +102,32 @@ export default function AdminGroupsPage() {
}
};

const handleConfirmRename = async () => {
if (!selectedGroup || !newGroupName.trim()) return;
if (newGroupName.trim() === selectedGroup.name) {
toast.error("New name must be different from current name");
return;
}
setIsRenaming(true);
try {
await renameGroup(selectedGroup.name, newGroupName.trim());
toast.success(
`Group renamed from "${selectedGroup.name}" to "${newGroupName.trim()}"`,
);
setRenameDialogOpen(false);
setSelectedGroup(null);
setNewGroupName("");
handleRefresh();
} catch (error) {
toast.error(
`Failed to rename group: ${error instanceof Error ? error.message : "Unknown error"}`,
);
console.error("Failed to rename group:", error);
} finally {
setIsRenaming(false);
}
};

return (
<AuthGuard adminOnly>
<PageLayout breadcrumbs={breadcrumbs}>
Expand All @@ -97,6 +146,45 @@ export default function AdminGroupsPage() {
</div>
</PageLayout>

{/* Rename Dialog */}
<Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Rename Group</DialogTitle>
<DialogDescription>
Enter a new name for &quot;{selectedGroup?.name}&quot;
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">New Group Name</Label>
<Input
id="name"
value={newGroupName}
onChange={(e) => setNewGroupName(e.target.value)}
placeholder="Enter new group name"
disabled={isRenaming}
/>
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setRenameDialogOpen(false)}
disabled={isRenaming}
>
Cancel
</Button>
<Button
onClick={handleConfirmRename}
disabled={isRenaming || !newGroupName.trim()}
>
{isRenaming ? "Renaming..." : "Rename"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>

{/* Delete Confirmation Dialog */}
<AlertDialog open={alertOpen} onOpenChange={setAlertOpen}>
<AlertDialogContent>
Expand Down
8 changes: 4 additions & 4 deletions my-app/app/admin/users/edit-groups-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,10 @@ export function EditGroupsDialog({
// Set initial selected groups when user changes
useEffect(() => {
if (user) {
// Filter out empty strings from groups
const validGroups = user.groups.filter((g) => g && g.trim() !== "");
setSelectedGroups(validGroups);
setInitialGroups(validGroups);
// Extract group names from Group objects
const groupNames = user.groups.map((g) => g.name);
setSelectedGroups(groupNames);
setInitialGroups(groupNames);
}
}, [user]);

Expand Down
Loading