@tristan2828/ui-foundation 4.37.0 → 4.38.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.
|
@@ -25,15 +25,15 @@
|
|
|
25
25
|
- [15. Yes/no you flip in the row](#15-yesno-you-flip-in-the-row) · lines 538–647
|
|
26
26
|
- [A variant: a pressed icon inside another value's cell](#a-variant-a-pressed-icon-inside-another-values-cell) · lines 564–647
|
|
27
27
|
- [16. Title linking to the record's view](#16-title-linking-to-the-records-view) · lines 649–684
|
|
28
|
-
- [17. Quick action on the view](#17-quick-action-on-the-view) · lines 686–
|
|
29
|
-
- [18. Editable value](#18-editable-value) · lines
|
|
30
|
-
- [A list of sub-items](#a-list-of-sub-items) · lines
|
|
31
|
-
- [In a table's rows](#in-a-tables-rows) · lines
|
|
32
|
-
- [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines
|
|
33
|
-
- [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines
|
|
34
|
-
- [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines
|
|
35
|
-
- [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines
|
|
36
|
-
- [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines
|
|
28
|
+
- [17. Quick action on the view](#17-quick-action-on-the-view) · lines 686–759
|
|
29
|
+
- [18. Editable value](#18-editable-value) · lines 761–1176
|
|
30
|
+
- [A list of sub-items](#a-list-of-sub-items) · lines 1020–1077
|
|
31
|
+
- [In a table's rows](#in-a-tables-rows) · lines 1079–1164
|
|
32
|
+
- [A table of a few short columns: `width="content"`](#a-table-of-a-few-short-columns-widthcontent) · lines 1166–1176
|
|
33
|
+
- [19. Stage circle for an ordered status](#19-stage-circle-for-an-ordered-status) · lines 1178–1244
|
|
34
|
+
- [20. Count linking to the related records](#20-count-linking-to-the-related-records) · lines 1246–1310
|
|
35
|
+
- [21. Rows grouped under headings, and a pill filter](#21-rows-grouped-under-headings-and-a-pill-filter) · lines 1312–1377
|
|
36
|
+
- [22. A highlighted row, and rows that are done](#22-a-highlighted-row-and-rows-that-are-done) · lines 1379–1407
|
|
37
37
|
|
|
38
38
|
<!-- /index -->
|
|
39
39
|
|
|
@@ -731,6 +731,12 @@ const save = useSaveWidgetField(widget.id) // one per control
|
|
|
731
731
|
page. When the save answers, the view takes the record the server
|
|
732
732
|
returned, so a server-side effect shows (a computed field, a flag the
|
|
733
733
|
change cleared). Never write a quick action's mutation by hand.
|
|
734
|
+
- **A refresh the save always needs goes on the hook**: another query
|
|
735
|
+
the change affects (a summary, a related list) is invalidated in
|
|
736
|
+
`useRecordUpdate`'s own `onSuccess`, never `mutate(change, { onSuccess })`,
|
|
737
|
+
which TanStack skips once the control unmounts (a popover closed
|
|
738
|
+
mid-save). Per-call effects (navigate, a toast in the call's words)
|
|
739
|
+
stay on `mutate()`.
|
|
734
740
|
- **A list is sent whole, built from the latest record**: pass a function
|
|
735
741
|
of the record (`(current) => ({ checklist: ... })`), not a list read from
|
|
736
742
|
props. Saves of one record run one at a time and each request is built
|
|
@@ -35,6 +35,17 @@ export type RecordUpdateOptions<TRecord extends {
|
|
|
35
35
|
* `AppError`, and `refusalReason` words it.
|
|
36
36
|
*/
|
|
37
37
|
toastOnError?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* After a save the server accepted, once the caches hold its record:
|
|
40
|
+
* a refresh the save always needs (another query it changes). Runs
|
|
41
|
+
* even when the calling component has unmounted, unlike a `mutate()`
|
|
42
|
+
* callback; keep per-call effects (navigate, a toast) on `mutate()`.
|
|
43
|
+
*/
|
|
44
|
+
onSuccess?: (saved: TRecord, change: RecordChange<TRecord, TUpdate>) => unknown;
|
|
45
|
+
/** After a refused save, once it has rolled back (and toasted, unless `toastOnError: false`). */
|
|
46
|
+
onError?: (error: AppError, change: RecordChange<TRecord, TUpdate>) => unknown;
|
|
47
|
+
/** After every save, accepted or refused, following `onSuccess` or `onError`. */
|
|
48
|
+
onSettled?: (saved: TRecord | undefined, error: AppError | null, change: RecordChange<TRecord, TUpdate>) => unknown;
|
|
38
49
|
};
|
|
39
50
|
type Entry<TRecord, TUpdate> = {
|
|
40
51
|
change: RecordChange<TRecord, TUpdate>;
|
|
@@ -52,7 +63,7 @@ export declare function refusalReason(error: AppError, sent?: object): string;
|
|
|
52
63
|
*/
|
|
53
64
|
export declare function recordUpdateMutationOptions<TRecord extends {
|
|
54
65
|
id: RecordId;
|
|
55
|
-
}, TUpdate extends Partial<TRecord>>(queryClient: QueryClient, { id, detailKey, listsKey, update, name, optimistic, toastOnError }: RecordUpdateOptions<TRecord, TUpdate>): MutationOptions<TRecord, AppError, RecordChange<TRecord, TUpdate>, {
|
|
66
|
+
}, TUpdate extends Partial<TRecord>>(queryClient: QueryClient, { id, detailKey, listsKey, update, name, optimistic, toastOnError, onSuccess, onError, onSettled, }: RecordUpdateOptions<TRecord, TUpdate>): MutationOptions<TRecord, AppError, RecordChange<TRecord, TUpdate>, {
|
|
56
67
|
entry: Entry<TRecord, TUpdate>;
|
|
57
68
|
}>;
|
|
58
69
|
/**
|
|
@@ -61,7 +72,8 @@ export declare function recordUpdateMutationOptions<TRecord extends {
|
|
|
61
72
|
* with the entity's query keys and its gateway PATCH, then
|
|
62
73
|
* `mutate({ field: value })` or `mutate((record) => ({ field: ... }))`.
|
|
63
74
|
* `isPending` is that control's own save in flight. A refusal rolls back
|
|
64
|
-
* and shows a toast by itself.
|
|
75
|
+
* and shows a toast by itself. A refresh every save needs goes in the
|
|
76
|
+
* options' `onSuccess`, which runs even if the control has unmounted.
|
|
65
77
|
*/
|
|
66
78
|
export declare function useRecordUpdate<TRecord extends {
|
|
67
79
|
id: RecordId;
|
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
// `toastOnError: false`: nothing changes on screen until the server
|
|
31
31
|
// agrees, and a refusal comes back to the field (`mutateAsync` rejects)
|
|
32
32
|
// instead of a toast.
|
|
33
|
+
//
|
|
34
|
+
// A follow-up the save needs once the server answers (refetching a
|
|
35
|
+
// summary or a related list) goes in the hook's own `onSuccess`,
|
|
36
|
+
// `onError` or `onSettled`, not `mutate(change, { onSuccess })`: TanStack
|
|
37
|
+
// runs a `mutate()` callback only while the calling component is still
|
|
38
|
+
// mounted, so a popover closed mid-save would skip it. These run on the
|
|
39
|
+
// mutation, after the package's own cache work, either way (#250).
|
|
33
40
|
import { hashKey, useMutation, useQueryClient, } from '@tanstack/react-query';
|
|
34
41
|
import { toast } from 'sonner';
|
|
35
42
|
const states = new WeakMap();
|
|
@@ -62,7 +69,7 @@ export function refusalReason(error, sent) {
|
|
|
62
69
|
* The mutation options behind `useRecordUpdate`, for a given QueryClient.
|
|
63
70
|
* Exported for tests and for code outside React; screens use the hook.
|
|
64
71
|
*/
|
|
65
|
-
export function recordUpdateMutationOptions(queryClient, { id, detailKey, listsKey, update, name, optimistic = true, toastOnError = true }) {
|
|
72
|
+
export function recordUpdateMutationOptions(queryClient, { id, detailKey, listsKey, update, name, optimistic = true, toastOnError = true, onSuccess, onError, onSettled, }) {
|
|
66
73
|
const key = hashKey(detailKey);
|
|
67
74
|
const lists = { queryKey: listsKey };
|
|
68
75
|
const map = recordStates(queryClient);
|
|
@@ -133,25 +140,27 @@ export function recordUpdateMutationOptions(queryClient, { id, detailKey, listsK
|
|
|
133
140
|
render(state);
|
|
134
141
|
return { entry };
|
|
135
142
|
},
|
|
136
|
-
onError: (error,
|
|
143
|
+
onError: (error, change, context) => {
|
|
137
144
|
const state = map.get(key);
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
145
|
+
if (state && context) {
|
|
146
|
+
// What was sent: the change built on the record it was sent against.
|
|
147
|
+
const record = state.confirmed;
|
|
148
|
+
const sent = record ? resolve(change, record) : typeof change === 'function' ? undefined : change;
|
|
149
|
+
settle(state, context.entry);
|
|
150
|
+
if (toastOnError)
|
|
151
|
+
toast.error(`Couldn't update ${record ? name(record) : 'this record'}: ${refusalReason(error, sent)}`);
|
|
152
|
+
}
|
|
153
|
+
return onError?.(error, change);
|
|
147
154
|
},
|
|
148
|
-
onSuccess: (saved,
|
|
155
|
+
onSuccess: (saved, change, context) => {
|
|
149
156
|
const state = map.get(key);
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
if (state && context) {
|
|
158
|
+
state.confirmed = saved;
|
|
159
|
+
settle(state, context.entry);
|
|
160
|
+
}
|
|
161
|
+
return onSuccess?.(saved, change);
|
|
154
162
|
},
|
|
163
|
+
onSettled: (saved, error, change) => onSettled?.(saved, error, change),
|
|
155
164
|
};
|
|
156
165
|
}
|
|
157
166
|
/**
|
|
@@ -160,7 +169,8 @@ export function recordUpdateMutationOptions(queryClient, { id, detailKey, listsK
|
|
|
160
169
|
* with the entity's query keys and its gateway PATCH, then
|
|
161
170
|
* `mutate({ field: value })` or `mutate((record) => ({ field: ... }))`.
|
|
162
171
|
* `isPending` is that control's own save in flight. A refusal rolls back
|
|
163
|
-
* and shows a toast by itself.
|
|
172
|
+
* and shows a toast by itself. A refresh every save needs goes in the
|
|
173
|
+
* options' `onSuccess`, which runs even if the control has unmounted.
|
|
164
174
|
*/
|
|
165
175
|
export function useRecordUpdate(options) {
|
|
166
176
|
const queryClient = useQueryClient();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.38.0",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|