@shwfed/config 2.12.18 → 3.0.1

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 (47) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.vue +17 -11
  3. package/dist/runtime/components/config/footer.vue +14 -8
  4. package/dist/runtime/components/config/index.d.vue.ts +1 -0
  5. package/dist/runtime/components/config/index.vue +30 -2
  6. package/dist/runtime/components/config/index.vue.d.ts +1 -0
  7. package/dist/runtime/components/config/use-editor.d.ts +12 -1
  8. package/dist/runtime/components/config/use-editor.js +47 -24
  9. package/dist/runtime/components/config/use-unsaved-guard.d.ts +1 -0
  10. package/dist/runtime/components/config/use-unsaved-guard.js +8 -0
  11. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.daterange/config.d.vue.ts +6 -6
  12. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.daterange/config.vue.d.ts +6 -6
  13. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.datetimerange/config.d.vue.ts +8 -8
  14. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.datetimerange/config.vue.d.ts +8 -8
  15. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.timerange/config.d.vue.ts +2 -2
  16. package/dist/runtime/components/form/fields/2026-04-27/com.shwfed.form.field.timerange/config.vue.d.ts +2 -2
  17. package/dist/runtime/components/form/fields/2026-05-24/com.shwfed.form.field.monthrange/config.d.vue.ts +6 -6
  18. package/dist/runtime/components/form/fields/2026-05-24/com.shwfed.form.field.monthrange/config.vue.d.ts +6 -6
  19. package/dist/runtime/components/modal.d.vue.ts +8 -8
  20. package/dist/runtime/components/modal.vue +9 -1
  21. package/dist/runtime/components/modal.vue.d.ts +8 -8
  22. package/dist/runtime/components/sidebar/footer.vue +13 -7
  23. package/dist/runtime/components/sidebar/index.d.vue.ts +1 -0
  24. package/dist/runtime/components/sidebar/index.vue +30 -2
  25. package/dist/runtime/components/sidebar/index.vue.d.ts +1 -0
  26. package/dist/runtime/components/sidebar/use-editor.d.ts +11 -1
  27. package/dist/runtime/components/sidebar/use-editor.js +38 -17
  28. package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/config.d.vue.ts +10 -0
  29. package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/config.vue +73 -0
  30. package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/config.vue.d.ts +10 -0
  31. package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/schema.d.ts +63 -0
  32. package/dist/runtime/components/table/columns/2026-07-12/com.shwfed.table.column.dynamic/schema.js +157 -0
  33. package/dist/runtime/components/table/index.vue +86 -12
  34. package/dist/runtime/components/table/utils/resolve.d.ts +42 -0
  35. package/dist/runtime/components/table/utils/resolve.js +11 -3
  36. package/dist/runtime/components/ui/date-picker/DatePickerInput.d.vue.ts +1 -1
  37. package/dist/runtime/components/ui/date-picker/DatePickerInput.vue.d.ts +1 -1
  38. package/dist/runtime/components/ui/date-picker/DatePickerTimeInput.d.vue.ts +1 -1
  39. package/dist/runtime/components/ui/date-picker/DatePickerTimeInput.vue.d.ts +1 -1
  40. package/dist/runtime/components/ui/date-range-picker/DateRangePickerInput.d.vue.ts +1 -1
  41. package/dist/runtime/components/ui/date-range-picker/DateRangePickerInput.vue.d.ts +1 -1
  42. package/dist/runtime/components/ui/date-range-picker/DateRangePickerTimeInput.d.vue.ts +2 -2
  43. package/dist/runtime/components/ui/date-range-picker/DateRangePickerTimeInput.vue.d.ts +2 -2
  44. package/dist/runtime/components/ui/expression-editor/ExpressionEditor.vue +4 -1
  45. package/dist/runtime/share/define-registry.d.ts +11 -0
  46. package/dist/runtime/share/define-registry.js +10 -3
  47. package/package.json +1 -1
@@ -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"
@@ -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();
@@ -28,8 +28,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
28
28
  "onUpdate:modelValue"?: ((args_0: Date | undefined) => any) | undefined;
29
29
  }>, {
30
30
  size: "sm" | "md" | "lg";
31
- clearable: boolean;
32
31
  clearIcon: string;
32
+ clearable: boolean;
33
33
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
34
34
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
35
35
  declare const _default: typeof __VLS_export;
@@ -28,8 +28,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
28
28
  "onUpdate:modelValue"?: ((args_0: Date | undefined) => any) | undefined;
29
29
  }>, {
30
30
  size: "sm" | "md" | "lg";
31
- clearable: boolean;
32
31
  clearIcon: string;
32
+ clearable: boolean;
33
33
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
34
34
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
35
35
  declare const _default: typeof __VLS_export;
@@ -22,8 +22,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
22
22
  size: "sm" | "md" | "lg";
23
23
  granularity: "hour" | "minute" | "second";
24
24
  hourCycle: 12 | 24;
25
- clearable: boolean;
26
25
  clearIcon: string;
26
+ clearable: boolean;
27
27
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
28
  declare const _default: typeof __VLS_export;
29
29
  export default _default;
@@ -22,8 +22,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
22
22
  size: "sm" | "md" | "lg";
23
23
  granularity: "hour" | "minute" | "second";
24
24
  hourCycle: 12 | 24;
25
- clearable: boolean;
26
25
  clearIcon: string;
26
+ clearable: boolean;
27
27
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
28
  declare const _default: typeof __VLS_export;
29
29
  export default _default;
@@ -29,8 +29,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
29
29
  "onUpdate:endValue"?: ((args_0: Date | undefined) => any) | undefined;
30
30
  }>, {
31
31
  size: "sm" | "md" | "lg";
32
- clearable: boolean;
33
32
  clearIcon: string;
33
+ clearable: boolean;
34
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
35
35
  declare const _default: typeof __VLS_export;
36
36
  export default _default;
@@ -29,8 +29,8 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
29
29
  "onUpdate:endValue"?: ((args_0: Date | undefined) => any) | undefined;
30
30
  }>, {
31
31
  size: "sm" | "md" | "lg";
32
- clearable: boolean;
33
32
  clearIcon: string;
33
+ clearable: boolean;
34
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
35
35
  declare const _default: typeof __VLS_export;
36
36
  export default _default;
@@ -26,9 +26,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
26
26
  size: "sm" | "md" | "lg";
27
27
  granularity: "hour" | "minute" | "second";
28
28
  hourCycle: 12 | 24;
29
- clearable: boolean;
30
- clearIcon: string;
31
29
  rangeSeparatorIcon: string;
30
+ clearIcon: string;
31
+ clearable: boolean;
32
32
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
33
  declare const _default: typeof __VLS_export;
34
34
  export default _default;
@@ -26,9 +26,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
26
26
  size: "sm" | "md" | "lg";
27
27
  granularity: "hour" | "minute" | "second";
28
28
  hourCycle: 12 | 24;
29
- clearable: boolean;
30
- clearIcon: string;
31
29
  rangeSeparatorIcon: string;
30
+ clearIcon: string;
31
+ clearable: boolean;
32
32
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
33
33
  declare const _default: typeof __VLS_export;
34
34
  export default _default;
@@ -119,6 +119,9 @@ const showAddon = computed(() => hasVars.value || !!resultTypeLabel.value || !!s
119
119
  const addonAlign = computed(
120
120
  () => props.multiline ? "block-end" : "inline-end"
121
121
  );
122
+ const leadingAlign = computed(
123
+ () => props.multiline ? "block-start" : "inline-start"
124
+ );
122
125
  </script>
123
126
 
124
127
  <template>
@@ -126,7 +129,7 @@ const addonAlign = computed(
126
129
  <InputGroup>
127
130
  <InputGroupAddon
128
131
  v-if="$slots.leading"
129
- align="inline-start"
132
+ :align="leadingAlign"
130
133
  >
131
134
  <slot name="leading" />
132
135
  </InputGroupAddon>
@@ -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
@@ -6,6 +6,7 @@ export function defineRegistry(options) {
6
6
  configModules,
7
7
  runtimeModules,
8
8
  validateModule,
9
+ runtimeOptional,
9
10
  deriveEntryExtras
10
11
  } = options;
11
12
  const pathRe = new RegExp(`/${dirSegment}/([^/]+)/([^/]+)/schema\\.(?:ts|js)$`);
@@ -14,12 +15,17 @@ export function defineRegistry(options) {
14
15
  if (!match) throw new Error(`[${hostName}] unrecognized schema path: ${path}`);
15
16
  return { date: match[1], type: match[2] };
16
17
  };
17
- const findSibling = (map, date, type, file) => {
18
+ const findSiblingOptional = (map, date, type, file) => {
18
19
  const suffix = `/${dirSegment}/${date}/${type}/${file}`;
19
20
  for (const [key, value] of Object.entries(map)) {
20
21
  if (key.endsWith(suffix)) return value;
21
22
  }
22
- throw new Error(`[${hostName}] missing ${file} for ${type}@${date}`);
23
+ return void 0;
24
+ };
25
+ const findSibling = (map, date, type, file) => {
26
+ const found = findSiblingOptional(map, date, type, file);
27
+ if (found === void 0) throw new Error(`[${hostName}] missing ${file} for ${type}@${date}`);
28
+ return found;
23
29
  };
24
30
  let cached = null;
25
31
  const compute = () => {
@@ -34,11 +40,12 @@ export function defineRegistry(options) {
34
40
  throw new Error(`[${hostName}] compatibilityDate "${mod.compatibilityDate}" does not match folder "${date}"`);
35
41
  }
36
42
  validateModule?.(mod, type, date);
43
+ const runtime = runtimeOptional?.(mod) ? findSiblingOptional(runtimeModules, date, type, "runtime.vue") : findSibling(runtimeModules, date, type, "runtime.vue");
37
44
  drafts.push({
38
45
  type: mod.type,
39
46
  compatibilityDate: mod.compatibilityDate,
40
47
  config: findSibling(configModules, date, type, "config.vue"),
41
- runtime: findSibling(runtimeModules, date, type, "runtime.vue"),
48
+ runtime,
42
49
  module: mod
43
50
  });
44
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/config",
3
- "version": "2.12.18",
3
+ "version": "3.0.1",
4
4
  "description": "Configurable UI for SHWFED",
5
5
  "type": "module",
6
6
  "publishConfig": {