Compare commits

...

1 Commits

Author SHA1 Message Date
utkuufuk 39150d5a8f add table row search matching logic 2026-06-16 18:08:16 +03:00
3 changed files with 99 additions and 7 deletions
@@ -36,6 +36,11 @@ export interface DocumentContext {
* @default false
*/
withLinkPreviews?: boolean;
/**
* Optional table row search query.
*/
tableSearchQuery?: string;
}
export interface DocumentContextProps {
@@ -1,4 +1,4 @@
import type { DocumentBlockTable, DocumentTableRecord } from '@gitbook/api';
import type { DocumentBlockTable } from '@gitbook/api';
import assertNever from 'assert-never';
import { tcls } from '@/lib/tailwind';
@@ -9,8 +9,9 @@ import { StickyViewGrid } from './StickyViewGrid';
import { ViewCards } from './ViewCards';
import { ViewGrid, ViewGridHeader } from './ViewGrid';
import { getViewGridLayout, hasVisibleHeader } from './layout';
import { type TableRecordKV, filterTableRecordsBySearchTerm } from './search';
export type TableRecordKV = [string, DocumentTableRecord];
export type { TableRecordKV };
export interface TableViewProps<View> extends BlockProps<DocumentBlockTable> {
view: View;
@@ -22,9 +23,15 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
const { block, ancestorBlocks, document, context, style } = props;
const isOffscreen = isBlockOffscreen({ block, ancestorBlocks, document });
const records: TableRecordKV[] = Object.entries(block.data.records).sort((a, b) => {
return a[1].orderIndex.localeCompare(b[1].orderIndex);
});
const records: TableRecordKV[] = Object.entries(block.data.records).sort((a, b) =>
a[1].orderIndex.localeCompare(b[1].orderIndex)
);
const filteredRecords = filterTableRecordsBySearchTerm(
block,
records,
context.tableSearchQuery
);
switch (block.data.view.type) {
case 'cards':
@@ -32,7 +39,7 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
<ViewCards
view={block.data.view}
isOffscreen={isOffscreen}
records={records}
records={filteredRecords}
{...props}
/>
);
@@ -41,7 +48,7 @@ export function Table(props: BlockProps<DocumentBlockTable>) {
...props,
view: block.data.view,
isOffscreen,
records,
records: filteredRecords,
};
const { tableWidth } = getViewGridLayout({
block,
@@ -0,0 +1,80 @@
import type { DocumentBlockTable, DocumentTableRecord } from '@gitbook/api';
import { getNodeFragmentByName, getNodeText } from '@/lib/document';
export type TableRecordKV = [string, DocumentTableRecord];
export function filterTableRecordsBySearchTerm(
block: DocumentBlockTable,
records: TableRecordKV[],
searchTerm: string | undefined
): TableRecordKV[] {
const trimmedSearchTerm = searchTerm?.trim();
if (!trimmedSearchTerm) {
return records;
}
return records.filter(([, record]) =>
block.data.view.columns.some((column) => {
const text = getTableCellSearchText(block, record, column);
return new RegExp(trimmedSearchTerm, 'iu').test(text);
})
);
}
function getTableCellSearchText(
block: DocumentBlockTable,
record: DocumentTableRecord,
column: string
): string {
const definition = block.data.definition[column];
const value = record.values[column];
if (!definition || value === null || value === undefined) {
return '';
}
switch (definition.type) {
case 'text': {
if (typeof value !== 'string') {
return '';
}
const fragment = getNodeFragmentByName(block, value);
return normalizeSearchText(fragment ? getNodeText(fragment) : '');
}
case 'select': {
if (!Array.isArray(value)) {
return '';
}
return normalizeSearchText(
value
.map((selectId) => {
return (
definition.options.find((option) => option.value === selectId)?.label ??
selectId
);
})
.join(' ')
);
}
case 'number':
case 'rating': {
return typeof value === 'number' ? `${value}` : '';
}
case 'checkbox': {
return typeof value === 'boolean' ? `${value}` : '';
}
case 'files':
case 'users': {
return Array.isArray(value) ? normalizeSearchText(value.join(' ')) : '';
}
default:
return '';
}
}
function normalizeSearchText(text: string): string {
return text.replace(/\s+/g, ' ').trim();
}