Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 116 additions & 64 deletions demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,19 @@ import type {
SuperDocTemplateBuilderHandle,
TemplateField,
FieldDefinition,
ExportEvent,
} from "@superdoc-dev/template-builder";
import "superdoc/dist/style.css";
import "superdoc/style.css";
import "./App.css";

const availableFields: FieldDefinition[] = [
{ id: '1242142770', label: 'Agreement Date' },
{ id: '1242142771', label: 'User Name' },
{ id: '1242142772', label: 'Company Name' },
{ id: '1242142773', label: 'Service Type' },
{ id: '1242142774', label: 'Agreement Jurisdiction' },
{ id: '1242142775', label: 'Company Address' },
{ id: '1242142776', label: 'Signature' },
{ id: "1242142770", label: "Agreement Date" },
{ id: "1242142771", label: "User Name" },
{ id: "1242142772", label: "Company Name" },
{ id: "1242142773", label: "Service Type" },
{ id: "1242142774", label: "Agreement Jurisdiction" },
{ id: "1242142775", label: "Company Address" },
{ id: "1242142776", label: "Signature" },
];

export function App() {
Expand All @@ -34,42 +35,68 @@ export function App() {
const log = useCallback((msg: string) => {
const time = new Date().toLocaleTimeString();
console.log(`[${time}] ${msg}`);
setEvents(prev => [...prev.slice(-4), `${time} - ${msg}`]);
setEvents((prev) => [...prev.slice(-4), `${time} - ${msg}`]);
}, []);

const handleFieldsChange = useCallback((updatedFields: TemplateField[]) => {
setFields(updatedFields);
log(`Fields: ${updatedFields.length} total`);
}, [log]);
const handleFieldsChange = useCallback(
(updatedFields: TemplateField[]) => {
setFields(updatedFields);
log(`Fields: ${updatedFields.length} total`);
},
[log],
);

const handleFieldInsert = useCallback((field: TemplateField) => {
log(`✓ Inserted: ${field.alias}`);
}, [log]);
const handleFieldInsert = useCallback(
(field: TemplateField) => {
log(`✓ Inserted: ${field.alias}`);
},
[log],
);

const handleFieldDelete = useCallback((fieldId: string | number) => {
log(`✗ Deleted: ${fieldId}`);
}, [log]);
const handleFieldDelete = useCallback(
(fieldId: string | number) => {
log(`✗ Deleted: ${fieldId}`);
},
[log],
);

const handleFieldSelect = useCallback((field: TemplateField | null) => {
if (field) {
log(`Selected: ${field.alias}`);
}
}, [log]);
const handleFieldSelect = useCallback(
(field: TemplateField | null) => {
if (field) {
log(`Selected: ${field.alias}`);
}
},
[log],
);

const handleReady = useCallback(() => {
log('✓ Template builder ready');
log("✓ Template builder ready");
if (importingRef.current) {
log('📄 Document imported');
log("📄 Document imported");
importingRef.current = false;
setImportError(null);
setIsImporting(false);
}
}, [log]);

const handleTrigger = useCallback(() => {
log('⌨ Trigger detected');
log("⌨ Trigger detected");
}, [log]);

const handleExport = useCallback(
(event: ExportEvent) => {
console.log("Export Event:", event);
console.log("Fields:", JSON.stringify(event.fields, null, 2));
log(`Exported ${event.fields.length} fields`);
event.fields.forEach((f) => {
console.log(
` - ${f.alias} (id: ${f.id}, mode: ${f.mode}, group: ${f.group || "none"})`,
);
});
},
[log],
);

const handleExportTemplate = useCallback(async () => {
if (!builderRef.current) {
return;
Expand All @@ -92,7 +119,7 @@ export function App() {
}, [log]);

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Tab') {
if (e.key === "Tab") {
e.preventDefault();
if (e.shiftKey) {
builderRef.current?.previousField();
Expand All @@ -102,62 +129,85 @@ export function App() {
}
};

const documentConfig = useMemo(() => ({
source: documentSource,
mode: 'editing' as const
}), [documentSource]);
const documentConfig = useMemo(
() => ({
source: documentSource,
mode: "editing" as const,
}),
[documentSource],
);

const handleImportButtonClick = useCallback(() => {
if (isImporting) return;
fileInputRef.current?.click();
}, [isImporting]);

const handleFileInputChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.target.value = "";
const handleFileInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.target.value = "";

if (!file) return;
if (!file) return;

const extension = file.name.split('.').pop()?.toLowerCase();
if (extension !== 'docx') {
const message = 'Invalid file type. Please choose a .docx file.';
setImportError(message);
log('⚠️ ' + message);
return;
}
const extension = file.name.split(".").pop()?.toLowerCase();
if (extension !== "docx") {
const message = "Invalid file type. Please choose a .docx file.";
setImportError(message);
log("⚠️ " + message);
return;
}

importingRef.current = true;
setImportError(null);
setIsImporting(true);
setDocumentSource(file);
log(`📥 Importing "${file.name}"`);
}, [log]);
importingRef.current = true;
setImportError(null);
setIsImporting(true);
setDocumentSource(file);
log(`📥 Importing "${file.name}"`);
},
[log],
);

const fieldsConfig = useMemo(() => ({
available: availableFields,
allowCreate: true
}), []);
const fieldsConfig = useMemo(
() => ({
available: availableFields,
allowCreate: true,
}),
[],
);

const listConfig = useMemo(() => ({
position: 'right' as const
}), []);
const listConfig = useMemo(
() => ({
position: "right" as const,
}),
[],
);

return (
<div className="demo" onKeyDown={handleKeyDown}>
<header>
<div className="header-content">
<div className="header-left">
<h1>
<a href="https://www.npmjs.com/package/@superdoc-dev/template-builder" target="_blank" rel="noopener">
<a
href="https://www.npmjs.com/package/@superdoc-dev/template-builder"
target="_blank"
rel="noopener"
>
@superdoc-dev/template-builder
</a>
</h1>
<p>
React template builder from <a href="https://superdoc.dev" target="_blank" rel="noopener">SuperDoc</a>
React template builder from{" "}
<a href="https://superdoc.dev" target="_blank" rel="noopener">
SuperDoc
</a>
</p>
</div>
<div className="header-nav">
<a href="https://github.com/superdoc-dev/template-builder" target="_blank" rel="noopener">
<a
href="https://github.com/superdoc-dev/template-builder"
target="_blank"
rel="noopener"
>
GitHub
</a>
<a href="https://docs.superdoc.dev" target="_blank" rel="noopener">
Expand All @@ -170,7 +220,7 @@ export function App() {
<div className="container">
<div className="toolbar">
<div className="toolbar-left">
<span className="hint">Type {'{{'} to insert a field</span>
<span className="hint">Type {"{{"} to insert a field</span>
<span className="divider">|</span>
<span className="hint">Tab/Shift+Tab to navigate</span>
</div>
Expand All @@ -179,15 +229,15 @@ export function App() {
type="file"
accept=".docx"
ref={fileInputRef}
style={{ display: 'none' }}
style={{ display: "none" }}
onChange={handleFileInputChange}
/>
<button
onClick={handleImportButtonClick}
className="import-button"
disabled={isImporting || isDownloading}
>
{isImporting ? 'Importing…' : 'Import File'}
{isImporting ? "Importing…" : "Import File"}
</button>
<button
onClick={handleExportTemplate}
Expand Down Expand Up @@ -217,6 +267,7 @@ export function App() {
onFieldDelete={handleFieldDelete}
onFieldSelect={handleFieldSelect}
onFieldsChange={handleFieldsChange}
onExport={handleExport}
documentHeight="600px"
/>

Expand All @@ -225,11 +276,12 @@ export function App() {
<div className="event-log">
<div className="event-log-header">EVENT LOG</div>
{events.map((evt, i) => (
<div key={i} className="event-log-item">{evt}</div>
<div key={i} className="event-log-item">
{evt}
</div>
))}
</div>
)}

</div>
</div>
);
Expand Down
10 changes: 9 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
type Editor = NonNullable<SuperDoc["activeEditor"]>;

const getTemplateFieldsFromEditor = (editor: Editor): Types.TemplateField[] => {
const structuredContentHelpers = (editor.helpers as any)

Check warning on line 19 in src/index.tsx

View workflow job for this annotation

GitHub Actions / validate

Unexpected any. Specify a different type
?.structuredContentCommands;

if (!structuredContentHelpers?.getStructuredContentTags) {
Expand All @@ -26,7 +26,7 @@
const tags =
structuredContentHelpers.getStructuredContentTags(editor.state) || [];

return tags.map((entry: any) => {

Check warning on line 29 in src/index.tsx

View workflow job for this annotation

GitHub Actions / validate

Unexpected any. Specify a different type
const node = entry?.node ?? entry;
const attrs = node?.attrs ?? {};
const nodeType = node?.type?.name || "";
Expand Down Expand Up @@ -137,6 +137,7 @@
onFieldsChange,
onFieldSelect,
onFieldCreate,
onExport,
className,
style,
documentHeight = "600px",
Expand Down Expand Up @@ -169,7 +170,7 @@

const trigger = menu.trigger || "{{";

const availableFields = fieldsRef.current.available || [];

Check warning on line 173 in src/index.tsx

View workflow job for this annotation

GitHub Actions / validate

The 'availableFields' logical expression could make the dependencies of useCallback Hook (at line 185) change on every render. Move it inside the useCallback callback. Alternatively, wrap the initialization of 'availableFields' in its own useMemo() Hook

const computeFilteredFields = useCallback(
(query: string) => {
Expand Down Expand Up @@ -403,7 +404,7 @@
if (instance.activeEditor) {
const editor = instance.activeEditor;

editor.on("update", ({ editor: e }: any) => {

Check warning on line 407 in src/index.tsx

View workflow job for this annotation

GitHub Actions / validate

Unexpected any. Specify a different type
const { state } = e;
const { from } = state.selection;

Expand All @@ -422,7 +423,7 @@
if (!editor) return;
const currentPos = editor.state.selection.from;
const tr = editor.state.tr.delete(triggerStart, currentPos);
(editor as any).view.dispatch(tr);

Check warning on line 426 in src/index.tsx

View workflow job for this annotation

GitHub Actions / validate

Unexpected any. Specify a different type
};

triggerCleanupRef.current = cleanup;
Expand Down Expand Up @@ -516,7 +517,7 @@

superdocRef.current = null;
};
}, [

Check warning on line 520 in src/index.tsx

View workflow job for this annotation

GitHub Actions / validate

React Hook useEffect has missing dependencies: 'resetMenuFilter', 'toolbarSettings', and 'updateMenuFilter'. Either include them or remove the dependency array
document?.source,
document?.mode,
trigger,
Expand Down Expand Up @@ -575,7 +576,7 @@
const editor = superdocRef.current?.activeEditor;
if (!editor) return;

const structuredContentHelpers = (editor.helpers as any)

Check warning on line 579 in src/index.tsx

View workflow job for this annotation

GitHub Actions / validate

Unexpected any. Specify a different type
?.structuredContentCommands;

if (!structuredContentHelpers) return;
Expand Down Expand Up @@ -666,9 +667,16 @@
triggerDownload,
});

const editor = superdocRef.current?.activeEditor;
if (editor) {
const fields = getTemplateFieldsFromEditor(editor);
const blob = triggerDownload ? undefined : (result as Blob);
onExport?.({ fields, blob, fileName });
}

return result;
},
[],
[onExport],
);

useImperativeHandle(ref, () => ({
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export interface TriggerEvent {
cleanup: () => void;
}

export interface ExportEvent {
fields: TemplateField[];
blob?: Blob;
fileName: string;
}

export interface FieldMenuProps {
isVisible: boolean;
position?: DOMRect;
Expand Down Expand Up @@ -116,6 +122,7 @@ export interface SuperDocTemplateBuilderProps {
onFieldCreate?: (
field: FieldDefinition,
) => void | Promise<FieldDefinition | void>;
onExport?: (event: ExportEvent) => void;

// UI
className?: string;
Expand Down
Loading