MOBILE-3592 user: Implement profile field delegate and component
parent
fa294d7135
commit
3722126b5b
|
@ -36,6 +36,7 @@ import { CoreContextMenuComponent } from './context-menu/context-menu';
|
|||
import { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
|
||||
import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
|
||||
import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
|
||||
import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
|
||||
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
|
@ -63,6 +64,7 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
|||
CoreContextMenuPopoverComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
CoreUserAvatarComponent,
|
||||
CoreDynamicComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
|
@ -92,6 +94,7 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
|||
CoreContextMenuPopoverComponent,
|
||||
CoreNavBarButtonsComponent,
|
||||
CoreUserAvatarComponent,
|
||||
CoreDynamicComponent,
|
||||
],
|
||||
})
|
||||
export class CoreComponentsModule {}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<!-- Content to display if no dynamic component. -->
|
||||
<ng-content *ngIf="!instance"></ng-content>
|
||||
|
||||
<!-- Container of the dynamic component -->
|
||||
<ng-container #dynamicComponent></ng-container>
|
|
@ -0,0 +1,198 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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 {
|
||||
Component,
|
||||
Input,
|
||||
ViewChild,
|
||||
OnChanges,
|
||||
DoCheck,
|
||||
ViewContainerRef,
|
||||
ComponentFactoryResolver,
|
||||
ComponentRef,
|
||||
KeyValueDiffers,
|
||||
SimpleChange,
|
||||
ChangeDetectorRef,
|
||||
Optional,
|
||||
ElementRef,
|
||||
KeyValueDiffer,
|
||||
Type,
|
||||
} from '@angular/core';
|
||||
import { NavController } from '@ionic/angular';
|
||||
|
||||
import { CoreDomUtils } from '@services/utils/dom';
|
||||
import { CoreLogger } from '@singletons/logger';
|
||||
|
||||
/**
|
||||
* Component to create another component dynamically.
|
||||
*
|
||||
* You need to pass the class of the component to this component (the class, not the name), along with the input data.
|
||||
*
|
||||
* So you should do something like:
|
||||
*
|
||||
* import { MyComponent } from './component';
|
||||
*
|
||||
* ...
|
||||
*
|
||||
* this.component = MyComponent;
|
||||
*
|
||||
* And in the template:
|
||||
*
|
||||
* <core-dynamic-component [component]="component" [data]="data">
|
||||
* <p>Cannot render the data.</p>
|
||||
* </core-dynamic-component>
|
||||
*
|
||||
* Please notice that the component that you pass needs to be declared in entryComponents of the module to be created dynamically.
|
||||
*
|
||||
* Alternatively, you can also supply a ComponentRef instead of the class of the component. In this case, the component won't
|
||||
* be instantiated because it already is, it will be attached to the view and the right data will be passed to it.
|
||||
* Passing ComponentRef is meant for site plugins, so we'll inject a NavController instance to the component.
|
||||
*
|
||||
* The contents of this component will be displayed if no component is supplied or it cannot be created. In the example above,
|
||||
* if no component is supplied then the template will show the message "Cannot render the data.".
|
||||
*/
|
||||
/* eslint-disable @angular-eslint/no-conflicting-lifecycle */
|
||||
@Component({
|
||||
selector: 'core-dynamic-component',
|
||||
templateUrl: 'core-dynamic-component.html',
|
||||
})
|
||||
export class CoreDynamicComponent implements OnChanges, DoCheck {
|
||||
|
||||
@Input() component?: Type<unknown>;
|
||||
@Input() data?: Record<string | number, unknown>;
|
||||
|
||||
// Get the container where to put the dynamic component.
|
||||
@ViewChild('dynamicComponent', { read: ViewContainerRef }) set dynamicComponent(el: ViewContainerRef) {
|
||||
this.container = el;
|
||||
this.createComponent();
|
||||
}
|
||||
|
||||
instance?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
container?: ViewContainerRef;
|
||||
|
||||
protected logger: CoreLogger;
|
||||
protected differ: KeyValueDiffer<unknown, unknown>; // To detect changes in the data input.
|
||||
protected lastComponent?: Type<unknown>;
|
||||
|
||||
constructor(
|
||||
protected factoryResolver: ComponentFactoryResolver,
|
||||
differs: KeyValueDiffers,
|
||||
@Optional() protected navCtrl: NavController,
|
||||
protected cdr: ChangeDetectorRef,
|
||||
protected element: ElementRef,
|
||||
) {
|
||||
|
||||
this.logger = CoreLogger.getInstance('CoreDynamicComponent');
|
||||
this.differ = differs.find([]).create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect changes on input properties.
|
||||
*/
|
||||
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
|
||||
if (changes.component && !this.component) {
|
||||
// Component not set, destroy the instance if any.
|
||||
this.lastComponent = undefined;
|
||||
this.instance = undefined;
|
||||
this.container?.clear();
|
||||
} else if (changes.component && (!this.instance || this.component != this.lastComponent)) {
|
||||
this.createComponent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect and act upon changes that Angular can’t or won’t detect on its own (objects and arrays).
|
||||
*/
|
||||
ngDoCheck(): void {
|
||||
if (this.instance) {
|
||||
// Check if there's any change in the data object.
|
||||
const changes = this.differ.diff(this.data || {});
|
||||
if (changes) {
|
||||
this.setInputData();
|
||||
if (this.instance.ngOnChanges) {
|
||||
this.instance.ngOnChanges(CoreDomUtils.instance.createChangesFromKeyValueDiff(changes));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a certain function on the component.
|
||||
*
|
||||
* @param name Name of the function to call.
|
||||
* @param params List of params to send to the function.
|
||||
* @return Result of the call. Undefined if no component instance or the function doesn't exist.
|
||||
*/
|
||||
callComponentFunction<T = unknown>(name: string, params?: unknown[]): T | undefined {
|
||||
if (this.instance && typeof this.instance[name] == 'function') {
|
||||
return this.instance[name].apply(this.instance, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a component, add it to a container and set the input data.
|
||||
*
|
||||
* @return Whether the component was successfully created.
|
||||
*/
|
||||
protected createComponent(): boolean {
|
||||
this.lastComponent = this.component;
|
||||
|
||||
if (!this.component || !this.container) {
|
||||
// No component to instantiate or container doesn't exist right now.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.instance) {
|
||||
// Component already instantiated.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.component instanceof ComponentRef) {
|
||||
// A ComponentRef was supplied instead of the component class. Add it to the view.
|
||||
this.container.insert(this.component.hostView);
|
||||
this.instance = this.component.instance;
|
||||
|
||||
// This feature is usually meant for site plugins. Inject some properties.
|
||||
this.instance['ChangeDetectorRef'] = this.cdr;
|
||||
this.instance['NavController'] = this.navCtrl;
|
||||
this.instance['componentContainer'] = this.element.nativeElement;
|
||||
} else {
|
||||
try {
|
||||
// Create the component and add it to the container.
|
||||
const factory = this.factoryResolver.resolveComponentFactory(this.component);
|
||||
const componentRef = this.container.createComponent(factory);
|
||||
|
||||
this.instance = componentRef.instance;
|
||||
} catch (ex) {
|
||||
this.logger.error('Error creating component', ex);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.setInputData();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the input data for the component.
|
||||
*/
|
||||
protected setInputData(): void {
|
||||
for (const name in this.data) {
|
||||
this.instance[name] = this.data[name];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -172,8 +172,8 @@
|
|||
<ion-item-divider class="ion-text-wrap">
|
||||
<ion-label>{{ category.name }}</ion-label>
|
||||
</ion-item-divider>
|
||||
<!-- @todo <core-user-profile-field *ngFor="let field of category.fields" [field]="field" edit="true" signup="true"
|
||||
registerAuth="email" [form]="signupForm"></core-user-profile-field> -->
|
||||
<core-user-profile-field *ngFor="let field of category.fields" [field]="field" [edit]="true" [signup]="true"
|
||||
registerAuth="email" [form]="signupForm"></core-user-profile-field>
|
||||
</ng-container>
|
||||
|
||||
<!-- ReCAPTCHA -->
|
||||
|
|
|
@ -21,6 +21,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CoreUserComponentsModule } from '@features/user/components/components.module';
|
||||
|
||||
import { CoreLoginEmailSignupPage } from './email-signup';
|
||||
|
||||
|
@ -41,6 +42,7 @@ const routes: Routes = [
|
|||
ReactiveFormsModule,
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreUserComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreLoginEmailSignupPage,
|
||||
|
|
|
@ -25,6 +25,7 @@ import { CoreWS, CoreWSExternalWarning } from '@services/ws';
|
|||
import { CoreConstants } from '@/core/constants';
|
||||
import { Translate } from '@singletons';
|
||||
import { CoreSitePublicConfigResponse } from '@classes/site';
|
||||
import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate';
|
||||
|
||||
import {
|
||||
AuthEmailSignupProfileFieldsCategory,
|
||||
|
@ -82,6 +83,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
|||
protected navCtrl: NavController,
|
||||
protected fb: FormBuilder,
|
||||
protected route: ActivatedRoute,
|
||||
protected userProfileFieldDelegate: CoreUserProfileFieldDelegate,
|
||||
) {
|
||||
// Create the ageVerificationForm.
|
||||
this.ageVerificationForm = this.fb.group({
|
||||
|
@ -156,7 +158,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
|||
if (typeof this.ageDigitalConsentVerification == 'undefined') {
|
||||
|
||||
const result = await CoreUtils.instance.ignoreErrors(
|
||||
CoreWS.instance.callAjax<IsAgeVerificationEnabledResponse>(
|
||||
CoreWS.instance.callAjax<IsAgeVerificationEnabledWSResponse>(
|
||||
'core_auth_is_age_digital_consent_verification_enabled',
|
||||
{},
|
||||
{ siteUrl: this.siteUrl },
|
||||
|
@ -189,7 +191,11 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
|||
{ siteUrl: this.siteUrl },
|
||||
);
|
||||
|
||||
// @todo userProfileFieldDelegate
|
||||
if (this.userProfileFieldDelegate.hasRequiredUnsupportedField(this.settings.profilefields)) {
|
||||
this.allRequiredSupported = false;
|
||||
|
||||
throw new Error(Translate.instance.instant('core.login.signuprequiredfieldnotsupported'));
|
||||
}
|
||||
|
||||
this.categories = CoreLoginHelper.instance.formatProfileFieldsForSignup(this.settings.profilefields);
|
||||
|
||||
|
@ -274,7 +280,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
|||
|
||||
const modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
|
||||
|
||||
const params: Record<string, unknown> = {
|
||||
const params: SignupUserWSParams = {
|
||||
username: this.signupForm.value.username.trim().toLowerCase(),
|
||||
password: this.signupForm.value.password,
|
||||
firstname: CoreTextUtils.instance.cleanTags(this.signupForm.value.firstname),
|
||||
|
@ -295,8 +301,15 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
|||
}
|
||||
|
||||
try {
|
||||
// @todo Get the data for the custom profile fields.
|
||||
const result = await CoreWS.instance.callAjax<SignupUserResult>(
|
||||
// Get the data for the custom profile fields.
|
||||
params.customprofilefields = await this.userProfileFieldDelegate.getDataForFields(
|
||||
this.settings?.profilefields,
|
||||
true,
|
||||
'email',
|
||||
this.signupForm.value,
|
||||
);
|
||||
|
||||
const result = await CoreWS.instance.callAjax<SignupUserWSResult>(
|
||||
'auth_email_signup_user',
|
||||
params,
|
||||
{ siteUrl: this.siteUrl },
|
||||
|
@ -376,7 +389,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
|||
params.age = parseInt(params.age, 10); // Use just the integer part.
|
||||
|
||||
try {
|
||||
const result = await CoreWS.instance.callAjax<IsMinorResult>('core_auth_is_minor', params, { siteUrl: this.siteUrl });
|
||||
const result = await CoreWS.instance.callAjax<IsMinorWSResult>('core_auth_is_minor', params, { siteUrl: this.siteUrl });
|
||||
|
||||
CoreDomUtils.instance.triggerFormSubmittedEvent(this.ageFormElement, true);
|
||||
|
||||
|
@ -404,14 +417,35 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
|||
/**
|
||||
* Result of WS core_auth_is_age_digital_consent_verification_enabled.
|
||||
*/
|
||||
export type IsAgeVerificationEnabledResponse = {
|
||||
type IsAgeVerificationEnabledWSResponse = {
|
||||
status: boolean; // True if digital consent verification is enabled, false otherwise.
|
||||
};
|
||||
|
||||
/**
|
||||
* Params for WS auth_email_signup_user.
|
||||
*/
|
||||
type SignupUserWSParams = {
|
||||
username: string; // Username.
|
||||
password: string; // Plain text password.
|
||||
firstname: string; // The first name(s) of the user.
|
||||
lastname: string; // The family name of the user.
|
||||
email: string; // A valid and unique email address.
|
||||
city?: string; // Home city of the user.
|
||||
country?: string; // Home country code.
|
||||
recaptchachallengehash?: string; // Recaptcha challenge hash.
|
||||
recaptcharesponse?: string; // Recaptcha response.
|
||||
customprofilefields?: { // User custom fields (also known as user profile fields).
|
||||
type: string; // The type of the custom field.
|
||||
name: string; // The name of the custom field.
|
||||
value: unknown; // Custom field value, can be an encoded json if required.
|
||||
}[];
|
||||
redirect?: string; // Redirect the user to this site url after confirmation.
|
||||
};
|
||||
|
||||
/**
|
||||
* Result of WS auth_email_signup_user.
|
||||
*/
|
||||
export type SignupUserResult = {
|
||||
type SignupUserWSResult = {
|
||||
success: boolean; // True if the user was created false otherwise.
|
||||
warnings?: CoreWSExternalWarning[];
|
||||
};
|
||||
|
@ -419,6 +453,6 @@ export type SignupUserResult = {
|
|||
/**
|
||||
* Result of WS core_auth_is_minor.
|
||||
*/
|
||||
export type IsMinorResult = {
|
||||
type IsMinorWSResult = {
|
||||
status: boolean; // True if the user is considered to be a digital minor, false if not.
|
||||
};
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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 { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { IonicModule } from '@ionic/angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CorePipesModule } from '@pipes/pipes.module';
|
||||
import { CoreUserProfileFieldComponent } from './user-profile-field/user-profile-field';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
CoreUserProfileFieldComponent,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CorePipesModule,
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
CoreUserProfileFieldComponent,
|
||||
],
|
||||
})
|
||||
export class CoreUserComponentsModule {}
|
|
@ -0,0 +1 @@
|
|||
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>
|
|
@ -0,0 +1,83 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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 { Component, Input, OnInit, Injector, Type } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
|
||||
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
|
||||
import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate';
|
||||
import { CoreUtils } from '@services/utils/utils';
|
||||
|
||||
/**
|
||||
* Directive to render user profile field.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'core-user-profile-field',
|
||||
templateUrl: 'core-user-profile-field.html',
|
||||
})
|
||||
export class CoreUserProfileFieldComponent implements OnInit {
|
||||
|
||||
@Input() field?: AuthEmailSignupProfileField; // The profile field to be rendered.
|
||||
@Input() signup = false; // True if editing the field in signup. Defaults to false.
|
||||
@Input() edit = false; // True if editing the field. Defaults to false.
|
||||
@Input() form?: FormGroup; // Form where to add the form control. Required if edit=true or signup=true.
|
||||
@Input() registerAuth?: string; // Register auth method. E.g. 'email'.
|
||||
@Input() contextLevel?: string; // The context level.
|
||||
@Input() contextInstanceId?: number; // The instance ID related to the context.
|
||||
@Input() courseId?: number; // Course ID the field belongs to (if any). It can be used to improve performance with filters.
|
||||
|
||||
componentClass?: Type<unknown>; // The class of the component to render.
|
||||
data: CoreUserProfileFieldComponentData = {}; // Data to pass to the component.
|
||||
|
||||
constructor(
|
||||
protected userProfileFieldsDelegate: CoreUserProfileFieldDelegate,
|
||||
protected injector: Injector,
|
||||
) { }
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
async ngOnInit(): Promise<void> {
|
||||
if (!this.field) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.componentClass = await this.userProfileFieldsDelegate.getComponent(this.injector, this.field, this.signup);
|
||||
|
||||
this.data.field = this.field;
|
||||
this.data.edit = CoreUtils.instance.isTrueOrOne(this.edit);
|
||||
if (this.edit) {
|
||||
this.data.signup = CoreUtils.instance.isTrueOrOne(this.signup);
|
||||
this.data.disabled = CoreUtils.instance.isTrueOrOne(this.field.locked);
|
||||
this.data.form = this.form;
|
||||
this.data.registerAuth = this.registerAuth;
|
||||
this.data.contextLevel = this.contextLevel;
|
||||
this.data.contextInstanceId = this.contextInstanceId;
|
||||
this.data.courseId = this.courseId;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type CoreUserProfileFieldComponentData = {
|
||||
field?: AuthEmailSignupProfileField;
|
||||
edit?: boolean;
|
||||
signup?: boolean;
|
||||
disabled?: boolean;
|
||||
form?: FormGroup;
|
||||
registerAuth?: string;
|
||||
contextLevel?: string;
|
||||
contextInstanceId?: number;
|
||||
courseId?: number;
|
||||
};
|
|
@ -41,7 +41,7 @@
|
|||
<ion-item class="ion-text-wrap" *ngIf="user.address">
|
||||
<ion-label>
|
||||
<h2>{{ 'core.user.address' | translate}}</h2>
|
||||
<p><a class="core-anchor" [href]="user.encodedAddress" core-link auto-login="no">
|
||||
<p><a class="core-anchor" [href]="encodedAddress" core-link auto-login="no">
|
||||
{{ user.address }}
|
||||
</a></p>
|
||||
</ion-label>
|
||||
|
|
|
@ -20,6 +20,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CoreUserComponentsModule } from '@features/user/components/components.module';
|
||||
|
||||
import { CoreUserAboutPage } from './about.page';
|
||||
|
||||
|
@ -38,6 +39,7 @@ const routes: Routes = [
|
|||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreUserComponentsModule,
|
||||
],
|
||||
declarations: [
|
||||
CoreUserAboutPage,
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
// (C) Copyright 2015 Moodle Pty Ltd.
|
||||
//
|
||||
// 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 { Injectable, Injector, Type } from '@angular/core';
|
||||
|
||||
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
|
||||
import { CoreError } from '@classes/errors/error';
|
||||
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
|
||||
import { CoreUserProfileField } from './user';
|
||||
|
||||
/**
|
||||
* Interface that all user profile field handlers must implement.
|
||||
*/
|
||||
export interface CoreUserProfileFieldHandler extends CoreDelegateHandler {
|
||||
/**
|
||||
* Type of the field the handler supports. E.g. 'checkbox'.
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Return the Component to use to display the user profile field.
|
||||
* It's recommended to return the class of the component, but you can also return an instance of the component.
|
||||
*
|
||||
* @param injector Injector.
|
||||
* @return The component (or promise resolved with component) to use, undefined if not found.
|
||||
*/
|
||||
getComponent(injector: Injector): Type<unknown> | Promise<Type<unknown>>;
|
||||
|
||||
/**
|
||||
* Get the data to send for the field based on the input data.
|
||||
*
|
||||
* @param field User field to get the data for.
|
||||
* @param signup True if user is in signup page.
|
||||
* @param registerAuth Register auth method. E.g. 'email'.
|
||||
* @param formValues Form Values.
|
||||
* @return Data to send for the field.
|
||||
*/
|
||||
getData?(
|
||||
field: AuthEmailSignupProfileField | CoreUserProfileField,
|
||||
signup: boolean,
|
||||
registerAuth: string,
|
||||
formValues: Record<string, unknown>,
|
||||
): Promise<CoreUserProfileFieldHandlerData>;
|
||||
}
|
||||
|
||||
export interface CoreUserProfileFieldHandlerData {
|
||||
/**
|
||||
* Name of the custom field.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The type of the custom field
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Value of the custom field.
|
||||
*/
|
||||
value: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service to interact with user profile fields.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CoreUserProfileFieldDelegate extends CoreDelegate<CoreUserProfileFieldHandler> {
|
||||
|
||||
protected handlerNameProperty = 'type';
|
||||
|
||||
constructor() {
|
||||
super('CoreUserProfileFieldDelegate', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of a field.
|
||||
*
|
||||
* @param field The field to get its type.
|
||||
* @return The field type.
|
||||
*/
|
||||
protected getType(field: AuthEmailSignupProfileField | CoreUserProfileField): string {
|
||||
return ('type' in field ? field.type : field.datatype) || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to use to display an user field.
|
||||
*
|
||||
* @param injector Injector.
|
||||
* @param field User field to get the directive for.
|
||||
* @param signup True if user is in signup page.
|
||||
* @return Promise resolved with component to use, undefined if not found.
|
||||
*/
|
||||
async getComponent(
|
||||
injector: Injector,
|
||||
field: AuthEmailSignupProfileField | CoreUserProfileField,
|
||||
signup: boolean,
|
||||
): Promise<Type<unknown> | undefined> {
|
||||
const type = this.getType(field);
|
||||
|
||||
try {
|
||||
if (signup) {
|
||||
return await this.executeFunction(type, 'getComponent', [injector]);
|
||||
} else {
|
||||
return await this.executeFunctionOnEnabled(type, 'getComponent', [injector]);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Error getting component for field', type, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to send for a certain field based on the input data.
|
||||
*
|
||||
* @param field User field to get the data for.
|
||||
* @param signup True if user is in signup page.
|
||||
* @param registerAuth Register auth method. E.g. 'email'.
|
||||
* @param formValues Form values.
|
||||
* @return Data to send for the field.
|
||||
*/
|
||||
async getDataForField(
|
||||
field: AuthEmailSignupProfileField | CoreUserProfileField,
|
||||
signup: boolean,
|
||||
registerAuth: string,
|
||||
formValues: Record<string, unknown>,
|
||||
): Promise<CoreUserProfileFieldHandlerData> {
|
||||
const type = this.getType(field);
|
||||
const handler = this.getHandler(type, !signup);
|
||||
|
||||
if (handler) {
|
||||
const name = 'profile_field_' + field.shortname;
|
||||
|
||||
if (handler.getData) {
|
||||
return await handler.getData(field, signup, registerAuth, formValues);
|
||||
} else if (field.shortname && typeof formValues[name] != 'undefined') {
|
||||
// Handler doesn't implement the function, but the form has data for the field.
|
||||
return {
|
||||
type: type,
|
||||
name: name,
|
||||
value: formValues[name],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new CoreError('User profile field handler not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data to send for a list of fields based on the input data.
|
||||
*
|
||||
* @param fields User fields to get the data for.
|
||||
* @param signup True if user is in signup page.
|
||||
* @param registerAuth Register auth method. E.g. 'email'.
|
||||
* @param formValues Form values.
|
||||
* @return Data to send.
|
||||
*/
|
||||
async getDataForFields(
|
||||
fields: (AuthEmailSignupProfileField | CoreUserProfileField)[] | undefined,
|
||||
signup: boolean = false,
|
||||
registerAuth: string = '',
|
||||
formValues: Record<string, unknown>,
|
||||
): Promise<CoreUserProfileFieldHandlerData[]> {
|
||||
if (!fields) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const result: CoreUserProfileFieldHandlerData[] = [];
|
||||
|
||||
await Promise.all(fields.map(async (field) => {
|
||||
try {
|
||||
const data = await this.getDataForField(field, signup, registerAuth, formValues);
|
||||
|
||||
if (data) {
|
||||
result.push(data);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors.
|
||||
}
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any of the profile fields is not supported in the app.
|
||||
*
|
||||
* @param fields List of fields.
|
||||
* @return Whether any of the profile fields is not supported in the app.
|
||||
*/
|
||||
hasRequiredUnsupportedField(fields?: AuthEmailSignupProfileField[]): boolean {
|
||||
if (!fields || !fields.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return fields.some((field) => field.required && !this.hasHandler(this.getType(field)));
|
||||
}
|
||||
|
||||
}
|
|
@ -18,6 +18,7 @@ import { Routes } from '@angular/router';
|
|||
import { CoreMainMenuMoreRoutingModule } from '@features/mainmenu/pages/more/more-routing.module';
|
||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/db/user';
|
||||
import { CoreUserComponentsModule } from './components/components.module';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
|
@ -29,6 +30,7 @@ const routes: Routes = [
|
|||
@NgModule({
|
||||
imports: [
|
||||
CoreMainMenuMoreRoutingModule.forChild({ siblings: routes }),
|
||||
CoreUserComponentsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue