diff --git a/cypress/component/contextLayout.cy.js b/cypress/component/contextLayout.cy.js new file mode 100644 index 0000000000..931046c5ad --- /dev/null +++ b/cypress/component/contextLayout.cy.js @@ -0,0 +1,48 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +import { applyPersonalWidths, loadContextLayout, saveContextLayout } from '../../src/shared/utils/contextLayout.js' + +describe('application layout', () => { + beforeEach(() => { + window.localStorage.clear() + }) + + it('keeps a saved layout per application and page', () => { + saveContextLayout(7, 3, { 'content-4': 6, 'content-9': 4 }) + + expect(loadContextLayout(7, 3)).to.deep.equal({ 'content-4': 6, 'content-9': 4 }) + expect(loadContextLayout(7, 5)).to.deep.equal({}) + expect(loadContextLayout(8, 3)).to.deep.equal({}) + }) + + it('resets to the page as arranged when an empty layout is saved', () => { + saveContextLayout(7, 3, { 'content-4': 6 }) + saveContextLayout(7, 3, {}) + + expect(loadContextLayout(7, 3)).to.deep.equal({}) + }) + + it('drops widths it does not know and survives a broken entry', () => { + saveContextLayout(7, 3, { a: 6, b: 5, c: '12' }) + expect(loadContextLayout(7, 3)).to.deep.equal({ a: 6 }) + + const key = Object.keys(window.localStorage).find(name => name.endsWith(':7:3')) + window.localStorage.setItem(key, '{not json') + expect(loadContextLayout(7, 3)).to.deep.equal({}) + }) + + it('applies the widths to the layout and keeps a widened widget on the grid', () => { + const layout = [ + { widgetId: 'a', gridX: 0, gridY: 0, gridWidth: 12, gridHeight: 5 }, + { widgetId: 'b', gridX: 8, gridY: 5, gridWidth: 4, gridHeight: 5 }, + ] + + const shown = applyPersonalWidths(layout, { a: 6, b: 8 }) + + expect(shown[0]).to.deep.include({ gridX: 0, gridWidth: 6 }) + expect(shown[1]).to.deep.include({ gridX: 4, gridWidth: 8 }) + expect(applyPersonalWidths(layout, {})).to.deep.equal(layout) + }) +}) diff --git a/playwright/e2e/application.spec.ts b/playwright/e2e/application.spec.ts index 6c1ec0b047..f62875149f 100644 --- a/playwright/e2e/application.spec.ts +++ b/playwright/e2e/application.spec.ts @@ -197,6 +197,35 @@ test.describe('Applications', () => { await expect(page.locator('[data-cy="context-title"]')).toContainText('listed application') }) + test('A width chosen for a table in an application opened on its own is kept after a reload', async ({ userPage: { page, user } }) => { + const tableResponse = await ocsRequest(page.request, user, { method: 'POST', url: '/index.php/apps/tables/api/1/tables', data: { title: 'Layout table' } }) + const table = await tableResponse.json() + const contextResponse = await ocsRequest(page.request, user, { + method: 'POST', + url: `${API}/contexts?format=json`, + data: { name: 'layout application', iconName: 'star', nodes: [{ id: table.id, type: 0, permissions: 1 }] }, + }) + const context = (await contextResponse.json()).ocs.data + + await page.goto(`/index.php/apps/tables/app/${context.id}`) + const item = page.locator('[data-cy="gridItem"]') + await expect(item).toHaveCount(1, { timeout: 30000 }) + await expect(item).toHaveAttribute('gs-w', '12') + + await page.locator('[data-cy="context-layout-edit"]').click() + await item.locator('[data-cy="gridWidgetWidth"]').getByRole('button').first().click() + await page.getByRole('menuitemradio', { name: 'Half' }).or(page.getByRole('radio', { name: 'Half' })).first().click() + await page.locator('[data-cy="context-layout-save"]').click() + await expect(item).toHaveAttribute('gs-w', '6') + + await page.reload() + await expect(page.locator('[data-cy="gridItem"]')).toHaveAttribute('gs-w', '6', { timeout: 30000 }) + // The page as arranged for everyone keeps its width + const stored = await getApplication(page.request, user, context.id) + const startPage = Object.values(stored.pages as Record }>).find(entry => entry.page_type === 'startpage') + expect(Object.values(startPage!.content).map(entry => entry.grid_width)).toEqual([12]) + }) + test('A page with widgets added on the overview gets a menu entry and opens in edit mode', async ({ userPage: { page } }) => { await page.goto('/index.php/apps/tables') await createContext(page, 'overview application') diff --git a/src/modules/grid/GridPage.vue b/src/modules/grid/GridPage.vue index 8f92cd3a20..13ec8e8ed2 100644 --- a/src/modules/grid/GridPage.vue +++ b/src/modules/grid/GridPage.vue @@ -4,9 +4,26 @@ -->