diff --git a/projects/netgrif-components-core/src/lib/authentication/authentication.module.ts b/projects/netgrif-components-core/src/lib/authentication/authentication.module.ts index 160aaaf5e..13bea4a6d 100644 --- a/projects/netgrif-components-core/src/lib/authentication/authentication.module.ts +++ b/projects/netgrif-components-core/src/lib/authentication/authentication.module.ts @@ -6,8 +6,9 @@ import {authenticationServiceFactory} from './authentication.factory'; import {ConfigurationService} from '../configuration/configuration.service'; import {AuthenticationMethodService} from './services/authentication-method.service'; import {OverlayModule} from '@angular/cdk/overlay'; -import {MatProgressSpinnerModule, MatSpinner} from '@angular/material/progress-spinner'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; import {AnonymousAuthenticationInterceptor} from './services/anonymous-authentication-interceptor'; +import { TimezoneInterceptor } from './services/timezone-interceptor'; @NgModule({ @@ -24,6 +25,7 @@ import {AnonymousAuthenticationInterceptor} from './services/anonymous-authentic providers: [ { provide: HTTP_INTERCEPTORS, useClass: AuthenticationInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: AnonymousAuthenticationInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: TimezoneInterceptor, multi: true }, { provide: AuthenticationMethodService, useFactory: authenticationServiceFactory, deps: [ConfigurationService, HttpClient] }, // AuthenticationEffects ] diff --git a/projects/netgrif-components-core/src/lib/authentication/services/timezone-interceptor.spec.ts b/projects/netgrif-components-core/src/lib/authentication/services/timezone-interceptor.spec.ts new file mode 100644 index 000000000..a03816a6b --- /dev/null +++ b/projects/netgrif-components-core/src/lib/authentication/services/timezone-interceptor.spec.ts @@ -0,0 +1,52 @@ +import {inject, TestBed} from '@angular/core/testing'; +import {ConfigurationService} from '../../configuration/configuration.service'; +import {TestConfigurationService} from '../../utility/tests/test-config'; +import {HTTP_INTERCEPTORS, HttpClient, HttpHeaders} from '@angular/common/http'; +import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; +import {RouterTestingModule} from '@angular/router/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {LoggerService} from '../../logger/services/logger.service'; +import { TimezoneInterceptor } from './timezone-interceptor'; + +describe('TimezoneInterceptor', () => { + let warnSpy: jasmine.Spy; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, NoopAnimationsModule, RouterTestingModule.withRoutes([])], + providers: [ + {provide: ConfigurationService, useClass: TestConfigurationService}, + { + provide: HTTP_INTERCEPTORS, + useClass: TimezoneInterceptor, + multi: true + } + ] + }); + warnSpy = spyOn(TestBed.inject(LoggerService), 'warn'); + }); + + describe('intercept HTTP request', () => { + it('should add timezone to Headers', (done) => { + inject([HttpClient, HttpTestingController], + (http: HttpClient, mock: HttpTestingController) => { + + http.get('/api').subscribe(response => { + expect(response).toBeTruthy(); + done(); + }); + const request = mock.expectOne(req => (req.headers.has('X-Timezone-Offset'))); + request.flush({data: 'test'}, {headers: new HttpHeaders({'X-Timezone-Offset': '-120'})}); + mock.verify(); + })(); + }); + afterEach(inject([HttpTestingController], (mock: HttpTestingController) => { + mock.verify(); + TestBed.resetTestingModule(); + })); + }); + + afterEach(() => { + TestBed.resetTestingModule(); + }); +}); diff --git a/projects/netgrif-components-core/src/lib/authentication/services/timezone-interceptor.ts b/projects/netgrif-components-core/src/lib/authentication/services/timezone-interceptor.ts new file mode 100644 index 000000000..2f0dfc087 --- /dev/null +++ b/projects/netgrif-components-core/src/lib/authentication/services/timezone-interceptor.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +@Injectable() +export class TimezoneInterceptor implements HttpInterceptor { + + private readonly TIMEZONE_OFFSET_HEADER_NAME = 'X-Timezone-Offset' + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + req = req.clone({ + headers: req.headers.set(this.TIMEZONE_OFFSET_HEADER_NAME, this.getTimeZoneOffset()) + }); + return next.handle(req) + } + + private getTimeZoneOffset(): string { + return (new Date()).getTimezoneOffset().toString(); + } + +} diff --git a/projects/netgrif-components-core/src/lib/data-fields/date-field/date-default-field/abstract-date-default-field.component.ts b/projects/netgrif-components-core/src/lib/data-fields/date-field/date-default-field/abstract-date-default-field.component.ts index d98161505..ef0c52bf9 100644 --- a/projects/netgrif-components-core/src/lib/data-fields/date-field/date-default-field/abstract-date-default-field.component.ts +++ b/projects/netgrif-components-core/src/lib/data-fields/date-field/date-default-field/abstract-date-default-field.component.ts @@ -14,10 +14,10 @@ import {LanguageService} from "../../../translate/language.service"; }) export abstract class AbstractDateDefaultFieldComponent extends AbstractTimeInstanceFieldComponent { - constructor(protected _translate: TranslateService, - protected _adapter: DateAdapter, - @Inject(MAT_DATE_LOCALE) protected _locale: string, - protected _languageService: LanguageService, + constructor(_translate: TranslateService, + _adapter: DateAdapter, + @Inject(MAT_DATE_LOCALE) _locale: string, + _languageService: LanguageService, @Optional() @Inject(DATA_FIELD_PORTAL_DATA) dataFieldPortalData: DataFieldPortalData) { super(_translate, _adapter, _locale, _languageService, dataFieldPortalData) } diff --git a/projects/netgrif-components-core/src/lib/data-fields/date-time-field/date-time-default-field/abstract-date-time-default-field.component.ts b/projects/netgrif-components-core/src/lib/data-fields/date-time-field/date-time-default-field/abstract-date-time-default-field.component.ts index 0c6517d1c..bcdfa156f 100644 --- a/projects/netgrif-components-core/src/lib/data-fields/date-time-field/date-time-default-field/abstract-date-time-default-field.component.ts +++ b/projects/netgrif-components-core/src/lib/data-fields/date-time-field/date-time-default-field/abstract-date-time-default-field.component.ts @@ -15,8 +15,8 @@ import {LanguageService} from "../../../translate/language.service"; }) export abstract class AbstractDateTimeDefaultFieldComponent extends AbstractTimeInstanceFieldComponent { - constructor(protected _translate: TranslateService, - protected _adapter: NgxMatDateAdapter, + constructor(_translate: TranslateService, + _adapter: NgxMatDateAdapter, @Inject(MAT_DATE_LOCALE) protected _locale: string, protected _languageService: LanguageService, @Optional() @Inject(DATA_FIELD_PORTAL_DATA) dataFieldPortalData: DataFieldPortalData) { diff --git a/projects/netgrif-components-core/src/lib/data-fields/date-time-field/zoned-date-time-field/abstract-zoned-date-time-field.component.spec.ts b/projects/netgrif-components-core/src/lib/data-fields/date-time-field/zoned-date-time-field/abstract-zoned-date-time-field.component.spec.ts new file mode 100644 index 000000000..a871679ef --- /dev/null +++ b/projects/netgrif-components-core/src/lib/data-fields/date-time-field/zoned-date-time-field/abstract-zoned-date-time-field.component.spec.ts @@ -0,0 +1,97 @@ +import {ComponentFixture, TestBed, waitForAsync} from '@angular/core/testing'; +import {AngularResizeEventModule} from 'angular-resize-event'; +import {NgxMatDatetimePickerModule} from '@angular-material-components/datetime-picker'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import {Component, CUSTOM_ELEMENTS_SCHEMA, Inject, Optional} from '@angular/core'; +import moment from 'moment'; +import {BehaviorSubject} from 'rxjs'; +import {DateTimeField} from '../models/date-time-field'; +import {ChangedFields} from '../../models/changed-fields'; +import {TranslateService} from '@ngx-translate/core'; +import {MaterialModule} from '../../../material/material.module'; +import {TranslateLibModule} from '../../../translate/translate-lib.module'; +import {MockAuthenticationMethodService} from '../../../utility/tests/mocks/mock-authentication-method-service'; +import {AuthenticationMethodService} from '../../../authentication/services/authentication-method.service'; +import {AuthenticationService} from '../../../authentication/services/authentication/authentication.service'; +import {MockAuthenticationService} from '../../../utility/tests/mocks/mock-authentication.service'; +import {UserResourceService} from '../../../resources/engine-endpoint/user-resource.service'; +import {MockUserResourceService} from '../../../utility/tests/mocks/mock-user-resource.service'; +import {TestConfigurationService} from '../../../utility/tests/test-config'; +import {ConfigurationService} from '../../../configuration/configuration.service'; +import {NAE_INFORM_ABOUT_INVALID_DATA} from '../../models/invalid-data-policy-token'; +import { AbstractZonedDateTimeFieldComponent } from './abstract-zoned-date-time-field.component'; + +describe('AbstractZonedDateTimeFieldComponent', () => { + let component: TestDateTimeFieldComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + MaterialModule, + AngularResizeEventModule, + NgxMatDatetimePickerModule, + TranslateLibModule, + HttpClientTestingModule, + NoopAnimationsModule + ], + providers: [ + {provide: AuthenticationMethodService, useClass: MockAuthenticationMethodService}, + {provide: AuthenticationService, useClass: MockAuthenticationService}, + {provide: UserResourceService, useClass: MockUserResourceService}, + {provide: ConfigurationService, useClass: TestConfigurationService} + ], + declarations: [ + TestDateTimeFieldComponent, + TestWrapperComponent + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }).compileComponents(); + fixture = TestBed.createComponent(TestWrapperComponent); + component = fixture.debugElement.children[0].componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should get error message', () => { + expect(component.getErrorMessage()).toEqual('This is custom message!'); + }); + + afterEach(() => { + TestBed.resetTestingModule(); + }); +}); + +@Component({ + selector: 'ncc-zoned-test-date-time', + template: '' +}) +class TestDateTimeFieldComponent extends AbstractZonedDateTimeFieldComponent { + constructor(translate: TranslateService, + @Optional() @Inject(NAE_INFORM_ABOUT_INVALID_DATA) informAboutInvalidData: boolean | null) { + super(translate, informAboutInvalidData); + } +} + +@Component({ + selector: 'ncc-test-wrapper', + template: '' +}) +class TestWrapperComponent { + field = new DateTimeField('', '', moment('2020-03-09'), { + required: true, + optional: true, + visible: true, + editable: true, + hidden: true + }, undefined, undefined, undefined, [ + {validationRule: 'between today,future', validationMessage: 'This is custom message!'}, + {validationRule: 'between past,today', validationMessage: 'This is custom message!'}, + {validationRule: 'between 2020-03-03,today', validationMessage: 'This is custom message!'}, + ]); + changedFields = new BehaviorSubject({behavior: {editable: true}, taskId: 'testTaskId'}); +} diff --git a/projects/netgrif-components-core/src/lib/data-fields/date-time-field/zoned-date-time-field/abstract-zoned-date-time-field.component.ts b/projects/netgrif-components-core/src/lib/data-fields/date-time-field/zoned-date-time-field/abstract-zoned-date-time-field.component.ts new file mode 100644 index 000000000..89b3b87e4 --- /dev/null +++ b/projects/netgrif-components-core/src/lib/data-fields/date-time-field/zoned-date-time-field/abstract-zoned-date-time-field.component.ts @@ -0,0 +1,33 @@ +import {Component, Inject, Optional} from '@angular/core'; +import {TranslateService} from '@ngx-translate/core'; +import { + AbstractDateDefaultFieldComponent +} from "../../date-field/date-default-field/abstract-date-default-field.component"; +import {DateAdapter, MAT_DATE_LOCALE} from "@angular/material/core"; +import {LanguageService} from "../../../translate/language.service"; +import {DATA_FIELD_PORTAL_DATA, DataFieldPortalData} from "../../models/data-field-portal-data-injection-token"; +import {DateField} from "../../date-field/models/date-field"; +import {DateTimeField} from "../models/date-time-field"; +import { + AbstractDateTimeDefaultFieldComponent +} from "../date-time-default-field/abstract-date-time-default-field.component"; +import {NgxMatDateAdapter} from "@angular-material-components/datetime-picker"; + + +@Component({ + selector: 'ncc-abstract-date-time-field', + template: '' +}) +export abstract class AbstractZonedDateTimeFieldComponent extends AbstractDateTimeDefaultFieldComponent { + + public timeZone: string; + + constructor(_translate: TranslateService, + _adapter: NgxMatDateAdapter, + @Inject(MAT_DATE_LOCALE) _locale: string, + _languageService: LanguageService, + @Optional() @Inject(DATA_FIELD_PORTAL_DATA) dataFieldPortalData: DataFieldPortalData) { + super(_translate, _adapter, _locale, _languageService, dataFieldPortalData) + this.timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + } +} diff --git a/projects/netgrif-components-core/src/lib/data-fields/public-api.ts b/projects/netgrif-components-core/src/lib/data-fields/public-api.ts index 26396ea66..3cc65a8e8 100644 --- a/projects/netgrif-components-core/src/lib/data-fields/public-api.ts +++ b/projects/netgrif-components-core/src/lib/data-fields/public-api.ts @@ -58,6 +58,7 @@ export * from './i18n-field/i18n-text-field/abstract-i18n-text-field.component'; export * from './i18n-field/i18n-divider-field/abstract-i18n-divider-field.component'; export * from './i18n-field/abstract-i18n-errors.component'; export * from './user-list-field/abstract-user-list-field.component'; +export * from './date-time-field/zoned-date-time-field/abstract-zoned-date-time-field.component'; export * from './user-list-field/user-list-default-field/abstract-user-list-default-field.component'; export * from './task-ref-field/abstract-task-ref-field.component'; export * from './task-ref-field/task-ref-dashboard-field/task-ref-dashboard-tile/abstract-task-ref-dashboard-tile.component'; diff --git a/projects/netgrif-components/src/lib/data-fields/data-fields.module.ts b/projects/netgrif-components/src/lib/data-fields/data-fields.module.ts index d25bd9c5e..6b7323aa5 100644 --- a/projects/netgrif-components/src/lib/data-fields/data-fields.module.ts +++ b/projects/netgrif-components/src/lib/data-fields/data-fields.module.ts @@ -80,6 +80,7 @@ import { MultichoiceAutocompleteFieldComponent } from './multichoice-field/multichoice-autocomplete-field/multichoice-autocomplete-field.component'; import { UserListFieldComponent } from './user-list-field/user-list-field.component'; +import { ZonedDateTimeFieldComponent } from './date-time-field/zoned-date-time-field/zoned-date-time-field.component'; import { SideMenuMultiUserAssignComponentModule } from "../side-menu/content-components/multi-user-assign/side-menu-multi-user-assign-component.module"; @@ -188,7 +189,8 @@ import { StringCollectionDefaultFieldComponent } from './string-collection-field CaseRefDefaultComponent, MultichoiceCaserefFieldComponent, EnumerationCaserefFieldComponent, - StringCollectionDefaultFieldComponent + StringCollectionDefaultFieldComponent, + ZonedDateTimeFieldComponent ], exports: [ DataFieldTemplateComponent @@ -228,6 +230,7 @@ export class DataFieldsComponentModule { registry.register("button-default", (injector: Injector) => new ComponentPortal(ButtonDefaultFieldComponent, null, injector)); registry.register("date-default", (injector: Injector) => new ComponentPortal(DateDefaultFieldComponent, null, injector)); registry.register("date-time-default", (injector: Injector) => new ComponentPortal(DateTimeDefaultFieldComponent, null, injector)); + registry.register("date-time-timeZone", (injector: Injector) => new ComponentPortal(DateTimeDefaultFieldComponent, null, injector)); registry.register("enumeration-default", (injector: Injector) => new ComponentPortal(EnumerationSelectFieldComponent, null, injector)); registry.register("enumeration-autocomplete_dynamic", (injector: Injector) => new ComponentPortal(EnumerationAutocompleteDynamicFieldComponent, null, injector)); registry.register("enumeration-autocomplete", (injector: Injector) => new ComponentPortal(EnumerationAutocompleteSelectFieldComponent, null, injector)); diff --git a/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.html b/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.html new file mode 100644 index 000000000..3dd335a7a --- /dev/null +++ b/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.html @@ -0,0 +1,22 @@ + + {{dataField.title}} + + + + + Time zone: {{this.timeZone}} {{!!dataField.description ? '-' + dataField.description : ''}} + {{getErrorMessage()}} + diff --git a/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.scss b/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.scss new file mode 100644 index 000000000..94f24bbe9 --- /dev/null +++ b/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.scss @@ -0,0 +1,5 @@ +.full-width { + display: block; + margin: 0 auto; + width: 100%; +} diff --git a/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.spec.ts b/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.spec.ts new file mode 100644 index 000000000..659a4a7b9 --- /dev/null +++ b/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.spec.ts @@ -0,0 +1,89 @@ +import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; +import {AngularResizeEventModule} from 'angular-resize-event'; +import {DataFieldTemplateComponent} from '../../data-field-template/data-field-template.component'; +import {RequiredLabelComponent} from '../../required-label/required-label.component'; +import {Component, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core'; +import moment from 'moment'; +import { NgxMatDatetimePickerModule, NgxMatNativeDateModule } from '@angular-material-components/datetime-picker'; +import {BehaviorSubject} from 'rxjs'; +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import { + AuthenticationMethodService, + AuthenticationService, + ChangedFields, + ConfigurationService, + DateTimeField, + MaterialModule, + MockAuthenticationMethodService, + MockAuthenticationService, + MockUserResourceService, + TestConfigurationService, + TranslateLibModule, + UserResourceService, + WrappedBoolean +} from '@netgrif/components-core'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; +import { ZonedDateTimeFieldComponent } from './zoned-date-time-field.component'; + +describe('ZonedDateTimeFieldComponent', () => { + let component: ZonedDateTimeFieldComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + MaterialModule, + AngularResizeEventModule, + NgxMatDatetimePickerModule, + NgxMatNativeDateModule, + TranslateLibModule, HttpClientTestingModule, NoopAnimationsModule + ], + providers: [ + {provide: AuthenticationMethodService, useClass: MockAuthenticationMethodService}, + {provide: AuthenticationService, useClass: MockAuthenticationService}, + {provide: UserResourceService, useClass: MockUserResourceService}, + {provide: ConfigurationService, useClass: TestConfigurationService} + ], + declarations: [ + ZonedDateTimeFieldComponent, + DataFieldTemplateComponent, + RequiredLabelComponent, + TestWrapperComponent, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] + }) + .compileComponents(); + fixture = TestBed.createComponent(TestWrapperComponent); + component = fixture.debugElement.children[0].componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + afterEach(() => { + TestBed.resetTestingModule(); + }); +}); + +@Component({ + selector: 'nc-test-wrapper', + template: '' +}) +class TestWrapperComponent { + field = new DateTimeField('', '', moment('2020-03-09'), { + required: true, + optional: true, + visible: true, + editable: true, + hidden: true + }, undefined, undefined, undefined, []); + changedFields = new BehaviorSubject({behavior: {editable: true}}); + showLargeLayout = new WrappedBoolean(); + + constructor() { + this.showLargeLayout.value = true; + } +} + diff --git a/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.ts b/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.ts new file mode 100644 index 000000000..c75f72264 --- /dev/null +++ b/projects/netgrif-components/src/lib/data-fields/date-time-field/zoned-date-time-field/zoned-date-time-field.component.ts @@ -0,0 +1,28 @@ +import { Component, Inject, Optional } from '@angular/core'; +import { + AbstractZonedDateTimeFieldComponent, + DATE_TIME_FORMAT, LanguageService, DATA_FIELD_PORTAL_DATA, DataFieldPortalData, DateTimeField +} from '@netgrif/components-core'; +import {NGX_MAT_DATE_FORMATS, NgxMatDateAdapter} from '@angular-material-components/datetime-picker'; +import { TranslateService } from '@ngx-translate/core'; +import {MAT_DATE_LOCALE} from "@angular/material/core"; + +@Component({ + selector: 'nc-zoned-date-time-field', + templateUrl: './zoned-date-time-field.component.html', + styleUrls: ['./zoned-date-time-field.component.scss'], + providers: [ + {provide: NGX_MAT_DATE_FORMATS, useValue: DATE_TIME_FORMAT} + ] +}) +export class ZonedDateTimeFieldComponent extends AbstractZonedDateTimeFieldComponent { + + constructor(_translate: TranslateService, + _adapter: NgxMatDateAdapter, + @Inject(MAT_DATE_LOCALE) protected _locale: string, + _languageService: LanguageService, + @Optional() @Inject(DATA_FIELD_PORTAL_DATA) dataFieldPortalData: DataFieldPortalData) { + super(_translate, _adapter, _locale, _languageService, dataFieldPortalData); + } + +}