Testcraft Studio frontend 33: src/pages/Templates.jsx
importReact,{useState}from'react';importIconfrom'../Icon.jsx';import{readTemplate}from'../excel.js';importTemplateUploadfrom'../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';exportdefaultfunctionTemplates(){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('');consttemplates=s.templates.filter(t=>t.team===s.session.team&&(t.visibility!=='workspace'||t.workspaceId===w?.id)&&`${t.name}${t.description}`.toLowerCase().includes(query.toLowerCase()));asyncfunctionimportFile(e){constfile=e.target.files?.[0];e.target.value='';if (!file)return;try{setImporting(awaitreadTemplate(file));setError('');}catch (failure){setError(failure.message);}}functioneditor(template){setNewField('');setEditing(template?{...template,fields:[...template.fields],originalId:template.id}:{name:'',description:'',fields:[...defaultFields],visibility:'team',version:0});}return<><Headingeyebrow={`${s.session.team} · PRESENTATION & EXAMPLES`}title="Template library"description="Choose the shape of a test case. Evidence still defines its expected behavior."><divclassName="row-actions"><TemplateUpload/><labelclassName="button button-secondary file-button"><Iconname="upload"size={16}/>Import examples<inputtype="file"accept=".csv,.xlsx"onChange={importFile}/></label><Buttonicon="plus"variant="primary"onClick={()=>editor()}>Create template</Button></div></Heading><Tabsvalue={tab}onChange={setTab}items={['Templates','Example suites']}/>{error&&<pclassName="form-error"role="alert">{error}</p>}{tab==='Templates'?<><divclassName="library-toolbar"><Searchvalue={query}onChange={setQuery}placeholder="Search templates…"/><spanclassName="subtle">Immutable template versions</span></div><divclassName="library-grid">{templates.map(t=><sectionclassName="card library-card"key={t.id}><divclassName="library-card-top"><spanclassName="soft-icon soft-purple"><Iconname="checkSquare"size={21}/></span><Badgetone={w?.templateId===t.id?'green':'neutral'}>{w?.templateId===t.id?'Selected':`Version ${t.version}`}</Badge></div><h3>{t.name}</h3><p>{t.description}</p><divclassName="field-chips">{t.fields.slice(0,5).map(f=><spankey={f}>{f}</span>)}{t.fields.length>5&&<span>+{t.fields.length-5} fields</span>}</div><divclassName="library-footer"><ButtononClick={()=>editor(t)}>View / edit</Button><Buttonvariant="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&&<sectionclassName="card"><Emptytitle="No templates found"description="Create a template or import a Excel or CSV example suite."/></section>}</>:<sectionclassName="card"><divclassName="section-heading"><h2>Imported example suites</h2><Badgetone="purple">Style references only</Badge></div><divclassName="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=><divclassName="history-row"key={e.id}><Iconname="file"size={18}/><span><strong>{e.name}</strong><small>{e.rows.length} rows · Source rows begin at 2 · {e.fields.length} columns</small></span><ButtononClick={()=>setImporting(e)}>Review import</Button></div>)}{!(s.examples||[]).some(e=>e.team===s.session.team)&&<Emptytitle="No examples imported"description="Import an Excel or CSV file to review its header fields and row coordinates."/>}</section>}{editing&&<Modaltitle={editing.originalId?'Create a template revision':'Create test template'}close={()=>setEditing(null)}wide><formonSubmit={e=>{e.preventDefault();constid=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.');}}><Fieldlabel="Template name"><inputrequiredvalue={editing.name}onChange={e=>setEditing(t=>({...t,name:e.target.value}))}maxLength={100}/></Field><Fieldlabel="Description"><textarearows={2}value={editing.description}onChange={e=>setEditing(t=>({...t,description:e.target.value}))}/></Field><Fieldlabel="Visibility"><selectvalue={editing.visibility}onChange={e=>setEditing(t=>({...t,visibility:e.target.value}))}><optionvalue="team">Team-shared</option><optionvalue="workspace">Workspace only</option></select></Field><h3className="form-section-label">Output fields and order</h3><divclassName="template-fields">{editing.fields.map((field,i)=><divkey={`${field}-${i}`}><span>{i+1}</span><strong>{field}</strong><buttontype="button"aria-label={`Move ${field} up`}disabled={i===0}onClick={()=>setEditing(t=>{constfields=[...t.fields];[fields[i-1],fields[i]]=[fields[i],fields[i-1]];return{...t,fields};})}><Iconname="arrowUp"size={13}/></button><buttontype="button"aria-label={`Remove ${field}`}onClick={()=>setEditing(t=>({...t,fields:t.fields.filter((_,index)=>index!==i)}))}><Iconname="close"size={14}/></button></div>)}</div><divclassName="inline-form"><inputaria-label="New field name"value={newField}onChange={e=>setNewField(e.target.value)}placeholder="Custom field name"/><Buttondisabled={!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><divclassName="modal-actions"><ButtononClick={()=>setEditing(null)}>Cancel</Button><Buttontype="submit"variant="primary"disabled={!editing.fields.length||(editing.visibility==="workspace"&&!w)}>Save template v{editing.version+1}</Button></div></form></Modal>}{importing&&<Modaltitle="Review imported examples"close={()=>setImporting(null)}wide><pclassName="modal-intro">{importing.name} · Fields are inferred from the first worksheet row.</p><divclassName="field-chips">{importing.fields.map((f,i)=><spankey={i}>Column {i+1}: {f}</span>)}</div><divclassName="table-scroll import-table"><table><thead><tr><th>SOURCE ROW</th>{importing.fields.map((f,i)=><thkey={i}>{f}</th>)}</tr></thead><tbody>{importing.rows.slice(0,4).map((row,i)=><trkey={i}><td>{i+2}</td>{row.map((value,j)=><tdkey={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><divclassName="modal-actions"><ButtononClick={()=>setImporting(null)}>Cancel</Button><Buttonvariant="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>}</>;}
Top comments (0)
Subscribe
We're a place where coders share, stay up-to-date and grow their careers.
Top comments (0)