From 30589672d8c9f7996c699acaafed3c79705991c3 Mon Sep 17 00:00:00 2001
From: Hsu Zhong Jun <27919917+dcshzj@users.noreply.github.com>
Date: Wed, 31 Jul 2024 18:30:24 +0800
Subject: [PATCH] feat: introduce discard changes modal (#400)
---
.../components/ComplexEditorStateDrawer.tsx | 37 +++++++++++---
.../DiscardChangesModal.tsx | 50 ++++++++++++++++++
.../components/DiscardChangesModal/index.ts | 1 +
.../components/MetadataEditorStateDrawer.tsx | 51 +++++++++++++++----
.../components/RootStateDrawer.tsx | 2 +-
.../components/TipTapComponent.tsx | 38 +++++++++++---
6 files changed, 153 insertions(+), 26 deletions(-)
create mode 100644 apps/studio/src/features/editing-experience/components/DiscardChangesModal/DiscardChangesModal.tsx
create mode 100644 apps/studio/src/features/editing-experience/components/DiscardChangesModal/index.ts
diff --git a/apps/studio/src/features/editing-experience/components/ComplexEditorStateDrawer.tsx b/apps/studio/src/features/editing-experience/components/ComplexEditorStateDrawer.tsx
index b543749ebb..f8495e9282 100644
--- a/apps/studio/src/features/editing-experience/components/ComplexEditorStateDrawer.tsx
+++ b/apps/studio/src/features/editing-experience/components/ComplexEditorStateDrawer.tsx
@@ -10,6 +10,7 @@ import {
import { Button, IconButton } from "@opengovsg/design-system-react"
import { getComponentSchema } from "@opengovsg/isomer-components"
import Ajv from "ajv"
+import _ from "lodash"
import { BiDollar, BiTrash, BiX } from "react-icons/bi"
import { useEditorDrawerContext } from "~/contexts/EditorDrawerContext"
@@ -17,11 +18,22 @@ import { useQueryParse } from "~/hooks/useQueryParse"
import { editPageSchema } from "~/pages/sites/[siteId]/pages/[pageId]"
import { trpc } from "~/utils/trpc"
import { DeleteBlockModal } from "./DeleteBlockModal"
+import { DiscardChangesModal } from "./DiscardChangesModal"
import FormBuilder from "./form-builder/FormBuilder"
const ajv = new Ajv({ strict: false, logger: false })
export default function ComplexEditorStateDrawer(): JSX.Element {
+ const {
+ isOpen: isDeleteBlockModalOpen,
+ onOpen: onDeleteBlockModalOpen,
+ onClose: onDeleteBlockModalClose,
+ } = useDisclosure()
+ const {
+ isOpen: isDiscardChangesModalOpen,
+ onOpen: onDiscardChangesModalOpen,
+ onClose: onDiscardChangesModalClose,
+ } = useDisclosure()
const {
setDrawerState,
currActiveIdx,
@@ -30,6 +42,7 @@ export default function ComplexEditorStateDrawer(): JSX.Element {
previewPageState,
setPreviewPageState,
} = useEditorDrawerContext()
+
const { pageId, siteId } = useQueryParse(editPageSchema)
const [{ content: pageContent }] = trpc.page.readPageAndBlob.useSuspenseQuery(
{ siteId, pageId },
@@ -41,11 +54,6 @@ export default function ComplexEditorStateDrawer(): JSX.Element {
await utils.page.readPageAndBlob.invalidate({ pageId, siteId })
},
})
- const {
- isOpen: isDeleteBlockModalOpen,
- onOpen: onDeleteBlockModalOpen,
- onClose: onDeleteBlockModalClose,
- } = useDisclosure()
if (
currActiveIdx === -1 ||
@@ -80,6 +88,12 @@ export default function ComplexEditorStateDrawer(): JSX.Element {
setDrawerState({ state: "root" })
}
+ const handleDiscardChanges = () => {
+ setPreviewPageState(savedPageState)
+ onDiscardChangesModalClose()
+ setDrawerState({ state: "root" })
+ }
+
const handleChange = (data: IsomerComponent) => {
const updatedBlocks = Array.from(previewPageState.content)
updatedBlocks[currActiveIdx] = data
@@ -99,6 +113,12 @@ export default function ComplexEditorStateDrawer(): JSX.Element {
onDelete={handleDeleteBlock}
/>
+
+
{
- setPreviewPageState(savedPageState)
- setDrawerState({ state: "root" })
+ if (!_.isEqual(previewPageState, savedPageState)) {
+ onDiscardChangesModalOpen()
+ } else {
+ handleDiscardChanges()
+ }
}}
aria-label="Close drawer"
/>
diff --git a/apps/studio/src/features/editing-experience/components/DiscardChangesModal/DiscardChangesModal.tsx b/apps/studio/src/features/editing-experience/components/DiscardChangesModal/DiscardChangesModal.tsx
new file mode 100644
index 0000000000..3491d81654
--- /dev/null
+++ b/apps/studio/src/features/editing-experience/components/DiscardChangesModal/DiscardChangesModal.tsx
@@ -0,0 +1,50 @@
+import {
+ HStack,
+ Modal,
+ ModalBody,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ Text,
+} from "@chakra-ui/react"
+import { Button, ModalCloseButton } from "@opengovsg/design-system-react"
+
+interface DiscardChangesModalProps {
+ isOpen: boolean
+ onClose: () => void
+ onDiscard: () => void
+}
+
+export const DiscardChangesModal = ({
+ isOpen,
+ onClose,
+ onDiscard,
+}: DiscardChangesModalProps): JSX.Element => {
+ return (
+
+
+
+
+ Are you sure you want to discard your changes?
+
+
+
+
+ All edits will be lost.
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/src/features/editing-experience/components/DiscardChangesModal/index.ts b/apps/studio/src/features/editing-experience/components/DiscardChangesModal/index.ts
new file mode 100644
index 0000000000..04702f8143
--- /dev/null
+++ b/apps/studio/src/features/editing-experience/components/DiscardChangesModal/index.ts
@@ -0,0 +1 @@
+export * from "./DiscardChangesModal"
diff --git a/apps/studio/src/features/editing-experience/components/MetadataEditorStateDrawer.tsx b/apps/studio/src/features/editing-experience/components/MetadataEditorStateDrawer.tsx
index 4736c84bda..75cb1b1f52 100644
--- a/apps/studio/src/features/editing-experience/components/MetadataEditorStateDrawer.tsx
+++ b/apps/studio/src/features/editing-experience/components/MetadataEditorStateDrawer.tsx
@@ -1,20 +1,43 @@
import type { IsomerSchema, schema } from "@opengovsg/isomer-components"
import type { Static } from "@sinclair/typebox"
-import { Box, Flex, Heading, HStack, Icon, IconButton } from "@chakra-ui/react"
+import {
+ Box,
+ Flex,
+ Heading,
+ HStack,
+ Icon,
+ IconButton,
+ useDisclosure,
+} from "@chakra-ui/react"
import { Button } from "@opengovsg/design-system-react"
import { getLayoutMetadataSchema } from "@opengovsg/isomer-components"
import Ajv from "ajv"
+import _ from "lodash"
import { BiDollar, BiX } from "react-icons/bi"
import { useEditorDrawerContext } from "~/contexts/EditorDrawerContext"
import { useQueryParse } from "~/hooks/useQueryParse"
import { editPageSchema } from "~/pages/sites/[siteId]/pages/[pageId]"
import { trpc } from "~/utils/trpc"
+import { DiscardChangesModal } from "./DiscardChangesModal"
import FormBuilder from "./form-builder/FormBuilder"
const ajv = new Ajv({ strict: false, logger: false })
export default function MetadataEditorStateDrawer(): JSX.Element {
+ const {
+ isOpen: isDiscardChangesModalOpen,
+ onOpen: onDiscardChangesModalOpen,
+ onClose: onDiscardChangesModalClose,
+ } = useDisclosure()
+ const {
+ setDrawerState,
+ savedPageState,
+ setSavedPageState,
+ previewPageState,
+ setPreviewPageState,
+ } = useEditorDrawerContext()
+
const { pageId, siteId } = useQueryParse(editPageSchema)
const utils = trpc.useUtils()
const [{ content: pageContent }] = trpc.page.readPageAndBlob.useSuspenseQuery(
@@ -25,13 +48,6 @@ export default function MetadataEditorStateDrawer(): JSX.Element {
await utils.page.readPageAndBlob.invalidate({ pageId, siteId })
},
})
- const {
- setDrawerState,
- savedPageState,
- setSavedPageState,
- previewPageState,
- setPreviewPageState,
- } = useEditorDrawerContext()
if (!previewPageState) {
return <>>
@@ -50,8 +66,20 @@ export default function MetadataEditorStateDrawer(): JSX.Element {
setPreviewPageState(newPageState)
}
+ const handleDiscardChanges = () => {
+ setPreviewPageState(savedPageState)
+ onDiscardChangesModalClose()
+ setDrawerState({ state: "root" })
+ }
+
return (
<>
+
+
{
- setPreviewPageState(savedPageState)
- setDrawerState({ state: "root" })
+ if (!_.isEqual(previewPageState, savedPageState)) {
+ onDiscardChangesModalOpen()
+ } else {
+ handleDiscardChanges()
+ }
}}
aria-label="Close drawer"
/>
diff --git a/apps/studio/src/features/editing-experience/components/RootStateDrawer.tsx b/apps/studio/src/features/editing-experience/components/RootStateDrawer.tsx
index f712e33e96..dd3ef938d9 100644
--- a/apps/studio/src/features/editing-experience/components/RootStateDrawer.tsx
+++ b/apps/studio/src/features/editing-experience/components/RootStateDrawer.tsx
@@ -57,7 +57,7 @@ export default function RootStateDrawer() {
const from = result.source.index
const to = result.destination.index
- const contentLength = savedPageState?.content.length ?? 0
+ const contentLength = savedPageState.content.length ?? 0
if (from >= contentLength || to >= contentLength || from < 0 || to < 0)
return
diff --git a/apps/studio/src/features/editing-experience/components/TipTapComponent.tsx b/apps/studio/src/features/editing-experience/components/TipTapComponent.tsx
index da48111241..51f067ed4f 100644
--- a/apps/studio/src/features/editing-experience/components/TipTapComponent.tsx
+++ b/apps/studio/src/features/editing-experience/components/TipTapComponent.tsx
@@ -10,6 +10,7 @@ import {
VStack,
} from "@chakra-ui/react"
import { Button, IconButton } from "@opengovsg/design-system-react"
+import _ from "lodash"
import { BiText, BiTrash, BiX } from "react-icons/bi"
import { PROSE_COMPONENT_NAME } from "~/constants/formBuilder"
@@ -18,6 +19,7 @@ import { useQueryParse } from "~/hooks/useQueryParse"
import { editPageSchema } from "~/pages/sites/[siteId]/pages/[pageId]"
import { trpc } from "~/utils/trpc"
import { DeleteBlockModal } from "./DeleteBlockModal"
+import { DiscardChangesModal } from "./DiscardChangesModal"
import { TiptapEditor } from "./form-builder/renderers/TipTapEditor"
interface TipTapComponentProps {
@@ -25,6 +27,16 @@ interface TipTapComponentProps {
}
function TipTapComponent({ content }: TipTapComponentProps) {
+ const {
+ isOpen: isDeleteBlockModalOpen,
+ onOpen: onDeleteBlockModalOpen,
+ onClose: onDeleteBlockModalClose,
+ } = useDisclosure()
+ const {
+ isOpen: isDiscardChangesModalOpen,
+ onOpen: onDiscardChangesModalOpen,
+ onClose: onDiscardChangesModalClose,
+ } = useDisclosure()
const {
savedPageState,
setDrawerState,
@@ -33,11 +45,6 @@ function TipTapComponent({ content }: TipTapComponentProps) {
setPreviewPageState,
currActiveIdx,
} = useEditorDrawerContext()
- const {
- isOpen: isDeleteBlockModalOpen,
- onOpen: onDeleteBlockModalOpen,
- onClose: onDeleteBlockModalClose,
- } = useDisclosure()
const { pageId, siteId } = useQueryParse(editPageSchema)
@@ -67,6 +74,12 @@ function TipTapComponent({ content }: TipTapComponentProps) {
setDrawerState({ state: "root" })
}
+ const handleDiscardChanges = () => {
+ setPreviewPageState(savedPageState)
+ onDiscardChangesModalClose()
+ setDrawerState({ state: "root" })
+ }
+
const utils = trpc.useUtils()
const { mutate } = trpc.page.updatePageBlob.useMutation({
@@ -88,6 +101,12 @@ function TipTapComponent({ content }: TipTapComponentProps) {
onDelete={handleDeleteBlock}
/>
+
+
}
onClick={() => {
- setDrawerState({ state: "root" })
- setPreviewPageState(savedPageState)
+ if (!_.isEqual(previewPageState, savedPageState)) {
+ onDiscardChangesModalOpen()
+ } else {
+ handleDiscardChanges()
+ }
}}
/>