Skip to content

Commit 6642ec8

Browse files
author
Juan Gabriel Guzman
committed
feat: #222 Extracting technologies dropdown as component
1 parent 3b8234e commit 6642ec8

File tree

9 files changed

+308
-242
lines changed

9 files changed

+308
-242
lines changed

src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import { CustomerEffects } from './modules/customer-management/store/customer-ma
5757
import { EntryEffects } from './modules/time-clock/store/entry.effects';
5858
import { InjectTokenInterceptor } from './modules/shared/interceptors/inject.token.interceptor';
5959
import { SubstractDatePipe } from './modules/shared/pipes/substract-date/substract-date.pipe';
60+
import {TechnologiesComponent} from './modules/shared/components/technologies/technologies.component';
6061

6162
@NgModule({
6263
declarations: [
@@ -92,6 +93,7 @@ import { SubstractDatePipe } from './modules/shared/pipes/substract-date/substra
9293
CreateProjectTypeComponent,
9394
EntryFieldsComponent,
9495
SubstractDatePipe,
96+
TechnologiesComponent,
9597
],
9698
imports: [
9799
CommonModule,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<div class="input-group input-group-sm">
2+
<div class="input-group-prepend">
3+
<span class="input-group-text span-width">Tecnologias</span>
4+
</div>
5+
<input
6+
(input)="queryTechnologies($event)"
7+
id="technologies"
8+
type="text"
9+
class="form-control"
10+
aria-label="Small"
11+
aria-describedby="inputGroup-sizing-sm"
12+
[(ngModel)]="query"
13+
/>
14+
</div>
15+
16+
<div *ngIf="isLoading">LOADING...</div>
17+
<div #technologiesDropdown *ngIf="technology && showList" class="d-flex flex-column technologies-dropdown-container">
18+
<div
19+
*ngFor="let item of technology.items"
20+
(click)="addTechnology(item.name)"
21+
class="technologies-dropdown-item">
22+
{{ item.name }}
23+
</div>
24+
</div>
25+
<div class="selected-technologies-container d-flex flex-wrap" *ngIf="selectedTechnologies.length">
26+
<div *ngFor="let technology of selectedTechnologies; let tagIndex = index" class="selected-technology">
27+
<span class="mr-3">{{ technology }}</span>
28+
<i class="fas fa-times text-white" (click)="removeTechnology(tagIndex)"></i>
29+
</div>
30+
</div>
31+
<div class="form-group" *ngIf="selectedTechnologies.length === 0">
32+
<!-- empty-space -->
33+
</div>
34+
35+
36+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
@import '../../../../../styles/colors.scss';
2+
3+
.technologies-dropdown-container {
4+
box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08);
5+
margin: 0 0 2rem 6rem;
6+
max-height: 7.5rem;
7+
overflow-y: auto;
8+
9+
.technologies-dropdown-item {
10+
cursor: pointer;
11+
font-size: 0.8rem;
12+
margin-bottom: 0.1rem;
13+
padding: 0.2rem 0.5rem;
14+
15+
&:hover {
16+
opacity: 0.7;
17+
background-color: #efefef;
18+
}
19+
}
20+
}
21+
22+
.selected-technologies-container {
23+
margin: 0.5rem 0 0.5rem 6rem;
24+
}
25+
26+
.selected-technology {
27+
background-color: $modal-button-secondary;
28+
color: #ffffff;
29+
border-radius: 0.2rem;
30+
box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.75);
31+
font-size: 0.8rem;
32+
padding: 0.1rem 1rem 0.2rem 1.5rem;
33+
position: relative;
34+
margin: 0 0.5rem 0.5rem 0;
35+
36+
&:hover {
37+
opacity: 0.8;
38+
}
39+
40+
i {
41+
cursor: pointer;
42+
}
43+
}
44+
45+
.span-width {
46+
width: 6rem;
47+
background-image: $background-pantone !important;
48+
color: white;
49+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
2+
import {provideMockStore, MockStore} from '@ngrx/store/testing';
3+
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
4+
5+
import {TechnologyState} from '../../store/technology.reducers';
6+
import {allTechnologies} from '../../store/technology.selectors';
7+
import {TechnologiesComponent} from './technologies.component';
8+
import * as actions from '../../store/technology.actions';
9+
import {ProjectState} from '../../../customer-management/components/projects/components/store/project.reducer';
10+
import {getCustomerProjects} from '../../../customer-management/components/projects/components/store/project.selectors';
11+
import * as entryActions from '../../../time-clock/store/entry.actions';
12+
13+
describe('Technologies component', () => {
14+
type Merged = TechnologyState & ProjectState;
15+
let component: TechnologiesComponent;
16+
let fixture: ComponentFixture<TechnologiesComponent>;
17+
let store: MockStore<TechnologyState>;
18+
let mockTechnologySelector;
19+
20+
const state = {
21+
technologies: {
22+
technologyList: {items: [{name: 'java'}]},
23+
isLoading: false,
24+
}
25+
};
26+
27+
beforeEach(async(() => {
28+
TestBed.configureTestingModule({
29+
declarations: [TechnologiesComponent],
30+
providers: [provideMockStore({initialState: state})],
31+
imports: [FormsModule, ReactiveFormsModule],
32+
}).compileComponents();
33+
store = TestBed.inject(MockStore);
34+
mockTechnologySelector = store.overrideSelector(allTechnologies, state.technologies);
35+
}));
36+
37+
beforeEach(() => {
38+
fixture = TestBed.createComponent(TechnologiesComponent);
39+
component = fixture.componentInstance;
40+
fixture.detectChanges();
41+
});
42+
43+
it('should create', () => {
44+
expect(component).toBeTruthy();
45+
});
46+
47+
it('when a new technology is added, it should be added to the selectedTechnologies list', () => {
48+
const name = 'ngrx';
49+
component.selectedTechnologies = ['java', 'javascript'];
50+
component.selectedTechnologies.indexOf(name);
51+
length = component.selectedTechnologies.length;
52+
component.addTechnology(name);
53+
expect(component.selectedTechnologies.length).toBe(3);
54+
});
55+
56+
it('when the max number of technologies is reached, then adding technologies is not allowed', () => {
57+
const name = 'ngrx';
58+
component.selectedTechnologies = [
59+
'java',
60+
'javascript',
61+
'angular',
62+
'angular-ui',
63+
'typescript',
64+
'scss',
65+
'bootstrap',
66+
'jasmine',
67+
'karma',
68+
'github',
69+
];
70+
console.log(component.selectedTechnologies.length);
71+
length = component.selectedTechnologies.length;
72+
component.addTechnology(name);
73+
expect(component.selectedTechnologies.length).toBe(10);
74+
});
75+
76+
it('when a technology is removed, then it should be removed from the technologies list', () => {
77+
const index = 1;
78+
component.selectedTechnologies = ['java', 'angular'];
79+
component.removeTechnology(index);
80+
expect(component.selectedTechnologies.length).toBe(1);
81+
});
82+
83+
it('when querying technologies, then a FindTechnology action should be dispatched', () => {
84+
const query = 'react';
85+
const target = {value: query};
86+
const event = new InputEvent('input');
87+
spyOnProperty(event, 'target').and.returnValue(target);
88+
spyOn(store, 'dispatch');
89+
component.queryTechnologies(event);
90+
91+
expect(store.dispatch).toHaveBeenCalledWith(new actions.FindTechnology(query));
92+
});
93+
});
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {Component, ElementRef, EventEmitter, Input, OnInit, Output, Renderer2, ViewChild} from '@angular/core';
2+
import * as actions from '../../store/technology.actions';
3+
import {select, Store} from '@ngrx/store';
4+
import {TechnologyState} from '../../store/technology.reducers';
5+
import {allTechnologies} from '../../store/technology.selectors';
6+
import {Technology} from '../../models';
7+
8+
9+
@Component({
10+
selector: 'app-technologies',
11+
templateUrl: './technologies.component.html',
12+
styleUrls: ['./technologies.component.scss']
13+
})
14+
export class TechnologiesComponent implements OnInit {
15+
private readonly MAX_NUM_TECHNOLOGIES = 10;
16+
private readonly MIN_QUERY_LENGTH = 2;
17+
private query = '';
18+
showList = false;
19+
isLoading = false;
20+
technology: Technology;
21+
22+
@Input()
23+
selectedTechnologies: string[] = [];
24+
@ViewChild('technologiesDropdown') list: ElementRef;
25+
@Output()
26+
technologyRemoved: EventEmitter<string[]> = new EventEmitter<string[]>();
27+
@Output()
28+
technologyAdded: EventEmitter<string[]> = new EventEmitter<string[]>();
29+
30+
constructor(private store: Store<TechnologyState>, private renderer: Renderer2) {
31+
this.renderer.listen('window', 'click', (e: Event) => {
32+
if (this.showList && !this.list.nativeElement.contains(e.target)) {
33+
this.showList = false;
34+
}
35+
});
36+
}
37+
38+
queryTechnologies(event: Event) {
39+
const inputElement: HTMLInputElement = (event.target) as HTMLInputElement;
40+
const query: string = inputElement.value;
41+
if (query.length >= this.MIN_QUERY_LENGTH) {
42+
this.showList = true;
43+
this.store.dispatch(new actions.FindTechnology(query));
44+
}
45+
}
46+
47+
ngOnInit(): void {
48+
const technologies$ = this.store.pipe(select(allTechnologies));
49+
technologies$.subscribe((response) => {
50+
this.isLoading = response.isLoading;
51+
const filteredItems = response.technologyList.items.filter(item => !this.selectedTechnologies.includes(item.name));
52+
this.technology = {items: filteredItems};
53+
});
54+
}
55+
56+
addTechnology(name: string) {
57+
const index = this.selectedTechnologies.indexOf(name);
58+
if (index > -1) {
59+
this.removeTechnology(index);
60+
} else if (this.selectedTechnologies.length < this.MAX_NUM_TECHNOLOGIES) {
61+
this.selectedTechnologies = [...this.selectedTechnologies, name];
62+
this.technologyAdded.emit(this.selectedTechnologies);
63+
this.showList = false;
64+
this.query = '';
65+
}
66+
}
67+
68+
removeTechnology(index: number) {
69+
this.selectedTechnologies = this.selectedTechnologies.filter((item) => item !== this.selectedTechnologies[index]);
70+
this.technologyRemoved.emit(this.selectedTechnologies);
71+
}
72+
}

src/app/modules/time-clock/components/entry-fields/entry-fields.component.html

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,23 @@
11
<form [formGroup]="entryForm" (ngSubmit)="onSubmit()">
22
<div class="input-group input-group-sm mb-3">
33
<div class="input-group-prepend">
4-
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Activity</span>
4+
<span class="input-group-text span-width">Activity</span>
55
</div>
66
<select id="activitiesSelect" (blur)="onSubmit()" class="form-control">
77
<option *ngFor="let activity of activities">{{ activity.name }}</option>
88
</select>
99
</div>
1010
<div class="input-group input-group-sm mb-3">
1111
<div class="input-group-prepend">
12-
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Ticket URI</span>
12+
<span class="input-group-text span-width">Ticket URI</span>
1313
</div>
14-
<input (blur)="onSubmit()" type="text" id="uri" formControlName="uri" class="form-control" aria-label="Small" aria-describedby="inputGroup-sizing-sm" />
15-
</div>
16-
17-
<div class="input-group input-group-sm">
18-
<div class="input-group-prepend">
19-
<span class="input-group-text span-width" id="inputGroup-sizing-sm">Technology</span>
20-
</div>
21-
<input
22-
(keyup)="getTechnologies($event.target.value)"
23-
id="technology"
24-
formControlName="technology"
25-
type="text"
26-
class="form-control"
27-
aria-label="Small"
28-
aria-describedby="inputGroup-sizing-sm"
29-
/>
30-
</div>
31-
32-
<div *ngIf="isLoading">LOADING...</div>
33-
<div #list *ngIf="technology && showlist" class="d-flex flex-column technology-content">
34-
<div
35-
*ngFor="let item of technology.items"
36-
(click)="setTechnology(item.name)"
37-
class="technology-list"
38-
[ngClass]="{ active: selectedTechnology && selectedTechnology.includes(item.name) }"
39-
>
40-
{{ item.name }}
41-
</div>
42-
</div>
43-
<div class="tags-content d-flex flex-wrap" *ngIf="selectedTechnology.length">
44-
<div *ngFor="let technology of selectedTechnology; let tagIndex = index" class="tag">
45-
<span class="mr-3">{{ technology }}</span>
46-
<i class="fas fa-times text-white" (click)="removeTag(tagIndex)"></i>
47-
</div>
48-
</div>
49-
50-
<div class="form-group" *ngIf="selectedTechnology.length === 0">
51-
<!-- empty-space -->
14+
<input (blur)="onSubmit()" type="text" id="uri" formControlName="uri" class="form-control" aria-label="Small"
15+
aria-describedby="inputGroup-sizing-sm"/>
5216
</div>
17+
<app-technologies (technologyAdded)="onTechnologyAdded($event)"
18+
(technologyRemoved)="onTechnologyRemoved($event)"
19+
[selectedTechnologies]="selectedTechnologies">
20+
</app-technologies>
5321

5422
<div class="form-group text-left">
5523
<label for="NotesTextarea">Description</label>

src/app/modules/time-clock/components/entry-fields/entry-fields.component.scss

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,5 @@
11
@import '../../../../../styles/colors.scss';
22

3-
@mixin tagTechnology() {
4-
background-color: $modal-button-secondary;
5-
color: #ffffff;
6-
7-
&:hover {
8-
opacity: 0.8;
9-
}
10-
}
11-
123
.span-width {
134
width: 6rem;
145
background-image: $background-pantone;
@@ -29,47 +20,6 @@
2920
color: white;
3021
}
3122

32-
.technology-content {
33-
box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08);
34-
margin: 0 0 2rem 6rem;
35-
max-height: 7.5rem;
36-
overflow-y: auto;
37-
38-
.technology-list {
39-
cursor: pointer;
40-
font-size: 0.8rem;
41-
margin-bottom: 0.1rem;
42-
padding: 0.2rem 0.5rem;
43-
44-
&:hover {
45-
opacity: 0.7;
46-
}
47-
}
48-
49-
.active {
50-
background-color: #efefef;
51-
}
52-
}
53-
54-
.tags-content {
55-
margin: 2rem 0;
56-
57-
div {
58-
@include tagTechnology();
59-
60-
border-radius: 0.2rem;
61-
box-shadow: 2px 2px 5px 0px rgba(0, 0, 0, 0.75);
62-
font-size: 0.8rem;
63-
padding: 0.1rem 1rem 0.2rem 1.5rem;
64-
position: relative;
65-
margin: 0 0.5rem 0.5rem 0;
66-
67-
i {
68-
cursor: pointer;
69-
}
70-
}
71-
}
72-
7323
.ng-autocomplete {
7424
width: 100%;
7525
}

0 commit comments

Comments
 (0)