@nan0web/ui 1.0.4 → 1.3.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 +19 -14
- package/package.json +6 -4
- package/src/App/Command/DepsCommand.js +25 -0
- package/src/App/Core/CoreApp.js +18 -17
- package/src/App/Core/UI.js +12 -19
- package/src/App/Core/Widget.js +6 -10
- package/src/App/Core/index.js +3 -3
- package/src/App/Scenario.js +4 -4
- package/src/App/User/Command/Message.js +2 -29
- package/src/App/User/Command/index.js +3 -4
- package/src/App/User/UserApp.js +30 -23
- package/src/App/User/UserUI.js +2 -2
- package/src/App/User/index.js +2 -2
- package/src/App/index.js +5 -10
- package/src/Component/Process/Input.js +10 -17
- package/src/Component/Process/Process.js +3 -5
- package/src/Component/Process/index.js +2 -2
- package/src/Component/SortableList/SortableList.js +100 -0
- package/src/Component/SortableList/index.js +3 -0
- package/src/Component/Welcome/Input.js +2 -4
- package/src/Component/Welcome/Welcome.js +5 -9
- package/src/Component/Welcome/index.js +2 -2
- package/src/Component/index.js +5 -3
- package/src/Frame/Frame.js +163 -146
- package/src/Frame/Props.js +20 -20
- package/src/Locale.js +17 -18
- package/src/Model/User/User.js +3 -6
- package/src/Model/index.js +1 -1
- package/src/README.md.js +84 -94
- package/src/StdIn.js +8 -12
- package/src/StdOut.js +23 -27
- package/src/View/RenderOptions.js +1 -1
- package/src/View/View.js +42 -38
- package/src/core/Error/CancelError.js +2 -2
- package/src/core/Error/index.js +3 -5
- package/src/core/Flow.js +347 -0
- package/src/core/Form/Form.js +35 -33
- package/src/core/Form/Input.js +29 -14
- package/src/core/Form/Message.js +3 -6
- package/src/core/Form/index.js +4 -8
- package/src/core/InputAdapter.js +4 -6
- package/src/core/Message/Message.js +9 -12
- package/src/core/Message/OutputMessage.js +19 -17
- package/src/core/Message/index.js +2 -2
- package/src/core/OutputAdapter.js +12 -10
- package/src/core/Stream.js +4 -3
- package/src/core/StreamEntry.js +2 -2
- package/src/core/UiAdapter.js +57 -29
- package/src/core/index.js +38 -9
- package/src/functions.js +8 -15
- package/src/index.js +21 -32
- package/types/App/Command/DepsCommand.d.ts +16 -0
- package/types/App/Command/Options.d.ts +37 -40
- package/types/App/Command/index.d.ts +6 -6
- package/types/App/Core/CoreApp.d.ts +2 -2
- package/types/App/Core/UI.d.ts +6 -7
- package/types/App/Core/Widget.d.ts +4 -4
- package/types/App/Core/index.d.ts +3 -3
- package/types/App/Scenario.d.ts +1 -1
- package/types/App/User/Command/Message.d.ts +2 -16
- package/types/App/User/Command/Options.d.ts +29 -29
- package/types/App/User/Command/index.d.ts +2 -3
- package/types/App/User/UserApp.d.ts +5 -5
- package/types/App/User/index.d.ts +2 -2
- package/types/App/index.d.ts +4 -4
- package/types/Component/Process/Process.d.ts +2 -2
- package/types/Component/Process/index.d.ts +2 -2
- package/types/Component/SortableList/SortableList.d.ts +58 -0
- package/types/Component/SortableList/index.d.ts +2 -0
- package/types/Component/Welcome/Input.d.ts +1 -1
- package/types/Component/Welcome/Welcome.d.ts +1 -1
- package/types/Component/Welcome/index.d.ts +2 -2
- package/types/Component/index.d.ts +5 -3
- package/types/Frame/Frame.d.ts +1 -1
- package/types/Frame/Props.d.ts +1 -1
- package/types/Model/index.d.ts +1 -1
- package/types/StdIn.d.ts +2 -2
- package/types/StdOut.d.ts +1 -1
- package/types/View/View.d.ts +7 -7
- package/types/core/Error/index.d.ts +1 -1
- package/types/core/Flow.d.ts +320 -0
- package/types/core/Form/Form.d.ts +2 -2
- package/types/core/Form/Input.d.ts +15 -4
- package/types/core/Form/Message.d.ts +1 -1
- package/types/core/Form/index.d.ts +3 -3
- package/types/core/InputAdapter.d.ts +2 -2
- package/types/core/Intent.d.ts +65 -68
- package/types/core/Message/InputMessage.d.ts +65 -65
- package/types/core/Message/Message.d.ts +1 -1
- package/types/core/Message/OutputMessage.d.ts +1 -1
- package/types/core/Message/index.d.ts +2 -2
- package/types/core/Stream.d.ts +1 -2
- package/types/core/UiAdapter.d.ts +22 -4
- package/types/core/index.d.ts +5 -2
- package/types/index.d.ts +10 -10
package/src/View/View.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { empty, equal, typeOf } from
|
|
2
|
-
import Frame, { FrameRenderMethod } from
|
|
3
|
-
import Locale from
|
|
4
|
-
import StdOut from
|
|
5
|
-
import StdIn from
|
|
6
|
-
import RenderOptions from
|
|
7
|
-
import UiMessage from
|
|
1
|
+
import { empty, equal, typeOf } from '@nan0web/types'
|
|
2
|
+
import Frame, { FrameRenderMethod } from '../Frame/Frame.js'
|
|
3
|
+
import Locale from '../Locale.js'
|
|
4
|
+
import StdOut from '../StdOut.js'
|
|
5
|
+
import StdIn from '../StdIn.js'
|
|
6
|
+
import RenderOptions from './RenderOptions.js'
|
|
7
|
+
import UiMessage from '../core/Message/Message.js'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* @typedef {Object} ComponentFn
|
|
@@ -54,7 +54,7 @@ export default class View {
|
|
|
54
54
|
stdout = new StdOut(),
|
|
55
55
|
startedAt = Date.now(),
|
|
56
56
|
frame = new Frame(),
|
|
57
|
-
locale = Locale.from(
|
|
57
|
+
locale = Locale.from('uk-UA'),
|
|
58
58
|
vocab = new Map(),
|
|
59
59
|
windowSize = [0, 0],
|
|
60
60
|
components = new Map(),
|
|
@@ -103,14 +103,18 @@ export default class View {
|
|
|
103
103
|
const [width, height] = this.getWindowSize()
|
|
104
104
|
options = this.RenderOptions.from({
|
|
105
105
|
...options,
|
|
106
|
-
renderMethod: this.renderMethod,
|
|
106
|
+
renderMethod: this.renderMethod,
|
|
107
|
+
width,
|
|
108
|
+
height,
|
|
107
109
|
// @ts-ignore
|
|
108
110
|
})
|
|
109
|
-
const renderFn =
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
:
|
|
111
|
+
const renderFn =
|
|
112
|
+
'function' === typeof shouldRender // no errors.
|
|
113
|
+
? // const renderFn = typeOf(Function)(shouldRender) // Property 'bind' does not exist on type 'number | boolean | Function'.
|
|
114
|
+
shouldRender.bind(this)
|
|
115
|
+
: 'string' === typeof shouldRender
|
|
116
|
+
? this.components.get(shouldRender)?.bind(this)
|
|
117
|
+
: null
|
|
114
118
|
|
|
115
119
|
return (value, ...args) => {
|
|
116
120
|
if (renderFn) {
|
|
@@ -137,7 +141,7 @@ export default class View {
|
|
|
137
141
|
let frame = Frame.from({ ...options, value })
|
|
138
142
|
frame = View.fixFrame(frame, options)
|
|
139
143
|
let clearFrame = false
|
|
140
|
-
if (String(frame.value[0] ??
|
|
144
|
+
if (String(frame.value[0] ?? '') === Frame.BOF) {
|
|
141
145
|
frame.value = frame.value.slice(1)
|
|
142
146
|
clearFrame = true
|
|
143
147
|
}
|
|
@@ -179,9 +183,9 @@ export default class View {
|
|
|
179
183
|
|
|
180
184
|
t(value) {
|
|
181
185
|
if (typeOf(Array)(value)) {
|
|
182
|
-
value = value.map(row => {
|
|
186
|
+
value = value.map((row) => {
|
|
183
187
|
if (typeOf(Array)(row)) {
|
|
184
|
-
return row.map(col => {
|
|
188
|
+
return row.map((col) => {
|
|
185
189
|
return this.vocab.has(col) ? this.vocab.get(col) : col
|
|
186
190
|
})
|
|
187
191
|
}
|
|
@@ -193,31 +197,32 @@ export default class View {
|
|
|
193
197
|
}
|
|
194
198
|
|
|
195
199
|
debug(...args) {
|
|
196
|
-
return this.render(1)(
|
|
197
|
-
[StdOut.STYLES.dim,
|
|
198
|
-
"Debug: ", args.join(" "), Frame.EOL, StdOut.RESET],
|
|
199
|
-
)
|
|
200
|
+
return this.render(1)([StdOut.STYLES.dim, 'Debug: ', args.join(' '), Frame.EOL, StdOut.RESET])
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
info(...args) {
|
|
203
|
-
return this.render(1)(
|
|
204
|
-
[StdOut.COLORS.green,
|
|
205
|
-
"Info : ", args.join(" "), Frame.EOL, StdOut.RESET],
|
|
206
|
-
)
|
|
204
|
+
return this.render(1)([StdOut.COLORS.green, 'Info : ', args.join(' '), Frame.EOL, StdOut.RESET])
|
|
207
205
|
}
|
|
208
206
|
|
|
209
207
|
warn(...args) {
|
|
210
|
-
return this.render(1)(
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
return this.render(1)([
|
|
209
|
+
StdOut.COLORS.yellow,
|
|
210
|
+
'Warn : ',
|
|
211
|
+
args.join(' '),
|
|
212
|
+
Frame.EOL,
|
|
213
|
+
StdOut.RESET,
|
|
214
|
+
])
|
|
214
215
|
}
|
|
215
216
|
|
|
216
217
|
error(...args) {
|
|
217
|
-
return this.render(1)(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
return this.render(1)([
|
|
219
|
+
StdOut.COLORS.red,
|
|
220
|
+
StdOut.STYLES.bold,
|
|
221
|
+
'Error: ',
|
|
222
|
+
args.join(' '),
|
|
223
|
+
Frame.EOL,
|
|
224
|
+
StdOut.RESET,
|
|
225
|
+
])
|
|
221
226
|
}
|
|
222
227
|
|
|
223
228
|
/**
|
|
@@ -225,7 +230,7 @@ export default class View {
|
|
|
225
230
|
* @param {ComponentFn} component
|
|
226
231
|
*/
|
|
227
232
|
register(name, component) {
|
|
228
|
-
if (undefined === component &&
|
|
233
|
+
if (undefined === component && 'function' === typeof name) {
|
|
229
234
|
component = name
|
|
230
235
|
name = component.name
|
|
231
236
|
}
|
|
@@ -260,7 +265,7 @@ export default class View {
|
|
|
260
265
|
* @returns {Promise<UiMessage | null>}
|
|
261
266
|
*/
|
|
262
267
|
async ask(input) {
|
|
263
|
-
const name = input.constructor.name.replace(/Input$/,
|
|
268
|
+
const name = input.constructor.name.replace(/Input$/, '')
|
|
264
269
|
const component = this.get(name)
|
|
265
270
|
if (component) {
|
|
266
271
|
return await component.ask.apply(this, [input])
|
|
@@ -269,8 +274,8 @@ export default class View {
|
|
|
269
274
|
do {
|
|
270
275
|
const answer = await this.stdin.read()
|
|
271
276
|
result = /** @type {typeof UiMessage} */ (input.constructor).from(answer)
|
|
272
|
-
} while (!result.isValid && !result.
|
|
273
|
-
return result.
|
|
277
|
+
} while (!result.isValid && !result.head.cancelled)
|
|
278
|
+
return result.head.cancelled ? null : result
|
|
274
279
|
}
|
|
275
280
|
|
|
276
281
|
/**
|
|
@@ -285,5 +290,4 @@ export default class View {
|
|
|
285
290
|
// @todo add multiline visibility, for instance extended frame row into rows if it's wider than width.
|
|
286
291
|
return frame
|
|
287
292
|
}
|
|
288
|
-
|
|
289
293
|
}
|
package/src/core/Error/index.js
CHANGED
package/src/core/Flow.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Universal Flow Runner for yield-based UI architecture.
|
|
3
|
+
*
|
|
4
|
+
* The Flow pattern enables "One Logic, Many UI" by separating business logic
|
|
5
|
+
* from presentation. A Flow is an async generator that yields Components,
|
|
6
|
+
* which are then rendered by platform-specific Adapters.
|
|
7
|
+
*
|
|
8
|
+
* @module @nan0web/ui/core/Flow
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import CancelError from './Error/CancelError.js'
|
|
12
|
+
|
|
13
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
14
|
+
// TYPES (JSDoc for pure JavaScript, TypeScript-compatible)
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Component types that can be yielded from a Flow.
|
|
19
|
+
*
|
|
20
|
+
* @typedef {'view' | 'prompt' | 'stream' | 'action' | 'flow'} ComponentType
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Base interface for all UI components.
|
|
25
|
+
*
|
|
26
|
+
* @typedef {Object} FlowComponent
|
|
27
|
+
* @property {ComponentType} type - Component type discriminator.
|
|
28
|
+
* @property {string} [name] - Optional component name for debugging.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Static view component (no user input required).
|
|
33
|
+
* Examples: Alert, Badge, Toast, Table, Text
|
|
34
|
+
*
|
|
35
|
+
* @typedef {Object} ViewComponent
|
|
36
|
+
* @property {'view'} type - Always 'view'.
|
|
37
|
+
* @property {string} name - Component name (e.g., 'Alert', 'Toast').
|
|
38
|
+
* @property {Object} props - Component-specific properties.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Interactive prompt component (requires user input).
|
|
43
|
+
* Examples: Input, Select, Confirm, Multiselect, Mask
|
|
44
|
+
*
|
|
45
|
+
* @typedef {Object} PromptComponent
|
|
46
|
+
* @property {'prompt'} type - Always 'prompt'.
|
|
47
|
+
* @property {string} name - Component name (e.g., 'Select', 'Input').
|
|
48
|
+
* @property {Object} props - Component-specific properties.
|
|
49
|
+
* @property {string} [props.message] - Prompt message to display.
|
|
50
|
+
* @property {any[]} [props.choices] - Options for Select/Multiselect.
|
|
51
|
+
* @property {Function} [props.validate] - Validation function.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Streaming component for progress/async operations.
|
|
56
|
+
* Examples: Spinner, ProgressBar, StreamChunk
|
|
57
|
+
*
|
|
58
|
+
* @typedef {Object} StreamComponent
|
|
59
|
+
* @property {'stream'} type - Always 'stream'.
|
|
60
|
+
* @property {string} name - Component name (e.g., 'Spinner', 'Progress').
|
|
61
|
+
* @property {Object} props - Component-specific properties.
|
|
62
|
+
* @property {AsyncIterable} [iterable] - Async iterator for streaming content.
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Action component for physical or side-effect operations.
|
|
67
|
+
* Examples: Move, Sound, Light, Notify
|
|
68
|
+
*
|
|
69
|
+
* @typedef {Object} ActionComponent
|
|
70
|
+
* @property {'action'} type - Always 'action'.
|
|
71
|
+
* @property {string} name - Component name (e.g., 'Move', 'Beep').
|
|
72
|
+
* @property {Object} props - Component-specific properties.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Result returned by prompt components.
|
|
77
|
+
*
|
|
78
|
+
* @typedef {Object} PromptResult
|
|
79
|
+
* @property {any} value - The value entered/selected by user.
|
|
80
|
+
* @property {boolean} [cancelled] - True if user cancelled the prompt.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Adapter interface that all platform adapters must implement.
|
|
85
|
+
*
|
|
86
|
+
* @typedef {Object} FlowAdapter
|
|
87
|
+
* @property {(component: ViewComponent) => void | Promise<void>} renderView
|
|
88
|
+
* Renders a static view component.
|
|
89
|
+
* @property {(component: PromptComponent) => Promise<PromptResult>} executePrompt
|
|
90
|
+
* Execute an interactive prompt and returns user input.
|
|
91
|
+
* @property {(component: StreamComponent) => AsyncIterable} streamProgress
|
|
92
|
+
* Handles streaming components.
|
|
93
|
+
* @property {(component: ActionComponent) => Promise<any>} [executeAction]
|
|
94
|
+
* Executes an action component (physical or side-effect).
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
98
|
+
// COMPONENT FACTORIES
|
|
99
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Creates a View component.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} name - Component name (e.g., 'Alert', 'Toast').
|
|
105
|
+
* @param {Object} props - Component properties.
|
|
106
|
+
* @returns {ViewComponent}
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* yield View('Alert', { variant: 'success', message: 'Done!' })
|
|
110
|
+
*/
|
|
111
|
+
export function View(name, props = {}) {
|
|
112
|
+
return { type: 'view', name, props }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Creates a Prompt component.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} name - Component name (e.g., 'Select', 'Input').
|
|
119
|
+
* @param {Object} props - Component properties.
|
|
120
|
+
* @returns {PromptComponent}
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const value = yield Prompt('Select', { message: 'Choose:', choices: ['a', 'b'] })
|
|
124
|
+
*/
|
|
125
|
+
export function Prompt(name, props = {}) {
|
|
126
|
+
return { type: 'prompt', name, props }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Creates a Stream component.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} name - Component name (e.g., 'Spinner', 'Progress').
|
|
133
|
+
* @param {Object} props - Component properties.
|
|
134
|
+
* @returns {StreamComponent}
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* yield* Stream('Progress', { total: 100, current: 50 })
|
|
138
|
+
*/
|
|
139
|
+
export function Stream(name, props = {}) {
|
|
140
|
+
return { type: 'stream', name, props }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Creates an Action component.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} name - Action name.
|
|
147
|
+
* @param {Object} props - Action properties.
|
|
148
|
+
* @returns {ActionComponent}
|
|
149
|
+
*/
|
|
150
|
+
export function Action(name, props = {}) {
|
|
151
|
+
return { type: 'action', name, props }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
155
|
+
// CONVENIENCE FACTORIES (Pre-defined components)
|
|
156
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
157
|
+
|
|
158
|
+
/** @param {Object} props */
|
|
159
|
+
export const Alert = (props) => View('Alert', props)
|
|
160
|
+
|
|
161
|
+
/** @param {Object} props */
|
|
162
|
+
export const Toast = (props) => View('Toast', props)
|
|
163
|
+
|
|
164
|
+
/** @param {Object} props */
|
|
165
|
+
export const Badge = (props) => View('Badge', props)
|
|
166
|
+
|
|
167
|
+
/** @param {Object} props */
|
|
168
|
+
export const Text = (props) => View('Text', props)
|
|
169
|
+
|
|
170
|
+
/** @param {Object} props */
|
|
171
|
+
export const Table = (props) => View('Table', props)
|
|
172
|
+
|
|
173
|
+
/** @param {Object} props */
|
|
174
|
+
export const Input = (props) => Prompt('Input', props)
|
|
175
|
+
|
|
176
|
+
/** @param {Object} props */
|
|
177
|
+
export const Select = (props) => Prompt('Select', props)
|
|
178
|
+
|
|
179
|
+
/** @param {Object} props */
|
|
180
|
+
export const Confirm = (props) => Prompt('Confirm', props)
|
|
181
|
+
|
|
182
|
+
/** @param {Object} props */
|
|
183
|
+
export const Multiselect = (props) => Prompt('Multiselect', props)
|
|
184
|
+
|
|
185
|
+
/** @param {Object} props */
|
|
186
|
+
export const Mask = (props) => Prompt('Mask', props)
|
|
187
|
+
|
|
188
|
+
/** @param {Object} props */
|
|
189
|
+
export const Password = (props) => Prompt('Password', props)
|
|
190
|
+
|
|
191
|
+
/** @param {Object} props */
|
|
192
|
+
export const Spinner = (props) => Stream('Spinner', props)
|
|
193
|
+
|
|
194
|
+
/** @param {Object} props */
|
|
195
|
+
export const Progress = (props) => Stream('Progress', props)
|
|
196
|
+
|
|
197
|
+
/** @param {Object} props */
|
|
198
|
+
export const Beep = (props) => Action('Beep', props)
|
|
199
|
+
|
|
200
|
+
/** @param {Object} props */
|
|
201
|
+
export const Move = (props) => Action('Move', props)
|
|
202
|
+
|
|
203
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
204
|
+
// FLOW RUNNER
|
|
205
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Runs a Flow (async generator) with the provided adapter.
|
|
209
|
+
*
|
|
210
|
+
* The Flow runner iterates through yielded components and dispatches them
|
|
211
|
+
* to the appropriate adapter method based on component type.
|
|
212
|
+
*
|
|
213
|
+
* @param {AsyncGenerator} flow - The flow generator to execute.
|
|
214
|
+
* @param {FlowAdapter} adapter - The platform-specific adapter.
|
|
215
|
+
* @param {Object} [options={}] - Additional options.
|
|
216
|
+
* @param {AbortSignal} [options.signal] - Abort signal for cancellation.
|
|
217
|
+
* @returns {Promise<any>} The final return value of the flow.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* async function* loginFlow() {
|
|
221
|
+
* yield Alert({ message: 'Welcome!' })
|
|
222
|
+
* const username = yield Input({ message: 'Username:' })
|
|
223
|
+
* const password = yield Password({ message: 'Password:' })
|
|
224
|
+
* return { username, password }
|
|
225
|
+
* }
|
|
226
|
+
*
|
|
227
|
+
* const result = await runFlow(loginFlow(), cliAdapter)
|
|
228
|
+
*/
|
|
229
|
+
export async function runFlow(flow, adapter, options = {}) {
|
|
230
|
+
const { signal } = options
|
|
231
|
+
let nextValue = undefined
|
|
232
|
+
|
|
233
|
+
while (true) {
|
|
234
|
+
// Check for abort
|
|
235
|
+
if (signal?.aborted) {
|
|
236
|
+
throw new CancelError('Flow aborted')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Get next component from flow
|
|
240
|
+
const { value: component, done } = await flow.next(nextValue)
|
|
241
|
+
|
|
242
|
+
if (done) {
|
|
243
|
+
return component // Final return value
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Handle null/undefined yields
|
|
247
|
+
if (!component) {
|
|
248
|
+
nextValue = undefined
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Dispatch based on component type
|
|
253
|
+
switch (component.type) {
|
|
254
|
+
case 'view':
|
|
255
|
+
await adapter.renderView?.(component)
|
|
256
|
+
nextValue = undefined
|
|
257
|
+
break
|
|
258
|
+
|
|
259
|
+
case 'prompt': {
|
|
260
|
+
const result = await adapter.executePrompt?.(component)
|
|
261
|
+
if (result?.cancelled) {
|
|
262
|
+
throw new CancelError('User cancelled prompt')
|
|
263
|
+
}
|
|
264
|
+
nextValue = result?.value
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
case 'stream':
|
|
269
|
+
// For streams, we iterate and yield each chunk
|
|
270
|
+
if (adapter.streamProgress) {
|
|
271
|
+
for await (const chunk of adapter.streamProgress(component)) {
|
|
272
|
+
// Optionally allow flow to react to stream chunks
|
|
273
|
+
// by using a specific protocol if needed
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
nextValue = undefined
|
|
277
|
+
break
|
|
278
|
+
|
|
279
|
+
case 'action':
|
|
280
|
+
nextValue = await adapter.executeAction?.(component)
|
|
281
|
+
break
|
|
282
|
+
|
|
283
|
+
case 'flow':
|
|
284
|
+
// Nested flow - recursive execution
|
|
285
|
+
nextValue = await runFlow(component.generator, adapter, options)
|
|
286
|
+
break
|
|
287
|
+
|
|
288
|
+
default:
|
|
289
|
+
// Unknown component type - try to render as view
|
|
290
|
+
if (typeof component === 'string') {
|
|
291
|
+
await adapter.renderView?.({ type: 'view', name: 'Text', props: { content: component } })
|
|
292
|
+
} else if (typeof component.toString === 'function') {
|
|
293
|
+
await adapter.renderView?.({
|
|
294
|
+
type: 'view',
|
|
295
|
+
name: 'Text',
|
|
296
|
+
props: { content: String(component) },
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
nextValue = undefined
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Wraps a nested flow for composition with yield*.
|
|
306
|
+
*
|
|
307
|
+
* @param {Function} flowFn - Flow generator function.
|
|
308
|
+
* @param {...any} args - Arguments to pass to the flow function.
|
|
309
|
+
* @returns {Object} A flow component.
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* async function* mainFlow() {
|
|
313
|
+
* yield Alert({ message: 'Starting...' })
|
|
314
|
+
* const user = yield* flow(loginFlow)
|
|
315
|
+
* yield Alert({ message: `Welcome, ${user.name}!` })
|
|
316
|
+
* }
|
|
317
|
+
*/
|
|
318
|
+
export function flow(flowFn, ...args) {
|
|
319
|
+
return { type: 'flow', generator: flowFn(...args) }
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
323
|
+
// EXPORTS
|
|
324
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
325
|
+
|
|
326
|
+
export default {
|
|
327
|
+
runFlow,
|
|
328
|
+
flow,
|
|
329
|
+
// Factories
|
|
330
|
+
View,
|
|
331
|
+
Prompt,
|
|
332
|
+
Stream,
|
|
333
|
+
// Components
|
|
334
|
+
Alert,
|
|
335
|
+
Toast,
|
|
336
|
+
Badge,
|
|
337
|
+
Text,
|
|
338
|
+
Table,
|
|
339
|
+
Input,
|
|
340
|
+
Select,
|
|
341
|
+
Confirm,
|
|
342
|
+
Multiselect,
|
|
343
|
+
Mask,
|
|
344
|
+
Password,
|
|
345
|
+
Spinner,
|
|
346
|
+
Progress,
|
|
347
|
+
}
|
package/src/core/Form/Form.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import Message from
|
|
2
|
-
import FormMessage from
|
|
3
|
-
import FormInput from
|
|
1
|
+
import Message from '@nan0web/co'
|
|
2
|
+
import FormMessage from './Message.js'
|
|
3
|
+
import FormInput from './Input.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Abstract form for data entry.
|
|
@@ -33,8 +33,8 @@ export default class UIForm extends FormMessage {
|
|
|
33
33
|
* otherwise returns an error message.
|
|
34
34
|
*/
|
|
35
35
|
static addValidation(name, fn) {
|
|
36
|
-
if (typeof name !==
|
|
37
|
-
throw new Error(
|
|
36
|
+
if (typeof name !== 'string' || typeof fn !== 'function') {
|
|
37
|
+
throw new Error('validation name must be a string and fn must be a function')
|
|
38
38
|
}
|
|
39
39
|
UIForm._validations[name] = fn
|
|
40
40
|
}
|
|
@@ -51,16 +51,10 @@ export default class UIForm extends FormMessage {
|
|
|
51
51
|
constructor(props = {}) {
|
|
52
52
|
super(props)
|
|
53
53
|
|
|
54
|
-
const {
|
|
55
|
-
title = '',
|
|
56
|
-
fields = [],
|
|
57
|
-
state = {},
|
|
58
|
-
schema = {},
|
|
59
|
-
...rest
|
|
60
|
-
} = props
|
|
54
|
+
const { title = '', fields = [], state = {}, schema = {}, ...rest } = props
|
|
61
55
|
|
|
62
56
|
// Normalise fields
|
|
63
|
-
this.fields = fields.map(f => FormInput.from(f))
|
|
57
|
+
this.fields = fields.map((f) => FormInput.from(f))
|
|
64
58
|
this.title = title
|
|
65
59
|
this.state = { ...state }
|
|
66
60
|
this.schema = schema
|
|
@@ -68,8 +62,8 @@ export default class UIForm extends FormMessage {
|
|
|
68
62
|
// Update meta with form data
|
|
69
63
|
this.meta = {
|
|
70
64
|
title: this.title,
|
|
71
|
-
fields: this.fields.map(f => f.toJSON ? f.toJSON() : f),
|
|
72
|
-
initialState: this.state
|
|
65
|
+
fields: this.fields.map((f) => (f.toJSON ? f.toJSON() : f)),
|
|
66
|
+
initialState: this.state,
|
|
73
67
|
}
|
|
74
68
|
}
|
|
75
69
|
|
|
@@ -86,7 +80,7 @@ export default class UIForm extends FormMessage {
|
|
|
86
80
|
setData(data) {
|
|
87
81
|
return new UIForm({
|
|
88
82
|
...this,
|
|
89
|
-
state: { ...this.state, ...data }
|
|
83
|
+
state: { ...this.state, ...data },
|
|
90
84
|
})
|
|
91
85
|
}
|
|
92
86
|
|
|
@@ -97,7 +91,7 @@ export default class UIForm extends FormMessage {
|
|
|
97
91
|
* @returns {FormInput|undefined}
|
|
98
92
|
*/
|
|
99
93
|
getField(name) {
|
|
100
|
-
return this.fields.find(f => f.name === name)
|
|
94
|
+
return this.fields.find((f) => f.name === name)
|
|
101
95
|
}
|
|
102
96
|
|
|
103
97
|
/**
|
|
@@ -122,14 +116,20 @@ export default class UIForm extends FormMessage {
|
|
|
122
116
|
const fieldValue = this.state[field.name]
|
|
123
117
|
|
|
124
118
|
// Required validation based on field definition or schema
|
|
125
|
-
if (
|
|
119
|
+
if (
|
|
120
|
+
field.required &&
|
|
121
|
+
(fieldValue === '' || fieldValue === null || fieldValue === undefined)
|
|
122
|
+
) {
|
|
126
123
|
errors.set(field.name, 'This field is required')
|
|
127
124
|
isValid = false
|
|
128
125
|
return
|
|
129
126
|
}
|
|
130
127
|
|
|
131
128
|
// Validation via schema (if provided) or field type
|
|
132
|
-
const { isValid: fieldValid, errors: fieldErrors } = this.validateField(
|
|
129
|
+
const { isValid: fieldValid, errors: fieldErrors } = this.validateField(
|
|
130
|
+
field.name,
|
|
131
|
+
fieldValue,
|
|
132
|
+
)
|
|
133
133
|
|
|
134
134
|
if (!fieldValid) {
|
|
135
135
|
for (const [key, err] of Object.entries(fieldErrors)) {
|
|
@@ -241,9 +241,9 @@ export default class UIForm extends FormMessage {
|
|
|
241
241
|
return {
|
|
242
242
|
time: new Date(this.time).toISOString(),
|
|
243
243
|
title: this.title,
|
|
244
|
-
fields: this.fields.map(f => f.toJSON ? f.toJSON() : f),
|
|
244
|
+
fields: this.fields.map((f) => (f.toJSON ? f.toJSON() : f)),
|
|
245
245
|
state: this.state,
|
|
246
|
-
meta: this.meta
|
|
246
|
+
meta: this.meta,
|
|
247
247
|
}
|
|
248
248
|
}
|
|
249
249
|
|
|
@@ -257,19 +257,21 @@ export default class UIForm extends FormMessage {
|
|
|
257
257
|
const Class = input.constructor
|
|
258
258
|
const fields = []
|
|
259
259
|
for (const [name, value] of Object.entries(input)) {
|
|
260
|
-
fields.push(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
260
|
+
fields.push(
|
|
261
|
+
new FormInput({
|
|
262
|
+
name,
|
|
263
|
+
label: Class[name]?.label ?? Class[`${name}Label`] ?? name,
|
|
264
|
+
type: Class[name]?.type ?? Class[`${name}Type`] ?? typeof value,
|
|
265
|
+
required: Class[name]?.required ?? Class[`${name}Required`] ?? false,
|
|
266
|
+
placeholder: Class[name]?.placeholder ?? Class[`${name}Placeholder`] ?? '',
|
|
267
|
+
defaultValue: Class[name]?.defaultValue ?? Class[`${name}Default`] ?? '',
|
|
268
|
+
validation: Class[name]?.validation ?? Class[`${name}Validation`] ?? (() => true),
|
|
269
|
+
}),
|
|
270
|
+
)
|
|
269
271
|
}
|
|
270
272
|
return new UIForm({
|
|
271
273
|
title: Class.name,
|
|
272
|
-
fields
|
|
274
|
+
fields,
|
|
273
275
|
})
|
|
274
276
|
}
|
|
275
277
|
return new UIForm(input)
|
|
@@ -298,10 +300,10 @@ export default class UIForm extends FormMessage {
|
|
|
298
300
|
label,
|
|
299
301
|
type: custom.type ?? FormInput.TYPES.TEXT,
|
|
300
302
|
required: !!custom.required,
|
|
301
|
-
placeholder: custom.placeholder ??
|
|
303
|
+
placeholder: custom.placeholder ?? '',
|
|
302
304
|
options: custom.options ?? [],
|
|
303
305
|
validation: custom.validation ?? undefined,
|
|
304
|
-
defaultValue: custom.defaultValue ??
|
|
306
|
+
defaultValue: custom.defaultValue ?? '',
|
|
305
307
|
})
|
|
306
308
|
})
|
|
307
309
|
return new UIForm({ fields })
|