@getaster/data 0.3.5 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/edit/edit-model.d.ts +36 -3
- package/dist/edit/edit-model.js +65 -10
- package/dist/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -15,6 +15,24 @@ export type Rule = (values: Entity) => Readonly<Record<string, string | undefine
|
|
|
15
15
|
export interface EditOptions {
|
|
16
16
|
readonly rules?: readonly Rule[];
|
|
17
17
|
}
|
|
18
|
+
/** Static defaults for one field. A page rule that names an effect overrides it. */
|
|
19
|
+
export interface EditFieldPolicy {
|
|
20
|
+
readonly readOnly?: boolean;
|
|
21
|
+
readonly disabled?: boolean;
|
|
22
|
+
readonly required?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Static edit defaults, applied through [[method:EditModel.applyPolicy]].
|
|
26
|
+
* They are defaults, not a second rule system: an effect a page rule sets
|
|
27
|
+
* wins, and the metadata's ceilings (computed, immutable, non-nullable)
|
|
28
|
+
* still apply. A form's presentation configuration carries them.
|
|
29
|
+
*/
|
|
30
|
+
export interface EditPolicy {
|
|
31
|
+
readonly readOnly?: boolean;
|
|
32
|
+
readonly disabled?: boolean;
|
|
33
|
+
/** Per-field defaults, merged over the form-wide entries. */
|
|
34
|
+
readonly fields?: Readonly<Record<string, EditFieldPolicy>>;
|
|
35
|
+
}
|
|
18
36
|
/**
|
|
19
37
|
* A field's page rules. With `clearWhenHidden`, hiding a shown field clears
|
|
20
38
|
* its value. `derived` gives the field a value from the page: cases for
|
|
@@ -114,6 +132,20 @@ export declare class EditModel {
|
|
|
114
132
|
* `explain`. Call once, before the fields are shown.
|
|
115
133
|
*/
|
|
116
134
|
applyRules(scope: PageScope, target: string, rules: Readonly<Record<string, FieldRules>>): void;
|
|
135
|
+
/**
|
|
136
|
+
* Static edit defaults, such as a form that is locked for viewing. They
|
|
137
|
+
* are defaults: a page rule that names an effect overrides it, and the
|
|
138
|
+
* metadata still refuses edits to computed or immutable fields. Passing
|
|
139
|
+
* `{}` clears them. Call it before or after `applyRules`; the state is
|
|
140
|
+
* recomputed for whoever reads it.
|
|
141
|
+
*/
|
|
142
|
+
applyPolicy(policy: EditPolicy): void;
|
|
143
|
+
/**
|
|
144
|
+
* Checks one field and shows or clears its error, leaving the other
|
|
145
|
+
* fields' errors alone. This is the per-field validation behind
|
|
146
|
+
* `validateOn: "change"` and `"exit"`; `validate()` checks everything.
|
|
147
|
+
*/
|
|
148
|
+
validateField(name: string): boolean;
|
|
117
149
|
/**
|
|
118
150
|
* Whether the field holds its derived value: suggested or replaced, not
|
|
119
151
|
* typed over. False for a field without one.
|
|
@@ -122,9 +154,10 @@ export declare class EditModel {
|
|
|
122
154
|
/** The page rules behind a field's state, with what they read now. */
|
|
123
155
|
explain(name: string): Explanation[];
|
|
124
156
|
/**
|
|
125
|
-
* A field's state:
|
|
126
|
-
*
|
|
127
|
-
*
|
|
157
|
+
* A field's state: static policy for effects no page rule names, then the
|
|
158
|
+
* page rules, then what the metadata and the record say. Computed fields
|
|
159
|
+
* are read-only, immutable ones once saved, and every field of a deleted
|
|
160
|
+
* record; a non-nullable field is required.
|
|
128
161
|
*/
|
|
129
162
|
state(name: string): ReadonlySignal<FieldState>;
|
|
130
163
|
pending(name: string): ReadonlySignal<boolean>;
|
package/dist/edit/edit-model.js
CHANGED
|
@@ -6,6 +6,8 @@ import { DEFAULT_STATE } from "../scope/scope.js";
|
|
|
6
6
|
import { keyFromText } from "../store/keys.js";
|
|
7
7
|
import { validateEntity } from "../validate/validate.js";
|
|
8
8
|
const NONE = new Set();
|
|
9
|
+
const NO_EFFECTS = new Set();
|
|
10
|
+
const EFFECTS = ["visible", "enabled", "readOnly", "required"];
|
|
9
11
|
/**
|
|
10
12
|
* Uncommitted values for one record, apart from the stored record.
|
|
11
13
|
*
|
|
@@ -42,6 +44,9 @@ export class EditModel {
|
|
|
42
44
|
#writes = [];
|
|
43
45
|
#pending = new Map();
|
|
44
46
|
#ruled = signal(new Map());
|
|
47
|
+
/** The effects each field's page rules name; an unnamed effect yields to static policy. */
|
|
48
|
+
#effects = new Map();
|
|
49
|
+
#policy = signal({});
|
|
45
50
|
#states = new Map();
|
|
46
51
|
#disposers = [];
|
|
47
52
|
/** A past change shown over its old values: read-only, never saved. */
|
|
@@ -194,6 +199,10 @@ export class EditModel {
|
|
|
194
199
|
this.#errors.value = { ...this.#errors.peek(), [name]: [state.reasons.readOnly ?? `${this.#title(name)} is read-only.`] };
|
|
195
200
|
return;
|
|
196
201
|
}
|
|
202
|
+
if (!state.enabled) {
|
|
203
|
+
this.#errors.value = { ...this.#errors.peek(), [name]: [state.reasons.enabled ?? `${this.#title(name)} is disabled.`] };
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
197
206
|
batch(() => {
|
|
198
207
|
this.#values.value = { ...this.#values.peek(), [name]: value };
|
|
199
208
|
if (this.#derived.has(name) && !this.#typed.peek().has(name))
|
|
@@ -214,6 +223,11 @@ export class EditModel {
|
|
|
214
223
|
const ruled = new Map(this.#ruled.peek());
|
|
215
224
|
for (const [name, { clearWhenHidden: _, derived, ...state }] of Object.entries(rules)) {
|
|
216
225
|
const field = this.#field(name);
|
|
226
|
+
const effects = new Set();
|
|
227
|
+
for (const effect of EFFECTS)
|
|
228
|
+
if (rules[name][effect] !== undefined)
|
|
229
|
+
effects.add(effect);
|
|
230
|
+
this.#effects.set(name, effects);
|
|
217
231
|
ruled.set(name, scope.state(`${target}.${name}`, state));
|
|
218
232
|
if (derived === undefined)
|
|
219
233
|
continue;
|
|
@@ -251,6 +265,32 @@ export class EditModel {
|
|
|
251
265
|
}));
|
|
252
266
|
}
|
|
253
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* Static edit defaults, such as a form that is locked for viewing. They
|
|
270
|
+
* are defaults: a page rule that names an effect overrides it, and the
|
|
271
|
+
* metadata still refuses edits to computed or immutable fields. Passing
|
|
272
|
+
* `{}` clears them. Call it before or after `applyRules`; the state is
|
|
273
|
+
* recomputed for whoever reads it.
|
|
274
|
+
*/
|
|
275
|
+
applyPolicy(policy) {
|
|
276
|
+
this.#policy.value = policy;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Checks one field and shows or clears its error, leaving the other
|
|
280
|
+
* fields' errors alone. This is the per-field validation behind
|
|
281
|
+
* `validateOn: "change"` and `"exit"`; `validate()` checks everything.
|
|
282
|
+
*/
|
|
283
|
+
validateField(name) {
|
|
284
|
+
this.#field(name);
|
|
285
|
+
const now = this.#check(this.#values.peek())[name];
|
|
286
|
+
const rest = { ...this.#errors.peek() };
|
|
287
|
+
if (now === undefined)
|
|
288
|
+
delete rest[name];
|
|
289
|
+
else
|
|
290
|
+
rest[name] = now;
|
|
291
|
+
this.#errors.value = rest;
|
|
292
|
+
return now === undefined;
|
|
293
|
+
}
|
|
254
294
|
/**
|
|
255
295
|
* Whether the field holds its derived value: suggested or replaced, not
|
|
256
296
|
* typed over. False for a field without one.
|
|
@@ -325,9 +365,10 @@ export class EditModel {
|
|
|
325
365
|
return rules.map((r) => r.effect === "derived" ? { ...r, overridden: this.#typed.peek().has(name), current: this.#values.peek()[name] } : r);
|
|
326
366
|
}
|
|
327
367
|
/**
|
|
328
|
-
* A field's state:
|
|
329
|
-
*
|
|
330
|
-
*
|
|
368
|
+
* A field's state: static policy for effects no page rule names, then the
|
|
369
|
+
* page rules, then what the metadata and the record say. Computed fields
|
|
370
|
+
* are read-only, immutable ones once saved, and every field of a deleted
|
|
371
|
+
* record; a non-nullable field is required.
|
|
331
372
|
*/
|
|
332
373
|
state(name) {
|
|
333
374
|
let state = this.#states.get(name);
|
|
@@ -335,8 +376,15 @@ export class EditModel {
|
|
|
335
376
|
const field = this.#field(name);
|
|
336
377
|
state = computed(() => {
|
|
337
378
|
const ruled = this.#ruled.value.get(name)?.value ?? DEFAULT_STATE;
|
|
379
|
+
const owned = this.#effects.get(name) ?? NO_EFFECTS;
|
|
380
|
+
const policy = this.#policy.value;
|
|
381
|
+
const own = policy.fields?.[name];
|
|
338
382
|
const reasons = { ...ruled.reasons };
|
|
339
|
-
|
|
383
|
+
const policyReadOnly = own?.readOnly ?? policy.readOnly ?? false;
|
|
384
|
+
const policyDisabled = own?.disabled ?? policy.disabled ?? false;
|
|
385
|
+
const policyRequired = own?.required ?? false;
|
|
386
|
+
let readOnly = owned.has("readOnly") ? ruled.readOnly : policyReadOnly;
|
|
387
|
+
let enabled = owned.has("enabled") ? ruled.enabled : !policyDisabled;
|
|
340
388
|
if (this.#replay) {
|
|
341
389
|
readOnly = true;
|
|
342
390
|
reasons.readOnly = "This is a past change.";
|
|
@@ -355,8 +403,12 @@ export class EditModel {
|
|
|
355
403
|
? `${this.#title(name)} is set by the server.`
|
|
356
404
|
: `${this.#title(name)} can't be changed once it's set.`;
|
|
357
405
|
}
|
|
358
|
-
|
|
359
|
-
|
|
406
|
+
if (!owned.has("readOnly") && policyReadOnly)
|
|
407
|
+
reasons.readOnly ??= "Read-only.";
|
|
408
|
+
if (!owned.has("enabled") && policyDisabled)
|
|
409
|
+
reasons.enabled ??= "Disabled.";
|
|
410
|
+
const required = (owned.has("required") ? ruled.required : policyRequired) || (!field.nullable && !field.computed);
|
|
411
|
+
return Object.freeze({ ...ruled, readOnly, enabled, required, reasons: Object.freeze(reasons) });
|
|
360
412
|
});
|
|
361
413
|
this.#states.set(name, state);
|
|
362
414
|
}
|
|
@@ -414,11 +466,14 @@ export class EditModel {
|
|
|
414
466
|
const mode = untracked(() => this.isNew.value) ? "create" : "update";
|
|
415
467
|
const changed = mode === "create" ? values : this.#changed(values);
|
|
416
468
|
const errors = { ...validateEntity(this.type, changed, mode) };
|
|
417
|
-
for (const
|
|
418
|
-
const
|
|
419
|
-
if (
|
|
469
|
+
for (const field of this.type.fields) {
|
|
470
|
+
const name = field.name;
|
|
471
|
+
if (field.computed || this.#hidden.value.has(name))
|
|
472
|
+
continue;
|
|
473
|
+
if ((values[name] ?? null) !== null || errors[name])
|
|
474
|
+
continue;
|
|
475
|
+
if (this.state(name).value.required)
|
|
420
476
|
errors[name] = [`${this.#title(name)} is required.`];
|
|
421
|
-
}
|
|
422
477
|
}
|
|
423
478
|
const ruled = new Set();
|
|
424
479
|
for (const rule of this.#rules) {
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export { Collection, type CollectionOptions, type CollectionStatus } from "./col
|
|
|
21
21
|
export { aggregate, countBy, countWhere, subtractExact, sum, sumValues } from "./collection/aggregate.ts";
|
|
22
22
|
export { apply, creatable, diff, invert, merge, writable, type ChangeSet, type FieldChange, type OwnedChanges } from "./changes/changeset.ts";
|
|
23
23
|
export { validateEntity, validateField, type FieldErrors } from "./validate/validate.ts";
|
|
24
|
-
export { EditModel, type Conflict, type EditDraft, type EditOptions, type FieldRules, type Rule } from "./edit/edit-model.ts";
|
|
24
|
+
export { EditModel, type Conflict, type EditDraft, type EditFieldPolicy, type EditOptions, type EditPolicy, type FieldRules, type Rule } from "./edit/edit-model.ts";
|
|
25
25
|
export { DataContext, type Saved } from "./context.ts";
|
|
26
26
|
export { Selection, selectionOptionsError, type SelectionOptions } from "./selection/selection.ts";
|
|
27
27
|
export { PageScope, toCriteria, DEFAULT_STATE, MAX_HOPS, type Case, type Condition, type Effect, type Explanation, type FieldState, type Publication, type Ruled, type ScopeName, type ScopeSchema, type SelectedValue, type StateRules, } from "./scope/scope.ts";
|