-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat / add edit, delete team feature
- Loading branch information
1 parent
352b3ec
commit cd80bfc
Showing
10 changed files
with
751 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
.../org/[organizationId]/(specific-organization-pages)/settings/teams/TeamsSettingsTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>) | ||
} |
52 changes: 52 additions & 0 deletions
52
...ication-pages)/org/[organizationId]/(specific-organization-pages)/settings/teams/page.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
</> | ||
); | ||
} |
Oops, something went wrong.