DEV Community

Rahul
Rahul

Posted on

Testcraft Studio frontend 38: src/store.jsx

import React, { createContext, useContext, useEffect, useState } from 'react';
import { makeSeed, upgradeDemoState } from './seed.js';

const KEY = 'testcraft-studio-v2';
const LEGACY_KEY = 'ccil-studio-demo-v1';
const Store = createContext(null);
export const uid = prefix => `${prefix}-${globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`}`;
export const timestamp = () => new Date().toISOString();
export const contextKey = w => `${w.id}:${w.version}`;
export const roleLabels = { user: 'User', administrator: 'Administrator' };
export const canReview = s => ['user', 'administrator'].includes(s.session.role);
export const canApprove = s => s.session.authenticated === true && s.session.role === 'administrator';
export const visibleWorkspaces = s => s.workspaces.filter(w => w.team === s.session.team);
export function questionsFor(s, w) { return w ? s.questions.filter(q => q.workspaceId === w.id && q.version === w.version) : []; }
export function eligibleDocuments(s, w) {
  if (!w) return [];
  const baseline = s.baselines.find(b => b.id === w.baselineId);
  const rules = new Map((baseline?.rules || []).map(r => [r.id, r]));
  s.crs.filter(c => w.crIds.includes(c.id)).forEach(cr => cr.changes.forEach(change => { if (change.op === 'REMOVE') rules.delete(change.ruleId); else rules.set(change.ruleId, { source: change.source }); }));
  (w.customRules || []).forEach(r => rules.set(r.id, r));
  const currentSources = new Set([...rules.values()].map(r => r.source));
  const ruleSources = new Set([...s.baselines.flatMap(b => b.rules.map(r => r.source)), ...s.crs.flatMap(cr => cr.changes.map(c => c.source))]);
  return s.documents.filter(d => {
    const member = (d.team === w.team && (baseline?.sourceIds.includes(d.id) || (w.sourceIds || []).includes(d.id) || d.workspaceId === w.id || w.crIds.includes(d.crId))) || ((w.sharedContractIds || []).includes(d.id) && s.shares.some(g => g.sourceId === d.id && g.consumer === w.team && g.status === 'Active'));
    if (!member) return false;
    if (d.crId && !w.crIds.includes(d.crId) && !baseline?.incorporatedCRs.includes(d.crId)) return false;
    return !ruleSources.has(d.id) || currentSources.has(d.id);
  });
}
export function resolveSelection(s, baselineId, crIds) {
  const baseline = s.baselines.find(b => b.id === baselineId);
  const issues = [];
  if (!baseline || !['Active', 'Approved'].includes(baseline.status)) issues.push('Select an active System Baseline.');
  if (baseline && baseline.team !== s.session.team) issues.push('The baseline must belong to the selected team.');
  const changes = s.crs.filter(c => crIds.includes(c.id));
  for (const cr of changes) {
    if (cr.team !== s.session.team) issues.push(`${cr.ref} belongs to another team.`);
    if (baseline?.incorporatedCRs.includes(cr.id)) issues.push(`${cr.ref} is already incorporated in ${baseline.ref}.`);
    for (const required of cr.requires) if (!crIds.includes(required) && !baseline?.incorporatedCRs.includes(required)) issues.push(`${cr.ref} requires ${s.crs.find(c => c.id === required)?.ref || required}.`);
    for (const conflict of cr.conflicts) if (crIds.includes(conflict)) issues.push(`${cr.ref} conflicts with ${s.crs.find(c => c.id === conflict)?.ref || conflict}.`);
  }
  return { baseline, changes, issues: [...new Set(issues)], diff: changes.flatMap(c => c.changes.map(change => ({ ...change, crRef: c.ref }))) };
}
export function readiness(s, w) {
  if (!w) return { ready: false, blockers: ['Select a workspace.'], questions: [], sources: [], verified: 0 };
  const questions = questionsFor(s, w);
  const sources = eligibleDocuments(s, w);
  const unresolved = questions.filter(q => q.status !== 'resolved');
  const selection = resolveSelection(s, w.baselineId, w.crIds);
  const blockers = [...selection.issues];

  if (!w.requirementCount) blockers.push('No reviewed requirements are available.');
  if (!sources.some(d => d.status === 'Ready' || d.status === 'Reviewed locally')) blockers.push('No reviewed evidence is available.');
  if (sources.some(d => !['Ready', 'Reviewed locally'].includes(d.status))) blockers.push('Some sources still need local review.');
  const missingShared = (w.sharedContractIds || []).filter(id => !sources.some(d => d.id === id));
  if (missingShared.length) blockers.push('A selected shared contract is no longer accessible. Update the knowledge selection.');
  const evidenceIds = new Set(sources.filter(d => ['Ready', 'Reviewed locally'].includes(d.status)).map(d => d.id));
  const ruleMap = new Map((selection.baseline?.rules || []).map(r => [r.id, r]));
  selection.diff.forEach(c => { if (c.op === 'REMOVE') ruleMap.delete(c.ruleId); else ruleMap.set(c.ruleId, { source: c.source }); });
  (w.customRules || []).forEach(r => ruleMap.set(r.id, r));
  if ([...ruleMap.values()].some(r => !evidenceIds.has(r.source))) blockers.push('A selected requirement is missing accessible reviewed evidence.');
  return { ready: !blockers.length, blockers, questions, sources, verified: Math.max(0, w.requirementCount - new Set(unresolved.map(q => q.requirement)).size), selection };
}
export function workspaceStatus(s, w) {
  if (!w.baselineId) return w.pendingBaselineId ? 'Awaiting baseline approval' : 'Add baseline';
  if (!w.requirementCount) return 'Draft';

  if (s.suites.some(t => t.workspaceId === w.id && t.version === w.version)) return 'Ready for review';
  return 'Ready to generate';
}
export function bumpVersion(draft, w) {
  const old = questionsFor(draft, w);
  w.version += 1; w.updatedAt = timestamp();
  old.forEach(q => draft.questions.push({ ...q, id: uid('question'), version: w.version, status: 'open', answer: '', sourceReference: '', answerSourceId: '', author: null, answeredAt: null, validatedBy: null, validatedAt: null }));
}
export function log(draft, action) { draft.audit.unshift({ id: uid('audit'), action, actor: draft.session.name, team: draft.session.team, at: timestamp() }); }
export function makeFreshState() {
  const state = makeSeed();
  for (const key of ['workspaces', 'documents', 'baselines', 'crs', 'questions', 'candidates', 'suites', 'jobs', 'shares', 'examples', 'audit']) state[key] = [];
  state.threads = {}; state.activeWorkspaceId = ''; state.session.authenticated = false;
  return state;
}
function load() {
  try {
    const stored = JSON.parse(localStorage.getItem(KEY) || localStorage.getItem(LEGACY_KEY));
    if (stored?.schema === 1) { stored.jobs = stored.jobs.map(j => ['running', 'queued'].includes(j.status) ? { ...j, status: 'Interrupted', stage: 'Browser restarted. Retry this run.' } : j); return upgradeDemoState(stored); }
  } catch { /* Missing or invalid local data falls back to the sample dataset. */ }
  return makeFreshState();
}
export function StoreProvider({ children }) {
  const [state, setState] = useState(load);
  const [saveError, setSaveError] = useState('');
  const [toast, setToast] = useState('');
  useEffect(() => { try { localStorage.setItem(KEY, JSON.stringify(state)); setSaveError(''); } catch { setSaveError('Browser storage is full or unavailable. Your latest changes are only in memory. Export your records from Administration.'); } }, [state]);
  useEffect(() => { if (!toast) return; const timer = setTimeout(() => setToast(''), 4500); return () => clearTimeout(timer); }, [toast]);
  const mutate = action => setState(previous => { const next = structuredClone(previous); action(next); return next; });
  const workspace = visibleWorkspaces(state).find(w => w.id === state.activeWorkspaceId);
  const reset = () => { setState(makeFreshState()); setToast('Application records reset.'); };
  return <Store.Provider value={{ state, mutate, workspace, toast, notify: setToast, saveError, reset }}>{children}</Store.Provider>;
}
export const useStudio = () => useContext(Store);

export function download(name, content, type = 'text/plain') {
  const url = URL.createObjectURL(new Blob([content], { type }));
  const a = document.createElement('a'); a.href = url; a.download = name; a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
export function csv(rows) { return '\uFEFF' + rows.map(row => row.map(value => { let text = String(value ?? ''); if (/^[=+@\-\t\r]/.test(text)) text = `'${text}`; return `"${text.replaceAll('"', '""')}"`; }).join(',')).join('\r\n'); }
export function parseCsv(text) {
  const rows = []; let row = [], cell = '', quoted = false;
  const input = text.replace(/^\uFEFF/, '');
  for (let i = 0; i < input.length; i++) {
    const ch = input[i];
    if (ch === '"') { if (quoted && input[i + 1] === '"') { cell += '"'; i++; } else quoted = !quoted; }
    else if (ch === ',' && !quoted) { row.push(cell); cell = ''; }
    else if (ch === '\n' && !quoted) { row.push(cell.replace(/\r$/, '')); rows.push(row); row = []; cell = ''; }
    else cell += ch;
  }
  if (quoted) throw new Error('The CSV contains an unclosed quoted value.');
  row.push(cell.replace(/\r$/, '')); if (row.some(Boolean)) rows.push(row);
  return rows;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)