@shwfed/config 2.12.13 → 2.12.15

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 (36) 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.d.vue.ts +8 -0
  3. package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.vue +118 -25
  4. package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/config.vue.d.ts +8 -0
  5. package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/runtime.vue +48 -21
  6. package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/schema.d.ts +5 -0
  7. package/dist/runtime/components/actions/buttons/2026-06-25/com.shwfed.actions.button.state.write/schema.js +24 -1
  8. package/dist/runtime/components/config/use-editor.js +7 -0
  9. package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.d.vue.ts +18 -18
  10. package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.vue.d.ts +18 -18
  11. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +18 -18
  12. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +18 -18
  13. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +18 -18
  14. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +18 -18
  15. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +18 -18
  16. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +18 -18
  17. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +18 -18
  18. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +18 -18
  19. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.d.vue.ts +18 -18
  20. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.vue.d.ts +18 -18
  21. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.d.vue.ts +18 -18
  22. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.vue.d.ts +18 -18
  23. package/dist/runtime/components/sidebar/use-editor.js +7 -0
  24. package/dist/runtime/components/table/config.vue +10 -4
  25. package/dist/runtime/components/table/index.vue +6 -1
  26. package/dist/runtime/components/ui/expression-editor/ExpressionEditor.vue +21 -3
  27. package/dist/runtime/components/ui/expression-editor/index.d.ts +1 -0
  28. package/dist/runtime/components/ui/expression-editor/index.js +5 -0
  29. package/dist/runtime/components/ui/expression-editor/validity.d.ts +12 -0
  30. package/dist/runtime/components/ui/expression-editor/validity.js +27 -0
  31. package/dist/runtime/utils/cel-context.d.ts +15 -0
  32. package/dist/runtime/utils/cel-context.js +18 -1
  33. package/dist/runtime/vendor/cel-js/CLAUDE.md +1 -1
  34. package/dist/runtime/vendor/cel-js/PROMPT.md +15 -0
  35. package/dist/runtime/vendor/cel-js/lib/functions.js +28 -0
  36. package/package.json +1 -1
@@ -55,15 +55,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
55
55
  readonly locale: "en" | "ja" | "ko";
56
56
  readonly message: string;
57
57
  }[]];
58
- readonly nodeTooltip?: readonly [{
59
- readonly locale: "zh";
60
- readonly message: string;
61
- }, ...{
62
- readonly locale: "en" | "ja" | "ko";
63
- readonly message: string;
64
- }[]] | undefined;
65
- readonly nodeSelectable?: string | undefined;
66
- readonly expandAll: boolean;
67
58
  readonly validations?: readonly {
68
59
  readonly message: readonly [{
69
60
  readonly locale: "zh";
@@ -75,6 +66,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
75
66
  readonly warning?: boolean | undefined;
76
67
  readonly when: string;
77
68
  }[] | undefined;
69
+ readonly nodeTooltip?: readonly [{
70
+ readonly locale: "zh";
71
+ readonly message: string;
72
+ }, ...{
73
+ readonly locale: "en" | "ja" | "ko";
74
+ readonly message: string;
75
+ }[]] | undefined;
76
+ readonly nodeSelectable?: string | undefined;
77
+ readonly expandAll: boolean;
78
78
  }) => any;
79
79
  }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
80
80
  "onUpdate:modelValue"?: ((value: {
@@ -129,15 +129,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
129
129
  readonly locale: "en" | "ja" | "ko";
130
130
  readonly message: string;
131
131
  }[]];
132
- readonly nodeTooltip?: readonly [{
133
- readonly locale: "zh";
134
- readonly message: string;
135
- }, ...{
136
- readonly locale: "en" | "ja" | "ko";
137
- readonly message: string;
138
- }[]] | undefined;
139
- readonly nodeSelectable?: string | undefined;
140
- readonly expandAll: boolean;
141
132
  readonly validations?: readonly {
142
133
  readonly message: readonly [{
143
134
  readonly locale: "zh";
@@ -149,6 +140,15 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
149
140
  readonly warning?: boolean | undefined;
150
141
  readonly when: string;
151
142
  }[] | undefined;
143
+ readonly nodeTooltip?: readonly [{
144
+ readonly locale: "zh";
145
+ readonly message: string;
146
+ }, ...{
147
+ readonly locale: "en" | "ja" | "ko";
148
+ readonly message: string;
149
+ }[]] | undefined;
150
+ readonly nodeSelectable?: string | undefined;
151
+ readonly expandAll: boolean;
152
152
  }) => any) | undefined;
153
153
  }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
154
154
  declare const _default: typeof __VLS_export;
@@ -1,6 +1,7 @@
1
1
  import { Schema } from "effect";
2
2
  import { computed, ref, watch } from "vue";
3
3
  import { formatValidationError } from "../config/utils/validation-error.js";
4
+ import { createExpressionValidityRegistry, provideExpressionValidity } from "../ui/expression-editor/index.js";
4
5
  import { SidebarConfig } from "./schema.js";
5
6
  function deepClone(value) {
6
7
  return JSON.parse(JSON.stringify(value));
@@ -14,10 +15,16 @@ export function useSidebarEditor(model, options = {}) {
14
15
  () => JSON.stringify(draft.value) !== JSON.stringify(baseline.value)
15
16
  );
16
17
  const validationError = ref(null);
18
+ const validityRegistry = createExpressionValidityRegistry();
19
+ provideExpressionValidity(validityRegistry);
17
20
  function confirm() {
18
21
  const schema = SidebarConfig(configure);
19
22
  try {
20
23
  const decoded = Schema.decodeUnknownSync(schema)(draft.value);
24
+ if (!validityRegistry.allValid()) {
25
+ validationError.value = "\u5B58\u5728\u65E0\u6548\u7684\u8DE8\u5C42\u4F5C\u7528\u57DF\u5F15\u7528\uFF0C\u8BF7\u4FEE\u6B63\u6807\u7EA2\u7684\u8868\u8FBE\u5F0F\u540E\u518D\u4FDD\u5B58";
26
+ return;
27
+ }
21
28
  validationError.value = null;
22
29
  model.value = decoded;
23
30
  baseline.value = deepClone(decoded);
@@ -1,6 +1,6 @@
1
1
  <script setup>
2
2
  import { computed, defineComponent, inject, onBeforeUnmount, provide, ref, watch } from "vue";
3
- import { celBindings, injectCELContext, provideCELContext, provideScopeAncestor, provideSelectionRoster, SELECTIONS_VAR } from "../../utils/cel-context";
3
+ import { celBindings, injectCELContext, provideCELContext, provideRowUnreachableScope, provideScopeAncestor, provideSelectionRoster, SELECTIONS_VAR } from "../../utils/cel-context";
4
4
  import { BREADCRUMB_EXTENSION_KEY } from "../config/breadcrumb-extension";
5
5
  import { TABLE_COLUMN_LAYOUT_KEY } from "./column-layout";
6
6
  import { Icon } from "@iconify/vue";
@@ -152,6 +152,7 @@ const TableActionsCELContext = defineComponent({
152
152
  value: []
153
153
  }
154
154
  });
155
+ provideRowUnreachableScope();
155
156
  return () => slots.default?.();
156
157
  }
157
158
  });
@@ -305,14 +306,19 @@ const tableScopeAncestor = computed(() => {
305
306
  typeIcon: tableMetadata.icon,
306
307
  members: [
307
308
  { key: "row", label: "\u5F53\u524D\u884C", type: "dyn", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u6574\u884C\u6570\u636E" },
308
- { key: "index", label: "\u884C\u5E8F\u53F7", type: "number", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u5E8F\u53F7\uFF0C\u4ECE `0` \u5F00\u59CB" }
309
+ { key: "index", label: "\u884C\u5E8F\u53F7", type: "number", description: "\u8BE5\u8868\u683C\u5F53\u524D\u884C\u7684\u5E8F\u53F7\uFF0C\u4ECE `0` \u5F00\u59CB" },
310
+ // Registered at the table HOST frame (index.vue), so it is reachable from
311
+ // table-level actions (top toolbar / bottom bar) — where the relative
312
+ // `selected` is shadowed by an inner table — as well as from cells.
313
+ { key: "selected", label: "\u5DF2\u9009\u884C", type: "list<dyn>", description: "\u8BE5\u8868\u683C\u5F53\u524D\u52FE\u9009\u7684\u6240\u6709\u884C" }
309
314
  ],
310
315
  // Each row-provider rebinds the relative `selections` to its own per-row
311
316
  // registry (see row-provider.vue), so a table row shadows an enclosing
312
317
  // form's selection options even though the table exposes no `selections`
313
318
  // member of its own — list it here so that form's options stay offerable to
314
- // a cell expression inside this table.
315
- shadows: ["row", "index", SELECTIONS_VAR]
319
+ // a cell expression inside this table. `selected` likewise: the table host
320
+ // rebinds it, shadowing an enclosing table's selection for everything below.
321
+ shadows: ["row", "index", "selected", SELECTIONS_VAR]
316
322
  };
317
323
  });
318
324
  provideScopeAncestor(tableScopeAncestor);
@@ -17,7 +17,7 @@ import { Pagination, ScrollArea } from "reka-ui/namespaced";
17
17
  import { computed, h, onMounted, ref, toRaw, watch } from "vue";
18
18
  import { asRequest, fetchJsonOption } from "../../share/request";
19
19
  import { useI18n } from "vue-i18n";
20
- import { celBindings, celScope, provideCELContext, injectCELContext } from "../../utils/cel-context";
20
+ import { celBindings, celScope, provideCELContext, injectCELContext, provideScopeAddress } from "../../utils/cel-context";
21
21
  import TableRowProvider from "./row-provider.vue";
22
22
  import * as addRowOp from "../operations/2026-07-06/com.shwfed.operation.table.add-row/schema";
23
23
  import { getLocalizedText } from "../../share/locale";
@@ -353,6 +353,11 @@ provideCELContext({
353
353
  }
354
354
  }
355
355
  });
356
+ if (config.value?.id) {
357
+ provideScopeAddress(config.value.id, {
358
+ selected: () => tableApi.getSelectedRowModel().rows.map((r) => r.original)
359
+ });
360
+ }
356
361
  provideSharedFetchLayer();
357
362
  const hasDragHandleColumn = computed(() => (config.value?.columns ?? []).some((c) => c.type === DRAG_HANDLE_COLUMN_TYPE));
358
363
  const reorderDisabledReason = computed(() => {
@@ -1,11 +1,12 @@
1
1
  <script setup>
2
2
  import { Icon } from "@iconify/vue";
3
- import { computed, ref, useSlots, useTemplateRef, watch } from "vue";
3
+ import { computed, onScopeDispose, ref, useSlots, useTemplateRef, watch } from "vue";
4
4
  import { buildCheckEnvironment, evaluateExpression } from "../../../share/expression";
5
5
  import { OPTIONS_VAR, SELECTIONS_VAR, injectCELContext, useScopeAncestry, useSelectionRoster, useStepRoster } from "../../../utils/cel-context";
6
6
  import { Markdown } from "../markdown";
7
7
  import CodeMirrorInput from "./CodeMirrorInput.vue";
8
- import { buildScopeLookup } from "./scope-refs";
8
+ import { buildScopeLookup, scanScopeRefs } from "./scope-refs";
9
+ import { useExpressionValidity } from "./validity";
9
10
  import { buildSelectionLookup } from "./selection-refs";
10
11
  import { buildStepLookup } from "./step-refs";
11
12
  import { buildOptionEntries, buildScopeEntries, buildSelectionEntries, buildStepEntries, buildVarEntries, selectShadowedScopes } from "./picker-entries";
@@ -62,11 +63,28 @@ const checkEnvironment = computed(() => {
62
63
  { unlistedVariablesAreDyn: props.unlistedVarsAreDyn }
63
64
  );
64
65
  });
66
+ const danglingScopeError = computed(() => {
67
+ const value = props.modelValue ?? props.defaultValue ?? "";
68
+ if (value.trim() === "") return null;
69
+ const lookup = scopeLookup.value;
70
+ for (const ref2 of scanScopeRefs(value)) {
71
+ const info = lookup.get(ref2.id);
72
+ if (!info || !info.members.has(ref2.member)) {
73
+ return "\u8868\u8FBE\u5F0F\u5F15\u7528\u4E86\u65E0\u6548\u7684\u8DE8\u5C42\u4F5C\u7528\u57DF\uFF08\u6765\u6E90\u5DF2\u4E0D\u5728\u53EF\u5BFB\u5740\u7684\u4E0A\u5C42\u4F5C\u7528\u57DF\u4E2D\uFF09";
74
+ }
75
+ }
76
+ return null;
77
+ });
65
78
  const validationError = computed(() => {
66
79
  const value = props.modelValue ?? props.defaultValue ?? "";
67
80
  if (value.trim() === "") return null;
68
- return evaluateExpression(checkEnvironment.value, value, props.resultType);
81
+ return danglingScopeError.value ?? evaluateExpression(checkEnvironment.value, value, props.resultType);
69
82
  });
83
+ const validityRegistry = useExpressionValidity();
84
+ if (validityRegistry) {
85
+ const unregister = validityRegistry.register(() => danglingScopeError.value === null);
86
+ onScopeDispose(unregister);
87
+ }
70
88
  const open = ref(false);
71
89
  const entries = ref([]);
72
90
  const shownVars = computed(() => entries.value.filter((e) => e.group === "var"));
@@ -1 +1,2 @@
1
1
  export { default as ExpressionEditor } from './ExpressionEditor.vue.js';
2
+ export { createExpressionValidityRegistry, provideExpressionValidity, useExpressionValidity, type ExpressionValidityRegistry, } from './validity.js';
@@ -1 +1,6 @@
1
1
  export { default as ExpressionEditor } from "./ExpressionEditor.vue";
2
+ export {
3
+ createExpressionValidityRegistry,
4
+ provideExpressionValidity,
5
+ useExpressionValidity
6
+ } from "./validity.js";
@@ -0,0 +1,12 @@
1
+ export type ExpressionValidityRegistry = {
2
+ /**
3
+ * Register a mounted editor's validity thunk (`true` ⇒ no ancestry error).
4
+ * Returns an unregister fn to call from `onScopeDispose`.
5
+ */
6
+ register: (isValid: () => boolean) => () => void;
7
+ /** `true` when every currently-registered editor reports valid. */
8
+ allValid: () => boolean;
9
+ };
10
+ export declare function createExpressionValidityRegistry(): ExpressionValidityRegistry;
11
+ export declare function provideExpressionValidity(registry: ExpressionValidityRegistry): void;
12
+ export declare function useExpressionValidity(): ExpressionValidityRegistry | undefined;
@@ -0,0 +1,27 @@
1
+ import { getCurrentInstance, inject, provide } from "vue";
2
+ const KEY = Symbol("shwfed-expression-validity");
3
+ export function createExpressionValidityRegistry() {
4
+ const entries = /* @__PURE__ */ new Map();
5
+ return {
6
+ register(isValid) {
7
+ const token = Symbol("expr");
8
+ entries.set(token, isValid);
9
+ return () => {
10
+ entries.delete(token);
11
+ };
12
+ },
13
+ allValid() {
14
+ for (const isValid of entries.values()) {
15
+ if (!isValid()) return false;
16
+ }
17
+ return true;
18
+ }
19
+ };
20
+ }
21
+ export function provideExpressionValidity(registry) {
22
+ if (!getCurrentInstance()) return;
23
+ provide(KEY, registry);
24
+ }
25
+ export function useExpressionValidity() {
26
+ return inject(KEY, void 0);
27
+ }
@@ -67,6 +67,21 @@ export declare function provideScopeAncestor(ancestor: Ref<ScopeAncestor | null>
67
67
  * first. Empty when nothing up the config tree exposes an addressable scope.
68
68
  */
69
69
  export declare function useScopeAncestry(): Ref<ReadonlyArray<ScopeAncestor>>;
70
+ /**
71
+ * Re-provide the inherited scope ancestry with the NEAREST frame's per-row
72
+ * members (`row` / `index`) dropped — its `selected` (and any other member)
73
+ * survives. Called by a table-LEVEL action editor (top toolbar / bottom bar):
74
+ * such an action renders OUTSIDE any row, so at runtime the owning table's
75
+ * `__scopes__[id]` there carries only `selected` (registered at the table host
76
+ * frame), never `row`/`index` (registered per row). Everything the action opens
77
+ * — nested modals and their tables — inherits this downgraded chain, so a
78
+ * cross-layer picker below never offers the owning table's 当前行 / 行序号, which
79
+ * would throw `No such key: row` at evaluation. Outer frames are untouched: each
80
+ * table masks only its own here, and an outer table reached via a genuine row
81
+ * action keeps its row/index reachable. `shadows` is left intact — this frame
82
+ * still masks the relative `row`/`index` for descendants at runtime.
83
+ */
84
+ export declare function provideRowUnreachableScope(): void;
70
85
  /** One selection-like field in the editing form, as the picker/chip surfaces it. */
71
86
  export type SelectionFieldRef = Readonly<{
72
87
  /** Field uuid keyed into `selections["<id>"]` — the registry/publish key. */
@@ -30,7 +30,8 @@ export function provideScopeAddress(uuid, accessor) {
30
30
  // bracket access throws a located `No such key` at the offending uuid.
31
31
  value: () => {
32
32
  const base = parentScopes ? typeof parentScopes.value === "function" ? parentScopes.value() : parentScopes.value : {};
33
- return { ...base, [uuid]: resolveScopeAccessor(accessor) };
33
+ const prior = base[uuid];
34
+ return { ...base, [uuid]: { ...prior, ...resolveScopeAccessor(accessor) } };
34
35
  }
35
36
  }
36
37
  });
@@ -49,6 +50,22 @@ export function provideScopeAncestor(ancestor) {
49
50
  export function useScopeAncestry() {
50
51
  return inject(SCOPE_ANCESTRY_KEY, void 0) ?? shallowRef([]);
51
52
  }
53
+ export function provideRowUnreachableScope() {
54
+ const parent = inject(SCOPE_ANCESTRY_KEY, void 0);
55
+ provide(
56
+ SCOPE_ANCESTRY_KEY,
57
+ computed(() => {
58
+ const chain = parent?.value ?? [];
59
+ if (chain.length === 0) return chain;
60
+ const [nearest, ...rest] = chain;
61
+ const members = nearest.members.filter((m) => {
62
+ const shortcut = m.shortcut ?? m.key;
63
+ return shortcut !== "row" && shortcut !== "index";
64
+ });
65
+ return [{ ...nearest, members }, ...rest];
66
+ })
67
+ );
68
+ }
52
69
  const SELECTION_ROSTER_KEY = Symbol("cel-selection-roster");
53
70
  export function provideSelectionRoster(roster) {
54
71
  provide(SELECTION_ROSTER_KEY, roster);
@@ -85,7 +85,7 @@ Internal-only (not re-exported from `index.js`): `parse`, standalone `evaluate`/
85
85
 
86
86
  Primitives: `string`, `bool`, `number`, `bytes`, `null_type`, `type`
87
87
  Composites: `list<T>`, `map<K, V>`, `optional<T>`, `dyn`, `message`
88
- Custom: `Date` (backed by `TZDate` from `@date-fns/tz`), `URL` (backed by the native `URL`; method `URL.query(string): optional<string>` reads a search param — `None` when absent, distinguishing it from a present-but-empty value), `File` (native), `FormData` (native — see `form-builtins.ts`), `http` / `HttpRequest` (local addition — see `http-builtins.ts`)
88
+ Custom: `Date` (backed by `TZDate` from `@date-fns/tz`), `URL` (backed by the native `URL`; host-supplied only — no CEL constructor. Reader `URL.query(string): optional<string>` returns the *first* value of a search param — `None` when absent, distinguishing it from a present-but-empty value. Writers return a **copy**, never mutating the receiver (a `URL` is host-injected shared state, unlike a fresh-per-expression `HttpRequestBuilder`): the set/replace `URL.query(string, string | number | optional<string> | optional<number>): URL` collapses repeated entries and on `None` *deletes* the key (the result mirrors the optional's state — deliberately unlike `HttpRequest.query`, whose `None` is a drop/no-op), while `URL.appendQuery(...)` with the same four value overloads appends an entry and no-ops on `None`. Number values render Decimal-exact, like `string(number)`. `string(URL)` is the full `href`.), `File` (native), `FormData` (native — see `form-builtins.ts`), `http` / `HttpRequest` (local addition — see `http-builtins.ts`)
89
89
 
90
90
  ## Function Signature Format
91
91
 
@@ -105,6 +105,7 @@ number(d) // Date → epoch-millisecond timestamp
105
105
  string(42) // "42"
106
106
  string(true) // "true"
107
107
  string(d) // Date → ISO 8601 string
108
+ string(location) // URL → the full href string
108
109
  bool("true") // true (also accepts "t", "1", "TRUE", "True")
109
110
  bool("false") // false (also accepts "f", "0", "FALSE", "False")
110
111
  bytes("text") // UTF-8 encode to bytes
@@ -310,6 +311,20 @@ location.query("missing").hasValue() // false — absent params are None
310
311
  location.query("id").orValue("") // first value when a param repeats
311
312
  ```
312
313
 
314
+ `query(name, value)` **sets** a search param and returns a new `URL` — the
315
+ original is never modified. The value may be a `string`, a `number`, or an
316
+ optional of either: `Some(v)` sets the param, `None` **removes** it, so the
317
+ result always mirrors the optional's state. `appendQuery(name, value)` takes
318
+ the same value types but **adds** an entry alongside existing ones (`None`
319
+ appends nothing). Convert the result with `string(...)` when the host expects
320
+ a URL string.
321
+ ```cel
322
+ location.query("page", 2) // ?page=1 → ?page=2 (replaced)
323
+ location.query("page", q.?page) // sets ?page= from the field, or removes it when absent
324
+ location.appendQuery("tag", "a") // ?tag=x → ?tag=x&tag=a (kept both)
325
+ string(location.query("page", 2)) // "https://…?page=2" — e.g. a navigation target
326
+ ```
327
+
313
328
  ## HTTP
314
329
 
315
330
  Build HTTP requests using the `http` builder. A CEL expression only *describes* a request — it always evaluates to an `HttpRequest`, never issues it. The host decides when and how to send it (downloading a file, reading a JSON response, etc.). There is no way to perform the request or read its response from CEL.
@@ -1,6 +1,7 @@
1
1
  import { evaluationError } from "./errors.js";
2
2
  import { Decimal } from "./decimal.js";
3
3
  import { TYPES, Type } from "./registry.js";
4
+ import { Option } from "effect";
4
5
  import { register as registerOptional, optionalOf } from "./optional.js";
5
6
  import { objKeys, arrayFrom } from "./globals.js";
6
7
  import { TZDate } from "@date-fns/tz";
@@ -410,6 +411,33 @@ export function registerFunctions(registry) {
410
411
  registry.registerType("File", File);
411
412
  registry.registerType("URL", URL);
412
413
  functionOverload("URL.query(string): optional<string>", (url, key) => optionalOf(url.searchParams.get(key) ?? void 0));
414
+ const withSearchParams = (u, write) => {
415
+ const url = new URL(u.href);
416
+ write(url.searchParams);
417
+ return url;
418
+ };
419
+ const numToParam = (v) => Decimal.from(v).toString();
420
+ functionOverload("URL.query(string, string): URL", (u, k, v) => withSearchParams(u, (p) => p.set(k, v)));
421
+ functionOverload("URL.query(string, number): URL", (u, k, v) => withSearchParams(u, (p) => p.set(k, numToParam(v))));
422
+ functionOverload("URL.query(string, optional<string>): URL", (u, k, v) => {
423
+ const value = v;
424
+ return withSearchParams(u, (p) => Option.isSome(value) ? p.set(k, value.value) : p.delete(k));
425
+ });
426
+ functionOverload("URL.query(string, optional<number>): URL", (u, k, v) => {
427
+ const value = v;
428
+ return withSearchParams(u, (p) => Option.isSome(value) ? p.set(k, numToParam(value.value)) : p.delete(k));
429
+ });
430
+ functionOverload("URL.appendQuery(string, string): URL", (u, k, v) => withSearchParams(u, (p) => p.append(k, v)));
431
+ functionOverload("URL.appendQuery(string, number): URL", (u, k, v) => withSearchParams(u, (p) => p.append(k, numToParam(v))));
432
+ functionOverload("URL.appendQuery(string, optional<string>): URL", (u, k, v) => {
433
+ const value = v;
434
+ return Option.isSome(value) ? withSearchParams(u, (p) => p.append(k, value.value)) : u;
435
+ });
436
+ functionOverload("URL.appendQuery(string, optional<number>): URL", (u, k, v) => {
437
+ const value = v;
438
+ return Option.isSome(value) ? withSearchParams(u, (p) => p.append(k, numToParam(value.value))) : u;
439
+ });
440
+ functionOverload("string(URL): string", (v) => v.href);
413
441
  registerOptional(registry);
414
442
  }
415
443
  function stringSize(str) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/config",
3
- "version": "2.12.13",
3
+ "version": "2.12.15",
4
4
  "description": "Configurable UI for SHWFED",
5
5
  "type": "module",
6
6
  "publishConfig": {