mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
chore(release): 1.2.4-rc.1
This commit is contained in:
@@ -58,7 +58,7 @@ function DialogContent({
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
"max-h-[90vh] overflow-y-auto",
|
||||
"max-h-[90vh] overflow-y-auto overflow-x-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import Link from "next/link";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {Plus} from "lucide-react";
|
||||
import {forwardRef} from "react";
|
||||
|
||||
type EmptyStatePlaceholderProps = {
|
||||
url?: string;
|
||||
onClick?: () => void;
|
||||
text: string;
|
||||
className?: string;
|
||||
};
|
||||
} & React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const EmptyStatePlaceholder = ({
|
||||
export const EmptyStatePlaceholder = forwardRef<HTMLDivElement, EmptyStatePlaceholderProps>(({
|
||||
url,
|
||||
onClick,
|
||||
text,
|
||||
className,
|
||||
}: EmptyStatePlaceholderProps) => {
|
||||
...props
|
||||
}, ref) => {
|
||||
const Container = (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -37,5 +39,7 @@ export const EmptyStatePlaceholder = ({
|
||||
);
|
||||
}
|
||||
|
||||
return <div className={cn(className)}>{Container}</div>;
|
||||
};
|
||||
return <div className={cn(className)} ref={ref} {...props}>{Container}</div>;
|
||||
});
|
||||
|
||||
EmptyStatePlaceholder.displayName = "EmptyStatePlaceholder";
|
||||
@@ -15,7 +15,7 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
||||
* Variants for the multi-select component to handle different styles.
|
||||
* Uses class-variance-authority (cva) to define different styles based on "variant" prop.
|
||||
*/
|
||||
const multiSelectVariants = cva("m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", {
|
||||
const multiSelectVariants = cva("m-1 transition ease-in-out duration-300", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-foreground/10 text-foreground bg-card hover:bg-card/80",
|
||||
@@ -177,13 +177,13 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
return (
|
||||
<Badge
|
||||
key={value}
|
||||
className={cn(isAnimating ? "animate-bounce" : "", multiSelectVariants({ variant }))}
|
||||
className={cn(isAnimating ? "animate-bounce" : "", "max-w-full", multiSelectVariants({ variant }))}
|
||||
style={{ animationDuration: `${animation}s` }}
|
||||
>
|
||||
{IconComponent && <IconComponent className="h-4 w-4 mr-2" />}
|
||||
{option?.label}
|
||||
<span className="truncate">{option?.label}</span>
|
||||
<XCircle
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
className="ml-2 h-4 w-4 cursor-pointer shrink-0"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
toggleOption(value);
|
||||
@@ -196,6 +196,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
<Badge
|
||||
className={cn(
|
||||
"bg-transparent text-foreground border-foreground/1 hover:bg-transparent",
|
||||
"max-w-full",
|
||||
isAnimating ? "animate-bounce" : "",
|
||||
multiSelectVariants({ variant })
|
||||
)}
|
||||
@@ -203,7 +204,7 @@ export const MultiSelect = React.forwardRef<HTMLButtonElement, MultiSelectProps>
|
||||
>
|
||||
{`+ ${selectedValues.length - maxCount} more`}
|
||||
<XCircle
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
className="ml-2 h-4 w-4 cursor-pointer shrink-0"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
clearExtraOptions();
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import {Trash2} from "lucide-react";
|
||||
import {toast} from "sonner";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {deleteOrganizationAction} from "@/features/organization/organization.action";
|
||||
|
||||
export type ButtonDeleteFleetProps = {
|
||||
text?: string;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import {Input} from "@/components/ui/input";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {toast} from "sonner";
|
||||
import {OrganizationWithMembersAndUsers} from "@/db/schema/03_organization";
|
||||
import {updateOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {updateOrganizationAction} from "@/features/organization/organization.action";
|
||||
|
||||
type UpdateOrganizationFormProps = {
|
||||
onSuccessAction?: () => void;
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
"use server";
|
||||
import {ActionError, userAction} from "@/lib/safe-actions/actions";
|
||||
import {AgentSchema} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
|
||||
import {z} from "zod";
|
||||
import {eq, and, ne, count} from "drizzle-orm";
|
||||
import {db} from "@/db";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {slugify} from "@/utils/slugify";
|
||||
|
||||
const verifySlugUniqueness = async (slug: string, agentId?: string) => {
|
||||
const conditions = agentId ? and(eq(drizzleDb.schemas.agent.slug, slug), ne(drizzleDb.schemas.agent.id, agentId)) : eq(drizzleDb.schemas.agent.slug, slug);
|
||||
|
||||
const [countResult] = await db.select({count: count()}).from(drizzleDb.schemas.agent).where(conditions);
|
||||
|
||||
if (countResult.count > 0) {
|
||||
throw new ActionError("Slug already exists");
|
||||
}
|
||||
};
|
||||
|
||||
export const createAgentAction = userAction.schema(AgentSchema).action(async ({parsedInput}) => {
|
||||
const slug = slugify(parsedInput.name);
|
||||
await verifySlugUniqueness(slug);
|
||||
const [createdAgent] = await db.insert(drizzleDb.schemas.agent).values({...parsedInput, slug: slug}).returning();
|
||||
return {
|
||||
data: createdAgent,
|
||||
};
|
||||
});
|
||||
|
||||
export const updateAgentAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
data: AgentSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}) => {
|
||||
const slug = slugify(parsedInput.data.name);
|
||||
await verifySlugUniqueness(slug, parsedInput.id);
|
||||
|
||||
const [updatedAgent] = await db.update(drizzleDb.schemas.agent).set({
|
||||
...parsedInput.data,
|
||||
slug: slug
|
||||
}).where(eq(drizzleDb.schemas.agent.id, parsedInput.id)).returning();
|
||||
|
||||
return {
|
||||
data: updatedAgent,
|
||||
};
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const AgentSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
});
|
||||
|
||||
export type AgentType = z.infer<typeof AgentSchema>;
|
||||
@@ -1,109 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Card, CardContent} 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 {useRouter} from "next/navigation";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {TooltipProvider} from "@/components/ui/tooltip";
|
||||
import {AgentSchema, AgentType} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.schema";
|
||||
import {toast} from "sonner";
|
||||
import {createAgentAction, updateAgentAction} from "@/components/wrappers/dashboard/agent/agent-form/agent-form.action";
|
||||
|
||||
export type agentFormProps = {
|
||||
defaultValues?: AgentType;
|
||||
agentId?: string;
|
||||
};
|
||||
|
||||
export const AgentForm = (props: agentFormProps) => {
|
||||
const isCreate = !Boolean(props.defaultValues);
|
||||
|
||||
const form = useZodForm({
|
||||
schema: AgentSchema,
|
||||
defaultValues: props.defaultValues,
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: AgentType) => {
|
||||
|
||||
const createAgent = isCreate
|
||||
? await createAgentAction(values)
|
||||
: await updateAgentAction({
|
||||
id: props.agentId ?? "-",
|
||||
data: values,
|
||||
});
|
||||
|
||||
const data = createAgent?.data?.data;
|
||||
if (createAgent?.serverError || !data) {
|
||||
toast.error(createAgent?.serverError);
|
||||
return;
|
||||
}
|
||||
toast.success(`Success ${isCreate ? "creating" : "updating"} agent`);
|
||||
router.refresh();
|
||||
router.push(`/dashboard/agents/${data.id}`);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<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="Agent 1" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>Your agent project name</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue=""
|
||||
name="description"
|
||||
render={({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="This agent is for the client exemple.com" {...field}
|
||||
value={field.value ?? ""}/>
|
||||
</FormControl>
|
||||
<FormDescription>Enter your project agent description</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">
|
||||
{isCreate ? "Create" : "Update"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
@@ -6,8 +6,8 @@ import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle} from "@/
|
||||
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 {CreateOrganizationSchema, CreateOrganizationType} from "@/features/organization/organization.schema";
|
||||
import {createOrganizationAction} from "@/features/organization/organization.action";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
@@ -32,12 +32,12 @@ export function CreateOrganizationModal({
|
||||
const router = useRouter();
|
||||
|
||||
const form = useZodForm({
|
||||
schema: OrganizationSchema,
|
||||
schema: CreateOrganizationSchema,
|
||||
});
|
||||
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: OrganizationSchema) => {
|
||||
mutationFn: async (values: CreateOrganizationType) => {
|
||||
|
||||
const result = await createOrganizationAction(values);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {deleteOrganizationAction} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {deleteOrganizationAction} from "@/features/organization/organization.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
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>;
|
||||
-167
@@ -1,167 +0,0 @@
|
||||
"use client";
|
||||
|
||||
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 {MultiSelect} from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import {
|
||||
OrganizationFormSchema,
|
||||
OrganizationFormType
|
||||
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
|
||||
import {MemberWithUser, OrganizationWithMembers} from "@/db/schema/03_organization";
|
||||
import {
|
||||
updateOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/organization/organization.action";
|
||||
import {toast} from "sonner";
|
||||
import {User as BetterAuthUser} from "better-auth";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
|
||||
export type organizationFormProps = {
|
||||
defaultValues?: OrganizationWithMembers;
|
||||
users: User[];
|
||||
currentUser: BetterAuthUser;
|
||||
};
|
||||
|
||||
export const OrganizationForm = (props: organizationFormProps) => {
|
||||
const {data: activeOrganization, refetch: refetchActiveOrga} = authClient.useActiveOrganization();
|
||||
const {data: organizations, refetch} = authClient.useListOrganizations();
|
||||
|
||||
const router = useRouter();
|
||||
const isCreate = !Boolean(props.defaultValues);
|
||||
|
||||
const formatUsersList = (users: User[]) => {
|
||||
|
||||
return users
|
||||
.filter((user) => user.id !== props.currentUser.id)
|
||||
.map((user) => ({
|
||||
value: user.id,
|
||||
label: `${user.name} | ${user.email}`,
|
||||
}));
|
||||
};
|
||||
|
||||
const formatDefaultUsers = (members: MemberWithUser[]): string[] => {
|
||||
return members
|
||||
.filter((member) => member.userId !== props.currentUser.id)
|
||||
.map((member) => member.userId);
|
||||
};
|
||||
|
||||
const formattedDefaultValues = {
|
||||
name: props.defaultValues?.name,
|
||||
slug: props.defaultValues?.slug,
|
||||
users: !isCreate ? formatDefaultUsers(props.defaultValues?.members as MemberWithUser[]) : [],
|
||||
};
|
||||
|
||||
const form = useZodForm({
|
||||
schema: OrganizationFormSchema,
|
||||
defaultValues: formattedDefaultValues,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (values: OrganizationFormType) => updateOrganizationAction({
|
||||
data: values,
|
||||
organizationId: props.defaultValues?.id ?? ""
|
||||
}),
|
||||
onSuccess: async (result) => {
|
||||
if (result?.data?.success) {
|
||||
toast.success(result.data.actionSuccess?.message || "Organization updated successfully.");
|
||||
refetch()
|
||||
refetchActiveOrga()
|
||||
router.push("/dashboard/settings");
|
||||
} else {
|
||||
// @ts-ignore
|
||||
const errorMsg = result?.data?.actionError?.message || result?.data?.actionError?.messageParams?.message || "Failed to update the organization.";
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
console.error("Mutation network error:", error);
|
||||
toast.error(error?.message || "A network error occurred.");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<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>Users</FormLabel>
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
options={formatUsersList(props.users)}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ?? []}
|
||||
placeholder="Select users"
|
||||
variant="inverted"
|
||||
animation={2}
|
||||
// maxCount={100}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Select users you want to add to this organization</FormDescription>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">
|
||||
{isCreate ? "Create" : "Update"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,224 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {z} from "zod";
|
||||
import {
|
||||
OrganizationFormSchema
|
||||
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
|
||||
import {db} from "@/db";
|
||||
import {and, eq, inArray, or} from "drizzle-orm";
|
||||
import {auth, checkSlugOrganization, createOrganization} from "@/lib/auth/auth";
|
||||
import {slugify} from "@/utils/slugify";
|
||||
import {Organization} from "@/db/schema/03_organization";
|
||||
import * as drizzleDb from "@/db";
|
||||
|
||||
export const createOrganizationAction = userAction.schema(OrganizationSchema).action(async ({parsedInput}): Promise<ServerActionResult<Organization>> => {
|
||||
try {
|
||||
const slug = slugify(parsedInput.name);
|
||||
if (!await checkSlugOrganization(slug)) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Slug is already taken",
|
||||
status: 500,
|
||||
messageParams: {message: "Error creating the organization"},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let createdOrganization: Organization;
|
||||
|
||||
try {
|
||||
createdOrganization = await createOrganization(parsedInput.name, slug) as unknown as Organization;
|
||||
} catch (authError: any) {
|
||||
console.error("Auth deletion failed:", authError);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: authError.message || "Authentication service error.",
|
||||
status: authError.status || 500,
|
||||
cause: "auth_error",
|
||||
messageParams: {message: authError.message},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: createdOrganization,
|
||||
actionSuccess: {
|
||||
message: "Organization has been successfully created.",
|
||||
messageParams: {organizationId: createdOrganization!.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>> => {
|
||||
try {
|
||||
const newUserList = parsedInput.data.users;
|
||||
const organization = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.id, parsedInput.organizationId),
|
||||
with: {
|
||||
members: true,
|
||||
}
|
||||
});
|
||||
|
||||
if (!organization) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Organization not found.",
|
||||
status: 404,
|
||||
cause: "not_found",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const existingItemIds = organization.members
|
||||
.filter((member) => member.userId !== ctx.user.id)
|
||||
.map((member) => member.userId);
|
||||
const usersToAdd = newUserList.filter((id) => !existingItemIds.includes(id));
|
||||
const usersToRemove = existingItemIds.filter((id) => !newUserList.includes(id));
|
||||
|
||||
if (usersToAdd.length > 0) {
|
||||
for (const userToAdd of usersToAdd) {
|
||||
await auth.api.addMember({
|
||||
body: {
|
||||
userId: userToAdd,
|
||||
role: "member",
|
||||
organizationId: organization.id,
|
||||
},
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
if (usersToRemove.length > 0) {
|
||||
await db.delete(drizzleDb.schemas.member).where(and(inArray(drizzleDb.schemas.member.userId, usersToRemove), eq(drizzleDb.schemas.member.organizationId, organization.id))).execute();
|
||||
|
||||
}
|
||||
const updatedOrganization = await db
|
||||
.update(drizzleDb.schemas.organization)
|
||||
.set({
|
||||
name: parsedInput.data.name,
|
||||
slug: parsedInput.data.slug,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.organization.id, parsedInput.organizationId))
|
||||
.returning()
|
||||
.execute();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: updatedOrganization as unknown as Organization,
|
||||
actionSuccess: {
|
||||
message: "Organization has been successfully updated.",
|
||||
messageParams: {organizationId: organization.id},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error updating organization:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update organization.",
|
||||
status: 500,
|
||||
cause: "server_error",
|
||||
messageParams: {message: "Error updating the organization"},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const deleteOrganizationAction = userAction.schema(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
slug: z.string().optional(),
|
||||
})
|
||||
).action(
|
||||
async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
|
||||
try {
|
||||
const conditions = [];
|
||||
if (parsedInput.id) {
|
||||
conditions.push(eq(drizzleDb.schemas.organization.id, parsedInput.id));
|
||||
}
|
||||
if (parsedInput.slug) {
|
||||
conditions.push(eq(drizzleDb.schemas.organization.slug, parsedInput.slug));
|
||||
}
|
||||
|
||||
const org = await db.query.organization.findFirst({
|
||||
where: or(...conditions),
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Organization not found.",
|
||||
status: 404,
|
||||
cause: "not_found",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let deletedOrganization: Organization;
|
||||
|
||||
try {
|
||||
[deletedOrganization] = await db
|
||||
.delete(drizzleDb.schemas.organization)
|
||||
.where(eq(drizzleDb.schemas.organization.id, org.id))
|
||||
.returning();
|
||||
|
||||
} catch (authError: any) {
|
||||
console.error("Auth deletion failed:", authError);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: authError.message || "Authentication service error.",
|
||||
status: authError.status || 500,
|
||||
cause: "auth_error",
|
||||
messageParams: {message: authError.message},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: deletedOrganization,
|
||||
actionSuccess: {
|
||||
message: "Organization has been successfully deleted.",
|
||||
messageParams: {organizationId: deletedOrganization.id},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Unexpected error in deleteOrganizationAction:", error);
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to delete organization due to a server error.",
|
||||
status: 500,
|
||||
cause: "server_error",
|
||||
messageParams: {message: "Internal server error while deleting the organization"},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,12 +0,0 @@
|
||||
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"),
|
||||
});
|
||||
|
||||
export type OrganizationSchema = z.infer<typeof OrganizationSchema>;
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {ProjectSchema} from "@/components/wrappers/dashboard/projects/project-form/project-form.schema";
|
||||
import {z} from "zod";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {db} from "@/db";
|
||||
import {and, eq, inArray} from "drizzle-orm";
|
||||
import {Project} from "@/db/schema/06_project";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {Database} from "@/db/schema/07_database";
|
||||
import {slugify} from "@/utils/slugify";
|
||||
|
||||
export const createProjectAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
data: ProjectSchema,
|
||||
organizationId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Project>> => {
|
||||
try {
|
||||
const slug = slugify(parsedInput.data.name);
|
||||
|
||||
const existingProject = await db.query.project.findFirst({
|
||||
where: and(eq(drizzleDb.schemas.project.name, parsedInput.data.name)),
|
||||
})
|
||||
|
||||
if (existingProject) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "A project with this name already exists.",
|
||||
status: 400,
|
||||
messageParams: {projectName: parsedInput.data.name},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [createdProject] = await db
|
||||
.insert(drizzleDb.schemas.project)
|
||||
.values({
|
||||
name: parsedInput.data.name,
|
||||
slug: slug,
|
||||
organizationId: parsedInput.organizationId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (parsedInput.data.databases.length > 0) {
|
||||
await db
|
||||
.update(drizzleDb.schemas.database)
|
||||
.set({projectId: createdProject.id})
|
||||
.where(inArray(drizzleDb.schemas.database.id, parsedInput.data.databases));
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: createdProject,
|
||||
actionSuccess: {
|
||||
message: "Project has been successfully created.",
|
||||
messageParams: {projectName: parsedInput.data.name},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to create project.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {projectName: parsedInput.data.name},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export const updateProjectAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
data: ProjectSchema,
|
||||
organizationId: z.string(),
|
||||
projectId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Project>> => {
|
||||
try {
|
||||
const existing = await db.query.project.findFirst({
|
||||
where: eq(drizzleDb.schemas.project.id, parsedInput.projectId),
|
||||
with: {
|
||||
databases: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
throw new Error("Project not found.");
|
||||
}
|
||||
|
||||
const existingDbIds = existing.databases.map((db: Database) => db.id);
|
||||
const newDbIds = parsedInput.data.databases;
|
||||
|
||||
const databasesToAdd = newDbIds.filter((id) => !existingDbIds.includes(id));
|
||||
const databasesToRemove = existingDbIds.filter((id: string) => !newDbIds.includes(id));
|
||||
|
||||
if (databasesToAdd.length > 0) {
|
||||
await db.update(drizzleDb.schemas.database).set({projectId: parsedInput.projectId}).where(inArray(drizzleDb.schemas.database.id, databasesToAdd));
|
||||
}
|
||||
|
||||
if (databasesToRemove.length > 0) {
|
||||
await db.update(drizzleDb.schemas.database).set({
|
||||
projectId: null,
|
||||
backupPolicy: null
|
||||
}).where(inArray(drizzleDb.schemas.database.id, databasesToRemove));
|
||||
|
||||
await db.delete(drizzleDb.schemas.retentionPolicy)
|
||||
.where(inArray(drizzleDb.schemas.retentionPolicy.databaseId, databasesToRemove)).execute();
|
||||
|
||||
await db.delete(drizzleDb.schemas.alertPolicy)
|
||||
.where(inArray(drizzleDb.schemas.alertPolicy.databaseId, databasesToRemove)).execute();
|
||||
|
||||
}
|
||||
// const slug = slugify(parsedInput.data.name);
|
||||
|
||||
const [updatedProject] = await db
|
||||
.update(drizzleDb.schemas.project)
|
||||
.set({
|
||||
name: parsedInput.data.name,
|
||||
// slug: slug,
|
||||
})
|
||||
.where(eq(drizzleDb.schemas.project.id, parsedInput.projectId))
|
||||
.returning();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: updatedProject,
|
||||
actionSuccess: {
|
||||
message: "Project has been successfully updated.",
|
||||
messageParams: {projectName: parsedInput.data.name},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Failed to update project.",
|
||||
status: 500,
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
messageParams: {projectName: parsedInput.data.name},
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ProjectSchema = z.object({
|
||||
name: z.string(),
|
||||
databases: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type ProjectType = z.infer<typeof ProjectSchema>;
|
||||
@@ -1,131 +0,0 @@
|
||||
"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 { ProjectSchema, ProjectType } from "@/components/wrappers/dashboard/projects/project-form/project-form.schema";
|
||||
import { createProjectAction, updateProjectAction } from "@/components/wrappers/dashboard/projects/project-form/project-form.action";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { MultiSelect } from "@/components/wrappers/common/multiselect/multi-select";
|
||||
import { toast } from "sonner";
|
||||
import {DatabaseWith} from "@/db/schema/07_database";
|
||||
import {Organization} from "@/db/schema/03_organization";
|
||||
|
||||
export type projectFormProps = {
|
||||
defaultValues?: ProjectType;
|
||||
databases: DatabaseWith[];
|
||||
organization: Organization;
|
||||
projectId?: string;
|
||||
};
|
||||
|
||||
export const ProjectForm = (props: projectFormProps) => {
|
||||
const router = useRouter();
|
||||
const isCreate = !Boolean(props.defaultValues);
|
||||
const formatDatabasesList = (databases: DatabaseWith[]) => {
|
||||
return databases.map((database) => ({
|
||||
value: database.id,
|
||||
label: `${database.name} (${database.agentDatabaseId}) | ${database.agent?.name}`,
|
||||
}));
|
||||
};
|
||||
|
||||
const formatDefaultDatabases = (databases: string[]): string[] => {
|
||||
return databases;
|
||||
};
|
||||
|
||||
const formattedDefaultValues = {
|
||||
...props.defaultValues,
|
||||
databases: !isCreate ? formatDefaultDatabases(props.defaultValues?.databases ?? []) : [],
|
||||
};
|
||||
|
||||
const form = useZodForm({
|
||||
schema: ProjectSchema,
|
||||
defaultValues: formattedDefaultValues,
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async (values: ProjectType) => {
|
||||
if (!isCreate && !props.projectId) {
|
||||
throw new Error("Project ID is required for updates");
|
||||
}
|
||||
const project = isCreate
|
||||
? await createProjectAction({
|
||||
data: values,
|
||||
organizationId: props.organization.id,
|
||||
})
|
||||
: await updateProjectAction({
|
||||
data: values,
|
||||
organizationId: props.organization.id,
|
||||
projectId: props.projectId!,
|
||||
});
|
||||
|
||||
if (project && project.data) {
|
||||
if (project.data.success) {
|
||||
project.data.actionSuccess && toast.success(project.data.actionSuccess.message);
|
||||
router.refresh();
|
||||
router.push(`/dashboard/projects/${project.data.value!.id}`);
|
||||
} else {
|
||||
project.data.actionError && toast.error(project.data.actionError.message || "Unknown error occurred.");
|
||||
router.refresh();
|
||||
}
|
||||
} else {
|
||||
toast.error("Failed to process request. No response received.");
|
||||
router.refresh();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
className="flex flex-col gap-4"
|
||||
onSubmit={async (values) => {
|
||||
await mutation.mutateAsync(values);
|
||||
}}
|
||||
>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Project 1" {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="databases"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Databases</FormLabel>
|
||||
<FormControl>
|
||||
<MultiSelect
|
||||
options={formatDatabasesList(props.databases)}
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value ?? []}
|
||||
placeholder="Select databases"
|
||||
variant="inverted"
|
||||
animation={2}
|
||||
// maxCount={100}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>Select databases you want to add to this project</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit">
|
||||
{isCreate ? "Create" : "Update"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user