@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
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/** Map panel.update patch keys → config field anchors / sidebar sections. */
|
|
2
|
+
|
|
3
|
+
export type ConfigRevealTarget = {
|
|
4
|
+
field: string;
|
|
5
|
+
section: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const FIELD_SECTION: Record<string, string> = {
|
|
9
|
+
name: "nodeInfo",
|
|
10
|
+
x: "nodeInfo",
|
|
11
|
+
y: "nodeInfo",
|
|
12
|
+
width: "nodeInfo",
|
|
13
|
+
height: "nodeInfo",
|
|
14
|
+
rotate: "nodeInfo",
|
|
15
|
+
locked: "nodeInfo",
|
|
16
|
+
zIndex: "nodeInfo",
|
|
17
|
+
layerId: "nodeInfo",
|
|
18
|
+
textHtml: "textConfig",
|
|
19
|
+
textColor: "textConfig",
|
|
20
|
+
textFontSize: "textConfig",
|
|
21
|
+
textFontFamily: "textConfig",
|
|
22
|
+
textFontWeight: "textConfig",
|
|
23
|
+
textAlign: "textConfig",
|
|
24
|
+
textLineHeight: "textConfig",
|
|
25
|
+
table: "tableConfig",
|
|
26
|
+
"table.source": "tableConfig",
|
|
27
|
+
"table.rowsText": "tableConfig",
|
|
28
|
+
"table.columns": "tableConfig",
|
|
29
|
+
"table.emptyText": "tableConfig",
|
|
30
|
+
"table.showHeader": "tableConfig",
|
|
31
|
+
"table.stripe": "tableConfig",
|
|
32
|
+
"table.rowHeight": "tableConfig",
|
|
33
|
+
chart: "chartBasic",
|
|
34
|
+
"chart.title": "chartBasic",
|
|
35
|
+
"chart.labelsText": "chartBasic",
|
|
36
|
+
"chart.valuesText": "chartBasic",
|
|
37
|
+
"chart.color": "chartBasic",
|
|
38
|
+
style: "nodeInfo",
|
|
39
|
+
"style.backgroundColor": "nodeInfo",
|
|
40
|
+
"style.borderColor": "nodeInfo",
|
|
41
|
+
geometryShape: "geometryConfig",
|
|
42
|
+
geometryColor: "geometryConfig",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function configTargetsFromPatch(
|
|
46
|
+
patch: Record<string, unknown>
|
|
47
|
+
): ConfigRevealTarget[] {
|
|
48
|
+
const out: ConfigRevealTarget[] = [];
|
|
49
|
+
const seen = new Set<string>();
|
|
50
|
+
|
|
51
|
+
const push = (field: string) => {
|
|
52
|
+
const section = FIELD_SECTION[field] ?? FIELD_SECTION[field.split(".")[0]!] ?? "nodeInfo";
|
|
53
|
+
if (seen.has(field)) return;
|
|
54
|
+
seen.add(field);
|
|
55
|
+
out.push({ field, section });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
for (const key of Object.keys(patch)) {
|
|
59
|
+
if (key === "table" && patch.table && typeof patch.table === "object") {
|
|
60
|
+
for (const sub of Object.keys(patch.table as object)) {
|
|
61
|
+
push(`table.${sub}`);
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (key === "chart" && patch.chart && typeof patch.chart === "object") {
|
|
66
|
+
for (const sub of Object.keys(patch.chart as object)) {
|
|
67
|
+
push(`chart.${sub}`);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (key === "style" && patch.style && typeof patch.style === "object") {
|
|
72
|
+
for (const sub of Object.keys(patch.style as object)) {
|
|
73
|
+
push(`style.${sub}`);
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
push(key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const REVEAL_EVENT = "abuilder:reveal-config";
|
|
84
|
+
|
|
85
|
+
export type RevealConfigDetail = {
|
|
86
|
+
fields: string[];
|
|
87
|
+
sections: string[];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export function revealPanelConfig(targets: ConfigRevealTarget[]) {
|
|
91
|
+
if (typeof window === "undefined" || !targets.length) return;
|
|
92
|
+
const detail: RevealConfigDetail = {
|
|
93
|
+
fields: targets.map((t) => t.field),
|
|
94
|
+
sections: [...new Set(targets.map((t) => t.section))],
|
|
95
|
+
};
|
|
96
|
+
window.dispatchEvent(new CustomEvent(REVEAL_EVENT, { detail }));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function revealPanelConfigFromPatch(patch: Record<string, unknown>) {
|
|
100
|
+
revealPanelConfig(configTargetsFromPatch(patch));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function subscribeRevealPanelConfig(
|
|
104
|
+
handler: (detail: RevealConfigDetail) => void
|
|
105
|
+
): () => void {
|
|
106
|
+
const listener = (event: Event) => {
|
|
107
|
+
const detail = (event as CustomEvent<RevealConfigDetail>).detail;
|
|
108
|
+
if (!detail) return;
|
|
109
|
+
handler(detail);
|
|
110
|
+
};
|
|
111
|
+
window.addEventListener(REVEAL_EVENT, listener);
|
|
112
|
+
return () => window.removeEventListener(REVEAL_EVENT, listener);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function highlightConfigField(field: string) {
|
|
116
|
+
const el = document.querySelector<HTMLElement>(
|
|
117
|
+
`[data-config-field="${CSS.escape(field)}"]`
|
|
118
|
+
);
|
|
119
|
+
if (!el) return;
|
|
120
|
+
|
|
121
|
+
const scrollRoot = el.closest(".scope-config-sidebar") as HTMLElement | null;
|
|
122
|
+
if (scrollRoot) {
|
|
123
|
+
const rootRect = scrollRoot.getBoundingClientRect();
|
|
124
|
+
const elRect = el.getBoundingClientRect();
|
|
125
|
+
const top =
|
|
126
|
+
elRect.top -
|
|
127
|
+
rootRect.top +
|
|
128
|
+
scrollRoot.scrollTop -
|
|
129
|
+
scrollRoot.clientHeight / 3;
|
|
130
|
+
scrollRoot.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
|
|
131
|
+
} else {
|
|
132
|
+
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
el.classList.add("config-field--highlight");
|
|
136
|
+
window.setTimeout(() => el.classList.remove("config-field--highlight"), 1800);
|
|
137
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
import {
|
|
3
4
|
Tooltip,
|
|
4
5
|
TooltipContent,
|
|
@@ -14,11 +15,14 @@ export type ConfigHintIconProps = {
|
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export function ConfigHintIcon({
|
|
17
|
-
label
|
|
18
|
+
label,
|
|
18
19
|
children,
|
|
19
20
|
className,
|
|
20
21
|
contentClassName,
|
|
21
22
|
}: ConfigHintIconProps) {
|
|
23
|
+
const { t } = useI18n();
|
|
24
|
+
const resolvedLabel = label ?? t("common.hint");
|
|
25
|
+
|
|
22
26
|
return (
|
|
23
27
|
<TooltipProvider delayDuration={120}>
|
|
24
28
|
<Tooltip>
|
|
@@ -29,7 +33,7 @@ export function ConfigHintIcon({
|
|
|
29
33
|
className ??
|
|
30
34
|
"inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border text-[10px] leading-none text-muted-foreground hover:bg-accent/50"
|
|
31
35
|
}
|
|
32
|
-
aria-label={
|
|
36
|
+
aria-label={t("common.hintAria", { label: resolvedLabel })}
|
|
33
37
|
>
|
|
34
38
|
?
|
|
35
39
|
</button>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useI18n } from "@arronqzy/i18n/react";
|
|
2
3
|
import * as echarts from "echarts";
|
|
3
4
|
import type { PanelElement } from "../types";
|
|
4
5
|
import { buildChartOption, CHART_TYPES } from "../utils/chartOptionBuilder";
|
|
5
6
|
import { PREVIEW_LAYOUT_EVENT } from "../utils/panelStateIO";
|
|
7
|
+
import { TableNodeContent, type TableCellActionHandler } from "./table/TableNodeContent";
|
|
6
8
|
|
|
7
9
|
export type ElementsLayerProps = {
|
|
8
10
|
elements: PanelElement[];
|
|
@@ -19,6 +21,7 @@ export type ElementsLayerProps = {
|
|
|
19
21
|
previewMode?: boolean;
|
|
20
22
|
/** 预览布局变更时递增,用于触发图表/画布重绘 */
|
|
21
23
|
previewLayoutKey?: number;
|
|
24
|
+
onTableCellAction?: TableCellActionHandler;
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
function TextNodeContent({
|
|
@@ -31,7 +34,8 @@ function TextNodeContent({
|
|
|
31
34
|
onChange: (nextHtml: string) => void;
|
|
32
35
|
}) {
|
|
33
36
|
const ref = useRef<HTMLDivElement | null>(null);
|
|
34
|
-
const
|
|
37
|
+
const { t } = useI18n();
|
|
38
|
+
const html = element.textHtml ?? `<p>${t("panel.defaults.doubleClickTextHtml")}</p>`;
|
|
35
39
|
const textStyle: React.CSSProperties = {
|
|
36
40
|
fontFamily: element.textFontFamily || undefined,
|
|
37
41
|
fontSize: element.textFontSize ? `${element.textFontSize}px` : undefined,
|
|
@@ -71,6 +75,7 @@ function GridNodeContent({
|
|
|
71
75
|
allElements: PanelElement[];
|
|
72
76
|
previewMode?: boolean;
|
|
73
77
|
}) {
|
|
78
|
+
const { t } = useI18n();
|
|
74
79
|
const rows = Math.max(1, Math.floor(element.gridRows ?? 2));
|
|
75
80
|
const cols = Math.max(1, Math.floor(element.gridCols ?? 3));
|
|
76
81
|
const gap = Math.max(0, element.gridGap ?? 8);
|
|
@@ -142,7 +147,7 @@ function GridNodeContent({
|
|
|
142
147
|
? "border-primary/70 bg-primary/10"
|
|
143
148
|
: "border-border/60 bg-muted/20",
|
|
144
149
|
].join(" ")}
|
|
145
|
-
title={
|
|
150
|
+
title={occupied.has(idx) ? t("panel.config.slotOccupied", { n: idx + 1 }) : t("panel.config.slotEmpty", { n: idx + 1 })}
|
|
146
151
|
/>
|
|
147
152
|
))}
|
|
148
153
|
</div>
|
|
@@ -179,6 +184,7 @@ function AudioNodeContent({
|
|
|
179
184
|
element: PanelElement;
|
|
180
185
|
selected?: boolean;
|
|
181
186
|
}) {
|
|
187
|
+
const { t } = useI18n();
|
|
182
188
|
const src = element.audioSrc || element.audioRemoteUrl || "";
|
|
183
189
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
184
190
|
const [playing, setPlaying] = useState(false);
|
|
@@ -269,11 +275,11 @@ function AudioNodeContent({
|
|
|
269
275
|
el.pause();
|
|
270
276
|
}
|
|
271
277
|
}}
|
|
272
|
-
title={!src ? "
|
|
278
|
+
title={!src ? t("panel.config.configureAudioFirst") : playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
|
|
273
279
|
>
|
|
274
280
|
<img
|
|
275
281
|
src={poster}
|
|
276
|
-
alt="
|
|
282
|
+
alt={t("panel.config.audioPoster")}
|
|
277
283
|
className="h-full w-full object-cover"
|
|
278
284
|
draggable={false}
|
|
279
285
|
onDragStart={(e) => e.preventDefault()}
|
|
@@ -295,7 +301,7 @@ function AudioNodeContent({
|
|
|
295
301
|
el.pause();
|
|
296
302
|
}
|
|
297
303
|
}}
|
|
298
|
-
title={!src ? "
|
|
304
|
+
title={!src ? t("panel.config.configureAudioFirst") : playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
|
|
299
305
|
>
|
|
300
306
|
{renderIcon()}
|
|
301
307
|
</button>
|
|
@@ -313,7 +319,7 @@ function AudioNodeContent({
|
|
|
313
319
|
</>
|
|
314
320
|
) : null}
|
|
315
321
|
<span className="pointer-events-none absolute right-2 top-2 rounded bg-black/55 px-1.5 py-0.5 text-[10px] text-white">
|
|
316
|
-
{!src ? "
|
|
322
|
+
{!src ? t("panel.config.audioNotConfigured") : playing ? t("panel.config.pause") : t("panel.config.play")}
|
|
317
323
|
</span>
|
|
318
324
|
<audio ref={audioRef} src={src} preload="metadata" className="hidden" />
|
|
319
325
|
</div>
|
|
@@ -322,14 +328,14 @@ function AudioNodeContent({
|
|
|
322
328
|
if (!src) {
|
|
323
329
|
return (
|
|
324
330
|
<div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
|
|
325
|
-
|
|
331
|
+
{t("panel.config.audioPlaceholder")}
|
|
326
332
|
</div>
|
|
327
333
|
);
|
|
328
334
|
}
|
|
329
335
|
return (
|
|
330
336
|
<div className="relative flex h-full w-full items-center justify-center rounded border border-border/60 bg-muted/10 px-2">
|
|
331
337
|
<div className="pointer-events-none text-[11px] text-muted-foreground">
|
|
332
|
-
{playing ? "
|
|
338
|
+
{playing ? t("panel.config.audioPlaying") : t("panel.config.audioReady")}
|
|
333
339
|
</div>
|
|
334
340
|
<button
|
|
335
341
|
type="button"
|
|
@@ -345,9 +351,9 @@ function AudioNodeContent({
|
|
|
345
351
|
el.pause();
|
|
346
352
|
}
|
|
347
353
|
}}
|
|
348
|
-
title={playing ? "
|
|
354
|
+
title={playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
|
|
349
355
|
>
|
|
350
|
-
{playing ? "
|
|
356
|
+
{playing ? t("panel.config.pause") : t("panel.config.play")}
|
|
351
357
|
</button>
|
|
352
358
|
<audio ref={audioRef} src={src} preload="metadata" className="hidden" />
|
|
353
359
|
</div>
|
|
@@ -361,6 +367,7 @@ function VideoNodeContent({
|
|
|
361
367
|
element: PanelElement;
|
|
362
368
|
selected?: boolean;
|
|
363
369
|
}) {
|
|
370
|
+
const { t } = useI18n();
|
|
364
371
|
const src = element.videoSrc || element.videoRemoteUrl || "";
|
|
365
372
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
366
373
|
const [playing, setPlaying] = useState(false);
|
|
@@ -388,7 +395,7 @@ function VideoNodeContent({
|
|
|
388
395
|
if (!src) {
|
|
389
396
|
return (
|
|
390
397
|
<div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
|
|
391
|
-
|
|
398
|
+
{t("panel.config.videoPlaceholder")}
|
|
392
399
|
</div>
|
|
393
400
|
);
|
|
394
401
|
}
|
|
@@ -409,9 +416,9 @@ function VideoNodeContent({
|
|
|
409
416
|
el.pause();
|
|
410
417
|
}
|
|
411
418
|
}}
|
|
412
|
-
title={playing ? "
|
|
419
|
+
title={playing ? t("panel.config.clickPause") : t("panel.config.clickPlay")}
|
|
413
420
|
>
|
|
414
|
-
{playing ? "
|
|
421
|
+
{playing ? t("panel.config.pause") : t("panel.config.play")}
|
|
415
422
|
</button>
|
|
416
423
|
</div>
|
|
417
424
|
);
|
|
@@ -423,20 +430,22 @@ function hasBackgroundImage(element: PanelElement) {
|
|
|
423
430
|
}
|
|
424
431
|
|
|
425
432
|
function ImageNodeContent({ element }: { element: PanelElement }) {
|
|
433
|
+
const { t } = useI18n();
|
|
426
434
|
if (hasBackgroundImage(element)) return null;
|
|
427
435
|
return (
|
|
428
436
|
<div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
|
|
429
|
-
|
|
437
|
+
{t("panel.config.imagePlaceholder")}
|
|
430
438
|
</div>
|
|
431
439
|
);
|
|
432
440
|
}
|
|
433
441
|
|
|
434
442
|
function EmptyNodePlaceholder({ element }: { element: PanelElement }) {
|
|
443
|
+
const { t } = useI18n();
|
|
435
444
|
const labelMap: Record<string, string> = {
|
|
436
|
-
video: "
|
|
437
|
-
audio: "
|
|
445
|
+
video: t("panel.config.videoPlaceholder"),
|
|
446
|
+
audio: t("panel.config.audioPlaceholder"),
|
|
438
447
|
};
|
|
439
|
-
const label = labelMap[element.materialType ?? ""] ??
|
|
448
|
+
const label = labelMap[element.materialType ?? ""] ?? t("panel.config.materialPlaceholder", { type: element.materialType ?? t("common.node") });
|
|
440
449
|
return (
|
|
441
450
|
<div className="flex h-full w-full items-center justify-center rounded border border-dashed border-border/70 bg-muted/15 px-2 text-[11px] text-muted-foreground">
|
|
442
451
|
{label}
|
|
@@ -681,6 +690,7 @@ function ReferenceNodeContent({
|
|
|
681
690
|
snapshotSource?: PanelElement[];
|
|
682
691
|
visitedIds?: string[];
|
|
683
692
|
}) {
|
|
693
|
+
const { t } = useI18n();
|
|
684
694
|
const sourceNodes = useMemo(
|
|
685
695
|
() => {
|
|
686
696
|
const fromDeep =
|
|
@@ -725,7 +735,7 @@ function ReferenceNodeContent({
|
|
|
725
735
|
}, [element.height, element.width, sourceNodes]);
|
|
726
736
|
|
|
727
737
|
if (!layout || sourceNodes.length === 0) {
|
|
728
|
-
const hintText = element.refLayerId ? "
|
|
738
|
+
const hintText = element.refLayerId ? t("panel.config.refLayerEmpty") : t("panel.config.selectRefLayer");
|
|
729
739
|
return (
|
|
730
740
|
<div className="flex h-full w-full items-center justify-center">
|
|
731
741
|
<div className="rounded border border-dashed border-border/70 px-2 py-1 text-[10px] text-muted-foreground">
|
|
@@ -779,6 +789,8 @@ function ReferenceNodeContent({
|
|
|
779
789
|
/>
|
|
780
790
|
) : node.materialType === "geometry" ? (
|
|
781
791
|
<GeometryNodeContent element={node} />
|
|
792
|
+
) : node.materialType === "table" ? (
|
|
793
|
+
<TableNodeContent element={node} interactive={false} />
|
|
782
794
|
) : (
|
|
783
795
|
<div className="h-full w-full" />
|
|
784
796
|
)}
|
|
@@ -799,6 +811,7 @@ export const ElementsLayer = React.memo(function ElementsLayer({
|
|
|
799
811
|
layerLocked = false,
|
|
800
812
|
previewMode = false,
|
|
801
813
|
previewLayoutKey,
|
|
814
|
+
onTableCellAction,
|
|
802
815
|
}: ElementsLayerProps) {
|
|
803
816
|
const sortedElements = useMemo(() => {
|
|
804
817
|
return [...elements].sort((a, b) => {
|
|
@@ -877,6 +890,12 @@ export const ElementsLayer = React.memo(function ElementsLayer({
|
|
|
877
890
|
<GeometryNodeContent element={el} />
|
|
878
891
|
) : el.materialType === "image" ? (
|
|
879
892
|
<ImageNodeContent element={el} />
|
|
893
|
+
) : el.materialType === "table" ? (
|
|
894
|
+
<TableNodeContent
|
|
895
|
+
element={el}
|
|
896
|
+
interactive={previewMode || Boolean(onTableCellAction)}
|
|
897
|
+
onCellAction={onTableCellAction}
|
|
898
|
+
/>
|
|
880
899
|
) : (
|
|
881
900
|
<EmptyNodePlaceholder element={el} />
|
|
882
901
|
)}
|