diff --git a/apps/studio/next.config.mjs b/apps/studio/next.config.mjs index 389fefca0..dc6bc85c4 100644 --- a/apps/studio/next.config.mjs +++ b/apps/studio/next.config.mjs @@ -20,7 +20,7 @@ const ContentSecurityPolicy = ` font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; - img-src * data:; + img-src * data: blob:; frame-src 'self'; object-src 'none'; script-src 'self' 'unsafe-eval'; diff --git a/apps/studio/src/constants/formBuilder.ts b/apps/studio/src/constants/formBuilder.ts index 98409e334..eb24e475c 100644 --- a/apps/studio/src/constants/formBuilder.ts +++ b/apps/studio/src/constants/formBuilder.ts @@ -3,6 +3,7 @@ export const JSON_FORMS_RANKING = { ArrayControl: 4, BooleanControl: 2, DropdownControl: 2, + ImageControl: 2, IntegerControl: 4, TextControl: 1, ObjectControl: 2, 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 01a40d8c5..c9b1f09e0 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 @@ -18,6 +18,8 @@ import { jsonFormsDropdownControlTester, jsonFormsGroupLayoutRenderer, jsonFormsGroupLayoutTester, + JsonFormsImageControl, + jsonFormsImageControlTester, JsonFormsIntegerControl, jsonFormsIntegerControlTester, JsonFormsLinkControl, @@ -43,6 +45,7 @@ const renderers: JsonFormsRendererRegistryEntry[] = [ renderer: JsonFormsDropdownControl, }, { tester: jsonFormsIntegerControlTester, renderer: JsonFormsIntegerControl }, + { tester: jsonFormsImageControlTester, renderer: JsonFormsImageControl }, { tester: jsonFormsLinkControlTester, renderer: JsonFormsLinkControl }, { tester: jsonFormsTextControlTester, renderer: JsonFormsTextControl }, { tester: jsonFormsAllOfControlTester, renderer: JsonFormsAllOfControl }, diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsImageControl.tsx b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsImageControl.tsx new file mode 100644 index 000000000..250af378d --- /dev/null +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsImageControl.tsx @@ -0,0 +1,79 @@ +import type { ControlProps, RankedTester } from "@jsonforms/core" +import { useState } from "react" +import { Box, FormControl, Text } from "@chakra-ui/react" +import { and, isStringControl, rankWith, schemaMatches } from "@jsonforms/core" +import { withJsonFormsControlProps } from "@jsonforms/react" +import { Attachment, FormLabel, useToast } from "@opengovsg/design-system-react" + +import { JSON_FORMS_RANKING } from "~/constants/formBuilder" +import { + IMAGE_UPLOAD_ACCEPTED_MIME_TYPES, + MAX_IMG_FILE_SIZE_BYTES, +} from "./constants" + +export const jsonFormsImageControlTester: RankedTester = rankWith( + JSON_FORMS_RANKING.ImageControl, + and( + isStringControl, + schemaMatches((schema) => schema.format === "image"), + ), +) +export function JsonFormsImageControl({ + label, + handleChange, + path, + description, + required, +}: ControlProps) { + const toast = useToast() + + const [pendingFile, setPendingFile] = useState() + + return ( + + + {label} + { + if (file) { + setPendingFile(file) + // TODO: Upload file logic? + handleChange(path, "https://picsum.photos/id/237/200/300") + } else { + // NOTE: Do we need to update backend on removal of file? + handleChange(path, "") + setPendingFile(undefined) + } + }} + onError={(error) => { + toast({ + title: "Image error", + description: error, + status: "error", + }) + }} + onRejection={(rejections) => { + if (rejections[0]?.errors[0]) { + toast({ + title: "Image rejected", + description: rejections[0].errors[0].message, + status: "error", + }) + } + }} + maxSize={MAX_IMG_FILE_SIZE_BYTES} + accept={IMAGE_UPLOAD_ACCEPTED_MIME_TYPES} + /> + + {`Maximum file size: ${MAX_IMG_FILE_SIZE_BYTES / 1000000} MB`} + + + + ) +} + +export default withJsonFormsControlProps(JsonFormsImageControl) diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/constants.ts b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/constants.ts new file mode 100644 index 000000000..a2e717b06 --- /dev/null +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/constants.ts @@ -0,0 +1,10 @@ +export const MAX_IMG_FILE_SIZE_BYTES = 5000000 +export const IMAGE_UPLOAD_ACCEPTED_MIME_TYPES = [ + "image/jpeg", + "image/png", + "image/gif", + "image/svg+xml", + "image/tiff", + "image/bmp", + "image/webp", +] diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/index.ts b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/index.ts index 491d18181..f5c860856 100644 --- a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/index.ts +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/index.ts @@ -42,3 +42,7 @@ export { default as JsonFormsTextControl, jsonFormsTextControlTester, } from "./JsonFormsTextControl" +export { + default as JsonFormsImageControl, + jsonFormsImageControlTester, +} from "./JsonFormsImageControl" diff --git a/packages/components/src/interfaces/complex/Image.ts b/packages/components/src/interfaces/complex/Image.ts index fc177a309..0cf0512fa 100644 --- a/packages/components/src/interfaces/complex/Image.ts +++ b/packages/components/src/interfaces/complex/Image.ts @@ -6,6 +6,7 @@ export const ImageSchema = Type.Object( type: Type.Literal("image", { default: "image" }), src: Type.String({ title: "Upload image", + format: "image", }), alt: Type.String({ title: "Alternate text", diff --git a/packages/components/src/interfaces/complex/InfoCards.ts b/packages/components/src/interfaces/complex/InfoCards.ts index 0bbb7ac95..8ba965763 100644 --- a/packages/components/src/interfaces/complex/InfoCards.ts +++ b/packages/components/src/interfaces/complex/InfoCards.ts @@ -23,6 +23,7 @@ export const SingleCardSchema = Type.Object({ }), imageUrl: Type.String({ title: "Upload image", + format: "image", }), imageAlt: Type.String({ title: "Alternate text", diff --git a/packages/components/src/interfaces/complex/Infopic.ts b/packages/components/src/interfaces/complex/Infopic.ts index 85c300c75..8fd247744 100644 --- a/packages/components/src/interfaces/complex/Infopic.ts +++ b/packages/components/src/interfaces/complex/Infopic.ts @@ -15,6 +15,7 @@ export const InfopicSchema = Type.Object( imageSrc: Type.String({ title: "Upload image", description: "The URL to the image", + format: "image", }), imageAlt: Type.Optional( Type.String({