Skip to content

Commit

Permalink
feat / add edit, delete team feature
Browse files Browse the repository at this point in the history
  • Loading branch information
psiddharthdesign committed Aug 5, 2024
1 parent 352b3ec commit cd80bfc
Show file tree
Hide file tree
Showing 10 changed files with 751 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,6 @@ export function EditOrganizationForm({
mutate(values);
}

function onReset() {
form.reset({
organizationTitle: initialTitle,
organizationSlug: initialSlug,
});
}

return (
<motion.div
initial={{ opacity: 0, y: 20 }}
Expand Down Expand Up @@ -105,22 +98,6 @@ export function EditOrganizationForm({
</FormItem>
)}
/>
<FormField
control={form.control}
name="organizationSlug"
render={({ field }) => (
<FormItem>
<FormLabel>Organization Slug</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>
This is the slug that will be displayed in the URL.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
<CardFooter className="flex justify-between">
<Button type="submit" disabled={isLoading}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ export default async function OrganizationSettingsLayout({
icon: <SquarePen />,
},
{
label: 'Organization Members',
label: 'Teams',
href: `/org/${organizationId}/settings/teams`,
icon: <UsersRound />,
},
{
label: 'Members',
href: `/org/${organizationId}/settings/members`,
icon: <UsersRound />,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client'

import {
Table as ShadcnTable,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Table } from "@/types";

import { DeleteTeamDialog } from "@/components/DeleteTeamDialog";
import { EditTeamDialog } from "@/components/EditTeamDialog";
import { deleteTeamFromOrganization } from "@/data/admin/teams";
import { useSAToastMutation } from "@/hooks/useSAToastMutation";
import moment from "moment";
import { useRouter } from "next/navigation";


export function TeamsSettingsTable(
{
teams,
isOrganizationAdmin,
organizationId
}: {
teams: Table<'teams'>[];
isOrganizationAdmin: boolean;
organizationId: string;
}
) {

const router = useRouter();

const { mutate: deleteTeam, isLoading: isDeletingTeam } = useSAToastMutation(
async (teamId: number) => {
return await deleteTeamFromOrganization(teamId, organizationId);
},
{
loadingMessage: "Deleting team...",
successMessage: "Team deleted successfully!",
errorMessage: "Failed to delete team",
onSuccess: () => {
router.refresh();
},
}
);
return (<ShadcnTable data-testid="teams-table">
<TableHeader>
<TableRow>
<TableHead> # </TableHead>
<TableHead>Name</TableHead>
<TableHead>Created On</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{teams.map((team, index) => {
return (
<TableRow data-user-id={team.id} key={team.id}>
<TableCell>{index + 1}</TableCell>
<TableCell data-testid={"member-name"}>
{team.name}
</TableCell>
<TableCell>{moment(team.created_at).format('MMMM D, YYYY')}</TableCell>
<TableCell className="flex flex-row gap-4">
<EditTeamDialog organizationId={organizationId} teamId={team.id} initialTeamName={team.name} isOrganizationAdmin={isOrganizationAdmin} />
<DeleteTeamDialog teamId={team.id} organizationId={organizationId} isOrganizationAdmin={isOrganizationAdmin} teamName={team.name} />
</TableCell>
</TableRow>
);
})}
</TableBody>
</ShadcnTable>)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
getLoggedInUserOrganizationRole
} from "@/data/user/organizations";
import { getTeamsInOrganization } from "@/data/user/teams";
import { organizationParamSchema } from "@/utils/zod-schemas/params";
import type { Metadata } from "next";
import { Suspense } from "react";
import ProjectsTableLoadingFallback from "../../projects/loading";
import { TeamsSettingsTable } from "./TeamsSettingsTable";

export const metadata: Metadata = {
title: "Teams",
description: "You can edit your organization's teams here.",
};

async function Teams({ organizationId }: { organizationId: string }) {
const teams = await getTeamsInOrganization(organizationId);
const organizationRole =
await getLoggedInUserOrganizationRole(organizationId);
const isOrganizationAdmin =
organizationRole === "admin" || organizationRole === "owner";

return (
<Card className="max-w-5xl">
<div className="flex flex-row justify-between items-center pr-6 w-full">
<CardHeader>
<CardTitle>Teams</CardTitle>
<CardDescription>
Manage your organization teams here.
</CardDescription>
</CardHeader> </div>
<CardContent className="px-6">
<TeamsSettingsTable teams={teams} isOrganizationAdmin={isOrganizationAdmin} organizationId={organizationId} />
</CardContent>
</Card>
);
}
export default async function OrganizationPage({
params,
}: {
params: unknown;
}) {
const { organizationId } = organizationParamSchema.parse(params);
return (
<div className="space-y-8">
<Suspense fallback={<ProjectsTableLoadingFallback />}>
<Teams organizationId={organizationId} />
</Suspense>
</div>
);
}
93 changes: 93 additions & 0 deletions src/components/DeleteTeamDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
'use client';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { deleteTeamFromOrganization } from '@/data/admin/teams';
import { useSAToastMutation } from '@/hooks/useSAToastMutation';
import { TrashIcon } from 'lucide-react';
import UsersIcon from 'lucide-react/dist/esm/icons/users';
import { useRouter } from 'next/navigation';
import { useState } from 'react';

type DeleteTeamDialogProps = {
organizationId: string;
teamId: number;
teamName: string;
isOrganizationAdmin: boolean;
};

export function DeleteTeamDialog({ organizationId, teamId, teamName, isOrganizationAdmin }: DeleteTeamDialogProps) {
const [open, setOpen] = useState(false);
const router = useRouter();
const { mutate, isLoading } = useSAToastMutation(
async () => await deleteTeamFromOrganization(teamId, organizationId),
{
loadingMessage: 'Deleting team...',
successMessage: 'Team deleted successfully!',
errorMessage: 'Failed to delete team',
onSuccess: () => {
setOpen(false);
router.refresh();
},
},
);

const handleDelete = () => {
if (!isOrganizationAdmin) return;
mutate();
};

return (
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="destructive" size="sm" disabled={!isOrganizationAdmin || isLoading}>
<TrashIcon className="size-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<div className="p-3 w-fit bg-gray-200/50 dark:bg-gray-700/40 mb-2 rounded-lg">
<UsersIcon className="w-6 h-6" />
</div>
<div className="p-1">
<DialogTitle className="text-lg">Delete Team</DialogTitle>
<DialogDescription className="text-base mt-0">
Are you sure you want to delete the team "{teamName}"? This action cannot be undone.
</DialogDescription>
</div>
</DialogHeader>
<DialogFooter>
<Button
type="button"
variant="outline"
disabled={isLoading}
className="w-full"
onClick={() => {
setOpen(false);
}}
>
Cancel
</Button>
<Button
type="button"
variant="destructive"
className="w-full"
disabled={isLoading}
onClick={handleDelete}
>
{isLoading ? 'Deleting Team...' : 'Delete Team'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
116 changes: 116 additions & 0 deletions src/components/EditTeamDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use client';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { editTeamAction } from '@/data/user/teams';
import { useSAToastMutation } from '@/hooks/useSAToastMutation';
import { PencilIcon } from 'lucide-react';
import UsersIcon from 'lucide-react/dist/esm/icons/users';
import { useRouter } from 'next/navigation';
import { useState } from 'react';

type EditTeamDialogProps = {
organizationId: string;
teamId: number;
initialTeamName: string;
isOrganizationAdmin: boolean;
};

export function EditTeamDialog({ organizationId, teamId, initialTeamName, isOrganizationAdmin }: EditTeamDialogProps) {
const [teamTitle, setTeamTitle] = useState<string>(initialTeamName);
const [open, setOpen] = useState(false);
const router = useRouter();
const { mutate, isLoading } = useSAToastMutation(
async () => await editTeamAction(teamId, organizationId, teamTitle),
{
loadingMessage: 'Updating team...',
successMessage: 'Team updated!',
errorMessage: 'Failed to update team.',
onSuccess: (data) => {
if (data.status === 'success') {
setOpen(false);
router.refresh();
}
},
},
);

const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!isOrganizationAdmin) return;
mutate();
};

return (
<>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline" size="sm" disabled={!isOrganizationAdmin}>
<PencilIcon className='size-4' />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<div className="p-3 w-fit bg-gray-200/50 dark:bg-gray-700/40 mb-2 rounded-lg">
<UsersIcon className="w-6 h-6" />
</div>
<div className="p-1">
<DialogTitle className="text-lg">Edit Team</DialogTitle>
<DialogDescription className="text-base mt-0">
Update your team's information.
</DialogDescription>
</div>
</DialogHeader>
<form onSubmit={handleSubmit}>
<div className="mb-8">
<Label className="text-muted-foreground">Team Name</Label>
<Input
value={teamTitle}
onChange={(event) => {
setTeamTitle(event.target.value);
}}
required
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"
id="name"
type="text"
placeholder="Team Name"
disabled={isLoading}
/>
</div>

<DialogFooter>
<Button
type="button"
variant="outline"
disabled={isLoading}
className="w-full"
onClick={() => {
setOpen(false);
}}
>
Cancel
</Button>
<Button
type="submit"
variant="default"
className="w-full"
disabled={isLoading}
>
{isLoading ? 'Updating Team...' : 'Update Team'}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
);
}
Loading

0 comments on commit cd80bfc

Please sign in to comment.