@shwfed/config 3.0.0 → 3.0.2
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/config/blocks/2026-05-06/com.shwfed.block.table/config.d.vue.ts +68 -68
- package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.vue.d.ts +68 -68
- package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.d.vue.ts +68 -68
- package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.vue.d.ts +68 -68
- package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/schema.d.ts +34 -34
- package/dist/runtime/components/operations/2026-06-11/com.shwfed.operation.http.request.json/schema.d.ts +8 -2
- package/dist/runtime/components/operations/2026-06-11/com.shwfed.operation.http.request.json/schema.js +5 -4
- package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.http.download/schema.d.ts +8 -2
- package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.http.download/schema.js +5 -4
- package/dist/runtime/components/sidebar/index.vue +1 -1
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
- package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
- package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
- package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
- package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/config.d.vue.ts +10 -0
- package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/config.vue +73 -0
- package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/config.vue.d.ts +10 -0
- package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/schema.d.ts +63 -0
- package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/schema.js +157 -0
- package/dist/runtime/components/table/index.vue +86 -12
- package/dist/runtime/components/table/schema.d.ts +68 -68
- package/dist/runtime/components/table/utils/resolve.d.ts +42 -0
- package/dist/runtime/components/table/utils/resolve.js +11 -3
- package/dist/runtime/share/define-registry.d.ts +11 -0
- package/dist/runtime/share/define-registry.js +10 -3
- package/package.json +1 -1
package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/schema.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
import type { Environment } from '../../../../../vendor/cel-js/lib/index.js';
|
|
3
|
+
import type { ColumnExpand } from '../../../utils/resolve.js';
|
|
4
|
+
export declare const type: "com.shwfed.table.column.dynamic";
|
|
5
|
+
export declare const compatibilityDate: "2026-07-12";
|
|
6
|
+
export declare const metadata: {
|
|
7
|
+
readonly name: "动态列";
|
|
8
|
+
readonly icon: "fluent:table-lightning-20-regular";
|
|
9
|
+
readonly description: import("../../../../../share/markdown.js").Markdown;
|
|
10
|
+
};
|
|
11
|
+
declare const DynColumnKind: Schema.Literal<["text", "number", "date"]>;
|
|
12
|
+
export type DynColumnKindValue = Schema.Schema.Type<typeof DynColumnKind>;
|
|
13
|
+
/**
|
|
14
|
+
* The narrow, module-owned DTO the `transform` must emit per column. Keeping
|
|
15
|
+
* this contract closed (vs. exposing the internal column union) means internal
|
|
16
|
+
* schema evolution never breaks app config, the transform stays terse, and dyn
|
|
17
|
+
* cols auto-upgrade to the current renderer. Each element is decoded against
|
|
18
|
+
* this at runtime (partial-drop, loud) — the strongest static guarantee is
|
|
19
|
+
* only that `transform` returns `list<dyn>`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const DynColumnDescriptor: Schema.Struct<{
|
|
22
|
+
key: Schema.filter<typeof Schema.String>;
|
|
23
|
+
header: typeof Schema.String;
|
|
24
|
+
kind: Schema.Literal<["text", "number", "date"]>;
|
|
25
|
+
accessor: Schema.filter<typeof Schema.String>;
|
|
26
|
+
/** px column width; defaults to 150. */
|
|
27
|
+
width: Schema.optional<typeof Schema.Number>;
|
|
28
|
+
/** Client-side sort by accessed value. */
|
|
29
|
+
sortable: Schema.optional<typeof Schema.Boolean>;
|
|
30
|
+
/** Backend sort field; only needed for server-side sort. */
|
|
31
|
+
sortKey: Schema.optional<typeof Schema.String>;
|
|
32
|
+
/** Default-hidden; the user can re-show it. */
|
|
33
|
+
hidden: Schema.optional<typeof Schema.Boolean>;
|
|
34
|
+
/** Header hover text; wrapped into a Locale by `expand`. */
|
|
35
|
+
tooltip: Schema.optional<typeof Schema.String>;
|
|
36
|
+
/** Text / number alignment. */
|
|
37
|
+
align: Schema.optional<Schema.Literal<["left", "center", "right"]>>;
|
|
38
|
+
}>;
|
|
39
|
+
export type DynColumnDescriptorValue = Schema.Schema.Type<typeof DynColumnDescriptor>;
|
|
40
|
+
export declare function schema(configure: (env: Environment) => void): Schema.Struct<{
|
|
41
|
+
request: Schema.Schema<string, string, never>;
|
|
42
|
+
transform: Schema.Schema<string, string, never>;
|
|
43
|
+
id: Schema.refine<string, typeof Schema.String>;
|
|
44
|
+
groupId: Schema.optional<typeof Schema.UUID>;
|
|
45
|
+
type: Schema.Literal<["com.shwfed.table.column.dynamic"]>;
|
|
46
|
+
compatibilityDate: Schema.Literal<["2026-07-12"]>;
|
|
47
|
+
}>;
|
|
48
|
+
export type Value = Schema.Schema.Type<ReturnType<typeof schema>>;
|
|
49
|
+
export declare function defaults(): Partial<Value>;
|
|
50
|
+
/**
|
|
51
|
+
* Runtime generator. Evaluates the block's `transform` against the fetched
|
|
52
|
+
* `json`, then decodes each element against `DynColumnDescriptor` — keeping the
|
|
53
|
+
* valid, dropping the invalid / duplicate / colliding (partial-drop, loud).
|
|
54
|
+
* Three failure layers (see the design doc):
|
|
55
|
+
* 1. transform throws / returns a non-list → the whole block yields 0 columns
|
|
56
|
+
* (other columns render normally).
|
|
57
|
+
* 2. individual elements malformed / duplicate `key` / `key` colliding with a
|
|
58
|
+
* static id → drop that element alone, first-wins on keys.
|
|
59
|
+
* 3. per-row accessor eval throws → handled downstream (`translateColumn`'s
|
|
60
|
+
* `accessorFn` try/catches per row) — not here.
|
|
61
|
+
*/
|
|
62
|
+
export declare const expand: ColumnExpand;
|
|
63
|
+
export {};
|
package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/schema.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Effect, Either, Schema } from "effect";
|
|
2
|
+
import { Expression, HttpRequestResult } from "../../../../../share/expression.js";
|
|
3
|
+
import { columnIdentityFields } from "../../../utils/shared.js";
|
|
4
|
+
import { md } from "../../../../../share/markdown.js";
|
|
5
|
+
export const type = "com.shwfed.table.column.dynamic";
|
|
6
|
+
export const compatibilityDate = "2026-07-12";
|
|
7
|
+
export const metadata = {
|
|
8
|
+
name: "\u52A8\u6001\u5217",
|
|
9
|
+
icon: "fluent:table-lightning-20-regular",
|
|
10
|
+
description: md`
|
|
11
|
+
运行时动态列:由服务端返回的**列定义列表**生成一组普通列,而非在设计时逐列编写。
|
|
12
|
+
适用于列集合随租户 / 渠道变化、无法预先固定的场景。本块自身不渲染单元格,只是一个
|
|
13
|
+
生成器 —— 它产出的每一列都是普通的文本 / 数值 / 日期列,可照常调整宽度、排序、隐藏。
|
|
14
|
+
`
|
|
15
|
+
};
|
|
16
|
+
const DynColumnKind = Schema.Literal("text", "number", "date");
|
|
17
|
+
const DynColumnAlign = Schema.Literal("left", "center", "right");
|
|
18
|
+
export const DynColumnDescriptor = Schema.Struct({
|
|
19
|
+
// Column `id` — stable, unique within the table; the identity customization /
|
|
20
|
+
// sort / selection key off. For the legacy app this is `parameter_name`.
|
|
21
|
+
key: Schema.String.pipe(Schema.minLength(1)),
|
|
22
|
+
// Column label; wrapped into a Locale by `expand`.
|
|
23
|
+
header: Schema.String,
|
|
24
|
+
// Selects the leaf renderer (closed vocabulary).
|
|
25
|
+
kind: DynColumnKind,
|
|
26
|
+
// Per-row value CEL (`row` / `index` in scope) — the only field carrying
|
|
27
|
+
// app-specific read logic. Treated as an opaque per-row expression.
|
|
28
|
+
accessor: Schema.String.pipe(Schema.minLength(1)),
|
|
29
|
+
/** px column width; defaults to 150. */
|
|
30
|
+
width: Schema.optional(Schema.Number),
|
|
31
|
+
/** Client-side sort by accessed value. */
|
|
32
|
+
sortable: Schema.optional(Schema.Boolean),
|
|
33
|
+
/** Backend sort field; only needed for server-side sort. */
|
|
34
|
+
sortKey: Schema.optional(Schema.String),
|
|
35
|
+
/** Default-hidden; the user can re-show it. */
|
|
36
|
+
hidden: Schema.optional(Schema.Boolean),
|
|
37
|
+
/** Header hover text; wrapped into a Locale by `expand`. */
|
|
38
|
+
tooltip: Schema.optional(Schema.String),
|
|
39
|
+
/** Text / number alignment. */
|
|
40
|
+
align: Schema.optional(DynColumnAlign)
|
|
41
|
+
});
|
|
42
|
+
const KIND_TO_COLUMN = {
|
|
43
|
+
text: { type: "com.shwfed.table.column.text", compatibilityDate: "2026-04-14" },
|
|
44
|
+
number: { type: "com.shwfed.table.column.number", compatibilityDate: "2026-04-14" },
|
|
45
|
+
date: { type: "com.shwfed.table.column.date", compatibilityDate: "2026-04-14" }
|
|
46
|
+
};
|
|
47
|
+
const DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
|
|
48
|
+
const DEFAULT_WIDTH = 150;
|
|
49
|
+
export function schema(configure) {
|
|
50
|
+
const CelRequest = Expression({ configure, resultType: HttpRequestResult });
|
|
51
|
+
const CelTransform = Expression({
|
|
52
|
+
configure: (env) => {
|
|
53
|
+
configure(env);
|
|
54
|
+
const declared = new Set(env.getDefinitions().variables.map((v) => v.name));
|
|
55
|
+
if (!declared.has("json")) {
|
|
56
|
+
env.registerVariable("json", "optional<dyn>", {
|
|
57
|
+
label: "HTTP \u54CD\u5E94\u4F53",
|
|
58
|
+
description: "\u5217\u5B9A\u4E49\u63A5\u53E3\u7684\u54CD\u5E94\u4F53\uFF08\u5DF2\u89E3\u6790 JSON\uFF09\uFF1B\u672A\u83B7\u53D6\u5230\u65F6\u4E3A none\uFF0C\u901A\u8FC7 `json.orValue(...)` \u89E3\u5305"
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (!declared.has("query")) {
|
|
62
|
+
env.registerVariable("query", "dyn", {
|
|
63
|
+
label: "\u641C\u7D22\u6761\u4EF6",
|
|
64
|
+
description: "\u641C\u7D22\u6761\u4EF6\u5F53\u524D\u72B6\u6001\uFF1B\u672A\u914D\u7F6E\u641C\u7D22\u6761\u4EF6\u65F6\u4E3A\u7A7A\u5BF9\u8C61"
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
resultType: (t) => t.startsWith("list") || t === "dyn" || t.startsWith("optional")
|
|
69
|
+
});
|
|
70
|
+
return Schema.Struct({
|
|
71
|
+
type: Schema.Literal(type),
|
|
72
|
+
compatibilityDate: Schema.Literal(compatibilityDate),
|
|
73
|
+
...columnIdentityFields(),
|
|
74
|
+
request: CelRequest.annotations({
|
|
75
|
+
title: "\u5217\u5B9A\u4E49\u8BF7\u6C42",
|
|
76
|
+
description: md`
|
|
77
|
+
获取列定义列表的 HTTP 请求表达式(与数据源 \`request\` 同为 \`HttpRequest\` 形状)。
|
|
78
|
+
该请求**独立于表格状态** —— 作用域内没有 \`query\` / \`pageIndex\` / \`pageSize\` /
|
|
79
|
+
\`sorts\`,列集合不会随搜索、翻页、排序而变化。
|
|
80
|
+
`
|
|
81
|
+
}),
|
|
82
|
+
transform: CelTransform.annotations({
|
|
83
|
+
title: "\u5217\u5B9A\u4E49\u6620\u5C04",
|
|
84
|
+
description: md`
|
|
85
|
+
将 \`json\`(列定义响应体,用 \`json.orValue([])\` 解包)映射为一组列描述(\`list\`)。
|
|
86
|
+
每个元素需符合固定字段(注意 CEL 映射字面量的键要加引号):\`key\`(列唯一标识)、
|
|
87
|
+
\`header\`(标题)、\`kind\`(\`"text"\`/\`"number"\`/\`"date"\`)、\`accessor\`(读取该列每行取值的
|
|
88
|
+
CEL,\`row\` 在作用域内);可选 \`width\` / \`sortable\` / \`sortKey\` / \`hidden\` / \`tooltip\` /
|
|
89
|
+
\`align\`。**所有与后端数据契约相关的知识都在这里**。
|
|
90
|
+
`
|
|
91
|
+
})
|
|
92
|
+
}).annotations({ title: "DynamicColumns", description: "\u8FD0\u884C\u65F6\u52A8\u6001\u5217\uFF08\u7531\u670D\u52A1\u7AEF\u5217\u5B9A\u4E49\u751F\u6210\uFF09" });
|
|
93
|
+
}
|
|
94
|
+
export function defaults() {
|
|
95
|
+
return { request: "", transform: "" };
|
|
96
|
+
}
|
|
97
|
+
function wrapLocale(text) {
|
|
98
|
+
return [{ locale: "zh", message: text }];
|
|
99
|
+
}
|
|
100
|
+
function toColumnValue(d, block) {
|
|
101
|
+
const pin = KIND_TO_COLUMN[d.kind];
|
|
102
|
+
const col = {
|
|
103
|
+
id: d.key,
|
|
104
|
+
type: pin.type,
|
|
105
|
+
compatibilityDate: pin.compatibilityDate,
|
|
106
|
+
title: wrapLocale(d.header),
|
|
107
|
+
accessor: d.accessor,
|
|
108
|
+
size: d.width ?? DEFAULT_WIDTH
|
|
109
|
+
};
|
|
110
|
+
if (block.groupId !== void 0) col.groupId = block.groupId;
|
|
111
|
+
if (d.sortable) col.enableSorting = true;
|
|
112
|
+
if (d.sortKey !== void 0 && d.sortKey.length > 0) col.sortKey = d.sortKey;
|
|
113
|
+
if (d.hidden) col.hidden = true;
|
|
114
|
+
if (d.tooltip !== void 0 && d.tooltip.length > 0) col.tooltip = wrapLocale(d.tooltip);
|
|
115
|
+
if (d.align !== void 0) col.align = d.align;
|
|
116
|
+
if (d.kind === "date") col.format = DEFAULT_DATE_FORMAT;
|
|
117
|
+
return col;
|
|
118
|
+
}
|
|
119
|
+
const decodeDescriptor = Schema.decodeUnknownEither(DynColumnDescriptor);
|
|
120
|
+
export const expand = (value, { json, $cel, context, seen }) => {
|
|
121
|
+
const block = value;
|
|
122
|
+
if (!block.transform || block.transform.trim().length === 0) return [];
|
|
123
|
+
let listRaw;
|
|
124
|
+
try {
|
|
125
|
+
listRaw = Effect.runSync($cel(block.transform, { ...context, json }));
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.warn("[shwfed-table] dynamic columns transform failed", e);
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
if (!Array.isArray(listRaw)) {
|
|
131
|
+
console.warn("[shwfed-table] dynamic columns transform did not return a list", listRaw);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const out = [];
|
|
135
|
+
const claimed = new Set(seen);
|
|
136
|
+
let dropped = 0;
|
|
137
|
+
for (const element of listRaw) {
|
|
138
|
+
const decoded = decodeDescriptor(element);
|
|
139
|
+
if (Either.isLeft(decoded)) {
|
|
140
|
+
dropped++;
|
|
141
|
+
console.warn("[shwfed-table] dropped invalid dynamic column descriptor", element);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const descriptor = decoded.right;
|
|
145
|
+
if (claimed.has(descriptor.key)) {
|
|
146
|
+
dropped++;
|
|
147
|
+
console.warn("[shwfed-table] dropped duplicate/colliding dynamic column key", descriptor.key);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
claimed.add(descriptor.key);
|
|
151
|
+
out.push(toColumnValue(descriptor, block));
|
|
152
|
+
}
|
|
153
|
+
if (dropped > 0) {
|
|
154
|
+
console.warn(`[shwfed-table] ${dropped} of ${listRaw.length} dynamic columns dropped`);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
};
|
|
@@ -110,6 +110,7 @@ const inheritedContext = injectCELContext();
|
|
|
110
110
|
const viewportRef = ref(null);
|
|
111
111
|
const containerRef = computed(() => viewportRef.value?.viewportElement ?? null);
|
|
112
112
|
const appliedInitialStateKey = ref("");
|
|
113
|
+
const descriptorJson = ref({});
|
|
113
114
|
const frozenGrowColumns = ref(/* @__PURE__ */ new Set());
|
|
114
115
|
function isEffectiveGrow(column) {
|
|
115
116
|
return !!column.columnDef.meta?.grow && !frozenGrowColumns.value.has(column.id);
|
|
@@ -136,7 +137,7 @@ let resizeGestureSeen = false;
|
|
|
136
137
|
function restoreFrozenGrowColumns() {
|
|
137
138
|
const userSizing = customization.snapshot()?.columnSizing;
|
|
138
139
|
if (!userSizing) return;
|
|
139
|
-
const grows =
|
|
140
|
+
const grows = effectiveColumns.value.filter((c) => c.grow === true && userSizing[c.id] !== void 0);
|
|
140
141
|
if (grows.length === 0) return;
|
|
141
142
|
const next = new Set(frozenGrowColumns.value);
|
|
142
143
|
grows.forEach((c) => next.add(c.id));
|
|
@@ -166,10 +167,44 @@ function mergeCelContext(local) {
|
|
|
166
167
|
}
|
|
167
168
|
function currentSorts() {
|
|
168
169
|
const sortKeyById = new Map(
|
|
169
|
-
|
|
170
|
+
// `effectiveColumns`, not `config.columns`: a dyn column's `sortKey` lives
|
|
171
|
+
// on the generated child, absent from the config's generator block.
|
|
172
|
+
effectiveColumns.value.map((c) => [c.id, c.sortKey]).filter((e) => typeof e[1] === "string" && e[1].length > 0)
|
|
170
173
|
);
|
|
171
174
|
return tableApi.getState().sorting.map((s) => ({ key: sortKeyById.get(s.id), mode: s.desc ? "desc" : "asc" })).filter((s) => s.key !== void 0);
|
|
172
175
|
}
|
|
176
|
+
const effectiveColumns = computed(() => {
|
|
177
|
+
const cols = config.value?.columns;
|
|
178
|
+
if (!cols) return [];
|
|
179
|
+
const out = [];
|
|
180
|
+
const seen = /* @__PURE__ */ new Set();
|
|
181
|
+
for (const col of cols) {
|
|
182
|
+
const entry = findColumn(col.type, col.compatibilityDate);
|
|
183
|
+
if (!entry?.expand) {
|
|
184
|
+
out.push(col);
|
|
185
|
+
seen.add(col.id);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const children = entry.expand(col, {
|
|
189
|
+
// `Option.none()` (not bare `undefined`) before the fetch lands, so the
|
|
190
|
+
// transform's `json.orValue(...)` yields an empty list rather than
|
|
191
|
+
// throwing on every pre-fetch render.
|
|
192
|
+
json: descriptorJson.value[col.id] ?? Option.none(),
|
|
193
|
+
$cel,
|
|
194
|
+
// Inherited host vars + inert `query` (pinned empty so the column set is
|
|
195
|
+
// independent of live search); `expand` injects `json` on top.
|
|
196
|
+
context: { ...celBindings(inheritedContext), query: {} },
|
|
197
|
+
getLocaleText,
|
|
198
|
+
seen
|
|
199
|
+
});
|
|
200
|
+
for (const child of children) {
|
|
201
|
+
out.push(child);
|
|
202
|
+
seen.add(child.id);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
});
|
|
207
|
+
const customizationConfig = computed(() => config.value ? { ...config.value, columns: effectiveColumns.value } : void 0);
|
|
173
208
|
function translateColumn(column) {
|
|
174
209
|
const entry = findColumn(column.type, column.compatibilityDate);
|
|
175
210
|
if (!entry) {
|
|
@@ -208,7 +243,7 @@ const columns = computed(() => {
|
|
|
208
243
|
const groupById = new Map(cfg.groups?.map((g) => [g.id, g]) ?? []);
|
|
209
244
|
const out = [];
|
|
210
245
|
let current = null;
|
|
211
|
-
for (const col of
|
|
246
|
+
for (const col of effectiveColumns.value) {
|
|
212
247
|
const def = translateColumn(col);
|
|
213
248
|
const group = col.groupId ? groupById.get(col.groupId) : void 0;
|
|
214
249
|
if (!group) {
|
|
@@ -529,6 +564,7 @@ provideEventTarget(tableInstanceId, {
|
|
|
529
564
|
});
|
|
530
565
|
const queryRef = ref(null);
|
|
531
566
|
onMounted(async () => {
|
|
567
|
+
void fetchDescriptors();
|
|
532
568
|
await queryRef.value?.seeded;
|
|
533
569
|
persistedQuery.restore();
|
|
534
570
|
await fetchDataSource();
|
|
@@ -574,6 +610,44 @@ watchDebounced(
|
|
|
574
610
|
},
|
|
575
611
|
{ debounce: 200 }
|
|
576
612
|
);
|
|
613
|
+
function dynColumnBlocks() {
|
|
614
|
+
return (config.value?.columns ?? []).filter((c) => {
|
|
615
|
+
const entry = findColumn(c.type, c.compatibilityDate);
|
|
616
|
+
return typeof entry?.expand === "function" && typeof c.request === "string" && c.request.length > 0;
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
async function fetchDescriptors() {
|
|
620
|
+
for (const block of dynColumnBlocks()) {
|
|
621
|
+
const expr = block.request;
|
|
622
|
+
const program = Effect.gen(function* () {
|
|
623
|
+
return yield* fetchJsonOption(yield* $cel(expr, celBindings(inheritedContext)));
|
|
624
|
+
});
|
|
625
|
+
try {
|
|
626
|
+
const jsonOpt = await Effect.runPromise(Effect.provide(program, Fetch.layer));
|
|
627
|
+
descriptorJson.value = { ...descriptorJson.value, [block.id]: jsonOpt };
|
|
628
|
+
} catch (e) {
|
|
629
|
+
console.warn("[shwfed-table] dynamic column descriptor fetch failed", e);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const descriptorRequestSignature = computed(() => {
|
|
634
|
+
const blocks = dynColumnBlocks();
|
|
635
|
+
if (blocks.length === 0) return null;
|
|
636
|
+
const parts = [];
|
|
637
|
+
for (const block of blocks) {
|
|
638
|
+
const expr = block.request;
|
|
639
|
+
try {
|
|
640
|
+
const req = asRequest(Effect.runSync($cel(expr, celScope(inheritedContext))));
|
|
641
|
+
parts.push(`${block.id}:${Option.isSome(req) ? JSON.stringify(req.value.describe()) : "none"}`);
|
|
642
|
+
} catch {
|
|
643
|
+
parts.push(`${block.id}:err`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return parts.join("|");
|
|
647
|
+
});
|
|
648
|
+
watch(descriptorRequestSignature, (next, prev) => {
|
|
649
|
+
if (next !== null && next !== prev) void fetchDescriptors();
|
|
650
|
+
});
|
|
577
651
|
const sortedRowIndexMap = computed(() => {
|
|
578
652
|
const map = /* @__PURE__ */ new Map();
|
|
579
653
|
tableApi.getSortedRowModel().flatRows.forEach((row, i) => {
|
|
@@ -584,11 +658,11 @@ const sortedRowIndexMap = computed(() => {
|
|
|
584
658
|
function getDisplayIndex(row) {
|
|
585
659
|
return sortedRowIndexMap.value.get(row.id) ?? row.index;
|
|
586
660
|
}
|
|
587
|
-
watch([config, customization.revision], ([config2]) => {
|
|
661
|
+
watch([config, customization.revision, descriptorJson], ([config2]) => {
|
|
588
662
|
if (!config2) return;
|
|
589
663
|
const initialState = config2.initialState;
|
|
590
664
|
const hiddenSeed = {};
|
|
591
|
-
for (const col of
|
|
665
|
+
for (const col of effectiveColumns.value) {
|
|
592
666
|
if (col.hidden) hiddenSeed[col.id] = false;
|
|
593
667
|
}
|
|
594
668
|
const hasSeed = Object.keys(hiddenSeed).length > 0;
|
|
@@ -596,7 +670,7 @@ watch([config, customization.revision], ([config2]) => {
|
|
|
596
670
|
const { state: merged, pageSize: pageSizeOverride } = mergeCustomization(
|
|
597
671
|
effective,
|
|
598
672
|
customization.snapshot(),
|
|
599
|
-
buildCustomizationContext(
|
|
673
|
+
buildCustomizationContext(),
|
|
600
674
|
{ explicit: customization.wasCustomized() }
|
|
601
675
|
);
|
|
602
676
|
const nextKey = merged ? JSON.stringify({ merged, pageSize: pageSizeOverride ?? null }) : "";
|
|
@@ -611,11 +685,11 @@ watch([config, customization.revision], ([config2]) => {
|
|
|
611
685
|
appliedInitialStateKey.value = nextKey;
|
|
612
686
|
syncPaginationPageSize();
|
|
613
687
|
}, { immediate: true });
|
|
614
|
-
function buildCustomizationContext(
|
|
688
|
+
function buildCustomizationContext() {
|
|
615
689
|
return {
|
|
616
|
-
columnIds: new Set(
|
|
617
|
-
pageSizes:
|
|
618
|
-
paginationEnabled:
|
|
690
|
+
columnIds: new Set(effectiveColumns.value.map((c) => c.id)),
|
|
691
|
+
pageSizes: config.value?.pagination?.pageSizes ?? [],
|
|
692
|
+
paginationEnabled: config.value?.pagination !== void 0
|
|
619
693
|
};
|
|
620
694
|
}
|
|
621
695
|
function onCustomizationConfirm(payload) {
|
|
@@ -824,8 +898,8 @@ export { TableConfig, createTableConfig, getColumnTechnicalKey } from "./schema"
|
|
|
824
898
|
far-right corner, outside the scroll container so it neither moves
|
|
825
899
|
on horizontal scroll nor collides with right-pinned columns. -->
|
|
826
900
|
<TableCustomization
|
|
827
|
-
v-if="
|
|
828
|
-
:config="
|
|
901
|
+
v-if="customizationConfig?.id"
|
|
902
|
+
:config="customizationConfig"
|
|
829
903
|
:table-api="tableApi"
|
|
830
904
|
@confirm="onCustomizationConfirm"
|
|
831
905
|
@reset="resetCustomization"
|
|
@@ -158,40 +158,6 @@ export declare function TableConfig(configure: (env: Environment) => void): Sche
|
|
|
158
158
|
readonly message: string;
|
|
159
159
|
}[]] | undefined;
|
|
160
160
|
}[];
|
|
161
|
-
readonly query?: {
|
|
162
|
-
readonly style?: string | undefined;
|
|
163
|
-
readonly id?: string | undefined;
|
|
164
|
-
readonly initial?: {
|
|
165
|
-
readonly data: string;
|
|
166
|
-
readonly request?: string | undefined;
|
|
167
|
-
} | undefined;
|
|
168
|
-
readonly displayName?: string | undefined;
|
|
169
|
-
readonly kind: "shwfed.component.form";
|
|
170
|
-
readonly fields: readonly any[];
|
|
171
|
-
readonly layouts: readonly {
|
|
172
|
-
readonly name: string;
|
|
173
|
-
readonly layout: {
|
|
174
|
-
readonly style?: string | undefined;
|
|
175
|
-
readonly columns: number;
|
|
176
|
-
readonly gap?: number | undefined;
|
|
177
|
-
readonly rows?: number | undefined;
|
|
178
|
-
readonly placements: {
|
|
179
|
-
readonly [x: string]: {
|
|
180
|
-
readonly area: readonly [readonly [number, number], readonly [number, number]];
|
|
181
|
-
readonly v?: "stretch" | "center" | "end" | "start" | undefined;
|
|
182
|
-
readonly h?: "stretch" | "center" | "end" | "start" | undefined;
|
|
183
|
-
};
|
|
184
|
-
};
|
|
185
|
-
};
|
|
186
|
-
readonly media?: string | undefined;
|
|
187
|
-
}[];
|
|
188
|
-
readonly readonly?: string | undefined;
|
|
189
|
-
} | undefined;
|
|
190
|
-
readonly dataSource?: {
|
|
191
|
-
readonly data: string;
|
|
192
|
-
readonly total?: string | undefined;
|
|
193
|
-
readonly request?: string | undefined;
|
|
194
|
-
} | undefined;
|
|
195
161
|
readonly actions?: {
|
|
196
162
|
readonly size: "default" | "sm" | "xs";
|
|
197
163
|
readonly style?: string | undefined;
|
|
@@ -259,6 +225,40 @@ export declare function TableConfig(configure: (env: Environment) => void): Sche
|
|
|
259
225
|
readonly variant?: "default" | "link" | "destructive" | "primary" | "ghost" | undefined;
|
|
260
226
|
}[];
|
|
261
227
|
} | undefined;
|
|
228
|
+
readonly query?: {
|
|
229
|
+
readonly style?: string | undefined;
|
|
230
|
+
readonly id?: string | undefined;
|
|
231
|
+
readonly initial?: {
|
|
232
|
+
readonly data: string;
|
|
233
|
+
readonly request?: string | undefined;
|
|
234
|
+
} | undefined;
|
|
235
|
+
readonly displayName?: string | undefined;
|
|
236
|
+
readonly kind: "shwfed.component.form";
|
|
237
|
+
readonly fields: readonly any[];
|
|
238
|
+
readonly layouts: readonly {
|
|
239
|
+
readonly name: string;
|
|
240
|
+
readonly layout: {
|
|
241
|
+
readonly style?: string | undefined;
|
|
242
|
+
readonly columns: number;
|
|
243
|
+
readonly gap?: number | undefined;
|
|
244
|
+
readonly rows?: number | undefined;
|
|
245
|
+
readonly placements: {
|
|
246
|
+
readonly [x: string]: {
|
|
247
|
+
readonly area: readonly [readonly [number, number], readonly [number, number]];
|
|
248
|
+
readonly v?: "stretch" | "center" | "end" | "start" | undefined;
|
|
249
|
+
readonly h?: "stretch" | "center" | "end" | "start" | undefined;
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
readonly media?: string | undefined;
|
|
254
|
+
}[];
|
|
255
|
+
readonly readonly?: string | undefined;
|
|
256
|
+
} | undefined;
|
|
257
|
+
readonly dataSource?: {
|
|
258
|
+
readonly data: string;
|
|
259
|
+
readonly total?: string | undefined;
|
|
260
|
+
readonly request?: string | undefined;
|
|
261
|
+
} | undefined;
|
|
262
262
|
readonly bottomActions?: {
|
|
263
263
|
readonly size: "default" | "sm" | "xs";
|
|
264
264
|
readonly style?: string | undefined;
|
|
@@ -827,40 +827,6 @@ export declare function createTableConfig(body: Omit<Schema.Schema.Type<ReturnTy
|
|
|
827
827
|
readonly message: string;
|
|
828
828
|
}[]] | undefined;
|
|
829
829
|
}[];
|
|
830
|
-
query?: {
|
|
831
|
-
readonly style?: string | undefined;
|
|
832
|
-
readonly id?: string | undefined;
|
|
833
|
-
readonly initial?: {
|
|
834
|
-
readonly data: string;
|
|
835
|
-
readonly request?: string | undefined;
|
|
836
|
-
} | undefined;
|
|
837
|
-
readonly displayName?: string | undefined;
|
|
838
|
-
readonly kind: "shwfed.component.form";
|
|
839
|
-
readonly fields: readonly any[];
|
|
840
|
-
readonly layouts: readonly {
|
|
841
|
-
readonly name: string;
|
|
842
|
-
readonly layout: {
|
|
843
|
-
readonly style?: string | undefined;
|
|
844
|
-
readonly columns: number;
|
|
845
|
-
readonly gap?: number | undefined;
|
|
846
|
-
readonly rows?: number | undefined;
|
|
847
|
-
readonly placements: {
|
|
848
|
-
readonly [x: string]: {
|
|
849
|
-
readonly area: readonly [readonly [number, number], readonly [number, number]];
|
|
850
|
-
readonly v?: "stretch" | "center" | "end" | "start" | undefined;
|
|
851
|
-
readonly h?: "stretch" | "center" | "end" | "start" | undefined;
|
|
852
|
-
};
|
|
853
|
-
};
|
|
854
|
-
};
|
|
855
|
-
readonly media?: string | undefined;
|
|
856
|
-
}[];
|
|
857
|
-
readonly readonly?: string | undefined;
|
|
858
|
-
} | undefined;
|
|
859
|
-
dataSource?: {
|
|
860
|
-
readonly data: string;
|
|
861
|
-
readonly total?: string | undefined;
|
|
862
|
-
readonly request?: string | undefined;
|
|
863
|
-
} | undefined;
|
|
864
830
|
actions?: {
|
|
865
831
|
readonly size: "default" | "sm" | "xs";
|
|
866
832
|
readonly style?: string | undefined;
|
|
@@ -928,6 +894,40 @@ export declare function createTableConfig(body: Omit<Schema.Schema.Type<ReturnTy
|
|
|
928
894
|
readonly variant?: "default" | "link" | "destructive" | "primary" | "ghost" | undefined;
|
|
929
895
|
}[];
|
|
930
896
|
} | undefined;
|
|
897
|
+
query?: {
|
|
898
|
+
readonly style?: string | undefined;
|
|
899
|
+
readonly id?: string | undefined;
|
|
900
|
+
readonly initial?: {
|
|
901
|
+
readonly data: string;
|
|
902
|
+
readonly request?: string | undefined;
|
|
903
|
+
} | undefined;
|
|
904
|
+
readonly displayName?: string | undefined;
|
|
905
|
+
readonly kind: "shwfed.component.form";
|
|
906
|
+
readonly fields: readonly any[];
|
|
907
|
+
readonly layouts: readonly {
|
|
908
|
+
readonly name: string;
|
|
909
|
+
readonly layout: {
|
|
910
|
+
readonly style?: string | undefined;
|
|
911
|
+
readonly columns: number;
|
|
912
|
+
readonly gap?: number | undefined;
|
|
913
|
+
readonly rows?: number | undefined;
|
|
914
|
+
readonly placements: {
|
|
915
|
+
readonly [x: string]: {
|
|
916
|
+
readonly area: readonly [readonly [number, number], readonly [number, number]];
|
|
917
|
+
readonly v?: "stretch" | "center" | "end" | "start" | undefined;
|
|
918
|
+
readonly h?: "stretch" | "center" | "end" | "start" | undefined;
|
|
919
|
+
};
|
|
920
|
+
};
|
|
921
|
+
};
|
|
922
|
+
readonly media?: string | undefined;
|
|
923
|
+
}[];
|
|
924
|
+
readonly readonly?: string | undefined;
|
|
925
|
+
} | undefined;
|
|
926
|
+
dataSource?: {
|
|
927
|
+
readonly data: string;
|
|
928
|
+
readonly total?: string | undefined;
|
|
929
|
+
readonly request?: string | undefined;
|
|
930
|
+
} | undefined;
|
|
931
931
|
bottomActions?: {
|
|
932
932
|
readonly size: "default" | "sm" | "xs";
|
|
933
933
|
readonly style?: string | undefined;
|
|
@@ -3,6 +3,7 @@ import type { Schema, Effect } from 'effect';
|
|
|
3
3
|
import type { ColumnDef } from '@tanstack/vue-table';
|
|
4
4
|
import type { Environment } from '../../../vendor/cel-js/lib/index.js';
|
|
5
5
|
import type { LocaleValue } from '../../../share/locale.js';
|
|
6
|
+
import type { ColumnValue } from '../schema.js';
|
|
6
7
|
type CelEvaluator = <T>(expression: string, context?: Record<string, unknown>) => Effect.Effect<T, any>;
|
|
7
8
|
type AnySchema = Schema.Schema<any, any, never>;
|
|
8
9
|
type SchemaFactory = (configure: (env: Environment) => void) => AnySchema;
|
|
@@ -29,6 +30,40 @@ export type ColumnDefDeps = Readonly<{
|
|
|
29
30
|
inheritedContext: Record<string, unknown>;
|
|
30
31
|
}>;
|
|
31
32
|
type ToColumnDef = (value: any, deps: ColumnDefDeps) => Partial<ColumnDef<unknown, unknown>>;
|
|
33
|
+
/**
|
|
34
|
+
* Capabilities a runtime column *generator* (`expand`) needs from the host.
|
|
35
|
+
* A generator column (e.g. `com.shwfed.table.column.dynamic`) renders no cell
|
|
36
|
+
* of its own — it maps a fetched payload into a list of ordinary
|
|
37
|
+
* `ColumnValue`s the *existing* `translateColumn` renders unchanged. All
|
|
38
|
+
* app-specific payload knowledge lives in the block's own CEL `transform`; the
|
|
39
|
+
* module never names the app's data contract.
|
|
40
|
+
*/
|
|
41
|
+
export type ColumnExpandDeps = Readonly<{
|
|
42
|
+
/**
|
|
43
|
+
* The block's fetched `request` response (parsed JSON), passed exactly as
|
|
44
|
+
* `dataSource.data` receives `json` — an `Option` (`None` before the fetch
|
|
45
|
+
* resolves or when the request opted out). `expand` runs the block's
|
|
46
|
+
* `transform` against it.
|
|
47
|
+
*/
|
|
48
|
+
json: unknown;
|
|
49
|
+
$cel: CelEvaluator;
|
|
50
|
+
/**
|
|
51
|
+
* Base CEL context the `transform` evaluates in (inherited host vars plus an
|
|
52
|
+
* inert `query`); `expand` injects `json` on top. Deliberately excludes
|
|
53
|
+
* table runtime state (`pageIndex` / `pageSize` / `sorts` / live `query`) so
|
|
54
|
+
* the generated column set never depends on page / search / sort.
|
|
55
|
+
*/
|
|
56
|
+
context: Record<string, unknown>;
|
|
57
|
+
getLocaleText: (value: LocaleValue | undefined) => string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Column ids already claimed by static columns / earlier-expanded blocks.
|
|
60
|
+
* `expand` MUST NOT emit a column whose id is in this set (first-wins), so
|
|
61
|
+
* a generated key never overwrites a static id and corrupts customization /
|
|
62
|
+
* sort / selection keying.
|
|
63
|
+
*/
|
|
64
|
+
seen: ReadonlySet<string>;
|
|
65
|
+
}>;
|
|
66
|
+
export type ColumnExpand = (value: any, deps: ColumnExpandDeps) => ColumnValue[];
|
|
32
67
|
/**
|
|
33
68
|
* Capabilities a column-migration function may need from the host. Mirrors
|
|
34
69
|
* the form host's `MigrationContext` so cross-host helpers can share a
|
|
@@ -91,6 +126,13 @@ export type ColumnEntry = Readonly<{
|
|
|
91
126
|
* the column is created with only `{ id, type, compatibilityDate }`.
|
|
92
127
|
*/
|
|
93
128
|
defaults?: ColumnDefaults;
|
|
129
|
+
/**
|
|
130
|
+
* Present iff this entry is a runtime column *generator* (see
|
|
131
|
+
* `ColumnSchemaModule.expand`). Such an entry carries no `toColumnDef` and no
|
|
132
|
+
* `runtime.vue`; the host expands it away before rendering rows, so
|
|
133
|
+
* `translateColumn` only ever sees the leaf columns it emits.
|
|
134
|
+
*/
|
|
135
|
+
expand?: ColumnExpand;
|
|
94
136
|
}>;
|
|
95
137
|
export declare const COLUMNS: ReadonlyArray<ColumnEntry>;
|
|
96
138
|
export declare function allColumnSchemas(configure: (env: Environment) => void): ReadonlyArray<AnySchema>;
|
|
@@ -19,10 +19,17 @@ const registry = defineRegistry({
|
|
|
19
19
|
configModules,
|
|
20
20
|
runtimeModules,
|
|
21
21
|
validateModule: (mod, type, date) => {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
const hasToColumnDef = typeof mod.toColumnDef === "function";
|
|
23
|
+
const hasExpand = typeof mod.expand === "function";
|
|
24
|
+
if (hasToColumnDef === hasExpand) {
|
|
25
|
+
throw new TypeError(
|
|
26
|
+
`[shwfed-table] column "${type}@${date}" must export exactly one of toColumnDef or expand (got ${hasToColumnDef ? "both" : "neither"})`
|
|
27
|
+
);
|
|
24
28
|
}
|
|
25
29
|
},
|
|
30
|
+
// Generator (`expand`) entries have no cell renderer, so they need no
|
|
31
|
+
// sibling `runtime.vue`. Every other column keeps requiring one.
|
|
32
|
+
runtimeOptional: (mod) => typeof mod.expand === "function",
|
|
26
33
|
// Deprecation rules mirror the form host:
|
|
27
34
|
// (a) date-versioned: another entry shares this entry's `type` with a
|
|
28
35
|
// strictly greater `compatibilityDate`;
|
|
@@ -77,7 +84,8 @@ export const COLUMNS = registry.ENTRIES.map((e) => ({
|
|
|
77
84
|
...e.supersededBy !== void 0 ? { supersededBy: e.supersededBy } : {},
|
|
78
85
|
...e.module.migrate !== void 0 ? { migrate: e.module.migrate } : {},
|
|
79
86
|
...e.module.migrateFrom !== void 0 ? { migrateFrom: e.module.migrateFrom } : {},
|
|
80
|
-
...e.module.defaults !== void 0 ? { defaults: e.module.defaults } : {}
|
|
87
|
+
...e.module.defaults !== void 0 ? { defaults: e.module.defaults } : {},
|
|
88
|
+
...e.module.expand !== void 0 ? { expand: e.module.expand } : {}
|
|
81
89
|
}));
|
|
82
90
|
const migrationTargets = (() => {
|
|
83
91
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -36,6 +36,17 @@ export type RegistryOptions<TModule extends RegistryModuleBase, TEntryExtras ext
|
|
|
36
36
|
* to be a function.
|
|
37
37
|
*/
|
|
38
38
|
validateModule?: (mod: TModule, type: string, date: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Optional predicate deciding whether a given module may ship WITHOUT a
|
|
41
|
+
* sibling `runtime.vue`. Returns true → the runtime lookup is tolerant and
|
|
42
|
+
* the entry's `runtime` holds `undefined` (still typed `Component` for
|
|
43
|
+
* call-site ergonomics). Such an entry must therefore never be rendered —
|
|
44
|
+
* the host enforces that via `validateModule` (e.g. table's dyn-column
|
|
45
|
+
* generator has `expand` instead of `toColumnDef`/`runtime.vue` and is
|
|
46
|
+
* expanded away before any row renders). Defaults to "runtime always
|
|
47
|
+
* required", so every other host is unaffected.
|
|
48
|
+
*/
|
|
49
|
+
runtimeOptional?: (mod: TModule) => boolean;
|
|
39
50
|
/**
|
|
40
51
|
* Optional factory producing host-specific extras for each draft. Receives
|
|
41
52
|
* the full draft set first (so hosts can compute cross-entry data such as
|