@gravitee/graphene-core 2.41.0 → 2.42.0-fix-improve-data-table.e563914

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.
Files changed (29) hide show
  1. package/USAGE_GUIDE.md +160 -0
  2. package/dist/__stories__/fixtures/index.d.ts +1 -1
  3. package/dist/__stories__/fixtures/index.d.ts.map +1 -1
  4. package/dist/__stories__/fixtures/json-schema-form/disableIf.fixture.d.ts +3 -0
  5. package/dist/__stories__/fixtures/json-schema-form/disableIf.fixture.d.ts.map +1 -0
  6. package/dist/__stories__/fixtures/json-schema-form/index.d.ts +1 -0
  7. package/dist/__stories__/fixtures/json-schema-form/index.d.ts.map +1 -1
  8. package/dist/composed/DataTable/DataTable.d.ts +33 -4
  9. package/dist/composed/DataTable/DataTable.d.ts.map +1 -1
  10. package/dist/composed/DataTable/cells.d.ts +38 -0
  11. package/dist/composed/DataTable/cells.d.ts.map +1 -0
  12. package/dist/composed/DataTable/index.d.ts +3 -1
  13. package/dist/composed/DataTable/index.d.ts.map +1 -1
  14. package/dist/composed/DataTableEmptyState/DataTableEmptyState.d.ts +33 -0
  15. package/dist/composed/DataTableEmptyState/DataTableEmptyState.d.ts.map +1 -0
  16. package/dist/composed/DataTableEmptyState/index.d.ts +3 -0
  17. package/dist/composed/DataTableEmptyState/index.d.ts.map +1 -0
  18. package/dist/composed/JsonSchemaForm/SchemaField.d.ts.map +1 -1
  19. package/dist/composed/JsonSchemaForm/conditional/DisabledGate.d.ts +15 -0
  20. package/dist/composed/JsonSchemaForm/conditional/DisabledGate.d.ts.map +1 -0
  21. package/dist/composed/JsonSchemaForm/schema/buildIndex.d.ts.map +1 -1
  22. package/dist/composed/JsonSchemaForm/schema/types.d.ts +4 -0
  23. package/dist/composed/JsonSchemaForm/schema/types.d.ts.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1184 -928
  27. package/dist/styles/globals.css +1 -1
  28. package/package.json +2 -1
  29. package/snippets/data-table-list-page.tsx +316 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravitee/graphene-core",
3
- "version": "2.41.0",
3
+ "version": "2.42.0-fix-improve-data-table.e563914",
4
4
  "type": "module",
5
5
  "description": "Gravitee's Graphene design system: accessible React components, design tokens, icons, and shared ESLint/TypeScript configs. Built on shadcn/ui and Tailwind.",
6
6
  "keywords": [
@@ -91,6 +91,7 @@
91
91
  },
92
92
  "peerDependencies": {
93
93
  "@fontsource/dm-sans": ">=5.0.0",
94
+ "@gravitee/graphene-core": "2.42.0-fix-improve-data-table.e563914",
94
95
  "@tanstack/react-table": "^8.0.0",
95
96
  "@testing-library/dom": ">=10.0.0",
96
97
  "@testing-library/react": ">=16.0.0",
@@ -0,0 +1,316 @@
1
+ // Snippet: Entity List page with DataTable
2
+ //
3
+ // Use when building a list page for server-paginated entity data (APIs, Applications,
4
+ // Policies, Agents, Plans, etc.) — any CRUD list where users manage entities.
5
+ //
6
+ // Do NOT use for:
7
+ // - Static key-value detail panels (use raw Table)
8
+ // - Time-series/triage explorers (logs, traces) — see Record Explorer pattern (deferred)
9
+ // - Tables with fewer than 5 fixed rows
10
+ //
11
+ // Empty state pattern (page-level decision):
12
+ // - totalCount === 0 && !hasFilters → DataTableEmptyState variant="first-use" (INSTEAD of DataTable)
13
+ // - filteredCount === 0 && hasFilters → DataTableEmptyState variant="no-results" (INSIDE DataTable)
14
+ //
15
+ // Header button coordination:
16
+ // - First-use → header hides "Add" button; empty state owns the primary CTA
17
+ // - Has data → header shows "Add" button (primary); table is main content
18
+ // - Both buttons trigger the same creation flow
19
+ //
20
+ // See Storybook "Composed/DataTableEmptyState → Integration" for interactive example.
21
+ //
22
+ // Column ordering (left to right):
23
+ // 1. Name (link, always first, font-medium hover:underline)
24
+ // 2. Status badge (lifecycle state)
25
+ // 3. Category badges (type, version, security)
26
+ // 4. Dates (relative format + full tooltip)
27
+ // 5. Owner/actor (truncated text)
28
+ // 6. Actions (always last, sr-only header)
29
+ //
30
+ // Target 4-7 visible columns. More than 7 → enable column visibility.
31
+ // Do NOT show raw entity IDs as visible columns.
32
+ //
33
+ // See Storybook "Patterns/Data Table → ApiList" for interactive example.
34
+
35
+ import type { ColumnDef, SortingState } from '@tanstack/react-table';
36
+ import { useEffect, useReducer, useRef, useState } from 'react';
37
+ import {
38
+ Badge,
39
+ Button,
40
+ DataTable,
41
+ DataTableColumnHeader,
42
+ DataTableEmptyState,
43
+ DropdownMenu,
44
+ DropdownMenuContent,
45
+ DropdownMenuItem,
46
+ DropdownMenuSeparator,
47
+ DropdownMenuTrigger,
48
+ FacetedFilter,
49
+ Input,
50
+ } from '@gravitee/graphene-core';
51
+ import { DateCell } from '@gravitee/graphene-core/composed/DataTable';
52
+ import {
53
+ GlobeIcon,
54
+ MoreVerticalIcon,
55
+ PencilIcon,
56
+ PlusIcon,
57
+ SearchIcon,
58
+ Trash2Icon,
59
+ } from '@gravitee/graphene-core/icons';
60
+
61
+ // Replace with your entity type
62
+ interface Entity {
63
+ id: string;
64
+ name: string;
65
+ status: 'active' | 'inactive' | 'draft';
66
+ createdAt: string;
67
+ owner: string;
68
+ }
69
+
70
+ // Replace with your API fetch function
71
+ declare function fetchEntities(params: {
72
+ page: number;
73
+ perPage: number;
74
+ sortBy?: string;
75
+ sortOrder?: 'asc' | 'desc';
76
+ search?: string;
77
+ status?: string[];
78
+ }): Promise<{ data: Entity[]; totalCount: number }>;
79
+
80
+ const STATUS_VARIANTS = {
81
+ active: 'success',
82
+ inactive: 'secondary',
83
+ draft: 'outline',
84
+ } as const;
85
+
86
+ const statusOptions = [
87
+ { value: 'active', label: 'Active' },
88
+ { value: 'inactive', label: 'Inactive' },
89
+ { value: 'draft', label: 'Draft' },
90
+ ];
91
+
92
+ const columns: ColumnDef<Entity, unknown>[] = [
93
+ {
94
+ accessorKey: 'name',
95
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
96
+ cell: ({ row }) => (
97
+ <a href={`#/entities/${row.original.id}`} className="font-medium hover:underline">
98
+ {row.original.name}
99
+ </a>
100
+ ),
101
+ },
102
+ {
103
+ accessorKey: 'status',
104
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
105
+ cell: ({ row }) => <Badge variant={STATUS_VARIANTS[row.original.status]}>{row.original.status}</Badge>,
106
+ },
107
+ {
108
+ accessorKey: 'createdAt',
109
+ header: ({ column }) => <DataTableColumnHeader column={column} title="Created" />,
110
+ cell: ({ row }) => <DateCell value={row.original.createdAt} />,
111
+ },
112
+ {
113
+ accessorKey: 'owner',
114
+ header: 'Owner',
115
+ enableSorting: false,
116
+ cell: ({ row }) => <span className="max-w-[150px] truncate text-muted-foreground">{row.original.owner}</span>,
117
+ },
118
+ {
119
+ id: 'actions',
120
+ header: () => <span className="sr-only">Actions</span>,
121
+ enableSorting: false,
122
+ enableHiding: false,
123
+ cell: () => (
124
+ <div className="flex justify-end">
125
+ <DropdownMenu>
126
+ <DropdownMenuTrigger asChild>
127
+ <Button variant="ghost" size="icon-xs">
128
+ <MoreVerticalIcon className="size-4" />
129
+ </Button>
130
+ </DropdownMenuTrigger>
131
+ <DropdownMenuContent align="end">
132
+ <DropdownMenuItem>
133
+ <PencilIcon className="size-3.5" />
134
+ Edit
135
+ </DropdownMenuItem>
136
+ <DropdownMenuSeparator />
137
+ <DropdownMenuItem className="text-destructive">
138
+ <Trash2Icon className="size-3.5" />
139
+ Delete
140
+ </DropdownMenuItem>
141
+ </DropdownMenuContent>
142
+ </DropdownMenu>
143
+ </div>
144
+ ),
145
+ },
146
+ ];
147
+
148
+ interface FetchState {
149
+ data: Entity[];
150
+ totalCount: number;
151
+ loading: boolean;
152
+ }
153
+
154
+ type FetchAction = { type: 'loading' } | { type: 'success'; data: Entity[]; totalCount: number };
155
+
156
+ function fetchReducer(state: FetchState, action: FetchAction): FetchState {
157
+ if (action.type === 'loading') return { ...state, loading: true };
158
+ return { data: action.data, totalCount: action.totalCount, loading: false };
159
+ }
160
+
161
+ export function EntityListPage() {
162
+ const [page, setPage] = useState(1);
163
+ const [pageSize, setPageSize] = useState(10);
164
+ const [search, setSearch] = useState('');
165
+ const [debouncedSearch, setDebouncedSearch] = useState('');
166
+ const [sorting, setSorting] = useState<SortingState>([]);
167
+ const [statusFilter, setStatusFilter] = useState<string[]>([]);
168
+ const [state, dispatch] = useReducer(fetchReducer, { data: [], totalCount: 0, loading: true });
169
+
170
+ const debounceRef = useRef<ReturnType<typeof setTimeout>>();
171
+ useEffect(() => {
172
+ debounceRef.current = setTimeout(() => {
173
+ setDebouncedSearch(search);
174
+ setPage(1);
175
+ }, 300);
176
+ return () => clearTimeout(debounceRef.current);
177
+ }, [search]);
178
+
179
+ useEffect(() => {
180
+ let active = true;
181
+ dispatch({ type: 'loading' });
182
+ fetchEntities({
183
+ page,
184
+ perPage: pageSize,
185
+ sortBy: sorting[0]?.id,
186
+ sortOrder: sorting[0]?.desc ? 'desc' : 'asc',
187
+ search: debouncedSearch || undefined,
188
+ status: statusFilter.length ? statusFilter : undefined,
189
+ }).then((res) => {
190
+ if (active) dispatch({ type: 'success', data: res.data, totalCount: res.totalCount });
191
+ });
192
+ return () => {
193
+ active = false;
194
+ };
195
+ }, [page, pageSize, sorting, debouncedSearch, statusFilter]);
196
+
197
+ const handleSortingChange = (updater: SortingState | ((prev: SortingState) => SortingState)) => {
198
+ setSorting(typeof updater === 'function' ? updater(sorting) : updater);
199
+ setPage(1);
200
+ };
201
+
202
+ const handleStatusChange = (values: string[]) => {
203
+ setStatusFilter(values);
204
+ setPage(1);
205
+ };
206
+
207
+ const { data, totalCount, loading } = state;
208
+ const hasFilters = search || statusFilter.length > 0;
209
+ const isFirstUse = totalCount === 0 && !hasFilters && !loading;
210
+
211
+ return (
212
+ <div className="space-y-4">
213
+ <div className="flex items-center justify-between">
214
+ <div>
215
+ <h2 className="text-lg font-semibold">Entities</h2>
216
+ <p className="text-sm text-muted-foreground">Manage your entity catalog.</p>
217
+ </div>
218
+ {!isFirstUse && (
219
+ <Button size="sm">
220
+ <PlusIcon />
221
+ Add entity
222
+ </Button>
223
+ )}
224
+ </div>
225
+
226
+ {isFirstUse ? (
227
+ <div className="rounded-lg border">
228
+ <DataTableEmptyState
229
+ variant="first-use"
230
+ icon={<GlobeIcon />}
231
+ title="No entities yet"
232
+ description="Get started by creating your first entity."
233
+ primaryAction={
234
+ <Button size="sm">
235
+ <PlusIcon />
236
+ Create entity
237
+ </Button>
238
+ }
239
+ />
240
+ </div>
241
+ ) : (
242
+ <DataTable
243
+ columns={columns}
244
+ data={data}
245
+ loading={loading}
246
+ skeletonCount={pageSize}
247
+ sorting={sorting}
248
+ onSortingChange={handleSortingChange}
249
+ enableColumnVisibility
250
+ serverSide
251
+ pagination={{
252
+ page,
253
+ pageSize,
254
+ totalCount,
255
+ pageSizeOptions: [10, 25, 50, 100],
256
+ onPageChange: setPage,
257
+ onPageSizeChange: (size) => {
258
+ setPageSize(size);
259
+ setPage(1);
260
+ },
261
+ }}
262
+ emptyMessage={
263
+ <DataTableEmptyState
264
+ variant="no-results"
265
+ icon={<SearchIcon />}
266
+ title="No entities match your search"
267
+ description="Try adjusting your search terms or clearing filters."
268
+ action={
269
+ <Button
270
+ size="sm"
271
+ variant="outline"
272
+ onClick={() => {
273
+ setSearch('');
274
+ setStatusFilter([]);
275
+ setPage(1);
276
+ }}
277
+ >
278
+ Clear filters
279
+ </Button>
280
+ }
281
+ />
282
+ }
283
+ toolbar={
284
+ <>
285
+ <Input
286
+ placeholder="Search..."
287
+ value={search}
288
+ onChange={(e) => setSearch(e.target.value)}
289
+ className="h-8 w-64"
290
+ />
291
+ <FacetedFilter
292
+ title="Status"
293
+ options={statusOptions}
294
+ selected={statusFilter}
295
+ onChange={handleStatusChange}
296
+ />
297
+ {hasFilters && (
298
+ <Button
299
+ variant="ghost"
300
+ size="sm"
301
+ onClick={() => {
302
+ setSearch('');
303
+ setStatusFilter([]);
304
+ setPage(1);
305
+ }}
306
+ >
307
+ Reset
308
+ </Button>
309
+ )}
310
+ </>
311
+ }
312
+ />
313
+ )}
314
+ </div>
315
+ );
316
+ }