From 079352d6510d4c9f40bf715ccaa3f8401ccccac6 Mon Sep 17 00:00:00 2001 From: seaerchin <44049504+seaerchin@users.noreply.github.com> Date: Thu, 16 Nov 2023 10:23:14 +0800 Subject: [PATCH 1/6] fix(editor): refactor to use `tooltip` (#1669) * refactor(tiptapeditpage): update width * refactor(menuitem): change to using tooltip and set delay to 500ms \ * fix(menuitem): update tooltip to ahve arrow --- src/layouts/EditPage/TiptapEditPage.tsx | 3 +- .../components/Editor/components/MenuBar.tsx | 2 - .../components/Editor/components/MenuItem.tsx | 37 ++++++++++--------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/layouts/EditPage/TiptapEditPage.tsx b/src/layouts/EditPage/TiptapEditPage.tsx index 0b0e95fa7..fd3387307 100644 --- a/src/layouts/EditPage/TiptapEditPage.tsx +++ b/src/layouts/EditPage/TiptapEditPage.tsx @@ -61,8 +61,9 @@ export const TiptapEditPage = ({ {/* Preview */} diff --git a/src/layouts/components/Editor/components/MenuBar.tsx b/src/layouts/components/Editor/components/MenuBar.tsx index 26cfaa895..c856a2126 100644 --- a/src/layouts/components/Editor/components/MenuBar.tsx +++ b/src/layouts/components/Editor/components/MenuBar.tsx @@ -1,7 +1,5 @@ import { Divider, HStack } from "@chakra-ui/react" import { Editor } from "@tiptap/react" -import { Fragment } from "react" -import { v4 as uuid } from "uuid" import { useEditorModal } from "contexts/EditorModalContext" diff --git a/src/layouts/components/Editor/components/MenuItem.tsx b/src/layouts/components/Editor/components/MenuItem.tsx index 82d66d0d7..ed44f377b 100644 --- a/src/layouts/components/Editor/components/MenuItem.tsx +++ b/src/layouts/components/Editor/components/MenuItem.tsx @@ -1,3 +1,4 @@ +import { Tooltip } from "@chakra-ui/react" import { IconButton } from "@opengovsg/design-system-react" import { MouseEventHandler } from "react" import remixiconUrl from "remixicon/fonts/remixicon.symbol.svg" @@ -17,21 +18,23 @@ export const MenuItem = ({ isRound, isActive = null, }: MenuItemProps) => ( - - - - - + // NOTE: Delay opening by 500ms + + + + + + + ) From 6a00900d89d71b56261564a75657dcf6f4508311 Mon Sep 17 00:00:00 2001 From: Kishore <42832651+kishore03109@users.noreply.github.com> Date: Thu, 16 Nov 2023 11:32:34 +0800 Subject: [PATCH 2/6] Fix/is724-apply-styles (#1677) * refactor(styles): make it less confusing to end user * refactor(styles): extend preview text styles * temp * fix(styles): add styles * fix(styles): fix editor+preview styles * fix(rte): remove colors * fix(min width): edite shifts size * fix(scss): use specificcity over !importants --------- Co-authored-by: seaerchin --- src/layouts/EditPage/EditPage.tsx | 2 +- src/layouts/components/Editor/Editor.tsx | 2 +- src/layouts/components/Editor/styles.scss | 50 ++++------------------- src/styles/isomer-cms/elements/base.scss | 2 +- 4 files changed, 10 insertions(+), 46 deletions(-) diff --git a/src/layouts/EditPage/EditPage.tsx b/src/layouts/EditPage/EditPage.tsx index 4cf3a3827..f29774100 100644 --- a/src/layouts/EditPage/EditPage.tsx +++ b/src/layouts/EditPage/EditPage.tsx @@ -70,7 +70,7 @@ export const EditPage = () => { pluginKey: "tableBubble", }), Table.configure({ - resizable: true, + resizable: false, }), TableRow, TableHeader, diff --git a/src/layouts/components/Editor/Editor.tsx b/src/layouts/components/Editor/Editor.tsx index 07b68ac95..e0a46a9e5 100644 --- a/src/layouts/components/Editor/Editor.tsx +++ b/src/layouts/components/Editor/Editor.tsx @@ -11,7 +11,7 @@ export const Editor = (props: BoxProps) => { const { editor } = useEditorContext() return ( - + * + * { margin-top: 0.75em; } @@ -37,10 +36,10 @@ td, th { - border: 2px solid #ced4da; + border: 1px solid #d6d6d6; box-sizing: border-box; min-width: 1em; - padding: 3px 5px; + padding: 0.5em 0.75em; position: relative; vertical-align: top; @@ -50,36 +49,10 @@ } th { - background-color: #f1f3f5; + color: #323232; font-weight: bold; text-align: left; } - - .selectedCell:after { - background: rgba(200, 200, 255, 0.4); - content: ""; - left: 0; - right: 0; - top: 0; - bottom: 0; - pointer-events: none; - position: absolute; - z-index: 2; - } - - .column-resize-handle { - background-color: #adf; - bottom: -2px; - position: absolute; - right: -2px; - pointer-events: none; - top: 0; - width: 4px; - } - - p { - margin: 0; - } } a { @@ -92,15 +65,6 @@ padding: 0 1rem; } - h1, - h2, - h3, - h4, - h5, - h6 { - line-height: 1.1; - } - code { background-color: rgba(#616161, 0.1); color: #616161; diff --git a/src/styles/isomer-cms/elements/base.scss b/src/styles/isomer-cms/elements/base.scss index 4ecd249dc..d8dc44456 100644 --- a/src/styles/isomer-cms/elements/base.scss +++ b/src/styles/isomer-cms/elements/base.scss @@ -60,7 +60,7 @@ h4, h5, h6 { font-family: "Inter", sans-serif; - font-weight: 700 !important; + font-weight: 700; margin: 0; padding: 0; letter-spacing: 0 !important; From 921aaa2464174171a6ec1a65fe2056dfe2ade0df Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Thu, 16 Nov 2023 16:48:29 +0800 Subject: [PATCH 3/6] Fix/is 689 reuse image/album tiles (#1678) * chore: add conditional checkbox for images * fix: add conditional render for item without checkbox and add highlight * chore: update mediaDirectoryCard to make menu optional * chore: update select media modal to use new components * chore: use media labels * fix: onClick behaviour when navigating to a different folder --- .../ImagePreviewCard/ImagePreviewCard.tsx | 46 ++++----- src/components/media/MediaModal.jsx | 1 + src/components/media/MediasSelectModal.jsx | 99 +++++++++++-------- .../Media/components/FilePreviewCard.tsx | 66 +++++++------ .../Media/components/MediaDirectoryCard.tsx | 90 +++++++++++------ 5 files changed, 180 insertions(+), 122 deletions(-) diff --git a/src/components/ImagePreviewCard/ImagePreviewCard.tsx b/src/components/ImagePreviewCard/ImagePreviewCard.tsx index bb8774356..b07e74d59 100644 --- a/src/components/ImagePreviewCard/ImagePreviewCard.tsx +++ b/src/components/ImagePreviewCard/ImagePreviewCard.tsx @@ -60,29 +60,29 @@ export const ImagePreviewCard = ({ return ( {/* Checkbox overlay over image */} - { - if (onCheck) onCheck() - }} - /> + {onCheck && ( + + )} { } onMediaSelect={onMediaSelect} onClose={onClose} + mediaType={type} /> ) } diff --git a/src/components/media/MediasSelectModal.jsx b/src/components/media/MediasSelectModal.jsx index df6b3f2a5..b3d3cf57c 100644 --- a/src/components/media/MediasSelectModal.jsx +++ b/src/components/media/MediasSelectModal.jsx @@ -27,9 +27,8 @@ import { useState, useEffect } from "react" import { useFormContext } from "react-hook-form" import { useRouteMatch } from "react-router-dom" -import { FolderCard } from "components/FolderCard" +import { ImagePreviewCard } from "components/ImagePreviewCard" import { LoadingButton } from "components/LoadingButton" -import MediaCard from "components/media/MediaCard" import { MEDIA_PAGINATION_SIZE } from "constants/media" @@ -38,9 +37,12 @@ import { useListMediaFolderFiles } from "hooks/directoryHooks/useListMediaFolder import { useListMediaFolderSubdirectories } from "hooks/directoryHooks/useListMediaFolderSubdirectories" import { usePaginate } from "hooks/usePaginate" -import contentStyles from "styles/isomer-cms/pages/Content.module.scss" +import { FilePreviewCard, MediaDirectoryCard } from "layouts/Media/components" + import mediaStyles from "styles/isomer-cms/pages/Media.module.scss" +import { getMediaLabels } from "utils/media" + import { deslugifyDirectory, getMediaDirectoryName } from "utils" const filterMediaByFileName = (medias, filterTerm) => @@ -48,6 +50,10 @@ const filterMediaByFileName = (medias, filterTerm) => media.name.toLowerCase().includes(filterTerm.toLowerCase()) ) +const titleCase = (str) => { + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() +} + const MediasSelectModal = ({ onProceed, onClose, @@ -55,9 +61,12 @@ const MediasSelectModal = ({ onUpload, queryParams, setQueryParams, + mediaType, }) => { const { params } = useRouteMatch() - const { siteName, fileName } = params + const { fileName } = params + + const { singularMediaLabel, pluralMediaLabel } = getMediaLabels(mediaType) const { mediaRoom, mediaDirectoryName } = queryParams @@ -138,8 +147,9 @@ const MediasSelectModal = ({ - Choose from your images or upload a new image. You can organise - your images in Workspace > Images. + Choose from your {pluralMediaLabel} or upload a new{" "} + {singularMediaLabel}. You can organise your {pluralMediaLabel} in + Workspace > {titleCase(pluralMediaLabel)}. {/* Search medias */} @@ -156,7 +166,7 @@ const MediasSelectModal = ({ color="interaction.main.default" onClick={onUpload} > - Upload new image + Upload new {singularMediaLabel} @@ -200,28 +210,26 @@ const MediasSelectModal = ({ : "fit-content" } isLoaded={!isListMediaFolderSubdirectoriesLoading} + pb="2.25rem" > - - - {/* Directories */} - {filteredDirectories.map((dir) => ( - - setQueryParams((prevState) => { - return { - ...prevState, - mediaDirectoryName: `${prevState.mediaDirectoryName}%2F${dir.name}`, - } - }) - } - key={dir.path} - hideSettings - /> - ))} - - + + {/* Directories */} + {filteredDirectories.map((dir) => ( + { + onMediaSelect("") + setQueryParams((prevState) => { + return { + ...prevState, + mediaDirectoryName: `${prevState.mediaDirectoryName}%2F${dir.name}`, + } + }) + }} + isMenuNeeded={false} + /> + ))} + filteredMedias.includes(data?.name)) - .map(({ data, isLoading }, mediaItemIndex) => ( + .map(({ data, isLoading }) => ( - onMediaSelect(data)} - isSelected={ - data.name === watch("selectedMedia")?.name - } - showSettings={false} - /> + {mediaType === "images" ? ( + onMediaSelect(data)} + isMenuNeeded={false} + /> + ) : ( + onMediaSelect(data)} + isMenuNeeded={false} + /> + )} ))} diff --git a/src/layouts/Media/components/FilePreviewCard.tsx b/src/layouts/Media/components/FilePreviewCard.tsx index 70ffadb31..c2b655018 100644 --- a/src/layouts/Media/components/FilePreviewCard.tsx +++ b/src/layouts/Media/components/FilePreviewCard.tsx @@ -36,31 +36,40 @@ export const FilePreviewCard = ({ const encodedName = encodeURIComponent(name) return ( - + {/* Checkbox overlay */} - { - if (onCheck) onCheck() - }} - /> + {onCheck && ( + + )} - {!isSelected && ( + {!(isSelected && onCheck) && ( + // Icon is hidden only if checkbox exists )} @@ -94,8 +104,8 @@ export const FilePreviewCard = ({ textStyle="body-1" color="text.label" noOfLines={3} - ml={isSelected ? "2.5rem" : 0} - _groupHover={{ marginLeft: "2.5rem" }} + ml={isSelected && onCheck ? "2.5rem" : 0} + _groupHover={{ marginLeft: `${onCheck && "2.5rem"}` }} > {name} diff --git a/src/layouts/Media/components/MediaDirectoryCard.tsx b/src/layouts/Media/components/MediaDirectoryCard.tsx index 99201c094..8c860fcd1 100644 --- a/src/layouts/Media/components/MediaDirectoryCard.tsx +++ b/src/layouts/Media/components/MediaDirectoryCard.tsx @@ -1,18 +1,24 @@ -import { LinkOverlay, LinkBox, Divider, Text, Icon } from "@chakra-ui/react" -import { BiEdit, BiFolder, BiTrash, BiWrench } from "react-icons/bi" +import { LinkOverlay, LinkBox, Text, Icon } from "@chakra-ui/react" +import { BiEdit, BiFolder, BiTrash } from "react-icons/bi" import { Link as RouterLink, useRouteMatch } from "react-router-dom" import { Card, CardBody } from "components/Card" import { ContextMenu } from "components/ContextMenu" +import { getMediaLabels } from "utils/media" + import { prettifyPageFileName } from "utils" interface MediaDirectoryCardProps { title: string + onClick?: () => void + isMenuNeeded?: boolean } export const MediaDirectoryCard = ({ title, + onClick, + isMenuNeeded = true, }: MediaDirectoryCardProps): JSX.Element => { const { params: { siteName, mediaRoom: mediaType, mediaDirectoryName }, @@ -25,44 +31,64 @@ export const MediaDirectoryCard = ({ const encodedDirectoryPath = `${mediaDirectoryName}%2F${encodeURIComponent( title )}` + const { singularDirectoryLabel } = getMediaLabels(mediaType) return ( - - - - - - - {prettifyPageFileName(title)} - - - - - - - - } + { + if (onClick) { + e.preventDefault() + onClick() + } + }} + > + {onClick ? ( + + + + {prettifyPageFileName(title)} + + + ) : ( + + - Rename {mediaType === "images" ? "album" : "directory"} - - <> + + + + {prettifyPageFileName(title)} + + + + + )} + {isMenuNeeded && ( + + + } + icon={} as={RouterLink} - to={`${url}/deleteDirectory/${encodedDirectoryPath}`} - color="text.danger" + to={`${url}/editDirectorySettings/${encodedDirectoryPath}`} > - Delete {mediaType === "images" ? "album" : "directory"} + Rename {singularDirectoryLabel} - > - - + <> + } + as={RouterLink} + to={`${url}/deleteDirectory/${encodedDirectoryPath}`} + color="text.danger" + > + Delete {singularDirectoryLabel} + + > + + + )} ) } From 145f61bbb1670337e0c1b8ab20057ee9c2ebbf03 Mon Sep 17 00:00:00 2001 From: Kishore <42832651+kishore03109@users.noreply.github.com> Date: Thu, 16 Nov 2023 17:19:54 +0800 Subject: [PATCH 4/6] feat(tiptap): rm redundant logos (#1679) * feat(tiptap): rm redundant logos * fix(rte): rm tasklist --- .../components/Editor/components/MenuBar.tsx | 124 +++++++----------- 1 file changed, 45 insertions(+), 79 deletions(-) diff --git a/src/layouts/components/Editor/components/MenuBar.tsx b/src/layouts/components/Editor/components/MenuBar.tsx index c856a2126..0930b23b2 100644 --- a/src/layouts/components/Editor/components/MenuBar.tsx +++ b/src/layouts/components/Editor/components/MenuBar.tsx @@ -9,39 +9,6 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { const { showModal } = useEditorModal() const items = [ - { - icon: "bold", - title: "Bold", - action: () => editor.chain().focus().toggleBold().run(), - isActive: () => editor.isActive("bold"), - }, - { - icon: "italic", - title: "Italic", - action: () => editor.chain().focus().toggleItalic().run(), - isActive: () => editor.isActive("italic"), - }, - { - icon: "strikethrough", - title: "Strike", - action: () => editor.chain().focus().toggleStrike().run(), - isActive: () => editor.isActive("strike"), - }, - { - icon: "code-view", - title: "Code", - action: () => editor.chain().focus().toggleCode().run(), - isActive: () => editor.isActive("code"), - }, - { - icon: "mark-pen-line", - title: "Highlight", - action: () => editor.chain().focus().toggleHighlight().run(), - isActive: () => editor.isActive("highlight"), - }, - { - type: "divider", - }, { icon: "h-1", title: "Heading 1", @@ -61,37 +28,25 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { isActive: () => editor.isActive("heading", { level: 3 }), }, { - icon: "paragraph", - title: "Paragraph", - action: () => editor.chain().focus().setParagraph().run(), - isActive: () => editor.isActive("paragraph"), - }, - { - icon: "list-unordered", - title: "Bullet List", - action: () => editor.chain().focus().toggleBulletList().run(), - isActive: () => editor.isActive("bulletList"), - }, - { - icon: "list-ordered", - title: "Ordered List", - action: () => editor.chain().focus().toggleOrderedList().run(), - isActive: () => editor.isActive("orderedList"), + type: "divider", }, { - icon: "list-check-2", - title: "Task List", - action: () => editor.chain().focus().toggleTaskList().run(), - isActive: () => editor.isActive("taskList"), + icon: "bold", + title: "Bold", + action: () => editor.chain().focus().toggleBold().run(), + isActive: () => editor.isActive("bold"), }, { - icon: "code-box-line", - title: "Code Block", - action: () => editor.chain().focus().toggleCodeBlock().run(), - isActive: () => editor.isActive("codeBlock"), + icon: "italic", + title: "Italic", + action: () => editor.chain().focus().toggleItalic().run(), + isActive: () => editor.isActive("italic"), }, { - type: "divider", + icon: "strikethrough", + title: "Strike", + action: () => editor.chain().focus().toggleStrike().run(), + isActive: () => editor.isActive("strike"), }, { icon: "double-quotes-l", @@ -99,37 +54,27 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { action: () => editor.chain().focus().toggleBlockquote().run(), isActive: () => editor.isActive("blockquote"), }, - { - icon: "separator", - title: "Horizontal Rule", - action: () => editor.chain().focus().setHorizontalRule().run(), - }, + { type: "divider", }, { - icon: "text-wrap", - title: "Hard Break", - action: () => editor.chain().focus().setHardBreak().run(), + icon: "list-ordered", + title: "Ordered List", + action: () => editor.chain().focus().toggleOrderedList().run(), + isActive: () => editor.isActive("orderedList"), }, + { - icon: "format-clear", - title: "Clear Format", - action: () => editor.chain().focus().clearNodes().unsetAllMarks().run(), + icon: "list-unordered", + title: "Bullet List", + action: () => editor.chain().focus().toggleBulletList().run(), + isActive: () => editor.isActive("bulletList"), }, + { type: "divider", }, - { - icon: "arrow-go-back-line", - title: "Undo", - action: () => editor.chain().focus().undo().run(), - }, - { - icon: "arrow-go-forward-line", - title: "Redo", - action: () => editor.chain().focus().redo().run(), - }, { icon: "file-image-line", title: "Add image", @@ -150,6 +95,9 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { title: "Remove link", action: () => editor.chain().focus().unsetLink().run(), }, + { + type: "divider", + }, { icon: "table-line", title: "Add table", @@ -161,6 +109,24 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { .insertTable({ rows: 3, cols: 3, withHeaderRow: true }) .run(), }, + { + icon: "separator", + title: "Horizontal Rule", + action: () => editor.chain().focus().setHorizontalRule().run(), + }, + { + type: "divider", + }, + { + icon: "arrow-go-back-line", + title: "Undo", + action: () => editor.chain().focus().undo().run(), + }, + { + icon: "arrow-go-forward-line", + title: "Redo", + action: () => editor.chain().focus().redo().run(), + }, ] return ( From 2b171a1b7855ddaa1b67a03a4f24a15686fabb6f Mon Sep 17 00:00:00 2001 From: Hsu Zhong Jun <27919917+dcshzj@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:27:54 +0800 Subject: [PATCH 5/6] feat(tiptap): allow inserting of embed code using a modal (#1658) * feat(tiptap): allow inserting of embed code using a modal * feat(embed): adjust modal styling and validate input * feat(embed): add support for Instagram embeds * feat(embed): adjust Instagram to use iframe instead * fix(eslint): adjust circular deps * feat(embed): add support for FormSG embeds * styles(embed): fix copy for invalid embed code * fix(eslint): remove circular dependency * fix(embed): use Instagram method instead for FormSG * styles(embed): fix icon and position of embed modal * fix(embed): allow mozallowfullscreen and webkitallowfullscreen attributes --- .../EditorEmbedModal.stories.tsx | 41 ++++ .../EditorEmbedModal/EditorEmbedModal.tsx | 185 ++++++++++++++++++ src/components/EditorEmbedModal/index.ts | 1 + src/contexts/EditorModalContext.tsx | 2 +- src/layouts/EditPage/EditPage.tsx | 41 +++- src/layouts/EditPage/utils.ts | 2 + .../components/Editor/components/MenuBar.tsx | 5 + src/types/editPage.ts | 3 + src/utils/allowedHTML.ts | 30 +++ src/utils/cspUtils.js | 2 +- 10 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 src/components/EditorEmbedModal/EditorEmbedModal.stories.tsx create mode 100644 src/components/EditorEmbedModal/EditorEmbedModal.tsx create mode 100644 src/components/EditorEmbedModal/index.ts create mode 100644 src/types/editPage.ts diff --git a/src/components/EditorEmbedModal/EditorEmbedModal.stories.tsx b/src/components/EditorEmbedModal/EditorEmbedModal.stories.tsx new file mode 100644 index 000000000..a93e34b6d --- /dev/null +++ b/src/components/EditorEmbedModal/EditorEmbedModal.stories.tsx @@ -0,0 +1,41 @@ +import { useDisclosure } from "@chakra-ui/react" +import { Button } from "@opengovsg/design-system-react" +import type { Meta, StoryFn } from "@storybook/react" + +import { useSuccessToast } from "utils" + +import { EditorEmbedModal } from "./EditorEmbedModal" + +const editorEmbedModalMeta = { + title: "Components/Editor/Embed Modal", + component: EditorEmbedModal, +} as Meta + +const editorEmbedModalTemplate: StoryFn = () => { + const { isOpen, onOpen, onClose } = useDisclosure({ defaultIsOpen: true }) + const successToast = useSuccessToast() + const onProceed = () => { + successToast({ + id: "storybook-editor-embed-success", + description: "STORYBOOK: Embed has been successfully added", + }) + onClose() + } + + return ( + <> + Open editor embed modal + + > + ) +} + +export const Default = editorEmbedModalTemplate.bind({}) +Default.args = {} + +export default editorEmbedModalMeta diff --git a/src/components/EditorEmbedModal/EditorEmbedModal.tsx b/src/components/EditorEmbedModal/EditorEmbedModal.tsx new file mode 100644 index 000000000..fcfe809ea --- /dev/null +++ b/src/components/EditorEmbedModal/EditorEmbedModal.tsx @@ -0,0 +1,185 @@ +import { + FormControl, + HStack, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + Text, + Textarea, +} from "@chakra-ui/react" +import { yupResolver } from "@hookform/resolvers/yup" +import { + Button, + FormErrorMessage, + FormLabel, + Link, + ModalCloseButton, +} from "@opengovsg/design-system-react" +import * as Cheerio from "cheerio" +import { useEffect } from "react" +import { FormProvider, useForm } from "react-hook-form" +import * as Yup from "yup" + +import { useCspHook } from "hooks/settingsHooks" + +import { isEmbedCodeValid } from "utils/allowedHTML" + +import { EditorEmbedContents } from "types/editPage" + +interface EditorEmbedModalProps { + isOpen: boolean + onClose: () => void + onProceed: (embedCode: EditorEmbedContents) => void + cursorValue: string +} + +export const EditorEmbedModal = ({ + isOpen, + onClose, + onProceed, + cursorValue, +}: EditorEmbedModalProps): JSX.Element => { + const { data: csp } = useCspHook() + + const handleSubmit = (embedCode: EditorEmbedContents) => { + const { value } = embedCode + const $ = Cheerio.load(value) + + if ($("blockquote").hasClass("instagram-media")) { + const postUrl = $(".instagram-media").attr("data-instgrm-permalink") + const url = document.createElement("a") + url.href = postUrl || "" + const code = `` + onProceed({ value: code }) + } else if ($('iframe[src^="https://form.gov.sg"]').length > 0) { + // We only want to keep the part, the rest is filled in by Tiptap + onProceed({ value: $('iframe[src^="https://form.gov.sg"]').toString() }) + } else { + onProceed(embedCode) + } + } + + const methods = useForm({ + mode: "onTouched", + resolver: yupResolver( + Yup.object().shape({ + value: Yup.string() + .required( + "Content to embed cannot be empty. Please enter valid HTML code." + ) + .matches( + // Blockquote is to allow for Instagram embeds + /^( isEmbedCodeValid(csp, value), + }), + }) + ), + defaultValues: { + value: "", + }, + }) + + useEffect(() => { + if (isOpen) { + methods.setValue( + "value", + cursorValue + .replace('', "") + // Remove the closing div tag + .slice(0, -6) + ) + } else { + methods.reset() + } + }, [cursorValue, isOpen, methods]) + + return ( + + + + + + + + + {cursorValue === "" + ? "Embed new content" + : "Edit embedded content"} + + + + + You can embed selected external content on your page.{" "} + + Click here + {" "} + to learn more about embedding content onto your Isomer site. + + + + Code to embed + + + + + {methods.formState.errors.value ? ( + + {methods.formState.errors.value.message} + + ) : ( + + Isomer does not support any unauthorised content that + violates the Content Security Policy (CSP). + + )} + + + + + + {cursorValue !== "" && ( + { + methods.reset() + onClose() + }} + > + Cancel + + )} + + {cursorValue === "" ? "Add to page" : "Save"} + + + + + + + + ) +} diff --git a/src/components/EditorEmbedModal/index.ts b/src/components/EditorEmbedModal/index.ts new file mode 100644 index 000000000..6f6f5a8f4 --- /dev/null +++ b/src/components/EditorEmbedModal/index.ts @@ -0,0 +1 @@ +export * from "./EditorEmbedModal" diff --git a/src/contexts/EditorModalContext.tsx b/src/contexts/EditorModalContext.tsx index 090e62b13..72620ced3 100644 --- a/src/contexts/EditorModalContext.tsx +++ b/src/contexts/EditorModalContext.tsx @@ -1,7 +1,7 @@ import { PropsWithChildren, createContext, useContext } from "react" interface EditorModalContextProps { - showModal: (modalVariant: "images" | "files" | "hyperlink") => void + showModal: (modalVariant: "images" | "files" | "hyperlink" | "embed") => void } const EditorModalContext = createContext(null) diff --git a/src/layouts/EditPage/EditPage.tsx b/src/layouts/EditPage/EditPage.tsx index f29774100..9873c30a8 100644 --- a/src/layouts/EditPage/EditPage.tsx +++ b/src/layouts/EditPage/EditPage.tsx @@ -11,12 +11,13 @@ 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 { useEditor } from "@tiptap/react" +import { getHTMLFromFragment, useEditor } from "@tiptap/react" import StarterKit from "@tiptap/starter-kit" import { Context, useContext, useEffect, useState } from "react" import { useParams } from "react-router-dom" import { Markdown } from "tiptap-markdown" +import { EditorEmbedModal } from "components/EditorEmbedModal" import HyperlinkModal from "components/HyperlinkModal" import MediaModal from "components/media/MediaModal" @@ -25,10 +26,14 @@ import { EditorModalContextProvider } from "contexts/EditorModalContext" import { ServicesContext } from "contexts/ServicesContext" import { useGetPageHook } from "hooks/pageHooks" +import { useCspHook } from "hooks/settingsHooks" import { Iframe } from "layouts/components/Editor/extensions" +import { isEmbedCodeValid } from "utils/allowedHTML" + import { MediaService } from "services" +import { EditorEmbedContents } from "types/editPage" import { getDecodedParams, getImageDetails } from "utils" import { MarkdownEditPage } from "./MarkdownEditPage" @@ -40,6 +45,7 @@ export const EditPage = () => { const { data: initialPageData, isLoading: isLoadingPage } = useGetPageHook( params ) + const { data: csp } = useCspHook() const [variant, setVariant] = useState( initialPageData?.content?.frontMatter?.variant || "markdown" ) @@ -92,12 +98,20 @@ export const EditPage = () => { onClose: onHyperlinkModalClose, } = useDisclosure() + const { + isOpen: isEmbedModalOpen, + onOpen: onEmbedModalOpen, + onClose: onEmbedModalClose, + } = useDisclosure() + const { siteName } = decodedParams const { mediaService } = useContext<{ mediaService: MediaService }>( (ServicesContext as unknown) as Context<{ mediaService: MediaService }> ) + if (!editor) return null + const getImageSrc = async (src: string) => { const { fileName, imageDirectory } = getImageDetails(src) return mediaService.get({ @@ -107,7 +121,13 @@ export const EditPage = () => { }) } - if (!editor) return null + const handleEmbedInsert = ({ value }: EditorEmbedContents) => { + if (isEmbedCodeValid(csp, value)) { + editor.chain().focus().insertContent(value.replaceAll("\n", "")).run() + } + + onEmbedModalClose() + } return ( @@ -115,6 +135,8 @@ export const EditPage = () => { showModal={(modalType) => { if (modalType === "hyperlink") { onHyperlinkModalOpen() + } else if (modalType === "embed") { + onEmbedModalOpen() } else { setMediaType(modalType) onMediaModalOpen() @@ -173,6 +195,21 @@ export const EditPage = () => { }} /> )} + {isEmbedModalOpen && ( + + )} {variant === "markdown" ? ( tag appears as the first line of the markdown FORCE_BODY: true, diff --git a/src/layouts/components/Editor/components/MenuBar.tsx b/src/layouts/components/Editor/components/MenuBar.tsx index 0930b23b2..e67fa34e7 100644 --- a/src/layouts/components/Editor/components/MenuBar.tsx +++ b/src/layouts/components/Editor/components/MenuBar.tsx @@ -95,6 +95,11 @@ export const MenuBar = ({ editor }: { editor: Editor }) => { title: "Remove link", action: () => editor.chain().focus().unsetLink().run(), }, + { + icon: "code-s-slash-line", + title: "Insert embed", + action: () => showModal("embed"), + }, { type: "divider", }, diff --git a/src/types/editPage.ts b/src/types/editPage.ts new file mode 100644 index 000000000..43b6526c6 --- /dev/null +++ b/src/types/editPage.ts @@ -0,0 +1,3 @@ +export interface EditorEmbedContents { + value: string +} diff --git a/src/utils/allowedHTML.ts b/src/utils/allowedHTML.ts index 15115020e..eadc0c3b9 100644 --- a/src/utils/allowedHTML.ts +++ b/src/utils/allowedHTML.ts @@ -1,3 +1,9 @@ +import DOMPurify from "dompurify" + +import * as CspService from "services/CspService" + +import checkCSP from "./cspUtils" + /** * HTML tag that do not count towards the total * character limit. @@ -11,3 +17,27 @@ const IFRAME_TAG_REGEX = new RegExp("()", "gm") export const getLengthWithoutTags = (text: string): number => { return text.replace(IFRAME_TAG_REGEX, "").length } + +/** + * Utility method for determinign if the given HTML embed code adheres to our + * Content Security Policy. + */ +export const isEmbedCodeValid = ( + csp: Awaited> | undefined, + embedCode: string | undefined +) => { + if (!csp || !embedCode) return false + + const { + isCspViolation: checkedIsCspViolation, + sanitisedHtml: CSPSanitisedHtml, + } = checkCSP(csp, embedCode) + DOMPurify.sanitize(CSPSanitisedHtml) + + // Using FORCE_BODY adds a fake + DOMPurify.removed = DOMPurify.removed.filter( + (el) => el.element?.tagName !== "REMOVE" + ) + + return !checkedIsCspViolation && DOMPurify.removed.length === 0 +} diff --git a/src/utils/cspUtils.js b/src/utils/cspUtils.js index 93ca3a935..9ed5f9ece 100644 --- a/src/utils/cspUtils.js +++ b/src/utils/cspUtils.js @@ -5,7 +5,7 @@ import cheerio from "cheerio" import escapeStringRegexp from "escape-string-regexp" import _ from "lodash" -import { isLinkInternal } from "utils" +import { isLinkInternal } from "utils/misc" function toRegExp(string) { const strippedString = string.replace(/\/$/, "") // removes ending '/' from domains eg 'abc.com/' From 146dd951f9504137137c675c7fec7c627fe9f6a7 Mon Sep 17 00:00:00 2001 From: Kishore <42832651+kishore03109@users.noreply.github.com> Date: Thu, 16 Nov 2023 18:57:04 +0800 Subject: [PATCH 6/6] 0.59.0 --- CHANGELOG.md | 11 +++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eeacf2aa..708a31715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,19 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v0.59.0](https://github.com/isomerpages/isomercms-frontend/compare/v0.58.0...v0.59.0) + +- feat(tiptap): allow inserting of embed code using a modal [`#1658`](https://github.com/isomerpages/isomercms-frontend/pull/1658) +- feat(tiptap): rm redundant logos [`#1679`](https://github.com/isomerpages/isomercms-frontend/pull/1679) +- Fix/is 689 reuse image/album tiles [`#1678`](https://github.com/isomerpages/isomercms-frontend/pull/1678) +- Fix/is724-apply-styles [`#1677`](https://github.com/isomerpages/isomercms-frontend/pull/1677) +- fix(editor): refactor to use `tooltip` [`#1669`](https://github.com/isomerpages/isomercms-frontend/pull/1669) +- release/0.58.0 [`#1675`](https://github.com/isomerpages/isomercms-frontend/pull/1675) + #### [v0.58.0](https://github.com/isomerpages/isomercms-frontend/compare/v0.57.0...v0.58.0) +> 14 November 2023 + - fix(markdown): update styling to remove overflow [`#1671`](https://github.com/isomerpages/isomercms-frontend/pull/1671) - feat/addReportingBUtton [`#1674`](https://github.com/isomerpages/isomercms-frontend/pull/1674) - feat(media): add announcements and feature tour for media enhancements [`#1632`](https://github.com/isomerpages/isomercms-frontend/pull/1632) diff --git a/package-lock.json b/package-lock.json index 9d4768f6c..a28ded033 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "isomercms-frontend", - "version": "0.58.0", + "version": "0.59.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "isomercms-frontend", - "version": "0.58.0", + "version": "0.59.0", "hasInstallScript": true, "dependencies": { "@braintree/sanitize-url": "^6.0.1", diff --git a/package.json b/package.json index 046dd4543..e37f58c10 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "isomercms-frontend", - "version": "0.58.0", + "version": "0.59.0", "private": true, "engines": { "node": ">=16.0.0"