@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.
Files changed (43) hide show
  1. package/dist/styles.css +555 -15
  2. package/package.json +14 -7
  3. package/readme.md +74 -5
  4. package/src/core/template.tsx +12 -2
  5. package/src/panel/ReactViewOnlinePreview.tsx +18 -8
  6. package/src/panel/ReactViewPanel.tsx +403 -177
  7. package/src/panel/ai/AssistantChatPanel.tsx +895 -0
  8. package/src/panel/ai/AssistantMarkdown.tsx +256 -0
  9. package/src/panel/ai/applyAssistantAction.ts +652 -0
  10. package/src/panel/ai/revealConfigField.ts +137 -0
  11. package/src/panel/components/ConfigHintIcon.tsx +50 -0
  12. package/src/panel/components/ElementsLayer.tsx +39 -22
  13. package/src/panel/components/MaterialSidebar.tsx +157 -109
  14. package/src/panel/components/MoveableLayer.tsx +75 -61
  15. package/src/panel/components/PanelCanvas.tsx +81 -64
  16. package/src/panel/components/PanelConfigSidebar.tsx +471 -448
  17. package/src/panel/components/SelectLayer.tsx +1 -0
  18. package/src/panel/components/ViewElementScopePanel.tsx +13 -12
  19. package/src/panel/components/WorkspaceConfigSidebar.tsx +5 -2
  20. package/src/panel/components/WorkspaceProjectNav.tsx +25 -14
  21. package/src/panel/components/WorkspaceStageSplit.tsx +3 -1
  22. package/src/panel/components/scope-config/ScopeConfigContext.tsx +9 -0
  23. package/src/panel/components/scope-config/ScopeTemplatePreviewHost.tsx +242 -0
  24. package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +12 -0
  25. package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +6 -3
  26. package/src/panel/components/table/ConditionEditor.tsx +205 -0
  27. package/src/panel/components/table/JsonCodeEditor.tsx +115 -0
  28. package/src/panel/components/table/PanelConfigTableSection.tsx +2192 -0
  29. package/src/panel/components/table/TableNodeContent.tsx +544 -0
  30. package/src/panel/constants/messages.ts +76 -34
  31. package/src/panel/hooks/usePanelElements.ts +64 -52
  32. package/src/panel/hooks/useRafThrottledScroll.ts +30 -0
  33. package/src/panel/hooks/useWorkspaceProjects.ts +11 -8
  34. package/src/panel/types.ts +6 -0
  35. package/src/panel/utils/chartOptionBuilder.ts +12 -1
  36. package/src/panel/utils/panelElementDefaults.ts +69 -39
  37. package/src/panel/utils/scope-field-labels.ts +51 -37
  38. package/src/panel/utils/scope-template-chart.ts +92 -0
  39. package/src/panel/utils/scope-template-preview.ts +128 -0
  40. package/src/panel/utils/scope-template-spread.ts +252 -0
  41. package/src/panel/utils/scope-template-warnings.ts +131 -11
  42. package/src/panel/utils/scope-template.ts +105 -3
  43. package/src/tailwind.css +36 -15
@@ -1,6 +1,22 @@
1
+ import type { TranslateFn } from "@arronqzy/i18n";
2
+ import { tForLocale } from "@arronqzy/i18n";
1
3
  import type { PanelElement } from "../types";
2
4
  import { hasScopeTemplate } from "./scope-template";
3
5
  import { resolveScopeFieldLabel } from "./scope-field-labels";
6
+ import {
7
+ SCOPE_SPREAD_TEMPLATE_RE,
8
+ buildSpreadInvalidExpressionWarningMessage,
9
+ buildSpreadNotArrayWarningMessage,
10
+ formatScopePathSegments,
11
+ isSpreadTemplateBrace,
12
+ parseScopePathSegments,
13
+ resolveSpreadScopePath,
14
+ } from "./scope-template-spread";
15
+
16
+ export type ScopeTemplateWarningKind =
17
+ | "missing-path"
18
+ | "spread-not-array"
19
+ | "spread-invalid-expression";
4
20
 
5
21
  export type ScopeTemplateWarning = {
6
22
  fieldId: string;
@@ -9,6 +25,7 @@ export type ScopeTemplateWarning = {
9
25
  expression: string;
10
26
  missingPath: string;
11
27
  message: string;
28
+ kind: ScopeTemplateWarningKind;
12
29
  };
13
30
 
14
31
  const SCOPE_PATH_RE = /scope(?:\?\.|\.)(\w+(?:\?\.\w+)*)/g;
@@ -41,19 +58,119 @@ function pathExistsInScope(scope: unknown, path: string): boolean {
41
58
  return true;
42
59
  }
43
60
 
61
+ function analyzeSpreadTemplateValue(
62
+ fieldId: string,
63
+ value: string,
64
+ scope: unknown,
65
+ t: TranslateFn
66
+ ): ScopeTemplateWarning[] {
67
+ const warnings: ScopeTemplateWarning[] = [];
68
+ const fieldLabel = resolveScopeFieldLabel(fieldId, t);
69
+ const spreadRe = new RegExp(SCOPE_SPREAD_TEMPLATE_RE.source, "g");
70
+ let match: RegExpExecArray | null;
71
+
72
+ while ((match = spreadRe.exec(value)) !== null) {
73
+ const spreadTemplate = match[0];
74
+ const expression = match[1].trim();
75
+ const segments = parseScopePathSegments(expression);
76
+
77
+ if (segments === null || segments.length === 0) {
78
+ warnings.push({
79
+ fieldId,
80
+ fieldLabel,
81
+ template: value,
82
+ expression,
83
+ missingPath: "",
84
+ kind: "spread-invalid-expression",
85
+ message: buildSpreadInvalidExpressionWarningMessage(
86
+ fieldLabel,
87
+ spreadTemplate,
88
+ t
89
+ ),
90
+ });
91
+ continue;
92
+ }
93
+
94
+ const resolved = resolveSpreadScopePath(expression, scope);
95
+ if (resolved.ok) continue;
96
+
97
+ if (resolved.issue === "invalid-expression") {
98
+ warnings.push({
99
+ fieldId,
100
+ fieldLabel,
101
+ template: value,
102
+ expression,
103
+ missingPath: "",
104
+ kind: "spread-invalid-expression",
105
+ message: buildSpreadInvalidExpressionWarningMessage(
106
+ fieldLabel,
107
+ spreadTemplate,
108
+ t
109
+ ),
110
+ });
111
+ continue;
112
+ }
113
+
114
+ const arrayPath =
115
+ resolved.arrayPath ?? formatScopePathSegments(segments.slice(0, -1));
116
+
117
+ if (resolved.issue === "path-missing") {
118
+ if (!pathExistsInScope(scope, arrayPath)) {
119
+ warnings.push({
120
+ fieldId,
121
+ fieldLabel,
122
+ template: value,
123
+ expression,
124
+ missingPath: arrayPath,
125
+ kind: "missing-path",
126
+ message: t("panel.scope.warnMissingPath", {
127
+ fieldLabel,
128
+ path: arrayPath,
129
+ }),
130
+ });
131
+ }
132
+ continue;
133
+ }
134
+
135
+ if (resolved.issue === "not-array") {
136
+ warnings.push({
137
+ fieldId,
138
+ fieldLabel,
139
+ template: value,
140
+ expression,
141
+ missingPath: arrayPath,
142
+ kind: "spread-not-array",
143
+ message: buildSpreadNotArrayWarningMessage(
144
+ fieldLabel,
145
+ spreadTemplate,
146
+ expression,
147
+ arrayPath,
148
+ t
149
+ ),
150
+ });
151
+ }
152
+ }
153
+
154
+ return warnings;
155
+ }
156
+
44
157
  function analyzeTemplateValue(
45
158
  fieldId: string,
46
159
  value: string,
47
- scope: unknown
160
+ scope: unknown,
161
+ t: TranslateFn
48
162
  ): ScopeTemplateWarning[] {
49
163
  if (!hasScopeTemplate(value)) return [];
50
164
 
51
- const warnings: ScopeTemplateWarning[] = [];
52
- const fieldLabel = resolveScopeFieldLabel(fieldId);
165
+ const warnings: ScopeTemplateWarning[] = [
166
+ ...analyzeSpreadTemplateValue(fieldId, value, scope, t),
167
+ ];
168
+ const fieldLabel = resolveScopeFieldLabel(fieldId, t);
53
169
  const expressionRe = /\{([^}]+)\}/g;
54
170
  let match: RegExpExecArray | null;
55
171
 
56
172
  while ((match = expressionRe.exec(value)) !== null) {
173
+ if (isSpreadTemplateBrace(value, match.index)) continue;
57
174
  const expression = match[1].trim();
58
175
  const paths = extractScopePropertyPaths(expression);
59
176
  for (const path of paths) {
@@ -64,7 +181,8 @@ function analyzeTemplateValue(
64
181
  template: value,
65
182
  expression,
66
183
  missingPath: path,
67
- message: `「${fieldLabel}」引用了 scope.${path},但当前 scope 数据中并没有「${path}」字段,建议检查一下。`,
184
+ kind: "missing-path",
185
+ message: t("panel.scope.warnMissingPath", { fieldLabel, path }),
68
186
  });
69
187
  }
70
188
  }
@@ -80,18 +198,19 @@ function walkElementStrings(
80
198
  value: unknown,
81
199
  fieldId: string,
82
200
  scope: unknown,
83
- warnings: ScopeTemplateWarning[]
201
+ warnings: ScopeTemplateWarning[],
202
+ t: TranslateFn
84
203
  ) {
85
204
  if (shouldSkipField(fieldId)) return;
86
205
 
87
206
  if (typeof value === "string") {
88
- warnings.push(...analyzeTemplateValue(fieldId, value, scope));
207
+ warnings.push(...analyzeTemplateValue(fieldId, value, scope, t));
89
208
  return;
90
209
  }
91
210
 
92
211
  if (Array.isArray(value)) {
93
212
  value.forEach((item, index) => {
94
- walkElementStrings(item, `${fieldId}.${index}`, scope, warnings);
213
+ walkElementStrings(item, `${fieldId}.${index}`, scope, warnings, t);
95
214
  });
96
215
  return;
97
216
  }
@@ -99,23 +218,24 @@ function walkElementStrings(
99
218
  if (value && typeof value === "object") {
100
219
  for (const [key, nested] of Object.entries(value)) {
101
220
  const nextId = fieldId ? `${fieldId}.${key}` : key;
102
- walkElementStrings(nested, nextId, scope, warnings);
221
+ walkElementStrings(nested, nextId, scope, warnings, t);
103
222
  }
104
223
  }
105
224
  }
106
225
 
107
226
  export function collectElementScopeWarnings(
108
227
  element: PanelElement,
109
- scope: unknown
228
+ scope: unknown,
229
+ t: TranslateFn = tForLocale("zh-CN")
110
230
  ): ScopeTemplateWarning[] {
111
231
  if (scope === undefined) return [];
112
232
 
113
233
  const warnings: ScopeTemplateWarning[] = [];
114
- walkElementStrings(element, "", scope, warnings);
234
+ walkElementStrings(element, "", scope, warnings, t);
115
235
 
116
236
  const seen = new Set<string>();
117
237
  return warnings.filter((warning) => {
118
- const key = `${warning.fieldId}|${warning.missingPath}|${warning.expression}`;
238
+ const key = `${warning.fieldId}|${warning.kind}|${warning.missingPath}|${warning.expression}`;
119
239
  if (seen.has(key)) return false;
120
240
  seen.add(key);
121
241
  return true;
@@ -1,6 +1,17 @@
1
1
  import type { PanelElement } from "../types";
2
+ import type { PanelTableConfig } from "@arronqzy/view-table";
3
+ import { isUsableTableRawData } from "@arronqzy/view-table";
4
+ import { materializeChartLabelsFromScope } from "./scope-template-chart";
5
+ import {
6
+ SCOPE_SPREAD_TEMPLATE_RE,
7
+ evaluateSpreadScopeExpression,
8
+ extractSpreadScopeExpression,
9
+ getValueAtScopePath,
10
+ isSpreadOnlyScopeTemplate,
11
+ resolveSpreadScopePath,
12
+ } from "./scope-template-spread";
2
13
 
3
- const SCOPE_TEMPLATE_RE = /\{[^}]+\}/;
14
+ const SCOPE_TEMPLATE_RE = /\{[^}]+\}|\[\.\.\.\{[^}]+\}\]/;
4
15
 
5
16
  export function hasScopeTemplate(value: string): boolean {
6
17
  return SCOPE_TEMPLATE_RE.test(value);
@@ -25,7 +36,16 @@ export function evaluateScopeTemplate(
25
36
  scope: unknown
26
37
  ): string {
27
38
  if (!hasScopeTemplate(template)) return template;
28
- return template.replace(/\{([^}]+)\}/g, (_, rawExpr: string) => {
39
+
40
+ const withSpread = template.replace(
41
+ SCOPE_SPREAD_TEMPLATE_RE,
42
+ (_: string, rawExpr: string) => {
43
+ const result = evaluateSpreadScopeExpression(rawExpr, scope);
44
+ return result.ok ? result.value : "";
45
+ }
46
+ );
47
+
48
+ return withSpread.replace(/\{([^}]+)\}/g, (_: string, rawExpr: string) => {
29
49
  const value = evaluateScopeExpression(rawExpr, scope);
30
50
  if (value === null || value === undefined) return "";
31
51
  if (typeof value === "string") return value;
@@ -40,6 +60,65 @@ export function evaluateScopeTemplate(
40
60
  });
41
61
  }
42
62
 
63
+ function tryParseJson(text: string): unknown | undefined {
64
+ try {
65
+ return JSON.parse(text);
66
+ } catch {
67
+ return undefined;
68
+ }
69
+ }
70
+
71
+ function materializeTemplateValue(template: string, scope: unknown): unknown {
72
+ const trimmed = template.trim();
73
+ if (!trimmed) return undefined;
74
+
75
+ if (isSpreadOnlyScopeTemplate(trimmed)) {
76
+ const expression = extractSpreadScopeExpression(trimmed);
77
+ if (!expression) return undefined;
78
+ const resolved = resolveSpreadScopePath(expression, scope);
79
+ if (!resolved.ok) return undefined;
80
+ if (resolved.itemPathSegments.length === 0) {
81
+ return resolved.arrayValue;
82
+ }
83
+ return resolved.arrayValue.map((item) =>
84
+ getValueAtScopePath(item, resolved.itemPathSegments)
85
+ );
86
+ }
87
+
88
+ const singleMatch = /^\{([^}]+)\}$/.exec(trimmed);
89
+ if (singleMatch) {
90
+ return evaluateScopeExpression(singleMatch[1]!, scope);
91
+ }
92
+
93
+ if (hasScopeTemplate(trimmed)) {
94
+ const text = evaluateScopeTemplate(trimmed, scope);
95
+ const parsed = tryParseJson(text);
96
+ return parsed !== undefined ? parsed : text;
97
+ }
98
+
99
+ return tryParseJson(trimmed);
100
+ }
101
+
102
+ export function materializeTableSourceFromScope(
103
+ table: PanelTableConfig | undefined,
104
+ scope: unknown
105
+ ): unknown | undefined {
106
+ if (!table) return undefined;
107
+
108
+ if (typeof table.source === "string" && table.source.trim()) {
109
+ return materializeTemplateValue(table.source, scope);
110
+ }
111
+
112
+ if (typeof table.rowsText === "string" && table.rowsText.trim()) {
113
+ const trimmed = table.rowsText.trim();
114
+ if (hasScopeTemplate(trimmed) || isSpreadOnlyScopeTemplate(trimmed)) {
115
+ return materializeTemplateValue(trimmed, scope);
116
+ }
117
+ }
118
+
119
+ return undefined;
120
+ }
121
+
43
122
  function resolveScopedValue<T>(value: T, scope: unknown): T {
44
123
  if (typeof value === "string" && hasScopeTemplate(value)) {
45
124
  return evaluateScopeTemplate(value, scope) as T;
@@ -62,7 +141,30 @@ export function resolvePanelElementScope(
62
141
  scope: unknown | undefined
63
142
  ): PanelElement {
64
143
  if (scope === undefined) return element;
65
- return resolveScopedValue(structuredClone(element), scope);
144
+ const resolved = resolveScopedValue(structuredClone(element), scope);
145
+ if (resolved.chart) {
146
+ const labels = materializeChartLabelsFromScope(element.chart, scope);
147
+ if (labels !== undefined) {
148
+ resolved.chart.labels = labels;
149
+ delete resolved.chart.labelsText;
150
+ }
151
+ }
152
+ if (resolved.table) {
153
+ const hasDynamicSource =
154
+ typeof element.table?.source === "string" && element.table.source.trim().length > 0;
155
+ if (hasDynamicSource) {
156
+ const raw = materializeTableSourceFromScope(element.table, scope);
157
+ if (isUsableTableRawData(raw)) {
158
+ resolved.table = { ...resolved.table, rows: raw };
159
+ } else {
160
+ // Dynamic missing/empty → fall back to static rowsText (do not keep empty rows)
161
+ const next = { ...resolved.table };
162
+ delete next.rows;
163
+ resolved.table = next;
164
+ }
165
+ }
166
+ }
167
+ return resolved;
66
168
  }
67
169
 
68
170
  export function formatViewElementScope(scope: unknown): string {
package/src/tailwind.css CHANGED
@@ -26,33 +26,54 @@
26
26
  background: hsl(var(--foreground) / 0.45);
27
27
  }
28
28
 
29
- /* Moveable:仅角点控制柄固定屏幕像素;勿作用于 moveable-direction 边线 */
29
+ /*
30
+ * Moveable 控制框:
31
+ * - 根节点必须高于画布元素 zIndex(节点默认 ≥1),否则多选组框线会被节点盖住,只剩锚点可见
32
+ * - 勿把 .moveable-group 压到 z-index:1(会覆盖根节点的 9999)
33
+ */
30
34
  .rv-moveable-layer {
31
35
  --moveable-color: #1990ff;
36
+ z-index: 9999 !important;
32
37
  }
33
38
 
34
- .rv-moveable-layer .moveable-control {
35
- width: 14px !important;
36
- height: 14px !important;
37
- margin-top: -7px !important;
38
- margin-left: -7px !important;
39
- border-radius: 50% !important;
40
- box-sizing: border-box !important;
41
- border: 2px solid hsl(var(--background)) !important;
42
- background: #1990ff !important;
43
- background: var(--moveable-color) !important;
39
+ .rv-moveable-layer.moveable-group,
40
+ .rv-moveable-layer.moveable-control-box {
41
+ z-index: 9999 !important;
44
42
  }
45
43
 
46
- .rv-moveable-layer .moveable-line.moveable-direction {
47
- width: 1px !important;
48
- height: 1px !important;
49
- margin: 0 !important;
44
+ /* Moveable 默认 .group { z-index: -1 },必须覆盖,避免整组掉到节点下方 */
45
+ .rv-moveable-layer.moveable-group {
46
+ z-index: 9999 !important;
50
47
  }
51
48
 
52
49
  .rv-moveable-layer .moveable-line {
53
50
  --moveable-color: #1990ff;
51
+ background: var(--moveable-color) !important;
52
+ }
53
+
54
+ /* 组选/边线:保持颜色;厚度仍由 moveable zoom 的 scaleY 补偿 */
55
+ .rv-moveable-layer .moveable-line.moveable-direction:not(.moveable-edge) {
56
+ background: var(--moveable-color) !important;
57
+ }
58
+
59
+ /* 子 control-box 相对组内层叠,勿压低根节点 */
60
+ .rv-moveable-layer.moveable-group > .moveable-control-box {
61
+ z-index: 2 !important;
54
62
  }
55
63
 
56
64
  .rv-moveable-layer .moveable-around-control {
57
65
  background: transparent !important;
58
66
  }
67
+
68
+ .rv-moveable-layer .moveable-control {
69
+ width: 14px !important;
70
+ height: 14px !important;
71
+ margin-top: -7px !important;
72
+ margin-left: -7px !important;
73
+ border-radius: 50% !important;
74
+ box-sizing: border-box !important;
75
+ border: 2px solid hsl(var(--background)) !important;
76
+ background: #1990ff !important;
77
+ background: var(--moveable-color) !important;
78
+ z-index: 10 !important;
79
+ }