Skip to content

Commit

Permalink
feat: limiting linked record selection (#994)
Browse files Browse the repository at this point in the history
* feat: the link field items support click-to-expand in the grid view

* feat: limiting linked record selection

* chore: update link view limit testing

* chore: update unit testing for link field

* feat: support go to foreign table in link editor

* perf: optimize the interaction for configuring the limit options of link fields

* chore: update unit testing

* fix: the selectable year range in the date editor

* perf: cache the state of hidden fields within the expand record

* feat: after the structure of the foreign table changes, update the limit condition in the link field

* chore: update e2e testing
  • Loading branch information
Sky-FE authored Oct 21, 2024
1 parent 307a745 commit 14ea9f7
Show file tree
Hide file tree
Showing 57 changed files with 1,349 additions and 153 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,35 @@ describe('FieldConvertingService', () => {
{
formatting: 'italic',
showAs: 'number',
filter: {
conjunction: 'and',
filterSet: [
{
fieldId: 'fldxxxxxxx01',
operator: 'is',
value: 'x',
},
],
},
filterByViewId: 'viewxxxxxxx01',
hiddenFieldIds: ['fldxxxxxxx01'],
anotherKey: 'anotherKey',
},
{
formatting: 'bold',
showAs: 'text',
filter: {
conjunction: 'and',
filterSet: [
{
fieldId: 'fldxxxxxxx02',
operator: 'is',
value: 'x',
},
],
},
filterByViewId: 'viewxxxxxxx02',
hiddenFieldIds: ['fldxxxxxxx02'],
otherKey: 'otherKey',
}
)
Expand All @@ -43,11 +67,35 @@ describe('FieldConvertingService', () => {
{
formatting: 'italic',
showAs: 'number',
filter: {
conjunction: 'and',
filterSet: [
{
fieldId: 'fldxxxxxxx01',
operator: 'is',
value: 'x',
},
],
},
filterByViewId: 'viewxxxxxxx01',
hiddenFieldIds: ['fldxxxxxxx01'],
anotherKey: 'anotherKey',
},
{
formatting: 'bold',
showAs: 'text',
filter: {
conjunction: 'and',
filterSet: [
{
fieldId: 'fldxxxxxxx02',
operator: 'is',
value: 'x',
},
],
},
filterByViewId: 'viewxxxxxxx02',
hiddenFieldIds: ['fldxxxxxxx02'],
otherKey: 'otherKey',
},
true
Expand All @@ -57,18 +105,45 @@ describe('FieldConvertingService', () => {
otherKey: null,
formatting: null,
showAs: null,
filter: null,
filterByViewId: null,
hiddenFieldIds: null,
});

expect(
service['getOptionsChanges'](
{
formatting: 'italic',
showAs: 'number',
filter: {
conjunction: 'and',
filterSet: [
{
fieldId: 'fldxxxxxxx01',
operator: 'is',
value: 'x',
},
],
},
filterByViewId: 'viewxxxxxxx01',
hiddenFieldIds: ['fldxxxxxxx01'],
otherKey: 'newOtherKey',
},
{
formatting: 'bold',
showAs: 'text',
filter: {
conjunction: 'and',
filterSet: [
{
fieldId: 'fldxxxxxxx02',
operator: 'is',
value: 'x',
},
],
},
filterByViewId: 'viewxxxxxxx02',
hiddenFieldIds: ['fldxxxxxxx02'],
otherKey: 'oldOtherKey',
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ import { PrismaService } from '@teable/db-main-prisma';
import { Knex } from 'knex';
import { difference, intersection, isEmpty, isEqual, keyBy, set } from 'lodash';
import { InjectModel } from 'nest-knexjs';
import { majorFieldKeysChanged } from '../../../utils/major-field-keys-changed';
import {
majorFieldKeysChanged,
majorOptionsKeyChanged,
NON_INFECT_OPTION_KEYS,
} from '../../../utils/major-field-keys-changed';
import { BatchService } from '../../calculation/batch.service';
import { FieldCalculationService } from '../../calculation/field-calculation.service';
import { LinkService } from '../../calculation/link.service';
Expand Down Expand Up @@ -253,7 +257,7 @@ export class FieldConvertingService {

newOptions = { ...newOptions };
oldOptions = { ...oldOptions };
const nonInfectKeys = ['formatting', 'showAs'];
const nonInfectKeys = Array.from(NON_INFECT_OPTION_KEYS);
nonInfectKeys.forEach((key) => {
delete newOptions[key];
delete oldOptions[key];
Expand Down Expand Up @@ -1018,7 +1022,7 @@ export class FieldConvertingService {
}

// for same field with options change
if (keys.includes('options')) {
if (keys.includes('options') && majorOptionsKeyChanged(oldField.options, newField.options)) {
return await this.modifyOptions(tableId, newField, oldField);
}
}
Expand Down Expand Up @@ -1152,7 +1156,6 @@ export class FieldConvertingService {
newField,
oldField
);

return {
newField,
oldField,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { Injectable, Logger } from '@nestjs/common';
import { getValidFilterOperators, FieldType, ViewOpBuilder } from '@teable/core';
import { getValidFilterOperators, FieldType, ViewOpBuilder, FieldOpBuilder } from '@teable/core';
import type {
IFilterSet,
ISelectFieldOptionsRo,
ISelectFieldOptions,
IFilterItem,
IFilter,
IFilterValue,
ILinkFieldOptions,
IOtOperation,
} from '@teable/core';
import { PrismaService } from '@teable/db-main-prisma';
import { isEqual, differenceBy, find, isEmpty } from 'lodash';
import { ViewService } from '../../view/view.service';
import { FieldService } from '../field.service';
import type { IFieldInstance } from '../model/factory';

/**
Expand All @@ -23,14 +26,142 @@ export class FieldViewSyncService {

constructor(
private readonly viewService: ViewService,
private readonly fieldService: FieldService,
private readonly prismaService: PrismaService
) {}

async deleteViewRelativeByFields(tableId: string, fieldIds: string[]) {
async deleteDependenciesByFieldIds(tableId: string, fieldIds: string[]) {
await this.viewService.deleteViewRelativeByFields(tableId, fieldIds);
await this.deleteLinkOptionsDependenciesByFieldIds(tableId, fieldIds);
}

async convertFieldRelative(tableId: string, newField: IFieldInstance, oldField: IFieldInstance) {
// eslint-disable-next-line sonarjs/cognitive-complexity
async deleteLinkOptionsDependenciesByFieldIds(tableId: string, fieldIds: string[]) {
const foreignFields = await this.getLinkForeignFields(tableId);
const deletedFieldIdSet = new Set(fieldIds);

for (const field of foreignFields) {
const ops: IOtOperation[] = [];
const { id: fieldId, tableId, options: rawOptions } = field;
const options = rawOptions ? JSON.parse(rawOptions) : null;

if (options == null) continue;

const { filter, hiddenFieldIds } = options as ILinkFieldOptions;
const newOptions: ILinkFieldOptions = { ...options };
let isOptionsChanged = false;

if (hiddenFieldIds?.length) {
const newHiddenFieldIds = hiddenFieldIds.filter((id) => !deletedFieldIdSet.has(id));
if (!isEqual(newHiddenFieldIds, hiddenFieldIds)) {
newOptions.hiddenFieldIds = newHiddenFieldIds?.length ? newHiddenFieldIds : null;
isOptionsChanged = true;
}
}

const filterString = JSON.stringify(filter);
const filteredFieldIds = fieldIds.filter((id) => filterString?.includes(id));

if (filter != null && filteredFieldIds.length) {
let newFilter: IFilterSet | null = filter;
filteredFieldIds.forEach((id) => {
if (newFilter) {
newFilter = this.viewService.getDeletedFilterByFieldId(newFilter, id);
}
});
newOptions.filter = newFilter ? (newFilter?.filterSet?.length ? newFilter : null) : null;
isOptionsChanged = true;
}

if (isOptionsChanged) {
ops.push(
FieldOpBuilder.editor.setFieldProperty.build({
key: 'options',
newValue: newOptions,
oldValue: options,
})
);
}

if (ops.length) {
await this.fieldService.batchUpdateFields(tableId, [{ fieldId, ops }]);
}
}
}

async deleteLinkOptionsDependenciesByViewId(tableId: string, viewId: string) {
const foreignFields = await this.getLinkForeignFields(tableId);

for (const field of foreignFields) {
const { id: fieldId, tableId, options: rawOptions } = field;
const options = rawOptions ? JSON.parse(rawOptions) : null;

if (options == null) continue;

const { filterByViewId } = options as ILinkFieldOptions;

if (filterByViewId == null || filterByViewId !== viewId) continue;

const ops = [
FieldOpBuilder.editor.setFieldProperty.build({
key: 'options',
oldValue: options,
newValue: { ...options, filterByViewId: null },
}),
];
await this.fieldService.batchUpdateFields(tableId, [{ fieldId, ops }]);
}
}

async convertDependenciesByFieldIds(
tableId: string,
newField: IFieldInstance,
oldField: IFieldInstance
) {
await this.convertViewDependenciesByFieldIds(tableId, newField, oldField);
await this.convertLinkOptionsDependenciesByFieldIds(tableId, newField, oldField);
}

async convertLinkOptionsDependenciesByFieldIds(
tableId: string,
newField: IFieldInstance,
oldField: IFieldInstance
) {
const convertedFieldId = newField.id;
const foreignFields = await this.getLinkForeignFields(tableId);

for (const field of foreignFields) {
const { id: fieldId, tableId, options: rawOptions } = field;
const options = rawOptions ? JSON.parse(rawOptions) : null;

if (options == null) continue;

const ops: IOtOperation[] = [];
const { filter } = options as ILinkFieldOptions;

if (filter == null || !JSON.stringify(filter).includes(convertedFieldId)) continue;

const newFilter = this.getNewFilterByFieldChanges(filter, newField, oldField);
ops.push(
FieldOpBuilder.editor.setFieldProperty.build({
key: 'options',
oldValue: options,
newValue: {
...options,
filter: newFilter ? (newFilter?.filterSet?.length ? newFilter : null) : null,
},
})
);

await this.fieldService.batchUpdateFields(tableId, [{ fieldId, ops }]);
}
}

async convertViewDependenciesByFieldIds(
tableId: string,
newField: IFieldInstance,
oldField: IFieldInstance
) {
const views = await this.prismaService.txClient().view.findMany({
select: {
filter: true,
Expand Down Expand Up @@ -64,6 +195,21 @@ export class FieldViewSyncService {
}
}

async getLinkForeignFields(tableId: string) {
const linkFields = await this.prismaService.txClient().field.findMany({
where: { tableId, type: FieldType.Link, deletedTime: null },
});
const foreignFieldIds = linkFields
.map(
({ options }) =>
((options ? JSON.parse(options) : null) as ILinkFieldOptions)?.symmetricFieldId
)
.filter(Boolean) as string[];
return await this.prismaService.txClient().field.findMany({
where: { id: { in: foreignFieldIds }, type: FieldType.Link, deletedTime: null },
});
}

getNewFilterByFieldChanges(
originalFilter: IFilter,
newField: IFieldInstance,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ import {
updateFieldRoSchema,
IUpdateFieldRo,
} from '@teable/core';
import {
deleteFieldsQuerySchema,
IDeleteFieldsQuery,
type IPlanFieldConvertVo,
type IPlanFieldVo,
import { deleteFieldsQuerySchema, IDeleteFieldsQuery } from '@teable/openapi';
import type {
IGetViewFilterLinkRecordsVo,
IPlanFieldConvertVo,
IPlanFieldVo,
} from '@teable/openapi';
import { ZodValidationPipe } from '../../../zod.validation.pipe';
import { Permissions } from '../../auth/decorators/permissions.decorator';
Expand Down Expand Up @@ -137,6 +137,15 @@ export class FieldOpenApiController {
await this.fieldOpenApiService.deleteFields(tableId, query.fieldIds, windowId);
}

@Permissions('field|update')
@Get('/:fieldId/filter-link-records')
async getFilterLinkRecords(
@Param('tableId') tableId: string,
@Param('fieldId') fieldId: string
): Promise<IGetViewFilterLinkRecordsVo> {
return this.fieldOpenApiService.getFilterLinkRecords(tableId, fieldId);
}

@Permissions('field|read')
@Get('/socket/snapshot-bulk')
async getSnapshotBulk(@Param('tableId') tableId: string, @Query('ids') ids: string[]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CalculationModule } from '../../calculation/calculation.module';
import { GraphModule } from '../../graph/graph.module';
import { RecordOpenApiModule } from '../../record/open-api/record-open-api.module';
import { RecordModule } from '../../record/record.module';
import { ViewOpenApiModule } from '../../view/open-api/view-open-api.module';
import { ViewModule } from '../../view/view.module';
import { FieldCalculateModule } from '../field-calculate/field-calculate.module';
import { FieldModule } from '../field.module';
Expand All @@ -15,7 +16,7 @@ import { FieldOpenApiService } from './field-open-api.service';
imports: [
FieldModule,
RecordModule,
ViewModule,
ViewOpenApiModule,
ShareDbModule,
CalculationModule,
RecordOpenApiModule,
Expand Down
Loading

0 comments on commit 14ea9f7

Please sign in to comment.