diff --git a/lib/utils/1m.js b/lib/utils/1m.js index 7fb41475..4a04eeda 100644 --- a/lib/utils/1m.js +++ b/lib/utils/1m.js @@ -38,15 +38,6 @@ Object or Attribute */ -// TODO -// user prompt -// - undo -// - DONE distinguish between primary key deletion on duplicate vs deleting lone primary key -// - DONE see `isSharedKeyNameDuplicatedInCells` -// - change of primary key -// - final primary key deleted -> propagate deletion -// - DONE ACTION.DELETE dispatch even if logic is shared - const SchemaClassesList = (schema) => { return Object.keys(schema.classes).filter( (key) => !['dh_interface', 'Container'].includes(key) @@ -180,25 +171,6 @@ const findUniqueKeysForClass = (schema, cls_key) => { return unique_keys_acc; }; -const findActiveDataHarmonizer = (appContext) => { - // active = focused - let active_dh = null; - for (let dh_name in appContext) { - if (appContext[dh_name].active) { - active_dh = dh_name; - break; - } - } - return active_dh; -}; - -const setActiveDataHarmonizer = (appContext, active_dh_name) => { - for (let dh_name in appContext) { - appContext[dh_name].active = false; - } - appContext[active_dh_name].active = true; -}; - const addChildClassesToUniqueKeysForAppContext = (appContext) => { for (let dh_class_name in appContext) { for (let key in appContext[dh_class_name].unique_keys) { @@ -284,7 +256,7 @@ const bindReadEmitter = (appContext, dh) => { ); // Emit an empty "Read" event to indicate no specific key selection - dispatchHandsontableUpdate2({ + dispatchHandsontableUpdate({ action: ACTION.READ, emitted_by: dh.class_assignment, row, @@ -315,7 +287,7 @@ const bindReadEmitter = (appContext, dh) => { // If there are any unique keys within the selected column range, emit "Read" events // TODO: one at a time vs reading several for (let key_name of maybe_unique_keys) { - dispatchHandsontableUpdate2({ + dispatchHandsontableUpdate({ action: ACTION.READ, emitted_by: dh.class_assignment, target: dh.class_assignment, @@ -461,35 +433,8 @@ Primary key table triggering event (may be on more than one unique key): Event: (classDataRowKeyChange, className, keyName, action) */ -function dispatchHandsontableUpdate(action, detail) { - const _detail = { ...detail, action }; - const event = new CustomEvent('classDataRowKeyChange', { - action, - detail: _detail, - }); - document.dispatchEvent(event); -} - -const bindActionHandler = (appContext, dh) => { - // namespace the listener event - dh.hot.addHook( - `classDataRowKeyChange:${dh.class_assignment}`, - (action, details) => { - handleAction(appContext, dh, action, details); - } - ); -}; - -const runHook = (dh, action, details) => { - dh.hot.runHooks( - `classDataRowKeyChange:${dh.class_assignment}`, - action, - details - ); -}; - const destroyHandsontableUpdateRouter = () => { - // document.removeEventListener('classDataRowKeyChange'); + document.removeEventListener('handsontableUpdate', provideActionCallback); }; const changesToRows = (changes) => { @@ -516,115 +461,6 @@ const changesToRows = (changes) => { return rowWiseChanges; }; -const bindChangeEmitter = (appContext, dh) => { - dh.hot.addHook('afterChange', (changes, source) => { - if (source === 'loadData') return; - else if (source === 'edit' && changes) { - // Array to store indices of changed cells - const changed_cols = []; - changes.forEach(([row, column, old_value, new_value]) => { - // Check if there is a change (oldValue is different from newValue) - if (old_value !== new_value) { - // Add the changed cell indices to the array - changed_cols.push(column); - - console.log( - `Change detected at row ${row + 1}, column ${column + 1}:` - ); - console.log(`Old Value: ${old_value}, New Value: ${new_value}`); - } - }); - - const minColumn = Math.min(changed_cols); - const maxColumn = Math.max(changed_cols); - - // TODO: key in unique_keys? - const [min, max] = [minColumn, maxColumn]; - const maybe_unique_keys_in_range = uniqueKeysInIndexRange(appContext, dh.class_assignment, min, max); - - if (maybe_unique_keys_in_range.length > 0) { - - const unique_keys_in_scope = relevantUniqueKeys(appContext, dh.class_assignment, maybe_unique_keys_in_range); - - for (let key_name in unique_keys_in_scope) { - const column_index = unique_keys_in_scope[key_name].key_indexes; - const row_changes = changesToRows(changes); - for (let row in row_changes) { - // dispatch action based on changes - - // Dispatch a DELETE action if the value the new value is an empty unit value - const updateActionType = - Object.values( - row_changes[row].newValues - ).map(isEmptyUnitVal).includes(true) ? - ACTION.DELETE - : ACTION.UPDATE; - - // TODO: modal continuation interface - const currentValue = row_changes[row].oldValues[column_index]; - - // console.warn('row_changes', row_changes, - // 'column_index', column_index, - // row_changes[row].oldValues, - // row_changes[row].newValues); - - // propagate deletion when the deleted cell was unique of its kind - // always propagate updates - const propagates = updateActionType === ACTION.DELETE ? - // if deletion is triggering, the current non empty column values should still be one down from the original value - // NOTE: if this is problematic, need to replace *all* handsontable with CRUD - getNonEmptyColumnValues(dh.hot, column_index).filter(el => el === currentValue).length === 0 - : true; - - // console.warn( - // updateActionType, - // currentValue, - // futureValue, - // getNonEmptyColumnValues(dh.hot, column_index), - // getNonEmptyColumnValues(dh.hot, column_index).filter(el => el === currentValue).length === 0, - // propagates); - - const details = { - emitted_by: dh.class_assignment, - target: dh.class_assignment, - row, - key_name, - key_old_values: takeKeys(column_index)( - row_changes[row].oldValues - ), - key_new_values: takeKeys(column_index)( - row_changes[row].newValues - ), - old_changes: row_changes[row].oldValues, - new_changes: row_changes[row].newValues, - row_changes, - propagates - }; - - if (propagates) { - // warn about deletion if it's the final key - if (updateActionType === ACTION.DELETE) { - console.warn("PROPAGATION:", "Row collections with key will be deleted", details); - } - - // // warn if update will cause a duplicate that merges entries in child tables which share that same key - if (updateActionType === ACTION.UPDATE && getNonEmptyColumnValues(dh.hot, column_index).filter(el => el === currentValue).length + 1 > 1) { - console.warn("PROPAGATION:", "Row collections will merge with existing those of existing key value", details); - } - } - - dispatchHandsontableUpdate(updateActionType, details); - - if (findActiveDataHarmonizer(appContext) === details.emitted_by) { - dispatchHandsontableUpdate(ACTION.SELECT, details); - } - } - } - } - } - }); -}; - /* 4: Foreign key table listener A listening table event handler triggers, based on an event on one of its foreign key slots. Importantly, it must throw the same event as it detected, as though its own primary key had changed. This enables trickle-down effects. @@ -637,39 +473,6 @@ Where action can be create, read, update, delete. Details can be fetched by listener via appContext[className].unique_keys[keyName] . Also appContext[class][“active”] flag can be updated accordingly. */ -// NOTE: Handsontable doesn't have a arbitrary listeners like the DOM. Hooks are fired through methods on individual tables. -// We emulate a listener by using a router instead. - -// if a unique key has a hook -// the events fire on the child, setting source and target to be the same -const dispatchChildHooks = (unique_key, action, details) => { - if ('child_classes' in unique_key) { - for (let child_class of unique_key.child_classes) { - dispatchHandsontableUpdate(action, { - ...details, - target: child_class, - }); - } - } -}; - -const handsontableUpdateRouter = (appContext, dhs) => { - document.addEventListener('classDataRowKeyChange', (event) => { - const { emitted_by, target, key_name, action, propagates = true } = event.detail; - - // The class can fire when it has unique keys - const classCanFire = - emitted_by in appContext && - key_name in appContext[emitted_by].unique_keys; - const unique_key = appContext[emitted_by]?.unique_keys[key_name]; - - if (classCanFire) { - routeAction(action, emitted_by, target, dhs, event, appContext, propagates, unique_key); - } - - }); -}; - 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 @@ -720,95 +523,26 @@ const updateRows = (dh, [col, oldVal, newVal]) => { }); }; -const handleAction = (appContext, dh, action, details) => { - const { - emitted_by, - key_name, - old_changes, - new_changes, - row_changes, - row, - column, - target, - currentSelection, - } = details; - const shared_key_details = appContext[emitted_by].unique_keys[key_name]; - if (!shared_key_details) { - console.error(`Key details not found for ${key_name} in ${emitted_by}.`); - return; - } - // Use Handsontable's batch operation to group multiple changes - console.info( - action, - emitted_by, - '->', - target, - old_changes, - new_changes, - currentSelection, - row, column - ); - switch (action) { - case ACTION.CREATE: - break; - case ACTION.READ: - if (row !== appContext[target].row && !isEmptyUnitVal(row)) { - appContext[target].row = row; - } - if (column !== appContext[target].column && !isEmptyUnitVal(column)) { - appContext[target].column = column; - } - setActiveDataHarmonizer(appContext, target); - break; - case ACTION.FILTER: - // TODO: generalize to multi-key - dh.hideMatchingRows( - dh.getColumnIndexByFieldName(key_name), - currentSelection.valueToMatch - ); - break; - case ACTION.UPDATE: - case ACTION.DELETE: - appContext[emitted_by].unique_keys[key_name].key_old_values = old_changes; - appContext[emitted_by].unique_keys[key_name].key_values = new_changes; - - if (emitted_by === target) { - for (let row in row_changes) { - for (let col_index in shared_key_details.key_indexes) { - if (col_index in row_changes[row].newValues) { - dh.hot.setSourceDataAtCell(row, col_index, row_changes[row].newValues[col_index]) - } - } - }; - } else { - // propagation - for (let col_index in shared_key_details.key_indexes) { - updateRows(dh, [ - col_index, - appContext[emitted_by].unique_keys[key_name].key_old_values[ - col_index - ], - appContext[emitted_by].unique_keys[key_name].key_values[col_index], - ]); - } - } - break; - case ACTION.SELECT: - triggerCurrentSelectionChange(currentSelection); - break; - case ACTION.VALIDATE: - // TODO: generalize to row targetting - dh.validate(); +const findActiveDataHarmonizer = (appContext) => { + // active = focused + let active_dh = null; + for (let dh_name in appContext) { + if (appContext[dh_name].active) { + active_dh = dh_name; break; - default: - console.error(`Can't handle action ${action}`); + } } + return active_dh; }; -const triggerCurrentSelectionChange = (currentSelection) => { - $(document).trigger('dhCurrentSelectionChange', { currentSelection }); +const setActiveDataHarmonizer = (appContext, active_dh_name) => { + for (let dh_name in appContext) { + appContext[dh_name].active = false; + } + appContext[active_dh_name].active = true; }; + const makeCurrentSelection = (appContext, dh) => { let { row, column, unique_keys } = structuredClone( appContext[dh.class_assignment] @@ -833,47 +567,9 @@ const makeCurrentSelection = (appContext, dh) => { return currentSelectionObject; } -function routeAction(action, emitted_by, target, dhs, event, appContext, propagates, unique_key) { - if (action === ACTION.READ && emitted_by === target) { - runHook(dhs[emitted_by], action, event.detail); - dispatchHandsontableUpdate(ACTION.SELECT, event.detail); - } else if (action === ACTION.SELECT) { - const activeDataHarmonizer = dhs[findActiveDataHarmonizer(appContext)]; - event.detail.currentSelection = - makeCurrentSelection(appContext, activeDataHarmonizer); - runHook(activeDataHarmonizer, action, event.detail); - // propagate filter actions to child tables if possible and allowed - if (propagates) { - // TODO: how to scope row changes - event.detail.row = null; - dispatchChildHooks(unique_key, ACTION.FILTER, event.detail); - } - } else if (action === ACTION.FILTER) { - runHook(dhs[target], action, event.detail); - } else if (action === ACTION.UPDATE || action === ACTION.DELETE) { - runHook(dhs[target], action, event.detail); - if (emitted_by === target) { - // propagate update actions to child table if possible - if (propagates) { - // TODO: how to scope row changes - event.detail.row = null; - dispatchChildHooks(unique_key, action, event.detail); - } - } - } else if (action === ACTION.VALIDATE) { - runHook(dhs[emitted_by], action, event.detail); - } else { - console.warn( - 'not firing hook', - emitted_by, - action, - appContext[emitted_by]?.unique_keys, - unique_key, - unique_key.foreign_key, - 'foreign_key_class' in unique_key - ); - } -} +const triggerCurrentSelectionChange = (currentSelection) => { + $(document).trigger('dhCurrentSelectionChange', { currentSelection }); +}; /** * Makes a column non-editable in a Handsontable instance based on a property key. @@ -905,12 +601,17 @@ const makeColumnsReadOnly = (appContext, dh) => { } }; -const dispatchHandsontableUpdate2 = (detail) => { + + +// NOTE: Handsontable doesn't have a arbitrary listeners like the DOM. Hooks are fired through methods on individual tables. +// We emulate a listener by using a router instead. + +const dispatchHandsontableUpdate = (detail) => { const event = new CustomEvent('handsontableUpdate', { detail }); document.dispatchEvent(event); } -function parentBroadcastsCRUD(appContext, dhs) { +function bindParentBroadcastsChange(appContext, dhs) { // look for nodes const nodes = Object.keys(appContext); nodes.forEach(node_name => { @@ -918,12 +619,15 @@ function parentBroadcastsCRUD(appContext, dhs) { for (let unique_key_name in node.unique_keys) { const unique_key = node.unique_keys[unique_key_name]; if (unique_key.foreign_key) { - // console.warn(unique_key.name, 'is foreign key'); + // child class "adds" listener to parent class // this way only the relevant fields ever emit CRUD events // this could be replaced with an emission for each slot as well if necessary const { foreign_key_class, foreign_key_slot } = unique_key; const dh = dhs[foreign_key_class]; + + // dispatch change event with dispatchHandsontableUpdate, which will get a child dataharmonizer to fire 'routeAction' + // TODO: namespace the event? dh.hot.addHook('afterChange', (changes, source) => { console.log('emitting change', foreign_key_class, foreign_key_slot); if (source === 'loadData') { @@ -948,10 +652,10 @@ function parentBroadcastsCRUD(appContext, dhs) { const detail = { action: updateActionType, - emitted_by: foreign_key_class, + emitted_by: foreign_key_class, + key_name: foreign_key_slot, target: node_name, row, - key_name: foreign_key_slot, key_old_values: takeKeys([column_index])( row_changes[row].oldValues ), @@ -963,11 +667,11 @@ function parentBroadcastsCRUD(appContext, dhs) { row_changes }; - dispatchHandsontableUpdate2(detail); + dispatchHandsontableUpdate(detail); if (findActiveDataHarmonizer(appContext) === detail.emitted_by) { detail.action = ACTION.SELECT; - dispatchHandsontableUpdate2(detail); + dispatchHandsontableUpdate(detail); } } @@ -976,50 +680,63 @@ function parentBroadcastsCRUD(appContext, dhs) { }); } else { // Do nothing - console.warn(unique_key.name, 'is not foreign key'); + // console.warn(unique_key.name, 'is not foreign key'); } } }); } -function childListensCRUD(appContext, dhs) { +function provideActionCallback(appContext, dhs) { + return function (event) { + // TODO: check here if I need the listener to restrict routeAction based on: + // - the foreign key and primary key relationship + // - the emitted and target class + routeAction(appContext, dhs, event.detail.action, event); + }; +} + +function bindChildListensAndHandlesChange(appContext, dhs) { const nodes = Object.keys(appContext); nodes.forEach(node_name => { const node = appContext[node_name]; for (let unique_key_name in node.unique_keys) { const unique_key = node.unique_keys[unique_key_name]; if (unique_key.foreign_key) { - document.addEventListener('handsontableUpdate', function (event) { - routeAction2(appContext, dhs, event.detail.action, event); - }); + document.addEventListener('handsontableUpdate', provideActionCallback(appContext, dhs)); } }; }); } -const routeAction2 = (appContext, dhs, action, event) => { +const routeAction = (appContext, dhs, action, event) => { const { emitted_by, target, key_name } = event.detail; const unique_key = appContext[emitted_by]?.unique_keys[key_name]; if (action === ACTION.READ && emitted_by === target) { - handleAction2(appContext, dhs, dhs[emitted_by], action, event.detail); + handleAction(appContext, dhs, dhs[emitted_by], action, event.detail); event.detail.action = ACTION.SELECT - dispatchHandsontableUpdate2(event.detail); + dispatchHandsontableUpdate(event.detail); } else if (action === ACTION.SELECT) { const activeDataHarmonizer = dhs[findActiveDataHarmonizer(appContext)] event.detail.currentSelection = makeCurrentSelection(appContext, activeDataHarmonizer); event.detail.target = event.detail.currentSelection.name; - handleAction2(appContext, dhs, activeDataHarmonizer, action, event.detail); - dispatchChildHooks2(unique_key, ACTION.FILTER, event.detail); + handleAction(appContext, dhs, activeDataHarmonizer, action, event.detail); + try { + dispatchChildHooks(unique_key, ACTION.FILTER, event.detail); + } catch(e) { + console.error(e); + console.warn(emitted_by, appContext[emitted_by], unique_key, key_name, event.detail); + debugger; + } } else if (action === ACTION.VALIDATE) { - handleAction2(appContext, dhs, dhs[emitted_by], action, event.detail); + handleAction(appContext, dhs, dhs[emitted_by], action, event.detail); } else { // FILTER, UPDATE, DELETE - handleAction2(appContext, dhs, dhs[target], action, event.detail); + handleAction(appContext, dhs, dhs[target], action, event.detail); } } -const handleAction2 = (appContext, dhs, dh, action, details) => { +const handleAction = (appContext, dhs, dh, action, details) => { let { emitted_by, key_name, @@ -1113,14 +830,12 @@ const handleAction2 = (appContext, dhs, dh, action, details) => { } }; -const dispatchChildHooks2 = (unique_key, action, details) => { +const dispatchChildHooks = (unique_key, action, details) => { if ('child_classes' in unique_key) { for (let child_class of unique_key.child_classes) { - dispatchHandsontableUpdate2({ + dispatchHandsontableUpdate({ ...details, action, - // emitted_by, - // key_name, target: child_class, }); } @@ -1128,6 +843,7 @@ const dispatchChildHooks2 = (unique_key, action, details) => { }; export const setup1M = ({ appContext }, dhs) => { + console.log(appContext) destroyHandsontableUpdateRouter(); for (let dh in dhs) { if (dh in appContext) { @@ -1136,13 +852,13 @@ export const setup1M = ({ appContext }, dhs) => { if (DEBUG) bindKeyConstraintValidation(appContext, dhs[dh]); createRowFocusTracker(dhs[dh], () => dhs[dh].validate()); - parentBroadcastsCRUD(appContext, dhs); - childListensCRUD(appContext, dhs); + bindParentBroadcastsChange(appContext, dhs); + bindChildListensAndHandlesChange(appContext, dhs); - // bindChangeEmitter(appContext, dhs[dh]); // create, update, delete - // bindActionHandler(appContext, dhs[dh]); } } - handsontableUpdateRouter(appContext, dhs); + // handsontableUpdateRouter(appContext, dhs); return appContext; }; + +