@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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rules for a record timeline. No react, no vue: both RecordTimeline
|
|
3
|
+
* adapters read the same entries from the same records and print the same
|
|
4
|
+
* relative time text.
|
|
5
|
+
*/
|
|
6
|
+
import type { Identifier } from "../resource/identifier.js";
|
|
7
|
+
import type { SourcePath } from "../resource/source-path.js";
|
|
8
|
+
export interface RecordTimelineEntry<R> {
|
|
9
|
+
id: Identifier;
|
|
10
|
+
record: R;
|
|
11
|
+
message: string;
|
|
12
|
+
/** `undefined` when the timestamp is missing or not a valid date. */
|
|
13
|
+
date: Date | undefined;
|
|
14
|
+
user: string | undefined;
|
|
15
|
+
icon: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
export interface ToTimelineEntriesOptions<R> {
|
|
18
|
+
messageSource: SourcePath<R>;
|
|
19
|
+
timestampSource: SourcePath<R>;
|
|
20
|
+
userSource?: SourcePath<R>;
|
|
21
|
+
iconSource?: SourcePath<R>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* One entry per record, in list order: the data provider's sort decides the
|
|
25
|
+
* order, and the records are not changed.
|
|
26
|
+
*/
|
|
27
|
+
export declare function toTimelineEntries<R extends {
|
|
28
|
+
id: Identifier;
|
|
29
|
+
}>(records: readonly R[], options: ToTimelineEntriesOptions<R>): RecordTimelineEntry<R>[];
|
|
30
|
+
/**
|
|
31
|
+
* "2 hours ago" or "in 3 days", from the distance between `date` and `now`
|
|
32
|
+
* in whole seconds, minutes, hours, days or 30-day months. It takes `now` so
|
|
33
|
+
* a caller decides what the present is.
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatRelativeTime(date: Date, now: Date, locale?: string): string;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getAtPath } from "../resource/get-at-path.js";
|
|
2
|
+
import { parseCalendarDate } from "./calendar-dates.js";
|
|
3
|
+
// src/controller/record-timeline.ts
|
|
4
|
+
/**
|
|
5
|
+
* Pure rules for a record timeline. No react, no vue: both RecordTimeline
|
|
6
|
+
* adapters read the same entries from the same records and print the same
|
|
7
|
+
* relative time text.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* One entry per record, in list order: the data provider's sort decides the
|
|
11
|
+
* order, and the records are not changed.
|
|
12
|
+
*/
|
|
13
|
+
function toTimelineEntries(records, options) {
|
|
14
|
+
const { messageSource, timestampSource, userSource, iconSource } = options;
|
|
15
|
+
return records.map((record) => {
|
|
16
|
+
const message = getAtPath(record, messageSource);
|
|
17
|
+
const user = userSource === void 0 ? void 0 : getAtPath(record, userSource);
|
|
18
|
+
const icon = iconSource === void 0 ? void 0 : getAtPath(record, iconSource);
|
|
19
|
+
return {
|
|
20
|
+
id: record.id,
|
|
21
|
+
record,
|
|
22
|
+
message: message == null ? "" : String(message),
|
|
23
|
+
date: parseCalendarDate(getAtPath(record, timestampSource)),
|
|
24
|
+
user: user == null || user === "" ? void 0 : String(user),
|
|
25
|
+
icon: icon == null || icon === "" ? void 0 : String(icon)
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
var MINUTE = 60;
|
|
30
|
+
var HOUR = 3600;
|
|
31
|
+
var DAY = 86400;
|
|
32
|
+
var MONTH = DAY * 30;
|
|
33
|
+
/**
|
|
34
|
+
* "2 hours ago" or "in 3 days", from the distance between `date` and `now`
|
|
35
|
+
* in whole seconds, minutes, hours, days or 30-day months. It takes `now` so
|
|
36
|
+
* a caller decides what the present is.
|
|
37
|
+
*/
|
|
38
|
+
function formatRelativeTime(date, now, locale) {
|
|
39
|
+
const delta = (date.getTime() - now.getTime()) / 1e3;
|
|
40
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
41
|
+
const distance = Math.abs(delta);
|
|
42
|
+
if (distance < MINUTE) return rtf.format(Math.round(delta), "second");
|
|
43
|
+
if (distance < HOUR) return rtf.format(Math.round(delta / MINUTE), "minute");
|
|
44
|
+
if (distance < DAY) return rtf.format(Math.round(delta / HOUR), "hour");
|
|
45
|
+
if (distance < MONTH) return rtf.format(Math.round(delta / DAY), "day");
|
|
46
|
+
return rtf.format(Math.round(delta / MONTH), "month");
|
|
47
|
+
}
|
|
48
|
+
export { formatRelativeTime, toTimelineEntries };
|
|
@@ -1,50 +1,60 @@
|
|
|
1
1
|
import { getAtPath } from "../resource/get-at-path.js";
|
|
2
|
+
// src/controller/record-tree.ts
|
|
3
|
+
/**
|
|
4
|
+
* Pure rule for nesting a flat record list into a tree from a parent field.
|
|
5
|
+
* No react, no vue: both TreeList adapters build the same tree from the same
|
|
6
|
+
* records, and no record disappears when the data holds a parent cycle.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Roots and siblings keep list order, so the data provider's sort decides
|
|
10
|
+
* the order. A record is a root when its parent value is null, undefined or
|
|
11
|
+
* `""`, names itself, or names no record in the list (its parent may be on
|
|
12
|
+
* another page). A cycle keeps its first record in list order as a root, with
|
|
13
|
+
* the rest beneath it. A later record that repeats an id is left out.
|
|
14
|
+
*/
|
|
2
15
|
function buildRecordTree(records, parentSource) {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return roots;
|
|
16
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
17
|
+
const order = /* @__PURE__ */ new Map();
|
|
18
|
+
for (const record of records) {
|
|
19
|
+
const id = String(record.id);
|
|
20
|
+
if (nodes.has(id)) continue;
|
|
21
|
+
order.set(id, nodes.size);
|
|
22
|
+
nodes.set(id, {
|
|
23
|
+
record,
|
|
24
|
+
children: []
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const parents = /* @__PURE__ */ new Map();
|
|
28
|
+
for (const [id, node] of nodes) {
|
|
29
|
+
const value = getAtPath(node.record, parentSource);
|
|
30
|
+
const parent = value == null || value === "" ? null : String(value);
|
|
31
|
+
parents.set(id, parent !== null && parent !== id && nodes.has(parent) ? parent : null);
|
|
32
|
+
}
|
|
33
|
+
const settled = /* @__PURE__ */ new Set();
|
|
34
|
+
for (const start of nodes.keys()) {
|
|
35
|
+
const path = [];
|
|
36
|
+
const onPath = /* @__PURE__ */ new Set();
|
|
37
|
+
let current = start;
|
|
38
|
+
while (current !== null && !settled.has(current)) {
|
|
39
|
+
if (onPath.has(current)) {
|
|
40
|
+
const cycle = path.slice(path.indexOf(current));
|
|
41
|
+
const rank = (id) => order.get(id) ?? Number.POSITIVE_INFINITY;
|
|
42
|
+
const first = cycle.reduce((a, b) => rank(a) < rank(b) ? a : b);
|
|
43
|
+
parents.set(first, null);
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
path.push(current);
|
|
47
|
+
onPath.add(current);
|
|
48
|
+
current = parents.get(current) ?? null;
|
|
49
|
+
}
|
|
50
|
+
for (const id of path) settled.add(id);
|
|
51
|
+
}
|
|
52
|
+
const roots = [];
|
|
53
|
+
for (const [id, node] of nodes) {
|
|
54
|
+
const parent = parents.get(id) ?? null;
|
|
55
|
+
const parentNode = parent === null ? void 0 : nodes.get(parent);
|
|
56
|
+
(parentNode ? parentNode.children : roots).push(node);
|
|
57
|
+
}
|
|
58
|
+
return roots;
|
|
47
59
|
}
|
|
48
|
-
export {
|
|
49
|
-
buildRecordTree
|
|
50
|
-
};
|
|
60
|
+
export { buildRecordTree };
|
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure rules behind an approval button
|
|
3
|
-
* every adapter decides the same way.
|
|
2
|
+
* Pure rules behind an approval button, a rejection button and a
|
|
3
|
+
* status-transition button, so every adapter decides the same way.
|
|
4
4
|
*
|
|
5
5
|
* These are UI rules, not security ones: the approvers list is a field on the
|
|
6
6
|
* record that the caller writes back whole, so two approvers acting at once
|
|
7
7
|
* can overwrite each other. The server has to enforce the count and who may
|
|
8
|
-
* approve.
|
|
8
|
+
* approve or reject.
|
|
9
9
|
*/
|
|
10
|
-
import type { SourceKey } from "../resource/source-path.js";
|
|
11
10
|
type Id = string | number;
|
|
12
|
-
export interface ApprovalOptions<TRecord
|
|
11
|
+
export interface ApprovalOptions<TRecord extends object> {
|
|
13
12
|
/**
|
|
14
13
|
* Record field holding the approver list, a top-level key: it is read as
|
|
15
14
|
* `record[approverSource]` and written back whole. Default `"approvers"`.
|
|
16
15
|
*/
|
|
17
|
-
approverSource?:
|
|
16
|
+
approverSource?: keyof TRecord & string;
|
|
18
17
|
/** Approvals needed. Floored, minimum 1; `NaN` reads as 2. Default 2. */
|
|
19
18
|
required?: number;
|
|
20
19
|
/** The current user. No identity means nobody can approve. */
|
|
@@ -23,21 +22,37 @@ export interface ApprovalOptions<TRecord = Record<string, unknown>> {
|
|
|
23
22
|
* Record field `buildApprovalUpdate` writes the status to, a top-level key.
|
|
24
23
|
* Default `"status"`.
|
|
25
24
|
*/
|
|
26
|
-
statusSource?:
|
|
25
|
+
statusSource?: keyof TRecord & string;
|
|
27
26
|
/** Status written while approvals are short of `required`. Default `"pending"`. */
|
|
28
|
-
pendingValue?:
|
|
27
|
+
pendingValue?: string;
|
|
29
28
|
/** Status written once approvals reach `required`. Default `"approved"`. */
|
|
30
|
-
approvedValue?:
|
|
29
|
+
approvedValue?: string;
|
|
31
30
|
}
|
|
32
31
|
/**
|
|
33
32
|
* What `buildApprovalUpdate` takes. `statusSource` is a write target, and a
|
|
34
33
|
* record often has no status field until its first approval, so it takes any
|
|
35
34
|
* string; `approverSource` is read, so it stays a key of the record.
|
|
36
35
|
*/
|
|
37
|
-
export type ApprovalUpdateOptions<TRecord
|
|
36
|
+
export type ApprovalUpdateOptions<TRecord extends object> = Omit<ApprovalOptions<TRecord>, "statusSource"> & {
|
|
38
37
|
/** Record field the status is written to. Default `"status"`. */
|
|
39
38
|
statusSource?: string;
|
|
40
39
|
};
|
|
40
|
+
export interface RejectionOptions {
|
|
41
|
+
/** The current user. No identity means nobody can reject. */
|
|
42
|
+
identityId?: Id | null;
|
|
43
|
+
/** Record field the status is written to and read from. Default `"status"`. */
|
|
44
|
+
statusSource?: string;
|
|
45
|
+
/** Status written on rejection. Default `"rejected"`. */
|
|
46
|
+
rejectedValue?: unknown;
|
|
47
|
+
/** Record field the reason is written to. Default `"rejectionNote"`. */
|
|
48
|
+
noteSource?: string;
|
|
49
|
+
/** Record field the rejecter's id is written to. Default `"rejectedBy"`. */
|
|
50
|
+
rejectedBySource?: string;
|
|
51
|
+
/** Why the record is rejected. Trimmed; an empty one is written as `null`. */
|
|
52
|
+
reason?: string;
|
|
53
|
+
/** Refuse a rejection whose trimmed reason is empty. Default `false`. */
|
|
54
|
+
requireReason?: boolean;
|
|
55
|
+
}
|
|
41
56
|
export interface ApprovalState {
|
|
42
57
|
/** Distinct approver ids, as strings. */
|
|
43
58
|
approvers: string[];
|
|
@@ -47,24 +62,31 @@ export interface ApprovalState {
|
|
|
47
62
|
isComplete: boolean;
|
|
48
63
|
canApprove: boolean;
|
|
49
64
|
}
|
|
50
|
-
export interface TransitionOptions<TRecord
|
|
65
|
+
export interface TransitionOptions<TRecord extends object> {
|
|
51
66
|
/** Record field holding the status, a top-level key. Default `"status"`. */
|
|
52
|
-
source?:
|
|
67
|
+
source?: keyof TRecord & string;
|
|
53
68
|
/** Next states by current state. */
|
|
54
69
|
transitions: Record<string, readonly string[]>;
|
|
55
70
|
/** Predicates keyed `"<from>-><to>"`. A transition with no guard is allowed. */
|
|
56
|
-
guards?: Record<string, (record:
|
|
71
|
+
guards?: Record<string, (record: TRecord) => boolean>;
|
|
57
72
|
}
|
|
58
73
|
export interface AllowedTransitions {
|
|
59
74
|
current: string;
|
|
60
75
|
allowed: string[];
|
|
61
76
|
}
|
|
62
|
-
export declare function getApprovalState<TRecord
|
|
77
|
+
export declare function getApprovalState<TRecord extends object>(record: TRecord & object, options?: ApprovalOptions<TRecord>): ApprovalState;
|
|
63
78
|
/**
|
|
64
79
|
* The patch that records `identityId`'s approval, or `null` when they cannot
|
|
65
80
|
* approve. Stored items stay as they were and the identity is appended in its
|
|
66
81
|
* own type.
|
|
67
82
|
*/
|
|
68
|
-
export declare function buildApprovalUpdate<TRecord
|
|
69
|
-
|
|
83
|
+
export declare function buildApprovalUpdate<TRecord extends object>(record: TRecord & object, options?: ApprovalUpdateOptions<TRecord>): Record<string, unknown> | null;
|
|
84
|
+
/**
|
|
85
|
+
* The patch that records `identityId`'s rejection, or `null` when they cannot
|
|
86
|
+
* reject: no identity, a blank reason while `requireReason` is set, or a record
|
|
87
|
+
* whose status already equals `rejectedValue`. All three sources are write
|
|
88
|
+
* targets, so they take any string.
|
|
89
|
+
*/
|
|
90
|
+
export declare function buildRejectionUpdate<TRecord extends object>(record: TRecord & object, options?: RejectionOptions): Record<string, unknown> | null;
|
|
91
|
+
export declare function getAllowedTransitions<TRecord extends object>(record: TRecord & object, options: TransitionOptions<TRecord>): AllowedTransitions;
|
|
70
92
|
export {};
|
|
@@ -1,74 +1,95 @@
|
|
|
1
|
-
|
|
1
|
+
// src/controller/record-workflow.ts
|
|
2
|
+
var hasOwn = (obj, key) => Object.hasOwn(obj, key);
|
|
2
3
|
function normalizeRequired(required) {
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
if (required === void 0 || Number.isNaN(required)) return 2;
|
|
5
|
+
return Math.max(1, Math.floor(required));
|
|
5
6
|
}
|
|
6
7
|
function storedItems(record, source) {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
const value = record[source];
|
|
9
|
+
return Array.isArray(value) ? value : [];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The approver list: `approverSource` when given, else the `"approvers"` field
|
|
13
|
+
* if the record has one. The default is a literal the record's type may not
|
|
14
|
+
* name, so it is read only after an `in` check.
|
|
15
|
+
*/
|
|
16
|
+
function approverItems(record, approverSource) {
|
|
17
|
+
if (approverSource !== void 0) return storedItems(record, approverSource);
|
|
18
|
+
return "approvers" in record ? storedItems(record, "approvers") : [];
|
|
9
19
|
}
|
|
10
20
|
function getApprovalState(record, options = {}) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
canApprove: hasIdentity && !hasApproved && !isComplete
|
|
27
|
-
};
|
|
21
|
+
const { approverSource, identityId } = options;
|
|
22
|
+
const required = normalizeRequired(options.required);
|
|
23
|
+
const approvers = [...new Set(approverItems(record, approverSource).map((item) => String(item)))];
|
|
24
|
+
const count = approvers.length;
|
|
25
|
+
const hasIdentity = identityId !== void 0 && identityId !== null;
|
|
26
|
+
const hasApproved = hasIdentity && approvers.includes(String(identityId));
|
|
27
|
+
const isComplete = count >= required;
|
|
28
|
+
return {
|
|
29
|
+
approvers,
|
|
30
|
+
count,
|
|
31
|
+
required,
|
|
32
|
+
hasApproved,
|
|
33
|
+
isComplete,
|
|
34
|
+
canApprove: hasIdentity && !hasApproved && !isComplete
|
|
35
|
+
};
|
|
28
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* The patch that records `identityId`'s approval, or `null` when they cannot
|
|
39
|
+
* approve. Stored items stay as they were and the identity is appended in its
|
|
40
|
+
* own type.
|
|
41
|
+
*/
|
|
29
42
|
function buildApprovalUpdate(record, options = {}) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
const { approverSource, statusSource = "status", identityId, pendingValue = "pending", approvedValue = "approved" } = options;
|
|
44
|
+
const state = getApprovalState(record, {
|
|
45
|
+
approverSource: options.approverSource,
|
|
46
|
+
required: options.required,
|
|
47
|
+
identityId
|
|
48
|
+
});
|
|
49
|
+
if (!state.canApprove || identityId === void 0 || identityId === null) return null;
|
|
50
|
+
return {
|
|
51
|
+
[approverSource ?? "approvers"]: [...approverItems(record, approverSource), identityId],
|
|
52
|
+
[statusSource]: state.count + 1 >= state.required ? approvedValue : pendingValue
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The patch that records `identityId`'s rejection, or `null` when they cannot
|
|
57
|
+
* reject: no identity, a blank reason while `requireReason` is set, or a record
|
|
58
|
+
* whose status already equals `rejectedValue`. All three sources are write
|
|
59
|
+
* targets, so they take any string.
|
|
60
|
+
*/
|
|
61
|
+
function buildRejectionUpdate(record, options = {}) {
|
|
62
|
+
const { identityId, statusSource = "status", rejectedValue = "rejected", noteSource = "rejectionNote", rejectedBySource = "rejectedBy", requireReason = false } = options;
|
|
63
|
+
const reason = options.reason?.trim() ?? "";
|
|
64
|
+
if (identityId === void 0 || identityId === null) return null;
|
|
65
|
+
if (requireReason && reason === "") return null;
|
|
66
|
+
if (Reflect.get(record, statusSource) === rejectedValue) return null;
|
|
67
|
+
return {
|
|
68
|
+
[statusSource]: rejectedValue,
|
|
69
|
+
[rejectedBySource]: identityId,
|
|
70
|
+
[noteSource]: reason === "" ? null : reason
|
|
71
|
+
};
|
|
49
72
|
}
|
|
50
73
|
function getAllowedTransitions(record, options) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
const { source, transitions, guards } = options;
|
|
75
|
+
const value = source !== void 0 ? record[source] : "status" in record ? record.status : void 0;
|
|
76
|
+
const current = String(value ?? "");
|
|
77
|
+
const next = hasOwn(transitions, current) ? transitions[current] : void 0;
|
|
78
|
+
const allowed = [];
|
|
79
|
+
for (const to of Array.isArray(next) ? next : []) {
|
|
80
|
+
if (allowed.includes(to)) continue;
|
|
81
|
+
const key = `${current}->${to}`;
|
|
82
|
+
const guard = guards && hasOwn(guards, key) ? guards[key] : void 0;
|
|
83
|
+
if (guard) try {
|
|
84
|
+
if (!guard(record)) continue;
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
allowed.push(to);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
current,
|
|
92
|
+
allowed
|
|
93
|
+
};
|
|
69
94
|
}
|
|
70
|
-
export {
|
|
71
|
-
buildApprovalUpdate,
|
|
72
|
-
getAllowedTransitions,
|
|
73
|
-
getApprovalState
|
|
74
|
-
};
|
|
95
|
+
export { buildApprovalUpdate, buildRejectionUpdate, getAllowedTransitions, getApprovalState };
|