@shwfed/config 3.0.4 → 3.0.7

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 (87) 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 +1 -1
  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/config.vue +109 -3
  9. package/dist/runtime/components/actions/utils/steps.d.ts +1 -1
  10. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.d.vue.ts +74 -74
  11. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.vue.d.ts +74 -74
  12. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.d.vue.ts +74 -74
  13. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.vue.d.ts +74 -74
  14. package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/schema.d.ts +37 -37
  15. package/dist/runtime/components/config/history-picker.vue +6 -4
  16. package/dist/runtime/components/config/index.vue +25 -0
  17. package/dist/runtime/components/config/prune-dialog.d.vue.ts +14 -0
  18. package/dist/runtime/components/config/prune-dialog.vue +83 -0
  19. package/dist/runtime/components/config/prune-dialog.vue.d.ts +14 -0
  20. package/dist/runtime/components/config/schema.js +2 -2
  21. package/dist/runtime/components/config/use-editor.d.ts +9 -0
  22. package/dist/runtime/components/config/use-editor.js +39 -11
  23. package/dist/runtime/components/config/utils/linkage.d.ts +8 -0
  24. package/dist/runtime/components/config/utils/linkage.js +7 -2
  25. package/dist/runtime/components/config/utils/prune.d.ts +68 -0
  26. package/dist/runtime/components/config/utils/prune.js +92 -0
  27. package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.text/config.d.vue.ts +22 -22
  28. package/dist/runtime/components/form/fields/2026-04-22/com.shwfed.form.field.text/config.vue.d.ts +22 -22
  29. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/config.d.vue.ts +22 -22
  30. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/config.vue.d.ts +22 -22
  31. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.d.vue.ts +31 -0
  32. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.vue +191 -0
  33. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.vue.d.ts +31 -0
  34. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/expand.d.ts +49 -0
  35. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/expand.js +87 -0
  36. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/runtime.d.vue.ts +8 -0
  37. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/runtime.vue +72 -0
  38. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/runtime.vue.d.ts +8 -0
  39. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/schema.d.ts +59 -0
  40. package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/schema.js +139 -0
  41. package/dist/runtime/components/form/unit-config.vue +2 -2
  42. package/dist/runtime/components/form/utils/form-vars.d.ts +5 -15
  43. package/dist/runtime/components/form/utils/form-vars.js +12 -6
  44. package/dist/runtime/components/operations/2026-06-11/com.shwfed.operation.http.request.json/schema.js +2 -2
  45. package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.http.download/schema.js +2 -2
  46. package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/runtime.js +5 -11
  47. package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/schema.js +1 -1
  48. package/dist/runtime/components/operations/utils/resolve.d.ts +1 -1
  49. package/dist/runtime/components/operations/utils/step-vars.d.ts +4 -2
  50. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.select/runtime.vue +1 -21
  51. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.select/schema.d.ts +2 -0
  52. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.select/schema.js +16 -1
  53. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  54. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  55. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  56. package/dist/runtime/components/table/columns/2026-05-27/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  57. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  58. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  59. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  60. package/dist/runtime/components/table/columns/2026-05-28/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  61. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.d.vue.ts +2 -2
  62. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-multi/config.vue.d.ts +2 -2
  63. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.d.vue.ts +2 -2
  64. package/dist/runtime/components/table/columns/2026-06-14/com.shwfed.table.column.tree-combobox-single/config.vue.d.ts +2 -2
  65. package/dist/runtime/components/table/config.vue +2 -2
  66. package/dist/runtime/components/table/index.vue +15 -0
  67. package/dist/runtime/components/table/schema.d.ts +74 -74
  68. package/dist/runtime/components/table/utils/resolve.d.ts +26 -0
  69. package/dist/runtime/components/table/utils/resolve.js +2 -1
  70. package/dist/runtime/components/ui/expression-editor/picker-entries.js +1 -1
  71. package/dist/runtime/components/ui/expression-editor/step-chip-extension.js +1 -1
  72. package/dist/runtime/components/ui/expression-editor/step-refs.js +1 -1
  73. package/dist/runtime/share/clipboard.d.ts +15 -9
  74. package/dist/runtime/share/clipboard.js +22 -10
  75. package/dist/runtime/share/event-bus.js +2 -2
  76. package/dist/runtime/share/navigation.d.ts +1 -0
  77. package/dist/runtime/share/navigation.js +4 -0
  78. package/dist/runtime/utils/cel-context.d.ts +2 -2
  79. package/dist/runtime/vendor/cel-js/CLAUDE.md +7 -2
  80. package/dist/runtime/vendor/cel-js/PROMPT.md +27 -0
  81. package/dist/runtime/vendor/cel-js/lib/evaluator.js +2 -0
  82. package/dist/runtime/vendor/cel-js/lib/globals.d.ts +2 -0
  83. package/dist/runtime/vendor/cel-js/lib/globals.js +10 -0
  84. package/dist/runtime/vendor/cel-js/lib/map-builtins.d.ts +2 -0
  85. package/dist/runtime/vendor/cel-js/lib/map-builtins.js +80 -0
  86. package/dist/runtime/vendor/cel-js/lib/operators.js +2 -12
  87. package/package.json +1 -1
@@ -47,40 +47,6 @@ export declare function schema(configure: (env: Environment) => void, _blockRef:
47
47
  readonly message: string;
48
48
  }[]] | undefined;
49
49
  }[];
50
- readonly query?: {
51
- readonly style?: string | undefined;
52
- readonly id?: string | undefined;
53
- readonly initial?: {
54
- readonly data: string;
55
- readonly request?: string | undefined;
56
- } | undefined;
57
- readonly displayName?: string | undefined;
58
- readonly kind: "shwfed.component.form";
59
- readonly fields: readonly any[];
60
- readonly layouts: readonly {
61
- readonly name: string;
62
- readonly layout: {
63
- readonly style?: string | undefined;
64
- readonly columns: number;
65
- readonly gap?: number | undefined;
66
- readonly rows?: number | undefined;
67
- readonly placements: {
68
- readonly [x: string]: {
69
- readonly area: readonly [readonly [number, number], readonly [number, number]];
70
- readonly v?: "stretch" | "center" | "end" | "start" | undefined;
71
- readonly h?: "stretch" | "center" | "end" | "start" | undefined;
72
- };
73
- };
74
- };
75
- readonly media?: string | undefined;
76
- }[];
77
- readonly readonly?: string | undefined;
78
- } | undefined;
79
- readonly dataSource?: {
80
- readonly data: string;
81
- readonly total?: string | undefined;
82
- readonly request?: string | undefined;
83
- } | undefined;
84
50
  readonly actions?: {
85
51
  readonly size: "default" | "sm" | "xs";
86
52
  readonly style?: string | undefined;
@@ -148,6 +114,40 @@ export declare function schema(configure: (env: Environment) => void, _blockRef:
148
114
  readonly variant?: "default" | "link" | "destructive" | "primary" | "ghost" | undefined;
149
115
  }[];
150
116
  } | undefined;
117
+ readonly query?: {
118
+ readonly style?: string | undefined;
119
+ readonly id?: string | undefined;
120
+ readonly initial?: {
121
+ readonly data: string;
122
+ readonly request?: string | undefined;
123
+ } | undefined;
124
+ readonly displayName?: string | undefined;
125
+ readonly kind: "shwfed.component.form";
126
+ readonly fields: readonly any[];
127
+ readonly layouts: readonly {
128
+ readonly name: string;
129
+ readonly layout: {
130
+ readonly style?: string | undefined;
131
+ readonly columns: number;
132
+ readonly gap?: number | undefined;
133
+ readonly rows?: number | undefined;
134
+ readonly placements: {
135
+ readonly [x: string]: {
136
+ readonly area: readonly [readonly [number, number], readonly [number, number]];
137
+ readonly v?: "stretch" | "center" | "end" | "start" | undefined;
138
+ readonly h?: "stretch" | "center" | "end" | "start" | undefined;
139
+ };
140
+ };
141
+ };
142
+ readonly media?: string | undefined;
143
+ }[];
144
+ readonly readonly?: string | undefined;
145
+ } | undefined;
146
+ readonly dataSource?: {
147
+ readonly data: string;
148
+ readonly total?: string | undefined;
149
+ readonly request?: string | undefined;
150
+ } | undefined;
151
151
  readonly bottomActions?: {
152
152
  readonly size: "default" | "sm" | "xs";
153
153
  readonly style?: string | undefined;
@@ -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">;
@@ -14,7 +14,6 @@ import {
14
14
  } from "../ui/command";
15
15
  import {
16
16
  AlertDialog,
17
- AlertDialogAction,
18
17
  AlertDialogCancel,
19
18
  AlertDialogContent,
20
19
  AlertDialogDescription,
@@ -140,14 +139,17 @@ function onCancel() {
140
139
  {{ t("history-confirm-body") }}
141
140
  </p>
142
141
  <AlertDialogFooter>
143
- <AlertDialogCancel @click="onCancel">
142
+ <AlertDialogCancel>
144
143
  <Icon icon="fluent:dismiss-20-regular" />
145
144
  {{ t("history-cancel") }}
146
145
  </AlertDialogCancel>
147
- <AlertDialogAction @click="onConfirm">
146
+ <Button
147
+ variant="primary"
148
+ @click="onConfirm"
149
+ >
148
150
  <Icon icon="fluent:checkmark-20-regular" />
149
151
  {{ t("history-confirm") }}
150
- </AlertDialogAction>
152
+ </Button>
151
153
  </AlertDialogFooter>
152
154
  </AlertDialogContent>
153
155
  </AlertDialog>
@@ -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,83 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { Icon } from "@iconify/vue";
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogCancel,
7
+ AlertDialogContent,
8
+ AlertDialogDescription,
9
+ AlertDialogFooter,
10
+ AlertDialogHeader,
11
+ AlertDialogTitle
12
+ } from "../ui/alert-dialog";
13
+ import { Button } from "../ui/button";
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
+ <!-- Closes itself, which `onOpenChange` reads as the 取消. -->
62
+ <AlertDialogCancel>
63
+ <Icon icon="fluent:dismiss-20-regular" />
64
+ 取消
65
+ </AlertDialogCancel>
66
+ <!-- A plain Button, NOT an AlertDialogAction: reka's action IS a
67
+ DialogClose, and its built-in close handler — bound in reka's own
68
+ template — runs BEFORE a `@click` we hang on it, which arrives as a
69
+ fallthrough attr and merges after. The close would reach
70
+ `onOpenChange` first and cancel the very save this button
71
+ authorizes. The dialog shuts anyway: the host drops `items` the
72
+ moment it takes the verdict. -->
73
+ <Button
74
+ variant="primary"
75
+ @click="emit('confirm')"
76
+ >
77
+ <Icon icon="fluent:checkmark-20-regular" />
78
+ 删除并保存
79
+ </Button>
80
+ </AlertDialogFooter>
81
+ </AlertDialogContent>
82
+ </AlertDialog>
83
+ </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));
@@ -199,7 +205,7 @@ export function useSlotClipboard(target) {
199
205
  if (p) placements[id] = p;
200
206
  }
201
207
  }
202
- if (!await writeClip("block", items, placements)) {
208
+ if (!await writeClip("block", { items, placements })) {
203
209
  toast.error("\u590D\u5236\u5931\u8D25");
204
210
  return;
205
211
  }
@@ -208,7 +214,7 @@ export function useSlotClipboard(target) {
208
214
  async function paste(anchor) {
209
215
  const payload = await readClip();
210
216
  if (!payload || payload.surface !== "block") return;
211
- const { items, placements } = reidFragment(payload.items, payload.placements);
217
+ const { items, placements } = reidFragment(payload);
212
218
  const pasted = items;
213
219
  if (pasted.length === 0) return;
214
220
  if (placements && Object.keys(placements).length > 0) {
@@ -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` : "";
@@ -0,0 +1,68 @@
1
+ import { type DanglingLinkage } from './linkage.js';
2
+ /** Which pool an item came out of — drives the noun in user-facing copy. */
3
+ export type PruneKind = 'block' | 'field';
4
+ export declare function pruneKindNoun(kind: PruneKind): string;
5
+ export type UnplacedItem = Readonly<{
6
+ kind: PruneKind;
7
+ id: string;
8
+ /**
9
+ * Property path from the ORIGINAL config root to the removed item
10
+ * (`['slot','blocks',2]`). Fed to `describePath` against that same root.
11
+ */
12
+ path: ReadonlyArray<PropertyKey>;
13
+ /**
14
+ * Every uuid defined anywhere in this item's subtree — the item's own id plus
15
+ * its trigger rows, nested blocks, nested fields, … Dropping the item
16
+ * un-defines all of them at once, so this is the set a surviving reference
17
+ * must not point into.
18
+ */
19
+ ids: ReadonlySet<string>;
20
+ }>;
21
+ /** A pruned item's label, pre-rendered for the confirmation dialog. */
22
+ export type PruneSummary = Readonly<{
23
+ kind: PruneKind;
24
+ /** Breadcrumb to the item in the pre-prune config (`标签页 › 表格`). */
25
+ label: string;
26
+ }>;
27
+ export type PruneConflict = Readonly<{
28
+ /** The surviving reference left pointing at nothing. */
29
+ ref: DanglingLinkage;
30
+ /** The item whose removal stranded it. */
31
+ owner: UnplacedItem;
32
+ }>;
33
+ /**
34
+ * Drop every item that no layout in its own pool places, at every depth.
35
+ * Returns the rewritten config plus the removals, outermost-first.
36
+ */
37
+ export declare function pruneUnplaced<T>(config: T): {
38
+ config: T;
39
+ removed: UnplacedItem[];
40
+ };
41
+ /**
42
+ * References held by SURVIVORS that point into a removed subtree. Non-empty
43
+ * means the prune would strand a linkage, and the save must be refused rather
44
+ * than quietly persisting a config whose trigger targets nothing.
45
+ *
46
+ * Runs the existing dangling-linkage walker over the PRUNED tree and keeps only
47
+ * the danglers the prune itself caused — a linkage that was already dangling
48
+ * before the prune is not our problem and stays with `findDanglingLinkages`.
49
+ */
50
+ export declare function findPruneConflicts(pruned: unknown, removed: ReadonlyArray<UnplacedItem>): PruneConflict[];
51
+ /**
52
+ * Pre-render each removal as a breadcrumb for the confirmation dialog.
53
+ * `root` must be the pre-prune config the paths index into.
54
+ */
55
+ export declare function summarizePrune(removed: ReadonlyArray<UnplacedItem>, root: unknown, blockLabelAt: (index: number) => string | undefined): PruneSummary[];
56
+ /**
57
+ * Save-footer message for a non-empty conflict list. Names both ends — the item
58
+ * about to be dropped and the surviving reference that still needs it — because
59
+ * the raw dangling message ("引用了已失效的资源") would point at the reference
60
+ * and leave the designer hunting for a block that looks perfectly fine sitting
61
+ * in the unplaced tray.
62
+ */
63
+ export declare function formatPruneConflicts(conflicts: ReadonlyArray<PruneConflict>, ctx: {
64
+ prunedRoot: unknown;
65
+ originalRoot: unknown;
66
+ prunedBlockLabelAt: (index: number) => string | undefined;
67
+ originalBlockLabelAt: (index: number) => string | undefined;
68
+ }): string;
@@ -0,0 +1,92 @@
1
+ import { definedIds, findDanglingLinkages, LINKAGE_KIND_LABEL } from "./linkage.js";
2
+ import { describePath } from "./validation-error.js";
3
+ const KIND_NOUN = {
4
+ block: "\u5757",
5
+ field: "\u5B57\u6BB5"
6
+ };
7
+ export function pruneKindNoun(kind) {
8
+ return KIND_NOUN[kind];
9
+ }
10
+ function isObject(v) {
11
+ return v !== null && typeof v === "object" && !Array.isArray(v);
12
+ }
13
+ const POOLS = [
14
+ { key: "blocks", kind: "block" },
15
+ { key: "fields", kind: "field" }
16
+ ];
17
+ function poolOf(node) {
18
+ if (!Array.isArray(node.layouts)) return void 0;
19
+ return POOLS.find((p) => Array.isArray(node[p.key]));
20
+ }
21
+ function placedIds(layouts) {
22
+ const out = /* @__PURE__ */ new Set();
23
+ for (const set of layouts) {
24
+ if (!isObject(set) || !isObject(set.layout)) continue;
25
+ const placements = set.layout.placements;
26
+ if (isObject(placements)) {
27
+ for (const id of Object.keys(placements)) out.add(id);
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+ function walk(node, path, removed) {
33
+ if (Array.isArray(node)) return node.map((v, i) => walk(v, [...path, i], removed));
34
+ if (!isObject(node)) return node;
35
+ const pool = poolOf(node);
36
+ const out = {};
37
+ for (const [key, value] of Object.entries(node)) {
38
+ if (pool && key === pool.key) {
39
+ const placed = placedIds(node.layouts);
40
+ const kept = [];
41
+ for (const [i, item] of value.entries()) {
42
+ const id = isObject(item) && typeof item.id === "string" ? item.id : void 0;
43
+ if (id !== void 0 && !placed.has(id)) {
44
+ removed.push({ kind: pool.kind, id, path: [...path, key, i], ids: definedIds(item) });
45
+ continue;
46
+ }
47
+ kept.push(walk(item, [...path, key, i], removed));
48
+ }
49
+ out[key] = kept;
50
+ continue;
51
+ }
52
+ out[key] = walk(value, [...path, key], removed);
53
+ }
54
+ return out;
55
+ }
56
+ export function pruneUnplaced(config) {
57
+ const removed = [];
58
+ const next = walk(config, [], removed);
59
+ return { config: next, removed };
60
+ }
61
+ export function findPruneConflicts(pruned, removed) {
62
+ if (removed.length === 0) return [];
63
+ const owner = /* @__PURE__ */ new Map();
64
+ for (const item of removed) {
65
+ for (const id of item.ids) {
66
+ if (!owner.has(id)) owner.set(id, item);
67
+ }
68
+ }
69
+ const out = [];
70
+ for (const ref of findDanglingLinkages(pruned)) {
71
+ const o = owner.get(ref.id);
72
+ if (o) out.push({ ref, owner: o });
73
+ }
74
+ return out;
75
+ }
76
+ export function summarizePrune(removed, root, blockLabelAt) {
77
+ return removed.map((item) => ({
78
+ kind: item.kind,
79
+ label: describePath(item.path, root, blockLabelAt).join(" \u203A ") || pruneKindNoun(item.kind)
80
+ }));
81
+ }
82
+ export function formatPruneConflicts(conflicts, ctx) {
83
+ const first = conflicts[0];
84
+ if (!first) return "";
85
+ const ownerSegs = describePath(first.owner.path, ctx.originalRoot, ctx.originalBlockLabelAt);
86
+ const owner = ownerSegs.length > 0 ? ownerSegs.join(" \u203A ") : pruneKindNoun(first.owner.kind);
87
+ const refSegs = describePath(first.ref.path, ctx.prunedRoot, ctx.prunedBlockLabelAt);
88
+ const kind = LINKAGE_KIND_LABEL[first.ref.kind];
89
+ const where = refSegs.length > 0 ? `\u300C${refSegs.join(" \u203A ")}\u300D\u7684${kind}` : kind;
90
+ const suffix = conflicts.length > 1 ? `\uFF08\u8FD8\u6709 ${conflicts.length - 1} \u4E2A\uFF09` : "";
91
+ return `${pruneKindNoun(first.owner.kind)}\u300C${owner}\u300D\u672A\u653E\u5165\u4EFB\u4F55\u5E03\u5C40\uFF0C\u4FDD\u5B58\u65F6\u4F1A\u88AB\u6E05\u7406\uFF0C\u4F46${where}\u4ECD\u5728\u5F15\u7528\u5B83\u3002\u8BF7\u5148\u5C06\u5B83\u653E\u5165\u5E03\u5C40\uFF0C\u6216\u79FB\u9664\u8BE5\u5F15\u7528${suffix}`;
92
+ }