@lad-sapienza/scms-core 0.1.0-alpha.0
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/LICENSE +14 -0
- package/README.md +73 -0
- package/components/BSNavbar.tsx +264 -0
- package/components/DataTb/DataTb.astro +33 -0
- package/components/DataTb/DataTb.tsx +340 -0
- package/components/DataTb/DataTbMdx.tsx +75 -0
- package/components/DataTb/README.md +275 -0
- package/components/DataTb/index.ts +36 -0
- package/components/DataTb/sources/ApiSource.tsx +89 -0
- package/components/DataTb/sources/CsvSource.tsx +69 -0
- package/components/DataTb/sources/DirectusSource.tsx +98 -0
- package/components/DataTb/sources/JsonSource.tsx +65 -0
- package/components/DataTb/sources/index.ts +17 -0
- package/components/DataTb/types.ts +215 -0
- package/components/DataTb/utils.ts +186 -0
- package/components/Gallery/Gallery.astro +65 -0
- package/components/Gallery/Gallery.tsx +136 -0
- package/components/Gallery/README.md +118 -0
- package/components/Gallery/__tests__/galleryUtils.test.ts +178 -0
- package/components/Gallery/galleryUtils.ts +186 -0
- package/components/Gallery/index.ts +7 -0
- package/components/Gallery/react-photoswipe-gallery.d.ts +26 -0
- package/components/Gallery/types.ts +46 -0
- package/components/Gallery/virtual-gallery.d.ts +15 -0
- package/components/Map/LayerControl.tsx +368 -0
- package/components/Map/Map.tsx +735 -0
- package/components/Map/MapMdx.tsx +37 -0
- package/components/Map/RasterLayerLibre.tsx +33 -0
- package/components/Map/Search/SearchOperators.ts +31 -0
- package/components/Map/Search/SearchUI.tsx +75 -0
- package/components/Map/Search/SearchUIAdvanced.tsx +289 -0
- package/components/Map/Search/SearchUISimple.tsx +115 -0
- package/components/Map/Search/index.ts +7 -0
- package/components/Map/defaultBasemaps.ts +88 -0
- package/components/Map/index.ts +6 -0
- package/components/Map/types.ts +179 -0
- package/components/Map/utils.ts +367 -0
- package/components/README.md +62 -0
- package/components/Record/Field.tsx +35 -0
- package/components/Record/Image.tsx +114 -0
- package/components/Record/RecordFetcher.tsx +127 -0
- package/components/Record/context.tsx +14 -0
- package/components/Record/index.ts +11 -0
- package/components/Record/useRecordFetcher.ts +104 -0
- package/components/Record/utils.ts +13 -0
- package/components/SEO/SEO.astro +132 -0
- package/components/TableOfContents/TableOfContents.astro +86 -0
- package/components/ZoteroGeoViewer/README.md +195 -0
- package/components/ZoteroGeoViewer/ZoteroGeoViewer.astro +26 -0
- package/components/ZoteroGeoViewer/ZoteroGeoViewer.tsx +483 -0
- package/components/ZoteroGeoViewer/ZoteroRecordsPreview.tsx +185 -0
- package/components/ZoteroGeoViewer/index.ts +16 -0
- package/components/ZoteroGeoViewer/types.ts +123 -0
- package/index.ts +26 -0
- package/integrations/__tests__/galleryIntegration.test.ts +70 -0
- package/integrations/contentAssetsIntegration.ts +132 -0
- package/integrations/directusLoader.ts +98 -0
- package/integrations/galleryIntegration.ts +78 -0
- package/package.json +56 -0
- package/scms.ts +95 -0
- package/utils/content.ts +88 -0
- package/utils/data-fetcher.ts +128 -0
- package/utils/directus-config.ts +160 -0
- package/utils/record-fetcher.ts +132 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DataTb Component
|
|
3
|
+
*
|
|
4
|
+
* Main data table component using TanStack Table
|
|
5
|
+
* Supports sorting, filtering, and pagination
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
useReactTable,
|
|
11
|
+
getCoreRowModel,
|
|
12
|
+
getSortedRowModel,
|
|
13
|
+
getFilteredRowModel,
|
|
14
|
+
getPaginationRowModel,
|
|
15
|
+
flexRender,
|
|
16
|
+
type SortingState,
|
|
17
|
+
type ColumnFiltersState,
|
|
18
|
+
} from '@tanstack/react-table';
|
|
19
|
+
import { fetchData } from '../../utils/data-fetcher';
|
|
20
|
+
import type { DataRow, DataTbProps } from './types';
|
|
21
|
+
import {
|
|
22
|
+
autoDetectColumns,
|
|
23
|
+
mergeColumns,
|
|
24
|
+
columnConfigToColumnDef,
|
|
25
|
+
globalFilterFn,
|
|
26
|
+
} from './utils';
|
|
27
|
+
|
|
28
|
+
export function DataTb({
|
|
29
|
+
columns: userColumns,
|
|
30
|
+
searchable = false,
|
|
31
|
+
pagination = true,
|
|
32
|
+
sortable = true,
|
|
33
|
+
initialSort,
|
|
34
|
+
className = '',
|
|
35
|
+
source,
|
|
36
|
+
loadingMessage = 'Loading data...',
|
|
37
|
+
emptyMessage = 'No data available',
|
|
38
|
+
errorMessage = 'Error loading data',
|
|
39
|
+
truncateContent = true,
|
|
40
|
+
truncateMaxWidth = '20rem',
|
|
41
|
+
contentSize = 'sm',
|
|
42
|
+
}: DataTbProps) {
|
|
43
|
+
const [data, setData] = useState<DataRow[]>([]);
|
|
44
|
+
const [loading, setLoading] = useState(true);
|
|
45
|
+
const [error, setError] = useState<Error | null>(null);
|
|
46
|
+
const [sorting, setSorting] = useState<SortingState>(
|
|
47
|
+
initialSort ? [{ id: initialSort.columnKey, desc: initialSort.direction === 'desc' }] : []
|
|
48
|
+
);
|
|
49
|
+
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
|
50
|
+
const [globalFilter, setGlobalFilter] = useState('');
|
|
51
|
+
|
|
52
|
+
// Use a ref to track source changes more reliably
|
|
53
|
+
const sourceRef = useRef<string>('');
|
|
54
|
+
const currentSourceKey = JSON.stringify(source);
|
|
55
|
+
|
|
56
|
+
// Pagination config
|
|
57
|
+
const paginationConfig = typeof pagination === 'object' ? pagination : {};
|
|
58
|
+
const pageSize = paginationConfig.pageSize || 10;
|
|
59
|
+
const pageSizeOptions = paginationConfig.pageSizeOptions || [10, 20, 50, 100];
|
|
60
|
+
const contentSizeClass = contentSize === 'sm' ? 'small' : '';
|
|
61
|
+
|
|
62
|
+
// Fetch data based on source configuration
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
// Check if source has actually changed
|
|
65
|
+
if (currentSourceKey === sourceRef.current) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
sourceRef.current = currentSourceKey;
|
|
70
|
+
|
|
71
|
+
if (!source) {
|
|
72
|
+
setError(new Error('No data source provided'));
|
|
73
|
+
setLoading(false);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Guards against a stale response overwriting newer state if the source
|
|
78
|
+
// changes again before this fetch resolves.
|
|
79
|
+
let cancelled = false;
|
|
80
|
+
|
|
81
|
+
const loadData = async () => {
|
|
82
|
+
try {
|
|
83
|
+
setLoading(true);
|
|
84
|
+
setError(null);
|
|
85
|
+
|
|
86
|
+
const fetchedData = await fetchData(source);
|
|
87
|
+
if (cancelled) return;
|
|
88
|
+
|
|
89
|
+
setData(fetchedData);
|
|
90
|
+
setLoading(false);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (cancelled) return;
|
|
93
|
+
setError(err instanceof Error ? err : new Error('Failed to load data'));
|
|
94
|
+
setLoading(false);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
loadData();
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
cancelled = true;
|
|
102
|
+
};
|
|
103
|
+
}, [currentSourceKey]); // Use stringified source as dependency
|
|
104
|
+
|
|
105
|
+
// Auto-detect or merge columns
|
|
106
|
+
const finalColumns = useMemo(() => {
|
|
107
|
+
if (data.length === 0) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const autoColumns = autoDetectColumns(data);
|
|
112
|
+
const merged = mergeColumns(autoColumns, userColumns);
|
|
113
|
+
return merged;
|
|
114
|
+
}, [data, userColumns]);
|
|
115
|
+
|
|
116
|
+
// Convert to TanStack column definitions
|
|
117
|
+
const columnDefs = useMemo(
|
|
118
|
+
() => finalColumns.map(columnConfigToColumnDef),
|
|
119
|
+
[finalColumns]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Initialize table
|
|
123
|
+
const table = useReactTable({
|
|
124
|
+
data,
|
|
125
|
+
columns: columnDefs,
|
|
126
|
+
getCoreRowModel: getCoreRowModel(),
|
|
127
|
+
getSortedRowModel: sortable ? getSortedRowModel() : undefined,
|
|
128
|
+
getFilteredRowModel: searchable ? getFilteredRowModel() : undefined,
|
|
129
|
+
getPaginationRowModel: pagination ? getPaginationRowModel() : undefined,
|
|
130
|
+
onSortingChange: setSorting,
|
|
131
|
+
onColumnFiltersChange: setColumnFilters,
|
|
132
|
+
onGlobalFilterChange: setGlobalFilter,
|
|
133
|
+
globalFilterFn: globalFilterFn,
|
|
134
|
+
state: {
|
|
135
|
+
sorting,
|
|
136
|
+
columnFilters,
|
|
137
|
+
globalFilter,
|
|
138
|
+
},
|
|
139
|
+
initialState: {
|
|
140
|
+
pagination: {
|
|
141
|
+
pageSize,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Render loading state
|
|
147
|
+
if (loading) {
|
|
148
|
+
return (
|
|
149
|
+
<div className="datatb-container">
|
|
150
|
+
<div className="datatb-loading p-4 text-center text-secondary">
|
|
151
|
+
{loadingMessage}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Render error state
|
|
158
|
+
if (error) {
|
|
159
|
+
return (
|
|
160
|
+
<div className="datatb-container">
|
|
161
|
+
<div className="datatb-error p-4 text-center text-danger">
|
|
162
|
+
{errorMessage}: {error.message}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Render empty state
|
|
169
|
+
if (data.length === 0) {
|
|
170
|
+
return (
|
|
171
|
+
<div className="datatb-container">
|
|
172
|
+
<div className="datatb-empty p-4 text-center text-secondary">
|
|
173
|
+
{emptyMessage}
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div className={`datatb-container ${className}`}>
|
|
181
|
+
|
|
182
|
+
{/* Search input */}
|
|
183
|
+
{searchable && (
|
|
184
|
+
<div className="datatb-search mb-3">
|
|
185
|
+
<input
|
|
186
|
+
type="text"
|
|
187
|
+
value={globalFilter ?? ''}
|
|
188
|
+
onChange={(e) => setGlobalFilter(e.target.value)}
|
|
189
|
+
placeholder="Search..."
|
|
190
|
+
className="form-control form-control-sm"
|
|
191
|
+
style={{ maxWidth: '24rem' }}
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
{/* Table */}
|
|
197
|
+
<div className="datatb-table-wrapper table-responsive border rounded shadow-sm">
|
|
198
|
+
<table className="datatb-table table table-sm table-hover mb-0">
|
|
199
|
+
<thead className="table-light">
|
|
200
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
201
|
+
<tr key={headerGroup.id}>
|
|
202
|
+
{headerGroup.headers.map((header, index) => (
|
|
203
|
+
<th
|
|
204
|
+
key={header.id}
|
|
205
|
+
className={`py-2 text-start small fw-semibold text-uppercase ${
|
|
206
|
+
index === 0 ? 'ps-3 pe-3' : 'px-3'
|
|
207
|
+
}`}
|
|
208
|
+
style={{ width: header.column.columnDef.meta?.width }}
|
|
209
|
+
>
|
|
210
|
+
{header.isPlaceholder ? null : (
|
|
211
|
+
<div
|
|
212
|
+
className={
|
|
213
|
+
header.column.getCanSort()
|
|
214
|
+
? 'd-flex align-items-center gap-2 user-select-none'
|
|
215
|
+
: 'd-flex align-items-center gap-2'
|
|
216
|
+
}
|
|
217
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
218
|
+
style={header.column.getCanSort() ? { cursor: 'pointer' } : undefined}
|
|
219
|
+
>
|
|
220
|
+
{flexRender(
|
|
221
|
+
header.column.columnDef.header,
|
|
222
|
+
header.getContext()
|
|
223
|
+
)}
|
|
224
|
+
{header.column.getCanSort() && (
|
|
225
|
+
<span className="text-secondary">
|
|
226
|
+
{{
|
|
227
|
+
asc: '↑',
|
|
228
|
+
desc: '↓',
|
|
229
|
+
}[header.column.getIsSorted() as string] ?? '↕'}
|
|
230
|
+
</span>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</th>
|
|
235
|
+
))}
|
|
236
|
+
</tr>
|
|
237
|
+
))}
|
|
238
|
+
</thead>
|
|
239
|
+
<tbody>
|
|
240
|
+
{table.getRowModel().rows.map((row) => (
|
|
241
|
+
<tr key={row.id}>
|
|
242
|
+
{row.getVisibleCells().map((cell, index) => (
|
|
243
|
+
<td
|
|
244
|
+
key={cell.id}
|
|
245
|
+
className={`py-2 ${contentSizeClass} ${
|
|
246
|
+
index === 0 ? 'ps-3 pe-3' : 'px-3'
|
|
247
|
+
}`}
|
|
248
|
+
>
|
|
249
|
+
{(() => {
|
|
250
|
+
const cellContent = flexRender(cell.column.columnDef.cell, cell.getContext());
|
|
251
|
+
const isSimpleText =
|
|
252
|
+
typeof cellContent === 'string' || typeof cellContent === 'number';
|
|
253
|
+
|
|
254
|
+
if (!truncateContent) {
|
|
255
|
+
return cellContent;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
<div
|
|
260
|
+
className="text-truncate"
|
|
261
|
+
style={{ maxWidth: truncateMaxWidth }}
|
|
262
|
+
title={isSimpleText ? String(cellContent) : undefined}
|
|
263
|
+
>
|
|
264
|
+
{cellContent}
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
})()}
|
|
268
|
+
</td>
|
|
269
|
+
))}
|
|
270
|
+
</tr>
|
|
271
|
+
))}
|
|
272
|
+
</tbody>
|
|
273
|
+
</table>
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{/* Pagination */}
|
|
277
|
+
{pagination && (
|
|
278
|
+
<div className="datatb-pagination mt-3 d-flex flex-row align-items-center justify-content-between gap-2">
|
|
279
|
+
<div className="btn-group" role="group">
|
|
280
|
+
<button
|
|
281
|
+
onClick={() => table.setPageIndex(0)}
|
|
282
|
+
disabled={!table.getCanPreviousPage()}
|
|
283
|
+
className="btn btn-outline-secondary btn-sm"
|
|
284
|
+
>
|
|
285
|
+
{'<<'}
|
|
286
|
+
</button>
|
|
287
|
+
<button
|
|
288
|
+
onClick={() => table.previousPage()}
|
|
289
|
+
disabled={!table.getCanPreviousPage()}
|
|
290
|
+
className="btn btn-outline-secondary btn-sm"
|
|
291
|
+
>
|
|
292
|
+
{'<'}
|
|
293
|
+
</button>
|
|
294
|
+
<button
|
|
295
|
+
onClick={() => table.nextPage()}
|
|
296
|
+
disabled={!table.getCanNextPage()}
|
|
297
|
+
className="btn btn-outline-secondary btn-sm"
|
|
298
|
+
>
|
|
299
|
+
{'>'}
|
|
300
|
+
</button>
|
|
301
|
+
<button
|
|
302
|
+
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
303
|
+
disabled={!table.getCanNextPage()}
|
|
304
|
+
className="btn btn-outline-secondary btn-sm"
|
|
305
|
+
>
|
|
306
|
+
{'>>'}
|
|
307
|
+
</button>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<div className="d-flex align-items-center gap-2 flex-nowrap">
|
|
311
|
+
<span className="small text-secondary text-nowrap">
|
|
312
|
+
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
|
|
313
|
+
</span>
|
|
314
|
+
|
|
315
|
+
{paginationConfig.showPageSize !== false && (
|
|
316
|
+
<select
|
|
317
|
+
value={table.getState().pagination.pageSize}
|
|
318
|
+
onChange={(e) => {
|
|
319
|
+
table.setPageSize(Number(e.target.value));
|
|
320
|
+
}}
|
|
321
|
+
className="form-select form-select-sm"
|
|
322
|
+
style={{ width: 'auto' }}
|
|
323
|
+
>
|
|
324
|
+
{pageSizeOptions.map((size) => (
|
|
325
|
+
<option key={size} value={size}>
|
|
326
|
+
{size}
|
|
327
|
+
</option>
|
|
328
|
+
))}
|
|
329
|
+
</select>
|
|
330
|
+
)}
|
|
331
|
+
|
|
332
|
+
<span className="small text-secondary text-nowrap">
|
|
333
|
+
({data.length} total)
|
|
334
|
+
</span>
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MDX-safe wrapper for DataTb component
|
|
3
|
+
*
|
|
4
|
+
* This wrapper ensures that object props are properly memoized
|
|
5
|
+
* to prevent infinite re-renders in MDX context
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useMemo } from 'react';
|
|
9
|
+
import { DataTb as DataTbCore } from './DataTb';
|
|
10
|
+
import type { DataTbProps, SourceConfig } from './types';
|
|
11
|
+
import { directusShorthandToConfig } from '../../utils/directus-config';
|
|
12
|
+
|
|
13
|
+
export function DataTb(props: DataTbProps) {
|
|
14
|
+
// Resolve source from simplified props or original source prop
|
|
15
|
+
const resolvedSource = useMemo<SourceConfig | undefined>(() => {
|
|
16
|
+
// If explicit source object is provided, use it (memoized later)
|
|
17
|
+
if (props.source) return props.source;
|
|
18
|
+
|
|
19
|
+
// Handle simplified CSV prop
|
|
20
|
+
if (props.csv) {
|
|
21
|
+
return { type: 'csv', url: props.csv } as const;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Handle simplified JSON prop
|
|
25
|
+
if (props.json) {
|
|
26
|
+
if (Array.isArray(props.json)) {
|
|
27
|
+
return { type: 'json', data: props.json } as const;
|
|
28
|
+
}
|
|
29
|
+
return { type: 'json', url: props.json as string } as const;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Handle simplified API prop
|
|
33
|
+
if (props.api) {
|
|
34
|
+
return { type: 'api', url: props.api } as const;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Handle simplified Directus prop (same interface as Map component)
|
|
38
|
+
if (props.directus) {
|
|
39
|
+
const directusSource = directusShorthandToConfig(props.directus);
|
|
40
|
+
return directusSource ?? undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return undefined;
|
|
44
|
+
}, [props.source, props.csv, props.json, props.api, JSON.stringify(props.directus)]);
|
|
45
|
+
|
|
46
|
+
// Memoize the source object to prevent re-creation on every render
|
|
47
|
+
// This is critical for MDX which might recreate objects on every pass
|
|
48
|
+
const stableSource = useMemo(() => resolvedSource, [JSON.stringify(resolvedSource)]);
|
|
49
|
+
|
|
50
|
+
// Memoize columns if provided
|
|
51
|
+
const stableColumns = useMemo(() => props.columns, [JSON.stringify(props.columns)]);
|
|
52
|
+
|
|
53
|
+
// Memoize pagination config if it's an object
|
|
54
|
+
const stablePagination = useMemo(() => {
|
|
55
|
+
if (typeof props.pagination === 'object') {
|
|
56
|
+
return props.pagination;
|
|
57
|
+
}
|
|
58
|
+
return props.pagination;
|
|
59
|
+
}, [JSON.stringify(props.pagination)]);
|
|
60
|
+
|
|
61
|
+
// Memoize initialSort if provided
|
|
62
|
+
const stableInitialSort = useMemo(() => props.initialSort, [JSON.stringify(props.initialSort)]);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<DataTbCore
|
|
66
|
+
{...props}
|
|
67
|
+
source={stableSource}
|
|
68
|
+
columns={stableColumns}
|
|
69
|
+
pagination={stablePagination}
|
|
70
|
+
initialSort={stableInitialSort}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export default DataTb;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# DataTb Component
|
|
2
|
+
|
|
3
|
+
A powerful, flexible data table component for Astro with support for multiple data sources, built on TanStack Table.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 📊 **Multiple Data Sources**: CSV, JSON, Directus, and generic APIs
|
|
8
|
+
- 🔍 **Global Search**: Filter data across all columns
|
|
9
|
+
- ↕️ **Column Sorting**: Click headers to sort data
|
|
10
|
+
- 📄 **Pagination**: Built-in pagination with customizable page sizes
|
|
11
|
+
- 🎨 **Auto-detection**: Automatically detects columns from data
|
|
12
|
+
- ⚙️ **Customizable**: Override columns, formatting, and rendering
|
|
13
|
+
- 🎯 **TypeScript**: Full type safety
|
|
14
|
+
- 💅 **Bootstrap Styled**: Uses the same Bootstrap 5 classes as the rest of s:CMS, themeable via `usr/styles/global.css`
|
|
15
|
+
|
|
16
|
+
## Basic Usage
|
|
17
|
+
|
|
18
|
+
### CSV Source
|
|
19
|
+
|
|
20
|
+
```jsx
|
|
21
|
+
import { DataTb } from '@lad-sapienza/scms-core/components/DataTb';
|
|
22
|
+
|
|
23
|
+
<DataTb
|
|
24
|
+
source={{ type: 'csv', url: '/data/products.csv' }}
|
|
25
|
+
searchable
|
|
26
|
+
pagination
|
|
27
|
+
client:idle
|
|
28
|
+
/>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### JSON Source
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
<DataTb
|
|
35
|
+
source={{
|
|
36
|
+
type: 'json',
|
|
37
|
+
data: [
|
|
38
|
+
{ name: 'Item 1', price: 10 },
|
|
39
|
+
{ name: 'Item 2', price: 20 }
|
|
40
|
+
]
|
|
41
|
+
}}
|
|
42
|
+
searchable
|
|
43
|
+
client:idle
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Directus Source
|
|
48
|
+
|
|
49
|
+
Reads `PUBLIC_DIRECTUS_URL` / `PUBLIC_DIRECTUS_TOKEN` from `.env` automatically — see [Directus Setup](#directus-setup) below.
|
|
50
|
+
|
|
51
|
+
```jsx
|
|
52
|
+
<DataTb
|
|
53
|
+
directus={{
|
|
54
|
+
table: 'articles',
|
|
55
|
+
queryString: 'filter[status][_eq]=published&sort=-date_published',
|
|
56
|
+
}}
|
|
57
|
+
searchable
|
|
58
|
+
pagination
|
|
59
|
+
client:idle
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### API Source
|
|
64
|
+
|
|
65
|
+
```jsx
|
|
66
|
+
<DataTb
|
|
67
|
+
source={{
|
|
68
|
+
type: 'api',
|
|
69
|
+
url: 'https://api.example.com/data',
|
|
70
|
+
transformer: (data) => data.results
|
|
71
|
+
}}
|
|
72
|
+
searchable
|
|
73
|
+
pagination
|
|
74
|
+
client:idle
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Custom Columns
|
|
79
|
+
|
|
80
|
+
Override column display, formatting, and rendering:
|
|
81
|
+
|
|
82
|
+
```jsx
|
|
83
|
+
<DataTb
|
|
84
|
+
source={{ type: 'csv', url: '/data/products.csv' }}
|
|
85
|
+
columns={[
|
|
86
|
+
{ key: 'id', header: 'ID', width: '80px' },
|
|
87
|
+
{ key: 'name', header: 'Product Name', sortable: true },
|
|
88
|
+
{ key: 'price', header: 'Price', format: 'currency' },
|
|
89
|
+
{ key: 'date', header: 'Date', format: 'date' },
|
|
90
|
+
{
|
|
91
|
+
key: 'status',
|
|
92
|
+
header: 'Status',
|
|
93
|
+
render: (val) => val === 'active' ? '✅' : '❌'
|
|
94
|
+
}
|
|
95
|
+
]}
|
|
96
|
+
searchable
|
|
97
|
+
pagination={{ pageSize: 20 }}
|
|
98
|
+
initialSort={{ columnKey: 'name', direction: 'asc' }}
|
|
99
|
+
client:idle
|
|
100
|
+
/>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Props
|
|
104
|
+
|
|
105
|
+
### DataTb Component
|
|
106
|
+
|
|
107
|
+
| Prop | Type | Default | Description |
|
|
108
|
+
|------|------|---------|-------------|
|
|
109
|
+
| `columns` | `ColumnConfig[]` | auto-detected | Column definitions |
|
|
110
|
+
| `searchable` | `boolean` | `false` | Enable global search |
|
|
111
|
+
| `pagination` | `boolean \| PaginationConfig` | `true` | Enable/configure pagination |
|
|
112
|
+
| `sortable` | `boolean` | `true` | Enable column sorting |
|
|
113
|
+
| `initialSort` | `{ columnKey, direction }` | - | Initial sort state |
|
|
114
|
+
| `className` | `string` | `''` | Custom CSS classes |
|
|
115
|
+
| `loadingMessage` | `string` | `'Loading data...'` | Loading message |
|
|
116
|
+
| `emptyMessage` | `string` | `'No data available'` | Empty state message |
|
|
117
|
+
| `errorMessage` | `string` | `'Error loading data'` | Error message |
|
|
118
|
+
| `truncateContent` | `boolean` | `true` | Truncate large cell content with ellipsis |
|
|
119
|
+
| `truncateMaxWidth` | `string` | `'20rem'` | Max width for truncated cell content |
|
|
120
|
+
| `contentSize` | `'sm' \| 'md'` | `'sm'` | Table body text size |
|
|
121
|
+
|
|
122
|
+
### Column Config
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
{
|
|
126
|
+
key: string; // Data property key
|
|
127
|
+
header: string; // Display header text
|
|
128
|
+
sortable?: boolean; // Enable sorting (default: true)
|
|
129
|
+
format?: 'date' | 'number' | 'currency' | 'percent';
|
|
130
|
+
render?: (value, row) => ReactNode; // Custom cell renderer
|
|
131
|
+
width?: string; // Column width (CSS value)
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Source Components
|
|
136
|
+
|
|
137
|
+
#### CsvSource
|
|
138
|
+
|
|
139
|
+
| Prop | Type | Description |
|
|
140
|
+
|------|------|-------------|
|
|
141
|
+
| `url` | `string` | CSV file URL (local or remote) |
|
|
142
|
+
| `delimiter` | `string` | Delimiter character (default: `','`) |
|
|
143
|
+
| `skipRows` | `number` | Skip first N rows |
|
|
144
|
+
|
|
145
|
+
#### JsonSource
|
|
146
|
+
|
|
147
|
+
| Prop | Type | Description |
|
|
148
|
+
|------|------|-------------|
|
|
149
|
+
| `data` | `DataRow[]` | Inline JSON data |
|
|
150
|
+
| `url` | `string` | URL to fetch JSON from |
|
|
151
|
+
|
|
152
|
+
#### DirectusSource
|
|
153
|
+
|
|
154
|
+
| Prop | Type | Description |
|
|
155
|
+
|------|------|-------------|
|
|
156
|
+
| `collection` | `string` | Collection name |
|
|
157
|
+
| `config` | `{ url, token }` | Directus configuration |
|
|
158
|
+
| `filter` | `object` | Directus filter query |
|
|
159
|
+
| `fields` | `string[]` | Fields to select |
|
|
160
|
+
| `sort` | `string[]` | Sort order (e.g., `['-date']`) |
|
|
161
|
+
| `limit` | `number` | Limit results |
|
|
162
|
+
|
|
163
|
+
#### ApiSource
|
|
164
|
+
|
|
165
|
+
| Prop | Type | Description |
|
|
166
|
+
|------|------|-------------|
|
|
167
|
+
| `url` | `string` | API endpoint URL |
|
|
168
|
+
| `method` | `'GET' \| 'POST'` | HTTP method (default: `'GET'`) |
|
|
169
|
+
| `headers` | `Record<string, string>` | Request headers |
|
|
170
|
+
| `body` | `any` | Request body (for POST) |
|
|
171
|
+
| `transformer` | `(data) => DataRow[]` | Transform response data |
|
|
172
|
+
|
|
173
|
+
## Formatting
|
|
174
|
+
|
|
175
|
+
Built-in formatters for common data types:
|
|
176
|
+
|
|
177
|
+
- `format: 'date'` - Formats dates (e.g., "Jan 10, 2026")
|
|
178
|
+
- `format: 'number'` - Formats numbers with thousands separators
|
|
179
|
+
- `format: 'currency'` - Formats as USD currency
|
|
180
|
+
- `format: 'percent'` - Formats as percentage
|
|
181
|
+
|
|
182
|
+
## Custom Rendering
|
|
183
|
+
|
|
184
|
+
Use the `render` function for complete control:
|
|
185
|
+
|
|
186
|
+
```jsx
|
|
187
|
+
columns={[
|
|
188
|
+
{
|
|
189
|
+
key: 'avatar',
|
|
190
|
+
header: 'Avatar',
|
|
191
|
+
render: (url) => <img src={url} alt="" className="w-10 h-10 rounded-full" />
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
key: 'status',
|
|
195
|
+
header: 'Status',
|
|
196
|
+
render: (status, row) => (
|
|
197
|
+
<span className={status === 'active' ? 'text-green-600' : 'text-red-600'}>
|
|
198
|
+
{status}
|
|
199
|
+
</span>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
]}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Directus Setup
|
|
206
|
+
|
|
207
|
+
1. Generate a token in the Directus admin panel:
|
|
208
|
+
- Settings → Access Tokens → Create Token
|
|
209
|
+
- Set appropriate permissions
|
|
210
|
+
- Copy the token
|
|
211
|
+
|
|
212
|
+
2. Add it to `.env` in the project root:
|
|
213
|
+
|
|
214
|
+
```env
|
|
215
|
+
PUBLIC_DIRECTUS_URL=https://your-directus-instance.com
|
|
216
|
+
PUBLIC_DIRECTUS_TOKEN=your-access-token
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
3. Use the simplified `directus` prop — `DataTb` reads the URL/token from those env vars automatically, no config object needed:
|
|
220
|
+
|
|
221
|
+
```jsx
|
|
222
|
+
<DataTb
|
|
223
|
+
directus={{
|
|
224
|
+
table: 'your_collection',
|
|
225
|
+
queryString: 'filter[status][_eq]=published',
|
|
226
|
+
}}
|
|
227
|
+
client:idle
|
|
228
|
+
/>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The `source={{ type: 'directus', config: { url, token }, collection }}` form shown above is still supported for cases where you need to pass an explicit URL/token instead of the env vars (e.g. multiple Directus instances on one site).
|
|
232
|
+
|
|
233
|
+
## Client Hydration
|
|
234
|
+
|
|
235
|
+
For optimal performance, use appropriate hydration strategies:
|
|
236
|
+
|
|
237
|
+
- `client:load` - For interactive tables (Directus, API, searchable)
|
|
238
|
+
- `client:idle` - For static data (CSV, JSON) - loads after initial page render
|
|
239
|
+
- `client:visible` - Load when table scrolls into view
|
|
240
|
+
|
|
241
|
+
```jsx
|
|
242
|
+
{/* Dynamic data - load immediately */}
|
|
243
|
+
<DataTb
|
|
244
|
+
source={{
|
|
245
|
+
type: 'directus',
|
|
246
|
+
collection: 'articles',
|
|
247
|
+
config: directusConfig
|
|
248
|
+
}}
|
|
249
|
+
searchable
|
|
250
|
+
pagination
|
|
251
|
+
client:load
|
|
252
|
+
/>
|
|
253
|
+
|
|
254
|
+
{/* Static data - load when idle */}
|
|
255
|
+
<DataTb
|
|
256
|
+
source={{ type: 'csv', url: '/data.csv' }}
|
|
257
|
+
client:idle
|
|
258
|
+
/>
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## Architecture
|
|
262
|
+
|
|
263
|
+
The DataTb component follows a simple, self-contained architecture:
|
|
264
|
+
|
|
265
|
+
- **DataTb.tsx** - Main React component using TanStack Table with built-in data fetching
|
|
266
|
+
- **DataTbMdx.tsx** - MDX-safe wrapper (memoizes props, resolves `csv`/`json`/`api`/`directus` shorthands) — this is what `@lad-sapienza/scms-core/components/DataTb`'s `DataTb` export actually is
|
|
267
|
+
- **types.ts** - TypeScript definitions for all source types
|
|
268
|
+
- **utils.ts** - Helper functions for column detection, formatting
|
|
269
|
+
- **sources/** - Standalone headless data-loaders (`CsvSource`, `JsonSource`, `DirectusSource`, `ApiSource`) for building a custom UI outside `DataTb` itself
|
|
270
|
+
|
|
271
|
+
Data fetching is handled internally based on the `source`/`csv`/`json`/`api`/`directus` prop, supporting CSV, JSON, Directus, and generic APIs.
|
|
272
|
+
|
|
273
|
+
## Examples
|
|
274
|
+
|
|
275
|
+
See the [DataTb docs page](../../../usr/content/docs/components/datatb.mdx) (rendered at `/docs/components/datatb` on a running site) for live, interactive examples of every prop and source type.
|