@maestro-js/components 1.0.0-alpha.18 → 1.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/add.ts +1 -16
- package/dist/components.json +1 -1
- package/dist/index.js +1 -14
- package/package.json +5 -5
- package/registry.json +790 -598
- package/scripts/build.ts +4 -11
- package/src/components/alert-dialog.tsx +3 -3
- package/src/components/avatar.tsx +1 -1
- package/src/components/button-link.tsx +0 -3
- package/src/components/button.tsx +0 -4
- package/src/components/checkbox.tsx +7 -7
- package/src/components/chip.tsx +7 -7
- package/src/components/container.tsx +3 -3
- package/src/components/dialog.tsx +7 -21
- package/src/components/disclosure.tsx +1 -1
- package/src/components/drawer.tsx +5 -19
- package/src/components/form.tsx +5 -27
- package/src/components/helpers/form-field.tsx +8 -7
- package/src/components/helpers/get-button-classes.ts +23 -23
- package/src/components/helpers/headless-button.tsx +1 -1
- package/src/components/inline-alert.tsx +3 -3
- package/src/components/labeled-value.tsx +2 -2
- package/src/components/menu.tsx +26 -60
- package/src/components/month-day-input.tsx +1 -2
- package/src/components/multiselect.tsx +70 -74
- package/src/components/radio.tsx +8 -8
- package/src/components/select.tsx +8 -9
- package/src/components/stepper.tsx +6 -6
- package/src/components/switch.tsx +7 -7
- package/src/components/tabs.tsx +1 -1
- package/src/components/tag-field.tsx +3 -3
- package/src/components/text-area.tsx +1 -1
- package/src/components/text-field.tsx +1 -4
- package/src/components/toast.tsx +6 -6
- package/src/components/toggle-button-group.tsx +2 -2
- package/src/utils/icons.d.ts +1 -2
- package/src/utils/use-tab-indicator.ts +9 -9
- package/src/components/date-range.tsx +0 -138
- package/src/components/optional-link.tsx +0 -15
- package/src/components/table/headless-templated-row-table.ts +0 -550
- package/src/components/table/index.tsx +0 -41
- package/src/components/table/server-table.ts +0 -143
- package/src/components/table/table-actions.tsx +0 -76
- package/src/components/table/table-container.tsx +0 -173
- package/src/components/table/table-container.type-test.ts +0 -155
- package/src/components/table/table-filters/date-range-filter.tsx +0 -227
- package/src/components/table/table-filters/select-filter.tsx +0 -211
- package/src/components/table/table-filters/sort.tsx +0 -85
- package/src/components/table/table-primitives.tsx +0 -226
- package/src/components/table/table-toolbar.tsx +0 -300
- package/src/components/table/table-types.ts +0 -61
- package/src/components/table/use-client-table.ts +0 -157
- package/src/components/table/use-client-table.type-test.ts +0 -217
- package/src/components/table/use-server-table.ts +0 -192
- package/src/components/table/use-server-table.type-test.ts +0 -174
- package/src/utils/use-query-state.ts +0 -626
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import { matchSorter, type MatchSorterOptions } from 'match-sorter'
|
|
2
|
-
import { SearchParams } from '../../utils/use-query-state'
|
|
3
|
-
import type { SearchParams as SearchParamsTypes } from '../../utils/use-query-state'
|
|
4
|
-
import type { HeadlessTemplatedRowTable } from './headless-templated-row-table'
|
|
5
|
-
|
|
6
|
-
export declare namespace ServerTable {
|
|
7
|
-
type RowData = HeadlessTemplatedRowTable.RowData
|
|
8
|
-
|
|
9
|
-
type ServerTableOptions<TData extends RowData, FC extends Record<string, any>, SC extends Record<string, any>> = {
|
|
10
|
-
rows: TData[]
|
|
11
|
-
url: string | URL
|
|
12
|
-
filters?: FilterFieldMap<TData, FC>
|
|
13
|
-
sorts?: HeadlessTemplatedRowTable.SortConfig<TData, SC>
|
|
14
|
-
defaultSort?: HeadlessTemplatedRowTable.SortState<SC>
|
|
15
|
-
search?: MatchSorterOptions<TData>
|
|
16
|
-
pagination?: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
type ServerTable<TData extends RowData, FC extends Record<string, any>, SC extends Record<string, any>> = {
|
|
20
|
-
rows: TData[]
|
|
21
|
-
totalRows: number
|
|
22
|
-
filters: SerializedFilterCodecs<FilterFieldMap<TData, FC>>
|
|
23
|
-
sorts: SerializedSortCodec<SC>
|
|
24
|
-
search: SearchParamsTypes.CodecDef<string | null> | null
|
|
25
|
-
pagination: {
|
|
26
|
-
limit: SearchParamsTypes.CodecDef<number | null>
|
|
27
|
-
skip: SearchParamsTypes.CodecDef<number | null>
|
|
28
|
-
} | null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// ---- Filtering ----
|
|
32
|
-
|
|
33
|
-
type FilterField<TData extends RowData, Name extends string = string, Value = any> = {
|
|
34
|
-
filter: HeadlessTemplatedRowTable.FilterPredicate<TData, Name, NonNullable<Value>>
|
|
35
|
-
codec: SearchParamsTypes.Codec<SearchParamsTypes.CodecDef<Value>>
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
type FilterFieldMap<TData extends RowData, FC extends Record<string, any> = Record<string, any>> = {
|
|
39
|
-
[K in keyof FC & string]: FilterField<TData, K, FC[K]>
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
type ExtractFilterValue<C extends FilterField<any, any, any>> = Parameters<C['filter']>[1]
|
|
43
|
-
|
|
44
|
-
type FilterValues<TData extends RowData, Def extends FilterFieldMap<TData>> = {
|
|
45
|
-
[key in keyof Def]: ExtractFilterValue<Def[key]> | null | undefined
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
type SerializedFilterCodecs<Def extends FilterFieldMap<any>> = {
|
|
49
|
-
[K in keyof Def & string]: SearchParamsTypes.SerializedCodec<Def[K]['codec']>
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ---- Sorting ----
|
|
53
|
-
|
|
54
|
-
type SortCodec<SC extends Record<string, any>> = SearchParamsTypes.Codec<
|
|
55
|
-
SearchParamsTypes.CodecDef<(keyof SC & string) | null>
|
|
56
|
-
>
|
|
57
|
-
|
|
58
|
-
type SerializedSortCodec<SC extends Record<string, any>> = SearchParamsTypes.SerializedCodec<SortCodec<SC>> & {
|
|
59
|
-
values: readonly (keyof SC & string)[]
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function createServerTable<
|
|
64
|
-
TData extends ServerTable.RowData,
|
|
65
|
-
FC extends Record<string, any> = {},
|
|
66
|
-
SC extends Record<string, any> = {}
|
|
67
|
-
>(config: ServerTable.ServerTableOptions<TData, FC, SC>): ServerTable.ServerTable<TData, FC, SC> {
|
|
68
|
-
type Def = ServerTable.FilterFieldMap<TData, FC>
|
|
69
|
-
let rows = config.rows
|
|
70
|
-
|
|
71
|
-
// Build a unified loader from all feature codecs and read state from URL
|
|
72
|
-
const filterDef = config.filters
|
|
73
|
-
const filterKeys = filterDef ? (Object.keys(filterDef) as (keyof FC & string)[]) : []
|
|
74
|
-
const sortKeys = config.sorts ? (Object.keys(config.sorts) as (keyof SC & string)[]) : []
|
|
75
|
-
|
|
76
|
-
// Build codecs once
|
|
77
|
-
const sortCodec = config.sorts
|
|
78
|
-
? SearchParams.codecs.enum(sortKeys).default(config.defaultSort ?? null)
|
|
79
|
-
: SearchParams.codecs.enum([] as string[])
|
|
80
|
-
const searchCodec = config.search ? SearchParams.codecs.string().default('') : null
|
|
81
|
-
const limitCodec = config.pagination != null ? SearchParams.codecs.int().default(config.pagination) : null
|
|
82
|
-
const skipCodec = config.pagination != null ? SearchParams.codecs.int().default(0) : null
|
|
83
|
-
|
|
84
|
-
const loader = {
|
|
85
|
-
...Object.fromEntries(filterKeys.map((key) => [key, filterDef![key]!.codec])),
|
|
86
|
-
...(config.sorts ? { sort: sortCodec } : {}),
|
|
87
|
-
...(searchCodec ? { search: searchCodec } : {}),
|
|
88
|
-
...(limitCodec && skipCodec ? { limit: limitCodec, skip: skipCodec } : {})
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const params = SearchParams.getSearchParamsState(config.url, loader)
|
|
92
|
-
|
|
93
|
-
// Filtering
|
|
94
|
-
const filterCodecs = filterDef
|
|
95
|
-
? (Object.fromEntries(
|
|
96
|
-
filterKeys.map((key) => [key, SearchParams.serializeCodec(filterDef[key]!.codec)])
|
|
97
|
-
) as ServerTable.SerializedFilterCodecs<Def>)
|
|
98
|
-
: ({} as ServerTable.SerializedFilterCodecs<Def>)
|
|
99
|
-
|
|
100
|
-
rows = rows.filter((row) =>
|
|
101
|
-
filterKeys.every((f) => {
|
|
102
|
-
const value = (params as Record<string, any>)[f]
|
|
103
|
-
if (value == null) return true
|
|
104
|
-
return filterDef![f]!.filter(row, value, f)
|
|
105
|
-
})
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
// Search
|
|
109
|
-
if (config.search && searchCodec) {
|
|
110
|
-
const query = (params as any).search as string
|
|
111
|
-
if (query) {
|
|
112
|
-
rows = query.split(' ').reduce((results, term) => matchSorter(results, term, config.search!), rows)
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Sort
|
|
117
|
-
if (config.sorts) {
|
|
118
|
-
const sortKey = ((params as any).sort as string) || null
|
|
119
|
-
if (sortKey && config.sorts[sortKey as keyof SC & string]) {
|
|
120
|
-
rows = [...rows].sort((a, b) => config.sorts![sortKey as keyof SC & string]!(a, b, sortKey))
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Pagination
|
|
125
|
-
const totalRows = rows.length
|
|
126
|
-
if (config.pagination != null) {
|
|
127
|
-
const limit = ((params as any).limit as number) ?? config.pagination
|
|
128
|
-
const skip = ((params as any).skip as number) ?? 0
|
|
129
|
-
rows = rows.slice(skip, skip + limit)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
rows,
|
|
134
|
-
totalRows,
|
|
135
|
-
filters: filterCodecs,
|
|
136
|
-
sorts: SearchParams.serializeCodec(sortCodec) as ServerTable.SerializedSortCodec<SC>,
|
|
137
|
-
search: searchCodec ? SearchParams.serializeCodec(searchCodec) : null,
|
|
138
|
-
pagination:
|
|
139
|
-
limitCodec && skipCodec
|
|
140
|
-
? { limit: SearchParams.serializeCodec(limitCodec), skip: SearchParams.serializeCodec(skipCodec) }
|
|
141
|
-
: null
|
|
142
|
-
}
|
|
143
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import React from 'react'
|
|
2
|
-
import { twMerge } from 'tailwind-merge'
|
|
3
|
-
import { Button, type ButtonProps } from '../button'
|
|
4
|
-
import { Menu, type MenuProps, type MenuItemProps, type MenuLinkItemProps } from '../menu'
|
|
5
|
-
import { Icon } from '../icon'
|
|
6
|
-
|
|
7
|
-
// ── TableActionButton ────────────────────────────────────────────────────────
|
|
8
|
-
// Primary action button for the table toolbar (e.g. "Add person").
|
|
9
|
-
// Defaults to contained/primary/sm — pass variant/color/size to override.
|
|
10
|
-
|
|
11
|
-
export type TableActionButtonProps = ButtonProps
|
|
12
|
-
|
|
13
|
-
export function TableActionButton({ size = 'md', children, ...props }: TableActionButtonProps) {
|
|
14
|
-
return (
|
|
15
|
-
<Button size={size} {...props}>
|
|
16
|
-
{children}
|
|
17
|
-
</Button>
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ── TableMenu ────────────────────────────────────────────────────────────────
|
|
22
|
-
// Secondary actions dropdown for the table toolbar (e.g. "More").
|
|
23
|
-
// Renders a neutral outlined trigger with a chevron, and a standard Menu popover.
|
|
24
|
-
|
|
25
|
-
export type TableMenuProps = {
|
|
26
|
-
label?: string
|
|
27
|
-
children?: React.ReactNode
|
|
28
|
-
placement?: MenuProps['placement']
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function TableMenuMain({ label = 'More', children, placement }: TableMenuProps) {
|
|
32
|
-
return (
|
|
33
|
-
<Menu
|
|
34
|
-
Button={
|
|
35
|
-
<Button variant="outlined" color="neutral" size="md">
|
|
36
|
-
{label}
|
|
37
|
-
<Icon name="chevron-down" className="size-4 text-neutral-400" />
|
|
38
|
-
</Button>
|
|
39
|
-
}
|
|
40
|
-
placement={placement}
|
|
41
|
-
>
|
|
42
|
-
{children}
|
|
43
|
-
</Menu>
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const TableMenu = Object.assign(TableMenuMain, {
|
|
48
|
-
Item: Menu.Item,
|
|
49
|
-
LinkItem: Menu.LinkItem,
|
|
50
|
-
Group: Menu.Group,
|
|
51
|
-
Separator: Menu.Separator
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
// ── TableSelectionAction ────────────────────────────────────────────────────
|
|
55
|
-
// Compact action button for the multi-select selection bar context.
|
|
56
|
-
|
|
57
|
-
export function TableSelectionActions({ children }: { children: React.ReactNode }) {
|
|
58
|
-
return <div className="flex items-center gap-1">{children}</div>
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export type TableSelectionActionProps = React.ComponentProps<'button'>
|
|
62
|
-
|
|
63
|
-
export function TableSelectionAction({ children, className, ...props }: TableSelectionActionProps) {
|
|
64
|
-
return (
|
|
65
|
-
<button
|
|
66
|
-
type="button"
|
|
67
|
-
{...props}
|
|
68
|
-
className={twMerge(
|
|
69
|
-
'inline-flex items-center rounded px-2 py-0.5 text-sm font-medium text-primary-700 hover:text-primary-900 hover:bg-primary-100 disabled:opacity-50 disabled:cursor-not-allowed',
|
|
70
|
-
className
|
|
71
|
-
)}
|
|
72
|
-
>
|
|
73
|
-
{children}
|
|
74
|
-
</button>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
TableBody,
|
|
3
|
-
TableCell,
|
|
4
|
-
TableDescription,
|
|
5
|
-
TableFooter,
|
|
6
|
-
TableMain,
|
|
7
|
-
TableRow,
|
|
8
|
-
TableRowContextProvider,
|
|
9
|
-
TableTitle,
|
|
10
|
-
TableTitleLink
|
|
11
|
-
} from './table-primitives'
|
|
12
|
-
import type { StandardTable } from './table-types'
|
|
13
|
-
import { TableHeader, type TableHeaderProps, TableSearch } from './table-toolbar'
|
|
14
|
-
import { TableDateRangeFilter } from './table-filters/date-range-filter'
|
|
15
|
-
import { TableSelectSort } from './table-filters/sort'
|
|
16
|
-
import { TableSelectFilter } from './table-filters/select-filter'
|
|
17
|
-
import type { TableFilterLocalization, TableLocalization } from './table-types'
|
|
18
|
-
import { twMerge } from 'tailwind-merge'
|
|
19
|
-
import React from 'react'
|
|
20
|
-
import type { Iso } from 'iso-fns'
|
|
21
|
-
|
|
22
|
-
export function TableContainer<TTable extends StandardTable>({
|
|
23
|
-
table,
|
|
24
|
-
localization,
|
|
25
|
-
children,
|
|
26
|
-
actions,
|
|
27
|
-
noRowElement = <StandardNoRows />,
|
|
28
|
-
multiSelect,
|
|
29
|
-
multiSelectActions,
|
|
30
|
-
hideActionsOnMobile = true,
|
|
31
|
-
stickyHeader = true,
|
|
32
|
-
hideSort,
|
|
33
|
-
hideSearch,
|
|
34
|
-
tableBodyClassName,
|
|
35
|
-
headerAction,
|
|
36
|
-
hideResultCount,
|
|
37
|
-
totalRows: totalRowsProp,
|
|
38
|
-
...props
|
|
39
|
-
}: React.ComponentProps<'div'> & {
|
|
40
|
-
table: TTable
|
|
41
|
-
localization: TableLocalization<ReturnType<TTable['getFilterConfig']>, ReturnType<TTable['getSortConfig']>>
|
|
42
|
-
actions?: React.ReactNode
|
|
43
|
-
noRowElement?: React.ReactNode
|
|
44
|
-
multiSelect?: boolean
|
|
45
|
-
hideActionsOnMobile?: boolean
|
|
46
|
-
hideSort?: boolean
|
|
47
|
-
hideSearch?: boolean
|
|
48
|
-
stickyHeader?: boolean
|
|
49
|
-
multiSelectActions?: React.ReactNode
|
|
50
|
-
tableBodyClassName?: string
|
|
51
|
-
headerAction?: TableHeaderProps['action']
|
|
52
|
-
hideResultCount?: boolean
|
|
53
|
-
totalRows?: number
|
|
54
|
-
}) {
|
|
55
|
-
const rows = table.getRows()
|
|
56
|
-
const filterConfig = table.getFilterConfig()
|
|
57
|
-
const sortConfig = table.getSortConfig()
|
|
58
|
-
const filterKeys = Object.keys(filterConfig).filter((k) => k in localization.filters)
|
|
59
|
-
const sortKeys = Object.keys(sortConfig).filter((k) => k !== 'mostRelevant' && k in localization.sorts)
|
|
60
|
-
|
|
61
|
-
const hasFilters = React.useMemo(() => {
|
|
62
|
-
const filters = table.state.filters ?? {}
|
|
63
|
-
const hasActiveFilters = Object.entries(filters).some(([, value]) => value != null)
|
|
64
|
-
const hasSort = table.state.sort != null
|
|
65
|
-
const hasSearch = (table.state.search ?? '') !== ''
|
|
66
|
-
return hasActiveFilters || hasSort || hasSearch
|
|
67
|
-
}, [table.state.filters, table.state.sort, table.state.search])
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<div {...props}>
|
|
71
|
-
{/* Search + actions outside the card */}
|
|
72
|
-
<TableSearch
|
|
73
|
-
table={table}
|
|
74
|
-
placeholder={localization.searchPlaceholder}
|
|
75
|
-
actions={actions}
|
|
76
|
-
hasFilters={hasFilters}
|
|
77
|
-
hideSearch={hideSearch}
|
|
78
|
-
hideActionsOnMobile={hideActionsOnMobile}
|
|
79
|
-
/>
|
|
80
|
-
|
|
81
|
-
{/* Bordered card container */}
|
|
82
|
-
<div className="rounded-lg border border-gray-950/10 overflow-hidden">
|
|
83
|
-
<TableMain className={twMerge(stickyHeader && 'relative')}>
|
|
84
|
-
<TableHeader
|
|
85
|
-
table={table}
|
|
86
|
-
multiSelect={multiSelect}
|
|
87
|
-
multiSelectActions={multiSelectActions}
|
|
88
|
-
sticky={stickyHeader}
|
|
89
|
-
hide={!!(hideSort && filterKeys.length === 0 && !multiSelect)}
|
|
90
|
-
action={headerAction}
|
|
91
|
-
>
|
|
92
|
-
<div className={twMerge('flex flex-nowrap items-center gap-x-1', hideSort && 'mr-1')}>
|
|
93
|
-
{filterKeys.map((f) => {
|
|
94
|
-
const filter = localization.filters[f] as TableFilterLocalization
|
|
95
|
-
if (filter.type === 'date-range') {
|
|
96
|
-
return (
|
|
97
|
-
<TableDateRangeFilter
|
|
98
|
-
table={table}
|
|
99
|
-
key={f}
|
|
100
|
-
name={f}
|
|
101
|
-
label={filter.label}
|
|
102
|
-
options={filter.options}
|
|
103
|
-
value={table.state.filters[f] as { startDate: Iso.Date; endDate: Iso.Date }}
|
|
104
|
-
/>
|
|
105
|
-
)
|
|
106
|
-
} else {
|
|
107
|
-
return (
|
|
108
|
-
<TableSelectFilter
|
|
109
|
-
table={table}
|
|
110
|
-
label={filter.label}
|
|
111
|
-
name={f}
|
|
112
|
-
options={filter.options}
|
|
113
|
-
autoComplete={filter.autoComplete}
|
|
114
|
-
value={table.state.filters[f]}
|
|
115
|
-
key={f}
|
|
116
|
-
/>
|
|
117
|
-
)
|
|
118
|
-
}
|
|
119
|
-
})}
|
|
120
|
-
</div>
|
|
121
|
-
<div className={twMerge('flex flex-nowrap items-center ml-1 mr-1', hideSort && 'hidden')}>
|
|
122
|
-
<TableSelectSort
|
|
123
|
-
table={table}
|
|
124
|
-
value={table.state.sort}
|
|
125
|
-
options={sortKeys.map((s) => ({ value: s, label: localization.sorts[s]?.label ?? s }))}
|
|
126
|
-
search={table.state.search}
|
|
127
|
-
/>
|
|
128
|
-
</div>
|
|
129
|
-
</TableHeader>
|
|
130
|
-
<TableRowContextProvider value={{ table, multiSelect }}>
|
|
131
|
-
{rows.length ? <TableBody className={tableBodyClassName}>{children}</TableBody> : noRowElement}
|
|
132
|
-
</TableRowContextProvider>
|
|
133
|
-
{rows.length ? <TableFooter table={table} hideResultCount={hideResultCount} /> : null}
|
|
134
|
-
</TableMain>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export function BasicTable({
|
|
141
|
-
table,
|
|
142
|
-
noRowElement = <StandardNoRows />,
|
|
143
|
-
children,
|
|
144
|
-
shouldHideFooter,
|
|
145
|
-
...props
|
|
146
|
-
}: React.ComponentProps<'div'> & {
|
|
147
|
-
table: StandardTable
|
|
148
|
-
noRowElement?: React.ReactNode
|
|
149
|
-
shouldHideFooter?: boolean
|
|
150
|
-
}) {
|
|
151
|
-
const rows = table.getRows()
|
|
152
|
-
return (
|
|
153
|
-
<div {...props}>
|
|
154
|
-
<div className="rounded-lg border border-gray-950/10 overflow-hidden">
|
|
155
|
-
<TableMain>
|
|
156
|
-
<TableRowContextProvider value={{ table }}>
|
|
157
|
-
{rows.length ? <TableBody>{children}</TableBody> : noRowElement}
|
|
158
|
-
</TableRowContextProvider>
|
|
159
|
-
{rows.length > 0 && !shouldHideFooter ? <TableFooter table={table} /> : null}
|
|
160
|
-
</TableMain>
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function StandardNoRows() {
|
|
167
|
-
return (
|
|
168
|
-
<div className="text-center py-8">
|
|
169
|
-
<h3 className="mt-2 text-sm font-semibold text-gray-900">No results</h3>
|
|
170
|
-
<p className="mt-1 text-sm text-gray-500">No results were found matching your query.</p>
|
|
171
|
-
</div>
|
|
172
|
-
)
|
|
173
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import type { StandardTable, TableLocalization, TableFilterLocalization } from './table-types'
|
|
2
|
-
import type { Iso } from 'iso-fns'
|
|
3
|
-
|
|
4
|
-
// =============================================================================
|
|
5
|
-
// Helpers
|
|
6
|
-
// =============================================================================
|
|
7
|
-
|
|
8
|
-
type Expect<T extends true> = T
|
|
9
|
-
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false
|
|
10
|
-
|
|
11
|
-
// =============================================================================
|
|
12
|
-
// Setup — concrete filter/sort configs
|
|
13
|
-
// =============================================================================
|
|
14
|
-
|
|
15
|
-
type StatusValue = 'active' | 'inactive'
|
|
16
|
-
type DateRangeValue = { startDate: Iso.Date; endDate: Iso.Date }
|
|
17
|
-
|
|
18
|
-
type FC = { status: StatusValue; dateRange: DateRangeValue }
|
|
19
|
-
type SC = { newest: null; oldest: null }
|
|
20
|
-
|
|
21
|
-
type MyTable = StandardTable<FC, SC>
|
|
22
|
-
type MyLocalization = TableLocalization<FC, SC>
|
|
23
|
-
|
|
24
|
-
// =============================================================================
|
|
25
|
-
// TableFilterLocalization discriminates on value type
|
|
26
|
-
// =============================================================================
|
|
27
|
-
|
|
28
|
-
// String union → DefaultTableFilter (has options array with typed values, no `type` discriminant)
|
|
29
|
-
const _selectFilter: TableFilterLocalization<StatusValue> = {
|
|
30
|
-
label: 'Status',
|
|
31
|
-
options: [{ label: 'Active', value: 'active' }, { label: 'Inactive', value: 'inactive' }]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Date range → DateRangeTableFilter (type: 'date-range', no value-typed options)
|
|
35
|
-
const _dateFilter: TableFilterLocalization<DateRangeValue> = {
|
|
36
|
-
label: 'Date range',
|
|
37
|
-
type: 'date-range'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// date-range value rejects DefaultTableFilter form
|
|
41
|
-
const _dateFilterRejectsSelect: TableFilterLocalization<DateRangeValue> = {
|
|
42
|
-
label: 'Date range',
|
|
43
|
-
type: 'date-range',
|
|
44
|
-
// @ts-expect-error — DefaultTableFilter 'options' shape is not valid for date-range
|
|
45
|
-
options: [{ label: 'Last 7 days', value: 'last7' }]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// string-union value rejects DateRangeTableFilter form
|
|
49
|
-
const _selectFilterRejectsDateRange: TableFilterLocalization<StatusValue> = {
|
|
50
|
-
label: 'Status',
|
|
51
|
-
// @ts-expect-error — type: 'date-range' is not valid for a select filter
|
|
52
|
-
type: 'date-range'
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// =============================================================================
|
|
56
|
-
// TableLocalization requires the exact filter + sort keys
|
|
57
|
-
// =============================================================================
|
|
58
|
-
|
|
59
|
-
type _FiltersHasKeys = Expect<Equal<keyof MyLocalization['filters'], 'status' | 'dateRange'>>
|
|
60
|
-
type _SortsHasKeys = Expect<Equal<keyof MyLocalization['sorts'], 'newest' | 'oldest'>>
|
|
61
|
-
type _SortShape = Expect<Equal<MyLocalization['sorts']['newest'], { label: string }>>
|
|
62
|
-
|
|
63
|
-
// =============================================================================
|
|
64
|
-
// Full valid localization object
|
|
65
|
-
// =============================================================================
|
|
66
|
-
|
|
67
|
-
const _valid: MyLocalization = {
|
|
68
|
-
filters: {
|
|
69
|
-
status: {
|
|
70
|
-
label: 'Status',
|
|
71
|
-
options: [
|
|
72
|
-
{ label: 'Active', value: 'active' },
|
|
73
|
-
{ label: 'Inactive', value: 'inactive' }
|
|
74
|
-
]
|
|
75
|
-
},
|
|
76
|
-
dateRange: {
|
|
77
|
-
label: 'Date range',
|
|
78
|
-
type: 'date-range'
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
sorts: {
|
|
82
|
-
newest: { label: 'Newest' },
|
|
83
|
-
oldest: { label: 'Oldest' }
|
|
84
|
-
},
|
|
85
|
-
searchPlaceholder: 'Search...'
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// =============================================================================
|
|
89
|
-
// Negative cases
|
|
90
|
-
// =============================================================================
|
|
91
|
-
|
|
92
|
-
declare function checkLocalization(l: MyLocalization): void
|
|
93
|
-
|
|
94
|
-
// Missing a required filter key
|
|
95
|
-
checkLocalization({
|
|
96
|
-
// @ts-expect-error — 'dateRange' is missing from filters
|
|
97
|
-
filters: {
|
|
98
|
-
status: { label: 'Status', options: [{ label: 'Active', value: 'active' }] }
|
|
99
|
-
},
|
|
100
|
-
sorts: { newest: { label: 'Newest' }, oldest: { label: 'Oldest' } }
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// Missing a required sort key
|
|
104
|
-
checkLocalization({
|
|
105
|
-
filters: {
|
|
106
|
-
status: { label: 'Status', options: [{ label: 'Active', value: 'active' }] },
|
|
107
|
-
dateRange: { label: 'Date range', type: 'date-range' }
|
|
108
|
-
},
|
|
109
|
-
// @ts-expect-error — 'oldest' is missing from sorts
|
|
110
|
-
sorts: { newest: { label: 'Newest' } }
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
// Wrong option value type — status options must use StatusValue
|
|
114
|
-
checkLocalization({
|
|
115
|
-
filters: {
|
|
116
|
-
status: {
|
|
117
|
-
label: 'Status',
|
|
118
|
-
// @ts-expect-error — 'unknown' is not assignable to StatusValue
|
|
119
|
-
options: [{ label: 'Unknown', value: 'unknown' }]
|
|
120
|
-
},
|
|
121
|
-
dateRange: { label: 'Date range', type: 'date-range' }
|
|
122
|
-
},
|
|
123
|
-
sorts: { newest: { label: 'Newest' }, oldest: { label: 'Oldest' } }
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
// Wrong filter form — dateRange requires DateRangeTableFilter, not DefaultTableFilter
|
|
127
|
-
checkLocalization({
|
|
128
|
-
filters: {
|
|
129
|
-
status: { label: 'Status', options: [{ label: 'Active', value: 'active' }] },
|
|
130
|
-
dateRange: {
|
|
131
|
-
label: 'Date range',
|
|
132
|
-
// @ts-expect-error — select-style options array is not valid for date-range filter
|
|
133
|
-
options: [{ label: 'Last 7 days', value: 'last7' }]
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
sorts: { newest: { label: 'Newest' }, oldest: { label: 'Oldest' } }
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
// =============================================================================
|
|
140
|
-
// TableContainer generic inference — localization is derived from table type
|
|
141
|
-
// =============================================================================
|
|
142
|
-
|
|
143
|
-
type InferredLocalization<T extends StandardTable> = TableLocalization<
|
|
144
|
-
ReturnType<T['getFilterConfig']>,
|
|
145
|
-
ReturnType<T['getSortConfig']>
|
|
146
|
-
>
|
|
147
|
-
|
|
148
|
-
// Inferred localization from MyTable matches the manually specified one
|
|
149
|
-
type _Inferred = Expect<Equal<InferredLocalization<MyTable>, MyLocalization>>
|
|
150
|
-
|
|
151
|
-
// A table with no filters/sorts infers empty objects
|
|
152
|
-
type EmptyTable = StandardTable<{}, {}>
|
|
153
|
-
type EmptyLocalization = InferredLocalization<EmptyTable>
|
|
154
|
-
type _EmptyFilters = Expect<Equal<EmptyLocalization['filters'], {}>>
|
|
155
|
-
type _EmptySorts = Expect<Equal<EmptyLocalization['sorts'], {}>>
|