mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
fix: Upload with new storage backend system.
This commit is contained in:
@@ -1,75 +0,0 @@
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {Unlink} from "lucide-react";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {unlinkUserProviderAction} from "@/components/wrappers/dashboard/profile2/user-form/user-form.action";
|
||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||
import {Account} from "better-auth";
|
||||
|
||||
|
||||
export const accountsColumns: ColumnDef<Account>[] = [
|
||||
{
|
||||
id: "provider",
|
||||
header: "Provider",
|
||||
cell: ({row}) => {
|
||||
return (
|
||||
<div>
|
||||
{providerSwitch(row.original.providerId)}
|
||||
</div>
|
||||
|
||||
)
|
||||
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Action",
|
||||
id: "actions",
|
||||
cell: ({row, table}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (row.original.providerId === "credential") {
|
||||
toast.error(`This provider cannot be unlinked.`);
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
if (table.getRowModel().rows.length <= 1) {
|
||||
toast.error(`You only have one provider linked to your account. Please add more one to unlink this`);
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await unlinkUserProviderAction({
|
||||
provider: row.original.providerId,
|
||||
account: row.original.accountId,
|
||||
});
|
||||
|
||||
if (status?.serverError || !status) {
|
||||
toast.error(status?.serverError);
|
||||
return;
|
||||
}
|
||||
toast.success(`Provider unlinked successfully.`);
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonWithLoading
|
||||
variant="outline"
|
||||
disabled={row.original.providerId === "credential" || table.getRowModel().rows.length <= 1}
|
||||
icon={<Unlink color="red" size={15}/>}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
size="icon"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,29 +0,0 @@
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card";
|
||||
import {DataTable} from "@/components/wrappers/common/table/data-table";
|
||||
import {usersColumnsAdmin} from "@/components/wrappers/dashboard/admin/users/columns-users";
|
||||
|
||||
export type AdminUsersTableProps = {
|
||||
users: UserWithAccounts[];
|
||||
|
||||
};
|
||||
|
||||
export const AdminUsersTable = (props: AdminUsersTableProps) => {
|
||||
const {users} = props;
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4 h-full py-4">
|
||||
<Card className="h-full ">
|
||||
<CardHeader>
|
||||
<CardTitle>Active users</CardTitle>
|
||||
<CardDescription>Manage your users</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-full">
|
||||
<DataTable
|
||||
enableSelect={false}
|
||||
columns={usersColumnsAdmin}
|
||||
data={users}/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,61 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {Trash2} from "lucide-react";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {toast} from "sonner";
|
||||
import {deleteUserAction} from "@/components/wrappers/dashboard/profile2/button-delete-account/delete-account.action";
|
||||
|
||||
export type ButtonDeleteUserProps = {
|
||||
userId: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const ButtonDeleteUser = (props: ButtonDeleteUserProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteUserAction(props.userId),
|
||||
onSuccess: async () => {
|
||||
toast.success("User deleted successfully.");
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<ButtonWithConfirm
|
||||
title={""}
|
||||
|
||||
description="Are you sure you want to remove this user? This action cannot be undone."
|
||||
button={{
|
||||
main: {
|
||||
disabled: !!props.disabled,
|
||||
text: "",
|
||||
variant: "outline",
|
||||
size: "sm",
|
||||
icon: <Trash2 color="red" size={15}/>,
|
||||
},
|
||||
confirm: {
|
||||
className: "w-full",
|
||||
text: "Delete",
|
||||
icon: <Trash2/>,
|
||||
variant: "destructive",
|
||||
onClick: () => {
|
||||
mutation.mutate()
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
className: "w-full",
|
||||
text: "Cancel",
|
||||
icon: <Trash2/>,
|
||||
variant: "outline",
|
||||
},
|
||||
}}
|
||||
isPending={mutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,108 +0,0 @@
|
||||
"use client"
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {Badge} from "@/components/ui/badge";
|
||||
import {updateUserAction} from "@/components/wrappers/dashboard/profile2/user-form/user-form.action";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {useState} from "react";
|
||||
import {UserWithAccounts} from "@/db/schema/02_user";
|
||||
import {authClient, useSession} from "@/lib/auth/auth-client";
|
||||
import {providerSwitch} from "@/components/wrappers/common/provider-switch";
|
||||
import {ButtonDeleteUser} from "@/components/wrappers/dashboard/admin/users/button-delete-use";
|
||||
import {formatLocalizedDate} from "@/utils/date-formatting";
|
||||
|
||||
export const usersColumnsAdmin: ColumnDef<UserWithAccounts>[] = [
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: "Role",
|
||||
cell: ({row}) => {
|
||||
const [role, setRole] = useState<string>(row.getValue("role"));
|
||||
|
||||
const {data: session, isPending, error} = authClient.useSession();
|
||||
const isSuperAdmin = session?.user.role == "superadmin";
|
||||
const isCurrentUser = session?.user.email === row.original.email;
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: () => updateUserAction({id: row.original.id, data: {role: role}}),
|
||||
onSuccess: () => {
|
||||
toast.success(`User updated successfully.`);
|
||||
},
|
||||
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();
|
||||
};
|
||||
|
||||
|
||||
if (isPending) return null;
|
||||
|
||||
if (error || !session) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Badge
|
||||
className={isCurrentUser || !isSuperAdmin ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
|
||||
onClick={isCurrentUser || !isSuperAdmin ? undefined : () => handleUpdateRole()}
|
||||
variant="outline"
|
||||
>
|
||||
{role}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: "Name",
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: "Email",
|
||||
},
|
||||
{
|
||||
accessorKey: "accounts",
|
||||
header: "Provider(s)",
|
||||
cell: ({row}) => {
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
{row.original.accounts.map((item) => (
|
||||
<div key={item.id}>
|
||||
{providerSwitch(item.providerId, true)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "updatedAt",
|
||||
header: "Updated At",
|
||||
cell: ({row}) => {
|
||||
return formatLocalizedDate(row.getValue("updatedAt"))
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Action",
|
||||
id: "actions",
|
||||
cell: ({row}) => {
|
||||
const router = useRouter();
|
||||
const {data: session, isPending, error} = useSession();
|
||||
const isSuperAdmin = session?.user.role == "superadmin";
|
||||
|
||||
if (isPending || error) return null;
|
||||
|
||||
return (
|
||||
<ButtonDeleteUser
|
||||
disabled={!isSuperAdmin || !session || session?.user.email === row.original.email}
|
||||
userId={row.original.id}/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,89 +0,0 @@
|
||||
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {ColumnDef} from "@tanstack/react-table";
|
||||
import {Session} from "better-auth";
|
||||
import {Unlink} from "lucide-react";
|
||||
import {toast} from "sonner";
|
||||
import {useRouter} from "next/navigation";
|
||||
import detectOSWithUA from "@/utils/os-parser";
|
||||
import {Icon} from "@iconify/react";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {timeAgo} from "@/utils/date-formatting";
|
||||
import {deleteUserSessionAction} from "@/components/wrappers/dashboard/profile2/user-form/user-form.action";
|
||||
|
||||
export const sessionsColumns: ColumnDef<Session>[] = [
|
||||
{
|
||||
accessorKey: "expiresAt",
|
||||
header: "Expires At",
|
||||
cell: ({row}) => {
|
||||
return timeAgo(row.original.expiresAt);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "device",
|
||||
header: "Device",
|
||||
cell: ({row}) => {
|
||||
const os = detectOSWithUA(row.original.userAgent!);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-x-2 items-center pt-4 pb-4">
|
||||
{os.icon &&
|
||||
<Icon icon={`logos:${os.icon.name}`} height={os.icon.size.height} width={os.icon.size.width}/>}
|
||||
{os.showText && <span>{os.name}</span>}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "userAgent",
|
||||
header: "User Agent",
|
||||
},
|
||||
{
|
||||
header: "Action",
|
||||
id: "actions",
|
||||
cell: ({row}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const {data: session, isPending, error} = authClient.useSession();
|
||||
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (session?.session.id === row.original.id) {
|
||||
toast.error(`Unable to unlink active session.`);
|
||||
router.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
const status = await deleteUserSessionAction(row.original.token);
|
||||
|
||||
if (status?.serverError || !status) {
|
||||
toast.error(status?.serverError);
|
||||
return;
|
||||
}
|
||||
toast.success(`Session deleted successfully.`);
|
||||
router.refresh();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
if (isPending || error) return null;
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<ButtonWithLoading
|
||||
isPending={mutation.isPending}
|
||||
variant="outline"
|
||||
disabled={session?.session.id === row.original.id}
|
||||
icon={<Unlink color="red" size={15}/>}
|
||||
onClick={async () => {
|
||||
await mutation.mutateAsync();
|
||||
}}
|
||||
size="icon"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -10,6 +10,7 @@ import {eq} from "drizzle-orm";
|
||||
import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
|
||||
import {z} from "zod";
|
||||
import {env} from "@/env.mjs";
|
||||
import {storeBackupFiles} from "@/features/storages/helpers";
|
||||
|
||||
|
||||
export const uploadBackupAction = userAction
|
||||
@@ -23,7 +24,8 @@ export const uploadBackupAction = userAction
|
||||
where: eq(drizzleDb.schemas.database.id, databaseId),
|
||||
with: {
|
||||
project: true,
|
||||
alertPolicies: true
|
||||
alertPolicies: true,
|
||||
storagePolicies: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -42,55 +44,56 @@ export const uploadBackupAction = userAction
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
|
||||
const fileSize = file.size;
|
||||
const uuid = uuidv4();
|
||||
const fileName = `imported_${uuid}${fileExtension}`;
|
||||
const fileName = `${uuid}${fileExtension}`;
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
|
||||
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
|
||||
if (!settings) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "Settings not set",
|
||||
status: 500,
|
||||
cause: "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let success: boolean, message: string, filePath: string;
|
||||
|
||||
const result =
|
||||
settings.storage === "local"
|
||||
? await uploadLocalPrivate(fileName, buffer)
|
||||
: await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
|
||||
|
||||
({success, message, filePath} = result);
|
||||
|
||||
if (!success) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "An error has occurred while uploading file",
|
||||
status: 500,
|
||||
cause: "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
// const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
|
||||
//
|
||||
// if (!settings) {
|
||||
// return {
|
||||
// success: false,
|
||||
// actionError: {
|
||||
// message: "Settings not set",
|
||||
// status: 500,
|
||||
// cause: "Unknown error",
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
const [backup] = await db
|
||||
.insert(drizzleDb.schemas.backup)
|
||||
.values({
|
||||
status: 'success',
|
||||
imported: true,
|
||||
status: 'ongoing',
|
||||
databaseId: database.id,
|
||||
file: fileName,
|
||||
fileSize: fileSize,
|
||||
})
|
||||
.returning();
|
||||
|
||||
|
||||
await storeBackupFiles(backup, database, buffer, fileName)
|
||||
|
||||
|
||||
// let success: boolean, message: string, filePath: string;
|
||||
//
|
||||
// const result =
|
||||
// settings.storage === "local"
|
||||
// ? await uploadLocalPrivate(fileName, buffer)
|
||||
// : await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
|
||||
//
|
||||
// ({success, message, filePath} = result);
|
||||
//
|
||||
// if (!success) {
|
||||
// return {
|
||||
// success: false,
|
||||
// actionError: {
|
||||
// message: "An error has occurred while uploading file",
|
||||
// status: 500,
|
||||
// cause: "Unknown error",
|
||||
// },
|
||||
// };
|
||||
// }
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: backup,
|
||||
|
||||
-61
@@ -1,61 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {useMutation} from "@tanstack/react-query";
|
||||
import {Trash2} from "lucide-react";
|
||||
import {ButtonWithConfirm} from "@/components/wrappers/common/button/button-with-confirm";
|
||||
import {signOut} from "@/lib/auth/auth-client";
|
||||
import {useRouter} from "next/navigation";
|
||||
import {deleteUserAction} from "./delete-account.action";
|
||||
|
||||
export type ButtonDeleteAccountProps = {
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export const ButtonDeleteAccount = (props: ButtonDeleteAccountProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: () => deleteUserAction(""),
|
||||
onSuccess: async () => {
|
||||
await signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/login");
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ButtonWithConfirm
|
||||
title={props.text ? props.text : ""}
|
||||
description="Are you sure you want to delete your account ? This action cannot be undone."
|
||||
button={{
|
||||
main: {
|
||||
text: props.text ? props.text : "",
|
||||
variant: "outline",
|
||||
icon: <Trash2 color="red"/>,
|
||||
},
|
||||
confirm: {
|
||||
className: "w-full",
|
||||
text: "Delete",
|
||||
icon: <Trash2/>,
|
||||
variant: "destructive",
|
||||
onClick: () => {
|
||||
mutation.mutate();
|
||||
},
|
||||
},
|
||||
cancel: {
|
||||
className: "w-full",
|
||||
text: "Cancel",
|
||||
icon: <Trash2/>,
|
||||
variant: "outline",
|
||||
},
|
||||
}}
|
||||
isPending={mutation.isPending}
|
||||
/>
|
||||
|
||||
|
||||
);
|
||||
};
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
"use server";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import { z } from "zod";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { db } from "@/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
|
||||
|
||||
export const deleteUserAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }) => {
|
||||
const userId = parsedInput.length > 0 ? parsedInput : ctx.user.id;
|
||||
const uuid = uuidv4();
|
||||
|
||||
|
||||
// const [updatedUser] = await db
|
||||
// .update(drizzleDb.schemas.user)
|
||||
// .set({
|
||||
// email: `${uuid}@portabase.com`,
|
||||
// name: `${uuid}`,
|
||||
// //deleted: true,
|
||||
// //todo: add deleted
|
||||
// })
|
||||
// .where(eq(drizzleDb.schemas.user.id, userId))
|
||||
// .returning();
|
||||
const [deletedUser] = await db
|
||||
.delete(drizzleDb.schemas.user)
|
||||
.where(eq(drizzleDb.schemas.user.id, userId))
|
||||
.returning();
|
||||
|
||||
|
||||
return {
|
||||
data: deletedUser,
|
||||
};
|
||||
});
|
||||
@@ -1,43 +0,0 @@
|
||||
"use server";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import { z } from "zod";
|
||||
import { UserSchema } from "@/components/wrappers/dashboard/profile2/user-form/user-form.schema";
|
||||
import { db } from "@/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import * as drizzleDb from "@/db";
|
||||
import {revokeSession, unlinkAccount} from "@/lib/auth/auth";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
|
||||
export const updateUserAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
data: UserSchema,
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt(parsedInput.data)).where(eq(drizzleDb.schemas.user.id, parsedInput.id)).returning();
|
||||
return {
|
||||
data: updatedUser,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
export const deleteUserSessionAction = userAction.schema(z.string()).action(async ({ parsedInput }) => {
|
||||
const status = await revokeSession(parsedInput);
|
||||
return status;
|
||||
});
|
||||
|
||||
|
||||
export const unlinkUserProviderAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
provider: z.string(),
|
||||
account: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({ parsedInput }) => {
|
||||
const status = await unlinkAccount(parsedInput.provider, parsedInput.account);
|
||||
|
||||
return status;
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import {z} from "zod";
|
||||
|
||||
export const UserSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
});
|
||||
|
||||
export type UserType = z.infer<typeof UserSchema>;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "backups" ADD COLUMN "imported" boolean DEFAULT false;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -190,6 +190,13 @@
|
||||
"when": 1768665081232,
|
||||
"tag": "0026_demonic_santa_claus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 27,
|
||||
"version": "7",
|
||||
"when": 1768676733859,
|
||||
"tag": "0027_special_the_santerians",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -41,6 +41,7 @@ export const backup = pgTable(
|
||||
databaseId: uuid("database_id")
|
||||
.notNull()
|
||||
.references(() => database.id, {onDelete: "cascade"}),
|
||||
imported: boolean('imported').default(false),
|
||||
...timestamps
|
||||
},
|
||||
);
|
||||
|
||||
@@ -66,9 +66,8 @@ export function backupColumns(
|
||||
accessorKey: "id",
|
||||
header: "Reference",
|
||||
cell: ({row}) => {
|
||||
const fileName = row.original.file
|
||||
const reference = row.original.id
|
||||
const isImported = isImportedFilename(`${fileName}`)
|
||||
const isImported = row.original.imported
|
||||
return isImported ? `${reference} (imported)` : `${reference}`
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user