ZDDC/classifier/js/store.js
2026-06-11 13:32:31 -05:00

451 lines
12 KiB
JavaScript

/**
* 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
};
})();