@chelone/core 0.5.0 → 0.7.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/assistant/assistant-session.d.ts +28 -0
- package/dist/assistant/assistant-session.js +208 -0
- package/dist/assistant/echo-transport.d.ts +7 -0
- package/dist/assistant/echo-transport.js +28 -0
- package/dist/assistant/http-transport.d.ts +10 -0
- package/dist/assistant/http-transport.js +98 -0
- package/dist/assistant/types.d.ts +54 -0
- package/dist/auth/access-control.d.ts +13 -5
- package/dist/auth/access-control.js +25 -21
- package/dist/auth/auth-options.d.ts +21 -17
- package/dist/auth/auth-options.js +80 -48
- package/dist/auth/permission-matrix.d.ts +12 -0
- package/dist/auth/permission-matrix.js +67 -0
- package/dist/auth/types.d.ts +14 -4
- package/dist/block-editor/block-document.d.ts +60 -0
- package/dist/block-editor/block-document.js +143 -0
- package/dist/block-editor/data-blocks.d.ts +63 -0
- package/dist/block-editor/data-blocks.js +117 -0
- package/dist/chart/chart-data.d.ts +25 -0
- package/dist/chart/chart-data.js +61 -0
- package/dist/command-menu/hotkey.js +58 -47
- package/dist/command-menu/recents.js +8 -11
- package/dist/command-menu/registry.d.ts +9 -5
- package/dist/command-menu/registry.js +36 -46
- package/dist/controller/calendar-dates.js +82 -113
- package/dist/controller/calendar-events.js +93 -88
- package/dist/controller/client-list-controller.d.ts +2 -2
- package/dist/controller/client-list-controller.js +251 -254
- package/dist/controller/data-table-columns.d.ts +2 -0
- package/dist/controller/data-table-columns.js +50 -56
- package/dist/controller/deep-equal.d.ts +1 -0
- package/dist/controller/deep-equal.js +14 -0
- package/dist/controller/filter-toggle.js +36 -44
- package/dist/controller/in-place-editor.d.ts +42 -0
- package/dist/controller/in-place-editor.js +42 -0
- package/dist/controller/infinite-list-controller.js +220 -190
- package/dist/controller/inspector.d.ts +49 -0
- package/dist/controller/inspector.js +64 -0
- package/dist/controller/job-monitor.d.ts +41 -0
- package/dist/controller/job-monitor.js +49 -0
- package/dist/controller/kanban-board.d.ts +2 -3
- package/dist/controller/kanban-board.js +43 -24
- package/dist/controller/list-controller.d.ts +5 -5
- package/dist/controller/list-controller.js +444 -400
- package/dist/controller/list-params.js +96 -87
- package/dist/controller/pivot-grid.d.ts +36 -0
- package/dist/controller/pivot-grid.js +99 -0
- package/dist/controller/prev-next.d.ts +2 -2
- package/dist/controller/prev-next.js +74 -54
- package/dist/controller/record-controllers.d.ts +14 -6
- package/dist/controller/record-controllers.js +288 -257
- package/dist/controller/record-diff.d.ts +21 -0
- package/dist/controller/record-diff.js +50 -0
- package/dist/controller/record-timeline.d.ts +35 -0
- package/dist/controller/record-timeline.js +48 -0
- package/dist/controller/record-tree.js +57 -47
- package/dist/controller/record-workflow.d.ts +38 -16
- package/dist/controller/record-workflow.js +85 -64
- package/dist/controller/reference-many-controller.js +198 -192
- package/dist/controller/reference-one-controller.d.ts +1 -1
- package/dist/controller/reference-one-controller.js +63 -56
- package/dist/controller/saved-queries.js +104 -91
- package/dist/controller/translate-fallback.js +10 -6
- package/dist/csv/export.d.ts +2 -1
- package/dist/csv/export.js +43 -40
- package/dist/csv/import-controller.js +185 -169
- package/dist/csv/match-headers.js +29 -22
- package/dist/csv/parse-with.js +60 -54
- package/dist/csv/run-import.js +69 -72
- package/dist/csv/validate-rows.d.ts +1 -1
- package/dist/csv/validate-rows.js +65 -52
- package/dist/data/cache-ops.d.ts +5 -4
- package/dist/data/cache-ops.js +208 -190
- package/dist/data/combine-data-providers.js +25 -23
- package/dist/data/data-provider-log.d.ts +45 -0
- package/dist/data/data-provider-log.js +93 -0
- package/dist/data/errors.js +14 -10
- package/dist/data/fake-provider.d.ts +6 -3
- package/dist/data/fake-provider.js +129 -114
- package/dist/data/fetch-utils.js +61 -44
- package/dist/data/filter-branch.d.ts +5 -0
- package/dist/data/filter-branch.js +10 -0
- package/dist/data/get-many-aggregate.js +84 -76
- package/dist/data/http-error.js +17 -12
- package/dist/data/keys.js +50 -36
- package/dist/data/mutation-options.d.ts +14 -9
- package/dist/data/mutation-options.js +93 -109
- package/dist/data/query-options.js +73 -68
- package/dist/data/remove-empty.d.ts +3 -0
- package/dist/data/remove-empty.js +11 -0
- package/dist/data/undoable.d.ts +2 -2
- package/dist/data/undoable.js +190 -174
- package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
- package/dist/data/with-lifecycle-callbacks.js +66 -52
- package/dist/environment/application-update-store.js +58 -55
- package/dist/environment/offline-store.js +21 -21
- package/dist/form/array-binding.d.ts +30 -11
- package/dist/form/array-binding.js +184 -166
- package/dist/form/choice-ids.d.ts +7 -0
- package/dist/form/choice-ids.js +11 -0
- package/dist/form/choices-controller.d.ts +3 -2
- package/dist/form/choices-controller.js +302 -275
- package/dist/form/date-format.d.ts +38 -0
- package/dist/form/date-format.js +89 -0
- package/dist/form/dirty-values.d.ts +2 -2
- package/dist/form/dirty-values.js +22 -11
- package/dist/form/duration.d.ts +22 -0
- package/dist/form/duration.js +49 -0
- package/dist/form/edit-create-form.d.ts +12 -5
- package/dist/form/edit-create-form.js +243 -188
- package/dist/form/event-value.d.ts +6 -0
- package/dist/form/event-value.js +17 -0
- package/dist/form/field-error.d.ts +14 -0
- package/dist/form/field-error.js +28 -0
- package/dist/form/field-name.d.ts +20 -0
- package/dist/form/field-name.js +6 -0
- package/dist/form/filter-form-values.js +63 -49
- package/dist/form/form-controller.d.ts +9 -1
- package/dist/form/form-controller.js +165 -160
- package/dist/form/form-groups.d.ts +4 -3
- package/dist/form/form-groups.js +79 -79
- package/dist/form/form-handle.d.ts +175 -0
- package/dist/form/form-handle.js +68 -0
- package/dist/form/input-binding.d.ts +56 -31
- package/dist/form/input-binding.js +169 -118
- package/dist/form/input-values.d.ts +130 -0
- package/dist/form/input-values.js +177 -0
- package/dist/form/json-text.js +51 -37
- package/dist/form/save-pipeline.d.ts +6 -8
- package/dist/form/save-pipeline.js +91 -82
- package/dist/form/set-form-values.d.ts +6 -0
- package/dist/form/set-form-values.js +10 -0
- package/dist/form/source.js +97 -65
- package/dist/form/text-of.d.ts +2 -0
- package/dist/form/text-of.js +7 -0
- package/dist/form/validators.d.ts +58 -19
- package/dist/form/validators.js +97 -71
- package/dist/geo/bbox.d.ts +5 -4
- package/dist/geo/bbox.js +69 -48
- package/dist/geo/bounds.d.ts +14 -3
- package/dist/geo/bounds.js +98 -68
- package/dist/geo/constraints.js +88 -93
- package/dist/geo/coordinates.js +31 -23
- package/dist/geo/editor.d.ts +13 -13
- package/dist/geo/editor.js +176 -153
- package/dist/geo/geocoding.d.ts +4 -4
- package/dist/geo/geocoding.js +38 -41
- package/dist/geo/geojson.d.ts +12 -74
- package/dist/geo/geojson.js +52 -40
- package/dist/geo/geometry-math.d.ts +4 -4
- package/dist/geo/geometry-math.js +100 -93
- package/dist/geo/index.d.ts +7 -6
- package/dist/geo/map-layers.d.ts +42 -7
- package/dist/geo/map-layers.js +88 -62
- package/dist/geo/map-style.js +31 -32
- package/dist/geo/messages.js +48 -45
- package/dist/geo/nominatim.js +88 -80
- package/dist/geo/osm-presets.d.ts +4 -3
- package/dist/geo/osm-presets.js +108 -86
- package/dist/geo/osm-tags.js +14 -12
- package/dist/geo/overpass.d.ts +14 -2
- package/dist/geo/overpass.js +83 -90
- package/dist/geo/shape-value.d.ts +9 -5
- package/dist/geo/shape-value.js +169 -146
- package/dist/geo/snap-to-roads.d.ts +3 -3
- package/dist/geo/snap-to-roads.js +55 -50
- package/dist/geo/throttle.js +36 -39
- package/dist/guards.d.ts +37 -0
- package/dist/guards.js +62 -0
- package/dist/i18n/default-messages.js +34 -29
- package/dist/i18n/english-messages.js +187 -189
- package/dist/i18n/i18n.d.ts +8 -2
- package/dist/i18n/i18n.js +47 -48
- package/dist/i18n/inflection.d.ts +34 -0
- package/dist/i18n/inflection.js +82 -0
- package/dist/i18n/messages.d.ts +10 -2
- package/dist/i18n/messages.js +17 -4
- package/dist/i18n/missing-keys.d.ts +19 -0
- package/dist/i18n/missing-keys.js +68 -0
- package/dist/i18n/translatable.d.ts +16 -1
- package/dist/i18n/translatable.js +40 -47
- package/dist/index.d.ts +96 -22
- package/dist/index.js +52 -257
- package/dist/json/deep-equal.d.ts +8 -0
- package/dist/json/deep-equal.js +24 -0
- package/dist/json/strip-json-comments.js +38 -42
- package/dist/noop.d.ts +2 -0
- package/dist/noop.js +4 -0
- package/dist/notification/error-message.d.ts +2 -0
- package/dist/notification/error-message.js +10 -0
- package/dist/notification/notification-store.d.ts +3 -2
- package/dist/notification/notification-store.js +41 -45
- package/dist/onboarding-tour/placement.d.ts +34 -0
- package/dist/onboarding-tour/placement.js +67 -0
- package/dist/onboarding-tour/tour-controller.d.ts +25 -0
- package/dist/onboarding-tour/tour-controller.js +50 -0
- package/dist/realtime/add-events-for-mutations.js +75 -81
- package/dist/realtime/announce-lock.d.ts +7 -0
- package/dist/realtime/announce-lock.js +18 -0
- package/dist/realtime/broadcast-channel-transport.d.ts +14 -0
- package/dist/realtime/broadcast-channel-transport.js +69 -0
- package/dist/realtime/copy-members.js +22 -14
- package/dist/realtime/in-memory-lock-provider.js +41 -60
- package/dist/realtime/live.js +95 -101
- package/dist/realtime/lock-options.js +16 -16
- package/dist/realtime/lock-session.js +91 -78
- package/dist/realtime/memory-transport.js +26 -67
- package/dist/realtime/presence.js +120 -122
- package/dist/realtime/realtime-data-provider.d.ts +10 -0
- package/dist/realtime/realtime-data-provider.js +45 -42
- package/dist/realtime/reconnect-backoff.d.ts +15 -0
- package/dist/realtime/reconnect-backoff.js +17 -0
- package/dist/realtime/sse-transport.d.ts +31 -0
- package/dist/realtime/sse-transport.js +235 -0
- package/dist/realtime/status-store.js +23 -28
- package/dist/realtime/topic-hub.d.ts +24 -0
- package/dist/realtime/topic-hub.js +66 -0
- package/dist/realtime/topics.js +10 -12
- package/dist/realtime/types.js +10 -12
- package/dist/realtime/unread-count.js +21 -21
- package/dist/realtime/websocket-transport.d.ts +32 -0
- package/dist/realtime/websocket-transport.js +306 -0
- package/dist/resource/describe-resource.d.ts +2 -0
- package/dist/resource/describe-resource.js +232 -219
- package/dist/resource/get-at-path.js +29 -14
- package/dist/resource/identifier.d.ts +18 -0
- package/dist/resource/identifier.js +27 -0
- package/dist/resource/record.d.ts +4 -0
- package/dist/resource/record.js +10 -0
- package/dist/resource/register.d.ts +7 -0
- package/dist/resource/registry-store.d.ts +45 -15
- package/dist/resource/registry-store.js +67 -60
- package/dist/resource/registry.d.ts +10 -0
- package/dist/resource/registry.js +8 -4
- package/dist/resource/source-path.d.ts +16 -1
- package/dist/rich-text/rich-text-extras.d.ts +132 -0
- package/dist/rich-text/rich-text-extras.js +179 -0
- package/dist/rich-text/rich-text-image.d.ts +124 -0
- package/dist/rich-text/rich-text-image.js +163 -0
- package/dist/rich-text/rich-text.d.ts +62 -0
- package/dist/rich-text/rich-text.js +142 -0
- package/dist/router/location.d.ts +22 -4
- package/dist/router/location.js +41 -18
- package/dist/router/match.js +47 -39
- package/dist/router/paths.js +40 -34
- package/dist/router/record-from-location.d.ts +1 -1
- package/dist/router/record-from-location.js +25 -17
- package/dist/store/adapters.js +51 -55
- package/dist/store/micro-store.js +57 -59
- package/dist/store/shallow-equal.d.ts +12 -0
- package/dist/store/shallow-equal.js +20 -0
- package/dist/store/store.js +54 -57
- package/package.json +13 -11
|
@@ -1,274 +1,305 @@
|
|
|
1
|
-
import { MutationObserver, QueryObserver } from "@tanstack/query-core";
|
|
2
1
|
import { ValidationError } from "../data/errors.js";
|
|
3
2
|
import { createMutationOptions, deleteMutationOptions, updateMutationOptions } from "../data/mutation-options.js";
|
|
4
3
|
import { getOneOptions } from "../data/query-options.js";
|
|
5
4
|
import { createMicroStore } from "../store/micro-store.js";
|
|
6
5
|
import { resolveNotificationKey } from "./translate-fallback.js";
|
|
6
|
+
import { MutationObserver, QueryObserver } from "@tanstack/query-core";
|
|
7
|
+
// src/controller/record-controllers.ts
|
|
8
|
+
/**
|
|
9
|
+
* THE shared save/delete flow (behaviors 9 + 10) for the edit, create and
|
|
10
|
+
* delete controllers — one implementation of: the `saving` flag (counter-
|
|
11
|
+
* guarded so overlapping mutations cannot flicker it false early), attached
|
|
12
|
+
* vs transient observer selection, `ValidationError` RESOLVING with
|
|
13
|
+
* `fieldErrors` (no notify), other errors notifying
|
|
14
|
+
* `chelone.notification.http_error`, caller onSuccess/onError REPLACING the
|
|
15
|
+
* defaults, the success notify, and MutationObserver release.
|
|
16
|
+
*
|
|
17
|
+
* MutationObserver RELEASE is explicit: mutate() attaches the observer to the
|
|
18
|
+
* built Mutation (clearing its GC timeout) and query-core only detaches an
|
|
19
|
+
* observer via onUnsubscribe (needs listeners), reset(), or a later mutate().
|
|
20
|
+
* An unsubscribed observer therefore pins its last mutation (full data +
|
|
21
|
+
* previousData) in the MutationCache forever unless we reset() — done in the
|
|
22
|
+
* attach-cycle teardown and after each mutation settles (the finally block).
|
|
23
|
+
*/
|
|
7
24
|
function createMutationFlow(config) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
config.ctx.translate,
|
|
42
|
-
config.resource,
|
|
43
|
-
config.successEvent
|
|
44
|
-
),
|
|
45
|
-
{ type: "success", messageArgs: { smart_count: 1 } }
|
|
46
|
-
);
|
|
47
|
-
config.onDefaultSuccess(result, variables);
|
|
48
|
-
return void 0;
|
|
49
|
-
};
|
|
25
|
+
/** Overlapping mutations: `saving` goes false only when the LAST one settles. */
|
|
26
|
+
let pending = 0;
|
|
27
|
+
return async function run(variables, handlers = {}) {
|
|
28
|
+
if (config.isDestroyed()) return void 0;
|
|
29
|
+
const observer = config.attachedObserver() ?? config.makeObserver();
|
|
30
|
+
pending += 1;
|
|
31
|
+
config.store.setState({ saving: true });
|
|
32
|
+
let result;
|
|
33
|
+
try {
|
|
34
|
+
result = await observer.mutate(variables);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (handlers.onError) await handlers.onError(error);
|
|
37
|
+
else if (!(error instanceof ValidationError)) config.ctx.notifications?.notify("chelone.notification.http_error", {
|
|
38
|
+
type: "error",
|
|
39
|
+
messageArgs: { _: String(error) }
|
|
40
|
+
});
|
|
41
|
+
return error instanceof ValidationError ? error.fieldErrors : void 0;
|
|
42
|
+
} finally {
|
|
43
|
+
pending -= 1;
|
|
44
|
+
if (pending === 0) config.store.setState({ saving: false });
|
|
45
|
+
if (observer !== config.attachedObserver() || pending === 0) observer.reset();
|
|
46
|
+
}
|
|
47
|
+
if (handlers.onSaved) await handlers.onSaved(result);
|
|
48
|
+
if (handlers.onSuccess) {
|
|
49
|
+
await handlers.onSuccess(result);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
config.ctx.notifications?.notify(resolveNotificationKey(config.ctx.translate, config.resource, config.successEvent), {
|
|
53
|
+
type: "success",
|
|
54
|
+
messageArgs: { smart_count: 1 }
|
|
55
|
+
});
|
|
56
|
+
config.onDefaultSuccess(result, variables);
|
|
57
|
+
};
|
|
50
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Shared getOne wiring for the edit + show controllers: returns the acquire()
|
|
61
|
+
* used as the micro-store's `onSubscribed` (post-4R pattern — construction is
|
|
62
|
+
* INERT; the QueryObserver exists only while the store has subscribers).
|
|
63
|
+
*
|
|
64
|
+
* ra-core behavior 8: `retry: false`; a fetched record whose id does not
|
|
65
|
+
* LOOSELY match the requested id becomes an error state (a local consistency
|
|
66
|
+
* error — no notify); an actual fetch error notifies
|
|
67
|
+
* `chelone.notification.item_doesnt_exist` and requests a list redirect.
|
|
68
|
+
*/
|
|
51
69
|
function createRecordFetch(ctx, resource, options, store) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
});
|
|
94
|
-
const unsubscribe = observer.subscribe(onResult);
|
|
95
|
-
onResult(observer.getCurrentResult());
|
|
96
|
-
return unsubscribe;
|
|
97
|
-
};
|
|
70
|
+
let lastNotifiedError = null;
|
|
71
|
+
/** Cached so repeated observer emissions keep a STABLE error reference. */
|
|
72
|
+
let mismatchError = null;
|
|
73
|
+
const onResult = (result) => {
|
|
74
|
+
const record = result.data;
|
|
75
|
+
const recordId = record === void 0 ? void 0 : record.id;
|
|
76
|
+
if (record !== void 0 && String(recordId) !== String(options.id)) {
|
|
77
|
+
mismatchError ??= /* @__PURE__ */ new Error(`createRecordFetch: fetched record id (${String(recordId)}) does not match the requested id (${String(options.id)})`);
|
|
78
|
+
store.setState({
|
|
79
|
+
record: void 0,
|
|
80
|
+
isPending: false,
|
|
81
|
+
isFetching: result.isFetching,
|
|
82
|
+
error: mismatchError
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
mismatchError = null;
|
|
87
|
+
store.setState({
|
|
88
|
+
record,
|
|
89
|
+
isPending: result.isPending,
|
|
90
|
+
isFetching: result.isFetching,
|
|
91
|
+
error: result.error ?? null
|
|
92
|
+
});
|
|
93
|
+
if (result.error) {
|
|
94
|
+
if (result.error !== lastNotifiedError) {
|
|
95
|
+
lastNotifiedError = result.error;
|
|
96
|
+
ctx.notifications?.notify("chelone.notification.item_doesnt_exist", { type: "error" });
|
|
97
|
+
options.onRedirect?.("list", resource);
|
|
98
|
+
}
|
|
99
|
+
} else lastNotifiedError = null;
|
|
100
|
+
};
|
|
101
|
+
return function acquire() {
|
|
102
|
+
const observer = new QueryObserver(ctx.queryClient, {
|
|
103
|
+
...getOneOptions(ctx, resource, { id: options.id }),
|
|
104
|
+
retry: false,
|
|
105
|
+
enabled: options.enabled ?? true
|
|
106
|
+
});
|
|
107
|
+
const unsubscribe = observer.subscribe(onResult);
|
|
108
|
+
onResult(observer.getCurrentResult());
|
|
109
|
+
return unsubscribe;
|
|
110
|
+
};
|
|
98
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Vanilla show controller: one getOne QueryObserver mapped into a micro-store.
|
|
114
|
+
* Construction is INERT; acquisition on first subscribe, release on last
|
|
115
|
+
* unsubscribe AND on destroy-while-attached (micro-store lifecycle).
|
|
116
|
+
*/
|
|
99
117
|
function createShowController(ctx, resource, options) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
118
|
+
function acquire() {
|
|
119
|
+
return acquireFetch();
|
|
120
|
+
}
|
|
121
|
+
const store = createMicroStore({
|
|
122
|
+
record: void 0,
|
|
123
|
+
isPending: true,
|
|
124
|
+
isFetching: false,
|
|
125
|
+
error: null
|
|
126
|
+
}, { onSubscribed: acquire });
|
|
127
|
+
const acquireFetch = createRecordFetch(ctx, resource, options, store);
|
|
128
|
+
return store;
|
|
109
129
|
}
|
|
130
|
+
/**
|
|
131
|
+
* Vanilla edit controller: the show-shaped record fetch plus a `save` flow
|
|
132
|
+
* backed by a MutationObserver over `updateMutationOptions`.
|
|
133
|
+
*
|
|
134
|
+
* Lifecycle: both observers are ACQUIRED in the micro-store's onSubscribed
|
|
135
|
+
* and released on last-unsubscribe/destroy. The MutationObserver is not
|
|
136
|
+
* subscribed — `saving` has a single writer (the shared mutation flow), which
|
|
137
|
+
* also keeps the flag truthful for detached imperative saves. Save semantics
|
|
138
|
+
* (validation errors, notify, release) live in `createMutationFlow`.
|
|
139
|
+
*/
|
|
110
140
|
function createEditController(ctx, resource, options) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
previousData,
|
|
165
|
-
meta: saveOptions.meta
|
|
166
|
-
},
|
|
167
|
-
saveOptions
|
|
168
|
-
);
|
|
169
|
-
};
|
|
170
|
-
return { ...store, save };
|
|
141
|
+
const store = createMicroStore({
|
|
142
|
+
record: void 0,
|
|
143
|
+
isPending: true,
|
|
144
|
+
isFetching: false,
|
|
145
|
+
error: null,
|
|
146
|
+
saving: false
|
|
147
|
+
}, { onSubscribed: acquire });
|
|
148
|
+
let destroyed = false;
|
|
149
|
+
store.onDestroy(() => {
|
|
150
|
+
destroyed = true;
|
|
151
|
+
});
|
|
152
|
+
const acquireFetch = createRecordFetch(ctx, resource, options, store);
|
|
153
|
+
const makeMutationObserver = () => new MutationObserver(ctx.queryClient, updateMutationOptions(ctx, resource, { mutationMode: options.mutationMode }));
|
|
154
|
+
/** Non-null exactly while attached. */
|
|
155
|
+
let mutationObserver = null;
|
|
156
|
+
function acquire() {
|
|
157
|
+
const observer = makeMutationObserver();
|
|
158
|
+
mutationObserver = observer;
|
|
159
|
+
const releaseFetch = acquireFetch();
|
|
160
|
+
return () => {
|
|
161
|
+
releaseFetch();
|
|
162
|
+
observer.reset();
|
|
163
|
+
mutationObserver = null;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const runSave = createMutationFlow({
|
|
167
|
+
ctx,
|
|
168
|
+
resource,
|
|
169
|
+
store,
|
|
170
|
+
isDestroyed: () => destroyed,
|
|
171
|
+
attachedObserver: () => mutationObserver,
|
|
172
|
+
makeObserver: makeMutationObserver,
|
|
173
|
+
successEvent: "updated",
|
|
174
|
+
onDefaultSuccess: (result) => {
|
|
175
|
+
options.onRedirect?.("list", resource, options.id, result);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const save = async (data, saveOptions = {}) => {
|
|
179
|
+
if (destroyed) return void 0;
|
|
180
|
+
const previousData = store.getSnapshot().record;
|
|
181
|
+
const transform = saveOptions.transform ?? options.transform;
|
|
182
|
+
const transformed = transform ? await transform(data, { previousData }) : data;
|
|
183
|
+
return runSave({
|
|
184
|
+
id: options.id,
|
|
185
|
+
data: transformed,
|
|
186
|
+
previousData,
|
|
187
|
+
meta: saveOptions.meta
|
|
188
|
+
}, saveOptions);
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
...store,
|
|
192
|
+
save
|
|
193
|
+
};
|
|
171
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Vanilla create controller: no fetch AT ALL — construction is trivially
|
|
197
|
+
* inert and `isPending` is false from birth. The only acquired resource is
|
|
198
|
+
* the shared MutationObserver (onSubscribed attach / last-unsubscribe or
|
|
199
|
+
* destroy release); detached saves use a transient observer, exactly like
|
|
200
|
+
* edit. Save semantics live in `createMutationFlow`.
|
|
201
|
+
*/
|
|
172
202
|
function createCreateController(ctx, resource, options = {}) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
203
|
+
const store = createMicroStore({
|
|
204
|
+
record: options.record ?? {},
|
|
205
|
+
saving: false,
|
|
206
|
+
isPending: false
|
|
207
|
+
}, { onSubscribed: acquire });
|
|
208
|
+
let destroyed = false;
|
|
209
|
+
store.onDestroy(() => {
|
|
210
|
+
destroyed = true;
|
|
211
|
+
});
|
|
212
|
+
const makeMutationObserver = () => new MutationObserver(ctx.queryClient, createMutationOptions(ctx, resource));
|
|
213
|
+
/** Non-null exactly while attached. */
|
|
214
|
+
let mutationObserver = null;
|
|
215
|
+
function acquire() {
|
|
216
|
+
const observer = makeMutationObserver();
|
|
217
|
+
mutationObserver = observer;
|
|
218
|
+
return () => {
|
|
219
|
+
observer.reset();
|
|
220
|
+
mutationObserver = null;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const runSave = createMutationFlow({
|
|
224
|
+
ctx,
|
|
225
|
+
resource,
|
|
226
|
+
store,
|
|
227
|
+
isDestroyed: () => destroyed,
|
|
228
|
+
attachedObserver: () => mutationObserver,
|
|
229
|
+
makeObserver: makeMutationObserver,
|
|
230
|
+
successEvent: "created",
|
|
231
|
+
onDefaultSuccess: (result) => {
|
|
232
|
+
options.onRedirect?.("edit", resource, result.id, result);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
const save = async (data, saveOptions = {}) => {
|
|
236
|
+
if (destroyed) return void 0;
|
|
237
|
+
const transform = saveOptions.transform ?? options.transform;
|
|
238
|
+
const transformed = transform ? await transform(data) : data;
|
|
239
|
+
return runSave({
|
|
240
|
+
data: transformed,
|
|
241
|
+
meta: saveOptions.meta
|
|
242
|
+
}, saveOptions);
|
|
243
|
+
};
|
|
244
|
+
return {
|
|
245
|
+
...store,
|
|
246
|
+
save
|
|
247
|
+
};
|
|
215
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Vanilla delete controller: no fetch — like create, construction is
|
|
251
|
+
* trivially inert and the only acquired resource is the shared
|
|
252
|
+
* MutationObserver. Delete semantics live in `createMutationFlow`; the
|
|
253
|
+
* delete-specific default success additionally cleans the KV selection so a
|
|
254
|
+
* deleted row cannot linger selected in list views.
|
|
255
|
+
*/
|
|
216
256
|
function createDeleteController(ctx, resource, options = {}) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
},
|
|
265
|
-
deleteOptions
|
|
266
|
-
);
|
|
267
|
-
return { ...store, deleteOne };
|
|
257
|
+
const store = createMicroStore({ saving: false }, { onSubscribed: acquire });
|
|
258
|
+
let destroyed = false;
|
|
259
|
+
store.onDestroy(() => {
|
|
260
|
+
destroyed = true;
|
|
261
|
+
});
|
|
262
|
+
const makeMutationObserver = () => new MutationObserver(ctx.queryClient, deleteMutationOptions(ctx, resource));
|
|
263
|
+
/** Non-null exactly while attached. */
|
|
264
|
+
let mutationObserver = null;
|
|
265
|
+
function acquire() {
|
|
266
|
+
const observer = makeMutationObserver();
|
|
267
|
+
mutationObserver = observer;
|
|
268
|
+
return () => {
|
|
269
|
+
observer.reset();
|
|
270
|
+
mutationObserver = null;
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** Strips the REQUESTED id from the KV selection — immutably (fresh array). */
|
|
274
|
+
const stripSelection = (id) => {
|
|
275
|
+
const kv = ctx.store;
|
|
276
|
+
if (!kv) return;
|
|
277
|
+
const key = `${resource}.selectedIds`;
|
|
278
|
+
const selection = kv.getItem(key);
|
|
279
|
+
if (!selection?.includes(id)) return;
|
|
280
|
+
kv.setItem(key, selection.filter((existing) => existing !== id));
|
|
281
|
+
};
|
|
282
|
+
const runDelete = createMutationFlow({
|
|
283
|
+
ctx,
|
|
284
|
+
resource,
|
|
285
|
+
store,
|
|
286
|
+
isDestroyed: () => destroyed,
|
|
287
|
+
attachedObserver: () => mutationObserver,
|
|
288
|
+
makeObserver: makeMutationObserver,
|
|
289
|
+
successEvent: "deleted",
|
|
290
|
+
onDefaultSuccess: (_result, variables) => {
|
|
291
|
+
options.onRedirect?.("list", resource);
|
|
292
|
+
stripSelection(variables.id);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
const deleteOne = async (id, deleteOptions = {}) => runDelete({
|
|
296
|
+
id,
|
|
297
|
+
previousData: deleteOptions.previousData,
|
|
298
|
+
meta: deleteOptions.meta
|
|
299
|
+
}, deleteOptions);
|
|
300
|
+
return {
|
|
301
|
+
...store,
|
|
302
|
+
deleteOne
|
|
303
|
+
};
|
|
268
304
|
}
|
|
269
|
-
export {
|
|
270
|
-
createCreateController,
|
|
271
|
-
createDeleteController,
|
|
272
|
-
createEditController,
|
|
273
|
-
createShowController
|
|
274
|
-
};
|
|
305
|
+
export { createCreateController, createDeleteController, createEditController, createShowController };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rules for a record diff. No react, no vue: both DiffViewer adapters
|
|
3
|
+
* mark the same field the same way and print the same default text.
|
|
4
|
+
*/
|
|
5
|
+
import type { SourcePath } from "../resource/source-path.js";
|
|
6
|
+
export type DiffStatus = "unchanged" | "added" | "removed" | "changed";
|
|
7
|
+
export interface RecordDiffRow {
|
|
8
|
+
source: string;
|
|
9
|
+
status: DiffStatus;
|
|
10
|
+
before: unknown;
|
|
11
|
+
after: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* One row per source, in the order given. Without `sources` the rows are the
|
|
15
|
+
* top-level keys of `before` in its order, then the keys only `after` has.
|
|
16
|
+
* `null` and `undefined` both count as missing; an empty string is a value.
|
|
17
|
+
* Neither record is changed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function diffRecords<R>(before: R, after: R, sources?: readonly SourcePath<R>[]): RecordDiffRow[];
|
|
20
|
+
/** The text a viewer shows for a value when the app gives no renderer. */
|
|
21
|
+
export declare function formatDiffValue(value: unknown): string;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { isPlainObject, isRecord } from "../guards.js";
|
|
2
|
+
import { getAtPath } from "../resource/get-at-path.js";
|
|
3
|
+
import { deepEqual } from "./deep-equal.js";
|
|
4
|
+
// src/controller/record-diff.ts
|
|
5
|
+
/**
|
|
6
|
+
* Pure rules for a record diff. No react, no vue: both DiffViewer adapters
|
|
7
|
+
* mark the same field the same way and print the same default text.
|
|
8
|
+
*/
|
|
9
|
+
function topLevelKeys(record) {
|
|
10
|
+
return isRecord(record) ? Object.keys(record) : [];
|
|
11
|
+
}
|
|
12
|
+
function statusOf(before, after) {
|
|
13
|
+
const hadBefore = before != null;
|
|
14
|
+
const hasAfter = after != null;
|
|
15
|
+
if (!hadBefore && !hasAfter) return "unchanged";
|
|
16
|
+
if (!hadBefore) return "added";
|
|
17
|
+
if (!hasAfter) return "removed";
|
|
18
|
+
return deepEqual(before, after) ? "unchanged" : "changed";
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* One row per source, in the order given. Without `sources` the rows are the
|
|
22
|
+
* top-level keys of `before` in its order, then the keys only `after` has.
|
|
23
|
+
* `null` and `undefined` both count as missing; an empty string is a value.
|
|
24
|
+
* Neither record is changed.
|
|
25
|
+
*/
|
|
26
|
+
function diffRecords(before, after, sources) {
|
|
27
|
+
const beforeKeys = topLevelKeys(before);
|
|
28
|
+
return (sources ?? [...beforeKeys, ...topLevelKeys(after).filter((key) => !beforeKeys.includes(key))]).map((source) => {
|
|
29
|
+
const was = getAtPath(before, source);
|
|
30
|
+
const now = getAtPath(after, source);
|
|
31
|
+
return {
|
|
32
|
+
source,
|
|
33
|
+
status: statusOf(was, now),
|
|
34
|
+
before: was,
|
|
35
|
+
after: now
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** The text a viewer shows for a value when the app gives no renderer. */
|
|
40
|
+
function formatDiffValue(value) {
|
|
41
|
+
if (value == null) return "";
|
|
42
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) return value.toISOString();
|
|
43
|
+
if (Array.isArray(value) || isPlainObject(value)) try {
|
|
44
|
+
return JSON.stringify(value);
|
|
45
|
+
} catch {
|
|
46
|
+
return String(value);
|
|
47
|
+
}
|
|
48
|
+
return String(value);
|
|
49
|
+
}
|
|
50
|
+
export { diffRecords, formatDiffValue };
|