-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: introduce database content layout (#713)
* feat: introduce database content layout * feat: add support for links * fix: improve performance of searching * chore: add story for search with no results * chore: apply design fixes * fix: update empty state title prose type * fix: handle undefined title * chore: remove hyperlink excel function in search key
- Loading branch information
Showing
26 changed files
with
1,695 additions
and
78 deletions.
There are no files selected for viewing
15 changes: 15 additions & 0 deletions
15
packages/components/src/interfaces/internal/SearchableTable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import type { Static } from "@sinclair/typebox" | ||
import { Type } from "@sinclair/typebox" | ||
|
||
import type { IsomerSitemap, LinkComponentType } from "~/types" | ||
|
||
export const SearchableTableSchema = Type.Object({ | ||
title: Type.Optional(Type.String()), | ||
headers: Type.Array(Type.Union([Type.String(), Type.Number()])), | ||
items: Type.Array(Type.Array(Type.Union([Type.String(), Type.Number()]))), | ||
}) | ||
|
||
export type SearchableTableProps = Static<typeof SearchableTableSchema> & { | ||
sitemap: IsomerSitemap | ||
LinkComponent?: LinkComponentType | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
packages/components/src/templates/next/components/internal/SearchableTable/CellContent.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import type { IsomerSitemap, LinkComponentType } from "~/types" | ||
import { getReferenceLinkHref } from "~/utils" | ||
import BaseParagraph from "../BaseParagraph" | ||
import { HYPERLINK_EXCEL_FUNCTION } from "./constants" | ||
|
||
interface CellContentProps { | ||
content: string | number | ||
sitemap: IsomerSitemap | ||
LinkComponent?: LinkComponentType | ||
} | ||
|
||
export const CellContent = ({ | ||
content, | ||
sitemap, | ||
LinkComponent, | ||
}: CellContentProps) => { | ||
if ( | ||
typeof content === "string" && | ||
content.startsWith(HYPERLINK_EXCEL_FUNCTION) && | ||
content.endsWith(")") | ||
) { | ||
const link = content.slice(HYPERLINK_EXCEL_FUNCTION.length, -1) | ||
const [linkHref, linkText] = link.split(",") | ||
|
||
return ( | ||
<BaseParagraph | ||
content={`<a href=${getReferenceLinkHref(linkHref?.replace(/"/g, ""), sitemap)}>${linkText || linkHref || "Link"}</a>`} | ||
LinkComponent={LinkComponent} | ||
/> | ||
) | ||
} | ||
|
||
return ( | ||
<BaseParagraph content={String(content)} LinkComponent={LinkComponent} /> | ||
) | ||
} |
29 changes: 29 additions & 0 deletions
29
...onents/src/templates/next/components/internal/SearchableTable/SearchableTable.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import type { Meta, StoryObj } from "@storybook/react" | ||
|
||
import type { SearchableTableProps } from "~/interfaces" | ||
import SearchableTable from "./SearchableTable" | ||
|
||
const meta: Meta<SearchableTableProps> = { | ||
title: "Next/Internal Components/SearchableTable", | ||
component: SearchableTable, | ||
argTypes: {}, | ||
parameters: { | ||
themes: { | ||
themeOverride: "Isomer Next", | ||
}, | ||
}, | ||
} | ||
export default meta | ||
type Story = StoryObj<typeof SearchableTable> | ||
|
||
export const Default: Story = { | ||
args: { | ||
title: "This is the title", | ||
headers: ["Header", "Header", "Header", "Header"], | ||
items: [ | ||
["Cell copy", "Cell copy", "Cell copy", "Cell copy"], | ||
["Cell copy", "Cell copy", "Cell copy", "Cell copy"], | ||
["Cell copy", "Cell copy", "Cell copy", "Cell copy"], | ||
], | ||
}, | ||
} |
30 changes: 30 additions & 0 deletions
30
...ges/components/src/templates/next/components/internal/SearchableTable/SearchableTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import type { SearchableTableProps } from "~/interfaces" | ||
import { HYPERLINK_EXCEL_FUNCTION } from "./constants" | ||
import { SearchableTableClient } from "./SearchableTableClient" | ||
|
||
const SearchableTable = ({ items, ...rest }: SearchableTableProps) => { | ||
const cacheItems = items.map((item) => ({ | ||
row: item, | ||
key: item | ||
.map((content) => { | ||
if ( | ||
typeof content === "string" && | ||
content.startsWith(HYPERLINK_EXCEL_FUNCTION) && | ||
content.endsWith(")") | ||
) { | ||
const link = content.slice(HYPERLINK_EXCEL_FUNCTION.length, -1) | ||
const [linkHref, linkText] = link.split(",") | ||
|
||
return linkText || linkHref | ||
} | ||
|
||
return content | ||
}) | ||
.join(" ") | ||
.toLowerCase(), | ||
})) | ||
|
||
return <SearchableTableClient items={cacheItems} {...rest} /> | ||
} | ||
|
||
export default SearchableTable |
224 changes: 224 additions & 0 deletions
224
...mponents/src/templates/next/components/internal/SearchableTable/SearchableTableClient.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,224 @@ | ||
"use client" | ||
|
||
import { useDeferredValue, useId, useMemo, useRef, useState } from "react" | ||
|
||
import type { SearchableTableProps } from "~/interfaces" | ||
import { tv } from "~/lib/tv" | ||
import BaseParagraph from "../BaseParagraph" | ||
import { PaginationControls } from "../PaginationControls" | ||
import { SearchField } from "../Search" | ||
import { CellContent } from "./CellContent" | ||
import { MAX_NUMBER_OF_COLUMNS, PAGINATION_MAX_ITEMS } from "./constants" | ||
import { getFilteredItems } from "./getFilteredItems" | ||
import { getPaginatedItems } from "./getPaginatedItems" | ||
|
||
const createSearchableTableStyles = tv({ | ||
slots: { | ||
container: "mx-auto w-full", | ||
title: "prose-display-md break-words text-base-content-strong", | ||
search: "mt-9", | ||
tableContainer: "mt-8 overflow-x-auto", | ||
table: | ||
"[&_>_tbody_>_tr:nth-child(even)_>_td]:bg-base-canvas-default w-full border-collapse border-spacing-0 [&_>_tbody_>_tr:nth-child(odd)_>_td]:bg-base-canvas-alt", | ||
tableRow: "text-left", | ||
tableCell: | ||
"max-w-40 break-words border border-base-divider-medium px-4 py-3 align-top last:max-w-full [&_li]:my-0 [&_li]:pl-1 [&_ol]:mt-0 [&_ol]:ps-5 [&_ul]:mt-0 [&_ul]:ps-5", | ||
emptyState: | ||
"flex flex-col items-center justify-center gap-8 self-stretch px-10 py-20 pt-24", | ||
emptyStateHeadings: "text-center", | ||
emptyStateTitle: "prose-headline-lg-regular", | ||
emptyStateSubtitle: | ||
"text-base-content-default prose-headline-lg-regular mt-3", | ||
emptyStateButton: | ||
"prose-headline-base-medium text-link hover:text-link-hover", | ||
pagination: "mt-8 flex w-full justify-center lg:justify-end", | ||
}, | ||
variants: { | ||
isHeader: { | ||
true: { | ||
tableCell: | ||
"bg-brand-interaction text-base-content-inverse [&_ol]:prose-label-md-medium [&_p]:prose-label-md-medium", | ||
}, | ||
false: { | ||
tableCell: "text-base-content [&_ol]:prose-body-sm [&_p]:prose-body-sm", | ||
}, | ||
}, | ||
bold: { | ||
true: { | ||
emptyStateTitle: "text-base-content-strong", | ||
}, | ||
false: { | ||
emptyStateTitle: "text-base-content-subtle", | ||
}, | ||
}, | ||
}, | ||
}) | ||
|
||
const compoundStyles = createSearchableTableStyles() | ||
|
||
export type SearchableTableClientProps = Omit<SearchableTableProps, "items"> & { | ||
items: { | ||
row: SearchableTableProps["items"][number] | ||
key: string | ||
}[] | ||
} | ||
|
||
export const SearchableTableClient = ({ | ||
title, | ||
headers, | ||
items, | ||
sitemap, | ||
LinkComponent, | ||
}: SearchableTableClientProps) => { | ||
const [_search, setSearch] = useState("") | ||
const search = useDeferredValue(_search) | ||
const [currPage, setCurrPage] = useState(1) | ||
const titleId = useId() | ||
|
||
const maxNoOfColumns = Math.min( | ||
headers.length, | ||
...items.map((row) => row.row.length), | ||
MAX_NUMBER_OF_COLUMNS, | ||
) | ||
const filteredItems = useMemo( | ||
() => | ||
getFilteredItems({ | ||
items, | ||
searchValue: search, | ||
}), | ||
[items, search], | ||
) | ||
|
||
const paginatedItems = useMemo( | ||
() => | ||
getPaginatedItems({ | ||
items: filteredItems, | ||
currPage, | ||
itemsPerPage: PAGINATION_MAX_ITEMS, | ||
}), | ||
[currPage, filteredItems], | ||
) | ||
|
||
const isInitiallyEmpty = items.length === 0 || maxNoOfColumns === 0 | ||
const isFilteredEmpty = items.length !== 0 && filteredItems.length === 0 | ||
|
||
const sectionTopRef = useRef<HTMLDivElement>(null) | ||
const onPageChange = () => { | ||
sectionTopRef.current?.scrollIntoView({ | ||
block: "start", | ||
}) | ||
} | ||
|
||
return ( | ||
<div className={compoundStyles.container()} ref={sectionTopRef}> | ||
{title && ( | ||
<h2 id={titleId} className={compoundStyles.title()}> | ||
{title} | ||
</h2> | ||
)} | ||
|
||
<div className={compoundStyles.search()}> | ||
<SearchField | ||
aria-label="Search table" | ||
placeholder="Enter a search term" | ||
value={search} | ||
onChange={(value) => { | ||
setSearch(value) | ||
setCurrPage(1) | ||
}} | ||
/> | ||
</div> | ||
|
||
{isInitiallyEmpty && ( | ||
<div className={compoundStyles.emptyState()}> | ||
<p className={compoundStyles.emptyStateTitle()}> | ||
There are no items to display | ||
</p> | ||
</div> | ||
)} | ||
|
||
{isFilteredEmpty && ( | ||
<div className={compoundStyles.emptyState()}> | ||
<div className={compoundStyles.emptyStateHeadings()}> | ||
<p className={compoundStyles.emptyStateTitle({ bold: false })}> | ||
No search results for “ | ||
<b className={compoundStyles.emptyStateTitle({ bold: true })}> | ||
{search} | ||
</b> | ||
” | ||
</p> | ||
|
||
<p className={compoundStyles.emptyStateSubtitle()}> | ||
Check if you have a spelling error or try a different search term. | ||
</p> | ||
</div> | ||
|
||
<button | ||
className={compoundStyles.emptyStateButton()} | ||
onClick={() => { | ||
setSearch("") | ||
setCurrPage(1) | ||
}} | ||
> | ||
Clear search | ||
</button> | ||
</div> | ||
)} | ||
|
||
{paginatedItems.length > 0 && ( | ||
<div className={compoundStyles.tableContainer()} tabIndex={0}> | ||
<table | ||
className={compoundStyles.table()} | ||
aria-describedby={!!title ? titleId : undefined} | ||
> | ||
<tbody> | ||
<tr className={compoundStyles.tableRow()}> | ||
{headers.slice(0, maxNoOfColumns).map((header, index) => ( | ||
<th | ||
key={index} | ||
className={compoundStyles.tableCell({ isHeader: true })} | ||
> | ||
<BaseParagraph content={String(header)} /> | ||
</th> | ||
))} | ||
</tr> | ||
|
||
{paginatedItems.map((row, rowIndex) => { | ||
return ( | ||
<tr key={rowIndex} className={compoundStyles.tableRow()}> | ||
{row.slice(0, maxNoOfColumns).map((cell, cellIndex) => ( | ||
<td | ||
key={cellIndex} | ||
className={compoundStyles.tableCell({ | ||
isHeader: false, | ||
})} | ||
> | ||
<CellContent | ||
content={cell} | ||
sitemap={sitemap} | ||
LinkComponent={LinkComponent} | ||
/> | ||
</td> | ||
))} | ||
</tr> | ||
) | ||
})} | ||
</tbody> | ||
</table> | ||
</div> | ||
)} | ||
|
||
{filteredItems.length > 0 && ( | ||
<div className={compoundStyles.pagination()}> | ||
<PaginationControls | ||
totalItems={filteredItems.length} | ||
onPageChange={onPageChange} | ||
itemsPerPage={PAGINATION_MAX_ITEMS} | ||
currPage={currPage} | ||
setCurrPage={setCurrPage} | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
) | ||
} |
8 changes: 8 additions & 0 deletions
8
packages/components/src/templates/next/components/internal/SearchableTable/constants.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
// This is the maximum number of columns that the table can have | ||
// 10 was decided because at 1240px, each column will only have 124px which is | ||
// the minimum width for a column to be readable | ||
export const MAX_NUMBER_OF_COLUMNS = 10 | ||
|
||
export const PAGINATION_MAX_ITEMS = 10 | ||
|
||
export const HYPERLINK_EXCEL_FUNCTION = "=HYPERLINK(" |
Oops, something went wrong.