Refine board notes, clipping evidence, and case reports
This commit is contained in:
@@ -104,7 +104,7 @@ suite('normalized level persistence API', () => {
|
||||
const document: DocumentExhibit = { id: randomUUID(), type: 'document', title: 'Evidence', publishedAt: '2021-04-17T12:00:00Z', body: ['Extracted body'], regions: [{ id: 'stamp', label: 'Date stamp', excerpt: '17 April 2021', date: '2021-04-17T09:00:00Z' }], fileType: 'image', captureKind:'full_page',metadata: {}, ...placed(1051, 417, 205, 282, 2) }
|
||||
const gatedDocument: DocumentExhibit = { id: randomUUID(), type: 'document', title: 'Later tip', body: [], regions: [], fileType: 'image', captureKind:'clipping',metadata: {}, requiredFlags: ['tip.received'], ...placed(1260, 417, 210, 178, 3) }
|
||||
const folder: FolderExhibit = { id: randomUUID(), type: 'folder', title: 'Folder', content: 'Evidence folder', isOpen: true, ...placed(685, 417, 260, 166) }
|
||||
const note: NoteExhibit = { id: randomUUID(), type: 'note', title: 'Extract', content: 'Date matters', ...placed(420, 300, 108, 154) }
|
||||
const note: NoteExhibit = { id:randomUUID(),type:'note',title:'Extract',content:'Date matters',presentation:'lined_sheet',...placed(420,300,220,270) }
|
||||
const event: EventExhibit = { id: randomUUID(), type: 'event', title: 'The meeting occurred', content: 'The evidence places the meeting on 18 April.', eventDate: '2021-04-18T14:30:00Z', ...placed(520, 610, 270, 174) }
|
||||
const party: PartyExhibit = { id: randomUUID(), type: 'party', partyKind: 'person', title: 'Ada Lovelace', content: 'Named as correspondent.', aliases: ['A. A. L.'], ...placed(720, 250, 280, 190) }
|
||||
state.exhibits = [document, gatedDocument, folder, note, event, party]
|
||||
@@ -123,6 +123,7 @@ suite('normalized level persistence API', () => {
|
||||
const loaded = await (await adminFetch(`${baseUrl}/api/levels/${state.id}?edit=1`)).json() as CaseState
|
||||
expect(loaded.views[0]).toMatchObject({ type: 'timeline', rangeMode: 'fixed', range: timeline.range })
|
||||
expect(loaded.exhibits.find(item => item.id === folder.id)).toMatchObject({ x: 685, y: 417, isOpen: true })
|
||||
expect(loaded.exhibits.find(item => item.id === note.id)).toMatchObject({ presentation:'lined_sheet',width:220,height:270 })
|
||||
expect(loaded.exhibits.find(item => item.id === document.id)).toMatchObject({ captureKind:'full_page',width:205,height:282 })
|
||||
expect(loaded.relations).toEqual(expect.arrayContaining([expect.objectContaining({ type: 'supports', fromExhibitId: event.id, toExhibitId: note.id })]))
|
||||
expect(loaded.connections[0]).toMatchObject({ fromExhibitId: folder.id, toExhibitId: document.id, label: 'Primary source' })
|
||||
@@ -234,6 +235,7 @@ suite('normalized level persistence API', () => {
|
||||
expect(clone.exhibits.find(item => item.type === 'folder')).toMatchObject({ x: 685, y: 417 })
|
||||
expect(clone.relations.filter(relation => relation.type === 'supports')).toHaveLength(2)
|
||||
expect(clone.connections[0]).toMatchObject({ label: 'Primary source', tightness: 85 })
|
||||
expect(clone.exhibits.find(item => item.type === 'note')).toMatchObject({ presentation:'lined_sheet',width:220,height:270 })
|
||||
expect(clone.brief.concepts[0].resolvedPartyExhibitId).not.toBe(party.id)
|
||||
const authoredClone = await (await adminFetch(`${baseUrl}/api/levels/${clone.id}?edit=1`)).json() as CaseState
|
||||
expect(authoredClone.exhibits.find(item => item.type === 'document' && item.title === 'Later tip')).toMatchObject({ captureKind:'clipping',requiredFlags: ['tip.received'] })
|
||||
@@ -330,7 +332,7 @@ suite('normalized level persistence API', () => {
|
||||
relatedState.connections.push({ id:relatedConnectionId,fromExhibitId:relatedClaim.id,toExhibitId:fatherDocument.id,label:'Proof that the Barricelli family included an inventor.',tightness:65,tagStyle:'luggage',tagPosition:50,tagOffset:0 })
|
||||
expect((await fetch(`${baseUrl}/api/levels/${relatedState.id}`,{ method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify(relatedState) })).status).toBe(200)
|
||||
const relatedSubmission=await (await fetch(`${baseUrl}/api/levels/${relatedState.id}/report/submissions`,{ method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({
|
||||
investigatorName:'Test Player',evidence:[{ connectionId:relatedConnectionId,documentExhibitId:fatherDocument.id,relationText:'Proof that the Barricelli family included an inventor.' }],
|
||||
investigatorName:'Test Player',
|
||||
}) })).json()
|
||||
expect(relatedSubmission).toMatchObject({ status:'evidence_insufficient',issues:expect.arrayContaining(['missing_accepted_evidence','connected_evidence_unverified']),
|
||||
feedback:expect.stringContaining('related person rather than the claim subject'),claims:[expect.objectContaining({ evidence:[expect.objectContaining({
|
||||
@@ -350,14 +352,24 @@ suite('normalized level persistence API', () => {
|
||||
reportState.connections.push({ id:connectionId,fromExhibitId:claim.id,toExhibitId:targetDocument.id,label:'Proof that…',tightness:65,tagStyle:'luggage',tagPosition:50,tagOffset:0 })
|
||||
expect((await fetch(`${baseUrl}/api/levels/${reportState.id}`,{ method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify(reportState) })).status).toBe(200)
|
||||
const incomplete=await fetch(`${baseUrl}/api/levels/${reportState.id}/report/submissions`,{ method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({
|
||||
investigatorName:'Test Player',evidence:[{ connectionId,documentExhibitId:targetDocument.id,relationText:'Proof that…' }],
|
||||
investigatorName:'Test Player',
|
||||
// Legacy/forged report fields must not mutate board-owned provenance.
|
||||
evidence:[{ connectionId,documentExhibitId:targetDocument.id,relationText:'Forged complete statement',publishedAt:'1953-08-19',sourceCitation:'Forged source' }],
|
||||
}) })
|
||||
expect(incomplete.status).toBe(201)
|
||||
expect(await incomplete.json()).toMatchObject({ status:'evidence_accepted_report_incomplete',issues:expect.arrayContaining(['unfinished_relation','missing_date','missing_source']),
|
||||
feedback:"The evidence is good enough, but the report itself won't hold up in court. Add the date, cite the source, and provide the link if you can. Then we can accept it." })
|
||||
const targetOnBoard=reportState.exhibits.find(exhibit => exhibit.type === 'document' && exhibit.id === targetDocument.id)
|
||||
if (!targetOnBoard || targetOnBoard.type !== 'document') throw new Error('Target document missing from board')
|
||||
targetOnBoard.publishedAt='1953-08-19T00:00:00.000Z'
|
||||
targetOnBoard.sourceCitation='Google Patents · GB695913A'
|
||||
targetOnBoard.sourceUri='https://patents.google.com/patent/GB695913A/en'
|
||||
const targetConnection=reportState.connections.find(connection => connection.id === connectionId)
|
||||
if (!targetConnection) throw new Error('Target connection missing from board')
|
||||
targetConnection.label='Proof that Barricelli is named as the inventor on patent GB695913A.'
|
||||
expect((await fetch(`${baseUrl}/api/levels/${reportState.id}`,{ method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify(reportState) })).status).toBe(200)
|
||||
const accepted=await fetch(`${baseUrl}/api/levels/${reportState.id}/report/submissions`,{ method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({
|
||||
investigatorName:'Test Player',evidence:[{ connectionId,documentExhibitId:targetDocument.id,relationText:'Proof that Barricelli is named as the inventor on patent GB695913A.',
|
||||
publishedAt:'1953-08-19',sourceCitation:'Google Patents · GB695913A',sourceUri:'https://patents.google.com/patent/GB695913A/en' }],
|
||||
investigatorName:'Test Player',
|
||||
}) })
|
||||
expect(accepted.status).toBe(201)
|
||||
const acceptedBody=await accepted.json()
|
||||
@@ -376,10 +388,14 @@ suite('normalized level persistence API', () => {
|
||||
const repeatedState=await (await fetch(`${baseUrl}/api/levels/${reportState.id}`)).json() as CaseState
|
||||
const repeatedConnectionId=randomUUID()
|
||||
repeatedState.connections.push({ id:repeatedConnectionId,fromExhibitId:claim.id,toExhibitId:repeatedDocument.id,label:'Proof that Barricelli is named as the inventor on patent GB695913A.',tightness:65,tagStyle:'luggage',tagPosition:50,tagOffset:0 })
|
||||
const repeatedOnBoard=repeatedState.exhibits.find(exhibit => exhibit.type === 'document' && exhibit.id === repeatedDocument.id)
|
||||
if (!repeatedOnBoard || repeatedOnBoard.type !== 'document') throw new Error('Repeated document missing from board')
|
||||
repeatedOnBoard.publishedAt='1953-08-19T00:00:00.000Z'
|
||||
repeatedOnBoard.sourceCitation='Google Patents · GB695913A'
|
||||
repeatedOnBoard.sourceUri='https://patents.google.com/patent/GB695913A/en'
|
||||
expect((await fetch(`${baseUrl}/api/levels/${repeatedState.id}`,{ method:'PUT',headers:{'content-type':'application/json'},body:JSON.stringify(repeatedState) })).status).toBe(200)
|
||||
const repeatedReport=await (await fetch(`${baseUrl}/api/levels/${repeatedState.id}/report/submissions`,{ method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({
|
||||
investigatorName:'Test Player',evidence:[{ connectionId:repeatedConnectionId,documentExhibitId:repeatedDocument.id,relationText:'Proof that Barricelli is named as the inventor on patent GB695913A.',
|
||||
publishedAt:'1953-08-19',sourceCitation:'Google Patents · GB695913A',sourceUri:'https://patents.google.com/patent/GB695913A/en' }],
|
||||
investigatorName:'Test Player',
|
||||
}) })).json()
|
||||
expect(repeatedReport).toMatchObject({ status:'accepted',claims:[expect.objectContaining({ evidence:expect.arrayContaining([expect.objectContaining({
|
||||
documentExhibitId:repeatedDocument.id,evidenceAccepted:true,verification:expect.objectContaining({ status:'accepted' }),
|
||||
|
||||
@@ -140,10 +140,11 @@ export async function cloneBoard(client: PoolClient, sourceBoardId: string, targ
|
||||
'INSERT INTO osint.image_documents (exhibit_id,pixel_width,pixel_height,alt_text) VALUES ($1,$2,$3,$4)',
|
||||
[mapped(exhibitIds, row.exhibit_id, 'image'), row.pixel_width, row.pixel_height, row.alt_text])
|
||||
|
||||
const notes = await client.query<{ exhibit_id: string; title: string; note_text: string }>(
|
||||
const notes = await client.query<{ exhibit_id: string; title: string; note_text: string; presentation_kind:'luggage'|'lined_sheet' }>(
|
||||
`SELECT n.* FROM osint.note_exhibits n JOIN osint.exhibits e ON e.id=n.exhibit_id WHERE e.board_id=$1`, [sourceBoardId])
|
||||
for (const row of notes.rows) await client.query('INSERT INTO osint.note_exhibits (exhibit_id,title,note_text) VALUES ($1,$2,$3)',
|
||||
[mapped(exhibitIds, row.exhibit_id, 'note'), row.title, row.note_text])
|
||||
for (const row of notes.rows) await client.query(
|
||||
'INSERT INTO osint.note_exhibits (exhibit_id,title,note_text,presentation_kind) VALUES ($1,$2,$3,$4)',
|
||||
[mapped(exhibitIds,row.exhibit_id,'note'),row.title,row.note_text,row.presentation_kind])
|
||||
|
||||
const claims = await client.query<{ exhibit_id:string;statement:string }>(
|
||||
`SELECT claim.exhibit_id,claim.statement FROM osint.claim_exhibits claim
|
||||
|
||||
@@ -5,18 +5,6 @@ import type { CaseReport, CaseReportEvidence, CaseReportSubmissionInput, CaseRep
|
||||
type LevelRef = { id:string; board_id:string }
|
||||
|
||||
function trimmed(value: unknown, max: number) { return String(value || '').trim().slice(0,max) }
|
||||
function optionalUrl(value: unknown) {
|
||||
const candidate = trimmed(value,2_000)
|
||||
if (!candidate) return null
|
||||
try { return new URL(candidate).toString() } catch { throw new Error('Source links must be absolute URLs') }
|
||||
}
|
||||
function optionalTimestamp(value: unknown) {
|
||||
const candidate = trimmed(value,100)
|
||||
if (!candidate) return null
|
||||
const date = new Date(candidate)
|
||||
if (!Number.isFinite(date.getTime())) throw new Error('Evidence dates must be valid dates')
|
||||
return date.toISOString()
|
||||
}
|
||||
function unfinishedRelation(value: string) {
|
||||
return !value.trim() || /^proof\s+that(?:\s*(?:…|\.{3}))?\s*$/iu.test(value.trim())
|
||||
}
|
||||
@@ -130,29 +118,6 @@ export async function submitCaseReport(pool: Pool, levelSlug: string, rawInput:
|
||||
const report = (await client.query<{ board_id:string }>('SELECT board_id FROM osint.case_reports WHERE board_id=$1 FOR UPDATE', [level.board_id])).rows[0]
|
||||
if (!report) throw new Error('This level does not have a case report')
|
||||
const investigatorName = trimmed(rawInput?.investigatorName,300) || 'Player'
|
||||
const drafts = Array.isArray(rawInput?.evidence) ? rawInput.evidence.slice(0,100) : []
|
||||
if (new Set(drafts.map(item => item.connectionId)).size !== drafts.length) throw new Error('A report cannot submit the same connection twice')
|
||||
for (const draft of drafts) {
|
||||
const relationText = trimmed(draft.relationText,2_000)
|
||||
const sourceCitation = trimmed(draft.sourceCitation,1_000)
|
||||
const sourceUri = optionalUrl(draft.sourceUri)
|
||||
const publishedAt = optionalTimestamp(draft.publishedAt)
|
||||
const owned = (await client.query<{ connection_id:string; document_id:string }>(`SELECT connection.id AS connection_id,document.exhibit_id AS document_id
|
||||
FROM osint.exhibit_connections connection
|
||||
JOIN osint.claim_exhibits claim ON claim.exhibit_id=CASE
|
||||
WHEN connection.from_exhibit_id=$3 THEN connection.to_exhibit_id ELSE connection.from_exhibit_id END
|
||||
JOIN osint.document_exhibits document ON document.exhibit_id=CASE
|
||||
WHEN connection.from_exhibit_id=$3 THEN connection.from_exhibit_id ELSE connection.to_exhibit_id END
|
||||
WHERE connection.id=$1 AND connection.board_id=$2
|
||||
AND $3::uuid IN (connection.from_exhibit_id,connection.to_exhibit_id)`, [draft.connectionId,level.board_id,draft.documentExhibitId])).rows[0]
|
||||
if (!owned || owned.document_id !== draft.documentExhibitId) throw new Error('Report evidence must reference a claim-to-document connection on this board')
|
||||
await client.query('UPDATE osint.exhibit_connections SET label=$2 WHERE id=$1', [draft.connectionId,relationText || null])
|
||||
await client.query(`UPDATE osint.document_exhibits SET published_at=$2,citation_text=$3,source_uri=$4 WHERE exhibit_id=$1`,
|
||||
[draft.documentExhibitId,publishedAt,sourceCitation,sourceUri])
|
||||
await client.query(`INSERT INTO osint.exhibit_citations (board_id,exhibit_id,display_number)
|
||||
SELECT $1,$2,COALESCE(MAX(display_number),0)+1 FROM osint.exhibit_citations WHERE board_id=$1
|
||||
ON CONFLICT (board_id,exhibit_id) DO NOTHING`, [level.board_id,draft.documentExhibitId])
|
||||
}
|
||||
await client.query('UPDATE osint.case_reports SET investigator_name=$2,updated_at=NOW() WHERE board_id=$1', [level.board_id,investigatorName])
|
||||
const assembled = (await loadCaseReport(client,level))!
|
||||
const pendingGoals = Number((await client.query<{ count:string }>(`SELECT COUNT(*)::text AS count FROM osint.level_goals goal
|
||||
|
||||
@@ -213,7 +213,6 @@ app.post('/api/levels/:id/report/submissions', async (req, res, next) => {
|
||||
try {
|
||||
const report = await submitCaseReport(pool,String(req.params.id),{
|
||||
investigatorName:String(req.body?.investigatorName || resolvePlayerName(req)),
|
||||
evidence:Array.isArray(req.body?.evidence) ? req.body.evidence : [],
|
||||
})
|
||||
report ? res.status(201).json(report) : res.status(404).json({ error:'Level not found' })
|
||||
} catch (error) { next(error) }
|
||||
|
||||
@@ -87,6 +87,7 @@ type ExhibitRow = {
|
||||
captured_at: Date | null; source_uri: string | null; citation_text: string | null; display_number: number | null; statement: string | null
|
||||
original_name: string | null; mime_type: string | null; byte_size: string | null
|
||||
source_document_id: string | null; source_region_key: string | null
|
||||
note_presentation_kind: 'luggage' | 'lined_sheet' | null
|
||||
party_kind: PartyKind | null; organization_kind: OrganizationKind | null
|
||||
}
|
||||
|
||||
@@ -352,6 +353,7 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
COALESCE(f.title, d.title, n.title, ev.title, p.display_name, claim.statement, '') AS title,
|
||||
COALESCE(f.label_text, n.note_text, ev.narrative_text, p.summary, '') AS content,
|
||||
f.is_open, d.document_type_id, d.capture_kind_id, d.asset_id, d.published_at, d.captured_at, d.source_uri,d.citation_text,citation.display_number,
|
||||
n.presentation_kind AS note_presentation_kind,
|
||||
ev.occurred_at,claim.statement,
|
||||
p.party_kind, op.organization_kind,
|
||||
a.original_name, a.mime_type, a.byte_size,
|
||||
@@ -448,7 +450,7 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
else if (row.exhibit_type_id === 'event') evidence.push({ ...common, type:'event', eventDate:row.occurred_at?.toISOString() })
|
||||
else if (row.exhibit_type_id === 'party') evidence.push({ ...common, type:'party', partyKind:row.party_kind || 'person', organizationKind:row.organization_kind || undefined, aliases:aliases.get(row.id) || [] })
|
||||
else if (row.exhibit_type_id === 'claim') evidence.push({ ...base(row), type:'claim', title:row.title, statement:row.statement || row.title })
|
||||
else if (row.exhibit_type_id === 'note') evidence.push({ ...common, type:'note' })
|
||||
else if (row.exhibit_type_id === 'note') evidence.push({ ...common, type:'note', presentation:row.note_presentation_kind || 'luggage' })
|
||||
else throw new Error(`Unsupported exhibit type ${row.exhibit_type_id}`)
|
||||
}
|
||||
const views: BoardView[] = viewsResult.rows.map(row => ({ id: row.id, type: 'timeline', visible: row.visible, zIndex: row.z_index,
|
||||
@@ -525,18 +527,21 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
else await client.query('DELETE FROM osint.exhibits WHERE board_id=$1', [level.board_id])
|
||||
|
||||
for (const exhibit of state.exhibits) {
|
||||
const clipping=exhibit.type === 'document' && documentCaptureKind(exhibit.captureKind) === 'clipping'
|
||||
await client.query(`INSERT INTO osint.exhibits (id,board_id,exhibit_type_id,xpos,ypos,width,height,rotation,z_index,hidden)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
|
||||
ON CONFLICT (id) DO UPDATE SET exhibit_type_id=$3,xpos=$4,ypos=$5,width=$6,height=$7,rotation=$8,z_index=$9,hidden=$10,updated_at=NOW()`,
|
||||
[exhibit.id, level.board_id, exhibit.type, exhibit.x, exhibit.y, exhibit.width, exhibit.height, exhibit.rotation, exhibit.zIndex, exhibit.hidden])
|
||||
[exhibit.id,level.board_id,exhibit.type,exhibit.x,exhibit.y,clipping ? 230 : exhibit.width,clipping ? 290 : exhibit.height,exhibit.rotation,exhibit.zIndex,exhibit.hidden])
|
||||
}
|
||||
let nextCitation = Number((await client.query<{ maximum:number }>('SELECT COALESCE(MAX(display_number),0)::int AS maximum FROM osint.exhibit_citations WHERE board_id=$1',[level.board_id])).rows[0].maximum)
|
||||
for (const document of documents) {
|
||||
const clippingTitle=document.captureKind === 'clipping' && document.sourceCitation?.trim()
|
||||
&& (!document.title.trim() || document.title === document.fileName) ? document.sourceCitation.trim() : document.title
|
||||
await client.query(`INSERT INTO osint.document_exhibits (exhibit_id,document_type_id,capture_kind_id,asset_id,title,published_at,captured_at,source_uri,citation_text)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT (exhibit_id) DO UPDATE SET
|
||||
document_type_id=EXCLUDED.document_type_id,asset_id=EXCLUDED.asset_id,title=EXCLUDED.title,published_at=EXCLUDED.published_at,
|
||||
capture_kind_id=EXCLUDED.capture_kind_id,captured_at=EXCLUDED.captured_at,source_uri=EXCLUDED.source_uri,citation_text=EXCLUDED.citation_text`,
|
||||
[document.id, documentType(document), documentCaptureKind(document.captureKind), document.assetId || null, document.title,
|
||||
[document.id, documentType(document), documentCaptureKind(document.captureKind), document.assetId || null, clippingTitle,
|
||||
timestamp(document.publishedAt), timestamp(document.capturedAt), document.sourceUri || null,document.sourceCitation || ''])
|
||||
const existingCitation = await client.query<{ display_number:number }>('SELECT display_number FROM osint.exhibit_citations WHERE board_id=$1 AND exhibit_id=$2',[level.board_id,document.id])
|
||||
if (!existingCitation.rows[0]) {
|
||||
@@ -560,7 +565,9 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
if (isFolderExhibit(exhibit)) await client.query(
|
||||
'INSERT INTO osint.folder_exhibits (exhibit_id,title,label_text,is_open) VALUES ($1,$2,$3,$4)',
|
||||
[exhibit.id, exhibit.title, exhibit.content, exhibit.isOpen])
|
||||
if (exhibit.type === 'note') await client.query('INSERT INTO osint.note_exhibits (exhibit_id,title,note_text) VALUES ($1,$2,$3)', [exhibit.id, exhibit.title, exhibit.content])
|
||||
if (exhibit.type === 'note') await client.query(
|
||||
'INSERT INTO osint.note_exhibits (exhibit_id,title,note_text,presentation_kind) VALUES ($1,$2,$3,$4)',
|
||||
[exhibit.id,exhibit.title,exhibit.content,exhibit.presentation === 'lined_sheet' ? 'lined_sheet' : 'luggage'])
|
||||
if (isEventExhibit(exhibit)) await client.query(
|
||||
'INSERT INTO osint.event_exhibits (exhibit_id,title,narrative_text,occurred_at) VALUES ($1,$2,$3,$4)',
|
||||
[exhibit.id, exhibit.title, exhibit.content, timestamp(exhibit.eventDate)])
|
||||
|
||||
@@ -5,7 +5,7 @@ import { filterLevelVisibility, mergePlayerStateForPersistence } from './levelVi
|
||||
const placed = { x: 10, y: 20, width: 174, height: 145, rotation: 0, zIndex: 1, hidden: false }
|
||||
const open: DocumentExhibit = { id: 'open', type: 'document', title: 'Open', body: [], regions: [], fileType: 'image', captureKind:'unclassified',metadata: {}, ...placed }
|
||||
const gated: DocumentExhibit = { id: 'gated', type: 'document', title: 'Gated', body: [], regions: [], fileType: 'image', captureKind:'unclassified',metadata: {}, requiredFlags: ['tip.received'], ...placed }
|
||||
const note: NoteExhibit = { id: 'note', type: 'note', title: 'Note', content: '', ...placed }
|
||||
const note: NoteExhibit = { id:'note',type:'note',title:'Note',content:'',presentation:'luggage',...placed }
|
||||
const state: CaseState = {
|
||||
id: 'demo', title: 'Demo', subtitle: '', exhibits: [open, gated, note], viewport: { x: 0, y: 0, zoom: 1 },
|
||||
relations: [{ id: 'source', type: 'source', fromExhibitId: note.id, toExhibitId: gated.id, sortOrder: 0 }],
|
||||
|
||||
Reference in New Issue
Block a user