Skip to content

Commit

Permalink
isom-1653 global search (#879)
Browse files Browse the repository at this point in the history
* global search backend (#875)

* add getUserViewableResourceTypes

* update test seed helper

* add "recently edited" endpoint

* add test for "recently edited" endpoint

* format

* remove RootPage from getUserSearchViewableResourceTypes

* rename API to search to be more generic

* move getResourcesWithFullPermalink into route since its only being used there

* move "await" and "execute" up

* add pg_tram pg extension + add gin index on resource title

* improve perf - only order by matching keywords if theres more than 1 searchterm

* fix typo

* fix - totalCount to be Number

* update test seed helper

* improve test case extensiveness

* add more test case

* only match by prefix

* add more test case

* better typechecking

* update recentlyUpdated to be sorted by lastedUpdatedAt desc

* limit recentlyEdited suggestions to page-ish items only

* simplify orderBy query

* refactor into services and types

* refactor - use output schema for validation instead

* undo: accidental edit

* remove array spreading

* refactor fallback content out

* remove "suggestions" and use "recentlyEdited" directly

* improve matching algo accuracy

* add site-level permission validation

* Feat/search fe (#882)

* add getUserViewableResourceTypes

* update test seed helper

* add "recently edited" endpoint

* add test for "recently edited" endpoint

* format

* remove RootPage from getUserSearchViewableResourceTypes

* rename API to search to be more generic

* move getResourcesWithFullPermalink into route since its only being used there

* move "await" and "execute" up

* add pg_tram pg extension + add gin index on resource title

* improve perf - only order by matching keywords if theres more than 1 searchterm

* fix typo

* fix - totalCount to be Number

* update test seed helper

* improve test case extensiveness

* add more test case

* only match by prefix

* add more test case

* better typechecking

* refactor: add header area to grid

* refactor: make base header for search

* fix: align searchbar to middle

* fix: add modal

* feat: add remaining frontend components

* chore: align modal vertically to searchbar

* refactor: use grid for header

* chore: use ogp search bar

* chore: add query

* feat: add conditional wording

* chore: fix search value

* chore: add story

* fix: add search

* feat: show results

* chore: add interaction states

* fix: update link references

* fix: textstyle

* chore: fix styling

thanks to @karrui for this - we finally got it centered LOL

* fix linting issue

* fix lint

* add test cases for getSiteName() endpoint

* split stories into 2 file

* update copywriting

* split into multiple files for improved readability

* add highlighted title text + add unique key

* do not pass in search limit

* refactor type out + improve readability

* add copywriting

* add display for no search result

* do not render empty Text div if no text (use gap instead)

* fix - wrong import

* make overflow scrollable content

* fix height

* align "no search results" to center

* fix logic for determining states

* fix gaps

* refactor conditional rendering of different states

* add loading state

* fit footer padding

* fix - SearchResultsState headerText copywriting

* refactor to use common SearchResults

* fix type

* add "last edited" to search result

* add search shortcut hint display

* trigger searchbar on hitting K with ctrl/cmd key

* fix - import filename error

* update recentlyUpdated to be sorted by lastedUpdatedAt desc

* limit recentlyEdited suggestions to page-ish items only

* simplify orderBy query

* add leading slash

* reduce rowgap for searchresult

* add border radius to searchresult

* conditional singular

* fix broken stories

* make highlights rounded

* align icon to the top + reduce gap

* truncate permalink to one line max

* refactor into services and types

* refactor - use output schema for validation instead

* undo: accidental edit

* remove unused import

* add storybook

* fix - use findByRole instead of getByRole

* remove waitFor

* remove unused waitFor

* fix - copywriting

* hide date for recentlyedited display

* remove array spreading

* refactor fallback content out

* remove "suggestions" and use "recentlyEdited" directly

* improve matching algo accuracy

* add site-level permission validation

* delete unused image

* move svg to correct folder

* remove suggestions

* remove commented code

* convert formatDate to utils

* extracted into isAllowedToHaveLastEditedText

* remove unused import

* make width and height responsive with custom hook (useSearchStyle)

* remove box

---------

Co-authored-by: adriangohjw <[email protected]>

---------

Co-authored-by: seaerchin <[email protected]>
  • Loading branch information
adriangohjw and seaerchin authored Nov 25, 2024
1 parent 9cbdf37 commit 3ae4ee5
Show file tree
Hide file tree
Showing 40 changed files with 2,514 additions and 100 deletions.
1 change: 1 addition & 0 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
"@trpc/next": "10.45.0",
"@trpc/react-query": "10.45.0",
"@trpc/server": "10.45.0",
"@uidotdev/usehooks": "^2.4.1",
"ajv": "^8.16.0",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.1.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- Enable the pg_trgm extension (if not already enabled)
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- Create the GIN index on the "title" column of the "Resource" table
CREATE INDEX "resource_title_trgm_idx" ON "Resource" USING GIN ("title" gin_trgm_ops);
11 changes: 9 additions & 2 deletions apps/studio/src/components/AppBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { Box } from "@chakra-ui/react"
import { Banner } from "@opengovsg/design-system-react"

import { useBanner } from "~/hooks/useBanner"
import { APP_BANNER_ID, useBanner } from "~/hooks/useBanner"

export const AppBanner = () => {
const banner = useBanner()
return banner && <Banner variant={banner.variant}>{banner.message}</Banner>
return (
banner && (
<Box id={APP_BANNER_ID}>
<Banner variant={banner.variant}>{banner.message}</Banner>
</Box>
)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,32 @@ export interface CmsSidebarContainerProps {
children: ReactNode
sidebar: ReactElement
sidenav: ReactElement
header: ReactElement
}

export function CmsSidebarContainer({
export function CmsContainer({
children,
sidebar,
sidenav,
header,
}: CmsSidebarContainerProps) {
return (
<Grid
templateAreas={`'sidebar sidenav main'`}
templateColumns="auto 18.75rem 1fr"
templateAreas={`'header header header'
'sidebar sidenav main'`}
gridTemplateColumns="auto 18.75rem 1fr"
gridTemplateRows="3.75rem 1fr"
width="100%"
>
<GridItem area="header" as="header" w="full" p={0}>
{header}
</GridItem>
<GridItem area="sidebar" as="aside" w="full" p={0}>
<Box
pos="sticky"
top={0}
borderRight="1px solid"
borderTop="1px solid"
borderColor="base.divider.medium"
py={{ base: 0, md: "0.75rem" }}
px={{ base: 0, md: "0.5rem" }}
Expand Down Expand Up @@ -51,6 +59,7 @@ export function CmsSidebarContainer({
pos="sticky"
top={0}
borderRight="1px solid"
borderTop="1px solid"
borderColor="base.divider.medium"
overflow="auto"
css={{
Expand All @@ -67,7 +76,14 @@ export function CmsSidebarContainer({
</Box>
</GridItem>
<GridItem as="main" area="main" overflow="hidden">
{children}
<Box
height={0}
minH="100%"
borderTop="1px solid"
borderColor="base.divider.medium"
>
{children}
</Box>
</GridItem>
</Grid>
)
Expand Down
19 changes: 0 additions & 19 deletions apps/studio/src/components/CmsSidebar/CmsSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import Image from "next/image"
import NextLink from "next/link"
import { VStack } from "@chakra-ui/react"
import { IconButton } from "@opengovsg/design-system-react"

import type { CmsSidebarItem } from "./CmsSidebarItems"
import { DASHBOARD } from "~/lib/routes"
import CmsSidebarItems from "./CmsSidebarItems"

export interface CmsSidebarProps {
Expand All @@ -19,21 +15,6 @@ export function CmsSidebar({
return (
<VStack spacing="0.75rem" as="nav" justify="space-between" height="100%">
<VStack spacing="0.75rem">
<IconButton
as={NextLink}
href={DASHBOARD}
variant="clear"
aria-label="Back to dashboard"
icon={
<Image
src="/assets/isomer-logo-color.svg"
height={24}
width={22}
alt="Back to dashboard"
priority
/>
}
/>
<CmsSidebarItems navItems={topNavItems} />
</VStack>
<CmsSidebarItems navItems={bottomNavItems} />
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/src/components/CmsSidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from "./CmsSidebarContainer"
export * from "./CmsContainer"
export * from "./CmsSidebar"
export * from "./CmsSidebarOnlyContainer"
130 changes: 130 additions & 0 deletions apps/studio/src/components/Searchbar/SearchModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useState } from "react"
import {
Modal,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
} from "@chakra-ui/react"
import { Searchbar as OgpSearchBar } from "@opengovsg/design-system-react"
import { useDebounce } from "@uidotdev/usehooks"

import type { SearchResultResource } from "~/server/modules/resource/resource.types"
import { trpc } from "~/utils/trpc"
import { isMac } from "./isMac"
import {
InitialState,
LoadingState,
NoResultsState,
SearchResultsState,
} from "./SearchModalBodyContentStates"
import { useSearchStyle } from "./useSearchStyle"

interface SearchModalProps {
isOpen: boolean
onClose: () => void
siteId: string
}
export const SearchModal = ({ siteId, isOpen, onClose }: SearchModalProps) => {
const [searchValue, setSearchValue] = useState("")
const debouncedSearchTerm = useDebounce(searchValue, 300)
const { data, isLoading } = trpc.resource.search.useInfiniteQuery({
siteId,
query: debouncedSearchTerm,
})
const resources: SearchResultResource[] =
data?.pages.flatMap((page) => page.resources) ?? []

const renderModalBody = (): React.ReactNode => {
if (!!debouncedSearchTerm) {
if (isLoading) {
return <LoadingState />
}
if (resources.length === 0) {
return <NoResultsState />
}
return (
<SearchResultsState
siteId={siteId}
items={resources}
totalResultsCount={
data?.pages.reduce(
(acc, page) => acc + (page.totalCount ?? 0),
0,
) ?? 0
}
searchTerm={debouncedSearchTerm}
/>
)
}
return (
<InitialState
siteId={siteId}
items={data?.pages[0]?.recentlyEdited ?? []}
/>
)
}
const { minWidth, maxWidth, marginTop } = useSearchStyle()

return (
<Modal isOpen={isOpen} onClose={onClose} motionPreset="none">
<ModalOverlay bg="none" backdropFilter="brightness(80%)" />
<ModalContent
rounded="base"
minW={minWidth}
maxW={maxWidth}
p={0}
mt={`calc(${marginTop} + 1px)`}
// NOTE: This is required to align the inner Searchbar
// with the outer search bar
ml="3px"
boxShadow="md"
h="30.625rem"
>
<ModalHeader p={0}>
<OgpSearchBar
defaultIsExpanded
onChange={({ target }) => setSearchValue(target.value)}
minW={minWidth}
maxW={maxWidth}
// border={0}
placeholder={`Search pages, collections, or folders by name. e.g. "Speech by Minister"`}
/>
</ModalHeader>
{renderModalBody()}
<ModalFooter
bg="base.canvas.alt"
border="1px solid"
borderColor="base.divider.medium"
px="1.25rem"
display="flex"
flexDir="row"
pt="0.75rem"
pb="1rem"
justifyContent="space-between"
borderBottomRadius="base"
>
<Text textStyle="caption-2" textColor="base.content.medium">
{resources.length === 0
? "Tip: Type in the full title to get the most accurate search results."
: "Scroll to see more results. Too many results? Try typing something longer."}
</Text>
<Text
textStyle="caption-1"
textColor="base.content.medium"
bg="white"
py="0.125rem"
px="0.375rem"
borderRadius="base"
border="1px solid"
borderColor="base.divider.medium"
boxShadow="sm"
>
{isMac ? "⌘ + K" : "Ctrl + K"}
</Text>
</ModalFooter>
</ModalContent>
</Modal>
)
}
Loading

0 comments on commit 3ae4ee5

Please sign in to comment.