From eea4580d83f1cbc0a67384216f8b1f39ba9f696d Mon Sep 17 00:00:00 2001 From: Leonardo Mendoza Fernadez Date: Fri, 12 Jan 2024 12:44:45 -0600 Subject: [PATCH 01/12] 8934-registration-add-an-affiliation-with-typeahead --- .../register2/register2.backend-validators.ts | 3 + .../core/register2/register2.form-adapter.ts | 24 ++ src/app/core/register2/register2.service.ts | 9 +- .../form-current-employment.component.html | 197 ++++++++++ .../form-current-employment.component.scss | 4 + .../form-current-employment.component.spec.ts | 48 +++ .../form-current-employment.component.ts | 353 ++++++++++++++++++ .../components/step-a/step-a.component.html | 2 +- .../components/step-b/step-b.component.html | 2 +- .../components/step-c/step-c.component.html | 2 +- .../components/step-c2/step-c.component.html | 82 ++++ .../components/step-c2/step-c.component.scss | 8 + .../components/step-c2/step-c.component.ts | 23 ++ .../components/step-c2/step-c.spec.ts | 24 ++ .../pages/register/register2.component.html | 10 +- .../pages/register/register2.component.ts | 11 + src/app/register2/register.module.ts | 8 + 17 files changed, 800 insertions(+), 10 deletions(-) create mode 100644 src/app/register2/components/form-current-employment/form-current-employment.component.html create mode 100644 src/app/register2/components/form-current-employment/form-current-employment.component.scss create mode 100644 src/app/register2/components/form-current-employment/form-current-employment.component.spec.ts create mode 100644 src/app/register2/components/form-current-employment/form-current-employment.component.ts create mode 100644 src/app/register2/components/step-c2/step-c.component.html create mode 100644 src/app/register2/components/step-c2/step-c.component.scss create mode 100644 src/app/register2/components/step-c2/step-c.component.ts create mode 100644 src/app/register2/components/step-c2/step-c.spec.ts diff --git a/src/app/core/register2/register2.backend-validators.ts b/src/app/core/register2/register2.backend-validators.ts index 92abbe4f71..5d730df0ad 100644 --- a/src/app/core/register2/register2.backend-validators.ts +++ b/src/app/core/register2/register2.backend-validators.ts @@ -24,6 +24,7 @@ interface HasFormAdapters { StepA: UntypedFormGroup, StepB: UntypedFormGroup, StepC: UntypedFormGroup, + StepC2: UntypedFormGroup, StepD: UntypedFormGroup ): RegisterForm } @@ -161,6 +162,7 @@ export function Register2BackendValidatorMixin< StepA: UntypedFormGroup, StepB: UntypedFormGroup, StepC: UntypedFormGroup, + StepC2: UntypedFormGroup, StepD: UntypedFormGroup, type?: 'shibboleth' @@ -169,6 +171,7 @@ export function Register2BackendValidatorMixin< StepA, StepB, StepC, + StepC2, StepD ) return this._http diff --git a/src/app/core/register2/register2.form-adapter.ts b/src/app/core/register2/register2.form-adapter.ts index 628eb6d2f9..02b1435e5c 100644 --- a/src/app/core/register2/register2.form-adapter.ts +++ b/src/app/core/register2/register2.form-adapter.ts @@ -116,16 +116,40 @@ export function Register2FormAdapterMixin>(base: T) { return value } + formGroupToAffiliationRegisterForm(formGroup: UntypedFormGroup) { + console.log(formGroup.value) + + const value = formGroup.controls['organization'].value + const departmentName = formGroup.controls['departmentName'].value + const roleTitle = formGroup.controls['roleTitle'].value + + if (typeof value === 'string') { + return { affiliationName: { value } } + } else { + return { + affiliationName: { value: value.value }, + disambiguatedAffiliationSourceId: { + value: value.disambiguatedAffiliationIdentifier, + }, + departmentName: { value: departmentName }, + roleTitle: { value: roleTitle }, + affiliationType: { value: 'Employment' }, + } + } + } + formGroupToFullRegistrationForm( StepA: UntypedFormGroup, StepB: UntypedFormGroup, StepC: UntypedFormGroup, + StepC2: UntypedFormGroup, StepD: UntypedFormGroup ): RegisterForm { return { ...StepA.value.personal, ...StepB.value.password, ...StepC.value.activitiesVisibilityDefault, + ...StepC2.value.affiliations, ...StepD.value.sendOrcidNews, ...StepD.value.termsOfUse, ...StepD.value.captcha, diff --git a/src/app/core/register2/register2.service.ts b/src/app/core/register2/register2.service.ts index a362de9d4c..3d14a5c7cb 100644 --- a/src/app/core/register2/register2.service.ts +++ b/src/app/core/register2/register2.service.ts @@ -1,8 +1,8 @@ import { HttpClient } from '@angular/common/http' import { Injectable } from '@angular/core' import { UntypedFormGroup } from '@angular/forms' -import { Observable } from 'rxjs' -import { catchError, first, map, retry, switchMap } from 'rxjs/operators' +import { Observable, throwError } from 'rxjs' +import { catchError, first, map, retry, switchMap, tap } from 'rxjs/operators' import { PlatformInfo, PlatformInfoService } from 'src/app/cdk/platform-info' import { RequestInfoForm } from 'src/app/types' import { @@ -85,6 +85,7 @@ export class Register2Service extends _RegisterServiceMixingBase { StepA: UntypedFormGroup, StepB: UntypedFormGroup, StepC: UntypedFormGroup, + StepC2: UntypedFormGroup, StepD: UntypedFormGroup, reactivation: ReactivationLocal, requestInfoForm?: RequestInfoForm, @@ -96,6 +97,7 @@ export class Register2Service extends _RegisterServiceMixingBase { StepA, StepB, StepC, + StepC2, StepD ) this.addOauthContext(registerForm, requestInfoForm) @@ -134,6 +136,9 @@ export class Register2Service extends _RegisterServiceMixingBase { ) ) .pipe( + tap((value) => { + throw 'nothing' + }), retry(3), catchError((error) => this._errorHandler.handleError(error, ERROR_REPORT.REGISTER) diff --git a/src/app/register2/components/form-current-employment/form-current-employment.component.html b/src/app/register2/components/form-current-employment/form-current-employment.component.html new file mode 100644 index 0000000000..5f6844889d --- /dev/null +++ b/src/app/register2/components/form-current-employment/form-current-employment.component.html @@ -0,0 +1,197 @@ + +

+ Current employment +

+ +
+
+ +
+
+
+

+ Affiliation found +

+
+
+ + Based on your emails we think you are currently affiliated with + + The University of Bath. + We’ve pre-selected this organization for you in the form + below. + +
+ When you complete registration an employment affiliation will be + automatically added to your new ORCID record. +
+
+
+
+ +
+
+
+

+ Organization + * +

+

+ * + Required information + +

+
+
+
+ +
+ +
+ +
+ + + + + +
+ {{ option.value }} +
+
+ {{ option.city }} {{ option.region }} {{ option.country }} +
+
+
+
+ + Identify as: + {{ selectedOrganizationFromDatabase.value }} + + + Unidentified organization + + + + + Please enter an organization + + + Must be less than 1000 characters + +
+
+
+ +
+ Department + + + + +
+
+ Role/Job title + + + + +
+
diff --git a/src/app/register2/components/form-current-employment/form-current-employment.component.scss b/src/app/register2/components/form-current-employment/form-current-employment.component.scss new file mode 100644 index 0000000000..4cef8a0ce4 --- /dev/null +++ b/src/app/register2/components/form-current-employment/form-current-employment.component.scss @@ -0,0 +1,4 @@ +:host { + display: flex; + flex-direction: column; +} diff --git a/src/app/register2/components/form-current-employment/form-current-employment.component.spec.ts b/src/app/register2/components/form-current-employment/form-current-employment.component.spec.ts new file mode 100644 index 0000000000..c97e68b88b --- /dev/null +++ b/src/app/register2/components/form-current-employment/form-current-employment.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { Overlay } from '@angular/cdk/overlay' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog' +import { MatLegacySnackBar as MatSnackBar } from '@angular/material/legacy-snack-bar' +import { RouterTestingModule } from '@angular/router/testing' +import { PlatformInfoService } from '../../../cdk/platform-info' +import { MdePopoverModule } from '../../../cdk/popover' +import { SnackbarService } from '../../../cdk/snackbar/snackbar.service' +import { WINDOW_PROVIDERS } from '../../../cdk/window' +import { ErrorHandlerService } from '../../../core/error-handler/error-handler.service' +import { ReactivationService } from '../../../core/reactivation/reactivation.service' +import { Register2Service } from '../../../core/register2/register2.service' +import { FormCurrentEmploymentComponent } from './form-current-employment.component' + +describe('FormPersonalComponent', () => { + let component: FormCurrentEmploymentComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, MdePopoverModule, RouterTestingModule], + declarations: [FormCurrentEmploymentComponent], + providers: [ + WINDOW_PROVIDERS, + ReactivationService, + Register2Service, + PlatformInfoService, + ErrorHandlerService, + SnackbarService, + MatSnackBar, + MatDialog, + Overlay, + ], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(FormCurrentEmploymentComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/src/app/register2/components/form-current-employment/form-current-employment.component.ts b/src/app/register2/components/form-current-employment/form-current-employment.component.ts new file mode 100644 index 0000000000..c0c048386f --- /dev/null +++ b/src/app/register2/components/form-current-employment/form-current-employment.component.ts @@ -0,0 +1,353 @@ +import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core' +import { + FormControl, + FormGroupDirective, + NG_ASYNC_VALIDATORS, + NG_VALUE_ACCESSOR, + NgForm, + UntypedFormControl, + UntypedFormGroup, + Validators, +} from '@angular/forms' +import { Register2Service } from 'src/app/core/register2/register2.service' +import { OrcidValidators } from 'src/app/validators' + +import { first, switchMap, tap } from 'rxjs/operators' +import { ReactivationService } from '../../../core/reactivation/reactivation.service' +import { ReactivationLocal } from '../../../types/reactivation.local' +import { BaseForm } from '../BaseForm' +import { ErrorStateMatcher } from '@angular/material/core' +import { PlatformInfoService } from 'src/app/cdk/platform-info' +import { Router } from '@angular/router' +import { MAX_LENGTH_LESS_THAN_ONE_THOUSAND } from 'src/app/constants' +import { LiveAnnouncer } from '@angular/cdk/a11y' +import { + AffiliationType, + Organization, +} from 'src/app/types/record-affiliation.endpoint' +import { EMPTY, Observable, of } from 'rxjs' +import { RecordAffiliationService } from 'src/app/core/record-affiliations/record-affiliations.service' +export class MyErrorStateMatcher implements ErrorStateMatcher { + isErrorState( + control: FormControl | null, + form: FormGroupDirective | NgForm | null + ): boolean { + const isSubmitted = form && form.submitted + return !!( + control && + control.invalid && + (control.dirty || control.touched || isSubmitted) + ) + } +} + +@Component({ + selector: 'app-form-current-employment', + templateUrl: './form-current-employment.component.html', + styleUrls: [ + './form-current-employment.component.scss', + '../register2.style.scss', + '../register2.scss-theme.scss', + ], + preserveWhitespaces: true, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => FormCurrentEmploymentComponent), + multi: true, + }, + { + provide: NG_ASYNC_VALIDATORS, + useExisting: forwardRef(() => FormCurrentEmploymentComponent), + multi: true, + }, + ], +}) +export class FormCurrentEmploymentComponent extends BaseForm implements OnInit { + // matcher = new MyErrorStateMatcher() + selectedOrganizationFromDatabase: Organization + displayOrganizationHint: boolean + requireOrganizationDisambiguatedDataOnRefresh = false + private _type: AffiliationType + + @Input() + public get type(): AffiliationType { + return this._type + } + + filteredOptions: Observable + + @Input() nextButtonWasClicked: boolean + @Input() reactivation: ReactivationLocal + @ViewChild(FormGroupDirective) formGroupDir: FormGroupDirective + organizationPlaceholder = $localize`:@@register.organizationPlaceholder:Type your organization name` + departmentPlaceholder = $localize`:@@register.departmentPlaceholder:School, college or department` + rolePlaceholder = $localize`:@@register.rolePlaceholder:Your role or job in the organization` + yearPlaceholder = $localize`:@@register.yearPlaceholder:Year` + monthPlaceholder = $localize`:@@register.monthPlaceholder:Month` + // emailPlaceholder = $localize`:@@register.emailPlaceholder:The email address you use most` + // arialabelConfirmEmail = $localize`:@@register.labelConfirmEmail:Confirm your email address` + // labelInfoAboutName = $localize`:@@register.ariaLabelInfo:info about names` + // labelClose = $localize`:@@register.ariaLabelClose:close` + // labelConfirmEmail = $localize`:@@register.confirmEmail:Confirm primary email` + // labelNameYouMostCommonly = $localize`:@@register.labelNameYouMostMost:The names you most commonly go by` + // labelFamilyNamePlaceholder = $localize`:@@register.familyNamePlaceholder:Your family name or surname + // ` + professionalEmail: boolean + personalEmail: boolean + undefinedEmail: boolean + emailsAreValidAlreadyChecked: boolean + organization: string | Organization = '' + platform: import('/Users/l.mendoza/code/orcid-angular/src/app/cdk/platform-info/platform-info.type').PlatformInfo + isMobile: boolean + constructor( + private _register: Register2Service, + private _reactivationService: ReactivationService, + private _platform: PlatformInfoService, + private _router: Router, + private _liveAnnouncer: LiveAnnouncer, + private _recordAffiliationService: RecordAffiliationService + ) { + super() + this._platform.get().subscribe((platform) => { + this.platform = platform + this.isMobile = platform.columns4 || platform.columns8 + }) + } + + emails: UntypedFormGroup = new UntypedFormGroup({}) + additionalEmails: UntypedFormGroup = new UntypedFormGroup({ + '0': new UntypedFormControl('', { + validators: [OrcidValidators.email], + }), + }) + + ngOnInit() { + this.form = new UntypedFormGroup({ + organization: new UntypedFormControl(this.organization, { + validators: [ + Validators.required, + Validators.maxLength(MAX_LENGTH_LESS_THAN_ONE_THOUSAND), + ], + }), + departmentName: new UntypedFormControl('', { + validators: [Validators.maxLength(MAX_LENGTH_LESS_THAN_ONE_THOUSAND)], + }), + roleTitle: new UntypedFormControl('', { + validators: [Validators.maxLength(MAX_LENGTH_LESS_THAN_ONE_THOUSAND)], + }), + }) + + this.filteredOptions = this.form.get('organization').valueChanges.pipe( + tap((organization: string | Organization) => { + // Auto fill form when the user select an organization from the autocomplete list + if ( + typeof organization === 'object' && + organization.disambiguatedAffiliationIdentifier + ) { + this.selectedOrganizationFromDatabase = organization + this.requireOrganizationDisambiguatedDataOnRefresh = true + this.displayOrganizationHint = true + // this.fillForm(organization) + } + // if (!organization) { + // this.selectedOrganizationFromDatabase = undefined + // this.requireOrganizationDisambiguatedDataOnRefresh = true + // this.displayOrganizationHint = false + // this.form.patchValue({ + // city: '', + // region: '', + // country: '', + // }) + // } + }), + switchMap((organization: string | Organization) => { + if ( + typeof organization === 'string' && + !this.selectedOrganizationFromDatabase + ) { + // Display matching organization based on the user string input + return this._filter((organization as string) || '').pipe( + tap((x) => { + this.displayOrganizationHint = true + }) + ) + } else { + // Do not display options once the user has selected an Organization + return of([]) + } + }) + ) + } + + get organizationIsInvalidAndTouched() { + return ( + this.form.hasError('required', 'organization') && + (this.form.get('organization').dirty || + this.form.get('organization').touched) + ) + } + + autoCompleteDisplayOrganization(organization: Organization) { + return organization.value + } + + private _filter(value: string): Observable { + if (value) { + return this._recordAffiliationService.getOrganization(value).pipe(first()) + } + + return EMPTY + } + + clearForm() { + this.form.patchValue({ + organization: '', + }) + } + + // allEmailsAreUnique(): ValidatorFn { + // return (formGroup: UntypedFormGroup) => { + // let hasError = false + // const registerForm = + // this._register.formGroupToEmailRegisterForm(formGroup) + + // const error = { backendErrors: { additionalEmails: {} } } + + // Object.keys(registerForm.emailsAdditional).forEach((key, i) => { + // const additionalEmail = registerForm.emailsAdditional[key] + // if (!error.backendErrors.additionalEmails[additionalEmail.value]) { + // error.backendErrors.additionalEmails[additionalEmail.value] = [] + // } + // const additionalEmailsErrors = error.backendErrors.additionalEmails + // if ( + // registerForm.email && + // additionalEmail.value === registerForm.email.value + // ) { + // hasError = true + // additionalEmailsErrors[additionalEmail.value] = [ + // 'additionalEmailCantBePrimaryEmail', + // ] + // } else { + // Object.keys(registerForm.emailsAdditional).forEach( + // (elementKey, i2) => { + // const element = registerForm.emailsAdditional[elementKey] + // if (i !== i2 && additionalEmail.value === element.value) { + // hasError = true + // additionalEmailsErrors[additionalEmail.value] = [ + // 'duplicatedAdditionalEmail', + // ] + // } + // } + // ) + // } + // }) + + // if (hasError) { + // return error + // } else { + // return null + // } + // } + // } + + // OVERWRITE + registerOnChange(fn: any) { + this.form.valueChanges.subscribe((value) => { + console.log('value', value) + const affiliation = this._register.formGroupToAffiliationRegisterForm( + this.form as UntypedFormGroup + ) + console.log('affiliationsForm', affiliation) + + fn({ + affiliationForm: { + ...affiliation, + }, + }) + }) + } + + get organizationFormTouched() { + return ( + ((this.form.controls.organization as any).controls?.organization as any) + ?.touched || this.nextButtonWasClicked + ) + } + + get emailFormTouched() { + return ( + ((this.form.controls.emails as any).controls?.email as any)?.touched || + this.nextButtonWasClicked + ) + } + + get emailConfirmationFormTouched() { + return ( + ((this.form.controls.emails as any).controls?.confirmEmail as any) + ?.touched || this.nextButtonWasClicked + ) + } + + get familyNamesFormTouched() { + return this.form.controls.familyNames?.touched || this.nextButtonWasClicked + } + + get emailValid() { + return ((this.form.controls.emails as any).controls?.email as any).valid + } + + get emailConfirmationValid() { + return ((this.form.controls.emails as any).controls?.confirmEmail as any) + .valid + } + + get givenNameFormTouched() { + return this.form.controls.givenNames?.touched || this.nextButtonWasClicked + } + + // get emailsAreValid() { + // const validStatus = this.emailConfirmationValid && this.emailValid + // if (!this.emailsAreValidAlreadyChecked && validStatus) { + // this.announce($localize`:@@register.emailAreValid:Your emails match`) + // } else if (this.emailsAreValidAlreadyChecked && !validStatus) { + // this.announce( + // $localize`:@@register.emailAreNotValid:Your emails do not match` + // ) + // } + // this.emailsAreValidAlreadyChecked = validStatus + // return validStatus + // } + + get emailError(): boolean { + if (this.emailFormTouched && this.emails.controls.email.errors) { + const backendError = this.emails.controls.email.errors?.backendError + return !( + backendError && + backendError[0] === 'orcid.frontend.verify.duplicate_email' && + !this.nextButtonWasClicked + ) + } + return false + } + + // private announce(announcement: string) { + // if (environment.debugger) { + // console.debug('📢' + announcement) + // } + // this._liveAnnouncer.announce(announcement, 'assertive') + // } + + // navigateToSignin(email) { + // this._platform + // .get() + // .pipe(take(1)) + // .subscribe((platform) => { + // return this._router.navigate([ApplicationRoutes.signin], { + // // keeps all parameters to support Oauth request + // // and set show login to true + // queryParams: { ...platform.queryParameters, email, show_login: true }, + // }) + // }) + // } +} diff --git a/src/app/register2/components/step-a/step-a.component.html b/src/app/register2/components/step-a/step-a.component.html index a4d9b83053..8e5b1552f5 100644 --- a/src/app/register2/components/step-a/step-a.component.html +++ b/src/app/register2/components/step-a/step-a.component.html @@ -22,7 +22,7 @@

- Step 1 of 4 - Names and emails + Step 1 of 5 - Names and emails

diff --git a/src/app/register2/components/step-b/step-b.component.html b/src/app/register2/components/step-b/step-b.component.html index 4dcb4196df..b7f95d3cd4 100644 --- a/src/app/register2/components/step-b/step-b.component.html +++ b/src/app/register2/components/step-b/step-b.component.html @@ -22,7 +22,7 @@

- Step 2 of 4 - Password + Step 2 of 5 - Password

diff --git a/src/app/register2/components/step-c/step-c.component.html b/src/app/register2/components/step-c/step-c.component.html index 51115eb700..0c7445cf33 100644 --- a/src/app/register2/components/step-c/step-c.component.html +++ b/src/app/register2/components/step-c/step-c.component.html @@ -28,7 +28,7 @@

- Step 3 of 4 - Visibility + Step 3 of 5 - Visibility

diff --git a/src/app/register2/components/step-c2/step-c.component.html b/src/app/register2/components/step-c2/step-c.component.html new file mode 100644 index 0000000000..e10f41a479 --- /dev/null +++ b/src/app/register2/components/step-c2/step-c.component.html @@ -0,0 +1,82 @@ + + + + + +
+ orcid logo +
+ +

+ Create your ORCID iD +

+
+ + Thank you for reactivating your ORCID iD. + +
+ +

+ Step 3 of 5 - Visibility +

+
+
+ + +
+ + + +
+ + +
+
+
+ +
diff --git a/src/app/register2/components/step-c2/step-c.component.scss b/src/app/register2/components/step-c2/step-c.component.scss new file mode 100644 index 0000000000..5c66fbdc42 --- /dev/null +++ b/src/app/register2/components/step-c2/step-c.component.scss @@ -0,0 +1,8 @@ +:host { + display: flex; + flex-direction: column; +} + +mat-error { + font-size: 14px; +} diff --git a/src/app/register2/components/step-c2/step-c.component.ts b/src/app/register2/components/step-c2/step-c.component.ts new file mode 100644 index 0000000000..6ab8b98895 --- /dev/null +++ b/src/app/register2/components/step-c2/step-c.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core' + +import { ReactivationLocal } from '../../../types/reactivation.local' +import { BaseStepDirective } from '../BaseStep' + +@Component({ + selector: 'app-step-c2', + templateUrl: './step-c.component.html', + styleUrls: [ + './step-c.component.scss', + '../register2.style.scss', + '../register2.scss-theme.scss', + ], +}) +export class StepC2Component extends BaseStepDirective { + @Input() loading + @Input() reactivation: ReactivationLocal + nextButtonWasClicked: boolean + + constructor() { + super() + } +} diff --git a/src/app/register2/components/step-c2/step-c.spec.ts b/src/app/register2/components/step-c2/step-c.spec.ts new file mode 100644 index 0000000000..db7d3c24ff --- /dev/null +++ b/src/app/register2/components/step-c2/step-c.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' + +import { StepCComponent } from './step-c.component' + +describe('StepCComponent', () => { + let component: StepCComponent + let fixture: ComponentFixture + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StepCComponent], + }).compileComponents() + }) + + beforeEach(() => { + fixture = TestBed.createComponent(StepCComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) +}) diff --git a/src/app/register2/pages/register/register2.component.html b/src/app/register2/pages/register/register2.component.html index e8b67c874d..acb1743fb4 100644 --- a/src/app/register2/pages/register/register2.component.html +++ b/src/app/register2/pages/register/register2.component.html @@ -31,16 +31,16 @@ [reactivation]="reactivation" > - + Visibility and terms - + > Date: Fri, 12 Jan 2024 13:04:07 -0600 Subject: [PATCH 02/12] Update employment string --- src/app/core/register2/register2.form-adapter.ts | 2 +- src/app/core/register2/register2.service.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app/core/register2/register2.form-adapter.ts b/src/app/core/register2/register2.form-adapter.ts index 02b1435e5c..58a41f699b 100644 --- a/src/app/core/register2/register2.form-adapter.ts +++ b/src/app/core/register2/register2.form-adapter.ts @@ -133,7 +133,7 @@ export function Register2FormAdapterMixin>(base: T) { }, departmentName: { value: departmentName }, roleTitle: { value: roleTitle }, - affiliationType: { value: 'Employment' }, + affiliationType: { value: 'employment' }, } } } diff --git a/src/app/core/register2/register2.service.ts b/src/app/core/register2/register2.service.ts index 3d14a5c7cb..3037803a45 100644 --- a/src/app/core/register2/register2.service.ts +++ b/src/app/core/register2/register2.service.ts @@ -136,9 +136,7 @@ export class Register2Service extends _RegisterServiceMixingBase { ) ) .pipe( - tap((value) => { - throw 'nothing' - }), + retry(3), catchError((error) => this._errorHandler.handleError(error, ERROR_REPORT.REGISTER) From ea532a2ab2c0fb2592d4833ced094a046d7686ad Mon Sep 17 00:00:00 2001 From: Leonardo Mendoza Fernadez Date: Sat, 13 Jan 2024 14:59:25 -0600 Subject: [PATCH 03/12] 8934-registration-add-an-affiliation-with-typeahead --- .../core/register2/register2.form-adapter.ts | 7 +- .../modal-affiliations.component.scss | 24 +-- .../form-current-employment.component.html | 168 ++++++++++++------ .../form-current-employment.component.scss | 47 +++++ ...rrent-employment.component.scss-theme.scss | 23 +++ .../form-current-employment.component.ts | 75 ++++++-- .../components/step-c2/step-c.component.html | 18 +- .../components/step-c2/step-c.component.ts | 16 +- .../pages/register/register2.component.html | 7 +- .../pages/register/register2.component.ts | 15 +- src/app/register2/register.module.ts | 10 +- .../shared/validators/date/date.validator.ts | 2 +- .../stepper.scss | 8 +- 13 files changed, 319 insertions(+), 101 deletions(-) create mode 100644 src/app/register2/components/form-current-employment/form-current-employment.component.scss-theme.scss diff --git a/src/app/core/register2/register2.form-adapter.ts b/src/app/core/register2/register2.form-adapter.ts index 58a41f699b..a2a0121240 100644 --- a/src/app/core/register2/register2.form-adapter.ts +++ b/src/app/core/register2/register2.form-adapter.ts @@ -122,18 +122,23 @@ export function Register2FormAdapterMixin>(base: T) { const value = formGroup.controls['organization'].value const departmentName = formGroup.controls['departmentName'].value const roleTitle = formGroup.controls['roleTitle'].value + const startDateGroup = formGroup.controls['startDateGroup'].value if (typeof value === 'string') { return { affiliationName: { value } } } else { return { affiliationName: { value: value.value }, - disambiguatedAffiliationSourceId: { + orgDisambiguatedId: { value: value.disambiguatedAffiliationIdentifier, }, departmentName: { value: departmentName }, roleTitle: { value: roleTitle }, affiliationType: { value: 'employment' }, + startDate: { + month: startDateGroup.startDateMonth, + year: startDateGroup.startDateYear, + }, } } } diff --git a/src/app/record/components/affiliation-stacks-groups/modals/modal-affiliations/modal-affiliations.component.scss b/src/app/record/components/affiliation-stacks-groups/modals/modal-affiliations/modal-affiliations.component.scss index 8c5461ad96..3be016dd3c 100644 --- a/src/app/record/components/affiliation-stacks-groups/modals/modal-affiliations/modal-affiliations.component.scss +++ b/src/app/record/components/affiliation-stacks-groups/modals/modal-affiliations/modal-affiliations.component.scss @@ -67,18 +67,18 @@ label { padding: 0 !important; } -.input-box { - flex-grow: 1; - max-width: 402px; - input { - width: 100%; - height: 40px; - margin: 4px 0; - border-width: 1px; - border-style: solid; - padding: 0 12px; - } -} +// .input-box { +// flex-grow: 1; +// max-width: 402px; +// input { +// width: 100%; +// height: 40px; +// margin: 4px 0; +// border-width: 1px; +// border-style: solid; +// padding: 0 12px; +// } +// } .visibility { margin-top: 60px; diff --git a/src/app/register2/components/form-current-employment/form-current-employment.component.html b/src/app/register2/components/form-current-employment/form-current-employment.component.html index 5f6844889d..f67481b8a8 100644 --- a/src/app/register2/components/form-current-employment/form-current-employment.component.html +++ b/src/app/register2/components/form-current-employment/form-current-employment.component.html @@ -3,7 +3,7 @@

Current employment

-
+
@@ -39,26 +39,7 @@

'orc-font-body': !platform.columns12, 'orc-font-body-large': platform.columns12 }" - > -
-

- Organization - * -

-

- * - Required information - -

-
-

-
+ >
@@ -82,11 +62,13 @@

class="mat-form-field-min" id="cy-org-dd-mat-form" [ngClass]="{ - 'two-line-hint': selectedOrganizationFromDatabase?.value.length > 50 + 'two-line-hint': selectedOrganizationFromDatabase?.value.length > 50, + 'selected-org': organizationIsValidAndTouched }" > - - Identify as: - {{ selectedOrganizationFromDatabase.value }} - - - Unidentified organization - - - Please enter an organization - - - Must be less than 1000 characters - +
+ {{ selectedOrganizationFromDatabase.value }}, + {{ selectedOrganizationFromDatabase.city }} + , + {{ selectedOrganizationFromDatabase.country }} +
+
+ We can’t identify this organization. Please try entering the + organization name again. +
+
+ Please enter an organization name +
@@ -177,6 +164,7 @@

/> +
Role/Job title @@ -194,4 +182,74 @@

/>

+ + + +
+
+ + Start date + + +
+ +
+
+
+ + + Year + + {{ year }} + + + +
+ +
/
+
+ + + Month + + {{ month | trailingZeros }} + + + +
+
+
+ + Invalid date + +
+
+
diff --git a/src/app/register2/components/form-current-employment/form-current-employment.component.scss b/src/app/register2/components/form-current-employment/form-current-employment.component.scss index 4cef8a0ce4..0829a3f1fa 100644 --- a/src/app/register2/components/form-current-employment/form-current-employment.component.scss +++ b/src/app/register2/components/form-current-employment/form-current-employment.component.scss @@ -2,3 +2,50 @@ display: flex; flex-direction: column; } +.mat-option { + min-height: 64px; + line-height: 32px; + height: auto; + margin-bottom: 8px; + white-space: normal; + line-height: 1.4; + + .title { + display: flex; + width: 100%; + font-weight: 500; + } +} +.title { + p { + margin: 6px 0; + } +} + +.selected-org { + margin-top: 8px; + line-height: 18px; +} + +.date-inputs-container { + text-align: center; + + gap: 8px; + .date-divider { + line-height: 42px; + font-size: 16px; + } + .date-input { + width: 100px; + } +} + +::ng-deep { + mat-select { + .mat-select-trigger { + .mat-select-arrow-wrapper { + height: 0; + } + } + } +} diff --git a/src/app/register2/components/form-current-employment/form-current-employment.component.scss-theme.scss b/src/app/register2/components/form-current-employment/form-current-employment.component.scss-theme.scss new file mode 100644 index 0000000000..ab05b44300 --- /dev/null +++ b/src/app/register2/components/form-current-employment/form-current-employment.component.scss-theme.scss @@ -0,0 +1,23 @@ +@use '@angular/material' as mat; +@import 'src/assets/scss/material.orcid-theme.scss'; + +@mixin theme($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, accent); + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); + + ::ng-deep { + mat-form-field.selected-org { + .mat-form-field-flex { + background-color: mat.get-color-from-palette( + $background, + ui-background-lightest + ); + } + } + } +} + +@include theme($orcid-app-theme); diff --git a/src/app/register2/components/form-current-employment/form-current-employment.component.ts b/src/app/register2/components/form-current-employment/form-current-employment.component.ts index c0c048386f..7016ec1a9e 100644 --- a/src/app/register2/components/form-current-employment/form-current-employment.component.ts +++ b/src/app/register2/components/form-current-employment/form-current-employment.component.ts @@ -1,5 +1,6 @@ import { Component, forwardRef, Input, OnInit, ViewChild } from '@angular/core' import { + FormBuilder, FormControl, FormGroupDirective, NG_ASYNC_VALIDATORS, @@ -7,6 +8,7 @@ import { NgForm, UntypedFormControl, UntypedFormGroup, + ValidatorFn, Validators, } from '@angular/forms' import { Register2Service } from 'src/app/core/register2/register2.service' @@ -27,6 +29,11 @@ import { } from 'src/app/types/record-affiliation.endpoint' import { EMPTY, Observable, of } from 'rxjs' import { RecordAffiliationService } from 'src/app/core/record-affiliations/record-affiliations.service' +import { + dateMonthYearValidator, + dateValidator, + endDateMonthYearValidator, +} from 'src/app/shared/validators/date/date.validator' export class MyErrorStateMatcher implements ErrorStateMatcher { isErrorState( control: FormControl | null, @@ -46,6 +53,7 @@ export class MyErrorStateMatcher implements ErrorStateMatcher { templateUrl: './form-current-employment.component.html', styleUrls: [ './form-current-employment.component.scss', + './form-current-employment.component.scss-theme.scss', '../register2.style.scss', '../register2.scss-theme.scss', ], @@ -69,6 +77,7 @@ export class FormCurrentEmploymentComponent extends BaseForm implements OnInit { displayOrganizationHint: boolean requireOrganizationDisambiguatedDataOnRefresh = false private _type: AffiliationType + affiliationFound = false @Input() public get type(): AffiliationType { @@ -85,6 +94,19 @@ export class FormCurrentEmploymentComponent extends BaseForm implements OnInit { rolePlaceholder = $localize`:@@register.rolePlaceholder:Your role or job in the organization` yearPlaceholder = $localize`:@@register.yearPlaceholder:Year` monthPlaceholder = $localize`:@@register.monthPlaceholder:Month` + ariaLabelStartDate = $localize`:@@shared.startDate:Start date` + ngOrcidYear = $localize`:@@shared.year:Year` + ngOrcidMonth = $localize`:@@shared.month:Month` + + years = Array(110) + .fill(0) + .map((i, idx) => idx + new Date().getFullYear() - 108) + .reverse() + + months = Array(12) + .fill(0) + .map((i, idx) => idx + 1) + // emailPlaceholder = $localize`:@@register.emailPlaceholder:The email address you use most` // arialabelConfirmEmail = $localize`:@@register.labelConfirmEmail:Confirm your email address` // labelInfoAboutName = $localize`:@@register.ariaLabelInfo:info about names` @@ -106,7 +128,8 @@ export class FormCurrentEmploymentComponent extends BaseForm implements OnInit { private _platform: PlatformInfoService, private _router: Router, private _liveAnnouncer: LiveAnnouncer, - private _recordAffiliationService: RecordAffiliationService + private _recordAffiliationService: RecordAffiliationService, + private _formBuilder: FormBuilder ) { super() this._platform.get().subscribe((platform) => { @@ -136,6 +159,13 @@ export class FormCurrentEmploymentComponent extends BaseForm implements OnInit { roleTitle: new UntypedFormControl('', { validators: [Validators.maxLength(MAX_LENGTH_LESS_THAN_ONE_THOUSAND)], }), + startDateGroup: this._formBuilder.group( + { + startDateMonth: [''], + startDateYear: [''], + }, + dateMonthYearValidator('startDate') + ), }) this.filteredOptions = this.form.get('organization').valueChanges.pipe( @@ -150,16 +180,11 @@ export class FormCurrentEmploymentComponent extends BaseForm implements OnInit { this.displayOrganizationHint = true // this.fillForm(organization) } - // if (!organization) { - // this.selectedOrganizationFromDatabase = undefined - // this.requireOrganizationDisambiguatedDataOnRefresh = true - // this.displayOrganizationHint = false - // this.form.patchValue({ - // city: '', - // region: '', - // country: '', - // }) - // } + if (!organization) { + this.selectedOrganizationFromDatabase = undefined + this.requireOrganizationDisambiguatedDataOnRefresh = true + this.displayOrganizationHint = false + } }), switchMap((organization: string | Organization) => { if ( @@ -182,9 +207,17 @@ export class FormCurrentEmploymentComponent extends BaseForm implements OnInit { get organizationIsInvalidAndTouched() { return ( - this.form.hasError('required', 'organization') && - (this.form.get('organization').dirty || - this.form.get('organization').touched) + (this.form.hasError('required', 'organization') || + this.form.hasError('mustBeOrganizationType', 'organization')) && + (this.form.get('organization').touched || this.nextButtonWasClicked) + ) + } + + get organizationIsValidAndTouched() { + return ( + !this.form.hasError('required', 'organization') && + !this.form.hasError('mustBeOrganizationType', 'organization') && + (this.form.get('organization').touched || this.nextButtonWasClicked) ) } @@ -204,6 +237,7 @@ export class FormCurrentEmploymentComponent extends BaseForm implements OnInit { this.form.patchValue({ organization: '', }) + this.form.controls.organization.markAsUntouched() } // allEmailsAreUnique(): ValidatorFn { @@ -254,7 +288,7 @@ export class FormCurrentEmploymentComponent extends BaseForm implements OnInit { // OVERWRITE registerOnChange(fn: any) { this.form.valueChanges.subscribe((value) => { - console.log('value', value) + console.log('previous value', value) const affiliation = this._register.formGroupToAffiliationRegisterForm( this.form as UntypedFormGroup ) @@ -331,6 +365,17 @@ export class FormCurrentEmploymentComponent extends BaseForm implements OnInit { return false } + mustBeOrganizationType(): ValidatorFn { + return (formGroup: UntypedFormGroup) => { + // const organization = formGroup.controls.organization.valuec + console.log('formGroup >', formGroup.value, '<') + if (formGroup.value && typeof formGroup.value === 'string') { + return { mustBeOrganizationType: true } + } + return null + } + } + // private announce(announcement: string) { // if (environment.debugger) { // console.debug('📢' + announcement) diff --git a/src/app/register2/components/step-c2/step-c.component.html b/src/app/register2/components/step-c2/step-c.component.html index e10f41a479..5a015786f3 100644 --- a/src/app/register2/components/step-c2/step-c.component.html +++ b/src/app/register2/components/step-c2/step-c.component.html @@ -34,6 +34,11 @@

+

+ Adding a current employment affiliation helps distinguish you from other + researchers with a similar name. +

+
mat-raised-button color="primary" [disabled]="loading" - matStepperNext + (click)="requiredNextStep()" > REACTIVATE +