openyida 2026.10.18-3 → 2026.10.19

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 CHANGED
@@ -490,6 +490,7 @@ Your AI prepares the summary from observed evidence and selected reproduction sc
490
490
  | `openyida create-form add-option <appType> <formUuid> <fieldLabel> <option1> [option2] ...` | Update a form page |
491
491
  | `openyida list-forms <appType> [--keyword <text>]` | List forms/pages in an app |
492
492
  | `openyida aggregate-table <list\|create-empty\|inspect\|preview\|save\|publish\|status> <appType> ...` | Manage aggregate tables (virtualView) |
493
+ | `openyida form-view <list\|inspect\|save\|create\|order> <appType> <formUuid> [viewUuid] [--config <file>] [--name <title>] [--with-config] [--expected-hash <hash>] [--backup <file>] [--replace] [--json]` | Native form views: list, inspect and configure columns, widths, frozen columns and filters |
493
494
  | `openyida get-schema <appType> <formUuid\|--all> [--summary-json\|--field-map-json\|--analysis-json]` | Get one form Schema or all form Schemas |
494
495
  | `openyida check-prd-completeness <prd.md> --app-type <appType> [--build-manifest <file>] [--json]` | Check resources, frontend menu hiding and application theme delivery |
495
496
  | `openyida check-design <design.md> [--prd <prd.md>] [--base-dir <dir>] [--json]` | Validate design documents, theme tokens and PRD handoff |
package/bin/yida.js CHANGED
@@ -815,6 +815,11 @@ async function main() {
815
815
  break;
816
816
  }
817
817
 
818
+ case 'form-view': {
819
+ const { run } = require('../lib/app/form-view');
820
+ await run(args);
821
+ break;
822
+ }
818
823
  case 'aggregate-table': {
819
824
  const { run } = require('../lib/aggregate-table/aggregate-table');
820
825
  await run(args);
@@ -513,19 +513,19 @@ function findUnavailableCanvasInstanceApiIssues(source) {
513
513
  const guardPlugin = ({ types: types }) => ({
514
514
  name: 'yida-canvas-instance-api-guard',
515
515
  visitor: {
516
- MemberExpression(path) {
516
+ 'MemberExpression|OptionalMemberExpression'(path) {
517
517
  const object = path.node.object;
518
518
  const property = path.node.property;
519
519
  const propertyName = path.node.computed && types.isStringLiteral(property)
520
520
  ? property.value
521
- : (types.isIdentifier(property) ? property.name : '');
521
+ : (!path.node.computed && types.isIdentifier(property) ? property.name : '');
522
522
 
523
- if (types.isMemberExpression(object)) {
523
+ if (types.isMemberExpression(object) || types.isOptionalMemberExpression(object)) {
524
524
  const owner = object.object;
525
525
  const ownerProperty = object.property;
526
526
  const ownerPropertyName = object.computed && types.isStringLiteral(ownerProperty)
527
527
  ? ownerProperty.value
528
- : (types.isIdentifier(ownerProperty) ? ownerProperty.name : '');
528
+ : (!object.computed && types.isIdentifier(ownerProperty) ? ownerProperty.name : '');
529
529
  if (
530
530
  types.isIdentifier(owner, { name: 'props' }) &&
531
531
  ownerPropertyName === 'utils' &&
@@ -0,0 +1,332 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const crypto = require('crypto');
6
+ const { isDeepStrictEqual } = require('util');
7
+ const { CliError } = require('../core/cli-error');
8
+ const { t } = require('../core/i18n');
9
+ const { resolveProjectRoot } = require('../core/utils');
10
+ const { createAuthRef, createYidaClient } = require('../core/yida-client');
11
+ const { buildYidaTitleI18n } = require('../core/yida-i18n');
12
+
13
+ const ACTIONS = ['list', 'inspect', 'save', 'create', 'order'];
14
+ const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
15
+
16
+ function fail(code, key, detail, details) {
17
+ throw new CliError(t(`form_view.${key}`, detail || ''), { code: `FORM_VIEW_${code}`, details });
18
+ }
19
+
20
+ function parseJson(value, label) {
21
+ try { return typeof value === 'string' ? JSON.parse(value) : value; }
22
+ catch (_) { return fail('INVALID_CONFIG', 'invalid_config', label); }
23
+ }
24
+
25
+ function assertObject(value, label) {
26
+ if (!object(value)) { fail('INVALID_CONFIG', 'invalid_config', label); }
27
+ return value;
28
+ }
29
+
30
+ function assertSafeKeys(value) {
31
+ if (!value || typeof value !== 'object') { return; }
32
+ for (const key of Object.keys(value)) {
33
+ if (['__proto__', 'prototype', 'constructor'].includes(key)) {
34
+ fail('INVALID_CONFIG', 'invalid_config', key);
35
+ }
36
+ assertSafeKeys(value[key]);
37
+ }
38
+ }
39
+
40
+ function normalizeConfig(raw, viewUuid) {
41
+ assertObject(raw, 'viewConfig');
42
+ if (raw.viewUuid !== viewUuid) { fail('TARGET_MISMATCH', 'target_mismatch', viewUuid); }
43
+ return {
44
+ ...raw,
45
+ viewUuid,
46
+ staticConfig: assertObject(parseJson(raw.staticConfig || {}, 'staticConfig'), 'staticConfig'),
47
+ defaultConfig: assertObject(parseJson(raw.defaultConfig || {}, 'defaultConfig'), 'defaultConfig'),
48
+ };
49
+ }
50
+
51
+ function stable(value) {
52
+ if (Array.isArray(value)) { return value.map(stable); }
53
+ if (!object(value)) { return value; }
54
+ return Object.fromEntries(Object.keys(value).sort().map(key => [key, stable(value[key])]));
55
+ }
56
+
57
+ function configHash(value) {
58
+ return crypto.createHash('sha256').update(JSON.stringify(stable({
59
+ staticConfig: value.staticConfig, defaultConfig: value.defaultConfig,
60
+ }))).digest('hex');
61
+ }
62
+
63
+ function writableConfig(value) {
64
+ // updateConfig generates this timestamp; it is not a user setting.
65
+ const defaultConfig = { ...value.defaultConfig };
66
+ delete defaultConfig.latestUpdateTime;
67
+ return { staticConfig: value.staticConfig, defaultConfig };
68
+ }
69
+
70
+ // Objects merge recursively; arrays and complete condition expressions replace.
71
+ function merge(base, patch) {
72
+ const result = { ...base };
73
+ for (const [key, value] of Object.entries(patch)) {
74
+ result[key] = object(value) && !['filter', 'dataRange'].includes(key)
75
+ ? merge(object(base[key]) ? base[key] : {}, value) : value;
76
+ }
77
+ return result;
78
+ }
79
+
80
+ function validatePatch(patch) {
81
+ assertObject(patch, 'config');
82
+ assertSafeKeys(patch);
83
+ if (!Object.keys(patch).length || Object.keys(patch).some(key => !['staticConfig', 'defaultConfig'].includes(key))) {
84
+ fail('INVALID_CONFIG', 'invalid_config', 'staticConfig / defaultConfig');
85
+ }
86
+ for (const key of Object.keys(patch)) { assertObject(patch[key], key); }
87
+ const def = patch.defaultConfig || {};
88
+ const stat = patch.staticConfig || {};
89
+ for (const [key, value] of [['showFields', def.showFields], ['lockFieldIds', def.lockFieldIds], ['columnFilter', stat.columnFilter]]) {
90
+ if (value === undefined || (key === 'showFields' && value === 'all')) { continue; }
91
+ if (!Array.isArray(value) || value.some(id => typeof id !== 'string' || !id.trim()) || new Set(value).size !== value.length) {
92
+ fail('INVALID_CONFIG', 'invalid_config', key);
93
+ }
94
+ if (key === 'showFields' && !value.length) { fail('INVALID_CONFIG', 'invalid_config', key); }
95
+ }
96
+ if (def.widths !== undefined && def.widths !== null) {
97
+ assertObject(def.widths, 'widths');
98
+ for (const width of Object.values(def.widths)) {
99
+ if (typeof width !== 'number' || !Number.isFinite(width) || width <= 0) { fail('INVALID_CONFIG', 'invalid_config', 'widths'); }
100
+ }
101
+ }
102
+ if (def.filter !== undefined) { assertObject(def.filter, 'filter'); }
103
+ for (const key of ['values', 'operationValues']) {
104
+ if (def.filter?.[key] !== undefined) { assertObject(def.filter[key], `filter.${key}`); }
105
+ }
106
+ if (def.filter?.conditionType !== undefined && !['AND', 'OR'].includes(def.filter.conditionType)) {
107
+ fail('INVALID_CONFIG', 'invalid_config', 'filter.conditionType');
108
+ }
109
+ if (def.sort !== undefined && !Array.isArray(def.sort)) { fail('INVALID_CONFIG', 'invalid_config', 'sort'); }
110
+ if (def.sort?.some(rule => !object(rule) || typeof rule.id !== 'string' || !['y', 'n'].includes(rule.isAsc))) {
111
+ fail('INVALID_CONFIG', 'invalid_config', 'sort');
112
+ }
113
+ if (stat.dataRange !== undefined) { assertObject(stat.dataRange, 'dataRange'); }
114
+ return patch;
115
+ }
116
+
117
+ function panelFields(panel) {
118
+ if (!object(panel) || !Array.isArray(panel.componentValueVoList)) { fail('UNSUPPORTED', 'unsupported'); }
119
+ const fields = [];
120
+ const visit = (items, parentId) => items.forEach(item => {
121
+ const id = item.componentId || item.id;
122
+ if (id) { fields.push({ id, parentId, name: item.varName, componentName: item.componentName,
123
+ supportVisible: item.supportVisible, showTerm: item.showTerm, operatorMap: item.operatorMap }); }
124
+ if (Array.isArray(item.children)) { visit(item.children, id); }
125
+ });
126
+ visit(panel.componentValueVoList);
127
+ return fields;
128
+ }
129
+
130
+ function validateFields(patch, fields) {
131
+ const known = new Map(fields.map(field => [field.id, field]));
132
+ const def = patch.defaultConfig || {};
133
+ const stat = patch.staticConfig || {};
134
+ const refs = [
135
+ ...(Array.isArray(def.showFields) ? def.showFields : []), ...(def.lockFieldIds || []),
136
+ ...Object.keys(def.widths || {}), ...(stat.columnFilter || []).filter(id => id !== 'all'),
137
+ ...Object.keys(def.filter?.values || {}), ...Object.keys(def.filter?.operationValues || {}),
138
+ ...(def.sort || []).map(rule => rule.id),
139
+ ];
140
+ for (const field of fields) {
141
+ if (['CascadeDateField', 'CascadeTimeZoneDateField', 'AddressField'].includes(field.componentName)) {
142
+ for (const suffix of ['_0', '_1']) { known.set(field.id + suffix, field); }
143
+ }
144
+ }
145
+ for (const id of refs) {
146
+ if (!known.has(id)) { fail('INVALID_CONFIG', 'invalid_config', `fieldId: ${id}`); }
147
+ }
148
+ for (const [id, op] of Object.entries(def.filter?.operationValues || {})) {
149
+ const operators = known.get(id)?.operatorMap;
150
+ if (Array.isArray(operators) && !operators.some(item => item.value === op)) {
151
+ fail('INVALID_CONFIG', 'invalid_config', `filter.operationValues.${id}: ${op}`);
152
+ }
153
+ }
154
+ }
155
+
156
+ function parseArgs(args) {
157
+ const positional = [];
158
+ const flags = {};
159
+ for (let i = 0; i < args.length; i++) {
160
+ const arg = args[i];
161
+ if (['--json', '--with-config', '--replace', '--help', '-h'].includes(arg)) { flags[arg] = true; continue; }
162
+ if (['--config', '--name', '--expected-hash', '--backup'].includes(arg)) {
163
+ if (!args[i + 1] || args[i + 1].startsWith('--')) { fail('USAGE', 'invalid_config', arg); }
164
+ flags[arg] = args[++i];
165
+ continue;
166
+ }
167
+ if (arg.startsWith('-')) { fail('USAGE', 'invalid_config', arg); }
168
+ positional.push(arg);
169
+ }
170
+ if (flags['--help'] || flags['-h']) { return { help: true }; }
171
+ const [action, appType, formUuid, viewUuid] = positional;
172
+ if (!ACTIONS.includes(action) || !appType || !formUuid || positional.length > (['inspect', 'save'].includes(action) ? 4 : 3)
173
+ || (['inspect', 'save'].includes(action) && !viewUuid)
174
+ || (['save', 'order'].includes(action) && !flags['--config'])
175
+ || (action === 'create' && !flags['--name']?.trim())) {
176
+ fail('USAGE', 'usage');
177
+ }
178
+ const allowed = { list: ['--with-config'], inspect: [], save: ['--config', '--expected-hash', '--backup', '--replace'], create: ['--name', '--backup'], order: ['--config', '--backup'] };
179
+ if (Object.keys(flags).some(flag => flag !== '--json' && !allowed[action].includes(flag))) { fail('USAGE', 'usage'); }
180
+ // IDs are path segments, never URLs or traversal components.
181
+ for (const id of [appType, formUuid, viewUuid].filter(Boolean)) {
182
+ if (!/^[A-Za-z0-9_-]+$/.test(id)) { fail('USAGE', 'invalid_config', id); }
183
+ }
184
+ return { action, appType, formUuid, viewUuid, flags };
185
+ }
186
+
187
+ function unwrap(response, endpoint) {
188
+ if (!response || response.success !== true || response.__needLogin || response.__csrfExpired) {
189
+ fail('API_ERROR', 'api_error', response?.errorMsg || response?.message || endpoint, {
190
+ endpoint, platformErrorCode: response?.errorCode,
191
+ });
192
+ }
193
+ return response.content;
194
+ }
195
+
196
+ function backup(value, filename) {
197
+ const output = filename ? path.resolve(filename) : path.join(resolveProjectRoot().projectRoot,
198
+ '.cache', 'openyida', 'form-view', `${Date.now()}-${crypto.randomUUID()}.json`);
199
+ fs.mkdirSync(path.dirname(output), { recursive: true });
200
+ fs.writeFileSync(output, JSON.stringify(value, null, 2), { flag: 'wx', mode: 0o600 });
201
+ return output;
202
+ }
203
+
204
+ async function execute(parsed, client = createYidaClient({ authRef: createAuthRef() })) {
205
+ const { action, appType, formUuid, viewUuid, flags } = parsed;
206
+ const prefix = `/dingtalk/web/${appType}/query/`;
207
+ const target = { appType, formUuid, ...(viewUuid ? { viewUuid } : {}) };
208
+ const read = async (endpoint, params) => unwrap(await client.postForm(prefix + endpoint, params), endpoint);
209
+ const list = async () => {
210
+ const form = await read('formdesign/getFormSchemaInfo.json', { formUuid });
211
+ if (!object(form) || !Array.isArray(form.multiViewInfos)) { fail('UNSUPPORTED', 'unsupported'); }
212
+ if (form.multiViewInfos.some(view => !object(view) || typeof view.viewUuid !== 'string' || !/^[A-Za-z0-9_-]+$/.test(view.viewUuid))
213
+ || new Set(form.multiViewInfos.map(view => view.viewUuid)).size !== form.multiViewInfos.length) { fail('UNSUPPORTED', 'unsupported'); }
214
+ return form.multiViewInfos.map(view => ({
215
+ ...view, name: parseJson(view.name, 'name'), status: parseJson(view.status, 'status'),
216
+ }));
217
+ };
218
+ const get = async (id) => normalizeConfig(await read('dataview/getViewConfig.json', { formUuid, viewUuid: id }), id);
219
+ const fields = async (id) => panelFields(unwrap(await client.get(prefix + 'dataview/getDataViewPanel.json', {
220
+ formUuid, viewUuid: id, pageType: 'manage', ccMode: 'tile',
221
+ }), 'dataview/getDataViewPanel.json'));
222
+ const views = await list();
223
+ if (viewUuid && !views.some(view => view.viewUuid === viewUuid)) { fail('TARGET_MISMATCH', 'target_mismatch', viewUuid); }
224
+ const output = { success: true, action, ...target };
225
+ if (action === 'list') {
226
+ const configs = [];
227
+ for (const view of views) {
228
+ configs.push(flags['--with-config'] ? { ...view, config: await get(view.viewUuid) } : view);
229
+ }
230
+ return { ...output, views: configs, totalCount: views.length };
231
+ }
232
+ if (action === 'inspect') {
233
+ const config = await get(viewUuid);
234
+ const view = views.find(item => item.viewUuid === viewUuid);
235
+ return { ...output, view, config, configHash: configHash(config), ...(view.viewType === 'table' ? { fields: await fields(viewUuid) } : {}) };
236
+ }
237
+ let params;
238
+ let endpoint;
239
+ let before;
240
+ let expected;
241
+ if (action === 'save') {
242
+ if (views.find(view => view.viewUuid === viewUuid).viewType !== 'table') { fail('UNSUPPORTED', 'unsupported'); }
243
+ const patch = validatePatch(parseJson(fs.readFileSync(flags['--config'], 'utf8'), 'config'));
244
+ if (flags['--replace'] && (!patch.staticConfig || !patch.defaultConfig || !flags['--expected-hash'])) {
245
+ fail('INVALID_CONFIG', 'invalid_config', '--replace requires staticConfig, defaultConfig, --expected-hash');
246
+ }
247
+ validateFields(patch, await fields(viewUuid));
248
+ before = await get(viewUuid);
249
+ if (flags['--expected-hash'] && flags['--expected-hash'] !== configHash(before)) { fail('CONFLICT', 'conflict'); }
250
+ expected = {
251
+ staticConfig: flags['--replace'] ? patch.staticConfig : merge(before.staticConfig, patch.staticConfig || {}),
252
+ defaultConfig: flags['--replace'] ? { ...patch.defaultConfig } : merge(before.defaultConfig, patch.defaultConfig || {}),
253
+ };
254
+ delete expected.defaultConfig.latestUpdateTime;
255
+ // Validate the patch, while preserving platform-owned or legacy settings as read.
256
+ if (Array.isArray(expected.defaultConfig.showFields) && expected.defaultConfig.lockFieldIds?.some(id => !expected.defaultConfig.showFields.includes(id))) {
257
+ fail('INVALID_CONFIG', 'invalid_config', 'lockFieldIds / showFields');
258
+ }
259
+ if (Array.isArray(expected.staticConfig.columnFilter) && !expected.staticConfig.columnFilter.includes('all')
260
+ && Array.isArray(expected.defaultConfig.showFields)
261
+ && expected.defaultConfig.showFields.some(id => !expected.staticConfig.columnFilter.includes(id))) {
262
+ fail('INVALID_CONFIG', 'invalid_config', 'columnFilter / showFields');
263
+ }
264
+ if (isDeepStrictEqual(writableConfig(expected), writableConfig(before))) {
265
+ return { ...output, changed: false, readbackVerified: true, config: before, configHash: configHash(before) };
266
+ }
267
+ // Re-read immediately before writing; the platform has no compare-and-swap API.
268
+ if (configHash(await get(viewUuid)) !== configHash(before)) { fail('CONFLICT', 'conflict'); }
269
+ params = { formUuid, viewUuid, staticConfig: JSON.stringify(expected.staticConfig), defaultConfig: JSON.stringify(expected.defaultConfig) };
270
+ endpoint = 'dataview/updateConfig.json';
271
+ } else if (action === 'order') {
272
+ expected = parseJson(fs.readFileSync(flags['--config'], 'utf8'), 'order');
273
+ const ids = views.map(view => view.viewUuid);
274
+ if (!Array.isArray(expected) || expected.length !== ids.length || new Set(expected).size !== ids.length || expected.some(id => !ids.includes(id))) {
275
+ fail('INVALID_CONFIG', 'invalid_config', 'order');
276
+ }
277
+ before = ids;
278
+ if (isDeepStrictEqual(before, expected)) { return { ...output, changed: false, views, readbackVerified: true }; }
279
+ if (!isDeepStrictEqual((await list()).map(view => view.viewUuid), before)) { fail('CONFLICT', 'conflict'); }
280
+ params = { formUuid, viewConfig: JSON.stringify(expected) };
281
+ endpoint = 'dataview/updateViewPageConfig.json';
282
+ } else {
283
+ expected = flags['--name'].trim();
284
+ before = views;
285
+ params = { formUuid, dataFormUuid: formUuid, viewType: 'table', name: JSON.stringify(buildYidaTitleI18n(expected)),
286
+ status: JSON.stringify({ PC: 'ACTIVATED', MOBILE: 'ACTIVATED' }) };
287
+ endpoint = 'dataview/createView.json';
288
+ }
289
+ const backupPath = backup({ ...target, action, config: before }, flags['--backup']);
290
+ const mutationDetails = { target, backupPath, retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true };
291
+ try {
292
+ // Never replay a mutation automatically after transport failure or auth refresh.
293
+ unwrap(await client.postFormOnce(prefix + endpoint, params), endpoint);
294
+ } catch (error) {
295
+ throw new CliError(error.message, { code: error.code || 'FORM_VIEW_WRITE_FAILED', details: mutationDetails });
296
+ }
297
+ try {
298
+ if (action === 'save') {
299
+ const config = await get(viewUuid);
300
+ if (!isDeepStrictEqual(stable(writableConfig(expected)), stable(writableConfig(config)))) {
301
+ fail('READBACK_MISMATCH', 'readback_mismatch');
302
+ }
303
+ return { ...output, changed: true, backupPath, readbackVerified: true, config, configHash: configHash(config) };
304
+ }
305
+ const after = await list();
306
+ if (action === 'order' && !isDeepStrictEqual(after.map(view => view.viewUuid), expected)) { fail('READBACK_MISMATCH', 'readback_mismatch'); }
307
+ if (action === 'create') {
308
+ const added = after.filter(view => !before.some(old => old.viewUuid === view.viewUuid));
309
+ if (added.length !== 1 || !Object.values(added[0].name || {}).includes(expected)) { fail('READBACK_MISMATCH', 'readback_mismatch'); }
310
+ return { ...output, viewUuid: added[0].viewUuid, view: added[0], backupPath, readbackVerified: true };
311
+ }
312
+ return { ...output, views: after, backupPath, readbackVerified: true };
313
+ } catch (error) {
314
+ throw new CliError(error.message, { code: error.code || 'FORM_VIEW_READBACK_FAILED',
315
+ details: { ...mutationDetails, mutationAccepted: true, sideEffectState: 'accepted_unverified' } });
316
+ }
317
+ }
318
+
319
+ async function run(args, options = {}) {
320
+ const parsed = parseArgs(args);
321
+ if (parsed.help) { process.stdout.write(t('form_view.usage') + '\n'); return; }
322
+ let result;
323
+ try { result = await execute(parsed, options.client); }
324
+ catch (error) {
325
+ if (error.isCliError) { throw error; }
326
+ throw new CliError(error.message, { code: 'FORM_VIEW_FAILED' });
327
+ }
328
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
329
+ return result;
330
+ }
331
+
332
+ module.exports = { run, execute, parseArgs, normalizeConfig, configHash, merge, validatePatch };
@@ -526,6 +526,12 @@ const COMMAND_SIDE_EFFECTS = new Map([
526
526
  mutating_actions: ['connect', 'run', 'disconnect', 'logout'],
527
527
  note: 'Explicit local-agent control. Remote runs use only the current task grant and never read a personal OpenYida profile.',
528
528
  })],
529
+ ['form-view', sideEffect('mixed', {
530
+ mutates_yida: true,
531
+ mutates_local: true,
532
+ read_actions: ['list', 'inspect'],
533
+ mutating_actions: ['save', 'create', 'order'],
534
+ })],
529
535
  ['aggregate-table', sideEffect('mixed', {
530
536
  mutates_yida: true,
531
537
  mutates_local: false,
@@ -808,6 +814,12 @@ const COMMAND_PERMISSIONS = new Map([
808
814
  ask_actions: ['connect', 'run', 'disconnect', 'logout'],
809
815
  })),
810
816
 
817
+ ...permissionEntries([
818
+ 'form-view',
819
+ ], actionDependentPermission({
820
+ preauthorized_actions: ['save', 'create', 'order'],
821
+ })),
822
+
811
823
  ...permissionEntries([
812
824
  'aggregate-table',
813
825
  ], actionDependentPermission({
@@ -1496,6 +1508,18 @@ const COMMAND_GROUPS = [
1496
1508
  },
1497
1509
  output: 'json',
1498
1510
  }),
1511
+ command('form-view', ['form-view'], 'form-view <list|inspect|save|create|order> <appType> <formUuid> [viewUuid] [--config <file>] [--name <title>] [--with-config] [--expected-hash <hash>] [--backup <file>] [--replace] [--json]', 'help.cmd_form_view', {
1512
+ managedRun: { scope: 'actions', actions: {
1513
+ list: { scope: 'app', appPosition: 1 },
1514
+ inspect: { scope: 'app', appPosition: 1 },
1515
+ save: { scope: 'app', appPosition: 1, mutation: { operation: 'update_form', formUuidPosition: 2 } },
1516
+ create: { scope: 'app', appPosition: 1, mutation: { operation: 'update_form', formUuidPosition: 2 } },
1517
+ order: { scope: 'app', appPosition: 1, mutation: { operation: 'update_form', formUuidPosition: 2 } },
1518
+ } },
1519
+ output: 'json',
1520
+ examples: ['openyida form-view list APP_XXX FORM_XXX --with-config --json', 'openyida form-view save APP_XXX FORM_XXX VIEW_XXX --config view-patch.json --json'],
1521
+ notes: ['Reads native multiViewInfos and per-view staticConfig/defaultConfig. Saves a recursive object patch; arrays, filter and dataRange replace. --replace restores full staticConfig/defaultConfig and requires --expected-hash. showFields controls visibility and order, widths controls pixel widths, lockFieldIds controls frozen columns, filter controls default filtering, staticConfig.columnFilter controls available fields. Backups precede mutations and success requires readback. --expected-hash detects stale edits; the platform has no atomic compare-and-swap. Personal browser view settings can override published defaults. create adds a table view; order requires every existing view UUID exactly once. No delete operation.'],
1522
+ }),
1499
1523
  command('get-schema', ['get-schema'], 'get-schema <appType> <formUuid|--all> [--summary-json|--field-map-json|--analysis-json]', 'help.cmd_get_schema', { managedRun: { scope: 'app', appPosition: 0 },
1500
1524
  examples: ['openyida get-schema APP_XXX FORM_XXX --field-map-json'],
1501
1525
  }),
@@ -4,6 +4,15 @@
4
4
  'use strict';
5
5
 
6
6
  module.exports = {
7
+ form_view: {
8
+ usage: 'Usage: openyida form-view <list|inspect|save|create|order> <appType> <formUuid> [viewUuid] [--config <file>] [--name <title>] [--with-config] [--expected-hash <hash>] [--backup <file>] [--replace] [--json]',
9
+ invalid_config: 'Invalid native view configuration or argument: %s',
10
+ target_mismatch: 'The view does not belong to the target form: %s',
11
+ api_error: 'Native view API failed: %s. Check platform permissions and edition availability; do not retry writes blindly.',
12
+ unsupported: 'The platform did not return a native multi-view list. Check the form type and platform support.',
13
+ conflict: 'The view changed since it was read. Inspect it again before saving.',
14
+ readback_mismatch: 'The saved view does not match readback. Keep the backup and inspect the remote view before retrying.',
15
+ },
7
16
  integration_manage: {'failed': 'Automation operation failed: {0}', 'usage': 'integration <monitor|copy|rename|delete> <appType> ...', 'desc_monitor': 'Read or save automation error monitors', 'desc_copy': 'Copy an automation as disabled', 'desc_rename': 'Rename an automation', 'desc_delete': 'Delete a disabled automation'},
8
17
  permission_manage: {'failed': 'Permission group operation failed: {0}', 'usage': 'permission-manage <copy|rename|delete> <appType> <formUuid> <packageUuid> [--name name] [--dry-run] [--confirm]', 'description': 'Copy, rename or delete a permission group'},
9
18
  matrix_manage: {'failed': 'Matrix operation failed: {0}', 'usage': 'matrix <list|detail|references|rows|create|update|delete|upsert-rows|delete-rows> [matrixId] [--file config.json] [--dry-run] [--confirm]', 'description': 'Manage permission matrix definitions, references and rows'},
@@ -148,6 +157,7 @@ module.exports = {
148
157
  cmd_validate_form: 'Validate form field JSON locally',
149
158
  cmd_update_form: 'Update a form page',
150
159
  cmd_list_forms: 'List forms/pages in an app',
160
+ cmd_form_view: 'Native form views: list, inspect and configure columns, widths, frozen columns and filters',
151
161
  cmd_aggregate_table: 'Manage aggregate tables (virtualView)',
152
162
  cmd_get_schema: 'Get one form Schema or all form Schemas',
153
163
  cmd_check_prd_completeness: 'Check resources, frontend menu hiding and application theme delivery',
@@ -4,6 +4,15 @@
4
4
  'use strict';
5
5
 
6
6
  module.exports = {
7
+ form_view: {
8
+ usage: '用法:openyida form-view <list|inspect|save|create|order> <appType> <formUuid> [viewUuid] [--config <文件>] [--name <名称>] [--with-config] [--expected-hash <哈希>] [--backup <文件>] [--replace] [--json]',
9
+ invalid_config: '原生视图配置或参数无效:%s',
10
+ target_mismatch: '视图不属于目标表单:%s',
11
+ api_error: '原生视图接口失败:%s。请检查平台权限和版本能力,不要盲目重试写入。',
12
+ unsupported: '平台未返回原生多视图列表,请检查表单类型及平台支持情况。',
13
+ conflict: '视图在读取后发生变化,请重新检查配置再保存。',
14
+ readback_mismatch: '保存配置与回读结果不一致,请保留备份并核对远程视图后再重试。',
15
+ },
7
16
  integration_manage: {'failed': '自动化操作失败:{0}', 'usage': 'integration <monitor|copy|rename|delete> <appType> ...', 'desc_monitor': '查询或保存自动化异常监听', 'desc_copy': '复制自动化,默认停用', 'desc_rename': '重命名自动化', 'desc_delete': '删除已停用的自动化'},
8
17
  permission_manage: {'failed': '权限组操作失败:{0}', 'usage': 'permission-manage <copy|rename|delete> <appType> <formUuid> <packageUuid> [--name name] [--dry-run] [--confirm]', 'description': '复制、重命名或删除权限组'},
9
18
  matrix_manage: {'failed': '矩阵操作失败:{0}', 'usage': 'matrix <list|detail|references|rows|create|update|delete|upsert-rows|delete-rows> [matrixId] [--file config.json] [--dry-run] [--confirm]', 'description': '管理权限矩阵定义、引用及数据行'},
@@ -148,6 +157,7 @@ module.exports = {
148
157
  cmd_validate_form: '本地校验表单字段 JSON',
149
158
  cmd_update_form: '更新表单页面',
150
159
  cmd_list_forms: '列出应用下的表单/页面',
160
+ cmd_form_view: '原生表单视图:查询、检查并配置显示列、列宽、冻结列和筛选',
151
161
  cmd_aggregate_table: '管理聚合表(virtualView)',
152
162
  cmd_get_schema: '获取单个或全部表单 Schema',
153
163
  cmd_check_prd_completeness: '检查资源数量、前台菜单隐藏及应用主题同步',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openyida",
3
- "version": "2026.10.18-3",
3
+ "version": "2026.10.19",
4
4
  "description": "OpenYida CLI - 宜搭低代码 AI 开发工具(安装即用,零配置)",
5
5
  "bin": {
6
6
  "openyida": "bin/yida.js",
@@ -140,7 +140,7 @@ Fast / Plan 选定的主题和主色属于整个应用:导航、原生页、
140
140
  | `yida-skills/context` | 登录、退出、切换组织、组织版本/容量、Schema、fieldId、执行前检查 | `yida-login`、`yida-logout`、`yida-basic-info`、`yida-get-schema`、`yida-corp-efficiency` |
141
141
  | `yida-skills/app` | 从零搭应用、完整系统、应用启停、应用导航、多语言、AI 老应用主题升级 | `yida-app`、`yida-create-app`、`yida-app-lifecycle`、`yida-nav-group`、`yida-i18n`、`yida-upgrade-app-theme`(仅明确升级要求并 ask_human 确认) |
142
142
  | `yida-skills/design` | 完整应用需求分析、PRD、视觉设计、单页 UI 改造、应用主题色、全局换肤、图片素材 | `yida-requirement-analysis`、`yida-prd`、`yida-design`、`yida-data-manage-ui`、`yida-image-assets` |
143
- | `yida-skills/form` | 表单字段、公式、校验、业务关联规则、批量录入、数据记录 | `yida-create-form-page`、`yida-formula`、`yida-formula-evaluate`、`yida-business-rule`、`yida-canvas-table-form`、`yida-table-form`、`yida-data-management` |
143
+ | `yida-skills/form` | 表单字段、公式、校验、业务关联规则、批量录入、数据记录 | `yida-create-form-page`、`yida-form-view`、`yida-formula`、`yida-formula-evaluate`、`yida-business-rule`、`yida-canvas-table-form`、`yida-table-form`、`yida-data-management` |
144
144
  | `yida-skills/process` | 审批、流程表单、流程规则、节点/分支/字段权限、流程代理 | `yida-create-process`、`yida-process-rule`、`yida-flow-node-config`、`yida-agent-center` |
145
145
  | `yida-skills/page` | 工作台、办事门户、任务看板、自定义展示页、页面源码开发、平台 JSX 组件页面维护、页面发布、页面内导航、PPT 页面 | `yida-create-page`、`yida-canvas-custom-page`、`yida-custom-page`、`yida-canvas-data-binding`、`yida-canvas-upgrade`、`yida-publish-page`、`yida-openyida-publish-guard`、`yida-density`、`yida-nav-shell`、`yida-ppt-slider` |
146
146
  | `yida-skills/analytics` | 聚合表、虚拟视图、报表、统计、图表、Recharts、ECharts、经营分析看板、驾驶舱、大屏 | `yida-aggregate-table`、`yida-report`、`yida-rechart`、`yida-chart`、`yida-dashboard` |
@@ -190,6 +190,7 @@ Fast / Plan 选定的主题和主色属于整个应用:导航、原生页、
190
190
  | 产品化经营分析看板/驾驶舱交付 | `yida-dashboard`,按指标分析任务触发 |
191
191
  | 待办、操作队列、业务工作台或办事门户页面 | `yida-canvas-custom-page`;接真实数据时加载 `yida-canvas-data-binding`,不因页面名称或辅助统计转为经营看板 |
192
192
  | 批量录入、表格填写、多行编辑 | 默认 `yida-canvas-table-form`;已检测到平台 JSX 组件页面、native 页面或存量源码使用 `this.utils.yida.saveFormData` 时用 `yida-table-form` |
193
+ | 原生数据管理页视图列表、显示列配置、字段显隐与顺序、列宽、冻结列、默认筛选、多视图 | `yida-form-view`;使用原生配置读写,保存前备份并回读,不通过 CSS 隐藏字段或重建自定义列表 |
193
194
  | 当前数据管理页换风格、只留横线、突出新增/筛选/行操作、列级强调、状态列标签、数据管理 UI 异常、自定义简单数据管理 | `yida-data-manage-ui`(优先于通用美化);保留承载类型和业务,从目标推导维度、定位 token/CSS 并按页改写;备份发布并实测,不要求选择配方 |
194
195
  | 页面视觉方向、页面美化、去 AI 味、优化应用 UI、不好看、太乱、美化后不满意或恢复 | `yida-design`;已有资源优先走 `sub_skill/ui-improve/SKILL.md`,先展示视觉方案,经 `ask_human` 确认并备份后才交接实施,只改 UI;不满意恢复本轮前状态。新页面设计沿用原设计流程 |
195
196
  | 应用级主题、品牌色、全局换肤 | `yida-design`;已有应用按 ui-improve 确认应用级影响范围和恢复方式后执行 |
@@ -26,6 +26,7 @@
26
26
  9. 分析、复刻或迁移已有表单时,执行 `use_skill("yida-get-schema", "读取字段与行为语义")`,对每个核心表单读取一次 `--analysis-json`;把字段结构与 `actions/fieldBehaviors/associationRuleCount` 分开规划,字段事件动作使用 `yida-create-form-page` 的原子 `field-action`,数据源使用 `bind-datasource`。
27
27
  10. PRD 明确包含原生报表时,执行 `use_skill("yida-report", "按业务统计语义创建原生报表")`;地域分布、日历统计分别使用已支持的 `map`、`calendarHeatmap`,不得无声明退化成柱/饼图。
28
28
  11. PRD 明确包含集成自动化时,执行 `use_skill("yida-integration", "按业务动作创建自动化")`;已有应用先用全类型 `integration list --json` 盘点,创建时区分通知、数据新增/更新、审批完成、定时和手动触发。CLI 不支持的触发类型输出 capability gap,不得用通知替代。
29
+ 12. PRD 已规划原生列表的显示列、列宽、冻结、默认筛选或多视图时,表单就绪后执行 `use_skill("yida-form-view", "配置原生数据管理视图")`;用 list/inspect 取得真实视图 UUID、字段与操作符,再按规划 save/create/order 并回读。未要求视图配置的资源级任务不额外改设置;不通过 CSS 隐藏字段或改建自定义列表实现原生列配置。
29
30
 
30
31
  ## 字段配置文件示例
31
32
 
@@ -7,6 +7,8 @@ description: 宜搭数据管理页美化方法:从用户目标推导线条、
7
7
 
8
8
  本技能沉淀的是**怎么改**,不是风格清单。先理解用户希望更容易看到什么、完成什么,再选择需要改变的视觉维度。可以产生未被命名、未收录的新样式;不要求先选 style ID,不要求运行配方生成器。已有明确风格、参考或方案授权时直接沿用,不重复选型;只有用户要求预览时展示方案,并区分示意与实际结果。
9
9
 
10
+ 字段显隐、字段顺序、列宽、冻结列、默认筛选及多视图配置使用 `yida-form-view` 的原生配置命令;不要用 CSS 隐藏字段、改虚拟表格几何或重建列表来实现。
11
+
10
12
  ## 1. 定位当前页面与约束
11
13
 
12
14
  从访问 URL、应用导航和浏览器定位 appType、formUuid、viewUuid。详情页 URL 不等于列表入口。用 `get-schema`、`list-forms` 和实际 DOM 核对原生/自定义类型、真实字段 ID、主题和源码;不能只看截图猜选择器。没有迁移要求时保留承载方式。
@@ -0,0 +1,58 @@
1
+ ---
2
+ name: yida-form-view
3
+ description: 宜搭原生数据管理视图配置。读取全部视图、显示列和表格设置,配置字段显隐与顺序、列宽、冻结列、默认筛选,新建表格视图及调整视图顺序。保留表单字段、业务记录和权限。
4
+ ---
5
+
6
+ # 原生表单视图
7
+
8
+ 用于原生数据管理页的配置;仅换颜色、边框和密度用 `yida-data-manage-ui`,修改表单字段用 `yida-create-form-page`,查询记录用 `yida-data-management`。
9
+
10
+ ## 执行
11
+
12
+ 1. 按根技能核验登录和 app/form 上下文,复用已有表单。不要创建同名表单或用自定义页面替换原生列表。
13
+ 2. 执行 `openyida form-view list <appType> <formUuid> --with-config --json`,读取全部视图及 `staticConfig/defaultConfig`。选择返回的真实 `viewUuid`,不要猜测或把 `formUuid` 当作视图 ID。
14
+ 3. 执行 `openyida form-view inspect <appType> <formUuid> <viewUuid> --json`。消费 `fields[].id/operatorMap`、`config` 和 `configHash`;需要业务字段身份时同时使用 `get-schema --compact --resolve-fields`。操作符大小写与平台返回一致,不猜测 `eq/EQ`。
15
+ 4. 将本轮最小配置写到项目缓存 JSON 文件;执行 `openyida form-view save <appType> <formUuid> <viewUuid> --config <file> --expected-hash <configHash> --json`。CLI 先备份,再保存并回读;仅 `readbackVerified=true` 为配置成功。
16
+ 5. 打开真实数据管理页,验证列顺序、宽度、冻结、筛选、横向滚动及记录。个人浏览器视图偏好可能覆盖管理员默认配置,不能仅凭 API 回读宣称浏览器验收通过。
17
+
18
+ ## 配置映射
19
+
20
+ | 配置 | 原生字段 |
21
+ | --- | --- |
22
+ | 默认显示列及顺序 | `defaultConfig.showFields`,真实字段 ID 数组;`"all"` 为全部;不能传空数组 |
23
+ | 列宽 | `defaultConfig.widths`,字段 ID 到正数像素宽度的对象;`null` 清空宽度设置 |
24
+ | 冻结列 | `defaultConfig.lockFieldIds`,已显示字段 ID 数组;`[]` 取消冻结 |
25
+ | 默认筛选 | `defaultConfig.filter`,`values/operationValues/conditionType` 原生对象;`{}` 清空 |
26
+ | 默认排序 | `defaultConfig.sort`,`[{"id":"真实字段ID","isAsc":"y"}]`;`n` 降序 |
27
+ | 可用字段 | `staticConfig.columnFilter`,字段 ID 数组,平台 `['all']` 表示全部;它不是数据权限 |
28
+ | 视图数据范围 | `staticConfig.dataRange`,使用现有原生规则结构;`{}` 清空,验收实际查询结果 |
29
+
30
+ 示例(必须替换为 inspect 返回的真实字段 ID):
31
+
32
+ ```json
33
+ {
34
+ "defaultConfig": {
35
+ "showFields": ["textField_REAL", "selectField_REAL"],
36
+ "widths": {"textField_REAL": 280, "selectField_REAL": 160},
37
+ "lockFieldIds": ["textField_REAL"]
38
+ }
39
+ }
40
+ ```
41
+
42
+ 对象递归合并,数组替换;`filter` 和 `dataRange` 整体替换。未指定的配置保留;不要提交从空对象重造的整套设置。服务端自动更新 `latestUpdateTime`,不能把它当成用户配置。
43
+
44
+ ## 多视图
45
+
46
+ ```bash
47
+ openyida form-view create <appType> <formUuid> --name "待处理订单" --json
48
+ openyida form-view order <appType> <formUuid> --config <order.json> --json
49
+ ```
50
+
51
+ create 只新建 table 视图,成功后复用返回 UUID 再配置。order 文件是包含当前全部视图 UUID 的数组(包括 form 视图),每项恰好一次,首项为默认入口。当前写配置仅支持 table,读取保留其他视图类型。
52
+
53
+ ## 失败与恢复
54
+
55
+ - `FORM_VIEW_CONFLICT`:重新 inspect 后合并本轮修改;平台没有原子条件写入,避免多人同时编辑。
56
+ - 平台版本或权限拒绝:保留平台错误,停止写入;不修改版本、授权或额度。
57
+ - 写入传输错误/回读不一致:保留 `backupPath`,先 inspect 核对线上结果;不自动重放 create/save。
58
+ - save 的备份文件包含原配置 `config.staticConfig/defaultConfig`。恢复时提取这两个对象写入恢复文件,重新 inspect 后使用 `save --replace --expected-hash <当前哈希>`;全量恢复必须提供两个完整配置对象,删除备份后新增的设置;其他资源和记录不在视图备份范围内。
@@ -3,6 +3,42 @@
3
3
  "source": "openyida",
4
4
  "entry": "openyida",
5
5
  "skills": [
6
+ {
7
+ "name": "yida-form-view",
8
+ "path": "skills/yida-form-view/SKILL.md",
9
+ "display_name": "原生数据管理视图配置",
10
+ "description": "读取原生视图列表、显示列和表格设置,配置字段显隐与顺序、列宽、冻结列、默认筛选,新建表格视图和调整顺序。",
11
+ "category": "yida-skills/form",
12
+ "tags": [
13
+ "原生视图",
14
+ "显示列",
15
+ "冻结列",
16
+ "列宽",
17
+ "默认筛选"
18
+ ],
19
+ "aliases": [
20
+ "表格设置",
21
+ "数据管理字段显示配置"
22
+ ],
23
+ "positive_signals": [
24
+ "获取表单全部视图配置",
25
+ "设置数据管理页显示列",
26
+ "调整原生表格列宽",
27
+ "冻结订单名称列",
28
+ "新建待处理视图",
29
+ "配置默认筛选条件"
30
+ ],
31
+ "negative_signals": [
32
+ "仅美化颜色边框",
33
+ "新增业务记录",
34
+ "修改表单字段结构"
35
+ ],
36
+ "command_ids": [
37
+ "form-view",
38
+ "get-schema"
39
+ ],
40
+ "requires_login": true
41
+ },
6
42
  {
7
43
  "name": "yida-data-manage-ui",
8
44
  "path": "skills/yida-data-manage-ui/SKILL.md",