Working on add/delete users to workspace. Edit workspaces and some permissions.

This commit is contained in:
charles-gauthereau
2024-12-30 21:10:49 +01:00
parent 379271501d
commit e1a7ca8558
19 changed files with 466 additions and 58 deletions
@@ -7,6 +7,7 @@ import {PaginationNavigation} from "@/components/wrappers/common/pagination/pagi
export type cardsWithPaginationProps = {
className?: string
organizationSlug?:string
data: Array<{}>;
cardItem: React.ComponentType;
cardsPerPage?: number
@@ -18,7 +19,7 @@ export type cardsWithPaginationProps = {
export const CardsWithPagination = (props: cardsWithPaginationProps) => {
const {className, data, cardItem, cardsPerPage = 5, numberOfColumns = 1, maxVisiblePages = 3} = props
const {className,organizationSlug, data, cardItem, cardsPerPage = 5, numberOfColumns = 1, maxVisiblePages = 3} = props
const CardItem = cardItem
@@ -45,7 +46,7 @@ export const CardsWithPagination = (props: cardsWithPaginationProps) => {
<div className={cn("flex flex-col h-full justify-between", className)}>
<div className={cn(`grid h-max auto-rows-min gap-4 md:grid-cols-${numberOfColumns}`)}>
{currentCards.map((card, key) => (
<CardItem key={key} data={card} extendedProps={props.extendedProps} />
<CardItem key={key} data={card} organizationSlug={organizationSlug} extendedProps={props.extendedProps} />
))}
</div>
<PaginationNavigation
@@ -2,9 +2,9 @@ import {flexRender, Row, RowData} from "@tanstack/react-table";
import {User} from "@prisma/client";
import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination";
import {usersColumns} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users";
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
import {cn} from "@/lib/utils";
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/columns-users";
export type AdminUsersTableProps = {
currentUser: User;
@@ -14,7 +14,6 @@ export type AdminUsersTableProps = {
export const AdminUsersTable = (props: AdminUsersTableProps) => {
const {currentUser, users} = props;
return (
<div className="flex flex-col h-full py-4">
<div className="flex gap-4 h-fit justify-between">
@@ -22,7 +21,7 @@ export const AdminUsersTable = (props: AdminUsersTableProps) => {
</div>
<div className="mt-5">
<DataTableWithPagination
columns={usersColumns}
columns={usersColumnsAdmin}
data={users}
DataTable={UsersDataTable}
dataTableProps={{currentUser}}
@@ -12,7 +12,7 @@ import {Trash2} from "lucide-react";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
export const usersColumns: ColumnDef<User>[] = [
export const usersColumnsAdmin: ColumnDef<User>[] = [
{
accessorKey: "role",
header: "Role",
@@ -55,10 +55,10 @@ export const usersColumns: ColumnDef<User>[] = [
header: "Email"
},
{
accessorKey: "createdAt",
header: "Created At",
accessorKey: "updatedAt",
header: "Updated At",
cell: ({row}) => {
return new Date(row.getValue("createdAt")).toLocaleString("fr-FR");
return new Date(row.getValue("updatedAt")).toLocaleString("fr-FR");
},
},
{
@@ -0,0 +1,162 @@
"use client";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
useZodForm
} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form"
import {Button} from "@/components/ui/button";
import {useMutation} from "@tanstack/react-query";
import {useRouter} from "next/navigation";
import {Organization, User} from "@prisma/client"
import {MultiSelect} from "@/components/wrappers/common/multiSelect/MultiSelect";
import {
OrganizationFormSchema,
OrganizationFormType
} from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
import {
createOrganizationAction,
updateOrganizationAction
} from "@/components/wrappers/dashboard/organization/organization.action";
import {toast} from "sonner";
export type organizationFormProps = {
defaultValues?: Organization;
users: User[]
}
export const OrganizationForm = (props: organizationFormProps) => {
const router = useRouter();
const isCreate = !Boolean(props.defaultValues)
const formatUsersList = (users: User[]) => {
return users.map(user => ({
value: user.id,
label: `${user.name} | ${user.email}`,
}));
};
const formatDefaultUsers = (users: OrganizationFormType['users']): string[] => {
console.log(users)
return users.map(user => user.userId );
};
const formattedDefaultValues = {
...props.defaultValues,
users: !isCreate ? formatDefaultUsers(props.defaultValues?.users) : []
}
const form = useZodForm({
schema: OrganizationFormSchema,
defaultValues: formattedDefaultValues,
});
const mutation = useMutation({
mutationFn: async (values: OrganizationFormType) => {
console.log(values)
const organization = await updateOrganizationAction({data: values, organizationId: props.defaultValues.id})
console.log(organization)
if (organization.data.success) {
toast.success(organization.data.actionSuccess.message);
router.push(`/dashboard/${organization.data.value.slug}/settings`);
router.refresh()
} else {
toast.success(organization.data.actionError.message);
}
}
})
return (
<Card>
<CardHeader>
</CardHeader>
<CardContent>
<Form form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Organization 1" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="slug"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input
placeholder="project-1" {...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
field.onChange(value)
}}/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="users"
render={({field}) => (
<FormItem>
<FormLabel>Databases</FormLabel>
<FormControl>
<MultiSelect
options={formatUsersList(props.users)}
onValueChange={field.onChange}
defaultValue={field.value ?? []}
placeholder="Select databases"
variant="inverted"
animation={2}
// maxCount={100}
/>
</FormControl>
<FormDescription>Select users you want to add to this organization</FormDescription>
<FormMessage/>
</FormItem>
)
}
/>
<Button>
{isCreate ? `Create Organization` : `Update Organization`}
</Button>
</Form>
</CardContent>
</Card>
)
}
@@ -0,0 +1,14 @@
import {z} from "zod";
export const OrganizationFormSchema = z.object({
name: z.string().min(5, 'Name must be at least 5 characters long').max(40, 'Name must be at most 40 characters long'),
slug: z.string()
.regex(/^[a-zA-Z0-9_-]*$/, 'Slug can only contain letters, numbers, underscores, and hyphens')
.min(5, 'Slug must be at least 5 characters long')
.max(20, 'Slug must be at most 20 characters long'),
users: z.array(z.string()),
});
export type OrganizationFormType = z.infer<typeof OrganizationFormSchema>;
@@ -8,6 +8,10 @@ import {Organization} from "@prisma/client";
import {z} from "zod";
import {v4 as uuidv4} from "uuid";
import {db} from "@/db";
import {
OrganizationFormSchema
} from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
const verifySlugUniqueness = async (slug: string) => {
@@ -71,6 +75,95 @@ export const createOrganizationAction = userAction
export const updateOrganizationAction = userAction
.schema(
z.object({
data: OrganizationFormSchema,
organizationId: z.string()
}))
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
try {
const newUserList = parsedInput.data.users
const organization = await prisma.organization.findFirst({
where:{
id: parsedInput.organizationId,
},
include: {
users : {}
}
})
const existingItemIds = organization.users.map((user) => user.userId);
const usersToAdd = newUserList.filter(
(id) => !existingItemIds.includes(id)
);
const usersToRemove = existingItemIds.filter(
(id) => !newUserList.includes(id)
);
console.log(usersToAdd);
console.log(usersToRemove);
if (usersToAdd.length > 0) {
await prisma.userOrganization.createMany({
data: usersToAdd.map((userId) => ({
userId: userId,
organizationId: organization.id,
})),
skipDuplicates: true, // Optional: to avoid duplicate insertion errors
});
}
if (usersToRemove.length > 0) {
await prisma.userOrganization.deleteMany({
where: {
userId: { in: usersToRemove },
},
});
}
const updatedOrganization = await prisma.organization.update({
where:{
id: organization.id
},
data:{
name: parsedInput.data.name,
slug: parsedInput.data.slug,
}
})
return {
success: true,
value: updatedOrganization,
actionSuccess: {
message: "Organization has been successfully updated.",
messageParams: {organizationId: updatedOrganization.id},
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "Failed to update organization.",
status: 500,
cause: error.message ?? "Unknown error",
messageParams: {message: "Error updating the organization"},
},
};
}
});
export const deleteOrganizationAction = userAction
.schema(z.string())
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
@@ -4,16 +4,17 @@ import {Card, CardContent, CardHeader} from "@/components/ui/card";
import Link from "next/link";
export type projectCardProps = {
data: any
data: any,
organizationSlug?: string
}
export const ProjectCard = (props: projectCardProps) => {
const {data: project} = props;
const {data: project, organizationSlug} = props;
return (
<Link href={`/dashboard/projects/${project.id}`}>
<Link href={`/dashboard/${organizationSlug}/projects/${project.id}`}>
<Card className="flex flex-row justify-between">
<div className="">
<CardHeader>{project.name}</CardHeader>
@@ -10,14 +10,15 @@ import {formatDateLastContact} from "@/utils/date-formatting";
export type projectDatabaseCardProps = {
data: Database,
extendedProps: any
organizationSlug: string
}
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
const {data: database, extendedProps: extendedProps} = props;
const {organizationSlug,data: database, extendedProps: extendedProps} = props;
return (
<Link href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
<Link href={`/dashboard/${organizationSlug}/projects/${extendedProps.id}/database/${database.id}`}>
<DatabaseCard data={database}/>
</Link>
)
@@ -77,7 +77,7 @@ export const ProjectForm = (props: projectFormProps) => {
if (project.data.success) {
toast.success(project.data.actionSuccess.message);
router.push(`/dashboard/projects/${project.data.value.id}`);
router.push(`/dashboard/${props.organization.slug}/projects/${project.data.value.id}`);
router.refresh()
} else {
toast.success(project.data.actionError.message);
@@ -0,0 +1,21 @@
"use client"
import {buttonVariants} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import Link from "next/link";
import {usePathname} from "next/navigation";
export type EditButtonSettings = {}
export const EditButtonSettings= (props:EditButtonSettings) => {
const pathname = usePathname();
return(
<Link className={buttonVariants({variant: "outline"})}
href={`${pathname}/edit/`}
>
<GearIcon className="w-7 h-7"/>
</Link>
)
}
@@ -2,7 +2,7 @@
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination";
import {usersColumns} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users";
import {usersColumns} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users-settings";
import {User, Settings} from "@prisma/client";
import {backupColumns} from "@/features/backup/columns";
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/AdminEmailTab/SettingsEmailTab";
@@ -21,34 +21,6 @@ export const SettingsTabs = (props: SettingsTabsProps) => {
const {currentUser, users} = props;
return (
// <Tabs defaultValue="informations" >
// <TabsList className="w-full">
// <TabsTrigger className="w-full " value="informations">Info</TabsTrigger>
// <TabsTrigger className="w-full " value="users">Users</TabsTrigger>
// <TabsTrigger className="w-full " value="email">Email</TabsTrigger>
// <TabsTrigger className="w-full " value="storage">Storage</TabsTrigger>
// </TabsList>
// <TabsContent value="informations">
// <div className="flex flex-1 flex-col gap-4 py-4">
// <div className="grid auto-rows-min gap-4 md:grid-cols-3">
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// </div>
// <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
// </div>
// </TabsContent>
// <TabsContent value="users" >
<SettingsUsersTab currentUser={currentUser} users={users}/>
// </TabsContent>
// <TabsContent value="email">
// <SettingsEmailTab settings={props.settings}/>
// </TabsContent>
// <TabsContent value="storage">
// <SettingsStorageTab settings={props.settings}/>
// </TabsContent>
// </Tabs>
)
}
@@ -1,6 +1,6 @@
import {User} from "@prisma/client";
import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination";
import {usersColumns} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users";
import {usersColumns} from "@/components/wrappers/dashboard/settings/SettingsUsersTab/columns-users-settings";
import {UsersDataTable} from "@/components/wrappers/dashboard/admin/admin-user-table";
@@ -0,0 +1,64 @@
"use client"
import {ColumnDef} from "@tanstack/react-table"
import {Badge} from "@/components/ui/badge";
import {User} from "@prisma/client";
import {updateUserAction} from "@/components/wrappers/dashboard/profile/UserForm/user-form.action";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {useRouter} from "next/navigation";
import {useState} from "react";
import {Trash2} from "lucide-react";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
export const usersColumns: ColumnDef<User>[] = [
{
accessorKey: "role",
header: "Role",
cell: ({row}) => {
const router = useRouter();
const [role, setRole] = useState<string>(row.getValue("role"))
const updateMutation = useMutation({
mutationFn: () => updateUserAction({id: row.original.id, data: {role: role}}),
onSuccess: () => {
toast.success(`User updated successfully.`);
router.refresh()
},
onError: () => {
toast.error(`An error occurred while updating user information.`);
},
});
const handleUpdateRole = async () => {
const nextRole = role === "admin" ? "pending"
: role === "pending" ? "user"
: "admin";
setRole(nextRole);
await updateMutation.mutateAsync()
};
return <Badge
className="cursor-pointer"
onClick={() => handleUpdateRole()}
variant="outline">{role}</Badge>
},
},
{
accessorKey: "name",
header: "Name"
},
{
accessorKey: "email",
header: "Email"
},
{
accessorKey: "updatedAt",
header: "Updated At",
cell: ({row}) => {
return new Date(row.getValue("updatedAt")).toLocaleString("fr-FR");
},
}
]