foldkit-crud 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -244,9 +244,32 @@ Placed.helpers.blank() // a new one: nothing to load, editing at once
244
244
  Placed.helpers.close()
245
245
  ```
246
246
 
247
- One editor serves one form. A create form and an edit form usually differ (the
248
- edit input has an `id`), so they are two editors; `blank` is for the one whose
249
- form creates.
247
+ One editor serves one form. Create and edit share one when the input's id is
248
+ an `Option`: a blank form saves with `Option.none()`, an opened one with the id
249
+ it was filled with, and the server branches on which (`foldkit-guard`'s
250
+ `GuardServer.mutation` checks a `none` against `Create`). Once a create is
251
+ saved, its form starts blank again: its values are the new row's now, and
252
+ saving the form again would be another create. A form the author went on
253
+ typing in while it saved is kept, as the next one's draft.
254
+
255
+ ```ts
256
+ const SaveProjectInput = Entity.input(
257
+ Project,
258
+ Schema.Struct({ id: Schema.OptionFromNullOr(Schema.String), name: Schema.String }),
259
+ )
260
+ const SaveProject = Mutation.make('SaveProject', {
261
+ Input: SaveProjectInput.schema,
262
+ Output: { id: Schema.String },
263
+ })
264
+ const SaveProjectForm = Form.make('SaveProject', SaveProjectInput, {
265
+ inputs: { id: Input.hidden() },
266
+ })
267
+ const Editor = Crud.editor('ProjectEditor', { form: SaveProjectForm, mutation: SaveProject })
268
+ ```
269
+
270
+ A declared write is one command (`Write.update` or `Write.insert`), so over
271
+ those the create form and the edit form are two editors, and `blank` is for
272
+ the one whose form creates.
250
273
 
251
274
  ### Its status
252
275
 
@@ -258,12 +281,20 @@ form creates.
258
281
  | `Saving` | this editor's mutation is pending |
259
282
  | `Saved` | it was applied |
260
283
  | `SaveFailed` | it failed; the drafts are kept, and `saveError(model)` says why |
284
+ | `Conflict` | the server refused it with `Refusal.conflict`: the row moved on since it was read |
285
+ | `Moved` | someone else saved the row since it was read (a declared write that `expect`s a revision); the draft is kept, and a save of it would be a conflict |
261
286
  | `LoadFailed` | reading it failed, with no earlier value to show; `refresh(model)` asks again |
262
287
  | `NotFound` | it is gone: the server answered without it, a mutation deleted it, or a live event did. This outranks a save that landed |
263
288
 
264
289
  An edit after a save returns to `Editing`: the last save no longer describes
265
290
  what is in the form.
266
291
 
292
+ A save the server refuses with `Refusal.field(key, reason)` for one of the
293
+ form's keys is shown on that key, as the form's own `Refused` would show it: the
294
+ key reads invalid with the reason, as text, keeping what was typed, until it is
295
+ edited. It is shown once per save, and the status is `SaveFailed`. A refusal of
296
+ a key the form does not have is left to the status and `saveError`.
297
+
267
298
  ### How it treats the form
268
299
 
269
300
  - **The form is filled once.** When the value is read again, by a refresh or a
@@ -275,6 +306,23 @@ what is in the form.
275
306
  - **An invalid submit starts no mutation.** The form emits nothing until every
276
307
  key passes the input's schema and its checks. The form's Commands are the
277
308
  editor's, so a check the form starts runs.
309
+ - **A declared write sends only what changed.** With a mutation made by
310
+ `Mutation.write`, a save names the input keys whose value differs, by each
311
+ key's own schema, from what the row held as the form knows it: what it was
312
+ filled with, and what each of this editor's saves sent once it lands. So
313
+ the server writes only those, two authors of different fields both land, and
314
+ a key this editor saved is not sent again over another author's later
315
+ change to it. Remote shows the write bound to those keys until the server
316
+ answers, with no optimistic patch written here. Something new, never
317
+ filled, is written whole.
318
+ - **A write that `expect`s a revision follows its own saves.** When this
319
+ editor's save lands, the form takes the revision it moved the row to, once,
320
+ so the next save is not refused as a conflict with itself, whether or not
321
+ the author typed on while it was saving. A revision moved after that is
322
+ another's, and reads as `Moved`, as one moved before any save does.
323
+ - **Status follows the author.** Changing the form after a save shows
324
+ `Editing` (or `Moved`) at once; the save's own answer is still taken when
325
+ it lands, and a refusal of one of its fields still shows.
278
326
 
279
327
  ### Drawing it
280
328
 
@@ -295,6 +343,29 @@ Placed.view(model, h, { options: pickers(model), words: { submit: 'Save' } })
295
343
  `Crud.editorView` works on any Submodel view of the form that takes view
296
344
  inputs; `foldkit-crud` itself stays headless.
297
345
 
346
+ A key the reader may not set can be drawn but not edited. Give the editor
347
+ `locks`, which name members to load beside the form's and say which the row
348
+ does not let its reader set; `foldkit-guard`'s `May.locks(Post)` reads the
349
+ server's `may`. `PostEditor.locked(model)` is then the form's keys to lock,
350
+ a key mapped to a relation by the relation's lock, for the form view's
351
+ `locked`:
352
+
353
+ ```ts
354
+ const Editor = Crud.editor('PostEditor', {
355
+ form: EditPostForm,
356
+ mutation: EditPostMutation,
357
+ locks: May.locks(Post),
358
+ })
359
+ const PostEditor = Editor.at({ data: Data, model: App.model.editor })
360
+
361
+ Placed.view(model, h, { options: pickers(model), locked: PostEditor.locked(model) })
362
+ ```
363
+
364
+ Nothing is locked for something new, whose create the server checks, nor
365
+ before the row is loaded. The Source must answer what `locks` selects, or the
366
+ read of the row, and so the editor, fails. A lock is a hint: the server
367
+ checks the save.
368
+
298
369
  ## Deleting
299
370
 
300
371
  A remover is a mutation with a yes in between. Like the editor it is a Bundle,
@@ -471,9 +542,14 @@ PostSort.toggle(model.postSort, 'title') // asc, then desc, then the server's ow
471
542
  PostSort.inputs(model.postSort, (_, column) => Message.SortedPosts({ column }))
472
543
  ```
473
544
 
474
- `Sort` holds nothing. A name is an order the server offers, never a column; on
475
- the server, `foldkit-remote-drizzle`'s `sortTerms(sort, { title: posts.title })`
476
- says what each name means and ignores any other.
545
+ `Sort` holds nothing. A name is an order the query offers, never a column. Say
546
+ what each name means in the query's body, where the server and the client both
547
+ read it: `Query.orderBy(Order.chosen(input.sort, { title: Post.fields.title }))`
548
+ (`foldkit-entity`). The client then knows which edits can move a row, so a
549
+ change to another field keeps the list. A `Query.make` list, which has no body,
550
+ says it on the server instead, with `foldkit-remote-drizzle`'s
551
+ `sortTerms(sort, { title: posts.title })`; any change to its Entity then
552
+ refetches it.
477
553
 
478
554
  ## Typed ids
479
555
 
package/dist/index.d.mts CHANGED
@@ -3,9 +3,9 @@ import { Bundle } from "foldkit-bundle";
3
3
  import { AnyEntity, EntityInput, EntityMember, IdOf, Selection, SelectionPage } from "foldkit-entity";
4
4
  import { FormControl, Submitted } from "foldkit-form";
5
5
  import { Metadata } from "foldkit-metadata";
6
+ import { MutationDescriptor, MutationStatus, Page, QueryDescriptor, RemoteClient, RemoteData, RemoteError, RemoteMessage } from "foldkit-remote";
6
7
  import { ActiveSurface, ModelRef, Projection } from "foldkit-surface";
7
8
  import * as Submodel from "foldkit/submodel";
8
- import { MutationDescriptor, MutationStatus, Page, QueryDescriptor, RemoteClient, RemoteData, RemoteError, RemoteMessage } from "foldkit-remote";
9
9
  import { Command } from "foldkit/command";
10
10
  import * as Update from "foldkit/update";
11
11
  //#region src/display.d.ts
@@ -158,18 +158,51 @@ export interface EditableForm<Name extends string, FormModel, FormMessage, Value
158
158
  readonly fill: (model: FormModel, values: Partial<Value>) => {
159
159
  readonly model: FormModel;
160
160
  };
161
+ readonly authoredChanged: (before: FormModel, after: FormModel) => boolean;
162
+ readonly controls: ReadonlyArray<FormControl>;
163
+ readonly Message: {
164
+ Refused(fields: {
165
+ readonly key: string;
166
+ readonly error: string;
167
+ }): FormMessage;
168
+ };
161
169
  }
162
170
  /**
163
171
  * An editor's slice of the parent Model. `target` is the id being edited, or
164
172
  * `null` for a new one; `filled` is whether the loaded value has been shown;
165
173
  * `requestId` is the save in progress or last settled.
166
174
  */
175
+ /**
176
+ * Which members of a row its reader may not set, read from members loaded
177
+ * beside the form's: what `Crud.editor`'s `locks` takes. `foldkit-guard`'s
178
+ * `May.locks(Todo)` is one, from the server's `may`.
179
+ */
180
+ export interface Locks {
181
+ /** The members to load beside the form's, by key: `{ may: true }`. */
182
+ readonly select: Readonly<Record<string, true>>;
183
+ /** Whether `member` of the loaded row may not be set by its reader. */
184
+ readonly locked: (row: Readonly<Record<string, unknown>>, member: string) => boolean;
185
+ }
167
186
  export interface EditorModel<FormModel> {
168
187
  readonly form: FormModel;
169
188
  readonly mode: 'closed' | 'new' | 'edit';
170
189
  readonly target: string | null;
171
190
  readonly filled: boolean;
172
191
  readonly requestId: string | null;
192
+ /** Whether the author changed the form since its last save was sent. */
193
+ readonly edited: boolean;
194
+ /** What the last save sent, for the form to take once it lands. */
195
+ readonly sent: Option.Option<Readonly<Record<string, unknown>>>;
196
+ /** The save whose answer the form already took, so it is taken once. */
197
+ readonly settledFor: Option.Option<string>;
198
+ /** The save whose refusal of a field the form already shows, so it is shown once. */
199
+ readonly refusedFor: Option.Option<string>;
200
+ /**
201
+ * The input values the row held as the form knows it: filled from it, and
202
+ * moved on by each save of this editor's. A save compares to it, to send
203
+ * only what changed.
204
+ */
205
+ readonly filledWith: Option.Option<Readonly<Record<string, unknown>>>;
173
206
  }
174
207
  /**
175
208
  * A remover's slice of the parent Model: the id it was asked to delete and is
@@ -189,10 +222,19 @@ export type RemoverStatus = 'Idle' |
189
222
  'Confirming' | 'Deleting' | 'Deleted' | 'DeleteFailed';
190
223
  export type EditorStatus = 'Closed' |
191
224
  /** Editing an id whose current values have not arrived. */
192
- 'Loading' | 'NotFound' | 'LoadFailed' | 'Editing' | 'Saving' | 'Saved' | 'SaveFailed';
225
+ 'Loading' | 'NotFound' | 'LoadFailed' | 'Editing' | 'Saving' | 'Saved' | 'SaveFailed' |
226
+ /** The server refused the save with `Refusal.conflict`: the row moved on since it was read. */
227
+ 'Conflict' |
228
+ /**
229
+ * Someone else saved the row since it was read: its revision moved past the
230
+ * one the form holds. The draft is kept; a save of it would be a conflict.
231
+ */
232
+ 'Moved';
193
233
  /** The parts of a bound Remote domain an editor uses. */
194
234
  export interface DomainLike<Root> {
195
- mutate(model: Root, mutation: any, input: any): {
235
+ mutate(model: Root, mutation: any, input: any, options?: {
236
+ readonly keys?: ReadonlyArray<string> | undefined;
237
+ }): {
196
238
  readonly model: Root;
197
239
  readonly requestId: string;
198
240
  readonly command: Command<RemoteMessage, never, RemoteClient>;
@@ -202,6 +244,7 @@ export interface DomainLike<Root> {
202
244
  more(model: Root, projection: any): Option.Option<Root>;
203
245
  refresh(model: Root, target: any): Root;
204
246
  mutation(model: Root, requestId: string): MutationStatus;
247
+ refusal(model: Root, requestId: string, mutation: any): Option.Option<unknown>;
205
248
  /** `Data.active`: a read of the domain as an active Surface of its application. */
206
249
  active(name: string, projectionOf: (model: Root) => Option.Option<Projection<Root, any>>): ActiveSurface<Root>;
207
250
  }
@@ -237,7 +280,7 @@ export declare const Crud: {
237
280
  readonly id: (root: Root) => Option.Option<string>;
238
281
  }) => {
239
282
  /** For `Data.subscriptions`: the value is fetched and retained while an id is shown. */
240
- active: ActiveSurface<Root>;
283
+ active: ActiveSurface<Root, unknown>;
241
284
  value: (root: Root) => RemoteData<Row>;
242
285
  /**
243
286
  * Asks for the value again: `Data.refresh` over this detail, for a retry
@@ -330,7 +373,7 @@ export declare const Crud: {
330
373
  readonly input: (root: Root) => Option.Option<Input>;
331
374
  }) => {
332
375
  /** For `Data.subscriptions`: the page and its rows are fetched and retained while shown. */
333
- active: ActiveSurface<Root>;
376
+ active: ActiveSurface<Root, unknown>;
334
377
  page: (root: Root) => RemoteData<Page<S["Type"]>>;
335
378
  /**
336
379
  * "Load more": the Model with the list showing one page more, for the
@@ -431,6 +474,12 @@ export declare const Crud: {
431
474
  }, Value, E extends AnyEntity, Fields extends Schema.Struct.Fields, Members, R = never>(name: Name, config: {
432
475
  readonly form: EditableForm<FormName, FormModel, FormMessage, Value, E, Fields, Members, R>;
433
476
  readonly mutation: MutationDescriptor<string, Value, any>;
477
+ /**
478
+ * Which of the form's keys the row being edited does not let its reader
479
+ * set, from members loaded beside the form's: `foldkit-guard`'s
480
+ * `May.locks(Todo)`. Read with `locked`.
481
+ */
482
+ readonly locks?: Locks | undefined;
434
483
  }) => {
435
484
  bundle: Bundle.Bundle<Name, void, EditorModel<FormModel>, FormMessage, Submitted<Value>, R, never, void, {}, {
436
485
  /** Edits `id`: the form is emptied, and filled once the current values arrive. */
@@ -441,6 +490,11 @@ export declare const Crud: {
441
490
  form: FormModel;
442
491
  filled: boolean;
443
492
  requestId: string | null;
493
+ edited: boolean;
494
+ sent: Option.Option<Readonly<Record<string, unknown>>>;
495
+ settledFor: Option.Option<string>;
496
+ refusedFor: Option.Option<string>;
497
+ filledWith: Option.Option<Readonly<Record<string, unknown>>>;
444
498
  };
445
499
  };
446
500
  /** A blank form for a new one: nothing to load. */
@@ -451,6 +505,11 @@ export declare const Crud: {
451
505
  form: FormModel;
452
506
  target: string | null;
453
507
  requestId: string | null;
508
+ edited: boolean;
509
+ sent: Option.Option<Readonly<Record<string, unknown>>>;
510
+ settledFor: Option.Option<string>;
511
+ refusedFor: Option.Option<string>;
512
+ filledWith: Option.Option<Readonly<Record<string, unknown>>>;
454
513
  };
455
514
  };
456
515
  close: () => {
@@ -487,7 +546,7 @@ export declare const Crud: {
487
546
  * For `Data.subscriptions`: while an id is being edited, what it loads is
488
547
  * a requirement like any Surface's, so Remote fetches and retains it.
489
548
  */
490
- active: ActiveSurface<Root>;
549
+ active: ActiveSurface<Root, unknown>;
491
550
  sync: Update.Step<Root, never, never>;
492
551
  /**
493
552
  * Wraps the application's `update` so `sync` runs after every Message:
@@ -504,6 +563,12 @@ export declare const Crud: {
504
563
  refresh: (root: Root) => Root;
505
564
  /** Why the last save failed, while `status` is `SaveFailed`. */
506
565
  saveError: (root: Root) => RemoteError | undefined;
566
+ /**
567
+ * The form's keys the row being edited does not let its reader set, by
568
+ * `locks`, for `foldkit-mixins-form`'s `locked`. None for something new,
569
+ * whose create the server checks, and none until the row is loaded.
570
+ */
571
+ locked: (root: Root) => ReadonlyArray<keyof Fields & string>;
507
572
  /** The id being edited, typed as the Entity's own; `null` for a new one or a closed editor. */
508
573
  target: (root: Root) => IdOf<E> | null;
509
574
  status: (root: Root) => EditorStatus;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/display.ts","../src/sort.ts","../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,8 +1,9 @@
1
- import { Option, Predicate, Schema, SchemaAST } from "effect";
1
+ import { Match, Option, Schema, SchemaAST } from "effect";
2
2
  import { Bundle } from "foldkit-bundle";
3
3
  import { Entity, SchemaShape, SelectionPageTypeId, Words } from "foldkit-entity";
4
4
  import { Form, Input } from "foldkit-form";
5
5
  import { Metadata } from "foldkit-metadata";
6
+ import { Refusal, RemoteData } from "foldkit-remote";
6
7
  import { Projection } from "foldkit-surface";
7
8
  import { defineMessageUnion } from "foldkit/message";
8
9
  import * as Submodel from "foldkit/submodel";
@@ -464,13 +465,21 @@ active: {
464
465
  * giving it to both guarantees.
465
466
  */
466
467
  editor: (name, config) => {
467
- const { form, mutation } = config;
468
+ const { form, mutation, locks } = config;
469
+ const sameValue = Object.fromEntries(Object.entries(form.input.schema.fields).map(([key, schema]) => [key, Schema.toEquivalence(schema)]));
470
+ const expectKey = mutation.write?.expect?.key;
471
+ const draftKeys = new Set(form.controls.filter((entry) => !Input.Nested.is(entry.control)).map((entry) => entry.key));
468
472
  const closed = {
469
473
  form: form.initial,
470
474
  mode: "closed",
471
475
  target: null,
472
476
  filled: false,
473
- requestId: null
477
+ requestId: null,
478
+ edited: false,
479
+ sent: Option.none(),
480
+ settledFor: Option.none(),
481
+ refusedFor: Option.none(),
482
+ filledWith: Option.none()
474
483
  };
475
484
  const Model = Schema.Struct({
476
485
  form: form.bundle.Model,
@@ -481,7 +490,12 @@ active: {
481
490
  ]),
482
491
  target: Schema.NullOr(Schema.String),
483
492
  filled: Schema.Boolean,
484
- requestId: Schema.NullOr(Schema.String)
493
+ requestId: Schema.NullOr(Schema.String),
494
+ edited: Schema.Boolean,
495
+ sent: Schema.OptionFromNullOr(Schema.Record(Schema.String, Schema.Unknown)),
496
+ settledFor: Schema.OptionFromNullOr(Schema.String),
497
+ refusedFor: Schema.OptionFromNullOr(Schema.String),
498
+ filledWith: Schema.OptionFromNullOr(Schema.Record(Schema.String, Schema.Unknown))
485
499
  });
486
500
  const bundle = Bundle.make(name, {
487
501
  Model,
@@ -489,17 +503,17 @@ active: {
489
503
  init: () => ({ model: closed }),
490
504
  update: (model, message) => {
491
505
  const next = form.bundle.update(model.form, message, void 0);
492
- const requestId = Predicate.isTagged(message, "Changed") ? null : model.requestId;
493
- const edited = {
494
- model: {
506
+ const edited = model.edited || form.authoredChanged(model.form, next.model);
507
+ const step = {
508
+ model: next.model === model.form && edited === model.edited ? model : {
495
509
  ...model,
496
510
  form: next.model,
497
- requestId
511
+ edited
498
512
  },
499
513
  ...next.commands === void 0 ? {} : { commands: next.commands }
500
514
  };
501
- return next.outMessage === void 0 ? edited : {
502
- ...edited,
515
+ return next.outMessage === void 0 ? step : {
516
+ ...step,
503
517
  outMessage: next.outMessage
504
518
  };
505
519
  },
@@ -519,7 +533,18 @@ active: {
519
533
  close: () => ({ model: closed })
520
534
  }
521
535
  });
522
- const current = Entity.selectFor(form.input);
536
+ const written = Entity.selectFor(form.input);
537
+ const current = locks === void 0 ? written : Entity.select(form.input.entity, {
538
+ ...written.members,
539
+ ...locks.select
540
+ });
541
+ /** The member each form key writes: a field's own key, or the relation it points. */
542
+ const writes = Object.entries(form.input.members).flatMap(([key, member]) => member === void 0 ? [] : Match.valueTags(member, {
543
+ Field: ({ key: written }) => [[key, written]],
544
+ RelationInput: ({ relation }) => [[key, relation.key]],
545
+ NestedInput: () => [],
546
+ Unmapped: () => []
547
+ }));
523
548
  return {
524
549
  bundle,
525
550
  /** What the editor loads before it shows the form: every member the form writes. */
@@ -541,18 +566,121 @@ active: {
541
566
  * last one known good, so it fills the form too: `status` goes on saying
542
567
  * `Editing`, and the failure is the read's to show.
543
568
  */
544
- const sync = (root) => {
569
+ /**
570
+ * A save the server refused for one of the form's keys shows on that key,
571
+ * once per save: `Refusal.field`'s reason, as text. Any other refusal is
572
+ * the status's to say.
573
+ */
574
+ const refused = (root) => {
545
575
  const editor = slice.get(root);
576
+ const { requestId } = editor;
577
+ if (requestId === null || Option.contains(editor.refusedFor, requestId)) return root;
578
+ const field = Option.filter(refusalOf(root), Refusal.isField);
579
+ if (Option.isNone(field) || !draftKeys.has(field.value.key)) return root;
580
+ const message = form.Message.Refused({
581
+ key: field.value.key,
582
+ error: String(field.value.reason)
583
+ });
584
+ return slice.set(root, {
585
+ ...editor,
586
+ form: form.bundle.update(editor.form, message, void 0).model,
587
+ refusedFor: Option.some(requestId)
588
+ });
589
+ };
590
+ /** The value loaded, or the last one known good when a refresh failed. */
591
+ const known = (root) => {
546
592
  const read = loaded(root);
547
- if (editor.filled || read === void 0) return { model: root };
548
- const value = read._tag === "Ready" || read._tag === "Refreshing" ? read.value : read._tag === "Failed" ? read.previous : void 0;
549
- if (value === void 0) return { model: root };
593
+ return read === void 0 ? Option.none() : RemoteData.match(read, {
594
+ Initial: () => Option.none(),
595
+ Loading: () => Option.none(),
596
+ Ready: Option.some,
597
+ Refreshing: Option.some,
598
+ Failed: (_, previous) => previous,
599
+ NotFound: () => Option.none()
600
+ });
601
+ };
602
+ const filledOf = (root) => {
603
+ const editor = slice.get(root);
604
+ if (editor.filled) return root;
605
+ const shown = known(root);
606
+ if (Option.isNone(shown)) return root;
607
+ const value = shown.value;
550
608
  const values = Entity.valuesFor(form.input, value);
551
- return { model: slice.set(root, {
609
+ return slice.set(root, {
552
610
  ...editor,
553
611
  form: form.fill(editor.form, values).model,
554
- filled: true
555
- }) };
612
+ filled: true,
613
+ filledWith: Option.some(values)
614
+ });
615
+ };
616
+ /**
617
+ * The input keys a save changes from what the form was filled with, by
618
+ * each key's own schema: none filled (something new) is every key, so
619
+ * none are named.
620
+ */
621
+ const changedKeys = (editor, value) => {
622
+ const submitted = value;
623
+ return Option.match(editor.filledWith, {
624
+ onNone: () => void 0,
625
+ onSome: (filled) => Object.keys(submitted).filter((key) => !Object.hasOwn(filled, key) || !sameValue[key](filled[key], submitted[key]))
626
+ });
627
+ };
628
+ /** The revision the row is at now, as the form's `expect` key holds it. */
629
+ const currentRevision = (root) => {
630
+ const read = loaded(root);
631
+ if (expectKey === void 0 || read === void 0) return Option.none();
632
+ if (read._tag !== "Ready" && read._tag !== "Refreshing") return Option.none();
633
+ const values = Entity.valuesFor(form.input, read.value);
634
+ return Object.hasOwn(values, expectKey) ? Option.some(values[expectKey]) : Option.none();
635
+ };
636
+ /** Whether the row is at another revision than the one the form was filled with. */
637
+ const moved = (root) => {
638
+ const { filledWith } = slice.get(root);
639
+ return expectKey !== void 0 && Option.isSome(filledWith) && Option.exists(currentRevision(root), (revision) => !sameValue[expectKey](filledWith.value[expectKey], revision));
640
+ };
641
+ /**
642
+ * Once this editor's save lands, the form takes its answer, once: what
643
+ * it sent is now what the row holds as the form knows it, and the
644
+ * revision is the one it moved the row to, or its next save would
645
+ * conflict with itself. A revision moved after that is another's, and
646
+ * reads as `Moved`. A create's form starts blank again, its values now
647
+ * the row's; one the author went on typing in is a new one's draft.
648
+ */
649
+ const settled = (root) => {
650
+ const editor = slice.get(root);
651
+ const { requestId } = editor;
652
+ if (requestId === null || Option.contains(editor.settledFor, requestId)) return root;
653
+ if (saveOf(root)._tag !== "Applied") return root;
654
+ const settledFor = Option.some(requestId);
655
+ if (editor.mode === "new") return slice.set(root, {
656
+ ...editor,
657
+ settledFor,
658
+ ...editor.edited ? {} : { form: form.initial }
659
+ });
660
+ const revision = expectKey === void 0 ? Option.none() : currentRevision(root);
661
+ const filledWith = Option.some({
662
+ ...Option.getOrElse(editor.filledWith, () => ({})),
663
+ ...Option.getOrElse(editor.sent, () => ({})),
664
+ ...Option.match(revision, {
665
+ onNone: () => ({}),
666
+ onSome: (value) => ({ [expectKey]: value })
667
+ })
668
+ });
669
+ return slice.set(root, {
670
+ ...editor,
671
+ settledFor,
672
+ filledWith,
673
+ form: Option.match(revision, {
674
+ onNone: () => editor.form,
675
+ onSome: (value) => form.fill(editor.form, { [expectKey]: value }).model
676
+ })
677
+ });
678
+ };
679
+ const sync = (root) => ({ model: settled(refused(filledOf(root))) });
680
+ /** Why the server refused the last save, when it refused it as data. */
681
+ const refusalOf = (root) => {
682
+ const { requestId } = slice.get(root);
683
+ return requestId === null ? Option.none() : data.refusal(root, requestId, mutation);
556
684
  };
557
685
  const saveOf = (root) => {
558
686
  const { requestId } = slice.get(root);
@@ -561,12 +689,14 @@ active: {
561
689
  return {
562
690
  /** For the placement: a valid submit becomes the mutation, and its request is remembered. */
563
691
  onOut: (submitted) => (root) => {
564
- const started = data.mutate(root, mutation, submitted.value);
692
+ const started = data.mutate(root, mutation, submitted.value, mutation.write === void 0 ? void 0 : { keys: changedKeys(slice.get(root), submitted.value) });
565
693
  const editor = slice.get(started.model);
566
694
  return {
567
695
  model: slice.set(started.model, {
568
696
  ...editor,
569
- requestId: started.requestId
697
+ requestId: started.requestId,
698
+ edited: false,
699
+ sent: Option.some(submitted.value)
570
700
  }),
571
701
  commands: [started.command]
572
702
  };
@@ -605,6 +735,15 @@ active: {
605
735
  const save = saveOf(root);
606
736
  return save._tag === "Failed" ? save.error : void 0;
607
737
  },
738
+ /**
739
+ * The form's keys the row being edited does not let its reader set, by
740
+ * `locks`, for `foldkit-mixins-form`'s `locked`. None for something new,
741
+ * whose create the server checks, and none until the row is loaded.
742
+ */
743
+ locked: (root) => locks === void 0 ? [] : Option.match(known(root), {
744
+ onNone: () => [],
745
+ onSome: (row) => writes.flatMap(([key, member]) => locks.locked(row, member) ? [key] : [])
746
+ }),
608
747
  /** The id being edited, typed as the Entity's own; `null` for a new one or a closed editor. */
609
748
  target: (root) => slice.get(root).target,
610
749
  status: (root) => {
@@ -612,12 +751,14 @@ active: {
612
751
  if (editor.mode === "closed") return "Closed";
613
752
  const read = loaded(root);
614
753
  if (read?._tag === "NotFound") return "NotFound";
615
- switch (saveOf(root)._tag) {
754
+ const save = saveOf(root);
755
+ if (editor.edited) return moved(root) ? "Moved" : "Editing";
756
+ switch (save._tag) {
616
757
  case "Pending": return "Saving";
617
- case "Failed": return "SaveFailed";
618
- case "Applied": return "Saved";
758
+ case "Failed": return Option.exists(refusalOf(root), Refusal.isConflict) ? "Conflict" : "SaveFailed";
759
+ case "Applied": return moved(root) ? "Moved" : "Saved";
619
760
  case "Unknown":
620
- if (editor.filled) return "Editing";
761
+ if (editor.filled) return moved(root) ? "Moved" : "Editing";
621
762
  return read?._tag === "Failed" ? "LoadFailed" : "Loading";
622
763
  }
623
764
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/display.ts","../src/sort.ts","../src/index.ts"],"sourcesContent":["/**\n * Displays: how one selected member shows in a list or a detail, described\n * without a renderer. It is `foldkit-form`'s `Input` from the reading side: the\n * member says what it is, a Display says how it reads, and which DOM draws it is\n * the application's.\n */\nimport { Schema, SchemaAST } from 'effect'\nimport { SchemaShape, type AnyEntity, type EntityMember } from 'foldkit-entity'\nimport { Metadata } from 'foldkit-metadata'\n\n/**\n * How one selected member shows: the primitive every kind of Display is a value\n * of. `Display.text()` and `Display.flag()` are values of it, made by\n * `Display.kind`, which is what an application calls for a badge or a relative\n * date. The kinds here are a collection, not a special case.\n */\nexport interface Display<Data = unknown> {\n /** The name a renderer is found by. */\n readonly kind: string\n /** Whether a view draws it. A member read and not shown, usually the id, is `false`. */\n readonly shown: boolean\n /** What this kind needs: a format, a relation's target. */\n readonly data: Data\n /**\n * The value as text: what a cell says when nothing draws it specially. It is\n * never given `null` or `undefined`; `Display.show` says `nothing` for those.\n */\n readonly text: (value: unknown, words: DisplayWords) => string\n}\n\n/** A kind of Display: how its values are made, and how one is told from another. */\nexport interface DisplayKind<Data> {\n readonly kind: string\n readonly of: (data: Data) => Display<Data>\n readonly is: (display: Display) => display is Display<Data>\n}\n\n/** A selected member with its words and its Display. A list's column, a detail's line. */\nexport interface DisplayColumn<Key extends string = string> {\n readonly key: Key\n readonly label: string\n readonly member: EntityMember\n readonly display: Display\n}\n\n/** The words a Display needs that are not the value's own. */\nexport interface DisplayWords {\n /** Default `yes`. */\n readonly yes?: string\n /** Default `no`. */\n readonly no?: string\n /** What stands for nothing: `null`, `undefined`, an empty list. Default the empty string. */\n readonly nothing?: string\n /** Between the items of a list, and between the members of a nested value. Default `, `. */\n readonly separator?: string\n}\n\nconst key = Metadata.key<Display>('foldkit-crud/display', {\n // The last Display attached wins: a later annotation refines an earlier one.\n merge: displays => displays.slice(-1),\n summarize: display => display.kind,\n})\n\nconst fromSchema = (schema: Schema.Top): Display => {\n const members = SchemaShape.present(Schema.toType(schema).ast)\n const [only] = members\n if (members.length === 1 && only !== undefined) {\n if (SchemaAST.isNumber(only)) return Number_.of({})\n if (SchemaAST.isBoolean(only)) return Flag.of(nothing)\n }\n return Text.of({})\n}\n\n/** A kind of Display. The kinds below are made with it, and so is an application's. */\nconst kind = <Data = Record<string, never>>(\n name: string,\n spec: {\n readonly shown?: boolean\n readonly text: (data: Data, value: unknown, words: DisplayWords) => string\n },\n): DisplayKind<Data> => ({\n kind: name,\n of: data =>\n Object.freeze({\n kind: name,\n shown: spec.shown ?? true,\n data,\n text: (value: unknown, words: DisplayWords) => spec.text(data, value, words),\n }),\n is: (display): display is Display<Data> => display.kind === name,\n})\n\nconst show = (display: Display, value: unknown, words: DisplayWords = {}): string =>\n value === null || value === undefined ? (words.nothing ?? '') : display.text(value, words)\n\nconst Text = kind<{ readonly format?: ((value: unknown) => string) | undefined }>('Text', {\n text: (data, value) => (data.format === undefined ? String(value) : data.format(value)),\n})\nconst Number_ = kind<{ readonly format?: ((value: number) => string) | undefined }>('Number', {\n text: (data, value) =>\n typeof value === 'number' && data.format !== undefined ? data.format(value) : String(value),\n})\nconst Flag = kind('Flag', {\n text: (_, value, words) => (value === true ? (words.yes ?? 'yes') : (words.no ?? 'no')),\n})\nconst Hidden = kind('Hidden', { shown: false, text: () => '' })\n/** A relation read as refs: which one, with nothing to say about it but its id. */\nconst Ref = kind<{ readonly target: AnyEntity; readonly many: boolean }>('Ref', {\n text: (_, value, words) => {\n const ids = (Array.isArray(value) ? value : [value]).map(ref =>\n typeof ref === 'object' && ref !== null && 'id' in ref ? String(ref.id) : String(ref),\n )\n return ids.length === 0 ? (words.nothing ?? '') : ids.join(words.separator ?? ', ')\n },\n})\n/**\n * A relation read through a Selection: the target's own members, each with a\n * Display. `shape` is how many there are: one, a list, or a page of a list.\n */\nconst Nested = kind<{\n readonly target: AnyEntity\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n}>('Nested', {\n text: (data, value, words) => {\n const one = (item: unknown): string =>\n data.columns\n .filter(column => column.display.shown)\n .map(column =>\n show(\n column.display,\n (item as Readonly<Record<string, unknown>> | null)?.[column.key],\n words,\n ),\n )\n .filter(text => text !== '')\n .join(' ')\n const items =\n data.shape === 'one'\n ? [value]\n : data.shape === 'page'\n ? ((value as { readonly items?: ReadonlyArray<unknown> }).items ?? [])\n : (value as ReadonlyArray<unknown>)\n return items.length === 0 ? (words.nothing ?? '') : items.map(one).join(words.separator ?? ', ')\n },\n})\n\nconst nothing: Record<string, never> = Object.freeze({})\n\nexport const Display = {\n /**\n * A kind of Display. `Display.kind<{ tone: string }>('Badge', { text: (_, value) => String(value) })`\n * gives `.of(data)` to make one and `.is(display)` to tell one, exactly as the\n * kinds below were made. `text` is the floor; a view draws it specially once\n * it is given a renderer for `Badge`.\n */\n kind,\n\n // The kinds this package resolves to. A view finds its renderer by `kind`.\n Text,\n Number: Number_,\n Flag,\n Hidden,\n Ref,\n Nested,\n\n text: (format?: (value: unknown) => string): Display => Text.of({ format }),\n number: (format?: (value: number) => string): Display => Number_.of({ format }),\n flag: (): Display => Flag.of(nothing),\n /** Read, so a row can be opened or keyed by it, and not shown: usually the id. */\n hidden: (): Display => Hidden.of(nothing),\n\n /**\n * Entity metadata: how a member shows wherever it is listed, e.g.\n * `Entity.annotateMembers({ price: Display.of(Display.number(cents => ...)) })`.\n */\n of: (display: Display) => key.of(display),\n\n /**\n * The Display of one selected member, from the most to the least explicit\n * source: the member's `Display.of` metadata, how the relation was selected,\n * then the shape of its schema. `nested` is the columns of a nested Selection,\n * with `shape` saying how many the relation yields.\n */\n resolve: (\n member: EntityMember,\n nested?: {\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n },\n ): Display => {\n const [explicit] = key.get(member.metadata)\n if (explicit !== undefined) return explicit\n if (member._tag !== 'Relation') return fromSchema(member.schema as Schema.Top)\n const target = member.target()\n return nested === undefined\n ? Ref.of({ target, many: member.cardinality === 'many' })\n : Nested.of({ target, shape: nested.shape, columns: nested.columns })\n },\n\n /**\n * A value as the text its Display calls for: what a cell says when nothing\n * draws it specially. A view that wants a link or a badge reads the Display\n * and the value itself; this is the floor every view can stand on.\n */\n show,\n}\n","/**\n * Sorting a list, written once. How a list is sorted is the application's state\n * and its query's input, so this holds none of it: it is the schema of that\n * state, what a click on a header makes of it, and the shape a drawn table\n * takes. The server reads the same state and decides which columns it means.\n */\nimport { Schema } from 'effect'\n\n/** How a list is sorted: by which of its orders, which way; `null` for the server's own order. */\nexport type SortState<Column extends string> = {\n readonly by: Column\n readonly direction: 'asc' | 'desc'\n} | null\n\n/** How one column is sorted now, and the Message a click on its header sends. */\nexport interface SortedColumn<Message> {\n readonly direction?: 'asc' | 'desc' | undefined\n readonly message: Message\n}\n\nexport const Sort = {\n /**\n * The orders a list offers, by name. A name is an order the server knows, not\n * a column: which columns may sort, and what `title` means in SQL, stay the\n * server's to decide.\n */\n make: <const Columns extends readonly [string, ...string[]]>(columns: Columns) => {\n type Column = Columns[number]\n type State = SortState<Column>\n /** A click on a header: ascending, then descending, then the server's own order again. */\n const toggle = (current: State, column: Column): State =>\n current === null || current.by !== column\n ? { by: column, direction: 'asc' }\n : current.direction === 'asc'\n ? { by: column, direction: 'desc' }\n : null\n return {\n columns,\n /** For the Model field, and for the query's input. */\n Schema: Schema.NullOr(\n Schema.Struct({\n by: Schema.Literals(columns),\n direction: Schema.Literals(['asc', 'desc']),\n }),\n ) as unknown as Schema.Codec<State, State>,\n /** The server's own order. */\n none: null as State,\n toggle,\n /**\n * What a drawn table takes as `sort`: every order with how it is sorted now\n * and the Message a click sends, made from the state the click leads to and\n * the column clicked. A click meets the state last drawn, so two clicks\n * inside one frame both send the same `next`; send the column instead\n * (`(_, column) => Sorted({ column })`) and `toggle` it in `update`, from\n * the Model as it is then.\n */\n inputs: <Message>(\n current: State,\n message: (next: State, column: Column) => 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), column),\n },\n ]),\n ) as { readonly [K in Column]: SortedColumn<Message> },\n }\n },\n}\n","/**\n * `foldkit-crud` — management screens assembled from parts an application\n * already has.\n *\n * An editor joins a `foldkit-form` form, the Remote mutation its value feeds,\n * and the Entity they share: it loads what the form writes, shows it, turns a\n * valid submit into the mutation, and says how that went. Everything it makes is\n * ordinary Foldkit: a Bundle, an ActiveSurface, Update Steps. Nothing is\n * generated from an Entity alone; each capability is declared.\n */\nimport { Option, Predicate, Schema } from 'effect'\nimport { Bundle } from 'foldkit-bundle'\nimport {\n Entity,\n SelectionPageTypeId,\n Words,\n type AnyEntity,\n type EntityInput,\n type EntityMember,\n type Selection,\n type IdOf,\n type SelectionPage,\n} from 'foldkit-entity'\nimport { Form, Input, type FormControl, type Submitted } from 'foldkit-form'\nimport { Display, type DisplayColumn } from './display.js'\nimport type {\n MutationDescriptor,\n Page,\n QueryDescriptor,\n MutationStatus,\n RemoteClient,\n RemoteData,\n RemoteError,\n RemoteMessage,\n} from 'foldkit-remote'\nimport { Projection, type ActiveSurface, type ModelRef } from 'foldkit-surface'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type { Html, HtmlBuilder } from 'foldkit/html'\nimport * as Submodel from 'foldkit/submodel'\nimport type * as Update from 'foldkit/update'\n\n/** The parts of a `Form.make` result an editor wraps. */\nexport interface EditableForm<\n Name extends string,\n FormModel,\n FormMessage,\n Value,\n E extends AnyEntity,\n Fields extends Schema.Struct.Fields,\n Members,\n R = never,\n> {\n readonly bundle: {\n readonly name: Name\n readonly Model: Schema.Codec<FormModel, unknown>\n readonly Message: Schema.Codec<FormMessage, unknown>\n readonly update: (\n model: FormModel,\n message: FormMessage,\n args: void,\n ) => Update.ReturnWithOutMessage<FormModel, FormMessage, Submitted<Value>, R>\n }\n readonly input: EntityInput<E, Fields, Members>\n readonly initial: FormModel\n readonly fill: (model: FormModel, values: Partial<Value>) => { readonly model: FormModel }\n}\n\n/**\n * An editor's slice of the parent Model. `target` is the id being edited, or\n * `null` for a new one; `filled` is whether the loaded value has been shown;\n * `requestId` is the save in progress or last settled.\n */\nexport interface EditorModel<FormModel> {\n readonly form: FormModel\n readonly mode: 'closed' | 'new' | 'edit'\n readonly target: string | null\n readonly filled: boolean\n readonly requestId: string | null\n}\n\n/**\n * A remover's slice of the parent Model: the id it was asked to delete and is\n * waiting on a yes for, and the delete in progress or last settled.\n */\nexport interface RemoverModel {\n readonly target: string | null\n readonly requestId: string | null\n}\n\n/** A remover's out Message: the user said yes to deleting `id`. */\nexport interface ConfirmedRemoval {\n readonly _tag: 'Confirmed'\n readonly id: string\n}\n\nexport type RemoverStatus =\n | 'Idle'\n /** Asked to delete an id, and waiting for a yes or a no. */\n | 'Confirming'\n | 'Deleting'\n | 'Deleted'\n | 'DeleteFailed'\n\nexport type EditorStatus =\n | 'Closed'\n /** Editing an id whose current values have not arrived. */\n | 'Loading'\n | 'NotFound'\n | 'LoadFailed'\n | 'Editing'\n | 'Saving'\n | 'Saved'\n | 'SaveFailed'\n\n/** The parts of a bound Remote domain an editor uses. */\nexport interface DomainLike<Root> {\n // Method syntax: the descriptors are checked where the editor is made, not here.\n mutate(\n model: Root,\n mutation: any,\n input: any,\n ): {\n readonly model: Root\n readonly requestId: string\n readonly command: Command<RemoteMessage, never, RemoteClient>\n }\n get(selection: any, id: string): Projection<Root, RemoteData<any>>\n query(query: any, input: any, options: any): Projection<Root, RemoteData<Page<any>>>\n more(model: Root, projection: any): Option.Option<Root>\n refresh(model: Root, target: any): Root\n mutation(model: Root, requestId: string): MutationStatus\n /** `Data.active`: a read of the domain as an active Surface of its application. */\n active(\n name: string,\n projectionOf: (model: Root) => Option.Option<Projection<Root, any>>,\n ): ActiveSurface<Root>\n}\n\ntype Step<Root> = Update.Step<Root, RemoteMessage, RemoteClient>\n\n/** One thing a relation picker offers. */\nexport interface Choice {\n readonly value: string\n readonly label: string\n}\n\n/**\n * One column of a list, or one line of a detail: a member the Selection reads,\n * with its words and its `Display`.\n */\nexport type ListColumn<Key extends string = string> = DisplayColumn<Key>\n\nconst columnLabel = (key: string, member: EntityMember): string =>\n Option.getOrElse(\n member._tag === 'Relation' ? Option.none() : Words.of(member.schema as Schema.Top).title,\n () => Form.labelOf(member) ?? key,\n )\n\ntype AnySelection = Selection<string, unknown, Schema.Constraint>\n\n/** The selected members in the Selection's order, each with its label and its Display. */\nconst columnsOf = <Members>(\n selection: Selection<string, Members, Schema.Constraint>,\n): ReadonlyArray<ListColumn<keyof Members & string>> => {\n const members: Readonly<Record<string, EntityMember>> = selection.entity.members\n const selected = selection.members as Readonly<\n Record<string, true | AnySelection | SelectionPage<string, Schema.Constraint>>\n >\n return Object.keys(selected).map(key => {\n const member = members[key]!\n const how = selected[key]!\n // A relation read through a Selection shows the target's own members.\n const nested =\n how === true || member._tag !== 'Relation'\n ? undefined\n : SelectionPageTypeId in how\n ? { shape: 'page' as const, columns: columnsOf(how.selection) }\n : {\n shape: member.cardinality === 'many' ? ('many' as const) : ('one' as const),\n columns: columnsOf(how),\n }\n return {\n key: key as keyof Members & string,\n label: columnLabel(key, member),\n member,\n display: Display.resolve(member, nested),\n }\n })\n}\n\nconst RemoverMessage = defineMessageUnion({ Confirmed: {}, Cancelled: {} })\ntype RemoverMessage = typeof RemoverMessage.Type\n\nconst idle: RemoverModel = { target: null, requestId: null }\n\nexport const Crud = {\n /**\n * One Entity read through a Selection, for a page that shows it. Like a list\n * it holds no state: the value is Remote's and the id is the application's.\n */\n detail: <const Name extends string, EntityName extends string, Members, Row>(\n name: Name,\n config: {\n readonly selection: Selection<EntityName, Members, Schema.Constraint & { readonly Type: Row }>\n },\n ) => ({\n name,\n /** The selected members in the Selection's order, each with its label. */\n fields: columnsOf(config.selection),\n /** Type-only: the value shown, for a view to be typed by. Never read. */\n Value: undefined as unknown as Row,\n\n /** The detail where it lives: `id` is the one shown, none while none is. */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly id: (root: Root) => Option.Option<string>\n }) => {\n const projectionOf = (root: Root) =>\n Option.map(where.id(root), id => where.data.get(config.selection, id))\n return {\n /** For `Data.subscriptions`: the value is fetched and retained while an id is shown. */\n active: where.data.active(name, projectionOf),\n value: (root: Root): RemoteData<Row> =>\n Option.match(projectionOf(root), {\n onNone: () => ({ _tag: 'Initial' }),\n onSome: projection => projection.read(root),\n }),\n /**\n * Asks for the value again: `Data.refresh` over this detail, for a retry\n * button. A failed read is not retried on its own. Unchanged while no id\n * is shown.\n */\n refresh: (root: Root): Root =>\n Option.match(projectionOf(root), {\n onNone: () => root,\n onSome: projection => where.data.refresh(root, projection),\n }),\n }\n },\n }),\n\n /**\n * Deleting through one mutation, with a yes in between: asked, confirmed,\n * deleted. What the mutation's input is for an id is `input`'s to say, and the\n * id it names there (`(id: PostId) => ...`) is the id `ask` takes.\n */\n remover: <\n const Name extends string,\n Input,\n Id extends string = string,\n const Key extends keyof Input = never,\n >(\n name: Name,\n config:\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n readonly input: (id: Id) => Input\n }\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n /**\n * The key of the mutation's input that holds the id, when that is all the\n * input is: `id: 'id'` for `{ id }`. The id's type is that key's.\n */\n readonly id: Key & (Input extends Readonly<Record<Key, string>> ? Key : never)\n },\n ) => {\n type AskedId = [Key] extends [never] ? Id : Input[Key] & string\n const inputFor = (id: string): Input =>\n 'input' in config ? config.input(id as Id) : ({ [config.id]: id } as Input)\n const bundle = Bundle.make(name, {\n Model: Schema.Struct({\n target: Schema.NullOr(Schema.String),\n requestId: Schema.NullOr(Schema.String),\n }) as unknown as Schema.Codec<RemoverModel, unknown>,\n Message: RemoverMessage,\n init: () => ({ model: idle }),\n update: (model: RemoverModel, message: RemoverMessage) =>\n message._tag === 'Cancelled' || model.target === null\n ? { model: idle }\n : { model, outMessage: { _tag: 'Confirmed', id: model.target } as ConfirmedRemoval },\n helpers: {\n /** Asks whether to delete `id`. Nothing is deleted until `Confirmed`. */\n ask: (_: RemoverModel, id: AskedId) => ({ model: { target: id, requestId: null } }),\n dismiss: () => ({ model: idle }),\n },\n })\n\n return {\n bundle,\n /** The remover's Messages, for the yes and the no of a confirmation. */\n Message: RemoverMessage,\n\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, RemoverModel>\n }) => {\n const { data, model: slice } = where\n const outcome = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n return {\n /** For the placement: a yes becomes the mutation, and its request is remembered. */\n onOut:\n (confirmed: ConfirmedRemoval): Step<Root> =>\n root => {\n const started = data.mutate(root, config.mutation, inputFor(confirmed.id))\n return {\n model: slice.set(started.model, {\n target: confirmed.id,\n requestId: started.requestId,\n }),\n commands: [started.command],\n }\n },\n\n /** The id being asked about or deleted, for the words of a confirmation. */\n target: (root: Root): AskedId | null => slice.get(root).target as AskedId | null,\n\n status: (root: Root): RemoverStatus => {\n const { target } = slice.get(root)\n const deleting = outcome(root)\n return deleting._tag === 'Pending'\n ? 'Deleting'\n : deleting._tag === 'Applied'\n ? 'Deleted'\n : deleting._tag === 'Failed'\n ? 'DeleteFailed'\n : target === null\n ? 'Idle'\n : 'Confirming'\n },\n\n /** Why the last delete failed, while `status` is `DeleteFailed`. */\n error: (root: Root): RemoteError | undefined => {\n const deleting = outcome(root)\n return deleting._tag === 'Failed' ? deleting.error : undefined\n },\n }\n },\n }\n },\n\n /**\n * A list over one query: which rows to show is the query's, what to show of\n * each is the Selection's. The pages live in Remote; the list holds no state.\n */\n list: <\n const Name extends string,\n EntityName extends string,\n Members,\n S extends Schema.Constraint,\n Input,\n >(\n name: Name,\n config: {\n readonly query: QueryDescriptor<string, Input, any>\n // The row is the Selection's own value, so it is typed with or without a `choice`.\n readonly selection: Selection<EntityName, Members, S>\n /** How many rows a page holds. Default 25. */\n readonly pageSize?: number\n /**\n * How a row reads as a choice in a relation picker: its id, and the words\n * that identify it to a person. Give it to a list that feeds pickers.\n */\n readonly choice?: {\n readonly value: (row: S['Type']) => string\n readonly label: (row: S['Type']) => string\n }\n },\n ) => {\n const { query, selection, pageSize = 25, choice } = config\n const columns = columnsOf(selection)\n\n return {\n name,\n /** The selected members in the Selection's order, each with its label. */\n columns,\n /** Type-only: a row, for a view to be typed by. Never read. */\n Row: undefined as unknown as S['Type'],\n\n /**\n * The list where it lives. `input` is the query's input as the Model has\n * it (filters, a search term), or `undefined` while the list is not shown.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n /** The list's input from the Model; none while the list is not shown. */\n readonly input: (root: Root) => Option.Option<Input>\n }) => {\n const { data, input } = where\n const projectionOf = (root: Root) =>\n Option.map(input(root), value =>\n data.query(query, value, { select: selection, first: pageSize }),\n )\n const page = (root: Root): RemoteData<Page<S['Type']>> =>\n Option.match(projectionOf(root), {\n onNone: () => ({ _tag: 'Initial' }),\n onSome: projection => projection.read(root),\n })\n\n const active = data.active(name, projectionOf)\n\n return {\n /** For `Data.subscriptions`: the page and its rows are fetched and retained while shown. */\n active,\n\n page,\n\n /**\n * \"Load more\": the Model with the list showing one page more, for the\n * read entry to fetch; none when it shows every row there is.\n */\n more: (root: Root): Option.Option<Root> =>\n Option.flatMap(projectionOf(root), projection => data.more(root, projection)),\n\n /**\n * Asks for the list again: `Data.refresh` over its page and rows, for a\n * retry button. A failed read is not retried on its own. Unchanged while\n * the list is not shown.\n */\n refresh: (root: Root): Root =>\n Option.match(projectionOf(root), {\n onNone: () => root,\n onSome: projection => data.refresh(root, projection),\n }),\n\n name,\n /** Whose requirement the list is, for anything that requires beside it. */\n owner: active.owner,\n /**\n * One of the list's Entity through the list's Selection, by id, whether or\n * not the query finds it now: what a picker reads to name what is chosen.\n */\n row: (id: string): Projection<Root, RemoteData<any>> => data.get(selection, id),\n /** The Entity the rows are of, which is how a picker finds the list for its target. */\n entity: selection.entity as AnyEntity,\n /** Whether the list was given a `choice`, so its rows can be a picker's choices. */\n offersChoices: choice !== undefined,\n\n /**\n * The loaded rows as a picker's choices, read through `choice`. Listing a\n * relation's target is this query, which the application chose and the\n * server authorizes; nothing is read because a relation exists.\n */\n choices: (root: Root): ReadonlyArray<Choice> => {\n if (choice === undefined)\n throw new Error(`Crud list \"${name}\": give it a \"choice\" to use its rows in a picker`)\n const read = page(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? read.value.items.map(row => ({\n value: choice.value(row),\n label: choice.label(row),\n }))\n : []\n },\n\n /** That row as a choice, once it is read. */\n choiceOf: (root: Root, id: string): Choice | undefined => {\n if (choice === undefined) return undefined\n const read = data.get(selection, id).read(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? { value: choice.value(read.value), label: choice.label(read.value) }\n : undefined\n },\n }\n },\n }\n },\n\n /**\n * A form's Submodel view as the view of the editor that wraps it, for\n * `Bundle.withView`: the editor's Messages are the form's, and its Model holds\n * the form's under `form`.\n *\n * ```ts\n * Editor.bundle.pipe(Bundle.withView(Crud.editorView(FormView.submodel(form, view))))\n * ```\n */\n editorView: <FormModel, Message, ViewInputs>(\n view: Submodel.View<FormModel, Message, ViewInputs>,\n ): Submodel.View<EditorModel<FormModel>, Message, ViewInputs> =>\n Submodel.defineView<EditorModel<FormModel>, Message, ViewInputs>(((\n model: EditorModel<FormModel>,\n inputs: ViewInputs,\n h: HtmlBuilder<Message>,\n ): Html =>\n (view as (model: FormModel, inputs: ViewInputs, h: HtmlBuilder<Message>) => Html)(\n model.form,\n inputs,\n h,\n )) as never),\n\n /**\n * The choices of every relation picker in a form, from the lists of their\n * targets: `foldkit-mixins-form`'s `options`, keyed by the form's keys. A\n * picker whose target no list here is over is a wiring mistake, reported now.\n *\n * With `chosen`, the form's Model as the page has it, what a picker already\n * holds stays among its choices when the list no longer finds it (a search\n * narrowed it, or it is on a later page), and `active` requires those rows so\n * their words are there. Give `chosen` to any form whose pickers search.\n */\n options: <Key extends string, Root, FormModel = never>(\n form: {\n readonly controls: ReadonlyArray<FormControl<Key>>\n readonly field?: (model: FormModel, key: never) => { readonly value: unknown }\n },\n lists: ReadonlyArray<{\n readonly name: string\n readonly entity: AnyEntity\n readonly offersChoices: boolean\n readonly owner: object\n readonly choices: (root: Root) => ReadonlyArray<Choice>\n readonly row: (id: string) => Projection<Root, RemoteData<any>>\n readonly choiceOf: (root: Root, id: string) => Choice | undefined\n }>,\n options: { readonly chosen?: (root: Root) => FormModel | undefined } = {},\n ) => {\n const pickers = form.controls.flatMap(({ key, control }) => {\n if (!Input.RelationOne.is(control) && !Input.RelationMany.is(control)) return []\n const { target } = control.data\n const list = lists.find(candidate => Entity.same(candidate.entity, target))\n if (list === undefined)\n throw new Error(\n `Crud.options: \"${key}\" picks a ${target.name}, and no list given is over ${target.name}`,\n )\n if (!list.offersChoices)\n throw new Error(\n `Crud.options: \"${key}\" would pick from list \"${list.name}\", which has no \"choice\"`,\n )\n return [[key, list] as const]\n })\n\n /** The ids each picker holds now: the draft of a `one`, or of a `many`. */\n const held = (root: Root): ReadonlyArray<readonly [Key, (typeof lists)[number], string]> => {\n const model = options.chosen?.(root)\n const field = form.field as\n ((model: FormModel, key: string) => { readonly value: unknown }) | undefined\n if (model === undefined || field === undefined) return []\n return pickers.flatMap(([key, list]) => {\n const draft = field(model, key).value\n const ids = Array.isArray(draft) ? draft : typeof draft === 'string' ? [draft] : []\n return ids\n .filter((id): id is string => typeof id === 'string' && id !== '')\n .map(id => [key, list, id] as const)\n })\n }\n\n const choices = (root: Root): { readonly [K in Key]?: ReadonlyArray<Choice> } => {\n const chosen = held(root)\n return Object.fromEntries(\n pickers.map(([key, list]) => {\n const listed = list.choices(root)\n const missing = chosen\n .filter(([heldBy, , id]) => heldBy === key && !listed.some(item => item.value === id))\n .flatMap(([, , id]) => list.choiceOf(root, id) ?? [])\n // What is chosen leads, so it is in reach whatever the list shows.\n return [key, [...missing, ...listed]]\n }),\n ) as unknown as { readonly [K in Key]?: ReadonlyArray<Choice> }\n }\n\n const [first] = lists\n if (first === undefined)\n throw new Error(\n 'Crud.options: give it the lists its pickers pick from; with none, it has nothing to read',\n )\n return Object.assign(choices, {\n /** For `Data.subscriptions`: what the pickers hold is read, so it can be named. */\n active: {\n name: `${pickers.map(([key]) => key).join('+')} chosen`,\n owner: first.owner,\n projectionOf: (root: Root) => {\n const rows = held(root)\n return rows.length === 0\n ? Option.none()\n : Option.some(\n Projection.struct(\n Object.fromEntries(rows.map(([key, list, id]) => [`${key}:${id}`, list.row(id)])),\n ),\n )\n },\n messages: [],\n } satisfies ActiveSurface<Root>,\n })\n },\n\n /**\n * What the pieces on a page require of Remote, by the names given: a placed\n * editor, list, or detail, or the pickers from `Crud.options`. It is each one's\n * `active`, gathered, so handing the pieces over is enough and none is left out:\n *\n * ```ts\n * Data.wiring(Crud.actives({ posts: Posts, authors: Authors, editor: PostEditor, pickers }))\n * ```\n */\n actives: <const Pieces extends Readonly<Record<string, { readonly active: unknown }>>>(\n pieces: Pieces,\n ): { readonly [K in keyof Pieces]: Pieces[K]['active'] } =>\n Object.fromEntries(Object.entries(pieces).map(([name, piece]) => [name, piece.active])) as {\n readonly [K in keyof Pieces]: Pieces[K]['active']\n },\n\n /**\n * An editor for one form and the mutation its value feeds. The form's value\n * must be the mutation's input, which is what declaring the input once and\n * giving it to both guarantees.\n */\n editor: <\n const Name extends string,\n FormName extends string,\n FormModel,\n FormMessage extends { readonly _tag: string },\n Value,\n E extends AnyEntity,\n Fields extends Schema.Struct.Fields,\n Members,\n R = never,\n >(\n name: Name,\n config: {\n readonly form: EditableForm<FormName, FormModel, FormMessage, Value, E, Fields, Members, R>\n readonly mutation: MutationDescriptor<string, Value, any>\n },\n ) => {\n type Model = EditorModel<FormModel>\n const { form, mutation } = config\n\n const closed: Model = {\n form: form.initial,\n mode: 'closed',\n target: null,\n filled: false,\n requestId: null,\n }\n const Model = Schema.Struct({\n form: form.bundle.Model,\n mode: Schema.Literals(['closed', 'new', 'edit']),\n target: Schema.NullOr(Schema.String),\n filled: Schema.Boolean,\n requestId: Schema.NullOr(Schema.String),\n }) as unknown as Schema.Codec<Model, unknown>\n\n // The editor's Messages are the form's own: it adds state around the form,\n // not a second vocabulary.\n const bundle = Bundle.make(name, {\n Model,\n Message: form.bundle.Message,\n init: () => ({ model: closed }),\n update: (model: Model, message: FormMessage) => {\n const next = form.bundle.update(model.form, message, undefined)\n // An edit after a save starts a new round; the last save no longer describes the form.\n const requestId = Predicate.isTagged(message, 'Changed') ? null : model.requestId\n // The form's Commands are the editor's: a check the form started has to run.\n const edited = {\n model: { ...model, form: next.model, requestId },\n ...(next.commands === undefined ? {} : { commands: next.commands }),\n }\n return next.outMessage === undefined ? edited : { ...edited, outMessage: next.outMessage }\n },\n helpers: {\n /** Edits `id`: the form is emptied, and filled once the current values arrive. */\n open: (_: Model, id: IdOf<E>) => ({\n model: { ...closed, mode: 'edit' as const, target: id },\n }),\n /** A blank form for a new one: nothing to load. */\n blank: () => ({ model: { ...closed, mode: 'new' as const, filled: true } }),\n close: () => ({ model: closed }),\n },\n })\n\n const current = Entity.selectFor(form.input)\n\n return {\n bundle,\n /** What the editor loads before it shows the form: every member the form writes. */\n selection: current,\n\n /**\n * The editor where it lives: `model` is its slice of the parent Model (the\n * field it was declared at), and `data` the Remote domain that registers the\n * mutation and the Entity.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, Model>\n }) => {\n const { data, model: slice } = where\n const loaded = (root: Root): RemoteData<any> | undefined => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.get(current, target).read(root)\n : undefined\n }\n\n /**\n * Shows the loaded value once, the first time it is there. Later refreshes\n * leave the drafts alone. A value whose latest refresh failed is still the\n * last one known good, so it fills the form too: `status` goes on saying\n * `Editing`, and the failure is the read's to show.\n */\n const sync: Update.Step<Root, never, never> = root => {\n const editor = slice.get(root)\n const read = loaded(root)\n if (editor.filled || read === undefined) return { model: root }\n const value =\n read._tag === 'Ready' || read._tag === 'Refreshing'\n ? read.value\n : read._tag === 'Failed'\n ? read.previous\n : undefined\n if (value === undefined) return { model: root }\n const values = Entity.valuesFor(form.input, value) as Partial<Value>\n return {\n model: slice.set(root, {\n ...editor,\n form: form.fill(editor.form, values).model,\n filled: true,\n }),\n }\n }\n\n const saveOf = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n\n return {\n /** For the placement: a valid submit becomes the mutation, and its request is remembered. */\n onOut:\n (submitted: Submitted<Value>): Step<Root> =>\n root => {\n const started = data.mutate(root, mutation, submitted.value)\n const editor = slice.get(started.model)\n return {\n model: slice.set(started.model, { ...editor, requestId: started.requestId }),\n commands: [started.command],\n }\n },\n\n /**\n * For `Data.subscriptions`: while an id is being edited, what it loads is\n * a requirement like any Surface's, so Remote fetches and retains it.\n */\n active: data.active(name, root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? Option.some(data.get(current, target))\n : Option.none()\n }),\n\n sync,\n\n /**\n * Wraps the application's `update` so `sync` runs after every Message:\n * the value may arrive with any of them.\n */\n after:\n <Message, Result extends { readonly model: Root }>(\n update: (model: Root, message: Message) => Result,\n ) =>\n (model: Root, message: Message): Result => {\n const next = update(model, message)\n return { ...next, model: sync(next.model).model }\n },\n\n /**\n * Asks for the value being edited again: `Data.refresh`, for a retry\n * button while `status` is `LoadFailed`. A failed read is not retried on\n * its own. Unchanged while nothing is open for editing.\n */\n refresh: (root: Root): Root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.refresh(root, data.get(current, target))\n : root\n },\n\n /** Why the last save failed, while `status` is `SaveFailed`. */\n saveError: (root: Root): RemoteError | undefined => {\n const save = saveOf(root)\n return save._tag === 'Failed' ? save.error : undefined\n },\n\n /** The id being edited, typed as the Entity's own; `null` for a new one or a closed editor. */\n target: (root: Root): IdOf<E> | null => slice.get(root).target as IdOf<E> | null,\n status: (root: Root): EditorStatus => {\n const editor = slice.get(root)\n if (editor.mode === 'closed') return 'Closed'\n // Gone outranks everything: a save that landed describes a thing that no longer exists.\n const read = loaded(root)\n if (read?._tag === 'NotFound') return 'NotFound'\n const save = saveOf(root)\n switch (save._tag) {\n case 'Pending':\n return 'Saving'\n case 'Failed':\n return 'SaveFailed'\n case 'Applied':\n return 'Saved'\n case 'Unknown':\n if (editor.filled) return 'Editing'\n return read?._tag === 'Failed' ? 'LoadFailed' : 'Loading'\n }\n },\n }\n },\n }\n },\n}\n\nexport { Display, type DisplayColumn, type DisplayKind, type DisplayWords } from './display.js'\nexport { Sort, type SortState, type SortedColumn } from './sort.js'\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/display.ts","../src/sort.ts","../src/index.ts"],"sourcesContent":["/**\n * Displays: how one selected member shows in a list or a detail, described\n * without a renderer. It is `foldkit-form`'s `Input` from the reading side: the\n * member says what it is, a Display says how it reads, and which DOM draws it is\n * the application's.\n */\nimport { Schema, SchemaAST } from 'effect'\nimport { SchemaShape, type AnyEntity, type EntityMember } from 'foldkit-entity'\nimport { Metadata } from 'foldkit-metadata'\n\n/**\n * How one selected member shows: the primitive every kind of Display is a value\n * of. `Display.text()` and `Display.flag()` are values of it, made by\n * `Display.kind`, which is what an application calls for a badge or a relative\n * date. The kinds here are a collection, not a special case.\n */\nexport interface Display<Data = unknown> {\n /** The name a renderer is found by. */\n readonly kind: string\n /** Whether a view draws it. A member read and not shown, usually the id, is `false`. */\n readonly shown: boolean\n /** What this kind needs: a format, a relation's target. */\n readonly data: Data\n /**\n * The value as text: what a cell says when nothing draws it specially. It is\n * never given `null` or `undefined`; `Display.show` says `nothing` for those.\n */\n readonly text: (value: unknown, words: DisplayWords) => string\n}\n\n/** A kind of Display: how its values are made, and how one is told from another. */\nexport interface DisplayKind<Data> {\n readonly kind: string\n readonly of: (data: Data) => Display<Data>\n readonly is: (display: Display) => display is Display<Data>\n}\n\n/** A selected member with its words and its Display. A list's column, a detail's line. */\nexport interface DisplayColumn<Key extends string = string> {\n readonly key: Key\n readonly label: string\n readonly member: EntityMember\n readonly display: Display\n}\n\n/** The words a Display needs that are not the value's own. */\nexport interface DisplayWords {\n /** Default `yes`. */\n readonly yes?: string\n /** Default `no`. */\n readonly no?: string\n /** What stands for nothing: `null`, `undefined`, an empty list. Default the empty string. */\n readonly nothing?: string\n /** Between the items of a list, and between the members of a nested value. Default `, `. */\n readonly separator?: string\n}\n\nconst key = Metadata.key<Display>('foldkit-crud/display', {\n // The last Display attached wins: a later annotation refines an earlier one.\n merge: displays => displays.slice(-1),\n summarize: display => display.kind,\n})\n\nconst fromSchema = (schema: Schema.Top): Display => {\n const members = SchemaShape.present(Schema.toType(schema).ast)\n const [only] = members\n if (members.length === 1 && only !== undefined) {\n if (SchemaAST.isNumber(only)) return Number_.of({})\n if (SchemaAST.isBoolean(only)) return Flag.of(nothing)\n }\n return Text.of({})\n}\n\n/** A kind of Display. The kinds below are made with it, and so is an application's. */\nconst kind = <Data = Record<string, never>>(\n name: string,\n spec: {\n readonly shown?: boolean\n readonly text: (data: Data, value: unknown, words: DisplayWords) => string\n },\n): DisplayKind<Data> => ({\n kind: name,\n of: data =>\n Object.freeze({\n kind: name,\n shown: spec.shown ?? true,\n data,\n text: (value: unknown, words: DisplayWords) => spec.text(data, value, words),\n }),\n is: (display): display is Display<Data> => display.kind === name,\n})\n\nconst show = (display: Display, value: unknown, words: DisplayWords = {}): string =>\n value === null || value === undefined ? (words.nothing ?? '') : display.text(value, words)\n\nconst Text = kind<{ readonly format?: ((value: unknown) => string) | undefined }>('Text', {\n text: (data, value) => (data.format === undefined ? String(value) : data.format(value)),\n})\nconst Number_ = kind<{ readonly format?: ((value: number) => string) | undefined }>('Number', {\n text: (data, value) =>\n typeof value === 'number' && data.format !== undefined ? data.format(value) : String(value),\n})\nconst Flag = kind('Flag', {\n text: (_, value, words) => (value === true ? (words.yes ?? 'yes') : (words.no ?? 'no')),\n})\nconst Hidden = kind('Hidden', { shown: false, text: () => '' })\n/** A relation read as refs: which one, with nothing to say about it but its id. */\nconst Ref = kind<{ readonly target: AnyEntity; readonly many: boolean }>('Ref', {\n text: (_, value, words) => {\n const ids = (Array.isArray(value) ? value : [value]).map(ref =>\n typeof ref === 'object' && ref !== null && 'id' in ref ? String(ref.id) : String(ref),\n )\n return ids.length === 0 ? (words.nothing ?? '') : ids.join(words.separator ?? ', ')\n },\n})\n/**\n * A relation read through a Selection: the target's own members, each with a\n * Display. `shape` is how many there are: one, a list, or a page of a list.\n */\nconst Nested = kind<{\n readonly target: AnyEntity\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n}>('Nested', {\n text: (data, value, words) => {\n const one = (item: unknown): string =>\n data.columns\n .filter(column => column.display.shown)\n .map(column =>\n show(\n column.display,\n (item as Readonly<Record<string, unknown>> | null)?.[column.key],\n words,\n ),\n )\n .filter(text => text !== '')\n .join(' ')\n const items =\n data.shape === 'one'\n ? [value]\n : data.shape === 'page'\n ? ((value as { readonly items?: ReadonlyArray<unknown> }).items ?? [])\n : (value as ReadonlyArray<unknown>)\n return items.length === 0 ? (words.nothing ?? '') : items.map(one).join(words.separator ?? ', ')\n },\n})\n\nconst nothing: Record<string, never> = Object.freeze({})\n\nexport const Display = {\n /**\n * A kind of Display. `Display.kind<{ tone: string }>('Badge', { text: (_, value) => String(value) })`\n * gives `.of(data)` to make one and `.is(display)` to tell one, exactly as the\n * kinds below were made. `text` is the floor; a view draws it specially once\n * it is given a renderer for `Badge`.\n */\n kind,\n\n // The kinds this package resolves to. A view finds its renderer by `kind`.\n Text,\n Number: Number_,\n Flag,\n Hidden,\n Ref,\n Nested,\n\n text: (format?: (value: unknown) => string): Display => Text.of({ format }),\n number: (format?: (value: number) => string): Display => Number_.of({ format }),\n flag: (): Display => Flag.of(nothing),\n /** Read, so a row can be opened or keyed by it, and not shown: usually the id. */\n hidden: (): Display => Hidden.of(nothing),\n\n /**\n * Entity metadata: how a member shows wherever it is listed, e.g.\n * `Entity.annotateMembers({ price: Display.of(Display.number(cents => ...)) })`.\n */\n of: (display: Display) => key.of(display),\n\n /**\n * The Display of one selected member, from the most to the least explicit\n * source: the member's `Display.of` metadata, how the relation was selected,\n * then the shape of its schema. `nested` is the columns of a nested Selection,\n * with `shape` saying how many the relation yields.\n */\n resolve: (\n member: EntityMember,\n nested?: {\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n },\n ): Display => {\n const [explicit] = key.get(member.metadata)\n if (explicit !== undefined) return explicit\n if (member._tag !== 'Relation') return fromSchema(member.schema as Schema.Top)\n const target = member.target()\n return nested === undefined\n ? Ref.of({ target, many: member.cardinality === 'many' })\n : Nested.of({ target, shape: nested.shape, columns: nested.columns })\n },\n\n /**\n * A value as the text its Display calls for: what a cell says when nothing\n * draws it specially. A view that wants a link or a badge reads the Display\n * and the value itself; this is the floor every view can stand on.\n */\n show,\n}\n","/**\n * Sorting a list, written once. How a list is sorted is the application's state\n * and its query's input, so this holds none of it: it is the schema of that\n * state, what a click on a header makes of it, and the shape a drawn table\n * takes. The server reads the same state and decides which columns it means.\n */\nimport { Schema } from 'effect'\n\n/** How a list is sorted: by which of its orders, which way; `null` for the server's own order. */\nexport type SortState<Column extends string> = {\n readonly by: Column\n readonly direction: 'asc' | 'desc'\n} | null\n\n/** How one column is sorted now, and the Message a click on its header sends. */\nexport interface SortedColumn<Message> {\n readonly direction?: 'asc' | 'desc' | undefined\n readonly message: Message\n}\n\nexport const Sort = {\n /**\n * The orders a list offers, by name. A name is an order the server knows, not\n * a column: which columns may sort, and what `title` means in SQL, stay the\n * server's to decide.\n */\n make: <const Columns extends readonly [string, ...string[]]>(columns: Columns) => {\n type Column = Columns[number]\n type State = SortState<Column>\n /** A click on a header: ascending, then descending, then the server's own order again. */\n const toggle = (current: State, column: Column): State =>\n current === null || current.by !== column\n ? { by: column, direction: 'asc' }\n : current.direction === 'asc'\n ? { by: column, direction: 'desc' }\n : null\n return {\n columns,\n /** For the Model field, and for the query's input. */\n Schema: Schema.NullOr(\n Schema.Struct({\n by: Schema.Literals(columns),\n direction: Schema.Literals(['asc', 'desc']),\n }),\n ) as unknown as Schema.Codec<State, State>,\n /** The server's own order. */\n none: null as State,\n toggle,\n /**\n * What a drawn table takes as `sort`: every order with how it is sorted now\n * and the Message a click sends, made from the state the click leads to and\n * the column clicked. A click meets the state last drawn, so two clicks\n * inside one frame both send the same `next`; send the column instead\n * (`(_, column) => Sorted({ column })`) and `toggle` it in `update`, from\n * the Model as it is then.\n */\n inputs: <Message>(\n current: State,\n message: (next: State, column: Column) => 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), 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 { Match, Option, Schema } from 'effect'\nimport { Bundle } from 'foldkit-bundle'\nimport {\n Entity,\n SelectionPageTypeId,\n Words,\n type AnyEntity,\n type EntityInput,\n type EntityMember,\n type InputMember,\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 {\n Refusal,\n RemoteData,\n type MutationDescriptor,\n type Page,\n type QueryDescriptor,\n type MutationStatus,\n type RemoteClient,\n type RemoteError,\n type 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 readonly authoredChanged: (before: FormModel, after: FormModel) => boolean\n readonly controls: ReadonlyArray<FormControl>\n readonly Message: {\n // Method syntax: the form's own takes only its keys, which the editor checks first.\n Refused(fields: { readonly key: string; readonly error: string }): FormMessage\n }\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 */\n/**\n * Which members of a row its reader may not set, read from members loaded\n * beside the form's: what `Crud.editor`'s `locks` takes. `foldkit-guard`'s\n * `May.locks(Todo)` is one, from the server's `may`.\n */\nexport interface Locks {\n /** The members to load beside the form's, by key: `{ may: true }`. */\n readonly select: Readonly<Record<string, true>>\n /** Whether `member` of the loaded row may not be set by its reader. */\n readonly locked: (row: Readonly<Record<string, unknown>>, member: string) => boolean\n}\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 /** Whether the author changed the form since its last save was sent. */\n readonly edited: boolean\n /** What the last save sent, for the form to take once it lands. */\n readonly sent: Option.Option<Readonly<Record<string, unknown>>>\n /** The save whose answer the form already took, so it is taken once. */\n readonly settledFor: Option.Option<string>\n /** The save whose refusal of a field the form already shows, so it is shown once. */\n readonly refusedFor: Option.Option<string>\n /**\n * The input values the row held as the form knows it: filled from it, and\n * moved on by each save of this editor's. A save compares to it, to send\n * only what changed.\n */\n readonly filledWith: Option.Option<Readonly<Record<string, unknown>>>\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 /** The server refused the save with `Refusal.conflict`: the row moved on since it was read. */\n | 'Conflict'\n /**\n * Someone else saved the row since it was read: its revision moved past the\n * one the form holds. The draft is kept; a save of it would be a conflict.\n */\n | 'Moved'\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 options?: { readonly keys?: ReadonlyArray<string> | undefined },\n ): {\n readonly model: Root\n readonly requestId: string\n readonly command: Command<RemoteMessage, never, RemoteClient>\n }\n get(selection: any, id: string): Projection<Root, RemoteData<any>>\n query(query: any, input: any, options: any): Projection<Root, RemoteData<Page<any>>>\n more(model: Root, projection: any): Option.Option<Root>\n refresh(model: Root, target: any): Root\n mutation(model: Root, requestId: string): MutationStatus\n refusal(model: Root, requestId: string, mutation: any): Option.Option<unknown>\n /** `Data.active`: a read of the domain as an active Surface of its application. */\n active(\n name: string,\n projectionOf: (model: Root) => Option.Option<Projection<Root, any>>,\n ): ActiveSurface<Root>\n}\n\ntype Step<Root> = Update.Step<Root, RemoteMessage, RemoteClient>\n\n/** One thing a relation picker offers. */\nexport interface Choice {\n readonly value: string\n readonly label: string\n}\n\n/**\n * One column of a list, or one line of a detail: a member the Selection reads,\n * with its words and its `Display`.\n */\nexport type ListColumn<Key extends string = string> = DisplayColumn<Key>\n\nconst columnLabel = (key: string, member: EntityMember): string =>\n Option.getOrElse(\n member._tag === 'Relation' ? Option.none() : Words.of(member.schema as Schema.Top).title,\n () => Form.labelOf(member) ?? key,\n )\n\ntype AnySelection = Selection<string, unknown, Schema.Constraint>\n\n/** The selected members in the Selection's order, each with its label and its Display. */\nconst columnsOf = <Members>(\n selection: Selection<string, Members, Schema.Constraint>,\n): ReadonlyArray<ListColumn<keyof Members & string>> => {\n const members: Readonly<Record<string, EntityMember>> = selection.entity.members\n const selected = selection.members as Readonly<\n Record<string, true | AnySelection | SelectionPage<string, Schema.Constraint>>\n >\n return Object.keys(selected).map(key => {\n const member = members[key]!\n const how = selected[key]!\n // A relation read through a Selection shows the target's own members.\n const nested =\n how === true || member._tag !== 'Relation'\n ? undefined\n : SelectionPageTypeId in how\n ? { shape: 'page' as const, columns: columnsOf(how.selection) }\n : {\n shape: member.cardinality === 'many' ? ('many' as const) : ('one' as const),\n columns: columnsOf(how),\n }\n return {\n key: key as keyof Members & string,\n label: columnLabel(key, member),\n member,\n display: Display.resolve(member, nested),\n }\n })\n}\n\nconst RemoverMessage = defineMessageUnion({ Confirmed: {}, Cancelled: {} })\ntype RemoverMessage = typeof RemoverMessage.Type\n\nconst idle: RemoverModel = { target: null, requestId: null }\n\nexport const Crud = {\n /**\n * One Entity read through a Selection, for a page that shows it. Like a list\n * it holds no state: the value is Remote's and the id is the application's.\n */\n detail: <const Name extends string, EntityName extends string, Members, Row>(\n name: Name,\n config: {\n readonly selection: Selection<EntityName, Members, Schema.Constraint & { readonly Type: Row }>\n },\n ) => ({\n name,\n /** The selected members in the Selection's order, each with its label. */\n fields: columnsOf(config.selection),\n /** Type-only: the value shown, for a view to be typed by. Never read. */\n Value: undefined as unknown as Row,\n\n /** The detail where it lives: `id` is the one shown, none while none is. */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly id: (root: Root) => Option.Option<string>\n }) => {\n const projectionOf = (root: Root) =>\n Option.map(where.id(root), id => where.data.get(config.selection, id))\n return {\n /** For `Data.subscriptions`: the value is fetched and retained while an id is shown. */\n active: where.data.active(name, projectionOf),\n value: (root: Root): RemoteData<Row> =>\n Option.match(projectionOf(root), {\n onNone: () => ({ _tag: 'Initial' }),\n onSome: projection => projection.read(root),\n }),\n /**\n * Asks for the value again: `Data.refresh` over this detail, for a retry\n * button. A failed read is not retried on its own. Unchanged while no id\n * is shown.\n */\n refresh: (root: Root): Root =>\n Option.match(projectionOf(root), {\n onNone: () => root,\n onSome: projection => where.data.refresh(root, projection),\n }),\n }\n },\n }),\n\n /**\n * Deleting through one mutation, with a yes in between: asked, confirmed,\n * deleted. What the mutation's input is for an id is `input`'s to say, and the\n * id it names there (`(id: PostId) => ...`) is the id `ask` takes.\n */\n remover: <\n const Name extends string,\n Input,\n Id extends string = string,\n const Key extends keyof Input = never,\n >(\n name: Name,\n config:\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n readonly input: (id: Id) => Input\n }\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n /**\n * The key of the mutation's input that holds the id, when that is all the\n * input is: `id: 'id'` for `{ id }`. The id's type is that key's.\n */\n readonly id: Key & (Input extends Readonly<Record<Key, string>> ? Key : never)\n },\n ) => {\n type AskedId = [Key] extends [never] ? Id : Input[Key] & string\n const inputFor = (id: string): Input =>\n 'input' in config ? config.input(id as Id) : ({ [config.id]: id } as Input)\n const bundle = Bundle.make(name, {\n Model: Schema.Struct({\n target: Schema.NullOr(Schema.String),\n requestId: Schema.NullOr(Schema.String),\n }) as unknown as Schema.Codec<RemoverModel, unknown>,\n Message: RemoverMessage,\n init: () => ({ model: idle }),\n update: (model: RemoverModel, message: RemoverMessage) =>\n message._tag === 'Cancelled' || model.target === null\n ? { model: idle }\n : { model, outMessage: { _tag: 'Confirmed', id: model.target } as ConfirmedRemoval },\n helpers: {\n /** Asks whether to delete `id`. Nothing is deleted until `Confirmed`. */\n ask: (_: RemoverModel, id: AskedId) => ({ model: { target: id, requestId: null } }),\n dismiss: () => ({ model: idle }),\n },\n })\n\n return {\n bundle,\n /** The remover's Messages, for the yes and the no of a confirmation. */\n Message: RemoverMessage,\n\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, RemoverModel>\n }) => {\n const { data, model: slice } = where\n const outcome = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n return {\n /** For the placement: a yes becomes the mutation, and its request is remembered. */\n onOut:\n (confirmed: ConfirmedRemoval): Step<Root> =>\n root => {\n const started = data.mutate(root, config.mutation, inputFor(confirmed.id))\n return {\n model: slice.set(started.model, {\n target: confirmed.id,\n requestId: started.requestId,\n }),\n commands: [started.command],\n }\n },\n\n /** The id being asked about or deleted, for the words of a confirmation. */\n target: (root: Root): AskedId | null => slice.get(root).target as AskedId | null,\n\n status: (root: Root): RemoverStatus => {\n const { target } = slice.get(root)\n const deleting = outcome(root)\n return deleting._tag === 'Pending'\n ? 'Deleting'\n : deleting._tag === 'Applied'\n ? 'Deleted'\n : deleting._tag === 'Failed'\n ? 'DeleteFailed'\n : target === null\n ? 'Idle'\n : 'Confirming'\n },\n\n /** Why the last delete failed, while `status` is `DeleteFailed`. */\n error: (root: Root): RemoteError | undefined => {\n const deleting = outcome(root)\n return deleting._tag === 'Failed' ? deleting.error : undefined\n },\n }\n },\n }\n },\n\n /**\n * A list over one query: which rows to show is the query's, what to show of\n * each is the Selection's. The pages live in Remote; the list holds no state.\n */\n list: <\n const Name extends string,\n EntityName extends string,\n Members,\n S extends Schema.Constraint,\n Input,\n >(\n name: Name,\n config: {\n readonly query: QueryDescriptor<string, Input, any>\n // The row is the Selection's own value, so it is typed with or without a `choice`.\n readonly selection: Selection<EntityName, Members, S>\n /** How many rows a page holds. Default 25. */\n readonly pageSize?: number\n /**\n * How a row reads as a choice in a relation picker: its id, and the words\n * that identify it to a person. Give it to a list that feeds pickers.\n */\n readonly choice?: {\n readonly value: (row: S['Type']) => string\n readonly label: (row: S['Type']) => string\n }\n },\n ) => {\n const { query, selection, pageSize = 25, choice } = config\n const columns = columnsOf(selection)\n\n return {\n name,\n /** The selected members in the Selection's order, each with its label. */\n columns,\n /** Type-only: a row, for a view to be typed by. Never read. */\n Row: undefined as unknown as S['Type'],\n\n /**\n * The list where it lives. `input` is the query's input as the Model has\n * it (filters, a search term), or `undefined` while the list is not shown.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n /** The list's input from the Model; none while the list is not shown. */\n readonly input: (root: Root) => Option.Option<Input>\n }) => {\n const { data, input } = where\n const projectionOf = (root: Root) =>\n Option.map(input(root), value =>\n data.query(query, value, { select: selection, first: pageSize }),\n )\n const page = (root: Root): RemoteData<Page<S['Type']>> =>\n Option.match(projectionOf(root), {\n onNone: () => ({ _tag: 'Initial' }),\n onSome: projection => projection.read(root),\n })\n\n const active = data.active(name, projectionOf)\n\n return {\n /** For `Data.subscriptions`: the page and its rows are fetched and retained while shown. */\n active,\n\n page,\n\n /**\n * \"Load more\": the Model with the list showing one page more, for the\n * read entry to fetch; none when it shows every row there is.\n */\n more: (root: Root): Option.Option<Root> =>\n Option.flatMap(projectionOf(root), projection => data.more(root, projection)),\n\n /**\n * Asks for the list again: `Data.refresh` over its page and rows, for a\n * retry button. A failed read is not retried on its own. Unchanged while\n * the list is not shown.\n */\n refresh: (root: Root): Root =>\n Option.match(projectionOf(root), {\n onNone: () => root,\n onSome: projection => data.refresh(root, projection),\n }),\n\n name,\n /** Whose requirement the list is, for anything that requires beside it. */\n owner: active.owner,\n /**\n * One of the list's Entity through the list's Selection, by id, whether or\n * not the query finds it now: what a picker reads to name what is chosen.\n */\n row: (id: string): Projection<Root, RemoteData<any>> => data.get(selection, id),\n /** The Entity the rows are of, which is how a picker finds the list for its target. */\n entity: selection.entity as AnyEntity,\n /** Whether the list was given a `choice`, so its rows can be a picker's choices. */\n offersChoices: choice !== undefined,\n\n /**\n * The loaded rows as a picker's choices, read through `choice`. Listing a\n * relation's target is this query, which the application chose and the\n * server authorizes; nothing is read because a relation exists.\n */\n choices: (root: Root): ReadonlyArray<Choice> => {\n if (choice === undefined)\n throw new Error(`Crud list \"${name}\": give it a \"choice\" to use its rows in a picker`)\n const read = page(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? read.value.items.map(row => ({\n value: choice.value(row),\n label: choice.label(row),\n }))\n : []\n },\n\n /** That row as a choice, once it is read. */\n choiceOf: (root: Root, id: string): Choice | undefined => {\n if (choice === undefined) return undefined\n const read = data.get(selection, id).read(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? { value: choice.value(read.value), label: choice.label(read.value) }\n : undefined\n },\n }\n },\n }\n },\n\n /**\n * A form's Submodel view as the view of the editor that wraps it, for\n * `Bundle.withView`: the editor's Messages are the form's, and its Model holds\n * the form's under `form`.\n *\n * ```ts\n * Editor.bundle.pipe(Bundle.withView(Crud.editorView(FormView.submodel(form, view))))\n * ```\n */\n editorView: <FormModel, Message, ViewInputs>(\n view: Submodel.View<FormModel, Message, ViewInputs>,\n ): Submodel.View<EditorModel<FormModel>, Message, ViewInputs> =>\n Submodel.defineView<EditorModel<FormModel>, Message, ViewInputs>(((\n model: EditorModel<FormModel>,\n inputs: ViewInputs,\n h: HtmlBuilder<Message>,\n ): Html =>\n (view as (model: FormModel, inputs: ViewInputs, h: HtmlBuilder<Message>) => Html)(\n model.form,\n inputs,\n h,\n )) as never),\n\n /**\n * The choices of every relation picker in a form, from the lists of their\n * targets: `foldkit-mixins-form`'s `options`, keyed by the form's keys. A\n * picker whose target no list here is over is a wiring mistake, reported now.\n *\n * With `chosen`, the form's Model as the page has it, what a picker already\n * holds stays among its choices when the list no longer finds it (a search\n * narrowed it, or it is on a later page), and `active` requires those rows so\n * their words are there. Give `chosen` to any form whose pickers search.\n */\n options: <Key extends string, Root, FormModel = never>(\n form: {\n readonly controls: ReadonlyArray<FormControl<Key>>\n readonly field?: (model: FormModel, key: never) => { readonly value: unknown }\n },\n lists: ReadonlyArray<{\n readonly name: string\n readonly entity: AnyEntity\n readonly offersChoices: boolean\n readonly owner: object\n readonly choices: (root: Root) => ReadonlyArray<Choice>\n readonly row: (id: string) => Projection<Root, RemoteData<any>>\n readonly choiceOf: (root: Root, id: string) => Choice | undefined\n }>,\n options: { readonly chosen?: (root: Root) => FormModel | undefined } = {},\n ) => {\n const pickers = form.controls.flatMap(({ key, control }) => {\n if (!Input.RelationOne.is(control) && !Input.RelationMany.is(control)) return []\n const { target } = control.data\n const list = lists.find(candidate => Entity.same(candidate.entity, target))\n if (list === undefined)\n throw new Error(\n `Crud.options: \"${key}\" picks a ${target.name}, and no list given is over ${target.name}`,\n )\n if (!list.offersChoices)\n throw new Error(\n `Crud.options: \"${key}\" would pick from list \"${list.name}\", which has no \"choice\"`,\n )\n return [[key, list] as const]\n })\n\n /** The ids each picker holds now: the draft of a `one`, or of a `many`. */\n const held = (root: Root): ReadonlyArray<readonly [Key, (typeof lists)[number], string]> => {\n const model = options.chosen?.(root)\n const field = form.field as\n ((model: FormModel, key: string) => { readonly value: unknown }) | undefined\n if (model === undefined || field === undefined) return []\n return pickers.flatMap(([key, list]) => {\n const draft = field(model, key).value\n const ids = Array.isArray(draft) ? draft : typeof draft === 'string' ? [draft] : []\n return ids\n .filter((id): id is string => typeof id === 'string' && id !== '')\n .map(id => [key, list, id] as const)\n })\n }\n\n const choices = (root: Root): { readonly [K in Key]?: ReadonlyArray<Choice> } => {\n const chosen = held(root)\n return Object.fromEntries(\n pickers.map(([key, list]) => {\n const listed = list.choices(root)\n const missing = chosen\n .filter(([heldBy, , id]) => heldBy === key && !listed.some(item => item.value === id))\n .flatMap(([, , id]) => list.choiceOf(root, id) ?? [])\n // What is chosen leads, so it is in reach whatever the list shows.\n return [key, [...missing, ...listed]]\n }),\n ) as unknown as { readonly [K in Key]?: ReadonlyArray<Choice> }\n }\n\n const [first] = lists\n if (first === undefined)\n throw new Error(\n 'Crud.options: give it the lists its pickers pick from; with none, it has nothing to read',\n )\n return Object.assign(choices, {\n /** For `Data.subscriptions`: what the pickers hold is read, so it can be named. */\n active: {\n name: `${pickers.map(([key]) => key).join('+')} chosen`,\n owner: first.owner,\n projectionOf: (root: Root) => {\n const rows = held(root)\n return rows.length === 0\n ? Option.none()\n : Option.some(\n Projection.struct(\n Object.fromEntries(rows.map(([key, list, id]) => [`${key}:${id}`, list.row(id)])),\n ),\n )\n },\n messages: [],\n } satisfies ActiveSurface<Root>,\n })\n },\n\n /**\n * What the pieces on a page require of Remote, by the names given: a placed\n * editor, list, or detail, or the pickers from `Crud.options`. It is each one's\n * `active`, gathered, so handing the pieces over is enough and none is left out:\n *\n * ```ts\n * Data.wiring(Crud.actives({ posts: Posts, authors: Authors, editor: PostEditor, pickers }))\n * ```\n */\n actives: <const Pieces extends Readonly<Record<string, { readonly active: unknown }>>>(\n pieces: Pieces,\n ): { readonly [K in keyof Pieces]: Pieces[K]['active'] } =>\n Object.fromEntries(Object.entries(pieces).map(([name, piece]) => [name, piece.active])) as {\n readonly [K in keyof Pieces]: Pieces[K]['active']\n },\n\n /**\n * An editor for one form and the mutation its value feeds. The form's value\n * must be the mutation's input, which is what declaring the input once and\n * giving it to both guarantees.\n */\n editor: <\n const Name extends string,\n FormName extends string,\n FormModel,\n FormMessage extends { readonly _tag: string },\n Value,\n E extends AnyEntity,\n Fields extends Schema.Struct.Fields,\n Members,\n R = never,\n >(\n name: Name,\n config: {\n readonly form: EditableForm<FormName, FormModel, FormMessage, Value, E, Fields, Members, R>\n readonly mutation: MutationDescriptor<string, Value, any>\n /**\n * Which of the form's keys the row being edited does not let its reader\n * set, from members loaded beside the form's: `foldkit-guard`'s\n * `May.locks(Todo)`. Read with `locked`.\n */\n readonly locks?: Locks | undefined\n },\n ) => {\n type Model = EditorModel<FormModel>\n const { form, mutation, locks } = config\n // How each input key's value is compared, built once from its own schema.\n const sameValue: Readonly<Record<string, (left: unknown, right: unknown) => boolean>> =\n Object.fromEntries(\n Object.entries(form.input.schema.fields).map(([key, schema]) => [\n key,\n Schema.toEquivalence(schema as Schema.Codec<unknown, unknown>),\n ]),\n )\n // The input key holding the revision a declared write expects, when it expects one.\n const expectKey = mutation.write?.expect?.key\n // The keys a refusal can name: a nested key holds rows, not a draft to mark.\n const draftKeys = new Set(\n form.controls.filter(entry => !Input.Nested.is(entry.control)).map(entry => entry.key),\n )\n\n const closed: Model = {\n form: form.initial,\n mode: 'closed',\n target: null,\n filled: false,\n requestId: null,\n edited: false,\n sent: Option.none(),\n settledFor: Option.none(),\n refusedFor: Option.none(),\n filledWith: Option.none(),\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 edited: Schema.Boolean,\n sent: Schema.OptionFromNullOr(Schema.Record(Schema.String, Schema.Unknown)),\n settledFor: Schema.OptionFromNullOr(Schema.String),\n refusedFor: Schema.OptionFromNullOr(Schema.String),\n filledWith: Schema.OptionFromNullOr(Schema.Record(Schema.String, Schema.Unknown)),\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\n // describes the form, though its answer is still the form's to take.\n // Any authored change counts, not only `Changed`: a row added or a\n // Bundle-backed control's edit is one too.\n const edited = model.edited || form.authoredChanged(model.form, next.model)\n const unchanged = next.model === model.form && edited === model.edited\n // The form's Commands are the editor's: a check the form started has to run.\n const step = {\n model: unchanged ? model : { ...model, form: next.model, edited },\n ...(next.commands === undefined ? {} : { commands: next.commands }),\n }\n return next.outMessage === undefined ? step : { ...step, 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 written = Entity.selectFor(form.input)\n // What is loaded: what the form writes, and what `locks` reads beside it,\n // which the form never fills from.\n const current: typeof written =\n locks === undefined\n ? written\n : (Entity.select(form.input.entity, {\n ...(written.members as Readonly<Record<string, unknown>>),\n ...locks.select,\n } as never) as typeof written)\n /** The member each form key writes: a field's own key, or the relation it points. */\n const writes: ReadonlyArray<readonly [keyof Fields & string, string]> = Object.entries(\n form.input.members as Readonly<Record<string, InputMember | undefined>>,\n ).flatMap(([key, member]) =>\n member === undefined\n ? []\n : Match.valueTags(member, {\n Field: ({ key: written }) => [[key as keyof Fields & string, written] as const],\n RelationInput: ({ relation }) => [\n [key as keyof Fields & string, relation.key] as const,\n ],\n NestedInput: () => [],\n Unmapped: () => [],\n }),\n )\n\n return {\n bundle,\n /** What the editor loads before it shows the form: every member the form writes. */\n selection: current,\n\n /**\n * The editor where it lives: `model` is its slice of the parent Model (the\n * field it was declared at), and `data` the Remote domain that registers the\n * mutation and the Entity.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, Model>\n }) => {\n const { data, model: slice } = where\n const loaded = (root: Root): RemoteData<any> | undefined => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.get(current, target).read(root)\n : undefined\n }\n\n /**\n * Shows the loaded value once, the first time it is there. Later refreshes\n * leave the drafts alone. A value whose latest refresh failed is still the\n * last one known good, so it fills the form too: `status` goes on saying\n * `Editing`, and the failure is the read's to show.\n */\n /**\n * A save the server refused for one of the form's keys shows on that key,\n * once per save: `Refusal.field`'s reason, as text. Any other refusal is\n * the status's to say.\n */\n const refused = (root: Root): Root => {\n const editor = slice.get(root)\n const { requestId } = editor\n if (requestId === null || Option.contains(editor.refusedFor, requestId)) return root\n const field = Option.filter(refusalOf(root), Refusal.isField)\n if (Option.isNone(field) || !draftKeys.has(field.value.key)) return root\n const message = form.Message.Refused({\n key: field.value.key,\n error: String(field.value.reason),\n })\n return slice.set(root, {\n ...editor,\n form: form.bundle.update(editor.form, message, undefined).model,\n refusedFor: Option.some(requestId),\n })\n }\n\n /** The value loaded, or the last one known good when a refresh failed. */\n const known = (root: Root): Option.Option<any> => {\n const read = loaded(root)\n return read === undefined\n ? Option.none()\n : RemoteData.match(read, {\n Initial: () => Option.none(),\n Loading: () => Option.none(),\n Ready: Option.some,\n Refreshing: Option.some,\n Failed: (_, previous) => previous,\n NotFound: () => Option.none(),\n })\n }\n\n const filledOf = (root: Root): Root => {\n const editor = slice.get(root)\n if (editor.filled) return root\n const shown = known(root)\n if (Option.isNone(shown)) return root\n const value = shown.value\n const values = Entity.valuesFor(form.input, value) as Partial<Value>\n return slice.set(root, {\n ...editor,\n form: form.fill(editor.form, values).model,\n filled: true,\n filledWith: Option.some(values),\n })\n }\n\n /**\n * The input keys a save changes from what the form was filled with, by\n * each key's own schema: none filled (something new) is every key, so\n * none are named.\n */\n const changedKeys = (editor: Model, value: Value): ReadonlyArray<string> | undefined => {\n // A submitted value is the form's decoded struct, so it reads by key.\n const submitted = value as Readonly<Record<string, unknown>>\n return Option.match(editor.filledWith, {\n onNone: () => undefined,\n onSome: filled =>\n Object.keys(submitted).filter(\n key => !Object.hasOwn(filled, key) || !sameValue[key]!(filled[key], submitted[key]),\n ),\n })\n }\n\n /** The revision the row is at now, as the form's `expect` key holds it. */\n const currentRevision = (root: Root): Option.Option<unknown> => {\n const read = loaded(root)\n if (expectKey === undefined || read === undefined) return Option.none()\n if (read._tag !== 'Ready' && read._tag !== 'Refreshing') return Option.none()\n const values = Entity.valuesFor(form.input, read.value) as Readonly<\n Record<string, unknown>\n >\n return Object.hasOwn(values, expectKey) ? Option.some(values[expectKey]) : Option.none()\n }\n /** Whether the row is at another revision than the one the form was filled with. */\n const moved = (root: Root): boolean => {\n const { filledWith } = slice.get(root)\n return (\n expectKey !== undefined &&\n Option.isSome(filledWith) &&\n Option.exists(\n currentRevision(root),\n revision => !sameValue[expectKey]!(filledWith.value[expectKey], revision),\n )\n )\n }\n\n /**\n * Once this editor's save lands, the form takes its answer, once: what\n * it sent is now what the row holds as the form knows it, and the\n * revision is the one it moved the row to, or its next save would\n * conflict with itself. A revision moved after that is another's, and\n * reads as `Moved`. A create's form starts blank again, its values now\n * the row's; one the author went on typing in is a new one's draft.\n */\n const settled = (root: Root): Root => {\n const editor = slice.get(root)\n const { requestId } = editor\n if (requestId === null || Option.contains(editor.settledFor, requestId)) return root\n if (saveOf(root)._tag !== 'Applied') return root\n const settledFor = Option.some(requestId)\n if (editor.mode === 'new') {\n return slice.set(root, {\n ...editor,\n settledFor,\n ...(editor.edited ? {} : { form: form.initial }),\n })\n }\n const revision = expectKey === undefined ? Option.none() : currentRevision(root)\n const filledWith = Option.some({\n ...Option.getOrElse(editor.filledWith, () => ({})),\n ...Option.getOrElse(editor.sent, () => ({})),\n ...Option.match(revision, {\n onNone: () => ({}),\n onSome: value => ({ [expectKey!]: value }),\n }),\n })\n return slice.set(root, {\n ...editor,\n settledFor,\n filledWith,\n form: Option.match(revision, {\n onNone: () => editor.form,\n onSome: value =>\n form.fill(editor.form, { [expectKey!]: value } as Partial<Value>).model,\n }),\n })\n }\n\n const sync: Update.Step<Root, never, never> = root => ({\n model: settled(refused(filledOf(root))),\n })\n\n /** Why the server refused the last save, when it refused it as data. */\n const refusalOf = (root: Root): Option.Option<unknown> => {\n const { requestId } = slice.get(root)\n return requestId === null ? Option.none() : data.refusal(root, requestId, mutation)\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 // A declared write is told what changed, so it writes, and shows, only that.\n const started = data.mutate(\n root,\n mutation,\n submitted.value,\n mutation.write === undefined\n ? undefined\n : { keys: changedKeys(slice.get(root), submitted.value) },\n )\n const editor = slice.get(started.model)\n return {\n model: slice.set(started.model, {\n ...editor,\n requestId: started.requestId,\n edited: false,\n sent: Option.some(submitted.value as Readonly<Record<string, unknown>>),\n }),\n commands: [started.command],\n }\n },\n\n /**\n * For `Data.subscriptions`: while an id is being edited, what it loads is\n * a requirement like any Surface's, so Remote fetches and retains it.\n */\n active: data.active(name, root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? Option.some(data.get(current, target))\n : Option.none()\n }),\n\n sync,\n\n /**\n * Wraps the application's `update` so `sync` runs after every Message:\n * the value may arrive with any of them.\n */\n after:\n <Message, Result extends { readonly model: Root }>(\n update: (model: Root, message: Message) => Result,\n ) =>\n (model: Root, message: Message): Result => {\n const next = update(model, message)\n return { ...next, model: sync(next.model).model }\n },\n\n /**\n * Asks for the value being edited again: `Data.refresh`, for a retry\n * button while `status` is `LoadFailed`. A failed read is not retried on\n * its own. Unchanged while nothing is open for editing.\n */\n refresh: (root: Root): Root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.refresh(root, data.get(current, target))\n : root\n },\n\n /** Why the last save failed, while `status` is `SaveFailed`. */\n saveError: (root: Root): RemoteError | undefined => {\n const save = saveOf(root)\n return save._tag === 'Failed' ? save.error : undefined\n },\n\n /**\n * The form's keys the row being edited does not let its reader set, by\n * `locks`, for `foldkit-mixins-form`'s `locked`. None for something new,\n * whose create the server checks, and none until the row is loaded.\n */\n locked: (root: Root): ReadonlyArray<keyof Fields & string> =>\n locks === undefined\n ? []\n : Option.match(known(root), {\n onNone: () => [],\n onSome: row =>\n writes.flatMap(([key, member]) => (locks.locked(row, member) ? [key] : [])),\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 // Changed since the last save: that save no longer describes the form.\n if (editor.edited) return moved(root) ? 'Moved' : 'Editing'\n switch (save._tag) {\n case 'Pending':\n return 'Saving'\n case 'Failed':\n return Option.exists(refusalOf(root), Refusal.isConflict)\n ? 'Conflict'\n : 'SaveFailed'\n case 'Applied':\n // Saved, and since moved on by someone else.\n return moved(root) ? 'Moved' : 'Saved'\n case 'Unknown':\n if (editor.filled) return moved(root) ? 'Moved' : 'Editing'\n return read?._tag === 'Failed' ? 'LoadFailed' : 'Loading'\n }\n },\n }\n },\n }\n },\n}\n\nexport { Display, type DisplayColumn, type DisplayKind, type DisplayWords } from './display.js'\nexport { Sort, type SortState, type SortedColumn } from './sort.js'\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit-crud",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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,12 +40,12 @@
40
40
  "LICENSE"
41
41
  ],
42
42
  "dependencies": {
43
- "foldkit-bundle": "0.5.0",
44
- "foldkit-form": "0.4.1",
43
+ "foldkit-bundle": "0.6.0",
44
+ "foldkit-entity": "0.8.0",
45
+ "foldkit-form": "0.4.2",
45
46
  "foldkit-metadata": "0.1.0",
46
- "foldkit-remote": "0.12.0",
47
- "foldkit-entity": "0.7.0",
48
- "foldkit-surface": "0.7.0"
47
+ "foldkit-surface": "0.8.0",
48
+ "foldkit-remote": "0.14.0"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "effect": "^4.0.0",