Skip to content

Commit 0e90544

Browse files
committed
dragging multiple rows change physical data layer
1 parent 87d87fc commit 0e90544

1 file changed

Lines changed: 130 additions & 37 deletions

File tree

lib/DataHarmonizer.js

Lines changed: 130 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -261,36 +261,60 @@ class DataHarmonizer {
261261
remove_row: {
262262
name: 'Remove row',
263263
callback() {
264-
// Enables removal of a row and all dependent table rows.
265-
// If there are 1-many cascading deletes, verify if that's ok.
266-
let selection = self.hot.getSelected()[0][0];
267-
let [change_report, change_message] = self.getChangeReport(self.template_name);
268-
if (!change_message.length) {
269-
self.hot.alter('remove_row', selection);
270-
return true;
264+
// Collect every selected visual row from all selection ranges
265+
// (user may have Ctrl-clicked non-contiguous rows or dragged a block).
266+
const selectedRanges = self.hot.getSelected() || [];
267+
const visualRowSet = new Set();
268+
for (const [r1, , r2] of selectedRanges) {
269+
const lo = Math.min(r1, r2);
270+
const hi = Math.max(r1, r2);
271+
for (let r = lo; r <= hi; r++) visualRowSet.add(r);
271272
}
272-
/*
273-
* For deletes: (For now, ignore duplicate root key case: If
274-
* encountering foreign key involving root_class slot, test if that has
275-
* > 1 row. If so, delete ok without examining other dependents.)
276-
*/
277-
278-
// Some cascading deletes to confirm here.
279-
if (
280-
confirm(
281-
'WARNING: If you proceed, this will include deletion of one\n or more dependent records, and this cannot be undone:\n' +
282-
change_message
283-
)
284-
) {
285-
// User has seen the warning and has confirmed ok to proceed.
286-
for (let [dependent_name, dependent_obj] of Object.entries(change_report)) {
287-
if (dependent_obj.rows?.length) {
288-
// rows are physical (source) indices. Delete via source-data reload
289-
// so hidden rows are also removed without needing toVisualRow().
290-
self.context.crudDeleteRowsByPhysical(dependent_name, dependent_obj.rows);
273+
const visualRows = [...visualRowSet].sort((a, b) => a - b);
274+
const rowsToDelete = [];
275+
276+
for (let i = 0; i < visualRows.length; i++) {
277+
const vRow = visualRows[i];
278+
279+
// Point HOT's selection at this row so crudGetDependentKeyVals
280+
// reads its primary-key values when building the change report.
281+
self.hot.selectCell(vRow, 0, vRow, 0, false);
282+
const [change_report, change_message] = self.getChangeReport(self.template_name);
283+
284+
if (!change_message.length) {
285+
// No cascading dependents — safe to delete without prompting.
286+
rowsToDelete.push(vRow);
287+
} else {
288+
/*
289+
* For deletes: (For now, ignore duplicate root key case: If
290+
* encountering foreign key involving root_class slot, test if
291+
* that has > 1 row. If so, delete ok without examining other
292+
* dependents.)
293+
*/
294+
const rowLabel = visualRows.length > 1
295+
? `row ${i + 1} of ${visualRows.length}`
296+
: 'this row';
297+
if (confirm(
298+
`WARNING: Deleting ${rowLabel} will also delete dependent records and cannot be undone:\n${change_message}`
299+
)) {
300+
// rows are physical (source) indices. Delete via source-data
301+
// reload so hidden rows are also removed without toVisualRow().
302+
for (const [dependent_name, dependent_obj] of Object.entries(change_report)) {
303+
if (dependent_obj.rows?.length) {
304+
self.context.crudDeleteRowsByPhysical(dependent_name, dependent_obj.rows);
305+
}
306+
}
307+
rowsToDelete.push(vRow);
291308
}
292-
};
293-
self.hot.alter('remove_row', selection);
309+
// If the user cancels this row it is skipped; remaining rows
310+
// in the selection continue to be processed.
311+
}
312+
}
313+
314+
// Delete confirmed primary rows in descending visual order so
315+
// earlier indices are not shifted by later removals.
316+
for (const vRow of rowsToDelete.sort((a, b) => b - a)) {
317+
self.hot.alter('remove_row', vRow);
294318
}
295319
},
296320
},
@@ -379,7 +403,7 @@ class DataHarmonizer {
379403
// Hide if no locales
380404
const current_row = schema.current_selection[0];
381405
if (current_row === null || current_row === undefined || current_row < 0)
382-
return false;
406+
return true;
383407
const locales = schema.hot.getCellMeta(current_row, 0).locales;
384408
return !locales;
385409
},
@@ -688,7 +712,11 @@ class DataHarmonizer {
688712
if (Object.keys(self.invalid_cells).length === 0) {
689713
$(`#tab-bar-${self.template_name} > a`).removeClass('tab-has-errors');
690714
$('#next-error-button').hide();
691-
$('#no-error-button').show().delay(5000).fadeOut('slow');
715+
// Only show "No errors" if the user actually fixed a previously
716+
// invalid cell — not when editing a cell that was already valid.
717+
if (prevInvalidChangedCells.length > 0) {
718+
$('#no-error-button').show().delay(5000).fadeOut('slow');
719+
}
692720
}
693721
// Track cells that became valid (for picker/dropdown Tab navigation in beforeKeyDown).
694722
const firstFixed = prevInvalidChangedCells.find(
@@ -838,6 +866,24 @@ class DataHarmonizer {
838866
}
839867
},
840868

869+
// Commit drag-and-drop row reordering into the underlying source data.
870+
// manualRowMove only updates the visual→physical index mapping; calling
871+
// loadData with the rows read in visual order resets the mapper so
872+
// physical order matches what the user sees.
873+
afterRowMove: (movedRows, finalIndex, dropIndex, movePossible, orderChanged) => {
874+
if (!orderChanged) return;
875+
const count = self.hot.countSourceRows();
876+
const reordered = [];
877+
for (let vRow = 0; vRow < count; vRow++) {
878+
const physRow = self.hot.toPhysicalRow(vRow);
879+
if (physRow !== null) {
880+
reordered.push([...self.hot.getSourceDataAtRow(physRow)]);
881+
}
882+
}
883+
self.invalid_cells = {}; // physical row indices will reset; stale cache must be cleared
884+
self.hot.loadData(reordered);
885+
},
886+
841887
// Reposition the validation status bar tooltip when the table scrolls
842888
// so it stays aligned with the focused cell even after a column shift.
843889
afterScrollHorizontally: () => self._updateStatusBar(),
@@ -1718,6 +1764,16 @@ class DataHarmonizer {
17181764
}
17191765

17201766
switch (slot.datatype) {
1767+
case 'xsd:boolean':
1768+
// Only use native checkbox for pure boolean fields. Fields that also
1769+
// have vocabulary sources (e.g. NullValueField mixed with boolean)
1770+
// remain key-value-list so their picklist values are preserved.
1771+
if (!slot.sources) {
1772+
col.type = 'checkbox';
1773+
col.checkedTemplate = true;
1774+
col.uncheckedTemplate = false;
1775+
}
1776+
break;
17211777
case 'xsd:date':
17221778
col.type = 'dh.date';
17231779
col.allowInvalid = true; // making a difference?
@@ -2533,6 +2589,21 @@ class DataHarmonizer {
25332589
*/
25342590
matrixFieldChangeRules(matrix) {
25352591
Object.entries(this.slots).forEach((field, col) => {
2592+
// Convert string boolean values to native JS booleans for xsd:boolean
2593+
// checkbox cells. Fields that also have vocabulary sources (e.g. NullValueField)
2594+
// remain key-value-list and store string values instead.
2595+
if (this.slots[col].datatype === 'xsd:boolean' && !this.slots[col].sources) {
2596+
for (let row = 0; row < matrix.length; row++) {
2597+
const val = matrix[row][col];
2598+
if (val === null || val === undefined || val === '') continue;
2599+
if (typeof val === 'string') {
2600+
const lower = val.toLowerCase();
2601+
if (lower === 'true' || lower === '1') matrix[row][col] = true;
2602+
else if (lower === 'false' || lower === '0') matrix[row][col] = false;
2603+
}
2604+
}
2605+
}
2606+
25362607
// Test field against capitalization change.
25372608
if (field.capitalize) {
25382609
for (let row = 0; row < matrix.length; row++) {
@@ -2785,6 +2856,21 @@ class DataHarmonizer {
27852856
cellChanges.push([row, col, cellVal, 'prevalidation']);
27862857
}
27872858

2859+
// Normalise string booleans to JS booleans for checkbox cells only.
2860+
// Skip fields with vocabulary sources (e.g. NullValueField); those
2861+
// remain as strings handled by validateValAgainstVocab below.
2862+
if (datatype === 'xsd:boolean' && !field.sources && typeof cellVal === 'string' && cellVal !== '') {
2863+
const lower = cellVal.toLowerCase();
2864+
let canonical = null;
2865+
if (lower === 'true' || lower === '1') canonical = true;
2866+
else if (lower === 'false' || lower === '0') canonical = false;
2867+
if (canonical !== null) {
2868+
cellVal = canonical;
2869+
data[row][col] = cellVal;
2870+
cellChanges.push([row, col, canonical, 'prevalidation']);
2871+
}
2872+
}
2873+
27882874
if (cellVal && datatype === 'xsd:token' && typeof cellVal === 'string') {
27892875
// console.log("valdation", cellVal)
27902876
const minimized = cellVal
@@ -2829,14 +2915,21 @@ class DataHarmonizer {
28292915
}
28302916
}
28312917
if (cellChanges.length) {
2832-
console.log(
2833-
'doPreValidationRepairs: setDataAtCell and afterChange Hook'
2834-
);
2835-
this.hot.addHookOnce('afterChange', resolve);
2836-
this.hot.setDataAtCell(cellChanges, 'validation');
2837-
} else {
2838-
resolve();
2918+
// Update HOT source data directly to avoid setDataAtCell's internal
2919+
// rendering machinery (done/getCell callbacks), which crashes for HOT
2920+
// instances in hidden tabs whose viewport calculator has never been
2921+
// initialized. getSourceData() returns the internal reference, so
2922+
// modifying it updates HOT's state without triggering rendering hooks.
2923+
const sourceData = this.hot.getSourceData();
2924+
for (const [vRow, col, val] of cellChanges) {
2925+
const pRow = this.hot.toPhysicalRow(vRow);
2926+
if (pRow !== null && sourceData[pRow]) {
2927+
sourceData[pRow][col] = val;
2928+
}
2929+
}
2930+
this.hot.render();
28392931
}
2932+
resolve();
28402933
});
28412934
}
28422935

0 commit comments

Comments
 (0)