@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.
Files changed (35) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.d.vue.ts +68 -68
  3. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.vue.d.ts +68 -68
  4. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.d.vue.ts +68 -68
  5. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.vue.d.ts +68 -68
  6. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/schema.d.ts +34 -34
  7. package/dist/runtime/components/operations/2026-06-11/com.shwfed.operation.http.request.json/schema.d.ts +8 -2
  8. package/dist/runtime/components/operations/2026-06-11/com.shwfed.operation.http.request.json/schema.js +5 -4
  9. package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.http.download/schema.d.ts +8 -2
  10. package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.http.download/schema.js +5 -4
  11. package/dist/runtime/components/sidebar/index.vue +1 -1
  12. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  13. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  14. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  15. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  16. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  17. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  18. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  19. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  20. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  21. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  22. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  23. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  24. package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/config.d.vue.ts +10 -0
  25. package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/config.vue +73 -0
  26. package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/config.vue.d.ts +10 -0
  27. package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/schema.d.ts +63 -0
  28. package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/schema.js +157 -0
  29. package/dist/runtime/components/table/index.vue +86 -12
  30. package/dist/runtime/components/table/schema.d.ts +68 -68
  31. package/dist/runtime/components/table/utils/resolve.d.ts +42 -0
  32. package/dist/runtime/components/table/utils/resolve.js +11 -3
  33. package/dist/runtime/share/define-registry.d.ts +11 -0
  34. package/dist/runtime/share/define-registry.js +10 -3
  35. package/package.json +1 -1
@@ -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 {};
@@ -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 = (config.value?.columns ?? []).filter((c) => c.grow === true && userSizing[c.id] !== void 0);
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
- (config.value?.columns ?? []).map((c) => [c.id, c.sortKey]).filter((e) => typeof e[1] === "string" && e[1].length > 0)
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 cfg.columns) {
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 config2.columns) {
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(config2),
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(config2) {
688
+ function buildCustomizationContext() {
615
689
  return {
616
- columnIds: new Set(config2.columns.map((c) => c.id)),
617
- pageSizes: config2.pagination?.pageSizes ?? [],
618
- paginationEnabled: config2.pagination !== void 0
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="config?.id"
828
- :config="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
- if (typeof mod.toColumnDef !== "function") {
23
- throw new TypeError(`[shwfed-table] column "${type}@${date}" is missing toColumnDef export`);
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