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: 13 additions & 0 deletions cypress/component/menuItems.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
outdentItem,
parentOfPosition,
removeItem,
toEditableMenuItems,
toMenuItemPayload,
} from '../../src/shared/utils/menuItems.js'

Expand Down Expand Up @@ -118,6 +119,18 @@ describe('menu items', () => {
expect(dragItemOver(items, 2, 2)).to.equal(null)
})

it('turns the stored menu into editor items that keep their parent', () => {
const editable = toEditableMenuItems([
{ id: 1, label: 'Cases', icon: 'star', targetType: 'table', targetId: 4, url: null, technicalName: 'cases', parentId: null },
{ id: 2, label: 'Docs', targetType: 'url', url: 'https://example.org', technicalName: 'docs', parentId: 1 },
])

expect(editable.map(entry => entry.key)).to.deep.equal(['item-1', 'item-2'])
expect(editable[0]).to.deep.include({ label: 'Cases', icon: 'star', targetType: 'table', targetId: 4, url: '', parentKey: null })
expect(editable[1]).to.deep.include({ icon: null, targetId: null, url: 'https://example.org', parentKey: 'item-1' })
expect(toEditableMenuItems(null)).to.deep.equal([])
})

it('routes a view or table entry inside the app and leaves a link outside it', () => {
expect(menuItemRoute({ targetType: 'view', technicalName: 'home' }, 7)).to.equal('/application/7/menu/home')
expect(menuItemRoute({ targetType: 'table', technicalName: 'cases' }, 7)).to.equal('/application/7/menu/cases')
Expand Down
51 changes: 51 additions & 0 deletions playwright/e2e/application.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import type { Page } from '@playwright/test'
import { test, expect } from '../support/fixtures'
import { ocsRequest, type TestUser } from '../support/api'
import { createContext, loadContext } from '../support/commands'

/**
* Creates a grid view and an application whose menu opens it, through the API.
Expand Down Expand Up @@ -48,4 +49,54 @@ test.describe('Applications', () => {
// Nothing focused the content area before the person touched the page, so no focus ring is drawn around it
expect(await page.evaluate(() => document.activeElement?.id ?? '')).not.toBe('app-content-vue')
})

test('A page with widgets added on the overview gets a menu entry and opens', async ({ userPage: { page } }) => {
await page.goto('/index.php/apps/tables')
await createContext(page, 'overview application')
await loadContext(page, 'overview application')

await page.locator('[data-cy="context-add-page"]').getByRole('button').first().click()
await page.getByRole('menuitem', { name: 'Page with widgets' }).click()
const dialog = page.locator('[data-cy="gridViewSettingsModal"]')
await dialog.getByRole('textbox', { name: 'Title', exact: true }).fill('Intake home')
await dialog.locator('[data-cy="gridViewSubmit"]').click()

await expect(page).toHaveURL(/\/menu\/intake_home$/)
await expect(page.locator('[data-cy="context-breadcrumb"]')).toContainText('Intake home')
await expect(page.locator('[data-cy="gridViewHeading"]')).toContainText('Intake home')

await page.locator('[data-cy="context-menu-overview"]').click()
await expect(page.locator('[data-cy="page-card"]').filter({ hasText: 'Intake home' })).toContainText('Page with widgets')
})

test('A menu entry is reordered with its buttons, sits under another one and keeps its place after a reload', async ({ userPage: { page } }) => {
await page.goto('/index.php/apps/tables')
await createContext(page, 'nested menu application')
await loadContext(page, 'nested menu application')

await page.locator('[data-cy="context-menu-edit"]').click()
for (const [label, url] of [['Handbook', 'https://docs.nextcloud.com'], ['Forum', 'https://help.nextcloud.com']]) {
await page.locator('[data-cy="menuItemAdd"]').click()
const row = page.locator('[data-cy="menuItemRow"]').last()
await row.getByRole('textbox', { name: 'Label' }).fill(label)
await row.getByRole('textbox', { name: 'Link' }).fill(url)
}

// Without a pointer: move Forum to the top, then put Handbook under it
await page.locator('[data-cy="menuItemRow"]').nth(1).locator('[data-cy="menuItemMoveUp"]').click()
await expect(page.locator('[data-cy="menuItemRow"]').first().getByRole('textbox', { name: 'Label' })).toHaveValue('Forum')
await page.locator('[data-cy="menuItemRow"]').nth(1).locator('[data-cy="menuItemIndent"]').click()
await expect(page.locator('[data-cy="menuItemRow"]').nth(1).locator('[data-cy="menuItemOutdent"]')).toBeVisible()

const saveResponse = page.waitForResponse(response => response.url().includes('/apps/tables/api/2/contexts/') && response.request().method() === 'PUT')
await page.locator('[data-cy="context-menu-save"]').click()
expect((await saveResponse).status()).toBe(200)

await page.reload()
const pages = page.locator('[data-cy="page-card"]')
await expect(pages).toHaveCount(2)
await expect(pages.nth(0)).toContainText('Forum')
await expect(pages.nth(1)).toContainText('1.1')
await expect(pages.nth(1)).toContainText('Handbook')
})
})
5 changes: 4 additions & 1 deletion playwright/e2e/column-relation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,11 @@ test.describe('Test column relation', () => {

await page.locator('[data-cy="editContextSubmitBtn"]').click()

// load the context and verify the relation label is rendered without crashing
// the tables of an application render in the application itself, opened on its own
await loadContext(page, contextTitle)
const contextId = page.url().match(/\/application\/(\d+)/)?.[1]
await page.goto(`/index.php/apps/tables/app/${contextId}`)
// verify the relation label is rendered without crashing
await expect(
page.locator('[data-cy="ncTable"] [data-cy="customTableRow"]').filter({ hasText: 'Bob' }),
).toBeVisible()
Expand Down
46 changes: 33 additions & 13 deletions playwright/e2e/context.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@
await loadContext(page, contextTitle)
}

/**
* Inside Tables the application page is where an application is configured; its tables and
* views render in the application itself.
*/
async function openApplication(page: Page, contextTitle: string) {
await loadContext(page, contextTitle)
const contextId = page.url().match(/\/application\/(\d+)/)?.[1]
if (!contextId) {
throw new Error(`Could not find the id of application ${contextTitle}`)
}
await page.goto(`/index.php/apps/tables/app/${contextId}`)
await expect(page.locator('[data-cy="application-nav-header"]')).toContainText(contextTitle, { timeout: 10000 })
}

async function selectFromVueDropdown(page: Page, inputSelector: string, value: string) {
const input = page.locator(inputSelector).first()
await input.click()
Expand Down Expand Up @@ -92,11 +106,10 @@
await expect(page.getByRole('dialog', { name: 'Edit application' })).toBeHidden({ timeout: 10000 })

await expect(page.locator('[data-cy="navigationContextItem"]').filter({ hasText: `updated ${contextTitle}` })).toBeVisible()
await loadContext(page, `updated ${contextTitle}`)
await openApplication(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()
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 })
await expect(page.locator('h1', { hasText: viewTitle })).toBeVisible({ timeout: 10000 })
})

test('Share context with resources', async ({ userPage: { page }, request }) => {
Expand All @@ -117,20 +130,18 @@
const updateResponse = page.waitForResponse(r => r.url().includes('/apps/tables/') && r.request().method() === 'PUT')
await page.locator('[data-cy="editContextSubmitBtn"]').click()
await updateResponse
await expect(page.locator('[class*="toast_success"]').first()).toBeVisible({ timeout: 10000 })

Check failure on line 133 in playwright/e2e/context.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable33)

[chromium] › playwright/e2e/context.spec.ts:115:2 › Manage a context › Share context with resources

1) [chromium] › playwright/e2e/context.spec.ts:115:2 › Manage a context › Share context with resources Error: expect(locator).toBeVisible() failed Locator: locator('[class*="toast_success"]').first() Expected: visible Timeout: 10000ms Error: element(s) not found Call log: - Expect "toBeVisible" locator('[class*="toast_success"]').first() with timeout 10000ms - waiting for locator('[class*="toast_success"]').first() 131 | await page.locator('[data-cy="editContextSubmitBtn"]').click() 132 | await updateResponse > 133 | await expect(page.locator('[class*="toast_success"]').first()).toBeVisible({ timeout: 10000 }) | ^ 134 | 135 | await openApplication(page, contextTitle) 136 | await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 }) at /home/runner/work/tables/tables/playwright/e2e/context.spec.ts:133:66

Check failure on line 133 in playwright/e2e/context.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable34)

[chromium] › playwright/e2e/context.spec.ts:115:2 › Manage a context › Share context with resources

1) [chromium] › playwright/e2e/context.spec.ts:115:2 › Manage a context › Share context with resources Error: expect(locator).toBeVisible() failed Locator: locator('[class*="toast_success"]').first() Expected: visible Timeout: 10000ms Error: element(s) not found Call log: - Expect "toBeVisible" locator('[class*="toast_success"]').first() with timeout 10000ms - waiting for locator('[class*="toast_success"]').first() 131 | await page.locator('[data-cy="editContextSubmitBtn"]').click() 132 | await updateResponse > 133 | await expect(page.locator('[class*="toast_success"]').first()).toBeVisible({ timeout: 10000 }) | ^ 134 | 135 | await openApplication(page, contextTitle) 136 | await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 }) at /home/runner/work/tables/tables/playwright/e2e/context.spec.ts:133:66

await loadContext(page, contextTitle)
await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible({ timeout: 10000 })
await openApplication(page, contextTitle)
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 })

// verify context was shared properly
await page.context().clearCookies()
await login(page, nonLocalUser)
await page.goto('/index.php/apps/tables')

await loadContext(page, contextTitle)
await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible()
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible()
await openApplication(page, contextTitle)
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 })
})

test('Transfer context', async ({ userPage: { page }, request }) => {
Expand Down Expand Up @@ -221,16 +232,22 @@
await page.locator('[data-cy="editContextSubmitBtn"]').click()

await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible()
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible()
await openApplication(page, contextTitle)
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 })

await page.goto('/index.php/apps/tables')
await loadContext(page, contextTitle)
await openContextEditModal(page, contextTitle)
await page.locator('[data-cy="contextResourceList"] button').filter({ hasText: 'Delete' }).click({ force: true })

await expect(page.locator('[data-cy="contextResourceList"]', { hasText: tableTitle })).toBeHidden()
await page.locator('[data-cy="editContextSubmitBtn"]').click()

await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible()
await openApplication(page, contextTitle)
// The application has loaded once its loading icon is gone; only then does a missing table mean anything
await expect(page.locator('.main-context-view > .icon-loading')).toHaveCount(0, { timeout: 10000 })
await expect(page.locator('h1', { hasText: tableTitle })).toBeHidden()

Check failure on line 250 in playwright/e2e/context.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable35)

[chromium] › playwright/e2e/context.spec.ts:219:2 › Manage a context › Remove context resource

1) [chromium] › playwright/e2e/context.spec.ts:219:2 › Manage a context › Remove context resource Error: expect(locator).toBeHidden() failed Locator: locator('h1').filter({ hasText: 'test table resource' }) Expected: hidden Received: visible Timeout: 30000ms Call log: - Expect "toBeHidden" locator('h1').filter({ hasText: 'test table resource' }) with timeout 30000ms - waiting for locator('h1').filter({ hasText: 'test table resource' }) 64 × locator resolved to <h1 data-v-d0e78243="">🔧 test table resource</h1> - unexpected value "visible" 248 | // The application has loaded once its loading icon is gone; only then does a missing table mean anything 249 | await expect(page.locator('.main-context-view > .icon-loading')).toHaveCount(0, { timeout: 10000 }) > 250 | await expect(page.locator('h1', { hasText: tableTitle })).toBeHidden() | ^ 251 | }) 252 | 253 | test('Modify resource rows and columns from context', async ({ userPage: { page } }) => { at /home/runner/work/tables/tables/playwright/e2e/context.spec.ts:250:61

Check failure on line 250 in playwright/e2e/context.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (master)

[chromium] › playwright/e2e/context.spec.ts:219:2 › Manage a context › Remove context resource

1) [chromium] › playwright/e2e/context.spec.ts:219:2 › Manage a context › Remove context resource Error: expect(locator).toBeHidden() failed Locator: locator('h1').filter({ hasText: 'test table resource' }) Expected: hidden Received: visible Timeout: 30000ms Call log: - Expect "toBeHidden" locator('h1').filter({ hasText: 'test table resource' }) with timeout 30000ms - waiting for locator('h1').filter({ hasText: 'test table resource' }) 63 × locator resolved to <h1 data-v-d0e78243="">🔧 test table resource</h1> - unexpected value "visible" 248 | // The application has loaded once its loading icon is gone; only then does a missing table mean anything 249 | await expect(page.locator('.main-context-view > .icon-loading')).toHaveCount(0, { timeout: 10000 }) > 250 | await expect(page.locator('h1', { hasText: tableTitle })).toBeHidden() | ^ 251 | }) 252 | 253 | test('Modify resource rows and columns from context', async ({ userPage: { page } }) => { at /home/runner/work/tables/tables/playwright/e2e/context.spec.ts:250:61
})

test('Modify resource rows and columns from context', async ({ userPage: { page } }) => {
Expand All @@ -248,7 +265,10 @@
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 expect(page.getByRole('dialog', { name: 'Edit application' })).toBeHidden({ timeout: 10000 })

await openApplication(page, contextTitle)
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 })
await createTextLineColumn(page, 'title', '', '', true)
await page.locator('button').filter({ hasText: 'Create row' }).first().click()
await fillInValueTextLine(page, 'title', 'first row')
Expand Down Expand Up @@ -293,16 +313,16 @@
await updatePermsResponse

await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible({ timeout: 10000 })
await openApplication(page, contextTitle)
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 })

// verify that shared user can modify and delete data in the resource
await page.context().clearCookies()
await login(page, nonLocalUser)
await page.goto('/index.php/apps/tables')

await loadContext(page, contextTitle)
await expect(page.locator('h1', { hasText: contextTitle })).toBeVisible()
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible()
await openApplication(page, contextTitle)
await expect(page.locator('h1', { hasText: tableTitle })).toBeVisible({ timeout: 10000 })

await page.locator('button').filter({ hasText: 'Create row' }).first().click()
await fillInValueTextLine(page, 'title', 'first row')
Expand Down
15 changes: 12 additions & 3 deletions src/modules/modals/GridViewSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,13 @@ export default {
type: Object,
default: null,
},
// Passed on with the created event, so an application can add the new view to its menu
contextId: {
type: Number,
default: null,
},
},
emits: ['close'],
emits: ['close', 'created'],
data() {
return {
title: '',
Expand Down Expand Up @@ -97,7 +102,8 @@ export default {
this.saving = false
},
/**
* Saves the view, or creates it and opens it.
* Saves the view, or creates it and opens it. A view created for an application is opened
* by the application, through its menu.
*/
async submit() {
this.errorTitle = this.title.trim() === ''
Expand All @@ -117,8 +123,11 @@ export default {
const created = await this.insertStandaloneView({ data })
this.saving = false
if (created) {
this.$emit('created', { view: created, contextId: this.contextId })
this.$emit('close')
await this.$router.push('/view/' + created.id).catch(err => err)
if (this.contextId === null) {
await this.$router.push('/view/' + created.id).catch(err => err)
}
}
},
},
Expand Down
13 changes: 12 additions & 1 deletion src/modules/modals/Modals.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
<GridViewSettings
:show-modal="gridView !== null"
:view="gridView?.view ?? null"
:context-id="gridView?.contextId ?? null"
@created="emitGridViewCreated"
@close="gridView = null" />

<Import
Expand All @@ -64,6 +66,7 @@

<script>

import { emit } from '@nextcloud/event-bus'
import { useEventBusSubscriptions } from '../../shared/composables/useEventBusSubscriptions.js'
import CreateRow from './CreateRow.vue'
import ImportScheme from './ImportScheme.vue'
Expand Down Expand Up @@ -159,7 +162,7 @@ export default {
}
this.viewToEdit = { ...view, createView: false }
})
this.subscribeToEventBus('tables:view:create-grid', () => { this.gridView = { view: null } })
this.subscribeToEventBus('tables:view:create-grid', payload => { this.gridView = { view: null, contextId: payload?.contextId ?? null } })
this.subscribeToEventBus('tables:view:create', tableInfos => {
this.viewToEdit = {
view: { tableId: tableInfos.tableId, sort: [], filter: [] },
Expand Down Expand Up @@ -196,6 +199,14 @@ export default {

},
methods: {
/**
* Tells the application a grid view was created from, so it can add the view to its menu.
*
* @param {{view: object, contextId: number|null}} payload the created view and its application
*/
emitGridViewCreated(payload) {
emit('tables:view:grid-created', payload)
},
onSaveNewColumn(event) {
if (this.createColumnInfo?.onSave) {
this.createColumnInfo.onSave(event)
Expand Down
Loading
Loading