@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.
Files changed (141) hide show
  1. package/.turbo/turbo-build.log +45 -8
  2. package/CHANGELOG.md +11 -0
  3. package/dist/AddressField-B1iVr404.js +96 -0
  4. package/dist/AutoNumberField-BxnFqllo.js +8 -0
  5. package/dist/AvatarField-Duw4xOLZ.js +82 -0
  6. package/dist/BooleanField-CZ4axVeq.js +37 -0
  7. package/dist/CodeField-BSz-mk2v.js +21 -0
  8. package/dist/ColorField-B522ad8m.js +42 -0
  9. package/dist/CurrencyField-Cwr3_pow.js +43 -0
  10. package/dist/DateField-DCo6dxud.js +21 -0
  11. package/dist/DateTimeField-BWfBuANO.js +28 -0
  12. package/dist/EmailField-CpwbdVCU.js +31 -0
  13. package/dist/FileField-DVAUAJ8e.js +133 -0
  14. package/dist/FormulaField-CJkkwIK8.js +9 -0
  15. package/dist/GeolocationField-DNCKitgo.js +123 -0
  16. package/dist/GridField-DSblZNfp.js +30 -0
  17. package/dist/ImageField-DBAlnMon.js +90 -0
  18. package/dist/LocationField-DsHsXA6R.js +31 -0
  19. package/dist/LookupField-CsT0QQz2.js +96 -0
  20. package/dist/MasterDetailField-Db8b7Gqs.js +108 -0
  21. package/dist/NumberField-0IGp7lcA.js +26 -0
  22. package/dist/ObjectField-BLApgJtS.js +48 -0
  23. package/dist/PasswordField-pHKyNlmo.js +38 -0
  24. package/dist/PercentField-CwgKmlIb.js +63 -0
  25. package/dist/PhoneField-lKtbYOdN.js +31 -0
  26. package/dist/QRCodeField-BTTasT3w.js +77 -0
  27. package/dist/RatingField-De2X-l44.js +47 -0
  28. package/dist/RichTextField-B5QnvUOr.js +38 -0
  29. package/dist/SelectField-C9AZRHWu.js +26 -0
  30. package/dist/SignatureField-BgcEmYzd.js +85 -0
  31. package/dist/SliderField-BzrttVOY.js +30 -0
  32. package/dist/SummaryField-ugYPYxjP.js +9 -0
  33. package/dist/TextAreaField-DSE_CaU6.js +39 -0
  34. package/dist/TextField-DFQ4T9PR.js +32 -0
  35. package/dist/TimeField-F0cfmsps.js +21 -0
  36. package/dist/UrlField-DLXrFIH-.js +33 -0
  37. package/dist/UserField-PXMmxJY9.js +49 -0
  38. package/dist/VectorField-CKg9jdGa.js +25 -0
  39. package/dist/index-qQ1C-yUR.js +59976 -0
  40. package/dist/index.js +32 -55026
  41. package/dist/index.umd.cjs +41 -30
  42. package/dist/plugin-detail.css +1 -1
  43. package/dist/src/ActivityTimeline.d.ts +20 -0
  44. package/dist/src/ActivityTimeline.d.ts.map +1 -0
  45. package/dist/src/CommentAttachment.d.ts +25 -0
  46. package/dist/src/CommentAttachment.d.ts.map +1 -0
  47. package/dist/src/CommentInput.d.ts +24 -0
  48. package/dist/src/CommentInput.d.ts.map +1 -0
  49. package/dist/src/DetailSection.d.ts +8 -0
  50. package/dist/src/DetailSection.d.ts.map +1 -1
  51. package/dist/src/DetailView.d.ts +4 -0
  52. package/dist/src/DetailView.d.ts.map +1 -1
  53. package/dist/src/DetailView.stories.d.ts +8 -0
  54. package/dist/src/DetailView.stories.d.ts.map +1 -1
  55. package/dist/src/DiffView.d.ts +24 -0
  56. package/dist/src/DiffView.d.ts.map +1 -0
  57. package/dist/src/FieldChangeItem.d.ts +21 -0
  58. package/dist/src/FieldChangeItem.d.ts.map +1 -0
  59. package/dist/src/HeaderHighlight.d.ts +18 -0
  60. package/dist/src/HeaderHighlight.d.ts.map +1 -0
  61. package/dist/src/InlineCreateRelated.d.ts +32 -0
  62. package/dist/src/InlineCreateRelated.d.ts.map +1 -0
  63. package/dist/src/MentionAutocomplete.d.ts +43 -0
  64. package/dist/src/MentionAutocomplete.d.ts.map +1 -0
  65. package/dist/src/PointInTimeRestore.d.ts +28 -0
  66. package/dist/src/PointInTimeRestore.d.ts.map +1 -0
  67. package/dist/src/ReactionPicker.d.ts +25 -0
  68. package/dist/src/ReactionPicker.d.ts.map +1 -0
  69. package/dist/src/RecordActivityTimeline.d.ts +49 -0
  70. package/dist/src/RecordActivityTimeline.d.ts.map +1 -0
  71. package/dist/src/RecordChatterPanel.d.ts +48 -0
  72. package/dist/src/RecordChatterPanel.d.ts.map +1 -0
  73. package/dist/src/RecordComments.d.ts +20 -0
  74. package/dist/src/RecordComments.d.ts.map +1 -0
  75. package/dist/src/RecordNavigationEnhanced.d.ts +18 -0
  76. package/dist/src/RecordNavigationEnhanced.d.ts.map +1 -0
  77. package/dist/src/RelatedList.d.ts +20 -0
  78. package/dist/src/RelatedList.d.ts.map +1 -1
  79. package/dist/src/RelationshipGraph.d.ts +23 -0
  80. package/dist/src/RelationshipGraph.d.ts.map +1 -0
  81. package/dist/src/RichTextCommentInput.d.ts +24 -0
  82. package/dist/src/RichTextCommentInput.d.ts.map +1 -0
  83. package/dist/src/SectionGroup.d.ts +21 -0
  84. package/dist/src/SectionGroup.d.ts.map +1 -0
  85. package/dist/src/SubscriptionToggle.d.ts +22 -0
  86. package/dist/src/SubscriptionToggle.d.ts.map +1 -0
  87. package/dist/src/ThreadedReplies.d.ts +26 -0
  88. package/dist/src/ThreadedReplies.d.ts.map +1 -0
  89. package/dist/src/autoLayout.d.ts +34 -0
  90. package/dist/src/autoLayout.d.ts.map +1 -0
  91. package/dist/src/index.d.ts +40 -0
  92. package/dist/src/index.d.ts.map +1 -1
  93. package/dist/src/useDetailTranslation.d.ts +34 -0
  94. package/dist/src/useDetailTranslation.d.ts.map +1 -0
  95. package/package.json +8 -7
  96. package/src/ActivityTimeline.tsx +184 -0
  97. package/src/CommentAttachment.tsx +192 -0
  98. package/src/CommentInput.tsx +81 -0
  99. package/src/DetailSection.tsx +81 -10
  100. package/src/DetailView.stories.tsx +76 -0
  101. package/src/DetailView.tsx +519 -66
  102. package/src/DiffView.tsx +231 -0
  103. package/src/FieldChangeItem.tsx +46 -0
  104. package/src/HeaderHighlight.tsx +67 -0
  105. package/src/InlineCreateRelated.tsx +291 -0
  106. package/src/MentionAutocomplete.tsx +123 -0
  107. package/src/PointInTimeRestore.tsx +261 -0
  108. package/src/ReactionPicker.tsx +106 -0
  109. package/src/RecordActivityTimeline.tsx +429 -0
  110. package/src/RecordChatterPanel.tsx +202 -0
  111. package/src/RecordComments.tsx +215 -0
  112. package/src/RecordNavigationEnhanced.tsx +211 -0
  113. package/src/RelatedList.tsx +314 -19
  114. package/src/RelationshipGraph.tsx +286 -0
  115. package/src/RichTextCommentInput.tsx +348 -0
  116. package/src/SectionGroup.tsx +101 -0
  117. package/src/SubscriptionToggle.tsx +60 -0
  118. package/src/ThreadedReplies.tsx +161 -0
  119. package/src/__tests__/ActivityTimeline.test.tsx +119 -0
  120. package/src/__tests__/ActivityTimelineFiltering.test.tsx +143 -0
  121. package/src/__tests__/CommentInput.test.tsx +57 -0
  122. package/src/__tests__/DetailSection.test.tsx +320 -0
  123. package/src/__tests__/DetailView.test.tsx +415 -1
  124. package/src/__tests__/FieldChangeItem.test.tsx +119 -0
  125. package/src/__tests__/HeaderHighlight.test.tsx +68 -0
  126. package/src/__tests__/MentionAutocomplete.test.tsx +97 -0
  127. package/src/__tests__/ReactionPicker.test.tsx +113 -0
  128. package/src/__tests__/RecordActivityTimeline.test.tsx +395 -0
  129. package/src/__tests__/RecordChatterPanel.test.tsx +227 -0
  130. package/src/__tests__/RecordComments.test.tsx +96 -0
  131. package/src/__tests__/RecordCommentsPinSearch.test.tsx +133 -0
  132. package/src/__tests__/RelatedList.test.tsx +160 -0
  133. package/src/__tests__/SectionGroup.test.tsx +101 -0
  134. package/src/__tests__/SubscriptionToggle.test.tsx +84 -0
  135. package/src/__tests__/ThreadedReplies.test.tsx +212 -0
  136. package/src/__tests__/autoLayout.test.ts +184 -0
  137. package/src/__tests__/phase12-features.test.tsx +583 -0
  138. package/src/__tests__/roadmap-features.test.tsx +478 -0
  139. package/src/autoLayout.ts +111 -0
  140. package/src/index.tsx +50 -0
  141. package/src/useDetailTranslation.ts +114 -0
@@ -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 if API or DataSource provided
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
- dataSource.findOne(schema.objectName, schema.resourceId).then((result) => {
75
- setData(result);
76
- setLoading(false);
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
- console.error('Failed to fetch detail data:', err);
79
- setLoading(false);
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
- setData(result?.data || result);
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
- }, [schema.api, schema.resourceId]);
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 || 'Are you sure you want to delete this record?';
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 || 'Record Details',
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>Back</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">{schema.title || 'Details'}</h1>
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 ? 'Remove from favorites' : 'Add to favorites'}
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
- {/* Share Button */}
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>Share</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">Edit</span>
520
+ <span className="hidden sm:inline">{t('detail.edit')}</span>
252
521
  </Button>
253
522
  </TooltipTrigger>
254
- <TooltipContent>Edit record</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>More actions</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
- Duplicate
560
+ {t('detail.duplicate')}
274
561
  </DropdownMenuItem>
275
562
  <DropdownMenuItem onClick={handleExport}>
276
563
  <Download className="h-4 w-4 mr-2" />
277
- Export
564
+ {t('detail.export')}
278
565
  </DropdownMenuItem>
279
566
  <DropdownMenuItem onClick={handleViewHistory}>
280
567
  <History className="h-4 w-4 mr-2" />
281
- View history
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
- Delete
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
- {/* Sections */}
308
- {schema.sections && schema.sections.length > 0 && (
309
- <div className="space-y-3 sm:space-y-4">
310
- {schema.sections.map((section, index) => (
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
- key={index}
313
- section={section}
314
- data={data}
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
- {/* Direct Fields (if no sections) */}
321
- {schema.fields && schema.fields.length > 0 && !schema.sections?.length && (
322
- <DetailSection
323
- section={{
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
- {/* Tabs */}
332
- {schema.tabs && schema.tabs.length > 0 && (
333
- <DetailTabs tabs={schema.tabs} data={data} />
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
- {/* Related Lists */}
337
- {schema.related && schema.related.length > 0 && (
338
- <div className="space-y-4">
339
- <h2 className="text-xl font-semibold">Related</h2>
340
- {schema.related.map((related, index) => (
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
- </div>
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 */}