Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement duplicate row functionality #930

Merged
merged 2 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
IGetRecordHistoryQuery,
updateRecordsRoSchema,
IUpdateRecordsRo,
recordInsertOrderRoSchema,
IRecordInsertOrderRo,
} from '@teable/openapi';
import { EmitControllerEvent } from '../../../event-emitter/decorators/emit-controller-event.decorator';
import { Events } from '../../../event-emitter/events';
Expand Down Expand Up @@ -139,6 +141,16 @@ export class RecordOpenApiController {
return await this.recordOpenApiService.multipleCreateRecords(tableId, createRecordsRo);
}

@Permissions('record|create')
@Post(':recordId')
async duplicateRecords(
@Param('tableId') tableId: string,
@Param('recordId') recordId: string,
@Body(new ZodValidationPipe(recordInsertOrderRoSchema)) order: IRecordInsertOrderRo
) {
return await this.recordOpenApiService.duplicateRecords(tableId, recordId, order);
}

@Permissions('record|delete')
@Delete(':recordId')
async deleteRecord(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -572,4 +572,19 @@ export class RecordOpenApiService {

return await this.updateRecord(tableId, recordId, updateRecordRo);
}

async duplicateRecords(tableId: string, recordId: string, order: IRecordInsertOrderRo) {
const query = { fieldKeyType: FieldKeyType.Id };
const result = await this.recordService.getRecord(tableId, recordId, query);
const records = { fields: result.fields };
const createRecordsRo = {
fieldKeyType: FieldKeyType.Id,
order,
records: [records],
};
const createdRecords = await this.prismaService.$tx(async () =>
this.createRecords(tableId, createRecordsRo)
);
return { ids: createdRecords.records.map((record) => record.id) };
}
}
26 changes: 26 additions & 0 deletions apps/nestjs-backend/test/record.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
deleteRecord,
deleteRecords,
permanentDeleteTable,
duplicateRecord,
getField,
getRecord,
getRecords,
Expand Down Expand Up @@ -280,6 +281,31 @@ describe('OpenAPI RecordController (e2e)', () => {
],
});
});

it('should duplicate a record', async () => {
const value1 = 'New Record';
const addRecordRes = await createRecords(table.id, {
fieldKeyType: FieldKeyType.Id,
records: [
{
fields: {
[table.fields[0].id]: value1,
},
},
],
});
const addRecord = await getRecord(table.id, addRecordRes.records[0].id, undefined, 200);
expect(addRecord.fields[table.fields[0].id]).toEqual(value1);

const viewId = table.views[0].id;
const duplicateRes = await duplicateRecord(table.id, addRecord.id, {
viewId,
anchorId: addRecord.id,
position: 'after',
});
const record = await getRecord(table.id, duplicateRes.ids[0], undefined, 200);
expect(record.fields[table.fields[0].id]).toEqual(value1);
});
});

describe('validate record value by field validation', () => {
Expand Down
22 changes: 22 additions & 0 deletions apps/nestjs-backend/test/utils/init-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import type {
ITableFullVo,
ICreateSpaceRo,
ICreateBaseRo,
IDuplicateVo,
IRecordInsertOrderRo,
} from '@teable/openapi';
import {
axios,
Expand All @@ -39,6 +41,7 @@ import {
createField as apiCreateField,
deleteField as apiDeleteField,
convertField as apiConvertField,
duplicateRecords as apiDuplicateRecords,
getFields as apiGetFields,
getField as apiGetField,
getViewList as apiGetViewList,
Expand Down Expand Up @@ -314,6 +317,25 @@ export async function getRecords(tableId: string, query?: IGetRecordsRo): Promis
return result.data;
}

export async function duplicateRecord(
tableId: string,
recordId: string,
order: IRecordInsertOrderRo,
expectStatus = 201
) {
try {
const res = await apiDuplicateRecords(tableId, recordId, order);

expect(res.status).toEqual(expectStatus);
return res.data;
} catch (e: unknown) {
if ((e as HttpError).status !== expectStatus) {
throw e;
}
return {} as IDuplicateVo;
}
}

export async function createRecords(
tableId: string,
recordsRo: ICreateRecordsRo,
Expand Down
1 change: 1 addition & 0 deletions packages/common-i18n/src/locales/en/sdk.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
},
"expandRecord": {
"copy": "Copy to clipboard",
"duplicateRecord": "Duplicate record",
"copyRecordUrl": "Copy record URL",
"deleteRecord": "Delete record",
"recordHistory": {
Expand Down
1 change: 1 addition & 0 deletions packages/common-i18n/src/locales/zh/sdk.json
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
},
"expandRecord": {
"copy": "复制到剪贴板",
"duplicateRecord": "复制记录",
"copyRecordUrl": "复制记录链接",
"deleteRecord": "删除记录",
"recordHistory": {
Expand Down
50 changes: 50 additions & 0 deletions packages/openapi/src/record/duplicate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { axios } from '../axios';
import { registerRoute, urlBuilder } from '../utils';
import { z } from '../zod';
import type { IRecordInsertOrderRo } from './create';
import { recordInsertOrderRoSchema } from './create';

export const DUPLICATE_URL = '/table/{tableId}/record/{recordId}';

export const duplicateVoSchema = z.object({
ids: z.array(z.string()),
});

export type IDuplicateVo = z.infer<typeof duplicateVoSchema>;
export const duplicateRoute = registerRoute({
method: 'post',
path: DUPLICATE_URL,
description: 'Duplicate the selected data',
request: {
params: z.object({
tableId: z.string(),
recordId: z.string(),
}),
body: {
content: {
'application/json': {
schema: recordInsertOrderRoSchema.optional(),
},
},
},
},
responses: {
201: {
description: 'Successful duplicate',
content: {
'application/json': {
schema: duplicateVoSchema,
},
},
},
},
tags: ['record'],
});

export const duplicateRecords = async (
tableId: string,
recordId: string,
order: IRecordInsertOrderRo
) => {
return axios.post<IDuplicateVo>(urlBuilder(DUPLICATE_URL, { tableId, recordId }), order);
};
1 change: 1 addition & 0 deletions packages/openapi/src/record/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export * from './create';
export * from './update';
export * from './update-records';
export * from './delete';
export * from './duplicate';
export * from './delete-list';
export * from './get-record-history';
export * from './get-record-list-history';
Expand Down
2 changes: 2 additions & 0 deletions packages/sdk/src/components/expand-record/ExpandRecord.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface IExpandRecordProps {
onRecordHistoryToggle?: () => void;
onCommentToggle?: () => void;
onDelete?: () => Promise<void>;
onDuplicate?: () => Promise<void>;
}

export const ExpandRecord = (props: IExpandRecordProps) => {
Expand All @@ -55,6 +56,7 @@ export const ExpandRecord = (props: IExpandRecordProps) => {
onRecordHistoryToggle,
onCommentToggle,
onDelete,
onDuplicate,
} = props;
const views = useViews() as (GridView | undefined)[];
const tableId = useTableId();
Expand Down
12 changes: 12 additions & 0 deletions packages/sdk/src/components/expand-record/ExpandRecordHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
ChevronDown,
ChevronUp,
Copy,
History,
Link,
MoreHorizontal,
Expand Down Expand Up @@ -38,6 +39,7 @@ interface IExpandRecordHeader {
onRecordHistoryToggle?: () => void;
onCommentToggle?: () => void;
onDelete?: () => Promise<void>;
onDuplicate?: () => Promise<void>;
}

// eslint-disable-next-line @typescript-eslint/naming-convention
Expand All @@ -61,6 +63,7 @@ export const ExpandRecordHeader = (props: IExpandRecordHeader) => {
onRecordHistoryToggle,
onCommentToggle,
onDelete,
onDuplicate,
} = props;

const permission = useTablePermission();
Expand Down Expand Up @@ -162,6 +165,15 @@ export const ExpandRecordHeader = (props: IExpandRecordHeader) => {
<MoreHorizontal />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="flex cursor-pointer items-center gap-2 px-4 py-2 text-sm outline-none"
onClick={async () => {
await onDuplicate?.();
onClose?.();
}}
>
<Copy /> {t('expandRecord.duplicateRecord')}
</DropdownMenuItem>
<DropdownMenuItem
className="flex cursor-pointer items-center gap-2 px-4 py-2 text-sm text-red-500 outline-none hover:text-red-500 focus:text-red-500 aria-selected:text-red-500"
onClick={async () => {
Expand Down
13 changes: 12 additions & 1 deletion packages/sdk/src/components/expand-record/ExpandRecorder.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IRecord } from '@teable/core';
import { deleteRecord } from '@teable/openapi';
import { deleteRecord, duplicateRecords } from '@teable/openapi';
import { useToast } from '@teable/ui-lib';
import { useEffect, type FC, type PropsWithChildren } from 'react';
import { useLocalStorage } from 'react-use';
Expand Down Expand Up @@ -47,6 +47,7 @@ export const ExpandRecorder = (props: IExpandRecorderProps) => {
onClose,
onUpdateRecordIdCallback,
commentId,
viewId,
} = props;
const { toast } = useToast();
const { t } = useTranslation();
Expand All @@ -72,6 +73,15 @@ export const ExpandRecorder = (props: IExpandRecorderProps) => {
return <></>;
}

const onDuplicate = async (tableId: string, recordId: string) => {
await duplicateRecords(tableId, recordId, {
viewId: viewId || '',
anchorId: recordId,
position: 'after',
});
toast({ description: t('expandRecord.duplicateRecord') });
};

Comment on lines +76 to +84
Copy link
Contributor

@boris-w boris-w Oct 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a successful duplicate, the current expand card should be updated with the new record, rather than just closing the expand card.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I found it. I know that recordsServerData triggers rendering. When I duplicate record, the length of recordsServerData keep same. I don't know how to update RecordsServerData.

Copy link
Contributor

@boris-w boris-w Oct 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I found it. I know that recordsServerData triggers rendering. When I duplicate record, the length of recordsServerData keep same. I don't know how to update RecordsServerData.

In fact, recordsServerData will not be updated, recordsServerData is only the initialization data needed for ssr, and will not be used when the page is rendered and the socket is connected, so don't care about it!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. Now I have to force refresh the page for the duplicated record to render. I have no cules to solve it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try it.

// source
const createdRecords =await this.createRecords(tableId, createRecordsRo);

// aliternative
const createdRecords = await this.prismaService.$tx(
      async () => {
        return await this.createRecords(tableId, createRecordsRo);
      }   
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you ,it works

const updateCurrentRecordId = (recordId: string) => {
onUpdateRecordIdCallback?.(recordId);
};
Expand Down Expand Up @@ -108,6 +118,7 @@ export const ExpandRecorder = (props: IExpandRecorderProps) => {
onPrev={updateCurrentRecordId}
onNext={updateCurrentRecordId}
onCopyUrl={onCopyUrl}
onDuplicate={async () => await onDuplicate(tableId, recordId)}
onRecordHistoryToggle={onRecordHistoryToggle}
onCommentToggle={onCommentToggle}
onDelete={async () => {
Expand Down
Loading