Skip to content

Commit

Permalink
feat: added file action to Files to import file into Tables
Browse files Browse the repository at this point in the history
Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

basic implementation of import to tables button

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

work on api request to import table

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

addition of import modal and import logic

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

update variable names for clarity and consistency

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

added error handling and feedback to import modal

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

updated valid importable file formats

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

added error handling for unimportable file type

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

removed unused import

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

modal organization/code cleanup

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

added error messages and modal visibility

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

reworked error handling

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

added import results dialog, renamed files

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

only render button when valid file type

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

fixed enabled for valid file types

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

fix lint errors

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

added info to aid static analysis

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

made ImportResults into reusable component

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

refactored to use ImportResults component

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

change icon from hardcoded svg string to icon import

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

added cypress tests for file action import

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

fix: dynamic import for FileActionImport component

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>

test: refactored tests for file importing

Signed-off-by: Elizabeth Danzberger <lizzy7128@tutanota.de>
  • Loading branch information
elzody committed Mar 5, 2024
1 parent bcb7d1e commit 94f1ad9
Show file tree
Hide file tree
Showing 8 changed files with 511 additions and 63 deletions.
67 changes: 62 additions & 5 deletions cypress/e2e/tables-import.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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')
})

})
5 changes: 5 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions lib/Listener/LoadAdditionalListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace OCA\Tables\Listener;

use OCA\Tables\AppInfo\Application;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;

/** @template-implements IEventListener<LoadAdditionalScriptsEvent> */
class LoadAdditionalListener implements IEventListener {
public function handle(Event $event): void {
if (!($event instanceof LoadAdditionalScriptsEvent)) {
return;
}

Util::addScript(Application::APP_ID, 'tables-files', 'files');
}
}
33 changes: 33 additions & 0 deletions src/file-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FileAction, registerFileAction } from '@nextcloud/files'

Check failure on line 1 in src/file-actions.js

View workflow job for this annotation

GitHub Actions / NPM lint

"@nextcloud/files" is extraneous
import { spawnDialog } from '@nextcloud/dialogs'
import tablesIcon from '@mdi/svg/svg/table-large.svg?raw'

Check failure on line 3 in src/file-actions.js

View workflow job for this annotation

GitHub Actions / NPM lint

Unable to resolve path to module '@mdi/svg/svg/table-large.svg?raw'

Check failure on line 3 in src/file-actions.js

View workflow job for this annotation

GitHub Actions / NPM lint

"@mdi/svg" is extraneous

__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)
Loading

0 comments on commit 94f1ad9

Please sign in to comment.