Skip to content

Commit eb27c2f

Browse files
committed
Enhancement: Update rows via import
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent 720b69e commit eb27c2f

10 files changed

Lines changed: 147 additions & 32 deletions

File tree

‎lib/Db/Column.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ class Column extends EntitySuper implements JsonSerializable {
101101

102102
public const SUBTYPE_TEXT_LINE = 'line';
103103

104+
public const META_ID_TITLE = 'id';
105+
104106
protected ?string $title = null;
105107
protected ?int $tableId = null;
106108
protected ?string $createdBy = null;

‎lib/ResponseDefinitions.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@
138138
* matching_columns_count: int,
139139
* created_columns_count: int,
140140
* inserted_rows_count: int,
141+
* updated_rows_count: int,
141142
* errors_parsing_count: int,
142143
* errors_count: int,
143144
* }

‎lib/Service/ImportService.php‎

Lines changed: 67 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ class ImportService extends SuperService {
4848
private ?int $viewId = null;
4949
private array $columns = [];
5050
private bool $createUnknownColumns = true;
51+
private ?int $idColumnIndex = null;
5152
private int $countMatchingColumns = 0;
5253
private int $countCreatedColumns = 0;
5354
private int $countInsertedRows = 0;
55+
private int $countUpdatedRows = 0;
5456
private int $countErrors = 0;
5557
private int $countParsingErrors = 0;
5658

@@ -136,14 +138,19 @@ private function getPreviewData(Worksheet $worksheet): array {
136138
$column = $this->columns[$colIndex];
137139
$columns[] = $column;
138140
} else {
139-
$columns[] = [
141+
$column = [
140142
'title' => $title,
141143
'type' => $this->rawColumnDataTypes[$colIndex]['type'],
142144
'subtype' => $this->rawColumnDataTypes[$colIndex]['subtype'] ?? null,
143145
'numberDecimals' => $this->rawColumnDataTypes[$colIndex]['number_decimals'] ?? 0,
144146
'numberPrefix' => $this->rawColumnDataTypes[$colIndex]['number_prefix'] ?? '',
145147
'numberSuffix' => $this->rawColumnDataTypes[$colIndex]['number_suffix'] ?? '',
146148
];
149+
if (mb_strtolower($title) === Column::META_ID_TITLE) {
150+
$column['id'] = Column::TYPE_META_ID;
151+
}
152+
153+
$columns[] = $column;
147154
}
148155
}
149156

@@ -304,6 +311,7 @@ public function import(?int $tableId, ?int $viewId, string $path, bool $createMi
304311
'matching_columns_count' => $this->countMatchingColumns,
305312
'created_columns_count' => $this->countCreatedColumns,
306313
'inserted_rows_count' => $this->countInsertedRows,
314+
'updated_rows_count' => $this->countUpdatedRows,
307315
'errors_parsing_count' => $this->countParsingErrors,
308316
'errors_count' => $this->countErrors,
309317
];
@@ -334,7 +342,7 @@ private function loop(Worksheet $worksheet): void {
334342

335343
foreach ($worksheet->getRowIterator(2) as $row) {
336344
// parse row data
337-
$this->createRow($row);
345+
$this->upsertRow($row);
338346
}
339347
}
340348

@@ -367,25 +375,46 @@ private function parseValueByColumnType(string $value, Column $column): string {
367375
* @throws MultipleObjectsReturnedException
368376
* @throws NotFoundError
369377
*/
370-
private function createRow(Row $row): void {
378+
private function upsertRow(Row $row): void {
371379
$cellIterator = $row->getCellIterator();
372380
$cellIterator->setIterateOnlyExistingCells(false);
373381

374382
try {
375383
$i = -1;
376384
$data = [];
377385
$hasData = false;
386+
$id = null;
378387
foreach ($cellIterator as $cell) {
379388
$i++;
380389

390+
if ($this->idColumnIndex !== null && $i === $this->idColumnIndex) {
391+
// if this is the ID column, we need to get the ID from the cell
392+
if ($cell && $cell->getValue() !== null) {
393+
$id = $cell->getValue();
394+
}
395+
if ($id !== null && !is_numeric($id)) {
396+
$this->logger->warning('ID column value is not numeric: ' . $id);
397+
$this->countErrors++;
398+
return;
399+
}
400+
$id = (int)$id;
401+
continue;
402+
}
403+
404+
$columnKey = $i;
405+
if ($this->idColumnIndex !== null && $i > $this->idColumnIndex) {
406+
// if we have an ID column, we need to adjust the index
407+
$columnKey = $i - 1;
408+
}
409+
381410
// only add the dataset if column is known
382-
if (!isset($this->columns[$i]) || $this->columns[$i] === '') {
411+
if (!isset($this->columns[$columnKey]) || $this->columns[$columnKey] === '') {
383412
$this->logger->debug('Column unknown while fetching rows data for importing.');
384413
continue;
385414
}
386415

387416
/** @var Column $column */
388-
$column = $this->columns[$i];
417+
$column = $this->columns[$columnKey];
389418

390419
// if cell is empty
391420
if (!$cell || $cell->getValue() === null) {
@@ -426,26 +455,36 @@ private function createRow(Row $row): void {
426455
];
427456
}
428457

429-
if ($hasData) {
430-
$this->rowService->create($this->tableId, $this->viewId, $data);
431-
$this->countInsertedRows++;
432-
} else {
458+
if (!$hasData) {
433459
$this->logger->debug('Skipped empty row ' . $row->getRowIndex() . ' during import');
460+
return;
461+
}
462+
463+
if ($id) {
464+
try {
465+
$this->rowService->updateSet($id, $this->viewId, $data, $this->userId);
466+
$this->countUpdatedRows++;
467+
return;
468+
} catch (\Throwable $e) {
469+
$this->logger->warning('Error while updating row for import.', ['row_id' => $id, 'exception' => $e]);
470+
}
434471
}
472+
473+
$this->rowService->create($this->tableId, $this->viewId, $data);
474+
$this->countInsertedRows++;
435475
} catch (PermissionError $e) {
436-
$this->logger->error('Could not create row while importing, no permission.', ['exception' => $e]);
476+
$this->logger->error('Could not create/update row while importing, no permission.', ['exception' => $e]);
437477
$this->countErrors++;
438478
} catch (InternalError $e) {
439-
$this->logger->error('Error while creating new row for import.', ['exception' => $e]);
479+
$this->logger->error('Error while creating/updating new row for import.', ['exception' => $e]);
440480
$this->countErrors++;
441481
} catch (NotFoundError $e) {
442482
$this->logger->error($e->getMessage(), ['exception' => $e]);
443-
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage());
483+
throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage(), 0, $e);
444484
} catch (\Throwable $e) {
445485
$this->countErrors++;
446-
$this->logger->error('Error while creating new row for import.', ['exception' => $e]);
486+
$this->logger->error('Error while creating/updating new row for import.', ['exception' => $e]);
447487
}
448-
449488
}
450489

451490
private function valueToDateTimeImmutable(mixed $value): ?\DateTimeImmutable {
@@ -502,9 +541,23 @@ private function getColumns(Row $firstRow, Row $secondRow): void {
502541
if ($cell && $cell->getValue() !== null && $cell->getValue() !== '') {
503542
$title = $cell->getValue();
504543

544+
if (!$this->columnsConfig && mb_strtolower($title) === Column::META_ID_TITLE) {
545+
$this->idColumnIndex = $index;
546+
$secondRowCellIterator->next();
547+
$index++;
548+
continue;
549+
}
505550
if (isset($this->columnsConfig[$index]) && $this->columnsConfig[$index]['action'] === 'exist' && $this->columnsConfig[$index]['existColumn']) {
506551
$title = $this->columnsConfig[$index]['existColumn']['label'];
507552
$countMatchingColumnsFromConfig++;
553+
554+
// no need to create the ID (Meta) column as it used for update
555+
if ($this->columnsConfig[$index]['existColumn']['id'] === Column::TYPE_META_ID) {
556+
$this->idColumnIndex = $index;
557+
$secondRowCellIterator->next();
558+
$index++;
559+
continue;
560+
}
508561
}
509562
if (isset($this->columnsConfig[$index]) && $this->columnsConfig[$index]['action'] === 'new' && $this->createUnknownColumns) {
510563
$column = $this->columnService->create(

‎openapi.json‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@
269269
"matching_columns_count",
270270
"created_columns_count",
271271
"inserted_rows_count",
272+
"updated_rows_count",
272273
"errors_parsing_count",
273274
"errors_count"
274275
],
@@ -289,6 +290,10 @@
289290
"type": "integer",
290291
"format": "int64"
291292
},
293+
"updated_rows_count": {
294+
"type": "integer",
295+
"format": "int64"
296+
},
292297
"errors_parsing_count": {
293298
"type": "integer",
294299
"format": "int64"

‎src/modules/modals/ImportPreview.vue‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { ColumnTypes } from '../../shared/components/ncTable/mixins/columnHandle
7070
import { emit } from '@nextcloud/event-bus'
7171
import { useTablesStore } from '../../store/store.js'
7272
import { useDataStore } from '../../store/data.js'
73+
import { TYPE_META_ID } from '../../shared/constants.js'
7374
7475
export default {
7576
name: 'ImportPreview',
@@ -116,10 +117,17 @@ export default {
116117
return []
117118
}
118119
119-
return this.existingColumns.map(column => ({
120+
const columns = this.existingColumns.map(column => ({
120121
id: column.id,
121122
label: column.title,
122123
}))
124+
125+
columns.unshift({
126+
id: TYPE_META_ID,
127+
label: t('tables', 'ID (Meta)'),
128+
})
129+
130+
return columns
123131
},
124132
},
125133

‎src/modules/modals/ImportResults.vue‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@
3737
</td>
3838
</tr>
3939

40+
<tr>
41+
<td>{{ t('tables', 'Updated rows') }}</td>
42+
<td data-cy="importResultRowsUpdated">
43+
{{ results.updated_rows_count }}
44+
</td>
45+
</tr>
46+
4047
<tr>
4148
<td>{{ t('tables', 'Value parsing errors') }}</td>
4249
<td data-cy="importResultParsingErrors">

‎src/shared/components/ncTable/mixins/exportTableMixin.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default {
2121

2222
const data = []
2323
rows.forEach(row => {
24-
const rowData = {}
24+
const rowData = { ID: row.id }
2525
columns.forEach(column => {
2626
// if a normal column
2727
if (column.id >= 0) {

‎src/types/openapi/openapi.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,8 @@ export type components = {
814814
/** Format: int64 */
815815
readonly inserted_rows_count: number;
816816
/** Format: int64 */
817+
readonly updated_rows_count: number;
818+
/** Format: int64 */
817819
readonly errors_parsing_count: number;
818820
/** Format: int64 */
819821
readonly errors_count: number;

‎tests/integration/features/APIv1.feature‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,38 @@ Feature: APIv1
248248
| import-from-ms365.xlsx |
249249
| import-from-libreoffice.csv |
250250

251+
@api1 @import @rows
252+
Scenario: Import a document file that updates existing rows
253+
Given table "Import check" with emoji "👨🏻‍💻" exists for user "participant1" as "base1"
254+
Then column "one" exists with following properties
255+
| type | text |
256+
| subtype | line |
257+
| mandatory | 1 |
258+
| description | This is a description! |
259+
Then column "two" exists with following properties
260+
| type | number |
261+
| mandatory | 1 |
262+
| description | This is a description! |
263+
Then row exists with following values
264+
| one | AHA |
265+
| two | 88 |
266+
Given file "update-rows.csv" exists for user "participant1" with the following data
267+
| ID | one | two |
268+
| {rowId} | AHA updated | 99 |
269+
| | new row | 100 |
270+
When user imports file "update-rows.csv" into last created table
271+
Then import results have the following data
272+
| found_columns_count | 2 |
273+
| matching_columns_count | 2 |
274+
| created_columns_count | 0 |
275+
| inserted_rows_count | 1 |
276+
| updated_rows_count | 1 |
277+
| errors_count | 0 |
278+
Then table contains at least following rows
279+
| one | two |
280+
| AHA updated | 99 |
281+
| new row | 100 |
282+
251283
@api1 @import
252284
Scenario: Import a document with optional field
253285
Given user "participant1" uploads file "import-from-libreoffice-optional-fields.csv"

‎tests/integration/features/bootstrap/FeatureContext.php‎

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -489,35 +489,40 @@ public function uploadFile(string $user, string $file): void {
489489
// IMPORT --------------------------
490490

491491
/**
492-
* @Given file :file exists for user :user with following data
492+
* @Given file :file exists for user :user with the following data
493493
*
494494
* @param string $user
495495
* @param string $file
496-
* @param TableNode|null $table
496+
* @param TableNode $table
497497
*/
498-
public function createCsvFile(string $user, string $file, ?TableNode $table = null): void {
498+
public function createCsvFile(string $user, string $file, TableNode $table): void {
499499
$this->setCurrentUser($user);
500-
$url = $this->baseUrl . 'remote.php/dav/files/' . $user . $file;
501-
$body = $this->tableNodeToCsv($table);
502-
$headers = ['Content-Type' => 'text/csv'];
503500

504-
$this->sendRequestFullUrl('PUT', $url, $body, $headers, []);
501+
$url = sprintf('%sremote.php/dav/files/%s/%s', $this->baseUrl, $user, $file);
502+
$body = Utils::streamFor($this->tableNodeToCsv($table));
503+
504+
$this->sendRequestFullUrl('PUT', $url, $body);
505505

506506
Assert::assertEquals(201, $this->response->getStatusCode());
507507
}
508508

509509
private function tableNodeToCsv(TableNode $node): string {
510-
$out = '';
510+
$file = new \SplTempFileObject();
511511
foreach ($node->getRows() as $row) {
512-
foreach ($row as $value) {
513-
if ($out !== '' && substr($out, -1) !== "\n") {
514-
$out .= ',';
515-
}
516-
$out .= trim($value);
517-
}
518-
$out .= "\n";
512+
$fields = array_map(function ($cell) {
513+
return str_replace('{rowId}', $this->rowId, $cell);
514+
}, $row);
515+
$file->fputcsv($fields);
519516
}
520-
return $out;
517+
518+
$file->rewind();
519+
520+
$lines = '';
521+
while (!$file->eof()) {
522+
$lines .= $file->fgets();
523+
}
524+
525+
return $lines;
521526
}
522527

523528
/**

0 commit comments

Comments
 (0)