Skip to content

Commit f95d6ba

Browse files
authored
feat: delete scheduled published events (#10504)
### What? Allows a user to delete a scheduled publish event after it has been added: ![image](https://github.com/user-attachments/assets/79b1a206-c8a7-4ffa-a9bf-d0f84f86b8f9) ### 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.
1 parent 6ada450 commit f95d6ba

File tree

6 files changed

+195
-12
lines changed

6 files changed

+195
-12
lines changed

packages/payload/src/versions/schedule/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@ export type SchedulePublishTaskInput = {
77
}
88
global?: GlobalSlug
99
locale?: string
10-
type: string
10+
type?: string
1111
user?: number | string
1212
}

packages/ui/src/elements/PublishButton/ScheduleDrawer/buildUpcomingColumns.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
import type { ClientConfig } from 'payload'
22

33
import { getTranslation, type I18nClient, type TFunction } from '@payloadcms/translations'
4+
import React from 'react'
45

56
import type { Column } from '../../Table/index.js'
67
import type { UpcomingEvent } from './types.js'
78

89
import { formatDate } from '../../../utilities/formatDate.js'
10+
import { Button } from '../../Button/index.js'
911
import { Pill } from '../../Pill/index.js'
1012

1113
type Args = {
1214
dateFormat: string
15+
deleteHandler: (id: number | string) => void
1316
docs: UpcomingEvent[]
1417
i18n: I18nClient
1518
localization: ClientConfig['localization']
@@ -18,6 +21,7 @@ type Args = {
1821

1922
export const buildUpcomingColumns = ({
2023
dateFormat,
24+
deleteHandler,
2125
docs,
2226
i18n,
2327
localization,
@@ -81,5 +85,28 @@ export const buildUpcomingColumns = ({
8185
})
8286
}
8387

88+
columns.push({
89+
accessor: 'delete',
90+
active: true,
91+
field: {
92+
name: 'delete',
93+
type: 'text',
94+
},
95+
Heading: <span>{t('general:delete')}</span>,
96+
renderedCells: docs.map((doc) => (
97+
<Button
98+
buttonStyle="icon-label"
99+
className="schedule-publish__delete"
100+
icon="x"
101+
key={doc.id}
102+
onClick={(e) => {
103+
e.preventDefault()
104+
deleteHandler(doc.id)
105+
}}
106+
tooltip={t('general:delete')}
107+
/>
108+
)),
109+
})
110+
84111
return columns
85112
}

packages/ui/src/elements/PublishButton/ScheduleDrawer/index.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,9 @@
4444
margin-bottom: var(--base);
4545
}
4646
}
47+
48+
&__delete {
49+
margin: 0;
50+
}
4751
}
4852
}

packages/ui/src/elements/PublishButton/ScheduleDrawer/index.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export const ScheduleDrawer: React.FC<Props> = ({ slug }) => {
6262
const modalTitle = t('general:schedulePublishFor', { title })
6363
const [upcoming, setUpcoming] = React.useState<UpcomingEvent[]>()
6464
const [upcomingColumns, setUpcomingColumns] = React.useState<Column[]>()
65+
const deleteHandlerRef = React.useRef<((id: number | string) => Promise<void>) | null>(() => null)
6566

6667
const localeOptions = React.useMemo(() => {
6768
if (localization) {
@@ -129,9 +130,39 @@ export const ScheduleDrawer: React.FC<Props> = ({ slug }) => {
129130
})
130131
.then((res) => res.json())
131132

132-
setUpcomingColumns(buildUpcomingColumns({ dateFormat, docs, i18n, localization, t }))
133+
setUpcomingColumns(
134+
buildUpcomingColumns({
135+
dateFormat,
136+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
137+
deleteHandler: deleteHandlerRef.current,
138+
docs,
139+
i18n,
140+
localization,
141+
t,
142+
}),
143+
)
133144
setUpcoming(docs)
134-
}, [api, collectionSlug, dateFormat, globalSlug, i18n, id, serverURL, t, localization])
145+
}, [collectionSlug, globalSlug, serverURL, api, dateFormat, id, t, i18n, localization])
146+
147+
const deleteHandler = React.useCallback(
148+
async (id: number | string) => {
149+
try {
150+
await schedulePublish({
151+
deleteID: id,
152+
})
153+
await fetchUpcoming()
154+
toast.success(t('general:deletedSuccessfully'))
155+
} catch (err) {
156+
console.error(err)
157+
toast.error(err.message)
158+
}
159+
},
160+
[fetchUpcoming, schedulePublish, t],
161+
)
162+
163+
React.useEffect(() => {
164+
deleteHandlerRef.current = deleteHandler
165+
}, [deleteHandler])
135166

136167
const handleSave = React.useCallback(async () => {
137168
if (!date) {

packages/ui/src/utilities/schedulePublishHandler.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import type { PayloadRequest, SchedulePublishTaskInput } from 'payload'
22

33
export type SchedulePublishHandlerArgs = {
4-
date: Date
4+
date?: Date
5+
/**
6+
* The job id to delete to remove a scheduled publish event
7+
*/
8+
deleteID?: number | string
59
req: PayloadRequest
610
} & SchedulePublishTaskInput
711

812
export const schedulePublishHandler = async ({
913
type,
1014
date,
15+
deleteID,
1116
doc,
1217
global,
1318
locale,
@@ -38,6 +43,14 @@ export const schedulePublishHandler = async ({
3843
}
3944

4045
try {
46+
if (deleteID) {
47+
await payload.delete({
48+
collection: 'payload-jobs',
49+
req,
50+
where: { id: { equals: deleteID } },
51+
})
52+
}
53+
4154
await payload.jobs.queue({
4255
input: {
4356
type,
@@ -50,10 +63,15 @@ export const schedulePublishHandler = async ({
5063
waitUntil: date,
5164
})
5265
} catch (err) {
53-
let error = `Error scheduling ${type} for `
66+
let error
5467

55-
if (doc) {
56-
error += `document with ID ${doc.value} in collection ${doc.relationTo}`
68+
if (deleteID) {
69+
error = `Error deleting scheduled publish event with ID ${deleteID}`
70+
} else {
71+
error = `Error scheduling ${type} for `
72+
if (doc) {
73+
error += `document with ID ${doc.value} in collection ${doc.relationTo}`
74+
}
5775
}
5876

5977
payload.logger.error(error)

test/versions/int.spec.ts

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { Payload, PayloadRequest } from 'payload'
1+
import { createLocalReq, Payload } from 'payload'
2+
import { schedulePublishHandler } from '@payloadcms/ui/utilities/schedulePublishHandler'
23

34
import path from 'path'
45
import { ValidationError } from 'payload'
@@ -48,6 +49,7 @@ const formatGraphQLID = (id: number | string) =>
4849
payload.db.defaultIDType === 'number' ? id : `"${id}"`
4950

5051
describe('Versions', () => {
52+
let user
5153
beforeAll(async () => {
5254
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
5355
;({ payload, restClient } = await initPayloadInt(dirname))
@@ -69,12 +71,16 @@ describe('Versions', () => {
6971
password: "${devUser.password}"
7072
) {
7173
token
74+
user {
75+
id
76+
}
7277
}
7378
}`
7479
const { data } = await restClient
7580
.GRAPHQL_POST({ body: JSON.stringify({ query: login }) })
7681
.then((res) => res.json())
7782

83+
user = { ...data.loginUser.user, collection: 'users' }
7884
token = data.loginUser.token
7985

8086
// now: initialize
@@ -1862,10 +1868,6 @@ describe('Versions', () => {
18621868

18631869
const currentDate = new Date()
18641870

1865-
const user = (
1866-
await payload.find({ collection: 'users', where: { email: { equals: devUser.email } } })
1867-
).docs[0]
1868-
18691871
await payload.jobs.queue({
18701872
task: 'schedulePublish',
18711873
waitUntil: new Date(currentDate.getTime() + 3000),
@@ -1998,6 +2000,107 @@ describe('Versions', () => {
19982000
expect(retrieved._status).toStrictEqual('draft')
19992001
expect(retrieved.title).toStrictEqual('i will be a draft')
20002002
})
2003+
2004+
describe('server functions', () => {
2005+
let draftDoc
2006+
let event
2007+
2008+
beforeAll(async () => {
2009+
draftDoc = await payload.create({
2010+
collection: draftCollectionSlug,
2011+
data: {
2012+
title: 'my doc',
2013+
description: 'hello',
2014+
_status: 'draft',
2015+
},
2016+
})
2017+
})
2018+
2019+
it('should create using schedule-publish', async () => {
2020+
const currentDate = new Date()
2021+
2022+
const req = await createLocalReq({ user }, payload)
2023+
2024+
// use server action to create the event
2025+
await schedulePublishHandler({
2026+
req,
2027+
type: 'publish',
2028+
date: new Date(currentDate.getTime() + 3000),
2029+
doc: {
2030+
relationTo: draftCollectionSlug,
2031+
value: draftDoc.id,
2032+
},
2033+
user,
2034+
locale: 'all',
2035+
})
2036+
2037+
// fetch the job
2038+
;[event] = (
2039+
await payload.find({
2040+
collection: 'payload-jobs',
2041+
where: {
2042+
'input.doc.value': {
2043+
equals: draftDoc.id,
2044+
},
2045+
},
2046+
})
2047+
).docs
2048+
2049+
expect(event).toBeDefined()
2050+
})
2051+
2052+
it('should delete using schedule-publish', async () => {
2053+
const currentDate = new Date()
2054+
2055+
const req = await createLocalReq({ user }, payload)
2056+
2057+
// use server action to create the event
2058+
await schedulePublishHandler({
2059+
req,
2060+
type: 'publish',
2061+
date: new Date(currentDate.getTime() + 3000),
2062+
doc: {
2063+
relationTo: draftCollectionSlug,
2064+
value: draftDoc.id,
2065+
},
2066+
user,
2067+
locale: 'all',
2068+
})
2069+
2070+
// fetch the job
2071+
;[event] = (
2072+
await payload.find({
2073+
collection: 'payload-jobs',
2074+
where: {
2075+
'input.doc.value': {
2076+
equals: draftDoc.id,
2077+
},
2078+
},
2079+
})
2080+
).docs
2081+
2082+
// use server action to delete the event
2083+
await schedulePublishHandler({
2084+
req,
2085+
deleteID: event.id,
2086+
user,
2087+
})
2088+
2089+
// fetch the job
2090+
;[event] = (
2091+
await payload.find({
2092+
collection: 'payload-jobs',
2093+
where: {
2094+
'input.doc.value': {
2095+
equals: String(draftDoc.id),
2096+
},
2097+
},
2098+
})
2099+
).docs
2100+
2101+
expect(event).toBeUndefined()
2102+
})
2103+
})
20012104
})
20022105

20032106
describe('Publish Individual Locale', () => {

0 commit comments

Comments
 (0)