@object-ui/plugin-detail 3.0.3 → 3.1.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/.turbo/turbo-build.log +45 -8
- package/CHANGELOG.md +11 -0
- package/dist/AddressField-B1iVr404.js +96 -0
- package/dist/AutoNumberField-BxnFqllo.js +8 -0
- package/dist/AvatarField-Duw4xOLZ.js +82 -0
- package/dist/BooleanField-CZ4axVeq.js +37 -0
- package/dist/CodeField-BSz-mk2v.js +21 -0
- package/dist/ColorField-B522ad8m.js +42 -0
- package/dist/CurrencyField-Cwr3_pow.js +43 -0
- package/dist/DateField-DCo6dxud.js +21 -0
- package/dist/DateTimeField-BWfBuANO.js +28 -0
- package/dist/EmailField-CpwbdVCU.js +31 -0
- package/dist/FileField-DVAUAJ8e.js +133 -0
- package/dist/FormulaField-CJkkwIK8.js +9 -0
- package/dist/GeolocationField-DNCKitgo.js +123 -0
- package/dist/GridField-DSblZNfp.js +30 -0
- package/dist/ImageField-DBAlnMon.js +90 -0
- package/dist/LocationField-DsHsXA6R.js +31 -0
- package/dist/LookupField-CsT0QQz2.js +96 -0
- package/dist/MasterDetailField-Db8b7Gqs.js +108 -0
- package/dist/NumberField-0IGp7lcA.js +26 -0
- package/dist/ObjectField-BLApgJtS.js +48 -0
- package/dist/PasswordField-pHKyNlmo.js +38 -0
- package/dist/PercentField-CwgKmlIb.js +63 -0
- package/dist/PhoneField-lKtbYOdN.js +31 -0
- package/dist/QRCodeField-BTTasT3w.js +77 -0
- package/dist/RatingField-De2X-l44.js +47 -0
- package/dist/RichTextField-B5QnvUOr.js +38 -0
- package/dist/SelectField-C9AZRHWu.js +26 -0
- package/dist/SignatureField-BgcEmYzd.js +85 -0
- package/dist/SliderField-BzrttVOY.js +30 -0
- package/dist/SummaryField-ugYPYxjP.js +9 -0
- package/dist/TextAreaField-DSE_CaU6.js +39 -0
- package/dist/TextField-DFQ4T9PR.js +32 -0
- package/dist/TimeField-F0cfmsps.js +21 -0
- package/dist/UrlField-DLXrFIH-.js +33 -0
- package/dist/UserField-PXMmxJY9.js +49 -0
- package/dist/VectorField-CKg9jdGa.js +25 -0
- package/dist/index-qQ1C-yUR.js +59976 -0
- package/dist/index.js +32 -55026
- package/dist/index.umd.cjs +41 -30
- package/dist/plugin-detail.css +1 -1
- package/dist/src/ActivityTimeline.d.ts +20 -0
- package/dist/src/ActivityTimeline.d.ts.map +1 -0
- package/dist/src/CommentAttachment.d.ts +25 -0
- package/dist/src/CommentAttachment.d.ts.map +1 -0
- package/dist/src/CommentInput.d.ts +24 -0
- package/dist/src/CommentInput.d.ts.map +1 -0
- package/dist/src/DetailSection.d.ts +8 -0
- package/dist/src/DetailSection.d.ts.map +1 -1
- package/dist/src/DetailView.d.ts +4 -0
- package/dist/src/DetailView.d.ts.map +1 -1
- package/dist/src/DetailView.stories.d.ts +8 -0
- package/dist/src/DetailView.stories.d.ts.map +1 -1
- package/dist/src/DiffView.d.ts +24 -0
- package/dist/src/DiffView.d.ts.map +1 -0
- package/dist/src/FieldChangeItem.d.ts +21 -0
- package/dist/src/FieldChangeItem.d.ts.map +1 -0
- package/dist/src/HeaderHighlight.d.ts +18 -0
- package/dist/src/HeaderHighlight.d.ts.map +1 -0
- package/dist/src/InlineCreateRelated.d.ts +32 -0
- package/dist/src/InlineCreateRelated.d.ts.map +1 -0
- package/dist/src/MentionAutocomplete.d.ts +43 -0
- package/dist/src/MentionAutocomplete.d.ts.map +1 -0
- package/dist/src/PointInTimeRestore.d.ts +28 -0
- package/dist/src/PointInTimeRestore.d.ts.map +1 -0
- package/dist/src/ReactionPicker.d.ts +25 -0
- package/dist/src/ReactionPicker.d.ts.map +1 -0
- package/dist/src/RecordActivityTimeline.d.ts +49 -0
- package/dist/src/RecordActivityTimeline.d.ts.map +1 -0
- package/dist/src/RecordChatterPanel.d.ts +48 -0
- package/dist/src/RecordChatterPanel.d.ts.map +1 -0
- package/dist/src/RecordComments.d.ts +20 -0
- package/dist/src/RecordComments.d.ts.map +1 -0
- package/dist/src/RecordNavigationEnhanced.d.ts +18 -0
- package/dist/src/RecordNavigationEnhanced.d.ts.map +1 -0
- package/dist/src/RelatedList.d.ts +20 -0
- package/dist/src/RelatedList.d.ts.map +1 -1
- package/dist/src/RelationshipGraph.d.ts +23 -0
- package/dist/src/RelationshipGraph.d.ts.map +1 -0
- package/dist/src/RichTextCommentInput.d.ts +24 -0
- package/dist/src/RichTextCommentInput.d.ts.map +1 -0
- package/dist/src/SectionGroup.d.ts +21 -0
- package/dist/src/SectionGroup.d.ts.map +1 -0
- package/dist/src/SubscriptionToggle.d.ts +22 -0
- package/dist/src/SubscriptionToggle.d.ts.map +1 -0
- package/dist/src/ThreadedReplies.d.ts +26 -0
- package/dist/src/ThreadedReplies.d.ts.map +1 -0
- package/dist/src/autoLayout.d.ts +34 -0
- package/dist/src/autoLayout.d.ts.map +1 -0
- package/dist/src/index.d.ts +40 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/useDetailTranslation.d.ts +34 -0
- package/dist/src/useDetailTranslation.d.ts.map +1 -0
- package/package.json +8 -7
- package/src/ActivityTimeline.tsx +184 -0
- package/src/CommentAttachment.tsx +192 -0
- package/src/CommentInput.tsx +81 -0
- package/src/DetailSection.tsx +81 -10
- package/src/DetailView.stories.tsx +76 -0
- package/src/DetailView.tsx +519 -66
- package/src/DiffView.tsx +231 -0
- package/src/FieldChangeItem.tsx +46 -0
- package/src/HeaderHighlight.tsx +67 -0
- package/src/InlineCreateRelated.tsx +291 -0
- package/src/MentionAutocomplete.tsx +123 -0
- package/src/PointInTimeRestore.tsx +261 -0
- package/src/ReactionPicker.tsx +106 -0
- package/src/RecordActivityTimeline.tsx +429 -0
- package/src/RecordChatterPanel.tsx +202 -0
- package/src/RecordComments.tsx +215 -0
- package/src/RecordNavigationEnhanced.tsx +211 -0
- package/src/RelatedList.tsx +314 -19
- package/src/RelationshipGraph.tsx +286 -0
- package/src/RichTextCommentInput.tsx +348 -0
- package/src/SectionGroup.tsx +101 -0
- package/src/SubscriptionToggle.tsx +60 -0
- package/src/ThreadedReplies.tsx +161 -0
- package/src/__tests__/ActivityTimeline.test.tsx +119 -0
- package/src/__tests__/ActivityTimelineFiltering.test.tsx +143 -0
- package/src/__tests__/CommentInput.test.tsx +57 -0
- package/src/__tests__/DetailSection.test.tsx +320 -0
- package/src/__tests__/DetailView.test.tsx +415 -1
- package/src/__tests__/FieldChangeItem.test.tsx +119 -0
- package/src/__tests__/HeaderHighlight.test.tsx +68 -0
- package/src/__tests__/MentionAutocomplete.test.tsx +97 -0
- package/src/__tests__/ReactionPicker.test.tsx +113 -0
- package/src/__tests__/RecordActivityTimeline.test.tsx +395 -0
- package/src/__tests__/RecordChatterPanel.test.tsx +227 -0
- package/src/__tests__/RecordComments.test.tsx +96 -0
- package/src/__tests__/RecordCommentsPinSearch.test.tsx +133 -0
- package/src/__tests__/RelatedList.test.tsx +160 -0
- package/src/__tests__/SectionGroup.test.tsx +101 -0
- package/src/__tests__/SubscriptionToggle.test.tsx +84 -0
- package/src/__tests__/ThreadedReplies.test.tsx +212 -0
- package/src/__tests__/autoLayout.test.ts +184 -0
- package/src/__tests__/phase12-features.test.tsx +583 -0
- package/src/__tests__/roadmap-features.test.tsx +478 -0
- package/src/autoLayout.ts +111 -0
- package/src/index.tsx +50 -0
- package/src/useDetailTranslation.ts +114 -0
package/src/DetailView.tsx
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as React from 'react';
|
|
10
10
|
import {
|
|
11
11
|
cn,
|
|
12
|
+
Badge,
|
|
12
13
|
Button,
|
|
13
14
|
Skeleton,
|
|
14
15
|
DropdownMenu,
|
|
@@ -20,6 +21,10 @@ import {
|
|
|
20
21
|
TooltipContent,
|
|
21
22
|
TooltipProvider,
|
|
22
23
|
TooltipTrigger,
|
|
24
|
+
Tabs,
|
|
25
|
+
TabsList,
|
|
26
|
+
TabsTrigger,
|
|
27
|
+
TabsContent,
|
|
23
28
|
} from '@object-ui/components';
|
|
24
29
|
import {
|
|
25
30
|
ArrowLeft,
|
|
@@ -32,12 +37,24 @@ import {
|
|
|
32
37
|
History,
|
|
33
38
|
Star,
|
|
34
39
|
StarOff,
|
|
40
|
+
Check,
|
|
41
|
+
ChevronLeft,
|
|
42
|
+
ChevronRight,
|
|
35
43
|
} from 'lucide-react';
|
|
36
44
|
import { DetailSection } from './DetailSection';
|
|
37
45
|
import { DetailTabs } from './DetailTabs';
|
|
38
46
|
import { RelatedList } from './RelatedList';
|
|
47
|
+
import { SectionGroup } from './SectionGroup';
|
|
48
|
+
import { HeaderHighlight } from './HeaderHighlight';
|
|
49
|
+
import { RecordComments } from './RecordComments';
|
|
50
|
+
import { ActivityTimeline } from './ActivityTimeline';
|
|
39
51
|
import { SchemaRenderer } from '@object-ui/react';
|
|
52
|
+
import { buildExpandFields } from '@object-ui/core';
|
|
40
53
|
import type { DetailViewSchema, DataSource } from '@object-ui/types';
|
|
54
|
+
import { useDetailTranslation } from './useDetailTranslation';
|
|
55
|
+
|
|
56
|
+
/** Default page size for related lists in the detail view */
|
|
57
|
+
const DEFAULT_RELATED_PAGE_SIZE = 5;
|
|
41
58
|
|
|
42
59
|
export interface DetailViewProps {
|
|
43
60
|
schema: DetailViewSchema;
|
|
@@ -46,6 +63,10 @@ export interface DetailViewProps {
|
|
|
46
63
|
onEdit?: () => void;
|
|
47
64
|
onDelete?: () => void;
|
|
48
65
|
onBack?: () => void;
|
|
66
|
+
/** Enable inline editing toggle for detail fields */
|
|
67
|
+
inlineEdit?: boolean;
|
|
68
|
+
/** Callback when a field value is saved inline */
|
|
69
|
+
onFieldSave?: (field: string, value: any, record: any) => void | Promise<void>;
|
|
49
70
|
}
|
|
50
71
|
|
|
51
72
|
export const DetailView: React.FC<DetailViewProps> = ({
|
|
@@ -55,13 +76,21 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
55
76
|
onEdit,
|
|
56
77
|
onDelete,
|
|
57
78
|
onBack,
|
|
79
|
+
inlineEdit = false,
|
|
80
|
+
onFieldSave,
|
|
58
81
|
}) => {
|
|
59
82
|
const [data, setData] = React.useState<any>(schema.data);
|
|
60
83
|
const [loading, setLoading] = React.useState(!schema.data && !!((schema.api && schema.resourceId) || (dataSource && schema.objectName && schema.resourceId)));
|
|
61
84
|
const [isFavorite, setIsFavorite] = React.useState(false);
|
|
85
|
+
const [isInlineEditing, setIsInlineEditing] = React.useState(false);
|
|
86
|
+
const [editedValues, setEditedValues] = React.useState<Record<string, any>>({});
|
|
87
|
+
const [objectSchema, setObjectSchema] = React.useState<any>(null);
|
|
88
|
+
const { t } = useDetailTranslation();
|
|
62
89
|
|
|
63
|
-
// Fetch data
|
|
90
|
+
// Fetch objectSchema + data with $expand when DataSource is provided
|
|
64
91
|
React.useEffect(() => {
|
|
92
|
+
let isMounted = true;
|
|
93
|
+
|
|
65
94
|
// If inline data provided, use it
|
|
66
95
|
if (schema.data) {
|
|
67
96
|
setData(schema.data);
|
|
@@ -71,26 +100,86 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
71
100
|
|
|
72
101
|
if (dataSource && schema.objectName && schema.resourceId) {
|
|
73
102
|
setLoading(true);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
103
|
+
// Clear stale state when navigating between objects/records
|
|
104
|
+
setObjectSchema(null);
|
|
105
|
+
setData(null);
|
|
106
|
+
const objectName = schema.objectName;
|
|
107
|
+
const resourceId = schema.resourceId;
|
|
108
|
+
const prefix = `${objectName}-`;
|
|
109
|
+
|
|
110
|
+
// Collect all visible fields from sections and top-level fields
|
|
111
|
+
const allFields = [
|
|
112
|
+
...(schema.sections?.flatMap(s => s.fields) || []),
|
|
113
|
+
...(schema.fields || []),
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
// Load objectSchema first, then fetch data with $expand
|
|
117
|
+
const schemaPromise = dataSource.getObjectSchema
|
|
118
|
+
? dataSource.getObjectSchema(objectName).catch(() => null)
|
|
119
|
+
: Promise.resolve(null);
|
|
120
|
+
|
|
121
|
+
schemaPromise.then((resolvedSchema) => {
|
|
122
|
+
if (!isMounted) return;
|
|
123
|
+
setObjectSchema(resolvedSchema);
|
|
124
|
+
|
|
125
|
+
// Compute $expand from objectSchema
|
|
126
|
+
const expandFields = buildExpandFields(resolvedSchema?.fields, allFields);
|
|
127
|
+
const params = expandFields.length > 0 ? { $expand: expandFields } : undefined;
|
|
128
|
+
|
|
129
|
+
const findOnePromise = params
|
|
130
|
+
? dataSource.findOne(objectName, resourceId, params)
|
|
131
|
+
: dataSource.findOne(objectName, resourceId);
|
|
132
|
+
|
|
133
|
+
return findOnePromise.then((result) => {
|
|
134
|
+
if (!isMounted) return;
|
|
135
|
+
if (result) {
|
|
136
|
+
setData(result);
|
|
137
|
+
setLoading(false);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Fallback: try alternate ID format for backward compatibility
|
|
141
|
+
const resIdStr = String(resourceId);
|
|
142
|
+
const altId = resIdStr.startsWith(prefix)
|
|
143
|
+
? resIdStr.slice(prefix.length) // strip prefix
|
|
144
|
+
: `${prefix}${resIdStr}`; // prepend prefix
|
|
145
|
+
return (params
|
|
146
|
+
? dataSource.findOne(objectName, altId, params)
|
|
147
|
+
: dataSource.findOne(objectName, altId)
|
|
148
|
+
).then((fallbackResult) => {
|
|
149
|
+
if (isMounted) {
|
|
150
|
+
setData(fallbackResult);
|
|
151
|
+
setLoading(false);
|
|
152
|
+
}
|
|
153
|
+
}).catch(() => {
|
|
154
|
+
if (isMounted) {
|
|
155
|
+
setData(null);
|
|
156
|
+
setLoading(false);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
77
160
|
}).catch((err) => {
|
|
78
|
-
|
|
79
|
-
|
|
161
|
+
if (isMounted) {
|
|
162
|
+
console.error('Failed to fetch detail data:', err);
|
|
163
|
+
setLoading(false);
|
|
164
|
+
}
|
|
80
165
|
});
|
|
81
166
|
} else if (schema.api && schema.resourceId) {
|
|
82
167
|
setLoading(true);
|
|
83
168
|
fetch(`${schema.api}/${schema.resourceId}`)
|
|
84
169
|
.then(res => res.json())
|
|
85
170
|
.then(result => {
|
|
86
|
-
|
|
171
|
+
if (isMounted) {
|
|
172
|
+
setData(result?.data || result);
|
|
173
|
+
}
|
|
87
174
|
})
|
|
88
175
|
.catch(err => {
|
|
89
176
|
console.error('Failed to fetch detail data:', err);
|
|
90
177
|
})
|
|
91
|
-
.finally(() => setLoading(false));
|
|
178
|
+
.finally(() => { if (isMounted) setLoading(false); });
|
|
92
179
|
}
|
|
93
|
-
|
|
180
|
+
|
|
181
|
+
return () => { isMounted = false; };
|
|
182
|
+
}, [schema.api, schema.resourceId, schema.objectName, dataSource, schema.sections, schema.fields]);
|
|
94
183
|
|
|
95
184
|
const handleBack = React.useCallback(() => {
|
|
96
185
|
if (onBack) {
|
|
@@ -121,7 +210,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
121
210
|
}, [onEdit, schema]);
|
|
122
211
|
|
|
123
212
|
const handleDelete = React.useCallback(() => {
|
|
124
|
-
const confirmMessage = schema.deleteConfirmation || '
|
|
213
|
+
const confirmMessage = schema.deleteConfirmation || t('detail.deleteConfirmation');
|
|
125
214
|
// Use window.confirm as fallback — the ActionProvider's onConfirm handler
|
|
126
215
|
// will intercept this if wired up via the action system.
|
|
127
216
|
if (window.confirm(confirmMessage)) {
|
|
@@ -137,7 +226,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
137
226
|
// Share functionality - could trigger share dialog or copy link
|
|
138
227
|
if (navigator.share && schema.objectName && schema.resourceId) {
|
|
139
228
|
navigator.share({
|
|
140
|
-
title: schema.title || '
|
|
229
|
+
title: schema.title || t('detail.details'),
|
|
141
230
|
text: `${schema.objectName} #${schema.resourceId}`,
|
|
142
231
|
url: window.location.href,
|
|
143
232
|
}).catch((err) => console.log('Share failed:', err));
|
|
@@ -168,6 +257,82 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
168
257
|
setIsFavorite(!isFavorite);
|
|
169
258
|
}, [isFavorite]);
|
|
170
259
|
|
|
260
|
+
const handleInlineEditToggle = React.useCallback(() => {
|
|
261
|
+
if (isInlineEditing) {
|
|
262
|
+
// Save changes
|
|
263
|
+
const changes = Object.entries(editedValues);
|
|
264
|
+
if (changes.length > 0) {
|
|
265
|
+
const updatedData = { ...data, ...editedValues };
|
|
266
|
+
setData(updatedData);
|
|
267
|
+
changes.forEach(([field, value]) => {
|
|
268
|
+
onFieldSave?.(field, value, updatedData);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
setEditedValues({});
|
|
272
|
+
}
|
|
273
|
+
setIsInlineEditing(!isInlineEditing);
|
|
274
|
+
}, [isInlineEditing, editedValues, data, onFieldSave]);
|
|
275
|
+
|
|
276
|
+
const handleInlineFieldChange = React.useCallback((field: string, value: any) => {
|
|
277
|
+
setEditedValues(prev => ({ ...prev, [field]: value }));
|
|
278
|
+
}, []);
|
|
279
|
+
|
|
280
|
+
// Keyboard shortcuts for prev/next record navigation (← / →)
|
|
281
|
+
React.useEffect(() => {
|
|
282
|
+
if (!schema.recordNavigation) return;
|
|
283
|
+
const nav = schema.recordNavigation;
|
|
284
|
+
const handler = (e: KeyboardEvent) => {
|
|
285
|
+
// Skip when focus is inside an input, textarea, or contenteditable
|
|
286
|
+
const tag = (e.target as HTMLElement)?.tagName;
|
|
287
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) return;
|
|
288
|
+
if (e.key === 'ArrowLeft' && nav.currentIndex > 0) {
|
|
289
|
+
e.preventDefault();
|
|
290
|
+
nav.onNavigate(nav.recordIds[nav.currentIndex - 1]);
|
|
291
|
+
} else if (e.key === 'ArrowRight' && nav.currentIndex < nav.recordIds.length - 1) {
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
nav.onNavigate(nav.recordIds[nav.currentIndex + 1]);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
document.addEventListener('keydown', handler);
|
|
297
|
+
return () => document.removeEventListener('keydown', handler);
|
|
298
|
+
}, [schema.recordNavigation]);
|
|
299
|
+
|
|
300
|
+
// Auto-discover related lists from objectSchema reference fields
|
|
301
|
+
const discoveredRelated = React.useMemo(() => {
|
|
302
|
+
if (!schema.autoDiscoverRelated || !objectSchema?.fields) return [];
|
|
303
|
+
// Only auto-discover when no explicit related config is provided
|
|
304
|
+
if (schema.related && schema.related.length > 0) return [];
|
|
305
|
+
const refs: Array<{ title: string; type: 'list' | 'grid' | 'table'; objectName: string; referenceField: string }> = [];
|
|
306
|
+
const fields = objectSchema.fields;
|
|
307
|
+
for (const [fieldName, fieldDef] of Object.entries<any>(fields)) {
|
|
308
|
+
const refTarget = fieldDef?.reference_to || fieldDef?.reference;
|
|
309
|
+
if (
|
|
310
|
+
fieldDef &&
|
|
311
|
+
(fieldDef.type === 'lookup' || fieldDef.type === 'master_detail') &&
|
|
312
|
+
refTarget
|
|
313
|
+
) {
|
|
314
|
+
refs.push({
|
|
315
|
+
title: fieldDef.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
|
|
316
|
+
type: 'table',
|
|
317
|
+
objectName: refTarget,
|
|
318
|
+
referenceField: fieldName,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return refs;
|
|
323
|
+
}, [schema.autoDiscoverRelated, schema.related, objectSchema]);
|
|
324
|
+
|
|
325
|
+
// Merge explicit and auto-discovered related lists
|
|
326
|
+
const effectiveRelated: NonNullable<DetailViewSchema['related']> = React.useMemo(() => {
|
|
327
|
+
if (schema.related && schema.related.length > 0) return schema.related;
|
|
328
|
+
return discoveredRelated.map((r) => ({
|
|
329
|
+
title: r.title,
|
|
330
|
+
type: r.type,
|
|
331
|
+
api: r.objectName,
|
|
332
|
+
data: [] as any[],
|
|
333
|
+
}));
|
|
334
|
+
}, [schema.related, discoveredRelated]);
|
|
335
|
+
|
|
171
336
|
if (loading || schema.loading) {
|
|
172
337
|
return (
|
|
173
338
|
<div className={cn('space-y-4', className)}>
|
|
@@ -178,6 +343,23 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
178
343
|
);
|
|
179
344
|
}
|
|
180
345
|
|
|
346
|
+
if (!data && !schema.data) {
|
|
347
|
+
return (
|
|
348
|
+
<div className={cn('flex flex-col items-center justify-center py-16 text-center', className)}>
|
|
349
|
+
<p className="text-lg font-semibold">{t('detail.recordNotFound')}</p>
|
|
350
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
351
|
+
{t('detail.recordNotFoundDescription')}
|
|
352
|
+
</p>
|
|
353
|
+
{(schema.showBack ?? true) && (
|
|
354
|
+
<Button variant="outline" size="sm" onClick={handleBack} className="mt-4 gap-2">
|
|
355
|
+
<ArrowLeft className="h-4 w-4" />
|
|
356
|
+
{t('detail.goBack')}
|
|
357
|
+
</Button>
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
181
363
|
return (
|
|
182
364
|
<TooltipProvider>
|
|
183
365
|
<div className={cn('space-y-6', className)}>
|
|
@@ -191,12 +373,23 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
191
373
|
<ArrowLeft className="h-4 w-4" />
|
|
192
374
|
</Button>
|
|
193
375
|
</TooltipTrigger>
|
|
194
|
-
<TooltipContent>
|
|
376
|
+
<TooltipContent>{t('detail.back')}</TooltipContent>
|
|
195
377
|
</Tooltip>
|
|
196
378
|
)}
|
|
197
379
|
<div className="flex-1 min-w-0">
|
|
198
|
-
<div className="flex items-center gap-2">
|
|
199
|
-
<h1 className="text-xl sm:text-2xl font-bold truncate">
|
|
380
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
381
|
+
<h1 className="text-xl sm:text-2xl font-bold truncate">
|
|
382
|
+
{(schema.primaryField && data?.[schema.primaryField]) || schema.title || t('detail.details')}
|
|
383
|
+
</h1>
|
|
384
|
+
{schema.summaryFields?.map((fieldName) => {
|
|
385
|
+
const val = data?.[fieldName];
|
|
386
|
+
if (val === null || val === undefined || val === '') return null;
|
|
387
|
+
return (
|
|
388
|
+
<Badge key={fieldName} variant="secondary" className="text-xs" aria-label={`${fieldName}: ${val}`}>
|
|
389
|
+
{String(val)}
|
|
390
|
+
</Badge>
|
|
391
|
+
);
|
|
392
|
+
})}
|
|
200
393
|
<Tooltip>
|
|
201
394
|
<TooltipTrigger asChild>
|
|
202
395
|
<Button
|
|
@@ -213,7 +406,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
213
406
|
</Button>
|
|
214
407
|
</TooltipTrigger>
|
|
215
408
|
<TooltipContent>
|
|
216
|
-
{isFavorite ? '
|
|
409
|
+
{isFavorite ? t('detail.removeFromFavorites') : t('detail.addToFavorites')}
|
|
217
410
|
</TooltipContent>
|
|
218
411
|
</Tooltip>
|
|
219
412
|
</div>
|
|
@@ -228,30 +421,106 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
228
421
|
</div>
|
|
229
422
|
|
|
230
423
|
<div className="flex flex-wrap items-center gap-1.5 shrink-0 w-full sm:w-auto">
|
|
424
|
+
{/* Prev/Next Record Navigation */}
|
|
425
|
+
{schema.recordNavigation && (
|
|
426
|
+
<div className="flex items-center gap-1 mr-2">
|
|
427
|
+
<Tooltip>
|
|
428
|
+
<TooltipTrigger asChild>
|
|
429
|
+
<Button
|
|
430
|
+
variant="outline"
|
|
431
|
+
size="icon"
|
|
432
|
+
className="h-8 w-8"
|
|
433
|
+
disabled={schema.recordNavigation.currentIndex <= 0}
|
|
434
|
+
onClick={() => {
|
|
435
|
+
const nav = schema.recordNavigation!;
|
|
436
|
+
if (nav.currentIndex > 0) {
|
|
437
|
+
nav.onNavigate(nav.recordIds[nav.currentIndex - 1]);
|
|
438
|
+
}
|
|
439
|
+
}}
|
|
440
|
+
>
|
|
441
|
+
<ChevronLeft className="h-4 w-4" />
|
|
442
|
+
</Button>
|
|
443
|
+
</TooltipTrigger>
|
|
444
|
+
<TooltipContent>{t('detail.previousRecord')}</TooltipContent>
|
|
445
|
+
</Tooltip>
|
|
446
|
+
<span className="text-xs text-muted-foreground whitespace-nowrap px-1">
|
|
447
|
+
{t('detail.recordOf', { current: schema.recordNavigation.currentIndex + 1, total: schema.recordNavigation.recordIds.length })}
|
|
448
|
+
</span>
|
|
449
|
+
<Tooltip>
|
|
450
|
+
<TooltipTrigger asChild>
|
|
451
|
+
<Button
|
|
452
|
+
variant="outline"
|
|
453
|
+
size="icon"
|
|
454
|
+
className="h-8 w-8"
|
|
455
|
+
disabled={schema.recordNavigation.currentIndex >= schema.recordNavigation.recordIds.length - 1}
|
|
456
|
+
onClick={() => {
|
|
457
|
+
const nav = schema.recordNavigation!;
|
|
458
|
+
if (nav.currentIndex < nav.recordIds.length - 1) {
|
|
459
|
+
nav.onNavigate(nav.recordIds[nav.currentIndex + 1]);
|
|
460
|
+
}
|
|
461
|
+
}}
|
|
462
|
+
>
|
|
463
|
+
<ChevronRight className="h-4 w-4" />
|
|
464
|
+
</Button>
|
|
465
|
+
</TooltipTrigger>
|
|
466
|
+
<TooltipContent>{t('detail.nextRecord')}</TooltipContent>
|
|
467
|
+
</Tooltip>
|
|
468
|
+
</div>
|
|
469
|
+
)}
|
|
470
|
+
|
|
231
471
|
{schema.actions?.map((action, index) => (
|
|
232
472
|
<SchemaRenderer key={index} schema={action} data={data} />
|
|
233
473
|
))}
|
|
234
474
|
|
|
235
|
-
{/*
|
|
475
|
+
{/* Inline Edit Toggle - hidden on mobile, accessible via more menu */}
|
|
476
|
+
{inlineEdit && (
|
|
477
|
+
<Tooltip>
|
|
478
|
+
<TooltipTrigger asChild>
|
|
479
|
+
<Button
|
|
480
|
+
variant={isInlineEditing ? 'default' : 'outline'}
|
|
481
|
+
size="sm"
|
|
482
|
+
onClick={handleInlineEditToggle}
|
|
483
|
+
className="gap-2 hidden sm:inline-flex"
|
|
484
|
+
>
|
|
485
|
+
{isInlineEditing ? (
|
|
486
|
+
<>
|
|
487
|
+
<Check className="h-4 w-4" />
|
|
488
|
+
<span className="hidden sm:inline">{t('detail.save')}</span>
|
|
489
|
+
</>
|
|
490
|
+
) : (
|
|
491
|
+
<>
|
|
492
|
+
<Edit className="h-4 w-4" />
|
|
493
|
+
<span className="hidden sm:inline">{t('detail.editInline')}</span>
|
|
494
|
+
</>
|
|
495
|
+
)}
|
|
496
|
+
</Button>
|
|
497
|
+
</TooltipTrigger>
|
|
498
|
+
<TooltipContent>
|
|
499
|
+
{isInlineEditing ? t('detail.saveChanges') : t('detail.editFieldsInline')}
|
|
500
|
+
</TooltipContent>
|
|
501
|
+
</Tooltip>
|
|
502
|
+
)}
|
|
503
|
+
|
|
504
|
+
{/* Share Button - hidden on mobile, accessible via more menu */}
|
|
236
505
|
<Tooltip>
|
|
237
506
|
<TooltipTrigger asChild>
|
|
238
|
-
<Button variant="outline" size="icon" onClick={handleShare}>
|
|
507
|
+
<Button variant="outline" size="icon" onClick={handleShare} className="hidden sm:inline-flex">
|
|
239
508
|
<Share2 className="h-4 w-4" />
|
|
240
509
|
</Button>
|
|
241
510
|
</TooltipTrigger>
|
|
242
|
-
<TooltipContent>
|
|
511
|
+
<TooltipContent>{t('detail.share')}</TooltipContent>
|
|
243
512
|
</Tooltip>
|
|
244
513
|
|
|
245
|
-
{/* Edit Button */}
|
|
514
|
+
{/* Edit Button - hidden on mobile, accessible via more menu */}
|
|
246
515
|
{schema.showEdit && (
|
|
247
516
|
<Tooltip>
|
|
248
517
|
<TooltipTrigger asChild>
|
|
249
|
-
<Button variant="default" onClick={handleEdit} className="gap-2">
|
|
518
|
+
<Button variant="default" onClick={handleEdit} className="gap-2 hidden sm:inline-flex">
|
|
250
519
|
<Edit className="h-4 w-4" />
|
|
251
|
-
<span className="hidden sm:inline">
|
|
520
|
+
<span className="hidden sm:inline">{t('detail.edit')}</span>
|
|
252
521
|
</Button>
|
|
253
522
|
</TooltipTrigger>
|
|
254
|
-
<TooltipContent>
|
|
523
|
+
<TooltipContent>{t('detail.editRecord')}</TooltipContent>
|
|
255
524
|
</Tooltip>
|
|
256
525
|
)}
|
|
257
526
|
|
|
@@ -265,20 +534,38 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
265
534
|
</Button>
|
|
266
535
|
</DropdownMenuTrigger>
|
|
267
536
|
</TooltipTrigger>
|
|
268
|
-
<TooltipContent>
|
|
537
|
+
<TooltipContent>{t('detail.moreActions')}</TooltipContent>
|
|
269
538
|
</Tooltip>
|
|
270
539
|
<DropdownMenuContent align="end" className="w-[calc(100vw-2rem)] sm:w-48 max-h-[60vh] overflow-y-auto">
|
|
540
|
+
{/* Mobile-only: Share, Edit, Inline Edit */}
|
|
541
|
+
<DropdownMenuItem onClick={handleShare} className="sm:hidden">
|
|
542
|
+
<Share2 className="h-4 w-4 mr-2" />
|
|
543
|
+
{t('detail.share')}
|
|
544
|
+
</DropdownMenuItem>
|
|
545
|
+
{schema.showEdit && (
|
|
546
|
+
<DropdownMenuItem onClick={handleEdit} className="sm:hidden">
|
|
547
|
+
<Edit className="h-4 w-4 mr-2" />
|
|
548
|
+
{t('detail.edit')}
|
|
549
|
+
</DropdownMenuItem>
|
|
550
|
+
)}
|
|
551
|
+
{inlineEdit && (
|
|
552
|
+
<DropdownMenuItem onClick={handleInlineEditToggle} className="sm:hidden">
|
|
553
|
+
<Edit className="h-4 w-4 mr-2" />
|
|
554
|
+
{isInlineEditing ? t('detail.save') : t('detail.editInline')}
|
|
555
|
+
</DropdownMenuItem>
|
|
556
|
+
)}
|
|
557
|
+
<DropdownMenuSeparator className="sm:hidden" />
|
|
271
558
|
<DropdownMenuItem onClick={handleDuplicate}>
|
|
272
559
|
<Copy className="h-4 w-4 mr-2" />
|
|
273
|
-
|
|
560
|
+
{t('detail.duplicate')}
|
|
274
561
|
</DropdownMenuItem>
|
|
275
562
|
<DropdownMenuItem onClick={handleExport}>
|
|
276
563
|
<Download className="h-4 w-4 mr-2" />
|
|
277
|
-
|
|
564
|
+
{t('detail.export')}
|
|
278
565
|
</DropdownMenuItem>
|
|
279
566
|
<DropdownMenuItem onClick={handleViewHistory}>
|
|
280
567
|
<History className="h-4 w-4 mr-2" />
|
|
281
|
-
|
|
568
|
+
{t('detail.viewHistory')}
|
|
282
569
|
</DropdownMenuItem>
|
|
283
570
|
{schema.showDelete && (
|
|
284
571
|
<>
|
|
@@ -288,7 +575,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
288
575
|
className="text-destructive focus:text-destructive"
|
|
289
576
|
>
|
|
290
577
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
291
|
-
|
|
578
|
+
{t('detail.delete')}
|
|
292
579
|
</DropdownMenuItem>
|
|
293
580
|
</>
|
|
294
581
|
)}
|
|
@@ -304,51 +591,217 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
304
591
|
</div>
|
|
305
592
|
)}
|
|
306
593
|
|
|
307
|
-
{/*
|
|
308
|
-
{schema.
|
|
309
|
-
<
|
|
310
|
-
|
|
594
|
+
{/* Header Highlight Area */}
|
|
595
|
+
{schema.highlightFields && schema.highlightFields.length > 0 && (
|
|
596
|
+
<HeaderHighlight fields={schema.highlightFields} data={data} objectName={schema.objectName} />
|
|
597
|
+
)}
|
|
598
|
+
|
|
599
|
+
{/* Auto Tabs mode: wrap sections, related, activity into tabs */}
|
|
600
|
+
{schema.autoTabs && !schema.tabs?.length ? (
|
|
601
|
+
<Tabs defaultValue="details" className="w-full">
|
|
602
|
+
<TabsList className="w-full justify-start border-b rounded-none bg-transparent p-0">
|
|
603
|
+
<TabsTrigger
|
|
604
|
+
value="details"
|
|
605
|
+
className="relative rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
|
|
606
|
+
>
|
|
607
|
+
{t('detail.details')}
|
|
608
|
+
</TabsTrigger>
|
|
609
|
+
{effectiveRelated.length > 0 && (
|
|
610
|
+
<TabsTrigger
|
|
611
|
+
value="related"
|
|
612
|
+
className="relative rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
|
|
613
|
+
>
|
|
614
|
+
<span className="flex items-center gap-1.5">
|
|
615
|
+
{t('detail.related')}
|
|
616
|
+
<Badge variant="secondary" className="text-xs">{effectiveRelated.length}</Badge>
|
|
617
|
+
</span>
|
|
618
|
+
</TabsTrigger>
|
|
619
|
+
)}
|
|
620
|
+
{schema.activities && schema.activities.length > 0 && (
|
|
621
|
+
<TabsTrigger
|
|
622
|
+
value="activity"
|
|
623
|
+
className="relative rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
|
|
624
|
+
>
|
|
625
|
+
<span className="flex items-center gap-1.5">
|
|
626
|
+
{t('detail.activity')}
|
|
627
|
+
<Badge variant="secondary" className="text-xs">{schema.activities.length}</Badge>
|
|
628
|
+
</span>
|
|
629
|
+
</TabsTrigger>
|
|
630
|
+
)}
|
|
631
|
+
</TabsList>
|
|
632
|
+
|
|
633
|
+
{/* Details Tab Content */}
|
|
634
|
+
<TabsContent value="details" className="mt-4">
|
|
635
|
+
<div className="space-y-3 sm:space-y-4">
|
|
636
|
+
{/* Section Groups */}
|
|
637
|
+
{schema.sectionGroups && schema.sectionGroups.length > 0 && (
|
|
638
|
+
schema.sectionGroups.map((group, index) => (
|
|
639
|
+
<SectionGroup
|
|
640
|
+
key={index}
|
|
641
|
+
group={group}
|
|
642
|
+
data={{ ...data, ...editedValues }}
|
|
643
|
+
objectSchema={objectSchema}
|
|
644
|
+
objectName={schema.objectName}
|
|
645
|
+
isEditing={isInlineEditing}
|
|
646
|
+
onFieldChange={handleInlineFieldChange}
|
|
647
|
+
/>
|
|
648
|
+
))
|
|
649
|
+
)}
|
|
650
|
+
{schema.sections && schema.sections.length > 0 && (
|
|
651
|
+
schema.sections.map((section, index) => (
|
|
652
|
+
<DetailSection
|
|
653
|
+
key={index}
|
|
654
|
+
section={section}
|
|
655
|
+
data={{ ...data, ...editedValues }}
|
|
656
|
+
objectSchema={objectSchema}
|
|
657
|
+
objectName={schema.objectName}
|
|
658
|
+
isEditing={isInlineEditing}
|
|
659
|
+
onFieldChange={handleInlineFieldChange}
|
|
660
|
+
/>
|
|
661
|
+
))
|
|
662
|
+
)}
|
|
663
|
+
{schema.fields && schema.fields.length > 0 && !schema.sections?.length && (
|
|
664
|
+
<DetailSection
|
|
665
|
+
section={{
|
|
666
|
+
fields: schema.fields,
|
|
667
|
+
columns: schema.columns,
|
|
668
|
+
}}
|
|
669
|
+
data={{ ...data, ...editedValues }}
|
|
670
|
+
objectSchema={objectSchema}
|
|
671
|
+
objectName={schema.objectName}
|
|
672
|
+
isEditing={isInlineEditing}
|
|
673
|
+
onFieldChange={handleInlineFieldChange}
|
|
674
|
+
/>
|
|
675
|
+
)}
|
|
676
|
+
{/* Comments in details tab */}
|
|
677
|
+
{schema.comments && (
|
|
678
|
+
<RecordComments
|
|
679
|
+
comments={schema.comments}
|
|
680
|
+
onAddComment={schema.onAddComment}
|
|
681
|
+
/>
|
|
682
|
+
)}
|
|
683
|
+
</div>
|
|
684
|
+
</TabsContent>
|
|
685
|
+
|
|
686
|
+
{/* Related Tab Content */}
|
|
687
|
+
{effectiveRelated.length > 0 && (
|
|
688
|
+
<TabsContent value="related" className="mt-4">
|
|
689
|
+
<div className="space-y-4">
|
|
690
|
+
{effectiveRelated.map((related, index) => (
|
|
691
|
+
<RelatedList
|
|
692
|
+
key={index}
|
|
693
|
+
title={related.title}
|
|
694
|
+
type={related.type}
|
|
695
|
+
api={related.api}
|
|
696
|
+
data={related.data}
|
|
697
|
+
columns={related.columns as any}
|
|
698
|
+
dataSource={dataSource}
|
|
699
|
+
objectName={related.api}
|
|
700
|
+
collapsible
|
|
701
|
+
pageSize={DEFAULT_RELATED_PAGE_SIZE}
|
|
702
|
+
/>
|
|
703
|
+
))}
|
|
704
|
+
</div>
|
|
705
|
+
</TabsContent>
|
|
706
|
+
)}
|
|
707
|
+
|
|
708
|
+
{/* Activity Tab Content */}
|
|
709
|
+
{schema.activities && schema.activities.length > 0 && (
|
|
710
|
+
<TabsContent value="activity" className="mt-4">
|
|
711
|
+
<ActivityTimeline activities={schema.activities} />
|
|
712
|
+
</TabsContent>
|
|
713
|
+
)}
|
|
714
|
+
</Tabs>
|
|
715
|
+
) : (
|
|
716
|
+
<>
|
|
717
|
+
{/* Section Groups */}
|
|
718
|
+
{schema.sectionGroups && schema.sectionGroups.length > 0 && (
|
|
719
|
+
<div className="space-y-3 sm:space-y-4">
|
|
720
|
+
{schema.sectionGroups.map((group, index) => (
|
|
721
|
+
<SectionGroup
|
|
722
|
+
key={index}
|
|
723
|
+
group={group}
|
|
724
|
+
data={{ ...data, ...editedValues }}
|
|
725
|
+
objectSchema={objectSchema}
|
|
726
|
+
objectName={schema.objectName}
|
|
727
|
+
isEditing={isInlineEditing}
|
|
728
|
+
onFieldChange={handleInlineFieldChange}
|
|
729
|
+
/>
|
|
730
|
+
))}
|
|
731
|
+
</div>
|
|
732
|
+
)}
|
|
733
|
+
|
|
734
|
+
{/* Sections */}
|
|
735
|
+
{schema.sections && schema.sections.length > 0 && (
|
|
736
|
+
<div className="space-y-3 sm:space-y-4">
|
|
737
|
+
{schema.sections.map((section, index) => (
|
|
738
|
+
<DetailSection
|
|
739
|
+
key={index}
|
|
740
|
+
section={section}
|
|
741
|
+
data={{ ...data, ...editedValues }}
|
|
742
|
+
objectSchema={objectSchema}
|
|
743
|
+
objectName={schema.objectName}
|
|
744
|
+
isEditing={isInlineEditing}
|
|
745
|
+
onFieldChange={handleInlineFieldChange}
|
|
746
|
+
/>
|
|
747
|
+
))}
|
|
748
|
+
</div>
|
|
749
|
+
)}
|
|
750
|
+
|
|
751
|
+
{/* Direct Fields (if no sections) */}
|
|
752
|
+
{schema.fields && schema.fields.length > 0 && !schema.sections?.length && (
|
|
311
753
|
<DetailSection
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
754
|
+
section={{
|
|
755
|
+
fields: schema.fields,
|
|
756
|
+
columns: schema.columns,
|
|
757
|
+
}}
|
|
758
|
+
data={{ ...data, ...editedValues }}
|
|
759
|
+
objectSchema={objectSchema}
|
|
760
|
+
objectName={schema.objectName}
|
|
761
|
+
isEditing={isInlineEditing}
|
|
762
|
+
onFieldChange={handleInlineFieldChange}
|
|
315
763
|
/>
|
|
316
|
-
)
|
|
317
|
-
</div>
|
|
318
|
-
)}
|
|
764
|
+
)}
|
|
319
765
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
fields: schema.fields,
|
|
325
|
-
columns: schema.columns || 2,
|
|
326
|
-
}}
|
|
327
|
-
data={data}
|
|
328
|
-
/>
|
|
329
|
-
)}
|
|
766
|
+
{/* Tabs */}
|
|
767
|
+
{schema.tabs && schema.tabs.length > 0 && (
|
|
768
|
+
<DetailTabs tabs={schema.tabs} data={data} />
|
|
769
|
+
)}
|
|
330
770
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
771
|
+
{/* Related Lists */}
|
|
772
|
+
{effectiveRelated.length > 0 && (
|
|
773
|
+
<div className="space-y-4">
|
|
774
|
+
<h2 className="text-xl font-semibold">{t('detail.related')}</h2>
|
|
775
|
+
{effectiveRelated.map((related, index) => (
|
|
776
|
+
<RelatedList
|
|
777
|
+
key={index}
|
|
778
|
+
title={related.title}
|
|
779
|
+
type={related.type}
|
|
780
|
+
api={related.api}
|
|
781
|
+
data={related.data}
|
|
782
|
+
columns={related.columns as any}
|
|
783
|
+
dataSource={dataSource}
|
|
784
|
+
objectName={related.api}
|
|
785
|
+
collapsible
|
|
786
|
+
pageSize={DEFAULT_RELATED_PAGE_SIZE}
|
|
787
|
+
/>
|
|
788
|
+
))}
|
|
789
|
+
</div>
|
|
790
|
+
)}
|
|
335
791
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
<RelatedList
|
|
342
|
-
key={index}
|
|
343
|
-
title={related.title}
|
|
344
|
-
type={related.type}
|
|
345
|
-
api={related.api}
|
|
346
|
-
data={related.data}
|
|
347
|
-
columns={related.columns as any}
|
|
348
|
-
dataSource={dataSource}
|
|
792
|
+
{/* Comments */}
|
|
793
|
+
{schema.comments && (
|
|
794
|
+
<RecordComments
|
|
795
|
+
comments={schema.comments}
|
|
796
|
+
onAddComment={schema.onAddComment}
|
|
349
797
|
/>
|
|
350
|
-
)
|
|
351
|
-
|
|
798
|
+
)}
|
|
799
|
+
|
|
800
|
+
{/* Activity Timeline */}
|
|
801
|
+
{schema.activities && schema.activities.length > 0 && (
|
|
802
|
+
<ActivityTimeline activities={schema.activities} />
|
|
803
|
+
)}
|
|
804
|
+
</>
|
|
352
805
|
)}
|
|
353
806
|
|
|
354
807
|
{/* Custom Footer */}
|