Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions src/app/core/_components/tables/ht-table/ht-table.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, SortDirection } from '@angular/material/sort';

import { BaseModel } from '@models/base.model';
import { TableSettingsKey, UIConfig } from '@models/config-ui.model';
import { Sorting, TableSettingsKey, UIConfig } from '@models/config-ui.model';
import { JHash } from '@models/hash.model';

import { ContextMenuService } from '@services/context-menu/base/context-menu.service';
Expand Down Expand Up @@ -386,10 +386,13 @@ export class HTTableComponent implements OnInit, AfterViewInit, OnDestroy {
* @param {any} column - The column that was clicked for sorting.
* @returns {void}
*/
onColumnHeaderClick(column: any): void {
const sorting = {
...column,
direction: this.dataSource.sort['_direction']
onColumnHeaderClick(column: HTTableColumn): void {
const sorting: Sorting = {
id: column.id,
dataKey: column.dataKey,
isSortable: column.isSortable,
direction: this.dataSource.sort['_direction'],
...(column.parent ? { parent: column.parent } : {})
};
this.dataSource.sortingColumn = sorting;
if (!this.isDetailPage) {
Expand Down
3 changes: 2 additions & 1 deletion src/app/core/_models/config-ui.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export interface Sorting {
id: number;
dataKey: string;
isSortable: boolean;
direction: 'asc' | 'desc';
direction: 'asc' | 'desc' | '';
parent?: string;
}

const _uiConfigDefault = {
Expand Down
108 changes: 107 additions & 1 deletion src/app/core/_models/config-ui.schema.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { uiConfigDefault } from '@models/config-ui.model';
import { uiConfigSchema, uisSettingsSchema } from '@models/config-ui.schema';
import { sortingSchema, uiConfigSchema, uisSettingsSchema } from '@models/config-ui.schema';

describe('uiConfigSchema', () => {
// Use a deep copy to avoid interference from other tests that mutate the
Expand Down Expand Up @@ -144,6 +144,28 @@ describe('uiConfigSchema', () => {
}
});

it('should preserve the parent field in sorting order (relationship sort)', () => {
const config = {
...defaults,
tableSettings: {
tasksTable: {
columns: [1, 2],
page: 25,
search: '',
order: { id: 2, dataKey: 'taskName', isSortable: true, direction: 'asc', parent: 'task' }
}
}
};

const result = uiConfigSchema.safeParse(config);
expect(result.success).toBeTrue();
if (result.success) {
const order = result.data.tableSettings['tasksTable']['order'];
expect(order).toBeDefined();
expect(order['parent']).toBe('task');
}
});

it('should coerce string numbers to actual numbers in columns', () => {
const config = {
...defaults,
Expand Down Expand Up @@ -326,3 +348,87 @@ describe('uisSettingsSchema', () => {
}
});
});

/**
* Replicates the sort parameter construction from
* RequestParamBuilder.addSorting (builder-implementation.service.ts).
*/
function buildSortParam(col: { dataKey: string; direction: string; parent?: string }): string {
const direction = col.direction === 'asc' ? '' : '-';
const parent = col.parent ? `${col.parent}.` : '';
return `${direction}${parent}${col.dataKey}`;
}

describe('sortingSchema – sort parameter round-trip', () => {
/**
* Every sortable column across all tables. Columns with a `parent` field
* produce a relationship sort like "task.taskName"; the rest produce a
* simple sort like "priority".
*/
const sortableColumns: { table: string; dataKey: string; id: number; parent?: string }[] = [
// tasks-table (the only table with parent-based sort columns)
{ table: 'tasks', dataKey: 'taskWrapperId', id: 0 },
{ table: 'tasks', dataKey: 'taskType', id: 1 },
{ table: 'tasks', dataKey: 'taskName', id: 2, parent: 'task' },
{ table: 'tasks', dataKey: 'hashlistId', id: 5, parent: 'hashlist' },
{ table: 'tasks', dataKey: 'cracked', id: 7 },
{ table: 'tasks', dataKey: 'groupName', id: 9, parent: 'accessGroup' },
{ table: 'tasks', dataKey: 'isSmall', id: 10, parent: 'task' },
{ table: 'tasks', dataKey: 'isCpuTask', id: 11, parent: 'task' },
{ table: 'tasks', dataKey: 'priority', id: 12 },
{ table: 'tasks', dataKey: 'maxAgents', id: 13 },
// Representative columns from other tables (no parent)
{ table: 'agents', dataKey: 'id', id: 0 },
{ table: 'agents', dataKey: 'agentName', id: 1 },
{ table: 'users', dataKey: 'name', id: 1 },
{ table: 'hashlists', dataKey: 'id', id: 0 },
{ table: 'files', dataKey: 'filename', id: 1 },
{ table: 'cracks', dataKey: 'timeCracked', id: 0 },
];

for (const direction of ['asc', 'desc'] as const) {
for (const col of sortableColumns) {
const label = col.parent ? `${col.parent}.${col.dataKey}` : col.dataKey;

it(`[${col.table}] sort=${direction === 'desc' ? '-' : ''}${label} should survive schema round-trip`, () => {
// 1. Build the sorting object as onColumnHeaderClick would
const initial = {
id: col.id,
dataKey: col.dataKey,
isSortable: true as const,
direction,
...(col.parent ? { parent: col.parent } : {})
};

// 2. Round-trip through sortingSchema (simulates localStorage save + restore)
const result = sortingSchema.safeParse(initial);
expect(result.success).toBeTrue();

// 3. The sort parameter sent to the API must be identical
const expected = buildSortParam(initial);
const actual = buildSortParam(result.data as typeof initial);
expect(actual).toBe(expected);
});
}
}

it('should strip unknown fields on the sorting object', () => {
const withExtra = {
id: 1,
dataKey: 'taskName',
isSortable: true,
direction: 'asc' as const,
parent: 'task',
render: 'should not survive',
routerLink: 'should not survive'
};

const result = sortingSchema.safeParse(withExtra);
expect(result.success).toBeTrue();
if (result.success) {
expect(result.data['render']).toBeUndefined();
expect(result.data['routerLink']).toBeUndefined();
expect(result.data.parent).toBe('task');
}
});
});
3 changes: 2 additions & 1 deletion src/app/core/_models/config-ui.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ export const sortingSchema = z.object({
id: z.coerce.number(),
dataKey: z.string(),
isSortable: z.boolean(),
direction: z.enum(['asc', 'desc'])
direction: z.enum(['asc', 'desc', '']),
parent: z.string().optional()
});

/**
Expand Down
13 changes: 3 additions & 10 deletions src/app/shared/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,10 @@
import { TableSettingsKey, UIConfig, UIConfigKeys, uiConfigDefault } from '@models/config-ui.model';
import { Sorting, TableSettingsKey, UIConfig, UIConfigKeys, uiConfigDefault } from '@models/config-ui.model';
import { uiConfigSchema } from '@models/config-ui.schema';

import { LocalStorageService } from '@services/storage/local-storage.service';

import { ThemeService } from '@src/app/core/_services/shared/theme.service';

export interface TableOrder {
id: number;
dataKey: string;
isSortable: boolean;
direction: 'asc' | 'desc';
}

/**
* Utility class for managing user interface settings and configurations.
*/
Expand Down Expand Up @@ -46,7 +39,7 @@ export class UISettingsUtilityClass {
* @param {number} [settings.start] - The start index to set.
* @param {number[]} [settings.columns] - An array of column numbers to set.
* @param {number[]} [settings.search] - An array of column numbers to set.
* @param {TableOrder[]} [settings.order] - An array defining the order of columns.
* @param {Sorting[]} [settings.order] - An array defining the order of columns.
*/
updateTableSettings(
key: TableSettingsKey,
Expand All @@ -57,7 +50,7 @@ export class UISettingsUtilityClass {
before?: number;
totalItems?: number;
columns?: number[];
order?: TableOrder[];
order?: Sorting | Sorting[];
search?: string;
}
): void {
Expand Down
Loading