Skip to content

Commit

Permalink
feat: delete scheduled published events (#10504)
Browse files Browse the repository at this point in the history
### 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.
  • Loading branch information
DanRibbens authored Jan 13, 2025
1 parent 6ada450 commit f95d6ba
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 12 deletions.
2 changes: 1 addition & 1 deletion packages/payload/src/versions/schedule/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export type SchedulePublishTaskInput = {
}
global?: GlobalSlug
locale?: string
type: string
type?: string
user?: number | string
}
Original file line number Diff line number Diff line change
@@ -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']
Expand All @@ -18,6 +21,7 @@ type Args = {

export const buildUpcomingColumns = ({
dateFormat,
deleteHandler,
docs,
i18n,
localization,
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,9 @@
margin-bottom: var(--base);
}
}

&__delete {
margin: 0;
}
}
}
35 changes: 33 additions & 2 deletions packages/ui/src/elements/PublishButton/ScheduleDrawer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
26 changes: 22 additions & 4 deletions packages/ui/src/utilities/schedulePublishHandler.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down
113 changes: 108 additions & 5 deletions test/versions/int.spec.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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))
Expand All @@ -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
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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', () => {
Expand Down

0 comments on commit f95d6ba

Please sign in to comment.