Skip to content

Commit

Permalink
feat: add disclosure for settings (#451)
Browse files Browse the repository at this point in the history
### TL;DR

This PR introduces a folder settings modal in the folder page.

### What changed?

- Imported necessary components like `Modal`, `Input`, `FormControl`, etc.
- Added folder settings modal state management using `useDisclosure` hook.
- Created a new `FolderSettingsModal` component for folder settings.
- Utilized the `useZodForm` for form handling and validation.
- Implemented the mutation to save folder settings changes to the database.
- Updated schema and router to support editing folder's title and permalink.

### How to test?

1. Navigate to the folder page.
2. Click on the `Folder settings` button.
3. Update the title and permalink in the modal.
4. Click `Save changes` and verify if the changes are reflected.

### Why make this change?

This change adds the ability to edit the folder title and permalink directly from the folder page, enhancing the user experience by providing a straightforward way to manage folder settings.

---


https://github.com/user-attachments/assets/206c6663-663b-4c8d-a002-25dfc2c25a72
  • Loading branch information
seaerchin authored Aug 12, 2024
1 parent 534c18d commit 0414c28
Show file tree
Hide file tree
Showing 9 changed files with 300 additions and 35 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { Suspense, useEffect } from "react"
import {
Box,
FormControl,
FormErrorMessage,
FormHelperText,
FormLabel,
Icon,
Input,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Skeleton,
VStack,
} from "@chakra-ui/react"
import { Button, useToast } from "@opengovsg/design-system-react"
import { BiLink } from "react-icons/bi"

import { generateResourceUrl } from "~/features/editing-experience/components/utils"
import { useZodForm } from "~/lib/form"
import {
baseEditFolderSchema,
MAX_FOLDER_PERMALINK_LENGTH,
MAX_FOLDER_TITLE_LENGTH,
} from "~/schemas/folder"
import { trpc } from "~/utils/trpc"

interface FolderSettingsModalProps {
isOpen: boolean
onClose: () => void
siteId: string
resourceId: number
}
export const FolderSettingsModal = ({
isOpen,
onClose,
siteId,
resourceId,
}: FolderSettingsModalProps) => {
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<SuspendableModalContent
isOpen={isOpen}
siteId={siteId}
resourceId={resourceId}
onClose={onClose}
/>
</Modal>
)
}

const SuspendableModalContent = ({
isOpen,
onClose,
siteId,
resourceId,
}: FolderSettingsModalProps) => {
const [{ title: originalTitle, permalink: originalPermalink, parentId }] =
trpc.folder.readFolder.useSuspenseQuery({
siteId: parseInt(siteId),
resourceId,
})
const { setValue, register, handleSubmit, watch, formState, getFieldState } =
useZodForm({
defaultValues: {
title: originalTitle,
permalink: originalPermalink,
},
schema: baseEditFolderSchema.omit({ siteId: true, resourceId: true }),
})
const { errors, isValid } = formState
const utils = trpc.useUtils()
const toast = useToast()
const { mutate, isLoading } = trpc.folder.editFolder.useMutation({
onSettled: onClose,
onSuccess: async () => {
await utils.site.list.invalidate()
await utils.resource.list.invalidate()
await utils.resource.getChildrenOf.invalidate({
resourceId: parentId ? String(parentId) : null,
})
await utils.folder.readFolder.invalidate()
toast({ title: "Folder created!", status: "success" })
},
onError: (err) => {
toast({
title: "Failed to create folder",
status: "error",
// TODO: check if this property is correct
description: err.message,
})
},
})

const onSubmit = handleSubmit((data) => {
mutate({ ...data, resourceId: String(resourceId), siteId })
})

const [title, permalink] = watch(["title", "permalink"])

useEffect(() => {
const permalinkFieldState = getFieldState("permalink")
// This allows the syncing to happen only when the page title is not dirty
// Dirty means user has changed the value AND the value is not the same as the default value of "".
// Once the value has been cleared, dirty state will reset.
if (!permalinkFieldState.isDirty) {
setValue("permalink", generateResourceUrl(title || ""), {
shouldValidate: !!title,
})
}
}, [getFieldState, setValue, title])

return (
<Suspense fallback={<Skeleton />}>
<ModalContent key={String(isOpen)}>
<form onSubmit={onSubmit}>
<ModalHeader>Edit "{originalTitle}"</ModalHeader>
<ModalCloseButton size="sm" />
<ModalBody>
<VStack alignItems="flex-start" spacing="1.5rem">
<FormControl isInvalid={!!errors.title}>
<FormLabel color="base.content.strong">
Folder name
<FormHelperText color="base.content.default">
This will be the title of the index page of your folder.
</FormHelperText>
</FormLabel>

<Input
placeholder="This is a title for your new folder"
{...register("title")}
/>
{errors.title?.message ? (
<FormErrorMessage>{errors.title.message}</FormErrorMessage>
) : (
<FormHelperText mt="0.5rem" color="base.content.medium">
{MAX_FOLDER_TITLE_LENGTH - (title || "").length} characters
left
</FormHelperText>
)}
</FormControl>
<FormControl isInvalid={!!errors.permalink}>
<FormLabel color="base.content.strong">
Folder URL
<FormHelperText color="base.content.default">
This will be applied to every child under this folder.
</FormHelperText>
</FormLabel>
<Input
placeholder="This is a url for your new page"
{...register("permalink")}
/>
{errors.permalink?.message && (
<FormErrorMessage>
{errors.permalink.message}
</FormErrorMessage>
)}

<Box
mt="0.5rem"
py="0.5rem"
px="0.75rem"
bg="interaction.support.disabled"
>
<Icon mr="0.5rem" as={BiLink} />
{permalink}
</Box>

<FormHelperText mt="0.5rem" color="base.content.medium">
{MAX_FOLDER_PERMALINK_LENGTH - (permalink || "").length}{" "}
characters left
</FormHelperText>
</FormControl>
</VStack>
</ModalBody>

<ModalFooter>
<Button mr={3} onClick={onClose} variant="clear">
Close
</Button>
<Button isLoading={isLoading} isDisabled={!isValid} type="submit">
Save changes
</Button>
</ModalFooter>
</form>
</ModalContent>
</Suspense>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./FolderSettingsModal"
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ export const CreateFolderModal = ({
const toast = useToast()
const { mutate, isLoading } = trpc.folder.create.useMutation({
onSettled: onClose,
onSuccess: () => {
void utils.site.list.invalidate()
void utils.resource.list.invalidate()
onSuccess: async () => {
await utils.site.list.invalidate()
await utils.resource.list.invalidate()
toast({ title: "Folder created!", status: "success" })
},
onError: (err) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,17 @@ const MoveResourceContent = withSuspense(
// TODO: actually close the modal
setMovedItem(null)
},
onSuccess: () => {
void utils.page.readPageAndBlob.invalidate()
void utils.resource.list.invalidate({
onSuccess: async () => {
await utils.page.readPageAndBlob.invalidate()
await utils.resource.list.invalidate({
// TODO: Update backend `list` to use the proper schema
resourceId: movedItem?.resourceId
? Number(movedItem.resourceId)
: undefined,
})
// NOTE: We might want to have smarter logic here
// and invalidate the new + old folders
void utils.folder.readFolder.invalidate()
await utils.folder.readFolder.invalidate()
toast({ title: "Resource moved!" })
},
})
Expand Down
40 changes: 28 additions & 12 deletions apps/studio/src/pages/sites/[siteId]/folders/[folderId]/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ import {
VStack,
} from "@chakra-ui/react"
import { Breadcrumb, Button, Menu } from "@opengovsg/design-system-react"
import { uniqueId } from "lodash"
import { BiData, BiFileBlank, BiFolder } from "react-icons/bi"
import { z } from "zod"

import { MenuItem } from "~/components/Menu"
import { FolderSettingsModal } from "~/features/dashboard/components/FolderSettingsModal"
import { ResourceTable } from "~/features/dashboard/components/ResourceTable"
import { CreateFolderModal } from "~/features/editing-experience/components/CreateFolderModal"
import { CreatePageModal } from "~/features/editing-experience/components/CreatePageModal"
Expand All @@ -27,8 +27,8 @@ import { AdminCmsSidebarLayout } from "~/templates/layouts/AdminCmsSidebarLayout
import { trpc } from "~/utils/trpc"

const folderPageSchema = z.object({
siteId: z.coerce.number(),
folderId: z.coerce.number(),
siteId: z.string(),
folderId: z.string(),
})

const FolderPage: NextPageWithLayout = () => {
Expand All @@ -42,14 +42,18 @@ const FolderPage: NextPageWithLayout = () => {
onOpen: onFolderCreateModalOpen,
onClose: onFolderCreateModalClose,
} = useDisclosure()
const {
isOpen: isFolderSettingsModalOpen,
onOpen: onFolderSettingsModalOpen,
onClose: onFolderSettingsModalClose,
} = useDisclosure()

const { folderId, siteId } = useQueryParse(folderPageSchema)

const [{ title, permalink, children, parentId }] =
trpc.folder.readFolder.useSuspenseQuery({
siteId,
resourceId: folderId,
})
const [{ title, permalink }] = trpc.folder.readFolder.useSuspenseQuery({
siteId: parseInt(siteId),
resourceId: parseInt(folderId),
})

return (
<>
Expand Down Expand Up @@ -78,7 +82,11 @@ const FolderPage: NextPageWithLayout = () => {
<Spacer />

<HStack>
<Button variant="outline" size="md">
<Button
variant="outline"
size="md"
onClick={onFolderSettingsModalOpen}
>
Folder settings
</Button>
<Menu isLazy size="sm">
Expand Down Expand Up @@ -109,19 +117,27 @@ const FolderPage: NextPageWithLayout = () => {
</HStack>
</VStack>
<Box width="100%">
<ResourceTable siteId={siteId} resourceId={folderId} />
<ResourceTable
siteId={parseInt(siteId)}
resourceId={parseInt(folderId)}
/>
</Box>
</VStack>
<CreatePageModal
isOpen={isPageCreateModalOpen}
onClose={onPageCreateModalClose}
siteId={siteId}
siteId={parseInt(siteId)}
/>
<CreateFolderModal
isOpen={isFolderCreateModalOpen}
onClose={onFolderCreateModalClose}
siteId={parseInt(siteId)}
/>
<FolderSettingsModal
isOpen={isFolderSettingsModalOpen}
onClose={onFolderSettingsModalClose}
siteId={siteId}
key={uniqueId()}
resourceId={parseInt(folderId)}
/>
</>
)
Expand Down
24 changes: 24 additions & 0 deletions apps/studio/src/schemas/folder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,27 @@ export const readFolderSchema = z.object({
siteId: z.number().min(1),
resourceId: z.number().min(1),
})

export const baseEditFolderSchema = z.object({
resourceId: z.string(),
permalink: z.optional(z.string()),
title: z.optional(z.string()),
siteId: z.string(),
})

export const editFolderSchema = baseEditFolderSchema.superRefine(
({ permalink, title }, ctx) => {
if (!permalink && !title) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["siteId"],
message: "Either permalink or title must be provided.",
})
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["title"],
message: "Either permalink or title must be provided.",
})
}
},
)
Loading

0 comments on commit 0414c28

Please sign in to comment.