Adding working board
This commit is contained in:
+100
-49
@@ -1,4 +1,4 @@
|
||||
import type { CaseState, Evidence, TimelineRange, Viewport, WidgetRelation } from './types'
|
||||
import type { BoardView, CaseState, Connection, Exhibit, ExhibitRelation, FolderExhibit, OrganizationKind, SourceFileType, TimelineRange, Viewport } from './types'
|
||||
|
||||
export interface BoardPoint { x: number; y: number }
|
||||
|
||||
@@ -99,7 +99,7 @@ export function moveBoardPoint(origin: { x: number; y: number }, screenDelta: {
|
||||
}
|
||||
|
||||
export function nextOpenBoardPosition(
|
||||
evidence: Pick<Evidence, 'x' | 'y' | 'width'>[],
|
||||
evidence: Pick<Exhibit, 'x' | 'y' | 'width' | 'height'>[],
|
||||
preferred: { x: number; y: number },
|
||||
size: { width: number; height?: number },
|
||||
bounds = { width: 2400, height: 1500 },
|
||||
@@ -108,7 +108,7 @@ export function nextOpenBoardPosition(
|
||||
const gap = 28
|
||||
const overlaps = (x: number, y: number) => evidence.some(item =>
|
||||
x < item.x + item.width + gap && x + size.width + gap > item.x &&
|
||||
y < item.y + 160 + gap && y + height + gap > item.y)
|
||||
y < item.y + item.height + gap && y + height + gap > item.y)
|
||||
const xStep = size.width + 46
|
||||
const yStep = height + 46
|
||||
for (let row = 0; row < 7; row += 1) {
|
||||
@@ -158,40 +158,33 @@ export function timelinePositionPercent(date: string, range: Pick<ReturnType<typ
|
||||
|
||||
export function containedIds(state: CaseState, widgetId: string) {
|
||||
return state.relations
|
||||
.filter(relation => relation.type === 'contains' && relation.fromWidgetId === widgetId)
|
||||
.filter(relation => relation.type === 'contains' && relation.fromExhibitId === widgetId)
|
||||
.sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0))
|
||||
.map(relation => relation.toWidgetId)
|
||||
.map(relation => relation.toExhibitId)
|
||||
}
|
||||
|
||||
export function folderIsOpen(folder: Evidence) {
|
||||
return folder.config?.open === true
|
||||
export function folderIsOpen(folder: FolderExhibit) {
|
||||
return folder.isOpen
|
||||
}
|
||||
|
||||
export function relationPosition(state: CaseState, relation: WidgetRelation) {
|
||||
const folder = state.evidence.find(widget => widget.id === relation.fromWidgetId)
|
||||
export function relationPosition(state: CaseState, relation: ExhibitRelation) {
|
||||
const target = state.exhibits.find(exhibit => exhibit.id === relation.toExhibitId)
|
||||
if (target) return { x: target.x, y: target.y }
|
||||
const folder = state.exhibits.find(exhibit => exhibit.id === relation.fromExhibitId)
|
||||
const order = relation.sortOrder || 0
|
||||
const configuredX = Number(relation.config?.x)
|
||||
const configuredY = Number(relation.config?.y)
|
||||
return {
|
||||
x: Number.isFinite(configuredX) ? configuredX : (folder?.x || 100) + (folder?.width || 240) + 90 + (order % 3) * 205,
|
||||
y: Number.isFinite(configuredY) ? configuredY : (folder?.y || 100) - 30 + Math.floor(order / 3) * 185,
|
||||
x: (folder?.x || 100) + (folder?.width || 240) + 90 + (order % 3) * 205,
|
||||
y: (folder?.y || 100) - 30 + Math.floor(order / 3) * 185,
|
||||
}
|
||||
}
|
||||
|
||||
export function discardExhibit(state: CaseState, exhibitId: string): CaseState {
|
||||
if (!state.evidence.some(exhibit => exhibit.id === exhibitId)) return state
|
||||
if (!state.exhibits.some(exhibit => exhibit.id === exhibitId)) return state
|
||||
return {
|
||||
...state,
|
||||
evidence: state.evidence
|
||||
.filter(exhibit => exhibit.id !== exhibitId)
|
||||
.map(exhibit => ({
|
||||
...exhibit,
|
||||
containedDocumentIds: exhibit.containedDocumentIds?.filter(id => id !== exhibitId),
|
||||
supportingEvidenceIds: exhibit.supportingEvidenceIds?.filter(id => id !== exhibitId),
|
||||
relatedEvidenceIds: exhibit.relatedEvidenceIds?.filter(id => id !== exhibitId),
|
||||
})),
|
||||
relations: state.relations.filter(relation => relation.fromWidgetId !== exhibitId && relation.toWidgetId !== exhibitId),
|
||||
connections: state.connections.filter(connection => connection.fromEvidenceId !== exhibitId && connection.toEvidenceId !== exhibitId),
|
||||
exhibits: state.exhibits.filter(exhibit => exhibit.id !== exhibitId),
|
||||
relations: state.relations.filter(relation => relation.fromExhibitId !== exhibitId && relation.toExhibitId !== exhibitId),
|
||||
connections: state.connections.filter(connection => connection.fromExhibitId !== exhibitId && connection.toExhibitId !== exhibitId),
|
||||
brief: {
|
||||
...state.brief,
|
||||
concepts: state.brief.concepts.map(concept => concept.resolvedPartyExhibitId === exhibitId
|
||||
@@ -201,30 +194,88 @@ export function discardExhibit(state: CaseState, exhibitId: string): CaseState {
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeCase(state: CaseState): CaseState {
|
||||
const relations = Array.isArray(state.relations)
|
||||
? state.relations
|
||||
: state.evidence.flatMap(widget => (widget.containedDocumentIds || (widget.sourceDocumentId ? [widget.sourceDocumentId] : [])).map((documentId, index) => ({
|
||||
id: `contains:${widget.id}:${documentId}`,
|
||||
fromWidgetId: widget.id,
|
||||
toWidgetId: documentId,
|
||||
type: 'contains',
|
||||
sortOrder: index,
|
||||
})))
|
||||
const normalized = { ...state, relations }
|
||||
return {
|
||||
...normalized,
|
||||
brief: state.brief || { body: '', concepts: [] },
|
||||
documents: state.documents.map(document => ({
|
||||
...document,
|
||||
fileType: document.fileType || (document.mimeType?.startsWith('image/') ? 'image' : document.mimeType === 'application/pdf' ? 'pdf' : 'file'),
|
||||
metadata: document.metadata || {},
|
||||
})),
|
||||
evidence: state.evidence.map(widget => ({
|
||||
...widget,
|
||||
type: widget.type === 'evidence' ? 'folder' : widget.type,
|
||||
config: widget.config || {},
|
||||
containedDocumentIds: containedIds(normalized, widget.id),
|
||||
})),
|
||||
export function defaultTimelineView(range?: TimelineRange | null): BoardView {
|
||||
return { id: crypto.randomUUID(), type: 'timeline', placement: { mode: 'docked', dockEdge: 'bottom', size: 112 }, visible: true, zIndex: 0,
|
||||
rangeMode: range ? 'fixed' : 'auto', range: range || undefined }
|
||||
}
|
||||
|
||||
type LegacyCaseState = {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
viewport: Viewport
|
||||
brief?: CaseState['brief']
|
||||
updatedAt?: string
|
||||
levelStatus?: string
|
||||
sourceTemplateVersionId?: string
|
||||
editingAllowed?: boolean
|
||||
revision?: number
|
||||
exhibits?: Exhibit[]
|
||||
views?: BoardView[]
|
||||
documents?: Array<Record<string, unknown>>
|
||||
evidence?: Array<Record<string, unknown>>
|
||||
timelineRange?: TimelineRange | null
|
||||
relations?: Array<Record<string, unknown>>
|
||||
connections?: Array<Record<string, unknown>>
|
||||
}
|
||||
|
||||
function placement(item: Record<string, unknown>, defaults: { width: number; height: number }, index: number) {
|
||||
return { x: Number(item.x ?? 100), y: Number(item.y ?? 100), width: Number(item.width ?? defaults.width), height: Number(item.height ?? defaults.height),
|
||||
rotation: Number(item.rotation ?? 0), zIndex: Number(item.zIndex ?? index), hidden: Boolean(item.hidden) }
|
||||
}
|
||||
|
||||
function sourceFileType(value: unknown, mimeType: unknown): SourceFileType {
|
||||
const allowed: SourceFileType[] = ['image', 'pdf', 'web_capture', 'email', 'article', 'filing', 'price_list', 'text', 'file']
|
||||
if (allowed.includes(value as SourceFileType)) return value as SourceFileType
|
||||
return String(mimeType || '').startsWith('image/') ? 'image' : mimeType === 'application/pdf' ? 'pdf' : 'file'
|
||||
}
|
||||
|
||||
/** Normalizes current API state and upgrades disposable pre-registry browser caches. */
|
||||
export function normalizeCase(input: CaseState | LegacyCaseState): CaseState {
|
||||
const state = input as LegacyCaseState
|
||||
if (Array.isArray(state.exhibits)) {
|
||||
return { id: state.id, title: state.title, subtitle: state.subtitle, viewport: state.viewport,
|
||||
relations: (state.relations || []) as unknown as ExhibitRelation[], connections: (state.connections || []) as unknown as Connection[],
|
||||
revision: Number(state.revision || 0), brief: state.brief || { body: '', concepts: [] },
|
||||
views: Array.isArray(state.views) && state.views.length ? state.views : [defaultTimelineView(state.timelineRange)],
|
||||
exhibits: state.exhibits.map((exhibit, index) => ({ ...exhibit, ...placement(exhibit as unknown as Record<string, unknown>, { width: exhibit.type === 'document' ? 174 : 240, height: exhibit.type === 'document' ? 145 : 160 }, index) })),
|
||||
updatedAt: state.updatedAt, levelStatus: state.levelStatus, sourceTemplateVersionId: state.sourceTemplateVersionId, editingAllowed: state.editingAllowed,
|
||||
}
|
||||
}
|
||||
|
||||
const legacyEvidence = state.evidence || []
|
||||
const legacyRelations = state.relations || []
|
||||
const documentPositions = new Map(legacyRelations.filter(relation => relation.type === 'contains').map(relation => [String(relation.toWidgetId), {
|
||||
x: Number((relation.config as Record<string, unknown> | undefined)?.x ?? 100), y: Number((relation.config as Record<string, unknown> | undefined)?.y ?? 100),
|
||||
}]))
|
||||
const documents: Exhibit[] = (state.documents || []).map((document, index) => ({
|
||||
id: String(document.id), type: 'document', title: String(document.title || ''),
|
||||
...placement({ ...document, ...(documentPositions.get(String(document.id)) || {}) }, { width: 174, height: 145 }, index),
|
||||
publishedAt: String(document.publishedAt || document.date || '') || undefined, capturedAt: String(document.capturedAt || '') || undefined,
|
||||
sourceUri: String(document.sourceUri || '') || undefined, body: Array.isArray(document.body) ? document.body.map(String) : [],
|
||||
regions: Array.isArray(document.regions) ? document.regions as never[] : [], assetId: String(document.assetId || '') || undefined,
|
||||
fileName: String(document.fileName || '') || undefined, mimeType: String(document.mimeType || '') || undefined,
|
||||
fileSize: document.fileSize === undefined ? undefined : Number(document.fileSize),
|
||||
fileType: sourceFileType(document.fileType, document.mimeType),
|
||||
metadata: document.metadata && typeof document.metadata === 'object' ? document.metadata as Record<string, string> : {},
|
||||
} as Exhibit))
|
||||
const evidence: Exhibit[] = legacyEvidence.map((item, index) => {
|
||||
const type = item.type === 'evidence' ? 'folder' : item.type
|
||||
const common = { id: String(item.id), type, title: String(item.title || ''), content: String(item.content || ''), ...placement(item, { width: 240, height: 160 }, documents.length + index) }
|
||||
if (type === 'folder') return { ...common, type: 'folder', isOpen: (item.config as Record<string, unknown> | undefined)?.open === true }
|
||||
if (type === 'event') return { ...common, type: 'event', eventDate: String(item.eventDate || '') || undefined }
|
||||
if (type === 'party') return { ...common, type: 'party', partyKind: item.partyKind === 'organization' ? 'organization' : 'person', organizationKind: item.organizationKind as OrganizationKind | undefined, aliases: Array.isArray(item.aliases) ? item.aliases.map(String) : [] }
|
||||
return { ...common, type: 'note' }
|
||||
})
|
||||
const derivedRelations: ExhibitRelation[] = [
|
||||
...legacyRelations.filter(relation => relation.type === 'contains').map(relation => ({ id: String(relation.id), type: 'contains' as const, fromExhibitId: String(relation.fromWidgetId), toExhibitId: String(relation.toWidgetId), sortOrder: Number(relation.sortOrder || 0) })),
|
||||
...legacyEvidence.flatMap(item => Array.isArray(item.supportingEvidenceIds) ? item.supportingEvidenceIds.map((id, index) => ({ id: `supports:${item.id}:${id}`, type: 'supports' as const, fromExhibitId: String(item.id), toExhibitId: String(id), sortOrder: index })) : []),
|
||||
...legacyEvidence.flatMap(item => Array.isArray(item.relatedEvidenceIds) ? item.relatedEvidenceIds.map((id, index) => ({ id: `concerns:${item.id}:${id}`, type: 'concerns' as const, fromExhibitId: String(item.id), toExhibitId: String(id), sortOrder: index })) : []),
|
||||
...legacyEvidence.flatMap(item => item.sourceDocumentId ? [{ id: `source:${item.id}`, type: 'source' as const, fromExhibitId: String(item.id), toExhibitId: String(item.sourceDocumentId), sourceRegionId: String(item.sourceRegionId || '') || undefined, sortOrder: 0 }] : []),
|
||||
]
|
||||
const connections: Connection[] = (state.connections || []).map(connection => ({ ...connection, id: String(connection.id),
|
||||
fromExhibitId: String(connection.fromExhibitId || connection.fromEvidenceId), toExhibitId: String(connection.toExhibitId || connection.toEvidenceId) } as Connection))
|
||||
return { id: state.id, title: state.title, subtitle: state.subtitle, exhibits: [...documents, ...evidence], relations: derivedRelations, connections,
|
||||
views: [defaultTimelineView(state.timelineRange)], viewport: state.viewport, brief: state.brief || { body: '', concepts: [] }, revision: Number(state.revision || 0),
|
||||
updatedAt: state.updatedAt, levelStatus: state.levelStatus, sourceTemplateVersionId: state.sourceTemplateVersionId, editingAllowed: state.editingAllowed }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user