Skip to content

Commit b8eedd0

Browse files
author
Gérard Collin
committed
feat: support for multisort
1 parent 8463074 commit b8eedd0

4 files changed

Lines changed: 199 additions & 25 deletions

File tree

libs/xt-store/projects/store/src/store-entity/store-entity-feature.ts

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
EntityId,
44
EntityMap,
55
removeEntity,
6-
SelectEntityId,
6+
SelectEntityId, setAllEntities,
77
setEntities,
88
setEntity,
99
withEntities
@@ -14,7 +14,8 @@ import { finalize, firstValueFrom, lastValueFrom, map, mergeMap, Observable, of
1414
import { Signal } from '@angular/core';
1515
import { ManagedData, XtTypeResolver } from 'xt-type';
1616
import { XtStoreManager } from '../store-manager/xt-store-manager';
17-
import { XtStoreCriteria } from '../xt-store-parameters';
17+
import { XtSortBy, XtStoreCriteria } from '../xt-store-parameters';
18+
import { XtStoreProviderHelper } from '../store-provider/xt-store-provider-helper';
1819

1920
export function selectId<T extends ManagedData=ManagedData>(): SelectEntityId<T> { return (data) => {
2021
if (data._id==null) throw new Error("ManagedData with no entity Id used in the store.", { cause: data });
@@ -30,9 +31,11 @@ export function xtStoreEntityConfig<T extends ManagedData=ManagedData> () {
3031
};
3132

3233

33-
export type StoreState = {
34+
export type StoreState<T extends ManagedData=ManagedData> = {
3435
entityName: string,
35-
loading:boolean
36+
loading:boolean,
37+
sort?: XtSortBy<T>[],
38+
filter?: XtStoreCriteria<T>[]
3639
};
3740

3841
export type XtSignalStore<T> = {
@@ -72,14 +75,14 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
7275
withMethods ((store) => ({
7376
async storeEntity (toStore:T): Promise<T> {
7477
patchState(store, {loading:true});
75-
return this.clearReferences(toStore).then((valAndRef) => {
78+
return this._clearReferences(toStore).then((valAndRef) => {
7679
return store._storeProvider.storeEntity(entityName, valAndRef.newValue).then((stored) => {
7780
return { newValue: stored, references: valAndRef.references };
7881
});
7982
}).then((valAndRef) => {
80-
return this.applyReferences(valAndRef.newValue, valAndRef.references);
83+
return this._applyReferences(valAndRef.newValue, valAndRef.references);
8184
}).then((stored) => {
82-
patchState(store, setEntity(stored, store._entityConfig));
85+
this._patchStateSetEntity (stored);
8386
return stored;
8487
}).finally(() => {
8588
patchState(store, {loading:false});
@@ -89,7 +92,7 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
8992
fetchEntities (): Promise<void> {
9093
patchState(store, {loading:true});
9194
return lastValueFrom(store._storeProvider.searchEntities(entityName).pipe ( mergeMap((entities:T[]) => {
92-
return this.resolveReferences(entities);
95+
return this._resolveReferences(entities);
9396
}),map( (entities: T[]) => {
9497
patchState(store, setEntities (entities, store._entityConfig));
9598
}),finalize(() => {
@@ -111,7 +114,7 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
111114
patchState(store, {loading:true});
112115
return store._storeProvider.loadEntity(entityName, id).then((loaded) => {
113116
if (loaded != null)
114-
return this.resolveReferences([loaded]).then((results) => results[0]);
117+
return this._resolveReferences([loaded]).then((results) => results[0]);
115118
return loaded;
116119
}).then ( (loaded)=> {
117120
if( loaded != null)
@@ -149,17 +152,11 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
149152
patchState(store, { loading: true });
150153
try {
151154
const listEntities = store.entities();
152-
let toAdd=true;
153155
const ret = new Array<T>();
154156
for (const entity of listEntities) {
155-
toAdd=true;
156-
for (const crit of criteria) {
157-
if (!crit.filter(entity)) {
158-
toAdd=false;
159-
break;
160-
}
157+
if (!this._isFiltered(entity, criteria)){
158+
ret.push(entity);
161159
}
162-
if (toAdd) ret.push(entity);
163160
}
164161
return of(ret);
165162
} finally {
@@ -171,7 +168,7 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
171168
* Detects and replace all referenced objects by the key value that will be stored.
172169
* @param toClear
173170
*/
174-
async clearReferences(toClear: T): Promise<{ newValue: T, references: any }> {
171+
async _clearReferences(toClear: T): Promise<{ newValue: T, references: any }> {
175172
if( typeRegistry==null) return { newValue:toClear, references:{}};
176173

177174
const refs = typeRegistry.listReferences(entityName);
@@ -190,7 +187,7 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
190187
* From the key values, assign the relevant referenced object so that it's transparent to the caller
191188
* @param values
192189
*/
193-
async resolveReferences(values: T[]): Promise<T[]> {
190+
async _resolveReferences(values: T[]): Promise<T[]> {
194191
if ((typeRegistry==null)||(storeMgr==null)) return values;
195192

196193
const refs = typeRegistry.listReferences(entityName);
@@ -217,11 +214,60 @@ export function withXtStoreProvider<T extends ManagedData = ManagedData> (entity
217214
* @param newValue
218215
* @param references
219216
*/
220-
async applyReferences(newValue: T, references: ManagedData): Promise<T> {
217+
async _applyReferences(newValue: T, references: ManagedData): Promise<T> {
221218
if (references != null) {
222219
// Override the values with the references
223220
return { ...newValue, ...references } as T;
224221
} else return newValue;
222+
},
223+
224+
_patchStateSetEntity (stored:T) {
225+
// Ensure the entity is still part of the list
226+
if (this._isFiltered (stored)==false) {
227+
// It's still in the list, now check if sort needs to be applied
228+
if (this._safeSort().length>0) {
229+
// We remove it from the old sort
230+
if (stored._id!=null)
231+
patchState(store, removeEntity (stored._id));
232+
233+
// And find it's right position
234+
const newList = XtStoreProviderHelper.insertInSortedList(stored, store.entities(), this._safeSort());
235+
patchState (store, setAllEntities (newList, store._entityConfig));
236+
} else {
237+
patchState(store, setEntity(stored, store._entityConfig));
238+
}
239+
} else {
240+
// Maybe it was in the list and it is no more due to the modification. Let's make sure by removing it
241+
if (stored._id!=null)
242+
patchState(store, removeEntity (stored._id));
243+
}
244+
245+
},
246+
_isFiltered (element:T, criteria?:XtStoreCriteria<T>[]):boolean {
247+
if (criteria===undefined) {
248+
criteria=this._safeFilter();
249+
}
250+
for (const crit of criteria) {
251+
if (crit.filter(element)) {
252+
return true;
253+
}
254+
255+
}
256+
return false;
257+
},
258+
_safeFilter (): XtStoreCriteria<T>[] {
259+
const ret=store.filter?store.filter():null;
260+
if ((ret!=null) && (ret.length>0)) {
261+
return ret;
262+
}
263+
return [];
264+
},
265+
_safeSort (): XtSortBy<T>[] {
266+
const ret=store.sort?store.sort():null;
267+
if ((ret!=null) && (ret.length>0)) {
268+
return ret;
269+
}
270+
return [];
225271
}
226272
})
227273
)

libs/xt-store/projects/store/src/store-provider/xt-store-provider-helper.spec.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {XtStoreProviderHelper} from './xt-store-provider-helper';
22
import { XtStoreGroupByAggregate, XtStoreGroupBy } from '../xt-reporting';
3-
import { XtGroupByOperation } from '../xt-store-parameters';
3+
import { XtGroupByOperation, XtSortByDirection } from '../xt-store-parameters';
44
import { ManagedData, ManagedDataHandler, SpecialFields, xtTypeManager } from 'xt-type';
55
import { describe, expect, it } from 'vitest';
66

@@ -236,5 +236,80 @@ describe('Store Provider Helper', () => {
236236
}
237237
}
238238
)
239+
240+
it ('should support simple sorting correctly', () => {
241+
const testArray=[{
242+
name: 'cTest',
243+
value: 4,
244+
date: new Date (1970,10,5)
245+
},{
246+
name:'aTest',
247+
value:1,
248+
date: new Date(1960, 5,10)
249+
},{
250+
name:'bTest',
251+
value: 10,
252+
date: new Date(2010,8,4)
253+
}];
254+
255+
const byNameSort = XtStoreProviderHelper.multiSortArray([...testArray], [{
256+
by:'name',
257+
direction:XtSortByDirection.Ascending
258+
}]);
259+
260+
expect(byNameSort.map((val)=> val.name)).toEqual(['aTest','bTest', 'cTest']);
261+
262+
const byValueSort= XtStoreProviderHelper.multiSortArray([...testArray], [{
263+
by:'value',
264+
direction:XtSortByDirection.Descending
265+
}]);
266+
267+
expect(byValueSort.map((val)=> val.value)).toEqual([10,4, 1]);
268+
269+
const byDateSort= XtStoreProviderHelper.multiSortArray([...testArray], [{
270+
by:'date',
271+
direction:XtSortByDirection.None
272+
}]);
273+
274+
expect(byDateSort.map((val)=> val.date)).toEqual([testArray[1].date, testArray[0].date, testArray[2].date]);
275+
276+
const byMultipleValueSort= XtStoreProviderHelper.multiSortArray([...testArray], [{
277+
by:'value',
278+
direction:XtSortByDirection.Descending
279+
}, {
280+
by:'name',
281+
direction:XtSortByDirection.Ascending
282+
}]);
283+
284+
expect(byMultipleValueSort.map((val)=> val.value)).toEqual([10,4, 1]);
285+
})
286+
287+
288+
it ('should support multiple sort correctly', () => {
289+
const testArray=[{
290+
name: 'cTest',
291+
value: 4,
292+
date: new Date (1970,10,5)
293+
},{
294+
name:'aTest',
295+
value:1,
296+
date: new Date(1960, 5,10)
297+
},{
298+
name:'cTest',
299+
value: 10,
300+
date: new Date(2010,8,4)
301+
}];
302+
303+
const byNameSort = XtStoreProviderHelper.multiSortArray([...testArray], [{
304+
by:'name',
305+
direction:XtSortByDirection.Descending
306+
}, {
307+
by:'value',
308+
direction:XtSortByDirection.Ascending
309+
}]);
310+
311+
expect(byNameSort.map((val)=> val.name+val.value)).toEqual(['cTest4','cTest10', 'aTest1']);
312+
313+
})
239314
}
240315
);

libs/xt-store/projects/store/src/store-provider/xt-store-provider-helper.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { Counters, ManagedData, XtTypeHandler, xtTypeManager } from 'xt-type';
2-
import { XtGroupBy, XtGroupByAggregate, XtGroupByOperation, XtSortBy, XtStoreCriteria } from '../xt-store-parameters';
2+
import {
3+
XtGroupBy,
4+
XtGroupByAggregate,
5+
XtGroupByOperation,
6+
XtSortBy,
7+
XtSortByDirection,
8+
XtStoreCriteria
9+
} from '../xt-store-parameters';
310

411
/**
512
* Helps handle metadata information about loaded items
@@ -121,10 +128,52 @@ export class XtStoreProviderHelper {
121128
* @param toSort
122129
* @param sortOptions
123130
*/
124-
static multiSortArray<T>(toSort: T[], sortOptions?: XtSortBy<T>): T[] {
131+
static multiSortArray<T>(toSort: T[], sortOptions?: XtSortBy<T>[]): T[] {
125132
if( sortOptions==null)
126133
return toSort;
127-
return toSort;
134+
return toSort.sort(this.compareFunction(sortOptions));
135+
}
136+
137+
/**
138+
* Quickly insert an element in an already sorted list
139+
* @param toInsert
140+
* @param list
141+
* @param sortOptions
142+
*/
143+
static insertInSortedList<T>(toInsert: T, list:T[], sortOptions?:XtSortBy<T>[]): T[] {
144+
if ((sortOptions==null) || (sortOptions.length==0)) {
145+
list.push(toInsert);
146+
return list;
147+
} else {
148+
const compareFunction=this.compareFunction(sortOptions);
149+
for (let index=0;index<list.length;index++) {
150+
if (list[index]>toInsert) {
151+
return list.splice(index, 0, toInsert);
152+
}
153+
}
154+
list.push(toInsert);
155+
return list;
156+
}
157+
}
158+
159+
/**
160+
* Returns a compare function based on the sort options given
161+
* @param sortOptions
162+
*/
163+
static compareFunction<T> (sortOptions:XtSortBy<T>[]): (a:T, b:T) => number {
164+
return (a:T, b:T):number => {
165+
for (const sortOption of sortOptions) {
166+
const aVal=a[sortOption.by];
167+
const bVal=b[sortOption.by];
168+
let factor=1;
169+
if (sortOption.direction==XtSortByDirection.Descending) factor=-1;
170+
if (aVal < bVal) return -1*factor;
171+
if (aVal > bVal) return 1*factor;
172+
// If aVal == bVal, then it will loop to the lower sort
173+
}
174+
// Couldn't find any difference, then let's say they are equals
175+
return 0;
176+
}
128177
}
129178

130179
/**

libs/xt-store/projects/store/src/store-provider/xt-store-provider.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,11 @@ export abstract class AbstractXtStoreProvider<T extends ManagedData = ManagedDat
111111
map (value => {
112112
let groupedByValues:DontCodeStoreGroupedByEntities<T>|undefined;
113113
if((sort!=null) || (groupBy?.atLeastOneGroupIsRequested()===true)) {
114-
value = XtStoreProviderHelper.multiSortArray(value, this.calculateSortHierarchy(sort, groupBy)) as T[];
114+
if (sort!=null) {
115+
const sortHierarchy=this.calculateSortHierarchy(sort, groupBy);
116+
if (sortHierarchy!=null)
117+
value = XtStoreProviderHelper.multiSortArray(value, [sortHierarchy]) as T[];
118+
}
115119
if (groupBy!=null) {
116120
groupedByValues = XtStoreProviderHelper.calculateGroupedByValues(name, value, groupBy);
117121
}

0 commit comments

Comments
 (0)