diff --git a/e2e/board.smoke.spec.ts b/e2e/board.smoke.spec.ts index 931d2e0..c12b19c 100644 --- a/e2e/board.smoke.spec.ts +++ b/e2e/board.smoke.spec.ts @@ -131,11 +131,19 @@ test('move, folder expansion, empty-board pan, desktop wheel zoom, mobile pinch, await organizationConcept.getByRole('button', { name: 'EDIT DOSSIER', exact: true }).click() await page.getByLabel('Organization type').selectOption('public_body') await waitForSave(page, () => page.getByRole('button', { name: 'SAVE DOSSIER', exact: true }).click()) + await page.getByRole('button', { name: 'CREATE PARTY NOT LISTED ABOVE', exact: true }).click() + await expect(page.getByText('Create party dossier', { exact: true })).toBeVisible() + await page.getByLabel('Party type').selectOption('person') + await page.getByLabel('Party name').fill('Mara Elise Voss') + await page.getByLabel('Party summary').fill('Identified by the investigator outside the supplied concept list.') + await page.locator('.party-editor .folder-members input[type="checkbox"]').first().check() + await waitForSave(page, () => page.getByRole('button', { name: 'SAVE DOSSIER', exact: true }).click()) + await expect(page.locator('.brief-panel')).toBeVisible() await page.getByRole('button', { name: 'RETURN TO BOARD', exact: true }).click() await page.reload() await expect(page.locator('.brief-panel')).toBeHidden() - await expect(page.locator('.evidence-card.party')).toHaveCount(2) - await expect(page.locator('.evidence-card.party')).toContainText(['Ada Lovelace', 'Difference Engine Bureau']) + await expect(page.locator('.evidence-card.party')).toHaveCount(3) + await expect(page.locator('.evidence-card.party')).toContainText(['Ada Lovelace', 'Difference Engine Bureau', 'Mara Elise Voss']) await page.getByRole('button', { name: 'NEW EVENT', exact: true }).click() await expect(page.getByText('Edit reconstructed event')).toBeVisible() diff --git a/src/App.tsx b/src/App.tsx index b221926..3190c38 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,6 +41,7 @@ export function App() { const [editingFileId, setEditingFileId] = useState(null) const [editingEventId, setEditingEventId] = useState(null) const [editingPartyId, setEditingPartyId] = useState(null) + const [newPartyDraft, setNewPartyDraft] = useState(null) const [briefOpen, setBriefOpen] = useState(false) const [editingBrief, setEditingBrief] = useState(false) const [editingTimeline, setEditingTimeline] = useState(false) @@ -147,6 +148,15 @@ export function App() { setSelected(event.id); setRecentlyCreatedExhibitId(event.id); setEditingEventId(event.id) } + const addParty = () => { + if (!caseState) return + const { viewport } = caseState + const position = nextOpenBoardPosition(caseState.evidence, { + x: Math.max(100, (670 - viewport.x) / viewport.zoom), y: Math.max(100, (310 - viewport.y) / viewport.zoom), + }, { width: 280 }) + setNewPartyDraft({ id: uid('party'), type: 'party', partyKind: 'person', title: '', content: '', aliases: [], relatedEvidenceIds: [], ...position, width: 280 }) + } + const classifyConcept = (conceptId: string, partyKind: PartyKind) => { if (!caseState) return const concept = caseState.brief.concepts.find(item => item.id === conceptId) @@ -330,6 +340,7 @@ export function App() { onClose={closeBrief} onEdit={() => setEditingBrief(true)} onClassify={classifyConcept} + onNewParty={addParty} onLocate={focusEvidence} onEditParty={setEditingPartyId} />} @@ -341,6 +352,7 @@ export function App() { + @@ -377,6 +389,21 @@ export function App() { setStatus('PARTY DOSSIER UPDATED') }} />} + {newPartyDraft && setNewPartyDraft(null)} + onSave={party => { + update(state => ({ ...state, evidence: [...state.evidence, party] })) + setNewPartyDraft(null) + setSelected(party.id) + setRecentlyCreatedExhibitId(party.id) + setStatus(`${party.partyKind === 'person' ? 'PERSON' : 'ORGANIZATION'} DOSSIER CREATED`) + }} + />} {editingBrief && setEditingBrief(false)} @@ -665,12 +692,13 @@ function ThreadEditor({ connection, sourceName, targetName, isNew, onClose, onSa } -function BriefPanel({ brief, parties, recentlyCreatedExhibitId, canEdit, onClose, onEdit, onClassify, onLocate, onEditParty }: { brief: LevelBrief; parties: Evidence[]; recentlyCreatedExhibitId: string | null; canEdit: boolean; onClose: () => void; onEdit: () => void; onClassify: (id: string, kind: PartyKind) => void; onLocate: (id: string) => void; onEditParty: (id: string) => void }) { +function BriefPanel({ brief, parties, recentlyCreatedExhibitId, canEdit, onClose, onEdit, onClassify, onNewParty, onLocate, onEditParty }: { brief: LevelBrief; parties: Evidence[]; recentlyCreatedExhibitId: string | null; canEdit: boolean; onClose: () => void; onEdit: () => void; onClassify: (id: string, kind: PartyKind) => void; onNewParty: () => void; onLocate: (id: string) => void; onEditParty: (id: string) => void }) { const partyById = new Map(parties.map(party => [party.id, party])) const unresolved = brief.concepts.filter(concept => !concept.resolvedPartyExhibitId).length return @@ -692,20 +720,22 @@ function BriefEditor({ brief, onClose, onSave }: { brief: LevelBrief; onClose: ( } -function PartyEditor({ party, evidence, documents, onClose, onSave }: { party: Evidence; evidence: Evidence[]; documents: CaseDocument[]; onClose: () => void; onSave: (party: Evidence) => void }) { +function PartyEditor({ party, evidence, documents, creating = false, onClose, onSave }: { party: Evidence; evidence: Evidence[]; documents: CaseDocument[]; creating?: boolean; onClose: () => void; onSave: (party: Evidence) => void }) { const [name, setName] = useState(party.title) const [summary, setSummary] = useState(party.content) + const [partyKind, setPartyKind] = useState(party.partyKind || 'person') const [organizationKind, setOrganizationKind] = useState(party.organizationKind || 'business') const [aliases, setAliases] = useState((party.aliases || []).join('\n')) const [related, setRelated] = useState(party.relatedEvidenceIds || []) const candidates = [...evidence.filter(item => item.id !== party.id && item.type !== 'party').map(item => ({ id: item.id, title: item.title, kind: item.type.toUpperCase() })), ...documents.map(item => ({ id: item.id, title: item.title, kind: item.fileType.toUpperCase() }))] const toggle = (id: string) => setRelated(current => current.includes(id) ? current.filter(item => item !== id) : [...current, id]) - return
{ submit.preventDefault(); onSave({ ...party, title: name.trim() || party.title, content: summary.trim(), organizationKind: party.partyKind === 'organization' ? organizationKind : undefined, aliases: aliases.split('\n').map(item => item.trim()).filter(Boolean), relatedEvidenceIds: related }) }}> -
{party.partyKind === 'person' ? : }Edit {party.partyKind} dossier
-
PARTY EXHIBIT · {party.partyKind?.toUpperCase()} - + return
{ submit.preventDefault(); onSave({ ...party, partyKind, title: name.trim(), content: summary.trim(), organizationKind: partyKind === 'organization' ? organizationKind : undefined, aliases: aliases.split('\n').map(item => item.trim()).filter(Boolean), relatedEvidenceIds: related }) }}> +
{partyKind === 'person' ? : }{creating ? 'Create party dossier' : `Edit ${partyKind} dossier`}
+
PARTY EXHIBIT · {partyKind.toUpperCase()} + {creating && } +