@xeplr/ui-factory 1.0.11 → 1.0.12
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 +19 -0
- package/package.json +1 -1
- package/src/designs/ControlView.jsx +5 -2
- package/src/designs/ScreenSample.jsx +20 -7
- package/src/designs/factory.css +4 -0
- package/src/document.js +40 -0
- package/src/hooks.js +23 -3
- package/src/model.js +1 -1
- package/src/useFactoryScreen.js +82 -13
package/README.md
CHANGED
|
@@ -235,6 +235,25 @@ class TaskHooks extends FactoryHooks {
|
|
|
235
235
|
}
|
|
236
236
|
```
|
|
237
237
|
|
|
238
|
+
A screen with branches can **rule steps out** from the same hook — they stay on
|
|
239
|
+
the bar, struck through, and Next and Back pass over them:
|
|
240
|
+
|
|
241
|
+
```js
|
|
242
|
+
step(ctx) {
|
|
243
|
+
if (ctx.from === 0 && ctx.values.kind === 'simple') {
|
|
244
|
+
ctx.disable(['pricing', 'approval']) // by key, label or number
|
|
245
|
+
return 'summary' // and go straight there
|
|
246
|
+
}
|
|
247
|
+
ctx.enable() // every step applies again
|
|
248
|
+
return super.step(ctx)
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
`ctx.go`, `ctx.disable` and `ctx.enable` act on that stepper; `ctx.steps` is the
|
|
253
|
+
same for a screen with more than one (`ctx.steps.disable('approval', stepperId)`).
|
|
254
|
+
`ctx.steps` is on `save` and `get` too, since a save is usually where an app
|
|
255
|
+
learns which branch it is on. "Step 2 of 4" counts only the steps that apply.
|
|
256
|
+
|
|
238
257
|
**A step is not a separate form.** Every field is a column, is checked, and is
|
|
239
258
|
saved, whichever step is showing — a screen still saves itself as it is filled
|
|
240
259
|
in. **Next** holds at a step whose own fields are not filled in, and says which,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xeplr/ui-factory",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "Design data-entry screens on @xeplr/ui-canvas, save them as JSON, run them as auto-saving forms with lists of saved records — built for Claude to draft and people to refine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -341,7 +341,8 @@ function StepperView({ node, p, design, stepper }) {
|
|
|
341
341
|
<div className="xeplr-factory-stepper" style={{ ...fieldStyle(p.style), ...text }}>
|
|
342
342
|
<ol className="xeplr-factory-steps">
|
|
343
343
|
{steps.map((s, i) => {
|
|
344
|
-
const
|
|
344
|
+
const off = Boolean(stepper && stepper.isDisabled && stepper.isDisabled(i))
|
|
345
|
+
const state = off ? 'is-off' : i === active ? 'is-active' : i < active ? 'is-done' : ''
|
|
345
346
|
return (
|
|
346
347
|
<li key={s.key} className={`xeplr-factory-step ${state}`} aria-current={i === active ? 'step' : undefined}>
|
|
347
348
|
<button
|
|
@@ -349,7 +350,9 @@ function StepperView({ node, p, design, stepper }) {
|
|
|
349
350
|
className="xeplr-factory-step-button"
|
|
350
351
|
// On the canvas a press must still be able to drag the control,
|
|
351
352
|
// so the step is taken on click, never on mousedown.
|
|
352
|
-
onClick={(e) => { if (go) { e.stopPropagation(); go(i) } }}
|
|
353
|
+
onClick={(e) => { if (go && !off) { e.stopPropagation(); go(i) } }}
|
|
354
|
+
disabled={off}
|
|
355
|
+
aria-disabled={off || undefined}
|
|
353
356
|
tabIndex={design ? -1 : undefined}
|
|
354
357
|
>
|
|
355
358
|
<span className="xeplr-factory-step-mark" aria-hidden="true">{i < active ? '✓' : (p.showNumbers === false ? '' : i + 1)}</span>
|
|
@@ -29,17 +29,26 @@ const STATUS = {
|
|
|
29
29
|
*/
|
|
30
30
|
function StepNav({ ctrl, node, children }) {
|
|
31
31
|
const at = ctrl.steps.active(node.id)
|
|
32
|
-
const
|
|
32
|
+
const total = ctrl.steps.count(node.id)
|
|
33
|
+
const off = ctrl.steps.disabled(node.id)
|
|
33
34
|
const unfinished = ctrl.steps.fieldsOn(node.id, at).filter((n) => ctrl.allErrors[n.props.name])
|
|
35
|
+
// Back and Next go to the nearest step that is switched on, passing over any
|
|
36
|
+
// the app has ruled out; null means there is none that way.
|
|
37
|
+
const back = ctrl.steps.nextOpen(node.id, 'back')
|
|
38
|
+
const forward = ctrl.steps.nextOpen(node.id, 'next')
|
|
34
39
|
const go = (to, direction) => {
|
|
35
|
-
if (to
|
|
36
|
-
|
|
40
|
+
if (to === null) return
|
|
41
|
+
if (direction === 'next' && unfinished.length) { unfinished.forEach((n) => ctrl.touch(n)); return }
|
|
42
|
+
ctrl.steps.go(node.id, to, direction)
|
|
37
43
|
}
|
|
44
|
+
// "Step 2 of 4" counts the steps that apply, not the ones ruled out.
|
|
45
|
+
const live = total - off.length
|
|
46
|
+
const place = at - off.filter((i) => i < at).length + 1
|
|
38
47
|
return (
|
|
39
48
|
<div className="xeplr-factory-stepnav">
|
|
40
|
-
<button type="button" className="xeplr-factory-secondary" onClick={() => go(
|
|
41
|
-
<span className="xeplr-factory-stepnav-where">Step {
|
|
42
|
-
<button type="button" className="xeplr-factory-primary" onClick={() => go(
|
|
49
|
+
<button type="button" className="xeplr-factory-secondary" onClick={() => go(back, 'back')} disabled={back === null}>Back</button>
|
|
50
|
+
<span className="xeplr-factory-stepnav-where">Step {place} of {live}</span>
|
|
51
|
+
<button type="button" className="xeplr-factory-primary" onClick={() => go(forward, 'next')} disabled={forward === null}>Next</button>
|
|
43
52
|
{children}
|
|
44
53
|
</div>
|
|
45
54
|
)
|
|
@@ -116,7 +125,11 @@ export default function ScreenSample({ ctrl, className, style, renderPopup }) {
|
|
|
116
125
|
error={node.props?.name ? ctrl.errors[node.props.name] : undefined}
|
|
117
126
|
options={CHOICE_TYPES.includes(node.type) ? ctrl.optionsFor(node) : undefined}
|
|
118
127
|
upload={node.type === 'file' ? ctrl.upload : undefined}
|
|
119
|
-
stepper={node.type === 'stepper' ? {
|
|
128
|
+
stepper={node.type === 'stepper' ? {
|
|
129
|
+
active: ctrl.steps.active(node.id),
|
|
130
|
+
isDisabled: (i) => ctrl.steps.isDisabled(node.id, i),
|
|
131
|
+
onStep: (i) => ctrl.steps.go(node.id, i, 'jump')
|
|
132
|
+
} : undefined}
|
|
120
133
|
onChange={(raw) => ctrl.setValue(node, raw)}
|
|
121
134
|
onBlur={() => ctrl.touch(node)}
|
|
122
135
|
list={node.type === 'list' ? {
|
package/src/designs/factory.css
CHANGED
|
@@ -129,6 +129,10 @@
|
|
|
129
129
|
.xeplr-factory-step.is-active .xeplr-factory-step-mark,
|
|
130
130
|
.xeplr-factory-step.is-done .xeplr-factory-step-mark { background: var(--xf-accent); color: #fff; }
|
|
131
131
|
.xeplr-factory-step.is-active .xeplr-factory-step-button { color: var(--xf-accent); font-weight: 600; }
|
|
132
|
+
/* A step the app has ruled out: still shown, so the journey reads the same,
|
|
133
|
+
but plainly not one of the stops. */
|
|
134
|
+
.xeplr-factory-step.is-off .xeplr-factory-step-button { opacity: 0.4; cursor: default; }
|
|
135
|
+
.xeplr-factory-step.is-off .xeplr-factory-step-label { text-decoration: line-through; }
|
|
132
136
|
|
|
133
137
|
/* Back / Next, under the page. */
|
|
134
138
|
.xeplr-factory-stepnav { display: flex; align-items: center; gap: 12px; padding: 10px 0; }
|
package/src/document.js
CHANGED
|
@@ -155,6 +155,46 @@ export function nodesForSteps(doc, active = {}, controls = CONTROLS) {
|
|
|
155
155
|
return (doc.nodes || []).filter((n) => shown(n, new Set()))
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// Which step a move lands on. Pure, and the only place the rules live: a step
|
|
159
|
+
// may be named by its key, its label or its number; a step that is switched
|
|
160
|
+
// off is passed over, in the direction of travel, and only when there is
|
|
161
|
+
// nothing that way is the other direction tried.
|
|
162
|
+
|
|
163
|
+
/** A step named by key, label or number → its index, or -1. */
|
|
164
|
+
export function resolveStep(steps, which) {
|
|
165
|
+
if (Number.isInteger(which)) return which >= 0 && which < steps.length ? which : -1
|
|
166
|
+
return steps.findIndex((s) => s.key === which || s.label === which)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The step actually reached when aiming at `index`.
|
|
171
|
+
* @param disabled indexes switched off
|
|
172
|
+
* @param direction 'next' | 'back' | 'jump'
|
|
173
|
+
* @param from where we are now, for when nothing is open either way
|
|
174
|
+
*/
|
|
175
|
+
export function reachableStep(count, index, disabled = [], direction = 'next', from = 0) {
|
|
176
|
+
if (count <= 0) return 0
|
|
177
|
+
const off = (i) => disabled.includes(i)
|
|
178
|
+
const clamp = (i) => Math.min(Math.max(i, 0), count - 1)
|
|
179
|
+
const way = direction === 'back' ? -1 : 1
|
|
180
|
+
let i = clamp(index)
|
|
181
|
+
while (i >= 0 && i < count && off(i)) i += way
|
|
182
|
+
if (i < 0 || i >= count) {
|
|
183
|
+
i = clamp(index)
|
|
184
|
+
while (i >= 0 && i < count && off(i)) i -= way
|
|
185
|
+
}
|
|
186
|
+
return i >= 0 && i < count ? i : clamp(from)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** The nearest open step in this direction, or null when there is none. */
|
|
190
|
+
export function nextOpenStep(count, at, disabled = [], direction = 'next') {
|
|
191
|
+
const step = direction === 'back' ? -1 : 1
|
|
192
|
+
const aim = at + step
|
|
193
|
+
if (aim < 0 || aim >= count) return null
|
|
194
|
+
const i = reachableStep(count, aim, disabled, direction, at)
|
|
195
|
+
return i === at || disabled.includes(i) ? null : i
|
|
196
|
+
}
|
|
197
|
+
|
|
158
198
|
/** Merges a geometry patch — what the canvas reports on drop. */
|
|
159
199
|
export function moveNode(doc, id, patch) {
|
|
160
200
|
const geometry = {}
|
package/src/hooks.js
CHANGED
|
@@ -42,6 +42,8 @@ export class FactoryHooks {
|
|
|
42
42
|
/**
|
|
43
43
|
* Save — called on every autosave, so keep it quick and safe to repeat.
|
|
44
44
|
* @param values what the form holds
|
|
45
|
+
* @param ctx.steps switch steps off or on as the record decides them —
|
|
46
|
+
* ctx.steps.disable('approval'), ctx.steps.enable()
|
|
45
47
|
* @returns the saved record (its id makes the next save an update)
|
|
46
48
|
*/
|
|
47
49
|
save(values, ctx) {
|
|
@@ -61,14 +63,32 @@ export class FactoryHooks {
|
|
|
61
63
|
* return super.step(ctx)
|
|
62
64
|
* }
|
|
63
65
|
*
|
|
66
|
+
* A screen with branches decides which of them apply from here:
|
|
67
|
+
*
|
|
68
|
+
* step(ctx) {
|
|
69
|
+
* if (ctx.from === 0) {
|
|
70
|
+
* if (ctx.values.kind === 'simple') { ctx.disable(['pricing', 'approval']); return 'summary' }
|
|
71
|
+
* ctx.enable() // every step applies again
|
|
72
|
+
* }
|
|
73
|
+
* return super.step(ctx)
|
|
74
|
+
* }
|
|
75
|
+
*
|
|
64
76
|
* @param ctx.from the step being left, counting from 0
|
|
65
77
|
* @param ctx.to the step asked for
|
|
66
78
|
* @param ctx.direction 'next' | 'back' | 'jump' (a click on the bar)
|
|
67
79
|
* @param ctx.values what the form holds
|
|
68
80
|
* @param ctx.stepper the stepper's node id
|
|
69
|
-
* @
|
|
70
|
-
*
|
|
71
|
-
*
|
|
81
|
+
* @param ctx.go (step) → move there (a key or a number)
|
|
82
|
+
* @param ctx.disable (steps) → switch them off: shown, but passed over by
|
|
83
|
+
* Next and Back and not clickable. A key, a number, or
|
|
84
|
+
* a list of either.
|
|
85
|
+
* @param ctx.enable (steps) → switch them back on; no arguments, all of them
|
|
86
|
+
* @param ctx.steps the same, for a screen with more than one stepper:
|
|
87
|
+
* disable(which, stepperId), active(id), disabled(id)
|
|
88
|
+
* @returns false to stay where you are, a step (its key or its number) to go
|
|
89
|
+
* somewhere else, anything else to move as asked. The step's own
|
|
90
|
+
* required fields are checked before this runs, so this is for your
|
|
91
|
+
* rules, not theirs.
|
|
72
92
|
*/
|
|
73
93
|
step(ctx) {
|
|
74
94
|
return true
|
package/src/model.js
CHANGED
|
@@ -8,7 +8,7 @@ export {
|
|
|
8
8
|
DOCUMENT_KIND, DOCUMENT_VERSION, DEFAULT_ASPECT, DEFAULT_WIDTH, MARGIN, GAP,
|
|
9
9
|
createScreen, renameScreen, setScreenProperty, addControl, moveNode, setNodeProperty, getNodeProperty, removeNodes,
|
|
10
10
|
inputNodes, readingOrder, contentBottom, camelName, slugify, uniqueFieldName, nextPosition,
|
|
11
|
-
steppers, stepsOf, stepOf, setNodeStep, nodesForSteps
|
|
11
|
+
steppers, stepsOf, stepOf, setNodeStep, nodesForSteps, resolveStep, reachableStep, nextOpenStep
|
|
12
12
|
} from './document.js'
|
|
13
13
|
export { validateDocument, assertValidDocument } from './validateDocument.js'
|
|
14
14
|
export { formSchema, initialValues, parseInput, validateValues, fieldError, sameId, optionValue, chooseable, saveState, recordValues, displayValue, fileLabel, toDbValue, fromDbValue, listColumns, listSource } from './values.js'
|
package/src/useFactoryScreen.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import { CONTROLS, CHOICE_TYPES } from './controls.js'
|
|
3
|
-
import { inputNodes, nodesForSteps, steppers, stepsOf, stepOf } from './document.js'
|
|
3
|
+
import { inputNodes, nodesForSteps, steppers, stepsOf, stepOf, resolveStep, reachableStep, nextOpenStep } from './document.js'
|
|
4
4
|
import { validateDocument } from './validateDocument.js'
|
|
5
5
|
import { initialValues, parseInput, saveState, fieldError, optionValue, recordValues, listSource } from './values.js'
|
|
6
6
|
import { hookMethod } from './hooks.js'
|
|
@@ -152,6 +152,9 @@ export function useFactoryScreen({
|
|
|
152
152
|
// the fields of the steps not showing are still filled in, still checked and
|
|
153
153
|
// still saved — a step is what the person sees, not a separate form.
|
|
154
154
|
const [openSteps, setOpenSteps] = useState({})
|
|
155
|
+
// Steps the app has switched off — { stepper id: [index, …] }. A screen may
|
|
156
|
+
// have branches; which of them apply is the app's to say, from its hooks.
|
|
157
|
+
const [disabledSteps, setDisabledSteps] = useState({})
|
|
155
158
|
const stepperNodes = useMemo(() => (check.ok ? steppers(doc, controls) : []), [check.ok, doc, controls])
|
|
156
159
|
const visibleNodes = useMemo(() => (check.ok ? nodesForSteps(doc, openSteps, controls) : []), [check.ok, doc, openSteps, controls])
|
|
157
160
|
|
|
@@ -173,42 +176,104 @@ export function useFactoryScreen({
|
|
|
173
176
|
|
|
174
177
|
const stepControl = useMemo(() => {
|
|
175
178
|
const at = (id) => (Number.isInteger(openSteps[id]) ? openSteps[id] : 0)
|
|
176
|
-
|
|
179
|
+
const only = () => (stepperNodes.length === 1 ? stepperNodes[0].id : null)
|
|
180
|
+
const nodeFor = (id) => stepperNodes.find((n) => n.id === id) || stepperNodes[0] || null
|
|
181
|
+
const lastOf = (id) => stepsOf(nodeFor(id)).length - 1
|
|
182
|
+
/** A step named by its key ("planning"), its label, or its number from 0. */
|
|
183
|
+
const indexOf = (id, which) => resolveStep(stepsOf(nodeFor(id)), which)
|
|
184
|
+
const offList = (id) => disabledSteps[id] || []
|
|
185
|
+
const isOff = (id, index) => offList(id).includes(index)
|
|
186
|
+
const reachable = (id, index, direction) => reachableStep(lastOf(id) + 1, index, offList(id), direction, at(id))
|
|
187
|
+
const setOff = (id, list) => setDisabledSteps((d) => ({ ...d, [id || only()]: list }))
|
|
188
|
+
|
|
189
|
+
const control = {
|
|
177
190
|
nodes: stepperNodes,
|
|
178
191
|
active: at,
|
|
179
|
-
count: (id) => stepsOf(
|
|
192
|
+
count: (id) => stepsOf(nodeFor(id)).length,
|
|
193
|
+
labels: (id) => stepsOf(nodeFor(id)),
|
|
194
|
+
indexOf,
|
|
195
|
+
/** Is this step switched off — shown, but not one you can be on? */
|
|
196
|
+
isDisabled: (id, index) => isOff(id, index),
|
|
197
|
+
disabled: (id) => (disabledSteps[id || only()] || []).slice(),
|
|
198
|
+
/**
|
|
199
|
+
* Switch steps off (a key, a number, or a list of them). A step that is
|
|
200
|
+
* off cannot be reached by Next, Back or a click, and Next passes over
|
|
201
|
+
* it. Switching off the step someone is on moves them to the nearest one
|
|
202
|
+
* that is open.
|
|
203
|
+
*/
|
|
204
|
+
disable: (which, id) => {
|
|
205
|
+
const on = id || only()
|
|
206
|
+
if (!on) return []
|
|
207
|
+
const add = (Array.isArray(which) ? which : [which]).map((w) => indexOf(on, w)).filter((i) => i >= 0)
|
|
208
|
+
const next = [...new Set([...(disabledSteps[on] || []), ...add])]
|
|
209
|
+
setOff(on, next)
|
|
210
|
+
return next
|
|
211
|
+
},
|
|
212
|
+
/** Switch them back on. No arguments switches every step of it back on. */
|
|
213
|
+
enable: (which, id) => {
|
|
214
|
+
const on = id || only()
|
|
215
|
+
if (!on) return []
|
|
216
|
+
if (which === undefined) { setOff(on, []); return [] }
|
|
217
|
+
const drop = (Array.isArray(which) ? which : [which]).map((w) => indexOf(on, w))
|
|
218
|
+
const next = (disabledSteps[on] || []).filter((i) => !drop.includes(i))
|
|
219
|
+
setOff(on, next)
|
|
220
|
+
return next
|
|
221
|
+
},
|
|
180
222
|
/**
|
|
181
223
|
* Move, if the app's `step` hook lets it: false keeps the person where
|
|
182
224
|
* they are, a number sends them somewhere else. The hook runs for Next,
|
|
183
225
|
* Back and a click on the bar alike, so a rule cannot be walked around.
|
|
184
226
|
*/
|
|
185
227
|
go: async (id, index, direction) => {
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
228
|
+
const on = id || only()
|
|
229
|
+
const from = at(on)
|
|
230
|
+
const way = direction || (index > from ? 'next' : 'back')
|
|
231
|
+
const to = reachable(on, indexOf(on, index), way)
|
|
189
232
|
if (to === from) return from
|
|
190
233
|
const decide = hookMethod(hooksRef.current, 'step')
|
|
191
234
|
const answer = await decide({
|
|
192
235
|
from,
|
|
193
236
|
to,
|
|
194
|
-
direction:
|
|
237
|
+
direction: way,
|
|
195
238
|
values: live.current.values,
|
|
196
|
-
stepper:
|
|
239
|
+
stepper: on,
|
|
197
240
|
screen: doc.id,
|
|
198
|
-
document: doc
|
|
241
|
+
document: doc,
|
|
242
|
+
// What the hook can DO from here: send them somewhere, or say which
|
|
243
|
+
// steps this answer makes beside the point.
|
|
244
|
+
steps: control,
|
|
245
|
+
go: (where) => control.go(on, where, 'jump'),
|
|
246
|
+
disable: (which) => control.disable(which, on),
|
|
247
|
+
enable: (which) => control.enable(which, on)
|
|
199
248
|
})
|
|
200
249
|
if (answer === false) return from
|
|
201
|
-
const
|
|
202
|
-
|
|
250
|
+
const asked = Number.isInteger(answer) || typeof answer === 'string' ? indexOf(on, answer) : to
|
|
251
|
+
const where = reachable(on, asked >= 0 ? asked : to, way)
|
|
252
|
+
setOpenSteps((o) => ({ ...o, [on]: where }))
|
|
203
253
|
return where
|
|
204
254
|
},
|
|
255
|
+
/** The next step in this direction that is open, for Back and Next. */
|
|
256
|
+
nextOpen: (id, direction) => {
|
|
257
|
+
const on = id || only()
|
|
258
|
+
return nextOpenStep(lastOf(on) + 1, at(on), offList(on), direction)
|
|
259
|
+
},
|
|
205
260
|
/** The fields on this step, so Next can hold at a step that is not filled in. */
|
|
206
261
|
fieldsOn: (id, index) => inputNodes(doc, controls).filter((n) => {
|
|
207
262
|
const step = stepOf(n)
|
|
208
|
-
return step && step.of === id && step.index === index
|
|
263
|
+
return step && step.of === (id || only()) && step.index === index
|
|
209
264
|
})
|
|
210
265
|
}
|
|
211
|
-
|
|
266
|
+
return control
|
|
267
|
+
}, [stepperNodes, openSteps, disabledSteps, doc, controls])
|
|
268
|
+
|
|
269
|
+
live.current.steps = stepControl
|
|
270
|
+
|
|
271
|
+
// A step that is switched off cannot be the one showing.
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
stepperNodes.forEach((n) => {
|
|
274
|
+
if (stepControl.isDisabled(n.id, stepControl.active(n.id))) stepControl.go(n.id, stepControl.active(n.id) + 1, 'next')
|
|
275
|
+
})
|
|
276
|
+
}, [stepperNodes, stepControl])
|
|
212
277
|
|
|
213
278
|
// ── a file field's upload ─────────────────────────────────────────────
|
|
214
279
|
const uploadRef = useRef(uploadFile); uploadRef.current = uploadFile
|
|
@@ -257,6 +322,9 @@ export function useFactoryScreen({
|
|
|
257
322
|
const saved = await hook('save')(l.values, {
|
|
258
323
|
...meta,
|
|
259
324
|
isNew: l.recordId === null || l.recordId === undefined,
|
|
325
|
+
// Save is where an app usually learns which branch it is on, so it
|
|
326
|
+
// can switch steps off from here as well as from step().
|
|
327
|
+
steps: live.current.steps,
|
|
260
328
|
defaults: { save: (vals) => (l.onSave ? l.onSave(vals, meta) : undefined) }
|
|
261
329
|
})
|
|
262
330
|
// A new record's first save creates it; its id makes the next save an update.
|
|
@@ -376,6 +444,7 @@ export function useFactoryScreen({
|
|
|
376
444
|
setLists((l) => ({ ...l, [node.id]: { rows: l[node.id]?.rows || [], loading: true, error: null } }))
|
|
377
445
|
const run = Promise.resolve().then(() => hook('get')({
|
|
378
446
|
many: true, id: null, screen: doc.id, source: src, node, document: doc,
|
|
447
|
+
steps: live.current.steps,
|
|
379
448
|
defaults: {
|
|
380
449
|
get: () => {
|
|
381
450
|
if (!fetchRecordsRef.current) throw new Error(`No fetchRecords was provided to list "${src}"`)
|