Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Cypress.Commands.add('openContextEditModal', (title) => {

Cypress.Commands.add('clickOnTableThreeDotMenu', (optionName) => {
cy.get('[data-cy="customTableAction"] button').click()
cy.get('[data-cy="dataTableExportBtn"]').contains(optionName).click({ force: true })
cy.get('.v-popper__popper button, [role="menuitem"]').contains(optionName).click({ force: true })
})

Cypress.Commands.add('sortTableColumn', (columnTitle, mode = 'ASC') => {
Expand Down
109 changes: 105 additions & 4 deletions playwright/e2e/tables-export-csv.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,46 @@

import { test, expect } from '../support/fixtures'
import * as fs from 'fs'
import { type Page } from '@playwright/test'
import { clickOnTableThreeDotMenu, getTutorialTableName, loadTable } from '../support/commands'

test.describe('Import csv', () => {
async function fillSearchInput(page: Page, value: string) {
// Scope to the NcTable container to avoid matching Nextcloud header search elements
const searchInput = page.locator('[data-cy="ncTable"]').getByRole('textbox', { name: 'Search' })
await expect(searchInput).toBeVisible({ timeout: 10000 })
await searchInput.fill(value)
await page.waitForTimeout(600) // debounce in SearchForm is 500 ms
}

test('Export csv', async ({ userPage: { page } }) => {
async function clickSelectionBarAction(page: Page, label: string) {
await expect(page.locator('.icon-loading').first()).toBeHidden({ timeout: 10000 })
await expect(page.locator('.selected-rows-option')).toBeVisible({ timeout: 10000 })
// NcActionButton does not forward data-cy to the DOM; match by button text content instead.
// With inline=2 the items render as plain <button> elements directly in the selection bar.
const item = page.locator('.selected-rows-option button').filter({ hasText: label })
await expect(item.first()).toBeVisible({ timeout: 5000 })
await item.first().click()
}

async function selectFirstRow(page: Page) {
const checkbox = page.locator('[data-cy="customTableRow"]:first-of-type input[type="checkbox"]').first()
await expect(checkbox).toBeVisible({ timeout: 10000 })
await checkbox.click({ force: true })
await expect(checkbox).toBeChecked()
}

test.describe('CSV export', () => {

test('Export all rows is always available in the three-dot menu', async ({ userPage: { page } }) => {
await page.goto('/index.php/apps/tables')
await loadTable(page, 'Welcome to Nextcloud Tables!')

const tutorialName = await getTutorialTableName(page)
const fileNamePattern = new RegExp(`^\\d{2}-\\d{2}-\\d{2}_\\d{2}-\\d{2}_${tutorialName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\\.csv$`)
const fileNamePattern = new RegExp(`^\\d{2}-\\d{2}-\\d{2}_\\d{2}-\\d{2}_${tutorialName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.csv$`)

const [download] = await Promise.all([
page.waitForEvent('download'),
clickOnTableThreeDotMenu(page, 'Export as CSV'),
clickOnTableThreeDotMenu(page, 'Export all rows'),
])

expect(download.suggestedFilename()).toMatch(fileNamePattern)
Expand All @@ -29,4 +55,79 @@ test.describe('Import csv', () => {
expect(content).toContain('What,How to do,Ease of use,Done')
expect(content).toContain('Open the tables app,Reachable via the Tables icon in the apps list.,5,true')
})

test('Export filtered rows only appears in three-dot menu when a filter is active', async ({ userPage: { page } }) => {
await page.goto('/index.php/apps/tables')
await loadTable(page, 'Welcome to Nextcloud Tables!')

await fillSearchInput(page, 'Open the tables app')

// Export filtered and verify only the matching row is in the CSV
const [download] = await Promise.all([
page.waitForEvent('download'),
clickOnTableThreeDotMenu(page, 'Export filtered rows'),
])

const path = await download.path()
const content = fs.readFileSync(path, 'utf8')

expect(content).toContain('Open the tables app')
expect(content).not.toContain('Add a new column')
})

test('Export selected rows appears in selection bar when rows are checked', async ({ userPage: { page } }) => {
await page.goto('/index.php/apps/tables')
await loadTable(page, 'Welcome to Nextcloud Tables!')

await selectFirstRow(page)

// Export selected — should only contain 1 data row
const [download] = await Promise.all([
page.waitForEvent('download'),
clickSelectionBarAction(page, 'Export selected rows'),
])

const path = await download.path()
const content = fs.readFileSync(path, 'utf8')
const lines = content.trim().split('\n')

// Header + exactly 1 data row
expect(lines).toHaveLength(2)
})

test('Export filtered rows appears in selection bar when rows are selected and filter is active', async ({ userPage: { page } }) => {
await page.goto('/index.php/apps/tables')
await loadTable(page, 'Welcome to Nextcloud Tables!')

await fillSearchInput(page, 'Open')
await selectFirstRow(page)

// Both export buttons must be visible in the selection bar
await expect(page.locator('.selected-rows-option')).toBeVisible({ timeout: 10000 })
for (const label of ['Export selected rows', 'Export filtered rows']) {
await expect(
page.locator('.selected-rows-option button').filter({ hasText: label }).first(),
).toBeVisible({ timeout: 5000 })
}
})

test('Export all rows includes unfiltered data even when filter is active', async ({ userPage: { page } }) => {
await page.goto('/index.php/apps/tables')
await loadTable(page, 'Welcome to Nextcloud Tables!')

await fillSearchInput(page, 'Open the tables app')

// Export ALL rows — must include rows not matching the filter
const [download] = await Promise.all([
page.waitForEvent('download'),
clickOnTableThreeDotMenu(page, 'Export all rows'),
])

const path = await download.path()
const content = fs.readFileSync(path, 'utf8')

expect(content).toContain('Open the tables app')
expect(content).toContain('Add a new column')
})

})
10 changes: 6 additions & 4 deletions playwright/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ async function openTableActionsMenu(page: Page) {

const menuButton = page.locator('[data-cy="customTableAction"] button').first()
const anyMenuAction = page.locator(
'[data-cy="dataTableEditTableBtn"], [data-cy="dataTableCreateViewBtn"], [data-cy="dataTableCreateColumnBtn"], [data-cy="dataTableShareBtn"], [data-cy="dataTableExportBtn"]',
'[data-cy="dataTableEditTableBtn"], [data-cy="dataTableCreateViewBtn"], [data-cy="dataTableCreateColumnBtn"], [data-cy="dataTableShareBtn"], [data-cy="dataTableExportAllBtn"]',
)

for (let attempt = 1; attempt <= 3; attempt++) {
Expand Down Expand Up @@ -97,9 +97,11 @@ function getTableActionLocator(page: Page, optionName: string) {
case 'Share':
return page.locator('[data-cy="dataTableShareBtn"]')
case 'Import':
return page.locator('[data-cy="dataTableExportBtn"]').filter({ hasText: /^Import$/ })
case 'Export as CSV':
return page.locator('[data-cy="dataTableExportBtn"]').filter({ hasText: /^Export as CSV$/ })
return page.locator('[data-cy="dataTableImportBtn"]')
case 'Export all rows':
return page.locator('[data-cy="dataTableExportAllBtn"]')
case 'Export filtered rows':
return page.locator('[data-cy="dataTableExportFilteredBtn"]')
default:
return null
}
Expand Down
4 changes: 2 additions & 2 deletions src/modules/main/partials/TableView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
@create-row="createRow"
@edit-row="editRow"
@delete-selected-rows="deleteSelectedRows">
<template #actions>
<slot name="actions" />
<template #actions="slotProps">
<slot name="actions" v-bind="slotProps" />
</template>
</NcTable>
</template>
Expand Down
36 changes: 27 additions & 9 deletions src/modules/main/sections/DataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@
</template>
{{ t('tables', 'Import') }}
</NcActionButton>
<NcActionButton v-if="canReadData(table)" :close-after-click="true"
icon="icon-download"
<NcActionButton v-if="canReadData(table)"
:close-after-click="true"
@click="$emit('download-csv')">
{{ t('tables', 'Export as CSV') }}
<template #icon>
<TrayArrowDown :size="20" decorative />
</template>
{{ t('tables', 'Export all rows') }}
</NcActionButton>
<NcActionButton v-if="canShareElement(table)"
:close-after-click="true"
Expand Down Expand Up @@ -85,7 +88,7 @@
:can-edit-columns="canManageTable(table)"
:can-delete-columns="canManageTable(table)"
:can-delete-table="canManageTable(table)">
<template #actions>
<template #actions="{ isFiltered, onExportFiltered }">
<NcActions :force-menu="true" :type="isViewSettingSet ? 'secondary' : 'tertiary'">
<NcActionCaption v-if="canManageElement(table)" :name="t('tables', 'Manage table')" />
<NcActionButton v-if="canManageElement(table)"
Expand Down Expand Up @@ -115,16 +118,29 @@
<NcActionCaption :name="t('tables', 'Integration')" />
<NcActionButton v-if="canCreateRowInElement(table)"
:close-after-click="true"
data-cy="dataTableExportBtn" @click="$emit('import', table)">
data-cy="dataTableImportBtn" @click="$emit('import', table)">
<template #icon>
<Import :size="20" decorative title="Import" />
</template>
{{ t('tables', 'Import') }}
</NcActionButton>
<NcActionButton v-if="canReadData(table)" :close-after-click="true"
icon="icon-download"
data-cy="dataTableExportBtn" @click="$emit('download-csv')">
{{ t('tables', 'Export as CSV') }}
<NcActionButton v-if="canReadData(table)"
:close-after-click="true"
data-cy="dataTableExportAllBtn"
@click="$emit('download-csv')">
<template #icon>
<TrayArrowDown :size="20" decorative />
</template>
{{ t('tables', 'Export all rows') }}
</NcActionButton>
<NcActionButton v-if="canReadData(table) && isFiltered"
:close-after-click="true"
data-cy="dataTableExportFilteredBtn"
@click="onExportFiltered">
<template #icon>
<TrayArrowDown :size="20" decorative />
</template>
{{ t('tables', 'Export filtered rows') }}
</NcActionButton>
<NcActionButton v-if="canShareElement(table)"
data-cy="dataTableShareBtn"
Expand Down Expand Up @@ -157,6 +173,7 @@ import IconTool from 'vue-material-design-icons/TableCog.vue'
import TableView from '../partials/TableView.vue'
import EmptyTable from './EmptyTable.vue'
import Connection from 'vue-material-design-icons/Connection.vue'
import TrayArrowDown from 'vue-material-design-icons/TrayArrowDown.vue'
import Import from 'vue-material-design-icons/Import.vue'
import { NcActionButton, NcActions, NcActionCaption } from '@nextcloud/vue'
import { mapState } from 'pinia'
Expand All @@ -169,6 +186,7 @@ export default {
TableView,
NcActionButton,
Connection,
TrayArrowDown,
NcActionCaption,
NcActions,
TableColumnPlusAfter,
Expand Down
7 changes: 6 additions & 1 deletion src/modules/main/sections/PublicElement.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
:can-edit-columns="false" :can-delete-columns="false" :can-delete-table="false">
<template #actions>
<NcActions :force-menu="true" type="tertiary">
<NcActionButton :close-after-click="true" icon="icon-download" data-cy="dataTableExportBtn"
<NcActionButton :close-after-click="true" data-cy="dataTableExportBtn"
@click="$emit('download-csv')">
<template #icon>
<TrayArrowDown :size="20" decorative />
</template>
{{ t('tables', 'Export as CSV') }}
</NcActionButton>
</NcActions>
Expand All @@ -30,11 +33,13 @@ import EmptyView from './EmptyView.vue'
import TableDescription from './TableDescription.vue'
import ElementTitle from './ElementTitle.vue'
import { NcActions, NcActionButton } from '@nextcloud/vue'
import TrayArrowDown from 'vue-material-design-icons/TrayArrowDown.vue'

export default {
name: 'PublicElement',
components: {
EmptyView,
TrayArrowDown,
TableView,
NcActions,
NcActionButton,
Expand Down
21 changes: 17 additions & 4 deletions src/modules/main/sections/View.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
:can-edit-columns="canManageTable(view)"
:can-delete-columns="canManageTable(view)"
:can-delete-table="canManageTable(view)">
<template #actions>
<template #actions="{ isFiltered, onExportFiltered }">
<NcActions :force-menu="true" :type="isViewSettingSet ? 'secondary' : 'tertiary'">
<NcActionCaption v-if="canManageElement(view)" :name="t('tables', 'Manage view')" />
<NcActionButton v-if="canManageElement(view) "
Expand All @@ -49,10 +49,21 @@
</template>
{{ t('tables', 'Import') }}
</NcActionButton>
<NcActionButton v-if="canReadData(view)" :close-after-click="true"
icon="icon-download"
<NcActionButton v-if="canReadData(view)"
:close-after-click="true"
@click="$emit('download-csv')">
{{ t('tables', 'Export as CSV') }}
<template #icon>
<TrayArrowDown :size="20" decorative />
</template>
{{ t('tables', 'Export all rows') }}
</NcActionButton>
<NcActionButton v-if="canReadData(view) && isFiltered"
:close-after-click="true"
@click="onExportFiltered">
<template #icon>
<TrayArrowDown :size="20" decorative />
</template>
{{ t('tables', 'Export filtered rows') }}
</NcActionButton>
<NcActionButton v-if="canShareElement(view)"
:close-after-click="true"
Expand Down Expand Up @@ -84,6 +95,7 @@ import { emit } from '@nextcloud/event-bus'
import { NcActions, NcActionButton, NcActionCaption } from '@nextcloud/vue'
import TableColumnPlusAfter from 'vue-material-design-icons/TableColumnPlusAfter.vue'
import PlaylistEdit from 'vue-material-design-icons/PlaylistEdit.vue'
import TrayArrowDown from 'vue-material-design-icons/TrayArrowDown.vue'
import IconImport from 'vue-material-design-icons/Import.vue'
import Connection from 'vue-material-design-icons/Connection.vue'
import ElementTitle from './ElementTitle.vue'
Expand All @@ -93,6 +105,7 @@ export default {
components: {
TableDescription,
EmptyView,
TrayArrowDown,
TableView,
PlaylistEdit,
IconImport,
Expand Down
6 changes: 3 additions & 3 deletions src/modules/navigation/partials/NavigationTableItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
<NcActionButton @click="exportFile">
{{ t('tables', 'Export') }}
<template #icon>
<Export :size="20" />
<TrayArrowDown :size="20" />
</template>
</NcActionButton>
<!-- INTEGRATION -->
Expand Down Expand Up @@ -151,7 +151,7 @@ import activityMixin from '../../../shared/mixins/activityMixin.js'
import { getCurrentUser, getRequestToken } from '@nextcloud/auth'
import Connection from 'vue-material-design-icons/Connection.vue'
import Import from 'vue-material-design-icons/Import.vue'
import Export from 'vue-material-design-icons/Export.vue'
import TrayArrowDown from 'vue-material-design-icons/TrayArrowDown.vue'
import NavigationViewItem from './NavigationViewItem.vue'
import PlaylistPlus from 'vue-material-design-icons/PlaylistPlus.vue'
import IconRename from 'vue-material-design-icons/RenameOutline.vue'
Expand All @@ -169,7 +169,7 @@ export default {
ArchiveArrowDown,
ArchiveArrowUpOutline,
Import,
Export,
TrayArrowDown,
NavigationViewItem,
NcActionButton,
NcAppNavigationItem,
Expand Down
Loading
Loading