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 { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
|
||||||
import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
|
import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
|
||||||
import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
|
import { CoreUserAvatarComponent } from './user-avatar/user-avatar';
|
||||||
|
import { CoreDynamicComponent } from './dynamic-component/dynamic-component';
|
||||||
|
|
||||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
import { CorePipesModule } from '@pipes/pipes.module';
|
import { CorePipesModule } from '@pipes/pipes.module';
|
||||||
|
@ -63,6 +64,7 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||||
CoreContextMenuPopoverComponent,
|
CoreContextMenuPopoverComponent,
|
||||||
CoreNavBarButtonsComponent,
|
CoreNavBarButtonsComponent,
|
||||||
CoreUserAvatarComponent,
|
CoreUserAvatarComponent,
|
||||||
|
CoreDynamicComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -92,6 +94,7 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
|
||||||
CoreContextMenuPopoverComponent,
|
CoreContextMenuPopoverComponent,
|
||||||
CoreNavBarButtonsComponent,
|
CoreNavBarButtonsComponent,
|
||||||
CoreUserAvatarComponent,
|
CoreUserAvatarComponent,
|
||||||
|
CoreDynamicComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
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-item-divider class="ion-text-wrap">
|
||||||
<ion-label>{{ category.name }}</ion-label>
|
<ion-label>{{ category.name }}</ion-label>
|
||||||
</ion-item-divider>
|
</ion-item-divider>
|
||||||
<!-- @todo <core-user-profile-field *ngFor="let field of category.fields" [field]="field" edit="true" signup="true"
|
<core-user-profile-field *ngFor="let field of category.fields" [field]="field" [edit]="true" [signup]="true"
|
||||||
registerAuth="email" [form]="signupForm"></core-user-profile-field> -->
|
registerAuth="email" [form]="signupForm"></core-user-profile-field>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<!-- ReCAPTCHA -->
|
<!-- ReCAPTCHA -->
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { CoreComponentsModule } from '@components/components.module';
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CoreUserComponentsModule } from '@features/user/components/components.module';
|
||||||
|
|
||||||
import { CoreLoginEmailSignupPage } from './email-signup';
|
import { CoreLoginEmailSignupPage } from './email-signup';
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ const routes: Routes = [
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
CoreComponentsModule,
|
CoreComponentsModule,
|
||||||
CoreDirectivesModule,
|
CoreDirectivesModule,
|
||||||
|
CoreUserComponentsModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreLoginEmailSignupPage,
|
CoreLoginEmailSignupPage,
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { CoreWS, CoreWSExternalWarning } from '@services/ws';
|
||||||
import { CoreConstants } from '@/core/constants';
|
import { CoreConstants } from '@/core/constants';
|
||||||
import { Translate } from '@singletons';
|
import { Translate } from '@singletons';
|
||||||
import { CoreSitePublicConfigResponse } from '@classes/site';
|
import { CoreSitePublicConfigResponse } from '@classes/site';
|
||||||
|
import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthEmailSignupProfileFieldsCategory,
|
AuthEmailSignupProfileFieldsCategory,
|
||||||
|
@ -82,6 +83,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
||||||
protected navCtrl: NavController,
|
protected navCtrl: NavController,
|
||||||
protected fb: FormBuilder,
|
protected fb: FormBuilder,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
|
protected userProfileFieldDelegate: CoreUserProfileFieldDelegate,
|
||||||
) {
|
) {
|
||||||
// Create the ageVerificationForm.
|
// Create the ageVerificationForm.
|
||||||
this.ageVerificationForm = this.fb.group({
|
this.ageVerificationForm = this.fb.group({
|
||||||
|
@ -156,7 +158,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
||||||
if (typeof this.ageDigitalConsentVerification == 'undefined') {
|
if (typeof this.ageDigitalConsentVerification == 'undefined') {
|
||||||
|
|
||||||
const result = await CoreUtils.instance.ignoreErrors(
|
const result = await CoreUtils.instance.ignoreErrors(
|
||||||
CoreWS.instance.callAjax<IsAgeVerificationEnabledResponse>(
|
CoreWS.instance.callAjax<IsAgeVerificationEnabledWSResponse>(
|
||||||
'core_auth_is_age_digital_consent_verification_enabled',
|
'core_auth_is_age_digital_consent_verification_enabled',
|
||||||
{},
|
{},
|
||||||
{ siteUrl: this.siteUrl },
|
{ siteUrl: this.siteUrl },
|
||||||
|
@ -189,7 +191,11 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
||||||
{ siteUrl: this.siteUrl },
|
{ 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);
|
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 modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
|
||||||
|
|
||||||
const params: Record<string, unknown> = {
|
const params: SignupUserWSParams = {
|
||||||
username: this.signupForm.value.username.trim().toLowerCase(),
|
username: this.signupForm.value.username.trim().toLowerCase(),
|
||||||
password: this.signupForm.value.password,
|
password: this.signupForm.value.password,
|
||||||
firstname: CoreTextUtils.instance.cleanTags(this.signupForm.value.firstname),
|
firstname: CoreTextUtils.instance.cleanTags(this.signupForm.value.firstname),
|
||||||
|
@ -295,8 +301,15 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @todo Get the data for the custom profile fields.
|
// Get the data for the custom profile fields.
|
||||||
const result = await CoreWS.instance.callAjax<SignupUserResult>(
|
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',
|
'auth_email_signup_user',
|
||||||
params,
|
params,
|
||||||
{ siteUrl: this.siteUrl },
|
{ siteUrl: this.siteUrl },
|
||||||
|
@ -376,7 +389,7 @@ export class CoreLoginEmailSignupPage implements OnInit {
|
||||||
params.age = parseInt(params.age, 10); // Use just the integer part.
|
params.age = parseInt(params.age, 10); // Use just the integer part.
|
||||||
|
|
||||||
try {
|
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);
|
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.
|
* 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.
|
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.
|
* Result of WS auth_email_signup_user.
|
||||||
*/
|
*/
|
||||||
export type SignupUserResult = {
|
type SignupUserWSResult = {
|
||||||
success: boolean; // True if the user was created false otherwise.
|
success: boolean; // True if the user was created false otherwise.
|
||||||
warnings?: CoreWSExternalWarning[];
|
warnings?: CoreWSExternalWarning[];
|
||||||
};
|
};
|
||||||
|
@ -419,6 +453,6 @@ export type SignupUserResult = {
|
||||||
/**
|
/**
|
||||||
* Result of WS core_auth_is_minor.
|
* 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.
|
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-item class="ion-text-wrap" *ngIf="user.address">
|
||||||
<ion-label>
|
<ion-label>
|
||||||
<h2>{{ 'core.user.address' | translate}}</h2>
|
<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 }}
|
{{ user.address }}
|
||||||
</a></p>
|
</a></p>
|
||||||
</ion-label>
|
</ion-label>
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { CoreComponentsModule } from '@components/components.module';
|
import { CoreComponentsModule } from '@components/components.module';
|
||||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||||
|
import { CoreUserComponentsModule } from '@features/user/components/components.module';
|
||||||
|
|
||||||
import { CoreUserAboutPage } from './about.page';
|
import { CoreUserAboutPage } from './about.page';
|
||||||
|
|
||||||
|
@ -38,6 +39,7 @@ const routes: Routes = [
|
||||||
TranslateModule.forChild(),
|
TranslateModule.forChild(),
|
||||||
CoreComponentsModule,
|
CoreComponentsModule,
|
||||||
CoreDirectivesModule,
|
CoreDirectivesModule,
|
||||||
|
CoreUserComponentsModule,
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
CoreUserAboutPage,
|
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 { CoreMainMenuMoreRoutingModule } from '@features/mainmenu/pages/more/more-routing.module';
|
||||||
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||||
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/db/user';
|
import { SITE_SCHEMA, OFFLINE_SITE_SCHEMA } from './services/db/user';
|
||||||
|
import { CoreUserComponentsModule } from './components/components.module';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
|
@ -29,6 +30,7 @@ const routes: Routes = [
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CoreMainMenuMoreRoutingModule.forChild({ siblings: routes }),
|
CoreMainMenuMoreRoutingModule.forChild({ siblings: routes }),
|
||||||
|
CoreUserComponentsModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue