@shwfed/config 3.0.3 → 3.0.6

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 (57) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/actions/buttons/2026-04-21/com.shwfed.actions.button.navigation/runtime.vue +4 -10
  3. package/dist/runtime/components/actions/buttons/2026-05-21/com.shwfed.actions.button.http.download/schema.d.ts +1 -1
  4. package/dist/runtime/components/actions/buttons/2026-05-21/com.shwfed.actions.button.http.download/schema.js +2 -2
  5. package/dist/runtime/components/actions/buttons/2026-07-13/com.shwfed.actions.button.state.write/runtime.vue +22 -2
  6. package/dist/runtime/components/actions/buttons/2026-07-13/com.shwfed.actions.button.state.write/schema.d.ts +4 -2
  7. package/dist/runtime/components/actions/buttons/2026-07-13/com.shwfed.actions.button.state.write/schema.js +1 -1
  8. package/dist/runtime/components/actions/utils/steps.d.ts +1 -1
  9. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.d.vue.ts +6 -6
  10. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.vue.d.ts +6 -6
  11. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.d.vue.ts +6 -6
  12. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.vue.d.ts +6 -6
  13. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/schema.d.ts +3 -3
  14. package/dist/runtime/components/config/index.vue +25 -0
  15. package/dist/runtime/components/config/prune-dialog.d.vue.ts +14 -0
  16. package/dist/runtime/components/config/prune-dialog.vue +72 -0
  17. package/dist/runtime/components/config/prune-dialog.vue.d.ts +14 -0
  18. package/dist/runtime/components/config/schema.js +2 -2
  19. package/dist/runtime/components/config/use-editor.d.ts +9 -0
  20. package/dist/runtime/components/config/use-editor.js +37 -9
  21. package/dist/runtime/components/config/utils/linkage.d.ts +8 -0
  22. package/dist/runtime/components/config/utils/linkage.js +7 -2
  23. package/dist/runtime/components/config/utils/prune.d.ts +68 -0
  24. package/dist/runtime/components/config/utils/prune.js +92 -0
  25. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.d.vue.ts +31 -0
  26. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.vue +191 -0
  27. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.vue.d.ts +31 -0
  28. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/expand.d.ts +49 -0
  29. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/expand.js +87 -0
  30. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/runtime.d.vue.ts +8 -0
  31. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/runtime.vue +72 -0
  32. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/runtime.vue.d.ts +8 -0
  33. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/schema.d.ts +59 -0
  34. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/schema.js +139 -0
  35. package/dist/runtime/components/form/utils/form-vars.d.ts +5 -15
  36. package/dist/runtime/components/form/utils/form-vars.js +12 -6
  37. package/dist/runtime/components/operations/2026-06-11/com.shwfed.operation.http.request.json/schema.js +2 -2
  38. package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.http.download/schema.js +2 -2
  39. package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/runtime.js +5 -11
  40. package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/schema.js +1 -1
  41. package/dist/runtime/components/operations/utils/resolve.d.ts +1 -1
  42. package/dist/runtime/components/operations/utils/step-vars.d.ts +4 -2
  43. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.select/runtime.vue +1 -21
  44. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.select/schema.d.ts +2 -0
  45. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.select/schema.js +16 -1
  46. package/dist/runtime/components/table/index.vue +15 -0
  47. package/dist/runtime/components/table/schema.d.ts +6 -6
  48. package/dist/runtime/components/table/utils/resolve.d.ts +26 -0
  49. package/dist/runtime/components/table/utils/resolve.js +2 -1
  50. package/dist/runtime/components/ui/expression-editor/picker-entries.js +1 -1
  51. package/dist/runtime/components/ui/expression-editor/step-chip-extension.js +1 -1
  52. package/dist/runtime/components/ui/expression-editor/step-refs.js +1 -1
  53. package/dist/runtime/share/event-bus.js +2 -2
  54. package/dist/runtime/share/navigation.d.ts +1 -0
  55. package/dist/runtime/share/navigation.js +4 -0
  56. package/dist/runtime/utils/cel-context.d.ts +2 -2
  57. package/package.json +1 -1
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shwfed",
3
3
  "configKey": "shwfed",
4
- "version": "3.0.3",
4
+ "version": "3.0.6",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { Effect } from "effect";
3
3
  import { navigateTo, useRouter } from "#app";
4
+ import { isAbsoluteUrl } from "../../../../../share/navigation";
4
5
  import { cel as _rawCel } from "../../../../../utils/cel";
5
6
  import { celBindings, injectCELContext } from "../../../../../utils/cel-context";
6
7
  import ShwfedActionDefinition from "../../../components/definition.vue";
@@ -12,23 +13,16 @@ const props = defineProps({
12
13
  const router = useRouter();
13
14
  const inherited = injectCELContext();
14
15
  const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
15
- function isExternal(url) {
16
- try {
17
- return new URL(url, window.location.href).origin !== window.location.origin;
18
- } catch {
19
- return false;
20
- }
21
- }
22
16
  const effect = Effect.suspend(
23
17
  () => Effect.flatMap(
24
18
  $cel(props.config.url),
25
19
  (url) => Effect.promise(async () => {
26
- const external = isExternal(url);
20
+ const absolute = isAbsoluteUrl(url);
27
21
  if (props.config.mode === "_blank") {
28
- window.open(external ? url : router.resolve(url).href, "_blank");
22
+ window.open(absolute ? url : router.resolve(url).href, "_blank");
29
23
  return;
30
24
  }
31
- await navigateTo(url, { external });
25
+ await navigateTo(url, { external: absolute });
32
26
  })
33
27
  )
34
28
  );
@@ -5,7 +5,7 @@ export declare const compatibilityDate: "2026-05-21";
5
5
  export declare const metadata: {
6
6
  readonly name: "下载文件";
7
7
  readonly icon: "fluent:arrow-download-20-regular";
8
- readonly deprecated: "本按钮已废弃:改用「触发事件」按钮,其触发列表串联页面的「下载文件」操作;原两步式(先取凭据再下载)现为一个「发送请求」行后接一个引用 `steps[\"<步骤标识>\"]` 的「下载文件」行。";
8
+ readonly deprecated: "本按钮已废弃:改用「触发事件」按钮,其触发列表串联页面的「下载文件」操作;原两步式(先取凭据再下载)现为一个「发送请求」行后接一个引用 `steps[?\"<步骤标识>\"]` 的「下载文件」行。";
9
9
  };
10
10
  export declare const JSON_VAR: {
11
11
  readonly type: "dyn";
@@ -9,8 +9,8 @@ export const metadata = {
9
9
  icon: "fluent:arrow-download-20-regular",
10
10
  // No structured migrate target — designers rebuild downloads by hand. Use a
11
11
  // 触发事件 按钮,其触发列表串联页面的「下载文件」操作即可替代本按钮;原来的两步式
12
- // (先取凭据再下载)现为一个「发送请求」行后接一个引用 `steps["<步骤标识>"]` 的「下载文件」行。
13
- deprecated: '\u672C\u6309\u94AE\u5DF2\u5E9F\u5F03\uFF1A\u6539\u7528\u300C\u89E6\u53D1\u4E8B\u4EF6\u300D\u6309\u94AE\uFF0C\u5176\u89E6\u53D1\u5217\u8868\u4E32\u8054\u9875\u9762\u7684\u300C\u4E0B\u8F7D\u6587\u4EF6\u300D\u64CD\u4F5C\uFF1B\u539F\u4E24\u6B65\u5F0F\uFF08\u5148\u53D6\u51ED\u636E\u518D\u4E0B\u8F7D\uFF09\u73B0\u4E3A\u4E00\u4E2A\u300C\u53D1\u9001\u8BF7\u6C42\u300D\u884C\u540E\u63A5\u4E00\u4E2A\u5F15\u7528 `steps["<\u6B65\u9AA4\u6807\u8BC6>"]` \u7684\u300C\u4E0B\u8F7D\u6587\u4EF6\u300D\u884C\u3002'
12
+ // (先取凭据再下载)现为一个「发送请求」行后接一个引用 `steps[?"<步骤标识>"]` 的「下载文件」行。
13
+ deprecated: '\u672C\u6309\u94AE\u5DF2\u5E9F\u5F03\uFF1A\u6539\u7528\u300C\u89E6\u53D1\u4E8B\u4EF6\u300D\u6309\u94AE\uFF0C\u5176\u89E6\u53D1\u5217\u8868\u4E32\u8054\u9875\u9762\u7684\u300C\u4E0B\u8F7D\u6587\u4EF6\u300D\u64CD\u4F5C\uFF1B\u539F\u4E24\u6B65\u5F0F\uFF08\u5148\u53D6\u51ED\u636E\u518D\u4E0B\u8F7D\uFF09\u73B0\u4E3A\u4E00\u4E2A\u300C\u53D1\u9001\u8BF7\u6C42\u300D\u884C\u540E\u63A5\u4E00\u4E2A\u5F15\u7528 `steps[?"<\u6B65\u9AA4\u6807\u8BC6>"]` \u7684\u300C\u4E0B\u8F7D\u6587\u4EF6\u300D\u884C\u3002'
14
14
  };
15
15
  export const JSON_VAR = {
16
16
  type: "dyn",
@@ -1,5 +1,5 @@
1
1
  <script setup>
2
- import { Effect } from "effect";
2
+ import { Cause, Effect } from "effect";
3
3
  import { createDefu } from "defu";
4
4
  import { cel as _rawCel } from "../../../../../utils/cel";
5
5
  import { celBindings, injectCELContext } from "../../../../../utils/cel-context";
@@ -51,7 +51,27 @@ const effect = Effect.suspend(() => {
51
51
  return Effect.map($cel(write.value, { steps }), (result) => {
52
52
  const merged = mergeAtPath(formState.getAt(path), result);
53
53
  formState.setAt(path, merged);
54
- });
54
+ }).pipe(
55
+ // A row that blows up here — an expression that parses but throws on
56
+ // this click's data (`steps[?"r1"].json.rate` when the response was
57
+ // null), a path dot-prop cannot descend — is a *config* bug, not a
58
+ // condition the end user can act on. So: no toast, but never silent
59
+ // either. The failure otherwise reaches only the action runner's
60
+ // `catch`, whose `console.error` is `import.meta.dev`-gated — and the
61
+ // designer configures inside the running (production-built) app, so
62
+ // that log is one they can never see. Log ungated, like every other
63
+ // CEL misconfiguration in the runtime (`form/index.vue`,
64
+ // `sidebar/tree.vue`, the field runtimes), naming the row that failed;
65
+ // then re-raise so the click still halts (rows after it, and 写入后,
66
+ // stay skipped). `tapErrorCause` covers a throwing `setAt` (a defect)
67
+ // as well as a failed evaluation.
68
+ Effect.tapErrorCause((cause) => Effect.sync(() => {
69
+ console.error(
70
+ `[shwfed-actions] state.write: failed to write "${path}" for ${props.buttonId}:`,
71
+ Cause.squash(cause)
72
+ );
73
+ }))
74
+ );
55
75
  },
56
76
  { discard: true }
57
77
  )
@@ -35,8 +35,10 @@ export type WriteValue = Value['writes'][number];
35
35
  * Adopts 2026-06-25 values, whose「写入前」was a `requests` list of bare
36
36
  * HTTP-request expressions. Each row becomes one「发送请求」op trigger on the
37
37
  * page target — the op the button now dispatches instead of fetching itself.
38
- * The row's `id` is carried over verbatim, so every `steps["<id>"]` already
39
- * written into a write value keeps resolving; the op publishes its parsed
38
+ * The row's `id` is carried over verbatim, so every step reference already
39
+ * written into a write value keeps resolving the strict `steps["<id>"]` those
40
+ * values carry as much as the optional `steps[?"<id>"]` the picker writes today,
41
+ * both addressing the same id; the op publishes its parsed
40
42
  * response under that same id and halts the chain on failure, which is what the
41
43
  * old inline requests did.
42
44
  *
@@ -36,7 +36,7 @@ export function schema(configure) {
36
36
  将取消本次写入。
37
37
 
38
38
  发布输出的操作(如「发送请求」)可在「写入值」与「写入后」中经
39
- \`steps["<第 N 步>"]\` 引用其输出。
39
+ \`steps[?"<第 N 步>"]\` 引用其输出。
40
40
  `
41
41
  })),
42
42
  writes: Schema.Array(Write).annotations({
@@ -1,7 +1,7 @@
1
1
  import type { TriggerValue } from '../../../share/event-bus.js';
2
2
  /**
3
3
  * One row of a trigger list that publishes a chain-step output — the rows a
4
- * later expression can address as `steps["<id>"]`.
4
+ * later expression can address as `steps[?"<id>"]`.
5
5
  *
6
6
  * A row qualifies on two counts, neither of them a type check: its operation's
7
7
  * registry `metadata.output` marks it as publishing (so a new publishing op
@@ -228,6 +228,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
228
228
  readonly expanded?: boolean | {
229
229
  readonly [x: string]: boolean;
230
230
  } | undefined;
231
+ readonly rowSelection?: {
232
+ readonly [x: string]: boolean;
233
+ } | undefined;
231
234
  readonly columnVisibility?: {
232
235
  readonly [x: string]: boolean;
233
236
  } | undefined;
@@ -248,9 +251,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
248
251
  readonly columnSizing?: {
249
252
  readonly [x: string]: number;
250
253
  } | undefined;
251
- readonly rowSelection?: {
252
- readonly [x: string]: boolean;
253
- } | undefined;
254
254
  } | undefined;
255
255
  };
256
256
  readonly compatibilityDate: "2026-05-06";
@@ -476,6 +476,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
476
476
  readonly expanded?: boolean | {
477
477
  readonly [x: string]: boolean;
478
478
  } | undefined;
479
+ readonly rowSelection?: {
480
+ readonly [x: string]: boolean;
481
+ } | undefined;
479
482
  readonly columnVisibility?: {
480
483
  readonly [x: string]: boolean;
481
484
  } | undefined;
@@ -496,9 +499,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
496
499
  readonly columnSizing?: {
497
500
  readonly [x: string]: number;
498
501
  } | undefined;
499
- readonly rowSelection?: {
500
- readonly [x: string]: boolean;
501
- } | undefined;
502
502
  } | undefined;
503
503
  };
504
504
  readonly compatibilityDate: "2026-05-06";
@@ -228,6 +228,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
228
228
  readonly expanded?: boolean | {
229
229
  readonly [x: string]: boolean;
230
230
  } | undefined;
231
+ readonly rowSelection?: {
232
+ readonly [x: string]: boolean;
233
+ } | undefined;
231
234
  readonly columnVisibility?: {
232
235
  readonly [x: string]: boolean;
233
236
  } | undefined;
@@ -248,9 +251,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
248
251
  readonly columnSizing?: {
249
252
  readonly [x: string]: number;
250
253
  } | undefined;
251
- readonly rowSelection?: {
252
- readonly [x: string]: boolean;
253
- } | undefined;
254
254
  } | undefined;
255
255
  };
256
256
  readonly compatibilityDate: "2026-05-06";
@@ -476,6 +476,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
476
476
  readonly expanded?: boolean | {
477
477
  readonly [x: string]: boolean;
478
478
  } | undefined;
479
+ readonly rowSelection?: {
480
+ readonly [x: string]: boolean;
481
+ } | undefined;
479
482
  readonly columnVisibility?: {
480
483
  readonly [x: string]: boolean;
481
484
  } | undefined;
@@ -496,9 +499,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
496
499
  readonly columnSizing?: {
497
500
  readonly [x: string]: number;
498
501
  } | undefined;
499
- readonly rowSelection?: {
500
- readonly [x: string]: boolean;
501
- } | undefined;
502
502
  } | undefined;
503
503
  };
504
504
  readonly compatibilityDate: "2026-05-06";
@@ -223,6 +223,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
223
223
  readonly expanded?: boolean | {
224
224
  readonly [x: string]: boolean;
225
225
  } | undefined;
226
+ readonly rowSelection?: {
227
+ readonly [x: string]: boolean;
228
+ } | undefined;
226
229
  readonly columnVisibility?: {
227
230
  readonly [x: string]: boolean;
228
231
  } | undefined;
@@ -243,9 +246,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
243
246
  readonly columnSizing?: {
244
247
  readonly [x: string]: number;
245
248
  } | undefined;
246
- readonly rowSelection?: {
247
- readonly [x: string]: boolean;
248
- } | undefined;
249
249
  } | undefined;
250
250
  };
251
251
  readonly compatibilityDate: "2026-05-06";
@@ -471,6 +471,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
471
471
  readonly expanded?: boolean | {
472
472
  readonly [x: string]: boolean;
473
473
  } | undefined;
474
+ readonly rowSelection?: {
475
+ readonly [x: string]: boolean;
476
+ } | undefined;
474
477
  readonly columnVisibility?: {
475
478
  readonly [x: string]: boolean;
476
479
  } | undefined;
@@ -491,9 +494,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
491
494
  readonly columnSizing?: {
492
495
  readonly [x: string]: number;
493
496
  } | undefined;
494
- readonly rowSelection?: {
495
- readonly [x: string]: boolean;
496
- } | undefined;
497
497
  } | undefined;
498
498
  };
499
499
  readonly compatibilityDate: "2026-05-06";
@@ -223,6 +223,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
223
223
  readonly expanded?: boolean | {
224
224
  readonly [x: string]: boolean;
225
225
  } | undefined;
226
+ readonly rowSelection?: {
227
+ readonly [x: string]: boolean;
228
+ } | undefined;
226
229
  readonly columnVisibility?: {
227
230
  readonly [x: string]: boolean;
228
231
  } | undefined;
@@ -243,9 +246,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
243
246
  readonly columnSizing?: {
244
247
  readonly [x: string]: number;
245
248
  } | undefined;
246
- readonly rowSelection?: {
247
- readonly [x: string]: boolean;
248
- } | undefined;
249
249
  } | undefined;
250
250
  };
251
251
  readonly compatibilityDate: "2026-05-06";
@@ -471,6 +471,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
471
471
  readonly expanded?: boolean | {
472
472
  readonly [x: string]: boolean;
473
473
  } | undefined;
474
+ readonly rowSelection?: {
475
+ readonly [x: string]: boolean;
476
+ } | undefined;
474
477
  readonly columnVisibility?: {
475
478
  readonly [x: string]: boolean;
476
479
  } | undefined;
@@ -491,9 +494,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {},
491
494
  readonly columnSizing?: {
492
495
  readonly [x: string]: number;
493
496
  } | undefined;
494
- readonly rowSelection?: {
495
- readonly [x: string]: boolean;
496
- } | undefined;
497
497
  } | undefined;
498
498
  };
499
499
  readonly compatibilityDate: "2026-05-06";
@@ -241,6 +241,9 @@ export declare function schema(configure: (env: Environment) => void, _blockRef:
241
241
  readonly expanded?: boolean | {
242
242
  readonly [x: string]: boolean;
243
243
  } | undefined;
244
+ readonly rowSelection?: {
245
+ readonly [x: string]: boolean;
246
+ } | undefined;
244
247
  readonly columnVisibility?: {
245
248
  readonly [x: string]: boolean;
246
249
  } | undefined;
@@ -261,9 +264,6 @@ export declare function schema(configure: (env: Environment) => void, _blockRef:
261
264
  readonly columnSizing?: {
262
265
  readonly [x: string]: number;
263
266
  } | undefined;
264
- readonly rowSelection?: {
265
- readonly [x: string]: boolean;
266
- } | undefined;
267
267
  } | undefined;
268
268
  }, Schema.Struct<{
269
269
  kind: Schema.tag<"shwfed.component.table">;
@@ -13,6 +13,7 @@ import ShwfedModal from "../modal.vue";
13
13
  import EditorBody from "./config.vue";
14
14
  import EditorBreadcrumb from "./breadcrumb.vue";
15
15
  import EditorFooter from "./footer.vue";
16
+ import PruneDialog from "./prune-dialog.vue";
16
17
  import { provideEventTarget } from "../../share/event-bus";
17
18
  import { operationHandlers } from "../operations/utils/resolve";
18
19
  import * as alertOperation from "../operations/2026-06-09/com.shwfed.operation.alert/schema";
@@ -95,12 +96,31 @@ provideEventTarget(PAGE_TARGET_ID, {
95
96
  )
96
97
  });
97
98
  const editorOpen = ref(false);
99
+ const pendingPrune = ref(null);
100
+ let settlePrune = null;
101
+ function confirmPrune(items) {
102
+ resolvePrune(false);
103
+ pendingPrune.value = items;
104
+ return new Promise((resolve) => {
105
+ settlePrune = resolve;
106
+ });
107
+ }
108
+ function resolvePrune(proceed) {
109
+ pendingPrune.value = null;
110
+ const settle = settlePrune;
111
+ settlePrune = null;
112
+ settle?.(proceed);
113
+ }
114
+ onBeforeUnmount(() => {
115
+ if (settlePrune) resolvePrune(false);
116
+ });
98
117
  const editorState = useConfigEditor(config, {
99
118
  configure: mergedConfigure,
100
119
  // Presence is decided once at setup (a host toggling the prop's existence
101
120
  // at runtime is not supported); the wrapper reads the current prop per
102
121
  // call so a rebound handler identity stays honored.
103
122
  save: props.save ? (cfg) => props.save(cfg) : void 0,
123
+ confirmPrune,
104
124
  onClose: () => {
105
125
  editorOpen.value = false;
106
126
  }
@@ -243,6 +263,11 @@ const isMac = computed(
243
263
  />
244
264
  </div>
245
265
  </div>
266
+ <PruneDialog
267
+ :items="pendingPrune"
268
+ @confirm="resolvePrune(true)"
269
+ @cancel="resolvePrune(false)"
270
+ />
246
271
  <template #footer>
247
272
  <EditorFooter
248
273
  :state="editorState"
@@ -0,0 +1,14 @@
1
+ import { type PruneSummary } from './utils/prune.js';
2
+ type __VLS_Props = {
3
+ /** Non-null while the gate is open; one entry per item about to be dropped. */
4
+ items: ReadonlyArray<PruneSummary> | null;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ cancel: () => any;
8
+ confirm: () => any;
9
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
+ onCancel?: (() => any) | undefined;
11
+ onConfirm?: (() => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -0,0 +1,72 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { Icon } from "@iconify/vue";
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle
13
+ } from "../ui/alert-dialog";
14
+ import { pruneKindNoun } from "./utils/prune";
15
+ defineOptions({ name: "ShwfedConfigPruneDialog" });
16
+ const props = defineProps({
17
+ items: { type: [Array, null], required: true }
18
+ });
19
+ const emit = defineEmits(["confirm", "cancel"]);
20
+ const open = computed(() => props.items !== null);
21
+ function onOpenChange(next) {
22
+ if (!next) emit("cancel");
23
+ }
24
+ </script>
25
+
26
+ <template>
27
+ <AlertDialog
28
+ :open="open"
29
+ @update:open="onOpenChange"
30
+ >
31
+ <AlertDialogContent class="sm:px-12 sm:py-8 flex flex-col gap-8">
32
+ <AlertDialogHeader class="sr-only">
33
+ <AlertDialogTitle>清理未使用的内容</AlertDialogTitle>
34
+ <AlertDialogDescription>Confirmation dialog</AlertDialogDescription>
35
+ </AlertDialogHeader>
36
+ <Icon
37
+ icon="fluent:broom-20-filled"
38
+ class="text-6xl mx-auto text-yellow-500"
39
+ />
40
+ <div class="flex flex-col gap-3">
41
+ <p class="text-sm text-zinc-900">
42
+ 以下内容未放入任何布局,不会在页面上显示。保存时将一并删除:
43
+ </p>
44
+ <ul class="max-h-60 overflow-y-auto flex flex-col gap-1 rounded border border-zinc-200 bg-zinc-50 p-3">
45
+ <li
46
+ v-for="(item, i) in props.items ?? []"
47
+ :key="i"
48
+ class="flex items-baseline gap-2 text-sm text-zinc-700"
49
+ >
50
+ <span class="shrink-0 rounded bg-zinc-200 px-1.5 py-0.5 text-xs text-zinc-600">
51
+ {{ pruneKindNoun(item.kind) }}
52
+ </span>
53
+ <span class="break-all">{{ item.label }}</span>
54
+ </li>
55
+ </ul>
56
+ <p class="text-xs text-zinc-500">
57
+ 若仍需保留,请点击取消,并将其放入布局。
58
+ </p>
59
+ </div>
60
+ <AlertDialogFooter>
61
+ <AlertDialogCancel @click="emit('cancel')">
62
+ <Icon icon="fluent:dismiss-20-regular" />
63
+ 取消
64
+ </AlertDialogCancel>
65
+ <AlertDialogAction @click="emit('confirm')">
66
+ <Icon icon="fluent:checkmark-20-regular" />
67
+ 删除并保存
68
+ </AlertDialogAction>
69
+ </AlertDialogFooter>
70
+ </AlertDialogContent>
71
+ </AlertDialog>
72
+ </template>
@@ -0,0 +1,14 @@
1
+ import { type PruneSummary } from './utils/prune.js';
2
+ type __VLS_Props = {
3
+ /** Non-null while the gate is open; one entry per item about to be dropped. */
4
+ items: ReadonlyArray<PruneSummary> | null;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
7
+ cancel: () => any;
8
+ confirm: () => any;
9
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
10
+ onCancel?: (() => any) | undefined;
11
+ onConfirm?: (() => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -33,7 +33,7 @@ export const metadata = {
33
33
  icon: httpRequestOperation.metadata.icon
34
34
  },
35
35
  // Navigation as an op (replaces the standalone navigation *button*): its URL
36
- // expression can reference `steps["<id>"]`, so a chain can navigate using an
36
+ // expression can reference `steps[?"<id>"]`, so a chain can navigate using an
37
37
  // earlier request's response. The page hosts it for the same reason as the
38
38
  // request op — navigation acts on nothing instance-specific.
39
39
  {
@@ -43,7 +43,7 @@ export const metadata = {
43
43
  icon: navigationOperation.metadata.icon
44
44
  },
45
45
  // Download as an op (replaces the standalone download *button*): its request
46
- // expression can reference `steps["<id>"]`, so the old two-step「先取凭据再下载」
46
+ // expression can reference `steps[?"<id>"]`, so the old two-step「先取凭据再下载」
47
47
  // becomes a 发送请求 row followed by this download row. The page hosts it for
48
48
  // the same reason as the request op — the download acts on nothing
49
49
  // instance-specific.
@@ -4,6 +4,7 @@ import { type LayoutSetValue, type PlacementValue, type SlotValue } from '../../
4
4
  import type { BreadcrumbCrumb, BreadcrumbParentSuffix } from './breadcrumb-extension.js';
5
5
  import { type BlockValue, type PageConfigValue } from './schema.js';
6
6
  import { type BlockEntry } from './utils/resolve.js';
7
+ import { type PruneSummary } from './utils/prune.js';
7
8
  export type PickerEntry = {
8
9
  type: string;
9
10
  compatibilityDate: string;
@@ -133,6 +134,14 @@ export interface UseConfigEditorOptions {
133
134
  * the editor guarantees the session is never lost to a failed save.
134
135
  */
135
136
  save?: (config: PageConfigValue) => void | Promise<void>;
137
+ /**
138
+ * Consent gate for the save-time prune of unplaced items (utils/prune.ts).
139
+ * Called only when 确认 would actually drop something, with a breadcrumb per
140
+ * removal; resolving `false` aborts the save with the draft untouched. Hosts
141
+ * wire this to a confirmation dialog — `ShwfedConfig` does. Left unwired, the
142
+ * prune is silent.
143
+ */
144
+ confirmPrune?: (items: ReadonlyArray<PruneSummary>) => boolean | Promise<boolean>;
136
145
  /**
137
146
  * Called after a successful 确认 commits the draft to the outer model.
138
147
  * Hosts wire this to close the modal; legacy mode emits `close`.
@@ -7,6 +7,12 @@ import { PageConfig } from "./schema.js";
7
7
  import { BLOCKS, findBlock } from "./utils/resolve.js";
8
8
  import { formatValidationError } from "./utils/validation-error.js";
9
9
  import { findDanglingLinkages, formatDanglingLinkages } from "./utils/linkage.js";
10
+ import {
11
+ findPruneConflicts,
12
+ formatPruneConflicts,
13
+ pruneUnplaced,
14
+ summarizePrune
15
+ } from "./utils/prune.js";
10
16
  import { createExpressionValidityRegistry, provideExpressionValidity } from "../ui/expression-editor/index.js";
11
17
  function deepClone(value) {
12
18
  return JSON.parse(JSON.stringify(value));
@@ -396,20 +402,42 @@ export function useConfigEditor(model, options = {}) {
396
402
  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";
397
403
  return false;
398
404
  }
399
- const dangling = findDanglingLinkages(decoded);
400
- if (dangling.length > 0) {
401
- validationError.value = formatDanglingLinkages(dangling, decoded, (i) => {
402
- const b = topSlotEditor.blocks.value[i];
403
- return b ? topSlotEditor.getBlockLabel(b) : void 0;
405
+ const { config: pruned, removed } = pruneUnplaced(decoded);
406
+ const draftBlockLabelAt = (i) => {
407
+ const b = topSlotEditor.blocks.value[i];
408
+ return b ? topSlotEditor.getBlockLabel(b) : void 0;
409
+ };
410
+ const prunedBlockLabelAt = (i) => {
411
+ const b = pruned.slot.blocks[i];
412
+ return b ? topSlotEditor.getBlockLabel(b) : void 0;
413
+ };
414
+ const conflicts = findPruneConflicts(pruned, removed);
415
+ if (conflicts.length > 0) {
416
+ validationError.value = formatPruneConflicts(conflicts, {
417
+ prunedRoot: pruned,
418
+ originalRoot: decoded,
419
+ prunedBlockLabelAt,
420
+ originalBlockLabelAt: draftBlockLabelAt
404
421
  });
405
422
  return false;
406
423
  }
424
+ const dangling = findDanglingLinkages(pruned);
425
+ if (dangling.length > 0) {
426
+ validationError.value = formatDanglingLinkages(dangling, pruned, prunedBlockLabelAt);
427
+ return false;
428
+ }
407
429
  validationError.value = null;
430
+ if (removed.length > 0 && options.confirmPrune) {
431
+ const proceed = await options.confirmPrune(
432
+ summarizePrune(removed, decoded, draftBlockLabelAt)
433
+ );
434
+ if (!proceed) return false;
435
+ }
408
436
  if (options.save) {
409
437
  saving.value = true;
410
438
  saveError.value = null;
411
439
  try {
412
- await options.save(deepClone(decoded));
440
+ await options.save(deepClone(pruned));
413
441
  } catch (err) {
414
442
  saveError.value = `\u4FDD\u5B58\u5931\u8D25\uFF1A${err instanceof Error ? err.message : String(err)}`;
415
443
  return false;
@@ -417,9 +445,9 @@ export function useConfigEditor(model, options = {}) {
417
445
  saving.value = false;
418
446
  }
419
447
  }
420
- model.value = decoded;
421
- baseline.value = deepClone(decoded);
422
- draft.value = deepClone(decoded);
448
+ model.value = pruned;
449
+ baseline.value = deepClone(pruned);
450
+ draft.value = deepClone(pruned);
423
451
  stack.value = [{ kind: "layout" }];
424
452
  topSlotEditor.selectedBlockIds.value = [];
425
453
  topSlotEditor.activeLayoutIndex.value = 0;
@@ -11,6 +11,13 @@ export type DanglingLinkage = Readonly<{
11
11
  */
12
12
  path: ReadonlyArray<PropertyKey>;
13
13
  }>;
14
+ /**
15
+ * Every uuid `id` defined under `node`. Exported so the save-time prune
16
+ * (utils/prune.ts) asks what a to-be-removed subtree *defines* with the very
17
+ * same rule this gate uses to decide what is *defined* — the two must agree, or
18
+ * a prune would strand a linkage this walker then fails to flag.
19
+ */
20
+ export declare function definedIds(node: unknown): Set<string>;
14
21
  /**
15
22
  * Find every linkage in `config` whose target uuid is not defined anywhere in
16
23
  * the tree. `PAGE_TARGET_ID` (the singleton page target sentinel) is always
@@ -19,6 +26,7 @@ export type DanglingLinkage = Readonly<{
19
26
  * the caller can render a field-level breadcrumb (see `describePath`).
20
27
  */
21
28
  export declare function findDanglingLinkages(config: unknown): DanglingLinkage[];
29
+ export declare const LINKAGE_KIND_LABEL: Record<LinkageKind, string>;
22
30
  /**
23
31
  * One-line save-footer message for a non-empty dangling list. Renders the
24
32
  * breadcrumb to the first offending reference (`「表单 › 确定 › value」`) via the
@@ -17,6 +17,11 @@ function collectDefinedIds(node, into) {
17
17
  collectDefinedIds(v, into);
18
18
  }
19
19
  }
20
+ export function definedIds(node) {
21
+ const out = /* @__PURE__ */ new Set();
22
+ collectDefinedIds(node, out);
23
+ return out;
24
+ }
20
25
  function collectRefs(node, path, into) {
21
26
  if (Array.isArray(node)) {
22
27
  node.forEach((v, i) => collectRefs(v, [...path, i], into));
@@ -41,7 +46,7 @@ export function findDanglingLinkages(config) {
41
46
  collectRefs(config, [], refs);
42
47
  return refs.filter((r) => r.id !== PAGE_TARGET_ID && !defined.has(r.id));
43
48
  }
44
- const KIND_LABEL = {
49
+ export const LINKAGE_KIND_LABEL = {
45
50
  operation: "\u89E6\u53D1\u64CD\u4F5C",
46
51
  step: "\u6B65\u9AA4\u5F15\u7528",
47
52
  scope: "\u8DE8\u5C42\u4F5C\u7528\u57DF\u5F15\u7528"
@@ -49,7 +54,7 @@ const KIND_LABEL = {
49
54
  export function formatDanglingLinkages(dangling, rootData, blockLabelAt) {
50
55
  const first = dangling[0];
51
56
  if (!first) return "";
52
- const kind = KIND_LABEL[first.kind];
57
+ const kind = LINKAGE_KIND_LABEL[first.kind];
53
58
  const segs = describePath(first.path, rootData, blockLabelAt);
54
59
  const where = segs.length > 0 ? `\u300C${segs.join(" \u203A ")}\u300D\u7684${kind}` : kind;
55
60
  const suffix = dangling.length > 1 ? `\uFF08\u8FD8\u6709 ${dangling.length - 1} \u4E2A\uFF09` : "";