pdf-tei-editor / app /src /modules /file-data-utils.js
cmboulanger's picture
disaster-recovery deploy of pdf-tei-editor
6a49f21 verified
Raw
History Blame Contribute Delete
12.9 kB
/**
* 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<string, LookupItem>} 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<string, LookupItem>} 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<string>} 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<string, DocumentItem[]>} Grouped files by collection name
*/
export function groupFilesByCollection(fileData) {
/** @type Record<string, DocumentItem[]> */
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;
}