@arronqzy/react-view 1.0.5 → 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 +519 -9
- package/package.json +12 -4
- package/src/core/template.tsx +12 -2
- package/src/panel/ReactViewOnlinePreview.tsx +18 -8
- package/src/panel/ReactViewPanel.tsx +386 -170
- 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 +6 -2
- package/src/panel/components/ElementsLayer.tsx +37 -18
- package/src/panel/components/MaterialSidebar.tsx +157 -109
- package/src/panel/components/MoveableLayer.tsx +7 -6
- package/src/panel/components/PanelConfigSidebar.tsx +434 -389
- package/src/panel/components/ViewElementScopePanel.tsx +9 -5
- 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/ScopeTemplatePreviewHost.tsx +11 -7
- package/src/panel/components/scope-config/ScopeTemplateUsageHint.tsx +6 -18
- package/src/panel/components/scope-config/ScopeTemplateWarningsPanel.tsx +5 -2
- 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/useWorkspaceProjects.ts +11 -8
- package/src/panel/types.ts +4 -0
- package/src/panel/utils/panelElementDefaults.ts +69 -39
- package/src/panel/utils/scope-field-labels.ts +51 -38
- package/src/panel/utils/scope-template-preview.ts +10 -6
- package/src/panel/utils/scope-template-spread.ts +44 -21
- package/src/panel/utils/scope-template-warnings.ts +28 -16
- package/src/panel/utils/scope-template.ts +82 -2
- package/src/tailwind.css +17 -9
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
|
|
3
4
|
import { formatViewElementScope } from "../utils/scope-template";
|
|
4
5
|
import { ConfigHintIcon } from "./ConfigHintIcon";
|
|
@@ -10,7 +11,10 @@ export type ViewElementScopePanelProps = {
|
|
|
10
11
|
scope: unknown;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
export function ViewElementScopePanel({
|
|
14
|
+
export function ViewElementScopePanel({
|
|
15
|
+
scope,
|
|
16
|
+
}: ViewElementScopePanelProps) {
|
|
17
|
+
const { t } = useI18n();
|
|
14
18
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
15
19
|
const [scopeUpdated, setScopeUpdated] = useState(false);
|
|
16
20
|
const prevScopeSerializedRef = useRef<string | null>(null);
|
|
@@ -44,13 +48,13 @@ export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
|
|
|
44
48
|
<div className="mt-2 border-t border-border/50 pt-2">
|
|
45
49
|
<div className="flex items-center justify-between gap-2">
|
|
46
50
|
<div className="flex min-w-0 items-center gap-1.5">
|
|
47
|
-
<div className="text-[11px] font-medium text-foreground">
|
|
48
|
-
<ConfigHintIcon label="
|
|
51
|
+
<div className="text-[11px] font-medium text-foreground">{t("panel.scope.dataTitle")}</div>
|
|
52
|
+
<ConfigHintIcon label={t("panel.scope.templateHintLabel")} contentClassName="max-w-[380px]">
|
|
49
53
|
<ScopeTemplateUsageHint />
|
|
50
54
|
</ConfigHintIcon>
|
|
51
55
|
{scopeUpdated ? (
|
|
52
56
|
<span className="shrink-0 rounded bg-sky-500/15 px-1.5 py-0.5 text-[10px] font-medium text-sky-700 dark:text-sky-300">
|
|
53
|
-
|
|
57
|
+
{t("panel.scope.updated")}
|
|
54
58
|
</span>
|
|
55
59
|
) : null}
|
|
56
60
|
</div>
|
|
@@ -59,7 +63,7 @@ export function ViewElementScopePanel({ scope }: ViewElementScopePanelProps) {
|
|
|
59
63
|
className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
|
|
60
64
|
onClick={() => setIsCollapsed((prev) => !prev)}
|
|
61
65
|
>
|
|
62
|
-
{isCollapsed ? "
|
|
66
|
+
{isCollapsed ? t("panel.scope.expand") : t("panel.scope.collapse")}
|
|
63
67
|
</button>
|
|
64
68
|
</div>
|
|
65
69
|
{!isCollapsed ? (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
BlueprintExecutionLogPanel,
|
|
@@ -82,6 +83,8 @@ export function WorkspaceConfigSidebar({
|
|
|
82
83
|
allViewElements,
|
|
83
84
|
...viewPanelProps
|
|
84
85
|
}: WorkspaceConfigSidebarProps) {
|
|
86
|
+
const { t } = useI18n();
|
|
87
|
+
|
|
85
88
|
const viewElementOptions = useMemo(
|
|
86
89
|
() =>
|
|
87
90
|
allViewElements.map((el) => ({
|
|
@@ -136,9 +139,9 @@ export function WorkspaceConfigSidebar({
|
|
|
136
139
|
<path d="M8 7l8 4M8 17l8-4" />
|
|
137
140
|
</svg>
|
|
138
141
|
</EmptyIcon>
|
|
139
|
-
<EmptyTitle
|
|
142
|
+
<EmptyTitle>{t("panel.workspace.configEmptyTitle")}</EmptyTitle>
|
|
140
143
|
<EmptyDescription>
|
|
141
|
-
|
|
144
|
+
{t("panel.workspace.configEmptyDesc")}
|
|
142
145
|
</EmptyDescription>
|
|
143
146
|
</Empty>
|
|
144
147
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
import {
|
|
3
4
|
AlertDialog,
|
|
4
5
|
AlertDialogAction,
|
|
@@ -52,6 +53,8 @@ export function WorkspaceProjectNav({
|
|
|
52
53
|
onDeleteProject,
|
|
53
54
|
onPreviewProject,
|
|
54
55
|
}: WorkspaceProjectNavProps) {
|
|
56
|
+
const { t } = useI18n();
|
|
57
|
+
|
|
55
58
|
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
|
56
59
|
const [busy, setBusy] = useState(false);
|
|
57
60
|
|
|
@@ -81,11 +84,11 @@ export function WorkspaceProjectNav({
|
|
|
81
84
|
disabled={busy}
|
|
82
85
|
onClick={() => void runAction(onCreateProject)}
|
|
83
86
|
>
|
|
84
|
-
|
|
87
|
+
{t("panel.workspace.create")}
|
|
85
88
|
</Button>
|
|
86
89
|
</TooltipTrigger>
|
|
87
90
|
<TooltipContent className="z-[10100]">
|
|
88
|
-
|
|
91
|
+
{t("panel.workspace.createHint")}
|
|
89
92
|
</TooltipContent>
|
|
90
93
|
</Tooltip>
|
|
91
94
|
|
|
@@ -100,13 +103,17 @@ export function WorkspaceProjectNav({
|
|
|
100
103
|
disabled={busy || !dirty}
|
|
101
104
|
onClick={() => void runAction(onSyncProject)}
|
|
102
105
|
>
|
|
103
|
-
|
|
106
|
+
{dirty ? t("panel.workspace.syncDirty") : t("common.sync")}
|
|
104
107
|
</Button>
|
|
105
108
|
</TooltipTrigger>
|
|
106
109
|
<TooltipContent className="z-[10100]">
|
|
107
110
|
{dirty
|
|
108
|
-
?
|
|
109
|
-
|
|
111
|
+
? t("panel.workspace.syncUpdate", {
|
|
112
|
+
name:
|
|
113
|
+
activeProjectName ??
|
|
114
|
+
t("panel.workspace.currentWorkspaceFallback"),
|
|
115
|
+
})
|
|
116
|
+
: t("panel.workspace.syncUpToDate")}
|
|
110
117
|
</TooltipContent>
|
|
111
118
|
</Tooltip>
|
|
112
119
|
) : null}
|
|
@@ -120,12 +127,14 @@ export function WorkspaceProjectNav({
|
|
|
120
127
|
className="h-7 max-w-[200px] truncate px-2 text-xs"
|
|
121
128
|
disabled={busy}
|
|
122
129
|
>
|
|
123
|
-
{activeProjectName
|
|
130
|
+
{activeProjectName
|
|
131
|
+
? t("panel.workspace.workspaceNamed", { name: activeProjectName })
|
|
132
|
+
: t("panel.workspace.savedWorkspaces")}
|
|
124
133
|
</Button>
|
|
125
134
|
</DropdownMenuTrigger>
|
|
126
135
|
<DropdownMenuContent align="start" className="z-[10100] w-72">
|
|
127
136
|
{projects.length === 0 ? (
|
|
128
|
-
<DropdownMenuItem disabled
|
|
137
|
+
<DropdownMenuItem disabled>{t("panel.workspace.noSavedWorkspaces")}</DropdownMenuItem>
|
|
129
138
|
) : (
|
|
130
139
|
projects.map((project) => (
|
|
131
140
|
<div key={project.id} className="px-1 py-0.5">
|
|
@@ -160,7 +169,7 @@ export function WorkspaceProjectNav({
|
|
|
160
169
|
})
|
|
161
170
|
}
|
|
162
171
|
>
|
|
163
|
-
|
|
172
|
+
{t("panel.workspace.previewDocTitle")}
|
|
164
173
|
</Button>
|
|
165
174
|
<Button
|
|
166
175
|
type="button"
|
|
@@ -169,7 +178,7 @@ export function WorkspaceProjectNav({
|
|
|
169
178
|
className="h-7 shrink-0 px-2 text-[10px] text-destructive hover:text-destructive"
|
|
170
179
|
onClick={() => setPendingDeleteId(project.id)}
|
|
171
180
|
>
|
|
172
|
-
|
|
181
|
+
{t("common.delete")}
|
|
173
182
|
</Button>
|
|
174
183
|
</div>
|
|
175
184
|
</div>
|
|
@@ -177,7 +186,7 @@ export function WorkspaceProjectNav({
|
|
|
177
186
|
)}
|
|
178
187
|
{/* <DropdownMenuSeparator />
|
|
179
188
|
<DropdownMenuItem onClick={() => void runAction(onCreateProject)}>
|
|
180
|
-
|
|
189
|
+
{t("panel.workspace.create")}
|
|
181
190
|
</DropdownMenuItem> */}
|
|
182
191
|
</DropdownMenuContent>
|
|
183
192
|
</DropdownMenu>
|
|
@@ -192,13 +201,15 @@ export function WorkspaceProjectNav({
|
|
|
192
201
|
>
|
|
193
202
|
<AlertDialogContent className="z-[10150]">
|
|
194
203
|
<AlertDialogHeader>
|
|
195
|
-
<AlertDialogTitle
|
|
204
|
+
<AlertDialogTitle>{t("panel.workspace.deleteConfirmTitle")}</AlertDialogTitle>
|
|
196
205
|
<AlertDialogDescription>
|
|
197
|
-
|
|
206
|
+
{t("panel.workspace.deleteConfirmBody", {
|
|
207
|
+
name: pendingDelete?.name ?? "",
|
|
208
|
+
})}
|
|
198
209
|
</AlertDialogDescription>
|
|
199
210
|
</AlertDialogHeader>
|
|
200
211
|
<AlertDialogFooter>
|
|
201
|
-
<AlertDialogCancel
|
|
212
|
+
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
|
|
202
213
|
<AlertDialogAction
|
|
203
214
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
204
215
|
onClick={() => {
|
|
@@ -210,7 +221,7 @@ export function WorkspaceProjectNav({
|
|
|
210
221
|
});
|
|
211
222
|
}}
|
|
212
223
|
>
|
|
213
|
-
|
|
224
|
+
{t("common.delete")}
|
|
214
225
|
</AlertDialogAction>
|
|
215
226
|
</AlertDialogFooter>
|
|
216
227
|
</AlertDialogContent>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type ComponentRef, type ReactNode } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
|
|
3
4
|
import {
|
|
4
5
|
BluePrintReactRoot,
|
|
@@ -58,6 +59,7 @@ function BlueprintPanel({
|
|
|
58
59
|
canSyncBlueprint?: boolean;
|
|
59
60
|
blueprintDebug?: BlueprintDebugToolbarProps;
|
|
60
61
|
}) {
|
|
62
|
+
const { t } = useI18n();
|
|
61
63
|
const wrapRef = useRef<HTMLDivElement>(null);
|
|
62
64
|
const [layoutReady, setLayoutReady] = useState(false);
|
|
63
65
|
|
|
@@ -104,7 +106,7 @@ function BlueprintPanel({
|
|
|
104
106
|
<BluePrintReactRoot style={{ width: "100%", height: "100%" }} {...blueprintProps} />
|
|
105
107
|
) : (
|
|
106
108
|
<div className="flex h-full items-center justify-center text-[11px] text-muted-foreground">
|
|
107
|
-
{blueprintOpen ? "
|
|
109
|
+
{blueprintOpen ? t("panel.workspace.canvasLoading") : ""}
|
|
108
110
|
</div>
|
|
109
111
|
)}
|
|
110
112
|
</div>
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
useState,
|
|
7
7
|
type RefObject,
|
|
8
8
|
} from "react";
|
|
9
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
9
10
|
import { createPortal } from "react-dom";
|
|
10
11
|
|
|
11
12
|
import type { PanelElement } from "../../types";
|
|
@@ -92,8 +93,9 @@ function inferFieldIdFromDom(
|
|
|
92
93
|
): string | undefined {
|
|
93
94
|
const label = input.closest("label");
|
|
94
95
|
const labelText = label?.textContent ?? "";
|
|
95
|
-
|
|
96
|
-
if (
|
|
96
|
+
// Match both zh-CN and en-US config labels
|
|
97
|
+
if (/类目|categor|labels/i.test(labelText)) return "chart.labelsText";
|
|
98
|
+
if (/数值|values/i.test(labelText)) return "chart.valuesText";
|
|
97
99
|
return undefined;
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -106,11 +108,12 @@ function ScopeTemplatePreviewPanel({
|
|
|
106
108
|
scope: unknown;
|
|
107
109
|
fieldId?: string;
|
|
108
110
|
}) {
|
|
111
|
+
const { t } = useI18n();
|
|
109
112
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
110
113
|
const previewValue = resolveScopeTemplatePreview(template, scope, fieldId);
|
|
111
114
|
const previewText = formatScopeTemplatePreview(previewValue);
|
|
112
115
|
const previewType = Array.isArray(previewValue)
|
|
113
|
-
?
|
|
116
|
+
? t("panel.scope.previewArray", { count: previewValue.length })
|
|
114
117
|
: typeof previewValue;
|
|
115
118
|
|
|
116
119
|
return (
|
|
@@ -120,9 +123,9 @@ function ScopeTemplatePreviewPanel({
|
|
|
120
123
|
className="flex w-full items-center justify-between gap-2 px-2 py-1 text-left text-[10px] text-muted-foreground hover:bg-accent/40"
|
|
121
124
|
onClick={() => setIsExpanded((prev) => !prev)}
|
|
122
125
|
>
|
|
123
|
-
<span className="font-medium text-foreground/80"
|
|
126
|
+
<span className="font-medium text-foreground/80">{t("panel.scope.previewTitle")}</span>
|
|
124
127
|
<span className="shrink-0 text-[10px]">
|
|
125
|
-
{previewType} · {isExpanded ? "
|
|
128
|
+
{previewType} · {isExpanded ? t("panel.scope.collapsePreview") : t("panel.scope.expandPreview")}
|
|
126
129
|
</span>
|
|
127
130
|
</button>
|
|
128
131
|
{isExpanded ? (
|
|
@@ -143,12 +146,13 @@ export function ScopeTemplatePreviewHost({
|
|
|
143
146
|
element: PanelElement | null;
|
|
144
147
|
containerRef?: RefObject<HTMLElement | null>;
|
|
145
148
|
}) {
|
|
149
|
+
const { t } = useI18n();
|
|
146
150
|
const keySeed = useId();
|
|
147
151
|
const [anchors, setAnchors] = useState<PreviewAnchor[]>([]);
|
|
148
152
|
|
|
149
153
|
const templateFields = useMemo(
|
|
150
|
-
() => (element ? collectElementScopeTemplateFields(element) : []),
|
|
151
|
-
[element]
|
|
154
|
+
() => (element ? collectElementScopeTemplateFields(element, t) : []),
|
|
155
|
+
[element, t]
|
|
152
156
|
);
|
|
153
157
|
|
|
154
158
|
const scanAnchors = useCallback(() => {
|
|
@@ -1,24 +1,12 @@
|
|
|
1
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
|
+
|
|
1
3
|
export function ScopeTemplateUsageHint() {
|
|
4
|
+
const { t } = useI18n();
|
|
5
|
+
|
|
2
6
|
return (
|
|
3
7
|
<div className="space-y-2">
|
|
4
|
-
<p>
|
|
5
|
-
|
|
6
|
-
<code className="rounded bg-muted px-1">{`{scope?.a||0}`}</code>
|
|
7
|
-
、
|
|
8
|
-
<code className="rounded bg-muted px-1">{`{scope?.name||''}`}</code>
|
|
9
|
-
。支持 <code className="rounded bg-muted px-1">?.</code> 与{" "}
|
|
10
|
-
<code className="rounded bg-muted px-1">||</code> 占位。
|
|
11
|
-
</p>
|
|
12
|
-
<p>
|
|
13
|
-
数组展开模版{" "}
|
|
14
|
-
<code className="rounded bg-muted px-1">{`[...{scope?.a?.b}]`}</code>
|
|
15
|
-
、
|
|
16
|
-
<code className="rounded bg-muted px-1">
|
|
17
|
-
{`[...{scope?.data?.data?.statusName}]`}
|
|
18
|
-
</code>
|
|
19
|
-
:在文本字段输出逗号分隔字符串;在图表「类目」等数组字段会展开为 n
|
|
20
|
-
个独立项(如 3 条数据对应 3 个类目)。
|
|
21
|
-
</p>
|
|
8
|
+
<p>{t("panel.scope.usageHint1")}</p>
|
|
9
|
+
<p>{t("panel.scope.usageHint2")}</p>
|
|
22
10
|
</div>
|
|
23
11
|
);
|
|
24
12
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
|
|
3
4
|
import { useScopeConfig } from "./ScopeConfigContext";
|
|
4
5
|
|
|
5
6
|
const WARNINGS_COLLAPSE_STORAGE_KEY = "panel:config-scope-warnings-collapsed";
|
|
6
7
|
|
|
7
8
|
export function ScopeTemplateWarningsPanel() {
|
|
9
|
+
const { t } = useI18n();
|
|
10
|
+
|
|
8
11
|
const ctx = useScopeConfig();
|
|
9
12
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
10
13
|
|
|
@@ -26,7 +29,7 @@ export function ScopeTemplateWarningsPanel() {
|
|
|
26
29
|
<div className="mt-2 border-t border-border/50 pt-2">
|
|
27
30
|
<div className="flex items-center justify-between gap-2">
|
|
28
31
|
<div className="text-[11px] font-medium text-amber-700 dark:text-amber-300">
|
|
29
|
-
|
|
32
|
+
{t("panel.scope.warningsTitle")}
|
|
30
33
|
<span className="ml-1.5 rounded bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-semibold">
|
|
31
34
|
{ctx.warnings.length}
|
|
32
35
|
</span>
|
|
@@ -36,7 +39,7 @@ export function ScopeTemplateWarningsPanel() {
|
|
|
36
39
|
className="rounded border border-border px-1.5 py-0.5 text-[11px] text-muted-foreground hover:bg-accent"
|
|
37
40
|
onClick={() => setIsCollapsed((prev) => !prev)}
|
|
38
41
|
>
|
|
39
|
-
{isCollapsed ? "
|
|
42
|
+
{isCollapsed ? t("panel.scope.expandWarnings") : t("panel.scope.collapseWarnings")}
|
|
40
43
|
</button>
|
|
41
44
|
</div>
|
|
42
45
|
{!isCollapsed ? (
|
|
@@ -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
|
+
}
|