DEV Community

Rahul
Rahul

Posted on

Testcraft Studio frontend 28: src/pages/Evidence.jsx

import React, { useState } from 'react';
import Icon from '../Icon.jsx';
import { categories } from '../seed.js';
import { useStudio, eligibleDocuments, readiness, resolveSelection, bumpVersion, log, uid } from '../store.jsx';
import { Badge, Button, ContextBar, Empty, Field, Heading, Modal, Note, SourceDialog, Tabs, UploadBox, WorkspaceRequired } from '../ui.jsx';
import { TeamAccess } from './Workspaces.jsx';
import CreateCr from './CreateCr.jsx';

export default function Evidence({ navigate }) {
  const { state: s, mutate, workspace: w, notify } = useStudio();
  const [category, setCategory] = useState(0), [tab, setTab] = useState('Evidence & scope'), [dialog, setDialog] = useState(''), [source, setSource] = useState('');
  const [ruleId, setRuleId] = useState(''), [title, setTitle] = useState(''), [behavior, setBehavior] = useState(''), [sourceId, setSourceId] = useState('');
  const [crName, setCrName] = useState(''), [crRef, setCrRef] = useState('');
  if (!w) return <WorkspaceRequired navigate={navigate}/>;
  if (!w.baselineId) {
    const pending = s.baselines.find(b => b.id === w.pendingBaselineId && b.status === 'Pending approval');
    return <><Heading eyebrow={`${w.ref} · YOUR NEW WORKSPACE`} title={w.name} description={w.purpose || 'Bring your system knowledge into this workspace.'}/><section className="card workspace-setup"><span className="setup-orbit"><Icon name={pending ? 'shield' : 'layers'} size={38}/></span><span className="eyebrow">{pending ? 'BASELINE SUBMITTED' : 'NEXT · SYSTEM KNOWLEDGE'}</span><h2>{pending ? 'Your baseline is awaiting approval' : 'Give your workspace a foundation'}</h2><p>{pending ? `${pending.ref} is ready for an Administrator to review. Your documents become available once the baseline is approved.` : 'Add your business, technical, or functional documents to create a System Baseline. One document is enough to get started.'}</p><div className="row-actions"><Button variant="primary" icon={pending ? 'shield' : 'plus'} onClick={() => navigate(pending ? 'approvals' : 'baselines?create')}>{pending ? 'View approval request' : 'Create System Baseline'}</Button><Button onClick={() => navigate('versions')}>Use an approved baseline</Button></div><div className="setup-path"><span className="done">Workspace created</span><Icon name="chevron" size={15}/><span>{pending ? 'Baseline submitted' : 'Add documents'}</span><Icon name="chevron" size={15}/><span>Administrator approval</span><Icon name="chevron" size={15}/><span>Generate tests</span></div></section></>;
  }
  const docs = eligibleDocuments(s, w), ready = readiness(s, w), selected = resolveSelection(s, w.baselineId, w.crIds);
  const rules = [...(selected.baseline?.rules || [])];
  selected.diff.forEach(change => { const i = rules.findIndex(r => r.id === change.ruleId); if (i >= 0) rules.splice(i, 1); if (change.op !== 'REMOVE') rules.push({ id: change.ruleId, title: change.title, text: change.after, source: change.source }); });
  const requirementRows = [...rules, ...(w.customRules || [])];
  ready.questions.filter((q, i, all) => all.findIndex(item => item.requirement === q.requirement) === i).forEach(q => { if (!requirementRows.some(r => r.id === q.requirement)) requirementRows.push({ id: q.requirement, title: q.title, text: q.why, source: q.sourceId, question: true }); });
  function review(id) { mutate(d => { d.documents.find(doc => doc.id === id).status = 'Reviewed locally'; log(d, `Reviewed local CR source for ${w.ref}`); }); notify('Source reviewed locally. '); }
  return <><Heading eyebrow={`${w.ref} · ${w.visibility.toUpperCase()} WORKSPACE`} title={w.name} description={`${w.purpose} · Shared team workspace`}><Button icon="users" onClick={() => setDialog('members')}>Team access</Button></Heading><ContextBar navigate={navigate}/><Tabs value={tab} onChange={setTab} items={['Evidence & scope', 'Requirements', 'Activity']}/>{tab === 'Activity' ? <section className="card"><div className="section-heading"><h2>Workspace and team activity</h2><Badge>Audit history</Badge></div>{s.audit.filter(a => a.team === w.team || a.actor === 'Demo fixture').slice(0, 20).map(a => <div className="activity-row" key={a.id}><span className="soft-icon soft-purple"><Icon name="clock" size={17}/></span><div><strong>{a.action.replace(/Team Baseline/g, 'System Baseline').replace(/local demo/gi, 'studio')}</strong><p>{a.actor === 'Demo fixture' ? 'System' : a.actor}</p></div><small>{new Date(a.at).toLocaleString('en-IN')}</small></div>)}</section> : <div className="workspace-grid"><div className="workspace-main">{tab === 'Evidence & scope' && <><section className="card evidence-card"><div className="section-heading"><div className="inline-heading"><h2>Supporting evidence</h2><span className="count-pill">{docs.length}</span></div><div className="row-actions"><Button onClick={() => setDialog('cr')}>Add CR</Button><Button icon="plus" onClick={() => setDialog('upload')}>Add evidence</Button></div></div><div className="category-tabs" role="tablist" aria-label="CR evidence categories">{categories.map((c, i) => <button key={c} role="tab" aria-selected={category === i} className={category === i ? 'active' : ''} onClick={() => setCategory(i)}>{c}<span>{docs.filter(d => d.category === i).length}</span></button>)}</div><p className="category-description">{['Procedures, functions, packages, tables, screens, APIs, and implementation validations.', 'Expected behavior, BRDs, acceptance criteria, defects, and regression requirements.', 'Terminology, processes, participant roles, and background for this change.'][category]}</p><div className="document-list">{docs.filter(d => d.category === category).map(d => <div className="evidence-source" key={d.id}><button className="document-row" onClick={() => setSource(d.id)}><span className={`file-type file-${d.kind.toLowerCase()}`}><Icon name="file" size={21}/><small>{d.kind}</small></span><span className="document-info"><strong>{d.name}</strong><small>{d.synthetic ? 'Reference document' : 'Uploaded document'}{d.team !== w.team ? ` · Shared by ${d.team}` : ''}</small><span>{d.size} · Revision {d.revision}</span></span><Badge>{d.status}</Badge><Icon name="chevron" size={15}/></button>{d.status === 'Staged' && <div className="source-review-actions"><label>Route to category<select value={d.category} onChange={e => mutate(next => { next.documents.find(doc => doc.id === d.id).category = Number(e.target.value); })}>{categories.map((c, i) => <option key={c} value={i}>{c}</option>)}</select></label><Button onClick={() => review(d.id)}>Confirm local routing</Button></div>}</div>)}{!docs.some(d => d.category === category) && <Empty title="No separate source in this category" description="A combined document or sufficient evidence in other categories may be enough." action={<Button onClick={() => setDialog('upload')}>Add evidence</Button>}/>}</div><div className="evidence-hint"><Icon name="info" size={15}/>One combined document is welcome. These are categories, not three compulsory files.</div></section><div className="scope-card"><span className="soft-icon soft-purple"><Icon name="shield" size={19}/></span><div><h3>Clear team boundaries</h3><p>{w.team}-owned requirements and explicitly shared contracts are in scope.</p><span><Icon name="lock" size={12}/>Other teams’ private implementation details are excluded.</span></div><button className="text-button" onClick={() => navigate('graph')}>Explore dependencies <Icon name="arrow" size={14}/></button></div></>}<section className="card requirements-card"><div className="section-heading"><div className="inline-heading"><h2>Requirements in scope</h2><span className="count-pill">{requirementRows.length}</span></div><Button icon="plus" onClick={() => setDialog('requirement')}>Add requirement</Button></div><div className="table-scroll"><table><thead><tr><th>REQUIREMENT</th><th>EVIDENCE</th><th>STATUS</th></tr></thead><tbody>{requirementRows.map(r => <tr key={r.id}><td><button className="requirement-name" onClick={() => setSource(r.source)}><small>{r.id}</small><strong>{r.title}</strong></button></td><td><button className="text-button" onClick={() => setSource(r.source)}>Open source <Icon name="external" size={12}/></button></td><td><Badge tone={r.question && ready.questions.some(q => q.requirement === r.id && q.status !== 'resolved') ? 'amber' : 'green'}>{r.question && ready.questions.some(q => q.requirement === r.id && q.status !== 'resolved') ? 'Evidence gap' : 'Source linked'}</Badge></td></tr>)}</tbody></table></div>{!requirementRows.length && <Empty title="No reviewed requirements yet" description="Record the expected behavior and link it to an uploaded document."/>}</section></div><section className="card readiness-card"><div className="section-heading"><h2>Your next step</h2><span className="soft-icon soft-purple"><Icon name="spark" size={20}/></span></div><Badge tone={ready.ready ? 'green' : 'neutral'}>{ready.ready ? 'Ready to generate' : 'Build your evidence'}</Badge><h3>Turn your evidence into test cases.</h3><p>Choose a test-case format and generate from the requirements available in this workspace.</p><Button className="full-width" variant="primary" icon="spark" onClick={() => navigate('readiness')}>Generate test cases</Button><Button className="full-width margin-top" icon="chat" onClick={() => navigate('chat')}>Ask the system</Button></section></div>}{dialog === 'members' && <TeamAccess close={() => setDialog('')}/>}{dialog === 'upload' && <Modal title="Add CR evidence" close={() => setDialog('')}><Field label="Proposed evidence category"><select value={category} onChange={e => setCategory(Number(e.target.value))}>{categories.map((c, i) => <option value={i} key={c}>{c}</option>)}</select></Field><Note>Functional evidence defines expected behavior. Business context explains terms and processes. Mixed documents can be reviewed by section later.</Note><div className="margin-top"><UploadBox scope={{ team: w.team, workspaceId: w.id, category, crId: w.crIds[0] }} onStaged={files => { mutate(d => { d.documents.push(...files); bumpVersion(d, d.workspaces.find(item => item.id === w.id)); log(d, `Staged ${files.length} CR sources for ${w.ref}`); }); setDialog(''); notify('Files saved locally. Review their routing; knowledge context was updated.'); }}/></div></Modal>}{dialog === 'requirement' && <Modal title="Add a reviewed requirement" close={() => setDialog('')}><form onSubmit={e => { e.preventDefault(); if (!sourceId || !ruleId.trim() || !title.trim() || !behavior.trim()) return; mutate(d => { const item = d.workspaces.find(item => item.id === w.id); item.customRules = [...item.customRules || [], { id: ruleId.trim(), title, text: behavior, source: sourceId }]; item.requirementCount += 1; bumpVersion(d, item); log(d, `Added source-linked requirement ${ruleId}`); }); setDialog(''); notify('Requirement added to this workspace context.'); }}><Field label="Requirement ID"><input required value={ruleId} onChange={e => setRuleId(e.target.value)} placeholder={`REQ-${w.team}-04`}/></Field><Field label="Title"><input required value={title} onChange={e => setTitle(e.target.value)}/></Field><Field label="Approved expected behavior"><textarea required rows={3} value={behavior} onChange={e => setBehavior(e.target.value)}/></Field><Field label="Supporting source"><select required value={sourceId} onChange={e => setSourceId(e.target.value)}><option value="">Choose evidence</option>{docs.filter(d => ['Ready', 'Reviewed locally'].includes(d.status)).map(d => <option value={d.id} key={d.id}>{d.name}</option>)}</select></Field><Note>User-provided requirements stay scoped to this workspace. They do not update a System Baseline.</Note><div className="modal-actions"><Button onClick={() => setDialog('')}>Cancel</Button><Button type="submit" variant="primary" disabled={requirementRows.some(r => r.id === ruleId.trim())}>Save requirement</Button></div></form></Modal>}{dialog === 'cr' && <CreateCr close={() => setDialog('')}/>}{source && <SourceDialog sourceId={source} close={() => setSource('')}/>}</>;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)