@chengzhao-dev/dsh-cpp-workflow 0.6.6 → 0.6.8
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.en.md +4 -2
- package/README.md +5 -3
- package/lib/client.js +358 -246
- package/lib/comment-policy.js +69 -52
- package/lib/index.js +2 -0
- package/package.json +2 -2
- package/skills/building-cpp/SKILL.md +1 -1
- package/skills/configuring-cmake/SKILL.md +1 -1
- package/skills/developing-cpp/SKILL.md +2 -2
- package/skills/developing-cpp/examples/structure.md +1 -1
- package/skills/developing-cpp/references/comments.md +17 -5
- package/skills/developing-cpp/references/google-cpp-style.md +1 -1
- package/skills/developing-cpp/references/language.md +2 -1
- package/skills/developing-cpp/scripts/checking-comments.mjs +122 -16
package/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 由 scripts/building-client.mjs 生成,请改 src/comment-policy.ts 与 src/client
|
|
1
|
+
// 由 scripts/building-client.mjs 生成,请改 src/comment-policy.ts 与 src/client/*.js。
|
|
2
2
|
window.__ModuleLoader__.load({
|
|
3
3
|
id: "@chengzhao-dev/dsh-cpp-workflow",
|
|
4
4
|
factory: (require) => {
|
|
@@ -8,8 +8,8 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
/**
|
|
9
9
|
* 注释标签策略:设置页的预览、技能快照与检查器参数共用这一份。
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* `skills/developing-cpp/references/comments.md`。设置页保存的值由宿主
|
|
11
|
+
* 配置只描述「写注释时默认带哪些标签、声明用哪种注释符号」,不描述写法本身;
|
|
12
|
+
* 写法在 `skills/developing-cpp/references/comments.md`。设置页保存的值由宿主
|
|
13
13
|
* 写回插件 Config,服务端 `apply(ctx, config)` 用它生成本页快照与检查器参数。
|
|
14
14
|
*
|
|
15
15
|
* 这份文件被 `tsc` 编进 `lib/comment-policy.js`,打包客户端半侧时再内联进
|
|
@@ -17,29 +17,16 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
*/
|
|
18
18
|
/** 文件头标签。 */
|
|
19
19
|
const HEADER_TAGS = ['brief', 'author', 'date', 'copyright', 'version'];
|
|
20
|
-
/**
|
|
20
|
+
/** 函数声明标签:开关决定是否要求写出。 */
|
|
21
21
|
const CONTRACT_TAGS = ['param', 'returns', 'throws'];
|
|
22
|
-
/**
|
|
22
|
+
/** 函数声明里默认关闭的补充标签。 */
|
|
23
|
+
const FUNCTION_EXTRA_TAGS = ['pre', 'post', 'retval'];
|
|
24
|
+
/** 类型标签:默认关闭,函数模板共用同一组键。 */
|
|
25
|
+
const TYPE_TAGS = ['tparam', 'note', 'see'];
|
|
26
|
+
/** 全部可选标签,快照与测试按它排序。 */
|
|
23
27
|
const EXTRA_TAGS = ['note', 'see', 'pre', 'post', 'tparam', 'retval'];
|
|
24
28
|
/** 只有用户主动填了非空值才放行的隐私标签。 */
|
|
25
29
|
const PRIVACY_TAGS = ['author', 'date', 'copyright', 'version'];
|
|
26
|
-
/** 标签说明:设置页逐行显示,用作行的辅助文字。 */
|
|
27
|
-
const TAG_TEXT = {
|
|
28
|
-
brief: '一句话说明这个文件定义或实现什么',
|
|
29
|
-
author: '只在用户填了非空值时写出',
|
|
30
|
-
date: '只在用户填了非空值时写出',
|
|
31
|
-
copyright: '只在用户填了非空值时写出',
|
|
32
|
-
version: '只在用户填了非空值时写出',
|
|
33
|
-
param: '函数参数的含义、范围或所有权',
|
|
34
|
-
returns: '返回值的条件;返回类型自明时可以省略',
|
|
35
|
-
throws: '抛出的异常类型与条件;不抛异常的函数不写',
|
|
36
|
-
note: '调用方需要注意的补充说明',
|
|
37
|
-
see: '相关函数、类型或文档',
|
|
38
|
-
pre: '调用前必须成立的条件',
|
|
39
|
-
post: '调用后保证成立的条件',
|
|
40
|
-
tparam: '模板参数',
|
|
41
|
-
retval: '特定返回值的含义',
|
|
42
|
-
};
|
|
43
30
|
/** 开关型标签在设置页的标题。 */
|
|
44
31
|
const TAG_LABEL = {
|
|
45
32
|
file: '@file',
|
|
@@ -62,8 +49,9 @@ const TAG_LABEL = {
|
|
|
62
49
|
const SAMPLE_BRIEF = '<一句话说明本文件>';
|
|
63
50
|
/** 预览里的示例文件名:只是占位符,不对应任何工程的真实文件。 */
|
|
64
51
|
const SAMPLE_FILE = '<文件名>';
|
|
65
|
-
/**
|
|
52
|
+
/** 出厂配置:声明用 `///`,文件头只留 @file 与 @brief,契约标签全开,其余关闭。 */
|
|
66
53
|
const DEFAULT_CONFIG = {
|
|
54
|
+
commentStyle: 'slash',
|
|
67
55
|
brief: { enabled: true, value: '' },
|
|
68
56
|
author: { enabled: false, value: '' },
|
|
69
57
|
date: { enabled: false, value: '' },
|
|
@@ -99,12 +87,16 @@ function flag(value, fallback) {
|
|
|
99
87
|
const plain = unwrap(value);
|
|
100
88
|
return typeof plain === 'boolean' ? plain : fallback;
|
|
101
89
|
}
|
|
90
|
+
/** 声明符号只认两档;未知值回到 `///`。 */
|
|
91
|
+
function commentStyleOf(raw) {
|
|
92
|
+
return unwrap(raw) === 'block' ? 'block' : 'slash';
|
|
93
|
+
}
|
|
102
94
|
/**
|
|
103
95
|
* 把宿主给的值补齐成完整配置;缺字段、旧版本或手工改坏的配置都回到出厂值。
|
|
104
96
|
*/
|
|
105
97
|
function normalizeConfig(raw) {
|
|
106
98
|
const source = raw && typeof raw === 'object' ? raw : {};
|
|
107
|
-
const config = {};
|
|
99
|
+
const config = { commentStyle: commentStyleOf(source.commentStyle) };
|
|
108
100
|
for (const tag of HEADER_TAGS) {
|
|
109
101
|
const entry = source[tag] && typeof source[tag] === 'object' ? source[tag] : {};
|
|
110
102
|
config[tag] = {
|
|
@@ -117,59 +109,85 @@ function normalizeConfig(raw) {
|
|
|
117
109
|
}
|
|
118
110
|
return config;
|
|
119
111
|
}
|
|
112
|
+
/** 一行声明注释:`/// 内容` 或块注释里的 ` * 内容`。 */
|
|
113
|
+
function declarationLine(style, content) {
|
|
114
|
+
return style === 'block' ? ` * ${content}` : `/// ${content}`;
|
|
115
|
+
}
|
|
116
|
+
/** 文件头后面的说明行:块注释里空行写成 ` *`。 */
|
|
117
|
+
function separatorLine(style) {
|
|
118
|
+
return style === 'block' ? ' *' : '///';
|
|
119
|
+
}
|
|
120
120
|
/**
|
|
121
|
-
* 按配置渲染文件头;`@file`
|
|
121
|
+
* 按配置渲染文件头;`@file` 始终是传入的文件名,块注释按 Doxygen 结构分行。
|
|
122
122
|
*/
|
|
123
123
|
function renderFileHeader(raw, fileName) {
|
|
124
124
|
const config = normalizeConfig(raw);
|
|
125
125
|
const name = text(fileName) || SAMPLE_FILE;
|
|
126
|
-
const lines = [
|
|
126
|
+
const lines = [`@file ${name}`];
|
|
127
127
|
if (config.brief.enabled) {
|
|
128
|
-
lines.push(config.brief.value ?
|
|
128
|
+
lines.push(config.brief.value ? `@brief ${config.brief.value}` : '@brief');
|
|
129
129
|
}
|
|
130
130
|
for (const tag of ['author', 'date', 'copyright', 'version']) {
|
|
131
131
|
if (config[tag].enabled && config[tag].value)
|
|
132
|
-
lines.push(
|
|
132
|
+
lines.push(`@${tag} ${config[tag].value}`);
|
|
133
|
+
}
|
|
134
|
+
if (config.commentStyle === 'block') {
|
|
135
|
+
return ['/**', ...lines.map((line) => ` * ${line}`), ' */'];
|
|
133
136
|
}
|
|
134
|
-
return lines;
|
|
137
|
+
return lines.map((line) => `/// ${line}`);
|
|
135
138
|
}
|
|
136
139
|
/**
|
|
137
140
|
* 设置页的预览:文件头 + 一段函数注释,只显示当前配置会写出的标签。
|
|
141
|
+
* 返回值里的 `header` 已按声明符号排好,`contract` 是每行标签的正文。
|
|
138
142
|
*/
|
|
139
143
|
function previewParts(raw, sampleName) {
|
|
140
144
|
const config = normalizeConfig(raw);
|
|
141
145
|
const header = renderFileHeader(config, sampleName || SAMPLE_FILE);
|
|
142
146
|
// 预览是示例:@brief 开着但用户没填句子时,用示例文件自己的摘要顶替。
|
|
143
|
-
const briefIndex = header.
|
|
144
|
-
if (briefIndex !== -1)
|
|
145
|
-
header[briefIndex] =
|
|
147
|
+
const briefIndex = header.findIndex((line) => /@brief$/.test(line.trim()));
|
|
148
|
+
if (briefIndex !== -1) {
|
|
149
|
+
header[briefIndex] = header[briefIndex].replace(/@brief$/, `@brief ${SAMPLE_BRIEF}`);
|
|
150
|
+
}
|
|
146
151
|
const contract = [];
|
|
147
152
|
if (config.param)
|
|
148
|
-
contract.push('
|
|
153
|
+
contract.push('@param row 目标行号。');
|
|
149
154
|
if (config.returns)
|
|
150
|
-
contract.push('
|
|
155
|
+
contract.push('@return 放置成功时为 true。');
|
|
151
156
|
if (config.throws)
|
|
152
|
-
contract.push('
|
|
153
|
-
|
|
154
|
-
return { header, contract, declaration };
|
|
157
|
+
contract.push('@throws std::out_of_range 行号越界时抛出。');
|
|
158
|
+
return { header, contract, declaration: '[[nodiscard]] bool Place(int row, int column);' };
|
|
155
159
|
}
|
|
156
160
|
/**
|
|
157
161
|
* 设置页预览的纯文本形式;客户端半侧用同一份实现渲染 `<pre>`。
|
|
162
|
+
* 块注释的每个内容行前加 ` * `,首尾各占一行。
|
|
158
163
|
*/
|
|
159
164
|
function previewText(raw, sampleName) {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
165
|
+
const config = normalizeConfig(raw);
|
|
166
|
+
const { header, contract, declaration } = previewParts(config, sampleName);
|
|
167
|
+
const style = config.commentStyle;
|
|
168
|
+
const summary = '放置成功时返回 true;行号越界或目标格子已占时返回 false。';
|
|
169
|
+
if (style === 'block') {
|
|
170
|
+
const body = [declarationLine(style, summary)];
|
|
171
|
+
if (contract.length > 0) {
|
|
172
|
+
body.push(separatorLine(style), ...contract.map((line) => declarationLine(style, line)));
|
|
173
|
+
}
|
|
174
|
+
return [...header, '/**', ...body, ' */', declaration].join('\n');
|
|
175
|
+
}
|
|
176
|
+
const lines = [...header, separatorLine(style), declarationLine(style, summary)];
|
|
162
177
|
if (contract.length > 0)
|
|
163
|
-
lines.push(
|
|
178
|
+
lines.push(separatorLine(style), ...contract.map((line) => declarationLine(style, line)));
|
|
164
179
|
lines.push(declaration);
|
|
165
180
|
return lines.join('\n');
|
|
166
181
|
}
|
|
167
182
|
/**
|
|
168
|
-
* 检查器参数:隐私标签要显式放行,@brief
|
|
183
|
+
* 检查器参数:隐私标签要显式放行,@brief 被关掉时跳过摘要检查,
|
|
184
|
+
* 声明符号切到块注释时让检查器按块注释收口。
|
|
169
185
|
*/
|
|
170
186
|
function checkerArgs(raw) {
|
|
171
187
|
const config = normalizeConfig(raw);
|
|
172
188
|
const args = [];
|
|
189
|
+
if (config.commentStyle === 'block')
|
|
190
|
+
args.push('--comment-style', 'block');
|
|
173
191
|
if (!config.brief.enabled)
|
|
174
192
|
args.push('--no-brief');
|
|
175
193
|
for (const tag of PRIVACY_TAGS) {
|
|
@@ -180,10 +198,11 @@ function checkerArgs(raw) {
|
|
|
180
198
|
}
|
|
181
199
|
/**
|
|
182
200
|
* 技能快照:`get()` 把它接在技能正文末尾,让代理看到当前 profile 的标签开关。
|
|
201
|
+
* 只写一行,列出声明符号、开启与关闭的标签,避免正文膨胀。
|
|
183
202
|
*/
|
|
184
203
|
function snapshotText(raw) {
|
|
185
204
|
const config = normalizeConfig(raw);
|
|
186
|
-
const on = [];
|
|
205
|
+
const on = ['@file'];
|
|
187
206
|
const off = [];
|
|
188
207
|
for (const tag of HEADER_TAGS) {
|
|
189
208
|
const entry = config[tag];
|
|
@@ -193,17 +212,15 @@ function snapshotText(raw) {
|
|
|
193
212
|
else
|
|
194
213
|
off.push(TAG_LABEL[tag]);
|
|
195
214
|
}
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
];
|
|
206
|
-
return head + body.join('\n');
|
|
215
|
+
for (const tag of [...CONTRACT_TAGS, ...EXTRA_TAGS]) {
|
|
216
|
+
if (config[tag])
|
|
217
|
+
on.push(TAG_LABEL[tag]);
|
|
218
|
+
else
|
|
219
|
+
off.push(TAG_LABEL[tag]);
|
|
220
|
+
}
|
|
221
|
+
const style = config.commentStyle === 'block' ? '/** ... */' : '///';
|
|
222
|
+
const line = `- 声明符号:${style}(函数体固定 //);开启 ${on.join('、')};关闭 ${off.join('、') || '无'}。`;
|
|
223
|
+
return `## 当前注释配置(profile)\n\n${line}`;
|
|
207
224
|
}
|
|
208
225
|
|
|
209
226
|
|
|
@@ -218,75 +235,81 @@ const PACKAGE_NAME = '@chengzhao-dev/dsh-cpp-workflow'
|
|
|
218
235
|
const STRINGS = {
|
|
219
236
|
zh: {
|
|
220
237
|
nav: 'C++ 工程流程',
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
238
|
+
heading: '文档注释',
|
|
239
|
+
style: '注释符号',
|
|
240
|
+
bodyRow: '函数体',
|
|
241
|
+
bodyPlaceholder: '函数体:固定 //',
|
|
224
242
|
header: '文件头',
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
authorHint: '作者署名。默认不写;打开并填写后才写进文件头。',
|
|
228
|
-
dateHint: '创建或最后修改日期。默认不写。',
|
|
229
|
-
copyrightHint: '版权声明。默认不写。',
|
|
230
|
-
versionHint: '文件版本号。默认不写。',
|
|
231
|
-
paramHint: '函数参数的含义、范围或所有权。',
|
|
232
|
-
returnHint: '返回值在什么条件下成立。返回类型自明时可以省略。',
|
|
233
|
-
throwsHint: '抛出的异常类型与条件。不抛异常的函数不写。',
|
|
234
|
-
contractHint: '函数声明的注释里必须交代的参数与失败信息。',
|
|
235
|
-
extra: '更多标签',
|
|
236
|
-
extraHint: '默认关闭,需要时在单个函数上补写。',
|
|
243
|
+
type: '类型',
|
|
244
|
+
declaration: '函数声明',
|
|
237
245
|
preview: '预览',
|
|
238
|
-
|
|
246
|
+
filePlaceholder: '跟随文件名',
|
|
247
|
+
briefPlaceholder: '文件摘要:留空则各文件自写',
|
|
248
|
+
authorPlaceholder: '作者署名:默认不写',
|
|
249
|
+
datePlaceholder: '日期:默认不写',
|
|
250
|
+
copyrightPlaceholder: '版权:默认不写',
|
|
251
|
+
versionPlaceholder: '版本:默认不写',
|
|
252
|
+
paramPlaceholder: '参数:默认使用',
|
|
253
|
+
returnPlaceholder: '返回值:默认使用',
|
|
254
|
+
throwsPlaceholder: '异常:默认使用',
|
|
255
|
+
prePlaceholder: '前置条件:默认不使用',
|
|
256
|
+
postPlaceholder: '后置条件:默认不使用',
|
|
257
|
+
retvalPlaceholder: '返回值枚举:默认不使用',
|
|
258
|
+
tparamPlaceholder: '模板参数:默认不使用',
|
|
259
|
+
notePlaceholder: '补充:默认不使用',
|
|
260
|
+
seePlaceholder: '参见:默认不使用',
|
|
239
261
|
save: '保存',
|
|
240
|
-
reset: '
|
|
241
|
-
|
|
242
|
-
|
|
262
|
+
reset: '撤销修改',
|
|
263
|
+
defaults: '恢复默认值',
|
|
264
|
+
restored: '已填入内置默认值,点“保存”后生效。',
|
|
265
|
+
saved: '已保存到 ~/.dsh/profiles/<profile>/cordis.patch.yml,下一次读取技能时生效。',
|
|
266
|
+
rejected: '保存被拒绝:配置可能已在别处修改。请先撤销修改,再重新编辑。',
|
|
243
267
|
unsaved: '有未保存的改动',
|
|
244
268
|
loading: '正在读取配置…',
|
|
245
269
|
unavailable: '这个部署没有提供这份配置。',
|
|
246
270
|
unreadable: '当前账户不能修改这份配置。',
|
|
247
|
-
readOnly: '
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
emptyValue: '留空则由文件自己写',
|
|
251
|
-
blankBrief: '留空则文件自述',
|
|
252
|
-
on: '写出',
|
|
253
|
-
off: '不写',
|
|
271
|
+
readOnly: '这个页面暂时没有连到配置表单,下面的值是内置默认值。重启 DeepSeek Harness 后重新打开本页即可编辑。',
|
|
272
|
+
reload: '重新载入页面',
|
|
273
|
+
noService: '(configForms 服务未连接)',
|
|
254
274
|
},
|
|
255
275
|
en: {
|
|
256
276
|
nav: 'C++ Workflow',
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
277
|
+
heading: 'Doxygen comments',
|
|
278
|
+
style: 'Comment symbol',
|
|
279
|
+
bodyRow: 'Body',
|
|
280
|
+
bodyPlaceholder: 'Function bodies: always //',
|
|
260
281
|
header: 'File header',
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
authorHint: 'Author name. Off by default; written only when filled.',
|
|
264
|
-
dateHint: 'Created or last modified date. Off by default.',
|
|
265
|
-
copyrightHint: 'Copyright notice. Off by default.',
|
|
266
|
-
versionHint: 'File version. Off by default.',
|
|
267
|
-
paramHint: 'Meaning, range, or ownership of a parameter.',
|
|
268
|
-
returnHint: 'When the returned value holds. Optional when the type is self-evident.',
|
|
269
|
-
throwsHint: 'Exception type and condition. Omitted when the function does not throw.',
|
|
270
|
-
contractHint: 'Parameters and failure information every function comment must carry.',
|
|
271
|
-
extra: 'More tags',
|
|
272
|
-
extraHint: 'Off by default; add them per function when needed.',
|
|
282
|
+
type: 'Types',
|
|
283
|
+
declaration: 'Function declarations',
|
|
273
284
|
preview: 'Preview',
|
|
274
|
-
|
|
285
|
+
filePlaceholder: 'Follows the file name',
|
|
286
|
+
briefPlaceholder: 'File summary: leave empty for each file',
|
|
287
|
+
authorPlaceholder: 'Author: off by default',
|
|
288
|
+
datePlaceholder: 'Date: off by default',
|
|
289
|
+
copyrightPlaceholder: 'Copyright: off by default',
|
|
290
|
+
versionPlaceholder: 'Version: off by default',
|
|
291
|
+
paramPlaceholder: 'Parameters: on by default',
|
|
292
|
+
returnPlaceholder: 'Return value: on by default',
|
|
293
|
+
throwsPlaceholder: 'Exceptions: on by default',
|
|
294
|
+
prePlaceholder: 'Preconditions: off by default',
|
|
295
|
+
postPlaceholder: 'Postconditions: off by default',
|
|
296
|
+
retvalPlaceholder: 'Named return values: off by default',
|
|
297
|
+
tparamPlaceholder: 'Template parameters: off by default',
|
|
298
|
+
notePlaceholder: 'Notes: off by default',
|
|
299
|
+
seePlaceholder: 'See also: off by default',
|
|
275
300
|
save: 'Save',
|
|
276
|
-
reset: '
|
|
277
|
-
|
|
278
|
-
|
|
301
|
+
reset: 'Discard changes',
|
|
302
|
+
defaults: 'Restore defaults',
|
|
303
|
+
restored: 'Built-in defaults are filled in; select Save to apply them.',
|
|
304
|
+
saved: 'Saved to ~/.dsh/profiles/<profile>/cordis.patch.yml; the next skill read uses it.',
|
|
305
|
+
rejected: 'The host rejected the save: the config changed elsewhere. Discard your changes and edit again.',
|
|
279
306
|
unsaved: 'Unsaved changes',
|
|
280
307
|
loading: 'Reading configuration…',
|
|
281
308
|
unavailable: 'This deployment does not serve this configuration.',
|
|
282
309
|
unreadable: 'This account cannot edit the configuration.',
|
|
283
|
-
readOnly: 'This
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
emptyValue: 'Leave empty and each file writes its own',
|
|
287
|
-
blankBrief: 'Leave empty for each file',
|
|
288
|
-
on: 'Write',
|
|
289
|
-
off: 'Omit',
|
|
310
|
+
readOnly: 'This page is not connected to a configuration form yet; the values below are built-in defaults. Restart DeepSeek Harness, then reopen this page to edit.',
|
|
311
|
+
reload: 'Reload page',
|
|
312
|
+
noService: ' (the configForms service is not connected)',
|
|
290
313
|
},
|
|
291
314
|
}
|
|
292
315
|
|
|
@@ -315,14 +338,18 @@ const CSS = `
|
|
|
315
338
|
.dcw-card-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 10px;padding:11px 14px 9px;border-bottom:1px solid var(--dcw-border);background:var(--dcw-bg2)}
|
|
316
339
|
.dcw-card-title{margin:0;font-size:12.5px;font-weight:620;letter-spacing:.01em}
|
|
317
340
|
.dcw-card-hint{margin:0;color:var(--dcw-muted);font-size:11.5px;line-height:1.5}
|
|
318
|
-
.dcw-row{display:grid;grid-template-columns:
|
|
341
|
+
.dcw-row{display:grid;grid-template-columns:84px 40px minmax(0,1fr);align-items:center;gap:8px 12px;padding:8px 14px;border-top:1px solid var(--dcw-border)}
|
|
319
342
|
.dcw-row:first-of-type{border-top:0}
|
|
320
|
-
.dcw-row-label{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:var(--dcw-fg)
|
|
321
|
-
.dcw-row-
|
|
322
|
-
.dcw-row-
|
|
323
|
-
.dcw-
|
|
324
|
-
.dcw-
|
|
325
|
-
.dcw-
|
|
343
|
+
.dcw-row-label{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;color:var(--dcw-fg)}
|
|
344
|
+
.dcw-row-style{grid-template-columns:84px minmax(0,1fr)}
|
|
345
|
+
.dcw-row-style .dcw-segmented{grid-column:2}
|
|
346
|
+
.dcw-lock{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border:1px dashed var(--dcw-border);border-radius:6px;background:var(--dcw-bg2);color:var(--dcw-muted)}
|
|
347
|
+
.dcw-segmented{display:inline-flex;padding:2px;border:1px solid var(--dcw-border);border-radius:8px;background:var(--dcw-bg2);justify-self:start}
|
|
348
|
+
.dcw-segment{height:26px;padding:0 12px;border:0;border-radius:6px;background:transparent;color:var(--dcw-muted);font:inherit;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;cursor:pointer}
|
|
349
|
+
.dcw-segment:hover:enabled{color:var(--dcw-fg)}
|
|
350
|
+
.dcw-segment[aria-pressed="true"]{background:var(--dcw-bg1);color:var(--dcw-fg);box-shadow:0 1px 2px rgba(0,0,0,.12)}
|
|
351
|
+
.dcw-segment:focus-visible{outline:2px solid var(--dcw-accent);outline-offset:1px}
|
|
352
|
+
.dcw-segment:disabled{opacity:.45;cursor:default}
|
|
326
353
|
.dcw-switch{position:relative;display:inline-flex;align-items:center;flex:none;width:34px;height:20px;cursor:pointer}
|
|
327
354
|
.dcw-switch input{position:absolute;inset:0;margin:0;opacity:0;cursor:pointer}
|
|
328
355
|
.dcw-track{position:absolute;inset:0;border-radius:999px;background:color-mix(in srgb,var(--dcw-fg) 18%,transparent);transition:background .16s ease}
|
|
@@ -357,7 +384,10 @@ const CSS = `
|
|
|
357
384
|
.dcw-status{flex:1 1 200px;margin:0;color:var(--dcw-muted);font-size:12px}
|
|
358
385
|
.dcw-status[data-tone="warn"]{color:var(--dcw-danger)}
|
|
359
386
|
.dcw-status[data-tone="ok"]{color:var(--dsw-alias-state-success-primary,#1a7f37)}
|
|
360
|
-
|
|
387
|
+
.dcw-note-code{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;opacity:.8;margin-left:6px}
|
|
388
|
+
.dcw-note-action{margin-left:auto;flex:none;padding:3px 10px;border:1px solid currentColor;border-radius:6px;background:transparent;color:inherit;font:inherit;font-size:11.5px;cursor:pointer;white-space:nowrap}
|
|
389
|
+
.dcw-note-action:hover{background:var(--dcw-bg1)}
|
|
390
|
+
@media (max-width:640px){.dcw-row,.dcw-row-style{grid-template-columns:1fr 40px}.dcw-row-label{grid-column:1/-1}.dcw-row-style .dcw-segmented{grid-column:1/-1}}
|
|
361
391
|
`
|
|
362
392
|
|
|
363
393
|
function ensureStyles() {
|
|
@@ -403,78 +433,122 @@ function Chevron() {
|
|
|
403
433
|
)
|
|
404
434
|
}
|
|
405
435
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
throws: 'throwsHint',
|
|
436
|
+
/** 五个区块统一用默认展开的 details,标题在摘要行。 */
|
|
437
|
+
function Section(props) {
|
|
438
|
+
return h(
|
|
439
|
+
'details',
|
|
440
|
+
{ className: 'dcw-card', open: true },
|
|
441
|
+
h('summary', null, h('span', { className: 'dcw-card-title' }, props.title), h(Chevron)),
|
|
442
|
+
props.children,
|
|
443
|
+
)
|
|
415
444
|
}
|
|
416
445
|
|
|
446
|
+
/** 文件头可填写的一行:标签、开关、文本框。 */
|
|
417
447
|
function ValueRow(props) {
|
|
418
|
-
const s = props.strings
|
|
419
448
|
const entry = props.entry
|
|
420
449
|
return h(
|
|
421
450
|
'div',
|
|
422
451
|
{ className: 'dcw-row' },
|
|
423
452
|
h('div', { className: 'dcw-row-label' }, TAG_LABEL[props.tag]),
|
|
424
|
-
h(
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
453
|
+
h(Switch, {
|
|
454
|
+
label: TAG_LABEL[props.tag],
|
|
455
|
+
checked: entry.enabled,
|
|
456
|
+
disabled: props.disabled,
|
|
457
|
+
onChange: (enabled) => props.onChange({ ...entry, enabled }),
|
|
458
|
+
}),
|
|
459
|
+
h('input', {
|
|
460
|
+
className: 'dcw-input',
|
|
461
|
+
type: 'text',
|
|
462
|
+
value: entry.value,
|
|
463
|
+
placeholder: props.placeholder,
|
|
464
|
+
disabled: props.disabled || !entry.enabled,
|
|
465
|
+
'aria-label': `${TAG_LABEL[props.tag]} 的值`,
|
|
466
|
+
onChange: (event) => props.onChange({ ...entry, value: event.target.value }),
|
|
467
|
+
}),
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** 只有开关的标签行;右侧框显示该标签的作用,不进入配置。 */
|
|
472
|
+
function FlagRow(props) {
|
|
473
|
+
return h(
|
|
474
|
+
'div',
|
|
475
|
+
{ className: 'dcw-row' },
|
|
476
|
+
h('div', { className: 'dcw-row-label' }, TAG_LABEL[props.tag]),
|
|
477
|
+
h(Switch, {
|
|
478
|
+
label: TAG_LABEL[props.tag],
|
|
479
|
+
checked: props.checked,
|
|
480
|
+
disabled: props.disabled,
|
|
481
|
+
onChange: props.onChange,
|
|
482
|
+
}),
|
|
483
|
+
h('input', {
|
|
484
|
+
className: 'dcw-input',
|
|
485
|
+
type: 'text',
|
|
486
|
+
value: '',
|
|
487
|
+
placeholder: props.placeholder,
|
|
488
|
+
disabled: true,
|
|
489
|
+
readOnly: true,
|
|
490
|
+
tabIndex: -1,
|
|
491
|
+
'aria-label': props.placeholder,
|
|
492
|
+
}),
|
|
445
493
|
)
|
|
446
494
|
}
|
|
447
495
|
|
|
448
|
-
|
|
496
|
+
/** 值由文件或语法固定的行:中间锁,右侧框给出固定值。 */
|
|
497
|
+
function LockRow(props) {
|
|
449
498
|
return h(
|
|
450
499
|
'div',
|
|
451
|
-
{ className: 'dcw-
|
|
500
|
+
{ className: 'dcw-row dcw-row-lock' },
|
|
501
|
+
h('div', { className: 'dcw-row-label' }, props.label),
|
|
502
|
+
h('span', { className: 'dcw-lock', title: props.placeholder }, h(LockIcon)),
|
|
503
|
+
h('input', {
|
|
504
|
+
className: 'dcw-input',
|
|
505
|
+
type: 'text',
|
|
506
|
+
value: '',
|
|
507
|
+
placeholder: props.placeholder,
|
|
508
|
+
disabled: true,
|
|
509
|
+
readOnly: true,
|
|
510
|
+
tabIndex: -1,
|
|
511
|
+
'aria-label': props.placeholder,
|
|
512
|
+
}),
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** 两种声明注释符号;按钮本身可用键盘切换。 */
|
|
517
|
+
function StyleRow(props) {
|
|
518
|
+
const options = [
|
|
519
|
+
{ value: 'slash', label: '///' },
|
|
520
|
+
{ value: 'block', label: '/** */' },
|
|
521
|
+
]
|
|
522
|
+
return h(
|
|
523
|
+
'div',
|
|
524
|
+
{ className: 'dcw-row dcw-row-style' },
|
|
525
|
+
h('div', { className: 'dcw-row-label' }, props.label),
|
|
452
526
|
h(
|
|
453
527
|
'div',
|
|
454
|
-
{ className: 'dcw-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
528
|
+
{ className: 'dcw-segmented', role: 'group', 'aria-label': props.label },
|
|
529
|
+
options.map((option) =>
|
|
530
|
+
h(
|
|
531
|
+
'button',
|
|
532
|
+
{
|
|
533
|
+
key: option.value,
|
|
534
|
+
type: 'button',
|
|
535
|
+
className: 'dcw-segment',
|
|
536
|
+
'aria-pressed': props.value === option.value ? 'true' : 'false',
|
|
537
|
+
disabled: props.disabled,
|
|
538
|
+
onClick: () => props.onChange(option.value),
|
|
539
|
+
},
|
|
540
|
+
option.label,
|
|
541
|
+
),
|
|
542
|
+
),
|
|
462
543
|
),
|
|
463
|
-
h('p', { className: 'dcw-row-hint' }, props.hint || ''),
|
|
464
544
|
)
|
|
465
545
|
}
|
|
466
546
|
|
|
467
547
|
function Preview(props) {
|
|
468
548
|
const s = props.strings
|
|
469
549
|
return h(
|
|
470
|
-
|
|
471
|
-
{
|
|
472
|
-
h(
|
|
473
|
-
'div',
|
|
474
|
-
{ className: 'dcw-card-head' },
|
|
475
|
-
h('h4', { className: 'dcw-card-title' }, s.preview),
|
|
476
|
-
h('p', { className: 'dcw-card-hint' }, s.previewHint),
|
|
477
|
-
),
|
|
550
|
+
Section,
|
|
551
|
+
{ title: s.preview },
|
|
478
552
|
h('pre', { className: 'dcw-preview' }, previewText(props.config, SAMPLE_FILE)),
|
|
479
553
|
)
|
|
480
554
|
}
|
|
@@ -521,10 +595,43 @@ function useFormSnapshot(form) {
|
|
|
521
595
|
return snapshot
|
|
522
596
|
}
|
|
523
597
|
|
|
598
|
+
// 配置卡片本身:状态机、保存/撤销/恢复默认值,以及组合包条目里的挂点。
|
|
599
|
+
// 分片按 scripts/building-client.mjs 的模块表顺序拼接:shell → text → styles →
|
|
600
|
+
// widgets → form-state → 本文件。分片共享同一个工厂作用域,不要各自 require。
|
|
601
|
+
|
|
602
|
+
// 宿主组合包 cordis.patch.yml 插入的 profile 条目 id;设置服务按它定位表单。
|
|
603
|
+
const ENTRY_ID = 'cpp-workflow'
|
|
604
|
+
// configForms 可能晚于本插件挂载;页面在首次绘制后短暂重试,拿不到再给手动恢复入口。
|
|
605
|
+
const FORM_RETRY_MS = 500
|
|
606
|
+
const MAX_FORM_RETRIES = 20
|
|
607
|
+
|
|
608
|
+
/** 文件头可填写标签的占位文字;顺序与 HEADER_TAGS 一致。 */
|
|
609
|
+
const HEADER_PLACEHOLDER = {
|
|
610
|
+
brief: 'briefPlaceholder',
|
|
611
|
+
author: 'authorPlaceholder',
|
|
612
|
+
date: 'datePlaceholder',
|
|
613
|
+
copyright: 'copyrightPlaceholder',
|
|
614
|
+
version: 'versionPlaceholder',
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** 只有开关的标签按区块取占位文字。 */
|
|
618
|
+
const FLAG_PLACEHOLDER = {
|
|
619
|
+
param: 'paramPlaceholder',
|
|
620
|
+
returns: 'returnPlaceholder',
|
|
621
|
+
throws: 'throwsPlaceholder',
|
|
622
|
+
pre: 'prePlaceholder',
|
|
623
|
+
post: 'postPlaceholder',
|
|
624
|
+
retval: 'retvalPlaceholder',
|
|
625
|
+
tparam: 'tparamPlaceholder',
|
|
626
|
+
note: 'notePlaceholder',
|
|
627
|
+
see: 'seePlaceholder',
|
|
628
|
+
}
|
|
629
|
+
|
|
524
630
|
function ConfigPage(props) {
|
|
525
631
|
const s = t()
|
|
526
632
|
ensureStyles()
|
|
527
|
-
|
|
633
|
+
// form 由挂点注入;resolveForm 兜住“服务晚到”的部署,拿到后下一次绘制自动接上。
|
|
634
|
+
const form = props.form || (typeof props.resolveForm === 'function' ? props.resolveForm() : undefined)
|
|
528
635
|
const state = useFormSnapshot(form)
|
|
529
636
|
const initial = React.useMemo(() => normalizeConfig(state.value), [])
|
|
530
637
|
const [draft, setDraft] = React.useState(initial)
|
|
@@ -533,11 +640,13 @@ function ConfigPage(props) {
|
|
|
533
640
|
const [busy, setBusy] = React.useState(false)
|
|
534
641
|
const [message, setMessage] = React.useState('')
|
|
535
642
|
const [tone, setTone] = React.useState('')
|
|
643
|
+
const [attempt, setAttempt] = React.useState(0)
|
|
536
644
|
const revision = state.revision
|
|
537
645
|
// 快照只有 status / writable / revision;旧版本多出来的 mode 字段已不存在,
|
|
538
646
|
// 再按它判断会把所有可写表单误判成只读。
|
|
539
|
-
const ready = Boolean(state.status === 'ready' && state.writable)
|
|
647
|
+
const ready = Boolean(form && state.status === 'ready' && state.writable)
|
|
540
648
|
const status = state.status
|
|
649
|
+
const missing = !form
|
|
541
650
|
|
|
542
651
|
React.useEffect(() => {
|
|
543
652
|
if (dirty || state.status !== 'ready') return
|
|
@@ -546,6 +655,12 @@ function ConfigPage(props) {
|
|
|
546
655
|
setDraft(next)
|
|
547
656
|
}, [revision, status, dirty, form, state.value])
|
|
548
657
|
|
|
658
|
+
React.useEffect(() => {
|
|
659
|
+
if (form || attempt >= MAX_FORM_RETRIES || typeof setTimeout !== 'function') return undefined
|
|
660
|
+
const timer = setTimeout(() => setAttempt((value) => value + 1), FORM_RETRY_MS)
|
|
661
|
+
return () => clearTimeout(timer)
|
|
662
|
+
}, [form, attempt])
|
|
663
|
+
|
|
549
664
|
const update = (patch) => {
|
|
550
665
|
setDraft((current) => ({ ...current, ...patch }))
|
|
551
666
|
setDirty(true)
|
|
@@ -553,6 +668,7 @@ function ConfigPage(props) {
|
|
|
553
668
|
setTone('')
|
|
554
669
|
}
|
|
555
670
|
|
|
671
|
+
// 撤销:回到最近一次从宿主读到的值,不写盘。
|
|
556
672
|
const reset = () => {
|
|
557
673
|
setDraft(base)
|
|
558
674
|
setDirty(false)
|
|
@@ -560,6 +676,20 @@ function ConfigPage(props) {
|
|
|
560
676
|
setTone('')
|
|
561
677
|
}
|
|
562
678
|
|
|
679
|
+
// 恢复默认值:只填进草稿,点“保存”后才写盘,避免误触直接覆盖已保存的配置。
|
|
680
|
+
const restoreDefaults = () => {
|
|
681
|
+
setDraft(normalizeConfig(DEFAULT_CONFIG))
|
|
682
|
+
setDirty(true)
|
|
683
|
+
setMessage(s.restored)
|
|
684
|
+
setTone('')
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const reload = () => {
|
|
688
|
+
if (typeof window !== 'undefined' && window.location && typeof window.location.reload === 'function') {
|
|
689
|
+
window.location.reload()
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
563
693
|
const save = async () => {
|
|
564
694
|
if (!ready || busy) return
|
|
565
695
|
const operations = diffOperations(draft, base)
|
|
@@ -573,6 +703,9 @@ function ConfigPage(props) {
|
|
|
573
703
|
setMessage(s.saved)
|
|
574
704
|
setTone('ok')
|
|
575
705
|
} else {
|
|
706
|
+
// 被拒时宿主已刷新镜像;把基准对齐到新快照,撤销才不会回到过期值。
|
|
707
|
+
const fresh = snapshotOf(form)
|
|
708
|
+
if (fresh && fresh.status === 'ready') setBase(normalizeConfig(fresh.value))
|
|
576
709
|
setMessage(s.rejected)
|
|
577
710
|
setTone('warn')
|
|
578
711
|
}
|
|
@@ -587,22 +720,33 @@ function ConfigPage(props) {
|
|
|
587
720
|
const editable = Boolean(form)
|
|
588
721
|
const disabled = !ready
|
|
589
722
|
|
|
723
|
+
/** 一个标签一行:中间开关,右侧框给默认值或占位说明。 */
|
|
724
|
+
const flag = (tag, checked) =>
|
|
725
|
+
h(FlagRow, {
|
|
726
|
+
key: tag,
|
|
727
|
+
tag,
|
|
728
|
+
checked,
|
|
729
|
+
disabled,
|
|
730
|
+
placeholder: s[FLAG_PLACEHOLDER[tag]],
|
|
731
|
+
onChange: (value) => update({ [tag]: value }),
|
|
732
|
+
})
|
|
733
|
+
|
|
590
734
|
return h(
|
|
591
735
|
'div',
|
|
592
736
|
{ className: 'dcw-page' },
|
|
593
|
-
h(
|
|
594
|
-
|
|
595
|
-
{ className: 'dcw-head' },
|
|
596
|
-
h('p', { className: 'dcw-eyebrow' }, s.eyebrow),
|
|
597
|
-
h('h3', { className: 'dcw-title' }, s.title),
|
|
598
|
-
h('p', { className: 'dcw-lede' }, s.intro),
|
|
599
|
-
),
|
|
600
|
-
!editable
|
|
737
|
+
h('header', { className: 'dcw-head' }, h('h3', { className: 'dcw-title' }, s.heading)),
|
|
738
|
+
missing
|
|
601
739
|
? h(
|
|
602
740
|
'div',
|
|
603
|
-
{ className: 'dcw-note', role: 'status' },
|
|
741
|
+
{ className: 'dcw-note', role: 'status', 'data-tone': 'warn' },
|
|
604
742
|
h('span', { className: 'dcw-note-dot' }),
|
|
605
|
-
h(
|
|
743
|
+
h(
|
|
744
|
+
'span',
|
|
745
|
+
null,
|
|
746
|
+
s.readOnly,
|
|
747
|
+
props.hasService === false ? h('span', { className: 'dcw-note-code' }, s.noService) : null,
|
|
748
|
+
),
|
|
749
|
+
h('button', { type: 'button', className: 'dcw-note-action', onClick: reload }, s.reload),
|
|
606
750
|
)
|
|
607
751
|
: null,
|
|
608
752
|
editable && !ready && status !== 'missing'
|
|
@@ -614,78 +758,36 @@ function ConfigPage(props) {
|
|
|
614
758
|
)
|
|
615
759
|
: null,
|
|
616
760
|
h(
|
|
617
|
-
|
|
618
|
-
{
|
|
619
|
-
h(
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
),
|
|
625
|
-
h(
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
{ className: 'dcw-row-control' },
|
|
632
|
-
h('span', { className: 'dcw-static' }, h(LockIcon), s.fixed),
|
|
633
|
-
h('p', { className: 'dcw-row-hint' }, s.fixedHint),
|
|
634
|
-
),
|
|
635
|
-
),
|
|
761
|
+
Section,
|
|
762
|
+
{ title: s.style },
|
|
763
|
+
h(StyleRow, {
|
|
764
|
+
label: s.style,
|
|
765
|
+
value: draft.commentStyle,
|
|
766
|
+
disabled,
|
|
767
|
+
onChange: (value) => update({ commentStyle: value }),
|
|
768
|
+
}),
|
|
769
|
+
h(LockRow, { label: s.bodyRow, placeholder: s.bodyPlaceholder }),
|
|
770
|
+
),
|
|
771
|
+
h(
|
|
772
|
+
Section,
|
|
773
|
+
{ title: s.header },
|
|
774
|
+
h(LockRow, { label: TAG_LABEL.file, placeholder: s.filePlaceholder }),
|
|
636
775
|
...HEADER_TAGS.map((tag) =>
|
|
637
776
|
h(ValueRow, {
|
|
638
777
|
key: tag,
|
|
639
778
|
tag,
|
|
640
|
-
strings: s,
|
|
641
779
|
entry: draft[tag],
|
|
642
780
|
disabled,
|
|
781
|
+
placeholder: s[HEADER_PLACEHOLDER[tag]],
|
|
643
782
|
onChange: (entry) => update({ [tag]: entry }),
|
|
644
783
|
}),
|
|
645
784
|
),
|
|
646
785
|
),
|
|
786
|
+
h(Section, { title: s.type }, ...TYPE_TAGS.map((tag) => flag(tag, draft[tag]))),
|
|
647
787
|
h(
|
|
648
|
-
|
|
649
|
-
{
|
|
650
|
-
|
|
651
|
-
'div',
|
|
652
|
-
{ className: 'dcw-card-head' },
|
|
653
|
-
h('h4', { className: 'dcw-card-title', id: 'dcw-contract-title' }, s.contract),
|
|
654
|
-
h('p', { className: 'dcw-card-hint' }, s.contractHint),
|
|
655
|
-
),
|
|
656
|
-
h(
|
|
657
|
-
'div',
|
|
658
|
-
{ className: 'dcw-flags' },
|
|
659
|
-
...CONTRACT_TAGS.map((tag) =>
|
|
660
|
-
h(Flag, {
|
|
661
|
-
key: tag,
|
|
662
|
-
tag,
|
|
663
|
-
checked: draft[tag],
|
|
664
|
-
disabled,
|
|
665
|
-
hint: s[HINT_KEY[tag]],
|
|
666
|
-
onChange: (value) => update({ [tag]: value }),
|
|
667
|
-
}),
|
|
668
|
-
),
|
|
669
|
-
),
|
|
670
|
-
),
|
|
671
|
-
h(
|
|
672
|
-
'details',
|
|
673
|
-
{ className: 'dcw-card' },
|
|
674
|
-
h('summary', null, h('span', { className: 'dcw-card-title' }, s.extra), h(Chevron)),
|
|
675
|
-
h('p', { className: 'dcw-card-hint', style: { margin: '0 14px 2px' } }, s.extraHint),
|
|
676
|
-
h(
|
|
677
|
-
'div',
|
|
678
|
-
{ className: 'dcw-flags' },
|
|
679
|
-
...EXTRA_TAGS.map((tag) =>
|
|
680
|
-
h(Flag, {
|
|
681
|
-
key: tag,
|
|
682
|
-
tag,
|
|
683
|
-
checked: draft[tag],
|
|
684
|
-
disabled,
|
|
685
|
-
onChange: (value) => update({ [tag]: value }),
|
|
686
|
-
}),
|
|
687
|
-
),
|
|
688
|
-
),
|
|
788
|
+
Section,
|
|
789
|
+
{ title: s.declaration },
|
|
790
|
+
...[...CONTRACT_TAGS, ...FUNCTION_EXTRA_TAGS].map((tag) => flag(tag, draft[tag])),
|
|
689
791
|
),
|
|
690
792
|
h(Preview, { config: draft, strings: s }),
|
|
691
793
|
editable
|
|
@@ -704,24 +806,31 @@ function ConfigPage(props) {
|
|
|
704
806
|
),
|
|
705
807
|
h('button', { type: 'button', className: 'dcw-btn', onClick: reset, disabled: busy || !dirty }, s.reset),
|
|
706
808
|
h(
|
|
707
|
-
'
|
|
708
|
-
{ className: 'dcw-
|
|
709
|
-
|
|
809
|
+
'button',
|
|
810
|
+
{ type: 'button', className: 'dcw-btn', onClick: restoreDefaults, disabled: busy || !ready },
|
|
811
|
+
s.defaults,
|
|
710
812
|
),
|
|
813
|
+
h('p', { className: 'dcw-status', role: 'status', 'data-tone': tone }, message || (dirty ? s.unsaved : '')),
|
|
711
814
|
)
|
|
712
815
|
: null,
|
|
713
816
|
)
|
|
714
817
|
}
|
|
715
818
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
function page(ownerProps, form) {
|
|
720
|
-
return h(ConfigPage, { ...(ownerProps || {}), form })
|
|
819
|
+
function page(ownerProps, form, resolveForm, hasService) {
|
|
820
|
+
return h(ConfigPage, { ...(ownerProps || {}), form, resolveForm, hasService })
|
|
721
821
|
}
|
|
722
822
|
|
|
723
823
|
function apply(ctx) {
|
|
724
|
-
|
|
824
|
+
// 每次取表单都重新查询服务:宿主可能在本插件之后才挂载 configForms。
|
|
825
|
+
const resolveForm = () => {
|
|
826
|
+
if (!ctx.configForms || typeof ctx.configForms.get !== 'function') return undefined
|
|
827
|
+
try {
|
|
828
|
+
return ctx.configForms.get(ENTRY_ID)
|
|
829
|
+
} catch {
|
|
830
|
+
return undefined
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
const hasService = Boolean(ctx.configForms && typeof ctx.configForms.get === 'function')
|
|
725
834
|
const mount = () =>
|
|
726
835
|
ctx.slots.inject('plugins.bundle.config', () =>
|
|
727
836
|
ctx.slots.register(
|
|
@@ -730,7 +839,10 @@ function apply(ctx) {
|
|
|
730
839
|
key: PACKAGE_NAME,
|
|
731
840
|
label: () => t().nav,
|
|
732
841
|
},
|
|
733
|
-
(ownerProps) =>
|
|
842
|
+
(ownerProps) =>
|
|
843
|
+
ownerProps && ownerProps.view === 'summary'
|
|
844
|
+
? null
|
|
845
|
+
: page(ownerProps, undefined, resolveForm, hasService),
|
|
734
846
|
),
|
|
735
847
|
)
|
|
736
848
|
if (ctx.configForms && typeof ctx.configForms.whileServed === 'function') {
|