mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on add/delete users to workspace. Edit workspaces and some permissions.
This commit is contained in:
@@ -8,11 +8,21 @@ import {ButtonDeleteProject} from "@/components/wrappers/dashboard/projects/Butt
|
|||||||
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
|
||||||
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
|
||||||
import {notFound} from "next/navigation";
|
import {notFound} from "next/navigation";
|
||||||
|
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
|
export default async function RoutePage(props: PageParams<{ slug:string, projectId: string }>) {
|
||||||
|
|
||||||
|
const {slug: organizationSlug, projectId} = await props.params
|
||||||
|
|
||||||
|
|
||||||
|
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
||||||
|
|
||||||
|
if(currentOrganizationSlug != organizationSlug) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const {projectId} = await props.params
|
|
||||||
|
|
||||||
const project = await prisma.project.findUnique({
|
const project = await prisma.project.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -33,7 +43,7 @@ export default async function RoutePage(props: PageParams<{ projectId: string }>
|
|||||||
<PageTitle className="flex items-center">
|
<PageTitle className="flex items-center">
|
||||||
{project.name}
|
{project.name}
|
||||||
<Link className={buttonVariants({variant: "outline"})}
|
<Link className={buttonVariants({variant: "outline"})}
|
||||||
href={`/dashboard/projects/${project.id}/edit`}>
|
href={`/dashboard/${currentOrganizationSlug}/projects/${project.id}/edit`}>
|
||||||
<GearIcon className="w-7 h-7"/>
|
<GearIcon className="w-7 h-7"/>
|
||||||
</Link>
|
</Link>
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
@@ -51,6 +61,7 @@ export default async function RoutePage(props: PageParams<{ projectId: string }>
|
|||||||
{project.databases.length > 0 ?
|
{project.databases.length > 0 ?
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
data={project.databases}
|
data={project.databases}
|
||||||
|
organizationSlug={organizationSlug}
|
||||||
cardItem={ProjectDatabaseCard}
|
cardItem={ProjectDatabaseCard}
|
||||||
cardsPerPage={4}
|
cardsPerPage={4}
|
||||||
numberOfColumns={1}
|
numberOfColumns={1}
|
||||||
|
|||||||
@@ -2,9 +2,17 @@ import {PageParams} from "@/types/next";
|
|||||||
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
import {prisma} from "@/prisma";
|
import {prisma} from "@/prisma";
|
||||||
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
|
import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{slug: string}>) {
|
||||||
|
const {slug: organizationSlug} = await props.params
|
||||||
|
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
||||||
|
if(currentOrganizationSlug != organizationSlug) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const availableDatabases = await prisma.database.findMany({
|
const availableDatabases = await prisma.database.findMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -20,7 +28,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
|
|
||||||
const organization = await prisma.organization.findFirst({
|
const organization = await prisma.organization.findFirst({
|
||||||
where: {
|
where: {
|
||||||
slug: 'default',
|
slug: currentOrganizationSlug,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default async function RoutePage(props: PageParams<{slug: string}>) {
|
|||||||
</PageTitle>
|
</PageTitle>
|
||||||
{projects.length > 0 && (
|
{projects.length > 0 && (
|
||||||
<PageActions>
|
<PageActions>
|
||||||
<Link href={"/dashboard/projects/new"}>
|
<Link href={`/dashboard/${currentOrganizationSlug}/projects/new`}>
|
||||||
<Button>+ Create Project</Button>
|
<Button>+ Create Project</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
@@ -59,6 +59,7 @@ export default async function RoutePage(props: PageParams<{slug: string}>) {
|
|||||||
|
|
||||||
{projects.length > 0 ?
|
{projects.length > 0 ?
|
||||||
<CardsWithPagination
|
<CardsWithPagination
|
||||||
|
organizationSlug={currentOrganizationSlug}
|
||||||
data={projects}
|
data={projects}
|
||||||
cardItem={ProjectCard}
|
cardItem={ProjectCard}
|
||||||
cardsPerPage={4}
|
cardsPerPage={4}
|
||||||
@@ -66,7 +67,7 @@ export default async function RoutePage(props: PageParams<{slug: string}>) {
|
|||||||
/>
|
/>
|
||||||
:
|
:
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/projects/new"
|
href={`/dashboard/${currentOrganizationSlug}/projects/new`}
|
||||||
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md">
|
className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md">
|
||||||
Create new Project
|
Create new Project
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import {PageParams} from "@/types/next";
|
||||||
|
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
|
||||||
|
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
||||||
|
import {prisma} from "@/prisma";
|
||||||
|
import {requiredCurrentUser} from "@/auth/current-user";
|
||||||
|
import {notFound} from "next/navigation";
|
||||||
|
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm";
|
||||||
|
|
||||||
|
|
||||||
|
export default async function RoutePage(props: PageParams<{
|
||||||
|
slug: string;
|
||||||
|
}>) {
|
||||||
|
const {slug: organizationSlug} = await props.params
|
||||||
|
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
||||||
|
|
||||||
|
if(currentOrganizationSlug != organizationSlug) {
|
||||||
|
notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const organization = await prisma.organization.findUnique({
|
||||||
|
where:{
|
||||||
|
slug: currentOrganizationSlug,
|
||||||
|
},
|
||||||
|
include:{
|
||||||
|
users:{
|
||||||
|
include:{
|
||||||
|
user:{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
deleted: {not: true},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return(
|
||||||
|
<Page>
|
||||||
|
<PageHeader>
|
||||||
|
<PageTitle>
|
||||||
|
Edit {organization.name}
|
||||||
|
</PageTitle>
|
||||||
|
</PageHeader>
|
||||||
|
<PageContent>
|
||||||
|
<OrganizationForm users={users} defaultValues={organization}/>
|
||||||
|
</PageContent>
|
||||||
|
</Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,11 +4,14 @@ import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle}
|
|||||||
import {requiredCurrentUser} from "@/auth/current-user";
|
import {requiredCurrentUser} from "@/auth/current-user";
|
||||||
import {SettingsTabs} from "@/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs";
|
import {SettingsTabs} from "@/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs";
|
||||||
import {getCurrentOrganizationId, getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
import {getCurrentOrganizationId, getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
|
||||||
import {Button} from "@/components/ui/button";
|
import {Button, buttonVariants} from "@/components/ui/button";
|
||||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||||
import {
|
import {
|
||||||
DeleteOrganizationButton
|
DeleteOrganizationButton
|
||||||
} from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton";
|
} from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton";
|
||||||
|
import {GearIcon} from "@radix-ui/react-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings";
|
||||||
|
|
||||||
|
|
||||||
export default async function RoutePage(props: PageParams<{}>) {
|
export default async function RoutePage(props: PageParams<{}>) {
|
||||||
@@ -17,7 +20,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
const currentOrganizationSlug = await getCurrentOrganizationSlug()
|
||||||
|
|
||||||
const organization = await prisma.organization.findUnique({
|
const organization = await prisma.organization.findUnique({
|
||||||
where:{
|
where: {
|
||||||
slug: currentOrganizationSlug,
|
slug: currentOrganizationSlug,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -35,6 +38,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const settings = await prisma.settings.findUnique({
|
const settings = await prisma.settings.findUnique({
|
||||||
where: {
|
where: {
|
||||||
name: "system"
|
name: "system"
|
||||||
@@ -44,12 +48,16 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<PageTitle>
|
<PageTitle className="flex items-center">
|
||||||
Settings
|
Settings
|
||||||
|
{organization.slug != "default" ?
|
||||||
|
<EditButtonSettings/>
|
||||||
|
: null}
|
||||||
</PageTitle>
|
</PageTitle>
|
||||||
<PageActions>
|
<PageActions>
|
||||||
{/*<Button variant="destructive">Delete Organization</Button>*/}
|
{organization.slug != "default" ?
|
||||||
<DeleteOrganizationButton organizationSlug={currentOrganizationSlug}/>
|
<DeleteOrganizationButton organizationSlug={currentOrganizationSlug}/>
|
||||||
|
: null}
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageDescription>
|
<PageDescription>
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export default async function RoutePage(props: PageParams<{}>) {
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PageContent className="mt-10">
|
<PageContent className="mt-10">
|
||||||
<AdminTabs settings={settings} currentUser={user} users={users}/>
|
<AdminTabs settings={settings} currentUser={user} users={users}/>
|
||||||
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {PaginationNavigation} from "@/components/wrappers/common/pagination/pagi
|
|||||||
|
|
||||||
export type cardsWithPaginationProps = {
|
export type cardsWithPaginationProps = {
|
||||||
className?: string
|
className?: string
|
||||||
|
organizationSlug?:string
|
||||||
data: Array<{}>;
|
data: Array<{}>;
|
||||||
cardItem: React.ComponentType;
|
cardItem: React.ComponentType;
|
||||||
cardsPerPage?: number
|
cardsPerPage?: number
|
||||||
@@ -18,7 +19,7 @@ export type cardsWithPaginationProps = {
|
|||||||
|
|
||||||
export const CardsWithPagination = (props: 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
|
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("flex flex-col h-full justify-between", className)}>
|
||||||
<div className={cn(`grid h-max auto-rows-min gap-4 md:grid-cols-${numberOfColumns}`)}>
|
<div className={cn(`grid h-max auto-rows-min gap-4 md:grid-cols-${numberOfColumns}`)}>
|
||||||
{currentCards.map((card, key) => (
|
{currentCards.map((card, key) => (
|
||||||
<CardItem key={key} data={card} extendedProps={props.extendedProps} />
|
<CardItem key={key} data={card} organizationSlug={organizationSlug} extendedProps={props.extendedProps} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<PaginationNavigation
|
<PaginationNavigation
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {flexRender, Row, RowData} from "@tanstack/react-table";
|
|||||||
|
|
||||||
import {User} from "@prisma/client";
|
import {User} from "@prisma/client";
|
||||||
import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination";
|
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 {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
|
||||||
import {cn} from "@/lib/utils";
|
import {cn} from "@/lib/utils";
|
||||||
|
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/columns-users";
|
||||||
|
|
||||||
export type AdminUsersTableProps = {
|
export type AdminUsersTableProps = {
|
||||||
currentUser: User;
|
currentUser: User;
|
||||||
@@ -14,7 +14,6 @@ export type AdminUsersTableProps = {
|
|||||||
export const AdminUsersTable = (props: AdminUsersTableProps) => {
|
export const AdminUsersTable = (props: AdminUsersTableProps) => {
|
||||||
|
|
||||||
const {currentUser, users} = props;
|
const {currentUser, users} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full py-4">
|
<div className="flex flex-col h-full py-4">
|
||||||
<div className="flex gap-4 h-fit justify-between">
|
<div className="flex gap-4 h-fit justify-between">
|
||||||
@@ -22,7 +21,7 @@ export const AdminUsersTable = (props: AdminUsersTableProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-5">
|
<div className="mt-5">
|
||||||
<DataTableWithPagination
|
<DataTableWithPagination
|
||||||
columns={usersColumns}
|
columns={usersColumnsAdmin}
|
||||||
data={users}
|
data={users}
|
||||||
DataTable={UsersDataTable}
|
DataTable={UsersDataTable}
|
||||||
dataTableProps={{currentUser}}
|
dataTableProps={{currentUser}}
|
||||||
|
|||||||
+4
-4
@@ -12,7 +12,7 @@ import {Trash2} from "lucide-react";
|
|||||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
|
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/delete-account.action";
|
||||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||||
|
|
||||||
export const usersColumns: ColumnDef<User>[] = [
|
export const usersColumnsAdmin: ColumnDef<User>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "role",
|
accessorKey: "role",
|
||||||
header: "Role",
|
header: "Role",
|
||||||
@@ -55,10 +55,10 @@ export const usersColumns: ColumnDef<User>[] = [
|
|||||||
header: "Email"
|
header: "Email"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "createdAt",
|
accessorKey: "updatedAt",
|
||||||
header: "Created At",
|
header: "Updated At",
|
||||||
cell: ({row}) => {
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+14
@@ -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 {z} from "zod";
|
||||||
import {v4 as uuidv4} from "uuid";
|
import {v4 as uuidv4} from "uuid";
|
||||||
import {db} from "@/db";
|
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) => {
|
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
|
export const deleteOrganizationAction = userAction
|
||||||
.schema(z.string())
|
.schema(z.string())
|
||||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
|
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<Organization>> => {
|
||||||
|
|||||||
@@ -4,16 +4,17 @@ import {Card, CardContent, CardHeader} from "@/components/ui/card";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export type projectCardProps = {
|
export type projectCardProps = {
|
||||||
data: any
|
data: any,
|
||||||
|
organizationSlug?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectCard = (props: projectCardProps) => {
|
export const ProjectCard = (props: projectCardProps) => {
|
||||||
|
|
||||||
const {data: project} = props;
|
const {data: project, organizationSlug} = props;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/dashboard/projects/${project.id}`}>
|
<Link href={`/dashboard/${organizationSlug}/projects/${project.id}`}>
|
||||||
<Card className="flex flex-row justify-between">
|
<Card className="flex flex-row justify-between">
|
||||||
<div className="">
|
<div className="">
|
||||||
<CardHeader>{project.name}</CardHeader>
|
<CardHeader>{project.name}</CardHeader>
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ import {formatDateLastContact} from "@/utils/date-formatting";
|
|||||||
export type projectDatabaseCardProps = {
|
export type projectDatabaseCardProps = {
|
||||||
data: Database,
|
data: Database,
|
||||||
extendedProps: any
|
extendedProps: any
|
||||||
|
organizationSlug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
export const ProjectDatabaseCard = (props: projectDatabaseCardProps) => {
|
||||||
|
|
||||||
const {data: database, extendedProps: extendedProps} = props;
|
const {organizationSlug,data: database, extendedProps: extendedProps} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/dashboard/projects/${extendedProps.id}/database/${database.id}`}>
|
<Link href={`/dashboard/${organizationSlug}/projects/${extendedProps.id}/database/${database.id}`}>
|
||||||
<DatabaseCard data={database}/>
|
<DatabaseCard data={database}/>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export const ProjectForm = (props: projectFormProps) => {
|
|||||||
|
|
||||||
if (project.data.success) {
|
if (project.data.success) {
|
||||||
toast.success(project.data.actionSuccess.message);
|
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()
|
router.refresh()
|
||||||
} else {
|
} else {
|
||||||
toast.success(project.data.actionError.message);
|
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 {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs";
|
||||||
import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination";
|
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 {User, Settings} from "@prisma/client";
|
||||||
import {backupColumns} from "@/features/backup/columns";
|
import {backupColumns} from "@/features/backup/columns";
|
||||||
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/AdminEmailTab/SettingsEmailTab";
|
import {SettingsEmailTab} from "@/components/wrappers/dashboard/admin/AdminEmailTab/SettingsEmailTab";
|
||||||
@@ -21,34 +21,6 @@ export const SettingsTabs = (props: SettingsTabsProps) => {
|
|||||||
const {currentUser, users} = props;
|
const {currentUser, users} = props;
|
||||||
|
|
||||||
return (
|
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}/>
|
<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 {User} from "@prisma/client";
|
||||||
import {DataTableWithPagination} from "@/components/wrappers/common/table/data-table-with-pagination";
|
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";
|
import {UsersDataTable} from "@/components/wrappers/dashboard/admin/admin-user-table";
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+64
@@ -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");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user