@arronqzy/react-view 1.0.3 → 1.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.
- package/dist/styles.css +555 -15
- package/package.json +14 -7
- package/readme.md +74 -5
- package/src/core/template.tsx +12 -2
- package/src/panel/ReactViewOnlinePreview.tsx +18 -8
- package/src/panel/ReactViewPanel.tsx +403 -177
- package/src/panel/ai/AssistantChatPanel.tsx +895 -0
- package/src/panel/ai/AssistantMarkdown.tsx +256 -0
- package/src/panel/ai/applyAssistantAction.ts +652 -0
- package/src/panel/ai/revealConfigField.ts +137 -0
- package/src/panel/components/ConfigHintIcon.tsx +50 -0
- package/src/panel/components/ElementsLayer.tsx +39 -22
- package/src/panel/components/MaterialSidebar.tsx +157 -109
- package/src/panel/components/MoveableLayer.tsx +75 -61
- package/src/panel/components/PanelCanvas.tsx +81 -64
- package/src/panel/components/PanelConfigSidebar.tsx +471 -448
- package/src/panel/components/SelectLayer.tsx +1 -0
- package/src/panel/components/ViewElementScopePanel.tsx +13 -12
- package/src/panel/components/WorkspaceConfigSidebar.tsx +5 -2
- package/src/panel/components/WorkspaceProjectNav.tsx +25 -14
- package/src/panel/components/WorkspaceStageSplit.tsx +3 -1
- package/src/panel/components/scope-config/ScopeConfigContext.tsx +9 -0
- package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +242 -0
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +12 -0
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +6 -3
- package/src/panel/components/table/ConditionEditor.tsx +205 -0
- package/src/panel/components/table/JsonCodeEditor.tsx +115 -0
- package/src/panel/components/table/PanelConfigTableSection.tsx +2192 -0
- package/src/panel/components/table/TableNodeContent.tsx +544 -0
- package/src/panel/constants/messages.ts +76 -34
- package/src/panel/hooks/usePanelElements.ts +64 -52
- package/src/panel/hooks/useRafThrottledScroll.ts +30 -0
- package/src/panel/hooks/useWorkspaceProjects.ts +11 -8
- package/src/panel/types.ts +6 -0
- package/src/panel/utils/chartOptionBuilder.ts +12 -1
- package/src/panel/utils/panelElementDefaults.ts +69 -39
- package/src/panel/utils/scope-field-labels.ts +51 -37
- package/src/panel/utils/scope-template-chart.ts +92 -0
- package/src/panel/utils/scope-template-preview.ts +128 -0
- package/src/panel/utils/scope-template-spread.ts +252 -0
- package/src/panel/utils/scope-template-warnings.ts +131 -11
- package/src/panel/utils/scope-template.ts +105 -3
- package/src/tailwind.css +36 -15
|
@@ -1,43 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"chart.
|
|
10
|
-
"chart.
|
|
11
|
-
"chart.
|
|
12
|
-
"chart.
|
|
13
|
-
"chart.
|
|
14
|
-
"chart.
|
|
15
|
-
"chart.
|
|
16
|
-
"chart.
|
|
17
|
-
"chart.
|
|
18
|
-
"chart.
|
|
19
|
-
"chart.
|
|
20
|
-
"chart.
|
|
21
|
-
"chart.
|
|
22
|
-
"chart.
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"style.
|
|
27
|
-
"style.
|
|
28
|
-
"style.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
import type { TranslateFn } from "@arronqzy/i18n";
|
|
2
|
+
|
|
3
|
+
const FIELD_KEY_MAP: Record<string, string> = {
|
|
4
|
+
name: "panel.scope.fieldName",
|
|
5
|
+
textHtml: "panel.scope.fieldTextHtml",
|
|
6
|
+
textFontFamily: "panel.scope.fieldTextFontFamily",
|
|
7
|
+
textFontWeight: "panel.scope.fieldTextFontWeight",
|
|
8
|
+
textColor: "panel.scope.fieldTextColor",
|
|
9
|
+
"chart.title": "panel.scope.fieldChartTitle",
|
|
10
|
+
"chart.color": "panel.scope.fieldChartColor",
|
|
11
|
+
"chart.gradientFrom": "panel.scope.fieldChartGradientFrom",
|
|
12
|
+
"chart.gradientTo": "panel.scope.fieldChartGradientTo",
|
|
13
|
+
"chart.xAxisName": "panel.scope.fieldChartXAxisName",
|
|
14
|
+
"chart.yAxisName": "panel.scope.fieldChartYAxisName",
|
|
15
|
+
"chart.xAxisTickColor": "panel.scope.fieldChartXAxisTickColor",
|
|
16
|
+
"chart.yAxisTickColor": "panel.scope.fieldChartYAxisTickColor",
|
|
17
|
+
"chart.xAxisSplitLineColor": "panel.scope.fieldChartXAxisSplitLineColor",
|
|
18
|
+
"chart.yAxisSplitLineColor": "panel.scope.fieldChartYAxisSplitLineColor",
|
|
19
|
+
"chart.xAxisLabelColor": "panel.scope.fieldChartXAxisLabelColor",
|
|
20
|
+
"chart.yAxisLabelColor": "panel.scope.fieldChartYAxisLabelColor",
|
|
21
|
+
"chart.tooltipBackgroundColor": "panel.scope.fieldChartTooltipBackgroundColor",
|
|
22
|
+
"chart.tooltipTextColor": "panel.scope.fieldChartTooltipTextColor",
|
|
23
|
+
"chart.tooltipFormatter": "panel.scope.fieldChartTooltipFormatter",
|
|
24
|
+
"chart.labelsText": "panel.scope.fieldChartLabelsText",
|
|
25
|
+
"chart.valuesText": "panel.scope.fieldChartValuesText",
|
|
26
|
+
"style.backgroundColor": "panel.scope.fieldStyleBackgroundColor",
|
|
27
|
+
"style.backgroundImage": "panel.scope.fieldStyleBackgroundImage",
|
|
28
|
+
"style.backgroundImageRemoteUrl": "panel.scope.fieldStyleBackgroundImageRemoteUrl",
|
|
29
|
+
"style.backgroundSize": "panel.scope.fieldStyleBackgroundSize",
|
|
30
|
+
"style.backgroundPosition": "panel.scope.fieldStyleBackgroundPosition",
|
|
31
|
+
"style.borderColor": "panel.scope.fieldStyleBorderColor",
|
|
32
|
+
audioSrc: "panel.scope.fieldAudioSrc",
|
|
33
|
+
audioRemoteUrl: "panel.scope.fieldAudioRemoteUrl",
|
|
34
|
+
videoSrc: "panel.scope.fieldVideoSrc",
|
|
35
|
+
videoRemoteUrl: "panel.scope.fieldVideoRemoteUrl",
|
|
36
|
+
geometryColor: "panel.scope.fieldGeometryColor",
|
|
37
|
+
geometryScript: "panel.scope.fieldGeometryScript",
|
|
35
38
|
};
|
|
36
39
|
|
|
37
|
-
export function
|
|
38
|
-
|
|
40
|
+
export function getScopeFieldLabels(t: TranslateFn): Record<string, string> {
|
|
41
|
+
const labels: Record<string, string> = {};
|
|
42
|
+
for (const [fieldId, key] of Object.entries(FIELD_KEY_MAP)) {
|
|
43
|
+
labels[fieldId] = t(key);
|
|
44
|
+
}
|
|
45
|
+
return labels;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveScopeFieldLabel(fieldId: string, t?: TranslateFn): string {
|
|
49
|
+
if (t) {
|
|
50
|
+
const key = FIELD_KEY_MAP[fieldId];
|
|
51
|
+
if (key) return t(key);
|
|
52
|
+
}
|
|
39
53
|
const leaf = fieldId.split(".").pop() ?? fieldId;
|
|
40
|
-
return
|
|
54
|
+
return leaf;
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
export function scopeFieldDomId(fieldId: string): string {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { PanelChartConfig } from "../types";
|
|
2
|
+
import { hasScopeTemplate, evaluateScopeTemplate } from "./scope-template";
|
|
3
|
+
import {
|
|
4
|
+
evaluateSpreadScopeExpressionValues,
|
|
5
|
+
extractSpreadScopeExpression,
|
|
6
|
+
isSpreadOnlyScopeTemplate,
|
|
7
|
+
} from "./scope-template-spread";
|
|
8
|
+
|
|
9
|
+
function splitCommaSeparatedLabels(text: string): string[] {
|
|
10
|
+
return text
|
|
11
|
+
.split(",")
|
|
12
|
+
.map((part) => part.trim())
|
|
13
|
+
.filter(Boolean);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function materializeSpreadTemplate(template: string, scope: unknown): string[] {
|
|
17
|
+
const expression = extractSpreadScopeExpression(template);
|
|
18
|
+
if (!expression) return [];
|
|
19
|
+
const result = evaluateSpreadScopeExpressionValues(expression, scope);
|
|
20
|
+
return result.ok ? result.values : [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function materializeLabelToken(token: string, scope: unknown): string[] {
|
|
24
|
+
const trimmed = token.trim();
|
|
25
|
+
if (!trimmed) return [];
|
|
26
|
+
|
|
27
|
+
if (isSpreadOnlyScopeTemplate(trimmed)) {
|
|
28
|
+
return materializeSpreadTemplate(trimmed, scope);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (hasScopeTemplate(trimmed)) {
|
|
32
|
+
return [evaluateScopeTemplate(trimmed, scope)];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return [trimmed];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 将类目模版文本展开为图表所需的字符串数组 */
|
|
39
|
+
export function materializeChartLabelsText(
|
|
40
|
+
text: string,
|
|
41
|
+
scope: unknown
|
|
42
|
+
): string[] {
|
|
43
|
+
const trimmed = text.trim();
|
|
44
|
+
if (!trimmed) return [];
|
|
45
|
+
|
|
46
|
+
if (isSpreadOnlyScopeTemplate(trimmed)) {
|
|
47
|
+
return materializeSpreadTemplate(trimmed, scope);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (hasScopeTemplate(trimmed)) {
|
|
51
|
+
return splitCommaSeparatedLabels(evaluateScopeTemplate(trimmed, scope));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return splitCommaSeparatedLabels(trimmed);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 兼容旧数据:labels 数组中每项可含展开模版 */
|
|
58
|
+
export function materializeChartLabelsArray(
|
|
59
|
+
labels: string[],
|
|
60
|
+
scope: unknown
|
|
61
|
+
): string[] {
|
|
62
|
+
const result: string[] = [];
|
|
63
|
+
for (const label of labels) {
|
|
64
|
+
result.push(...materializeLabelToken(label, scope));
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 将数值模版文本展开为图表所需的数字数组 */
|
|
70
|
+
export function materializeChartValuesText(
|
|
71
|
+
text: string,
|
|
72
|
+
scope: unknown
|
|
73
|
+
): number[] {
|
|
74
|
+
return materializeChartLabelsText(text, scope).map((part) => {
|
|
75
|
+
const value = Number(part);
|
|
76
|
+
return Number.isFinite(value) ? value : 0;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function materializeChartLabelsFromScope(
|
|
81
|
+
chart: PanelChartConfig | undefined,
|
|
82
|
+
scope: unknown
|
|
83
|
+
): string[] | undefined {
|
|
84
|
+
if (!chart) return undefined;
|
|
85
|
+
if (chart.labelsText !== undefined) {
|
|
86
|
+
return materializeChartLabelsText(chart.labelsText, scope);
|
|
87
|
+
}
|
|
88
|
+
if (chart.labels?.length) {
|
|
89
|
+
return materializeChartLabelsArray(chart.labels, scope);
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { TranslateFn } from "@arronqzy/i18n";
|
|
2
|
+
import { tForLocale } from "@arronqzy/i18n";
|
|
3
|
+
import type { PanelElement } from "../types";
|
|
4
|
+
import {
|
|
5
|
+
materializeChartLabelsText,
|
|
6
|
+
materializeChartValuesText,
|
|
7
|
+
} from "./scope-template-chart";
|
|
8
|
+
import { hasScopeTemplate, evaluateScopeTemplate } from "./scope-template";
|
|
9
|
+
import { SCOPE_SPREAD_TEMPLATE_RE } from "./scope-template-spread";
|
|
10
|
+
import { resolveScopeFieldLabel } from "./scope-field-labels";
|
|
11
|
+
|
|
12
|
+
export type ScopeTemplateField = {
|
|
13
|
+
fieldId: string;
|
|
14
|
+
fieldLabel: string;
|
|
15
|
+
template: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const SKIP_FIELD_PREFIXES = new Set(["refSnapshot", "geometrySketchDataUrl"]);
|
|
19
|
+
|
|
20
|
+
function shouldSkipField(fieldId: string): boolean {
|
|
21
|
+
return [...SKIP_FIELD_PREFIXES].some((prefix) => fieldId.startsWith(prefix));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function walkElementTemplateFields(
|
|
25
|
+
value: unknown,
|
|
26
|
+
fieldId: string,
|
|
27
|
+
results: ScopeTemplateField[],
|
|
28
|
+
t: TranslateFn
|
|
29
|
+
) {
|
|
30
|
+
if (shouldSkipField(fieldId)) return;
|
|
31
|
+
|
|
32
|
+
if (typeof value === "string") {
|
|
33
|
+
if (!hasScopeTemplate(value)) return;
|
|
34
|
+
results.push({
|
|
35
|
+
fieldId,
|
|
36
|
+
fieldLabel: resolveScopeFieldLabel(fieldId, t),
|
|
37
|
+
template: value,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
value.forEach((item, index) => {
|
|
44
|
+
walkElementTemplateFields(item, `${fieldId}.${index}`, results, t);
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (value && typeof value === "object") {
|
|
50
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
51
|
+
const nextId = fieldId ? `${fieldId}.${key}` : key;
|
|
52
|
+
walkElementTemplateFields(nested, nextId, results, t);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function collectElementScopeTemplateFields(
|
|
58
|
+
element: PanelElement,
|
|
59
|
+
t: TranslateFn = tForLocale("zh-CN")
|
|
60
|
+
): ScopeTemplateField[] {
|
|
61
|
+
const results: ScopeTemplateField[] = [];
|
|
62
|
+
walkElementTemplateFields(element, "", results, t);
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function matchScopeTemplateFieldId(
|
|
67
|
+
template: string,
|
|
68
|
+
fields: ScopeTemplateField[]
|
|
69
|
+
): string | undefined {
|
|
70
|
+
const exact = fields.find((field) => field.template === template);
|
|
71
|
+
if (exact) return exact.fieldId;
|
|
72
|
+
|
|
73
|
+
if (!hasScopeTemplate(template)) return undefined;
|
|
74
|
+
|
|
75
|
+
const partial = fields.filter(
|
|
76
|
+
(field) =>
|
|
77
|
+
field.template.includes(template) || template.includes(field.template)
|
|
78
|
+
);
|
|
79
|
+
if (partial.length === 1) return partial[0].fieldId;
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveScopeTemplatePreview(
|
|
84
|
+
template: string,
|
|
85
|
+
scope: unknown,
|
|
86
|
+
fieldId?: string
|
|
87
|
+
): unknown {
|
|
88
|
+
if (!hasScopeTemplate(template)) return template;
|
|
89
|
+
if (scope === undefined) return undefined;
|
|
90
|
+
|
|
91
|
+
const leafFieldId = fieldId?.split(".").pop();
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
fieldId === "chart.labelsText" ||
|
|
95
|
+
leafFieldId === "labelsText"
|
|
96
|
+
) {
|
|
97
|
+
return materializeChartLabelsText(template, scope);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
fieldId === "chart.valuesText" ||
|
|
102
|
+
leafFieldId === "valuesText"
|
|
103
|
+
) {
|
|
104
|
+
return materializeChartValuesText(template, scope);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (SCOPE_SPREAD_TEMPLATE_RE.test(template)) {
|
|
108
|
+
return materializeChartLabelsText(template, scope);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const evaluated = evaluateScopeTemplate(template, scope);
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse(evaluated);
|
|
114
|
+
} catch {
|
|
115
|
+
return evaluated;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function formatScopeTemplatePreview(value: unknown): string {
|
|
120
|
+
if (value === undefined) return "undefined";
|
|
121
|
+
if (value === null) return "null";
|
|
122
|
+
if (typeof value === "string") return value;
|
|
123
|
+
try {
|
|
124
|
+
return JSON.stringify(value, null, 2);
|
|
125
|
+
} catch {
|
|
126
|
+
return String(value);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/** 数组展开模版:[...{scope?.a?.b}] 从 scope 中定位数组并提取每项字段,输出逗号分隔值 */
|
|
2
|
+
|
|
3
|
+
import type { TranslateFn } from "@arronqzy/i18n";
|
|
4
|
+
import { tForLocale } from "@arronqzy/i18n";
|
|
5
|
+
|
|
6
|
+
export const SCOPE_SPREAD_TEMPLATE_RE = /\[\.\.\.\{([^}]+)\}\]/g;
|
|
7
|
+
|
|
8
|
+
export const SCOPE_SPREAD_TEMPLATE_DEMO = {
|
|
9
|
+
scope: { a: [{ b: 1 }, { b: 2 }] },
|
|
10
|
+
template: "[...{scope?.a?.b}]",
|
|
11
|
+
output: "1,2",
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
export function getScopeSpreadDeepTemplateDemo(t: TranslateFn = tForLocale("zh-CN")) {
|
|
15
|
+
const inProgress = t("panel.scope.demoInProgress");
|
|
16
|
+
const done = t("panel.scope.demoDone");
|
|
17
|
+
return {
|
|
18
|
+
scope: {
|
|
19
|
+
data: {
|
|
20
|
+
data: [{ statusName: inProgress }, { statusName: done }],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
template: "[...{scope?.data?.data?.statusName}]",
|
|
24
|
+
output: t("panel.scope.demoJoined"),
|
|
25
|
+
} as const;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @deprecated Prefer getScopeSpreadDeepTemplateDemo(t) */
|
|
29
|
+
export const SCOPE_SPREAD_DEEP_TEMPLATE_DEMO = getScopeSpreadDeepTemplateDemo();
|
|
30
|
+
|
|
31
|
+
export type ScopeSpreadEvalIssue =
|
|
32
|
+
| "invalid-expression"
|
|
33
|
+
| "path-missing"
|
|
34
|
+
| "not-array";
|
|
35
|
+
|
|
36
|
+
export type ScopeSpreadEvalResult =
|
|
37
|
+
| { ok: true; value: string }
|
|
38
|
+
| { ok: false; issue: ScopeSpreadEvalIssue; arrayPath?: string };
|
|
39
|
+
|
|
40
|
+
export type SpreadScopePathResolution =
|
|
41
|
+
| {
|
|
42
|
+
ok: true;
|
|
43
|
+
arrayPathSegments: string[];
|
|
44
|
+
itemPathSegments: string[];
|
|
45
|
+
arrayValue: unknown[];
|
|
46
|
+
}
|
|
47
|
+
| { ok: false; issue: ScopeSpreadEvalIssue; arrayPath?: string };
|
|
48
|
+
|
|
49
|
+
/** 解析 scope?.a?.b 形式的路径段(不含 scope 前缀) */
|
|
50
|
+
export function parseScopePathSegments(expression: string): string[] | null {
|
|
51
|
+
const trimmed = expression.trim();
|
|
52
|
+
if (!trimmed.startsWith("scope")) return null;
|
|
53
|
+
|
|
54
|
+
let body = trimmed.slice("scope".length);
|
|
55
|
+
if (body.startsWith("?.")) body = body.slice(2);
|
|
56
|
+
else if (body.startsWith(".")) body = body.slice(1);
|
|
57
|
+
else if (body.length > 0) return null;
|
|
58
|
+
|
|
59
|
+
if (!body) return [];
|
|
60
|
+
return body.split("?.").filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatScopePathSegments(segments: string[]): string {
|
|
64
|
+
return segments.join(".");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getValueAtScopePath(scope: unknown, path: string[]): unknown {
|
|
68
|
+
let current: unknown = scope;
|
|
69
|
+
for (const part of path) {
|
|
70
|
+
if (current === null || current === undefined) return undefined;
|
|
71
|
+
if (typeof current !== "object") return undefined;
|
|
72
|
+
current = (current as Record<string, unknown>)[part];
|
|
73
|
+
}
|
|
74
|
+
return current;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 从完整路径中定位数组段与项内字段段。
|
|
79
|
+
* 例如 scope?.data?.data?.statusName → 数组在 data.data,项字段为 statusName。
|
|
80
|
+
*/
|
|
81
|
+
export function resolveSpreadScopePath(
|
|
82
|
+
expression: string,
|
|
83
|
+
scope: unknown
|
|
84
|
+
): SpreadScopePathResolution {
|
|
85
|
+
const segments = parseScopePathSegments(expression);
|
|
86
|
+
if (segments === null || segments.length === 0) {
|
|
87
|
+
return { ok: false, issue: "invalid-expression" };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (segments.length === 1) {
|
|
91
|
+
const arrayPathSegments = segments;
|
|
92
|
+
const arrayValue = getValueAtScopePath(scope, arrayPathSegments);
|
|
93
|
+
const arrayPath = formatScopePathSegments(arrayPathSegments);
|
|
94
|
+
if (arrayValue === undefined) {
|
|
95
|
+
return { ok: false, issue: "path-missing", arrayPath };
|
|
96
|
+
}
|
|
97
|
+
if (!Array.isArray(arrayValue)) {
|
|
98
|
+
return { ok: false, issue: "not-array", arrayPath };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
ok: true,
|
|
102
|
+
arrayPathSegments,
|
|
103
|
+
itemPathSegments: [],
|
|
104
|
+
arrayValue,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (let splitAt = segments.length - 1; splitAt >= 1; splitAt--) {
|
|
109
|
+
const arrayPathSegments = segments.slice(0, splitAt);
|
|
110
|
+
const itemPathSegments = segments.slice(splitAt);
|
|
111
|
+
const arrayValue = getValueAtScopePath(scope, arrayPathSegments);
|
|
112
|
+
if (arrayValue === undefined) continue;
|
|
113
|
+
if (Array.isArray(arrayValue)) {
|
|
114
|
+
return {
|
|
115
|
+
ok: true,
|
|
116
|
+
arrayPathSegments,
|
|
117
|
+
itemPathSegments,
|
|
118
|
+
arrayValue,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const rootValue = getValueAtScopePath(scope, [segments[0]!]);
|
|
124
|
+
if (rootValue === undefined) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
issue: "path-missing",
|
|
128
|
+
arrayPath: segments[0],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (let splitAt = 1; splitAt < segments.length; splitAt++) {
|
|
133
|
+
const arrayPathSegments = segments.slice(0, splitAt);
|
|
134
|
+
const arrayValue = getValueAtScopePath(scope, arrayPathSegments);
|
|
135
|
+
if (arrayValue === undefined) continue;
|
|
136
|
+
if (!Array.isArray(arrayValue)) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
issue: "not-array",
|
|
140
|
+
arrayPath: formatScopePathSegments(arrayPathSegments),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
issue: "path-missing",
|
|
148
|
+
arrayPath: formatScopePathSegments(segments.slice(0, -1)),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function formatSpreadItemValue(value: unknown): string {
|
|
153
|
+
if (value === null || value === undefined) return "";
|
|
154
|
+
if (typeof value === "string") return value;
|
|
155
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
156
|
+
return String(value);
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return JSON.stringify(value);
|
|
160
|
+
} catch {
|
|
161
|
+
return String(value);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function evaluateSpreadScopeExpression(
|
|
166
|
+
expression: string,
|
|
167
|
+
scope: unknown
|
|
168
|
+
): ScopeSpreadEvalResult {
|
|
169
|
+
const valuesResult = evaluateSpreadScopeExpressionValues(expression, scope);
|
|
170
|
+
if (!valuesResult.ok) {
|
|
171
|
+
return valuesResult;
|
|
172
|
+
}
|
|
173
|
+
return { ok: true, value: valuesResult.values.join(",") };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function evaluateSpreadScopeExpressionValues(
|
|
177
|
+
expression: string,
|
|
178
|
+
scope: unknown
|
|
179
|
+
): { ok: true; values: string[] } | { ok: false; issue: ScopeSpreadEvalIssue; arrayPath?: string } {
|
|
180
|
+
const resolved = resolveSpreadScopePath(expression, scope);
|
|
181
|
+
if (!resolved.ok) {
|
|
182
|
+
return {
|
|
183
|
+
ok: false,
|
|
184
|
+
issue: resolved.issue,
|
|
185
|
+
arrayPath: resolved.arrayPath,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const values = resolved.arrayValue
|
|
190
|
+
.map((item) =>
|
|
191
|
+
resolved.itemPathSegments.length === 0
|
|
192
|
+
? item
|
|
193
|
+
: getValueAtScopePath(item, resolved.itemPathSegments)
|
|
194
|
+
)
|
|
195
|
+
.map(formatSpreadItemValue)
|
|
196
|
+
.filter((value) => value.length > 0);
|
|
197
|
+
|
|
198
|
+
return { ok: true, values };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function isSpreadOnlyScopeTemplate(value: string): boolean {
|
|
202
|
+
return /^\[\.\.\.\{[^}]+\}\]$/.test(value.trim());
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function extractSpreadScopeExpression(template: string): string | null {
|
|
206
|
+
const match = /^\[\.\.\.\{([^}]+)\}\]$/.exec(template.trim());
|
|
207
|
+
return match ? match[1]!.trim() : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function isSpreadTemplateBrace(value: string, braceIndex: number): boolean {
|
|
211
|
+
return value.slice(Math.max(0, braceIndex - 4), braceIndex) === "[...";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function buildSpreadNotArrayWarningMessage(
|
|
215
|
+
fieldLabel: string,
|
|
216
|
+
template: string,
|
|
217
|
+
expression: string,
|
|
218
|
+
arrayPath: string,
|
|
219
|
+
t: TranslateFn = tForLocale("zh-CN")
|
|
220
|
+
): string {
|
|
221
|
+
void expression;
|
|
222
|
+
const demoScope = JSON.stringify(SCOPE_SPREAD_TEMPLATE_DEMO.scope);
|
|
223
|
+
const deepDemo = getScopeSpreadDeepTemplateDemo(t);
|
|
224
|
+
return t("panel.scope.warnSpreadNotArray", {
|
|
225
|
+
fieldLabel,
|
|
226
|
+
template,
|
|
227
|
+
arrayPath,
|
|
228
|
+
demoScope,
|
|
229
|
+
demoTemplate: SCOPE_SPREAD_TEMPLATE_DEMO.template,
|
|
230
|
+
demoOutput: SCOPE_SPREAD_TEMPLATE_DEMO.output,
|
|
231
|
+
deepTemplate: deepDemo.template,
|
|
232
|
+
deepScope: JSON.stringify(deepDemo.scope),
|
|
233
|
+
deepOutput: deepDemo.output,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function buildSpreadInvalidExpressionWarningMessage(
|
|
238
|
+
fieldLabel: string,
|
|
239
|
+
template: string,
|
|
240
|
+
t: TranslateFn = tForLocale("zh-CN")
|
|
241
|
+
): string {
|
|
242
|
+
const demoScope = JSON.stringify(SCOPE_SPREAD_TEMPLATE_DEMO.scope);
|
|
243
|
+
const deepDemo = getScopeSpreadDeepTemplateDemo(t);
|
|
244
|
+
return t("panel.scope.warnSpreadInvalid", {
|
|
245
|
+
fieldLabel,
|
|
246
|
+
template,
|
|
247
|
+
demoTemplate: SCOPE_SPREAD_TEMPLATE_DEMO.template,
|
|
248
|
+
deepTemplate: deepDemo.template,
|
|
249
|
+
demoScope,
|
|
250
|
+
demoOutput: SCOPE_SPREAD_TEMPLATE_DEMO.output,
|
|
251
|
+
});
|
|
252
|
+
}
|