From 42d2279f232fd7db4dd24deafb072abde21da180 Mon Sep 17 00:00:00 2001 From: Kenneth Bruskiewicz Date: Thu, 22 Aug 2024 17:28:15 -0700 Subject: [PATCH] row removal in 1m rework --- lib/utils/1m.js | 360 ++++++++++++++++++++++++------------------------ 1 file changed, 182 insertions(+), 178 deletions(-) diff --git a/lib/utils/1m.js b/lib/utils/1m.js index 96ee66dd..414bd296 100644 --- a/lib/utils/1m.js +++ b/lib/utils/1m.js @@ -57,41 +57,6 @@ function findSharedKeys(schema) { return shared_keys_per_class; } -/** - * Visits each class in the schema tree and performs a callback function. - * @param {Object} schema_tree - The schema tree to visit. - * @param {string} cls_key - The starting class key for the visitation. - * @param {Function} callback - The function to perform on each class node. - */ -function visitSchemaTree(schema_tree, callback = tap, next = 'Container') { - if (!schema_tree[next]) return; // Base case: If the class key is not found - callback(schema_tree[next]); // Perform the callback on the current node - - // Recurse on each child node - const children = schema_tree[next].children.filter((el) => el); - children.forEach((next) => { - visitSchemaTree(schema_tree, callback, next); - }); -} - -/** - * Makes a column non-editable in a Handsontable instance based on a property key. - * @param {object} data_harmonizer - An object containing the Handsontable instance (`hot`). - * @param {string} property - The column data property or header name. - */ -function makeColumnReadOnly(data_harmonizer, shared_key_name) { - const hot = data_harmonizer.hot; - const columnIndex = data_harmonizer.getColumnIndexByFieldName(shared_key_name); - - if (columnIndex >= 0 && columnIndex < hot.countCols()) { - updateColumnSettings(hot, columnIndex, { readOnly: true }); - } else { - console.error( - `makeColumnReadOnly: Column not found for '${shared_key_name}'` - ); - } -} - function createFlatSchemaTree(classNames) { return { Container: { @@ -194,6 +159,23 @@ export function buildSchemaTree(schema) { return updateWithChildrenAndSharedKeys(preSchemaTree); } +/** + * Visits each class in the schema tree and performs a callback function. + * @param {Object} schema_tree - The schema tree to visit. + * @param {string} cls_key - The starting class key for the visitation. + * @param {Function} callback - The function to perform on each class node. + */ +function visitSchemaTree(schema_tree, callback = tap, next = 'Container') { + if (!schema_tree[next]) return; // Base case: If the class key is not found + callback(schema_tree[next]); // Perform the callback on the current node + + // Recurse on each child node + const children = schema_tree[next].children.filter((el) => el); + children.forEach((next) => { + visitSchemaTree(schema_tree, callback, next); + }); +} + // Utility to set column read-only function updateColumnSettings(hot, columnIndex, options) { const currentColumns = hot.getSettings().columns || []; @@ -201,6 +183,24 @@ function updateColumnSettings(hot, columnIndex, options) { hot.updateSettings({ columns: currentColumns }); } +/** + * Makes a column non-editable in a Handsontable instance based on a property key. + * @param {object} data_harmonizer - An object containing the Handsontable instance (`hot`). + * @param {string} property - The column data property or header name. + */ +function makeColumnReadOnly(data_harmonizer, shared_key_name) { + const hot = data_harmonizer.hot; + const columnIndex = data_harmonizer.getColumnIndexByFieldName(shared_key_name); + + if (columnIndex >= 0 && columnIndex < hot.countCols()) { + updateColumnSettings(hot, columnIndex, { readOnly: true }); + } else { + console.error( + `makeColumnReadOnly: Column not found for '${shared_key_name}'` + ); + } +} + // Two kinds of edits to be dealt with in 1-M: // - multiedit inside of a column // - propagating an edit when it occurs in one column but not another @@ -233,74 +233,6 @@ function makeColumnsReadOnly(data_harmonizers, schema_tree) { return data_harmonizers; } -function bindSelectHandlerEvents( - data_harmonizers, - schema_tree -) { - Object.values(data_harmonizers).forEach((dh) => { - - dh.hot.addHook('afterSelection', (row, ) => { - - // TODO: generalize to all shared keys! - const idValue = dh.hot.getDataAtCell(row, 0); - if (!isEmptyUnitVal(idValue)) { - const parent_name = dh.class_assignment; - const child_keys = schema_tree[parent_name].shared_keys.filter(el => el.relation === 'child'); - child_keys.forEach(child_key => { - - const shared_key_name = child_key.name; - const target_child_table = child_key.related_concept; - const shared_key_column = dh.getColumnIndexByFieldName(shared_key_name); - const valueToMatch = dh.hot.getDataAtCell(row, shared_key_column); - - if (!isEmptyUnitVal(valueToMatch)) { - - $(document).trigger('dhCurrentSelectionChange', { - currentSelection: { - source: dh.class_assignment, - shared_key_name, - valueToMatch, - parentCol: shared_key_column, - }, - }); - - const child_dh = data_harmonizers[target_child_table]; - const child_shared_key_column = child_dh.getColumnIndexByFieldName(shared_key_name); - - // Add a condition where the column value equals the specified value - const plugin = child_dh.hot.getPlugin('filters'); - plugin.clearConditions(child_shared_key_column); - if (valueToMatch !== null) { - plugin.addCondition(child_shared_key_column, 'eq', [valueToMatch]); - } - plugin.filter(); - - }; - }); - } else { - // empty row selection - $(document).trigger('dhCurrentSelectionChange', { - currentSelection: null, - }); - schema_tree[dh.class_assignment].children.forEach((child_name) => { - data_harmonizers[child_name].filterAll(); - }); - } - }); - - dh.hot.addHook('afterDeselect', () => { - $(document).trigger('dhCurrentSelectionChange', { - currentSelection: null, - }); - schema_tree[dh.class_assignment].children.forEach((child_name) => { - data_harmonizers[child_name].filterAll(); - }); - }); - }); - - return data_harmonizers; -} - const modifyRow = (hot, rowIndex, col, newVal) => { // need to use setSourceDataAtCell rather than setDataAtCell, or else the 'physical' dataset isn't edited // when only modifying the 'non-physical' dataset (returned by getData), there were rows that were duplicated when modified @@ -315,10 +247,12 @@ const clearRow = (hot, rowIndex) => { }; // closure to keep track of row data before removal -const createRowDataManager = (hot, contex_menu_callback) => { +const createRowDataManager = (hot, contextMenuCallback) => { let rowDataBeforeRemoval = {}; + let rowDataBeforeAdd = {}; return { + // Before removing a row beforeRemoveRow: function (index, amount, physicalRows) { for (let i = 0; i < amount; i++) { let rowIndex = physicalRows[i]; @@ -326,54 +260,45 @@ const createRowDataManager = (hot, contex_menu_callback) => { rowDataBeforeRemoval[rowIndex] = rowData; // Store the row data } }, + + // After removing a row afterRemoveRow: function (index, amount, physicalRows, source) { for (let i = 0; i < amount; i++) { let rowIndex = physicalRows[i]; let rowData = rowDataBeforeRemoval[rowIndex]; // Custom logic using the rowData - contex_menu_callback(rowData, source); + contextMenuCallback(rowData, 'removeRow'); } // Clear the temporary storage rowDataBeforeRemoval = {}; }, + + // Before adding a row + beforeCreateRow: function (index, amount, source) { + rowDataBeforeAdd = []; + for (let i = 0; i < amount; i++) { + let rowIndex = index + i; + let rowData = hot.getDataAtRow(rowIndex); + rowDataBeforeAdd.push(rowData); // Store the initial state before row is added + } + }, + + // After adding a row + afterCreateRow: function (index, amount, source) { + for (let i = 0; i < amount; i++) { + let rowIndex = index + i; + let rowData = hot.getDataAtRow(rowIndex); + // Custom logic using the rowData + contextMenuCallback(rowData, 'insertRow'); + } + // Clear the temporary storage + rowDataBeforeAdd = {}; + } }; }; -// const createContextMenuHandler = (data_harmonizers, shared_key_spec) => { -// return (rowData, source) => { -// schema_tree_node.children.forEach((cls_key) => { -// const hot = data_harmonizers[cls_key].hot; - -// const col = getColumnIndexByFieldName( -// data_harmonizers[cls_key], -// shared_key_spec.name -// ); -// const colVal = rowData[col]; - -// console.log(rowData, source, col, colVal); - -// hot.batch(() => { -// const CONTINUE_SIGNAL = true; -// const data = hot.getSourceData(); -// data.every((childRow, rowIndex) => { -// if (source === 'ContextMenu.removeRow') { -// if (childRow[col] === colVal) { -// // TODO: revise to shared key name? -// clearRow(hot, rowIndex); -// } -// } else if (source === 'ContextMenu.insertRow') { -// modifyRow(hot, rowIndex, col, colVal); -// return !CONTINUE_SIGNAL; -// } -// return CONTINUE_SIGNAL; -// }); -// }); -// hot.render(); -// }); -// }; -// }; - -const handleChange = (hot, [, col, oldVal, newVal]) => { +const handleChange = (hot, [col, oldVal, newVal]) => { + console.log('change', hot, [col, oldVal, newVal]) hot.batch(() => { const data = hot.getSourceData(); data.every((childRow, rowIndex) => { @@ -402,6 +327,20 @@ const handleChange = (hot, [, col, oldVal, newVal]) => { }); }; +function dispatchHandsontableUpdate(detail) { + const event = new CustomEvent('handsontableUpdate', { detail }); + document.dispatchEvent(event); +} + +function reselect(dh, row, prop) { + dh.hot.selectCell(row + 1, prop); + dh.hot.runHooks( + 'afterSelection', + row, + prop + ); +} + function parentBroadcastsCRUD(data_harmonizers, schema_tree) { // look for schema_tree Object.values(schema_tree).forEach((node) => { @@ -414,18 +353,15 @@ function parentBroadcastsCRUD(data_harmonizers, schema_tree) { // broadcast CRUD // Create and dispatch a custom event propagatesChangesOnTheseKeys.forEach((shared_key) => { - // afterChange + data_harmonizers[node.name].hot.addHook( 'afterChange', (changes, source) => { - if (source === 'loadData') { - return; // don't act on changes caused by loading data - } + if (source === 'loadData') return; // don't act on changes caused by loading data if (changes) { changes.forEach(function ([row, prop, oldValue, newValue]) { - const event = new CustomEvent('handsontableUpdate', { + dispatchHandsontableUpdate({ detail: { - row, prop, oldValue, newValue, @@ -433,24 +369,10 @@ function parentBroadcastsCRUD(data_harmonizers, schema_tree) { sharedKey: shared_key, }, }); - document.dispatchEvent(event); - - // TODO: Follow up selections - // harmonizes behaviour between enter and clicks for enums - data_harmonizers[node.name].hot.selectCell(row, prop); - data_harmonizers[node.name].hot.runHooks( - 'afterSelection', - row, - prop - ); - data_harmonizers[node.name].hot.selectCell(row + 1, prop); - data_harmonizers[node.name].hot.runHooks( - 'afterSelection', - row + 1, - prop - ); - }); + // TODO: Follow up selections + // harmonizes behaviour between enter and clicks for enums + reselect(data_harmonizers[node.name], row, prop); } } ); @@ -458,17 +380,34 @@ function parentBroadcastsCRUD(data_harmonizers, schema_tree) { // Add the beforeRemoveRow and afterRemoveRow hooks using addHook const rowDataManager = createRowDataManager( data_harmonizers[node.name].hot, - (id) => id - ); - data_harmonizers[node.name].hot.addHook( - 'beforeRemoveRow', - rowDataManager.beforeRemoveRow - ); - data_harmonizers[node.name].hot.addHook( - 'afterRemoveRow', - rowDataManager.afterRemoveRow + (rowData, source) => { + console.log('context menu callback', source); + switch (source) { + case 'removeRow': + dispatchHandsontableUpdate({ + detail: { + prop: data_harmonizers[node.name].getColumnIndexByFieldName(shared_key.name), + oldValue: rowData[prop], + newValue: null, + sourceTable: node.name, + sharedKey: shared_key, + } + }) + // TODO: get proper row and prop + // reselect(data_harmonizers[node.name], row, prop); + break; + // TODO: insert row case + case 'insertRow': + break; + default: + } + } ); + for (const callbackName in rowDataManager) { + data_harmonizers[node.name].hot.addHook(callbackName, rowDataManager[callbackName]); + }; + // // TODO: undo/redo // hot.addHook('afterUndo', function (action) { // console.log('undo', action); @@ -480,6 +419,7 @@ function parentBroadcastsCRUD(data_harmonizers, schema_tree) { // undo_redo_callback('redo', action); // }); // // TODO: copy/paste + }); } else { console.warn('no parenthood defined', node.name); @@ -501,7 +441,7 @@ function childListensCRUD(data_harmonizers, schema_tree) { // afterChange // assign event to DH container? document.addEventListener('handsontableUpdate', function (event) { - const { row, prop, oldValue, newValue, sourceTable, sharedKey } = + const { oldValue, newValue, sourceTable, sharedKey } = event.detail; const childCol = data_harmonizers[node.name].getColumnIndexByFieldName( sharedKey.name @@ -512,16 +452,12 @@ function childListensCRUD(data_harmonizers, schema_tree) { .includes(sourceTable) ) { handleChange(data_harmonizers[node.name].hot, [ - row, childCol, oldValue, newValue, ]); } }); - // TODO: row insertion/deletion - // TODO: undo/redo - // TODO: copy/paste } else { console.warn('no childhood defined', node.name); } @@ -529,9 +465,77 @@ function childListensCRUD(data_harmonizers, schema_tree) { }); } +function bindSelectHandlerEvents( + data_harmonizers, + schema_tree +) { + Object.values(data_harmonizers).forEach((dh) => { + + dh.hot.addHook('afterSelection', (row, ) => { + + // TODO: generalize to all shared keys! + const idValue = dh.hot.getDataAtCell(row, 0); + if (!isEmptyUnitVal(idValue)) { + const parent_name = dh.class_assignment; + const child_keys = schema_tree[parent_name].shared_keys.filter(el => el.relation === 'child'); + child_keys.forEach(child_key => { + + const shared_key_name = child_key.name; + const target_child_table = child_key.related_concept; + const shared_key_column = dh.getColumnIndexByFieldName(shared_key_name); + const valueToMatch = dh.hot.getDataAtCell(row, shared_key_column); + + if (!isEmptyUnitVal(valueToMatch)) { + + $(document).trigger('dhCurrentSelectionChange', { + currentSelection: { + source: dh.class_assignment, + shared_key_name, + valueToMatch, + parentCol: shared_key_column, + }, + }); + + const child_dh = data_harmonizers[target_child_table]; + const child_shared_key_column = child_dh.getColumnIndexByFieldName(shared_key_name); + + // Add a condition where the column value equals the specified value + const plugin = child_dh.hot.getPlugin('filters'); + plugin.clearConditions(child_shared_key_column); + if (valueToMatch !== null) { + plugin.addCondition(child_shared_key_column, 'eq', [valueToMatch]); + } + plugin.filter(); + + }; + }); + } else { + // empty row selection + $(document).trigger('dhCurrentSelectionChange', { + currentSelection: null, + }); + schema_tree[dh.class_assignment].children.forEach((child_name) => { + data_harmonizers[child_name].filterAll(); + }); + } + }); + + dh.hot.addHook('afterDeselect', () => { + $(document).trigger('dhCurrentSelectionChange', { + currentSelection: null, + }); + schema_tree[dh.class_assignment].children.forEach((child_name) => { + data_harmonizers[child_name].filterAll(); + }); + }); + }); + + return data_harmonizers; +} + export const setup1M = (data_harmonizers, schema_tree) => { makeColumnsReadOnly(data_harmonizers, schema_tree); - bindSelectHandlerEvents(data_harmonizers, schema_tree); parentBroadcastsCRUD(data_harmonizers, schema_tree); childListensCRUD(data_harmonizers, schema_tree); + bindSelectHandlerEvents(data_harmonizers, schema_tree); };