Skip to content

Commit 7fdcb70

Browse files
authored
feat(scheduled-publishing): add cancel schedule frontend (#1568)
## Problem Implement cancel schedule flow for an already-scheduled page: 1. Add CancelScheduleModal to confirm cancellation of a scheduled page 2. Add CancelSchedulePublishIndicator, which includes the scheduled date & time and the 'Cancel schedule' button 3. Update disableBlocks to include the case when the page is scheduled, so users won't be able to make any edits to the page until the schedule is cancelled **Breaking Changes** <!-- Does this PR contain any backward incompatible changes? If so, what are they and should there be special considerations for release? --> - [ ] Yes - this PR contains breaking changes - Details ... - [x] No - this PR is backwards compatible Feature flag ENABLE_SCHEDULED_PUBLISHING_FEATURE_KEY enabled for dev, but NOT enabled for other environments. This PR does not add any functionality visible for a non-scheduled page ## Video recordings With feature flag enabled: https://github.com/user-attachments/assets/10959018-41b7-4ad4-aa1e-1509ccb1507d ## Tests Chromatic tests to be added in a follow-up PR
1 parent 5f4d8e2 commit 7fdcb70

File tree

6 files changed

+181
-21
lines changed

6 files changed

+181
-21
lines changed

apps/studio/src/features/editing-experience/components/PublishButton.tsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { withSuspense } from "~/hocs/withSuspense"
1313
import { ENABLE_SCHEDULED_PUBLISHING_FEATURE_KEY } from "~/lib/growthbook"
1414
import { trpc } from "~/utils/trpc"
1515
import { ScheduledPublishingModal } from "./ScheduledPublishingModal"
16+
import { CancelSchedulePublishIndicator } from "./ScheduledPublishingModal/CancelSchedulePublishIndicator"
1617

1718
interface PublishButtonProps extends ButtonProps {
1819
pageId: number
@@ -84,23 +85,31 @@ const SuspendablePublishButton = ({
8485
isPublishingNow={isPending}
8586
/>
8687
)}
87-
<Button
88-
isDisabled={!isChangesPendingPublish || isDisabled}
89-
variant="solid"
90-
size="sm"
91-
onClick={(e) => {
92-
if (enableScheduledPublishing) {
93-
scheduledPublishingDisclosure.onOpen()
94-
} else {
95-
mutate({ pageId, siteId })
96-
onClick?.(e)
97-
}
98-
}}
99-
isLoading={isPending}
100-
{...rest}
101-
>
102-
Publish
103-
</Button>
88+
{currPage.scheduledAt ? (
89+
<CancelSchedulePublishIndicator
90+
siteId={siteId}
91+
pageId={pageId}
92+
scheduledAt={currPage.scheduledAt}
93+
/>
94+
) : (
95+
<Button
96+
isDisabled={!isChangesPendingPublish || isDisabled}
97+
variant="solid"
98+
size="sm"
99+
onClick={(e) => {
100+
if (enableScheduledPublishing) {
101+
scheduledPublishingDisclosure.onOpen()
102+
} else {
103+
mutate({ pageId, siteId })
104+
onClick?.(e)
105+
}
106+
}}
107+
isLoading={isPending}
108+
{...rest}
109+
>
110+
Publish
111+
</Button>
112+
)}
104113
</>
105114
)}
106115
</TouchableTooltip>

apps/studio/src/features/editing-experience/components/RootStateDrawer.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,16 @@ export default function RootStateDrawer() {
6969
onClose: onConfirmConvertIndexPageModalClose,
7070
} = useDisclosure()
7171
const { pageId, siteId } = useQueryParse(pageSchema)
72+
const [{ scheduledAt }] = trpc.page.readPage.useSuspenseQuery({
73+
pageId,
74+
siteId,
75+
})
76+
const disableBlocks = isPreviewingIndexPage || !!scheduledAt
7277
const utils = trpc.useUtils()
7378
const isUserIsomerAdmin = useIsUserIsomerAdmin({
7479
roles: [ADMIN_ROLE.CORE, ADMIN_ROLE.MIGRATORS],
7580
})
7681
const toast = useToast()
77-
7882
const { mutate } = trpc.page.reorderBlock.useMutation({
7983
onSuccess: async () => {
8084
await utils.page.readPage.invalidate({ pageId, siteId })
@@ -269,7 +273,19 @@ export default function RootStateDrawer() {
269273
</VStack>
270274
</Infobox>
271275
)}
272-
276+
{!!scheduledAt && (
277+
<Infobox
278+
size="sm"
279+
border="1px solid"
280+
borderColor="utility.feedback.info"
281+
borderRadius="0.25rem"
282+
>
283+
<Text textStyle="body-2">
284+
This page is scheduled for publishing. To make changes, cancel
285+
the schedule first.
286+
</Text>
287+
</Infobox>
288+
)}
273289
{isPreviewingIndexPage && (
274290
<Infobox
275291
size="sm"
@@ -285,7 +301,7 @@ export default function RootStateDrawer() {
285301
)}
286302

287303
{/* Fixed Blocks Section */}
288-
<Disable when={isPreviewingIndexPage}>
304+
<Disable when={disableBlocks}>
289305
<VStack gap="1.5rem" flex={1} w="full">
290306
<VStack gap="1rem" w="100%" align="start">
291307
<VStack gap="0.25rem" align="start">
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { UseDisclosureReturn } from "@chakra-ui/react"
2+
import {
3+
Button,
4+
Modal,
5+
ModalBody,
6+
ModalCloseButton,
7+
ModalContent,
8+
ModalFooter,
9+
ModalHeader,
10+
ModalOverlay,
11+
Text,
12+
} from "@chakra-ui/react"
13+
import { useToast } from "@opengovsg/design-system-react"
14+
15+
import { BRIEF_TOAST_SETTINGS } from "~/constants/toast"
16+
import { trpc } from "~/utils/trpc"
17+
18+
interface CancelScheduleModalProps extends UseDisclosureReturn {
19+
pageId: number
20+
siteId: number
21+
}
22+
23+
export const CancelScheduleModal = ({
24+
pageId,
25+
siteId,
26+
onClose,
27+
...rest
28+
}: CancelScheduleModalProps): JSX.Element => {
29+
const utils = trpc.useUtils()
30+
const toast = useToast()
31+
const { mutate, isPending } = trpc.page.cancelSchedulePage.useMutation({
32+
onSettled: async () => {
33+
await utils.page.readPage.refetch({ pageId, siteId })
34+
onClose()
35+
},
36+
onSuccess: () => {
37+
toast({
38+
status: "success",
39+
title: "Schedule cancelled successfully",
40+
...BRIEF_TOAST_SETTINGS,
41+
})
42+
},
43+
onError: (error) => {
44+
console.error(`Error occurred when cancelling schedule: ${error.message}`)
45+
toast({
46+
status: "error",
47+
title: "Failed to cancel schedule. Please contact Isomer support.",
48+
...BRIEF_TOAST_SETTINGS,
49+
})
50+
},
51+
})
52+
return (
53+
<Modal onClose={onClose} {...rest}>
54+
<ModalOverlay />
55+
<ModalContent>
56+
<ModalHeader mr="3.5rem">
57+
Are you sure you want to cancel the schedule to publish?
58+
</ModalHeader>
59+
<ModalCloseButton size="lg" />
60+
<ModalBody>
61+
<Text textStyle="body-2">This page will go back to draft mode.</Text>
62+
</ModalBody>
63+
<ModalFooter>
64+
<Button
65+
mr={3}
66+
onClick={onClose}
67+
variant="clear"
68+
color="base.content.strong"
69+
>
70+
No, leave it
71+
</Button>
72+
<Button
73+
onClick={() => mutate({ pageId, siteId })}
74+
isLoading={isPending}
75+
colorScheme="critical"
76+
>
77+
Yes, cancel the schedule
78+
</Button>
79+
</ModalFooter>
80+
</ModalContent>
81+
</Modal>
82+
)
83+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Button, HStack, Icon, Text, useDisclosure } from "@chakra-ui/react"
2+
import { TouchableTooltip } from "@opengovsg/design-system-react"
3+
import { BiTimeFive } from "react-icons/bi"
4+
5+
import { CancelScheduleModal } from "."
6+
import { formatScheduledAtDate } from "./utils"
7+
8+
interface CancelSchedulePublishIndicatorProps {
9+
pageId: number
10+
siteId: number
11+
scheduledAt: Date
12+
}
13+
14+
export const CancelSchedulePublishIndicator = ({
15+
pageId,
16+
siteId,
17+
scheduledAt,
18+
}: CancelSchedulePublishIndicatorProps) => {
19+
const cancelScheduleDisclosure = useDisclosure()
20+
return (
21+
<>
22+
{cancelScheduleDisclosure.isOpen && (
23+
<CancelScheduleModal
24+
{...cancelScheduleDisclosure}
25+
siteId={siteId}
26+
pageId={pageId}
27+
/>
28+
)}
29+
<HStack alignItems="center" spacing="1rem">
30+
<TouchableTooltip label="This page is scheduled to publish. To make changes, cancel the schedule or wait until the page is published.">
31+
<HStack spacing="0.25rem">
32+
<Icon as={BiTimeFive} boxSize="1rem" />
33+
<Text textStyle="caption-1">
34+
{formatScheduledAtDate(scheduledAt)}
35+
</Text>
36+
</HStack>
37+
</TouchableTooltip>
38+
<Button
39+
colorScheme="critical"
40+
onClick={cancelScheduleDisclosure.onOpen}
41+
>
42+
Cancel schedule
43+
</Button>
44+
</HStack>
45+
</>
46+
)
47+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export * from "./ScheduledPublishingModal"
2+
export * from "./CancelScheduleModal"
3+
export * from "./utils"

apps/studio/src/features/editing-experience/components/ScheduledPublishingModal/utils.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isSameDay } from "date-fns"
1+
import { format, isSameDay } from "date-fns"
22

33
/**
44
* if the date provided is equal to the earliestSchedule's date, the earliest allowable time should be set to the FIRST
@@ -19,3 +19,6 @@ export const getEarliestAllowableTime = (
1919
}
2020
return null
2121
}
22+
23+
export const formatScheduledAtDate = (d: Date) =>
24+
format(d, "hh:mma, dd/MM/yyyy")

0 commit comments

Comments
 (0)