From df2af8fec57a21f275eedcc088d95f8f2810ce30 Mon Sep 17 00:00:00 2001 From: seaerchin <44049504+seaerchin@users.noreply.github.com> Date: Fri, 10 Nov 2023 13:24:04 +0800 Subject: [PATCH 01/13] refactor(tiptap-editor): update width (#1667) --- src/layouts/EditPage/TiptapEditPage.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/layouts/EditPage/TiptapEditPage.tsx b/src/layouts/EditPage/TiptapEditPage.tsx index c755bfdae..0b0e95fa7 100644 --- a/src/layouts/EditPage/TiptapEditPage.tsx +++ b/src/layouts/EditPage/TiptapEditPage.tsx @@ -58,10 +58,11 @@ export const TiptapEditPage = ({ variant="tiptap" > {/* Editor */} - + {/* Preview */} From b9e29912a53f63a8801840a39c1ea8548ffbac76 Mon Sep 17 00:00:00 2001 From: seaerchin <44049504+seaerchin@users.noreply.github.com> Date: Fri, 10 Nov 2023 13:25:12 +0800 Subject: [PATCH 02/13] refactor(editpage): add alt text modal (#1665) --- src/layouts/EditPage/EditPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/layouts/EditPage/EditPage.tsx b/src/layouts/EditPage/EditPage.tsx index 32c6cd2ce..4cf3a3827 100644 --- a/src/layouts/EditPage/EditPage.tsx +++ b/src/layouts/EditPage/EditPage.tsx @@ -139,6 +139,7 @@ export const EditPage = () => { )} {isMediaModalOpen && ( { From fbe4d68803cf97406d180b8c00247937c8c05a88 Mon Sep 17 00:00:00 2001 From: Hsu Zhong Jun <27919917+dcshzj@users.noreply.github.com> Date: Fri, 10 Nov 2023 13:56:25 +0800 Subject: [PATCH 03/13] chore(login): automatically focus on input field (#1664) --- src/layouts/Login/components/OtpForm.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/layouts/Login/components/OtpForm.tsx b/src/layouts/Login/components/OtpForm.tsx index 6b45ae723..7268a08b2 100644 --- a/src/layouts/Login/components/OtpForm.tsx +++ b/src/layouts/Login/components/OtpForm.tsx @@ -64,10 +64,12 @@ export const OtpForm = ({ Date: Fri, 10 Nov 2023 18:50:57 +0800 Subject: [PATCH 04/13] fix(editor): change inner prosemirror stuff to have 100% height (#1670) --- src/layouts/components/Editor/styles.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/layouts/components/Editor/styles.scss b/src/layouts/components/Editor/styles.scss index 212e2ad92..bf1759f41 100644 --- a/src/layouts/components/Editor/styles.scss +++ b/src/layouts/components/Editor/styles.scss @@ -18,6 +18,8 @@ margin-top: 0.75em; } + height: 100%; + p.is-empty::before { color: #adb5bd; content: attr(data-placeholder); From 7b25cfb1e3556f53a619cbcf0e0c06b9bda0bd4a Mon Sep 17 00:00:00 2001 From: Kishore <42832651+kishore03109@users.noreply.github.com> Date: Tue, 14 Nov 2023 09:01:58 +0800 Subject: [PATCH 05/13] fix(image): search pagination (#1668) --- src/components/media/MediasSelectModal.jsx | 3 ++- src/services/DirectoryService/DirectoryService.ts | 3 ++- src/types/folders.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/media/MediasSelectModal.jsx b/src/components/media/MediasSelectModal.jsx index ce993f0c7..225a1ac2a 100644 --- a/src/components/media/MediasSelectModal.jsx +++ b/src/components/media/MediasSelectModal.jsx @@ -68,6 +68,7 @@ const MediasSelectModal = ({ { initialData: { directories: [] } } ) + const [searchValue, setSearchedValue] = useState("") const { data: { files: mediaFolderFiles, total }, isLoading: isListMediaFilesLoading, @@ -78,6 +79,7 @@ const MediasSelectModal = ({ // returns an index with 1 offset curPage: curPage - 1, limit: MEDIA_PAGINATION_SIZE, + search: searchValue, }, { initialData: { files: [] } } ) @@ -88,7 +90,6 @@ const MediasSelectModal = ({ mediaDirectoryName ) - const [searchValue, setSearchedValue] = useState("") const filteredDirectories = filterMediaByFileName( mediaFolderSubdirectories, searchValue diff --git a/src/services/DirectoryService/DirectoryService.ts b/src/services/DirectoryService/DirectoryService.ts index 1a4286d84..53641b451 100644 --- a/src/services/DirectoryService/DirectoryService.ts +++ b/src/services/DirectoryService/DirectoryService.ts @@ -101,11 +101,12 @@ export const getMediaFolderFiles = ({ mediaDirectoryName, curPage = 0, limit = 1000, + search = "", }: MediaDirectoryParams): Promise => { const endpoint = `/sites/${siteName}/media/${mediaDirectoryName}/files` return apiService .get(endpoint, { - params: { page: curPage, limit }, + params: { page: curPage, limit, search }, }) .then(({ data }) => data) } diff --git a/src/types/folders.ts b/src/types/folders.ts index f4178bf5e..fb4f35d86 100644 --- a/src/types/folders.ts +++ b/src/types/folders.ts @@ -11,6 +11,7 @@ export interface MediaDirectoryParams { mediaDirectoryName: string curPage?: number limit?: number + search?: string } export type DirectoryParams = Omit From ff2e0055b1ffae1682e074094c4e5517f5f6a10f Mon Sep 17 00:00:00 2001 From: Alexander Lee Date: Tue, 14 Nov 2023 10:25:19 +0800 Subject: [PATCH 06/13] chore: update select media modal (#1627) * chore: update select media modal * fix: style changes * chore: upgrade design system --- package-lock.json | 73 ++++---- package.json | 2 +- src/components/media/MediasSelectModal.jsx | 183 ++++++++++++--------- 3 files changed, 141 insertions(+), 117 deletions(-) diff --git a/package-lock.json b/package-lock.json index aa651b21a..6613f291c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "@growthbook/growthbook-react": "^0.17.0", "@hello-pangea/dnd": "^16.3.0", "@hookform/resolvers": "^2.8.2", - "@opengovsg/design-system-react": "^1.3.0", + "@opengovsg/design-system-react": "^1.9.0", "@sentry/react": "^7.12.1", "@sentry/tracing": "^7.12.1", "@tanstack/react-table": "^8.5.13", @@ -5467,11 +5467,12 @@ } }, "node_modules/@floating-ui/react": { - "version": "0.22.3", - "license": "MIT", + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.2.tgz", + "integrity": "sha512-ocpz3MxYoZlgsASiVFayiTnKukR8QZDQUMqxMdF0YFLbu8lw/IL6AHKLROI8SOpp6CxpUGPh9Q4a03eBAVEZNQ==", "dependencies": { - "@floating-ui/react-dom": "^1.3.0", - "aria-hidden": "^1.1.3", + "@floating-ui/react-dom": "^2.0.3", + "@floating-ui/utils": "^0.1.5", "tabbable": "^6.0.1" }, "peerDependencies": { @@ -5480,10 +5481,11 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "1.3.0", - "license": "MIT", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz", + "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==", "dependencies": { - "@floating-ui/dom": "^1.2.1" + "@floating-ui/dom": "^1.5.1" }, "peerDependencies": { "react": ">=16.8.0", @@ -5491,8 +5493,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.1.1", - "license": "MIT" + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", + "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, "node_modules/@fontsource/ibm-plex-mono": { "version": "5.0.5", @@ -7102,11 +7105,12 @@ "license": "MIT" }, "node_modules/@opengovsg/design-system-react": { - "version": "1.4.0", - "license": "SEE LICENSE IN LICENSE.md", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opengovsg/design-system-react/-/design-system-react-1.9.0.tgz", + "integrity": "sha512-gMkQfUDfzyKdTqy6fJz16wY/ozC6ghblG2/aw3CPjJ+b2JGP2Siy1KCqllN2VZoqpBxRcydO7iuSwp3pzYK8QQ==", "dependencies": { "@chakra-ui/utils": "^2.0.12", - "@floating-ui/react": "^0.22.2", + "@floating-ui/react": "^0.26.1", "@fontsource/ibm-plex-mono": "^5.0.3", "country-flag-icons": "^1.4.19", "date-fns": "^2.28.0", @@ -7116,8 +7120,8 @@ "inter-ui": "^3.19.3", "libphonenumber-js": "^1.9.44", "lodash": "^4.17.21", - "nanoid": "^3.3.4", - "react-dropzone": "^11.5.1", + "nanoid": "^5.0.2", + "react-dropzone": "^14.2.3", "react-input-mask": "^3.0.0-alpha.2", "react-roving-tabindex": "^3.2.0", "react-textarea-autosize": "^8.3.3", @@ -7152,31 +7156,21 @@ "react": ">=16.12.0" } }, - "node_modules/@opengovsg/design-system-react/node_modules/file-selector": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.4.0.tgz", - "integrity": "sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg==", - "dependencies": { - "tslib": "^2.0.3" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@opengovsg/design-system-react/node_modules/react-dropzone": { - "version": "11.7.1", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.7.1.tgz", - "integrity": "sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ==", - "dependencies": { - "attr-accept": "^2.2.2", - "file-selector": "^0.4.0", - "prop-types": "^15.8.1" + "node_modules/@opengovsg/design-system-react/node_modules/nanoid": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.3.tgz", + "integrity": "sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.js" }, "engines": { - "node": ">= 10.13" - }, - "peerDependencies": { - "react": ">= 16.8" + "node": "^18 || >=20" } }, "node_modules/@opengovsg/design-system-react/node_modules/react-input-mask": { @@ -36420,7 +36414,8 @@ }, "node_modules/tabbable": { "version": "6.2.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/table": { "version": "6.8.1", diff --git a/package.json b/package.json index 9c602488b..970acca91 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@growthbook/growthbook-react": "^0.17.0", "@hello-pangea/dnd": "^16.3.0", "@hookform/resolvers": "^2.8.2", - "@opengovsg/design-system-react": "^1.3.0", + "@opengovsg/design-system-react": "^1.9.0", "@sentry/react": "^7.12.1", "@sentry/tracing": "^7.12.1", "@tanstack/react-table": "^8.5.13", diff --git a/src/components/media/MediasSelectModal.jsx b/src/components/media/MediasSelectModal.jsx index 225a1ac2a..df6b3f2a5 100644 --- a/src/components/media/MediasSelectModal.jsx +++ b/src/components/media/MediasSelectModal.jsx @@ -13,16 +13,21 @@ import { ModalFooter, ModalBody, ModalCloseButton, - Spacer, - Divider, + HStack, + BreadcrumbItem, + BreadcrumbLink, } from "@chakra-ui/react" -import { Button, Searchbar, Pagination } from "@opengovsg/design-system-react" -import { useState } from "react" +import { + Button, + Searchbar, + Pagination, + Breadcrumb, +} from "@opengovsg/design-system-react" +import { useState, useEffect } from "react" import { useFormContext } from "react-hook-form" -import { Link, useRouteMatch } from "react-router-dom" +import { useRouteMatch } from "react-router-dom" import { FolderCard } from "components/FolderCard" -import { BreadcrumbItem } from "components/folders/Breadcrumb" import { LoadingButton } from "components/LoadingButton" import MediaCard from "components/media/MediaCard" @@ -52,25 +57,26 @@ const MediasSelectModal = ({ setQueryParams, }) => { const { params } = useRouteMatch() - const { siteName } = params + const { siteName, fileName } = params const { mediaRoom, mediaDirectoryName } = queryParams const [curPage, setCurPage] = usePaginate() + const [mediaFolderSubdirectories, setMediaFolderSubdirectories] = useState([]) + const [mediaFolderFiles, setMediaFolderFiles] = useState([]) + const [total, setTotal] = useState() + + const [searchValue, setSearchedValue] = useState("") const { - data: { directories: mediaFolderSubdirectories }, + data: listMediaDirectoriesData, isLoading: isListMediaFolderSubdirectoriesLoading, - } = useListMediaFolderSubdirectories( - { - ...queryParams, - }, - { initialData: { directories: [] } } - ) + } = useListMediaFolderSubdirectories({ + ...queryParams, + }) - const [searchValue, setSearchedValue] = useState("") const { - data: { files: mediaFolderFiles, total }, + data: listMediaFilesData, isLoading: isListMediaFilesLoading, } = useListMediaFolderFiles( { @@ -84,6 +90,18 @@ const MediasSelectModal = ({ { initialData: { files: [] } } ) + useEffect(() => { + if (listMediaDirectoriesData) + setMediaFolderSubdirectories(listMediaDirectoriesData.directories) + }, [listMediaDirectoriesData]) + + useEffect(() => { + if (listMediaFilesData) { + setMediaFolderFiles(listMediaFilesData.files) + setTotal(listMediaFilesData.total) + } + }, [listMediaFilesData]) + const files = useGetAllMediaFiles( mediaFolderFiles || [], params.siteName, @@ -110,60 +128,67 @@ const MediasSelectModal = ({ closeOnOverlayClick={false} > - + - - {`Select ${mediaRoom.slice(0, -1)}`} - - {/* Search medias */} - - setSearchedValue(val)} /> - - {/* Upload medias */} - - + + + {`Add ${mediaRoom.slice(0, -1)}${ + fileName ? ` to ${decodeURIComponent(fileName)}` : "" + }`} + + + + Choose from your images or upload a new image. You can organise + your images in Workspace > Images. + + + {/* Search medias */} + + setSearchedValue(val)} + placeholder="Press enter to search" + /> + + {/* Upload medias */} + + + - - -
-

- For {mediaRoom} other than - {mediaRoom === "images" - ? ` 'png', 'jpg', '.jpeg', 'gif', 'tif', '.tiff', 'bmp', 'ico', 'svg'` - : ` 'pdf'`} - , please use - - https://go.gov.sg - - to upload and link them to your Isomer site. -

-
-
+ {queryParams.mediaDirectoryName ? queryParams.mediaDirectoryName.split("%2F").map((dir, idx) => ( { - e.preventDefault() - setQueryParams((prevState) => ({ - ...prevState, - mediaDirectoryName: getMediaDirectoryName( - queryParams.mediaDirectoryName, - { end: idx + 1 } - ), - })) - }} - /> + > + { + e.preventDefault() + setQueryParams((prevState) => ({ + ...prevState, + mediaDirectoryName: getMediaDirectoryName( + queryParams.mediaDirectoryName, + { end: idx + 1 } + ), + })) + }} + > + {deslugifyDirectory(dir)} + + )) : null} -
+
@@ -205,24 +230,28 @@ const MediasSelectModal = ({ isLoaded={!isListMediaFilesLoading} > - {files - .filter(({ data }) => filteredMedias.includes(data?.name)) - .map(({ data, isLoading }, mediaItemIndex) => ( - - onMediaSelect(data)} - isSelected={data.name === watch("selectedMedia")?.name} - /> - - ))} + {files && + files + .filter(({ data }) => filteredMedias.includes(data?.name)) + .map(({ data, isLoading }, mediaItemIndex) => ( + + onMediaSelect(data)} + isSelected={ + data.name === watch("selectedMedia")?.name + } + showSettings={false} + /> + + ))}
From 83a1adc8677a491741e049be3de884fcaa7b2cf5 Mon Sep 17 00:00:00 2001 From: Hsu Zhong Jun <27919917+dcshzj@users.noreply.github.com> Date: Tue, 14 Nov 2023 11:33:51 +0800 Subject: [PATCH 07/13] fix(preview): use site colours for headings in preview (#1663) * fix(preview): use site colours for headings in preview * fix(preview): use secondary colours for quotes as well --- src/utils/siteColorUtils.js | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/utils/siteColorUtils.js b/src/utils/siteColorUtils.js index ad92358df..369f04be7 100644 --- a/src/utils/siteColorUtils.js +++ b/src/utils/siteColorUtils.js @@ -106,6 +106,41 @@ const createPageStyleSheet = (repoName, primaryColor, secondaryColor) => { `.content h5 strong { color: ${secondaryColor} !important;}`, 0 ) + customStyleSheet.insertRule( + `.content h1 { color: ${secondaryColor} !important;}`, + 0 + ) + customStyleSheet.insertRule( + `.content h2 { color: ${secondaryColor} !important;}`, + 0 + ) + customStyleSheet.insertRule( + `.content h3 { color: ${secondaryColor} !important;}`, + 0 + ) + customStyleSheet.insertRule( + `.content h4 { color: ${secondaryColor} !important;}`, + 0 + ) + customStyleSheet.insertRule( + `.content h5 { color: ${secondaryColor} !important;}`, + 0 + ) + + // EditPage: Blockquotes + customStyleSheet.insertRule( + `.content blockquote { border-left-color: ${secondaryColor} !important;}`, + 0 + ) + customStyleSheet.insertRule( + `.content blockquote > p { color: ${secondaryColor} !important;}`, + 0 + ) + customStyleSheet.insertRule( + `.content blockquote > ul { color: ${secondaryColor} !important;}`, + 0 + ) + customStyleSheet.insertRule( `.has-text-secondary { color: ${secondaryColor} !important;}`, 0 From b0a1e980686bad8e66e9aa02990f78578e144389 Mon Sep 17 00:00:00 2001 From: Kishore <42832651+kishore03109@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:02:10 +0800 Subject: [PATCH 08/13] Fix/buildStatusBadgeAPI (#1673) * fix(statusBadge): rm lingering api call * fix(stagingStatus): rm unused imports * fix(status Badge): fix storybook not rendering * fix(badgeStatus): add in qnmark * fix(badgeStatus): fix styling issues --- src/components/Header.jsx | 32 +++++++++---------- src/components/Header/StatusBadge.tsx | 20 ++++++------ src/constants/featureFlags.ts | 1 + src/hooks/useGetStagingStatus.ts | 2 -- .../layouts/SiteEditLayout/SiteEditHeader.tsx | 6 ++-- .../layouts/SiteViewLayout/SiteViewHeader.tsx | 4 +-- src/utils/growthbook.ts | 17 +++++++++- 7 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/components/Header.jsx b/src/components/Header.jsx index c1565f54a..0274a1774 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,12 +1,4 @@ -import { - Box, - Flex, - Icon, - Text, - HStack, - useDisclosure, - Skeleton, -} from "@chakra-ui/react" +import { Box, Flex, Icon, Text, HStack, useDisclosure } from "@chakra-ui/react" import { Button, IconButton } from "@opengovsg/design-system-react" import axios from "axios" import PropTypes from "prop-types" @@ -19,17 +11,20 @@ import { StatusBadge } from "components/Header/StatusBadge" import { ViewStagingSiteModal } from "components/ViewStagingSiteModal" import { WarningModal } from "components/WarningModal" +import { FEATURE_FLAGS } from "constants/featureFlags" + import { useLoginContext } from "contexts/LoginContext" import { useGetReviewRequests, useGetStagingUrl, } from "hooks/siteDashboardHooks" -import { useGetStagingStatus } from "hooks/useGetStagingStatus" import useRedirectHook from "hooks/useRedirectHook" import { ReviewRequestModal } from "layouts/ReviewRequest" +import { useIsIsomerFeatureOn } from "utils/growthbook" + import { getBackButton } from "utils" // axios settings @@ -88,10 +83,9 @@ const Header = ({ if (isEditPage && !shouldAllowEditPageBackNav) onWarningModalOpen() else toggleBackNav() } - const { - data: getStagingStatusData, - isLoading: isGetStagingStatusLoading, - } = useGetStagingStatus(siteName) + const isShowStagingBuildStatusEnabled = useIsIsomerFeatureOn( + FEATURE_FLAGS.IS_SHOW_STAGING_BUILD_STATUS_ENABLED + ) return ( <> @@ -139,9 +133,13 @@ const Header = ({ ) : null} - - {getStagingStatusData && } - + ( + {isShowStagingBuildStatusEnabled && ( + + + + )} + ) + {" "} or drag and drop here diff --git a/src/components/CreateMediaFolderModal/CreateMediaFolderModal.stories.tsx b/src/components/CreateMediaFolderModal/CreateMediaFolderModal.stories.tsx new file mode 100644 index 000000000..9ed7964d8 --- /dev/null +++ b/src/components/CreateMediaFolderModal/CreateMediaFolderModal.stories.tsx @@ -0,0 +1,96 @@ +import { useDisclosure } from "@chakra-ui/react" +import { Button } from "@opengovsg/design-system-react" +import type { Meta, StoryFn } from "@storybook/react" +import { MemoryRouter, Route } from "react-router-dom" + +import { getMediaLabels } from "utils/media" + +import { MOCK_MEDIA_ITEM_DATA, MOCK_MEDIA_ITEM_ONE } from "mocks/constants" +import { handlers } from "mocks/handlers" +import { buildMediaFileData, buildMediaFolderFilesData } from "mocks/utils" +import { useSuccessToast } from "utils" + +import { CreateMediaFolderModal } from "./CreateMediaFolderModal" + +const createMediaFolderModalMeta = { + title: "Components/Create Media Folder Modal", + component: CreateMediaFolderModal, + decorators: [ + (Story) => { + return ( + + + + + + ) + }, + ], +} as Meta + +const createMediaFolderModalTemplate: StoryFn< + typeof CreateMediaFolderModal +> = ({ originalSelectedMedia }) => { + const { isOpen, onOpen, onClose } = useDisclosure({ defaultIsOpen: true }) + const successToast = useSuccessToast() + const onProceed = async (result: any) => { + console.log(result) + successToast({ + id: "storybook-create-media-folder-success", + description: "STORYBOOK: Media folder has been successfully created", + }) + onClose() + } + + return ( + <> + + + + ) +} + +export const Default = createMediaFolderModalTemplate.bind({}) +Default.args = { + originalSelectedMedia: [], +} +Default.parameters = { + msw: { + handlers: [ + ...handlers, + buildMediaFolderFilesData(MOCK_MEDIA_ITEM_DATA), + buildMediaFileData(MOCK_MEDIA_ITEM_ONE), + ], + }, +} + +export const OneSelected = createMediaFolderModalTemplate.bind({}) +OneSelected.args = { + originalSelectedMedia: [ + { filePath: "/images/hero-banner.png", size: 1234, sha: "sha1234" }, + ], +} + +export const MultipleSelected = createMediaFolderModalTemplate.bind({}) +MultipleSelected.args = { + originalSelectedMedia: [ + { filePath: "/images/hero-banner.png", size: 1234, sha: "sha1234" }, + { filePath: "/images/hero-banner2.png", size: 2345, sha: "sha1234" }, + ], +} + +export default createMediaFolderModalMeta diff --git a/src/components/CreateMediaFolderModal/CreateMediaFolderModal.tsx b/src/components/CreateMediaFolderModal/CreateMediaFolderModal.tsx new file mode 100644 index 000000000..8e756ce3b --- /dev/null +++ b/src/components/CreateMediaFolderModal/CreateMediaFolderModal.tsx @@ -0,0 +1,373 @@ +import { + Box, + Center, + FormControl, + HStack, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + ModalOverlay, + SimpleGrid, + Skeleton, + Text, +} from "@chakra-ui/react" +import { yupResolver } from "@hookform/resolvers/yup" +import { + Button, + FormErrorMessage, + FormLabel, + Input, + ModalCloseButton, + Pagination, +} from "@opengovsg/design-system-react" +import { AxiosError } from "axios" +import _ from "lodash" +import { useEffect } from "react" +import { FormProvider, useForm } from "react-hook-form" +import { UseMutateAsyncFunction } from "react-query" +import { useRouteMatch } from "react-router-dom" + +import { DirectorySettingsSchema } from "components/DirectorySettingsModal" +import { ImagePreviewCard } from "components/ImagePreviewCard" + +import { MEDIA_PAGINATION_SIZE } from "constants/media" + +import { useListMediaFolderFiles } from "hooks/directoryHooks" +import { useGetAllMediaFiles } from "hooks/directoryHooks/useGetAllMediaFiles" +import { usePaginate } from "hooks/usePaginate" + +import { FilePreviewCard } from "layouts/Media/components" + +import { getSelectedMediaDto } from "utils/media" +import { isWriteActionsDisabled } from "utils/reviewRequests" + +import { GetMediaSubdirectoriesDto, MediaData } from "types/directory" +import { MiddlewareError } from "types/error" +import { + MediaFolderCreationInfo, + MediaFolderTypes, + MediaLabels, + SelectedMediaDto, +} from "types/media" + +interface CreateMediaFolderModalProps { + originalSelectedMedia: SelectedMediaDto[] + mediaLabels: MediaLabels + mediaType: MediaFolderTypes + subDirectories: GetMediaSubdirectoriesDto | undefined + mediaDirectoryName: string + isOpen: boolean + isLoading: boolean + onClose: () => void + onProceed: UseMutateAsyncFunction< + void, + AxiosError, + MediaFolderCreationInfo, + unknown + > +} + +const getButtonLabel = ( + originalSelectedMedia: SelectedMediaDto[], + selectedMedia: SelectedMediaDto[], + mediaLabels: MediaLabels +) => { + const { + singularDirectoryLabel, + singularMediaLabel, + pluralMediaLabel, + } = mediaLabels + + if (originalSelectedMedia.length > 0) { + return `Create ${singularDirectoryLabel}` + } + + if (selectedMedia.length > 1) { + return `Add ${selectedMedia.length} ${pluralMediaLabel} to ${singularDirectoryLabel}` + } + + if (selectedMedia.length === 1) { + return `Add ${singularMediaLabel} to ${singularDirectoryLabel}` + } + + return `Add ${pluralMediaLabel} to ${singularDirectoryLabel}` +} + +export const CreateMediaFolderModal = ({ + originalSelectedMedia, + mediaLabels, + mediaType, + subDirectories, + mediaDirectoryName, + isOpen, + isLoading, + onClose, + onProceed, +}: CreateMediaFolderModalProps): JSX.Element => { + const { params } = useRouteMatch<{ + siteName: string + }>() + const { siteName } = params + const [curPage, setCurPage] = usePaginate() + const isWriteDisabled = isWriteActionsDisabled(siteName) + const { + singularMediaLabel, + pluralMediaLabel, + singularDirectoryLabel, + } = mediaLabels + + const existingTitles = subDirectories?.directories.map( + (directory) => directory.name + ) + + const { + data: mediaFolderFiles, + isLoading: isListMediaFilesLoading, + } = useListMediaFolderFiles({ + siteName, + mediaDirectoryName, + // NOTE: Subtracting 1 here because `usePaginate` + // returns an index with 1 offset + curPage: curPage - 1, + limit: MEDIA_PAGINATION_SIZE, + }) + + const files = useGetAllMediaFiles( + mediaFolderFiles?.files || [], + siteName, + mediaDirectoryName + ) + + const methods = useForm({ + mode: "onTouched", + resolver: yupResolver(DirectorySettingsSchema(existingTitles)), + context: { + type: "mediaDirectoryName", + }, + defaultValues: { + newDirectoryName: "", + selectedPages: originalSelectedMedia, + }, + }) + + const handleSelect = (fileData: MediaData) => { + if ( + methods + .getValues("selectedPages") + .some((selectedData) => selectedData.filePath === fileData.mediaPath) + ) { + methods.setValue( + "selectedPages", + methods + .getValues("selectedPages") + .filter( + (selectedData) => selectedData.filePath !== fileData.mediaPath + ) + ) + } else { + const selectedData = getSelectedMediaDto(fileData) + methods.setValue("selectedPages", [ + ...methods.getValues("selectedPages"), + selectedData, + ]) + } + } + + useEffect(() => { + methods.setValue("selectedPages", originalSelectedMedia) + }, [methods, originalSelectedMedia]) + + const onModalClose = () => { + methods.reset() + onClose() + } + + const onSubmit = async (data: MediaFolderCreationInfo) => { + await onProceed(data) + methods.reset() + } + + return ( + + + +
+ + + + + + Create a new {singularDirectoryLabel} + {originalSelectedMedia.length > 0 + ? ` with ${originalSelectedMedia.length} ${ + originalSelectedMedia.length === 1 + ? singularMediaLabel + : pluralMediaLabel + }` + : ""} + + + + + + + {_.upperFirst(singularDirectoryLabel)} name + + + + {methods.formState.errors.newDirectoryName?.message} + + + + {originalSelectedMedia.length === 0 && ( + <> + + Select {pluralMediaLabel} to add to this{" "} + {singularDirectoryLabel} + + + + Click ‘Skip, I’ll add {pluralMediaLabel} later’ to create an + empty {singularDirectoryLabel}. You can add{" "} + {pluralMediaLabel} to the {singularDirectoryLabel} later. + + + + {files.length > 0 && ( + + + {files.map(({ data }) => { + return data && mediaType === "images" ? ( + + selectedData.filePath === data.mediaPath + )} + isMenuNeeded={false} + onClick={() => handleSelect(data)} + onCheck={() => handleSelect(data)} + /> + ) : ( + data && ( + + selectedData.filePath === data.mediaPath + )} + isMenuNeeded={false} + onClick={() => handleSelect(data)} + onCheck={() => handleSelect(data)} + /> + ) + ) + })} + + + {/* Pagination segment */} + {mediaFolderFiles?.total !== 0 && ( +
+ setCurPage(page)} + /> +
+ )} +
+ )} +
+ + )} +
+ + + + {originalSelectedMedia.length > 0 && ( + + )} + {originalSelectedMedia.length === 0 && ( + + )} + + + +
+
+
+
+ ) +} diff --git a/src/components/CreateMediaFolderModal/index.ts b/src/components/CreateMediaFolderModal/index.ts new file mode 100644 index 000000000..3bba53832 --- /dev/null +++ b/src/components/CreateMediaFolderModal/index.ts @@ -0,0 +1 @@ +export * from "./CreateMediaFolderModal" diff --git a/src/components/DeleteMediaModal/DeleteMediaModal.tsx b/src/components/DeleteMediaModal/DeleteMediaModal.tsx index f4ffd0642..320ba4c1c 100644 --- a/src/components/DeleteMediaModal/DeleteMediaModal.tsx +++ b/src/components/DeleteMediaModal/DeleteMediaModal.tsx @@ -64,7 +64,7 @@ export const DeleteMediaModal = ({ {selectedMedia.length === 1 && ( <> - + Delete {selectedMedia[0].filePath.split("/").pop()}? diff --git a/src/components/DirectorySettingsModal/DirectorySettingsSchema.jsx b/src/components/DirectorySettingsModal/DirectorySettingsSchema.jsx index b5740fc14..ffd3099e1 100644 --- a/src/components/DirectorySettingsModal/DirectorySettingsSchema.jsx +++ b/src/components/DirectorySettingsModal/DirectorySettingsSchema.jsx @@ -78,4 +78,5 @@ export const DirectorySettingsSchema = (existingTitlesArray = []) => () => false ) }), + selectedPages: Yup.array().ensure(), }) diff --git a/src/components/ImagePreviewCard/ImagePreviewCard.tsx b/src/components/ImagePreviewCard/ImagePreviewCard.tsx index 3a10facbe..bb8774356 100644 --- a/src/components/ImagePreviewCard/ImagePreviewCard.tsx +++ b/src/components/ImagePreviewCard/ImagePreviewCard.tsx @@ -5,12 +5,13 @@ import { Grid, GridItem, Image, + ImageProps, Text, useMultiStyleConfig, VStack, } from "@chakra-ui/react" import { Checkbox } from "@opengovsg/design-system-react" -import { BiEditAlt, BiTrash } from "react-icons/bi" +import { BiEditAlt, BiFolder, BiTrash } from "react-icons/bi" import { Link as RouterLink, useRouteMatch } from "react-router-dom" import { ContextMenu } from "components/ContextMenu" @@ -25,11 +26,14 @@ export interface ImagePreviewCardProps { name: string addedTime: number mediaUrl: string + imageHeight?: ImageProps["height"] isSelected: boolean isMenuNeeded?: boolean onOpen?: () => void + onClick?: () => void onCheck?: () => void onDelete?: () => void + onMove?: () => void } // Note: This is written as a separate component as the current Card API is not @@ -38,11 +42,14 @@ export const ImagePreviewCard = ({ name, addedTime, mediaUrl, + imageHeight = "15rem", isSelected, isMenuNeeded = true, onOpen, + onClick, onCheck, onDelete, + onMove, }: ImagePreviewCardProps): JSX.Element => { const { url } = useRouteMatch() const { setRedirectToPage } = useRedirectHook() @@ -91,9 +98,18 @@ export const ImagePreviewCard = ({ // Note: Outline is required to avoid the card from shifting when selected outline={isSelected ? "solid 2px" : "solid 1px"} outlineColor={isSelected ? "base.divider.brand" : "base.divider.medium"} - onClick={() => - setRedirectToPage(`${url}/editMediaSettings/${encodedName}`) - } + onClick={(e) => { + // For some weird reason, the onClick event is treated as a submit event + // We can safely disable the default behaviour here since we define the + // onClick behaviour ourselves + e.preventDefault() + + if (onClick) { + onClick() + } else { + setRedirectToPage(`${url}/editMediaSettings/${encodedName}`) + } + }} > @@ -113,7 +129,7 @@ export const ImagePreviewCard = ({
Rename image + } onClick={onMove}> + Move to + } color="interaction.critical.default" diff --git a/src/components/MediaCreationModal/MediaCreationModal.tsx b/src/components/MediaCreationModal/MediaCreationModal.tsx index b3725ed10..cd7ef8a1a 100644 --- a/src/components/MediaCreationModal/MediaCreationModal.tsx +++ b/src/components/MediaCreationModal/MediaCreationModal.tsx @@ -17,6 +17,7 @@ import { Flex, } from "@chakra-ui/react" import { Button, Infobox } from "@opengovsg/design-system-react" +import _ from "lodash" import { useEffect, useState } from "react" import { FileRejection } from "react-dropzone" import { BiCheckCircle, BiSolidErrorCircle } from "react-icons/bi" @@ -26,6 +27,8 @@ import { Attachment } from "components/Attachment" import { useCreateMultipleMedia } from "hooks/mediaHooks/useCreateMultipleMedia" +import { getMediaLabels } from "utils/media" + import { MediaDirectoryParams } from "types/folders" import { MediaFolderTypes } from "types/media" import { MEDIA_FILE_MAX_SIZE } from "utils" @@ -67,15 +70,16 @@ const MediaDropzone = ({ mediaType, }: MediaDropzoneProps) => { const { onClose } = useModalContext() + const { singularMediaLabel, pluralMediaLabel } = getMediaLabels(mediaType) return ( <> - Upload files + Upload {pluralMediaLabel} - You can upload more than 1 file at once. Having too many files can - slow down the site loading time, so we recommend only uploading - necessary files to your site. + You can upload more than 1 {singularMediaLabel} at once. Having too + many {pluralMediaLabel} can slow down the site loading time, so we + recommend only uploading necessary {pluralMediaLabel} to your site. Cancel - + ) @@ -112,17 +115,20 @@ const MediaDropzone = ({ interface UploadProgressIndicatorProps { cur: number total: number + mediaType: MediaFolderTypes } const UploadProgressIndicator = ({ cur, total, + mediaType, }: UploadProgressIndicatorProps) => { const { onClose } = useModalContext() + const { pluralMediaLabel } = getMediaLabels(mediaType) return ( <> - Upload files + Upload {pluralMediaLabel} {`Uploading ${cur} of ${total} files`} + >{`Uploading ${cur} of ${total} ${pluralMediaLabel}`} Do not close this screen or navigate away @@ -152,16 +158,26 @@ const UploadProgressIndicator = ({ interface MediaUploadSuccessDropzoneProps { numMedia: number errorMessages: string[] + mediaType: MediaFolderTypes } const MediaUploadSuccessDropzone = ({ numMedia, errorMessages, + mediaType, }: MediaUploadSuccessDropzoneProps) => { const { onClose } = useModalContext() + const { + singularMediaLabel, + pluralMediaLabel, + singularDirectoryLabel, + } = getMediaLabels(mediaType) return ( <> - Files uploaded! + + {_.upperFirst(numMedia === 1 ? singularMediaLabel : pluralMediaLabel)}{" "} + uploaded! + - {`Successfully uploaded ${numMedia} files`} + + {`Successfully uploaded ${numMedia} ${ + numMedia === 1 ? singularMediaLabel : pluralMediaLabel + }`} + {errorMessages.length > 0 && ( <> @@ -187,7 +204,7 @@ const MediaUploadSuccessDropzone = ({ mr="0.5rem" /> - {`${errorMessages.length} files failed to upload`} + {`${errorMessages.length} ${pluralMediaLabel} failed to upload`} @@ -203,7 +220,7 @@ const MediaUploadSuccessDropzone = ({ )} - + ) @@ -211,11 +228,18 @@ const MediaUploadSuccessDropzone = ({ interface MediaUploadFailedDropzoneProps { errorMessages: string[] + mediaType: MediaFolderTypes } const MediaUploadFailedDropzone = ({ errorMessages, + mediaType, }: MediaUploadFailedDropzoneProps) => { const { onClose } = useModalContext() + const { + singularMediaLabel, + pluralMediaLabel, + singularDirectoryLabel, + } = getMediaLabels(mediaType) return ( <> @@ -225,7 +249,9 @@ const MediaUploadFailedDropzone = ({ {`${errorMessages.length} ${ - errorMessages.length > 1 ? "files" : "file" + errorMessages.length === 1 + ? singularMediaLabel + : pluralMediaLabel } failed to upload`} @@ -247,7 +273,7 @@ const MediaUploadFailedDropzone = ({ - + ) @@ -334,16 +360,21 @@ export const MediaCreationModal = ({ )} {curStep === "success" && ( )} {curStep === "failed" && ( - + )} diff --git a/src/components/MoveMediaModal/MoveMediaModal.tsx b/src/components/MoveMediaModal/MoveMediaModal.tsx index 67fe270b3..afcf5b8d6 100644 --- a/src/components/MoveMediaModal/MoveMediaModal.tsx +++ b/src/components/MoveMediaModal/MoveMediaModal.tsx @@ -118,7 +118,7 @@ export const MoveMediaModal = ({ - + Move{" "} {selectedMedia.length === 1 ? getLastChildOfPath(selectedMedia[0].filePath) diff --git a/src/hooks/directoryHooks/useCreateDirectoryAndMoveFilesHook.ts b/src/hooks/directoryHooks/useCreateDirectoryAndMoveFilesHook.ts new file mode 100644 index 000000000..3a7c2925f --- /dev/null +++ b/src/hooks/directoryHooks/useCreateDirectoryAndMoveFilesHook.ts @@ -0,0 +1,101 @@ +import { AxiosError } from "axios" +import { + useMutation, + UseMutationOptions, + UseMutationResult, + useQueryClient, +} from "react-query" + +import { + LIST_MEDIA_DIRECTORY_FILES_KEY, + GET_ALL_MEDIA_FILES_KEY, + LIST_MEDIA_FOLDERS_KEY, +} from "constants/queryKeys" + +import { moveMultipleMedia } from "hooks/moveHooks" + +import { apiService } from "services/ApiService" + +import { DirectoryService } from "services" +import { MiddlewareError } from "types/error" +import { MediaDirectoryParams } from "types/folders" +import { MediaFolderCreationInfo } from "types/media" + +const createDirectoryAndMoveFiles = async ( + params: MediaDirectoryParams, + { newDirectoryName, selectedPages }: MediaFolderCreationInfo +) => { + const directoryService = new DirectoryService({ apiClient: apiService }) + const { siteName, mediaDirectoryName } = params + + const newDirectoryPath = [ + decodeURIComponent(mediaDirectoryName), + newDirectoryName, + ].join("/") + + if (selectedPages.length === 0) { + return directoryService.create( + { siteName, mediaDirectoryName, isCreate: true }, + { newDirectoryName: newDirectoryPath, items: [] } + ) + } + + return ( + directoryService + .create( + { siteName, mediaDirectoryName, isCreate: true }, + { newDirectoryName: newDirectoryPath, items: [] } + ) + // This wait is necessary to avoid the repo lock + .then(() => new Promise((resolve) => setTimeout(resolve, 500))) + .then(() => { + moveMultipleMedia(params, { + target: { directoryName: newDirectoryPath }, + items: selectedPages, + }) + }) + // This wait is necessary to allow the backend to catch up + .then(() => new Promise((resolve) => setTimeout(resolve, 500))) + ) +} + +export const useCreateDirectoryAndMoveFilesHook = ( + params: MediaDirectoryParams, + mutationOptions?: Omit< + UseMutationOptions< + void, + AxiosError, + MediaFolderCreationInfo + >, + "mutationFn" | "mutationKey" + > +): UseMutationResult< + void, + AxiosError, + MediaFolderCreationInfo +> => { + const queryClient = useQueryClient() + + return useMutation< + void, + AxiosError, + MediaFolderCreationInfo + >((data) => createDirectoryAndMoveFiles(params, data), { + ...mutationOptions, + onSettled: (data, error, variables, context) => { + queryClient.invalidateQueries([LIST_MEDIA_DIRECTORY_FILES_KEY]) + queryClient.invalidateQueries([GET_ALL_MEDIA_FILES_KEY]) + queryClient.invalidateQueries([LIST_MEDIA_FOLDERS_KEY]) + if (mutationOptions?.onSettled) + mutationOptions.onSettled(data, error, variables, context) + }, + onSuccess: (data, variables, context) => { + if (mutationOptions?.onSuccess) + mutationOptions.onSuccess(data, variables, context) + }, + onError: (err, variables, context) => { + if (mutationOptions?.onError) + mutationOptions.onError(err, variables, context) + }, + }) +} diff --git a/src/hooks/moveHooks/useMoveMultipleMediaHook.tsx b/src/hooks/moveHooks/useMoveMultipleMediaHook.tsx index ffe6e3967..51aa03954 100644 --- a/src/hooks/moveHooks/useMoveMultipleMediaHook.tsx +++ b/src/hooks/moveHooks/useMoveMultipleMediaHook.tsx @@ -19,7 +19,7 @@ import { MiddlewareError } from "types/error" import { MediaDirectoryParams } from "types/folders" import { MoveMultipleMediaDto, MoveSelectedMediaDto } from "types/media" -const moveMultipleMedia = async ( +export const moveMultipleMedia = async ( { siteName }: MediaDirectoryParams, { target, items }: MoveSelectedMediaDto ) => { diff --git a/src/layouts/Media/Media.tsx b/src/layouts/Media/Media.tsx index a9376fa94..0eb0a0332 100644 --- a/src/layouts/Media/Media.tsx +++ b/src/layouts/Media/Media.tsx @@ -19,9 +19,10 @@ import { } from "@opengovsg/design-system-react" import _ from "lodash" import { useEffect, useState } from "react" -import { BiFolderOpen, BiTrash } from "react-icons/bi" +import { BiFolderOpen, BiFolderPlus, BiTrash } from "react-icons/bi" import { Link, Switch, useHistory, useRouteMatch } from "react-router-dom" +import { CreateMediaFolderModal } from "components/CreateMediaFolderModal" import { DeleteMediaModal } from "components/DeleteMediaModal" import { Greyscale } from "components/Greyscale" import { ImagePreviewCard } from "components/ImagePreviewCard" @@ -29,12 +30,14 @@ import { MoveMediaModal } from "components/MoveMediaModal" import { MAX_MEDIA_LEVELS, MEDIA_PAGINATION_SIZE } from "constants/media" +import { useCreateDirectoryAndMoveFilesHook } from "hooks/directoryHooks/useCreateDirectoryAndMoveFilesHook" import { useGetAllMediaFiles } from "hooks/directoryHooks/useGetAllMediaFiles" import { useListMediaFolderFiles } from "hooks/directoryHooks/useListMediaFolderFiles" import { useListMediaFolderSubdirectories } from "hooks/directoryHooks/useListMediaFolderSubdirectories" import { useDeleteMultipleMediaHook } from "hooks/mediaHooks" import { useMoveMultipleMediaHook } from "hooks/moveHooks" import { usePaginate } from "hooks/usePaginate" +import useRedirectHook from "hooks/useRedirectHook" import { DeleteWarningScreen } from "layouts/screens/DeleteWarningScreen" import { DirectoryCreationScreen } from "layouts/screens/DirectoryCreationScreen" @@ -44,7 +47,7 @@ import { MediaSettingsScreen } from "layouts/screens/MediaSettingsScreen" import { ProtectedRouteWithProps } from "routing/ProtectedRouteWithProps" -import { getMediaLabels } from "utils/media" +import { getMediaLabels, getSelectedMediaDto } from "utils/media" import { isWriteActionsDisabled } from "utils/reviewRequests" import { EmptyAlbumImage, EmptyDirectoryImage } from "assets" @@ -65,7 +68,7 @@ interface CreateDirectoryButtonProps { isWriteDisabled: boolean | undefined directoryLevel: number singularDirectoryLabel: string - url: string + onOpen: () => void } // Utility method for getting the text under the page title in the header @@ -115,27 +118,15 @@ const getPlaceholderText = ( return results.join(" ") } -// Utility method to construct a SelectedMediaDto from MediaData -const getSelectedMediaDto = (fileData: MediaData) => { - const selectedData: SelectedMediaDto = { - filePath: fileData.mediaPath, - sha: fileData.sha, - size: fileData.size || 0, - } - - return selectedData -} - const CreateDirectoryButton = ({ isWriteDisabled, directoryLevel, singularDirectoryLabel, - url, + onOpen, }: CreateDirectoryButtonProps) => ( = MAX_MEDIA_LEVELS} > {`Create ${singularDirectoryLabel}`} @@ -151,6 +142,7 @@ export const Media = (): JSX.Element => { mediaRoom: MediaFolderTypes mediaDirectoryName: string }>() + const { setRedirectToPage } = useRedirectHook() const { siteName, mediaRoom: mediaType, mediaDirectoryName } = params const [selectedMedia, setSelectedMedia] = useState([]) // Note: We need a separate variable here so that we do not lose the selection @@ -161,6 +153,12 @@ export const Media = (): JSX.Element => { setIndividualMedia, ] = useState(null) + const { + isOpen: isCreateMediaFolderModalOpen, + onOpen: onCreateMediaFolderModalOpen, + onClose: onCreateMediaFolderModalClose, + } = useDisclosure() + const { isOpen: isMoveModalOpen, onOpen: onMoveModalOpen, @@ -248,6 +246,43 @@ export const Media = (): JSX.Element => { params.mediaDirectoryName ) + const { + mutateAsync: createDirectoryAndMoveFiles, + isLoading: isCreateDirectoryAndMoveFilesLoading, + } = useCreateDirectoryAndMoveFilesHook(params, { + onSettled: () => { + setSelectedMedia([]) + onCreateMediaFolderModalClose() + }, + onSuccess: (data, variables, context) => { + if (variables.selectedPages.length === 0) { + successToast({ + id: "create-directory-success", + description: `Successfully created ${singularDirectoryLabel}!`, + }) + } else { + successToast({ + id: "create-directory-and-move-files-success", + description: `Successfully created ${singularDirectoryLabel} and moved ${ + variables.selectedPages.length === 1 + ? singularMediaLabel + : pluralMediaLabel + }!`, + }) + } + + setRedirectToPage( + `${url}%2F${encodeURIComponent(variables.newDirectoryName)}` + ) + }, + onError: (err, variables, context) => { + errorToast({ + id: "create-directory-error", + description: `Your ${singularDirectoryLabel} could not be created successfully. ${DEFAULT_RETRY_MSG}`, + }) + }, + }) + const { mutate: moveMultipleMedia, isLoading: isMoveMultipleMediaLoading, @@ -319,6 +354,18 @@ export const Media = (): JSX.Element => { return ( <> + + { onMoveModalOpen()}> - Move images to album + Move{" "} + {selectedMedia.length === 1 + ? singularMediaLabel + : pluralMediaLabel}{" "} + to {singularDirectoryLabel} - {/* FIXME: To add back when flow is available - - - Create new album with images - */} + + {directoryLevel < MAX_MEDIA_LEVELS && ( + onCreateMediaFolderModalOpen()} + > + + Create new {singularDirectoryLabel} with{" "} + {selectedMedia.length === 1 + ? singularMediaLabel + : pluralMediaLabel} + + )} + onDeleteModalOpen()} @@ -460,7 +519,7 @@ export const Media = (): JSX.Element => { isWriteDisabled={isWriteDisabled} directoryLevel={directoryLevel} singularDirectoryLabel={singularDirectoryLabel} - url={url} + onOpen={onCreateMediaFolderModalOpen} /> ) : ( @@ -468,7 +527,7 @@ export const Media = (): JSX.Element => { isWriteDisabled={isWriteDisabled} directoryLevel={directoryLevel} singularDirectoryLabel={singularDirectoryLabel} - url={url} + onOpen={onCreateMediaFolderModalOpen} /> )} @@ -581,14 +640,20 @@ export const Media = (): JSX.Element => { } onCheck={() => handleSelect(data)} onDelete={onDeleteModalOpen} + onMove={onMoveModalOpen} /> ) : ( data && ( + selectedData.filePath === data.mediaPath + )} onOpen={() => setIndividualMedia(getSelectedMediaDto(data)) } + onCheck={() => handleSelect(data)} onDelete={onDeleteModalOpen} onMove={onMoveModalOpen} /> @@ -626,11 +691,6 @@ export const Media = (): JSX.Element => { component={MediaSettingsScreen} onClose={() => history.goBack()} /> - history.goBack()} - /> void + onClick?: () => void + onCheck?: () => void onDelete?: () => void onMove?: () => void } export const FilePreviewCard = ({ name, + isSelected, + isMenuNeeded = true, onOpen, + onClick, + onCheck, onDelete, onMove, }: FilePreviewCardProps): JSX.Element => { const { url } = useRouteMatch() + const { setRedirectToPage } = useRedirectHook() const encodedName = encodeURIComponent(name) return ( - - - - - - - - {name} - - - - - - - - - } - as={RouterLink} - to={`${url}/editMediaSettings/${encodedName}`} - > - Edit details - - } onClick={onMove}> - - Move to - - - - <> - + + {/* Checkbox overlay */} + { + if (onCheck) onCheck() + }} + /> + + { + // For some weird reason, the onClick event is treated as a submit event + // We can safely disable the default behaviour here since we define the + // onClick behaviour ourselves + e.preventDefault() + + if (onClick) { + onClick() + } else { + setRedirectToPage(`${url}/editMediaSettings/${encodedName}`) + } + }} + > + + + {!isSelected && ( + + )} + + {name} + + + + + + {isMenuNeeded && ( + + + } - color="interaction.critical.default" - onClick={onDelete} + icon={} + as={RouterLink} + to={`${url}/editMediaSettings/${encodedName}`} > - Delete file + Edit details + + } onClick={onMove}> + + Move to + + - - - + <> + + } + color="interaction.critical.default" + onClick={onDelete} + > + Delete file + + + + + )} ) } diff --git a/src/types/media.ts b/src/types/media.ts index 6796791aa..4bb24a6f8 100644 --- a/src/types/media.ts +++ b/src/types/media.ts @@ -36,3 +36,8 @@ export interface MoveMultipleMediaDto { target: { directoryName: string } items: Array<{ name: string; type: "file" }> } + +export interface MediaFolderCreationInfo { + newDirectoryName: string + selectedPages: SelectedMediaDto[] +} diff --git a/src/utils/media.ts b/src/utils/media.ts index 84867f191..5653073ea 100644 --- a/src/utils/media.ts +++ b/src/utils/media.ts @@ -1,4 +1,5 @@ -import { MediaLabels } from "types/media" +import { MediaData } from "types/directory" +import { MediaLabels, SelectedMediaDto } from "types/media" // Utility method to help ease over the various labels associated // with the media type so that we can avoid repeated conditionals @@ -21,3 +22,14 @@ export const getMediaLabels = (mediaType: "files" | "images"): MediaLabels => { pluralDirectoryLabel: "albums", } } + +// Utility method to construct a SelectedMediaDto from MediaData +export const getSelectedMediaDto = (fileData: MediaData) => { + const selectedData: SelectedMediaDto = { + filePath: fileData.mediaPath, + sha: fileData.sha, + size: fileData.size || 0, + } + + return selectedData +} From d694c190e5d5bd5c89ae39eb00e1cc318edb7564 Mon Sep 17 00:00:00 2001 From: Hsu Zhong Jun <27919917+dcshzj@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:52:49 +0800 Subject: [PATCH 10/13] feat(media): add announcements and feature tour for media enhancements (#1632) * feat(media): add announcements and feature tour for media enhancements * feat(media): add announcement image * chore(media): adjust to use feature tour target IDs directly * fix(assets): fix exports of images --- .../images/BulkUploadAnnouncementImage.tsx | 507 ++++++++++++++++++ src/assets/images/index.ts | 1 + src/constants/localStorage.ts | 2 + .../AnnouncementModal.stories.tsx | 9 +- .../AnnouncementModal/AnnouncementModal.tsx | 7 +- .../AnnouncementModal/Announcements.ts | 20 +- .../FeatureTour/FeatureTourSequence.ts | 28 + src/layouts/Media/Media.tsx | 45 +- 8 files changed, 610 insertions(+), 9 deletions(-) create mode 100644 src/assets/images/BulkUploadAnnouncementImage.tsx diff --git a/src/assets/images/BulkUploadAnnouncementImage.tsx b/src/assets/images/BulkUploadAnnouncementImage.tsx new file mode 100644 index 000000000..5dba66dfc --- /dev/null +++ b/src/assets/images/BulkUploadAnnouncementImage.tsx @@ -0,0 +1,507 @@ +export const BulkUploadAnnouncementImage = ( + props: React.SVGProps +): JSX.Element => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/assets/images/index.ts b/src/assets/images/index.ts index 8781b7240..30ca9f72a 100644 --- a/src/assets/images/index.ts +++ b/src/assets/images/index.ts @@ -25,3 +25,4 @@ export * from "./HomepageNewFeatures" export * from "./SingpassLogo" export * from "./EmptyAlbumImage" export * from "./EmptyDirectoryImage" +export * from "./BulkUploadAnnouncementImage" diff --git a/src/constants/localStorage.ts b/src/constants/localStorage.ts index 9a99255e6..50c589cc2 100644 --- a/src/constants/localStorage.ts +++ b/src/constants/localStorage.ts @@ -4,6 +4,8 @@ export enum LOCAL_STORAGE_KEYS { Email = "email", Announcements = "announcements", DashboardFeatureTour = "dashboard-identity-feature-tour-v1", + MediaFeatureTour = "media-identity-feature-tour-v1", + MediaOnSelectFeatureTour = "media-onselect-identity-feature-tour-v1", WorkspaceFeatureTour = "workspace-identity-feature-tour-v1", Feedback = "feedback", HeroOptionsFeatureTour = "hero-options-feature-tour-v1", diff --git a/src/features/AnnouncementModal/AnnouncementModal.stories.tsx b/src/features/AnnouncementModal/AnnouncementModal.stories.tsx index 2ad261aa6..14854a8db 100644 --- a/src/features/AnnouncementModal/AnnouncementModal.stories.tsx +++ b/src/features/AnnouncementModal/AnnouncementModal.stories.tsx @@ -30,10 +30,17 @@ SiteCollaboratorsAnnouncement.args = { } export const HeroBannerNewFeaturesAnnouncement = Template.bind({}) - HeroBannerNewFeaturesAnnouncement.args = { onClose, isOpen: true, announcements: ANNOUNCEMENT_BATCH[1].announcements, onCloseButtonText: ANNOUNCEMENT_BATCH[1].onCloseButtonText, } + +export const BulkUploadingAnnouncement = Template.bind({}) +BulkUploadingAnnouncement.args = { + onClose, + isOpen: true, + announcements: ANNOUNCEMENT_BATCH[2].announcements, + onCloseButtonText: ANNOUNCEMENT_BATCH[2].onCloseButtonText, +} diff --git a/src/features/AnnouncementModal/AnnouncementModal.tsx b/src/features/AnnouncementModal/AnnouncementModal.tsx index 1b773a809..d84258d7f 100644 --- a/src/features/AnnouncementModal/AnnouncementModal.tsx +++ b/src/features/AnnouncementModal/AnnouncementModal.tsx @@ -89,7 +89,12 @@ export const AnnouncementModal = ({ bg="base.canvas.brandLight" borderRadius="4px" /> - + {tags.map((tagVariant) => { switch (tagVariant) { diff --git a/src/features/AnnouncementModal/Announcements.ts b/src/features/AnnouncementModal/Announcements.ts index 851eae620..ac416b6c7 100644 --- a/src/features/AnnouncementModal/Announcements.ts +++ b/src/features/AnnouncementModal/Announcements.ts @@ -1,5 +1,9 @@ -import { HomepageNewFeatures, IsomerThumbsUp } from "assets" -import { IsomerWaitingLine } from "assets/images/IsomerWaitingLine" +import { + HomepageNewFeatures, + IsomerThumbsUp, + IsomerWaitingLine, + BulkUploadAnnouncementImage, +} from "assets" import { AnnouncementBatch } from "types/announcements" import { AnnouncementDescription } from "./components/AnnouncementDescription" @@ -36,4 +40,16 @@ export const ANNOUNCEMENT_BATCH: AnnouncementBatch[] = [ }, ], }, + { + onCloseButtonText: "Got it", + announcements: [ + { + title: "Bulk image uploads are here!", + description: + "Now, you can upload multiple images at once. To ensure that your site runs fast, we recommend only uploading images that you need for your Isomer site.", + image: BulkUploadAnnouncementImage, + tags: ["New Feature"], + }, + ], + }, ] diff --git a/src/features/FeatureTour/FeatureTourSequence.ts b/src/features/FeatureTour/FeatureTourSequence.ts index 91ddb3d7d..89e3f9b3c 100644 --- a/src/features/FeatureTour/FeatureTourSequence.ts +++ b/src/features/FeatureTour/FeatureTourSequence.ts @@ -31,6 +31,34 @@ export const DASHBOARD_FEATURE_STEPS: Array = [ }, ] +export const MEDIA_FEATURE_STEPS: Array = [ + { + target: "#isomer-media-feature-tour-step-1", + title: "Upload more than 1 image at a time", + content: "Click ‘Upload images’ to add multiple images to your albums.", + floaterProps: { placement: "bottom-end" }, + placement: "bottom-end", + }, + { + target: "#isomer-media-feature-tour-step-2", + title: "Select multiple images and organise them", + content: "Now, you can select multiple images by clicking on them.", + floaterProps: { placement: "top-end" }, + placement: "top-end", + }, +] + +export const MEDIA_ONSELECT_FEATURE_STEPS: Array = [ + { + target: "#isomer-media-onselect-feature-tour-step-1", + title: "Bulk actions can be found here", + content: + "You can create a new folder with your selected images or move them to a different folder! Be careful when you’re deleting multiple images though.", + floaterProps: { placement: "bottom-end" }, + placement: "bottom-end", + }, +] + export const WORKSPACE_FEATURE_STEPS: Array = [ { target: "#isomer-workspace-feature-tour-step-1", diff --git a/src/layouts/Media/Media.tsx b/src/layouts/Media/Media.tsx index 0eb0a0332..d2ad8e525 100644 --- a/src/layouts/Media/Media.tsx +++ b/src/layouts/Media/Media.tsx @@ -28,7 +28,8 @@ import { Greyscale } from "components/Greyscale" import { ImagePreviewCard } from "components/ImagePreviewCard" import { MoveMediaModal } from "components/MoveMediaModal" -import { MAX_MEDIA_LEVELS, MEDIA_PAGINATION_SIZE } from "constants/media" +import { LOCAL_STORAGE_KEYS } from "constants/localStorage" +import { MEDIA_PAGINATION_SIZE, MAX_MEDIA_LEVELS } from "constants/media" import { useCreateDirectoryAndMoveFilesHook } from "hooks/directoryHooks/useCreateDirectoryAndMoveFilesHook" import { useGetAllMediaFiles } from "hooks/directoryHooks/useGetAllMediaFiles" @@ -40,7 +41,6 @@ import { usePaginate } from "hooks/usePaginate" import useRedirectHook from "hooks/useRedirectHook" import { DeleteWarningScreen } from "layouts/screens/DeleteWarningScreen" -import { DirectoryCreationScreen } from "layouts/screens/DirectoryCreationScreen" import { DirectorySettingsScreen } from "layouts/screens/DirectorySettingsScreen" import { MediaCreationScreen } from "layouts/screens/MediaCreationScreen" import { MediaSettingsScreen } from "layouts/screens/MediaSettingsScreen" @@ -51,6 +51,11 @@ import { getMediaLabels, getSelectedMediaDto } from "utils/media" import { isWriteActionsDisabled } from "utils/reviewRequests" import { EmptyAlbumImage, EmptyDirectoryImage } from "assets" +import { FeatureTourHandler } from "features/FeatureTour/FeatureTour" +import { + MEDIA_FEATURE_STEPS, + MEDIA_ONSELECT_FEATURE_STEPS, +} from "features/FeatureTour/FeatureTourSequence" import { MediaData } from "types/directory" import { MediaFolderTypes, MediaLabels, SelectedMediaDto } from "types/media" import { DEFAULT_RETRY_MSG, useErrorToast, useSuccessToast } from "utils" @@ -396,6 +401,21 @@ export const Media = (): JSX.Element => { ) }} /> + {(subDirCount !== 0 || filesCount !== 0) && + selectedMedia.length === 0 && ( + + )} + + {(subDirCount !== 0 || filesCount !== 0) && + selectedMedia.length !== 0 && ( + + )} @@ -433,7 +453,12 @@ export const Media = (): JSX.Element => { Deselect all - + setIndividualMedia(null)} @@ -531,7 +556,12 @@ export const Media = (): JSX.Element => { /> )} - +
) : ( <> - + {files.map(({ data, isLoading }) => { return ( // NOTE: Inner skeleton here is to allow for progressive load. From 44ea8f8360d17d5f31f58ad67ba0398be9f72115 Mon Sep 17 00:00:00 2001 From: Kishore <42832651+kishore03109@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:57:14 +0800 Subject: [PATCH 11/13] feat/addReportingBUtton (#1674) --- src/components/Header/AllSitesHeader.tsx | 14 +++++++++++++- src/constants/config.ts | 1 + .../layouts/SiteViewLayout/SiteViewHeader.tsx | 16 ++++++++++++++-- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/components/Header/AllSitesHeader.tsx b/src/components/Header/AllSitesHeader.tsx index dd93cae31..8c08eff11 100644 --- a/src/components/Header/AllSitesHeader.tsx +++ b/src/components/Header/AllSitesHeader.tsx @@ -10,7 +10,7 @@ import { import { AvatarMenu } from "components/Header/AvatarMenu" -import { ISOMER_GUIDE_LINK } from "constants/config" +import { ISOMER_GUIDE_LINK, ISOMER_REPORT_ISSUE_LINK } from "constants/config" import { useLoginContext } from "contexts/LoginContext" @@ -45,6 +45,18 @@ export const AllSitesHeader = (): JSX.Element => {
+ + + + Report issue + + +
diff --git a/src/constants/config.ts b/src/constants/config.ts index f88ea8366..138cbac1e 100644 --- a/src/constants/config.ts +++ b/src/constants/config.ts @@ -6,3 +6,4 @@ export const PRIVACY_POLICY_LINK = "https://guide.isomer.gov.sg/terms-and-privacy/privacy-statement" export const ISOMER_GUIDE_LINK = "https://guide.isomer.gov.sg/" export const IDENTITY_GUIDE_LINK = "https://go.gov.sg/isomer-identity" +export const ISOMER_REPORT_ISSUE_LINK = "https://go.gov.sg/isomer-issue" diff --git a/src/layouts/layouts/SiteViewLayout/SiteViewHeader.tsx b/src/layouts/layouts/SiteViewLayout/SiteViewHeader.tsx index 9c20d9e98..ded08b589 100644 --- a/src/layouts/layouts/SiteViewLayout/SiteViewHeader.tsx +++ b/src/layouts/layouts/SiteViewLayout/SiteViewHeader.tsx @@ -17,7 +17,7 @@ import { AvatarMenu } from "components/Header/AvatarMenu" import { NotificationMenu } from "components/Header/NotificationMenu" import { StatusBadge } from "components/Header/StatusBadge" -import { ISOMER_GUIDE_LINK } from "constants/config" +import { ISOMER_GUIDE_LINK, ISOMER_REPORT_ISSUE_LINK } from "constants/config" import { FEATURE_FLAGS } from "constants/featureFlags" import { useLoginContext } from "contexts/LoginContext" @@ -59,7 +59,7 @@ export const SiteViewHeader = (): JSX.Element => {
- + ({isShowStagingBuildStatusEnabled && }) @@ -73,6 +73,18 @@ export const SiteViewHeader = (): JSX.Element => { + + + + Report issue + + + From e1418301fcead7fee064622e3a7c693aab5888f0 Mon Sep 17 00:00:00 2001 From: seaerchin <44049504+seaerchin@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:57:43 +0800 Subject: [PATCH 12/13] fix(markdown): update styling to remove overflow (#1671) * fix(template): u9pdate styling for editor * refactor(editor): update styling to own file --- src/components/pages/MarkdownEditor.jsx | 2 ++ src/components/pages/editor.scss | 5 +++++ src/styles/isomer-cms/pages/Editor.module.scss | 1 + src/styles/isomer-template.scss | 7 +------ 4 files changed, 9 insertions(+), 6 deletions(-) create mode 100644 src/components/pages/editor.scss diff --git a/src/components/pages/MarkdownEditor.jsx b/src/components/pages/MarkdownEditor.jsx index 629aa6c1d..38dd11c53 100644 --- a/src/components/pages/MarkdownEditor.jsx +++ b/src/components/pages/MarkdownEditor.jsx @@ -7,6 +7,7 @@ import EditorModals from "components/pages/EditorModals" import { useMarkdown } from "hooks/useMarkdown" import editorStyles from "styles/isomer-cms/pages/Editor.module.scss" +import "./editor.scss" import { boldButton, @@ -137,6 +138,7 @@ const MarkdownEditor = ({ siteName, onChange, value, isLoading }) => { > Date: Tue, 14 Nov 2023 16:28:56 +0800 Subject: [PATCH 13/13] 0.58.0 --- CHANGELOG.md | 18 ++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f663310c..6eeacf2aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,26 @@ 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.58.0](https://github.com/isomerpages/isomercms-frontend/compare/v0.57.0...v0.58.0) + +- 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) +- feat(media): allow create album with selected media [`#1666`](https://github.com/isomerpages/isomercms-frontend/pull/1666) +- Fix/buildStatusBadgeAPI [`#1673`](https://github.com/isomerpages/isomercms-frontend/pull/1673) +- fix(preview): use site colours for headings in preview [`#1663`](https://github.com/isomerpages/isomercms-frontend/pull/1663) +- chore: update select media modal [`#1627`](https://github.com/isomerpages/isomercms-frontend/pull/1627) +- fix(image): search pagination [`#1668`](https://github.com/isomerpages/isomercms-frontend/pull/1668) +- fix(editor): change inner prosemirror stuff to have 100% height [`#1670`](https://github.com/isomerpages/isomercms-frontend/pull/1670) +- chore(login): automatically focus on input field [`#1664`](https://github.com/isomerpages/isomercms-frontend/pull/1664) +- refactor(editpage): add alt text modal [`#1665`](https://github.com/isomerpages/isomercms-frontend/pull/1665) +- refactor(tiptap-editor): update width [`#1667`](https://github.com/isomerpages/isomercms-frontend/pull/1667) +- 0.57.0 [`#1661`](https://github.com/isomerpages/isomercms-frontend/pull/1661) + #### [v0.57.0](https://github.com/isomerpages/isomercms-frontend/compare/v0.56.0...v0.57.0) +> 9 November 2023 + - fix(menubar): remove uuid [`#1659`](https://github.com/isomerpages/isomercms-frontend/pull/1659) - fix(badge): flickering issue [`#1657`](https://github.com/isomerpages/isomercms-frontend/pull/1657) - feat(images): bulk upload [`#1654`](https://github.com/isomerpages/isomercms-frontend/pull/1654) diff --git a/package-lock.json b/package-lock.json index 6613f291c..9d4768f6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "isomercms-frontend", - "version": "0.57.0", + "version": "0.58.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "isomercms-frontend", - "version": "0.57.0", + "version": "0.58.0", "hasInstallScript": true, "dependencies": { "@braintree/sanitize-url": "^6.0.1", diff --git a/package.json b/package.json index 970acca91..046dd4543 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "isomercms-frontend", - "version": "0.57.0", + "version": "0.58.0", "private": true, "engines": { "node": ">=16.0.0"