Skip to content
Merged
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
1 change: 1 addition & 0 deletions packages/payload/src/collections/operations/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export const createOperation = async <
collection,
config,
data,
draft: isSavingDraft,
isDuplicating: Boolean(duplicateFromID),
operation: 'create',
originalDoc: duplicatedFromDoc,
Expand Down
25 changes: 17 additions & 8 deletions packages/payload/src/collections/operations/utilities/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,14 +169,23 @@ export const updateDocument = async <
// Delete any associated files
// /////////////////////////////////////

await deleteAssociatedFiles({
collectionConfig,
config,
doc: docWithLocales,
files: filesToUpload,
overrideDelete: false,
req,
})
// When saving a draft on a document whose latest version is published, the file
// referenced by docWithLocales is still actively used by the published main document.
// Deleting it here would break the published document's file even though no publish
// is happening. Only skip deletion in this case; when the latest version is already a
// draft, it is safe to delete the old draft file as it is being replaced.
const isDraftOverPublished = isSavingDraft && docWithLocales._status === 'published'

if (!isDraftOverPublished) {
await deleteAssociatedFiles({
collectionConfig,
config,
doc: docWithLocales,
files: filesToUpload,
overrideDelete: false,
req,
})
}

// /////////////////////////////////////
// beforeValidate - Fields
Expand Down
3 changes: 3 additions & 0 deletions packages/payload/src/uploads/generateFileData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Args<T> = {
collection: Collection
config: SanitizedConfig
data: T
draft?: boolean
isDuplicating?: boolean
operation: 'create' | 'update'
originalDoc?: T
Expand Down Expand Up @@ -68,6 +69,7 @@ const shouldReupload = (
export const generateFileData = async <T>({
collection: { config: collectionConfig },
data,
draft,
isDuplicating,
operation,
originalDoc,
Expand Down Expand Up @@ -422,6 +424,7 @@ export const generateFileData = async <T>({
newData = {
...newData,
...fileData,
...(draft ? { _status: 'draft' } : {}),
}

return {
Expand Down
1 change: 1 addition & 0 deletions test/versions/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
uploads
uploads2
uploads-draft
25 changes: 25 additions & 0 deletions test/versions/collections/DraftsWithUpload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { CollectionConfig } from 'payload'

import path from 'path'
import { fileURLToPath } from 'url'

import { draftWithUploadCollectionSlug } from '../slugs.js'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

export const DraftsWithUpload: CollectionConfig = {
slug: draftWithUploadCollectionSlug,
upload: {
staticDir: path.resolve(dirname, './uploads-draft'),
},
versions: {
drafts: true,
},
fields: [
{
name: 'alt',
type: 'text',
},
],
}
2 changes: 2 additions & 0 deletions test/versions/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import DraftWithMax from './collections/DraftsWithMax.js'
import DraftsWithValidate from './collections/DraftsWithValidate.js'
import ErrorOnUnpublish from './collections/ErrorOnUnpublish.js'
import LocalizedPosts from './collections/Localized.js'
import { DraftsWithUpload } from './collections/DraftsWithUpload.js'
import { Media } from './collections/Media.js'
import { Media2 } from './collections/Media2.js'
import Posts from './collections/Posts.js'
Expand Down Expand Up @@ -62,6 +63,7 @@ export default buildConfigWithDefaults({
CustomIDs,
Diff,
TextCollection,
DraftsWithUpload,
Media,
Media2,
],
Expand Down
123 changes: 123 additions & 0 deletions test/versions/e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import {
draftWithChangeHookCollectionSlug,
draftWithMaxCollectionSlug,
draftWithMaxGlobalSlug,
draftWithUploadCollectionSlug,
draftWithValidateCollectionSlug,
errorOnUnpublishSlug,
localizedCollectionSlug,
Expand Down Expand Up @@ -1131,6 +1132,128 @@ describe('Versions', () => {
})
})

describe('draft upload collections', () => {
let uploadURL: AdminUrlUtil

beforeAll(() => {
uploadURL = new AdminUrlUtil(serverURL, draftWithUploadCollectionSlug)
})

test('should keep published status after reuploading a file and saving as draft', async () => {
const publishedDoc = await payload.create({
collection: draftWithUploadCollectionSlug,
data: {
_status: 'published',
alt: 'Original image',
},
filePath: path.resolve(dirname, './image.jpg'),
})

await page.goto(uploadURL.edit(publishedDoc.id))
await waitForFormReady(page)

await expect(page.locator('.doc-controls__status .status__value')).toContainText('Published')

// The file input is only rendered once the existing file is removed.
// Click the remove button on the current file to reveal the file input.
await page.locator('.file-details__remove').click()
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png'), {
force: true,
})

await saveDocAndAssert(page, '#action-save-draft')

await expect(page.locator('.doc-controls__status .status__value')).toContainText('Changed')

await expect(async () => {
const { docs } = await payload.find({
collection: draftWithUploadCollectionSlug,
where: { id: { equals: publishedDoc.id } },
})
expect(docs[0]!._status).toStrictEqual('published')
expect(docs[0]!.filename).toStrictEqual(publishedDoc.filename)
}).toPass({ timeout: POLL_TOPASS_TIMEOUT })
})

test('should create a draft version with the new file without altering the published doc', async () => {
const publishedDoc = await payload.create({
collection: draftWithUploadCollectionSlug,
data: {
_status: 'published',
alt: 'Original image',
},
filePath: path.resolve(dirname, './image.jpg'),
})

await page.goto(uploadURL.edit(publishedDoc.id))
await waitForFormReady(page)

// The file input is only rendered once the existing file is removed.
// Click the remove button on the current file to reveal the file input.
await page.locator('.file-details__remove').click()
await page.setInputFiles('input[type="file"]', path.resolve(dirname, './image.png'), {
force: true,
})

await saveDocAndAssert(page, '#action-save-draft')

await expect(async () => {
const { docs: draftDocs } = await payload.find({
collection: draftWithUploadCollectionSlug,
draft: true,
where: { id: { equals: publishedDoc.id } },
})
expect(draftDocs[0]!._status).toStrictEqual('draft')
expect(draftDocs[0]!.filename).not.toStrictEqual(publishedDoc.filename)

const { docs: mainDocs } = await payload.find({
collection: draftWithUploadCollectionSlug,
where: { id: { equals: publishedDoc.id } },
})
expect(mainDocs[0]!.filename).toStrictEqual(publishedDoc.filename)
}).toPass({ timeout: POLL_TOPASS_TIMEOUT })
})

test('should create a draft when duplicating a published upload document', async () => {
const publishedDoc = await payload.create({
collection: draftWithUploadCollectionSlug,
data: {
_status: 'published',
alt: 'Original image',
},
filePath: path.resolve(dirname, './image.jpg'),
})

await page.goto(uploadURL.edit(publishedDoc.id))
await waitForFormReady(page)

await openDocControls(page)
await page.locator('#action-duplicate').click()
await expect(page.locator('.payload-toast-container')).toContainText('successfully')
await expect.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT }).not.toContain(publishedDoc.id)

await expect(page.locator('.doc-controls__status .status__value')).toContainText('Draft')
await waitForFormReady(page)

const duplicatedDocID = new URL(page.url()).pathname.split('/').pop()

await expect(async () => {
const { docs: draftDocs } = await payload.find({
collection: draftWithUploadCollectionSlug,
draft: true,
where: { id: { equals: duplicatedDocID } },
})
expect(draftDocs[0]!._status).toStrictEqual('draft')

const { docs: mainDocs } = await payload.find({
collection: draftWithUploadCollectionSlug,
where: { id: { equals: duplicatedDocID } },
})
expect(mainDocs[0]!._status).toStrictEqual('draft')
}).toPass({ timeout: POLL_TOPASS_TIMEOUT })
})
})

describe('draft globals', () => {
test('should show global versions view level action in globals versions view', async () => {
const global = new AdminUrlUtil(serverURL, draftGlobalSlug)
Expand Down
Loading
Loading