@gravitee/graphene-core 3.25.0 → 3.26.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/USAGE_GUIDE.md +60 -34
- package/dist/composed/DataTable/DataTable.d.ts +14 -1
- package/dist/composed/DataTable/DataTable.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/styles/globals.css +1 -1
- package/package.json +3 -1
- package/snippets/list-page/DeleteEntityDialog.tsx +71 -0
- package/snippets/list-page/EntitiesPage.tsx +221 -0
- package/snippets/list-page/EntityActionsMenu.tsx +69 -0
- package/snippets/list-page/entities.service.ts +108 -0
- package/snippets/list-page/entityColumns.tsx +79 -0
- package/snippets/list-page/snippet.json +6 -0
- package/snippets/list-page/useEntityList.ts +142 -0
- package/snippets/data-table-list-page.tsx +0 -316
package/USAGE_GUIDE.md
CHANGED
|
@@ -482,7 +482,7 @@ Follow these steps to apply the layout width system to an existing consumer modu
|
|
|
482
482
|
4. **Remove ad-hoc width constraints** — delete any `max-w-*` classes on page-level wrapper divs. The design system owns content width.
|
|
483
483
|
5. **Verify** — resize the browser to confirm content stays centered without clipping on narrow viewports.
|
|
484
484
|
|
|
485
|
-
See `node_modules/@gravitee/graphene-core/snippets/
|
|
485
|
+
See the folder `node_modules/@gravitee/graphene-core/snippets/list-page/` and `creation-wizard-page.tsx` for complete examples.
|
|
486
486
|
|
|
487
487
|
### Page header
|
|
488
488
|
|
|
@@ -606,7 +606,7 @@ export function ApisPage() {
|
|
|
606
606
|
|
|
607
607
|
Use `DataTable` for any entity list — whether the data is fetched page-by-page from an API or loaded in full on the client. The component provides sorting, filtering, pagination, column visibility, row selection, and bulk actions through a composable slot API.
|
|
608
608
|
|
|
609
|
-
**To implement:** copy the
|
|
609
|
+
**To implement:** copy the folder `node_modules/@gravitee/graphene-core/snippets/list-page/` into your module, rename `Entity` and `entities` after your resource, and point `useEntityList.ts` at your API client instead of the sample `entities.service.ts`. Its `snippet.json` lists the packages it needs beyond Graphene's peers (`@tanstack/react-query`, `react-router-dom`). See Storybook **Patterns/List Page** for the page in its five states.
|
|
610
610
|
|
|
611
611
|
#### When to use DataTable vs raw Table
|
|
612
612
|
|
|
@@ -656,11 +656,12 @@ Use `DataTable` for any entity list — whether the data is fetched page-by-page
|
|
|
656
656
|
|
|
657
657
|
#### Search and filters
|
|
658
658
|
|
|
659
|
-
- Search and filters live in the DataTable `toolbar` slot.
|
|
660
|
-
- Search: `SearchField`
|
|
661
|
-
- **Do NOT add a search field to a server-paginated table unless the backend endpoint supports a text search parameter** (`query`, `q`, or equivalent). Client-side search on a single page of server-paginated data is misleading
|
|
662
|
-
-
|
|
663
|
-
-
|
|
659
|
+
- Search and filters live in the DataTable `toolbar` slot, inside `<div className="flex min-w-0 flex-wrap items-center gap-2">`: DataTable's own toolbar row does not wrap, this container does. Group the filters and Reset in a nested `flex flex-wrap items-center gap-2` so that they wrap together.
|
|
660
|
+
- Search: `SearchField` first, with an `aria-label` that says what is searched, a placeholder, and `className="w-64"`. It debounces 300 ms and reports the settled query: reset the page to 1 in `onValueChange`.
|
|
661
|
+
- **Do NOT add a search field to a server-paginated table unless the backend endpoint supports a text search parameter** (`query`, `q`, or equivalent). Client-side search on a single page of server-paginated data is misleading: users will miss matches on other pages. If the backend does not support search, either request the backend change or omit the search field entirely.
|
|
662
|
+
- Single-choice filter: a `Select` whose default option is "All ..." ("All statuses"), named by a `Label` in `sr-only`, with a minimum width (`min-w-32`) so that it does not jump as its value changes.
|
|
663
|
+
- Multiple-choice filter: a `FacetedFilter`.
|
|
664
|
+
- Reset: a `Button variant="ghost" size="sm"`, rendered only while a criterion is active. It clears every criterion and the page, then moves the focus to the search field (pass a `ref` to `SearchField`).
|
|
664
665
|
|
|
665
666
|
#### Row navigation
|
|
666
667
|
|
|
@@ -675,6 +676,9 @@ Use **name-column-as-link**: the primary identifier column (always leftmost) is
|
|
|
675
676
|
- **Quick-action escape hatch:** one high-discoverability action (e.g. "Deploy") may appear as an inline icon *alongside* the dropdown, but the dropdown must still exist.
|
|
676
677
|
- Use `MoreVerticalIcon` (⋮) for table rows. Do not use `MoreHorizontalIcon` — the horizontal variant is for toolbars and card headers.
|
|
677
678
|
- Destructive actions go last in the menu, after a `DropdownMenuSeparator`.
|
|
679
|
+
- Name the ⋮ button after its row: ``aria-label={`Actions for ${entity.name}`}``.
|
|
680
|
+
- A destructive action asks for confirmation in a `Dialog` before it runs, then reports the outcome in a toast (`toast` from `@gravitee/graphene-core`, shown by the app's `Toaster`). When the dialog closes, give the focus back to the ⋮ if the user cancelled. Once the entity is deleted, its row and ⋮ go: move the focus to the search field, as Reset does. See `EntityActionsMenu.tsx` and `DeleteEntityDialog.tsx` in the list page snippet.
|
|
681
|
+
- Key the rows by entity (`tableOptions={{ getRowId: (entity) => entity.id }}`), so that a removed row never hands its focused ⋮ to the next entity. Not with `selectionMode="multi"` for now: `onSelectionChange` still maps the selection by row index.
|
|
678
682
|
|
|
679
683
|
#### Column ordering
|
|
680
684
|
|
|
@@ -688,7 +692,9 @@ Use **name-column-as-link**: the primary identifier column (always leftmost) is
|
|
|
688
692
|
|
|
689
693
|
**Minimum 3 data columns** (excluding actions). If an entity has fewer than 3 meaningful attributes, use a simpler component (Item list, card grid) instead of DataTable.
|
|
690
694
|
|
|
691
|
-
Target **4-7 visible columns**.
|
|
695
|
+
Target **4-7 visible columns**. List pages show the View menu (`enableColumnVisibility`), with the name and the actions always visible (`enableHiding: false`); more than 7 → hide the low-priority columns by default. Do NOT show raw entity IDs as visible columns.
|
|
696
|
+
|
|
697
|
+
`enableColumnVisibility` adds the View menu, which names each column by its `meta.label`, else by its header when that is text: give a column whose header is a component, such as a sortable `DataTableColumnHeader`, a `meta: { label: 'Created' }`, or the menu shows its id.
|
|
692
698
|
|
|
693
699
|
#### Cell renderers
|
|
694
700
|
|
|
@@ -704,56 +710,59 @@ Import from `@gravitee/graphene-core`:
|
|
|
704
710
|
|
|
705
711
|
#### Empty states
|
|
706
712
|
|
|
707
|
-
Use `DataTableEmptyState`
|
|
713
|
+
Use `DataTableEmptyState` for both cases, and decide only once the query has settled: never conclude "empty" from the previous page kept while the next one loads (`isPlaceholderData`).
|
|
708
714
|
|
|
709
|
-
**`variant="first-use"
|
|
710
|
-
- Render **instead of** `DataTable
|
|
711
|
-
-
|
|
712
|
-
-
|
|
713
|
-
- Decision: `totalCount === 0 && !
|
|
715
|
+
**`variant="first-use"`**: the collection is empty and no criterion is active.
|
|
716
|
+
- Render it **instead of** `DataTable`, wrapped in `<div className="rounded-lg border">`.
|
|
717
|
+
- In a list page: an icon, a title, one sentence, and **no button**. The header's Add button is the one creation CTA (see below).
|
|
718
|
+
- `primaryAction`, `secondaryAction` and `children` exist for other surfaces, such as an educational first-use landing.
|
|
719
|
+
- Decision: `totalCount === 0 && !hasCriteria`.
|
|
714
720
|
|
|
715
|
-
**`variant="no-results"
|
|
716
|
-
- Render **inside** `DataTable` as
|
|
717
|
-
-
|
|
718
|
-
-
|
|
721
|
+
**`variant="no-results"`**: a search or a filter matched nothing.
|
|
722
|
+
- Render it **inside** `DataTable` as `emptyMessage`, so the toolbar stays.
|
|
723
|
+
- `icon={<SearchIcon />}`, a title ("No APIs found"), one sentence inviting to adjust the search or the filters, and **no button**: the search field's clear button and the toolbar's Reset clear the criteria.
|
|
724
|
+
- Its `action` slot exists for the rare table without a Reset in its toolbar.
|
|
725
|
+
- Decision: `totalCount === 0 && hasCriteria`.
|
|
719
726
|
|
|
720
727
|
```tsx
|
|
721
|
-
// Page-level decision
|
|
728
|
+
// Page-level decision, once the query has settled
|
|
722
729
|
{isFirstUse ? (
|
|
723
730
|
<div className="rounded-lg border">
|
|
724
|
-
<DataTableEmptyState variant="first-use" icon={<GlobeIcon />} title="No APIs yet" ... />
|
|
731
|
+
<DataTableEmptyState variant="first-use" icon={<GlobeIcon />} title="No APIs yet" description="..." />
|
|
725
732
|
</div>
|
|
726
733
|
) : (
|
|
727
734
|
<DataTable
|
|
728
|
-
emptyMessage={<DataTableEmptyState variant="no-results" icon={<SearchIcon />} ... />}
|
|
735
|
+
emptyMessage={<DataTableEmptyState variant="no-results" icon={<SearchIcon />} title="No APIs found" description="..." />}
|
|
729
736
|
...
|
|
730
737
|
/>
|
|
731
738
|
)}
|
|
732
739
|
```
|
|
733
740
|
|
|
734
|
-
See Storybook **
|
|
741
|
+
See Storybook **Patterns/List Page** (states `empty` and `no-results`) for the full interactive example.
|
|
735
742
|
|
|
736
743
|
#### Header button coordination
|
|
737
744
|
|
|
738
|
-
|
|
745
|
+
The Add button stays in the header in every state: one creation CTA on screen.
|
|
739
746
|
|
|
740
|
-
| State | Header
|
|
747
|
+
| State | Header | Content area |
|
|
741
748
|
|---|---|---|
|
|
742
|
-
| **First
|
|
743
|
-
| **
|
|
744
|
-
| **No results** (
|
|
749
|
+
| **First use** (`totalCount === 0`, no criterion) | Add | `DataTableEmptyState variant="first-use"` instead of the table, without a button |
|
|
750
|
+
| **Data** (`totalCount > 0`) | Add | `DataTable` |
|
|
751
|
+
| **No results** (a criterion active, zero matches) | Add | `DataTable` with `DataTableEmptyState variant="no-results"` inside, without a button |
|
|
745
752
|
|
|
746
|
-
|
|
753
|
+
Gate the button on the permission, never on the list's state:
|
|
747
754
|
|
|
748
755
|
```tsx
|
|
749
756
|
<PageHeader
|
|
750
|
-
title="
|
|
751
|
-
description="
|
|
757
|
+
title="APIs"
|
|
758
|
+
description="Design, deploy and secure the APIs your gateways expose to consumers."
|
|
752
759
|
actions={
|
|
753
|
-
|
|
754
|
-
<Button
|
|
755
|
-
<
|
|
756
|
-
|
|
760
|
+
canCreate && (
|
|
761
|
+
<Button asChild>
|
|
762
|
+
<Link to="new">
|
|
763
|
+
<PlusIcon />
|
|
764
|
+
Add API
|
|
765
|
+
</Link>
|
|
757
766
|
</Button>
|
|
758
767
|
)
|
|
759
768
|
}
|
|
@@ -762,9 +771,26 @@ Both the header button and the empty state CTA trigger the same creation flow. N
|
|
|
762
771
|
|
|
763
772
|
#### Loading state
|
|
764
773
|
|
|
774
|
+
- Fetch with TanStack Query and `placeholderData: keepPreviousData`, and pass `loading={isFetching}`: the first load shows skeleton rows, and later fetches keep the rows under DataTable's overlay. There is no code to write for either.
|
|
765
775
|
- Pass `skeletonCount={pageSize}` to prevent layout shift.
|
|
766
776
|
- `loadingDelay={200}` (default) prevents flash on fast responses.
|
|
767
777
|
|
|
778
|
+
#### Error state
|
|
779
|
+
|
|
780
|
+
- A destructive `Alert` above the table: a short title ("Could not load APIs") and a Retry button that calls `refetch()`, disabled while it runs.
|
|
781
|
+
- Keep the last loaded page on screen under the alert, with the toolbar and the pagination, and say that it may be out of date. `keepPreviousData` holds the previous page only while the next one loads, so keep the last successful response in your list hook: the search field must never disappear while the user types.
|
|
782
|
+
- When nothing has loaded yet, the alert alone, with the error message.
|
|
783
|
+
|
|
784
|
+
#### List state in the URL
|
|
785
|
+
|
|
786
|
+
Keep the list state in the URL with `useSearchParams` (react-router-dom), so that a reload, a shared link and the Back button restore it:
|
|
787
|
+
|
|
788
|
+
- Keys: `q` for the search, one key per filter (`status`), repeated for a multiple choice (`type=internal&type=partner`), `page`, `size`, and `sort` as `field:asc` or `field:desc`.
|
|
789
|
+
- Defaults are not written, and every value is read with a fallback: an unknown status, size or sort gives the default.
|
|
790
|
+
- A new criterion (search, filter, sort) deletes `page` and replaces the history entry; changing the page or its size adds one.
|
|
791
|
+
- Give the search back to `SearchField` exactly as it reported it, and trim it where it is used (the request).
|
|
792
|
+
- A page past the last one (its last row deleted, an old link) goes to the last page.
|
|
793
|
+
|
|
768
794
|
#### `serverSide` prop
|
|
769
795
|
|
|
770
796
|
Set `serverSide` when data is fetched page-by-page from an API. It disables TanStack's built-in sort/filter/pagination models so you control everything via your fetch logic.
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import { ColumnDef, OnChangeFn, SortingState, TableOptions } from '@tanstack/react-table';
|
|
1
|
+
import { ColumnDef, OnChangeFn, RowData, SortingState, TableOptions } from '@tanstack/react-table';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
+
declare module '@tanstack/react-table' {
|
|
4
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
5
|
+
/**
|
|
6
|
+
* The column's name in the View menu (`enableColumnVisibility`), for a header that is not text,
|
|
7
|
+
* such as a sortable `DataTableColumnHeader`. Without it, the menu shows the column id.
|
|
8
|
+
*/
|
|
9
|
+
label?: string;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
3
12
|
interface DataTablePaginationConfig {
|
|
4
13
|
readonly page: number;
|
|
5
14
|
readonly pageSize: number;
|
|
@@ -21,6 +30,10 @@ interface DataTableProps<TData> {
|
|
|
21
30
|
/** Called when the user clicks a sortable column header. Use to trigger a server re-fetch with the new sort params. */
|
|
22
31
|
readonly onSortingChange?: OnChangeFn<SortingState>;
|
|
23
32
|
readonly emptyMessage?: ReactNode;
|
|
33
|
+
/**
|
|
34
|
+
* Adds the View menu, which shows and hides the columns whose `enableHiding` is not false. It names a
|
|
35
|
+
* column by its `meta.label`, else by its header when that is text, else by its id.
|
|
36
|
+
*/
|
|
24
37
|
readonly enableColumnVisibility?: boolean;
|
|
25
38
|
readonly enableColumnResizing?: boolean;
|
|
26
39
|
/** Makes the table header sticky. Consumer should set a max-height via className. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../src/composed/DataTable/DataTable.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,SAAS,EAMd,KAAK,UAAU,
|
|
1
|
+
{"version":3,"file":"DataTable.d.ts","sourceRoot":"","sources":["../../../src/composed/DataTable/DataTable.tsx"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -26965,7 +26965,7 @@ function uK({ table: e }) {
|
|
|
26965
26965
|
checked: e.getIsVisible(),
|
|
26966
26966
|
onCheckedChange: (t) => e.toggleVisibility(!!t),
|
|
26967
26967
|
className: "text-sm",
|
|
26968
|
-
children: typeof e.columnDef.header == "string" ? e.columnDef.header : e.id
|
|
26968
|
+
children: e.columnDef.meta?.label ?? (typeof e.columnDef.header == "string" ? e.columnDef.header : e.id)
|
|
26969
26969
|
}, e.id))
|
|
26970
26970
|
]
|
|
26971
26971
|
})] });
|