@object-ui/plugin-detail 3.1.5 → 3.3.1
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/CHANGELOG.md +31 -0
- package/README.md +21 -1
- package/dist/AddressField-LgHnO2Lk.js +98 -0
- package/dist/AutoNumberField-xZCrU0eW.js +14 -0
- package/dist/{AvatarField-YGj51ozd.js → AvatarField-Dy2XGlPz.js} +16 -15
- package/dist/{BooleanField-CaA898Tk.js → BooleanField-C0Clfka5.js} +11 -10
- package/dist/CodeField-CHUa07B6.js +23 -0
- package/dist/ColorField-vxHqEhcS.js +38 -0
- package/dist/CurrencyField-DiWjYWDo.js +49 -0
- package/dist/DateField-DGaRPM4P.js +22 -0
- package/dist/DateTimeField-8QnpsI_h.js +30 -0
- package/dist/EmailField-CkVgMbpI.js +26 -0
- package/dist/FileField-5UPV7uek.js +149 -0
- package/dist/FormulaField-BUgt6-Pi.js +17 -0
- package/dist/GeolocationField-D9T_jgG6.js +118 -0
- package/dist/GridField-DE_HwiIN.js +49 -0
- package/dist/ImageField-Dswnqtzf.js +73 -0
- package/dist/LocationField-gjqbE6na.js +36 -0
- package/dist/LookupField-BcS3LRKc.js +901 -0
- package/dist/{MasterDetailField-I1A9oEGC.js → MasterDetailField-BF6_-X3A.js} +20 -19
- package/dist/NumberField-Dj2rYmrS.js +27 -0
- package/dist/ObjectField-BymIojwd.js +50 -0
- package/dist/{PasswordField-DBtluGJ1.js → PasswordField-ED_Xgqz-.js} +8 -7
- package/dist/PercentField-D-JKOxKC.js +61 -0
- package/dist/PhoneField-DSCaGYq7.js +26 -0
- package/dist/QRCodeField-CtcOUapi.js +73 -0
- package/dist/{RatingField-B_Mnr63i.js → RatingField-BDnyQFWy.js} +10 -9
- package/dist/RichTextField-CH6LVZQA.js +33 -0
- package/dist/SelectField-DE4dpkMV.js +36 -0
- package/dist/{SignatureField-CddhEK9u.js → SignatureField-B1wh3f5A.js} +18 -17
- package/dist/{SliderField-Df5hMzNc.js → SliderField-zoTCKh9n.js} +2 -1
- package/dist/SummaryField-BeBVT6VN.js +22 -0
- package/dist/TextAreaField-rfUGrRxh.js +37 -0
- package/dist/TextField-C_yM7ATQ.js +30 -0
- package/dist/TimeField-BcQmBZi9.js +22 -0
- package/dist/UrlField-BakaF6NI.js +31 -0
- package/dist/UserField-zS7y3eKb.js +76 -0
- package/dist/VectorField-CTZ4myDM.js +34 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1741 -1504
- package/dist/index.umd.cjs +43 -51
- package/dist/packages/plugin-detail/src/ActivityTimeline.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/CommentAttachment.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/CommentInput.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/DetailSection.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/DetailTabs.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/DetailView.d.ts +47 -0
- package/dist/packages/plugin-detail/src/DetailView.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/DetailView.stories.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/DiffView.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/FieldChangeItem.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/HeaderHighlight.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/InlineCreateRelated.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/MentionAutocomplete.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/PointInTimeRestore.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/ReactionPicker.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/RecordActivityTimeline.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/RecordChatterPanel.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/RecordComments.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/RecordNavigationEnhanced.d.ts.map +1 -0
- package/dist/{src → packages/plugin-detail/src}/RelatedList.d.ts +8 -0
- package/dist/packages/plugin-detail/src/RelatedList.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/RelationshipGraph.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/RichTextCommentInput.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/SectionGroup.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/SubscriptionToggle.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/ThreadedReplies.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/autoLayout.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/index.d.ts.map +1 -0
- package/dist/packages/plugin-detail/src/useDetailTranslation.d.ts.map +1 -0
- package/dist/plugin-detail.css +1 -2
- package/dist/rolldown-runtime-DnwLefa7.js +23 -0
- package/dist/{src-CXr1-vVl.js → src-DyUKLvMN.js} +29788 -37711
- package/dist/useFieldTranslation-BRgjC1oq.js +9 -0
- package/package.json +34 -12
- package/.turbo/turbo-build.log +0 -61
- package/dist/AddressField-DBkEyMcG.js +0 -93
- package/dist/AutoNumberField-Baa191z-.js +0 -14
- package/dist/CodeField-BU51nl1L.js +0 -22
- package/dist/ColorField-Cnf6ZM7c.js +0 -37
- package/dist/CurrencyField-Wg-XOId2.js +0 -51
- package/dist/DateField-Cth1ky_m.js +0 -21
- package/dist/DateTimeField-B0m6FhHL.js +0 -32
- package/dist/EmailField-Do7qT_L_.js +0 -28
- package/dist/FileField-aRJAdbQb.js +0 -151
- package/dist/FormulaField-DTMkagFx.js +0 -14
- package/dist/GeolocationField-RqpHWTEv.js +0 -113
- package/dist/GridField-D4IH0cpo.js +0 -51
- package/dist/ImageField-BYCFajjr.js +0 -75
- package/dist/LocationField-Bi_ew9sd.js +0 -35
- package/dist/LookupField-BjwlDPtt.js +0 -902
- package/dist/NumberField-D_NucQlp.js +0 -26
- package/dist/ObjectField-CG-LaM65.js +0 -52
- package/dist/PercentField-B6sO_J3i.js +0 -63
- package/dist/PhoneField-CcQAWwR6.js +0 -28
- package/dist/QRCodeField-CEjWs-J5.js +0 -72
- package/dist/RichTextField-qOEJl5Ai.js +0 -32
- package/dist/SelectField-C8hWu3gm.js +0 -30
- package/dist/SummaryField-DgiFm-Cr.js +0 -19
- package/dist/TextAreaField-DuriTqsD.js +0 -36
- package/dist/TextField-CGNSl7RU.js +0 -29
- package/dist/TimeField-YO58ctFg.js +0 -21
- package/dist/UrlField-1-BMM1jn.js +0 -33
- package/dist/UserField-B6GqxP_S.js +0 -78
- package/dist/VectorField-BkEjbSt0.js +0 -36
- package/dist/src/ActivityTimeline.d.ts.map +0 -1
- package/dist/src/CommentAttachment.d.ts.map +0 -1
- package/dist/src/CommentInput.d.ts.map +0 -1
- package/dist/src/DetailSection.d.ts.map +0 -1
- package/dist/src/DetailTabs.d.ts.map +0 -1
- package/dist/src/DetailView.d.ts +0 -23
- package/dist/src/DetailView.d.ts.map +0 -1
- package/dist/src/DetailView.stories.d.ts.map +0 -1
- package/dist/src/DiffView.d.ts.map +0 -1
- package/dist/src/FieldChangeItem.d.ts.map +0 -1
- package/dist/src/HeaderHighlight.d.ts.map +0 -1
- package/dist/src/InlineCreateRelated.d.ts.map +0 -1
- package/dist/src/MentionAutocomplete.d.ts.map +0 -1
- package/dist/src/PointInTimeRestore.d.ts.map +0 -1
- package/dist/src/ReactionPicker.d.ts.map +0 -1
- package/dist/src/RecordActivityTimeline.d.ts.map +0 -1
- package/dist/src/RecordChatterPanel.d.ts.map +0 -1
- package/dist/src/RecordComments.d.ts.map +0 -1
- package/dist/src/RecordNavigationEnhanced.d.ts.map +0 -1
- package/dist/src/RelatedList.d.ts.map +0 -1
- package/dist/src/RelationshipGraph.d.ts.map +0 -1
- package/dist/src/RichTextCommentInput.d.ts.map +0 -1
- package/dist/src/SectionGroup.d.ts.map +0 -1
- package/dist/src/SubscriptionToggle.d.ts.map +0 -1
- package/dist/src/ThreadedReplies.d.ts.map +0 -1
- package/dist/src/autoLayout.d.ts.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/useDetailTranslation.d.ts.map +0 -1
- package/src/ActivityTimeline.tsx +0 -184
- package/src/CommentAttachment.tsx +0 -192
- package/src/CommentInput.tsx +0 -81
- package/src/DetailSection.tsx +0 -340
- package/src/DetailTabs.tsx +0 -73
- package/src/DetailView.stories.tsx +0 -334
- package/src/DetailView.tsx +0 -823
- package/src/DiffView.tsx +0 -231
- package/src/FieldChangeItem.tsx +0 -46
- package/src/HeaderHighlight.tsx +0 -88
- package/src/InlineCreateRelated.tsx +0 -291
- package/src/MentionAutocomplete.tsx +0 -123
- package/src/PointInTimeRestore.tsx +0 -261
- package/src/ReactionPicker.tsx +0 -106
- package/src/RecordActivityTimeline.tsx +0 -429
- package/src/RecordChatterPanel.tsx +0 -207
- package/src/RecordComments.tsx +0 -215
- package/src/RecordNavigationEnhanced.tsx +0 -211
- package/src/RelatedList.tsx +0 -413
- package/src/RelationshipGraph.tsx +0 -286
- package/src/RichTextCommentInput.tsx +0 -348
- package/src/SectionGroup.tsx +0 -101
- package/src/SubscriptionToggle.tsx +0 -60
- package/src/ThreadedReplies.tsx +0 -161
- package/src/__tests__/ActivityTimeline.test.tsx +0 -119
- package/src/__tests__/ActivityTimelineFiltering.test.tsx +0 -143
- package/src/__tests__/CommentInput.test.tsx +0 -57
- package/src/__tests__/DetailSection.test.tsx +0 -490
- package/src/__tests__/DetailView.test.tsx +0 -694
- package/src/__tests__/FieldChangeItem.test.tsx +0 -119
- package/src/__tests__/HeaderHighlight.test.tsx +0 -213
- package/src/__tests__/MentionAutocomplete.test.tsx +0 -97
- package/src/__tests__/ReactionPicker.test.tsx +0 -113
- package/src/__tests__/RecordActivityTimeline.test.tsx +0 -395
- package/src/__tests__/RecordChatterPanel.test.tsx +0 -265
- package/src/__tests__/RecordComments.test.tsx +0 -96
- package/src/__tests__/RecordCommentsPinSearch.test.tsx +0 -133
- package/src/__tests__/RelatedList.test.tsx +0 -160
- package/src/__tests__/SectionGroup.test.tsx +0 -101
- package/src/__tests__/SubscriptionToggle.test.tsx +0 -84
- package/src/__tests__/ThreadedReplies.test.tsx +0 -212
- package/src/__tests__/autoLayout.test.ts +0 -228
- package/src/__tests__/phase12-features.test.tsx +0 -583
- package/src/__tests__/roadmap-features.test.tsx +0 -478
- package/src/autoLayout.ts +0 -128
- package/src/index.tsx +0 -149
- package/src/useDetailTranslation.ts +0 -114
- package/tsconfig.json +0 -18
- package/vite.config.ts +0 -56
- package/vitest.config.ts +0 -13
- package/vitest.setup.ts +0 -1
- /package/dist/{src → packages/plugin-detail/src}/ActivityTimeline.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/CommentAttachment.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/CommentInput.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/DetailSection.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/DetailTabs.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/DetailView.stories.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/DiffView.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/FieldChangeItem.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/HeaderHighlight.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/InlineCreateRelated.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/MentionAutocomplete.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/PointInTimeRestore.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/ReactionPicker.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/RecordActivityTimeline.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/RecordChatterPanel.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/RecordComments.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/RecordNavigationEnhanced.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/RelationshipGraph.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/RichTextCommentInput.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/SectionGroup.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/SubscriptionToggle.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/ThreadedReplies.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/autoLayout.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/index.d.ts +0 -0
- /package/dist/{src → packages/plugin-detail/src}/useDetailTranslation.d.ts +0 -0
package/src/RelatedList.tsx
DELETED
|
@@ -1,413 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as React from 'react';
|
|
10
|
-
import {
|
|
11
|
-
Card,
|
|
12
|
-
CardHeader,
|
|
13
|
-
CardTitle,
|
|
14
|
-
CardContent,
|
|
15
|
-
Badge,
|
|
16
|
-
Button,
|
|
17
|
-
Input,
|
|
18
|
-
} from '@object-ui/components';
|
|
19
|
-
import { SchemaRenderer } from '@object-ui/react';
|
|
20
|
-
import {
|
|
21
|
-
Plus,
|
|
22
|
-
ExternalLink,
|
|
23
|
-
Edit,
|
|
24
|
-
Trash2,
|
|
25
|
-
ChevronLeft,
|
|
26
|
-
ChevronRight,
|
|
27
|
-
ArrowUpDown,
|
|
28
|
-
ChevronDown,
|
|
29
|
-
} from 'lucide-react';
|
|
30
|
-
import type { DataSource, FieldMetadata } from '@object-ui/types';
|
|
31
|
-
import { getCellRenderer } from '@object-ui/fields';
|
|
32
|
-
import { useDetailTranslation } from './useDetailTranslation';
|
|
33
|
-
import { useSafeFieldLabel } from '@object-ui/react';
|
|
34
|
-
|
|
35
|
-
export interface RelatedListProps {
|
|
36
|
-
title: string;
|
|
37
|
-
type: 'list' | 'grid' | 'table';
|
|
38
|
-
api?: string;
|
|
39
|
-
data?: any[];
|
|
40
|
-
schema?: any;
|
|
41
|
-
columns?: any[];
|
|
42
|
-
className?: string;
|
|
43
|
-
dataSource?: DataSource;
|
|
44
|
-
/** Object name for i18n field label resolution */
|
|
45
|
-
objectName?: string;
|
|
46
|
-
/** Callback when "New" button is clicked */
|
|
47
|
-
onNew?: () => void;
|
|
48
|
-
/** Callback when "View All" button is clicked */
|
|
49
|
-
onViewAll?: () => void;
|
|
50
|
-
/** Callback when a row Edit action is clicked */
|
|
51
|
-
onRowEdit?: (row: any) => void;
|
|
52
|
-
/** Callback when a row Delete action is clicked */
|
|
53
|
-
onRowDelete?: (row: any) => void;
|
|
54
|
-
/** Page size for pagination (enables pagination when set) */
|
|
55
|
-
pageSize?: number;
|
|
56
|
-
/** Enable column sorting */
|
|
57
|
-
sortable?: boolean;
|
|
58
|
-
/** Enable text filtering */
|
|
59
|
-
filterable?: boolean;
|
|
60
|
-
/** Whether the card is collapsible */
|
|
61
|
-
collapsible?: boolean;
|
|
62
|
-
/** Whether the card starts collapsed (requires collapsible=true) */
|
|
63
|
-
defaultCollapsed?: boolean;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const RelatedList: React.FC<RelatedListProps> = ({
|
|
67
|
-
title,
|
|
68
|
-
type,
|
|
69
|
-
api,
|
|
70
|
-
data = [],
|
|
71
|
-
schema,
|
|
72
|
-
columns,
|
|
73
|
-
className,
|
|
74
|
-
dataSource,
|
|
75
|
-
objectName,
|
|
76
|
-
onNew,
|
|
77
|
-
onViewAll,
|
|
78
|
-
onRowEdit,
|
|
79
|
-
onRowDelete,
|
|
80
|
-
pageSize,
|
|
81
|
-
sortable = false,
|
|
82
|
-
filterable = false,
|
|
83
|
-
collapsible = false,
|
|
84
|
-
defaultCollapsed = false,
|
|
85
|
-
}) => {
|
|
86
|
-
const [relatedData, setRelatedData] = React.useState(data);
|
|
87
|
-
const [loading, setLoading] = React.useState(false);
|
|
88
|
-
const [currentPage, setCurrentPage] = React.useState(0);
|
|
89
|
-
const [sortField, setSortField] = React.useState<string | null>(null);
|
|
90
|
-
const [sortDirection, setSortDirection] = React.useState<'asc' | 'desc'>('asc');
|
|
91
|
-
const [filterText, setFilterText] = React.useState('');
|
|
92
|
-
const [objectSchema, setObjectSchema] = React.useState<any>(null);
|
|
93
|
-
const [collapsed, setCollapsed] = React.useState(defaultCollapsed);
|
|
94
|
-
const { t } = useDetailTranslation();
|
|
95
|
-
const { fieldLabel: resolveFieldLabel } = useSafeFieldLabel();
|
|
96
|
-
|
|
97
|
-
// Sync internal state when data prop changes (e.g., parent fetches async data)
|
|
98
|
-
React.useEffect(() => {
|
|
99
|
-
setRelatedData(data);
|
|
100
|
-
}, [data]);
|
|
101
|
-
|
|
102
|
-
// Auto-fetch object schema when api/dataSource available but columns missing
|
|
103
|
-
React.useEffect(() => {
|
|
104
|
-
if (api && dataSource?.getObjectSchema && !columns?.length) {
|
|
105
|
-
dataSource.getObjectSchema(api).then(setObjectSchema).catch((err: unknown) => {
|
|
106
|
-
console.warn(`[RelatedList] Failed to fetch schema for ${api}:`, err);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}, [api, dataSource, columns]);
|
|
110
|
-
|
|
111
|
-
React.useEffect(() => {
|
|
112
|
-
if (api && !data.length) {
|
|
113
|
-
setLoading(true);
|
|
114
|
-
if (dataSource && typeof dataSource.find === 'function') {
|
|
115
|
-
dataSource.find(api).then((result) => {
|
|
116
|
-
const items = Array.isArray(result)
|
|
117
|
-
? result
|
|
118
|
-
: Array.isArray((result as any)?.data)
|
|
119
|
-
? (result as any).data
|
|
120
|
-
: [];
|
|
121
|
-
setRelatedData(items);
|
|
122
|
-
setLoading(false);
|
|
123
|
-
}).catch((err) => {
|
|
124
|
-
console.error('Failed to fetch related data:', err);
|
|
125
|
-
setLoading(false);
|
|
126
|
-
});
|
|
127
|
-
} else {
|
|
128
|
-
fetch(api)
|
|
129
|
-
.then(res => res.json())
|
|
130
|
-
.then(result => {
|
|
131
|
-
const items = Array.isArray(result) ? result : (result?.data || []);
|
|
132
|
-
setRelatedData(items);
|
|
133
|
-
})
|
|
134
|
-
.catch(err => {
|
|
135
|
-
console.error('Failed to fetch related data:', err);
|
|
136
|
-
})
|
|
137
|
-
.finally(() => setLoading(false));
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}, [api, data, dataSource]);
|
|
141
|
-
|
|
142
|
-
// Filter data
|
|
143
|
-
const filteredData = React.useMemo(() => {
|
|
144
|
-
if (!filterText) return relatedData;
|
|
145
|
-
const lower = filterText.toLowerCase();
|
|
146
|
-
return relatedData.filter((row) =>
|
|
147
|
-
Object.values(row).some((val) =>
|
|
148
|
-
val !== null && val !== undefined && String(val).toLowerCase().includes(lower)
|
|
149
|
-
)
|
|
150
|
-
);
|
|
151
|
-
}, [relatedData, filterText]);
|
|
152
|
-
|
|
153
|
-
// Sort data
|
|
154
|
-
const sortedData = React.useMemo(() => {
|
|
155
|
-
if (!sortField) return filteredData;
|
|
156
|
-
return [...filteredData].sort((a, b) => {
|
|
157
|
-
const aVal = a[sortField];
|
|
158
|
-
const bVal = b[sortField];
|
|
159
|
-
if (aVal == null && bVal == null) return 0;
|
|
160
|
-
if (aVal == null) return 1;
|
|
161
|
-
if (bVal == null) return -1;
|
|
162
|
-
const cmp = String(aVal).localeCompare(String(bVal), undefined, { numeric: true });
|
|
163
|
-
return sortDirection === 'asc' ? cmp : -cmp;
|
|
164
|
-
});
|
|
165
|
-
}, [filteredData, sortField, sortDirection]);
|
|
166
|
-
|
|
167
|
-
// Paginate data
|
|
168
|
-
const effectivePageSize = pageSize && pageSize > 0 ? pageSize : 0;
|
|
169
|
-
const totalPages = effectivePageSize ? Math.max(1, Math.ceil(sortedData.length / effectivePageSize)) : 1;
|
|
170
|
-
const paginatedData = effectivePageSize
|
|
171
|
-
? sortedData.slice(currentPage * effectivePageSize, (currentPage + 1) * effectivePageSize)
|
|
172
|
-
: sortedData;
|
|
173
|
-
|
|
174
|
-
// Reset to first page when filter/sort changes
|
|
175
|
-
React.useEffect(() => {
|
|
176
|
-
setCurrentPage(0);
|
|
177
|
-
}, [filterText, sortField, sortDirection]);
|
|
178
|
-
|
|
179
|
-
const handleSort = React.useCallback((field: string) => {
|
|
180
|
-
if (sortField === field) {
|
|
181
|
-
setSortDirection((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
182
|
-
} else {
|
|
183
|
-
setSortField(field);
|
|
184
|
-
setSortDirection('asc');
|
|
185
|
-
}
|
|
186
|
-
}, [sortField]);
|
|
187
|
-
|
|
188
|
-
const handleDeleteRow = React.useCallback((row: any) => {
|
|
189
|
-
if (window.confirm(t('detail.deleteRowConfirmation'))) {
|
|
190
|
-
onRowDelete?.(row);
|
|
191
|
-
}
|
|
192
|
-
}, [onRowDelete, t]);
|
|
193
|
-
|
|
194
|
-
// Generate effective columns from explicit prop or object schema fields
|
|
195
|
-
const effectiveColumns = React.useMemo(() => {
|
|
196
|
-
if (columns && columns.length > 0) return columns;
|
|
197
|
-
if (!objectSchema?.fields) return [];
|
|
198
|
-
const resolvedObjectName = objectName || api || '';
|
|
199
|
-
return Object.entries(objectSchema.fields)
|
|
200
|
-
.filter(([key]) => !key.startsWith('_') && key !== 'id')
|
|
201
|
-
.map(([key, def]: [string, any]) => {
|
|
202
|
-
const col: any = {
|
|
203
|
-
accessorKey: key,
|
|
204
|
-
header: resolveFieldLabel(resolvedObjectName, key, def.label || key),
|
|
205
|
-
};
|
|
206
|
-
// Add type-aware cell renderer for typed fields
|
|
207
|
-
if (def.type) {
|
|
208
|
-
const CellRenderer = getCellRenderer(def.type);
|
|
209
|
-
if (CellRenderer) {
|
|
210
|
-
const fieldMeta: FieldMetadata = {
|
|
211
|
-
name: key,
|
|
212
|
-
label: def.label || key,
|
|
213
|
-
type: def.type,
|
|
214
|
-
...(def.options && { options: def.options }),
|
|
215
|
-
...(def.currency && { currency: def.currency }),
|
|
216
|
-
...(def.precision !== undefined && { precision: def.precision }),
|
|
217
|
-
...(def.format && { format: def.format }),
|
|
218
|
-
...((def.reference_to || def.reference) && { reference_to: def.reference_to || def.reference }),
|
|
219
|
-
...(def.reference_field && { reference_field: def.reference_field }),
|
|
220
|
-
};
|
|
221
|
-
col.cell = (value: any) => {
|
|
222
|
-
if (value === null || value === undefined) {
|
|
223
|
-
return React.createElement('span', { className: 'text-muted-foreground/50 text-xs italic' }, '—');
|
|
224
|
-
}
|
|
225
|
-
return React.createElement(CellRenderer, { value, field: fieldMeta });
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return col;
|
|
230
|
-
});
|
|
231
|
-
}, [columns, objectSchema, objectName, api, resolveFieldLabel]);
|
|
232
|
-
|
|
233
|
-
const viewSchema = React.useMemo(() => {
|
|
234
|
-
if (schema) return schema;
|
|
235
|
-
|
|
236
|
-
// Auto-generate schema based on type
|
|
237
|
-
switch (type) {
|
|
238
|
-
case 'grid':
|
|
239
|
-
case 'table':
|
|
240
|
-
return {
|
|
241
|
-
type: 'data-table',
|
|
242
|
-
data: paginatedData,
|
|
243
|
-
columns: effectiveColumns,
|
|
244
|
-
pagination: false, // We handle pagination ourselves
|
|
245
|
-
pageSize: effectivePageSize || 10,
|
|
246
|
-
};
|
|
247
|
-
case 'list':
|
|
248
|
-
return {
|
|
249
|
-
type: 'data-list',
|
|
250
|
-
data: paginatedData,
|
|
251
|
-
};
|
|
252
|
-
default:
|
|
253
|
-
return { type: 'div', children: 'No view configured' };
|
|
254
|
-
}
|
|
255
|
-
}, [type, paginatedData, effectiveColumns, schema, effectivePageSize]);
|
|
256
|
-
|
|
257
|
-
const hasRowActions = !!onRowEdit || !!onRowDelete;
|
|
258
|
-
|
|
259
|
-
const headerClassName = collapsible ? 'cursor-pointer select-none' : undefined;
|
|
260
|
-
const handleHeaderClick = collapsible ? () => setCollapsed((c) => !c) : undefined;
|
|
261
|
-
|
|
262
|
-
return (
|
|
263
|
-
<Card className={className}>
|
|
264
|
-
<CardHeader className={headerClassName} onClick={handleHeaderClick}>
|
|
265
|
-
<CardTitle className="flex items-center justify-between">
|
|
266
|
-
<div className="flex items-center gap-2">
|
|
267
|
-
{collapsible && (
|
|
268
|
-
collapsed
|
|
269
|
-
? (<ChevronRight className="h-4 w-4 text-muted-foreground" />)
|
|
270
|
-
: (<ChevronDown className="h-4 w-4 text-muted-foreground" />)
|
|
271
|
-
)}
|
|
272
|
-
<span>{title}</span>
|
|
273
|
-
<Badge variant="secondary" className="text-xs font-normal" aria-label={`${relatedData.length} records`}>
|
|
274
|
-
{relatedData.length}
|
|
275
|
-
</Badge>
|
|
276
|
-
</div>
|
|
277
|
-
<div className="flex items-center gap-1">
|
|
278
|
-
{onNew && (
|
|
279
|
-
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); onNew(); }} className="gap-1 h-7 text-xs">
|
|
280
|
-
<Plus className="h-3.5 w-3.5" />
|
|
281
|
-
{t('detail.new')}
|
|
282
|
-
</Button>
|
|
283
|
-
)}
|
|
284
|
-
{onViewAll && (
|
|
285
|
-
<Button variant="ghost" size="sm" onClick={(e) => { e.stopPropagation(); onViewAll(); }} className="gap-1 h-7 text-xs">
|
|
286
|
-
{t('detail.viewAll')}
|
|
287
|
-
<ExternalLink className="h-3 w-3" />
|
|
288
|
-
</Button>
|
|
289
|
-
)}
|
|
290
|
-
</div>
|
|
291
|
-
</CardTitle>
|
|
292
|
-
</CardHeader>
|
|
293
|
-
{!collapsed && <CardContent>
|
|
294
|
-
{/* Filter bar */}
|
|
295
|
-
{filterable && relatedData.length > 0 && (
|
|
296
|
-
<div className="mb-3">
|
|
297
|
-
<Input
|
|
298
|
-
placeholder={t('detail.filterPlaceholder')}
|
|
299
|
-
value={filterText}
|
|
300
|
-
onChange={(e) => setFilterText(e.target.value)}
|
|
301
|
-
className="h-8 text-sm"
|
|
302
|
-
/>
|
|
303
|
-
</div>
|
|
304
|
-
)}
|
|
305
|
-
|
|
306
|
-
{/* Sortable column headers */}
|
|
307
|
-
{sortable && effectiveColumns && effectiveColumns.length > 0 && relatedData.length > 0 && (
|
|
308
|
-
<div className="flex flex-wrap gap-1 mb-3">
|
|
309
|
-
{effectiveColumns.map((col: any) => {
|
|
310
|
-
const field = col.accessorKey || col.field || col.name;
|
|
311
|
-
if (!field) return null;
|
|
312
|
-
const label = col.header || col.label || field;
|
|
313
|
-
const isActive = sortField === field;
|
|
314
|
-
return (
|
|
315
|
-
<Button
|
|
316
|
-
key={field}
|
|
317
|
-
variant={isActive ? 'secondary' : 'ghost'}
|
|
318
|
-
size="sm"
|
|
319
|
-
className="gap-1 h-7 text-xs"
|
|
320
|
-
onClick={() => handleSort(field)}
|
|
321
|
-
>
|
|
322
|
-
<ArrowUpDown className="h-3 w-3" />
|
|
323
|
-
{label}
|
|
324
|
-
{isActive && (sortDirection === 'asc' ? ' ↑' : ' ↓')}
|
|
325
|
-
</Button>
|
|
326
|
-
);
|
|
327
|
-
})}
|
|
328
|
-
</div>
|
|
329
|
-
)}
|
|
330
|
-
|
|
331
|
-
{loading ? (
|
|
332
|
-
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
|
333
|
-
{t('detail.loading')}
|
|
334
|
-
</div>
|
|
335
|
-
) : relatedData.length === 0 ? (
|
|
336
|
-
<div className="flex items-center justify-center py-8 text-muted-foreground text-sm">
|
|
337
|
-
{t('detail.noRelatedRecords')}
|
|
338
|
-
</div>
|
|
339
|
-
) : (
|
|
340
|
-
<>
|
|
341
|
-
<SchemaRenderer schema={viewSchema} />
|
|
342
|
-
|
|
343
|
-
{/* Row-level actions (rendered as a simple action list below data) */}
|
|
344
|
-
{hasRowActions && paginatedData.length > 0 && (
|
|
345
|
-
<div className="mt-2 space-y-1" data-testid="row-actions">
|
|
346
|
-
{paginatedData.map((row, idx) => (
|
|
347
|
-
<div key={row.id || idx} className="flex items-center justify-between px-2 py-1 text-xs border-b last:border-b-0">
|
|
348
|
-
<span className="truncate text-muted-foreground">
|
|
349
|
-
{row.name || row.title || row.id || `Row ${idx + 1}`}
|
|
350
|
-
</span>
|
|
351
|
-
<div className="flex items-center gap-1">
|
|
352
|
-
{onRowEdit && (
|
|
353
|
-
<Button
|
|
354
|
-
variant="ghost"
|
|
355
|
-
size="sm"
|
|
356
|
-
className="h-6 text-xs gap-1 px-2"
|
|
357
|
-
onClick={() => onRowEdit(row)}
|
|
358
|
-
>
|
|
359
|
-
<Edit className="h-3 w-3" />
|
|
360
|
-
{t('detail.editRow')}
|
|
361
|
-
</Button>
|
|
362
|
-
)}
|
|
363
|
-
{onRowDelete && (
|
|
364
|
-
<Button
|
|
365
|
-
variant="ghost"
|
|
366
|
-
size="sm"
|
|
367
|
-
className="h-6 text-xs gap-1 px-2 text-destructive hover:text-destructive"
|
|
368
|
-
onClick={() => handleDeleteRow(row)}
|
|
369
|
-
>
|
|
370
|
-
<Trash2 className="h-3 w-3" />
|
|
371
|
-
{t('detail.deleteRow')}
|
|
372
|
-
</Button>
|
|
373
|
-
)}
|
|
374
|
-
</div>
|
|
375
|
-
</div>
|
|
376
|
-
))}
|
|
377
|
-
</div>
|
|
378
|
-
)}
|
|
379
|
-
</>
|
|
380
|
-
)}
|
|
381
|
-
|
|
382
|
-
{/* Pagination controls */}
|
|
383
|
-
{effectivePageSize > 0 && sortedData.length > effectivePageSize && (
|
|
384
|
-
<div className="flex items-center justify-between mt-3 pt-3 border-t">
|
|
385
|
-
<Button
|
|
386
|
-
variant="outline"
|
|
387
|
-
size="sm"
|
|
388
|
-
className="h-7 text-xs gap-1"
|
|
389
|
-
disabled={currentPage === 0}
|
|
390
|
-
onClick={() => setCurrentPage((p) => Math.max(0, p - 1))}
|
|
391
|
-
>
|
|
392
|
-
<ChevronLeft className="h-3 w-3" />
|
|
393
|
-
{t('detail.previousPage')}
|
|
394
|
-
</Button>
|
|
395
|
-
<span className="text-xs text-muted-foreground">
|
|
396
|
-
{t('detail.pageOf', { current: currentPage + 1, total: totalPages })}
|
|
397
|
-
</span>
|
|
398
|
-
<Button
|
|
399
|
-
variant="outline"
|
|
400
|
-
size="sm"
|
|
401
|
-
className="h-7 text-xs gap-1"
|
|
402
|
-
disabled={currentPage >= totalPages - 1}
|
|
403
|
-
onClick={() => setCurrentPage((p) => Math.min(totalPages - 1, p + 1))}
|
|
404
|
-
>
|
|
405
|
-
{t('detail.nextPage')}
|
|
406
|
-
<ChevronRight className="h-3 w-3" />
|
|
407
|
-
</Button>
|
|
408
|
-
</div>
|
|
409
|
-
)}
|
|
410
|
-
</CardContent>}
|
|
411
|
-
</Card>
|
|
412
|
-
);
|
|
413
|
-
};
|
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as React from 'react';
|
|
10
|
-
import { cn, Card, CardHeader, CardTitle, CardContent } from '@object-ui/components';
|
|
11
|
-
import { Network } from 'lucide-react';
|
|
12
|
-
|
|
13
|
-
export interface GraphNode {
|
|
14
|
-
id: string;
|
|
15
|
-
label: string;
|
|
16
|
-
type?: string;
|
|
17
|
-
relatedRecords?: GraphNode[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface RelationshipGraphProps {
|
|
21
|
-
record: GraphNode;
|
|
22
|
-
relatedRecords: GraphNode[];
|
|
23
|
-
levels?: number;
|
|
24
|
-
onNodeClick?: (nodeId: string) => void;
|
|
25
|
-
className?: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface LayoutNode {
|
|
29
|
-
id: string;
|
|
30
|
-
label: string;
|
|
31
|
-
type?: string;
|
|
32
|
-
x: number;
|
|
33
|
-
y: number;
|
|
34
|
-
level: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface LayoutEdge {
|
|
38
|
-
fromId: string;
|
|
39
|
-
toId: string;
|
|
40
|
-
fromX: number;
|
|
41
|
-
fromY: number;
|
|
42
|
-
toX: number;
|
|
43
|
-
toY: number;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const NODE_RADIUS = 28;
|
|
47
|
-
const LEVEL_COLORS = [
|
|
48
|
-
'fill-primary stroke-primary',
|
|
49
|
-
'fill-blue-500 stroke-blue-500',
|
|
50
|
-
'fill-emerald-500 stroke-emerald-500',
|
|
51
|
-
'fill-amber-500 stroke-amber-500',
|
|
52
|
-
];
|
|
53
|
-
const LEVEL_TEXT_COLORS = [
|
|
54
|
-
'fill-primary-foreground',
|
|
55
|
-
'fill-white',
|
|
56
|
-
'fill-white',
|
|
57
|
-
'fill-white',
|
|
58
|
-
];
|
|
59
|
-
|
|
60
|
-
/** Compute layout positions for nodes in concentric rings. */
|
|
61
|
-
function computeLayout(
|
|
62
|
-
center: GraphNode,
|
|
63
|
-
relatedRecords: GraphNode[],
|
|
64
|
-
levels: number,
|
|
65
|
-
width: number,
|
|
66
|
-
height: number,
|
|
67
|
-
): { nodes: LayoutNode[]; edges: LayoutEdge[] } {
|
|
68
|
-
const nodes: LayoutNode[] = [];
|
|
69
|
-
const edges: LayoutEdge[] = [];
|
|
70
|
-
const seen = new Set<string>();
|
|
71
|
-
|
|
72
|
-
const cx = width / 2;
|
|
73
|
-
const cy = height / 2;
|
|
74
|
-
|
|
75
|
-
// Center node
|
|
76
|
-
nodes.push({ id: center.id, label: center.label, type: center.type, x: cx, y: cy, level: 0 });
|
|
77
|
-
seen.add(center.id);
|
|
78
|
-
|
|
79
|
-
// Level 1: direct relations
|
|
80
|
-
const ringRadius1 = Math.min(width, height) * 0.32;
|
|
81
|
-
const level1Nodes = relatedRecords.filter((r) => !seen.has(r.id));
|
|
82
|
-
|
|
83
|
-
level1Nodes.forEach((node, i) => {
|
|
84
|
-
const angle = (2 * Math.PI * i) / level1Nodes.length - Math.PI / 2;
|
|
85
|
-
const x = cx + ringRadius1 * Math.cos(angle);
|
|
86
|
-
const y = cy + ringRadius1 * Math.sin(angle);
|
|
87
|
-
nodes.push({ id: node.id, label: node.label, type: node.type, x, y, level: 1 });
|
|
88
|
-
edges.push({ fromId: center.id, toId: node.id, fromX: cx, fromY: cy, toX: x, toY: y });
|
|
89
|
-
seen.add(node.id);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
// Level 2+: related records of related records
|
|
93
|
-
if (levels >= 2) {
|
|
94
|
-
const ringRadius2 = Math.min(width, height) * 0.46;
|
|
95
|
-
const level2Nodes: { node: GraphNode; parentX: number; parentY: number; parentId: string }[] = [];
|
|
96
|
-
|
|
97
|
-
level1Nodes.forEach((parentNode) => {
|
|
98
|
-
const parentLayoutNode = nodes.find((n) => n.id === parentNode.id);
|
|
99
|
-
if (!parentLayoutNode) return;
|
|
100
|
-
const children = (parentNode.relatedRecords || []).filter((r) => !seen.has(r.id));
|
|
101
|
-
children.forEach((child) => {
|
|
102
|
-
level2Nodes.push({
|
|
103
|
-
node: child,
|
|
104
|
-
parentX: parentLayoutNode.x,
|
|
105
|
-
parentY: parentLayoutNode.y,
|
|
106
|
-
parentId: parentNode.id,
|
|
107
|
-
});
|
|
108
|
-
seen.add(child.id);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
level2Nodes.forEach((item, i) => {
|
|
113
|
-
const angle = (2 * Math.PI * i) / Math.max(level2Nodes.length, 1) - Math.PI / 2;
|
|
114
|
-
const x = cx + ringRadius2 * Math.cos(angle);
|
|
115
|
-
const y = cy + ringRadius2 * Math.sin(angle);
|
|
116
|
-
nodes.push({
|
|
117
|
-
id: item.node.id,
|
|
118
|
-
label: item.node.label,
|
|
119
|
-
type: item.node.type,
|
|
120
|
-
x,
|
|
121
|
-
y,
|
|
122
|
-
level: 2,
|
|
123
|
-
});
|
|
124
|
-
edges.push({
|
|
125
|
-
fromId: item.parentId,
|
|
126
|
-
toId: item.node.id,
|
|
127
|
-
fromX: item.parentX,
|
|
128
|
-
fromY: item.parentY,
|
|
129
|
-
toX: x,
|
|
130
|
-
toY: y,
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
return { nodes, edges };
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Truncate label to fit inside a node circle. */
|
|
139
|
-
function truncateLabel(label: string, maxLen: number = 6): string {
|
|
140
|
-
if (label.length <= maxLen) return label;
|
|
141
|
-
return label.slice(0, maxLen - 1) + '…';
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export const RelationshipGraph: React.FC<RelationshipGraphProps> = ({
|
|
145
|
-
record,
|
|
146
|
-
relatedRecords,
|
|
147
|
-
levels = 1,
|
|
148
|
-
onNodeClick,
|
|
149
|
-
className,
|
|
150
|
-
}) => {
|
|
151
|
-
const svgRef = React.useRef<SVGSVGElement>(null);
|
|
152
|
-
const [dimensions, setDimensions] = React.useState({ width: 500, height: 400 });
|
|
153
|
-
const [hoveredNode, setHoveredNode] = React.useState<string | null>(null);
|
|
154
|
-
|
|
155
|
-
// Observe container size
|
|
156
|
-
React.useEffect(() => {
|
|
157
|
-
const svg = svgRef.current;
|
|
158
|
-
if (!svg) return;
|
|
159
|
-
const parent = svg.parentElement;
|
|
160
|
-
if (!parent) return;
|
|
161
|
-
|
|
162
|
-
const observer = new ResizeObserver((entries) => {
|
|
163
|
-
for (const entry of entries) {
|
|
164
|
-
const { width } = entry.contentRect;
|
|
165
|
-
if (width > 0) {
|
|
166
|
-
setDimensions({ width, height: Math.max(300, width * 0.7) });
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
observer.observe(parent);
|
|
171
|
-
return () => observer.disconnect();
|
|
172
|
-
}, []);
|
|
173
|
-
|
|
174
|
-
const { nodes, edges } = React.useMemo(
|
|
175
|
-
() => computeLayout(record, relatedRecords, levels, dimensions.width, dimensions.height),
|
|
176
|
-
[record, relatedRecords, levels, dimensions],
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<Card className={cn('overflow-hidden', className)}>
|
|
181
|
-
<CardHeader className="pb-2">
|
|
182
|
-
<CardTitle className="flex items-center gap-2 text-base">
|
|
183
|
-
<Network className="h-4 w-4" />
|
|
184
|
-
Relationships
|
|
185
|
-
<span className="text-sm font-normal text-muted-foreground">
|
|
186
|
-
({relatedRecords.length} related)
|
|
187
|
-
</span>
|
|
188
|
-
</CardTitle>
|
|
189
|
-
</CardHeader>
|
|
190
|
-
<CardContent className="p-0">
|
|
191
|
-
<svg
|
|
192
|
-
ref={svgRef}
|
|
193
|
-
width="100%"
|
|
194
|
-
height={dimensions.height}
|
|
195
|
-
viewBox={`0 0 ${dimensions.width} ${dimensions.height}`}
|
|
196
|
-
className="select-none"
|
|
197
|
-
>
|
|
198
|
-
{/* Edges */}
|
|
199
|
-
{edges.map((edge, i) => (
|
|
200
|
-
<line
|
|
201
|
-
key={`edge-${i}`}
|
|
202
|
-
x1={edge.fromX}
|
|
203
|
-
y1={edge.fromY}
|
|
204
|
-
x2={edge.toX}
|
|
205
|
-
y2={edge.toY}
|
|
206
|
-
className="stroke-border"
|
|
207
|
-
strokeWidth={1.5}
|
|
208
|
-
strokeOpacity={0.5}
|
|
209
|
-
/>
|
|
210
|
-
))}
|
|
211
|
-
|
|
212
|
-
{/* Nodes */}
|
|
213
|
-
{nodes.map((node) => {
|
|
214
|
-
const isHovered = hoveredNode === node.id;
|
|
215
|
-
const levelColor = LEVEL_COLORS[Math.min(node.level, LEVEL_COLORS.length - 1)];
|
|
216
|
-
const textColor = LEVEL_TEXT_COLORS[Math.min(node.level, LEVEL_TEXT_COLORS.length - 1)];
|
|
217
|
-
const radius = node.level === 0 ? NODE_RADIUS + 6 : NODE_RADIUS;
|
|
218
|
-
|
|
219
|
-
return (
|
|
220
|
-
<g
|
|
221
|
-
key={node.id}
|
|
222
|
-
className={cn('cursor-pointer transition-transform', onNodeClick && 'hover:opacity-80')}
|
|
223
|
-
onClick={() => onNodeClick?.(node.id)}
|
|
224
|
-
onMouseEnter={() => setHoveredNode(node.id)}
|
|
225
|
-
onMouseLeave={() => setHoveredNode(null)}
|
|
226
|
-
>
|
|
227
|
-
<circle
|
|
228
|
-
cx={node.x}
|
|
229
|
-
cy={node.y}
|
|
230
|
-
r={isHovered ? radius + 3 : radius}
|
|
231
|
-
className={levelColor}
|
|
232
|
-
fillOpacity={node.level === 0 ? 1 : 0.85}
|
|
233
|
-
strokeWidth={2}
|
|
234
|
-
strokeOpacity={0.3}
|
|
235
|
-
/>
|
|
236
|
-
<text
|
|
237
|
-
x={node.x}
|
|
238
|
-
y={node.y}
|
|
239
|
-
textAnchor="middle"
|
|
240
|
-
dominantBaseline="central"
|
|
241
|
-
className={cn('text-[10px] font-medium pointer-events-none', textColor)}
|
|
242
|
-
>
|
|
243
|
-
{truncateLabel(node.label)}
|
|
244
|
-
</text>
|
|
245
|
-
{/* Type label below */}
|
|
246
|
-
{node.type && (
|
|
247
|
-
<text
|
|
248
|
-
x={node.x}
|
|
249
|
-
y={node.y + radius + 12}
|
|
250
|
-
textAnchor="middle"
|
|
251
|
-
className="fill-muted-foreground text-[9px] pointer-events-none"
|
|
252
|
-
>
|
|
253
|
-
{node.type}
|
|
254
|
-
</text>
|
|
255
|
-
)}
|
|
256
|
-
{/* Tooltip on hover */}
|
|
257
|
-
{isHovered && (
|
|
258
|
-
<>
|
|
259
|
-
<rect
|
|
260
|
-
x={node.x - 50}
|
|
261
|
-
y={node.y - radius - 28}
|
|
262
|
-
width={100}
|
|
263
|
-
height={20}
|
|
264
|
-
rx={4}
|
|
265
|
-
className="fill-popover stroke-border"
|
|
266
|
-
strokeWidth={1}
|
|
267
|
-
/>
|
|
268
|
-
<text
|
|
269
|
-
x={node.x}
|
|
270
|
-
y={node.y - radius - 16}
|
|
271
|
-
textAnchor="middle"
|
|
272
|
-
dominantBaseline="central"
|
|
273
|
-
className="fill-popover-foreground text-[10px] pointer-events-none"
|
|
274
|
-
>
|
|
275
|
-
{node.label}
|
|
276
|
-
</text>
|
|
277
|
-
</>
|
|
278
|
-
)}
|
|
279
|
-
</g>
|
|
280
|
-
);
|
|
281
|
-
})}
|
|
282
|
-
</svg>
|
|
283
|
-
</CardContent>
|
|
284
|
-
</Card>
|
|
285
|
-
);
|
|
286
|
-
};
|