feat: delete scheduled published events (#10504)
### What? Allows a user to delete a scheduled publish event after it has been added:  ### Why? Previously a user had no control over making changes once scheduled. ### How? Extends the `scheduledPublishHandler` server action to accept a `deleteID` for the event that should be removed and exposes this to the user via the admin UI in a new column in the Upcoming Events table.
This commit is contained in:
@@ -7,6 +7,6 @@ export type SchedulePublishTaskInput = {
|
|||||||
}
|
}
|
||||||
global?: GlobalSlug
|
global?: GlobalSlug
|
||||||
locale?: string
|
locale?: string
|
||||||
type: string
|
type?: string
|
||||||
user?: number | string
|
user?: number | string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import type { ClientConfig } from 'payload'
|
import type { ClientConfig } from 'payload'
|
||||||
|
|
||||||
import { getTranslation, type I18nClient, type TFunction } from '@payloadcms/translations'
|
import { getTranslation, type I18nClient, type TFunction } from '@payloadcms/translations'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
import type { Column } from '../../Table/index.js'
|
import type { Column } from '../../Table/index.js'
|
||||||
import type { UpcomingEvent } from './types.js'
|
import type { UpcomingEvent } from './types.js'
|
||||||
|
|
||||||
import { formatDate } from '../../../utilities/formatDate.js'
|
import { formatDate } from '../../../utilities/formatDate.js'
|
||||||
|
import { Button } from '../../Button/index.js'
|
||||||
import { Pill } from '../../Pill/index.js'
|
import { Pill } from '../../Pill/index.js'
|
||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
dateFormat: string
|
dateFormat: string
|
||||||
|
deleteHandler: (id: number | string) => void
|
||||||
docs: UpcomingEvent[]
|
docs: UpcomingEvent[]
|
||||||
i18n: I18nClient
|
i18n: I18nClient
|
||||||
localization: ClientConfig['localization']
|
localization: ClientConfig['localization']
|
||||||
@@ -18,6 +21,7 @@ type Args = {
|
|||||||
|
|
||||||
export const buildUpcomingColumns = ({
|
export const buildUpcomingColumns = ({
|
||||||
dateFormat,
|
dateFormat,
|
||||||
|
deleteHandler,
|
||||||
docs,
|
docs,
|
||||||
i18n,
|
i18n,
|
||||||
localization,
|
localization,
|
||||||
@@ -81,5 +85,28 @@ export const buildUpcomingColumns = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
columns.push({
|
||||||
|
accessor: 'delete',
|
||||||
|
active: true,
|
||||||
|
field: {
|
||||||
|
name: 'delete',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
Heading: <span>{t('general:delete')}</span>,
|
||||||
|
renderedCells: docs.map((doc) => (
|
||||||
|
<Button
|
||||||
|
buttonStyle="icon-label"
|
||||||
|
className="schedule-publish__delete"
|
||||||
|
icon="x"
|
||||||
|
key={doc.id}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
deleteHandler(doc.id)
|
||||||
|
}}
|
||||||
|
tooltip={t('general:delete')}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,5 +44,9 @@
|
|||||||
margin-bottom: var(--base);
|
margin-bottom: var(--base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__delete {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const ScheduleDrawer: React.FC<Props> = ({ slug }) => {
|
|||||||
const modalTitle = t('general:schedulePublishFor', { title })
|
const modalTitle = t('general:schedulePublishFor', { title })
|
||||||
const [upcoming, setUpcoming] = React.useState<UpcomingEvent[]>()
|
const [upcoming, setUpcoming] = React.useState<UpcomingEvent[]>()
|
||||||
const [upcomingColumns, setUpcomingColumns] = React.useState<Column[]>()
|
const [upcomingColumns, setUpcomingColumns] = React.useState<Column[]>()
|
||||||
|
const deleteHandlerRef = React.useRef<((id: number | string) => Promise<void>) | null>(() => null)
|
||||||
|
|
||||||
const localeOptions = React.useMemo(() => {
|
const localeOptions = React.useMemo(() => {
|
||||||
if (localization) {
|
if (localization) {
|
||||||
@@ -129,9 +130,39 @@ export const ScheduleDrawer: React.FC<Props> = ({ slug }) => {
|
|||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
|
|
||||||
setUpcomingColumns(buildUpcomingColumns({ dateFormat, docs, i18n, localization, t }))
|
setUpcomingColumns(
|
||||||
|
buildUpcomingColumns({
|
||||||
|
dateFormat,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
|
deleteHandler: deleteHandlerRef.current,
|
||||||
|
docs,
|
||||||
|
i18n,
|
||||||
|
localization,
|
||||||
|
t,
|
||||||
|
}),
|
||||||
|
)
|
||||||
setUpcoming(docs)
|
setUpcoming(docs)
|
||||||
}, [api, collectionSlug, dateFormat, globalSlug, i18n, id, serverURL, t, localization])
|
}, [collectionSlug, globalSlug, serverURL, api, dateFormat, id, t, i18n, localization])
|
||||||
|
|
||||||
|
const deleteHandler = React.useCallback(
|
||||||
|
async (id: number | string) => {
|
||||||
|
try {
|
||||||
|
await schedulePublish({
|
||||||
|
deleteID: id,
|
||||||
|
})
|
||||||
|
await fetchUpcoming()
|
||||||
|
toast.success(t('general:deletedSuccessfully'))
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
toast.error(err.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[fetchUpcoming, schedulePublish, t],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
deleteHandlerRef.current = deleteHandler
|
||||||
|
}, [deleteHandler])
|
||||||
|
|
||||||
const handleSave = React.useCallback(async () => {
|
const handleSave = React.useCallback(async () => {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import type { PayloadRequest, SchedulePublishTaskInput } from 'payload'
|
import type { PayloadRequest, SchedulePublishTaskInput } from 'payload'
|
||||||
|
|
||||||
export type SchedulePublishHandlerArgs = {
|
export type SchedulePublishHandlerArgs = {
|
||||||
date: Date
|
date?: Date
|
||||||
|
/**
|
||||||
|
* The job id to delete to remove a scheduled publish event
|
||||||
|
*/
|
||||||
|
deleteID?: number | string
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
} & SchedulePublishTaskInput
|
} & SchedulePublishTaskInput
|
||||||
|
|
||||||
export const schedulePublishHandler = async ({
|
export const schedulePublishHandler = async ({
|
||||||
type,
|
type,
|
||||||
date,
|
date,
|
||||||
|
deleteID,
|
||||||
doc,
|
doc,
|
||||||
global,
|
global,
|
||||||
locale,
|
locale,
|
||||||
@@ -38,6 +43,14 @@ export const schedulePublishHandler = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (deleteID) {
|
||||||
|
await payload.delete({
|
||||||
|
collection: 'payload-jobs',
|
||||||
|
req,
|
||||||
|
where: { id: { equals: deleteID } },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
await payload.jobs.queue({
|
await payload.jobs.queue({
|
||||||
input: {
|
input: {
|
||||||
type,
|
type,
|
||||||
@@ -50,10 +63,15 @@ export const schedulePublishHandler = async ({
|
|||||||
waitUntil: date,
|
waitUntil: date,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let error = `Error scheduling ${type} for `
|
let error
|
||||||
|
|
||||||
if (doc) {
|
if (deleteID) {
|
||||||
error += `document with ID ${doc.value} in collection ${doc.relationTo}`
|
error = `Error deleting scheduled publish event with ID ${deleteID}`
|
||||||
|
} else {
|
||||||
|
error = `Error scheduling ${type} for `
|
||||||
|
if (doc) {
|
||||||
|
error += `document with ID ${doc.value} in collection ${doc.relationTo}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
payload.logger.error(error)
|
payload.logger.error(error)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Payload, PayloadRequest } from 'payload'
|
import { createLocalReq, Payload } from 'payload'
|
||||||
|
import { schedulePublishHandler } from '@payloadcms/ui/utilities/schedulePublishHandler'
|
||||||
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { ValidationError } from 'payload'
|
import { ValidationError } from 'payload'
|
||||||
@@ -48,6 +49,7 @@ const formatGraphQLID = (id: number | string) =>
|
|||||||
payload.db.defaultIDType === 'number' ? id : `"${id}"`
|
payload.db.defaultIDType === 'number' ? id : `"${id}"`
|
||||||
|
|
||||||
describe('Versions', () => {
|
describe('Versions', () => {
|
||||||
|
let user
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
|
process.env.SEED_IN_CONFIG_ONINIT = 'false' // Makes it so the payload config onInit seed is not run. Otherwise, the seed would be run unnecessarily twice for the initial test run - once for beforeEach and once for onInit
|
||||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||||
@@ -69,12 +71,16 @@ describe('Versions', () => {
|
|||||||
password: "${devUser.password}"
|
password: "${devUser.password}"
|
||||||
) {
|
) {
|
||||||
token
|
token
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
const { data } = await restClient
|
const { data } = await restClient
|
||||||
.GRAPHQL_POST({ body: JSON.stringify({ query: login }) })
|
.GRAPHQL_POST({ body: JSON.stringify({ query: login }) })
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
|
|
||||||
|
user = { ...data.loginUser.user, collection: 'users' }
|
||||||
token = data.loginUser.token
|
token = data.loginUser.token
|
||||||
|
|
||||||
// now: initialize
|
// now: initialize
|
||||||
@@ -1862,10 +1868,6 @@ describe('Versions', () => {
|
|||||||
|
|
||||||
const currentDate = new Date()
|
const currentDate = new Date()
|
||||||
|
|
||||||
const user = (
|
|
||||||
await payload.find({ collection: 'users', where: { email: { equals: devUser.email } } })
|
|
||||||
).docs[0]
|
|
||||||
|
|
||||||
await payload.jobs.queue({
|
await payload.jobs.queue({
|
||||||
task: 'schedulePublish',
|
task: 'schedulePublish',
|
||||||
waitUntil: new Date(currentDate.getTime() + 3000),
|
waitUntil: new Date(currentDate.getTime() + 3000),
|
||||||
@@ -1998,6 +2000,107 @@ describe('Versions', () => {
|
|||||||
expect(retrieved._status).toStrictEqual('draft')
|
expect(retrieved._status).toStrictEqual('draft')
|
||||||
expect(retrieved.title).toStrictEqual('i will be a draft')
|
expect(retrieved.title).toStrictEqual('i will be a draft')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('server functions', () => {
|
||||||
|
let draftDoc
|
||||||
|
let event
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
draftDoc = await payload.create({
|
||||||
|
collection: draftCollectionSlug,
|
||||||
|
data: {
|
||||||
|
title: 'my doc',
|
||||||
|
description: 'hello',
|
||||||
|
_status: 'draft',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create using schedule-publish', async () => {
|
||||||
|
const currentDate = new Date()
|
||||||
|
|
||||||
|
const req = await createLocalReq({ user }, payload)
|
||||||
|
|
||||||
|
// use server action to create the event
|
||||||
|
await schedulePublishHandler({
|
||||||
|
req,
|
||||||
|
type: 'publish',
|
||||||
|
date: new Date(currentDate.getTime() + 3000),
|
||||||
|
doc: {
|
||||||
|
relationTo: draftCollectionSlug,
|
||||||
|
value: draftDoc.id,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
locale: 'all',
|
||||||
|
})
|
||||||
|
|
||||||
|
// fetch the job
|
||||||
|
;[event] = (
|
||||||
|
await payload.find({
|
||||||
|
collection: 'payload-jobs',
|
||||||
|
where: {
|
||||||
|
'input.doc.value': {
|
||||||
|
equals: draftDoc.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).docs
|
||||||
|
|
||||||
|
expect(event).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete using schedule-publish', async () => {
|
||||||
|
const currentDate = new Date()
|
||||||
|
|
||||||
|
const req = await createLocalReq({ user }, payload)
|
||||||
|
|
||||||
|
// use server action to create the event
|
||||||
|
await schedulePublishHandler({
|
||||||
|
req,
|
||||||
|
type: 'publish',
|
||||||
|
date: new Date(currentDate.getTime() + 3000),
|
||||||
|
doc: {
|
||||||
|
relationTo: draftCollectionSlug,
|
||||||
|
value: draftDoc.id,
|
||||||
|
},
|
||||||
|
user,
|
||||||
|
locale: 'all',
|
||||||
|
})
|
||||||
|
|
||||||
|
// fetch the job
|
||||||
|
;[event] = (
|
||||||
|
await payload.find({
|
||||||
|
collection: 'payload-jobs',
|
||||||
|
where: {
|
||||||
|
'input.doc.value': {
|
||||||
|
equals: draftDoc.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).docs
|
||||||
|
|
||||||
|
// use server action to delete the event
|
||||||
|
await schedulePublishHandler({
|
||||||
|
req,
|
||||||
|
deleteID: event.id,
|
||||||
|
user,
|
||||||
|
})
|
||||||
|
|
||||||
|
// fetch the job
|
||||||
|
;[event] = (
|
||||||
|
await payload.find({
|
||||||
|
collection: 'payload-jobs',
|
||||||
|
where: {
|
||||||
|
'input.doc.value': {
|
||||||
|
equals: String(draftDoc.id),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).docs
|
||||||
|
|
||||||
|
expect(event).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Publish Individual Locale', () => {
|
describe('Publish Individual Locale', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user