Skip to content

Commit 403bf3c

Browse files
authored
Tt 218 dont allow deleting projects (#679)
* feat: TT-218-Dont-allow-deleting-projects * test: TT-218-Dont-allow-deleting-projects * feat: TT-218 remove delete button * refactor: TT-218 sonarcloud
1 parent c757b7e commit 403bf3c

File tree

12 files changed

+313
-48
lines changed

12 files changed

+313
-48
lines changed

src/app/modules/customer-management/components/projects/components/project-list/project-list.component.html

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,31 @@
22
<table class="table table-sm table-bordered table-striped">
33
<thead class="thead-blue">
44
<tr class="d-flex">
5-
<th class="col-4">Project ID</th>
6-
<th class="col-5">Project</th>
7-
<th class="col-3 text-center"></th>
5+
<th scope="col" class="col-4 text-center">Project ID</th>
6+
<th scope="col" class="col-4 text-center">Project</th>
7+
<th scope="col" class="col-2 text-center">Options</th>
8+
<th scope="col" class="col-2 text-center">Visibility</th>
89
</tr>
910
</thead>
1011
<tbody>
1112
<tr class="d-flex" *ngFor="let project of projects">
1213
<td class="col-sm-4">{{ project.id }}</td>
13-
<td class="col-sm-5">{{ project.name }}</td>
14-
<td class="col-sm-3 text-center">
14+
<td class="col-sm-4">{{ project.name }}</td>
15+
<td class="col-sm-2 text-center">
1516
<button type="button" class="btn btn-sm btn-primary" (click)="updateProject(project)">
1617
<i class="fa fa-pencil fa-xs"></i>
1718
</button>
18-
<button
19-
type="button"
20-
data-toggle="modal"
21-
data-target="#deleteModal"
22-
class="btn btn-sm btn-danger ml-2"
23-
(click)="openModal(project)"
19+
</td>
20+
<td class="col-sm-2 text-center">
21+
<button
22+
class="btn btn-sm"
23+
data-toggle="modal"
24+
data-target="#deleteModal"
25+
[ngClass]="project.btnColor"
26+
(click)="switchStatus(project)"
2427
>
25-
<i class="fa fa-trash-alt fa-xs"></i>
28+
<em class="fa" [ngClass]="project.btnIcon" ></em>
29+
{{project.btnName}}
2630
</button>
2731
</td>
2832
</tr>
@@ -37,7 +41,7 @@
3741
tabindex="-1"
3842
role="dialog"
3943
aria-hidden="true"
40-
[title]="'Delete Project'"
44+
[title]="'Archive Project'"
4145
[body]="message"
4246
(closeModalEvent)="deleteProject()"
4347
>

src/app/modules/customer-management/components/projects/components/project-list/project-list.component.spec.ts

Lines changed: 65 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ProjectState } from '../store/project.reducer';
99
import { getCustomerProjects } from '../store/project.selectors';
1010
import { SetProjectToEdit, DeleteProject } from '../store/project.actions';
1111
import { FilterProjectPipe } from '../../../../../shared/pipes';
12+
import { ProjectUI } from 'src/app/modules/shared/models';
1213

1314
describe('ProjectListComponent', () => {
1415
let component: ProjectListComponent;
@@ -17,18 +18,34 @@ describe('ProjectListComponent', () => {
1718
let getCustomerProjectsSelectorMock;
1819
let allCustomerProjectsSelectorMock;
1920

21+
const project = { id: '123', name: 'aaa', description: 'xxx', project_type_id: '1234', status: 'inactive' };
22+
2023
const state: ProjectState = {
21-
projects: [],
22-
customerProjects: [],
24+
projects: [project],
25+
customerProjects: [project],
2326
isLoading: false,
2427
message: '',
2528
projectToEdit: undefined,
2629
};
2730

28-
const project = { id: '123', name: 'aaa', description: 'xxx', project_type_id: '1234' };
29-
30-
beforeEach(
31-
() => {
31+
const btnProps = [
32+
{
33+
key: 'active',
34+
_status: false,
35+
btnColor: 'btn-danger',
36+
btnIcon: 'fa-arrow-circle-down',
37+
btnName: 'Archive',
38+
},
39+
{
40+
key: 'inactive',
41+
_status: true,
42+
btnColor: 'btn-primary',
43+
btnIcon: 'fa-arrow-circle-up',
44+
btnName: 'Active',
45+
},
46+
];
47+
48+
beforeEach(() => {
3249
TestBed.configureTestingModule({
3350
imports: [NgxPaginationModule],
3451
declarations: [ProjectListComponent, FilterProjectPipe],
@@ -58,7 +75,11 @@ describe('ProjectListComponent', () => {
5875
it('loads projects from state onInit', () => {
5976
component.ngOnInit();
6077

61-
expect(component.projects).toBe(state.customerProjects);
78+
const StateWithBtnProperties = state.customerProjects.map((projectfilter: ProjectUI) => {
79+
const addProps = btnProps.find((prop) => prop.key === component.setActive(projectfilter.status));
80+
return { ...projectfilter, ...addProps };
81+
});
82+
expect(component.projects).toEqual(StateWithBtnProperties);
6283
});
6384

6485
it('should destroy the subscriptions', () => {
@@ -96,4 +117,41 @@ describe('ProjectListComponent', () => {
96117
expect(store.dispatch).toHaveBeenCalledTimes(1);
97118
expect(store.dispatch).toHaveBeenCalledWith(new DeleteProject(project.id));
98119
});
120+
121+
it('switchStatus should call openModal() on item.status = activate', () => {
122+
const itemData = {
123+
id: '123',
124+
name: 'aaa',
125+
description: 'xxx',
126+
project_type_id: '1234',
127+
status: 'activate',
128+
key: 'activate',
129+
_status: false,
130+
btnColor: 'btn-danger',
131+
btnIcon: 'fa-arrow-circle-down',
132+
btnName: 'Archive',
133+
};
134+
135+
spyOn(component, 'openModal');
136+
component.switchStatus(itemData);
137+
expect(component.openModal).toHaveBeenCalled();
138+
});
139+
140+
it('switchStatus should set showModal false when item.status = inactive', () => {
141+
const itemData = {
142+
id: '123',
143+
name: 'aaa',
144+
description: 'xxx',
145+
project_type_id: '1234',
146+
status: 'inactive',
147+
key: 'inactive',
148+
_status: true,
149+
btnColor: 'btn-primary',
150+
btnIcon: 'fa-arrow-circle-up',
151+
btnName: 'Active',
152+
};
153+
154+
component.switchStatus(itemData);
155+
expect(component.showModal).toBeFalse();
156+
});
99157
});

src/app/modules/customer-management/components/projects/components/project-list/project-list.component.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
22
import { Store, select } from '@ngrx/store';
33
import { Subscription } from 'rxjs';
44
import { ITEMS_PER_PAGE } from 'src/environments/environment';
5-
import { Project } from 'src/app/modules/shared/models';
65
import { ProjectState } from '../store/project.reducer';
76
import { getCustomerProjects } from '../store/project.selectors';
87
import * as actions from '../store/project.actions';
8+
import { ProjectUI } from '../../../../../shared/models/project.model';
99

1010
@Component({
1111
selector: 'app-project-list',
@@ -16,7 +16,7 @@ export class ProjectListComponent implements OnInit, OnDestroy {
1616
initPage3 = 1;
1717
itemsPerPage = ITEMS_PER_PAGE;
1818
isLoading = false;
19-
projects: Project[] = [];
19+
projects: ProjectUI[] = [];
2020
filterProjects = '';
2121
showModal = false;
2222
idToDelete: string;
@@ -27,10 +27,30 @@ export class ProjectListComponent implements OnInit, OnDestroy {
2727
constructor(private store: Store<ProjectState>) {}
2828

2929
ngOnInit(): void {
30+
const btnProps = [
31+
{
32+
key: 'active',
33+
_status: false,
34+
btnColor: 'btn-danger',
35+
btnIcon: 'fa-arrow-circle-down',
36+
btnName: 'Archive',
37+
},
38+
{
39+
key: 'inactive',
40+
_status: true,
41+
btnColor: 'btn-primary',
42+
btnIcon: 'fa-arrow-circle-up',
43+
btnName: 'Active',
44+
},
45+
];
46+
3047
const projects$ = this.store.pipe(select(getCustomerProjects));
3148
this.projectsSubscription = projects$.subscribe((response) => {
3249
this.isLoading = response.isLoading;
33-
this.projects = response.customerProjects;
50+
this.projects = response.customerProjects.map((project: ProjectUI) => {
51+
const addProps = btnProps.find((prop) => prop.key === this.setActive(project.status));
52+
return { ...project, ...addProps };
53+
});
3454
});
3555
}
3656

@@ -47,9 +67,22 @@ export class ProjectListComponent implements OnInit, OnDestroy {
4767
this.showModal = false;
4868
}
4969

50-
openModal(item: Project) {
70+
openModal(item: ProjectUI) {
5171
this.idToDelete = item.id;
52-
this.message = `Are you sure you want to delete ${item.name}?`;
72+
this.message = `Are you sure you want archive ${item.name}?`;
5373
this.showModal = true;
5474
}
75+
76+
switchStatus(item: ProjectUI): void {
77+
if (item.key !== 'inactive') {
78+
this.openModal(item);
79+
} else {
80+
this.showModal = false;
81+
this.store.dispatch(new actions.UnarchiveProject(item.id));
82+
}
83+
}
84+
85+
setActive(status: any): string {
86+
return status === 'inactive' ? 'inactive' : 'active';
87+
}
5588
}

src/app/modules/customer-management/components/projects/components/store/project.actions.spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,4 +81,22 @@ describe('Actions for Projects', () => {
8181
const deleteProjectFail = new actions.DeleteProjectFail('error');
8282
expect(deleteProjectFail.type).toEqual(actions.ProjectActionTypes.DELETE_PROJECT_FAIL);
8383
});
84+
85+
it('UnarchiveProject type is ProjectActionTypes.UNARCHIVE_PROJECT', () => {
86+
const unarchiveProject = new actions.UnarchiveProject('id');
87+
expect(unarchiveProject.type).toEqual(actions.ProjectActionTypes.UNARCHIVE_PROJECT);
88+
});
89+
90+
it('UnarchiveProjectSuccess type is ProjectActionTypes.UNARCHIVE_PROJECT_SUCCESS', () => {
91+
const unarchiveProjecttSuccess = new actions.UnarchiveProjectSuccess({
92+
id: 'id_test',
93+
status: 'active',
94+
});
95+
expect(unarchiveProjecttSuccess.type).toEqual(actions.ProjectActionTypes.UNARCHIVE_PROJECT_SUCCESS);
96+
});
97+
98+
it('UnarchiveProjectProjectFail type is ProjectActionTypes.UNARCHIVE_PROJECT_FAIL', () => {
99+
const unarchiveProjecttFail = new actions.UnarchiveProjectFail('error');
100+
expect(unarchiveProjecttFail.type).toEqual(actions.ProjectActionTypes.UNARCHIVE_PROJECT_FAIL);
101+
});
84102
});

src/app/modules/customer-management/components/projects/components/store/project.actions.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Action } from '@ngrx/store';
2-
import { Project } from '../../../../../shared/models';
2+
import { Project, Status } from '../../../../../shared/models';
33

44
export enum ProjectActionTypes {
55
LOAD_PROJECTS = '[Projects] LOAD_PROJECTS',
@@ -20,6 +20,9 @@ export enum ProjectActionTypes {
2020
DELETE_PROJECT_SUCCESS = '[Projects] DELETE_PROJECT_SUCESS',
2121
DELETE_PROJECT_FAIL = '[Projects] DELETE_PROJECT_FAIL',
2222
CLEAN_CUSTOMER_PROJECTS = '[Projects] CLEAN_CUSTOMER_PROJECTS',
23+
UNARCHIVE_PROJECT = '[Projects] UNARCHIVE_PROJECT',
24+
UNARCHIVE_PROJECT_SUCCESS = '[Projects] UNARCHIVE_PROJECT_SUCCESS',
25+
UNARCHIVE_PROJECT_FAIL = '[Projects] UNARCHIVE_PROJECT_FAIL',
2326
}
2427

2528
export class CleanCustomerProjects implements Action {
@@ -42,7 +45,6 @@ export class LoadProjectsFail implements Action {
4245
constructor(public error: string) {}
4346
}
4447

45-
4648
export class LoadCustomerProjects implements Action {
4749
public readonly type = ProjectActionTypes.LOAD_CUSTOMER_PROJECTS;
4850
constructor(public customerId: string) {}
@@ -122,6 +124,24 @@ export class DeleteProjectFail implements Action {
122124
constructor(public error: string) {}
123125
}
124126

127+
export class UnarchiveProject implements Action {
128+
public readonly type = ProjectActionTypes.UNARCHIVE_PROJECT;
129+
130+
constructor(public payload: string) {}
131+
}
132+
133+
export class UnarchiveProjectSuccess implements Action {
134+
public readonly type = ProjectActionTypes.UNARCHIVE_PROJECT_SUCCESS;
135+
136+
constructor(public payload: Status) {}
137+
}
138+
139+
export class UnarchiveProjectFail implements Action {
140+
public readonly type = ProjectActionTypes.UNARCHIVE_PROJECT_FAIL;
141+
142+
constructor(public error: string) {}
143+
}
144+
125145
export type ProjectActions =
126146
| CleanCustomerProjects
127147
| LoadProjects
@@ -140,4 +160,7 @@ export type ProjectActions =
140160
| ResetProjectToEdit
141161
| DeleteProject
142162
| DeleteProjectSuccess
143-
| DeleteProjectFail;
163+
| DeleteProjectFail
164+
| UnarchiveProject
165+
| UnarchiveProjectSuccess
166+
| UnarchiveProjectFail;

src/app/modules/customer-management/components/projects/components/store/project.effects.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('ProjectEffects', () => {
1515
let effects: ProjectEffects;
1616
let service: ProjectService;
1717
let toastrService;
18-
const project: Project = { id: 'id', name: 'name', description: 'descrition' };
18+
const project: Project = { id: 'id', name: 'name', description: 'descrition', status: 'inactive' };
1919
const projects: Project[] = [];
2020

2121
beforeEach(() => {
@@ -146,4 +146,27 @@ describe('ProjectEffects', () => {
146146
expect(action.type).toEqual(ProjectActionTypes.LOAD_CUSTOMER_PROJECTS_FAIL);
147147
});
148148
});
149+
150+
it('action type is UNARCHIVE_PROJECT_SUCCESS when service is executed sucessfully', async () => {
151+
const projectId = 'projectId';
152+
actions$ = of({ type: ProjectActionTypes.UNARCHIVE_PROJECT, projectId });
153+
spyOn(toastrService, 'success');
154+
spyOn(service, 'updateProject').and.returnValue(of(project));
155+
156+
effects.unarchiveProject$.subscribe((action) => {
157+
expect(toastrService.success).toHaveBeenCalledWith(INFO_SAVED_SUCCESSFULLY);
158+
expect(action.type).toEqual(ProjectActionTypes.UNARCHIVE_PROJECT_SUCCESS);
159+
});
160+
});
161+
162+
it('action type is UNARCHIVE_PROJECT_FAIL when service fail in execution', async () => {
163+
actions$ = of({ type: ProjectActionTypes.UNARCHIVE_PROJECT, project });
164+
spyOn(toastrService, 'error');
165+
spyOn(service, 'updateProject').and.returnValue(throwError({ error: { message: 'fail!' } }));
166+
167+
effects.unarchiveProject$.subscribe((action) => {
168+
expect(toastrService.error).toHaveBeenCalled();
169+
expect(action.type).toEqual(ProjectActionTypes.UNARCHIVE_PROJECT_FAIL);
170+
});
171+
});
149172
});

src/app/modules/customer-management/components/projects/components/store/project.effects.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ofType, Actions, Effect } from '@ngrx/effects';
77
import { ProjectService } from '../services/project.service';
88
import * as actions from './project.actions';
99
import { ToastrService } from 'ngx-toastr';
10+
import { Status } from 'src/app/modules/shared/models';
1011

1112
@Injectable()
1213
export class ProjectEffects {
@@ -101,4 +102,25 @@ export class ProjectEffects {
101102
)
102103
)
103104
);
105+
106+
@Effect()
107+
unarchiveProject$: Observable<Action> = this.actions$.pipe(
108+
ofType(actions.ProjectActionTypes.UNARCHIVE_PROJECT),
109+
map((action: actions.UnarchiveProject) => ({
110+
id: action.payload,
111+
status: 'active',
112+
})),
113+
mergeMap((project: Status) =>
114+
this.projectService.updateProject(project).pipe(
115+
map((projectData) => {
116+
this.toastrService.success(INFO_SAVED_SUCCESSFULLY);
117+
return new actions.UnarchiveProjectSuccess(projectData);
118+
}),
119+
catchError((error) => {
120+
this.toastrService.error(error.error.message);
121+
return of(new actions.UnarchiveProjectFail(error));
122+
})
123+
)
124+
)
125+
);
104126
}

0 commit comments

Comments
 (0)