@object-ui/plugin-detail 3.0.3 → 3.1.0
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/dist/AddressField-C07oUOY6.js +96 -0
- package/dist/AutoNumberField-BxnFqllo.js +8 -0
- package/dist/AvatarField-VThNABzo.js +82 -0
- package/dist/BooleanField-CGHKBzAi.js +37 -0
- package/dist/CodeField-Co_muhRR.js +21 -0
- package/dist/ColorField-DLid_tFz.js +42 -0
- package/dist/CurrencyField-Bw-LqANM.js +43 -0
- package/dist/DateField-BNHAzMB2.js +21 -0
- package/dist/DateTimeField-DjAyn_DQ.js +28 -0
- package/dist/EmailField-xoNcSppb.js +31 -0
- package/dist/FileField-DbNJwjU2.js +133 -0
- package/dist/FormulaField-CJkkwIK8.js +9 -0
- package/dist/GeolocationField-C1AnS6VV.js +123 -0
- package/dist/GridField-DATAHIKf.js +30 -0
- package/dist/ImageField-CEKJpyJp.js +90 -0
- package/dist/LocationField-jDWXjlpx.js +31 -0
- package/dist/LookupField-DQ08L9UQ.js +96 -0
- package/dist/MasterDetailField-Dbk529Ea.js +108 -0
- package/dist/NumberField-BVroN9aV.js +26 -0
- package/dist/ObjectField-CT3l_IHW.js +48 -0
- package/dist/PasswordField-DweVLEE0.js +38 -0
- package/dist/PercentField-ZpWUK97K.js +63 -0
- package/dist/PhoneField-mw-9fqZ_.js +31 -0
- package/dist/QRCodeField-Cbb9ck59.js +77 -0
- package/dist/RatingField-CSqgLS6t.js +47 -0
- package/dist/RichTextField-BpfBOd99.js +38 -0
- package/dist/SelectField-B9Ei-5jl.js +26 -0
- package/dist/SignatureField-DgGpHnQ8.js +85 -0
- package/dist/SliderField-C6HvOHd8.js +30 -0
- package/dist/SummaryField-ugYPYxjP.js +9 -0
- package/dist/TextAreaField-BK3RgzY3.js +39 -0
- package/dist/TextField-Bvzx3atT.js +32 -0
- package/dist/TimeField-Cuz9-Uai.js +21 -0
- package/dist/UrlField-B6XHTV73.js +33 -0
- package/dist/UserField-ooTul2d6.js +49 -0
- package/dist/VectorField-CKg9jdGa.js +25 -0
- package/dist/index-CnlyRfY_.js +59461 -0
- package/dist/index.js +30 -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 +6 -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/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 +4 -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/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 +36 -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 +74 -9
- package/src/DetailView.stories.tsx +76 -0
- package/src/DetailView.tsx +270 -27
- package/src/DiffView.tsx +231 -0
- package/src/FieldChangeItem.tsx +46 -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 +37 -8
- package/src/RelationshipGraph.tsx +286 -0
- package/src/RichTextCommentInput.tsx +348 -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__/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 +66 -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/autoLayout.ts +111 -0
- package/src/index.tsx +46 -0
- package/src/useDetailTranslation.ts +103 -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,
|
|
@@ -32,12 +33,19 @@ import {
|
|
|
32
33
|
History,
|
|
33
34
|
Star,
|
|
34
35
|
StarOff,
|
|
36
|
+
Check,
|
|
37
|
+
ChevronLeft,
|
|
38
|
+
ChevronRight,
|
|
35
39
|
} from 'lucide-react';
|
|
36
40
|
import { DetailSection } from './DetailSection';
|
|
37
41
|
import { DetailTabs } from './DetailTabs';
|
|
38
42
|
import { RelatedList } from './RelatedList';
|
|
43
|
+
import { RecordComments } from './RecordComments';
|
|
44
|
+
import { ActivityTimeline } from './ActivityTimeline';
|
|
39
45
|
import { SchemaRenderer } from '@object-ui/react';
|
|
46
|
+
import { buildExpandFields } from '@object-ui/core';
|
|
40
47
|
import type { DetailViewSchema, DataSource } from '@object-ui/types';
|
|
48
|
+
import { useDetailTranslation } from './useDetailTranslation';
|
|
41
49
|
|
|
42
50
|
export interface DetailViewProps {
|
|
43
51
|
schema: DetailViewSchema;
|
|
@@ -46,6 +54,10 @@ export interface DetailViewProps {
|
|
|
46
54
|
onEdit?: () => void;
|
|
47
55
|
onDelete?: () => void;
|
|
48
56
|
onBack?: () => void;
|
|
57
|
+
/** Enable inline editing toggle for detail fields */
|
|
58
|
+
inlineEdit?: boolean;
|
|
59
|
+
/** Callback when a field value is saved inline */
|
|
60
|
+
onFieldSave?: (field: string, value: any, record: any) => void | Promise<void>;
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
export const DetailView: React.FC<DetailViewProps> = ({
|
|
@@ -55,13 +67,21 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
55
67
|
onEdit,
|
|
56
68
|
onDelete,
|
|
57
69
|
onBack,
|
|
70
|
+
inlineEdit = false,
|
|
71
|
+
onFieldSave,
|
|
58
72
|
}) => {
|
|
59
73
|
const [data, setData] = React.useState<any>(schema.data);
|
|
60
74
|
const [loading, setLoading] = React.useState(!schema.data && !!((schema.api && schema.resourceId) || (dataSource && schema.objectName && schema.resourceId)));
|
|
61
75
|
const [isFavorite, setIsFavorite] = React.useState(false);
|
|
76
|
+
const [isInlineEditing, setIsInlineEditing] = React.useState(false);
|
|
77
|
+
const [editedValues, setEditedValues] = React.useState<Record<string, any>>({});
|
|
78
|
+
const [objectSchema, setObjectSchema] = React.useState<any>(null);
|
|
79
|
+
const { t } = useDetailTranslation();
|
|
62
80
|
|
|
63
|
-
// Fetch data
|
|
81
|
+
// Fetch objectSchema + data with $expand when DataSource is provided
|
|
64
82
|
React.useEffect(() => {
|
|
83
|
+
let isMounted = true;
|
|
84
|
+
|
|
65
85
|
// If inline data provided, use it
|
|
66
86
|
if (schema.data) {
|
|
67
87
|
setData(schema.data);
|
|
@@ -71,26 +91,86 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
71
91
|
|
|
72
92
|
if (dataSource && schema.objectName && schema.resourceId) {
|
|
73
93
|
setLoading(true);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
// Clear stale state when navigating between objects/records
|
|
95
|
+
setObjectSchema(null);
|
|
96
|
+
setData(null);
|
|
97
|
+
const objectName = schema.objectName;
|
|
98
|
+
const resourceId = schema.resourceId;
|
|
99
|
+
const prefix = `${objectName}-`;
|
|
100
|
+
|
|
101
|
+
// Collect all visible fields from sections and top-level fields
|
|
102
|
+
const allFields = [
|
|
103
|
+
...(schema.sections?.flatMap(s => s.fields) || []),
|
|
104
|
+
...(schema.fields || []),
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
// Load objectSchema first, then fetch data with $expand
|
|
108
|
+
const schemaPromise = dataSource.getObjectSchema
|
|
109
|
+
? dataSource.getObjectSchema(objectName).catch(() => null)
|
|
110
|
+
: Promise.resolve(null);
|
|
111
|
+
|
|
112
|
+
schemaPromise.then((resolvedSchema) => {
|
|
113
|
+
if (!isMounted) return;
|
|
114
|
+
setObjectSchema(resolvedSchema);
|
|
115
|
+
|
|
116
|
+
// Compute $expand from objectSchema
|
|
117
|
+
const expandFields = buildExpandFields(resolvedSchema?.fields, allFields);
|
|
118
|
+
const params = expandFields.length > 0 ? { $expand: expandFields } : undefined;
|
|
119
|
+
|
|
120
|
+
const findOnePromise = params
|
|
121
|
+
? dataSource.findOne(objectName, resourceId, params)
|
|
122
|
+
: dataSource.findOne(objectName, resourceId);
|
|
123
|
+
|
|
124
|
+
return findOnePromise.then((result) => {
|
|
125
|
+
if (!isMounted) return;
|
|
126
|
+
if (result) {
|
|
127
|
+
setData(result);
|
|
128
|
+
setLoading(false);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// Fallback: try alternate ID format for backward compatibility
|
|
132
|
+
const resIdStr = String(resourceId);
|
|
133
|
+
const altId = resIdStr.startsWith(prefix)
|
|
134
|
+
? resIdStr.slice(prefix.length) // strip prefix
|
|
135
|
+
: `${prefix}${resIdStr}`; // prepend prefix
|
|
136
|
+
return (params
|
|
137
|
+
? dataSource.findOne(objectName, altId, params)
|
|
138
|
+
: dataSource.findOne(objectName, altId)
|
|
139
|
+
).then((fallbackResult) => {
|
|
140
|
+
if (isMounted) {
|
|
141
|
+
setData(fallbackResult);
|
|
142
|
+
setLoading(false);
|
|
143
|
+
}
|
|
144
|
+
}).catch(() => {
|
|
145
|
+
if (isMounted) {
|
|
146
|
+
setData(null);
|
|
147
|
+
setLoading(false);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
77
151
|
}).catch((err) => {
|
|
78
|
-
|
|
79
|
-
|
|
152
|
+
if (isMounted) {
|
|
153
|
+
console.error('Failed to fetch detail data:', err);
|
|
154
|
+
setLoading(false);
|
|
155
|
+
}
|
|
80
156
|
});
|
|
81
157
|
} else if (schema.api && schema.resourceId) {
|
|
82
158
|
setLoading(true);
|
|
83
159
|
fetch(`${schema.api}/${schema.resourceId}`)
|
|
84
160
|
.then(res => res.json())
|
|
85
161
|
.then(result => {
|
|
86
|
-
|
|
162
|
+
if (isMounted) {
|
|
163
|
+
setData(result?.data || result);
|
|
164
|
+
}
|
|
87
165
|
})
|
|
88
166
|
.catch(err => {
|
|
89
167
|
console.error('Failed to fetch detail data:', err);
|
|
90
168
|
})
|
|
91
|
-
.finally(() => setLoading(false));
|
|
169
|
+
.finally(() => { if (isMounted) setLoading(false); });
|
|
92
170
|
}
|
|
93
|
-
|
|
171
|
+
|
|
172
|
+
return () => { isMounted = false; };
|
|
173
|
+
}, [schema.api, schema.resourceId, schema.objectName, dataSource, schema.sections, schema.fields]);
|
|
94
174
|
|
|
95
175
|
const handleBack = React.useCallback(() => {
|
|
96
176
|
if (onBack) {
|
|
@@ -121,7 +201,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
121
201
|
}, [onEdit, schema]);
|
|
122
202
|
|
|
123
203
|
const handleDelete = React.useCallback(() => {
|
|
124
|
-
const confirmMessage = schema.deleteConfirmation || '
|
|
204
|
+
const confirmMessage = schema.deleteConfirmation || t('detail.deleteConfirmation');
|
|
125
205
|
// Use window.confirm as fallback — the ActionProvider's onConfirm handler
|
|
126
206
|
// will intercept this if wired up via the action system.
|
|
127
207
|
if (window.confirm(confirmMessage)) {
|
|
@@ -137,7 +217,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
137
217
|
// Share functionality - could trigger share dialog or copy link
|
|
138
218
|
if (navigator.share && schema.objectName && schema.resourceId) {
|
|
139
219
|
navigator.share({
|
|
140
|
-
title: schema.title || '
|
|
220
|
+
title: schema.title || t('detail.details'),
|
|
141
221
|
text: `${schema.objectName} #${schema.resourceId}`,
|
|
142
222
|
url: window.location.href,
|
|
143
223
|
}).catch((err) => console.log('Share failed:', err));
|
|
@@ -168,6 +248,46 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
168
248
|
setIsFavorite(!isFavorite);
|
|
169
249
|
}, [isFavorite]);
|
|
170
250
|
|
|
251
|
+
const handleInlineEditToggle = React.useCallback(() => {
|
|
252
|
+
if (isInlineEditing) {
|
|
253
|
+
// Save changes
|
|
254
|
+
const changes = Object.entries(editedValues);
|
|
255
|
+
if (changes.length > 0) {
|
|
256
|
+
const updatedData = { ...data, ...editedValues };
|
|
257
|
+
setData(updatedData);
|
|
258
|
+
changes.forEach(([field, value]) => {
|
|
259
|
+
onFieldSave?.(field, value, updatedData);
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
setEditedValues({});
|
|
263
|
+
}
|
|
264
|
+
setIsInlineEditing(!isInlineEditing);
|
|
265
|
+
}, [isInlineEditing, editedValues, data, onFieldSave]);
|
|
266
|
+
|
|
267
|
+
const handleInlineFieldChange = React.useCallback((field: string, value: any) => {
|
|
268
|
+
setEditedValues(prev => ({ ...prev, [field]: value }));
|
|
269
|
+
}, []);
|
|
270
|
+
|
|
271
|
+
// Keyboard shortcuts for prev/next record navigation (← / →)
|
|
272
|
+
React.useEffect(() => {
|
|
273
|
+
if (!schema.recordNavigation) return;
|
|
274
|
+
const nav = schema.recordNavigation;
|
|
275
|
+
const handler = (e: KeyboardEvent) => {
|
|
276
|
+
// Skip when focus is inside an input, textarea, or contenteditable
|
|
277
|
+
const tag = (e.target as HTMLElement)?.tagName;
|
|
278
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || (e.target as HTMLElement)?.isContentEditable) return;
|
|
279
|
+
if (e.key === 'ArrowLeft' && nav.currentIndex > 0) {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
nav.onNavigate(nav.recordIds[nav.currentIndex - 1]);
|
|
282
|
+
} else if (e.key === 'ArrowRight' && nav.currentIndex < nav.recordIds.length - 1) {
|
|
283
|
+
e.preventDefault();
|
|
284
|
+
nav.onNavigate(nav.recordIds[nav.currentIndex + 1]);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
document.addEventListener('keydown', handler);
|
|
288
|
+
return () => document.removeEventListener('keydown', handler);
|
|
289
|
+
}, [schema.recordNavigation]);
|
|
290
|
+
|
|
171
291
|
if (loading || schema.loading) {
|
|
172
292
|
return (
|
|
173
293
|
<div className={cn('space-y-4', className)}>
|
|
@@ -178,6 +298,23 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
178
298
|
);
|
|
179
299
|
}
|
|
180
300
|
|
|
301
|
+
if (!data && !schema.data) {
|
|
302
|
+
return (
|
|
303
|
+
<div className={cn('flex flex-col items-center justify-center py-16 text-center', className)}>
|
|
304
|
+
<p className="text-lg font-semibold">{t('detail.recordNotFound')}</p>
|
|
305
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
306
|
+
{t('detail.recordNotFoundDescription')}
|
|
307
|
+
</p>
|
|
308
|
+
{(schema.showBack ?? true) && (
|
|
309
|
+
<Button variant="outline" size="sm" onClick={handleBack} className="mt-4 gap-2">
|
|
310
|
+
<ArrowLeft className="h-4 w-4" />
|
|
311
|
+
{t('detail.goBack')}
|
|
312
|
+
</Button>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
181
318
|
return (
|
|
182
319
|
<TooltipProvider>
|
|
183
320
|
<div className={cn('space-y-6', className)}>
|
|
@@ -191,12 +328,23 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
191
328
|
<ArrowLeft className="h-4 w-4" />
|
|
192
329
|
</Button>
|
|
193
330
|
</TooltipTrigger>
|
|
194
|
-
<TooltipContent>
|
|
331
|
+
<TooltipContent>{t('detail.back')}</TooltipContent>
|
|
195
332
|
</Tooltip>
|
|
196
333
|
)}
|
|
197
334
|
<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">
|
|
335
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
336
|
+
<h1 className="text-xl sm:text-2xl font-bold truncate">
|
|
337
|
+
{(schema.primaryField && data?.[schema.primaryField]) || schema.title || t('detail.details')}
|
|
338
|
+
</h1>
|
|
339
|
+
{schema.summaryFields?.map((fieldName) => {
|
|
340
|
+
const val = data?.[fieldName];
|
|
341
|
+
if (val === null || val === undefined || val === '') return null;
|
|
342
|
+
return (
|
|
343
|
+
<Badge key={fieldName} variant="secondary" className="text-xs" aria-label={`${fieldName}: ${val}`}>
|
|
344
|
+
{String(val)}
|
|
345
|
+
</Badge>
|
|
346
|
+
);
|
|
347
|
+
})}
|
|
200
348
|
<Tooltip>
|
|
201
349
|
<TooltipTrigger asChild>
|
|
202
350
|
<Button
|
|
@@ -213,7 +361,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
213
361
|
</Button>
|
|
214
362
|
</TooltipTrigger>
|
|
215
363
|
<TooltipContent>
|
|
216
|
-
{isFavorite ? '
|
|
364
|
+
{isFavorite ? t('detail.removeFromFavorites') : t('detail.addToFavorites')}
|
|
217
365
|
</TooltipContent>
|
|
218
366
|
</Tooltip>
|
|
219
367
|
</div>
|
|
@@ -228,10 +376,86 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
228
376
|
</div>
|
|
229
377
|
|
|
230
378
|
<div className="flex flex-wrap items-center gap-1.5 shrink-0 w-full sm:w-auto">
|
|
379
|
+
{/* Prev/Next Record Navigation */}
|
|
380
|
+
{schema.recordNavigation && (
|
|
381
|
+
<div className="flex items-center gap-1 mr-2">
|
|
382
|
+
<Tooltip>
|
|
383
|
+
<TooltipTrigger asChild>
|
|
384
|
+
<Button
|
|
385
|
+
variant="outline"
|
|
386
|
+
size="icon"
|
|
387
|
+
className="h-8 w-8"
|
|
388
|
+
disabled={schema.recordNavigation.currentIndex <= 0}
|
|
389
|
+
onClick={() => {
|
|
390
|
+
const nav = schema.recordNavigation!;
|
|
391
|
+
if (nav.currentIndex > 0) {
|
|
392
|
+
nav.onNavigate(nav.recordIds[nav.currentIndex - 1]);
|
|
393
|
+
}
|
|
394
|
+
}}
|
|
395
|
+
>
|
|
396
|
+
<ChevronLeft className="h-4 w-4" />
|
|
397
|
+
</Button>
|
|
398
|
+
</TooltipTrigger>
|
|
399
|
+
<TooltipContent>{t('detail.previousRecord')}</TooltipContent>
|
|
400
|
+
</Tooltip>
|
|
401
|
+
<span className="text-xs text-muted-foreground whitespace-nowrap px-1">
|
|
402
|
+
{t('detail.recordOf', { current: schema.recordNavigation.currentIndex + 1, total: schema.recordNavigation.recordIds.length })}
|
|
403
|
+
</span>
|
|
404
|
+
<Tooltip>
|
|
405
|
+
<TooltipTrigger asChild>
|
|
406
|
+
<Button
|
|
407
|
+
variant="outline"
|
|
408
|
+
size="icon"
|
|
409
|
+
className="h-8 w-8"
|
|
410
|
+
disabled={schema.recordNavigation.currentIndex >= schema.recordNavigation.recordIds.length - 1}
|
|
411
|
+
onClick={() => {
|
|
412
|
+
const nav = schema.recordNavigation!;
|
|
413
|
+
if (nav.currentIndex < nav.recordIds.length - 1) {
|
|
414
|
+
nav.onNavigate(nav.recordIds[nav.currentIndex + 1]);
|
|
415
|
+
}
|
|
416
|
+
}}
|
|
417
|
+
>
|
|
418
|
+
<ChevronRight className="h-4 w-4" />
|
|
419
|
+
</Button>
|
|
420
|
+
</TooltipTrigger>
|
|
421
|
+
<TooltipContent>{t('detail.nextRecord')}</TooltipContent>
|
|
422
|
+
</Tooltip>
|
|
423
|
+
</div>
|
|
424
|
+
)}
|
|
425
|
+
|
|
231
426
|
{schema.actions?.map((action, index) => (
|
|
232
427
|
<SchemaRenderer key={index} schema={action} data={data} />
|
|
233
428
|
))}
|
|
234
429
|
|
|
430
|
+
{/* Inline Edit Toggle */}
|
|
431
|
+
{inlineEdit && (
|
|
432
|
+
<Tooltip>
|
|
433
|
+
<TooltipTrigger asChild>
|
|
434
|
+
<Button
|
|
435
|
+
variant={isInlineEditing ? 'default' : 'outline'}
|
|
436
|
+
size="sm"
|
|
437
|
+
onClick={handleInlineEditToggle}
|
|
438
|
+
className="gap-2"
|
|
439
|
+
>
|
|
440
|
+
{isInlineEditing ? (
|
|
441
|
+
<>
|
|
442
|
+
<Check className="h-4 w-4" />
|
|
443
|
+
<span className="hidden sm:inline">{t('detail.save')}</span>
|
|
444
|
+
</>
|
|
445
|
+
) : (
|
|
446
|
+
<>
|
|
447
|
+
<Edit className="h-4 w-4" />
|
|
448
|
+
<span className="hidden sm:inline">{t('detail.editInline')}</span>
|
|
449
|
+
</>
|
|
450
|
+
)}
|
|
451
|
+
</Button>
|
|
452
|
+
</TooltipTrigger>
|
|
453
|
+
<TooltipContent>
|
|
454
|
+
{isInlineEditing ? t('detail.saveChanges') : t('detail.editFieldsInline')}
|
|
455
|
+
</TooltipContent>
|
|
456
|
+
</Tooltip>
|
|
457
|
+
)}
|
|
458
|
+
|
|
235
459
|
{/* Share Button */}
|
|
236
460
|
<Tooltip>
|
|
237
461
|
<TooltipTrigger asChild>
|
|
@@ -239,7 +463,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
239
463
|
<Share2 className="h-4 w-4" />
|
|
240
464
|
</Button>
|
|
241
465
|
</TooltipTrigger>
|
|
242
|
-
<TooltipContent>
|
|
466
|
+
<TooltipContent>{t('detail.share')}</TooltipContent>
|
|
243
467
|
</Tooltip>
|
|
244
468
|
|
|
245
469
|
{/* Edit Button */}
|
|
@@ -248,10 +472,10 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
248
472
|
<TooltipTrigger asChild>
|
|
249
473
|
<Button variant="default" onClick={handleEdit} className="gap-2">
|
|
250
474
|
<Edit className="h-4 w-4" />
|
|
251
|
-
<span className="hidden sm:inline">
|
|
475
|
+
<span className="hidden sm:inline">{t('detail.edit')}</span>
|
|
252
476
|
</Button>
|
|
253
477
|
</TooltipTrigger>
|
|
254
|
-
<TooltipContent>
|
|
478
|
+
<TooltipContent>{t('detail.editRecord')}</TooltipContent>
|
|
255
479
|
</Tooltip>
|
|
256
480
|
)}
|
|
257
481
|
|
|
@@ -265,20 +489,20 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
265
489
|
</Button>
|
|
266
490
|
</DropdownMenuTrigger>
|
|
267
491
|
</TooltipTrigger>
|
|
268
|
-
<TooltipContent>
|
|
492
|
+
<TooltipContent>{t('detail.moreActions')}</TooltipContent>
|
|
269
493
|
</Tooltip>
|
|
270
494
|
<DropdownMenuContent align="end" className="w-[calc(100vw-2rem)] sm:w-48 max-h-[60vh] overflow-y-auto">
|
|
271
495
|
<DropdownMenuItem onClick={handleDuplicate}>
|
|
272
496
|
<Copy className="h-4 w-4 mr-2" />
|
|
273
|
-
|
|
497
|
+
{t('detail.duplicate')}
|
|
274
498
|
</DropdownMenuItem>
|
|
275
499
|
<DropdownMenuItem onClick={handleExport}>
|
|
276
500
|
<Download className="h-4 w-4 mr-2" />
|
|
277
|
-
|
|
501
|
+
{t('detail.export')}
|
|
278
502
|
</DropdownMenuItem>
|
|
279
503
|
<DropdownMenuItem onClick={handleViewHistory}>
|
|
280
504
|
<History className="h-4 w-4 mr-2" />
|
|
281
|
-
|
|
505
|
+
{t('detail.viewHistory')}
|
|
282
506
|
</DropdownMenuItem>
|
|
283
507
|
{schema.showDelete && (
|
|
284
508
|
<>
|
|
@@ -288,7 +512,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
288
512
|
className="text-destructive focus:text-destructive"
|
|
289
513
|
>
|
|
290
514
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
291
|
-
|
|
515
|
+
{t('detail.delete')}
|
|
292
516
|
</DropdownMenuItem>
|
|
293
517
|
</>
|
|
294
518
|
)}
|
|
@@ -311,7 +535,10 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
311
535
|
<DetailSection
|
|
312
536
|
key={index}
|
|
313
537
|
section={section}
|
|
314
|
-
data={data}
|
|
538
|
+
data={{ ...data, ...editedValues }}
|
|
539
|
+
objectSchema={objectSchema}
|
|
540
|
+
isEditing={isInlineEditing}
|
|
541
|
+
onFieldChange={handleInlineFieldChange}
|
|
315
542
|
/>
|
|
316
543
|
))}
|
|
317
544
|
</div>
|
|
@@ -322,9 +549,12 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
322
549
|
<DetailSection
|
|
323
550
|
section={{
|
|
324
551
|
fields: schema.fields,
|
|
325
|
-
columns: schema.columns
|
|
552
|
+
columns: schema.columns,
|
|
326
553
|
}}
|
|
327
|
-
data={data}
|
|
554
|
+
data={{ ...data, ...editedValues }}
|
|
555
|
+
objectSchema={objectSchema}
|
|
556
|
+
isEditing={isInlineEditing}
|
|
557
|
+
onFieldChange={handleInlineFieldChange}
|
|
328
558
|
/>
|
|
329
559
|
)}
|
|
330
560
|
|
|
@@ -336,7 +566,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
336
566
|
{/* Related Lists */}
|
|
337
567
|
{schema.related && schema.related.length > 0 && (
|
|
338
568
|
<div className="space-y-4">
|
|
339
|
-
<h2 className="text-xl font-semibold">
|
|
569
|
+
<h2 className="text-xl font-semibold">{t('detail.related')}</h2>
|
|
340
570
|
{schema.related.map((related, index) => (
|
|
341
571
|
<RelatedList
|
|
342
572
|
key={index}
|
|
@@ -351,6 +581,19 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
351
581
|
</div>
|
|
352
582
|
)}
|
|
353
583
|
|
|
584
|
+
{/* Comments */}
|
|
585
|
+
{schema.comments && (
|
|
586
|
+
<RecordComments
|
|
587
|
+
comments={schema.comments}
|
|
588
|
+
onAddComment={schema.onAddComment}
|
|
589
|
+
/>
|
|
590
|
+
)}
|
|
591
|
+
|
|
592
|
+
{/* Activity Timeline */}
|
|
593
|
+
{schema.activities && schema.activities.length > 0 && (
|
|
594
|
+
<ActivityTimeline activities={schema.activities} />
|
|
595
|
+
)}
|
|
596
|
+
|
|
354
597
|
{/* Custom Footer */}
|
|
355
598
|
{schema.footer && (
|
|
356
599
|
<div>
|