From a86740e82a971a18122760ff2f1abcac5608d987 Mon Sep 17 00:00:00 2001 From: Shijun Sun <30999793+AllForNothing@users.noreply.github.com> Date: Fri, 6 Jan 2023 11:36:01 +0800 Subject: [PATCH] Update datePicker component (#18070) 1.Fix date validator 2.Add i18n support Signed-off-by: AllForNothing --- .../account/sign-in/sign-in.component.spec.ts | 13 +++- .../security/security.component.spec.ts | 14 ++++ .../config/security/security.component.ts | 8 +++ .../project-log/audit-log.component.spec.ts | 17 ++++- .../date-validator.directive.ts | 70 ------------------- .../datetime-picker.component.scss | 7 +- .../datetime-picker.component.spec.ts | 42 +++++++++++ .../datetime-picker.component.ts | 8 +++ .../navigator/navigator.component.html | 2 +- .../navigator/navigator.component.spec.ts | 3 + .../navigator/navigator.component.ts | 11 ++- .../directives/date-validator.directive.ts | 9 ++- .../src/app/shared/entities/shared.const.ts | 29 +++++--- src/portal/src/app/shared/shared.module.ts | 25 ------- src/portal/src/app/shared/units/utils.ts | 2 - 15 files changed, 143 insertions(+), 117 deletions(-) delete mode 100644 src/portal/src/app/shared/components/datetime-picker/date-validator.directive.ts create mode 100644 src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.spec.ts diff --git a/src/portal/src/app/account/sign-in/sign-in.component.spec.ts b/src/portal/src/app/account/sign-in/sign-in.component.spec.ts index 22f111f64..935f0563f 100644 --- a/src/portal/src/app/account/sign-in/sign-in.component.spec.ts +++ b/src/portal/src/app/account/sign-in/sign-in.component.spec.ts @@ -8,8 +8,11 @@ import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; import { of } from 'rxjs'; import { throwError as observableThrowError } from 'rxjs/internal/observable/throwError'; import { HttpErrorResponse } from '@angular/common/http'; -import { SharedTestingModule } from '../../shared/shared.module'; import { UserPermissionService } from '../../shared/services'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ClarityModule } from '@clr/angular'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; describe('SignInComponent', () => { let component: SignInComponent; @@ -27,9 +30,15 @@ describe('SignInComponent', () => { }; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SharedTestingModule], + imports: [ + TranslateModule.forRoot(), + RouterTestingModule, + ClarityModule, + FormsModule, + ], declarations: [SignInComponent], providers: [ + TranslateService, { provide: UserPermissionService, useValue: mockedUserPermissionService, diff --git a/src/portal/src/app/base/left-side-nav/config/security/security.component.spec.ts b/src/portal/src/app/base/left-side-nav/config/security/security.component.spec.ts index eecedc934..243277ddf 100644 --- a/src/portal/src/app/base/left-side-nav/config/security/security.component.spec.ts +++ b/src/portal/src/app/base/left-side-nav/config/security/security.component.spec.ts @@ -4,6 +4,9 @@ import { ErrorHandler } from '../../../../shared/units/error-handler'; import { of } from 'rxjs'; import { SharedTestingModule } from '../../../../shared/shared.module'; import { SecurityComponent } from './security.component'; +import { LOCALE_ID } from '@angular/core'; +import { registerLocaleData } from '@angular/common'; +import locale_en from '@angular/common/locales/en'; describe('SecurityComponent', () => { let component: SecurityComponent; let fixture: ComponentFixture; @@ -26,7 +29,18 @@ describe('SecurityComponent', () => { return null; }, }; + registerLocaleData(locale_en, 'en-us'); beforeEach(() => { + TestBed.overrideComponent(SecurityComponent, { + set: { + providers: [ + { + provide: LOCALE_ID, + useValue: 'en-us', + }, + ], + }, + }); TestBed.configureTestingModule({ imports: [SharedTestingModule], providers: [ diff --git a/src/portal/src/app/base/left-side-nav/config/security/security.component.ts b/src/portal/src/app/base/left-side-nav/config/security/security.component.ts index 0ea5ee809..6dd5cf5ff 100644 --- a/src/portal/src/app/base/left-side-nav/config/security/security.component.ts +++ b/src/portal/src/app/base/left-side-nav/config/security/security.component.ts @@ -1,6 +1,7 @@ import { Component, ElementRef, + LOCALE_ID, OnDestroy, OnInit, ViewChild, @@ -10,6 +11,7 @@ import { ErrorHandler } from '../../../../shared/units/error-handler'; import { ConfirmationState, ConfirmationTargets, + DEFAULT_LANG_LOCALSTORAGE_KEY, } from '../../../../shared/entities/shared.const'; import { SystemCVEAllowlist, @@ -28,6 +30,12 @@ const TARGET_BLANK = '_blank'; selector: 'app-security', templateUrl: './security.component.html', styleUrls: ['./security.component.scss'], + providers: [ + { + provide: LOCALE_ID, + useValue: localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY), + }, + ], }) export class SecurityComponent implements OnInit, OnDestroy { onGoing = false; diff --git a/src/portal/src/app/base/project/project-log/audit-log.component.spec.ts b/src/portal/src/app/base/project/project-log/audit-log.component.spec.ts index 0ae178dd6..2f27f7185 100644 --- a/src/portal/src/app/base/project/project-log/audit-log.component.spec.ts +++ b/src/portal/src/app/base/project/project-log/audit-log.component.spec.ts @@ -3,13 +3,16 @@ import { AuditLogComponent } from './audit-log.component'; import { MessageHandlerService } from '../../../shared/services/message-handler.service'; import { ActivatedRoute, Router } from '@angular/router'; import { of } from 'rxjs'; -import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, LOCALE_ID } from '@angular/core'; import { delay } from 'rxjs/operators'; import { AuditLog } from '../../../../../ng-swagger-gen/models/audit-log'; import { HttpHeaders, HttpResponse } from '@angular/common/http'; import { ProjectService } from '../../../../../ng-swagger-gen/services/project.service'; import { click } from '../../../shared/units/utils'; import { SharedTestingModule } from '../../../shared/shared.module'; +import { registerLocaleData } from '@angular/common'; +import locale_en from '@angular/common/locales/en'; +import { DatePickerComponent } from '../../../shared/components/datetime-picker/datetime-picker.component'; describe('AuditLogComponent', () => { let component: AuditLogComponent; @@ -78,8 +81,18 @@ describe('AuditLogComponent', () => { } }, }; - + registerLocaleData(locale_en, 'en-us'); beforeEach(async () => { + TestBed.overrideComponent(DatePickerComponent, { + set: { + providers: [ + { + provide: LOCALE_ID, + useValue: 'en-us', + }, + ], + }, + }); await TestBed.configureTestingModule({ schemas: [CUSTOM_ELEMENTS_SCHEMA], imports: [SharedTestingModule], diff --git a/src/portal/src/app/shared/components/datetime-picker/date-validator.directive.ts b/src/portal/src/app/shared/components/datetime-picker/date-validator.directive.ts deleted file mode 100644 index 03294e494..000000000 --- a/src/portal/src/app/shared/components/datetime-picker/date-validator.directive.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright Project Harbor Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -import { Directive, OnChanges, Input, SimpleChanges } from '@angular/core'; -import { - NG_VALIDATORS, - Validator, - Validators, - ValidatorFn, - AbstractControl, -} from '@angular/forms'; - -@Directive({ - selector: '[dateValidator]', - providers: [ - { - provide: NG_VALIDATORS, - useExisting: DateValidatorDirective, - multi: true, - }, - ], -}) -export class DateValidatorDirective implements Validator, OnChanges { - @Input() dateValidator: string; - private valFn = Validators.nullValidator; - - ngOnChanges(changes: SimpleChanges): void { - const change = changes['dateValidator']; - if (change) { - this.valFn = dateValidator(); - } else { - this.valFn = Validators.nullValidator; - } - } - validate(control: AbstractControl): { [key: string]: any } { - return this.valFn(control) || Validators.nullValidator; - } -} - -export function dateValidator(): ValidatorFn { - return (control: AbstractControl): { [key: string]: any } => { - let controlValue = control.value; - let valid = true; - if (controlValue) { - const regYMD = - /^(19|20)\d\d([- /.])(0[1-9]|1[012])\2(0[1-9]|[12][0-9]|3[01])$/g; - const regDMY = - /^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/g; - const regMDY = - /^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](19|20)\d\d$/g; - valid = - regYMD.test(controlValue) || - regDMY.test(controlValue) || - regMDY.test(controlValue); - } - return valid - ? Validators.nullValidator - : { dateValidator: { value: controlValue } }; - }; -} diff --git a/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.scss b/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.scss index 82380e4bf..2acce4c62 100644 --- a/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.scss +++ b/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.scss @@ -12,4 +12,9 @@ left: 75%; background-color: #c92100; } -} \ No newline at end of file + + >.tooltip-content::before { + border-left-color:#c92100; + border-top-color: #c92100; + } +} diff --git a/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.spec.ts b/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.spec.ts new file mode 100644 index 000000000..3733d50fc --- /dev/null +++ b/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.spec.ts @@ -0,0 +1,42 @@ +import { LOCALE_ID, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DatePickerComponent } from './datetime-picker.component'; +import { FormsModule } from '@angular/forms'; +import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { DateValidatorDirective } from '../../directives/date-validator.directive'; +import { registerLocaleData } from '@angular/common'; +import locale_en from '@angular/common/locales/en'; + +describe('DatePickerComponent', () => { + let component: DatePickerComponent; + let fixture: ComponentFixture; + registerLocaleData(locale_en, 'en-us'); + beforeEach(async () => { + TestBed.overrideComponent(DatePickerComponent, { + set: { + providers: [ + { + provide: LOCALE_ID, + useValue: 'en-us', + }, + ], + }, + }); + await TestBed.configureTestingModule({ + imports: [FormsModule, TranslateModule.forRoot()], + declarations: [DatePickerComponent, DateValidatorDirective], + providers: [TranslateService], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DatePickerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.ts b/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.ts index 27bf087dc..52c5ff0da 100644 --- a/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.ts +++ b/src/portal/src/app/shared/components/datetime-picker/datetime-picker.component.ts @@ -5,13 +5,21 @@ import { EventEmitter, ViewChild, OnChanges, + LOCALE_ID, } from '@angular/core'; import { NgModel } from '@angular/forms'; +import { DEFAULT_LANG_LOCALSTORAGE_KEY } from '../../entities/shared.const'; @Component({ selector: 'hbr-datetime', templateUrl: './datetime-picker.component.html', styleUrls: ['./datetime-picker.component.scss'], + providers: [ + { + provide: LOCALE_ID, + useValue: localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY), + }, + ], }) export class DatePickerComponent implements OnChanges { @Input() dateInput: string; diff --git a/src/portal/src/app/shared/components/navigator/navigator.component.html b/src/portal/src/app/shared/components/navigator/navigator.component.html index c36c6d563..7ae366ce2 100644 --- a/src/portal/src/app/shared/components/navigator/navigator.component.html +++ b/src/portal/src/app/shared/components/navigator/navigator.component.html @@ -45,7 +45,7 @@ clrDropdownItem (click)="switchLanguage(lang[0])" [class.lang-selected]="matchLang(lang[0])" - >{{ lang[1] }}{{ lang[1][0] }} diff --git a/src/portal/src/app/shared/components/navigator/navigator.component.spec.ts b/src/portal/src/app/shared/components/navigator/navigator.component.spec.ts index 31c6d34d1..96f154bd1 100644 --- a/src/portal/src/app/shared/components/navigator/navigator.component.spec.ts +++ b/src/portal/src/app/shared/components/navigator/navigator.component.spec.ts @@ -9,6 +9,9 @@ import { MessageHandlerService } from '../../services/message-handler.service'; import { SearchTriggerService } from '../global-search/search-trigger.service'; import { SkinableConfig } from '../../../services/skinable-config.service'; import { SharedTestingModule } from '../../shared.module'; +import { TranslateService } from '@ngx-translate/core'; +import { DeFaultLang } from '../../entities/shared.const'; +import { of } from 'rxjs'; describe('NavigatorComponent', () => { let component: TestComponentWrapperComponent; diff --git a/src/portal/src/app/shared/components/navigator/navigator.component.ts b/src/portal/src/app/shared/components/navigator/navigator.component.ts index 9e48b9bd4..98306b1aa 100644 --- a/src/portal/src/app/shared/components/navigator/navigator.component.ts +++ b/src/portal/src/app/shared/components/navigator/navigator.component.ts @@ -14,7 +14,7 @@ import { Component, Output, EventEmitter, OnInit } from '@angular/core'; import { Router, NavigationExtras } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { PlatformLocation } from '@angular/common'; +import { PlatformLocation, registerLocaleData } from '@angular/common'; import { ModalEvent } from '../../../base/modal-event'; import { modalEvents } from '../../../base/modal-events.const'; import { SessionService } from '../../services/session.service'; @@ -70,6 +70,12 @@ export class NavigatorComponent implements OnInit { // custom skin this.customStyle = this.skinableConfig.getSkinConfig(); this.selectedLang = this.translate.currentLang as SupportedLanguage; + if (this.selectedLang) { + registerLocaleData( + LANGUAGES[this.selectedLang][1], + this.selectedLang + ); + } this.selectedDatetimeRendering = getDatetimeRendering(); if (this.appConfigService.isIntegrationMode()) { this.appTitle = 'APP_TITLE.VIC'; @@ -91,7 +97,7 @@ export class NavigatorComponent implements OnInit { } public get currentLang(): string { - return LANGUAGES[this.selectedLang]; + return LANGUAGES[this.selectedLang][0] as string; } public get currentDatetimeRendering(): string { @@ -180,6 +186,7 @@ export class NavigatorComponent implements OnInit { // Switch languages switchLanguage(lang: SupportedLanguage): void { this.selectedLang = lang; + registerLocaleData(LANGUAGES[this.selectedLang][1], this.selectedLang); localStorage.setItem(DEFAULT_LANG_LOCALSTORAGE_KEY, lang); // due to the bug(https://github.com/ngx-translate/core/issues/1258) of translate module // have to reload diff --git a/src/portal/src/app/shared/directives/date-validator.directive.ts b/src/portal/src/app/shared/directives/date-validator.directive.ts index 4f2c97d7b..b425b87c2 100644 --- a/src/portal/src/app/shared/directives/date-validator.directive.ts +++ b/src/portal/src/app/shared/directives/date-validator.directive.ts @@ -53,10 +53,15 @@ export function dateValidator(): ValidatorFn { let valid = true; if (controlValue) { const regYMD = - /^(19|20)\d\d([- /.])(0[1-9]|1[012])\2(0[1-9]|[12][0-9]|3[01])$/g; + /^(19|20)\d\d([- /.])(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])$/g; const regDMY = /^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/g; - valid = regYMD.test(controlValue) || regDMY.test(controlValue); + const regMDY = + /^(0[1-9]|1[012])[- /.](0[1-9]|[12][0-9]|3[01])[- /.](19|20)\d\d$/g; + valid = + regYMD.test(controlValue) || + regDMY.test(controlValue) || + regMDY.test(controlValue); } return valid ? null : { dateValidator: { value: controlValue } }; }; diff --git a/src/portal/src/app/shared/entities/shared.const.ts b/src/portal/src/app/shared/entities/shared.const.ts index 58fb6b90b..3052df22b 100644 --- a/src/portal/src/app/shared/entities/shared.const.ts +++ b/src/portal/src/app/shared/entities/shared.const.ts @@ -12,6 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. +import locale_en from '@angular/common/locales/en'; +import locale_zh_CN from '@angular/common/locales/zh-Hans'; +import locale_zh_TW from '@angular/common/locales/zh-Hans-HK'; +import locale_es from '@angular/common/locales/es'; +import locale_fr from '@angular/common/locales/fr'; +import locale_pt from '@angular/common/locales/pt-PT'; +import locale_tr from '@angular/common/locales/tr'; +import locale_de from '@angular/common/locales/de'; + export const enum AlertType { DANGER, WARNING, @@ -223,16 +232,16 @@ export const REFRESH_TIME_DIFFERENCE = 10000; // export const DeFaultLang = 'en-us'; -export type SupportedLanguage = keyof typeof LANGUAGES; +export type SupportedLanguage = string; export const LANGUAGES = { - 'en-us': 'English', - 'zh-cn': '中文简体', - 'zh-tw': '中文繁體', - 'es-es': 'Español', - 'fr-fr': 'Français', - 'pt-br': 'Português do Brasil', - 'tr-tr': 'Türkçe', - 'de-de': 'Deutsch', + 'en-us': ['English', locale_en], + 'zh-cn': ['中文简体', locale_zh_CN], + 'zh-tw': ['中文繁體', locale_zh_TW], + 'es-es': ['Español', locale_es], + 'fr-fr': ['Français', locale_fr], + 'pt-br': ['Português do Brasil', locale_pt], + 'tr-tr': ['Türkçe', locale_tr], + 'de-de': ['Deutsch', locale_de], } as const; export const supportedLangs = Object.keys(LANGUAGES) as SupportedLanguage[]; /** @@ -240,7 +249,7 @@ export const supportedLangs = Object.keys(LANGUAGES) as SupportedLanguage[]; */ export const DEFAULT_LANG_LOCALSTORAGE_KEY = 'harbor-lang'; -export type DatetimeRendering = keyof typeof DATETIME_RENDERINGS; +export type DatetimeRendering = string; export const DATETIME_RENDERINGS = { 'locale-default': 'TOP_NAV.DATETIME_RENDERING_DEFAULT', 'iso-8601': 'ISO 8601', diff --git a/src/portal/src/app/shared/shared.module.ts b/src/portal/src/app/shared/shared.module.ts index f4a31a4ed..0151a347f 100644 --- a/src/portal/src/app/shared/shared.module.ts +++ b/src/portal/src/app/shared/shared.module.ts @@ -79,33 +79,8 @@ import { RouterTestingModule } from '@angular/router/testing'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { HarborDatetimePipe } from './pipes/harbor-datetime.pipe'; import { RemainingTimeComponent } from './components/remaining-time/remaining-time.component'; - -import { registerLocaleData } from '@angular/common'; -import locale_en from '@angular/common/locales/en'; -import locale_zh_CN from '@angular/common/locales/zh-Hans'; -import locale_zh_TW from '@angular/common/locales/zh-Hans-HK'; -import locale_es from '@angular/common/locales/es'; -import locale_fr from '@angular/common/locales/fr'; -import locale_pt from '@angular/common/locales/pt-PT'; -import locale_tr from '@angular/common/locales/tr'; -import locale_de from '@angular/common/locales/de'; -import { SupportedLanguage } from './entities/shared.const'; import { LabelSelectorComponent } from './components/label-selector/label-selector.component'; -const localesForSupportedLangs: Record = { - 'en-us': locale_en, - 'zh-cn': locale_zh_CN, - 'zh-tw': locale_zh_TW, - 'es-es': locale_es, - 'fr-fr': locale_fr, - 'pt-br': locale_pt, - 'tr-tr': locale_tr, - 'de-de': locale_de, -}; -for (const [lang, locale] of Object.entries(localesForSupportedLangs)) { - registerLocaleData(locale, lang); -} - // ClarityIcons is publicly accessible from the browser's window object. declare const ClarityIcons: ClarityIconsApi; diff --git a/src/portal/src/app/shared/units/utils.ts b/src/portal/src/app/shared/units/utils.ts index 9fc9414ce..a45bed002 100644 --- a/src/portal/src/app/shared/units/utils.ts +++ b/src/portal/src/app/shared/units/utils.ts @@ -17,8 +17,6 @@ import { import { AbstractControl } from '@angular/forms'; import { isValidCron } from 'cron-validator'; import { ClrDatagridStateInterface } from '@clr/angular'; -import { ScheduleListComponent } from '../../base/left-side-nav/job-service-dashboard/schedule-list/schedule-list.component'; -import { PendingListComponent } from '../../base/left-side-nav/job-service-dashboard/pending-job-list/pending-job-list.component'; /** * Api levels