@nan0web/ui-cli 1.0.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/.editorconfig +20 -0
- package/CONTRIBUTING.md +42 -0
- package/LICENSE +15 -0
- package/README.md +277 -0
- package/docs/uk/README.md +294 -0
- package/package.json +38 -0
- package/playground/forms/addressForm.js +37 -0
- package/playground/forms/ageForm.js +26 -0
- package/playground/forms/profileForm.js +33 -0
- package/playground/forms/userForm.js +36 -0
- package/playground/main.js +81 -0
- package/playground/vocabs/en.js +25 -0
- package/playground/vocabs/index.js +12 -0
- package/playground/vocabs/uk.js +25 -0
- package/src/InputAdapter.js +219 -0
- package/src/InputAdapter.test.js +117 -0
- package/src/README.md.js +459 -0
- package/src/index.js +25 -0
- package/src/ui/index.js +4 -0
- package/src/ui/input.js +63 -0
- package/src/ui/input.test.js +27 -0
- package/src/ui/next.js +70 -0
- package/src/ui/select.js +58 -0
- package/src/ui/select.test.js +34 -0
- package/system.md +99 -0
- package/tsconfig.json +23 -0
- package/types/InputAdapter.d.ts +106 -0
- package/types/README.md.d.ts +1 -0
- package/types/index.d.ts +11 -0
- package/types/test/ReadLine.d.ts +1 -0
- package/types/ui/errors.d.ts +3 -0
- package/types/ui/index.d.ts +4 -0
- package/types/ui/input.d.ts +15 -0
- package/types/ui/next.d.ts +12 -0
- package/types/ui/select.d.ts +30 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { UIForm, FormInput } from '@nan0web/ui'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates an address information form
|
|
5
|
+
* @param {Function} t - Translation function
|
|
6
|
+
* @returns {UIForm} Address form
|
|
7
|
+
*/
|
|
8
|
+
export function createAddressForm(t) {
|
|
9
|
+
return new UIForm({
|
|
10
|
+
title: t('Address information'),
|
|
11
|
+
id: 'address-form',
|
|
12
|
+
state: {},
|
|
13
|
+
fields: [
|
|
14
|
+
new FormInput({
|
|
15
|
+
name: 'street',
|
|
16
|
+
label: t('Street'),
|
|
17
|
+
required: true
|
|
18
|
+
}),
|
|
19
|
+
new FormInput({
|
|
20
|
+
name: 'city',
|
|
21
|
+
label: t('City'),
|
|
22
|
+
required: true
|
|
23
|
+
}),
|
|
24
|
+
new FormInput({
|
|
25
|
+
name: 'postalCode',
|
|
26
|
+
label: t('Postal code'),
|
|
27
|
+
type: FormInput.TYPES.TEXT,
|
|
28
|
+
required: false
|
|
29
|
+
}),
|
|
30
|
+
new FormInput({
|
|
31
|
+
name: 'country',
|
|
32
|
+
label: t('Country'),
|
|
33
|
+
required: true
|
|
34
|
+
})
|
|
35
|
+
]
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { UIForm, FormInput } from '@nan0web/ui'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates an age confirmation form
|
|
5
|
+
* @param {Function} t - Translation function
|
|
6
|
+
* @returns {UIForm} Age confirmation form
|
|
7
|
+
*/
|
|
8
|
+
export function createAgeForm(t) {
|
|
9
|
+
return new UIForm({
|
|
10
|
+
title: t('Age confirmation form'),
|
|
11
|
+
id: 'age-confirmation',
|
|
12
|
+
state: {},
|
|
13
|
+
fields: [
|
|
14
|
+
new FormInput({
|
|
15
|
+
name: 'age',
|
|
16
|
+
label: t('Please enter your age'),
|
|
17
|
+
type: FormInput.TYPES.NUMBER,
|
|
18
|
+
validator: (value) => {
|
|
19
|
+
const num = Number(value)
|
|
20
|
+
return num >= 18 ? null : t('You must be at least 18 years old')
|
|
21
|
+
},
|
|
22
|
+
required: true
|
|
23
|
+
})
|
|
24
|
+
]
|
|
25
|
+
})
|
|
26
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { UIForm, FormInput } from '@nan0web/ui'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a profile update form
|
|
5
|
+
* @param {Function} t - Translation function
|
|
6
|
+
* @returns {UIForm} Profile update form
|
|
7
|
+
*/
|
|
8
|
+
export function createProfileForm(t) {
|
|
9
|
+
return new UIForm({
|
|
10
|
+
title: t('Profile update form'),
|
|
11
|
+
id: 'profile-update',
|
|
12
|
+
state: {},
|
|
13
|
+
fields: [
|
|
14
|
+
new FormInput({
|
|
15
|
+
name: 'username',
|
|
16
|
+
label: t('Username'),
|
|
17
|
+
required: true
|
|
18
|
+
}),
|
|
19
|
+
new FormInput({
|
|
20
|
+
name: 'bio',
|
|
21
|
+
label: t('Biography'),
|
|
22
|
+
type: FormInput.TYPES.TEXTAREA,
|
|
23
|
+
required: false
|
|
24
|
+
}),
|
|
25
|
+
new FormInput({
|
|
26
|
+
name: 'newsletter',
|
|
27
|
+
label: t('Subscribe to newsletter'),
|
|
28
|
+
type: FormInput.TYPES.CHECKBOX,
|
|
29
|
+
required: false
|
|
30
|
+
})
|
|
31
|
+
]
|
|
32
|
+
})
|
|
33
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { UIForm, FormInput } from '@nan0web/ui'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Creates a user registration form
|
|
5
|
+
* @param {Function} t - Translation function
|
|
6
|
+
* @returns {UIForm} User registration form
|
|
7
|
+
*/
|
|
8
|
+
export function createUserForm(t) {
|
|
9
|
+
return new UIForm({
|
|
10
|
+
title: t('User Registration Form'),
|
|
11
|
+
id: 'user-registration',
|
|
12
|
+
state: {},
|
|
13
|
+
fields: [
|
|
14
|
+
new FormInput({
|
|
15
|
+
name: 'name',
|
|
16
|
+
label: t('Full name'),
|
|
17
|
+
required: true
|
|
18
|
+
}),
|
|
19
|
+
new FormInput({
|
|
20
|
+
name: 'email',
|
|
21
|
+
label: t('Email address'),
|
|
22
|
+
type: FormInput.TYPES.EMAIL,
|
|
23
|
+
required: true
|
|
24
|
+
}),
|
|
25
|
+
new FormInput({
|
|
26
|
+
name: 'phone',
|
|
27
|
+
label: t('Phone number'),
|
|
28
|
+
type: FormInput.TYPES.TEXT,
|
|
29
|
+
validator: (value) => {
|
|
30
|
+
return value.length >= 10 && value.length <= 15 ? null : t('Invalid phone number')
|
|
31
|
+
},
|
|
32
|
+
required: false
|
|
33
|
+
})
|
|
34
|
+
]
|
|
35
|
+
})
|
|
36
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import Logger from '@nan0web/log'
|
|
4
|
+
import { CLIInputAdapter } from '../src/index.js'
|
|
5
|
+
import { createUserForm } from './forms/userForm.js'
|
|
6
|
+
import { createAgeForm } from './forms/ageForm.js'
|
|
7
|
+
import { createAddressForm } from './forms/addressForm.js'
|
|
8
|
+
import { createProfileForm } from './forms/profileForm.js'
|
|
9
|
+
import createT, { localesMap } from "./vocabs/index.js"
|
|
10
|
+
|
|
11
|
+
console = new Logger()
|
|
12
|
+
|
|
13
|
+
const adapter = new CLIInputAdapter()
|
|
14
|
+
|
|
15
|
+
console.info(Logger.style(Logger.LOGO, { color: "magenta" }))
|
|
16
|
+
console.warn('=== @nan0web/ui CLI Playground ===\n')
|
|
17
|
+
|
|
18
|
+
// Language selection
|
|
19
|
+
const langResult = await adapter.requestSelect({
|
|
20
|
+
title: "Language selection",
|
|
21
|
+
prompt: "Choose language (1-2): ",
|
|
22
|
+
options: localesMap,
|
|
23
|
+
elementId: "language-select"
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if (langResult.action === 'select-cancel') {
|
|
27
|
+
console.warn('Language selection cancelled')
|
|
28
|
+
process.exit(0)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const selectedLang = langResult.value
|
|
32
|
+
const t = createT(selectedLang)
|
|
33
|
+
|
|
34
|
+
console.success(`${t('Language selection')}: ${t(selectedLang)}`)
|
|
35
|
+
console.info()
|
|
36
|
+
|
|
37
|
+
// User registration form
|
|
38
|
+
const userForm = createUserForm(t)
|
|
39
|
+
const userResult = await adapter.requestForm(userForm, { silent: false })
|
|
40
|
+
|
|
41
|
+
if (userResult.action === 'form-submit') {
|
|
42
|
+
console.success(t('Welcome to our platform!'))
|
|
43
|
+
console.info('User data:', userResult.data)
|
|
44
|
+
console.info()
|
|
45
|
+
} else {
|
|
46
|
+
console.warn('User registration cancelled')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Age confirmation form
|
|
50
|
+
const ageForm = createAgeForm(t)
|
|
51
|
+
const ageResult = await adapter.requestForm(ageForm, { silent: false })
|
|
52
|
+
|
|
53
|
+
if (ageResult.action === 'form-submit') {
|
|
54
|
+
console.success(`Confirmed age: ${ageResult.data.age}`)
|
|
55
|
+
} else {
|
|
56
|
+
console.warn('Age confirmation cancelled')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Address information form
|
|
60
|
+
const addressForm = createAddressForm(t)
|
|
61
|
+
const addressResult = await adapter.requestForm(addressForm, { silent: false })
|
|
62
|
+
|
|
63
|
+
if (addressResult.action === 'form-submit') {
|
|
64
|
+
console.success('Address information collected')
|
|
65
|
+
console.info('Address data:', addressResult.data)
|
|
66
|
+
console.info()
|
|
67
|
+
} else {
|
|
68
|
+
console.warn('Address form cancelled')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Profile update form
|
|
72
|
+
const profileForm = createProfileForm(t)
|
|
73
|
+
const profileResult = await adapter.requestForm(profileForm, { silent: false })
|
|
74
|
+
|
|
75
|
+
if (profileResult.action === 'form-submit') {
|
|
76
|
+
console.success('Profile updated')
|
|
77
|
+
console.info('Profile data:', profileResult.data)
|
|
78
|
+
console.info()
|
|
79
|
+
} else {
|
|
80
|
+
console.warn('Profile update cancelled')
|
|
81
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
"User Registration Form": "User Registration Form",
|
|
3
|
+
"Full name": "Full name",
|
|
4
|
+
"Email address": "Email address",
|
|
5
|
+
"Phone number": "Phone number",
|
|
6
|
+
"Age confirmation form": "Age confirmation form",
|
|
7
|
+
"Please enter your age": "Please enter your age",
|
|
8
|
+
"Language selection": "Language selection",
|
|
9
|
+
"Choose language": "Choose language",
|
|
10
|
+
"English": "English",
|
|
11
|
+
"Ukrainian": "Ukrainian",
|
|
12
|
+
"You must be at least 18 years old": "You must be at least 18 years old",
|
|
13
|
+
"Invalid phone number": "Invalid phone number",
|
|
14
|
+
"Welcome to our platform!": "Welcome to our platform!",
|
|
15
|
+
"Address information": "Address information",
|
|
16
|
+
"Street": "Street",
|
|
17
|
+
"City": "City",
|
|
18
|
+
"Postal code": "Postal code",
|
|
19
|
+
"Country": "Country",
|
|
20
|
+
"Profile update form": "Profile update form",
|
|
21
|
+
"Username": "Username",
|
|
22
|
+
"Biography": "Biography",
|
|
23
|
+
"Newsletter subscription": "Newsletter subscription",
|
|
24
|
+
"Subscribe to newsletter": "Subscribe to newsletter"
|
|
25
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import i18n, { createT } from "@nan0web/i18n"
|
|
2
|
+
import en from "./en.js"
|
|
3
|
+
import uk from "./uk.js"
|
|
4
|
+
|
|
5
|
+
const getVocab = i18n({ en, uk })
|
|
6
|
+
|
|
7
|
+
export const localesMap = new Map([
|
|
8
|
+
["en", "English"],
|
|
9
|
+
["uk", "Українська"],
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
export default (locale) => createT(getVocab(locale))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
"User Registration Form": "Форма реєстрації користувача",
|
|
3
|
+
"Full name": "Повне ім'я",
|
|
4
|
+
"Email address": "Електронна пошта",
|
|
5
|
+
"Phone number": "Номер телефону",
|
|
6
|
+
"Age confirmation form": "Форма підтвердження віку",
|
|
7
|
+
"Please enter your age": "Будь ласка, введіть ваш вік",
|
|
8
|
+
"Language selection": "Вибір мови",
|
|
9
|
+
"Choose language": "Виберіть мову",
|
|
10
|
+
"English": "Англійська",
|
|
11
|
+
"Ukrainian": "Українська",
|
|
12
|
+
"You must be at least 18 years old": "Вам має бути не менше 18 років",
|
|
13
|
+
"Invalid phone number": "Невірний номер телефону",
|
|
14
|
+
"Welcome to our platform!": "Ласкаво просимо на нашу платформу!",
|
|
15
|
+
"Address information": "Інформація про адресу",
|
|
16
|
+
"Street": "Вулиця",
|
|
17
|
+
"City": "Місто",
|
|
18
|
+
"Postal code": "Поштовий код",
|
|
19
|
+
"Country": "Країна",
|
|
20
|
+
"Profile update form": "Форма оновлення профілю",
|
|
21
|
+
"Username": "Ім'я користувача",
|
|
22
|
+
"Biography": "Біографія",
|
|
23
|
+
"Newsletter subscription": "Підписка на розсилку",
|
|
24
|
+
"Subscribe to newsletter": "Підписатися на розсилку"
|
|
25
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { Message } from '@nan0web/co'
|
|
2
|
+
import { UIForm, InputAdapter as BaseInputAdapter, InputMessage as BaseInputMessage } from '@nan0web/ui'
|
|
3
|
+
import { ask } from './ui/input.js'
|
|
4
|
+
import { select } from './ui/select.js'
|
|
5
|
+
|
|
6
|
+
/** @typedef {Partial<UIForm>} FormMessageValue */
|
|
7
|
+
/** @typedef {Partial<Message> | null} InputMessageValue */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Extends the generic {@link BaseInputMessage} to carry a {@link UIForm}
|
|
11
|
+
* instance alongside the usual input message payload.
|
|
12
|
+
*
|
|
13
|
+
* The original {@link BaseInputMessage} expects a `value` of type
|
|
14
|
+
* {@link InputMessageValue} (a {@link Message} payload). To remain
|
|
15
|
+
* compatible we keep `value` unchanged and store the form data in a
|
|
16
|
+
* separate `form` property.
|
|
17
|
+
*/
|
|
18
|
+
class FormMessage extends BaseInputMessage {
|
|
19
|
+
/** @type {UIForm} Form data associated with the message */
|
|
20
|
+
form
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new {@link FormMessage}.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} props - Message properties.
|
|
26
|
+
* @param {FormMessageValue} [props.form={}] UIForm instance or data.
|
|
27
|
+
* @param {InputMessageValue} [props.value=null] Retained for compatibility.
|
|
28
|
+
* @param {string[]|string} [props.options=[]] Available options.
|
|
29
|
+
* @param {boolean} [props.waiting=false] Waiting flag.
|
|
30
|
+
* @param {boolean} [props.escaped=false] Escape flag.
|
|
31
|
+
*/
|
|
32
|
+
constructor(props = {}) {
|
|
33
|
+
const {
|
|
34
|
+
form = {},
|
|
35
|
+
...rest
|
|
36
|
+
} = props
|
|
37
|
+
// Initialise the parent with the remaining properties.
|
|
38
|
+
// Cast to `any` to avoid type‑mismatch between duplicated InputMessage
|
|
39
|
+
// definitions across packages.
|
|
40
|
+
super(/** @type {any} */ (rest))
|
|
41
|
+
|
|
42
|
+
// Store the UIForm; accept an object, UIForm or a plain payload.
|
|
43
|
+
this.form = UIForm.from(form)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a {@link FormMessage} from an existing instance or plain data.
|
|
48
|
+
*
|
|
49
|
+
* @param {FormMessage|object} input – Existing message or raw data.
|
|
50
|
+
* @returns {FormMessage}
|
|
51
|
+
*/
|
|
52
|
+
static from(input) {
|
|
53
|
+
if (input instanceof FormMessage) return input
|
|
54
|
+
const {
|
|
55
|
+
form = {},
|
|
56
|
+
...rest
|
|
57
|
+
} = input ?? {}
|
|
58
|
+
return new FormMessage({ form, ...rest })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* CLI specific adapter extending the generic {@link BaseInputAdapter}.
|
|
64
|
+
* Implements concrete `ask` and `select` helpers that rely on the CLI utilities.
|
|
65
|
+
*/
|
|
66
|
+
export default class CLIInputAdapter extends BaseInputAdapter {
|
|
67
|
+
/**
|
|
68
|
+
* Interactively fill a {@link UIForm} field‑by‑field.
|
|
69
|
+
*
|
|
70
|
+
* @param {UIForm} form – Form definition to be filled.
|
|
71
|
+
* @param {Object} options – Request options.
|
|
72
|
+
* @param {boolean} [options.silent=true] Suppress title output when `true`.
|
|
73
|
+
* @returns {Promise<FormMessage>} Message with `escaped` = true on cancel,
|
|
74
|
+
* otherwise `escaped` = false and the completed form attached as `form`.
|
|
75
|
+
*/
|
|
76
|
+
async requestForm(form, options = {}) {
|
|
77
|
+
const { silent = true } = options
|
|
78
|
+
|
|
79
|
+
if (!silent) {
|
|
80
|
+
console.log(`\n${form.title}\n`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let formData = { ...form.state }
|
|
84
|
+
let currentFieldIndex = 0
|
|
85
|
+
|
|
86
|
+
while (currentFieldIndex < form.fields.length) {
|
|
87
|
+
const field = form.fields[currentFieldIndex]
|
|
88
|
+
const prompt = field.label || field.name
|
|
89
|
+
|
|
90
|
+
const input = await this.ask(`${prompt}${field.required ? ' *' : ''}: `)
|
|
91
|
+
|
|
92
|
+
// Cancel (Esc or empty string)
|
|
93
|
+
if ([FormMessage.ESCAPE, ''].includes(input)) {
|
|
94
|
+
return FormMessage.from({
|
|
95
|
+
form: {},
|
|
96
|
+
escaped: true,
|
|
97
|
+
action: 'form-cancel',
|
|
98
|
+
id: form.id,
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Navigation shortcuts
|
|
103
|
+
const trimmed = input.trim()
|
|
104
|
+
if (trimmed === '::prev' || trimmed === '::back') {
|
|
105
|
+
currentFieldIndex = Math.max(0, currentFieldIndex - 1)
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
if (trimmed === '::next' || trimmed === '::skip') {
|
|
109
|
+
currentFieldIndex++
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Skip optional fields when empty
|
|
114
|
+
if (trimmed === '' && !field.required) {
|
|
115
|
+
currentFieldIndex++
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Validate using the form's schema / field definition
|
|
120
|
+
const schema = field.constructor
|
|
121
|
+
const { isValid, errors } = form.validateValue(field.name, trimmed, schema)
|
|
122
|
+
if (!isValid) {
|
|
123
|
+
const errorMessages = Object.values(errors)
|
|
124
|
+
console.log(`\x1b[31mError: ${errorMessages.join(', ')}\x1b[0m`)
|
|
125
|
+
continue // stay on current field
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Store validated value
|
|
129
|
+
formData[field.name] = trimmed
|
|
130
|
+
currentFieldIndex++
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Final form validation
|
|
134
|
+
const finalForm = form.setData(formData)
|
|
135
|
+
const { isValid, errors } = finalForm.validate()
|
|
136
|
+
if (!isValid) {
|
|
137
|
+
console.log('\n' + Object.values(errors).join('\n'))
|
|
138
|
+
return await this.requestForm(form, options) // retry recursively
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return FormMessage.from({
|
|
142
|
+
form: finalForm,
|
|
143
|
+
escaped: false,
|
|
144
|
+
action: 'form-submit',
|
|
145
|
+
id: form.id,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Request a selection from a list of options.
|
|
151
|
+
*
|
|
152
|
+
* @param {Object} config – Selection configuration.
|
|
153
|
+
* @param {string} config.title – Title displayed before the list.
|
|
154
|
+
* @param {string} config.prompt – Prompt text.
|
|
155
|
+
* @param {Array<string>|Map<string,string>|Array<{label:string,value:string}>} config.options – Options to choose from.
|
|
156
|
+
* @param {string} config.id – Identifier for the resulting message.
|
|
157
|
+
* @returns {Promise<BaseInputMessage>} Message containing chosen value and metadata.
|
|
158
|
+
*/
|
|
159
|
+
async requestSelect(config) {
|
|
160
|
+
try {
|
|
161
|
+
const result = await this.select({
|
|
162
|
+
title: config.title ?? 'Select an option:',
|
|
163
|
+
prompt: config.prompt ?? 'Choose (1‑N): ',
|
|
164
|
+
options: config.options,
|
|
165
|
+
console: console,
|
|
166
|
+
})
|
|
167
|
+
return BaseInputMessage.from({
|
|
168
|
+
value: result.value,
|
|
169
|
+
data: result,
|
|
170
|
+
id: config.id,
|
|
171
|
+
})
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error instanceof this.CancelError) {
|
|
174
|
+
return BaseInputMessage.from({
|
|
175
|
+
value: '',
|
|
176
|
+
id: config.id,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
throw error
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Simple string input request.
|
|
185
|
+
*
|
|
186
|
+
* @param {Object} config Input configuration.
|
|
187
|
+
* @param {string} config.prompt Prompt text.
|
|
188
|
+
* @param {string} config.id Identifier for the resulting message.
|
|
189
|
+
* @param {string} [config.label] Optional label used as fallback.
|
|
190
|
+
* @param {string} [config.name] Optional name used as fallback.
|
|
191
|
+
* @returns {Promise<BaseInputMessage>} Message containing the entered text.
|
|
192
|
+
*/
|
|
193
|
+
async requestInput(config) {
|
|
194
|
+
const prompt = config.prompt ?? `${config.label ?? config.name}: `
|
|
195
|
+
const input = await this.ask(prompt)
|
|
196
|
+
|
|
197
|
+
if (input === '') {
|
|
198
|
+
return BaseInputMessage.from({
|
|
199
|
+
value: '',
|
|
200
|
+
id: config.id,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return BaseInputMessage.from({
|
|
205
|
+
value: input,
|
|
206
|
+
id: config.id,
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** @inheritDoc */
|
|
211
|
+
async ask(question) {
|
|
212
|
+
return ask(question)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** @inheritDoc */
|
|
216
|
+
async select(config) {
|
|
217
|
+
return select(config)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { UIForm, FormInput, InputMessage } from '@nan0web/ui'
|
|
4
|
+
import { CancelError } from '@nan0web/ui/core'
|
|
5
|
+
import CLIInputAdapter from './InputAdapter.js'
|
|
6
|
+
|
|
7
|
+
// Awaits for the user input.
|
|
8
|
+
describe('CLIInputAdapter', () => {
|
|
9
|
+
it('should create instance', () => {
|
|
10
|
+
const adapter = new CLIInputAdapter()
|
|
11
|
+
assert.ok(adapter instanceof CLIInputAdapter)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should validate form fields', async () => {
|
|
15
|
+
const adapter = new CLIInputAdapter()
|
|
16
|
+
|
|
17
|
+
// Mock ask for testing
|
|
18
|
+
adapter.ask = () => Promise.resolve('test@example.com')
|
|
19
|
+
|
|
20
|
+
const form = new UIForm({
|
|
21
|
+
title: 'Test Form',
|
|
22
|
+
elementId: 'test-form',
|
|
23
|
+
fields: [
|
|
24
|
+
new FormInput({
|
|
25
|
+
name: 'email',
|
|
26
|
+
label: 'Email',
|
|
27
|
+
type: 'email',
|
|
28
|
+
required: true
|
|
29
|
+
})
|
|
30
|
+
]
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const result = await adapter.requestForm(form, { silent: true })
|
|
34
|
+
assert.equal(result.form.state.email, 'test@example.com')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should handle form cancellation', async () => {
|
|
38
|
+
const adapter = new CLIInputAdapter()
|
|
39
|
+
|
|
40
|
+
// Mock ask for cancellation
|
|
41
|
+
adapter.ask = () => Promise.resolve(InputMessage.ESCAPE)
|
|
42
|
+
|
|
43
|
+
const form = new UIForm({
|
|
44
|
+
title: 'Test Form',
|
|
45
|
+
elementId: 'test-form',
|
|
46
|
+
fields: [
|
|
47
|
+
new FormInput({
|
|
48
|
+
name: 'name',
|
|
49
|
+
label: 'Name',
|
|
50
|
+
required: true
|
|
51
|
+
})
|
|
52
|
+
]
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const result = await adapter.requestForm(form, { silent: true })
|
|
56
|
+
assert.equal(result.escaped, true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should have CancelError static property', () => {
|
|
60
|
+
assert.ok(CLIInputAdapter.CancelError)
|
|
61
|
+
const error = new CLIInputAdapter.CancelError()
|
|
62
|
+
assert.equal(error.name, "CancelError")
|
|
63
|
+
assert.equal(error.message, "Operation cancelled by user")
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should implement ask method', async () => {
|
|
67
|
+
const adapter = new CLIInputAdapter()
|
|
68
|
+
// Mock internal ask to avoid stdin interaction
|
|
69
|
+
const originalAsk = adapter.ask
|
|
70
|
+
adapter.ask = () => Promise.resolve('test answer')
|
|
71
|
+
|
|
72
|
+
const answer = await adapter.ask('Test question?')
|
|
73
|
+
assert.equal(answer, 'test answer')
|
|
74
|
+
|
|
75
|
+
// Restore original
|
|
76
|
+
adapter.ask = originalAsk
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('should implement select method', async () => {
|
|
80
|
+
const adapter = new CLIInputAdapter()
|
|
81
|
+
// Mock internal select to avoid stdin interaction
|
|
82
|
+
const originalSelect = adapter.select
|
|
83
|
+
adapter.select = () => Promise.resolve({ value: 'option1', index: 0 })
|
|
84
|
+
|
|
85
|
+
const result = await adapter.select({
|
|
86
|
+
title: 'Test Select',
|
|
87
|
+
prompt: 'Choose:',
|
|
88
|
+
options: ['option1', 'option2']
|
|
89
|
+
})
|
|
90
|
+
assert.equal(result.value, 'option1')
|
|
91
|
+
assert.equal(result.index, 0)
|
|
92
|
+
|
|
93
|
+
// Restore original
|
|
94
|
+
adapter.select = originalSelect
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should handle select cancellation', async () => {
|
|
98
|
+
const adapter = new CLIInputAdapter()
|
|
99
|
+
|
|
100
|
+
// Mock select to throw CancelError
|
|
101
|
+
const originalSelect = adapter.select
|
|
102
|
+
adapter.select = () => Promise.reject(new CancelError())
|
|
103
|
+
|
|
104
|
+
const config = {
|
|
105
|
+
title: 'Test Select',
|
|
106
|
+
prompt: 'Choose:',
|
|
107
|
+
id: 'test-select',
|
|
108
|
+
options: ['option1', 'option2']
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = await adapter.requestSelect(config)
|
|
112
|
+
assert.deepStrictEqual(result.value, InputMessage.from({}).value)
|
|
113
|
+
|
|
114
|
+
// Restore original
|
|
115
|
+
adapter.select = originalSelect
|
|
116
|
+
})
|
|
117
|
+
})
|