@fastnd/components 1.0.26 → 1.0.28
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/dist/components/ColumnConfigPopover/ColumnConfigPopover.d.ts +20 -0
- package/dist/components/DoubleTextCell/DoubleTextCell.d.ts +9 -0
- package/dist/components/ProgressCircle/ProgressCircle.d.ts +9 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/button.d.ts +1 -1
- package/dist/components/ui/input-group.d.ts +1 -1
- package/dist/components/ui/item.d.ts +1 -1
- package/dist/components/ui/tabs.d.ts +1 -1
- package/dist/examples/dashboard/DashboardPage/DashboardPage.tsx +54 -0
- package/dist/examples/dashboard/ProjectList/ProjectList.tsx +120 -0
- package/dist/examples/dashboard/StatusDonutChart/StatusDonutChart.tsx +123 -0
- package/dist/examples/dashboard/StatusFilterLegend/StatusFilterLegend.tsx +70 -0
- package/dist/examples/dashboard/StatusOverview/StatusOverview.tsx +51 -0
- package/dist/examples/dashboard/constants.ts +18 -0
- package/dist/examples/dashboard/hooks/use-dashboard-state.ts +98 -0
- package/dist/examples/dashboard/index.ts +6 -0
- package/dist/examples/dashboard/types.ts +19 -0
- package/dist/examples/data-visualization/DataGrid/DataGrid.tsx +136 -0
- package/dist/examples/data-visualization/DataGridCardView/DataGridCardView.tsx +179 -0
- package/dist/examples/data-visualization/DataGridListView/DataGridListView.tsx +190 -0
- package/dist/examples/data-visualization/DataGridPage/DataGridPage.tsx +43 -0
- package/dist/examples/data-visualization/DataGridPagination/DataGridPagination.tsx +111 -0
- package/dist/examples/data-visualization/DataGridTableView/DataGridTableView.tsx +282 -0
- package/dist/examples/data-visualization/DataGridToolbar/DataGridToolbar.tsx +283 -0
- package/dist/examples/data-visualization/DomainSwitcher/DomainSwitcher.tsx +41 -0
- package/dist/examples/data-visualization/ExpansionDrawer/ExpansionDrawer.tsx +139 -0
- package/dist/examples/data-visualization/MoreFiltersPopover/MoreFiltersPopover.tsx +230 -0
- package/dist/examples/data-visualization/ResultCount/ResultCount.tsx +33 -0
- package/dist/examples/data-visualization/cell-renderers.tsx +119 -0
- package/dist/examples/data-visualization/constants.ts +1251 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-columns.ts +65 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-expansion.ts +40 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-favorites.ts +41 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-filters.ts +61 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-pagination.ts +32 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-sort.ts +32 -0
- package/dist/examples/data-visualization/hooks/use-data-grid-state.ts +133 -0
- package/dist/examples/data-visualization/hooks/use-filtered-data.ts +84 -0
- package/dist/examples/data-visualization/index.ts +10 -0
- package/dist/examples/data-visualization/types.ts +103 -0
- package/dist/fastnd-components.js +18759 -15519
- package/package.json +2 -1
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback } from 'react'
|
|
2
|
+
import type { ColumnConfig } from '../types'
|
|
3
|
+
|
|
4
|
+
export interface DataGridColumnsState {
|
|
5
|
+
visibleColumns: Set<string>
|
|
6
|
+
columnOrder: string[]
|
|
7
|
+
columnWidths: Record<string, number>
|
|
8
|
+
toggleColumnVisibility: (columnKey: string) => void
|
|
9
|
+
reorderColumns: (newOrder: string[]) => void
|
|
10
|
+
resizeColumn: (columnKey: string, width: number) => void
|
|
11
|
+
orderedVisibleColumns: Array<[string, ColumnConfig]>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useDataGridColumns(
|
|
15
|
+
columnConfig: Record<string, ColumnConfig>,
|
|
16
|
+
): DataGridColumnsState {
|
|
17
|
+
const [visibleColumns, setVisibleColumns] = useState<Set<string>>(
|
|
18
|
+
() =>
|
|
19
|
+
new Set(
|
|
20
|
+
Object.entries(columnConfig)
|
|
21
|
+
.filter(([, config]) => config.visible)
|
|
22
|
+
.map(([key]) => key),
|
|
23
|
+
),
|
|
24
|
+
)
|
|
25
|
+
const [columnOrder, setColumnOrder] = useState<string[]>(() => Object.keys(columnConfig))
|
|
26
|
+
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({})
|
|
27
|
+
|
|
28
|
+
const toggleColumnVisibility = useCallback((columnKey: string) => {
|
|
29
|
+
setVisibleColumns((prev) => {
|
|
30
|
+
const next = new Set(prev)
|
|
31
|
+
if (next.has(columnKey)) {
|
|
32
|
+
next.delete(columnKey)
|
|
33
|
+
} else {
|
|
34
|
+
next.add(columnKey)
|
|
35
|
+
}
|
|
36
|
+
return next
|
|
37
|
+
})
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
const reorderColumns = useCallback((newOrder: string[]) => {
|
|
41
|
+
setColumnOrder(newOrder)
|
|
42
|
+
}, [])
|
|
43
|
+
|
|
44
|
+
const resizeColumn = useCallback((columnKey: string, width: number) => {
|
|
45
|
+
setColumnWidths((prev) => ({ ...prev, [columnKey]: width }))
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const orderedVisibleColumns = useMemo<Array<[string, ColumnConfig]>>(
|
|
49
|
+
() =>
|
|
50
|
+
columnOrder
|
|
51
|
+
.filter((key) => visibleColumns.has(key) && key in columnConfig)
|
|
52
|
+
.map((key) => [key, columnConfig[key]]),
|
|
53
|
+
[columnOrder, visibleColumns, columnConfig],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
visibleColumns,
|
|
58
|
+
columnOrder,
|
|
59
|
+
columnWidths,
|
|
60
|
+
toggleColumnVisibility,
|
|
61
|
+
reorderColumns,
|
|
62
|
+
resizeColumn,
|
|
63
|
+
orderedVisibleColumns,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface DataGridExpansionState {
|
|
4
|
+
expandedRows: Set<string>
|
|
5
|
+
toggleExpand: (rowId: string, expandKey: string) => void
|
|
6
|
+
isExpanded: (rowId: string, expandKey: string) => boolean
|
|
7
|
+
collapseAll: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function compositeKey(rowId: string, expandKey: string): string {
|
|
11
|
+
return `${rowId}::${expandKey}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useDataGridExpansion(): DataGridExpansionState {
|
|
15
|
+
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
|
|
16
|
+
|
|
17
|
+
const toggleExpand = useCallback((rowId: string, expandKey: string) => {
|
|
18
|
+
const key = compositeKey(rowId, expandKey)
|
|
19
|
+
setExpandedRows((prev) => {
|
|
20
|
+
const next = new Set(prev)
|
|
21
|
+
if (next.has(key)) {
|
|
22
|
+
next.delete(key)
|
|
23
|
+
} else {
|
|
24
|
+
next.add(key)
|
|
25
|
+
}
|
|
26
|
+
return next
|
|
27
|
+
})
|
|
28
|
+
}, [])
|
|
29
|
+
|
|
30
|
+
const isExpanded = useCallback(
|
|
31
|
+
(rowId: string, expandKey: string) => expandedRows.has(compositeKey(rowId, expandKey)),
|
|
32
|
+
[expandedRows],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const collapseAll = useCallback(() => {
|
|
36
|
+
setExpandedRows(new Set())
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
return { expandedRows, toggleExpand, isExpanded, collapseAll }
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect } from 'react'
|
|
2
|
+
import type { DataRow } from '../types'
|
|
3
|
+
|
|
4
|
+
export interface DataGridFavoritesState {
|
|
5
|
+
favorites: Set<string>
|
|
6
|
+
toggleFavorite: (rowId: string) => void
|
|
7
|
+
isFavorite: (rowId: string) => boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function buildInitialFavorites(data: DataRow[]): Set<string> {
|
|
11
|
+
return new Set(data.filter((row) => row.favorite).map((row) => row.id))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useDataGridFavorites(initialData: DataRow[]): DataGridFavoritesState {
|
|
15
|
+
const [favorites, setFavorites] = useState<Set<string>>(() =>
|
|
16
|
+
buildInitialFavorites(initialData),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// Re-initialize when the dataset changes (e.g. domain switch via parent key remount).
|
|
20
|
+
// This useEffect handles cases where the same hook instance receives new data without
|
|
21
|
+
// remounting — keeps the hook correct in both key-based and prop-based reset scenarios.
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setFavorites(buildInitialFavorites(initialData))
|
|
24
|
+
}, [initialData])
|
|
25
|
+
|
|
26
|
+
const toggleFavorite = useCallback((rowId: string) => {
|
|
27
|
+
setFavorites((prev) => {
|
|
28
|
+
const next = new Set(prev)
|
|
29
|
+
if (next.has(rowId)) {
|
|
30
|
+
next.delete(rowId)
|
|
31
|
+
} else {
|
|
32
|
+
next.add(rowId)
|
|
33
|
+
}
|
|
34
|
+
return next
|
|
35
|
+
})
|
|
36
|
+
}, [])
|
|
37
|
+
|
|
38
|
+
const isFavorite = useCallback((rowId: string) => favorites.has(rowId), [favorites])
|
|
39
|
+
|
|
40
|
+
return { favorites, toggleFavorite, isFavorite }
|
|
41
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useState, useMemo, useCallback } from 'react'
|
|
2
|
+
import type { ColumnConfig, DataGridFilters } from '../types'
|
|
3
|
+
|
|
4
|
+
export interface DataGridFiltersState {
|
|
5
|
+
filters: DataGridFilters
|
|
6
|
+
searchQuery: string
|
|
7
|
+
setFilter: (columnKey: string, values: string[] | null) => void
|
|
8
|
+
setSearchQuery: (query: string) => void
|
|
9
|
+
resetFilters: () => void
|
|
10
|
+
hasActiveFilters: boolean
|
|
11
|
+
activeFilterCount: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function buildInitialFilters(columnConfig: Record<string, ColumnConfig>): DataGridFilters {
|
|
15
|
+
return Object.entries(columnConfig).reduce<DataGridFilters>((acc, [key, config]) => {
|
|
16
|
+
if (config.filterable) acc[key] = null
|
|
17
|
+
return acc
|
|
18
|
+
}, {})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useDataGridFilters(
|
|
22
|
+
columnConfig: Record<string, ColumnConfig>,
|
|
23
|
+
): DataGridFiltersState {
|
|
24
|
+
const [filters, setFilters] = useState<DataGridFilters>(() =>
|
|
25
|
+
buildInitialFilters(columnConfig),
|
|
26
|
+
)
|
|
27
|
+
const [searchQuery, setSearchQuery] = useState('')
|
|
28
|
+
|
|
29
|
+
const setFilter = useCallback((columnKey: string, value: string | null) => {
|
|
30
|
+
setFilters((prev) => ({ ...prev, [columnKey]: value }))
|
|
31
|
+
}, [])
|
|
32
|
+
|
|
33
|
+
const handleSetSearchQuery = useCallback((query: string) => {
|
|
34
|
+
setSearchQuery(query)
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
const resetFilters = useCallback(() => {
|
|
38
|
+
setFilters(buildInitialFilters(columnConfig))
|
|
39
|
+
setSearchQuery('')
|
|
40
|
+
}, [columnConfig])
|
|
41
|
+
|
|
42
|
+
const activeFilterCount = useMemo(
|
|
43
|
+
() => Object.values(filters).filter((v) => v !== null && v.length > 0).length,
|
|
44
|
+
[filters],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const hasActiveFilters = useMemo(
|
|
48
|
+
() => activeFilterCount > 0 || searchQuery !== '',
|
|
49
|
+
[activeFilterCount, searchQuery],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
filters,
|
|
54
|
+
searchQuery,
|
|
55
|
+
setFilter,
|
|
56
|
+
setSearchQuery: handleSetSearchQuery,
|
|
57
|
+
resetFilters,
|
|
58
|
+
hasActiveFilters,
|
|
59
|
+
activeFilterCount,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import type { DataRow } from '../types'
|
|
3
|
+
|
|
4
|
+
export type PageSize = 25 | 50 | 100
|
|
5
|
+
|
|
6
|
+
export interface PaginationState {
|
|
7
|
+
pageSize: PageSize
|
|
8
|
+
setPageSize: (size: PageSize) => void
|
|
9
|
+
currentPage: number
|
|
10
|
+
setCurrentPage: (page: number) => void
|
|
11
|
+
totalPages: number
|
|
12
|
+
pagedData: DataRow[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useDataGridPagination(filteredData: DataRow[]): PaginationState {
|
|
16
|
+
const [pageSize, setPageSize] = useState<PageSize>(25)
|
|
17
|
+
const [currentPage, setCurrentPage] = useState(1)
|
|
18
|
+
|
|
19
|
+
// Reset to page 1 whenever the filtered result set changes size
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setCurrentPage(1)
|
|
22
|
+
}, [filteredData.length])
|
|
23
|
+
|
|
24
|
+
const totalPages = Math.max(1, Math.ceil(filteredData.length / pageSize))
|
|
25
|
+
const safeCurrentPage = Math.min(currentPage, totalPages)
|
|
26
|
+
const pagedData = filteredData.slice(
|
|
27
|
+
(safeCurrentPage - 1) * pageSize,
|
|
28
|
+
safeCurrentPage * pageSize,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
return { pageSize, setPageSize, currentPage: safeCurrentPage, setCurrentPage, totalPages, pagedData }
|
|
32
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
import type { SortDirection } from '../types'
|
|
3
|
+
|
|
4
|
+
export interface DataGridSortState {
|
|
5
|
+
sortColumn: string | null
|
|
6
|
+
sortDirection: SortDirection
|
|
7
|
+
toggleSort: (columnKey: string) => void
|
|
8
|
+
resetSort: () => void
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function useDataGridSort(): DataGridSortState {
|
|
12
|
+
const [sortColumn, setSortColumn] = useState<string | null>(null)
|
|
13
|
+
const [sortDirection, setSortDirection] = useState<SortDirection>('asc')
|
|
14
|
+
|
|
15
|
+
const toggleSort = useCallback((columnKey: string) => {
|
|
16
|
+
setSortColumn((prevColumn) => {
|
|
17
|
+
if (prevColumn === columnKey) {
|
|
18
|
+
setSortDirection((prevDir) => (prevDir === 'asc' ? 'desc' : 'asc'))
|
|
19
|
+
return columnKey
|
|
20
|
+
}
|
|
21
|
+
setSortDirection('asc')
|
|
22
|
+
return columnKey
|
|
23
|
+
})
|
|
24
|
+
}, [])
|
|
25
|
+
|
|
26
|
+
const resetSort = useCallback(() => {
|
|
27
|
+
setSortColumn(null)
|
|
28
|
+
setSortDirection('asc')
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
31
|
+
return { sortColumn, sortDirection, toggleSort, resetSort }
|
|
32
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { DomainConfig, ColumnConfig, DataGridFilters, SortDirection, ViewMode, DataRow, ViewLayoutConfig } from '../types'
|
|
3
|
+
import { useDataGridSort } from './use-data-grid-sort'
|
|
4
|
+
import { useDataGridFilters } from './use-data-grid-filters'
|
|
5
|
+
import { useDataGridColumns } from './use-data-grid-columns'
|
|
6
|
+
import { useDataGridExpansion } from './use-data-grid-expansion'
|
|
7
|
+
import { useDataGridFavorites } from './use-data-grid-favorites'
|
|
8
|
+
import { useFilteredData } from './use-filtered-data'
|
|
9
|
+
import { useDataGridPagination, type PageSize } from './use-data-grid-pagination'
|
|
10
|
+
|
|
11
|
+
export interface DataGridState {
|
|
12
|
+
// View
|
|
13
|
+
currentView: ViewMode
|
|
14
|
+
setCurrentView: (view: ViewMode) => void
|
|
15
|
+
// Sort
|
|
16
|
+
sortColumn: string | null
|
|
17
|
+
sortDirection: SortDirection
|
|
18
|
+
toggleSort: (columnKey: string) => void
|
|
19
|
+
// Filters
|
|
20
|
+
filters: DataGridFilters
|
|
21
|
+
searchQuery: string
|
|
22
|
+
setFilter: (columnKey: string, value: string[] | null) => void
|
|
23
|
+
setSearchQuery: (query: string) => void
|
|
24
|
+
resetFilters: () => void
|
|
25
|
+
hasActiveFilters: boolean
|
|
26
|
+
activeFilterCount: number
|
|
27
|
+
// Columns
|
|
28
|
+
visibleColumns: Set<string>
|
|
29
|
+
columnOrder: string[]
|
|
30
|
+
columnWidths: Record<string, number>
|
|
31
|
+
toggleColumnVisibility: (columnKey: string) => void
|
|
32
|
+
reorderColumns: (newOrder: string[]) => void
|
|
33
|
+
resizeColumn: (columnKey: string, width: number) => void
|
|
34
|
+
orderedVisibleColumns: Array<[string, ColumnConfig]>
|
|
35
|
+
// Expansion
|
|
36
|
+
expandedRows: Set<string>
|
|
37
|
+
toggleExpand: (rowId: string, expandKey: string) => void
|
|
38
|
+
isExpanded: (rowId: string, expandKey: string) => boolean
|
|
39
|
+
// Favorites
|
|
40
|
+
favorites: Set<string>
|
|
41
|
+
toggleFavorite: (rowId: string) => void
|
|
42
|
+
isFavorite: (rowId: string) => boolean
|
|
43
|
+
// Data
|
|
44
|
+
filteredData: DataRow[]
|
|
45
|
+
pagedData: DataRow[]
|
|
46
|
+
totalCount: number
|
|
47
|
+
filteredCount: number
|
|
48
|
+
// Pagination
|
|
49
|
+
pageSize: PageSize
|
|
50
|
+
setPageSize: (size: PageSize) => void
|
|
51
|
+
currentPage: number
|
|
52
|
+
setCurrentPage: (page: number) => void
|
|
53
|
+
totalPages: number
|
|
54
|
+
// Domain config pass-through
|
|
55
|
+
columnConfig: Record<string, ColumnConfig>
|
|
56
|
+
layout: ViewLayoutConfig
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Orchestrates all data-grid sub-hooks for a given domain configuration.
|
|
61
|
+
*
|
|
62
|
+
* Domain switching:
|
|
63
|
+
* The cleanest reset strategy is to mount the component that calls this hook
|
|
64
|
+
* with `key={domainConfig.key}`. React will unmount and remount the component
|
|
65
|
+
* when the key changes, naturally resetting all hook state. No useEffect or
|
|
66
|
+
* manual reset logic is needed inside this hook for domain switches.
|
|
67
|
+
*/
|
|
68
|
+
export function useDataGridState(domainConfig: DomainConfig): DataGridState {
|
|
69
|
+
const [currentView, setCurrentView] = useState<ViewMode>('table')
|
|
70
|
+
|
|
71
|
+
const sort = useDataGridSort()
|
|
72
|
+
const filters = useDataGridFilters(domainConfig.columns)
|
|
73
|
+
const columns = useDataGridColumns(domainConfig.columns)
|
|
74
|
+
const expansion = useDataGridExpansion()
|
|
75
|
+
const favorites = useDataGridFavorites(domainConfig.data)
|
|
76
|
+
const data = useFilteredData(
|
|
77
|
+
domainConfig.data,
|
|
78
|
+
domainConfig.columns,
|
|
79
|
+
filters.filters,
|
|
80
|
+
filters.searchQuery,
|
|
81
|
+
sort.sortColumn,
|
|
82
|
+
sort.sortDirection,
|
|
83
|
+
)
|
|
84
|
+
const pagination = useDataGridPagination(data.filteredData)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
// View
|
|
88
|
+
currentView,
|
|
89
|
+
setCurrentView,
|
|
90
|
+
// Sort
|
|
91
|
+
sortColumn: sort.sortColumn,
|
|
92
|
+
sortDirection: sort.sortDirection,
|
|
93
|
+
toggleSort: sort.toggleSort,
|
|
94
|
+
// Filters
|
|
95
|
+
filters: filters.filters,
|
|
96
|
+
searchQuery: filters.searchQuery,
|
|
97
|
+
setFilter: filters.setFilter,
|
|
98
|
+
setSearchQuery: filters.setSearchQuery,
|
|
99
|
+
resetFilters: filters.resetFilters,
|
|
100
|
+
hasActiveFilters: filters.hasActiveFilters,
|
|
101
|
+
activeFilterCount: filters.activeFilterCount,
|
|
102
|
+
// Columns
|
|
103
|
+
visibleColumns: columns.visibleColumns,
|
|
104
|
+
columnOrder: columns.columnOrder,
|
|
105
|
+
columnWidths: columns.columnWidths,
|
|
106
|
+
toggleColumnVisibility: columns.toggleColumnVisibility,
|
|
107
|
+
reorderColumns: columns.reorderColumns,
|
|
108
|
+
resizeColumn: columns.resizeColumn,
|
|
109
|
+
orderedVisibleColumns: columns.orderedVisibleColumns,
|
|
110
|
+
// Expansion
|
|
111
|
+
expandedRows: expansion.expandedRows,
|
|
112
|
+
toggleExpand: expansion.toggleExpand,
|
|
113
|
+
isExpanded: expansion.isExpanded,
|
|
114
|
+
// Favorites
|
|
115
|
+
favorites: favorites.favorites,
|
|
116
|
+
toggleFavorite: favorites.toggleFavorite,
|
|
117
|
+
isFavorite: favorites.isFavorite,
|
|
118
|
+
// Data
|
|
119
|
+
filteredData: data.filteredData,
|
|
120
|
+
pagedData: pagination.pagedData,
|
|
121
|
+
totalCount: data.totalCount,
|
|
122
|
+
filteredCount: data.filteredCount,
|
|
123
|
+
// Pagination
|
|
124
|
+
pageSize: pagination.pageSize,
|
|
125
|
+
setPageSize: pagination.setPageSize,
|
|
126
|
+
currentPage: pagination.currentPage,
|
|
127
|
+
setCurrentPage: pagination.setCurrentPage,
|
|
128
|
+
totalPages: pagination.totalPages,
|
|
129
|
+
// Domain config pass-through
|
|
130
|
+
columnConfig: domainConfig.columns,
|
|
131
|
+
layout: domainConfig.layout,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import type { DataRow, ColumnConfig, DataGridFilters, SortDirection } from '../types'
|
|
3
|
+
|
|
4
|
+
export interface FilteredDataState {
|
|
5
|
+
filteredData: DataRow[]
|
|
6
|
+
totalCount: number
|
|
7
|
+
filteredCount: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function applySearch(
|
|
11
|
+
data: DataRow[],
|
|
12
|
+
columnConfig: Record<string, ColumnConfig>,
|
|
13
|
+
query: string,
|
|
14
|
+
): DataRow[] {
|
|
15
|
+
if (query === '') return data
|
|
16
|
+
const lower = query.toLowerCase()
|
|
17
|
+
return data.filter((row) =>
|
|
18
|
+
Object.entries(columnConfig).some(([key, config]) => {
|
|
19
|
+
if (!config.searchable) return false
|
|
20
|
+
const primary = String(row[key] ?? '').toLowerCase()
|
|
21
|
+
if (primary.includes(lower)) return true
|
|
22
|
+
// Also check secondary field for double-text cells
|
|
23
|
+
if (config.secondary) {
|
|
24
|
+
const secondary = String(row[config.secondary] ?? '').toLowerCase()
|
|
25
|
+
return secondary.includes(lower)
|
|
26
|
+
}
|
|
27
|
+
return false
|
|
28
|
+
}),
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applyFilters(
|
|
33
|
+
data: DataRow[],
|
|
34
|
+
columnConfig: Record<string, ColumnConfig>,
|
|
35
|
+
filters: DataGridFilters,
|
|
36
|
+
): DataRow[] {
|
|
37
|
+
return data.filter((row) =>
|
|
38
|
+
Object.entries(filters).every(([key, values]) => {
|
|
39
|
+
if (!values || values.length === 0) return true
|
|
40
|
+
const config = columnConfig[key]
|
|
41
|
+
if (!config) return true
|
|
42
|
+
if (config.filterFn) return values.some((v) => config.filterFn!(row, v))
|
|
43
|
+
return values.includes(String(row[key] ?? ''))
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compareValues(a: unknown, b: unknown): number {
|
|
49
|
+
if (typeof a === 'number' && typeof b === 'number') return a - b
|
|
50
|
+
return String(a ?? '').localeCompare(String(b ?? ''))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function applySort(
|
|
54
|
+
data: DataRow[],
|
|
55
|
+
sortColumn: string | null,
|
|
56
|
+
sortDirection: SortDirection,
|
|
57
|
+
): DataRow[] {
|
|
58
|
+
if (!sortColumn) return data
|
|
59
|
+
return [...data].sort((a, b) => {
|
|
60
|
+
const result = compareValues(a[sortColumn], b[sortColumn])
|
|
61
|
+
return sortDirection === 'asc' ? result : -result
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function useFilteredData(
|
|
66
|
+
data: DataRow[],
|
|
67
|
+
columnConfig: Record<string, ColumnConfig>,
|
|
68
|
+
filters: DataGridFilters,
|
|
69
|
+
searchQuery: string,
|
|
70
|
+
sortColumn: string | null,
|
|
71
|
+
sortDirection: SortDirection,
|
|
72
|
+
): FilteredDataState {
|
|
73
|
+
const totalCount = useMemo(() => data.length, [data])
|
|
74
|
+
|
|
75
|
+
const filteredData = useMemo(() => {
|
|
76
|
+
const searched = applySearch(data, columnConfig, searchQuery)
|
|
77
|
+
const filtered = applyFilters(searched, columnConfig, filters)
|
|
78
|
+
return applySort(filtered, sortColumn, sortDirection)
|
|
79
|
+
}, [data, columnConfig, searchQuery, filters, sortColumn, sortDirection])
|
|
80
|
+
|
|
81
|
+
const filteredCount = useMemo(() => filteredData.length, [filteredData])
|
|
82
|
+
|
|
83
|
+
return { filteredData, totalCount, filteredCount }
|
|
84
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { DataGridPage } from './DataGridPage/DataGridPage'
|
|
2
|
+
export type { DataGridPageProps } from './DataGridPage/DataGridPage'
|
|
3
|
+
export { DataGrid } from './DataGrid/DataGrid'
|
|
4
|
+
export type { DataGridProps } from './DataGrid/DataGrid'
|
|
5
|
+
export { DomainSwitcher } from './DomainSwitcher/DomainSwitcher'
|
|
6
|
+
export type { DomainSwitcherProps } from './DomainSwitcher/DomainSwitcher'
|
|
7
|
+
// Types
|
|
8
|
+
export type { DomainConfig, ColumnConfig, DataRow, ViewMode, CellType } from './types'
|
|
9
|
+
// Constants for consumers
|
|
10
|
+
export { DATA_SOURCES, DOMAIN_KEYS } from './constants'
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
export type CellType =
|
|
2
|
+
| 'text'
|
|
3
|
+
| 'double-text'
|
|
4
|
+
| 'link'
|
|
5
|
+
| 'status-badge'
|
|
6
|
+
| 'currency'
|
|
7
|
+
| 'inventory'
|
|
8
|
+
| 'inventory-label'
|
|
9
|
+
| 'progress'
|
|
10
|
+
| 'favorite'
|
|
11
|
+
| 'expand'
|
|
12
|
+
|
|
13
|
+
export type ViewMode = 'table' | 'list' | 'cards'
|
|
14
|
+
|
|
15
|
+
export type SortDirection = 'asc' | 'desc'
|
|
16
|
+
|
|
17
|
+
export type DataRow = Record<string, unknown> & { id: string; favorite?: boolean }
|
|
18
|
+
|
|
19
|
+
export interface ExpandColumnConfig {
|
|
20
|
+
key: string
|
|
21
|
+
label: string
|
|
22
|
+
type?: 'score-bar'
|
|
23
|
+
bold?: boolean
|
|
24
|
+
muted?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ColumnConfig {
|
|
28
|
+
label: string
|
|
29
|
+
type: CellType
|
|
30
|
+
sortable?: boolean
|
|
31
|
+
filterable?: boolean
|
|
32
|
+
primaryFilter?: boolean
|
|
33
|
+
visible?: boolean
|
|
34
|
+
searchable?: boolean
|
|
35
|
+
hideTablet?: boolean
|
|
36
|
+
hideMobile?: boolean
|
|
37
|
+
secondary?: string // for 'double-text'
|
|
38
|
+
statusMap?: Record<string, string> // for 'status-badge' - maps value to CSS class
|
|
39
|
+
currencyField?: string // for 'currency'
|
|
40
|
+
levelFn?: (value: number) => 'high' | 'medium' | 'low' // for 'inventory'
|
|
41
|
+
formatFn?: (value: number) => string // for 'inventory'
|
|
42
|
+
labelMap?: Record<string, string> // for 'inventory-label'
|
|
43
|
+
rowLines?: 1 | 2 | 3
|
|
44
|
+
filterOptions?: string[]
|
|
45
|
+
filterFn?: (row: DataRow, selectedValue: string) => boolean
|
|
46
|
+
expandLabel?: string // for 'expand'
|
|
47
|
+
expandColumns?: ExpandColumnConfig[] // for 'expand'
|
|
48
|
+
expandTitleFn?: (row: DataRow) => string // for 'expand'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ListLayoutConfig {
|
|
52
|
+
titleField: string
|
|
53
|
+
metaFields: string[]
|
|
54
|
+
badgeFields: string[]
|
|
55
|
+
valueField: string | null
|
|
56
|
+
expandField: string | null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface CardRowConfig {
|
|
60
|
+
label: string
|
|
61
|
+
field: string
|
|
62
|
+
rendererOverride?: CellType
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CardLayoutConfig {
|
|
66
|
+
titleField: string
|
|
67
|
+
subtitleField?: string
|
|
68
|
+
badgeFields: string[]
|
|
69
|
+
rows: CardRowConfig[]
|
|
70
|
+
footerField: string | null
|
|
71
|
+
expandField: string | null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface ViewLayoutConfig {
|
|
75
|
+
list: ListLayoutConfig
|
|
76
|
+
card: CardLayoutConfig
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface DomainConfig {
|
|
80
|
+
key: string
|
|
81
|
+
label: string
|
|
82
|
+
resultLabel: string
|
|
83
|
+
columns: Record<string, ColumnConfig>
|
|
84
|
+
layout: ViewLayoutConfig
|
|
85
|
+
data: DataRow[]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface DataGridFilters {
|
|
89
|
+
[columnKey: string]: string[] | null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface DataGridState {
|
|
93
|
+
currentView: ViewMode
|
|
94
|
+
searchQuery: string
|
|
95
|
+
filters: DataGridFilters
|
|
96
|
+
sortColumn: string | null
|
|
97
|
+
sortDirection: SortDirection
|
|
98
|
+
expandedRows: Set<string> // composite key: `${rowId}::${expandKey}`
|
|
99
|
+
favorites: Set<string>
|
|
100
|
+
visibleColumns: Set<string>
|
|
101
|
+
columnOrder: string[]
|
|
102
|
+
columnWidths: Record<string, number>
|
|
103
|
+
}
|