foldkit-crud 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +299 -235
- package/dist/index.d.mts +10 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +23 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -10
package/README.md
CHANGED
|
@@ -1,51 +1,82 @@
|
|
|
1
1
|
# foldkit-crud
|
|
2
2
|
|
|
3
|
-
The screens that
|
|
4
|
-
application already has
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
[`foldkit-entity`](../entity/README.md) Entity they share. A **list** joins a
|
|
9
|
-
Remote query and an Entity Selection, a **detail** reads one Entity through a
|
|
10
|
-
Selection, and a **remover** deletes through a mutation with a yes in between.
|
|
3
|
+
The screens that list, show, edit and delete records, built from parts your
|
|
4
|
+
application already has: a [`foldkit-remote`](../remote/README.md) domain, the
|
|
5
|
+
[`foldkit-entity`](../entity/README.md) Entities it holds, and, for an editor, a
|
|
6
|
+
[`foldkit-form`](../form/README.md) form. It draws nothing and holds almost no
|
|
7
|
+
state. A list is one query and what to show of each row:
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
```ts
|
|
10
|
+
const Authors = Crud.list('Authors', {
|
|
11
|
+
query: AuthorsQuery,
|
|
12
|
+
selection: Entity.select(Blog.Author, { id: true, name: true }),
|
|
13
|
+
})
|
|
14
|
+
const AuthorList = Authors.at({ data: Data, input: model => ({ search: model.search }) })
|
|
15
|
+
|
|
16
|
+
AuthorList.page(model) // RemoteData of the page: its typed rows, hasNext, hasPrevious
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
It is for any such screen, a user editing their own post as much as a back
|
|
20
|
+
office. [`examples/entity`](../../examples/entity) draws a list, an editor and a
|
|
21
|
+
remover, and runs in a browser.
|
|
22
|
+
|
|
23
|
+
## Why you would use it
|
|
24
|
+
|
|
25
|
+
An edit screen looks like a form plus a request. By hand it is more: load the
|
|
26
|
+
record when the screen opens, and keep it loaded while it is shown; fill the
|
|
27
|
+
form once when the value arrives, and never again over what the user typed;
|
|
28
|
+
turn a valid submit into the mutation, and remember which request is this
|
|
29
|
+
screen's; show loading, saving, saved and failed from that request; notice when
|
|
30
|
+
the record was deleted while it was open. A list needs its pages loaded,
|
|
31
|
+
retained and extended, its columns labelled, and its rows offered to a picker.
|
|
32
|
+
|
|
33
|
+
`foldkit-crud` does that glue once. What makes it safe to use is what it
|
|
34
|
+
leaves alone: the rows and the save state stay in Remote's store, the drafts
|
|
35
|
+
stay in the form, and every status a piece reports is derived from them, so it
|
|
36
|
+
cannot disagree with them.
|
|
37
|
+
|
|
38
|
+
## The four pieces
|
|
39
|
+
|
|
40
|
+
| Piece | Joins | State of its own | Placed as |
|
|
41
|
+
| --- | --- | --- | --- |
|
|
42
|
+
| **list** | a Remote query and an Entity Selection | none: the pages are Remote's | a value; nothing to place |
|
|
43
|
+
| **detail** | an Entity Selection and an id the Model gives | none | a value |
|
|
44
|
+
| **editor** | a form, the mutation its value feeds, and their Entity | which id is open, whether it has been filled, which save is its own | a Bundle, like the form it wraps |
|
|
45
|
+
| **remover** | a delete mutation, with a yes in between | which id is being asked about, which delete is its own | a Bundle |
|
|
15
46
|
|
|
16
|
-
|
|
47
|
+
Nothing is generated from an Entity alone. An editor exists because you named
|
|
48
|
+
a form and a mutation, and a list because you named a query, which your server
|
|
49
|
+
authorizes.
|
|
17
50
|
|
|
18
|
-
|
|
19
|
-
things that each already own their part:
|
|
51
|
+
## What it owns, and what it does not
|
|
20
52
|
|
|
21
53
|
| Fact | Owner |
|
|
22
54
|
| --- | --- |
|
|
23
|
-
| The
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
| Which id is
|
|
27
|
-
|
|
|
55
|
+
| The records, and whether a save or delete is pending, applied or failed | Remote, in its store |
|
|
56
|
+
| What a record is: its members, labels and relations | the Entity |
|
|
57
|
+
| The drafts, and whether they are valid | the form |
|
|
58
|
+
| Which id is open or being asked about, and which request is this screen's | the editor or remover, in its slice of your Model |
|
|
59
|
+
| Which rows a list shows: search, sort, filters | your Model, as the query's input |
|
|
60
|
+
| What happens after a save or delete: navigate, close, a toast | your application, reading the piece's status |
|
|
61
|
+
| How it looks | you, or [`foldkit-mixins-crud`](../mixins-crud/README.md) and [`foldkit-mixins-form`](../mixins-form/README.md) |
|
|
28
62
|
|
|
29
|
-
|
|
30
|
-
form and a mutation; an Entity with neither has no editor.
|
|
31
|
-
|
|
32
|
-
## Mental model
|
|
63
|
+
## The mental model
|
|
33
64
|
|
|
34
65
|
```text
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
the user
|
|
42
|
-
|
|
43
|
-
v
|
|
44
|
-
onOut ──► Data.mutate ──► Remote settles it ──► status: Saving ─► Saved / SaveFailed
|
|
66
|
+
your Model says what is on screen
|
|
67
|
+
──▶ each piece's `active`: what it requires of Remote
|
|
68
|
+
──▶ Data.wiring ──▶ Remote fetches and retains it
|
|
69
|
+
|
|
70
|
+
Remote's store ──▶ each piece reads it: a page, a value, a status ──▶ your view
|
|
71
|
+
|
|
72
|
+
the user acts ──▶ a Message ──▶ a piece's helper, or the editor's onOut
|
|
73
|
+
──▶ Data.mutate ──▶ Remote settles it ──▶ the status moves
|
|
45
74
|
```
|
|
46
75
|
|
|
47
|
-
|
|
48
|
-
|
|
76
|
+
The rule to remember: **a piece loads only what its `active` requires, and only
|
|
77
|
+
if that `active` is wired.** A piece that is not wired never loads, and nothing
|
|
78
|
+
reports it. [Wiring a page](#wiring-a-page) gathers every piece's `active` so
|
|
79
|
+
none is left out.
|
|
49
80
|
|
|
50
81
|
## Install
|
|
51
82
|
|
|
@@ -53,29 +84,88 @@ state, and the loaded value, so it cannot disagree with them.
|
|
|
53
84
|
pnpm add effect foldkit foldkit-crud foldkit-bundle foldkit-entity foldkit-form foldkit-remote foldkit-surface
|
|
54
85
|
```
|
|
55
86
|
|
|
56
|
-
##
|
|
87
|
+
## A first list
|
|
57
88
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
89
|
+
A list is one query and one Selection: the query decides which rows, the
|
|
90
|
+
Selection what to show of each.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
const Authors = Crud.list('Authors', {
|
|
94
|
+
query: AuthorsQuery, // Query.make('Authors', { Input: { search }, Result: Query.connection(Blog.Author) })
|
|
95
|
+
selection: Entity.select(Blog.Author, { id: true, name: true }),
|
|
96
|
+
pageSize: 25,
|
|
97
|
+
// How a row reads as a choice, for a list that feeds relation pickers.
|
|
98
|
+
choice: { value: row => row.id, label: row => row.name },
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const AuthorList = Authors.at({
|
|
102
|
+
data: Data,
|
|
103
|
+
// The query's input as the Model has it; `undefined` while the list is not shown.
|
|
104
|
+
input: model => (model.search === null ? undefined : { search: model.search }),
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const subscriptions = Data.subscriptions({ authors: AuthorList.active })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
What each call does:
|
|
111
|
+
|
|
112
|
+
- **`Crud.list`** is a declaration: no request, no state. `Authors.columns` is
|
|
113
|
+
the selected members in the Selection's order, each with its `key`,
|
|
114
|
+
`member`, a `label` found the way a form finds one (the schema's `title`,
|
|
115
|
+
else `Form.label` metadata, else the key), and a `display`.
|
|
116
|
+
- **`Authors.at`** binds it to your Remote domain and your Model. It still does
|
|
117
|
+
no I/O.
|
|
118
|
+
- **`AuthorList.active`** is the requirement: while `input` gives a value, the
|
|
119
|
+
page and its rows are fetched and retained. Another input is another
|
|
120
|
+
connection, fetched on its own.
|
|
121
|
+
- **`AuthorList.page(model)`** reads Remote's store: a `RemoteData` of the page,
|
|
122
|
+
with `items` typed by the Selection, `hasNext` and `hasPrevious`.
|
|
123
|
+
- **`AuthorList.more(model)`** is the Command that loads the next page onto
|
|
124
|
+
this one, or `undefined` when there is none. Return it from `update`.
|
|
125
|
+
- **`AuthorList.refresh(model)`** is the Model with the list asked for again. A
|
|
126
|
+
failed read is not retried on its own, so this is what a retry button's
|
|
127
|
+
Message returns.
|
|
128
|
+
|
|
129
|
+
`AuthorList.row(id)` is a Projection of one row through the list's Selection,
|
|
130
|
+
whether or not the query finds it now, and `AuthorList.choiceOf(model, id)` is
|
|
131
|
+
that row as a choice once it is read. Relation pickers use both.
|
|
132
|
+
|
|
133
|
+
## Wiring a page
|
|
134
|
+
|
|
135
|
+
Every placed piece has an `active`. `Crud.actives` gathers them from the pieces
|
|
136
|
+
themselves, so a page cannot forget one:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
Data.wiring(Crud.actives({ posts: Posts, authors: Authors, editor: PostEditor, pickers }))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
It takes a placed editor, list or detail, and the pickers from `Crud.options`,
|
|
143
|
+
and gives `{ posts: Posts.active, ... }`, for `Data.wiring` in a
|
|
144
|
+
`foldkit-bundle` assembly, or for `Data.subscriptions`.
|
|
145
|
+
|
|
146
|
+
## An editor
|
|
147
|
+
|
|
148
|
+
An editor joins a form, the mutation its value feeds, and the Entity they
|
|
149
|
+
share. `EditPostForm` is a `Form.make` result and `EditPostMutation` a Remote
|
|
150
|
+
mutation over the same input struct, as in
|
|
151
|
+
[`examples/entity`](../../examples/entity); `Blog` is that application's Entity
|
|
152
|
+
domain.
|
|
62
153
|
|
|
63
154
|
```ts
|
|
64
|
-
import { Schema } from 'effect'
|
|
65
155
|
import { Crud } from 'foldkit-crud'
|
|
66
156
|
import { Bundle } from 'foldkit-bundle'
|
|
67
|
-
import { defineMessageUnion } from 'foldkit/message'
|
|
68
157
|
import { Remote, type RemoteClient } from 'foldkit-remote'
|
|
69
158
|
import { Surface } from 'foldkit-surface'
|
|
70
159
|
|
|
71
160
|
const Editor = Crud.editor('PostEditor', { form: EditPostForm, mutation: EditPostMutation })
|
|
72
161
|
|
|
73
162
|
// The editor is a Submodel of the page: a Model field and a Message variant.
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
163
|
+
const Base = Bundle.compose({ remote: Remote.Model }).pipe(
|
|
164
|
+
Bundle.withMessages(Remote.messages),
|
|
165
|
+
Bundle.withChild('editor', Editor.bundle),
|
|
166
|
+
)
|
|
77
167
|
|
|
78
|
-
const App = Surface.application(
|
|
168
|
+
const App = Surface.application(Base)
|
|
79
169
|
const Data = Remote.make({
|
|
80
170
|
model: App.model.remote,
|
|
81
171
|
entities: Object.values(Blog),
|
|
@@ -85,9 +175,13 @@ const Data = Remote.make({
|
|
|
85
175
|
// Where it lives: its slice of the Model, and the domain it saves through.
|
|
86
176
|
const PostEditor = Editor.at({ data: Data, model: App.model.editor })
|
|
87
177
|
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
178
|
+
// Its onOut needs the domain, so it is given once the domain exists.
|
|
179
|
+
const Page = Base.pipe(
|
|
180
|
+
Bundle.withServices<RemoteClient>(),
|
|
181
|
+
Bundle.configure('editor', { onOut: PostEditor.onOut }),
|
|
182
|
+
)
|
|
183
|
+
const Placed = Page.children.editor
|
|
184
|
+
const { placements } = Page
|
|
91
185
|
|
|
92
186
|
const update = PostEditor.after(
|
|
93
187
|
placements.update((model, message) =>
|
|
@@ -98,61 +192,48 @@ const update = PostEditor.after(
|
|
|
98
192
|
const subscriptions = Data.subscriptions({ editor: PostEditor.active })
|
|
99
193
|
```
|
|
100
194
|
|
|
101
|
-
-
|
|
102
|
-
the input struct once and giving it to both is
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
195
|
+
- **`Crud.editor`** checks, in the types, that the form's value is the
|
|
196
|
+
mutation's input. Declaring the input struct once and giving it to both is
|
|
197
|
+
what makes that true.
|
|
198
|
+
- **`Editor.bundle`** wraps the form's Bundle. Its Messages are the form's own,
|
|
199
|
+
so a view dispatches `EditPostForm.Message.Changed(...)` exactly as before.
|
|
200
|
+
- **`Bundle.compose`** states the page once, with the editor under `editor`.
|
|
201
|
+
The editor's `onOut` is made from the Remote domain, which is made from the
|
|
202
|
+
page's own Model, so it is given afterwards with `Bundle.configure`; until
|
|
203
|
+
then the page's `placements` are a type error that names the editor. See
|
|
204
|
+
[composing a parent](../bundle/README.md#composing-a-parent).
|
|
205
|
+
- **`Editor.at`** takes the editor's slice as a `ModelRef` (`App.model.editor`)
|
|
206
|
+
and the bound Remote domain, and returns what needs the parent:
|
|
107
207
|
- `onOut` for the placement: a valid submit becomes `Data.mutate`, and the
|
|
108
208
|
editor remembers the request id.
|
|
109
|
-
- `active
|
|
110
|
-
|
|
111
|
-
- `after(update)`: wraps `update` so
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
209
|
+
- `active`: while an id is open, what the form writes is fetched and
|
|
210
|
+
retained.
|
|
211
|
+
- `after(update)`: wraps `update` so the editor fills the form and
|
|
212
|
+
reconciles its status after every Message. `sync` is that Step on its own,
|
|
213
|
+
for an `update` composed another way.
|
|
214
|
+
- `status(model)`, `saveError(model)` while it is `SaveFailed`,
|
|
215
|
+
`refresh(model)` to ask for the value again while it is `LoadFailed`, and
|
|
216
|
+
`target(model)`, the id being edited (`null` for a new one, or when
|
|
217
|
+
closed).
|
|
218
|
+
- The save is a Command that needs `RemoteClient`, so the page names it with
|
|
219
|
+
`Bundle.withServices<RemoteClient>()`.
|
|
220
|
+
|
|
221
|
+
### What starts the work
|
|
119
222
|
|
|
120
223
|
| Call | What happens |
|
|
121
224
|
| --- | --- |
|
|
122
|
-
| `Crud.editor(...)` / `Editor.at(...)` |
|
|
123
|
-
| `Placed.helpers.open(id)` |
|
|
124
|
-
| `Data.subscriptions({ editor: PostEditor.active })` |
|
|
125
|
-
| `PostEditor.after(update)` |
|
|
126
|
-
|
|
|
127
|
-
|
|
128
|
-
Include the placement's own subscriptions as well if your form uses them.
|
|
129
|
-
Do not interpret `Submitted` as `Saved`: only the returned mutation outcome
|
|
130
|
-
settles the save. The status section below describes what the view should show.
|
|
131
|
-
|
|
132
|
-
### Drawing it
|
|
133
|
-
|
|
134
|
-
The editor wraps the form's Model, so it takes the form's view, lifted:
|
|
225
|
+
| `Crud.editor(...)` / `Editor.at(...)` | Builds the editor and its integration; no request runs |
|
|
226
|
+
| `Placed.helpers.open(id)` | Returns an Update Step; apply it in your `update` to open a record |
|
|
227
|
+
| `Data.subscriptions({ editor: PostEditor.active })` | Describes the loader; the runtime installs it and provides `RemoteClient` |
|
|
228
|
+
| `PostEditor.after(update)` | Fills the form once the value loads, and reconciles the status after each Message |
|
|
229
|
+
| A valid submit, through `onOut` | Returns the mutation Command; its result comes back through Remote |
|
|
135
230
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const Slot = Bundle.declare(
|
|
140
|
-
Editor.bundle.pipe(
|
|
141
|
-
Bundle.withView(Crud.editorView(FormView.submodel(EditPostForm, FormView.define(EditPostForm)))),
|
|
142
|
-
),
|
|
143
|
-
'editor',
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
// where the page draws it:
|
|
147
|
-
Placed.view(model, h, { options: pickers(model), words: { submit: 'Save' } })
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
`Crud.editorView` works on any Submodel view of the form that takes view
|
|
151
|
-
inputs; `foldkit-crud` itself stays headless.
|
|
231
|
+
Include the placement's own Subscriptions as well if your form has any. Do not
|
|
232
|
+
read `Submitted` as `Saved`: only the mutation's outcome settles the save.
|
|
152
233
|
|
|
153
234
|
### Opening it
|
|
154
235
|
|
|
155
|
-
`open`, `blank
|
|
236
|
+
`open`, `blank` and `close` are helpers of the placement, so they are Update
|
|
156
237
|
Steps to use from your own `update`:
|
|
157
238
|
|
|
158
239
|
```ts
|
|
@@ -165,75 +246,117 @@ One editor serves one form. A create form and an edit form usually differ (the
|
|
|
165
246
|
edit input has an `id`), so they are two editors; `blank` is for the one whose
|
|
166
247
|
form creates.
|
|
167
248
|
|
|
168
|
-
|
|
249
|
+
### Its status
|
|
169
250
|
|
|
170
251
|
| Status | When |
|
|
171
252
|
| --- | --- |
|
|
172
253
|
| `Closed` | nothing is open |
|
|
173
254
|
| `Loading` | an id is open and its current values have not arrived |
|
|
174
|
-
| `NotFound` | it is gone: the server answered without it, a mutation deleted it, or a live event did. This outranks a save that landed |
|
|
175
|
-
| `LoadFailed` | reading it failed, in the request or in decoding what came back. Once the form is filled it stays `Editing`, whatever a later read says |
|
|
176
255
|
| `Editing` | the form is showing, with no save in progress or just settled |
|
|
177
256
|
| `Saving` | this editor's mutation is pending |
|
|
178
257
|
| `Saved` | it was applied |
|
|
179
258
|
| `SaveFailed` | it failed; the drafts are kept, and `saveError(model)` says why |
|
|
259
|
+
| `LoadFailed` | reading it failed, with no earlier value to show; `refresh(model)` asks again |
|
|
260
|
+
| `NotFound` | it is gone: the server answered without it, a mutation deleted it, or a live event did. This outranks a save that landed |
|
|
180
261
|
|
|
181
|
-
An edit after a save returns to `Editing`: the last save no longer describes
|
|
182
|
-
is in the form.
|
|
262
|
+
An edit after a save returns to `Editing`: the last save no longer describes
|
|
263
|
+
what is in the form.
|
|
183
264
|
|
|
184
|
-
|
|
265
|
+
### How it treats the form
|
|
185
266
|
|
|
186
|
-
- **The form is filled once.** When the value is read again
|
|
187
|
-
update
|
|
188
|
-
Opening the same or another id empties the form and fills it
|
|
189
|
-
|
|
190
|
-
|
|
267
|
+
- **The form is filled once.** When the value is read again, by a refresh or a
|
|
268
|
+
live update, the drafts are left alone, so nothing the user typed is
|
|
269
|
+
overwritten. Opening the same or another id empties the form and fills it
|
|
270
|
+
again.
|
|
271
|
+
- **What is loaded is what the form writes**, with each relation as a ref. A
|
|
272
|
+
key the form marks `Entity.unmapped` is not loaded and starts empty.
|
|
191
273
|
- **An invalid submit starts no mutation.** The form emits nothing until every
|
|
192
274
|
key passes the input's schema and its checks. The form's Commands are the
|
|
193
275
|
editor's, so a check the form starts runs.
|
|
194
276
|
|
|
195
|
-
|
|
277
|
+
### Drawing it
|
|
196
278
|
|
|
197
|
-
|
|
198
|
-
Selection what to show of each. The pages live in Remote, so a list holds no
|
|
199
|
-
state of its own and is not a Bundle.
|
|
279
|
+
The editor wraps the form's Model, so it takes the form's view, lifted:
|
|
200
280
|
|
|
201
281
|
```ts
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
282
|
+
import { FormView } from 'foldkit-mixins-form'
|
|
283
|
+
|
|
284
|
+
const DrawnEditor = Editor.bundle.pipe(
|
|
285
|
+
Bundle.withView(Crud.editorView(FormView.submodel(EditPostForm, FormView.define(EditPostForm)))),
|
|
286
|
+
)
|
|
287
|
+
// Place it as before: Bundle.withChild('editor', DrawnEditor)
|
|
288
|
+
|
|
289
|
+
// where the page draws it:
|
|
290
|
+
Placed.view(model, h, { options: pickers(model), words: { submit: 'Save' } })
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`Crud.editorView` works on any Submodel view of the form that takes view
|
|
294
|
+
inputs; `foldkit-crud` itself stays headless.
|
|
295
|
+
|
|
296
|
+
## Deleting
|
|
297
|
+
|
|
298
|
+
A remover is a mutation with a yes in between. Like the editor it is a Bundle,
|
|
299
|
+
because which id is being asked about is state.
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const Remover = Crud.remover('PostRemover', {
|
|
303
|
+
mutation: DeletePostMutation,
|
|
304
|
+
input: id => ({ id }), // the mutation's input for an id
|
|
208
305
|
})
|
|
306
|
+
// In the page: Bundle.withChild('remover', Remover.bundle), beside the editor.
|
|
209
307
|
|
|
210
|
-
const
|
|
308
|
+
const PostRemover = Remover.at({ data: Data, model: App.model.remover })
|
|
309
|
+
// With the editor's: Bundle.configure('remover', { onOut: PostRemover.onOut })
|
|
310
|
+
const RemoveForm = Page.children.remover
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
- `RemoveForm.helpers.ask(id)` asks; nothing is deleted yet. `dismiss()`
|
|
314
|
+
clears it.
|
|
315
|
+
- `Remover.Message.Confirmed()` and `Cancelled()` are the yes and the no. A
|
|
316
|
+
yes with nothing asked does nothing.
|
|
317
|
+
- `PostRemover.status(model)` is `Idle`, `Confirming`, `Deleting`, `Deleted`
|
|
318
|
+
or `DeleteFailed`; `target(model)` is the id, for the confirmation's words,
|
|
319
|
+
and `error(model)` says why a delete failed.
|
|
320
|
+
|
|
321
|
+
The server's mutation says what is gone, with `deleted` in its outcome, and
|
|
322
|
+
names no list. Remote then knows the record absent: it leaves every list and
|
|
323
|
+
relation it was in with no refetch, a detail of it reads `NotFound`, and an
|
|
324
|
+
editor open on it reports `NotFound`.
|
|
325
|
+
|
|
326
|
+
## Detail
|
|
327
|
+
|
|
328
|
+
One record through a Selection, for a page that shows it. Like a list it holds
|
|
329
|
+
no state.
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
const PostDetail = Crud.detail('PostDetail', { selection: PostPage }).at({
|
|
211
333
|
data: Data,
|
|
212
|
-
|
|
213
|
-
input: model => (model.search === null ? undefined : { search: model.search }),
|
|
334
|
+
id: model => model.shownPostId ?? undefined, // `undefined` while none is shown
|
|
214
335
|
})
|
|
215
336
|
|
|
216
|
-
|
|
337
|
+
PostDetail.value(model) // RemoteData of the Selection's value
|
|
217
338
|
```
|
|
218
339
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
340
|
+
`Crud.detail(...).fields` lists the selected members with their labels, as a
|
|
341
|
+
list's `columns` does. `PostDetail.active` makes the value a requirement while
|
|
342
|
+
an id is shown, and `PostDetail.refresh(model)` asks for it again.
|
|
343
|
+
|
|
344
|
+
## When something fails
|
|
345
|
+
|
|
346
|
+
Nothing here retries on its own: a failure stays visible until your
|
|
347
|
+
application asks again.
|
|
348
|
+
|
|
349
|
+
| What happened | What the piece reports | What to do |
|
|
350
|
+
| --- | --- | --- |
|
|
351
|
+
| A list's page could not be read | `page(model)` is `Failed`, with `previous` rows if it had some | `refresh(model)` from a retry button |
|
|
352
|
+
| A detail's value could not be read | `value(model)` is `Failed` | `refresh(model)` |
|
|
353
|
+
| An editor's record could not be read | `LoadFailed`; a record read before and failed on refresh stays `Editing`, showing the last good value | `refresh(model)` |
|
|
354
|
+
| A save failed | `SaveFailed`; the drafts are kept | show `saveError(model)`; the user submits again |
|
|
355
|
+
| A delete failed | `DeleteFailed` | show `error(model)`; ask again |
|
|
356
|
+
| The record was deleted, here or elsewhere | a detail reads `NotFound`, an open editor reports `NotFound`, and it leaves every list | close the screen or say so |
|
|
357
|
+
| A piece's `active` is not wired | it stays `Initial` or `Loading`, and nothing reports it | wire every piece with [`Crud.actives`](#wiring-a-page) |
|
|
358
|
+
|
|
359
|
+
## Showing values
|
|
237
360
|
|
|
238
361
|
### How a column shows
|
|
239
362
|
|
|
@@ -259,6 +382,13 @@ Display.show(column.display, row[column.key], { yes: 'Live', no: 'Draft' }) // t
|
|
|
259
382
|
| `Ref` | a relation selected with `true`: its id, or ids |
|
|
260
383
|
| `Nested` | a relation read through a Selection: the target's own columns, with `shape` `one`, `many`, or `page` |
|
|
261
384
|
|
|
385
|
+
A display is resolved from the most to the least explicit source: `Display.of`
|
|
386
|
+
metadata on the member, how the relation was selected, then the shape of the
|
|
387
|
+
schema. `Display.show` is the text any view can fall back on; a view that wants
|
|
388
|
+
a link or a badge reads `display` and the value.
|
|
389
|
+
[`foldkit-mixins-crud`](../mixins-crud/README.md) draws a list as a table and a
|
|
390
|
+
detail as a description list from exactly this.
|
|
391
|
+
|
|
262
392
|
There is one primitive, as with a form's controls: a `kind`, whether it is
|
|
263
393
|
`shown`, its `data`, and its `text`. The kinds above are made with
|
|
264
394
|
`Display.kind`, and so is yours:
|
|
@@ -271,23 +401,16 @@ const Badge = Display.kind<{ readonly tone: string }>('Badge', {
|
|
|
271
401
|
const Post = Blog.Post.pipe(Entity.annotateMembers({ status: Display.of(Badge.of({ tone: 'soft' })) }))
|
|
272
402
|
```
|
|
273
403
|
|
|
274
|
-
`Badge.is(display)` narrows `display.data`. A drawn view draws it specially
|
|
275
|
-
given a renderer for `Badge`.
|
|
276
|
-
|
|
277
|
-
It is resolved from the most to the least explicit source: `Display.of`
|
|
278
|
-
metadata on the member, how the relation was selected, then the shape of the
|
|
279
|
-
schema. `Display.show` is the text any view can fall back on; a view that wants
|
|
280
|
-
a link or a badge reads `display` and the value.
|
|
281
|
-
[`foldkit-mixins-crud`](../mixins-crud/README.md) draws a list as a table and a
|
|
282
|
-
detail as a description list from exactly this.
|
|
404
|
+
`Badge.is(display)` narrows `display.data`. A drawn view draws it specially
|
|
405
|
+
once given a renderer for `Badge`.
|
|
283
406
|
|
|
284
407
|
### A relation picker's choices
|
|
285
408
|
|
|
286
409
|
A form names a relation's target Entity and leaves listing it to you, because a
|
|
287
410
|
relationship existing is no licence to read a table. A list is that licence: a
|
|
288
|
-
query you declared and your server authorizes. A list with a `choice` offers
|
|
289
|
-
loaded rows as choices, and `Crud.options` hands each picker of a form the
|
|
290
|
-
over its target:
|
|
411
|
+
query you declared and your server authorizes. A list with a `choice` offers
|
|
412
|
+
its loaded rows as choices, and `Crud.options` hands each picker of a form the
|
|
413
|
+
list over its target:
|
|
291
414
|
|
|
292
415
|
```ts
|
|
293
416
|
const pickers = Crud.options(EditPostForm, [AuthorList])
|
|
@@ -295,6 +418,14 @@ const pickers = Crud.options(EditPostForm, [AuthorList])
|
|
|
295
418
|
Placed.view(model, h, { options: pickers(model) }) // with foldkit-mixins-form
|
|
296
419
|
```
|
|
297
420
|
|
|
421
|
+
`pickers(model)` is keyed by the form's keys:
|
|
422
|
+
`{ authorId: [{ value, label }, …] }`. A list is matched to a picker by
|
|
423
|
+
Entity, so one author list serves `authorId` and `editorId` alike. A picker
|
|
424
|
+
whose target no list is over, or whose list has no `choice`, throws when
|
|
425
|
+
`Crud.options` is called, not when the form is drawn. `AuthorList.choices(model)`
|
|
426
|
+
is one list's choices on its own. Both are empty until the page is loaded, and
|
|
427
|
+
hold only the rows loaded so far.
|
|
428
|
+
|
|
298
429
|
#### When a picker searches
|
|
299
430
|
|
|
300
431
|
```ts
|
|
@@ -317,74 +448,10 @@ in it. With `chosen`, those rows stay among the choices, first, and
|
|
|
317
448
|
named. Without it a `select` would show a post's author as blank the moment a
|
|
318
449
|
search stopped finding them.
|
|
319
450
|
|
|
320
|
-
`pickers(model)` is keyed by the form's keys: `{ authorId: [{ value, label }, …] }`.
|
|
321
|
-
A list is matched to a picker by Entity, so one author list serves `authorId`
|
|
322
|
-
and `editorId` alike. A picker whose target no list is over, or whose list has no `choice`, throws
|
|
323
|
-
when `Crud.options` is called, not when the form is drawn. `AuthorList.choices(model)`
|
|
324
|
-
is one list's choices on its own. Both are empty until the page is loaded, and
|
|
325
|
-
hold only the rows loaded so far.
|
|
326
|
-
|
|
327
|
-
## Deleting
|
|
328
|
-
|
|
329
|
-
A remover is a mutation with a yes in between. Like the editor it is a Bundle,
|
|
330
|
-
because which id is being asked about is state.
|
|
331
|
-
|
|
332
|
-
```ts
|
|
333
|
-
const Remover = Crud.remover('PostRemover', {
|
|
334
|
-
mutation: DeletePostMutation,
|
|
335
|
-
input: id => ({ id }), // the mutation's input for an id
|
|
336
|
-
})
|
|
337
|
-
const RemoveSlot = Bundle.declare(Remover.bundle, 'remover')
|
|
338
|
-
// ...spread RemoveSlot.fields and RemoveSlot.cases into the page's Model and Message...
|
|
339
|
-
|
|
340
|
-
const PostRemover = Remover.at({ data: Data, model: App.model.remover })
|
|
341
|
-
const RemoveForm = Page.at(RemoveSlot, { onOut: PostRemover.onOut })
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
- `RemoveForm.helpers.ask(id)` asks; nothing is deleted yet. `dismiss()` clears it.
|
|
345
|
-
- `Remover.Message.Confirmed()` and `Cancelled()` are the yes and the no. A yes
|
|
346
|
-
with nothing asked does nothing.
|
|
347
|
-
- `PostRemover.status(model)` is `Idle`, `Confirming`, `Deleting`, `Deleted`, or
|
|
348
|
-
`DeleteFailed`; `target(model)` is the id for the confirmation's words, and
|
|
349
|
-
`error(model)` says why a delete failed.
|
|
350
|
-
|
|
351
|
-
The server's mutation says what is gone, with `deleted` in its outcome, and
|
|
352
|
-
names no list. Remote then knows the entity absent: it leaves every list and
|
|
353
|
-
relation it was in with no refetch, a detail of it reads `NotFound`, and an
|
|
354
|
-
editor open on it reports `NotFound`.
|
|
355
|
-
|
|
356
|
-
## Detail
|
|
357
|
-
|
|
358
|
-
One Entity through a Selection, for a page that shows it. Like a list it holds no
|
|
359
|
-
state.
|
|
360
|
-
|
|
361
|
-
```ts
|
|
362
|
-
const PostDetail = Crud.detail('PostDetail', { selection: PostPage }).at({
|
|
363
|
-
data: Data,
|
|
364
|
-
id: model => model.shownPostId ?? undefined, // `undefined` while none is shown
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
PostDetail.value(model) // RemoteData of the Selection's value
|
|
368
|
-
```
|
|
369
|
-
|
|
370
|
-
`Crud.detail(...).fields` lists the selected members with their labels, as a
|
|
371
|
-
list's `columns` does, and `PostDetail.active` makes the value a requirement
|
|
372
|
-
while an id is shown.
|
|
373
|
-
|
|
374
|
-
## Typed ids
|
|
375
|
-
|
|
376
|
-
When the Entity's `id` is branded, the ids here are too. An editor's
|
|
377
|
-
`helpers.open(id)` takes the `IdOf` of its form's Entity, and `target(model)`
|
|
378
|
-
reads the id being edited as that type. A remover whose mutation's input is just the id names the key, `id: 'id'`, and is
|
|
379
|
-
asked about that key's type; with a fuller input, annotate the function:
|
|
380
|
-
`input: (id: PostId) => ({ id, reason: 'spam' })`. A list's rows
|
|
381
|
-
are its Selection's own value, so `row.id` is a `PostId` when the Selection
|
|
382
|
-
reads `id`.
|
|
383
|
-
|
|
384
451
|
## Sorting a list
|
|
385
452
|
|
|
386
453
|
How a list is sorted is your state and the query's input. `Sort` writes that
|
|
387
|
-
state down once, so the Model, the header clicks
|
|
454
|
+
state down once, so the Model, the header clicks and the server agree:
|
|
388
455
|
|
|
389
456
|
```ts
|
|
390
457
|
const PostSort = Sort.make(['title', 'created']) // the orders offered, by name
|
|
@@ -404,30 +471,27 @@ PostSort.inputs(model.postSort, sort => Message.SortedPosts({ sort })) // a draw
|
|
|
404
471
|
the server, `foldkit-remote-drizzle`'s `sortTerms(sort, { title: posts.title })`
|
|
405
472
|
says what each name means and ignores any other.
|
|
406
473
|
|
|
407
|
-
##
|
|
408
|
-
|
|
409
|
-
Every placed piece has an `active`: what it requires of Remote while it is on
|
|
410
|
-
screen. A piece whose `active` is not wired never loads, and says nothing.
|
|
411
|
-
`Crud.actives` gathers them from the pieces themselves:
|
|
412
|
-
|
|
413
|
-
```ts
|
|
414
|
-
Data.wiring(Crud.actives({ posts: Posts, authors: Authors, editor: PostEditor, pickers }))
|
|
415
|
-
```
|
|
474
|
+
## Typed ids
|
|
416
475
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
476
|
+
When the Entity's `id` is branded, the ids here are too. An editor's
|
|
477
|
+
`helpers.open(id)` takes the `IdOf` of its form's Entity, and `target(model)`
|
|
478
|
+
reads the id being edited as that type. A remover whose mutation's input is
|
|
479
|
+
just the id names the key, `id: 'id'`, and is asked about that key's type; with
|
|
480
|
+
a fuller input, annotate the function:
|
|
481
|
+
`input: (id: PostId) => ({ id, reason: 'spam' })`. A list's rows are its
|
|
482
|
+
Selection's own value, so `row.id` is a `PostId` when the Selection reads `id`.
|
|
420
483
|
|
|
421
484
|
## Limits
|
|
422
485
|
|
|
423
|
-
- A list has no sorting, filtering
|
|
424
|
-
query's input, which your Model holds. Change the input and the list is
|
|
425
|
-
connection, fetched because it is on screen; `foldkit-remote-drizzle`'s
|
|
426
|
-
reads the input in `where` and `orderBy`.
|
|
427
|
-
searches and sorts its post list
|
|
428
|
-
|
|
486
|
+
- **A list has no sorting, filtering or selection state of its own.** Those are
|
|
487
|
+
the query's input, which your Model holds. Change the input and the list is
|
|
488
|
+
another connection, fetched because it is on screen; `foldkit-remote-drizzle`'s
|
|
489
|
+
`query` reads the input in `where` and `orderBy`.
|
|
490
|
+
[`examples/entity`](../../examples/entity) searches and sorts its post list
|
|
491
|
+
this way.
|
|
492
|
+
- **Headless.** Draw a list or a detail with
|
|
429
493
|
[`foldkit-mixins-crud`](../mixins-crud/README.md), or from `columns` and
|
|
430
494
|
`Display.show`. Draw the editor with `Crud.editorView` over a
|
|
431
495
|
[`foldkit-mixins-form`](../mixins-form/README.md) view, or from
|
|
432
|
-
`EditPostForm.controls`.
|
|
433
|
-
|
|
496
|
+
`EditPostForm.controls`.
|
|
497
|
+
- **One editor per form.** A create screen and an edit screen are two editors.
|
package/dist/index.d.mts
CHANGED
|
@@ -240,6 +240,7 @@ export declare const Crud: {
|
|
|
240
240
|
active: {
|
|
241
241
|
name: Name;
|
|
242
242
|
owner: object;
|
|
243
|
+
messages: never[];
|
|
243
244
|
projectionOf: (root: Root) => Projection<Root, RemoteData<any>> | undefined;
|
|
244
245
|
};
|
|
245
246
|
value: (root: Root) => RemoteData<Row>;
|
|
@@ -336,6 +337,7 @@ export declare const Crud: {
|
|
|
336
337
|
active: {
|
|
337
338
|
name: Name;
|
|
338
339
|
owner: object;
|
|
340
|
+
messages: never[];
|
|
339
341
|
projectionOf: (root: Root) => Projection<Root, RemoteData<Page<any>>> | undefined;
|
|
340
342
|
};
|
|
341
343
|
page: (root: Root) => RemoteData<Page<S["Type"]>>;
|
|
@@ -412,6 +414,7 @@ export declare const Crud: {
|
|
|
412
414
|
projectionOf: (root: Root) => Projection<Root, {
|
|
413
415
|
readonly [x: string]: RemoteData<any>;
|
|
414
416
|
}> | undefined;
|
|
417
|
+
messages: never[];
|
|
415
418
|
};
|
|
416
419
|
};
|
|
417
420
|
/**
|
|
@@ -495,6 +498,7 @@ export declare const Crud: {
|
|
|
495
498
|
active: {
|
|
496
499
|
name: Name;
|
|
497
500
|
owner: object;
|
|
501
|
+
messages: never[];
|
|
498
502
|
projectionOf: (root: Root) => Projection<Root, RemoteData<any>> | undefined;
|
|
499
503
|
};
|
|
500
504
|
sync: Update.Step<Root, never, never>;
|
|
@@ -505,6 +509,12 @@ export declare const Crud: {
|
|
|
505
509
|
after: <Message, Result extends {
|
|
506
510
|
readonly model: Root;
|
|
507
511
|
}>(update: (model: Root, message: Message) => Result) => (model: Root, message: Message) => Result;
|
|
512
|
+
/**
|
|
513
|
+
* Asks for the value being edited again: `Data.refresh`, for a retry
|
|
514
|
+
* button while `status` is `LoadFailed`. A failed read is not retried on
|
|
515
|
+
* its own. Unchanged while nothing is open for editing.
|
|
516
|
+
*/
|
|
517
|
+
refresh: (root: Root) => Root;
|
|
508
518
|
/** Why the last save failed, while `status` is `SaveFailed`. */
|
|
509
519
|
saveError: (root: Root) => RemoteError | undefined;
|
|
510
520
|
/** The id being edited, typed as the Entity's own; `null` for a new one or a closed editor. */
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/display.ts","../src/sort.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAgBiB,QAAQ;;WAEd;;WAEA;;WAEA,MAAM;;;;;WAKN,OAAO,gBAAgB,OAAO;;;UAIxB,YAAY;WAClB;WACA,KAAK,MAAM,SAAS,QAAQ;WAC5B,KAAK,SAAS,YAAY,WAAW,QAAQ;;;UAIvC,cAAc;WACpB,KAAK;WACL;WACA,QAAQ;WACR,SAAS;;;UAIH;;WAEN;;WAEA;;WAEA;;WAEA;;qBAsGE;;;;;;;EA3EC,OAAA,OAAI,uBAAA,cACJ;aAED;aACA,OAAO,MAAM,MAAM,gBAAgB,OAAO;QAEpD,YAAY;;aAeuB,WAAE;;;aAGC,WAAE;;;;;aASP,QAAA;aAA0B;;;aAa3C,QAAA;aACD;aACE,SAAA,cAAc;;EA4ChB,OAAA,UAAC,8BAA4B;EAC3B,SAAA,UAAC,6BAA2B;EACpC,YAAA;;EAEE,cAAA;;;;;EAME,KAAA,SAAA,YAAO;;;;;;;EASX,UAAA,QAAA,cAAY;aAET;aACA,SAAS,cAAc;QAEjC;;;;;;EAlGkB,OAAA,SAAA,SAAO,gBAAgB,QAAS;;;;;KC1F3C,UAAU;WACX,IAAI;WACJ;;;UAIM,aAAa;WACnB;WACA,SAAS;;qBAGP;;;;;;EAME,aAAA,gDAA8C,SAAW;;;IAkBlD,QAAA,OAAO,MAAK,UAAA,kBAAA,UAAA;;;;;;;;IAQnB,SAAA,SAAO,SAAA,UAAA,kBAAA,UAEJ,MAAI,UAAA,qBAAY,wBACb,KAAC,kBAAa,aAAa;;;;;;iBCb/B,aACf,qBACA,WACA,aACA,OACA,UAAU,WACV,eAAe,OAAO,OAAO,QAC7B,SACA;WAES;aACE,MAAM;aACN,OAAO,OAAO,MAAM;aACpB,SAAS,OAAO,MAAM;aACtB,SACP,OAAO,WACP,SAAS,aACT,eACG,OAAO,qBAAqB,WAAW,aAAa,UAAU,QAAQ;;WAEpE,OAAO,YAAY,GAAG,QAAQ;WAC9B,SAAS;WACT,OAAO,OAAO,WAAW,QAAQ,QAAQ;aAAsB,OAAO;;;;;;;;iBAQhE,YAAY;WAClB,MAAM;WACN;WACA;WACA;WACA;;;;;;iBAOM;WACN;WACA;;;iBAIM;WACN;WACA;;YAGC;;;YAQA;;;;iBAYK,WAAW;EAE1B,OACE,OAAO,MACP,eACA;aAES,OAAO;aACP;aACA,SAAS,QAAQ,sBAAsB;;EAElD,IAAI,gBAAgB,aAAa,WAAW,MAAM;EAClD,MAAM,YAAY,YAAY,eAAe,WAAW,MAAM,WAAW;EACzE,KAAK,OAAO,MAAM;aAA6B;;EAC/C,MAAM,WAAW,QAAQ,sBAAsB;EAC/C,QAAQ,OAAO,MAAM,cAAc;EACnC,SAAS,OAAO,MAAM,oBAAoB;WACjC;aAAqB;;;KAG3B,KAAK,QAAQ,OAAO,KAAK,MAAM,eAAe;;iBAGlC;WACN;WACA;;;;;;YAOC,WAAW,+BAA+B,cAAc;qBA+CvD;;;;;EAKI,eAAA,qBAAqB,2BAA2B,SAAS,KAAG,MACnE,MAAI;aAEC,WAAW,UAAU,YAAY,SAAS,OAAO;eAAwB,MAAM;;;;;;;IAO3D,OAAA;;IAG1B,KAAA,MAAI;eACE,MAAM,WAAW;eACjB,KAAK,MAAM
|
|
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
|
@@ -221,6 +221,7 @@ const Crud = {
|
|
|
221
221
|
active: {
|
|
222
222
|
name,
|
|
223
223
|
owner: where.data.contract.owner ?? {},
|
|
224
|
+
messages: [],
|
|
224
225
|
projectionOf
|
|
225
226
|
},
|
|
226
227
|
value: (root) => projectionOf(root)?.read(root) ?? { _tag: "Initial" },
|
|
@@ -334,6 +335,7 @@ const Crud = {
|
|
|
334
335
|
active: {
|
|
335
336
|
name,
|
|
336
337
|
owner: data.contract.owner ?? {},
|
|
338
|
+
messages: [],
|
|
337
339
|
projectionOf
|
|
338
340
|
},
|
|
339
341
|
page,
|
|
@@ -449,7 +451,8 @@ active: {
|
|
|
449
451
|
projectionOf: (root) => {
|
|
450
452
|
const rows = held(root);
|
|
451
453
|
return rows.length === 0 ? void 0 : Projection.struct(Object.fromEntries(rows.map(([key, list, id]) => [`${key}:${id}`, list.row(id)])));
|
|
452
|
-
}
|
|
454
|
+
},
|
|
455
|
+
messages: []
|
|
453
456
|
} });
|
|
454
457
|
},
|
|
455
458
|
/**
|
|
@@ -539,13 +542,19 @@ active: {
|
|
|
539
542
|
const { mode, target } = slice.get(root);
|
|
540
543
|
return mode === "edit" && target !== null ? data.get(current, target).read(root) : void 0;
|
|
541
544
|
};
|
|
542
|
-
/**
|
|
545
|
+
/**
|
|
546
|
+
* Shows the loaded value once, the first time it is there. Later refreshes
|
|
547
|
+
* leave the drafts alone. A value whose latest refresh failed is still the
|
|
548
|
+
* last one known good, so it fills the form too: `status` goes on saying
|
|
549
|
+
* `Editing`, and the failure is the read's to show.
|
|
550
|
+
*/
|
|
543
551
|
const sync = (root) => {
|
|
544
552
|
const editor = slice.get(root);
|
|
545
553
|
const read = loaded(root);
|
|
546
554
|
if (editor.filled || read === void 0) return { model: root };
|
|
547
|
-
|
|
548
|
-
|
|
555
|
+
const value = read._tag === "Ready" || read._tag === "Refreshing" ? read.value : read._tag === "Failed" ? read.previous : void 0;
|
|
556
|
+
if (value === void 0) return { model: root };
|
|
557
|
+
const values = Entity.valuesFor(form.input, value);
|
|
549
558
|
return { model: slice.set(root, {
|
|
550
559
|
...editor,
|
|
551
560
|
form: form.fill(editor.form, values).model,
|
|
@@ -576,6 +585,7 @@ active: {
|
|
|
576
585
|
active: {
|
|
577
586
|
name,
|
|
578
587
|
owner: data.contract.owner ?? {},
|
|
588
|
+
messages: [],
|
|
579
589
|
projectionOf: (root) => {
|
|
580
590
|
const { mode, target } = slice.get(root);
|
|
581
591
|
return mode === "edit" && target !== null ? data.get(current, target) : void 0;
|
|
@@ -593,6 +603,15 @@ active: {
|
|
|
593
603
|
model: sync(next.model).model
|
|
594
604
|
};
|
|
595
605
|
},
|
|
606
|
+
/**
|
|
607
|
+
* Asks for the value being edited again: `Data.refresh`, for a retry
|
|
608
|
+
* button while `status` is `LoadFailed`. A failed read is not retried on
|
|
609
|
+
* its own. Unchanged while nothing is open for editing.
|
|
610
|
+
*/
|
|
611
|
+
refresh: (root) => {
|
|
612
|
+
const { mode, target } = slice.get(root);
|
|
613
|
+
return mode === "edit" && target !== null ? data.refresh(root, data.get(current, target)) : root;
|
|
614
|
+
},
|
|
596
615
|
/** Why the last save failed, while `status` is `SaveFailed`. */
|
|
597
616
|
saveError: (root) => {
|
|
598
617
|
const save = saveOf(root);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/display.ts","../src/sort.ts","../src/index.ts"],"sourcesContent":["/**\n * Displays: how one selected member shows in a list or a detail, described\n * without a renderer. It is `foldkit-form`'s `Input` from the reading side: the\n * member says what it is, a Display says how it reads, and which DOM draws it is\n * the application's.\n */\nimport { Schema } from 'effect'\nimport type { AnyEntity, EntityMember } from 'foldkit-entity'\nimport { Metadata } from 'foldkit-metadata'\n\n/**\n * How one selected member shows: the primitive every kind of Display is a value\n * of. `Display.text()` and `Display.flag()` are values of it, made by\n * `Display.kind`, which is what an application calls for a badge or a relative\n * date. The kinds here are a collection, not a special case.\n */\nexport interface Display<Data = unknown> {\n /** The name a renderer is found by. */\n readonly kind: string\n /** Whether a view draws it. A member read and not shown, usually the id, is `false`. */\n readonly shown: boolean\n /** What this kind needs: a format, a relation's target. */\n readonly data: Data\n /**\n * The value as text: what a cell says when nothing draws it specially. It is\n * never given `null` or `undefined`; `Display.show` says `nothing` for those.\n */\n readonly text: (value: unknown, words: DisplayWords) => string\n}\n\n/** A kind of Display: how its values are made, and how one is told from another. */\nexport interface DisplayKind<Data> {\n readonly kind: string\n readonly of: (data: Data) => Display<Data>\n readonly is: (display: Display) => display is Display<Data>\n}\n\n/** A selected member with its words and its Display. A list's column, a detail's line. */\nexport interface DisplayColumn<Key extends string = string> {\n readonly key: Key\n readonly label: string\n readonly member: EntityMember\n readonly display: Display\n}\n\n/** The words a Display needs that are not the value's own. */\nexport interface DisplayWords {\n /** Default `yes`. */\n readonly yes?: string\n /** Default `no`. */\n readonly no?: string\n /** What stands for nothing: `null`, `undefined`, an empty list. Default the empty string. */\n readonly nothing?: string\n /** Between the items of a list, and between the members of a nested value. Default `, `. */\n readonly separator?: string\n}\n\nconst key = Metadata.key<Display>('foldkit-crud/display', {\n // The last Display attached wins: a later annotation refines an earlier one.\n merge: displays => displays.slice(-1),\n summarize: display => display.kind,\n})\n\ninterface AstLike {\n readonly _tag: string\n readonly types?: ReadonlyArray<AstLike>\n}\n\nconst fromSchema = (schema: Schema.Top): Display => {\n const ast = Schema.toType(schema).ast as unknown as AstLike\n const members =\n ast._tag === 'Union'\n ? (ast.types ?? []).filter(member => member._tag !== 'Null' && member._tag !== 'Undefined')\n : [ast]\n const [only] = members\n if (members.length === 1 && only?._tag === 'Number') return Number_.of({})\n if (members.length === 1 && only?._tag === 'Boolean') return Flag.of(nothing)\n return Text.of({})\n}\n\n/** A kind of Display. The kinds below are made with it, and so is an application's. */\nconst kind = <Data = Record<string, never>>(\n name: string,\n spec: {\n readonly shown?: boolean\n readonly text: (data: Data, value: unknown, words: DisplayWords) => string\n },\n): DisplayKind<Data> => ({\n kind: name,\n of: data =>\n Object.freeze({\n kind: name,\n shown: spec.shown ?? true,\n data,\n text: (value: unknown, words: DisplayWords) => spec.text(data, value, words),\n }),\n is: (display): display is Display<Data> => display.kind === name,\n})\n\nconst show = (display: Display, value: unknown, words: DisplayWords = {}): string =>\n value === null || value === undefined ? (words.nothing ?? '') : display.text(value, words)\n\nconst Text = kind<{ readonly format?: ((value: unknown) => string) | undefined }>('Text', {\n text: (data, value) => (data.format === undefined ? String(value) : data.format(value)),\n})\nconst Number_ = kind<{ readonly format?: ((value: number) => string) | undefined }>('Number', {\n text: (data, value) =>\n typeof value === 'number' && data.format !== undefined ? data.format(value) : String(value),\n})\nconst Flag = kind('Flag', {\n text: (_, value, words) => (value === true ? (words.yes ?? 'yes') : (words.no ?? 'no')),\n})\nconst Hidden = kind('Hidden', { shown: false, text: () => '' })\n/** A relation read as refs: which one, with nothing to say about it but its id. */\nconst Ref = kind<{ readonly target: AnyEntity; readonly many: boolean }>('Ref', {\n text: (_, value, words) => {\n const ids = (Array.isArray(value) ? value : [value]).map(ref =>\n typeof ref === 'object' && ref !== null && 'id' in ref ? String(ref.id) : String(ref),\n )\n return ids.length === 0 ? (words.nothing ?? '') : ids.join(words.separator ?? ', ')\n },\n})\n/**\n * A relation read through a Selection: the target's own members, each with a\n * Display. `shape` is how many there are: one, a list, or a page of a list.\n */\nconst Nested = kind<{\n readonly target: AnyEntity\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n}>('Nested', {\n text: (data, value, words) => {\n const one = (item: unknown): string =>\n data.columns\n .filter(column => column.display.shown)\n .map(column =>\n show(\n column.display,\n (item as Readonly<Record<string, unknown>> | null)?.[column.key],\n words,\n ),\n )\n .filter(text => text !== '')\n .join(' ')\n const items =\n data.shape === 'one'\n ? [value]\n : data.shape === 'page'\n ? ((value as { readonly items?: ReadonlyArray<unknown> }).items ?? [])\n : (value as ReadonlyArray<unknown>)\n return items.length === 0 ? (words.nothing ?? '') : items.map(one).join(words.separator ?? ', ')\n },\n})\n\nconst nothing: Record<string, never> = Object.freeze({})\n\nexport const Display = {\n /**\n * A kind of Display. `Display.kind<{ tone: string }>('Badge', { text: (_, value) => String(value) })`\n * gives `.of(data)` to make one and `.is(display)` to tell one, exactly as the\n * kinds below were made. `text` is the floor; a view draws it specially once\n * it is given a renderer for `Badge`.\n */\n kind,\n\n // The kinds this package resolves to. A view finds its renderer by `kind`.\n Text,\n Number: Number_,\n Flag,\n Hidden,\n Ref,\n Nested,\n\n text: (format?: (value: unknown) => string): Display => Text.of({ format }),\n number: (format?: (value: number) => string): Display => Number_.of({ format }),\n flag: (): Display => Flag.of(nothing),\n /** Read, so a row can be opened or keyed by it, and not shown: usually the id. */\n hidden: (): Display => Hidden.of(nothing),\n\n /**\n * Entity metadata: how a member shows wherever it is listed, e.g.\n * `Entity.annotateMembers({ price: Display.of(Display.number(cents => ...)) })`.\n */\n of: (display: Display) => key.of(display),\n\n /**\n * The Display of one selected member, from the most to the least explicit\n * source: the member's `Display.of` metadata, how the relation was selected,\n * then the shape of its schema. `nested` is the columns of a nested Selection,\n * with `shape` saying how many the relation yields.\n */\n resolve: (\n member: EntityMember,\n nested?: {\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n },\n ): Display => {\n const [explicit] = key.get(member.metadata)\n if (explicit !== undefined) return explicit\n if (member._tag !== 'Relation') return fromSchema(member.schema as Schema.Top)\n const target = member.target()\n return nested === undefined\n ? Ref.of({ target, many: member.cardinality === 'many' })\n : Nested.of({ target, shape: nested.shape, columns: nested.columns })\n },\n\n /**\n * A value as the text its Display calls for: what a cell says when nothing\n * draws it specially. A view that wants a link or a badge reads the Display\n * and the value itself; this is the floor every view can stand on.\n */\n show,\n}\n","/**\n * Sorting a list, written once. How a list is sorted is the application's state\n * and its query's input, so this holds none of it: it is the schema of that\n * state, what a click on a header makes of it, and the shape a drawn table\n * takes. The server reads the same state and decides which columns it means.\n */\nimport { Schema } from 'effect'\n\n/** How a list is sorted: by which of its orders, which way; `null` for the server's own order. */\nexport type SortState<Column extends string> = {\n readonly by: Column\n readonly direction: 'asc' | 'desc'\n} | null\n\n/** How one column is sorted now, and the Message a click on its header sends. */\nexport interface SortedColumn<Message> {\n readonly direction?: 'asc' | 'desc' | undefined\n readonly message: Message\n}\n\nexport const Sort = {\n /**\n * The orders a list offers, by name. A name is an order the server knows, not\n * a column: which columns may sort, and what `title` means in SQL, stay the\n * server's to decide.\n */\n make: <const Columns extends readonly [string, ...string[]]>(columns: Columns) => {\n type Column = Columns[number]\n type State = SortState<Column>\n /** A click on a header: ascending, then descending, then the server's own order again. */\n const toggle = (current: State, column: Column): State =>\n current === null || current.by !== column\n ? { by: column, direction: 'asc' }\n : current.direction === 'asc'\n ? { by: column, direction: 'desc' }\n : null\n return {\n columns,\n /** For the Model field, and for the query's input. */\n Schema: Schema.NullOr(\n Schema.Struct({\n by: Schema.Literals(columns),\n direction: Schema.Literals(['asc', 'desc']),\n }),\n ) as unknown as Schema.Codec<State, State>,\n /** The server's own order. */\n none: null as State,\n toggle,\n /**\n * What a drawn table takes as `sort`: every order with how it is sorted now\n * and the Message a click sends, made from the state the click leads to.\n */\n inputs: <Message>(\n current: State,\n message: (next: State) => Message,\n ): { readonly [K in Column]: SortedColumn<Message> } =>\n Object.fromEntries(\n columns.map(column => [\n column,\n {\n direction: current?.by === column ? current.direction : undefined,\n message: message(toggle(current, column)),\n },\n ]),\n ) as { readonly [K in Column]: SortedColumn<Message> },\n }\n },\n}\n","/**\n * `foldkit-crud` — management screens assembled from parts an application\n * already has.\n *\n * An editor joins a `foldkit-form` form, the Remote mutation its value feeds,\n * and the Entity they share: it loads what the form writes, shows it, turns a\n * valid submit into the mutation, and says how that went. Everything it makes is\n * ordinary Foldkit: a Bundle, an ActiveSurface, Update Steps. Nothing is\n * generated from an Entity alone; each capability is declared.\n */\nimport { Schema } from 'effect'\nimport { Bundle } from 'foldkit-bundle'\nimport {\n Entity,\n SelectionPageTypeId,\n type AnyEntity,\n type EntityInput,\n type EntityMember,\n type Selection,\n type IdOf,\n type SelectionPage,\n} from 'foldkit-entity'\nimport { Form, Input, type FormControl, type Submitted } from 'foldkit-form'\nimport { Display, type DisplayColumn } from './display.js'\nimport type {\n MutationDescriptor,\n Page,\n QueryDescriptor,\n MutationStatus,\n RemoteClient,\n RemoteData,\n RemoteError,\n RemoteMessage,\n} from 'foldkit-remote'\nimport { Projection, type ActiveSurface, type ModelRef } from 'foldkit-surface'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type { Html, HtmlBuilder } from 'foldkit/html'\nimport * as Submodel from 'foldkit/submodel'\nimport type * as Update from 'foldkit/update'\n\n/** The parts of a `Form.make` result an editor wraps. */\nexport interface EditableForm<\n Name extends string,\n FormModel,\n FormMessage,\n Value,\n E extends AnyEntity,\n Fields extends Schema.Struct.Fields,\n Members,\n R = never,\n> {\n readonly bundle: {\n readonly name: Name\n readonly Model: Schema.Codec<FormModel, unknown>\n readonly Message: Schema.Codec<FormMessage, unknown>\n readonly update: (\n model: FormModel,\n message: FormMessage,\n args: void,\n ) => Update.ReturnWithOutMessage<FormModel, FormMessage, Submitted<Value>, R>\n }\n readonly input: EntityInput<E, Fields, Members>\n readonly initial: FormModel\n readonly fill: (model: FormModel, values: Partial<Value>) => { readonly model: FormModel }\n}\n\n/**\n * An editor's slice of the parent Model. `target` is the id being edited, or\n * `null` for a new one; `filled` is whether the loaded value has been shown;\n * `requestId` is the save in progress or last settled.\n */\nexport interface EditorModel<FormModel> {\n readonly form: FormModel\n readonly mode: 'closed' | 'new' | 'edit'\n readonly target: string | null\n readonly filled: boolean\n readonly requestId: string | null\n}\n\n/**\n * A remover's slice of the parent Model: the id it was asked to delete and is\n * waiting on a yes for, and the delete in progress or last settled.\n */\nexport interface RemoverModel {\n readonly target: string | null\n readonly requestId: string | null\n}\n\n/** A remover's out Message: the user said yes to deleting `id`. */\nexport interface ConfirmedRemoval {\n readonly _tag: 'Confirmed'\n readonly id: string\n}\n\nexport type RemoverStatus =\n | 'Idle'\n /** Asked to delete an id, and waiting for a yes or a no. */\n | 'Confirming'\n | 'Deleting'\n | 'Deleted'\n | 'DeleteFailed'\n\nexport type EditorStatus =\n | 'Closed'\n /** Editing an id whose current values have not arrived. */\n | 'Loading'\n | 'NotFound'\n | 'LoadFailed'\n | 'Editing'\n | 'Saving'\n | 'Saved'\n | 'SaveFailed'\n\n/** The parts of a bound Remote domain an editor uses. */\nexport interface DomainLike<Root> {\n // Method syntax: the descriptors are checked where the editor is made, not here.\n mutate(\n model: Root,\n mutation: any,\n input: any,\n ): {\n readonly model: Root\n readonly requestId: string\n readonly command: Command<RemoteMessage, never, RemoteClient>\n }\n get(selection: any, id: string): Projection<Root, RemoteData<any>>\n query(query: any, input: any, options: any): Projection<Root, RemoteData<Page<any>>>\n next(model: Root, projection: any): { readonly query: string } | undefined\n fetch(ref: any): Command<RemoteMessage, never, RemoteClient>\n refresh(model: Root, target: any): Root\n mutation(model: Root, requestId: string): MutationStatus\n readonly contract: { readonly owner?: object | undefined }\n}\n\ntype Step<Root> = Update.Step<Root, RemoteMessage, RemoteClient>\n\n/** One thing a relation picker offers. */\nexport interface Choice {\n readonly value: string\n readonly label: string\n}\n\n/**\n * One column of a list, or one line of a detail: a member the Selection reads,\n * with its words and its `Display`.\n */\nexport type ListColumn<Key extends string = string> = DisplayColumn<Key>\n\nconst columnLabel = (key: string, member: EntityMember): string => {\n const title =\n member._tag === 'Relation'\n ? undefined\n : Schema.resolveAnnotations(member.schema as Schema.Top)?.title\n return typeof title === 'string' ? title : (Form.labelOf(member) ?? key)\n}\n\ntype AnySelection = Selection<string, unknown, Schema.Constraint>\n\n/** The selected members in the Selection's order, each with its label and its Display. */\nconst columnsOf = <Members>(\n selection: Selection<string, Members, Schema.Constraint>,\n): ReadonlyArray<ListColumn<keyof Members & string>> => {\n const members: Readonly<Record<string, EntityMember>> = selection.entity.members\n const selected = selection.members as Readonly<\n Record<string, true | AnySelection | SelectionPage<string, Schema.Constraint>>\n >\n return Object.keys(selected).map(key => {\n const member = members[key]!\n const how = selected[key]!\n // A relation read through a Selection shows the target's own members.\n const nested =\n how === true || member._tag !== 'Relation'\n ? undefined\n : SelectionPageTypeId in how\n ? { shape: 'page' as const, columns: columnsOf(how.selection) }\n : {\n shape: member.cardinality === 'many' ? ('many' as const) : ('one' as const),\n columns: columnsOf(how),\n }\n return {\n key: key as keyof Members & string,\n label: columnLabel(key, member),\n member,\n display: Display.resolve(member, nested),\n }\n })\n}\n\nconst RemoverMessage = defineMessageUnion({ Confirmed: {}, Cancelled: {} })\ntype RemoverMessage = typeof RemoverMessage.Type\n\nconst idle: RemoverModel = { target: null, requestId: null }\n\nexport const Crud = {\n /**\n * One Entity read through a Selection, for a page that shows it. Like a list\n * it holds no state: the value is Remote's and the id is the application's.\n */\n detail: <const Name extends string, EntityName extends string, Members, Row>(\n name: Name,\n config: {\n readonly selection: Selection<EntityName, Members, Schema.Constraint & { readonly Type: Row }>\n },\n ) => ({\n name,\n /** The selected members in the Selection's order, each with its label. */\n fields: columnsOf(config.selection),\n /** Type-only: the value shown, for a view to be typed by. Never read. */\n Value: undefined as unknown as Row,\n\n /** The detail where it lives: `id` is the one shown, or `undefined` while none is. */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly id: (root: Root) => string | undefined\n }) => {\n const projectionOf = (root: Root) => {\n const id = where.id(root)\n return id === undefined ? undefined : where.data.get(config.selection, id)\n }\n return {\n /** For `Data.subscriptions`: the value is fetched and retained while an id is shown. */\n active: {\n name,\n owner: where.data.contract.owner ?? {},\n projectionOf,\n } satisfies ActiveSurface<Root>,\n value: (root: Root): RemoteData<Row> =>\n projectionOf(root)?.read(root) ?? { _tag: 'Initial' },\n /**\n * Asks for the value again: `Data.refresh` over this detail, for a retry\n * button. A failed read is not retried on its own. Unchanged while no id\n * is shown.\n */\n refresh: (root: Root): Root => {\n const projection = projectionOf(root)\n return projection === undefined ? root : where.data.refresh(root, projection)\n },\n }\n },\n }),\n\n /**\n * Deleting through one mutation, with a yes in between: asked, confirmed,\n * deleted. What the mutation's input is for an id is `input`'s to say, and the\n * id it names there (`(id: PostId) => ...`) is the id `ask` takes.\n */\n remover: <\n const Name extends string,\n Input,\n Id extends string = string,\n const Key extends keyof Input = never,\n >(\n name: Name,\n config:\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n readonly input: (id: Id) => Input\n }\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n /**\n * The key of the mutation's input that holds the id, when that is all the\n * input is: `id: 'id'` for `{ id }`. The id's type is that key's.\n */\n readonly id: Key & (Input extends Readonly<Record<Key, string>> ? Key : never)\n },\n ) => {\n type AskedId = [Key] extends [never] ? Id : Input[Key] & string\n const inputFor = (id: string): Input =>\n 'input' in config ? config.input(id as Id) : ({ [config.id]: id } as Input)\n const bundle = Bundle.make(name, {\n Model: Schema.Struct({\n target: Schema.NullOr(Schema.String),\n requestId: Schema.NullOr(Schema.String),\n }) as unknown as Schema.Codec<RemoverModel, unknown>,\n Message: RemoverMessage,\n init: () => ({ model: idle }),\n update: (model: RemoverModel, message: RemoverMessage) =>\n message._tag === 'Cancelled' || model.target === null\n ? { model: idle }\n : { model, outMessage: { _tag: 'Confirmed', id: model.target } as ConfirmedRemoval },\n helpers: {\n /** Asks whether to delete `id`. Nothing is deleted until `Confirmed`. */\n ask: (_: RemoverModel, id: AskedId) => ({ model: { target: id, requestId: null } }),\n dismiss: () => ({ model: idle }),\n },\n })\n\n return {\n bundle,\n /** The remover's Messages, for the yes and the no of a confirmation. */\n Message: RemoverMessage,\n\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, RemoverModel>\n }) => {\n const { data, model: slice } = where\n const outcome = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n return {\n /** For the placement: a yes becomes the mutation, and its request is remembered. */\n onOut:\n (confirmed: ConfirmedRemoval): Step<Root> =>\n root => {\n const started = data.mutate(root, config.mutation, inputFor(confirmed.id))\n return {\n model: slice.set(started.model, {\n target: confirmed.id,\n requestId: started.requestId,\n }),\n commands: [started.command],\n }\n },\n\n /** The id being asked about or deleted, for the words of a confirmation. */\n target: (root: Root): AskedId | null => slice.get(root).target as AskedId | null,\n\n status: (root: Root): RemoverStatus => {\n const { target } = slice.get(root)\n const deleting = outcome(root)\n return deleting._tag === 'Pending'\n ? 'Deleting'\n : deleting._tag === 'Applied'\n ? 'Deleted'\n : deleting._tag === 'Failed'\n ? 'DeleteFailed'\n : target === null\n ? 'Idle'\n : 'Confirming'\n },\n\n /** Why the last delete failed, while `status` is `DeleteFailed`. */\n error: (root: Root): RemoteError | undefined => {\n const deleting = outcome(root)\n return deleting._tag === 'Failed' ? deleting.error : undefined\n },\n }\n },\n }\n },\n\n /**\n * A list over one query: which rows to show is the query's, what to show of\n * each is the Selection's. The pages live in Remote; the list holds no state.\n */\n list: <\n const Name extends string,\n EntityName extends string,\n Members,\n S extends Schema.Constraint,\n Input,\n >(\n name: Name,\n config: {\n readonly query: QueryDescriptor<string, Input, any>\n // The row is the Selection's own value, so it is typed with or without a `choice`.\n readonly selection: Selection<EntityName, Members, S>\n /** How many rows a page holds. Default 25. */\n readonly pageSize?: number\n /**\n * How a row reads as a choice in a relation picker: its id, and the words\n * that identify it to a person. Give it to a list that feeds pickers.\n */\n readonly choice?: {\n readonly value: (row: S['Type']) => string\n readonly label: (row: S['Type']) => string\n }\n },\n ) => {\n const { query, selection, pageSize = 25, choice } = config\n const columns = columnsOf(selection)\n\n return {\n name,\n /** The selected members in the Selection's order, each with its label. */\n columns,\n /** Type-only: a row, for a view to be typed by. Never read. */\n Row: undefined as unknown as S['Type'],\n\n /**\n * The list where it lives. `input` is the query's input as the Model has\n * it (filters, a search term), or `undefined` while the list is not shown.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly input: (root: Root) => Input | undefined\n }) => {\n const { data, input } = where\n const projectionOf = (root: Root) => {\n const value = input(root)\n return value === undefined\n ? undefined\n : data.query(query, value, { select: selection, first: pageSize })\n }\n const page = (root: Root): RemoteData<Page<S['Type']>> =>\n projectionOf(root)?.read(root) ?? { _tag: 'Initial' }\n\n return {\n /** For `Data.subscriptions`: the page and its rows are fetched and retained while shown. */\n active: {\n name,\n owner: data.contract.owner ?? {},\n projectionOf,\n } satisfies ActiveSurface<Root>,\n\n page,\n\n /** The Command that loads the next page onto this one, or `undefined` when there is none. */\n more: (root: Root): Command<RemoteMessage, never, RemoteClient> | undefined => {\n const projection = projectionOf(root)\n const next = projection === undefined ? undefined : data.next(root, projection)\n return next === undefined ? undefined : data.fetch(next)\n },\n\n /**\n * Asks for the list again: `Data.refresh` over its page and rows, for a\n * retry button. A failed read is not retried on its own. Unchanged while\n * the list is not shown.\n */\n refresh: (root: Root): Root => {\n const projection = projectionOf(root)\n return projection === undefined ? root : data.refresh(root, projection)\n },\n\n name,\n /** Whose requirement the list is, for anything that requires beside it. */\n owner: data.contract.owner ?? {},\n /**\n * One of the list's Entity through the list's Selection, by id, whether or\n * not the query finds it now: what a picker reads to name what is chosen.\n */\n row: (id: string): Projection<Root, RemoteData<any>> => data.get(selection, id),\n /** The Entity the rows are of, which is how a picker finds the list for its target. */\n entity: selection.entity as AnyEntity,\n /** Whether the list was given a `choice`, so its rows can be a picker's choices. */\n offersChoices: choice !== undefined,\n\n /**\n * The loaded rows as a picker's choices, read through `choice`. Listing a\n * relation's target is this query, which the application chose and the\n * server authorizes; nothing is read because a relation exists.\n */\n choices: (root: Root): ReadonlyArray<Choice> => {\n if (choice === undefined)\n throw new Error(`Crud list \"${name}\": give it a \"choice\" to use its rows in a picker`)\n const read = page(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? read.value.items.map(row => ({\n value: choice.value(row),\n label: choice.label(row),\n }))\n : []\n },\n\n /** That row as a choice, once it is read. */\n choiceOf: (root: Root, id: string): Choice | undefined => {\n if (choice === undefined) return undefined\n const read = data.get(selection, id).read(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? { value: choice.value(read.value), label: choice.label(read.value) }\n : undefined\n },\n }\n },\n }\n },\n\n /**\n * A form's Submodel view as the view of the editor that wraps it, for\n * `Bundle.withView`: the editor's Messages are the form's, and its Model holds\n * the form's under `form`.\n *\n * ```ts\n * Editor.bundle.pipe(Bundle.withView(Crud.editorView(FormView.submodel(form, view))))\n * ```\n */\n editorView: <FormModel, Message, ViewInputs>(\n view: Submodel.View<FormModel, Message, ViewInputs>,\n ): Submodel.View<EditorModel<FormModel>, Message, ViewInputs> =>\n Submodel.defineView<EditorModel<FormModel>, Message, ViewInputs>(((\n model: EditorModel<FormModel>,\n inputs: ViewInputs,\n h: HtmlBuilder<Message>,\n ): Html =>\n (view as (model: FormModel, inputs: ViewInputs, h: HtmlBuilder<Message>) => Html)(\n model.form,\n inputs,\n h,\n )) as never),\n\n /**\n * The choices of every relation picker in a form, from the lists of their\n * targets: `foldkit-mixins-form`'s `options`, keyed by the form's keys. A\n * picker whose target no list here is over is a wiring mistake, reported now.\n *\n * With `chosen`, the form's Model as the page has it, what a picker already\n * holds stays among its choices when the list no longer finds it (a search\n * narrowed it, or it is on a later page), and `active` requires those rows so\n * their words are there. Give `chosen` to any form whose pickers search.\n */\n options: <Key extends string, Root, FormModel = never>(\n form: {\n readonly controls: ReadonlyArray<FormControl<Key>>\n readonly field?: (model: FormModel, key: never) => { readonly value: unknown }\n },\n lists: ReadonlyArray<{\n readonly name: string\n readonly entity: AnyEntity\n readonly offersChoices: boolean\n readonly owner: object\n readonly choices: (root: Root) => ReadonlyArray<Choice>\n readonly row: (id: string) => Projection<Root, RemoteData<any>>\n readonly choiceOf: (root: Root, id: string) => Choice | undefined\n }>,\n options: { readonly chosen?: (root: Root) => FormModel | undefined } = {},\n ) => {\n const pickers = form.controls.flatMap(({ key, control }) => {\n if (!Input.RelationOne.is(control) && !Input.RelationMany.is(control)) return []\n const { target } = control.data\n const list = lists.find(candidate => Entity.same(candidate.entity, target))\n if (list === undefined)\n throw new Error(\n `Crud.options: \"${key}\" picks a ${target.name}, and no list given is over ${target.name}`,\n )\n if (!list.offersChoices)\n throw new Error(\n `Crud.options: \"${key}\" would pick from list \"${list.name}\", which has no \"choice\"`,\n )\n return [[key, list] as const]\n })\n\n /** The ids each picker holds now: the draft of a `one`, or of a `many`. */\n const held = (root: Root): ReadonlyArray<readonly [Key, (typeof lists)[number], string]> => {\n const model = options.chosen?.(root)\n const field = form.field as\n ((model: FormModel, key: string) => { readonly value: unknown }) | undefined\n if (model === undefined || field === undefined) return []\n return pickers.flatMap(([key, list]) => {\n const draft = field(model, key).value\n const ids = Array.isArray(draft) ? draft : typeof draft === 'string' ? [draft] : []\n return ids\n .filter((id): id is string => typeof id === 'string' && id !== '')\n .map(id => [key, list, id] as const)\n })\n }\n\n const choices = (root: Root): { readonly [K in Key]?: ReadonlyArray<Choice> } => {\n const chosen = held(root)\n return Object.fromEntries(\n pickers.map(([key, list]) => {\n const listed = list.choices(root)\n const missing = chosen\n .filter(([heldBy, , id]) => heldBy === key && !listed.some(item => item.value === id))\n .flatMap(([, , id]) => list.choiceOf(root, id) ?? [])\n // What is chosen leads, so it is in reach whatever the list shows.\n return [key, [...missing, ...listed]]\n }),\n ) as unknown as { readonly [K in Key]?: ReadonlyArray<Choice> }\n }\n\n const [first] = lists\n return Object.assign(choices, {\n /** For `Data.subscriptions`: what the pickers hold is read, so it can be named. */\n active: {\n name: `${pickers.map(([key]) => key).join('+')} chosen`,\n owner: first?.owner ?? {},\n projectionOf: (root: Root) => {\n const rows = held(root)\n return rows.length === 0\n ? undefined\n : Projection.struct(\n Object.fromEntries(rows.map(([key, list, id]) => [`${key}:${id}`, list.row(id)])),\n )\n },\n } satisfies ActiveSurface<Root>,\n })\n },\n\n /**\n * What the pieces on a page require of Remote, by the names given: a placed\n * editor, list, or detail, or the pickers from `Crud.options`. It is each one's\n * `active`, gathered, so handing the pieces over is enough and none is left out:\n *\n * ```ts\n * Data.wiring(Crud.actives({ posts: Posts, authors: Authors, editor: PostEditor, pickers }))\n * ```\n */\n actives: <const Pieces extends Readonly<Record<string, { readonly active: unknown }>>>(\n pieces: Pieces,\n ): { readonly [K in keyof Pieces]: Pieces[K]['active'] } =>\n Object.fromEntries(Object.entries(pieces).map(([name, piece]) => [name, piece.active])) as {\n readonly [K in keyof Pieces]: Pieces[K]['active']\n },\n\n /**\n * An editor for one form and the mutation its value feeds. The form's value\n * must be the mutation's input, which is what declaring the input once and\n * giving it to both guarantees.\n */\n editor: <\n const Name extends string,\n FormName extends string,\n FormModel,\n FormMessage extends { readonly _tag: string },\n Value,\n E extends AnyEntity,\n Fields extends Schema.Struct.Fields,\n Members,\n R = never,\n >(\n name: Name,\n config: {\n readonly form: EditableForm<FormName, FormModel, FormMessage, Value, E, Fields, Members, R>\n readonly mutation: MutationDescriptor<string, Value, any>\n },\n ) => {\n type Model = EditorModel<FormModel>\n const { form, mutation } = config\n\n const closed: Model = {\n form: form.initial,\n mode: 'closed',\n target: null,\n filled: false,\n requestId: null,\n }\n const Model = Schema.Struct({\n form: form.bundle.Model,\n mode: Schema.Literals(['closed', 'new', 'edit']),\n target: Schema.NullOr(Schema.String),\n filled: Schema.Boolean,\n requestId: Schema.NullOr(Schema.String),\n }) as unknown as Schema.Codec<Model, unknown>\n\n // The editor's Messages are the form's own: it adds state around the form,\n // not a second vocabulary.\n const bundle = Bundle.make(name, {\n Model,\n Message: form.bundle.Message,\n init: () => ({ model: closed }),\n update: (model: Model, message: FormMessage) => {\n const next = form.bundle.update(model.form, message, undefined)\n // An edit after a save starts a new round; the last save no longer describes the form.\n const requestId = message._tag === 'Changed' ? null : model.requestId\n // The form's Commands are the editor's: a check the form started has to run.\n const edited = {\n model: { ...model, form: next.model, requestId },\n ...(next.commands === undefined ? {} : { commands: next.commands }),\n }\n return next.outMessage === undefined ? edited : { ...edited, outMessage: next.outMessage }\n },\n helpers: {\n /** Edits `id`: the form is emptied, and filled once the current values arrive. */\n open: (_: Model, id: IdOf<E>) => ({\n model: { ...closed, mode: 'edit' as const, target: id },\n }),\n /** A blank form for a new one: nothing to load. */\n blank: () => ({ model: { ...closed, mode: 'new' as const, filled: true } }),\n close: () => ({ model: closed }),\n },\n })\n\n const current = Entity.selectFor(form.input)\n\n return {\n bundle,\n /** What the editor loads before it shows the form: every member the form writes. */\n selection: current,\n\n /**\n * The editor where it lives: `model` is its slice of the parent Model (the\n * field it was declared at), and `data` the Remote domain that registers the\n * mutation and the Entity.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, Model>\n }) => {\n const { data, model: slice } = where\n const loaded = (root: Root): RemoteData<any> | undefined => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.get(current, target).read(root)\n : undefined\n }\n\n /** Shows the loaded value once, the first time it is there. Later refreshes leave the drafts alone. */\n const sync: Update.Step<Root, never, never> = root => {\n const editor = slice.get(root)\n const read = loaded(root)\n if (editor.filled || read === undefined) return { model: root }\n if (read._tag !== 'Ready' && read._tag !== 'Refreshing') return { model: root }\n const values = Entity.valuesFor(form.input, read.value) as Partial<Value>\n return {\n model: slice.set(root, {\n ...editor,\n form: form.fill(editor.form, values).model,\n filled: true,\n }),\n }\n }\n\n const saveOf = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n\n return {\n /** For the placement: a valid submit becomes the mutation, and its request is remembered. */\n onOut:\n (submitted: Submitted<Value>): Step<Root> =>\n root => {\n const started = data.mutate(root, mutation, submitted.value)\n const editor = slice.get(started.model)\n return {\n model: slice.set(started.model, { ...editor, requestId: started.requestId }),\n commands: [started.command],\n }\n },\n\n /**\n * For `Data.subscriptions`: while an id is being edited, what it loads is\n * a requirement like any Surface's, so Remote fetches and retains it.\n */\n active: {\n name,\n owner: data.contract.owner ?? {},\n projectionOf: root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null ? data.get(current, target) : undefined\n },\n } satisfies ActiveSurface<Root>,\n\n sync,\n\n /**\n * Wraps the application's `update` so `sync` runs after every Message:\n * the value may arrive with any of them.\n */\n after:\n <Message, Result extends { readonly model: Root }>(\n update: (model: Root, message: Message) => Result,\n ) =>\n (model: Root, message: Message): Result => {\n const next = update(model, message)\n return { ...next, model: sync(next.model).model }\n },\n\n /** Why the last save failed, while `status` is `SaveFailed`. */\n saveError: (root: Root): RemoteError | undefined => {\n const save = saveOf(root)\n return save._tag === 'Failed' ? save.error : undefined\n },\n\n /** The id being edited, typed as the Entity's own; `null` for a new one or a closed editor. */\n target: (root: Root): IdOf<E> | null => slice.get(root).target as IdOf<E> | null,\n status: (root: Root): EditorStatus => {\n const editor = slice.get(root)\n if (editor.mode === 'closed') return 'Closed'\n // Gone outranks everything: a save that landed describes a thing that no longer exists.\n const read = loaded(root)\n if (read?._tag === 'NotFound') return 'NotFound'\n const save = saveOf(root)\n if (save._tag === 'Pending') return 'Saving'\n if (save._tag === 'Failed') return 'SaveFailed'\n if (save._tag === 'Applied') return 'Saved'\n if (editor.filled) return 'Editing'\n return read?._tag === 'Failed' ? 'LoadFailed' : 'Loading'\n },\n }\n },\n }\n },\n}\n\nexport { Display, type DisplayColumn, type DisplayKind, type DisplayWords } from './display.js'\nexport { Sort, type SortState, type SortedColumn } from './sort.js'\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/display.ts","../src/sort.ts","../src/index.ts"],"sourcesContent":["/**\n * Displays: how one selected member shows in a list or a detail, described\n * without a renderer. It is `foldkit-form`'s `Input` from the reading side: the\n * member says what it is, a Display says how it reads, and which DOM draws it is\n * the application's.\n */\nimport { Schema } from 'effect'\nimport type { AnyEntity, EntityMember } from 'foldkit-entity'\nimport { Metadata } from 'foldkit-metadata'\n\n/**\n * How one selected member shows: the primitive every kind of Display is a value\n * of. `Display.text()` and `Display.flag()` are values of it, made by\n * `Display.kind`, which is what an application calls for a badge or a relative\n * date. The kinds here are a collection, not a special case.\n */\nexport interface Display<Data = unknown> {\n /** The name a renderer is found by. */\n readonly kind: string\n /** Whether a view draws it. A member read and not shown, usually the id, is `false`. */\n readonly shown: boolean\n /** What this kind needs: a format, a relation's target. */\n readonly data: Data\n /**\n * The value as text: what a cell says when nothing draws it specially. It is\n * never given `null` or `undefined`; `Display.show` says `nothing` for those.\n */\n readonly text: (value: unknown, words: DisplayWords) => string\n}\n\n/** A kind of Display: how its values are made, and how one is told from another. */\nexport interface DisplayKind<Data> {\n readonly kind: string\n readonly of: (data: Data) => Display<Data>\n readonly is: (display: Display) => display is Display<Data>\n}\n\n/** A selected member with its words and its Display. A list's column, a detail's line. */\nexport interface DisplayColumn<Key extends string = string> {\n readonly key: Key\n readonly label: string\n readonly member: EntityMember\n readonly display: Display\n}\n\n/** The words a Display needs that are not the value's own. */\nexport interface DisplayWords {\n /** Default `yes`. */\n readonly yes?: string\n /** Default `no`. */\n readonly no?: string\n /** What stands for nothing: `null`, `undefined`, an empty list. Default the empty string. */\n readonly nothing?: string\n /** Between the items of a list, and between the members of a nested value. Default `, `. */\n readonly separator?: string\n}\n\nconst key = Metadata.key<Display>('foldkit-crud/display', {\n // The last Display attached wins: a later annotation refines an earlier one.\n merge: displays => displays.slice(-1),\n summarize: display => display.kind,\n})\n\ninterface AstLike {\n readonly _tag: string\n readonly types?: ReadonlyArray<AstLike>\n}\n\nconst fromSchema = (schema: Schema.Top): Display => {\n const ast = Schema.toType(schema).ast as unknown as AstLike\n const members =\n ast._tag === 'Union'\n ? (ast.types ?? []).filter(member => member._tag !== 'Null' && member._tag !== 'Undefined')\n : [ast]\n const [only] = members\n if (members.length === 1 && only?._tag === 'Number') return Number_.of({})\n if (members.length === 1 && only?._tag === 'Boolean') return Flag.of(nothing)\n return Text.of({})\n}\n\n/** A kind of Display. The kinds below are made with it, and so is an application's. */\nconst kind = <Data = Record<string, never>>(\n name: string,\n spec: {\n readonly shown?: boolean\n readonly text: (data: Data, value: unknown, words: DisplayWords) => string\n },\n): DisplayKind<Data> => ({\n kind: name,\n of: data =>\n Object.freeze({\n kind: name,\n shown: spec.shown ?? true,\n data,\n text: (value: unknown, words: DisplayWords) => spec.text(data, value, words),\n }),\n is: (display): display is Display<Data> => display.kind === name,\n})\n\nconst show = (display: Display, value: unknown, words: DisplayWords = {}): string =>\n value === null || value === undefined ? (words.nothing ?? '') : display.text(value, words)\n\nconst Text = kind<{ readonly format?: ((value: unknown) => string) | undefined }>('Text', {\n text: (data, value) => (data.format === undefined ? String(value) : data.format(value)),\n})\nconst Number_ = kind<{ readonly format?: ((value: number) => string) | undefined }>('Number', {\n text: (data, value) =>\n typeof value === 'number' && data.format !== undefined ? data.format(value) : String(value),\n})\nconst Flag = kind('Flag', {\n text: (_, value, words) => (value === true ? (words.yes ?? 'yes') : (words.no ?? 'no')),\n})\nconst Hidden = kind('Hidden', { shown: false, text: () => '' })\n/** A relation read as refs: which one, with nothing to say about it but its id. */\nconst Ref = kind<{ readonly target: AnyEntity; readonly many: boolean }>('Ref', {\n text: (_, value, words) => {\n const ids = (Array.isArray(value) ? value : [value]).map(ref =>\n typeof ref === 'object' && ref !== null && 'id' in ref ? String(ref.id) : String(ref),\n )\n return ids.length === 0 ? (words.nothing ?? '') : ids.join(words.separator ?? ', ')\n },\n})\n/**\n * A relation read through a Selection: the target's own members, each with a\n * Display. `shape` is how many there are: one, a list, or a page of a list.\n */\nconst Nested = kind<{\n readonly target: AnyEntity\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n}>('Nested', {\n text: (data, value, words) => {\n const one = (item: unknown): string =>\n data.columns\n .filter(column => column.display.shown)\n .map(column =>\n show(\n column.display,\n (item as Readonly<Record<string, unknown>> | null)?.[column.key],\n words,\n ),\n )\n .filter(text => text !== '')\n .join(' ')\n const items =\n data.shape === 'one'\n ? [value]\n : data.shape === 'page'\n ? ((value as { readonly items?: ReadonlyArray<unknown> }).items ?? [])\n : (value as ReadonlyArray<unknown>)\n return items.length === 0 ? (words.nothing ?? '') : items.map(one).join(words.separator ?? ', ')\n },\n})\n\nconst nothing: Record<string, never> = Object.freeze({})\n\nexport const Display = {\n /**\n * A kind of Display. `Display.kind<{ tone: string }>('Badge', { text: (_, value) => String(value) })`\n * gives `.of(data)` to make one and `.is(display)` to tell one, exactly as the\n * kinds below were made. `text` is the floor; a view draws it specially once\n * it is given a renderer for `Badge`.\n */\n kind,\n\n // The kinds this package resolves to. A view finds its renderer by `kind`.\n Text,\n Number: Number_,\n Flag,\n Hidden,\n Ref,\n Nested,\n\n text: (format?: (value: unknown) => string): Display => Text.of({ format }),\n number: (format?: (value: number) => string): Display => Number_.of({ format }),\n flag: (): Display => Flag.of(nothing),\n /** Read, so a row can be opened or keyed by it, and not shown: usually the id. */\n hidden: (): Display => Hidden.of(nothing),\n\n /**\n * Entity metadata: how a member shows wherever it is listed, e.g.\n * `Entity.annotateMembers({ price: Display.of(Display.number(cents => ...)) })`.\n */\n of: (display: Display) => key.of(display),\n\n /**\n * The Display of one selected member, from the most to the least explicit\n * source: the member's `Display.of` metadata, how the relation was selected,\n * then the shape of its schema. `nested` is the columns of a nested Selection,\n * with `shape` saying how many the relation yields.\n */\n resolve: (\n member: EntityMember,\n nested?: {\n readonly shape: 'one' | 'many' | 'page'\n readonly columns: ReadonlyArray<DisplayColumn>\n },\n ): Display => {\n const [explicit] = key.get(member.metadata)\n if (explicit !== undefined) return explicit\n if (member._tag !== 'Relation') return fromSchema(member.schema as Schema.Top)\n const target = member.target()\n return nested === undefined\n ? Ref.of({ target, many: member.cardinality === 'many' })\n : Nested.of({ target, shape: nested.shape, columns: nested.columns })\n },\n\n /**\n * A value as the text its Display calls for: what a cell says when nothing\n * draws it specially. A view that wants a link or a badge reads the Display\n * and the value itself; this is the floor every view can stand on.\n */\n show,\n}\n","/**\n * Sorting a list, written once. How a list is sorted is the application's state\n * and its query's input, so this holds none of it: it is the schema of that\n * state, what a click on a header makes of it, and the shape a drawn table\n * takes. The server reads the same state and decides which columns it means.\n */\nimport { Schema } from 'effect'\n\n/** How a list is sorted: by which of its orders, which way; `null` for the server's own order. */\nexport type SortState<Column extends string> = {\n readonly by: Column\n readonly direction: 'asc' | 'desc'\n} | null\n\n/** How one column is sorted now, and the Message a click on its header sends. */\nexport interface SortedColumn<Message> {\n readonly direction?: 'asc' | 'desc' | undefined\n readonly message: Message\n}\n\nexport const Sort = {\n /**\n * The orders a list offers, by name. A name is an order the server knows, not\n * a column: which columns may sort, and what `title` means in SQL, stay the\n * server's to decide.\n */\n make: <const Columns extends readonly [string, ...string[]]>(columns: Columns) => {\n type Column = Columns[number]\n type State = SortState<Column>\n /** A click on a header: ascending, then descending, then the server's own order again. */\n const toggle = (current: State, column: Column): State =>\n current === null || current.by !== column\n ? { by: column, direction: 'asc' }\n : current.direction === 'asc'\n ? { by: column, direction: 'desc' }\n : null\n return {\n columns,\n /** For the Model field, and for the query's input. */\n Schema: Schema.NullOr(\n Schema.Struct({\n by: Schema.Literals(columns),\n direction: Schema.Literals(['asc', 'desc']),\n }),\n ) as unknown as Schema.Codec<State, State>,\n /** The server's own order. */\n none: null as State,\n toggle,\n /**\n * What a drawn table takes as `sort`: every order with how it is sorted now\n * and the Message a click sends, made from the state the click leads to.\n */\n inputs: <Message>(\n current: State,\n message: (next: State) => Message,\n ): { readonly [K in Column]: SortedColumn<Message> } =>\n Object.fromEntries(\n columns.map(column => [\n column,\n {\n direction: current?.by === column ? current.direction : undefined,\n message: message(toggle(current, column)),\n },\n ]),\n ) as { readonly [K in Column]: SortedColumn<Message> },\n }\n },\n}\n","/**\n * `foldkit-crud` — management screens assembled from parts an application\n * already has.\n *\n * An editor joins a `foldkit-form` form, the Remote mutation its value feeds,\n * and the Entity they share: it loads what the form writes, shows it, turns a\n * valid submit into the mutation, and says how that went. Everything it makes is\n * ordinary Foldkit: a Bundle, an ActiveSurface, Update Steps. Nothing is\n * generated from an Entity alone; each capability is declared.\n */\nimport { Schema } from 'effect'\nimport { Bundle } from 'foldkit-bundle'\nimport {\n Entity,\n SelectionPageTypeId,\n type AnyEntity,\n type EntityInput,\n type EntityMember,\n type Selection,\n type IdOf,\n type SelectionPage,\n} from 'foldkit-entity'\nimport { Form, Input, type FormControl, type Submitted } from 'foldkit-form'\nimport { Display, type DisplayColumn } from './display.js'\nimport type {\n MutationDescriptor,\n Page,\n QueryDescriptor,\n MutationStatus,\n RemoteClient,\n RemoteData,\n RemoteError,\n RemoteMessage,\n} from 'foldkit-remote'\nimport { Projection, type ActiveSurface, type ModelRef } from 'foldkit-surface'\nimport type { Command } from 'foldkit/command'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type { Html, HtmlBuilder } from 'foldkit/html'\nimport * as Submodel from 'foldkit/submodel'\nimport type * as Update from 'foldkit/update'\n\n/** The parts of a `Form.make` result an editor wraps. */\nexport interface EditableForm<\n Name extends string,\n FormModel,\n FormMessage,\n Value,\n E extends AnyEntity,\n Fields extends Schema.Struct.Fields,\n Members,\n R = never,\n> {\n readonly bundle: {\n readonly name: Name\n readonly Model: Schema.Codec<FormModel, unknown>\n readonly Message: Schema.Codec<FormMessage, unknown>\n readonly update: (\n model: FormModel,\n message: FormMessage,\n args: void,\n ) => Update.ReturnWithOutMessage<FormModel, FormMessage, Submitted<Value>, R>\n }\n readonly input: EntityInput<E, Fields, Members>\n readonly initial: FormModel\n readonly fill: (model: FormModel, values: Partial<Value>) => { readonly model: FormModel }\n}\n\n/**\n * An editor's slice of the parent Model. `target` is the id being edited, or\n * `null` for a new one; `filled` is whether the loaded value has been shown;\n * `requestId` is the save in progress or last settled.\n */\nexport interface EditorModel<FormModel> {\n readonly form: FormModel\n readonly mode: 'closed' | 'new' | 'edit'\n readonly target: string | null\n readonly filled: boolean\n readonly requestId: string | null\n}\n\n/**\n * A remover's slice of the parent Model: the id it was asked to delete and is\n * waiting on a yes for, and the delete in progress or last settled.\n */\nexport interface RemoverModel {\n readonly target: string | null\n readonly requestId: string | null\n}\n\n/** A remover's out Message: the user said yes to deleting `id`. */\nexport interface ConfirmedRemoval {\n readonly _tag: 'Confirmed'\n readonly id: string\n}\n\nexport type RemoverStatus =\n | 'Idle'\n /** Asked to delete an id, and waiting for a yes or a no. */\n | 'Confirming'\n | 'Deleting'\n | 'Deleted'\n | 'DeleteFailed'\n\nexport type EditorStatus =\n | 'Closed'\n /** Editing an id whose current values have not arrived. */\n | 'Loading'\n | 'NotFound'\n | 'LoadFailed'\n | 'Editing'\n | 'Saving'\n | 'Saved'\n | 'SaveFailed'\n\n/** The parts of a bound Remote domain an editor uses. */\nexport interface DomainLike<Root> {\n // Method syntax: the descriptors are checked where the editor is made, not here.\n mutate(\n model: Root,\n mutation: any,\n input: any,\n ): {\n readonly model: Root\n readonly requestId: string\n readonly command: Command<RemoteMessage, never, RemoteClient>\n }\n get(selection: any, id: string): Projection<Root, RemoteData<any>>\n query(query: any, input: any, options: any): Projection<Root, RemoteData<Page<any>>>\n next(model: Root, projection: any): { readonly query: string } | undefined\n fetch(ref: any): Command<RemoteMessage, never, RemoteClient>\n refresh(model: Root, target: any): Root\n mutation(model: Root, requestId: string): MutationStatus\n readonly contract: { readonly owner?: object | undefined }\n}\n\ntype Step<Root> = Update.Step<Root, RemoteMessage, RemoteClient>\n\n/** One thing a relation picker offers. */\nexport interface Choice {\n readonly value: string\n readonly label: string\n}\n\n/**\n * One column of a list, or one line of a detail: a member the Selection reads,\n * with its words and its `Display`.\n */\nexport type ListColumn<Key extends string = string> = DisplayColumn<Key>\n\nconst columnLabel = (key: string, member: EntityMember): string => {\n const title =\n member._tag === 'Relation'\n ? undefined\n : Schema.resolveAnnotations(member.schema as Schema.Top)?.title\n return typeof title === 'string' ? title : (Form.labelOf(member) ?? key)\n}\n\ntype AnySelection = Selection<string, unknown, Schema.Constraint>\n\n/** The selected members in the Selection's order, each with its label and its Display. */\nconst columnsOf = <Members>(\n selection: Selection<string, Members, Schema.Constraint>,\n): ReadonlyArray<ListColumn<keyof Members & string>> => {\n const members: Readonly<Record<string, EntityMember>> = selection.entity.members\n const selected = selection.members as Readonly<\n Record<string, true | AnySelection | SelectionPage<string, Schema.Constraint>>\n >\n return Object.keys(selected).map(key => {\n const member = members[key]!\n const how = selected[key]!\n // A relation read through a Selection shows the target's own members.\n const nested =\n how === true || member._tag !== 'Relation'\n ? undefined\n : SelectionPageTypeId in how\n ? { shape: 'page' as const, columns: columnsOf(how.selection) }\n : {\n shape: member.cardinality === 'many' ? ('many' as const) : ('one' as const),\n columns: columnsOf(how),\n }\n return {\n key: key as keyof Members & string,\n label: columnLabel(key, member),\n member,\n display: Display.resolve(member, nested),\n }\n })\n}\n\nconst RemoverMessage = defineMessageUnion({ Confirmed: {}, Cancelled: {} })\ntype RemoverMessage = typeof RemoverMessage.Type\n\nconst idle: RemoverModel = { target: null, requestId: null }\n\nexport const Crud = {\n /**\n * One Entity read through a Selection, for a page that shows it. Like a list\n * it holds no state: the value is Remote's and the id is the application's.\n */\n detail: <const Name extends string, EntityName extends string, Members, Row>(\n name: Name,\n config: {\n readonly selection: Selection<EntityName, Members, Schema.Constraint & { readonly Type: Row }>\n },\n ) => ({\n name,\n /** The selected members in the Selection's order, each with its label. */\n fields: columnsOf(config.selection),\n /** Type-only: the value shown, for a view to be typed by. Never read. */\n Value: undefined as unknown as Row,\n\n /** The detail where it lives: `id` is the one shown, or `undefined` while none is. */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly id: (root: Root) => string | undefined\n }) => {\n const projectionOf = (root: Root) => {\n const id = where.id(root)\n return id === undefined ? undefined : where.data.get(config.selection, id)\n }\n return {\n /** For `Data.subscriptions`: the value is fetched and retained while an id is shown. */\n active: {\n name,\n owner: where.data.contract.owner ?? {},\n // A requirement, not a sender: the page's own Surfaces list its Messages.\n messages: [],\n projectionOf,\n } satisfies ActiveSurface<Root>,\n value: (root: Root): RemoteData<Row> =>\n projectionOf(root)?.read(root) ?? { _tag: 'Initial' },\n /**\n * Asks for the value again: `Data.refresh` over this detail, for a retry\n * button. A failed read is not retried on its own. Unchanged while no id\n * is shown.\n */\n refresh: (root: Root): Root => {\n const projection = projectionOf(root)\n return projection === undefined ? root : where.data.refresh(root, projection)\n },\n }\n },\n }),\n\n /**\n * Deleting through one mutation, with a yes in between: asked, confirmed,\n * deleted. What the mutation's input is for an id is `input`'s to say, and the\n * id it names there (`(id: PostId) => ...`) is the id `ask` takes.\n */\n remover: <\n const Name extends string,\n Input,\n Id extends string = string,\n const Key extends keyof Input = never,\n >(\n name: Name,\n config:\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n readonly input: (id: Id) => Input\n }\n | {\n readonly mutation: MutationDescriptor<string, Input, any>\n /**\n * The key of the mutation's input that holds the id, when that is all the\n * input is: `id: 'id'` for `{ id }`. The id's type is that key's.\n */\n readonly id: Key & (Input extends Readonly<Record<Key, string>> ? Key : never)\n },\n ) => {\n type AskedId = [Key] extends [never] ? Id : Input[Key] & string\n const inputFor = (id: string): Input =>\n 'input' in config ? config.input(id as Id) : ({ [config.id]: id } as Input)\n const bundle = Bundle.make(name, {\n Model: Schema.Struct({\n target: Schema.NullOr(Schema.String),\n requestId: Schema.NullOr(Schema.String),\n }) as unknown as Schema.Codec<RemoverModel, unknown>,\n Message: RemoverMessage,\n init: () => ({ model: idle }),\n update: (model: RemoverModel, message: RemoverMessage) =>\n message._tag === 'Cancelled' || model.target === null\n ? { model: idle }\n : { model, outMessage: { _tag: 'Confirmed', id: model.target } as ConfirmedRemoval },\n helpers: {\n /** Asks whether to delete `id`. Nothing is deleted until `Confirmed`. */\n ask: (_: RemoverModel, id: AskedId) => ({ model: { target: id, requestId: null } }),\n dismiss: () => ({ model: idle }),\n },\n })\n\n return {\n bundle,\n /** The remover's Messages, for the yes and the no of a confirmation. */\n Message: RemoverMessage,\n\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, RemoverModel>\n }) => {\n const { data, model: slice } = where\n const outcome = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n return {\n /** For the placement: a yes becomes the mutation, and its request is remembered. */\n onOut:\n (confirmed: ConfirmedRemoval): Step<Root> =>\n root => {\n const started = data.mutate(root, config.mutation, inputFor(confirmed.id))\n return {\n model: slice.set(started.model, {\n target: confirmed.id,\n requestId: started.requestId,\n }),\n commands: [started.command],\n }\n },\n\n /** The id being asked about or deleted, for the words of a confirmation. */\n target: (root: Root): AskedId | null => slice.get(root).target as AskedId | null,\n\n status: (root: Root): RemoverStatus => {\n const { target } = slice.get(root)\n const deleting = outcome(root)\n return deleting._tag === 'Pending'\n ? 'Deleting'\n : deleting._tag === 'Applied'\n ? 'Deleted'\n : deleting._tag === 'Failed'\n ? 'DeleteFailed'\n : target === null\n ? 'Idle'\n : 'Confirming'\n },\n\n /** Why the last delete failed, while `status` is `DeleteFailed`. */\n error: (root: Root): RemoteError | undefined => {\n const deleting = outcome(root)\n return deleting._tag === 'Failed' ? deleting.error : undefined\n },\n }\n },\n }\n },\n\n /**\n * A list over one query: which rows to show is the query's, what to show of\n * each is the Selection's. The pages live in Remote; the list holds no state.\n */\n list: <\n const Name extends string,\n EntityName extends string,\n Members,\n S extends Schema.Constraint,\n Input,\n >(\n name: Name,\n config: {\n readonly query: QueryDescriptor<string, Input, any>\n // The row is the Selection's own value, so it is typed with or without a `choice`.\n readonly selection: Selection<EntityName, Members, S>\n /** How many rows a page holds. Default 25. */\n readonly pageSize?: number\n /**\n * How a row reads as a choice in a relation picker: its id, and the words\n * that identify it to a person. Give it to a list that feeds pickers.\n */\n readonly choice?: {\n readonly value: (row: S['Type']) => string\n readonly label: (row: S['Type']) => string\n }\n },\n ) => {\n const { query, selection, pageSize = 25, choice } = config\n const columns = columnsOf(selection)\n\n return {\n name,\n /** The selected members in the Selection's order, each with its label. */\n columns,\n /** Type-only: a row, for a view to be typed by. Never read. */\n Row: undefined as unknown as S['Type'],\n\n /**\n * The list where it lives. `input` is the query's input as the Model has\n * it (filters, a search term), or `undefined` while the list is not shown.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly input: (root: Root) => Input | undefined\n }) => {\n const { data, input } = where\n const projectionOf = (root: Root) => {\n const value = input(root)\n return value === undefined\n ? undefined\n : data.query(query, value, { select: selection, first: pageSize })\n }\n const page = (root: Root): RemoteData<Page<S['Type']>> =>\n projectionOf(root)?.read(root) ?? { _tag: 'Initial' }\n\n return {\n /** For `Data.subscriptions`: the page and its rows are fetched and retained while shown. */\n active: {\n name,\n owner: data.contract.owner ?? {},\n messages: [],\n projectionOf,\n } satisfies ActiveSurface<Root>,\n\n page,\n\n /** The Command that loads the next page onto this one, or `undefined` when there is none. */\n more: (root: Root): Command<RemoteMessage, never, RemoteClient> | undefined => {\n const projection = projectionOf(root)\n const next = projection === undefined ? undefined : data.next(root, projection)\n return next === undefined ? undefined : data.fetch(next)\n },\n\n /**\n * Asks for the list again: `Data.refresh` over its page and rows, for a\n * retry button. A failed read is not retried on its own. Unchanged while\n * the list is not shown.\n */\n refresh: (root: Root): Root => {\n const projection = projectionOf(root)\n return projection === undefined ? root : data.refresh(root, projection)\n },\n\n name,\n /** Whose requirement the list is, for anything that requires beside it. */\n owner: data.contract.owner ?? {},\n /**\n * One of the list's Entity through the list's Selection, by id, whether or\n * not the query finds it now: what a picker reads to name what is chosen.\n */\n row: (id: string): Projection<Root, RemoteData<any>> => data.get(selection, id),\n /** The Entity the rows are of, which is how a picker finds the list for its target. */\n entity: selection.entity as AnyEntity,\n /** Whether the list was given a `choice`, so its rows can be a picker's choices. */\n offersChoices: choice !== undefined,\n\n /**\n * The loaded rows as a picker's choices, read through `choice`. Listing a\n * relation's target is this query, which the application chose and the\n * server authorizes; nothing is read because a relation exists.\n */\n choices: (root: Root): ReadonlyArray<Choice> => {\n if (choice === undefined)\n throw new Error(`Crud list \"${name}\": give it a \"choice\" to use its rows in a picker`)\n const read = page(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? read.value.items.map(row => ({\n value: choice.value(row),\n label: choice.label(row),\n }))\n : []\n },\n\n /** That row as a choice, once it is read. */\n choiceOf: (root: Root, id: string): Choice | undefined => {\n if (choice === undefined) return undefined\n const read = data.get(selection, id).read(root)\n return read._tag === 'Ready' || read._tag === 'Refreshing'\n ? { value: choice.value(read.value), label: choice.label(read.value) }\n : undefined\n },\n }\n },\n }\n },\n\n /**\n * A form's Submodel view as the view of the editor that wraps it, for\n * `Bundle.withView`: the editor's Messages are the form's, and its Model holds\n * the form's under `form`.\n *\n * ```ts\n * Editor.bundle.pipe(Bundle.withView(Crud.editorView(FormView.submodel(form, view))))\n * ```\n */\n editorView: <FormModel, Message, ViewInputs>(\n view: Submodel.View<FormModel, Message, ViewInputs>,\n ): Submodel.View<EditorModel<FormModel>, Message, ViewInputs> =>\n Submodel.defineView<EditorModel<FormModel>, Message, ViewInputs>(((\n model: EditorModel<FormModel>,\n inputs: ViewInputs,\n h: HtmlBuilder<Message>,\n ): Html =>\n (view as (model: FormModel, inputs: ViewInputs, h: HtmlBuilder<Message>) => Html)(\n model.form,\n inputs,\n h,\n )) as never),\n\n /**\n * The choices of every relation picker in a form, from the lists of their\n * targets: `foldkit-mixins-form`'s `options`, keyed by the form's keys. A\n * picker whose target no list here is over is a wiring mistake, reported now.\n *\n * With `chosen`, the form's Model as the page has it, what a picker already\n * holds stays among its choices when the list no longer finds it (a search\n * narrowed it, or it is on a later page), and `active` requires those rows so\n * their words are there. Give `chosen` to any form whose pickers search.\n */\n options: <Key extends string, Root, FormModel = never>(\n form: {\n readonly controls: ReadonlyArray<FormControl<Key>>\n readonly field?: (model: FormModel, key: never) => { readonly value: unknown }\n },\n lists: ReadonlyArray<{\n readonly name: string\n readonly entity: AnyEntity\n readonly offersChoices: boolean\n readonly owner: object\n readonly choices: (root: Root) => ReadonlyArray<Choice>\n readonly row: (id: string) => Projection<Root, RemoteData<any>>\n readonly choiceOf: (root: Root, id: string) => Choice | undefined\n }>,\n options: { readonly chosen?: (root: Root) => FormModel | undefined } = {},\n ) => {\n const pickers = form.controls.flatMap(({ key, control }) => {\n if (!Input.RelationOne.is(control) && !Input.RelationMany.is(control)) return []\n const { target } = control.data\n const list = lists.find(candidate => Entity.same(candidate.entity, target))\n if (list === undefined)\n throw new Error(\n `Crud.options: \"${key}\" picks a ${target.name}, and no list given is over ${target.name}`,\n )\n if (!list.offersChoices)\n throw new Error(\n `Crud.options: \"${key}\" would pick from list \"${list.name}\", which has no \"choice\"`,\n )\n return [[key, list] as const]\n })\n\n /** The ids each picker holds now: the draft of a `one`, or of a `many`. */\n const held = (root: Root): ReadonlyArray<readonly [Key, (typeof lists)[number], string]> => {\n const model = options.chosen?.(root)\n const field = form.field as\n ((model: FormModel, key: string) => { readonly value: unknown }) | undefined\n if (model === undefined || field === undefined) return []\n return pickers.flatMap(([key, list]) => {\n const draft = field(model, key).value\n const ids = Array.isArray(draft) ? draft : typeof draft === 'string' ? [draft] : []\n return ids\n .filter((id): id is string => typeof id === 'string' && id !== '')\n .map(id => [key, list, id] as const)\n })\n }\n\n const choices = (root: Root): { readonly [K in Key]?: ReadonlyArray<Choice> } => {\n const chosen = held(root)\n return Object.fromEntries(\n pickers.map(([key, list]) => {\n const listed = list.choices(root)\n const missing = chosen\n .filter(([heldBy, , id]) => heldBy === key && !listed.some(item => item.value === id))\n .flatMap(([, , id]) => list.choiceOf(root, id) ?? [])\n // What is chosen leads, so it is in reach whatever the list shows.\n return [key, [...missing, ...listed]]\n }),\n ) as unknown as { readonly [K in Key]?: ReadonlyArray<Choice> }\n }\n\n const [first] = lists\n return Object.assign(choices, {\n /** For `Data.subscriptions`: what the pickers hold is read, so it can be named. */\n active: {\n name: `${pickers.map(([key]) => key).join('+')} chosen`,\n owner: first?.owner ?? {},\n projectionOf: (root: Root) => {\n const rows = held(root)\n return rows.length === 0\n ? undefined\n : Projection.struct(\n Object.fromEntries(rows.map(([key, list, id]) => [`${key}:${id}`, list.row(id)])),\n )\n },\n 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 = message._tag === 'Changed' ? null : model.requestId\n // The form's Commands are the editor's: a check the form started has to run.\n const edited = {\n model: { ...model, form: next.model, requestId },\n ...(next.commands === undefined ? {} : { commands: next.commands }),\n }\n return next.outMessage === undefined ? edited : { ...edited, outMessage: next.outMessage }\n },\n helpers: {\n /** Edits `id`: the form is emptied, and filled once the current values arrive. */\n open: (_: Model, id: IdOf<E>) => ({\n model: { ...closed, mode: 'edit' as const, target: id },\n }),\n /** A blank form for a new one: nothing to load. */\n blank: () => ({ model: { ...closed, mode: 'new' as const, filled: true } }),\n close: () => ({ model: closed }),\n },\n })\n\n const current = Entity.selectFor(form.input)\n\n return {\n bundle,\n /** What the editor loads before it shows the form: every member the form writes. */\n selection: current,\n\n /**\n * The editor where it lives: `model` is its slice of the parent Model (the\n * field it was declared at), and `data` the Remote domain that registers the\n * mutation and the Entity.\n */\n at: <Root>(where: {\n readonly data: DomainLike<Root>\n readonly model: ModelRef<Root, Model>\n }) => {\n const { data, model: slice } = where\n const loaded = (root: Root): RemoteData<any> | undefined => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.get(current, target).read(root)\n : undefined\n }\n\n /**\n * Shows the loaded value once, the first time it is there. Later refreshes\n * leave the drafts alone. A value whose latest refresh failed is still the\n * last one known good, so it fills the form too: `status` goes on saying\n * `Editing`, and the failure is the read's to show.\n */\n const sync: Update.Step<Root, never, never> = root => {\n const editor = slice.get(root)\n const read = loaded(root)\n if (editor.filled || read === undefined) return { model: root }\n const value =\n read._tag === 'Ready' || read._tag === 'Refreshing'\n ? read.value\n : read._tag === 'Failed'\n ? read.previous\n : undefined\n if (value === undefined) return { model: root }\n const values = Entity.valuesFor(form.input, value) as Partial<Value>\n return {\n model: slice.set(root, {\n ...editor,\n form: form.fill(editor.form, values).model,\n filled: true,\n }),\n }\n }\n\n const saveOf = (root: Root): MutationStatus => {\n const { requestId } = slice.get(root)\n return requestId === null ? { _tag: 'Unknown' } : data.mutation(root, requestId)\n }\n\n return {\n /** For the placement: a valid submit becomes the mutation, and its request is remembered. */\n onOut:\n (submitted: Submitted<Value>): Step<Root> =>\n root => {\n const started = data.mutate(root, mutation, submitted.value)\n const editor = slice.get(started.model)\n return {\n model: slice.set(started.model, { ...editor, requestId: started.requestId }),\n commands: [started.command],\n }\n },\n\n /**\n * For `Data.subscriptions`: while an id is being edited, what it loads is\n * a requirement like any Surface's, so Remote fetches and retains it.\n */\n active: {\n name,\n owner: data.contract.owner ?? {},\n messages: [],\n projectionOf: root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null ? data.get(current, target) : undefined\n },\n } satisfies ActiveSurface<Root>,\n\n sync,\n\n /**\n * Wraps the application's `update` so `sync` runs after every Message:\n * the value may arrive with any of them.\n */\n after:\n <Message, Result extends { readonly model: Root }>(\n update: (model: Root, message: Message) => Result,\n ) =>\n (model: Root, message: Message): Result => {\n const next = update(model, message)\n return { ...next, model: sync(next.model).model }\n },\n\n /**\n * Asks for the value being edited again: `Data.refresh`, for a retry\n * button while `status` is `LoadFailed`. A failed read is not retried on\n * its own. Unchanged while nothing is open for editing.\n */\n refresh: (root: Root): Root => {\n const { mode, target } = slice.get(root)\n return mode === 'edit' && target !== null\n ? data.refresh(root, data.get(current, target))\n : root\n },\n\n /** Why the last save failed, while `status` is `SaveFailed`. */\n saveError: (root: Root): RemoteError | undefined => {\n const save = saveOf(root)\n return save._tag === 'Failed' ? save.error : undefined\n },\n\n /** The id being edited, typed as the Entity's own; `null` for a new one or a closed editor. */\n target: (root: Root): IdOf<E> | null => slice.get(root).target as IdOf<E> | null,\n status: (root: Root): EditorStatus => {\n const editor = slice.get(root)\n if (editor.mode === 'closed') return 'Closed'\n // Gone outranks everything: a save that landed describes a thing that no longer exists.\n const read = loaded(root)\n if (read?._tag === 'NotFound') return 'NotFound'\n const save = saveOf(root)\n if (save._tag === 'Pending') return 'Saving'\n if (save._tag === 'Failed') return 'SaveFailed'\n if (save._tag === 'Applied') return 'Saved'\n if (editor.filled) return 'Editing'\n return read?._tag === 'Failed' ? 'LoadFailed' : 'Loading'\n },\n }\n },\n }\n },\n}\n\nexport { Display, type DisplayColumn, type DisplayKind, type DisplayWords } from './display.js'\nexport { Sort, type SortState, type SortedColumn } from './sort.js'\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "foldkit-crud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|
|
@@ -39,20 +39,21 @@
|
|
|
39
39
|
"LICENSE"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"foldkit-bundle": "0.
|
|
43
|
-
"foldkit-entity": "0.
|
|
42
|
+
"foldkit-bundle": "0.3.0",
|
|
43
|
+
"foldkit-entity": "0.4.0",
|
|
44
|
+
"foldkit-form": "0.2.0",
|
|
44
45
|
"foldkit-metadata": "0.1.0",
|
|
45
|
-
"foldkit-
|
|
46
|
-
"foldkit-
|
|
47
|
-
"foldkit-remote": "0.6.0"
|
|
46
|
+
"foldkit-remote": "0.8.0",
|
|
47
|
+
"foldkit-surface": "0.5.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"effect": "^4.0.0-rc.
|
|
51
|
-
"foldkit": "^0.
|
|
50
|
+
"effect": "^4.0.0-rc.116",
|
|
51
|
+
"foldkit": "^0.163.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"effect": "4.0.0-rc.
|
|
55
|
-
"
|
|
54
|
+
"@effect/platform-browser": "4.0.0-rc.116",
|
|
55
|
+
"effect": "4.0.0-rc.116",
|
|
56
|
+
"foldkit": "0.163.0",
|
|
56
57
|
"tsdown": "^0.23.0",
|
|
57
58
|
"typescript": "5.9.3"
|
|
58
59
|
},
|