@chelone/core 0.1.1 → 0.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/dist/controller/calendar-events.js +5 -8
- package/dist/controller/client-list-controller.js +4 -13
- package/dist/controller/record-tree.js +2 -5
- package/dist/controller/record-workflow.d.ts +27 -11
- package/dist/controller/record-workflow.js +5 -1
- package/dist/data/cache-ops.d.ts +8 -0
- package/dist/data/cache-ops.js +80 -0
- package/dist/data/mutation-options.d.ts +5 -1
- package/dist/data/mutation-options.js +33 -3
- package/dist/form/array-binding.d.ts +4 -3
- package/dist/form/input-binding.d.ts +6 -4
- package/dist/form/source.d.ts +22 -4
- package/dist/form/source.js +32 -2
- package/dist/i18n/english-messages.d.ts +250 -0
- package/dist/i18n/english-messages.js +193 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/resource/get-at-path.d.ts +18 -0
- package/dist/resource/get-at-path.js +16 -0
- package/dist/resource/register.d.ts +26 -0
- package/dist/resource/registry.d.ts +16 -1
- package/dist/resource/source-path.d.ts +114 -0
- package/package.json +4 -1
|
@@ -1,20 +1,17 @@
|
|
|
1
|
+
import { getAtPath } from "../resource/get-at-path.js";
|
|
1
2
|
import { calendarDayKey, parseCalendarDate } from "./calendar-dates.js";
|
|
2
|
-
const readPath = (record, path) => path.split(".").reduce(
|
|
3
|
-
(value, key) => value == null ? void 0 : value[key],
|
|
4
|
-
record
|
|
5
|
-
);
|
|
6
3
|
const midnight = (date, dayOffset = 0) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + dayOffset);
|
|
7
4
|
const isMidnight = (date) => date.getTime() === midnight(date).getTime();
|
|
8
5
|
function toCalendarEvents(records, options) {
|
|
9
6
|
const events = [];
|
|
10
7
|
for (const record of records) {
|
|
11
|
-
const start = parseCalendarDate(
|
|
8
|
+
const start = parseCalendarDate(getAtPath(record, options.startSource));
|
|
12
9
|
if (!start) continue;
|
|
13
|
-
const parsedEnd = options.endSource ? parseCalendarDate(
|
|
10
|
+
const parsedEnd = options.endSource ? parseCalendarDate(getAtPath(record, options.endSource)) : void 0;
|
|
14
11
|
const end = parsedEnd && parsedEnd.getTime() >= start.getTime() ? parsedEnd : void 0;
|
|
15
|
-
const titleValue = options.titleSource ?
|
|
12
|
+
const titleValue = options.titleSource ? getAtPath(record, options.titleSource) : void 0;
|
|
16
13
|
const title = (typeof titleValue === "string" || typeof titleValue === "number") && titleValue !== "" ? String(titleValue) : options.getTitle(record);
|
|
17
|
-
const colorValue = options.colorSource ?
|
|
14
|
+
const colorValue = options.colorSource ? getAtPath(record, options.colorSource) : void 0;
|
|
18
15
|
const color = colorValue == null ? void 0 : options.colorMap?.[String(colorValue)];
|
|
19
16
|
const event = { record, start, title };
|
|
20
17
|
if (end) event.end = end;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getAtPath } from "../resource/get-at-path.js";
|
|
1
2
|
import { createMicroStore } from "../store/micro-store.js";
|
|
2
3
|
import { removeFilterPath, listParamsFromPatch } from "./list-controller.js";
|
|
3
4
|
const DEFAULT_PER_PAGE = 1e3;
|
|
@@ -15,16 +16,6 @@ function removeEmpty(filter) {
|
|
|
15
16
|
}
|
|
16
17
|
return result;
|
|
17
18
|
}
|
|
18
|
-
function getByPath(record, path) {
|
|
19
|
-
if (record == null) return void 0;
|
|
20
|
-
if (!path.includes(".")) return record[path];
|
|
21
|
-
let current = record;
|
|
22
|
-
for (const segment of path.split(".")) {
|
|
23
|
-
if (current == null || typeof current !== "object") return void 0;
|
|
24
|
-
current = current[segment];
|
|
25
|
-
}
|
|
26
|
-
return current;
|
|
27
|
-
}
|
|
28
19
|
function flattenFilter(filter, prefix = "") {
|
|
29
20
|
const result = {};
|
|
30
21
|
for (const [key, value] of Object.entries(filter)) {
|
|
@@ -39,7 +30,7 @@ function flattenFilter(filter, prefix = "") {
|
|
|
39
30
|
}
|
|
40
31
|
function matchesFilter(record, flatFilter) {
|
|
41
32
|
return Object.entries(flatFilter).every(([filterName, filterValue]) => {
|
|
42
|
-
const recordValue =
|
|
33
|
+
const recordValue = getAtPath(record, filterName);
|
|
43
34
|
if (Array.isArray(recordValue)) {
|
|
44
35
|
return Array.isArray(filterValue) ? recordValue.some((item) => filterValue.includes(item)) : recordValue.includes(filterValue);
|
|
45
36
|
}
|
|
@@ -67,8 +58,8 @@ function applyFilterAndSort(records, filter, sort, filterCallback) {
|
|
|
67
58
|
result = [...result].sort((a, b) => {
|
|
68
59
|
for (const { field, order } of keys) {
|
|
69
60
|
const asc = order === "ASC";
|
|
70
|
-
const av =
|
|
71
|
-
const bv =
|
|
61
|
+
const av = getAtPath(a, field);
|
|
62
|
+
const bv = getAtPath(b, field);
|
|
72
63
|
if (av === bv || av == null && bv == null) continue;
|
|
73
64
|
if (av == null) return asc ? -1 : 1;
|
|
74
65
|
if (bv == null) return asc ? 1 : -1;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
(value, key) => value == null ? void 0 : value[key],
|
|
3
|
-
record
|
|
4
|
-
);
|
|
1
|
+
import { getAtPath } from "../resource/get-at-path.js";
|
|
5
2
|
function buildRecordTree(records, parentSource) {
|
|
6
3
|
const nodes = /* @__PURE__ */ new Map();
|
|
7
4
|
const order = /* @__PURE__ */ new Map();
|
|
@@ -13,7 +10,7 @@ function buildRecordTree(records, parentSource) {
|
|
|
13
10
|
}
|
|
14
11
|
const parents = /* @__PURE__ */ new Map();
|
|
15
12
|
for (const [id, node] of nodes) {
|
|
16
|
-
const value =
|
|
13
|
+
const value = getAtPath(node.record, parentSource);
|
|
17
14
|
const parent = value == null || value === "" ? null : String(value);
|
|
18
15
|
parents.set(
|
|
19
16
|
id,
|
|
@@ -7,21 +7,37 @@
|
|
|
7
7
|
* can overwrite each other. The server has to enforce the count and who may
|
|
8
8
|
* approve.
|
|
9
9
|
*/
|
|
10
|
+
import type { SourceKey } from "../resource/source-path.js";
|
|
10
11
|
type Id = string | number;
|
|
11
|
-
export interface ApprovalOptions {
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
export interface ApprovalOptions<TRecord = Record<string, unknown>> {
|
|
13
|
+
/**
|
|
14
|
+
* Record field holding the approver list, a top-level key: it is read as
|
|
15
|
+
* `record[approverSource]` and written back whole. Default `"approvers"`.
|
|
16
|
+
*/
|
|
17
|
+
approverSource?: SourceKey<TRecord>;
|
|
14
18
|
/** Approvals needed. Floored, minimum 1; `NaN` reads as 2. Default 2. */
|
|
15
19
|
required?: number;
|
|
16
20
|
/** The current user. No identity means nobody can approve. */
|
|
17
21
|
identityId?: Id | null;
|
|
18
|
-
/**
|
|
19
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Record field `buildApprovalUpdate` writes the status to, a top-level key.
|
|
24
|
+
* Default `"status"`.
|
|
25
|
+
*/
|
|
26
|
+
statusSource?: SourceKey<TRecord>;
|
|
20
27
|
/** Status written while approvals are short of `required`. Default `"pending"`. */
|
|
21
28
|
pendingValue?: unknown;
|
|
22
29
|
/** Status written once approvals reach `required`. Default `"approved"`. */
|
|
23
30
|
approvedValue?: unknown;
|
|
24
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* What `buildApprovalUpdate` takes. `statusSource` is a write target, and a
|
|
34
|
+
* record often has no status field until its first approval, so it takes any
|
|
35
|
+
* string; `approverSource` is read, so it stays a key of the record.
|
|
36
|
+
*/
|
|
37
|
+
export type ApprovalUpdateOptions<TRecord = Record<string, unknown>> = Omit<ApprovalOptions<TRecord>, "statusSource"> & {
|
|
38
|
+
/** Record field the status is written to. Default `"status"`. */
|
|
39
|
+
statusSource?: string;
|
|
40
|
+
};
|
|
25
41
|
export interface ApprovalState {
|
|
26
42
|
/** Distinct approver ids, as strings. */
|
|
27
43
|
approvers: string[];
|
|
@@ -31,9 +47,9 @@ export interface ApprovalState {
|
|
|
31
47
|
isComplete: boolean;
|
|
32
48
|
canApprove: boolean;
|
|
33
49
|
}
|
|
34
|
-
export interface TransitionOptions {
|
|
35
|
-
/** Record field holding the status. Default `"status"`. */
|
|
36
|
-
source?:
|
|
50
|
+
export interface TransitionOptions<TRecord = Record<string, unknown>> {
|
|
51
|
+
/** Record field holding the status, a top-level key. Default `"status"`. */
|
|
52
|
+
source?: SourceKey<TRecord>;
|
|
37
53
|
/** Next states by current state. */
|
|
38
54
|
transitions: Record<string, readonly string[]>;
|
|
39
55
|
/** Predicates keyed `"<from>-><to>"`. A transition with no guard is allowed. */
|
|
@@ -43,12 +59,12 @@ export interface AllowedTransitions {
|
|
|
43
59
|
current: string;
|
|
44
60
|
allowed: string[];
|
|
45
61
|
}
|
|
46
|
-
export declare function getApprovalState
|
|
62
|
+
export declare function getApprovalState<TRecord = Record<string, unknown>>(record: TRecord & object, options?: ApprovalOptions<TRecord>): ApprovalState;
|
|
47
63
|
/**
|
|
48
64
|
* The patch that records `identityId`'s approval, or `null` when they cannot
|
|
49
65
|
* approve. Stored items stay as they were and the identity is appended in its
|
|
50
66
|
* own type.
|
|
51
67
|
*/
|
|
52
|
-
export declare function buildApprovalUpdate
|
|
53
|
-
export declare function getAllowedTransitions
|
|
68
|
+
export declare function buildApprovalUpdate<TRecord = Record<string, unknown>>(record: TRecord & object, options?: ApprovalUpdateOptions<TRecord>): Record<string, unknown> | null;
|
|
69
|
+
export declare function getAllowedTransitions<TRecord = Record<string, unknown>>(record: TRecord & object, options: TransitionOptions<TRecord>): AllowedTransitions;
|
|
54
70
|
export {};
|
|
@@ -34,7 +34,11 @@ function buildApprovalUpdate(record, options = {}) {
|
|
|
34
34
|
pendingValue = "pending",
|
|
35
35
|
approvedValue = "approved"
|
|
36
36
|
} = options;
|
|
37
|
-
const state = getApprovalState(record,
|
|
37
|
+
const state = getApprovalState(record, {
|
|
38
|
+
approverSource: options.approverSource,
|
|
39
|
+
required: options.required,
|
|
40
|
+
identityId
|
|
41
|
+
});
|
|
38
42
|
if (!state.canApprove || identityId === void 0 || identityId === null) {
|
|
39
43
|
return null;
|
|
40
44
|
}
|
package/dist/data/cache-ops.d.ts
CHANGED
|
@@ -12,6 +12,14 @@ export declare function removeRecordsFromCache(queryClient: QueryClient, resourc
|
|
|
12
12
|
* `getManyReference`, `getInfiniteList` page and `getMany` entry.
|
|
13
13
|
*/
|
|
14
14
|
export declare function updateRecordsInCache(queryClient: QueryClient, resource: string, ids: Identifier[], data: Record<string, unknown>): void;
|
|
15
|
+
/**
|
|
16
|
+
* Shallow-merge `data` into the matching rows of every cached `getList`,
|
|
17
|
+
* `getManyReference` and `getInfiniteList` page, and return a `rollback` that
|
|
18
|
+
* puts back only the patched keys of those rows into whatever each entry holds
|
|
19
|
+
* when it is called, so another update's patch to a different row survives.
|
|
20
|
+
* An entry that holds none of the ids keeps its cached object.
|
|
21
|
+
*/
|
|
22
|
+
export declare function patchListEntries(queryClient: QueryClient, resource: string, idSet: Set<string>, data: Record<string, unknown>): () => void;
|
|
15
23
|
/**
|
|
16
24
|
* Shallow-merge `data` into the matching records of every cached `getMany`
|
|
17
25
|
* entry (what `ReferenceField` and `ReferenceArrayField` read through).
|
package/dist/data/cache-ops.js
CHANGED
|
@@ -95,6 +95,85 @@ function updateRecordsInCache(queryClient, resource, ids, data) {
|
|
|
95
95
|
}
|
|
96
96
|
patchManyEntries(queryClient, resource, idSet, data);
|
|
97
97
|
}
|
|
98
|
+
const ABSENT = /* @__PURE__ */ Symbol("absent");
|
|
99
|
+
function priorsOf(page, idSet, data) {
|
|
100
|
+
const priors = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const row of page.data) {
|
|
102
|
+
const id = String(row.id);
|
|
103
|
+
if (!idSet.has(id)) continue;
|
|
104
|
+
const prior = {};
|
|
105
|
+
for (const field of Object.keys(data)) {
|
|
106
|
+
prior[field] = field in row ? row[field] : ABSENT;
|
|
107
|
+
}
|
|
108
|
+
priors.set(id, prior);
|
|
109
|
+
}
|
|
110
|
+
return priors;
|
|
111
|
+
}
|
|
112
|
+
function withRestoredRows(page, priors) {
|
|
113
|
+
return {
|
|
114
|
+
...page,
|
|
115
|
+
data: page.data.map((record) => {
|
|
116
|
+
const prior = priors.get(String(record.id));
|
|
117
|
+
if (!prior) return record;
|
|
118
|
+
const restored = { ...record };
|
|
119
|
+
for (const [field, value] of Object.entries(prior)) {
|
|
120
|
+
if (value === ABSENT) delete restored[field];
|
|
121
|
+
else restored[field] = value;
|
|
122
|
+
}
|
|
123
|
+
return restored;
|
|
124
|
+
})
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function patchListEntries(queryClient, resource, idSet, data) {
|
|
128
|
+
const rollbacks = [];
|
|
129
|
+
for (const queryKey of singleResultListKeyPrefixes(resource)) {
|
|
130
|
+
for (const [key, value] of queryClient.getQueriesData({
|
|
131
|
+
queryKey
|
|
132
|
+
})) {
|
|
133
|
+
if (!value) continue;
|
|
134
|
+
const next = withPatchedRows(value, idSet, data);
|
|
135
|
+
if (!next) continue;
|
|
136
|
+
const priors = priorsOf(value, idSet, data);
|
|
137
|
+
queryClient.setQueryData(key, next);
|
|
138
|
+
rollbacks.push(() => {
|
|
139
|
+
const current = queryClient.getQueryData(key);
|
|
140
|
+
if (current) {
|
|
141
|
+
queryClient.setQueryData(
|
|
142
|
+
key,
|
|
143
|
+
withRestoredRows(current, priors)
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const infinite = queryClient.getQueriesData({ queryKey: infiniteListKeyPrefix(resource) });
|
|
150
|
+
for (const [key, value] of infinite) {
|
|
151
|
+
if (!value) continue;
|
|
152
|
+
const pages = value.pages.map(
|
|
153
|
+
(page) => withPatchedRows(page, idSet, data) ?? page
|
|
154
|
+
);
|
|
155
|
+
if (pages.every((page, i) => page === value.pages[i])) continue;
|
|
156
|
+
const priorsByPage = value.pages.map((page) => priorsOf(page, idSet, data));
|
|
157
|
+
queryClient.setQueryData(key, {
|
|
158
|
+
...value,
|
|
159
|
+
pages
|
|
160
|
+
});
|
|
161
|
+
rollbacks.push(() => {
|
|
162
|
+
const current = queryClient.getQueryData(key);
|
|
163
|
+
if (!current) return;
|
|
164
|
+
queryClient.setQueryData(key, {
|
|
165
|
+
...current,
|
|
166
|
+
pages: current.pages.map((page, i) => {
|
|
167
|
+
const priors = priorsByPage[i];
|
|
168
|
+
return priors && priors.size > 0 ? withRestoredRows(page, priors) : page;
|
|
169
|
+
})
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return () => {
|
|
174
|
+
for (const rollback of rollbacks) rollback();
|
|
175
|
+
};
|
|
176
|
+
}
|
|
98
177
|
function patchManyEntries(queryClient, resource, idSet, data) {
|
|
99
178
|
const manys = queryClient.getQueriesData({
|
|
100
179
|
queryKey: manyKeyPrefix(resource)
|
|
@@ -125,6 +204,7 @@ function removeManyEntries(queryClient, resource, idSet) {
|
|
|
125
204
|
}
|
|
126
205
|
}
|
|
127
206
|
export {
|
|
207
|
+
patchListEntries,
|
|
128
208
|
patchManyEntries,
|
|
129
209
|
removeManyEntries,
|
|
130
210
|
removeRecordsFromCache,
|
|
@@ -16,17 +16,20 @@ interface OptimisticContext {
|
|
|
16
16
|
previous?: unknown;
|
|
17
17
|
key?: readonly unknown[];
|
|
18
18
|
manySnapshot?: ManySnapshot;
|
|
19
|
+
rollbackLists?: () => void;
|
|
19
20
|
}
|
|
20
21
|
export declare function createMutationOptions<TResources extends ResourceMap, TName extends keyof TResources & string>(ctx: DataContext<TResources>, resource: TName): {
|
|
21
22
|
mutationFn: (params: CreateParams<RecordOf<TResources, TName>>) => Promise<RecordOf<TResources, TName>>;
|
|
22
23
|
onSettled: () => Promise<void>;
|
|
23
24
|
};
|
|
24
25
|
export declare function updateMutationOptions<TResources extends ResourceMap, TName extends keyof TResources & string>(ctx: DataContext<TResources>, resource: TName, options?: MutationModeOptions): {
|
|
26
|
+
mutationKey: string[];
|
|
25
27
|
mutationFn: (params: UpdateParams<RecordOf<TResources, TName>>) => Promise<RecordOf<TResources, TName>>;
|
|
26
28
|
onMutate: (params: UpdateParams<RecordOf<TResources, TName>>) => Promise<{
|
|
27
29
|
previous?: undefined;
|
|
28
30
|
key?: undefined;
|
|
29
31
|
manySnapshot?: undefined;
|
|
32
|
+
rollbackLists?: undefined;
|
|
30
33
|
} | {
|
|
31
34
|
previous: RecordOf<TResources, TName> | undefined;
|
|
32
35
|
key: readonly [string, "getOne", {
|
|
@@ -37,9 +40,10 @@ export declare function updateMutationOptions<TResources extends ResourceMap, TN
|
|
|
37
40
|
meta: {} | null;
|
|
38
41
|
}];
|
|
39
42
|
manySnapshot: ManySnapshot;
|
|
43
|
+
rollbackLists: () => void;
|
|
40
44
|
}>;
|
|
41
45
|
onError: (_error: unknown, _params: UpdateParams<RecordOf<TResources, TName>>, context: OptimisticContext | undefined) => void;
|
|
42
|
-
onSettled: () => Promise<void
|
|
46
|
+
onSettled: () => Promise<void> | undefined;
|
|
43
47
|
};
|
|
44
48
|
export declare function deleteMutationOptions<TResources extends ResourceMap, TName extends keyof TResources & string>(ctx: DataContext<TResources>, resource: TName, options?: MutationModeOptions): {
|
|
45
49
|
mutationFn: (params: DeleteParams<RecordOf<TResources, TName>>) => Promise<RecordOf<TResources, TName>>;
|
|
@@ -1,8 +1,22 @@
|
|
|
1
|
-
import { removeManyEntries, patchManyEntries } from "./cache-ops.js";
|
|
1
|
+
import { removeManyEntries, patchManyEntries, patchListEntries } from "./cache-ops.js";
|
|
2
2
|
import { oneKey, resourceKey, manyKeyPrefix } from "./keys.js";
|
|
3
3
|
function invalidateResource(ctx, resource) {
|
|
4
4
|
return ctx.queryClient.invalidateQueries({ queryKey: resourceKey(resource) });
|
|
5
5
|
}
|
|
6
|
+
const unsettledUpdates = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
function countUpdate(queryClient, resource, by) {
|
|
8
|
+
const counts = unsettledUpdates.get(queryClient) ?? /* @__PURE__ */ new Map();
|
|
9
|
+
unsettledUpdates.set(queryClient, counts);
|
|
10
|
+
const next = (counts.get(resource) ?? 0) + by;
|
|
11
|
+
counts.set(resource, next);
|
|
12
|
+
return next;
|
|
13
|
+
}
|
|
14
|
+
function cancelListQueries(ctx, resource) {
|
|
15
|
+
return ctx.queryClient.cancelQueries({
|
|
16
|
+
queryKey: resourceKey(resource),
|
|
17
|
+
predicate: ({ queryKey }) => queryKey[1] === "getList" || queryKey[1] === "getManyReference" || queryKey[1] === "getInfiniteList"
|
|
18
|
+
});
|
|
19
|
+
}
|
|
6
20
|
async function snapshotManyEntries(ctx, resource) {
|
|
7
21
|
await ctx.queryClient.cancelQueries({ queryKey: manyKeyPrefix(resource) });
|
|
8
22
|
return ctx.queryClient.getQueriesData({ queryKey: manyKeyPrefix(resource) });
|
|
@@ -23,13 +37,16 @@ function createMutationOptions(ctx, resource) {
|
|
|
23
37
|
}
|
|
24
38
|
function updateMutationOptions(ctx, resource, options = {}) {
|
|
25
39
|
const mode = options.mutationMode ?? "pessimistic";
|
|
40
|
+
const mutationKey = [resource, "update"];
|
|
26
41
|
return {
|
|
42
|
+
mutationKey,
|
|
27
43
|
mutationFn: async (params) => {
|
|
28
44
|
const result = await ctx.dataProvider.update(resource, params);
|
|
29
45
|
return result.data;
|
|
30
46
|
},
|
|
31
47
|
onMutate: async (params) => {
|
|
32
48
|
if (mode !== "optimistic") return {};
|
|
49
|
+
countUpdate(ctx.queryClient, resource, 1);
|
|
33
50
|
const key = oneKey(resource, { id: params.id });
|
|
34
51
|
await ctx.queryClient.cancelQueries({ queryKey: key });
|
|
35
52
|
const previous = ctx.queryClient.getQueryData(key);
|
|
@@ -47,15 +64,28 @@ function updateMutationOptions(ctx, resource, options = {}) {
|
|
|
47
64
|
/* @__PURE__ */ new Set([String(params.id)]),
|
|
48
65
|
structuredClone(params.data)
|
|
49
66
|
);
|
|
50
|
-
|
|
67
|
+
await cancelListQueries(ctx, resource);
|
|
68
|
+
const rollbackLists = patchListEntries(
|
|
69
|
+
ctx.queryClient,
|
|
70
|
+
resource,
|
|
71
|
+
/* @__PURE__ */ new Set([String(params.id)]),
|
|
72
|
+
structuredClone(params.data)
|
|
73
|
+
);
|
|
74
|
+
return { previous, key, manySnapshot, rollbackLists };
|
|
51
75
|
},
|
|
52
76
|
onError: (_error, _params, context) => {
|
|
53
77
|
if (context?.key && context.previous !== void 0) {
|
|
54
78
|
ctx.queryClient.setQueryData(context.key, context.previous);
|
|
55
79
|
}
|
|
56
80
|
restoreManyEntries(ctx, context?.manySnapshot);
|
|
81
|
+
context?.rollbackLists?.();
|
|
57
82
|
},
|
|
58
|
-
onSettled: () =>
|
|
83
|
+
onSettled: () => {
|
|
84
|
+
if (mode === "optimistic" && countUpdate(ctx.queryClient, resource, -1) > 0) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
return invalidateResource(ctx, resource);
|
|
88
|
+
}
|
|
59
89
|
};
|
|
60
90
|
}
|
|
61
91
|
function deleteMutationOptions(ctx, resource, options = {}) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { LooseRecord } from "../resource/record.js";
|
|
1
2
|
import type { CheloneFormApi } from "./form-controller.js";
|
|
2
3
|
import { type FieldBinding, type FieldBindingOptions } from "./input-binding.js";
|
|
3
4
|
/**
|
|
@@ -27,7 +28,7 @@ export interface ArrayRow {
|
|
|
27
28
|
/** Moves THIS row to `to` — {@link ArrayBinding.reOrder} from the row's index. */
|
|
28
29
|
reOrder(to: number): void;
|
|
29
30
|
}
|
|
30
|
-
export interface ArrayBindingOptions extends Omit<FieldBindingOptions
|
|
31
|
+
export interface ArrayBindingOptions<TRecord = LooseRecord> extends Omit<FieldBindingOptions<TRecord>, "format" | "parse"> {
|
|
31
32
|
/**
|
|
32
33
|
* Seed rows for a value hole (ra precedence: an existing record value,
|
|
33
34
|
* including `[]`, always wins). Defaults to `[]` so positional ops always
|
|
@@ -51,7 +52,7 @@ export interface ArrayBindingOptions extends Omit<FieldBindingOptions, "format"
|
|
|
51
52
|
itemDefaults?: Record<string, unknown>;
|
|
52
53
|
}
|
|
53
54
|
/** `update()` payload: everything but the identity-defining source. */
|
|
54
|
-
export type ArrayBindingUpdateOptions = Omit<ArrayBindingOptions
|
|
55
|
+
export type ArrayBindingUpdateOptions = Omit<ArrayBindingOptions<LooseRecord>, "source">;
|
|
55
56
|
/**
|
|
56
57
|
* Vanilla array binding over a form-core array field — the useArrayInput
|
|
57
58
|
* substrate (T8 wraps it in React contexts).
|
|
@@ -128,4 +129,4 @@ export interface ArrayBinding<TRecord> {
|
|
|
128
129
|
*/
|
|
129
130
|
destroy(): void;
|
|
130
131
|
}
|
|
131
|
-
export declare function createArrayBinding<TRecord>(form: CheloneFormApi<TRecord>, options: ArrayBindingOptions): ArrayBinding<TRecord>;
|
|
132
|
+
export declare function createArrayBinding<TRecord>(form: CheloneFormApi<TRecord>, options: ArrayBindingOptions<TRecord>): ArrayBinding<TRecord>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type DeepKeys, type DeepValue, FieldApi, type FieldAsyncValidateOrFn, type FieldValidateOrFn, type StandardSchemaV1 } from "@tanstack/form-core";
|
|
2
|
+
import type { LooseRecord } from "../resource/record.js";
|
|
3
|
+
import type { SourcePath } from "../resource/source-path.js";
|
|
2
4
|
import type { AsyncValidator, CheloneFormApi, SyncValidator } from "./form-controller.js";
|
|
3
5
|
import { type Validator } from "./validators.js";
|
|
4
6
|
/**
|
|
@@ -55,9 +57,9 @@ export interface FieldValidatorMeta {
|
|
|
55
57
|
source: string;
|
|
56
58
|
[key: string]: unknown;
|
|
57
59
|
}
|
|
58
|
-
export interface FieldBindingOptions {
|
|
60
|
+
export interface FieldBindingOptions<TRecord = LooseRecord> {
|
|
59
61
|
/** The FINAL field path (already source-scoped — see `source.ts`). */
|
|
60
|
-
source:
|
|
62
|
+
source: SourcePath<TRecord>;
|
|
61
63
|
/**
|
|
62
64
|
* Input-level default, ra precedence: seeds ONLY into a hole (untouched
|
|
63
65
|
* field whose form value is `undefined`) — an existing record value,
|
|
@@ -81,7 +83,7 @@ export interface FieldBindingOptions {
|
|
|
81
83
|
validatorMeta?: Record<string, unknown>;
|
|
82
84
|
}
|
|
83
85
|
/** `update()` payload: everything but the identity-defining source. */
|
|
84
|
-
export type FieldBindingUpdateOptions = Omit<FieldBindingOptions
|
|
86
|
+
export type FieldBindingUpdateOptions = Omit<FieldBindingOptions<LooseRecord>, "source">;
|
|
85
87
|
/**
|
|
86
88
|
* Vanilla field binding over a single `@tanstack/form-core` FieldApi —
|
|
87
89
|
* the useInput substrate (T4 wraps it in React contexts).
|
|
@@ -137,5 +139,5 @@ export interface FieldBinding<TRecord> {
|
|
|
137
139
|
*/
|
|
138
140
|
destroy(): void;
|
|
139
141
|
}
|
|
140
|
-
export declare function createFieldBinding<TRecord>(form: CheloneFormApi<TRecord>, options: FieldBindingOptions): FieldBinding<TRecord>;
|
|
142
|
+
export declare function createFieldBinding<TRecord>(form: CheloneFormApi<TRecord>, options: FieldBindingOptions<TRecord>): FieldBinding<TRecord>;
|
|
141
143
|
export {};
|
package/dist/form/source.d.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* identical path/label composition.
|
|
6
6
|
*
|
|
7
7
|
* Paths follow form-core's OWN syntax: array indices use brackets
|
|
8
|
-
* (`items[0].sku`), object members use dots.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* (`items[0].sku`), object members use dots. A source written the other way
|
|
9
|
+
* (`items.0.sku`) is normalized to it — see {@link normalizeSource}. Labels
|
|
10
|
+
* are translation-key fragments and NEVER contain indices (`items.sku`), so
|
|
11
|
+
* one catalog entry covers every row.
|
|
11
12
|
*/
|
|
12
13
|
export interface SourceScope {
|
|
13
14
|
/** The final field path for a child source (`''` = the scope itself). */
|
|
@@ -20,7 +21,24 @@ export interface SourceScope {
|
|
|
20
21
|
* directly (form-core index syntax), everything else joins with a dot.
|
|
21
22
|
*/
|
|
22
23
|
export declare function composeSource(parent: string, child: string): string;
|
|
23
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Rewrite a dot-spelled array index as form-core's bracket spelling:
|
|
26
|
+
* `tags.0.label` -> `tags[0].label`. Without it an input written with a dot
|
|
27
|
+
* index binds a field named `tags.0.label`, which no validator, error or
|
|
28
|
+
* field group matches, while the same path read as data (a `DataTable.Col`)
|
|
29
|
+
* works — form-core's `getBy`/`setBy` accept both spellings, so only the
|
|
30
|
+
* FIELD NAME differs.
|
|
31
|
+
*
|
|
32
|
+
* Only a digits-only member that FOLLOWS another member is an index: a
|
|
33
|
+
* leading numeral (`0.a`) is a field name, and bracket spellings, non-numeric
|
|
34
|
+
* members and the empty string pass through untouched. Labels never go
|
|
35
|
+
* through this — translation keys keep the spelling as written.
|
|
36
|
+
*/
|
|
37
|
+
export declare function normalizeSource(source: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* What a top-level form provides: the identity scope for everything but a
|
|
40
|
+
* dot-spelled index, which it rewrites to form-core's bracket spelling.
|
|
41
|
+
*/
|
|
24
42
|
export declare const rootSourceScope: SourceScope;
|
|
25
43
|
/**
|
|
26
44
|
* The scope an array input provides to its iterator: sources nest under the
|
package/dist/form/source.js
CHANGED
|
@@ -3,14 +3,43 @@ function composeSource(parent, child) {
|
|
|
3
3
|
if (!parent) return child;
|
|
4
4
|
return child.startsWith("[") ? `${parent}${child}` : `${parent}.${child}`;
|
|
5
5
|
}
|
|
6
|
+
function normalizeSource(source) {
|
|
7
|
+
let out = "";
|
|
8
|
+
let depth = 0;
|
|
9
|
+
let afterMember = false;
|
|
10
|
+
for (let i = 0; i < source.length; i++) {
|
|
11
|
+
const char = source[i];
|
|
12
|
+
if (char === "[") depth++;
|
|
13
|
+
if (char === "]") depth--;
|
|
14
|
+
if (char === "." && depth === 0 && afterMember) {
|
|
15
|
+
let end = i + 1;
|
|
16
|
+
while (end < source.length && source[end] !== "." && source[end] !== "[") {
|
|
17
|
+
end++;
|
|
18
|
+
}
|
|
19
|
+
const member = source.slice(i + 1, end);
|
|
20
|
+
if (/^\d+$/.test(member)) {
|
|
21
|
+
out += `[${member}]`;
|
|
22
|
+
i = end - 1;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
out += char;
|
|
27
|
+
if (char !== ".") afterMember = true;
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
6
31
|
const rootSourceScope = {
|
|
7
|
-
getSource: (source) => source,
|
|
32
|
+
getSource: (source) => normalizeSource(source),
|
|
8
33
|
getLabel: (source) => source
|
|
9
34
|
};
|
|
10
35
|
function createArraySourceScope(parent, arraySource) {
|
|
11
36
|
const arrayPath = parent.getSource(arraySource);
|
|
12
37
|
return {
|
|
13
|
-
|
|
38
|
+
// Normalizing the JOIN, not just the child: a child of `0` composes to
|
|
39
|
+
// `tags.0`, where the index only follows a member once the array is there.
|
|
40
|
+
// Row and locale scopes need neither — they hand the whole path to a
|
|
41
|
+
// parent that normalizes it.
|
|
42
|
+
getSource: (source) => normalizeSource(composeSource(arrayPath, source)),
|
|
14
43
|
getLabel: (source) => parent.getLabel(composeSource(arraySource, source))
|
|
15
44
|
};
|
|
16
45
|
}
|
|
@@ -42,5 +71,6 @@ export {
|
|
|
42
71
|
createArraySourceScope,
|
|
43
72
|
createLocaleSourceScope,
|
|
44
73
|
createRowSourceScope,
|
|
74
|
+
normalizeSource,
|
|
45
75
|
rootSourceScope
|
|
46
76
|
};
|