diff --git a/cypress/e2e/tables-import.cy.js b/cypress/e2e/tables-import.cy.js index e29299690..81b9dd00c 100644 --- a/cypress/e2e/tables-import.cy.js +++ b/cypress/e2e/tables-import.cy.js @@ -20,13 +20,15 @@ describe('Import csv', () => { 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 +37,67 @@ 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 + }) + }) + + beforeEach(function() { + cy.login(localUser) + cy.visit('apps/files/files') + cy.uploadFile('test-import.csv', 'text/csv') + }) + + it('Import to new table', () => { + cy.reload() + + cy.get('[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-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/src/file-actions.js b/src/file-actions.js new file mode 100644 index 000000000..50dd896d0 --- /dev/null +++ b/src/file-actions.js @@ -0,0 +1,33 @@ +import { FileAction, registerFileAction } from '@nextcloud/files' +import { spawnDialog } from '@nextcloud/dialogs' +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