From 7256309c3279a12ce92fd598c50a05454e93847b Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Thu, 7 Nov 2024 15:37:55 +0800 Subject: [PATCH 001/104] make standalone UI work --- .../components/PageEditor/LinkEditorModal.tsx | 31 +-- .../ResourceSelector/ResourceItem.tsx | 133 +++++----- .../ResourceSelector/ResourceSelector.tsx | 246 ++++++++++-------- .../controls/JsonFormsLinkControl.tsx | 26 -- 4 files changed, 214 insertions(+), 222 deletions(-) diff --git a/apps/studio/src/components/PageEditor/LinkEditorModal.tsx b/apps/studio/src/components/PageEditor/LinkEditorModal.tsx index 42dfac4301..92695e9c2b 100644 --- a/apps/studio/src/components/PageEditor/LinkEditorModal.tsx +++ b/apps/studio/src/components/PageEditor/LinkEditorModal.tsx @@ -7,8 +7,7 @@ import { ModalContent, ModalFooter, ModalHeader, - ModalOverlay, - Text, + ModalOverlay } from "@chakra-ui/react" import { Button, @@ -25,7 +24,6 @@ import { LinkHrefEditor } from "~/features/editing-experience/components/LinkEdi import { useQueryParse } from "~/hooks/useQueryParse" import { useZodForm } from "~/lib/form" import { getReferenceLink, getResourceIdFromReferenceLink } from "~/utils/link" -import { trpc } from "~/utils/trpc" import { ResourceSelector } from "../ResourceSelector" import { FileAttachment } from "./FileAttachment" @@ -43,28 +41,13 @@ const PageLinkElement = ({ value, onChange }: PageLinkElementProps) => { const selectedResourceId = getResourceIdFromReferenceLink(value) - const { data: resource } = trpc.resource.getWithFullPermalink.useQuery({ - resourceId: selectedResourceId, - }) - return ( - <> - - onChange(getReferenceLink({ siteId: String(siteId), resourceId })) - } - selectedResourceId={selectedResourceId} - /> - - {!!resource && ( - - - You selected /{resource.fullPermalink} - - - )} - + + onChange(getReferenceLink({ siteId: String(siteId), resourceId })) + } + selectedResourceId={selectedResourceId} + /> ) } diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index ee8baa1a28..bee89b88d5 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -1,86 +1,95 @@ import type { IconType } from "react-icons" -import { Suspense, useMemo } from "react" -import { Box, HStack, Icon, Skeleton, Text } from "@chakra-ui/react" -import { dataAttr } from "@chakra-ui/utils" +import { Suspense } from "react" +import { Icon, Text } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" +import { ResourceType } from "@prisma/client" import { QueryErrorResetBoundary } from "@tanstack/react-query" -import { ResourceType } from "~prisma/generated/generatedEnums" import { ErrorBoundary } from "react-error-boundary" -import { BiData, BiFile, BiFolder, BiLink, BiLockAlt } from "react-icons/bi" +import { BiData, BiFile, BiFolder, BiLink } from "react-icons/bi" import type { RouterOutput } from "~/utils/trpc" type ResourceItemProps = Pick< - RouterOutput["resource"]["getChildrenOf"]["items"][number], - "permalink" | "type" + RouterOutput["resource"]["getFolderChildrenOf"]["items"][number], + "permalink" > & { - isSelected: boolean - isDisabled: boolean - onResourceItemSelect: () => void + handleOnClick: () => void + type: ResourceType | undefined + isDisabled?: boolean + isHighlighted?: boolean +} + +const getButtonProps = ({ isHighlighted }: { isHighlighted: boolean }) => { + if (isHighlighted) { + return { + color: "interaction.main.default", + bg: "interaction.muted.main.active", + _hover: { + color: "interaction.main.default", + bg: "interaction.muted.main.active", + }, + } + } + + return { + color: "base.content.default", + } +} + +const getButtonIcon = ({ + type, +}: { + type: ResourceType | undefined +}): IconType => { + switch (type) { + case ResourceType.CollectionLink: + return BiLink + case ResourceType.Folder: + return BiFolder + case ResourceType.CollectionPage: + case ResourceType.Page: + return BiFile + case ResourceType.Collection: + return BiData + default: + return BiData // Default to data icon + } } const SuspendableResourceItem = ({ permalink, - type, - isSelected, - isDisabled, - onResourceItemSelect, + isHighlighted, + handleOnClick, + ...rest }: ResourceItemProps) => { - const icon: IconType = useMemo(() => { - switch (type) { - case ResourceType.CollectionLink: - return BiLink - case ResourceType.Folder: - return BiFolder - case ResourceType.CollectionPage: - case ResourceType.Page: - return BiFile - case ResourceType.Collection: - return BiData - } - }, [type]) + const { type, ...restWithoutType } = rest + + const buttonProps = getButtonProps({ + isHighlighted: !!isHighlighted, + }) return ( ) } @@ -92,13 +101,13 @@ export const ResourceItem = (props: ResourceItemProps) => { ( - +
There was an error! - +
)} > - }> +
diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 99f7022182..711d4afd6f 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -3,155 +3,181 @@ import { Box, Flex, HStack, - Icon, Skeleton, Spacer, Text, + VStack, } from "@chakra-ui/react" import { Button, Link } from "@opengovsg/design-system-react" -import { ResourceType } from "~prisma/generated/generatedEnums" +import { ResourceType } from "@prisma/client" import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" +import type { PendingMoveResource } from "~/features/editing-experience/types" +import { useQueryParse } from "~/hooks/useQueryParse" +import { sitePageSchema } from "~/pages/sites/[siteId]" import { trpc } from "~/utils/trpc" import { ResourceItem } from "./ResourceItem" +interface ResourceSelectorProps { + selectedResourceId?: string + onChange: (resourceId: string) => void +} + const SuspensableResourceSelector = ({ - siteId, selectedResourceId, onChange, - isDisabledFn, }: ResourceSelectorProps) => { - const [ancestryStack] = trpc.resource.getAncestryOf.useSuspenseQuery({ - siteId, - resourceId: selectedResourceId, - }) - const [parentIdStack, setParentIdStack] = useState( - ancestryStack.map((item) => item.id), - ) - const currResourceId = parentIdStack[parentIdStack.length - 1] ?? null + // NOTE: This is the stack of user's navigation through the resource tree + // NOTE: We should always start the stack from `/` (root) + // so that the user will see a full overview of their site structure + const [resourceStack, setResourceStack] = useState([]) + const [isResourceHighlighted, setIsResourceHighlighted] = + useState(true) + const { siteId } = useQueryParse(sitePageSchema) - const [data, { fetchNextPage, hasNextPage, isFetchingNextPage }] = - trpc.resource.getChildrenOf.useSuspenseInfiniteQuery( + const moveDest = resourceStack[resourceStack.length - 1] + const parentDest = resourceStack[resourceStack.length - 2] + const curResourceId = moveDest?.resourceId + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = + trpc.resource.getChildrenOf.useInfiniteQuery( { - resourceId: currResourceId, - siteId, + resourceId: + (isResourceHighlighted + ? parentDest?.resourceId + : moveDest?.resourceId) ?? null, + siteId: String(siteId), limit: 25, }, { getNextPageParam: (lastPage) => lastPage.nextOffset, }, ) - - const onBack = () => { - setParentIdStack((prev) => prev.slice(0, -1)) - } + const shouldShowBackButton: boolean = + (resourceStack.length === 1 && !isResourceHighlighted) || + resourceStack.length > 1 return ( - - {parentIdStack.length > 0 ? ( - - - - Back to parent folder - - - ) : ( - - - - / - - - + + {shouldShowBackButton ? ( + { + if (isResourceHighlighted) { + setIsResourceHighlighted(false) + setResourceStack((prev) => prev.slice(0, -2)) + } else { + setResourceStack((prev) => prev.slice(0, -1)) + } + }} + as="button" > - Home - - - )} + + + Back to parent folder + + + ) : ( + + + + / + + + + Home + + + )} - {data.pages.flatMap(({ items }) => items).length === 0 ? ( - - - No matching results - - - ) : ( - data.pages.map(({ items }) => - items.map((child) => { - const isDisabled = isDisabledFn?.(child.id) ?? false + {data?.pages.map(({ items }) => + items.map((item) => { + const isItemHighlighted: boolean = + isResourceHighlighted && item.id === curResourceId + + const canClickIntoItem: boolean = + item.type === ResourceType.Folder || + item.type === ResourceType.Collection return ( { - if ( - child.type === ResourceType.Folder || - child.type === ResourceType.Collection - ) { - setParentIdStack((prev) => [...prev, child.id]) + {...item} + key={item.id} + isDisabled={false} + isHighlighted={isItemHighlighted} + handleOnClick={() => { + if (isItemHighlighted) { + if (canClickIntoItem) { + setIsResourceHighlighted(false) + } + return + } + + const newResource = { + ...item, + parentId: parentDest?.resourceId ?? null, + resourceId: item.id, + } + if (isResourceHighlighted) { + setResourceStack((prev) => [ + ...prev.slice(0, -1), + newResource, + ]) } else { - onChange(child.id) + setIsResourceHighlighted(true) + setResourceStack((prev) => [...prev, newResource]) } }} /> ) }), - ) - )} - - {hasNextPage && ( - + )} + {hasNextPage && ( + + )} + + {!!moveDest && ( + + You selected /{moveDest.permalink} + )} - + ) } -interface ResourceSelectorProps { - siteId: string - selectedResourceId?: string - onChange: (resourceId: string) => void - isDisabledFn?: (resourceId: string) => boolean -} - export const ResourceSelector = (props: ResourceSelectorProps) => { return ( }> diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx index d802ab5bc8..7f3943eae1 100644 --- a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx @@ -53,7 +53,6 @@ interface PageLinkModalContentProps { const PageLinkModalContent = ({ data, - siteId, description, onClose, onSave, @@ -70,16 +69,6 @@ const PageLinkModalContent = ({ const onSubmit = handleSubmit(({ destination }) => onSave(destination)) const destination = watch("destination") - const { data: selectedResource } = - trpc.resource.getWithFullPermalink.useQuery( - { - resourceId: destination, - }, - { - enabled: !!destination, - }, - ) - return (
@@ -91,25 +80,10 @@ const PageLinkModalContent = ({ {description || "When this link is clicked, open..."} - setValue("destination", resourceId)} selectedResourceId={destination} /> - - {destination !== "" && ( - - - You selected /{selectedResource?.fullPermalink || ""} - - - )} From c9754c75a88fecd9b0629dbe9018c89cbc74d8d1 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Thu, 7 Nov 2024 15:48:35 +0800 Subject: [PATCH 002/104] integrate to handle onChange --- .../src/components/ResourceSelector/ResourceSelector.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 711d4afd6f..cebd47454e 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -1,4 +1,4 @@ -import { Suspense, useState } from "react" +import { Suspense, useEffect, useState } from "react" import { Box, Flex, @@ -56,6 +56,12 @@ const SuspensableResourceSelector = ({ (resourceStack.length === 1 && !isResourceHighlighted) || resourceStack.length > 1 + useEffect(() => { + if (curResourceId) { + onChange(curResourceId) + } + }, [curResourceId]) + return ( Date: Thu, 7 Nov 2024 17:32:04 +0800 Subject: [PATCH 003/104] update getAncestry to getAncestryWithSelf --- .../__tests__/resource.router.test.ts | 26 +++++++++++-------- .../modules/resource/resource.router.ts | 13 +++++++--- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/apps/studio/src/server/modules/resource/__tests__/resource.router.test.ts b/apps/studio/src/server/modules/resource/__tests__/resource.router.test.ts index a3265cc0ef..25573cff41 100644 --- a/apps/studio/src/server/modules/resource/__tests__/resource.router.test.ts +++ b/apps/studio/src/server/modules/resource/__tests__/resource.router.test.ts @@ -1663,7 +1663,7 @@ describe("resource.router", async () => { it.skip("should throw 403 if user does not have read access to the resource", async () => {}) }) - describe("getAncestryOf", () => { + describe("getAncestryWithSelf", () => { const RESOURCE_FIELDS_TO_PICK = [ "id", "title", @@ -1675,7 +1675,7 @@ describe("resource.router", async () => { const unauthedSession = applySession() const unauthedCaller = createCaller(createMockRequest(unauthedSession)) - const result = unauthedCaller.getAncestryOf({ + const result = unauthedCaller.getAncestryWithSelf({ resourceId: "1", siteId: "1", }) @@ -1690,7 +1690,7 @@ describe("resource.router", async () => { const { site } = await setupSite() // Act - const result = await caller.getAncestryOf({ + const result = await caller.getAncestryWithSelf({ siteId: String(site.id), resourceId: "99999", }) @@ -1706,7 +1706,7 @@ describe("resource.router", async () => { }) // Act - const result = await caller.getAncestryOf({ + const result = await caller.getAncestryWithSelf({ resourceId: page.id, siteId: String(site.id), }) @@ -1720,7 +1720,7 @@ describe("resource.router", async () => { const { site } = await setupSite() // Act - const result = await caller.getAncestryOf({ + const result = await caller.getAncestryWithSelf({ siteId: String(site.id), }) @@ -1728,9 +1728,12 @@ describe("resource.router", async () => { expect(result).toEqual([]) }) - it("should return the ancestry of a nested resource", async () => { + it("should return the ancestry (including self and excluding root page) of a nested resource", async () => { // Arrange - const { folder: parentFolder, site } = await setupFolder({ + const { site } = await setupPageResource({ + resourceType: "RootPage", + }) + const { folder: parentFolder } = await setupFolder({ permalink: "parent-folder", title: "Parent folder", }) @@ -1747,7 +1750,7 @@ describe("resource.router", async () => { }) // Act - const result = await caller.getAncestryOf({ + const result = await caller.getAncestryWithSelf({ resourceId: nestedPage.id, siteId: String(site.id), }) @@ -1756,24 +1759,25 @@ describe("resource.router", async () => { const expected = [ pick(parentFolder, RESOURCE_FIELDS_TO_PICK), pick(nestedFolder, RESOURCE_FIELDS_TO_PICK), + pick(nestedPage, RESOURCE_FIELDS_TO_PICK), ] expect(result).toEqual(expected) }) - it("should return empty array if resource is a root-level resource", async () => { + it("should return empty resource if resource is a root-level resource", async () => { // Arrange const { page, site } = await setupPageResource({ resourceType: "Page", }) // Act - const result = await caller.getAncestryOf({ + const result = await caller.getAncestryWithSelf({ resourceId: page.id, siteId: String(site.id), }) // Assert - expect(result).toEqual([]) + expect(result).toEqual([pick(page, RESOURCE_FIELDS_TO_PICK)]) }) it.skip("should throw 403 if user does not have read access to the resource", async () => {}) diff --git a/apps/studio/src/server/modules/resource/resource.router.ts b/apps/studio/src/server/modules/resource/resource.router.ts index a7d86c4295..a15b7a1697 100644 --- a/apps/studio/src/server/modules/resource/resource.router.ts +++ b/apps/studio/src/server/modules/resource/resource.router.ts @@ -493,14 +493,14 @@ export const resourceRouter = router({ return query.select(["role"]).execute() }), - getAncestryOf: protectedProcedure + getAncestryWithSelf: protectedProcedure .input(getAncestrySchema) .query(async ({ input: { siteId, resourceId } }) => { if (!resourceId) { return [] } - const ancestors = await db + const ancestorsWithSelf = await db .withRecursive("Resources", (eb) => eb .selectFrom("Resource") @@ -512,6 +512,13 @@ export const resourceRouter = router({ ]) .where("Resource.siteId", "=", Number(siteId)) .where("Resource.id", "=", resourceId) + .where((eb) => + // to exclude root page + eb.and([ + eb("Resource.permalink", "is not", null), + eb("Resource.permalink", "!=", ""), + ]), + ) .unionAll( eb .selectFrom("Resource") @@ -533,6 +540,6 @@ export const resourceRouter = router({ ]) .execute() - return ancestors.reverse().slice(0, -1) + return ancestorsWithSelf.reverse() }), }) From 81dc97acd3dcdba525467fc506ccc4f245e41016 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Thu, 7 Nov 2024 17:33:11 +0800 Subject: [PATCH 004/104] show existing link location on open --- .../ResourceSelector/ResourceSelector.tsx | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index cebd47454e..f44f41a4b5 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -52,9 +52,22 @@ const SuspensableResourceSelector = ({ getNextPageParam: (lastPage) => lastPage.nextOffset, }, ) - const shouldShowBackButton: boolean = - (resourceStack.length === 1 && !isResourceHighlighted) || - resourceStack.length > 1 + const ancestryStack: PendingMoveResource[] = trpc.resource.getAncestryWithSelf + .useSuspenseQuery({ + siteId: String(siteId), + resourceId: selectedResourceId, + })[0] + .map((resource) => ({ ...resource, resourceId: resource.id })) + + useEffect(() => { + if ( + ancestryStack.length <= 0 || + JSON.stringify(ancestryStack) === JSON.stringify(resourceStack) + ) { + return + } + setResourceStack(ancestryStack) + }, []) useEffect(() => { if (curResourceId) { @@ -62,6 +75,10 @@ const SuspensableResourceSelector = ({ } }, [curResourceId]) + const shouldShowBackButton: boolean = + (resourceStack.length === 1 && !isResourceHighlighted) || + resourceStack.length > 1 + return ( Date: Thu, 7 Nov 2024 17:39:31 +0800 Subject: [PATCH 005/104] lint LinkEditorModal --- apps/studio/src/components/PageEditor/LinkEditorModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/src/components/PageEditor/LinkEditorModal.tsx b/apps/studio/src/components/PageEditor/LinkEditorModal.tsx index 92695e9c2b..9d9fe9debe 100644 --- a/apps/studio/src/components/PageEditor/LinkEditorModal.tsx +++ b/apps/studio/src/components/PageEditor/LinkEditorModal.tsx @@ -7,7 +7,7 @@ import { ModalContent, ModalFooter, ModalHeader, - ModalOverlay + ModalOverlay, } from "@chakra-ui/react" import { Button, From dc3e5c877d8107403c984588a0473c137c050ec4 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Thu, 7 Nov 2024 17:41:42 +0800 Subject: [PATCH 006/104] refactor - remove moveItem (use ResourceItem instead) --- .../components/MoveResourceModal/MoveItem.tsx | 92 ------------------- .../MoveResourceModal/MoveResourceModal.tsx | 4 +- 2 files changed, 2 insertions(+), 94 deletions(-) delete mode 100644 apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveItem.tsx diff --git a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveItem.tsx b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveItem.tsx deleted file mode 100644 index a3222cb618..0000000000 --- a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveItem.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Suspense } from "react" -import { Text } from "@chakra-ui/react" -import { Button } from "@opengovsg/design-system-react" -import { QueryErrorResetBoundary } from "@tanstack/react-query" -import { ErrorBoundary } from "react-error-boundary" -import { BiFolder } from "react-icons/bi" - -import type { RouterOutput } from "~/utils/trpc" - -type MoveItemProps = Pick< - RouterOutput["resource"]["getFolderChildrenOf"]["items"][number], - "permalink" -> & { - handleOnClick: () => void - isDisabled?: boolean - isHighlighted?: boolean -} - -const getButtonProps = ({ isHighlighted }: { isHighlighted: boolean }) => { - if (isHighlighted) { - return { - color: "interaction.main.default", - bg: "interaction.muted.main.active", - _hover: { - color: "interaction.main.default", - bg: "interaction.muted.main.active", - }, - } - } - - return { - color: "base.content.default", - } -} - -const SuspendableMoveItem = ({ - permalink, - isHighlighted, - handleOnClick, - ...rest -}: MoveItemProps) => { - const buttonProps = getButtonProps({ - isHighlighted: !!isHighlighted, - }) - - return ( - - ) -} - -export const MoveItem = (props: MoveItemProps) => { - return ( - - {({ reset }) => ( - ( -
- There was an error! - -
- )} - > - - - -
- )} -
- ) -} diff --git a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx index c82fcb750a..ff8d29b593 100644 --- a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx +++ b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx @@ -21,13 +21,13 @@ import { useAtom, useAtomValue, useSetAtom } from "jotai" import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" import type { PendingMoveResource } from "../../types" +import { ResourceItem } from "~/components/ResourceSelector/ResourceItem" import { usePermissions } from "~/features/permissions" import { withSuspense } from "~/hocs/withSuspense" import { useQueryParse } from "~/hooks/useQueryParse" import { sitePageSchema } from "~/pages/sites/[siteId]" import { trpc } from "~/utils/trpc" import { moveResourceAtom } from "../../atoms" -import { MoveItem } from "./MoveItem" const generatePermalinkPrefix = (parents: PendingMoveResource[]) => { return parents.map((parent) => parent.permalink).join("/") @@ -209,7 +209,7 @@ const MoveResourceContent = withSuspense( isResourceHighlighted && item.id === curResourceId return ( - Date: Thu, 7 Nov 2024 18:18:39 +0800 Subject: [PATCH 007/104] refactor: reuse ResourceSelector --- .../components/PageEditor/LinkEditorModal.tsx | 2 + .../ResourceSelector/ResourceSelector.tsx | 60 ++++-- .../MoveResourceModal/MoveResourceModal.tsx | 175 +----------------- .../controls/JsonFormsLinkControl.tsx | 1 + 4 files changed, 54 insertions(+), 184 deletions(-) diff --git a/apps/studio/src/components/PageEditor/LinkEditorModal.tsx b/apps/studio/src/components/PageEditor/LinkEditorModal.tsx index 9d9fe9debe..bf5ab18eff 100644 --- a/apps/studio/src/components/PageEditor/LinkEditorModal.tsx +++ b/apps/studio/src/components/PageEditor/LinkEditorModal.tsx @@ -24,6 +24,7 @@ import { LinkHrefEditor } from "~/features/editing-experience/components/LinkEdi import { useQueryParse } from "~/hooks/useQueryParse" import { useZodForm } from "~/lib/form" import { getReferenceLink, getResourceIdFromReferenceLink } from "~/utils/link" +import { trpc } from "~/utils/trpc" import { ResourceSelector } from "../ResourceSelector" import { FileAttachment } from "./FileAttachment" @@ -43,6 +44,7 @@ const PageLinkElement = ({ value, onChange }: PageLinkElementProps) => { return ( onChange(getReferenceLink({ siteId: String(siteId), resourceId })) } diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index f44f41a4b5..60489fc7e1 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -19,13 +19,23 @@ import { trpc } from "~/utils/trpc" import { ResourceItem } from "./ResourceItem" interface ResourceSelectorProps { - selectedResourceId?: string + queryFn: + | typeof trpc.resource.getChildrenOf.useInfiniteQuery + | typeof trpc.resource.getFolderChildrenOf.useInfiniteQuery onChange: (resourceId: string) => void + selectedResourceId?: string + existingResource?: PendingMoveResource | null +} + +const generatePermalinkPrefix = (parents: PendingMoveResource[]) => { + return parents.map((parent) => parent.permalink).join("/") } const SuspensableResourceSelector = ({ - selectedResourceId, + queryFn, onChange, + selectedResourceId, + existingResource, }: ResourceSelectorProps) => { // NOTE: This is the stack of user's navigation through the resource tree // NOTE: We should always start the stack from `/` (root) @@ -38,20 +48,19 @@ const SuspensableResourceSelector = ({ const moveDest = resourceStack[resourceStack.length - 1] const parentDest = resourceStack[resourceStack.length - 2] const curResourceId = moveDest?.resourceId - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = - trpc.resource.getChildrenOf.useInfiniteQuery( - { - resourceId: - (isResourceHighlighted - ? parentDest?.resourceId - : moveDest?.resourceId) ?? null, - siteId: String(siteId), - limit: 25, - }, - { - getNextPageParam: (lastPage) => lastPage.nextOffset, - }, - ) + const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = queryFn( + { + resourceId: + (isResourceHighlighted + ? parentDest?.resourceId + : moveDest?.resourceId) ?? null, + siteId: String(siteId), + limit: 25, + }, + { + getNextPageParam: (lastPage) => lastPage.nextOffset, + }, + ) const ancestryStack: PendingMoveResource[] = trpc.resource.getAncestryWithSelf .useSuspenseQuery({ siteId: String(siteId), @@ -80,7 +89,7 @@ const SuspensableResourceSelector = ({ resourceStack.length > 1 return ( - + items.map((item) => { + const isItemDisabled: boolean = + item.id === existingResource?.resourceId + const isItemHighlighted: boolean = isResourceHighlighted && item.id === curResourceId @@ -151,7 +163,7 @@ const SuspensableResourceSelector = ({ { if (isItemHighlighted) { @@ -194,7 +206,17 @@ const SuspensableResourceSelector = ({ {!!moveDest && ( - You selected /{moveDest.permalink} + + + You selected /{moveDest.permalink} + + {existingResource && ( + + The URL for {existingResource.title} will change to{" "} + {`${generatePermalinkPrefix(resourceStack)}/${existingResource.permalink}`} + + )} + )} diff --git a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx index ff8d29b593..afd4d58b78 100644 --- a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx +++ b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx @@ -1,9 +1,6 @@ import { useState } from "react" import { - Box, Button, - Flex, - HStack, Modal, ModalBody, ModalCloseButton, @@ -12,16 +9,12 @@ import { ModalHeader, ModalOverlay, Skeleton, - Spacer, - Text, VStack, } from "@chakra-ui/react" -import { Infobox, Link, useToast } from "@opengovsg/design-system-react" +import { Infobox, useToast } from "@opengovsg/design-system-react" import { useAtom, useAtomValue, useSetAtom } from "jotai" -import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" -import type { PendingMoveResource } from "../../types" -import { ResourceItem } from "~/components/ResourceSelector/ResourceItem" +import { ResourceSelector } from "~/components/ResourceSelector/ResourceSelector" import { usePermissions } from "~/features/permissions" import { withSuspense } from "~/hocs/withSuspense" import { useQueryParse } from "~/hooks/useQueryParse" @@ -29,10 +22,6 @@ import { sitePageSchema } from "~/pages/sites/[siteId]" import { trpc } from "~/utils/trpc" import { moveResourceAtom } from "../../atoms" -const generatePermalinkPrefix = (parents: PendingMoveResource[]) => { - return parents.map((parent) => parent.permalink).join("/") -} - export const MoveResourceModal = () => { // NOTE: This is what we are trying to move const [moveItem, setMoveItem] = useAtom(moveResourceAtom) @@ -53,36 +42,12 @@ export const MoveResourceModal = () => { const MoveResourceContent = withSuspense( ({ resourceId, onClose }: { resourceId: string; onClose: () => void }) => { - // NOTE: This is the stack of user's navigation through the resource tree - // NOTE: We should always start the stack from `/` (root) - // so that the user will see a full overview of their site structure - const [resourceStack, setResourceStack] = useState( - [], - ) - const [isResourceHighlighted, setIsResourceHighlighted] = - useState(true) + const [curResourceId, setCurResourceId] = useState(null) const { siteId } = useQueryParse(sitePageSchema) const setMovedItem = useSetAtom(moveResourceAtom) const [{ title }] = trpc.resource.getMetadataById.useSuspenseQuery({ resourceId, }) - const moveDest = resourceStack[resourceStack.length - 1] - const parentDest = resourceStack[resourceStack.length - 2] - const curResourceId = moveDest?.resourceId - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = - trpc.resource.getFolderChildrenOf.useInfiniteQuery( - { - resourceId: - (isResourceHighlighted - ? parentDest?.resourceId - : moveDest?.resourceId) ?? null, - siteId: String(siteId), - limit: 25, - }, - { - getNextPageParam: (lastPage) => lastPage.nextOffset, - }, - ) const ability = usePermissions() const utils = trpc.useUtils() const toast = useToast({ status: "success" }) @@ -134,10 +99,6 @@ const MoveResourceContent = withSuspense( const movedItem = useAtomValue(moveResourceAtom) - const shouldShowBackButton: boolean = - (resourceStack.length === 1 && !isResourceHighlighted) || - resourceStack.length > 1 - return ( Move "{title}" to... @@ -147,127 +108,11 @@ const MoveResourceContent = withSuspense( Moving a page or folder changes its URL, effective immediately - - {shouldShowBackButton ? ( - { - if (isResourceHighlighted) { - setIsResourceHighlighted(false) - setResourceStack((prev) => prev.slice(0, -2)) - } else { - setResourceStack((prev) => prev.slice(0, -1)) - } - }} - as="button" - > - - - Back to parent folder - - - ) : ( - - - - / - - - - Home - - - )} - {data?.pages.map(({ items }) => - items.map((item) => { - const isItemDisabled: boolean = - item.id === movedItem?.resourceId - const isItemHighlighted: boolean = - isResourceHighlighted && item.id === curResourceId - - return ( - { - if (isItemDisabled) { - return - } - - if (isItemHighlighted) { - setIsResourceHighlighted(false) - return - } - - const newResource = { - ...item, - parentId: parentDest?.resourceId ?? null, - resourceId: item.id, - } - if (isResourceHighlighted) { - setResourceStack((prev) => [ - ...prev.slice(0, -1), - newResource, - ]) - } else { - setIsResourceHighlighted(true) - setResourceStack((prev) => [...prev, newResource]) - } - }} - /> - ) - }), - )} - {hasNextPage && ( - - )} - - {!!moveDest && ( - - - - You selected {moveDest.permalink} - - - The URL for {movedItem?.title} will change to{" "} - {`${generatePermalinkPrefix(resourceStack)}/${movedItem?.permalink}`} - - - - )} + setCurResourceId(resourceId)} + /> @@ -284,7 +129,7 @@ const MoveResourceContent = withSuspense( // or if the user does not have sufficient permissions to move to the destination isDisabled={ ability.cannot("move", { - parentId: moveDest?.resourceId ?? null, + parentId: curResourceId, }) || ability.cannot("move", { parentId: movedItem?.parentId ?? null }) } @@ -294,7 +139,7 @@ const MoveResourceContent = withSuspense( mutate({ siteId, movedResourceId: movedItem.resourceId, - destinationResourceId: moveDest?.resourceId ?? null, + destinationResourceId: curResourceId, }) } > diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx index 7f3943eae1..be18fe9a49 100644 --- a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx @@ -81,6 +81,7 @@ const PageLinkModalContent = ({ {description || "When this link is clicked, open..."} setValue("destination", resourceId)} selectedResourceId={destination} /> From a63bce0fd27b40bcdbcd86a3bb0e6029a6aee1b2 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Thu, 7 Nov 2024 18:27:57 +0800 Subject: [PATCH 008/104] add gap --- .../ResourceSelector/ResourceSelector.tsx | 14 +++----------- .../components/LinkEditor/LinkTypeRadioContent.tsx | 6 +++--- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 60489fc7e1..336dada9d2 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -1,13 +1,5 @@ import { Suspense, useEffect, useState } from "react" -import { - Box, - Flex, - HStack, - Skeleton, - Spacer, - Text, - VStack, -} from "@chakra-ui/react" +import { Box, Flex, HStack, Skeleton, Spacer, Text } from "@chakra-ui/react" import { Button, Link } from "@opengovsg/design-system-react" import { ResourceType } from "@prisma/client" import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" @@ -89,7 +81,7 @@ const SuspensableResourceSelector = ({ resourceStack.length > 1 return ( - + <> )} - + ) } diff --git a/apps/studio/src/features/editing-experience/components/LinkEditor/LinkTypeRadioContent.tsx b/apps/studio/src/features/editing-experience/components/LinkEditor/LinkTypeRadioContent.tsx index d418ae1948..60571731e9 100644 --- a/apps/studio/src/features/editing-experience/components/LinkEditor/LinkTypeRadioContent.tsx +++ b/apps/studio/src/features/editing-experience/components/LinkEditor/LinkTypeRadioContent.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react" -import { InputGroup, InputLeftAddon } from "@chakra-ui/react" +import { InputGroup, InputLeftAddon, VStack } from "@chakra-ui/react" import { Input } from "@opengovsg/design-system-react" const HTTPS_PREFIX = "https://" @@ -29,7 +29,7 @@ export const LinkTypeRadioContent = ({ fileLinkElement, }: LinkTypeRadioContentProps): JSX.Element => { return ( - <> + {selectedLinkType === "page" && pageLinkElement} {selectedLinkType === "file" && fileLinkElement} {selectedLinkType === "external" && ( @@ -70,6 +70,6 @@ export const LinkTypeRadioContent = ({ /> )} - + ) } From fb65e93a0c4d2369675bd21ac7e522abf03f665e Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 12 Nov 2024 15:20:42 +0800 Subject: [PATCH 009/104] use Resource.type for checking instead --- apps/studio/src/server/modules/resource/resource.router.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/studio/src/server/modules/resource/resource.router.ts b/apps/studio/src/server/modules/resource/resource.router.ts index a15b7a1697..999a2805c1 100644 --- a/apps/studio/src/server/modules/resource/resource.router.ts +++ b/apps/studio/src/server/modules/resource/resource.router.ts @@ -513,11 +513,7 @@ export const resourceRouter = router({ .where("Resource.siteId", "=", Number(siteId)) .where("Resource.id", "=", resourceId) .where((eb) => - // to exclude root page - eb.and([ - eb("Resource.permalink", "is not", null), - eb("Resource.permalink", "!=", ""), - ]), + eb.and([eb("Resource.type", "!=", ResourceType.RootPage)]), ) .unionAll( eb From 4e9e5d187ba60e20d1e84593a58b9ffdeed39071 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 12 Nov 2024 15:53:53 +0800 Subject: [PATCH 010/104] make isHighlighted compulsory --- apps/studio/src/components/ResourceSelector/ResourceItem.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index bee89b88d5..baeaec7397 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -16,7 +16,7 @@ type ResourceItemProps = Pick< handleOnClick: () => void type: ResourceType | undefined isDisabled?: boolean - isHighlighted?: boolean + isHighlighted: boolean } const getButtonProps = ({ isHighlighted }: { isHighlighted: boolean }) => { @@ -65,7 +65,7 @@ const SuspendableResourceItem = ({ const { type, ...restWithoutType } = rest const buttonProps = getButtonProps({ - isHighlighted: !!isHighlighted, + isHighlighted, }) return ( From 917f299c96cff7264799a1d7cec1d72b73e16dee Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 12 Nov 2024 16:27:14 +0800 Subject: [PATCH 011/104] ensure api returns desired type --- .../modules/resource/resource.router.ts | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/apps/studio/src/server/modules/resource/resource.router.ts b/apps/studio/src/server/modules/resource/resource.router.ts index 999a2805c1..ed28fc6385 100644 --- a/apps/studio/src/server/modules/resource/resource.router.ts +++ b/apps/studio/src/server/modules/resource/resource.router.ts @@ -24,6 +24,16 @@ import { validateUserPermissionsForResource, } from "../permissions/permissions.service" +interface ResourceChildrenOfType { + items: { + title: string + permalink: string + type: ResourceType + id: string + }[] + nextOffset: number | null +} + const fetchResource = async (resourceId: string | null) => { if (resourceId === null) return { parentId: null } @@ -130,18 +140,13 @@ export const resourceRouter = router({ } const result = await query.execute() - if (result.length > limit) { - // Dont' return the last element, it's just for checking if there are more - result.pop() - return { - items: result, - nextOffset: offset + limit, - } - } - return { - items: result, - nextOffset: null, - } + const output: ResourceChildrenOfType = + result.length > limit + ? // Dont' return the last element, it's just for checking if there are more + { items: result.slice(0, limit), nextOffset: offset + limit } + : { items: result, nextOffset: null } + + return output }), getChildrenOf: protectedProcedure .input(getChildrenSchema) @@ -189,20 +194,13 @@ export const resourceRouter = router({ } else { query = query.where("Resource.parentId", "=", String(resourceId)) } - const result = await query.execute() - if (result.length > limit) { + const output: ResourceChildrenOfType = { // Dont' return the last element, it's just for checking if there are more - result.pop() - return { - items: result, - nextOffset: offset + limit, - } - } - return { - items: result, - nextOffset: null, + items: result.length > limit ? result.slice(0, limit) : result, + nextOffset: result.length > limit ? offset + limit : null, } + return output }), move: protectedProcedure From 153f69ba56196bb2af164b6b10b5a129d2ed9b55 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 12 Nov 2024 17:54:28 +0800 Subject: [PATCH 012/104] better typecheck of values from trpc.resource --- .../components/ResourceSelector/ResourceSelector.tsx | 11 +++++++++-- apps/studio/src/schemas/resource.ts | 11 +++++++++++ .../src/server/modules/resource/resource.router.ts | 11 +---------- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 336dada9d2..3d77f52eb9 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -5,6 +5,7 @@ import { ResourceType } from "@prisma/client" import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" import type { PendingMoveResource } from "~/features/editing-experience/types" +import type { ResourceChildrenOfType } from "~/schemas/resource" import { useQueryParse } from "~/hooks/useQueryParse" import { sitePageSchema } from "~/pages/sites/[siteId]" import { trpc } from "~/utils/trpc" @@ -40,7 +41,12 @@ const SuspensableResourceSelector = ({ const moveDest = resourceStack[resourceStack.length - 1] const parentDest = resourceStack[resourceStack.length - 2] const curResourceId = moveDest?.resourceId - const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = queryFn( + const { + data: { pages } = { pages: [{ items: [], nextOffset: null }] }, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = queryFn( { resourceId: (isResourceHighlighted @@ -53,6 +59,7 @@ const SuspensableResourceSelector = ({ getNextPageParam: (lastPage) => lastPage.nextOffset, }, ) + const data: ResourceChildrenOfType[] = pages const ancestryStack: PendingMoveResource[] = trpc.resource.getAncestryWithSelf .useSuspenseQuery({ siteId: String(siteId), @@ -139,7 +146,7 @@ const SuspensableResourceSelector = ({ )} - {data?.pages.map(({ items }) => + {data.map(({ items }) => items.map((item) => { const isItemDisabled: boolean = item.id === existingResource?.resourceId diff --git a/apps/studio/src/schemas/resource.ts b/apps/studio/src/schemas/resource.ts index f7ad5674b9..00e00c66da 100644 --- a/apps/studio/src/schemas/resource.ts +++ b/apps/studio/src/schemas/resource.ts @@ -1,5 +1,6 @@ import { z } from "zod" +import type { ResourceType } from "~prisma/generated/generatedEnums" import { infiniteOffsetPaginationSchema, offsetPaginationSchema, @@ -70,3 +71,13 @@ export const getAncestrySchema = z.object({ siteId: z.string(), resourceId: z.string().optional(), }) + +export interface ResourceChildrenOfType { + items: { + title: string + permalink: string + type: ResourceType + id: string + }[] + nextOffset: number | null +} diff --git a/apps/studio/src/server/modules/resource/resource.router.ts b/apps/studio/src/server/modules/resource/resource.router.ts index ed28fc6385..09c497916f 100644 --- a/apps/studio/src/server/modules/resource/resource.router.ts +++ b/apps/studio/src/server/modules/resource/resource.router.ts @@ -4,6 +4,7 @@ import { get } from "lodash" import { z } from "zod" import type { PermissionsProps } from "../permissions/permissions.type" +import type { ResourceChildrenOfType } from "~/schemas/resource" import { countResourceSchema, deleteResourceSchema, @@ -24,16 +25,6 @@ import { validateUserPermissionsForResource, } from "../permissions/permissions.service" -interface ResourceChildrenOfType { - items: { - title: string - permalink: string - type: ResourceType - id: string - }[] - nextOffset: number | null -} - const fetchResource = async (resourceId: string | null) => { if (resourceId === null) return { parentId: null } From 1346700631fb1a0613e3877e51b9cbc67bb0d490 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 12 Nov 2024 19:07:06 +0800 Subject: [PATCH 013/104] update to use onlyShowFolders instead --- .../studio/src/components/PageEditor/LinkEditorModal.tsx | 2 -- .../src/components/ResourceSelector/ResourceSelector.tsx | 9 +++++---- .../components/MoveResourceModal/MoveResourceModal.tsx | 2 +- .../renderers/controls/JsonFormsLinkControl.tsx | 1 - 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/studio/src/components/PageEditor/LinkEditorModal.tsx b/apps/studio/src/components/PageEditor/LinkEditorModal.tsx index bf5ab18eff..9d9fe9debe 100644 --- a/apps/studio/src/components/PageEditor/LinkEditorModal.tsx +++ b/apps/studio/src/components/PageEditor/LinkEditorModal.tsx @@ -24,7 +24,6 @@ import { LinkHrefEditor } from "~/features/editing-experience/components/LinkEdi import { useQueryParse } from "~/hooks/useQueryParse" import { useZodForm } from "~/lib/form" import { getReferenceLink, getResourceIdFromReferenceLink } from "~/utils/link" -import { trpc } from "~/utils/trpc" import { ResourceSelector } from "../ResourceSelector" import { FileAttachment } from "./FileAttachment" @@ -44,7 +43,6 @@ const PageLinkElement = ({ value, onChange }: PageLinkElementProps) => { return ( onChange(getReferenceLink({ siteId: String(siteId), resourceId })) } diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 3d77f52eb9..6303470936 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -12,12 +12,10 @@ import { trpc } from "~/utils/trpc" import { ResourceItem } from "./ResourceItem" interface ResourceSelectorProps { - queryFn: - | typeof trpc.resource.getChildrenOf.useInfiniteQuery - | typeof trpc.resource.getFolderChildrenOf.useInfiniteQuery onChange: (resourceId: string) => void selectedResourceId?: string existingResource?: PendingMoveResource | null + onlyShowFolders?: boolean } const generatePermalinkPrefix = (parents: PendingMoveResource[]) => { @@ -25,10 +23,10 @@ const generatePermalinkPrefix = (parents: PendingMoveResource[]) => { } const SuspensableResourceSelector = ({ - queryFn, onChange, selectedResourceId, existingResource, + onlyShowFolders = false, }: ResourceSelectorProps) => { // NOTE: This is the stack of user's navigation through the resource tree // NOTE: We should always start the stack from `/` (root) @@ -41,6 +39,9 @@ const SuspensableResourceSelector = ({ const moveDest = resourceStack[resourceStack.length - 1] const parentDest = resourceStack[resourceStack.length - 2] const curResourceId = moveDest?.resourceId + const queryFn = onlyShowFolders + ? trpc.resource.getFolderChildrenOf.useInfiniteQuery + : trpc.resource.getChildrenOf.useInfiniteQuery const { data: { pages } = { pages: [{ items: [], nextOffset: null }] }, fetchNextPage, diff --git a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx index afd4d58b78..ce772a6cf4 100644 --- a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx +++ b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx @@ -109,7 +109,7 @@ const MoveResourceContent = withSuspense( Moving a page or folder changes its URL, effective immediately setCurResourceId(resourceId)} /> diff --git a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx index 615a30c7ff..c5dd83f65f 100644 --- a/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx +++ b/apps/studio/src/features/editing-experience/components/form-builder/renderers/controls/JsonFormsLinkControl.tsx @@ -81,7 +81,6 @@ const PageLinkModalContent = ({ {description || "When this link is clicked, open..."} setValue("destination", resourceId)} selectedResourceId={destination} /> From 37021b46236cd555c31ce14b152119ad3cc7fcdc Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Fri, 15 Nov 2024 14:22:06 +0800 Subject: [PATCH 014/104] fix - display full permalink --- .../src/components/ResourceSelector/ResourceSelector.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 6303470936..324eea2d4c 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -18,7 +18,7 @@ interface ResourceSelectorProps { onlyShowFolders?: boolean } -const generatePermalinkPrefix = (parents: PendingMoveResource[]) => { +const generatePermalink = (parents: PendingMoveResource[]) => { return parents.map((parent) => parent.permalink).join("/") } @@ -208,12 +208,12 @@ const SuspensableResourceSelector = ({ - You selected /{moveDest.permalink} + You selected {generatePermalink(resourceStack)} {existingResource && ( The URL for {existingResource.title} will change to{" "} - {`${generatePermalinkPrefix(resourceStack)}/${existingResource.permalink}`} + {`${generatePermalink(resourceStack)}/${existingResource.permalink}`} )} From 9bf96e0224d96f930d6127d748ee4d71ec13ff42 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 14:44:46 +0800 Subject: [PATCH 015/104] update from div to Box --- .../studio/src/components/ResourceSelector/ResourceItem.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index baeaec7397..77dcf62e27 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -1,6 +1,6 @@ import type { IconType } from "react-icons" import { Suspense } from "react" -import { Icon, Text } from "@chakra-ui/react" +import { Box, Icon, Text } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" import { ResourceType } from "@prisma/client" import { QueryErrorResetBoundary } from "@tanstack/react-query" @@ -101,10 +101,10 @@ export const ResourceItem = (props: ResourceItemProps) => { ( -
+ There was an error! -
+
)} > From 23bdd3cf29df5d512a08e0c2dc8fb6ed9ed692f7 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 14:45:21 +0800 Subject: [PATCH 016/104] replace to use import from generatedEnums --- apps/studio/src/components/ResourceSelector/ResourceItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index 77dcf62e27..861eaf1935 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -2,8 +2,8 @@ import type { IconType } from "react-icons" import { Suspense } from "react" import { Box, Icon, Text } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" -import { ResourceType } from "@prisma/client" import { QueryErrorResetBoundary } from "@tanstack/react-query" +import { ResourceType } from "~prisma/generated/generatedEnums" import { ErrorBoundary } from "react-error-boundary" import { BiData, BiFile, BiFolder, BiLink } from "react-icons/bi" From 566791045179fea7be595a6eb992779cb6efdbbb Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 16:55:39 +0800 Subject: [PATCH 017/104] refactor - use single source of truth getIcon --- .../ResourceSelector/ResourceItem.tsx | 27 ++-------------- .../DirectorySidebarContent.tsx | 6 ++-- .../components/DirectorySidebar/constants.ts | 21 ------------- .../components/ResourceTable/TitleCell.tsx | 28 ++--------------- .../TypeOptionsInput.tsx | 6 ++-- apps/studio/src/utils/resources.ts | 31 +++++++++++++++++++ 6 files changed, 42 insertions(+), 77 deletions(-) delete mode 100644 apps/studio/src/features/dashboard/components/DirectorySidebar/constants.ts diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index 861eaf1935..a952206d7e 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -1,13 +1,12 @@ -import type { IconType } from "react-icons" import { Suspense } from "react" import { Box, Icon, Text } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" import { QueryErrorResetBoundary } from "@tanstack/react-query" -import { ResourceType } from "~prisma/generated/generatedEnums" import { ErrorBoundary } from "react-error-boundary" -import { BiData, BiFile, BiFolder, BiLink } from "react-icons/bi" import type { RouterOutput } from "~/utils/trpc" +import type { ResourceType } from "~prisma/generated/generatedEnums" +import { getIcon } from "~/utils/resources" type ResourceItemProps = Pick< RouterOutput["resource"]["getFolderChildrenOf"]["items"][number], @@ -36,26 +35,6 @@ const getButtonProps = ({ isHighlighted }: { isHighlighted: boolean }) => { } } -const getButtonIcon = ({ - type, -}: { - type: ResourceType | undefined -}): IconType => { - switch (type) { - case ResourceType.CollectionLink: - return BiLink - case ResourceType.Folder: - return BiFolder - case ResourceType.CollectionPage: - case ResourceType.Page: - return BiFile - case ResourceType.Collection: - return BiData - default: - return BiData // Default to data icon - } -} - const SuspendableResourceItem = ({ permalink, isHighlighted, @@ -84,7 +63,7 @@ const SuspendableResourceItem = ({ pl="2.25rem" size="xs" onClick={handleOnClick} - leftIcon={} + leftIcon={} {...restWithoutType} > diff --git a/apps/studio/src/features/dashboard/components/DirectorySidebar/DirectorySidebarContent.tsx b/apps/studio/src/features/dashboard/components/DirectorySidebar/DirectorySidebarContent.tsx index e96ca26ef7..6205fee10d 100644 --- a/apps/studio/src/features/dashboard/components/DirectorySidebar/DirectorySidebarContent.tsx +++ b/apps/studio/src/features/dashboard/components/DirectorySidebar/DirectorySidebarContent.tsx @@ -4,8 +4,8 @@ import { Button } from "@opengovsg/design-system-react" import { ResourceType } from "~prisma/generated/generatedEnums" import { getResourceSubpath } from "~/utils/resource" +import { getIcon } from "~/utils/resources" import { trpc } from "~/utils/trpc" -import { ICON_MAPPINGS } from "./constants" import { RowEntry } from "./RowEntry" import { useIsActive } from "./useIsActive" @@ -87,7 +87,7 @@ export const DirectorySidebarContent = ({ > = { - [ResourceType.Page]: BiFile, - [ResourceType.Folder]: BiFolder, - [ResourceType.Collection]: BiData, - [ResourceType.CollectionPage]: BiFile, - [ResourceType.CollectionLink]: BiLink, - [ResourceType.RootPage]: BiHomeAlt, - [ResourceType.IndexPage]: BiFile, - [ResourceType.FolderMeta]: BiSort, -} diff --git a/apps/studio/src/features/dashboard/components/ResourceTable/TitleCell.tsx b/apps/studio/src/features/dashboard/components/ResourceTable/TitleCell.tsx index 84a01d508f..863a920c29 100644 --- a/apps/studio/src/features/dashboard/components/ResourceTable/TitleCell.tsx +++ b/apps/studio/src/features/dashboard/components/ResourceTable/TitleCell.tsx @@ -3,18 +3,10 @@ import { useMemo } from "react" import NextLink from "next/link" import { HStack, Icon, Text, VStack } from "@chakra-ui/react" import { Link } from "@opengovsg/design-system-react" -import { ResourceType } from "~prisma/generated/generatedEnums" -import { - BiData, - BiFile, - BiFolder, - BiHome, - BiLink, - BiSort, -} from "react-icons/bi" import type { ResourceTableData } from "./types" import { getLinkToResource } from "~/utils/resource" +import { getIcon } from "~/utils/resources" export interface TitleCellProps extends Pick { @@ -33,23 +25,7 @@ export const TitleCell = ({ }, [id, siteId, type]) const ResourceTypeIcon: IconType = useMemo(() => { - switch (type) { - case ResourceType.RootPage: - return BiHome - case ResourceType.IndexPage: - case ResourceType.Page: - return BiFile - case ResourceType.Folder: - return BiFolder - case ResourceType.Collection: - return BiData - case ResourceType.CollectionPage: - return BiFile - case ResourceType.CollectionLink: - return BiLink - case ResourceType.FolderMeta: - return BiSort - } + return getIcon(type) }, [type]) return ( diff --git a/apps/studio/src/features/editing-experience/components/CreateCollectionPageModal/TypeOptionsInput.tsx b/apps/studio/src/features/editing-experience/components/CreateCollectionPageModal/TypeOptionsInput.tsx index 2f292d736b..c7456f96e8 100644 --- a/apps/studio/src/features/editing-experience/components/CreateCollectionPageModal/TypeOptionsInput.tsx +++ b/apps/studio/src/features/editing-experience/components/CreateCollectionPageModal/TypeOptionsInput.tsx @@ -12,9 +12,9 @@ import { } from "@chakra-ui/react" import { Badge } from "@opengovsg/design-system-react" import { ResourceType } from "~prisma/generated/generatedEnums" -import { BiFile, BiLink } from "react-icons/bi" import type { CollectionItemType } from "./constants" +import { getIcon } from "~/utils/resources" import { COLLECTION_ITEM_TYPES } from "./constants" interface TypeTileProps extends UseRadioProps { @@ -34,7 +34,7 @@ const TypeOptionRadio = forwardRef( switch (value) { case ResourceType.CollectionPage: { return { - TileIcon: BiFile, + TileIcon: getIcon(value), title: "Page", description: "Select this option if you want an empty page where you can place article content.", @@ -47,7 +47,7 @@ const TypeOptionRadio = forwardRef( } case ResourceType.CollectionLink: { return { - TileIcon: BiLink, + TileIcon: getIcon(value), title: "Link or file", description: "Select this option if you want to link to an existing page on your site, link an external page, or upload a PDF file.", diff --git a/apps/studio/src/utils/resources.ts b/apps/studio/src/utils/resources.ts index 0ed1d385ff..1149ae5224 100644 --- a/apps/studio/src/utils/resources.ts +++ b/apps/studio/src/utils/resources.ts @@ -1,4 +1,13 @@ +import type { IconType } from "react-icons" import { ResourceType } from "~prisma/generated/generatedEnums" +import { + BiData, + BiFile, + BiFolder, + BiHome, + BiLink, + BiSort, +} from "react-icons/bi" export const isAllowedToHaveChildren = ( resourceType: ResourceType, @@ -9,3 +18,25 @@ export const isAllowedToHaveChildren = ( resourceType === ResourceType.RootPage ) } + +export const getIcon = (resourceType: ResourceType): IconType => { + switch (resourceType) { + case ResourceType.Page: + case ResourceType.IndexPage: + case ResourceType.CollectionPage: + return BiFile + case ResourceType.Folder: + return BiFolder + case ResourceType.Collection: + return BiData + case ResourceType.CollectionLink: + return BiLink + case ResourceType.RootPage: + return BiHome + case ResourceType.FolderMeta: + return BiSort + default: + const _: never = resourceType // exhaustive check + return BiData + } +} From ce114633af667ebd1a5731b656b224a7bed0ee29 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 17:07:29 +0800 Subject: [PATCH 018/104] update schema name to getAncestryWithSelfSchema to be consistent --- apps/studio/src/schemas/resource.ts | 2 +- apps/studio/src/server/modules/resource/resource.router.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/studio/src/schemas/resource.ts b/apps/studio/src/schemas/resource.ts index 00e00c66da..b7a44a4c91 100644 --- a/apps/studio/src/schemas/resource.ts +++ b/apps/studio/src/schemas/resource.ts @@ -67,7 +67,7 @@ export const getFullPermalinkSchema = z.object({ resourceId: bigIntSchema, }) -export const getAncestrySchema = z.object({ +export const getAncestryWithSelfSchema = z.object({ siteId: z.string(), resourceId: z.string().optional(), }) diff --git a/apps/studio/src/server/modules/resource/resource.router.ts b/apps/studio/src/server/modules/resource/resource.router.ts index 09c497916f..39518aea58 100644 --- a/apps/studio/src/server/modules/resource/resource.router.ts +++ b/apps/studio/src/server/modules/resource/resource.router.ts @@ -8,7 +8,7 @@ import type { ResourceChildrenOfType } from "~/schemas/resource" import { countResourceSchema, deleteResourceSchema, - getAncestrySchema, + getAncestryWithSelfSchema, getChildrenSchema, getFullPermalinkSchema, getMetadataSchema, @@ -483,7 +483,7 @@ export const resourceRouter = router({ }), getAncestryWithSelf: protectedProcedure - .input(getAncestrySchema) + .input(getAncestryWithSelfSchema) .query(async ({ input: { siteId, resourceId } }) => { if (!resourceId) { return [] From 1ca9ef974cd2c1067687e9f4dce43a737e6fe726 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 17:15:22 +0800 Subject: [PATCH 019/104] fix mock - getAncestry to getAncestryWithSelf --- .../stories/Page/EditPage/EditArticlePage.stories.tsx | 2 +- .../Page/EditPage/EditCollectionLink.stories.tsx | 2 +- .../stories/Page/EditPage/EditContentPage.stories.tsx | 2 +- apps/studio/tests/msw/handlers/resource.ts | 10 ++++++++-- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/studio/src/stories/Page/EditPage/EditArticlePage.stories.tsx b/apps/studio/src/stories/Page/EditPage/EditArticlePage.stories.tsx index 7d6bc249c3..0a36a7760d 100644 --- a/apps/studio/src/stories/Page/EditPage/EditArticlePage.stories.tsx +++ b/apps/studio/src/stories/Page/EditPage/EditArticlePage.stories.tsx @@ -22,7 +22,7 @@ const COMMON_HANDLERS = [ sitesHandlers.getLocalisedSitemap.default(), resourceHandlers.getRolesFor.default(), resourceHandlers.getWithFullPermalink.default(), - resourceHandlers.getAncestryOf.collectionLink(), + resourceHandlers.getAncestryWithSelf.collectionLink(), resourceHandlers.getChildrenOf.default(), resourceHandlers.getMetadataById.article(), pageHandlers.readPageAndBlob.article(), diff --git a/apps/studio/src/stories/Page/EditPage/EditCollectionLink.stories.tsx b/apps/studio/src/stories/Page/EditPage/EditCollectionLink.stories.tsx index 73f1b85500..91493c2949 100644 --- a/apps/studio/src/stories/Page/EditPage/EditCollectionLink.stories.tsx +++ b/apps/studio/src/stories/Page/EditPage/EditCollectionLink.stories.tsx @@ -23,7 +23,7 @@ const COMMON_HANDLERS = [ sitesHandlers.getLocalisedSitemap.default(), resourceHandlers.getRolesFor.default(), resourceHandlers.getWithFullPermalink.default(), - resourceHandlers.getAncestryOf.collectionLink(), + resourceHandlers.getAncestryWithSelf.collectionLink(), resourceHandlers.getChildrenOf.default(), resourceHandlers.getMetadataById.article(), resourceHandlers.getParentOf.collection(), diff --git a/apps/studio/src/stories/Page/EditPage/EditContentPage.stories.tsx b/apps/studio/src/stories/Page/EditPage/EditContentPage.stories.tsx index 3167fc7c23..b3c0f280ab 100644 --- a/apps/studio/src/stories/Page/EditPage/EditContentPage.stories.tsx +++ b/apps/studio/src/stories/Page/EditPage/EditContentPage.stories.tsx @@ -22,7 +22,7 @@ const COMMON_HANDLERS = [ sitesHandlers.getLocalisedSitemap.default(), resourceHandlers.getChildrenOf.default(), resourceHandlers.getWithFullPermalink.default(), - resourceHandlers.getAncestryOf.collectionLink(), + resourceHandlers.getAncestryWithSelf.collectionLink(), resourceHandlers.getMetadataById.content(), resourceHandlers.getRolesFor.default(), pageHandlers.readPageAndBlob.content(), diff --git a/apps/studio/tests/msw/handlers/resource.ts b/apps/studio/tests/msw/handlers/resource.ts index 72db929c25..a7ae4df8b9 100644 --- a/apps/studio/tests/msw/handlers/resource.ts +++ b/apps/studio/tests/msw/handlers/resource.ts @@ -43,9 +43,9 @@ export const resourceHandlers = { }) }, }, - getAncestryOf: { + getAncestryWithSelf: { collectionLink: () => { - return trpcMsw.resource.getAncestryOf.query(() => { + return trpcMsw.resource.getAncestryWithSelf.query(() => { return [ { parentId: null, @@ -53,6 +53,12 @@ export const resourceHandlers = { title: "Homepage", permalink: "/", }, + { + parentId: "1", + id: "2", + title: "Collection", + permalink: "collection", + }, ] }) }, From 787aca8b2fed001b354b165904d4f03addf7e57e Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 17:39:35 +0800 Subject: [PATCH 020/104] remove "undefined" from "type" --- apps/studio/src/components/ResourceSelector/ResourceItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index a952206d7e..406e2868dc 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -13,7 +13,7 @@ type ResourceItemProps = Pick< "permalink" > & { handleOnClick: () => void - type: ResourceType | undefined + type: ResourceType isDisabled?: boolean isHighlighted: boolean } From 118ebb8c9ab8d09935a8d35ef2583a70709c5c7c Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 17:45:59 +0800 Subject: [PATCH 021/104] remove "null" from acceptable type value --- .../studio/src/components/ResourceSelector/ResourceSelector.tsx | 2 +- .../components/MoveResourceModal/MoveResourceModal.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 324eea2d4c..b9d706ce9d 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -14,7 +14,7 @@ import { ResourceItem } from "./ResourceItem" interface ResourceSelectorProps { onChange: (resourceId: string) => void selectedResourceId?: string - existingResource?: PendingMoveResource | null + existingResource?: PendingMoveResource onlyShowFolders?: boolean } diff --git a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx index ce772a6cf4..18535644ce 100644 --- a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx +++ b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx @@ -110,7 +110,7 @@ const MoveResourceContent = withSuspense( setCurResourceId(resourceId)} />
From bb4d6515d636f565d743a3dfcc9275d2d3c361e8 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 18:08:07 +0800 Subject: [PATCH 022/104] refactor - do a 1-for-1 abstraction to useResourceStack --- .../ResourceSelector/ResourceSelector.tsx | 52 +++---------- .../ResourceSelector/useResourceStack.tsx | 76 +++++++++++++++++++ 2 files changed, 88 insertions(+), 40 deletions(-) create mode 100644 apps/studio/src/components/ResourceSelector/useResourceStack.tsx diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index b9d706ce9d..e43386cce5 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -1,15 +1,12 @@ -import { Suspense, useEffect, useState } from "react" +import { Suspense, useEffect } from "react" import { Box, Flex, HStack, Skeleton, Spacer, Text } from "@chakra-ui/react" import { Button, Link } from "@opengovsg/design-system-react" import { ResourceType } from "@prisma/client" import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" import type { PendingMoveResource } from "~/features/editing-experience/types" -import type { ResourceChildrenOfType } from "~/schemas/resource" -import { useQueryParse } from "~/hooks/useQueryParse" -import { sitePageSchema } from "~/pages/sites/[siteId]" -import { trpc } from "~/utils/trpc" import { ResourceItem } from "./ResourceItem" +import { useResourceStack } from "./useResourceStack" interface ResourceSelectorProps { onChange: (resourceId: string) => void @@ -28,45 +25,20 @@ const SuspensableResourceSelector = ({ existingResource, onlyShowFolders = false, }: ResourceSelectorProps) => { - // NOTE: This is the stack of user's navigation through the resource tree - // NOTE: We should always start the stack from `/` (root) - // so that the user will see a full overview of their site structure - const [resourceStack, setResourceStack] = useState([]) - const [isResourceHighlighted, setIsResourceHighlighted] = - useState(true) - const { siteId } = useQueryParse(sitePageSchema) - - const moveDest = resourceStack[resourceStack.length - 1] - const parentDest = resourceStack[resourceStack.length - 2] - const curResourceId = moveDest?.resourceId - const queryFn = onlyShowFolders - ? trpc.resource.getFolderChildrenOf.useInfiniteQuery - : trpc.resource.getChildrenOf.useInfiniteQuery const { - data: { pages } = { pages: [{ items: [], nextOffset: null }] }, + resourceStack, + setResourceStack, + isResourceHighlighted, + setIsResourceHighlighted, + moveDest, + parentDest, + curResourceId, + ancestryStack, + data, fetchNextPage, hasNextPage, isFetchingNextPage, - } = queryFn( - { - resourceId: - (isResourceHighlighted - ? parentDest?.resourceId - : moveDest?.resourceId) ?? null, - siteId: String(siteId), - limit: 25, - }, - { - getNextPageParam: (lastPage) => lastPage.nextOffset, - }, - ) - const data: ResourceChildrenOfType[] = pages - const ancestryStack: PendingMoveResource[] = trpc.resource.getAncestryWithSelf - .useSuspenseQuery({ - siteId: String(siteId), - resourceId: selectedResourceId, - })[0] - .map((resource) => ({ ...resource, resourceId: resource.id })) + } = useResourceStack({ onlyShowFolders, selectedResourceId }) useEffect(() => { if ( diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx new file mode 100644 index 0000000000..7330c012b9 --- /dev/null +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -0,0 +1,76 @@ +import { useState } from "react" + +import type { PendingMoveResource } from "~/features/editing-experience/types" +import type { ResourceChildrenOfType } from "~/schemas/resource" +import { useQueryParse } from "~/hooks/useQueryParse" +import { sitePageSchema } from "~/pages/sites/[siteId]" +import { trpc } from "~/utils/trpc" + +export const useResourceStack = ({ + onlyShowFolders, + selectedResourceId, +}: { + onlyShowFolders: boolean + selectedResourceId: string | undefined +}) => { + // NOTE: This is the stack of user's navigation through the resource tree + // NOTE: We should always start the stack from `/` (root) + // so that the user will see a full overview of their site structure + const [resourceStack, setResourceStack] = useState([]) + + const [isResourceHighlighted, setIsResourceHighlighted] = + useState(true) + + const { siteId } = useQueryParse(sitePageSchema) + + const moveDest = resourceStack[resourceStack.length - 1] + const parentDest = resourceStack[resourceStack.length - 2] + const curResourceId = moveDest?.resourceId + + const ancestryStack: PendingMoveResource[] = trpc.resource.getAncestryWithSelf + .useSuspenseQuery({ + siteId: String(siteId), + resourceId: selectedResourceId, + })[0] + .map((resource) => ({ ...resource, resourceId: resource.id })) + + const queryFn = onlyShowFolders + ? trpc.resource.getFolderChildrenOf.useInfiniteQuery + : trpc.resource.getChildrenOf.useInfiniteQuery + const { + data: { pages } = { pages: [{ items: [], nextOffset: null }] }, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = queryFn( + { + resourceId: + (isResourceHighlighted + ? parentDest?.resourceId + : moveDest?.resourceId) ?? null, + siteId: String(siteId), + limit: 25, + }, + { + getNextPageParam: (lastPage) => lastPage.nextOffset, + }, + ) + const data: ResourceChildrenOfType[] = pages + + return { + resourceStack, + setResourceStack, + isResourceHighlighted, + setIsResourceHighlighted, + moveDest, + parentDest, + curResourceId, + ancestryStack, + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } +} + +export type UseResourceStackReturn = ReturnType From 95140000402cf4124bb756718e4acb35587e9406 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 19:04:42 +0800 Subject: [PATCH 023/104] abstract away more stuff + usememo/usecallback for perf --- .../ResourceSelector/ResourceItem.tsx | 13 +++- .../ResourceSelector/ResourceSelector.tsx | 65 ++++------------ .../ResourceSelector/useResourceStack.tsx | 78 ++++++++++++++++--- 3 files changed, 94 insertions(+), 62 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index 406e2868dc..ec1aeaad87 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -2,10 +2,10 @@ import { Suspense } from "react" import { Box, Icon, Text } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" import { QueryErrorResetBoundary } from "@tanstack/react-query" +import { ResourceType } from "~prisma/generated/generatedEnums" import { ErrorBoundary } from "react-error-boundary" import type { RouterOutput } from "~/utils/trpc" -import type { ResourceType } from "~prisma/generated/generatedEnums" import { getIcon } from "~/utils/resources" type ResourceItemProps = Pick< @@ -18,6 +18,17 @@ type ResourceItemProps = Pick< isHighlighted: boolean } +export const canClickIntoItem = ({ + resourceType, +}: { + resourceType: ResourceType +}): boolean => { + return ( + resourceType === ResourceType.Folder || + resourceType === ResourceType.Collection + ) +} + const getButtonProps = ({ isHighlighted }: { isHighlighted: boolean }) => { if (isHighlighted) { return { diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index e43386cce5..0bcb6f7edc 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -1,11 +1,10 @@ -import { Suspense, useEffect } from "react" +import { Suspense } from "react" import { Box, Flex, HStack, Skeleton, Spacer, Text } from "@chakra-ui/react" import { Button, Link } from "@opengovsg/design-system-react" -import { ResourceType } from "@prisma/client" import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" import type { PendingMoveResource } from "~/features/editing-experience/types" -import { ResourceItem } from "./ResourceItem" +import { canClickIntoItem, ResourceItem } from "./ResourceItem" import { useResourceStack } from "./useResourceStack" interface ResourceSelectorProps { @@ -27,38 +26,18 @@ const SuspensableResourceSelector = ({ }: ResourceSelectorProps) => { const { resourceStack, - setResourceStack, isResourceHighlighted, setIsResourceHighlighted, moveDest, - parentDest, - curResourceId, - ancestryStack, data, fetchNextPage, hasNextPage, isFetchingNextPage, - } = useResourceStack({ onlyShowFolders, selectedResourceId }) - - useEffect(() => { - if ( - ancestryStack.length <= 0 || - JSON.stringify(ancestryStack) === JSON.stringify(resourceStack) - ) { - return - } - setResourceStack(ancestryStack) - }, []) - - useEffect(() => { - if (curResourceId) { - onChange(curResourceId) - } - }, [curResourceId]) - - const shouldShowBackButton: boolean = - (resourceStack.length === 1 && !isResourceHighlighted) || - resourceStack.length > 1 + addToStack, + removeFromStack, + isResourceIdHighlighted, + shouldShowBackButton, + } = useResourceStack({ onChange, selectedResourceId, onlyShowFolders }) return ( <> @@ -81,9 +60,9 @@ const SuspensableResourceSelector = ({ onClick={() => { if (isResourceHighlighted) { setIsResourceHighlighted(false) - setResourceStack((prev) => prev.slice(0, -2)) + removeFromStack(2) } else { - setResourceStack((prev) => prev.slice(0, -1)) + removeFromStack(1) } }} as="button" @@ -121,44 +100,28 @@ const SuspensableResourceSelector = ({ {data.map(({ items }) => items.map((item) => { - const isItemDisabled: boolean = - item.id === existingResource?.resourceId - - const isItemHighlighted: boolean = - isResourceHighlighted && item.id === curResourceId - - const canClickIntoItem: boolean = - item.type === ResourceType.Folder || - item.type === ResourceType.Collection + const isItemHighlighted: boolean = isResourceIdHighlighted(item.id) return ( { if (isItemHighlighted) { - if (canClickIntoItem) { + if (canClickIntoItem({ resourceType: item.type })) { setIsResourceHighlighted(false) } return } - const newResource = { - ...item, - parentId: parentDest?.resourceId ?? null, - resourceId: item.id, - } if (isResourceHighlighted) { - setResourceStack((prev) => [ - ...prev.slice(0, -1), - newResource, - ]) + removeFromStack(1) } else { setIsResourceHighlighted(true) - setResourceStack((prev) => [...prev, newResource]) } + addToStack({ resourceChildrenOfType: item }) }} /> ) diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index 7330c012b9..7e1c7e7b8f 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -1,4 +1,4 @@ -import { useState } from "react" +import { useCallback, useEffect, useMemo, useState } from "react" import type { PendingMoveResource } from "~/features/editing-experience/types" import type { ResourceChildrenOfType } from "~/schemas/resource" @@ -7,11 +7,13 @@ import { sitePageSchema } from "~/pages/sites/[siteId]" import { trpc } from "~/utils/trpc" export const useResourceStack = ({ - onlyShowFolders, + onChange, selectedResourceId, + onlyShowFolders, }: { - onlyShowFolders: boolean + onChange: (resourceId: string) => void selectedResourceId: string | undefined + onlyShowFolders: boolean }) => { // NOTE: This is the stack of user's navigation through the resource tree // NOTE: We should always start the stack from `/` (root) @@ -23,9 +25,15 @@ export const useResourceStack = ({ const { siteId } = useQueryParse(sitePageSchema) - const moveDest = resourceStack[resourceStack.length - 1] - const parentDest = resourceStack[resourceStack.length - 2] - const curResourceId = moveDest?.resourceId + const moveDest = useMemo( + () => resourceStack[resourceStack.length - 1], + [resourceStack], + ) + const parentDest = useMemo( + () => resourceStack[resourceStack.length - 2], + [resourceStack], + ) + const curResourceId = useMemo(() => moveDest?.resourceId, [moveDest]) const ancestryStack: PendingMoveResource[] = trpc.resource.getAncestryWithSelf .useSuspenseQuery({ @@ -57,19 +65,69 @@ export const useResourceStack = ({ ) const data: ResourceChildrenOfType[] = pages + const addToStack = useCallback( + ({ + resourceChildrenOfType, + }: { + resourceChildrenOfType: ResourceChildrenOfType["items"][number] + }): void => { + const newResource: PendingMoveResource = { + ...resourceChildrenOfType, + parentId: parentDest?.resourceId ?? null, + resourceId: resourceChildrenOfType.id, + } + setResourceStack((prev) => [...prev, newResource]) + }, + [], + ) + + const removeFromStack = useCallback((numberOfResources: number): void => { + setResourceStack((prev) => prev.slice(0, -numberOfResources)) + }, []) + + const isResourceIdHighlighted = useCallback( + (resourceId: string): boolean => { + return isResourceHighlighted && curResourceId === resourceId + }, + [isResourceHighlighted, curResourceId], + ) + + const shouldShowBackButton = useMemo( + () => + (resourceStack.length === 1 && !isResourceHighlighted) || + resourceStack.length > 1, + [resourceStack.length, isResourceHighlighted], + ) + + useEffect(() => { + if ( + ancestryStack.length <= 0 || + JSON.stringify(ancestryStack) === JSON.stringify(resourceStack) + ) { + return + } + setResourceStack(ancestryStack) + }, []) + + useEffect(() => { + if (curResourceId) { + onChange(curResourceId) + } + }, [curResourceId]) + return { resourceStack, - setResourceStack, isResourceHighlighted, setIsResourceHighlighted, moveDest, - parentDest, - curResourceId, - ancestryStack, data, fetchNextPage, hasNextPage, isFetchingNextPage, + addToStack, + removeFromStack, + isResourceIdHighlighted, + shouldShowBackButton, } } From 6c4868cf17ae52ea5da5334c895dbb3b19256f8a Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 19:29:34 +0800 Subject: [PATCH 024/104] add missing dependencies in addToStack --- .../studio/src/components/ResourceSelector/useResourceStack.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index 7e1c7e7b8f..52de64ee62 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -78,7 +78,7 @@ export const useResourceStack = ({ } setResourceStack((prev) => [...prev, newResource]) }, - [], + [parentDest], ) const removeFromStack = useCallback((numberOfResources: number): void => { From 1690a82d9156dd823a69c18c49639126c57a5bbf Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Tue, 19 Nov 2024 19:40:32 +0800 Subject: [PATCH 025/104] refactor - move logic into ResourceItem --- .../ResourceSelector/ResourceItem.tsx | 67 +++++++++++-------- .../ResourceSelector/ResourceSelector.tsx | 25 ++----- .../ResourceSelector/useResourceStack.tsx | 15 ++--- apps/studio/src/schemas/resource.ts | 14 ++-- 4 files changed, 60 insertions(+), 61 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index ec1aeaad87..a0c679c0b6 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -1,32 +1,21 @@ -import { Suspense } from "react" +import { Suspense, useMemo } from "react" import { Box, Icon, Text } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" import { QueryErrorResetBoundary } from "@tanstack/react-query" import { ResourceType } from "~prisma/generated/generatedEnums" import { ErrorBoundary } from "react-error-boundary" -import type { RouterOutput } from "~/utils/trpc" +import type { ResourceItemContent } from "~/schemas/resource" import { getIcon } from "~/utils/resources" -type ResourceItemProps = Pick< - RouterOutput["resource"]["getFolderChildrenOf"]["items"][number], - "permalink" -> & { - handleOnClick: () => void - type: ResourceType +interface ResourceItemProps { + item: ResourceItemContent isDisabled?: boolean isHighlighted: boolean -} - -export const canClickIntoItem = ({ - resourceType, -}: { - resourceType: ResourceType -}): boolean => { - return ( - resourceType === ResourceType.Folder || - resourceType === ResourceType.Collection - ) + isResourceHighlighted: boolean + setIsResourceHighlighted: (isHighlighted: boolean) => void + addToStack: (resourceItemContent: ResourceItemContent) => void + removeFromStack: (count: number) => void } const getButtonProps = ({ isHighlighted }: { isHighlighted: boolean }) => { @@ -47,17 +36,39 @@ const getButtonProps = ({ isHighlighted }: { isHighlighted: boolean }) => { } const SuspendableResourceItem = ({ - permalink, + item, + isDisabled, isHighlighted, - handleOnClick, - ...rest + isResourceHighlighted, + setIsResourceHighlighted, + addToStack, + removeFromStack, }: ResourceItemProps) => { - const { type, ...restWithoutType } = rest - const buttonProps = getButtonProps({ isHighlighted, }) + const canClickIntoItem = useMemo( + () => + item.type === ResourceType.Folder || + item.type === ResourceType.Collection, + [item.type], + ) + + const handleClick = (): void => { + if (isHighlighted && canClickIntoItem) { + setIsResourceHighlighted(false) + return + } + + if (isResourceHighlighted) { + removeFromStack(1) + } else { + setIsResourceHighlighted(true) + } + addToStack(item) + } + return ( ) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 0bcb6f7edc..b4fb1df942 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -100,29 +100,16 @@ const SuspensableResourceSelector = ({ {data.map(({ items }) => items.map((item) => { - const isItemHighlighted: boolean = isResourceIdHighlighted(item.id) - return ( { - if (isItemHighlighted) { - if (canClickIntoItem({ resourceType: item.type })) { - setIsResourceHighlighted(false) - } - return - } - - if (isResourceHighlighted) { - removeFromStack(1) - } else { - setIsResourceHighlighted(true) - } - addToStack({ resourceChildrenOfType: item }) - }} + isHighlighted={isResourceIdHighlighted(item.id)} + isResourceHighlighted={isResourceHighlighted} + setIsResourceHighlighted={setIsResourceHighlighted} + addToStack={addToStack} + removeFromStack={removeFromStack} /> ) }), diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index 52de64ee62..c69131cd24 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -1,7 +1,10 @@ import { useCallback, useEffect, useMemo, useState } from "react" import type { PendingMoveResource } from "~/features/editing-experience/types" -import type { ResourceChildrenOfType } from "~/schemas/resource" +import type { + ResourceChildrenOfType, + ResourceItemContent, +} from "~/schemas/resource" import { useQueryParse } from "~/hooks/useQueryParse" import { sitePageSchema } from "~/pages/sites/[siteId]" import { trpc } from "~/utils/trpc" @@ -66,15 +69,11 @@ export const useResourceStack = ({ const data: ResourceChildrenOfType[] = pages const addToStack = useCallback( - ({ - resourceChildrenOfType, - }: { - resourceChildrenOfType: ResourceChildrenOfType["items"][number] - }): void => { + (resourceItemContent: ResourceItemContent): void => { const newResource: PendingMoveResource = { - ...resourceChildrenOfType, + ...resourceItemContent, parentId: parentDest?.resourceId ?? null, - resourceId: resourceChildrenOfType.id, + resourceId: resourceItemContent.id, } setResourceStack((prev) => [...prev, newResource]) }, diff --git a/apps/studio/src/schemas/resource.ts b/apps/studio/src/schemas/resource.ts index b7a44a4c91..a78f706f17 100644 --- a/apps/studio/src/schemas/resource.ts +++ b/apps/studio/src/schemas/resource.ts @@ -72,12 +72,14 @@ export const getAncestryWithSelfSchema = z.object({ resourceId: z.string().optional(), }) +export interface ResourceItemContent { + title: string + permalink: string + type: ResourceType + id: string +} + export interface ResourceChildrenOfType { - items: { - title: string - permalink: string - type: ResourceType - id: string - }[] + items: ResourceItemContent[] nextOffset: number | null } From 74e4e7ac58daf5b06b789361abfc88ff037cf411 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Wed, 20 Nov 2024 00:41:05 +0800 Subject: [PATCH 026/104] more explicit about the props --- .../src/components/ResourceSelector/useResourceStack.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index c69131cd24..570b9c4c48 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -71,9 +71,10 @@ export const useResourceStack = ({ const addToStack = useCallback( (resourceItemContent: ResourceItemContent): void => { const newResource: PendingMoveResource = { - ...resourceItemContent, - parentId: parentDest?.resourceId ?? null, + permalink: resourceItemContent.permalink, + title: resourceItemContent.title, resourceId: resourceItemContent.id, + parentId: parentDest?.resourceId ?? null, } setResourceStack((prev) => [...prev, newResource]) }, From a692eff2a89d7e4aa5c6c641e687df2ccad7d5f2 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Wed, 20 Nov 2024 00:41:24 +0800 Subject: [PATCH 027/104] remove unused import --- .../studio/src/components/ResourceSelector/ResourceSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index b4fb1df942..f6b84674e0 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -4,7 +4,7 @@ import { Button, Link } from "@opengovsg/design-system-react" import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" import type { PendingMoveResource } from "~/features/editing-experience/types" -import { canClickIntoItem, ResourceItem } from "./ResourceItem" +import { ResourceItem } from "./ResourceItem" import { useResourceStack } from "./useResourceStack" interface ResourceSelectorProps { From 9a5ab6be959989e54015168b0b90f1144f54ca9d Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Wed, 20 Nov 2024 00:45:01 +0800 Subject: [PATCH 028/104] return flatten item --- .../ResourceSelector/ResourceSelector.tsx | 33 +++++++++---------- .../ResourceSelector/useResourceStack.tsx | 12 +++---- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index f6b84674e0..d74e34a8d0 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -29,7 +29,7 @@ const SuspensableResourceSelector = ({ isResourceHighlighted, setIsResourceHighlighted, moveDest, - data, + resourceItems, fetchNextPage, hasNextPage, isFetchingNextPage, @@ -97,23 +97,20 @@ const SuspensableResourceSelector = ({ )} - - {data.map(({ items }) => - items.map((item) => { - return ( - - ) - }), - )} + {resourceItems.map((item) => { + return ( + + ) + })} {hasNextPage && ( ) } From 526b3a1a89fba3b5ac3c502f85f218399b025ed4 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Thu, 28 Nov 2024 15:47:45 +0800 Subject: [PATCH 036/104] refactor headers out --- .../ResourceSelector/ResourceSelector.tsx | 79 ++++++------------- .../ResourceSelectorHeader.tsx | 53 +++++++++++++ 2 files changed, 75 insertions(+), 57 deletions(-) create mode 100644 apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 415b5ff7d9..07462bf60a 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -1,19 +1,11 @@ import { Suspense } from "react" -import { - Box, - Flex, - HStack, - Skeleton, - Spacer, - Text, - VStack, -} from "@chakra-ui/react" -import { Button, Link } from "@opengovsg/design-system-react" +import { Box, Flex, Skeleton, Text, VStack } from "@chakra-ui/react" +import { Button } from "@opengovsg/design-system-react" import { ResourceType } from "~prisma/generated/generatedEnums" -import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" import type { PendingMoveResource } from "~/features/editing-experience/types" import { ResourceItem } from "./ResourceItem" +import { BackButton, HomeHeader } from "./ResourceSelectorHeader" import { SearchBar } from "./SearchBar" import { useResourceStack } from "./useResourceStack" @@ -46,6 +38,24 @@ const SuspensableResourceSelector = ({ setSearchValue, } = useResourceStack({ onChange, selectedResourceId, onlyShowFolders }) + const renderHeader = () => { + if (shouldShowBackButton) { + return ( + { + if (isResourceHighlighted) { + setIsResourceHighlighted(false) + removeFromStack(2) + } else { + removeFromStack(1) + } + }} + /> + ) + } + return + } + return ( @@ -59,52 +69,7 @@ const SuspensableResourceSelector = ({ maxH="20rem" overflowY="auto" > - {shouldShowBackButton ? ( - { - if (isResourceHighlighted) { - setIsResourceHighlighted(false) - removeFromStack(2) - } else { - removeFromStack(1) - } - }} - as="button" - > - - - Back to parent folder - - - ) : ( - - - - / - - - - Home - - - )} + {renderHeader()} {resourceItems.map((item) => { const isHighlighted = isResourceIdHighlighted(item.id) const canClickIntoItem = diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx new file mode 100644 index 0000000000..be8c3e61dc --- /dev/null +++ b/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx @@ -0,0 +1,53 @@ +import { Flex, HStack, Spacer, Text } from "@chakra-ui/react" +import { Link } from "@opengovsg/design-system-react" +import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" + +export const HomeHeader = () => { + return ( + + + + / + + + + Home + + + ) +} + +export const BackButton = ({ + handleOnClick, +}: { + handleOnClick: () => void +}) => { + return ( + + + + Back to parent folder + + + ) +} From 9f7d42f1f9d8cb5c51e4f33f57be8841524ab44e Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Thu, 28 Nov 2024 15:51:42 +0800 Subject: [PATCH 037/104] abstract logic away --- .../ResourceSelector/ResourceSelector.tsx | 32 +++++++------------ .../ResourceSelectorHeader.tsx | 21 ++++++++---- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 07462bf60a..2c355a9b56 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -5,7 +5,7 @@ import { ResourceType } from "~prisma/generated/generatedEnums" import type { PendingMoveResource } from "~/features/editing-experience/types" import { ResourceItem } from "./ResourceItem" -import { BackButton, HomeHeader } from "./ResourceSelectorHeader" +import { ResourceSelectorHeader } from "./ResourceSelectorHeader" import { SearchBar } from "./SearchBar" import { useResourceStack } from "./useResourceStack" @@ -38,24 +38,6 @@ const SuspensableResourceSelector = ({ setSearchValue, } = useResourceStack({ onChange, selectedResourceId, onlyShowFolders }) - const renderHeader = () => { - if (shouldShowBackButton) { - return ( - { - if (isResourceHighlighted) { - setIsResourceHighlighted(false) - removeFromStack(2) - } else { - removeFromStack(1) - } - }} - /> - ) - } - return - } - return ( @@ -69,7 +51,17 @@ const SuspensableResourceSelector = ({ maxH="20rem" overflowY="auto" > - {renderHeader()} + { + if (isResourceHighlighted) { + setIsResourceHighlighted(false) + removeFromStack(2) + } else { + removeFromStack(1) + } + }} + /> {resourceItems.map((item) => { const isHighlighted = isResourceIdHighlighted(item.id) const canClickIntoItem = diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx index be8c3e61dc..309add3f05 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx @@ -2,7 +2,7 @@ import { Flex, HStack, Spacer, Text } from "@chakra-ui/react" import { Link } from "@opengovsg/design-system-react" import { BiHomeAlt, BiLeftArrowAlt } from "react-icons/bi" -export const HomeHeader = () => { +const HomeHeader = () => { return ( { ) } -export const BackButton = ({ - handleOnClick, -}: { - handleOnClick: () => void -}) => { +const BackButton = ({ handleOnClick }: { handleOnClick: () => void }) => { return ( ) } + +export const ResourceSelectorHeader = ({ + shouldShowBackButton, + handleBackButtonClick, +}: { + shouldShowBackButton: boolean + handleBackButtonClick: () => void +}) => { + if (shouldShowBackButton) { + return + } + return +} From 804e2b8da2391d4dd417acc98ed9e53894ec1e2c Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Thu, 28 Nov 2024 15:57:46 +0800 Subject: [PATCH 038/104] add "results count" state --- .../ResourceSelector/ResourceSelector.tsx | 3 ++ .../ResourceSelectorHeader.tsx | 29 +++++++++++++++++++ .../ResourceSelector/useResourceStack.tsx | 1 + 3 files changed, 33 insertions(+) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 2c355a9b56..db64dfdcd0 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -36,6 +36,7 @@ const SuspensableResourceSelector = ({ isResourceIdHighlighted, shouldShowBackButton, setSearchValue, + searchQuery, } = useResourceStack({ onChange, selectedResourceId, onlyShowFolders }) return ( @@ -61,6 +62,8 @@ const SuspensableResourceSelector = ({ removeFromStack(1) } }} + resultsCount={resourceItems.length} + searchQuery={searchQuery} /> {resourceItems.map((item) => { const isHighlighted = isResourceIdHighlighted(item.id) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx index 309add3f05..5d81ae61f0 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx @@ -48,15 +48,44 @@ const BackButton = ({ handleOnClick }: { handleOnClick: () => void }) => { ) } +const SearchResultsHeader = ({ + resultsCount, + searchQuery, +}: { + resultsCount: number + searchQuery: string +}) => { + return ( + + {resultsCount} result{resultsCount > 1 ? "s" : ""} with "{searchQuery}" in + title + + ) +} + export const ResourceSelectorHeader = ({ shouldShowBackButton, handleBackButtonClick, + resultsCount, + searchQuery, }: { shouldShowBackButton: boolean handleBackButtonClick: () => void + resultsCount: number + searchQuery: string }) => { if (shouldShowBackButton) { return } + + if (!!searchQuery && resultsCount > 0) { + return ( + + ) + } + return } diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index 3c332f5e35..50add4137e 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -137,6 +137,7 @@ export const useResourceStack = ({ isResourceIdHighlighted, shouldShowBackButton, setSearchValue, + searchQuery: debouncedSearchTerm, } } From 4ecd1788c6a1bbf612e9dcb021d0e764a4953cf8 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Thu, 28 Nov 2024 16:30:53 +0800 Subject: [PATCH 039/104] option for additional padding based on whether user is using search --- apps/studio/src/components/ResourceSelector/ResourceItem.tsx | 4 +++- .../src/components/ResourceSelector/ResourceSelector.tsx | 4 ++++ .../components/ResourceSelector/ResourceSelectorHeader.tsx | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index 096b464df9..24203797b4 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -12,6 +12,7 @@ interface ResourceItemProps { isDisabled?: boolean isHighlighted: boolean handleOnClick: () => void + hasAdditionalLeftPadding?: boolean } const getButtonProps = ({ isHighlighted }: { isHighlighted: boolean }) => { @@ -36,6 +37,7 @@ const SuspendableResourceItem = ({ isDisabled, isHighlighted, handleOnClick, + hasAdditionalLeftPadding = false, }: ResourceItemProps) => { const buttonProps = getButtonProps({ isHighlighted, @@ -54,7 +56,7 @@ const SuspendableResourceItem = ({ bg: buttonProps._hover.bg, }, })} - pl="2.25rem" + {...(hasAdditionalLeftPadding && { pl: "2.25rem" })} onClick={() => handleOnClick()} leftIcon={} isDisabled={isDisabled} diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index db64dfdcd0..fffc1eb9d2 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -39,6 +39,8 @@ const SuspensableResourceSelector = ({ searchQuery, } = useResourceStack({ onChange, selectedResourceId, onlyShowFolders }) + const isShowingSearchResults = !!searchQuery && searchQuery.length > 0 + return ( @@ -62,6 +64,7 @@ const SuspensableResourceSelector = ({ removeFromStack(1) } }} + isShowingSearchResults={isShowingSearchResults} resultsCount={resourceItems.length} searchQuery={searchQuery} /> @@ -89,6 +92,7 @@ const SuspensableResourceSelector = ({ } addToStack(item) }} + hasAdditionalLeftPadding={!isShowingSearchResults} /> ) })} diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx index 5d81ae61f0..3309d5e9ec 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx @@ -66,11 +66,13 @@ const SearchResultsHeader = ({ export const ResourceSelectorHeader = ({ shouldShowBackButton, handleBackButtonClick, + isShowingSearchResults, resultsCount, searchQuery, }: { shouldShowBackButton: boolean handleBackButtonClick: () => void + isShowingSearchResults: boolean resultsCount: number searchQuery: string }) => { @@ -78,7 +80,7 @@ export const ResourceSelectorHeader = ({ return } - if (!!searchQuery && resultsCount > 0) { + if (isShowingSearchResults) { return ( Date: Fri, 29 Nov 2024 02:57:50 +0800 Subject: [PATCH 040/104] set fixed height to 280px --- .../studio/src/components/ResourceSelector/ResourceSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index fffc1eb9d2..c24fc010d8 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -51,7 +51,7 @@ const SuspensableResourceSelector = ({ w="full" py="0.75rem" px="0.5rem" - maxH="20rem" + h="17.5rem" overflowY="auto" > Date: Fri, 29 Nov 2024 03:16:37 +0800 Subject: [PATCH 041/104] fix text alignment issue --- apps/studio/src/components/ResourceSelector/ResourceItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index 24203797b4..d2a7906d45 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -64,7 +64,7 @@ const SuspendableResourceItem = ({ alignItems="flex-start" gap="0.25rem" > - + {item.title} From 86f049c9348d3b820c927ec0f82a5ffbf33c5919 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Fri, 29 Nov 2024 03:16:49 +0800 Subject: [PATCH 042/104] fix "back to home" padding --- .../src/components/ResourceSelector/ResourceSelectorHeader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx index 3309d5e9ec..4e5259a13e 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelectorHeader.tsx @@ -39,6 +39,7 @@ const BackButton = ({ handleOnClick }: { handleOnClick: () => void }) => { color="base.content.default" onClick={handleOnClick} as="button" + py="0.375rem" > From c347f697d639c64ae7f2709460d1b2d334155437 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Fri, 29 Nov 2024 03:47:49 +0800 Subject: [PATCH 043/104] refactor into new file + add "NoItemsInFolderResult" --- .../ResourceSelector/ResourceSelector.tsx | 58 +++++----- .../ResourceSelector/ResultStates.tsx | 107 ++++++++++++++++++ 2 files changed, 135 insertions(+), 30 deletions(-) create mode 100644 apps/studio/src/components/ResourceSelector/ResultStates.tsx diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index c24fc010d8..355bd4cef1 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -1,11 +1,14 @@ import { Suspense } from "react" import { Box, Flex, Skeleton, Text, VStack } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" -import { ResourceType } from "~prisma/generated/generatedEnums" import type { PendingMoveResource } from "~/features/editing-experience/types" -import { ResourceItem } from "./ResourceItem" import { ResourceSelectorHeader } from "./ResourceSelectorHeader" +import { + NoItemsInFolderResult, + ResourceItemsResults, + ZeroResult, +} from "./ResultStates" import { SearchBar } from "./SearchBar" import { useResourceStack } from "./useResourceStack" @@ -39,6 +42,7 @@ const SuspensableResourceSelector = ({ searchQuery, } = useResourceStack({ onChange, selectedResourceId, onlyShowFolders }) + // TODO: Fix this const isShowingSearchResults = !!searchQuery && searchQuery.length > 0 return ( @@ -53,6 +57,9 @@ const SuspensableResourceSelector = ({ px="0.5rem" h="17.5rem" overflowY="auto" + display="flex" + flexDirection="column" + gap="0.25rem" > - {resourceItems.map((item) => { - const isHighlighted = isResourceIdHighlighted(item.id) - const canClickIntoItem = - item.type === ResourceType.Folder || - item.type === ResourceType.Collection - return ( - { - if (isHighlighted && canClickIntoItem) { - setIsResourceHighlighted(false) - return - } - - if (isResourceHighlighted) { - removeFromStack(1) - } else { - setIsResourceHighlighted(true) - } - addToStack(item) - }} - hasAdditionalLeftPadding={!isShowingSearchResults} - /> - ) - })} + {isShowingSearchResults && resourceItems.length === 0 ? ( + setSearchValue("")} + /> + ) : resourceItems.length === 0 ? ( + + ) : ( + + )} {hasNextPage && ( + + ) +} From 492f4b753a792b4dea9b36f17b38b1040a1dcd7e Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Fri, 29 Nov 2024 03:52:30 +0800 Subject: [PATCH 044/104] refactor - move all logic into ResourceSelector to reduce number of props passed --- .../ResourceSelector/ResourceSelector.tsx | 93 ++++++++++++------- .../ResourceSelectorHeader.tsx | 39 ++------ 2 files changed, 66 insertions(+), 66 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 355bd4cef1..5a9cad946a 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -3,7 +3,11 @@ import { Box, Flex, Skeleton, Text, VStack } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" import type { PendingMoveResource } from "~/features/editing-experience/types" -import { ResourceSelectorHeader } from "./ResourceSelectorHeader" +import { + BackButtonHeader, + HomeHeader, + SearchResultsHeader, +} from "./ResourceSelectorHeader" import { NoItemsInFolderResult, ResourceItemsResults, @@ -45,6 +49,58 @@ const SuspensableResourceSelector = ({ // TODO: Fix this const isShowingSearchResults = !!searchQuery && searchQuery.length > 0 + const renderHeader = () => { + if (shouldShowBackButton) { + return ( + { + if (isResourceHighlighted) { + setIsResourceHighlighted(false) + removeFromStack(2) + } else { + removeFromStack(1) + } + }} + /> + ) + } + if (isShowingSearchResults) { + return ( + + ) + } + return + } + + const renderContent = () => { + if (isShowingSearchResults && resourceItems.length === 0) { + return ( + setSearchValue("")} + /> + ) + } + if (resourceItems.length === 0) { + return + } + return ( + + ) + } + return ( @@ -61,39 +117,8 @@ const SuspensableResourceSelector = ({ flexDirection="column" gap="0.25rem" > - { - if (isResourceHighlighted) { - setIsResourceHighlighted(false) - removeFromStack(2) - } else { - removeFromStack(1) - } - }} - isShowingSearchResults={isShowingSearchResults} - resultsCount={resourceItems.length} - searchQuery={searchQuery} - /> - {isShowingSearchResults && resourceItems.length === 0 ? ( - setSearchValue("")} - /> - ) : resourceItems.length === 0 ? ( - - ) : ( - - )} + {renderHeader()} + {renderContent()} {hasNextPage && ( ) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 89f1777b89..5b11a806c9 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -7,6 +7,7 @@ import type { PendingMoveResource } from "~/features/editing-experience/types" import { useSearchQuery } from "~/hooks/useSearchQuery" import { getUserViewableResourceTypes } from "~/utils/resources" import { + LoadingResourceItemsResults, NoItemsInFolderResult, ResourceItemsResults, ZeroResult, @@ -14,6 +15,7 @@ import { import { BackButtonHeader, HomeHeader, + LoadingHeader, SearchResultsHeader, } from "./ResourceSelectorHeader" import { SearchBar } from "./SearchBar" @@ -38,7 +40,7 @@ const SuspensableResourceSelector = ({ searchValue, setSearchValue, debouncedSearchTerm: searchQuery, - resources, + isLoading, } = useSearchQuery({ siteId: String(siteId), resourceTypes: onlyShowFolders @@ -70,6 +72,9 @@ const SuspensableResourceSelector = ({ const isShowingSearchResults = !!searchQuery && searchQuery.length > 0 const renderHeader = () => { + if (isLoading) { + return + } if (shouldShowBackButton) { return ( { + if (isLoading) { + return + } if (isShowingSearchResults && resourceItems.length === 0) { return ( { @@ -22,16 +22,21 @@ export const NoItemsInFolderResult = () => { ) } -interface ResourceItemsProps { - resourceItems: ResourceItemContent[] - isResourceIdHighlighted: (resourceId: string) => boolean - existingResource: PendingMoveResource | undefined - isResourceHighlighted: boolean - setIsResourceHighlighted: (isHighlighted: boolean) => void - addToStack: (item: ResourceItemContent) => void - removeFromStack: (numberOfResources: number) => void - hasAdditionalLeftPadding: boolean +export const LoadingResourceItemsResults = () => { + return Array.from({ length: 5 }).map((_, index) => ( + + )) } + export const ResourceItemsResults = ({ resourceItems, isResourceIdHighlighted, @@ -41,7 +46,16 @@ export const ResourceItemsResults = ({ addToStack, removeFromStack, hasAdditionalLeftPadding, -}: ResourceItemsProps) => { +}: { + resourceItems: ResourceItemContent[] + isResourceIdHighlighted: (resourceId: string) => boolean + existingResource: PendingMoveResource | undefined + isResourceHighlighted: boolean + setIsResourceHighlighted: (isHighlighted: boolean) => void + addToStack: (item: ResourceItemContent) => void + removeFromStack: (numberOfResources: number) => void + hasAdditionalLeftPadding: boolean +}) => { return resourceItems.map((item) => { const isHighlighted = isResourceIdHighlighted(item.id) const canClickIntoItem = @@ -71,14 +85,13 @@ export const ResourceItemsResults = ({ }) } -interface ZeroResultProps { - searchQuery: string - handleClickClearSearch: () => void -} export const ZeroResult = ({ searchQuery, handleClickClearSearch, -}: ZeroResultProps) => { +}: { + searchQuery: string + handleClickClearSearch: () => void +}) => { return ( { ) } +export const LoadingHeader = () => { + return ( + + Searching your website, high and low + + ) +} + export const BackButtonHeader = ({ handleOnClick, }: { From 6547e6c8e0281d0b33653e5460e3c379883aa80a Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Fri, 29 Nov 2024 13:19:09 +0800 Subject: [PATCH 053/104] remove unused import --- apps/studio/src/components/ResourceSelector/useResourceStack.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index b480cce2ec..84755560d2 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -1,5 +1,4 @@ import { useCallback, useEffect, useMemo, useState } from "react" -import { ResourceType } from "~prisma/generated/generatedEnums" import type { PendingMoveResource } from "~/features/editing-experience/types" import type { ResourceItemContent } from "~/schemas/resource" From 52a8014a9222b353a8cdd2edb3b16826ec6c3959 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Fri, 29 Nov 2024 14:07:03 +0800 Subject: [PATCH 054/104] prep: abstract logic into useResourceStack for easier refactoring --- .../ResourceSelector/ResourceSelector.tsx | 7 ++--- .../ResourceSelectorContent.tsx | 29 +++--------------- .../ResourceSelector/useResourceStack.tsx | 30 ++++++++++++++++++- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 5b11a806c9..b79c5b98f9 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -57,10 +57,10 @@ const SuspensableResourceSelector = ({ fetchNextPage, hasNextPage, isFetchingNextPage, - addToStack, removeFromStack, isResourceIdHighlighted, shouldShowBackButton, + resourceItemHandleClick, } = useResourceStack({ siteId, onChange, @@ -120,11 +120,8 @@ const SuspensableResourceSelector = ({ resourceItems={resourceItems} isResourceIdHighlighted={isResourceIdHighlighted} existingResource={existingResource} - isResourceHighlighted={isResourceHighlighted} - setIsResourceHighlighted={setIsResourceHighlighted} - addToStack={addToStack} - removeFromStack={removeFromStack} hasAdditionalLeftPadding={!isShowingSearchResults} + resourceItemHandleClick={resourceItemHandleClick} /> ) } diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx index 254ee8fb72..71c02a5950 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx @@ -41,44 +41,23 @@ export const ResourceItemsResults = ({ resourceItems, isResourceIdHighlighted, existingResource, - isResourceHighlighted, - setIsResourceHighlighted, - addToStack, - removeFromStack, hasAdditionalLeftPadding, + resourceItemHandleClick, }: { resourceItems: ResourceItemContent[] isResourceIdHighlighted: (resourceId: string) => boolean existingResource: PendingMoveResource | undefined - isResourceHighlighted: boolean - setIsResourceHighlighted: (isHighlighted: boolean) => void - addToStack: (item: ResourceItemContent) => void - removeFromStack: (numberOfResources: number) => void hasAdditionalLeftPadding: boolean + resourceItemHandleClick: (item: ResourceItemContent) => void }) => { return resourceItems.map((item) => { - const isHighlighted = isResourceIdHighlighted(item.id) - const canClickIntoItem = - item.type === ResourceType.Folder || item.type === ResourceType.Collection return ( { - if (isHighlighted && canClickIntoItem) { - setIsResourceHighlighted(false) - return - } - - if (isResourceHighlighted) { - removeFromStack(1) - } else { - setIsResourceHighlighted(true) - } - addToStack(item) - }} + isHighlighted={isResourceIdHighlighted(item.id)} + handleOnClick={() => resourceItemHandleClick(item)} hasAdditionalLeftPadding={hasAdditionalLeftPadding} /> ) diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index 84755560d2..5cff7c000d 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from "react" +import { ResourceType } from "~prisma/generated/generatedEnums" import type { PendingMoveResource } from "~/features/editing-experience/types" import type { ResourceItemContent } from "~/schemas/resource" @@ -101,6 +102,33 @@ export const useResourceStack = ({ return resourceStack.map((resource) => resource.permalink).join("/") }, [resourceStack]) + const resourceItemHandleClick = useCallback( + (item: ResourceItemContent): void => { + const isItemHighlighted = isResourceIdHighlighted(item.id) + const canClickIntoItem = + item.type === ResourceType.Folder || + item.type === ResourceType.Collection + + if (isItemHighlighted && canClickIntoItem) { + setIsResourceHighlighted(false) + return + } + + if (isResourceHighlighted) { + removeFromStack(1) + } else { + setIsResourceHighlighted(true) + } + addToStack(item) + }, + [ + isResourceIdHighlighted, + isResourceHighlighted, + addToStack, + removeFromStack, + ], + ) + useEffect(() => { if ( ancestryStack.length <= 0 || @@ -126,10 +154,10 @@ export const useResourceStack = ({ fetchNextPage, hasNextPage, isFetchingNextPage, - addToStack, removeFromStack, isResourceIdHighlighted, shouldShowBackButton, + resourceItemHandleClick, } } From 9797551135942725c5c8fddc63341ebbe034156d Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Fri, 29 Nov 2024 15:16:43 +0800 Subject: [PATCH 055/104] refactor into getAncestryWithSelf + add getBatchAncestryWithSelf endpoint --- apps/studio/src/schemas/resource.ts | 5 ++ .../modules/resource/resource.router.ts | 54 ++++++------------- .../modules/resource/resource.service.ts | 46 ++++++++++++++++ 3 files changed, 68 insertions(+), 37 deletions(-) diff --git a/apps/studio/src/schemas/resource.ts b/apps/studio/src/schemas/resource.ts index 770caeb2b6..528f1515cf 100644 --- a/apps/studio/src/schemas/resource.ts +++ b/apps/studio/src/schemas/resource.ts @@ -73,6 +73,11 @@ export const getAncestryWithSelfSchema = z.object({ resourceId: z.string().optional(), }) +export const getBatchAncestryWithSelfSchema = z.object({ + siteId: z.string(), + resourceIds: z.array(z.string()), +}) + export interface ResourceItemContent { title: string permalink: string diff --git a/apps/studio/src/server/modules/resource/resource.router.ts b/apps/studio/src/server/modules/resource/resource.router.ts index f32de88ba3..8fbe9fb05b 100644 --- a/apps/studio/src/server/modules/resource/resource.router.ts +++ b/apps/studio/src/server/modules/resource/resource.router.ts @@ -9,6 +9,7 @@ import { countResourceSchema, deleteResourceSchema, getAncestryWithSelfSchema, + getBatchAncestryWithSelfSchema, getChildrenSchema, getFullPermalinkSchema, getMetadataSchema, @@ -28,6 +29,7 @@ import { } from "../permissions/permissions.service" import { validateUserPermissionsForSite } from "../site/site.service" import { + getAncestryWithSelf, getSearchRecentlyEdited, getSearchResults, getWithFullPermalink, @@ -467,44 +469,22 @@ export const resourceRouter = router({ if (!resourceId) { return [] } + return await getAncestryWithSelf({ + siteId: Number(siteId), + resourceId: Number(resourceId), + }) + }), - const ancestorsWithSelf = await db - .withRecursive("Resources", (eb) => - eb - .selectFrom("Resource") - .select([ - "Resource.id", - "Resource.title", - "Resource.permalink", - "Resource.parentId", - ]) - .where("Resource.siteId", "=", Number(siteId)) - .where("Resource.id", "=", resourceId) - .where((eb) => - eb.and([eb("Resource.type", "!=", ResourceType.RootPage)]), - ) - .unionAll( - eb - .selectFrom("Resource") - .innerJoin("Resources", "Resources.parentId", "Resource.id") - .select([ - "Resource.id", - "Resource.title", - "Resource.permalink", - "Resource.parentId", - ]), - ), - ) - .selectFrom("Resources") - .select([ - "Resources.id", - "Resources.title", - "Resources.permalink", - "Resources.parentId", - ]) - .execute() - - return ancestorsWithSelf.reverse() + getBatchAncestryWithSelf: protectedProcedure + .input(getBatchAncestryWithSelfSchema) + .query(async ({ input: { siteId, resourceIds } }) => { + const ancestryPromises = resourceIds.map(async (id) => { + return await getAncestryWithSelf({ + siteId: Number(siteId), + resourceId: Number(id), + }) + }) + return await Promise.all(ancestryPromises) }), search: protectedProcedure diff --git a/apps/studio/src/server/modules/resource/resource.service.ts b/apps/studio/src/server/modules/resource/resource.service.ts index 03f1cc393b..971ae972ad 100644 --- a/apps/studio/src/server/modules/resource/resource.service.ts +++ b/apps/studio/src/server/modules/resource/resource.service.ts @@ -411,6 +411,52 @@ export const publishResource = async ( return addedVersionResult } +export const getAncestryWithSelf = async ({ + siteId, + resourceId, +}: { + siteId: number + resourceId: number +}) => { + const ancestorsWithSelf = await db + .withRecursive("Resources", (eb) => + eb + .selectFrom("Resource") + .select([ + "Resource.id", + "Resource.title", + "Resource.permalink", + "Resource.parentId", + ]) + .where("Resource.siteId", "=", Number(siteId)) + .where("Resource.id", "=", String(resourceId)) + .where((eb) => + eb.and([eb("Resource.type", "!=", ResourceType.RootPage)]), + ) + .unionAll( + eb + .selectFrom("Resource") + .innerJoin("Resources", "Resources.parentId", "Resource.id") + .select([ + "Resource.id", + "Resource.title", + "Resource.permalink", + "Resource.parentId", + ]), + ), + ) + .selectFrom("Resources") + .select([ + "Resources.id", + "Resources.title", + "Resources.permalink", + "Resources.parentId", + ]) + .execute() + + return ancestorsWithSelf.reverse() +} + export const getWithFullPermalink = async ({ resourceId, }: { From 88cffc6ba1ec7922e3db24bc0c5d1897e5659818 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Fri, 29 Nov 2024 16:09:09 +0800 Subject: [PATCH 056/104] fix - use handleOnClick directly --- apps/studio/src/components/ResourceSelector/ResourceItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx index 86d90fd151..ca56325015 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceItem.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceItem.tsx @@ -59,7 +59,7 @@ const SuspendableResourceItem = ({ }, })} {...(hasAdditionalLeftPadding && { pl: "2.25rem" })} - onClick={() => handleOnClick()} + onClick={handleOnClick} leftIcon={} isDisabled={isDisabled} height="fit-content" From bfc2c7bce344c7a5039dd5d247f009ad75024e66 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Fri, 29 Nov 2024 16:19:30 +0800 Subject: [PATCH 057/104] fix - make resourceTypes optional for search endpoint --- apps/studio/src/schemas/resource.ts | 2 +- .../studio/src/server/modules/resource/resource.router.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/schemas/resource.ts b/apps/studio/src/schemas/resource.ts index 528f1515cf..e0d6d45052 100644 --- a/apps/studio/src/schemas/resource.ts +++ b/apps/studio/src/schemas/resource.ts @@ -94,7 +94,7 @@ export const searchSchema = z .object({ siteId: z.string(), query: z.string().optional(), - resourceTypes: z.array(z.nativeEnum(ResourceType)), + resourceTypes: z.array(z.nativeEnum(ResourceType)).optional(), }) .merge(infiniteOffsetPaginationSchema) diff --git a/apps/studio/src/server/modules/resource/resource.router.ts b/apps/studio/src/server/modules/resource/resource.router.ts index 8fbe9fb05b..534ec298d8 100644 --- a/apps/studio/src/server/modules/resource/resource.router.ts +++ b/apps/studio/src/server/modules/resource/resource.router.ts @@ -493,7 +493,13 @@ export const resourceRouter = router({ .query( async ({ ctx, - input: { siteId, query = "", resourceTypes, cursor: offset, limit }, + input: { + siteId, + query = "", + resourceTypes = Object.values(ResourceType), + cursor: offset, + limit, + }, }) => { await validateUserPermissionsForSite({ siteId: Number(siteId), From c538e9078ce9b1a47e9b76997f66342c6425ea99 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Fri, 29 Nov 2024 16:19:35 +0800 Subject: [PATCH 058/104] fix test --- .../server/modules/resource/__tests__/resource.router.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/studio/src/server/modules/resource/__tests__/resource.router.test.ts b/apps/studio/src/server/modules/resource/__tests__/resource.router.test.ts index f1d3236c43..fe0a30500b 100644 --- a/apps/studio/src/server/modules/resource/__tests__/resource.router.test.ts +++ b/apps/studio/src/server/modules/resource/__tests__/resource.router.test.ts @@ -17,6 +17,7 @@ import { } from "tests/integration/helpers/seed" import { createCallerFactory } from "~/server/trpc" +import { getUserViewableResourceTypes } from "~/utils/resources" import { db } from "../../database" import { resourceRouter } from "../resource.router" @@ -2306,7 +2307,7 @@ describe("resource.router", async () => { expect(result).toEqual(expected) }) - it("should only return user viewable resource types", async () => { + it("should only return user viewable resource types if specified", async () => { // Arrange const { site } = await setupSite() await setupAdminPermissions({ @@ -2336,6 +2337,7 @@ describe("resource.router", async () => { const result = await caller.search({ siteId: String(site.id), query: "test", + resourceTypes: getUserViewableResourceTypes(), }) // Assert From ab64810252ba1bdb3889829703121ebc99b93f4b Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Sat, 30 Nov 2024 02:27:29 +0800 Subject: [PATCH 059/104] standardize types into 1 --- .../ResourceSelector/ResourceSelector.tsx | 6 +- .../ResourceSelectorContent.tsx | 6 +- .../ResourceSelector/useResourceStack.tsx | 55 +++++++++++-------- .../ResourceTable/ResourceTableMenu.tsx | 2 +- .../src/features/editing-experience/atoms.ts | 4 +- .../MoveResourceModal/MoveResourceModal.tsx | 11 ++-- .../src/features/editing-experience/types.ts | 6 -- apps/studio/src/schemas/resource.ts | 1 + .../modules/resource/resource.router.ts | 4 +- .../modules/resource/resource.service.ts | 3 + apps/studio/tests/msw/handlers/resource.ts | 3 + 11 files changed, 55 insertions(+), 46 deletions(-) delete mode 100644 apps/studio/src/features/editing-experience/types.ts diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index b79c5b98f9..003916ddc9 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -3,7 +3,7 @@ import { Box, Flex, Skeleton, Text, VStack } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" import { ResourceType } from "~prisma/generated/generatedEnums" -import type { PendingMoveResource } from "~/features/editing-experience/types" +import type { ResourceItemContent } from "~/schemas/resource" import { useSearchQuery } from "~/hooks/useSearchQuery" import { getUserViewableResourceTypes } from "~/utils/resources" import { @@ -25,7 +25,7 @@ interface ResourceSelectorProps { siteId: number onChange: (resourceId: string) => void selectedResourceId?: string - existingResource?: PendingMoveResource + existingResource?: ResourceItemContent onlyShowFolders?: boolean } @@ -41,6 +41,7 @@ const SuspensableResourceSelector = ({ setSearchValue, debouncedSearchTerm: searchQuery, isLoading, + resources, } = useSearchQuery({ siteId: String(siteId), resourceTypes: onlyShowFolders @@ -66,6 +67,7 @@ const SuspensableResourceSelector = ({ onChange, selectedResourceId, onlyShowFolders, + resourceIds: resources.map((resource) => resource.id), }) // TODO: Fix this diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx index 71c02a5950..3c5a74896b 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx @@ -2,7 +2,6 @@ import { Text, VStack } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" import { ResourceType } from "~prisma/generated/generatedEnums" -import type { PendingMoveResource } from "~/features/editing-experience/types" import type { ResourceItemContent } from "~/schemas/resource" import { ResourceItem } from "./ResourceItem" @@ -31,6 +30,7 @@ export const LoadingResourceItemsResults = () => { title: `Loading...`, type: ResourceType.Folder, permalink: "", + parentId: null, }} isLoading={true} /> @@ -46,7 +46,7 @@ export const ResourceItemsResults = ({ }: { resourceItems: ResourceItemContent[] isResourceIdHighlighted: (resourceId: string) => boolean - existingResource: PendingMoveResource | undefined + existingResource: ResourceItemContent | undefined hasAdditionalLeftPadding: boolean resourceItemHandleClick: (item: ResourceItemContent) => void }) => { @@ -55,7 +55,7 @@ export const ResourceItemsResults = ({ resourceItemHandleClick(item)} hasAdditionalLeftPadding={hasAdditionalLeftPadding} diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index 5cff7c000d..6b17f5f57a 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from "react" import { ResourceType } from "~prisma/generated/generatedEnums" -import type { PendingMoveResource } from "~/features/editing-experience/types" import type { ResourceItemContent } from "~/schemas/resource" import { trpc } from "~/utils/trpc" @@ -10,16 +9,18 @@ export const useResourceStack = ({ onChange, selectedResourceId, onlyShowFolders, + resourceIds = [], }: { siteId: number onChange: (resourceId: string) => void selectedResourceId: string | undefined onlyShowFolders: boolean + resourceIds?: string[] }) => { // NOTE: This is the stack of user's navigation through the resource tree // NOTE: We should always start the stack from `/` (root) // so that the user will see a full overview of their site structure - const [resourceStack, setResourceStack] = useState([]) + const [resourceStack, setResourceStack] = useState([]) const [isResourceHighlighted, setIsResourceHighlighted] = useState(true) @@ -32,14 +33,15 @@ export const useResourceStack = ({ () => resourceStack[resourceStack.length - 2], [resourceStack], ) - const curResourceId = useMemo(() => moveDest?.resourceId, [moveDest]) + const curResourceId = useMemo(() => moveDest?.id, [moveDest]) - const ancestryStack: PendingMoveResource[] = trpc.resource.getAncestryWithSelf - .useSuspenseQuery({ - siteId: String(siteId), - resourceId: selectedResourceId, - })[0] - .map((resource) => ({ ...resource, resourceId: resource.id })) + const existingAncestryStack: ResourceItemContent[] = + trpc.resource.getAncestryWithSelf + .useSuspenseQuery({ + siteId: String(siteId), + resourceId: selectedResourceId, + })[0] + .map((resource) => ({ ...resource, resourceId: resource.id })) const queryFn = onlyShowFolders ? trpc.resource.getFolderChildrenOf.useInfiniteQuery @@ -52,9 +54,7 @@ export const useResourceStack = ({ } = queryFn( { resourceId: - (isResourceHighlighted - ? parentDest?.resourceId - : moveDest?.resourceId) ?? null, + (isResourceHighlighted ? parentDest?.id : moveDest?.id) ?? null, siteId: String(siteId), limit: 25, }, @@ -62,22 +62,31 @@ export const useResourceStack = ({ getNextPageParam: (lastPage) => lastPage.nextOffset, }, ) + + const resourceItemsWithAncestryStack: ResourceItemContent[][] = + trpc.resource.getBatchAncestryWithSelf + .useSuspenseQuery({ + siteId: String(siteId), + resourceIds: + resourceIds.length > 0 + ? resourceIds + : pages.flatMap(({ items }) => items).map((item) => item.id), + })[0] + .map((resource) => resource.map((r) => ({ ...r, resourceId: r.id }))) + + console.log(2222, resourceItemsWithAncestryStack) + const resourceItems: ResourceItemContent[] = useMemo( () => pages.flatMap(({ items }) => items), [pages], ) + console.log(1111, resourceItems) const addToStack = useCallback( (resourceItemContent: ResourceItemContent): void => { - const newResource: PendingMoveResource = { - permalink: resourceItemContent.permalink, - title: resourceItemContent.title, - resourceId: resourceItemContent.id, - parentId: parentDest?.resourceId ?? null, - } - setResourceStack((prev) => [...prev, newResource]) + setResourceStack((prev) => [...prev, resourceItemContent]) }, - [parentDest], + [], ) const removeFromStack = useCallback((numberOfResources: number): void => { @@ -131,12 +140,12 @@ export const useResourceStack = ({ useEffect(() => { if ( - ancestryStack.length <= 0 || - JSON.stringify(ancestryStack) === JSON.stringify(resourceStack) + existingAncestryStack.length <= 0 || + JSON.stringify(existingAncestryStack) === JSON.stringify(resourceStack) ) { return } - setResourceStack(ancestryStack) + setResourceStack(existingAncestryStack) }, []) useEffect(() => { diff --git a/apps/studio/src/features/dashboard/components/ResourceTable/ResourceTableMenu.tsx b/apps/studio/src/features/dashboard/components/ResourceTable/ResourceTableMenu.tsx index e71dd6dbd0..e3c1672243 100644 --- a/apps/studio/src/features/dashboard/components/ResourceTable/ResourceTableMenu.tsx +++ b/apps/studio/src/features/dashboard/components/ResourceTable/ResourceTableMenu.tsx @@ -39,7 +39,7 @@ export const ResourceTableMenu = ({ }: ResourceTableMenuProps) => { const setMoveResource = useSetAtom(moveResourceAtom) const handleMoveResourceClick = () => - setMoveResource({ resourceId, title, permalink, parentId }) + setMoveResource({ id: resourceId, title, permalink, parentId, type }) const setResourceModalState = useSetAtom(deleteResourceModalAtom) const setFolderSettingsModalState = useSetAtom(folderSettingsModalAtom) const setPageSettingsModalState = useSetAtom(pageSettingsModalAtom) diff --git a/apps/studio/src/features/editing-experience/atoms.ts b/apps/studio/src/features/editing-experience/atoms.ts index 650a46b40b..088a20806a 100644 --- a/apps/studio/src/features/editing-experience/atoms.ts +++ b/apps/studio/src/features/editing-experience/atoms.ts @@ -1,9 +1,9 @@ import { format } from "date-fns" import { atom } from "jotai" -import type { PendingMoveResource } from "./types" +import type { ResourceItemContent } from "~/schemas/resource" -export const moveResourceAtom = atom(null) +export const moveResourceAtom = atom(null) export interface CollectionLinkProps { ref: string diff --git a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx index 2ac24b4588..2b8027b5ae 100644 --- a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx +++ b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx @@ -31,10 +31,7 @@ export const MoveResourceModal = () => { {moveItem && ( - + )} ) @@ -91,7 +88,7 @@ const MoveResourceContent = withSuspense( // and invalidate the new + old folders await utils.folder.getMetadata.invalidate() await utils.resource.getMetadataById.invalidate({ - resourceId: movedItem?.resourceId, + resourceId: movedItem?.id, }) toast({ title: "Resource moved!" }) }, @@ -136,10 +133,10 @@ const MoveResourceContent = withSuspense( } isLoading={isLoading} onClick={() => - movedItem?.resourceId && + movedItem?.id && mutate({ siteId, - movedResourceId: movedItem.resourceId, + movedResourceId: movedItem.id, destinationResourceId: curResourceId, }) } diff --git a/apps/studio/src/features/editing-experience/types.ts b/apps/studio/src/features/editing-experience/types.ts deleted file mode 100644 index cef3716b47..0000000000 --- a/apps/studio/src/features/editing-experience/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface PendingMoveResource { - permalink: string - title: string - resourceId: string - parentId: string | null -} diff --git a/apps/studio/src/schemas/resource.ts b/apps/studio/src/schemas/resource.ts index e0d6d45052..4f0cca5334 100644 --- a/apps/studio/src/schemas/resource.ts +++ b/apps/studio/src/schemas/resource.ts @@ -83,6 +83,7 @@ export interface ResourceItemContent { permalink: string type: ResourceType id: string + parentId: string | null } export interface ResourceChildrenOfType { diff --git a/apps/studio/src/server/modules/resource/resource.router.ts b/apps/studio/src/server/modules/resource/resource.router.ts index 534ec298d8..086ba2cbc8 100644 --- a/apps/studio/src/server/modules/resource/resource.router.ts +++ b/apps/studio/src/server/modules/resource/resource.router.ts @@ -124,7 +124,7 @@ export const resourceRouter = router({ let query = db .selectFrom("Resource") - .select(["title", "permalink", "type", "id"]) + .select(["title", "permalink", "type", "id", "parentId"]) .where("Resource.type", "in", [ResourceType.Folder]) .where("Resource.siteId", "=", Number(siteId)) .$narrowType<{ @@ -171,7 +171,7 @@ export const resourceRouter = router({ } let query = db .selectFrom("Resource") - .select(["title", "permalink", "type", "id"]) + .select(["title", "permalink", "type", "id", "parentId"]) .where("Resource.type", "!=", ResourceType.RootPage) .where("Resource.type", "!=", ResourceType.IndexPage) .where("Resource.type", "!=", ResourceType.FolderMeta) diff --git a/apps/studio/src/server/modules/resource/resource.service.ts b/apps/studio/src/server/modules/resource/resource.service.ts index 971ae972ad..6fe5ab3d89 100644 --- a/apps/studio/src/server/modules/resource/resource.service.ts +++ b/apps/studio/src/server/modules/resource/resource.service.ts @@ -427,6 +427,7 @@ export const getAncestryWithSelf = async ({ "Resource.title", "Resource.permalink", "Resource.parentId", + "Resource.type", ]) .where("Resource.siteId", "=", Number(siteId)) .where("Resource.id", "=", String(resourceId)) @@ -442,6 +443,7 @@ export const getAncestryWithSelf = async ({ "Resource.title", "Resource.permalink", "Resource.parentId", + "Resources.type", ]), ), ) @@ -451,6 +453,7 @@ export const getAncestryWithSelf = async ({ "Resources.title", "Resources.permalink", "Resources.parentId", + "Resources.type", ]) .execute() diff --git a/apps/studio/tests/msw/handlers/resource.ts b/apps/studio/tests/msw/handlers/resource.ts index b3407b0e79..7ad33758b0 100644 --- a/apps/studio/tests/msw/handlers/resource.ts +++ b/apps/studio/tests/msw/handlers/resource.ts @@ -15,6 +15,7 @@ export const resourceHandlers = { | "CollectionPage", // ID must be unique so infinite loop won't occur id: `${resourceId}-${item.title}-${item.id}`, + parentId: item.parentId, })) return { items, @@ -52,12 +53,14 @@ export const resourceHandlers = { id: "1", title: "Homepage", permalink: "/", + type: "RootPage", }, { parentId: "1", id: "2", title: "Collection", permalink: "collection", + type: "Collection", }, ] }) From 14db37f50866c905126dabc27a1f138b92c66c72 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Sat, 30 Nov 2024 14:22:00 +0800 Subject: [PATCH 060/104] hide logic in hook + fix search --- .../ResourceSelector/ResourceSelector.tsx | 61 ++++---- .../ResourceSelectorContent.tsx | 30 ++-- .../ResourceSelector/useResourceStack.tsx | 135 +++++++++--------- 3 files changed, 114 insertions(+), 112 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 003916ddc9..55d75b1f19 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -49,81 +49,68 @@ const SuspensableResourceSelector = ({ : getUserViewableResourceTypes(), }) + const isSearchQueryEmpty: boolean = searchQuery.trim().length === 0 + const { fullPermalink, - isResourceHighlighted, - setIsResourceHighlighted, moveDest, - resourceItems, + resourceItemsWithAncestryStack, fetchNextPage, hasNextPage, isFetchingNextPage, - removeFromStack, isResourceIdHighlighted, shouldShowBackButton, - resourceItemHandleClick, + handleClickBackButton, + handleClickResourceItem, } = useResourceStack({ siteId, onChange, selectedResourceId, onlyShowFolders, - resourceIds: resources.map((resource) => resource.id), + resourceIds: isSearchQueryEmpty + ? undefined + : resources.map((resource) => resource.id), }) - // TODO: Fix this - const isShowingSearchResults = !!searchQuery && searchQuery.length > 0 - const renderHeader = () => { if (isLoading) { return } if (shouldShowBackButton) { - return ( - { - if (isResourceHighlighted) { - setIsResourceHighlighted(false) - removeFromStack(2) - } else { - removeFromStack(1) - } - }} - /> - ) + return } - if (isShowingSearchResults) { - return ( - - ) + if (isSearchQueryEmpty) { + return } - return + return ( + + ) } const renderContent = () => { if (isLoading) { return } - if (isShowingSearchResults && resourceItems.length === 0) { - return ( + if (resourceItemsWithAncestryStack.length === 0) { + return isSearchQueryEmpty ? ( + + ) : ( setSearchValue("")} /> ) } - if (resourceItems.length === 0) { - return - } return ( ) } diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx index 3c5a74896b..5f56b72992 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx @@ -4,6 +4,7 @@ import { ResourceType } from "~prisma/generated/generatedEnums" import type { ResourceItemContent } from "~/schemas/resource" import { ResourceItem } from "./ResourceItem" +import { lastResourceItemInAncestryStack } from "./useResourceStack" export const NoItemsInFolderResult = () => { return ( @@ -38,26 +39,35 @@ export const LoadingResourceItemsResults = () => { } export const ResourceItemsResults = ({ - resourceItems, + resourceItemsWithAncestryStack, isResourceIdHighlighted, existingResource, hasAdditionalLeftPadding, - resourceItemHandleClick, + handleClickResourceItem, }: { - resourceItems: ResourceItemContent[] + resourceItemsWithAncestryStack: ResourceItemContent[][] isResourceIdHighlighted: (resourceId: string) => boolean existingResource: ResourceItemContent | undefined hasAdditionalLeftPadding: boolean - resourceItemHandleClick: (item: ResourceItemContent) => void + handleClickResourceItem: ( + resourceItemWithAncestryStack: ResourceItemContent[], + ) => void }) => { - return resourceItems.map((item) => { + return resourceItemsWithAncestryStack.map((resourceItemWithAncestryStack) => { + const lastChild = lastResourceItemInAncestryStack( + resourceItemWithAncestryStack, + ) + if (!lastChild) return + return ( resourceItemHandleClick(item)} + key={lastChild.id} + item={lastChild} + isDisabled={lastChild.id === existingResource?.id} + isHighlighted={isResourceIdHighlighted(lastChild.id)} + handleOnClick={() => + handleClickResourceItem(resourceItemWithAncestryStack) + } hasAdditionalLeftPadding={hasAdditionalLeftPadding} /> ) diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index 6b17f5f57a..ce777cf032 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -4,18 +4,24 @@ import { ResourceType } from "~prisma/generated/generatedEnums" import type { ResourceItemContent } from "~/schemas/resource" import { trpc } from "~/utils/trpc" +export const lastResourceItemInAncestryStack = ( + resourceItemWithAncestryStack: ResourceItemContent[], +): ResourceItemContent | undefined => { + return resourceItemWithAncestryStack[resourceItemWithAncestryStack.length - 1] +} + export const useResourceStack = ({ siteId, onChange, selectedResourceId, onlyShowFolders, - resourceIds = [], + resourceIds, }: { siteId: number onChange: (resourceId: string) => void selectedResourceId: string | undefined onlyShowFolders: boolean - resourceIds?: string[] + resourceIds?: ResourceItemContent["id"][] }) => { // NOTE: This is the stack of user's navigation through the resource tree // NOTE: We should always start the stack from `/` (root) @@ -26,23 +32,15 @@ export const useResourceStack = ({ useState(true) const moveDest = useMemo( - () => resourceStack[resourceStack.length - 1], + () => resourceStack[resourceStack.length - 1], // last item in stack [resourceStack], ) const parentDest = useMemo( - () => resourceStack[resourceStack.length - 2], + () => resourceStack[resourceStack.length - 2], // second last item in stack [resourceStack], ) const curResourceId = useMemo(() => moveDest?.id, [moveDest]) - const existingAncestryStack: ResourceItemContent[] = - trpc.resource.getAncestryWithSelf - .useSuspenseQuery({ - siteId: String(siteId), - resourceId: selectedResourceId, - })[0] - .map((resource) => ({ ...resource, resourceId: resource.id })) - const queryFn = onlyShowFolders ? trpc.resource.getFolderChildrenOf.useInfiniteQuery : trpc.resource.getChildrenOf.useInfiniteQuery @@ -67,28 +65,12 @@ export const useResourceStack = ({ trpc.resource.getBatchAncestryWithSelf .useSuspenseQuery({ siteId: String(siteId), - resourceIds: - resourceIds.length > 0 - ? resourceIds - : pages.flatMap(({ items }) => items).map((item) => item.id), + resourceIds: !!resourceIds + ? resourceIds + : pages.flatMap(({ items }) => items).map((item) => item.id), })[0] .map((resource) => resource.map((r) => ({ ...r, resourceId: r.id }))) - console.log(2222, resourceItemsWithAncestryStack) - - const resourceItems: ResourceItemContent[] = useMemo( - () => pages.flatMap(({ items }) => items), - [pages], - ) - console.log(1111, resourceItems) - - const addToStack = useCallback( - (resourceItemContent: ResourceItemContent): void => { - setResourceStack((prev) => [...prev, resourceItemContent]) - }, - [], - ) - const removeFromStack = useCallback((numberOfResources: number): void => { setResourceStack((prev) => prev.slice(0, -numberOfResources)) }, []) @@ -111,41 +93,66 @@ export const useResourceStack = ({ return resourceStack.map((resource) => resource.permalink).join("/") }, [resourceStack]) - const resourceItemHandleClick = useCallback( - (item: ResourceItemContent): void => { - const isItemHighlighted = isResourceIdHighlighted(item.id) - const canClickIntoItem = - item.type === ResourceType.Folder || - item.type === ResourceType.Collection - - if (isItemHighlighted && canClickIntoItem) { - setIsResourceHighlighted(false) - return - } - - if (isResourceHighlighted) { - removeFromStack(1) - } else { - setIsResourceHighlighted(true) - } - addToStack(item) - }, - [ - isResourceIdHighlighted, - isResourceHighlighted, - addToStack, - removeFromStack, - ], - ) + const handleClickResourceItem = ( + resourceItemWithAncestryStack: ResourceItemContent[], + ): void => { + const lastChild = lastResourceItemInAncestryStack( + resourceItemWithAncestryStack, + ) + + // this should never happen. only added here to satisfy typescript + if (!lastChild) return + + const isItemHighlighted = isResourceIdHighlighted(lastChild.id) + const canClickIntoItem = + lastChild.type === ResourceType.Folder || + lastChild.type === ResourceType.Collection + + if (isItemHighlighted && canClickIntoItem) { + setIsResourceHighlighted(false) + return + } + + if (isResourceHighlighted) { + setResourceStack(resourceItemWithAncestryStack) + } else { + setIsResourceHighlighted(true) + } + } + + const handleClickBackButton = useCallback(() => { + if (isResourceHighlighted) { + setIsResourceHighlighted(false) + removeFromStack(2) + } else { + removeFromStack(1) + } + }, [isResourceHighlighted, removeFromStack]) useEffect(() => { + // If there is no selected resource, we don't need to update the stack + if (!selectedResourceId) return + + const pendingMovedItemAncestryStack: ResourceItemContent[] = + trpc.resource.getAncestryWithSelf + .useSuspenseQuery({ + siteId: String(siteId), + resourceId: selectedResourceId, + })[0] + .map((resource) => ({ ...resource, resourceId: resource.id })) + + // If the ancestry stack is empty, we don't need to update the stack + if (pendingMovedItemAncestryStack.length <= 0) return + + // If the ancestry stack is the same as the current stack, we don't need to update the stack if ( - existingAncestryStack.length <= 0 || - JSON.stringify(existingAncestryStack) === JSON.stringify(resourceStack) + JSON.stringify(pendingMovedItemAncestryStack) === + JSON.stringify(resourceStack) ) { return } - setResourceStack(existingAncestryStack) + + setResourceStack(pendingMovedItemAncestryStack) }, []) useEffect(() => { @@ -156,17 +163,15 @@ export const useResourceStack = ({ return { fullPermalink, - isResourceHighlighted, - setIsResourceHighlighted, moveDest, - resourceItems, + resourceItemsWithAncestryStack, fetchNextPage, hasNextPage, isFetchingNextPage, - removeFromStack, isResourceIdHighlighted, shouldShowBackButton, - resourceItemHandleClick, + handleClickBackButton, + handleClickResourceItem, } } From 18c3fbe76bad3d0968fd2712bb23d7fb02d30197 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Sat, 30 Nov 2024 14:25:58 +0800 Subject: [PATCH 061/104] add comment --- .../ResourceSelector/ResourceSelectorContent.tsx | 7 ++++--- .../src/components/ResourceSelector/useResourceStack.tsx | 5 ++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx index 5f56b72992..b9f79a7a97 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelectorContent.tsx @@ -54,9 +54,10 @@ export const ResourceItemsResults = ({ ) => void }) => { return resourceItemsWithAncestryStack.map((resourceItemWithAncestryStack) => { - const lastChild = lastResourceItemInAncestryStack( - resourceItemWithAncestryStack, - ) + const lastChild: ResourceItemContent | undefined = + lastResourceItemInAncestryStack(resourceItemWithAncestryStack) + + // this should never happen. only added here to satisfy typescript if (!lastChild) return return ( diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index ce777cf032..3e5c7b5e1c 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -96,9 +96,8 @@ export const useResourceStack = ({ const handleClickResourceItem = ( resourceItemWithAncestryStack: ResourceItemContent[], ): void => { - const lastChild = lastResourceItemInAncestryStack( - resourceItemWithAncestryStack, - ) + const lastChild: ResourceItemContent | undefined = + lastResourceItemInAncestryStack(resourceItemWithAncestryStack) // this should never happen. only added here to satisfy typescript if (!lastChild) return From d1eff5b35849b0cac5267520b7f770da09dbbc3c Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Sat, 30 Nov 2024 15:58:07 +0800 Subject: [PATCH 062/104] set higher height for Skeleton --- .../studio/src/components/ResourceSelector/ResourceSelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 55d75b1f19..8a847c18e4 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -164,7 +164,7 @@ const SuspensableResourceSelector = ({ export const ResourceSelector = (props: ResourceSelectorProps) => { return ( - }> + }> ) From de9403f26a87f2d9deeca08880eb6f3b7aefd3c6 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Sat, 30 Nov 2024 15:59:59 +0800 Subject: [PATCH 063/104] remove unused type export --- .../studio/src/components/ResourceSelector/useResourceStack.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index 3e5c7b5e1c..8f6f301e76 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -173,5 +173,3 @@ export const useResourceStack = ({ handleClickResourceItem, } } - -export type UseResourceStackReturn = ReturnType From 5147212c7e28a4c1d1e29d6ac2f468cbe1968863 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Sat, 30 Nov 2024 16:04:10 +0800 Subject: [PATCH 064/104] fix nested use hooks issue --- .../ResourceSelector/useResourceStack.tsx | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index 8f6f301e76..ea0e8f5086 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -128,18 +128,20 @@ export const useResourceStack = ({ } }, [isResourceHighlighted, removeFromStack]) + const pendingMovedItemAncestryStack: ResourceItemContent[] = + !!selectedResourceId + ? trpc.resource.getAncestryWithSelf + .useSuspenseQuery({ + siteId: String(siteId), + resourceId: selectedResourceId, + })[0] + .map((resource) => ({ ...resource, resourceId: resource.id })) + : [] + useEffect(() => { // If there is no selected resource, we don't need to update the stack if (!selectedResourceId) return - const pendingMovedItemAncestryStack: ResourceItemContent[] = - trpc.resource.getAncestryWithSelf - .useSuspenseQuery({ - siteId: String(siteId), - resourceId: selectedResourceId, - })[0] - .map((resource) => ({ ...resource, resourceId: resource.id })) - // If the ancestry stack is empty, we don't need to update the stack if (pendingMovedItemAncestryStack.length <= 0) return From 55073f3e1e13b18bdcff02687cdec977e2943531 Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Sat, 30 Nov 2024 16:05:54 +0800 Subject: [PATCH 065/104] fix navigation --- .../src/components/ResourceSelector/useResourceStack.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx index ea0e8f5086..be34b17afa 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceStack.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceStack.tsx @@ -29,7 +29,7 @@ export const useResourceStack = ({ const [resourceStack, setResourceStack] = useState([]) const [isResourceHighlighted, setIsResourceHighlighted] = - useState(true) + useState(!!selectedResourceId) const moveDest = useMemo( () => resourceStack[resourceStack.length - 1], // last item in stack @@ -112,11 +112,8 @@ export const useResourceStack = ({ return } - if (isResourceHighlighted) { - setResourceStack(resourceItemWithAncestryStack) - } else { - setIsResourceHighlighted(true) - } + setResourceStack(resourceItemWithAncestryStack) + setIsResourceHighlighted(true) } const handleClickBackButton = useCallback(() => { From a1f123546248693e71aed06152a6c2d64c4144eb Mon Sep 17 00:00:00 2001 From: adriangohjw Date: Sat, 30 Nov 2024 16:43:32 +0800 Subject: [PATCH 066/104] fix padding on "load more" button --- .../src/components/ResourceSelector/ResourceSelector.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index 8a847c18e4..4a1376846c 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -50,6 +50,7 @@ const SuspensableResourceSelector = ({ }) const isSearchQueryEmpty: boolean = searchQuery.trim().length === 0 + const hasAdditionalLeftPadding: boolean = isSearchQueryEmpty const { fullPermalink, @@ -109,7 +110,7 @@ const SuspensableResourceSelector = ({ resourceItemsWithAncestryStack={resourceItemsWithAncestryStack} isResourceIdHighlighted={isResourceIdHighlighted} existingResource={existingResource} - hasAdditionalLeftPadding={isSearchQueryEmpty} + hasAdditionalLeftPadding={hasAdditionalLeftPadding} handleClickResourceItem={handleClickResourceItem} /> ) @@ -136,7 +137,8 @@ const SuspensableResourceSelector = ({ {hasNextPage && ( - ) -} - -export const ResourceItem = (props: ResourceItemProps) => { - return ( - - {({ reset }) => ( - ( - - There was an error! - - - )} - > - - - - - )} - + ) } diff --git a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx index daf1425c10..411c925961 100644 --- a/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/ResourceSelector.tsx @@ -1,4 +1,4 @@ -import { Suspense } from "react" +import { Suspense, useMemo } from "react" import { Box, Flex, Skeleton, Text, VStack } from "@chakra-ui/react" import { Button } from "@opengovsg/design-system-react" import { ResourceType } from "~prisma/generated/generatedEnums" @@ -83,7 +83,7 @@ const SuspensableResourceSelector = ({ : resources.map((resource) => resource.id), }) - const renderHeader = () => { + const renderedHeader = useMemo(() => { if (isLoading) { return } @@ -100,9 +100,16 @@ const SuspensableResourceSelector = ({ searchQuery={searchQuery} /> ) - } + }, [ + isLoading, + isSearchQueryEmpty, + resourceItemsWithAncestryStack.length, + searchQuery, + hasParentInStack, + handleClickBackButton, + ]) - const renderContent = () => { + const renderedContent = useMemo(() => { if (isLoading) { return } @@ -125,7 +132,17 @@ const SuspensableResourceSelector = ({ handleClickResourceItem={handleClickResourceItem} /> ) - } + }, [ + isLoading, + resourceItemsWithAncestryStack, + isResourceIdHighlighted, + isResourceItemDisabled, + hasAdditionalLeftPadding, + handleClickResourceItem, + isSearchQueryEmpty, + searchQuery, + clearSearchValue, + ]) return ( @@ -143,8 +160,8 @@ const SuspensableResourceSelector = ({ flexDirection="column" gap="0.25rem" > - {renderHeader()} - {renderContent()} + {renderedHeader} + {renderedContent} {hasNextPage && ( )}
- {!!moveDest && ( - - - You selected {fullPermalink} - {existingResource && ( - - The URL for {existingResource.title} will change to{" "} - {`${fullPermalink}/${existingResource.permalink}`} - - )} - - - )} + + + You selected {fullPermalink} + {existingResource && ( + + The URL for "{existingResource.title}" will change to{" "} + {existingResource.id === moveDest?.id + ? fullPermalink + : `${fullPermalink}/${existingResource.permalink}`} + + )} + + ) } diff --git a/apps/studio/src/components/ResourceSelector/useResourceSelector.tsx b/apps/studio/src/components/ResourceSelector/useResourceSelector.tsx index 5d98b5e3fa..6bc42a6742 100644 --- a/apps/studio/src/components/ResourceSelector/useResourceSelector.tsx +++ b/apps/studio/src/components/ResourceSelector/useResourceSelector.tsx @@ -7,6 +7,7 @@ import { trpc } from "~/utils/trpc" import { lastResourceItemInAncestryStack } from "./utils" export const useResourceSelector = ({ + interactionType, siteId, moveDest, resourceStack, @@ -17,6 +18,7 @@ export const useResourceSelector = ({ removeFromStack, onChange, }: { + interactionType: "move" | "link" siteId: number moveDest: ResourceItemContent | undefined resourceStack: ResourceItemContent[] @@ -49,10 +51,11 @@ export const useResourceSelector = ({ const isResourceItemDisabled = useCallback( (resourceItem: ResourceItemContent): boolean => { - // If there is no existing resource, + if (!existingResource) return false + // Then we are linking the resource and not moving any resource // Thus, no checks are needed because we can link to any resource - if (!existingResource) return false + if (interactionType === "link") return false // A resource should not be able to move to within itself if (existingResource.id === resourceItem.id) return true @@ -68,7 +71,7 @@ export const useResourceSelector = ({ ) || false ) }, - [existingResource, nestedChildrenOfExistingResource], + [existingResource, interactionType, nestedChildrenOfExistingResource], ) const hasParentInStack = useMemo( diff --git a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx index 80c3e882ca..52177ea868 100644 --- a/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx +++ b/apps/studio/src/features/editing-experience/components/MoveResourceModal/MoveResourceModal.tsx @@ -106,6 +106,7 @@ const MoveResourceContent = withSuspense( Moving a page or folder changes its URL, effective immediately Date: Thu, 26 Dec 2024 01:44:40 +0800 Subject: [PATCH 104/104] fix getBatchAncestryWithSelfQuery --- .../modules/resource/resource.service.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/studio/src/server/modules/resource/resource.service.ts b/apps/studio/src/server/modules/resource/resource.service.ts index 42a395b161..30b6662cb6 100644 --- a/apps/studio/src/server/modules/resource/resource.service.ts +++ b/apps/studio/src/server/modules/resource/resource.service.ts @@ -484,18 +484,23 @@ export const getBatchAncestryWithSelfQuery = async ({ if (!currentResource) break // Group all resources that are found in the current resource's path - const group = currentResource.path - .map( - (childId) => - remainingResources.find((item) => item.id === childId) ?? - currentResource, - ) - .filter(Boolean) + const group = [currentResource].concat( + currentResource.path + .slice(1) // without the current resource + .map( + (childId) => + remainingResources.find((item) => item.id === childId) ?? + currentResource, + ) + .filter(Boolean), + ) // Remove all items in this group from remainingResources group.forEach((node) => { const index = remainingResources.findIndex( - (resource) => resource.id === node.id, + (resource) => + resource.id === node.id && + JSON.stringify(resource.path) === JSON.stringify(node.path), ) if (index !== -1) remainingResources.splice(index, 1) })