Skip to content

Commit cd80bfc

Browse files
feat / add edit, delete team feature
1 parent 352b3ec commit cd80bfc

File tree

10 files changed

+751
-25
lines changed

10 files changed

+751
-25
lines changed

src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/EditOrganizationForm.tsx

-23
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,6 @@ export function EditOrganizationForm({
6262
mutate(values);
6363
}
6464

65-
function onReset() {
66-
form.reset({
67-
organizationTitle: initialTitle,
68-
organizationSlug: initialSlug,
69-
});
70-
}
71-
7265
return (
7366
<motion.div
7467
initial={{ opacity: 0, y: 20 }}
@@ -105,22 +98,6 @@ export function EditOrganizationForm({
10598
</FormItem>
10699
)}
107100
/>
108-
<FormField
109-
control={form.control}
110-
name="organizationSlug"
111-
render={({ field }) => (
112-
<FormItem>
113-
<FormLabel>Organization Slug</FormLabel>
114-
<FormControl>
115-
<Input {...field} />
116-
</FormControl>
117-
<FormDescription>
118-
This is the slug that will be displayed in the URL.
119-
</FormDescription>
120-
<FormMessage />
121-
</FormItem>
122-
)}
123-
/>
124101
</CardContent>
125102
<CardFooter className="flex justify-between">
126103
<Button type="submit" disabled={isLoading}>

src/app/(dynamic-pages)/(authenticated-pages)/(application-pages)/org/[organizationId]/(specific-organization-pages)/settings/layout.tsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ export default async function OrganizationSettingsLayout({
1818
icon: <SquarePen />,
1919
},
2020
{
21-
label: 'Organization Members',
21+
label: 'Teams',
22+
href: `/org/${organizationId}/settings/teams`,
23+
icon: <UsersRound />,
24+
},
25+
{
26+
label: 'Members',
2227
href: `/org/${organizationId}/settings/members`,
2328
icon: <UsersRound />,
2429
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use client'
2+
3+
import {
4+
Table as ShadcnTable,
5+
TableBody,
6+
TableCell,
7+
TableHead,
8+
TableHeader,
9+
TableRow,
10+
} from "@/components/ui/table";
11+
import { Table } from "@/types";
12+
13+
import { DeleteTeamDialog } from "@/components/DeleteTeamDialog";
14+
import { EditTeamDialog } from "@/components/EditTeamDialog";
15+
import { deleteTeamFromOrganization } from "@/data/admin/teams";
16+
import { useSAToastMutation } from "@/hooks/useSAToastMutation";
17+
import moment from "moment";
18+
import { useRouter } from "next/navigation";
19+
20+
21+
export function TeamsSettingsTable(
22+
{
23+
teams,
24+
isOrganizationAdmin,
25+
organizationId
26+
}: {
27+
teams: Table<'teams'>[];
28+
isOrganizationAdmin: boolean;
29+
organizationId: string;
30+
}
31+
) {
32+
33+
const router = useRouter();
34+
35+
const { mutate: deleteTeam, isLoading: isDeletingTeam } = useSAToastMutation(
36+
async (teamId: number) => {
37+
return await deleteTeamFromOrganization(teamId, organizationId);
38+
},
39+
{
40+
loadingMessage: "Deleting team...",
41+
successMessage: "Team deleted successfully!",
42+
errorMessage: "Failed to delete team",
43+
onSuccess: () => {
44+
router.refresh();
45+
},
46+
}
47+
);
48+
return (<ShadcnTable data-testid="teams-table">
49+
<TableHeader>
50+
<TableRow>
51+
<TableHead> # </TableHead>
52+
<TableHead>Name</TableHead>
53+
<TableHead>Created On</TableHead>
54+
<TableHead>Actions</TableHead>
55+
</TableRow>
56+
</TableHeader>
57+
<TableBody>
58+
{teams.map((team, index) => {
59+
return (
60+
<TableRow data-user-id={team.id} key={team.id}>
61+
<TableCell>{index + 1}</TableCell>
62+
<TableCell data-testid={"member-name"}>
63+
{team.name}
64+
</TableCell>
65+
<TableCell>{moment(team.created_at).format('MMMM D, YYYY')}</TableCell>
66+
<TableCell className="flex flex-row gap-4">
67+
<EditTeamDialog organizationId={organizationId} teamId={team.id} initialTeamName={team.name} isOrganizationAdmin={isOrganizationAdmin} />
68+
<DeleteTeamDialog teamId={team.id} organizationId={organizationId} isOrganizationAdmin={isOrganizationAdmin} teamName={team.name} />
69+
</TableCell>
70+
</TableRow>
71+
);
72+
})}
73+
</TableBody>
74+
</ShadcnTable>)
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
2+
import {
3+
getLoggedInUserOrganizationRole
4+
} from "@/data/user/organizations";
5+
import { getTeamsInOrganization } from "@/data/user/teams";
6+
import { organizationParamSchema } from "@/utils/zod-schemas/params";
7+
import type { Metadata } from "next";
8+
import { Suspense } from "react";
9+
import ProjectsTableLoadingFallback from "../../projects/loading";
10+
import { TeamsSettingsTable } from "./TeamsSettingsTable";
11+
12+
export const metadata: Metadata = {
13+
title: "Teams",
14+
description: "You can edit your organization's teams here.",
15+
};
16+
17+
async function Teams({ organizationId }: { organizationId: string }) {
18+
const teams = await getTeamsInOrganization(organizationId);
19+
const organizationRole =
20+
await getLoggedInUserOrganizationRole(organizationId);
21+
const isOrganizationAdmin =
22+
organizationRole === "admin" || organizationRole === "owner";
23+
24+
return (
25+
<Card className="max-w-5xl">
26+
<div className="flex flex-row justify-between items-center pr-6 w-full">
27+
<CardHeader>
28+
<CardTitle>Teams</CardTitle>
29+
<CardDescription>
30+
Manage your organization teams here.
31+
</CardDescription>
32+
</CardHeader> </div>
33+
<CardContent className="px-6">
34+
<TeamsSettingsTable teams={teams} isOrganizationAdmin={isOrganizationAdmin} organizationId={organizationId} />
35+
</CardContent>
36+
</Card>
37+
);
38+
}
39+
export default async function OrganizationPage({
40+
params,
41+
}: {
42+
params: unknown;
43+
}) {
44+
const { organizationId } = organizationParamSchema.parse(params);
45+
return (
46+
<div className="space-y-8">
47+
<Suspense fallback={<ProjectsTableLoadingFallback />}>
48+
<Teams organizationId={organizationId} />
49+
</Suspense>
50+
</div>
51+
);
52+
}

src/components/DeleteTeamDialog.tsx

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
'use client';
2+
import { Button } from '@/components/ui/button';
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
DialogTrigger,
11+
} from '@/components/ui/dialog';
12+
import { deleteTeamFromOrganization } from '@/data/admin/teams';
13+
import { useSAToastMutation } from '@/hooks/useSAToastMutation';
14+
import { TrashIcon } from 'lucide-react';
15+
import UsersIcon from 'lucide-react/dist/esm/icons/users';
16+
import { useRouter } from 'next/navigation';
17+
import { useState } from 'react';
18+
19+
type DeleteTeamDialogProps = {
20+
organizationId: string;
21+
teamId: number;
22+
teamName: string;
23+
isOrganizationAdmin: boolean;
24+
};
25+
26+
export function DeleteTeamDialog({ organizationId, teamId, teamName, isOrganizationAdmin }: DeleteTeamDialogProps) {
27+
const [open, setOpen] = useState(false);
28+
const router = useRouter();
29+
const { mutate, isLoading } = useSAToastMutation(
30+
async () => await deleteTeamFromOrganization(teamId, organizationId),
31+
{
32+
loadingMessage: 'Deleting team...',
33+
successMessage: 'Team deleted successfully!',
34+
errorMessage: 'Failed to delete team',
35+
onSuccess: () => {
36+
setOpen(false);
37+
router.refresh();
38+
},
39+
},
40+
);
41+
42+
const handleDelete = () => {
43+
if (!isOrganizationAdmin) return;
44+
mutate();
45+
};
46+
47+
return (
48+
<>
49+
<Dialog open={open} onOpenChange={setOpen}>
50+
<DialogTrigger asChild>
51+
<Button variant="destructive" size="sm" disabled={!isOrganizationAdmin || isLoading}>
52+
<TrashIcon className="size-4" />
53+
</Button>
54+
</DialogTrigger>
55+
<DialogContent>
56+
<DialogHeader>
57+
<div className="p-3 w-fit bg-gray-200/50 dark:bg-gray-700/40 mb-2 rounded-lg">
58+
<UsersIcon className="w-6 h-6" />
59+
</div>
60+
<div className="p-1">
61+
<DialogTitle className="text-lg">Delete Team</DialogTitle>
62+
<DialogDescription className="text-base mt-0">
63+
Are you sure you want to delete the team "{teamName}"? This action cannot be undone.
64+
</DialogDescription>
65+
</div>
66+
</DialogHeader>
67+
<DialogFooter>
68+
<Button
69+
type="button"
70+
variant="outline"
71+
disabled={isLoading}
72+
className="w-full"
73+
onClick={() => {
74+
setOpen(false);
75+
}}
76+
>
77+
Cancel
78+
</Button>
79+
<Button
80+
type="button"
81+
variant="destructive"
82+
className="w-full"
83+
disabled={isLoading}
84+
onClick={handleDelete}
85+
>
86+
{isLoading ? 'Deleting Team...' : 'Delete Team'}
87+
</Button>
88+
</DialogFooter>
89+
</DialogContent>
90+
</Dialog>
91+
</>
92+
);
93+
}

src/components/EditTeamDialog.tsx

+116
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
'use client';
2+
import { Button } from '@/components/ui/button';
3+
import {
4+
Dialog,
5+
DialogContent,
6+
DialogDescription,
7+
DialogFooter,
8+
DialogHeader,
9+
DialogTitle,
10+
DialogTrigger,
11+
} from '@/components/ui/dialog';
12+
import { Input } from '@/components/ui/input';
13+
import { Label } from '@/components/ui/label';
14+
import { editTeamAction } from '@/data/user/teams';
15+
import { useSAToastMutation } from '@/hooks/useSAToastMutation';
16+
import { PencilIcon } from 'lucide-react';
17+
import UsersIcon from 'lucide-react/dist/esm/icons/users';
18+
import { useRouter } from 'next/navigation';
19+
import { useState } from 'react';
20+
21+
type EditTeamDialogProps = {
22+
organizationId: string;
23+
teamId: number;
24+
initialTeamName: string;
25+
isOrganizationAdmin: boolean;
26+
};
27+
28+
export function EditTeamDialog({ organizationId, teamId, initialTeamName, isOrganizationAdmin }: EditTeamDialogProps) {
29+
const [teamTitle, setTeamTitle] = useState<string>(initialTeamName);
30+
const [open, setOpen] = useState(false);
31+
const router = useRouter();
32+
const { mutate, isLoading } = useSAToastMutation(
33+
async () => await editTeamAction(teamId, organizationId, teamTitle),
34+
{
35+
loadingMessage: 'Updating team...',
36+
successMessage: 'Team updated!',
37+
errorMessage: 'Failed to update team.',
38+
onSuccess: (data) => {
39+
if (data.status === 'success') {
40+
setOpen(false);
41+
router.refresh();
42+
}
43+
},
44+
},
45+
);
46+
47+
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
48+
e.preventDefault();
49+
if (!isOrganizationAdmin) return;
50+
mutate();
51+
};
52+
53+
return (
54+
<>
55+
<Dialog open={open} onOpenChange={setOpen}>
56+
<DialogTrigger asChild>
57+
<Button variant="outline" size="sm" disabled={!isOrganizationAdmin}>
58+
<PencilIcon className='size-4' />
59+
</Button>
60+
</DialogTrigger>
61+
<DialogContent>
62+
<DialogHeader>
63+
<div className="p-3 w-fit bg-gray-200/50 dark:bg-gray-700/40 mb-2 rounded-lg">
64+
<UsersIcon className="w-6 h-6" />
65+
</div>
66+
<div className="p-1">
67+
<DialogTitle className="text-lg">Edit Team</DialogTitle>
68+
<DialogDescription className="text-base mt-0">
69+
Update your team's information.
70+
</DialogDescription>
71+
</div>
72+
</DialogHeader>
73+
<form onSubmit={handleSubmit}>
74+
<div className="mb-8">
75+
<Label className="text-muted-foreground">Team Name</Label>
76+
<Input
77+
value={teamTitle}
78+
onChange={(event) => {
79+
setTeamTitle(event.target.value);
80+
}}
81+
required
82+
className="mt-1.5 shadow appearance-none border h-11 rounded-lg w-full py-2 px-3 focus:ring-0 text-gray-700 dark:text-gray-300 leading-tight focus:outline-none focus:shadow-outline text-base"
83+
id="name"
84+
type="text"
85+
placeholder="Team Name"
86+
disabled={isLoading}
87+
/>
88+
</div>
89+
90+
<DialogFooter>
91+
<Button
92+
type="button"
93+
variant="outline"
94+
disabled={isLoading}
95+
className="w-full"
96+
onClick={() => {
97+
setOpen(false);
98+
}}
99+
>
100+
Cancel
101+
</Button>
102+
<Button
103+
type="submit"
104+
variant="default"
105+
className="w-full"
106+
disabled={isLoading}
107+
>
108+
{isLoading ? 'Updating Team...' : 'Update Team'}
109+
</Button>
110+
</DialogFooter>
111+
</form>
112+
</DialogContent>
113+
</Dialog>
114+
</>
115+
);
116+
}

0 commit comments

Comments
 (0)