@shwfed/config 3.0.9 → 3.0.11
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/module.json +1 -1
- package/dist/runtime/components/actions/buttons/2026-04-21/com.shwfed.actions.button.navigation/runtime.vue +3 -2
- package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/config.d.vue.ts +18 -18
- package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/config.vue.d.ts +18 -18
- package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/runtime.d.vue.ts +18 -18
- package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/runtime.vue.d.ts +18 -18
- package/dist/runtime/components/config/index.d.vue.ts +2 -2
- package/dist/runtime/components/config/index.vue.d.ts +2 -2
- package/dist/runtime/components/form/fields/2026-04-24/com.shwfed.form.field.combobox.single/config.d.vue.ts +16 -16
- package/dist/runtime/components/form/fields/2026-04-24/com.shwfed.form.field.combobox.single/config.vue.d.ts +16 -16
- package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.combobox.single.remote/config.d.vue.ts +16 -16
- package/dist/runtime/components/form/fields/2026-05-13/com.shwfed.form.field.combobox.single.remote/config.vue.d.ts +16 -16
- package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-25/com.shwfed.form.field.combobox.multi/config.d.vue.ts +16 -16
- package/dist/runtime/components/form/fields/2026-05-25/com.shwfed.form.field.combobox.multi/config.vue.d.ts +16 -16
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.d.vue.ts +26 -26
- package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.vue.d.ts +26 -26
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.d.vue.ts +34 -0
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.vue +109 -1
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/config.vue.d.ts +34 -0
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/prompt.md +7 -0
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/runtime.vue +66 -22
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/schema.d.ts +23 -0
- package/dist/runtime/components/form/fields/2026-06-09/com.shwfed.form.field.upload/schema.js +26 -0
- package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/runtime.js +3 -2
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +10 -10
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +10 -10
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/config.d.vue.ts +10 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/config.vue +1004 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/config.vue.d.ts +10 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/prompt.md +19 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.d.vue.ts +9 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.vue +503 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/runtime.vue.d.ts +9 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/schema.d.ts +138 -0
- package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/schema.js +181 -0
- package/dist/runtime/share/navigation.d.ts +23 -0
- package/dist/runtime/share/navigation.js +15 -0
- package/dist/runtime/vendor/cel-js/CLAUDE.md +3 -1
- package/dist/runtime/vendor/cel-js/PROMPT.md +13 -0
- package/dist/runtime/vendor/cel-js/lib/macros.js +64 -1
- package/package.json +2 -1
package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/schema.d.ts
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
import type { ColumnDef } from '@tanstack/vue-table';
|
|
3
|
+
import type { Environment } from '../../../../../vendor/cel-js/lib/index.js';
|
|
4
|
+
import type { ColumnDefDeps } from '../../../utils/resolve.js';
|
|
5
|
+
export declare const type: "com.shwfed.table.column.upload";
|
|
6
|
+
export declare const compatibilityDate: "2026-07-16";
|
|
7
|
+
export declare const metadata: {
|
|
8
|
+
readonly name: "文件上传";
|
|
9
|
+
readonly icon: "fluent:cloud-arrow-up-20-regular";
|
|
10
|
+
readonly description: "可编辑的文件上传单元格,值为文件描述对象列表,写回行数据";
|
|
11
|
+
};
|
|
12
|
+
export declare const JSON_VAR: {
|
|
13
|
+
readonly type: "dyn";
|
|
14
|
+
readonly label: "HTTP 响应体";
|
|
15
|
+
readonly description: "第一步 `请求` 的响应体(已解析 JSON);用于构造第二步下载请求";
|
|
16
|
+
};
|
|
17
|
+
export declare const FILES_VAR: {
|
|
18
|
+
readonly type: "list<dyn>";
|
|
19
|
+
readonly label: "已选文件";
|
|
20
|
+
readonly description: "本次选择的文件列表(`File` 数组);通常通过 `form({...})` 构造 multipart 请求体";
|
|
21
|
+
};
|
|
22
|
+
export declare const UPLOAD_JSON_VAR: {
|
|
23
|
+
readonly type: "dyn";
|
|
24
|
+
readonly label: "上传响应";
|
|
25
|
+
readonly description: "上传 `请求` 的响应体(已解析 JSON);用于提取已上传文件项";
|
|
26
|
+
};
|
|
27
|
+
export declare const FILE_VAR: {
|
|
28
|
+
readonly type: "dyn";
|
|
29
|
+
readonly label: "文件项";
|
|
30
|
+
readonly description: "行数据中的单个文件项(服务端回填或上传响应映射所得);返回其展示文件名";
|
|
31
|
+
};
|
|
32
|
+
export declare function schema(configure: (env: Environment) => void): Schema.Struct<{
|
|
33
|
+
placeholder: Schema.optional<Schema.suspend<readonly [{
|
|
34
|
+
readonly locale: "zh";
|
|
35
|
+
readonly message: string;
|
|
36
|
+
}, ...{
|
|
37
|
+
readonly locale: "en" | "ja" | "ko";
|
|
38
|
+
readonly message: string;
|
|
39
|
+
}[]], string | readonly [{
|
|
40
|
+
readonly locale: "zh";
|
|
41
|
+
readonly message: string;
|
|
42
|
+
}, ...{
|
|
43
|
+
readonly locale: "en" | "ja" | "ko";
|
|
44
|
+
readonly message: string;
|
|
45
|
+
}[]], never>>;
|
|
46
|
+
disabled: Schema.optional<Schema.Schema<string, string, never>>;
|
|
47
|
+
readonly: Schema.optional<Schema.Schema<string, string, never>>;
|
|
48
|
+
multiple: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
|
|
49
|
+
accept: Schema.optional<Schema.Array$<typeof Schema.String>>;
|
|
50
|
+
maxFileSize: Schema.optional<Schema.refine<number, typeof Schema.Number>>;
|
|
51
|
+
maxTotalSize: Schema.optional<Schema.refine<number, typeof Schema.Number>>;
|
|
52
|
+
maxFiles: Schema.optional<Schema.refine<number, Schema.filter<typeof Schema.Number>>>;
|
|
53
|
+
upload: Schema.optional<Schema.Struct<{
|
|
54
|
+
request: Schema.Schema<string, string, never>;
|
|
55
|
+
handle: Schema.Schema<string, string, never>;
|
|
56
|
+
filename: Schema.optional<Schema.Schema<string, string, never>>;
|
|
57
|
+
}>>;
|
|
58
|
+
templates: Schema.optional<Schema.Array$<Schema.Struct<{
|
|
59
|
+
request: Schema.Schema<string, string, never>;
|
|
60
|
+
download: Schema.optional<Schema.Schema<string, string, never>>;
|
|
61
|
+
icon: Schema.optional<Schema.refine<string, typeof Schema.String>>;
|
|
62
|
+
label: Schema.optional<Schema.suspend<readonly [{
|
|
63
|
+
readonly locale: "zh";
|
|
64
|
+
readonly message: string;
|
|
65
|
+
}, ...{
|
|
66
|
+
readonly locale: "en" | "ja" | "ko";
|
|
67
|
+
readonly message: string;
|
|
68
|
+
}[]], string | readonly [{
|
|
69
|
+
readonly locale: "zh";
|
|
70
|
+
readonly message: string;
|
|
71
|
+
}, ...{
|
|
72
|
+
readonly locale: "en" | "ja" | "ko";
|
|
73
|
+
readonly message: string;
|
|
74
|
+
}[]], never>>;
|
|
75
|
+
}>>>;
|
|
76
|
+
download: Schema.optional<Schema.Struct<{
|
|
77
|
+
operations: Schema.Array$<Schema.Struct<{
|
|
78
|
+
id: Schema.optional<Schema.refine<string, typeof Schema.String>>;
|
|
79
|
+
target: Schema.refine<string, typeof Schema.String>;
|
|
80
|
+
operation: Schema.SchemaClass<string, string, never>;
|
|
81
|
+
compatibilityDate: Schema.optional<Schema.SchemaClass<string, string, never>>;
|
|
82
|
+
params: Schema.optional<Schema.SchemaClass<unknown, unknown, never>>;
|
|
83
|
+
}>>;
|
|
84
|
+
icon: Schema.optional<Schema.refine<string, typeof Schema.String>>;
|
|
85
|
+
label: Schema.optional<Schema.suspend<readonly [{
|
|
86
|
+
readonly locale: "zh";
|
|
87
|
+
readonly message: string;
|
|
88
|
+
}, ...{
|
|
89
|
+
readonly locale: "en" | "ja" | "ko";
|
|
90
|
+
readonly message: string;
|
|
91
|
+
}[]], string | readonly [{
|
|
92
|
+
readonly locale: "zh";
|
|
93
|
+
readonly message: string;
|
|
94
|
+
}, ...{
|
|
95
|
+
readonly locale: "en" | "ja" | "ko";
|
|
96
|
+
readonly message: string;
|
|
97
|
+
}[]], never>>;
|
|
98
|
+
}>>;
|
|
99
|
+
title: Schema.suspend<readonly [{
|
|
100
|
+
readonly locale: "zh";
|
|
101
|
+
readonly message: string;
|
|
102
|
+
}, ...{
|
|
103
|
+
readonly locale: "en" | "ja" | "ko";
|
|
104
|
+
readonly message: string;
|
|
105
|
+
}[]], string | readonly [{
|
|
106
|
+
readonly locale: "zh";
|
|
107
|
+
readonly message: string;
|
|
108
|
+
}, ...{
|
|
109
|
+
readonly locale: "en" | "ja" | "ko";
|
|
110
|
+
readonly message: string;
|
|
111
|
+
}[]], never>;
|
|
112
|
+
binding: Schema.refine<string, typeof Schema.String>;
|
|
113
|
+
enableSorting: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
|
|
114
|
+
sortKey: Schema.optional<Schema.refine<string, typeof Schema.String>>;
|
|
115
|
+
size: Schema.optional<Schema.refine<number, Schema.filter<typeof Schema.Number>>>;
|
|
116
|
+
grow: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
|
|
117
|
+
tooltip: Schema.optional<Schema.suspend<readonly [{
|
|
118
|
+
readonly locale: "zh";
|
|
119
|
+
readonly message: string;
|
|
120
|
+
}, ...{
|
|
121
|
+
readonly locale: "en" | "ja" | "ko";
|
|
122
|
+
readonly message: string;
|
|
123
|
+
}[]], string | readonly [{
|
|
124
|
+
readonly locale: "zh";
|
|
125
|
+
readonly message: string;
|
|
126
|
+
}, ...{
|
|
127
|
+
readonly locale: "en" | "ja" | "ko";
|
|
128
|
+
readonly message: string;
|
|
129
|
+
}[]], never>>;
|
|
130
|
+
hidden: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
|
|
131
|
+
id: Schema.refine<string, typeof Schema.String>;
|
|
132
|
+
groupId: Schema.optional<typeof Schema.UUID>;
|
|
133
|
+
type: Schema.Literal<["com.shwfed.table.column.upload"]>;
|
|
134
|
+
compatibilityDate: Schema.Literal<["2026-07-16"]>;
|
|
135
|
+
}>;
|
|
136
|
+
export type Value = Schema.Schema.Type<ReturnType<typeof schema>>;
|
|
137
|
+
export declare function defaults(): Partial<Value>;
|
|
138
|
+
export declare function toColumnDef(value: Value, { getLocaleText }: ColumnDefDeps): Partial<ColumnDef<unknown, unknown>>;
|
package/dist/runtime/components/table/columns/2026-07-16/com.shwfed.table.column.upload/schema.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { getProperty } from "dot-prop";
|
|
3
|
+
import { Expression } from "../../../../../share/expression.js";
|
|
4
|
+
import { Triggers } from "../../../../../share/event-bus.js";
|
|
5
|
+
import { Locale } from "../../../../../share/locale.js";
|
|
6
|
+
import {
|
|
7
|
+
CelRowAccess,
|
|
8
|
+
editableColumnFields,
|
|
9
|
+
editableHeader,
|
|
10
|
+
registerRowVariablesIfAbsent
|
|
11
|
+
} from "../../../utils/shared.js";
|
|
12
|
+
export const type = "com.shwfed.table.column.upload";
|
|
13
|
+
export const compatibilityDate = "2026-07-16";
|
|
14
|
+
export const metadata = {
|
|
15
|
+
name: "\u6587\u4EF6\u4E0A\u4F20",
|
|
16
|
+
icon: "fluent:cloud-arrow-up-20-regular",
|
|
17
|
+
description: "\u53EF\u7F16\u8F91\u7684\u6587\u4EF6\u4E0A\u4F20\u5355\u5143\u683C\uFF0C\u503C\u4E3A\u6587\u4EF6\u63CF\u8FF0\u5BF9\u8C61\u5217\u8868\uFF0C\u5199\u56DE\u884C\u6570\u636E"
|
|
18
|
+
};
|
|
19
|
+
export const JSON_VAR = {
|
|
20
|
+
type: "dyn",
|
|
21
|
+
label: "HTTP \u54CD\u5E94\u4F53",
|
|
22
|
+
description: "\u7B2C\u4E00\u6B65 `\u8BF7\u6C42` \u7684\u54CD\u5E94\u4F53\uFF08\u5DF2\u89E3\u6790 JSON\uFF09\uFF1B\u7528\u4E8E\u6784\u9020\u7B2C\u4E8C\u6B65\u4E0B\u8F7D\u8BF7\u6C42"
|
|
23
|
+
};
|
|
24
|
+
export const FILES_VAR = {
|
|
25
|
+
type: "list<dyn>",
|
|
26
|
+
label: "\u5DF2\u9009\u6587\u4EF6",
|
|
27
|
+
description: "\u672C\u6B21\u9009\u62E9\u7684\u6587\u4EF6\u5217\u8868\uFF08`File` \u6570\u7EC4\uFF09\uFF1B\u901A\u5E38\u901A\u8FC7 `form({...})` \u6784\u9020 multipart \u8BF7\u6C42\u4F53"
|
|
28
|
+
};
|
|
29
|
+
export const UPLOAD_JSON_VAR = {
|
|
30
|
+
type: "dyn",
|
|
31
|
+
label: "\u4E0A\u4F20\u54CD\u5E94",
|
|
32
|
+
description: "\u4E0A\u4F20 `\u8BF7\u6C42` \u7684\u54CD\u5E94\u4F53\uFF08\u5DF2\u89E3\u6790 JSON\uFF09\uFF1B\u7528\u4E8E\u63D0\u53D6\u5DF2\u4E0A\u4F20\u6587\u4EF6\u9879"
|
|
33
|
+
};
|
|
34
|
+
export const FILE_VAR = {
|
|
35
|
+
type: "dyn",
|
|
36
|
+
label: "\u6587\u4EF6\u9879",
|
|
37
|
+
description: "\u884C\u6570\u636E\u4E2D\u7684\u5355\u4E2A\u6587\u4EF6\u9879\uFF08\u670D\u52A1\u7AEF\u56DE\u586B\u6216\u4E0A\u4F20\u54CD\u5E94\u6620\u5C04\u6240\u5F97\uFF09\uFF1B\u8FD4\u56DE\u5176\u5C55\u793A\u6587\u4EF6\u540D"
|
|
38
|
+
};
|
|
39
|
+
function withRowAndVar(configure, name, spec) {
|
|
40
|
+
return (env) => {
|
|
41
|
+
configure(env);
|
|
42
|
+
registerRowVariablesIfAbsent(env);
|
|
43
|
+
env.registerVariable(name, spec.type, { description: spec.description });
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function schema(configure) {
|
|
47
|
+
const CelBool = CelRowAccess(configure, { resultType: "bool" });
|
|
48
|
+
const CelHttpRequest = CelRowAccess(configure, { resultType: "HttpRequest" });
|
|
49
|
+
const CelDownloadRequest = Expression({ configure: withRowAndVar(configure, "json", JSON_VAR), resultType: "HttpRequest" });
|
|
50
|
+
const CelUploadRequest = Expression({ configure: withRowAndVar(configure, "files", FILES_VAR), resultType: "HttpRequest" });
|
|
51
|
+
const CelUploadHandle = Expression({ configure: withRowAndVar(configure, "json", UPLOAD_JSON_VAR), resultType: "list" });
|
|
52
|
+
const CelItemName = Expression({ configure: withRowAndVar(configure, "file", FILE_VAR), resultType: "string" });
|
|
53
|
+
return Schema.Struct({
|
|
54
|
+
type: Schema.Literal(type),
|
|
55
|
+
compatibilityDate: Schema.Literal(compatibilityDate),
|
|
56
|
+
...editableColumnFields(),
|
|
57
|
+
placeholder: Schema.optional(Locale.annotations({
|
|
58
|
+
title: "\u5360\u4F4D\u7B26",
|
|
59
|
+
description: "\u5355\u5143\u683C\u4E3A\u7A7A\u65F6\u4E0A\u4F20\u533A\u57DF\u7684\u63D0\u793A\u6587\u672C"
|
|
60
|
+
})),
|
|
61
|
+
disabled: Schema.optional(CelBool.annotations({
|
|
62
|
+
title: "\u7981\u7528\u6761\u4EF6",
|
|
63
|
+
description: "\u8FD4\u56DE `true` \u65F6\u4E0A\u4F20\u533A\u57DF\u4ECD\u7136\u6E32\u67D3\u4F46\u4E0D\u53EF\u64CD\u4F5C"
|
|
64
|
+
})),
|
|
65
|
+
readonly: Schema.optional(CelBool.annotations({
|
|
66
|
+
title: "\u53EA\u8BFB\u6761\u4EF6",
|
|
67
|
+
description: "\u8FD4\u56DE `true` \u65F6\u4EC5\u4EE5\u6587\u4EF6\u5217\u8868\u5C55\u793A\u5F53\u524D\u503C\uFF0C\u4E0D\u5141\u8BB8\u589E\u5220"
|
|
68
|
+
})),
|
|
69
|
+
multiple: Schema.optional(Schema.Boolean.annotations({
|
|
70
|
+
title: "\u5141\u8BB8\u591A\u6587\u4EF6",
|
|
71
|
+
description: "\u5F00\u542F\u540E\u53EF\u4E00\u6B21\u9009\u62E9/\u62D6\u5165\u591A\u4E2A\u6587\u4EF6\uFF1B\u5173\u95ED\u65F6\u65B0\u6587\u4EF6\u4F1A\u66FF\u6362\u5DF2\u9009\u6587\u4EF6"
|
|
72
|
+
})),
|
|
73
|
+
accept: Schema.optional(Schema.Array(Schema.String).annotations({
|
|
74
|
+
title: "\u5141\u8BB8\u7684\u6587\u4EF6\u7C7B\u578B",
|
|
75
|
+
description: "\u5141\u8BB8\u4E0A\u4F20\u7684 MIME \u7C7B\u578B\u5217\u8868\uFF1B\u7559\u7A7A\u8868\u793A\u4E0D\u9650\u5236"
|
|
76
|
+
})),
|
|
77
|
+
maxFileSize: Schema.optional(Schema.Number.pipe(Schema.positive()).annotations({
|
|
78
|
+
title: "\u5355\u6587\u4EF6\u5927\u5C0F\u9650\u5236",
|
|
79
|
+
description: "\u5355\u4E2A\u6587\u4EF6\u5141\u8BB8\u7684\u6700\u5927\u5B57\u8282\u6570\uFF1B\u8D85\u8FC7\u6B64\u9650\u5236\u7684\u6587\u4EF6\u4F1A\u88AB\u62D2\u7EDD"
|
|
80
|
+
})),
|
|
81
|
+
maxTotalSize: Schema.optional(Schema.Number.pipe(Schema.positive()).annotations({
|
|
82
|
+
title: "\u603B\u5927\u5C0F\u9650\u5236",
|
|
83
|
+
description: "\u6240\u6709\u5DF2\u9009\u6587\u4EF6\u7D2F\u8BA1\u7684\u6700\u5927\u5B57\u8282\u6570\uFF1B\u65B0\u589E\u5BFC\u81F4\u8D85\u51FA\u65F6\u4F1A\u88AB\u62D2\u7EDD"
|
|
84
|
+
})),
|
|
85
|
+
maxFiles: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.positive()).annotations({
|
|
86
|
+
title: "\u6587\u4EF6\u6570\u91CF\u9650\u5236",
|
|
87
|
+
description: "\u5141\u8BB8\u540C\u65F6\u4E0A\u4F20\u7684\u6700\u5927\u6587\u4EF6\u6570\uFF1B\u8FBE\u5230\u4E0A\u9650\u540E\u65B0\u589E\u6587\u4EF6\u4F1A\u88AB\u62D2\u7EDD"
|
|
88
|
+
})),
|
|
89
|
+
// Immediate upload. Its presence enables "upload on pick": each picked file
|
|
90
|
+
// is POSTed right away (`request`), the response is mapped into the stored
|
|
91
|
+
// items (`handle`), and each stored item's display name is resolved via
|
|
92
|
+
// `filename`. Absent → files are held as raw `File[]` at the binding and
|
|
93
|
+
// submitted with whatever consumes the row. Mirrors the form upload field.
|
|
94
|
+
upload: Schema.optional(Schema.Struct({
|
|
95
|
+
request: CelUploadRequest.annotations({
|
|
96
|
+
title: "\u4E0A\u4F20\u8BF7\u6C42",
|
|
97
|
+
description: '\u8FD4\u56DE `HttpRequest` \u7684 CEL \u8868\u8FBE\u5F0F\uFF1B\u53EF\u901A\u8FC7 `files` \u5F15\u7528\u672C\u6B21\u9009\u62E9\u7684\u6587\u4EF6\uFF0C\u4F8B\u5982 `http.post(url).body(form({"files": files}))`\u3002\u4EE5 `.json()` \u53D1\u8D77'
|
|
98
|
+
}),
|
|
99
|
+
handle: CelUploadHandle.annotations({
|
|
100
|
+
title: "\u5904\u7406\u54CD\u5E94",
|
|
101
|
+
description: '\u8FD4\u56DE**\u5217\u8868**\u7684 CEL \u8868\u8FBE\u5F0F\uFF0C\u53EF\u901A\u8FC7 `json` \u5F15\u7528\u4E0A\u4F20\u54CD\u5E94\uFF1B\u6620\u5C04\u4E3A\u5199\u5165\u7ED1\u5B9A\u503C\u7684\u6587\u4EF6\u9879\uFF0C\u4F8B\u5982 `json.data.map(d, {"cacheKey": d.key, "filename": d.fileOriginalName})`'
|
|
102
|
+
}),
|
|
103
|
+
filename: Schema.optional(CelItemName.annotations({
|
|
104
|
+
title: "\u6587\u4EF6\u540D",
|
|
105
|
+
description: "\u8FD4\u56DE `string` \u7684 CEL \u8868\u8FBE\u5F0F\uFF0C\u53EF\u901A\u8FC7 `file` \u5F15\u7528\u5355\u4E2A\u6587\u4EF6\u9879\uFF1B\u7528\u4E8E\u5C55\u793A\u6587\u4EF6\u540D\uFF0C\u4F8B\u5982 `file.filename`\u3002\u7559\u7A7A\u65F6\u56DE\u9000\u5230 `filename` / `name` \u5B57\u6BB5"
|
|
106
|
+
}))
|
|
107
|
+
}).annotations({
|
|
108
|
+
title: "\u5373\u65F6\u4E0A\u4F20",
|
|
109
|
+
description: "\u914D\u7F6E\u540E\u6587\u4EF6\u5728\u9009\u62E9\u65F6\u7ACB\u5373\u4E0A\u4F20\uFF0C\u7ED1\u5B9A\u503C\u5B58\u50A8\u6620\u5C04\u540E\u7684\u6587\u4EF6\u9879\uFF08\u800C\u975E\u539F\u59CB `File`\uFF09\uFF1B\u4E0D\u914D\u7F6E\u5219\u4FDD\u7559\u6587\u4EF6\u7B49\u5F85\u6574\u884C\u63D0\u4EA4"
|
|
110
|
+
})),
|
|
111
|
+
// Download templates — each entry renders one button next to the upload
|
|
112
|
+
// zone. `request` is the (possibly only) step; `download` is an optional
|
|
113
|
+
// second step that may reference the first response via `json`.
|
|
114
|
+
templates: Schema.optional(Schema.Array(Schema.Struct({
|
|
115
|
+
request: CelHttpRequest.annotations({
|
|
116
|
+
title: "\u8BF7\u6C42",
|
|
117
|
+
description: "\u8FD4\u56DE `HttpRequest` \u7684 CEL \u8868\u8FBE\u5F0F\u3002\u672A\u914D\u7F6E\u300C\u4E0B\u8F7D\u8BF7\u6C42\u300D\u65F6\u76F4\u63A5\u53D1\u8D77\u5E76\u4E0B\u8F7D\u5176\u54CD\u5E94\uFF1B\u914D\u7F6E\u4E86\u300C\u4E0B\u8F7D\u8BF7\u6C42\u300D\u65F6\u4F5C\u4E3A\u83B7\u53D6\u4E0B\u8F7D\u51ED\u636E\u7684\u7B2C\u4E00\u6B65\u8BF7\u6C42"
|
|
118
|
+
}),
|
|
119
|
+
download: Schema.optional(CelDownloadRequest.annotations({
|
|
120
|
+
title: "\u4E0B\u8F7D\u8BF7\u6C42",
|
|
121
|
+
description: "\u53EF\u9009\u7684\u7B2C\u4E8C\u6B65\uFF1A\u8FD4\u56DE `HttpRequest` \u7684 CEL \u8868\u8FBE\u5F0F\uFF0C\u53EF\u901A\u8FC7 `json` \u5F15\u7528\u7B2C\u4E00\u6B65\u7684\u54CD\u5E94\u4F53\uFF0C\u4F8B\u5982\u7528 `json.data.key` \u6784\u9020\u771F\u6B63\u7684\u4E0B\u8F7D\u8BF7\u6C42"
|
|
122
|
+
})),
|
|
123
|
+
icon: Schema.optional(Schema.String.pipe(Schema.minLength(1)).annotations({
|
|
124
|
+
title: "\u6A21\u677F\u6309\u94AE\u56FE\u6807",
|
|
125
|
+
description: "Iconify \u56FE\u6807\u6807\u8BC6\u7B26\uFF0C\u4F8B\u5982 `fluent:arrow-download-20-regular`\uFF1B\u7559\u7A7A\u65F6\u4F7F\u7528\u9ED8\u8BA4\u56FE\u6807"
|
|
126
|
+
})),
|
|
127
|
+
label: Schema.optional(Locale.annotations({
|
|
128
|
+
title: "\u6A21\u677F\u6309\u94AE\u6587\u672C",
|
|
129
|
+
description: "\u4E0B\u8F7D\u6A21\u677F\u6309\u94AE\u4E0A\u7684\u672C\u5730\u5316\u6587\u672C\uFF1B\u7559\u7A7A\u65F6\u4F7F\u7528\u300C\u4E0B\u8F7D\u6A21\u677F\u300D"
|
|
130
|
+
}))
|
|
131
|
+
})).annotations({
|
|
132
|
+
title: "\u4E0B\u8F7D\u6A21\u677F",
|
|
133
|
+
description: "\u4E0B\u8F7D\u6A21\u677F\u5217\u8868\uFF1B\u6BCF\u4E00\u9879\u5728\u4E0A\u4F20\u533A\u57DF\u65C1\u6E32\u67D3\u4E00\u4E2A\u4E0B\u8F7D\u6309\u94AE"
|
|
134
|
+
})),
|
|
135
|
+
// Per-file download — a 触发操作 (trigger-operation) list run when the user
|
|
136
|
+
// clicks a file item's 下载 button, with the clicked stored item in scope as
|
|
137
|
+
// `file` (the same `FILE_VAR` as `upload.filename`), on top of the row scope.
|
|
138
|
+
// Object entries download via these operations; a freshly-picked native
|
|
139
|
+
// `File` bypasses the list and downloads from its in-memory blob. Mirrors the
|
|
140
|
+
// form upload field.
|
|
141
|
+
download: Schema.optional(Schema.Struct({
|
|
142
|
+
operations: Triggers,
|
|
143
|
+
icon: Schema.optional(Schema.String.pipe(Schema.minLength(1)).annotations({
|
|
144
|
+
title: "\u4E0B\u8F7D\u6309\u94AE\u56FE\u6807",
|
|
145
|
+
description: "Iconify \u56FE\u6807\u6807\u8BC6\u7B26\uFF0C\u4F8B\u5982 `fluent:arrow-download-20-regular`\uFF1B\u7559\u7A7A\u65F6\u4F7F\u7528\u9ED8\u8BA4\u56FE\u6807"
|
|
146
|
+
})),
|
|
147
|
+
label: Schema.optional(Locale.annotations({
|
|
148
|
+
title: "\u4E0B\u8F7D\u6309\u94AE\u6587\u672C",
|
|
149
|
+
description: "\u6BCF\u4E2A\u6587\u4EF6\u9879\u4E0B\u8F7D\u6309\u94AE\u7684\u672C\u5730\u5316\u65E0\u969C\u788D\u6807\u7B7E\uFF1B\u7559\u7A7A\u65F6\u4F7F\u7528\u300C\u4E0B\u8F7D\u300D"
|
|
150
|
+
}))
|
|
151
|
+
}).annotations({
|
|
152
|
+
title: "\u6587\u4EF6\u4E0B\u8F7D",
|
|
153
|
+
description: "\u914D\u7F6E\u540E\u6BCF\u4E2A\u6587\u4EF6\u9879\u65C1\u6E32\u67D3\u4E0B\u8F7D\u6309\u94AE\uFF1A\u5DF2\u4E0A\u4F20/\u56DE\u586B\u7684\u5BF9\u8C61\u9879\u6309\u300C\u89E6\u53D1\u64CD\u4F5C\u300D\u5217\u8868\u4E0B\u8F7D\uFF08`file` \u4E3A\u8BE5\u6587\u4EF6\u9879\uFF09\uFF1B\u5C1A\u672A\u4E0A\u4F20\u7684\u672C\u5730\u6587\u4EF6\u76F4\u63A5\u4ECE\u5185\u5B58\u4E0B\u8F7D\uFF0C\u4E0D\u7ECF\u8FC7\u8BE5\u5217\u8868"
|
|
154
|
+
}))
|
|
155
|
+
}).annotations({ title: "UploadRenderer", description: "\u6587\u4EF6\u4E0A\u4F20\u6E32\u67D3\u5668\uFF08\u53EF\u7F16\u8F91\uFF09" });
|
|
156
|
+
}
|
|
157
|
+
export function defaults() {
|
|
158
|
+
return {
|
|
159
|
+
title: [{ locale: "zh", message: "" }],
|
|
160
|
+
binding: "",
|
|
161
|
+
size: 200
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
export function toColumnDef(value, { getLocaleText }) {
|
|
165
|
+
return {
|
|
166
|
+
header: editableHeader(getLocaleText(value.title)),
|
|
167
|
+
// `binding` is a dot-prop path (validated non-empty); the cell renderer
|
|
168
|
+
// reads through the same path it writes to. No CEL — accessor is literal.
|
|
169
|
+
accessorFn: (row) => {
|
|
170
|
+
if (!row || typeof row !== "object") return void 0;
|
|
171
|
+
return getProperty(row, value.binding);
|
|
172
|
+
},
|
|
173
|
+
enableSorting: value.enableSorting ?? false,
|
|
174
|
+
sortingFn: "alphanumeric",
|
|
175
|
+
size: value.size,
|
|
176
|
+
meta: {
|
|
177
|
+
grow: value.grow ?? false,
|
|
178
|
+
tooltip: getLocaleText(value.tooltip)
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -1 +1,24 @@
|
|
|
1
1
|
export declare function isAbsoluteUrl(url: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Demote an absolute href that names OUR OWN origin to a router-ready in-app
|
|
4
|
+
* path, so `_self` navigation to it is an SPA push rather than a full document
|
|
5
|
+
* reload. Returns `null` for any other origin — and for opaque-origin schemes
|
|
6
|
+
* like `mailto:` / `tel:` / custom app schemes — which stay full-page
|
|
7
|
+
* navigations governed by `isAbsoluteUrl`.
|
|
8
|
+
*
|
|
9
|
+
* Why it exists: a CEL author who writes `location.query('tab', 'x')` gets a
|
|
10
|
+
* `URL` back, and `string(URL)` is the full `.href` — origin and all. Reloading
|
|
11
|
+
* our own page just to flip a query param is almost never the intent, so a
|
|
12
|
+
* same-origin absolute is routed internally instead.
|
|
13
|
+
*
|
|
14
|
+
* Two subtleties make this more than an `=== origin` check — and are exactly
|
|
15
|
+
* why it cannot fold back into `isAbsoluteUrl`:
|
|
16
|
+
* - The router prepends its history base (`app.baseURL`) to whatever path it
|
|
17
|
+
* routes, and a full pathname ALREADY carries that base. Handing it back
|
|
18
|
+
* unstripped double-prepends (`/base/x` → `/base/base/x`), so the base is
|
|
19
|
+
* peeled off here. Vue Router's `history.base` may or may not carry a
|
|
20
|
+
* trailing slash; both normalize.
|
|
21
|
+
* - A protocol-relative `//host/x` has no scheme of its own, so it resolves
|
|
22
|
+
* against `origin` to inherit ours before the comparison.
|
|
23
|
+
*/
|
|
24
|
+
export declare function sameOriginInternalPath(href: string, origin: string, base: string): string | null;
|
|
@@ -2,3 +2,18 @@ const ABSOLUTE = /^(?:[a-z][a-z0-9+\-.]*:|\/\/)/i;
|
|
|
2
2
|
export function isAbsoluteUrl(url) {
|
|
3
3
|
return ABSOLUTE.test(url.trim());
|
|
4
4
|
}
|
|
5
|
+
export function sameOriginInternalPath(href, origin, base) {
|
|
6
|
+
let url;
|
|
7
|
+
try {
|
|
8
|
+
url = new URL(href.trim(), origin);
|
|
9
|
+
} catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (url.origin !== origin) return null;
|
|
13
|
+
const path = `${url.pathname}${url.search}${url.hash}`;
|
|
14
|
+
const prefix = base.replace(/\/+$/, "");
|
|
15
|
+
if (prefix && (path === prefix || path.startsWith(`${prefix}/`))) {
|
|
16
|
+
return path.slice(prefix.length) || "/";
|
|
17
|
+
}
|
|
18
|
+
return path;
|
|
19
|
+
}
|
|
@@ -26,6 +26,8 @@ The `homogeneousAggregateLiterals` and `enableOptionalTypes` environment options
|
|
|
26
26
|
|
|
27
27
|
The custom `Optional` class has been replaced with Effect's `Option` type (`import { Option } from 'effect'`). Internal helpers `optionalOf(value)` and `optionalValue(opt)` in `optional.js` handle the CEL-specific semantics (treating `undefined` as `None`, throwing `EvaluationError` on missing value access). The optional surface is `optional.of(A): optional<A>` (the lift / `pure` — `of(null)` is `Some(null)`), `optional.ofNullable(A): optional<A>` (the null-tolerant lift, Java-style — `null` becomes `None`), `optional.none()`, `optional.hasValue()`, `optional.value()`, `optional.or(optional)`, `optional.orValue(value)`, plus `optional<A>.flatMap(var, body): optional<B>` (monadic bind), `optional<A>.map(var, body): optional<B>` (the Functor `fmap`), `optional<A>.zip(other, x, y, body): optional<C>` (the applicative `liftA2` / `zipWith`), `optional<A>.filter(var, predicate): optional<A>` (Rust `Option::filter` / Haskell `mfilter`), and the folds `optional<A>.exists(var, predicate): bool` (predicate on `Some`, `false` on `None` — Rust `is_some_and`) and `optional<A>.all(var, predicate): bool` (predicate on `Some`, vacuously `true` on `None` — Scala `forall`) — all local additions (not from upstream). All are variable-binding macros (registered with `ast` arg types so the unbound args stay unevaluated): on `Some` they bind the value(s) and evaluate `body`; on `None` they short-circuit without touching `body`. `flatMap`'s body must itself yield an `optional`; `map`'s body is a plain value that gets re-wrapped in `Some`; `filter` / `exists` / `all` predicates must be `bool` (enforced at type-check when static, at runtime via `invalid_condition_type` otherwise). `map` is the derived `o.flatMap(v, optional.of(body))` promoted to a first-class macro. `zip` is the flattened `a.flatMap(x, b.map(y, body))`: it type-checks `other` in the outer ctx, binds `x` then `y` under two chained overlays, re-wraps the plain body in `Some`, and short-circuits to `None` if either side is `None` (a `None` receiver skips evaluating `other`). Because CEL has no tuple type the transform is fused (there is no useful `optional<pair>` to return), so `zip` is `zipWith`, not Rust/Scala's pair-building `Option.zip`. The short-circuit makes `zip` observationally monadic — harmless for `optional` (a `None` carries no payload) but it would not generalise to an error-accumulating applicative. An `optional.none()` receiver/argument carries a `param` element type; the shared `bindOptionalValueType` helper (exported from `optional.ts`) binds it as `dyn` so a boolean body still type-checks — every binding macro (`map` / `flatMap` / `zip` / `filter` / `exists` / `all`) goes through it (leaking the `param` into the iteration variable was a real bug in `map` / `flatMap`, regression-tested in `optional-extras.test.ts`). `zip` lives in `optional.ts` beside `flatMap`; the two-variable binding is why it is not folded into the shared `map` expander. NOTE: `map`, `filter`, `all`, and `exists` are **registered in `macros.ts`**, not here — the parser resolves a macro by name before types are known, so the optional variants and the list-comprehension variants must share one expander per name that dispatches on the receiver type at type-check (an `optional` receiver → optional semantics; `list`/`map` → comprehension; a `dyn` receiver is deferred to a runtime `Option.isOption` check). `exists_one` and `find` stay list-only.
|
|
28
28
|
|
|
29
|
+
A `list.filterMap(var, body): list<dyn>` macro has been added (not from upstream, in `macros.ts` beside `find`) — Rust's `Iterator::filter_map`. The body evaluates to an `optional<B>` per element; `Some` values are kept (unwrapped), `None`s dropped, yielding `list<B>`. It fuses the map-then-strip-absent pattern (which would otherwise need `.map(...)` to build a `list<optional>` plus a second pass to drop the `None`s) and is the optional-gated complement to the 3-arg `map(var, filter, transform)`, whose gate is a bool predicate. NOTE: it is **not** a comprehension desugar — the accu/ternary primitives gate on a bool, not on `Option` presence — so unlike the 2/3-arg `map` and `filter` it runs a manual fold like `find` (eval receiver → coerce Set/Map/object to an array → per-element `ev.eval(body)` → `ensureOptional` → push the `Some`s). Type-check: the receiver element type comes from list `valueType` / map `keyType` / `dyn` (same `invalid_comprehension_range` contract as `find`); the body must be `optional<B>` when statically typed (via `ensureOptionalType`), and a `dyn` body defers to the runtime `ensureOptional` (matching `flatMap`); the result is `registry.getListType(bindOptionalValueType(bodyType))`. Because the body already returns an `optional`, `filterMap` needs no receiver-type dispatch (there is no `optional<A>.filterMap` — that role is `flatMap`), so it is a single list-only expander. Tested in `__tests__/filter-map-macro.test.ts`.
|
|
30
|
+
|
|
29
31
|
An `http` built-in has been added (`http-builtins.ts`, `http-builder.ts`) — not from upstream. It registers the `http` constant and the `http` / `HttpRequest` types on `globalRegistry`, so `http.get(url).header(...).body(...)` expressions type-check and evaluate in every `Environment`. A CEL expression only builds an `HttpRequestBuilder` — a pure description of a request; it never issues one. The terminal methods `.json()`, `.file()`, and `.fileOrJson()` are dispatched by the host on the returned builder (none is a CEL method), so expression evaluation stays free of IO. `.fileOrJson()` branches on the response `Content-Type`: a JSON media type (`application/json` or a `+json` suffix) is parsed and returned as `{ _tag: 'json', json }`; anything else is returned as `{ _tag: 'file', file }`. Note the `Fetch` service itself `ensureOk`s (both `FetchLive` and the mock layer), so a non-2xx fails at `Fetch.fetch` before the branch and this only ever distinguishes 2xx bodies — i.e. the HTTP-200-with-error-envelope case. The download operation uses it so a backend that answers a download with a JSON error envelope (commonly `{ code: -1 }` on a 200) is classified rather than saved as the file. Endpoints must be absolute URLs — there is no base-URL resolution. Depends on `fx-fetch`. The host can register process-wide default headers via `HttpRequestBuilder.setDefaultHeader(name, valueOrGetter)` / `clearDefaultHeader(name)`; they are merged in at `#buildRequest()` time, with case-insensitive precedence to an explicit `.header(...)` on the builder. A getter that returns `null` / `undefined` / `''` skips the header for that request, so the host can source values from live refs (e.g. the active i18n locale for `Accept-Language`) without baking in stale snapshots. `.query` accepts `(string, string)` / `(string, number)`, the optional-valued `(string, optional<string>)` / `(string, optional<number>)` (a `None`, e.g. an absent `.?` field, drops the param rather than emitting `key=`; `Some` appends it via the string/number coercion), and the record-splat `(dyn)`.
|
|
30
32
|
|
|
31
33
|
A `form(dyn): FormData` built-in has been added (`form-builtins.ts`) — not from upstream. It registers the `FormData` type on `globalRegistry` and a global `form` function that turns a CEL map into a native `FormData`, so authors can write `http.post(url).body(form({"file": myFile, "name": "Alice"}))` (`.body()` passes `FormData` through verbatim; `#buildRequest()` serializes it). NOTE: `#buildRequest()` must serialize a `FormData` body itself rather than hand it to fx-fetch — fx-fetch clones bodies via `new Response(body).blob()`, and the File API ASCII-lowercases `Blob#type`, so in Chrome the multipart boundary in the derived `Content-Type` loses its case while the body bytes keep it (boundaries are case-sensitive → every parser sees zero parts). The builder lifts the case-preserved `Content-Type` off the `Response` *header* (header values are never case-normalized), adds it as an explicit header (unless the author set their own `Content-Type`), and passes the bytes as a `Promise<Blob>` (fx-fetch resolves promises verbatim). Value coercion mirrors `.query()`'s flat-record semantics: `null` / `undefined` / `Option.None` skip the key; `Option.Some(x)` recurses on `x`; `File` / `Blob` pass through (preserves the `File` filename); `Decimal` → `.toString()` (preserves precision past safe-integer); `Date` (`TZDate`) → `.toISOString()`; `bool` → `'true'` / `'false'`; arrays append one entry per element; nested objects throw (`multipart/form-data` is flat — same rule as `.query(dyn)`). Top-level input must be a map literal / record (Map or plain object); passing an array or scalar throws.
|
|
@@ -51,7 +53,7 @@ lib/
|
|
|
51
53
|
├── operators.ts — Binary/unary operator implementations
|
|
52
54
|
├── overloads.ts — Operator overload definitions
|
|
53
55
|
├── functions.ts — Built-in functions + Date type (TZDate)
|
|
54
|
-
├── macros.ts — map / filter / all / exists (each dispatching list comprehension vs optional receiver), exists_one, find, has, cel.bind
|
|
56
|
+
├── macros.ts — map / filter / all / exists (each dispatching list comprehension vs optional receiver), exists_one, find, filterMap, has, cel.bind
|
|
55
57
|
├── http-builtins.ts — `http` constant + http/HttpRequest types (local addition)
|
|
56
58
|
├── http-builder.ts — `HttpRequestBuilder` — what an http.* expression returns
|
|
57
59
|
├── json-builtins.ts — `JSON` constant + `JSON.parse` / `JSON.stringify` (local addition)
|
|
@@ -505,6 +505,19 @@ Macros are special forms — they look like method calls but have special evalua
|
|
|
505
505
|
[1, 2, 3, 4, 5].filter(x, x > 3) // [4, 5]
|
|
506
506
|
```
|
|
507
507
|
|
|
508
|
+
### filterMap — Transform-and-keep in one pass (Rust `filter_map`)
|
|
509
|
+
The body returns an `optional<B>` per element; the `Some` values are kept
|
|
510
|
+
(unwrapped) and the `None`s dropped, yielding a `list<B>`. Use it when the
|
|
511
|
+
keep/drop decision *is* the transform — computing the mapped value and its
|
|
512
|
+
presence together — rather than a separate bool predicate. (The 3-arg
|
|
513
|
+
`map(x, filter, transform)` is the bool-predicate form; reach for `filterMap`
|
|
514
|
+
when the gate rides on an `optional`, e.g. a `.?` field or `optional.ofNullable`.)
|
|
515
|
+
```cel
|
|
516
|
+
[1, 2, 3, 4].filterMap(x, x % 2 == 0 ? optional.of(x * 10) : optional.none()) // [20, 40]
|
|
517
|
+
users.filterMap(u, optional.ofNullable(u.email)) // emails of users that have one
|
|
518
|
+
rows.filterMap(r, r.?qty.map(q, q * 2)) // doubled qty, skipping rows without one
|
|
519
|
+
```
|
|
520
|
+
|
|
508
521
|
### all — True if ALL elements match
|
|
509
522
|
```cel
|
|
510
523
|
[1, 2, 3].all(x, x > 0) // true
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Effect, Option } from "effect";
|
|
2
2
|
import { evaluationError, parseError } from "./errors.js";
|
|
3
3
|
import { OPERATORS as OPS } from "./operators.js";
|
|
4
|
-
import { optionalOf, ensureOptional, bindOptionalValueType } from "./optional.js";
|
|
4
|
+
import { optionalOf, ensureOptional, ensureOptionalType, bindOptionalValueType } from "./optional.js";
|
|
5
5
|
const identity = (x) => x;
|
|
6
6
|
function assertIdentifier(node, message) {
|
|
7
7
|
if (node.op === "id") return node.args;
|
|
@@ -294,6 +294,68 @@ function createFindExpander() {
|
|
|
294
294
|
};
|
|
295
295
|
};
|
|
296
296
|
}
|
|
297
|
+
function createFilterMapExpander() {
|
|
298
|
+
const functionDesc = "filterMap(var, body)";
|
|
299
|
+
const invalidMsg = `${functionDesc} invalid iteration variable`;
|
|
300
|
+
const invalidBody = `${functionDesc} body`;
|
|
301
|
+
function typeCheck(chk, macro, ctx) {
|
|
302
|
+
const iterType = chk.check(macro.receiver, ctx);
|
|
303
|
+
let elemType;
|
|
304
|
+
if (iterType.kind === "dyn") elemType = iterType;
|
|
305
|
+
else if (iterType.kind === "list") elemType = iterType.valueType;
|
|
306
|
+
else if (iterType.kind === "map") elemType = iterType.keyType;
|
|
307
|
+
else {
|
|
308
|
+
throw chk.createError(
|
|
309
|
+
"invalid_comprehension_range",
|
|
310
|
+
`Expression of type '${iterType}' cannot be range of filterMap (must be list, map, or dynamic).`,
|
|
311
|
+
macro.receiver
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
macro.iterCtx = ctx.forkWithVariable(macro.iterVarName, elemType);
|
|
315
|
+
const bodyType = ensureOptionalType(chk, macro.body, macro.iterCtx, invalidBody);
|
|
316
|
+
return chk.registry.getListType(bindOptionalValueType(chk, bodyType));
|
|
317
|
+
}
|
|
318
|
+
function evaluate(ev, macro, ctx) {
|
|
319
|
+
return Effect.gen(function* () {
|
|
320
|
+
let items = yield* ev.eval(macro.receiver, ctx);
|
|
321
|
+
if (!Array.isArray(items)) {
|
|
322
|
+
if (items instanceof Set) items = [...items];
|
|
323
|
+
else if (items instanceof Map) items = [...items.keys()];
|
|
324
|
+
else if (items && typeof items === "object") items = Object.keys(items);
|
|
325
|
+
else {
|
|
326
|
+
return yield* Effect.fail(
|
|
327
|
+
ev.createError(
|
|
328
|
+
"invalid_comprehension_range",
|
|
329
|
+
`Expression of type '${ev.debugType(items)}' cannot be range of filterMap.`,
|
|
330
|
+
macro.receiver
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const iterCtx = macro.iterCtx.reuse(ctx);
|
|
336
|
+
const arr = items;
|
|
337
|
+
const out = [];
|
|
338
|
+
for (let i = 0; i < arr.length; i++) {
|
|
339
|
+
const result = yield* ev.eval(macro.body, iterCtx.setIterValue(arr[i], ev));
|
|
340
|
+
const optional = ensureOptional(result, macro.body, invalidBody);
|
|
341
|
+
if (Option.isSome(optional)) out.push(optional.value);
|
|
342
|
+
}
|
|
343
|
+
return out;
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return (opts) => {
|
|
347
|
+
const { args, receiver, ast } = opts;
|
|
348
|
+
return {
|
|
349
|
+
ast,
|
|
350
|
+
receiver,
|
|
351
|
+
iterVarName: assertIdentifier(args[0], invalidMsg),
|
|
352
|
+
body: args[1],
|
|
353
|
+
iterCtx: void 0,
|
|
354
|
+
evaluate,
|
|
355
|
+
typeCheck
|
|
356
|
+
};
|
|
357
|
+
};
|
|
358
|
+
}
|
|
297
359
|
function createHasExpander() {
|
|
298
360
|
const invalidHasArgument = "has() invalid argument";
|
|
299
361
|
function evaluate(ev, macro, ctx) {
|
|
@@ -395,6 +457,7 @@ export function registerMacros(registry) {
|
|
|
395
457
|
functionOverload("list.map(ast, ast, ast): list<dyn>", createMapExpander(true));
|
|
396
458
|
functionOverload("list.filter(ast, ast): list<dyn>", createFilterExpander());
|
|
397
459
|
functionOverload("list.find(ast, ast): optional<dyn>", createFindExpander());
|
|
460
|
+
functionOverload("list.filterMap(ast, ast): list<dyn>", createFilterMapExpander());
|
|
398
461
|
class CelNamespace {
|
|
399
462
|
#kind = "cel";
|
|
400
463
|
toString() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shwfed/config",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.11",
|
|
4
4
|
"description": "Configurable UI for SHWFED",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"lint": "eslint .",
|
|
37
37
|
"test": "vitest run",
|
|
38
38
|
"test:watch": "vitest watch",
|
|
39
|
+
"test:ai-eval": "AI_EVAL=1 vitest run --config vitest.eval.config.mjs",
|
|
39
40
|
"test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|