Skip to content
Snippets Groups Projects
Commit 2da155ff authored by Isaac Evavold's avatar Isaac Evavold
Browse files

ROENROLL-1356

parent 3252ba66
No related branches found
No related tags found
No related merge requests found
<mat-sidenav-container id="plans-container" *ngIf="hasLoadedDegreePlan$ | async">
<mat-sidenav-container id="plans-container" *ngIf="(visibleDegreePlan$ | async) as degreePlan">
<!-- Menu side nav -->
<mat-sidenav #rightMenu position="end" [mode]="mobileView.matches ? 'over' : 'side'" [opened]="mobileView.matches ? false : true">
<cse-sidenav-menu-item></cse-sidenav-menu-item>
......@@ -15,7 +15,7 @@
(selectionChange)="handleDegreePlanChange($event)">
<!-- Render the name of the currently visible degree plan. -->
<mat-select-trigger *ngIf="(visibleDegreePlan$ | async) as degreePlan">
<mat-select-trigger>
<mat-icon class="primary-star" *ngIf="degreePlan.primary">star_rate</mat-icon>
<span class="plan-name">{{degreePlan.name}}</span>
</mat-select-trigger>
......@@ -33,12 +33,12 @@
</button>
<mat-menu #degreePlanMenu="matMenu">
<button mat-menu-item (click)="onCreatePlanClick()">Create new plan</button>
<button mat-menu-item (click)="onRenamePlanClick()">Rename plan</button>
<button mat-menu-item (click)="onDeletePlanClick()">Delete plan</button>
<button mat-menu-item (click)="onMakePrimayClick()">Make primary</button>
<button mat-menu-item (click)="onRenamePlanClick(degreePlan)">Rename plan</button>
<button mat-menu-item (click)="onDeletePlanClick(degreePlan)">Delete plan</button>
<button mat-menu-item (click)="onMakePrimayClick(degreePlan)">Make primary</button>
<hr>
<button mat-menu-item (click)="onPrintPlanClick()">Print plan</button>
<button mat-menu-item (click)="onSharePlanClick()">Share plan</button>
<button mat-menu-item (click)="onPrintPlanClick(degreePlan)">Print plan</button>
<button mat-menu-item (click)="onSharePlanClick(degreePlan)">Share plan</button>
</mat-menu>
</div>
......
......@@ -12,6 +12,7 @@ import {
import { MatDialog } from '@angular/material';
import { Store } from '@ngrx/store';
import { MediaMatcher } from '@angular/cdk/layout';
import { filter } from 'rxjs/operators';
// Models
import { GlobalState } from '@app/core/state';
......@@ -25,16 +26,20 @@ import {
firstActiveTermCode,
getAllVisibleTermsByYear,
getVisibleDegreePlan,
hasLoadedDegreePlan,
} from '@app/degree-planner/store/selectors';
// Actions
import {
SwitchPlan,
MakePlanPrimary,
CreatePlan,
ChangePlanName,
} from '@app/degree-planner/store/actions/plan.actions';
import { ModifyPlanDialogComponent } from './dialogs/modify-plan-dialog/modify-plan-dialog.component';
import {
ModifyPlanDialogComponent,
DialogMode,
} from './dialogs/modify-plan-dialog/modify-plan-dialog.component';
import { ToggleAcademicYear } from './store/actions/ui.actions';
@Component({
......@@ -47,9 +52,8 @@ export class DegreePlannerComponent implements OnInit {
public mobileView: MediaQueryList;
public coursesData$: any;
public hasLoadedDegreePlan$: Observable<boolean>;
public visibleRoadmapId$: Observable<number | undefined>;
public visibleDegreePlan$: Observable<DegreePlan | undefined>;
public visibleDegreePlan$: Observable<DegreePlan>;
public allDegreePlans$: Observable<DegreePlan[]>;
public firstActiveTermCode$: Observable<string | undefined>;
public termsByYear$: Observable<Year[]>;
......@@ -63,9 +67,11 @@ export class DegreePlannerComponent implements OnInit {
}
public ngOnInit() {
this.hasLoadedDegreePlan$ = this.store.pipe(select(hasLoadedDegreePlan));
this.visibleRoadmapId$ = this.store.pipe(select(getVisibleRoadmapId));
this.visibleDegreePlan$ = this.store.pipe(select(getVisibleDegreePlan));
this.visibleDegreePlan$ = this.store.pipe(
select(getVisibleDegreePlan),
filter(isntUndefined),
);
this.allDegreePlans$ = this.store.pipe(select(getAllDegreePlans));
this.firstActiveTermCode$ = this.store.pipe(select(firstActiveTermCode));
this.termsByYear$ = this.store.pipe(select(getAllVisibleTermsByYear));
......@@ -84,22 +90,37 @@ export class DegreePlannerComponent implements OnInit {
}
public onCreatePlanClick() {
// TODO
console.log('onCreatePlanClick');
const data = {};
this.dialog.open(ModifyPlanDialogComponent, { data });
const data: DialogMode = { mode: 'create' };
this.dialog
.open(ModifyPlanDialogComponent, { data })
.afterClosed()
.subscribe((result: { name: string } | undefined) => {
if (result !== undefined && typeof result.name === 'string') {
const name = result.name;
const action = new CreatePlan({ name, primary: false });
this.store.dispatch(action);
}
});
}
public onRenamePlanClick() {
// TODO
console.log('onRenamePlanClick');
const data = {};
this.dialog.open(ModifyPlanDialogComponent, { data });
public onRenamePlanClick(currentPlan: DegreePlan) {
const data: DialogMode = { mode: 'rename', oldName: currentPlan.name };
this.dialog
.open(ModifyPlanDialogComponent, { data })
.afterClosed()
.subscribe((result: { name: string } | undefined) => {
if (result !== undefined && typeof result.name === 'string') {
const newName = result.name;
const { roadmapId } = currentPlan;
const action = new ChangePlanName({ roadmapId, newName });
this.store.dispatch(action);
}
});
}
public onMakePrimayClick() {
// TODO open confirm dialog
this.store.dispatch(new MakePlanPrimary());
public onMakePrimayClick(currentPlan: DegreePlan) {
// TODO
console.warn('onMakePrimayClick');
}
public onDeletePlanClick() {
......@@ -137,3 +158,7 @@ export class DegreePlannerComponent implements OnInit {
return termCodes;
}
}
const isntUndefined = <T>(anything: T | undefined): anything is T => {
return anything !== undefined;
};
......@@ -6,22 +6,18 @@
</style>
<mat-toolbar color="primary" class="dialog-toolbar">
<span class="dialog-toolbar-title">Modify Plan</span>
<span class="dialog-toolbar-title" [ngSwitch]="data.mode">
<span *ngSwitchCase="'rename'">Rename plan</span>
<span *ngSwitchCase="'create'">Create plan</span>
</span>
<button mat-button mat-dialog-close class="close-btn">
<i class="material-icons">clear</i>
</button>
</mat-toolbar>
<mat-dialog-content class="mat-typography dialog-with-toolbar">
<div class="create-plan-content" *ngIf="true">
<mat-dialog-content [formGroup]="form">
<mat-form-field class="form-field-stretch">
<input matInput placeholder="Name" formControlName="planName">
</mat-form-field>
</mat-dialog-content>
</div>
<div class="rename-plan-content" *ngIf="false">
<ng-container [ngSwitch]="data.mode">
<div class="rename-plan-content" *ngSwitchCase="'rename'">
<mat-dialog-content [formGroup]="form">
<mat-form-field class="form-field-stretch">
<input matInput placeholder="Name" formControlName="planName">
......@@ -29,25 +25,25 @@
</mat-dialog-content>
</div>
<div class="delete-plan-content" *ngIf="false">
<div class="create-plan-content" *ngSwitchCase="'create'">
<mat-dialog-content [formGroup]="form">
<mat-form-field>
<mat-form-field class="form-field-stretch">
<input matInput placeholder="Name" formControlName="planName">
</mat-form-field>
</mat-dialog-content>
</div>
</ng-container>
<div class="make-plan-primary-content" *ngIf="false">
<mat-dialog-content [formGroup]="form">
<mat-form-field>
<input matInput placeholder="Name" formControlName="planName">
</mat-form-field>
</mat-dialog-content>
</div>
<mat-dialog-actions align="end">
<button mat-button class="btn-secondary" mat-dialog-close aria-label="Close note dialog" (click)="onCancel()">
Cancel
</button>
<mat-dialog-actions align="end">
<button mat-button class="btn-secondary" mat-dialog-close aria-label="Close note dialog"> Cancel </button>
<button mat-raised-button class="btn-primary mat-button" (click)="createPlanSave()" *ngIf="false">Save</button>
<button mat-raised-button class="btn-primary mat-button" (click)="renamePlanSave()">Save</button>
</mat-dialog-actions>
</mat-dialog-content>
\ No newline at end of file
<button mat-rasied-button class="btn-primary mat-button" [disabled]="form.invalid" (click)="onSubmit()">
<ng-container [ngSwitch]="data.mode">
<span *ngSwitchCase="'rename'">Rename plan</span>
<span *ngSwitchCase="'create'">Create plan</span>
</ng-container>
</button>
</mat-dialog-actions>
</mat-dialog-content>
import { FormGroup, FormControl } from '@angular/forms';
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { FormGroup, FormControl, Validators } from '@angular/forms';
import { Component, Input, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material';
// Models
import { GlobalState } from '@app/core/state';
// Actions
import { ChangePlanName } from '@app/degree-planner/store/actions/plan.actions';
export type DialogMode =
| { mode: 'rename'; oldName: string }
| { mode: 'create' };
@Component({
selector: 'cse-course-details-dialog',
templateUrl: './modify-plan-dialog.component.html',
})
export class ModifyPlanDialogComponent implements OnInit {
export class ModifyPlanDialogComponent {
public form: FormGroup;
constructor(private store: Store<GlobalState>) {
constructor(
private dialogRef: MatDialogRef<ModifyPlanDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: DialogMode,
) {
const initialName = this.data.mode === 'rename' ? this.data.oldName : '';
this.form = new FormGroup({
planName: new FormControl(),
planName: new FormControl(initialName, Validators.required),
});
}
ngOnInit() {}
renamePlanSave() {
this.store.dispatch(new ChangePlanName(this.form.value.planName));
console.log('PUT /degreePlan/<planId> { primary: <bool>, name: <string> }');
onSubmit() {
switch (this.data.mode) {
case 'rename':
this.dialogRef.close({ name: `${this.form.value.planName}` });
break;
case 'create':
this.dialogRef.close({ name: `${this.form.value.planName}` });
break;
default:
this.dialogRef.close();
}
}
makePlanPrimarySave() {
// Open confirm dialog
console.log('PUT /degreePlan/<planId> { primary: true, name: <string> }');
}
createPlanSave() {
console.log('POST /degreePlan { primary: <bool>, name: <string> }');
onCancel() {
this.dialogRef.close();
}
}
......@@ -32,6 +32,17 @@ export class DegreePlannerApiService {
);
}
public createDegreePlan(name: string, primary: boolean = false) {
const url = `${this.config.apiPlannerUrl}/degreePlan`;
const payload = { name, primary };
return this.http.post<DegreePlan>(url, payload);
}
public deleteDegreePlan(roadmapId: number) {
const url = `${this.config.apiPlannerUrl}/degreePlan/${roadmapId}`;
return this.http.delete<void>(url);
}
public getAllDegreePlans(): Observable<DegreePlan[]> {
return this.http.get<DegreePlan[]>(this.degreePlanEndpoint());
}
......@@ -188,8 +199,8 @@ export class DegreePlannerApiService {
planId: number,
name: string,
primary: boolean,
): Observable<Object> {
return this.http.put(
): Observable<1> {
return this.http.put<1>(
this.degreePlanEndpoint(planId),
{ name, primary },
HTTP_OPTIONS,
......
......@@ -9,15 +9,21 @@ export enum PlanActionTypes {
SwitchPlan = '[Plan] Switch',
SwitchPlanSuccess = '[Plan] Switch (Success)',
CreatePlan = '[Plan] Create',
CreatePlanSuccess = '[Plan] Create (Success)',
DeletePlan = '[Plan] Delete',
DeletePlanSuccess = '[Plan] Delete (Success)',
PlanError = '[Plan] Error',
MakePlanPrimary = '[Plan] Make Plan Primary',
MakePlanPrimarySuccess = '[Plan] Make Plan Primary Success',
MakePlanPrimaryFailure = '[Plan] Make Plan Primary Failure',
MakePlanPrimarySuccess = '[Plan] Make Plan Primary (Success)',
MakePlanPrimaryFailure = '[Plan] Make Plan Primary (Failure)',
ChangePlanName = '[Plan] Change Plan Name',
ChangePlanNameSuccess = '[Plan] Change Plan Name Success',
ChangePlanNameFailure = '[Plan] Change Plan Name Failure',
ChangePlanNameSuccess = '[Plan] Change Plan Name (Success)',
ChangePlanNameFailure = '[Plan] CHange Plan Name (Failure)',
}
export class InitialLoadSuccess implements Action {
......@@ -40,6 +46,26 @@ export class SwitchPlanSuccess implements Action {
) {}
}
export class CreatePlan implements Action {
public readonly type = PlanActionTypes.CreatePlan;
constructor(public payload: { name: string; primary: boolean }) {}
}
export class CreatePlanSuccess implements Action {
public readonly type = PlanActionTypes.CreatePlanSuccess;
constructor(public payload: { newPlan: DegreePlan }) {}
}
export class DeletePlan implements Action {
public readonly type = PlanActionTypes.DeletePlan;
constructor(public payload: { roadmapId: number }) {}
}
export class DeletePlanSuccess implements Action {
public readonly type = PlanActionTypes.DeletePlanSuccess;
constructor(public payload: { roadmapId: number }) {}
}
export class PlanError implements Action {
public readonly type = PlanActionTypes.PlanError;
constructor(public payload: { message: string; error: any }) {}
......@@ -47,30 +73,27 @@ export class PlanError implements Action {
export class MakePlanPrimary implements Action {
public readonly type = PlanActionTypes.MakePlanPrimary;
constructor() {}
}
export class MakePlanPrimarySuccess implements Action {
public readonly type = PlanActionTypes.MakePlanPrimarySuccess;
constructor() {}
}
export class MakePlanPrimaryFailure implements Action {
public readonly type = PlanActionTypes.MakePlanPrimaryFailure;
constructor() {}
}
export class ChangePlanName implements Action {
public readonly type = PlanActionTypes.ChangePlanName;
constructor(public name: string) {}
constructor(public payload: { roadmapId: number; newName: string }) {}
}
export class ChangePlanNameSuccess implements Action {
public readonly type = PlanActionTypes.ChangePlanNameSuccess;
constructor(public name: string) {}
constructor(public payload: { roadmapId: number; newName: string }) {}
}
export class ChangePlanNameFailure implements Action {
public readonly type = PlanActionTypes.ChangePlanNameFailure;
constructor(public name: string) {}
constructor(public payload: { roadmapId: number; oldName: string }) {}
}
......@@ -11,7 +11,7 @@ import {
filter,
} from 'rxjs/operators';
import { GlobalState } from '@app/core/state';
import { Store } from '@ngrx/store';
import { Store, Action } from '@ngrx/store';
// Services
import { DegreePlannerApiService } from '@app/degree-planner/services/api.service';
......@@ -24,10 +24,16 @@ import {
SwitchPlanSuccess,
PlanActionTypes,
PlanError,
MakePlanPrimary,
MakePlanPrimarySuccess,
MakePlanPrimaryFailure,
ChangePlanName,
ChangePlanNameSuccess,
ChangePlanNameFailure,
CreatePlan,
CreatePlanSuccess,
DeletePlan,
DeletePlanSuccess,
} from '@app/degree-planner/store/actions/plan.actions';
// Models
......@@ -35,6 +41,7 @@ import { DegreePlan } from '@app/core/models/degree-plan';
import { PlannedTerm } from '@app/core/models/planned-term';
import { SubjectMapping } from '@app/core/models/course';
import { SavedForLaterCourse } from '@app/core/models/saved-for-later-course';
import { DegreePlannerState } from '@app/degree-planner/store/state';
@Injectable()
export class DegreePlanEffects {
......@@ -134,27 +141,19 @@ export class DegreePlanEffects {
@Effect()
MakePlanPrimary$ = this.actions$.pipe(
ofType<any>(PlanActionTypes.MakePlanPrimary),
ofType<MakePlanPrimary>(PlanActionTypes.MakePlanPrimary),
withLatestFrom(this.store$.select(getDegreePlannerState)),
filter(([_, state]) => state.visibleDegreePlan !== undefined),
// Get term data for the degree plan specified by the roadmap ID.
flatMap(([action, state]) => {
flatMap(([_action, state]) => {
const { roadmapId, name } = state.visibleDegreePlan as DegreePlan;
// TODO error handle the API calls
return this.api.updatePlan(roadmapId, name, true).pipe(
map(response => {
return {
response,
action,
};
}),
);
return this.api.updatePlan(roadmapId, name, true);
}),
// // Wrap data in an Action for dispatch
map(({ response, action }) => {
map(response => {
if (response === 1) {
return new MakePlanPrimarySuccess();
} else {
......@@ -165,41 +164,47 @@ export class DegreePlanEffects {
@Effect()
ChangePlanName$ = this.actions$.pipe(
ofType<any>(PlanActionTypes.ChangePlanName),
ofType<ChangePlanName>(PlanActionTypes.ChangePlanName),
withLatestFrom(this.store$.select(getDegreePlannerState)),
filter(([_, state]) => state.visibleDegreePlan !== undefined),
// Get term data for the degree plan specified by the roadmap ID.
flatMap(([action, state]) => {
console.log(action);
const { name } = action;
const {
roadmapId,
name: previousName,
primary,
} = state.visibleDegreePlan as DegreePlan;
action.previousName = previousName;
// TODO error handle the API calls
return this.api.updatePlan(roadmapId, name, primary).pipe(
map(response => {
return {
response,
action,
};
}),
);
const { roadmapId, newName } = action.payload;
const oldDegreePlan = state.allDegreePlans.find(plan => {
return plan.roadmapId === roadmapId;
}) as DegreePlan;
const oldName = oldDegreePlan.name;
return this.api
.updatePlan(roadmapId, newName, oldDegreePlan.primary)
.pipe(
map(() => {
return new ChangePlanNameSuccess({ roadmapId, newName });
}),
catchError(() => {
return of(new ChangePlanNameFailure({ roadmapId, oldName }));
}),
);
}),
);
// // Wrap data in an Action for dispatch
map(({ response, action }) => {
if (response === 1) {
return new ChangePlanNameSuccess(action.name);
} else {
return new ChangePlanNameFailure(action.previousName);
}
@Effect()
createPlan$ = this.actions$.pipe(
ofType<CreatePlan>(PlanActionTypes.CreatePlan),
flatMap(action => {
const { name, primary } = action.payload;
return this.api
.createDegreePlan(name, primary)
.pipe(map(newPlan => new CreatePlanSuccess({ newPlan })));
}),
);
@Effect()
deletePlan$ = this.actions$.pipe(
ofType<DeletePlan>(PlanActionTypes.DeletePlan),
flatMap(action => {
const { roadmapId } = action.payload;
return this.api
.deleteDegreePlan(roadmapId)
.pipe(map(() => new DeletePlanSuccess({ roadmapId })));
}),
);
......@@ -295,3 +300,9 @@ const pickPrimaryDegreePlan = (plans: DegreePlan[]): DegreePlan => {
const checkExpanded = (activeTermCodes, visibleTerms) => {
console.log(visibleTerms);
};
const hasVisibleDegreePlan = <T extends Action>(
pair: [T, DegreePlannerState],
): pair is [T, { visibleDegreePlan: DegreePlan } & DegreePlannerState] => {
return pair[1].visibleDegreePlan !== undefined;
};
......@@ -12,6 +12,8 @@ import {
MakePlanPrimaryFailure,
ChangePlanNameSuccess,
ChangePlanNameFailure,
CreatePlan,
CreatePlanSuccess,
} from '@app/degree-planner/store/actions/plan.actions';
import {
CourseActionTypes,
......@@ -45,6 +47,7 @@ type SupportedActions =
| RemoveSaveForLaterSuccess
| AddSaveForLaterSuccess
| AddAcademicYearRequest
| CreatePlanSuccess
| MakePlanPrimary
| MakePlanPrimarySuccess
| MakePlanPrimaryFailure
......@@ -262,6 +265,14 @@ export function degreePlannerReducer(
return { ...state, savedForLaterCourses: newSavedForLater };
}
case PlanActionTypes.CreatePlanSuccess: {
const { newPlan } = action.payload;
return {
...state,
allDegreePlans: state.allDegreePlans.concat(newPlan),
};
}
case PlanActionTypes.MakePlanPrimary: {
// TODO add global loading state
return state;
......@@ -298,17 +309,42 @@ export function degreePlannerReducer(
return state;
}
case PlanActionTypes.ChangePlanNameSuccess:
case PlanActionTypes.ChangePlanNameFailure: {
// TODO add global loading state
case PlanActionTypes.ChangePlanNameSuccess: {
const visibleDegreePlan = {
...(state.visibleDegreePlan as DegreePlan),
name: action.payload.newName,
};
// Update the visible plan object
const newVisibleDegreePlan = {
return {
...state,
allDegreePlans: state.allDegreePlans.map(plan => {
if (plan.roadmapId === action.payload.roadmapId) {
return { ...plan, name: action.payload.newName };
} else {
return plan;
}
}),
visibleDegreePlan,
};
}
case PlanActionTypes.ChangePlanNameFailure: {
const visibleDegreePlan = {
...(state.visibleDegreePlan as DegreePlan),
name: action.name,
name: action.payload.oldName,
};
return { ...state, visibleDegreePlan: newVisibleDegreePlan };
return {
...state,
allDegreePlans: state.allDegreePlans.map(plan => {
if (plan.roadmapId === action.payload.roadmapId) {
return { ...plan, name: action.payload.oldName };
} else {
return plan;
}
}),
visibleDegreePlan,
};
}
/**
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment