/** * Store Module * Single source of truth for all application state * Manages files, folders, sorting, filtering */ (function() { 'use strict'; // State const state = { // Directory structure rootHandle: null, folderTree: [], selectedFolders: new Set(), // Files allFiles: [], // All files from selected folders displayFiles: [], // After sorting and filtering // Sort state sortColumns: [], // [{column: 'original', direction: 'asc'}] // Filter state filters: {}, // {columnName: AST (from zddc.filter.parse)} // UI state hideCompliant: false, calculateSha256: false }; // Listeners for state changes const listeners = { 'files': [], 'folders': [], 'sort': [], 'filter': [] }; /** * Set sort columns */ function on(event, callback) { if (listeners[event]) { listeners[event].push(callback); } } /** * Notify listeners of state change */ function notify(event) { if (listeners[event]) { listeners[event].forEach(cb => cb()); } } /** * Set root directory handle */ function setRootHandle(handle) { state.rootHandle = handle; } /** * Set folder tree */ function setFolderTree(tree) { state.folderTree = tree; notify('folders'); } /** * Select/deselect folder */ function toggleFolder(folderPath) { if (state.selectedFolders.has(folderPath)) { state.selectedFolders.delete(folderPath); } else { state.selectedFolders.add(folderPath); } loadFilesFromSelectedFolders(); } /** * Select multiple folders */ function setSelectedFolders(folderPaths) { state.selectedFolders.clear(); folderPaths.forEach(path => state.selectedFolders.add(path)); loadFilesFromSelectedFolders(); } /** * Load files from selected folders */ function loadFilesFromSelectedFolders() { state.allFiles = []; if (state.selectedFolders.size === 0) { updateDisplayFiles(); return; } // Collect files from selected folders for (const folderPath of state.selectedFolders) { const folder = findFolderByPath(folderPath); if (folder && folder.files) { const files = folder.files.filter(f => !f.isDirectory); state.allFiles.push(...files); } } // Apply default sort if no sort set if (state.sortColumns.length === 0) { state.sortColumns = [{ column: 'original', direction: 'asc' }]; } updateDisplayFiles(); } /** * Find folder by path in tree */ function findFolderByPath(path) { function search(folders) { for (const folder of folders) { if (folder.path === path) return folder; if (folder.children) { const found = search(folder.children); if (found) return found; } } return null; } return search(state.folderTree); } /** * Update display files (apply sort, filter, hide compliant) */ function updateDisplayFiles() { let files = [...state.allFiles]; // Apply filters files = applyFilters(files); // Apply hide compliant if (state.hideCompliant) { files = files.filter(file => { const newFilename = computeNewFilename(file); const validation = validateFilename(newFilename); return !validation.isValid; }); } // Apply sort files = applySort(files); state.displayFiles = files; notify('files'); } /** * Apply filters to files using zddc.filter ASTs */ function applyFilters(files) { if (Object.keys(state.filters).length === 0) { return files; } return files.filter(file => { for (const [columnName, ast] of Object.entries(state.filters)) { const value = getColumnValue(file, columnName); if (!window.zddc.filter.matches(value, ast)) { return false; } } return true; }); } /** * Apply sort to files */ function applySort(files) { if (state.sortColumns.length === 0) { return files; } return files.sort((a, b) => { for (const sort of state.sortColumns) { const result = compareValues(a, b, sort.column, sort.direction); if (result !== 0) return result; } return 0; }); } /** * Compare two values for sorting */ function compareValues(a, b, columnName, direction) { let aVal = getColumnValue(a, columnName); let bVal = getColumnValue(b, columnName); const comparison = String(aVal).localeCompare(String(bVal), undefined, { numeric: true, sensitivity: 'base' }); return direction === 'asc' ? comparison : -comparison; } /** * Get column value from file (delegates to utils) */ function getColumnValue(file, columnName) { return window.app.modules.utils.getColumnValue(file, columnName); } function computeNewFilename(file) { return window.app.modules.utils.computeNewFilename(file); } /** * Validate filename */ function validateFilename(filename) { // Use existing validator module if (window.app.modules.validator) { return window.app.modules.validator.validateFilename(filename); } return { isValid: true, errors: [], warnings: [] }; } /** * Match filter text against value */ function matchesFilter(value, filterText) { // Simple contains for now - can enhance later return String(value).toLowerCase().includes(filterText.toLowerCase()); } /** * Set sort columns */ function setSortColumns(columns) { state.sortColumns = columns; updateDisplayFiles(); } /** * Toggle sort on column */ function toggleSort(columnName, multiSort) { if (!multiSort) { state.sortColumns = []; } const existingIndex = state.sortColumns.findIndex(s => s.column === columnName); if (existingIndex >= 0) { const current = state.sortColumns[existingIndex]; if (current.direction === 'asc') { current.direction = 'desc'; } else { state.sortColumns.splice(existingIndex, 1); } } else { state.sortColumns.push({ column: columnName, direction: 'asc' }); } updateDisplayFiles(); notify('sort'); } /** * Set filter for column. ast is the pre-parsed zddc.filter AST. */ function setFilter(columnName, filterText, ast) { if (filterText && ast && ast.length > 0) { state.filters[columnName] = ast; } else { delete state.filters[columnName]; } updateDisplayFiles(); } /** * Replace all filters at once. filtersObj is {columnName: rawString}. * Parses each value. Pass {} to clear all filters. */ function setAllFilters(filtersObj) { state.filters = {}; for (const [columnName, raw] of Object.entries(filtersObj)) { if (raw) { const ast = window.zddc.filter.parse(raw); if (ast && ast.length > 0) { state.filters[columnName] = ast; } } } updateDisplayFiles(); } /** * Set hide compliant flag */ function setHideCompliant(hide) { state.hideCompliant = hide; updateDisplayFiles(); } /** * Update file data */ function updateFile(index, updates) { const file = state.displayFiles[index]; if (!file) return; // Apply updates Object.assign(file, updates); // Mark as dirty unless explicitly set to false if (updates.isDirty !== false) { file.isDirty = true; } // Notify listeners (will trigger re-render) notify('files'); } /** * Update file field (for editing) */ function updateFileField(index, fieldName, value) { const file = state.displayFiles[index]; if (!file) return; file[fieldName] = value; file.autoPopulated = false; // Clear auto-populated flag // Re-evaluate dirty: if every field still matches the parsed original, // and there is no manual filename override, the file is clean again. file.isDirty = _isFileDirty(file); // Notify listeners notify('files'); } /** * A file is dirty if its computed filename differs from the original, * or if it has a manual filename override. */ function _isFileDirty(file) { if (file.manualFilename) return true; const computed = zddc.formatFilename({ trackingNumber: file.trackingNumber || '', revision: file.revision || '', status: file.status || '', title: file.title || '', extension: file.extension || '', }); const original = zddc.joinExtension(file.originalFilename, file.extension); // If formatFilename returns '' (missing fields) fall back to original — not dirty return computed !== '' && computed !== original; } /** * Get display files (what should be shown in table) */ function getDisplayFiles() { return state.displayFiles; } /** * Get all files (unfiltered) */ function getAllFiles() { return state.allFiles; } /** * Get sort columns */ function getSortColumns() { return state.sortColumns; } /** * Get selected folder count */ function getSelectedFolderCount() { return state.selectedFolders.size; } /** * Get state (read-only) */ function getState() { return { rootHandle: state.rootHandle, folderTree: state.folderTree, selectedFolders: Array.from(state.selectedFolders), allFiles: state.allFiles, displayFiles: state.displayFiles, sortColumns: state.sortColumns, filters: state.filters, hideCompliant: state.hideCompliant }; } /** * Reset all state */ function reset() { state.rootHandle = null; state.folderTree = []; state.selectedFolders.clear(); state.allFiles = []; state.displayFiles = []; state.sortColumns = []; state.filters = {}; state.hideCompliant = false; notify('files'); notify('folders'); } // Export window.app.modules.store = { on, notify, setRootHandle, setFolderTree, toggleFolder, setSelectedFolders, toggleSort, setFilter, setAllFilters, setHideCompliant, updateFile, updateFileField, getDisplayFiles, getAllFiles, getSortColumns, getSelectedFolderCount, getState, reset }; })();