@quazardous/qdadm 2.4.2 → 2.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { QueryExecutor, type QueryObject } from '../query/QueryExecutor'
|
|
2
2
|
import type { ListParams, ListResult } from '../types'
|
|
3
3
|
import type { EntityManagerInternal, OperationStats, QueryOptions } from './EntityManager.types'
|
|
4
|
+
import { nullSortRank, type NullSortMode } from '../query/clientFilter'
|
|
4
5
|
|
|
5
6
|
type Self = EntityManagerInternal<any>
|
|
6
7
|
|
|
@@ -172,17 +173,22 @@ export function applyQueryMethods(EntityManagerClass: { prototype: any }): void
|
|
|
172
173
|
// may still BE cache.items — an in-place sort would mutate the cache,
|
|
173
174
|
// making later unsorted reads return whatever order the last sort left.
|
|
174
175
|
if (sort_by) {
|
|
176
|
+
// Resolve null placement: field config > manager option > 'last'
|
|
177
|
+
const fieldCfg = this.getFieldConfig?.(sort_by) as { nullSort?: NullSortMode } | null
|
|
178
|
+
const nullMode: NullSortMode = fieldCfg?.nullSort ?? this.nullSort ?? 'last'
|
|
179
|
+
const nullCmp = nullSortRank(nullMode, sort_order === 'asc' ? 'asc' : 'desc')
|
|
175
180
|
result = [...result]
|
|
176
181
|
result.sort((a: any, b: any) => {
|
|
177
182
|
const aVal = a[sort_by]
|
|
178
183
|
const bVal = b[sort_by]
|
|
179
184
|
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
185
|
+
// Null placement is configurable per field/manager (#1222 —
|
|
186
|
+
// depends on the API and the field semantics). Default 'last'
|
|
187
|
+
// keeps the #1221 behavior; e.g. nullSort: 'low' makes "Never"
|
|
188
|
+
// sort beyond the oldest period on a last-seen date column.
|
|
183
189
|
if (aVal == null && bVal == null) return 0
|
|
184
|
-
if (aVal == null) return
|
|
185
|
-
if (bVal == null) return -
|
|
190
|
+
if (aVal == null) return nullCmp
|
|
191
|
+
if (bVal == null) return -nullCmp
|
|
186
192
|
|
|
187
193
|
// Compare
|
|
188
194
|
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
StorageResolution,
|
|
31
31
|
Orchestrator,
|
|
32
32
|
} from './EntityManager.types'
|
|
33
|
+
import type { NullSortMode } from '../query/clientFilter'
|
|
33
34
|
|
|
34
35
|
// Re-export public types from types file
|
|
35
36
|
export type {
|
|
@@ -83,6 +84,8 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
83
84
|
protected _fields: Record<string, FieldConfig>
|
|
84
85
|
|
|
85
86
|
localFilterThreshold: number | null
|
|
87
|
+
/** Default null placement for local sorts (#1222); field-level nullSort wins. */
|
|
88
|
+
nullSort?: NullSortMode
|
|
86
89
|
protected _cacheTtlMs: number | null
|
|
87
90
|
protected _asymmetric: boolean
|
|
88
91
|
protected _detailCache: DetailCacheState<T> = { items: new Map() }
|
|
@@ -153,6 +156,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
153
156
|
badges = null,
|
|
154
157
|
fields = {},
|
|
155
158
|
localFilterThreshold = null,
|
|
159
|
+
nullSort,
|
|
156
160
|
cacheTtlMs = null,
|
|
157
161
|
asymmetric = false,
|
|
158
162
|
detailCacheTtlMs = 0,
|
|
@@ -183,6 +187,7 @@ export class EntityManager<T extends EntityRecord = EntityRecord> {
|
|
|
183
187
|
this._fields = fields
|
|
184
188
|
|
|
185
189
|
this.localFilterThreshold = localFilterThreshold
|
|
190
|
+
this.nullSort = nullSort
|
|
186
191
|
this._cacheTtlMs = cacheTtlMs
|
|
187
192
|
this._asymmetric = asymmetric
|
|
188
193
|
this._detailCacheTtlMs = detailCacheTtlMs
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
} from '../types'
|
|
12
12
|
import type { AuthUser, EntityAuthAdapter as IEntityAuthAdapter } from './auth/EntityAuthAdapter'
|
|
13
13
|
import type { SignalBus } from '../kernel/SignalBus'
|
|
14
|
+
import type { NullSortMode } from '../query/clientFilter'
|
|
14
15
|
import type { HookRegistry } from '../hooks/HookRegistry'
|
|
15
16
|
|
|
16
17
|
// Circular type import — safe because `import type` is erased at runtime
|
|
@@ -214,6 +215,8 @@ export interface EntityManagerOptions<T extends EntityRecord = EntityRecord> {
|
|
|
214
215
|
badges?: (entity: T) => EntityBadge[]
|
|
215
216
|
fields?: Record<string, FieldConfig>
|
|
216
217
|
localFilterThreshold?: number | null
|
|
218
|
+
/** Default null placement for local sorts: 'first' | 'last' | 'low' | 'high' (default 'last', #1222). Field-level nullSort wins. */
|
|
219
|
+
nullSort?: NullSortMode
|
|
217
220
|
/** Cache TTL in milliseconds (0=disabled, -1=infinite, >0=TTL). Overrides global, overridden by storage. */
|
|
218
221
|
cacheTtlMs?: number | null
|
|
219
222
|
/** Asymmetric mode: list() and get() return different structures. get() skips list cache. */
|
package/src/index.ts
CHANGED
|
@@ -305,8 +305,11 @@ export {
|
|
|
305
305
|
searchItems,
|
|
306
306
|
paginate,
|
|
307
307
|
defaultGenerateId,
|
|
308
|
+
nullSortRank,
|
|
308
309
|
type SortOrder,
|
|
309
310
|
type FilterItemsOptions,
|
|
311
|
+
type NullSortMode,
|
|
312
|
+
type SortItemsOptions,
|
|
310
313
|
} from './query/clientFilter'
|
|
311
314
|
export { useChildPage, type UseChildPageReturn } from './composables/useChildPage'
|
|
312
315
|
export { useSidebarState, SIDEBAR_COLLAPSED_KEY } from './composables/useSidebarState'
|
|
@@ -15,14 +15,55 @@
|
|
|
15
15
|
|
|
16
16
|
export type SortOrder = 'asc' | 'desc'
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Null placement when sorting (#1222 — configurable, it depends on the
|
|
20
|
+
* API and the field semantics):
|
|
21
|
+
* - 'last' (default): nulls at the end, both directions — the historical
|
|
22
|
+
* adapter behavior, keeps empty rows out of the way.
|
|
23
|
+
* - 'first': nulls at the top, both directions.
|
|
24
|
+
* - 'low': null behaves as the lowest value — first in asc, last in
|
|
25
|
+
* desc. Right for "last seen" dates ("Never" sorts beyond the oldest
|
|
26
|
+
* period); equals PrimeVue nullSortOrder: -1.
|
|
27
|
+
* - 'high': null behaves as the highest value — last in asc, first in desc.
|
|
28
|
+
*/
|
|
29
|
+
export type NullSortMode = 'first' | 'last' | 'low' | 'high'
|
|
30
|
+
|
|
31
|
+
export interface SortItemsOptions {
|
|
32
|
+
nulls?: NullSortMode
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function nullSortRank(mode: NullSortMode, sortOrder: SortOrder): number {
|
|
36
|
+
// Return the comparator result for "a is null, b is not"
|
|
37
|
+
switch (mode) {
|
|
38
|
+
case 'first':
|
|
39
|
+
return -1
|
|
40
|
+
case 'low':
|
|
41
|
+
return sortOrder === 'asc' ? -1 : 1
|
|
42
|
+
case 'high':
|
|
43
|
+
return sortOrder === 'asc' ? 1 : -1
|
|
44
|
+
case 'last':
|
|
45
|
+
default:
|
|
46
|
+
return 1
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Sort by a field — the canonical comparator (null placement configurable). */
|
|
51
|
+
export function sortItems<T>(
|
|
52
|
+
items: T[],
|
|
53
|
+
sortBy?: string | null,
|
|
54
|
+
sortOrder: SortOrder = 'asc',
|
|
55
|
+
options: SortItemsOptions = {}
|
|
56
|
+
): T[] {
|
|
20
57
|
if (!sortBy) return items
|
|
58
|
+
const { nulls = 'last' } = options
|
|
21
59
|
return items.sort((a, b) => {
|
|
22
60
|
const aVal = a[sortBy as keyof T]
|
|
23
61
|
const bVal = b[sortBy as keyof T]
|
|
24
|
-
|
|
25
|
-
|
|
62
|
+
const aNull = aVal === undefined || aVal === null
|
|
63
|
+
const bNull = bVal === undefined || bVal === null
|
|
64
|
+
if (aNull && bNull) return 0
|
|
65
|
+
if (aNull) return nullSortRank(nulls, sortOrder)
|
|
66
|
+
if (bNull) return -nullSortRank(nulls, sortOrder)
|
|
26
67
|
if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1
|
|
27
68
|
if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1
|
|
28
69
|
return 0
|
package/src/types/index.ts
CHANGED
|
@@ -79,6 +79,8 @@ export type FieldType =
|
|
|
79
79
|
* Field configuration
|
|
80
80
|
*/
|
|
81
81
|
export interface FieldConfig {
|
|
82
|
+
/** Null placement when this field is locally sorted: 'first' | 'last' | 'low' | 'high' (#1222). */
|
|
83
|
+
nullSort?: 'first' | 'last' | 'low' | 'high'
|
|
82
84
|
type: FieldType
|
|
83
85
|
label?: string
|
|
84
86
|
required?: boolean
|