@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.
- package/dist/module.json +1 -1
- package/dist/runtime/components/actions/buttons/2026-04-21/com.shwfed.actions.button.navigation/runtime.vue +4 -10
- package/dist/runtime/components/actions/buttons/2026-05-21/com.shwfed.actions.button.http.download/schema.d.ts +1 -1
- package/dist/runtime/components/actions/buttons/2026-05-21/com.shwfed.actions.button.http.download/schema.js +2 -2
- package/dist/runtime/components/actions/buttons/2026-07-13/com.shwfed.actions.button.state.write/runtime.vue +22 -2
- package/dist/runtime/components/actions/buttons/2026-07-13/com.shwfed.actions.button.state.write/schema.d.ts +4 -2
- package/dist/runtime/components/actions/buttons/2026-07-13/com.shwfed.actions.button.state.write/schema.js +1 -1
- package/dist/runtime/components/actions/utils/steps.d.ts +1 -1
- package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.d.vue.ts +6 -6
- package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/config.vue.d.ts +6 -6
- package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.d.vue.ts +6 -6
- package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/runtime.vue.d.ts +6 -6
- package/dist/runtime/components/config/blocks/2026-05-06/com.shwfed.block.table/schema.d.ts +3 -3
- package/dist/runtime/components/config/index.vue +25 -0
- package/dist/runtime/components/config/prune-dialog.d.vue.ts +14 -0
- package/dist/runtime/components/config/prune-dialog.vue +72 -0
- package/dist/runtime/components/config/prune-dialog.vue.d.ts +14 -0
- package/dist/runtime/components/config/schema.js +2 -2
- package/dist/runtime/components/config/use-editor.d.ts +9 -0
- package/dist/runtime/components/config/use-editor.js +37 -9
- package/dist/runtime/components/config/utils/linkage.d.ts +8 -0
- package/dist/runtime/components/config/utils/linkage.js +7 -2
- package/dist/runtime/components/config/utils/prune.d.ts +68 -0
- package/dist/runtime/components/config/utils/prune.js +92 -0
- package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.d.vue.ts +31 -0
- package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.vue +191 -0
- package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.vue.d.ts +31 -0
- package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/expand.d.ts +49 -0
- package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/expand.js +87 -0
- package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/runtime.d.vue.ts +8 -0
- package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/runtime.vue +72 -0
- package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/runtime.vue.d.ts +8 -0
- package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/schema.d.ts +59 -0
- package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/schema.js +139 -0
- package/dist/runtime/components/form/utils/form-vars.d.ts +5 -15
- package/dist/runtime/components/form/utils/form-vars.js +12 -6
- package/dist/runtime/components/operations/2026-06-11/com.shwfed.operation.http.request.json/schema.js +2 -2
- package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.http.download/schema.js +2 -2
- package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/runtime.js +5 -11
- package/dist/runtime/components/operations/2026-06-15/com.shwfed.operation.navigation/schema.js +1 -1
- package/dist/runtime/components/operations/utils/resolve.d.ts +1 -1
- package/dist/runtime/components/operations/utils/step-vars.d.ts +4 -2
- package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.select/runtime.vue +1 -21
- package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.select/schema.d.ts +2 -0
- package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.select/schema.js +16 -1
- package/dist/runtime/components/table/index.vue +15 -0
- package/dist/runtime/components/table/schema.d.ts +6 -6
- package/dist/runtime/components/table/utils/resolve.d.ts +26 -0
- package/dist/runtime/components/table/utils/resolve.js +2 -1
- package/dist/runtime/components/ui/expression-editor/picker-entries.js +1 -1
- package/dist/runtime/components/ui/expression-editor/step-chip-extension.js +1 -1
- package/dist/runtime/components/ui/expression-editor/step-refs.js +1 -1
- package/dist/runtime/share/event-bus.js +2 -2
- package/dist/runtime/share/navigation.d.ts +1 -0
- package/dist/runtime/share/navigation.js +4 -0
- package/dist/runtime/utils/cel-context.d.ts +2 -2
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.d.vue.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Value } from './schema.js';
|
|
2
|
+
type __VLS_ModelProps = {
|
|
3
|
+
modelValue: Value;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
|
+
"update:modelValue": (value: {
|
|
7
|
+
readonly type: "com.shwfed.form.field.dynamic";
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly hidden?: string | undefined;
|
|
10
|
+
readonly displayName?: string | undefined;
|
|
11
|
+
readonly transform: string;
|
|
12
|
+
readonly columns?: number | undefined;
|
|
13
|
+
readonly compatibilityDate: "2026-07-13";
|
|
14
|
+
readonly orientation?: "vertical" | "floating" | undefined;
|
|
15
|
+
readonly request: string;
|
|
16
|
+
}) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
18
|
+
"onUpdate:modelValue"?: ((value: {
|
|
19
|
+
readonly type: "com.shwfed.form.field.dynamic";
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly hidden?: string | undefined;
|
|
22
|
+
readonly displayName?: string | undefined;
|
|
23
|
+
readonly transform: string;
|
|
24
|
+
readonly columns?: number | undefined;
|
|
25
|
+
readonly compatibilityDate: "2026-07-13";
|
|
26
|
+
readonly orientation?: "vertical" | "floating" | undefined;
|
|
27
|
+
readonly request: string;
|
|
28
|
+
}) => any) | undefined;
|
|
29
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
30
|
+
declare const _default: typeof __VLS_export;
|
|
31
|
+
export default _default;
|
package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.vue
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Field, FieldLabel } from "../../../../ui/field";
|
|
3
|
+
import { ExpressionEditor } from "../../../../ui/expression-editor";
|
|
4
|
+
import { InputGroup, InputGroupInput } from "../../../../ui/input-group";
|
|
5
|
+
import { Markdown } from "../../../../ui/markdown";
|
|
6
|
+
import {
|
|
7
|
+
Select,
|
|
8
|
+
SelectContent,
|
|
9
|
+
SelectItem,
|
|
10
|
+
SelectTrigger,
|
|
11
|
+
SelectValue
|
|
12
|
+
} from "../../../../ui/select";
|
|
13
|
+
import { getStructFieldDescription, getStructFieldTitle } from "../../../schema";
|
|
14
|
+
import { DEFAULT_FIELD_ORIENTATION, FIELD_ORIENTATION_OPTIONS } from "../../../utils/common";
|
|
15
|
+
import { schema } from "./schema";
|
|
16
|
+
defineOptions({ name: "ShwfedDynamicFieldConfig" });
|
|
17
|
+
const value = defineModel({ type: null, ...{ required: true } });
|
|
18
|
+
const fieldSchema = schema(() => {
|
|
19
|
+
});
|
|
20
|
+
const fieldTitle = (f) => getStructFieldTitle(fieldSchema, f) ?? f;
|
|
21
|
+
const fieldDescription = (f) => getStructFieldDescription(fieldSchema, f);
|
|
22
|
+
const TRANSFORM_VARS = {
|
|
23
|
+
json: { type: "optional<dyn>", label: "HTTP \u54CD\u5E94\u4F53" }
|
|
24
|
+
};
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<div class="flex flex-col gap-3">
|
|
29
|
+
<Field orientation="vertical">
|
|
30
|
+
<FieldLabel class="text-xs text-zinc-500">
|
|
31
|
+
<template
|
|
32
|
+
v-if="fieldDescription('displayName')"
|
|
33
|
+
#tooltip
|
|
34
|
+
>
|
|
35
|
+
<Markdown
|
|
36
|
+
:source="fieldDescription('displayName')"
|
|
37
|
+
block
|
|
38
|
+
class="prose prose-sm prose-zinc"
|
|
39
|
+
/>
|
|
40
|
+
</template>
|
|
41
|
+
{{ fieldTitle("displayName") }}
|
|
42
|
+
</FieldLabel>
|
|
43
|
+
<InputGroup>
|
|
44
|
+
<InputGroupInput
|
|
45
|
+
:model-value="value.displayName ?? ''"
|
|
46
|
+
placeholder="例:渠道自定义字段"
|
|
47
|
+
@update:model-value="(v) => {
|
|
48
|
+
const s = String(v ?? '');
|
|
49
|
+
value = { ...value, displayName: s.length > 0 ? s : void 0 };
|
|
50
|
+
}"
|
|
51
|
+
/>
|
|
52
|
+
</InputGroup>
|
|
53
|
+
</Field>
|
|
54
|
+
|
|
55
|
+
<Field orientation="vertical">
|
|
56
|
+
<FieldLabel class="text-xs text-zinc-500">
|
|
57
|
+
<template
|
|
58
|
+
v-if="fieldDescription('request')"
|
|
59
|
+
#tooltip
|
|
60
|
+
>
|
|
61
|
+
<Markdown
|
|
62
|
+
:source="fieldDescription('request')"
|
|
63
|
+
block
|
|
64
|
+
class="prose prose-sm prose-zinc"
|
|
65
|
+
/>
|
|
66
|
+
</template>
|
|
67
|
+
{{ fieldTitle("request") }}
|
|
68
|
+
</FieldLabel>
|
|
69
|
+
<ExpressionEditor
|
|
70
|
+
:model-value="value.request ?? ''"
|
|
71
|
+
placeholder="如 http.get('/api/fields').query('channel', 'other_inv')"
|
|
72
|
+
result-type="HttpRequest"
|
|
73
|
+
multiline
|
|
74
|
+
class="min-h-20"
|
|
75
|
+
@update:model-value="(v) => value = { ...value, request: v }"
|
|
76
|
+
/>
|
|
77
|
+
</Field>
|
|
78
|
+
|
|
79
|
+
<Field orientation="vertical">
|
|
80
|
+
<FieldLabel class="text-xs text-zinc-500">
|
|
81
|
+
<template
|
|
82
|
+
v-if="fieldDescription('transform')"
|
|
83
|
+
#tooltip
|
|
84
|
+
>
|
|
85
|
+
<Markdown
|
|
86
|
+
:source="fieldDescription('transform')"
|
|
87
|
+
block
|
|
88
|
+
class="prose prose-sm prose-zinc"
|
|
89
|
+
/>
|
|
90
|
+
</template>
|
|
91
|
+
{{ fieldTitle("transform") }}
|
|
92
|
+
</FieldLabel>
|
|
93
|
+
<ExpressionEditor
|
|
94
|
+
:model-value="value.transform ?? ''"
|
|
95
|
+
placeholder="如 json.orValue([]).map(f, { 'binding': f.parameter_name, 'label': f.name, 'kind': 'text', 'required': f.is_mandatory })"
|
|
96
|
+
result-type="list"
|
|
97
|
+
multiline
|
|
98
|
+
class="min-h-32"
|
|
99
|
+
:extra-vars="TRANSFORM_VARS"
|
|
100
|
+
@update:model-value="(v) => value = { ...value, transform: v }"
|
|
101
|
+
/>
|
|
102
|
+
</Field>
|
|
103
|
+
|
|
104
|
+
<div class="grid grid-cols-2 gap-3">
|
|
105
|
+
<Field orientation="vertical">
|
|
106
|
+
<FieldLabel class="text-xs text-zinc-500">
|
|
107
|
+
<template
|
|
108
|
+
v-if="fieldDescription('columns')"
|
|
109
|
+
#tooltip
|
|
110
|
+
>
|
|
111
|
+
<Markdown
|
|
112
|
+
:source="fieldDescription('columns')"
|
|
113
|
+
block
|
|
114
|
+
class="prose prose-sm prose-zinc"
|
|
115
|
+
/>
|
|
116
|
+
</template>
|
|
117
|
+
{{ fieldTitle("columns") }}
|
|
118
|
+
</FieldLabel>
|
|
119
|
+
<InputGroup>
|
|
120
|
+
<InputGroupInput
|
|
121
|
+
:model-value="value.columns ?? ''"
|
|
122
|
+
type="number"
|
|
123
|
+
min="1"
|
|
124
|
+
step="1"
|
|
125
|
+
placeholder="1"
|
|
126
|
+
@update:model-value="(v) => {
|
|
127
|
+
const n = Math.floor(Number(v));
|
|
128
|
+
value = Number.isFinite(n) && n > 0 ? { ...value, columns: n } : (() => {
|
|
129
|
+
const { columns: _o, ...rest } = value;
|
|
130
|
+
return rest;
|
|
131
|
+
})();
|
|
132
|
+
}"
|
|
133
|
+
/>
|
|
134
|
+
</InputGroup>
|
|
135
|
+
</Field>
|
|
136
|
+
|
|
137
|
+
<Field orientation="vertical">
|
|
138
|
+
<FieldLabel class="text-xs text-zinc-500">
|
|
139
|
+
<template
|
|
140
|
+
v-if="fieldDescription('orientation')"
|
|
141
|
+
#tooltip
|
|
142
|
+
>
|
|
143
|
+
<Markdown
|
|
144
|
+
:source="fieldDescription('orientation')"
|
|
145
|
+
block
|
|
146
|
+
class="prose prose-sm prose-zinc"
|
|
147
|
+
/>
|
|
148
|
+
</template>
|
|
149
|
+
{{ fieldTitle("orientation") }}
|
|
150
|
+
</FieldLabel>
|
|
151
|
+
<Select
|
|
152
|
+
:model-value="value.orientation ?? DEFAULT_FIELD_ORIENTATION"
|
|
153
|
+
@update:model-value="(v) => value = { ...value, orientation: v }"
|
|
154
|
+
>
|
|
155
|
+
<SelectTrigger class="w-full">
|
|
156
|
+
<SelectValue />
|
|
157
|
+
</SelectTrigger>
|
|
158
|
+
<SelectContent>
|
|
159
|
+
<SelectItem
|
|
160
|
+
v-for="opt in FIELD_ORIENTATION_OPTIONS"
|
|
161
|
+
:key="opt.value"
|
|
162
|
+
:value="opt.value"
|
|
163
|
+
>
|
|
164
|
+
{{ opt.label }}
|
|
165
|
+
</SelectItem>
|
|
166
|
+
</SelectContent>
|
|
167
|
+
</Select>
|
|
168
|
+
</Field>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<Field orientation="vertical">
|
|
172
|
+
<FieldLabel class="text-xs text-zinc-500">
|
|
173
|
+
<template #tooltip>
|
|
174
|
+
<Markdown
|
|
175
|
+
:source="fieldDescription('hidden') ?? '\u8FD4\u56DE `true` \u65F6\u6574\u4E2A\u52A8\u6001\u533A\u57DF\u5728\u6240\u6709\u5E03\u5C40\u4E2D\u90FD\u4E0D\u6E32\u67D3'"
|
|
176
|
+
block
|
|
177
|
+
class="prose prose-sm prose-zinc"
|
|
178
|
+
/>
|
|
179
|
+
</template>
|
|
180
|
+
{{ fieldTitle("hidden") }}
|
|
181
|
+
</FieldLabel>
|
|
182
|
+
<ExpressionEditor
|
|
183
|
+
:model-value="value.hidden ?? ''"
|
|
184
|
+
placeholder="例:form.channel == ''"
|
|
185
|
+
result-type="bool"
|
|
186
|
+
class="min-h-10"
|
|
187
|
+
@update:model-value="(v) => value = { ...value, hidden: v.length > 0 ? v : void 0 }"
|
|
188
|
+
/>
|
|
189
|
+
</Field>
|
|
190
|
+
</div>
|
|
191
|
+
</template>
|
package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/config.vue.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Value } from './schema.js';
|
|
2
|
+
type __VLS_ModelProps = {
|
|
3
|
+
modelValue: Value;
|
|
4
|
+
};
|
|
5
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
6
|
+
"update:modelValue": (value: {
|
|
7
|
+
readonly type: "com.shwfed.form.field.dynamic";
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly hidden?: string | undefined;
|
|
10
|
+
readonly displayName?: string | undefined;
|
|
11
|
+
readonly transform: string;
|
|
12
|
+
readonly columns?: number | undefined;
|
|
13
|
+
readonly compatibilityDate: "2026-07-13";
|
|
14
|
+
readonly orientation?: "vertical" | "floating" | undefined;
|
|
15
|
+
readonly request: string;
|
|
16
|
+
}) => any;
|
|
17
|
+
}, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
|
|
18
|
+
"onUpdate:modelValue"?: ((value: {
|
|
19
|
+
readonly type: "com.shwfed.form.field.dynamic";
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly hidden?: string | undefined;
|
|
22
|
+
readonly displayName?: string | undefined;
|
|
23
|
+
readonly transform: string;
|
|
24
|
+
readonly columns?: number | undefined;
|
|
25
|
+
readonly compatibilityDate: "2026-07-13";
|
|
26
|
+
readonly orientation?: "vertical" | "floating" | undefined;
|
|
27
|
+
readonly request: string;
|
|
28
|
+
}) => any) | undefined;
|
|
29
|
+
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
30
|
+
declare const _default: typeof __VLS_export;
|
|
31
|
+
export default _default;
|
package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/expand.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Value } from './schema.js';
|
|
2
|
+
import type { FieldValue, FormUnitValue } from '../../../schema.js';
|
|
3
|
+
import type { LayoutSetValue } from '../../../../../share/layout.js';
|
|
4
|
+
export declare const LAYOUT_NAME = "\u52A8\u6001\u5B57\u6BB5";
|
|
5
|
+
/**
|
|
6
|
+
* Looks up a pinned leaf's creation-time `defaults()` seed by
|
|
7
|
+
* `(type, compatibilityDate)` — the registry knowledge the expansion needs for
|
|
8
|
+
* picker parity, injected so this file stays registry-free (the runtime passes
|
|
9
|
+
* a `findField`-backed lookup, tests inject stubs).
|
|
10
|
+
*/
|
|
11
|
+
export type DefaultsLookup = (type: string, compatibilityDate: string) => Record<string, unknown> | undefined;
|
|
12
|
+
/** One initial-value seed: written at `binding` iff that path is currently undefined. */
|
|
13
|
+
export type DynFieldSeed = Readonly<{
|
|
14
|
+
binding: string;
|
|
15
|
+
value: unknown;
|
|
16
|
+
}>;
|
|
17
|
+
export type ExpandResult = Readonly<{
|
|
18
|
+
fields: FieldValue[];
|
|
19
|
+
/** Exactly one synthesized layout — `FormUnitRenderer` requires ≥ 1. */
|
|
20
|
+
layouts: [LayoutSetValue];
|
|
21
|
+
seeds: DynFieldSeed[];
|
|
22
|
+
/** How many list elements were dropped (malformed / duplicate / optionless). */
|
|
23
|
+
dropped: number;
|
|
24
|
+
/** Total elements the transform returned (for the dev aggregate warning). */
|
|
25
|
+
total: number;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Expand a fetched descriptor list into a synthesized form unit.
|
|
29
|
+
*
|
|
30
|
+
* Failure handling (partial-drop, loud — mirrors dyn cols):
|
|
31
|
+
* 1. `transform` throwing / a non-list result → handled by the caller
|
|
32
|
+
* (runtime.vue) for the throw; a non-list reaching here yields zero fields
|
|
33
|
+
* + one warn.
|
|
34
|
+
* 2. per-element decode failure / duplicate `binding` within the block /
|
|
35
|
+
* choice-kind without `options` → drop that element alone, warn with the
|
|
36
|
+
* reason + binding; a dev aggregate count is emitted at the end.
|
|
37
|
+
* 3. a child field's own runtime failure is contained per-field by the form's
|
|
38
|
+
* normal rendering — not here.
|
|
39
|
+
*
|
|
40
|
+
* Ordering is transform-output order. Every child occupies exactly one 1×1
|
|
41
|
+
* grid cell, so `columns` literally means "fields per row" — the semantics the
|
|
42
|
+
* config editor promises. Registry `w.initial` / `h.initial` are deliberately
|
|
43
|
+
* NOT consulted: those step units are calibrated against fine-grained authored
|
|
44
|
+
* grids (text = 8, daterange = 10), so any span math over them collapses to
|
|
45
|
+
* one-field-per-row at realistic block column counts.
|
|
46
|
+
*/
|
|
47
|
+
export declare function expandDynFields(rawList: unknown, block: Value, defaultsOf: DefaultsLookup): ExpandResult;
|
|
48
|
+
/** An empty inner unit for first paint, before the fetch lands. */
|
|
49
|
+
export declare function emptyUnit(block: Value): FormUnitValue;
|
package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/expand.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Either, Schema } from "effect";
|
|
2
|
+
import { CHOICE_KINDS, DynFieldDescriptor, KIND_TO_FIELD } from "./schema.js";
|
|
3
|
+
export const LAYOUT_NAME = "\u52A8\u6001\u5B57\u6BB5";
|
|
4
|
+
function wrapLocale(text) {
|
|
5
|
+
return [{ locale: "zh", message: text }];
|
|
6
|
+
}
|
|
7
|
+
const decodeDescriptor = Schema.decodeUnknownEither(DynFieldDescriptor);
|
|
8
|
+
function buildChild(d, block, defaultsOf) {
|
|
9
|
+
const pin = KIND_TO_FIELD[d.kind];
|
|
10
|
+
const id = `${block.id}::${d.binding}`;
|
|
11
|
+
const field = {
|
|
12
|
+
...defaultsOf(pin.type, pin.compatibilityDate),
|
|
13
|
+
id,
|
|
14
|
+
type: pin.type,
|
|
15
|
+
compatibilityDate: pin.compatibilityDate,
|
|
16
|
+
binding: d.binding,
|
|
17
|
+
label: wrapLocale(d.label)
|
|
18
|
+
};
|
|
19
|
+
if (d.required) field.required = "true";
|
|
20
|
+
if (block.orientation) field.orientation = block.orientation;
|
|
21
|
+
if (CHOICE_KINDS.has(d.kind)) {
|
|
22
|
+
if (!d.options || d.options.length === 0) {
|
|
23
|
+
console.warn("[shwfed-form] dropped choice-kind dynamic field without options", d.binding);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const items = d.options.map((opt, i) => ({
|
|
27
|
+
id: `${id}::opt::${i}`,
|
|
28
|
+
label: wrapLocale(String(opt.label)),
|
|
29
|
+
value: typeof opt.value === "number" ? { kind: "number", value: opt.value } : { kind: "text", value: opt.value }
|
|
30
|
+
}));
|
|
31
|
+
field.options = { kind: "static", items };
|
|
32
|
+
}
|
|
33
|
+
const seed = d.default !== void 0 ? { binding: d.binding, value: d.default } : void 0;
|
|
34
|
+
return { field, seed };
|
|
35
|
+
}
|
|
36
|
+
export function expandDynFields(rawList, block, defaultsOf) {
|
|
37
|
+
const columns = block.columns && block.columns > 0 ? Math.floor(block.columns) : 1;
|
|
38
|
+
const emptyLayouts = () => [{ name: LAYOUT_NAME, layout: { columns, placements: {} } }];
|
|
39
|
+
if (!Array.isArray(rawList)) {
|
|
40
|
+
console.warn("[shwfed-form] dynamic fields transform did not return a list", rawList);
|
|
41
|
+
return { fields: [], layouts: emptyLayouts(), seeds: [], dropped: 0, total: 0 };
|
|
42
|
+
}
|
|
43
|
+
const fields = [];
|
|
44
|
+
const seeds = [];
|
|
45
|
+
const placements = {};
|
|
46
|
+
const seenBindings = /* @__PURE__ */ new Set();
|
|
47
|
+
let dropped = 0;
|
|
48
|
+
let x = 1;
|
|
49
|
+
let y = 1;
|
|
50
|
+
for (const element of rawList) {
|
|
51
|
+
const decoded = decodeDescriptor(element);
|
|
52
|
+
if (Either.isLeft(decoded)) {
|
|
53
|
+
dropped++;
|
|
54
|
+
console.warn("[shwfed-form] dropped invalid dynamic field descriptor", element);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const d = decoded.right;
|
|
58
|
+
if (seenBindings.has(d.binding)) {
|
|
59
|
+
dropped++;
|
|
60
|
+
console.warn("[shwfed-form] dropped duplicate dynamic field binding", d.binding);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const built = buildChild(d, block, defaultsOf);
|
|
64
|
+
if (!built) {
|
|
65
|
+
dropped++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
seenBindings.add(d.binding);
|
|
69
|
+
fields.push(built.field);
|
|
70
|
+
if (built.seed) seeds.push(built.seed);
|
|
71
|
+
placements[built.field.id] = { area: [[x, y], [x + 1, y + 1]] };
|
|
72
|
+
x++;
|
|
73
|
+
if (x > columns) {
|
|
74
|
+
x = 1;
|
|
75
|
+
y++;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (dropped > 0) {
|
|
79
|
+
console.warn(`[shwfed-form] ${dropped} of ${rawList.length} dynamic fields dropped`);
|
|
80
|
+
}
|
|
81
|
+
const layout = { name: LAYOUT_NAME, layout: { columns, placements } };
|
|
82
|
+
return { fields, layouts: [layout], seeds, dropped, total: rawList.length };
|
|
83
|
+
}
|
|
84
|
+
export function emptyUnit(block) {
|
|
85
|
+
const columns = block.columns && block.columns > 0 ? Math.floor(block.columns) : 1;
|
|
86
|
+
return { fields: [], layouts: [{ name: LAYOUT_NAME, layout: { columns, placements: {} } }] };
|
|
87
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Value } from './schema.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
fieldId: string;
|
|
4
|
+
config: Value;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|
package/dist/runtime/components/form/fields/2026-07-13/com.shwfed.form.field.dynamic/runtime.vue
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { Effect, Option } from "effect";
|
|
3
|
+
import { Fetch } from "fx-fetch";
|
|
4
|
+
import { onMounted, shallowRef } from "vue";
|
|
5
|
+
import { cel as _rawCel } from "../../../../../utils/cel";
|
|
6
|
+
import { celBindings, injectCELContext } from "../../../../../utils/cel-context";
|
|
7
|
+
import { fetchJsonOption } from "../../../../../share/request";
|
|
8
|
+
import FormUnitRenderer from "../../../FormUnitRenderer.vue";
|
|
9
|
+
import { findField } from "../../../utils/resolve";
|
|
10
|
+
import { useDerivedQuiescence } from "../../../utils/derived";
|
|
11
|
+
import { useFormState } from "../../../utils/state";
|
|
12
|
+
import { emptyUnit, expandDynFields } from "./expand";
|
|
13
|
+
defineOptions({ name: "ShwfedDynamicFieldRuntime" });
|
|
14
|
+
const props = defineProps({
|
|
15
|
+
fieldId: { type: String, required: true },
|
|
16
|
+
config: { type: null, required: true }
|
|
17
|
+
});
|
|
18
|
+
const inherited = injectCELContext();
|
|
19
|
+
const $cel = (expression, context) => _rawCel(expression, { ...celBindings(inherited), ...context });
|
|
20
|
+
const formState = useFormState();
|
|
21
|
+
const quiescence = useDerivedQuiescence();
|
|
22
|
+
const unit = shallowRef(emptyUnit(props.config));
|
|
23
|
+
const defaultsOf = (type, compatibilityDate) => findField(type, compatibilityDate)?.defaults?.();
|
|
24
|
+
async function loadDescriptors() {
|
|
25
|
+
quiescence.begin();
|
|
26
|
+
try {
|
|
27
|
+
const transform = props.config.transform;
|
|
28
|
+
if (!transform || transform.trim().length === 0) return;
|
|
29
|
+
const program = Effect.gen(function* () {
|
|
30
|
+
let json = Option.none();
|
|
31
|
+
const request = props.config.request;
|
|
32
|
+
if (request && request.trim().length > 0) {
|
|
33
|
+
json = yield* fetchJsonOption(yield* $cel(request));
|
|
34
|
+
}
|
|
35
|
+
return yield* $cel(transform, { json });
|
|
36
|
+
});
|
|
37
|
+
let list;
|
|
38
|
+
try {
|
|
39
|
+
list = await Effect.runPromise(Effect.provide(program, Fetch.layer));
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.warn(`[shwfed-form] dynamic fields ${props.fieldId}: request/transform failed`, err);
|
|
42
|
+
unit.value = emptyUnit(props.config);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const result = expandDynFields(list, props.config, defaultsOf);
|
|
46
|
+
for (const { binding, value } of result.seeds) {
|
|
47
|
+
if (formState.getAt(binding) === void 0) formState.setAtSilent(binding, value);
|
|
48
|
+
}
|
|
49
|
+
unit.value = { fields: result.fields, layouts: result.layouts };
|
|
50
|
+
} finally {
|
|
51
|
+
quiescence.end();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
onMounted(() => {
|
|
55
|
+
void loadDescriptors();
|
|
56
|
+
});
|
|
57
|
+
function evaluateMedia(expression) {
|
|
58
|
+
return Effect.runSync($cel(expression));
|
|
59
|
+
}
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<div
|
|
64
|
+
data-slot="dynamic-fields"
|
|
65
|
+
class="min-w-0"
|
|
66
|
+
>
|
|
67
|
+
<FormUnitRenderer
|
|
68
|
+
:unit="unit"
|
|
69
|
+
:evaluate-media="evaluateMedia"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
</template>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Value } from './schema.js';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
fieldId: string;
|
|
4
|
+
config: Value;
|
|
5
|
+
};
|
|
6
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
7
|
+
declare const _default: typeof __VLS_export;
|
|
8
|
+
export default _default;
|