diff --git a/README.md b/README.md index 99859696f..7460be3b2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Time-Tracker-UI +# Time-Tracker-UI v1.50.6 This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 9.0.4. diff --git a/package-lock.json b/package-lock.json index ada2b395b..51b02e338 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.50.5", + "version": "1.51.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 72af7e891..7719570cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "time-tracker", - "version": "1.50.5", + "version": "1.51.1", "scripts": { "preinstall": "npx npm-force-resolutions", "ng": "ng", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 2fb23cb82..4ac8a1693 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -86,6 +86,7 @@ import { TechnologyReportComponent } from './modules/technology-report/pages/tec import { CalendarComponent } from './modules/time-entries/components/calendar/calendar.component'; import { DropdownComponent } from './modules/shared/components/dropdown/dropdown.component'; import { NgSelectModule } from '@ng-select/ng-select'; +import { DarkModeComponent } from './modules/shared/components/dark-mode/dark-mode.component'; const maskConfig: Partial = { validation: false, @@ -140,6 +141,7 @@ const maskConfig: Partial = { TechnologyReportTableComponent, CalendarComponent, DropdownComponent, + DarkModeComponent, ], imports: [ NgxMaskModule.forRoot(maskConfig), diff --git a/src/app/modules/customer-management/components/customer-info/components/customer-list/customer-list.component.ts b/src/app/modules/customer-management/components/customer-info/components/customer-list/customer-list.component.ts index ecf91a753..c47f51ba7 100644 --- a/src/app/modules/customer-management/components/customer-info/components/customer-list/customer-list.component.ts +++ b/src/app/modules/customer-management/components/customer-info/components/customer-list/customer-list.component.ts @@ -70,12 +70,6 @@ export class CustomerListComponent implements OnInit, OnDestroy, AfterViewInit { }, ]; - this.dtOptions = { - scrollY: '325px', - paging: false, - responsive: true, - }; - const customerIdToEdit$ = this.store.pipe(select(customerIdtoEdit)); this.customerIdToEditSubscription = customerIdToEdit$.subscribe((customerId: string) => { this.currentCustomerIdToEdit = customerId; diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts index 27cc614ea..8cbdee2d9 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.spec.ts @@ -12,6 +12,7 @@ describe('Reports Page', () => { let fixture: ComponentFixture; let store: MockStore; let getReportDataSourceSelectorMock; + let durationTime: number; const timeEntry: Entry = { id: '123', start_date: new Date(), @@ -62,6 +63,10 @@ describe('Reports Page', () => { }) ); + beforeEach(() => { + durationTime = new Date().setHours(5, 30); + }); + it('component should be created', async () => { expect(component).toBeTruthy(); }); @@ -107,6 +112,14 @@ describe('Reports Page', () => { }); }); + it('The data should be displayed as a multiple of hour when column is equal to 3', () => { + expect(component.bodyExportOptions(durationTime, 0, 3, 0)).toMatch(/^[+-]?([0-9]+\.?[0-9]*|\.[0-9]+)$/); + }); + + it('The data should not be displayed as a multiple of hour when column is different of 3', () => { + expect(component.bodyExportOptions(durationTime, 0, 4, 0)).toBe(durationTime); + }); + afterEach(() => { fixture.destroy(); }); diff --git a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts index 5ac1ca58b..3ed1dedc4 100644 --- a/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts +++ b/src/app/modules/reports/components/time-entries-table/time-entries-table.component.ts @@ -2,6 +2,7 @@ import { formatDate } from '@angular/common'; import { AfterViewInit, Component, OnDestroy, OnInit, ViewChild} from '@angular/core'; import { select, Store } from '@ngrx/store'; import { DataTableDirective } from 'angular-datatables'; +import * as moment from 'moment'; import { Observable, Subject, Subscription } from 'rxjs'; import { Entry } from 'src/app/modules/shared/models'; import { DataSource } from 'src/app/modules/shared/models/data-source.model'; @@ -28,11 +29,21 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn }, { extend: 'excel', + exportOptions: { + format: { + body: this.bodyExportOptions + } + }, text: 'Excel', filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}` }, { extend: 'csv', + exportOptions: { + format: { + body: this.bodyExportOptions + } + }, text: 'CSV', filename: `time-entries-${formatDate(new Date(), 'MM_dd_yyyy-HH_mm', 'en')}` }, @@ -83,4 +94,10 @@ export class TimeEntriesTableComponent implements OnInit, OnDestroy, AfterViewIn const regex = new RegExp('http*', 'g'); return regex.test(uri); } + + bodyExportOptions(data, row, column, node){ + console.log(data); + const durationColumnIndex = 3; + return column === durationColumnIndex ? moment.duration(data).asHours().toFixed(2) : data; + } } diff --git a/src/app/modules/shared/components/dark-mode/dark-mode.component.html b/src/app/modules/shared/components/dark-mode/dark-mode.component.html new file mode 100644 index 000000000..df5a11423 --- /dev/null +++ b/src/app/modules/shared/components/dark-mode/dark-mode.component.html @@ -0,0 +1,10 @@ + diff --git a/src/app/modules/shared/components/dark-mode/dark-mode.component.spec.ts b/src/app/modules/shared/components/dark-mode/dark-mode.component.spec.ts new file mode 100644 index 000000000..900854ed9 --- /dev/null +++ b/src/app/modules/shared/components/dark-mode/dark-mode.component.spec.ts @@ -0,0 +1,96 @@ +import { ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import { of } from 'rxjs'; +import { delay } from 'rxjs/operators'; +import { FeatureToggleGeneralService } from '../../feature-toggles/feature-toggle-general/feature-toggle-general.service'; +import { FeatureToggleModel } from '../../feature-toggles/feature-toggle.model'; +import { FeatureFilterModel } from '../../feature-toggles/filters/feature-filter.model'; +import { DarkModeComponent } from './dark-mode.component'; + +describe('DarkModeComponent', () => { + let component: DarkModeComponent; + let fixture: ComponentFixture; + let html: HTMLElement; + let featureToggleGeneralService: FeatureToggleGeneralService; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DarkModeComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DarkModeComponent); + component = fixture.componentInstance; + html = document.documentElement; + featureToggleGeneralService = TestBed.inject(FeatureToggleGeneralService); + fixture.detectChanges(); + }); + + afterEach(() => { + localStorage.removeItem('theme'); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); + + it('should be light the default theme', () => { + expect(component.theme).toEqual('light'); + }); + + it('should change the value of the theme property if it exists in the local storage', () => { + localStorage.setItem('theme', 'dark'); + component.checkThemeInLocalStorage(); + expect(component.theme).toEqual('dark'); + }); + + it('should be light theme if it does not exist in local storage', () => { + component.checkThemeInLocalStorage(); + expect(component.theme).toEqual('light'); + }); + + it('should be light mode if property ‘theme’ does not have a value in local storage', () => { + localStorage.setItem('theme', ''); + component.checkThemeInLocalStorage(); + expect(component.theme).toEqual('light'); + }); + + it('should switch to dark theme if the theme property is light and vice versa', () => { + component.theme = component.setTheme(); + expect(component.theme).toEqual('dark'); + component.theme = component.setTheme(); + expect(component.theme).toEqual('light'); + }); + + it('should add the dark class in the html tag to apply dark mode', () => { + component.theme = 'dark'; + component.addOrRemoveDarkMode(); + fixture.detectChanges(); + expect(html.classList.contains('dark')).toBe(true); + }); + + it('should not have dark class in the html tag when theme is light', () => { + component.addOrRemoveDarkMode(); + fixture.detectChanges(); + expect(component.theme).toEqual('light'); + expect(html.classList.contains('dark')).toBe(false); + }); + + it('should change the value of the theme property, save it in the local storage and add the dark class to the HTML tag to change the theme', () => { + component.changeToDarkOrLightTheme(); + fixture.detectChanges(); + expect(component.theme).toEqual('dark'); + expect(localStorage.getItem('theme')).toEqual('dark'); + expect(html.classList.contains('dark')).toBe(true); + }); + + it('should be true the isFeatureToggleDarkModeActive property when the user has the dark-mode feature toggle enabled', fakeAsync(() => { + const filters: FeatureFilterModel[] = []; + const featureToggle: FeatureToggleModel = {name: 'dark-mode', enabled: true, filters}; + spyOn(featureToggleGeneralService, 'getActivated').and.returnValue(of([featureToggle]).pipe(delay(1))); + component.ngOnInit(); + tick(1); + expect(component.isFeatureToggleDarkModeActive).toBeTruthy(); + })); + +}); diff --git a/src/app/modules/shared/components/dark-mode/dark-mode.component.ts b/src/app/modules/shared/components/dark-mode/dark-mode.component.ts new file mode 100644 index 000000000..58ff4691f --- /dev/null +++ b/src/app/modules/shared/components/dark-mode/dark-mode.component.ts @@ -0,0 +1,65 @@ +import { Component, OnInit } from '@angular/core'; +import { FeatureToggle } from 'src/environments/enum'; +import { FeatureToggleGeneralService } from '../../feature-toggles/feature-toggle-general/feature-toggle-general.service'; + +@Component({ + selector: 'app-dark-mode', + templateUrl: './dark-mode.component.html', +}) +export class DarkModeComponent implements OnInit { + public theme = 'light'; + public isFeatureToggleDarkModeActive: boolean; + + constructor( + private featureToggleGeneralService: FeatureToggleGeneralService + ) {} + + ngOnInit() { + this.featureToggleGeneralService.getActivated().subscribe((featuresToggles) => { + const darkModeToggle = featuresToggles.find( (item) => item.name === FeatureToggle.DARK_MODE); + this.isFeatureToggleDarkModeActive = darkModeToggle.enabled; + if (this.isFeatureToggleDarkModeActive) { + this.checkThemeInLocalStorage(); + this.addOrRemoveDarkMode(); + } + }); + } + + getLocalStorageTheme(): string { + return localStorage.getItem('theme') || 'light'; + } + + setLocalStorageTheme(theme: string): void { + localStorage.setItem('theme', theme); + } + + checkThemeInLocalStorage(): void { + if ('theme' in localStorage) { + this.theme = this.getLocalStorageTheme(); + } else { + this.setLocalStorageTheme(this.theme); + } + } + + isDarkTheme(): boolean { + return this.theme === 'dark' ? true : false; + } + + setTheme(): string { + return this.isDarkTheme() ? 'light' : 'dark'; + } + + addOrRemoveDarkMode(): void { + if (this.isDarkTheme()) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.remove('dark'); + } + } + + changeToDarkOrLightTheme(): void { + this.theme = this.setTheme(); + this.setLocalStorageTheme(this.theme); + this.addOrRemoveDarkMode(); + } +} diff --git a/src/app/modules/shared/components/sidebar/sidebar.component.html b/src/app/modules/shared/components/sidebar/sidebar.component.html index b18c05e2d..8cb6e71c3 100644 --- a/src/app/modules/shared/components/sidebar/sidebar.component.html +++ b/src/app/modules/shared/components/sidebar/sidebar.component.html @@ -1,38 +1,50 @@ -
-