@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.
Files changed (66) hide show
  1. package/.turbo/turbo-build.log +41 -41
  2. package/CHANGELOG.md +21 -0
  3. package/dist/{AddressField-C07oUOY6.js → AddressField-QBIlXCFl.js} +1 -1
  4. package/dist/{AvatarField-VThNABzo.js → AvatarField-BEZuQTAH.js} +1 -1
  5. package/dist/{BooleanField-CGHKBzAi.js → BooleanField-doa93aFX.js} +1 -1
  6. package/dist/{CodeField-Co_muhRR.js → CodeField-jVV-hIXg.js} +1 -1
  7. package/dist/{ColorField-DLid_tFz.js → ColorField-B53qKQGW.js} +1 -1
  8. package/dist/{CurrencyField-Bw-LqANM.js → CurrencyField-og0NJ2ax.js} +1 -1
  9. package/dist/{DateField-BNHAzMB2.js → DateField-BFx64AtG.js} +1 -1
  10. package/dist/{DateTimeField-DjAyn_DQ.js → DateTimeField-Cxs2Rx2f.js} +1 -1
  11. package/dist/{EmailField-xoNcSppb.js → EmailField-BfcpzRe7.js} +1 -1
  12. package/dist/{FileField-DbNJwjU2.js → FileField-KarqvhYm.js} +1 -1
  13. package/dist/{GeolocationField-C1AnS6VV.js → GeolocationField-B5SKZaqn.js} +1 -1
  14. package/dist/{GridField-DATAHIKf.js → GridField-DOotrUTo.js} +1 -1
  15. package/dist/{ImageField-CEKJpyJp.js → ImageField-Ddotp4u-.js} +1 -1
  16. package/dist/{LocationField-jDWXjlpx.js → LocationField-tOkQaPIM.js} +1 -1
  17. package/dist/{LookupField-DQ08L9UQ.js → LookupField-DF36GvIP.js} +1 -1
  18. package/dist/{MasterDetailField-Dbk529Ea.js → MasterDetailField-CpHw3nTE.js} +1 -1
  19. package/dist/{NumberField-BVroN9aV.js → NumberField-CzBb2a28.js} +1 -1
  20. package/dist/{ObjectField-CT3l_IHW.js → ObjectField-BoL-JqE4.js} +1 -1
  21. package/dist/{PasswordField-DweVLEE0.js → PasswordField-DrTzkYgj.js} +1 -1
  22. package/dist/{PercentField-ZpWUK97K.js → PercentField-B9ZUQ3zE.js} +1 -1
  23. package/dist/{PhoneField-mw-9fqZ_.js → PhoneField-Bf9lhpdu.js} +1 -1
  24. package/dist/{QRCodeField-Cbb9ck59.js → QRCodeField-PzMpdBKd.js} +1 -1
  25. package/dist/{RatingField-CSqgLS6t.js → RatingField-CeBMFe8o.js} +1 -1
  26. package/dist/{RichTextField-BpfBOd99.js → RichTextField-Ch7CHSQ0.js} +1 -1
  27. package/dist/{SelectField-B9Ei-5jl.js → SelectField-f5Nbi02x.js} +1 -1
  28. package/dist/{SignatureField-DgGpHnQ8.js → SignatureField-CpxTX2tR.js} +1 -1
  29. package/dist/{SliderField-C6HvOHd8.js → SliderField-BoZtzgcr.js} +1 -1
  30. package/dist/{TextAreaField-BK3RgzY3.js → TextAreaField-rT1DLnV2.js} +1 -1
  31. package/dist/{TextField-Bvzx3atT.js → TextField-CflRxusu.js} +1 -1
  32. package/dist/{TimeField-Cuz9-Uai.js → TimeField-DeVeCpRu.js} +1 -1
  33. package/dist/{UrlField-B6XHTV73.js → UrlField-UWKfhP9T.js} +1 -1
  34. package/dist/{UserField-ooTul2d6.js → UserField-Cp2zQDjz.js} +1 -1
  35. package/dist/index-V_WBvcaA.js +100249 -0
  36. package/dist/index.js +20 -18
  37. package/dist/index.umd.cjs +117 -46
  38. package/dist/plugin-detail.css +1 -1
  39. package/dist/src/DetailSection.d.ts +11 -0
  40. package/dist/src/DetailSection.d.ts.map +1 -1
  41. package/dist/src/DetailView.d.ts.map +1 -1
  42. package/dist/src/HeaderHighlight.d.ts +18 -0
  43. package/dist/src/HeaderHighlight.d.ts.map +1 -0
  44. package/dist/src/RelatedList.d.ts +16 -0
  45. package/dist/src/RelatedList.d.ts.map +1 -1
  46. package/dist/src/SectionGroup.d.ts +21 -0
  47. package/dist/src/SectionGroup.d.ts.map +1 -0
  48. package/dist/src/index.d.ts +4 -0
  49. package/dist/src/index.d.ts.map +1 -1
  50. package/dist/src/useDetailTranslation.d.ts.map +1 -1
  51. package/package.json +6 -6
  52. package/src/DetailSection.tsx +50 -26
  53. package/src/DetailView.tsx +286 -69
  54. package/src/HeaderHighlight.tsx +67 -0
  55. package/src/RelatedList.tsx +287 -21
  56. package/src/SectionGroup.tsx +101 -0
  57. package/src/__tests__/DetailSection.test.tsx +111 -2
  58. package/src/__tests__/DetailView.test.tsx +31 -0
  59. package/src/__tests__/HeaderHighlight.test.tsx +68 -0
  60. package/src/__tests__/RelatedList.test.tsx +101 -7
  61. package/src/__tests__/SectionGroup.test.tsx +101 -0
  62. package/src/__tests__/roadmap-features.test.tsx +478 -0
  63. package/src/index.tsx +4 -0
  64. package/src/useDetailTranslation.ts +11 -0
  65. package/dist/index-CnlyRfY_.js +0 -59461
  66. package/src/registration.test.tsx +0 -18
@@ -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
- 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
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
- {/* Sections */}
532
- {schema.sections && schema.sections.length > 0 && (
533
- <div className="space-y-3 sm:space-y-4">
534
- {schema.sections.map((section, index) => (
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
- key={index}
537
- section={section}
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
- </div>
545
- )}
546
-
547
- {/* Direct Fields (if no sections) */}
548
- {schema.fields && schema.fields.length > 0 && !schema.sections?.length && (
549
- <DetailSection
550
- section={{
551
- fields: schema.fields,
552
- columns: schema.columns,
553
- }}
554
- data={{ ...data, ...editedValues }}
555
- objectSchema={objectSchema}
556
- isEditing={isInlineEditing}
557
- onFieldChange={handleInlineFieldChange}
558
- />
559
- )}
560
-
561
- {/* Tabs */}
562
- {schema.tabs && schema.tabs.length > 0 && (
563
- <DetailTabs tabs={schema.tabs} data={data} />
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
- {/* Related Lists */}
567
- {schema.related && schema.related.length > 0 && (
568
- <div className="space-y-4">
569
- <h2 className="text-xl font-semibold">{t('detail.related')}</h2>
570
- {schema.related.map((related, index) => (
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
- {/* Activity Timeline */}
593
- {schema.activities && schema.activities.length > 0 && (
594
- <ActivityTimeline activities={schema.activities} />
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
+ };