@object-ui/plugin-detail 3.1.0 → 3.1.2
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 +41 -41
- package/CHANGELOG.md +21 -0
- package/dist/{AddressField-C07oUOY6.js → AddressField-QBIlXCFl.js} +1 -1
- package/dist/{AvatarField-VThNABzo.js → AvatarField-BEZuQTAH.js} +1 -1
- package/dist/{BooleanField-CGHKBzAi.js → BooleanField-doa93aFX.js} +1 -1
- package/dist/{CodeField-Co_muhRR.js → CodeField-jVV-hIXg.js} +1 -1
- package/dist/{ColorField-DLid_tFz.js → ColorField-B53qKQGW.js} +1 -1
- package/dist/{CurrencyField-Bw-LqANM.js → CurrencyField-og0NJ2ax.js} +1 -1
- package/dist/{DateField-BNHAzMB2.js → DateField-BFx64AtG.js} +1 -1
- package/dist/{DateTimeField-DjAyn_DQ.js → DateTimeField-Cxs2Rx2f.js} +1 -1
- package/dist/{EmailField-xoNcSppb.js → EmailField-BfcpzRe7.js} +1 -1
- package/dist/{FileField-DbNJwjU2.js → FileField-KarqvhYm.js} +1 -1
- package/dist/{GeolocationField-C1AnS6VV.js → GeolocationField-B5SKZaqn.js} +1 -1
- package/dist/{GridField-DATAHIKf.js → GridField-DOotrUTo.js} +1 -1
- package/dist/{ImageField-CEKJpyJp.js → ImageField-Ddotp4u-.js} +1 -1
- package/dist/{LocationField-jDWXjlpx.js → LocationField-tOkQaPIM.js} +1 -1
- package/dist/{LookupField-DQ08L9UQ.js → LookupField-DF36GvIP.js} +1 -1
- package/dist/{MasterDetailField-Dbk529Ea.js → MasterDetailField-CpHw3nTE.js} +1 -1
- package/dist/{NumberField-BVroN9aV.js → NumberField-CzBb2a28.js} +1 -1
- package/dist/{ObjectField-CT3l_IHW.js → ObjectField-BoL-JqE4.js} +1 -1
- package/dist/{PasswordField-DweVLEE0.js → PasswordField-DrTzkYgj.js} +1 -1
- package/dist/{PercentField-ZpWUK97K.js → PercentField-B9ZUQ3zE.js} +1 -1
- package/dist/{PhoneField-mw-9fqZ_.js → PhoneField-Bf9lhpdu.js} +1 -1
- package/dist/{QRCodeField-Cbb9ck59.js → QRCodeField-PzMpdBKd.js} +1 -1
- package/dist/{RatingField-CSqgLS6t.js → RatingField-CeBMFe8o.js} +1 -1
- package/dist/{RichTextField-BpfBOd99.js → RichTextField-Ch7CHSQ0.js} +1 -1
- package/dist/{SelectField-B9Ei-5jl.js → SelectField-f5Nbi02x.js} +1 -1
- package/dist/{SignatureField-DgGpHnQ8.js → SignatureField-CpxTX2tR.js} +1 -1
- package/dist/{SliderField-C6HvOHd8.js → SliderField-BoZtzgcr.js} +1 -1
- package/dist/{TextAreaField-BK3RgzY3.js → TextAreaField-rT1DLnV2.js} +1 -1
- package/dist/{TextField-Bvzx3atT.js → TextField-CflRxusu.js} +1 -1
- package/dist/{TimeField-Cuz9-Uai.js → TimeField-DeVeCpRu.js} +1 -1
- package/dist/{UrlField-B6XHTV73.js → UrlField-UWKfhP9T.js} +1 -1
- package/dist/{UserField-ooTul2d6.js → UserField-Cp2zQDjz.js} +1 -1
- package/dist/index-V_WBvcaA.js +100249 -0
- package/dist/index.js +20 -18
- package/dist/index.umd.cjs +117 -46
- package/dist/plugin-detail.css +1 -1
- package/dist/src/DetailSection.d.ts +11 -0
- package/dist/src/DetailSection.d.ts.map +1 -1
- package/dist/src/DetailView.d.ts.map +1 -1
- package/dist/src/HeaderHighlight.d.ts +18 -0
- package/dist/src/HeaderHighlight.d.ts.map +1 -0
- package/dist/src/RelatedList.d.ts +16 -0
- package/dist/src/RelatedList.d.ts.map +1 -1
- package/dist/src/SectionGroup.d.ts +21 -0
- package/dist/src/SectionGroup.d.ts.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/useDetailTranslation.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/DetailSection.tsx +50 -26
- package/src/DetailView.tsx +286 -69
- package/src/HeaderHighlight.tsx +67 -0
- package/src/RelatedList.tsx +287 -21
- package/src/SectionGroup.tsx +101 -0
- package/src/__tests__/DetailSection.test.tsx +111 -2
- package/src/__tests__/DetailView.test.tsx +31 -0
- package/src/__tests__/HeaderHighlight.test.tsx +68 -0
- package/src/__tests__/RelatedList.test.tsx +101 -7
- package/src/__tests__/SectionGroup.test.tsx +101 -0
- package/src/__tests__/roadmap-features.test.tsx +478 -0
- package/src/index.tsx +4 -0
- package/src/useDetailTranslation.ts +11 -0
- package/dist/index-CnlyRfY_.js +0 -59461
- package/src/registration.test.tsx +0 -18
package/src/DetailView.tsx
CHANGED
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
TooltipContent,
|
|
22
22
|
TooltipProvider,
|
|
23
23
|
TooltipTrigger,
|
|
24
|
+
Tabs,
|
|
25
|
+
TabsList,
|
|
26
|
+
TabsTrigger,
|
|
27
|
+
TabsContent,
|
|
24
28
|
} from '@object-ui/components';
|
|
25
29
|
import {
|
|
26
30
|
ArrowLeft,
|
|
@@ -40,6 +44,8 @@ import {
|
|
|
40
44
|
import { DetailSection } from './DetailSection';
|
|
41
45
|
import { DetailTabs } from './DetailTabs';
|
|
42
46
|
import { RelatedList } from './RelatedList';
|
|
47
|
+
import { SectionGroup } from './SectionGroup';
|
|
48
|
+
import { HeaderHighlight } from './HeaderHighlight';
|
|
43
49
|
import { RecordComments } from './RecordComments';
|
|
44
50
|
import { ActivityTimeline } from './ActivityTimeline';
|
|
45
51
|
import { SchemaRenderer } from '@object-ui/react';
|
|
@@ -47,6 +53,9 @@ import { buildExpandFields } from '@object-ui/core';
|
|
|
47
53
|
import type { DetailViewSchema, DataSource } from '@object-ui/types';
|
|
48
54
|
import { useDetailTranslation } from './useDetailTranslation';
|
|
49
55
|
|
|
56
|
+
/** Default page size for related lists in the detail view */
|
|
57
|
+
const DEFAULT_RELATED_PAGE_SIZE = 5;
|
|
58
|
+
|
|
50
59
|
export interface DetailViewProps {
|
|
51
60
|
schema: DetailViewSchema;
|
|
52
61
|
dataSource?: DataSource;
|
|
@@ -121,14 +130,8 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
121
130
|
? dataSource.findOne(objectName, resourceId, params)
|
|
122
131
|
: dataSource.findOne(objectName, resourceId);
|
|
123
132
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (result) {
|
|
127
|
-
setData(result);
|
|
128
|
-
setLoading(false);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
// Fallback: try alternate ID format for backward compatibility
|
|
133
|
+
// Helper: try alternate ID format (strip or prepend objectName prefix)
|
|
134
|
+
const tryAltId = () => {
|
|
132
135
|
const resIdStr = String(resourceId);
|
|
133
136
|
const altId = resIdStr.startsWith(prefix)
|
|
134
137
|
? resIdStr.slice(prefix.length) // strip prefix
|
|
@@ -147,6 +150,19 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
147
150
|
setLoading(false);
|
|
148
151
|
}
|
|
149
152
|
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return findOnePromise
|
|
156
|
+
.catch(() => null) // Convert any error to null to trigger alternate ID fallback
|
|
157
|
+
.then((result) => {
|
|
158
|
+
if (!isMounted) return;
|
|
159
|
+
if (result) {
|
|
160
|
+
setData(result);
|
|
161
|
+
setLoading(false);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Fallback: try alternate ID format for backward compatibility
|
|
165
|
+
return tryAltId();
|
|
150
166
|
});
|
|
151
167
|
}).catch((err) => {
|
|
152
168
|
if (isMounted) {
|
|
@@ -288,6 +304,42 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
288
304
|
return () => document.removeEventListener('keydown', handler);
|
|
289
305
|
}, [schema.recordNavigation]);
|
|
290
306
|
|
|
307
|
+
// Auto-discover related lists from objectSchema reference fields
|
|
308
|
+
const discoveredRelated = React.useMemo(() => {
|
|
309
|
+
if (!schema.autoDiscoverRelated || !objectSchema?.fields) return [];
|
|
310
|
+
// Only auto-discover when no explicit related config is provided
|
|
311
|
+
if (schema.related && schema.related.length > 0) return [];
|
|
312
|
+
const refs: Array<{ title: string; type: 'list' | 'grid' | 'table'; objectName: string; referenceField: string }> = [];
|
|
313
|
+
const fields = objectSchema.fields;
|
|
314
|
+
for (const [fieldName, fieldDef] of Object.entries<any>(fields)) {
|
|
315
|
+
const refTarget = fieldDef?.reference_to || fieldDef?.reference;
|
|
316
|
+
if (
|
|
317
|
+
fieldDef &&
|
|
318
|
+
(fieldDef.type === 'lookup' || fieldDef.type === 'master_detail') &&
|
|
319
|
+
refTarget
|
|
320
|
+
) {
|
|
321
|
+
refs.push({
|
|
322
|
+
title: fieldDef.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
|
|
323
|
+
type: 'table',
|
|
324
|
+
objectName: refTarget,
|
|
325
|
+
referenceField: fieldName,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return refs;
|
|
330
|
+
}, [schema.autoDiscoverRelated, schema.related, objectSchema]);
|
|
331
|
+
|
|
332
|
+
// Merge explicit and auto-discovered related lists
|
|
333
|
+
const effectiveRelated: NonNullable<DetailViewSchema['related']> = React.useMemo(() => {
|
|
334
|
+
if (schema.related && schema.related.length > 0) return schema.related;
|
|
335
|
+
return discoveredRelated.map((r) => ({
|
|
336
|
+
title: r.title,
|
|
337
|
+
type: r.type,
|
|
338
|
+
api: r.objectName,
|
|
339
|
+
data: [] as any[],
|
|
340
|
+
}));
|
|
341
|
+
}, [schema.related, discoveredRelated]);
|
|
342
|
+
|
|
291
343
|
if (loading || schema.loading) {
|
|
292
344
|
return (
|
|
293
345
|
<div className={cn('space-y-4', className)}>
|
|
@@ -427,7 +479,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
427
479
|
<SchemaRenderer key={index} schema={action} data={data} />
|
|
428
480
|
))}
|
|
429
481
|
|
|
430
|
-
{/* Inline Edit Toggle */}
|
|
482
|
+
{/* Inline Edit Toggle - hidden on mobile, accessible via more menu */}
|
|
431
483
|
{inlineEdit && (
|
|
432
484
|
<Tooltip>
|
|
433
485
|
<TooltipTrigger asChild>
|
|
@@ -435,7 +487,7 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
435
487
|
variant={isInlineEditing ? 'default' : 'outline'}
|
|
436
488
|
size="sm"
|
|
437
489
|
onClick={handleInlineEditToggle}
|
|
438
|
-
className="gap-2"
|
|
490
|
+
className="gap-2 hidden sm:inline-flex"
|
|
439
491
|
>
|
|
440
492
|
{isInlineEditing ? (
|
|
441
493
|
<>
|
|
@@ -456,21 +508,21 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
456
508
|
</Tooltip>
|
|
457
509
|
)}
|
|
458
510
|
|
|
459
|
-
{/* Share Button */}
|
|
511
|
+
{/* Share Button - hidden on mobile, accessible via more menu */}
|
|
460
512
|
<Tooltip>
|
|
461
513
|
<TooltipTrigger asChild>
|
|
462
|
-
<Button variant="outline" size="icon" onClick={handleShare}>
|
|
514
|
+
<Button variant="outline" size="icon" onClick={handleShare} className="hidden sm:inline-flex">
|
|
463
515
|
<Share2 className="h-4 w-4" />
|
|
464
516
|
</Button>
|
|
465
517
|
</TooltipTrigger>
|
|
466
518
|
<TooltipContent>{t('detail.share')}</TooltipContent>
|
|
467
519
|
</Tooltip>
|
|
468
520
|
|
|
469
|
-
{/* Edit Button */}
|
|
521
|
+
{/* Edit Button - hidden on mobile, accessible via more menu */}
|
|
470
522
|
{schema.showEdit && (
|
|
471
523
|
<Tooltip>
|
|
472
524
|
<TooltipTrigger asChild>
|
|
473
|
-
<Button variant="default" onClick={handleEdit} className="gap-2">
|
|
525
|
+
<Button variant="default" onClick={handleEdit} className="gap-2 hidden sm:inline-flex">
|
|
474
526
|
<Edit className="h-4 w-4" />
|
|
475
527
|
<span className="hidden sm:inline">{t('detail.edit')}</span>
|
|
476
528
|
</Button>
|
|
@@ -492,6 +544,24 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
492
544
|
<TooltipContent>{t('detail.moreActions')}</TooltipContent>
|
|
493
545
|
</Tooltip>
|
|
494
546
|
<DropdownMenuContent align="end" className="w-[calc(100vw-2rem)] sm:w-48 max-h-[60vh] overflow-y-auto">
|
|
547
|
+
{/* Mobile-only: Share, Edit, Inline Edit */}
|
|
548
|
+
<DropdownMenuItem onClick={handleShare} className="sm:hidden">
|
|
549
|
+
<Share2 className="h-4 w-4 mr-2" />
|
|
550
|
+
{t('detail.share')}
|
|
551
|
+
</DropdownMenuItem>
|
|
552
|
+
{schema.showEdit && (
|
|
553
|
+
<DropdownMenuItem onClick={handleEdit} className="sm:hidden">
|
|
554
|
+
<Edit className="h-4 w-4 mr-2" />
|
|
555
|
+
{t('detail.edit')}
|
|
556
|
+
</DropdownMenuItem>
|
|
557
|
+
)}
|
|
558
|
+
{inlineEdit && (
|
|
559
|
+
<DropdownMenuItem onClick={handleInlineEditToggle} className="sm:hidden">
|
|
560
|
+
<Edit className="h-4 w-4 mr-2" />
|
|
561
|
+
{isInlineEditing ? t('detail.save') : t('detail.editInline')}
|
|
562
|
+
</DropdownMenuItem>
|
|
563
|
+
)}
|
|
564
|
+
<DropdownMenuSeparator className="sm:hidden" />
|
|
495
565
|
<DropdownMenuItem onClick={handleDuplicate}>
|
|
496
566
|
<Copy className="h-4 w-4 mr-2" />
|
|
497
567
|
{t('detail.duplicate')}
|
|
@@ -528,70 +598,217 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
528
598
|
</div>
|
|
529
599
|
)}
|
|
530
600
|
|
|
531
|
-
{/*
|
|
532
|
-
{schema.
|
|
533
|
-
<
|
|
534
|
-
|
|
601
|
+
{/* Header Highlight Area */}
|
|
602
|
+
{schema.highlightFields && schema.highlightFields.length > 0 && (
|
|
603
|
+
<HeaderHighlight fields={schema.highlightFields} data={data} objectName={schema.objectName} />
|
|
604
|
+
)}
|
|
605
|
+
|
|
606
|
+
{/* Auto Tabs mode: wrap sections, related, activity into tabs */}
|
|
607
|
+
{schema.autoTabs && !schema.tabs?.length ? (
|
|
608
|
+
<Tabs defaultValue="details" className="w-full">
|
|
609
|
+
<TabsList className="w-full justify-start border-b rounded-none bg-transparent p-0">
|
|
610
|
+
<TabsTrigger
|
|
611
|
+
value="details"
|
|
612
|
+
className="relative rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
|
|
613
|
+
>
|
|
614
|
+
{t('detail.details')}
|
|
615
|
+
</TabsTrigger>
|
|
616
|
+
{effectiveRelated.length > 0 && (
|
|
617
|
+
<TabsTrigger
|
|
618
|
+
value="related"
|
|
619
|
+
className="relative rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
|
|
620
|
+
>
|
|
621
|
+
<span className="flex items-center gap-1.5">
|
|
622
|
+
{t('detail.related')}
|
|
623
|
+
<Badge variant="secondary" className="text-xs">{effectiveRelated.length}</Badge>
|
|
624
|
+
</span>
|
|
625
|
+
</TabsTrigger>
|
|
626
|
+
)}
|
|
627
|
+
{schema.activities && schema.activities.length > 0 && (
|
|
628
|
+
<TabsTrigger
|
|
629
|
+
value="activity"
|
|
630
|
+
className="relative rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
|
|
631
|
+
>
|
|
632
|
+
<span className="flex items-center gap-1.5">
|
|
633
|
+
{t('detail.activity')}
|
|
634
|
+
<Badge variant="secondary" className="text-xs">{schema.activities.length}</Badge>
|
|
635
|
+
</span>
|
|
636
|
+
</TabsTrigger>
|
|
637
|
+
)}
|
|
638
|
+
</TabsList>
|
|
639
|
+
|
|
640
|
+
{/* Details Tab Content */}
|
|
641
|
+
<TabsContent value="details" className="mt-4">
|
|
642
|
+
<div className="space-y-3 sm:space-y-4">
|
|
643
|
+
{/* Section Groups */}
|
|
644
|
+
{schema.sectionGroups && schema.sectionGroups.length > 0 && (
|
|
645
|
+
schema.sectionGroups.map((group, index) => (
|
|
646
|
+
<SectionGroup
|
|
647
|
+
key={index}
|
|
648
|
+
group={group}
|
|
649
|
+
data={{ ...data, ...editedValues }}
|
|
650
|
+
objectSchema={objectSchema}
|
|
651
|
+
objectName={schema.objectName}
|
|
652
|
+
isEditing={isInlineEditing}
|
|
653
|
+
onFieldChange={handleInlineFieldChange}
|
|
654
|
+
/>
|
|
655
|
+
))
|
|
656
|
+
)}
|
|
657
|
+
{schema.sections && schema.sections.length > 0 && (
|
|
658
|
+
schema.sections.map((section, index) => (
|
|
659
|
+
<DetailSection
|
|
660
|
+
key={index}
|
|
661
|
+
section={section}
|
|
662
|
+
data={{ ...data, ...editedValues }}
|
|
663
|
+
objectSchema={objectSchema}
|
|
664
|
+
objectName={schema.objectName}
|
|
665
|
+
isEditing={isInlineEditing}
|
|
666
|
+
onFieldChange={handleInlineFieldChange}
|
|
667
|
+
/>
|
|
668
|
+
))
|
|
669
|
+
)}
|
|
670
|
+
{schema.fields && schema.fields.length > 0 && !schema.sections?.length && (
|
|
671
|
+
<DetailSection
|
|
672
|
+
section={{
|
|
673
|
+
fields: schema.fields,
|
|
674
|
+
columns: schema.columns,
|
|
675
|
+
}}
|
|
676
|
+
data={{ ...data, ...editedValues }}
|
|
677
|
+
objectSchema={objectSchema}
|
|
678
|
+
objectName={schema.objectName}
|
|
679
|
+
isEditing={isInlineEditing}
|
|
680
|
+
onFieldChange={handleInlineFieldChange}
|
|
681
|
+
/>
|
|
682
|
+
)}
|
|
683
|
+
{/* Comments in details tab */}
|
|
684
|
+
{schema.comments && (
|
|
685
|
+
<RecordComments
|
|
686
|
+
comments={schema.comments}
|
|
687
|
+
onAddComment={schema.onAddComment}
|
|
688
|
+
/>
|
|
689
|
+
)}
|
|
690
|
+
</div>
|
|
691
|
+
</TabsContent>
|
|
692
|
+
|
|
693
|
+
{/* Related Tab Content */}
|
|
694
|
+
{effectiveRelated.length > 0 && (
|
|
695
|
+
<TabsContent value="related" className="mt-4">
|
|
696
|
+
<div className="space-y-4">
|
|
697
|
+
{effectiveRelated.map((related, index) => (
|
|
698
|
+
<RelatedList
|
|
699
|
+
key={index}
|
|
700
|
+
title={related.title}
|
|
701
|
+
type={related.type}
|
|
702
|
+
api={related.api}
|
|
703
|
+
data={related.data}
|
|
704
|
+
columns={related.columns as any}
|
|
705
|
+
dataSource={dataSource}
|
|
706
|
+
objectName={related.api}
|
|
707
|
+
collapsible
|
|
708
|
+
pageSize={DEFAULT_RELATED_PAGE_SIZE}
|
|
709
|
+
/>
|
|
710
|
+
))}
|
|
711
|
+
</div>
|
|
712
|
+
</TabsContent>
|
|
713
|
+
)}
|
|
714
|
+
|
|
715
|
+
{/* Activity Tab Content */}
|
|
716
|
+
{schema.activities && schema.activities.length > 0 && (
|
|
717
|
+
<TabsContent value="activity" className="mt-4">
|
|
718
|
+
<ActivityTimeline activities={schema.activities} />
|
|
719
|
+
</TabsContent>
|
|
720
|
+
)}
|
|
721
|
+
</Tabs>
|
|
722
|
+
) : (
|
|
723
|
+
<>
|
|
724
|
+
{/* Section Groups */}
|
|
725
|
+
{schema.sectionGroups && schema.sectionGroups.length > 0 && (
|
|
726
|
+
<div className="space-y-3 sm:space-y-4">
|
|
727
|
+
{schema.sectionGroups.map((group, index) => (
|
|
728
|
+
<SectionGroup
|
|
729
|
+
key={index}
|
|
730
|
+
group={group}
|
|
731
|
+
data={{ ...data, ...editedValues }}
|
|
732
|
+
objectSchema={objectSchema}
|
|
733
|
+
objectName={schema.objectName}
|
|
734
|
+
isEditing={isInlineEditing}
|
|
735
|
+
onFieldChange={handleInlineFieldChange}
|
|
736
|
+
/>
|
|
737
|
+
))}
|
|
738
|
+
</div>
|
|
739
|
+
)}
|
|
740
|
+
|
|
741
|
+
{/* Sections */}
|
|
742
|
+
{schema.sections && schema.sections.length > 0 && (
|
|
743
|
+
<div className="space-y-3 sm:space-y-4">
|
|
744
|
+
{schema.sections.map((section, index) => (
|
|
745
|
+
<DetailSection
|
|
746
|
+
key={index}
|
|
747
|
+
section={section}
|
|
748
|
+
data={{ ...data, ...editedValues }}
|
|
749
|
+
objectSchema={objectSchema}
|
|
750
|
+
objectName={schema.objectName}
|
|
751
|
+
isEditing={isInlineEditing}
|
|
752
|
+
onFieldChange={handleInlineFieldChange}
|
|
753
|
+
/>
|
|
754
|
+
))}
|
|
755
|
+
</div>
|
|
756
|
+
)}
|
|
757
|
+
|
|
758
|
+
{/* Direct Fields (if no sections) */}
|
|
759
|
+
{schema.fields && schema.fields.length > 0 && !schema.sections?.length && (
|
|
535
760
|
<DetailSection
|
|
536
|
-
|
|
537
|
-
|
|
761
|
+
section={{
|
|
762
|
+
fields: schema.fields,
|
|
763
|
+
columns: schema.columns,
|
|
764
|
+
}}
|
|
538
765
|
data={{ ...data, ...editedValues }}
|
|
539
766
|
objectSchema={objectSchema}
|
|
767
|
+
objectName={schema.objectName}
|
|
540
768
|
isEditing={isInlineEditing}
|
|
541
769
|
onFieldChange={handleInlineFieldChange}
|
|
542
770
|
/>
|
|
543
|
-
)
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
771
|
+
)}
|
|
772
|
+
|
|
773
|
+
{/* Tabs */}
|
|
774
|
+
{schema.tabs && schema.tabs.length > 0 && (
|
|
775
|
+
<DetailTabs tabs={schema.tabs} data={data} />
|
|
776
|
+
)}
|
|
777
|
+
|
|
778
|
+
{/* Related Lists */}
|
|
779
|
+
{effectiveRelated.length > 0 && (
|
|
780
|
+
<div className="space-y-4">
|
|
781
|
+
<h2 className="text-xl font-semibold">{t('detail.related')}</h2>
|
|
782
|
+
{effectiveRelated.map((related, index) => (
|
|
783
|
+
<RelatedList
|
|
784
|
+
key={index}
|
|
785
|
+
title={related.title}
|
|
786
|
+
type={related.type}
|
|
787
|
+
api={related.api}
|
|
788
|
+
data={related.data}
|
|
789
|
+
columns={related.columns as any}
|
|
790
|
+
dataSource={dataSource}
|
|
791
|
+
objectName={related.api}
|
|
792
|
+
collapsible
|
|
793
|
+
pageSize={DEFAULT_RELATED_PAGE_SIZE}
|
|
794
|
+
/>
|
|
795
|
+
))}
|
|
796
|
+
</div>
|
|
797
|
+
)}
|
|
565
798
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
<RelatedList
|
|
572
|
-
key={index}
|
|
573
|
-
title={related.title}
|
|
574
|
-
type={related.type}
|
|
575
|
-
api={related.api}
|
|
576
|
-
data={related.data}
|
|
577
|
-
columns={related.columns as any}
|
|
578
|
-
dataSource={dataSource}
|
|
799
|
+
{/* Comments */}
|
|
800
|
+
{schema.comments && (
|
|
801
|
+
<RecordComments
|
|
802
|
+
comments={schema.comments}
|
|
803
|
+
onAddComment={schema.onAddComment}
|
|
579
804
|
/>
|
|
580
|
-
)
|
|
581
|
-
</div>
|
|
582
|
-
)}
|
|
583
|
-
|
|
584
|
-
{/* Comments */}
|
|
585
|
-
{schema.comments && (
|
|
586
|
-
<RecordComments
|
|
587
|
-
comments={schema.comments}
|
|
588
|
-
onAddComment={schema.onAddComment}
|
|
589
|
-
/>
|
|
590
|
-
)}
|
|
805
|
+
)}
|
|
591
806
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
807
|
+
{/* Activity Timeline */}
|
|
808
|
+
{schema.activities && schema.activities.length > 0 && (
|
|
809
|
+
<ActivityTimeline activities={schema.activities} />
|
|
810
|
+
)}
|
|
811
|
+
</>
|
|
595
812
|
)}
|
|
596
813
|
|
|
597
814
|
{/* Custom Footer */}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
import { cn, Card, CardContent } from '@object-ui/components';
|
|
11
|
+
import type { HighlightField } from '@object-ui/types';
|
|
12
|
+
import { useSafeFieldLabel } from '@object-ui/react';
|
|
13
|
+
|
|
14
|
+
export interface HeaderHighlightProps {
|
|
15
|
+
fields: HighlightField[];
|
|
16
|
+
data?: any;
|
|
17
|
+
className?: string;
|
|
18
|
+
/** Object name for i18n field label resolution */
|
|
19
|
+
objectName?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const HeaderHighlight: React.FC<HeaderHighlightProps> = ({
|
|
23
|
+
fields,
|
|
24
|
+
data,
|
|
25
|
+
className,
|
|
26
|
+
objectName,
|
|
27
|
+
}) => {
|
|
28
|
+
const { fieldLabel } = useSafeFieldLabel();
|
|
29
|
+
if (!fields.length || !data) return null;
|
|
30
|
+
|
|
31
|
+
// Filter to only fields with values
|
|
32
|
+
const visibleFields = fields.filter((f) => {
|
|
33
|
+
const val = data?.[f.name];
|
|
34
|
+
return val !== null && val !== undefined && val !== '';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (visibleFields.length === 0) return null;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<Card className={cn('bg-muted/30 border-dashed', className)}>
|
|
41
|
+
<CardContent className="py-3 px-4">
|
|
42
|
+
<div className={cn(
|
|
43
|
+
'grid gap-4',
|
|
44
|
+
visibleFields.length === 1 ? 'grid-cols-1' :
|
|
45
|
+
visibleFields.length === 2 ? 'grid-cols-2' :
|
|
46
|
+
visibleFields.length === 3 ? 'grid-cols-3' :
|
|
47
|
+
'grid-cols-2 md:grid-cols-4'
|
|
48
|
+
)}>
|
|
49
|
+
{visibleFields.map((field) => {
|
|
50
|
+
const value = data[field.name];
|
|
51
|
+
return (
|
|
52
|
+
<div key={field.name} className="flex flex-col gap-0.5">
|
|
53
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
54
|
+
{field.icon && <span className="mr-1">{field.icon}</span>}
|
|
55
|
+
{fieldLabel(objectName || '', field.name, field.label)}
|
|
56
|
+
</span>
|
|
57
|
+
<span className="text-sm font-semibold truncate">
|
|
58
|
+
{String(value)}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
})}
|
|
63
|
+
</div>
|
|
64
|
+
</CardContent>
|
|
65
|
+
</Card>
|
|
66
|
+
);
|
|
67
|
+
};
|