migration

This commit is contained in:
Théo LAGACHE
2025-05-16 15:54:17 +02:00
parent 01019fe1a3
commit 6f2523e524
285 changed files with 15492 additions and 46090 deletions
@@ -1,42 +1,38 @@
"use client"
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
import {
deleteOrganizationAction,
} from "@/components/wrappers/dashboard/organization/organization.action";
import {useMutation} from "@tanstack/react-query";
import {setCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {useRouter} from "next/navigation";
import {toast} from "sonner";
"use client";
import { ButtonWithConfirm } from "@/components/wrappers/common/button/button-with-confirm";
import { deleteOrganizationAction } from "@/components/wrappers/dashboard/organization/organization.action";
import { useMutation } from "@tanstack/react-query";
import { setCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
export type DeleteOrganizationButtonProps = {
organizationSlug: string;
}
};
export const DeleteOrganizationButton = (props: DeleteOrganizationButtonProps) => {
const router = useRouter();
const mutation = useMutation({
mutationFn: () => deleteOrganizationAction(props.organizationSlug),
onSuccess: async (result) => {
console.log(result);
if(result.data.success) {
await setCurrentOrganizationSlug("default")
router.push("/")
if (result.data.success) {
await setCurrentOrganizationSlug("default");
router.push("/");
toast.success(result.data.actionSuccess.message);
}else{
} else {
toast.error(result.data.actionError.message);
}
},
})
});
return(
return (
<ButtonWithConfirm
onClick={() => {
mutation.mutate()
mutation.mutate();
}}
isPending={mutation.isPending}
text="Delete Organization"
variant="destructive"/>
)
}
variant="destructive"
/>
);
};
@@ -1,111 +1,86 @@
"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";
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/multi-select";
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[]
}
users: User[];
};
export const OrganizationForm = (props: organizationFormProps) => {
const router = useRouter();
const isCreate = !Boolean(props.defaultValues)
const isCreate = !Boolean(props.defaultValues);
const formatUsersList = (users: User[]) => {
return users.map(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 formatDefaultUsers = (users: OrganizationFormType["users"]): string[] => {
console.log(users);
return users.map((user) => user.userId);
};
const formattedDefaultValues = {
...props.defaultValues,
users: !isCreate ? formatDefaultUsers(props.defaultValues?.users) : []
}
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)
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()
router.refresh();
} else {
toast.success(organization.data.actionError.message);
}
}
})
},
});
return (
<Card>
<CardHeader>
</CardHeader>
<CardHeader></CardHeader>
<CardContent>
<Form form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
<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}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Organization 1" {...field} />
<Input placeholder="Organization 1" {...field} />
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -113,29 +88,30 @@ export const OrganizationForm = (props: organizationFormProps) => {
control={form.control}
name="slug"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input
placeholder="project-1" {...field}
placeholder="project-1"
{...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
field.onChange(value)
}}/>
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
field.onChange(value);
}}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="users"
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Databases</FormLabel>
<FormControl>
<MultiSelect
options={formatUsersList(props.users)}
onValueChange={field.onChange}
@@ -147,16 +123,13 @@ export const OrganizationForm = (props: organizationFormProps) => {
/>
</FormControl>
<FormDescription>Select users you want to add to this organization</FormDescription>
<FormMessage/>
<FormMessage />
</FormItem>
)
}
)}
/>
<Button>
{isCreate ? `Create Organization` : `Update Organization`}
</Button>
<Button>{isCreate ? `Create Organization` : `Update Organization`}</Button>
</Form>
</CardContent>
</Card>
)
}
);
};
@@ -1,27 +1,27 @@
"use client"
"use client";
import {useMutation} from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from "@/components/ui/dialog";
import {Button} from "@/components/ui/button";
import {Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
import {createOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
import {useRouter} from "next/navigation";
import {useState} from "react";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { OrganizationSchema } from "@/components/wrappers/dashboard/organization/organization.schema";
import { createOrganizationAction } from "@/components/wrappers/dashboard/organization/organization.action";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { authClient } from "@/lib/auth/auth-client";
export type createOrganizationModalProps = {
children: any
}
children: any;
};
export function CreateOrganizationModal(props: createOrganizationModalProps) {
const [open, setOpen] = useState(false);
const {children} = props;
const { children } = props;
const router = useRouter()
const router = useRouter();
const form = useZodForm({
schema: OrganizationSchema,
@@ -29,46 +29,49 @@ export function CreateOrganizationModal(props: createOrganizationModalProps) {
const mutation = useMutation({
mutationFn: async (values: OrganizationSchema) => {
console.log(values)
const result = await createOrganizationAction(values)
setOpen(false);
router.refresh()
}
})
console.log(values);
const result = await createOrganizationAction(values);
if (result && result.data) {
if (result.data.success && result.data.value) {
setOpen(false);
await authClient.organization.setActive({ organizationSlug: result.data.value.slug });
router.replace(`/dashboard/${result.data.value.slug}/home`);
}
}
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{children}
</DialogTrigger>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent className="sm:max-w-[425px] w-full">
<DialogHeader>
<DialogTitle>Create a new organization</DialogTitle>
</DialogHeader>
<div className="sm:max-w-[375px] w-full">
<Form form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
console.log(values)
await mutation.mutateAsync(values);
}}
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
console.log(values);
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -76,18 +79,19 @@ export function CreateOrganizationModal(props: createOrganizationModalProps) {
control={form.control}
name="slug"
defaultValue=""
render={({field}) => (
render={({ field }) => (
<FormItem>
<FormLabel>Slug</FormLabel>
<FormControl>
<Input {...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase()
field.onChange(value)
}}
<Input
{...field}
onChange={(e) => {
const value = e.target.value.replaceAll(" ", "-").toLowerCase();
field.onChange(value);
}}
/>
</FormControl>
<FormMessage/>
<FormMessage />
</FormItem>
)}
/>
@@ -98,10 +102,7 @@ export function CreateOrganizationModal(props: createOrganizationModalProps) {
</DialogFooter>
</Form>
</div>
</DialogContent>
</Dialog>
)
}
);
}
@@ -1,76 +1,36 @@
"use client"
"use client";
import {useEffect, useState} from "react";
import { ComboBox } from "@/components/wrappers/common/combobox";
import { useSidebar } from "@/components/ui/sidebar";
import { useRouter } from "next/navigation";
import { authClient } from "@/lib/auth/auth-client";
import {ComboBox} from "@/components/wrappers/common/combobox";
import {Organization} from "@prisma/client";
import {
getCurrentOrganizationSlug,
setCurrentOrganizationSlug
} from "@/features/dashboard/organization-cookie";
import {useSidebar} from "@/components/ui/sidebar";
import {useRouter} from "next/navigation";
export function OrganizationCombobox() {
const router = useRouter();
export type organizationComboBoxProps = {
organizations: Organization[]
defaultOrganization: Organization
}
const { data: organizations } = authClient.useListOrganizations();
const { data: activeOrganization } = authClient.useActiveOrganization();
if (!organizations) return null;
export function OrganizationCombobox(props: organizationComboBoxProps) {
const router = useRouter()
// const {organizationId, moveToAnotherOrganization} = useStore((state) => state);
console.log("organizations", organizations);
console.log("activeOrganization", activeOrganization);
const [organizationSlug, setOrganizationSlug] = useState<string>()
const {organizations, defaultOrganization} = props
useEffect(() => {
getCurrentOrganizationSlug().then(slug => {
if (slug == "") {
setOrganizationSlug(defaultOrganization.id)
setCurrentOrganizationSlug(defaultOrganization.slug)
} else {
setOrganizationSlug(slug)
const organization = organizations.find(organization => organization.slug === slug)
if (!organization) {
setOrganizationSlug(defaultOrganization.id)
setCurrentOrganizationSlug(defaultOrganization.slug)
}
}
})
}, [organizationSlug])
const values = organizations.map(organization => {
return ({
const values = organizations.map((organization) => {
return {
value: organization.slug,
label: organization.name,
})
})
};
});
const onValueChange = (slug: string) => {
if (organizationSlug !== slug) {
setOrganizationSlug(slug)
setCurrentOrganizationSlug(slug)
router.replace("/dashboard")
}
}
const {state, isMobile} = useSidebar();
const onValueChange = async (slug: string) => {
await authClient.organization.setActive({
organizationSlug: slug,
});
router.replace(`/dashboard/${slug}/home`);
router.refresh();
};
const { state } = useSidebar();
return (
<>
{state === 'expanded' && (
<ComboBox
sideBar
values={values}
defaultValue={organizationSlug}
onValueChange={onValueChange}/>
)}
</>
)
}
return <>{state === "expanded" && <ComboBox sideBar values={values} defaultValue={activeOrganization?.slug} onValueChange={onValueChange} />}</>;
}
@@ -1,218 +1,160 @@
"use server"
"use server";
import {ActionError, userAction} from "@/safe-actions";
import {prisma} from "@/prisma";
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
import {ServerActionResult} from "@/types/action-type";
import {Organization} from "@prisma/client";
import {z} from "zod";
import {v4 as uuidv4} from "uuid";
import {getDb} from "@/db";
import {
OrganizationFormSchema
} from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm.schema";
import { userAction } from "@/safe-actions";
import { OrganizationSchema } from "@/components/wrappers/dashboard/organization/organization.schema";
import { ServerActionResult } from "@/types/action-type";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { OrganizationFormSchema } from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
import { db } from "@/db";
import { Organization, organization as drizzleOrganization, organizationMember as drizzleOrganizationMember } from "@/db/schema";
import { eq } from "drizzle-orm";
import { checkSlugOrganization, createOrganization } from "@/lib/auth/auth";
const verifySlugUniqueness = async (slug: string) => {
const slugExists = await prisma.organization.count({
where: {
slug: slug
},
})
if (slugExists) {
throw new ActionError("Slug already exists.");
}
}
export const createOrganizationAction = userAction
.schema(OrganizationSchema)
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
try {
await verifySlugUniqueness(parsedInput.slug)
const organization = await prisma.organization.create({
data: {
...parsedInput
}
})
await prisma.userOrganization.create({
data: {
userId: ctx.user.id,
organizationId: organization.id,
role: "admin"
},
});
return {
success: true,
value: organization,
actionSuccess: {
message: "Organization has been successfully created.",
messageParams: {organizationId: organization.id},
},
};
} catch (error) {
console.error("Error creating organization:", error);
export const createOrganizationAction = userAction.schema(OrganizationSchema).action(async ({ parsedInput }): Promise<ServerActionResult<Organization>> => {
try {
if (!checkSlugOrganization(parsedInput.slug)) {
return {
success: false,
actionError: {
message: "Failed to create organization.",
message: "Slug is already taken",
status: 500,
cause: error.message ?? "Unknown error",
messageParams: {message: "Error creating the organization"},
messageParams: { message: "Error creating the organization" },
},
};
}
});
const organization = await createOrganization(parsedInput.name, parsedInput.slug);
return {
success: true,
value: organization!,
actionSuccess: {
message: "Organization has been successfully created.",
messageParams: { organizationId: organization!.id },
},
};
} catch (error) {
console.error("Error creating organization:", error);
return {
success: false,
actionError: {
message: "Failed to create organization.",
status: 500,
messageParams: { message: "Error creating the organization" },
},
};
}
});
export const updateOrganizationAction = userAction
.schema(
z.object({
data: OrganizationFormSchema,
organizationId: z.string()
}))
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
organizationId: z.string(),
})
)
.action(async ({ parsedInput }): Promise<ServerActionResult<Organization>> => {
try {
const newUserList = parsedInput.data.users;
const newUserList = parsedInput.data.users
const organization = await db.select().from(organization).where(eq(organization.id, parsedInput.organizationId)).execute();
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 (organization.length === 0) {
throw new Error("Organization not found.");
}
const existingItemIds = organization[0].users.map((user) => user.userId);
const usersToAdd = newUserList.filter((id) => !existingItemIds.includes(id));
const usersToRemove = existingItemIds.filter((id) => !newUserList.includes(id));
if (usersToAdd.length > 0) {
await prisma.userOrganization.createMany({
data: usersToAdd.map((userId) => ({
userId: userId,
organizationId: organization.id,
role: "member"
})),
skipDuplicates: true, // Optional: to avoid duplicate insertion errors
});
await db
.insert(userOrganization)
.values(
usersToAdd.map((userId) => ({
userId,
organizationId: organization[0].id,
role: "member",
}))
)
.execute();
}
if (usersToRemove.length > 0) {
await prisma.userOrganization.deleteMany({
where: {
userId: { in: usersToRemove },
},
});
await db.delete().from(userOrganization).where(inArray(userOrganization.userId, usersToRemove)).execute();
}
const updatedOrganization = await prisma.organization.update({
where:{
id: organization.id
},
data:{
const updatedOrganization = await db
.update(organization)
.set({
name: parsedInput.data.name,
slug: parsedInput.data.slug,
}
})
})
.where(eq(organization.id, parsedInput.organizationId))
.returning()
.execute();
return {
success: true,
value: updatedOrganization,
value: updatedOrganization[0],
actionSuccess: {
message: "Organization has been successfully updated.",
messageParams: {organizationId: updatedOrganization.id},
messageParams: { organizationId: updatedOrganization[0].id },
},
};
} catch (error) {
console.error("Error updating organization:", error);
return {
success: false,
actionError: {
message: "Failed to update organization.",
status: 500,
cause: error.message ?? "Unknown error",
messageParams: {message: "Error updating the organization"},
messageParams: { message: "Error updating the organization" },
},
};
}
});
export const deleteOrganizationAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }): Promise<ServerActionResult<Organization>> => {
try {
const uuid = uuidv4();
const organization = await db.select().from(organization).where(eq(organization.slug, parsedInput)).execute();
export const deleteOrganizationAction = userAction
.schema(z.string())
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
console.log(parsedInput);
try {
const db = await getDb()
const uuid = uuidv4()
const organization = await db.organization.findFirst({
where: {
slug: parsedInput,
}
})
console.log(organization);
const organizationUpdated = await db.organization.update({
where: {
slug: parsedInput,
},
data: {
name: `${organization.name}-${uuid}`,
slug: `${organization.slug}-${uuid}`,
deleted : true
}
})
return {
success: true,
value: organizationUpdated,
actionSuccess: {
message: "Organization has been successfully deleted.",
messageParams: {organizationId: organizationUpdated.id},
},
};
} catch (error) {
console.error("Error deleting organization:", error);
return {
success: false,
actionError: {
message: "Failed to delete organization.",
status: 500,
cause: error.message ?? "Unknown error",
messageParams: {message: "Error deleting the organization"},
},
};
if (organization.length === 0) {
throw new Error("Organization not found.");
}
});
const updatedOrganization = await db
.update(organization)
.set({
name: `${organization[0].name}-${uuid}`,
slug: `${organization[0].slug}-${uuid}`,
deleted: true,
})
.where(eq(organization.id, organization[0].id))
.returning()
.execute();
return {
success: true,
value: updatedOrganization[0],
actionSuccess: {
message: "Organization has been successfully deleted.",
messageParams: { organizationId: updatedOrganization[0].id },
},
};
} catch (error) {
console.error("Error deleting organization:", error);
return {
success: false,
actionError: {
message: "Failed to delete organization.",
status: 500,
cause: error.message ?? "Unknown error",
messageParams: { message: "Error deleting the organization" },
},
};
}
});
@@ -1,12 +1,12 @@
import {z} from "zod";
import { z } from "zod";
export const OrganizationSchema = 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'),
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"),
});
export type OrganizationSchema = z.infer<typeof OrganizationSchema>;