@chelone/core 0.1.0 → 0.2.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/list-controller.d.ts +22 -0
- package/dist/controller/list-controller.js +18 -0
- package/dist/controller/record-tree.js +2 -5
- package/dist/data/undoable.d.ts +50 -0
- package/dist/data/undoable.js +66 -6
- 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 +7 -3
- package/dist/index.js +11 -2
- package/dist/resource/get-at-path.d.ts +18 -0
- package/dist/resource/get-at-path.js +23 -0
- package/dist/resource/register.d.ts +26 -0
- 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;
|
|
@@ -39,6 +39,8 @@ export interface ListParamsPatch {
|
|
|
39
39
|
page?: number;
|
|
40
40
|
}
|
|
41
41
|
export interface ListController<TRecord> extends MicroStore<ListControllerState<TRecord>> {
|
|
42
|
+
/** The `perPage` the list starts at when nothing is persisted or in the URL. Optional so other list controllers and fakes need not set it. */
|
|
43
|
+
readonly defaultPerPage?: number;
|
|
42
44
|
setPage(page: number): void;
|
|
43
45
|
setPerPage(perPage: number): void;
|
|
44
46
|
/** Single payload on the SAME single-sorted field toggles order; anything else replaces. */
|
|
@@ -136,4 +138,24 @@ export declare function resetPageTarget(s: Pick<ListControllerState<unknown>, "p
|
|
|
136
138
|
* action path and are guarded to run only on SETTLED results, so they
|
|
137
139
|
* cannot ping-pong.
|
|
138
140
|
*/
|
|
141
|
+
/**
|
|
142
|
+
* The actions of a list controller, for an adapter to spread next to its own
|
|
143
|
+
* reactive state. They are the same functions across state changes.
|
|
144
|
+
*/
|
|
145
|
+
export declare function listControllerActions(controller: Omit<ListController<unknown>, keyof MicroStore<ListControllerState<unknown>> | "defaultPerPage">): {
|
|
146
|
+
setPage: (page: number) => void;
|
|
147
|
+
setPerPage: (perPage: number) => void;
|
|
148
|
+
setSort: (sort: SortPayload | SortPayload[]) => void;
|
|
149
|
+
setFilters: (filter: FilterPayload, displayedFilters?: Record<string, boolean>, debounced?: boolean) => void;
|
|
150
|
+
setParams: (patch: ListParamsPatch) => void;
|
|
151
|
+
showFilter: (name: string, defaultValue?: unknown) => void;
|
|
152
|
+
hideFilter: (name: string) => void;
|
|
153
|
+
select: (ids: Identifier[]) => void;
|
|
154
|
+
toggleItem: (id: Identifier) => void;
|
|
155
|
+
unselectAll: () => void;
|
|
156
|
+
selectAll: (options?: {
|
|
157
|
+
limit?: number;
|
|
158
|
+
}) => Promise<void>;
|
|
159
|
+
refetch: () => void;
|
|
160
|
+
};
|
|
139
161
|
export declare function createListController<TResources extends ResourceMap, TName extends keyof TResources & string>(ctx: ListControllerContext<TResources>, resource: TName, options?: ListControllerOptions): ListController<RecordOf<TResources, TName>>;
|
|
@@ -121,6 +121,22 @@ function resetPageTarget(s) {
|
|
|
121
121
|
if (s.page > 1 && s.data !== void 0 && s.data.length === 0) return 1;
|
|
122
122
|
return void 0;
|
|
123
123
|
}
|
|
124
|
+
function listControllerActions(controller) {
|
|
125
|
+
return {
|
|
126
|
+
setPage: controller.setPage,
|
|
127
|
+
setPerPage: controller.setPerPage,
|
|
128
|
+
setSort: controller.setSort,
|
|
129
|
+
setFilters: controller.setFilters,
|
|
130
|
+
setParams: controller.setParams,
|
|
131
|
+
showFilter: controller.showFilter,
|
|
132
|
+
hideFilter: controller.hideFilter,
|
|
133
|
+
select: controller.select,
|
|
134
|
+
toggleItem: controller.toggleItem,
|
|
135
|
+
unselectAll: controller.unselectAll,
|
|
136
|
+
selectAll: controller.selectAll,
|
|
137
|
+
refetch: controller.refetch
|
|
138
|
+
};
|
|
139
|
+
}
|
|
124
140
|
function createListController(ctx, resource, options = {}) {
|
|
125
141
|
const storeKey = options.storeKey === false ? null : options.storeKey ?? resource;
|
|
126
142
|
const paramsKey = storeKey ? `${storeKey}.listParams` : null;
|
|
@@ -377,6 +393,7 @@ function createListController(ctx, resource, options = {}) {
|
|
|
377
393
|
}
|
|
378
394
|
return {
|
|
379
395
|
...store,
|
|
396
|
+
defaultPerPage: defaults.perPage,
|
|
380
397
|
setPage,
|
|
381
398
|
setPerPage,
|
|
382
399
|
setSort,
|
|
@@ -397,6 +414,7 @@ export {
|
|
|
397
414
|
createErrorAnnouncer,
|
|
398
415
|
createListController,
|
|
399
416
|
errorMessageKey,
|
|
417
|
+
listControllerActions,
|
|
400
418
|
listParamsFromPatch,
|
|
401
419
|
notifyFetchError,
|
|
402
420
|
notifySelectAllLimit,
|
|
@@ -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,
|
package/dist/data/undoable.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { type QueryClient, type QueryKey } from "@tanstack/query-core";
|
|
2
|
+
import type { Identifier } from "../resource/identifier.js";
|
|
3
|
+
import type { ResourceRecord } from "../resource/record.js";
|
|
4
|
+
import type { DeleteParams, UpdateManyParams, UpdateParams } from "./types.js";
|
|
2
5
|
/**
|
|
3
6
|
* A snapshot of the query cache taken BEFORE an optimistic apply: an array of
|
|
4
7
|
* `[query key, data-at-that-key]` tuples. Rolling back writes each value back.
|
|
@@ -57,6 +60,53 @@ export interface UndoableHandle<TResult = unknown> {
|
|
|
57
60
|
* `commit()` settle it. React/Vue-free — pure query-core.
|
|
58
61
|
*/
|
|
59
62
|
export declare function startUndoableMutation<TResult = unknown>(config: UndoableMutationConfig<TResult>): UndoableHandle<TResult>;
|
|
63
|
+
/**
|
|
64
|
+
* The callbacks an undoable `mutate` takes as its second argument. They are
|
|
65
|
+
* not TanStack Query's: the server call is deferred, so `onSuccess` fires with
|
|
66
|
+
* the optimistic result and `onError`/`onSettled` follow the deferred commit.
|
|
67
|
+
*/
|
|
68
|
+
export interface UndoableCallbacks<TData, TParams> {
|
|
69
|
+
/** Runs right after the optimistic cache change, not after the undo window. */
|
|
70
|
+
onSuccess?: (data: TData, params: TParams) => void;
|
|
71
|
+
/** Runs when the deferred commit fails, after the rollback and error notification. */
|
|
72
|
+
onError?: (error: unknown, params: TParams) => void;
|
|
73
|
+
/** Runs once the commit ends, whether it succeeded or failed. Never after an Undo. */
|
|
74
|
+
onSettled?: (params: TParams) => void;
|
|
75
|
+
}
|
|
76
|
+
/** What an undoable `useUpdate` hands `onSuccess`: the patched record. */
|
|
77
|
+
export type UndoableUpdateCallbacks<TRecord extends ResourceRecord> = UndoableCallbacks<TRecord, UpdateParams<TRecord>>;
|
|
78
|
+
/** What an undoable `useDelete` hands `onSuccess`: the previous record, when given. */
|
|
79
|
+
export type UndoableDeleteCallbacks<TRecord extends ResourceRecord> = UndoableCallbacks<TRecord | undefined, DeleteParams<TRecord>>;
|
|
80
|
+
/** What an undoable `useUpdateMany` hands `onSuccess`: the ids. */
|
|
81
|
+
export type UndoableUpdateManyCallbacks<TRecord extends ResourceRecord> = UndoableCallbacks<Identifier[], UpdateManyParams<TRecord>>;
|
|
82
|
+
/** The calls an undoable `mutate` makes for its callbacks; see {@link undoableCallbacks}. */
|
|
83
|
+
export interface UndoableSettler<TData> {
|
|
84
|
+
/** Wraps the deferred `commit` so `onError` and `onSettled` run when it ends. */
|
|
85
|
+
commit<TResult>(commit: () => Promise<TResult>): () => Promise<TResult>;
|
|
86
|
+
/** Runs `onSuccess`: call it once the optimistic change is applied. */
|
|
87
|
+
success(data: TData): void;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Bind an undoable `mutate`'s callbacks to its `params`. The wrapped commit
|
|
91
|
+
* rethrows a failure, so the handle still rolls back and the adapter still
|
|
92
|
+
* notifies; the callbacks wait a macrotask to run after both.
|
|
93
|
+
*/
|
|
94
|
+
export declare function undoableCallbacks<TData, TParams>(params: TParams, callbacks?: UndoableCallbacks<TData, TParams>): UndoableSettler<TData>;
|
|
95
|
+
/** What an adapter's `runUndoable` needs besides the deferred `commit`. */
|
|
96
|
+
export interface UndoableCacheOptions {
|
|
97
|
+
queryKeys: QueryKey[];
|
|
98
|
+
applyOptimistic: () => void;
|
|
99
|
+
message: string;
|
|
100
|
+
messageArgs: {
|
|
101
|
+
smart_count: number;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Patch `ids` in the `resource` caches now; the notification reads "updated". */
|
|
105
|
+
export declare function undoableUpdateOptions(queryClient: QueryClient, resource: string, ids: Identifier[], data: object): UndoableCacheOptions;
|
|
106
|
+
/** Remove `ids` from the `resource` caches now; the notification reads "deleted". */
|
|
107
|
+
export declare function undoableDeleteOptions(queryClient: QueryClient, resource: string, ids: Identifier[]): UndoableCacheOptions;
|
|
108
|
+
/** The record an undoable update shows at once: the previous one with the patch on top. */
|
|
109
|
+
export declare function patchedRecord<TRecord extends ResourceRecord>(params: UpdateParams<TRecord>): TRecord;
|
|
60
110
|
/** True while at least one undoable mutation is still awaiting commit/undo. */
|
|
61
111
|
export declare function hasPendingUndoables(): boolean;
|
|
62
112
|
/** True while an undoable is pending or its committed server call is still in flight. */
|
package/dist/data/undoable.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { partialMatchKey } from "@tanstack/query-core";
|
|
2
|
+
import { removeRecordsFromCache, updateRecordsInCache } from "./cache-ops.js";
|
|
2
3
|
const pendingUndoables = /* @__PURE__ */ new Set();
|
|
3
4
|
const settledListeners = /* @__PURE__ */ new Set();
|
|
4
5
|
let inFlightCommits = 0;
|
|
@@ -23,10 +24,10 @@ function keysOverlap(a, b) {
|
|
|
23
24
|
(x) => b.some((y) => partialMatchKey(x, y) || partialMatchKey(y, x))
|
|
24
25
|
);
|
|
25
26
|
}
|
|
26
|
-
function entriesToReplay(
|
|
27
|
+
function entriesToReplay(later2, entry) {
|
|
27
28
|
const affectedKeys = [...entry.queryKeys];
|
|
28
29
|
const affected = [];
|
|
29
|
-
for (const other of
|
|
30
|
+
for (const other of later2) {
|
|
30
31
|
if (!keysOverlap(affectedKeys, other.queryKeys)) continue;
|
|
31
32
|
affected.push(other);
|
|
32
33
|
affectedKeys.push(...other.queryKeys);
|
|
@@ -59,12 +60,12 @@ function startUndoableMutation(config) {
|
|
|
59
60
|
const rollback = () => {
|
|
60
61
|
const open = order;
|
|
61
62
|
const index = open.indexOf(entry);
|
|
62
|
-
const
|
|
63
|
+
const later2 = entriesToReplay(open.slice(index + 1), entry);
|
|
63
64
|
open.splice(index, 1);
|
|
64
|
-
for (const other of [...
|
|
65
|
+
for (const other of [...later2].reverse())
|
|
65
66
|
restore(queryClient, other.snapshot);
|
|
66
67
|
restore(queryClient, entry.snapshot);
|
|
67
|
-
for (const other of
|
|
68
|
+
for (const other of later2) {
|
|
68
69
|
other.snapshot = takeSnapshot(queryClient, other.queryKeys);
|
|
69
70
|
other.applyOptimistic();
|
|
70
71
|
}
|
|
@@ -111,6 +112,61 @@ function startUndoableMutation(config) {
|
|
|
111
112
|
pendingUndoables.add(handle);
|
|
112
113
|
return handle;
|
|
113
114
|
}
|
|
115
|
+
function later(run) {
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
try {
|
|
118
|
+
run();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error(error);
|
|
121
|
+
}
|
|
122
|
+
}, 0);
|
|
123
|
+
}
|
|
124
|
+
function undoableCallbacks(params, callbacks = {}) {
|
|
125
|
+
const { onSuccess, onError, onSettled } = callbacks;
|
|
126
|
+
return {
|
|
127
|
+
success: (data) => onSuccess?.(data, params),
|
|
128
|
+
// The callbacks run in a timer, never inside the commit's promise: one that
|
|
129
|
+
// throws must not read as a failed commit and roll the cache back.
|
|
130
|
+
commit: (commit) => async () => {
|
|
131
|
+
let result;
|
|
132
|
+
try {
|
|
133
|
+
result = await commit();
|
|
134
|
+
} catch (error) {
|
|
135
|
+
later(() => {
|
|
136
|
+
onError?.(error, params);
|
|
137
|
+
onSettled?.(params);
|
|
138
|
+
});
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
later(() => onSettled?.(params));
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function undoableUpdateOptions(queryClient, resource, ids, data) {
|
|
147
|
+
return {
|
|
148
|
+
queryKeys: [[resource]],
|
|
149
|
+
applyOptimistic: () => updateRecordsInCache(
|
|
150
|
+
queryClient,
|
|
151
|
+
resource,
|
|
152
|
+
ids,
|
|
153
|
+
data
|
|
154
|
+
),
|
|
155
|
+
message: "chelone.notification.updated",
|
|
156
|
+
messageArgs: { smart_count: ids.length }
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function undoableDeleteOptions(queryClient, resource, ids) {
|
|
160
|
+
return {
|
|
161
|
+
queryKeys: [[resource]],
|
|
162
|
+
applyOptimistic: () => removeRecordsFromCache(queryClient, resource, ids),
|
|
163
|
+
message: "chelone.notification.deleted",
|
|
164
|
+
messageArgs: { smart_count: ids.length }
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function patchedRecord(params) {
|
|
168
|
+
return { ...params.previousData, ...params.data, id: params.id };
|
|
169
|
+
}
|
|
114
170
|
function hasPendingUndoables() {
|
|
115
171
|
return pendingUndoables.size > 0;
|
|
116
172
|
}
|
|
@@ -132,5 +188,9 @@ export {
|
|
|
132
188
|
hasPendingUndoables,
|
|
133
189
|
hasUnsettledUndoables,
|
|
134
190
|
onUndoablesSettled,
|
|
135
|
-
|
|
191
|
+
patchedRecord,
|
|
192
|
+
startUndoableMutation,
|
|
193
|
+
undoableCallbacks,
|
|
194
|
+
undoableDeleteOptions,
|
|
195
|
+
undoableUpdateOptions
|
|
136
196
|
};
|
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
|
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The English catalog an `<Admin>` with no `i18n` prop reads from, and the base
|
|
3
|
+
* an app extends: spread it, then add or override entries.
|
|
4
|
+
*
|
|
5
|
+
* The ported shadcn components translate `ra.*` keys (the react-admin namespace
|
|
6
|
+
* they were ported from): page titles, list pagination, the bulk-action bar,
|
|
7
|
+
* menu a11y labels, boolean fields. Chelone's i18n never throws on a missing key
|
|
8
|
+
* (it returns the key or a `_` fallback), so a partial catalog shows raw
|
|
9
|
+
* `ra.page.list`-style keys.
|
|
10
|
+
*
|
|
11
|
+
* Interpolation is single-brace `{name}` (not react-admin's `%{name}`). Plural
|
|
12
|
+
* strings are `{ one, other }` objects keyed by Intl.PluralRules categories
|
|
13
|
+
* (react-admin's `a |||| b` form).
|
|
14
|
+
*
|
|
15
|
+
* `defaultMessages` (the `chelone.*` keys the core controllers emit) and
|
|
16
|
+
* `mapMessages` (the maps add-on's labels) sit underneath.
|
|
17
|
+
*/
|
|
18
|
+
export declare const englishMessages: {
|
|
19
|
+
readonly "ra.page.list": "{name}";
|
|
20
|
+
readonly "ra.page.create": "Create {name}";
|
|
21
|
+
readonly "ra.page.edit": "{name} {recordRepresentation}";
|
|
22
|
+
readonly "ra.page.show": "{name} {recordRepresentation}";
|
|
23
|
+
readonly "ra.page.dashboard": "Dashboard";
|
|
24
|
+
readonly "ra.page.error": "Something went wrong";
|
|
25
|
+
readonly "ra.page.not_found": "Not Found";
|
|
26
|
+
readonly "ra.page.loading": "Loading";
|
|
27
|
+
readonly "ra.page.empty": "No {name} yet.";
|
|
28
|
+
readonly "ra.page.access_denied": "Access denied";
|
|
29
|
+
readonly "ra.page.authentication_error": "Authentication error";
|
|
30
|
+
readonly "ra.action.add": "Add";
|
|
31
|
+
readonly "ra.action.add_filter": "Add filter";
|
|
32
|
+
readonly "ra.action.bulk_actions": {
|
|
33
|
+
readonly one: "1 item selected";
|
|
34
|
+
readonly other: "{count} items selected";
|
|
35
|
+
};
|
|
36
|
+
readonly "ra.action.cancel": "Cancel";
|
|
37
|
+
readonly "ra.action.clear_input_value": "Clear value";
|
|
38
|
+
readonly "ra.action.clear_search": "Clear search";
|
|
39
|
+
readonly "ra.action.close": "Close";
|
|
40
|
+
readonly "ra.action.close_menu": "Close menu";
|
|
41
|
+
readonly "ra.action.confirm": "Confirm";
|
|
42
|
+
readonly "ra.action.create": "Create";
|
|
43
|
+
readonly "ra.action.delete": "Delete";
|
|
44
|
+
readonly "ra.action.edit": "Edit";
|
|
45
|
+
readonly "ra.action.export": "Export";
|
|
46
|
+
readonly "ra.action.list": "List";
|
|
47
|
+
readonly "ra.action.open_menu": "Open menu";
|
|
48
|
+
readonly "ra.action.refresh": "Refresh";
|
|
49
|
+
readonly "ra.action.remove": "Remove";
|
|
50
|
+
readonly "ra.action.remove_filter": "Remove this filter";
|
|
51
|
+
readonly "ra.action.save": "Save";
|
|
52
|
+
readonly "ra.action.search": "Search";
|
|
53
|
+
readonly "ra.action.select_all": "Select all";
|
|
54
|
+
readonly "ra.action.select_row": "Select this row";
|
|
55
|
+
readonly "ra.action.show": "Show";
|
|
56
|
+
readonly "ra.action.sort": "Sort";
|
|
57
|
+
readonly "ra.action.toggle_theme": "Toggle light/dark mode";
|
|
58
|
+
readonly "ra.action.undo": "Undo";
|
|
59
|
+
readonly "ra.action.unselect": "Unselect";
|
|
60
|
+
readonly "ra.action.update": "Update";
|
|
61
|
+
readonly "ra.boolean.true": "Yes";
|
|
62
|
+
readonly "ra.boolean.false": "No";
|
|
63
|
+
readonly "ra.boolean.null": " ";
|
|
64
|
+
readonly "ra.auth.logout": "Logout";
|
|
65
|
+
readonly "ra.auth.sign_in": "Sign in";
|
|
66
|
+
readonly "ra.auth.sign_in_error": "Authentication failed, please retry";
|
|
67
|
+
readonly "ra.auth.user_menu": "Profile";
|
|
68
|
+
readonly "ra.auth.username": "Username";
|
|
69
|
+
readonly "ra.auth.password": "Password";
|
|
70
|
+
readonly "ra.auth.auth_check_error": "Please login to continue";
|
|
71
|
+
readonly "ra.message.access_denied": "You don't have the right permissions to access this page";
|
|
72
|
+
readonly "ra.message.auth_error": "An error occurred while validating the authentication token.";
|
|
73
|
+
readonly "ra.message.authentication_error": "The authentication server returned an error and your credentials could not be checked.";
|
|
74
|
+
readonly "ra.message.error": "A client error occurred and your request couldn't be completed.";
|
|
75
|
+
readonly "ra.message.invalid_form": "The form is not valid. Please check for errors";
|
|
76
|
+
readonly "ra.message.loading": "Please wait";
|
|
77
|
+
readonly "ra.message.not_found": "Either you typed a wrong URL, or you followed a bad link.";
|
|
78
|
+
readonly "ra.navigation.next": "Go to next page";
|
|
79
|
+
readonly "ra.navigation.previous": "Go to previous page";
|
|
80
|
+
readonly "ra.navigation.first": "Go to first page";
|
|
81
|
+
readonly "ra.navigation.last": "Go to last page";
|
|
82
|
+
readonly "ra.navigation.no_results": "No results found";
|
|
83
|
+
readonly "ra.navigation.no_filtered_results": "No results found with the current filters.";
|
|
84
|
+
readonly "ra.navigation.page_range_info": "{offsetBegin}-{offsetEnd} of {total}";
|
|
85
|
+
readonly "ra.navigation.partial_page_range_info": "{offsetBegin}-{offsetEnd} of more than {offsetEnd}";
|
|
86
|
+
readonly "ra.navigation.page_rows_per_page": "Rows per page:";
|
|
87
|
+
readonly "ra.navigation.current_page": "Page {page}";
|
|
88
|
+
readonly "ra.navigation.skip_nav": "Skip to content";
|
|
89
|
+
readonly "ra.input.file.upload_several": "Drop some files to upload, or click to select one.";
|
|
90
|
+
readonly "ra.input.file.upload_single": "Drop a file to upload, or click to select it.";
|
|
91
|
+
readonly "ra.input.image.upload_several": "Drop some pictures to upload, or click to select one.";
|
|
92
|
+
readonly "ra.input.image.upload_single": "Drop a picture to upload, or click to select it.";
|
|
93
|
+
readonly "ra.input.password.toggle_visible": "Hide password";
|
|
94
|
+
readonly "ra.input.password.toggle_hidden": "Show password";
|
|
95
|
+
readonly "ra.theme.light": "Light";
|
|
96
|
+
readonly "ra.theme.dark": "Dark";
|
|
97
|
+
readonly "ra.theme.system": "System";
|
|
98
|
+
readonly "ra.validation.required": "Required";
|
|
99
|
+
readonly "ra.validation.minLength": "Must be {min} characters at least";
|
|
100
|
+
readonly "ra.validation.maxLength": "Must be {max} characters or less";
|
|
101
|
+
readonly "ra.validation.minValue": "Must be at least {min}";
|
|
102
|
+
readonly "ra.validation.maxValue": "Must be {max} or less";
|
|
103
|
+
readonly "ra.validation.number": "Must be a number";
|
|
104
|
+
readonly "ra.validation.email": "Must be a valid email";
|
|
105
|
+
readonly "ra.action.back": "Back";
|
|
106
|
+
readonly "ra.action.bulk_edit_apply": {
|
|
107
|
+
readonly one: "Apply to 1 record";
|
|
108
|
+
readonly other: "Apply to {count} records";
|
|
109
|
+
};
|
|
110
|
+
readonly "ra.action.change_locale": "Change locale";
|
|
111
|
+
readonly "ra.action.clear_array_input": "Clear the array";
|
|
112
|
+
readonly "ra.action.clone": "Clone";
|
|
113
|
+
readonly "ra.action.collapse": "Collapse";
|
|
114
|
+
readonly "ra.action.expand": "Expand";
|
|
115
|
+
readonly "ra.action.move_column_down": "Move down";
|
|
116
|
+
readonly "ra.action.move_column_up": "Move up";
|
|
117
|
+
readonly "ra.action.move_down": "Move down";
|
|
118
|
+
readonly "ra.action.move_up": "Move up";
|
|
119
|
+
readonly "ra.action.reload": "Reload";
|
|
120
|
+
readonly "ra.action.remove_all_filters": "Remove all filters";
|
|
121
|
+
readonly "ra.action.reset": "Reset";
|
|
122
|
+
readonly "ra.action.reset_columns": "Reset";
|
|
123
|
+
readonly "ra.action.search_columns": "Search columns";
|
|
124
|
+
readonly "ra.action.select_columns": "Columns";
|
|
125
|
+
readonly "ra.auth.or": "or";
|
|
126
|
+
readonly "ra.calendar.agenda": "Agenda";
|
|
127
|
+
readonly "ra.calendar.month": "Month";
|
|
128
|
+
readonly "ra.calendar.week": "Week";
|
|
129
|
+
readonly "ra.calendar.next": "Next";
|
|
130
|
+
readonly "ra.calendar.previous": "Previous";
|
|
131
|
+
readonly "ra.calendar.today": "Today";
|
|
132
|
+
readonly "ra.calendar.view": "View";
|
|
133
|
+
readonly "ra.calendar.no_events": "No events";
|
|
134
|
+
readonly "ra.csv_import.back": "Back";
|
|
135
|
+
readonly "ra.csv_import.close": "Close";
|
|
136
|
+
readonly "ra.csv_import.complete": "Import complete";
|
|
137
|
+
readonly "ra.csv_import.counts": "{created} created · {failed} failed · {total} total";
|
|
138
|
+
readonly "ra.csv_import.download_errors": "Download error report";
|
|
139
|
+
readonly "ra.csv_import.drop": "Drop a CSV file here, or click to choose one";
|
|
140
|
+
readonly "ra.csv_import.import": "Import";
|
|
141
|
+
readonly "ra.csv_import.issues": "Issues";
|
|
142
|
+
readonly "ra.csv_import.next": "Next";
|
|
143
|
+
readonly "ra.csv_import.not_utf8": "This file is not UTF-8, so some letters may be wrong. Save it as CSV UTF-8 and try again.";
|
|
144
|
+
readonly "ra.csv_import.preview_summary": "{valid} valid · {errors} errors · {total} total";
|
|
145
|
+
readonly "ra.csv_import.required_unmapped": "Required fields are not mapped";
|
|
146
|
+
readonly "ra.csv_import.rows_parsed": "{count} rows parsed";
|
|
147
|
+
readonly "ra.csv_import.step.commit": "Importing";
|
|
148
|
+
readonly "ra.csv_import.step.map": "Map columns";
|
|
149
|
+
readonly "ra.csv_import.step.preview": "Preview";
|
|
150
|
+
readonly "ra.csv_import.step.upload": "Upload";
|
|
151
|
+
readonly "ra.csv_import.unmapped": "Not mapped";
|
|
152
|
+
readonly "ra.csv_import.wrong_file": "Choose a .csv file";
|
|
153
|
+
readonly "ra.message.bulk_delete_content": {
|
|
154
|
+
readonly one: "Are you sure you want to delete this item?";
|
|
155
|
+
readonly other: "Are you sure you want to delete these {count} items?";
|
|
156
|
+
};
|
|
157
|
+
readonly "ra.message.bulk_delete_title": {
|
|
158
|
+
readonly one: "Delete 1 item";
|
|
159
|
+
readonly other: "Delete {count} items";
|
|
160
|
+
};
|
|
161
|
+
readonly "ra.message.bulk_edit_description": {
|
|
162
|
+
readonly one: "Only the fields you change are applied to the selected record.";
|
|
163
|
+
readonly other: "Only the fields you change are applied to the {count} selected records.";
|
|
164
|
+
};
|
|
165
|
+
readonly "ra.message.bulk_edit_nested_source": "Bulk edit takes flat sources only, but {source} is nested.";
|
|
166
|
+
readonly "ra.message.bulk_update_content": {
|
|
167
|
+
readonly one: "Are you sure you want to update this item?";
|
|
168
|
+
readonly other: "Are you sure you want to update these {count} items?";
|
|
169
|
+
};
|
|
170
|
+
readonly "ra.message.bulk_update_title": {
|
|
171
|
+
readonly one: "Update 1 item";
|
|
172
|
+
readonly other: "Update {count} items";
|
|
173
|
+
};
|
|
174
|
+
readonly "ra.message.clear_array_input": "Are you sure you want to clear the whole array?";
|
|
175
|
+
readonly "ra.message.delete_content": "Are you sure you want to delete this item?";
|
|
176
|
+
readonly "ra.message.delete_title": "Delete {name} {recordRepresentation}";
|
|
177
|
+
readonly "ra.navigation.clear_filters": "Clear filters";
|
|
178
|
+
readonly "ra.notification.application_update_available": "A new version of the application is available";
|
|
179
|
+
readonly "ra.notification.http_error": "Error";
|
|
180
|
+
readonly "ra.notification.offline": "You are offline";
|
|
181
|
+
readonly "ra.saved_queries.new_label": "Save current query";
|
|
182
|
+
readonly "ra.saved_queries.query_name": "Query name";
|
|
183
|
+
readonly "ra.saved_queries.remove_label_with_name": "Remove saved query \"{name}\"";
|
|
184
|
+
readonly "chelone.map.load_failed": "The map could not be loaded";
|
|
185
|
+
readonly "chelone.map.no_coordinates": "No coordinates";
|
|
186
|
+
readonly "chelone.map.no_geometry": "No geometry";
|
|
187
|
+
readonly "chelone.map.shape_not_shown": "Part of this shape cannot be shown on the map";
|
|
188
|
+
readonly "chelone.map.snap_failed": "The line could not be snapped to roads";
|
|
189
|
+
readonly "chelone.map.toolbar": "Map tools";
|
|
190
|
+
readonly "chelone.map.tool.draw_point": "Draw a point";
|
|
191
|
+
readonly "chelone.map.tool.draw_line": "Draw a line";
|
|
192
|
+
readonly "chelone.map.tool.draw_polygon": "Draw a polygon";
|
|
193
|
+
readonly "chelone.map.tool.draw_rectangle": "Draw a rectangle";
|
|
194
|
+
readonly "chelone.map.tool.draw_circle": "Draw a circle";
|
|
195
|
+
readonly "chelone.map.tool.edit": "Edit";
|
|
196
|
+
readonly "chelone.map.tool.move": "Move";
|
|
197
|
+
readonly "chelone.map.tool.delete": "Delete";
|
|
198
|
+
readonly "chelone.map.tool.cut": "Cut a hole";
|
|
199
|
+
readonly "chelone.map.search_placeholder": "Search for a place";
|
|
200
|
+
readonly "chelone.map.search_button": "Search";
|
|
201
|
+
readonly "chelone.map.searching": "Searching...";
|
|
202
|
+
readonly "chelone.map.no_results": "No results";
|
|
203
|
+
readonly "chelone.map.search_failed": "The search failed, try again shortly";
|
|
204
|
+
readonly "chelone.map.simplify.tolerance": "Tolerance: {value}";
|
|
205
|
+
readonly "chelone.map.simplify.default": "Default";
|
|
206
|
+
readonly "chelone.map.simplify.high": "High";
|
|
207
|
+
readonly "chelone.map.osm.subtract": "Subtract OpenStreetMap areas";
|
|
208
|
+
readonly "chelone.map.osm.add": "Add OpenStreetMap areas";
|
|
209
|
+
readonly "chelone.map.osm.no_sources": "No OpenStreetMap sources configured";
|
|
210
|
+
readonly "chelone.map.osm.no_polygon": "Draw a polygon first";
|
|
211
|
+
readonly "chelone.map.osm.no_match": "No matching OpenStreetMap area found";
|
|
212
|
+
readonly "chelone.map.osm.covered": "The polygon is already covered";
|
|
213
|
+
readonly "chelone.map.osm.subtracted": "Subtracted {area}";
|
|
214
|
+
readonly "chelone.map.osm.added": "Added {area}";
|
|
215
|
+
readonly "chelone.map.osm.would_split": "The result has more than one piece, so the polygon was left as it was";
|
|
216
|
+
readonly "chelone.map.osm.union_empty": "The union is empty";
|
|
217
|
+
readonly "chelone.map.osm.error": "OpenStreetMap request failed";
|
|
218
|
+
readonly "chelone.map.osm.rate_limited": "OpenStreetMap is rate limiting requests, try again shortly";
|
|
219
|
+
readonly "chelone.validation.latitude": "Must be a latitude between -90 and 90";
|
|
220
|
+
readonly "chelone.validation.longitude": "Must be a longitude between -180 and 180";
|
|
221
|
+
readonly "chelone.validation.minVertices": "Must have at least {min} vertices";
|
|
222
|
+
readonly "chelone.validation.maxVertices": "Must have {max} vertices or fewer";
|
|
223
|
+
readonly "chelone.validation.minArea": "Area {area} is below the minimum of {min}";
|
|
224
|
+
readonly "chelone.validation.selfIntersection": "Must not cross itself";
|
|
225
|
+
readonly "chelone.auth.auth_check_error": "Please login to continue";
|
|
226
|
+
readonly "chelone.auth.logout": "Logout";
|
|
227
|
+
readonly "chelone.message.access_denied": "You don't have access to this page";
|
|
228
|
+
readonly "chelone.message.authentication_error": "The authentication server returned an error";
|
|
229
|
+
readonly "chelone.message.invalid_form": "The form is not valid. Please check for errors";
|
|
230
|
+
readonly "chelone.message.loading": "The page is loading, just a moment please";
|
|
231
|
+
readonly "chelone.message.not_found": "Either you typed a wrong URL, or you followed a bad link.";
|
|
232
|
+
readonly "chelone.message.select_all_limit_reached": "There are too many elements to select them all. Only the first {max} elements were selected";
|
|
233
|
+
readonly "chelone.message.unsaved_changes": "Some of your changes weren't saved. Are you sure you want to ignore them?";
|
|
234
|
+
readonly "chelone.notification.created": "Element created";
|
|
235
|
+
readonly "chelone.notification.deleted": "Element deleted";
|
|
236
|
+
readonly "chelone.notification.http_error": "Server communication error";
|
|
237
|
+
readonly "chelone.notification.item_doesnt_exist": "Element does not exist";
|
|
238
|
+
readonly "chelone.notification.logged_out": "Your session has ended, please reconnect";
|
|
239
|
+
readonly "chelone.notification.not_authorized": "You are not authorized to access this resource";
|
|
240
|
+
readonly "chelone.notification.updated": "Element updated";
|
|
241
|
+
readonly "chelone.validation.email": "Must be a valid email";
|
|
242
|
+
readonly "chelone.validation.maxLength": "Must be {max} characters or less";
|
|
243
|
+
readonly "chelone.validation.maxValue": "Must be {max} or less";
|
|
244
|
+
readonly "chelone.validation.minLength": "Must be {min} characters at least";
|
|
245
|
+
readonly "chelone.validation.minValue": "Must be at least {min}";
|
|
246
|
+
readonly "chelone.validation.number": "Must be a number";
|
|
247
|
+
readonly "chelone.validation.oneOf": "Must be one of: {options}";
|
|
248
|
+
readonly "chelone.validation.regex": "Must match a specific format (regexp): {pattern}";
|
|
249
|
+
readonly "chelone.validation.required": "Required";
|
|
250
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { mapMessages } from "../geo/messages.js";
|
|
2
|
+
import { defaultMessages } from "./default-messages.js";
|
|
3
|
+
import { defineMessages } from "./messages.js";
|
|
4
|
+
const englishMessages = defineMessages({
|
|
5
|
+
...defaultMessages,
|
|
6
|
+
...mapMessages,
|
|
7
|
+
// Pages
|
|
8
|
+
"ra.page.list": "{name}",
|
|
9
|
+
"ra.page.create": "Create {name}",
|
|
10
|
+
"ra.page.edit": "{name} {recordRepresentation}",
|
|
11
|
+
"ra.page.show": "{name} {recordRepresentation}",
|
|
12
|
+
"ra.page.dashboard": "Dashboard",
|
|
13
|
+
"ra.page.error": "Something went wrong",
|
|
14
|
+
"ra.page.not_found": "Not Found",
|
|
15
|
+
"ra.page.loading": "Loading",
|
|
16
|
+
"ra.page.empty": "No {name} yet.",
|
|
17
|
+
"ra.page.access_denied": "Access denied",
|
|
18
|
+
"ra.page.authentication_error": "Authentication error",
|
|
19
|
+
// Actions
|
|
20
|
+
"ra.action.add": "Add",
|
|
21
|
+
"ra.action.add_filter": "Add filter",
|
|
22
|
+
"ra.action.bulk_actions": {
|
|
23
|
+
one: "1 item selected",
|
|
24
|
+
other: "{count} items selected"
|
|
25
|
+
},
|
|
26
|
+
"ra.action.cancel": "Cancel",
|
|
27
|
+
"ra.action.clear_input_value": "Clear value",
|
|
28
|
+
"ra.action.clear_search": "Clear search",
|
|
29
|
+
"ra.action.close": "Close",
|
|
30
|
+
"ra.action.close_menu": "Close menu",
|
|
31
|
+
"ra.action.confirm": "Confirm",
|
|
32
|
+
"ra.action.create": "Create",
|
|
33
|
+
"ra.action.delete": "Delete",
|
|
34
|
+
"ra.action.edit": "Edit",
|
|
35
|
+
"ra.action.export": "Export",
|
|
36
|
+
"ra.action.list": "List",
|
|
37
|
+
"ra.action.open_menu": "Open menu",
|
|
38
|
+
"ra.action.refresh": "Refresh",
|
|
39
|
+
"ra.action.remove": "Remove",
|
|
40
|
+
"ra.action.remove_filter": "Remove this filter",
|
|
41
|
+
"ra.action.save": "Save",
|
|
42
|
+
"ra.action.search": "Search",
|
|
43
|
+
"ra.action.select_all": "Select all",
|
|
44
|
+
"ra.action.select_row": "Select this row",
|
|
45
|
+
"ra.action.show": "Show",
|
|
46
|
+
"ra.action.sort": "Sort",
|
|
47
|
+
"ra.action.toggle_theme": "Toggle light/dark mode",
|
|
48
|
+
"ra.action.undo": "Undo",
|
|
49
|
+
"ra.action.unselect": "Unselect",
|
|
50
|
+
"ra.action.update": "Update",
|
|
51
|
+
// Booleans
|
|
52
|
+
"ra.boolean.true": "Yes",
|
|
53
|
+
"ra.boolean.false": "No",
|
|
54
|
+
"ra.boolean.null": " ",
|
|
55
|
+
// Auth
|
|
56
|
+
"ra.auth.logout": "Logout",
|
|
57
|
+
"ra.auth.sign_in": "Sign in",
|
|
58
|
+
"ra.auth.sign_in_error": "Authentication failed, please retry",
|
|
59
|
+
"ra.auth.user_menu": "Profile",
|
|
60
|
+
"ra.auth.username": "Username",
|
|
61
|
+
"ra.auth.password": "Password",
|
|
62
|
+
"ra.auth.auth_check_error": "Please login to continue",
|
|
63
|
+
// Messages
|
|
64
|
+
"ra.message.access_denied": "You don't have the right permissions to access this page",
|
|
65
|
+
"ra.message.auth_error": "An error occurred while validating the authentication token.",
|
|
66
|
+
"ra.message.authentication_error": "The authentication server returned an error and your credentials could not be checked.",
|
|
67
|
+
"ra.message.error": "A client error occurred and your request couldn't be completed.",
|
|
68
|
+
"ra.message.invalid_form": "The form is not valid. Please check for errors",
|
|
69
|
+
"ra.message.loading": "Please wait",
|
|
70
|
+
"ra.message.not_found": "Either you typed a wrong URL, or you followed a bad link.",
|
|
71
|
+
// Navigation / pagination
|
|
72
|
+
"ra.navigation.next": "Go to next page",
|
|
73
|
+
"ra.navigation.previous": "Go to previous page",
|
|
74
|
+
"ra.navigation.first": "Go to first page",
|
|
75
|
+
"ra.navigation.last": "Go to last page",
|
|
76
|
+
// No `{name}`: the list and input call sites pass no params for these.
|
|
77
|
+
"ra.navigation.no_results": "No results found",
|
|
78
|
+
"ra.navigation.no_filtered_results": "No results found with the current filters.",
|
|
79
|
+
"ra.navigation.page_range_info": "{offsetBegin}-{offsetEnd} of {total}",
|
|
80
|
+
"ra.navigation.partial_page_range_info": "{offsetBegin}-{offsetEnd} of more than {offsetEnd}",
|
|
81
|
+
"ra.navigation.page_rows_per_page": "Rows per page:",
|
|
82
|
+
"ra.navigation.current_page": "Page {page}",
|
|
83
|
+
"ra.navigation.skip_nav": "Skip to content",
|
|
84
|
+
// Inputs
|
|
85
|
+
"ra.input.file.upload_several": "Drop some files to upload, or click to select one.",
|
|
86
|
+
"ra.input.file.upload_single": "Drop a file to upload, or click to select it.",
|
|
87
|
+
"ra.input.image.upload_several": "Drop some pictures to upload, or click to select one.",
|
|
88
|
+
"ra.input.image.upload_single": "Drop a picture to upload, or click to select it.",
|
|
89
|
+
"ra.input.password.toggle_visible": "Hide password",
|
|
90
|
+
"ra.input.password.toggle_hidden": "Show password",
|
|
91
|
+
// Theme
|
|
92
|
+
"ra.theme.light": "Light",
|
|
93
|
+
"ra.theme.dark": "Dark",
|
|
94
|
+
"ra.theme.system": "System",
|
|
95
|
+
// Validation (ra namespace — some ported inputs read these)
|
|
96
|
+
"ra.validation.required": "Required",
|
|
97
|
+
"ra.validation.minLength": "Must be {min} characters at least",
|
|
98
|
+
"ra.validation.maxLength": "Must be {max} characters or less",
|
|
99
|
+
"ra.validation.minValue": "Must be at least {min}",
|
|
100
|
+
"ra.validation.maxValue": "Must be {max} or less",
|
|
101
|
+
"ra.validation.number": "Must be a number",
|
|
102
|
+
"ra.validation.email": "Must be a valid email",
|
|
103
|
+
// Actions the components translate beyond the base set
|
|
104
|
+
"ra.action.back": "Back",
|
|
105
|
+
"ra.action.bulk_edit_apply": {
|
|
106
|
+
one: "Apply to 1 record",
|
|
107
|
+
other: "Apply to {count} records"
|
|
108
|
+
},
|
|
109
|
+
"ra.action.change_locale": "Change locale",
|
|
110
|
+
"ra.action.clear_array_input": "Clear the array",
|
|
111
|
+
"ra.action.clone": "Clone",
|
|
112
|
+
"ra.action.collapse": "Collapse",
|
|
113
|
+
"ra.action.expand": "Expand",
|
|
114
|
+
"ra.action.move_column_down": "Move down",
|
|
115
|
+
"ra.action.move_column_up": "Move up",
|
|
116
|
+
"ra.action.move_down": "Move down",
|
|
117
|
+
"ra.action.move_up": "Move up",
|
|
118
|
+
"ra.action.reload": "Reload",
|
|
119
|
+
"ra.action.remove_all_filters": "Remove all filters",
|
|
120
|
+
"ra.action.reset": "Reset",
|
|
121
|
+
"ra.action.reset_columns": "Reset",
|
|
122
|
+
"ra.action.search_columns": "Search columns",
|
|
123
|
+
"ra.action.select_columns": "Columns",
|
|
124
|
+
// Auth
|
|
125
|
+
"ra.auth.or": "or",
|
|
126
|
+
// Calendar
|
|
127
|
+
"ra.calendar.agenda": "Agenda",
|
|
128
|
+
"ra.calendar.month": "Month",
|
|
129
|
+
"ra.calendar.week": "Week",
|
|
130
|
+
"ra.calendar.next": "Next",
|
|
131
|
+
"ra.calendar.previous": "Previous",
|
|
132
|
+
"ra.calendar.today": "Today",
|
|
133
|
+
"ra.calendar.view": "View",
|
|
134
|
+
"ra.calendar.no_events": "No events",
|
|
135
|
+
// CSV import
|
|
136
|
+
"ra.csv_import.back": "Back",
|
|
137
|
+
"ra.csv_import.close": "Close",
|
|
138
|
+
"ra.csv_import.complete": "Import complete",
|
|
139
|
+
"ra.csv_import.counts": "{created} created · {failed} failed · {total} total",
|
|
140
|
+
"ra.csv_import.download_errors": "Download error report",
|
|
141
|
+
"ra.csv_import.drop": "Drop a CSV file here, or click to choose one",
|
|
142
|
+
"ra.csv_import.import": "Import",
|
|
143
|
+
"ra.csv_import.issues": "Issues",
|
|
144
|
+
"ra.csv_import.next": "Next",
|
|
145
|
+
"ra.csv_import.not_utf8": "This file is not UTF-8, so some letters may be wrong. Save it as CSV UTF-8 and try again.",
|
|
146
|
+
"ra.csv_import.preview_summary": "{valid} valid · {errors} errors · {total} total",
|
|
147
|
+
"ra.csv_import.required_unmapped": "Required fields are not mapped",
|
|
148
|
+
"ra.csv_import.rows_parsed": "{count} rows parsed",
|
|
149
|
+
"ra.csv_import.step.commit": "Importing",
|
|
150
|
+
"ra.csv_import.step.map": "Map columns",
|
|
151
|
+
"ra.csv_import.step.preview": "Preview",
|
|
152
|
+
"ra.csv_import.step.upload": "Upload",
|
|
153
|
+
"ra.csv_import.unmapped": "Not mapped",
|
|
154
|
+
"ra.csv_import.wrong_file": "Choose a .csv file",
|
|
155
|
+
// Confirmation dialogs and other messages
|
|
156
|
+
"ra.message.bulk_delete_content": {
|
|
157
|
+
one: "Are you sure you want to delete this item?",
|
|
158
|
+
other: "Are you sure you want to delete these {count} items?"
|
|
159
|
+
},
|
|
160
|
+
"ra.message.bulk_delete_title": {
|
|
161
|
+
one: "Delete 1 item",
|
|
162
|
+
other: "Delete {count} items"
|
|
163
|
+
},
|
|
164
|
+
"ra.message.bulk_edit_description": {
|
|
165
|
+
one: "Only the fields you change are applied to the selected record.",
|
|
166
|
+
other: "Only the fields you change are applied to the {count} selected records."
|
|
167
|
+
},
|
|
168
|
+
"ra.message.bulk_edit_nested_source": "Bulk edit takes flat sources only, but {source} is nested.",
|
|
169
|
+
"ra.message.bulk_update_content": {
|
|
170
|
+
one: "Are you sure you want to update this item?",
|
|
171
|
+
other: "Are you sure you want to update these {count} items?"
|
|
172
|
+
},
|
|
173
|
+
"ra.message.bulk_update_title": {
|
|
174
|
+
one: "Update 1 item",
|
|
175
|
+
other: "Update {count} items"
|
|
176
|
+
},
|
|
177
|
+
"ra.message.clear_array_input": "Are you sure you want to clear the whole array?",
|
|
178
|
+
"ra.message.delete_content": "Are you sure you want to delete this item?",
|
|
179
|
+
"ra.message.delete_title": "Delete {name} {recordRepresentation}",
|
|
180
|
+
// More navigation
|
|
181
|
+
"ra.navigation.clear_filters": "Clear filters",
|
|
182
|
+
// Notifications
|
|
183
|
+
"ra.notification.application_update_available": "A new version of the application is available",
|
|
184
|
+
"ra.notification.http_error": "Error",
|
|
185
|
+
"ra.notification.offline": "You are offline",
|
|
186
|
+
// Saved queries
|
|
187
|
+
"ra.saved_queries.new_label": "Save current query",
|
|
188
|
+
"ra.saved_queries.query_name": "Query name",
|
|
189
|
+
"ra.saved_queries.remove_label_with_name": 'Remove saved query "{name}"'
|
|
190
|
+
});
|
|
191
|
+
export {
|
|
192
|
+
englishMessages
|
|
193
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export { isFilterSelected, toggleFilterValue, } from "./controller/filter-toggle
|
|
|
22
22
|
export type { InfiniteListController, InfiniteListControllerState, } from "./controller/infinite-list-controller.js";
|
|
23
23
|
export { createInfiniteListController } from "./controller/infinite-list-controller.js";
|
|
24
24
|
export type { ListController, ListControllerContext, ListControllerOptions, ListControllerState, ListParamsPatch, } from "./controller/list-controller.js";
|
|
25
|
-
export { createListController } from "./controller/list-controller.js";
|
|
25
|
+
export { createListController, listControllerActions, } from "./controller/list-controller.js";
|
|
26
26
|
export type { ListParamsQuery, ListUrlState, } from "./controller/list-params.js";
|
|
27
27
|
export { hasCustomParams, listPatchFromSearch, parseListParams, serializeListParams, serializeListState, } from "./controller/list-params.js";
|
|
28
28
|
export type { PrevNextOptions, PrevNextPage, PrevNextParams, PrevNextResolution, PrevNextStoredParams, } from "./controller/prev-next.js";
|
|
@@ -61,8 +61,8 @@ export { createMutationOptions, deleteMutationOptions, updateMutationOptions, }
|
|
|
61
61
|
export type { DataContext } from "./data/query-options.js";
|
|
62
62
|
export { getInfiniteListOptions, getListOptions, getManyOptions, getManyReferenceOptions, getOneOptions, } from "./data/query-options.js";
|
|
63
63
|
export type { CreateParams, CreateResult, CursorPagination, DataProvider, DeleteManyParams, DeleteManyResult, DeleteParams, DeleteResult, FilterPayload, GetListParams, GetListResult, GetManyParams, GetManyReferenceParams, GetManyReferenceResult, GetManyResult, GetOneParams, GetOneResult, InfiniteListParams, PageInfo, PagePagination, Pagination, SortPayload, UpdateManyParams, UpdateManyResult, UpdateParams, UpdateResult, } from "./data/types.js";
|
|
64
|
-
export type { UndoableHandle, UndoableMutationConfig, UndoableSnapshot, } from "./data/undoable.js";
|
|
65
|
-
export { commitAllUndoables, hasPendingUndoables, startUndoableMutation, } from "./data/undoable.js";
|
|
64
|
+
export type { UndoableCacheOptions, UndoableCallbacks, UndoableDeleteCallbacks, UndoableHandle, UndoableMutationConfig, UndoableSettler, UndoableSnapshot, UndoableUpdateCallbacks, UndoableUpdateManyCallbacks, } from "./data/undoable.js";
|
|
65
|
+
export { commitAllUndoables, hasPendingUndoables, patchedRecord, startUndoableMutation, undoableCallbacks, undoableDeleteOptions, undoableUpdateOptions, } from "./data/undoable.js";
|
|
66
66
|
export type { LifecycleCallbacks, LifecycleHandler, } from "./data/with-lifecycle-callbacks.js";
|
|
67
67
|
export { withLifecycleCallbacks } from "./data/with-lifecycle-callbacks.js";
|
|
68
68
|
export type { ApplicationUpdateSnapshot, ApplicationUpdateStoreOptions, } from "./environment/application-update-store.js";
|
|
@@ -118,6 +118,7 @@ export { featuresToShape, heldFeatures, shapeToFeatures, } from "./geo/shape-val
|
|
|
118
118
|
export type { OsrmProviderOptions, RoutingProvider } from "./geo/snap-to-roads.js";
|
|
119
119
|
export { createOsrmProvider } from "./geo/snap-to-roads.js";
|
|
120
120
|
export { defaultMessages } from "./i18n/default-messages.js";
|
|
121
|
+
export { englishMessages } from "./i18n/english-messages.js";
|
|
121
122
|
export type { AnyCheloneI18n, CheloneI18n } from "./i18n/i18n.js";
|
|
122
123
|
export { createI18n } from "./i18n/i18n.js";
|
|
123
124
|
export type { ExtractParams, MessageCatalog, MessageKey, MessageValue, PluralMessage, TranslateParams, } from "./i18n/messages.js";
|
|
@@ -150,12 +151,15 @@ export type { SubscribeUnreadCountOptions } from "./realtime/unread-count.js";
|
|
|
150
151
|
export { clearUnread, subscribeUnreadCount, unreadKey, } from "./realtime/unread-count.js";
|
|
151
152
|
export type { FieldComponentName, FieldDescriptor, FieldKind, InputComponentName, ResourceDescriptor, } from "./resource/describe-resource.js";
|
|
152
153
|
export { describeResource } from "./resource/describe-resource.js";
|
|
154
|
+
export { getAtPath } from "./resource/get-at-path.js";
|
|
153
155
|
export type { Identifier } from "./resource/identifier.js";
|
|
154
156
|
export type { LooseRecord, ResourceRecord } from "./resource/record.js";
|
|
157
|
+
export type { Register, RegisteredRecord, RegisteredResourceName, RegisteredResources, } from "./resource/register.js";
|
|
155
158
|
export type { RecordOf, ResourceConfig, ResourceMap, } from "./resource/registry.js";
|
|
156
159
|
export { defineResources } from "./resource/registry.js";
|
|
157
160
|
export type { ComputeDefinitionProps, ResourceDefinition, ResourceDefinitionMap, ResourceRegistry, ResourceRegistryListener, } from "./resource/registry-store.js";
|
|
158
161
|
export { computeDefinition, createResourceRegistry, } from "./resource/registry-store.js";
|
|
162
|
+
export type { AnySourcePath, AnySourcePathOf, FilterSourceKey, RowSourcePath, SourceKey, SourcePath, SourceProp, SourceValue, } from "./resource/source-path.js";
|
|
159
163
|
export type { CheloneLocation, NavigateOpts } from "./router/location.js";
|
|
160
164
|
export { parseLocation } from "./router/location.js";
|
|
161
165
|
export type { PathMatch } from "./router/match.js";
|
package/dist/index.js
CHANGED
|
@@ -10,7 +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 { createListController } from "./controller/list-controller.js";
|
|
13
|
+
import { createListController, listControllerActions } from "./controller/list-controller.js";
|
|
14
14
|
import { hasCustomParams, listPatchFromSearch, parseListParams, serializeListParams, serializeListState } from "./controller/list-params.js";
|
|
15
15
|
import { canUseCachedPage, getPrevNextParams, resolvePrevNext } from "./controller/prev-next.js";
|
|
16
16
|
import { createCreateController, createDeleteController, createEditController, createShowController } from "./controller/record-controllers.js";
|
|
@@ -36,7 +36,7 @@ import { HttpError } from "./data/http-error.js";
|
|
|
36
36
|
import { infiniteListKey, listKey, lockKey, locksKey, manyKey, manyReferenceKey, oneKey, resourceKey } from "./data/keys.js";
|
|
37
37
|
import { createMutationOptions, deleteMutationOptions, updateMutationOptions } from "./data/mutation-options.js";
|
|
38
38
|
import { getInfiniteListOptions, getListOptions, getManyOptions, getManyReferenceOptions, getOneOptions } from "./data/query-options.js";
|
|
39
|
-
import { commitAllUndoables, hasPendingUndoables, startUndoableMutation } from "./data/undoable.js";
|
|
39
|
+
import { commitAllUndoables, hasPendingUndoables, patchedRecord, startUndoableMutation, undoableCallbacks, undoableDeleteOptions, undoableUpdateOptions } from "./data/undoable.js";
|
|
40
40
|
import { withLifecycleCallbacks } from "./data/with-lifecycle-callbacks.js";
|
|
41
41
|
import { createApplicationUpdateStore } from "./environment/application-update-store.js";
|
|
42
42
|
import { createOfflineStore } from "./environment/offline-store.js";
|
|
@@ -69,6 +69,7 @@ import { OverpassError, OverpassRateLimitError, OverpassTimeoutError, createOver
|
|
|
69
69
|
import { featuresToShape, heldFeatures, shapeToFeatures } from "./geo/shape-value.js";
|
|
70
70
|
import { createOsrmProvider } from "./geo/snap-to-roads.js";
|
|
71
71
|
import { defaultMessages } from "./i18n/default-messages.js";
|
|
72
|
+
import { englishMessages } from "./i18n/english-messages.js";
|
|
72
73
|
import { createI18n } from "./i18n/i18n.js";
|
|
73
74
|
import { defineMessages } from "./i18n/messages.js";
|
|
74
75
|
import { createTranslatableController, getRecordForLocale } from "./i18n/translatable.js";
|
|
@@ -87,6 +88,7 @@ import { lockResourceTopic, lockTopic, presenceTopic, recordTopic, resourceTopic
|
|
|
87
88
|
import { LockConflictError } from "./realtime/types.js";
|
|
88
89
|
import { clearUnread, subscribeUnreadCount, unreadKey } from "./realtime/unread-count.js";
|
|
89
90
|
import { describeResource } from "./resource/describe-resource.js";
|
|
91
|
+
import { getAtPath } from "./resource/get-at-path.js";
|
|
90
92
|
import { defineResources } from "./resource/registry.js";
|
|
91
93
|
import { computeDefinition, createResourceRegistry } from "./resource/registry-store.js";
|
|
92
94
|
import { parseLocation } from "./router/location.js";
|
|
@@ -191,6 +193,7 @@ export {
|
|
|
191
193
|
downloadCSV,
|
|
192
194
|
editorStyles,
|
|
193
195
|
email,
|
|
196
|
+
englishMessages,
|
|
194
197
|
expandTileUrl,
|
|
195
198
|
extractValidSavedQueries,
|
|
196
199
|
fakeDataProvider,
|
|
@@ -206,6 +209,7 @@ export {
|
|
|
206
209
|
geocodeQueryOptions,
|
|
207
210
|
getAllowedTransitions,
|
|
208
211
|
getApprovalState,
|
|
212
|
+
getAtPath,
|
|
209
213
|
getDataTableColumnId,
|
|
210
214
|
getDirtyFormValues,
|
|
211
215
|
getFilterFormValues,
|
|
@@ -246,6 +250,7 @@ export {
|
|
|
246
250
|
joinBasename,
|
|
247
251
|
latLngToLngLat,
|
|
248
252
|
latitude,
|
|
253
|
+
listControllerActions,
|
|
249
254
|
listKey,
|
|
250
255
|
listPatchFromSearch,
|
|
251
256
|
lngLatToLatLng,
|
|
@@ -283,6 +288,7 @@ export {
|
|
|
283
288
|
parseCsvWith,
|
|
284
289
|
parseListParams,
|
|
285
290
|
parseLocation,
|
|
291
|
+
patchedRecord,
|
|
286
292
|
permissionsQueryOptions,
|
|
287
293
|
polygonToBBox,
|
|
288
294
|
presenceTopic,
|
|
@@ -330,6 +336,9 @@ export {
|
|
|
330
336
|
toPresenceUser,
|
|
331
337
|
toggleFilterValue,
|
|
332
338
|
toggleHiddenColumn,
|
|
339
|
+
undoableCallbacks,
|
|
340
|
+
undoableDeleteOptions,
|
|
341
|
+
undoableUpdateOptions,
|
|
333
342
|
unreadKey,
|
|
334
343
|
updateMutationOptions,
|
|
335
344
|
updateRecordsInCache,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one path grammar chelone reads a record with. Dot members (`a.b`),
|
|
3
|
+
* bracket indices (`a[0].b`) and numeric-dot indices (`a.0.b`) all resolve to
|
|
4
|
+
* the same value, matching `lodash/get`'s relevant semantics.
|
|
5
|
+
*
|
|
6
|
+
* The core tree, calendar and client-list readers read through this, so a
|
|
7
|
+
* `source` written one way reads the same record everywhere. The adapters'
|
|
8
|
+
* `useFieldValue` reads the same grammar and moves onto this helper next.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Read the value at `source` off `record`. An empty `source` returns the record
|
|
12
|
+
* itself; a step that is null, undefined or a primitive returns `undefined`.
|
|
13
|
+
*
|
|
14
|
+
* An own key equal to the whole `source` wins before any split, as in
|
|
15
|
+
* `lodash/get`: a record that literally holds a `"nested.level"` key — a
|
|
16
|
+
* flattened filter payload, for instance — still reads.
|
|
17
|
+
*/
|
|
18
|
+
export declare function getAtPath(record: unknown, source: string): unknown;
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
function getAtPath(record, source) {
|
|
10
|
+
if (source === "") return record;
|
|
11
|
+
if (record !== null && typeof record === "object" && Object.hasOwn(record, source)) {
|
|
12
|
+
return record[source];
|
|
13
|
+
}
|
|
14
|
+
let current = record;
|
|
15
|
+
for (const key of toPathSegments(source)) {
|
|
16
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
17
|
+
current = current[key];
|
|
18
|
+
}
|
|
19
|
+
return current;
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
getAtPath
|
|
23
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { RecordOf, ResourceMap } from "./registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* The app's augmentation point: the one place a Chelone program tells core
|
|
4
|
+
* which resources it has, so `source` props can be typed against a registered
|
|
5
|
+
* resource's record instead of a plain `string`.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* const resources = defineResources({ posts: { schema: postSchema } });
|
|
9
|
+
* declare module "@chelone/core" {
|
|
10
|
+
* interface Register { resources: typeof resources }
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Augmenting `Register` is global to a TypeScript program. Without it every
|
|
15
|
+
* `Registered*` type below resolves to its loose fallback and nothing breaks.
|
|
16
|
+
*/
|
|
17
|
+
export interface Register {
|
|
18
|
+
}
|
|
19
|
+
/** The registered resource map, or {@link ResourceMap} when nothing is registered. */
|
|
20
|
+
export type RegisteredResources = Register extends {
|
|
21
|
+
resources: infer TResources extends ResourceMap;
|
|
22
|
+
} ? TResources : ResourceMap;
|
|
23
|
+
/** The registered resource names, or `string` when nothing is registered. */
|
|
24
|
+
export type RegisteredResourceName = keyof RegisteredResources & string;
|
|
25
|
+
/** The record type of a registered resource, or {@link LooseRecord} when it has no schema. */
|
|
26
|
+
export type RegisteredRecord<TName extends RegisteredResourceName> = RecordOf<RegisteredResources, TName>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Get, Paths } from "dot.paths";
|
|
2
|
+
import type { LooseRecord } from "./record.js";
|
|
3
|
+
import type { RegisteredResources } from "./register.js";
|
|
4
|
+
import type { RecordOf, ResourceMap } from "./registry.js";
|
|
5
|
+
/**
|
|
6
|
+
* Source-path types for a registered resource's record.
|
|
7
|
+
*
|
|
8
|
+
* Every path prop in Chelone is composed at runtime (`source.ts`,
|
|
9
|
+
* `input-binding.ts`); these types say which paths are legal for a given
|
|
10
|
+
* record, so `<TextInput source="titel" />` stops compiling once an app
|
|
11
|
+
* registers its resources. They are built on `dot.paths`' `Paths` and `Get`,
|
|
12
|
+
* and accept the bracket grammar only (`tags[0].label`) — the syntax
|
|
13
|
+
* `composeSource` and form-core's `DeepKeys` use. A dot index
|
|
14
|
+
* (`tags.0.label`) still works at runtime but is not a legal prop type.
|
|
15
|
+
*/
|
|
16
|
+
/** Object levels walked while looking for arrays in a record. */
|
|
17
|
+
type _WalkDepth = 8;
|
|
18
|
+
/** How deep `RowSourcePath` follows nested arrays. */
|
|
19
|
+
type _MaxArrayNesting = 3;
|
|
20
|
+
/** Decrement tables for the two counters above (`_Prev[N]` is `N - 1`). */
|
|
21
|
+
type _PrevWalk = [never, 0, 1, 2, 3, 4, 5, 6, 7];
|
|
22
|
+
type _PrevNesting = [never, 0, 1, 2];
|
|
23
|
+
/**
|
|
24
|
+
* The item type of every array reachable in `T`. Object members are walked up
|
|
25
|
+
* to `_WalkDepth` levels; array nesting up to `_MaxArrayNesting`, so a
|
|
26
|
+
* recursive record type terminates and the resulting path union stays small.
|
|
27
|
+
*
|
|
28
|
+
* The union is walked one member at a time (`T extends unknown` distributes,
|
|
29
|
+
* and object members are walked per key) because `any` absorbs a union: walked
|
|
30
|
+
* as one type, a single `any`-typed field would turn every array in the record
|
|
31
|
+
* into `any`.
|
|
32
|
+
*/
|
|
33
|
+
type _ArrayItems<T, Walk extends number, Nesting extends number> = [
|
|
34
|
+
Walk
|
|
35
|
+
] extends [0] ? never : [Nesting] extends [0] ? never : T extends unknown ? _ItemsOf<T, Walk, Nesting> : never;
|
|
36
|
+
type _ItemsOf<T, Walk extends number, Nesting extends number> = 0 extends 1 & T ? never : T extends readonly unknown[] ? T extends readonly [] ? never : _Row<NonNullable<T[number]>> | _ArrayItems<NonNullable<T[number]>, _PrevWalk[Walk], _PrevNesting[Nesting]> : T extends object ? {
|
|
37
|
+
[K in keyof T]: _ArrayItems<T[K], _PrevWalk[Walk], Nesting>;
|
|
38
|
+
}[keyof T] : never;
|
|
39
|
+
/**
|
|
40
|
+
* The item type of an array, minus `any` (it names no key set) and minus any
|
|
41
|
+
* member that is itself an array: a row that is an array has no named path
|
|
42
|
+
* under it, and `Paths` of it is a wall of `[${number}]` patterns.
|
|
43
|
+
*/
|
|
44
|
+
type _Row<T> = Exclude<0 extends 1 & T ? never : T, readonly unknown[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Every legal `source` path into `TRecord`: dotted object paths and bracket
|
|
47
|
+
* array indices (`items[0].sku`).
|
|
48
|
+
*
|
|
49
|
+
* The `unknown` guard is load-bearing: `Paths<unknown>` is `never`, which would
|
|
50
|
+
* reject every source of a resource with no schema, while
|
|
51
|
+
* `Paths<LooseRecord, { arrays: "bracket" }>` is `string`.
|
|
52
|
+
*/
|
|
53
|
+
export type SourcePath<TRecord> = unknown extends TRecord ? string : Paths<TRecord, {
|
|
54
|
+
arrays: "bracket";
|
|
55
|
+
}>;
|
|
56
|
+
/**
|
|
57
|
+
* The value type at `TPath`, including the `null`/`undefined` of every parent
|
|
58
|
+
* along it — what reading `record.author?.name` gives. `unknown` for a loose
|
|
59
|
+
* record.
|
|
60
|
+
*/
|
|
61
|
+
export type SourceValue<TRecord, TPath extends string> = unknown extends TRecord ? unknown : Get<TRecord, TPath, {
|
|
62
|
+
nullish: "path";
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* The top-level keys of `TRecord`, for props that read or write one flat key
|
|
66
|
+
* (a filter's field, a reference's target). `string` for a loose record.
|
|
67
|
+
*/
|
|
68
|
+
export type SourceKey<TRecord> = unknown extends TRecord ? string : keyof TRecord & string;
|
|
69
|
+
/**
|
|
70
|
+
* The paths *relative to an array item* of every array in `TRecord`: `sku` is
|
|
71
|
+
* one for a record with `items: { sku: string }[]`.
|
|
72
|
+
*
|
|
73
|
+
* Children of an `ArrayInput` bind a path relative to their row
|
|
74
|
+
* (`createRowSourceScope`), so a root-only path type would reject every
|
|
75
|
+
* iterator child.
|
|
76
|
+
*
|
|
77
|
+
* A row whose type names no keys contributes nothing, and it is filtered per
|
|
78
|
+
* member so it cannot poison the rest: a `z.record` column, a jsonb `Json`
|
|
79
|
+
* member and a `z.any()` field all give `Paths` = `string`, and one `string`
|
|
80
|
+
* here would swallow every other array's paths and let every typo through
|
|
81
|
+
* again. A record that wants its jsonb rows checked gives them a schema.
|
|
82
|
+
*/
|
|
83
|
+
export type RowSourcePath<TRecord> = unknown extends TRecord ? string : string extends keyof TRecord ? string : _ArrayItems<TRecord, _WalkDepth, _MaxArrayNesting> extends infer TItem ? TItem extends unknown ? Paths<TItem, {
|
|
84
|
+
arrays: "bracket";
|
|
85
|
+
}> extends infer TPath extends string ? string extends TPath ? never : TPath : never : never : never;
|
|
86
|
+
/**
|
|
87
|
+
* A filter key: the free-text `q`, or a source path with an operator suffix —
|
|
88
|
+
* the keys Chelone writes itself (`calendarRangeFilter` builds
|
|
89
|
+
* `` `${startSource}_gte` ``) and a filter input's `source` is named with.
|
|
90
|
+
*/
|
|
91
|
+
export type FilterSourceKey<TRecord> = "q" | `${SourcePath<TRecord>}_${string}`;
|
|
92
|
+
/** The source props one record accepts: its paths, its row paths, its filter keys. */
|
|
93
|
+
type _RecordSourcePath<TRecord> = SourcePath<TRecord> | RowSourcePath<TRecord> | FilterSourceKey<TRecord>;
|
|
94
|
+
/**
|
|
95
|
+
* The union of every registered resource's source paths.
|
|
96
|
+
*
|
|
97
|
+
* `string` when the name set is `string` — the unregistered case, and the one
|
|
98
|
+
* that keeps `AnySourcePath` from recursing on itself through
|
|
99
|
+
* {@link SourceProp} — and `string` when any resource has no schema.
|
|
100
|
+
*/
|
|
101
|
+
export type AnySourcePathOf<TResources extends ResourceMap> = string extends keyof TResources ? string : {
|
|
102
|
+
[TName in keyof TResources & string]: _RecordSourcePath<RecordOf<TResources, TName>>;
|
|
103
|
+
}[keyof TResources & string];
|
|
104
|
+
/** {@link AnySourcePathOf} over the app's registered resources. */
|
|
105
|
+
export type AnySourcePath = AnySourcePathOf<RegisteredResources>;
|
|
106
|
+
/**
|
|
107
|
+
* The type a `source` prop takes.
|
|
108
|
+
*
|
|
109
|
+
* A loose record (the default, and every app that registers nothing) gets
|
|
110
|
+
* {@link AnySourcePath}, which is `string` until `Register` is augmented; a
|
|
111
|
+
* typed record gets its own paths.
|
|
112
|
+
*/
|
|
113
|
+
export type SourceProp<TRecord = LooseRecord> = LooseRecord extends TRecord ? AnySourcePath : _RecordSourcePath<TRecord>;
|
|
114
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chelone/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
"vitest": "^3.0.0",
|
|
44
44
|
"zod": "^3.25.0"
|
|
45
45
|
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"dot.paths": "^0.2.0"
|
|
48
|
+
},
|
|
46
49
|
"scripts": {
|
|
47
50
|
"build": "vite build && tsc -p tsconfig.build.json && node ../../scripts/dts-extensions.mjs dist",
|
|
48
51
|
"check:pack": "publint --pack pnpm --strict && d=$(mktemp -d) && trap 'rm -rf $d' EXIT && pnpm pack --pack-destination $d && attw $d/*.tgz --profile esm-only",
|