Skip to content

Commit 1975da0

Browse files
author
teable-bot
committed
[sync] feat(t1051): improve field delete confirmation with semantic prompts (#1100)
Synced from teableio/teable-ee@58c1d37
1 parent 54af851 commit 1975da0

File tree

32 files changed

+793
-928
lines changed

32 files changed

+793
-928
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
</div>
1616

1717
<p align="center">
18-
<a target="_blank" href="https://teable.ai">Home</a> | <a target="_blank" href="https://help.teable.ai">Help</a> | <a target="_blank" href="https://teable.ai/blog">Blog</a> | <a target="_blank" href="https://teable.ai/templates">Template</a> | <a target="_blank" href="https://help.teable.ai/en/api-doc/token">API</a> | <a target="_blank" href="https://discord.gg/uZwp7tDE5W">Discord</a> | <a target="_blank" href="https://twitter.com/teableio">Twitter</a>
18+
<a target="_blank" href="https://teable.ai">Home</a> | <a target="_blank" href="https://help.teable.ai">Help</a> | <a target="_blank" href="https://blog.teable.ai">Blog</a> | <a target="_blank" href="https://app.teable.ai/public/template">Template</a> | <a target="_blank" href="https://help.teable.ai/en/api-doc/token">API</a> | <a target="_blank" href="https://app.teable.ai/share/shr04TEw1u9EOQojPmG/view">Roadmap</a> | <a target="_blank" href="https://discord.gg/uZwp7tDE5W">Discord</a> | <a target="_blank" href="https://twitter.com/teableio">Twitter</a>
1919
</p>
2020

2121
<p align="center">

apps/nestjs-backend/src/features/record/open-api/record-open-api.controller.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import {
4343
IRecordInsertOrderRo,
4444
recordGetCollaboratorsRoSchema,
4545
IRecordGetCollaboratorsRo,
46+
formSubmitRoSchema,
47+
IFormSubmitRo,
4648
} from '@teable/openapi';
4749
import { ClsService } from 'nestjs-cls';
4850
import { EmitControllerEvent } from '../../../event-emitter/decorators/emit-controller-event.decorator';
@@ -188,6 +190,15 @@ export class RecordOpenApiController {
188190
);
189191
}
190192

193+
@Permissions('record|create')
194+
@Post('form-submit')
195+
async formSubmit(
196+
@Param('tableId') tableId: string,
197+
@Body(new ZodValidationPipe(formSubmitRoSchema)) formSubmitRo: IFormSubmitRo
198+
): Promise<IRecord> {
199+
return await this.recordOpenApiService.formSubmit(tableId, formSubmitRo);
200+
}
201+
191202
@Permissions('record|create', 'record|read')
192203
@Post(':recordId/duplicate')
193204
@EmitControllerEvent(Events.OPERATION_RECORDS_CREATE)

apps/nestjs-backend/src/features/record/open-api/record-open-api.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { AttachmentsModule } from '../../attachments/attachments.module';
44
import { CalculationModule } from '../../calculation/calculation.module';
55
import { CollaboratorModule } from '../../collaborator/collaborator.module';
66
import { FieldCalculateModule } from '../../field/field-calculate/field-calculate.module';
7+
import { FieldModule } from '../../field/field.module';
78
import { TableDomainQueryModule } from '../../table-domain';
89
import { ViewOpenApiModule } from '../../view/open-api/view-open-api.module';
910
import { ViewModule } from '../../view/view.module';
@@ -17,6 +18,7 @@ import { RecordOpenApiService } from './record-open-api.service';
1718
RecordModule,
1819
RecordModifyModule,
1920
FieldCalculateModule,
21+
FieldModule,
2022
CalculationModule,
2123
AttachmentsStorageModule,
2224
AttachmentsModule,

apps/nestjs-backend/src/features/record/open-api/record-open-api.service.ts

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
IButtonFieldOptions,
88
IMakeOptional,
99
} from '@teable/core';
10-
import { FieldKeyType, FieldType, HttpErrorCode } from '@teable/core';
10+
import { FieldKeyType, FieldType, HttpErrorCode, ViewType } from '@teable/core';
1111
import { PrismaService } from '@teable/db-main-prisma';
1212
import {
1313
CreateRecordAction,
@@ -18,20 +18,24 @@ import {
1818
import type {
1919
IRecordHistoryItemVo,
2020
ICreateRecordsVo,
21+
IFormSubmitRo,
2122
IGetRecordHistoryQuery,
2223
IRecord,
2324
IRecordHistoryVo,
2425
IRecordInsertOrderRo,
2526
IUpdateRecordRo,
2627
} from '@teable/openapi';
27-
import { keyBy, pick } from 'lodash';
28+
import { isEmpty, keyBy, pick } from 'lodash';
2829
import { ClsService } from 'nestjs-cls';
2930
import { IThresholdConfig, ThresholdConfig } from '../../../configs/threshold.config';
3031
import { CustomHttpException } from '../../../custom.exception';
32+
import { EventEmitterService } from '../../../event-emitter/event-emitter.service';
33+
import { Events } from '../../../event-emitter/events';
3134
import type { IClsStore } from '../../../types/cls';
3235
import { retryOnDeadlock } from '../../../utils/retry-decorator';
3336
import { AttachmentsService } from '../../attachments/attachments.service';
3437
import { getPublicFullStorageUrl } from '../../attachments/plugins/utils';
38+
import { FieldService } from '../../field/field.service';
3539
import { createFieldInstanceByRaw } from '../../field/model/factory';
3640
import { TableDomainQueryService } from '../../table-domain';
3741
import { RecordModifyService } from '../record-modify/record-modify.service';
@@ -50,7 +54,9 @@ export class RecordOpenApiService {
5054
@ThresholdConfig() private readonly thresholdConfig: IThresholdConfig,
5155
private readonly recordModifySharedService: RecordModifySharedService,
5256
private readonly tableDomainQueryService: TableDomainQueryService,
53-
private readonly cls: ClsService<IClsStore>
57+
private readonly fieldService: FieldService,
58+
private readonly cls: ClsService<IClsStore>,
59+
private readonly eventEmitterService: EventEmitterService
5460
) {}
5561

5662
@retryOnDeadlock()
@@ -535,4 +541,92 @@ export class RecordOpenApiService {
535541
ignoreMissingFields
536542
);
537543
}
544+
545+
async formSubmit(
546+
tableId: string,
547+
formSubmitRo: IFormSubmitRo,
548+
options?: { includeHiddenField?: boolean }
549+
): Promise<IRecord> {
550+
const { viewId, fields, typecast } = formSubmitRo;
551+
const { includeHiddenField = false } = options ?? {};
552+
553+
// 1. Validate view exists and is Form type
554+
await this.prismaService.view
555+
.findFirstOrThrow({
556+
where: { id: viewId, tableId, deletedTime: null, type: ViewType.Form },
557+
})
558+
.catch(() => {
559+
throw new CustomHttpException('View is not a form', HttpErrorCode.RESTRICTED_RESOURCE, {
560+
localization: {
561+
i18nKey: 'httpErrors.share.viewTypeNotAllowed',
562+
},
563+
});
564+
});
565+
566+
// 2. Check field visibility - only allow submission of visible fields
567+
const visibleFields = await this.fieldService.getFieldsByQuery(tableId, {
568+
viewId,
569+
filterHidden: !includeHiddenField,
570+
});
571+
const visibleFieldIdSet = new Set(visibleFields.map(({ id }) => id));
572+
573+
if (
574+
(!visibleFields.length && !isEmpty(fields)) ||
575+
Object.keys(fields).some((fieldId) => !visibleFieldIdSet.has(fieldId))
576+
) {
577+
throw new CustomHttpException(
578+
'The form contains hidden fields, submission not allowed.',
579+
HttpErrorCode.RESTRICTED_RESOURCE,
580+
{
581+
localization: {
582+
i18nKey: 'httpErrors.share.hiddenFieldsSubmissionNotAllowed',
583+
},
584+
}
585+
);
586+
}
587+
588+
// 3. Create record with form entry context
589+
const { records } = await this.prismaService.$tx(async () => {
590+
this.cls.set('entry', { type: 'form', id: viewId });
591+
this.cls.set('skipRecordAuditLog', true);
592+
return this.createRecords(tableId, {
593+
records: [{ fields }],
594+
fieldKeyType: FieldKeyType.Id,
595+
typecast,
596+
});
597+
});
598+
599+
// 4. Emit form audit log
600+
await this.emitFormAuditLog(tableId, records.length);
601+
602+
// 5. Validate record creation
603+
if (records.length === 0) {
604+
throw new CustomHttpException(
605+
'The number of successful submit records is 0',
606+
HttpErrorCode.INTERNAL_SERVER_ERROR,
607+
{
608+
localization: {
609+
i18nKey: 'httpErrors.share.submitRecordsError',
610+
},
611+
}
612+
);
613+
}
614+
615+
return records[0];
616+
}
617+
618+
private async emitFormAuditLog(tableId: string, length: number) {
619+
const userId = this.cls.get('user.id');
620+
const origin = this.cls.get('origin');
621+
622+
await this.cls.run(async () => {
623+
this.cls.set('user.id', userId);
624+
this.cls.set('origin', origin!);
625+
await this.eventEmitterService.emitAsync(Events.TABLE_RECORD_CREATE_RELATIVE, {
626+
action: CreateRecordAction.FormSubmit,
627+
resourceId: tableId,
628+
recordCount: length,
629+
});
630+
});
631+
}
538632
}

apps/nestjs-backend/src/features/share/share.service.ts

Lines changed: 7 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
33
import type { IFilter, IFieldVo, IViewVo, ILinkFieldOptions, StatisticsFunc } from '@teable/core';
44
import { CellFormat, FieldKeyType, FieldType, HttpErrorCode, ViewType } from '@teable/core';
55
import { PrismaService } from '@teable/db-main-prisma';
6-
import { ShareViewLinkRecordsType, PluginPosition, CreateRecordAction } from '@teable/openapi';
6+
import { ShareViewLinkRecordsType, PluginPosition } from '@teable/openapi';
77
import type {
88
IShareViewCalendarDailyCollectionRo,
99
ShareViewFormSubmitRo,
@@ -22,14 +22,11 @@ import type {
2222
ISearchIndexByQueryRo,
2323
} from '@teable/openapi';
2424
import { Knex } from 'knex';
25-
import { isEmpty } from 'lodash';
2625
import { InjectModel } from 'nest-knexjs';
2726
import { ClsService } from 'nestjs-cls';
2827
import { CustomHttpException } from '../../custom.exception';
2928
import { InjectDbProvider } from '../../db-provider/db.provider';
3029
import { IDbProvider } from '../../db-provider/db.provider.interface';
31-
import { EventEmitterService } from '../../event-emitter/event-emitter.service';
32-
import { Events } from '../../event-emitter/events';
3330
import type { IClsStore } from '../../types/cls';
3431
import { convertViewVoAttachmentUrl } from '../../utils/convert-view-vo-attachment-url';
3532
import { isNotHiddenField } from '../../utils/is-not-hidden-field';
@@ -64,8 +61,7 @@ export class ShareService {
6461
private readonly shareSocketService: ShareSocketService,
6562
private readonly cls: ClsService<IClsStore>,
6663
@InjectDbProvider() private readonly dbProvider: IDbProvider,
67-
@InjectModel('CUSTOM_KNEX') private readonly knex: Knex,
68-
private readonly eventEmitterService: EventEmitterService
64+
@InjectModel('CUSTOM_KNEX') private readonly knex: Knex
6965
) {}
7066

7167
async getShareView(shareInfo: IShareViewInfo): Promise<ShareViewGetVo> {
@@ -232,53 +228,11 @@ export class ShareService {
232228
});
233229
}
234230

235-
const viewId = view.id;
236-
237-
// check field hidden
238-
const visibleFields = await this.fieldService.getFieldsByQuery(tableId, {
239-
viewId,
240-
filterHidden: !view.shareMeta?.includeHiddenField,
241-
});
242-
const visibleFieldIds = visibleFields.map(({ id }) => id);
243-
const visibleFieldIdSet = new Set(visibleFieldIds);
244-
245-
if (
246-
(!visibleFields.length && !isEmpty(fields)) ||
247-
Object.keys(fields).some((fieldId) => !visibleFieldIdSet.has(fieldId))
248-
) {
249-
throw new CustomHttpException(
250-
'The form contains hidden fields, submission not allowed.',
251-
HttpErrorCode.RESTRICTED_RESOURCE,
252-
{
253-
localization: {
254-
i18nKey: 'httpErrors.share.hiddenFieldsSubmissionNotAllowed',
255-
},
256-
}
257-
);
258-
}
259-
260-
const { records } = await this.prismaService.$tx(async () => {
261-
this.cls.set('entry', { type: 'form', id: viewId });
262-
this.cls.set('skipRecordAuditLog', true);
263-
return this.recordOpenApiService.createRecords(tableId, {
264-
records: [{ fields }],
265-
fieldKeyType: FieldKeyType.Id,
266-
typecast,
267-
});
268-
});
269-
await this.emitFormAuditLog(tableId, records.length);
270-
if (records.length === 0) {
271-
throw new CustomHttpException(
272-
'The number of successful submit records is 0',
273-
HttpErrorCode.INTERNAL_SERVER_ERROR,
274-
{
275-
localization: {
276-
i18nKey: 'httpErrors.share.submitRecordsError',
277-
},
278-
}
279-
);
280-
}
281-
return records[0];
231+
return this.recordOpenApiService.formSubmit(
232+
tableId,
233+
{ viewId: view.id, fields, typecast },
234+
{ includeHiddenField: view.shareMeta?.includeHiddenField }
235+
);
282236
}
283237

284238
async copy(shareInfo: IShareViewInfo, shareViewCopyRo: IRangesRo) {
@@ -612,19 +566,4 @@ export class ShareService {
612566
await this.shareSocketService.validRecordSnapshotPermission(shareInfo, [recordId]);
613567
return this.recordOpenApiService.buttonClick(shareInfo.tableId, recordId, fieldId);
614568
}
615-
616-
async emitFormAuditLog(tableId: string, length: number) {
617-
const userId = this.cls.get('user.id');
618-
const origin = this.cls.get('origin');
619-
620-
await this.cls.run(async () => {
621-
this.cls.set('user.id', userId);
622-
this.cls.set('origin', origin!);
623-
await this.eventEmitterService.emitAsync(Events.TABLE_RECORD_CREATE_RELATIVE, {
624-
action: CreateRecordAction.FormSubmit,
625-
resourceId: tableId,
626-
recordCount: length,
627-
});
628-
});
629-
}
630569
}

apps/nestjs-backend/src/types/i18n.generated.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3632,7 +3632,7 @@ export type I18nTranslations = {
36323632
"reset": string;
36333633
"fieldUpdated": string;
36343634
"fieldCreated": string;
3635-
"previewDependenciesGraph": string;
3635+
"confirmFieldChange": string;
36363636
"areYouSurePerformIt": string;
36373637
"addDescription": string;
36383638
"dbFieldName": string;
@@ -3719,6 +3719,16 @@ export type I18nTranslations = {
37193719
"selectBaseField": string;
37203720
"noMappings": string;
37213721
};
3722+
"deleteField": {
3723+
"title": string;
3724+
"simpleConfirm": string;
3725+
"withDependencies": string;
3726+
"affectedFields": string;
3727+
"fieldsToDelete": string;
3728+
"unviewedHint": string;
3729+
"deleteCount": string;
3730+
"noAffectedFields": string;
3731+
};
37223732
};
37233733
"subTitle": {
37243734
"link": string;

apps/nextjs-app/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@
9393
"vitest": "4.0.17"
9494
},
9595
"dependencies": {
96-
"@antv/g6": "4.8.24",
9796
"@asteasolutions/zod-to-openapi": "8.1.0",
9897
"@belgattitude/http-exception": "1.5.0",
9998
"@codemirror/autocomplete": "6.15.0",

apps/nextjs-app/src/features/app/blocks/design/data-table/useDataColumns.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@teabl
55
import { useTranslation } from 'next-i18next';
66
import { Actions } from '../components/Actions';
77
import { FieldPropertyEditor } from '../components/FieldPropertyEditor';
8-
import { FieldGraph } from './FieldGraph';
98

109
function checkBox(key: string) {
1110
return {
@@ -75,10 +74,6 @@ export function useDataColumns() {
7574
</TooltipProvider>
7675
),
7776
},
78-
{
79-
header: 'graph',
80-
cell: ({ row }) => <FieldGraph fieldId={row.getValue('id')} />,
81-
},
8277
{
8378
accessorKey: 'dbFieldType',
8479
header: 'dbFieldType',

0 commit comments

Comments
 (0)