diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index cfa202fe4a..82f295f28e 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1411,6 +1411,40 @@ + "profile.breadcrumbs": "Update Profile", + + "profile.card.identify": "Identify", + + "profile.card.security": "Security", + + "profile.form.submit": "Update Profile", + + "profile.head": "Update Profile", + + "profile.metadata.form.error.firstname": "First Name is required", + + "profile.metadata.form.error.lastname": "Last Name is required", + + "profile.metadata.form.label.email": "Email Address", + + "profile.metadata.form.label.firstname": "First Name", + + "profile.metadata.form.label.language": "Language", + + "profile.metadata.form.label.lastname": "Last Name", + + "profile.metadata.form.label.phone": "Contact Telephone", + + "profile.security.form.info": "Optionally, you can enter a new password in the box below, and confirm it by typing it again into the second box. It should be at least six characters long.", + + "profile.security.form.label.password": "Password", + + "profile.security.form.label.passwordrepeat": "Retype to confirm", + + "profile.title": "Update Profile", + + + "project.listelement.badge": "Research Project", "project.page.contributor": "Contributors", diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 4cf5efae41..da2b896241 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -68,6 +68,7 @@ export function getDSOPath(dso: DSpaceObject): string { { path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' }, { path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' }, { path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowItemsEditPageModule' }, + { path: 'profile', loadChildren: './profile-page/profile-page.module#ProfilePageModule', canActivate: [AuthenticatedGuard] }, { path: '**', pathMatch: 'full', component: PageNotFoundComponent }, ]) ], diff --git a/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.html b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.html new file mode 100644 index 0000000000..8f68b82b7b --- /dev/null +++ b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.html @@ -0,0 +1,5 @@ + + diff --git a/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts new file mode 100644 index 0000000000..6c5b9acb55 --- /dev/null +++ b/src/app/profile-page/profile-page-metadata-form/profile-page-metadata-form.component.ts @@ -0,0 +1,147 @@ +import { Component, Inject, Input, OnInit } from '@angular/core'; +import { + DynamicFormControlModel, + DynamicFormService, DynamicFormValueControlModel, + DynamicInputModel, DynamicSelectModel +} from '@ng-dynamic-forms/core'; +import { FormGroup } from '@angular/forms'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { Location } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; +import { LangConfig } from '../../../config/lang-config.interface'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { cloneDeep } from 'lodash'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../core/shared/operators'; + +@Component({ + selector: 'ds-profile-page-metadata-form', + templateUrl: './profile-page-metadata-form.component.html' +}) +export class ProfilePageMetadataFormComponent implements OnInit { + /** + * The user to display the form for + */ + @Input() user: EPerson; + + formModel: DynamicFormControlModel[] = [ + new DynamicInputModel({ + id: 'email', + name: 'email', + readOnly: true + }), + new DynamicInputModel({ + id: 'firstname', + name: 'eperson.firstname', + required: true, + validators: { + required: null + }, + errorMessages: { + required: 'This field is required' + }, + }), + new DynamicInputModel({ + id: 'lastname', + name: 'eperson.lastname', + required: true, + validators: { + required: null + }, + errorMessages: { + required: 'This field is required' + }, + }), + new DynamicInputModel({ + id: 'phone', + name: 'eperson.phone' + }), + new DynamicSelectModel({ + id: 'language', + name: 'eperson.language' + }) + ]; + + /** + * The form group of this form + */ + formGroup: FormGroup; + + LABEL_PREFIX = 'profile.metadata.form.label.'; + + ERROR_PREFIX = 'profile.metadata.form.error.'; + + /** + * All of the configured active languages + */ + activeLangs: LangConfig[]; + + constructor(@Inject(GLOBAL_CONFIG) protected config: GlobalConfig, + protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected epersonService: EPersonDataService) { + } + + ngOnInit(): void { + this.activeLangs = this.config.languages.filter((MyLangConfig) => MyLangConfig.active === true); + this.formModel.forEach( + (fieldModel: DynamicInputModel | DynamicSelectModel) => { + if (fieldModel.name === 'email') { + fieldModel.value = this.user.email; + } else { + fieldModel.value = this.user.firstMetadataValue(fieldModel.name); + } + if (fieldModel.id === 'language') { + (fieldModel as DynamicSelectModel).options = + this.activeLangs.map((langConfig) => Object.assign({ value: langConfig.code, label: langConfig.label })) + } + } + ); + this.formGroup = this.formService.createFormGroup(this.formModel); + this.updateFieldTranslations(); + this.translate.onLangChange + .subscribe(() => { + this.updateFieldTranslations(); + }); + } + + updateFieldTranslations() { + this.formModel.forEach( + (fieldModel: DynamicInputModel) => { + fieldModel.label = this.translate.instant(this.LABEL_PREFIX + fieldModel.id); + if (isNotEmpty(fieldModel.validators)) { + fieldModel.errorMessages = {}; + Object.keys(fieldModel.validators).forEach((key) => { + fieldModel.errorMessages[key] = this.translate.instant(this.ERROR_PREFIX + fieldModel.id + '.' + key); + }); + } + } + ); + } + + updateProfile() { + const newMetadata = Object.assign({}, this.user.metadata); + this.formModel.forEach((fieldModel: DynamicFormValueControlModel) => { + if (newMetadata.hasOwnProperty(fieldModel.name) && newMetadata[fieldModel.name].length > 0) { + if (hasValue(fieldModel.value)) { + newMetadata[fieldModel.name][0].value = fieldModel.value; + } else { + newMetadata[fieldModel.name] = []; + } + } else if (hasValue(fieldModel.value)) { + newMetadata[fieldModel.name] = [{ + value: fieldModel.value, + language: null + } as any]; + } + }); + this.epersonService.update(Object.assign(cloneDeep(this.user), { metadata: newMetadata })).pipe( + getSucceededRemoteData(), + getRemoteDataPayload() + ).subscribe((user) => { + this.user = user; + }); + } +} diff --git a/src/app/profile-page/profile-page-routing.module.ts b/src/app/profile-page/profile-page-routing.module.ts new file mode 100644 index 0000000000..4b9f2b7fff --- /dev/null +++ b/src/app/profile-page/profile-page-routing.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { ProfilePageComponent } from './profile-page.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: '', pathMatch: 'full', component: ProfilePageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'profile', title: 'profile.title' } } + ]) + ] +}) +export class ProfilePageRoutingModule { + +} diff --git a/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.html b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.html new file mode 100644 index 0000000000..59c6f5c248 --- /dev/null +++ b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.html @@ -0,0 +1,6 @@ +
{{'profile.security.form.info' | translate}}
+ + diff --git a/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.ts b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.ts new file mode 100644 index 0000000000..dd5ac478e2 --- /dev/null +++ b/src/app/profile-page/profile-page-security-form/profile-page-security-form.component.ts @@ -0,0 +1,56 @@ +import { Component, OnInit } from '@angular/core'; +import { + DynamicFormControlModel, + DynamicFormService, + DynamicInputModel +} from '@ng-dynamic-forms/core'; +import { TranslateService } from '@ngx-translate/core'; +import { FormGroup } from '@angular/forms'; + +@Component({ + selector: 'ds-profile-page-security-form', + templateUrl: './profile-page-security-form.component.html' +}) +export class ProfilePageSecurityFormComponent implements OnInit { + + formModel: DynamicFormControlModel[] = [ + new DynamicInputModel({ + id: 'password', + name: 'password', + inputType: 'password' + }), + new DynamicInputModel({ + id: 'passwordrepeat', + name: 'passwordrepeat', + inputType: 'password' + }) + ]; + + /** + * The form group of this form + */ + formGroup: FormGroup; + + LABEL_PREFIX = 'profile.security.form.label.'; + + constructor(protected formService: DynamicFormService, + protected translate: TranslateService) { + } + + ngOnInit(): void { + this.formGroup = this.formService.createFormGroup(this.formModel); + this.updateFieldTranslations(); + this.translate.onLangChange + .subscribe(() => { + this.updateFieldTranslations(); + }); + } + + updateFieldTranslations() { + this.formModel.forEach( + (fieldModel: DynamicInputModel) => { + fieldModel.label = this.translate.instant(this.LABEL_PREFIX + fieldModel.id); + } + ); + } +} diff --git a/src/app/profile-page/profile-page.component.html b/src/app/profile-page/profile-page.component.html new file mode 100644 index 0000000000..3b0b2d712b --- /dev/null +++ b/src/app/profile-page/profile-page.component.html @@ -0,0 +1,18 @@ + +
+

{{'profile.head' | translate}}

+
+
{{'profile.card.identify' | translate}}
+
+ +
+
+
+
{{'profile.card.security' | translate}}
+
+ +
+
+ +
+
diff --git a/src/app/profile-page/profile-page.component.ts b/src/app/profile-page/profile-page.component.ts new file mode 100644 index 0000000000..eb182aab4e --- /dev/null +++ b/src/app/profile-page/profile-page.component.ts @@ -0,0 +1,35 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { EPerson } from '../core/eperson/models/eperson.model'; +import { select, Store } from '@ngrx/store'; +import { getAuthenticatedUser } from '../core/auth/selectors'; +import { AppState } from '../app.reducer'; +import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component'; + +@Component({ + selector: 'ds-profile-page', + templateUrl: './profile-page.component.html' +}) +/** + * Component for a user to edit their profile information + */ +export class ProfilePageComponent implements OnInit { + + @ViewChild(ProfilePageMetadataFormComponent, { static: false }) metadataForm: ProfilePageMetadataFormComponent; + + /** + * The authenticated user + */ + user$: Observable; + + constructor(private store: Store) { + } + + ngOnInit(): void { + this.user$ = this.store.pipe(select(getAuthenticatedUser)); + } + + updateProfile() { + this.metadataForm.updateProfile(); + } +} diff --git a/src/app/profile-page/profile-page.module.ts b/src/app/profile-page/profile-page.module.ts new file mode 100644 index 0000000000..f40c125ff8 --- /dev/null +++ b/src/app/profile-page/profile-page.module.ts @@ -0,0 +1,23 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../shared/shared.module'; +import { ProfilePageRoutingModule } from './profile-page-routing.module'; +import { ProfilePageComponent } from './profile-page.component'; +import { ProfilePageMetadataFormComponent } from './profile-page-metadata-form/profile-page-metadata-form.component'; +import { ProfilePageSecurityFormComponent } from './profile-page-security-form/profile-page-security-form.component'; + +@NgModule({ + imports: [ + ProfilePageRoutingModule, + CommonModule, + SharedModule + ], + declarations: [ + ProfilePageComponent, + ProfilePageMetadataFormComponent, + ProfilePageSecurityFormComponent + ] +}) +export class ProfilePageModule { + +}