commit task-fix-template

This commit is contained in:
phaichayon
2026-06-27 19:44:56 +07:00
parent 3fdd681dd0
commit 1c019d12f3
3 changed files with 703 additions and 31 deletions

View File

@@ -0,0 +1,542 @@
# Task Fix: Quotation Template JSON Viewer Overflow & Sheet Refactor
## Status
Planned Fix
## Objective
Fix the Quotation Template settings page layout overflow caused by rendering a full PDFME JSON schema inline.
The page should no longer render large JSON content directly in the main content area. Instead, each template version should expose a compact `View JSON` action that opens a scrollable Sheet viewer.
This fix improves:
* page readability
* button visibility
* layout stability
* JSON review experience
* future maintainability for large PDFME templates
---
## Problem
Current page:
```txt
/dashboard/crm/settings/templates
```
renders full JSON schema inline.
Observed issue:
* full JSON expands vertically and horizontally
* content overflows the visible page
* buttons/actions become hard to find
* page is difficult to operate
* JSON block dominates the whole settings page
Current pattern is likely similar to:
```tsx
<pre>{JSON.stringify(schemaJson, null, 2)}</pre>
```
or:
```tsx
<code>{JSON.stringify(version.schemaJson, null, 2)}</code>
```
This should be removed from the main page layout.
---
## Screenshot Context
Current UI shows:
```txt
Quotation Templates
Template Configuration Center
ALLA Quotation Standard
Version 1.0
Large inline JSON block
```
The large inline JSON causes the page to become visually noisy and may hide or push important actions away from the viewport.
---
## Mandatory Review
Review before implementation:
```txt
src/app/dashboard/crm/settings/templates/**
src/features/crm/document-templates/**
src/features/crm/quotations/**
src/components/ui/sheet.tsx
src/components/ui/button.tsx
```
Also search for inline JSON rendering:
```bash
rg "JSON.stringify|schemaJson|schema_json|<pre|<code" src/app/dashboard/crm/settings/templates src/features/crm/document-templates src/features/crm/quotations
```
---
## Required UX Change
Replace inline JSON rendering with a Sheet-based viewer.
Main page should display only compact information.
Each template version card/row should show:
```txt
Version
File path
Mapping count
Table column count
Active status
View JSON button
```
When user clicks:
```txt
View JSON
```
Open a right-side Sheet.
Sheet should show:
```txt
Title: Template Schema JSON
Description: version + file path
Actions: Copy JSON
Content: formatted JSON in a scrollable code block
```
---
## Scope 1: Remove Inline JSON From Main Page
Find the current inline JSON block and remove it from normal page flow.
Do not render full JSON directly inside:
```txt
Card
Accordion
Tabs
Main page content
```
Replace it with a compact action button.
Incorrect:
```tsx
<pre className="...">
{JSON.stringify(schemaJson, null, 2)}
</pre>
```
Correct:
```tsx
<TemplateJsonSheet
version={version.version}
filePath={version.filePath}
schemaJson={version.schemaJson}
/>
```
---
## Scope 2: Create Template JSON Sheet Component
Create component:
```txt
src/features/crm/document-templates/components/template-json-sheet.tsx
```
If the feature folder differs, place it near the current template settings components.
Component props:
```ts
type TemplateJsonSheetProps = {
version: string;
filePath?: string | null;
schemaJson: unknown;
triggerLabel?: string;
};
```
Required behavior:
* client component
* uses shadcn `Sheet`
* trigger button opens Sheet
* formats JSON with `JSON.stringify(schemaJson ?? {}, null, 2)`
* displays JSON inside scrollable area
* includes copy-to-clipboard button
* handles invalid or empty JSON safely
* does not crash if schemaJson is null
---
## Scope 3: Sheet Layout Rules
Use:
```tsx
<SheetContent side="right" className="w-full sm:max-w-4xl">
```
or if project layout requires:
```tsx
<SheetContent side="right" className="w-full max-w-[90vw] sm:max-w-5xl">
```
JSON block should use:
```txt
max-h-[calc(100vh-180px)]
overflow-auto
rounded-lg
border
bg-muted
p-4
font-mono
text-xs
whitespace-pre
```
Important:
* JSON scrolls inside Sheet
* main page does not grow because of JSON
* horizontal JSON overflow is contained inside Sheet
* Sheet remains usable on smaller screens
---
## Scope 4: Copy JSON Action
Add:
```txt
Copy JSON
```
Behavior:
* copy formatted JSON to clipboard
* show success toast if toast utility exists
* if no toast utility exists, copy silently or use existing notification pattern
Recommended:
```tsx
await navigator.clipboard.writeText(formattedJson);
toast.success('Copied JSON');
```
Rules:
* do not import a new toast library if project already has one
* use existing toast/sonner convention
---
## Scope 5: Compact Version Card UI
Refactor version display.
Instead of showing JSON inline, display a compact card section like:
```txt
Version 1.0
src/pdfme_template/ALLA_template_pdfme_fainal3.json
Mappings: 12
Table Columns: 24
Status: Active
[View JSON]
```
If mapping counts are already available, use them.
If not available, show only safe existing metadata.
Do not introduce extra server queries unless necessary.
---
## Scope 6: Empty / Missing JSON State
If `schemaJson` is missing:
Sheet should show:
```txt
No schema JSON available.
```
or:
```json
{}
```
Do not throw runtime error.
---
## Scope 7: Optional JSON Summary
If easy, add small metadata before the button:
```txt
Pages: n
Schemas: n
PDFME Version: x
```
Possible calculation:
```ts
const pageCount = Array.isArray(schemaJson?.schemas)
? schemaJson.schemas.length
: 0;
```
This is optional and should not block the fix.
---
## Scope 8: Accessibility
Requirements:
* button label must be clear
* Sheet title must exist
* code block should be selectable
* copy button should have visible text, not icon only
* no hidden overflow on the whole page
---
## Scope 9: Do Not Change Template Data Model
This is UI-only.
Do not change:
```txt
crm_document_templates
crm_document_template_versions
crm_document_template_mappings
crm_document_template_table_columns
PDFME schema format
template seed behavior
template upload behavior
quotation PDF generation
```
---
## Suggested Component Example
```tsx
'use client';
import { useMemo } from 'react';
import { Copy, Eye } from 'lucide-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger
} from '@/components/ui/sheet';
type TemplateJsonSheetProps = {
version: string;
filePath?: string | null;
schemaJson: unknown;
triggerLabel?: string;
};
export function TemplateJsonSheet({
version,
filePath,
schemaJson,
triggerLabel = 'View JSON'
}: TemplateJsonSheetProps) {
const formattedJson = useMemo(() => {
try {
return JSON.stringify(schemaJson ?? {}, null, 2);
} catch {
return '{}';
}
}, [schemaJson]);
const handleCopy = async () => {
await navigator.clipboard.writeText(formattedJson);
toast.success('Copied JSON');
};
return (
<Sheet>
<SheetTrigger asChild>
<Button variant='outline' size='sm'>
<Eye className='mr-2 h-4 w-4' />
{triggerLabel}
</Button>
</SheetTrigger>
<SheetContent side='right' className='w-full sm:max-w-4xl'>
<SheetHeader>
<SheetTitle>Template Schema JSON</SheetTitle>
<SheetDescription>
Version {version}
{filePath ? ` · ${filePath}` : ''}
</SheetDescription>
</SheetHeader>
<div className='mt-4 flex items-center justify-end'>
<Button variant='outline' size='sm' onClick={handleCopy}>
<Copy className='mr-2 h-4 w-4' />
Copy JSON
</Button>
</div>
<pre className='mt-4 max-h-[calc(100vh-180px)] overflow-auto rounded-lg border bg-muted p-4 font-mono text-xs whitespace-pre'>
<code>{formattedJson}</code>
</pre>
</SheetContent>
</Sheet>
);
}
```
Adjust import paths and formatting to match the project style.
---
## Scope 10: Page-Level Overflow Guard
Review parent containers.
Ensure the templates page does not use layout classes that create viewport overflow unexpectedly.
Recommended:
```txt
space-y-6
overflow-hidden only when intentional
min-w-0 on flex/grid children
```
If cards contain long paths, use:
```txt
truncate
break-all
```
For file paths:
```tsx
<p className="break-all text-sm text-muted-foreground">
{filePath}
</p>
```
---
## Verification
Run:
```bash
npm exec tsc --noEmit
npm run build
```
Manual verification:
```txt
Open /dashboard/crm/settings/templates
Page no longer shows full JSON inline
Page does not overflow because of JSON
Template version card remains compact
View JSON button is visible
Click View JSON
Sheet opens from right side
JSON displays formatted
JSON scrolls inside Sheet
Copy JSON works
Close Sheet
No regression in quotation PDF preview/download
```
Search verification:
```bash
rg "JSON.stringify|<pre|schemaJson|schema_json" src/app/dashboard/crm/settings/templates src/features/crm/document-templates src/features/crm/quotations
```
Allowed:
```txt
TemplateJsonSheet
intentional JSON formatting inside Sheet
server-side schema serialization
```
Not allowed:
```txt
inline full JSON rendering on main settings page
```
---
## Definition of Done
Task is complete when:
* Quotation Template settings page no longer renders full JSON inline
* JSON schema opens only through Sheet
* Sheet has independent scroll area
* Copy JSON action works
* Page actions remain visible
* Layout overflow is fixed
* TypeScript passes
* Build passes
Result:
```txt
Quotation Template JSON Viewer = Refactored
Template Settings Layout = Stable
Admin UX = Improved
```

View File

@@ -0,0 +1,104 @@
'use client';
import * as React from 'react';
import { IconCopy, IconEye } from '@tabler/icons-react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger
} from '@/components/ui/sheet';
type TemplateJsonSheetProps = {
version: string;
filePath?: string | null;
schemaJson: unknown;
triggerLabel?: string;
};
function formatSchemaJson(schemaJson: unknown) {
try {
return JSON.stringify(schemaJson ?? {}, null, 2);
} catch {
return '{}';
}
}
function getSchemaPageCount(schemaJson: unknown) {
if (
schemaJson &&
typeof schemaJson === 'object' &&
'schemas' in schemaJson &&
Array.isArray((schemaJson as { schemas?: unknown }).schemas)
) {
return (schemaJson as { schemas: unknown[] }).schemas.length;
}
return 0;
}
export function TemplateJsonSheet({
version,
filePath,
schemaJson,
triggerLabel = 'View JSON'
}: TemplateJsonSheetProps) {
const formattedJson = React.useMemo(() => formatSchemaJson(schemaJson), [schemaJson]);
const pageCount = React.useMemo(() => getSchemaPageCount(schemaJson), [schemaJson]);
const hasSchemaJson = formattedJson !== '{}';
async function handleCopy() {
try {
await navigator.clipboard.writeText(formattedJson);
toast.success('Copied JSON');
} catch {
toast.error('Unable to copy JSON');
}
}
return (
<Sheet>
<SheetTrigger asChild>
<Button variant='outline' size='sm'>
<IconEye className='size-4' />
{triggerLabel}
</Button>
</SheetTrigger>
<SheetContent side='right' className='w-full max-w-[90vw] sm:max-w-5xl'>
<SheetHeader>
<SheetTitle>Template Schema JSON</SheetTitle>
<SheetDescription>
Version {version}
{filePath ? ` | ${filePath}` : ''}
</SheetDescription>
</SheetHeader>
<div className='flex items-center justify-between gap-3'>
<div className='text-muted-foreground text-sm'>
{pageCount > 0 ? `Pages: ${pageCount}` : 'Schema metadata unavailable'}
</div>
<Button variant='outline' size='sm' onClick={handleCopy}>
<IconCopy className='size-4' />
Copy JSON
</Button>
</div>
{hasSchemaJson ? (
<pre className='bg-muted max-h-[calc(100vh-180px)] overflow-auto rounded-lg border p-4 font-mono text-xs whitespace-pre'>
<code>{formattedJson}</code>
</pre>
) : (
<div className='text-muted-foreground rounded-lg border border-dashed p-6 text-sm'>
No schema JSON available.
</div>
)}
</SheetContent>
</Sheet>
);
}

View File

@@ -18,6 +18,7 @@ import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { TemplateJsonSheet } from './template-json-sheet';
import { import {
createDocumentTemplateMappingMutation, createDocumentTemplateMappingMutation,
createDocumentTemplateMutation, createDocumentTemplateMutation,
@@ -182,6 +183,10 @@ function Field({
); );
} }
function getTableColumnCount(version: DocumentTemplateVersionDetail) {
return version.mappings.reduce((total, mapping) => total + mapping.columns.length, 0);
}
function TemplateDialog({ function TemplateDialog({
open, open,
onOpenChange, onOpenChange,
@@ -577,17 +582,41 @@ function TemplateDetailCard({ templateId }: { templateId: string }) {
{template.versions.map((version) => ( {template.versions.map((version) => (
<TabsContent key={version.id} value={version.id} className='space-y-4'> <TabsContent key={version.id} value={version.id} className='space-y-4'>
<div className='rounded-lg border p-4'> <div className='rounded-lg border p-4'>
<div className='flex flex-wrap items-center justify-between gap-3'> <div className='flex flex-wrap items-start justify-between gap-3'>
<div> <div className='min-w-0 flex-1 space-y-3'>
<div className='font-medium'>Version {version.version}</div> <div className='font-medium'>Version {version.version}</div>
<div className='text-muted-foreground text-sm'> <div className='grid gap-3 sm:grid-cols-2 xl:grid-cols-4'>
{version.filePath || 'No file path set'}{version.previewImageUrl ? `${version.previewImageUrl}` : ''} <div className='rounded-lg bg-muted/40 p-3'>
<div className='text-muted-foreground text-xs'>File Path</div>
<div className='mt-1 break-all text-sm font-medium'>
{version.filePath || 'No file path set'}
</div> </div>
</div> </div>
<div className='flex items-center gap-2'> <div className='rounded-lg bg-muted/40 p-3'>
<div className='text-muted-foreground text-xs'>Mappings</div>
<div className='mt-1 text-sm font-medium'>{version.mappings.length}</div>
</div>
<div className='rounded-lg bg-muted/40 p-3'>
<div className='text-muted-foreground text-xs'>Table Columns</div>
<div className='mt-1 text-sm font-medium'>{getTableColumnCount(version)}</div>
</div>
<div className='rounded-lg bg-muted/40 p-3'>
<div className='text-muted-foreground text-xs'>Preview Image</div>
<div className='mt-1 break-all text-sm font-medium'>
{version.previewImageUrl || '-'}
</div>
</div>
</div>
</div>
<div className='flex flex-wrap items-center gap-2'>
<Badge variant={version.isActive ? 'secondary' : 'outline'}> <Badge variant={version.isActive ? 'secondary' : 'outline'}>
{version.isActive ? 'Active' : 'Inactive'} {version.isActive ? 'Active' : 'Inactive'}
</Badge> </Badge>
<TemplateJsonSheet
version={version.version}
filePath={version.filePath}
schemaJson={version.schemaJson}
/>
<Button variant='outline' onClick={() => setVersionDialogOpen(true)}> <Button variant='outline' onClick={() => setVersionDialogOpen(true)}>
Edit Version Edit Version
</Button> </Button>
@@ -604,9 +633,6 @@ function TemplateDetailCard({ templateId }: { templateId: string }) {
</Button> </Button>
</div> </div>
</div> </div>
<pre className='bg-muted mt-4 max-h-72 overflow-auto rounded-lg p-4 text-xs'>
{JSON.stringify(version.schemaJson, null, 2)}
</pre>
</div> </div>
<div className='space-y-3'> <div className='space-y-3'>
{version.mappings.map((mapping) => ( {version.mappings.map((mapping) => (