DEV Community

Rahul
Rahul

Posted on

Testcraft Studio frontend 33: src/pages/Templates.jsx

import React, { useState } from 'react';
import Icon from '../Icon.jsx';
import { readTemplate } from '../excel.js';
import TemplateUpload from '../TemplateUpload.jsx';
import { defaultFields } from '../seed.js';
import { useStudio, uid, parseCsv, log } from '../store.jsx';
import { Badge, Button, Empty, Field, Heading, Modal, Note, Search, Tabs } from '../ui.jsx';

export default function Templates() {
  const { state: s, mutate, workspace: w, notify } = useStudio();
  const [query, setQuery] = useState(''), [tab, setTab] = useState('Templates'), [editing, setEditing] = useState(null), [newField, setNewField] = useState(''), [importing, setImporting] = useState(null), [error, setError] = useState('');
  const templates = s.templates.filter(t => t.team === s.session.team && (t.visibility !== 'workspace' || t.workspaceId === w?.id) && `${t.name} ${t.description}`.toLowerCase().includes(query.toLowerCase()));
  async function importFile(e) {
    const file = e.target.files?.[0]; e.target.value = ''; if (!file) return;
    try { setImporting(await readTemplate(file)); setError(''); } catch (failure) { setError(failure.message); }
  }
  function editor(template) { setNewField(''); setEditing(template ? { ...template, fields: [...template.fields], originalId: template.id } : { name: '', description: '', fields: [...defaultFields], visibility: 'team', version: 0 }); }
  return <><Heading eyebrow={`${s.session.team} · PRESENTATION & EXAMPLES`} title="Template library" description="Choose the shape of a test case. Evidence still defines its expected behavior."><div className="row-actions"><TemplateUpload/><label className="button button-secondary file-button"><Icon name="upload" size={16}/>Import examples<input type="file" accept=".csv,.xlsx" onChange={importFile}/></label><Button icon="plus" variant="primary" onClick={() => editor()}>Create template</Button></div></Heading><Tabs value={tab} onChange={setTab} items={['Templates', 'Example suites']}/>{error && <p className="form-error" role="alert">{error}</p>}{tab === 'Templates' ? <><div className="library-toolbar"><Search value={query} onChange={setQuery} placeholder="Search templates…"/><span className="subtle">Immutable template versions</span></div><div className="library-grid">{templates.map(t => <section className="card library-card" key={t.id}><div className="library-card-top"><span className="soft-icon soft-purple"><Icon name="checkSquare" size={21}/></span><Badge tone={w?.templateId === t.id ? 'green' : 'neutral'}>{w?.templateId === t.id ? 'Selected' : `Version ${t.version}`}</Badge></div><h3>{t.name}</h3><p>{t.description}</p><div className="field-chips">{t.fields.slice(0, 5).map(f => <span key={f}>{f}</span>)}{t.fields.length > 5 && <span>+{t.fields.length - 5} fields</span>}</div><div className="library-footer"><Button onClick={() => editor(t)}>View / edit</Button><Button variant="primary" disabled={!w || w.templateId === t.id} onClick={() => { mutate(d => { d.workspaces.find(item => item.id === w.id).templateId = t.id; }); notify('Template selected for this workspace.'); }}>Use template</Button></div></section>)}</div>{!templates.length && <section className="card"><Empty title="No templates found" description="Create a template or import a Excel or CSV example suite."/></section>}</> : <section className="card"><div className="section-heading"><h2>Imported example suites</h2><Badge tone="purple">Style references only</Badge></div><div className="padded"><Note>Example suites teach formatting and coverage style. Their old expected results are not current business truth.</Note></div>{(s.examples || []).filter(e => e.team === s.session.team).map(e => <div className="history-row" key={e.id}><Icon name="file" size={18}/><span><strong>{e.name}</strong><small>{e.rows.length} rows · Source rows begin at 2 · {e.fields.length} columns</small></span><Button onClick={() => setImporting(e)}>Review import</Button></div>)}{!(s.examples || []).some(e => e.team === s.session.team) && <Empty title="No examples imported" description="Import an Excel or CSV file to review its header fields and row coordinates."/>}</section>}{editing && <Modal title={editing.originalId ? 'Create a template revision' : 'Create test template'} close={() => setEditing(null)} wide><form onSubmit={e => { e.preventDefault(); const id = uid('template'); mutate(d => { d.templates.push({ id, name: editing.name.trim(), description: editing.description, fields: editing.fields, visibility: editing.visibility, workspaceId: editing.visibility === "workspace" ? w?.id : null, version: editing.version + 1, team: s.session.team }); log(d, `Saved template ${editing.name} v${editing.version + 1}`); }); setEditing(null); notify('Immutable template version saved.'); }}><Field label="Template name"><input required value={editing.name} onChange={e => setEditing(t => ({ ...t, name: e.target.value }))} maxLength={100}/></Field><Field label="Description"><textarea rows={2} value={editing.description} onChange={e => setEditing(t => ({ ...t, description: e.target.value }))}/></Field><Field label="Visibility"><select value={editing.visibility} onChange={e => setEditing(t => ({ ...t, visibility: e.target.value }))}><option value="team">Team-shared</option><option value="workspace">Workspace only</option></select></Field><h3 className="form-section-label">Output fields and order</h3><div className="template-fields">{editing.fields.map((field, i) => <div key={`${field}-${i}`}><span>{i + 1}</span><strong>{field}</strong><button type="button" aria-label={`Move ${field} up`} disabled={i === 0} onClick={() => setEditing(t => { const fields = [...t.fields]; [fields[i - 1], fields[i]] = [fields[i], fields[i - 1]]; return { ...t, fields }; })}><Icon name="arrowUp" size={13}/></button><button type="button" aria-label={`Remove ${field}`} onClick={() => setEditing(t => ({ ...t, fields: t.fields.filter((_, index) => index !== i) }))}><Icon name="close" size={14}/></button></div>)}</div><div className="inline-form"><input aria-label="New field name" value={newField} onChange={e => setNewField(e.target.value)} placeholder="Custom field name"/><Button disabled={!newField.trim() || editing.fields.includes(newField.trim())} onClick={() => { setEditing(t => ({ ...t, fields: [...t.fields, newField.trim()] })); setNewField(''); }}>Add field</Button></div><Note>Saving creates a new version. Existing suites keep their original template identity. Traceability fields are retained in exports even for a compact presentation.</Note><div className="modal-actions"><Button onClick={() => setEditing(null)}>Cancel</Button><Button type="submit" variant="primary" disabled={!editing.fields.length || (editing.visibility === "workspace" && !w)}>Save template v{editing.version + 1}</Button></div></form></Modal>}{importing && <Modal title="Review imported examples" close={() => setImporting(null)} wide><p className="modal-intro">{importing.name} · Fields are inferred from the first worksheet row.</p><div className="field-chips">{importing.fields.map((f, i) => <span key={i}>Column {i + 1}: {f}</span>)}</div><div className="table-scroll import-table"><table><thead><tr><th>SOURCE ROW</th>{importing.fields.map((f, i) => <th key={i}>{f}</th>)}</tr></thead><tbody>{importing.rows.slice(0, 4).map((row, i) => <tr key={i}><td>{i + 2}</td>{row.map((value, j) => <td key={j}>{value.slice(0, 120)}</td>)}</tr>)}</tbody></table></div><Note>Review the fields before saving. Up to 100 example rows are kept, with source row numbers. Old expected results do not override selected knowledge.</Note><div className="modal-actions"><Button onClick={() => setImporting(null)}>Cancel</Button><Button variant="primary" onClick={() => { if (!importing.id) mutate(d => { d.examples = [...d.examples || [], { ...importing, id: uid('example'), team: s.session.team }]; }); setEditing({ name: importing.name.replace(/\.(xlsx|csv)$/i, ''), description: `Fields reviewed from ${importing.name}`, fields: importing.fields, visibility: 'team', version: 0 }); setImporting(null); }}>Review as a template</Button></div></Modal>}</>;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)