foldkit-crud 0.1.0 → 0.1.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 CHANGED
@@ -107,7 +107,8 @@ const subscriptions = Data.subscriptions({ editor: PostEditor.active })
107
107
  what the form writes is fetched and retained like any Surface's requirement.
108
108
  - `after(update)`: wraps `update` so `sync` runs after every Message.
109
109
  - `sync`, the Step itself, if you compose `update` another way.
110
- - `status(model)`, and `saveError(model)` while it is `SaveFailed`.
110
+ - `status(model)`, `saveError(model)` while it is `SaveFailed`, and
111
+ `target(model)`, the id being edited (`null` for a new one or when closed).
111
112
  - The save is a Command that needs `RemoteClient`, so the parent scope names it
112
113
  with `withServices<RemoteClient>()`.
113
114
 
@@ -126,7 +127,7 @@ const Slot = Bundle.declare(
126
127
  )
127
128
 
128
129
  // where the page draws it:
129
- Placed.view(model, h, { options: pickers(model), submitLabel: 'Save' })
130
+ Placed.view(model, h, { options: pickers(model), words: { submit: 'Save' } })
130
131
  ```
131
132
 
132
133
  `Crud.editorView` works on any Submodel view of the form that takes view
@@ -199,16 +200,66 @@ const subscriptions = Data.subscriptions({ authors: AuthorList.active })
199
200
  ```
200
201
 
201
202
  - `Authors.columns` is the selected members in the Selection's order, each with
202
- its `key`, `member`, and a `label` found the way a form finds one: the schema's
203
- `title`, else `Form.label` metadata, else the key.
203
+ its `key`, `member`, a `label` found the way a form finds one (the schema's
204
+ `title`, else `Form.label` metadata, else the key), and a `display`.
204
205
  - `AuthorList.page(model)` is a `RemoteData` of the page: `items` typed by the
205
206
  Selection, `hasNext`, `hasPrevious`.
206
207
  - `AuthorList.more(model)` is the Command that loads the next page onto this one,
207
208
  or `undefined` when there is none. Return it from `update`.
209
+ - `AuthorList.row(id)` is a Projection of one row through the list's Selection,
210
+ whether or not the query finds it now, and `AuthorList.choiceOf(model, id)` is
211
+ that row as a choice once it is read. `Crud.options` uses both for a picker
212
+ that searches.
208
213
  - `AuthorList.active` makes the page and its rows a requirement while `input`
209
214
  gives a value, so Remote fetches and retains them. Another input is another
210
215
  connection.
211
216
 
217
+ ### How a column shows
218
+
219
+ A `Display` is `foldkit-form`'s `Input` from the reading side: how one selected
220
+ member shows, described without a renderer.
221
+
222
+ ```ts
223
+ const Post = Blog.Post.pipe(
224
+ Entity.annotateMembers({
225
+ id: Display.of(Display.hidden()), // read, so a row can be opened by it, and not shown
226
+ cents: Display.of(Display.number(cents => `$${(cents / 100).toFixed(2)}`)),
227
+ }),
228
+ )
229
+
230
+ Display.show(column.display, row[column.key], { yes: 'Live', no: 'Draft' }) // the cell's text
231
+ ```
232
+
233
+ | Kind | For |
234
+ | --- | --- |
235
+ | `Text`, `Number` | a value as it is, or through `format` |
236
+ | `Flag` | a boolean, in the words given (`yes` / `no`) |
237
+ | `Hidden` | a member read and not shown |
238
+ | `Ref` | a relation selected with `true`: its id, or ids |
239
+ | `Nested` | a relation read through a Selection: the target's own columns, with `shape` `one`, `many`, or `page` |
240
+
241
+ There is one primitive, as with a form's controls: a `kind`, whether it is
242
+ `shown`, its `data`, and its `text`. The kinds above are made with
243
+ `Display.kind`, and so is yours:
244
+
245
+ ```ts
246
+ const Badge = Display.kind<{ readonly tone: string }>('Badge', {
247
+ text: (_, value) => String(value), // the floor: what it says when nothing draws it specially
248
+ })
249
+
250
+ const Post = Blog.Post.pipe(Entity.annotateMembers({ status: Display.of(Badge.of({ tone: 'soft' })) }))
251
+ ```
252
+
253
+ `Badge.is(display)` narrows `display.data`. A drawn view draws it specially once
254
+ given a renderer for `Badge`.
255
+
256
+ It is resolved from the most to the least explicit source: `Display.of`
257
+ metadata on the member, how the relation was selected, then the shape of the
258
+ schema. `Display.show` is the text any view can fall back on; a view that wants
259
+ a link or a badge reads `display` and the value.
260
+ [`foldkit-mixins-crud`](../mixins-crud/README.md) draws a list as a table and a
261
+ detail as a description list from exactly this.
262
+
212
263
  ### A relation picker's choices
213
264
 
214
265
  A form names a relation's target Entity and leaves listing it to you, because a
@@ -223,6 +274,28 @@ const pickers = Crud.options(EditPostForm, [AuthorList])
223
274
  Placed.view(model, h, { options: pickers(model) }) // with foldkit-mixins-form
224
275
  ```
225
276
 
277
+ #### When a picker searches
278
+
279
+ ```ts
280
+ const AuthorList = Authors.at({
281
+ data: Data,
282
+ // What the picker's search box holds is the form's; here it is the query's input.
283
+ input: model => ({ search: EditPostForm.search(model.editor.form, 'authorId') }),
284
+ })
285
+
286
+ const pickers = Crud.options(EditPostForm, [AuthorList], {
287
+ chosen: model => model.editor.form, // the form's Model, wherever the page keeps it
288
+ })
289
+
290
+ const subscriptions = Data.subscriptions({ authors: AuthorList.active, chosen: pickers.active })
291
+ ```
292
+
293
+ A search narrows the list, and what the picker already holds may no longer be
294
+ in it. With `chosen`, those rows stay among the choices, first, and
295
+ `pickers.active` makes them a requirement so Remote reads them and they can be
296
+ named. Without it a `select` would show a post's author as blank the moment a
297
+ search stopped finding them.
298
+
226
299
  `pickers(model)` is keyed by the form's keys: `{ authorId: [{ value, label }, …] }`.
227
300
  A list is matched to a picker by Entity, so one author list serves `authorId`
228
301
  and `editorId` alike. A picker whose target no list is over, or whose list has no `choice`, throws
@@ -277,11 +350,63 @@ PostDetail.value(model) // RemoteData of the Selection's value
277
350
  list's `columns` does, and `PostDetail.active` makes the value a requirement
278
351
  while an id is shown.
279
352
 
353
+ ## Typed ids
354
+
355
+ When the Entity's `id` is branded, the ids here are too. An editor's
356
+ `helpers.open(id)` takes the `IdOf` of its form's Entity, and `target(model)`
357
+ reads the id being edited as that type. A remover whose mutation's input is just the id names the key, `id: 'id'`, and is
358
+ asked about that key's type; with a fuller input, annotate the function:
359
+ `input: (id: PostId) => ({ id, reason: 'spam' })`. A list's rows
360
+ are its Selection's own value, so `row.id` is a `PostId` when the Selection
361
+ reads `id`.
362
+
363
+ ## Sorting a list
364
+
365
+ How a list is sorted is your state and the query's input. `Sort` writes that
366
+ state down once, so the Model, the header clicks, and the server agree:
367
+
368
+ ```ts
369
+ const PostSort = Sort.make(['title', 'created']) // the orders offered, by name
370
+
371
+ const PostsQuery = Query.make('Posts', {
372
+ Input: { sort: PostSort.Schema },
373
+ Result: Query.connection(Blog.Post),
374
+ })
375
+ // Model: `postSort: PostSort.Schema`, starting at `PostSort.none`
376
+ // update: `SortedPosts` sets `postSort` to the state the Message carries
377
+
378
+ PostSort.toggle(model.postSort, 'title') // asc, then desc, then the server's own order
379
+ PostSort.inputs(model.postSort, sort => Message.SortedPosts({ sort })) // a drawn table's `sort`
380
+ ```
381
+
382
+ `Sort` holds nothing. A name is an order the server offers, never a column; on
383
+ the server, `foldkit-remote-drizzle`'s `sortTerms(sort, { title: posts.title })`
384
+ says what each name means and ignores any other.
385
+
386
+ ## Wiring a page
387
+
388
+ Every placed piece has an `active`: what it requires of Remote while it is on
389
+ screen. A piece whose `active` is not wired never loads, and says nothing.
390
+ `Crud.actives` gathers them from the pieces themselves:
391
+
392
+ ```ts
393
+ Data.wiring(Crud.actives({ posts: Posts, authors: Authors, editor: PostEditor, pickers }))
394
+ ```
395
+
396
+ It takes a placed editor, list, or detail, and the pickers from `Crud.options`,
397
+ and gives `{ posts: Posts.active, ... }`, for `Data.wiring` or
398
+ `Data.subscriptions`.
399
+
280
400
  ## Limits
281
401
 
282
402
  - A list has no sorting, filtering, or selection state of its own: those are the
283
- query's input, which your Model holds.
284
- - Headless. Draw the editor with `Crud.editorView` over a
403
+ query's input, which your Model holds. Change the input and the list is another
404
+ connection, fetched because it is on screen; `foldkit-remote-drizzle`'s `query`
405
+ reads the input in `where` and `orderBy`. [`examples/entity`](../../examples/entity)
406
+ searches and sorts its post list this way.
407
+ - Headless. Draw a list or a detail with
408
+ [`foldkit-mixins-crud`](../mixins-crud/README.md), or from `columns` and
409
+ `Display.show`. Draw the editor with `Crud.editorView` over a
285
410
  [`foldkit-mixins-form`](../mixins-form/README.md) view, or from
286
411
  `EditPostForm.controls`. [`examples/entity`](../../examples/entity) draws a
287
412
  list and an editor, and runs in a browser.
package/dist/index.d.mts CHANGED
@@ -1,12 +1,145 @@
1
1
  import { Schema } from "effect";
2
2
  import { Bundle } from "foldkit-bundle";
3
- import { AnyEntity, EntityInput, EntityMember, Selection } from "foldkit-entity";
3
+ import { AnyEntity, EntityInput, EntityMember, IdOf, Selection, SelectionPage } from "foldkit-entity";
4
4
  import { FormControl, Submitted } from "foldkit-form";
5
+ import { Metadata } from "foldkit-metadata";
6
+ import { ModelRef, Projection } from "foldkit-surface";
5
7
  import * as Submodel from "foldkit/submodel";
6
8
  import { MutationDescriptor, MutationStatus, Page, QueryDescriptor, RemoteClient, RemoteData, RemoteError, RemoteMessage } from "foldkit-remote";
7
- import { ModelRef, Projection } from "foldkit-surface";
8
9
  import { Command } from "foldkit/command";
9
10
  import * as Update from "foldkit/update";
11
+ //#region src/display.d.ts
12
+ /**
13
+ * How one selected member shows: the primitive every kind of Display is a value
14
+ * of. `Display.text()` and `Display.flag()` are values of it, made by
15
+ * `Display.kind`, which is what an application calls for a badge or a relative
16
+ * date. The kinds here are a collection, not a special case.
17
+ */
18
+ export interface Display<Data = unknown> {
19
+ /** The name a renderer is found by. */
20
+ readonly kind: string;
21
+ /** Whether a view draws it. A member read and not shown, usually the id, is `false`. */
22
+ readonly shown: boolean;
23
+ /** What this kind needs: a format, a relation's target. */
24
+ readonly data: Data;
25
+ /**
26
+ * The value as text: what a cell says when nothing draws it specially. It is
27
+ * never given `null` or `undefined`; `Display.show` says `nothing` for those.
28
+ */
29
+ readonly text: (value: unknown, words: DisplayWords) => string;
30
+ }
31
+ /** A kind of Display: how its values are made, and how one is told from another. */
32
+ interface DisplayKind<Data> {
33
+ readonly kind: string;
34
+ readonly of: (data: Data) => Display<Data>;
35
+ readonly is: (display: Display) => display is Display<Data>;
36
+ }
37
+ /** A selected member with its words and its Display. A list's column, a detail's line. */
38
+ interface DisplayColumn<Key extends string = string> {
39
+ readonly key: Key;
40
+ readonly label: string;
41
+ readonly member: EntityMember;
42
+ readonly display: Display;
43
+ }
44
+ /** The words a Display needs that are not the value's own. */
45
+ interface DisplayWords {
46
+ /** Default `yes`. */
47
+ readonly yes?: string;
48
+ /** Default `no`. */
49
+ readonly no?: string;
50
+ /** What stands for nothing: `null`, `undefined`, an empty list. Default the empty string. */
51
+ readonly nothing?: string;
52
+ /** Between the items of a list, and between the members of a nested value. Default `, `. */
53
+ readonly separator?: string;
54
+ }
55
+ export declare const Display: {
56
+ /**
57
+ * A kind of Display. `Display.kind<{ tone: string }>('Badge', { text: (_, value) => String(value) })`
58
+ * gives `.of(data)` to make one and `.is(display)` to tell one, exactly as the
59
+ * kinds below were made. `text` is the floor; a view draws it specially once
60
+ * it is given a renderer for `Badge`.
61
+ */
62
+ kind: <Data = Record<string, never>>(name: string, spec: {
63
+ readonly shown?: boolean;
64
+ readonly text: (data: Data, value: unknown, words: DisplayWords) => string;
65
+ }) => DisplayKind<Data>;
66
+ Text: DisplayKind<{
67
+ readonly format?: ((value: unknown) => string) | undefined;
68
+ }>;
69
+ Number: DisplayKind<{
70
+ readonly format?: ((value: number) => string) | undefined;
71
+ }>;
72
+ Flag: DisplayKind<Record<string, never>>;
73
+ Hidden: DisplayKind<Record<string, never>>;
74
+ Ref: DisplayKind<{
75
+ readonly target: AnyEntity;
76
+ readonly many: boolean;
77
+ }>;
78
+ Nested: DisplayKind<{
79
+ readonly target: AnyEntity;
80
+ readonly shape: "one" | "many" | "page";
81
+ readonly columns: ReadonlyArray<DisplayColumn>;
82
+ }>;
83
+ text: (format?: (value: unknown) => string) => Display;
84
+ number: (format?: (value: number) => string) => Display;
85
+ flag: () => Display;
86
+ /** Read, so a row can be opened or keyed by it, and not shown: usually the id. */
87
+ hidden: () => Display;
88
+ /**
89
+ * Entity metadata: how a member shows wherever it is listed, e.g.
90
+ * `Entity.annotateMembers({ price: Display.of(Display.number(cents => ...)) })`.
91
+ */
92
+ of: (display: Display) => Metadata;
93
+ /**
94
+ * The Display of one selected member, from the most to the least explicit
95
+ * source: the member's `Display.of` metadata, how the relation was selected,
96
+ * then the shape of its schema. `nested` is the columns of a nested Selection,
97
+ * with `shape` saying how many the relation yields.
98
+ */
99
+ resolve: (member: EntityMember, nested?: {
100
+ readonly shape: "one" | "many" | "page";
101
+ readonly columns: ReadonlyArray<DisplayColumn>;
102
+ }) => Display;
103
+ /**
104
+ * A value as the text its Display calls for: what a cell says when nothing
105
+ * draws it specially. A view that wants a link or a badge reads the Display
106
+ * and the value itself; this is the floor every view can stand on.
107
+ */
108
+ show: (display: Display, value: unknown, words?: DisplayWords) => string;
109
+ };
110
+ //#endregion
111
+ //#region src/sort.d.ts
112
+ /** How a list is sorted: by which of its orders, which way; `null` for the server's own order. */
113
+ type SortState<Column extends string> = {
114
+ readonly by: Column;
115
+ readonly direction: 'asc' | 'desc';
116
+ } | null;
117
+ /** How one column is sorted now, and the Message a click on its header sends. */
118
+ interface SortedColumn<Message> {
119
+ readonly direction?: 'asc' | 'desc' | undefined;
120
+ readonly message: Message;
121
+ }
122
+ export declare const Sort: {
123
+ /**
124
+ * The orders a list offers, by name. A name is an order the server knows, not
125
+ * a column: which columns may sort, and what `title` means in SQL, stay the
126
+ * server's to decide.
127
+ */
128
+ make: <const Columns extends readonly [string, ...string[]]>(columns: Columns) => {
129
+ columns: Columns;
130
+ /** For the Model field, and for the query's input. */
131
+ Schema: Schema.Codec<SortState<Columns[number]>, SortState<Columns[number]>>;
132
+ /** The server's own order. */
133
+ none: SortState<Columns[number]>;
134
+ toggle: (current: SortState<Columns[number]>, column: Columns[number]) => SortState<Columns[number]>;
135
+ /**
136
+ * What a drawn table takes as `sort`: every order with how it is sorted now
137
+ * and the Message a click sends, made from the state the click leads to.
138
+ */
139
+ inputs: <Message>(current: SortState<Columns[number]>, message: (next: SortState<Columns[number]>) => Message) => { readonly [K in Columns[number]]: SortedColumn<Message>; };
140
+ };
141
+ };
142
+ //#endregion
10
143
  //#region src/index.d.ts
11
144
  /** The parts of a `Form.make` result an editor wraps. */
12
145
  export interface EditableForm<Name extends string, FormModel, FormMessage, Value, E extends AnyEntity, Fields extends Schema.Struct.Fields, Members, R = never> {
@@ -77,13 +210,11 @@ export interface Choice {
77
210
  readonly value: string;
78
211
  readonly label: string;
79
212
  }
80
- /** One column of a list, or one line of a detail: a member the Selection reads. */
81
- export interface ListColumn<Key extends string = string> {
82
- readonly key: Key;
83
- /** The schema's `title` annotation, else `Form.label` metadata, else the key. */
84
- readonly label: string;
85
- readonly member: EntityMember;
86
- }
213
+ /**
214
+ * One column of a list, or one line of a detail: a member the Selection reads,
215
+ * with its words and its `Display`.
216
+ */
217
+ export type ListColumn<Key extends string = string> = DisplayColumn<Key>;
87
218
  export declare const Crud: {
88
219
  /**
89
220
  * One Entity read through a Selection, for a page that shows it. Like a list
@@ -97,6 +228,8 @@ export declare const Crud: {
97
228
  name: Name;
98
229
  /** The selected members in the Selection's order, each with its label. */
99
230
  fields: readonly ListColumn<keyof Members & string>[];
231
+ /** Type-only: the value shown, for a view to be typed by. Never read. */
232
+ Value: Row;
100
233
  /** The detail where it lives: `id` is the one shown, or `undefined` while none is. */
101
234
  at: <Root>(where: {
102
235
  readonly data: DomainLike<Root>;
@@ -113,11 +246,19 @@ export declare const Crud: {
113
246
  };
114
247
  /**
115
248
  * Deleting through one mutation, with a yes in between: asked, confirmed,
116
- * deleted. What the mutation's input is for an id is `input`'s to say.
249
+ * deleted. What the mutation's input is for an id is `input`'s to say, and the
250
+ * id it names there (`(id: PostId) => ...`) is the id `ask` takes.
117
251
  */
118
- remover: <const Name extends string, Input>(name: Name, config: {
252
+ remover: <const Name extends string, Input, Id extends string = string, const Key extends keyof Input = never>(name: Name, config: {
253
+ readonly mutation: MutationDescriptor<string, Input, any>;
254
+ readonly input: (id: Id) => Input;
255
+ } | {
119
256
  readonly mutation: MutationDescriptor<string, Input, any>;
120
- readonly input: (id: string) => Input;
257
+ /**
258
+ * The key of the mutation's input that holds the id, when that is all the
259
+ * input is: `id: 'id'` for `{ id }`. The id's type is that key's.
260
+ */
261
+ readonly id: Key & (Input extends Readonly<Record<Key, string>> ? Key : never);
121
262
  }) => {
122
263
  bundle: Bundle.Bundle<Name, void, RemoverModel, {
123
264
  readonly _tag: "Confirmed";
@@ -125,9 +266,9 @@ export declare const Crud: {
125
266
  readonly _tag: "Cancelled";
126
267
  }, ConfirmedRemoval, never, never, void, {}, {
127
268
  /** Asks whether to delete `id`. Nothing is deleted until `Confirmed`. */
128
- ask: (_: RemoverModel, id: string) => {
269
+ ask: (_: RemoverModel, id: [Key] extends [never] ? Id : Input[Key] & string) => {
129
270
  model: {
130
- target: string;
271
+ target: [Key] extends [never] ? Id : Input[Key] & string;
131
272
  requestId: null;
132
273
  };
133
274
  };
@@ -147,7 +288,7 @@ export declare const Crud: {
147
288
  /** For the placement: a yes becomes the mutation, and its request is remembered. */
148
289
  onOut: (confirmed: ConfirmedRemoval) => Step<Root>;
149
290
  /** The id being asked about or deleted, for the words of a confirmation. */
150
- target: (root: Root) => string | null;
291
+ target: (root: Root) => ([Key] extends [never] ? Id : Input[Key] & string) | null;
151
292
  status: (root: Root) => RemoverStatus;
152
293
  /** Why the last delete failed, while `status` is `DeleteFailed`. */
153
294
  error: (root: Root) => RemoteError | undefined;
@@ -157,11 +298,9 @@ export declare const Crud: {
157
298
  * A list over one query: which rows to show is the query's, what to show of
158
299
  * each is the Selection's. The pages live in Remote; the list holds no state.
159
300
  */
160
- list: <const Name extends string, EntityName extends string, Members, Row, Input>(name: Name, config: {
301
+ list: <const Name extends string, EntityName extends string, Members, S extends Schema.Constraint, Input>(name: Name, config: {
161
302
  readonly query: QueryDescriptor<string, Input, any>;
162
- readonly selection: Selection<EntityName, Members, Schema.Constraint & {
163
- readonly Type: Row;
164
- }>;
303
+ readonly selection: Selection<EntityName, Members, S>;
165
304
  /** How many rows a page holds. Default 25. */
166
305
  readonly pageSize?: number;
167
306
  /**
@@ -169,13 +308,15 @@ export declare const Crud: {
169
308
  * that identify it to a person. Give it to a list that feeds pickers.
170
309
  */
171
310
  readonly choice?: {
172
- readonly value: (row: Row) => string;
173
- readonly label: (row: Row) => string;
311
+ readonly value: (row: S["Type"]) => string;
312
+ readonly label: (row: S["Type"]) => string;
174
313
  };
175
314
  }) => {
176
315
  name: Name;
177
316
  /** The selected members in the Selection's order, each with its label. */
178
317
  columns: readonly ListColumn<keyof Members & string>[];
318
+ /** Type-only: a row, for a view to be typed by. Never read. */
319
+ Row: S["Type"];
179
320
  /**
180
321
  * The list where it lives. `input` is the query's input as the Model has
181
322
  * it (filters, a search term), or `undefined` while the list is not shown.
@@ -190,10 +331,17 @@ export declare const Crud: {
190
331
  owner: object;
191
332
  projectionOf: (root: Root) => Projection<Root, RemoteData<Page<any>>> | undefined;
192
333
  };
193
- page: (root: Root) => RemoteData<Page<Row>>;
334
+ page: (root: Root) => RemoteData<Page<S["Type"]>>;
194
335
  /** The Command that loads the next page onto this one, or `undefined` when there is none. */
195
336
  more: (root: Root) => Command<RemoteMessage, never, RemoteClient> | undefined;
196
337
  name: Name;
338
+ /** Whose requirement the list is, for anything that requires beside it. */
339
+ owner: object;
340
+ /**
341
+ * One of the list's Entity through the list's Selection, by id, whether or
342
+ * not the query finds it now: what a picker reads to name what is chosen.
343
+ */
344
+ row: (id: string) => Projection<Root, RemoteData<any>>;
197
345
  /** The Entity the rows are of, which is how a picker finds the list for its target. */
198
346
  entity: AnyEntity;
199
347
  /** Whether the list was given a `choice`, so its rows can be a picker's choices. */
@@ -204,6 +352,8 @@ export declare const Crud: {
204
352
  * server authorizes; nothing is read because a relation exists.
205
353
  */
206
354
  choices: (root: Root) => ReadonlyArray<Choice>;
355
+ /** That row as a choice, once it is read. */
356
+ choiceOf: (root: Root, id: string) => Choice | undefined;
207
357
  };
208
358
  };
209
359
  /**
@@ -220,15 +370,49 @@ export declare const Crud: {
220
370
  * The choices of every relation picker in a form, from the lists of their
221
371
  * targets: `foldkit-mixins-form`'s `options`, keyed by the form's keys. A
222
372
  * picker whose target no list here is over is a wiring mistake, reported now.
373
+ *
374
+ * With `chosen`, the form's Model as the page has it, what a picker already
375
+ * holds stays among its choices when the list no longer finds it (a search
376
+ * narrowed it, or it is on a later page), and `active` requires those rows so
377
+ * their words are there. Give `chosen` to any form whose pickers search.
223
378
  */
224
- options: <Key extends string, Root>(form: {
379
+ options: <Key extends string, Root, FormModel = never>(form: {
225
380
  readonly controls: ReadonlyArray<FormControl<Key>>;
381
+ readonly field?: (model: FormModel, key: never) => {
382
+ readonly value: unknown;
383
+ };
226
384
  }, lists: ReadonlyArray<{
227
385
  readonly name: string;
228
386
  readonly entity: AnyEntity;
229
387
  readonly offersChoices: boolean;
388
+ readonly owner: object;
230
389
  readonly choices: (root: Root) => ReadonlyArray<Choice>;
231
- }>) => ((root: Root) => { readonly [K in Key]?: ReadonlyArray<Choice>; });
390
+ readonly row: (id: string) => Projection<Root, RemoteData<any>>;
391
+ readonly choiceOf: (root: Root, id: string) => Choice | undefined;
392
+ }>, options?: {
393
+ readonly chosen?: (root: Root) => FormModel | undefined;
394
+ }) => ((root: Root) => { readonly [K in Key]?: ReadonlyArray<Choice>; }) & {
395
+ /** For `Data.subscriptions`: what the pickers hold is read, so it can be named. */
396
+ active: {
397
+ name: string;
398
+ owner: object;
399
+ projectionOf: (root: Root) => Projection<Root, {
400
+ readonly [x: string]: RemoteData<any>;
401
+ }> | undefined;
402
+ };
403
+ };
404
+ /**
405
+ * What the pieces on a page require of Remote, by the names given: a placed
406
+ * editor, list, or detail, or the pickers from `Crud.options`. It is each one's
407
+ * `active`, gathered, so handing the pieces over is enough and none is left out:
408
+ *
409
+ * ```ts
410
+ * Data.wiring(Crud.actives({ posts: Posts, authors: Authors, editor: PostEditor, pickers }))
411
+ * ```
412
+ */
413
+ actives: <const Pieces extends Readonly<Record<string, {
414
+ readonly active: unknown;
415
+ }>>>(pieces: Pieces) => { readonly [K in keyof Pieces]: Pieces[K]["active"]; };
232
416
  /**
233
417
  * An editor for one form and the mutation its value feeds. The form's value
234
418
  * must be the mutation's input, which is what declaring the input once and
@@ -242,10 +426,10 @@ export declare const Crud: {
242
426
  }) => {
243
427
  bundle: Bundle.Bundle<Name, void, EditorModel<FormModel>, FormMessage, Submitted<Value>, R, never, void, {}, {
244
428
  /** Edits `id`: the form is emptied, and filled once the current values arrive. */
245
- open: (_: EditorModel<FormModel>, id: string) => {
429
+ open: (_: EditorModel<FormModel>, id: IdOf<E>) => {
246
430
  model: {
247
431
  mode: "edit";
248
- target: string;
432
+ target: IdOf<E>;
249
433
  form: FormModel;
250
434
  filled: boolean;
251
435
  requestId: string | null;
@@ -266,20 +450,20 @@ export declare const Crud: {
266
450
  };
267
451
  }>;
268
452
  /** What the editor loads before it shows the form: every member the form writes. */
269
- selection: Selection<E["name"], import("foldkit-entity").WrittenSpec<Members>, Schema.Struct<{ readonly [K in keyof import("foldkit-entity").WrittenSpec<Members> & keyof E["members"]]: E["members"][K] extends (infer T) ? T extends E["members"][K] ? T extends import("foldkit-entity").EntityField<any, any, infer S extends Schema.Constraint> | import("foldkit-entity").EntityDerived<any, any, infer S extends Schema.Constraint> ? S : T extends import("foldkit-entity").EntityRelation<any, any, infer Target extends AnyEntity, infer C extends import("foldkit-entity").Cardinality, infer Optional extends boolean> ? import("foldkit-entity").WrittenSpec<Members>[K] extends (infer T_1) ? T_1 extends import("foldkit-entity").WrittenSpec<Members>[K] ? T_1 extends import("foldkit-entity").SelectionPage<any, infer S_1 extends Schema.Constraint> ? Schema.Struct<{
453
+ selection: Selection<E["name"], import("foldkit-entity").WrittenSpec<Members>, Schema.Struct<{ readonly [K in keyof import("foldkit-entity").WrittenSpec<Members> & keyof E["members"]]: E["members"][K] extends (infer T) ? T extends E["members"][K] ? T extends import("foldkit-entity").EntityField<any, any, infer S extends Schema.Constraint> | import("foldkit-entity").EntityDerived<any, any, infer S extends Schema.Constraint> ? S : T extends import("foldkit-entity").EntityRelation<any, any, infer Target extends AnyEntity, infer C extends import("foldkit-entity").Cardinality, infer Optional extends boolean> ? import("foldkit-entity").WrittenSpec<Members>[K] extends (infer T_1) ? T_1 extends import("foldkit-entity").WrittenSpec<Members>[K] ? T_1 extends SelectionPage<any, infer S_1 extends Schema.Constraint> ? Schema.Struct<{
270
454
  readonly items: Schema.$Array<S_1>;
271
455
  readonly hasNext: Schema.Boolean;
272
456
  readonly hasPrevious: Schema.Boolean;
273
- }> : C extends "many" ? Schema.$Array<T_1 extends Selection<any, any, infer S_2 extends Schema.Constraint> ? S_2 : Schema.Struct<{
457
+ }> : C extends "many" ? Schema.$Array<T_1 extends Selection<any, any, infer S_2 extends Schema.Constraint, string> ? S_2 : Schema.Struct<{
274
458
  readonly entity: Schema.Literal<Target["name"]>;
275
- readonly id: Schema.String;
276
- }>> : Optional extends true ? Schema.NullOr<T_1 extends Selection<any, any, infer S_2 extends Schema.Constraint> ? S_2 : Schema.Struct<{
459
+ readonly id: import("foldkit-entity").IdSchemaOf<Target>;
460
+ }>> : Optional extends true ? Schema.NullOr<T_1 extends Selection<any, any, infer S_2 extends Schema.Constraint, string> ? S_2 : Schema.Struct<{
277
461
  readonly entity: Schema.Literal<Target["name"]>;
278
- readonly id: Schema.String;
279
- }>> : T_1 extends Selection<any, any, infer S_2 extends Schema.Constraint> ? S_2 : Schema.Struct<{
462
+ readonly id: import("foldkit-entity").IdSchemaOf<Target>;
463
+ }>> : T_1 extends Selection<any, any, infer S_2 extends Schema.Constraint, string> ? S_2 : Schema.Struct<{
280
464
  readonly entity: Schema.Literal<Target["name"]>;
281
- readonly id: Schema.String;
282
- }> : never : never : never : never : never; }>>;
465
+ readonly id: import("foldkit-entity").IdSchemaOf<Target>;
466
+ }> : never : never : never : never : never; }>, IdOf<E>>;
283
467
  /**
284
468
  * The editor where it lives: `model` is its slice of the parent Model (the
285
469
  * field it was declared at), and `data` the Remote domain that registers the
@@ -310,9 +494,12 @@ export declare const Crud: {
310
494
  }>(update: (model: Root, message: Message) => Result) => (model: Root, message: Message) => Result;
311
495
  /** Why the last save failed, while `status` is `SaveFailed`. */
312
496
  saveError: (root: Root) => RemoteError | undefined;
497
+ /** The id being edited, typed as the Entity's own; `null` for a new one or a closed editor. */
498
+ target: (root: Root) => IdOf<E> | null;
313
499
  status: (root: Root) => EditorStatus;
314
500
  };
315
501
  };
316
502
  };
317
503
  //#endregion
504
+ export type { DisplayColumn, DisplayKind, DisplayWords, SortState, SortedColumn };
318
505
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":"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"}
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,9 +1,159 @@
1
1
  import { Schema } from "effect";
2
2
  import { Bundle } from "foldkit-bundle";
3
- import { Entity } from "foldkit-entity";
4
- import { Form } from "foldkit-form";
3
+ import { Entity, SelectionPageTypeId } from "foldkit-entity";
4
+ import { Form, Input } from "foldkit-form";
5
+ import { Metadata } from "foldkit-metadata";
6
+ import { Projection } from "foldkit-surface";
5
7
  import { defineMessageUnion } from "foldkit/message";
6
8
  import * as Submodel from "foldkit/submodel";
9
+ //#region src/display.ts
10
+ /**
11
+ * Displays: how one selected member shows in a list or a detail, described
12
+ * without a renderer. It is `foldkit-form`'s `Input` from the reading side: the
13
+ * member says what it is, a Display says how it reads, and which DOM draws it is
14
+ * the application's.
15
+ */
16
+ const key = Metadata.key("foldkit-crud/display", {
17
+ merge: (displays) => displays.slice(-1),
18
+ summarize: (display) => display.kind
19
+ });
20
+ const fromSchema = (schema) => {
21
+ const ast = Schema.toType(schema).ast;
22
+ const members = ast._tag === "Union" ? (ast.types ?? []).filter((member) => member._tag !== "Null" && member._tag !== "Undefined") : [ast];
23
+ const [only] = members;
24
+ if (members.length === 1 && only?._tag === "Number") return Number_.of({});
25
+ if (members.length === 1 && only?._tag === "Boolean") return Flag.of(nothing);
26
+ return Text.of({});
27
+ };
28
+ /** A kind of Display. The kinds below are made with it, and so is an application's. */
29
+ const kind = (name, spec) => ({
30
+ kind: name,
31
+ of: (data) => Object.freeze({
32
+ kind: name,
33
+ shown: spec.shown ?? true,
34
+ data,
35
+ text: (value, words) => spec.text(data, value, words)
36
+ }),
37
+ is: (display) => display.kind === name
38
+ });
39
+ const show = (display, value, words = {}) => value === null || value === void 0 ? words.nothing ?? "" : display.text(value, words);
40
+ const Text = kind("Text", { text: (data, value) => data.format === void 0 ? String(value) : data.format(value) });
41
+ const Number_ = kind("Number", { text: (data, value) => typeof value === "number" && data.format !== void 0 ? data.format(value) : String(value) });
42
+ const Flag = kind("Flag", { text: (_, value, words) => value === true ? words.yes ?? "yes" : words.no ?? "no" });
43
+ const Hidden = kind("Hidden", {
44
+ shown: false,
45
+ text: () => ""
46
+ });
47
+ /** A relation read as refs: which one, with nothing to say about it but its id. */
48
+ const Ref = kind("Ref", { text: (_, value, words) => {
49
+ const ids = (Array.isArray(value) ? value : [value]).map((ref) => typeof ref === "object" && ref !== null && "id" in ref ? String(ref.id) : String(ref));
50
+ return ids.length === 0 ? words.nothing ?? "" : ids.join(words.separator ?? ", ");
51
+ } });
52
+ /**
53
+ * A relation read through a Selection: the target's own members, each with a
54
+ * Display. `shape` is how many there are: one, a list, or a page of a list.
55
+ */
56
+ const Nested = kind("Nested", { text: (data, value, words) => {
57
+ const one = (item) => data.columns.filter((column) => column.display.shown).map((column) => show(column.display, item?.[column.key], words)).filter((text) => text !== "").join(" ");
58
+ const items = data.shape === "one" ? [value] : data.shape === "page" ? value.items ?? [] : value;
59
+ return items.length === 0 ? words.nothing ?? "" : items.map(one).join(words.separator ?? ", ");
60
+ } });
61
+ const nothing = Object.freeze({});
62
+ const Display = {
63
+ /**
64
+ * A kind of Display. `Display.kind<{ tone: string }>('Badge', { text: (_, value) => String(value) })`
65
+ * gives `.of(data)` to make one and `.is(display)` to tell one, exactly as the
66
+ * kinds below were made. `text` is the floor; a view draws it specially once
67
+ * it is given a renderer for `Badge`.
68
+ */
69
+ kind,
70
+ Text,
71
+ Number: Number_,
72
+ Flag,
73
+ Hidden,
74
+ Ref,
75
+ Nested,
76
+ text: (format) => Text.of({ format }),
77
+ number: (format) => Number_.of({ format }),
78
+ flag: () => Flag.of(nothing),
79
+ /** Read, so a row can be opened or keyed by it, and not shown: usually the id. */
80
+ hidden: () => Hidden.of(nothing),
81
+ /**
82
+ * Entity metadata: how a member shows wherever it is listed, e.g.
83
+ * `Entity.annotateMembers({ price: Display.of(Display.number(cents => ...)) })`.
84
+ */
85
+ of: (display) => key.of(display),
86
+ /**
87
+ * The Display of one selected member, from the most to the least explicit
88
+ * source: the member's `Display.of` metadata, how the relation was selected,
89
+ * then the shape of its schema. `nested` is the columns of a nested Selection,
90
+ * with `shape` saying how many the relation yields.
91
+ */
92
+ resolve: (member, nested) => {
93
+ const [explicit] = key.get(member.metadata);
94
+ if (explicit !== void 0) return explicit;
95
+ if (member._tag !== "Relation") return fromSchema(member.schema);
96
+ const target = member.target();
97
+ return nested === void 0 ? Ref.of({
98
+ target,
99
+ many: member.cardinality === "many"
100
+ }) : Nested.of({
101
+ target,
102
+ shape: nested.shape,
103
+ columns: nested.columns
104
+ });
105
+ },
106
+ /**
107
+ * A value as the text its Display calls for: what a cell says when nothing
108
+ * draws it specially. A view that wants a link or a badge reads the Display
109
+ * and the value itself; this is the floor every view can stand on.
110
+ */
111
+ show
112
+ };
113
+ //#endregion
114
+ //#region src/sort.ts
115
+ /**
116
+ * Sorting a list, written once. How a list is sorted is the application's state
117
+ * and its query's input, so this holds none of it: it is the schema of that
118
+ * state, what a click on a header makes of it, and the shape a drawn table
119
+ * takes. The server reads the same state and decides which columns it means.
120
+ */
121
+ const Sort = {
122
+ /**
123
+ * The orders a list offers, by name. A name is an order the server knows, not
124
+ * a column: which columns may sort, and what `title` means in SQL, stay the
125
+ * server's to decide.
126
+ */
127
+ make: (columns) => {
128
+ /** A click on a header: ascending, then descending, then the server's own order again. */
129
+ const toggle = (current, column) => current === null || current.by !== column ? {
130
+ by: column,
131
+ direction: "asc"
132
+ } : current.direction === "asc" ? {
133
+ by: column,
134
+ direction: "desc"
135
+ } : null;
136
+ return {
137
+ columns,
138
+ /** For the Model field, and for the query's input. */
139
+ Schema: Schema.NullOr(Schema.Struct({
140
+ by: Schema.Literals(columns),
141
+ direction: Schema.Literals(["asc", "desc"])
142
+ })),
143
+ /** The server's own order. */
144
+ none: null,
145
+ toggle,
146
+ /**
147
+ * What a drawn table takes as `sort`: every order with how it is sorted now
148
+ * and the Message a click sends, made from the state the click leads to.
149
+ */
150
+ inputs: (current, message) => Object.fromEntries(columns.map((column) => [column, {
151
+ direction: current?.by === column ? current.direction : void 0,
152
+ message: message(toggle(current, column))
153
+ }]))
154
+ };
155
+ } };
156
+ //#endregion
7
157
  //#region src/index.ts
8
158
  /**
9
159
  * `foldkit-crud` — management screens assembled from parts an application
@@ -19,14 +169,27 @@ const columnLabel = (key, member) => {
19
169
  const title = member._tag === "Relation" ? void 0 : Schema.resolveAnnotations(member.schema)?.title;
20
170
  return typeof title === "string" ? title : Form.labelOf(member) ?? key;
21
171
  };
22
- /** The selected members in the Selection's order, each with its label. */
172
+ /** The selected members in the Selection's order, each with its label and its Display. */
23
173
  const columnsOf = (selection) => {
24
174
  const members = selection.entity.members;
25
- return Object.keys(selection.members).map((key) => ({
26
- key,
27
- label: columnLabel(key, members[key]),
28
- member: members[key]
29
- }));
175
+ const selected = selection.members;
176
+ return Object.keys(selected).map((key) => {
177
+ const member = members[key];
178
+ const how = selected[key];
179
+ const nested = how === true || member._tag !== "Relation" ? void 0 : SelectionPageTypeId in how ? {
180
+ shape: "page",
181
+ columns: columnsOf(how.selection)
182
+ } : {
183
+ shape: member.cardinality === "many" ? "many" : "one",
184
+ columns: columnsOf(how)
185
+ };
186
+ return {
187
+ key,
188
+ label: columnLabel(key, member),
189
+ member,
190
+ display: Display.resolve(member, nested)
191
+ };
192
+ });
30
193
  };
31
194
  const RemoverMessage = defineMessageUnion({
32
195
  Confirmed: {},
@@ -45,6 +208,8 @@ const Crud = {
45
208
  name,
46
209
  /** The selected members in the Selection's order, each with its label. */
47
210
  fields: columnsOf(config.selection),
211
+ /** Type-only: the value shown, for a view to be typed by. Never read. */
212
+ Value: void 0,
48
213
  /** The detail where it lives: `id` is the one shown, or `undefined` while none is. */
49
214
  at: (where) => {
50
215
  const projectionOf = (root) => {
@@ -64,9 +229,11 @@ const Crud = {
64
229
  }),
65
230
  /**
66
231
  * Deleting through one mutation, with a yes in between: asked, confirmed,
67
- * deleted. What the mutation's input is for an id is `input`'s to say.
232
+ * deleted. What the mutation's input is for an id is `input`'s to say, and the
233
+ * id it names there (`(id: PostId) => ...`) is the id `ask` takes.
68
234
  */
69
235
  remover: (name, config) => {
236
+ const inputFor = (id) => "input" in config ? config.input(id) : { [config.id]: id };
70
237
  return {
71
238
  bundle: Bundle.make(name, {
72
239
  Model: Schema.Struct({
@@ -102,7 +269,7 @@ const Crud = {
102
269
  return {
103
270
  /** For the placement: a yes becomes the mutation, and its request is remembered. */
104
271
  onOut: (confirmed) => (root) => {
105
- const started = data.mutate(root, config.mutation, config.input(confirmed.id));
272
+ const started = data.mutate(root, config.mutation, inputFor(confirmed.id));
106
273
  return {
107
274
  model: slice.set(started.model, {
108
275
  target: confirmed.id,
@@ -137,6 +304,8 @@ const Crud = {
137
304
  name,
138
305
  /** The selected members in the Selection's order, each with its label. */
139
306
  columns: columnsOf(selection),
307
+ /** Type-only: a row, for a view to be typed by. Never read. */
308
+ Row: void 0,
140
309
  /**
141
310
  * The list where it lives. `input` is the query's input as the Model has
142
311
  * it (filters, a search term), or `undefined` while the list is not shown.
@@ -166,6 +335,13 @@ const Crud = {
166
335
  return next === void 0 ? void 0 : data.fetch(next);
167
336
  },
168
337
  name,
338
+ /** Whose requirement the list is, for anything that requires beside it. */
339
+ owner: data.contract.owner ?? {},
340
+ /**
341
+ * One of the list's Entity through the list's Selection, by id, whether or
342
+ * not the query finds it now: what a picker reads to name what is chosen.
343
+ */
344
+ row: (id) => data.get(selection, id),
169
345
  /** The Entity the rows are of, which is how a picker finds the list for its target. */
170
346
  entity: selection.entity,
171
347
  /** Whether the list was given a `choice`, so its rows can be a picker's choices. */
@@ -182,6 +358,15 @@ const Crud = {
182
358
  value: choice.value(row),
183
359
  label: choice.label(row)
184
360
  })) : [];
361
+ },
362
+ /** That row as a choice, once it is read. */
363
+ choiceOf: (root, id) => {
364
+ if (choice === void 0) return void 0;
365
+ const read = data.get(selection, id).read(root);
366
+ return read._tag === "Ready" || read._tag === "Refreshing" ? {
367
+ value: choice.value(read.value),
368
+ label: choice.label(read.value)
369
+ } : void 0;
185
370
  }
186
371
  };
187
372
  }
@@ -201,18 +386,65 @@ const Crud = {
201
386
  * The choices of every relation picker in a form, from the lists of their
202
387
  * targets: `foldkit-mixins-form`'s `options`, keyed by the form's keys. A
203
388
  * picker whose target no list here is over is a wiring mistake, reported now.
389
+ *
390
+ * With `chosen`, the form's Model as the page has it, what a picker already
391
+ * holds stays among its choices when the list no longer finds it (a search
392
+ * narrowed it, or it is on a later page), and `active` requires those rows so
393
+ * their words are there. Give `chosen` to any form whose pickers search.
204
394
  */
205
- options: (form, lists) => {
395
+ options: (form, lists, options = {}) => {
206
396
  const pickers = form.controls.flatMap(({ key, control }) => {
207
- if (control._tag !== "RelationOne" && control._tag !== "RelationMany") return [];
208
- const list = lists.find((candidate) => Entity.same(candidate.entity, control.target));
209
- if (list === void 0) throw new Error(`Crud.options: "${key}" picks a ${control.target.name}, and no list given is over ${control.target.name}`);
397
+ if (!Input.RelationOne.is(control) && !Input.RelationMany.is(control)) return [];
398
+ const { target } = control.data;
399
+ const list = lists.find((candidate) => Entity.same(candidate.entity, target));
400
+ if (list === void 0) throw new Error(`Crud.options: "${key}" picks a ${target.name}, and no list given is over ${target.name}`);
210
401
  if (!list.offersChoices) throw new Error(`Crud.options: "${key}" would pick from list "${list.name}", which has no "choice"`);
211
402
  return [[key, list]];
212
403
  });
213
- return (root) => Object.fromEntries(pickers.map(([key, list]) => [key, list.choices(root)]));
404
+ /** The ids each picker holds now: the draft of a `one`, or of a `many`. */
405
+ const held = (root) => {
406
+ const model = options.chosen?.(root);
407
+ const field = form.field;
408
+ if (model === void 0 || field === void 0) return [];
409
+ return pickers.flatMap(([key, list]) => {
410
+ const draft = field(model, key).value;
411
+ return (Array.isArray(draft) ? draft : typeof draft === "string" ? [draft] : []).filter((id) => typeof id === "string" && id !== "").map((id) => [
412
+ key,
413
+ list,
414
+ id
415
+ ]);
416
+ });
417
+ };
418
+ const choices = (root) => {
419
+ const chosen = held(root);
420
+ return Object.fromEntries(pickers.map(([key, list]) => {
421
+ const listed = list.choices(root);
422
+ return [key, [...chosen.filter(([heldBy, , id]) => heldBy === key && !listed.some((item) => item.value === id)).flatMap(([, , id]) => list.choiceOf(root, id) ?? []), ...listed]];
423
+ }));
424
+ };
425
+ const [first] = lists;
426
+ return Object.assign(choices, {
427
+ /** For `Data.subscriptions`: what the pickers hold is read, so it can be named. */
428
+ active: {
429
+ name: `${pickers.map(([key]) => key).join("+")} chosen`,
430
+ owner: first?.owner ?? {},
431
+ projectionOf: (root) => {
432
+ const rows = held(root);
433
+ return rows.length === 0 ? void 0 : Projection.struct(Object.fromEntries(rows.map(([key, list, id]) => [`${key}:${id}`, list.row(id)])));
434
+ }
435
+ } });
214
436
  },
215
437
  /**
438
+ * What the pieces on a page require of Remote, by the names given: a placed
439
+ * editor, list, or detail, or the pickers from `Crud.options`. It is each one's
440
+ * `active`, gathered, so handing the pieces over is enough and none is left out:
441
+ *
442
+ * ```ts
443
+ * Data.wiring(Crud.actives({ posts: Posts, authors: Authors, editor: PostEditor, pickers }))
444
+ * ```
445
+ */
446
+ actives: (pieces) => Object.fromEntries(Object.entries(pieces).map(([name, piece]) => [name, piece.active])),
447
+ /**
216
448
  * An editor for one form and the mutation its value feeds. The form's value
217
449
  * must be the mutation's input, which is what declaring the input once and
218
450
  * giving it to both guarantees.
@@ -348,6 +580,8 @@ const Crud = {
348
580
  const save = saveOf(root);
349
581
  return save._tag === "Failed" ? save.error : void 0;
350
582
  },
583
+ /** The id being edited, typed as the Entity's own; `null` for a new one or a closed editor. */
584
+ target: (root) => slice.get(root).target,
351
585
  status: (root) => {
352
586
  const editor = slice.get(root);
353
587
  if (editor.mode === "closed") return "Closed";
@@ -366,6 +600,6 @@ const Crud = {
366
600
  }
367
601
  };
368
602
  //#endregion
369
- export { Crud };
603
+ export { Crud, Display, Sort };
370
604
 
371
605
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\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 type AnyEntity,\n type EntityInput,\n type EntityMember,\n type Selection,\n} from 'foldkit-entity'\nimport { Form, type FormControl, type Submitted } from 'foldkit-form'\nimport type {\n MutationDescriptor,\n Page,\n QueryDescriptor,\n MutationStatus,\n RemoteClient,\n RemoteData,\n RemoteError,\n RemoteMessage,\n} from 'foldkit-remote'\nimport type { ActiveSurface, ModelRef, Projection } 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 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/** One column of a list, or one line of a detail: a member the Selection reads. */\nexport interface ListColumn<Key extends string = string> {\n readonly key: Key\n /** The schema's `title` annotation, else `Form.label` metadata, else the key. */\n readonly label: string\n readonly member: EntityMember\n}\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\n/** The selected members in the Selection's order, each with its label. */\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 return Object.keys(selection.members as object).map(key => ({\n key: key as keyof Members & string,\n label: columnLabel(key, members[key]!),\n member: members[key]!,\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\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 },\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.\n */\n remover: <const Name extends string, Input>(\n name: Name,\n config: {\n readonly mutation: MutationDescriptor<string, Input, any>\n readonly input: (id: string) => Input\n },\n ) => {\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: string) => ({ 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, config.input(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): string | null => slice.get(root).target,\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: <const Name extends string, EntityName extends string, Members, Row, Input>(\n name: Name,\n config: {\n readonly query: QueryDescriptor<string, Input, any>\n readonly selection: Selection<EntityName, Members, Schema.Constraint & { readonly Type: Row }>\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: Row) => string\n readonly label: (row: Row) => 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\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<Row>> =>\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 name,\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 },\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 options: <Key extends string, Root>(\n form: { readonly controls: ReadonlyArray<FormControl<Key>> },\n lists: ReadonlyArray<{\n readonly name: string\n readonly entity: AnyEntity\n readonly offersChoices: boolean\n readonly choices: (root: Root) => ReadonlyArray<Choice>\n }>,\n ): ((root: Root) => { readonly [K in Key]?: ReadonlyArray<Choice> }) => {\n const pickers = form.controls.flatMap(({ key, control }) => {\n if (control._tag !== 'RelationOne' && control._tag !== 'RelationMany') return []\n const list = lists.find(candidate => Entity.same(candidate.entity, control.target))\n if (list === undefined)\n throw new Error(\n `Crud.options: \"${key}\" picks a ${control.target.name}, and no list given is over ${control.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 return root =>\n Object.fromEntries(pickers.map(([key, list]) => [key, list.choices(root)])) as {\n readonly [K in Key]?: ReadonlyArray<Choice>\n }\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: string) => ({\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 /** Shows the loaded value once, the first time it is there. Later refreshes leave the drafts alone. */\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 if (read._tag !== 'Ready' && read._tag !== 'Refreshing') return { model: root }\n const values = Entity.valuesFor(form.input, read.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 /** 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 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"],"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 } 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 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 },\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 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 /** Shows the loaded value once, the first time it is there. Later refreshes leave the drafts alone. */\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 if (read._tag !== 'Ready' && read._tag !== 'Refreshing') return { model: root }\n const values = Entity.valuesFor(form.input, read.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 /** 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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit-crud",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Create, read, update and delete screens for Foldkit Plus, assembled from a form, a Remote operation, and the Entity they share. Each capability is declared, never implied.",
5
5
  "keywords": [
6
6
  "foldkit",
@@ -40,9 +40,10 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "foldkit-bundle": "0.2.0",
43
- "foldkit-entity": "0.1.0",
44
- "foldkit-form": "0.1.0",
45
- "foldkit-remote": "0.4.0",
43
+ "foldkit-entity": "0.2.0",
44
+ "foldkit-form": "0.1.1",
45
+ "foldkit-metadata": "0.1.0",
46
+ "foldkit-remote": "0.5.0",
46
47
  "foldkit-surface": "0.3.0"
47
48
  },
48
49
  "peerDependencies": {