451 lines
12 KiB
JavaScript
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
|
|
};
|
|
})();
|