DEV Community

Rahul
Rahul

Posted on

Testcraft Studio frontend 35: src/pages/Workspaces.jsx

import React, { useState } from 'react';
import Icon from '../Icon.jsx';
import { useStudio, uid, timestamp, log, workspaceStatus, questionsFor, resolveSelection, eligibleDocuments, visibleWorkspaces } from '../store.jsx';
import { Badge, Button, Empty, Field, Heading, Modal, Note, Pagination, Search } from '../ui.jsx';

export function TeamAccess({ close }) {
  const { state, workspace } = useStudio();
  return <Modal title="Shared team access" close={close}><p className="modal-intro">{workspace.name}</p><div className="selection-summary"><span>Owning team</span><strong>{workspace.team}</strong><small>Everyone signed in to this team uses the same workspace access.</small></div><Note>Users can create System Baselines, upload evidence, resolve questions, generate tests, and edit suites. Administrators also add teams and approve System Baseline creation and edits.</Note><div className="modal-actions"><Button onClick={close}>Done</Button></div></Modal>;
}

export function WorkspaceWizard({ close, navigate, copyId = '' }) {
  const { state: s, mutate, notify } = useStudio();
  const [step, setStep] = useState(0), [path, setPath] = useState(copyId ? 'copy' : 'fresh');
  const [source, setSource] = useState(copyId || visibleWorkspaces(s)[0]?.id || '');
  const [baselineId, setBaselineId] = useState(s.baselines.find(b => b.team === s.session.team && ['Active', 'Approved'].includes(b.status))?.id || '');
  const [name, setName] = useState(''), [purpose, setPurpose] = useState(''), [visibility] = useState('team');
  const original = visibleWorkspaces(s).find(w => w.id === source);
  const selectedId = path === 'fresh' ? '' : path === 'copy' ? original?.baselineId : baselineId;
  const selectedCrs = path === 'copy' ? original?.crIds || [] : [];
  const resolution = selectedId ? resolveSelection(s, selectedId, selectedCrs) : { issues: [], baseline: null };
  const canContinue = path === 'fresh' || (path === 'copy' ? !!original : !!baselineId);
  function create(e) {
    e.preventDefault(); if (step !== 2 || resolution.issues.length || !name.trim()) return;
    const id = uid('workspace');
    mutate(d => {
      const w = { id, name: name.trim(), ref: `WS-${String(d.workspaces.length + 1).padStart(4, '0')}`, purpose: purpose.trim(), team: s.session.team, baselineId: selectedId || '', crIds: selectedCrs, visibility, members: [s.session.team], version: 1, updatedAt: timestamp(), requirementCount: path === 'copy' ? original.requirementCount : (resolution.baseline?.rules.length || 0), sharedContractIds: path === 'copy' ? original.sharedContractIds : [], sourceIds: path === 'copy' ? eligibleDocuments(s, original).map(doc => doc.id) : [], customRules: path === 'copy' ? structuredClone(original.customRules || []) : [] };
      d.workspaces.unshift(w); d.activeWorkspaceId = id;
      if (path === 'copy') questionsFor(s, original).forEach(q => d.questions.push({ ...q, id: uid('question'), workspaceId: id, version: 1, answer: '', sourceReference: '', answerSourceId: '', author: null, resolvedBy: null, status: 'open', validatedAt: null }));
      log(d, `Created ${w.ref}${resolution.baseline ? ` from ${resolution.baseline.ref}` : ''}`);
    }); notify('Workspace created. Chat starts with a fresh context.'); close(); navigate('workspace');
  }
  return <Modal title="Create a workspace" close={close} wide><div className="wizard-steps">{['Starting point', 'Workspace details', 'Review & create'].map((text, i) => <span key={text} className={step >= i ? 'active' : ''}>{i + 1}<small>{text}</small></span>)}</div>{step === 0 ? <><p className="modal-intro">Start with the right knowledge for your change.</p><div className="choice-list">{[{ id: 'fresh', icon: 'plus', title: 'Start a fresh workspace', text: 'Create your workspace first, then upload its baseline documents.' }, { id: 'baseline', icon: 'book', title: 'Use a System Baseline', text: 'Start from available, reusable system knowledge.' }, { id: 'copy', icon: 'folder', title: 'Copy an existing workspace', text: 'Reuse its selection. Chat history and answers start fresh.' }].map(c => <button className={`choice-card ${path === c.id ? 'selected' : ''}`} key={c.id} onClick={() => setPath(c.id)}><span className="soft-icon soft-purple"><Icon name={c.icon} size={20}/></span><span><strong>{c.title}</strong><p>{c.text}</p></span><span className="radio-dot"/></button>)}</div>{path === 'baseline' && <Field label="System Baseline"><select value={baselineId} onChange={e => setBaselineId(e.target.value)}><option value="">Choose a baseline</option>{s.baselines.filter(b => b.team === s.session.team && ['Active', 'Approved'].includes(b.status)).map(b => <option key={b.id} value={b.id}>{b.ref} · v{b.version}</option>)}</select></Field>}{path === 'copy' && <Field label="Source workspace"><select value={source} onChange={e => setSource(e.target.value)}><option value="">Choose a workspace</option>{visibleWorkspaces(s).map(w => <option key={w.id} value={w.id}>{w.ref} · {w.name}</option>)}</select></Field>}<div className="modal-actions"><Button onClick={close}>Cancel</Button><Button variant="primary" disabled={!canContinue} onClick={() => { setStep(1); }}>Continue <Icon name="arrow" size={15}/></Button></div></> : <form onSubmit={create}>{step === 1 ? <><Field label="Workspace name"><input required autoFocus value={name} maxLength={100} onChange={e => setName(e.target.value)} placeholder="e.g. Participant validation review"/></Field><Field label="Purpose"><textarea value={purpose} maxLength={600} onChange={e => setPurpose(e.target.value)} placeholder="What change will this workspace test?" rows={3}/></Field><Note>This workspace is shared by everyone signed in to {s.session.team}. There are no individual access roles.</Note></> : <><div className="selection-summary"><span>Workspace</span><strong>{name}</strong><small>{purpose || 'No description added.'}</small><strong>{resolution.baseline?.ref || 'Add a System Baseline after creating the workspace'} {selectedCrs.map(id => `+ ${s.crs.find(c => c.id === id)?.ref}`).join(' ')}</strong><small>{visibility} · {s.session.team} · New chat context</small></div>{resolution.issues.map(issue => <Note key={issue} tone="amber">{issue}</Note>)}<Note>Immutable source revisions are referenced. Copies do not duplicate source files or inherit private chat history.</Note></>}<div className="modal-actions"><Button onClick={() => setStep(step - 1)}>Back</Button>{step === 1 ? <Button variant="primary" disabled={!name.trim()} onClick={e => { e.preventDefault(); setStep(2); }}>Review workspace</Button> : <Button type="submit" variant="primary" icon="check" disabled={resolution.issues.length > 0}>Create workspace</Button>}</div></form>}</Modal>;
}

export default function Workspaces({ navigate, create = false }) {
  const { state: s, mutate } = useStudio();
  const [query, setQuery] = useState(''), [filter, setFilter] = useState('All statuses'), [page, setPage] = useState(1), [wizard, setWizard] = useState(create), [copy, setCopy] = useState('');
  const list = visibleWorkspaces(s).filter(w => `${w.name} ${w.ref}`.toLowerCase().includes(query.toLowerCase()) && (filter === 'All statuses' || workspaceStatus(s, w) === filter));
  const open = w => { mutate(d => { d.activeWorkspaceId = w.id; }); navigate('workspace'); };
  return <><Heading eyebrow={`${s.session.team} · TEAM SPACE`} title="Your workspaces" description="Every change, its own knowledge context and review history."><Button icon="plus" variant="primary" onClick={() => { setCopy(''); setWizard(true); }}>Create workspace</Button></Heading><section className="card"><div className="table-toolbar spacious"><Search value={query} onChange={v => { setQuery(v); setPage(1); }} placeholder="Search names or workspace references…"/><select className="simple-select" value={filter} onChange={e => { setFilter(e.target.value); setPage(1); }}>{['All statuses', 'Awaiting baseline approval', 'Add baseline', 'Ready for review', 'Ready to generate', 'Draft'].map(v => <option key={v}>{v}</option>)}</select></div><div className="table-scroll"><table><thead><tr><th>WORKSPACE</th><th>BASELINE</th><th>STATUS</th><th>ACCESS</th><th>ACTIONS</th></tr></thead><tbody>{list.slice((page - 1) * 6, page * 6).map(w => <tr key={w.id}><td><button className="workspace-table-name" onClick={() => open(w)}><span className="table-icon soft-purple"><Icon name="folder" size={18}/></span><span><strong>{w.name}</strong><small>{w.ref} · Shared team access</small></span></button></td><td>{s.baselines.find(b => b.id === w.baselineId)?.ref || 'Not connected yet'}</td><td><Badge>{workspaceStatus(s, w)}</Badge></td><td><Icon name="users" size={13}/> Shared team</td><td><div className="row-actions"><Button onClick={() => open(w)}>Open</Button><Button onClick={() => { setCopy(w.id); setWizard(true); }}>Copy</Button></div></td></tr>)}</tbody></table></div>{!list.length && <Empty title="No workspaces found" description="Try another search or create your first workspace."/>}<Pagination count={list.length} page={page} onChange={setPage}/></section>{wizard && <WorkspaceWizard key={copy} copyId={copy} close={() => { setWizard(false); if (create) navigate('workspaces'); }} navigate={navigate}/>}</>;
}

Enter fullscreen mode Exit fullscreen mode

Top comments (0)