feat: maintains column state in url (#11387)
Maintains column state in the URL. This makes it possible to share direct links to the list view in a specific column order or active column state, similar to the behavior of filters. This also makes it possible to change both the filters and columns in the same rendering cycle, a requirement of the "list presets" feature being worked on here: #11330. For example: ``` ?columns=%5B"title"%2C"content"%2C"-updatedAt"%2C"createdAt"%2C"id"%5D ``` The `-` prefix denotes that the column is inactive. This strategy performs a single round trip to the server, ultimately simplifying the table columns provider as it no longer needs to request a newly rendered table for itself. Without this change, column state would need to be replaced first, followed by a change to the filters. This would make an unnecessary number of requests to the server and briefly render the UI in a stale state. This all happens behind an optimistic update, where the state of the columns is immediately reflected in the UI while the request takes place in the background. Technically speaking, an additional database query in performed compared to the old strategy, whereas before we'd send the data through the request to avoid this. But this is a necessary tradeoff and doesn't have huge performance implications. One could argue that this is actually a good thing, as the data might have changed in the background which would not have been reflected in the result otherwise.
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
import type { User as PayloadUser } from 'payload'
|
||||
|
||||
import { expect, test } from '@playwright/test'
|
||||
import { mapAsync } from 'payload'
|
||||
import * as qs from 'qs-esm'
|
||||
|
||||
import type { Config, Geo, Post } from '../../payload-types.js'
|
||||
import type { Config, Geo, Post, User } from '../../payload-types.js'
|
||||
|
||||
import {
|
||||
ensureCompilationIsDone,
|
||||
exactText,
|
||||
getRoutes,
|
||||
initPageConsoleErrorCatch,
|
||||
openDocDrawer,
|
||||
} from '../../../helpers.js'
|
||||
import { AdminUrlUtil } from '../../../helpers/adminUrlUtil.js'
|
||||
import { initPayloadE2ENoConfig } from '../../../helpers/initPayloadE2ENoConfig.js'
|
||||
@@ -31,11 +31,15 @@ const description = 'Description'
|
||||
|
||||
let payload: PayloadTestSDK<Config>
|
||||
|
||||
import { devUser } from 'credentials.js'
|
||||
import { addListFilter } from 'helpers/e2e/addListFilter.js'
|
||||
import { goToFirstCell } from 'helpers/e2e/navigateToDoc.js'
|
||||
import { openListColumns } from 'helpers/e2e/openListColumns.js'
|
||||
import { openListFilters } from 'helpers/e2e/openListFilters.js'
|
||||
import { toggleColumn } from 'helpers/e2e/toggleColumn.js'
|
||||
import { deletePreferences } from 'helpers/e2e/preferences.js'
|
||||
import { toggleColumn, waitForColumnInURL } from 'helpers/e2e/toggleColumn.js'
|
||||
import { openDocDrawer } from 'helpers/e2e/toggleDocDrawer.js'
|
||||
import { closeListDrawer } from 'helpers/e2e/toggleListDrawer.js'
|
||||
import path from 'path'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
@@ -58,6 +62,7 @@ describe('List View', () => {
|
||||
let customViewsUrl: AdminUrlUtil
|
||||
let with300DocumentsUrl: AdminUrlUtil
|
||||
let withListViewUrl: AdminUrlUtil
|
||||
let user: any
|
||||
|
||||
let serverURL: string
|
||||
let adminRoutes: ReturnType<typeof getRoutes>
|
||||
@@ -87,6 +92,14 @@ describe('List View', () => {
|
||||
await ensureCompilationIsDone({ customAdminRoutes, page, serverURL })
|
||||
|
||||
adminRoutes = getRoutes({ customAdminRoutes })
|
||||
|
||||
user = await payload.login({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -831,49 +844,91 @@ describe('List View', () => {
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('should toggle columns', async () => {
|
||||
const columnCountLocator = 'table > thead > tr > th'
|
||||
await createPost()
|
||||
test('should toggle columns and effect table', async () => {
|
||||
const tableHeaders = 'table > thead > tr > th'
|
||||
|
||||
await openListColumns(page, {})
|
||||
const numberOfColumns = await page.locator(columnCountLocator).count()
|
||||
const numberOfColumns = await page.locator(tableHeaders).count()
|
||||
await expect(page.locator('.column-selector')).toBeVisible()
|
||||
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID')
|
||||
await toggleColumn(page, { columnLabel: 'ID', targetState: 'off' })
|
||||
|
||||
await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'off' })
|
||||
|
||||
await page.locator('#heading-id').waitFor({ state: 'detached' })
|
||||
await page.locator('.cell-id').first().waitFor({ state: 'detached' })
|
||||
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns - 1)
|
||||
await expect(page.locator(tableHeaders)).toHaveCount(numberOfColumns - 1)
|
||||
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('Number')
|
||||
await toggleColumn(page, { columnLabel: 'ID', targetState: 'on' })
|
||||
|
||||
await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'on' })
|
||||
|
||||
await expect(page.locator('.cell-id').first()).toBeVisible()
|
||||
await expect(page.locator(columnCountLocator)).toHaveCount(numberOfColumns)
|
||||
await expect(page.locator(tableHeaders)).toHaveCount(numberOfColumns)
|
||||
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('ID')
|
||||
|
||||
await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'off' })
|
||||
})
|
||||
|
||||
test('should toggle columns and save to preferences', async () => {
|
||||
const tableHeaders = 'table > thead > tr > th'
|
||||
const numberOfColumns = await page.locator(tableHeaders).count()
|
||||
|
||||
await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'off' })
|
||||
|
||||
await page.reload()
|
||||
|
||||
await expect(page.locator('#heading-id')).toBeHidden()
|
||||
await expect(page.locator('.cell-id').first()).toBeHidden()
|
||||
await expect(page.locator(tableHeaders)).toHaveCount(numberOfColumns - 1)
|
||||
await expect(page.locator('table > thead > tr > th:nth-child(2)')).toHaveText('Number')
|
||||
})
|
||||
|
||||
test('should inject preferred columns into URL search params on load', async () => {
|
||||
await toggleColumn(page, { columnLabel: 'ID', columnName: 'id', targetState: 'off' })
|
||||
|
||||
// reload to ensure the columns were stored and loaded from preferences
|
||||
await page.reload()
|
||||
|
||||
// The `columns` search params _should_ contain "-id"
|
||||
await waitForColumnInURL({ page, columnName: 'id', state: 'off' })
|
||||
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
|
||||
test('should not inject default columns into URL search params on load', async () => {
|
||||
// clear preferences first, ensure that they don't automatically populate in the URL on load
|
||||
await deletePreferences({
|
||||
payload,
|
||||
key: `${postsCollectionSlug}.list`,
|
||||
user,
|
||||
})
|
||||
|
||||
// wait for the URL search params to populate
|
||||
await page.waitForURL(/posts\?/)
|
||||
|
||||
// The `columns` search params should _not_ appear in the URL
|
||||
expect(page.url()).not.toMatch(/columns=/)
|
||||
})
|
||||
|
||||
test('should drag to reorder columns and save to preferences', async () => {
|
||||
await createPost()
|
||||
|
||||
await reorderColumns(page, { fromColumn: 'Number', toColumn: 'ID' })
|
||||
|
||||
// reload to ensure the preferred order was stored in the database
|
||||
// reload to ensure the columns were stored and loaded from preferences
|
||||
await page.reload()
|
||||
|
||||
await expect(
|
||||
page.locator('.list-controls .column-selector .column-selector__column').first(),
|
||||
).toHaveText('Number')
|
||||
|
||||
await expect(page.locator('table thead tr th').nth(1)).toHaveText('Number')
|
||||
})
|
||||
|
||||
test('should render drawer columns in order', async () => {
|
||||
// Re-order columns like done in the previous test
|
||||
await createPost()
|
||||
test('should render list drawer columns in proper order', async () => {
|
||||
await reorderColumns(page, { fromColumn: 'Number', toColumn: 'ID' })
|
||||
|
||||
await page.reload()
|
||||
|
||||
await createPost()
|
||||
await page.goto(postsUrl.create)
|
||||
|
||||
await openDocDrawer(page, '.rich-text .list-drawer__toggler')
|
||||
|
||||
await openDocDrawer({ page, selector: '.rich-text .list-drawer__toggler' })
|
||||
const listDrawer = page.locator('[id^=list-drawer_1_]')
|
||||
await expect(listDrawer).toBeVisible()
|
||||
|
||||
@@ -883,17 +938,17 @@ describe('List View', () => {
|
||||
|
||||
// select the "Post" collection
|
||||
await collectionSelector.click()
|
||||
|
||||
await page
|
||||
.locator('[id^=list-drawer_1_] .list-header__select-collection.react-select .rs__option', {
|
||||
hasText: exactText('Post'),
|
||||
})
|
||||
.click()
|
||||
|
||||
// open the column controls
|
||||
const columnSelector = page.locator('[id^=list-drawer_1_] .list-controls__toggle-columns')
|
||||
await columnSelector.click()
|
||||
// wait until the column toggle UI is visible and fully expanded
|
||||
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
|
||||
await openListColumns(page, {
|
||||
columnContainerSelector: '.list-controls__columns',
|
||||
togglerSelector: '[id^=list-drawer_1_] .list-controls__toggle-columns',
|
||||
})
|
||||
|
||||
// ensure that the columns are in the correct order
|
||||
await expect(
|
||||
@@ -903,48 +958,94 @@ describe('List View', () => {
|
||||
).toHaveText('Number')
|
||||
})
|
||||
|
||||
test('should toggle columns in list drawer', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
|
||||
// Open the drawer
|
||||
await openDocDrawer({ page, selector: '.rich-text .list-drawer__toggler' })
|
||||
const listDrawer = page.locator('[id^=list-drawer_1_]')
|
||||
await expect(listDrawer).toBeVisible()
|
||||
|
||||
await openListColumns(page, {
|
||||
columnContainerSelector: '.list-controls__columns',
|
||||
togglerSelector: '[id^=list-drawer_1_] .list-controls__toggle-columns',
|
||||
})
|
||||
|
||||
await toggleColumn(page, {
|
||||
togglerSelector: '[id^=list-drawer_1_] .list-controls__toggle-columns',
|
||||
columnContainerSelector: '.list-controls__columns',
|
||||
columnLabel: 'ID',
|
||||
targetState: 'off',
|
||||
expectURLChange: false,
|
||||
})
|
||||
|
||||
await closeListDrawer({ page })
|
||||
|
||||
await openDocDrawer({ page, selector: '.rich-text .list-drawer__toggler' })
|
||||
|
||||
await openListColumns(page, {
|
||||
columnContainerSelector: '.list-controls__columns',
|
||||
togglerSelector: '[id^=list-drawer_1_] .list-controls__toggle-columns',
|
||||
})
|
||||
|
||||
const columnContainer = page.locator('.list-controls__columns').first()
|
||||
|
||||
const column = columnContainer.locator(`.column-selector .column-selector__column`, {
|
||||
hasText: exactText('ID'),
|
||||
})
|
||||
|
||||
await expect(column).not.toHaveClass(/column-selector__column--active/)
|
||||
})
|
||||
|
||||
test('should retain preferences when changing drawer collections', async () => {
|
||||
await page.goto(postsUrl.create)
|
||||
|
||||
// Open the drawer
|
||||
await openDocDrawer(page, '.rich-text .list-drawer__toggler')
|
||||
await openDocDrawer({ page, selector: '.rich-text .list-drawer__toggler' })
|
||||
const listDrawer = page.locator('[id^=list-drawer_1_]')
|
||||
await expect(listDrawer).toBeVisible()
|
||||
|
||||
await openListColumns(page, {
|
||||
columnContainerSelector: '.list-controls__columns',
|
||||
togglerSelector: '[id^=list-drawer_1_] .list-controls__toggle-columns',
|
||||
})
|
||||
|
||||
const collectionSelector = page.locator(
|
||||
'[id^=list-drawer_1_] .list-header__select-collection.react-select',
|
||||
)
|
||||
const columnSelector = page.locator('[id^=list-drawer_1_] .list-controls__toggle-columns')
|
||||
|
||||
// open the column controls
|
||||
await columnSelector.click()
|
||||
// wait until the column toggle UI is visible and fully expanded
|
||||
await expect(page.locator('.list-controls__columns.rah-static--height-auto')).toBeVisible()
|
||||
|
||||
// deselect the "id" column
|
||||
await page
|
||||
.locator('[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column', {
|
||||
hasText: exactText('ID'),
|
||||
})
|
||||
.click()
|
||||
await toggleColumn(page, {
|
||||
togglerSelector: '[id^=list-drawer_1_] .list-controls__toggle-columns',
|
||||
columnContainerSelector: '.list-controls__columns',
|
||||
columnLabel: 'ID',
|
||||
targetState: 'off',
|
||||
expectURLChange: false,
|
||||
})
|
||||
|
||||
// select the "Post" collection
|
||||
await collectionSelector.click()
|
||||
|
||||
await page
|
||||
.locator('[id^=list-drawer_1_] .list-header__select-collection.react-select .rs__option', {
|
||||
hasText: exactText('Post'),
|
||||
})
|
||||
.click()
|
||||
|
||||
// deselect the "number" column
|
||||
await page
|
||||
.locator('[id^=list-drawer_1_] .list-controls .column-selector .column-selector__column', {
|
||||
hasText: exactText('Number'),
|
||||
})
|
||||
.click()
|
||||
await toggleColumn(page, {
|
||||
togglerSelector: '[id^=list-drawer_1_] .list-controls__toggle-columns',
|
||||
columnContainerSelector: '.list-controls__columns',
|
||||
columnLabel: 'Number',
|
||||
targetState: 'off',
|
||||
expectURLChange: false,
|
||||
})
|
||||
|
||||
// select the "User" collection again
|
||||
await collectionSelector.click()
|
||||
|
||||
await page
|
||||
.locator('[id^=list-drawer_1_] .list-header__select-collection.react-select .rs__option', {
|
||||
hasText: exactText('User'),
|
||||
@@ -1139,7 +1240,9 @@ describe('List View', () => {
|
||||
|
||||
test('should sort with existing filters', async () => {
|
||||
await page.goto(postsUrl.list)
|
||||
await toggleColumn(page, { columnLabel: 'ID', targetState: 'off' })
|
||||
|
||||
await toggleColumn(page, { columnLabel: 'ID', targetState: 'off', columnName: 'id' })
|
||||
|
||||
await page.locator('#heading-id').waitFor({ state: 'detached' })
|
||||
await page.locator('#heading-title button.sort-column__asc').click()
|
||||
await page.waitForURL(/sort=title/)
|
||||
@@ -1157,13 +1260,10 @@ describe('List View', () => {
|
||||
})
|
||||
|
||||
test('should sort without resetting column preferences', async () => {
|
||||
await payload.delete({
|
||||
collection: 'payload-preferences',
|
||||
where: {
|
||||
key: {
|
||||
equals: `${postsCollectionSlug}.list`,
|
||||
},
|
||||
},
|
||||
await deletePreferences({
|
||||
key: `${postsCollectionSlug}.list`,
|
||||
payload,
|
||||
user,
|
||||
})
|
||||
|
||||
await page.goto(postsUrl.list)
|
||||
@@ -1173,7 +1273,8 @@ describe('List View', () => {
|
||||
await page.waitForURL(/sort=title/)
|
||||
|
||||
// enable a column that is _not_ part of this collection's default columns
|
||||
await toggleColumn(page, { columnLabel: 'Status', targetState: 'on' })
|
||||
await toggleColumn(page, { columnLabel: 'Status', targetState: 'on', columnName: '_status' })
|
||||
|
||||
await page.locator('#heading-_status').waitFor({ state: 'visible' })
|
||||
|
||||
const columnAfterSort = page.locator(
|
||||
|
||||
Reference in New Issue
Block a user