Skip to content

Commit

Permalink
Update data flow to use redux
Browse files Browse the repository at this point in the history
Signed-off-by: Sascha Grunert <sgrunert@suse.com>
  • Loading branch information
saschagrunert committed Aug 7, 2019
1 parent 10623db commit 8b44b24
Show file tree
Hide file tree
Showing 12 changed files with 206 additions and 125 deletions.
6 changes: 4 additions & 2 deletions src/app/app.module.ts
Expand Up @@ -17,10 +17,12 @@ import { LoggerService } from '@shared/services/logger.service';

import { EffectsModule } from '@ngrx/effects';
import { NotesEffects } from './notes/notes.effects';
import { FilterEffects } from './filter/filter.effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { StoreModule } from '@ngrx/store';
import { environment } from '../environments/environment';
import { notesReducer } from './notes/notes.reducer';
import { filterReducer } from './filter/filter.reducer';
import { reducers } from './app.reducer';
import { MarkdownModule, MarkedOptions } from 'ngx-markdown';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
Expand Down Expand Up @@ -48,8 +50,8 @@ import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
},
},
}),
StoreModule.forRoot({ notes: notesReducer }),
EffectsModule.forRoot([NotesEffects]),
StoreModule.forRoot({ filter: filterReducer, notes: notesReducer }),
EffectsModule.forRoot([FilterEffects, NotesEffects]),
StoreDevtoolsModule.instrument({ maxAge: 25, logOnly: environment.production }),
FontAwesomeModule,
],
Expand Down
3 changes: 3 additions & 0 deletions src/app/app.reducer.ts
Expand Up @@ -6,11 +6,14 @@ import {
MetaReducer,
} from '@ngrx/store';
import { State as NotesState, notesReducer } from './notes/notes.reducer';
import { State as FilterState, filterReducer } from './filter/filter.reducer';

export interface State {
filter: FilterState;
notes: NotesState;
}

export const reducers: ActionReducerMap<State> = {
filter: filterReducer,
notes: notesReducer,
};
26 changes: 26 additions & 0 deletions src/app/filter/filter.actions.ts
@@ -0,0 +1,26 @@
import { Action } from '@ngrx/store';
import { Filter } from '@app/shared/model/options.model';

export enum ActionTypes {
Failed = '[Filter Component] Failed',

UpdateFilter = '[Filter Component] Update Filter',
UpdateFilterSuccess = '[Filter Component] Update Filter Success',
}

export class Failed implements Action {
readonly type = ActionTypes.Failed;
constructor(public error: string) {}
}

export class UpdateFilter implements Action {
readonly type = ActionTypes.UpdateFilter;
constructor(public filter: Filter) {}
}

export class UpdateFilterSuccess implements Action {
readonly type = ActionTypes.UpdateFilterSuccess;
constructor(public filter: Filter) {}
}

export type FilterAction = Failed | UpdateFilter | UpdateFilterSuccess;
2 changes: 1 addition & 1 deletion src/app/filter/filter.component.html
Expand Up @@ -8,7 +8,7 @@
type="checkbox"
name="{{ data.key }}[]"
[ngModel]="filter[data.key][a.value]"
(ngModelChange)="updateFilterObject(data.key, a.value, $event)"
(ngModelChange)="updateFilter(data.key, a.value, $event)"
value="{{ a.value }}"
/>
{{ a.value }} </label
Expand Down
55 changes: 46 additions & 9 deletions src/app/filter/filter.component.ts
@@ -1,26 +1,63 @@
import { Component, ElementRef, EventEmitter, Input, Output } from '@angular/core';
import { Note } from '@app/notes/notes.model';
import { Store, select } from '@ngrx/store';
import { Filter, Options } from '@app/shared/model/options.model';
import { Note } from '@app/notes/notes.model';
import { getAllNotesSelector } from '@app/notes/notes.reducer';
import { State } from '@app/app.reducer';
import { UpdateFilter } from './filter.actions';
import { getFilterSelector } from '@app/filter/filter.reducer';

@Component({
selector: 'app-filter',
templateUrl: './filter.component.html',
})
export class FilterComponent {
@Input() options: Options = new Options();
@Input() filter: Filter = new Filter();
@Output() filterUpdate = new EventEmitter<Filter>();
private options: Options = new Options();
private filter: Filter = new Filter();

/**
* FilterComponent's constructor
*/
constructor(private store: Store<State>) {
this.store.pipe(select(getAllNotesSelector)).subscribe(n => {
this.updateOptions(n);
});

this.store.pipe(select(getFilterSelector)).subscribe(filter => {
this.filter = filter;
});
}

/**
* Update the options from the provided notes
*/
updateOptions(notes: Note[]): void {
for (const note of Object.values(notes)) {
if ('areas' in note) {
this.options.areas = [...new Set(this.options.areas.concat(note.areas))];
}
if ('kinds' in note) {
this.options.kinds = [...new Set(this.options.kinds.concat(note.kinds))];
}
if ('sigs' in note) {
this.options.sigs = [...new Set(this.options.sigs.concat(note.sigs))];
}
if (this.options.releaseVersions.indexOf(note.release_version) < 0) {
this.options.releaseVersions.push(note.release_version);
}
}
}

/**
* Update the filter object based on the given parameters
*/
updateFilterObject(a: string, b: string, val: any): void {
if (val) {
this.filter.add(a, b);
updateFilter(key: string, value: string, event: any): void {
if (event) {
this.filter.add(key, value);
} else {
delete this.filter[a][b];
this.filter.del(key, value);
}
this.filterUpdate.emit(this.filter);
this.store.dispatch(new UpdateFilter(this.filter));
}

/**
Expand Down
24 changes: 24 additions & 0 deletions src/app/filter/filter.effects.ts
@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { exhaustMap, map } from 'rxjs/operators';
import { ActionTypes, UpdateFilterSuccess, UpdateFilter } from './filter.actions';
import { LoggerService } from '@shared/services/logger.service';
import { Filter } from '@app/shared/model/options.model';

@Injectable()
export class FilterEffects {
@Effect()
updateFilter$ = this.actions$.pipe(
ofType(ActionTypes.UpdateFilter),
map((action: UpdateFilter) => action.filter),
exhaustMap(filter => {
this.logger.debug('[Filter Effects:UpdateFilter] SUCCESS');
const copy = new (filter.constructor as { new () })();
Object.assign(copy, filter);
return of(new UpdateFilterSuccess(copy));
}),
);

constructor(private actions$: Actions, private logger: LoggerService) {}
}
41 changes: 41 additions & 0 deletions src/app/filter/filter.reducer.ts
@@ -0,0 +1,41 @@
import { Action, createSelector, createFeatureSelector } from '@ngrx/store';
import { ActionTypes, FilterAction } from './filter.actions';
import { Filter } from '@app/shared/model/options.model';
import { State as RootState } from '@app/app.reducer';

export interface State {
error: string | null;
filter: Filter;
}

export const initialState: State = {
error: null,
filter: new Filter(),
};

export function filterReducer(state = initialState, action: FilterAction): State {
switch (action.type) {
case ActionTypes.UpdateFilterSuccess: {
return {
...state,
filter: action.filter,
};
}

case ActionTypes.Failed: {
return {
...state,
error: action.error,
};
}

default:
return state;
}
}

export const selectFilter = (state: RootState) => state.filter;
export const getFilterSelector = createSelector(
selectFilter,
(state: State) => state.filter,
);
14 changes: 2 additions & 12 deletions src/app/main/main.component.html
Expand Up @@ -37,25 +37,15 @@
<div class="row">
<nav class="col-md-2 d-none d-md-block bg-light sidebar">
<div class="sidebar-sticky">
<app-filter
id="filterComponent"
[options]="options"
[filter]="filter"
(filterUpdate)="onUpdateFromFilterComponent($event)"
></app-filter>
<app-filter id="filterComponent"></app-filter>
</div>
</nav>

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-4">
<div
class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"
>
<app-notes
id="notesComponent"
(filterUpdate)="onUpdateFromNotesComponent($event)"
(gotNotes)="gotNotes($event)"
[filter]="filter"
></app-notes>
<app-notes id="notesComponent"></app-notes>
</div>
</main>
</div>
Expand Down
31 changes: 0 additions & 31 deletions src/app/main/main.component.spec.ts
Expand Up @@ -76,37 +76,6 @@ describe('MainComponent', () => {
expect(component.filter.areas).toBe('');
});

it('should succeed to update options on gotNotes', () => {
component.gotNotes(notesMock);
expect(component.options.areas.length).toEqual(1);
expect(component.options.kinds.length).toEqual(1);
expect(component.options.sigs.length).toEqual(1);
expect(component.options.releaseVersions.length).toEqual(1);
});

it('should succeed to update options on empty/invalid notes', () => {
component.gotNotes([{} as Note]);
expect(component.options.areas.length).toEqual(0);
expect(component.options.kinds.length).toEqual(0);
expect(component.options.sigs.length).toEqual(0);
expect(component.options.releaseVersions.length).toEqual(1);
});

it('should succeed to toggle filter to true', () => {
const event = { key: areas, value: b };
component.onUpdateFromNotesComponent(event);
expect(component.filter[areas][b]).toBeTruthy();
});

it('should succeed to toggle filter to false', () => {
const event = { key: areas, value: b };
component.onUpdateFromNotesComponent(event);
expect(component.filter[areas][b]).toBeTruthy();

component.onUpdateFromNotesComponent(event);
expect(component.filter[areas][b]).toBeFalsy();
});

it('should succeed to open the modal view', () => {
component.openModal();
});
Expand Down
81 changes: 27 additions & 54 deletions src/app/main/main.component.ts
@@ -1,46 +1,54 @@
import { Component, ViewChild, ElementRef, OnInit } from '@angular/core';
import { Component, ViewChild, ElementRef } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { skip } from 'rxjs/operators';
import { State } from '@app/app.reducer';
import { Note } from '@app/notes/notes.model';
import { Filter, Options } from '@app/shared/model/options.model';
import { Filter } from '@app/shared/model/options.model';
import { NotesComponent } from '@app/notes/notes.component';
import { FilterComponent } from '@app/filter/filter.component';
import { UpdateFilter } from '@app/filter/filter.actions';
import { ModalComponent } from '@app/modal/modal.component';
import { Router, ActivatedRoute } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { getFilterSelector } from '@app/filter/filter.reducer';

@Component({
selector: 'app-main',
templateUrl: './main.component.html',
})
export class MainComponent implements OnInit {
options: Options = new Options();
filter: Filter = new Filter();
export class MainComponent {
private filter: Filter = new Filter();

@ViewChild(NotesComponent, { static: true }) noteChild;
@ViewChild(FilterComponent, { static: true }) filterChild;

constructor(
private store: Store<State>,
private router: Router,
private route: ActivatedRoute,
private modalService: NgbModal,
) {}

ngOnInit() {
) {
this.route.queryParamMap.subscribe(queryParamMap => {
if ('filter' in this && this.filter.isEmpty()) {
for (const i of Object.keys(this.filter)) {
if (queryParamMap.getAll(i).length > 0) {
if (i !== 'markdown') {
for (const x of queryParamMap.getAll(i)) {
this.filter.add(i, x);
}
} else {
this.filter.setMarkdown(queryParamMap.get(i));
}
for (let i = 0, len = queryParamMap.keys.length; i < len; i++) {
const key = queryParamMap.keys[i];
if (key !== 'markdown') {
for (const value of queryParamMap.getAll(key)) {
this.filter.add(key, value);
}
} else {
this.filter.setMarkdown(queryParamMap.get(key));
}
this.noteChild.update(this.filter);
}
});
this.store.dispatch(new UpdateFilter(this.filter));

this.store
.pipe(select(getFilterSelector))
.pipe(skip(1))
.subscribe(filter => {
this.filter = filter;
this.updateURI();
});
}

updateFilterString(a, b): void {
Expand All @@ -49,41 +57,6 @@ export class MainComponent implements OnInit {
} else {
this.filter[a] = '';
}
this.noteChild.update(this.filter);

this.updateURI();
}

gotNotes(notes: Note[]): void {
for (const note of Object.values(notes)) {
if ('areas' in note) {
this.options.areas = [...new Set(this.options.areas.concat(note.areas))];
}
if ('kinds' in note) {
this.options.kinds = [...new Set(this.options.kinds.concat(note.kinds))];
}
if ('sigs' in note) {
this.options.sigs = [...new Set(this.options.sigs.concat(note.sigs))];
}
if (this.options.releaseVersions.indexOf(note.release_version) < 0) {
this.options.releaseVersions.push(note.release_version);
}
}
}

onUpdateFromFilterComponent(filter: Filter): void {
this.filter = filter;
this.noteChild.update(this.filter);
this.updateURI();
}

onUpdateFromNotesComponent(event: any): void {
if (typeof this.filter[event.key][event.value] === 'boolean') {
delete this.filter[event.key][event.value];
} else {
this.filter.add(event.key, event.value);
}

this.updateURI();
}

Expand Down

0 comments on commit 8b44b24

Please sign in to comment.