apps/nextjs-app/src/features/app/blocks/base/duplicate/DuplicateBaseModal.tsx
import { useMutation, useQuery } from '@tanstack/react-query';
import { hasPermission } from '@teable/core';
import { Database } from '@teable/icons';
import { duplicateBase, getSpaceList, type IGetBaseVo } from '@teable/openapi';
import { ReactQueryKeys } from '@teable/sdk/config';
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
Input,
Label,
Switch,
} from '@teable/ui-lib/shadcn';
import { toast } from '@teable/ui-lib/shadcn/ui/sonner';
import { useRouter } from 'next/router';
import { useTranslation } from 'next-i18next';
import { useEffect, useMemo, useState } from 'react';
import { Selector } from '@/components/Selector';
import { Emoji } from '@/features/app/components/emoji/Emoji';
import { spaceConfig } from '@/features/i18n/space.config';
import { useDuplicateBaseStore } from './useDuplicateBaseStore';
const DuplicateBase = ({ base }: { base: IGetBaseVo }) => {
const { closeModal } = useDuplicateBaseStore();
const [withRecords, setWithRecords] = useState(true);
const [targetSpaceId, setTargetSpaceId] = useState<string>();
const router = useRouter();
const { t } = useTranslation(spaceConfig.i18nNamespaces);
const [baseName, setBaseName] = useState(`${base.name} (${t('space:baseModal.copy')})`);
const { data: spaceList } = useQuery({
queryKey: ReactQueryKeys.spaceList(),
queryFn: () => getSpaceList().then((res) => res.data),
});
const { mutateAsync: duplicateBaseMutator } = useMutation({
mutationFn: duplicateBase,
onSuccess: ({ data }) => {
closeModal();
router.push({
pathname: '/base/[baseId]',
query: { baseId: data.id },
});
},
});
const editableSpaceList = useMemo(() => {
return spaceList?.filter((space) => hasPermission(space.role, 'base|create')) || [];
}, [spaceList]);
const onSubmit = () => {
if (!targetSpaceId) {
toast.error(t('space:baseModal.missTargetTip'));
return;
}
toast.message(t('space:baseModal.copying'));
duplicateBaseMutator({
fromBaseId: base.id,
spaceId: targetSpaceId,
name: baseName,
withRecords,
});
};
useEffect(() => {
if (!targetSpaceId && editableSpaceList?.length) {
const currentSpace = editableSpaceList.find((space) => space.id === base.spaceId);
if (currentSpace) {
setTargetSpaceId(currentSpace.id);
} else {
setTargetSpaceId(editableSpaceList[0].id);
}
}
}, [base.spaceId, editableSpaceList, targetSpaceId]);
return (
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>
{t('space:baseModal.duplicate', {
baseName: base.name,
})}
</DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center gap-4 py-4">
{base.icon ? (
<div className="size-14 min-w-14 text-[3.5rem] leading-none">
<Emoji emoji={base.icon} size={56} />
</div>
) : (
<Database className="size-14 min-w-14" />
)}
<div>
<Input value={baseName} onChange={(e) => setBaseName(e.target.value)} />
</div>
</div>
<hr />
<div className="space-y-4">
<div className="flex items-center gap-4">
<Label htmlFor="duplicate-records-mode">{t('space:baseModal.duplicateRecords')}</Label>
<Switch
id="duplicate-records-mode"
checked={withRecords}
onCheckedChange={(v) => setWithRecords(v)}
/>
</div>
<p className="text-xs text-secondary-foreground">
{t('space:baseModal.duplicateRecordsTip')}
</p>
<div className="flex items-center gap-4">
<Label htmlFor="username" className="text-right">
{t('space:baseModal.copyToSpace')}
</Label>
<Selector
candidates={editableSpaceList}
selectedId={targetSpaceId}
onChange={(id) => setTargetSpaceId(id)}
/>
</div>
</div>
<DialogFooter className="mt-4">
<DialogClose asChild>
<Button size="sm" type="button" variant="ghost">
{t('common:actions.cancel')}
</Button>
</DialogClose>
<Button size="sm" type="submit" onClick={() => onSubmit()}>
{t('space:baseModal.duplicateBase')}
</Button>
</DialogFooter>
</DialogContent>
);
};
export const DuplicateBaseModal = () => {
const { base, closeModal } = useDuplicateBaseStore();
return (
<Dialog open={Boolean(base)} onOpenChange={(isOpen) => !isOpen && closeModal()}>
{base && <DuplicateBase base={base} />}
</Dialog>
);
};