Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use react table to render resources subpage #313

Merged
merged 15 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions apps/studio/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const StorybookEnvDecorator: Decorator = (story) => {
return <EnvProvider env={mockEnv}>{story()}</EnvProvider>
}

const SetupDecorator: Decorator = (story) => {
const SetupDecorator: Decorator = (Story) => {
const [queryClient] = useState(
new QueryClient({
defaultOptions: {
Expand All @@ -76,7 +76,7 @@ const SetupDecorator: Decorator = (story) => {
<Suspense fallback={<Skeleton width="100vw" height="100vh" />}>
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
{story()}
<Story />
</QueryClientProvider>
</trpc.Provider>
</Suspense>
Expand Down Expand Up @@ -186,17 +186,17 @@ export const MockDateDecorator: Decorator<Args> = (story, { parameters }) => {

const decorators: Decorator[] = [
WithLayoutDecorator,
StorybookEnvDecorator,
MockDateDecorator,
MockFeatureFlagsDecorator,
LoginStateDecorator,
SetupDecorator,
StorybookEnvDecorator,
withThemeFromJSXProvider<ReactRenderer>({
themes: {
default: theme,
},
Provider: ThemeProvider,
}) as Decorator, // FIXME: Remove this cast when types are fixed
MockDateDecorator,
LoginStateDecorator,
]

const preview: Preview = {
Expand Down
1 change: 1 addition & 0 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@tanstack/match-sorter-utils": "^8.15.1",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query-devtools": "^4.36.1",
"@tanstack/react-table": "^8.19.3",
"@tiptap/extension-blockquote": "^2.4.0",
"@tiptap/extension-bold": "^2.4.0",
"@tiptap/extension-bullet-list": "^2.4.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export function CmsSidebarContainer({
{sidebar}
</Box>
</GridItem>
<GridItem as="main" area="main">
<GridItem as="main" area="main" overflow="hidden">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sehyunidaaa - seekign input on this!

{children}
</GridItem>
</Grid>
Expand Down
155 changes: 155 additions & 0 deletions apps/studio/src/components/Datatable/Datatable.tsx
karrui marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import type { LayoutProps, TableProps } from "@chakra-ui/react"
import type { Table as ReactTable } from "@tanstack/react-table"
import {
Box,
Flex,
Spinner,
Table,
Tbody,
Td,
Text,
Th,
Thead,
Tr,
useMultiStyleConfig,
} from "@chakra-ui/react"
import { flexRender } from "@tanstack/react-table"

import { DatatablePagination } from "./DatatablePagination"

export interface DatatableProps<D> extends TableProps {
instance: ReactTable<D>
/**
* If provided, this number will be used for pagination instead of retrieving
* from react-table's filtered row count.
*/
totalRowCount?: number
pagination?: boolean
/**
* If provided, this string will be used to display the total row count.
*/
totalRowCountString?: string
isFetching?: boolean
emptyPlaceholder?: React.ReactElement
overflow?: LayoutProps["overflow"]
}

export function createAccessor<T>(props: (keyof T)[]) {
return (row: T): string => {
return props.map((prop) => String(row[prop])).join(" ")
}
}

export const Datatable = <T extends object>({
instance,
isFetching,
pagination,
totalRowCount,
totalRowCountString,
emptyPlaceholder,
overflow = "auto",
...tableProps
}: DatatableProps<T>): JSX.Element => {
const { rows } = instance.getRowModel()
const styles = useMultiStyleConfig("Table", tableProps)

return (
<Flex flexDirection="column" layerStyle="shadow" pos="relative">
{isFetching && (
<>
<Flex
// white alpha to denote loading
bg="whiteAlpha.800"
bottom={0}
left={0}
p="1rem"
pos="absolute"
right={0}
top={0}
zIndex="1"
karrui marked this conversation as resolved.
Show resolved Hide resolved
/>
<Flex
bottom={0}
flex={1}
left={0}
pos="fixed"
right={0}
top={0}
w="100vw"
zIndex={2}
karrui marked this conversation as resolved.
Show resolved Hide resolved
>
<Box m="auto">
<Spinner />
</Box>
</Flex>
</>
)}
Comment on lines +58 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this component has to care about loading states but actually what i feel is the right abstraction here would be for the component here to only care about rendering the table and not care about loading.

Suspense feels like a good fit here, we can omit hte isLoading and it'll still fit nicely! wdyt?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cannot use suspense. We do not want the full fallback. We want to show the previous items in the table still, which suspense will not do (since it will swap out the current rendered table items with the suspense item completely).
That is also the reason why data fetching is still done with useQuery and not useSuspenseQuery.

The loading state is a semi-transparent overlay.

<Box overflow={overflow} sx={styles.container}>
<Table sx={{ tableLayout: "fixed" }} {...tableProps} pos="relative">
<Thead>
{instance.getHeaderGroups().map((headerGroup) => (
<Tr
key={headerGroup.id}
// To toggle _groupHover styles to show divider when header is hovered.
data-group
borderBottomWidth="1px"
>
{headerGroup.headers.map((header) => (
<Th
key={header.id}
pos="relative"
px={0}
style={{
width: header.getSize(),
}}
>
<Flex align="center">
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</Flex>
</Th>
))}
</Tr>
))}
</Thead>
<Tbody>
{rows.length === 0 && emptyPlaceholder}
{rows.map((row) => {
return (
<Tr key={row.id} borderBottomWidth="1px">
{row.getVisibleCells().map((cell) => {
return (
<Td key={cell.id} verticalAlign="center">
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</Td>
)
})}
</Tr>
)
})}
</Tbody>
</Table>
</Box>
<Flex py="1rem" gap="1rem">
{totalRowCountString && (
<Text textStyle="caption-2" color="base.content.medium">
{totalRowCountString}
</Text>
)}
{pagination && (
<Flex ml="auto">
<DatatablePagination
instance={instance}
totalRowCount={totalRowCount}
/>
</Flex>
)}
karrui marked this conversation as resolved.
Show resolved Hide resolved
</Flex>
</Flex>
)
}
27 changes: 27 additions & 0 deletions apps/studio/src/components/Datatable/DatatablePagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Pagination } from "@opengovsg/design-system-react"
import { type Table } from "@tanstack/react-table"

export interface DataTablePaginationProps<D> {
instance: Table<D>
totalRowCount?: number
}

export const DatatablePagination = <T extends object>({
instance,
totalRowCount: totalRowCountProp,
}: DataTablePaginationProps<T>): JSX.Element => {
const paginationState = instance.getState().pagination
const totalRowCount =
totalRowCountProp ?? instance.getFilteredRowModel().rows.length

return (
<Pagination
currentPage={paginationState.pageIndex + 1}
onPageChange={(newPage) => {
instance.setPageIndex(newPage - 1)
}}
pageSize={10}
totalCount={totalRowCount}
/>
)
}
27 changes: 27 additions & 0 deletions apps/studio/src/components/Datatable/EmptyTablePlaceholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Flex, Stack, Td, Text, Tr } from "@chakra-ui/react"

export const EmptyTablePlaceholder = ({
entityName,
hasSearchTerm,
}: {
entityName: string
hasSearchTerm: boolean
}) => {
return (
<Tr aria-hidden>
<Td colSpan={8}>
<Flex align="center" justify="center" p="2rem">
<Stack align="center" spacing="0.375rem">
<Text textStyle="subhead-4">
No {entityName}
{hasSearchTerm ? " found" : ""}
</Text>
{hasSearchTerm && (
<Text textStyle="body-2">Try different search terms</Text>
)}
</Stack>
</Flex>
</Td>
</Tr>
)
}
39 changes: 39 additions & 0 deletions apps/studio/src/components/Datatable/InfoCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { memo } from "react"
import NextLink from "next/link"
import { Link, Text, VStack } from "@chakra-ui/react"

export interface InfoCellProps {
caption?: string | null
subcaption?: string | null
href?: string
}

export const InfoCell = memo(
karrui marked this conversation as resolved.
Show resolved Hide resolved
({ caption, subcaption, href }: InfoCellProps): JSX.Element => {
return (
<VStack align="start" spacing="0.25rem">
{href ? (
<Link
as={NextLink}
color="base.content.strong"
href={href}
textStyle="subhead-2"
>
{caption}
</Link>
) : (
<Text textStyle="body-2">{caption}</Text>
)}
<Text textStyle="body-2">{subcaption}</Text>
</VStack>
)
},
(prevProps, nextProps) => {
return (
prevProps.caption === nextProps.caption &&
prevProps.subcaption === nextProps.subcaption
)
},
)

InfoCell.displayName = "InfoCell"
7 changes: 7 additions & 0 deletions apps/studio/src/components/Datatable/TableCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { TextProps } from "@chakra-ui/react"
import React from "react"
import { Text } from "@chakra-ui/react"

export const TableCell = ({ children, ...props }: TextProps) => {
return <Text {...props}>{children}</Text>
}
11 changes: 11 additions & 0 deletions apps/studio/src/components/Datatable/TableHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { BoxProps } from "@chakra-ui/react"
import React from "react"
import { Box } from "@chakra-ui/react"

export const TableHeader = ({ children, ...props }: BoxProps) => {
return (
<Box px="1rem" {...props}>
{children}
</Box>
)
}
4 changes: 4 additions & 0 deletions apps/studio/src/components/Datatable/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./Datatable"
export * from "./TableCell"
export * from "./TableHeader"
export * from "./InfoCell"
47 changes: 47 additions & 0 deletions apps/studio/src/components/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { MenuItemProps as ChakraMenuItemProps } from "@chakra-ui/react"
import { useMemo } from "react"
import { MenuItem as ChakraMenuItem, cssVar } from "@chakra-ui/react"

const $bg = cssVar("menu-bg")

export interface MenuItemProps extends ChakraMenuItemProps {
colorScheme?: "critical"
}

export const MenuItem = ({
colorScheme,
...menuItemProps
}: MenuItemProps): JSX.Element => {
// Unable to use useMultiStyleConfig here because Menu parent still controls
// other styles such as size and placement
const extraStyles = useMemo(() => {
if (!colorScheme) return {}
switch (colorScheme) {
case "critical": {
return {
bg: $bg.reference,
color: "interaction.critical.default",
_hover: {
[$bg.variable]: `colors.interaction.muted.critical.hover`,
},
_focus: {
[$bg.variable]: `colors.interaction.muted.critical.hover`,
_active: {
[$bg.variable]: `colors.interaction.muted.critical.active`,
},
},
_focusVisible: {
_active: {
[$bg.variable]: `colors.interaction.muted.critical.active`,
},
},
_active: {
[$bg.variable]: `colors.interaction.muted.critical.active`,
},
}
}
}
}, [colorScheme])

return <ChakraMenuItem {...menuItemProps} sx={extraStyles} />
}
1 change: 1 addition & 0 deletions apps/studio/src/components/Menu/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MenuItem } from "./MenuItem"
Loading
Loading