• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

teableio / teable / 10056753074

23 Jul 2024 09:54AM UTC coverage: 82.459% (+64.6%) from 17.849%
10056753074

Pull #748

github

web-flow
Merge 756f11bd7 into 8d891a1e3
Pull Request #748: feat: copy and paste operations for pre-filled rows in the grid

4267 of 4468 branches covered (95.5%)

17 of 54 new or added lines in 2 files covered. (31.48%)

28421 of 34467 relevant lines covered (82.46%)

1220.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.64
/apps/nestjs-backend/src/features/selection/selection.service.ts
1
import { BadRequestException, Injectable } from '@nestjs/common';
4✔
2
import type {
4✔
3
  IDateFieldOptions,
4✔
4
  IFieldOptionsRo,
4✔
5
  IFieldOptionsVo,
4✔
6
  IFieldRo,
4✔
7
  IFieldVo,
4✔
8
  INumberFieldOptionsRo,
4✔
9
  IRecord,
4✔
10
  ISingleLineTextFieldOptions,
4✔
11
  IUserFieldOptions,
4✔
12
} from '@teable/core';
4✔
13
import {
4✔
14
  CellValueType,
4✔
15
  FieldKeyType,
4✔
16
  FieldType,
4✔
17
  datetimeFormattingSchema,
4✔
18
  defaultDatetimeFormatting,
4✔
19
  defaultNumberFormatting,
4✔
20
  defaultUserFieldOptions,
4✔
21
  nullsToUndefined,
4✔
22
  numberFormattingSchema,
4✔
23
  parseClipboardText,
4✔
24
  singleLineTextShowAsSchema,
4✔
25
  singleNumberShowAsSchema,
4✔
26
  stringifyClipboardText,
4✔
27
} from '@teable/core';
4✔
28
import { PrismaService } from '@teable/db-main-prisma';
4✔
29
import type {
4✔
30
  IUpdateRecordsRo,
4✔
31
  IRangesToIdQuery,
4✔
32
  IRangesToIdVo,
4✔
33
  IPasteRo,
4✔
34
  IPasteVo,
4✔
35
  IRangesRo,
4✔
36
  IDeleteVo,
4✔
37
} from '@teable/openapi';
4✔
38
import { IdReturnType, RangeType } from '@teable/openapi';
4✔
39
import { isNumber, isString, map, pick } from 'lodash';
4✔
40
import { ClsService } from 'nestjs-cls';
4✔
41
import { ThresholdConfig, IThresholdConfig } from '../../configs/threshold.config';
4✔
42
import type { IClsStore } from '../../types/cls';
4✔
43
import { AggregationService } from '../aggregation/aggregation.service';
4✔
44
import { FieldCreatingService } from '../field/field-calculate/field-creating.service';
4✔
45
import { FieldSupplementService } from '../field/field-calculate/field-supplement.service';
4✔
46
import { FieldService } from '../field/field.service';
4✔
47
import type { IFieldInstance } from '../field/model/factory';
4✔
48
import { createFieldInstanceByVo } from '../field/model/factory';
4✔
49
import { AttachmentFieldDto } from '../field/model/field-dto/attachment-field.dto';
4✔
50
import { RecordOpenApiService } from '../record/open-api/record-open-api.service';
4✔
51
import { RecordService } from '../record/record.service';
4✔
52

4✔
53
@Injectable()
4✔
54
export class SelectionService {
4✔
55
  constructor(
132✔
56
    private readonly recordService: RecordService,
132✔
57
    private readonly fieldService: FieldService,
132✔
58
    private readonly prismaService: PrismaService,
132✔
59
    private readonly aggregationService: AggregationService,
132✔
60
    private readonly recordOpenApiService: RecordOpenApiService,
132✔
61
    private readonly fieldCreatingService: FieldCreatingService,
132✔
62
    private readonly fieldSupplementService: FieldSupplementService,
132✔
63
    private readonly cls: ClsService<IClsStore>,
132✔
64
    @ThresholdConfig() private readonly thresholdConfig: IThresholdConfig
132✔
65
  ) {}
132✔
66

132✔
67
  async getIdsFromRanges(tableId: string, query: IRangesToIdQuery): Promise<IRangesToIdVo> {
132✔
68
    const { returnType } = query;
18✔
69
    if (returnType === IdReturnType.RecordId) {
18✔
70
      return {
6✔
71
        recordIds: await this.rowSelectionToIds(tableId, query),
6✔
72
      };
6✔
73
    }
6✔
74

12✔
75
    if (returnType === IdReturnType.FieldId) {
18✔
76
      return {
6✔
77
        fieldIds: await this.columnSelectionToIds(tableId, query),
6✔
78
      };
6✔
79
    }
6✔
80

6✔
81
    if (returnType === IdReturnType.All) {
6✔
82
      return {
6✔
83
        fieldIds: await this.columnSelectionToIds(tableId, query),
6✔
84
        recordIds: await this.rowSelectionToIds(tableId, query),
6✔
85
      };
6✔
86
    }
6✔
87

×
88
    throw new BadRequestException('Invalid return type');
×
89
  }
×
90

132✔
91
  private async columnSelectionToIds(tableId: string, query: IRangesToIdQuery): Promise<string[]> {
132✔
92
    const { type, viewId, ranges, excludeFieldIds } = query;
12✔
93
    const result = await this.fieldService.getDocIdsByQuery(tableId, {
12✔
94
      viewId,
12✔
95
      filterHidden: true,
12✔
96
      excludeFieldIds,
12✔
97
    });
12✔
98

12✔
99
    if (type === RangeType.Rows) {
12✔
100
      return result.ids;
4✔
101
    }
4✔
102

8✔
103
    if (type === RangeType.Columns) {
12✔
104
      return ranges.reduce<string[]>((acc, range) => {
4✔
105
        return acc.concat(result.ids.slice(range[0], range[1] + 1));
4✔
106
      }, []);
4✔
107
    }
4✔
108

4✔
109
    const [start, end] = ranges;
4✔
110
    return result.ids.slice(start[0], end[0] + 1);
4✔
111
  }
4✔
112

132✔
113
  private async rowSelectionToIds(tableId: string, query: IRangesToIdQuery): Promise<string[]> {
132✔
114
    const { type, ranges } = query;
12✔
115
    if (type === RangeType.Columns) {
12✔
116
      const result = await this.recordService.getDocIdsByQuery(tableId, {
4✔
117
        ...query,
4✔
118
        skip: 0,
4✔
119
        take: -1,
4✔
120
      });
4✔
121
      return result.ids;
4✔
122
    }
4✔
123

8✔
124
    if (type === RangeType.Rows) {
12✔
125
      let recordIds: string[] = [];
4✔
126
      const total = ranges.reduce((acc, range) => acc + range[1] - range[0] + 1, 0);
4✔
127
      if (total > this.thresholdConfig.maxReadRows) {
4✔
128
        throw new BadRequestException(`Exceed max read rows ${this.thresholdConfig.maxReadRows}`);
×
129
      }
×
130
      for (const [start, end] of ranges) {
4✔
131
        const result = await this.recordService.getDocIdsByQuery(tableId, {
4✔
132
          ...query,
4✔
133
          skip: start,
4✔
134
          take: end + 1 - start,
4✔
135
        });
4✔
136
        recordIds = recordIds.concat(result.ids);
4✔
137
      }
4✔
138

4✔
139
      return ranges.reduce<string[]>((acc, range) => {
4✔
140
        return acc.concat(recordIds.slice(range[0], range[1] + 1));
4✔
141
      }, []);
4✔
142
    }
4✔
143

4✔
144
    const [start, end] = ranges;
4✔
145
    const total = end[1] - start[1] + 1;
4✔
146
    if (total > this.thresholdConfig.maxReadRows) {
12✔
147
      throw new BadRequestException(`Exceed max read rows ${this.thresholdConfig.maxReadRows}`);
×
148
    }
✔
149
    const result = await this.recordService.getDocIdsByQuery(tableId, {
4✔
150
      ...query,
4✔
151
      skip: start[1],
4✔
152
      take: end[1] + 1 - start[1],
4✔
153
    });
4✔
154

4✔
155
    return result.ids;
4✔
156
  }
4✔
157

132✔
158
  private fieldsToProjection(fields: IFieldVo[], fieldKeyType: FieldKeyType) {
132✔
159
    return fields.map((f) => f[fieldKeyType]);
14✔
160
  }
14✔
161

132✔
162
  private async columnsSelectionCtx(tableId: string, rangesRo: IRangesRo) {
132✔
163
    const { ranges, type, excludeFieldIds, ...queryRo } = rangesRo;
×
164

×
165
    const fields = await this.fieldService.getFieldsByQuery(tableId, {
×
166
      viewId: queryRo.viewId,
×
167
      filterHidden: true,
×
168
      excludeFieldIds,
×
169
    });
×
170

×
171
    const records = await this.recordService.getRecordsFields(tableId, {
×
172
      ...queryRo,
×
173
      skip: 0,
×
174
      take: -1,
×
175
      fieldKeyType: FieldKeyType.Id,
×
176
      projection: this.fieldsToProjection(fields, FieldKeyType.Id),
×
177
    });
×
178

×
179
    return {
×
180
      records,
×
181
      fields: ranges.reduce((acc, range) => {
×
182
        return acc.concat(fields.slice(range[0], range[1] + 1));
×
183
      }, [] as IFieldVo[]),
×
184
    };
×
185
  }
×
186

132✔
187
  private async rowsSelectionCtx(tableId: string, rangesRo: IRangesRo) {
132✔
188
    const { ranges, type, excludeFieldIds, ...queryRo } = rangesRo;
2✔
189
    const fields = await this.fieldService.getFieldsByQuery(tableId, {
2✔
190
      viewId: queryRo.viewId,
2✔
191
      filterHidden: true,
2✔
192
      excludeFieldIds,
2✔
193
    });
2✔
194
    let records: Pick<IRecord, 'id' | 'fields'>[] = [];
2✔
195
    for (const [start, end] of ranges) {
2✔
196
      const recordsFields = await this.recordService.getRecordsFields(tableId, {
4✔
197
        ...queryRo,
4✔
198
        skip: start,
4✔
199
        take: end + 1 - start,
4✔
200
        fieldKeyType: FieldKeyType.Id,
4✔
201
        projection: this.fieldsToProjection(fields, FieldKeyType.Id),
4✔
202
      });
4✔
203
      records = records.concat(recordsFields);
4✔
204
    }
4✔
205

2✔
206
    return {
2✔
207
      records,
2✔
208
      fields,
2✔
209
    };
2✔
210
  }
2✔
211

132✔
212
  private async defaultSelectionCtx(tableId: string, rangesRo: IRangesRo) {
132✔
213
    const { ranges, type, excludeFieldIds, ...queryRo } = rangesRo;
10✔
214
    const [start, end] = ranges;
10✔
215
    const fields = await this.fieldService.getFieldInstances(tableId, {
10✔
216
      viewId: queryRo.viewId,
10✔
217
      filterHidden: true,
10✔
218
      excludeFieldIds,
10✔
219
    });
10✔
220

10✔
221
    const records = await this.recordService.getRecordsFields(tableId, {
10✔
222
      ...queryRo,
10✔
223
      skip: start[1],
10✔
224
      take: end[1] + 1 - start[1],
10✔
225
      fieldKeyType: FieldKeyType.Id,
10✔
226
      projection: this.fieldsToProjection(fields, FieldKeyType.Id),
10✔
227
    });
10✔
228
    return { records, fields: fields.slice(start[0], end[0] + 1) };
10✔
229
  }
10✔
230

132✔
231
  private async parseRange(
132✔
232
    tableId: string,
14✔
233
    rangesRo: IRangesRo
14✔
234
  ): Promise<{ cellCount: number; columnCount: number; rowCount: number }> {
14✔
235
    const { ranges, type, excludeFieldIds, ...queryRo } = rangesRo;
14✔
236
    switch (type) {
14✔
237
      case RangeType.Columns: {
14!
238
        const { rowCount } = await this.aggregationService.performRowCount(tableId, queryRo);
×
239
        const columnCount = ranges.reduce((acc, range) => acc + range[1] - range[0] + 1, 0);
×
240
        const cellCount = rowCount * columnCount;
×
241

×
242
        return { cellCount, columnCount, rowCount };
×
243
      }
×
244
      case RangeType.Rows: {
14!
245
        const fields = await this.fieldService.getFieldsByQuery(tableId, {
×
246
          viewId: queryRo.viewId,
×
247
          filterHidden: true,
×
248
          excludeFieldIds,
×
249
        });
×
250
        const columnCount = fields.length;
×
251
        const rowCount = ranges.reduce((acc, range) => acc + range[1] - range[0] + 1, 0);
×
252
        const cellCount = rowCount * columnCount;
×
253

×
254
        return { cellCount, columnCount, rowCount };
×
255
      }
×
256
      default: {
14✔
257
        const [start, end] = ranges;
14✔
258
        const columnCount = end[0] - start[0] + 1;
14✔
259
        const rowCount = end[1] - start[1] + 1;
14✔
260
        const cellCount = rowCount * columnCount;
14✔
261

14✔
262
        return { cellCount, columnCount, rowCount };
14✔
263
      }
14✔
264
    }
14✔
265
  }
14✔
266

132✔
267
  private async getSelectionCtxByRange(tableId: string, rangesRo: IRangesRo) {
132✔
268
    const { type } = rangesRo;
12✔
269
    switch (type) {
12✔
270
      case RangeType.Columns: {
12✔
271
        return await this.columnsSelectionCtx(tableId, rangesRo);
×
272
      }
×
273
      case RangeType.Rows: {
12✔
274
        return await this.rowsSelectionCtx(tableId, rangesRo);
2✔
275
      }
2✔
276
      default:
12✔
277
        return await this.defaultSelectionCtx(tableId, rangesRo);
10✔
278
    }
12✔
279
  }
12✔
280

132✔
281
  private optionsRoToVoByCvType(
132✔
282
    cellValueType: CellValueType,
14✔
283
    options: IFieldOptionsVo = {}
14✔
284
  ): { type: FieldType; options: IFieldOptionsRo } {
14✔
285
    switch (cellValueType) {
14✔
286
      case CellValueType.Number: {
14✔
287
        const numberOptions = options as INumberFieldOptionsRo;
6✔
288
        const formattingRes = numberFormattingSchema.safeParse(numberOptions?.formatting);
6✔
289
        const showAsRes = singleNumberShowAsSchema.safeParse(numberOptions?.showAs);
6✔
290
        return {
6✔
291
          type: FieldType.Number,
6✔
292
          options: {
6✔
293
            formatting: formattingRes.success ? formattingRes?.data : defaultNumberFormatting,
6!
294
            showAs: showAsRes.success ? showAsRes?.data : undefined,
6!
295
          },
6✔
296
        };
6✔
297
      }
6✔
298
      case CellValueType.DateTime: {
14✔
299
        const dateOptions = options as IDateFieldOptions;
2✔
300
        const formattingRes = datetimeFormattingSchema.safeParse(dateOptions?.formatting);
2✔
301
        return {
2✔
302
          type: FieldType.Date,
2✔
303
          options: {
2✔
304
            formatting: formattingRes.success ? formattingRes?.data : defaultDatetimeFormatting,
2!
305
          },
2✔
306
        };
2✔
307
      }
2✔
308
      case CellValueType.String: {
14✔
309
        const singleLineTextOptions = options as ISingleLineTextFieldOptions;
2✔
310
        const showAsRes = singleLineTextShowAsSchema.safeParse(singleLineTextOptions.showAs);
2✔
311
        return {
2✔
312
          type: FieldType.SingleLineText,
2✔
313
          options: {
2✔
314
            showAs: showAsRes.success ? showAsRes?.data : undefined,
2!
315
          },
2✔
316
        };
2✔
317
      }
2✔
318
      case CellValueType.Boolean: {
14✔
319
        return {
2✔
320
          type: FieldType.Checkbox,
2✔
321
          options: {},
2✔
322
        };
2✔
323
      }
2✔
324
      default:
14✔
325
        throw new BadRequestException('Invalid cellValueType');
2✔
326
    }
14✔
327
  }
14✔
328

132✔
329
  private lookupOptionsRoToVo(field: IFieldVo): { type: FieldType; options: IFieldOptionsRo } {
132✔
330
    const { type, isMultipleCellValue, options } = field;
6✔
331
    if (type === FieldType.SingleSelect && isMultipleCellValue) {
6✔
332
      return {
2✔
333
        type: FieldType.MultipleSelect,
2✔
334
        options,
2✔
335
      };
2✔
336
    }
2✔
337
    if (type === FieldType.User && isMultipleCellValue) {
6✔
338
      const userOptions = options as IUserFieldOptions;
2✔
339
      return {
2✔
340
        type,
2✔
341
        options: {
2✔
342
          ...userOptions,
2✔
343
          isMultiple: true,
2✔
344
        },
2✔
345
      };
2✔
346
    }
2✔
347
    return { type, options };
2✔
348
  }
2✔
349

132✔
350
  private fieldVoToRo(field?: IFieldVo): IFieldRo {
132✔
351
    if (!field) {
16✔
352
      return {
2✔
353
        type: FieldType.SingleLineText,
2✔
354
      };
2✔
355
    }
2✔
356
    const { isComputed, isLookup } = field;
14✔
357
    const baseField = pick(field, 'name', 'type', 'options', 'description');
14✔
358

14✔
359
    if (isComputed && !isLookup) {
16✔
360
      if ([FieldType.CreatedBy, FieldType.LastModifiedBy].includes(field.type)) {
8✔
361
        return {
4✔
362
          ...baseField,
4✔
363
          type: FieldType.User,
4✔
364
          options: defaultUserFieldOptions,
4✔
365
        };
4✔
366
      }
4✔
367
      return {
4✔
368
        ...baseField,
4✔
369
        ...this.optionsRoToVoByCvType(field.cellValueType, field.options),
4✔
370
      };
4✔
371
    }
4✔
372

6✔
373
    if (isLookup) {
14!
374
      return {
×
375
        ...baseField,
×
376
        ...this.lookupOptionsRoToVo(field),
×
377
      };
×
378
    }
✔
379

6✔
380
    return baseField;
6✔
381
  }
6✔
382

132✔
383
  private async expandColumns({
132✔
384
    tableId,
8✔
385
    header,
8✔
386
    numColsToExpand,
8✔
387
  }: {
8✔
388
    tableId: string;
8✔
389
    header: IFieldVo[];
8✔
390
    numColsToExpand: number;
8✔
391
  }) {
8✔
392
    const colLen = header.length;
8✔
393
    const res: IFieldVo[] = [];
8✔
394
    for (let i = colLen - numColsToExpand; i < colLen; i++) {
8✔
395
      const field = this.fieldVoToRo(header[i]);
6✔
396
      const fieldVo = await this.fieldSupplementService.prepareCreateField(tableId, field);
6✔
397
      const fieldInstance = createFieldInstanceByVo(fieldVo);
6✔
398
      // expend columns do not need to calculate
6✔
399
      await this.fieldCreatingService.alterCreateField(tableId, fieldInstance);
6✔
400
      res.push(fieldVo);
6✔
401
    }
6✔
402
    return res;
8✔
403
  }
8✔
404

132✔
405
  private async collectionAttachment({
132✔
406
    fields,
2✔
407
    tableData,
2✔
408
  }: {
2✔
409
    tableData: string[][];
2✔
410
    fields: IFieldInstance[];
2✔
411
  }) {
2✔
412
    const attachmentFieldsIndex = fields
2✔
413
      .map((field, index) => (field.type === FieldType.Attachment ? index : null))
2✔
414
      .filter(isNumber);
2✔
415

2✔
416
    const tokens = tableData.reduce((acc, recordData) => {
2✔
417
      const tokensInRecord = attachmentFieldsIndex.reduce((acc, index) => {
4✔
418
        if (!recordData[index]) return acc;
4✔
419

4✔
420
        const tokensAndNames = recordData[index]
4✔
421
          .split(',')
4✔
422
          .map(AttachmentFieldDto.getTokenAndNameByString);
4✔
423
        return acc.concat(map(tokensAndNames, 'token').filter(isString));
4✔
424
      }, [] as string[]);
4✔
425
      return acc.concat(tokensInRecord);
4✔
426
    }, [] as string[]);
2✔
427

2✔
428
    const attachments = await this.prismaService.attachments.findMany({
2✔
429
      where: {
2✔
430
        token: {
2✔
431
          in: tokens,
2✔
432
        },
2✔
433
      },
2✔
434
      select: {
2✔
435
        token: true,
2✔
436
        size: true,
2✔
437
        mimetype: true,
2✔
438
        width: true,
2✔
439
        height: true,
2✔
440
        path: true,
2✔
441
      },
2✔
442
    });
2✔
443
    return attachments.map(nullsToUndefined);
2✔
444
  }
2✔
445

132✔
446
  private parseCopyContent(content: string): string[][] {
132✔
447
    return parseClipboardText(content);
8✔
448
  }
8✔
449

132✔
450
  private stringifyCopyContent(content: string[][]): string {
132✔
451
    return stringifyClipboardText(content);
6✔
452
  }
6✔
453

132✔
454
  private calculateExpansion(
132✔
455
    tableSize: [number, number],
12✔
456
    cell: [number, number],
12✔
457
    tableDataSize: [number, number]
12✔
458
  ): [number, number] {
12✔
459
    const permissions = this.cls.get('permissions');
12✔
460
    const [numCols, numRows] = tableSize;
12✔
461
    const [dataNumCols, dataNumRows] = tableDataSize;
12✔
462

12✔
463
    const endCol = cell[0] + dataNumCols;
12✔
464
    const endRow = cell[1] + dataNumRows;
12✔
465

12✔
466
    const numRowsToExpand = Math.max(0, endRow - numRows);
12✔
467
    const numColsToExpand = Math.max(0, endCol - numCols);
12✔
468

12✔
469
    const hasFieldCreatePermission = permissions.includes('field|create');
12✔
470
    const hasRecordCreatePermission = permissions.includes('record|create');
12✔
471
    return [
12✔
472
      hasFieldCreatePermission ? numColsToExpand : 0,
12✔
473
      hasRecordCreatePermission ? numRowsToExpand : 0,
12!
474
    ];
12✔
475
  }
12✔
476

132✔
477
  private async tableDataToRecords({
132✔
478
    tableId,
12✔
479
    tableData,
12✔
480
    fields,
12✔
481
    headerFields,
12✔
482
  }: {
12✔
483
    tableId: string;
12✔
484
    tableData: string[][];
12✔
485
    fields: IFieldInstance[];
12✔
486
    headerFields: IFieldInstance[] | undefined;
12✔
487
  }) {
12✔
488
    const fieldConvertContext = await this.fieldConvertContext(tableId, tableData, fields);
12✔
489

12✔
490
    const records: { fields: IRecord['fields'] }[] = [];
12✔
491
    fields.forEach((field, col) => {
12✔
492
      if (field.isComputed) {
28!
493
        return;
×
494
      }
×
495
      tableData.forEach((cellCols, row) => {
28✔
496
        const stringValue = cellCols?.[col] ?? null;
60!
497
        const recordField = records[row]?.fields || {};
60✔
498

60✔
499
        if (stringValue === null) {
60!
500
          recordField[field.id] = null;
×
501
        } else {
60✔
502
          switch (field.type) {
60✔
503
            case FieldType.Attachment:
60!
504
              {
×
505
                recordField[field.id] = field.convertStringToCellValue(
×
506
                  stringValue,
×
507
                  fieldConvertContext?.attachments
×
508
                );
×
509
              }
×
510
              break;
×
511
            case FieldType.Date:
60✔
512
              // handle format
4✔
513
              recordField[field.id] = (headerFields?.[col] || field).convertStringToCellValue(
4!
514
                stringValue
4✔
515
              );
4✔
516
              break;
4✔
517
            default:
60✔
518
              recordField[field.id] = field.convertStringToCellValue(stringValue);
56✔
519
          }
60✔
520
        }
60✔
521

60✔
522
        records[row] = {
60✔
523
          fields: recordField,
60✔
524
        };
60✔
525
      });
60✔
526
    });
28✔
527
    return records;
12✔
528
  }
12✔
529

132✔
530
  private fillCells(
132✔
531
    oldRecords: {
16✔
532
      id: string;
16✔
533
      fields: IRecord['fields'];
16✔
534
    }[],
16✔
535
    newRecords?: { fields: IRecord['fields'] }[]
16✔
536
  ): IUpdateRecordsRo {
16✔
537
    return {
16✔
538
      fieldKeyType: FieldKeyType.Id,
16✔
539
      typecast: true,
16✔
540
      records: oldRecords.map(({ id, fields }, index) => {
16✔
541
        const newFields = newRecords?.[index]?.fields;
28✔
542
        const updateFields = newFields ? { ...fields, ...newFields } : {};
28✔
543
        return {
28✔
544
          id,
28✔
545
          fields: updateFields,
28✔
546
        };
28✔
547
      }),
28✔
548
    };
16✔
549
  }
16✔
550

132✔
551
  private async fieldConvertContext(
132✔
552
    tableId: string,
12✔
553
    tableData: string[][],
12✔
554
    fields: IFieldInstance[]
12✔
555
  ) {
12✔
556
    const hasFieldType = (type: FieldType) => fields.some((field) => field.type === type);
12✔
557

12✔
558
    const loadAttachments = hasFieldType(FieldType.Attachment)
12!
559
      ? this.collectionAttachment({ fields, tableData })
×
560
      : Promise.resolve(undefined);
12✔
561

12✔
562
    const [attachments] = await Promise.all([loadAttachments]);
12✔
563

12✔
564
    return {
12✔
565
      attachments: attachments,
12✔
566
    };
12✔
567
  }
12✔
568

132✔
569
  async copy(tableId: string, rangesRo: IRangesRo) {
132✔
570
    const { cellCount } = await this.parseRange(tableId, rangesRo);
6✔
571

6✔
572
    if (cellCount > this.thresholdConfig.maxCopyCells) {
6!
573
      throw new BadRequestException(`Exceed max copy cells ${this.thresholdConfig.maxCopyCells}`);
×
574
    }
×
575

6✔
576
    const { fields, records } = await this.getSelectionCtxByRange(tableId, rangesRo);
6✔
577
    const fieldInstances = fields.map(createFieldInstanceByVo);
6✔
578
    const rectangleData = records.map((record) =>
6✔
579
      fieldInstances.map((fieldInstance) =>
14✔
580
        fieldInstance.cellValue2String(record.fields[fieldInstance.id] as never)
28✔
581
      )
6✔
582
    );
6✔
583
    return {
6✔
584
      content: this.stringifyCopyContent(rectangleData),
6✔
585
      header: fields,
6✔
586
    };
6✔
587
  }
6✔
588

132✔
589
  // If the pasted selection is twice the size of the content,
132✔
590
  // the content is automatically expanded to the selection size
132✔
591
  private expandPasteContent(pasteData: string[][], range: [[number, number], [number, number]]) {
132✔
592
    const [start, end] = range;
12✔
593
    const [startCol, startRow] = start;
12✔
594
    const [endCol, endRow] = end;
12✔
595

12✔
596
    const rangeRows = endRow - startRow + 1;
12✔
597
    const rangeCols = endCol - startCol + 1;
12✔
598

12✔
599
    const pasteRows = pasteData.length;
12✔
600
    const pasteCols = pasteData[0].length;
12✔
601

12✔
602
    if (rangeRows % pasteRows !== 0 || rangeCols % pasteCols !== 0) {
12✔
603
      return pasteData;
10✔
604
    }
10✔
605

2✔
606
    return Array.from({ length: rangeRows }, (_, i) =>
2✔
607
      Array.from({ length: rangeCols }, (_, j) => pasteData[i % pasteRows][j % pasteCols])
8✔
608
    );
2✔
609
  }
2✔
610

132✔
611
  // Paste does not support non-contiguous selections,
132✔
612
  // the first selection is taken by default.
132✔
613
  private getRangeCell(
132✔
614
    maxRange: [number, number][],
14✔
615
    range: [number, number][],
14✔
616
    type?: RangeType
14✔
617
  ): [[number, number], [number, number]] {
14✔
618
    const [maxStart, maxEnd] = maxRange;
14✔
619
    const [maxStartCol, maxStartRow] = maxStart;
14✔
620
    const [maxEndCol, maxEndRow] = maxEnd;
14✔
621

14✔
622
    if (type === RangeType.Columns) {
14✔
623
      return [
2✔
624
        [range[0][0], maxStartRow],
2✔
625
        [range[0][1], maxEndRow],
2✔
626
      ];
2✔
627
    }
2✔
628

12✔
629
    if (type === RangeType.Rows) {
14✔
630
      return [
2✔
631
        [maxStartCol, range[0][0]],
2✔
632
        [maxEndCol, range[0][1]],
2✔
633
      ];
2✔
634
    }
2✔
635
    return [range[0], range[1]];
10✔
636
  }
10✔
637

132✔
638
  // For pasting to add new lines
132✔
639
  async temporaryPaste(tableId: string, pasteRo: IPasteRo) {
132✔
NEW
640
    const { content, header = [], viewId, ranges, excludeFieldIds } = pasteRo;
×
NEW
641

×
NEW
642
    const fields = await this.fieldService.getFieldInstances(tableId, {
×
NEW
643
      viewId,
×
NEW
644
      filterHidden: true,
×
NEW
645
      excludeFieldIds: excludeFieldIds,
×
NEW
646
    });
×
NEW
647

×
NEW
648
    const rangeCell = ranges as [[number, number], [number, number]];
×
NEW
649
    const startColumnIndex = rangeCell[0][0];
×
NEW
650

×
NEW
651
    const tableData = this.expandPasteContent(this.parseCopyContent(content), rangeCell);
×
NEW
652
    const tableColCount = tableData[0].length;
×
NEW
653
    const effectFields = fields.slice(startColumnIndex, startColumnIndex + tableColCount);
×
NEW
654

×
NEW
655
    const newRecords = await this.tableDataToRecords({
×
NEW
656
      tableId,
×
NEW
657
      tableData,
×
NEW
658
      headerFields: header.map(createFieldInstanceByVo),
×
NEW
659
      fields: effectFields,
×
NEW
660
    });
×
NEW
661

×
NEW
662
    return await this.recordOpenApiService.validateFieldsAndTypecast(
×
NEW
663
      tableId,
×
NEW
664
      newRecords,
×
NEW
665
      FieldKeyType.Id,
×
NEW
666
      true
×
NEW
667
    );
×
NEW
668
  }
×
669

132✔
670
  async paste(
132✔
671
    tableId: string,
8✔
672
    pasteRo: IPasteRo,
8✔
673
    expansionChecker?: (col: number, row: number) => Promise<void>
8✔
674
  ) {
8✔
675
    const { content, header = [], ...rangesRo } = pasteRo;
8✔
676
    const { ranges, type, ...queryRo } = rangesRo;
8✔
677
    const { viewId } = queryRo;
8✔
678
    const { cellCount } = await this.parseRange(tableId, rangesRo);
8✔
679

8✔
680
    if (cellCount > this.thresholdConfig.maxPasteCells) {
8!
681
      throw new BadRequestException(`Exceed max paste cells ${this.thresholdConfig.maxPasteCells}`);
×
682
    }
×
683

8✔
684
    const { rowCount: rowCountInView } = await this.aggregationService.performRowCount(
8✔
685
      tableId,
8✔
686
      queryRo
8✔
687
    );
8✔
688
    const fields = await this.fieldService.getFieldInstances(tableId, {
8✔
689
      viewId,
8✔
690
      filterHidden: true,
8✔
691
      excludeFieldIds: rangesRo.excludeFieldIds,
8✔
692
    });
8✔
693

8✔
694
    const tableSize: [number, number] = [fields.length, rowCountInView];
8✔
695
    const rangeCell = this.getRangeCell(
8✔
696
      [
8✔
697
        [0, 0],
8✔
698
        [tableSize[0] - 1, tableSize[1] - 1],
8✔
699
      ],
8✔
700
      ranges,
8✔
701
      type
8✔
702
    );
8✔
703

8✔
704
    const tableData = this.expandPasteContent(this.parseCopyContent(content), rangeCell);
8✔
705
    const tableColCount = tableData[0].length;
8✔
706
    const tableRowCount = tableData.length;
8✔
707

8✔
708
    const cell = rangeCell[0];
8✔
709
    const [col, row] = cell;
8✔
710

8✔
711
    const effectFields = fields.slice(col, col + tableColCount);
8✔
712

8✔
713
    const projection = effectFields.map((f) => f.id);
8✔
714

8✔
715
    const records = await this.recordService.getRecordsFields(tableId, {
8✔
716
      ...queryRo,
8✔
717
      projection,
8✔
718
      skip: row,
8✔
719
      take: tableData.length,
8✔
720
      fieldKeyType: FieldKeyType.Id,
8✔
721
    });
8✔
722

8✔
723
    const [numColsToExpand, numRowsToExpand] = this.calculateExpansion(tableSize, cell, [
8✔
724
      tableColCount,
8✔
725
      tableRowCount,
8✔
726
    ]);
8✔
727
    await expansionChecker?.(numColsToExpand, numRowsToExpand);
8!
728

8✔
729
    const updateRange: IPasteVo['ranges'] = [cell, cell];
8✔
730

8✔
731
    const expandColumns = await this.prismaService.$tx(async () => {
8✔
732
      // Expansion col
8✔
733
      return await this.expandColumns({
8✔
734
        tableId,
8✔
735
        header,
8✔
736
        numColsToExpand,
8✔
737
      });
8✔
738
    });
8✔
739

8✔
740
    await this.prismaService.$tx(async () => {
8✔
741
      const updateFields = effectFields.concat(expandColumns.map(createFieldInstanceByVo));
8✔
742

8✔
743
      // get all effect records, contains update and need create record
8✔
744
      const newRecords = await this.tableDataToRecords({
8✔
745
        tableId,
8✔
746
        tableData,
8✔
747
        headerFields: header.map(createFieldInstanceByVo),
8✔
748
        fields: updateFields,
8✔
749
      });
8✔
750

8✔
751
      // Warning: Update before creating
8✔
752
      // Fill cells
8✔
753
      const updateNewRecords = newRecords.slice(0, records.length);
8✔
754
      const updateRecordsRo = this.fillCells(records, updateNewRecords);
8✔
755
      await this.recordOpenApiService.updateRecords(tableId, updateRecordsRo);
8✔
756

8✔
757
      // create record
8✔
758
      if (numRowsToExpand) {
8✔
759
        const createNewRecords = newRecords.slice(records.length);
2✔
760
        const createRecordsRo = {
2✔
761
          fieldKeyType: FieldKeyType.Id,
2✔
762
          typecast: true,
2✔
763
          records: createNewRecords,
2✔
764
        };
2✔
765
        await this.recordOpenApiService.createRecords(tableId, createRecordsRo);
2✔
766
      }
2✔
767

8✔
768
      updateRange[1] = [col + updateFields.length - 1, row + updateFields.length - 1];
8✔
769
    });
8✔
770

8✔
771
    return updateRange;
8✔
772
  }
8✔
773

132✔
774
  async clear(tableId: string, rangesRo: IRangesRo) {
132✔
775
    const { fields, records } = await this.getSelectionCtxByRange(tableId, rangesRo);
2✔
776
    const fieldInstances = fields.map(createFieldInstanceByVo);
2✔
777
    const updateRecords = await this.tableDataToRecords({
2✔
778
      tableId,
2✔
779
      tableData: Array.from({ length: records.length }, () => []),
2✔
780
      fields: fieldInstances,
2✔
781
      headerFields: undefined,
2✔
782
    });
2✔
783
    const updateRecordsRo = this.fillCells(records, updateRecords);
2✔
784
    await this.recordOpenApiService.updateRecords(tableId, updateRecordsRo);
2✔
785
  }
2✔
786

132✔
787
  async delete(tableId: string, rangesRo: IRangesRo): Promise<IDeleteVo> {
132✔
788
    const { records } = await this.getSelectionCtxByRange(tableId, rangesRo);
8✔
789
    const recordIds = records.map(({ id }) => id);
8✔
790
    await this.recordOpenApiService.deleteRecords(tableId, recordIds);
8✔
791
    return { ids: recordIds };
8✔
792
  }
8✔
793
}
132✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc