foldkit-crud 0.3.0 → 0.4.1
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/README.md +302 -237
- package/dist/index.d.mts +19 -32
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +57 -67
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -11
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Option, Schema } from "effect";
|
|
2
2
|
import { Bundle } from "foldkit-bundle";
|
|
3
3
|
import { AnyEntity, EntityInput, EntityMember, IdOf, Selection, SelectionPage } from "foldkit-entity";
|
|
4
4
|
import { FormControl, Submitted } from "foldkit-form";
|
|
5
5
|
import { Metadata } from "foldkit-metadata";
|
|
6
|
-
import { ModelRef, Projection } from "foldkit-surface";
|
|
6
|
+
import { ActiveSurface, ModelRef, Projection } from "foldkit-surface";
|
|
7
7
|
import * as Submodel from "foldkit/submodel";
|
|
8
8
|
import { MutationDescriptor, MutationStatus, Page, QueryDescriptor, RemoteClient, RemoteData, RemoteError, RemoteMessage } from "foldkit-remote";
|
|
9
9
|
import { Command } from "foldkit/command";
|
|
@@ -195,15 +195,11 @@ export interface DomainLike<Root> {
|
|
|
195
195
|
};
|
|
196
196
|
get(selection: any, id: string): Projection<Root, RemoteData<any>>;
|
|
197
197
|
query(query: any, input: any, options: any): Projection<Root, RemoteData<Page<any>>>;
|
|
198
|
-
|
|
199
|
-
readonly query: string;
|
|
200
|
-
} | undefined;
|
|
201
|
-
fetch(ref: any): Command<RemoteMessage, never, RemoteClient>;
|
|
198
|
+
more(model: Root, projection: any): Option.Option<Root>;
|
|
202
199
|
refresh(model: Root, target: any): Root;
|
|
203
200
|
mutation(model: Root, requestId: string): MutationStatus;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
};
|
|
201
|
+
/** `Data.active`: a read of the domain as an active Surface of its application. */
|
|
202
|
+
active(name: string, projectionOf: (model: Root) => Option.Option<Projection<Root, any>>): ActiveSurface<Root>;
|
|
207
203
|
}
|
|
208
204
|
type Step<Root> = Update.Step<Root, RemoteMessage, RemoteClient>;
|
|
209
205
|
/** One thing a relation picker offers. */
|
|
@@ -231,17 +227,13 @@ export declare const Crud: {
|
|
|
231
227
|
fields: readonly ListColumn<keyof Members & string>[];
|
|
232
228
|
/** Type-only: the value shown, for a view to be typed by. Never read. */
|
|
233
229
|
Value: Row;
|
|
234
|
-
/** The detail where it lives: `id` is the one shown,
|
|
230
|
+
/** The detail where it lives: `id` is the one shown, none while none is. */
|
|
235
231
|
at: <Root>(where: {
|
|
236
232
|
readonly data: DomainLike<Root>;
|
|
237
|
-
readonly id: (root: Root) => string
|
|
233
|
+
readonly id: (root: Root) => Option.Option<string>;
|
|
238
234
|
}) => {
|
|
239
235
|
/** For `Data.subscriptions`: the value is fetched and retained while an id is shown. */
|
|
240
|
-
active:
|
|
241
|
-
name: Name;
|
|
242
|
-
owner: object;
|
|
243
|
-
projectionOf: (root: Root) => Projection<Root, RemoteData<any>> | undefined;
|
|
244
|
-
};
|
|
236
|
+
active: ActiveSurface<Root>;
|
|
245
237
|
value: (root: Root) => RemoteData<Row>;
|
|
246
238
|
/**
|
|
247
239
|
* Asks for the value again: `Data.refresh` over this detail, for a retry
|
|
@@ -330,17 +322,17 @@ export declare const Crud: {
|
|
|
330
322
|
*/
|
|
331
323
|
at: <Root>(where: {
|
|
332
324
|
readonly data: DomainLike<Root>;
|
|
333
|
-
|
|
325
|
+
/** The list's input from the Model; none while the list is not shown. */
|
|
326
|
+
readonly input: (root: Root) => Option.Option<Input>;
|
|
334
327
|
}) => {
|
|
335
328
|
/** For `Data.subscriptions`: the page and its rows are fetched and retained while shown. */
|
|
336
|
-
active:
|
|
337
|
-
name: Name;
|
|
338
|
-
owner: object;
|
|
339
|
-
projectionOf: (root: Root) => Projection<Root, RemoteData<Page<any>>> | undefined;
|
|
340
|
-
};
|
|
329
|
+
active: ActiveSurface<Root>;
|
|
341
330
|
page: (root: Root) => RemoteData<Page<S["Type"]>>;
|
|
342
|
-
/**
|
|
343
|
-
|
|
331
|
+
/**
|
|
332
|
+
* "Load more": the Model with the list showing one page more, for the
|
|
333
|
+
* read entry to fetch; none when it shows every row there is.
|
|
334
|
+
*/
|
|
335
|
+
more: (root: Root) => Option.Option<Root>;
|
|
344
336
|
/**
|
|
345
337
|
* Asks for the list again: `Data.refresh` over its page and rows, for a
|
|
346
338
|
* retry button. A failed read is not retried on its own. Unchanged while
|
|
@@ -409,9 +401,8 @@ export declare const Crud: {
|
|
|
409
401
|
active: {
|
|
410
402
|
name: string;
|
|
411
403
|
owner: object;
|
|
412
|
-
projectionOf: (root: Root) => Projection<Root,
|
|
413
|
-
|
|
414
|
-
}> | undefined;
|
|
404
|
+
projectionOf: (root: Root) => Option.None<Projection<Root, unknown>> | Option.Some<Projection<Root, unknown>>;
|
|
405
|
+
messages: never[];
|
|
415
406
|
};
|
|
416
407
|
};
|
|
417
408
|
/**
|
|
@@ -492,11 +483,7 @@ export declare const Crud: {
|
|
|
492
483
|
* For `Data.subscriptions`: while an id is being edited, what it loads is
|
|
493
484
|
* a requirement like any Surface's, so Remote fetches and retains it.
|
|
494
485
|
*/
|
|
495
|
-
active:
|
|
496
|
-
name: Name;
|
|
497
|
-
owner: object;
|
|
498
|
-
projectionOf: (root: Root) => Projection<Root, RemoteData<any>> | undefined;
|
|
499
|
-
};
|
|
486
|
+
active: ActiveSurface<Root>;
|
|
500
487
|
sync: Update.Step<Root, never, never>;
|
|
501
488
|
/**
|
|
502
489
|
* Wraps the application's `update` so `sync` runs after every Message:
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/display.ts","../src/sort.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAgBiB,QAAQ;;WAEd;;WAEA;;WAEA,MAAM;;;;;WAKN,OAAO,gBAAgB,OAAO;;;UAIxB,YAAY;WAClB;WACA,KAAK,MAAM,SAAS,QAAQ;WAC5B,KAAK,SAAS,YAAY,WAAW,QAAQ;;;UAIvC,cAAc;WACpB,KAAK;WACL;WACA,QAAQ;WACR,SAAS;;;UAIH;;WAEN;;WAEA;;WAEA;;WAEA;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/display.ts","../src/sort.ts","../src/index.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Option, Predicate, Schema, SchemaAST } from "effect";
|
|
2
2
|
import { Bundle } from "foldkit-bundle";
|
|
3
|
-
import { Entity, SelectionPageTypeId } from "foldkit-entity";
|
|
3
|
+
import { Entity, SchemaShape, SelectionPageTypeId, Words } from "foldkit-entity";
|
|
4
4
|
import { Form, Input } from "foldkit-form";
|
|
5
5
|
import { Metadata } from "foldkit-metadata";
|
|
6
6
|
import { Projection } from "foldkit-surface";
|
|
@@ -18,11 +18,12 @@ const key = Metadata.key("foldkit-crud/display", {
|
|
|
18
18
|
summarize: (display) => display.kind
|
|
19
19
|
});
|
|
20
20
|
const fromSchema = (schema) => {
|
|
21
|
-
const
|
|
22
|
-
const members = ast._tag === "Union" ? (ast.types ?? []).filter((member) => member._tag !== "Null" && member._tag !== "Undefined") : [ast];
|
|
21
|
+
const members = SchemaShape.present(Schema.toType(schema).ast);
|
|
23
22
|
const [only] = members;
|
|
24
|
-
if (members.length === 1 && only
|
|
25
|
-
|
|
23
|
+
if (members.length === 1 && only !== void 0) {
|
|
24
|
+
if (SchemaAST.isNumber(only)) return Number_.of({});
|
|
25
|
+
if (SchemaAST.isBoolean(only)) return Flag.of(nothing);
|
|
26
|
+
}
|
|
26
27
|
return Text.of({});
|
|
27
28
|
};
|
|
28
29
|
/** A kind of Display. The kinds below are made with it, and so is an application's. */
|
|
@@ -165,10 +166,7 @@ make: (columns) => {
|
|
|
165
166
|
* ordinary Foldkit: a Bundle, an ActiveSurface, Update Steps. Nothing is
|
|
166
167
|
* generated from an Entity alone; each capability is declared.
|
|
167
168
|
*/
|
|
168
|
-
const columnLabel = (key, member) =>
|
|
169
|
-
const title = member._tag === "Relation" ? void 0 : Schema.resolveAnnotations(member.schema)?.title;
|
|
170
|
-
return typeof title === "string" ? title : Form.labelOf(member) ?? key;
|
|
171
|
-
};
|
|
169
|
+
const columnLabel = (key, member) => Option.getOrElse(member._tag === "Relation" ? Option.none() : Words.of(member.schema).title, () => Form.labelOf(member) ?? key);
|
|
172
170
|
/** The selected members in the Selection's order, each with its label and its Display. */
|
|
173
171
|
const columnsOf = (selection) => {
|
|
174
172
|
const members = selection.entity.members;
|
|
@@ -210,29 +208,25 @@ const Crud = {
|
|
|
210
208
|
fields: columnsOf(config.selection),
|
|
211
209
|
/** Type-only: the value shown, for a view to be typed by. Never read. */
|
|
212
210
|
Value: void 0,
|
|
213
|
-
/** The detail where it lives: `id` is the one shown,
|
|
211
|
+
/** The detail where it lives: `id` is the one shown, none while none is. */
|
|
214
212
|
at: (where) => {
|
|
215
|
-
const projectionOf = (root) =>
|
|
216
|
-
const id = where.id(root);
|
|
217
|
-
return id === void 0 ? void 0 : where.data.get(config.selection, id);
|
|
218
|
-
};
|
|
213
|
+
const projectionOf = (root) => Option.map(where.id(root), (id) => where.data.get(config.selection, id));
|
|
219
214
|
return {
|
|
220
215
|
/** For `Data.subscriptions`: the value is fetched and retained while an id is shown. */
|
|
221
|
-
active:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
},
|
|
226
|
-
value: (root) => projectionOf(root)?.read(root) ?? { _tag: "Initial" },
|
|
216
|
+
active: where.data.active(name, projectionOf),
|
|
217
|
+
value: (root) => Option.match(projectionOf(root), {
|
|
218
|
+
onNone: () => ({ _tag: "Initial" }),
|
|
219
|
+
onSome: (projection) => projection.read(root)
|
|
220
|
+
}),
|
|
227
221
|
/**
|
|
228
222
|
* Asks for the value again: `Data.refresh` over this detail, for a retry
|
|
229
223
|
* button. A failed read is not retried on its own. Unchanged while no id
|
|
230
224
|
* is shown.
|
|
231
225
|
*/
|
|
232
|
-
refresh: (root) => {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
226
|
+
refresh: (root) => Option.match(projectionOf(root), {
|
|
227
|
+
onNone: () => root,
|
|
228
|
+
onSome: (projection) => where.data.refresh(root, projection)
|
|
229
|
+
})
|
|
236
230
|
};
|
|
237
231
|
}
|
|
238
232
|
}),
|
|
@@ -321,40 +315,36 @@ const Crud = {
|
|
|
321
315
|
*/
|
|
322
316
|
at: (where) => {
|
|
323
317
|
const { data, input } = where;
|
|
324
|
-
const projectionOf = (root) => {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
})
|
|
330
|
-
|
|
331
|
-
|
|
318
|
+
const projectionOf = (root) => Option.map(input(root), (value) => data.query(query, value, {
|
|
319
|
+
select: selection,
|
|
320
|
+
first: pageSize
|
|
321
|
+
}));
|
|
322
|
+
const page = (root) => Option.match(projectionOf(root), {
|
|
323
|
+
onNone: () => ({ _tag: "Initial" }),
|
|
324
|
+
onSome: (projection) => projection.read(root)
|
|
325
|
+
});
|
|
326
|
+
const active = data.active(name, projectionOf);
|
|
332
327
|
return {
|
|
333
328
|
/** For `Data.subscriptions`: the page and its rows are fetched and retained while shown. */
|
|
334
|
-
active
|
|
335
|
-
name,
|
|
336
|
-
owner: data.contract.owner ?? {},
|
|
337
|
-
projectionOf
|
|
338
|
-
},
|
|
329
|
+
active,
|
|
339
330
|
page,
|
|
340
|
-
/**
|
|
341
|
-
more:
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
},
|
|
331
|
+
/**
|
|
332
|
+
* "Load more": the Model with the list showing one page more, for the
|
|
333
|
+
* read entry to fetch; none when it shows every row there is.
|
|
334
|
+
*/
|
|
335
|
+
more: (root) => Option.flatMap(projectionOf(root), (projection) => data.more(root, projection)),
|
|
346
336
|
/**
|
|
347
337
|
* Asks for the list again: `Data.refresh` over its page and rows, for a
|
|
348
338
|
* retry button. A failed read is not retried on its own. Unchanged while
|
|
349
339
|
* the list is not shown.
|
|
350
340
|
*/
|
|
351
|
-
refresh: (root) => {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
},
|
|
341
|
+
refresh: (root) => Option.match(projectionOf(root), {
|
|
342
|
+
onNone: () => root,
|
|
343
|
+
onSome: (projection) => data.refresh(root, projection)
|
|
344
|
+
}),
|
|
355
345
|
name,
|
|
356
346
|
/** Whose requirement the list is, for anything that requires beside it. */
|
|
357
|
-
owner:
|
|
347
|
+
owner: active.owner,
|
|
358
348
|
/**
|
|
359
349
|
* One of the list's Entity through the list's Selection, by id, whether or
|
|
360
350
|
* not the query finds it now: what a picker reads to name what is chosen.
|
|
@@ -441,15 +431,17 @@ const Crud = {
|
|
|
441
431
|
}));
|
|
442
432
|
};
|
|
443
433
|
const [first] = lists;
|
|
434
|
+
if (first === void 0) throw new Error("Crud.options: give it the lists its pickers pick from; with none, it has nothing to read");
|
|
444
435
|
return Object.assign(choices, {
|
|
445
436
|
/** For `Data.subscriptions`: what the pickers hold is read, so it can be named. */
|
|
446
437
|
active: {
|
|
447
438
|
name: `${pickers.map(([key]) => key).join("+")} chosen`,
|
|
448
|
-
owner: first
|
|
439
|
+
owner: first.owner,
|
|
449
440
|
projectionOf: (root) => {
|
|
450
441
|
const rows = held(root);
|
|
451
|
-
return rows.length === 0 ?
|
|
452
|
-
}
|
|
442
|
+
return rows.length === 0 ? Option.none() : Option.some(Projection.struct(Object.fromEntries(rows.map(([key, list, id]) => [`${key}:${id}`, list.row(id)]))));
|
|
443
|
+
},
|
|
444
|
+
messages: []
|
|
453
445
|
} });
|
|
454
446
|
},
|
|
455
447
|
/**
|
|
@@ -493,7 +485,7 @@ active: {
|
|
|
493
485
|
init: () => ({ model: closed }),
|
|
494
486
|
update: (model, message) => {
|
|
495
487
|
const next = form.bundle.update(model.form, message, void 0);
|
|
496
|
-
const requestId = message
|
|
488
|
+
const requestId = Predicate.isTagged(message, "Changed") ? null : model.requestId;
|
|
497
489
|
const edited = {
|
|
498
490
|
model: {
|
|
499
491
|
...model,
|
|
@@ -579,14 +571,10 @@ active: {
|
|
|
579
571
|
* For `Data.subscriptions`: while an id is being edited, what it loads is
|
|
580
572
|
* a requirement like any Surface's, so Remote fetches and retains it.
|
|
581
573
|
*/
|
|
582
|
-
active: {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
const { mode, target } = slice.get(root);
|
|
587
|
-
return mode === "edit" && target !== null ? data.get(current, target) : void 0;
|
|
588
|
-
}
|
|
589
|
-
},
|
|
574
|
+
active: data.active(name, (root) => {
|
|
575
|
+
const { mode, target } = slice.get(root);
|
|
576
|
+
return mode === "edit" && target !== null ? Option.some(data.get(current, target)) : Option.none();
|
|
577
|
+
}),
|
|
590
578
|
sync,
|
|
591
579
|
/**
|
|
592
580
|
* Wraps the application's `update` so `sync` runs after every Message:
|
|
@@ -620,12 +608,14 @@ active: {
|
|
|
620
608
|
if (editor.mode === "closed") return "Closed";
|
|
621
609
|
const read = loaded(root);
|
|
622
610
|
if (read?._tag === "NotFound") return "NotFound";
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
611
|
+
switch (saveOf(root)._tag) {
|
|
612
|
+
case "Pending": return "Saving";
|
|
613
|
+
case "Failed": return "SaveFailed";
|
|
614
|
+
case "Applied": return "Saved";
|
|
615
|
+
case "Unknown":
|
|
616
|
+
if (editor.filled) return "Editing";
|
|
617
|
+
return read?._tag === "Failed" ? "LoadFailed" : "Loading";
|
|
618
|
+
}
|
|
629
619
|
}
|
|
630
620
|
};
|
|
631
621
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/display.ts","../src/sort.ts","../src/index.ts"],"sourcesContent":["/**\n * Displays: how one selected member shows in a list or a detail, described\n * without a renderer. It is `foldkit-form`'s `Input` from the reading side: the\n * member says what it is, a Display says how it reads, and which DOM draws it is\n * the application's.\n */\nimport { Schema } from 'effect'\nimport type { AnyEntity, EntityMember } from 'foldkit-entity'\nimport { Metadata } from 'foldkit-metadata'\n\n/**\n * How one selected member shows: the primitive every kind of Display is a value\n * of. `Display.text()` and `Display.flag()` are values of it, made by\n * `Display.kind`, which is what an application calls for a badge or a relative\n * date. The kinds here are a collection, not a special case.\n */\nexport interface Display<Data = unknown> {\n /** The name a renderer is found by. */\n readonly kind: string\n /** Whether a view draws it. A member read and not shown, usually the id, is `false`. */\n readonly shown: boolean\n /** What this kind needs: a format, a relation's target. */\n readonly data: Data\n /**\n * The value as text: what a cell says when nothing draws it specially. It is\n * never given `null` or `undefined`; `Display.show` says `nothing` for those.\n */\n readonly text: (value: unknown, words: DisplayWords) => string\n}\n\n/** A kind of Display: how its values are made, and how one is told from another. */\nexport interface DisplayKind<Data> {\n readonly kind: string\n readonly of: (data: Data) => Display<Data>\n readonly is: (display: Display) => display is Display<Data>\n}\n\n/** A selected member with its words and its Display. A list's column, a detail's line. */\nexport interface DisplayColumn<Key extends string = string> {\n readonly key: Key\n readonly label: string\n readonly member: EntityMember\n readonly display: Display\n}\n\n/** The words a Display needs that are not the value's own. */\nexport interface DisplayWords {\n /** Default `yes`. */\n readonly yes?: string\n /** Default `no`. */\n readonly no?: string\n /** What stands for nothing: `null`, `undefined`, an empty list. Default the empty string. */\n readonly nothing?: string\n /** Between the items of a list, and between the members of a nested value. Default `, `. */\n readonly separator?: string\n}\n\nconst key = Metadata.key<Display>('foldkit-crud/display', {\n // The last Display attached wins: a later annotation refines an earlier one.\n merge: displays => displays.slice(-1),\n summarize: display => display.kind,\n})\n\ninterface AstLike {\n readonly _tag: string\n readonly types?: ReadonlyArray<AstLike>\n}\n\nconst fromSchema = (schema: Schema.Top): Display => {\n const ast = Schema.toType(schema).ast as unknown as AstLike\n const members =\n ast._tag === 'Union'\n ? (ast.types ?? []).filter(member => member._tag !== 'Null' && member._tag !== 'Undefined')\n : [ast]\n const [only] = members\n if (members.length === 1 && only?._tag === 'Number') return Number_.of({})\n if (members.length === 1 && only?._tag === 'Boolean') return Flag.of(nothing)\n return Text.of({})\n}\n\n/** A kind of Display. The kinds below are made with it, and so is an application's. */\nconst kind = <Data = Record<string, never>>(\n name: string,\n spec: {\n readonly shown?: boolean\n readonly text: (data: Data, value: unknown, words: DisplayWords) => string\n },\n): DisplayKind<Data> => ({\n kind: name,\n of: data =>\n Object.freeze({\n kind: name,\n shown: spec.shown ?? true,\n data,\n text: (value: unknown, words: DisplayWords) => spec.text(data, value, words),\n }),\n is: (display): display is Display<Data> => display.kind === name,\n})\n\nconst show = (display: Display, value: unknown, words: DisplayWords = {}): string =>\n value === null || value === undefined ? (words.nothing ?? '') : display.text(value, words)\n\nconst Text = kind<{ readonly format?: ((value: unknown) => string) | undefined }>('Text', {\n text: (data, value) => (data.format === undefined ? String(value) : data.format(value)),\n})\nconst Number_ = kind<{ readonly format?: ((value: number) => string) | undefined }>('Number', {\n text: (data, value) =>\n typeof value === 'number' && data.format !== undefined ? data.format(value) : String(value),\n})\nconst Flag = kind('Flag', {\n text: (_, value, words) => (value === true ? (words.yes ?? 'yes') : (words.no ?? 'no')),\n})\nconst Hidden = kind('Hidden', { shown: false, text: () => '' })\n/** A relation read as refs: which one, with nothing to say about it but its id. */\nconst Ref = kind<{ readonly target: AnyEntity; readonly many: boolean }>('Ref', {\n text: (_, value, words) => {\n const ids = (Array.isArray(value) ? value : [value]).map(ref =>\n typeof ref === 'object' && ref !== null && 'id' in ref ? String(ref.id) : String(ref),\n )\n return ids.length === 0 ? (words.nothing ?? '') : ids.join(words.separator ?? ', ')\n },\n})\n/**\n * A relation read through a Selection: the target's own members, each with a\n * Display. `shape` is how many there are: one, a list, or a page of a list.\n */\nconst Nested = kind<{\n readonly target: AnyEntity\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n}>('Nested', {\n text: (data, value, words) => {\n const one = (item: unknown): string =>\n data.columns\n .filter(column => column.display.shown)\n .map(column =>\n show(\n column.display,\n (item as Readonly<Record<string, unknown>> | null)?.[column.key],\n words,\n ),\n )\n .filter(text => text !== '')\n .join(' ')\n const items =\n data.shape === 'one'\n ? [value]\n : data.shape === 'page'\n ? ((value as { readonly items?: ReadonlyArray<unknown> }).items ?? [])\n : (value as ReadonlyArray<unknown>)\n return items.length === 0 ? (words.nothing ?? '') : items.map(one).join(words.separator ?? ', ')\n },\n})\n\nconst nothing: Record<string, never> = Object.freeze({})\n\nexport const Display = {\n /**\n * A kind of Display. `Display.kind<{ tone: string }>('Badge', { text: (_, value) => String(value) })`\n * gives `.of(data)` to make one and `.is(display)` to tell one, exactly as the\n * kinds below were made. `text` is the floor; a view draws it specially once\n * it is given a renderer for `Badge`.\n */\n kind,\n\n // The kinds this package resolves to. A view finds its renderer by `kind`.\n Text,\n Number: Number_,\n Flag,\n Hidden,\n Ref,\n Nested,\n\n text: (format?: (value: unknown) => string): Display => Text.of({ format }),\n number: (format?: (value: number) => string): Display => Number_.of({ format }),\n flag: (): Display => Flag.of(nothing),\n /** Read, so a row can be opened or keyed by it, and not shown: usually the id. */\n hidden: (): Display => Hidden.of(nothing),\n\n /**\n * Entity metadata: how a member shows wherever it is listed, e.g.\n * `Entity.annotateMembers({ price: Display.of(Display.number(cents => ...)) })`.\n */\n of: (display: Display) => key.of(display),\n\n /**\n * The Display of one selected member, from the most to the least explicit\n * source: the member's `Display.of` metadata, how the relation was selected,\n * then the shape of its schema. `nested` is the columns of a nested Selection,\n * with `shape` saying how many the relation yields.\n */\n resolve: (\n member: EntityMember,\n nested?: {\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n },\n ): Display => {\n const [explicit] = key.get(member.metadata)\n if (explicit !== undefined) return explicit\n if (member._tag !== 'Relation') return fromSchema(member.schema as Schema.Top)\n const target = member.target()\n return nested === undefined\n ? Ref.of({ target, many: member.cardinality === 'many' })\n : Nested.of({ target, shape: nested.shape, columns: nested.columns })\n },\n\n /**\n * A value as the text its Display calls for: what a cell says when nothing\n * draws it specially. A view that wants a link or a badge reads the Display\n * and the value itself; this is the floor every view can stand on.\n */\n show,\n}\n","/**\n * Sorting a list, written once. How a list is sorted is the application's state\n * and its query's input, so this holds none of it: it is the schema of that\n * state, what a click on a header makes of it, and the shape a drawn table\n * takes. The server reads the same state and decides which columns it means.\n */\nimport { Schema } from 'effect'\n\n/** How a list is sorted: by which of its orders, which way; `null` for the server's own order. */\nexport type SortState<Column extends string> = {\n readonly by: Column\n readonly direction: 'asc' | 'desc'\n} | null\n\n/** How one column is sorted now, and the Message a click on its header sends. */\nexport interface SortedColumn<Message> {\n readonly direction?: 'asc' | 'desc' | undefined\n readonly message: Message\n}\n\nexport const Sort = {\n /**\n * The orders a list offers, by name. A name is an order the server knows, not\n * a column: which columns may sort, and what `title` means in SQL, stay the\n * server's to decide.\n */\n make: <const Columns extends readonly [string, ...string[]]>(columns: Columns) => {\n type Column = Columns[number]\n type State = SortState<Column>\n /** A click on a header: ascending, then descending, then the server's own order again. */\n const toggle = (current: State, column: Column): State =>\n current === null || current.by !== column\n ? { by: column, direction: 'asc' }\n : current.direction === 'asc'\n ? { by: column, direction: 'desc' }\n : null\n return {\n columns,\n /** For the Model field, and for the query's input. */\n Schema: Schema.NullOr(\n Schema.Struct({\n by: Schema.Literals(columns),\n direction: Schema.Literals(['asc', 'desc']),\n }),\n ) as unknown as Schema.Codec<State, State>,\n /** The server's own order. */\n none: null as State,\n toggle,\n /**\n * What a drawn table takes as `sort`: every order with how it is sorted now\n * and the Message a click sends, made from the state the click leads to.\n */\n inputs: <Message>(\n current: State,\n message: (next: State) => Message,\n ): { readonly [K in Column]: SortedColumn<Message> } =>\n Object.fromEntries(\n columns.map(column => [\n column,\n {\n direction: current?.by === column ? current.direction : undefined,\n message: message(toggle(current, column)),\n },\n ]),\n ) as { readonly [K in Column]: SortedColumn<Message> },\n }\n },\n}\n","/**\n * `foldkit-crud` — management screens assembled from parts an application\n * already has.\n *\n * An editor joins a `foldkit-form` form, the Remote mutation its value feeds,\n * and the Entity they share: it loads what the form writes, shows it, turns a\n * valid submit into the mutation, and says how that went. Everything it makes is\n * ordinary Foldkit: a Bundle, an ActiveSurface, Update Steps. Nothing is\n * generated from an Entity alone; each capability is declared.\n */\nimport { Schema } from 'effect'\nimport { Bundle } from 'foldkit-bundle'\nimport {\n Entity,\n SelectionPageTypeId,\n type AnyEntity,\n type EntityInput,\n type EntityMember,\n type Selection,\n type IdOf,\n type SelectionPage,\n} from 'foldkit-entity'\nimport { Form, Input, type FormControl, type Submitted } from 'foldkit-form'\nimport { Display, type DisplayColumn } from './display.js'\nimport type {\n MutationDescriptor,\n Page,\n QueryDescriptor,\n MutationStatus,\n RemoteClient,\n RemoteData,\n RemoteError,\n RemoteMessage,\n} from 'foldkit-remote'\nimport { Projection, type ActiveSurface, type ModelRef } from 'foldkit-surface'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type { Html, HtmlBuilder } from 'foldkit/html'\nimport * as Submodel from 'foldkit/submodel'\nimport type * as Update from 'foldkit/update'\n\n/** The parts of a `Form.make` result an editor wraps. */\nexport interface EditableForm<\n Name extends string,\n FormModel,\n FormMessage,\n Value,\n E extends AnyEntity,\n Fields extends Schema.Struct.Fields,\n Members,\n R = never,\n> {\n readonly bundle: {\n readonly name: Name\n readonly Model: Schema.Codec<FormModel, unknown>\n readonly Message: Schema.Codec<FormMessage, unknown>\n readonly update: (\n model: FormModel,\n message: FormMessage,\n args: void,\n ) => Update.ReturnWithOutMessage<FormModel, FormMessage, Submitted<Value>, R>\n }\n readonly input: EntityInput<E, Fields, Members>\n readonly initial: FormModel\n readonly fill: (model: FormModel, values: Partial<Value>) => { readonly model: FormModel }\n}\n\n/**\n * An editor's slice of the parent Model. `target` is the id being edited, or\n * `null` for a new one; `filled` is whether the loaded value has been shown;\n * `requestId` is the save in progress or last settled.\n */\nexport interface EditorModel<FormModel> {\n readonly form: FormModel\n readonly mode: 'closed' | 'new' | 'edit'\n readonly target: string | null\n readonly filled: boolean\n readonly requestId: string | null\n}\n\n/**\n * A remover's slice of the parent Model: the id it was asked to delete and is\n * waiting on a yes for, and the delete in progress or last settled.\n */\nexport interface RemoverModel {\n readonly target: string | null\n readonly requestId: string | null\n}\n\n/** A remover's out Message: the user said yes to deleting `id`. */\nexport interface ConfirmedRemoval {\n readonly _tag: 'Confirmed'\n readonly id: string\n}\n\nexport type RemoverStatus =\n | 'Idle'\n /** Asked to delete an id, and waiting for a yes or a no. */\n | 'Confirming'\n | 'Deleting'\n | 'Deleted'\n | 'DeleteFailed'\n\nexport type EditorStatus =\n | 'Closed'\n /** Editing an id whose current values have not arrived. */\n | 'Loading'\n | 'NotFound'\n | 'LoadFailed'\n | 'Editing'\n | 'Saving'\n | 'Saved'\n | 'SaveFailed'\n\n/** The parts of a bound Remote domain an editor uses. */\nexport interface DomainLike<Root> {\n // Method syntax: the descriptors are checked where the editor is made, not here.\n mutate(\n model: Root,\n mutation: any,\n input: any,\n ): {\n readonly model: Root\n readonly requestId: string\n readonly command: Command<RemoteMessage, never, RemoteClient>\n }\n get(selection: any, id: string): Projection<Root, RemoteData<any>>\n query(query: any, input: any, options: any): Projection<Root, RemoteData<Page<any>>>\n next(model: Root, projection: any): { readonly query: string } | undefined\n fetch(ref: any): Command<RemoteMessage, never, RemoteClient>\n refresh(model: Root, target: any): Root\n mutation(model: Root, requestId: string): MutationStatus\n readonly contract: { readonly owner?: object | undefined }\n}\n\ntype Step<Root> = Update.Step<Root, RemoteMessage, RemoteClient>\n\n/** One thing a relation picker offers. */\nexport interface Choice {\n readonly value: string\n readonly label: string\n}\n\n/**\n * One column of a list, or one line of a detail: a member the Selection reads,\n * with its words and its `Display`.\n */\nexport type ListColumn<Key extends string = string> = DisplayColumn<Key>\n\nconst columnLabel = (key: string, member: EntityMember): string => {\n const title =\n member._tag === 'Relation'\n ? undefined\n : Schema.resolveAnnotations(member.schema as Schema.Top)?.title\n return typeof title === 'string' ? title : (Form.labelOf(member) ?? key)\n}\n\ntype AnySelection = Selection<string, unknown, Schema.Constraint>\n\n/** The selected members in the Selection's order, each with its label and its Display. */\nconst columnsOf = <Members>(\n selection: Selection<string, Members, Schema.Constraint>,\n): ReadonlyArray<ListColumn<keyof Members & string>> => {\n const members: Readonly<Record<string, EntityMember>> = selection.entity.members\n const selected = selection.members as Readonly<\n Record<string, true | AnySelection | SelectionPage<string, Schema.Constraint>>\n >\n return Object.keys(selected).map(key => {\n const member = members[key]!\n const how = selected[key]!\n // A relation read through a Selection shows the target's own members.\n const nested =\n how === true || member._tag !== 'Relation'\n ? undefined\n : SelectionPageTypeId in how\n ? { shape: 'page' as const, columns: columnsOf(how.selection) }\n : {\n shape: member.cardinality === 'many' ? ('many' as const) : ('one' as const),\n columns: columnsOf(how),\n }\n return {\n key: key as keyof Members & string,\n label: columnLabel(key, member),\n member,\n display: Display.resolve(member, nested),\n }\n })\n}\n\nconst RemoverMessage = defineMessageUnion({ Confirmed: {}, Cancelled: {} })\ntype RemoverMessage = typeof RemoverMessage.Type\n\nconst idle: RemoverModel = { target: null, requestId: null }\n\nexport const Crud = {\n /**\n * One Entity read through a Selection, for a page that shows it. Like a list\n * it holds no state: the value is Remote's and the id is the application's.\n */\n detail: <const Name extends string, EntityName extends string, Members, Row>(\n name: Name,\n config: {\n readonly selection: Selection<EntityName, Members, Schema.Constraint & { readonly Type: Row }>\n },\n ) => ({\n name,\n /** The selected members in the Selection's order, each with its label. */\n fields: columnsOf(config.selection),\n /** Type-only: the value shown, for a view to be typed by. Never read. */\n Value: undefined as unknown as Row,\n\n /** The detail where it lives: `id` is the one shown, or `undefined` while none is. */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly id: (root: Root) => string | undefined\n }) => {\n const projectionOf = (root: Root) => {\n const id = where.id(root)\n return id === undefined ? undefined : where.data.get(config.selection, id)\n }\n return {\n /** For `Data.subscriptions`: the value is fetched and retained while an id is shown. */\n active: {\n name,\n owner: where.data.contract.owner ?? {},\n projectionOf,\n } satisfies ActiveSurface<Root>,\n value: (root: Root): RemoteData<Row> =>\n projectionOf(root)?.read(root) ?? { _tag: 'Initial' },\n /**\n * Asks for the value again: `Data.refresh` over this detail, for a retry\n * button. A failed read is not retried on its own. Unchanged while no id\n * is shown.\n */\n refresh: (root: Root): Root => {\n const projection = projectionOf(root)\n return projection === undefined ? root : where.data.refresh(root, projection)\n },\n }\n },\n }),\n\n /**\n * Deleting through one mutation, with a yes in between: asked, confirmed,\n * deleted. What the mutation's input is for an id is `input`'s to say, and the\n * id it names there (`(id: PostId) => ...`) is the id `ask` takes.\n */\n remover: <\n const Name extends string,\n Input,\n Id extends string = string,\n const Key extends keyof Input = never,\n >(\n name: Name,\n config:\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n readonly input: (id: Id) => Input\n }\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n /**\n * The key of the mutation's input that holds the id, when that is all the\n * input is: `id: 'id'` for `{ id }`. The id's type is that key's.\n */\n readonly id: Key & (Input extends Readonly<Record<Key, string>> ? Key : never)\n },\n ) => {\n type AskedId = [Key] extends [never] ? Id : Input[Key] & string\n const inputFor = (id: string): Input =>\n 'input' in config ? config.input(id as Id) : ({ [config.id]: id } as Input)\n const bundle = Bundle.make(name, {\n Model: Schema.Struct({\n target: Schema.NullOr(Schema.String),\n requestId: Schema.NullOr(Schema.String),\n }) as unknown as Schema.Codec<RemoverModel, unknown>,\n Message: RemoverMessage,\n init: () => ({ model: idle }),\n update: (model: RemoverModel, message: RemoverMessage) =>\n message._tag === 'Cancelled' || model.target === null\n ? { model: idle }\n : { model, outMessage: { _tag: 'Confirmed', id: model.target } as ConfirmedRemoval },\n helpers: {\n /** Asks whether to delete `id`. Nothing is deleted until `Confirmed`. */\n ask: (_: RemoverModel, id: AskedId) => ({ model: { target: id, requestId: null } }),\n dismiss: () => ({ model: idle }),\n },\n })\n\n return {\n bundle,\n /** The remover's Messages, for the yes and the no of a confirmation. */\n Message: RemoverMessage,\n\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, RemoverModel>\n }) => {\n const { data, model: slice } = where\n const outcome = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n return {\n /** For the placement: a yes becomes the mutation, and its request is remembered. */\n onOut:\n (confirmed: ConfirmedRemoval): Step<Root> =>\n root => {\n const started = data.mutate(root, config.mutation, inputFor(confirmed.id))\n return {\n model: slice.set(started.model, {\n target: confirmed.id,\n requestId: started.requestId,\n }),\n commands: [started.command],\n }\n },\n\n /** The id being asked about or deleted, for the words of a confirmation. */\n target: (root: Root): AskedId | null => slice.get(root).target as AskedId | null,\n\n status: (root: Root): RemoverStatus => {\n const { target } = slice.get(root)\n const deleting = outcome(root)\n return deleting._tag === 'Pending'\n ? 'Deleting'\n : deleting._tag === 'Applied'\n ? 'Deleted'\n : deleting._tag === 'Failed'\n ? 'DeleteFailed'\n : target === null\n ? 'Idle'\n : 'Confirming'\n },\n\n /** Why the last delete failed, while `status` is `DeleteFailed`. */\n error: (root: Root): RemoteError | undefined => {\n const deleting = outcome(root)\n return deleting._tag === 'Failed' ? deleting.error : undefined\n },\n }\n },\n }\n },\n\n /**\n * A list over one query: which rows to show is the query's, what to show of\n * each is the Selection's. The pages live in Remote; the list holds no state.\n */\n list: <\n const Name extends string,\n EntityName extends string,\n Members,\n S extends Schema.Constraint,\n Input,\n >(\n name: Name,\n config: {\n readonly query: QueryDescriptor<string, Input, any>\n // The row is the Selection's own value, so it is typed with or without a `choice`.\n readonly selection: Selection<EntityName, Members, S>\n /** How many rows a page holds. Default 25. */\n readonly pageSize?: number\n /**\n * How a row reads as a choice in a relation picker: its id, and the words\n * that identify it to a person. Give it to a list that feeds pickers.\n */\n readonly choice?: {\n readonly value: (row: S['Type']) => string\n readonly label: (row: S['Type']) => string\n }\n },\n ) => {\n const { query, selection, pageSize = 25, choice } = config\n const columns = columnsOf(selection)\n\n return {\n name,\n /** The selected members in the Selection's order, each with its label. */\n columns,\n /** Type-only: a row, for a view to be typed by. Never read. */\n Row: undefined as unknown as S['Type'],\n\n /**\n * The list where it lives. `input` is the query's input as the Model has\n * it (filters, a search term), or `undefined` while the list is not shown.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly input: (root: Root) => Input | undefined\n }) => {\n const { data, input } = where\n const projectionOf = (root: Root) => {\n const value = input(root)\n return value === undefined\n ? undefined\n : data.query(query, value, { select: selection, first: pageSize })\n }\n const page = (root: Root): RemoteData<Page<S['Type']>> =>\n projectionOf(root)?.read(root) ?? { _tag: 'Initial' }\n\n return {\n /** For `Data.subscriptions`: the page and its rows are fetched and retained while shown. */\n active: {\n name,\n owner: data.contract.owner ?? {},\n projectionOf,\n } satisfies ActiveSurface<Root>,\n\n page,\n\n /** The Command that loads the next page onto this one, or `undefined` when there is none. */\n more: (root: Root): Command<RemoteMessage, never, RemoteClient> | undefined => {\n const projection = projectionOf(root)\n const next = projection === undefined ? undefined : data.next(root, projection)\n return next === undefined ? undefined : data.fetch(next)\n },\n\n /**\n * Asks for the list again: `Data.refresh` over its page and rows, for a\n * retry button. A failed read is not retried on its own. Unchanged while\n * the list is not shown.\n */\n refresh: (root: Root): Root => {\n const projection = projectionOf(root)\n return projection === undefined ? root : data.refresh(root, projection)\n },\n\n name,\n /** Whose requirement the list is, for anything that requires beside it. */\n owner: data.contract.owner ?? {},\n /**\n * One of the list's Entity through the list's Selection, by id, whether or\n * not the query finds it now: what a picker reads to name what is chosen.\n */\n row: (id: string): Projection<Root, RemoteData<any>> => data.get(selection, id),\n /** The Entity the rows are of, which is how a picker finds the list for its target. */\n entity: selection.entity as AnyEntity,\n /** Whether the list was given a `choice`, so its rows can be a picker's choices. */\n offersChoices: choice !== undefined,\n\n /**\n * The loaded rows as a picker's choices, read through `choice`. Listing a\n * relation's target is this query, which the application chose and the\n * server authorizes; nothing is read because a relation exists.\n */\n choices: (root: Root): ReadonlyArray<Choice> => {\n if (choice === undefined)\n throw new Error(`Crud list \"${name}\": give it a \"choice\" to use its rows in a picker`)\n const read = page(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? read.value.items.map(row => ({\n value: choice.value(row),\n label: choice.label(row),\n }))\n : []\n },\n\n /** That row as a choice, once it is read. */\n choiceOf: (root: Root, id: string): Choice | undefined => {\n if (choice === undefined) return undefined\n const read = data.get(selection, id).read(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? { value: choice.value(read.value), label: choice.label(read.value) }\n : undefined\n },\n }\n },\n }\n },\n\n /**\n * A form's Submodel view as the view of the editor that wraps it, for\n * `Bundle.withView`: the editor's Messages are the form's, and its Model holds\n * the form's under `form`.\n *\n * ```ts\n * Editor.bundle.pipe(Bundle.withView(Crud.editorView(FormView.submodel(form, view))))\n * ```\n */\n editorView: <FormModel, Message, ViewInputs>(\n view: Submodel.View<FormModel, Message, ViewInputs>,\n ): Submodel.View<EditorModel<FormModel>, Message, ViewInputs> =>\n Submodel.defineView<EditorModel<FormModel>, Message, ViewInputs>(((\n model: EditorModel<FormModel>,\n inputs: ViewInputs,\n h: HtmlBuilder<Message>,\n ): Html =>\n (view as (model: FormModel, inputs: ViewInputs, h: HtmlBuilder<Message>) => Html)(\n model.form,\n inputs,\n h,\n )) as never),\n\n /**\n * The choices of every relation picker in a form, from the lists of their\n * targets: `foldkit-mixins-form`'s `options`, keyed by the form's keys. A\n * picker whose target no list here is over is a wiring mistake, reported now.\n *\n * With `chosen`, the form's Model as the page has it, what a picker already\n * holds stays among its choices when the list no longer finds it (a search\n * narrowed it, or it is on a later page), and `active` requires those rows so\n * their words are there. Give `chosen` to any form whose pickers search.\n */\n options: <Key extends string, Root, FormModel = never>(\n form: {\n readonly controls: ReadonlyArray<FormControl<Key>>\n readonly field?: (model: FormModel, key: never) => { readonly value: unknown }\n },\n lists: ReadonlyArray<{\n readonly name: string\n readonly entity: AnyEntity\n readonly offersChoices: boolean\n readonly owner: object\n readonly choices: (root: Root) => ReadonlyArray<Choice>\n readonly row: (id: string) => Projection<Root, RemoteData<any>>\n readonly choiceOf: (root: Root, id: string) => Choice | undefined\n }>,\n options: { readonly chosen?: (root: Root) => FormModel | undefined } = {},\n ) => {\n const pickers = form.controls.flatMap(({ key, control }) => {\n if (!Input.RelationOne.is(control) && !Input.RelationMany.is(control)) return []\n const { target } = control.data\n const list = lists.find(candidate => Entity.same(candidate.entity, target))\n if (list === undefined)\n throw new Error(\n `Crud.options: \"${key}\" picks a ${target.name}, and no list given is over ${target.name}`,\n )\n if (!list.offersChoices)\n throw new Error(\n `Crud.options: \"${key}\" would pick from list \"${list.name}\", which has no \"choice\"`,\n )\n return [[key, list] as const]\n })\n\n /** The ids each picker holds now: the draft of a `one`, or of a `many`. */\n const held = (root: Root): ReadonlyArray<readonly [Key, (typeof lists)[number], string]> => {\n const model = options.chosen?.(root)\n const field = form.field as\n ((model: FormModel, key: string) => { readonly value: unknown }) | undefined\n if (model === undefined || field === undefined) return []\n return pickers.flatMap(([key, list]) => {\n const draft = field(model, key).value\n const ids = Array.isArray(draft) ? draft : typeof draft === 'string' ? [draft] : []\n return ids\n .filter((id): id is string => typeof id === 'string' && id !== '')\n .map(id => [key, list, id] as const)\n })\n }\n\n const choices = (root: Root): { readonly [K in Key]?: ReadonlyArray<Choice> } => {\n const chosen = held(root)\n return Object.fromEntries(\n pickers.map(([key, list]) => {\n const listed = list.choices(root)\n const missing = chosen\n .filter(([heldBy, , id]) => heldBy === key && !listed.some(item => item.value === id))\n .flatMap(([, , id]) => list.choiceOf(root, id) ?? [])\n // What is chosen leads, so it is in reach whatever the list shows.\n return [key, [...missing, ...listed]]\n }),\n ) as unknown as { readonly [K in Key]?: ReadonlyArray<Choice> }\n }\n\n const [first] = lists\n return Object.assign(choices, {\n /** For `Data.subscriptions`: what the pickers hold is read, so it can be named. */\n active: {\n name: `${pickers.map(([key]) => key).join('+')} chosen`,\n owner: first?.owner ?? {},\n projectionOf: (root: Root) => {\n const rows = held(root)\n return rows.length === 0\n ? undefined\n : Projection.struct(\n Object.fromEntries(rows.map(([key, list, id]) => [`${key}:${id}`, list.row(id)])),\n )\n },\n } satisfies ActiveSurface<Root>,\n })\n },\n\n /**\n * What the pieces on a page require of Remote, by the names given: a placed\n * editor, list, or detail, or the pickers from `Crud.options`. It is each one's\n * `active`, gathered, so handing the pieces over is enough and none is left out:\n *\n * ```ts\n * Data.wiring(Crud.actives({ posts: Posts, authors: Authors, editor: PostEditor, pickers }))\n * ```\n */\n actives: <const Pieces extends Readonly<Record<string, { readonly active: unknown }>>>(\n pieces: Pieces,\n ): { readonly [K in keyof Pieces]: Pieces[K]['active'] } =>\n Object.fromEntries(Object.entries(pieces).map(([name, piece]) => [name, piece.active])) as {\n readonly [K in keyof Pieces]: Pieces[K]['active']\n },\n\n /**\n * An editor for one form and the mutation its value feeds. The form's value\n * must be the mutation's input, which is what declaring the input once and\n * giving it to both guarantees.\n */\n editor: <\n const Name extends string,\n FormName extends string,\n FormModel,\n FormMessage extends { readonly _tag: string },\n Value,\n E extends AnyEntity,\n Fields extends Schema.Struct.Fields,\n Members,\n R = never,\n >(\n name: Name,\n config: {\n readonly form: EditableForm<FormName, FormModel, FormMessage, Value, E, Fields, Members, R>\n readonly mutation: MutationDescriptor<string, Value, any>\n },\n ) => {\n type Model = EditorModel<FormModel>\n const { form, mutation } = config\n\n const closed: Model = {\n form: form.initial,\n mode: 'closed',\n target: null,\n filled: false,\n requestId: null,\n }\n const Model = Schema.Struct({\n form: form.bundle.Model,\n mode: Schema.Literals(['closed', 'new', 'edit']),\n target: Schema.NullOr(Schema.String),\n filled: Schema.Boolean,\n requestId: Schema.NullOr(Schema.String),\n }) as unknown as Schema.Codec<Model, unknown>\n\n // The editor's Messages are the form's own: it adds state around the form,\n // not a second vocabulary.\n const bundle = Bundle.make(name, {\n Model,\n Message: form.bundle.Message,\n init: () => ({ model: closed }),\n update: (model: Model, message: FormMessage) => {\n const next = form.bundle.update(model.form, message, undefined)\n // An edit after a save starts a new round; the last save no longer describes the form.\n const requestId = message._tag === 'Changed' ? null : model.requestId\n // The form's Commands are the editor's: a check the form started has to run.\n const edited = {\n model: { ...model, form: next.model, requestId },\n ...(next.commands === undefined ? {} : { commands: next.commands }),\n }\n return next.outMessage === undefined ? edited : { ...edited, outMessage: next.outMessage }\n },\n helpers: {\n /** Edits `id`: the form is emptied, and filled once the current values arrive. */\n open: (_: Model, id: IdOf<E>) => ({\n model: { ...closed, mode: 'edit' as const, target: id },\n }),\n /** A blank form for a new one: nothing to load. */\n blank: () => ({ model: { ...closed, mode: 'new' as const, filled: true } }),\n close: () => ({ model: closed }),\n },\n })\n\n const current = Entity.selectFor(form.input)\n\n return {\n bundle,\n /** What the editor loads before it shows the form: every member the form writes. */\n selection: current,\n\n /**\n * The editor where it lives: `model` is its slice of the parent Model (the\n * field it was declared at), and `data` the Remote domain that registers the\n * mutation and the Entity.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, Model>\n }) => {\n const { data, model: slice } = where\n const loaded = (root: Root): RemoteData<any> | undefined => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.get(current, target).read(root)\n : undefined\n }\n\n /**\n * Shows the loaded value once, the first time it is there. Later refreshes\n * leave the drafts alone. A value whose latest refresh failed is still the\n * last one known good, so it fills the form too: `status` goes on saying\n * `Editing`, and the failure is the read's to show.\n */\n const sync: Update.Step<Root, never, never> = root => {\n const editor = slice.get(root)\n const read = loaded(root)\n if (editor.filled || read === undefined) return { model: root }\n const value =\n read._tag === 'Ready' || read._tag === 'Refreshing'\n ? read.value\n : read._tag === 'Failed'\n ? read.previous\n : undefined\n if (value === undefined) return { model: root }\n const values = Entity.valuesFor(form.input, value) as Partial<Value>\n return {\n model: slice.set(root, {\n ...editor,\n form: form.fill(editor.form, values).model,\n filled: true,\n }),\n }\n }\n\n const saveOf = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n\n return {\n /** For the placement: a valid submit becomes the mutation, and its request is remembered. */\n onOut:\n (submitted: Submitted<Value>): Step<Root> =>\n root => {\n const started = data.mutate(root, mutation, submitted.value)\n const editor = slice.get(started.model)\n return {\n model: slice.set(started.model, { ...editor, requestId: started.requestId }),\n commands: [started.command],\n }\n },\n\n /**\n * For `Data.subscriptions`: while an id is being edited, what it loads is\n * a requirement like any Surface's, so Remote fetches and retains it.\n */\n active: {\n name,\n owner: data.contract.owner ?? {},\n projectionOf: root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null ? data.get(current, target) : undefined\n },\n } satisfies ActiveSurface<Root>,\n\n sync,\n\n /**\n * Wraps the application's `update` so `sync` runs after every Message:\n * the value may arrive with any of them.\n */\n after:\n <Message, Result extends { readonly model: Root }>(\n update: (model: Root, message: Message) => Result,\n ) =>\n (model: Root, message: Message): Result => {\n const next = update(model, message)\n return { ...next, model: sync(next.model).model }\n },\n\n /**\n * Asks for the value being edited again: `Data.refresh`, for a retry\n * button while `status` is `LoadFailed`. A failed read is not retried on\n * its own. Unchanged while nothing is open for editing.\n */\n refresh: (root: Root): Root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.refresh(root, data.get(current, target))\n : root\n },\n\n /** Why the last save failed, while `status` is `SaveFailed`. */\n saveError: (root: Root): RemoteError | undefined => {\n const save = saveOf(root)\n return save._tag === 'Failed' ? save.error : undefined\n },\n\n /** The id being edited, typed as the Entity's own; `null` for a new one or a closed editor. */\n target: (root: Root): IdOf<E> | null => slice.get(root).target as IdOf<E> | null,\n status: (root: Root): EditorStatus => {\n const editor = slice.get(root)\n if (editor.mode === 'closed') return 'Closed'\n // Gone outranks everything: a save that landed describes a thing that no longer exists.\n const read = loaded(root)\n if (read?._tag === 'NotFound') return 'NotFound'\n const save = saveOf(root)\n if (save._tag === 'Pending') return 'Saving'\n if (save._tag === 'Failed') return 'SaveFailed'\n if (save._tag === 'Applied') return 'Saved'\n if (editor.filled) return 'Editing'\n return read?._tag === 'Failed' ? 'LoadFailed' : 'Loading'\n },\n }\n },\n }\n },\n}\n\nexport { Display, type DisplayColumn, type DisplayKind, type DisplayWords } from './display.js'\nexport { Sort, type SortState, type SortedColumn } from './sort.js'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/display.ts","../src/sort.ts","../src/index.ts"],"sourcesContent":["/**\n * Displays: how one selected member shows in a list or a detail, described\n * without a renderer. It is `foldkit-form`'s `Input` from the reading side: the\n * member says what it is, a Display says how it reads, and which DOM draws it is\n * the application's.\n */\nimport { Schema, SchemaAST } from 'effect'\nimport { SchemaShape, type AnyEntity, type EntityMember } from 'foldkit-entity'\nimport { Metadata } from 'foldkit-metadata'\n\n/**\n * How one selected member shows: the primitive every kind of Display is a value\n * of. `Display.text()` and `Display.flag()` are values of it, made by\n * `Display.kind`, which is what an application calls for a badge or a relative\n * date. The kinds here are a collection, not a special case.\n */\nexport interface Display<Data = unknown> {\n /** The name a renderer is found by. */\n readonly kind: string\n /** Whether a view draws it. A member read and not shown, usually the id, is `false`. */\n readonly shown: boolean\n /** What this kind needs: a format, a relation's target. */\n readonly data: Data\n /**\n * The value as text: what a cell says when nothing draws it specially. It is\n * never given `null` or `undefined`; `Display.show` says `nothing` for those.\n */\n readonly text: (value: unknown, words: DisplayWords) => string\n}\n\n/** A kind of Display: how its values are made, and how one is told from another. */\nexport interface DisplayKind<Data> {\n readonly kind: string\n readonly of: (data: Data) => Display<Data>\n readonly is: (display: Display) => display is Display<Data>\n}\n\n/** A selected member with its words and its Display. A list's column, a detail's line. */\nexport interface DisplayColumn<Key extends string = string> {\n readonly key: Key\n readonly label: string\n readonly member: EntityMember\n readonly display: Display\n}\n\n/** The words a Display needs that are not the value's own. */\nexport interface DisplayWords {\n /** Default `yes`. */\n readonly yes?: string\n /** Default `no`. */\n readonly no?: string\n /** What stands for nothing: `null`, `undefined`, an empty list. Default the empty string. */\n readonly nothing?: string\n /** Between the items of a list, and between the members of a nested value. Default `, `. */\n readonly separator?: string\n}\n\nconst key = Metadata.key<Display>('foldkit-crud/display', {\n // The last Display attached wins: a later annotation refines an earlier one.\n merge: displays => displays.slice(-1),\n summarize: display => display.kind,\n})\n\nconst fromSchema = (schema: Schema.Top): Display => {\n const members = SchemaShape.present(Schema.toType(schema).ast)\n const [only] = members\n if (members.length === 1 && only !== undefined) {\n if (SchemaAST.isNumber(only)) return Number_.of({})\n if (SchemaAST.isBoolean(only)) return Flag.of(nothing)\n }\n return Text.of({})\n}\n\n/** A kind of Display. The kinds below are made with it, and so is an application's. */\nconst kind = <Data = Record<string, never>>(\n name: string,\n spec: {\n readonly shown?: boolean\n readonly text: (data: Data, value: unknown, words: DisplayWords) => string\n },\n): DisplayKind<Data> => ({\n kind: name,\n of: data =>\n Object.freeze({\n kind: name,\n shown: spec.shown ?? true,\n data,\n text: (value: unknown, words: DisplayWords) => spec.text(data, value, words),\n }),\n is: (display): display is Display<Data> => display.kind === name,\n})\n\nconst show = (display: Display, value: unknown, words: DisplayWords = {}): string =>\n value === null || value === undefined ? (words.nothing ?? '') : display.text(value, words)\n\nconst Text = kind<{ readonly format?: ((value: unknown) => string) | undefined }>('Text', {\n text: (data, value) => (data.format === undefined ? String(value) : data.format(value)),\n})\nconst Number_ = kind<{ readonly format?: ((value: number) => string) | undefined }>('Number', {\n text: (data, value) =>\n typeof value === 'number' && data.format !== undefined ? data.format(value) : String(value),\n})\nconst Flag = kind('Flag', {\n text: (_, value, words) => (value === true ? (words.yes ?? 'yes') : (words.no ?? 'no')),\n})\nconst Hidden = kind('Hidden', { shown: false, text: () => '' })\n/** A relation read as refs: which one, with nothing to say about it but its id. */\nconst Ref = kind<{ readonly target: AnyEntity; readonly many: boolean }>('Ref', {\n text: (_, value, words) => {\n const ids = (Array.isArray(value) ? value : [value]).map(ref =>\n typeof ref === 'object' && ref !== null && 'id' in ref ? String(ref.id) : String(ref),\n )\n return ids.length === 0 ? (words.nothing ?? '') : ids.join(words.separator ?? ', ')\n },\n})\n/**\n * A relation read through a Selection: the target's own members, each with a\n * Display. `shape` is how many there are: one, a list, or a page of a list.\n */\nconst Nested = kind<{\n readonly target: AnyEntity\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n}>('Nested', {\n text: (data, value, words) => {\n const one = (item: unknown): string =>\n data.columns\n .filter(column => column.display.shown)\n .map(column =>\n show(\n column.display,\n (item as Readonly<Record<string, unknown>> | null)?.[column.key],\n words,\n ),\n )\n .filter(text => text !== '')\n .join(' ')\n const items =\n data.shape === 'one'\n ? [value]\n : data.shape === 'page'\n ? ((value as { readonly items?: ReadonlyArray<unknown> }).items ?? [])\n : (value as ReadonlyArray<unknown>)\n return items.length === 0 ? (words.nothing ?? '') : items.map(one).join(words.separator ?? ', ')\n },\n})\n\nconst nothing: Record<string, never> = Object.freeze({})\n\nexport const Display = {\n /**\n * A kind of Display. `Display.kind<{ tone: string }>('Badge', { text: (_, value) => String(value) })`\n * gives `.of(data)` to make one and `.is(display)` to tell one, exactly as the\n * kinds below were made. `text` is the floor; a view draws it specially once\n * it is given a renderer for `Badge`.\n */\n kind,\n\n // The kinds this package resolves to. A view finds its renderer by `kind`.\n Text,\n Number: Number_,\n Flag,\n Hidden,\n Ref,\n Nested,\n\n text: (format?: (value: unknown) => string): Display => Text.of({ format }),\n number: (format?: (value: number) => string): Display => Number_.of({ format }),\n flag: (): Display => Flag.of(nothing),\n /** Read, so a row can be opened or keyed by it, and not shown: usually the id. */\n hidden: (): Display => Hidden.of(nothing),\n\n /**\n * Entity metadata: how a member shows wherever it is listed, e.g.\n * `Entity.annotateMembers({ price: Display.of(Display.number(cents => ...)) })`.\n */\n of: (display: Display) => key.of(display),\n\n /**\n * The Display of one selected member, from the most to the least explicit\n * source: the member's `Display.of` metadata, how the relation was selected,\n * then the shape of its schema. `nested` is the columns of a nested Selection,\n * with `shape` saying how many the relation yields.\n */\n resolve: (\n member: EntityMember,\n nested?: {\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n },\n ): Display => {\n const [explicit] = key.get(member.metadata)\n if (explicit !== undefined) return explicit\n if (member._tag !== 'Relation') return fromSchema(member.schema as Schema.Top)\n const target = member.target()\n return nested === undefined\n ? Ref.of({ target, many: member.cardinality === 'many' })\n : Nested.of({ target, shape: nested.shape, columns: nested.columns })\n },\n\n /**\n * A value as the text its Display calls for: what a cell says when nothing\n * draws it specially. A view that wants a link or a badge reads the Display\n * and the value itself; this is the floor every view can stand on.\n */\n show,\n}\n","/**\n * Sorting a list, written once. How a list is sorted is the application's state\n * and its query's input, so this holds none of it: it is the schema of that\n * state, what a click on a header makes of it, and the shape a drawn table\n * takes. The server reads the same state and decides which columns it means.\n */\nimport { Schema } from 'effect'\n\n/** How a list is sorted: by which of its orders, which way; `null` for the server's own order. */\nexport type SortState<Column extends string> = {\n readonly by: Column\n readonly direction: 'asc' | 'desc'\n} | null\n\n/** How one column is sorted now, and the Message a click on its header sends. */\nexport interface SortedColumn<Message> {\n readonly direction?: 'asc' | 'desc' | undefined\n readonly message: Message\n}\n\nexport const Sort = {\n /**\n * The orders a list offers, by name. A name is an order the server knows, not\n * a column: which columns may sort, and what `title` means in SQL, stay the\n * server's to decide.\n */\n make: <const Columns extends readonly [string, ...string[]]>(columns: Columns) => {\n type Column = Columns[number]\n type State = SortState<Column>\n /** A click on a header: ascending, then descending, then the server's own order again. */\n const toggle = (current: State, column: Column): State =>\n current === null || current.by !== column\n ? { by: column, direction: 'asc' }\n : current.direction === 'asc'\n ? { by: column, direction: 'desc' }\n : null\n return {\n columns,\n /** For the Model field, and for the query's input. */\n Schema: Schema.NullOr(\n Schema.Struct({\n by: Schema.Literals(columns),\n direction: Schema.Literals(['asc', 'desc']),\n }),\n ) as unknown as Schema.Codec<State, State>,\n /** The server's own order. */\n none: null as State,\n toggle,\n /**\n * What a drawn table takes as `sort`: every order with how it is sorted now\n * and the Message a click sends, made from the state the click leads to.\n */\n inputs: <Message>(\n current: State,\n message: (next: State) => Message,\n ): { readonly [K in Column]: SortedColumn<Message> } =>\n Object.fromEntries(\n columns.map(column => [\n column,\n {\n direction: current?.by === column ? current.direction : undefined,\n message: message(toggle(current, column)),\n },\n ]),\n ) as { readonly [K in Column]: SortedColumn<Message> },\n }\n },\n}\n","/**\n * `foldkit-crud` — management screens assembled from parts an application\n * already has.\n *\n * An editor joins a `foldkit-form` form, the Remote mutation its value feeds,\n * and the Entity they share: it loads what the form writes, shows it, turns a\n * valid submit into the mutation, and says how that went. Everything it makes is\n * ordinary Foldkit: a Bundle, an ActiveSurface, Update Steps. Nothing is\n * generated from an Entity alone; each capability is declared.\n */\nimport { Option, Predicate, Schema } from 'effect'\nimport { Bundle } from 'foldkit-bundle'\nimport {\n Entity,\n SelectionPageTypeId,\n Words,\n type AnyEntity,\n type EntityInput,\n type EntityMember,\n type Selection,\n type IdOf,\n type SelectionPage,\n} from 'foldkit-entity'\nimport { Form, Input, type FormControl, type Submitted } from 'foldkit-form'\nimport { Display, type DisplayColumn } from './display.js'\nimport type {\n MutationDescriptor,\n Page,\n QueryDescriptor,\n MutationStatus,\n RemoteClient,\n RemoteData,\n RemoteError,\n RemoteMessage,\n} from 'foldkit-remote'\nimport { Projection, type ActiveSurface, type ModelRef } from 'foldkit-surface'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type { Html, HtmlBuilder } from 'foldkit/html'\nimport * as Submodel from 'foldkit/submodel'\nimport type * as Update from 'foldkit/update'\n\n/** The parts of a `Form.make` result an editor wraps. */\nexport interface EditableForm<\n Name extends string,\n FormModel,\n FormMessage,\n Value,\n E extends AnyEntity,\n Fields extends Schema.Struct.Fields,\n Members,\n R = never,\n> {\n readonly bundle: {\n readonly name: Name\n readonly Model: Schema.Codec<FormModel, unknown>\n readonly Message: Schema.Codec<FormMessage, unknown>\n readonly update: (\n model: FormModel,\n message: FormMessage,\n args: void,\n ) => Update.ReturnWithOutMessage<FormModel, FormMessage, Submitted<Value>, R>\n }\n readonly input: EntityInput<E, Fields, Members>\n readonly initial: FormModel\n readonly fill: (model: FormModel, values: Partial<Value>) => { readonly model: FormModel }\n}\n\n/**\n * An editor's slice of the parent Model. `target` is the id being edited, or\n * `null` for a new one; `filled` is whether the loaded value has been shown;\n * `requestId` is the save in progress or last settled.\n */\nexport interface EditorModel<FormModel> {\n readonly form: FormModel\n readonly mode: 'closed' | 'new' | 'edit'\n readonly target: string | null\n readonly filled: boolean\n readonly requestId: string | null\n}\n\n/**\n * A remover's slice of the parent Model: the id it was asked to delete and is\n * waiting on a yes for, and the delete in progress or last settled.\n */\nexport interface RemoverModel {\n readonly target: string | null\n readonly requestId: string | null\n}\n\n/** A remover's out Message: the user said yes to deleting `id`. */\nexport interface ConfirmedRemoval {\n readonly _tag: 'Confirmed'\n readonly id: string\n}\n\nexport type RemoverStatus =\n | 'Idle'\n /** Asked to delete an id, and waiting for a yes or a no. */\n | 'Confirming'\n | 'Deleting'\n | 'Deleted'\n | 'DeleteFailed'\n\nexport type EditorStatus =\n | 'Closed'\n /** Editing an id whose current values have not arrived. */\n | 'Loading'\n | 'NotFound'\n | 'LoadFailed'\n | 'Editing'\n | 'Saving'\n | 'Saved'\n | 'SaveFailed'\n\n/** The parts of a bound Remote domain an editor uses. */\nexport interface DomainLike<Root> {\n // Method syntax: the descriptors are checked where the editor is made, not here.\n mutate(\n model: Root,\n mutation: any,\n input: any,\n ): {\n readonly model: Root\n readonly requestId: string\n readonly command: Command<RemoteMessage, never, RemoteClient>\n }\n get(selection: any, id: string): Projection<Root, RemoteData<any>>\n query(query: any, input: any, options: any): Projection<Root, RemoteData<Page<any>>>\n more(model: Root, projection: any): Option.Option<Root>\n refresh(model: Root, target: any): Root\n mutation(model: Root, requestId: string): MutationStatus\n /** `Data.active`: a read of the domain as an active Surface of its application. */\n active(\n name: string,\n projectionOf: (model: Root) => Option.Option<Projection<Root, any>>,\n ): ActiveSurface<Root>\n}\n\ntype Step<Root> = Update.Step<Root, RemoteMessage, RemoteClient>\n\n/** One thing a relation picker offers. */\nexport interface Choice {\n readonly value: string\n readonly label: string\n}\n\n/**\n * One column of a list, or one line of a detail: a member the Selection reads,\n * with its words and its `Display`.\n */\nexport type ListColumn<Key extends string = string> = DisplayColumn<Key>\n\nconst columnLabel = (key: string, member: EntityMember): string =>\n Option.getOrElse(\n member._tag === 'Relation' ? Option.none() : Words.of(member.schema as Schema.Top).title,\n () => Form.labelOf(member) ?? key,\n )\n\ntype AnySelection = Selection<string, unknown, Schema.Constraint>\n\n/** The selected members in the Selection's order, each with its label and its Display. */\nconst columnsOf = <Members>(\n selection: Selection<string, Members, Schema.Constraint>,\n): ReadonlyArray<ListColumn<keyof Members & string>> => {\n const members: Readonly<Record<string, EntityMember>> = selection.entity.members\n const selected = selection.members as Readonly<\n Record<string, true | AnySelection | SelectionPage<string, Schema.Constraint>>\n >\n return Object.keys(selected).map(key => {\n const member = members[key]!\n const how = selected[key]!\n // A relation read through a Selection shows the target's own members.\n const nested =\n how === true || member._tag !== 'Relation'\n ? undefined\n : SelectionPageTypeId in how\n ? { shape: 'page' as const, columns: columnsOf(how.selection) }\n : {\n shape: member.cardinality === 'many' ? ('many' as const) : ('one' as const),\n columns: columnsOf(how),\n }\n return {\n key: key as keyof Members & string,\n label: columnLabel(key, member),\n member,\n display: Display.resolve(member, nested),\n }\n })\n}\n\nconst RemoverMessage = defineMessageUnion({ Confirmed: {}, Cancelled: {} })\ntype RemoverMessage = typeof RemoverMessage.Type\n\nconst idle: RemoverModel = { target: null, requestId: null }\n\nexport const Crud = {\n /**\n * One Entity read through a Selection, for a page that shows it. Like a list\n * it holds no state: the value is Remote's and the id is the application's.\n */\n detail: <const Name extends string, EntityName extends string, Members, Row>(\n name: Name,\n config: {\n readonly selection: Selection<EntityName, Members, Schema.Constraint & { readonly Type: Row }>\n },\n ) => ({\n name,\n /** The selected members in the Selection's order, each with its label. */\n fields: columnsOf(config.selection),\n /** Type-only: the value shown, for a view to be typed by. Never read. */\n Value: undefined as unknown as Row,\n\n /** The detail where it lives: `id` is the one shown, none while none is. */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly id: (root: Root) => Option.Option<string>\n }) => {\n const projectionOf = (root: Root) =>\n Option.map(where.id(root), id => where.data.get(config.selection, id))\n return {\n /** For `Data.subscriptions`: the value is fetched and retained while an id is shown. */\n active: where.data.active(name, projectionOf),\n value: (root: Root): RemoteData<Row> =>\n Option.match(projectionOf(root), {\n onNone: () => ({ _tag: 'Initial' }),\n onSome: projection => projection.read(root),\n }),\n /**\n * Asks for the value again: `Data.refresh` over this detail, for a retry\n * button. A failed read is not retried on its own. Unchanged while no id\n * is shown.\n */\n refresh: (root: Root): Root =>\n Option.match(projectionOf(root), {\n onNone: () => root,\n onSome: projection => where.data.refresh(root, projection),\n }),\n }\n },\n }),\n\n /**\n * Deleting through one mutation, with a yes in between: asked, confirmed,\n * deleted. What the mutation's input is for an id is `input`'s to say, and the\n * id it names there (`(id: PostId) => ...`) is the id `ask` takes.\n */\n remover: <\n const Name extends string,\n Input,\n Id extends string = string,\n const Key extends keyof Input = never,\n >(\n name: Name,\n config:\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n readonly input: (id: Id) => Input\n }\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n /**\n * The key of the mutation's input that holds the id, when that is all the\n * input is: `id: 'id'` for `{ id }`. The id's type is that key's.\n */\n readonly id: Key & (Input extends Readonly<Record<Key, string>> ? Key : never)\n },\n ) => {\n type AskedId = [Key] extends [never] ? Id : Input[Key] & string\n const inputFor = (id: string): Input =>\n 'input' in config ? config.input(id as Id) : ({ [config.id]: id } as Input)\n const bundle = Bundle.make(name, {\n Model: Schema.Struct({\n target: Schema.NullOr(Schema.String),\n requestId: Schema.NullOr(Schema.String),\n }) as unknown as Schema.Codec<RemoverModel, unknown>,\n Message: RemoverMessage,\n init: () => ({ model: idle }),\n update: (model: RemoverModel, message: RemoverMessage) =>\n message._tag === 'Cancelled' || model.target === null\n ? { model: idle }\n : { model, outMessage: { _tag: 'Confirmed', id: model.target } as ConfirmedRemoval },\n helpers: {\n /** Asks whether to delete `id`. Nothing is deleted until `Confirmed`. */\n ask: (_: RemoverModel, id: AskedId) => ({ model: { target: id, requestId: null } }),\n dismiss: () => ({ model: idle }),\n },\n })\n\n return {\n bundle,\n /** The remover's Messages, for the yes and the no of a confirmation. */\n Message: RemoverMessage,\n\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, RemoverModel>\n }) => {\n const { data, model: slice } = where\n const outcome = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n return {\n /** For the placement: a yes becomes the mutation, and its request is remembered. */\n onOut:\n (confirmed: ConfirmedRemoval): Step<Root> =>\n root => {\n const started = data.mutate(root, config.mutation, inputFor(confirmed.id))\n return {\n model: slice.set(started.model, {\n target: confirmed.id,\n requestId: started.requestId,\n }),\n commands: [started.command],\n }\n },\n\n /** The id being asked about or deleted, for the words of a confirmation. */\n target: (root: Root): AskedId | null => slice.get(root).target as AskedId | null,\n\n status: (root: Root): RemoverStatus => {\n const { target } = slice.get(root)\n const deleting = outcome(root)\n return deleting._tag === 'Pending'\n ? 'Deleting'\n : deleting._tag === 'Applied'\n ? 'Deleted'\n : deleting._tag === 'Failed'\n ? 'DeleteFailed'\n : target === null\n ? 'Idle'\n : 'Confirming'\n },\n\n /** Why the last delete failed, while `status` is `DeleteFailed`. */\n error: (root: Root): RemoteError | undefined => {\n const deleting = outcome(root)\n return deleting._tag === 'Failed' ? deleting.error : undefined\n },\n }\n },\n }\n },\n\n /**\n * A list over one query: which rows to show is the query's, what to show of\n * each is the Selection's. The pages live in Remote; the list holds no state.\n */\n list: <\n const Name extends string,\n EntityName extends string,\n Members,\n S extends Schema.Constraint,\n Input,\n >(\n name: Name,\n config: {\n readonly query: QueryDescriptor<string, Input, any>\n // The row is the Selection's own value, so it is typed with or without a `choice`.\n readonly selection: Selection<EntityName, Members, S>\n /** How many rows a page holds. Default 25. */\n readonly pageSize?: number\n /**\n * How a row reads as a choice in a relation picker: its id, and the words\n * that identify it to a person. Give it to a list that feeds pickers.\n */\n readonly choice?: {\n readonly value: (row: S['Type']) => string\n readonly label: (row: S['Type']) => string\n }\n },\n ) => {\n const { query, selection, pageSize = 25, choice } = config\n const columns = columnsOf(selection)\n\n return {\n name,\n /** The selected members in the Selection's order, each with its label. */\n columns,\n /** Type-only: a row, for a view to be typed by. Never read. */\n Row: undefined as unknown as S['Type'],\n\n /**\n * The list where it lives. `input` is the query's input as the Model has\n * it (filters, a search term), or `undefined` while the list is not shown.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n /** The list's input from the Model; none while the list is not shown. */\n readonly input: (root: Root) => Option.Option<Input>\n }) => {\n const { data, input } = where\n const projectionOf = (root: Root) =>\n Option.map(input(root), value =>\n data.query(query, value, { select: selection, first: pageSize }),\n )\n const page = (root: Root): RemoteData<Page<S['Type']>> =>\n Option.match(projectionOf(root), {\n onNone: () => ({ _tag: 'Initial' }),\n onSome: projection => projection.read(root),\n })\n\n const active = data.active(name, projectionOf)\n\n return {\n /** For `Data.subscriptions`: the page and its rows are fetched and retained while shown. */\n active,\n\n page,\n\n /**\n * \"Load more\": the Model with the list showing one page more, for the\n * read entry to fetch; none when it shows every row there is.\n */\n more: (root: Root): Option.Option<Root> =>\n Option.flatMap(projectionOf(root), projection => data.more(root, projection)),\n\n /**\n * Asks for the list again: `Data.refresh` over its page and rows, for a\n * retry button. A failed read is not retried on its own. Unchanged while\n * the list is not shown.\n */\n refresh: (root: Root): Root =>\n Option.match(projectionOf(root), {\n onNone: () => root,\n onSome: projection => data.refresh(root, projection),\n }),\n\n name,\n /** Whose requirement the list is, for anything that requires beside it. */\n owner: active.owner,\n /**\n * One of the list's Entity through the list's Selection, by id, whether or\n * not the query finds it now: what a picker reads to name what is chosen.\n */\n row: (id: string): Projection<Root, RemoteData<any>> => data.get(selection, id),\n /** The Entity the rows are of, which is how a picker finds the list for its target. */\n entity: selection.entity as AnyEntity,\n /** Whether the list was given a `choice`, so its rows can be a picker's choices. */\n offersChoices: choice !== undefined,\n\n /**\n * The loaded rows as a picker's choices, read through `choice`. Listing a\n * relation's target is this query, which the application chose and the\n * server authorizes; nothing is read because a relation exists.\n */\n choices: (root: Root): ReadonlyArray<Choice> => {\n if (choice === undefined)\n throw new Error(`Crud list \"${name}\": give it a \"choice\" to use its rows in a picker`)\n const read = page(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? read.value.items.map(row => ({\n value: choice.value(row),\n label: choice.label(row),\n }))\n : []\n },\n\n /** That row as a choice, once it is read. */\n choiceOf: (root: Root, id: string): Choice | undefined => {\n if (choice === undefined) return undefined\n const read = data.get(selection, id).read(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? { value: choice.value(read.value), label: choice.label(read.value) }\n : undefined\n },\n }\n },\n }\n },\n\n /**\n * A form's Submodel view as the view of the editor that wraps it, for\n * `Bundle.withView`: the editor's Messages are the form's, and its Model holds\n * the form's under `form`.\n *\n * ```ts\n * Editor.bundle.pipe(Bundle.withView(Crud.editorView(FormView.submodel(form, view))))\n * ```\n */\n editorView: <FormModel, Message, ViewInputs>(\n view: Submodel.View<FormModel, Message, ViewInputs>,\n ): Submodel.View<EditorModel<FormModel>, Message, ViewInputs> =>\n Submodel.defineView<EditorModel<FormModel>, Message, ViewInputs>(((\n model: EditorModel<FormModel>,\n inputs: ViewInputs,\n h: HtmlBuilder<Message>,\n ): Html =>\n (view as (model: FormModel, inputs: ViewInputs, h: HtmlBuilder<Message>) => Html)(\n model.form,\n inputs,\n h,\n )) as never),\n\n /**\n * The choices of every relation picker in a form, from the lists of their\n * targets: `foldkit-mixins-form`'s `options`, keyed by the form's keys. A\n * picker whose target no list here is over is a wiring mistake, reported now.\n *\n * With `chosen`, the form's Model as the page has it, what a picker already\n * holds stays among its choices when the list no longer finds it (a search\n * narrowed it, or it is on a later page), and `active` requires those rows so\n * their words are there. Give `chosen` to any form whose pickers search.\n */\n options: <Key extends string, Root, FormModel = never>(\n form: {\n readonly controls: ReadonlyArray<FormControl<Key>>\n readonly field?: (model: FormModel, key: never) => { readonly value: unknown }\n },\n lists: ReadonlyArray<{\n readonly name: string\n readonly entity: AnyEntity\n readonly offersChoices: boolean\n readonly owner: object\n readonly choices: (root: Root) => ReadonlyArray<Choice>\n readonly row: (id: string) => Projection<Root, RemoteData<any>>\n readonly choiceOf: (root: Root, id: string) => Choice | undefined\n }>,\n options: { readonly chosen?: (root: Root) => FormModel | undefined } = {},\n ) => {\n const pickers = form.controls.flatMap(({ key, control }) => {\n if (!Input.RelationOne.is(control) && !Input.RelationMany.is(control)) return []\n const { target } = control.data\n const list = lists.find(candidate => Entity.same(candidate.entity, target))\n if (list === undefined)\n throw new Error(\n `Crud.options: \"${key}\" picks a ${target.name}, and no list given is over ${target.name}`,\n )\n if (!list.offersChoices)\n throw new Error(\n `Crud.options: \"${key}\" would pick from list \"${list.name}\", which has no \"choice\"`,\n )\n return [[key, list] as const]\n })\n\n /** The ids each picker holds now: the draft of a `one`, or of a `many`. */\n const held = (root: Root): ReadonlyArray<readonly [Key, (typeof lists)[number], string]> => {\n const model = options.chosen?.(root)\n const field = form.field as\n ((model: FormModel, key: string) => { readonly value: unknown }) | undefined\n if (model === undefined || field === undefined) return []\n return pickers.flatMap(([key, list]) => {\n const draft = field(model, key).value\n const ids = Array.isArray(draft) ? draft : typeof draft === 'string' ? [draft] : []\n return ids\n .filter((id): id is string => typeof id === 'string' && id !== '')\n .map(id => [key, list, id] as const)\n })\n }\n\n const choices = (root: Root): { readonly [K in Key]?: ReadonlyArray<Choice> } => {\n const chosen = held(root)\n return Object.fromEntries(\n pickers.map(([key, list]) => {\n const listed = list.choices(root)\n const missing = chosen\n .filter(([heldBy, , id]) => heldBy === key && !listed.some(item => item.value === id))\n .flatMap(([, , id]) => list.choiceOf(root, id) ?? [])\n // What is chosen leads, so it is in reach whatever the list shows.\n return [key, [...missing, ...listed]]\n }),\n ) as unknown as { readonly [K in Key]?: ReadonlyArray<Choice> }\n }\n\n const [first] = lists\n if (first === undefined)\n throw new Error(\n 'Crud.options: give it the lists its pickers pick from; with none, it has nothing to read',\n )\n return Object.assign(choices, {\n /** For `Data.subscriptions`: what the pickers hold is read, so it can be named. */\n active: {\n name: `${pickers.map(([key]) => key).join('+')} chosen`,\n owner: first.owner,\n projectionOf: (root: Root) => {\n const rows = held(root)\n return rows.length === 0\n ? Option.none()\n : Option.some(\n Projection.struct(\n Object.fromEntries(rows.map(([key, list, id]) => [`${key}:${id}`, list.row(id)])),\n ),\n )\n },\n messages: [],\n } satisfies ActiveSurface<Root>,\n })\n },\n\n /**\n * What the pieces on a page require of Remote, by the names given: a placed\n * editor, list, or detail, or the pickers from `Crud.options`. It is each one's\n * `active`, gathered, so handing the pieces over is enough and none is left out:\n *\n * ```ts\n * Data.wiring(Crud.actives({ posts: Posts, authors: Authors, editor: PostEditor, pickers }))\n * ```\n */\n actives: <const Pieces extends Readonly<Record<string, { readonly active: unknown }>>>(\n pieces: Pieces,\n ): { readonly [K in keyof Pieces]: Pieces[K]['active'] } =>\n Object.fromEntries(Object.entries(pieces).map(([name, piece]) => [name, piece.active])) as {\n readonly [K in keyof Pieces]: Pieces[K]['active']\n },\n\n /**\n * An editor for one form and the mutation its value feeds. The form's value\n * must be the mutation's input, which is what declaring the input once and\n * giving it to both guarantees.\n */\n editor: <\n const Name extends string,\n FormName extends string,\n FormModel,\n FormMessage extends { readonly _tag: string },\n Value,\n E extends AnyEntity,\n Fields extends Schema.Struct.Fields,\n Members,\n R = never,\n >(\n name: Name,\n config: {\n readonly form: EditableForm<FormName, FormModel, FormMessage, Value, E, Fields, Members, R>\n readonly mutation: MutationDescriptor<string, Value, any>\n },\n ) => {\n type Model = EditorModel<FormModel>\n const { form, mutation } = config\n\n const closed: Model = {\n form: form.initial,\n mode: 'closed',\n target: null,\n filled: false,\n requestId: null,\n }\n const Model = Schema.Struct({\n form: form.bundle.Model,\n mode: Schema.Literals(['closed', 'new', 'edit']),\n target: Schema.NullOr(Schema.String),\n filled: Schema.Boolean,\n requestId: Schema.NullOr(Schema.String),\n }) as unknown as Schema.Codec<Model, unknown>\n\n // The editor's Messages are the form's own: it adds state around the form,\n // not a second vocabulary.\n const bundle = Bundle.make(name, {\n Model,\n Message: form.bundle.Message,\n init: () => ({ model: closed }),\n update: (model: Model, message: FormMessage) => {\n const next = form.bundle.update(model.form, message, undefined)\n // An edit after a save starts a new round; the last save no longer describes the form.\n const requestId = Predicate.isTagged(message, 'Changed') ? null : model.requestId\n // The form's Commands are the editor's: a check the form started has to run.\n const edited = {\n model: { ...model, form: next.model, requestId },\n ...(next.commands === undefined ? {} : { commands: next.commands }),\n }\n return next.outMessage === undefined ? edited : { ...edited, outMessage: next.outMessage }\n },\n helpers: {\n /** Edits `id`: the form is emptied, and filled once the current values arrive. */\n open: (_: Model, id: IdOf<E>) => ({\n model: { ...closed, mode: 'edit' as const, target: id },\n }),\n /** A blank form for a new one: nothing to load. */\n blank: () => ({ model: { ...closed, mode: 'new' as const, filled: true } }),\n close: () => ({ model: closed }),\n },\n })\n\n const current = Entity.selectFor(form.input)\n\n return {\n bundle,\n /** What the editor loads before it shows the form: every member the form writes. */\n selection: current,\n\n /**\n * The editor where it lives: `model` is its slice of the parent Model (the\n * field it was declared at), and `data` the Remote domain that registers the\n * mutation and the Entity.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, Model>\n }) => {\n const { data, model: slice } = where\n const loaded = (root: Root): RemoteData<any> | undefined => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.get(current, target).read(root)\n : undefined\n }\n\n /**\n * Shows the loaded value once, the first time it is there. Later refreshes\n * leave the drafts alone. A value whose latest refresh failed is still the\n * last one known good, so it fills the form too: `status` goes on saying\n * `Editing`, and the failure is the read's to show.\n */\n const sync: Update.Step<Root, never, never> = root => {\n const editor = slice.get(root)\n const read = loaded(root)\n if (editor.filled || read === undefined) return { model: root }\n const value =\n read._tag === 'Ready' || read._tag === 'Refreshing'\n ? read.value\n : read._tag === 'Failed'\n ? read.previous\n : undefined\n if (value === undefined) return { model: root }\n const values = Entity.valuesFor(form.input, value) as Partial<Value>\n return {\n model: slice.set(root, {\n ...editor,\n form: form.fill(editor.form, values).model,\n filled: true,\n }),\n }\n }\n\n const saveOf = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n\n return {\n /** For the placement: a valid submit becomes the mutation, and its request is remembered. */\n onOut:\n (submitted: Submitted<Value>): Step<Root> =>\n root => {\n const started = data.mutate(root, mutation, submitted.value)\n const editor = slice.get(started.model)\n return {\n model: slice.set(started.model, { ...editor, requestId: started.requestId }),\n commands: [started.command],\n }\n },\n\n /**\n * For `Data.subscriptions`: while an id is being edited, what it loads is\n * a requirement like any Surface's, so Remote fetches and retains it.\n */\n active: data.active(name, root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? Option.some(data.get(current, target))\n : Option.none()\n }),\n\n sync,\n\n /**\n * Wraps the application's `update` so `sync` runs after every Message:\n * the value may arrive with any of them.\n */\n after:\n <Message, Result extends { readonly model: Root }>(\n update: (model: Root, message: Message) => Result,\n ) =>\n (model: Root, message: Message): Result => {\n const next = update(model, message)\n return { ...next, model: sync(next.model).model }\n },\n\n /**\n * Asks for the value being edited again: `Data.refresh`, for a retry\n * button while `status` is `LoadFailed`. A failed read is not retried on\n * its own. Unchanged while nothing is open for editing.\n */\n refresh: (root: Root): Root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.refresh(root, data.get(current, target))\n : root\n },\n\n /** Why the last save failed, while `status` is `SaveFailed`. */\n saveError: (root: Root): RemoteError | undefined => {\n const save = saveOf(root)\n return save._tag === 'Failed' ? save.error : undefined\n },\n\n /** The id being edited, typed as the Entity's own; `null` for a new one or a closed editor. */\n target: (root: Root): IdOf<E> | null => slice.get(root).target as IdOf<E> | null,\n status: (root: Root): EditorStatus => {\n const editor = slice.get(root)\n if (editor.mode === 'closed') return 'Closed'\n // Gone outranks everything: a save that landed describes a thing that no longer exists.\n const read = loaded(root)\n if (read?._tag === 'NotFound') return 'NotFound'\n const save = saveOf(root)\n switch (save._tag) {\n case 'Pending':\n return 'Saving'\n case 'Failed':\n return 'SaveFailed'\n case 'Applied':\n return 'Saved'\n case 'Unknown':\n if (editor.filled) return 'Editing'\n return read?._tag === 'Failed' ? 'LoadFailed' : 'Loading'\n }\n },\n }\n },\n }\n },\n}\n\nexport { Display, type DisplayColumn, type DisplayKind, type DisplayWords } from './display.js'\nexport { Sort, type SortState, type SortedColumn } from './sort.js'\n"],"mappings":"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"}
|