@dsh-workbench/shared 0.3.3 → 0.3.5
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/dist/api/blocker-table.d.ts +13 -0
- package/dist/api/blocker-table.js +13 -0
- package/dist/api/blocker.d.ts +6 -0
- package/dist/api/context-ref.d.ts +31 -0
- package/dist/api/context-ref.js +28 -0
- package/dist/api/dsh-client.d.ts +99 -0
- package/dist/api/dsh.d.ts +55 -3
- package/dist/api/facts.d.ts +42 -0
- package/dist/api/facts.js +33 -0
- package/dist/api/host-write.d.ts +19 -1
- package/dist/api/host-write.js +12 -0
- package/dist/api/long-task.d.ts +36 -0
- package/dist/api/long-task.js +27 -0
- package/dist/api/unit-board.d.ts +25 -0
- package/dist/api/unit-board.js +16 -0
- package/dist/api/unit-declaration.d.ts +36 -0
- package/dist/api/unit-declaration.js +24 -0
- package/dist/api/unit.d.ts +32 -0
- package/dist/api/unit.js +5 -0
- package/dist/api/workbench-client.d.ts +25 -0
- package/dist/api/workbench-client.js +13 -0
- package/dist/api/workbench-contract.d.ts +12 -0
- package/dist/components/Activity.d.ts +33 -0
- package/dist/components/Activity.js +24 -0
- package/dist/components/ApprovalDetail.d.ts +15 -1
- package/dist/components/ApprovalDetail.js +13 -1
- package/dist/components/ApprovalPlan.d.ts +14 -0
- package/dist/components/ApprovalPlan.js +8 -0
- package/dist/components/Card.d.ts +13 -0
- package/dist/components/Card.js +10 -0
- package/dist/components/CardAnchor.d.ts +14 -0
- package/dist/components/CardAnchor.js +11 -0
- package/dist/components/CardBoundary.d.ts +20 -0
- package/dist/components/CardBoundary.js +14 -0
- package/dist/components/CardHead.d.ts +13 -0
- package/dist/components/CardHead.js +10 -0
- package/dist/components/ChoiceGroup.d.ts +24 -0
- package/dist/components/ChoiceGroup.js +12 -0
- package/dist/components/Field.d.ts +10 -0
- package/dist/components/Field.js +7 -0
- package/dist/components/Form.css +6 -0
- package/dist/components/Form.d.ts +20 -0
- package/dist/components/Form.js +58 -0
- package/dist/components/FormActions.d.ts +14 -0
- package/dist/components/FormActions.js +5 -0
- package/dist/components/FormNodes.d.ts +103 -0
- package/dist/components/FormNodes.js +153 -0
- package/dist/components/FormRows.d.ts +20 -0
- package/dist/components/FormRows.js +14 -0
- package/dist/components/FormTypes.d.ts +59 -0
- package/dist/components/FormTypes.js +1 -0
- package/dist/components/Link.d.ts +12 -0
- package/dist/components/Link.js +9 -0
- package/dist/components/LongTaskRow.d.ts +19 -0
- package/dist/components/LongTaskRow.js +13 -0
- package/dist/components/Note.d.ts +11 -0
- package/dist/components/Note.js +8 -0
- package/dist/components/ResourceStates.d.ts +25 -0
- package/dist/components/ResourceStates.js +22 -0
- package/dist/components/Rows.d.ts +17 -0
- package/dist/components/Rows.js +11 -0
- package/dist/components/SaveBar.d.ts +12 -0
- package/dist/components/SaveBar.js +9 -0
- package/dist/components/SelectField.d.ts +26 -0
- package/dist/components/SelectField.js +22 -0
- package/dist/components/SelectFieldNodes.d.ts +24 -0
- package/dist/components/SelectFieldNodes.js +24 -0
- package/dist/components/SettingsSection.d.ts +11 -0
- package/dist/components/SettingsSection.js +8 -0
- package/dist/components/Spinner.d.ts +9 -0
- package/dist/components/Spinner.js +6 -0
- package/dist/components/Stack.d.ts +8 -0
- package/dist/components/Stack.js +5 -0
- package/dist/components/StateBlock.d.ts +13 -0
- package/dist/components/StateBlock.js +10 -0
- package/dist/components/TextField.d.ts +8 -0
- package/dist/components/TextField.js +5 -0
- package/dist/components/board-module.d.ts +30 -0
- package/dist/components/icons.d.ts +38 -0
- package/dist/components/icons.js +35 -0
- package/dist/config/constants.d.ts +45 -0
- package/dist/config/constants.js +45 -0
- package/dist/config/storage.d.ts +4 -0
- package/dist/config/storage.js +4 -0
- package/dist/host/browser-read.d.ts +1 -0
- package/dist/host/browser-read.js +1 -0
- package/dist/host/http.d.ts +3 -0
- package/dist/host/http.js +10 -1
- package/dist/host/tool-approval.d.ts +5 -0
- package/dist/host/tool-approval.js +11 -5
- package/dist/host/unit-face.d.ts +5 -1
- package/dist/host.d.ts +1 -0
- package/dist/host.js +1 -0
- package/dist/i18n/en.d.ts +1 -0
- package/dist/i18n/en.js +22 -8
- package/dist/i18n/index.d.ts +25 -9
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/merge.d.ts +5 -0
- package/dist/i18n/merge.js +2 -0
- package/dist/i18n/zh.d.ts +23 -8
- package/dist/i18n/zh.js +22 -8
- package/dist/index.d.ts +7 -2
- package/dist/index.js +7 -2
- package/dist/lib/blocker-action.d.ts +22 -0
- package/dist/lib/blocker-action.js +13 -0
- package/dist/lib/board-fact.d.ts +27 -0
- package/dist/lib/board-fact.js +18 -0
- package/dist/lib/busy.d.ts +10 -0
- package/dist/lib/busy.js +7 -0
- package/dist/lib/diy-debug.d.ts +42 -2
- package/dist/lib/diy-debug.js +19 -1
- package/dist/lib/diy-record.d.ts +98 -0
- package/dist/lib/diy-record.js +96 -0
- package/dist/lib/form-problem.d.ts +10 -0
- package/dist/lib/form-problem.js +10 -0
- package/dist/lib/panel-state.d.ts +60 -0
- package/dist/lib/panel-state.js +30 -0
- package/dist/lib/react.d.ts +39 -0
- package/dist/lib/react.js +36 -0
- package/dist/lib/text.d.ts +22 -0
- package/dist/lib/text.js +22 -0
- package/dist/lib/ui.d.ts +50 -0
- package/dist/lib/ui.js +41 -0
- package/package.json +1 -1
- package/src/api/blocker-table.ts +13 -0
- package/src/api/blocker.ts +6 -0
- package/src/api/context-ref.ts +31 -0
- package/src/api/dsh-client.ts +99 -0
- package/src/api/dsh.ts +56 -1
- package/src/api/facts.ts +42 -0
- package/src/api/host-write.ts +20 -1
- package/src/api/long-task.ts +36 -0
- package/src/api/unit-board.ts +25 -0
- package/src/api/unit-declaration.ts +36 -0
- package/src/api/unit.ts +32 -0
- package/src/api/workbench-client.ts +25 -0
- package/src/api/workbench-contract.ts +12 -0
- package/src/components/Activity.tsx +33 -0
- package/src/components/ApprovalDetail.tsx +17 -1
- package/src/components/ApprovalPlan.tsx +14 -0
- package/src/components/Card.tsx +13 -0
- package/src/components/CardAnchor.tsx +14 -0
- package/src/components/CardBoundary.tsx +20 -0
- package/src/components/CardHead.tsx +13 -0
- package/src/components/ChoiceGroup.tsx +24 -0
- package/src/components/Field.tsx +10 -0
- package/src/components/Form.css +6 -0
- package/src/components/Form.test.tsx +475 -0
- package/src/components/Form.tsx +78 -0
- package/src/components/FormActions.tsx +14 -0
- package/src/components/FormNodes.tsx +193 -0
- package/src/components/FormRows.tsx +20 -0
- package/src/components/FormTypes.ts +65 -0
- package/src/components/FormWiring.test.tsx +88 -0
- package/src/components/Link.tsx +12 -0
- package/src/components/LongTaskRow.tsx +19 -0
- package/src/components/Note.tsx +11 -0
- package/src/components/ResourceStates.tsx +25 -0
- package/src/components/Rows.tsx +17 -0
- package/src/components/SaveBar.tsx +12 -0
- package/src/components/SelectField.tsx +26 -0
- package/src/components/SelectFieldNodes.tsx +24 -0
- package/src/components/SettingsSection.tsx +11 -0
- package/src/components/Spinner.tsx +9 -0
- package/src/components/Stack.tsx +8 -0
- package/src/components/StateBlock.tsx +13 -0
- package/src/components/TextField.tsx +8 -0
- package/src/components/board-module.ts +30 -0
- package/src/components/icons.tsx +38 -0
- package/src/config/constants.ts +48 -0
- package/src/config/storage.ts +4 -0
- package/src/host/browser-read.ts +1 -0
- package/src/host/http.ts +15 -1
- package/src/host/tool-approval.ts +18 -4
- package/src/host/unit-face.ts +6 -1
- package/src/host.ts +1 -0
- package/src/i18n/en.ts +22 -8
- package/src/i18n/index.ts +3 -0
- package/src/i18n/merge.ts +5 -0
- package/src/i18n/zh.ts +23 -8
- package/src/index.ts +7 -2
- package/src/lib/blocker-action.ts +22 -0
- package/src/lib/board-fact.ts +27 -0
- package/src/lib/busy.ts +10 -0
- package/src/lib/diy-debug.test.ts +2 -1
- package/src/lib/diy-debug.ts +44 -3
- package/src/lib/diy-record.test.ts +171 -0
- package/src/lib/diy-record.ts +185 -0
- package/src/lib/form-problem.ts +10 -0
- package/src/lib/panel-state.ts +60 -0
- package/src/lib/react.ts +39 -0
- package/src/lib/text.ts +22 -0
- package/src/lib/ui.ts +50 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import type { ReactNode } from '../lib/react.js';
|
|
2
|
+
import { h } from '../lib/react.js';
|
|
3
|
+
import type { FormAction } from './FormActions.js';
|
|
4
|
+
import type { FormField, FormValue } from './FormTypes.js';
|
|
5
|
+
import { FormRow } from './FormRows.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 只剩要显示的那些格子 —— `visible: false` 的当它不存在:不渲染、不校验、不进 `values`,也不参与找第一处错误。
|
|
9
|
+
* @param fields - 表单的字段清单。
|
|
10
|
+
* @returns 保序的新数组,`visible` 不给或为 `true` 的都在;全不可见时给空数组。
|
|
11
|
+
*/
|
|
12
|
+
export function visibleFieldsOf(fields: readonly FormField[]): readonly FormField[] {
|
|
13
|
+
return fields.filter((field) => field.visible !== false);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 找出装配错的键:**假值**的键(空串 / `undefined` / `null`,运行期真漏给 `key` 就是这条)每次出现都算一处,
|
|
18
|
+
* 以及同一张表单里出现两次的键 —— 这两样都会让 `id` 撞车、`values` 少一格。
|
|
19
|
+
* @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
|
|
20
|
+
* @returns 出问题的键的清单、按首次出现顺序去重;缺失的键一律记成空串(报的时候印成 `(空 key)`),空表给空数组。
|
|
21
|
+
*/
|
|
22
|
+
export function duplicateKeysOf(fields: readonly FormField[]): readonly string[] {
|
|
23
|
+
const seen = new Set<string>();
|
|
24
|
+
const duplicated = new Set<string>();
|
|
25
|
+
for (const field of fields) {
|
|
26
|
+
const key = field.key;
|
|
27
|
+
if (!key) {
|
|
28
|
+
duplicated.add('');
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (seen.has(key)) duplicated.add(key);
|
|
32
|
+
seen.add(key);
|
|
33
|
+
}
|
|
34
|
+
return [...duplicated];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 装配错了要当场诊断:`key` 缺失或重复时给 `console.error` —— 但照渲不误,不 throw。
|
|
39
|
+
* 形式与理由同 `FormActions` 里那颗多出来的 `primary`:装配的小错不该让 `CardBoundary` 把用户的整张卡变空白。
|
|
40
|
+
* @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
|
|
41
|
+
*/
|
|
42
|
+
export function reportDuplicateKeys(fields: readonly FormField[]): void {
|
|
43
|
+
const duplicated = duplicateKeysOf(fields);
|
|
44
|
+
if (duplicated.length === 0) return;
|
|
45
|
+
const named = duplicated.map((key) => (key === '' ? '(空 key)' : key));
|
|
46
|
+
console.error(
|
|
47
|
+
`[dsh-workbench] Form 里有重复或缺失的字段 key:${named.join('、')} —— ` +
|
|
48
|
+
'同一张表单里 key 必须唯一且非空(它决定 id 与 values 的键),否则 id 会撞车、values 会少一格',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 把每格的 `value` 收成一张键值表交给 `onSubmit` —— `Form` 自己一个值都不存,这张表就是从 `fields` 现取的快照。
|
|
54
|
+
* @param fields - 表单的字段清单;取的就是它们的 `value`,`value` 是数组的那格(多选)原样带出、不复制。
|
|
55
|
+
* @returns 键为字段 `key`、值为该格 `value` 的表;`fields` 为空时给空表,不是 `undefined`。
|
|
56
|
+
*/
|
|
57
|
+
export function formValuesOf(fields: readonly FormField[]): Readonly<Record<string, FormValue>> {
|
|
58
|
+
const values: Record<string, FormValue> = {};
|
|
59
|
+
for (const field of fields) values[field.key] = field.value;
|
|
60
|
+
return values;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 算每格现在该不该报错:还没提交过(`show` 为 false)时一格都不报 —— 不边打字边骂人。
|
|
65
|
+
* `validate` 返回**真值**才算错,假值(`undefined` / `null` / `false` / 空串)一律当"这格没错" ——
|
|
66
|
+
* 所以 `v.trim() === '' && '必填'` 这种用 `&&` 算文案的写法是对的。
|
|
67
|
+
* @param fields - 表单的字段清单(通常是已经过滤过可见性的那份)。
|
|
68
|
+
* @param show - 提交过没有;false 时全给 `undefined`,true 时逐格调 `field.validate` 并按真值收。
|
|
69
|
+
* @returns 键为 `key`、值为那句文案或 `undefined` 的表;没给 `validate` 的格子也是 `undefined`。
|
|
70
|
+
*/
|
|
71
|
+
export function formProblemsOf(
|
|
72
|
+
fields: readonly FormField[],
|
|
73
|
+
show: boolean,
|
|
74
|
+
): Readonly<Record<string, ReactNode | undefined>> {
|
|
75
|
+
const values = formValuesOf(fields);
|
|
76
|
+
const problems: Record<string, ReactNode | undefined> = {};
|
|
77
|
+
for (const field of fields) {
|
|
78
|
+
const found = show ? field.validate?.(field.value, values) : undefined;
|
|
79
|
+
problems[field.key] = found ? found : undefined;
|
|
80
|
+
}
|
|
81
|
+
return problems;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 按字段顺序取第一处错误 —— 提交被挡住时要带进视口的就是这一格。
|
|
86
|
+
* 判的是**真值**(与 `formProblemsOf` 同一口径):假值那种"错误文案"渲染出来本来就是空的,不算错。
|
|
87
|
+
* @param fields - 表单的字段清单,顺序就是「第一处」的判据。
|
|
88
|
+
* @param problems - `formProblemsOf` 那张表,也可以是调用点自己拼的(同样按真值判)。
|
|
89
|
+
* @returns 第一格有错的字段;一格都没错时给 `undefined`。
|
|
90
|
+
*/
|
|
91
|
+
export function formFirstProblemOf(
|
|
92
|
+
fields: readonly FormField[],
|
|
93
|
+
problems: Readonly<Record<string, ReactNode | undefined>>,
|
|
94
|
+
): FormField | undefined {
|
|
95
|
+
return fields.find((field) => Boolean(problems[field.key]));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 提交那一拍的结果:有错就挡住(并点出第一处错在哪一格),没错就把键值表交出去。
|
|
100
|
+
*/
|
|
101
|
+
export type FormSubmitOutcome =
|
|
102
|
+
| { readonly blocked: true; readonly first: FormField }
|
|
103
|
+
| { readonly blocked: false; readonly values: Readonly<Record<string, FormValue>> };
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 算提交那一拍该干什么 —— `Form` 的提交钮点下去、以及 `<form>` 的 Enter 提交,走的都是它。
|
|
107
|
+
* 错的判据只有一份(`formProblemsOf` + `formFirstProblemOf`),这里不重算第二遍。
|
|
108
|
+
* @param fields - 要提交的那些格子(通常是调用点先 `visibleFieldsOf` 过滤过的,不可见那格不进 `values` 也不算错)。
|
|
109
|
+
* @returns `blocked: true` 时带上是按字段顺序数第一格有错的字段(拿它去 `revealFirstProblem`);
|
|
110
|
+
* `blocked: false` 时带上从 `fields` 现取的键值表。空表一定给 `blocked: false` 与空 values。
|
|
111
|
+
*/
|
|
112
|
+
export function formSubmitOf(fields: readonly FormField[]): FormSubmitOutcome {
|
|
113
|
+
const problems = formProblemsOf(fields, true);
|
|
114
|
+
const first = formFirstProblemOf(fields, problems);
|
|
115
|
+
if (first !== undefined) return { blocked: true, first };
|
|
116
|
+
return { blocked: false, values: formValuesOf(fields) };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 组表单的操作区:调用点给的其余动作在最左,取消在中间,提交永远是最右那颗 `primary`。
|
|
121
|
+
* @param args.extra - 其余动作(default / danger),保持调用点给的顺序;不给就是没有。
|
|
122
|
+
* @param args.cancel - 取消那格的 `label` 与 `onClick`;不给就少一颗钮。
|
|
123
|
+
* @param args.submit - 提交钮的 `label` 与 `busyLabel`。
|
|
124
|
+
* @param args.busy - 提交中:提交钮带转圈并换 `busyLabel`(不给就沿用 `label`),`extra` 与取消一起禁用。
|
|
125
|
+
* @param args.disabled - 只再禁提交钮(如没改动可存),其余动作不受影响。
|
|
126
|
+
* @param args.onSubmitClick - 提交钮点下去做什么,通常是把这一拍交给 `Form` 的提交处理。
|
|
127
|
+
* @returns 给 `FormActions` 的动作清单;没给 `extra` 也没给 `cancel` 时只剩提交那一颗。
|
|
128
|
+
*/
|
|
129
|
+
export function formActionsOf(args: {
|
|
130
|
+
readonly extra: readonly FormAction[] | undefined;
|
|
131
|
+
readonly cancel: { readonly label: ReactNode; readonly onClick: () => void } | undefined;
|
|
132
|
+
readonly submit: { readonly label: ReactNode; readonly busyLabel?: ReactNode | undefined };
|
|
133
|
+
readonly busy: boolean;
|
|
134
|
+
readonly disabled: boolean;
|
|
135
|
+
readonly onSubmitClick: () => void;
|
|
136
|
+
}): readonly FormAction[] {
|
|
137
|
+
const submit: FormAction = {
|
|
138
|
+
key: 'submit',
|
|
139
|
+
label: args.busy ? (args.submit.busyLabel ?? args.submit.label) : args.submit.label,
|
|
140
|
+
kind: 'primary',
|
|
141
|
+
busy: args.busy,
|
|
142
|
+
disabled: args.busy || args.disabled,
|
|
143
|
+
onClick: args.onSubmitClick,
|
|
144
|
+
};
|
|
145
|
+
const cancel: readonly FormAction[] =
|
|
146
|
+
args.cancel === undefined
|
|
147
|
+
? []
|
|
148
|
+
: [
|
|
149
|
+
{
|
|
150
|
+
key: 'cancel',
|
|
151
|
+
label: args.cancel.label,
|
|
152
|
+
kind: 'default',
|
|
153
|
+
disabled: args.busy,
|
|
154
|
+
onClick: args.cancel.onClick,
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
const extra = (args.extra ?? []).map(
|
|
158
|
+
(action): FormAction => (args.busy ? { ...action, disabled: true } : action),
|
|
159
|
+
);
|
|
160
|
+
return [...extra, ...cancel, submit];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 把整张表单的行画出来 —— 每格一个 `FormRow`,且**直接**是 `FormRows` 的孩子。
|
|
165
|
+
* 中间不许再包一层:`FormRow` 一旦不是 `.wb-formRows` 的网格项,`FormRows.css` 里那条 `subgrid` 就不匹配,
|
|
166
|
+
* 每行各自算列宽,控件左沿就参差了。
|
|
167
|
+
* @param args.fields - 要画的格子(通常是已经过滤过可见性的那份),顺序就是行的顺序。
|
|
168
|
+
* @param args.problems - `formProblemsOf` 那张表,决定哪一格显示错误文案(按**真值**判,假值不当错)。
|
|
169
|
+
* @param args.showProblem - 这一拍该不该显示错误(还没提交过时为 false,一格都不显示)。
|
|
170
|
+
* @param args.busy - 提交中:每格的绑定 `disabled` 为真。
|
|
171
|
+
* @returns `FormRow` 组成的数组;没有可画的格子时给空数组。
|
|
172
|
+
*/
|
|
173
|
+
export function formRowsOf(args: {
|
|
174
|
+
readonly fields: readonly FormField[];
|
|
175
|
+
readonly problems: Readonly<Record<string, ReactNode | undefined>>;
|
|
176
|
+
readonly showProblem: boolean;
|
|
177
|
+
readonly busy: boolean;
|
|
178
|
+
}): readonly ReactNode[] {
|
|
179
|
+
return args.fields.map((field) =>
|
|
180
|
+
h(FormRow, {
|
|
181
|
+
key: field.key,
|
|
182
|
+
label: field.label,
|
|
183
|
+
hint: field.hint,
|
|
184
|
+
problem: args.showProblem ? args.problems[field.key] : undefined,
|
|
185
|
+
layout: field.layout,
|
|
186
|
+
}, field.control({
|
|
187
|
+
id: `wb-form-${field.key}`,
|
|
188
|
+
name: field.key,
|
|
189
|
+
invalid: args.showProblem && Boolean(args.problems[field.key]),
|
|
190
|
+
disabled: args.busy || field.disabled === true,
|
|
191
|
+
})),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -2,14 +2,25 @@ import type { ReactNode } from '../lib/react.js';
|
|
|
2
2
|
import { h } from '../lib/react.js';
|
|
3
3
|
import './FormRows.css';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* `FormRows` 的契约:一列 `FormRow` 的容器。
|
|
7
|
+
*/
|
|
5
8
|
export interface FormRowsProps {
|
|
6
9
|
readonly children?: ReactNode | undefined;
|
|
7
10
|
}
|
|
8
11
|
|
|
12
|
+
/**
|
|
13
|
+
* 表单行的容器:同一份 `FormRows` 共用一个标签列(列宽由最长的标签定),行距也归它。
|
|
14
|
+
* @param props.children - 一串 `FormRow`;非行的孩子(如分组标题)自动通栏。
|
|
15
|
+
* @returns 竖排的 div。
|
|
16
|
+
*/
|
|
9
17
|
export function FormRows({ children }: FormRowsProps) {
|
|
10
18
|
return <div className="wb-formRows">{children}</div>;
|
|
11
19
|
}
|
|
12
20
|
|
|
21
|
+
/**
|
|
22
|
+
* `FormRow` 的契约:标签、提示、错误与控件的排布。
|
|
23
|
+
*/
|
|
13
24
|
export interface FormRowProps {
|
|
14
25
|
readonly label: ReactNode;
|
|
15
26
|
readonly hint?: ReactNode | undefined;
|
|
@@ -18,6 +29,15 @@ export interface FormRowProps {
|
|
|
18
29
|
readonly children?: ReactNode | undefined;
|
|
19
30
|
}
|
|
20
31
|
|
|
32
|
+
/**
|
|
33
|
+
* 表单一行:左边标签、右边控件,提示与错误占控件下面那一格。
|
|
34
|
+
* @param props.label - 这一行的标签;进了这一行,控件自己就别再给 `label`,否则会多一个标签。
|
|
35
|
+
* @param props.hint - 控件下的说明;给了 `problem` 就不画它 —— 同一格不叠两段话。
|
|
36
|
+
* @param props.problem - 校验失败那句话;给了就顶掉 `hint` 并给整行加上出错标记。
|
|
37
|
+
* @param props.layout - `stacked` 才是标签在上、控件在下(默认 `inline`:标签在左)。
|
|
38
|
+
* @param props.children - 控件本体。
|
|
39
|
+
* @returns 一个 `<label>`。
|
|
40
|
+
*/
|
|
21
41
|
export function FormRow({ label, hint, problem, layout = 'inline', children }: FormRowProps) {
|
|
22
42
|
return (
|
|
23
43
|
<label
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ReactNode } from '../lib/react.js';
|
|
2
|
+
import type { FormAction } from './FormActions.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 一格控件的值:单值(`TextField` / `SelectField` / `ChoiceGroup` 单选),或一组值(`ChoiceGroup` 多选)。
|
|
6
|
+
*/
|
|
7
|
+
export type FormValue = string | readonly string[];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `Form` 递给控件那一格的绑定:`id` / `name` / `invalid` / `disabled` 由 `Form` 定,控件只负责接上。
|
|
11
|
+
*/
|
|
12
|
+
export interface FormFieldBinding {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
readonly invalid: boolean;
|
|
16
|
+
readonly disabled: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 一格字段的配置:键、标签、值、怎么算错(`validate`)、控件本身 —— 排布、时机、阻断都不写在这里,那些归 `Form`。
|
|
21
|
+
* `validate` 是纯函数、同步、只读 `values`:返回**真值**才算这一格错,假值(`undefined` / `null` / `false` / 空串)
|
|
22
|
+
* 一律当"没错"—— 所以 `(value) => value.trim() === '' && '必填'` 这种用 `&&` 算文案的写法是对的,
|
|
23
|
+
* 它的返回值就是错误行那句话。
|
|
24
|
+
*/
|
|
25
|
+
export interface FormField {
|
|
26
|
+
readonly key: string;
|
|
27
|
+
readonly label: ReactNode;
|
|
28
|
+
readonly hint?: ReactNode | undefined;
|
|
29
|
+
readonly layout?: 'inline' | 'stacked' | undefined;
|
|
30
|
+
readonly value: FormValue;
|
|
31
|
+
readonly visible?: boolean | undefined;
|
|
32
|
+
readonly disabled?: boolean | undefined;
|
|
33
|
+
readonly validate?: (value: FormValue, values: Readonly<Record<string, FormValue>>) => ReactNode | undefined;
|
|
34
|
+
readonly control: (binding: FormFieldBinding) => ReactNode;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 提交钮:文案归调用点,位置、禁用与忙态归 `Form`。
|
|
39
|
+
*/
|
|
40
|
+
export interface FormSubmit {
|
|
41
|
+
readonly label: ReactNode;
|
|
42
|
+
readonly busyLabel?: ReactNode | undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 取消钮:文案与点下去做什么,`Form` 把它排在提交钮左边。
|
|
47
|
+
*/
|
|
48
|
+
export interface FormCancel {
|
|
49
|
+
readonly label: ReactNode;
|
|
50
|
+
readonly onClick: () => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* `Form` 的契约:fields 配置进,排布、校验时机、提交阻断、操作区全归组件。
|
|
55
|
+
*/
|
|
56
|
+
export interface FormProps {
|
|
57
|
+
readonly fields: readonly FormField[];
|
|
58
|
+
readonly onSubmit: (values: Readonly<Record<string, FormValue>>) => void;
|
|
59
|
+
readonly submit: FormSubmit;
|
|
60
|
+
readonly cancel?: FormCancel | undefined;
|
|
61
|
+
readonly extra?: readonly FormAction[] | undefined;
|
|
62
|
+
readonly busy?: boolean | undefined;
|
|
63
|
+
readonly disabled?: boolean | undefined;
|
|
64
|
+
readonly problem?: ReactNode | undefined;
|
|
65
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import type { FormField, FormProps, FormValue } from './FormTypes.js';
|
|
3
|
+
import { Form } from './Form.js';
|
|
4
|
+
|
|
5
|
+
function field(key: string, value: FormValue, validate?: FormField['validate']): FormField {
|
|
6
|
+
return {
|
|
7
|
+
key,
|
|
8
|
+
label: key,
|
|
9
|
+
value,
|
|
10
|
+
control: (binding) => `${key}:${binding.id}:${String(binding.invalid)}:${String(binding.disabled)}`,
|
|
11
|
+
...(validate === undefined ? {} : { validate }),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
vi.mock('../lib/react.js', async () => {
|
|
16
|
+
const actual = await vi.importActual<Record<string, unknown>>('../lib/react.js');
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
useState: (initial: unknown) => [typeof initial === 'function' ? (initial as () => unknown)() : initial, () => {}],
|
|
20
|
+
useRef: (initial: unknown) => ({ current: initial ?? null }),
|
|
21
|
+
useEffect: () => {},
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('Form 的接线(hooks 换成假的,在 node 里当函数调)', () => {
|
|
26
|
+
function formEvent(): { preventDefault: () => void; prevented: () => boolean } {
|
|
27
|
+
let prevented = false;
|
|
28
|
+
return {
|
|
29
|
+
preventDefault: () => {
|
|
30
|
+
prevented = true;
|
|
31
|
+
},
|
|
32
|
+
prevented: () => prevented,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function submitHandlerOf(props: FormProps): (event: { preventDefault: () => void }) => void {
|
|
37
|
+
const node = Form(props) as unknown as { props: { onSubmit: (event: { preventDefault: () => void }) => void } };
|
|
38
|
+
return node.props.onSubmit;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function propsOfForm(onSubmit: (values: Readonly<Record<string, FormValue>>) => void, over: Partial<FormProps> = {}): FormProps {
|
|
42
|
+
return { fields: [field('env', 'prod')], onSubmit, submit: { label: '保存' }, ...over };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
it('★ 没错那一拍:preventDefault(不用浏览器默认提交)并调 onSubmit(values) 一次', () => {
|
|
46
|
+
const seen: (Readonly<Record<string, FormValue>>)[] = [];
|
|
47
|
+
const event = formEvent();
|
|
48
|
+
|
|
49
|
+
submitHandlerOf(propsOfForm((values) => void seen.push(values)))(event);
|
|
50
|
+
|
|
51
|
+
expect(event.prevented()).toBe(true);
|
|
52
|
+
expect(seen).toEqual([{ env: 'prod' }]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('★ 有错那一拍:不调 onSubmit', () => {
|
|
56
|
+
const onSubmit = vi.fn();
|
|
57
|
+
const fields = [field('project', '', () => '这个项目不存在')];
|
|
58
|
+
|
|
59
|
+
submitHandlerOf(propsOfForm(onSubmit, { fields }))({ preventDefault: () => {} });
|
|
60
|
+
|
|
61
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('★ busy 时 Enter / 点提交都不提交(隐式提交也走这道闸)', () => {
|
|
65
|
+
const onSubmit = vi.fn();
|
|
66
|
+
|
|
67
|
+
submitHandlerOf(propsOfForm(onSubmit, { busy: true }))({ preventDefault: () => {} });
|
|
68
|
+
|
|
69
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('★ disabled 时也不提交(没改动可存那颗钮禁着,回车同样进不来)', () => {
|
|
73
|
+
const onSubmit = vi.fn();
|
|
74
|
+
|
|
75
|
+
submitHandlerOf(propsOfForm(onSubmit, { disabled: true }))({ preventDefault: () => {} });
|
|
76
|
+
|
|
77
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('★ 不可见那格不进 values(过滤发生在提交那一拍)', () => {
|
|
81
|
+
const seen: (Readonly<Record<string, FormValue>>)[] = [];
|
|
82
|
+
const fields = [field('env', 'prod'), { ...field('secret', 'shh'), visible: false }];
|
|
83
|
+
|
|
84
|
+
submitHandlerOf(propsOfForm((values) => void seen.push(values), { fields }))({ preventDefault: () => {} });
|
|
85
|
+
|
|
86
|
+
expect(seen).toEqual([{ env: 'prod' }]);
|
|
87
|
+
});
|
|
88
|
+
});
|
package/src/components/Link.tsx
CHANGED
|
@@ -3,6 +3,9 @@ import { h } from '../lib/react.js';
|
|
|
3
3
|
import { IconRightUpOutlineRegular } from '@deepseek-ai/dsh-client-ui-primitives';
|
|
4
4
|
import './Link.css';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* `Link` 的契约:目标地址、文案与外链开关。
|
|
8
|
+
*/
|
|
6
9
|
export interface LinkProps {
|
|
7
10
|
readonly href: string;
|
|
8
11
|
readonly children?: ReactNode | undefined;
|
|
@@ -11,6 +14,15 @@ export interface LinkProps {
|
|
|
11
14
|
readonly className?: string | undefined;
|
|
12
15
|
}
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* 唯一的外链写法:自带新窗口、`rel="noopener noreferrer"`、正文色与行尾那颗小箭头。
|
|
19
|
+
* @param props.href - 目标地址,必给。
|
|
20
|
+
* @param props.children - 链接文字(别自己再带 `→`)。
|
|
21
|
+
* @param props.title - 原生 title 提示;不给就不写这个属性。
|
|
22
|
+
* @param props.external - 默认 true:新窗口打开并画那颗 ↗;站内跳转给 false,箭头也一起不画。
|
|
23
|
+
* @param props.className - 追加在基础类名之后,不是替换。
|
|
24
|
+
* @returns 一个 `<a>`。
|
|
25
|
+
*/
|
|
14
26
|
export function Link({ href, children, title, external = true, className }: LinkProps) {
|
|
15
27
|
return (
|
|
16
28
|
<a
|
|
@@ -10,11 +10,17 @@ import {
|
|
|
10
10
|
} from '@deepseek-ai/dsh-client-ui-primitives';
|
|
11
11
|
import './LongTaskRow.css';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* 长任务行尾的一个链接:地址与文字。
|
|
15
|
+
*/
|
|
13
16
|
export interface LongTaskRowLink {
|
|
14
17
|
readonly href: string;
|
|
15
18
|
readonly text: string;
|
|
16
19
|
}
|
|
17
20
|
|
|
21
|
+
/**
|
|
22
|
+
* `LongTaskRow` 的契约:状态、标题、问题、链接,与两处可展开的原始输出。
|
|
23
|
+
*/
|
|
18
24
|
export interface LongTaskRowProps {
|
|
19
25
|
readonly status: LongTaskStatus;
|
|
20
26
|
readonly headline: string;
|
|
@@ -36,6 +42,19 @@ function RowLink({ href, text }: LongTaskRowLink) {
|
|
|
36
42
|
);
|
|
37
43
|
}
|
|
38
44
|
|
|
45
|
+
/**
|
|
46
|
+
* 长任务那一行:状态点 + 标题 + 链接,跑失败时把步骤与原始输出收在两处可展开里。
|
|
47
|
+
* 发布、扫描、迁移这类跑得久的事都用它,别自己画进度。
|
|
48
|
+
* @param props.status - `longTaskDot` 认的四态之一,决定状态点颜色与要不要画"失败"那两块。
|
|
49
|
+
* @param props.headline - 这一行在干什么(一句话)。
|
|
50
|
+
* @param props.problem - 失败原因;不给不画。
|
|
51
|
+
* @param props.links - 行尾的链接,通常指向产物或日志。
|
|
52
|
+
* @param props.steps - 步骤文本;只在失败时画,且要同时给 `stepsTitle` 才成一块可展开。
|
|
53
|
+
* @param props.raw - 原始输出;只在失败时画,且要同时给 `rawTitle` 才成一块可展开。
|
|
54
|
+
* @param props.stepsTitle - 步骤那块展开行的标题;`rawTitle` 同理是原始输出那块。
|
|
55
|
+
* @param props.copyLabel - 复制钮文案:给了才画复制钮;`copiedLabel` 是复制成功后那句。
|
|
56
|
+
* @returns 一行长任务。
|
|
57
|
+
*/
|
|
39
58
|
export function LongTaskRow(props: LongTaskRowProps) {
|
|
40
59
|
const [stepsOpen, setStepsOpen] = useState(false);
|
|
41
60
|
const [rawOpen, setRawOpen] = useState(false);
|
package/src/components/Note.tsx
CHANGED
|
@@ -2,6 +2,9 @@ import type { ReactNode } from '../lib/react.js';
|
|
|
2
2
|
import { h } from '../lib/react.js';
|
|
3
3
|
import './Note.css';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* `Note` 的契约:语气、标签名与读屏角色。
|
|
7
|
+
*/
|
|
5
8
|
export interface NoteProps {
|
|
6
9
|
readonly tone?: 'muted' | 'problem';
|
|
7
10
|
readonly as?: 'p' | 'span';
|
|
@@ -9,6 +12,14 @@ export interface NoteProps {
|
|
|
9
12
|
readonly children?: ReactNode | undefined;
|
|
10
13
|
}
|
|
11
14
|
|
|
15
|
+
/**
|
|
16
|
+
* 一句话说明或一句报错:卡片里的提示条用它,不承载交互。
|
|
17
|
+
* @param props.tone - `problem` 是报错色;默认 `muted`,普通说明色。
|
|
18
|
+
* @param props.as - `span` 是跟在别的行里,`p`(默认)独占一段。
|
|
19
|
+
* @param props.role - 读屏角色:变化中的提示给 `status`、报错给 `alert`;不给不设。
|
|
20
|
+
* @param props.children - 那句话本身。
|
|
21
|
+
* @returns 一个 `span` 或 `p`。
|
|
22
|
+
*/
|
|
12
23
|
export function Note({ tone = 'muted', as, role, children }: NoteProps) {
|
|
13
24
|
const className = tone === 'problem' ? 'wb-problem' : 'wb-note';
|
|
14
25
|
return as === 'span' ? (
|
|
@@ -2,6 +2,11 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import { h } from '../lib/react.js';
|
|
3
3
|
import './ResourceStates.css';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* 骨架屏:读数期间已经知道大概形状时用它(连形状都不知道就用 `CenteredState` 那句"正在读")。
|
|
7
|
+
* @param props.rows - 画几行骨架(默认 4);整块 `aria-hidden`,不读给读屏。
|
|
8
|
+
* @returns 一叠占位条。
|
|
9
|
+
*/
|
|
5
10
|
export function SkeletonRows({ rows = 4 }: { rows?: number }) {
|
|
6
11
|
return (
|
|
7
12
|
<div className="wb-skeleton" aria-hidden="true">
|
|
@@ -15,6 +20,9 @@ export function SkeletonRows({ rows = 4 }: { rows?: number }) {
|
|
|
15
20
|
);
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
/**
|
|
24
|
+
* `CenteredState` 的契约:图标、标题、说明、明细与动作。
|
|
25
|
+
*/
|
|
18
26
|
export interface CenteredStateProps {
|
|
19
27
|
icon: ReactNode;
|
|
20
28
|
title: ReactNode;
|
|
@@ -24,6 +32,16 @@ export interface CenteredStateProps {
|
|
|
24
32
|
compact?: boolean;
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
/**
|
|
36
|
+
* 居中一屏:要他先选一个 / 正在读 / 读不到时用它占住卡片版面。
|
|
37
|
+
* @param props.icon - 标题上方那枚图标;给 `null` 就只留标题(加载与报错常见)。
|
|
38
|
+
* @param props.title - 正中那行大字。
|
|
39
|
+
* @param props.hint - 标题下的一行说明。
|
|
40
|
+
* @param props.detail - 更细的一行(如原始错误码与那句话);"读不到"时把错误放这里,绝不显示 0。
|
|
41
|
+
* @param props.action - 底部动作(如"重试");不给不画。
|
|
42
|
+
* @param props.compact - 紧凑档(默认 false),卡里空间小的时候给 true。
|
|
43
|
+
* @returns 一个 `role="status"` 的居中块。
|
|
44
|
+
*/
|
|
27
45
|
export function CenteredState({ icon, title, hint, detail, action, compact = false }: CenteredStateProps) {
|
|
28
46
|
return (
|
|
29
47
|
<div className={compact ? 'wb-centered wb-centeredCompact' : 'wb-centered'} role="status">
|
|
@@ -40,6 +58,13 @@ export function CenteredState({ icon, title, hint, detail, action, compact = fal
|
|
|
40
58
|
);
|
|
41
59
|
}
|
|
42
60
|
|
|
61
|
+
/**
|
|
62
|
+
* 就绪但空:它只是 `CenteredState` 的简写,标题就是那句空态文案。
|
|
63
|
+
* @param props.text - 空态那句话。
|
|
64
|
+
* @param props.icon - 标题上方的图标;默认 `null`,不画图标。
|
|
65
|
+
* @param props.compact - 紧凑档,默认 false。
|
|
66
|
+
* @returns 一个居中块。
|
|
67
|
+
*/
|
|
43
68
|
export function EmptyState({ text, icon = null, compact = false }: { text: string; icon?: ReactNode; compact?: boolean }) {
|
|
44
69
|
return <CenteredState icon={icon} title={text} compact={compact} />;
|
|
45
70
|
}
|
package/src/components/Rows.tsx
CHANGED
|
@@ -2,19 +2,36 @@ import type { ReactNode } from '../lib/react.js';
|
|
|
2
2
|
import { h } from '../lib/react.js';
|
|
3
3
|
import './Rows.css';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* `Rows` 的契约:一组键值行的容器。
|
|
7
|
+
*/
|
|
5
8
|
export interface RowsProps {
|
|
6
9
|
readonly children?: ReactNode | undefined;
|
|
7
10
|
}
|
|
8
11
|
|
|
12
|
+
/**
|
|
13
|
+
* 键值行的容器:分隔线、行距与两端对齐在它和 `Row` 里,别自己写 `ul` / `li` 拼。
|
|
14
|
+
* @param props.children - 一串 `Row`。
|
|
15
|
+
* @returns 竖排的 div。
|
|
16
|
+
*/
|
|
9
17
|
export function Rows({ children }: RowsProps) {
|
|
10
18
|
return <div className="wb-rows">{children}</div>;
|
|
11
19
|
}
|
|
12
20
|
|
|
21
|
+
/**
|
|
22
|
+
* `Row` 的契约:左边名字 + 右边值。
|
|
23
|
+
*/
|
|
13
24
|
export interface RowProps {
|
|
14
25
|
readonly label: ReactNode;
|
|
15
26
|
readonly value: ReactNode;
|
|
16
27
|
}
|
|
17
28
|
|
|
29
|
+
/**
|
|
30
|
+
* 一行键值:左边名字、右边值;要成对说明一项数据就用它,别自己写两栏。
|
|
31
|
+
* @param props.label - 左边那格(键)。
|
|
32
|
+
* @param props.value - 右边那格(值),可以是数字、时间或一小段节点。
|
|
33
|
+
* @returns 一行两格。
|
|
34
|
+
*/
|
|
18
35
|
export function Row({ label, value }: RowProps) {
|
|
19
36
|
return (
|
|
20
37
|
<div className="wb-row">
|
|
@@ -2,6 +2,9 @@ import { h } from '../lib/react.js';
|
|
|
2
2
|
import type { FormAction } from './FormActions.js';
|
|
3
3
|
import { FormActions } from './FormActions.js';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* `SaveBar` 的契约:忙碌态、三句文案与两个回调。
|
|
7
|
+
*/
|
|
5
8
|
export interface SaveBarProps {
|
|
6
9
|
readonly busy: boolean;
|
|
7
10
|
readonly disabled?: boolean | undefined;
|
|
@@ -14,6 +17,15 @@ export interface SaveBarProps {
|
|
|
14
17
|
readonly onSave: () => void;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
/**
|
|
21
|
+
* 设置面底下那条"取消 / 保存":就是 `FormActions` 的两颗固定动作,顺序与间距不归调用点。
|
|
22
|
+
* @param props.busy - 保存中:两颗钮一起禁用,保存那颗换成 `saving` 文案并带转圈。
|
|
23
|
+
* @param props.disabled - 再额外禁掉保存钮(如没改动可存);默认不禁,取消钮不受它管。
|
|
24
|
+
* @param props.labels - 三句文案:`cancel` / `save` / `saving`。
|
|
25
|
+
* @param props.onCancel - 点取消。
|
|
26
|
+
* @param props.onSave - 点保存。
|
|
27
|
+
* @returns 一组按钮。
|
|
28
|
+
*/
|
|
17
29
|
export function SaveBar({ busy, disabled, labels, onCancel, onSave }: SaveBarProps) {
|
|
18
30
|
const actions: readonly FormAction[] = [
|
|
19
31
|
{ key: 'cancel', label: labels.cancel, kind: 'default', onClick: onCancel, disabled: busy },
|