Skip to content

Commit f26e15a

Browse files
authored
Merge pull request #970 from objectstack-ai/copilot/complete-roadmap-development-71036824-96ec-4c9a-80b1-cd3a32e00f4b
2 parents 70ac344 + 6eb3fbd commit f26e15a

File tree

12 files changed

+1329
-70
lines changed

12 files changed

+1329
-70
lines changed

ROADMAP.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,13 +1461,13 @@ All 313 `@object-ui/fields` tests pass.
14611461
- [x] 2 new DetailView i18n fallback tests (Record not found text, Related heading)
14621462
- [x] Updated DetailSection tests for new empty value styling
14631463

1464-
**Remaining (future PRs):**
1465-
- [ ] Auto-discover related lists from objectSchema reference fields
1466-
- [ ] Tab layout (Details/Related/Activity) for detail page
1467-
- [ ] Related list row-level Edit/Delete quick actions
1468-
- [ ] Related list pagination, sorting, filtering
1469-
- [ ] Collapsible section groups
1470-
- [ ] Header highlight area with key fields
1464+
**Completed:**
1465+
- [x] Auto-discover related lists from objectSchema reference fields
1466+
- [x] Tab layout (Details/Related/Activity) for detail page
1467+
- [x] Related list row-level Edit/Delete quick actions
1468+
- [x] Related list pagination, sorting, filtering
1469+
- [x] Collapsible section groups
1470+
- [x] Header highlight area with key fields
14711471

14721472
---
14731473

packages/plugin-detail/src/DetailView.tsx

Lines changed: 230 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import {
2121
TooltipContent,
2222
TooltipProvider,
2323
TooltipTrigger,
24+
Tabs,
25+
TabsList,
26+
TabsTrigger,
27+
TabsContent,
2428
} from '@object-ui/components';
2529
import {
2630
ArrowLeft,
@@ -40,6 +44,8 @@ import {
4044
import { DetailSection } from './DetailSection';
4145
import { DetailTabs } from './DetailTabs';
4246
import { RelatedList } from './RelatedList';
47+
import { SectionGroup } from './SectionGroup';
48+
import { HeaderHighlight } from './HeaderHighlight';
4349
import { RecordComments } from './RecordComments';
4450
import { ActivityTimeline } from './ActivityTimeline';
4551
import { SchemaRenderer } from '@object-ui/react';
@@ -288,6 +294,40 @@ export const DetailView: React.FC<DetailViewProps> = ({
288294
return () => document.removeEventListener('keydown', handler);
289295
}, [schema.recordNavigation]);
290296

297+
// Auto-discover related lists from objectSchema reference fields
298+
const discoveredRelated = React.useMemo(() => {
299+
if (!schema.autoDiscoverRelated || !objectSchema?.fields) return [];
300+
// Only auto-discover when no explicit related config is provided
301+
if (schema.related && schema.related.length > 0) return [];
302+
const refs: Array<{ title: string; type: 'list' | 'grid' | 'table'; objectName: string; referenceField: string }> = [];
303+
const fields = objectSchema.fields;
304+
for (const [fieldName, fieldDef] of Object.entries<any>(fields)) {
305+
if (
306+
fieldDef &&
307+
(fieldDef.type === 'lookup' || fieldDef.type === 'master_detail') &&
308+
fieldDef.reference_to
309+
) {
310+
refs.push({
311+
title: fieldDef.label || fieldName.charAt(0).toUpperCase() + fieldName.slice(1),
312+
type: 'table',
313+
objectName: fieldDef.reference_to,
314+
referenceField: fieldName,
315+
});
316+
}
317+
}
318+
return refs;
319+
}, [schema.autoDiscoverRelated, schema.related, objectSchema]);
320+
321+
// Merge explicit and auto-discovered related lists
322+
const effectiveRelated = React.useMemo(() => {
323+
if (schema.related && schema.related.length > 0) return schema.related;
324+
return discoveredRelated.map((r) => ({
325+
title: r.title,
326+
type: r.type,
327+
data: [] as any[],
328+
}));
329+
}, [schema.related, discoveredRelated]);
330+
291331
if (loading || schema.loading) {
292332
return (
293333
<div className={cn('space-y-4', className)}>
@@ -528,70 +568,205 @@ export const DetailView: React.FC<DetailViewProps> = ({
528568
</div>
529569
)}
530570

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) => (
571+
{/* Header Highlight Area */}
572+
{schema.highlightFields && schema.highlightFields.length > 0 && (
573+
<HeaderHighlight fields={schema.highlightFields} data={data} />
574+
)}
575+
576+
{/* Auto Tabs mode: wrap sections, related, activity into tabs */}
577+
{schema.autoTabs && !schema.tabs?.length ? (
578+
<Tabs defaultValue="details" className="w-full">
579+
<TabsList className="w-full justify-start border-b rounded-none bg-transparent p-0">
580+
<TabsTrigger
581+
value="details"
582+
className="relative rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
583+
>
584+
{t('detail.details')}
585+
</TabsTrigger>
586+
{effectiveRelated.length > 0 && (
587+
<TabsTrigger
588+
value="related"
589+
className="relative rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
590+
>
591+
<span className="flex items-center gap-1.5">
592+
{t('detail.related')}
593+
<Badge variant="secondary" className="text-xs">{effectiveRelated.length}</Badge>
594+
</span>
595+
</TabsTrigger>
596+
)}
597+
{schema.activities && schema.activities.length > 0 && (
598+
<TabsTrigger
599+
value="activity"
600+
className="relative rounded-none border-b-2 border-transparent data-[state=active]:border-primary data-[state=active]:bg-transparent"
601+
>
602+
<span className="flex items-center gap-1.5">
603+
{t('detail.activity')}
604+
<Badge variant="secondary" className="text-xs">{schema.activities.length}</Badge>
605+
</span>
606+
</TabsTrigger>
607+
)}
608+
</TabsList>
609+
610+
{/* Details Tab Content */}
611+
<TabsContent value="details" className="mt-4">
612+
<div className="space-y-3 sm:space-y-4">
613+
{/* Section Groups */}
614+
{schema.sectionGroups && schema.sectionGroups.length > 0 && (
615+
schema.sectionGroups.map((group, index) => (
616+
<SectionGroup
617+
key={index}
618+
group={group}
619+
data={{ ...data, ...editedValues }}
620+
objectSchema={objectSchema}
621+
isEditing={isInlineEditing}
622+
onFieldChange={handleInlineFieldChange}
623+
/>
624+
))
625+
)}
626+
{schema.sections && schema.sections.length > 0 && (
627+
schema.sections.map((section, index) => (
628+
<DetailSection
629+
key={index}
630+
section={section}
631+
data={{ ...data, ...editedValues }}
632+
objectSchema={objectSchema}
633+
isEditing={isInlineEditing}
634+
onFieldChange={handleInlineFieldChange}
635+
/>
636+
))
637+
)}
638+
{schema.fields && schema.fields.length > 0 && !schema.sections?.length && (
639+
<DetailSection
640+
section={{
641+
fields: schema.fields,
642+
columns: schema.columns,
643+
}}
644+
data={{ ...data, ...editedValues }}
645+
objectSchema={objectSchema}
646+
isEditing={isInlineEditing}
647+
onFieldChange={handleInlineFieldChange}
648+
/>
649+
)}
650+
{/* Comments in details tab */}
651+
{schema.comments && (
652+
<RecordComments
653+
comments={schema.comments}
654+
onAddComment={schema.onAddComment}
655+
/>
656+
)}
657+
</div>
658+
</TabsContent>
659+
660+
{/* Related Tab Content */}
661+
{effectiveRelated.length > 0 && (
662+
<TabsContent value="related" className="mt-4">
663+
<div className="space-y-4">
664+
{effectiveRelated.map((related, index) => (
665+
<RelatedList
666+
key={index}
667+
title={related.title}
668+
type={related.type}
669+
api={related.api}
670+
data={related.data}
671+
columns={related.columns as any}
672+
dataSource={dataSource}
673+
/>
674+
))}
675+
</div>
676+
</TabsContent>
677+
)}
678+
679+
{/* Activity Tab Content */}
680+
{schema.activities && schema.activities.length > 0 && (
681+
<TabsContent value="activity" className="mt-4">
682+
<ActivityTimeline activities={schema.activities} />
683+
</TabsContent>
684+
)}
685+
</Tabs>
686+
) : (
687+
<>
688+
{/* Section Groups */}
689+
{schema.sectionGroups && schema.sectionGroups.length > 0 && (
690+
<div className="space-y-3 sm:space-y-4">
691+
{schema.sectionGroups.map((group, index) => (
692+
<SectionGroup
693+
key={index}
694+
group={group}
695+
data={{ ...data, ...editedValues }}
696+
objectSchema={objectSchema}
697+
isEditing={isInlineEditing}
698+
onFieldChange={handleInlineFieldChange}
699+
/>
700+
))}
701+
</div>
702+
)}
703+
704+
{/* Sections */}
705+
{schema.sections && schema.sections.length > 0 && (
706+
<div className="space-y-3 sm:space-y-4">
707+
{schema.sections.map((section, index) => (
708+
<DetailSection
709+
key={index}
710+
section={section}
711+
data={{ ...data, ...editedValues }}
712+
objectSchema={objectSchema}
713+
isEditing={isInlineEditing}
714+
onFieldChange={handleInlineFieldChange}
715+
/>
716+
))}
717+
</div>
718+
)}
719+
720+
{/* Direct Fields (if no sections) */}
721+
{schema.fields && schema.fields.length > 0 && !schema.sections?.length && (
535722
<DetailSection
536-
key={index}
537-
section={section}
723+
section={{
724+
fields: schema.fields,
725+
columns: schema.columns,
726+
}}
538727
data={{ ...data, ...editedValues }}
539728
objectSchema={objectSchema}
540729
isEditing={isInlineEditing}
541730
onFieldChange={handleInlineFieldChange}
542731
/>
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-
)}
732+
)}
733+
734+
{/* Tabs */}
735+
{schema.tabs && schema.tabs.length > 0 && (
736+
<DetailTabs tabs={schema.tabs} data={data} />
737+
)}
738+
739+
{/* Related Lists */}
740+
{effectiveRelated.length > 0 && (
741+
<div className="space-y-4">
742+
<h2 className="text-xl font-semibold">{t('detail.related')}</h2>
743+
{effectiveRelated.map((related, index) => (
744+
<RelatedList
745+
key={index}
746+
title={related.title}
747+
type={related.type}
748+
api={related.api}
749+
data={related.data}
750+
columns={related.columns as any}
751+
dataSource={dataSource}
752+
/>
753+
))}
754+
</div>
755+
)}
565756

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}
757+
{/* Comments */}
758+
{schema.comments && (
759+
<RecordComments
760+
comments={schema.comments}
761+
onAddComment={schema.onAddComment}
579762
/>
580-
))}
581-
</div>
582-
)}
583-
584-
{/* Comments */}
585-
{schema.comments && (
586-
<RecordComments
587-
comments={schema.comments}
588-
onAddComment={schema.onAddComment}
589-
/>
590-
)}
763+
)}
591764

592-
{/* Activity Timeline */}
593-
{schema.activities && schema.activities.length > 0 && (
594-
<ActivityTimeline activities={schema.activities} />
765+
{/* Activity Timeline */}
766+
{schema.activities && schema.activities.length > 0 && (
767+
<ActivityTimeline activities={schema.activities} />
768+
)}
769+
</>
595770
)}
596771

597772
{/* Custom Footer */}

0 commit comments

Comments
 (0)