diff --git a/res/app/device-list/device-list-details-directive.js b/res/app/device-list/device-list-details-directive.js index d649a49d..8ed0b901 100644 --- a/res/app/device-list/device-list-details-directive.js +++ b/res/app/device-list/device-list-details-directive.js @@ -1,3 +1,5 @@ +var patchArray = require('./util/patch-array') + var directive = module.exports = function DeviceListDetailsDirective( $filter , gettext @@ -216,7 +218,7 @@ var directive = module.exports = function DeviceListDetailsDirective( return scope.sort } , function(newValue) { - activeSorting = scope.sort.fixed.concat(scope.sort.user) + activeSorting = newValue.fixed.concat(newValue.user) scope.sortedColumns = Object.create(null) activeSorting.forEach(function(sort) { scope.sortedColumns[sort.name] = sort @@ -245,54 +247,18 @@ var directive = module.exports = function DeviceListDetailsDirective( // the fastest because it shouldn't get called all the time. function updateColumns(columnSettings) { var newActiveColumns = [] - , newMapping = Object.create(null) - , oldMapping = Object.create(null) - , patch = [] - , i - , l - // First, check what we are supposed to have now - for (i = 0, l = columnSettings.length; i < l; ++i) { - var columnSetting = columnSettings[i] - if (columnSetting.selected) { - newActiveColumns.push(columnSetting.name) - newMapping[columnSetting.name] = true + // Check what we're supposed to show now + columnSettings.forEach(function(column) { + if (column.selected) { + newActiveColumns.push(column.name) } - } + }) - // Check what we need to remove from the currently active columns - var removeAdjust = 0 - for (i = 0, l = activeColumns.length; i < l; ++i) { - var oldColumn = activeColumns[i] - // Fix index so that they make sense when the operations are - // run in order. - oldMapping[oldColumn] = true - if (!newMapping[oldColumn]) { - patch.push({ - type: 'remove' - , column: oldColumn - , index: i - removeAdjust - }) - removeAdjust += 1 - } - } - - // Check what we need to add - var insertAdjust = 0 - for (i = 0, l = newActiveColumns.length; i < l; ++i) { - var newColumn = newActiveColumns[i] - if (!oldMapping[newColumn]) { - patch.push({ - type: 'insert' - , column: newColumn - , index: i + insertAdjust - }) - insertAdjust += 1 - } - } - - // @TODO move operations + // Figure out the patch + var patch = patchArray(activeColumns, newActiveColumns) + // Set up new active columns activeColumns = newActiveColumns return patchAll(patch) @@ -361,13 +327,17 @@ var directive = module.exports = function DeviceListDetailsDirective( function patchRow(tr, device, patch) { for (var i = 0, l = patch.length; i < l; ++i) { var op = patch[i] - switch (op.type) { + switch (op[0]) { case 'insert': - var col = scope.columnDefinitions[op.column] - tr.insertBefore(col.update(col.build(), device), tr.cells[op.index]) + var col = scope.columnDefinitions[op[2]] + tr.insertBefore(col.update(col.build(), device), tr.cells[op[1]]) break case 'remove': - tr.deleteCell(op.index) + tr.deleteCell(op[1]) + break + case 'swap': + tr.insertBefore(tr.cells[op[1]], tr.cells[op[2]]) + tr.insertBefore(tr.cells[op[2]], tr.cells[op[1]]) break } } diff --git a/res/app/device-list/device-list.jade b/res/app/device-list/device-list.jade index 89fe91b8..79fc25fa 100644 --- a/res/app/device-list/device-list.jade +++ b/res/app/device-list/device-list.jade @@ -28,10 +28,9 @@ div.stf-device-list .col-md-12 .widget-container.fluid-height .widget-content.padded - label(ng-repeat='column in columns') + label(ng-repeat='column in columns track by column.name') input(type='checkbox', ng-model='column.selected') span {{ column.title | translate }} - button(ng-click='columns.push(columns.shift())') swap ends device-list-details(tracker='tracker', columns='columns', sort='sort') //tabset.overflow-auto(ng-if='activeTabs').device-list-active-tabs tab(active='activeTabs.devices') diff --git a/res/app/device-list/util/patch-array-test.js b/res/app/device-list/util/patch-array-test.js new file mode 100644 index 00000000..751c75cb --- /dev/null +++ b/res/app/device-list/util/patch-array-test.js @@ -0,0 +1,127 @@ +var assert = require('assert') + +var patchArray = require('./patch-array') + +var tests = [ + { + a: ['a', 'b', 'c', 'd', 'e'] + , b: ['a', 'e', 'c', 'd', 'b'] + , ops: [ + ['swap', 4, 1] + ] + } +, { + a: ['a', 'b', 'c', 'd', 'e'] + , b: ['e', 'd', 'c', 'b', 'a'] + , ops: [ + ['swap', 4, 0] + , ['swap', 3, 1] + ] + } +, { + a: ['a', 'b', 'c', 'd', 'e', 'f'] + , b: ['f', 'e', 'd', 'c', 'b', 'a'] + , ops: [ + ['swap', 5, 0] + , ['swap', 4, 1] + , ['swap', 3, 2] + ] + } +, { + a: ['a', 'b', 'c', 'd', 'e', 'f'] + , b: ['f', 'e'] + , ops: [ + ['remove', 0] + , ['remove', 0] + , ['remove', 0] + , ['remove', 0] + , ['swap', 1, 0] + ] + } +, { + a: ['a', 'b', 'c', 'd', 'e', 'f'] + , b: ['f', 'a'] + , ops: [ + ['remove', 1] + , ['remove', 1] + , ['remove', 1] + , ['remove', 1] + , ['swap', 1, 0] + ] + } +, { + a: [] + , b: ['a', 'b', 'c', 'd', 'e', 'f'] + , ops: [ + ['insert', 0, 'a'] + , ['insert', 1, 'b'] + , ['insert', 2, 'c'] + , ['insert', 3, 'd'] + , ['insert', 4, 'e'] + , ['insert', 5, 'f'] + ] + } +, { + a: ['a', 'd'] + , b: ['a', 'b', 'c', 'd', 'e', 'f'] + , ops: [ + ['insert', 1, 'b'] + , ['insert', 2, 'c'] + , ['insert', 4, 'e'] + , ['insert', 5, 'f'] + ] + } +, { + a: ['b', 'd', 'a'] + , b: ['a', 'b', 'c', 'd', 'e', 'f'] + , ops: [ + ['swap', 2, 0] + , ['swap', 2, 1] + , ['insert', 2, 'c'] + , ['insert', 4, 'e'] + , ['insert', 5, 'f'] + ] + } +, { + a: ["state","model","name","operator","releasedAt","version","network","provider","owner"] + , b: ["owner","model","name","operator","releasedAt","version","network","provider","state"] + } +] + +function verify(a, b, ops) { + var c = [].concat(a) + ops.forEach(function(op) { + switch (op[0]) { + case 'swap': + var temp = c[op[1]] + c[op[1]] = c[op[2]] + c[op[2]] = temp + break + case 'move': + c.splice(op[2] + 1, 0, c[op[1]]) + c.splice(op[1], 1) + break + case 'insert': + c.splice(op[1], 0, op[2]) + break + case 'remove': + c.splice(op[1], 1) + break + default: + throw new Error('Unknown op ' + op[0]) + } + }) + assert.deepEqual(c, b) +} + +tests.forEach(function(test) { + console.log('Running test:') + console.log(' <- ', test.a) + console.log(' -> ', test.b) + console.log('Verifying test expectations') + //verify(test.a, test.b, test.ops) + console.log('Verifying patchArray') + var patch = patchArray(test.a, test.b) + console.log(' patch', patch) + verify(test.a, test.b, patch) +}) diff --git a/res/app/device-list/util/patch-array.js b/res/app/device-list/util/patch-array.js new file mode 100644 index 00000000..d72e81ff --- /dev/null +++ b/res/app/device-list/util/patch-array.js @@ -0,0 +1,71 @@ +function existenceMap(array) { + var map = Object.create(null) + for (var i = 0, l = array.length; i < l; ++i) { + map[array[i]] = 1 + } + return map +} + +// Returns a list of operations to transform array a into array b. May not +// return the optimal set of operations. +module.exports = function patchArray(a, b) { + var ops = [] + + var workA = [].concat(a) + , inA = Object.create(null) + , itemA + , cursorA + + var inB = existenceMap(b) + , posB = Object.create(null) + , itemB + , cursorB + + // First, check what was removed from a. + for (cursorA = 0; cursorA < workA.length;) { + itemA = workA[cursorA] + + // If b does not contain the item, surely it must have been removed. + if (!inB[itemA]) { + workA.splice(cursorA, 1) + ops.push(['remove', cursorA]) + } + // Otherwise, the item is still alive. + else { + inA[itemA] = true + cursorA += 1 + } + } + + // Then, check what was inserted into b. + for (cursorB = 0; cursorB < b.length; ++cursorB) { + itemB = b[cursorB] + + // If a does not contain the item, it must have been added. + if (!inA[itemB]) { + workA.splice(cursorB, 0, itemB) + ops.push(['insert', cursorB, itemB]) + } + + posB[itemB] = cursorB + } + + // At this point, workA contains the same items as b, but swaps may + // be needed. + for (cursorA = 0; cursorA < workA.length;) { + itemA = workA[cursorA] + var posInB = posB[itemA] + + if (posInB === cursorA) { + cursorA += 1 + } + else { + var temp = workA[posInB] + workA[posInB] = itemA + workA[cursorA] = temp + ops.push(['swap', cursorA, posInB]) + } + } + + return ops +}