Skip to content

Commit 394c024

Browse files
authored
fix(next): status component incorrectly shows as published status on new documents saved as drafts when readVersions permissions are false (#14950)
### What? Fixes incorrect `status` display when creating and saving new draft documents in collections where users don't have readVersions permission. ### Why? The `getVersions` function was incorrectly assuming any document with an ID is published, causing the Status component to show "Published" for newly created drafts. This happens when `readVersions` permission is disabled because the code takes an early return path that doesn't query the versions collection. ### How? - Changed the early return logic in `getVersions.ts` to check the document's `_status` field directly - Returns `hasPublishedDoc = false` only when `_status === 'draft'`, otherwise `true`
1 parent 98b6791 commit 394c024

6 files changed

Lines changed: 117 additions & 1 deletion

File tree

packages/next/src/views/Document/getVersions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ export const getVersions = async ({
6060
const shouldFetchVersions = Boolean(versionsConfig && docPermissions?.readVersions)
6161

6262
if (!shouldFetchVersions) {
63-
const hasPublishedDoc = Boolean((collectionConfig && id) || globalConfig)
63+
// Without readVersions permission, determine published status from the _status field
64+
const hasPublishedDoc = doc?._status !== 'draft'
6465

6566
return {
6667
hasPublishedDoc,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { CollectionConfig } from 'payload'
2+
3+
import { draftsNoReadVersionsSlug } from '../slugs.js'
4+
5+
const DraftsNoReadVersions: CollectionConfig = {
6+
slug: draftsNoReadVersionsSlug,
7+
access: {
8+
readVersions: () => false,
9+
},
10+
admin: {
11+
defaultColumns: ['title', 'createdAt', '_status'],
12+
useAsTitle: 'title',
13+
},
14+
fields: [
15+
{
16+
name: 'title',
17+
type: 'text',
18+
required: true,
19+
},
20+
{
21+
name: 'description',
22+
type: 'textarea',
23+
},
24+
],
25+
versions: {
26+
drafts: true,
27+
},
28+
}
29+
30+
export default DraftsNoReadVersions

test/versions/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import CustomIDs from './collections/CustomIDs.js'
1111
import { Diff } from './collections/Diff/index.js'
1212
import DisablePublish from './collections/DisablePublish.js'
1313
import DraftPosts from './collections/Drafts.js'
14+
import DraftsNoReadVersions from './collections/DraftsNoReadVersions.js'
1415
import DraftWithChangeHook from './collections/DraftsWithChangeHook.js'
1516
import DraftWithMax from './collections/DraftsWithMax.js'
1617
import DraftsWithValidate from './collections/DraftsWithValidate.js'
@@ -47,6 +48,7 @@ export default buildConfigWithDefaults({
4748
AutosaveWithMultiSelectPosts,
4849
AutosaveWithDraftValidate,
4950
DraftPosts,
51+
DraftsNoReadVersions,
5052
DraftWithMax,
5153
DraftWithChangeHook,
5254
DraftsWithValidate,

test/versions/e2e.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import {
6363
disablePublishSlug,
6464
draftCollectionSlug,
6565
draftGlobalSlug,
66+
draftsNoReadVersionsSlug,
6667
draftWithChangeHookCollectionSlug,
6768
draftWithMaxCollectionSlug,
6869
draftWithMaxGlobalSlug,
@@ -96,6 +97,7 @@ describe('Versions', () => {
9697
let customIDURL: AdminUrlUtil
9798
let postURL: AdminUrlUtil
9899
let errorOnUnpublishURL: AdminUrlUtil
100+
let draftsNoReadVersionsURL: AdminUrlUtil
99101

100102
beforeAll(async ({ browser }, testInfo) => {
101103
testInfo.setTimeout(TEST_TIMEOUT_LONG)
@@ -134,6 +136,7 @@ describe('Versions', () => {
134136
customIDURL = new AdminUrlUtil(serverURL, customIDSlug)
135137
postURL = new AdminUrlUtil(serverURL, postCollectionSlug)
136138
errorOnUnpublishURL = new AdminUrlUtil(serverURL, errorOnUnpublishSlug)
139+
draftsNoReadVersionsURL = new AdminUrlUtil(serverURL, draftsNoReadVersionsSlug)
137140
})
138141

139142
test('collection — should show "has published version" status in list view when draft is saved after publish', async () => {
@@ -937,6 +940,54 @@ describe('Versions', () => {
937940
expect(scanResults.elementsWithoutIndicators).toBe(0)
938941
})
939942
})
943+
944+
describe('without readVersions permission', () => {
945+
test('should show Draft status when creating and saving a new draft document', async () => {
946+
await page.goto(draftsNoReadVersionsURL.create)
947+
await page.locator('#field-title').fill('Test Draft Title')
948+
await page.locator('#field-description').fill('Test Draft Description')
949+
950+
await saveDocAndAssert(page, '#action-save-draft')
951+
952+
await expect(page.locator('.doc-controls__status .status__value')).toContainText('Draft')
953+
954+
await expect(page.locator('#action-unpublish')).toBeHidden()
955+
})
956+
957+
test('should show Published status after publishing a draft document', async () => {
958+
await page.goto(draftsNoReadVersionsURL.create)
959+
await page.locator('#field-title').fill('Test Publish Title')
960+
await page.locator('#field-description').fill('Test Publish Description')
961+
962+
await saveDocAndAssert(page, '#action-save-draft')
963+
964+
await expect(page.locator('.doc-controls__status .status__value')).toContainText('Draft')
965+
966+
await page.locator('#action-save').click()
967+
968+
await expect(page.locator('.doc-controls__status .status__value')).toContainText(
969+
'Published',
970+
)
971+
972+
await expect(page.locator('#action-unpublish')).toBeVisible()
973+
})
974+
975+
test('should maintain Draft status when saving draft multiple times', async () => {
976+
await page.goto(draftsNoReadVersionsURL.create)
977+
await page.locator('#field-title').fill('Test Multiple Saves')
978+
await page.locator('#field-description').fill('Initial Description')
979+
980+
await saveDocAndAssert(page, '#action-save-draft')
981+
982+
await expect(page.locator('.doc-controls__status .status__value')).toContainText('Draft')
983+
984+
await page.locator('#field-description').fill('Updated Description')
985+
await saveDocAndAssert(page, '#action-save-draft')
986+
987+
await expect(page.locator('.doc-controls__status .status__value')).toContainText('Draft')
988+
await expect(page.locator('#action-unpublish')).toBeHidden()
989+
})
990+
})
940991
})
941992

942993
describe('draft globals', () => {

test/versions/payload-types.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export interface Config {
7474
'autosave-multi-select-posts': AutosaveMultiSelectPost;
7575
'autosave-with-validate-posts': AutosaveWithValidatePost;
7676
'draft-posts': DraftPost;
77+
'drafts-no-read-versions': DraftsNoReadVersion;
7778
'draft-with-max-posts': DraftWithMaxPost;
7879
'draft-posts-with-change-hook': DraftPostsWithChangeHook;
7980
'draft-with-validate-posts': DraftWithValidatePost;
@@ -101,6 +102,7 @@ export interface Config {
101102
'autosave-multi-select-posts': AutosaveMultiSelectPostsSelect<false> | AutosaveMultiSelectPostsSelect<true>;
102103
'autosave-with-validate-posts': AutosaveWithValidatePostsSelect<false> | AutosaveWithValidatePostsSelect<true>;
103104
'draft-posts': DraftPostsSelect<false> | DraftPostsSelect<true>;
105+
'drafts-no-read-versions': DraftsNoReadVersionsSelect<false> | DraftsNoReadVersionsSelect<true>;
104106
'draft-with-max-posts': DraftWithMaxPostsSelect<false> | DraftWithMaxPostsSelect<true>;
105107
'draft-posts-with-change-hook': DraftPostsWithChangeHookSelect<false> | DraftPostsWithChangeHookSelect<true>;
106108
'draft-with-validate-posts': DraftWithValidatePostsSelect<false> | DraftWithValidatePostsSelect<true>;
@@ -122,6 +124,7 @@ export interface Config {
122124
db: {
123125
defaultIDType: string;
124126
};
127+
fallbackLocale: ('false' | 'none' | 'null') | false | null | ('en' | 'es' | 'de') | ('en' | 'es' | 'de')[];
125128
globals: {
126129
'autosave-global': AutosaveGlobal;
127130
'autosave-with-draft-button-global': AutosaveWithDraftButtonGlobal;
@@ -312,6 +315,18 @@ export interface AutosaveWithValidatePost {
312315
createdAt: string;
313316
_status?: ('draft' | 'published') | null;
314317
}
318+
/**
319+
* This interface was referenced by `Config`'s JSON-Schema
320+
* via the `definition` "drafts-no-read-versions".
321+
*/
322+
export interface DraftsNoReadVersion {
323+
id: string;
324+
title: string;
325+
description?: string | null;
326+
updatedAt: string;
327+
createdAt: string;
328+
_status?: ('draft' | 'published') | null;
329+
}
315330
/**
316331
* This interface was referenced by `Config`'s JSON-Schema
317332
* via the `definition` "draft-with-max-posts".
@@ -775,6 +790,10 @@ export interface PayloadLockedDocument {
775790
relationTo: 'draft-posts';
776791
value: string | DraftPost;
777792
} | null)
793+
| ({
794+
relationTo: 'drafts-no-read-versions';
795+
value: string | DraftsNoReadVersion;
796+
} | null)
778797
| ({
779798
relationTo: 'draft-with-max-posts';
780799
value: string | DraftWithMaxPost;
@@ -965,6 +984,17 @@ export interface DraftPostsSelect<T extends boolean = true> {
965984
createdAt?: T;
966985
_status?: T;
967986
}
987+
/**
988+
* This interface was referenced by `Config`'s JSON-Schema
989+
* via the `definition` "drafts-no-read-versions_select".
990+
*/
991+
export interface DraftsNoReadVersionsSelect<T extends boolean = true> {
992+
title?: T;
993+
description?: T;
994+
updatedAt?: T;
995+
createdAt?: T;
996+
_status?: T;
997+
}
968998
/**
969999
* This interface was referenced by `Config`'s JSON-Schema
9701000
* via the `definition` "draft-with-max-posts_select".

test/versions/slugs.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export const customIDSlug = 'custom-ids'
99

1010
export const draftCollectionSlug = 'draft-posts'
1111

12+
export const draftsNoReadVersionsSlug = 'drafts-no-read-versions'
13+
1214
export const draftWithValidateCollectionSlug = 'draft-with-validate-posts'
1315
export const draftWithMaxCollectionSlug = 'draft-with-max-posts'
1416

0 commit comments

Comments
 (0)