Testcraft Studio frontend 23: src/pages/Approvals.jsx
importReact,{useEffect,useState}from'react';importIconfrom'../Icon.jsx';import{useStudio,canApprove,resolveSelection,log}from'../store.jsx';import{approvalBlockers,decideBaseline,requestBaselineMerge}from'../baseline.js';import{Badge,Button,Empty,Field,Heading,Modal,Note,SourceDialog}from'../ui.jsx';export{approvalBlockers}from'../baseline.js';asyncfunctiondigest(value){constbytes=awaitcrypto.subtle.digest('SHA-256',newTextEncoder().encode(value));return[...newUint8Array(bytes)].map(b=>b.toString(16).padStart(2,'0')).join('');}exportdefaultfunctionApprovals({navigate,merge=false}){const{state:s,mutate,workspace:w,notify}=useStudio();constlist=s.candidates.filter(c=>c.team===s.session.team);const[id,setId]=useState(''),[checks,setChecks]=useState([false,false]),[hash,setHash]=useState(''),[source,setSource]=useState(''),[rationale,setRationale]=useState(''),[busy,setBusy]=useState(false),[requesting,setRequesting]=useState(merge),[name,setName]=useState(''),[mergeError,setMergeError]=useState('');useEffect(()=>{if (merge)setRequesting(true);},[merge]);constselection=w?resolveSelection(s,w.baselineId,w.crIds):null;functionsubmitMerge(event){event.preventDefault();setMergeError('');try{constprepared=structuredClone(s),request=requestBaselineMerge(prepared,w?.id,name);mutate(d=>{d.candidates.unshift(request);log(d,`Submitted CR merge: ${request.ref}`);});setId(request.id);setRequesting(false);notify('CR merge sent for Administrator approval.');}catch (error){setMergeError(error.message);}}constcandidate=list.find(c=>c.id===id)||list[0],snapshot=candidate?.manifest,canonical=JSON.stringify(snapshot||{});useEffect(()=>{setChecks([false,false]);setRationale('');setHash('');letlive=true;if (candidate)digest(canonical).then(value=>{if (live)setHash(value);});return ()=>{live=false;};},[candidate?.id,canonical]);constblockers=approvalBlockers(s,candidate),pending=candidate?.status==='Pending review';constsourceIds=snapshot?.baseline?.sourceIds||snapshot?.sources?.map(item=>item.id)||[],removed=(snapshot?.previousSourceIds||[]).filter(item=>!sourceIds.includes(item));asyncfunctiondecide(decision){if (busy)return;setBusy(true);try{constexactHash=awaitdigest(canonical);decideBaseline(structuredClone(s),candidate.id,decision,rationale,exactHash);mutate(d=>{decideBaseline(d,candidate.id,decision,rationale,exactHash);});notify(decision==='Approved'?'Baseline approved. Its workspace can use the approved version.':'Baseline request rejected.');}catch (error){notify(error.message);}finally{setBusy(false);}}return<><Headingeyebrow={`${s.session.team} · KNOWLEDGE GOVERNANCE`}title="Baseline approvals"description="Review baseline creation and edits before they become available."><Buttondisabled={!w?.baselineId||!w.crIds.length}onClick={()=>setRequesting(true)}>Merge selected CRs</Button></Heading><divclassName="approval-layout"><asideclassName="card approval-queue"><divclassName="section-heading"><h2>Requests</h2></div>{list.map(item=><buttonclassName={`approval-queue-item ${item.id===candidate?.id?'selected':''}`}key={item.id}onClick={()=>setId(item.id)}><span><strong>{item.ref}</strong><small>{item.type==='create'?'Baseline creation':item.type==='changes'?'CR merge':'Baseline edit'} · {newDate(item.createdAt).toLocaleDateString('en-IN')}</small></span><Badgetone={item.status==='Approved'?'green':'amber'}>{item.status}</Badge></button>)}{!list.length&&<Emptytitle="No approval requests"description="New baselines and document edits appear here."/>}</aside><div>{candidate?<><sectionclassName="card padded"><divclassName="section-heading inset"><div><spanclassName="eyebrow">{candidate.type==='create'?'BASELINE CREATION':candidate.type==='changes'?'CR MERGE':'BASELINE EDIT'}</span><h2>{candidate.ref}</h2></div><Badge>{candidate.status}</Badge></div><pclassName="supporting-copy">{candidate.description||'System knowledge submitted for approval.'}</p><divclassName="approval-document-list">{sourceIds.map(sourceId=>{constdoc=s.documents.find(item=>item.id===sourceId),added=snapshot?.previousSourceIds&&!snapshot.previousSourceIds.includes(sourceId);return<divclassName="review-source"key={sourceId}><buttonclassName="source-link"onClick={()=>setSource(sourceId)}><Iconname="file"size={19}/><span><strong>{doc?.name||sourceId}</strong><small>Revision {doc?.revision} · {doc?.size}</small></span></button><Badgetone={added?'green':'neutral'}>{added?'Added':'Retained'}</Badge></div>;})}{removed.map(sourceId=><divclassName="review-source"key={sourceId}><span>{s.documents.find(doc=>doc.id===sourceId)?.name}</span><Badgetone="amber">Removed in this edit</Badge></div>)}</div>{snapshot?.changes?.flatMap(cr=>cr.changes).map((change,index)=><divclassName="diff-block"key={index}><strong>{change.op} · {change.title}</strong>{change.before&&<p><small>BEFORE</small>{change.before}</p>}<p><small>AFTER</small>{change.after||'Removed from knowledge'}</p></div>)}<detailsclassName="manifest-details"><summary>Exact manifest SHA-256</summary><code>{hash||'Preparing manifest…'}</code></details></section><sectionclassName="card padded margin-top"><h2>Approval decision</h2>{blockers.map(issue=><Notetone="amber"key={issue}>{issue}</Note>)}{['I reviewed the submitted documents and proposed changes.','I approve this exact baseline version for use.'].map((text,index)=><labelclassName="check-label"key={text}><inputtype="checkbox"checked={candidate.status==='Approved'||checks[index]}disabled={!pending||!canApprove(s)}onChange={e=>setChecks(values=>values.map((value,i)=>i===index?e.target.checked:value))}/>{text}</label>)}<Fieldlabel="Decision notes"><textareavalue={rationale}onChange={e=>setRationale(e.target.value)}disabled={!pending}rows={2}placeholder="Add a review note…"/></Field><divclassName="row-actions"><Buttonvariant="primary"icon="shield"disabled={!canApprove(s)||!checks.every(Boolean)||!!blockers.length||!hash||busy||!pending}onClick={()=>decide('Approved')}>{candidate.type==='changes'?'Approve CR merge':`Approve baseline ${candidate.type==='create'?'creation':'edit'}`}</Button><Buttondisabled={!canApprove(s)||!pending||!rationale.trim()||busy}onClick={()=>decide('Rejected')}>Reject request</Button></div>{!canApprove(s)&&pending&&<Note>Awaiting an Administrator’s decision. <buttonclassName="text-button"onClick={()=>navigate('login')}>Administrator sign-in</button></Note>}</section>{!!candidate.decisions.length&&<sectionclassName="card padded margin-top"><h2>Decision history</h2>{candidate.decisions.map((decision,index)=><divclassName="decision-record"key={index}><Badge>{decision.decision}</Badge><strong>{decision.actor}</strong><small>{newDate(decision.at).toLocaleString('en-IN')}</small><p>{decision.rationale||'Submitted version reviewed and approved.'}</p></div>)}</section>}</>:<sectionclassName="card"><Emptytitle="Everything starts with a baseline"description="Create a baseline in your workspace and submit it for approval."action={<ButtononClick={()=>navigate('workspaces')}>Open workspaces</Button>}/></section>}</div></div>{requesting&&<Modaltitle="Merge CRs into a new baseline"close={()=>setRequesting(false)}wide><formonSubmit={submitMerge}><divclassName="selection-summary"><span>Current approved baseline</span><strong>{selection?.baseline?.ref} · v{selection?.baseline?.version}</strong><small>Unchanged requirements and existing documents are retained.</small></div><h3className="form-section-label">CRs to incorporate</h3>{selection?.changes.map(cr=><divclassName="history-row"key={cr.id}><Iconname="branch"size={18}/><span><strong>{cr.ref} · {cr.name}</strong><small>Revision {cr.revision} · {cr.changes.length} source-linked changes</small></span></div>)}<Fieldlabel="New baseline name"><inputrequiredmaxLength={60}placeholder="e.g. Membership and collateral - CR-1001"value={name}onChange={e=>setName(e.target.value)}/></Field><Note>Submit this merge for Administrator approval. Approval creates a new baseline version containing the selected CR changes. The current approved baseline stays available.</Note>{mergeError&&<pclassName="form-error"role="alert">{mergeError}</p>}<divclassName="modal-actions"><ButtononClick={()=>setRequesting(false)}>Cancel</Button><Buttontype="submit"variant="primary"disabled={!name.trim()||!selection?.changes.length}>Submit merge for approval</Button></div></form></Modal>}{source&&<SourceDialogsourceId={source}close={()=>setSource('')}/>}</>;}
Top comments (0)
Subscribe
We're a place where coders share, stay up-to-date and grow their careers.
Top comments (0)