/** * Utility functions for processing file data across different file selection components * This module provides reusable functionality for filtering, grouping, and processing fileData */ /** * Access control metadata * @typedef {object} AccessControl * @property {string} editability - Edit permissions ("editable", etc.) * @property {string|null} owner - Owner of the file * @property {string[]} status_values - Available status values * @property {string} visibility - Visibility setting ("public", etc.) */ /** * Document metadata from TEI header * @typedef {object} DocumentMetadata * @property {string} title - Document title * @property {object[]} authors - Array of author objects {given, family} * @property {string} [date] - Publication date * @property {string} [publisher] - Publisher name */ /** * Base file item - used for source files (PDF, primary XML) * @typedef {object} FileItem * @property {string} id - Stable ID for URLs and references * @property {string} filename - Filename * @property {string} file_type - 'pdf' or 'tei' * @property {string} label - Display label * @property {number} file_size - File size in bytes * @property {string} created_at - ISO timestamp * @property {string} updated_at - ISO timestamp * @property {string|null} created_by - Username of user who created this file */ /** * Artifact file item - extends FileItem with artifact-specific properties * @typedef {FileItem & { * variant: string|null, * version: number|null, * is_gold_standard: boolean, * is_locked: boolean, * metadata?: { access_control: AccessControl | null }, * created_by: string|null * }} Artifact */ /** * Document item with source and artifacts * @typedef {object} DocumentItem * @property {string} doc_id - Document identifier * @property {string[]} collections - All collections for this document * @property {DocumentMetadata } doc_metadata - Document metadata from TEI header * @property {FileItem} source - Source file (PDF or primary XML) * @property {Artifact[]} artifacts - All artifact files (TEI, etc.) */ /** * File list response * @typedef {object} FileListResponse * @property {DocumentItem[]} files - Array of documents */ /** * Lookup item for index * @typedef {{ type: 'source', item: FileItem, file: DocumentItem, label: string } | { type: 'artifact', item: Artifact, file: DocumentItem, label: string }} LookupItem */ // Global lookup index for efficient ID-based queries let idLookupIndex = new Map(); /** * Creates a lookup index that maps stable IDs to items * @param {DocumentItem[]} fileData - The file data array * @returns {Map} Map of id to item with metadata */ export function createIdLookupIndex(fileData) { const index = new Map(); fileData.forEach((file) => { // Index source file ID if (file.source && file.source.id) { index.set(file.source.id, { type: 'source', item: file.source, file: file, label: file.source.label }); } // Index all artifacts if (file.artifacts) { file.artifacts.forEach((artifact) => { if (artifact.id) { index.set(artifact.id, { type: 'artifact', item: artifact, file: file, label: artifact.label }); } }); } }); // Store globally for use by other functions idLookupIndex = index; return index; } /** * Legacy function for backward compatibility - calls createIdLookupIndex * @deprecated Use createIdLookupIndex instead * @param {DocumentItem[]} fileData - The file data array * @returns {Map} Map of id to item with metadata */ export function createHashLookupIndex(fileData) { return createIdLookupIndex(fileData); } /** * Gets file data entry from idLookupIndex based on stable ID * @param {string} id - Stable ID of any file (source or artifact) * @returns {LookupItem|null} Entry object with {type, item, file, label} or null if not found */ export function getFileDataById(id) { if (!id) return null; if (!idLookupIndex || idLookupIndex.size === 0) { return null; } return idLookupIndex.get(id) || null; } /** * Legacy function for backward compatibility - calls getFileDataById * @deprecated Use getFileDataById instead * @param {string} hash - Hash/ID of any file * @returns {LookupItem|null} Entry object or null if not found */ export function getFileDataByHash(hash) { return getFileDataById(hash); } /** * Gets document title/label from fileData based on stable ID * @param {string} id - Stable ID of any file (source or artifact) * @returns {string} Document title/label or empty string if not found */ export function getDocumentTitle(id) { const entry = getFileDataById(id); return entry?.label || ''; } /** * Extracts all unique variants from file data * @param {DocumentItem[]} fileData - The file data array * @returns {Set} Set of unique variant IDs */ export function extractVariants(fileData) { const variants = new Set(); fileData.forEach(file => { // Add variant from all artifacts if (file.artifacts) { file.artifacts.forEach(artifact => { if (artifact.variant) { variants.add(artifact.variant); } }); } }); return variants; } /** * Filters file data by variant selection * @param {DocumentItem[]} fileData - The file data array * @param {string|null} variant - Selected variant ("", "none", or variant ID) * @returns {DocumentItem[]} Filtered file data */ export function filterFileDataByVariant(fileData, variant) { if (variant === "none") { // Show only files with artifacts that have no variant return fileData.filter(file => { return file.artifacts && file.artifacts.some(a => !a.variant); }); } else if (variant && variant !== "") { // Show only files with artifacts matching the selected variant return fileData.filter(file => { return file.artifacts && file.artifacts.some(a => a.variant === variant); }); } // If variant is "" (All), show all files return fileData; } /** * Filters file data by label text search * @param {DocumentItem[]} fileData - The file data array * @param {string} searchText - Text to search for in labels * @returns {DocumentItem[]} Filtered file data */ export function filterFileDataByLabel(fileData, searchText) { if (!searchText || searchText.trim() === '') { return fileData; } const search = searchText.toLowerCase(); return fileData.filter(file => file.source.label && file.source.label.toLowerCase().includes(search) ); } /** * Groups file data by collection * @param {DocumentItem[]} fileData - The file data array * @returns {Record} Grouped files by collection name */ export function groupFilesByCollection(fileData) { /** @type Record */ const groups = {} return fileData.reduce((groups, file) => { // Add document to all its collections (documents can appear in multiple collection groups) if (file.collections && file.collections.length > 0) { for (const collection_name of file.collections) { groups[collection_name] ||= [] groups[collection_name].push(file); } } else { // No collections - add to unfiled groups["__unfiled"] ||= [] groups["__unfiled"].push(file); } return groups; }, groups); } /** * Filters artifacts by variant - simplified version for flat artifacts array * @param {DocumentItem} file - File object containing artifacts * @param {string|null} variant - Selected variant * @returns {{versions: Artifact[], gold: Artifact[]}} Object with filtered versions and gold arrays */ export function filterFileContentByVariant(file, variant) { let artifacts = file.artifacts || []; if (variant === "none") { // Show only artifacts without variant artifacts = file.artifacts ? file.artifacts.filter(a => !a.variant) : []; } else if (variant && variant !== "") { // Show only artifacts with the selected variant artifacts = file.artifacts ? file.artifacts.filter(a => a.variant === variant) : []; } // If variant is "" (All), show all artifacts (already assigned above) // Separate into versions and gold for backward compatibility const versions = artifacts.filter(a => !a.is_gold_standard); const gold = artifacts.filter(a => a.is_gold_standard); return { versions, gold }; } /** * Finds a matching gold file based on variant selection * @param {DocumentItem} file - File object containing artifacts * @param {string|null} variant - Selected variant * @returns {Artifact|null} Matching gold artifact or null */ export function findMatchingGold(file, variant) { if (!file.artifacts) return null; // Filter to only gold standards const goldArtifacts = file.artifacts.filter(a => a.is_gold_standard); if (variant === "none") { // Find gold without variant return goldArtifacts.find(gold => !gold.variant) || null; } else if (variant && variant !== "") { // Find gold with matching variant return goldArtifacts.find(gold => gold.variant === variant) || null; } else { // No variant filter - use first gold file return goldArtifacts[0] || null; } } /** * Finds the collection for a given file ID by searching through file data * @param {DocumentItem[]} fileData - The file data array * @param {string} id - Stable ID of the file to find * @returns {string|null} Collection name or null if not found */ export function findCollectionById(id) { const entry = getFileDataById(id); if (entry && entry.file.collections && entry.file.collections[0]) { return entry.file.collections[0]; } return null; } /** * Legacy function for backward compatibility - calls findCollectionById * @deprecated Use findCollectionById instead * @param {DocumentItem[]} fileData - The file data array * @param {string} hash - ID of the file to find * @returns {string|null} Collection name or null if not found */ export function findCollectionByHash(fileData, hash) { return findCollectionById(hash); } /** * Finds a file object by source ID * @param {DocumentItem[]} fileData - The file data array * @param {string} sourceId - Stable ID of the source file * @returns {DocumentItem|null} File object or null if not found */ export function findFileBySourceId(fileData, sourceId) { return fileData.find(file => file.source.id === sourceId) || null; } /** * Legacy function for backward compatibility - calls findFileBySourceId * @deprecated Use findFileBySourceId instead * @param {DocumentItem[]} fileData - The file data array * @param {string} pdfHash - ID of the source file * @returns {DocumentItem|null} File object or null if not found */ export function findFileByPdfHash(fileData, pdfHash) { return findFileBySourceId(fileData, pdfHash); } /** * Gets the display name for a collection * @param {string} collectionId - Collection ID * @param {Array<{id: string, name: string, description: string}>} collections - Collections array from state * @returns {string} Display name for the collection */ export function getCollectionName(collectionId, collections) { if (collectionId === "__unfiled") { return "Unfiled"; } if (!collections || collections.length === 0) { // Fallback to converting underscores to spaces if collections not loaded return collectionId.replaceAll("_", " ").trim(); } const collection = collections.find(c => c.id === collectionId); return collection ? collection.name : collectionId.replaceAll("_", " ").trim(); } /** * Finds the corresponding source ID and collection for a given artifact ID * @param {DocumentItem[]} fileData - The file data array * @param {string} artifactId - Stable ID of the artifact file * @returns {{sourceId: string, collection: string}|null} Object with {sourceId, collection} or null if not found */ export function findCorrespondingSource(fileData, artifactId) { const entry = getFileDataById(artifactId); if (entry && entry.type === 'artifact' && entry.file) { return { sourceId: entry.file.source.id, collection: entry.file.collections && entry.file.collections[0] }; } return null; } /** * Legacy function for backward compatibility - calls findCorrespondingSource * @deprecated Use findCorrespondingSource instead * @param {DocumentItem[]} fileData - The file data array * @param {string} xmlHash - ID of the artifact file * @returns {{pdfHash: string, collection: string}|null} Object with {pdfHash, collection} or null if not found */ export function findCorrespondingPdf(fileData, xmlHash) { const result = findCorrespondingSource(fileData, xmlHash); if (result) { return { pdfHash: result.sourceId, collection: result.collection }; } return null; }