Skip to content

Commit

Permalink
feat: support editing map and video embeds (#885)
Browse files Browse the repository at this point in the history
* feat: support editing map and video embeds

* chore: update based on feedback

* chore: add map and video embeds in storybook
  • Loading branch information
dcshzj authored Nov 25, 2024
1 parent 3ae4ee5 commit 4e4f373
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 11 deletions.
25 changes: 14 additions & 11 deletions apps/studio/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,14 @@ const ContentSecurityPolicy = `
frame-src
'self'
https://intercom-sheets.com
https://www.intercom-reporting.com
https://www.youtube.com
https://www.intercom-reporting.com
https://player.vimeo.com
https://fast.wistia.net
https://www.google.com
https://www.youtube.com
https://www.youtube-nocookie.com
https://www.onemap.gov.sg
https://www.facebook.com
;
object-src 'none';
script-src
Expand All @@ -63,7 +67,6 @@ const ContentSecurityPolicy = `
;
connect-src
'self'
https://schema.isomer.gov.sg
https://browser-intake-datadoghq.com
https://*.browser-intake-datadoghq.com
https://vitals.vercel-insights.com
Expand All @@ -78,27 +81,27 @@ const ContentSecurityPolicy = `
https://api.eu.intercom.io
https://api-iam.intercom.io
https://api-iam.eu.intercom.io
https://api-iam.au.intercom.io
https://api-ping.intercom.io
https://api-iam.au.intercom.io
https://api-ping.intercom.io
https://nexus-websocket-a.intercom.io
wss://nexus-websocket-a.intercom.io
https://nexus-websocket-b.intercom.io
wss://nexus-websocket-b.intercom.io
https://nexus-europe-websocket.intercom.io
wss://nexus-europe-websocket.intercom.io
https://nexus-europe-websocket.intercom.io
wss://nexus-europe-websocket.intercom.io
https://nexus-australia-websocket.intercom.io
wss://nexus-australia-websocket.intercom.io
wss://nexus-australia-websocket.intercom.io
https://uploads.intercomcdn.com
https://uploads.intercomcdn.eu
https://uploads.au.intercomcdn.com
https://uploads.intercomcdn.eu
https://uploads.au.intercomcdn.com
https://uploads.eu.intercomcdn.com
https://uploads.intercomusercontent.com
;
worker-src
'self'
blob:
https://intercom-sheets.com
https://www.intercom-reporting.com
https://www.intercom-reporting.com
https://www.youtube.com
https://player.vimeo.com
https://fast.wistia.net
Expand Down
2 changes: 2 additions & 0 deletions apps/studio/src/components/PageEditor/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ type AllowedBlockSections = {

export const ARTICLE_ALLOWED_BLOCKS: AllowedBlockSections = [
{ label: "Basic building blocks", types: ["prose", "image", "callout"] },
{ label: "Embed external content", types: ["map", "video"] },
]

export const CONTENT_ALLOWED_BLOCKS: AllowedBlockSections = [
Expand All @@ -287,6 +288,7 @@ export const CONTENT_ALLOWED_BLOCKS: AllowedBlockSections = [
label: "Organise complex content",
types: ["contentpic", "infocards", "accordion", "infocols"],
},
{ label: "Embed external content", types: ["map", "video"] },
]
export const HOMEPAGE_ALLOWED_BLOCKS: AllowedBlockSections = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
jsonFormsConstControlTester,
JsonFormsDateControl,
jsonFormsDateControlTester,
JsonFormsEmbedControl,
jsonFormsEmbedControlTester,
jsonFormsGroupLayoutRenderer,
jsonFormsGroupLayoutTester,
JsonFormsImageControl,
Expand Down Expand Up @@ -57,6 +59,7 @@ const renderers: JsonFormsRendererRegistryEntry[] = [
{ tester: jsonFormsArrayControlTester, renderer: JsonFormsArrayControl },
{ tester: jsonFormsBooleanControlTester, renderer: JsonFormsBooleanControl },
{ tester: jsonFormsConstControlTester, renderer: JsonFormsConstControl },
{ tester: jsonFormsEmbedControlTester, renderer: JsonFormsEmbedControl },
{ tester: jsonFormsIntegerControlTester, renderer: JsonFormsIntegerControl },
{ tester: jsonFormsImageControlTester, renderer: JsonFormsImageControl },
{ tester: jsonFormsLinkControlTester, renderer: JsonFormsLinkControl },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import type { ControlProps, RankedTester } from "@jsonforms/core"
import {
Box,
FormControl,
HStack,
Icon,
ListItem,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
UnorderedList,
useDisclosure,
VStack,
} from "@chakra-ui/react"
import { and, isStringControl, rankWith, schemaMatches } from "@jsonforms/core"
import { withJsonFormsControlProps } from "@jsonforms/react"
import {
Button,
FormErrorMessage,
FormLabel,
Infobox,
ModalCloseButton,
Textarea,
} from "@opengovsg/design-system-react"
import {
MAPS_EMBED_URL_REGEXES,
VIDEO_EMBED_URL_REGEXES,
} from "@opengovsg/isomer-components"
import { BiLink } from "react-icons/bi"
import { z } from "zod"

import { JSON_FORMS_RANKING } from "~/constants/formBuilder"
import { useZodForm } from "~/lib/form"
import {
EMBED_NAME_MAPPING,
getEmbedNameFromUrl,
getIframeSrc,
} from "../../../utils"

const SUPPORTED_MAPS = Object.keys(MAPS_EMBED_URL_REGEXES).map(
(key) => EMBED_NAME_MAPPING[key as keyof typeof MAPS_EMBED_URL_REGEXES],
)

const SUPPORTED_VIDEOS = Object.keys(VIDEO_EMBED_URL_REGEXES).map(
(key) => EMBED_NAME_MAPPING[key as keyof typeof VIDEO_EMBED_URL_REGEXES],
)

export const jsonFormsEmbedControlTester: RankedTester = rankWith(
JSON_FORMS_RANKING.TextControl,
and(
isStringControl,
schemaMatches((schema) => schema.format === "embed"),
),
)

interface EmbedCodeModalProps {
isOpen: boolean
onClose: () => void
onSave: (embedCode: string) => void
urlPattern?: string
}

function EmbedCodeModal({
isOpen,
onClose,
onSave,
urlPattern,
}: EmbedCodeModalProps) {
const {
register,
handleSubmit,
reset,
formState: { errors, isValid },
} = useZodForm({
mode: "onChange",
schema: z.object({
embedCode: z
.string()
.min(1, "Embed code is required")
.refine(
(value) => {
const iframeSrc = getIframeSrc(value)

if (!urlPattern || !iframeSrc) {
return !!iframeSrc
}

return new RegExp(urlPattern).test(iframeSrc)
},
{
message:
"This code doesn’t look valid. Copy and paste the code as-is without modifications.",
},
),
}),
reValidateMode: "onChange",
})

const onSubmit = handleSubmit(({ embedCode }) => {
onClose()
onSave(embedCode)
reset()
})

return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<form onSubmit={onSubmit}>
<ModalHeader mr="3.5rem">Insert embed code</ModalHeader>
<ModalCloseButton size="lg" />

<ModalBody>
<FormControl mt="1rem" isRequired isInvalid={!!errors.embedCode}>
<FormLabel description="The template may override settings like width and height of the iframe">
Embed code
</FormLabel>

<Textarea
placeholder="Paste embed code here"
fontFamily="monospace"
minAutosizeRows={5}
{...register("embedCode")}
/>

<FormErrorMessage>{errors.embedCode?.message}</FormErrorMessage>
</FormControl>

<Infobox size="sm" variant="info" mt="1.25rem">
<Box>
<Text>You can embed content from:</Text>
<UnorderedList ml="1.5rem" mt="0.25rem">
<ListItem>Video: {SUPPORTED_VIDEOS.join(", ")}</ListItem>
<ListItem>Map: {SUPPORTED_MAPS.join(", ")}</ListItem>
</UnorderedList>
</Box>
</Infobox>
</ModalBody>

<ModalFooter>
<HStack spacing="0.75rem">
<Button
variant="clear"
color="base.content.default"
onClick={onClose}
>
Cancel
</Button>
<Button type="submit" onClick={onSubmit} isDisabled={!isValid}>
Save code
</Button>
</HStack>
</ModalFooter>
</form>
</ModalContent>
</Modal>
)
}

export function JsonFormsEmbedControl({
data,
label,
handleChange,
path,
description,
required,
errors,
schema,
}: ControlProps) {
const {
isOpen: isEmbedModalOpen,
onOpen: onEmbedModalOpen,
onClose: onEmbedModalClose,
} = useDisclosure()

const handleEmbedCodeSave = (embedCode: string) => {
const src = getIframeSrc(embedCode)
handleChange(path, src)
}

return (
<>
<EmbedCodeModal
isOpen={isEmbedModalOpen}
onClose={onEmbedModalClose}
onSave={(embedCode) => handleEmbedCodeSave(embedCode)}
urlPattern={schema.pattern}
/>

<Box>
<FormControl isRequired={required} isInvalid={!!errors}>
<FormLabel description={description}>{label}</FormLabel>

<HStack
justifyContent="space-between"
p="1rem"
bgColor="utility.ui"
borderRadius="0.25rem"
borderWidth="1px"
borderStyle="solid"
borderColor="base.divider.medium"
>
<VStack gap="0.25rem" align="start">
<HStack gap="0.25rem">
<Icon as={BiLink} />
<Text textStyle="caption-1">
{getEmbedNameFromUrl(String(data || "")) ??
"External content"}
</Text>
</HStack>

<Box>
<Text
noOfLines={1}
wordBreak="break-all"
textStyle="caption-2"
color="base.content.medium"
>
{String(data || "")}
</Text>
</Box>
</VStack>

<Button variant="clear" onClick={onEmbedModalOpen}>
Edit
</Button>
</HStack>

<FormErrorMessage>
{label} {errors}
</FormErrorMessage>
</FormControl>
</Box>
</>
)
}

export default withJsonFormsControlProps(JsonFormsEmbedControl)
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,7 @@ export {
default as JsonFormsRefControl,
jsonFormsRefControlTester,
} from "./JsonFormsRefControl"
export {
default as JsonFormsEmbedControl,
jsonFormsEmbedControlTester,
} from "./JsonFormsEmbedControl"
Loading

0 comments on commit 4e4f373

Please sign in to comment.