task-p.5
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { useMutation, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { Icons } from '@/components/icons';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import {
|
||||
activateDocumentTemplateVersionMutation,
|
||||
archiveDocumentTemplateVersionMutation,
|
||||
compareDocumentTemplateVersionsMutation,
|
||||
previewDocumentTemplateVersionMutation,
|
||||
publishDocumentTemplateVersionMutation,
|
||||
rollbackDocumentTemplateVersionMutation,
|
||||
validateDocumentTemplateVersionMutation,
|
||||
} from '../mutations';
|
||||
import { documentTemplateVersionManagementQueryOptions } from '../queries';
|
||||
import type {
|
||||
DocumentTemplateVersionAuditSummary,
|
||||
DocumentTemplateVersionDetail,
|
||||
DocumentTemplateVersionValidationSummary,
|
||||
} from '../types';
|
||||
|
||||
function getStatusBadgeVariant(status: 'PASS' | 'WARNING' | 'FAIL' | 'NOT_RUN') {
|
||||
if (status === 'PASS') {
|
||||
return 'secondary' as const;
|
||||
}
|
||||
|
||||
if (status === 'FAIL') {
|
||||
return 'destructive' as const;
|
||||
}
|
||||
|
||||
return 'outline' as const;
|
||||
}
|
||||
|
||||
function getLifecycleBadgeVariant(
|
||||
status: DocumentTemplateVersionDetail['lifecycleStatus'],
|
||||
) {
|
||||
if (status === 'active') {
|
||||
return 'secondary' as const;
|
||||
}
|
||||
|
||||
if (status === 'archived') {
|
||||
return 'outline' as const;
|
||||
}
|
||||
|
||||
return 'default' as const;
|
||||
}
|
||||
|
||||
function SummaryCard({
|
||||
label,
|
||||
value,
|
||||
}: {
|
||||
label: string;
|
||||
value: React.ReactNode;
|
||||
}) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function IssueList({
|
||||
title,
|
||||
items,
|
||||
}: {
|
||||
title: string;
|
||||
items: string[];
|
||||
}) {
|
||||
if (!items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-2 rounded-lg border p-3'>
|
||||
<div className='text-sm font-medium'>{title}</div>
|
||||
<div className='space-y-1 text-sm'>
|
||||
{items.map((item) => (
|
||||
<div key={item} className='text-muted-foreground break-all'>
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimeIssueList({
|
||||
validation,
|
||||
}: {
|
||||
validation: DocumentTemplateVersionValidationSummary | null | undefined;
|
||||
}) {
|
||||
if (!validation?.runtimeIssues.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='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'>
|
||||
<Badge variant={issue.severity === 'error' ? 'destructive' : 'outline'}>
|
||||
{issue.severity}
|
||||
</Badge>
|
||||
<span className='font-medium'>{issue.code}</span>
|
||||
</div>
|
||||
<div className='text-muted-foreground mt-1'>{issue.message}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CompareDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
templateId,
|
||||
currentVersion,
|
||||
versions,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
templateId: string;
|
||||
currentVersion: DocumentTemplateVersionDetail;
|
||||
versions: DocumentTemplateVersionDetail[];
|
||||
}) {
|
||||
const [rightVersionId, setRightVersionId] = React.useState(
|
||||
versions.find((item) => item.id !== currentVersion.id)?.id ?? currentVersion.id,
|
||||
);
|
||||
const compareMutation = useMutation({
|
||||
...compareDocumentTemplateVersionsMutation,
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Compare failed'),
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (open) {
|
||||
setRightVersionId(
|
||||
versions.find((item) => item.id !== currentVersion.id)?.id ?? currentVersion.id,
|
||||
);
|
||||
}
|
||||
}, [currentVersion.id, open, versions]);
|
||||
|
||||
async function handleCompare() {
|
||||
const result = await compareMutation.mutateAsync({
|
||||
templateId,
|
||||
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);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Compare Versions</DialogTitle>
|
||||
<DialogDescription>
|
||||
Compare current version against another version in the same template family.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className='grid gap-4'>
|
||||
<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>
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<div className='text-sm font-medium'>Compare With</div>
|
||||
<select
|
||||
className='flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm'
|
||||
value={rightVersionId}
|
||||
onChange={(event) => setRightVersionId(event.target.value)}
|
||||
>
|
||||
{versions
|
||||
.filter((item) => item.id !== currentVersion.id)
|
||||
.map((version) => (
|
||||
<option key={version.id} value={version.id}>
|
||||
{version.version}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant='outline' onClick={() => onOpenChange(false)}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCompare}
|
||||
isLoading={compareMutation.isPending}
|
||||
disabled={!rightVersionId || rightVersionId === currentVersion.id}
|
||||
>
|
||||
Compare
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function TemplateVersionManagementPanel({
|
||||
templateId,
|
||||
version,
|
||||
versions,
|
||||
}: {
|
||||
templateId: string;
|
||||
version: DocumentTemplateVersionDetail;
|
||||
versions: DocumentTemplateVersionDetail[];
|
||||
}) {
|
||||
const { data } = useSuspenseQuery(
|
||||
documentTemplateVersionManagementQueryOptions(version.id),
|
||||
);
|
||||
const managedVersion = data.version;
|
||||
const [compareOpen, setCompareOpen] = React.useState(false);
|
||||
|
||||
const validateMutation = useMutation({
|
||||
...validateDocumentTemplateVersionMutation,
|
||||
onSuccess: (result) => toast.success(`Validation: ${result.validation.status}`),
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Validate failed'),
|
||||
});
|
||||
const publishMutation = useMutation({
|
||||
...publishDocumentTemplateVersionMutation,
|
||||
onSuccess: () => toast.success('Version published'),
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Publish failed'),
|
||||
});
|
||||
const activateMutation = useMutation({
|
||||
...activateDocumentTemplateVersionMutation,
|
||||
onSuccess: () => toast.success('Version activated'),
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Activate failed'),
|
||||
});
|
||||
const rollbackMutation = useMutation({
|
||||
...rollbackDocumentTemplateVersionMutation,
|
||||
onSuccess: () => toast.success('Rollback completed'),
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Rollback failed'),
|
||||
});
|
||||
const archiveMutation = useMutation({
|
||||
...archiveDocumentTemplateVersionMutation,
|
||||
onSuccess: () => toast.success('Version archived'),
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Archive failed'),
|
||||
});
|
||||
const previewMutation = useMutation({
|
||||
...previewDocumentTemplateVersionMutation,
|
||||
onSuccess: (result) => {
|
||||
const url = `data:${result.preview.contentType};base64,${result.preview.base64}`;
|
||||
window.open(url, '_blank', 'noopener,noreferrer');
|
||||
toast.success(`Preview ready (${result.preview.pageCount} pages)`);
|
||||
},
|
||||
onError: (error) =>
|
||||
toast.error(error instanceof Error ? error.message : 'Preview failed'),
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='space-y-4 rounded-lg border p-4'>
|
||||
<div className='flex flex-wrap items-start justify-between gap-3'>
|
||||
<div className='space-y-2'>
|
||||
<div className='font-medium'>Version Lifecycle Management</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Badge variant={getLifecycleBadgeVariant(managedVersion.lifecycleStatus)}>
|
||||
{managedVersion.lifecycleStatus ?? 'draft'}
|
||||
</Badge>
|
||||
<Badge variant={managedVersion.isActive ? 'secondary' : 'outline'}>
|
||||
{managedVersion.isActive ? 'Active Runtime' : 'Inactive Runtime'}
|
||||
</Badge>
|
||||
{validation ? (
|
||||
<Badge variant={getStatusBadgeVariant(validation.status)}>
|
||||
Validation {validation.status}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-wrap gap-2'>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => previewMutation.mutate(version.id)}
|
||||
isLoading={previewMutation.isPending}
|
||||
>
|
||||
<Icons.post className='mr-2 h-4 w-4' />
|
||||
Preview
|
||||
</Button>
|
||||
<Button variant='outline' onClick={() => setCompareOpen(true)}>
|
||||
<Icons.share className='mr-2 h-4 w-4' />
|
||||
Compare
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => validateMutation.mutate(version.id)}
|
||||
isLoading={validateMutation.isPending}
|
||||
>
|
||||
Validate
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => publishMutation.mutate({ templateId, versionId: version.id })}
|
||||
isLoading={publishMutation.isPending}
|
||||
>
|
||||
Publish
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => activateMutation.mutate({ templateId, versionId: version.id })}
|
||||
isLoading={activateMutation.isPending}
|
||||
>
|
||||
Activate
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => rollbackMutation.mutate({ templateId, versionId: version.id })}
|
||||
isLoading={rollbackMutation.isPending}
|
||||
>
|
||||
Rollback
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
onClick={() => archiveMutation.mutate({ templateId, versionId: version.id })}
|
||||
isLoading={archiveMutation.isPending}
|
||||
>
|
||||
Archive
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-3 sm:grid-cols-2 xl:grid-cols-4'>
|
||||
<SummaryCard
|
||||
label='Runtime Version'
|
||||
value={managedVersion.runtimeVersion ?? 'Not set'}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Template Variant'
|
||||
value={managedVersion.templateVariant ?? 'Not set'}
|
||||
/>
|
||||
<SummaryCard label='Brand' value={managedVersion.brand ?? 'Not set'} />
|
||||
<SummaryCard
|
||||
label='Published At'
|
||||
value={managedVersion.publishedAt ?? 'Not published'}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Activated At'
|
||||
value={managedVersion.activatedAt ?? 'Not activated'}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Archived At'
|
||||
value={managedVersion.archivedAt ?? 'Not archived'}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Previous Version'
|
||||
value={managedVersion.previousVersionId ?? 'None'}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Next Version'
|
||||
value={managedVersion.nextVersionId ?? 'None'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='space-y-3'>
|
||||
<div className='font-medium'>Validation Summary</div>
|
||||
{validation ? (
|
||||
<div className='space-y-3'>
|
||||
<div className='grid gap-3 sm:grid-cols-2 xl:grid-cols-4'>
|
||||
<SummaryCard
|
||||
label='Validation Status'
|
||||
value={<Badge variant={getStatusBadgeVariant(validation.status)}>{validation.status}</Badge>}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Validated At'
|
||||
value={validation.validatedAt ?? 'Not validated'}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Required Missing'
|
||||
value={validation.requiredFieldsMissing.length}
|
||||
/>
|
||||
<SummaryCard
|
||||
label='Unmapped Placeholders'
|
||||
value={validation.unmappedPlaceholders.length}
|
||||
/>
|
||||
</div>
|
||||
<IssueList
|
||||
title='Required Fields Missing'
|
||||
items={validation.requiredFieldsMissing}
|
||||
/>
|
||||
<IssueList title='Duplicate Fields' items={validation.duplicateFields} />
|
||||
<IssueList
|
||||
title='Unmapped Placeholders'
|
||||
items={validation.unmappedPlaceholders}
|
||||
/>
|
||||
<RuntimeIssueList validation={validation} />
|
||||
</div>
|
||||
) : (
|
||||
<div className='text-muted-foreground rounded-lg border border-dashed p-4 text-sm'>
|
||||
Validation has not been run for this version yet.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='space-y-3'>
|
||||
<div className='font-medium'>Audit Summary</div>
|
||||
{renderAuditSummary(audit)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CompareDialog
|
||||
open={compareOpen}
|
||||
onOpenChange={setCompareOpen}
|
||||
templateId={templateId}
|
||||
currentVersion={version}
|
||||
versions={versions}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user