task-p.5.1
This commit is contained in:
@@ -14,10 +14,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import {
|
||||
activateDocumentTemplateVersionMutation,
|
||||
archiveDocumentTemplateVersionMutation,
|
||||
compareDocumentTemplateVersionsMutation,
|
||||
duplicateDocumentTemplateVersionMutation,
|
||||
previewDocumentTemplateVersionMutation,
|
||||
publishDocumentTemplateVersionMutation,
|
||||
rollbackDocumentTemplateVersionMutation,
|
||||
@@ -26,8 +28,10 @@ import {
|
||||
import { documentTemplateVersionManagementQueryOptions } from '../queries';
|
||||
import type {
|
||||
DocumentTemplateVersionAuditSummary,
|
||||
DocumentTemplateVersionCompareResponse,
|
||||
DocumentTemplateVersionDetail,
|
||||
DocumentTemplateVersionValidationSummary,
|
||||
DocumentTemplateVersionVisualSummary,
|
||||
} from '../types';
|
||||
|
||||
function getStatusBadgeVariant(status: 'PASS' | 'WARNING' | 'FAIL' | 'NOT_RUN') {
|
||||
@@ -66,12 +70,12 @@ function SummaryCard({
|
||||
return (
|
||||
<div className='rounded-lg bg-muted/40 p-3'>
|
||||
<div className='text-muted-foreground text-xs'>{label}</div>
|
||||
<div className='mt-1 text-sm font-medium'>{value}</div>
|
||||
<div className='mt-1 text-sm font-medium break-words'>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function IssueList({
|
||||
function StringList({
|
||||
title,
|
||||
items,
|
||||
}: {
|
||||
@@ -96,28 +100,35 @@ function IssueList({
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimeIssueList({
|
||||
function ValidationIssues({
|
||||
validation,
|
||||
}: {
|
||||
validation: DocumentTemplateVersionValidationSummary | null | undefined;
|
||||
}) {
|
||||
if (!validation?.runtimeIssues.length) {
|
||||
if (!validation?.issues.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-2 rounded-lg border p-3'>
|
||||
<div className='text-sm font-medium'>Runtime Issues</div>
|
||||
<div className='text-sm font-medium'>Template Health Issues</div>
|
||||
<div className='space-y-2'>
|
||||
{validation.runtimeIssues.map((issue) => (
|
||||
<div key={`${issue.code}-${issue.message}`} className='rounded-md bg-muted/40 p-2 text-sm'>
|
||||
<div className='flex items-center gap-2'>
|
||||
{validation.issues.map((issue) => (
|
||||
<div
|
||||
key={`${issue.code}-${issue.location}-${issue.message}`}
|
||||
className='rounded-md bg-muted/40 p-3 text-sm'
|
||||
>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<Badge variant={issue.severity === 'error' ? 'destructive' : 'outline'}>
|
||||
{issue.severity}
|
||||
</Badge>
|
||||
<span className='font-medium'>{issue.code}</span>
|
||||
<span className='text-muted-foreground'>{issue.location}</span>
|
||||
</div>
|
||||
<div className='text-muted-foreground mt-1'>{issue.message}</div>
|
||||
<div className='mt-1'>{issue.message}</div>
|
||||
{issue.suggestedFix ? (
|
||||
<div className='text-muted-foreground mt-1'>{issue.suggestedFix}</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -125,6 +136,45 @@ function RuntimeIssueList({
|
||||
);
|
||||
}
|
||||
|
||||
function AuditSummary({
|
||||
audit,
|
||||
visual,
|
||||
}: {
|
||||
audit: DocumentTemplateVersionAuditSummary | null;
|
||||
visual: DocumentTemplateVersionVisualSummary | null | undefined;
|
||||
}) {
|
||||
if (!audit) {
|
||||
return (
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
||||
No audit summary available.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='grid gap-3 sm:grid-cols-2 xl:grid-cols-4'>
|
||||
<SummaryCard
|
||||
label='Audit'
|
||||
value={<Badge variant={getStatusBadgeVariant(audit.status)}>{audit.status}</Badge>}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Visual Regression'
|
||||
value={
|
||||
<Badge variant={getStatusBadgeVariant(audit.visualRegressionStatus)}>
|
||||
{audit.visualRegressionStatus}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
<SummaryCard label='Audited At' value={audit.auditedAt ?? 'Not available'} />
|
||||
<SummaryCard label='Runtime Version' value={audit.runtimeVersion ?? 'Not set'} />
|
||||
<SummaryCard label='Visual Generated' value={visual?.generatedAt ?? 'Not run'} />
|
||||
<SummaryCard label='Changed Pages' value={visual?.changedPages ?? 0} />
|
||||
<SummaryCard label='Layout Differences' value={visual?.layoutDifferences ?? 0} />
|
||||
<SummaryCard label='Visual Status' value={visual?.status ?? 'NOT_RUN'} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CompareDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -141,6 +191,10 @@ function CompareDialog({
|
||||
const [rightVersionId, setRightVersionId] = React.useState(
|
||||
versions.find((item) => item.id !== currentVersion.id)?.id ?? currentVersion.id,
|
||||
);
|
||||
const [comparison, setComparison] = React.useState<
|
||||
DocumentTemplateVersionCompareResponse['comparison'] | null
|
||||
>(null);
|
||||
|
||||
const compareMutation = useMutation({
|
||||
...compareDocumentTemplateVersionsMutation,
|
||||
onError: (error) =>
|
||||
@@ -149,6 +203,7 @@ function CompareDialog({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open) {
|
||||
setComparison(null);
|
||||
setRightVersionId(
|
||||
versions.find((item) => item.id !== currentVersion.id)?.id ?? currentVersion.id,
|
||||
);
|
||||
@@ -161,29 +216,21 @@ function CompareDialog({
|
||||
leftVersionId: currentVersion.id,
|
||||
rightVersionId,
|
||||
});
|
||||
|
||||
const metadataCount = result.comparison.metadataChanges.length;
|
||||
const placeholderCount =
|
||||
result.comparison.placeholderOnlyLeft.length +
|
||||
result.comparison.placeholderOnlyRight.length;
|
||||
const mappingCount = result.comparison.mappingDifferences.length;
|
||||
|
||||
toast.success(
|
||||
`Compare complete: ${metadataCount} metadata, ${placeholderCount} placeholders, ${mappingCount} mappings`,
|
||||
);
|
||||
onOpenChange(false);
|
||||
setComparison(result.comparison);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogContent className='max-h-[85vh] overflow-y-auto sm:max-w-4xl'>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Compare Versions</DialogTitle>
|
||||
<DialogDescription>
|
||||
Compare current version against another version in the same template family.
|
||||
Review metadata, placeholders, mappings, schema fields, and health state
|
||||
without opening raw JSON manually.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className='grid gap-4'>
|
||||
|
||||
<div className='grid gap-4 md:grid-cols-2'>
|
||||
<div className='space-y-2'>
|
||||
<div className='text-sm font-medium'>Base Version</div>
|
||||
<div className='rounded-lg border px-3 py-2 text-sm'>{currentVersion.version}</div>
|
||||
@@ -205,6 +252,123 @@ function CompareDialog({
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{comparison ? (
|
||||
<Tabs defaultValue='metadata' className='space-y-4'>
|
||||
<TabsList className='flex flex-wrap'>
|
||||
<TabsTrigger value='metadata'>Metadata</TabsTrigger>
|
||||
<TabsTrigger value='placeholders'>Placeholders</TabsTrigger>
|
||||
<TabsTrigger value='mappings'>Mappings</TabsTrigger>
|
||||
<TabsTrigger value='schema'>Schema</TabsTrigger>
|
||||
<TabsTrigger value='health'>Health</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value='metadata' className='space-y-3'>
|
||||
{comparison.metadataChanges.length ? (
|
||||
comparison.metadataChanges.map((change) => (
|
||||
<div key={change.field} className='rounded-lg border p-3 text-sm'>
|
||||
<div className='font-medium'>{change.field}</div>
|
||||
<div className='text-muted-foreground mt-1'>
|
||||
Left: {String(change.left ?? 'null')}
|
||||
</div>
|
||||
<div className='text-muted-foreground'>
|
||||
Right: {String(change.right ?? 'null')}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
||||
No metadata differences detected.
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='placeholders' className='space-y-3'>
|
||||
<StringList
|
||||
title='Only In Base Version'
|
||||
items={comparison.placeholderOnlyLeft}
|
||||
/>
|
||||
<StringList
|
||||
title='Only In Compared Version'
|
||||
items={comparison.placeholderOnlyRight}
|
||||
/>
|
||||
{!comparison.placeholderOnlyLeft.length &&
|
||||
!comparison.placeholderOnlyRight.length ? (
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
||||
Placeholder sets are identical.
|
||||
</div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='mappings' className='space-y-3'>
|
||||
{comparison.mappingDifferences.length ? (
|
||||
comparison.mappingDifferences.map((difference) => (
|
||||
<div
|
||||
key={`${difference.placeholderKey}-${difference.difference}`}
|
||||
className='rounded-lg border p-3 text-sm'
|
||||
>
|
||||
<div className='font-medium'>{difference.placeholderKey}</div>
|
||||
<div className='text-muted-foreground mt-1'>
|
||||
{difference.difference}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
||||
No mapping differences detected.
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='schema' className='space-y-3'>
|
||||
<StringList
|
||||
title='Added Fields'
|
||||
items={comparison.fieldSchemaChanges.addedFields}
|
||||
/>
|
||||
<StringList
|
||||
title='Removed Fields'
|
||||
items={comparison.fieldSchemaChanges.removedFields}
|
||||
/>
|
||||
{comparison.fieldSchemaChanges.changedFields.length ? (
|
||||
<div className='space-y-2 rounded-lg border p-3'>
|
||||
<div className='text-sm font-medium'>Changed Field Types</div>
|
||||
{comparison.fieldSchemaChanges.changedFields.map((item) => (
|
||||
<div key={item.field} className='text-muted-foreground text-sm'>
|
||||
{item.field}: {item.difference}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{comparison.jsonDiff.length ? (
|
||||
<div className='space-y-2 rounded-lg border p-3'>
|
||||
<div className='text-sm font-medium'>JSON Differences</div>
|
||||
{comparison.jsonDiff.map((item) => (
|
||||
<div key={item.path} className='text-muted-foreground text-sm'>
|
||||
{item.path}: {String(item.left ?? 'null')} {'->'}{' '}
|
||||
{String(item.right ?? 'null')}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value='health' className='space-y-3'>
|
||||
<div className='grid gap-3 md:grid-cols-2'>
|
||||
<SummaryCard
|
||||
label='Base Validation'
|
||||
value={comparison.leftValidation.status}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Compared Validation'
|
||||
value={comparison.rightValidation.status}
|
||||
/>
|
||||
</div>
|
||||
<ValidationIssues validation={comparison.leftValidation} />
|
||||
<ValidationIssues validation={comparison.rightValidation} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : null}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
@@ -267,6 +431,12 @@ export function TemplateVersionManagementPanel({
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Archive failed'),
|
||||
});
|
||||
const duplicateMutation = useMutation({
|
||||
...duplicateDocumentTemplateVersionMutation,
|
||||
onSuccess: () => toast.success('Draft duplicate created'),
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Duplicate failed'),
|
||||
});
|
||||
const previewMutation = useMutation({
|
||||
...previewDocumentTemplateVersionMutation,
|
||||
onSuccess: (result) => {
|
||||
@@ -280,37 +450,7 @@ export function TemplateVersionManagementPanel({
|
||||
|
||||
const validation = managedVersion.validationSummary ?? null;
|
||||
const audit = managedVersion.auditSummary ?? null;
|
||||
|
||||
function renderAuditSummary(auditSummary: DocumentTemplateVersionAuditSummary | null) {
|
||||
if (!auditSummary) {
|
||||
return <div className='text-muted-foreground text-sm'>No audit summary available.</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='grid gap-3 sm:grid-cols-2 xl:grid-cols-4'>
|
||||
<SummaryCard
|
||||
label='Audit'
|
||||
value={<Badge variant={getStatusBadgeVariant(auditSummary.status)}>{auditSummary.status}</Badge>}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Visual Regression'
|
||||
value={
|
||||
<Badge variant={getStatusBadgeVariant(auditSummary.visualRegressionStatus)}>
|
||||
{auditSummary.visualRegressionStatus}
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Audited At'
|
||||
value={auditSummary.auditedAt ?? 'Not available'}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Runtime Version'
|
||||
value={auditSummary.runtimeVersion ?? 'Not set'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const visual = managedVersion.visualSummary ?? null;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -332,6 +472,7 @@ export function TemplateVersionManagementPanel({
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
@@ -341,6 +482,17 @@ export function TemplateVersionManagementPanel({
|
||||
<Icons.post className='mr-2 h-4 w-4' />
|
||||
Preview
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() =>
|
||||
duplicateMutation.mutate({ templateId, versionId: version.id })
|
||||
}
|
||||
isLoading={duplicateMutation.isPending}
|
||||
disabled={managedVersion.lifecycleStatus === 'archived'}
|
||||
>
|
||||
<Icons.add className='mr-2 h-4 w-4' />
|
||||
Duplicate as Draft
|
||||
</Button>
|
||||
<Button variant='outline' onClick={() => setCompareOpen(true)}>
|
||||
<Icons.share className='mr-2 h-4 w-4' />
|
||||
Compare
|
||||
@@ -428,24 +580,34 @@ export function TemplateVersionManagementPanel({
|
||||
value={validation.validatedAt ?? 'Not validated'}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Required Missing'
|
||||
value={validation.requiredFieldsMissing.length}
|
||||
label='Health Status'
|
||||
value={validation.healthStatus}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Unmapped Placeholders'
|
||||
value={validation.unmappedPlaceholders.length}
|
||||
label='Suggested Next Step'
|
||||
value={validation.suggestedNextStep ?? '-'}
|
||||
/>
|
||||
</div>
|
||||
<IssueList
|
||||
|
||||
<StringList
|
||||
title='Required Fields Missing'
|
||||
items={validation.requiredFieldsMissing}
|
||||
/>
|
||||
<IssueList title='Duplicate Fields' items={validation.duplicateFields} />
|
||||
<IssueList
|
||||
<StringList title='Duplicate Fields' items={validation.duplicateFields} />
|
||||
<StringList title='Orphan Mappings' items={validation.orphanMappings} />
|
||||
<StringList
|
||||
title='Unmapped Placeholders'
|
||||
items={validation.unmappedPlaceholders}
|
||||
/>
|
||||
<RuntimeIssueList validation={validation} />
|
||||
<StringList
|
||||
title='Unsupported Schema Types'
|
||||
items={validation.unsupportedSchemaTypes}
|
||||
/>
|
||||
<StringList
|
||||
title='Section Marker Issues'
|
||||
items={validation.sectionMarkerIssues}
|
||||
/>
|
||||
<ValidationIssues validation={validation} />
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
||||
@@ -456,7 +618,7 @@ export function TemplateVersionManagementPanel({
|
||||
|
||||
<div className='space-y-3'>
|
||||
<div className='font-medium'>Audit Summary</div>
|
||||
{renderAuditSummary(audit)}
|
||||
<AuditSummary audit={audit} visual={visual} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user