diff --git a/cypress/e2e/tables-import.cy.js b/cypress/e2e/tables-import.cy.js
index e29299690..320e13f13 100644
--- a/cypress/e2e/tables-import.cy.js
+++ b/cypress/e2e/tables-import.cy.js
@@ -5,6 +5,8 @@ describe('Import csv', () => {
before(function() {
cy.createRandomUser().then(user => {
localUser = user
+ cy.login(localUser)
+ cy.uploadFile('test-import.csv', 'text/csv')
})
})
@@ -14,19 +16,20 @@ describe('Import csv', () => {
})
it('Import csv from Files', () => {
- cy.uploadFile('test-import.csv', 'text/csv')
cy.loadTable('Tutorial')
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 button span').contains('Choose test-import.csv').click()
+ cy.get('.modal__content .import-filename', { timeout: 5000 }).should('be.visible')
cy.get('.modal__content button').contains('Import').click()
+
cy.get('[data-cy="importResultColumnsFound"]').should('contain.text', '4')
cy.get('[data-cy="importResultColumnsMatch"]').should('contain.text', '4')
cy.get('[data-cy="importResultColumnsCreated"]').should('contain.text', '0')
cy.get('[data-cy="importResultRowsInserted"]').should('contain.text', '3')
- cy.get('[data-cy="importResultParsingErrors"]').should('not.exist')
- cy.get('[data-cy="importResultRowErrors"]').should('not.exist')
+ cy.get('[data-cy="importResultParsingErrors"]').should('contain.text', '0')
+ cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0')
})
it('Import csv from device', () => {
@@ -35,12 +38,66 @@ describe('Import csv', () => {
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('.modal__content button').contains('Import').click()
- cy.get('[data-cy="importResultColumnsFound"]').should('contain.text', '4')
+
+ cy.get('[data-cy="importResultColumnsFound"]', { timeout: 20000 }).should('contain.text', '4')
cy.get('[data-cy="importResultColumnsMatch"]').should('contain.text', '4')
cy.get('[data-cy="importResultColumnsCreated"]').should('contain.text', '0')
cy.get('[data-cy="importResultRowsInserted"]').should('contain.text', '3')
- cy.get('[data-cy="importResultParsingErrors"]').should('not.exist')
- cy.get('[data-cy="importResultRowErrors"]').should('not.exist')
+ cy.get('[data-cy="importResultParsingErrors"]').should('contain.text', '0')
+ cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0')
+ })
+
+})
+
+describe('Import csv from Files file action', () => {
+
+ before(function() {
+ cy.createRandomUser().then(user => {
+ localUser = user
+ cy.login(localUser)
+ cy.uploadFile('test-import.csv', 'text/csv')
+ })
+ })
+
+ beforeEach(function() {
+ cy.login(localUser)
+ cy.visit('apps/files/files')
+ })
+
+ it('Import to new table', () => {
+ cy.get('[data-cy-files-list-row-name="test-import.csv"] [data-cy-files-list-row-actions] .action-item button').click()
+ cy.get('[data-cy-files-list-row-action="import-to-tables"]').click()
+
+ cy.intercept({ method: 'POST', url: '**/apps/tables/import/table/*'}).as('importNewTableReq')
+ cy.get('[data-cy="fileActionImportButton"]').click()
+ cy.wait('@importNewTableReq').its('response.statusCode').should('equal', 200)
+
+ cy.get('[data-cy="importResultColumnsFound"]').should('contain.text', '4')
+ cy.get('[data-cy="importResultColumnsMatch"]').should('contain.text', '0')
+ cy.get('[data-cy="importResultColumnsCreated"]').should('contain.text', '4')
+ cy.get('[data-cy="importResultRowsInserted"]').should('contain.text', '3')
+ cy.get('[data-cy="importResultParsingErrors"]').should('contain.text', '0')
+ cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0')
})
+ it('Import to existing table', () => {
+ cy.get('[data-cy-files-list-row-name="test-import.csv"] [data-cy-files-list-row-actions] .action-item button').click()
+ cy.get('[data-cy-files-list-row-action="import-to-tables"]').click()
+
+ cy.get('[data-cy="importAsNewTableSwitch"]').click()
+ cy.get('[data-cy="selectExistingTableDropdown"]').type('tutorial')
+ cy.get('.name-parts').click()
+
+ cy.intercept({ method: 'POST', url: '**/apps/tables/import/table/*'}).as('importExistingTableReq')
+ cy.get('[data-cy="fileActionImportButton"]').click()
+ cy.wait('@importExistingTableReq').its('response.statusCode').should('equal', 200)
+
+ cy.get('[data-cy="importResultColumnsFound"]').should('contain.text', '4')
+ cy.get('[data-cy="importResultColumnsMatch"]').should('contain.text', '4')
+ cy.get('[data-cy="importResultColumnsCreated"]').should('contain.text', '0')
+ cy.get('[data-cy="importResultRowsInserted"]').should('contain.text', '3')
+ cy.get('[data-cy="importResultParsingErrors"]').should('contain.text', '0')
+ cy.get('[data-cy="importResultRowErrors"]').should('contain.text', '0')
+ })
+
})
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index 05047f29a..738534a66 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -6,6 +6,7 @@
use OCA\Analytics\Datasource\DatasourceEvent;
use OCA\Tables\Capabilities;
use OCA\Tables\Listener\AnalyticsDatasourceListener;
+use OCA\Tables\Listener\LoadAdditionalListener;
use OCA\Tables\Listener\TablesReferenceListener;
use OCA\Tables\Listener\UserDeletedListener;
use OCA\Tables\Reference\ContentReferenceProvider;
@@ -17,10 +18,12 @@
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Collaboration\Reference\RenderReferenceEvent;
+use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent;
use OCP\IConfig;
use OCP\Server;
use OCP\User\Events\BeforeUserDeletedEvent;
use Psr\Container\ContainerExceptionInterface;
+
use Psr\Container\NotFoundExceptionInterface;
class Application extends App implements IBootstrap {
@@ -45,6 +48,8 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(DatasourceEvent::class, AnalyticsDatasourceListener::class);
$context->registerEventListener(RenderReferenceEvent::class, TablesReferenceListener::class);
+ $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
+
$context->registerSearchProvider(SearchTablesProvider::class);
try {
diff --git a/lib/Listener/LoadAdditionalListener.php b/lib/Listener/LoadAdditionalListener.php
new file mode 100644
index 000000000..6978f047b
--- /dev/null
+++ b/lib/Listener/LoadAdditionalListener.php
@@ -0,0 +1,20 @@
+ */
+class LoadAdditionalListener implements IEventListener {
+ public function handle(Event $event): void {
+ if (!($event instanceof LoadAdditionalScriptsEvent)) {
+ return;
+ }
+
+ Util::addScript(Application::APP_ID, 'tables-files', 'files');
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 31bdbb116..965e16604 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,10 +9,12 @@
"version": "0.7.0-beta.1",
"license": "agpl",
"dependencies": {
+ "@mdi/svg": "^7.4.47",
"@nextcloud/auth": "^2.2.1",
"@nextcloud/axios": "^2.4.0",
"@nextcloud/dialogs": "^4.2.6",
"@nextcloud/event-bus": "^3.1.0",
+ "@nextcloud/files": "^3.1.0",
"@nextcloud/l10n": "^2.2.0",
"@nextcloud/moment": "^1.3.1",
"@nextcloud/router": "^3.0.0",
@@ -1759,9 +1761,9 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw=="
},
"node_modules/@buttercup/fetch": {
- "version": "0.1.2",
- "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.1.2.tgz",
- "integrity": "sha512-mDBtsysQ0Gnrp4FamlRJGpu7HUHwbyLC4uUav1I7QAqThFAa/4d1cdZCxrV5gKvh6zO1fu95bILNJi4Y2hALhQ==",
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@buttercup/fetch/-/fetch-0.2.1.tgz",
+ "integrity": "sha512-sCgECOx8wiqY8NN1xN22BqqKzXYIG2AicNLlakOAI4f0WgyLVUbAigMf8CZhBtJxdudTcB1gD5lciqi44jwJvg==",
"optionalDependencies": {
"node-fetch": "^3.3.0"
}
@@ -3160,6 +3162,23 @@
"vue": "^2.7.14"
}
},
+ "node_modules/@nextcloud/dialogs/node_modules/@nextcloud/files": {
+ "version": "3.0.0-beta.21",
+ "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.0.0-beta.21.tgz",
+ "integrity": "sha512-haydsUhF3t7DTUcC48lveztXZA1KMAkn+DRZUwSWu0S0VF4tTjn/+ZM7pqnNBIqOkPMTW9azAU8h6mmENpvd9w==",
+ "dependencies": {
+ "@nextcloud/auth": "^2.1.0",
+ "@nextcloud/l10n": "^2.2.0",
+ "@nextcloud/logger": "^2.5.0",
+ "@nextcloud/router": "^2.1.2",
+ "is-svg": "^5.0.0",
+ "webdav": "^5.2.3"
+ },
+ "engines": {
+ "node": "^20.0.0",
+ "npm": "^9.0.0"
+ }
+ },
"node_modules/@nextcloud/dialogs/node_modules/@nextcloud/router": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-2.2.1.tgz",
@@ -3255,6 +3274,20 @@
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
"integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
},
+ "node_modules/@nextcloud/dialogs/node_modules/is-svg": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-5.0.0.tgz",
+ "integrity": "sha512-sRl7J0oX9yUNamSdc8cwgzh9KBLnQXNzGmW0RVHwg/jEYjGNYHC6UvnYD8+hAeut9WwxRvhG9biK7g/wDGxcMw==",
+ "dependencies": {
+ "fast-xml-parser": "^4.1.3"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@nextcloud/dialogs/node_modules/node-polyfill-webpack-plugin": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/node-polyfill-webpack-plugin/-/node-polyfill-webpack-plugin-2.0.1.tgz",
@@ -3391,16 +3424,17 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/@nextcloud/files": {
- "version": "3.0.0-beta.21",
- "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.0.0-beta.21.tgz",
- "integrity": "sha512-haydsUhF3t7DTUcC48lveztXZA1KMAkn+DRZUwSWu0S0VF4tTjn/+ZM7pqnNBIqOkPMTW9azAU8h6mmENpvd9w==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@nextcloud/files/-/files-3.1.0.tgz",
+ "integrity": "sha512-i0g9L5HRBJ2vr/gXYb0Gtg379u6nYZJFL30W50OV0F0qlf8OtkAlNpfOVOg3sJf9zklARE2lVY9g2Y9sv/iQ3g==",
"dependencies": {
- "@nextcloud/auth": "^2.1.0",
+ "@nextcloud/auth": "^2.2.1",
"@nextcloud/l10n": "^2.2.0",
- "@nextcloud/logger": "^2.5.0",
- "@nextcloud/router": "^2.1.2",
+ "@nextcloud/logger": "^2.7.0",
+ "@nextcloud/paths": "^2.1.0",
+ "@nextcloud/router": "^2.2.0",
"is-svg": "^5.0.0",
- "webdav": "^5.2.3"
+ "webdav": "^5.3.1"
},
"engines": {
"node": "^20.0.0",
@@ -3473,16 +3507,16 @@
}
},
"node_modules/@nextcloud/logger": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/@nextcloud/logger/-/logger-2.5.0.tgz",
- "integrity": "sha512-vJx5YxPyS9/tg3YoqA8CBN7YTZFHfuhMKJIIWFV28phxXqKhGwKVKh+/Ir8ZIPweIM5n8VNT6JOJq1JjGiMg2w==",
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/@nextcloud/logger/-/logger-2.7.0.tgz",
+ "integrity": "sha512-DSJg9H1jT2zfr7uoP4tL5hKncyY+LOuxJzLauj0M/f6gnpoXU5WG1Zw8EFPOrRWjkC0ZE+NCqrMnITgdRRpXJQ==",
"dependencies": {
"@nextcloud/auth": "^2.0.0",
"core-js": "^3.6.4"
},
"engines": {
- "node": "^16.0.0",
- "npm": "^7.0.0 || ^8.0.0"
+ "node": "^20.0.0",
+ "npm": "^9.0.0"
}
},
"node_modules/@nextcloud/moment": {
@@ -3499,6 +3533,14 @@
"npm": "^9.0.0"
}
},
+ "node_modules/@nextcloud/paths": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-2.1.0.tgz",
+ "integrity": "sha512-8wX0gqwez0bTuAS8A0OEiqbbp0ZsqLr07zSErmS6OYhh9KZcSt/kO6lQV5tnrFqIqJVsxwz4kHUjtZXh6DSf9Q==",
+ "dependencies": {
+ "core-js": "^3.6.4"
+ }
+ },
"node_modules/@nextcloud/router": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@nextcloud/router/-/router-3.0.0.tgz",
@@ -25006,20 +25048,20 @@
}
},
"node_modules/web-streams-polyfill": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
- "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
+ "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"optional": true,
"engines": {
"node": ">= 8"
}
},
"node_modules/webdav": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.3.0.tgz",
- "integrity": "sha512-xRu/URZGCxDPXmT+9Gu6tNGvlETBwjcuz69lx/6Qlq/0q3Gu2GSVyRt+mP0vTlLFfaY3xZ5O/SPTQ578tC/45Q==",
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.4.0.tgz",
+ "integrity": "sha512-QCvHbzDvr1NKldx0lUHo2Z8Hc+dgvfORNP+a7Da8TRXVJXYTMIa3UKEwXIgkl8iVQUl4cJvU5EaT8hQ5HlfgPQ==",
"dependencies": {
- "@buttercup/fetch": "^0.1.1",
+ "@buttercup/fetch": "^0.2.1",
"base-64": "^1.0.0",
"byte-length": "^1.0.2",
"fast-xml-parser": "^4.2.4",
diff --git a/package.json b/package.json
index 267e1214f..1a25a4f3f 100644
--- a/package.json
+++ b/package.json
@@ -24,10 +24,12 @@
"stylelint:fix": "stylelint 'css/*.css' 'css/*.scss' 'src/**/*.scss' 'src/**/*.vue' --fix"
},
"dependencies": {
+ "@mdi/svg": "^7.4.47",
"@nextcloud/auth": "^2.2.1",
"@nextcloud/axios": "^2.4.0",
"@nextcloud/dialogs": "^4.2.6",
"@nextcloud/event-bus": "^3.1.0",
+ "@nextcloud/files": "^3.1.0",
"@nextcloud/l10n": "^2.2.0",
"@nextcloud/moment": "^1.3.1",
"@nextcloud/router": "^3.0.0",
diff --git a/src/file-actions.js b/src/file-actions.js
new file mode 100644
index 000000000..ff903052c
--- /dev/null
+++ b/src/file-actions.js
@@ -0,0 +1,34 @@
+import { FileAction, registerFileAction } from '@nextcloud/files'
+import { spawnDialog } from '@nextcloud/dialogs'
+// eslint-disable-next-line import/no-unresolved
+import tablesIcon from '@mdi/svg/svg/table-large.svg?raw'
+
+__webpack_nonce__ = btoa(OC.requestToken) // eslint-disable-line
+__webpack_public_path__ = OC.linkTo('tables', 'js/') // eslint-disable-line
+
+const validMimeTypes = [
+ 'text/csv',
+ 'text/html',
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ 'application/vnd.ms-excel',
+]
+
+const fileAction = new FileAction({
+ id: 'import-to-tables',
+ displayName: () => t('tables', 'Import into Tables'),
+ iconSvgInline: () => tablesIcon,
+
+ enabled: (files) => {
+ const file = files[0]
+
+ return file.type === 'file' && validMimeTypes.includes(file.mime)
+ },
+
+ exec: async (file) => {
+ const { default: FileActionImport } = await import('./modules/modals/FileActionImport.vue')
+ spawnDialog(FileActionImport, { file })
+ return null
+ },
+})
+
+registerFileAction(fileAction)
diff --git a/src/modules/modals/FileActionImport.vue b/src/modules/modals/FileActionImport.vue
new file mode 100644
index 000000000..072925014
--- /dev/null
+++ b/src/modules/modals/FileActionImport.vue
@@ -0,0 +1,281 @@
+
+ {{ t('tables', 'Import file into Tables') }}
+
+
{{ t('tables', 'Found columns') }} | ++ {{ results.found_columns_count }} + | +
{{ t('tables', 'Matching columns') }} | ++ {{ results.matching_columns_count }} + | +
{{ t('tables', 'Created columns') }} | ++ {{ results.created_columns_count }} + | +
{{ t('tables', 'Inserted rows') }} | ++ {{ results.inserted_rows_count }} + | +
{{ t('tables', 'Value parsing errors') }} | ++ {{ results.errors_parsing_count }} + | +
{{ t('tables', 'Row creation errors') }} | ++ {{ results.errors_count }} + | +