DEV Community

Rahul
Rahul

Posted on

Testcraft Studio frontend 23: src/pages/Approvals.jsx

import React, { useEffect, useState } from 'react';
import Icon from '../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';
async function digest(value) { const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value)); return [...new Uint8Array(bytes)].map(b => b.toString(16).padStart(2, '0')).join(''); }

export default function Approvals({ navigate, merge = false }) {
  const { state: s, mutate, workspace: w, notify } = useStudio();
  const list = 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]);
  const selection = w ? resolveSelection(s, w.baselineId, w.crIds) : null;
  function submitMerge(event) {
    event.preventDefault(); setMergeError('');
    try { const prepared = 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); }
  }
  const candidate = list.find(c => c.id === id) || list[0], snapshot = candidate?.manifest, canonical = JSON.stringify(snapshot || {});
  useEffect(() => { setChecks([false, false]); setRationale(''); setHash(''); let live = true; if (candidate) digest(canonical).then(value => { if (live) setHash(value); }); return () => { live = false; }; }, [candidate?.id, canonical]);
  const blockers = approvalBlockers(s, candidate), pending = candidate?.status === 'Pending review';
  const sourceIds = snapshot?.baseline?.sourceIds || snapshot?.sources?.map(item => item.id) || [], removed = (snapshot?.previousSourceIds || []).filter(item => !sourceIds.includes(item));
  async function decide(decision) { if (busy) return; setBusy(true); try { const exactHash = await digest(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 <><Heading eyebrow={`${s.session.team} · KNOWLEDGE GOVERNANCE`} title="Baseline approvals" description="Review baseline creation and edits before they become available."><Button disabled={!w?.baselineId || !w.crIds.length} onClick={() => setRequesting(true)}>Merge selected CRs</Button></Heading><div className="approval-layout"><aside className="card approval-queue"><div className="section-heading"><h2>Requests</h2></div>{list.map(item => <button className={`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'} · {new Date(item.createdAt).toLocaleDateString('en-IN')}</small></span><Badge tone={item.status === 'Approved' ? 'green' : 'amber'}>{item.status}</Badge></button>)}{!list.length && <Empty title="No approval requests" description="New baselines and document edits appear here."/>}</aside><div>{candidate ? <>
    <section className="card padded"><div className="section-heading inset"><div><span className="eyebrow">{candidate.type === 'create' ? 'BASELINE CREATION' : candidate.type === 'changes' ? 'CR MERGE' : 'BASELINE EDIT'}</span><h2>{candidate.ref}</h2></div><Badge>{candidate.status}</Badge></div><p className="supporting-copy">{candidate.description || 'System knowledge submitted for approval.'}</p><div className="approval-document-list">{sourceIds.map(sourceId => { const doc = s.documents.find(item => item.id === sourceId), added = snapshot?.previousSourceIds && !snapshot.previousSourceIds.includes(sourceId); return <div className="review-source" key={sourceId}><button className="source-link" onClick={() => setSource(sourceId)}><Icon name="file" size={19}/><span><strong>{doc?.name || sourceId}</strong><small>Revision {doc?.revision} · {doc?.size}</small></span></button><Badge tone={added ? 'green' : 'neutral'}>{added ? 'Added' : 'Retained'}</Badge></div>; })}{removed.map(sourceId => <div className="review-source" key={sourceId}><span>{s.documents.find(doc => doc.id === sourceId)?.name}</span><Badge tone="amber">Removed in this edit</Badge></div>)}</div>{snapshot?.changes?.flatMap(cr => cr.changes).map((change, index) => <div className="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>)}<details className="manifest-details"><summary>Exact manifest SHA-256</summary><code>{hash || 'Preparing manifest…'}</code></details></section>
    <section className="card padded margin-top"><h2>Approval decision</h2>{blockers.map(issue => <Note tone="amber" key={issue}>{issue}</Note>)}{['I reviewed the submitted documents and proposed changes.', 'I approve this exact baseline version for use.'].map((text, index) => <label className="check-label" key={text}><input type="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>)}<Field label="Decision notes"><textarea value={rationale} onChange={e => setRationale(e.target.value)} disabled={!pending} rows={2} placeholder="Add a review note…"/></Field><div className="row-actions"><Button variant="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><Button disabled={!canApprove(s) || !pending || !rationale.trim() || busy} onClick={() => decide('Rejected')}>Reject request</Button></div>{!canApprove(s) && pending && <Note>Awaiting an Administrator’s decision. <button className="text-button" onClick={() => navigate('login')}>Administrator sign-in</button></Note>}</section>{!!candidate.decisions.length && <section className="card padded margin-top"><h2>Decision history</h2>{candidate.decisions.map((decision, index) => <div className="decision-record" key={index}><Badge>{decision.decision}</Badge><strong>{decision.actor}</strong><small>{new Date(decision.at).toLocaleString('en-IN')}</small><p>{decision.rationale || 'Submitted version reviewed and approved.'}</p></div>)}</section>}
    </> : <section className="card"><Empty title="Everything starts with a baseline" description="Create a baseline in your workspace and submit it for approval." action={<Button onClick={() => navigate('workspaces')}>Open workspaces</Button>}/></section>}</div></div>
    {requesting && <Modal title="Merge CRs into a new baseline" close={() => setRequesting(false)} wide>
      <form onSubmit={submitMerge}>
        <div className="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>
        <h3 className="form-section-label">CRs to incorporate</h3>
        {selection?.changes.map(cr => <div className="history-row" key={cr.id}><Icon name="branch" size={18}/><span><strong>{cr.ref} · {cr.name}</strong><small>Revision {cr.revision} · {cr.changes.length} source-linked changes</small></span></div>)}
        <Field label="New baseline name"><input required maxLength={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 && <p className="form-error" role="alert">{mergeError}</p>}
        <div className="modal-actions"><Button onClick={() => setRequesting(false)}>Cancel</Button><Button type="submit" variant="primary" disabled={!name.trim() || !selection?.changes.length}>Submit merge for approval</Button></div>
      </form>
    </Modal>} {source && <SourceDialog sourceId={source} close={() => setSource('')}/>}</>;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)