Skip to content

Commit 4da7ca1

Browse files
authored
Multi profile (#73)
* add component store extension mixin * add ability to save multiple profiles * fix saving metadata change some ui * fix column builders running an extra time * fix to no longer mutate when cleaning records
1 parent 98d3ee7 commit 4da7ca1

File tree

17 files changed

+423
-136
lines changed

17 files changed

+423
-136
lines changed

projects/table-builder/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "mx-table-builder",
3-
"version": "0.3.13",
3+
"version": "0.4.0",
44
"peerDependencies": {
55
"@angular/common": "~11.0.0",
66
"@angular/core": "~11.0.0",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentStore } from '@ngrx/component-store';
2+
import { Observable } from 'rxjs';
3+
import { tap } from 'rxjs/operators';
4+
5+
6+
interface ConcreteClass<C> { new (...args: any[]): C; }
7+
8+
9+
interface ComponentStoreType<T extends {}> extends ConcreteClass<ComponentStore<T>> {
10+
11+
}
12+
13+
export function ComponentStoreExtensions<T extends {}>(){
14+
return function <U extends ComponentStoreType<T>>(constructor: U) {
15+
return class U extends constructor {
16+
on = <V>(srcObservable: Observable<V>, func: (obj: V) => void) => {
17+
this.effect((src: Observable<V>) => {
18+
return src.pipe(tap(func));
19+
})(srcObservable);
20+
}
21+
};
22+
}
23+
}

projects/table-builder/src/lib/classes/table-builder.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { MetaData, FieldType, ReportDef } from '../interfaces/report-def';
33
import { first, map, switchMap, shareReplay, publishReplay, refCount } from 'rxjs/operators';
44
import { mapArray } from '../functions/rxjs-operators';
55

6-
export class TableBuilder {
6+
export class TableBuilder<T = any> {
77
constructor(private data$: Observable<any[]>, public metaData$?: Observable<MetaData[]> ) {
88
this.data$ = this.data$.pipe(publishReplay(1),refCount());
99
this.metaData$ = this.metaData$ ?
@@ -41,11 +41,12 @@ export class TableBuilder {
4141
return val;
4242
}
4343

44-
cleanRecord( record: any, metadata: MetaData []): any {
45-
metadata.forEach( md => {
46-
record[md.key] = this.cleanVal(record[md.key], md);
47-
});
48-
return record;
44+
cleanRecord( record: T, metadata: MetaData []): T {
45+
const cleaned = metadata.reduce( (prev: T, curr: MetaData) => {
46+
prev[curr.key] = this.cleanVal(record[curr.key], curr);
47+
return prev;
48+
}, {} )
49+
return {...record, ...cleaned};
4950
}
5051
}
5152

projects/table-builder/src/lib/classes/table-store.ts

Lines changed: 38 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,51 @@ import { defaultTableState, TableState } from './TableState';
55
import { Injectable, Inject } from '@angular/core';
66
import { TableBuilderConfig, TableBuilderConfigToken } from './TableBuilderConfig';
77
import { FilterInfo } from './filter-info';
8-
import { selectStorageStateItem, StateStorage } from '../ngrx/reducer';
98
import { Sort, SortDirection } from '@angular/material/sort' ;
109
import { ComponentStore } from '@ngrx/component-store' ;
1110
import update from 'immutability-helper';
1211
import { Dictionary } from '../interfaces/dictionary';
13-
import { select, Store } from '@ngrx/store';
14-
import { first, mergeMap, tap } from 'rxjs/operators';
15-
import { loadState, saveState } from '../ngrx/actions';
12+
import { map } from 'rxjs/operators';
13+
import { ComponentStoreExtensions } from './component-store-extensions';
1614

15+
const TableStateStore = ComponentStoreExtensions<TableState>()(ComponentStore);
1716

18-
@Injectable()
19-
export class TableStore extends ComponentStore<TableState> {
17+
@Injectable({
18+
providedIn: 'root',
19+
})
20+
export class TableStore extends TableStateStore<TableState> {
2021

21-
constructor(@Inject(TableBuilderConfigToken) private config: TableBuilderConfig,
22-
private store: Store<{storageState: StateStorage}>) {
23-
super( { ...defaultTableState, ...config.defaultTableState});
24-
}
25-
readonly filters$ = this.select(state => state.filters );
26-
27-
getFilter$ = (filterId) : Observable<FilterInfo | undefined> => {
28-
return this.select( this.filters$, filters => filters[filterId]);
22+
constructor(@Inject(TableBuilderConfigToken) config: TableBuilderConfig) {
23+
super( { ...defaultTableState, ...config.defaultTableState});
2924
}
3025

26+
getSavableState() : Observable<TableState> {
27+
return this.state$.pipe(
28+
map( s => {
29+
const metaData = Object.values(s.metaData)
30+
.map( md => ({...md, transform: undefined }))
31+
.reduce((prev: Dictionary<MetaData>, current)=> ({...prev, [current.key]: current}), {});
32+
return {...s, metaData };
33+
})
34+
);
3135

32-
setFromSavedState = (id:string) => {
33-
this.store.dispatch(loadState({id}));
34-
this.updateState( this.store.pipe(
35-
select(selectStorageStateItem(id)),
36-
mergeMap( (state: TableState ) => (state ? [state] : [] )),
37-
first(),
38-
));
3936
}
37+
readonly filters$ = this.select(state => state.filters );
4038

41-
on = <T>( srcObservable: Observable<T>, func: (obj:T)=> void) => {
42-
this.effect((src: Observable<T>) => {
43-
return src.pipe(tap(func));
44-
})(srcObservable);
39+
getFilter$ = (filterId) : Observable<FilterInfo | undefined> => {
40+
return this.select( state => state.filters[filterId] );
4541
}
4642

47-
saveToState = async (id:string) => {
48-
const state = await this.state$.pipe(first()).toPromise();
49-
const metaData = Object.values(state.metaData).map( md => ({...md, transform: undefined }))
50-
.reduce((prev: Dictionary<MetaData>, current)=> ({...prev, [current.key]: current}), {})
51-
this.store.dispatch(saveState({id,state: {...state,metaData},persist: true}));
52-
}
43+
readonly metaData$ = this.select( state => state.metaData);
5344

54-
readonly metaData$ = this.select(
55-
state => Object.values(state.metaData)
45+
readonly metaDataArray$ = this.select(
46+
this.metaData$,
47+
md => Object.values(md)
5648
.sort( (a,b)=> a.order - b.order )
5749
);
5850

5951
getMetaData$ = (key: string) : Observable<MetaData> => {
60-
return this.select(this.metaData$, md => md.find(m => m.key === key ))
52+
return this.select( state => state.metaData[key] )
6153
}
6254

6355
createPreSort = (metaDatas: Dictionary<MetaData>): Sort[] => {
@@ -128,18 +120,18 @@ export class TableStore extends ComponentStore<TableState> {
128120
};
129121
});
130122

131-
mergeMetaDatas = (existingMetaData: Dictionary<MetaData>, newMetaDatas: Dictionary<MetaData>) => {
132-
const metaData: Dictionary<MetaData> = {};
133-
const metaDatas = Object.values(existingMetaData);
134-
metaDatas.forEach( md => {
135-
const existing = metaData[md.key] ?? existingMetaData[md.key];
136-
if(!existing) {
137-
metaData[md.key] = { ...md, noExport: md.customCell }
138-
} else {
139-
metaData[md.key] = this.mergeMeta(existing,md);
140-
}
141-
});
142-
return {...metaData, ...newMetaDatas};
123+
mergeMetaDatas = (existingMetaData: Dictionary<MetaData>, incomingMetaData: Dictionary<MetaData>) : Dictionary<MetaData> => {
124+
const keys = [...new Set(Object.keys(existingMetaData).concat(Object.keys(incomingMetaData)))];
125+
return keys.reduce( (prev: Dictionary<MetaData>, key: string) => {
126+
const existing = existingMetaData[key];
127+
const incoming = incomingMetaData[key];
128+
if(existing && incoming) {
129+
prev[key] = this.mergeMeta(existing,incoming);
130+
} else {
131+
prev[key] = incoming ?? existing;
132+
}
133+
return prev;
134+
}, {});
143135
}
144136

145137
updateStateFunc = (state: TableState, incomingTableState: Partial<TableState>) : TableState => {

projects/table-builder/src/lib/components/gen-filter-displayer/gen-filter-displayer.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { map } from 'rxjs/operators';
1414
export class GenFilterDisplayerComponent {
1515

1616
constructor( public tableState: TableStore) {
17-
this.filterCols$ = tableState.metaData$.pipe(
17+
this.filterCols$ = tableState.metaDataArray$.pipe(
1818
map(md => Object.values( md ).filter(m => (m.fieldType !== FieldType.Hidden) && (!m.noFilter))),
1919
);
2020
}

projects/table-builder/src/lib/components/table-container/table-container.html

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<ng-container multiSort >
1+
<ng-container multiSort *ngIf="currentStateKey$ | async as currentKey" >
22
<ng-container >
33
<div style="width:100%;">
44
<div style="display: flex; flex-direction: row;justify-content: flex-end; ">
@@ -7,17 +7,51 @@
77
</tb-filter-displayer>
88
<tb-col-displayer></tb-col-displayer>
99

10-
<button mat-icon-button (click)="state.resetState();OnStateReset.next()" matTooltip="Reset table">
11-
<mat-icon color="primary">autorenew</mat-icon>
12-
</button>
13-
<button *ngIf="tableId" mat-icon-button (click)="state.saveToState(tableId);OnSaveState.next()" matTooltip="Save Table">
14-
<mat-icon color="primary">save</mat-icon>
15-
</button>
16-
<button mat-icon-button (click)="exportToCsv()" matTooltip="Export Table">
17-
<mat-icon color="primary">file_download</mat-icon>
18-
</button>
10+
<button mat-icon-button color='primary' [matMenuTriggerFor]="mainMenu"><mat-icon>more_vert</mat-icon></button>
11+
<mat-menu #mainMenu='matMenu' >
12+
<button mat-menu-item (click)="state.resetState();OnStateReset.next()" >
13+
<mat-icon color="primary">autorenew</mat-icon>
14+
<span>Reset table</span>
15+
</button>
16+
<button mat-menu-item *ngIf="tableId" (click)="saveState()" >
17+
<mat-icon color="primary">save</mat-icon>
18+
<span>Save to {{currentKey}}</span>
19+
</button>
20+
<button mat-menu-item (click)="exportToCsvService.exportToCsv(data)" >
21+
<mat-icon color="primary">file_download</mat-icon>
22+
<span>Export Table</span>
23+
</button>
24+
<button *ngIf='tableId' mat-menu-item [matMenuTriggerFor]="savedNames" >
25+
<span>Choose Profile</span>
26+
</button>
27+
</mat-menu>
28+
<mat-menu #savedNames='matMenu' panelClass='wide-menu' >
29+
<button mat-menu-item clickSubject #add='clickSubject' >
30+
<mat-icon>add</mat-icon>
31+
<span>New</span>
32+
</button>
33+
<ng-container *ngFor='let key of stateKeys$ | async'>
34+
<button mat-menu-item (click)='setProfileState(key)' >
35+
<div style='display: flex; align-items: center; justify-content: space-between;'>
36+
<span style='display:flex;'>{{key}}</span>
37+
<span style='display:flex;'>
38+
<span style="width: 120px;"></span>
39+
<mat-icon color='warn' (click)='deleteProfileState(key)' stop-propagation >delete_forever</mat-icon>
40+
</span>
41+
</div>
42+
</button>
43+
</ng-container>
1944

2045

46+
47+
</mat-menu>
48+
<div *appDialog='add' >
49+
<mat-form-field >
50+
<input style='width:90%' matInput #addedKey />
51+
</mat-form-field>
52+
<button mat-button (click)='setProfileState(addedKey.value); add.next(false);'>Add</button>
53+
</div>
54+
2155
</div>
2256
</div>
2357
<div style="clear: both;">

projects/table-builder/src/lib/components/table-container/table-container.ts

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,22 @@ import {
1010
} from '@angular/core';
1111
import { Observable } from 'rxjs';
1212
import { ArrayAdditional, FieldType, MetaData } from '../../interfaces/report-def';
13-
import { first, map } from 'rxjs/operators';
13+
import { filter, first, map, tap } from 'rxjs/operators';
1414
import { TableBuilder } from '../../classes/table-builder';
1515
import { MatRowDef } from '@angular/material/table';
1616
import { CustomCellDirective } from '../../directives';
1717
import { TableStore } from '../../classes/table-store';
1818
import * as _ from 'lodash';
1919
import { DataFilter } from '../../classes/data-filter';
20-
import { mapArray } from '../../functions/rxjs-operators';
20+
import { mapArray, skipOneWhen } from '../../functions/rxjs-operators';
2121
import { ExportToCsvService } from '../../services/export-to-csv.service';
2222
import { ArrayDefaults } from '../../classes/DefaultSettings';
2323
import { TableBuilderConfig, TableBuilderConfigToken } from '../../classes/TableBuilderConfig';
24+
import { GlobalStorageState } from '../../ngrx/reducer';
25+
import * as selectors from '../../ngrx/selectors';
26+
import { select, Store } from '@ngrx/store';
27+
import { TableState } from 'dist/mx-table-builder/lib/classes/TableState';
28+
import { deleteLocalProfilesState, setLocalProfile, setLocalProfilesState } from '../../ngrx/actions';
2429

2530
@Component({
2631
selector: 'tb-table-container',
@@ -45,52 +50,91 @@ import { TableBuilderConfig, TableBuilderConfigToken } from '../../classes/Table
4550

4651
@ContentChildren(MatRowDef) customRows: QueryList<MatRowDef<any>>;
4752
@ContentChildren(CustomCellDirective) customCells: QueryList<CustomCellDirective>;
48-
49-
5053
@Output() OnStateReset = new EventEmitter();
5154
@Output() OnSaveState = new EventEmitter();
5255

5356
myColumns$: Observable<ColumnInfo[]>;
5457

58+
stateKeys$?: Observable<string[]>;
59+
currentStateKey$?: Observable<string>;
60+
5561

5662
constructor(
5763
public state: TableStore,
58-
private exportToCsvService: ExportToCsvService<T>,
59-
@Inject(TableBuilderConfigToken) private config: TableBuilderConfig
64+
public exportToCsvService: ExportToCsvService<T>,
65+
@Inject(TableBuilderConfigToken) private config: TableBuilderConfig,
66+
private store: Store<{globalStorageState: GlobalStorageState}>
6067
) {
6168
}
6269

6370
ngOnInit() {
6471
if(this.tableId) {
65-
this.state.setFromSavedState(this.tableId);
72+
this.state.updateState(
73+
this.store.pipe(
74+
select(selectors.selectLocalProfileState<TableState>(this.tableId)),
75+
tap( state => {
76+
// for backwards compatability we want to load the state from the old schema.
77+
if(!state) {
78+
const oldLocalState = localStorage.getItem(this.tableId);
79+
if(oldLocalState){
80+
this.store.dispatch(setLocalProfile({ key: this.tableId, value: JSON.parse( oldLocalState), persist: true} ));
81+
}
82+
}
83+
}),
84+
filter( state => !!state ),
85+
skipOneWhen(this.OnSaveState),
86+
)
87+
);
88+
this.stateKeys$ = this.store.select(selectors.selectLocalProfileKeys(this.tableId));
89+
this.currentStateKey$ = this.store.select(selectors.selectLocalProfileCurrentKey(this.tableId));
6690
}
6791
const filters$ = this.state.filters$.pipe(map( filters => Object.values(filters) ))
6892
this.data = new DataFilter(this.inputFilters)
6993
.appendFilters(filters$)
7094
.filterData(this.tableBuilder.getData$());
7195
}
7296

97+
saveState() {
98+
this.state.getSavableState().pipe(
99+
first()
100+
).subscribe( tableState => {
101+
this.OnSaveState.next();
102+
this.store.dispatch(setLocalProfile({ key: this.tableId, value:tableState, persist: true} ));
103+
});
104+
}
105+
106+
setProfileState(val: string) {
107+
this.store.dispatch(setLocalProfilesState({key:this.tableId, current: val}));
108+
}
109+
110+
deleteProfileState(stateKey: string) {
111+
this.store.dispatch(deleteLocalProfilesState({key:this.tableId, stateKey}));
112+
}
113+
114+
73115
ngAfterContentInit() {
74116
this.InitializeColumns();
75117
}
76118

77119

78120
InitializeColumns() {
79121
const customCellMap = new Map(this.customCells.map(cc => [cc.customCell,cc]));
80-
this.state.setMetaData(this.tableBuilder.metaData$.pipe(first(),map((mds) => {
81-
mds = mds.map(this.mapMetaDatas);
82-
return [...mds, ...this.customCells.map( cc => cc.getMetaData() )]
83-
})));
122+
this.state.setMetaData(this.tableBuilder.metaData$.pipe(
123+
first(),
124+
map((mds) => {
125+
mds = mds.map(this.mapMetaDatas);
126+
return [
127+
...mds,
128+
...this.customCells.map( cc => cc.getMetaData(mds.find( item => item.key === cc.customCell )) )
129+
]
130+
})
131+
));
84132

85-
this.myColumns$ = this.state.metaData$.pipe(
133+
this.myColumns$ = this.state.metaDataArray$.pipe(
86134
mapArray( metaData => ({metaData, customCell: customCellMap.get(metaData.key)}))
87135
);
88136
}
89137

90-
exportToCsv = () => {
91-
this.exportToCsvService.exportToCsv(this.data)
92-
}
93-
94138
mapMetaDatas = (meta : MetaData<T>) => {
95139
if(meta.fieldType === FieldType.Array){
96140
const additional = {...meta.additional} as ArrayAdditional;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Directive } from '@angular/core';
2+
import { Subject } from 'rxjs';
3+
4+
@Directive({
5+
selector: 'button[clickSubject]',
6+
exportAs: 'clickSubject',
7+
host: {
8+
'(click)': 'next(true)'
9+
}
10+
}) export class DialogOpenDirective extends Subject<any> {
11+
// TODO: add explicit constructor
12+
}

0 commit comments

Comments
 (0)