mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
feat(renderer): TableGeneratorDialog (markdown table generator)
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { useState } from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { useAppStore } from '@/stores/app-store';
|
||||
import { toast } from '@/lib/toast';
|
||||
|
||||
function pad(s: string, w: number) {
|
||||
return s.padEnd(w);
|
||||
}
|
||||
|
||||
function generateTable(rows: number, cols: number, hasHeader: boolean, headers: string[], data: string[][]): string {
|
||||
const width = cols;
|
||||
const colWidths: number[] = Array.from({ length: width }, (_, c) => {
|
||||
const all = [headers[c] ?? '', ...data.map((r) => r[c] ?? '')];
|
||||
return Math.max(...all.map((s) => s.length), 3);
|
||||
});
|
||||
const lines: string[] = [];
|
||||
if (hasHeader) {
|
||||
lines.push('| ' + Array.from({ length: width }, (_, c) => pad(headers[c] ?? `Col ${c + 1}`, colWidths[c])).join(' | ') + ' |');
|
||||
lines.push('| ' + colWidths.map((w) => '-'.repeat(w)).join(' | ') + ' |');
|
||||
}
|
||||
for (const row of data) {
|
||||
lines.push('| ' + Array.from({ length: width }, (_, c) => pad(row[c] ?? '', colWidths[c])).join(' | ') + ' |');
|
||||
}
|
||||
// Add empty rows if data has fewer
|
||||
for (let i = data.length; i < rows; i++) {
|
||||
lines.push('| ' + colWidths.map((w) => pad('', w)).join(' | ') + ' |');
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function TableGeneratorDialog() {
|
||||
const closeModal = useAppStore((s) => s.closeModal);
|
||||
const [rows, setRows] = useState(3);
|
||||
const [cols, setCols] = useState(3);
|
||||
const [hasHeader, setHasHeader] = useState(true);
|
||||
const [headers, setHeaders] = useState<string[]>(['Col 1', 'Col 2', 'Col 3']);
|
||||
|
||||
const output = generateTable(rows, cols, hasHeader, headers, []);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(output);
|
||||
toast.success('Copied to clipboard');
|
||||
} catch {
|
||||
toast.error('Failed to copy');
|
||||
}
|
||||
};
|
||||
|
||||
const updateHeadersCount = (newCols: number) => {
|
||||
setCols(newCols);
|
||||
setHeaders((prev) => {
|
||||
if (prev.length < newCols) return [...prev, ...Array(newCols - prev.length).fill('').map((_, i) => `Col ${prev.length + i + 1}`)];
|
||||
return prev.slice(0, newCols);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={(o) => !o && closeModal()}>
|
||||
<DialogContent aria-describedby="table-desc" className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Table generator</DialogTitle>
|
||||
<DialogDescription id="table-desc">Specify rows × columns to generate a markdown table</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="table-rows">Rows</Label>
|
||||
<Input
|
||||
id="table-rows"
|
||||
aria-label="Rows"
|
||||
type="number"
|
||||
min={1}
|
||||
max={50}
|
||||
value={rows}
|
||||
onChange={(e) => setRows(Math.max(1, Math.min(50, Number(e.target.value) || 1)))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="table-cols">Columns</Label>
|
||||
<Input
|
||||
id="table-cols"
|
||||
aria-label="Cols"
|
||||
type="number"
|
||||
min={1}
|
||||
max={20}
|
||||
value={cols}
|
||||
onChange={(e) => updateHeadersCount(Math.max(1, Math.min(20, Number(e.target.value) || 1)))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<label className="flex items-center gap-2">
|
||||
<Checkbox checked={hasHeader} onCheckedChange={(c) => setHasHeader(!!c)} aria-label="Header" />
|
||||
Include header row
|
||||
</label>
|
||||
{hasHeader && (
|
||||
<div>
|
||||
<Label>Header names</Label>
|
||||
<div className="grid gap-2" style={{ gridTemplateColumns: `repeat(${cols}, minmax(0, 1fr))` }}>
|
||||
{Array.from({ length: cols }, (_, i) => (
|
||||
<Input
|
||||
key={i}
|
||||
aria-label={`Header ${i + 1}`}
|
||||
value={headers[i] ?? ''}
|
||||
onChange={(e) => {
|
||||
const next = [...headers];
|
||||
next[i] = e.target.value;
|
||||
setHeaders(next);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Label>Output</Label>
|
||||
<pre className="overflow-auto rounded border border-border bg-card/30 p-3 text-xs" data-testid="table-output">
|
||||
{output}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="ghost" onClick={closeModal}>Close</Button>
|
||||
<Button onClick={handleCopy}>Copy</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { TableGeneratorDialog } from '@/components/modals/TableGeneratorDialog';
|
||||
|
||||
describe('TableGeneratorDialog', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('renders with rows, cols, and header inputs', () => {
|
||||
render(<TableGeneratorDialog />);
|
||||
expect(screen.getByText(/table generator/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('spinbutton', { name: /rows/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('spinbutton', { name: /cols/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('checkbox', { name: /header/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('generates a markdown table on rows/cols change', async () => {
|
||||
render(<TableGeneratorDialog />);
|
||||
const rowsInput = screen.getByRole('spinbutton', { name: /rows/i });
|
||||
const colsInput = screen.getByRole('spinbutton', { name: /cols/i });
|
||||
await userEvent.clear(rowsInput);
|
||||
await userEvent.type(rowsInput, '2');
|
||||
await userEvent.clear(colsInput);
|
||||
await userEvent.type(colsInput, '3');
|
||||
// Output should have header + separator + 2 rows = 4 lines for a 3-col table with header
|
||||
const output = screen.getByTestId('table-output');
|
||||
expect(output.textContent).toContain('|');
|
||||
});
|
||||
|
||||
it('shows a copy button next to the output', () => {
|
||||
render(<TableGeneratorDialog />);
|
||||
expect(screen.getByRole('button', { name: /copy/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user