diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 896f8fb47e..a1dba6cd3b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -230,7 +230,7 @@ jobs: # - plugin-seo # - refresh-permissions # - uploads - # - versions + - versions steps: - name: Use Node.js 18 diff --git a/packages/next/src/views/Version/SelectComparison/index.tsx b/packages/next/src/views/Version/SelectComparison/index.tsx index a8566139e0..a19ccced33 100644 --- a/packages/next/src/views/Version/SelectComparison/index.tsx +++ b/packages/next/src/views/Version/SelectComparison/index.tsx @@ -5,6 +5,7 @@ import type { Where } from 'payload/types' import { ReactSelect } from '@payloadcms/ui/elements/ReactSelect' import { fieldBaseClass } from '@payloadcms/ui/fields/shared' import { useConfig } from '@payloadcms/ui/providers/Config' +import { useDocumentInfo } from '@payloadcms/ui/providers/DocumentInfo' import { useTranslation } from '@payloadcms/ui/providers/Translation' import { formatDate } from '@payloadcms/ui/utilities/formatDate' import qs from 'qs' @@ -28,6 +29,7 @@ export const SelectComparison: React.FC = (props) => { admin: { dateFormat }, } = useConfig() + const { docConfig } = useDocumentInfo() const [options, setOptions] = useState(baseOptions) const [lastLoadedPage, setLastLoadedPage] = useState(1) const [errorLoading, setErrorLoading] = useState('') @@ -51,15 +53,18 @@ export const SelectComparison: React.FC = (props) => { not_equals: versionID, }, }, - { - latest: { - not_equals: true, - }, - }, ], }, } + if (docConfig.versions?.drafts) { + query.where.and.push({ + latest: { + not_equals: true, + }, + }) + } + if (parentID) { query.where.and.push({ parent: { @@ -79,7 +84,6 @@ export const SelectComparison: React.FC = (props) => { if (response.ok) { const data: PaginatedDocs = await response.json() - if (data.docs.length > 0) { setOptions((existingOptions) => [ ...existingOptions, @@ -98,7 +102,7 @@ export const SelectComparison: React.FC = (props) => { setErrorLoading(t('error:unspecific')) } }, - [dateFormat, baseURL, parentID, versionID, t, i18n], + [dateFormat, baseURL, parentID, versionID, t, i18n, docConfig.versions?.drafts], ) useEffect(() => { diff --git a/packages/next/src/views/Versions/buildColumns.tsx b/packages/next/src/views/Versions/buildColumns.tsx index 5870562b87..b794a18a36 100644 --- a/packages/next/src/views/Versions/buildColumns.tsx +++ b/packages/next/src/views/Versions/buildColumns.tsx @@ -15,52 +15,62 @@ import { IDCell } from './cells/ID/index.js' export const buildVersionColumns = ({ collectionConfig, - config, docID, globalConfig, i18n: { t }, - i18n, }: { collectionConfig?: SanitizedCollectionConfig config: SanitizedConfig docID?: number | string globalConfig?: SanitizedGlobalConfig i18n: I18n -}): Column[] => [ - { - name: '', - Label: '', - accessor: 'updatedAt', - active: true, - components: { - Cell: ( - - ), - Heading: , +}): Column[] => { + const entityConfig = collectionConfig || globalConfig + + const columns = [ + { + name: '', + Label: '', + accessor: 'updatedAt', + active: true, + components: { + Cell: ( + + ), + Heading: , + }, }, - }, - { - name: '', - Label: '', - accessor: 'id', - active: true, - components: { - Cell: , - Heading: , + { + name: '', + Label: '', + accessor: 'id', + active: true, + components: { + Cell: , + Heading: , + }, }, - }, - { - name: '', - Label: '', - accessor: 'autosave', - active: true, - components: { - Cell: , - Heading: , - }, - }, -] + ] + + if ( + entityConfig?.versions?.drafts || + (entityConfig?.versions?.drafts && entityConfig.versions.drafts?.autosave) + ) { + columns.push({ + name: '', + Label: '', + accessor: '_status', + active: true, + components: { + Cell: , + Heading: , + }, + }) + } + + return columns +} diff --git a/packages/next/src/views/Versions/cells/AutosaveCell/index.tsx b/packages/next/src/views/Versions/cells/AutosaveCell/index.tsx index 9e4a047655..ae46973d4f 100644 --- a/packages/next/src/views/Versions/cells/AutosaveCell/index.tsx +++ b/packages/next/src/views/Versions/cells/AutosaveCell/index.tsx @@ -8,15 +8,11 @@ export const AutosaveCell: React.FC = () => { const { t } = useTranslation() const { rowData } = useTableCell() - return ( {rowData?.autosave && ( - - Autosave - {t('version:autosave')} - + {t('version:autosave')}    )} diff --git a/packages/ui/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx b/packages/ui/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx index ed2536c648..2b5b558252 100644 --- a/packages/ui/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx +++ b/packages/ui/src/elements/DocumentHeader/Tabs/Tab/TabLink.tsx @@ -11,6 +11,7 @@ const Link = (LinkImport.default || LinkImport) as unknown as typeof LinkImport. export const DocumentTabLink: React.FC<{ adminRoute: SanitizedConfig['routes']['admin'] + ariaLabel?: string baseClass: string children?: React.ReactNode href: string @@ -19,6 +20,7 @@ export const DocumentTabLink: React.FC<{ newTab?: boolean }> = ({ adminRoute, + ariaLabel, baseClass, children, href: hrefFromProps, @@ -53,7 +55,10 @@ export const DocumentTabLink: React.FC<{ isActiveFromProps return ( -
  • +
  • = (prop return ( { +export const DefaultPublishButton: React.FC<{ label?: string }> = ({ label: labelProp }) => { const { code } = useLocale() const { id, collectionSlug, globalSlug, publishedDoc, unpublishedVersions } = useDocumentInfo() const [hasPublishPermission, setHasPublishPermission] = React.useState(false) @@ -22,7 +22,7 @@ const DefaultPublishButton: React.FC = () => { serverURL, } = useConfig() const { t } = useTranslation() - const label = t('version:publishChanges') + const label = labelProp || t('version:publishChanges') const hasNewerVersions = unpublishedVersions?.totalDocs > 0 const canPublish = hasPublishPermission && (modified || hasNewerVersions || !publishedDoc) diff --git a/packages/ui/src/elements/Save/index.tsx b/packages/ui/src/elements/Save/index.tsx index 508653aeaf..3cfd5c9a65 100644 --- a/packages/ui/src/elements/Save/index.tsx +++ b/packages/ui/src/elements/Save/index.tsx @@ -8,10 +8,10 @@ import { useHotkey } from '../../hooks/useHotkey.js' import { useEditDepth } from '../../providers/EditDepth/index.js' import { useTranslation } from '../../providers/Translation/index.js' -const DefaultSaveButton: React.FC = () => { +export const DefaultSaveButton: React.FC<{ label?: string }> = ({ label: labelProp }) => { const { t } = useTranslation() const { submit } = useForm() - const label = t('general:save') + const label = labelProp || t('general:save') const ref = useRef(null) const editDepth = useEditDepth() diff --git a/test/versions/e2e.spec.ts b/test/versions/e2e.spec.ts index 22c65222a4..0a4ddbd102 100644 --- a/test/versions/e2e.spec.ts +++ b/test/versions/e2e.spec.ts @@ -28,7 +28,6 @@ import type { Payload } from 'payload/types' import { expect, test } from '@playwright/test' import path from 'path' -import { wait } from 'payload/utilities' import { fileURLToPath } from 'url' import { globalSlug } from '../admin/slugs.js' @@ -42,6 +41,7 @@ import { } from '../helpers.js' import { AdminUrlUtil } from '../helpers/adminUrlUtil.js' import { initPayloadE2E } from '../helpers/initPayloadE2E.js' +import { POLL_TOPASS_TIMEOUT } from '../playwright.config.js' import config from './config.js' import { clearAndSeedEverything } from './seed.js' import { titleToDelete } from './shared.js' @@ -62,6 +62,22 @@ const { beforeAll, beforeEach, describe } = test let payload: Payload +const waitForAutoSaveToComplete = async (page: Page) => { + await expect(async () => { + await expect(page.locator('.autosave:has-text("Saving...")')).toBeVisible() + }).toPass({ + timeout: 45000, + }) + + await expect(async () => { + await expect( + page.locator('.autosave:has-text("Last saved less than a minute ago")'), + ).toBeVisible() + }).toPass({ + timeout: 45000, + }) +} + describe('versions', () => { let page: Page let url: AdminUrlUtil @@ -294,16 +310,11 @@ describe('versions', () => { const url = new AdminUrlUtil(serverURL, autoSaveGlobalSlug) // fill out global title and wait for autosave await page.goto(url.global(autoSaveGlobalSlug)) - await wait(500) + await page.waitForURL(`**/${autoSaveGlobalSlug}`) const titleField = page.locator('#field-title') - await expect(titleField).toBeVisible() await titleField.fill('global title') - await wait(500) - await expect(page.locator('.autosave:has-text("Saving...")')).toBeVisible() - await expect( - page.locator('.autosave:has-text("Last saved less than a minute ago")'), - ).toBeVisible() + await waitForAutoSaveToComplete(page) await expect(titleField).toHaveValue('global title') // refresh the page and ensure value autosaved @@ -314,39 +325,37 @@ describe('versions', () => { test('should retain localized data during autosave', async () => { const en = 'en' const es = 'es' - const title = 'english title' + const englishTitle = 'english title' const spanishTitle = 'spanish title' - const description = 'description' const newDescription = 'new description' await page.goto(autosaveURL.create) - await expect(page.locator('.id-label')).toBeVisible() - await wait(500) + await page.waitForURL(`**/${autosaveURL.create}`) + await page.waitForURL(/\/(?!create$)[\w-]+$/) const titleField = page.locator('#field-title') const descriptionField = page.locator('#field-description') // fill out en doc - await titleField.fill(title) - await descriptionField.fill(description) - await wait(500) + await titleField.fill(englishTitle) + await descriptionField.fill('description') + await waitForAutoSaveToComplete(page) // change locale to spanish await changeLocale(page, es) // set localized title field await titleField.fill(spanishTitle) - await wait(500) + await waitForAutoSaveToComplete(page) // change locale back to en await changeLocale(page, en) // verify en loads its own title - await expect(titleField).toHaveValue(title) + await expect(titleField).toHaveValue(englishTitle) // change non-localized description field await descriptionField.fill(newDescription) - await wait(500) + await waitForAutoSaveToComplete(page) // change locale to spanish await changeLocale(page, es) - await wait(500) // reload page in spanish // title should not be english title @@ -362,7 +371,7 @@ describe('versions', () => { const englishTitle = 'english title' await page.goto(url.create) - await wait(500) + await page.waitForURL(`**/${url.create}`) // fill out doc in english await page.locator('#field-title').fill(englishTitle) @@ -376,6 +385,15 @@ describe('versions', () => { await page.locator('#field-title').fill(spanishTitle) await saveDocAndAssert(page) + // wait for the page to load with the new version + await expect + .poll( + async () => + await page.locator('.doc-tab[aria-label="Versions"] .doc-tab__count').textContent(), + { timeout: POLL_TOPASS_TIMEOUT }, + ) + .toEqual('2') + // fill out draft content in spanish await page.locator('#field-title').fill(`${spanishTitle}--draft`) await saveDocAndAssert(page, '#action-save-draft') @@ -391,33 +409,40 @@ describe('versions', () => { test('collection - autosave should only update the current document', async () => { // create and save first doc await page.goto(autosaveURL.create) - await wait(500) + await page.waitForURL(`**/${autosaveURL.create}`) + await page.waitForURL(/\/(?!create$)[\w-]+$/) + await page.locator('#field-title').fill('first post title') await page.locator('#field-description').fill('first post description') await page.locator('#action-save').click() // create and save second doc await page.goto(autosaveURL.create) - await wait(500) + await page.waitForURL(`**/${autosaveURL.create}`) + await page.waitForURL(/\/(?!create$)[\w-]+$/) await page.locator('#field-title').fill('second post title') await page.locator('#field-description').fill('second post description') - await page.locator('#action-save').click() + // publish changes + await saveDocAndAssert(page) // update second doc and wait for autosave await page.locator('#field-title').fill('updated second post title') await page.locator('#field-description').fill('updated second post description') - await wait(500) + await waitForAutoSaveToComplete(page) // verify that the first doc is unchanged await page.goto(autosaveURL.list) - await page.locator('tbody tr .cell-title a').nth(1).click() + const secondRowLink = page.locator('tbody tr:nth-child(2) .cell-title a') + const docURL = await secondRowLink.getAttribute('href') + await secondRowLink.click() + await page.waitForURL(`**${docURL}`) await expect(page.locator('#field-title')).toHaveValue('first post title') await expect(page.locator('#field-description')).toHaveValue('first post description') }) test('should save versions with custom IDs', async () => { await page.goto(customIDURL.create) - await wait(500) + await page.waitForURL(`**/${customIDURL.create}`) await page.locator('#field-id').fill('custom') await page.locator('#field-title').fill('title') await saveDocAndAssert(page) diff --git a/test/versions/elements/CustomSaveButton/index.tsx b/test/versions/elements/CustomSaveButton/index.tsx index 0f5e3cfe48..d67bd20181 100644 --- a/test/versions/elements/CustomSaveButton/index.tsx +++ b/test/versions/elements/CustomSaveButton/index.tsx @@ -1,16 +1,15 @@ -import * as React from 'react' - -// In your projects, you can import as follows: -// import { CustomPublishButtonProps } from 'payload/types'; - +'use client' import type { CustomPublishButtonProps } from 'payload/types' +import { DefaultPublishButton } from '@payloadcms/ui/elements/Publish' +import * as React from 'react' + import classes from './index.module.scss' -export const CustomPublishButton: CustomPublishButtonProps = ({ DefaultButton, ...rest }) => { +export const CustomPublishButton: CustomPublishButtonProps = () => { return (
    - +
    ) } diff --git a/tsconfig.json b/tsconfig.json index 51ba1562d6..0208151c53 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,7 +37,7 @@ ], "paths": { "@payload-config": [ - "./test/_community/config.ts" + "./test/versions/config.ts" ], "@payloadcms/live-preview": [ "./packages/live-preview/src" @@ -160,4 +160,4 @@ ".next/types/**/*.ts", "scripts/**/*.ts" ] -} +} \ No newline at end of file