Update datePicker component (#18070)

1.Fix date validator
2.Add i18n support

Signed-off-by: AllForNothing <sshijun@vmware.com>
This commit is contained in:
Shijun Sun 2023-01-06 11:36:01 +08:00 committed by GitHub
parent e995b59a94
commit a86740e82a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 143 additions and 117 deletions

View File

@ -8,8 +8,11 @@ import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { throwError as observableThrowError } from 'rxjs/internal/observable/throwError'; import { throwError as observableThrowError } from 'rxjs/internal/observable/throwError';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { SharedTestingModule } from '../../shared/shared.module';
import { UserPermissionService } from '../../shared/services'; 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', () => { describe('SignInComponent', () => {
let component: SignInComponent; let component: SignInComponent;
@ -27,9 +30,15 @@ describe('SignInComponent', () => {
}; };
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [SharedTestingModule], imports: [
TranslateModule.forRoot(),
RouterTestingModule,
ClarityModule,
FormsModule,
],
declarations: [SignInComponent], declarations: [SignInComponent],
providers: [ providers: [
TranslateService,
{ {
provide: UserPermissionService, provide: UserPermissionService,
useValue: mockedUserPermissionService, useValue: mockedUserPermissionService,

View File

@ -4,6 +4,9 @@ import { ErrorHandler } from '../../../../shared/units/error-handler';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { SharedTestingModule } from '../../../../shared/shared.module'; import { SharedTestingModule } from '../../../../shared/shared.module';
import { SecurityComponent } from './security.component'; 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', () => { describe('SecurityComponent', () => {
let component: SecurityComponent; let component: SecurityComponent;
let fixture: ComponentFixture<SecurityComponent>; let fixture: ComponentFixture<SecurityComponent>;
@ -26,7 +29,18 @@ describe('SecurityComponent', () => {
return null; return null;
}, },
}; };
registerLocaleData(locale_en, 'en-us');
beforeEach(() => { beforeEach(() => {
TestBed.overrideComponent(SecurityComponent, {
set: {
providers: [
{
provide: LOCALE_ID,
useValue: 'en-us',
},
],
},
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [SharedTestingModule], imports: [SharedTestingModule],
providers: [ providers: [

View File

@ -1,6 +1,7 @@
import { import {
Component, Component,
ElementRef, ElementRef,
LOCALE_ID,
OnDestroy, OnDestroy,
OnInit, OnInit,
ViewChild, ViewChild,
@ -10,6 +11,7 @@ import { ErrorHandler } from '../../../../shared/units/error-handler';
import { import {
ConfirmationState, ConfirmationState,
ConfirmationTargets, ConfirmationTargets,
DEFAULT_LANG_LOCALSTORAGE_KEY,
} from '../../../../shared/entities/shared.const'; } from '../../../../shared/entities/shared.const';
import { import {
SystemCVEAllowlist, SystemCVEAllowlist,
@ -28,6 +30,12 @@ const TARGET_BLANK = '_blank';
selector: 'app-security', selector: 'app-security',
templateUrl: './security.component.html', templateUrl: './security.component.html',
styleUrls: ['./security.component.scss'], styleUrls: ['./security.component.scss'],
providers: [
{
provide: LOCALE_ID,
useValue: localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY),
},
],
}) })
export class SecurityComponent implements OnInit, OnDestroy { export class SecurityComponent implements OnInit, OnDestroy {
onGoing = false; onGoing = false;

View File

@ -3,13 +3,16 @@ import { AuditLogComponent } from './audit-log.component';
import { MessageHandlerService } from '../../../shared/services/message-handler.service'; import { MessageHandlerService } from '../../../shared/services/message-handler.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { of } from 'rxjs'; 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 { delay } from 'rxjs/operators';
import { AuditLog } from '../../../../../ng-swagger-gen/models/audit-log'; import { AuditLog } from '../../../../../ng-swagger-gen/models/audit-log';
import { HttpHeaders, HttpResponse } from '@angular/common/http'; import { HttpHeaders, HttpResponse } from '@angular/common/http';
import { ProjectService } from '../../../../../ng-swagger-gen/services/project.service'; import { ProjectService } from '../../../../../ng-swagger-gen/services/project.service';
import { click } from '../../../shared/units/utils'; import { click } from '../../../shared/units/utils';
import { SharedTestingModule } from '../../../shared/shared.module'; 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', () => { describe('AuditLogComponent', () => {
let component: AuditLogComponent; let component: AuditLogComponent;
@ -78,8 +81,18 @@ describe('AuditLogComponent', () => {
} }
}, },
}; };
registerLocaleData(locale_en, 'en-us');
beforeEach(async () => { beforeEach(async () => {
TestBed.overrideComponent(DatePickerComponent, {
set: {
providers: [
{
provide: LOCALE_ID,
useValue: 'en-us',
},
],
},
});
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [SharedTestingModule], imports: [SharedTestingModule],

View File

@ -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 } };
};
}

View File

@ -12,4 +12,9 @@
left: 75%; left: 75%;
background-color: #c92100; background-color: #c92100;
} }
>.tooltip-content::before {
border-left-color:#c92100;
border-top-color: #c92100;
}
} }

View File

@ -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<DatePickerComponent>;
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();
});
});

View File

@ -5,13 +5,21 @@ import {
EventEmitter, EventEmitter,
ViewChild, ViewChild,
OnChanges, OnChanges,
LOCALE_ID,
} from '@angular/core'; } from '@angular/core';
import { NgModel } from '@angular/forms'; import { NgModel } from '@angular/forms';
import { DEFAULT_LANG_LOCALSTORAGE_KEY } from '../../entities/shared.const';
@Component({ @Component({
selector: 'hbr-datetime', selector: 'hbr-datetime',
templateUrl: './datetime-picker.component.html', templateUrl: './datetime-picker.component.html',
styleUrls: ['./datetime-picker.component.scss'], styleUrls: ['./datetime-picker.component.scss'],
providers: [
{
provide: LOCALE_ID,
useValue: localStorage.getItem(DEFAULT_LANG_LOCALSTORAGE_KEY),
},
],
}) })
export class DatePickerComponent implements OnChanges { export class DatePickerComponent implements OnChanges {
@Input() dateInput: string; @Input() dateInput: string;

View File

@ -45,7 +45,7 @@
clrDropdownItem clrDropdownItem
(click)="switchLanguage(lang[0])" (click)="switchLanguage(lang[0])"
[class.lang-selected]="matchLang(lang[0])" [class.lang-selected]="matchLang(lang[0])"
>{{ lang[1] }}</a >{{ lang[1][0] }}</a
> >
</clr-dropdown-menu> </clr-dropdown-menu>
</clr-dropdown> </clr-dropdown>

View File

@ -9,6 +9,9 @@ import { MessageHandlerService } from '../../services/message-handler.service';
import { SearchTriggerService } from '../global-search/search-trigger.service'; import { SearchTriggerService } from '../global-search/search-trigger.service';
import { SkinableConfig } from '../../../services/skinable-config.service'; import { SkinableConfig } from '../../../services/skinable-config.service';
import { SharedTestingModule } from '../../shared.module'; import { SharedTestingModule } from '../../shared.module';
import { TranslateService } from '@ngx-translate/core';
import { DeFaultLang } from '../../entities/shared.const';
import { of } from 'rxjs';
describe('NavigatorComponent', () => { describe('NavigatorComponent', () => {
let component: TestComponentWrapperComponent; let component: TestComponentWrapperComponent;

View File

@ -14,7 +14,7 @@
import { Component, Output, EventEmitter, OnInit } from '@angular/core'; import { Component, Output, EventEmitter, OnInit } from '@angular/core';
import { Router, NavigationExtras } from '@angular/router'; import { Router, NavigationExtras } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { PlatformLocation } from '@angular/common'; import { PlatformLocation, registerLocaleData } from '@angular/common';
import { ModalEvent } from '../../../base/modal-event'; import { ModalEvent } from '../../../base/modal-event';
import { modalEvents } from '../../../base/modal-events.const'; import { modalEvents } from '../../../base/modal-events.const';
import { SessionService } from '../../services/session.service'; import { SessionService } from '../../services/session.service';
@ -70,6 +70,12 @@ export class NavigatorComponent implements OnInit {
// custom skin // custom skin
this.customStyle = this.skinableConfig.getSkinConfig(); this.customStyle = this.skinableConfig.getSkinConfig();
this.selectedLang = this.translate.currentLang as SupportedLanguage; this.selectedLang = this.translate.currentLang as SupportedLanguage;
if (this.selectedLang) {
registerLocaleData(
LANGUAGES[this.selectedLang][1],
this.selectedLang
);
}
this.selectedDatetimeRendering = getDatetimeRendering(); this.selectedDatetimeRendering = getDatetimeRendering();
if (this.appConfigService.isIntegrationMode()) { if (this.appConfigService.isIntegrationMode()) {
this.appTitle = 'APP_TITLE.VIC'; this.appTitle = 'APP_TITLE.VIC';
@ -91,7 +97,7 @@ export class NavigatorComponent implements OnInit {
} }
public get currentLang(): string { public get currentLang(): string {
return LANGUAGES[this.selectedLang]; return LANGUAGES[this.selectedLang][0] as string;
} }
public get currentDatetimeRendering(): string { public get currentDatetimeRendering(): string {
@ -180,6 +186,7 @@ export class NavigatorComponent implements OnInit {
// Switch languages // Switch languages
switchLanguage(lang: SupportedLanguage): void { switchLanguage(lang: SupportedLanguage): void {
this.selectedLang = lang; this.selectedLang = lang;
registerLocaleData(LANGUAGES[this.selectedLang][1], this.selectedLang);
localStorage.setItem(DEFAULT_LANG_LOCALSTORAGE_KEY, lang); localStorage.setItem(DEFAULT_LANG_LOCALSTORAGE_KEY, lang);
// due to the bug(https://github.com/ngx-translate/core/issues/1258) of translate module // due to the bug(https://github.com/ngx-translate/core/issues/1258) of translate module
// have to reload // have to reload

View File

@ -53,10 +53,15 @@ export function dateValidator(): ValidatorFn {
let valid = true; let valid = true;
if (controlValue) { if (controlValue) {
const regYMD = 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 = const regDMY =
/^(0[1-9]|[12][0-9]|3[01])[- /.](0[1-9]|1[012])[- /.](19|20)\d\d$/g; /^(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 } }; return valid ? null : { dateValidator: { value: controlValue } };
}; };

View File

@ -12,6 +12,15 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 { export const enum AlertType {
DANGER, DANGER,
WARNING, WARNING,
@ -223,16 +232,16 @@ export const REFRESH_TIME_DIFFERENCE = 10000;
// //
export const DeFaultLang = 'en-us'; export const DeFaultLang = 'en-us';
export type SupportedLanguage = keyof typeof LANGUAGES; export type SupportedLanguage = string;
export const LANGUAGES = { export const LANGUAGES = {
'en-us': 'English', 'en-us': ['English', locale_en],
'zh-cn': '中文简体', 'zh-cn': ['中文简体', locale_zh_CN],
'zh-tw': '中文繁體', 'zh-tw': ['中文繁體', locale_zh_TW],
'es-es': 'Español', 'es-es': ['Español', locale_es],
'fr-fr': 'Français', 'fr-fr': ['Français', locale_fr],
'pt-br': 'Português do Brasil', 'pt-br': ['Português do Brasil', locale_pt],
'tr-tr': 'Türkçe', 'tr-tr': ['Türkçe', locale_tr],
'de-de': 'Deutsch', 'de-de': ['Deutsch', locale_de],
} as const; } as const;
export const supportedLangs = Object.keys(LANGUAGES) as SupportedLanguage[]; 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 const DEFAULT_LANG_LOCALSTORAGE_KEY = 'harbor-lang';
export type DatetimeRendering = keyof typeof DATETIME_RENDERINGS; export type DatetimeRendering = string;
export const DATETIME_RENDERINGS = { export const DATETIME_RENDERINGS = {
'locale-default': 'TOP_NAV.DATETIME_RENDERING_DEFAULT', 'locale-default': 'TOP_NAV.DATETIME_RENDERING_DEFAULT',
'iso-8601': 'ISO 8601', 'iso-8601': 'ISO 8601',

View File

@ -79,33 +79,8 @@ import { RouterTestingModule } from '@angular/router/testing';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HarborDatetimePipe } from './pipes/harbor-datetime.pipe'; import { HarborDatetimePipe } from './pipes/harbor-datetime.pipe';
import { RemainingTimeComponent } from './components/remaining-time/remaining-time.component'; 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'; import { LabelSelectorComponent } from './components/label-selector/label-selector.component';
const localesForSupportedLangs: Record<SupportedLanguage, unknown[]> = {
'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. // ClarityIcons is publicly accessible from the browser's window object.
declare const ClarityIcons: ClarityIconsApi; declare const ClarityIcons: ClarityIconsApi;

View File

@ -17,8 +17,6 @@ import {
import { AbstractControl } from '@angular/forms'; import { AbstractControl } from '@angular/forms';
import { isValidCron } from 'cron-validator'; import { isValidCron } from 'cron-validator';
import { ClrDatagridStateInterface } from '@clr/angular'; 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 * Api levels