@morlay/dsh-client-ui-primitives 0.0.3 → 0.0.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/README.md +18 -266
- package/dist/client.cjs +11 -612
- package/dist/client.d.cts +33 -658
- package/dist/client.d.mts +33 -658
- package/dist/index.mjs +1 -1
- package/dist/{reference-Dxy9uQYz.mjs → reference-Cjcn21uW.mjs} +0 -1
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +18 -18
- package/src/client/index.ts +7 -9
- package/src/client/schema-form/Editor.tsx +25 -38
- package/src/client/schema-form/SchemaForm.tsx +6 -12
- package/src/client/schema-form/controller.ts +65 -123
- package/src/client/schema-form/draft.ts +55 -138
- package/src/client/schema-form/fields.tsx +5 -14
- package/src/client/schema-form/hints.ts +31 -131
- package/src/client/schema-form/index.ts +15 -20
- package/src/client/schema-form/labels.ts +7 -9
- package/src/client/schema-form/lines.ts +36 -60
- package/src/client/schema-form/locales.ts +6 -8
- package/src/client/schema-form/rows.ts +22 -41
- package/src/client/schema-form/schema-node.ts +87 -150
- package/src/client/schema-form/slot-contract.ts +55 -76
- package/src/client/schema-form/styles.ts +39 -51
- package/src/client/schema-form/testing/fake-describe.ts +3 -9
- package/src/client/schema-form/testing/fake-scope.ts +8 -10
- package/src/client/schema-form/value.tsx +14 -26
- package/src/client/settings-form/SettingsForm.tsx +13 -17
- package/src/client/settings-form/fields.tsx +25 -35
- package/src/client/settings-form/form-model.ts +63 -145
- package/src/client/styling/styled.tsx +3 -5
- package/src/client/theme.generated.ts +18 -7
- package/src/reference-markdown.tsx +1 -1
- package/src/reference.ts +1 -1
|
@@ -1,27 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* priority 0 注册,按 slots 的 cell winner 规则自然遮住这里的自动项,与加载顺序无关。
|
|
6
|
-
*/
|
|
1
|
+
// 自动注册面:把 describe 出来的命名空间与 bundle 的行清单配对,为每个能渲染的行注册本行的配置入口。
|
|
2
|
+
//
|
|
3
|
+
// 注册用 `<bundle 包名>#<行 id>`(页面按同一个 key 打开这一行的配置页)与兜底 priority:手写卡片用默认
|
|
4
|
+
// priority 0 注册,按 slots 的 cell winner 规则自然遮住这里的自动项,与加载顺序无关。
|
|
7
5
|
|
|
8
6
|
import type { BundleInfo, SettingsNamespaceView } from "@deepseek-ai/dsh-api-remotes/client";
|
|
9
7
|
import type { SettingsDescribeFace } from "@deepseek-ai/dsh-client-ui-settings/client";
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
// 一个该自动长出配置页的行。
|
|
12
10
|
export interface RowRegistrationPlan {
|
|
13
|
-
|
|
11
|
+
// `plugins.row.config` 的 key:`<bundle 包名>#<行 id>`。
|
|
14
12
|
key: string;
|
|
15
|
-
|
|
13
|
+
// settings 命名空间(行 id)。
|
|
16
14
|
ns: string;
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
* 配对命名空间与 bundle 行清单。
|
|
21
|
-
* @param namespaces - host 发来的命名空间视图。
|
|
22
|
-
* @param bundles - host 的 bundle 清单(含各 bundle 声明的行 id)。
|
|
23
|
-
* @returns 该自动注册的行,按命名空间视图的顺序。
|
|
24
|
-
*/
|
|
17
|
+
// 配对命名空间与 bundle 行清单:只给 `autoGenerate` 且能在清单里找到 owner 的行,顺序即命名空间视图的顺序。
|
|
25
18
|
export function rowRegistrations(
|
|
26
19
|
namespaces: readonly SettingsNamespaceView[],
|
|
27
20
|
bundles: readonly BundleInfo[],
|
|
@@ -40,9 +33,9 @@ export function rowRegistrations(
|
|
|
40
33
|
return plans;
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
|
|
36
|
+
// 注册器要的外部面。
|
|
44
37
|
export interface RowRegistrationDeps {
|
|
45
|
-
|
|
38
|
+
// 槽位注册:返回注销函数。
|
|
46
39
|
slots: {
|
|
47
40
|
register(
|
|
48
41
|
options: {
|
|
@@ -54,48 +47,36 @@ export interface RowRegistrationDeps {
|
|
|
54
47
|
component: unknown,
|
|
55
48
|
): () => void;
|
|
56
49
|
};
|
|
57
|
-
|
|
50
|
+
// describe 读面:命名空间清单与变更通知。
|
|
58
51
|
describe: SettingsDescribeFace;
|
|
59
|
-
|
|
52
|
+
// 读 bundle 清单。
|
|
60
53
|
bundles(): Promise<readonly BundleInfo[]>;
|
|
61
|
-
|
|
54
|
+
// 订阅 bundle 清单变化。
|
|
62
55
|
subscribeBundles(listener: () => void): () => void;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
* 每次调用返回同一个实例(控制器按命名空间缓存)。
|
|
66
|
-
*/
|
|
56
|
+
// 一行的注册项组件;`undefined` 表示这一行渲染不了(schema rehydrate 不了或段根不是对象)。
|
|
57
|
+
// 每次调用返回同一个实例(控制器按命名空间缓存)。
|
|
67
58
|
entryFor(ns: string): { component: unknown } | undefined;
|
|
68
|
-
|
|
59
|
+
// 槽项文案的命名空间。
|
|
69
60
|
locale: string;
|
|
70
|
-
|
|
61
|
+
// 一行注销时释放它的控制器。
|
|
71
62
|
release(ns: string): void;
|
|
72
63
|
}
|
|
73
64
|
|
|
74
|
-
|
|
65
|
+
// 自动项的兜底优先级:手写卡片(默认 0)遮住它。
|
|
75
66
|
const FALLBACK_PRIORITY = 100;
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
* 自动注册的差分器:按 describe 与 bundle 清单把「该有的行」与「已在的行」对齐。
|
|
79
|
-
*/
|
|
68
|
+
// 自动注册的差分器:按 describe 与 bundle 清单把「该有的行」与「已在的行」对齐。
|
|
80
69
|
export class RowRegistration {
|
|
81
70
|
readonly #deps: RowRegistrationDeps;
|
|
82
71
|
readonly #live = new Map<string, () => void>();
|
|
83
72
|
#pending: Promise<void> | undefined;
|
|
84
73
|
#disposed = false;
|
|
85
74
|
|
|
86
|
-
/**
|
|
87
|
-
* @param deps - 槽位、清单读面与每行的槽项工厂。
|
|
88
|
-
*/
|
|
89
75
|
constructor(deps: RowRegistrationDeps) {
|
|
90
76
|
this.#deps = deps;
|
|
91
77
|
}
|
|
92
78
|
|
|
93
|
-
|
|
94
|
-
* 对齐一次:新增该有的行,注销不该有的行。
|
|
95
|
-
*
|
|
96
|
-
* 并发调用折叠成一次(bundle 清单只读一趟),后到的调用等同一趟结束。
|
|
97
|
-
* @returns 对齐完成。
|
|
98
|
-
*/
|
|
79
|
+
// 对齐一次:新增该有的行,注销不该有的行;并发调用折叠成一次(bundle 清单只读一趟)。
|
|
99
80
|
sync(): Promise<void> {
|
|
100
81
|
if (this.#disposed) return Promise.resolve();
|
|
101
82
|
this.#pending ??= this.#run().finally(() => {
|
|
@@ -104,7 +85,7 @@ export class RowRegistration {
|
|
|
104
85
|
return this.#pending;
|
|
105
86
|
}
|
|
106
87
|
|
|
107
|
-
|
|
88
|
+
// 收掉自己:注销还活着的全部注册并释放它们的控制器。
|
|
108
89
|
dispose(): void {
|
|
109
90
|
this.#disposed = true;
|
|
110
91
|
this.#release();
|
|
@@ -142,7 +123,7 @@ export class RowRegistration {
|
|
|
142
123
|
}
|
|
143
124
|
}
|
|
144
125
|
|
|
145
|
-
|
|
126
|
+
// 注销全部并释放控制器。
|
|
146
127
|
#release(): void {
|
|
147
128
|
for (const [ns, dispose] of this.#live) {
|
|
148
129
|
dispose();
|
|
@@ -1,103 +1,91 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* 文案在 `labels.ts`,控件在 `fields/`。动态子节点(dict 的键与值、array 的项)的路径段用
|
|
6
|
-
* {@link DYNAMIC_SEGMENT} 占位,渲染层按真实键/索引替换后再交给字段槽。
|
|
7
|
-
*/
|
|
1
|
+
// schema 投影:把 host 发来的 volatile 表单 schema 变成渲染层要的字段树。
|
|
2
|
+
//
|
|
3
|
+
// 只描述结构(字段、路径、meta、只读原因),不读值、不做本地化、不碰 React;动态子节点的路径段用
|
|
4
|
+
// `DYNAMIC_SEGMENT` 占位,渲染层按真实键 / 索引替换后再交给字段槽。
|
|
8
5
|
|
|
9
6
|
import type { SchemaNode } from "@deepseek-ai/dsh-client-ui-settings/client";
|
|
10
7
|
|
|
11
|
-
|
|
8
|
+
// 动态子节点的路径占位段(dict 的键与值、array 的项):渲染层按真实键或索引替换。
|
|
12
9
|
export const DYNAMIC_SEGMENT = "*";
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
// 节点不可编辑的原因;`null` 表示可编辑。
|
|
15
12
|
export type ReadOnlyReason = "transform" | "function" | "constructor";
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
// 归一化后的字段元数据(schemastery `meta` 的可用子集)。
|
|
18
15
|
export interface FieldMeta {
|
|
19
|
-
|
|
16
|
+
// `description`(可能是本地化字典):渲染时经 locale 解析。
|
|
20
17
|
description: string | Readonly<Record<string, string>> | undefined;
|
|
21
|
-
|
|
18
|
+
// `comment`:纯文本补充说明。
|
|
22
19
|
comment: string | undefined;
|
|
23
|
-
|
|
20
|
+
// `link`:外部文档。
|
|
24
21
|
link: string | undefined;
|
|
25
|
-
|
|
22
|
+
// `role`:控件提示(`secret` / `slider` / `datetime` …)。
|
|
26
23
|
role: string | undefined;
|
|
27
|
-
|
|
24
|
+
// `role === 'secret'`:值不回传,只能写。
|
|
28
25
|
secret: boolean;
|
|
29
|
-
|
|
26
|
+
// `required()`。
|
|
30
27
|
required: boolean;
|
|
31
|
-
|
|
28
|
+
// `disabled()`:控件禁用。
|
|
32
29
|
disabled: boolean;
|
|
33
|
-
|
|
30
|
+
// `collapse()`:嵌套分组默认折叠。
|
|
34
31
|
collapse: boolean;
|
|
35
|
-
|
|
32
|
+
// `deprecated()` / `experimental()` 的徽标文本。
|
|
36
33
|
badges: readonly string[];
|
|
37
|
-
|
|
34
|
+
// `pattern()` 的正则来源。
|
|
38
35
|
pattern: string | undefined;
|
|
39
|
-
|
|
36
|
+
// `min()`。
|
|
40
37
|
min: number | undefined;
|
|
41
|
-
|
|
38
|
+
// `max()`。
|
|
42
39
|
max: number | undefined;
|
|
43
|
-
|
|
40
|
+
// `step()`:`1` 表示整数。
|
|
44
41
|
step: number | undefined;
|
|
45
|
-
|
|
42
|
+
// 是否声明了默认值(与「默认值是 undefined」区分开)。
|
|
46
43
|
hasDefault: boolean;
|
|
47
|
-
|
|
44
|
+
// 声明的默认值。
|
|
48
45
|
defaultValue: unknown;
|
|
49
|
-
|
|
46
|
+
// `extra()`:角色附带的任意数据,字段槽的自定义控件可用。
|
|
50
47
|
extra: unknown;
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
// 每种节点共有的部分。
|
|
54
51
|
export interface FieldNodeBase {
|
|
55
|
-
|
|
52
|
+
// 这一层的名字:对象字段名、tuple 的索引、动态子节点的占位段。
|
|
56
53
|
key: string;
|
|
57
|
-
|
|
54
|
+
// 从段根起的路径;动态段是 `DYNAMIC_SEGMENT`。
|
|
58
55
|
path: readonly string[];
|
|
59
|
-
|
|
60
|
-
* 按值展开时算好的显示名:数组成员的**项身份**(见 {@link FieldNodeBase} 所在的 array 节点)。
|
|
61
|
-
* 投影时没有(`undefined`),渲染层用它优先于字段名。
|
|
62
|
-
*/
|
|
56
|
+
// 按值展开时算好的显示名(数组成员的项身份):投影时为 `undefined`,渲染层用它优先于字段名。
|
|
63
57
|
label: string | undefined;
|
|
64
|
-
|
|
58
|
+
// 归一化后的元数据。
|
|
65
59
|
meta: FieldMeta;
|
|
66
|
-
|
|
60
|
+
// 不可编辑的原因;`null` 表示可编辑。
|
|
67
61
|
readOnly: ReadOnlyReason | null;
|
|
68
62
|
}
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
// 位集的一位。
|
|
71
65
|
export interface BitsEntry {
|
|
72
66
|
name: string;
|
|
73
67
|
bit: number;
|
|
74
68
|
}
|
|
75
69
|
|
|
76
|
-
|
|
70
|
+
// 渲染层要的一个字段节点。
|
|
77
71
|
export type FieldNode =
|
|
78
72
|
| (FieldNodeBase & { type: "object"; fields: readonly FieldNode[] })
|
|
79
73
|
| (FieldNodeBase & { type: "dict"; keyNode: FieldNode; valueNode: FieldNode })
|
|
80
74
|
| (FieldNodeBase & {
|
|
81
75
|
type: "array";
|
|
82
76
|
item: FieldNode;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
* (`z.array(inner).role('items', { mergeKey: 'id' })`),否则 item 上有 `id` 字段就用 `id`。
|
|
86
|
-
* 它只决定显示名与「同名即冲突」的判据——写盘仍按索引(host 的 path op 就是索引语义)。
|
|
87
|
-
*/
|
|
77
|
+
// 项的**身份键**:显式声明优先(`z.array(inner).role('items', { mergeKey: 'id' })`),否则 item 上有 `id`
|
|
78
|
+
// 字段就用 `id`。只决定显示名与「同名即冲突」的判据——写盘仍按索引。
|
|
88
79
|
mergeKey: string | undefined;
|
|
89
80
|
})
|
|
90
81
|
| (FieldNodeBase & { type: "tuple"; items: readonly FieldNode[] })
|
|
91
82
|
| (FieldNodeBase & {
|
|
92
83
|
type: "union";
|
|
93
|
-
|
|
84
|
+
// 成员全是字面量时的可选项;否则 `undefined`。
|
|
94
85
|
choices: readonly unknown[] | undefined;
|
|
95
86
|
variants: readonly FieldNode[];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* (`z.union([...]).role('union', { tag: 'kind' })`),否则取所有对象成员共有的**唯一**常量字段。
|
|
99
|
-
* schemastery 本身没有 tag 机制(union 按声明顺序逐个试),所以这只是渲染与校验的选支依据。
|
|
100
|
-
*/
|
|
87
|
+
// 判别标签:按哪个字段的值挑变体。显式声明优先(`z.union([...]).role('union', { tag: 'kind' })`),
|
|
88
|
+
// 否则取所有对象成员共有的**唯一**常量字段——schemastery 没有 tag 机制,这只是渲染与校验的选支依据。
|
|
101
89
|
tagKey: string | undefined;
|
|
102
90
|
})
|
|
103
91
|
| (FieldNodeBase & { type: "intersect"; members: readonly FieldNode[] })
|
|
@@ -105,10 +93,10 @@ export type FieldNode =
|
|
|
105
93
|
| (FieldNodeBase & { type: "const"; value: unknown })
|
|
106
94
|
| (FieldNodeBase & { type: "lazy"; inner: FieldNode })
|
|
107
95
|
| (FieldNodeBase & { type: "string" | "number" | "boolean" | "any" | "never" | "function" })
|
|
108
|
-
|
|
96
|
+
// 自引用 schema 回到祖先处:值这一层以下不再描述,由渲染层展开或只读呈现。
|
|
109
97
|
| (FieldNodeBase & { type: "recursive"; origin: string });
|
|
110
98
|
|
|
111
|
-
|
|
99
|
+
// 空的元数据:动态子节点(无声明 schema 的 dict 键)与空字段树用它。
|
|
112
100
|
export function emptyMeta(): FieldMeta {
|
|
113
101
|
return {
|
|
114
102
|
description: undefined,
|
|
@@ -130,12 +118,12 @@ export function emptyMeta(): FieldMeta {
|
|
|
130
118
|
};
|
|
131
119
|
}
|
|
132
120
|
|
|
133
|
-
|
|
121
|
+
// `badges` 只留文本:配色与文案由字典决定。
|
|
134
122
|
function badgesOf(meta: SchemaNode["meta"]): string[] {
|
|
135
123
|
return (meta.badges ?? []).map((badge) => badge.text);
|
|
136
124
|
}
|
|
137
125
|
|
|
138
|
-
|
|
126
|
+
// 归一化一个节点的 meta。
|
|
139
127
|
function metaOf(schema: SchemaNode): FieldMeta {
|
|
140
128
|
const meta = schema.meta ?? {};
|
|
141
129
|
return {
|
|
@@ -159,7 +147,7 @@ function metaOf(schema: SchemaNode): FieldMeta {
|
|
|
159
147
|
};
|
|
160
148
|
}
|
|
161
149
|
|
|
162
|
-
|
|
150
|
+
// 外层 meta 与内层 meta 合并:外层的 undefined 不覆盖内层(transform 的 inner 带着自己的角色)。
|
|
163
151
|
function mergeMeta(inner: FieldMeta, outer: FieldMeta): FieldMeta {
|
|
164
152
|
const merged: Record<string, unknown> = { ...inner };
|
|
165
153
|
for (const [key, value] of Object.entries(outer)) {
|
|
@@ -168,17 +156,17 @@ function mergeMeta(inner: FieldMeta, outer: FieldMeta): FieldMeta {
|
|
|
168
156
|
return merged as unknown as FieldMeta;
|
|
169
157
|
}
|
|
170
158
|
|
|
171
|
-
|
|
159
|
+
// 节点自己的路径末段:动态段保持占位,根节点为空串。
|
|
172
160
|
function keyOf(path: readonly string[]): string {
|
|
173
161
|
return path[path.length - 1] ?? "";
|
|
174
162
|
}
|
|
175
163
|
|
|
176
|
-
|
|
164
|
+
// 一个纯字符串叶子(dict 没有声明键 schema 时用它)。
|
|
177
165
|
function stringLeaf(path: readonly string[], meta: FieldMeta): FieldNode {
|
|
178
166
|
return { key: keyOf(path), path, label: undefined, meta, readOnly: null, type: "string" };
|
|
179
167
|
}
|
|
180
168
|
|
|
181
|
-
|
|
169
|
+
// 把 schema 的类型名收口成渲染层认识的那几种。
|
|
182
170
|
function leafTypeOf(type: string): "string" | "number" | "boolean" | "any" | "never" | "function" {
|
|
183
171
|
switch (type) {
|
|
184
172
|
case "string":
|
|
@@ -196,12 +184,7 @@ function leafTypeOf(type: string): "string" | "number" | "boolean" | "any" | "ne
|
|
|
196
184
|
}
|
|
197
185
|
}
|
|
198
186
|
|
|
199
|
-
|
|
200
|
-
* 投影一个 schema 节点。
|
|
201
|
-
* @param schema - rehydrate 之后的 volatile 表单 schema(或它的子节点)。
|
|
202
|
-
* @param path - 该节点相对段根的路径;动态子节点由调用方给占位段。
|
|
203
|
-
* @returns 渲染层要的字段树。
|
|
204
|
-
*/
|
|
187
|
+
// 投影一个 schema 节点:`path` 是相对段根的路径,动态子节点由调用方给占位段。
|
|
205
188
|
export function projectNode(schema: SchemaNode, path: readonly string[] = []): FieldNode {
|
|
206
189
|
return project(schema, path, []);
|
|
207
190
|
}
|
|
@@ -303,14 +286,14 @@ function project(
|
|
|
303
286
|
}
|
|
304
287
|
}
|
|
305
288
|
|
|
306
|
-
|
|
289
|
+
// 展开 `lazy` 包装:投影里 lazy 只是一层,值仍在同一路径上。
|
|
307
290
|
export function unwrapLazy(node: FieldNode | undefined): FieldNode | undefined {
|
|
308
291
|
let current = node;
|
|
309
292
|
while (current?.type === "lazy") current = current.inner;
|
|
310
293
|
return current;
|
|
311
294
|
}
|
|
312
295
|
|
|
313
|
-
|
|
296
|
+
// 某一层可见的字段:object 直接给;union / intersect 把各变体的对象字段并起来。
|
|
314
297
|
export function fieldsOf(node: FieldNode | undefined): readonly FieldNode[] {
|
|
315
298
|
const current = unwrapLazy(node);
|
|
316
299
|
switch (current?.type) {
|
|
@@ -325,12 +308,7 @@ export function fieldsOf(node: FieldNode | undefined): readonly FieldNode[] {
|
|
|
325
308
|
}
|
|
326
309
|
}
|
|
327
310
|
|
|
328
|
-
|
|
329
|
-
* 按具体路径找回投影节点(动态段匹配真实键或索引)。
|
|
330
|
-
* @param root - 段根的字段树。
|
|
331
|
-
* @param path - 具体路径。
|
|
332
|
-
* @returns 命中的节点,或 `undefined`。
|
|
333
|
-
*/
|
|
311
|
+
// 按具体路径找回投影节点(动态段匹配真实键或索引)。
|
|
334
312
|
export function seekNode(root: FieldNode, path: readonly string[]): FieldNode | undefined {
|
|
335
313
|
let current: FieldNode | undefined = root;
|
|
336
314
|
for (const segment of path) {
|
|
@@ -358,14 +336,12 @@ export function seekNode(root: FieldNode, path: readonly string[]): FieldNode |
|
|
|
358
336
|
return unwrapLazy(current);
|
|
359
337
|
}
|
|
360
338
|
|
|
361
|
-
|
|
339
|
+
// 按当前值展开出的一个字段:路径与名字已按真实键与索引替换。
|
|
362
340
|
export interface WalkedField {
|
|
363
341
|
path: readonly string[];
|
|
364
342
|
node: FieldNode;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
* 项的位置。渲染层用它画成员行(名字 + 移除);成员的子字段不带这个标记。
|
|
368
|
-
*/
|
|
343
|
+
// 这一行是某个动态容器的**成员本身**(dict 的键 / array 的项)时给出它的身份:容器路径、键、以及数组
|
|
344
|
+
// 项的位置。渲染层用它画成员行(名字 + 移除);成员的子字段不带这个标记。
|
|
369
345
|
member?: {
|
|
370
346
|
parent: readonly string[];
|
|
371
347
|
key: string;
|
|
@@ -373,26 +349,16 @@ export interface WalkedField {
|
|
|
373
349
|
};
|
|
374
350
|
}
|
|
375
351
|
|
|
376
|
-
|
|
352
|
+
// 候选键的来源(业务注册;见 `hints.ts`):dict 的「可以加哪些键」。
|
|
377
353
|
export interface SuggestedKeys {
|
|
378
|
-
|
|
379
|
-
* 读某个 dict 字段的候选键。
|
|
380
|
-
* @param path - 该字段的具体路径。
|
|
381
|
-
* @returns 候选键;没有就是空数组。
|
|
382
|
-
*/
|
|
354
|
+
// 读某个 dict 字段的候选键;没有就是空数组。
|
|
383
355
|
keysFor(path: readonly string[]): readonly string[];
|
|
384
356
|
}
|
|
385
357
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
* 按值出现——值里没有就不占行,它在这一层的「添加属性」候选里,选中才加。数组按索引、字典按键展开;字面量
|
|
391
|
-
* union 是叶子控件,对象 union 只展开与当前值匹配的那个变体,intersect 的各成员在同一层展开。
|
|
392
|
-
* @param root - 段根的字段树。
|
|
393
|
-
* @param value - 生效值(或草稿后的值)。
|
|
394
|
-
* @returns 渲染顺序的字段列表。
|
|
395
|
-
*/
|
|
358
|
+
// 按当前值把字段树展开成渲染顺序的扁平列表(前序):容器在前、子项紧随其后。
|
|
359
|
+
//
|
|
360
|
+
// 出现的**只有值里有的东西,加上必填的**:非必填字段值里没有就不占行,它在这一层的「添加属性」候选里。
|
|
361
|
+
// 数组按索引、字典按键展开;字面量 union 是叶子控件,对象 union 只展开与当前值匹配的那个变体。
|
|
396
362
|
export function walkFields(root: FieldNode, value: unknown): WalkedField[] {
|
|
397
363
|
const out: WalkedField[] = [];
|
|
398
364
|
visit(root, root.path, value, out, true, undefined, new Set(), undefined);
|
|
@@ -491,7 +457,7 @@ function visit(
|
|
|
491
457
|
}
|
|
492
458
|
}
|
|
493
459
|
|
|
494
|
-
|
|
460
|
+
// 数组的项身份:显式声明优先,否则 item 上的 `id` 字段。
|
|
495
461
|
function mergeKeyOf(schema: SchemaNode, item: FieldNode): string | undefined {
|
|
496
462
|
const declared = declaredExtra(schema, "mergeKey");
|
|
497
463
|
if (declared !== undefined) return declared;
|
|
@@ -501,7 +467,7 @@ function mergeKeyOf(schema: SchemaNode, item: FieldNode): string | undefined {
|
|
|
501
467
|
: undefined;
|
|
502
468
|
}
|
|
503
469
|
|
|
504
|
-
|
|
470
|
+
// union 的判别标签:显式声明优先,否则所有对象成员共有的唯一常量字段。
|
|
505
471
|
function tagKeyOf(schema: SchemaNode, variants: readonly FieldNode[]): string | undefined {
|
|
506
472
|
const declared = declaredExtra(schema, "tag");
|
|
507
473
|
if (declared !== undefined) return declared;
|
|
@@ -520,7 +486,7 @@ function tagKeyOf(schema: SchemaNode, variants: readonly FieldNode[]): string |
|
|
|
520
486
|
return candidates.length === 1 ? candidates[0] : undefined;
|
|
521
487
|
}
|
|
522
488
|
|
|
523
|
-
|
|
489
|
+
// `role(text, { … })` 里声明的字符串约定(项身份 `mergeKey`、判别标签 `tag`)。
|
|
524
490
|
function declaredExtra(schema: SchemaNode, name: string): string | undefined {
|
|
525
491
|
const extra = schema.meta?.extra;
|
|
526
492
|
if (typeof extra !== "object" || extra === null) return undefined;
|
|
@@ -528,7 +494,7 @@ function declaredExtra(schema: SchemaNode, name: string): string | undefined {
|
|
|
528
494
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
529
495
|
}
|
|
530
496
|
|
|
531
|
-
|
|
497
|
+
// 一项的项身份值(渲染与冲突检查共用的读法)。
|
|
532
498
|
function memberLabel(value: unknown, mergeKey: string): string | undefined {
|
|
533
499
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
534
500
|
const member: unknown = Reflect.get(value, mergeKey);
|
|
@@ -537,27 +503,20 @@ function memberLabel(value: unknown, mergeKey: string): string | undefined {
|
|
|
537
503
|
return undefined;
|
|
538
504
|
}
|
|
539
505
|
|
|
540
|
-
|
|
506
|
+
// 项身份的一处冲突。
|
|
541
507
|
export interface MergeKeyConflict {
|
|
542
|
-
|
|
508
|
+
// `duplicate`:两个成员同名;`missing`:成员没有名字。
|
|
543
509
|
kind: "duplicate" | "missing";
|
|
544
|
-
|
|
510
|
+
// 冲突成员的具体路径。
|
|
545
511
|
path: readonly string[];
|
|
546
|
-
|
|
512
|
+
// 判据用的字段名。
|
|
547
513
|
mergeKey: string;
|
|
548
|
-
|
|
514
|
+
// 那个名字(缺名字时是空串)。
|
|
549
515
|
value: string;
|
|
550
516
|
}
|
|
551
517
|
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
*
|
|
555
|
-
* 名字是成员的「身份」,写盘仍按索引——所以这只是保存前的本地校验:重名的数组在页面上是非法草稿,
|
|
556
|
-
* 而不是悄悄写到 host 上。
|
|
557
|
-
* @param root - 段根的字段树。
|
|
558
|
-
* @param value - 草稿后的段值。
|
|
559
|
-
* @returns 第一处冲突,或 `undefined`。
|
|
560
|
-
*/
|
|
518
|
+
// 项身份冲突:同一层里两个成员同名,或某个成员没有名字。名字是成员的「身份」而写盘仍按索引,
|
|
519
|
+
// 所以这是保存前的本地校验——重名的数组在页面上是非法草稿,不会悄悄写到 host 上。
|
|
561
520
|
export function mergeKeyConflict(root: FieldNode, value: unknown): MergeKeyConflict | undefined {
|
|
562
521
|
return scanConflict(root, root.path, value);
|
|
563
522
|
}
|
|
@@ -627,14 +586,14 @@ function scanConflict(
|
|
|
627
586
|
}
|
|
628
587
|
}
|
|
629
588
|
|
|
630
|
-
|
|
589
|
+
// 一个容器值里的成员(对象键或数组索引)。
|
|
631
590
|
function entriesOf(value: unknown): [string, unknown][] {
|
|
632
591
|
if (Array.isArray(value)) return value.map((item, index) => [String(index), item]);
|
|
633
592
|
if (typeof value === "object" && value !== null) return Object.entries(value);
|
|
634
593
|
return [];
|
|
635
594
|
}
|
|
636
595
|
|
|
637
|
-
|
|
596
|
+
// 读一个成员的值。
|
|
638
597
|
function memberOf(value: unknown, key: string | number): unknown {
|
|
639
598
|
if (Array.isArray(value)) return value[Number(key)];
|
|
640
599
|
if (typeof value === "object" && value !== null)
|
|
@@ -642,33 +601,28 @@ function memberOf(value: unknown, key: string | number): unknown {
|
|
|
642
601
|
return undefined;
|
|
643
602
|
}
|
|
644
603
|
|
|
645
|
-
|
|
604
|
+
// union 的变体切换:一个选项要写的值。
|
|
646
605
|
export interface VariantChoice {
|
|
647
|
-
|
|
606
|
+
// 页面上显示的名字:判别标签的值,或变体的形状。
|
|
648
607
|
label: string;
|
|
649
|
-
|
|
608
|
+
// 写这个值就等于选中这一支。
|
|
650
609
|
value: unknown;
|
|
651
610
|
}
|
|
652
611
|
|
|
653
|
-
|
|
612
|
+
// 把节点收成 union:渲染层判形状与选支都从这里进。
|
|
654
613
|
export function unionOf(node: FieldNode | undefined): (FieldNode & { type: "union" }) | undefined {
|
|
655
614
|
const current = unwrapLazy(node);
|
|
656
615
|
return current?.type === "union" ? current : undefined;
|
|
657
616
|
}
|
|
658
617
|
|
|
659
|
-
|
|
660
|
-
* 当前值落在哪一支:与 {@link walkFields} 的展开同一判据(标签值优先,其次值的形状,最后第一支)。
|
|
661
|
-
* @param node - 字段节点(不是 union 就原样返回)。
|
|
662
|
-
* @param value - 这一层的值。
|
|
663
|
-
* @returns 选中变体的节点。
|
|
664
|
-
*/
|
|
618
|
+
// 当前值落在哪一支:与 `walkFields` 的展开同一判据(标签值优先,其次值的形状,最后第一支)。
|
|
665
619
|
export function variantOf(node: FieldNode, value: unknown): FieldNode {
|
|
666
620
|
const union = unionOf(node);
|
|
667
621
|
if (union === undefined) return unwrapLazy(node) ?? node;
|
|
668
622
|
return pickVariant(union, value) ?? union;
|
|
669
623
|
}
|
|
670
624
|
|
|
671
|
-
|
|
625
|
+
// 变体里那个判别标签字段的常量值(不是 `const` 就没有)。
|
|
672
626
|
function tagValueOf(variant: FieldNode, tagKey: string): { found: boolean; value: unknown } {
|
|
673
627
|
const current = unwrapLazy(variant);
|
|
674
628
|
if (current?.type !== "object" && current?.type !== "intersect") {
|
|
@@ -682,7 +636,7 @@ function tagValueOf(variant: FieldNode, tagKey: string): { found: boolean; value
|
|
|
682
636
|
return { found: false, value: undefined };
|
|
683
637
|
}
|
|
684
638
|
|
|
685
|
-
|
|
639
|
+
// 一个变体在页面上的名字:能看出"这一支是什么",而不是内部类型名。
|
|
686
640
|
function variantLabel(variant: FieldNode, tag: { found: boolean; value: unknown }): string {
|
|
687
641
|
if (tag.found) return JSON.stringify(tag.value);
|
|
688
642
|
switch (unwrapLazy(variant)?.type) {
|
|
@@ -704,14 +658,8 @@ function variantLabel(variant: FieldNode, tag: { found: boolean; value: unknown
|
|
|
704
658
|
}
|
|
705
659
|
}
|
|
706
660
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
*
|
|
710
|
-
* 带判别标签的 union 写**标签值**(`session-rdb` 的 `type: sqlite` ↔ `type: postgres`);没有标签的写目标变体的
|
|
711
|
-
* 空值(`access` 在「一段文本」与「一组文本」之间切)。
|
|
712
|
-
* @param union - 投影出来的 union 节点。
|
|
713
|
-
* @returns 每个变体一项,顺序即声明顺序。
|
|
714
|
-
*/
|
|
661
|
+
// 变体切换的选项:带判别标签的 union 写**标签值**(`session-rdb` 的 `type: sqlite` ↔ `type: postgres`),
|
|
662
|
+
// 没有标签的写目标变体的空值(`access` 在「一段文本」与「一组文本」之间切)。
|
|
715
663
|
export function variantChoices(union: FieldNode & { type: "union" }): readonly VariantChoice[] {
|
|
716
664
|
return union.variants.map((variant) => {
|
|
717
665
|
const tag =
|
|
@@ -725,7 +673,7 @@ export function variantChoices(union: FieldNode & { type: "union" }): readonly V
|
|
|
725
673
|
});
|
|
726
674
|
}
|
|
727
675
|
|
|
728
|
-
|
|
676
|
+
// 按路径读一个值:对象键与数组下标都认(值的读法只有这一处)。
|
|
729
677
|
export function readPath(root: unknown, path: readonly string[]): unknown {
|
|
730
678
|
return path.reduce<unknown>((node, segment) => {
|
|
731
679
|
if (Array.isArray(node)) return node[Number(segment)];
|
|
@@ -734,14 +682,8 @@ export function readPath(root: unknown, path: readonly string[]): unknown {
|
|
|
734
682
|
}, root);
|
|
735
683
|
}
|
|
736
684
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
*
|
|
740
|
-
* 两种:`null`("没有值"的判断位,例如 `defaultModel` 用 `.default(null)` 表达"没配就没有这个键"),以及
|
|
741
|
-
* schemastery 给容器类型自动塞的空壳(`{}`)。两种都该让容器把字段摆出来。
|
|
742
|
-
* @param node - 字段节点。
|
|
743
|
-
* @returns 是不是空壳。
|
|
744
|
-
*/
|
|
685
|
+
// 这个"默认值"其实不是值——`null`("没有值"的判断位)或 schemastery 给容器自动塞的空壳(`{}`),
|
|
686
|
+
// 两种都该让容器把字段摆出来,而不是拿来当新加项的初值。
|
|
745
687
|
function isBlankDefault(node: FieldNode): boolean {
|
|
746
688
|
const value = node.meta.defaultValue;
|
|
747
689
|
if (value === null) return true;
|
|
@@ -749,15 +691,10 @@ function isBlankDefault(node: FieldNode): boolean {
|
|
|
749
691
|
return typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
|
|
750
692
|
}
|
|
751
693
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
* 而不是 `""`、`0` 这种看着像已经填过的值。host 侧也认这个语义:非必填字段拿到 `null` 回退到默认值,
|
|
757
|
-
* 必填字段拿到 `null` 就是"必须填"。
|
|
758
|
-
* @param node - 子节点的字段树。
|
|
759
|
-
* @returns 初值。
|
|
760
|
-
*/
|
|
694
|
+
// 一个新子节点的初值:声明的默认值优先,对象把字段摆出来,字典 / 数组给空容器,其余标量给 `null`。
|
|
695
|
+
//
|
|
696
|
+
// `null` 在这里是"还没有值"的判断位(`null` / `undefined` 都算),页面据此画成等着输入的位子——而不是 `""`、
|
|
697
|
+
// `0` 这种看着像已填过的值;host 侧也认这个语义(非必填字段拿到 `null` 回退默认值,必填就是"必须填")。
|
|
761
698
|
export function emptyValueOf(node: FieldNode): unknown {
|
|
762
699
|
if (node.meta.hasDefault && !isBlankDefault(node)) {
|
|
763
700
|
return structuredClone(node.meta.defaultValue);
|
|
@@ -785,7 +722,7 @@ export function emptyValueOf(node: FieldNode): unknown {
|
|
|
785
722
|
}
|
|
786
723
|
}
|
|
787
724
|
|
|
788
|
-
|
|
725
|
+
// 选中与当前值匹配的 union 变体:对象按判别标签的值判,叶子按值类型判。
|
|
789
726
|
function pickVariant(node: FieldNode & { type: "union" }, value: unknown): FieldNode | undefined {
|
|
790
727
|
if (
|
|
791
728
|
node.tagKey !== undefined &&
|