mirror of
https://github.com/pdfme/pdfme.git
synced 2026-06-16 18:29:17 -04:00
feat(playground): standardize template metadata
This commit is contained in:
@@ -26,17 +26,17 @@ export const readAuthoringStarterFixtures = (kind: 'jsx' | 'md2pdf'): AuthoringS
|
||||
if (!fs.existsSync(sourcePath) || !fs.existsSync(metadataPath)) return [];
|
||||
|
||||
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8')) as {
|
||||
description?: string;
|
||||
sourceKind?: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
sourceKind: string;
|
||||
title: string;
|
||||
};
|
||||
if (metadata.sourceKind !== kind) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
assetName: entry.name,
|
||||
description: metadata.description ?? '',
|
||||
label: metadata.title ?? entry.name,
|
||||
description: metadata.description,
|
||||
label: metadata.title,
|
||||
source: fs.readFileSync(sourcePath, 'utf-8'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -110,7 +110,12 @@ describe('file workspace helpers', () => {
|
||||
invoice.addFile('template.json', serializeTemplateForFileWorkspace(blankTemplate));
|
||||
invoice.addFile(
|
||||
'metadata.json',
|
||||
JSON.stringify({ description: 'Invoice template', tags: ['Invoice'], title: 'Invoice' }),
|
||||
JSON.stringify({
|
||||
description: 'Invoice template',
|
||||
sourceKind: 'designer',
|
||||
tags: ['Invoice'],
|
||||
title: 'Invoice',
|
||||
}),
|
||||
);
|
||||
root
|
||||
.addDirectory('.cache')
|
||||
@@ -197,6 +202,8 @@ describe('file workspace helpers', () => {
|
||||
const templateFile = await directory.getFileHandle('template.json');
|
||||
|
||||
expect(entry.name).toBe('untitled-template');
|
||||
expect(entry.sourceKind).toBe('designer');
|
||||
expect(entry.title).toBe('Untitled Template');
|
||||
expect(templateFile.text).toContain('"schemas": [');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"order": 130,
|
||||
"title": "A4 Blank",
|
||||
"description": "A clean blank A4 document for starting from scratch in Designer.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Blank",
|
||||
"Starter"
|
||||
]
|
||||
],
|
||||
"order": 130
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Address Label 10",
|
||||
"description": "A 10-label address sheet for shipping and mailing workflows.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Labels",
|
||||
"Shipping"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Address Label 30",
|
||||
"description": "A compact 30-label address sheet for dense mailing labels.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Labels",
|
||||
"Shipping"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Address Label 6",
|
||||
"description": "A larger 6-label address sheet with room for longer addresses.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Labels",
|
||||
"Shipping"
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"order": 120,
|
||||
"title": "Certificate Black",
|
||||
"description": "A high-contrast certificate layout with a formal dark theme.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
]
|
||||
],
|
||||
"order": 120
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Certificate Blue",
|
||||
"description": "A polished blue certificate layout for awards and completion documents.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Certificate Gold",
|
||||
"description": "A warm gold certificate layout with a classic presentation style.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Certificate White",
|
||||
"description": "A minimal certificate layout that works well with light branding.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Social Insurance Enrollment Form",
|
||||
"description": "A Japanese social insurance form template for structured government-style documents.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Government",
|
||||
"CJK",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Inline Markdown MVT",
|
||||
"description": "A focused demo of inline markdown and MultiVariableText editing.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Markdown",
|
||||
"MVT",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Invoice Blue",
|
||||
"description": "A blue invoice variant for a more branded business document.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Invoice Green",
|
||||
"description": "A green invoice variant with a calm accounting-oriented look.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Japanese Invoice Landscape",
|
||||
"description": "A landscape Japanese invoice layout for wider table content.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Japanese Invoice",
|
||||
"description": "A simple Japanese invoice layout with CJK font usage.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Invoice White",
|
||||
"description": "A restrained white invoice layout with a clean printable style.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"order": 90,
|
||||
"title": "Invoice",
|
||||
"description": "A practical invoice with customer details, line items, totals, and payment notes.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"order": 90
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Location Arrow",
|
||||
"description": "A location marker template that highlights points with arrow indicators.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Map",
|
||||
"Visual"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Location Number",
|
||||
"description": "A location marker template that labels points with numbered badges.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Map",
|
||||
"Visual"
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"Business",
|
||||
"Table",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"title": "Invoice"
|
||||
},
|
||||
{
|
||||
"name": "quotes",
|
||||
@@ -54,7 +55,8 @@
|
||||
"Business",
|
||||
"Table",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"title": "Quotes"
|
||||
},
|
||||
{
|
||||
"name": "pedigree",
|
||||
@@ -80,7 +82,8 @@
|
||||
"QR",
|
||||
"Image",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"title": "Pedigree"
|
||||
},
|
||||
{
|
||||
"name": "certificate-black",
|
||||
@@ -103,7 +106,8 @@
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
]
|
||||
],
|
||||
"title": "Certificate Black"
|
||||
},
|
||||
{
|
||||
"name": "a4-blank",
|
||||
@@ -122,7 +126,8 @@
|
||||
"tags": [
|
||||
"Blank",
|
||||
"Starter"
|
||||
]
|
||||
],
|
||||
"title": "A4 Blank"
|
||||
},
|
||||
{
|
||||
"name": "qr-lines",
|
||||
@@ -144,7 +149,8 @@
|
||||
"tags": [
|
||||
"QR",
|
||||
"Label"
|
||||
]
|
||||
],
|
||||
"title": "QR Lines"
|
||||
},
|
||||
{
|
||||
"name": "address-label-10",
|
||||
@@ -164,7 +170,8 @@
|
||||
"tags": [
|
||||
"Labels",
|
||||
"Shipping"
|
||||
]
|
||||
],
|
||||
"title": "Address Label 10"
|
||||
},
|
||||
{
|
||||
"name": "address-label-30",
|
||||
@@ -184,7 +191,8 @@
|
||||
"tags": [
|
||||
"Labels",
|
||||
"Shipping"
|
||||
]
|
||||
],
|
||||
"title": "Address Label 30"
|
||||
},
|
||||
{
|
||||
"name": "address-label-6",
|
||||
@@ -204,7 +212,8 @@
|
||||
"tags": [
|
||||
"Labels",
|
||||
"Shipping"
|
||||
]
|
||||
],
|
||||
"title": "Address Label 6"
|
||||
},
|
||||
{
|
||||
"name": "md2pdf-article",
|
||||
@@ -251,7 +260,8 @@
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
]
|
||||
],
|
||||
"title": "Certificate Blue"
|
||||
},
|
||||
{
|
||||
"name": "certificate-gold",
|
||||
@@ -273,7 +283,8 @@
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
]
|
||||
],
|
||||
"title": "Certificate Gold"
|
||||
},
|
||||
{
|
||||
"name": "certificate-white",
|
||||
@@ -295,7 +306,8 @@
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
]
|
||||
],
|
||||
"title": "Certificate White"
|
||||
},
|
||||
{
|
||||
"name": "jsx-form-fields",
|
||||
@@ -325,29 +337,6 @@
|
||||
],
|
||||
"title": "Form fields"
|
||||
},
|
||||
{
|
||||
"name": "hihokensha-shikaku-shutoku-todoke",
|
||||
"author": "EedgeY",
|
||||
"path": "hihokensha-shikaku-shutoku-todoke/template.json",
|
||||
"thumbnailPath": "hihokensha-shikaku-shutoku-todoke/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 104,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [
|
||||
"NotoSansJP"
|
||||
],
|
||||
"hasCJK": true,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A Japanese social insurance form template for structured government-style documents.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Government",
|
||||
"CJK",
|
||||
"Form"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "inline-markdown-mvt",
|
||||
"author": "pdfme",
|
||||
@@ -370,7 +359,52 @@
|
||||
"Markdown",
|
||||
"MVT",
|
||||
"Form"
|
||||
]
|
||||
],
|
||||
"title": "Inline Markdown MVT"
|
||||
},
|
||||
{
|
||||
"name": "invoice-blue",
|
||||
"author": "pdfme",
|
||||
"path": "invoice-blue/template.json",
|
||||
"thumbnailPath": "invoice-blue/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 35,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A blue invoice variant for a more branded business document.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table"
|
||||
],
|
||||
"title": "Invoice Blue"
|
||||
},
|
||||
{
|
||||
"name": "invoice-green",
|
||||
"author": "pdfme",
|
||||
"path": "invoice-green/template.json",
|
||||
"thumbnailPath": "invoice-green/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 25,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A green invoice variant with a calm accounting-oriented look.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table"
|
||||
],
|
||||
"title": "Invoice Green"
|
||||
},
|
||||
{
|
||||
"name": "jsx-invoice",
|
||||
@@ -406,10 +440,10 @@
|
||||
"title": "Invoice layout"
|
||||
},
|
||||
{
|
||||
"name": "invoice-blue",
|
||||
"name": "invoice-white",
|
||||
"author": "pdfme",
|
||||
"path": "invoice-blue/template.json",
|
||||
"thumbnailPath": "invoice-blue/thumbnail.png",
|
||||
"path": "invoice-white/template.json",
|
||||
"thumbnailPath": "invoice-white/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 35,
|
||||
"schemaTypes": [
|
||||
@@ -418,34 +452,14 @@
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A blue invoice variant for a more branded business document.",
|
||||
"description": "A restrained white invoice layout with a clean printable style.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "invoice-green",
|
||||
"author": "pdfme",
|
||||
"path": "invoice-green/template.json",
|
||||
"thumbnailPath": "invoice-green/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 25,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A green invoice variant with a calm accounting-oriented look.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table"
|
||||
]
|
||||
"title": "Invoice White"
|
||||
},
|
||||
{
|
||||
"name": "invoice-ja-simple",
|
||||
@@ -472,7 +486,8 @@
|
||||
"Invoice",
|
||||
"Business",
|
||||
"CJK"
|
||||
]
|
||||
],
|
||||
"title": "Japanese Invoice"
|
||||
},
|
||||
{
|
||||
"name": "invoice-ja-simple-landscape",
|
||||
@@ -499,28 +514,8 @@
|
||||
"Invoice",
|
||||
"Business",
|
||||
"CJK"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "invoice-white",
|
||||
"author": "pdfme",
|
||||
"path": "invoice-white/template.json",
|
||||
"thumbnailPath": "invoice-white/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 35,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A restrained white invoice layout with a clean printable style.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table"
|
||||
]
|
||||
"title": "Japanese Invoice Landscape"
|
||||
},
|
||||
{
|
||||
"name": "jsx-japanese-notice",
|
||||
@@ -594,7 +589,8 @@
|
||||
"tags": [
|
||||
"Map",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"title": "Location Arrow"
|
||||
},
|
||||
{
|
||||
"name": "location-number",
|
||||
@@ -615,32 +611,8 @@
|
||||
"tags": [
|
||||
"Map",
|
||||
"Visual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "new-sale-quotation",
|
||||
"author": "pdfme",
|
||||
"path": "new-sale-quotation/template.json",
|
||||
"thumbnailPath": "new-sale-quotation/thumbnail.png",
|
||||
"pageCount": 3,
|
||||
"fieldCount": 49,
|
||||
"schemaTypes": [
|
||||
"image",
|
||||
"line",
|
||||
"rectangle",
|
||||
"table",
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "blank",
|
||||
"description": "A sales quotation template with product rows and business summary fields.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Quote",
|
||||
"Business",
|
||||
"Table"
|
||||
]
|
||||
"title": "Location Number"
|
||||
},
|
||||
{
|
||||
"name": "md2pdf-overview",
|
||||
@@ -689,7 +661,8 @@
|
||||
"tags": [
|
||||
"QR",
|
||||
"Label"
|
||||
]
|
||||
],
|
||||
"title": "QR Title"
|
||||
},
|
||||
{
|
||||
"name": "md2pdf-release-notes",
|
||||
@@ -771,6 +744,56 @@
|
||||
],
|
||||
"title": "Research paper"
|
||||
},
|
||||
{
|
||||
"name": "new-sale-quotation",
|
||||
"author": "pdfme",
|
||||
"path": "new-sale-quotation/template.json",
|
||||
"thumbnailPath": "new-sale-quotation/thumbnail.png",
|
||||
"pageCount": 3,
|
||||
"fieldCount": 49,
|
||||
"schemaTypes": [
|
||||
"image",
|
||||
"line",
|
||||
"rectangle",
|
||||
"table",
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "blank",
|
||||
"description": "A sales quotation template with product rows and business summary fields.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Quote",
|
||||
"Business",
|
||||
"Table"
|
||||
],
|
||||
"title": "Sales Quotation"
|
||||
},
|
||||
{
|
||||
"name": "hihokensha-shikaku-shutoku-todoke",
|
||||
"author": "EedgeY",
|
||||
"path": "hihokensha-shikaku-shutoku-todoke/template.json",
|
||||
"thumbnailPath": "hihokensha-shikaku-shutoku-todoke/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 104,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [
|
||||
"NotoSansJP"
|
||||
],
|
||||
"hasCJK": true,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A Japanese social insurance form template for structured government-style documents.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Government",
|
||||
"CJK",
|
||||
"Form"
|
||||
],
|
||||
"title": "Social Insurance Enrollment Form"
|
||||
},
|
||||
{
|
||||
"name": "z-fold-brochure",
|
||||
"author": "hitomi-t260g",
|
||||
@@ -796,7 +819,8 @@
|
||||
"tags": [
|
||||
"Brochure",
|
||||
"Print"
|
||||
]
|
||||
],
|
||||
"title": "Z-Fold Brochure"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -27,7 +27,8 @@
|
||||
"Business",
|
||||
"Table",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"title": "Invoice"
|
||||
},
|
||||
{
|
||||
"name": "quotes",
|
||||
@@ -54,7 +55,8 @@
|
||||
"Business",
|
||||
"Table",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"title": "Quotes"
|
||||
},
|
||||
{
|
||||
"name": "pedigree",
|
||||
@@ -80,7 +82,8 @@
|
||||
"QR",
|
||||
"Image",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"title": "Pedigree"
|
||||
},
|
||||
{
|
||||
"name": "certificate-black",
|
||||
@@ -103,7 +106,8 @@
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
]
|
||||
],
|
||||
"title": "Certificate Black"
|
||||
},
|
||||
{
|
||||
"name": "a4-blank",
|
||||
@@ -122,7 +126,8 @@
|
||||
"tags": [
|
||||
"Blank",
|
||||
"Starter"
|
||||
]
|
||||
],
|
||||
"title": "A4 Blank"
|
||||
},
|
||||
{
|
||||
"name": "qr-lines",
|
||||
@@ -144,7 +149,8 @@
|
||||
"tags": [
|
||||
"QR",
|
||||
"Label"
|
||||
]
|
||||
],
|
||||
"title": "QR Lines"
|
||||
},
|
||||
{
|
||||
"name": "address-label-10",
|
||||
@@ -164,7 +170,8 @@
|
||||
"tags": [
|
||||
"Labels",
|
||||
"Shipping"
|
||||
]
|
||||
],
|
||||
"title": "Address Label 10"
|
||||
},
|
||||
{
|
||||
"name": "address-label-30",
|
||||
@@ -184,7 +191,8 @@
|
||||
"tags": [
|
||||
"Labels",
|
||||
"Shipping"
|
||||
]
|
||||
],
|
||||
"title": "Address Label 30"
|
||||
},
|
||||
{
|
||||
"name": "address-label-6",
|
||||
@@ -204,7 +212,8 @@
|
||||
"tags": [
|
||||
"Labels",
|
||||
"Shipping"
|
||||
]
|
||||
],
|
||||
"title": "Address Label 6"
|
||||
},
|
||||
{
|
||||
"name": "md2pdf-article",
|
||||
@@ -251,7 +260,8 @@
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
]
|
||||
],
|
||||
"title": "Certificate Blue"
|
||||
},
|
||||
{
|
||||
"name": "certificate-gold",
|
||||
@@ -273,7 +283,8 @@
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
]
|
||||
],
|
||||
"title": "Certificate Gold"
|
||||
},
|
||||
{
|
||||
"name": "certificate-white",
|
||||
@@ -295,7 +306,8 @@
|
||||
"tags": [
|
||||
"Certificate",
|
||||
"Award"
|
||||
]
|
||||
],
|
||||
"title": "Certificate White"
|
||||
},
|
||||
{
|
||||
"name": "jsx-form-fields",
|
||||
@@ -325,29 +337,6 @@
|
||||
],
|
||||
"title": "Form fields"
|
||||
},
|
||||
{
|
||||
"name": "hihokensha-shikaku-shutoku-todoke",
|
||||
"author": "EedgeY",
|
||||
"path": "hihokensha-shikaku-shutoku-todoke/template.json",
|
||||
"thumbnailPath": "hihokensha-shikaku-shutoku-todoke/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 104,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [
|
||||
"NotoSansJP"
|
||||
],
|
||||
"hasCJK": true,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A Japanese social insurance form template for structured government-style documents.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Government",
|
||||
"CJK",
|
||||
"Form"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "inline-markdown-mvt",
|
||||
"author": "pdfme",
|
||||
@@ -370,7 +359,52 @@
|
||||
"Markdown",
|
||||
"MVT",
|
||||
"Form"
|
||||
]
|
||||
],
|
||||
"title": "Inline Markdown MVT"
|
||||
},
|
||||
{
|
||||
"name": "invoice-blue",
|
||||
"author": "pdfme",
|
||||
"path": "invoice-blue/template.json",
|
||||
"thumbnailPath": "invoice-blue/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 35,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A blue invoice variant for a more branded business document.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table"
|
||||
],
|
||||
"title": "Invoice Blue"
|
||||
},
|
||||
{
|
||||
"name": "invoice-green",
|
||||
"author": "pdfme",
|
||||
"path": "invoice-green/template.json",
|
||||
"thumbnailPath": "invoice-green/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 25,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A green invoice variant with a calm accounting-oriented look.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table"
|
||||
],
|
||||
"title": "Invoice Green"
|
||||
},
|
||||
{
|
||||
"name": "jsx-invoice",
|
||||
@@ -406,10 +440,10 @@
|
||||
"title": "Invoice layout"
|
||||
},
|
||||
{
|
||||
"name": "invoice-blue",
|
||||
"name": "invoice-white",
|
||||
"author": "pdfme",
|
||||
"path": "invoice-blue/template.json",
|
||||
"thumbnailPath": "invoice-blue/thumbnail.png",
|
||||
"path": "invoice-white/template.json",
|
||||
"thumbnailPath": "invoice-white/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 35,
|
||||
"schemaTypes": [
|
||||
@@ -418,34 +452,14 @@
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A blue invoice variant for a more branded business document.",
|
||||
"description": "A restrained white invoice layout with a clean printable style.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "invoice-green",
|
||||
"author": "pdfme",
|
||||
"path": "invoice-green/template.json",
|
||||
"thumbnailPath": "invoice-green/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 25,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A green invoice variant with a calm accounting-oriented look.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table"
|
||||
]
|
||||
"title": "Invoice White"
|
||||
},
|
||||
{
|
||||
"name": "invoice-ja-simple",
|
||||
@@ -472,7 +486,8 @@
|
||||
"Invoice",
|
||||
"Business",
|
||||
"CJK"
|
||||
]
|
||||
],
|
||||
"title": "Japanese Invoice"
|
||||
},
|
||||
{
|
||||
"name": "invoice-ja-simple-landscape",
|
||||
@@ -499,28 +514,8 @@
|
||||
"Invoice",
|
||||
"Business",
|
||||
"CJK"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "invoice-white",
|
||||
"author": "pdfme",
|
||||
"path": "invoice-white/template.json",
|
||||
"thumbnailPath": "invoice-white/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 35,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A restrained white invoice layout with a clean printable style.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Invoice",
|
||||
"Business",
|
||||
"Table"
|
||||
]
|
||||
"title": "Japanese Invoice Landscape"
|
||||
},
|
||||
{
|
||||
"name": "jsx-japanese-notice",
|
||||
@@ -594,7 +589,8 @@
|
||||
"tags": [
|
||||
"Map",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"title": "Location Arrow"
|
||||
},
|
||||
{
|
||||
"name": "location-number",
|
||||
@@ -615,32 +611,8 @@
|
||||
"tags": [
|
||||
"Map",
|
||||
"Visual"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "new-sale-quotation",
|
||||
"author": "pdfme",
|
||||
"path": "new-sale-quotation/template.json",
|
||||
"thumbnailPath": "new-sale-quotation/thumbnail.png",
|
||||
"pageCount": 3,
|
||||
"fieldCount": 49,
|
||||
"schemaTypes": [
|
||||
"image",
|
||||
"line",
|
||||
"rectangle",
|
||||
"table",
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "blank",
|
||||
"description": "A sales quotation template with product rows and business summary fields.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Quote",
|
||||
"Business",
|
||||
"Table"
|
||||
]
|
||||
"title": "Location Number"
|
||||
},
|
||||
{
|
||||
"name": "md2pdf-overview",
|
||||
@@ -689,7 +661,8 @@
|
||||
"tags": [
|
||||
"QR",
|
||||
"Label"
|
||||
]
|
||||
],
|
||||
"title": "QR Title"
|
||||
},
|
||||
{
|
||||
"name": "md2pdf-release-notes",
|
||||
@@ -771,6 +744,56 @@
|
||||
],
|
||||
"title": "Research paper"
|
||||
},
|
||||
{
|
||||
"name": "new-sale-quotation",
|
||||
"author": "pdfme",
|
||||
"path": "new-sale-quotation/template.json",
|
||||
"thumbnailPath": "new-sale-quotation/thumbnail.png",
|
||||
"pageCount": 3,
|
||||
"fieldCount": 49,
|
||||
"schemaTypes": [
|
||||
"image",
|
||||
"line",
|
||||
"rectangle",
|
||||
"table",
|
||||
"text"
|
||||
],
|
||||
"fontNames": [],
|
||||
"hasCJK": false,
|
||||
"basePdfKind": "blank",
|
||||
"description": "A sales quotation template with product rows and business summary fields.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Quote",
|
||||
"Business",
|
||||
"Table"
|
||||
],
|
||||
"title": "Sales Quotation"
|
||||
},
|
||||
{
|
||||
"name": "hihokensha-shikaku-shutoku-todoke",
|
||||
"author": "EedgeY",
|
||||
"path": "hihokensha-shikaku-shutoku-todoke/template.json",
|
||||
"thumbnailPath": "hihokensha-shikaku-shutoku-todoke/thumbnail.png",
|
||||
"pageCount": 1,
|
||||
"fieldCount": 104,
|
||||
"schemaTypes": [
|
||||
"text"
|
||||
],
|
||||
"fontNames": [
|
||||
"NotoSansJP"
|
||||
],
|
||||
"hasCJK": true,
|
||||
"basePdfKind": "dataUri",
|
||||
"description": "A Japanese social insurance form template for structured government-style documents.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Government",
|
||||
"CJK",
|
||||
"Form"
|
||||
],
|
||||
"title": "Social Insurance Enrollment Form"
|
||||
},
|
||||
{
|
||||
"name": "z-fold-brochure",
|
||||
"author": "hitomi-t260g",
|
||||
@@ -796,7 +819,8 @@
|
||||
"tags": [
|
||||
"Brochure",
|
||||
"Print"
|
||||
]
|
||||
],
|
||||
"title": "Z-Fold Brochure"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Sales Quotation",
|
||||
"description": "A sales quotation template with product rows and business summary fields.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Quote",
|
||||
"Business",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"order": 110,
|
||||
"title": "Pedigree",
|
||||
"description": "A pedigree-style relationship chart for structured family or lineage data.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Chart",
|
||||
"QR",
|
||||
"Image",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"order": 110
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"order": 140,
|
||||
"title": "QR Lines",
|
||||
"description": "A QR code template with line-based metadata and compact supporting text.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"QR",
|
||||
"Label"
|
||||
]
|
||||
],
|
||||
"order": 140
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "QR Title",
|
||||
"description": "A QR code template with a prominent title and simple scan instructions.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"QR",
|
||||
"Label"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"order": 100,
|
||||
"title": "Quotes",
|
||||
"description": "A quote document layout for proposals, estimates, and short commercial offers.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Quote",
|
||||
"Business",
|
||||
"Table",
|
||||
"Visual"
|
||||
]
|
||||
],
|
||||
"order": 100
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"title": "Z-Fold Brochure",
|
||||
"description": "A z-fold brochure layout for tri-fold print and promotional material.",
|
||||
"sourceKind": "designer",
|
||||
"tags": [
|
||||
"Brochure",
|
||||
"Print"
|
||||
|
||||
@@ -90,7 +90,9 @@ function normalizeMetadata(rawMetadata) {
|
||||
}
|
||||
|
||||
const metadata = {};
|
||||
if (typeof rawMetadata.title === 'string') metadata.title = rawMetadata.title;
|
||||
if (typeof rawMetadata.title === 'string' && rawMetadata.title.trim()) {
|
||||
metadata.title = rawMetadata.title.trim();
|
||||
}
|
||||
if (typeof rawMetadata.description === 'string') metadata.description = rawMetadata.description;
|
||||
if (typeof rawMetadata.order === 'number' && Number.isFinite(rawMetadata.order)) {
|
||||
metadata.order = rawMetadata.order;
|
||||
@@ -127,15 +129,21 @@ function validateTemplateMetadata(metadataByTemplate, templateDirs) {
|
||||
|
||||
for (const [name, rawMetadata] of Object.entries(metadataByTemplate)) {
|
||||
const metadata = normalizeMetadata(rawMetadata);
|
||||
if (!metadata.title) {
|
||||
throw new Error(`template asset metadata entry "${name}" must include title.`);
|
||||
}
|
||||
if (!metadata.description) {
|
||||
throw new Error(`template asset metadata entry "${name}" must include description.`);
|
||||
}
|
||||
if (!metadata.sourceKind) {
|
||||
throw new Error(`template asset metadata entry "${name}" must include sourceKind.`);
|
||||
}
|
||||
if (!metadata.tags || metadata.tags.length === 0) {
|
||||
throw new Error(`template asset metadata entry "${name}" must include tags.`);
|
||||
}
|
||||
|
||||
const inferredSourceKind = inferSourceKind(name);
|
||||
if (metadata.sourceKind && metadata.sourceKind !== inferredSourceKind) {
|
||||
if (metadata.sourceKind !== inferredSourceKind) {
|
||||
throw new Error(
|
||||
`template asset metadata entry "${name}" has sourceKind "${metadata.sourceKind}", expected "${inferredSourceKind}".`,
|
||||
);
|
||||
@@ -167,7 +175,13 @@ function buildTemplateEntry(name, templateJson, rawMetadata) {
|
||||
const schemas = normalizeSchemas(templateJson.schemas);
|
||||
const flattenedSchemas = schemas.flat();
|
||||
const metadata = normalizeMetadata(rawMetadata);
|
||||
const sourceKind = metadata.sourceKind ?? inferSourceKind(name);
|
||||
if (!metadata.title) {
|
||||
throw new Error(`template asset metadata entry "${name}" must include title.`);
|
||||
}
|
||||
if (!metadata.sourceKind) {
|
||||
throw new Error(`template asset metadata entry "${name}" must include sourceKind.`);
|
||||
}
|
||||
const sourceKind = metadata.sourceKind;
|
||||
const schemaTypes = [
|
||||
...new Set(flattenedSchemas.map((schema) => schema.type).filter(Boolean)),
|
||||
].sort();
|
||||
@@ -190,7 +204,7 @@ function buildTemplateEntry(name, templateJson, rawMetadata) {
|
||||
description: metadata.description,
|
||||
order: metadata.order,
|
||||
sourceKind,
|
||||
tags: metadata.tags ?? [],
|
||||
tags: metadata.tags,
|
||||
title: metadata.title,
|
||||
};
|
||||
}
|
||||
@@ -212,9 +226,7 @@ function compareTemplateEntries(a, b) {
|
||||
if (a.order != null) return -1;
|
||||
if (b.order != null) return 1;
|
||||
|
||||
const aTitle = a.title ?? a.name;
|
||||
const bTitle = b.title ?? b.name;
|
||||
const titleResult = aTitle.localeCompare(bTitle);
|
||||
const titleResult = a.title.localeCompare(b.title);
|
||||
if (titleResult !== 0) return titleResult;
|
||||
|
||||
return a.name.localeCompare(b.name);
|
||||
|
||||
@@ -10,12 +10,20 @@ import { Form, Viewer, Designer } from '@pdfme/ui';
|
||||
import { generate, generateForm } from '@pdfme/generator';
|
||||
import { getPlugins } from './plugins';
|
||||
|
||||
export function fromKebabCase(str: string): string {
|
||||
return str
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
const templateAssetSourceKinds = ['designer', 'jsx', 'md2pdf'] as const;
|
||||
|
||||
type TemplateAssetSourceKind = (typeof templateAssetSourceKinds)[number];
|
||||
|
||||
export type TemplateAssetMetadata = {
|
||||
description: string;
|
||||
order?: number;
|
||||
sourceKind: TemplateAssetSourceKind;
|
||||
tags: string[];
|
||||
title: string;
|
||||
};
|
||||
|
||||
const isTemplateAssetSourceKind = (value: unknown): value is TemplateAssetSourceKind =>
|
||||
templateAssetSourceKinds.includes(value as TemplateAssetSourceKind);
|
||||
|
||||
export const getFontsData = (): Font => ({
|
||||
...getDefaultFont(),
|
||||
@@ -150,4 +158,37 @@ export const getTemplateById = async (templateId: string): Promise<Template> =>
|
||||
return template as Template;
|
||||
};
|
||||
|
||||
export const getTemplateMetadataById = async (
|
||||
templateId: string,
|
||||
): Promise<TemplateAssetMetadata> => {
|
||||
const response = await fetch(`/template-assets/${templateId}/metadata.json`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load template metadata: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const metadata = (await response.json()) as Partial<TemplateAssetMetadata>;
|
||||
if (!metadata.title?.trim()) {
|
||||
throw new Error(`Template metadata "${templateId}" must include title.`);
|
||||
}
|
||||
if (!metadata.description?.trim()) {
|
||||
throw new Error(`Template metadata "${templateId}" must include description.`);
|
||||
}
|
||||
if (!isTemplateAssetSourceKind(metadata.sourceKind)) {
|
||||
throw new Error(`Template metadata "${templateId}" must include sourceKind.`);
|
||||
}
|
||||
if (!metadata.tags || metadata.tags.length === 0) {
|
||||
throw new Error(`Template metadata "${templateId}" must include tags.`);
|
||||
}
|
||||
|
||||
return {
|
||||
description: metadata.description,
|
||||
order: metadata.order,
|
||||
sourceKind: metadata.sourceKind,
|
||||
tags: metadata.tags,
|
||||
title: metadata.title.trim(),
|
||||
};
|
||||
};
|
||||
|
||||
export const getDefaultPlaygroundTemplate = () => getTemplateById(DEFAULT_PLAYGROUND_TEMPLATE_ID);
|
||||
export const getDefaultPlaygroundTemplateMetadata = () =>
|
||||
getTemplateMetadataById(DEFAULT_PLAYGROUND_TEMPLATE_ID);
|
||||
|
||||
@@ -2,7 +2,7 @@ export type AuthoringStarterKind = 'jsx' | 'md2pdf';
|
||||
|
||||
export type AuthoringStarter = {
|
||||
assetName: string;
|
||||
description?: string;
|
||||
description: string;
|
||||
id: string;
|
||||
kind: AuthoringStarterKind;
|
||||
label: string;
|
||||
@@ -10,11 +10,11 @@ export type AuthoringStarter = {
|
||||
};
|
||||
|
||||
type TemplateAssetEntry = {
|
||||
description?: string;
|
||||
description: string;
|
||||
name: string;
|
||||
sourceKind?: string;
|
||||
sourceKind: string;
|
||||
sourcePath?: string;
|
||||
title?: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
const TEMPLATE_ASSETS_BASE_PATH = '/template-assets';
|
||||
@@ -40,7 +40,7 @@ export const loadAuthoringStarters = async (
|
||||
description: entry.description,
|
||||
id: getAuthoringStarterId(entry.name, kind),
|
||||
kind,
|
||||
label: entry.title ?? entry.name,
|
||||
label: entry.title,
|
||||
sourcePath: entry.sourcePath!,
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -17,7 +17,7 @@ type SourceKind = 'designer' | 'jsx' | 'md2pdf';
|
||||
export type FileWorkspaceMetadata = {
|
||||
description?: string;
|
||||
order?: number;
|
||||
sourceKind?: SourceKind;
|
||||
sourceKind: SourceKind;
|
||||
tags: string[];
|
||||
title?: string;
|
||||
};
|
||||
@@ -393,7 +393,7 @@ const buildTemplateEntry = async (
|
||||
name,
|
||||
order: metadata.order,
|
||||
path: `${name}/template.json`,
|
||||
sourceKind: metadata.sourceKind ?? inferSourceKind(name),
|
||||
sourceKind: metadata.sourceKind,
|
||||
tags: metadata.tags,
|
||||
template: readResult.template,
|
||||
templateDirectoryHandle: directoryHandle,
|
||||
@@ -585,7 +585,7 @@ export const subscribeTemplateEntryChanges = (
|
||||
|
||||
export const createBlankTemplateEntry = async (
|
||||
rootHandle: FileSystemDirectoryHandle,
|
||||
title = 'untitled-template',
|
||||
title = 'Untitled Template',
|
||||
) => {
|
||||
const name = await createUniqueDirectoryName(rootHandle, title);
|
||||
const directoryHandle = await rootHandle.getDirectoryHandle(name, { create: true });
|
||||
@@ -603,7 +603,9 @@ export const createBlankTemplateEntry = async (
|
||||
`${JSON.stringify(
|
||||
{
|
||||
description: 'A blank template created from the pdfme Playground.',
|
||||
sourceKind: 'designer',
|
||||
tags: ['Blank', 'Starter'],
|
||||
title: title.trim() || titleFromDirectoryName(name),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -636,6 +638,7 @@ export const createTemplateEntryFromTemplate = async (
|
||||
`${JSON.stringify(
|
||||
{
|
||||
description: 'A template saved from the pdfme Playground.',
|
||||
sourceKind: 'designer',
|
||||
tags: ['Designer'],
|
||||
title: title.trim() || titleFromDirectoryName(name),
|
||||
},
|
||||
|
||||
@@ -5,11 +5,12 @@ import { Code2, Copy, Download, Save } from 'lucide-react';
|
||||
import { cloneDeep, Template, checkTemplate, Lang, isBlankPdf } from '@pdfme/common';
|
||||
import { Designer } from '@pdfme/ui';
|
||||
import {
|
||||
fromKebabCase,
|
||||
getFontsData,
|
||||
getTemplateById,
|
||||
getTemplateMetadataById,
|
||||
getBlankTemplate,
|
||||
getDefaultPlaygroundTemplate,
|
||||
getDefaultPlaygroundTemplateMetadata,
|
||||
readFile,
|
||||
generatePDF,
|
||||
downloadJsonFile,
|
||||
@@ -295,7 +296,12 @@ function DesignerApp() {
|
||||
refreshedCollection.rootHandle,
|
||||
refreshedEntry.name,
|
||||
);
|
||||
toast.success(`Saved ${refreshedEntry.path}`);
|
||||
toast.success(
|
||||
<ProjectSavedToast
|
||||
formPath={`/form-viewer?workspace=${encodeURIComponent(refreshedEntry.name)}`}
|
||||
title={refreshedEntry.path}
|
||||
/>,
|
||||
);
|
||||
} finally {
|
||||
isSavingFileWorkspaceRef.current = false;
|
||||
}
|
||||
@@ -382,18 +388,25 @@ function DesignerApp() {
|
||||
template = project.template;
|
||||
} else if (templateIdFromQuery) {
|
||||
setActiveFileWorkspaceEntry(null, null);
|
||||
const templateJson = await getTemplateById(templateIdFromQuery);
|
||||
const [templateJson, metadata] = await Promise.all([
|
||||
getTemplateById(templateIdFromQuery),
|
||||
getTemplateMetadataById(templateIdFromQuery),
|
||||
]);
|
||||
checkTemplate(templateJson);
|
||||
template = templateJson;
|
||||
setCurrentProjectTitle(fromKebabCase(templateIdFromQuery));
|
||||
setCurrentProjectTitle(metadata.title);
|
||||
} else {
|
||||
setActiveFileWorkspaceEntry(null, null);
|
||||
project = getActivePlaygroundProject();
|
||||
if (project) {
|
||||
template = project.template;
|
||||
} else {
|
||||
template = await getDefaultPlaygroundTemplate();
|
||||
setCurrentProjectTitle(fromKebabCase('invoice'));
|
||||
const [defaultTemplate, metadata] = await Promise.all([
|
||||
getDefaultPlaygroundTemplate(),
|
||||
getDefaultPlaygroundTemplateMetadata(),
|
||||
]);
|
||||
template = defaultTemplate;
|
||||
setCurrentProjectTitle(metadata.title);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ import { toast } from 'react-toastify';
|
||||
import { Template, checkTemplate, getInputFromTemplate, Lang } from '@pdfme/common';
|
||||
import { Form, Viewer } from '@pdfme/ui';
|
||||
import {
|
||||
fromKebabCase,
|
||||
getFontsData,
|
||||
getTemplateById,
|
||||
getTemplateMetadataById,
|
||||
getBlankTemplate,
|
||||
getDefaultPlaygroundTemplate,
|
||||
getDefaultPlaygroundTemplateMetadata,
|
||||
generatePDF,
|
||||
isJsonString,
|
||||
translations,
|
||||
@@ -133,10 +134,13 @@ function FormAndViewerApp() {
|
||||
diskVersionRef.current = null;
|
||||
setFileWorkspaceEntry(null);
|
||||
setFileWorkspaceStatus(null);
|
||||
const templateJson = await getTemplateById(templateIdFromQuery);
|
||||
const [templateJson, metadata] = await Promise.all([
|
||||
getTemplateById(templateIdFromQuery),
|
||||
getTemplateMetadataById(templateIdFromQuery),
|
||||
]);
|
||||
checkTemplate(templateJson);
|
||||
template = templateJson;
|
||||
setProjectTitle(fromKebabCase(templateIdFromQuery));
|
||||
setProjectTitle(metadata.title);
|
||||
} else {
|
||||
fileWorkspaceEntryRef.current = null;
|
||||
diskVersionRef.current = null;
|
||||
@@ -147,8 +151,12 @@ function FormAndViewerApp() {
|
||||
template = project.template;
|
||||
inputs = project.inputs;
|
||||
} else {
|
||||
template = await getDefaultPlaygroundTemplate();
|
||||
setProjectTitle(fromKebabCase('invoice'));
|
||||
const [defaultTemplate, metadata] = await Promise.all([
|
||||
getDefaultPlaygroundTemplate(),
|
||||
getDefaultPlaygroundTemplateMetadata(),
|
||||
]);
|
||||
template = defaultTemplate;
|
||||
setProjectTitle(metadata.title);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +255,12 @@ function FormAndViewerApp() {
|
||||
currentTemplateRef.current = nextTemplate;
|
||||
currentInputsRef.current = nextInputs;
|
||||
setProjectTitle(savedProject.title);
|
||||
toast.success(<ProjectSavedToast title={savedProject.title} />);
|
||||
toast.success(
|
||||
<ProjectSavedToast
|
||||
formPath={`/form-viewer?project=${encodeURIComponent(savedProject.id)}`}
|
||||
title={savedProject.title}
|
||||
/>,
|
||||
);
|
||||
};
|
||||
|
||||
const onResetInputs = () => {
|
||||
@@ -366,8 +379,12 @@ function FormAndViewerApp() {
|
||||
<div className="flex gap-1">
|
||||
<PlaygroundButton onClick={onGetInputs}>Get</PlaygroundButton>
|
||||
<PlaygroundButton onClick={onSetInputs}>Set</PlaygroundButton>
|
||||
<PlaygroundButton onClick={() => void onSaveInputs()}>Save</PlaygroundButton>
|
||||
<PlaygroundButton onClick={() => void onSaveInputs(true)}>Save As</PlaygroundButton>
|
||||
<PlaygroundButton onClick={() => void onSaveInputs()}>
|
||||
{fileWorkspaceEntry ? 'Save Local Copy' : 'Save'}
|
||||
</PlaygroundButton>
|
||||
<PlaygroundButton onClick={() => void onSaveInputs(true)}>
|
||||
{fileWorkspaceEntry ? 'Save As Local Copy' : 'Save As'}
|
||||
</PlaygroundButton>
|
||||
<PlaygroundButton onClick={onResetInputs}>Reset</PlaygroundButton>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -12,12 +12,11 @@ import {
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
PencilRuler,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
Upload,
|
||||
} from 'lucide-react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { downloadJsonFile, fromKebabCase, readFile } from '../helper';
|
||||
import { downloadJsonFile, readFile } from '../helper';
|
||||
import PlaygroundButton from '../components/PlaygroundButton';
|
||||
import { getAuthoringStarterId, type AuthoringStarterKind } from '../lib/authoringStarters';
|
||||
import {
|
||||
@@ -62,16 +61,16 @@ type TemplateData = {
|
||||
name: string;
|
||||
author: string;
|
||||
basePdfKind?: string;
|
||||
description?: string;
|
||||
description: string;
|
||||
fieldCount?: number;
|
||||
fontNames?: string[];
|
||||
hasCJK?: boolean;
|
||||
pageCount?: number;
|
||||
schemaTypes?: string[];
|
||||
sourceKind?: Exclude<GenerationFilter, 'all'>;
|
||||
sourceKind: Exclude<GenerationFilter, 'all'>;
|
||||
sourcePath?: string;
|
||||
tags?: string[];
|
||||
title?: string;
|
||||
tags: string[];
|
||||
title: string;
|
||||
};
|
||||
|
||||
type UIType = 'designer' | 'form-viewer';
|
||||
@@ -114,7 +113,13 @@ const generationFilters: Array<{ label: string; value: GenerationFilter }> = [
|
||||
];
|
||||
|
||||
const getTemplateGeneration = (template: TemplateData): Exclude<GenerationFilter, 'all'> =>
|
||||
template.sourceKind ?? 'designer';
|
||||
template.sourceKind;
|
||||
|
||||
const getGenerationLabel = (generation: Exclude<GenerationFilter, 'all'>) => {
|
||||
if (generation === 'jsx') return 'JSX';
|
||||
if (generation === 'md2pdf') return 'md2pdf';
|
||||
return 'Designer';
|
||||
};
|
||||
|
||||
const getAuthoringPreset = (template: TemplateData): AuthoringPreset | null => {
|
||||
const kind = getTemplateGeneration(template);
|
||||
@@ -127,7 +132,7 @@ const getAuthoringPreset = (template: TemplateData): AuthoringPreset | null => {
|
||||
};
|
||||
|
||||
const getTemplateTags = (template: TemplateData) => {
|
||||
const tags = new Set(template.tags ?? []);
|
||||
const tags = new Set(template.tags);
|
||||
|
||||
return [...tags].sort((a, b) => {
|
||||
const aIndex = tagSortOrder.indexOf(a);
|
||||
@@ -442,7 +447,6 @@ function TemplatesApp() {
|
||||
const [mountedCollection, setMountedCollection] = useState<FileWorkspaceCollection | null>(null);
|
||||
const [lastFolderName, setLastFolderName] = useState<string | null>(null);
|
||||
const [isOpeningFolder, setIsOpeningFolder] = useState(false);
|
||||
const [isRefreshingFolder, setIsRefreshingFolder] = useState(false);
|
||||
const [generationFilter, setGenerationFilter] = useState<GenerationFilter>('all');
|
||||
const [tagFilter, setTagFilter] = useState('all');
|
||||
|
||||
@@ -450,7 +454,6 @@ function TemplatesApp() {
|
||||
const refreshMountedCollection = useCallback(() => {
|
||||
if (!mountedCollection) return;
|
||||
|
||||
setIsRefreshingFolder(true);
|
||||
void refreshTemplateCollection(mountedCollection)
|
||||
.then((collection) => {
|
||||
setMountedCollection(collection);
|
||||
@@ -459,8 +462,7 @@ function TemplatesApp() {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to refresh folder');
|
||||
})
|
||||
.finally(() => setIsRefreshingFolder(false));
|
||||
});
|
||||
}, [mountedCollection]);
|
||||
|
||||
const tagOptions = useMemo(() => {
|
||||
@@ -685,6 +687,28 @@ function TemplatesApp() {
|
||||
toast.info('Disconnected mounted folder');
|
||||
};
|
||||
|
||||
const onCreateMountedTemplate = async () => {
|
||||
if (!mountedCollection) return;
|
||||
|
||||
const title = window.prompt('Template name', 'Untitled Template') ?? '';
|
||||
if (!title.trim()) return;
|
||||
|
||||
try {
|
||||
const entry = await createBlankTemplateEntry(mountedCollection.rootHandle, title);
|
||||
const nextCollection = await refreshTemplateCollection({
|
||||
...mountedCollection,
|
||||
selectedTemplateName: entry.name,
|
||||
});
|
||||
const nextEntry = findTemplateEntry(nextCollection, entry.name) ?? entry;
|
||||
setMountedCollection(nextCollection);
|
||||
setLastFolderName(nextCollection.rootName);
|
||||
await navigateToMountedTemplate(nextCollection, nextEntry, 'designer');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to create template');
|
||||
}
|
||||
};
|
||||
|
||||
const onImportTemplateJson = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
@@ -759,125 +783,131 @@ function TemplatesApp() {
|
||||
<div className="bg-white">
|
||||
<div className="mx-auto max-w-2xl px-4 py-8 sm:px-6 sm:py-12 lg:max-w-7xl lg:px-8">
|
||||
<div className="mb-10 rounded-lg border border-green-200 bg-green-50 p-5">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">My Workspace</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm text-green-900">
|
||||
Save templates from Designer, JSX, or md2pdf as local projects. A project keeps the
|
||||
generated template, inputs, and source when available.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<PlaygroundButton
|
||||
disabled={!fileWorkspaceSupported || isOpeningFolder}
|
||||
onClick={() => void onOpenFolder()}
|
||||
variant="secondary"
|
||||
>
|
||||
<FolderOpen className="size-4" />
|
||||
{isOpeningFolder ? 'Opening...' : 'Open Folder'}
|
||||
</PlaygroundButton>
|
||||
<PlaygroundButton
|
||||
onClick={() => importTemplateInputRef.current?.click()}
|
||||
variant="secondary"
|
||||
>
|
||||
<Upload className="size-4" />
|
||||
Import Template JSON
|
||||
</PlaygroundButton>
|
||||
<input
|
||||
ref={importTemplateInputRef}
|
||||
type="file"
|
||||
accept="application/json"
|
||||
className="sr-only"
|
||||
onChange={onImportTemplateJson}
|
||||
/>
|
||||
<PlaygroundButton onClick={() => navigate('/designer?new=1')} variant="primary">
|
||||
<PencilRuler className="size-4" />
|
||||
New Template
|
||||
</PlaygroundButton>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900">My Workspace</h2>
|
||||
<p className="mt-2 max-w-3xl text-sm text-green-900">
|
||||
Work with templates saved in this browser, or mount a folder to edit template files
|
||||
directly on disk.
|
||||
</p>
|
||||
</div>
|
||||
{projects.length > 0 ? (
|
||||
<div className="mt-5 grid grid-cols-1 gap-y-8 sm:grid-cols-2 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
|
||||
{projects.map((project) => (
|
||||
<GalleryCard
|
||||
key={project.id}
|
||||
tag={getProjectKindLabel(project.kind)}
|
||||
title={project.title}
|
||||
description={
|
||||
<p className="text-xs text-gray-500">
|
||||
Updated {new Date(project.updatedAt).toLocaleString()}
|
||||
</p>
|
||||
}
|
||||
thumbnail={
|
||||
<ProjectThumbnailImage project={project} onCreated={refreshProjects} />
|
||||
}
|
||||
actions={
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] gap-2">
|
||||
<PlaygroundButton
|
||||
onClick={() =>
|
||||
navigateToProject(project, project.source ? 'source' : 'designer')
|
||||
}
|
||||
>
|
||||
{project.source ? (
|
||||
<>
|
||||
<Code2 className="size-4" />
|
||||
Source
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PencilRuler className="size-4" />
|
||||
Designer
|
||||
</>
|
||||
)}
|
||||
</PlaygroundButton>
|
||||
<PlaygroundButton onClick={() => navigateToProject(project, 'form-viewer')}>
|
||||
Preview
|
||||
</PlaygroundButton>
|
||||
<ProjectMoreActions
|
||||
project={project}
|
||||
onOpenDesigner={(item) => navigateToProject(item, 'designer')}
|
||||
onRenameProject={onRenameProject}
|
||||
onDuplicateProject={onDuplicateProject}
|
||||
onDownloadProjectTemplate={onDownloadProjectTemplate}
|
||||
onDeleteProject={onDeleteProject}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="mt-5 border-t border-green-200 pt-5">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">Browser Projects</h3>
|
||||
<p className="mt-1 text-sm text-green-900">
|
||||
Drafts stored in this browser. They include template JSON, form inputs, and source
|
||||
code when available.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
<PlaygroundButton
|
||||
onClick={() => importTemplateInputRef.current?.click()}
|
||||
variant="secondary"
|
||||
>
|
||||
<Upload className="size-4" />
|
||||
Import Template JSON
|
||||
</PlaygroundButton>
|
||||
<input
|
||||
ref={importTemplateInputRef}
|
||||
type="file"
|
||||
accept="application/json"
|
||||
className="sr-only"
|
||||
onChange={onImportTemplateJson}
|
||||
/>
|
||||
))}
|
||||
<PlaygroundButton onClick={() => navigate('/designer?new=1')} variant="primary">
|
||||
<PencilRuler className="size-4" />
|
||||
New Local Template
|
||||
</PlaygroundButton>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-5 rounded-md border border-dashed border-green-300 bg-white px-4 py-6 text-sm text-green-900">
|
||||
No local projects yet. Start from a sample, JSX, md2pdf, or a blank Designer template.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{projects.length > 0 ? (
|
||||
<div className="mt-5 grid grid-cols-1 gap-y-8 sm:grid-cols-2 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8">
|
||||
{projects.map((project) => (
|
||||
<GalleryCard
|
||||
key={project.id}
|
||||
tag={`Local ${getProjectKindLabel(project.kind)}`}
|
||||
title={project.title}
|
||||
description={
|
||||
<p className="text-xs text-gray-500">
|
||||
Updated {new Date(project.updatedAt).toLocaleString()}
|
||||
</p>
|
||||
}
|
||||
thumbnail={
|
||||
<ProjectThumbnailImage project={project} onCreated={refreshProjects} />
|
||||
}
|
||||
actions={
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_auto] gap-2">
|
||||
<PlaygroundButton
|
||||
onClick={() =>
|
||||
navigateToProject(project, project.source ? 'source' : 'designer')
|
||||
}
|
||||
>
|
||||
{project.source ? (
|
||||
<>
|
||||
<Code2 className="size-4" />
|
||||
Source
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PencilRuler className="size-4" />
|
||||
Designer
|
||||
</>
|
||||
)}
|
||||
</PlaygroundButton>
|
||||
<PlaygroundButton onClick={() => navigateToProject(project, 'form-viewer')}>
|
||||
Preview
|
||||
</PlaygroundButton>
|
||||
<ProjectMoreActions
|
||||
project={project}
|
||||
onOpenDesigner={(item) => navigateToProject(item, 'designer')}
|
||||
onRenameProject={onRenameProject}
|
||||
onDuplicateProject={onDuplicateProject}
|
||||
onDownloadProjectTemplate={onDownloadProjectTemplate}
|
||||
onDeleteProject={onDeleteProject}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-5 rounded-md border border-dashed border-green-300 bg-white px-4 py-6 text-sm text-green-900">
|
||||
No browser projects yet. Create a local template, import JSON, or save from
|
||||
Designer, JSX, or md2pdf.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 border-t border-green-200 pt-5">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-gray-900">Mounted Folder</h3>
|
||||
<p className="mt-1 text-sm text-green-900">
|
||||
Edit a template-assets style folder directly on disk.
|
||||
Templates in this section are read from and saved back to template files on disk.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 sm:flex-row">
|
||||
{mountedCollection && (
|
||||
{!mountedCollection && (
|
||||
<PlaygroundButton
|
||||
disabled={isRefreshingFolder}
|
||||
onClick={() => void refreshMountedCollection()}
|
||||
variant="secondary"
|
||||
disabled={!fileWorkspaceSupported || isOpeningFolder}
|
||||
onClick={() => void onOpenFolder()}
|
||||
variant="primary"
|
||||
>
|
||||
<RefreshCw className="size-4" />
|
||||
Refresh
|
||||
<FolderOpen className="size-4" />
|
||||
{isOpeningFolder ? 'Opening...' : 'Open Folder'}
|
||||
</PlaygroundButton>
|
||||
)}
|
||||
{!mountedCollection && lastFolderName && (
|
||||
<PlaygroundButton
|
||||
disabled={!fileWorkspaceSupported || isOpeningFolder}
|
||||
onClick={() => void onReopenFolder()}
|
||||
title={lastFolderName}
|
||||
variant="secondary"
|
||||
>
|
||||
<FolderOpen className="size-4" />
|
||||
Reopen last folder
|
||||
Reopen Folder
|
||||
</PlaygroundButton>
|
||||
)}
|
||||
{mountedCollection && (
|
||||
@@ -886,12 +916,18 @@ function TemplatesApp() {
|
||||
Disconnect
|
||||
</PlaygroundButton>
|
||||
)}
|
||||
{mountedCollection && (
|
||||
<PlaygroundButton onClick={() => void onCreateMountedTemplate()} variant="primary">
|
||||
<PencilRuler className="size-4" />
|
||||
New Mounted Template
|
||||
</PlaygroundButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!fileWorkspaceSupported && (
|
||||
<div className="mt-4 rounded-md border border-dashed border-green-300 bg-white px-4 py-4 text-sm text-green-900">
|
||||
Folder workspaces need a Chromium browser in a secure context. Template JSON import
|
||||
and download are still available.
|
||||
Folder workspaces need a Chromium browser in a secure context. Browser projects,
|
||||
JSON import, and JSON download are still available.
|
||||
</div>
|
||||
)}
|
||||
{mountedCollection && (
|
||||
@@ -913,7 +949,7 @@ function TemplatesApp() {
|
||||
{mountedCollection.entries.map((entry) => (
|
||||
<GalleryCard
|
||||
key={entry.name}
|
||||
tag="Mounted"
|
||||
tag={`Disk ${getGenerationLabel(entry.sourceKind)}`}
|
||||
title={entry.title}
|
||||
tags={entry.tags}
|
||||
description={
|
||||
@@ -954,7 +990,6 @@ function TemplatesApp() {
|
||||
)
|
||||
}
|
||||
>
|
||||
<Eye className="size-4" />
|
||||
Form/Viewer
|
||||
</PlaygroundButton>
|
||||
</div>
|
||||
@@ -964,7 +999,8 @@ function TemplatesApp() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 rounded-md border border-dashed border-green-300 bg-white px-4 py-4 text-sm text-green-900">
|
||||
No valid template directories are mounted.
|
||||
No valid template directories are mounted. Create a mounted template to write a
|
||||
new folder with template.json.
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -1042,7 +1078,7 @@ function TemplatesApp() {
|
||||
{filteredTemplates.map((template, index) => {
|
||||
const { name, author } = template;
|
||||
const authoringPreset = getAuthoringPreset(template);
|
||||
const title = template.title ?? fromKebabCase(name);
|
||||
const title = template.title;
|
||||
const generation = getTemplateGeneration(template);
|
||||
const tag =
|
||||
generation === 'jsx' ? 'JSX' : generation === 'md2pdf' ? 'md2pdf' : 'Designer';
|
||||
@@ -1064,7 +1100,7 @@ function TemplatesApp() {
|
||||
tags={tags}
|
||||
description={
|
||||
<div className="space-y-3">
|
||||
<p>{template.description ?? 'A ready-to-edit pdfme sample template.'}</p>
|
||||
<p>{template.description}</p>
|
||||
<p className="flex items-center gap-2 text-xs text-gray-500">
|
||||
by{' '}
|
||||
{avatarUrlMap[author] && (
|
||||
|
||||
Reference in New Issue
Block a user