OPEN-715 Column Reordering POC Breakdown
Date: 2026-03-09
Parent:: OPEN-997 New Column Reordering
JIRA:: OPEN-715 | OPEN-997
Friend:: 2026-03-09
Branch: Config/MR/Feature/OPEN-997_NewColumnReordering.INT
Repo: MiX.Config.Frangular.UI
Files changed: package.json, app.module.ts, configgroups.component.html, configgroups.component.ts
ASCII Wireframe β What Changed in the UI
Before (integration β no reordering):
βββββββββββββββββββββββββββββββββββ
β [β] Column Chooser Button β
βββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β COLUMN CHOOSER POPUP β
β ββββββββββββββββββββββββββββββββββ β
β β [β] Vehicle Name β β
β β [β] Registration β β
β β [ ] Driver β β
β β [β] Speed β β
β ββββββββββββββββββββββββββββββββββ β
β (No way to reorder β just show/hide)β
ββββββββββββββββββββββββββββββββββββββββ
GRID: [reorderable]="true" β user can drag columns on the grid directly
After (POC branch β with drag-drop reordering):
βββββββββββββββββββββββββββββββββββ
β [β] Column Chooser Button β
βββββββββββββββββββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββββββ
β COLUMN CHOOSER POPUP β
β ββββββββββββββββββββββββββββββββββ β
β β [β] Vehicle Name β° β ββββ drag handle (cdkDragHandle)
β β [β] Registration β° β β
β β [ ] Driver β° β β
β β [β] Speed β° β β
β ββββββββββββββββββββββββββββββββββ β
β (Drag rows up/down to reorder) β
ββββββββββββββββββββββββββββββββββββββββ
β
βΌ
Drag fires dropConfigGroups()
or dropAssets() handler
β
βΌ
reorderManual() called
β
βββββββ΄ββββββββ
βΌ βΌ
changeColumnOrdering() reorderGridColumnsOnStartup()
(persists to (physically moves the columns
selection criteria) in the Kendo grid)
GRID: [reorderable]="false" β forced to use popup only
Plain English β What Changed and Why
The problem: Users had no way to change the order of columns except by dragging them directly in the grid. The column chooser popup only let you show/hide columns, not reorder them.
What the POC added: Three capabilities, all wired together:
-
Drag handles in the popup β A β° hamburger icon appeared next to each column in the column chooser popup. You could grab it and drag the row up or down within the popup list.
-
Live grid reordering β When you dropped the row in a new position, the gridβs actual columns moved to match. This was done by constructing a fake
ColumnReorderEventand passing it to the existing persistence service. -
Persistence β The new order was saved to the selection criteria (the existing backend mechanism the app already used for remembering column visibility and sort). So when you refreshed or came back later, the order was restored.
The grid was also locked ([reorderable]="false") so users could only reorder through the popup β keeping the two sources of truth in sync.
The key trick was that the drag-drop index in the popup is offset by 1 from the grid column index (because the grid has a hidden first column for checkboxes), so the code always added 1 when translating popup positions to grid positions.
The Moving Parts
| Part | What it does |
|---|---|
@angular/cdk + DragDropModule | Provides the drag-drop capability |
cdkDropList on the popup wrapper | Makes the popup a drag-drop container |
cdkDrag on each row | Makes each column row draggable |
cdkDragHandle on the β° icon | Restricts dragging to only the handle |
trackByField() | Prevents Angular from losing checkbox state when rows move |
dropConfigGroups() / dropAssets() | Catches the drop event, gets old/new indexes |
reorderManual() | Translates popup indexes β grid indexes, builds fake event, calls persist |
changeColumnOrdering() (existing service) | Saves new order to selection criteria |
reorderGridColumnsOnStartup() (existing service) | Applies persisted order to the Kendo grid |
setupConfigGroupsGrid() / setupConfigAssetsGrid() | Rebuilds the popup list to reflect new order |
The Code β Section by Section
1. New dependency added
// package.json
// Angular CDK (Component Dev Kit) β provides drag-drop primitives
"@angular/cdk": "^12.2.13",2. DragDropModule wired into Angular
// app.module.ts
// Import the CDK drag-drop module so it's available in templates
import { DragDropModule } from '@angular/cdk/drag-drop';
@NgModule({
imports: [
// ...existing imports...
DragDropModule // β added to NgModule
]
})3. HTML β Config Groups popup (column chooser gets drag-drop)
<!-- configgroups.component.html -->
<!--
The popup wrapper div becomes a drop ZONE.
(cdkDropListDropped) fires when the user releases a dragged item.
"column-chooser-list" is a CSS class for styling.
-->
<div cdkDropList (cdkDropListDropped)="dropConfigGroups($event)" class="column-chooser-list">
<!--
Each row is now a draggable item (cdkDrag).
trackBy: trackByField prevents Angular from losing checkbox state on re-render.
-->
<div class="wrap d-flex align-items-center"
*ngFor="let item of configGroupsColumnsOrdered; let i = index; trackBy: trackByField"
role="menuitem"
cdkDrag>
<input type="checkbox" id="{{item?.field}}" class="k-checkbox"
[checked]="!item?.hidden"
[disabled]="item?.locked"
(click)="columnVisibilityChanged(item)" />
<label class="k-checkbox-label flex-grow-1 mb-0"
for="{{item?.field}}"
(click)="$event.stopPropagation()">{{item?.title|dmxTranslate}}</label>
<!--
The grab handle (β° hamburger). cdkDragHandle restricts dragging to this icon only.
Locked columns get a 'disabled' CSS class so they appear greyed out.
-->
<div class="ml-2">
<span class="column-chooser-drag-handle hand-cursor"
cdkDragHandle
[class.disabled]="item?.locked"
style="font-size: 18px; color: #888;">β°</span>
</div>
</div>
</div>4. HTML β Config Assets popup (same pattern, Assets grid)
<!-- configgroups.component.html β the assets panel column chooser -->
<!-- Same drag-drop structure for the assets grid column chooser -->
<div cdkDropList (cdkDropListDropped)="dropAssets($event)"
class="column-chooser-list popup-column-chooser-scroll"
style="white-space: nowrap;">
<div class="wrap d-flex align-items-center"
*ngFor="let item of configAssetsColumnsOrdered; let i = index; trackBy: trackByField"
role="menuitem"
cdkDrag>
<input type="checkbox" id="{{item?.field}}" class="k-checkbox"
[checked]="!item?.hidden"
[disabled]="item?.locked"
(click)="assetsColumnVisibilityChanged(item)" />
<label class="k-checkbox-label flex-grow-1 mb-0"
for="{{item?.field}}"
(click)="$event.stopPropagation()">
{{item?.title|dmxTranslate}}
</label>
<div class="ml-2 mr-2">
<span class="column-chooser-drag-handle hand-cursor"
cdkDragHandle
[class.disabled]="item?.locked"
style="font-size: 18px; color: #888;">β°</span>
</div>
</div>
</div>5. HTML β Grids locked to popup-only reordering
<!-- Config Groups grid: reorderable="false" β user can no longer drag columns directly on the grid -->
<kendo-grid #configGroupsGrid
[reorderable]="false"
...>
<!-- Assets grid: same β forces all reordering through the popup -->
<kendo-grid #assetsGrid
[reorderable]="false"
...>6. TypeScript β Imports and ViewChild
// configgroups.component.ts
// CDK drag-drop types needed for the drop handler
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
// Need a reference to the config groups grid so we can programmatically reorder its columns
@ViewChild("configGroupsGrid", { static: false }) configGroupsGrid: GridComponent;
// (assetsGrid ViewChild already existed)7. TypeScript β Drop handler for Config Groups
// Fires when the user drops a dragged row in the popup.
// event.previousIndex = where it came from, event.currentIndex = where it landed.
dropConfigGroups(event: CdkDragDrop<any[]>): void {
if (event.previousIndex === event.currentIndex) return; // nothing moved
// Build the new order for the popup list
const newOrder = [...this.configGroupsColumnsOrdered];
moveItemInArray(newOrder, event.previousIndex, event.currentIndex);
// Persist + move the actual grid columns
this.reorderManual(
this.configGroupsColumnsOrdered,
event.previousIndex,
event.currentIndex,
'configGroups'
);
// Update the popup list reference
this.configGroupsColumnsOrdered = newOrder;
}8. TypeScript β Drop handler for Assets
// Same pattern as dropConfigGroups, but for the assets grid
dropAssets(event: CdkDragDrop<any[]>): void {
if (event.previousIndex === event.currentIndex) return;
const newOrder = [...this.configAssetsColumnsOrdered];
moveItemInArray(newOrder, event.previousIndex, event.currentIndex);
this.reorderManual(
this.configAssetsColumnsOrdered,
event.previousIndex,
event.currentIndex,
'assets'
);
this.configAssetsColumnsOrdered = newOrder;
}9. TypeScript β trackByField (keeps checkboxes stable)
// Without trackBy, Angular destroys and recreates each DOM row when the array changes.
// This would cause checkboxes to lose their state mid-drag.
// By tracking by field name, Angular reuses the existing DOM elements.
trackByField(index: number, item: any): string {
return item.field;
}10. TypeScript β The core reorderManual logic
// Called by both the drag-drop handlers AND the up/down chevron buttons (earlier POC version).
// popupOldIndex / popupNewIndex = positions in the popup list (0-based)
// Grid columns are offset by +1 because index 0 in the grid is the hidden checkbox column.
private reorderManual(columns: IColumn[], popupOldIndex: number, popupNewIndex: number, gridType: string) {
const item = columns[popupOldIndex];
if (item.locked) return; // locked columns can't be moved
// Translate popup index β grid column index (offset by 1 for the checkbox column)
const gridOldIndex = popupOldIndex + 1;
const gridNewIndex = popupNewIndex + 1;
// Good way to debug indexes:
// console.log(`Moving ${item.field} from ${gridOldIndex} to ${gridNewIndex}`);
// Build a fake ColumnReorderEvent β the existing persistence service expects this format
const event = new ColumnReorderEvent({
column: { field: item.field } as any,
newIndex: gridNewIndex,
oldIndex: gridOldIndex
});
if (gridType === 'configGroups') {
// 1. Persist the new order to selection criteria
this.gridSelectionCriteriaService.changeColumnOrdering(
SelectionCriteriaKeys.configGroupsColumnSettings,
this.configGroupsColumnSettings,
event, 1, this.configGroupsColumns.length
);
// 2. Physically reorder the Kendo grid columns to match
this.gridSelectionCriteriaService.reorderGridColumnsOnStartup(
this.configGroupsGrid, this.configGroupsColumnSettings
);
} else {
this.gridSelectionCriteriaService.changeColumnOrdering(
SelectionCriteriaKeys.assetsColumnSettings,
this.assetsColumnSettings,
event, 1, this.assetsColumns.length
);
this.gridSelectionCriteriaService.reorderGridColumnsOnStartup(
this.assetsGrid, this.assetsColumnSettings
);
}
// Rebuild the popup list so it reflects the new order
if (gridType === 'configGroups') {
this.setupConfigGroupsGrid();
} else {
this.setupConfigAssetsGrid();
}
}11. TypeScript β Reapply saved order on load
// After fetching column settings from the backend on page load,
// apply the previously persisted column order to the Kendo grid.
// Without this, the grid always starts in default order even if the user had reordered it.
// Config Groups:
this.configGroupsHiddenColumns = gridColumnData.hiddenColumns;
this.setupConfigGroupsGrid();
if (this.configGroupsGrid) {
this.gridSelectionCriteriaService.reorderGridColumnsOnStartup(
this.configGroupsGrid, this.configGroupsColumnSettings
);
}
// Assets:
this.configAssetsHiddenColumns = gridColumnData.hiddenColumns;
this.setupConfigAssetsGrid();
if (this.assetsGrid) {
this.gridSelectionCriteriaService.reorderGridColumnsOnStartup(
this.assetsGrid, this.assetsColumnSettings
);
}12. TypeScript β setupConfigGroupsGrid / setupConfigAssetsGrid
// When rebuilding the popup list, filter out undefined entries
// and maintain the grid's actual current column order (don't sort alphabetically).
private setupConfigGroupsGrid() {
this.configGroupsColumnsOrdered = [];
this.configGroupsColumnsOrdered.push(...this.configGroupsColumns);
// Remove any undefined slots (can appear when columns are dynamically added/removed)
this.configGroupsColumnsOrdered = this.configGroupsColumnsOrdered.filter(c => c !== undefined);
// NOTE: Shawn's recommendation was to NOT sort alphabetically here,
// so that the popup reflects the actual current grid column order.
// The old alphabetical sort was commented out.
}Summary of All Changed Files
package.json β added @angular/cdk
app.module.ts β imported DragDropModule
configgroups.component.html β added cdkDropList/cdkDrag/cdkDragHandle to both
column chooser popups; set reorderable=false on grids
configgroups.component.ts β added CdkDragDrop import, ViewChild for configGroupsGrid,
dropConfigGroups(), dropAssets(), trackByField(),
moveColumnUp/Down(), reorderManual(),
reorderGridColumnsOnStartup() calls on load,
filter(undefined) in setupGrid methods