@12-apps/ui 6.1.0 → 6.3.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/components-catalog.md +8 -0
- package/dist/{chunk-35VGGLOC.js → chunk-CEKPLUQV.js} +2 -2
- package/dist/{chunk-35VGGLOC.js.map → chunk-CEKPLUQV.js.map} +1 -1
- package/dist/{chunk-62XQWGXU.js → chunk-MXPXKORR.js} +19 -17
- package/dist/chunk-MXPXKORR.js.map +1 -0
- package/dist/{chunk-7XLEWBKL.js → chunk-N554AFDB.js} +7 -10
- package/dist/chunk-N554AFDB.js.map +1 -0
- package/dist/{chunk-JDO7W4D4.js → chunk-R5ZQZSEQ.js} +2 -20
- package/dist/{chunk-JDO7W4D4.js.map → chunk-R5ZQZSEQ.js.map} +1 -1
- package/dist/{chunk-R4TYXKDD.js → chunk-TPQRIPCN.js} +52 -48
- package/dist/chunk-TPQRIPCN.js.map +1 -0
- package/dist/copy.js +1 -0
- package/dist/copy.js.map +1 -0
- package/dist/data-display/CardKit.js +277 -0
- package/dist/data-display/CardKit.js.map +1 -0
- package/dist/data-display/DataViews.js +664 -616
- package/dist/data-display/DataViews.js.map +1 -1
- package/dist/data-display/SectionOnboarding.js +3 -3
- package/dist/data-display/SectionOnboarding.js.map +1 -1
- package/dist/feedback/ConfirmAction.js +3 -5
- package/dist/form/CategorySelect.js +1 -1
- package/dist/form/CepField.js +1 -1
- package/dist/form/total-form.js +8 -4
- package/dist/form/total-form.js.map +1 -1
- package/dist/layout/ContentToolbar.js +1 -1
- package/dist/layout/TableFilter.js +1 -1
- package/dist/pt-BR.js +222 -0
- package/dist/pt-BR.js.map +1 -0
- package/dist/types/components/data-display/CardKit/CardKebab.d.ts +23 -0
- package/dist/types/components/data-display/CardKit/CardKebab.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/CardKit.types.d.ts +55 -0
- package/dist/types/components/data-display/CardKit/CardKit.types.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/card-actions-context.d.ts +51 -0
- package/dist/types/components/data-display/CardKit/card-actions-context.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/index.d.ts +15 -0
- package/dist/types/components/data-display/CardKit/index.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/list-card-parts.d.ts +60 -0
- package/dist/types/components/data-display/CardKit/list-card-parts.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/row-actions-to-menu.d.ts +17 -0
- package/dist/types/components/data-display/CardKit/row-actions-to-menu.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/use-remove-confirm.d.ts +65 -0
- package/dist/types/components/data-display/CardKit/use-remove-confirm.d.ts.map +1 -0
- package/dist/types/components/data-display/CardKit/use-row-confirm.d.ts +60 -0
- package/dist/types/components/data-display/CardKit/use-row-confirm.d.ts.map +1 -0
- package/dist/types/components/data-display/DataViews/DataViewsBoard.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/DeleteViewDialog.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/ManageViewsDialog.d.ts +4 -1
- package/dist/types/components/data-display/DataViews/ManageViewsDialog.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/SaveViewModal.d.ts +0 -5
- package/dist/types/components/data-display/DataViews/SaveViewModal.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/base-card.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/base-list-card-gutters.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-bar-controls.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-category-pill.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-columns-tab.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-copy-context.d.ts +17 -0
- package/dist/types/components/data-display/DataViews/data-views-copy-context.d.ts.map +1 -0
- package/dist/types/components/data-display/DataViews/data-views-copy.d.ts +205 -0
- package/dist/types/components/data-display/DataViews/data-views-copy.d.ts.map +1 -0
- package/dist/types/components/data-display/DataViews/data-views-display-panel.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-display-tabs.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-export.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-filter-panel.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-grid-helpers.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-more-fields.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-more-filters.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-range-pill.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-range-presets.d.ts +3 -2
- package/dist/types/components/data-display/DataViews/data-views-range-presets.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-scopes.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-search.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-select-all-strip.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-table-parts.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-view-nav.d.ts +1 -1
- package/dist/types/components/data-display/DataViews/data-views-view-nav.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/index.d.ts +2 -0
- package/dist/types/components/data-display/DataViews/index.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/save-view-preview.d.ts +10 -0
- package/dist/types/components/data-display/DataViews/save-view-preview.d.ts.map +1 -0
- package/dist/types/components/data-display/DataViews/use-saved-views-controller.d.ts.map +1 -1
- package/dist/types/components/data-display/SectionOnboarding/SectionOnboarding.types.d.ts +6 -5
- package/dist/types/components/data-display/SectionOnboarding/SectionOnboarding.types.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.hooks.d.ts +1 -3
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.hooks.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.parts.d.ts +4 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.parts.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.types.d.ts +9 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmAction.types.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmButton.d.ts +2 -1
- package/dist/types/components/feedback/ConfirmAction/ConfirmButton.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/index.d.ts +2 -2
- package/dist/types/components/feedback/ConfirmAction/index.d.ts.map +1 -1
- package/dist/types/components/feedback/ConfirmAction/with-confirmation.d.ts +5 -1
- package/dist/types/components/feedback/ConfirmAction/with-confirmation.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryPanel.d.ts +4 -1
- package/dist/types/components/form/CategorySelect/CategoryPanel.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryPanelFoot.d.ts +5 -2
- package/dist/types/components/form/CategorySelect/CategoryPanelFoot.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryPanelHead.d.ts +5 -2
- package/dist/types/components/form/CategorySelect/CategoryPanelHead.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts +3 -0
- package/dist/types/components/form/CategorySelect/CategoryPanelList.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategorySelect.types.d.ts +5 -0
- package/dist/types/components/form/CategorySelect/CategorySelect.types.d.ts.map +1 -1
- package/dist/types/components/form/CategorySelect/CategoryTrigger.d.ts +4 -1
- package/dist/types/components/form/CategorySelect/CategoryTrigger.d.ts.map +1 -1
- package/dist/types/components/form/CepField/CepField.d.ts +1 -1
- package/dist/types/components/form/CepField/CepField.d.ts.map +1 -1
- package/dist/types/components/form/CepField/CepField.types.d.ts +3 -0
- package/dist/types/components/form/CepField/CepField.types.d.ts.map +1 -1
- package/dist/types/components/form/total-form/Fields/CategoryField.d.ts +4 -1
- package/dist/types/components/form/total-form/Fields/CategoryField.d.ts.map +1 -1
- package/dist/types/components/form/total-form/Fields/CepField.d.ts +4 -1
- package/dist/types/components/form/total-form/Fields/CepField.d.ts.map +1 -1
- package/dist/types/components/layout/ContentToolbar/ContentToolbar.types.d.ts +6 -1
- package/dist/types/components/layout/ContentToolbar/ContentToolbar.types.d.ts.map +1 -1
- package/dist/types/components/layout/ContentToolbar/SavedViewsMenu.d.ts.map +1 -1
- package/dist/types/copy.d.ts +99 -0
- package/dist/types/copy.d.ts.map +1 -0
- package/dist/types/pt-BR.d.ts +28 -0
- package/dist/types/pt-BR.d.ts.map +1 -0
- package/dist/types/utils/export-rows.d.ts +31 -0
- package/dist/types/utils/export-rows.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/utils.js +50 -0
- package/dist/utils.js.map +1 -1
- package/package.json +14 -2
- package/src/components/data-display/CardKit/CardKebab.tsx +43 -0
- package/src/components/data-display/CardKit/CardKit.md +87 -0
- package/src/components/data-display/CardKit/CardKit.types.ts +60 -0
- package/src/components/data-display/CardKit/card-actions-context.tsx +98 -0
- package/src/components/data-display/CardKit/index.ts +19 -0
- package/src/components/data-display/CardKit/list-card-parts.tsx +163 -0
- package/src/components/data-display/CardKit/row-actions-to-menu.ts +29 -0
- package/src/components/data-display/CardKit/use-remove-confirm.tsx +111 -0
- package/src/components/data-display/CardKit/use-row-confirm.tsx +96 -0
- package/src/components/data-display/DataViews/DataViewsBoard.tsx +7 -4
- package/src/components/data-display/DataViews/DeleteViewDialog.tsx +6 -4
- package/src/components/data-display/DataViews/ManageViewsDialog.tsx +26 -10
- package/src/components/data-display/DataViews/SaveViewModal.tsx +56 -90
- package/src/components/data-display/DataViews/base-card.tsx +3 -1
- package/src/components/data-display/DataViews/base-list-card-gutters.tsx +3 -1
- package/src/components/data-display/DataViews/data-views-bar-controls.tsx +8 -5
- package/src/components/data-display/DataViews/data-views-category-pill.tsx +6 -3
- package/src/components/data-display/DataViews/data-views-columns-tab.tsx +5 -3
- package/src/components/data-display/DataViews/data-views-copy-context.tsx +43 -0
- package/src/components/data-display/DataViews/data-views-copy.ts +214 -0
- package/src/components/data-display/DataViews/data-views-display-panel.tsx +15 -9
- package/src/components/data-display/DataViews/data-views-display-tabs.tsx +19 -29
- package/src/components/data-display/DataViews/data-views-export.tsx +9 -10
- package/src/components/data-display/DataViews/data-views-filter-panel.tsx +9 -5
- package/src/components/data-display/DataViews/data-views-grid-helpers.tsx +5 -2
- package/src/components/data-display/DataViews/data-views-more-fields.tsx +5 -3
- package/src/components/data-display/DataViews/data-views-more-filters.tsx +6 -3
- package/src/components/data-display/DataViews/data-views-range-pill.tsx +9 -5
- package/src/components/data-display/DataViews/data-views-range-presets.ts +7 -8
- package/src/components/data-display/DataViews/data-views-scopes.tsx +3 -1
- package/src/components/data-display/DataViews/data-views-search.tsx +7 -4
- package/src/components/data-display/DataViews/data-views-select-all-strip.tsx +5 -3
- package/src/components/data-display/DataViews/data-views-table-parts.tsx +7 -2
- package/src/components/data-display/DataViews/data-views-view-nav.tsx +22 -16
- package/src/components/data-display/DataViews/index.ts +20 -0
- package/src/components/data-display/DataViews/save-view-preview.tsx +78 -0
- package/src/components/data-display/DataViews/use-saved-views-controller.ts +3 -1
- package/src/components/data-display/SectionOnboarding/SectionOnboarding.tsx +3 -3
- package/src/components/data-display/SectionOnboarding/SectionOnboarding.types.ts +8 -7
- package/src/components/feedback/ConfirmAction/ConfirmAction.hooks.ts +2 -2
- package/src/components/feedback/ConfirmAction/ConfirmAction.parts.tsx +12 -5
- package/src/components/feedback/ConfirmAction/ConfirmAction.types.ts +9 -1
- package/src/components/feedback/ConfirmAction/ConfirmButton.tsx +12 -4
- package/src/components/feedback/ConfirmAction/index.ts +2 -2
- package/src/components/feedback/ConfirmAction/with-confirmation.tsx +4 -3
- package/src/components/form/CategorySelect/CategoryPanel.tsx +18 -6
- package/src/components/form/CategorySelect/CategoryPanelFoot.tsx +10 -5
- package/src/components/form/CategorySelect/CategoryPanelHead.tsx +8 -3
- package/src/components/form/CategorySelect/CategoryPanelList.tsx +17 -10
- package/src/components/form/CategorySelect/CategorySelect.tsx +3 -3
- package/src/components/form/CategorySelect/CategorySelect.types.ts +5 -0
- package/src/components/form/CategorySelect/CategoryTrigger.tsx +5 -1
- package/src/components/form/CepField/CepField.tsx +6 -9
- package/src/components/form/CepField/CepField.types.ts +3 -0
- package/src/components/form/total-form/Fields/CategoryField.tsx +5 -0
- package/src/components/form/total-form/Fields/CepField.tsx +5 -0
- package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +6 -1
- package/src/components/layout/ContentToolbar/SavedViewsMenu.tsx +1 -21
- package/src/components/layout/TableFilter/TableFilter.tsx +1 -1
- package/src/copy.ts +103 -0
- package/src/pt-BR.ts +247 -0
- package/src/utils/export-rows.ts +95 -0
- package/src/utils/index.ts +1 -0
- package/dist/chunk-62XQWGXU.js.map +0 -1
- package/dist/chunk-7XLEWBKL.js.map +0 -1
- package/dist/chunk-R4TYXKDD.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/ui",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"packageManager": "pnpm@
|
|
5
|
+
"packageManager": "pnpm@10.34.5",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
8
|
"node": ">=18.0.0",
|
|
@@ -532,6 +532,18 @@
|
|
|
532
532
|
"./utility/WorkflowStep": {
|
|
533
533
|
"types": "./dist/types/components/utility/WorkflowStep/index.d.ts",
|
|
534
534
|
"default": "./dist/utility/WorkflowStep.js"
|
|
535
|
+
},
|
|
536
|
+
"./data-display/CardKit": {
|
|
537
|
+
"types": "./dist/types/components/data-display/CardKit/index.d.ts",
|
|
538
|
+
"default": "./dist/data-display/CardKit.js"
|
|
539
|
+
},
|
|
540
|
+
"./copy": {
|
|
541
|
+
"types": "./dist/types/copy.d.ts",
|
|
542
|
+
"default": "./dist/copy.js"
|
|
543
|
+
},
|
|
544
|
+
"./pt-BR": {
|
|
545
|
+
"types": "./dist/types/pt-BR.d.ts",
|
|
546
|
+
"default": "./dist/pt-BR.js"
|
|
535
547
|
}
|
|
536
548
|
},
|
|
537
549
|
"typesVersions": {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { JSX } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Button } from '../../form/Button';
|
|
6
|
+
import { DropdownMenu, type DropdownMenuItem } from '../../navigation/DropdownMenu';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The "⋮" trigger every row and card menu opens from.
|
|
10
|
+
*
|
|
11
|
+
* One component rather than a `DropdownMenu` per entity, so the kebab looks and
|
|
12
|
+
* behaves identically in the card, in the list row and in the table row. The
|
|
13
|
+
* entity's menu owns its own popups; this is the trigger and the item list.
|
|
14
|
+
*/
|
|
15
|
+
export function CardKebab({
|
|
16
|
+
items,
|
|
17
|
+
menuLabel,
|
|
18
|
+
dataTestId,
|
|
19
|
+
}: {
|
|
20
|
+
items: DropdownMenuItem[];
|
|
21
|
+
/**
|
|
22
|
+
* The trigger's accessible name — required, and with no default.
|
|
23
|
+
*
|
|
24
|
+
* It is the only text this component renders, and a screen reader announces
|
|
25
|
+
* it verbatim. A default here would be a word in the origin's language that
|
|
26
|
+
* reads as finished to the next adopter right up until somebody using a
|
|
27
|
+
* screen reader meets it.
|
|
28
|
+
*/
|
|
29
|
+
menuLabel: string;
|
|
30
|
+
dataTestId?: string;
|
|
31
|
+
}): JSX.Element {
|
|
32
|
+
return (
|
|
33
|
+
<DropdownMenu
|
|
34
|
+
size="sm"
|
|
35
|
+
items={items}
|
|
36
|
+
trigger={
|
|
37
|
+
<Button variant="text" size="sm" aria-label={menuLabel} dataTestId={dataTestId}>
|
|
38
|
+
⋮
|
|
39
|
+
</Button>
|
|
40
|
+
}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# CardKit
|
|
2
|
+
|
|
3
|
+
The furniture an admin list's cards and row menus sit in.
|
|
4
|
+
|
|
5
|
+
`BaseCard` and `BaseListCard` deliberately say nothing about what goes inside
|
|
6
|
+
them: the envelope is the library's and the design is the consumer's. That is
|
|
7
|
+
right for a card component and exactly the wrong place for every entity in an
|
|
8
|
+
admin app to invent its own kebab, its own two-column body, its own label/value
|
|
9
|
+
pair and its own confirm-before-deleting. Those are not per-entity decisions —
|
|
10
|
+
they are the same shape repeated — so they live here once.
|
|
11
|
+
|
|
12
|
+
**The line this kit does not cross** is the entity card itself. A "product card"
|
|
13
|
+
or a "discount card" knows a domain, and a domain is the consumer's.
|
|
14
|
+
|
|
15
|
+
## What is in it
|
|
16
|
+
|
|
17
|
+
| Export | What it is |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `CardKebab` | The `⋮` trigger every row and card menu opens from |
|
|
20
|
+
| `rowActionsToMenuItems` | One `RowAction[]` → the kebab's items, so a grid and a card cannot drift |
|
|
21
|
+
| `CardActionsProvider` / `useCardActions` | The tenant, the refresh and one shared error snackbar |
|
|
22
|
+
| `DetailColumns` `Fact` `Ledger` `BodyHeading` `TagList` | The expanded list-card body's repeated shapes |
|
|
23
|
+
| `useRowConfirm` | Confirm a grid SELECTION before a destructive write |
|
|
24
|
+
| `useRemoveConfirm` | Confirm a single row from a self-contained menu |
|
|
25
|
+
| `KindCardProps` / `KindListCardProps` | The `renderCard` / `renderListRow` contracts, named |
|
|
26
|
+
|
|
27
|
+
## Every sentence is yours
|
|
28
|
+
|
|
29
|
+
Three props exist only because a default would have been someone else's
|
|
30
|
+
language shipped as a finished-looking silence:
|
|
31
|
+
|
|
32
|
+
- `CardKebab`'s **`menuLabel`** — the trigger's accessible name, announced
|
|
33
|
+
verbatim by a screen reader.
|
|
34
|
+
- `CardActionsProvider`'s **`errorTitle`** — the one sentence the provider
|
|
35
|
+
renders on its own. The message beside it comes from whatever failed.
|
|
36
|
+
- Both hooks' **`confirmText`** — the verb. `ConfirmOptions` already refuses to
|
|
37
|
+
default one, for the same reason: a confirm button reading "OK" tells the
|
|
38
|
+
operator nothing about what is about to happen.
|
|
39
|
+
|
|
40
|
+
## Which confirm hook
|
|
41
|
+
|
|
42
|
+
They differ by the surface they serve, not by what they do.
|
|
43
|
+
|
|
44
|
+
**`useRowConfirm`** is for a grid whose actions are declared as `RowAction[]`
|
|
45
|
+
and act on a **selection**. The popup is described from that selection, because
|
|
46
|
+
the per-row kebab entry and the multi-select menu run the same handler —
|
|
47
|
+
"Delete the category Drinks?" and "Delete 4 categories?" are one question asked
|
|
48
|
+
of a different-sized selection, and an operator who multi-selected by mistake is
|
|
49
|
+
exactly who the popup is for. The selection is held while the popup is open, so
|
|
50
|
+
the write sees the rows the operator was looking at when they were asked; the
|
|
51
|
+
list can refresh underneath an open dialog, and re-reading the live selection at
|
|
52
|
+
confirm time is the bug this shape avoids. An empty selection opens nothing.
|
|
53
|
+
|
|
54
|
+
**`useRemoveConfirm`** is for a self-contained menu already inside a
|
|
55
|
+
`CardActionsProvider`. It reads the refresh and the error channel from context
|
|
56
|
+
and answers the `{ ok, error }` envelope those actions use.
|
|
57
|
+
|
|
58
|
+
Both close the three gaps a hand-rolled version keeps re-opening:
|
|
59
|
+
|
|
60
|
+
1. the write leaves **only** on confirm;
|
|
61
|
+
2. a refusal is surfaced **twice** — inside the popup, where the operator is
|
|
62
|
+
still deciding, and in the shared snackbar behind it, which is what remains
|
|
63
|
+
once they dismiss it;
|
|
64
|
+
3. a failed attempt leaves the popup **open** to be retried, instead of
|
|
65
|
+
vanishing over a delete that never happened.
|
|
66
|
+
|
|
67
|
+
## `useCardActions` throws outside its provider
|
|
68
|
+
|
|
69
|
+
Deliberately, rather than answering a null object. A menu whose `onRefresh`
|
|
70
|
+
silently did nothing would leave the operator looking at a stale row after a
|
|
71
|
+
delete that worked — which reads as the delete having failed. Failing at mount
|
|
72
|
+
says where the provider is missing.
|
|
73
|
+
|
|
74
|
+
## The context is small on purpose
|
|
75
|
+
|
|
76
|
+
It is **not** an edit-dialog host. A menu that read its dialogs from here would
|
|
77
|
+
stop being portable, because the set of dialogs is per-entity. What it may read
|
|
78
|
+
is three facts: the tenant it acts inside, what to do after a write lands, and
|
|
79
|
+
where to put a failure.
|
|
80
|
+
|
|
81
|
+
## See also
|
|
82
|
+
|
|
83
|
+
- `exportRows` (`@12-apps/ui/utils`) — the producing half of the grid's Export
|
|
84
|
+
control, for the common case where the host already has the rows.
|
|
85
|
+
- `useServerDataViews` (`@12-apps/app-shell/react`) — the router half of
|
|
86
|
+
`DataViewServer`. It lives there rather than here because it needs a router,
|
|
87
|
+
and a design system should not.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { CardAspectRatio, DataViewCardSelection } from '../DataViews';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* THE CARD KIT — what an admin list needs above `BaseCard`, and no more.
|
|
7
|
+
*
|
|
8
|
+
* `BaseCard` and `BaseListCard` deliberately say nothing about what goes inside
|
|
9
|
+
* them: the envelope is the library's and the design is the consumer's. That is
|
|
10
|
+
* the right call for a card component and exactly the wrong place for every
|
|
11
|
+
* entity in an admin app to invent its own kebab, its own two-column body, its
|
|
12
|
+
* own label/value pair and its own confirm-before-deleting. Those shapes are
|
|
13
|
+
* not per-entity decisions — they are the same shape repeated — so they live
|
|
14
|
+
* here once.
|
|
15
|
+
*
|
|
16
|
+
* The line this kit does NOT cross is the entity card itself. A "product card"
|
|
17
|
+
* or a "discount card" knows a domain, and a domain is the consumer's. What is
|
|
18
|
+
* here is the furniture such a card sits in.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Props every KIND CARD receives — the grid's `renderCard` contract, named.
|
|
23
|
+
*
|
|
24
|
+
* `row` is `Record<string, unknown>` rather than a generic on purpose: a grid
|
|
25
|
+
* renders rows of one shape and the renderer is registered per kind, so the
|
|
26
|
+
* cast happens once inside each card rather than threading a type parameter
|
|
27
|
+
* through the grid, the layout and the selection.
|
|
28
|
+
*/
|
|
29
|
+
export interface KindCardProps {
|
|
30
|
+
row: Record<string, unknown>;
|
|
31
|
+
selection: DataViewCardSelection;
|
|
32
|
+
/** Force a ratio (a uniform grid); defaults to the card's own. */
|
|
33
|
+
aspectRatio?: CardAspectRatio;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Props every kind LIST card receives — the "list" layout's counterpart.
|
|
38
|
+
*
|
|
39
|
+
* The same `(row, selection)` pair `renderListRow` hands over, and nothing
|
|
40
|
+
* more. No density: a row inside a list group takes the group's, so a card
|
|
41
|
+
* accepting one could only contradict its list. No `aspectRatio`: a full-width
|
|
42
|
+
* row has no ratio to force.
|
|
43
|
+
*/
|
|
44
|
+
export interface KindListCardProps {
|
|
45
|
+
row: Record<string, unknown>;
|
|
46
|
+
selection: DataViewCardSelection;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One labelled fact inside an expanded body. */
|
|
50
|
+
export interface CardFactProps {
|
|
51
|
+
label: string;
|
|
52
|
+
value: ReactNode;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One line of a money ledger. `info` tints a line that is not a plain cost. */
|
|
56
|
+
export interface CardLedgerLine {
|
|
57
|
+
label: string;
|
|
58
|
+
value: ReactNode;
|
|
59
|
+
tone?: 'default' | 'info';
|
|
60
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useCallback, useContext, useState, type JSX, type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Snackbar } from '@mui/material';
|
|
6
|
+
|
|
7
|
+
import { Alert } from '../Alert';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The ambient wiring a self-contained row/card menu needs, and nothing else.
|
|
11
|
+
*
|
|
12
|
+
* A menu that owns its own popups still needs three things it cannot know: the
|
|
13
|
+
* tenant it is acting inside, what to do after a write lands, and somewhere to
|
|
14
|
+
* put a failure. Passing those down through the grid, the layout and the card
|
|
15
|
+
* is four props of drilling per entity; holding them in context is one.
|
|
16
|
+
*
|
|
17
|
+
* Kept deliberately SMALL — it is not an edit-dialog host. A menu that read its
|
|
18
|
+
* dialogs from here would stop being portable, because the set of dialogs is
|
|
19
|
+
* per-entity. What it may read is the three facts below.
|
|
20
|
+
*
|
|
21
|
+
* The provider renders ONE shared error snackbar, which is the whole reason the
|
|
22
|
+
* error channel is here rather than per-menu: before it, every page carried its
|
|
23
|
+
* own error alert and every menu had to find its page's.
|
|
24
|
+
*/
|
|
25
|
+
interface CardActionsValue {
|
|
26
|
+
/** The tenant every action in this subtree acts inside. */
|
|
27
|
+
tenantSlug: string;
|
|
28
|
+
/** Re-read the list after a write. */
|
|
29
|
+
onRefresh: () => void;
|
|
30
|
+
/** Surface a failed action's message (the shared snackbar). */
|
|
31
|
+
notifyError: (message: string) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const CardActionsContext = createContext<CardActionsValue | null>(null);
|
|
35
|
+
|
|
36
|
+
export function CardActionsProvider({
|
|
37
|
+
tenantSlug,
|
|
38
|
+
onRefresh,
|
|
39
|
+
errorTitle,
|
|
40
|
+
autoHideMs = 6_000,
|
|
41
|
+
children,
|
|
42
|
+
}: {
|
|
43
|
+
tenantSlug: string;
|
|
44
|
+
onRefresh: () => void;
|
|
45
|
+
/**
|
|
46
|
+
* The snackbar's heading — required, no default.
|
|
47
|
+
*
|
|
48
|
+
* It is the one sentence this provider renders on its own, and a default
|
|
49
|
+
* would be the origin's language shipped as a finished-looking silence. The
|
|
50
|
+
* MESSAGE beside it comes from whatever failed, which is already the host's.
|
|
51
|
+
*/
|
|
52
|
+
errorTitle: string;
|
|
53
|
+
/** How long the snackbar stays up. */
|
|
54
|
+
autoHideMs?: number;
|
|
55
|
+
children: ReactNode;
|
|
56
|
+
}): JSX.Element {
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
58
|
+
const notifyError = useCallback((message: string) => setError(message), []);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<CardActionsContext.Provider value={{ tenantSlug, onRefresh, notifyError }}>
|
|
62
|
+
{children}
|
|
63
|
+
<Snackbar
|
|
64
|
+
open={error !== null}
|
|
65
|
+
autoHideDuration={autoHideMs}
|
|
66
|
+
onClose={() => setError(null)}
|
|
67
|
+
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
|
68
|
+
>
|
|
69
|
+
<span>
|
|
70
|
+
<Alert
|
|
71
|
+
variant="danger"
|
|
72
|
+
title={errorTitle}
|
|
73
|
+
description={error ?? ''}
|
|
74
|
+
closable
|
|
75
|
+
onClose={() => setError(null)}
|
|
76
|
+
data-testid="card-action-error"
|
|
77
|
+
/>
|
|
78
|
+
</span>
|
|
79
|
+
</Snackbar>
|
|
80
|
+
</CardActionsContext.Provider>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read the tenant + refresh + error wiring.
|
|
86
|
+
*
|
|
87
|
+
* THROWS outside a provider rather than answering a null object, deliberately:
|
|
88
|
+
* a menu whose `onRefresh` silently did nothing would leave the operator
|
|
89
|
+
* looking at a stale row after a delete that worked, which reads as the delete
|
|
90
|
+
* having failed. Failing at mount says where the provider is missing.
|
|
91
|
+
*/
|
|
92
|
+
export function useCardActions(): CardActionsValue {
|
|
93
|
+
const value = useContext(CardActionsContext);
|
|
94
|
+
if (!value) {
|
|
95
|
+
throw new Error('useCardActions must be used within a CardActionsProvider');
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@12-apps/ui/data-display/CardKit` — the furniture an admin list's cards and
|
|
3
|
+
* row menus sit in.
|
|
4
|
+
*
|
|
5
|
+
* See `./CardKit.types` for where the line falls: the envelope and the repeated
|
|
6
|
+
* shapes are here; the entity card that knows a domain is the consumer's.
|
|
7
|
+
*/
|
|
8
|
+
export { CardKebab } from './CardKebab';
|
|
9
|
+
export { CardActionsProvider, useCardActions } from './card-actions-context';
|
|
10
|
+
export { rowActionsToMenuItems } from './row-actions-to-menu';
|
|
11
|
+
export { BodyHeading, DetailColumns, Fact, Ledger, TagList } from './list-card-parts';
|
|
12
|
+
export { useRowConfirm, type RowConfirm, type RowConfirmCopy } from './use-row-confirm';
|
|
13
|
+
export { useRemoveConfirm, type CardWriteResult } from './use-remove-confirm';
|
|
14
|
+
export type {
|
|
15
|
+
CardFactProps,
|
|
16
|
+
CardLedgerLine,
|
|
17
|
+
KindCardProps,
|
|
18
|
+
KindListCardProps,
|
|
19
|
+
} from './CardKit.types';
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { JSX, ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import { Box } from '../../../mui/Box';
|
|
6
|
+
|
|
7
|
+
import type { CardFactProps, CardLedgerLine } from './CardKit.types';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* THE SHARED FURNITURE OF AN EXPANDED LIST-CARD BODY.
|
|
11
|
+
*
|
|
12
|
+
* `BaseListCard` says nothing about what goes inside a row's expanded area — it
|
|
13
|
+
* spans the rails and stops, because the design belongs to the consumer. Right
|
|
14
|
+
* for the card; wrong as the place where every entity in an admin app invents
|
|
15
|
+
* its own two-column layout, its own label/value pair and its own ledger.
|
|
16
|
+
*
|
|
17
|
+
* So the four shapes that actually repeat live here, once. Nothing here knows a
|
|
18
|
+
* domain: they take `ReactNode` and lay it out.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** The expanded body's two-column split: detail on the left, a ledger right. */
|
|
22
|
+
export function DetailColumns({ left, right }: { left: ReactNode; right: ReactNode }): JSX.Element {
|
|
23
|
+
return (
|
|
24
|
+
<Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' }, gap: 3 }}>
|
|
25
|
+
<Box sx={{ minWidth: 0 }}>{left}</Box>
|
|
26
|
+
<Box sx={{ minWidth: 0 }}>{right}</Box>
|
|
27
|
+
</Box>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One labelled fact inside an expanded body.
|
|
33
|
+
*
|
|
34
|
+
* Horizontal, not stacked: the body is already a wide area with room for a
|
|
35
|
+
* label beside its value, and stacking them there would ape the summary rails
|
|
36
|
+
* above without their alignment.
|
|
37
|
+
*/
|
|
38
|
+
export function Fact({ label, value }: CardFactProps): JSX.Element {
|
|
39
|
+
return (
|
|
40
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', gap: 2, fontSize: 13, py: 0.25 }}>
|
|
41
|
+
<Box component="span" sx={{ color: 'text.secondary', flexShrink: 0 }}>
|
|
42
|
+
{label}
|
|
43
|
+
</Box>
|
|
44
|
+
<Box component="span" sx={{ minWidth: 0, textAlign: 'right', overflowWrap: 'anywhere' }}>
|
|
45
|
+
{value}
|
|
46
|
+
</Box>
|
|
47
|
+
</Box>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A money ledger: rows of label/amount, then a ruled total.
|
|
53
|
+
*
|
|
54
|
+
* The total is separated by a RULE rather than by weight alone, because a
|
|
55
|
+
* subtotal and a total in one column with only bold between them is the classic
|
|
56
|
+
* misread — the eye lands on whichever number is larger.
|
|
57
|
+
*
|
|
58
|
+
* The figures arrive pre-formatted. This is a layout, and a component that
|
|
59
|
+
* formatted money would be choosing a locale on the consumer's behalf.
|
|
60
|
+
*/
|
|
61
|
+
export function Ledger({
|
|
62
|
+
lines,
|
|
63
|
+
total,
|
|
64
|
+
}: {
|
|
65
|
+
lines: CardLedgerLine[];
|
|
66
|
+
total: { label: string; value: ReactNode };
|
|
67
|
+
}): JSX.Element {
|
|
68
|
+
return (
|
|
69
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.25 }}>
|
|
70
|
+
{lines.map((line) => (
|
|
71
|
+
<Box
|
|
72
|
+
key={line.label}
|
|
73
|
+
sx={{
|
|
74
|
+
display: 'flex',
|
|
75
|
+
justifyContent: 'space-between',
|
|
76
|
+
fontSize: 13,
|
|
77
|
+
color: line.tone === 'info' ? 'info.main' : 'text.secondary',
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<Box component="span">{line.label}</Box>
|
|
81
|
+
<Box component="span" sx={{ fontVariantNumeric: 'tabular-nums' }}>
|
|
82
|
+
{line.value}
|
|
83
|
+
</Box>
|
|
84
|
+
</Box>
|
|
85
|
+
))}
|
|
86
|
+
<Box
|
|
87
|
+
sx={{
|
|
88
|
+
display: 'flex',
|
|
89
|
+
justifyContent: 'space-between',
|
|
90
|
+
fontWeight: 700,
|
|
91
|
+
borderTop: 1,
|
|
92
|
+
borderColor: 'divider',
|
|
93
|
+
pt: 0.5,
|
|
94
|
+
mt: 0.5,
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<Box component="span">{total.label}</Box>
|
|
98
|
+
<Box component="span" sx={{ fontVariantNumeric: 'tabular-nums' }}>
|
|
99
|
+
{total.value}
|
|
100
|
+
</Box>
|
|
101
|
+
</Box>
|
|
102
|
+
</Box>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A heading inside the expanded body, for a section that needs naming. */
|
|
107
|
+
export function BodyHeading({ children }: { children: ReactNode }): JSX.Element {
|
|
108
|
+
return (
|
|
109
|
+
<Box
|
|
110
|
+
sx={{
|
|
111
|
+
fontSize: 11,
|
|
112
|
+
fontWeight: 700,
|
|
113
|
+
letterSpacing: 0.4,
|
|
114
|
+
textTransform: 'uppercase',
|
|
115
|
+
color: 'text.disabled',
|
|
116
|
+
mb: 0.5,
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{children}
|
|
120
|
+
</Box>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A wrapping run of small items — permission names, role names, target ids.
|
|
126
|
+
*
|
|
127
|
+
* The whole reason several entities earn a list card at all: a `string[]` has no
|
|
128
|
+
* honest table cell. Truncated to "3 items" it says nothing; printed in full it
|
|
129
|
+
* destroys the column. Here it simply wraps.
|
|
130
|
+
*/
|
|
131
|
+
export function TagList({
|
|
132
|
+
items,
|
|
133
|
+
empty,
|
|
134
|
+
}: {
|
|
135
|
+
items: readonly string[];
|
|
136
|
+
/** What to show for an empty run — the consumer's sentence, no default. */
|
|
137
|
+
empty: string;
|
|
138
|
+
}): JSX.Element {
|
|
139
|
+
if (items.length === 0) {
|
|
140
|
+
return <Box sx={{ fontSize: 13, color: 'text.disabled' }}>{empty}</Box>;
|
|
141
|
+
}
|
|
142
|
+
return (
|
|
143
|
+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
|
144
|
+
{items.map((item) => (
|
|
145
|
+
<Box
|
|
146
|
+
key={item}
|
|
147
|
+
component="span"
|
|
148
|
+
sx={{
|
|
149
|
+
fontSize: 12,
|
|
150
|
+
px: 0.75,
|
|
151
|
+
py: 0.25,
|
|
152
|
+
borderRadius: 1,
|
|
153
|
+
bgcolor: 'action.hover',
|
|
154
|
+
color: 'text.secondary',
|
|
155
|
+
fontFamily: 'monospace',
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{item}
|
|
159
|
+
</Box>
|
|
160
|
+
))}
|
|
161
|
+
</Box>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DropdownMenuItem } from '../../navigation/DropdownMenu';
|
|
2
|
+
import type { RowAction } from '../DataViews';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Adapt a list of {@link RowAction}s into single-row kebab items.
|
|
6
|
+
*
|
|
7
|
+
* Applies each action's `isVisible` guard, resolves its per-row `rowLabel`
|
|
8
|
+
* override, carries its colour, and fires `onSelect([row])` on click.
|
|
9
|
+
*
|
|
10
|
+
* The point is that ONE definition drives both surfaces. An entity that already
|
|
11
|
+
* declares its per-row actions as `RowAction[]` — because the table's multi-
|
|
12
|
+
* select menu needs them — gets the card's kebab from the same array through
|
|
13
|
+
* `renderRowMenu` / `renderCard`, with no second list to keep in step. Two
|
|
14
|
+
* lists is how a "Duplicar" that appears in the table and not on the card
|
|
15
|
+
* happens, and nothing reports it.
|
|
16
|
+
*/
|
|
17
|
+
export function rowActionsToMenuItems<T extends Record<string, unknown>>(
|
|
18
|
+
actions: RowAction<T>[],
|
|
19
|
+
row: T,
|
|
20
|
+
): DropdownMenuItem[] {
|
|
21
|
+
return actions
|
|
22
|
+
.filter((action) => action.isVisible?.(row) ?? true)
|
|
23
|
+
.map((action) => ({
|
|
24
|
+
id: action.id,
|
|
25
|
+
label: action.rowLabel?.(row) ?? action.label,
|
|
26
|
+
color: action.color,
|
|
27
|
+
onClick: () => void action.onSelect([row]),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ConfirmActionCopy } from "../../../copy";
|
|
4
|
+
import type { JSX } from 'react';
|
|
5
|
+
|
|
6
|
+
import { ConfirmActionDialog, useConfirmAction } from '../../feedback/ConfirmAction';
|
|
7
|
+
|
|
8
|
+
import { useCardActions } from './card-actions-context';
|
|
9
|
+
|
|
10
|
+
/** The `{ ok, error }` envelope a write action answers with. */
|
|
11
|
+
export interface CardWriteResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RemoveConfirmOptions {
|
|
17
|
+
/** The destructive write. Called ONLY after the operator confirms. */
|
|
18
|
+
write: () => Promise<CardWriteResult>;
|
|
19
|
+
/** The popup's question. */
|
|
20
|
+
title: string;
|
|
21
|
+
/** The row being removed, echoed into the popup so it names a thing. */
|
|
22
|
+
entityName: string;
|
|
23
|
+
/**
|
|
24
|
+
* What happens, and whether it can be walked back. Say which — the difference
|
|
25
|
+
* between a soft delete a recycle bin recovers and one nothing recovers is
|
|
26
|
+
* the whole content of the decision being asked for.
|
|
27
|
+
*/
|
|
28
|
+
description: string;
|
|
29
|
+
/** The confirm button's VERB. Required, never defaulted. */
|
|
30
|
+
confirmText: string;
|
|
31
|
+
/** Used when the write fails without a message of its own. */
|
|
32
|
+
fallbackError: string;
|
|
33
|
+
/** The dialog's own words. REQUIRED — this package ships no default copy. */
|
|
34
|
+
copy: ConfirmActionCopy;
|
|
35
|
+
dataTestId: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface RemoveConfirm {
|
|
39
|
+
/** Hand this to the menu item's `onClick`: it opens the popup, nothing more. */
|
|
40
|
+
request: () => void;
|
|
41
|
+
/** Render alongside the trigger. */
|
|
42
|
+
dialog: JSX.Element;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The confirm-before-removing wiring every card/row menu repeats, in one place.
|
|
47
|
+
*
|
|
48
|
+
* It closes the three gaps a hand-rolled version keeps re-opening: the write
|
|
49
|
+
* leaves ONLY on confirm; a refusal is surfaced twice — inside the popup, where
|
|
50
|
+
* the operator is still deciding, and in the shared snackbar behind it — and a
|
|
51
|
+
* failed attempt leaves the popup OPEN to be retried instead of vanishing over
|
|
52
|
+
* a delete that never happened.
|
|
53
|
+
*
|
|
54
|
+
* The difference from {@link useRowConfirm} is which surface it serves: this one
|
|
55
|
+
* is for a SELF-CONTAINED menu that already sits inside a
|
|
56
|
+
* `CardActionsProvider`, so it reads the refresh and the error channel from
|
|
57
|
+
* context and answers the `{ ok, error }` envelope those actions use.
|
|
58
|
+
* `useRowConfirm` is for a grid whose actions are declared as `RowAction[]` and
|
|
59
|
+
* act on a selection.
|
|
60
|
+
*
|
|
61
|
+
* ```tsx
|
|
62
|
+
* const remove = useRemoveConfirm({
|
|
63
|
+
* write: () => deleteCategory({ tenantSlug, id: row.id }),
|
|
64
|
+
* title: copy.deleteTitle,
|
|
65
|
+
* entityName: row.name,
|
|
66
|
+
* description: copy.deleteDescription,
|
|
67
|
+
* confirmText: copy.delete,
|
|
68
|
+
* fallbackError: copy.deleteFailed,
|
|
69
|
+
* dataTestId: 'category-delete-confirm',
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function useRemoveConfirm({
|
|
74
|
+
write,
|
|
75
|
+
title,
|
|
76
|
+
entityName,
|
|
77
|
+
description,
|
|
78
|
+
confirmText,
|
|
79
|
+
fallbackError,
|
|
80
|
+
dataTestId,
|
|
81
|
+
copy,
|
|
82
|
+
}: RemoveConfirmOptions): RemoveConfirm {
|
|
83
|
+
const { onRefresh, notifyError } = useCardActions();
|
|
84
|
+
|
|
85
|
+
const state = useConfirmAction(async () => {
|
|
86
|
+
const result = await write();
|
|
87
|
+
if (!result.ok) {
|
|
88
|
+
const message = result.error ?? fallbackError;
|
|
89
|
+
notifyError(message);
|
|
90
|
+
// Rejecting is what keeps the popup open carrying `message`; the snackbar
|
|
91
|
+
// above is what remains once the operator dismisses it.
|
|
92
|
+
throw new Error(message);
|
|
93
|
+
}
|
|
94
|
+
onRefresh();
|
|
95
|
+
}, fallbackError);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
request: state.request,
|
|
99
|
+
dialog: (
|
|
100
|
+
<ConfirmActionDialog
|
|
101
|
+
state={state}
|
|
102
|
+
copy={copy}
|
|
103
|
+
title={title}
|
|
104
|
+
entityName={entityName}
|
|
105
|
+
description={description}
|
|
106
|
+
confirmText={confirmText}
|
|
107
|
+
dataTestId={dataTestId}
|
|
108
|
+
/>
|
|
109
|
+
),
|
|
110
|
+
};
|
|
111
|
+
}
|