From 5360635c113e373f916975911218052e454fadc6 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Fri, 5 Jul 2024 10:06:15 +0800 Subject: [PATCH 01/23] fix(Image-Upload-Rendere): add image upload renderer to jsonforms, modified schema adding "format":"image" to relevant fields --- apps/studio/src/constants/formBuilder.ts | 1 + .../components/form-builder/FormBuilder.tsx | 3 + .../controls/JsonFormsImageControl.tsx | 61 +++++++++++++++++++ .../form-builder/renderers/controls/index.ts | 4 ++ .../editing-experience/data/0.1.0.json | 36 +++++++---- 5 files changed, 93 insertions(+), 12 deletions(-) create mode 100644 apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsImageControl.tsx diff --git a/apps/studio/src/constants/formBuilder.ts b/apps/studio/src/constants/formBuilder.ts index 54c32c397..b38f399d6 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: 3, 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 69cc22809..0ffb5cf9f 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 @@ -8,6 +8,7 @@ import { JsonFormsArrayControl, JsonFormsBooleanControl, JsonFormsDropdownControl, + JsonFormsImageControl, JsonFormsIntegerControl, JsonFormsObjectControl, JsonFormsOneOfControl, @@ -19,6 +20,7 @@ import { jsonFormsDropdownControlTester, jsonFormsGroupLayoutRenderer, jsonFormsGroupLayoutTester, + jsonFormsImageControlTester, jsonFormsIntegerControlTester, jsonFormsObjectControlTester, jsonFormsOneOfControlTester, @@ -38,6 +40,7 @@ const renderers: JsonFormsRendererRegistryEntry[] = [ renderer: JsonFormsDropdownControl, }, { tester: jsonFormsIntegerControlTester, renderer: JsonFormsIntegerControl }, + { tester: jsonFormsImageControlTester, renderer: JsonFormsImageControl }, { tester: jsonFormsTextControlTester, renderer: JsonFormsTextControl }, { tester: jsonFormsOneOfControlTester, renderer: JsonFormsOneOfControl }, { tester: jsonFormsProseControlTester, renderer: JsonFormsProseControl }, 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..3dc10258d --- /dev/null +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsImageControl.tsx @@ -0,0 +1,61 @@ +import { + and, + isBooleanControl, + isStringControl, + or, + rankWith, + schemaMatches, + schemaTypeIs, + scopeEndsWith, + uiTypeIs, + type ControlProps, + type RankedTester, +} from '@jsonforms/core' +import { JSON_FORMS_RANKING } from '~/constants/formBuilder' +import { Attachment, FormLabel } from '@opengovsg/design-system-react' +import { Box, FormControl } from '@chakra-ui/react' +import { withJsonFormsControlProps } from '@jsonforms/react' + +export const jsonFormsImageControlTester: RankedTester = rankWith( + JSON_FORMS_RANKING.ImageControl, + and( + schemaMatches((schema) => { + console.log('Schema being tested:', schema) + return schema.format === 'image' + }), + isStringControl, + ), +) +export function JsonFormsImageControl({ + label, + schema, + handleChange, + errors, + path, + description, + required, +}: ControlProps) { + return ( + + + {label} + { + console.log(file?.name) + }} + onError={(error) => { + console.log(error) + }} + onRejection={(rejections) => { + console.log(rejections) + }} + /> + + + ) +} + +export default withJsonFormsControlProps(JsonFormsImageControl) 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 72fb28340..5b67149a9 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 @@ -34,3 +34,7 @@ export { default as JsonFormsTextControl, jsonFormsTextControlTester, } from './JsonFormsTextControl' +export { + default as JsonFormsImageControl, + jsonFormsImageControlTester, +} from './JsonFormsImageControl' diff --git a/apps/studio/src/features/editing-experience/data/0.1.0.json b/apps/studio/src/features/editing-experience/data/0.1.0.json index f3a306e29..4944787d9 100644 --- a/apps/studio/src/features/editing-experience/data/0.1.0.json +++ b/apps/studio/src/features/editing-experience/data/0.1.0.json @@ -596,7 +596,8 @@ "src": { "type": "string", "title": "Image URL", - "description": "The URL to the image to display" + "description": "The URL to the image to display", + "format": "image" }, "alt": { "type": "string", @@ -712,7 +713,8 @@ "imageUrl": { "type": "string", "title": "Card image URL", - "description": "The URL to the image to display for the card" + "description": "The URL to the image to display for the card", + "format": "image" }, "imageAlt": { "type": "string", @@ -837,7 +839,8 @@ "imageSrc": { "type": "string", "title": "Infopic image URL", - "description": "The URL to the image to display" + "description": "The URL to the image to display", + "format": "image" }, "imageAlt": { "type": "string", @@ -1040,7 +1043,8 @@ "backgroundUrl": { "type": "string", "title": "Hero background image URL", - "description": "The URL to the background image" + "description": "The URL to the background image", + "format": "image" }, "keyHighlights": { "$ref": "#/components/internal/heroKeyHighlights" @@ -1060,7 +1064,8 @@ "backgroundUrl": { "type": "string", "title": "Hero background image URL", - "description": "The URL to the background image" + "description": "The URL to the background image", + "format": "image" }, "keyHighlights": { "$ref": "#/components/internal/heroKeyHighlights" @@ -1134,7 +1139,8 @@ "backgroundUrl": { "type": "string", "title": "Hero background image URL", - "description": "The URL to the background image" + "description": "The URL to the background image", + "format": "image" }, "keyHighlights": { "$ref": "#/components/internal/heroKeyHighlights" @@ -1184,7 +1190,8 @@ "backgroundUrl": { "type": "string", "title": "Hero background image URL", - "description": "The URL to the background image" + "description": "The URL to the background image", + "format": "image" }, "keyHighlights": { "$ref": "#/components/internal/heroKeyHighlights" @@ -1237,7 +1244,8 @@ "backgroundUrl": { "type": "string", "title": "Hero background image URL", - "description": "The URL to the background image" + "description": "The URL to the background image", + "format": "image" }, "alignment": { "type": "string", @@ -1290,7 +1298,8 @@ "backgroundUrl": { "type": "string", "title": "Hero background image URL", - "description": "The URL to the background image" + "description": "The URL to the background image", + "format": "image" }, "alignment": { "type": "string", @@ -1349,7 +1358,8 @@ "backgroundUrl": { "type": "string", "title": "Hero background image URL", - "description": "The URL to the background image" + "description": "The URL to the background image", + "format": "image" } } }, @@ -1396,7 +1406,8 @@ "backgroundUrl": { "type": "string", "title": "Hero background image URL", - "description": "The URL to the background image" + "description": "The URL to the background image", + "format": "image" } } }, @@ -1467,7 +1478,8 @@ "src": { "type": "string", "title": "Image URL", - "description": "The URL to the image to display" + "description": "The URL to the image to display", + "format": "image" }, "alt": { "type": "string", From 67e821c9fb02a82f7778c0f952caabcb4a7ac95d Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:12:55 +0800 Subject: [PATCH 02/23] fix(image-control): added logic to display current image --- .../controls/JsonFormsImageControl.tsx | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) 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 index 3dc10258d..d28157d84 100644 --- 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 @@ -15,43 +15,73 @@ import { JSON_FORMS_RANKING } from '~/constants/formBuilder' import { Attachment, FormLabel } from '@opengovsg/design-system-react' import { Box, FormControl } from '@chakra-ui/react' import { withJsonFormsControlProps } from '@jsonforms/react' +import { useEffect, useState } from 'react' + +const MAX_IMG_FILE_SIZE_BYTES = 5000000 export const jsonFormsImageControlTester: RankedTester = rankWith( JSON_FORMS_RANKING.ImageControl, and( schemaMatches((schema) => { - console.log('Schema being tested:', schema) return schema.format === 'image' }), isStringControl, ), ) export function JsonFormsImageControl({ + data, label, - schema, handleChange, - errors, path, description, required, }: ControlProps) { + const [selectedFile, setSelectedFile] = useState() + + useEffect(() => { + // file should always reflect the linked URL image + const fetchImage = async () => { + const res = await fetch(data) + const blob = await res.blob() + const filename = 'current' + setSelectedFile(new File([blob], filename, { type: blob.type })) + } + if (data) { + fetchImage().catch((error) => + console.error('Error in fetching current image:', error), + ) + } + }, [data]) return ( - - {label} + + {label} { console.log(file?.name) + if (file) { + // TODO: file attached, upload file + const newImgUrl = 'replace_with_image_url' + handleChange(path, newImgUrl) + console.log('new url', newImgUrl) + } else { + handleChange(path, '') + } + console.log(file) + setSelectedFile(file) }} onError={(error) => { - console.log(error) + console.log('file attachment error ', error) }} onRejection={(rejections) => { console.log(rejections) }} + maxSize={MAX_IMG_FILE_SIZE_BYTES} + accept={['image/*']} /> From a171f1fd5c2fcdae64e94f005b5a599cbaa1b121 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:35:44 +0800 Subject: [PATCH 03/23] fix(image-control): add text for max image size --- .../renderers/controls/JsonFormsImageControl.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 index d28157d84..c02bd947a 100644 --- 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 @@ -13,7 +13,7 @@ import { } from '@jsonforms/core' import { JSON_FORMS_RANKING } from '~/constants/formBuilder' import { Attachment, FormLabel } from '@opengovsg/design-system-react' -import { Box, FormControl } from '@chakra-ui/react' +import { Box, FormControl, Text } from '@chakra-ui/react' import { withJsonFormsControlProps } from '@jsonforms/react' import { useEffect, useState } from 'react' @@ -43,7 +43,9 @@ export function JsonFormsImageControl({ const fetchImage = async () => { const res = await fetch(data) const blob = await res.blob() - const filename = 'current' + const splitUrl = data.split('.') + const extension = splitUrl[splitUrl.length - 1] + const filename = `image.${extension}` setSelectedFile(new File([blob], filename, { type: blob.type })) } if (data) { @@ -65,7 +67,7 @@ export function JsonFormsImageControl({ console.log(file?.name) if (file) { // TODO: file attached, upload file - const newImgUrl = 'replace_with_image_url' + const newImgUrl = '/assets/restricted-ogp-logo-full.svg' handleChange(path, newImgUrl) console.log('new url', newImgUrl) } else { @@ -83,6 +85,9 @@ export function JsonFormsImageControl({ maxSize={MAX_IMG_FILE_SIZE_BYTES} accept={['image/*']} /> + + {`Maximum file size: ${MAX_IMG_FILE_SIZE_BYTES / 1000000} MB`} + ) From 83928fd20de710cca35723b579edeb3432183e17 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Fri, 12 Jul 2024 17:45:30 +0800 Subject: [PATCH 04/23] fix(image-renderer): logic change to proxy preview image fetch through BE, but need to fix CSP(or rewrite dataurl2blob) and suspense, loading&error msg --- .../controls/JsonFormsImageControl.tsx | 95 +++++++++++-------- .../renderers/controls/constants.ts | 10 ++ apps/studio/src/schemas/page.ts | 3 + .../src/server/modules/page/page.router.ts | 17 ++++ 4 files changed, 87 insertions(+), 38 deletions(-) create mode 100644 apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/constants.ts 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 index c02bd947a..f3d2b3fee 100644 --- 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 @@ -1,6 +1,10 @@ +import type { ControlProps, RankedTester } from "@jsonforms/core" +import { useEffect, useState } from "react" +import { Box, FormControl, Text } from "@chakra-ui/react" import { and, isBooleanControl, + isEnabled, isStringControl, or, rankWith, @@ -8,25 +12,20 @@ import { schemaTypeIs, scopeEndsWith, uiTypeIs, - type ControlProps, - type RankedTester, -} from '@jsonforms/core' -import { JSON_FORMS_RANKING } from '~/constants/formBuilder' -import { Attachment, FormLabel } from '@opengovsg/design-system-react' -import { Box, FormControl, Text } from '@chakra-ui/react' -import { withJsonFormsControlProps } from '@jsonforms/react' -import { useEffect, useState } from 'react' +} from "@jsonforms/core" +import { withJsonFormsControlProps } from "@jsonforms/react" +import { Attachment, FormLabel } from "@opengovsg/design-system-react" -const MAX_IMG_FILE_SIZE_BYTES = 5000000 +import { JSON_FORMS_RANKING } from "~/constants/formBuilder" +import { trpc } from "~/utils/trpc" +import { + IMAGE_UPLOAD_ACCEPTED_MIME_TYPES, + MAX_IMG_FILE_SIZE_BYTES, +} from "./constants" export const jsonFormsImageControlTester: RankedTester = rankWith( JSON_FORMS_RANKING.ImageControl, - and( - schemaMatches((schema) => { - return schema.format === 'image' - }), - isStringControl, - ), + and(schemaMatches((schema) => schema.format === "image")), ) export function JsonFormsImageControl({ data, @@ -38,22 +37,44 @@ export function JsonFormsImageControl({ }: ControlProps) { const [selectedFile, setSelectedFile] = useState() - useEffect(() => { - // file should always reflect the linked URL image - const fetchImage = async () => { - const res = await fetch(data) - const blob = await res.blob() - const splitUrl = data.split('.') - const extension = splitUrl[splitUrl.length - 1] - const filename = `image.${extension}` - setSelectedFile(new File([blob], filename, { type: blob.type })) - } - if (data) { - fetchImage().catch((error) => - console.error('Error in fetching current image:', error), - ) + async function dataURLToFile(dataURL: string): Promise { + try { + const response = await fetch(dataURL) + const blob = await response.blob() + const mimeType = response.headers.get("Content-Type") || "" + + return new File([blob], "Currently selected image", { type: mimeType }) + } catch (error) { + return undefined } - }, [data]) + } + + trpc.page.readImageInPage.useQuery( + { + imageUrlInSchema: data, + }, + { + enabled: !!data, + async onSettled(queryData, error) { + if (!!error) { + // handle fetch error + console.log("image fetch error!") + } + if (!!queryData && !!queryData.imageDataURL) { + // Convert dataURL to file + // TODO: Figure out the CSP issue with feth + console.log("RECIEVE IMAGE", queryData.imageDataURL) + const file = await dataURLToFile(queryData.imageDataURL) + if (!!file) { + setSelectedFile(file) + } else { + console.log("Error setting selected file!") + } + } + }, + }, + ) + return ( @@ -67,26 +88,24 @@ export function JsonFormsImageControl({ console.log(file?.name) if (file) { // TODO: file attached, upload file - const newImgUrl = '/assets/restricted-ogp-logo-full.svg' + const newImgUrl = "https://picsum.photos/200/300" handleChange(path, newImgUrl) - console.log('new url', newImgUrl) + console.log("new url", newImgUrl) } else { - handleChange(path, '') + handleChange(path, "") } - console.log(file) - setSelectedFile(file) }} onError={(error) => { - console.log('file attachment error ', error) + console.log("file attachment error ", error) }} onRejection={(rejections) => { console.log(rejections) }} maxSize={MAX_IMG_FILE_SIZE_BYTES} - accept={['image/*']} + accept={IMAGE_UPLOAD_ACCEPTED_MIME_TYPES} /> - {`Maximum file size: ${MAX_IMG_FILE_SIZE_BYTES / 1000000} MB`} + {`Maximum file size: ${MAX_IMG_FILE_SIZE_BYTES / 1000000} MB`}`` 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..d21672167 --- /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/schemas/page.ts b/apps/studio/src/schemas/page.ts index 39e7a8edf..44ef2b8b2 100644 --- a/apps/studio/src/schemas/page.ts +++ b/apps/studio/src/schemas/page.ts @@ -49,3 +49,6 @@ export const createPageSchema = z.object({ // NOTE: implies that top level pages are allowed folderId: z.number().min(1).optional(), }) +export const readImageInPageSchema = z.object({ + imageUrlInSchema: z.string(), +}) diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index c898abb25..12db4a324 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -3,6 +3,7 @@ import { type ContentPageSchemaType } from "@opengovsg/isomer-components" import { createPageSchema, getEditPageSchema, + readImageInPageSchema, updatePageBlobSchema, updatePageSchema, } from "~/schemas/page" @@ -63,4 +64,20 @@ export const pageRouter = router({ return { pageId: "" } }), // TODO: Delete page stuff here + + readImageInPage: pageProcedure + .input(readImageInPageSchema) + .query(async ({ input, ctx }) => { + // NOTE: If image is not publically accessible, might need to add logic to get it + const res = await fetch(input.imageUrlInSchema) + const blob = await res.blob() + const arrayBuffer = await blob.arrayBuffer() + const base64String = Buffer.from(arrayBuffer).toString("base64") + const splitUrl = input.imageUrlInSchema.split(".") + const extension = splitUrl[splitUrl.length - 1] + const base64Data = `data:image/${extension};base64,${base64String}` + return { imageDataURL: base64Data } + }), + + //TODO: Might proxy image upload through backend too. }) From 906b259f0f41f7fc23a2972f4ae8f599db34fadb Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Tue, 16 Jul 2024 14:15:49 +0800 Subject: [PATCH 05/23] fix(image-prop-renderer): add feature(fetch image once on load, otherwise cache user image and display if mutation succeeds) --- apps/studio/next.config.mjs | 2 +- .../controls/JsonFormsImageControl.tsx | 75 +++++++++++++------ .../form-builder/renderers/controls/utils.ts | 30 ++++++++ apps/studio/src/schemas/page.ts | 4 + .../src/server/modules/page/page.router.ts | 16 ++-- 5 files changed, 98 insertions(+), 29 deletions(-) create mode 100644 apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/utils.ts diff --git a/apps/studio/next.config.mjs b/apps/studio/next.config.mjs index bea71f4f9..19ef6287a 100644 --- a/apps/studio/next.config.mjs +++ b/apps/studio/next.config.mjs @@ -19,7 +19,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' ${env.NODE_ENV === "production" ? "" : "'unsafe-eval'"}; 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 index f3d2b3fee..8b74bc1a4 100644 --- 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 @@ -1,6 +1,8 @@ +// don't use backend to render, proxy once on load, 3 states, cache locally till success +import { constants } from "fs/promises" import type { ControlProps, RankedTester } from "@jsonforms/core" -import { useEffect, useState } from "react" -import { Box, FormControl, Text } from "@chakra-ui/react" +import { useEffect, useMemo, useState } from "react" +import { Box, FormControl, Image, Text } from "@chakra-ui/react" import { and, isBooleanControl, @@ -22,10 +24,14 @@ import { IMAGE_UPLOAD_ACCEPTED_MIME_TYPES, MAX_IMG_FILE_SIZE_BYTES, } from "./constants" +import { BlobToImageDataURL, imageDataURLToFile } from "./utils" export const jsonFormsImageControlTester: RankedTester = rankWith( JSON_FORMS_RANKING.ImageControl, - and(schemaMatches((schema) => schema.format === "image")), + and( + isStringControl, + schemaMatches((schema) => schema.format === "image"), + ), ) export function JsonFormsImageControl({ data, @@ -36,35 +42,49 @@ export function JsonFormsImageControl({ required, }: ControlProps) { const [selectedFile, setSelectedFile] = useState() + const [pendingFile, setPendingFile] = useState() + const [shouldFetchImage, setShouldFetchImage] = useState(false) - async function dataURLToFile(dataURL: string): Promise { - try { - const response = await fetch(dataURL) - const blob = await response.blob() - const mimeType = response.headers.get("Content-Type") || "" - - return new File([blob], "Currently selected image", { type: mimeType }) - } catch (error) { - return undefined + useEffect(() => { + if (!!data) { + setShouldFetchImage(true) } - } + // NOTE: Using empty dependency array because we are checking if fetch is needed only upon initial load. + // After this load, we are the only editor of this page, and any image url changes are caused by us and we will be caching the file locally. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - trpc.page.readImageInPage.useQuery( + // NOTE: Run once only if initial load has non empty data(imageURL). + const uploadImageMutation = trpc.page.uploadImageGetURL.useMutation({ + onSettled(data, error, variables, context) { + if (!!error) { + console.log("upload trpc error") + } else { + const newImgUrl = data?.uploadedImageURL + setSelectedFile(pendingFile) + handleChange(path, newImgUrl) + console.log("new file url", newImgUrl) + } + }, + }) + const readImageQuery = trpc.page.readImageInPage.useQuery( { imageUrlInSchema: data, }, { - enabled: !!data, + enabled: shouldFetchImage, async onSettled(queryData, error) { + console.log("RECIEVE IMAGE", queryData?.imageDataURL) + + // setSelectedFile(new File([queryData?.imageData], "CurrentImage.jpeg")) if (!!error) { // handle fetch error console.log("image fetch error!") } if (!!queryData && !!queryData.imageDataURL) { // Convert dataURL to file - // TODO: Figure out the CSP issue with feth console.log("RECIEVE IMAGE", queryData.imageDataURL) - const file = await dataURLToFile(queryData.imageDataURL) + const file = imageDataURLToFile(queryData.imageDataURL) if (!!file) { setSelectedFile(file) } else { @@ -87,12 +107,23 @@ export function JsonFormsImageControl({ onChange={(file) => { console.log(file?.name) if (file) { - // TODO: file attached, upload file - const newImgUrl = "https://picsum.photos/200/300" - handleChange(path, newImgUrl) - console.log("new url", newImgUrl) + console.log("set pending file") + setPendingFile(file) + BlobToImageDataURL(file, file.type) + .then((imageDataURL) => { + uploadImageMutation.mutate({ imageDataURL }) + }) + .catch((reason) => + console.log("error converting image to dataurl, ", reason), + ) + // TODO: file attached, upload file. Below code could be in callback of upload TRPC call. + // Upload succeeded, note the race condition that we could have removed the file while uploading it! } else { + // NOTE: Do we need to update backend on removal of file? + console.log("remove file") handleChange(path, "") + setPendingFile(undefined) + setSelectedFile(undefined) } }} onError={(error) => { @@ -105,7 +136,7 @@ export function JsonFormsImageControl({ accept={IMAGE_UPLOAD_ACCEPTED_MIME_TYPES} /> - {`Maximum file size: ${MAX_IMG_FILE_SIZE_BYTES / 1000000} MB`}`` + {`Maximum file size: ${MAX_IMG_FILE_SIZE_BYTES / 1000000} MB`} diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/utils.ts b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/utils.ts new file mode 100644 index 000000000..713e5ef71 --- /dev/null +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/utils.ts @@ -0,0 +1,30 @@ +function getMIMEFromDataURL(dataURL: string): string { + const prefix = dataURL.split(",")[0] || "" + const mediaType = prefix.split(":")[1] || "" + let mimeType = mediaType.split(";")[0] || "" + mimeType = mimeType === "image/jpg" ? "image/jpeg" : mimeType + return mimeType.toUpperCase() +} +export function imageDataURLToFile(imageDataURL: string): File | undefined { + if (!imageDataURL) { + return undefined + } + const splitInput = imageDataURL.split(",") + const byteString = atob(splitInput[1] || "") + const mimeType = getMIMEFromDataURL(imageDataURL) + const ab = new ArrayBuffer(byteString.length) + const ia = new Uint8Array(ab) + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i) + } + return new File([ia], "Current image", { type: mimeType }) +} +export async function BlobToImageDataURL( + blob: Blob, + extension: string, +): Promise { + const arrayBuffer = await blob.arrayBuffer() + const base64String = Buffer.from(arrayBuffer).toString("base64") + const base64Data = `data:image/${extension};base64,${base64String}` + return base64Data +} diff --git a/apps/studio/src/schemas/page.ts b/apps/studio/src/schemas/page.ts index 44ef2b8b2..dd5e233ae 100644 --- a/apps/studio/src/schemas/page.ts +++ b/apps/studio/src/schemas/page.ts @@ -52,3 +52,7 @@ export const createPageSchema = z.object({ export const readImageInPageSchema = z.object({ imageUrlInSchema: z.string(), }) + +export const uploadImageGetURLSchema = z.object({ + imageDataURL: z.string(), +}) diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index 12db4a324..4c01fbc04 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -1,11 +1,13 @@ import { type ContentPageSchemaType } from "@opengovsg/isomer-components" +import { BlobToImageDataURL } from "~/features/editing-experience/components/form-builder/renderers/controls/utils" import { createPageSchema, getEditPageSchema, readImageInPageSchema, updatePageBlobSchema, updatePageSchema, + uploadImageGetURLSchema, } from "~/schemas/page" import { protectedProcedure, publicProcedure, router } from "~/server/trpc" import { @@ -71,13 +73,15 @@ export const pageRouter = router({ // NOTE: If image is not publically accessible, might need to add logic to get it const res = await fetch(input.imageUrlInSchema) const blob = await res.blob() - const arrayBuffer = await blob.arrayBuffer() - const base64String = Buffer.from(arrayBuffer).toString("base64") const splitUrl = input.imageUrlInSchema.split(".") - const extension = splitUrl[splitUrl.length - 1] - const base64Data = `data:image/${extension};base64,${base64String}` + const extension = splitUrl[splitUrl.length - 1] || "" + const base64Data = await BlobToImageDataURL(blob, extension) return { imageDataURL: base64Data } }), - - //TODO: Might proxy image upload through backend too. + uploadImageGetURL: pageProcedure + .input(uploadImageGetURLSchema) + .mutation(async ({ input, ctx }) => { + // TODO: Perform image upload logic here + return { uploadedImageURL: "https://picsum.photos/200/300.jpg" } + }), }) From 85bf482001bbe191278f520964a2fdbc18cec64a Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Tue, 16 Jul 2024 15:18:54 +0800 Subject: [PATCH 06/23] style(image-renderer): clean up code remove useless prints --- .../controls/JsonFormsImageControl.tsx | 42 +++++-------------- 1 file changed, 11 insertions(+), 31 deletions(-) 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 index 8b74bc1a4..648451217 100644 --- 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 @@ -1,20 +1,8 @@ // don't use backend to render, proxy once on load, 3 states, cache locally till success -import { constants } from "fs/promises" import type { ControlProps, RankedTester } from "@jsonforms/core" -import { useEffect, useMemo, useState } from "react" -import { Box, FormControl, Image, Text } from "@chakra-ui/react" -import { - and, - isBooleanControl, - isEnabled, - isStringControl, - or, - rankWith, - schemaMatches, - schemaTypeIs, - scopeEndsWith, - uiTypeIs, -} from "@jsonforms/core" +import { useEffect, 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 } from "@opengovsg/design-system-react" @@ -56,9 +44,9 @@ export function JsonFormsImageControl({ // NOTE: Run once only if initial load has non empty data(imageURL). const uploadImageMutation = trpc.page.uploadImageGetURL.useMutation({ - onSettled(data, error, variables, context) { + onSettled(data, error) { if (!!error) { - console.log("upload trpc error") + console.log("upload mutation error", error) } else { const newImgUrl = data?.uploadedImageURL setSelectedFile(pendingFile) @@ -67,23 +55,17 @@ export function JsonFormsImageControl({ } }, }) - const readImageQuery = trpc.page.readImageInPage.useQuery( + trpc.page.readImageInPage.useQuery( { - imageUrlInSchema: data, + imageUrlInSchema: data as string, }, { enabled: shouldFetchImage, - async onSettled(queryData, error) { - console.log("RECIEVE IMAGE", queryData?.imageDataURL) - - // setSelectedFile(new File([queryData?.imageData], "CurrentImage.jpeg")) + onSettled(queryData, error) { if (!!error) { - // handle fetch error - console.log("image fetch error!") + console.log("Image fetch error!") } if (!!queryData && !!queryData.imageDataURL) { - // Convert dataURL to file - console.log("RECIEVE IMAGE", queryData.imageDataURL) const file = imageDataURLToFile(queryData.imageDataURL) if (!!file) { setSelectedFile(file) @@ -107,27 +89,25 @@ export function JsonFormsImageControl({ onChange={(file) => { console.log(file?.name) if (file) { - console.log("set pending file") setPendingFile(file) BlobToImageDataURL(file, file.type) .then((imageDataURL) => { uploadImageMutation.mutate({ imageDataURL }) }) .catch((reason) => - console.log("error converting image to dataurl, ", reason), + console.log("Error converting image to dataurl, ", reason), ) // TODO: file attached, upload file. Below code could be in callback of upload TRPC call. // Upload succeeded, note the race condition that we could have removed the file while uploading it! } else { // NOTE: Do we need to update backend on removal of file? - console.log("remove file") handleChange(path, "") setPendingFile(undefined) setSelectedFile(undefined) } }} onError={(error) => { - console.log("file attachment error ", error) + console.log("File attachment error ", error) }} onRejection={(rejections) => { console.log(rejections) From 09ff019f09f0d46794dceb06a01c85830b30c3ac Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Tue, 16 Jul 2024 18:12:47 +0800 Subject: [PATCH 07/23] fix(image-renderer): add error messages --- .../controls/JsonFormsImageControl.tsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) 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 index 648451217..e544e3b20 100644 --- 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 @@ -1,10 +1,13 @@ -// don't use backend to render, proxy once on load, 3 states, cache locally till success import type { ControlProps, RankedTester } from "@jsonforms/core" import { useEffect, 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 } from "@opengovsg/design-system-react" +import { + Attachment, + FormErrorMessage, + FormLabel, +} from "@opengovsg/design-system-react" import { JSON_FORMS_RANKING } from "~/constants/formBuilder" import { trpc } from "~/utils/trpc" @@ -25,6 +28,7 @@ export function JsonFormsImageControl({ data, label, handleChange, + errors, path, description, required, @@ -32,7 +36,7 @@ export function JsonFormsImageControl({ const [selectedFile, setSelectedFile] = useState() const [pendingFile, setPendingFile] = useState() const [shouldFetchImage, setShouldFetchImage] = useState(false) - + const [errorMessage, setErrorMessage] = useState("") useEffect(() => { if (!!data) { setShouldFetchImage(true) @@ -46,10 +50,14 @@ export function JsonFormsImageControl({ const uploadImageMutation = trpc.page.uploadImageGetURL.useMutation({ onSettled(data, error) { if (!!error) { + setErrorMessage( + "Unable to upload image, please check your connection or try again.", + ) console.log("upload mutation error", error) } else { const newImgUrl = data?.uploadedImageURL setSelectedFile(pendingFile) + setErrorMessage("") handleChange(path, newImgUrl) console.log("new file url", newImgUrl) } @@ -70,6 +78,7 @@ export function JsonFormsImageControl({ if (!!file) { setSelectedFile(file) } else { + setErrorMessage("Previous selected image is not found.") console.log("Error setting selected file!") } } @@ -79,7 +88,7 @@ export function JsonFormsImageControl({ return ( - + {label} { + setErrorMessage("An error occured, please try again") console.log("File attachment error ", error) }} onRejection={(rejections) => { - console.log(rejections) + setErrorMessage("Please check your file size or file type.") + console.log(rejections, rejections.length) }} maxSize={MAX_IMG_FILE_SIZE_BYTES} accept={IMAGE_UPLOAD_ACCEPTED_MIME_TYPES} @@ -118,6 +129,7 @@ export function JsonFormsImageControl({ {`Maximum file size: ${MAX_IMG_FILE_SIZE_BYTES / 1000000} MB`} + {errorMessage} ) From ad99c394ff2a1553c8f97a7f0e0def240e678ea9 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Wed, 17 Jul 2024 10:56:56 +0800 Subject: [PATCH 08/23] fix(schema): add format:'image' for schemas where imageurl is expected to trigger image upload renderer --- packages/components/src/interfaces/complex/Image.ts | 1 + packages/components/src/interfaces/complex/InfoCards.ts | 1 + packages/components/src/interfaces/complex/Infopic.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/components/src/interfaces/complex/Image.ts b/packages/components/src/interfaces/complex/Image.ts index babe55de6..bf21b6038 100644 --- a/packages/components/src/interfaces/complex/Image.ts +++ b/packages/components/src/interfaces/complex/Image.ts @@ -7,6 +7,7 @@ export const ImageSchema = Type.Object( src: Type.String({ title: "Image source URL", description: "The source URL of the image", + format: "image", }), alt: Type.String({ title: "Image alt text", diff --git a/packages/components/src/interfaces/complex/InfoCards.ts b/packages/components/src/interfaces/complex/InfoCards.ts index a4568d57e..9e8dbfbde 100644 --- a/packages/components/src/interfaces/complex/InfoCards.ts +++ b/packages/components/src/interfaces/complex/InfoCards.ts @@ -13,6 +13,7 @@ export const SingleCardSchema = Type.Object({ imageUrl: Type.String({ title: "Card image URL", description: "The URL of the image to display on the card", + format: "image", }), imageAlt: Type.String({ title: "Card image alt text", diff --git a/packages/components/src/interfaces/complex/Infopic.ts b/packages/components/src/interfaces/complex/Infopic.ts index 7f3dc1d91..441d9e49e 100644 --- a/packages/components/src/interfaces/complex/Infopic.ts +++ b/packages/components/src/interfaces/complex/Infopic.ts @@ -25,6 +25,7 @@ export const InfopicSchema = Type.Object( imageSrc: Type.String({ title: "Infopic image URL", description: "The URL to the image", + format: "image", }), imageAlt: Type.Optional( Type.String({ From 509669a0cfc7de56a65b96b65d36a15b8c5e92b2 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:00:38 +0800 Subject: [PATCH 09/23] fix(pagerouter,-image-renderer,-constants): fix linting errors --- .../controls/JsonFormsImageControl.tsx | 34 +++++++++---------- .../renderers/controls/constants.ts | 14 ++++---- .../src/server/modules/page/page.router.ts | 2 +- 3 files changed, 24 insertions(+), 26 deletions(-) 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 index e544e3b20..1278c6d21 100644 --- 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 @@ -28,7 +28,6 @@ export function JsonFormsImageControl({ data, label, handleChange, - errors, path, description, required, @@ -48,19 +47,18 @@ export function JsonFormsImageControl({ // NOTE: Run once only if initial load has non empty data(imageURL). const uploadImageMutation = trpc.page.uploadImageGetURL.useMutation({ - onSettled(data, error) { - if (!!error) { - setErrorMessage( - "Unable to upload image, please check your connection or try again.", - ) - console.log("upload mutation error", error) - } else { - const newImgUrl = data?.uploadedImageURL - setSelectedFile(pendingFile) - setErrorMessage("") - handleChange(path, newImgUrl) - console.log("new file url", newImgUrl) - } + onSuccess: (data) => { + const newImgUrl = data.uploadedImageURL + setSelectedFile(pendingFile) + setErrorMessage("") + handleChange(path, newImgUrl) + console.log("new file url", newImgUrl) + }, + onError: (error) => { + setErrorMessage( + "Unable to upload image, please check your connection or try again.", + ) + console.log("upload mutation error", error) }, }) trpc.page.readImageInPage.useQuery( @@ -107,7 +105,6 @@ export function JsonFormsImageControl({ console.log("Error converting image to dataurl, ", reason), ) // TODO: file attached, upload file. Below code could be in callback of upload TRPC call. - // Upload succeeded, note the race condition that we could have removed the file while uploading it! } else { // NOTE: Do we need to update backend on removal of file? handleChange(path, "") @@ -116,12 +113,13 @@ export function JsonFormsImageControl({ } }} onError={(error) => { - setErrorMessage("An error occured, please try again") + setErrorMessage("An error occured, please try again: " + error) console.log("File attachment error ", error) }} onRejection={(rejections) => { - setErrorMessage("Please check your file size or file type.") - console.log(rejections, rejections.length) + if (rejections[0]?.errors[0]) { + setErrorMessage(rejections[0].errors[0].message) + } }} maxSize={MAX_IMG_FILE_SIZE_BYTES} accept={IMAGE_UPLOAD_ACCEPTED_MIME_TYPES} 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 index d21672167..a2e717b06 100644 --- 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 @@ -1,10 +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', + "image/jpeg", + "image/png", + "image/gif", + "image/svg+xml", + "image/tiff", + "image/bmp", + "image/webp", ] diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index 4c01fbc04..67fab6f3e 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -80,7 +80,7 @@ export const pageRouter = router({ }), uploadImageGetURL: pageProcedure .input(uploadImageGetURLSchema) - .mutation(async ({ input, ctx }) => { + .mutation(({ input, ctx }) => { // TODO: Perform image upload logic here return { uploadedImageURL: "https://picsum.photos/200/300.jpg" } }), From 20810af00217ee6a475f39aa71e52f5b297e9456 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:26:43 +0800 Subject: [PATCH 10/23] style(trpc.ts-iron-session.ts): linter fix --- apps/studio/src/server/trpc.ts | 40 +++++++++---------- .../tests/integration/helpers/iron-session.ts | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/apps/studio/src/server/trpc.ts b/apps/studio/src/server/trpc.ts index abeea5c23..fb70177bc 100644 --- a/apps/studio/src/server/trpc.ts +++ b/apps/studio/src/server/trpc.ts @@ -8,17 +8,17 @@ * @see https://trpc.io/docs/v10/procedures */ -import { initTRPC, TRPCError } from '@trpc/server' -import superjson from 'superjson' -import { ZodError } from 'zod' - -import { APP_VERSION_HEADER_KEY } from '~/constants/version' -import { env } from '~/env.mjs' -import { createBaseLogger } from '~/lib/logger' -import getIP from '~/utils/getClientIp' -import { type Context } from './context' -import { defaultMeSelect } from './modules/me/me.select' -import { prisma } from './prisma' +import { initTRPC, TRPCError } from "@trpc/server" +import superjson from "superjson" +import { ZodError } from "zod" + +import { APP_VERSION_HEADER_KEY } from "~/constants/version" +import { env } from "~/env.mjs" +import { createBaseLogger } from "~/lib/logger" +import getIP from "~/utils/getClientIp" +import { type Context } from "./context" +import { defaultMeSelect } from "./modules/me/me.select" +import { prisma } from "./prisma" const t = initTRPC.context().create({ /** @@ -34,7 +34,7 @@ const t = initTRPC.context().create({ data: { ...shape.data, zodError: - error.code === 'BAD_REQUEST' && error.cause instanceof ZodError + error.code === "BAD_REQUEST" && error.cause instanceof ZodError ? error.cause.flatten() : null, }, @@ -77,12 +77,12 @@ const loggerWithVersionMiddleware = loggerMiddleware.unstable_pipe( const clientVersion = req.headers[APP_VERSION_HEADER_KEY.toLowerCase()] if (clientVersion && serverVersion !== clientVersion) { - logger.warn('Application version mismatch', { + logger.warn("Application version mismatch", { clientVersion, serverVersion, }) } else if (!clientVersion) { - logger.warn('Client version not available', { + logger.warn("Client version not available", { serverVersion, }) } @@ -94,10 +94,10 @@ const loggerWithVersionMiddleware = loggerMiddleware.unstable_pipe( ) const contentTypeHeaderMiddleware = t.middleware(async ({ ctx, next }) => { - if (ctx.req.body && ctx.req.headers['content-type'] !== 'application/json') { + if (ctx.req.body && ctx.req.headers["content-type"] !== "application/json") { throw new TRPCError({ - code: 'BAD_REQUEST', - message: 'Invalid Content-Type', + code: "BAD_REQUEST", + message: "Invalid Content-Type", }) } return next() @@ -105,7 +105,7 @@ const contentTypeHeaderMiddleware = t.middleware(async ({ ctx, next }) => { const baseMiddleware = t.middleware(async ({ ctx, next }) => { if (ctx.session === undefined) { - throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR' }) + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }) } return next({ ctx: { @@ -116,7 +116,7 @@ const baseMiddleware = t.middleware(async ({ ctx, next }) => { const authMiddleware = t.middleware(async ({ next, ctx }) => { if (!ctx.session?.userId) { - throw new TRPCError({ code: 'UNAUTHORIZED' }) + throw new TRPCError({ code: "UNAUTHORIZED" }) } // this code path is needed if a user does not exist in the database as they were deleted, but the session was active before @@ -126,7 +126,7 @@ const authMiddleware = t.middleware(async ({ next, ctx }) => { }) if (user === null) { - throw new TRPCError({ code: 'UNAUTHORIZED' }) + throw new TRPCError({ code: "UNAUTHORIZED" }) } return next({ diff --git a/apps/studio/tests/integration/helpers/iron-session.ts b/apps/studio/tests/integration/helpers/iron-session.ts index e34e014c7..be63daa92 100644 --- a/apps/studio/tests/integration/helpers/iron-session.ts +++ b/apps/studio/tests/integration/helpers/iron-session.ts @@ -59,7 +59,7 @@ export const createMockRequest = ( { ...reqOptions, headers: { - 'content-type': 'application/json', // will always be application/json + "content-type": "application/json", // will always be application/json ...reqOptions.headers, }, }, From d4d743c5252412e4a6a3279a506280ec757fab57 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:10:42 +0800 Subject: [PATCH 11/23] fix(jsonformsimagecontrol): modify to presigned url upload and direct fetching of initial image upon load --- .../controls/JsonFormsImageControl.tsx | 82 +++++++++---------- apps/studio/src/schemas/page.ts | 5 +- .../src/server/modules/page/page.router.ts | 17 ++-- 3 files changed, 52 insertions(+), 52 deletions(-) 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 index 1278c6d21..60e66c76d 100644 --- 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 @@ -1,5 +1,6 @@ import type { ControlProps, RankedTester } from "@jsonforms/core" import { useEffect, useState } from "react" +import { useParams } from "next/navigation" import { Box, FormControl, Text } from "@chakra-ui/react" import { and, isStringControl, rankWith, schemaMatches } from "@jsonforms/core" import { withJsonFormsControlProps } from "@jsonforms/react" @@ -8,6 +9,7 @@ import { FormErrorMessage, FormLabel, } from "@opengovsg/design-system-react" +import wretch from "wretch" import { JSON_FORMS_RANKING } from "~/constants/formBuilder" import { trpc } from "~/utils/trpc" @@ -15,7 +17,6 @@ import { IMAGE_UPLOAD_ACCEPTED_MIME_TYPES, MAX_IMG_FILE_SIZE_BYTES, } from "./constants" -import { BlobToImageDataURL, imageDataURLToFile } from "./utils" export const jsonFormsImageControlTester: RankedTester = rankWith( JSON_FORMS_RANKING.ImageControl, @@ -32,13 +33,25 @@ export function JsonFormsImageControl({ description, required, }: ControlProps) { + const { pageId, siteId } = useParams() + const [selectedFile, setSelectedFile] = useState() const [pendingFile, setPendingFile] = useState() - const [shouldFetchImage, setShouldFetchImage] = useState(false) const [errorMessage, setErrorMessage] = useState("") + useEffect(() => { if (!!data) { - setShouldFetchImage(true) + wretch(data as string) + .get() + .blob() + .then((blob) => { + const splitData = (data as string).split("/") + const fileName = splitData[-1] || "Current Image" + setSelectedFile(new File([blob], fileName)) + }) + .catch((error) => { + console.log("error fetching initial image", error) + }) } // NOTE: Using empty dependency array because we are checking if fetch is needed only upon initial load. // After this load, we are the only editor of this page, and any image url changes are caused by us and we will be caching the file locally. @@ -46,43 +59,31 @@ export function JsonFormsImageControl({ }, []) // NOTE: Run once only if initial load has non empty data(imageURL). - const uploadImageMutation = trpc.page.uploadImageGetURL.useMutation({ - onSuccess: (data) => { - const newImgUrl = data.uploadedImageURL + const getPresignedMutation = + trpc.page.getPresignUrlForImageUpload.useMutation() + const uploadImage = async (image: File) => { + const { presignedUploadURL, fileURL } = + await getPresignedMutation.mutateAsync({ + pageId: Number(pageId), + siteId: Number(siteId), + }) + const response = await wretch(presignedUploadURL) + .content(image.type) + .put(image) + .res() + if (response.ok) { setSelectedFile(pendingFile) setErrorMessage("") - handleChange(path, newImgUrl) - console.log("new file url", newImgUrl) - }, - onError: (error) => { + handleChange(path, fileURL) + console.log("new file url", fileURL) + } else { + setPendingFile(undefined) setErrorMessage( - "Unable to upload image, please check your connection or try again.", + "There is an error uploading your file. Please try again or contact support.", ) - console.log("upload mutation error", error) - }, - }) - trpc.page.readImageInPage.useQuery( - { - imageUrlInSchema: data as string, - }, - { - enabled: shouldFetchImage, - onSettled(queryData, error) { - if (!!error) { - console.log("Image fetch error!") - } - if (!!queryData && !!queryData.imageDataURL) { - const file = imageDataURLToFile(queryData.imageDataURL) - if (!!file) { - setSelectedFile(file) - } else { - setErrorMessage("Previous selected image is not found.") - console.log("Error setting selected file!") - } - } - }, - }, - ) + console.log("file upload failure", response) + } + } return ( @@ -97,14 +98,7 @@ export function JsonFormsImageControl({ console.log(file?.name) if (file) { setPendingFile(file) - BlobToImageDataURL(file, file.type) - .then((imageDataURL) => { - uploadImageMutation.mutate({ imageDataURL }) - }) - .catch((reason) => - console.log("Error converting image to dataurl, ", reason), - ) - // TODO: file attached, upload file. Below code could be in callback of upload TRPC call. + void uploadImage(file) } else { // NOTE: Do we need to update backend on removal of file? handleChange(path, "") diff --git a/apps/studio/src/schemas/page.ts b/apps/studio/src/schemas/page.ts index dd5e233ae..4a8c36b9d 100644 --- a/apps/studio/src/schemas/page.ts +++ b/apps/studio/src/schemas/page.ts @@ -53,6 +53,7 @@ export const readImageInPageSchema = z.object({ imageUrlInSchema: z.string(), }) -export const uploadImageGetURLSchema = z.object({ - imageDataURL: z.string(), +export const getPresignUrlForImageUploadSchema = z.object({ + siteId: z.number().min(1), + pageId: z.number().min(1), }) diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index 8ca0e7c58..4e38f243a 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -1,5 +1,6 @@ import type { ContentPageSchemaType } from "@opengovsg/isomer-components" import { schema } from "@opengovsg/isomer-components" +import { createId } from "@paralleldrive/cuid2" import { TRPCError } from "@trpc/server" import Ajv from "ajv" @@ -7,10 +8,10 @@ import { BlobToImageDataURL } from "~/features/editing-experience/components/for import { createPageSchema, getEditPageSchema, + getPresignUrlForImageUploadSchema, readImageInPageSchema, updatePageBlobSchema, updatePageSchema, - uploadImageGetURLSchema, } from "~/schemas/page" import { protectedProcedure, router } from "~/server/trpc" import { safeJsonParse } from "~/utils/safeJsonParse" @@ -100,7 +101,7 @@ export const pageRouter = router({ }), // TODO: Delete page stuff here - readImageInPage: pageProcedure + readImageInPage: protectedProcedure .input(readImageInPageSchema) .query(async ({ input, ctx }) => { // NOTE: If image is not publically accessible, might need to add logic to get it @@ -111,10 +112,14 @@ export const pageRouter = router({ const base64Data = await BlobToImageDataURL(blob, extension) return { imageDataURL: base64Data } }), - uploadImageGetURL: pageProcedure - .input(uploadImageGetURLSchema) + getPresignUrlForImageUpload: protectedProcedure + .input(getPresignUrlForImageUploadSchema) .mutation(({ input, ctx }) => { - // TODO: Perform image upload logic here - return { uploadedImageURL: "https://picsum.photos/200/300.jpg" } + // TODO: Generate key and presign S3 url. + return { + presignedUploadURL: "", + // fileURL like https://BUCKET.s3.amazonaws.com/key + fileURL: "", + } }), }) From 93d5005660fd7d5e202edfc2cb9eab4a98bccd89 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Mon, 22 Jul 2024 09:35:25 +0800 Subject: [PATCH 12/23] fix(page.router.ts): remove read image endpoint from page.router.ts --- .../renderers/controls/JsonFormsImageControl.tsx | 2 -- apps/studio/src/server/modules/page/page.router.ts | 11 ----------- 2 files changed, 13 deletions(-) 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 index 60e66c76d..9c3073837 100644 --- 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 @@ -57,8 +57,6 @@ export function JsonFormsImageControl({ // After this load, we are the only editor of this page, and any image url changes are caused by us and we will be caching the file locally. // eslint-disable-next-line react-hooks/exhaustive-deps }, []) - - // NOTE: Run once only if initial load has non empty data(imageURL). const getPresignedMutation = trpc.page.getPresignUrlForImageUpload.useMutation() const uploadImage = async (image: File) => { diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index eab249770..92cd44363 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -144,17 +144,6 @@ export const pageRouter = router({ }, ), - readImageInPage: protectedProcedure - .input(readImageInPageSchema) - .query(async ({ input, ctx }) => { - // NOTE: If image is not publically accessible, might need to add logic to get it - const res = await fetch(input.imageUrlInSchema) - const blob = await res.blob() - const splitUrl = input.imageUrlInSchema.split(".") - const extension = splitUrl[splitUrl.length - 1] || "" - const base64Data = await BlobToImageDataURL(blob, extension) - return { imageDataURL: base64Data } - }), getPresignUrlForImageUpload: protectedProcedure .input(getPresignUrlForImageUploadSchema) .mutation(({ input, ctx }) => { From 743fb97f7f74fc2ad081382e6c19be3c06077bf1 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:57:43 +0800 Subject: [PATCH 13/23] fix(page.router.ts): fix import --- apps/studio/src/server/modules/page/page.router.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index 92cd44363..d9ac29752 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -1,17 +1,12 @@ import { schema } from "@opengovsg/isomer-components" -import { createId } from "@paralleldrive/cuid2" import { TRPCError } from "@trpc/server" import Ajv from "ajv" import { z } from "zod" -import { BlobToImageDataURL } from "~/features/editing-experience/components/form-builder/renderers/controls/utils" import { createPageSchema, getEditPageSchema, getPresignUrlForImageUploadSchema, - readImageInPageSchema, - updatePageBlobSchema, - updatePageSchema, } from "~/schemas/page" import { protectedProcedure, router } from "~/server/trpc" import { safeJsonParse } from "~/utils/safeJsonParse" From 8fbc23b60d64fb1c17073a37e9ebe6cca33ca492 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:10:50 +0800 Subject: [PATCH 14/23] style(page.router.ts): rename getPresign... to getPresigned --- .../renderers/controls/JsonFormsImageControl.tsx | 4 ++-- apps/studio/src/schemas/page.ts | 2 +- apps/studio/src/server/modules/page/page.router.ts | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) 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 index 9c3073837..a8b58a315 100644 --- 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 @@ -46,7 +46,7 @@ export function JsonFormsImageControl({ .blob() .then((blob) => { const splitData = (data as string).split("/") - const fileName = splitData[-1] || "Current Image" + const fileName = splitData[-1] || "image" setSelectedFile(new File([blob], fileName)) }) .catch((error) => { @@ -58,7 +58,7 @@ export function JsonFormsImageControl({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []) const getPresignedMutation = - trpc.page.getPresignUrlForImageUpload.useMutation() + trpc.page.getPresignedUrlForImageUpload.useMutation() const uploadImage = async (image: File) => { const { presignedUploadURL, fileURL } = await getPresignedMutation.mutateAsync({ diff --git a/apps/studio/src/schemas/page.ts b/apps/studio/src/schemas/page.ts index c9e2deed1..818d21bb7 100644 --- a/apps/studio/src/schemas/page.ts +++ b/apps/studio/src/schemas/page.ts @@ -43,7 +43,7 @@ export const readImageInPageSchema = z.object({ imageUrlInSchema: z.string(), }) -export const getPresignUrlForImageUploadSchema = z.object({ +export const getPresignedUrlForImageUploadSchema = z.object({ siteId: z.number().min(1), pageId: z.number().min(1), }) diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index c01a532b6..0e359c0cf 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -7,7 +7,7 @@ import { z } from "zod" import { createPageSchema, getEditPageSchema, - getPresignUrlForImageUploadSchema, + getPresignUrlForImageUploadSchema as getPresignedUrlForImageUploadSchema, } from "~/schemas/page" import { protectedProcedure, router } from "~/server/trpc" import { safeJsonParse } from "~/utils/safeJsonParse" @@ -142,8 +142,8 @@ export const pageRouter = router({ }, ), - getPresignUrlForImageUpload: protectedProcedure - .input(getPresignUrlForImageUploadSchema) + getPresignedUrlForImageUpload: protectedProcedure + .input(getPresignedUrlForImageUploadSchema) .mutation(({ input, ctx }) => { // TODO: Generate key and presign S3 url. return { From a119866b6421ef11297e60909e2f0d065763a0e2 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:11:45 +0800 Subject: [PATCH 15/23] style(page.router.ts): correct import --- apps/studio/src/server/modules/page/page.router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index 0e359c0cf..4ee4a7539 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -7,7 +7,7 @@ import { z } from "zod" import { createPageSchema, getEditPageSchema, - getPresignUrlForImageUploadSchema as getPresignedUrlForImageUploadSchema, + getPresignedUrlForImageUploadSchema, } from "~/schemas/page" import { protectedProcedure, router } from "~/server/trpc" import { safeJsonParse } from "~/utils/safeJsonParse" From 7d37a8ab0d6499372f5fac414b718f44a0eb6ade Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:37:46 +0800 Subject: [PATCH 16/23] fix(image-renderer): remove upload logic --- .../controls/JsonFormsImageControl.tsx | 77 ++++--------------- 1 file changed, 16 insertions(+), 61 deletions(-) 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 index a8b58a315..9624318dd 100644 --- 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 @@ -4,11 +4,7 @@ import { useParams } from "next/navigation" import { Box, FormControl, Text } from "@chakra-ui/react" import { and, isStringControl, rankWith, schemaMatches } from "@jsonforms/core" import { withJsonFormsControlProps } from "@jsonforms/react" -import { - Attachment, - FormErrorMessage, - FormLabel, -} from "@opengovsg/design-system-react" +import { Attachment, FormLabel, useToast } from "@opengovsg/design-system-react" import wretch from "wretch" import { JSON_FORMS_RANKING } from "~/constants/formBuilder" @@ -26,7 +22,6 @@ export const jsonFormsImageControlTester: RankedTester = rankWith( ), ) export function JsonFormsImageControl({ - data, label, handleChange, path, @@ -34,83 +29,44 @@ export function JsonFormsImageControl({ required, }: ControlProps) { const { pageId, siteId } = useParams() + const toast = useToast() - const [selectedFile, setSelectedFile] = useState() const [pendingFile, setPendingFile] = useState() - const [errorMessage, setErrorMessage] = useState("") - - useEffect(() => { - if (!!data) { - wretch(data as string) - .get() - .blob() - .then((blob) => { - const splitData = (data as string).split("/") - const fileName = splitData[-1] || "image" - setSelectedFile(new File([blob], fileName)) - }) - .catch((error) => { - console.log("error fetching initial image", error) - }) - } - // NOTE: Using empty dependency array because we are checking if fetch is needed only upon initial load. - // After this load, we are the only editor of this page, and any image url changes are caused by us and we will be caching the file locally. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - const getPresignedMutation = - trpc.page.getPresignedUrlForImageUpload.useMutation() - const uploadImage = async (image: File) => { - const { presignedUploadURL, fileURL } = - await getPresignedMutation.mutateAsync({ - pageId: Number(pageId), - siteId: Number(siteId), - }) - const response = await wretch(presignedUploadURL) - .content(image.type) - .put(image) - .res() - if (response.ok) { - setSelectedFile(pendingFile) - setErrorMessage("") - handleChange(path, fileURL) - console.log("new file url", fileURL) - } else { - setPendingFile(undefined) - setErrorMessage( - "There is an error uploading your file. Please try again or contact support.", - ) - console.log("file upload failure", response) - } - } return ( - + {label} { - console.log(file?.name) if (file) { setPendingFile(file) - void uploadImage(file) + // TODO: Upload file logic? + handleChange(path, "https://127.0.0.1/dummyurl") } else { // NOTE: Do we need to update backend on removal of file? handleChange(path, "") setPendingFile(undefined) - setSelectedFile(undefined) } }} onError={(error) => { - setErrorMessage("An error occured, please try again: " + error) - console.log("File attachment error ", error) + toast({ + title: "Image error", + description: error, + status: "error", + }) }} onRejection={(rejections) => { if (rejections[0]?.errors[0]) { - setErrorMessage(rejections[0].errors[0].message) + toast({ + title: "Image rejected", + description: rejections[0].errors[0].message, + status: "error", + }) } }} maxSize={MAX_IMG_FILE_SIZE_BYTES} @@ -119,7 +75,6 @@ export function JsonFormsImageControl({ {`Maximum file size: ${MAX_IMG_FILE_SIZE_BYTES / 1000000} MB`} - {errorMessage} ) From af13e3ed9ce5157078c718a12b22a9f61afb8115 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:50:04 +0800 Subject: [PATCH 17/23] fix(next.config.mjs): remove blob from img-src --- apps/studio/next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/next.config.mjs b/apps/studio/next.config.mjs index dc6bc85c4..389fefca0 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: blob:; + img-src * data:; frame-src 'self'; object-src 'none'; script-src 'self' 'unsafe-eval'; From 09c69dd41073c31b33228f974ed4ffb9e511cd10 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:51:20 +0800 Subject: [PATCH 18/23] fix(page.router): remove added endpoints for presignedurl generation --- apps/studio/src/schemas/page.ts | 8 -------- .../src/server/modules/page/page.router.ts | 17 +---------------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/apps/studio/src/schemas/page.ts b/apps/studio/src/schemas/page.ts index 818d21bb7..a565809f0 100644 --- a/apps/studio/src/schemas/page.ts +++ b/apps/studio/src/schemas/page.ts @@ -39,11 +39,3 @@ export const createPageSchema = z.object({ // NOTE: implies that top level pages are allowed folderId: z.number().min(1).optional(), }) -export const readImageInPageSchema = z.object({ - imageUrlInSchema: z.string(), -}) - -export const getPresignedUrlForImageUploadSchema = z.object({ - siteId: z.number().min(1), - pageId: z.number().min(1), -}) diff --git a/apps/studio/src/server/modules/page/page.router.ts b/apps/studio/src/server/modules/page/page.router.ts index 4ee4a7539..d7204ccdd 100644 --- a/apps/studio/src/server/modules/page/page.router.ts +++ b/apps/studio/src/server/modules/page/page.router.ts @@ -4,11 +4,7 @@ import { TRPCError } from "@trpc/server" import Ajv from "ajv" import { z } from "zod" -import { - createPageSchema, - getEditPageSchema, - getPresignedUrlForImageUploadSchema, -} from "~/schemas/page" +import { createPageSchema, getEditPageSchema } from "~/schemas/page" import { protectedProcedure, router } from "~/server/trpc" import { safeJsonParse } from "~/utils/safeJsonParse" import { db, ResourceType } from "../database" @@ -141,15 +137,4 @@ export const pageRouter = router({ return { pageId: resource.id } }, ), - - getPresignedUrlForImageUpload: protectedProcedure - .input(getPresignedUrlForImageUploadSchema) - .mutation(({ input, ctx }) => { - // TODO: Generate key and presign S3 url. - return { - presignedUploadURL: "", - // fileURL like https://BUCKET.s3.amazonaws.com/key - fileURL: "", - } - }), }) From 08c59829fa387ebb3acba6199d84d5188cc97646 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:52:26 +0800 Subject: [PATCH 19/23] fix(0.1.0): remove 0.1.0.json --- apps/studio/src/features/editing-experience/data/0.1.0.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/studio/src/features/editing-experience/data/0.1.0.json diff --git a/apps/studio/src/features/editing-experience/data/0.1.0.json b/apps/studio/src/features/editing-experience/data/0.1.0.json deleted file mode 100644 index e69de29bb..000000000 From a27117e78988fc493831acdc6f645a7286b5fd65 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Thu, 25 Jul 2024 14:55:42 +0800 Subject: [PATCH 20/23] fix(next.config.mjs): need to add blob: to imgsrc for content policy to allow preview --- apps/studio/next.config.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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'; From 6ebea0dd5fd8188157b54e1e8b0a9b40dd0775cf Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:54:58 +0800 Subject: [PATCH 21/23] style(JsonFormsImageControl): remove unused imports --- .../renderers/controls/JsonFormsImageControl.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 index 9624318dd..4827c317e 100644 --- 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 @@ -1,14 +1,11 @@ import type { ControlProps, RankedTester } from "@jsonforms/core" -import { useEffect, useState } from "react" -import { useParams } from "next/navigation" +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 wretch from "wretch" import { JSON_FORMS_RANKING } from "~/constants/formBuilder" -import { trpc } from "~/utils/trpc" import { IMAGE_UPLOAD_ACCEPTED_MIME_TYPES, MAX_IMG_FILE_SIZE_BYTES, @@ -28,7 +25,6 @@ export function JsonFormsImageControl({ description, required, }: ControlProps) { - const { pageId, siteId } = useParams() const toast = useToast() const [pendingFile, setPendingFile] = useState() From db0752ed45f27c27b4f98751b289270d5c1f09c0 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:39:18 +0800 Subject: [PATCH 22/23] style(utils(blob-<=>-dataurl-conversion-helper_): removed utils file(blob<=>dataurl conversion helper functions) since out of scope for this pr --- .../form-builder/renderers/controls/utils.ts | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/utils.ts diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/utils.ts b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/utils.ts deleted file mode 100644 index 713e5ef71..000000000 --- a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/utils.ts +++ /dev/null @@ -1,30 +0,0 @@ -function getMIMEFromDataURL(dataURL: string): string { - const prefix = dataURL.split(",")[0] || "" - const mediaType = prefix.split(":")[1] || "" - let mimeType = mediaType.split(";")[0] || "" - mimeType = mimeType === "image/jpg" ? "image/jpeg" : mimeType - return mimeType.toUpperCase() -} -export function imageDataURLToFile(imageDataURL: string): File | undefined { - if (!imageDataURL) { - return undefined - } - const splitInput = imageDataURL.split(",") - const byteString = atob(splitInput[1] || "") - const mimeType = getMIMEFromDataURL(imageDataURL) - const ab = new ArrayBuffer(byteString.length) - const ia = new Uint8Array(ab) - for (let i = 0; i < byteString.length; i++) { - ia[i] = byteString.charCodeAt(i) - } - return new File([ia], "Current image", { type: mimeType }) -} -export async function BlobToImageDataURL( - blob: Blob, - extension: string, -): Promise { - const arrayBuffer = await blob.arrayBuffer() - const base64String = Buffer.from(arrayBuffer).toString("base64") - const base64Data = `data:image/${extension};base64,${base64String}` - return base64Data -} From e0b0e7d5bbf6223d970a5c913f48f6cfdf156299 Mon Sep 17 00:00:00 2001 From: Hanpu Liu <26217378+hanpuliu-charles@users.noreply.github.com> Date: Mon, 29 Jul 2024 18:14:27 +0800 Subject: [PATCH 23/23] style(image-custom-renderer): change placeholder path to placeholder image so it shows in the preview --- .../form-builder/renderers/controls/JsonFormsImageControl.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 4827c317e..250af378d 100644 --- 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 @@ -42,7 +42,7 @@ export function JsonFormsImageControl({ if (file) { setPendingFile(file) // TODO: Upload file logic? - handleChange(path, "https://127.0.0.1/dummyurl") + handleChange(path, "https://picsum.photos/id/237/200/300") } else { // NOTE: Do we need to update backend on removal of file? handleChange(path, "")