openyida 2026.9.8 → 2026.9.10
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 +4 -1
- package/lib/app/create-form/args.js +40 -2
- package/lib/app/create-form/batch.js +205 -20
- package/lib/app/create-form/commands.js +2 -0
- package/lib/app/create-form.js +337 -14
- package/lib/app/services/canvas-page-schema-builder.js +3 -3
- package/lib/core/cli-error.js +10 -1
- package/lib/core/command-manifest.js +5 -2
- package/lib/core/locales/en.js +9 -1
- package/lib/core/locales/zh.js +9 -1
- package/lib/core/sample.js +13 -1
- package/lib/design-plan/init.js +90 -12
- package/lib/design-plan/materialize.js +18 -16
- package/lib/design-plan/normalize.js +88 -1
- package/lib/process/configure-process.js +18 -1
- package/lib/samples/openyida-scaffold/canvas-form-drawer.canvas.jsx +107 -23
- package/lib/samples/openyida-scaffold/canvas-nav/content.jsx +42 -0
- package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +1 -1
- package/lib/samples/yida-canvas-table-form/table-form-batch-submit.canvas.jsx +84 -17
- package/lib/samples/yida-rechart/trend-combo.canvas.jsx +97 -18
- package/package.json +1 -1
- package/yida-skills/skills/yida-app/SKILL.md +11 -2
- package/yida-skills/skills/yida-app/workflow/parallel-work.md +3 -1
- package/yida-skills/skills/yida-app/workflow/plan/step-4-deliver.md +7 -5
- package/yida-skills/skills/yida-app/workflow/plan/workflow.md +8 -5
- package/yida-skills/skills/yida-app/workflow/step-2-design.md +4 -0
- package/yida-skills/skills/yida-app/workflow/step-4-forms-processes.md +4 -2
- package/yida-skills/skills/yida-app/workflow/step-7-page-code.md +15 -3
- package/yida-skills/skills/yida-app/workflow/step-8-publish-navigation.md +4 -1
- package/yida-skills/skills/yida-app/workflow/step-9-output-finish.md +2 -0
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +27 -7
- package/yida-skills/skills/yida-canvas-custom-page/references/canvas-authoring-examples.md +2 -41
- package/yida-skills/skills/yida-canvas-custom-page/references/canvas-style-implementation-guide.md +93 -65
- package/yida-skills/skills/yida-canvas-custom-page/references/canvas-theme-provider.md +89 -0
- package/yida-skills/skills/yida-canvas-custom-page/references/component-library-guide.md +2 -2
- package/yida-skills/skills/yida-canvas-custom-page/references/navigation-and-entry-guide.md +11 -3
- package/yida-skills/skills/yida-canvas-custom-page/references/page-generation-guide.md +6 -2
- package/yida-skills/skills/yida-canvas-custom-page/scripts/build-canvas-theme.js +122 -0
- package/yida-skills/skills/yida-canvas-custom-page/scripts/canvas-theme-provider.template.jsx +102 -0
- package/yida-skills/skills/yida-canvas-table-form/SKILL.md +2 -2
- package/yida-skills/skills/yida-create-form-page/SKILL.md +68 -3
- package/yida-skills/skills/yida-create-form-page/references/batch-forms.md +18 -2
- package/yida-skills/skills/yida-design/references/ask-human-interaction-contract.md +18 -4
- package/yida-skills/skills/yida-design/references/design-mode.md +1 -1
- package/yida-skills/skills/yida-design/references/theme/app-custom-theme-template.css +2 -4
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +3 -1
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-media-grid.md +2 -2
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-modular-clarity.md +3 -3
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/airy-structured-clarity.md +3 -3
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ambient-halo-layered.md +4 -4
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-focus-layered.md +3 -3
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/dark-luminous-modular.md +7 -7
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/deep-stage-duotone.md +2 -2
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/hairline-runway-clarity.md +4 -4
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/high-contrast-modular.md +3 -3
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/interlocked-vivid-modules.md +5 -5
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/matrix-grid-focus.md +2 -2
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/media-rail-inspector.md +4 -4
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mist-layered-signal.md +2 -2
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/modular-rail-signal.md +2 -2
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/mono-grid-signal.md +4 -4
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/ribbon-ledger-lift.md +4 -4
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/segmented-meter-clarity.md +5 -5
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/split-rail-precision.md +2 -2
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/templates/design-themes/status-framed-media-grid.md +3 -3
- package/yida-skills/skills/yida-design/workflow/output-design.md +1 -1
- package/yida-skills/skills/yida-nav-shell/SKILL.md +7 -2
- package/yida-skills/skills/yida-nav-shell/references/nav-shell-patterns.md +84 -3
- package/yida-skills/skills/yida-prd/workflow/plan-business.md +3 -1
- package/yida-skills/skills/yida-requirement-analysis/SKILL.md +9 -8
- package/yida-skills/skills/yida-requirement-analysis/workflow/prepare-brief.md +23 -1
package/README.md
CHANGED
|
@@ -174,6 +174,8 @@ openyida update-app APP_XXX --theme-file .cache/openyida/crm/app-theme.css --nav
|
|
|
174
174
|
openyida corp-efficiency
|
|
175
175
|
openyida create-form create APP_XXX "Customer" .cache/openyida/forms/customer-fields.json
|
|
176
176
|
openyida create-form update APP_XXX FORM_XXX .cache/openyida/forms/customer-changes.json
|
|
177
|
+
openyida create-form update APP_XXX FORM_XXX --data-file .cache/openyida/forms/customer-changes.json
|
|
178
|
+
openyida create-form resume APP_XXX FORM_XXX .cache/openyida/forms/customer-fields.json --json
|
|
177
179
|
openyida sample openyida-page-template form-fields --output .cache/openyida/forms/customer-fields.json
|
|
178
180
|
openyida sample openyida-page-template canvas-form-drawer --output project/pages/src/customer-entry.canvas.jsx --var APP_TYPE=APP_XXX --var FORM_UUID=FORM_XXX
|
|
179
181
|
openyida get-schema APP_XXX FORM_XXX
|
|
@@ -413,7 +415,8 @@ Run `openyida --help` or `openyida <command> --help` for detailed usage.
|
|
|
413
415
|
| `openyida create-form create <appType> "<formTitle>" <fieldsJsonFile> [--icon auto\|<iconName>] [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Create a form page |
|
|
414
416
|
| `openyida create-form icons [--json]` | List available form navigation icons |
|
|
415
417
|
| `openyida create-form validate-fields <fieldsJsonOrFile> [--json]` | Validate form field JSON locally |
|
|
416
|
-
| `openyida create-form update <appType>
|
|
418
|
+
| `openyida create-form update <appType> <formUuid> (<changesJsonOrFile> \| --data-file <changesJsonOrFile>) [--locale zh_CN\|en_US\|ja_JP] [--open\|--no-open]` | Update a form page |
|
|
419
|
+
| `openyida create-form resume <appType> <formUuid> <fieldsJsonOrFile> [--json]` | Update a form page |
|
|
417
420
|
| `openyida create-form patch <appType> <formUuid> <patchJsonOrFile> [--open\|--no-open]` | Update a form page |
|
|
418
421
|
| `openyida create-form rule <appType> <formUuid> <rulesJsonOrFile> [--open\|--no-open]` | Update a form page |
|
|
419
422
|
| `openyida create-form validation <appType> <formUuid> <validationsJsonOrFile> [--open\|--no-open]` | Update a form page |
|
|
@@ -89,6 +89,7 @@ function createParseArgs(dependencies) {
|
|
|
89
89
|
contentLocale: null,
|
|
90
90
|
browserOpenMode: openOption.mode,
|
|
91
91
|
};
|
|
92
|
+
let dataFileInput;
|
|
92
93
|
|
|
93
94
|
const args = [...rawArgs];
|
|
94
95
|
|
|
@@ -111,6 +112,14 @@ function createParseArgs(dependencies) {
|
|
|
111
112
|
i--;
|
|
112
113
|
} else if (args[i] === '--icon') {
|
|
113
114
|
throwCreateFormError('--icon requires a value', 'CREATE_FORM_INVALID_ARGUMENTS');
|
|
115
|
+
} else if (args[i] === '--data-file') {
|
|
116
|
+
const next = args[i + 1];
|
|
117
|
+
if (dataFileInput !== undefined || !next || next.startsWith('--')) {
|
|
118
|
+
throwCreateFormError('--data-file requires exactly one value', 'CREATE_FORM_INVALID_ARGUMENTS');
|
|
119
|
+
}
|
|
120
|
+
dataFileInput = next;
|
|
121
|
+
args.splice(i, 2);
|
|
122
|
+
i--;
|
|
114
123
|
} else if ((args[i] === '--locale' || args[i] === '--content-locale' || args[i] === '--lang') && i + 1 < args.length) {
|
|
115
124
|
options.contentLocale = args[i + 1];
|
|
116
125
|
if (!normalizeYidaLocale(options.contentLocale)) {
|
|
@@ -134,8 +143,10 @@ function createParseArgs(dependencies) {
|
|
|
134
143
|
if (args.includes('--help') || args.includes('-h')) {
|
|
135
144
|
usage(t('create_form.usage_create_short'));
|
|
136
145
|
hint(t('create_form.usage_update_short'));
|
|
146
|
+
hint(' batch: openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--json]');
|
|
137
147
|
hint(t('create_form.example_create'));
|
|
138
148
|
hint(t('create_form.example_update'));
|
|
149
|
+
hint('Example: openyida create-form batch "APP_XXX" .cache/openyida/forms/forms.json --json');
|
|
139
150
|
return {
|
|
140
151
|
mode: 'help',
|
|
141
152
|
help: true,
|
|
@@ -145,6 +156,10 @@ function createParseArgs(dependencies) {
|
|
|
145
156
|
|
|
146
157
|
const mode = args[0];
|
|
147
158
|
|
|
159
|
+
if (dataFileInput !== undefined && mode !== 'update') {
|
|
160
|
+
throwCreateFormError('--data-file is only supported by create-form update', 'CREATE_FORM_INVALID_ARGUMENTS');
|
|
161
|
+
}
|
|
162
|
+
|
|
148
163
|
if (mode === 'icons' || mode === 'list-icons') {
|
|
149
164
|
return {
|
|
150
165
|
mode: 'icons',
|
|
@@ -182,7 +197,12 @@ function createParseArgs(dependencies) {
|
|
|
182
197
|
}
|
|
183
198
|
|
|
184
199
|
if (mode === 'update') {
|
|
185
|
-
|
|
200
|
+
const positionalInput = args[3];
|
|
201
|
+
if (args.length > 4 || (positionalInput && dataFileInput !== undefined)) {
|
|
202
|
+
throwCreateFormError('Use either <changesJsonOrFile> or --data-file, not both', 'CREATE_FORM_INVALID_ARGUMENTS');
|
|
203
|
+
}
|
|
204
|
+
const changesJsonOrFile = dataFileInput !== undefined ? dataFileInput : positionalInput;
|
|
205
|
+
if (args.length < 3 || !changesJsonOrFile) {
|
|
186
206
|
usage(t('create_form.usage_update'), t('create_form.example_update'));
|
|
187
207
|
throwCreateFormError(t('create_form.usage_update'), 'CREATE_FORM_INVALID_ARGUMENTS');
|
|
188
208
|
}
|
|
@@ -190,7 +210,23 @@ function createParseArgs(dependencies) {
|
|
|
190
210
|
mode: 'update',
|
|
191
211
|
appType: args[1],
|
|
192
212
|
formUuid: args[2],
|
|
193
|
-
changesJsonOrFile
|
|
213
|
+
changesJsonOrFile,
|
|
214
|
+
...options
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (mode === 'resume') {
|
|
219
|
+
if (args.length !== 4) {
|
|
220
|
+
throwCreateFormError(
|
|
221
|
+
'openyida create-form resume <appType> <formUuid> <fieldsJsonOrFile> --json',
|
|
222
|
+
'CREATE_FORM_INVALID_ARGUMENTS'
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
mode: 'resume',
|
|
227
|
+
appType: args[1],
|
|
228
|
+
formUuid: args[2],
|
|
229
|
+
fieldsJsonOrFile: args[3],
|
|
194
230
|
...options
|
|
195
231
|
};
|
|
196
232
|
}
|
|
@@ -313,8 +349,10 @@ function createParseArgs(dependencies) {
|
|
|
313
349
|
|
|
314
350
|
usage(t('create_form.usage_create_short'));
|
|
315
351
|
hint(t('create_form.usage_update_short'));
|
|
352
|
+
hint(' batch: openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--json]');
|
|
316
353
|
hint(t('create_form.example_create'));
|
|
317
354
|
hint(t('create_form.example_update'));
|
|
355
|
+
hint('Example: openyida create-form batch "APP_XXX" .cache/openyida/forms/forms.json --json');
|
|
318
356
|
throwCreateFormError(t('create_form.usage_create_short'), 'CREATE_FORM_INVALID_ARGUMENTS');
|
|
319
357
|
};
|
|
320
358
|
}
|
|
@@ -6,12 +6,50 @@ const crypto = require('crypto');
|
|
|
6
6
|
const { execFile } = require('child_process');
|
|
7
7
|
const { CliError } = require('../../core/cli-error');
|
|
8
8
|
const { t } = require('../../core/i18n');
|
|
9
|
+
const { usage, hint } = require('../../core/chalk');
|
|
10
|
+
const { assertNoEmojiInText, assertNoEmojiInValue } = require('../../core/no-emoji-guard');
|
|
11
|
+
const { normalizeYidaLocale } = require('../../core/yida-i18n');
|
|
12
|
+
const { FORM_NAV_ICONS, resolveFormNavIcon } = require('./nav-icon');
|
|
13
|
+
|
|
14
|
+
const BATCH_USAGE = 'openyida create-form batch <appType> <plan.json> [--concurrency 1..4] [--check] [--json]';
|
|
15
|
+
const BATCH_PLAN_EXAMPLE = '{"forms":[{"key":"customer","title":"客户","fieldsFile":"customer-fields.json"},{"key":"order","title":"订单","fieldsFile":"order-fields.json","dependsOn":["customer"]}]}';
|
|
9
16
|
|
|
10
17
|
function invalid(reason) {
|
|
11
18
|
throw new CliError(t('create_form.batch_invalid'), { code: 'FORM_BATCH_INVALID', details: { reason } });
|
|
12
19
|
}
|
|
13
20
|
|
|
21
|
+
function validateStaticDefinition(form, fields) {
|
|
22
|
+
try {
|
|
23
|
+
assertNoEmojiInText(form.title, { artifact: `forms.${form.key}.title` });
|
|
24
|
+
assertNoEmojiInValue(fields, { artifact: `forms.${form.key}.fields` });
|
|
25
|
+
} catch (error) {
|
|
26
|
+
invalid({
|
|
27
|
+
formKey: form.key,
|
|
28
|
+
errorCode: error?.code || 'OPENYIDA_ARTIFACT_EMOJI_FORBIDDEN',
|
|
29
|
+
errorMsg: error?.message || String(error),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (form.icon !== undefined && !resolveFormNavIcon(form.icon, form.title, fields).icon) {
|
|
33
|
+
invalid({
|
|
34
|
+
formKey: form.key,
|
|
35
|
+
errorCode: 'CREATE_FORM_NAV_ICON_INVALID',
|
|
36
|
+
errorMsg: `Unsupported form navigation icon: ${form.icon}`,
|
|
37
|
+
icon: form.icon,
|
|
38
|
+
availableIconCount: FORM_NAV_ICONS.length,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (form.locale !== undefined && !normalizeYidaLocale(form.locale)) {
|
|
42
|
+
invalid({
|
|
43
|
+
formKey: form.key,
|
|
44
|
+
errorCode: 'CREATE_FORM_INVALID_ARGUMENTS',
|
|
45
|
+
errorMsg: `Unsupported locale: ${form.locale}`,
|
|
46
|
+
locale: form.locale,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
14
51
|
function parseArgs(args) {
|
|
52
|
+
if (args.includes('--help') || args.includes('-h')) { return { help: true }; }
|
|
15
53
|
const options = { concurrency: 3, check: false };
|
|
16
54
|
const positional = [];
|
|
17
55
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -25,11 +63,19 @@ function parseArgs(args) {
|
|
|
25
63
|
}
|
|
26
64
|
}
|
|
27
65
|
if (positional.length !== 2 || !Number.isInteger(options.concurrency) || options.concurrency < 1 || options.concurrency > 4) {
|
|
28
|
-
invalid(
|
|
66
|
+
invalid(BATCH_USAGE.replace(/^openyida /, ''));
|
|
29
67
|
}
|
|
30
68
|
return { ...options, appType: positional[0], file: path.resolve(positional[1]) };
|
|
31
69
|
}
|
|
32
70
|
|
|
71
|
+
function showHelp() {
|
|
72
|
+
usage(BATCH_USAGE);
|
|
73
|
+
hint('plan.json: ' + BATCH_PLAN_EXAMPLE);
|
|
74
|
+
hint('fieldsFile is resolved relative to plan.json; use dependsOn and {"$form":"key","field":"label"} for dependencies.');
|
|
75
|
+
hint('Run one authoritative batch. If it continues in background, do not stop it, delete its state, or fall back to create/update/resume.');
|
|
76
|
+
return { success: true, help: true, usage: BATCH_USAGE };
|
|
77
|
+
}
|
|
78
|
+
|
|
33
79
|
// References occupy a complete JSON value; ordinary text is preserved.
|
|
34
80
|
function mapReferences(value, resolve) {
|
|
35
81
|
if (Array.isArray(value)) { return value.map(item => mapReferences(item, resolve)); }
|
|
@@ -42,7 +88,38 @@ function mapReferences(value, resolve) {
|
|
|
42
88
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, mapReferences(item, resolve)]));
|
|
43
89
|
}
|
|
44
90
|
|
|
45
|
-
function
|
|
91
|
+
function normalizeAssociationReferences(value, appType) {
|
|
92
|
+
if (Array.isArray(value)) {
|
|
93
|
+
return value.map(item => normalizeAssociationReferences(item, appType));
|
|
94
|
+
}
|
|
95
|
+
if (!value || typeof value !== 'object') { return value; }
|
|
96
|
+
const normalized = Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
97
|
+
key,
|
|
98
|
+
normalizeAssociationReferences(item, appType),
|
|
99
|
+
]));
|
|
100
|
+
const association = value.associationForm;
|
|
101
|
+
if (
|
|
102
|
+
value.type === 'AssociationFormField' &&
|
|
103
|
+
association &&
|
|
104
|
+
typeof association === 'object' &&
|
|
105
|
+
!Array.isArray(association) &&
|
|
106
|
+
typeof association.$form === 'string'
|
|
107
|
+
) {
|
|
108
|
+
const reference = { $form: association.$form };
|
|
109
|
+
normalized.associationForm = {
|
|
110
|
+
...(appType ? { appType } : {}),
|
|
111
|
+
formUuid: reference,
|
|
112
|
+
...(typeof association.field === 'string' && association.field.trim() ? {
|
|
113
|
+
mainFieldId: { ...reference, field: association.field },
|
|
114
|
+
mainFieldLabel: association.field,
|
|
115
|
+
mainComponentName: 'TextField',
|
|
116
|
+
} : {}),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function loadPlan(file, options = {}) {
|
|
46
123
|
const input = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
47
124
|
if (!Array.isArray(input.forms) || !input.forms.length) { invalid('forms'); }
|
|
48
125
|
const forms = input.forms.map(form => {
|
|
@@ -52,7 +129,9 @@ function loadPlan(file) {
|
|
|
52
129
|
if (Object.keys(form).some(key => !['key', 'title', 'fields', 'fieldsFile', 'dependsOn', 'formUuid', 'icon', 'locale'].includes(key))) { invalid(form.key); }
|
|
53
130
|
if (form.formUuid !== undefined && (typeof form.formUuid !== 'string' || !form.formUuid.startsWith('FORM'))) { invalid(form.key); }
|
|
54
131
|
for (const key of ['icon', 'locale']) { if (form[key] !== undefined && typeof form[key] !== 'string') { invalid(key); } }
|
|
55
|
-
const
|
|
132
|
+
const rawFields = form.fieldsFile === undefined ? form.fields : JSON.parse(fs.readFileSync(path.resolve(path.dirname(file), form.fieldsFile), 'utf8'));
|
|
133
|
+
const fields = normalizeAssociationReferences(rawFields, options.appType);
|
|
134
|
+
validateStaticDefinition(form, fields);
|
|
56
135
|
const dependencies = new Set(form.dependsOn || []);
|
|
57
136
|
mapReferences(fields, key => { dependencies.add(key); return 'FORM-PRECHECK'; });
|
|
58
137
|
return { ...form, fields, dependsOn: [...dependencies] };
|
|
@@ -87,21 +166,85 @@ function parseOutput(stdout) {
|
|
|
87
166
|
return output;
|
|
88
167
|
}
|
|
89
168
|
|
|
90
|
-
function
|
|
169
|
+
function mergeCommandOutput(stdout, stderr, failed) {
|
|
170
|
+
const stdoutOutput = parseOutput(stdout);
|
|
171
|
+
const stderrOutput = parseOutput(stderr);
|
|
172
|
+
if (!failed) { return stdoutOutput || stderrOutput; }
|
|
173
|
+
if (!stdoutOutput) { return stderrOutput; }
|
|
174
|
+
if (!stderrOutput) { return stdoutOutput; }
|
|
175
|
+
return { ...stdoutOutput, ...stderrOutput };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function boundedDiagnostic(value) {
|
|
179
|
+
const text = String(value || '').trim();
|
|
180
|
+
return text.length > 4000 ? text.slice(-4000) : text;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function expectedReadbackFields(fields) {
|
|
184
|
+
const expected = [];
|
|
185
|
+
for (const field of Array.isArray(fields) ? fields : []) {
|
|
186
|
+
if (Array.isArray(field)) {
|
|
187
|
+
expected.push(...expectedReadbackFields(field));
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!field || typeof field !== 'object') { continue; }
|
|
191
|
+
if (field.type === 'ColumnContainer') {
|
|
192
|
+
expected.push(...expectedReadbackFields(field.children));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (['Divider', 'RichText', 'PageSection', 'GroupContainer'].includes(field.type)) { continue; }
|
|
196
|
+
if (typeof field.label === 'string' && field.label.trim()) {
|
|
197
|
+
expected.push({ label: field.label.trim(), componentName: field.type });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return expected;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function readbackMatchesExpectedFields(schema, fields) {
|
|
204
|
+
if (!schema || typeof schema.formUuid !== 'string' || !Array.isArray(schema.fields)) { return false; }
|
|
205
|
+
const actual = schema.fields.filter(field => field && typeof field === 'object');
|
|
206
|
+
return expectedReadbackFields(fields).every(expected => actual.some(field =>
|
|
207
|
+
field.label === expected.label && (!expected.componentName || field.componentName === expected.componentName)
|
|
208
|
+
));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function readbackMismatch(formUuid, form, schema) {
|
|
212
|
+
const expected = expectedReadbackFields(form.fields);
|
|
213
|
+
const actual = Array.isArray(schema?.fields) ? schema.fields.map(field => ({
|
|
214
|
+
label: field?.label,
|
|
215
|
+
componentName: field?.componentName,
|
|
216
|
+
})) : [];
|
|
217
|
+
return Object.assign(new Error(`Form batch readback mismatch: ${form.key}`), {
|
|
218
|
+
output: {
|
|
219
|
+
success: false,
|
|
220
|
+
errorCode: 'FORM_BATCH_READBACK_MISMATCH',
|
|
221
|
+
errorMsg: `Created form ${form.key} did not contain all expected fields`,
|
|
222
|
+
formUuid,
|
|
223
|
+
details: { formKey: form.key, formUuid, expected, actual },
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function execute(args, { execFile: execFileImpl = execFile } = {}) {
|
|
91
229
|
return new Promise((resolve, reject) => {
|
|
92
|
-
|
|
230
|
+
execFileImpl(process.execPath, [path.resolve(__dirname, '../../../bin/yida.js'), ...args, '--quiet'], {
|
|
93
231
|
env: { ...process.env, YIDA_QUIET: '1' }, timeout: 180000, maxBuffer: 8 * 1024 * 1024,
|
|
94
|
-
}, (error, stdout) => {
|
|
95
|
-
const output =
|
|
232
|
+
}, (error, stdout, stderr) => {
|
|
233
|
+
const output = mergeCommandOutput(stdout, stderr, Boolean(error));
|
|
96
234
|
if (error || !output || typeof output !== 'object' || output.success === false) {
|
|
97
|
-
reject(Object.assign(new Error(
|
|
235
|
+
reject(Object.assign(new Error(
|
|
236
|
+
output?.errorMsg || output?.error || output?.message || boundedDiagnostic(stderr) || error?.message || 'Missing command result'
|
|
237
|
+
), { output }));
|
|
98
238
|
} else { resolve(output); }
|
|
99
239
|
});
|
|
100
240
|
});
|
|
101
241
|
}
|
|
102
242
|
|
|
103
243
|
async function schedule(forms, concurrency, results, worker, save) {
|
|
104
|
-
const pending = new Set(forms.filter(form =>
|
|
244
|
+
const pending = new Set(forms.filter(form => {
|
|
245
|
+
const result = results[form.key];
|
|
246
|
+
return !result || result.status === 'blocked' || (result.status === 'failed' && result.formUuid);
|
|
247
|
+
}).map(form => form.key));
|
|
105
248
|
const active = new Map();
|
|
106
249
|
try {
|
|
107
250
|
while (pending.size || active.size) {
|
|
@@ -114,7 +257,7 @@ async function schedule(forms, concurrency, results, worker, save) {
|
|
|
114
257
|
}
|
|
115
258
|
if (active.size >= concurrency || deps.some(status => status !== 'success')) { continue; }
|
|
116
259
|
pending.delete(form.key);
|
|
117
|
-
results[form.key] = { status: 'running' };
|
|
260
|
+
results[form.key] = { ...results[form.key], status: 'running' };
|
|
118
261
|
save(); // Record intent before the request; an interrupted create is never retried automatically.
|
|
119
262
|
const task = Promise.resolve().then(() => worker(form)).then(output => {
|
|
120
263
|
results[form.key] = { ...output, status: 'success' };
|
|
@@ -137,7 +280,8 @@ async function schedule(forms, concurrency, results, worker, save) {
|
|
|
137
280
|
|
|
138
281
|
async function run(args, dependencies = {}) {
|
|
139
282
|
const options = parseArgs(args);
|
|
140
|
-
|
|
283
|
+
if (options.help) { return showHelp(); }
|
|
284
|
+
const { forms, groups } = loadPlan(options.file, { appType: options.appType });
|
|
141
285
|
const call = dependencies.execute || execute;
|
|
142
286
|
// Preflight all definitions before the first remote mutation.
|
|
143
287
|
await schedule(forms.map(form => ({ ...form, dependsOn: [] })), options.concurrency, {}, async form => {
|
|
@@ -178,21 +322,49 @@ async function run(args, dependencies = {}) {
|
|
|
178
322
|
return matches[0].fieldId;
|
|
179
323
|
};
|
|
180
324
|
await schedule(forms, options.concurrency, state.results, async form => {
|
|
181
|
-
let formUuid = form.formUuid;
|
|
325
|
+
let formUuid = form.formUuid || state.results[form.key]?.formUuid;
|
|
326
|
+
let shouldResume = Boolean(!form.formUuid && formUuid);
|
|
327
|
+
const resolvedFields = mapReferences(form.fields, resolve);
|
|
182
328
|
if (!formUuid) {
|
|
183
|
-
const argv = ['create-form', 'create', options.appType, form.title, JSON.stringify(
|
|
329
|
+
const argv = ['create-form', 'create', options.appType, form.title, JSON.stringify(resolvedFields), '--no-open'];
|
|
184
330
|
for (const key of ['icon', 'locale']) { if (form[key]) { argv.push(`--${key}`, form[key]); } }
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
331
|
+
try {
|
|
332
|
+
const created = await call(argv);
|
|
333
|
+
if (typeof created.formUuid !== 'string' || !created.formUuid.startsWith('FORM')) { invalid(`create result: ${form.key}`); }
|
|
334
|
+
formUuid = created.formUuid;
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const createdFormUuid = error.output?.formUuid || error.output?.details?.formUuid;
|
|
337
|
+
if (typeof createdFormUuid !== 'string' || !createdFormUuid.startsWith('FORM')) {
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
formUuid = createdFormUuid;
|
|
341
|
+
shouldResume = true;
|
|
342
|
+
}
|
|
188
343
|
}
|
|
189
344
|
state.results[form.key].formUuid = formUuid;
|
|
190
345
|
save(state);
|
|
191
|
-
|
|
192
|
-
if (
|
|
346
|
+
let resumed = false;
|
|
347
|
+
if (shouldResume) {
|
|
348
|
+
await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
|
|
349
|
+
resumed = true;
|
|
350
|
+
}
|
|
351
|
+
let schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
|
|
352
|
+
if (!readbackMatchesExpectedFields(schema, resolvedFields) && !resumed && !form.formUuid) {
|
|
353
|
+
await call(['create-form', 'resume', options.appType, formUuid, JSON.stringify(resolvedFields), '--json']);
|
|
354
|
+
resumed = true;
|
|
355
|
+
schema = await call(['get-schema', options.appType, formUuid, '--field-map-json']);
|
|
356
|
+
}
|
|
357
|
+
if (schema.formUuid !== formUuid || !readbackMatchesExpectedFields(schema, resolvedFields)) {
|
|
358
|
+
throw readbackMismatch(formUuid, { ...form, fields: resolvedFields }, schema);
|
|
359
|
+
}
|
|
193
360
|
return { formUuid, fields: schema.fields };
|
|
194
361
|
}, () => save(state));
|
|
195
|
-
const
|
|
362
|
+
const success = Object.values(state.results).every(item => item.status === 'success');
|
|
363
|
+
const output = { success, groups, stateFile, results: state.results };
|
|
364
|
+
if (!success) {
|
|
365
|
+
output.errorCode = 'FORM_BATCH_PARTIAL_FAILURE';
|
|
366
|
+
output.nextAction = 'Inspect the saved state and child error, then fix the batch input or recover known formUuid values. Do not fall back to create-form create.';
|
|
367
|
+
}
|
|
196
368
|
console.log(JSON.stringify(output));
|
|
197
369
|
if (!output.success) { process.exitCode = 1; }
|
|
198
370
|
return output;
|
|
@@ -201,4 +373,17 @@ async function run(args, dependencies = {}) {
|
|
|
201
373
|
}
|
|
202
374
|
}
|
|
203
375
|
|
|
204
|
-
module.exports = {
|
|
376
|
+
module.exports = {
|
|
377
|
+
run,
|
|
378
|
+
parseArgs,
|
|
379
|
+
loadPlan,
|
|
380
|
+
mapReferences,
|
|
381
|
+
normalizeAssociationReferences,
|
|
382
|
+
schedule,
|
|
383
|
+
parseOutput,
|
|
384
|
+
mergeCommandOutput,
|
|
385
|
+
execute,
|
|
386
|
+
expectedReadbackFields,
|
|
387
|
+
readbackMatchesExpectedFields,
|
|
388
|
+
validateStaticDefinition,
|
|
389
|
+
};
|
|
@@ -6,6 +6,8 @@ function dispatchCreateFormCommand(parsedArgs, authContext, handlers) {
|
|
|
6
6
|
switch (parsedArgs.mode) {
|
|
7
7
|
case 'update':
|
|
8
8
|
return handlers.update(...args);
|
|
9
|
+
case 'resume':
|
|
10
|
+
return handlers.resume(...args);
|
|
9
11
|
case 'patch':
|
|
10
12
|
return handlers.patch(...args);
|
|
11
13
|
case 'rule':
|