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
13 changes: 2 additions & 11 deletions playwright/e2e/column-relation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
createTextLineColumn,
loadContext,
loadTable,
openContextEditModal,
openCreateColumnModal,
openCreateRowModal,
fillInValueTextLine,
Expand Down Expand Up @@ -147,16 +146,8 @@ test.describe('Test column relation', () => {
await selectFromVueDropdown(page, 'Select relation value', 'Bob')
await page.locator('[data-cy="createRowSaveButton"]').click()

// add the target table to an application context
await createContext(page, contextTitle)
await openContextEditModal(page, contextTitle)

const resourceInput = page.locator('[data-cy="contextResourceForm"] input').first()
await resourceInput.click()
await resourceInput.fill(contextTargetTitle)
await page.getByRole('option', { name: new RegExp(contextTargetTitle, 'i') }).first().click()

await page.locator('[data-cy="editContextSubmitBtn"]').click()
// an application created with the target table shows it on its start page
await createContext(page, contextTitle, false, [contextTargetTitle])

// load the context and verify the relation label is rendered without crashing
await loadContext(page, contextTitle)
Expand Down
39 changes: 32 additions & 7 deletions playwright/e2e/context-grid-pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,21 +48,46 @@
}

test.describe('Grid pages of an application', () => {
test('The start page lists the tables as before, with the page bar under the description', async ({ userPage: { page, user } }) => {
test('The start page of a new application shows its name and its tables as widgets', async ({ userPage: { page, user } }) => {
const first = await createTableViaApi(page.request, user, 'grid first table')
const second = await createTableViaApi(page.request, user, 'grid second table')
const contextId = await createApplicationViaApi(page.request, user, 'grid existing app', [first, second])

await openApplication(page, contextId)

const firstHeading = page.locator('h1', { hasText: 'grid first table' })
const secondHeading = page.locator('h1', { hasText: 'grid second table' })
await expect(firstHeading).toBeVisible()
await expect(secondHeading).toBeVisible()
const startPage = page.locator('[data-cy="startPageGrid"]')
await expect(startPage).toBeVisible()
await expect(startPage.locator('.header-widget')).toContainText('grid existing app')
const firstHeading = page.locator('[data-cy="gridWidget"]').getByRole('heading', { name: 'grid first table' })
const secondHeading = page.locator('[data-cy="gridWidget"]').getByRole('heading', { name: 'grid second table' })
await expect(firstHeading).toBeVisible({ timeout: 30000 })
await expect(secondHeading).toBeVisible({ timeout: 30000 })
expect((await firstHeading.boundingBox())!.y).toBeLessThan((await secondHeading.boundingBox())!.y)
await expect(page.locator('[data-cy="gridWidget"]')).toHaveCount(3)
await expect(page.locator('[data-cy="contextStartPageLink"]')).toHaveAttribute('aria-selected', 'true')
await expect(page.locator('[data-cy="contextPageAdd"]')).toBeVisible()
await expect(page.locator('[data-cy="gridPage"]')).toHaveCount(0)
await expect(page.locator('.resources')).toHaveCount(0)
})

test('The start page can be edited like a grid page, but not deleted', async ({ userPage: { page, user } }) => {
const table = await createTableViaApi(page.request, user, 'grid start edit table')
const contextId = await createApplicationViaApi(page.request, user, 'grid start edit app', [table])
await openApplication(page, contextId)

await page.locator('[data-cy="gridPageEdit"]').click()
await page.locator('[data-cy="gridPageAddWidget"]').click()
await pickOption(page, 'addWidgetType', 'Description')
await page.locator('[data-cy="widgetFieldText"] textarea').fill('Read this first')
await page.locator('[data-cy="addWidgetSubmit"]').click()
const saved = page.waitForResponse(r => /\/pages\/\d+/.test(r.url()) && r.request().method() === 'PATCH')
await page.locator('[data-cy="gridPageSave"]').click()
expect((await saved).ok()).toBe(true)

Check failure on line 84 in playwright/e2e/context-grid-pages.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable34)

[chromium] › playwright/e2e/context-grid-pages.spec.ts:72:2 › Grid pages of an application › The start page can be edited like a grid page

1) [chromium] › playwright/e2e/context-grid-pages.spec.ts:72:2 › Grid pages of an application › The start page can be edited like a grid page, but not deleted Error: expect(received).toBe(expected) // Object.is equality Expected: true Received: false 82 | const saved = page.waitForResponse(r => /\/pages\/\d+/.test(r.url()) && r.request().method() === 'PATCH') 83 | await page.locator('[data-cy="gridPageSave"]').click() > 84 | expect((await saved).ok()).toBe(true) | ^ 85 | 86 | await page.reload() 87 | await expect(page.getByText('Read this first')).toBeVisible({ timeout: 30000 }) at /home/runner/work/tables/tables/playwright/e2e/context-grid-pages.spec.ts:84:30

await page.reload()
await expect(page.getByText('Read this first')).toBeVisible({ timeout: 30000 })
// With only one action left the menu shows it as a button of its own
await expect(page.getByRole('button', { name: 'Edit page details' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Page actions' })).toHaveCount(0)
})

test('A new page holds a header and a table of the application after a reload', async ({ userPage: { page, user } }) => {
Expand Down Expand Up @@ -123,7 +148,7 @@
const startPage = Object.values(application.pages as Record<string, { page_type: string, content: object }>).find(entry => entry.page_type === 'startpage')
expect(Object.keys(startPage!.content)).toHaveLength(1)
await page.locator('[data-cy="contextStartPageLink"]').click()
await expect(page.locator('h1', { hasText: 'grid table in app' })).toBeVisible({ timeout: 30000 })
await expect(page.locator('[data-cy="gridWidget"]').getByRole('heading', { name: 'grid table in app' })).toBeVisible({ timeout: 30000 })
await expect(page.locator('[data-cy="gridPage"]')).toHaveCount(0)
})

Expand Down
42 changes: 34 additions & 8 deletions playwright/e2e/context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ async function expectSelectedShare(page: Page, userId: string) {
).toBeVisible()
}

/**
* Places a table of the application on its start page. Tables added after the application was
* created are not placed there by themselves.
*/
async function addTableWidgetToStartPage(page: Page, tableTitle: string) {
await page.locator('[data-cy="gridPageEdit"]').click()
await page.locator('[data-cy="gridPageAddWidget"]').click()
await selectFromVueDropdown(page, '[data-cy="addWidgetType"] input', 'Table or view')
await selectFromVueDropdown(page, '[data-cy="addWidgetNode"] input', tableTitle)
await page.locator('[data-cy="addWidgetSubmit"]').click()
const saved = page.waitForResponse(r => /\/pages\/\d+/.test(r.url()) && r.request().method() === 'PATCH')
await page.locator('[data-cy="gridPageSave"]').click()
expect((await saved).ok()).toBe(true)
}

test.describe('Manage a context', () => {
test.describe.configure({ mode: 'serial' })

Expand Down Expand Up @@ -95,8 +110,15 @@ test.describe('Manage a context', () => {
await loadContext(page, `updated ${contextTitle}`)

await expect(page.locator('h1', { hasText: `updated ${contextTitle}` })).toBeVisible()
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible()
await expect(page.locator('h1', { hasText: viewTitle })).toBeVisible()
// Tables and views added after the application was created are not placed on its start page
await expect(page.locator('[data-cy="startPageGrid"]')).toBeVisible()
await expect(page.locator('[data-cy="gridWidget"]').getByRole('heading', { name: tableTitle })).toHaveCount(0)
await page.locator('[data-cy="gridPageEdit"]').click()
await page.locator('[data-cy="gridPageAddWidget"]').click()
await selectFromVueDropdown(page, '[data-cy="addWidgetType"] input', 'Table or view')
await page.locator('[data-cy="addWidgetNode"] input').click()
await expect(page.locator('.vs__dropdown-menu li').filter({ hasText: tableTitle })).toBeVisible()
await expect(page.locator('.vs__dropdown-menu li').filter({ hasText: viewTitle })).toBeVisible()
})

test('Share context with resources', async ({ userPage: { page }, request }) => {
Expand All @@ -121,7 +143,8 @@ test.describe('Manage a context', () => {

await loadContext(page, contextTitle)
await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible({ timeout: 10000 })
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 })
await addTableWidgetToStartPage(page, tableTitle)
await expect(page.locator('[data-cy="gridWidget"]').getByRole('heading', { name: tableTitle })).toBeVisible({ timeout: 10000 })

// verify context was shared properly
await page.context().clearCookies()
Expand All @@ -130,7 +153,7 @@ test.describe('Manage a context', () => {

await loadContext(page, contextTitle)
await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible()
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible()
await expect(page.locator('[data-cy="gridWidget"]').getByRole('heading', { name: tableTitle })).toBeVisible()
})

test('Transfer context', async ({ userPage: { page }, request }) => {
Expand Down Expand Up @@ -221,7 +244,8 @@ test.describe('Manage a context', () => {
await page.locator('[data-cy="editContextSubmitBtn"]').click()

await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible()
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible()
await addTableWidgetToStartPage(page, tableTitle)
await expect(page.locator('[data-cy="gridWidget"]').getByRole('heading', { name: tableTitle })).toBeVisible()

await openContextEditModal(page, contextTitle)
await page.locator('[data-cy="contextResourceList"] button').filter({ hasText: 'Delete' }).click({ force: true })
Expand All @@ -230,7 +254,7 @@ test.describe('Manage a context', () => {
await page.locator('[data-cy="editContextSubmitBtn"]').click()

await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible()
await expect(page.locator('h1', { hasText: tableTitle })).toBeHidden()
await expect(page.locator('[data-cy="gridWidget"]').getByRole('heading', { name: tableTitle })).toBeHidden()
})

test('Modify resource rows and columns from context', async ({ userPage: { page } }) => {
Expand All @@ -248,6 +272,7 @@ test.describe('Manage a context', () => {
await expect(page.locator('[data-cy="contextResourceList"]')).toContainText(tableTitle)
await expect(page.locator('[data-cy="contextResourcePerms"]')).toContainText(tableTitle)
await page.locator('[data-cy="editContextSubmitBtn"]').click()
await addTableWidgetToStartPage(page, tableTitle)

await createTextLineColumn(page, 'title', '', '', true)
await page.locator('button').filter({ hasText: 'Create row' }).first().click()
Expand Down Expand Up @@ -293,7 +318,8 @@ test.describe('Manage a context', () => {
await updatePermsResponse

await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible({ timeout: 10000 })
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 })
await addTableWidgetToStartPage(page, tableTitle)
await expect(page.locator('[data-cy="gridWidget"]').getByRole('heading', { name: tableTitle })).toBeVisible({ timeout: 10000 })

// verify that shared user can modify and delete data in the resource
await page.context().clearCookies()
Expand All @@ -302,7 +328,7 @@ test.describe('Manage a context', () => {

await loadContext(page, contextTitle)
await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible()
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible()
await expect(page.locator('[data-cy="gridWidget"]').getByRole('heading', { name: tableTitle })).toBeVisible()

await page.locator('button').filter({ hasText: 'Create row' }).first().click()
await fillInValueTextLine(page, 'title', 'first row')
Expand Down
17 changes: 17 additions & 0 deletions playwright/support/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,10 +370,20 @@ export async function openCreateColumnModal(
await expect(page.locator('[data-cy="columnTypeFormInput"]')).toBeVisible()
}

/**
* Creates an application. Tables and views given by title are its resources from the start, so
* its start page shows them.
*
* @param page the page
* @param title the title of the application
* @param showInNav whether it shows in the app list
* @param resourceTitles titles of its tables and views
*/
export async function createContext(
page: Page,
title: string,
showInNav: boolean = false,
resourceTitles: string[] = [],
) {
await ensureNavigationOpen(page)

Expand Down Expand Up @@ -404,6 +414,13 @@ export async function createContext(
.locator('[data-cy="createContextShowInNavSwitch"] input')
.check({ force: true })
}
for (const resourceTitle of resourceTitles) {
const resourceInput = createContextModal.locator('[data-cy="contextResourceForm"] input').first()
await resourceInput.click()
await resourceInput.fill(resourceTitle)
await page.getByRole('option', { name: new RegExp(resourceTitle, 'i') }).first().click()
await expect(createContextModal.locator('[data-cy="contextResourceList"]')).toContainText(resourceTitle)
}
await page.locator('[data-cy="createContextSubmitBtn"]').click()

await expect(
Expand Down
28 changes: 20 additions & 8 deletions src/modules/grid/GridPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="grid-page" data-cy="gridPage">
<div class="grid-page__header">
<div class="grid-page__heading">
<div class="grid-page" :data-cy="isStartPage ? 'startPageGrid' : 'gridPage'">
<div v-if="showHeading || canEdit" class="grid-page__header">
<div v-if="showHeading" class="grid-page__heading">
<h2 class="grid-page__title" data-cy="gridPageTitle">
<NamedIcon v-if="page.icon" :name="page.icon" :size="20" />
{{ title }}
Expand Down Expand Up @@ -46,7 +46,10 @@
</template>
{{ t('tables', 'Edit page details') }}
</NcActionButton>
<NcActionButton :close-after-click="true" data-cy="gridPageDelete" @click="$emit('delete', page)">
<NcActionButton v-if="!isStartPage"
:close-after-click="true"
data-cy="gridPageDelete"
@click="$emit('delete', page)">
<template #icon>
<TrashCanOutline :size="20" />
</template>
Expand Down Expand Up @@ -110,7 +113,7 @@ import GridWidget from './GridWidget.vue'
import AddWidget from '../modals/AddWidget.vue'
import NamedIcon from '../../shared/components/ncIconPicker/NamedIcon.vue'
import { useTablesStore } from '../../store/store.js'
import { GRID_COLUMNS, GRID_MIN_WIDTH, cloneGrid, findWidgetType, gridToWidgets, nextFreeRow, pageTitle, pageToGrid } from './gridLayout.js'
import { GRID_COLUMNS, GRID_MIN_WIDTH, cloneGrid, findWidgetType, gridToWidgets, isStartPage, nextFreeRow, pageTitle, pageToGrid } from './gridLayout.js'

/**
* A grid page of an application: its widgets on a grid, with an edit mode that saves the
Expand All @@ -134,9 +137,11 @@ export default {
AddWidget,
NamedIcon,
},
provide: {
// Widget titles sit below the title of the page
gridHeadingLevel: 3,
provide() {
return {
// Widget titles sit below the title of the page, or of the application on a start page without one
gridHeadingLevel: computed(() => this.showHeading ? 3 : 2),
}
},
props: {
// The application, for its id and its nodes
Expand Down Expand Up @@ -201,6 +206,13 @@ export default {
title() {
return pageTitle(this.page)
},
isStartPage() {
return isStartPage(this.page)
},
// The start page sits under the title of its application, so it only has a heading of its own once it gets a title
showHeading() {
return !this.isStartPage || !!this.page.title || !!this.page.description
},
activeGrid() {
return this.draftGrid ?? this.storedGrid
},
Expand Down
35 changes: 34 additions & 1 deletion src/modules/grid/widgets/DataWidget.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
:view-setting="viewSetting"
@create-column="createColumn"
@import="openImportModal"
@download-csv="downloadCSV(rows)"
@download-filtered-csv="downloadCSV"
@update:view-setting="setting => viewSetting = setting" />
<TableWrapper v-else
:show-title="showTitle"
Expand All @@ -37,6 +39,9 @@
:view-setting="viewSetting"
@create-column="createColumn"
@import="openImportModal"
@import-scheme="openImportSchemeModal"
@download-csv="downloadCSV(rows)"
@download-filtered-csv="downloadCSV"
@update:view-setting="setting => viewSetting = setting" />
</template>
</div>
Expand All @@ -47,10 +52,12 @@ import { NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
import TableOff from 'vue-material-design-icons/TableOff.vue'
import { mapActions, mapState, storeToRefs } from 'pinia'
import { emit } from '@nextcloud/event-bus'
import { showError } from '@nextcloud/dialogs'
import TableWrapper from '../../main/sections/TableWrapper.vue'
import CustomView from '../../main/sections/View.vue'
import { useTablesStore } from '../../../store/store.js'
import { useDataStore } from '../../../store/data.js'
import exportTableMixin from '../../../shared/components/ncTable/mixins/exportTableMixin.js'

/**
* Shows the rows of one table or view of the application on a grid page, with the
Expand All @@ -65,6 +72,7 @@ export default {
TableWrapper,
CustomView,
},
mixins: [exportTableMixin],
props: {
// The table or view to show, as {type: 'table'|'view', id}; null when the widget has none,
// also when its table or view left the application
Expand Down Expand Up @@ -134,7 +142,7 @@ export default {
this.load()
},
methods: {
...mapActions(useTablesStore, ['loadContextTable', 'loadContextView']),
...mapActions(useTablesStore, ['loadContextTable', 'loadContextView', 'validateExportAccess']),
...mapActions(useDataStore, ['loadColumnsFromBE', 'loadRowsFromBE', 'loadRelationsFromBE']),
/**
* Loads the table or view with its columns, rows and relations.
Expand Down Expand Up @@ -162,6 +170,10 @@ export default {
await this.loadRowsFromBE({ viewId: null, tableId: this.targetId })
await this.loadRelationsFromBE({ tableId: this.targetId })
}
if (request === this.loadRequest) {
// The rows start in the sorting the table or view was saved with
this.viewSetting = this.element?.sort?.length ? { presetSorting: [...this.element.sort] } : {}
}
} catch {
// The store already reported the error
if (request === this.loadRequest) {
Expand All @@ -185,6 +197,27 @@ export default {
openImportModal() {
emit('tables:modal:import', { element: this.element, isView: this.isView })
},
/**
* Asks the app to open the scheme import dialog for the shown table.
*/
openImportSchemeModal() {
emit('tables:modal:table-scheme-import', { table: this.element })
},
/**
* Downloads rows of the shown table or view as CSV, after checking the user may still read it.
*
* @param {Array} rows the rows to download
*/
async downloadCSV(rows) {
const access = await this.validateExportAccess({ id: this.element.id, isView: this.isView })
if (!access?.ok) {
if (access?.reason === 'NO_ACCESS') {
showError(t('tables', 'Your access was revoked. Reload the page to update your permissions.'))
}
return
}
this.downloadCsv(rows, this.columns, this.element.title)
},
},
}
</script>
Expand Down
Loading
Loading