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 @@ + + + + + diff --git a/src/modules/modals/Import.vue b/src/modules/modals/Import.vue index fe943b999..7662a6018 100644 --- a/src/modules/modals/Import.vue +++ b/src/modules/modals/Import.vue @@ -67,59 +67,7 @@
- -
-

- - - {{ t('tables', 'Imported from ') + importFileName }} -

-
-
- {{ t('tables', 'Found columns') }} -
-
- {{ result['found_columns_count'] }} -
-
- {{ t('tables', 'Matching columns') }} -
-
- {{ result['matching_columns_count'] }} -
-
- {{ t('tables', 'Created columns') }} -
-
- {{ result['created_columns_count'] }} -
-
- {{ t('tables', 'Inserted rows') }} -
-
- {{ result['inserted_rows_count'] }} -
- -
- - - {{ t('tables', 'Error during importing. Please read the logs for more information.') }} - +
@@ -154,12 +102,11 @@ import permissionsMixin from '../../shared/components/ncTable/mixins/permissions import IconFolder from 'vue-material-design-icons/Folder.vue' import IconUpload from 'vue-material-design-icons/Upload.vue' import IconFile from 'vue-material-design-icons/File.vue' -import IconCheck from 'vue-material-design-icons/Check.vue' -import IconAlert from 'vue-material-design-icons/Alert.vue' import axios from '@nextcloud/axios' import { generateUrl } from '@nextcloud/router' import { mapGetters } from 'vuex' import NcIconTimerSand from '../../shared/components/ncIconTimerSand/NcIconTimerSand.vue' +import ImportResults from './ImportResults.vue' export default { @@ -169,10 +116,9 @@ export default { IconFolder, IconUpload, IconFile, - IconCheck, - IconAlert, NcModal, NcButton, + ImportResults, NcCheckboxRadioSwitch, RowFormWrapper, NcEmptyContent, diff --git a/src/modules/modals/ImportResults.vue b/src/modules/modals/ImportResults.vue new file mode 100644 index 000000000..7cddbb43c --- /dev/null +++ b/src/modules/modals/ImportResults.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/webpack.js b/webpack.js index 648f1f210..7daa253f6 100644 --- a/webpack.js +++ b/webpack.js @@ -1,6 +1,20 @@ const path = require('path') const webpackConfig = require('@nextcloud/webpack-vue-config') -webpackConfig.entry['reference'] = path.join(__dirname, 'src', 'reference.js') +webpackConfig.entry = { + main: path.join(__dirname, 'src', 'main.js'), + files: path.join(__dirname, 'src', 'file-actions.js'), + reference: path.join(__dirname, 'src', 'reference.js'), +} + +webpackConfig.module = { + rules: [ + ...webpackConfig.module.rules, + { + resourceQuery: /raw/, + type: 'asset/source', + } + ] +} module.exports = webpackConfig