Skip to content

Commit ec8c1f9

Browse files
committed
Revising row edit behavour
Processing data grid changes as batch per row.
1 parent 42590fe commit ec8c1f9

File tree

2 files changed

+190
-36
lines changed

2 files changed

+190
-36
lines changed

lib/AppContext.js

+40-21
Original file line numberDiff line numberDiff line change
@@ -526,29 +526,34 @@ export default class AppContext {
526526
const hiddenRowsPlugin = hotInstance.getPlugin('hiddenRows');
527527

528528
// Ensure all rows are visible
529-
const oldHiddenRows = hiddenRowsPlugin.getHiddenRows()
529+
const oldHiddenRows = hiddenRowsPlugin.getHiddenRows();
530+
// arrayRange() makes [0, ... n]
531+
let rowsToHide = arrayRange(0, hotInstance.countSourceRows());
530532

531533
//Fetch key-values to match
532534
const parents = this.crudGetParents(class_name);
533535
let [required_selections, errors] = this.crudGetForeignKeyValues(parents);
534536

535-
// arrayRange() makes [0, ... n]
536-
let rowsToHide = arrayRange(0, hotInstance.countSourceRows()) // countSourceRows?
537-
.filter((row) => {
538-
for (let [slot_name, value] of Object.entries(required_selections)) {
539-
const col = this.dhs[class_name].getColumnIndexByFieldName(slot_name);
540-
const cellValue = hotInstance.getDataAtCell(row, col);
541-
if (cellValue != value)
542-
return true;
543-
}
544-
});
545-
546-
if (rowsToHide != oldHiddenRows) {
537+
// FUTURE: may be able to specialize to hide display based on particular
538+
// parent(s) keys?
539+
if (errors.length == 0) {
540+
rowsToHide = rowsToHide.filter((row) => {
541+
for (let [slot_name, value] of Object.entries(required_selections)) {
542+
const col = this.dhs[class_name].getColumnIndexByFieldName(slot_name);
543+
const cellValue = hotInstance.getDataAtCell(row, col);
544+
// Null value test in case where parent or subordinate field is null.
545+
if (cellValue != value)
546+
return true;
547+
}
548+
});
549+
}
550+
// Make more efficient using Set() difference comparison?
551+
//if (rowsToHide != oldHiddenRows) {
547552
//Future: make more efficient by switching state of show/hide deltas?
548553
hiddenRowsPlugin.showRows(oldHiddenRows);
549554
hiddenRowsPlugin.hideRows(rowsToHide); // Hide the calculated rows
550555
hotInstance.render(); // Render the table to apply changes
551-
}
556+
//}
552557
}
553558

554559
/* Here we deal with adding rows that need foreign keys. Locate each foreign
@@ -621,7 +626,8 @@ export default class AppContext {
621626
* isn't unique, then no deletions should take place in child table, and
622627
* no need to report an error.
623628
* Dependent may depend on more than one table, but regarding deletion,
624-
* values of foreign keys to other tables don't matter.
629+
* values of foreign keys to other tables can be taken as a separate
630+
* deletion case to consider?
625631
*
626632
* 2) Report back how many deletions would occur (often all that are
627633
* visible, since child table record visibility would be tuned to
@@ -672,14 +678,31 @@ export default class AppContext {
672678
return (do_delete || found_dependent_rows == false) ? false : warning_text;
673679
}
674680

681+
/* Looks for 2nd instance of row containing keyVals, returns that row or
682+
* null if not found. PROBABLY NOT USEFUL since our work on changing or
683+
* adding unique_key fields tests for keys BEFORE user change, i.e. just
684+
* finding one existing key is enough to halt operation.
685+
*/
686+
crudFindDuplicate(dh, keyVals) {
687+
let found_row = this.crudFindByKeyVals(dh, keyVals);
688+
// Looking for another record besides parent being deleted.
689+
//if (!found_row)
690+
// return null;
691+
//console.log(found_row)
692+
return this.crudFindByKeyVals(dh, keyVals, found_row+1);
693+
}
675694

695+
/* Looks for row (starting from 0 by default) containing given keyVals.
696+
* returns null if not found.
697+
* ISSUE: This isn't using indexes on tables, so not very efficient.
698+
*/
676699
crudFindByKeyVals(dh, keyVals, row = 0) {
677700
const total_rows = dh.hot.countSourceRows(); // .countSourceRows();
678701
while (row < total_rows) {
679702
// .every() causes return on first break
680703
if (Object.entries(keyVals).every(([slot_name, value]) => {
681-
const col = dh.getColumnIndexByFieldName(slot_name); // map to col#
682-
return (value == dh.hot.getDataAtCell(row,col));
704+
const col = dh.getColumnIndexByFieldName(slot_name);
705+
return (value == dh.hot.getDataAtCell(row, col));
683706
})
684707
)
685708
break;
@@ -719,8 +742,4 @@ export default class AppContext {
719742
});
720743
}
721744

722-
723-
724-
725-
726745
}

lib/DataHarmonizer.js

+150-15
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,6 @@ class DataHarmonizer {
210210
)
211211
.filter(
212212
([cls_key]) =>
213-
//cls_key !== 'dh_interface' &&
214-
//cls_key !== 'Container' &&
215213
cls_key === this.class_assignment
216214
)
217215
.reduce((acc, [, spec]) => {
@@ -720,6 +718,15 @@ class DataHarmonizer {
720718
self.addRows('insert_row_above', 1, parseInt(self.hot.getSelected()[0][0])+1);
721719
},
722720
},
721+
{
722+
key: 'change_primary_key',
723+
name: 'Change primary key',
724+
callback: function () {
725+
let entry = prompt("Enter new value for this primary key field, or press Cancel");
726+
alert("Coming soon...")
727+
728+
},
729+
},
723730
],
724731
outsideClickDeselects: false, // for maintaining selection between tabs
725732
manualColumnResize: true,
@@ -743,30 +750,159 @@ class DataHarmonizer {
743750
// REEXAMINE ABOVE assumption now that setDataAtCell() problem solved.
744751
invalidCellClassName: '',
745752
licenseKey: 'non-commercial-and-evaluation',
746-
// beforeChange source: https://handsontable.com/docs/8.1.0/tutorial-using-callbacks.html#page-source-definition
747-
beforeChange: function (changes) {
748-
if (!changes) {
753+
754+
/* Changes = array of [row, column, old value (incl. undefined), new
755+
* value (incl. null)]. NOTE: Cut & paste can include 2 or more
756+
* changes in different columns on a row, or on multiple rows, so we
757+
* need to validate 1 row at a time. Row_change data structure enables
758+
* this.
759+
*/
760+
beforeChange: function (grid_changes) {
761+
// beforeChange source: https://handsontable.com/docs/8.1.0/tutorial-using-callbacks.html#page-source-definition
762+
763+
if (!grid_changes) { // Can this ever happen here?
749764
return;
750765
}
751766

752-
// When a change in one field triggers a change in another field.
753-
let triggered_changes = [];
767+
/* row_change is a tacitly ordered dict of row #s.
768+
* {[row]: {[slot_name]: {old_value: _, value: _} } }
769+
*/
770+
let row_change = {};
771+
for (const change of grid_changes) {
772+
let slot = self.slots[change[1]];
773+
if (!(change[0] in row_change)) {
774+
row_change[change[0]] = {};
775+
}
776+
row_change[change[0]][slot.name] = {
777+
slot: slot,
778+
col: change[1],
779+
old_value: change[2],
780+
value: change[3]
781+
};
782+
}
754783

755-
for (const change of changes) {
784+
// Validate each row of changes
785+
for (const row in row_change) {
786+
let changes = row_change[row];
787+
788+
/* Change request cancelled (sudden death) if user tries to change
789+
* an identifier (unique key) value but this would result in a
790+
* duplicate key.
791+
*/
792+
for (const slot_name in changes) {
793+
let change = changes[slot_name];
794+
if (change.slot.identifier == true && change.value != null) {
795+
// Look for collision with an existing row's column value.
796+
// Change hasn't been implemented yet so we won't run into
797+
// change's row.
798+
let search_row = self.context.crudFindByKeyVals(self, {[slot_name]: change.value});
799+
if (search_row && search_row != row) {
800+
alert(`Skipping change on row ${parseInt(row)+1} because this would create a duplicate key! Check:\n\n * [${slot_name}] change to "${change.value}"\n`);
801+
return false;
802+
}
803+
}
804+
}
805+
806+
/*
807+
* If a class has more than one slot in its unique_key then multiple
808+
* changes to key must be tested for duplicates as well as multiple
809+
* possible impacts on dependent tables.
810+
*
811+
* NOTE: user can be updating one field of a primary key; neets to
812+
* be able to edit it in incomplete form.
813+
*
814+
* Determine if change(s) (including cut & paste changes to more
815+
* than one slot) contribute to a complete unique_key, and if so
816+
* test if there's a duplication of that key. If key slot data entry
817+
* is unfinished, then no need to throw error.
818+
* Requires that we iterate through class's unique_keys list,
819+
* seeing if any new column (changes[0]) value (changes[3]) pertains
820+
* to one. If so, test to see if key has a value. If no value,
821+
* then skip key, otherwise test new key uniqueness.
822+
*
823+
* Additional case: turning a component of a unique key into null
824+
* in itself doesn't necessarily trigger anything other than a
825+
* validation error on that construct when user presses "Validate".
826+
* Note: Validator.js validate() also has step for applying the
827+
* doUniquenessValidation() method on unique_key_slots.
828+
*
829+
* However, any field being used as the target of a foreign key of
830+
* another table, if it had a value and that changes, leaving no
831+
* other key with the same value that foreign table key could use
832+
* in its place, necessitates a cancel of that operation.
833+
*
834+
* Only route to making a change to a unique key is in the
835+
* "change_primary_key" right-click menu option above.
836+
*
837+
* [class]: {
838+
* "unique_keys": {
839+
* "grdisample_id": {
840+
* "unique_key_name": "grdisample_id",
841+
* "unique_key_slots": [
842+
* "sample_collector_sample_id"
843+
* ],
844+
*/
845+
// FUTURE slot_ptr version of self.context.relations for efficiency.
846+
let this_class = self.schema.classes[self.template_name];
847+
for (let key_name in this_class?.unique_keys) {
848+
var key_obj = this_class.unique_keys[key_name];
849+
var keyVals = {};
850+
var change_log = '';
851+
// If change doesn't pertain to any index slots then keyVals remains empty.
852+
if ( Object.entries(key_obj.unique_key_slots).every(([index, slot_name]) => {
853+
// Key has a changed value (incl. null?), so update it.
854+
if (slot_name in changes) {
855+
let change = changes[slot_name];
856+
change_log += `* [${slot_name}] change to "${change.value}"\n`
857+
keyVals[slot_name] = change.value;
858+
return true;
859+
}
860+
else {
861+
let col = self.getColumnIndexByFieldName(slot_name);
862+
let value = self.hot.getDataAtCell(row, col);
863+
if (value != null) {
864+
keyVals[key_name] = value;
865+
return true;
866+
}
867+
}
868+
869+
// Key value hasn't changed, and is missing a value, so
870+
// unique_key is not established, so user's change can go
871+
// through.
872+
return false;
873+
}) ) {
874+
// Here we have complete unique_keys entry to test for duplication
875+
let duplicate_row = self.context.crudFindByKeyVals(self, keyVals);
876+
// There is a matching row so fail this action.
877+
if (duplicate_row && duplicate_row != row) {
878+
alert(`Your change cannot be completed because it would lead to a primary key [${key_name}] duplicate on row ${parseInt(row)+1}! Check:\n\n ${change_log}`);
879+
return false;
880+
};
881+
} // end if of keyVals construction.
882+
} // end forEach of unique_keys
883+
884+
} // end of row in row_change
885+
886+
let triggered_changes = [];
887+
for (const change of grid_changes) {
888+
// When a change in one field triggers a change in another field.
756889
// Check field change rules
757-
self.fieldChangeRules(change, fields, triggered_changes);
890+
self.fieldChangeRules(change, self.slots, triggered_changes);
758891
}
759892
// Add any indirect field changes onto end of existing changes.
760893
if (triggered_changes) {
761-
changes.push(...triggered_changes);
894+
grid_changes.push(...triggered_changes);
762895
}
763896
},
764897
afterSelection: (row, column, row2, column2) => {
765898
if (self.current_selection[0] != row) {
766-
// Row has changed so possibly values that make up some other
767-
// table's foreign key has changed, so that table's view should
768-
// be refreshed. Dependent tables determine what parent foreign
769-
// key values they need to filter view by.
899+
/* ASSUMPTION: only 1st row counts for relevant row in dependent
900+
* table views.
901+
* Row has changed so possibly values that make up some other
902+
* table's foreign key has changed, so that table's view should
903+
* be refreshed. Dependent tables determine what parent foreign
904+
* key values they need to filter view by.
905+
*/
770906
this.context.crudFilterDependentViews(this.template_name);
771907
};
772908

@@ -2163,7 +2299,6 @@ class DataHarmonizer {
21632299
}
21642300

21652301
getInferredIndexSlot() {
2166-
// using the 1-m DH?
21672302
if (this.class_assignment) {
21682303
return this.class_assignment;
21692304
}

0 commit comments

Comments
 (0)