@chelone/core 0.2.0 → 0.4.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.d.ts +5 -4
- package/dist/controller/kanban-board.d.ts +46 -0
- package/dist/controller/kanban-board.js +30 -0
- package/dist/controller/record-tree.d.ts +2 -1
- 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/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/resource/get-at-path.d.ts +3 -3
- package/dist/resource/get-at-path.js +2 -9
- package/dist/resource/registry.d.ts +16 -1
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SourcePath } from "../resource/source-path.js";
|
|
1
2
|
import { type CalendarRange } from "./calendar-dates.js";
|
|
2
3
|
export interface CalendarEvent<R> {
|
|
3
4
|
record: R;
|
|
@@ -7,10 +8,10 @@ export interface CalendarEvent<R> {
|
|
|
7
8
|
color?: string;
|
|
8
9
|
}
|
|
9
10
|
export interface ToCalendarEventsOptions<R> {
|
|
10
|
-
startSource:
|
|
11
|
-
endSource?:
|
|
12
|
-
titleSource?:
|
|
13
|
-
colorSource?:
|
|
11
|
+
startSource: SourcePath<R>;
|
|
12
|
+
endSource?: SourcePath<R>;
|
|
13
|
+
titleSource?: SourcePath<R>;
|
|
14
|
+
colorSource?: SourcePath<R>;
|
|
14
15
|
/** Maps the value at `colorSource` to a color. */
|
|
15
16
|
colorMap?: Record<string, string>;
|
|
16
17
|
/** The title when `titleSource` is unset or empty: the adapter fills it from the record representation. */
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rules behind a kanban board: which column holds which record, and what
|
|
3
|
+
* a drop writes back. No react, no vue: both `KanbanBoard` adapters build the
|
|
4
|
+
* same columns from the same records, and a drag handler that decides for
|
|
5
|
+
* itself lets the two boards disagree about the same drop.
|
|
6
|
+
*
|
|
7
|
+
* Drag events stay in the adapters, as `CalendarList` keeps its drag glue
|
|
8
|
+
* beside {@link moveCalendarEvent}.
|
|
9
|
+
*/
|
|
10
|
+
import type { Identifier } from "../resource/identifier.js";
|
|
11
|
+
import type { ResourceRecord } from "../resource/record.js";
|
|
12
|
+
import type { SourceKey } from "../resource/source-path.js";
|
|
13
|
+
export interface KanbanColumnDef {
|
|
14
|
+
/** Value written to `groupSource` by a drop into this column. */
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface KanbanBucket<R> {
|
|
20
|
+
column: KanbanColumnDef;
|
|
21
|
+
records: R[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The update a drop asks for, in the shape `update` takes: `data` is the patch
|
|
25
|
+
* and `previousData` the record as it was, so an adapter can undo the drop
|
|
26
|
+
* without a second read.
|
|
27
|
+
*/
|
|
28
|
+
export interface KanbanMove<R> {
|
|
29
|
+
id: Identifier;
|
|
30
|
+
data: Record<string, string>;
|
|
31
|
+
previousData: R;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One bucket per column, in column order, empty columns included. A record
|
|
35
|
+
* lands in the column whose id is its group key; a record that names no column
|
|
36
|
+
* is in no bucket, and records keep list order inside a bucket. A repeated
|
|
37
|
+
* column id keeps the first column's records and leaves the later one empty.
|
|
38
|
+
*/
|
|
39
|
+
export declare function buildKanbanBuckets<R extends ResourceRecord>(records: readonly R[], groupSource: SourceKey<R>, columns: readonly KanbanColumnDef[]): KanbanBucket<R>[];
|
|
40
|
+
/**
|
|
41
|
+
* The update a drop into `targetColumnId` asks for, or `null` when there is
|
|
42
|
+
* nothing to write: the target is not a column, or the record already sits in
|
|
43
|
+
* it. A drop that writes nothing still fires a data-provider `update` on every
|
|
44
|
+
* board that re-renders on a dropped drag, so the adapters ask here first.
|
|
45
|
+
*/
|
|
46
|
+
export declare function planKanbanMove<R extends ResourceRecord>(record: R, groupSource: SourceKey<R>, columns: readonly KanbanColumnDef[], targetColumnId: string): KanbanMove<R> | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function bucketKey(record, groupSource) {
|
|
2
|
+
const value = record[groupSource];
|
|
3
|
+
return String(value ?? "");
|
|
4
|
+
}
|
|
5
|
+
function buildKanbanBuckets(records, groupSource, columns) {
|
|
6
|
+
const buckets = columns.map((column) => ({
|
|
7
|
+
column,
|
|
8
|
+
records: []
|
|
9
|
+
}));
|
|
10
|
+
const bucketOf = /* @__PURE__ */ new Map();
|
|
11
|
+
columns.forEach((column, index) => {
|
|
12
|
+
if (!bucketOf.has(column.id)) bucketOf.set(column.id, index);
|
|
13
|
+
});
|
|
14
|
+
for (const record of records) {
|
|
15
|
+
const index = bucketOf.get(bucketKey(record, groupSource));
|
|
16
|
+
if (index === void 0) continue;
|
|
17
|
+
buckets[index].records.push(record);
|
|
18
|
+
}
|
|
19
|
+
return buckets;
|
|
20
|
+
}
|
|
21
|
+
function planKanbanMove(record, groupSource, columns, targetColumnId) {
|
|
22
|
+
if (!columns.some((column) => column.id === targetColumnId)) return null;
|
|
23
|
+
if (bucketKey(record, groupSource) === targetColumnId) return null;
|
|
24
|
+
const data = { [groupSource]: targetColumnId };
|
|
25
|
+
return { id: record.id, data, previousData: record };
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
buildKanbanBuckets,
|
|
29
|
+
planKanbanMove
|
|
30
|
+
};
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* records, and no record disappears when the data holds a parent cycle.
|
|
5
5
|
*/
|
|
6
6
|
import type { Identifier } from "../resource/identifier.js";
|
|
7
|
+
import type { SourcePath } from "../resource/source-path.js";
|
|
7
8
|
export interface RecordTreeNode<R> {
|
|
8
9
|
record: R;
|
|
9
10
|
children: RecordTreeNode<R>[];
|
|
@@ -17,4 +18,4 @@ export interface RecordTreeNode<R> {
|
|
|
17
18
|
*/
|
|
18
19
|
export declare function buildRecordTree<R extends {
|
|
19
20
|
id: Identifier;
|
|
20
|
-
}>(records: readonly R[], parentSource:
|
|
21
|
+
}>(records: readonly R[], parentSource: SourcePath<R>): RecordTreeNode<R>[];
|
|
@@ -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/index.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export { arrangeColumns, columnLabelMatches, dataTableColumnKeys, defaultDataTab
|
|
|
21
21
|
export { isFilterSelected, toggleFilterValue, } from "./controller/filter-toggle.js";
|
|
22
22
|
export type { InfiniteListController, InfiniteListControllerState, } from "./controller/infinite-list-controller.js";
|
|
23
23
|
export { createInfiniteListController } from "./controller/infinite-list-controller.js";
|
|
24
|
+
export type { KanbanBucket, KanbanColumnDef, KanbanMove, } from "./controller/kanban-board.js";
|
|
25
|
+
export { buildKanbanBuckets, planKanbanMove, } from "./controller/kanban-board.js";
|
|
24
26
|
export type { ListController, ListControllerContext, ListControllerOptions, ListControllerState, ListParamsPatch, } from "./controller/list-controller.js";
|
|
25
27
|
export { createListController, listControllerActions, } from "./controller/list-controller.js";
|
|
26
28
|
export type { ListParamsQuery, ListUrlState, } from "./controller/list-params.js";
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createClientListController } from "./controller/client-list-controller.
|
|
|
10
10
|
import { arrangeColumns, columnLabelMatches, dataTableColumnKeys, defaultDataTableStoreKey, getDataTableColumnId, moveColumn, toggleHiddenColumn } from "./controller/data-table-columns.js";
|
|
11
11
|
import { isFilterSelected, toggleFilterValue } from "./controller/filter-toggle.js";
|
|
12
12
|
import { createInfiniteListController } from "./controller/infinite-list-controller.js";
|
|
13
|
+
import { buildKanbanBuckets, planKanbanMove } from "./controller/kanban-board.js";
|
|
13
14
|
import { createListController, listControllerActions } from "./controller/list-controller.js";
|
|
14
15
|
import { hasCustomParams, listPatchFromSearch, parseListParams, serializeListParams, serializeListState } from "./controller/list-params.js";
|
|
15
16
|
import { canUseCachedPage, getPrevNextParams, resolvePrevNext } from "./controller/prev-next.js";
|
|
@@ -117,6 +118,7 @@ export {
|
|
|
117
118
|
authStateQueryOptions,
|
|
118
119
|
bboxToPolygon,
|
|
119
120
|
buildApprovalUpdate,
|
|
121
|
+
buildKanbanBuckets,
|
|
120
122
|
buildOverpassQuery,
|
|
121
123
|
buildRecordTree,
|
|
122
124
|
calendarDayKey,
|
|
@@ -290,6 +292,7 @@ export {
|
|
|
290
292
|
parseLocation,
|
|
291
293
|
patchedRecord,
|
|
292
294
|
permissionsQueryOptions,
|
|
295
|
+
planKanbanMove,
|
|
293
296
|
polygonToBBox,
|
|
294
297
|
presenceTopic,
|
|
295
298
|
rasterTileStyle,
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* bracket indices (`a[0].b`) and numeric-dot indices (`a.0.b`) all resolve to
|
|
4
4
|
* the same value, matching `lodash/get`'s relevant semantics.
|
|
5
5
|
*
|
|
6
|
-
* The core tree, calendar and client-list readers read through this, so
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
6
|
+
* The core tree, calendar and client-list readers read through this, and so do
|
|
7
|
+
* both adapters' `useFieldValue` and `useGetRecordRepresentation`: a `source`
|
|
8
|
+
* written one way reads the same record everywhere, and one walker is left.
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
11
|
* Read the value at `source` off `record`. An empty `source` returns the record
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
const PATH_SEGMENT = /\[([^\]]*)\]|[^.[\]]+/g;
|
|
2
|
-
const toPathSegments = (source) => {
|
|
3
|
-
const segments = [];
|
|
4
|
-
for (const match of source.matchAll(PATH_SEGMENT)) {
|
|
5
|
-
segments.push(match[1] !== void 0 ? match[1] : match[0]);
|
|
6
|
-
}
|
|
7
|
-
return segments;
|
|
8
|
-
};
|
|
9
2
|
function getAtPath(record, source) {
|
|
10
3
|
if (source === "") return record;
|
|
11
4
|
if (record !== null && typeof record === "object" && Object.hasOwn(record, source)) {
|
|
12
5
|
return record[source];
|
|
13
6
|
}
|
|
14
7
|
let current = record;
|
|
15
|
-
for (const
|
|
8
|
+
for (const match of source.matchAll(PATH_SEGMENT)) {
|
|
16
9
|
if (current === null || typeof current !== "object") return void 0;
|
|
17
|
-
current = current[
|
|
10
|
+
current = current[match[1] !== void 0 ? match[1] : match[0]];
|
|
18
11
|
}
|
|
19
12
|
return current;
|
|
20
13
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { StandardSchemaV1 } from "@tanstack/form-core";
|
|
2
2
|
import type { LooseRecord } from "./record.js";
|
|
3
|
+
import type { SourceKey } from "./source-path.js";
|
|
3
4
|
/** Per-resource configuration: an optional Standard Schema (Zod 3, Zod 4, Valibot, ...) and the references its fields hold. */
|
|
4
5
|
export interface ResourceConfig<TSchema extends StandardSchemaV1 = StandardSchemaV1> {
|
|
5
6
|
schema?: TSchema;
|
|
@@ -20,7 +21,20 @@ export type ResourceMap = {
|
|
|
20
21
|
* `defineResources({ posts: { schema: postSchema } })` keeps `"posts"` literal
|
|
21
22
|
* and `typeof postSchema` exact, enabling end-to-end inference.
|
|
22
23
|
*/
|
|
23
|
-
export declare function defineResources<const T extends ResourceMap>(resources: T): T;
|
|
24
|
+
export declare function defineResources<const T extends ResourceMap>(resources: T & Checked<T>): T;
|
|
25
|
+
/**
|
|
26
|
+
* Narrows each resource's `references` to keys that are top-level fields of its
|
|
27
|
+
* record and values that are registered resource names. A mapped type over the
|
|
28
|
+
* keys actually written is what makes a misspelled key an error: an optional
|
|
29
|
+
* `{ [K in SourceKey]?: ... }` intersection lets inference widen `T` instead.
|
|
30
|
+
*/
|
|
31
|
+
type Checked<T extends ResourceMap> = {
|
|
32
|
+
[N in keyof T & string]: {
|
|
33
|
+
references?: {
|
|
34
|
+
[K in keyof NonNullable<T[N]["references"]>]: K extends SourceKey<RecordOf<T, N>> ? keyof T & string : never;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
};
|
|
24
38
|
/**
|
|
25
39
|
* The record type for a given resource name in a registry:
|
|
26
40
|
* the schema's Standard Schema output type when a schema is present, else `LooseRecord`.
|
|
@@ -28,3 +42,4 @@ export declare function defineResources<const T extends ResourceMap>(resources:
|
|
|
28
42
|
export type RecordOf<TResources extends ResourceMap, TName extends keyof TResources> = TResources[TName] extends {
|
|
29
43
|
schema: infer S;
|
|
30
44
|
} ? S extends StandardSchemaV1<unknown, infer Output> ? Output : LooseRecord : LooseRecord;
|
|
45
|
+
export {};
|