From 7c11803af7493c078b987f192845e3e24cce6e58 Mon Sep 17 00:00:00 2001 From: seaerchin <44049504+seaerchin@users.noreply.github.com> Date: Wed, 24 Jul 2024 19:48:03 +0800 Subject: [PATCH] feat(frontend): add prose-renderer (#317) * chore: convert add script * refactor: resource use 2 queries cos inner join + update in same query doesn't work * refactor: component selector update typings * feat: editor drawer context add curr active idx * feat: editpagedrawer update so data is dynamic * chore: componentselector only paragraph uses component selector * chore: update convert function 1. add zod validation 2. simplify the function * chore: component selector * chore: components selector update to correct names * chore: constants update default prose block * feat: convert add convert functions * chore: typings * chore: next state compute next state from block * feat: update update UI on save click * feat: update update pages automatically * chore: rename editorstate * chore: utils delete obsolete function * chore: menubar remove title cos we don't want them to set h1 * chore: remove unused imports * chore: remove json schema setting * fix: tiptap update typings + validation * chore: componentselector update to remove unsafe cast * fix: add snapshot * chore: add added block state to drawer context * wip commit * chore: export update export from components package * chore: update imports * chore: update import * fix: editing experience update schema for type checking * chore: remove unused comment * chore: menubar remove extra itesm * chore: prose add format * chore: menubar ui fixes * feat: add tiptap renderer * chore: delete old imports * chore: format * fix: resource update service select for blob * chore: style lint and format fixes * chore: fixup * chore: style/lint * fix: root state drawer add title for prose * chore: formbuilder * fix: prose shift into top level * fix: component selector * fix: preview issues * docs: component selector update docs * fix: import * chore: style * chore: fix imports * chore: utils removed unused function * chore: route params use query parse --- .../PageEditor/ComponentSelector.tsx | 23 +++- .../src/components/PageEditor/MenuBar.tsx | 51 +------- .../src/components/PageEditor/constants.ts | 2 +- .../src/contexts/EditorDrawerContext.tsx | 9 +- .../components/ComplexEditorStateDrawer.tsx | 2 +- .../components/RootStateDrawer.tsx | 6 +- .../components/TipTapComponent.tsx | 113 +----------------- .../components/form-builder/FormBuilder.tsx | 2 +- .../form-builder/renderers/TipTapEditor.tsx | 110 +++++++++++++++++ .../controls/JsonFormsProseControl.tsx | 17 ++- .../sites/[siteId]/pages/[pageId]/index.tsx | 2 +- packages/components/src/index.ts | 2 + 12 files changed, 162 insertions(+), 177 deletions(-) create mode 100644 apps/studio/src/features/editing-experience/components/form-builder/renderers/TipTapEditor.tsx diff --git a/apps/studio/src/components/PageEditor/ComponentSelector.tsx b/apps/studio/src/components/PageEditor/ComponentSelector.tsx index 4fbd40edfd..9ee6c94dfc 100644 --- a/apps/studio/src/components/PageEditor/ComponentSelector.tsx +++ b/apps/studio/src/components/PageEditor/ComponentSelector.tsx @@ -1,3 +1,4 @@ +import type { IsomerComponent } from "@opengovsg/isomer-components" import { Flex, Popover, @@ -9,7 +10,6 @@ import { Wrap, } from "@chakra-ui/react" import { Button, IconButton } from "@opengovsg/design-system-react" -import { type IsomerComponent } from "@opengovsg/isomer-components" import { type IconType } from "react-icons" import { BiCard, @@ -28,7 +28,10 @@ import { } from "react-icons/bi" import { useEditorDrawerContext } from "~/contexts/EditorDrawerContext" +import { useQueryParse } from "~/hooks/useQueryParse" +import { editPageSchema } from "~/pages/sites/[siteId]/pages/[pageId]" import { type DrawerState } from "~/types/editorDrawer" +import { trpc } from "~/utils/trpc" import { DEFAULT_BLOCKS } from "./constants" import { type SectionType } from "./types" @@ -103,7 +106,15 @@ function ComponentSelector() { setDrawerState, setSavedPageState, setPreviewPageState, + setAddedBlock, } = useEditorDrawerContext() + + const { pageId, siteId } = useQueryParse(editPageSchema) + const [page] = trpc.page.readPageAndBlob.useSuspenseQuery({ + pageId, + siteId, + }) + const onProceed = (sectionType: SectionType) => { // TODO: add new section to page/editor state // NOTE: Only paragraph should go to tiptap editor @@ -117,10 +128,18 @@ function ComponentSelector() { const nextPageState = !!newComponent ? [...savedPageState, newComponent] : savedPageState - setSavedPageState(nextPageState) + setDrawerState({ state: nextState }) setCurrActiveIdx(nextPageState.length - 1) setPreviewPageState(nextPageState) + + // TODO: Decide if setting addedBlocks + // to only be for complex blocks is a good idea + // or if we should combine prose to addedBlocks as well + // and handle prose -> complex components in the renderer itself + if (sectionType !== "prose") { + setAddedBlock(sectionType) + } } return ( diff --git a/apps/studio/src/components/PageEditor/MenuBar.tsx b/apps/studio/src/components/PageEditor/MenuBar.tsx index aeb1caa0a7..322b5a7740 100644 --- a/apps/studio/src/components/PageEditor/MenuBar.tsx +++ b/apps/studio/src/components/PageEditor/MenuBar.tsx @@ -18,19 +18,11 @@ import { BiBold, BiChevronDown, BiChevronUp, - BiCodeAlt, - BiFile, - BiImageAdd, BiItalic, - BiLink, BiListOl, BiListUl, - BiPlus, - BiRedo, BiStrikethrough, - BiTable, BiUnderline, - BiUndo, } from "react-icons/bi" import { MdSubscript, MdSuperscript } from "react-icons/md" @@ -224,48 +216,6 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { }, ], }, - { - type: "divider", - }, - // { - // type: 'item', - // icon: BiLink, - // title: 'Add link', - // action: () => showModal('hyperlink'), - // }, - { - type: "item", - icon: BiTable, - title: "Add table", - action: () => - editor - .chain() - .focus() - // NOTE: Default to smallest multi table - .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) - .run(), - }, - // { - // type: 'item', - // icon: BiFile, - // title: 'Add file', - // action: () => showModal('files'), - // }, - { - type: "divider", - }, - { - type: "item", - icon: BiUndo, - title: "Undo", - action: () => editor.chain().focus().undo().run(), - }, - { - type: "item", - icon: BiRedo, - title: "Redo", - action: () => editor.chain().focus().redo().run(), - }, ] return ( @@ -276,6 +226,7 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { pl="0.75rem" pr="0.25rem" py="0.25rem" + w="100%" borderBottom="1px solid" borderColor="base.divider.strong" borderTopRadius="0.25rem" diff --git a/apps/studio/src/components/PageEditor/constants.ts b/apps/studio/src/components/PageEditor/constants.ts index 9889268d45..e8a86253f3 100644 --- a/apps/studio/src/components/PageEditor/constants.ts +++ b/apps/studio/src/components/PageEditor/constants.ts @@ -1,4 +1,4 @@ -import { type IsomerComponent } from "@opengovsg/isomer-components" +import type { IsomerComponent } from "@opengovsg/isomer-components" // TODO: add in default blocks for remaining export const DEFAULT_BLOCKS: Record< diff --git a/apps/studio/src/contexts/EditorDrawerContext.tsx b/apps/studio/src/contexts/EditorDrawerContext.tsx index 1597de69f4..77a70b2b88 100644 --- a/apps/studio/src/contexts/EditorDrawerContext.tsx +++ b/apps/studio/src/contexts/EditorDrawerContext.tsx @@ -1,7 +1,8 @@ +import type { IsomerComponent } from "@opengovsg/isomer-components" import type { Dispatch, PropsWithChildren, SetStateAction } from "react" import { createContext, useContext, useState } from "react" -import { type IsomerComponent } from "@opengovsg/isomer-components" +import type { SectionType } from "~/components/PageEditor/types" import { type DrawerState } from "~/types/editorDrawer" export interface DrawerContextType { @@ -9,6 +10,8 @@ export interface DrawerContextType { setCurrActiveIdx: (currActiveIdx: number) => void drawerState: DrawerState setDrawerState: (state: DrawerState) => void + addedBlock: Exclude + setAddedBlock: (addedBlock: Exclude) => void savedPageState: IsomerComponent[] setSavedPageState: Dispatch> previewPageState: IsomerComponent[] @@ -28,6 +31,8 @@ export function EditorDrawerProvider({ children }: PropsWithChildren) { const [previewPageState, setPreviewPageState] = useState( [], ) + const [addedBlock, setAddedBlock] = + useState>("button") return ( {children} diff --git a/apps/studio/src/features/editing-experience/components/ComplexEditorStateDrawer.tsx b/apps/studio/src/features/editing-experience/components/ComplexEditorStateDrawer.tsx index baad682887..749c5161a2 100644 --- a/apps/studio/src/features/editing-experience/components/ComplexEditorStateDrawer.tsx +++ b/apps/studio/src/features/editing-experience/components/ComplexEditorStateDrawer.tsx @@ -20,7 +20,7 @@ export default function ComplexEditorStateDrawer(): JSX.Element { return <> } - const component = savedPageState[currActiveIdx] + const component = previewPageState[currActiveIdx] if (!component) { return <> diff --git a/apps/studio/src/features/editing-experience/components/RootStateDrawer.tsx b/apps/studio/src/features/editing-experience/components/RootStateDrawer.tsx index 16ba7cacf2..4c66f189df 100644 --- a/apps/studio/src/features/editing-experience/components/RootStateDrawer.tsx +++ b/apps/studio/src/features/editing-experience/components/RootStateDrawer.tsx @@ -129,7 +129,11 @@ export default function RootStateDrawer() { ml="0.75rem" /> - {getComponentSchema(block.type).title} + {/* NOTE: Because we use `Type.Ref` for prose, */} + {/* this gets a `$Ref` only and not the concrete values */} + {block.type === "prose" + ? "Prose component" + : getComponentSchema(block.type).title} diff --git a/apps/studio/src/features/editing-experience/components/TipTapComponent.tsx b/apps/studio/src/features/editing-experience/components/TipTapComponent.tsx index 5a0b424810..abc8ae0c09 100644 --- a/apps/studio/src/features/editing-experience/components/TipTapComponent.tsx +++ b/apps/studio/src/features/editing-experience/components/TipTapComponent.tsx @@ -2,35 +2,11 @@ import type { ProseProps } from "@opengovsg/isomer-components/dist/cjs/interface import type { JSONContent } from "@tiptap/react" import { Box, Text as ChakraText, Flex, Icon, VStack } from "@chakra-ui/react" import { Button, IconButton } from "@opengovsg/design-system-react" -import { Blockquote } from "@tiptap/extension-blockquote" -import { Bold } from "@tiptap/extension-bold" -import { BulletList } from "@tiptap/extension-bullet-list" -import { Document } from "@tiptap/extension-document" -import { Dropcursor } from "@tiptap/extension-dropcursor" -import { Gapcursor } from "@tiptap/extension-gapcursor" -import { HardBreak } from "@tiptap/extension-hard-break" -import { Heading } from "@tiptap/extension-heading" -import { History } from "@tiptap/extension-history" -import { HorizontalRule } from "@tiptap/extension-horizontal-rule" -import { Italic } from "@tiptap/extension-italic" -import { ListItem } from "@tiptap/extension-list-item" -import { OrderedList } from "@tiptap/extension-ordered-list" -import { Paragraph } from "@tiptap/extension-paragraph" -import { Strike } from "@tiptap/extension-strike" -import { Subscript } from "@tiptap/extension-subscript" -import { Superscript } from "@tiptap/extension-superscript" -import TableCell from "@tiptap/extension-table-cell" -import TableHeader from "@tiptap/extension-table-header" -import TableRow from "@tiptap/extension-table-row" -import { Text } from "@tiptap/extension-text" -import Underline from "@tiptap/extension-underline" -import { EditorContent, useEditor } from "@tiptap/react" import { cloneDeep } from "lodash" import { BiText, BiX } from "react-icons/bi" -import { MenuBar } from "~/components/PageEditor/MenuBar" import { useEditorDrawerContext } from "~/contexts/EditorDrawerContext" -import { Table } from "./extensions/Table" +import { TiptapEditor } from "./form-builder/renderers/TipTapEditor" interface TipTapComponentProps { content: ProseProps @@ -58,61 +34,7 @@ function TipTapComponent({ content }: TipTapComponentProps) { }) } - const editor = useEditor({ - extensions: [ - Blockquote, - Bold, - BulletList.extend({ - name: "unorderedList", - }).configure({ - HTMLAttributes: { - class: "list-disc", - }, - }), - Document.extend({ - name: "prose", - }), - Dropcursor, - Gapcursor, - HardBreak, - Heading.configure({ - levels: [2, 3, 4, 6], - }), - History, - HorizontalRule.extend({ - name: "divider", - }), - Italic, - ListItem, - OrderedList.extend({ - name: "orderedList", - }).configure({ - HTMLAttributes: { - class: "list-decimal", - }, - }), - Paragraph, - Strike, - Superscript, - Subscript, - Table.configure({ - resizable: false, - }), - TableRow, - TableHeader, - TableCell, - Text, - Underline, - ], - content, - onUpdate: (e) => { - const jsonContent = e.editor.getJSON() - updatePageState(jsonContent) - }, - }) - // TODO: Add a loading state or use suspsense - if (!editor) return null return ( - - - - editor.chain().focus().run()} - cursor="text" - /> - + + } - const component = savedPageState[currActiveIdx] + const component = previewPageState[currActiveIdx] if (!component) { return <> diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/TipTapEditor.tsx b/apps/studio/src/features/editing-experience/components/form-builder/renderers/TipTapEditor.tsx new file mode 100644 index 0000000000..be66cd5363 --- /dev/null +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/TipTapEditor.tsx @@ -0,0 +1,110 @@ +import type { BoxProps } from "@chakra-ui/react" +import type { ControlProps } from "@jsonforms/core" +import type { JSONContent } from "@tiptap/react" +import { Box, VStack } from "@chakra-ui/react" +import { Blockquote } from "@tiptap/extension-blockquote" +import { Bold } from "@tiptap/extension-bold" +import { BulletList } from "@tiptap/extension-bullet-list" +import { Document } from "@tiptap/extension-document" +import { Dropcursor } from "@tiptap/extension-dropcursor" +import { Gapcursor } from "@tiptap/extension-gapcursor" +import { HardBreak } from "@tiptap/extension-hard-break" +import { Heading } from "@tiptap/extension-heading" +import { History } from "@tiptap/extension-history" +import { HorizontalRule } from "@tiptap/extension-horizontal-rule" +import { Italic } from "@tiptap/extension-italic" +import { ListItem } from "@tiptap/extension-list-item" +import { OrderedList } from "@tiptap/extension-ordered-list" +import { Paragraph } from "@tiptap/extension-paragraph" +import { Strike } from "@tiptap/extension-strike" +import { Subscript } from "@tiptap/extension-subscript" +import { Superscript } from "@tiptap/extension-superscript" +import { Text } from "@tiptap/extension-text" +import Underline from "@tiptap/extension-underline" +import { EditorContent, useEditor } from "@tiptap/react" + +import { MenuBar } from "~/components/PageEditor/MenuBar" + +interface TiptapEditorProps extends BoxProps { + data: ControlProps["data"] + handleChange: (content: JSONContent) => void +} + +export function TiptapEditor({ data, handleChange }: TiptapEditorProps) { + const editor = useEditor({ + extensions: [ + Blockquote, + Bold, + BulletList.extend({ + name: "unorderedList", + }).configure({ + HTMLAttributes: { + class: "list-disc", + }, + }), + Document.extend({ + name: "prose", + }), + Dropcursor, + Gapcursor, + HardBreak, + Heading.configure({ + levels: [2, 3, 4, 6], + }), + History, + HorizontalRule.extend({ + name: "divider", + }), + Italic, + ListItem, + OrderedList.extend({ + name: "orderedList", + }).configure({ + HTMLAttributes: { + class: "list-decimal", + }, + }), + Paragraph, + Strike, + Superscript, + Subscript, + Text, + Underline, + ], + content: data, + onUpdate: (e) => { + const jsonContent = e.editor.getJSON() + handleChange(jsonContent) + }, + }) + + // TODO: Add a loading state or use suspsense + if (!editor) return null + + return ( + + + + editor.chain().focus().run()} + cursor="text" + /> + + + ) +} diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsProseControl.tsx b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsProseControl.tsx index 10981172cf..e2d39386a7 100644 --- a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsProseControl.tsx +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsProseControl.tsx @@ -1,10 +1,11 @@ import type { ControlProps, RankedTester } from "@jsonforms/core" -import { Box, FormControl } from "@chakra-ui/react" +import { Flex, FormControl } from "@chakra-ui/react" import { rankWith, schemaMatches } from "@jsonforms/core" import { withJsonFormsControlProps } from "@jsonforms/react" -import { FormLabel, Textarea } from "@opengovsg/design-system-react" +import { FormLabel } from "@opengovsg/design-system-react" import { JSON_FORMS_RANKING } from "~/constants/formBuilder" +import { TiptapEditor } from "../TipTapEditor" export const jsonFormsProseControlTester: RankedTester = rankWith( JSON_FORMS_RANKING.ProseControl, @@ -13,7 +14,6 @@ export const jsonFormsProseControlTester: RankedTester = rankWith( }), ) -// TODO: Replace this with the Tiptap editor export function JsonFormsProseControl({ data, label, @@ -23,16 +23,15 @@ export function JsonFormsProseControl({ required, }: ControlProps) { return ( - + {label} -