@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
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { Condition, ConditionLeaf, ConditionOp } from "@arronqzy/view-table";
|
|
2
|
+
import {
|
|
3
|
+
addConditionItem,
|
|
4
|
+
getConditionLogic,
|
|
5
|
+
isConditionLeaf,
|
|
6
|
+
listConditionItems,
|
|
7
|
+
removeConditionItem,
|
|
8
|
+
setConditionLogic,
|
|
9
|
+
updateConditionLeaf,
|
|
10
|
+
type ConditionLogic,
|
|
11
|
+
} from "@arronqzy/view-table";
|
|
12
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
13
|
+
import {
|
|
14
|
+
Button,
|
|
15
|
+
Input,
|
|
16
|
+
Select,
|
|
17
|
+
SelectContent,
|
|
18
|
+
SelectItem,
|
|
19
|
+
SelectTrigger,
|
|
20
|
+
SelectValue,
|
|
21
|
+
} from "@arronqzy/ui";
|
|
22
|
+
import React from "react";
|
|
23
|
+
|
|
24
|
+
const CONDITION_OPS: ConditionOp[] = [
|
|
25
|
+
"eq",
|
|
26
|
+
"neq",
|
|
27
|
+
"gt",
|
|
28
|
+
"gte",
|
|
29
|
+
"lt",
|
|
30
|
+
"lte",
|
|
31
|
+
"contains",
|
|
32
|
+
"startsWith",
|
|
33
|
+
"endsWith",
|
|
34
|
+
"in",
|
|
35
|
+
"empty",
|
|
36
|
+
"notEmpty",
|
|
37
|
+
"regex",
|
|
38
|
+
"truthy",
|
|
39
|
+
"falsy",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function leafValueText(value: unknown): string {
|
|
43
|
+
if (value == null) return "";
|
|
44
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return JSON.stringify(value);
|
|
49
|
+
} catch {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseLeafValue(text: string): unknown {
|
|
55
|
+
const trimmed = text.trim();
|
|
56
|
+
if (trimmed === "") return "";
|
|
57
|
+
if (trimmed === "true") return true;
|
|
58
|
+
if (trimmed === "false") return false;
|
|
59
|
+
if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ConditionEditor({
|
|
64
|
+
value,
|
|
65
|
+
onChange,
|
|
66
|
+
showField = false,
|
|
67
|
+
fieldClass = "space-y-1",
|
|
68
|
+
inputClass = "h-8 text-[11px]",
|
|
69
|
+
}: {
|
|
70
|
+
value: Condition | undefined;
|
|
71
|
+
onChange: (next: Condition) => void;
|
|
72
|
+
showField?: boolean;
|
|
73
|
+
fieldClass?: string;
|
|
74
|
+
inputClass?: string;
|
|
75
|
+
}) {
|
|
76
|
+
const { t } = useI18n();
|
|
77
|
+
const logic = getConditionLogic(value);
|
|
78
|
+
const items = listConditionItems(value);
|
|
79
|
+
|
|
80
|
+
const opLabel = (op: ConditionOp) => t(`panel.config.tableOp.${op}`);
|
|
81
|
+
|
|
82
|
+
const logicLabel =
|
|
83
|
+
logic === "or"
|
|
84
|
+
? t("panel.config.tableConditionLogicOr")
|
|
85
|
+
: logic === "not"
|
|
86
|
+
? t("panel.config.tableConditionLogicNot")
|
|
87
|
+
: t("panel.config.tableConditionLogicAnd");
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div className="space-y-2">
|
|
91
|
+
<div className="flex items-center justify-between gap-2">
|
|
92
|
+
<label className={`${fieldClass} min-w-0 flex-1`}>
|
|
93
|
+
<div className="text-[11px] text-muted-foreground">{t("panel.config.tableConditionLogic")}</div>
|
|
94
|
+
<Select
|
|
95
|
+
value={logic}
|
|
96
|
+
onValueChange={(next) => onChange(setConditionLogic(value, next as ConditionLogic))}
|
|
97
|
+
>
|
|
98
|
+
<SelectTrigger className={inputClass}>
|
|
99
|
+
<SelectValue>{logicLabel}</SelectValue>
|
|
100
|
+
</SelectTrigger>
|
|
101
|
+
<SelectContent>
|
|
102
|
+
<SelectItem value="and">{t("panel.config.tableConditionLogicAnd")}</SelectItem>
|
|
103
|
+
<SelectItem value="or">{t("panel.config.tableConditionLogicOr")}</SelectItem>
|
|
104
|
+
<SelectItem value="not">{t("panel.config.tableConditionLogicNot")}</SelectItem>
|
|
105
|
+
</SelectContent>
|
|
106
|
+
</Select>
|
|
107
|
+
</label>
|
|
108
|
+
<Button
|
|
109
|
+
type="button"
|
|
110
|
+
variant="outline"
|
|
111
|
+
size="sm"
|
|
112
|
+
className="mt-4 h-7 shrink-0 px-2 text-[11px]"
|
|
113
|
+
onClick={() => onChange(addConditionItem(value))}
|
|
114
|
+
>
|
|
115
|
+
{t("panel.config.tableConditionAddItem")}
|
|
116
|
+
</Button>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="text-[10px] text-muted-foreground">{t("panel.config.tableConditionLogicHint")}</div>
|
|
119
|
+
{items.map((item, index) => {
|
|
120
|
+
const leaf: ConditionLeaf = isConditionLeaf(item)
|
|
121
|
+
? item
|
|
122
|
+
: { op: "eq", value: "" };
|
|
123
|
+
const needsValue = !["empty", "notEmpty", "truthy", "falsy"].includes(leaf.op);
|
|
124
|
+
return (
|
|
125
|
+
<div
|
|
126
|
+
key={`cond-${index}`}
|
|
127
|
+
className="space-y-1.5 rounded-md border border-border/50 bg-background/70 p-2"
|
|
128
|
+
>
|
|
129
|
+
<div className="flex items-center justify-between gap-2">
|
|
130
|
+
<div className="text-[10px] font-medium text-muted-foreground">
|
|
131
|
+
{t("panel.config.tableConditionItem", { n: index + 1 })}
|
|
132
|
+
</div>
|
|
133
|
+
<Button
|
|
134
|
+
type="button"
|
|
135
|
+
variant="ghost"
|
|
136
|
+
size="sm"
|
|
137
|
+
className="h-6 px-1.5 text-[11px] text-destructive"
|
|
138
|
+
disabled={items.length <= 1}
|
|
139
|
+
onClick={() => onChange(removeConditionItem(value, index))}
|
|
140
|
+
>
|
|
141
|
+
×
|
|
142
|
+
</Button>
|
|
143
|
+
</div>
|
|
144
|
+
<div className={`grid grid-cols-1 gap-1.5 ${showField ? "sm:grid-cols-3" : "sm:grid-cols-2"}`}>
|
|
145
|
+
<label className={fieldClass}>
|
|
146
|
+
<div className="text-[11px] text-muted-foreground">{t("panel.config.tableConditionOp")}</div>
|
|
147
|
+
<Select
|
|
148
|
+
value={leaf.op}
|
|
149
|
+
onValueChange={(next) =>
|
|
150
|
+
onChange(updateConditionLeaf(value, index, { op: next as ConditionOp }))
|
|
151
|
+
}
|
|
152
|
+
>
|
|
153
|
+
<SelectTrigger className={inputClass}>
|
|
154
|
+
<SelectValue>{opLabel(leaf.op)}</SelectValue>
|
|
155
|
+
</SelectTrigger>
|
|
156
|
+
<SelectContent>
|
|
157
|
+
{CONDITION_OPS.map((op) => (
|
|
158
|
+
<SelectItem key={op} value={op}>
|
|
159
|
+
{opLabel(op)}
|
|
160
|
+
</SelectItem>
|
|
161
|
+
))}
|
|
162
|
+
</SelectContent>
|
|
163
|
+
</Select>
|
|
164
|
+
</label>
|
|
165
|
+
{showField ? (
|
|
166
|
+
<label className={fieldClass}>
|
|
167
|
+
<div className="text-[11px] text-muted-foreground">{t("panel.config.tableConditionField")}</div>
|
|
168
|
+
<Input
|
|
169
|
+
value={leaf.field ?? ""}
|
|
170
|
+
onChange={(e) =>
|
|
171
|
+
onChange(
|
|
172
|
+
updateConditionLeaf(value, index, {
|
|
173
|
+
field: e.target.value || undefined,
|
|
174
|
+
})
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
className={inputClass}
|
|
178
|
+
placeholder={t("panel.config.tableConditionFieldPlaceholder")}
|
|
179
|
+
/>
|
|
180
|
+
</label>
|
|
181
|
+
) : null}
|
|
182
|
+
{needsValue ? (
|
|
183
|
+
<label className={fieldClass}>
|
|
184
|
+
<div className="text-[11px] text-muted-foreground">{t("panel.config.tableConditionValue")}</div>
|
|
185
|
+
<Input
|
|
186
|
+
value={leafValueText(leaf.value)}
|
|
187
|
+
onChange={(e) =>
|
|
188
|
+
onChange(
|
|
189
|
+
updateConditionLeaf(value, index, {
|
|
190
|
+
value: parseLeafValue(e.target.value),
|
|
191
|
+
})
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
className={inputClass}
|
|
195
|
+
placeholder={t("panel.config.tableConditionValuePlaceholder")}
|
|
196
|
+
/>
|
|
197
|
+
</label>
|
|
198
|
+
) : null}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
})}
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { Button } from "@arronqzy/ui";
|
|
3
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
4
|
+
import CodeMirror from "@uiw/react-codemirror";
|
|
5
|
+
import { json } from "@codemirror/lang-json";
|
|
6
|
+
import { oneDark } from "@codemirror/theme-one-dark";
|
|
7
|
+
import { EditorView } from "@codemirror/view";
|
|
8
|
+
|
|
9
|
+
export function JsonCodeEditor({
|
|
10
|
+
value,
|
|
11
|
+
onChange,
|
|
12
|
+
className,
|
|
13
|
+
}: {
|
|
14
|
+
value: string;
|
|
15
|
+
onChange: (next: string) => void;
|
|
16
|
+
className?: string;
|
|
17
|
+
}) {
|
|
18
|
+
const { t } = useI18n();
|
|
19
|
+
const extensions = useMemo(
|
|
20
|
+
() => [
|
|
21
|
+
json(),
|
|
22
|
+
EditorView.lineWrapping,
|
|
23
|
+
EditorView.theme({
|
|
24
|
+
"&": {
|
|
25
|
+
height: "100%",
|
|
26
|
+
maxHeight: "100%",
|
|
27
|
+
fontSize: "13px",
|
|
28
|
+
backgroundColor: "transparent",
|
|
29
|
+
},
|
|
30
|
+
"&.cm-editor": {
|
|
31
|
+
height: "100%",
|
|
32
|
+
maxHeight: "100%",
|
|
33
|
+
backgroundColor: "transparent",
|
|
34
|
+
},
|
|
35
|
+
".cm-scroller": {
|
|
36
|
+
overflow: "auto",
|
|
37
|
+
fontFamily:
|
|
38
|
+
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
39
|
+
lineHeight: "1.55",
|
|
40
|
+
},
|
|
41
|
+
".cm-content": { padding: "8px 0", caretColor: "#e2e8f0" },
|
|
42
|
+
".cm-gutters": {
|
|
43
|
+
backgroundColor: "transparent",
|
|
44
|
+
borderRight: "1px solid rgba(148,163,184,0.12)",
|
|
45
|
+
color: "#64748b",
|
|
46
|
+
},
|
|
47
|
+
".cm-activeLineGutter": { backgroundColor: "rgba(148,163,184,0.08)" },
|
|
48
|
+
".cm-activeLine": { backgroundColor: "rgba(148,163,184,0.06)" },
|
|
49
|
+
}),
|
|
50
|
+
],
|
|
51
|
+
[]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const formatJson = () => {
|
|
55
|
+
const trimmed = value.trim();
|
|
56
|
+
if (!trimmed) return;
|
|
57
|
+
try {
|
|
58
|
+
onChange(JSON.stringify(JSON.parse(trimmed), null, 2));
|
|
59
|
+
} catch {
|
|
60
|
+
// keep as-is; parent validates on save
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div
|
|
66
|
+
className={[
|
|
67
|
+
"flex h-full min-h-0 max-h-full flex-col overflow-hidden rounded-xl border border-slate-700/60 bg-[#0c1220] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
|
|
68
|
+
className,
|
|
69
|
+
]
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.join(" ")}
|
|
72
|
+
>
|
|
73
|
+
<div className="flex shrink-0 items-center justify-between gap-2 border-b border-white/5 px-2.5 py-1.5">
|
|
74
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
75
|
+
<span className="rounded-md bg-sky-500/15 px-1.5 py-0.5 font-mono text-[10px] font-medium tracking-wide text-sky-300">
|
|
76
|
+
JSON
|
|
77
|
+
</span>
|
|
78
|
+
<span className="truncate text-[11px] text-slate-400">
|
|
79
|
+
{t("panel.config.tableRowsEditEditorHint")}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
<Button
|
|
83
|
+
type="button"
|
|
84
|
+
variant="ghost"
|
|
85
|
+
size="sm"
|
|
86
|
+
className="h-6 shrink-0 rounded-md px-2 text-[11px] text-slate-300 hover:bg-white/10 hover:text-white"
|
|
87
|
+
onClick={formatJson}
|
|
88
|
+
>
|
|
89
|
+
{t("panel.config.tableRowsEditFormat")}
|
|
90
|
+
</Button>
|
|
91
|
+
</div>
|
|
92
|
+
<div className="relative min-h-0 flex-1 overflow-hidden">
|
|
93
|
+
<div className="absolute inset-0 overflow-hidden">
|
|
94
|
+
<CodeMirror
|
|
95
|
+
value={value}
|
|
96
|
+
height="100%"
|
|
97
|
+
maxHeight="100%"
|
|
98
|
+
theme={oneDark}
|
|
99
|
+
extensions={extensions}
|
|
100
|
+
basicSetup={{
|
|
101
|
+
lineNumbers: true,
|
|
102
|
+
foldGutter: true,
|
|
103
|
+
highlightActiveLine: true,
|
|
104
|
+
bracketMatching: true,
|
|
105
|
+
autocompletion: true,
|
|
106
|
+
}}
|
|
107
|
+
onChange={(next) => onChange(next)}
|
|
108
|
+
className="h-full max-h-full [&_.cm-editor]:h-full [&_.cm-editor]:max-h-full [&_.cm-editor]:outline-none"
|
|
109
|
+
aria-label={t("panel.config.tableRowsEditTitle")}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|