openyida 2026.10.21 → 2026.10.22-beta.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 +24 -3
- package/bin/yida.js +4 -0
- package/lib/app/canvas-compile.js +8 -0
- package/lib/app/page-linter.js +10 -0
- package/lib/app/responsive-layout-guard.js +82 -0
- package/lib/auth/qwenwork-client.js +190 -0
- package/lib/auth/qwenwork.js +37 -0
- package/lib/core/command-manifest.js +18 -10
- package/lib/core/locales/en.js +63 -4
- package/lib/core/locales/zh.js +62 -3
- package/lib/core/query-data.js +40 -2
- package/lib/core/sample.js +3 -0
- package/lib/feedback/feedback.js +2 -1
- package/lib/feedback/github-setup.js +97 -0
- package/lib/feedback/github.js +17 -8
- package/lib/feedback/report.js +35 -15
- package/lib/integration/integration-triggers.js +8 -3
- package/lib/process/configure-process.js +8 -2
- package/lib/process/process-tasks.js +175 -0
- package/lib/process/services/process-actor-config.js +3 -0
- package/lib/process/services/process-compiler.js +15 -0
- package/lib/process/services/process-dynamic-actors.js +165 -0
- package/lib/samples/openyida-scaffold/canvas-nav/mixed.jsx +1 -1
- package/lib/samples/openyida-scaffold/canvas-nav/shared.jsx +1 -1
- package/lib/samples/openyida-scaffold/canvas-nav/side.jsx +1 -1
- package/lib/samples/openyida-scaffold/canvas-nav/sidebar.jsx +36 -24
- package/lib/samples/openyida-scaffold/canvas-nav/top.jsx +1 -1
- package/lib/samples/openyida-scaffold/canvas-responsive-layout.jsx +104 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -0
- package/yida-skills/SKILL.md +5 -2
- package/yida-skills/skills/yida-canvas-custom-page/SKILL.md +5 -1
- package/yida-skills/skills/yida-custom-page/SKILL.md +6 -2
- package/yida-skills/skills/yida-data-management/SKILL.md +24 -3
- package/yida-skills/skills/yida-data-management/references/process-task-transfer.md +53 -0
- package/yida-skills/skills/yida-density/SKILL.md +6 -1
- package/yida-skills/skills/yida-design/SKILL.md +1 -1
- package/yida-skills/skills/yida-design/references/responsive-layout.md +69 -22
- package/yida-skills/skills/yida-design/references/task-driven-visual-design.md +2 -0
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/SKILL.md +1 -0
- package/yida-skills/skills/yida-design/sub_skill/yida-design-plan/references/build-plan-compact-schema.md +1 -1
- package/yida-skills/skills/yida-design/workflow/output-design.md +2 -2
- package/yida-skills/skills/yida-design/workflow/step-1-read-brief.md +2 -0
- package/yida-skills/skills/yida-design/workflow/step-4-wireframe-interaction.md +10 -10
- package/yida-skills/skills/yida-feedback/SKILL.md +22 -5
- package/yida-skills/skills/yida-feedback/references/github-contribution.md +14 -8
- package/yida-skills/skills/yida-feedback/references/github-setup.md +74 -0
- package/yida-skills/skills/yida-feedback/references/report-input.md +10 -2
- package/yida-skills/skills/yida-feedback/references/user-interaction.md +19 -2
- package/yida-skills/skills/yida-flow-node-config/SKILL.md +1 -1
- package/yida-skills/skills/yida-flow-node-config/references/actors.md +4 -0
- package/yida-skills/skills/yida-flow-node-config/references/dynamic-actors.md +84 -0
- package/yida-skills/skills/yida-integration/SKILL.md +1 -0
- package/yida-skills/skills/yida-integration/references/triggers.md +14 -1
- package/yida-skills/skills/yida-login/SKILL.md +12 -0
- package/yida-skills/skills/yida-prd/SKILL.md +1 -0
- package/yida-skills/skills-index.json +13 -4
package/lib/feedback/report.js
CHANGED
|
@@ -13,6 +13,15 @@ const MAX_INPUT_BYTES = 1024 * 1024;
|
|
|
13
13
|
const REPOSITORY = 'openyida/openyida';
|
|
14
14
|
const TEXT_FIELDS = ['title', 'summary', 'command', 'expected', 'actual', 'impact', 'tool', 'model'];
|
|
15
15
|
const LIST_FIELDS = ['steps', 'evidence', 'attempts'];
|
|
16
|
+
const INCIDENT_FIELDS = ['openyida', 'node', 'platform', 'arch', 'installation', 'browser', 'deployment', 'organizationContext'];
|
|
17
|
+
|
|
18
|
+
function incidentEnvironment(value) {
|
|
19
|
+
if (value === undefined) {value = {};}
|
|
20
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {throw invalid('object');}
|
|
21
|
+
// Only explicit incident facts belong here. The reporting process may run
|
|
22
|
+
// after an upgrade or on another machine; never infer incident versions.
|
|
23
|
+
return Object.fromEntries(INCIDENT_FIELDS.map(key => [key, sanitizeText(value[key])]));
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
function comparisonRun(value) {
|
|
18
27
|
if (value === undefined) {return { status: 'not_run', evidence: [] };}
|
|
@@ -98,6 +107,8 @@ function buildReport(input) {
|
|
|
98
107
|
throw invalid('confirmed');
|
|
99
108
|
}
|
|
100
109
|
report.screenshots = [];
|
|
110
|
+
report.incidentEnvironment = incidentEnvironment(input.incidentEnvironment);
|
|
111
|
+
// Retain this output contract for existing consumers: generator context only.
|
|
101
112
|
report.environment = {
|
|
102
113
|
openyida: require('../../package.json').version,
|
|
103
114
|
node: process.version,
|
|
@@ -114,17 +125,28 @@ function buildReport(input) {
|
|
|
114
125
|
|
|
115
126
|
// Escape markup so copied diagnostics cannot inject images, links or HTML in an issue.
|
|
116
127
|
function markdownText(value) {
|
|
117
|
-
return String(value || '
|
|
128
|
+
return String(value || t('feedback_report.body.unknown')).replace(/[\\`*_{}[\]<>()#!|~]/g, '\\$&');
|
|
118
129
|
}
|
|
119
130
|
|
|
120
131
|
function renderMarkdown(report) {
|
|
132
|
+
const label = key => t('feedback_report.body.' + key);
|
|
133
|
+
const incident = report.incidentEnvironment || {};
|
|
134
|
+
const environmentLines = Object.entries({
|
|
135
|
+
openyida: incident.openyida, node: incident.node, platform: incident.platform, arch: incident.arch,
|
|
136
|
+
installation: incident.installation, tool: report.tool, model: report.model,
|
|
137
|
+
...Object.fromEntries(['browser', 'deployment', 'organizationContext']
|
|
138
|
+
.filter(key => incident[key]).map(key => [key, incident[key]])),
|
|
139
|
+
}).map(([key, value]) => `${key}: ${value || label('unknown')}`);
|
|
121
140
|
const sections = [
|
|
122
|
-
['
|
|
123
|
-
['
|
|
124
|
-
['
|
|
125
|
-
['
|
|
126
|
-
['
|
|
127
|
-
|
|
141
|
+
[label('summary'), report.summary || label('unknown')],
|
|
142
|
+
[label('actual'), report.actual], [label('expected'), report.expected || label('unknown')],
|
|
143
|
+
[label('incident'), environmentLines],
|
|
144
|
+
[label('command'), report.command], [label('steps'), report.steps],
|
|
145
|
+
[label('impact'), report.impact], [label('evidence'), report.evidence], [label('attempts'), report.attempts],
|
|
146
|
+
...(report.cause.summary || report.cause.status !== 'unknown' || report.cause.category !== 'unknown'
|
|
147
|
+
? [[label('cause'), `${report.cause.category} / ${report.cause.status}\n${report.cause.summary}`]] : []),
|
|
148
|
+
[label('cause_evidence'), report.cause.evidence], [label('next'), report.cause.nextSteps],
|
|
149
|
+
[label('screenshots'), report.screenshots.map(item => `${item.file}: ${item.description || label('review_image')}`)],
|
|
128
150
|
...(['before', 'after', 'regression'].some(key => report.comparison[key].status !== 'not_run') || report.comparison.limitations.length ? ['before', 'after', 'regression'] : []).map(key => [
|
|
129
151
|
`Test comparison: ${key}`, [
|
|
130
152
|
`Status: ${report.comparison[key].status}`,
|
|
@@ -134,19 +156,17 @@ function renderMarkdown(report) {
|
|
|
134
156
|
...(report.comparison[key].evidence || []),
|
|
135
157
|
],
|
|
136
158
|
]),
|
|
137
|
-
...(report.comparison.limitations.length ? [['
|
|
138
|
-
|
|
159
|
+
...(report.comparison.limitations.length ? [[label('limitations'), report.comparison.limitations]] : []),
|
|
160
|
+
[label('generator'), Object.entries(report.environment).map(([key, value]) => `${key}: ${value}`)],
|
|
161
|
+
].filter(([, value]) => Array.isArray(value) ? value.length : Boolean(value));
|
|
139
162
|
return [
|
|
140
163
|
`# ${markdownText(report.title)}`, '',
|
|
141
|
-
'
|
|
142
|
-
`Fingerprint: ${report.fingerprint}`, `Observed occurrences: ${report.occurrenceCount || 'Unknown'}`, '',
|
|
143
|
-
'## Environment', '',
|
|
144
|
-
...Object.entries({ ...report.environment, tool: report.tool, model: report.model })
|
|
145
|
-
.map(([key, value]) => `- ${key}: ${markdownText(value)}`), '',
|
|
164
|
+
label('draft'), '',
|
|
146
165
|
...sections.flatMap(([heading, value]) => [
|
|
147
166
|
`## ${heading}`, '',
|
|
148
|
-
Array.isArray(value) ?
|
|
167
|
+
Array.isArray(value) ? value.map(item => `- ${markdownText(item)}`).join('\n') : markdownText(value), '',
|
|
149
168
|
]),
|
|
169
|
+
`Fingerprint: ${report.fingerprint}`, `Observed occurrences: ${report.occurrenceCount || label('unknown')}`, '',
|
|
150
170
|
].join('\n');
|
|
151
171
|
}
|
|
152
172
|
|
|
@@ -18,11 +18,16 @@ function scheduleNodes(schedule, id, nextId) {
|
|
|
18
18
|
if (!/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:00$/.test(startDate || '')
|
|
19
19
|
|| !Number.isFinite(Date.parse(startDate.replace(' ', 'T') + '+08:00'))
|
|
20
20
|
|| !['ONCE', 'DAILY', 'WORKDAY', 'WEEKLY', 'BI_WEEKLY', 'MONTHLY', 'LAST_DAY_PER_MONTH'].includes(repeat)
|
|
21
|
-
|| maxCount !== undefined && (!Number.isInteger(maxCount) || maxCount < 1)
|
|
21
|
+
|| maxCount !== undefined && (!Number.isInteger(maxCount) || maxCount < 1 || maxCount > 100)
|
|
22
22
|
|| expireDate !== undefined && (!Number.isFinite(expireDate)
|
|
23
23
|
|| expireDate <= Date.parse(startDate.replace(' ', 'T') + '+08:00'))
|
|
24
24
|
|| maxCount !== undefined && expireDate !== undefined
|
|
25
25
|
|| repeat === 'ONCE' && (maxCount !== undefined || expireDate !== undefined)) { invalid('schedule values'); }
|
|
26
|
+
if (repeat !== 'ONCE' && maxCount === undefined && expireDate === undefined) {
|
|
27
|
+
throw new CliError(t('integration_triggers.end_required'), {
|
|
28
|
+
code: 'INTEGRATION_TRIGGER_INVALID', details: { reason: 'schedule end condition', remoteWrites: 0 },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
26
31
|
const startTimestamp = Date.parse(startDate.replace(' ', 'T') + '+08:00');
|
|
27
32
|
if (new Date(startTimestamp + 8 * 3600000).toISOString().slice(0, 19).replace('T', ' ') !== startDate) {
|
|
28
33
|
invalid('invalid calendar date');
|
|
@@ -37,8 +42,8 @@ function scheduleNodes(schedule, id, nextId) {
|
|
|
37
42
|
viewNode: { componentName: 'StartNode', id, props: { nodeName: 'StartNode', name, nodeError: '',
|
|
38
43
|
start: { type: 'trigger', triggerType: 'TimerEvent', startDate: startTimestamp, repeatRuleType: repeat,
|
|
39
44
|
startTrigger: { startTriggerTime: 'specifiedTime' },
|
|
40
|
-
overTimeType:
|
|
41
|
-
...(
|
|
45
|
+
overTimeType: expireDate !== undefined ? 'time' : 'count',
|
|
46
|
+
...(expireDate === undefined ? { overCount: maxCount ?? 1 } : {}),
|
|
42
47
|
...(expireDate !== undefined ? { overTime: expireDate } : {}), rules: true } } },
|
|
43
48
|
formEventTypes: [],
|
|
44
49
|
};
|
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
} = require('./services/process-compiler');
|
|
20
20
|
const { verifyPlatformView } = require('./services/process-view-verifier');
|
|
21
21
|
const { inspectProcessFields, assertPermissionFields } = require('./services/process-form-fields');
|
|
22
|
+
const { collectFormMemberIds, assertFormMemberFields } = require('./services/process-dynamic-actors');
|
|
22
23
|
const { completeSubtablePermissions } = require('./services/process-field-permissions');
|
|
23
24
|
const {
|
|
24
25
|
getProcessById,
|
|
@@ -365,6 +366,7 @@ async function run(args, options) {
|
|
|
365
366
|
'build_definition',
|
|
366
367
|
error,
|
|
367
368
|
{
|
|
369
|
+
...(error.code === 'PROCESS_DYNAMIC_ACTOR_INVALID' ? error.details : {}),
|
|
368
370
|
retryable: false,
|
|
369
371
|
nextAction: 'fix_definition_file',
|
|
370
372
|
diagnostics: [summarizeRemoteResult(error)],
|
|
@@ -417,13 +419,17 @@ async function run(args, options) {
|
|
|
417
419
|
}
|
|
418
420
|
stageTracker.complete('preflight_form_mode');
|
|
419
421
|
|
|
420
|
-
|
|
422
|
+
const memberFieldIds = collectFormMemberIds(compiled.viewJson);
|
|
423
|
+
if (Object.keys(compiled.processJson.flowConfig).length || memberFieldIds.length) {
|
|
421
424
|
try {
|
|
422
425
|
const fieldInspection = await inspectProcessFields(authRef, { appType, formUuid, processCode }, { binding: initialMode });
|
|
423
426
|
assertPermissionFields(compiled.processJson.flowConfig, fieldInspection.fields);
|
|
427
|
+
assertFormMemberFields(memberFieldIds, fieldInspection.fields);
|
|
424
428
|
completeSubtablePermissions(compiled, fieldInspection.fields);
|
|
425
429
|
} catch (error) {
|
|
426
|
-
failure(error.code || 'PROCESS_FORM_FIELDS_READ_FAILED', error.message, 'preflight_form_fields', error, {
|
|
430
|
+
failure(error.code || 'PROCESS_FORM_FIELDS_READ_FAILED', error.message, 'preflight_form_fields', error, {
|
|
431
|
+
...(error.code === 'PROCESS_DYNAMIC_ACTOR_INVALID' ? error.details : {}), remoteWrites: 0,
|
|
432
|
+
});
|
|
427
433
|
}
|
|
428
434
|
}
|
|
429
435
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('../core/cli-error');
|
|
4
|
+
const { t } = require('../core/i18n');
|
|
5
|
+
|
|
6
|
+
function fail(code, key, details = {}) {
|
|
7
|
+
throw new CliError(t(`process_tasks.${key}`), {
|
|
8
|
+
code,
|
|
9
|
+
details: { retryable: false, retrySafe: true, sideEffectState: 'none', ...details },
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function taskId(value) {
|
|
14
|
+
if (typeof value === 'number' && (!Number.isSafeInteger(value) || value <= 0)) {return null;}
|
|
15
|
+
const id = typeof value === 'number' ? String(value) : value;
|
|
16
|
+
return typeof id === 'string' && /^[1-9][0-9]*$/.test(id) ? id : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validateArguments(positionals, options, transfer = false) {
|
|
20
|
+
const required = ['process_inst_id', ...(transfer
|
|
21
|
+
? ['to_user_id', 'form_uuid', 'expect_form_name', 'expect_form_type'] : [])];
|
|
22
|
+
const allowed = [...required, 'json', ...(transfer ? ['task_id', 'remark'] : [])];
|
|
23
|
+
if (positionals.length !== 1 || !/^APP_[A-Za-z0-9_-]+$/.test(positionals[0])) {
|
|
24
|
+
fail('DATA_INVALID_ARGUMENTS', 'arguments_invalid');
|
|
25
|
+
}
|
|
26
|
+
if (Object.keys(options).some(key => !allowed.includes(key))
|
|
27
|
+
|| required.some(key => typeof options[key] !== 'string' || !options[key].trim())
|
|
28
|
+
|| (options.remark !== undefined && (typeof options.remark !== 'string' || !options.remark.trim()))
|
|
29
|
+
|| !/^[A-Za-z0-9_-]+$/.test(options.process_inst_id)
|
|
30
|
+
|| (transfer && !/^FORM[-_][A-Za-z0-9_-]+$/.test(options.form_uuid))
|
|
31
|
+
|| ['process_inst_id', 'form_uuid', 'to_user_id'].some(key =>
|
|
32
|
+
typeof options[key] === 'string' && /\s/.test(options[key]))
|
|
33
|
+
|| (options.task_id !== undefined && !taskId(options.task_id))) {
|
|
34
|
+
fail('DATA_INVALID_ARGUMENTS', 'arguments_invalid');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function successful(response) {
|
|
39
|
+
return response && response.success === true && !response.__needLogin && !response.__csrfExpired
|
|
40
|
+
&& [undefined, null, '', 0, '0'].includes(response.errorCode);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function queryProcessTasks(client, appType, processInstanceId) {
|
|
44
|
+
const response = await client.get(
|
|
45
|
+
`/dingtalk/web/${appType}/query/formProcInstData/getInstTaskDatas.json`,
|
|
46
|
+
{
|
|
47
|
+
_api: 'DataManage.getInstTasks', _mock: 'false', _stamp: String(Date.now()),
|
|
48
|
+
_locale_time_zone_offset: '28800000', procInstId: processInstanceId,
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
if (!successful(response)) {
|
|
52
|
+
fail('DATA_PROCESS_TASK_QUERY_FAILED', 'query_failed', {
|
|
53
|
+
causeCode: response && response.errorCode,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
if (!Array.isArray(response.content)) {
|
|
57
|
+
fail('DATA_PROCESS_TASK_RESPONSE_INVALID', 'response_invalid');
|
|
58
|
+
}
|
|
59
|
+
const ids = new Set();
|
|
60
|
+
const tasks = response.content.map(task => {
|
|
61
|
+
const id = task && taskId(task.id);
|
|
62
|
+
if (!id || task.processInstanceId !== processInstanceId || ids.has(id)
|
|
63
|
+
|| typeof task.status !== 'string' || !task.status) {
|
|
64
|
+
fail('DATA_PROCESS_TASK_RESPONSE_INVALID', 'response_invalid');
|
|
65
|
+
}
|
|
66
|
+
ids.add(id);
|
|
67
|
+
return { ...task, taskId: id };
|
|
68
|
+
});
|
|
69
|
+
return { ...response, content: tasks };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function selectTask(tasks, requestedId) {
|
|
73
|
+
const running = tasks.filter(task => task.status === 'RUNNING');
|
|
74
|
+
if (requestedId !== undefined) {
|
|
75
|
+
const selected = running.find(task => task.taskId === requestedId);
|
|
76
|
+
if (!selected) {fail('DATA_PROCESS_TASK_NOT_RUNNING', 'not_running', { taskId: requestedId });}
|
|
77
|
+
return selected;
|
|
78
|
+
}
|
|
79
|
+
if (!running.length) {fail('DATA_PROCESS_TASK_NOT_RUNNING', 'not_running');}
|
|
80
|
+
if (running.length !== 1) {
|
|
81
|
+
fail('DATA_PROCESS_TASK_AMBIGUOUS', 'ambiguous', {
|
|
82
|
+
tasks: running.map(task => ({ taskId: task.taskId, groupName: task.groupName,
|
|
83
|
+
activityCode: task.activityCode, actualActionerId: task.actualActionerId })),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return running[0];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function verifiedTask(before, selected, after, toUserId) {
|
|
90
|
+
const original = after.find(task => task.taskId === selected.taskId);
|
|
91
|
+
if (original && original.status === 'RUNNING') {
|
|
92
|
+
return original.actualActionerId === toUserId ? original : null;
|
|
93
|
+
}
|
|
94
|
+
// A recipient's existing parallel task is not evidence of this transfer.
|
|
95
|
+
// Require the old task to close and a uniquely new task in the same node/group.
|
|
96
|
+
if (!selected.activityCode || !taskId(selected.taskGroupId)) {return null;}
|
|
97
|
+
const previousIds = new Set(before.map(task => task.taskId));
|
|
98
|
+
const candidates = after.filter(task => task.status === 'RUNNING'
|
|
99
|
+
&& !previousIds.has(task.taskId)
|
|
100
|
+
&& task.activityCode === selected.activityCode
|
|
101
|
+
&& String(task.taskGroupId) === String(selected.taskGroupId));
|
|
102
|
+
return candidates.length === 1 && candidates[0].actualActionerId === toUserId ? candidates[0] : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function transferTask(client, input) {
|
|
106
|
+
const { appType, formUuid, processInstanceId, toUserId, remark = '转交' } = input;
|
|
107
|
+
const before = (await queryProcessTasks(client, appType, processInstanceId)).content;
|
|
108
|
+
const selected = selectTask(before, input.taskId);
|
|
109
|
+
if (typeof selected.actualActionerId !== 'string' || !selected.actualActionerId.trim()) {
|
|
110
|
+
fail('DATA_PROCESS_TASK_RESPONSE_INVALID', 'response_invalid');
|
|
111
|
+
}
|
|
112
|
+
const target = { appType, formUuid, processInstanceId, taskId: selected.taskId, toUserId };
|
|
113
|
+
const nextStep = `openyida data query process-tasks ${appType} --process-inst-id ${processInstanceId} --json`;
|
|
114
|
+
const result = {
|
|
115
|
+
...target, target, previousActionerId: selected.actualActionerId,
|
|
116
|
+
mutationPerformed: false, mutationAccepted: false, readbackVerified: false,
|
|
117
|
+
};
|
|
118
|
+
if (selected.actualActionerId === toUserId) {
|
|
119
|
+
return { ...result, success: true, status: 'ALREADY_ASSIGNED', alreadyAssigned: true,
|
|
120
|
+
transferred: false, readbackVerified: true, sideEffectState: 'none' };
|
|
121
|
+
}
|
|
122
|
+
let response;
|
|
123
|
+
try {
|
|
124
|
+
const query = new URLSearchParams({
|
|
125
|
+
_api: 'DataManage.redirectTask', _mock: 'false', _stamp: String(Date.now()),
|
|
126
|
+
});
|
|
127
|
+
response = await client.postFormOnce(
|
|
128
|
+
`/dingtalk/web/${appType}/query/instance/redirectTask.json?${query}`,
|
|
129
|
+
{ _locale_time_zone_offset: '28800000', taskId: selected.taskId,
|
|
130
|
+
procInstId: processInstanceId, nowActionerId: toUserId, remark, byManager: 'true' },
|
|
131
|
+
{ referer: `${client.getAuthRef().baseUrl}/${appType}/admin/${formUuid}/dataManage` }
|
|
132
|
+
);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
fail('DATA_PROCESS_TRANSFER_RESULT_UNKNOWN', 'result_unknown', {
|
|
135
|
+
...result, mutationPerformed: true, status: 'RESULT_UNKNOWN', mutationAccepted: null,
|
|
136
|
+
retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true, nextStep,
|
|
137
|
+
causeCode: error.code || 'TRANSFER_TRANSPORT_FAILED',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (!successful(response)) {
|
|
141
|
+
const rejected = response && response.success === false && !response.__needLogin
|
|
142
|
+
&& !response.__csrfExpired && !response.__nonJsonResponse;
|
|
143
|
+
fail(rejected ? 'DATA_PROCESS_TRANSFER_REJECTED' : 'DATA_PROCESS_TRANSFER_RESULT_UNKNOWN',
|
|
144
|
+
rejected ? 'rejected' : 'result_unknown', {
|
|
145
|
+
...result, mutationPerformed: true, mutationAccepted: rejected ? false : null,
|
|
146
|
+
status: rejected ? 'REJECTED' : 'RESULT_UNKNOWN', retrySafe: false,
|
|
147
|
+
sideEffectState: rejected ? 'none' : 'unknown', readbackAllowed: true, nextStep,
|
|
148
|
+
causeCode: response && response.errorCode,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
let verified;
|
|
152
|
+
try {
|
|
153
|
+
const after = (await queryProcessTasks(client, appType, processInstanceId)).content;
|
|
154
|
+
verified = verifiedTask(before, selected, after, toUserId);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
fail('DATA_PROCESS_TRANSFER_UNVERIFIED', 'unverified', {
|
|
157
|
+
...result, mutationPerformed: true, mutationAccepted: true, status: 'UNVERIFIED',
|
|
158
|
+
retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true, nextStep,
|
|
159
|
+
causeCode: error.code || 'TRANSFER_READBACK_FAILED',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (!verified) {
|
|
163
|
+
fail('DATA_PROCESS_TRANSFER_UNVERIFIED', 'unverified', {
|
|
164
|
+
...result, mutationPerformed: true, mutationAccepted: true, status: 'UNVERIFIED',
|
|
165
|
+
retrySafe: false, sideEffectState: 'unknown', readbackAllowed: true, nextStep,
|
|
166
|
+
causeCode: 'TRANSFER_READBACK_MISMATCH',
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return { ...result, success: true, transferred: true, status: 'TRANSFERRED',
|
|
170
|
+
mutationPerformed: true, mutationAccepted: true, readbackVerified: true,
|
|
171
|
+
currentTaskId: verified.taskId, actualActionerId: verified.actualActionerId,
|
|
172
|
+
retryable: false, sideEffectState: 'committed' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { validateArguments, queryProcessTasks, transferTask };
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { verifyDynamicActors } = require('./process-dynamic-actors');
|
|
4
|
+
|
|
3
5
|
// The native MultiApproval setter stores flat UI rules, while runtime rules
|
|
4
6
|
// are grouped by status. Keep the two representations separate.
|
|
5
7
|
function buildMultiApproverRules(config, legacyRules) {
|
|
@@ -26,6 +28,7 @@ function verifyActorRoles(expectedNodes, actualNodes, errors) {
|
|
|
26
28
|
}
|
|
27
29
|
(expectedNodes || []).forEach((node, index) => {
|
|
28
30
|
const actual = (actualNodes || [])[index];
|
|
31
|
+
verifyDynamicActors(node, actual, errors);
|
|
29
32
|
const expectedConditional = node.props?.approverRules;
|
|
30
33
|
if (expectedConditional?.mode === 'ApprovalNode_rules_multiple'
|
|
31
34
|
&& JSON.stringify(expectedConditional) !== JSON.stringify(actual?.props?.approverRules)) {
|
|
@@ -11,6 +11,7 @@ const { t } = require('../../core/i18n');
|
|
|
11
11
|
const { buildNodeActions } = require('./process-actions');
|
|
12
12
|
const { buildFlowConfig, normalizeBehaviorList } = require('./process-field-permissions');
|
|
13
13
|
const { buildMultiApproverRules } = require('./process-actor-config');
|
|
14
|
+
const { buildDynamicActor, validateNativeActor, assertNativeActorTypes } = require('./process-dynamic-actors');
|
|
14
15
|
|
|
15
16
|
const KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
16
17
|
|
|
@@ -369,6 +370,8 @@ function getFriendlyApproverKind(approver) {
|
|
|
369
370
|
}
|
|
370
371
|
const normalized = rawKind.toLowerCase().replace(/[-_\s]/g, '');
|
|
371
372
|
const kindMap = {
|
|
373
|
+
formfield: 'formField',
|
|
374
|
+
selfselect: 'selfSelect',
|
|
372
375
|
originator: 'originator',
|
|
373
376
|
self: 'originator',
|
|
374
377
|
starter: 'originator',
|
|
@@ -638,6 +641,11 @@ function buildFriendlyApproverConfig(node, approver) {
|
|
|
638
641
|
return null;
|
|
639
642
|
}
|
|
640
643
|
const raw = isPlainObject(approver) ? approver : {};
|
|
644
|
+
if (kind === 'formField' || kind === 'selfSelect') {
|
|
645
|
+
return buildDynamicActor(node, raw, kind, normalizeNodeType(node), {
|
|
646
|
+
createBaseApproverRules, normalizeApprovalMode, normalizeUserApprovers, normalizeRoleApprovers,
|
|
647
|
+
});
|
|
648
|
+
}
|
|
641
649
|
if (kind === 'originator') {
|
|
642
650
|
return buildOriginatorApproverConfig(node, raw);
|
|
643
651
|
}
|
|
@@ -1066,8 +1074,12 @@ function buildApproverConfig(node) {
|
|
|
1066
1074
|
return friendlyApprover;
|
|
1067
1075
|
}
|
|
1068
1076
|
|
|
1077
|
+
if (typeof approverInput === 'string') {
|
|
1078
|
+
validateNativeActor({ kind: approverInput }, nodeType);
|
|
1079
|
+
}
|
|
1069
1080
|
const rawApprover = node.approverConfig || (approverInput && typeof approverInput === 'object' ? approverInput : null);
|
|
1070
1081
|
if (rawApprover) {
|
|
1082
|
+
validateNativeActor(rawApprover, nodeType, node._nodeId);
|
|
1071
1083
|
return {
|
|
1072
1084
|
approvalType: rawApprover.approvalType || rawApprover.type || 'ext_target_approval_originator',
|
|
1073
1085
|
approvals: rawApprover.approvals || [['originator']],
|
|
@@ -1086,6 +1098,7 @@ function buildApproverConfig(node) {
|
|
|
1086
1098
|
}
|
|
1087
1099
|
|
|
1088
1100
|
if (node.approvalType || node.approvals || node.approverRules || node.processProps) {
|
|
1101
|
+
validateNativeActor({ ...node, type: node.approvalType }, nodeType, node._nodeId);
|
|
1089
1102
|
return {
|
|
1090
1103
|
approvalType: node.approvalType || 'ext_target_approval_originator',
|
|
1091
1104
|
approvals: node.approvals || [['originator']],
|
|
@@ -2053,6 +2066,8 @@ function buildProcessAndViewJson(definition, processCode, formUuid, baseUrl, app
|
|
|
2053
2066
|
},
|
|
2054
2067
|
};
|
|
2055
2068
|
|
|
2069
|
+
assertNativeActorTypes(processJson.nodes);
|
|
2070
|
+
assertNativeActorTypes(viewJson.schema.children);
|
|
2056
2071
|
processJson.flowConfig = buildFlowConfig(viewJson);
|
|
2057
2072
|
const result = {
|
|
2058
2073
|
processJson,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { CliError } = require('../../core/cli-error');
|
|
4
|
+
const { t } = require('../../core/i18n');
|
|
5
|
+
|
|
6
|
+
const FORM_MEMBER = 'ext_target_approval_variable';
|
|
7
|
+
const SELF_SELECT = 'ext_target_optional_approval';
|
|
8
|
+
// Native designer ApproverType, plus the existing conditional-actors adapter.
|
|
9
|
+
const NATIVE_TYPES = new Set(['ext_target_noset', 'ext_target_approval', 'ext_target_approval_role',
|
|
10
|
+
'ext_target_approval_originator', 'ext_target_approval_liaison', 'ext_target_dept_leader',
|
|
11
|
+
'ext_target_direct_leader', 'ext_target_multi_management', SELF_SELECT, FORM_MEMBER,
|
|
12
|
+
'ext_target_approval_invoke', 'ext_target_approval_connector', 'ext_target_approval_matrix',
|
|
13
|
+
'ext_target_approval_condition']);
|
|
14
|
+
|
|
15
|
+
function invalid(reason, details = {}) {
|
|
16
|
+
throw new CliError(t('process_dynamic_actors.invalid'), {
|
|
17
|
+
code: 'PROCESS_DYNAMIC_ACTOR_INVALID', details: { reason, ...details, remoteWrites: 0 },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildDynamicActor(node, input, kind, nodeType, helpers) {
|
|
22
|
+
const allowed = ['kind', 'type', 'approverType', 'approvalMode', 'multiApproverType', 'description'];
|
|
23
|
+
allowed.push(...(kind === 'formField' ? ['fieldId', 'fieldIds'] : ['range']));
|
|
24
|
+
if (Object.keys(input).some(key => !allowed.includes(key))) { invalid('unsupported property'); }
|
|
25
|
+
if (input.kind && input.type && input.kind !== input.type) { invalid('conflicting kind/type'); }
|
|
26
|
+
const mode = helpers.normalizeApprovalMode(input.approvalMode || input.multiApproverType || 'all');
|
|
27
|
+
if (!['all', 'or', 'oneByOne'].includes(mode)) { invalid('approvalMode'); }
|
|
28
|
+
const approvalType = kind === 'formField' ? FORM_MEMBER : SELF_SELECT;
|
|
29
|
+
const description = input.description || kind;
|
|
30
|
+
const approverRules = helpers.createBaseApproverRules(approvalType, description, {});
|
|
31
|
+
approverRules.multiApproverType = mode;
|
|
32
|
+
approverRules['approvalType_' + approvalType] = mode;
|
|
33
|
+
if (kind === 'formField') {
|
|
34
|
+
if (input.fieldId !== undefined && input.fieldIds !== undefined) { invalid('use fieldId or fieldIds'); }
|
|
35
|
+
const ids = input.fieldId === undefined ? input.fieldIds : [input.fieldId];
|
|
36
|
+
if (!Array.isArray(ids) || !ids.length || ids.some(id => typeof id !== 'string' || !id.trim() || id !== id.trim())
|
|
37
|
+
|| new Set(ids).size !== ids.length) { invalid('fieldId/fieldIds'); }
|
|
38
|
+
approverRules.formMember = ids.map(value => ({ value, label: value }));
|
|
39
|
+
return { approvalType, approvals: [ids], approverRules, description,
|
|
40
|
+
processProps: { multiApprove: mode }, carbonReceiver: { type: 'VARIABLE', value: [ids] } };
|
|
41
|
+
}
|
|
42
|
+
// Carbon self-selection needs a separate non-free entitlement and feature flag.
|
|
43
|
+
// MultiApproval deliberately excludes this source in the native designer.
|
|
44
|
+
if (!['approval', 'operator'].includes(nodeType)) { invalid('selfSelect requires approval/operator', { nodeType }); }
|
|
45
|
+
const range = input.range === undefined ? { type: 'ALL' } : input.range;
|
|
46
|
+
if (!range || typeof range !== 'object' || Array.isArray(range)
|
|
47
|
+
|| !['ALL', 'USER', 'ROLE'].includes(range.type)) { invalid('range.type'); }
|
|
48
|
+
const rangeKey = range.type === 'USER' ? 'users' : range.type === 'ROLE' ? 'roles' : null;
|
|
49
|
+
if (Object.keys(range).some(key => key !== 'type' && key !== rangeKey)) { invalid('range properties'); }
|
|
50
|
+
const runtimeRange = { type: range.type };
|
|
51
|
+
const viewRange = { type: range.type };
|
|
52
|
+
if (rangeKey) {
|
|
53
|
+
if (!Array.isArray(range[rangeKey]) || !range[rangeKey].length) { invalid('range must be nonempty'); }
|
|
54
|
+
let items;
|
|
55
|
+
try {
|
|
56
|
+
items = rangeKey === 'users' ? helpers.normalizeUserApprovers({ users: range.users })
|
|
57
|
+
: helpers.normalizeRoleApprovers({ roles: range.roles });
|
|
58
|
+
} catch { invalid('range member/role requires a real ID'); }
|
|
59
|
+
if (items.some(item => !item.id.trim() || !['YIDA', 'DINGTALK'].includes(item.roleType))) { invalid('range ID/roleType'); }
|
|
60
|
+
if (new Set(items.map(item => String(item.id))).size !== items.length) { invalid('duplicate range ID'); }
|
|
61
|
+
const key = rangeKey === 'users' ? 'approvalRange' : 'roleRange';
|
|
62
|
+
viewRange[key] = items;
|
|
63
|
+
runtimeRange[key] = rangeKey === 'users' ? items.map(item => item.id) : items;
|
|
64
|
+
}
|
|
65
|
+
approverRules.approvalOptionalRange = viewRange;
|
|
66
|
+
return { approvalType, approvals: ['__optionalApproval_' + node._nodeId], approverRules, description,
|
|
67
|
+
processProps: { multiApprove: mode, optionalApprovalRange: runtimeRange } };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateNativeActor(raw, nodeType, nodeId) {
|
|
71
|
+
if (Array.isArray(raw)) { invalid('actor must be an object'); }
|
|
72
|
+
if (raw.kind || raw.approverType) { invalid('unknown friendly actor kind', { kind: raw.kind || raw.approverType }); }
|
|
73
|
+
const type = raw.approvalType || raw.type || 'ext_target_approval_originator';
|
|
74
|
+
if (!NATIVE_TYPES.has(type)) { invalid('unknown native approvalType', { approvalType: type }); }
|
|
75
|
+
if (raw.approverRules?.type && raw.approverRules.type !== type) { invalid('runtime/view approvalType mismatch'); }
|
|
76
|
+
// Dynamic native payloads need both halves. Never default their runtime to originator.
|
|
77
|
+
if (type === FORM_MEMBER || type === SELF_SELECT) {
|
|
78
|
+
if (!raw.approvals || !raw.approverRules) { invalid('dynamic native actors require approvals and approverRules'); }
|
|
79
|
+
if (type === SELF_SELECT && !['approval', 'operator'].includes(nodeType)) {
|
|
80
|
+
invalid('selfSelect requires approval/operator', { nodeType });
|
|
81
|
+
}
|
|
82
|
+
if (type === FORM_MEMBER) {
|
|
83
|
+
const members = raw.approverRules.formMember;
|
|
84
|
+
const ids = Array.isArray(members) ? members.map(item => item?.value) : [];
|
|
85
|
+
if (!ids?.length || ids.some(id => typeof id !== 'string' || !id.trim())
|
|
86
|
+
|| JSON.stringify(raw.approvals) !== JSON.stringify([ids])) { invalid('formMember/runtime mismatch'); }
|
|
87
|
+
if (nodeType === 'carbon' && (raw.carbonReceiver?.type !== 'VARIABLE'
|
|
88
|
+
|| JSON.stringify(raw.carbonReceiver.value) !== JSON.stringify([ids]))) { invalid('carbonReceiver mismatch'); }
|
|
89
|
+
} else {
|
|
90
|
+
const view = raw.approverRules.approvalOptionalRange;
|
|
91
|
+
const runtime = (raw.processProps || raw.props || {}).optionalApprovalRange;
|
|
92
|
+
if (!view || !runtime || !['ALL', 'USER', 'ROLE'].includes(view.type) || view.type !== runtime.type
|
|
93
|
+
|| JSON.stringify(raw.approvals) !== JSON.stringify(['__optionalApproval_' + nodeId])) {
|
|
94
|
+
invalid('selfSelect runtime/view range or generated node ID mismatch');
|
|
95
|
+
}
|
|
96
|
+
const key = view.type === 'USER' ? 'approvalRange' : view.type === 'ROLE' ? 'roleRange' : null;
|
|
97
|
+
if (key) {
|
|
98
|
+
const members = view[key];
|
|
99
|
+
if (!Array.isArray(members) || !members.length || members.some(item => !item?.id)) { invalid('selfSelect range IDs'); }
|
|
100
|
+
const ids = members.map(item => item.id);
|
|
101
|
+
const runtimeIds = view.type === 'USER' ? runtime[key] : runtime[key]?.map(item => item.id);
|
|
102
|
+
if (JSON.stringify(ids) !== JSON.stringify(runtimeIds)) { invalid('selfSelect runtime/view range IDs mismatch'); }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check both emitted contracts, including legacy multiRules and nested branches.
|
|
109
|
+
function assertNativeActorTypes(value) {
|
|
110
|
+
if (!value || typeof value !== 'object') { return; }
|
|
111
|
+
if (Array.isArray(value)) { value.forEach(assertNativeActorTypes); return; }
|
|
112
|
+
for (const [key, item] of Object.entries(value)) {
|
|
113
|
+
if (['type', 'approvalType'].includes(key) && typeof item === 'string' && item.startsWith('ext_target_')
|
|
114
|
+
&& item !== 'ext_target_activity' && !NATIVE_TYPES.has(item)) {
|
|
115
|
+
invalid('unknown native approvalType', { approvalType: item });
|
|
116
|
+
}
|
|
117
|
+
if (item && typeof item === 'object') { assertNativeActorTypes(item); }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function actorRules(node) {
|
|
122
|
+
const props = node?.props || {};
|
|
123
|
+
return props.multiApproverRules ? props.multiApproverRules.multiRules || [] : [props.approverRules].filter(Boolean);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function collectFormMemberIds(view) {
|
|
127
|
+
const ids = new Set();
|
|
128
|
+
function visit(nodes) {
|
|
129
|
+
for (const node of nodes || []) {
|
|
130
|
+
for (const rule of actorRules(node)) {
|
|
131
|
+
if (rule.type === FORM_MEMBER) { (rule.formMember || []).forEach(item => ids.add(item.value)); }
|
|
132
|
+
}
|
|
133
|
+
visit(node.children);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
visit(view?.schema?.children);
|
|
137
|
+
return [...ids];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function assertFormMemberFields(ids, fields) {
|
|
141
|
+
for (const fieldId of ids) {
|
|
142
|
+
const field = fields.find(item => item.fieldId === fieldId);
|
|
143
|
+
if (!field || field.componentName !== 'EmployeeField' || field.parentFieldId) {
|
|
144
|
+
invalid('formField requires an existing top-level EmployeeField', { fieldId, componentName: field?.componentName });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function verifyDynamicActors(expected, actual, errors) {
|
|
150
|
+
function signature(node) {
|
|
151
|
+
return actorRules(node).filter(rule => [FORM_MEMBER, SELF_SELECT].includes(rule.type)).map(rule => {
|
|
152
|
+
if (rule.type === FORM_MEMBER) { return { type: rule.type, mode: rule['approvalType_' + rule.type], ids: (rule.formMember || []).map(item => item.value) }; }
|
|
153
|
+
const range = rule.approvalOptionalRange || {};
|
|
154
|
+
return { type: rule.type, mode: rule['approvalType_' + rule.type], rangeType: range.type,
|
|
155
|
+
users: range.type === 'USER' ? (range.approvalRange || []).map(item => String(item.id)) : [],
|
|
156
|
+
roles: range.type === 'ROLE' ? (range.roleRange || []).map(item => ({ id: String(item.id), roleType: item.roleType || 'YIDA' })) : [] };
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const wanted = signature(expected);
|
|
160
|
+
if (wanted.length && JSON.stringify(wanted) !== JSON.stringify(signature(actual))) {
|
|
161
|
+
errors.push({ code: 'PROCESS_PLATFORM_VIEW_DYNAMIC_ACTORS_MISMATCH', nodeId: expected.id });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { assertNativeActorTypes, buildDynamicActor, validateNativeActor, collectFormMemberIds, assertFormMemberFields, verifyDynamicActors };
|
|
@@ -12,7 +12,7 @@ function CanvasNav({ items = [], activeKey, onSelect, title, logo, actions, chil
|
|
|
12
12
|
.oy-canvas-nav.oy-nav-mixed .oy-nav-sidebar { padding: var(--pod-nav-l-aside-padding, 8px); }
|
|
13
13
|
.oy-nav-mixed .oy-nav-content { flex: 1; }
|
|
14
14
|
.oy-nav-mixed .oy-nav-menu { display: flex; flex-direction: column; gap: var(--pod-nav-menu-gap, 8px); }
|
|
15
|
-
@
|
|
15
|
+
@container (max-width: 767px) {
|
|
16
16
|
.oy-nav-mixed .oy-nav-groups { flex-basis: 100%; }
|
|
17
17
|
.oy-nav-mixed .oy-nav-body { display: block; }
|
|
18
18
|
.oy-nav-mixed .oy-nav-menu { flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--pod-nav-sub-divider-color, var(--color-line1-1, #ddd)); }
|
|
@@ -4,7 +4,7 @@ import React from 'react';
|
|
|
4
4
|
// 这是可选实现示例;导航外观按 design.md 设计,已有 UI 无需替换成示例默认样式。
|
|
5
5
|
// 选中菜单标识当前页面,main 用 aria-label 命名,children 直接承载业务内容。
|
|
6
6
|
const CANVAS_NAV_CSS = `
|
|
7
|
-
.oy-canvas-nav { display: flow-root; min-width: 0; min-height: 100vh; background: var(--pod-page-bg-color, var(--color-white, #fff)); }
|
|
7
|
+
.oy-canvas-nav { container-type: inline-size; display: flow-root; min-width: 0; min-height: 100vh; background: var(--pod-page-bg-color, var(--color-white, #fff)); }
|
|
8
8
|
.oy-canvas-nav .oy-nav-surface {
|
|
9
9
|
background: var(--pod-shell-theme-bg-color, var(--pod-nav-popup-bg-color, var(--color-white, #fff)));
|
|
10
10
|
color: var(--pod-nav-item-text-color, var(--color-text1-4, #1f2329));
|
|
@@ -6,7 +6,7 @@ function CanvasNav({ items = [], activeKey, onSelect, title, logo, actions, chil
|
|
|
6
6
|
.oy-nav-side .oy-nav-content { flex: 1; }
|
|
7
7
|
.oy-nav-side .oy-nav-brand { min-height: var(--pod-nav-platform-header-height, 48px); }
|
|
8
8
|
.oy-nav-side .oy-nav-menu { display: flex; flex-direction: column; gap: var(--pod-nav-menu-gap, 8px); }
|
|
9
|
-
@
|
|
9
|
+
@container (max-width: 767px) { .oy-nav-side { display: block; } }
|
|
10
10
|
`}</style>
|
|
11
11
|
<CanvasSidebar title={title} logo={logo} actions={actions} activeKey={activeKey} defaultWidth={sidebarWidth} defaultCollapsed={defaultCollapsed}>
|
|
12
12
|
<nav aria-label="应用导航" className="oy-nav-menu">
|