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, "")