From a8274e03f4b9dc24ab369f089aef43110c5af09f Mon Sep 17 00:00:00 2001 From: Hsu Zhong Jun <27919917+dcshzj@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:53:11 +0800 Subject: [PATCH 1/8] fix(media): bring the user back to the select media modal screen after upload (#1684) --- .../MediaCreationModal/MediaCreationModal.tsx | 8 ++++++-- src/components/media/MediaModal.jsx | 18 ++---------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/components/MediaCreationModal/MediaCreationModal.tsx b/src/components/MediaCreationModal/MediaCreationModal.tsx index cd7ef8a1a..a5e0c7fba 100644 --- a/src/components/MediaCreationModal/MediaCreationModal.tsx +++ b/src/components/MediaCreationModal/MediaCreationModal.tsx @@ -159,13 +159,14 @@ interface MediaUploadSuccessDropzoneProps { numMedia: number errorMessages: string[] mediaType: MediaFolderTypes + onProceed: () => void } const MediaUploadSuccessDropzone = ({ numMedia, errorMessages, mediaType, + onProceed, }: MediaUploadSuccessDropzoneProps) => { - const { onClose } = useModalContext() const { singularMediaLabel, pluralMediaLabel, @@ -220,7 +221,7 @@ const MediaUploadSuccessDropzone = ({ )} - + ) @@ -281,6 +282,7 @@ const MediaUploadFailedDropzone = ({ interface MediaCreationModalProps { onClose: () => void + onProceed: () => void variant: MediaFolderTypes } @@ -295,6 +297,7 @@ interface MediaCreationRouteParams export const MediaCreationModal = ({ onClose, + onProceed, variant, }: MediaCreationModalProps) => { const { onClose: onModalClose } = useDisclosure() @@ -368,6 +371,7 @@ export const MediaCreationModal = ({ numMedia={numCreated} errorMessages={errorMessages} mediaType={variant} + onProceed={onProceed} /> )} {curStep === "failed" && ( diff --git a/src/components/media/MediaModal.jsx b/src/components/media/MediaModal.jsx index 71efeccf6..a922837dd 100644 --- a/src/components/media/MediaModal.jsx +++ b/src/components/media/MediaModal.jsx @@ -9,8 +9,6 @@ import { MediaAltText } from "components/media/MediaAltText" import MediasSelectModal from "components/media/MediasSelectModal" import { MediaCreationModal } from "components/MediaCreationModal/MediaCreationModal" -import { useCreateMediaHook } from "hooks/mediaHooks/useCreateMediaHook" - import { getMediaDirectoryName } from "utils" const MediaModal = ({ onClose, onProceed, type, showAltTextModal = false }) => { @@ -36,8 +34,6 @@ const MediaModal = ({ onClose, onProceed, type, showAltTextModal = false }) => { mediaDirectoryName: type, }) - const { mutateAsync: createHandler } = useCreateMediaHook(queryParams) - const retrieveMediaDirectoryParams = () => `/${getMediaDirectoryName(queryParams.mediaDirectoryName, { joinOn: "/", @@ -64,18 +60,8 @@ const MediaModal = ({ onClose, onProceed, type, showAltTextModal = false }) => { { - await createHandler({ data }) - onMediaSelect({ ...data, mediaUrl: data.content }) - if (showAltTextModal) setMediaMode("details") - else - onProceed({ - selectedMediaPath: `${retrieveMediaDirectoryParams()}/${ - data.name - }`, - }) - }} - onClose={onClose} + onProceed={() => setMediaMode("select")} + onClose={() => setMediaMode("select")} /> ) } From 860c32a9a50bb1f8a315a82fae0028ce7b472a00 Mon Sep 17 00:00:00 2001 From: Hsu Zhong Jun <27919917+dcshzj@users.noreply.github.com> Date: Tue, 21 Nov 2023 13:56:36 +0800 Subject: [PATCH 2/8] feat(tiptap): update icon options and use boxicons (#1683) * feat(assets): add icon assets for table actions * feat(tiptap): update icon options and use boxicons * style(editor): adjust text styling and spacing based on design feedback * style(tiptap): update tooltip copies for some options --- package-lock.json | 13 + package.json | 1 + src/assets/icons/BxAddColLeft.tsx | 28 ++ src/assets/icons/BxAddColRight.tsx | 28 ++ src/assets/icons/BxAddRowAbove.tsx | 28 ++ src/assets/icons/BxAddRowBelow.tsx | 28 ++ src/assets/icons/BxDelCol.tsx | 18 + src/assets/icons/BxDelRow.tsx | 18 + src/assets/icons/index.ts | 6 + src/layouts/EditPage/EditPage.tsx | 2 + .../components/Editor/components/MenuBar.tsx | 358 +++++++++++++++--- .../components/Editor/components/MenuItem.tsx | 10 +- .../Editor/components/TableBubbleMenu.tsx | 21 +- 13 files changed, 486 insertions(+), 73 deletions(-) create mode 100644 src/assets/icons/BxAddColLeft.tsx create mode 100644 src/assets/icons/BxAddColRight.tsx create mode 100644 src/assets/icons/BxAddRowAbove.tsx create mode 100644 src/assets/icons/BxAddRowBelow.tsx create mode 100644 src/assets/icons/BxDelCol.tsx create mode 100644 src/assets/icons/BxDelRow.tsx diff --git a/package-lock.json b/package-lock.json index a28ded033..00c0845ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "@tiptap/extension-table-row": "^2.1.12", "@tiptap/extension-task-item": "^2.1.12", "@tiptap/extension-task-list": "^2.1.12", + "@tiptap/extension-underline": "^2.1.12", "@tiptap/pm": "^2.1.12", "@tiptap/react": "^2.1.12", "@tiptap/starter-kit": "^2.1.12", @@ -11079,6 +11080,18 @@ "@tiptap/core": "^2.0.0" } }, + "node_modules/@tiptap/extension-underline": { + "version": "2.1.12", + "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.1.12.tgz", + "integrity": "sha512-NwwdhFT8gDD0VUNLQx85yFBhP9a8qg8GPuxlGzAP/lPTV8Ubh3vSeQ5N9k2ZF/vHlEvnugzeVCbmYn7wf8vn1g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, "node_modules/@tiptap/pm": { "version": "2.1.12", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.1.12.tgz", diff --git a/package.json b/package.json index e37f58c10..7a0a9542e 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@tiptap/extension-table-row": "^2.1.12", "@tiptap/extension-task-item": "^2.1.12", "@tiptap/extension-task-list": "^2.1.12", + "@tiptap/extension-underline": "^2.1.12", "@tiptap/pm": "^2.1.12", "@tiptap/react": "^2.1.12", "@tiptap/starter-kit": "^2.1.12", diff --git a/src/assets/icons/BxAddColLeft.tsx b/src/assets/icons/BxAddColLeft.tsx new file mode 100644 index 000000000..c6b776838 --- /dev/null +++ b/src/assets/icons/BxAddColLeft.tsx @@ -0,0 +1,28 @@ +export const BxAddColLeft = ( + props: React.SVGProps +): JSX.Element => { + return ( + + + + + + + ) +} diff --git a/src/assets/icons/BxAddColRight.tsx b/src/assets/icons/BxAddColRight.tsx new file mode 100644 index 000000000..dd7ae4e36 --- /dev/null +++ b/src/assets/icons/BxAddColRight.tsx @@ -0,0 +1,28 @@ +export const BxAddColRight = ( + props: React.SVGProps +): JSX.Element => { + return ( + + + + + + + ) +} diff --git a/src/assets/icons/BxAddRowAbove.tsx b/src/assets/icons/BxAddRowAbove.tsx new file mode 100644 index 000000000..60b094294 --- /dev/null +++ b/src/assets/icons/BxAddRowAbove.tsx @@ -0,0 +1,28 @@ +export const BxAddRowAbove = ( + props: React.SVGProps +): JSX.Element => { + return ( + + + + + + + ) +} diff --git a/src/assets/icons/BxAddRowBelow.tsx b/src/assets/icons/BxAddRowBelow.tsx new file mode 100644 index 000000000..49bf6c608 --- /dev/null +++ b/src/assets/icons/BxAddRowBelow.tsx @@ -0,0 +1,28 @@ +export const BxAddRowBelow = ( + props: React.SVGProps +): JSX.Element => { + return ( + + + + + + + ) +} diff --git a/src/assets/icons/BxDelCol.tsx b/src/assets/icons/BxDelCol.tsx new file mode 100644 index 000000000..65a22edfc --- /dev/null +++ b/src/assets/icons/BxDelCol.tsx @@ -0,0 +1,18 @@ +export const BxDelCol = (props: React.SVGProps): JSX.Element => { + return ( + + + + + ) +} diff --git a/src/assets/icons/BxDelRow.tsx b/src/assets/icons/BxDelRow.tsx new file mode 100644 index 000000000..237df10ba --- /dev/null +++ b/src/assets/icons/BxDelRow.tsx @@ -0,0 +1,18 @@ +export const BxDelRow = (props: React.SVGProps): JSX.Element => { + return ( + + + + + ) +} diff --git a/src/assets/icons/index.ts b/src/assets/icons/index.ts index 3d2822fbb..f52fd3631 100644 --- a/src/assets/icons/index.ts +++ b/src/assets/icons/index.ts @@ -19,3 +19,9 @@ export * from "./BxDraggable" export * from "./BxDraggableVertical" export * from "./BxGrayTranslucent" export * from "./BxErrorSolid" +export * from "./BxAddColLeft" +export * from "./BxAddColRight" +export * from "./BxAddRowAbove" +export * from "./BxAddRowBelow" +export * from "./BxDelCol" +export * from "./BxDelRow" diff --git a/src/layouts/EditPage/EditPage.tsx b/src/layouts/EditPage/EditPage.tsx index 9873c30a8..436481956 100644 --- a/src/layouts/EditPage/EditPage.tsx +++ b/src/layouts/EditPage/EditPage.tsx @@ -11,6 +11,7 @@ import TableHeader from "@tiptap/extension-table-header" import TableRow from "@tiptap/extension-table-row" import TaskItem from "@tiptap/extension-task-item" import TaskList from "@tiptap/extension-task-list" +import Underline from "@tiptap/extension-underline" import { getHTMLFromFragment, useEditor } from "@tiptap/react" import StarterKit from "@tiptap/starter-kit" import { Context, useContext, useEffect, useState } from "react" @@ -81,6 +82,7 @@ export const EditPage = () => { TableRow, TableHeader, TableCell, + Underline, Placeholder, ], autofocus: "start", diff --git a/src/layouts/components/Editor/components/MenuBar.tsx b/src/layouts/components/Editor/components/MenuBar.tsx index e67fa34e7..966c16707 100644 --- a/src/layouts/components/Editor/components/MenuBar.tsx +++ b/src/layouts/components/Editor/components/MenuBar.tsx @@ -1,102 +1,231 @@ -import { Divider, HStack } from "@chakra-ui/react" +import { + Divider, + HStack, + Icon, + MenuButtonProps, + MenuListProps, + Popover, + PopoverBody, + PopoverContent, + PopoverTrigger, + Text, +} from "@chakra-ui/react" +import { Button, Menu } from "@opengovsg/design-system-react" import { Editor } from "@tiptap/react" +import { + BiBold, + BiChevronDown, + BiChevronUp, + BiCodeAlt, + BiFile, + BiImageAdd, + BiItalic, + BiLink, + BiListOl, + BiListUl, + BiMinus, + BiRedo, + BiStrikethrough, + BiTable, + BiUnderline, + BiUndo, +} from "react-icons/bi" +import { IconType } from "react-icons/lib" import { useEditorModal } from "contexts/EditorModalContext" import { MenuItem } from "./MenuItem" +interface MenuBarItem { + type: "item" + title: string + icon?: IconType + textStyle?: string + useSecondaryColor?: boolean + leftItem?: JSX.Element + action: () => void + isActive?: () => boolean +} + +interface MenuBarDivider { + type: "divider" +} + +interface MenuBarVeritcalList { + type: "vertical-list" + buttonWidth: MenuButtonProps["width"] + menuWidth: MenuListProps["width"] + defaultTitle: string + items: MenuBarItem[] +} + +interface MenuBarHorizontalList { + type: "horizontal-list" + label: string + defaultIcon: IconType + items: MenuBarItem[] +} + +type MenuBarEntry = + | MenuBarDivider + | MenuBarVeritcalList + | MenuBarHorizontalList + | MenuBarItem + export const MenuBar = ({ editor }: { editor: Editor }) => { const { showModal } = useEditorModal() - const items = [ - { - icon: "h-1", - title: "Heading 1", - action: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), - isActive: () => editor.isActive("heading", { level: 1 }), - }, - { - icon: "h-2", - title: "Heading 2", - action: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), - isActive: () => editor.isActive("heading", { level: 2 }), - }, + const items: MenuBarEntry[] = [ { - icon: "h-3", - title: "Heading 3", - action: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), - isActive: () => editor.isActive("heading", { level: 3 }), + type: "vertical-list", + buttonWidth: "9rem", + menuWidth: "19rem", + defaultTitle: "Heading 1", + items: [ + { + type: "item", + title: "Title", + textStyle: "h1", + useSecondaryColor: true, + action: () => + editor.chain().focus().toggleHeading({ level: 1 }).run(), + isActive: () => editor.isActive("heading", { level: 1 }), + }, + { + type: "item", + title: "Heading 1", + textStyle: "h2", + useSecondaryColor: true, + action: () => + editor.chain().focus().toggleHeading({ level: 2 }).run(), + isActive: () => editor.isActive("heading", { level: 2 }), + }, + { + type: "item", + title: "Heading 2", + textStyle: "h3", + useSecondaryColor: true, + action: () => + editor.chain().focus().toggleHeading({ level: 3 }).run(), + isActive: () => editor.isActive("heading", { level: 3 }), + }, + { + type: "item", + title: "Heading 3", + textStyle: "h4", + useSecondaryColor: true, + action: () => + editor.chain().focus().toggleHeading({ level: 4 }).run(), + isActive: () => editor.isActive("heading", { level: 4 }), + }, + { + type: "item", + title: "Quote block", + textStyle: "body-1", + useSecondaryColor: true, + leftItem: ( + + ), + action: () => editor.chain().focus().toggleBlockquote().run(), + isActive: () => editor.isActive("blockquote"), + }, + { + type: "item", + title: "Paragraph", + textStyle: "body-1", + action: () => + editor.chain().focus().clearNodes().unsetAllMarks().run(), + isActive: () => editor.isActive("paragraph"), + }, + ], }, { type: "divider", }, { - icon: "bold", + type: "item", + icon: BiBold, title: "Bold", action: () => editor.chain().focus().toggleBold().run(), isActive: () => editor.isActive("bold"), }, { - icon: "italic", - title: "Italic", + type: "item", + icon: BiItalic, + title: "Italicise", action: () => editor.chain().focus().toggleItalic().run(), isActive: () => editor.isActive("italic"), }, { - icon: "strikethrough", - title: "Strike", - action: () => editor.chain().focus().toggleStrike().run(), - isActive: () => editor.isActive("strike"), + type: "item", + icon: BiUnderline, + title: "Underline", + action: () => editor.chain().focus().toggleUnderline().run(), + isActive: () => editor.isActive("underline"), }, { - icon: "double-quotes-l", - title: "Blockquote", - action: () => editor.chain().focus().toggleBlockquote().run(), - isActive: () => editor.isActive("blockquote"), + type: "item", + icon: BiStrikethrough, + title: "Strikethrough", + action: () => editor.chain().focus().toggleStrike().run(), + isActive: () => editor.isActive("strike"), }, { type: "divider", }, { - icon: "list-ordered", - title: "Ordered List", - action: () => editor.chain().focus().toggleOrderedList().run(), - isActive: () => editor.isActive("orderedList"), - }, + type: "horizontal-list", + label: "Lists", + defaultIcon: BiListOl, + items: [ + { + type: "item", + icon: BiListOl, + title: "Ordered list", + action: () => editor.chain().focus().toggleOrderedList().run(), + isActive: () => editor.isActive("orderedList"), + }, - { - icon: "list-unordered", - title: "Bullet List", - action: () => editor.chain().focus().toggleBulletList().run(), - isActive: () => editor.isActive("bulletList"), + { + type: "item", + icon: BiListUl, + title: "Bullet list", + action: () => editor.chain().focus().toggleBulletList().run(), + isActive: () => editor.isActive("bulletList"), + }, + ], }, - { type: "divider", }, { - icon: "file-image-line", + type: "item", + icon: BiLink, + title: "Add link", + action: () => showModal("hyperlink"), + }, + { + type: "item", + icon: BiImageAdd, title: "Add image", action: () => showModal("images"), }, { - icon: "file-pdf-line", + type: "item", + icon: BiFile, title: "Add file", action: () => showModal("files"), }, { - icon: "links-line", - title: "Add link", - action: () => showModal("hyperlink"), - }, - { - icon: "link-unlink", - title: "Remove link", - action: () => editor.chain().focus().unsetLink().run(), - }, - { - icon: "code-s-slash-line", + type: "item", + icon: BiCodeAlt, title: "Insert embed", action: () => showModal("embed"), }, @@ -104,7 +233,8 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { type: "divider", }, { - icon: "table-line", + type: "item", + icon: BiTable, title: "Add table", action: () => editor @@ -115,20 +245,23 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { .run(), }, { - icon: "separator", - title: "Horizontal Rule", + type: "item", + icon: BiMinus, + title: "Divider", action: () => editor.chain().focus().setHorizontalRule().run(), }, { type: "divider", }, { - icon: "arrow-go-back-line", + type: "item", + icon: BiUndo, title: "Undo", action: () => editor.chain().focus().undo().run(), }, { - icon: "arrow-go-forward-line", + type: "item", + icon: BiRedo, title: "Redo", action: () => editor.chain().focus().redo().run(), }, @@ -143,19 +276,122 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { borderBottom="1px solid" borderColor="base.divider.strong" borderTopRadius="0.25rem" + spacing="0.125rem" > {items.map((item) => ( <> - {item.type === "divider" ? ( + {item.type === "divider" && ( - ) : ( - )} + + {item.type === "vertical-list" && ( + + {({ isOpen }) => { + const activeItem = item.items.find( + (subItem) => subItem.isActive && subItem.isActive() + ) + + return ( + <> + + {activeItem?.title || item.defaultTitle} + + + + {item.items.map((subItem) => ( + + {subItem.leftItem} + {subItem.title && !subItem.icon && ( + + {subItem.title} + + )} + {subItem.icon && ( + + )} + + ))} + + + ) + }} + + )} + + {item.type === "horizontal-list" && ( + + {({ isOpen }) => ( + <> + + + + + + + + + {item.items.map((subItem) => ( + + ))} + + + + + )} + + )} + + {item.type === "item" && } ))} diff --git a/src/layouts/components/Editor/components/MenuItem.tsx b/src/layouts/components/Editor/components/MenuItem.tsx index ed44f377b..47adf7bb0 100644 --- a/src/layouts/components/Editor/components/MenuItem.tsx +++ b/src/layouts/components/Editor/components/MenuItem.tsx @@ -1,10 +1,10 @@ -import { Tooltip } from "@chakra-ui/react" +import { Icon, Tooltip } from "@chakra-ui/react" import { IconButton } from "@opengovsg/design-system-react" import { MouseEventHandler } from "react" -import remixiconUrl from "remixicon/fonts/remixicon.symbol.svg" +import { IconType } from "react-icons/lib" interface MenuItemProps { - icon?: string + icon?: IconType title?: string action?: MouseEventHandler isActive?: null | (() => boolean) @@ -32,9 +32,7 @@ export const MenuItem = ({ aria-label={title || "divider"} isRound={isRound} > - - - + ) diff --git a/src/layouts/components/Editor/components/TableBubbleMenu.tsx b/src/layouts/components/Editor/components/TableBubbleMenu.tsx index 66452e69b..ea2d33fc1 100644 --- a/src/layouts/components/Editor/components/TableBubbleMenu.tsx +++ b/src/layouts/components/Editor/components/TableBubbleMenu.tsx @@ -3,6 +3,15 @@ import { BubbleMenu } from "@tiptap/react" import { useEditorContext } from "contexts/EditorContext" +import { + BxAddColLeft, + BxAddColRight, + BxAddRowAbove, + BxAddRowBelow, + BxDelCol, + BxDelRow, +} from "assets" + import { MenuItem } from "./MenuItem" export const TableBubbleMenu = () => { @@ -24,37 +33,37 @@ export const TableBubbleMenu = () => { }} > editor.chain().focus().addColumnBefore().run()} title="Add column before" isRound /> editor.chain().focus().addColumnAfter().run()} title="Add column after" isRound /> editor.chain().focus().deleteColumn().run()} title="Delete column" isRound /> editor.chain().focus().addRowBefore().run()} title="Add row before" isRound /> editor.chain().focus().addRowAfter().run()} title="Add row after" isRound /> editor.chain().focus().deleteRow().run()} title="Delete row" isRound From 5943fea2445fc4e58df7f5276ed7c30efdf7224f Mon Sep 17 00:00:00 2001 From: Hsu Zhong Jun <27919917+dcshzj@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:09:18 +0800 Subject: [PATCH 3/8] feat(rte): use proper node for FormSG embed (#1682) * feat(rte): use proper node for FormSG embed * fix(rte): adjust imports * chore(rte): remove unused import * chore(tiptap): extract priority to constant --- .../EditorEmbedModal/EditorEmbedModal.tsx | 4 +- src/constants/tiptap.ts | 1 + src/layouts/EditPage/EditPage.tsx | 10 +- .../Editor/extensions/FormSG/FormSG.ts | 123 ++++++++++++++++++ .../Editor/extensions/FormSG/FormSGView.tsx | 36 +++++ .../Editor/extensions/FormSG/index.ts | 2 + .../Editor/extensions/Iframe/Iframe.ts | 2 +- .../components/Editor/extensions/index.ts | 1 + 8 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 src/constants/tiptap.ts create mode 100644 src/layouts/components/Editor/extensions/FormSG/FormSG.ts create mode 100644 src/layouts/components/Editor/extensions/FormSG/FormSGView.tsx create mode 100644 src/layouts/components/Editor/extensions/FormSG/index.ts diff --git a/src/components/EditorEmbedModal/EditorEmbedModal.tsx b/src/components/EditorEmbedModal/EditorEmbedModal.tsx index fcfe809ea..9ea0b78ce 100644 --- a/src/components/EditorEmbedModal/EditorEmbedModal.tsx +++ b/src/components/EditorEmbedModal/EditorEmbedModal.tsx @@ -54,9 +54,6 @@ export const EditorEmbedModal = ({ url.href = postUrl || "" const code = `` onProceed({ value: code }) - } else if ($('iframe[src^="https://form.gov.sg"]').length > 0) { - // We only want to keep the