From ef3db38541539bd4b21edd7df1dfaeb3c2ccbe56 Mon Sep 17 00:00:00 2001 From: Kostiantyn Miakshyn Date: Fri, 31 Oct 2025 15:00:49 +0100 Subject: [PATCH] Enhancement: Make tables import asynchronous Signed-off-by: Kostiantyn Miakshyn --- REUSE.toml | 2 +- appinfo/routes.php | 11 +- cypress/e2e/tables-import.cy.js | 89 +- cypress/e2e/tables-table.cy.js | 6 +- cypress/fixtures/test-import-large.csv | 1101 +++++++++++++++++ ...{test-import.csv => test-import-small.csv} | 0 lib/Activity/ActivityManager.php | 34 +- lib/Activity/TablesProvider.php | 47 +- lib/BackgroundJob/ImportTableJob.php | 83 ++ lib/Controller/ImportController.php | 85 +- lib/Db/Row2Mapper.php | 14 +- lib/Model/ImportStats.php | 21 + lib/Service/ColumnService.php | 5 +- lib/Service/ImportService.php | 209 +++- lib/Service/PermissionsService.php | 2 +- lib/Service/RowService.php | 29 +- src/modules/modals/FileActionImport.vue | 13 +- src/modules/modals/Import.vue | 41 +- src/modules/modals/ImportResults.vue | 28 +- 19 files changed, 1728 insertions(+), 92 deletions(-) create mode 100644 cypress/fixtures/test-import-large.csv rename cypress/fixtures/{test-import.csv => test-import-small.csv} (100%) create mode 100644 lib/BackgroundJob/ImportTableJob.php create mode 100644 lib/Model/ImportStats.php diff --git a/REUSE.toml b/REUSE.toml index cd7320c4be..13b3297281 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -66,7 +66,7 @@ SPDX-FileCopyrightText = "2017 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" [[annotations]] -path = ["cypress/styleguide/assets/img/filetypes/font.svg", "cypress/styleguide/assets/img/actions/star-rounded.svg", "cypress/fixtures/example.json", "cypress/fixtures/NC_server_test.pdf", "cypress/fixtures/photo-test-1.jpeg", "cypress/fixtures/test-import.csv", "playwright/fixtures/example.json", "playwright/fixtures/NC_server_test.pdf", "playwright/fixtures/photo-test-1.jpeg", "playwright/fixtures/test-import.csv", "docs/permissions-matrix.xlsx", "tests/stub.phpstub", "tests/integration/composer.json", "tests/integration/composer.lock", "tests/integration/base-query-count.txt"] +path = ["cypress/styleguide/assets/img/filetypes/font.svg", "cypress/styleguide/assets/img/actions/star-rounded.svg", "cypress/fixtures/example.json", "cypress/fixtures/NC_server_test.pdf", "cypress/fixtures/photo-test-1.jpeg", "cypress/fixtures/test-import-large.csv", "cypress/fixtures/test-import-small.csv", "playwright/fixtures/example.json", "playwright/fixtures/NC_server_test.pdf", "playwright/fixtures/photo-test-1.jpeg", "playwright/fixtures/test-import.csv", "docs/permissions-matrix.xlsx", "tests/stub.phpstub", "tests/integration/composer.json", "tests/integration/composer.lock", "tests/integration/base-query-count.txt"] precedence = "aggregate" SPDX-FileCopyrightText = "2023 Nextcloud GmbH and Nextcloud contributors" SPDX-License-Identifier = "AGPL-3.0-or-later" diff --git a/appinfo/routes.php b/appinfo/routes.php index c12bdcc82e..c8046c0e9c 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -104,13 +104,18 @@ // import ['name' => 'import#previewImportTable', 'url' => '/import-preview/table/{tableId}', 'verb' => 'POST'], - ['name' => 'import#importInTable', 'url' => '/import/table/{tableId}', 'verb' => 'POST'], + ['name' => 'import#importV2InTable', 'url' => '/v2/import/table/{tableId}', 'verb' => 'POST'], + ['name' => 'import#importV2InView', 'url' => '/v2/import/view/{viewId}', 'verb' => 'POST'], ['name' => 'import#previewImportView', 'url' => '/import-preview/view/{viewId}', 'verb' => 'POST'], - ['name' => 'import#importInView', 'url' => '/import/view/{viewId}', 'verb' => 'POST'], ['name' => 'import#previewUploadImportTable', 'url' => '/importupload-preview/table/{tableId}', 'verb' => 'POST'], - ['name' => 'import#importUploadInTable', 'url' => '/importupload/table/{tableId}', 'verb' => 'POST'], ['name' => 'import#previewUploadImportView', 'url' => '/importupload-preview/view/{viewId}', 'verb' => 'POST'], + ['name' => 'import#importV2UploadInTable', 'url' => '/v2/importupload/table/{tableId}', 'verb' => 'POST'], + ['name' => 'import#importV2UploadInView', 'url' => '/v2/importupload/view/{viewId}', 'verb' => 'POST'], + // deprecated endpoints + ['name' => 'import#importUploadInTable', 'url' => '/importupload/table/{tableId}', 'verb' => 'POST'], ['name' => 'import#importUploadInView', 'url' => '/importupload/view/{viewId}', 'verb' => 'POST'], + ['name' => 'import#importInTable', 'url' => '/import/table/{tableId}', 'verb' => 'POST'], + ['name' => 'import#importInView', 'url' => '/import/view/{viewId}', 'verb' => 'POST'], // search ['name' => 'search#all', 'url' => '/search/all', 'verb' => 'GET'], diff --git a/cypress/e2e/tables-import.cy.js b/cypress/e2e/tables-import.cy.js index ea02e7c874..d98e280514 100644 --- a/cypress/e2e/tables-import.cy.js +++ b/cypress/e2e/tables-import.cy.js @@ -10,7 +10,8 @@ describe('Import csv', () => { cy.createRandomUser().then(user => { localUser = user cy.login(localUser) - cy.uploadFile('test-import.csv', 'text/csv') + cy.uploadFile('test-import-small.csv', 'text/csv') + cy.uploadFile('test-import-large.csv', 'text/csv') }) }) @@ -19,11 +20,11 @@ describe('Import csv', () => { cy.visit('apps/tables') }) - it('Import csv from Files', () => { + it('Import small csv from Files', () => { cy.loadTable('Welcome to Nextcloud Tables!') cy.clickOnTableThreeDotMenu('Import') cy.get('.modal__content button').contains('Select from Files').click() - cy.get('.file-picker__files').contains('test-import').click() + cy.get('.file-picker__files').contains('test-import-small').click() cy.get('.file-picker button span').contains('Import').click() cy.get('.modal__content .import-filename', { timeout: 5000 }).should('be.visible') @@ -32,7 +33,7 @@ describe('Import csv', () => { cy.wait('@importPreviewPath') cy.get('.file_import__preview tbody tr', { timeout: 20000 }).should('have.length', 4) - cy.intercept({ method: 'POST', url: '**/apps/tables/import/table/*' }).as('importUploadReq') + cy.intercept({ method: 'POST', url: '**/apps/tables/v2/import/table/*' }).as('importUploadReq') cy.get('.modal__content button').contains('Import').click() cy.wait('@importUploadReq') cy.get('[data-cy="importResultColumnsFound"]').should('contain.text', '4') @@ -43,18 +44,36 @@ describe('Import csv', () => { cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0') }) - it('Import csv from device', () => { + it('Import large csv from Files', () => { + cy.loadTable('Welcome to Nextcloud Tables!') + cy.clickOnTableThreeDotMenu('Import') + cy.get('.modal__content button').contains('Select from Files').click() + cy.get('.file-picker__files').contains('test-import-large').click() + cy.get('.file-picker button span').contains('Import').click() + cy.get('.modal__content .import-filename', { timeout: 5000 }).should('be.visible') + + cy.intercept({ method: 'POST', url: '**/apps/tables/import-preview/**' }).as('importPreviewPath') + cy.get('.modal__content button').contains('Preview').click() + cy.wait('@importPreviewPath') + cy.get('.file_import__preview tbody tr', { timeout: 20000 }).should('have.length', 4) + + cy.intercept({ method: 'POST', url: '**/apps/tables/v2/import/table/*' }).as('importUploadReq') + cy.get('.modal__content button').contains('Import').click() + cy.wait('@importUploadReq').its('response.statusCode').should('equal', 200) + }) + + it('Import small csv from device', () => { cy.loadTable('Welcome to Nextcloud Tables!') cy.clickOnTableThreeDotMenu('Import') cy.get('.modal__content button').contains('Upload from device').click() - cy.get('input[type="file"]').selectFile('cypress/fixtures/test-import.csv', { force: true }) + cy.get('input[type="file"]').selectFile('cypress/fixtures/test-import-small.csv', { force: true }) cy.intercept({ method: 'POST', url: '**/apps/tables/importupload-preview/**' }).as('importPreviewUpload') cy.get('.modal__content button').contains('Preview').click() cy.wait('@importPreviewUpload') cy.get('.file_import__preview tbody tr', { timeout: 20000 }).should('have.length', 4) - cy.intercept({ method: 'POST', url: '**/apps/tables/importupload/table/*' }).as('importUploadReq') + cy.intercept({ method: 'POST', url: '**/apps/tables/v2/importupload/table/*' }).as('importUploadReq') cy.get('.modal__content button').contains('Import').click() cy.wait('@importUploadReq') cy.get('[data-cy="importResultColumnsFound"]', { timeout: 20000 }).should('contain.text', '4') @@ -65,6 +84,22 @@ describe('Import csv', () => { cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0') }) + it('Import large csv from device', () => { + cy.loadTable('Welcome to Nextcloud Tables!') + cy.clickOnTableThreeDotMenu('Import') + cy.get('.modal__content button').contains('Upload from device').click() + cy.get('input[type="file"]').selectFile('cypress/fixtures/test-import-large.csv', { force: true }) + + cy.intercept({ method: 'POST', url: '**/apps/tables/importupload-preview/**' }).as('importPreviewUpload') + cy.get('.modal__content button').contains('Preview').click() + cy.wait('@importPreviewUpload') + cy.get('.file_import__preview tbody tr', { timeout: 20000 }).should('have.length', 4) + + cy.intercept({ method: 'POST', url: '**/apps/tables/v2/importupload/table/*' }).as('importUploadReq') + cy.get('.modal__content button').contains('Import').click() + cy.wait('@importUploadReq').its('response.statusCode').should('equal', 200) + }) + it('Import csv from device with updating of existent files', () => { cy.intercept({ method: 'GET', url: '**/apps/tables/row/table/*' }).as('rowsReq') @@ -89,7 +124,7 @@ describe('Import csv', () => { cy.wait('@importPreviewUpdate') cy.get('.file_import__preview tbody tr', { timeout: 20000 }).should('have.length', 3) - cy.intercept({ method: 'POST', url: '**/apps/tables/importupload/table/*' }).as('importUploadReq') + cy.intercept({ method: 'POST', url: '**/apps/tables/v2/importupload/table/*' }).as('importUploadReq') cy.get('.modal__content button').contains('Import').click() cy.wait('@importUploadReq') cy.get('[data-cy="importResultColumnsFound"]', { timeout: 20000 }).should('contain.text', '2') @@ -104,10 +139,9 @@ describe('Import csv', () => { }) describe('Import csv from Files file action', () => { - const rowActionsButton = '[data-cy-files-list-row-name="test-import.csv"] [data-cy-files-list-row-actions] .action-item button' const importToTablesAction = '[data-cy-files-list-row-action="import-to-tables"]' - - const openImportToTablesAction = (retries = 8) => { + const openImportToTablesAction = (file, retries = 8) => { + const rowActionsButton = `[data-cy-files-list-row-name="${file}"] [data-cy-files-list-row-actions] .action-item button` cy.get(rowActionsButton).click() cy.get('body').then($body => { if ($body.find(importToTablesAction).length > 0) { @@ -116,12 +150,12 @@ describe('Import csv from Files file action', () => { } if (retries <= 0) { - throw new Error('Import to Tables action did not become available in file actions menu') + throw new Error(`Import to Tables action did not become available in file actions menu for file: ${file}`) } cy.get('body').click(0, 0) cy.wait(1000) - openImportToTablesAction(retries - 1) + openImportToTablesAction(file, retries - 1) }) } @@ -129,22 +163,24 @@ describe('Import csv from Files file action', () => { cy.createRandomUser().then(user => { localUser = user cy.login(localUser) - cy.uploadFile('test-import.csv', 'text/csv') + cy.uploadFile('test-import-small.csv', 'text/csv') + cy.uploadFile('test-import-large.csv', 'text/csv') }) }) beforeEach(function() { cy.login(localUser) cy.visit('apps/files/files', { timeout: 120000 }) - cy.get('[data-cy-files-list-row-name="test-import.csv"]', { timeout: 120000 }).should('be.visible') + cy.get('[data-cy-files-list-row-name="test-import-small.csv"]', { timeout: 120000 }).should('be.visible') + cy.get('[data-cy-files-list-row-name="test-import-large.csv"]', { timeout: 120000 }).should('be.visible') }) - it('Import to new table', () => { - openImportToTablesAction() + it('Import small file to new table', () => { + openImportToTablesAction('test-import-small.csv') cy.intercept({ method: 'POST', - url: '**/apps/tables/import/table/*', + url: '**/apps/tables/v2/import/table/*', }).as('importNewTableReq') cy.get('[data-cy="fileActionImportButton"]').click({ force: true }) cy.wait('@importNewTableReq').its('response.statusCode').should('equal', 200) @@ -157,8 +193,19 @@ describe('Import csv from Files file action', () => { cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0') }) - it('Import to existing table', () => { - openImportToTablesAction() + it('Import large file to new table', () => { + openImportToTablesAction('test-import-large.csv') + + cy.intercept({ + method: 'POST', + url: '**/apps/tables/v2/import/table/*', + }).as('importNewTableReq') + cy.get('[data-cy="fileActionImportButton"]').click({ force: true }) + cy.wait('@importNewTableReq').its('response.statusCode').should('equal', 200) + }) + + it('Import small file to existing table', () => { + openImportToTablesAction('test-import-small.csv') cy.get('.modal__content [data-cy="importAsNewTableSwitch"] input').uncheck({ force: true }) cy.get('[data-cy="selectExistingTableDropdown"]').type('Welcome to Nextcloud Tables!') @@ -166,7 +213,7 @@ describe('Import csv from Files file action', () => { cy.intercept({ method: 'POST', - url: '**/apps/tables/import/table/*', + url: '**/apps/tables/v2/import/table/*', }).as('importExistingTableReq') cy.get('[data-cy="fileActionImportButton"]').click({ force: true }) cy.wait('@importExistingTableReq').its('response.statusCode').should('equal', 200) diff --git a/cypress/e2e/tables-table.cy.js b/cypress/e2e/tables-table.cy.js index a2f8c17b1f..6f526759f4 100644 --- a/cypress/e2e/tables-table.cy.js +++ b/cypress/e2e/tables-table.cy.js @@ -37,7 +37,7 @@ describe('Manage a table', () => { }) it('Create with import', () => { - cy.uploadFile('test-import.csv', 'text/csv') + cy.uploadFile('test-import-small.csv', 'text/csv') cy.get('.icon-loading').should('not.exist') cy.get('[data-cy="navigationCreateTableIcon"]').click({ force: true }) // should type before selecting the table type tile @@ -47,11 +47,11 @@ describe('Manage a table', () => { cy.contains('h2', 'Import').should('be.visible') cy.get('.modal__content button').contains('Select from Files').click() - cy.get('.file-picker__files').contains('test-import').click() + cy.get('.file-picker__files').contains('test-import-small').click() cy.get('.file-picker button span').contains('Import').click() cy.get('.modal__content button').contains('Preview').click() cy.get('.file_import__preview tbody tr').should('have.length', 4) - cy.intercept({ method: 'POST', url: '**/apps/tables/import/table/*' }).as('importUploadReq') + cy.intercept({ method: 'POST', url: '**/apps/tables/v2/import/table/*' }).as('importUploadReq') cy.get('.modal__content button').contains('Import').scrollIntoView().click() cy.wait('@importUploadReq') cy.get('[data-cy="importResultColumnsFound"]').should('contain.text', '4') diff --git a/cypress/fixtures/test-import-large.csv b/cypress/fixtures/test-import-large.csv new file mode 100644 index 0000000000..a11f445cc8 --- /dev/null +++ b/cypress/fixtures/test-import-large.csv @@ -0,0 +1,1101 @@ +What,How to do,Ease of use,Done +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False +I1,I2,2,True +J1,J2,3,False +K1,K2,4,True +L1,L2,5,False +M1,M2,0,True +N1,N2,1,False +O1,O2,2,True +P1,P2,3,False +Q1,Q2,4,True +R1,R2,5,False +S1,S2,0,True +T1,T2,1,False +U1,U2,2,True +V1,V2,3,False +W1,W2,4,True +X1,X2,5,False +Y1,Y2,0,True +Z1,Z2,1,False +A1,A2,2,True +B1,B2,3,False +C1,C2,4,True +D1,D2,5,False +E1,E2,0,True +F1,F2,1,False +G1,G2,2,True +H1,H2,3,False +I1,I2,4,True +J1,J2,5,False +K1,K2,0,True +L1,L2,1,False +M1,M2,2,True +N1,N2,3,False +O1,O2,4,True +P1,P2,5,False +Q1,Q2,0,True +R1,R2,1,False +S1,S2,2,True +T1,T2,3,False +U1,U2,4,True +V1,V2,5,False +W1,W2,0,True +X1,X2,1,False +Y1,Y2,2,True +Z1,Z2,3,False +A1,A2,4,True +B1,B2,5,False +C1,C2,0,True +D1,D2,1,False +E1,E2,2,True +F1,F2,3,False +G1,G2,4,True +H1,H2,5,False +I1,I2,0,True +J1,J2,1,False +K1,K2,2,True +L1,L2,3,False +M1,M2,4,True +N1,N2,5,False +O1,O2,0,True +P1,P2,1,False +Q1,Q2,2,True +R1,R2,3,False +S1,S2,4,True +T1,T2,5,False +U1,U2,0,True +V1,V2,1,False +W1,W2,2,True +X1,X2,3,False +Y1,Y2,4,True +Z1,Z2,5,False +A1,A2,0,True +B1,B2,1,False +C1,C2,2,True +D1,D2,3,False +E1,E2,4,True +F1,F2,5,False +G1,G2,0,True +H1,H2,1,False diff --git a/cypress/fixtures/test-import.csv b/cypress/fixtures/test-import-small.csv similarity index 100% rename from cypress/fixtures/test-import.csv rename to cypress/fixtures/test-import-small.csv diff --git a/lib/Activity/ActivityManager.php b/lib/Activity/ActivityManager.php index 80f9d92bd0..fd54a40b88 100644 --- a/lib/Activity/ActivityManager.php +++ b/lib/Activity/ActivityManager.php @@ -35,6 +35,8 @@ class ActivityManager { public const SUBJECT_ROW_UPDATE = 'row_update'; public const SUBJECT_ROW_DELETE = 'row_delete'; + public const SUBJECT_IMPORT_FINISHED = 'import_finished'; + public function __construct( private readonly IManager $manager, private readonly IFactory $l10nFactory, @@ -135,9 +137,11 @@ private function createEvent($objectType, $object, $subject, $additionalParams = case self::SUBJECT_ROW_DELETE: $subjectParams['row'] = $object; break; - default: - throw new \Exception('Unknown subject for activity.'); + case self::SUBJECT_IMPORT_FINISHED: + $subjectParams['importStats'] = $additionalParams['importStats'] ?? null; break; + default: + throw new \Exception(sprintf('Unknown subject "%s" for activity.', $subject)); } if ($subject === self::SUBJECT_ROW_UPDATE) { @@ -200,7 +204,7 @@ private function sendToUsers(IEvent $event, $object) { } } - public function getActivityFormat($language, $subjectIdentifier, $subjectParams = [], $ownActivity = false) { + public function getActivitySubject($language, $subjectIdentifier, $subjectParams = [], $ownActivity = false) { $subject = ''; $l = $this->l10nFactory->get(Application::APP_ID, $language); @@ -248,10 +252,34 @@ public function getActivityFormat($language, $subjectIdentifier, $subjectParams case self::SUBJECT_ROW_DELETE: $subject = $ownActivity ? $l->t('You have deleted the row {row} in table {table}') : $l->t('{user} has deleted the row {row} in table {table}'); break; + case self::SUBJECT_IMPORT_FINISHED: + $subject = $ownActivity ? $l->t('You have imported file to table {table}') : $l->t('{user} has imported file to table {table}'); + break; default: break; } return $subject; } + + public function getActivityMessage($language, $subjectIdentifier) { + $l = $this->l10nFactory->get(Application::APP_ID, $language); + + switch ($subjectIdentifier) { + case self::SUBJECT_IMPORT_FINISHED: + $lines = [ + $l->t('Found columns: {foundColumnsCount}'), + $l->t('Matching columns: {matchingColumnsCount}'), + $l->t('Created columns: {createdColumnsCount}'), + $l->t('Inserted rows: {insertedRowsCount}'), + $l->t('Updated rows: {updatedRowsCount}'), + $l->t('Value parsing errors: {errorsParsingCount}'), + $l->t('Row creation errors: {errorsCount}'), + ]; + return implode("\n", $lines); + + default: + return null; + } + } } diff --git a/lib/Activity/TablesProvider.php b/lib/Activity/TablesProvider.php index 246efb7c24..2ec665acf0 100644 --- a/lib/Activity/TablesProvider.php +++ b/lib/Activity/TablesProvider.php @@ -12,6 +12,7 @@ use OCP\Activity\IProvider; use OCP\IURLGenerator; use OCP\IUserManager; +use Psr\Log\LoggerInterface; class TablesProvider implements IProvider { @@ -20,6 +21,7 @@ public function __construct( private IURLGenerator $urlGenerator, private ActivityManager $activityManager, private IUserManager $userManager, + private LoggerInterface $logger, ) { } @@ -40,7 +42,7 @@ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): $user = $this->userManager->get($author); if ($user !== null) { - $params = [ + $subjectParameters = [ 'user' => [ 'type' => 'user', 'id' => $author, @@ -49,7 +51,7 @@ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): ]; $event->setAuthor($author); } else { - $params = [ + $subjectParameters = [ 'user' => [ 'type' => 'user', 'id' => 'deleted_users', @@ -65,7 +67,7 @@ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): 'name' => $event->getObjectName(), 'link' => $this->tablesUrl('/table/' . $event->getObjectId()), ]; - $params['table'] = $table; + $subjectParameters['table'] = $table; $event->setLink($this->tablesUrl('/table/' . $event->getObjectId())); } @@ -76,19 +78,19 @@ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): 'name' => (string)$subjectParams['table']['title'], 'link' => $this->tablesUrl('/table/' . $subjectParams['table']['id']), ]; - $params['table'] = $table; + $subjectParameters['table'] = $table; $row = [ 'type' => 'highlight', 'id' => (string)$event->getObjectId(), 'name' => '#' . $event->getObjectId(), 'link' => $this->tablesUrl('/table/' . $subjectParams['table']['id'] . '/row/' . $event->getObjectId()), ]; - $params['row'] = $row; + $subjectParameters['row'] = $row; $event->setLink($this->tablesUrl('/table/' . $subjectParams['table']['id'] . '/row/' . $event->getObjectId())); if ($event->getSubject() === ActivityManager::SUBJECT_ROW_UPDATE) { foreach ($subjectParams['changeCols'] as $changeCol) { - $params['col-' . $changeCol['id']] = [ + $subjectParameters['col-' . $changeCol['id']] = [ 'type' => 'highlight', 'id' => (string)$changeCol['id'], 'name' => $changeCol['name'] ?? '', @@ -98,7 +100,7 @@ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): } if (array_key_exists('before', $subjectParams) && is_string($subjectParams['before'])) { - $params['before'] = [ + $subjectParameters['before'] = [ 'type' => 'highlight', 'id' => $subjectParams['before'], 'name' => $subjectParams['before'] ?? '' @@ -106,17 +108,30 @@ public function parse($language, IEvent $event, ?IEvent $previousEvent = null): } if (array_key_exists('after', $subjectParams)) { - $params['after'] = [ + $subjectParameters['after'] = [ 'type' => 'highlight', 'id' => (string)$subjectParams['after'], 'name' => $subjectParams['after'] ?? '' ]; } + $messageParameters = []; + if ($event->getSubject() === ActivityManager::SUBJECT_IMPORT_FINISHED) { + $messageParameters['{foundColumnsCount}'] = $subjectParams['importStats']['foundColumnsCount']; + $messageParameters['{matchingColumnsCount}'] = $subjectParams['importStats']['matchingColumnsCount']; + $messageParameters['{createdColumnsCount}'] = $subjectParams['importStats']['createdColumnsCount']; + $messageParameters['{insertedRowsCount}'] = $subjectParams['importStats']['insertedRowsCount']; + $messageParameters['{updatedRowsCount}'] = $subjectParams['importStats']['updatedRowsCount']; + $messageParameters['{errorsParsingCount}'] = $subjectParams['importStats']['errorsParsingCount']; + $messageParameters['{errorsCount}'] = $subjectParams['importStats']['errorsCount']; + } + try { - $subject = $this->activityManager->getActivityFormat($language, $subjectIdentifier, $subjectParams, $ownActivity); - $this->setSubjects($event, $subject, $params); + $subject = $this->activityManager->getActivitySubject($language, $subjectIdentifier, $subjectParams, $ownActivity); + $message = $this->activityManager->getActivityMessage($language, $subjectIdentifier); + $this->parseEvent($event, $subject, $subjectParameters, $message, $messageParameters); } catch (\Exception $e) { + $this->logger->warning('Could not parse activity: ' . $e->getMessage(), ['exception' => $e]); } return $event; @@ -144,10 +159,10 @@ private function tablesUrl(string $endpoint) { return $this->urlGenerator->linkToRouteAbsolute('tables.page.index') . '#/' . trim($endpoint, '/'); } - private function setSubjects(IEvent $event, $subject, array $parameters) { + private function parseEvent(IEvent $event, string $subject, array $subjectParameters, ?string $message, array $messageParameters = []) { $placeholders = $replacements = $richParameters = []; - foreach ($parameters as $placeholder => $parameter) { + foreach ($subjectParameters as $placeholder => $parameter) { $placeholders[] = '{' . $placeholder . '}'; if (is_array($parameter) && array_key_exists('name', $parameter)) { $replacements[] = $parameter['name']; @@ -157,8 +172,12 @@ private function setSubjects(IEvent $event, $subject, array $parameters) { } } - $event->setParsedSubject(str_replace($placeholders, $replacements, $subject)) + $event->setSubject($subject, $subjectParameters) + ->setParsedSubject(str_replace($placeholders, $replacements, $subject)) ->setRichSubject($subject, $richParameters); - $event->setSubject($subject, $parameters); + + if ($message) { + $event->setParsedMessage(strtr($message, $messageParameters)); + } } } diff --git a/lib/BackgroundJob/ImportTableJob.php b/lib/BackgroundJob/ImportTableJob.php new file mode 100644 index 0000000000..cf63446f05 --- /dev/null +++ b/lib/BackgroundJob/ImportTableJob.php @@ -0,0 +1,83 @@ +userSession->getUser(); + + try { + $user = $this->userManager->get($userId); + $this->userSession->setUser($user); + + $importType = $argument['user_file_path'] ? ImportService::IMPORT_TYPE_USER_FILE : ImportService::IMPORT_TYPE_APP_FILE; + $path = $argument['user_file_path'] ?: $argument['import_file_name']; + + $importStats = $this->importService + ->importV2( + $userId, + $importType, + $path, + $tableId, + $viewId, + $argument['create_missing_columns'], + $argument['columns_config'] + ); + } finally { + $this->userSession->setUser($oldUser); + } + + if (!$tableId && $viewId) { + $tableId = $this->viewMapper->find($viewId)->getTableId(); + } + + $this->activityManager->triggerEvent( + objectType: ActivityManager::TABLES_OBJECT_TABLE, + object: $this->tableMapper->find($tableId), + subject: ActivityManager::SUBJECT_IMPORT_FINISHED, + additionalParams: [ + 'importStats' => $importStats, + ], + author: $userId + ); + } +} diff --git a/lib/Controller/ImportController.php b/lib/Controller/ImportController.php index a491c8b90d..2159d29282 100644 --- a/lib/Controller/ImportController.php +++ b/lib/Controller/ImportController.php @@ -42,7 +42,6 @@ class ImportController extends Controller { use Errors; - public function __construct( IRequest $request, LoggerInterface $logger, @@ -65,6 +64,9 @@ public function previewImportTable(int $tableId, String $path): DataResponse { }); } + /** + * @deprecated Use {@link importV2InTable} instead + */ #[NoAdminRequired] #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')] public function importInTable(int $tableId, String $path, bool $createMissingColumns = true, array $columnsConfig = []): DataResponse { @@ -74,6 +76,21 @@ public function importInTable(int $tableId, String $path, bool $createMissingCol }); } + #[NoAdminRequired] + #[UserRateLimit(limit: 20, period: 60)] + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')] + public function importV2InTable(int $tableId, String $path, bool $createMissingColumns = true, array $columnsConfig = []): DataResponse { + return $this->handleError(function () use ($tableId, $path, $createMissingColumns, $columnsConfig) { + // minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer + if ($this->service->shouldImportAsync($path)) { + $this->service->scheduleImport($tableId, null, $path, $createMissingColumns, $columnsConfig); + return ['async' => true]; + } + $result = $this->service->importV2($this->userId, ImportService::IMPORT_TYPE_USER_FILE, $path, $tableId, null, $createMissingColumns, $columnsConfig); + return ['async' => false, 'result' => $result]; + }); + } + #[NoAdminRequired] #[UserRateLimit(limit: 20, period: 60)] #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')] @@ -83,6 +100,9 @@ public function previewImportView(int $viewId, String $path): DataResponse { }); } + /** + * @deprecated Use {@link importV2InView} instead + */ #[NoAdminRequired] #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')] public function importInView(int $viewId, String $path, bool $createMissingColumns = true, array $columnsConfig = []): DataResponse { @@ -92,6 +112,21 @@ public function importInView(int $viewId, String $path, bool $createMissingColum }); } + #[NoAdminRequired] + #[UserRateLimit(limit: 20, period: 60)] + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')] + public function importV2InView(int $viewId, String $path, bool $createMissingColumns = true, array $columnsConfig = []): DataResponse { + return $this->handleError(function () use ($viewId, $path, $createMissingColumns, $columnsConfig) { + // minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer + if ($this->service->shouldImportAsync($path)) { + $this->service->scheduleImport(null, $viewId, $path, $createMissingColumns, $columnsConfig); + return ['async' => true]; + } + $result = $this->service->importV2($this->userId, ImportService::IMPORT_TYPE_USER_FILE, $path, null, $viewId, $createMissingColumns, $columnsConfig); + return ['async' => false, 'result' => $result]; + }); + } + #[NoAdminRequired] #[UserRateLimit(limit: 20, period: 60)] #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')] @@ -107,6 +142,9 @@ public function previewUploadImportTable(int $tableId): DataResponse { } } + /** + * @deprecated Use {@link importV2UploadInTable} instead + */ #[NoAdminRequired] #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')] public function importUploadInTable(int $tableId, bool $createMissingColumns = true, string $columnsConfig = ''): DataResponse { @@ -138,6 +176,28 @@ public function previewUploadImportView(int $viewId): DataResponse { } } + #[NoAdminRequired] + #[UserRateLimit(limit: 20, period: 60)] + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_TABLE, idParam: 'tableId')] + public function importV2UploadInTable(int $tableId, bool $createMissingColumns = true, string $columnsConfig = ''): DataResponse { + try { + $columnsConfigArray = json_decode($columnsConfig, true); + $file = $this->getUploadedFile('uploadfile'); + return $this->handleError(function () use ($tableId, $file, $createMissingColumns, $columnsConfigArray) { + // minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer + if ($this->service->shouldImportAsync($file['tmp_name'])) { + $this->service->scheduleImport($tableId, null, $file['tmp_name'], $createMissingColumns, $columnsConfigArray); + return ['async' => true]; + } + $result = $this->service->importV2($this->userId, ImportService::IMPORT_TYPE_UPLOADED_FILE, $file['tmp_name'], $tableId, null, $createMissingColumns, $columnsConfigArray); + return ['async' => false, 'result' => $result]; + }); + } catch (UploadException|NotPermittedException $e) { + $this->logger->error('Upload error', ['exception' => $e]); + return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + } + } + #[NoAdminRequired] #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')] public function importUploadInView(int $viewId, bool $createMissingColumns = true, string $columnsConfig = ''): DataResponse { @@ -154,6 +214,29 @@ public function importUploadInView(int $viewId, bool $createMissingColumns = tru } } + + #[NoAdminRequired] + #[UserRateLimit(limit: 20, period: 60)] + #[RequirePermission(permission: Application::PERMISSION_CREATE, type: Application::NODE_TYPE_VIEW, idParam: 'viewId')] + public function importV2UploadInView(int $viewId, bool $createMissingColumns = true, string $columnsConfig = ''): DataResponse { + try { + $columnsConfigArray = json_decode($columnsConfig, true); + $file = $this->getUploadedFile('uploadfile'); + return $this->handleError(function () use ($viewId, $file, $createMissingColumns, $columnsConfigArray) { + // minimal permission is checked, creating columns requires MANAGE permissions - currently tested on service layer + if ($this->service->shouldImportAsync($file['tmp_name'])) { + $this->service->scheduleImport(null, $viewId, $file['tmp_name'], $createMissingColumns, $columnsConfigArray); + return ['async' => true]; + } + $result = $this->service->importV2($this->userId, ImportService::IMPORT_TYPE_UPLOADED_FILE, $file['tmp_name'], null, $viewId, $createMissingColumns, $columnsConfigArray); + return ['async' => false, 'result' => $result]; + }); + } catch (UploadException|NotPermittedException $e) { + $this->logger->error('Upload error', ['exception' => $e]); + return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); + } + } + /** * @param string $key * @return array diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index 95eb4c17c7..d24c372d97 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -693,11 +693,15 @@ public function isRowInViewPresent(int $rowId, View $view, string $userId): bool /** * @param Row2 $row + * @param string|null $userId * @return Row2 * @throws InternalError * @throws Exception */ - public function insert(Row2 $row): Row2 { + public function insert(Row2 $row, ?string $userId = null): Row2 { + if ($userId) { + $this->userId = $userId; + } if ($row->getId()) { // if row has an id from migration or import etc. $rowSleeve = $this->createRowSleeveFromExistingData($row->getId(), $row->getTableId(), $row->getCreatedAt(), $row->getCreatedBy(), $row->getLastEditBy(), $row->getLastEditAt()); @@ -716,9 +720,15 @@ public function insert(Row2 $row): Row2 { } /** + * @param Row2 $row + * @param string|null $userId + * @return Row2 * @throws InternalError */ - public function update(Row2 $row): Row2 { + public function update(Row2 $row, ?string $userId = null): Row2 { + if ($userId) { + $this->userId = $userId; + } $changedCells = $row->getChangedCells(); // if nothing has changed if (count($changedCells) === 0) { diff --git a/lib/Model/ImportStats.php b/lib/Model/ImportStats.php new file mode 100644 index 0000000000..5c7fa296d3 --- /dev/null +++ b/lib/Model/ImportStats.php @@ -0,0 +1,21 @@ +viewService->find($viewId); + $view = $this->viewService->find($viewId, true, $userId); } catch (InternalError|MultipleObjectsReturnedException $e) { $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); @@ -274,7 +274,7 @@ public function create( throw new InternalError('Cannot create column without table or view in context'); } - if (!$this->permissionsService->canCreateColumns($table)) { + if (!$this->permissionsService->canCreateColumns($table, $userId)) { throw new PermissionError('create column for the table id = ' . $table->getId() . ' is not allowed.'); } @@ -299,7 +299,6 @@ public function create( $this->validateCustomSettings($columnDto->getCustomSettings()); - $time = new DateTime(); $item = Column::fromDto($columnDto); $item->setTitle($newTitle); $item->setTableId($table->getId()); diff --git a/lib/Service/ImportService.php b/lib/Service/ImportService.php index e945a127ec..20e90f4cbc 100644 --- a/lib/Service/ImportService.php +++ b/lib/Service/ImportService.php @@ -10,6 +10,8 @@ use DateTimeImmutable; use LogicException; use OC\User\NoUserException; +use OCA\Tables\AppInfo\Application; +use OCA\Tables\BackgroundJob\ImportTableJob; use OCA\Tables\Db\Column; use OCA\Tables\Dto\Column as ColumnDto; use OCA\Tables\Errors\BadRequestError; @@ -17,8 +19,10 @@ use OCA\Tables\Errors\NotFoundError; use OCA\Tables\Errors\PermissionError; use OCA\Tables\Helper\ColumnsHelper; +use OCA\Tables\Model\ImportStats; use OCA\Tables\Service\ColumnTypes\IColumnTypeBusiness; use OCA\Tables\Vendor\PhpOffice\PhpSpreadsheet\Cell\Cell; +use OCA\Tables\Vendor\PhpOffice\PhpSpreadsheet\Cell\Coordinate; use OCA\Tables\Vendor\PhpOffice\PhpSpreadsheet\Cell\DataType; use OCA\Tables\Vendor\PhpOffice\PhpSpreadsheet\IOFactory; use OCA\Tables\Vendor\PhpOffice\PhpSpreadsheet\Shared\Date; @@ -26,10 +30,15 @@ use OCA\Tables\Vendor\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\BackgroundJob\IJobList; use OCP\DB\Exception; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\IAppData; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\Files\SimpleFS\ISimpleFolder; +use OCP\ITempManager; use OCP\IUserManager; use Psr\Container\ContainerExceptionInterface; use Psr\Container\NotFoundExceptionInterface; @@ -49,6 +58,7 @@ class ImportService extends SuperService { private TableService $tableService; private ViewService $viewService; private IUserManager $userManager; + private IAppData $appData; private ?int $tableId = null; private ?int $viewId = null; @@ -69,6 +79,13 @@ class ImportService extends SuperService { private array $rawColumnDataTypes = []; private array $columnsConfig = []; + public const IMPORT_TYPE_USER_FILE = 'user_file'; + public const IMPORT_TYPE_APP_FILE = 'app_file'; + public const IMPORT_TYPE_UPLOADED_FILE = 'uploaded_file'; + + private const MAX_ROWS_FOR_IMMEDIATE_IMPORT = 200; + private const MAX_COLUMNS_FOR_IMMEDIATE_IMPORT = 20; + public function __construct( PermissionsService $permissionsService, LoggerInterface $logger, @@ -80,6 +97,9 @@ public function __construct( ViewService $viewService, IUserManager $userManager, private readonly ColumnsHelper $columnsHelper, + private IJobList $jobList, + private ITempManager $tempManager, + IAppDataFactory $appDataFactory, ) { parent::__construct($logger, $userId, $permissionsService); $this->rootFolder = $rootFolder; @@ -88,6 +108,7 @@ public function __construct( $this->tableService = $tableService; $this->viewService = $viewService; $this->userManager = $userManager; + $this->appData = $appDataFactory->get(Application::APP_ID); } public function previewImport(?int $tableId, ?int $viewId, string $path): array { @@ -135,6 +156,38 @@ public function previewImport(?int $tableId, ?int $viewId, string $path): array return $previewData; } + /** + * Check if import should be done asynchronously based on file size + */ + public function shouldImportAsync(string $path): bool { + if (is_uploaded_file($path) && file_exists($path)) { + $spreadsheet = IOFactory::load($path); + } else { + $userFolder = $this->rootFolder->getUserFolder($this->userId); + if ($userFolder->nodeExists($path)) { + $file = $userFolder->get($path); + $tmpFileName = $file->getStorage()->getLocalFile($file->getInternalPath()); + if ($tmpFileName) { + $spreadsheet = IOFactory::load($tmpFileName); + } else { + throw new NotFoundError('File for import could not be read.'); + } + } else { + throw new NotFoundError('File for import could not be found.'); + } + } + + $worksheet = $spreadsheet->getActiveSheet(); + + $highestColumn = $worksheet->getHighestColumn(); + $columnCount = Coordinate::columnIndexFromString($highestColumn); + + // Count rows (excluding header row) + $rowCount = $worksheet->getHighestRow() - 1; + + return $columnCount * $rowCount > self::MAX_COLUMNS_FOR_IMMEDIATE_IMPORT * self::MAX_ROWS_FOR_IMMEDIATE_IMPORT; + } + /** * @param Worksheet $worksheet * @throws DoesNotExistException @@ -250,6 +303,7 @@ private function getPreviewData(Worksheet $worksheet): array { } /** + * @deprecated Use {@link scheduleImport} and {@link importV2} instead. * @param ?int $tableId * @param ?int $viewId * @param string $path @@ -340,6 +394,146 @@ public function import(?int $tableId, ?int $viewId, string $path, bool $createMi ]; } + /** + * @param ?int $tableId + * @param ?int $viewId + * @param string $path + * @param bool $createMissingColumns + * @throws DoesNotExistException + * @throws InternalError + * @throws MultipleObjectsReturnedException + * @throws NotFoundError + * @throws PermissionError + */ + public function scheduleImport(?int $tableId, ?int $viewId, string $path, bool $createMissingColumns = true, array $columnsConfig = []) { + if ($viewId !== null) { + $view = $this->viewService->find($viewId); + if (!$this->permissionsService->canCreateRows($view)) { + throw new PermissionError('create row at the view id = ' . $viewId . ' is not allowed.'); + } + if ($createMissingColumns && !$this->permissionsService->canManageTableById($view->getTableId())) { + throw new PermissionError('create columns at the view id = ' . $viewId . ' is not allowed.'); + } + $this->viewId = $viewId; + } + if ($tableId) { + $table = $this->tableService->find($tableId); + if (!$this->permissionsService->canCreateRows($table, 'table')) { + throw new PermissionError('create row at the view id = ' . (string)$viewId . ' is not allowed.'); + } + if ($createMissingColumns && !$this->permissionsService->canManageTable($table)) { + throw new PermissionError('create columns at the view id = ' . (string)$viewId . ' is not allowed.'); + } + $this->tableId = $tableId; + } + if (!$this->tableId && !$this->viewId) { + $e = new \Exception('Neither tableId nor viewId is given.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + } + if ($this->tableId && $this->viewId) { + $e = new \LogicException('Both table ID and view ID are provided, but only one of them is allowed'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + } + + if ($this->userId === null || $this->userManager->get($this->userId) === null) { + $error = 'No user in context, can not import data. Cancel.'; + $this->logger->debug($error); + throw new InternalError($error); + } + + $importFileName = null; + // uploaded file + if (\file_exists($path)) { + $importFileName = 'import_u-' . $this->userId; + if ($this->tableId) { + $importFileName .= '_t-' . $this->tableId; + } + if ($this->viewId) { + $importFileName .= '_v-' . $this->viewId; + } + $importFileName .= '_' . \date('Y-m-d_H-i-s') . '.' . pathinfo($path, PATHINFO_EXTENSION); + + $this->getImportAppDataDir() + ->newFile($importFileName) + ->putContent(\file_get_contents($path)); + } + + $this->jobList->add( + ImportTableJob::class, + [ + 'user_id' => $this->userId, + 'table_id' => $this->tableId, + 'view_id' => $this->viewId, + 'user_file_path' => $importFileName ? null : $path, + 'import_file_name' => $importFileName, + 'create_missing_columns' => $createMissingColumns, + 'columns_config' => $columnsConfig, + ] + ); + } + + + /** + * @param string $userId + * @param ?int $tableId + * @param ?int $viewId + * @param self::IMPORT_TYPE_* $importType + * @param string $path Path to a file in the user's storage, if the file is not uploaded + * or Name of the file in the app data directory, if the file is uploaded + * or Path to uploaded file + * @param bool $createMissingColumns + * @param array $columnsConfig + * @return ImportStats + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + * @throws NotFoundError + */ + public function importV2(string $userId, string $importType, string $path, ?int $tableId, ?int $viewId, bool $createMissingColumns = true, array $columnsConfig = []): ImportStats { + $this->userId = $userId; + $this->tableId = $tableId; + $this->viewId = $viewId; + $this->createUnknownColumns = $createMissingColumns; + $this->columnsConfig = $columnsConfig; + + try { + if ($importType === self::IMPORT_TYPE_USER_FILE) { + $file = $this->rootFolder->getUserFolder($this->userId)->get($path); + $temporaryFile = $file->getStorage()->getLocalFile($file->getInternalPath()); + $spreadsheet = IOFactory::load($temporaryFile); + $this->loop($spreadsheet->getActiveSheet()); + } elseif ($importType === self::IMPORT_TYPE_UPLOADED_FILE) { + if (!is_uploaded_file($path) || !file_exists($path)) { + throw new NotFoundError('Uploaded file for import not found.'); + } + $spreadsheet = IOFactory::load($path); + $this->loop($spreadsheet->getActiveSheet()); + } elseif ($importType === self::IMPORT_TYPE_APP_FILE) { + $file = $this->getImportAppDataDir()->getFile($path); + $temporaryFile = $this->tempManager->getTemporaryFile('.' . $file->getExtension()); + file_put_contents($temporaryFile, $file->getContent()); + $spreadsheet = IOFactory::load($temporaryFile); + $this->loop($spreadsheet->getActiveSheet()); + } else { + throw new NotFoundError('No file for import given.'); + } + } catch (NotFoundException|NotPermittedException|NoUserException|InternalError|PermissionError $e) { + $this->logger->warning('Storage for user could not be found', ['exception' => $e]); + throw new NotFoundError('Storage for user could not be found', 0, $e); + } + + return new ImportStats( + count($this->columns), + $this->countMatchingColumns, + $this->countCreatedColumns, + $this->countInsertedRows, + $this->countUpdatedRows, + $this->countParsingErrors, + $this->countErrors + ); + } + /** * @param Worksheet $worksheet * @throws DoesNotExistException @@ -362,6 +556,7 @@ private function loop(Worksheet $worksheet): void { if (empty(array_filter($this->columns))) { return; } + $columnBusinesses = []; foreach ($this->columns as $column) { $columnBusinesses[$column->getId()] = $this->columnsHelper->getColumnBusinessObject($column); @@ -488,7 +683,7 @@ private function upsertRow(Row $row, array $columnBusinesses): void { $this->rowService->updateSet($rowId, $this->viewId, $data, $this->userId, $this->tableId); $this->countUpdatedRows++; } else { - $this->rowService->create($this->tableId, $this->viewId, $data); + $this->rowService->create($this->tableId, $this->viewId, $data, $this->userId); $this->countInsertedRows++; } } catch (PermissionError $e) { @@ -719,4 +914,16 @@ private function parseColumnDataType(Cell $cell): array { return $dataType; } + + /** + * @return ISimpleFolder + * @throws \OCP\Files\NotPermittedException + */ + private function getImportAppDataDir(): ISimpleFolder { + try { + return $this->appData->getFolder('import'); + } catch (NotFoundException) { + return $this->appData->newFolder('import'); + } + } } diff --git a/lib/Service/PermissionsService.php b/lib/Service/PermissionsService.php index 0f21fead98..3140f1de46 100644 --- a/lib/Service/PermissionsService.php +++ b/lib/Service/PermissionsService.php @@ -659,7 +659,7 @@ private function basisCheck(Table|View|Context $element, string $nodeType, ?stri try { $userId = $this->preCheckUserId($userId); } catch (InternalError $e) { - $e = new \Exception('Cannot pre check the user id'); + $e = new \Exception('Cannot pre check the user id', 0, $e); $this->logger->error($e->getMessage(), ['exception' => $e]); return false; } diff --git a/lib/Service/RowService.php b/lib/Service/RowService.php index f74d819e26..d07afeed63 100644 --- a/lib/Service/RowService.php +++ b/lib/Service/RowService.php @@ -169,6 +169,7 @@ public function find(int $rowId): Row2 { * @param int|null $tableId * @param int|null $viewId * @param RowDataInput|list $data + * @param string|null $userId * @return Row2 * * @throws BadRequestError @@ -177,8 +178,11 @@ public function find(int $rowId): Row2 { * @throws Exception * @throws InternalError */ - public function create(?int $tableId, ?int $viewId, RowDataInput|array $data): Row2 { - if ($this->userId === null) { + public function create(?int $tableId, ?int $viewId, RowDataInput|array $data, ?string $userId = null): Row2 { + if ($userId) { + $this->userId = $userId; + } + if ($this->userId === null || $this->userId === '') { $e = new \Exception('No user id in context, but needed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); @@ -198,7 +202,7 @@ public function create(?int $tableId, ?int $viewId, RowDataInput|array $data): R } // security - if (!$this->permissionsService->canCreateRows($view)) { + if (!$this->permissionsService->canCreateRows($view, 'view', $this->userId)) { throw new PermissionError('create row at the view id = ' . $viewId . ' is not allowed.'); } @@ -216,7 +220,7 @@ public function create(?int $tableId, ?int $viewId, RowDataInput|array $data): R } // security - if (!$this->permissionsService->canCreateRows($table, 'table')) { + if (!$this->permissionsService->canCreateRows($table, 'table', $this->userId)) { throw new PermissionError('create row at the table id = ' . $tableId . ' is not allowed.'); } @@ -237,7 +241,7 @@ public function create(?int $tableId, ?int $viewId, RowDataInput|array $data): R $row2->setTableId($tableId); $row2->setData($data); try { - $insertedRow = $this->row2Mapper->insert($row2); + $insertedRow = $this->row2Mapper->insert($row2, $this->userId); $this->eventDispatcher->dispatchTyped(new RowAddedEvent($insertedRow)); $this->activityManager->triggerEvent( @@ -564,6 +568,15 @@ public function updateSet( string $userId, ?int $tableId, ): Row2 { + if ($userId) { + $this->userId = $userId; + } + if ($this->userId === null || $this->userId === '') { + $e = new \Exception('No user id in context, but needed.'); + $this->logger->error($e->getMessage(), ['exception' => $e]); + throw new InternalError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); + } + try { $item = $this->getRowById($id); } catch (InternalError $e) { @@ -581,7 +594,7 @@ public function updateSet( $this->logger->error($e->getMessage(), ['exception' => $e]); throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } - if (!$this->permissionsService->canUpdateRowsByViewId($viewId)) { + if (!$this->permissionsService->canUpdateRowsByViewId($viewId, $userId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); @@ -627,7 +640,7 @@ public function updateSet( $this->logger->error($e->getMessage(), ['exception' => $e]); throw new NotFoundError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); } - if (!$this->permissionsService->canUpdateRowsByTableId($tableId)) { + if (!$this->permissionsService->canUpdateRowsByTableId($tableId, $userId)) { $e = new \Exception('Update row is not allowed.'); $this->logger->error($e->getMessage(), ['exception' => $e]); throw new PermissionError(get_class($this) . ' - ' . __FUNCTION__ . ': ' . $e->getMessage()); @@ -654,7 +667,7 @@ public function updateSet( } } - $updatedRow = $this->row2Mapper->update($item); + $updatedRow = $this->row2Mapper->update($item, $this->userId); $this->eventDispatcher->dispatchTyped(new RowUpdatedEvent($updatedRow, $previousData)); diff --git a/src/modules/modals/FileActionImport.vue b/src/modules/modals/FileActionImport.vue index 6ef692723e..803acc9778 100644 --- a/src/modules/modals/FileActionImport.vue +++ b/src/modules/modals/FileActionImport.vue @@ -89,7 +89,7 @@ - @@ -99,7 +99,7 @@ - + @@ -115,7 +115,7 @@ import { import { generateUrl } from '@nextcloud/router' import { Node } from '@nextcloud/files' -import { showError } from '@nextcloud/dialogs' +import { showError, showInfo } from '@nextcloud/dialogs' import axios from '@nextcloud/axios' import { translate as t } from '@nextcloud/l10n' import RowFormWrapper from '../../shared/components/ncTable/partials/rowTypePartials/RowFormWrapper.vue' @@ -156,7 +156,6 @@ export default { }, existingTables: [], selectedTable: null, - importResults: null, showResultsDialog: true, } @@ -210,6 +209,8 @@ export default { if (!this.importResults) { showError(t('tables', 'Could not create table')) + } else if (this.importResults.async) { + showInfo(t('tables', 'File import started, this might take a while. You will be notified once it finished.')) } } else { if (!this.selectedTable) { @@ -221,6 +222,8 @@ export default { if (!this.importResults) { showError(t('tables', 'Could not import data to table')) + } else if (this.importResults.async) { + showInfo(t('tables', 'File import started, this might take a while. You will be notified once it finished.')) } } @@ -257,7 +260,7 @@ async function insertTable(title, emoji) { } async function updateTable(tableId, path, createMissingColumns) { - const res = await axios.post(generateUrl(`/apps/tables/import/table/${tableId}`), { + const res = await axios.post(generateUrl(`/apps/tables/v2/import/table/${tableId}`), { path, createMissingColumns, }) diff --git a/src/modules/modals/Import.vue b/src/modules/modals/Import.vue index 1e05d97edd..2a3dd76e49 100644 --- a/src/modules/modals/Import.vue +++ b/src/modules/modals/Import.vue @@ -9,7 +9,7 @@ @closing="actionCancel">