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
|
||||
locale?: string
|
||||
type: string
|
||||
type?: string
|
||||
user?: number | string
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import type { ClientConfig } from 'payload'
|
||||
|
||||
import { getTranslation, type I18nClient, type TFunction } from '@payloadcms/translations'
|
||||
import React from 'react'
|
||||
|
||||
import type { Column } from '../../Table/index.js'
|
||||
import type { UpcomingEvent } from './types.js'
|
||||
|
||||
import { formatDate } from '../../../utilities/formatDate.js'
|
||||
import { Button } from '../../Button/index.js'
|
||||
import { Pill } from '../../Pill/index.js'
|
||||
|
||||
type Args = {
|
||||
dateFormat: string
|
||||
deleteHandler: (id: number | string) => void
|
||||
docs: UpcomingEvent[]
|
||||
i18n: I18nClient
|
||||
localization: ClientConfig['localization']
|
||||
@@ -18,6 +21,7 @@ type Args = {
|
||||
|
||||
export const buildUpcomingColumns = ({
|
||||
dateFormat,
|
||||
deleteHandler,
|
||||
docs,
|
||||
i18n,
|
||||
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
|
||||
}
|
||||
|
||||
@@ -44,5 +44,9 @@
|
||||
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 [upcoming, setUpcoming] = React.useState<UpcomingEvent[]>()
|
||||
const [upcomingColumns, setUpcomingColumns] = React.useState<Column[]>()
|
||||
const deleteHandlerRef = React.useRef<((id: number | string) => Promise<void>) | null>(() => null)
|
||||
|
||||
const localeOptions = React.useMemo(() => {
|
||||
if (localization) {
|
||||
@@ -129,9 +130,39 @@ export const ScheduleDrawer: React.FC<Props> = ({ slug }) => {
|
||||
})
|
||||
.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)
|
||||
}, [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 () => {
|
||||
if (!date) {
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import type { PayloadRequest, SchedulePublishTaskInput } from 'payload'
|
||||
|
||||
export type SchedulePublishHandlerArgs = {
|
||||
date: Date
|
||||
date?: Date
|
||||
/**
|
||||
* The job id to delete to remove a scheduled publish event
|
||||
*/
|
||||
deleteID?: number | string
|
||||
req: PayloadRequest
|
||||
} & SchedulePublishTaskInput
|
||||
|
||||
export const schedulePublishHandler = async ({
|
||||
type,
|
||||
date,
|
||||
deleteID,
|
||||
doc,
|
||||
global,
|
||||
locale,
|
||||
@@ -38,6 +43,14 @@ export const schedulePublishHandler = async ({
|
||||
}
|
||||
|
||||
try {
|
||||
if (deleteID) {
|
||||
await payload.delete({
|
||||
collection: 'payload-jobs',
|
||||
req,
|
||||
where: { id: { equals: deleteID } },
|
||||
})
|
||||
}
|
||||
|
||||
await payload.jobs.queue({
|
||||
input: {
|
||||
type,
|
||||
@@ -50,10 +63,15 @@ export const schedulePublishHandler = async ({
|
||||
waitUntil: date,
|
||||
})
|
||||
} catch (err) {
|
||||
let error = `Error scheduling ${type} for `
|
||||
let error
|
||||
|
||||
if (doc) {
|
||||
error += `document with ID ${doc.value} in collection ${doc.relationTo}`
|
||||
if (deleteID) {
|
||||
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)
|
||||
|
||||
@@ -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 { ValidationError } from 'payload'
|
||||
@@ -48,6 +49,7 @@ const formatGraphQLID = (id: number | string) =>
|
||||
payload.db.defaultIDType === 'number' ? id : `"${id}"`
|
||||
|
||||
describe('Versions', () => {
|
||||
let user
|
||||
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
|
||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||
@@ -69,12 +71,16 @@ describe('Versions', () => {
|
||||
password: "${devUser.password}"
|
||||
) {
|
||||
token
|
||||
user {
|
||||
id
|
||||
}
|
||||
}
|
||||
}`
|
||||
const { data } = await restClient
|
||||
.GRAPHQL_POST({ body: JSON.stringify({ query: login }) })
|
||||
.then((res) => res.json())
|
||||
|
||||
user = { ...data.loginUser.user, collection: 'users' }
|
||||
token = data.loginUser.token
|
||||
|
||||
// now: initialize
|
||||
@@ -1862,10 +1868,6 @@ describe('Versions', () => {
|
||||
|
||||
const currentDate = new Date()
|
||||
|
||||
const user = (
|
||||
await payload.find({ collection: 'users', where: { email: { equals: devUser.email } } })
|
||||
).docs[0]
|
||||
|
||||
await payload.jobs.queue({
|
||||
task: 'schedulePublish',
|
||||
waitUntil: new Date(currentDate.getTime() + 3000),
|
||||
@@ -1998,6 +2000,107 @@ describe('Versions', () => {
|
||||
expect(retrieved._status).toStrictEqual('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', () => {
|
||||
|
||||
Reference in New Issue
Block a user