diff --git a/apps/studio/next.config.mjs b/apps/studio/next.config.mjs index 131adb8367..e11fa94fb6 100644 --- a/apps/studio/next.config.mjs +++ b/apps/studio/next.config.mjs @@ -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 @@ -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 @@ -78,19 +81,19 @@ 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 ; @@ -98,7 +101,7 @@ const ContentSecurityPolicy = ` '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 diff --git a/apps/studio/src/components/PageEditor/constants.ts b/apps/studio/src/components/PageEditor/constants.ts index cd0ece3f94..7c27733412 100644 --- a/apps/studio/src/components/PageEditor/constants.ts +++ b/apps/studio/src/components/PageEditor/constants.ts @@ -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 = [ @@ -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 = [ { diff --git a/apps/studio/src/features/editing-experience/components/form-builder/FormBuilder.tsx b/apps/studio/src/features/editing-experience/components/form-builder/FormBuilder.tsx index d5e7a35367..d9f548b553 100644 --- a/apps/studio/src/features/editing-experience/components/form-builder/FormBuilder.tsx +++ b/apps/studio/src/features/editing-experience/components/form-builder/FormBuilder.tsx @@ -25,6 +25,8 @@ import { jsonFormsConstControlTester, JsonFormsDateControl, jsonFormsDateControlTester, + JsonFormsEmbedControl, + jsonFormsEmbedControlTester, jsonFormsGroupLayoutRenderer, jsonFormsGroupLayoutTester, JsonFormsImageControl, @@ -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 }, diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsEmbedControl.tsx b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsEmbedControl.tsx new file mode 100644 index 0000000000..12242c6fa0 --- /dev/null +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsEmbedControl.tsx @@ -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 ( + + + +
+ Insert embed code + + + + + + Embed code + + +