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
17 changes: 17 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 { APIRequestContext, Page } from '@playwright/test'
import { test, expect } from '../support/fixtures'
import { ocsRequest, type TestUser } from '../support/api'
import { createContext, ensureNavigationOpen } from '../support/commands'

test.use({ viewport: { width: 1600, height: 1000 } })

Expand Down Expand Up @@ -191,7 +192,7 @@
await page.getByRole('menuitem', { name: 'Edit name, icon and members' }).click()
await page.locator('[data-cy="editContextTechnicalName"]').fill(technicalName)
await page.locator('[data-cy="editContextSubmitBtn"]').click()
await expect(page.locator('[data-cy="context-address"]')).toContainText('/apps/tables/app/' + technicalName)

Check failure on line 195 in playwright/e2e/application.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable34)

[chromium] › playwright/e2e/application.spec.ts:185:2 › Applications › An application with a technical name is opened at its readable address

1) [chromium] › playwright/e2e/application.spec.ts:185:2 › Applications › An application with a technical name is opened at its readable address Error: expect(locator).toContainText(expected) failed Locator: locator('[data-cy="context-address"]') Expected substring: "/apps/tables/app/intake_1792143181892" Received string: "http://localhost:8089/index.php/apps/tables/app/7" Timeout: 30000ms Call log: - Expect "toContainText" locator('[data-cy="context-address"]') with timeout 30000ms - waiting for locator('[data-cy="context-address"]') 64 × locator resolved to <a rel="noopener" target="_blank" data-v-2eb5b8e7="" class="overview__address" data-cy="context-address" href="http://localhost:8089/index.php/apps/tables/app/7">http://localhost:8089/index.php/apps/tables/app/7</a> - unexpected value "http://localhost:8089/index.php/apps/tables/app/7" 193 | await page.locator('[data-cy="editContextTechnicalName"]').fill(technicalName) 194 | await page.locator('[data-cy="editContextSubmitBtn"]').click() > 195 | await expect(page.locator('[data-cy="context-address"]')).toContainText('/apps/tables/app/' + technicalName) | ^ 196 | // Pages carry their own technical name, shown next to their title 197 | await expect(page.locator('[data-cy="page-card-technical-name"]')).toHaveText(['dashboard']) 198 | at /home/runner/work/tables/tables/playwright/e2e/application.spec.ts:195:61
// Pages carry their own technical name, shown next to their title
await expect(page.locator('[data-cy="page-card-technical-name"]')).toHaveText(['dashboard'])

Expand All @@ -200,6 +201,22 @@
await expect(page).toHaveURL(/\/menu\/dashboard$/)
})

test('All applications are listed on one page that opens each of them', async ({ userPage: { page } }) => {
await page.goto('/index.php/apps/tables')
await createContext(page, 'listed application')

// Opening the new application collapses the Tables navigation
await ensureNavigationOpen(page)
await page.locator('[data-cy="navigationAllApplications"] a').first().click()
const card = page.locator('[data-cy="application-card"]').filter({ hasText: 'listed application' })
await expect(card).toBeVisible()
await expect(card).toContainText('1 menu item')
await card.click()

await expect(page).toHaveURL(/\/application\/\d+\/overview$/)
await expect(page.locator('[data-cy="context-title"]')).toContainText('listed application')
})

test('The overview lists the pages; a page added there gets a menu entry and opens in edit mode', async ({ userPage: { page, user } }) => {
const contextResponse = await ocsRequest(page.request, user, { method: 'POST', url: `${API}/contexts?format=json`, data: { name: 'overview application', iconName: 'star' } })
const contextId = (await contextResponse.json()).ocs.data.id
Expand Down Expand Up @@ -257,7 +274,7 @@

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)

Check failure on line 277 in playwright/e2e/application.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable35)

[chromium] › playwright/e2e/application.spec.ts:255:2 › Applications › A menu entry is reordered with its buttons

1) [chromium] › playwright/e2e/application.spec.ts:255:2 › Applications › A menu entry is reordered with its buttons, sits under another one and keeps its place after a reload Error: expect(received).toBe(expected) // Object.is equality Expected: 200 Received: 500 275 | const saveResponse = page.waitForResponse(response => response.url().includes('/apps/tables/api/2/contexts/') && response.request().method() === 'PUT') 276 | await page.locator('[data-cy="context-menu-save"]').click() > 277 | expect((await saveResponse).status()).toBe(200) | ^ 278 | 279 | await page.reload() 280 | const items = page.locator('[data-cy="context-menu-preview-item"]') at /home/runner/work/tables/tables/playwright/e2e/application.spec.ts:277:41

Check failure on line 277 in playwright/e2e/application.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (master)

[chromium] › playwright/e2e/application.spec.ts:255:2 › Applications › A menu entry is reordered with its buttons

1) [chromium] › playwright/e2e/application.spec.ts:255:2 › Applications › A menu entry is reordered with its buttons, sits under another one and keeps its place after a reload Error: expect(received).toBe(expected) // Object.is equality Expected: 200 Received: 500 275 | const saveResponse = page.waitForResponse(response => response.url().includes('/apps/tables/api/2/contexts/') && response.request().method() === 'PUT') 276 | await page.locator('[data-cy="context-menu-save"]').click() > 277 | expect((await saveResponse).status()).toBe(200) | ^ 278 | 279 | await page.reload() 280 | const items = page.locator('[data-cy="context-menu-preview-item"]') at /home/runner/work/tables/tables/playwright/e2e/application.spec.ts:277:41

Check failure on line 277 in playwright/e2e/application.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable33)

[chromium] › playwright/e2e/application.spec.ts:255:2 › Applications › A menu entry is reordered with its buttons

1) [chromium] › playwright/e2e/application.spec.ts:255:2 › Applications › A menu entry is reordered with its buttons, sits under another one and keeps its place after a reload Error: expect(received).toBe(expected) // Object.is equality Expected: 200 Received: 500 275 | const saveResponse = page.waitForResponse(response => response.url().includes('/apps/tables/api/2/contexts/') && response.request().method() === 'PUT') 276 | await page.locator('[data-cy="context-menu-save"]').click() > 277 | expect((await saveResponse).status()).toBe(200) | ^ 278 | 279 | await page.reload() 280 | const items = page.locator('[data-cy="context-menu-preview-item"]') at /home/runner/work/tables/tables/playwright/e2e/application.spec.ts:277:41

await page.reload()
const items = page.locator('[data-cy="context-menu-preview-item"]')
Expand All @@ -282,7 +299,7 @@

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)

Check failure on line 302 in playwright/e2e/application.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable35)

[chromium] › playwright/e2e/application.spec.ts:288:2 › Applications › A menu entry can be pointed at a page of the application

2) [chromium] › playwright/e2e/application.spec.ts:288:2 › Applications › A menu entry can be pointed at a page of the application Error: expect(received).toBe(expected) // Object.is equality Expected: 200 Received: 500 300 | const saveResponse = page.waitForResponse(response => response.url().includes('/apps/tables/api/2/contexts/') && response.request().method() === 'PUT') 301 | await page.locator('[data-cy="context-menu-save"]').click() > 302 | expect((await saveResponse).status()).toBe(200) | ^ 303 | await expect(page.locator('[data-cy="context-menu-preview-item"]').filter({ hasText: 'Reports' }).last()).toBeVisible() 304 | 305 | await page.goto(`/index.php/apps/tables/app/${contextId}`) at /home/runner/work/tables/tables/playwright/e2e/application.spec.ts:302:41
await expect(page.locator('[data-cy="context-menu-preview-item"]').filter({ hasText: 'Reports' }).last()).toBeVisible()

await page.goto(`/index.php/apps/tables/app/${contextId}`)
Expand Down Expand Up @@ -445,6 +462,6 @@
const importResponse = page.waitForResponse(r => r.url().includes('/scheme/import') && r.request().method() === 'POST')
await dialog.getByRole('button', { name: 'Import' }).click()
const response = await importResponse
await expect(dialog).toContainText('Import completed successfully.')

Check failure on line 465 in playwright/e2e/application.spec.ts

View workflow job for this annotation

GitHub Actions / Playwright (stable34)

[chromium] › playwright/e2e/application.spec.ts:352:2 › Applications › A scheme imported with its menu and pages switched off keeps the current menu

2) [chromium] › playwright/e2e/application.spec.ts:352:2 › Applications › A scheme imported with its menu and pages switched off keeps the current menu Error: expect(locator).toContainText(expected) failed Locator: getByRole('dialog', { name: /application scheme/ }) Expected substring: "Import completed successfully." Received string: "Import application schemeFailedCould not import scheme due to unknown errors.Done" Timeout: 30000ms Call log: - Expect "toContainText" getByRole('dialog', { name: /application scheme/ }) with timeout 30000ms - waiting for getByRole('dialog', { name: /application scheme/ }) 63 × locator resolved to <div role="dialog" tabindex="-1" aria-modal="true" data-v-c6039372="" data-v-adda232f="" data-v-789e3d11="" data-v-owner="133" aria-labelledby="nc-vue-14" class="dialog__modal modal-mask" aria-describedby="modal-description-nc-vue-15">…</div> - unexpected value "Import application schemeFailedCould not import scheme due to unknown errors.Done" 463 | await dialog.getByRole('button', { name: 'Import' }).click() 464 | const response = await importResponse > 465 | await expect(dialog).toContainText('Import completed successfully.') | ^ 466 | return response 467 | } 468 | at importThroughDialog (/home/runner/work/tables/tables/playwright/e2e/application.spec.ts:465:23) at /home/runner/work/tables/tables/playwright/e2e/application.spec.ts:357:20
return response
}
3 changes: 3 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ export default {
|| document.querySelector(`header .header-left .app-menu a[href="${url}"]`)
this.switchActiveMenuEntry(targetElement)
}
} else if (currentRoute.path === '/applications') {
this.setActiveContextId(null)
this.setPageTitle(t('tables', 'Applications'))
} else if (currentRoute.path.startsWith('/application/')) {
const contextId = parseInt(currentRoute.params.contextId)
this.setActiveContextId(contextId)
Expand Down
8 changes: 8 additions & 0 deletions src/modules/navigation/sections/Navigation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@
</template>
</NcAppNavigationCaption>

<NcAppNavigationItem :name="t('tables', 'All applications')" to="/applications" data-cy="navigationAllApplications">
<template #icon>
<ViewGridOutline :size="20" />
</template>
</NcAppNavigationItem>

<template v-for="node in getAllContexts" :key="node.id">
<NavigationContextItem :context="node" />
</template>
Expand Down Expand Up @@ -108,6 +114,7 @@ import { emit } from '@nextcloud/event-bus'
import { useEventBusSubscriptions } from '../../../shared/composables/useEventBusSubscriptions.js'
import Magnify from 'vue-material-design-icons/Magnify.vue'
import ArchiveOutline from 'vue-material-design-icons/ArchiveOutline.vue'
import ViewGridOutline from 'vue-material-design-icons/ViewGridOutline.vue'
import { getCurrentUser } from '@nextcloud/auth'
import { loadState } from '@nextcloud/initial-state'
import { mapState, mapActions } from 'pinia'
Expand All @@ -125,6 +132,7 @@ export default {
NcTextField,
Magnify,
ArchiveOutline,
ViewGridOutline,
NcButton,
NcCounterBubble,
NcEmptyContent,
Expand Down
147 changes: 147 additions & 0 deletions src/pages/Applications.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="applications-page" data-cy="applications-page">
<div class="applications-page__header">
<h1>{{ t('tables', 'Applications') }}</h1>
<NcButton variant="primary" data-cy="applications-create" @click="createContext">
<template #icon>
<Plus :size="20" />
</template>
{{ t('tables', 'Create application') }}
</NcButton>
</div>

<NcEmptyContent v-if="contexts.length === 0"
:name="t('tables', 'No applications yet')"
:description="t('tables', 'An application bundles pages, tables and views behind one menu.')">
<template #icon>
<ViewGridOutline />
</template>
</NcEmptyContent>

<ul v-else class="applications-page__list">
<li v-for="context in sortedContexts" :key="context.id">
<router-link :to="'/application/' + context.id + '/overview'" class="application-card" data-cy="application-card">
<ApplicationIcon :icon-name="context.iconName" :size="32" />
<div class="application-card__text">
<h2 class="application-card__title">
{{ context.name }}
</h2>
<p v-if="context.description" class="application-card__description">
{{ context.description }}
</p>
<p class="application-card__meta">
{{ n('tables', '%n menu item', '%n menu items', (context.menuItems ?? []).length) }}
<template v-if="context.technicalName">
· /{{ context.technicalName }}
</template>
</p>
</div>
</router-link>
</li>
</ul>

<MainModals />
</div>
</template>

<script>
import { NcButton, NcEmptyContent } from '@nextcloud/vue'
import Plus from 'vue-material-design-icons/Plus.vue'
import ViewGridOutline from 'vue-material-design-icons/ViewGridOutline.vue'
import { emit } from '@nextcloud/event-bus'
import { mapState } from 'pinia'
import MainModals from '../modules/modals/Modals.vue'
import ApplicationIcon from '../shared/components/ApplicationIcon.vue'
import { useTablesStore } from '../store/store.js'

export default {
name: 'Applications',
components: {
NcButton,
NcEmptyContent,
Plus,
ViewGridOutline,
MainModals,
ApplicationIcon,
},
computed: {
...mapState(useTablesStore, ['contexts']),
sortedContexts() {
return [...this.contexts].sort((a, b) => a.name.localeCompare(b.name))
},
},
methods: {
createContext() {
emit('tables:context:create')
},
},
}
</script>

<style lang="scss" scoped>
.applications-page {
width: 100%;
padding: calc(4 * var(--default-grid-baseline));

&__header {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: calc(4 * var(--default-grid-baseline));
margin-bottom: calc(4 * var(--default-grid-baseline));

h1 {
// Leave room for the navigation toggle that floats over the top left corner of the content
margin: 0;
margin-inline-start: var(--default-clickable-area);
}
}

&__list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: calc(4 * var(--default-grid-baseline));
list-style: none;
}
}

.application-card {
display: flex;
gap: calc(3 * var(--default-grid-baseline));
padding: calc(4 * var(--default-grid-baseline));
border: 1px solid var(--color-border);
border-radius: var(--border-radius-large);
background-color: var(--color-main-background);
color: inherit;
text-decoration: none;

&:hover,
&:focus-visible {
background-color: var(--color-background-hover);
}

&__text {
min-width: 0;
}

&__title {
margin: 0;
font-size: var(--default-font-size);
font-weight: bold;
}

&__description {
margin: var(--default-grid-baseline) 0 0;
}

&__meta {
margin: var(--default-grid-baseline) 0 0;
color: var(--color-text-maxcontrast);
}
}
</style>
6 changes: 6 additions & 0 deletions src/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import MainViewWrapper from './pages/View.vue'
import MainDashboardWrapper from './pages/Table.vue'
import Startpage from './pages/Startpage.vue'
import Context from './pages/Context.vue'
import Applications from './pages/Applications.vue'
import PublicTableView from './pages/PublicTableView.vue'

export default createRouter({
Expand All @@ -18,6 +19,11 @@ export default createRouter({
path: '/',
component: Startpage,
},
{
path: '/applications',
component: Applications,
name: 'applications',
},
{
path: '/application/:contextId',
component: Context,
Expand Down
Loading