Merge pull request #2647 from dpalou/MOBILE-3592

Mobile 3592
main
Dani Palou 2020-12-14 15:56:04 +01:00 committed by GitHub
commit 5e085f69b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
112 changed files with 7792 additions and 416 deletions

32
package-lock.json generated
View File

@ -2380,18 +2380,18 @@
}
},
"@ionic/angular": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-5.3.4.tgz",
"integrity": "sha512-eqw03rWvQKM0pcuxPxQvXeCUGhcaQCZ+yqQRp9A6cBDE6u6JJS2tGs6OyZ07hhyDF8IJ5BLpJwjiRjEDnhyTDQ==",
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/@ionic/angular/-/angular-5.5.2.tgz",
"integrity": "sha512-yXIydPTIMAX4RobidAByaQ/y+yMS6FYgwEs08GTN/GyvQ4XeWVbojwTm62ILLN2qYS/80ok2uupFwlcyKSMztw==",
"requires": {
"@ionic/core": "5.3.4",
"@ionic/core": "5.5.2",
"tslib": "^1.9.3"
},
"dependencies": {
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
@ -2424,18 +2424,18 @@
}
},
"@ionic/core": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.3.4.tgz",
"integrity": "sha512-4UVzj+Vd7o0VJ06dReG01PvttnLLPSzUVgXSYMBKKR849Pvuh5Q9t5s4GEEQgGoxhv1S6Ai+zphWGFMvviOyfw==",
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-5.5.2.tgz",
"integrity": "sha512-rOfPj8D5NRWdOYYulNTdKtMAMURfmutDQ3ciA3L7daCooG3MWt2/0siiL6rcZFMxfG7KDxHctuwVwYoC1mPuhg==",
"requires": {
"ionicons": "^5.1.2",
"tslib": "^1.10.0"
},
"dependencies": {
"tslib": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
"integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
}
}
},
@ -11042,9 +11042,9 @@
"dev": true
},
"ionicons": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-5.1.2.tgz",
"integrity": "sha512-zO7ZgbBbXhpA7cXO2rDzTNdcCqErjg1Sprq/ossTvaiV0MriOjRE7JO3EGvYjDTPzF9YALGpvLXqCgsRT0tprA=="
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-5.2.3.tgz",
"integrity": "sha512-87qtgBkieKVFagwYA9Cf91B3PCahQbEOMwMt8bSvlQSgflZ4eE5qI4MGj2ZlIyadeX0dgo+0CzZsy3ow0CsBAg=="
},
"ios-sim": {
"version": "8.0.2",

View File

@ -64,7 +64,7 @@
"@ionic-native/status-bar": "^5.0.0",
"@ionic-native/web-intent": "^5.28.0",
"@ionic-native/zip": "^5.28.0",
"@ionic/angular": "^5.0.0",
"@ionic/angular": "^5.5.2",
"@ngx-translate/core": "^13.0.0",
"@ngx-translate/http-loader": "^6.0.0",
"@types/cordova": "0.0.34",

View File

@ -16,11 +16,13 @@ import { NgModule } from '@angular/core';
import { AddonPrivateFilesModule } from './privatefiles/privatefiles.module';
import { AddonFilterModule } from './filter/filter.module';
import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield.module';
@NgModule({
imports: [
AddonPrivateFilesModule,
AddonFilterModule,
AddonUserProfileFieldModule,
],
})
export class AddonsModule {}

View File

@ -12,23 +12,33 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Injector, NgModule } from '@angular/core';
import { RouterModule, ROUTES, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
redirectTo: 'root', // Fake "hash".
pathMatch: 'full',
},
{
path: ':hash',
loadChildren: () => import('./pages/index/index.module').then(m => m.AddonPrivateFilesIndexPageModule),
},
];
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
function buildRoutes(injector: Injector): Routes {
return [
{
path: ':hash',
loadChildren: () => import('./pages/index/index.module').then(m => m.AddonPrivateFilesIndexPageModule),
},
...buildTabMainRoutes(injector, {
redirectTo: 'root', // Fake "hash".
pathMatch: 'full',
}),
];
}
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
{
provide: ROUTES,
multi: true,
deps: [Injector],
useFactory: buildRoutes,
},
],
})
export class AddonPrivateFilesLazyModule {}

View File

@ -0,0 +1,54 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonUserProfileFieldCheckboxHandler } from './services/handlers/checkbox';
import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate';
import { AddonUserProfileFieldCheckboxComponent } from './component/checkbox';
import { CoreComponentsModule } from '@components/components.module';
@NgModule({
declarations: [
AddonUserProfileFieldCheckboxComponent,
],
imports: [
CommonModule,
IonicModule.forRoot(),
TranslateModule.forChild(),
FormsModule,
ReactiveFormsModule,
CoreComponentsModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () =>
CoreUserProfileFieldDelegate.instance.registerHandler(AddonUserProfileFieldCheckboxHandler.instance),
},
],
exports: [
AddonUserProfileFieldCheckboxComponent,
],
entryComponents: [
AddonUserProfileFieldCheckboxComponent,
],
})
export class AddonUserProfileFieldCheckboxModule {}

View File

@ -0,0 +1,22 @@
<!-- Render (no edit). -->
<ion-item *ngIf="!edit && field && field.name">
<ion-label>
<h2>{{ field.name }}</h2>
<p *ngIf="value != '0'">
{{ 'core.yes' | translate }}
</p>
<p *ngIf="value == '0'">
{{ 'core.no' | translate }}
</p>
</ion-label>
</ion-item>
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname && form" [formGroup]="form">
<ion-label>
<span [core-mark-required]="required">{{ field.name }}</span>
<core-input-errors [control]="form.controls[modelName]"></core-input-errors>
</ion-label>
<ion-checkbox item-end [formControlName]="modelName">
</ion-checkbox>
</ion-item>

View File

@ -0,0 +1,45 @@
// (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 } from '@angular/core';
import { Validators, FormControl } from '@angular/forms';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component';
import { CoreUtils } from '@services/utils/utils';
/**
* Directive to render a checkbox user profile field.
*/
@Component({
selector: 'addon-user-profile-field-checkbox',
templateUrl: 'addon-user-profile-field-checkbox.html',
})
export class AddonUserProfileFieldCheckboxComponent extends CoreUserProfileFieldBaseComponent {
/**
* Create the Form control.
*
* @return Form control.
*/
protected createFormControl(field: AuthEmailSignupProfileField): FormControl {
const formData = {
value: CoreUtils.instance.isTrueOrOne(field.defaultdata),
disabled: this.disabled,
};
return new FormControl(formData, this.required && !field.locked ? Validators.requiredTrue : null);
}
}

View File

@ -0,0 +1,79 @@
// (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, Type } from '@angular/core';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileField } from '@features/user/services/user';
import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate';
import { makeSingleton } from '@singletons';
import { AddonUserProfileFieldCheckboxComponent } from '../../component/checkbox';
/**
* Checkbox user profile field handlers.
*/
@Injectable({ providedIn: 'root' })
export class AddonUserProfileFieldCheckboxHandlerService implements CoreUserProfileFieldHandler {
name = 'AddonUserProfileFieldCheckbox';
type = 'checkbox';
/**
* Whether or not the handler is enabled on a site level.
*
* @return Promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* 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.
*/
async getData(
field: AuthEmailSignupProfileField | CoreUserProfileField,
signup: boolean,
registerAuth: string,
formValues: Record<string, unknown>,
): Promise<CoreUserProfileFieldHandlerData | undefined> {
const name = 'profile_field_' + field.shortname;
if (typeof formValues[name] != 'undefined') {
return {
type: 'checkbox',
name: name,
value: formValues[name] ? 1 : 0,
};
}
}
/**
* 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.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): Type<unknown> | Promise<Type<unknown>> {
return AddonUserProfileFieldCheckboxComponent;
}
}
export class AddonUserProfileFieldCheckboxHandler extends makeSingleton(AddonUserProfileFieldCheckboxHandlerService) {}

View File

@ -0,0 +1,18 @@
<!-- Render (no edit). -->
<ion-item *ngIf="!edit && field && field.name">
<ion-label>
<h2>{{ field.name }}</h2>
<p>{{ valueNumber * 1000 | coreFormatDate }}</p>
</ion-label>
</ion-item>
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname && form" text-wrap [formGroup]="form">
<ion-label position="stacked">
<span [core-mark-required]="required">{{ field.name }}</span>
</ion-label>
<ion-datetime [formControlName]="modelName" [placeholder]="'core.choosedots' | translate" [displayFormat]="format"
[max]="max" [min]="min">
</ion-datetime>
<core-input-errors [control]="form.controls[modelName]"></core-input-errors>
</ion-item>

View File

@ -0,0 +1,95 @@
// (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 { FormControl, Validators } from '@angular/forms';
import { Component } from '@angular/core';
import { CoreTimeUtils } from '@services/utils/time';
import { CoreUtils } from '@services/utils/utils';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileField } from '@features/user/services/user';
import { Translate } from '@singletons';
import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component';
/**
* Directive to render a datetime user profile field.
*/
@Component({
selector: 'addon-user-profile-field-datetime',
templateUrl: 'addon-user-profile-field-datetime.html',
})
export class AddonUserProfileFieldDatetimeComponent extends CoreUserProfileFieldBaseComponent {
format?: string;
min?: number;
max?: number;
valueNumber = 0;
/**
* Init the data when the field is meant to be displayed without editing.
*
* @param field Field to render.
*/
protected initForNonEdit(field: CoreUserProfileField): void {
this.valueNumber = Number(field.value);
}
/**
* Init the data when the field is meant to be displayed for editing.
*
* @param field Field to render.
*/
protected initForEdit(field: AuthEmailSignupProfileField): void {
super.initForEdit(field);
// Check if it's only date or it has time too.
const hasTime = CoreUtils.instance.isTrueOrOne(field.param3);
// Calculate format to use.
this.format = CoreTimeUtils.instance.fixFormatForDatetime(CoreTimeUtils.instance.convertPHPToMoment(
Translate.instance.instant('core.' + (hasTime ? 'strftimedatetime' : 'strftimedate')),
));
// Check min value.
if (field.param1) {
const year = parseInt(field.param1, 10);
if (year) {
this.min = year;
}
}
// Check max value.
if (field.param2) {
const year = parseInt(field.param2, 10);
if (year) {
this.max = year;
}
}
}
/**
* Create the Form control.
*
* @return Form control.
*/
protected createFormControl(field: AuthEmailSignupProfileField): FormControl {
const formData = {
value: field.defaultdata != '0' ? field.defaultdata : undefined,
disabled: this.disabled,
};
return new FormControl(formData, this.required && !field.locked ? Validators.required : null);
}
}

View File

@ -0,0 +1,56 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonUserProfileFieldDatetimeHandler } from './services/handlers/datetime';
import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate';
import { AddonUserProfileFieldDatetimeComponent } from './component/datetime';
import { CoreComponentsModule } from '@components/components.module';
import { CorePipesModule } from '@pipes/pipes.module';
@NgModule({
declarations: [
AddonUserProfileFieldDatetimeComponent,
],
imports: [
CommonModule,
IonicModule.forRoot(),
TranslateModule.forChild(),
FormsModule,
ReactiveFormsModule,
CoreComponentsModule,
CorePipesModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () =>
CoreUserProfileFieldDelegate.instance.registerHandler(AddonUserProfileFieldDatetimeHandler.instance),
},
],
exports: [
AddonUserProfileFieldDatetimeComponent,
],
entryComponents: [
AddonUserProfileFieldDatetimeComponent,
],
})
export class AddonUserProfileFieldDatetimeModule {}

View File

@ -0,0 +1,81 @@
// (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, Type } from '@angular/core';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileField } from '@features/user/services/user';
import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate';
import { CoreTimeUtils } from '@services/utils/time';
import { makeSingleton } from '@singletons';
import { AddonUserProfileFieldDatetimeComponent } from '../../component/datetime';
/**
* Datetime user profile field handlers.
*/
@Injectable({ providedIn: 'root' })
export class AddonUserProfileFieldDatetimeHandlerService implements CoreUserProfileFieldHandler {
name = 'AddonUserProfileFieldDatetime';
type = 'datetime';
/**
* Whether or not the handler is enabled on a site level.
*
* @return Promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* 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.
*/
async getData(
field: AuthEmailSignupProfileField | CoreUserProfileField,
signup: boolean,
registerAuth: string,
formValues: Record<string, unknown>,
): Promise<CoreUserProfileFieldHandlerData | undefined> {
const name = 'profile_field_' + field.shortname;
if (formValues[name]) {
return {
type: 'datetime',
name: 'profile_field_' + field.shortname,
value: CoreTimeUtils.instance.convertToTimestamp(<string> formValues[name]),
};
}
}
/**
* 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(): Type<unknown> | Promise<Type<unknown>> {
return AddonUserProfileFieldDatetimeComponent;
}
}
export class AddonUserProfileFieldDatetimeHandler extends makeSingleton(AddonUserProfileFieldDatetimeHandlerService) {}

View File

@ -0,0 +1,21 @@
<!-- Render (no edit). -->
<ion-item *ngIf="!edit && field && field.name">
<ion-label>
<h2>{{ field.name }}</h2>
<p><core-format-text [text]="value" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
[courseId]="courseId">
</core-format-text></p>
</ion-label>
</ion-item>
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname && form" text-wrap [formGroup]="form">
<ion-label position="stacked">
<span [core-mark-required]="required">{{ field.name }}</span>
</ion-label>
<ion-select [formControlName]="modelName" [placeholder]="'core.choosedots' | translate" interface="action-sheet">
<ion-select-option value="">{{ 'core.choosedots' | translate }}</ion-select-option>
<ion-select-option *ngFor="let option of options" [value]="option">{{option}}</ion-select-option>
</ion-select>
<core-input-errors [control]="form.controls[modelName]"></core-input-errors>
</ion-item>

View File

@ -0,0 +1,47 @@
// (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 } from '@angular/core';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component';
/**
* Directive to render a menu user profile field.
*/
@Component({
selector: 'addon-user-profile-field-menu',
templateUrl: 'addon-user-profile-field-menu.html',
})
export class AddonUserProfileFieldMenuComponent extends CoreUserProfileFieldBaseComponent {
options?: string[];
/**
* Init the data when the field is meant to be displayed for editing.
*
* @param field Field to render.
*/
protected initForEdit(field: AuthEmailSignupProfileField): void {
super.initForEdit(field);
// Parse options.
if (field.param1) {
this.options = field.param1.split(/\r\n|\r|\n/g);
} else {
this.options = [];
}
}
}

View File

@ -0,0 +1,56 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonUserProfileFieldMenuHandler } from './services/handlers/menu';
import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate';
import { AddonUserProfileFieldMenuComponent } from './component/menu';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonUserProfileFieldMenuComponent,
],
imports: [
CommonModule,
IonicModule.forRoot(),
TranslateModule.forChild(),
FormsModule,
ReactiveFormsModule,
CoreComponentsModule,
CoreDirectivesModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () =>
CoreUserProfileFieldDelegate.instance.registerHandler(AddonUserProfileFieldMenuHandler.instance),
},
],
exports: [
AddonUserProfileFieldMenuComponent,
],
entryComponents: [
AddonUserProfileFieldMenuComponent,
],
})
export class AddonUserProfileFieldMenuModule {}

View File

@ -0,0 +1,79 @@
// (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, Type } from '@angular/core';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileField } from '@features/user/services/user';
import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate';
import { makeSingleton } from '@singletons';
import { AddonUserProfileFieldMenuComponent } from '../../component/menu';
/**
* Menu user profile field handlers.
*/
@Injectable({ providedIn: 'root' })
export class AddonUserProfileFieldMenuHandlerService implements CoreUserProfileFieldHandler {
name = 'AddonUserProfileFieldMenu';
type = 'menu';
/**
* Whether or not the handler is enabled on a site level.
*
* @return Promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* 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.
*/
async getData(
field: AuthEmailSignupProfileField | CoreUserProfileField,
signup: boolean,
registerAuth: string,
formValues: Record<string, unknown>,
): Promise<CoreUserProfileFieldHandlerData | undefined> {
const name = 'profile_field_' + field.shortname;
if (formValues[name]) {
return {
type: 'menu',
name: name,
value: formValues[name],
};
}
}
/**
* 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.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): Type<unknown> | Promise<Type<unknown>> {
return AddonUserProfileFieldMenuComponent;
}
}
export class AddonUserProfileFieldMenuHandler extends makeSingleton(AddonUserProfileFieldMenuHandlerService) {}

View File

@ -0,0 +1,18 @@
<!-- Render (no edit). -->
<ion-item *ngIf="!edit && field && field.name">
<ion-label>
<h2>{{ field.name }}</h2>
<p><core-format-text [text]="value" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
[courseId]="courseId">
</core-format-text></p>
</ion-label>
</ion-item>
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname && form" text-wrap [formGroup]="form">
<ion-label position="stacked">
<span [core-mark-required]="required">{{ field.name }}</span>
</ion-label>
<ion-input [type]="inputType" [formControlName]="modelName" [placeholder]="field.name" maxlength="{{maxLength}}"></ion-input>
<core-input-errors [control]="form.controls[modelName]"></core-input-errors>
</ion-item>

View File

@ -0,0 +1,50 @@
// (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 } from '@angular/core';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component';
import { CoreUtils } from '@services/utils/utils';
/**
* Directive to render a text user profile field.
*/
@Component({
selector: 'addon-user-profile-field-text',
templateUrl: 'addon-user-profile-field-text.html',
})
export class AddonUserProfileFieldTextComponent extends CoreUserProfileFieldBaseComponent {
inputType?: string;
maxLength?: number;
/**
* Init the data when the field is meant to be displayed for editing.
*
* @param field Field to render.
*/
protected initForEdit(field: AuthEmailSignupProfileField): void {
super.initForEdit(field);
// Check max length.
if (field.param2) {
this.maxLength = parseInt(field.param2, 10) || Number.MAX_VALUE;
}
// Check if it's a password or text.
this.inputType = CoreUtils.instance.isTrueOrOne(field.param3) ? 'password' : 'text';
}
}

View File

@ -0,0 +1,78 @@
// (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, Type } from '@angular/core';
import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate';
import { AddonUserProfileFieldTextComponent } from '../../component/text';
import { CoreTextUtils } from '@services/utils/text';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileField } from '@features/user/services/user';
import { makeSingleton } from '@singletons';
/**
* Text user profile field handlers.
*/
@Injectable({ providedIn: 'root' })
export class AddonUserProfileFieldTextHandlerService implements CoreUserProfileFieldHandler {
name = 'AddonUserProfileFieldText';
type = 'text';
/**
* Whether or not the handler is enabled on a site level.
*
* @return True or promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* 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.
*/
async getData(
field: AuthEmailSignupProfileField | CoreUserProfileField,
signup: boolean,
registerAuth: string,
formValues: Record<string, unknown>,
): Promise<CoreUserProfileFieldHandlerData | undefined> {
const name = 'profile_field_' + field.shortname;
return {
type: 'text',
name: name,
value: CoreTextUtils.instance.cleanTags(<string> formValues[name]),
};
}
/**
* 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.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): Type<unknown> | Promise<Type<unknown>> {
return AddonUserProfileFieldTextComponent;
}
}
export class AddonUserProfileFieldTextHandler extends makeSingleton(AddonUserProfileFieldTextHandlerService) {}

View File

@ -0,0 +1,56 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonUserProfileFieldTextHandler } from './services/handlers/text';
import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate';
import { AddonUserProfileFieldTextComponent } from './component/text';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
@NgModule({
declarations: [
AddonUserProfileFieldTextComponent,
],
imports: [
CommonModule,
IonicModule.forRoot(),
TranslateModule.forChild(),
FormsModule,
ReactiveFormsModule,
CoreComponentsModule,
CoreDirectivesModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () =>
CoreUserProfileFieldDelegate.instance.registerHandler(AddonUserProfileFieldTextHandler.instance),
},
],
exports: [
AddonUserProfileFieldTextComponent,
],
entryComponents: [
AddonUserProfileFieldTextComponent,
],
})
export class AddonUserProfileFieldTextModule {}

View File

@ -0,0 +1,20 @@
<!-- Render (no edit). -->
<ion-item *ngIf="!edit && field && field.name">
<ion-label>
<h2>{{ field.name }}</h2>
<p><core-format-text [text]="value" [contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId"
[courseId]="courseId">
</core-format-text></p>
</ion-label>
</ion-item>
<!-- Edit. -->
<ion-item *ngIf="edit && field && field.shortname" text-wrap [formGroup]="form">
<ion-label position="stacked">
<span [core-mark-required]="required">{{ field.name }}</span>
<core-input-errors [control]="control"></core-input-errors>
</ion-label>
<core-rich-text-editor item-content [control]="control" [placeholder]="field.name" [autoSave]="true"
[contextLevel]="contextLevel" [contextInstanceId]="contextInstanceId" [elementId]="modelName">
</core-rich-text-editor>
</ion-item>

View File

@ -0,0 +1,26 @@
// (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 } from '@angular/core';
import { CoreUserProfileFieldBaseComponent } from '@features/user/classes/base-profilefield-component';
/**
* Directive to render a textarea user profile field.
*/
@Component({
selector: 'addon-user-profile-field-textarea',
templateUrl: 'addon-user-profile-field-textarea.html',
})
export class AddonUserProfileFieldTextareaComponent extends CoreUserProfileFieldBaseComponent {}

View File

@ -0,0 +1,88 @@
// (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, Type } from '@angular/core';
import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@features/user/services/user-profile-field-delegate';
import { AddonUserProfileFieldTextareaComponent } from '../../component/textarea';
import { CoreTextUtils } from '@services/utils/text';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileField } from '@features/user/services/user';
import { makeSingleton } from '@singletons';
/**
* Textarea user profile field handlers.
*/
@Injectable({ providedIn: 'root' })
export class AddonUserProfileFieldTextareaHandlerService implements CoreUserProfileFieldHandler {
name = 'AddonUserProfileFieldTextarea';
type = 'textarea';
/**
* Whether or not the handler is enabled on a site level.
*
* @return True or promise resolved with true if enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* 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.
*/
async getData(
field: AuthEmailSignupProfileField | CoreUserProfileField,
signup: boolean,
registerAuth: string,
formValues: Record<string, unknown>,
): Promise<CoreUserProfileFieldHandlerData | undefined> {
const name = 'profile_field_' + field.shortname;
if (formValues[name]) {
let text = <string> formValues[name] || '';
// Add some HTML to the message in case the user edited with textarea.
text = CoreTextUtils.instance.formatHtmlLines(text);
return {
type: 'textarea',
name: name,
value: JSON.stringify({
text: text,
format: 1, // Always send this format.
}),
};
}
}
/**
* 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(): Type<unknown> | Promise<Type<unknown>> {
return AddonUserProfileFieldTextareaComponent;
}
}
export class AddonUserProfileFieldTextareaHandler extends makeSingleton(AddonUserProfileFieldTextareaHandlerService) {}

View File

@ -0,0 +1,58 @@
// (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 { APP_INITIALIZER, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { AddonUserProfileFieldTextareaHandler } from './services/handlers/textarea';
import { CoreUserProfileFieldDelegate } from '@features/user/services/user-profile-field-delegate';
import { AddonUserProfileFieldTextareaComponent } from './component/textarea';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreEditorComponentsModule } from '@features/editor/components/components.module';
@NgModule({
declarations: [
AddonUserProfileFieldTextareaComponent,
],
imports: [
CommonModule,
IonicModule.forRoot(),
TranslateModule.forChild(),
FormsModule,
ReactiveFormsModule,
CoreComponentsModule,
CoreDirectivesModule,
CoreEditorComponentsModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () =>
CoreUserProfileFieldDelegate.instance.registerHandler(AddonUserProfileFieldTextareaHandler.instance),
},
],
exports: [
AddonUserProfileFieldTextareaComponent,
],
entryComponents: [
AddonUserProfileFieldTextareaComponent,
],
})
export class AddonUserProfileFieldTextareaModule {}

View File

@ -0,0 +1,33 @@
// (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 { AddonUserProfileFieldCheckboxModule } from './checkbox/checkbox.module';
import { AddonUserProfileFieldDatetimeModule } from './datetime/datetime.module';
import { AddonUserProfileFieldMenuModule } from './menu/menu.module';
import { AddonUserProfileFieldTextModule } from './text/text.module';
import { AddonUserProfileFieldTextareaModule } from './textarea/textarea.module';
@NgModule({
declarations: [],
imports: [
AddonUserProfileFieldCheckboxModule,
AddonUserProfileFieldDatetimeModule,
AddonUserProfileFieldMenuModule,
AddonUserProfileFieldTextModule,
AddonUserProfileFieldTextareaModule,
],
exports: [],
})
export class AddonUserProfileFieldModule { }

View File

@ -0,0 +1,307 @@
// (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 { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreSync } from '@services/sync';
import { CoreTextUtils } from '@services/utils/text';
import { CoreTimeUtils } from '@services/utils/time';
import { Translate } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreError } from '@classes/errors/error';
/**
* Blocked sync error.
*/
export class CoreSyncBlockedError extends CoreError {}
/**
* Base class to create sync providers. It provides some common functions.
*/
export class CoreSyncBaseProvider<T = void> {
/**
* Logger instance.
*/
protected logger: CoreLogger;
/**
* Component of the sync provider.
*/
component = 'core';
/**
* Sync provider's interval.
*/
syncInterval = 300000;
// Store sync promises.
protected syncPromises: { [siteId: string]: { [uniqueId: string]: Promise<T> } } = {};
constructor(component: string) {
this.logger = CoreLogger.getInstance(component);
this.component = component;
}
/**
* Add an offline data deleted warning to a list of warnings.
*
* @param warnings List of warnings.
* @param component Component.
* @param name Instance name.
* @param error Specific error message.
*/
protected addOfflineDataDeletedWarning(warnings: string[], component: string, name: string, error: string): void {
const warning = Translate.instance.instant('core.warningofflinedatadeleted', {
component: component,
name: name,
error: error,
});
if (warnings.indexOf(warning) == -1) {
warnings.push(warning);
}
}
/**
* Add an ongoing sync to the syncPromises list. On finish the promise will be removed.
*
* @param id Unique sync identifier per component.
* @param promise The promise of the sync to add.
* @param siteId Site ID. If not defined, current site.
* @return The sync promise.
*/
async addOngoingSync(id: string | number, promise: Promise<T>, siteId?: string): Promise<T> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
if (!siteId) {
throw new CoreError('CoreSyncBaseProvider: Site ID not supplied');
}
const uniqueId = this.getUniqueSyncId(id);
if (!this.syncPromises[siteId]) {
this.syncPromises[siteId] = {};
}
this.syncPromises[siteId][uniqueId] = promise;
// Promise will be deleted when finish.
try {
return await promise;
} finally {
delete this.syncPromises[siteId!][uniqueId];
}
}
/**
* If there's an ongoing sync for a certain identifier return it.
*
* @param id Unique sync identifier per component.
* @param siteId Site ID. If not defined, current site.
* @return Promise of the current sync or undefined if there isn't any.
*/
getOngoingSync(id: string | number, siteId?: string): Promise<T> | undefined {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
if (!this.isSyncing(id, siteId)) {
return;
}
// There's already a sync ongoing for this id, return the promise.
const uniqueId = this.getUniqueSyncId(id);
return this.syncPromises[siteId][uniqueId];
}
/**
* Get the synchronization time in a human readable format.
*
* @param id Unique sync identifier per component.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the readable time.
*/
async getReadableSyncTime(id: string | number, siteId?: string): Promise<string> {
const time = await this.getSyncTime(id, siteId);
return this.getReadableTimeFromTimestamp(time);
}
/**
* Given a timestamp return it in a human readable format.
*
* @param timestamp Timestamp
* @return Human readable time.
*/
getReadableTimeFromTimestamp(timestamp: number): string {
if (!timestamp) {
return Translate.instance.instant('core.never');
} else {
return CoreTimeUtils.instance.userDate(timestamp);
}
}
/**
* Get the synchronization time. Returns 0 if no time stored.
*
* @param id Unique sync identifier per component.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the time.
*/
async getSyncTime(id: string | number, siteId?: string): Promise<number> {
try {
const entry = await CoreSync.instance.getSyncRecord(this.component, id, siteId);
return entry.time;
} catch {
return 0;
}
}
/**
* Get the synchronization warnings of an instance.
*
* @param id Unique sync identifier per component.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the warnings.
*/
async getSyncWarnings(id: string | number, siteId?: string): Promise<string[]> {
try {
const entry = await CoreSync.instance.getSyncRecord(this.component, id, siteId);
return <string[]> CoreTextUtils.instance.parseJSON(entry.warnings, []);
} catch {
return [];
}
}
/**
* Create a unique identifier from component and id.
*
* @param id Unique sync identifier per component.
* @return Unique identifier from component and id.
*/
protected getUniqueSyncId(id: string | number): string {
return this.component + '#' + id;
}
/**
* Check if a there's an ongoing syncronization for the given id.
*
* @param id Unique sync identifier per component.
* @param siteId Site ID. If not defined, current site.
* @return Whether it's synchronizing.
*/
isSyncing(id: string | number, siteId?: string): boolean {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const uniqueId = this.getUniqueSyncId(id);
return !!(this.syncPromises[siteId] && this.syncPromises[siteId][uniqueId]);
}
/**
* Check if a sync is needed: if a certain time has passed since the last time.
*
* @param id Unique sync identifier per component.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with boolean: whether sync is needed.
*/
async isSyncNeeded(id: string | number, siteId?: string): Promise<boolean> {
const time = await this.getSyncTime(id, siteId);
return Date.now() - this.syncInterval >= time;
}
/**
* Set the synchronization time.
*
* @param id Unique sync identifier per component.
* @param siteId Site ID. If not defined, current site.
* @param time Time to set. If not defined, current time.
* @return Promise resolved when the time is set.
*/
async setSyncTime(id: string, siteId?: string, time?: number): Promise<void> {
time = typeof time != 'undefined' ? time : Date.now();
await CoreSync.instance.insertOrUpdateSyncRecord(this.component, id, { time: time }, siteId);
}
/**
* Set the synchronization warnings.
*
* @param id Unique sync identifier per component.
* @param warnings Warnings to set.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async setSyncWarnings(id: string, warnings: string[], siteId?: string): Promise<void> {
const warningsText = JSON.stringify(warnings || []);
await CoreSync.instance.insertOrUpdateSyncRecord(this.component, id, { warnings: warningsText }, siteId);
}
/**
* Execute a sync function on selected sites.
*
* @param syncFunctionLog Log message to explain the sync function purpose.
* @param syncFunction Sync function to execute.
* @param siteId Site ID to sync. If not defined, sync all sites.
* @return Resolved with siteIds selected. Rejected if offline.
*/
async syncOnSites(syncFunctionLog: string, syncFunction: (siteId: string) => void, siteId?: string): Promise<void> {
if (!CoreApp.instance.isOnline()) {
const message = `Cannot sync '${syncFunctionLog}' because device is offline.`;
this.logger.debug(message);
throw new CoreError(message);
}
let siteIds: string[] = [];
if (!siteId) {
// No site ID defined, sync all sites.
this.logger.debug(`Try to sync '${syncFunctionLog}' in all sites.`);
siteIds = await CoreSites.instance.getLoggedInSitesIds();
} else {
this.logger.debug(`Try to sync '${syncFunctionLog}' in site '${siteId}'.`);
siteIds = [siteId];
}
// Execute function for every site.
await Promise.all(siteIds.map((siteId) => syncFunction(siteId)));
}
/**
* If there's an ongoing sync for a certain identifier, wait for it to end.
* If there's no sync ongoing the promise will be resolved right away.
*
* @param id Unique sync identifier per component.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when there's no sync going on for the identifier.
*/
async waitForSync(id: string | number, siteId?: string): Promise<T | undefined> {
const promise = this.getOngoingSync(id, siteId);
if (!promise) {
return;
}
try {
return await promise;
} catch {
return;
}
}
}

View File

@ -86,7 +86,7 @@ export class CoreDelegate<HandlerType extends CoreDelegateHandler> {
* @param delegateName Delegate name used for logging purposes.
* @param listenSiteEvents Whether to update the handler when a site event occurs (login, site updated, ...).
*/
constructor(delegateName: string, listenSiteEvents?: boolean) {
constructor(delegateName: string, listenSiteEvents: boolean = true) {
this.logger = CoreLogger.getInstance(delegateName);
this.handlersInitPromise = new Promise((resolve): void => {

View File

@ -17,7 +17,7 @@ import { Md5 } from 'ts-md5/dist/md5';
import { CoreApp } from '@services/app';
import { CoreDB } from '@services/db';
import { CoreEvents } from '@singletons/events';
import { CoreEvents, CoreEventUserDeletedData } from '@singletons/events';
import { CoreFile } from '@services/file';
import {
CoreWS,
@ -588,7 +588,7 @@ export class CoreSite {
error.message = Translate.instance.instant('core.lostconnection');
} else if (error.errorcode === 'userdeleted') {
// User deleted, trigger event.
CoreEvents.trigger(CoreEvents.USER_DELETED, { params: data }, this.id);
CoreEvents.trigger<CoreEventUserDeletedData>(CoreEvents.USER_DELETED, { params: data }, this.id);
error.message = Translate.instance.instant('core.userdeleted');
throw new CoreWSError(error);

View File

@ -35,6 +35,8 @@ import { CoreProgressBarComponent } from './progress-bar/progress-bar';
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';
@ -61,6 +63,8 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
CoreContextMenuItemComponent,
CoreContextMenuPopoverComponent,
CoreNavBarButtonsComponent,
CoreUserAvatarComponent,
CoreDynamicComponent,
],
imports: [
CommonModule,
@ -89,6 +93,8 @@ import { CoreNavBarButtonsComponent } from './navbar-buttons/navbar-buttons';
CoreContextMenuItemComponent,
CoreContextMenuPopoverComponent,
CoreNavBarButtonsComponent,
CoreUserAvatarComponent,
CoreDynamicComponent,
],
})
export class CoreComponentsModule {}

View File

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

View File

@ -0,0 +1,200 @@
// (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;
// Use a timeout to avoid ExpressionChangedAfterItHasBeenCheckedError.
setTimeout(() => 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 cant or wont 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];
}
}
}

View File

@ -6,7 +6,7 @@
<ion-icon *ngIf="showPrevButton" name="fas-chevron-left"></ion-icon>
</ion-col>
<ion-col class="ion-no-padding" size="10">
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slideOpts" [dir]="direction" role="tablist"
<ion-slides (ionSlideDidChange)="slideChanged()" [options]="slidesOpts" [dir]="direction" role="tablist"
[attr.aria-label]="description" aria-hidden="false">
<ng-container *ngFor="let tab of tabs">
<ion-slide [hidden]="!hideUntil" [attr.aria-selected]="selected == tab.id" class="tab-slide"

View File

@ -82,7 +82,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
direction = 'ltr';
description = '';
lastScroll = 0;
slideOpts = {
slidesOpts = {
initialSlide: 0,
slidesPerView: 3,
centerInsufficientSlides: true,
@ -381,13 +381,12 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
protected async updateSlides(): Promise<void> {
this.numTabsShown = this.tabs.reduce((prev: number, current: CoreTab) => current.enabled ? prev + 1 : prev, 0);
this.slideOpts.slidesPerView = Math.min(this.maxSlides, this.numTabsShown);
this.slidesSwiper.params.slidesPerView = this.slideOpts.slidesPerView;
this.slidesOpts = { ...this.slidesOpts, slidesPerView: Math.min(this.maxSlides, this.numTabsShown) };
this.calculateTabBarHeight();
await this.slides!.update();
if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slideOpts.slidesPerView) {
if (!this.hasSliddenToInitial && this.selectedIndex && this.selectedIndex >= this.slidesOpts.slidesPerView) {
this.hasSliddenToInitial = true;
this.shouldSlideToInitial = true;
@ -637,6 +636,7 @@ export class CoreTabsComponent implements OnInit, AfterViewInit, OnChanges, OnDe
window.removeEventListener('resize', this.resizeFunction);
}
this.stackEventsSubscription?.unsubscribe();
this.languageChangedSubscription.unsubscribe();
}
}

View File

@ -0,0 +1,11 @@
<img *ngIf="avatarUrl" [src]="avatarUrl" [alt]="'core.pictureof' | translate:{$a: fullname}" core-external-content
onError="this.src='assets/img/user-avatar.png'" role="presentation" (click)="gotoProfile($event)">
<img *ngIf="!avatarUrl" src="assets/img/user-avatar.png" [alt]="'core.pictureof' | translate:{$a: fullname}" role="presentation"
(click)="gotoProfile($event)">
<span *ngIf="checkOnline && isOnline()" class="contact-status online"></span>
<img *ngIf="extraIcon" [src]="extraIcon" alt="" role="presentation" class="core-avatar-extra-icon">
<ng-content></ng-content>

View File

@ -0,0 +1,43 @@
:host {
position: relative;
cursor: pointer;
img {
border-radius: 50%;
width: var(--core-avatar-size);
height: var(--core-avatar-size);
}
.contact-status {
position: absolute;
right: 0;
bottom: 0;
width: 14px;
height: 14px;
border-radius: 50%;
&.online {
border: 1px solid white;
background-color: var(--core-online-color);
}
}
.core-avatar-extra-icon {
margin: 0 !important;
border-radius: 0 !important;
background: none;
position: absolute;
right: -4px;
bottom: -4px;
width: 24px;
height: 24px;
}
}
:host-context(.toolbar) .contact-status {
width: 10px;
height: 10px;
}
:host-context([dir="rtl"]) .contact-status {
left: 0;
right: unset;
}

View File

@ -0,0 +1,179 @@
// (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, OnChanges, OnDestroy, SimpleChange } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NavController } from '@ionic/angular';
import { CoreApp } from '@services/app';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import { CoreObject } from '@singletons/object';
import { CoreUserProvider, CoreUserBasicData, CoreUserProfilePictureUpdatedData } from '@features/user/services/user';
/**
* Component to display a "user avatar".
*
* Example: <core-user-avatar [user]="participant"></core-user-avatar>
*/
@Component({
selector: 'core-user-avatar',
templateUrl: 'core-user-avatar.html',
styleUrls: ['user-avatar.scss'],
})
export class CoreUserAvatarComponent implements OnInit, OnChanges, OnDestroy {
@Input() user?: CoreUserWithAvatar;
// The following params will override the ones in user object.
@Input() profileUrl?: string;
@Input() protected linkProfile = true; // Avoid linking to the profile if wanted.
@Input() fullname?: string;
@Input() protected userId?: number; // If provided or found it will be used to link the image to the profile.
@Input() protected courseId?: number;
@Input() checkOnline = false; // If want to check and show online status.
@Input() extraIcon?: string; // Extra icon to show near the avatar.
avatarUrl?: string;
// Variable to check if we consider this user online or not.
// @TODO: Use setting when available (see MDL-63972) so we can use site setting.
protected timetoshowusers = 300000; // Miliseconds default.
protected currentUserId: number;
protected pictureObserver: CoreEventObserver;
constructor(
protected navCtrl: NavController,
protected route: ActivatedRoute,
) {
this.currentUserId = CoreSites.instance.getCurrentSiteUserId();
this.pictureObserver = CoreEvents.on<CoreUserProfilePictureUpdatedData>(
CoreUserProvider.PROFILE_PICTURE_UPDATED,
(data) => {
if (data.userId == this.userId) {
this.avatarUrl = data.picture;
}
},
CoreSites.instance.getCurrentSiteId(),
);
}
/**
* Component being initialized.
*/
ngOnInit(): void {
this.setFields();
}
/**
* Listen to changes.
*/
ngOnChanges(changes: { [name: string]: SimpleChange }): void {
// If something change, update the fields.
if (changes) {
this.setFields();
}
}
/**
* Set fields from user.
*/
protected setFields(): void {
const profileUrl = this.profileUrl || (this.user && (this.user.profileimageurl || this.user.userprofileimageurl ||
this.user.userpictureurl || this.user.profileimageurlsmall || (this.user.urls && this.user.urls.profileimage)));
if (typeof profileUrl == 'string') {
this.avatarUrl = profileUrl;
}
this.fullname = this.fullname || (this.user && (this.user.fullname || this.user.userfullname));
this.userId = this.userId || (this.user && (this.user.userid || this.user.id));
this.courseId = this.courseId || (this.user && this.user.courseid);
}
/**
* Helper function for checking the time meets the 'online' condition.
*
* @return boolean
*/
isOnline(): boolean {
if (!this.user) {
return false;
}
if (CoreUtils.instance.isFalseOrZero(this.user.isonline)) {
return false;
}
if (this.user.lastaccess) {
// If the time has passed, don't show the online status.
const time = new Date().getTime() - this.timetoshowusers;
return this.user.lastaccess * 1000 >= time;
} else {
// You have to have Internet access first.
return !!this.user.isonline && CoreApp.instance.isOnline();
}
}
/**
* Go to user profile.
*
* @param event Click event.
*/
gotoProfile(event: Event): void {
if (!this.linkProfile || !this.userId) {
return;
}
event.preventDefault();
event.stopPropagation();
// @todo Decide which navCtrl to use. If this component is inside a split view, use the split view's master nav.
this.navCtrl.navigateForward(['user'], {
relativeTo: this.route,
queryParams: CoreObject.removeUndefined({
userId: this.userId,
courseId: this.courseId,
}),
});
}
/**
* Component destroyed.
*/
ngOnDestroy(): void {
this.pictureObserver.off();
}
}
/**
* Type with all possible formats of user.
*/
type CoreUserWithAvatar = CoreUserBasicData & {
userpictureurl?: string;
userprofileimageurl?: string;
profileimageurlsmall?: string;
urls?: {
profileimage?: string;
};
userfullname?: string;
userid?: number;
isonline?: boolean;
courseid?: number;
lastaccess?: number;
};

View File

@ -22,6 +22,7 @@ import { CoreLinkDirective } from './link';
import { CoreLongPressDirective } from './long-press';
import { CoreSupressEventsDirective } from './supress-events';
import { CoreFaIconDirective } from './fa-icon';
import { CoreUserLinkDirective } from './user-link';
@NgModule({
declarations: [
@ -33,6 +34,7 @@ import { CoreFaIconDirective } from './fa-icon';
CoreSupressEventsDirective,
CoreFabDirective,
CoreFaIconDirective,
CoreUserLinkDirective,
],
imports: [],
exports: [
@ -44,6 +46,7 @@ import { CoreFaIconDirective } from './fa-icon';
CoreSupressEventsDirective,
CoreFabDirective,
CoreFaIconDirective,
CoreUserLinkDirective,
],
})
export class CoreDirectivesModule {}

View File

@ -0,0 +1,64 @@
// (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 { Directive, Input, OnInit, ElementRef } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { NavController } from '@ionic/angular';
import { CoreNavHelper } from '@services/nav-helper';
import { CoreObject } from '@singletons/object';
/**
* Directive to go to user profile on click.
*/
@Directive({
selector: '[core-user-link]',
})
export class CoreUserLinkDirective implements OnInit {
@Input() userId?: number; // User id to open the profile.
@Input() courseId?: number; // If set, course id to show the user info related to that course.
protected element: HTMLElement;
constructor(
element: ElementRef,
protected navCtrl: NavController,
protected route: ActivatedRoute,
) {
this.element = element.nativeElement;
}
/**
* Function executed when the component is initialized.
*/
ngOnInit(): void {
this.element.addEventListener('click', (event) => {
// If the event prevented default action, do nothing.
if (event.defaultPrevented || !this.userId) {
return;
}
event.preventDefault();
event.stopPropagation();
// @todo If this directive is inside a split view, use the split view's master nav.
CoreNavHelper.instance.goInCurrentMainMenuTab('user', CoreObject.removeUndefined({
userId: this.userId,
courseId: this.courseId,
}));
});
}
}

View File

@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { CoreContentLinksHelper } from '../services/contentlinks-helper';
import { CoreContentLinksHandlerBase } from './base-handler';
import { Translate } from '@singletons';
import { Params } from '@angular/router';
import { CoreContentLinksAction } from '../services/contentlinks-delegate';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Handler to handle URLs pointing to a list of a certain type of modules.
@ -65,7 +65,7 @@ export class CoreContentLinksModuleListHandler extends CoreContentLinksHandlerBa
title: this.title || Translate.instance.instant('addon.mod_' + this.modName + '.modulenameplural'),
};
CoreContentLinksHelper.instance.goInSite('CoreCourseListModTypePage @todo', stateParams, siteId);
CoreNavHelper.instance.goInSite('CoreCourseListModTypePage @todo', stateParams, siteId);
},
}];
}

View File

@ -17,11 +17,11 @@ import { NavController } from '@ionic/angular';
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreContentLinksAction } from '../../services/contentlinks-delegate';
import { CoreContentLinksHelper } from '../../services/contentlinks-helper';
import { ActivatedRoute } from '@angular/router';
import { CoreError } from '@classes/errors/error';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Page to display the list of sites to choose one to perform a content link action.
@ -102,7 +102,7 @@ export class CoreContentLinksChooseSitePage implements OnInit {
*/
siteClicked(siteId: string): void {
if (this.isRootURL) {
CoreLoginHelper.instance.redirect('', {}, siteId);
CoreNavHelper.instance.openInSiteMainMenu('', {}, siteId);
} else if (this.action) {
this.action.action(siteId);
}

View File

@ -16,13 +16,11 @@ import { Injectable } from '@angular/core';
import { NavController } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreUtils } from '@services/utils/utils';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreContentLinksDelegate, CoreContentLinksAction } from './contentlinks-delegate';
import { CoreSite } from '@classes/site';
import { CoreMainMenu } from '@features/mainmenu/services/mainmenu';
import { makeSingleton, NgZone, Translate } from '@singletons';
import { makeSingleton, Translate } from '@singletons';
import { Params } from '@angular/router';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Service that provides some features regarding content links.
@ -94,50 +92,10 @@ export class CoreContentLinksHelperProvider {
* @param siteId Site ID. If not defined, current site.
* @param checkMenu If true, check if the root page of a main menu tab. Only the page name will be checked.
* @return Promise resolved when done.
* @deprecated since 3.9.5. Use CoreNavHelperService.goInSite instead.
*/
goInSite(
pageName: string,
pageParams: Params,
siteId?: string,
checkMenu?: boolean,
): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const deferred = CoreUtils.instance.promiseDefer<void>();
// Execute the code in the Angular zone, so change detection doesn't stop working.
NgZone.instance.run(async () => {
try {
if (siteId == CoreSites.instance.getCurrentSiteId()) {
if (checkMenu) {
let isInMenu = false;
// Check if the page is in the main menu.
try {
isInMenu = await CoreMainMenu.instance.isCurrentMainMenuHandler(pageName);
} catch {
isInMenu = false;
}
if (isInMenu) {
// Just select the tab. @todo test.
CoreLoginHelper.instance.loadPageInMainMenu(pageName, pageParams);
} else {
await this.navCtrl.navigateForward(pageName, { queryParams: pageParams });
}
} else {
await this.navCtrl.navigateForward(pageName, { queryParams: pageParams });
}
} else {
await CoreLoginHelper.instance.redirect(pageName, pageParams, siteId);
}
deferred.resolve();
} catch (error) {
deferred.reject(error);
}
});
return deferred.promise;
goInSite(pageName: string, pageParams: Params, siteId?: string, checkMenu?: boolean): Promise<void> {
return CoreNavHelper.instance.goInSite(pageName, pageParams, siteId, checkMenu);
}
/**
@ -234,7 +192,7 @@ export class CoreContentLinksHelperProvider {
}
} else {
// Login in the site.
return CoreLoginHelper.instance.redirect('', {}, site.getId());
return CoreNavHelper.instance.openInSiteMainMenu('', {}, site.getId());
}
}

View File

@ -32,9 +32,9 @@ import {
} from '@features/courses/services/courses';
import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper';
import { CoreArray } from '@singletons/array';
import { CoreLoginHelper, CoreLoginHelperProvider } from '@features/login/services/login-helper';
import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreCourseOffline } from './course-offline';
import { CoreNavHelper, CoreNavHelperService } from '@services/nav-helper';
/**
* Prefetch info of a module.
@ -938,7 +938,7 @@ export class CoreCourseHelperProvider {
params = params || {};
Object.assign(params, { course: course });
return CoreLoginHelper.instance.redirect(CoreLoginHelperProvider.OPEN_COURSE, params, siteId);
return CoreNavHelper.instance.openInSiteMainMenu(CoreNavHelperService.OPEN_COURSE, params, siteId);
}
}

View File

@ -43,19 +43,15 @@
<h2>{{ 'core.teachers' | translate }}</h2>
</ion-label>
</ion-item-divider>
<!-- @todo <ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link
[userId]="contact.id"
[courseId]="isEnrolled ? course.id : null"
[attr.aria-label]="'core.viewprofile' | translate">
<ion-avatar core-user-avatar
[user]="contact" slot="start"
[userId]="contact.id"
<ion-item class="ion-text-wrap" *ngFor="let contact of course.contacts" core-user-link [userId]="contact.id"
[courseId]="isEnrolled ? course.id : null" [attr.aria-label]="'core.viewprofile' | translate">
<core-user-avatar [user]="contact" slot="start" [userId]="contact.id"
[courseId]="isEnrolled ? course.id : null">
</ion-avatar>
</core-user-avatar>
<ion-label>
<h2>{{contact.fullname}}</h2>
</ion-label>
</ion-item>-->
</ion-item>
<ion-item-divider></ion-item-divider>
</ng-container>

View File

@ -0,0 +1,42 @@
// (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 { CoreEditorRichTextEditorComponent } from './rich-text-editor/rich-text-editor';
import { CoreComponentsModule } from '@components/components.module';
@NgModule({
declarations: [
CoreEditorRichTextEditorComponent,
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
],
providers: [
],
exports: [
CoreEditorRichTextEditorComponent,
],
entryComponents: [
CoreEditorRichTextEditorComponent,
],
})
export class CoreEditorComponentsModule {}

View File

@ -0,0 +1,113 @@
<div class="core-rte-editor-container" (click)="focusRTE()" [class.toolbar-hidden]="toolbarHidden">
<div [hidden]="!rteEnabled" #editor contenteditable="true" class="core-rte-editor" button (focus)="showToolbar($event)"
(longPress)="showToolbar($event)" (blur)="hideToolbar($event)" [attr.data-placeholder-text]="placeholder" role="textbox">
</div>
<ion-textarea [hidden]="rteEnabled" #textarea class="core-textarea" [placeholder]="placeholder" [attr.name]="name"
ngControl="control" (ionChange)="onChange()" (focus)="showToolbar($event)" (longPress)="showToolbar($event)"
(blur)="hideToolbar($event)" role="textbox">
</ion-textarea>
<div class="core-rte-info-message" *ngIf="infoMessage">
<ion-icon name="fas-info-circle"></ion-icon>
{{ infoMessage | translate }}
</div>
</div>
<div #toolbar class="core-rte-toolbar" [class.toolbar-hidden]="toolbarHidden">
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarPrevHidden"
(click)="toolbarPrev($event)" (mousedown)="mouseDownAction($event)">
<ion-icon name="fas-chevron-left"></ion-icon>
</button>
<ion-slides [options]="slidesOpts" [dir]="direction" (ionSlideDidChange)="updateToolbarArrows()">
<!-- https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand -->
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strong" [title]="'core.editor.bold' | translate"
(click)="buttonAction($event, 'bold', 'strong')" (mousedown)="mouseDownAction($event)">
<core-icon name="fas-bold"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.em" (click)="buttonAction($event, 'italic', 'em')"
(mousedown)="mouseDownAction($event)" [title]=" 'core.editor.italic' | translate">
<core-icon name="fas-italic"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.u" (click)="buttonAction($event, 'underline', 'u')"
(mousedown)="mouseDownAction($event)" [title]="'core.editor.underline' | translate">
<core-icon name="fas-underline"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.strike" [title]="'core.editor.strike' | translate"
(click)="buttonAction($event, 'strikethrough', 'strike')" (mousedown)="mouseDownAction($event)">
<core-icon name="fas-strikethrough"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.p" (click)="buttonAction($event, 'p', 'block')"
(mousedown)="mouseDownAction($event)" [title]="'core.editor.p' | translate">
<core-icon name="fas-paragraph"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h3" (click)="buttonAction($event, 'h3', 'block')"
(mousedown)="mouseDownAction($event)" [title]="'core.editor.h3' | translate">
<core-icon name="fas-heading"></core-icon>3
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h4" (click)="buttonAction($event, 'h4', 'block')"
(mousedown)="mouseDownAction($event)" [title]="'core.editor.h4' | translate">
<core-icon name="fas-heading"></core-icon>4
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.h5" (click)="buttonAction($event, 'h5', 'block')"
(mousedown)="mouseDownAction($event)" [title]="'core.editor.h5' | translate">
<core-icon name="fas-heading"></core-icon>5
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.ul" (mousedown)="mouseDownAction($event)"
(click)="buttonAction($event, 'insertUnorderedList')" [title]="'core.editor.unorderedlist' | translate">
<core-icon name="fas-list-ul"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" [attr.aria-pressed]="toolbarStyles.ol" (mousedown)="mouseDownAction($event)"
(click)="buttonAction($event, 'insertOrderedList')" [title]="'core.editor.orderedlist' | translate">
<core-icon name="fas-list-ol"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [disabled]="!rteEnabled" (click)="buttonAction($event, 'removeFormat')" (mousedown)="mouseDownAction($event)"
[title]="'core.editor.clear' | translate">
<core-icon name="fas-eraser"></core-icon>
</button>
</ion-slide>
<ion-slide *ngIf="canScanQR">
<button [disabled]="!rteEnabled" (click)="scanQR($event)" (mousedown)="stopBubble($event)"
[title]="'core.scanqr' | translate">
<core-icon name="fas-qrcode"></core-icon>
</button>
</ion-slide>
<ion-slide>
<button [attr.aria-pressed]="!rteEnabled" (click)="toggleEditor($event)" (mousedown)="mouseDownAction($event)"
[title]=" 'core.editor.toggle' | translate">
<core-icon name="fas-code"></core-icon>
</button>
</ion-slide>
<ion-slide *ngIf="isPhone">
<button (click)="hideToolbar($event)" (mousedown)="mouseDownAction($event)"
[title]=" 'core.editor.hidetoolbar' | translate">
<core-icon name="fas-times"></core-icon>
</button>
</ion-slide>
</ion-slides>
<button *ngIf="toolbarArrows" class="toolbar-arrow" [class.toolbar-arrow-hidden]="toolbarNextHidden"
(click)="toolbarNext($event)" (mousedown)="mouseDownAction($event)">
<ion-icon name="fas-chevron-right"></ion-icon>
</button>
</div>

View File

@ -0,0 +1,181 @@
:host {
height: 40vh;
overflow: hidden;
min-height: 200px; /* Just in case vh is not supported */
min-height: 40vh;
width: 100%;
display: flex;
flex-direction: column;
// @include darkmode() {
// background-color: $gray-darker;
// }
.core-rte-editor-container {
max-height: calc(100% - 46px);
display: flex;
flex-direction: column;
flex-grow: 1;
&.toolbar-hidden {
max-height: 100%;
}
.core-rte-info-message {
padding: 5px;
border-top: 1px solid var(--ion-color-secondary);
background: white;
flex-shrink: 1;
font-size: 1.4rem;
.icon {
color: var(--ion-color-secondary);
}
}
}
.core-rte-editor, .core-textarea {
padding: 2px;
margin: 2px;
width: 100%;
resize: none;
background-color: white;
flex-grow: 1;
// @include darkmode() {
// background-color: var(--gray-darker);
// color: var(--white);
// }
}
.core-rte-editor {
flex-grow: 1;
flex-shrink: 1;
-webkit-user-select: auto !important;
word-wrap: break-word;
overflow-x: hidden;
overflow-y: auto;
cursor: text;
img {
// @include padding(null, null, null, 2px);
max-width: 95%;
width: auto;
}
&:empty:before {
content: attr(data-placeholder-text);
display: block;
color: var(--gray-light);
font-weight: bold;
// @include darkmode() {
// color: $gray;
// }
}
// Make empty elements selectable (to move the cursor).
*:empty:after {
content: '\200B';
}
}
.core-textarea {
flex-grow: 1;
flex-shrink: 1;
position: relative;
textarea {
margin: 0 !important;
padding: 0;
height: 100% !important;
width: 100% !important;
resize: none;
overflow-x: hidden;
overflow-y: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}
div.core-rte-toolbar {
display: flex;
width: 100%;
z-index: 1;
flex-grow: 0;
flex-shrink: 0;
background-color: var(--white);
// @include darkmode() {
// background-color: $black;
// }
// @include padding(5px, null);
border-top: 1px solid var(--gray);
ion-slides {
width: 240px;
flex-grow: 1;
flex-shrink: 1;
}
button {
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
padding-right: 6px;
padding-left: 6px;
margin: 0 auto;
font-size: 18px;
background-color: var(--white);
border-radius: 4px;
// @include core-transition(background-color, 200ms);
color: var(--ion-text-color);
cursor: pointer;
// @include darkmode() {
// background-color: $black;
// color: $core-dark-text-color;
// }
&.toolbar-button-enable {
width: 100%;
}
&:active, &[aria-pressed="true"] {
background-color: var(--gray);
// @include darkmode() {
// background-color: $gray-dark;
// }
}
&.toolbar-arrow {
width: 28px;
flex-grow: 0;
flex-shrink: 0;
opacity: 1;
// @include core-transition(opacity, 200ms);
&:active {
background-color: var(--white);
// @include darkmode() {
// background-color: $black;
// }
}
&.toolbar-arrow-hidden {
opacity: 0;
}
}
}
&.toolbar-hidden {
visibility: none;
height: 0;
border: none;
}
}
}
:host-context(.keyboard-is-open) {
min-height: 200px;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
// (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 { CORE_SITE_SCHEMAS } from '@services/sites';
import { CoreEditorComponentsModule } from './components/components.module';
import { SITE_SCHEMA } from './services/database/editor';
@NgModule({
declarations: [
],
imports: [
CoreEditorComponentsModule,
],
providers: [
{
provide: CORE_SITE_SCHEMAS,
useValue: [SITE_SCHEMA],
multi: true,
},
],
})
export class CoreEditorModule {}

View File

@ -0,0 +1,17 @@
{
"autosavesucceeded": "Draft saved.",
"bold": "Bold",
"clear": "Clear formatting",
"h3": "Heading (large)",
"h4": "Heading (medium)",
"h5": "Heading (small)",
"hidetoolbar": "Hide toolbar",
"italic": "Italic",
"orderedlist": "Ordered list",
"p": "Paragraph",
"strike": "Strike through",
"textrecovered": "A draft version of this text was automatically restored.",
"toggle": "Toggle editor",
"underline": "Underline",
"unorderedlist": "Unordered list"
}

View File

@ -0,0 +1,93 @@
// (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 { CoreSiteSchema } from '@services/sites';
/**
* Database variables for CoreEditorOffline service.
*/
export const DRAFT_TABLE = 'editor_draft';
export const SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreEditorProvider',
version: 1,
tables: [
{
name: DRAFT_TABLE,
columns: [
{
name: 'contextlevel',
type: 'TEXT',
},
{
name: 'contextinstanceid',
type: 'INTEGER',
},
{
name: 'elementid',
type: 'TEXT',
},
{
name: 'extraparams', // Moodle web uses a page hash built with URL. App will use some params stringified.
type: 'TEXT',
},
{
name: 'drafttext',
type: 'TEXT',
notNull: true,
},
{
name: 'pageinstance',
type: 'TEXT',
notNull: true,
},
{
name: 'timecreated',
type: 'INTEGER',
notNull: true,
},
{
name: 'timemodified',
type: 'INTEGER',
notNull: true,
},
{
name: 'originalcontent',
type: 'TEXT',
},
],
primaryKeys: ['contextlevel', 'contextinstanceid', 'elementid', 'extraparams'],
},
],
};
/**
* Primary data to identify a stored draft.
*/
export type CoreEditorDraftPrimaryData = {
contextlevel: string; // Context level.
contextinstanceid: number; // The instance ID related to the context.
elementid: string; // Element ID.
extraparams: string; // Extra params stringified.
};
/**
* Draft data stored.
*/
export type CoreEditorDraft = CoreEditorDraftPrimaryData & {
drafttext?: string; // Draft text stored.
pageinstance?: string; // Unique identifier to prevent storing data from several sources at the same time.
timecreated?: number; // Time created.
timemodified?: number; // Time modified.
originalcontent?: string; // Original content of the editor.
};

View File

@ -0,0 +1,239 @@
// (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 } from '@angular/core';
import { CoreError } from '@classes/errors/error';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreEditorDraft, CoreEditorDraftPrimaryData, DRAFT_TABLE } from './database/editor';
/**
* Service with features regarding rich text editor in offline.
*/
@Injectable({ providedIn: 'root' })
export class CoreEditorOfflineProvider {
protected logger: CoreLogger;
constructor() {
this.logger = CoreLogger.getInstance('CoreEditorOfflineProvider');
}
/**
* Delete a draft from DB.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async deleteDraft(
contextLevel: string,
contextInstanceId: number,
elementId: string,
extraParams: Record<string, unknown>,
siteId?: string,
): Promise<void> {
try {
const db = await CoreSites.instance.getSiteDb(siteId);
const params = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams);
await db.deleteRecords(DRAFT_TABLE, params);
} catch (error) {
// Ignore errors, probably no draft stored.
}
}
/**
* Return an object with the draft primary data converted to the right format.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @return Object with the fixed primary data.
*/
protected fixDraftPrimaryData(
contextLevel: string,
contextInstanceId: number,
elementId: string,
extraParams: Record<string, unknown>,
): CoreEditorDraftPrimaryData {
return {
contextlevel: contextLevel,
contextinstanceid: contextInstanceId,
elementid: elementId,
extraparams: CoreUtils.instance.sortAndStringify(extraParams || {}),
};
}
/**
* Get a draft from DB.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the draft data. Undefined if no draft stored.
*/
async getDraft(
contextLevel: string,
contextInstanceId: number,
elementId: string,
extraParams: Record<string, unknown>,
siteId?: string,
): Promise<CoreEditorDraft> {
const db = await CoreSites.instance.getSiteDb(siteId);
const params = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams);
return db.getRecord(DRAFT_TABLE, params);
}
/**
* Get draft to resume it.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param pageInstance Unique identifier to prevent storing data from several sources at the same time.
* @param originalContent Original content of the editor.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the draft data. Undefined if no draft stored.
*/
async resumeDraft(
contextLevel: string,
contextInstanceId: number,
elementId: string,
extraParams: Record<string, unknown>,
pageInstance: string,
originalContent?: string,
siteId?: string,
): Promise<CoreEditorDraft | undefined> {
try {
// Check if there is a draft stored.
const entry = await this.getDraft(contextLevel, contextInstanceId, elementId, extraParams, siteId);
// There is a draft stored. Update its page instance.
try {
const db = await CoreSites.instance.getSiteDb(siteId);
entry.pageinstance = pageInstance;
entry.timemodified = Date.now();
if (originalContent && entry.originalcontent != originalContent) {
entry.originalcontent = originalContent;
entry.drafttext = ''; // "Discard" the draft.
}
await db.insertRecord(DRAFT_TABLE, entry);
} catch (error) {
// Ignore errors saving the draft. It shouldn't happen.
}
return entry;
} catch (error) {
// No draft stored. Store an empty draft to save the pageinstance.
await this.saveDraft(
contextLevel,
contextInstanceId,
elementId,
extraParams,
pageInstance,
'',
originalContent,
siteId,
);
}
}
/**
* Save a draft in DB.
*
* @param contextLevel Context level.
* @param contextInstanceId The instance ID related to the context.
* @param elementId Element ID.
* @param extraParams Object with extra params to identify the draft.
* @param pageInstance Unique identifier to prevent storing data from several sources at the same time.
* @param draftText The text to store.
* @param originalContent Original content of the editor.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async saveDraft(
contextLevel: string,
contextInstanceId: number,
elementId: string,
extraParams: Record<string, unknown>,
pageInstance: string,
draftText: string,
originalContent?: string,
siteId?: string,
): Promise<void> {
let timecreated = Date.now();
let entry: CoreEditorDraft | undefined;
// Check if there is a draft already stored.
try {
entry = await this.getDraft(contextLevel, contextInstanceId, elementId, extraParams, siteId);
timecreated = entry.timecreated || timecreated;
} catch (error) {
// No draft already stored.
}
if (entry) {
if (entry.pageinstance != pageInstance) {
this.logger.warn(`Discarding draft because of pageinstance. Context '${contextLevel}' '${contextInstanceId}', ` +
`element '${elementId}'`);
throw new CoreError('Draft was discarded because it was modified in another page.');
}
if (!originalContent) {
// Original content not set, use the one in the entry.
originalContent = entry.originalcontent;
}
}
const db = await CoreSites.instance.getSiteDb(siteId);
const data: CoreEditorDraft = this.fixDraftPrimaryData(contextLevel, contextInstanceId, elementId, extraParams);
data.drafttext = (draftText || '').trim();
data.pageinstance = pageInstance;
data.timecreated = timecreated;
data.timemodified = Date.now();
if (originalContent) {
data.originalcontent = originalContent;
}
await db.insertRecord(DRAFT_TABLE, data);
}
}
export class CoreEditorOffline extends makeSingleton(CoreEditorOfflineProvider) {}

View File

@ -23,6 +23,7 @@ import { CoreMainMenuModule } from './mainmenu/mainmenu.module';
import { CoreSettingsModule } from './settings/settings.module';
import { CoreSiteHomeModule } from './sitehome/sitehome.module';
import { CoreTagModule } from './tag/tag.module';
import { CoreUserModule } from './user/user.module';
@NgModule({
imports: [
@ -35,6 +36,7 @@ import { CoreTagModule } from './tag/tag.module';
CoreSettingsModule,
CoreSiteHomeModule,
CoreTagModule,
CoreUserModule,
],
})
export class CoreFeaturesModule {}

View File

@ -18,6 +18,7 @@ import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { Translate } from '@singletons';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Page that shows instructions to change the password.
@ -62,7 +63,7 @@ export class CoreLoginChangePasswordPage {
* Login the user.
*/
login(): void {
CoreLoginHelper.instance.goToSiteInitialPage();
CoreNavHelper.instance.goToSiteInitialPage();
this.changingPassword = false;
}

View File

@ -26,6 +26,7 @@ import { CoreConstants } from '@/core/constants';
import { Translate } from '@singletons';
import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site';
import { CoreEvents } from '@singletons/events';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Page to enter the user credentials.
@ -244,7 +245,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
this.siteId = id;
await CoreLoginHelper.instance.goToSiteInitialPage({ urlToOpen: this.urlToOpen });
await CoreNavHelper.instance.goToSiteInitialPage({ urlToOpen: this.urlToOpen });
} catch (error) {
CoreLoginHelper.instance.treatUserTokenError(siteUrl, error, username, password);

View File

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

View File

@ -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,

View File

@ -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,
@ -156,7 +157,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 +190,11 @@ export class CoreLoginEmailSignupPage implements OnInit {
{ siteUrl: this.siteUrl },
);
// @todo userProfileFieldDelegate
if (CoreUserProfileFieldDelegate.instance.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 +279,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 +300,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 CoreUserProfileFieldDelegate.instance.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 +388,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 +416,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 +452,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.
};

View File

@ -20,6 +20,7 @@ import { ApplicationInit, SplashScreen } from '@singletons';
import { CoreConstants } from '@/core/constants';
import { CoreSites } from '@services/sites';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Page that displays a "splash screen" while the app is being initialized.
@ -79,7 +80,7 @@ export class CoreLoginInitPage implements OnInit {
return;
}
return CoreLoginHelper.instance.goToSiteInitialPage({
return CoreNavHelper.instance.goToSiteInitialPage({
redirectPage: redirectData.page,
redirectParams: redirectData.params,
});
@ -89,7 +90,7 @@ export class CoreLoginInitPage implements OnInit {
}
} else if (redirectData.page) {
// No site to load, open the page.
return CoreLoginHelper.instance.goToNoSitePage(redirectData.page, redirectData.params);
return CoreNavHelper.instance.goToNoSitePage(redirectData.page, redirectData.params);
}
}
@ -109,7 +110,7 @@ export class CoreLoginInitPage implements OnInit {
return this.loadPage();
}
return CoreLoginHelper.instance.goToSiteInitialPage();
return CoreNavHelper.instance.goToSiteInitialPage();
}
await this.navCtrl.navigateRoot('/login/sites');

View File

@ -25,6 +25,7 @@ import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@classes/site';
import { CoreEvents } from '@singletons/events';
import { CoreError } from '@classes/errors/error';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Page to enter the user password to reconnect to a site.
@ -206,7 +207,7 @@ export class CoreLoginReconnectPage implements OnInit, OnDestroy {
this.credForm.controls['password'].reset();
// Go to the site initial page.
await CoreLoginHelper.instance.goToSiteInitialPage({
await CoreNavHelper.instance.goToSiteInitialPage({
redirectPage: this.page,
redirectParams: this.pageParams,
});

View File

@ -22,6 +22,7 @@ import { CoreUtils } from '@services/utils/utils';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreSite } from '@classes/site';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Page to accept a site policy.
@ -128,7 +129,7 @@ export class CoreLoginSitePolicyPage implements OnInit {
// Invalidate cache since some WS don't return error if site policy is not accepted.
await CoreUtils.instance.ignoreErrors(this.currentSite!.invalidateWsCache());
await CoreLoginHelper.instance.goToSiteInitialPage();
await CoreNavHelper.instance.goToSiteInitialPage();
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'Error accepting site policy.');
} finally {

View File

@ -31,6 +31,7 @@ import { CoreUrl } from '@singletons/url';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreLoginSiteHelpComponent } from '@features/login/components/site-help/site-help';
import { CoreLoginSiteOnboardingComponent } from '@features/login/components/site-onboarding/site-onboarding';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Page that displays a "splash screen" while the app is being initialized.
@ -328,7 +329,7 @@ export class CoreLoginSitePage implements OnInit {
CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true);
return CoreLoginHelper.instance.goToSiteInitialPage();
return CoreNavHelper.instance.goToSiteInitialPage();
} catch (error) {
CoreLoginHelper.instance.treatUserTokenError(siteData.url, error, siteData.username, siteData.password);

View File

@ -19,6 +19,7 @@ import { Component, OnInit } from '@angular/core';
import { CoreSiteBasicInfo, CoreSites } from '@services/sites';
import { CoreLogger } from '@singletons/logger';
import { CoreLoginHelper } from '@features/login/services/login-helper';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Page that displays a "splash screen" while the app is being initialized.
@ -124,7 +125,7 @@ export class CoreLoginSitesPage implements OnInit {
const loggedIn = await CoreSites.instance.loadSite(siteId);
if (loggedIn) {
return CoreLoginHelper.instance.goToSiteInitialPage();
return CoreNavHelper.instance.goToSiteInitialPage();
}
} catch (error) {
this.logger.error('Error loading site ' + siteId, error);

View File

@ -13,7 +13,6 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { Location } from '@angular/common';
import { Params } from '@angular/router';
import { NavController } from '@ionic/angular';
import { Md5 } from 'ts-md5/dist/md5';
@ -34,8 +33,8 @@ import { CoreWSError } from '@classes/errors/wserror';
import { makeSingleton, Translate } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import { CoreUrl } from '@singletons/url';
import { NavigationOptions } from '@ionic/angular/providers/nav-controller';
import { CoreObject } from '@singletons/object';
import { CoreNavHelper, CoreNavHelperOpenMainMenuOptions, CoreNavHelperService } from '@services/nav-helper';
/**
* Helper provider that provides some common features regarding authentication.
@ -43,7 +42,7 @@ import { CoreObject } from '@singletons/object';
@Injectable({ providedIn: 'root' })
export class CoreLoginHelperProvider {
static readonly OPEN_COURSE = 'open_course';
static readonly OPEN_COURSE = CoreNavHelperService.OPEN_COURSE; // @deprecated since 3.9.5.
static readonly ONBOARDING_DONE = 'onboarding_done';
static readonly FAQ_URL_IMAGE_HTML = '<img src="assets/img/login/faq_url.png" role="presentation">';
static readonly FAQ_QRCODE_IMAGE_HTML = '<img src="assets/img/login/faq_qrcode.png" role="presentation">';
@ -51,24 +50,13 @@ export class CoreLoginHelperProvider {
protected logger: CoreLogger;
protected isSSOConfirmShown = false;
protected isOpenEditAlertShown = false;
protected pageToLoad?: {page: string; params?: Params; time: number}; // Page to load once main menu is opened.
protected isOpeningReconnect = false;
waitingForBrowser = false;
constructor(
protected location: Location,
protected navCtrl: NavController,
) {
this.logger = CoreLogger.getInstance('CoreLoginHelper');
CoreEvents.on(CoreEvents.MAIN_MENU_OPEN, () => {
/* If there is any page pending to be opened, do it now. Don't open pages stored more than 5 seconds ago, probably
the function to open the page was called when it shouldn't. */
if (this.pageToLoad && Date.now() - this.pageToLoad.time < 5000) {
this.loadPageInMainMenu(this.pageToLoad.page, this.pageToLoad.params);
delete this.pageToLoad;
}
});
}
/**
@ -123,7 +111,7 @@ export class CoreLoginHelperProvider {
*/
checkLogout(): void {
const currentSite = CoreSites.instance.getCurrentSite();
const currentPage = CoreApp.instance.getCurrentPage();
const currentPage = CoreNavHelper.instance.getCurrentPage();
if (!CoreApp.instance.isSSOAuthenticationOngoing() && currentSite?.isLoggedOut() && currentPage == '/login/reconnect') {
// User must reauthenticate but he closed the InAppBrowser without doing so, logout him.
@ -448,39 +436,10 @@ export class CoreLoginHelperProvider {
* @param page Page to open.
* @param params Params of the page.
* @return Promise resolved when done.
* @deprecated since 3.9.5. Use CoreNavHelperService.goToNoSitePage instead.
*/
async goToNoSitePage(page: string, params?: Params): Promise<void> {
const currentPage = CoreApp.instance.getCurrentPage();
if (currentPage == page) {
// Already at page, nothing to do.
} else if (page == '/login/sites') {
// Just open the page as root.
await this.navCtrl.navigateRoot(page, { queryParams: params });
} else if (page == '/login/credentials' && currentPage == '/login/site') {
// Just open the new page to keep the navigation history.
await this.navCtrl.navigateForward(page, { queryParams: params });
} else {
// Check if there is any site stored.
const hasSites = await CoreSites.instance.hasSites();
if (!hasSites) {
// There are sites stored, open sites page first to be able to go back.
await this.navCtrl.navigateRoot('/login/sites');
await this.navCtrl.navigateForward(page, { queryParams: params });
} else {
if (page != '/login/site') {
// Open the new site page to be able to go back.
await this.navCtrl.navigateRoot('/login/site');
await this.navCtrl.navigateForward(page, { queryParams: params });
} else {
// Just open the page as root.
await this.navCtrl.navigateRoot(page, { queryParams: params });
}
}
}
goToNoSitePage(page: string, params?: Params): Promise<void> {
return CoreNavHelper.instance.goToNoSitePage(page, params);
}
/**
@ -488,9 +447,10 @@ export class CoreLoginHelperProvider {
*
* @param options Options.
* @return Promise resolved when done.
* @deprecated since 3.9.5. Use CoreNavHelperService.goToSiteInitialPage instead.
*/
goToSiteInitialPage(options?: OpenMainMenuOptions): Promise<void> {
return this.openMainMenu(options);
goToSiteInitialPage(options?: CoreNavHelperOpenMainMenuOptions): Promise<void> {
return CoreNavHelper.instance.goToSiteInitialPage(options);
}
/**
@ -641,97 +601,15 @@ export class CoreLoginHelperProvider {
return code == CoreConstants.LOGIN_SSO_CODE || code == CoreConstants.LOGIN_SSO_INAPP_CODE;
}
/**
* Load a site and load a certain page in that site.
*
* @param siteId Site to load.
* @param page Name of the page to load.
* @param params Params to pass to the page.
* @return Promise resolved when done.
*/
protected async loadSiteAndPage(siteId: string, page: string, params?: Params): Promise<void> {
if (siteId == CoreConstants.NO_SITE_ID) {
// Page doesn't belong to a site, just load the page.
await this.navCtrl.navigateRoot(page, params);
return;
}
const modal = await CoreDomUtils.instance.showModalLoading();
try {
const loggedIn = await CoreSites.instance.loadSite(siteId, page, params);
if (!loggedIn) {
return;
}
await this.openMainMenu({
redirectPage: page,
redirectParams: params,
});
} catch (error) {
// Site doesn't exist.
await this.navCtrl.navigateRoot('/login/sites');
} finally {
modal.dismiss();
}
}
/**
* Load a certain page in the main menu page.
*
* @param page Name of the page to load.
* @param params Params to pass to the page.
* @deprecated since 3.9.5. Use CoreNavHelperService.loadPageInMainMenu instead.
*/
loadPageInMainMenu(page: string, params?: Params): void {
if (!CoreApp.instance.isMainMenuOpen()) {
// Main menu not open. Store the page to be loaded later.
this.pageToLoad = {
page: page,
params: params,
time: Date.now(),
};
return;
}
if (page == CoreLoginHelperProvider.OPEN_COURSE) {
// @todo Use the openCourse function.
} else {
CoreEvents.trigger(CoreEvents.LOAD_PAGE_MAIN_MENU, { redirectPage: page, redirectParams: params });
}
}
/**
* Open the main menu, loading a certain page.
*
* @param options Options.
* @return Promise resolved when done.
*/
protected async openMainMenu(options?: OpenMainMenuOptions): Promise<void> {
// Due to DeepLinker, we need to remove the path from the URL before going to main menu.
// IonTabs checks the URL to determine which path to load for deep linking, so we clear the URL.
// @todo this.location.replaceState('');
if (options?.redirectPage == CoreLoginHelperProvider.OPEN_COURSE) {
// Load the main menu first, and then open the course.
try {
await this.navCtrl.navigateRoot('/');
} finally {
// @todo: Open course.
}
} else {
// Open the main menu.
const queryParams: Params = Object.assign({}, options);
delete queryParams.navigationOptions;
await this.navCtrl.navigateRoot('/', {
queryParams,
...options?.navigationOptions,
});
}
CoreNavHelper.instance.loadPageInMainMenu(page, params);
}
/**
@ -889,7 +767,7 @@ export class CoreLoginHelperProvider {
return; // Site that triggered the event is not current site.
}
const currentPage = CoreApp.instance.getCurrentPage();
const currentPage = CoreNavHelper.instance.getCurrentPage();
// If current page is already change password, stop.
if (currentPage == '/login/changepassword') {
@ -948,31 +826,14 @@ export class CoreLoginHelperProvider {
/**
* Redirect to a new page, setting it as the root page and loading the right site if needed.
*
* @param page Name of the page to load. Special cases: OPEN_COURSE (to open course page).
* @param page Name of the page to load. Special cases: CoreNavHelperService.OPEN_COURSE (to open course page).
* @param params Params to pass to the page.
* @param siteId Site to load. If not defined, current site.
* @return Promise resolved when done.
* @deprecated since 3.9.5. Use CoreNavHelperService.openInSiteMainMenu instead.
*/
async redirect(page: string, params?: Params, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
if (CoreSites.instance.isLoggedIn()) {
if (siteId && siteId != CoreSites.instance.getCurrentSiteId()) {
// Target page belongs to a different site. Change site.
// @todo: Check site plugins.
await CoreSites.instance.logout();
await this.loadSiteAndPage(siteId, page, params);
} else {
this.loadPageInMainMenu(page, params);
}
} else {
if (siteId) {
await this.loadSiteAndPage(siteId, page, params);
} else {
await this.navCtrl.navigateRoot('/login/sites');
}
}
redirect(page: string, params?: Params, siteId?: string): Promise<void> {
return CoreNavHelper.instance.openInSiteMainMenu(page, params, siteId);
}
/**
@ -1098,7 +959,7 @@ export class CoreLoginHelperProvider {
const info = currentSite.getInfo();
if (typeof info != 'undefined' && typeof info.username != 'undefined' && !this.isOpeningReconnect) {
// If current page is already reconnect, stop.
if (CoreApp.instance.getCurrentPage() == '/login/reconnect') {
if (CoreNavHelper.instance.getCurrentPage() == '/login/reconnect') {
return;
}
@ -1266,7 +1127,7 @@ export class CoreLoginHelperProvider {
}
// If current page is already site policy, stop.
if (CoreApp.instance.getCurrentPage() == '/login/sitepolicy') {
if (CoreNavHelper.instance.getCurrentPage() == '/login/sitepolicy') {
return;
}
@ -1474,10 +1335,3 @@ type StoredLoginLaunchData = {
pageParams: Params;
ssoUrlParams: CoreUrlParams;
};
type OpenMainMenuOptions = {
redirectPage?: string; // Route of the page to open in main menu. If not defined, default tab will be selected.
redirectParams?: Params; // Params to pass to the selected tab if any.
urlToOpen?: string; // URL to open once the main menu is loaded.
navigationOptions?: NavigationOptions; // Navigation options.
};

View File

@ -0,0 +1,47 @@
// (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 { InjectionToken, Injector, ModuleWithProviders, NgModule } from '@angular/core';
import { Route, Routes } from '@angular/router';
import { ModuleRoutes, resolveModuleRoutes } from '@/app/app-routing.module';
export const MAIN_MENU_TAB_ROUTES = new InjectionToken('MAIN_MENU_TAB_ROUTES');
export function buildTabMainRoutes(injector: Injector, mainRoute: Route): Routes {
const routes = resolveModuleRoutes(injector, MAIN_MENU_TAB_ROUTES);
mainRoute.path = mainRoute.path || '';
mainRoute.children = mainRoute.children || [];
mainRoute.children.concat(routes.children);
return [
mainRoute,
...routes.siblings,
];
}
@NgModule()
export class CoreMainMenuTabRoutingModule {
static forChild(routes: Partial<ModuleRoutes>): ModuleWithProviders<CoreMainMenuTabRoutingModule> {
return {
ngModule: CoreMainMenuTabRoutingModule,
providers: [
{ provide: MAIN_MENU_TAB_ROUTES, multi: true, useValue: routes },
],
};
}
}

View File

@ -24,16 +24,17 @@ import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreMainMenuHomePage } from './home';
import { MAIN_MENU_HOME_ROUTES } from './home-routing.module';
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
function buildRoutes(injector: Injector): Routes {
const routes = resolveModuleRoutes(injector, MAIN_MENU_HOME_ROUTES);
return [
{
...buildTabMainRoutes(injector, {
path: '',
component: CoreMainMenuHomePage,
children: routes.children,
},
}),
...routes.siblings,
];
}

View File

@ -25,6 +25,7 @@ import { CoreMainMenu } from '../../services/mainmenu';
import { CoreMainMenuDelegate, CoreMainMenuHandlerToDisplay } from '../../services/mainmenu-delegate';
import { CoreDomUtils } from '@services/utils/dom';
import { Translate } from '@singletons';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Page that displays the main menu of the app.
@ -60,7 +61,7 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
protected changeDetector: ChangeDetectorRef,
protected router: Router,
) {
this.mainMenuId = CoreApp.instance.getMainMenuId();
this.mainMenuId = CoreNavHelper.instance.getMainMenuId();
}
/**
@ -131,7 +132,7 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
});
}
CoreApp.instance.setMainMenuOpen(this.mainMenuId, true);
CoreNavHelper.instance.setMainMenuOpen(this.mainMenuId, true);
}
/**
@ -226,7 +227,7 @@ export class CoreMainMenuPage implements OnInit, OnDestroy {
this.subscription?.unsubscribe();
this.redirectObs?.off();
window.removeEventListener('resize', this.initHandlers.bind(this));
CoreApp.instance.setMainMenuOpen(this.mainMenuId, false);
CoreNavHelper.instance.setMainMenuOpen(this.mainMenuId, false);
this.keyboardObserver?.off();
}

View File

@ -9,8 +9,8 @@
</ion-header>
<ion-content>
<ion-list>
<ion-item *ngIf="siteInfo" class="ion-text-wrap"> <!-- @todo core-user-link [userId]="siteInfo.userid" -->
<ion-avatar slot="start"></ion-avatar> <!-- @todo core-user-avatar [user]="siteInfo" -->
<ion-item button *ngIf="siteInfo" class="ion-text-wrap" core-user-link [userId]="siteInfo.userid">
<core-user-avatar [user]="siteInfo" slot="start"></core-user-avatar>
<ion-label>
<h2>{{siteInfo.fullname}}</h2>
<p>

View File

@ -14,29 +14,14 @@
import { Injector, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, ROUTES, Routes } from '@angular/router';
import { RouterModule, ROUTES } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { resolveModuleRoutes } from '@/app/app-routing.module';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreMainMenuMorePage } from './more';
import { MAIN_MENU_MORE_ROUTES } from './more-routing.module';
function buildRoutes(injector: Injector): Routes {
const routes = resolveModuleRoutes(injector, MAIN_MENU_MORE_ROUTES);
return [
{
path: '',
component: CoreMainMenuMorePage,
children: routes.children,
},
...routes.siblings,
];
}
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
@NgModule({
imports: [
@ -47,7 +32,14 @@ function buildRoutes(injector: Injector): Routes {
CoreDirectivesModule,
],
providers: [
{ provide: ROUTES, multi: true, useFactory: buildRoutes, deps: [Injector] },
{
provide: ROUTES,
multi: true,
deps: [Injector],
useFactory: (injector: Injector) => buildTabMainRoutes(injector, {
component: CoreMainMenuMorePage,
}),
},
],
declarations: [
CoreMainMenuMorePage,

View File

@ -16,7 +16,7 @@ import { APP_INITIALIZER, NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { AppRoutingModule } from '@/app/app-routing.module';
import { CoreMainMenuMoreRoutingModule } from '@features/mainmenu/pages/more/more-routing.module';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CoreSettingsHelperProvider } from './services/settings-helper';
@ -41,7 +41,7 @@ const mainMenuMoreRoutes: Routes = [
@NgModule({
imports: [
AppRoutingModule.forChild(appRoutes),
CoreMainMenuMoreRoutingModule.forChild({ siblings: mainMenuMoreRoutes }),
CoreMainMenuTabRoutingModule.forChild({ siblings: mainMenuMoreRoutes }),
],
providers: [
{

View File

@ -16,10 +16,10 @@ import { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreSites } from '@services/sites';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreSiteHome } from '../sitehome';
import { makeSingleton } from '@singletons';
import { CoreNavHelper } from '@services/nav-helper';
/**
* Handler to treat links to site home index.
@ -43,7 +43,7 @@ export class CoreSiteHomeIndexLinkHandlerService extends CoreContentLinksHandler
getActions(): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId: string): void => {
CoreContentLinksHelper.instance.goInSite('sitehome', [], siteId);
CoreNavHelper.instance.goInSite('sitehome', [], siteId);
},
}];
}

View File

@ -13,9 +13,9 @@
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="loaded">
<!-- @todo <ng-container *ngIf="loaded">
<ng-container *ngIf="loaded">
<core-dynamic-component [component]="areaComponent" [data]="{items: items}"></core-dynamic-component>
</ng-container>-->
</ng-container>
<core-infinite-loading [enabled]="canLoadMore" (action)="loadMore($event)" [error]="loadMoreError">
</core-infinite-loading>
</core-loading>

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, Type } from '@angular/core';
import { IonInfiniteScroll, IonRefresher } from '@ionic/angular';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTag } from '@features/tag/services/tag';
@ -20,6 +20,7 @@ import { CoreTagFeedElement } from '../../services/tag-helper';
import { ActivatedRoute } from '@angular/router';
import { CoreTagAreaDelegate } from '../../services/tag-area-delegate';
import { Translate } from '@singletons';
import { CoreUtils } from '@services/utils/utils';
/**
* Page that displays the tag index area.
@ -47,7 +48,7 @@ export class CoreTagIndexAreaPage implements OnInit {
items: CoreTagFeedElement[] = [];
nextPage = 0;
canLoadMore = false;
areaComponent: any; // @todo
areaComponent?: Type<unknown>;
loadMoreError = false;
constructor(
@ -59,7 +60,7 @@ export class CoreTagIndexAreaPage implements OnInit {
*/
async ngOnInit(): Promise<void> {
const navParams = this.route.snapshot.queryParamMap;
const navParams = this.route.snapshot.queryParams;
this.tagId = navParams['tagId'] ? parseInt(navParams['tagId'], 10) : this.tagId;
this.tagName = navParams['tagName'] || this.tagName;
@ -74,8 +75,8 @@ export class CoreTagIndexAreaPage implements OnInit {
this.componentName = navParams['componentName'];
this.itemType = navParams['itemType'];
this.items = []; // @todo navParams['items'] || [];
this.nextPage = navParams.has('nextPage') ? parseInt(navParams['nextPage']!, 10) : 0;
this.canLoadMore = !!navParams['canLoadMore'];
this.nextPage = typeof navParams['nextPage'] != 'undefined' ? parseInt(navParams['nextPage'], 10) : 0;
this.canLoadMore = CoreUtils.instance.isTrueOrOne(navParams['canLoadMore']);
try {
if (!this.componentName || !this.itemType || !this.items.length || this.nextPage == 0) {

View File

@ -168,8 +168,11 @@ export class CoreTagIndexPage implements OnInit {
canLoadMore: area.canLoadMore,
nextPage: 1,
};
// this.splitviewCtrl.push('core-tag-index-area', params);
this.router.navigate(['core-tag-index-area'], { queryParams: params });
// this.splitviewCtrl.push('index-area', params);
this.router.navigate(['../index-area'], {
queryParams: params,
relativeTo: this.route,
});
}

View File

@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { CoreNavHelper } from '@services/nav-helper';
import { makeSingleton } from '@singletons';
import { CoreTag } from '../tag';
@ -57,11 +57,11 @@ export class CoreTagIndexLinkHandlerService extends CoreContentLinksHandlerBase
};
if (!pageParams.tagId && (!pageParams.tagName || !pageParams.collectionId)) {
CoreContentLinksHelper.instance.goInSite('/main/tag/search', {}, siteId);
CoreNavHelper.instance.goInSite('/tag/search', {}, siteId);
} else if (pageParams.areaId) {
CoreContentLinksHelper.instance.goInSite('/main/tag/index-area', pageParams, siteId);
CoreNavHelper.instance.goInSite('/tag/index-area', pageParams, siteId);
} else {
CoreContentLinksHelper.instance.goInSite('/main/tag/index', pageParams, siteId);
CoreNavHelper.instance.goInSite('/tag/index', pageParams, siteId);
}
},
}];

View File

@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
import { Params } from '@angular/router';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreContentLinksHelper } from '@features/contentlinks/services/contentlinks-helper';
import { CoreNavHelper } from '@services/nav-helper';
import { makeSingleton } from '@singletons';
import { CoreTag } from '../tag';
@ -47,7 +47,7 @@ export class CoreTagSearchLinkHandlerService extends CoreContentLinksHandlerBase
query: params.query || '',
};
CoreContentLinksHelper.instance.goInSite('/main/tag/search', pageParams, siteId);
CoreNavHelper.instance.goInSite('/tag/search', pageParams, siteId);
},
}];
}

View File

@ -52,7 +52,7 @@ export class CoreTagAreaDelegateService extends CoreDelegate<CoreTagAreaHandler>
protected handlerNameProperty = 'type';
constructor() {
super('CoreTagAreaDelegate');
super('CoreTagAreaDelegate', true);
}
/**

View File

@ -12,31 +12,42 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Injector, NgModule } from '@angular/core';
import { RouterModule, ROUTES, Routes } from '@angular/router';
const routes: Routes = [
{
path: 'index',
loadChildren: () => import('@features/tag/pages/index/index.page.module').then(m => m.CoreTagIndexPageModule),
},
{
path: 'search',
loadChildren: () => import('@features/tag//pages/search/search.page.module').then(m => m.CoreTagSearchPageModule),
},
{
path: 'index-area',
loadChildren: () => import('@features/tag/pages/index-area/index-area.page.module').then(m => m.CoreTagIndexAreaPageModule),
},
{
path: '',
redirectTo: 'search',
pathMatch: 'full',
},
];
import { buildTabMainRoutes } from '@features/mainmenu/mainmenu-tab-routing.module';
function buildRoutes(injector: Injector): Routes {
return [
{
path: 'index',
loadChildren: () => import('@features/tag/pages/index/index.page.module').then(m => m.CoreTagIndexPageModule),
},
{
path: 'search',
loadChildren: () => import('@features/tag//pages/search/search.page.module').then(m => m.CoreTagSearchPageModule),
},
{
path: 'index-area',
loadChildren: () =>
import('@features/tag/pages/index-area/index-area.page.module').then(m => m.CoreTagIndexAreaPageModule),
},
...buildTabMainRoutes(injector, {
redirectTo: 'search',
pathMatch: 'full',
}),
];
}
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
providers: [
{
provide: ROUTES,
multi: true,
deps: [Injector],
useFactory: buildRoutes,
},
],
})
export class CoreTagLazyModule { }

View File

@ -0,0 +1,100 @@
// (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 } from '@angular/core';
import { FormGroup, Validators, FormControl } from '@angular/forms';
import { AuthEmailSignupProfileField } from '@features/login/services/login-helper';
import { CoreUserProfileField } from '@features/user/services/user';
/**
* Base class for components to render a user profile field.
*/
@Component({
template: '',
})
export abstract class CoreUserProfileFieldBaseComponent implements OnInit {
@Input() field?: AuthEmailSignupProfileField | CoreUserProfileField; // The profile field to be rendered.
@Input() edit = false; // True if editing the field. Defaults to false.
@Input() disabled = false; // True if disabled. Defaults to false.
@Input() form?: FormGroup; // Form where to add the form control.
@Input() contextLevel?: string; // The context level.
@Input() contextInstanceId?: number; // The instance ID related to the context.
@Input() courseId?: number; // The course the field belongs to (if any).
control?: FormControl;
modelName = '';
value?: string;
required?: boolean;
/**
* Component being initialized.
*/
ngOnInit(): void {
if (!this.field) {
return;
}
if (!this.edit && 'value' in this.field) {
this.initForNonEdit(this.field);
return;
}
if (this.edit && 'required' in this.field) {
this.initForEdit(this.field);
return;
}
}
/**
* Init the data when the field is meant to be displayed without editing.
*
* @param field Field to render.
*/
protected initForNonEdit(field: CoreUserProfileField): void {
this.value = field.value;
}
/**
* Init the data when the field is meant to be displayed for editing.
*
* @param field Field to render.
*/
protected initForEdit(field: AuthEmailSignupProfileField): void {
this.modelName = 'profile_field_' + field.shortname;
this.required = !!field.required;
this.control = this.createFormControl(field);
this.form?.addControl(this.modelName, this.control);
}
/**
* Create the Form control.
*
* @return Form control.
*/
protected createFormControl(field: AuthEmailSignupProfileField): FormControl {
const formData = {
value: field.defaultdata,
disabled: this.disabled,
};
return new FormControl(formData, this.required && !field.locked ? Validators.required : null);
}
}

View File

@ -0,0 +1,46 @@
// (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';
import { CoreUserTagAreaComponent } from './tag-area/tag-area';
@NgModule({
declarations: [
CoreUserProfileFieldComponent,
CoreUserTagAreaComponent,
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
],
providers: [
],
exports: [
CoreUserProfileFieldComponent,
CoreUserTagAreaComponent,
],
})
export class CoreUserComponentsModule {}

View File

@ -0,0 +1,6 @@
<ion-item class="ion-text-wrap" *ngFor="let item of items" core-user-link [userId]="item.user.id">
<core-user-avatar [user]="item.user" slot="start"></core-user-avatar>
<ion-label>
<h2>{{ item.heading }}</h2>
</ion-label>
</ion-item>

View File

@ -12,22 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { InjectionToken, ModuleWithProviders, NgModule } from '@angular/core';
import { Component, Input } from '@angular/core';
import { ModuleRoutes } from '@/app/app-routing.module';
import { CoreUserTagFeedElement } from '@features/user/services/handlers/tag-area-handler';
export const MAIN_MENU_MORE_ROUTES = new InjectionToken('MAIN_MENU_MORE_ROUTES');
/**
* Component to render the user tag area.
*/
@Component({
selector: 'core-user-tag-area',
templateUrl: 'core-user-tag-area.html',
})
export class CoreUserTagAreaComponent {
@NgModule()
export class CoreMainMenuMoreRoutingModule {
static forChild(routes: Partial<ModuleRoutes>): ModuleWithProviders<CoreMainMenuMoreRoutingModule> {
return {
ngModule: CoreMainMenuMoreRoutingModule,
providers: [
{ provide: MAIN_MENU_MORE_ROUTES, multi: true, useValue: routes },
],
};
}
@Input() items?: CoreUserTagFeedElement[]; // Area items to render.
}

View File

@ -0,0 +1 @@
<core-dynamic-component [component]="componentClass" [data]="data"></core-dynamic-component>

View File

@ -0,0 +1,78 @@
// (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, 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.
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
if (!this.field) {
return;
}
this.componentClass = await CoreUserProfileFieldDelegate.instance.getComponent(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;
};

View File

@ -0,0 +1,27 @@
{
"address": "Address",
"city": "City/town",
"contact": "Contact",
"country": "Country",
"description": "Description",
"details": "Details",
"detailsnotavailable": "The details of this user are not available to you.",
"editingteacher": "Teacher",
"email": "Email address",
"emailagain": "Email (again)",
"errorloaduser": "Error loading user.",
"firstname": "First name",
"interests": "Interests",
"lastname": "Surname",
"manager": "Manager",
"newpicture": "New picture",
"noparticipants": "No participants found for this course",
"participants": "Participants",
"phone1": "Phone",
"phone2": "Mobile phone",
"roles": "Roles",
"sendemail": "Email",
"student": "Student",
"teacher": "Non-editing teacher",
"webpage": "Web page"
}

View File

@ -0,0 +1,97 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title *ngIf="title">{{ title }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!userLoaded" (ionRefresh)="refreshUser($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="userLoaded">
<ion-list *ngIf="user">
<ion-item-group *ngIf="hasContact">
<ion-item-divider>{{ 'core.user.contact' | translate}}</ion-item-divider>
<ion-item class="ion-text-wrap" *ngIf="user.email">
<ion-label>
<h2>{{ 'core.user.email' | translate }}</h2>
<p><a class="core-anchor" href="mailto:{{user.email}}" core-link auto-login="no">
{{ user.email }}
</a></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="user.phone1">
<ion-label>
<h2>{{ 'core.user.phone1' | translate}}</h2>
<p><a class="core-anchor" href="tel:{{user.phone1}}" core-link auto-login="no">
{{ user.phone1 }}
</a></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="user.phone2">
<ion-label>
<h2>{{ 'core.user.phone2' | translate}}</h2>
<p><a class="core-anchor" href="tel:{{user.phone2}}" core-link auto-login="no">
{{ user.phone2 }}
</a></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="user.address">
<ion-label>
<h2>{{ 'core.user.address' | translate}}</h2>
<p><a class="core-anchor" [href]="encodedAddress" core-link auto-login="no">
{{ user.address }}
</a></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="user.city && !user.address">
<ion-label>
<h2>{{ 'core.user.city' | translate}}</h2>
<p>{{ user.city }}</p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="user.country && !user.address">
<ion-label>
<h2>{{ 'core.user.country' | translate}}</h2>
<p>{{ user.country }}</p>
</ion-label>
</ion-item>
</ion-item-group>
<ion-item-group *ngIf="hasDetails">
<ion-item-divider>{{ 'core.userdetails' | translate}}</ion-item-divider>
<ion-item class="ion-text-wrap" *ngIf="user.url">
<ion-label>
<h2>{{ 'core.user.webpage' | translate}}</h2>
<p><a class="core-anchor" href="{{user.url}}" core-link>
{{ user.url }}
</a></p>
</ion-label>
</ion-item>
<ion-item class="ion-text-wrap" *ngIf="user.interests">
<ion-label>
<h2>{{ 'core.user.interests' | translate}}</h2>
<p>{{ user.interests }}</p>
</ion-label>
</ion-item>
<core-user-profile-field *ngFor="let field of user.customfields" [field]="field" contextLevel="course"
[contextInstanceId]="courseId" [courseId]="courseId">
</core-user-profile-field>
</ion-item-group>
<ion-item-group *ngIf="user.description">
<ion-item-divider>{{ 'core.user.description' | translate}}</ion-item-divider>
<ion-item class="ion-text-wrap">
<ion-label>
<p><core-format-text [text]="user.description" contextLevel="user" [contextInstanceId]="user.id">
</core-format-text></p>
</ion-label>
</ion-item>
</ion-item-group>
</ion-list>
<core-empty-box *ngIf="!user || (!hasContact && !hasDetails && !user.description)" icon="fa-user"
[message]=" 'core.user.detailsnotavailable' | translate">
</core-empty-box>
</core-loading>
</ion-content>

View File

@ -0,0 +1,49 @@
// (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 { RouterModule, Routes } from '@angular/router';
import { IonicModule } from '@ionic/angular';
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';
const routes: Routes = [
{
path: '',
component: CoreUserAboutPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CoreUserComponentsModule,
],
declarations: [
CoreUserAboutPage,
],
exports: [RouterModule],
})
export class CoreUserAboutPageModule {}

View File

@ -0,0 +1,115 @@
// (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, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { SafeUrl } from '@angular/platform-browser';
import { IonRefresher } from '@ionic/angular';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { CoreEvents } from '@singletons/events';
import { CoreUser, CoreUserProfile, CoreUserProfileRefreshedData, CoreUserProvider } from '@features/user/services/user';
import { CoreUserHelper } from '@features/user/services/user-helper';
/**
* Page that displays info about a user.
*/
@Component({
selector: 'page-core-user-about',
templateUrl: 'about.html',
})
export class CoreUserAboutPage implements OnInit {
protected userId!: number;
protected siteId: string;
courseId!: number;
userLoaded = false;
hasContact = false;
hasDetails = false;
user?: CoreUserProfile;
title?: string;
formattedAddress?: string;
encodedAddress?: SafeUrl;
constructor(
protected route: ActivatedRoute,
) {
this.siteId = CoreSites.instance.getCurrentSiteId();
}
/**
* On init.
*
* @return Promise resolved when done.
*/
async ngOnInit(): Promise<void> {
this.userId = this.route.snapshot.queryParams['userId'];
this.courseId = this.route.snapshot.queryParams['courseId'];
this.fetchUser().finally(() => {
this.userLoaded = true;
});
}
/**
* Fetches the user data.
*
* @return Promise resolved when done.
*/
async fetchUser(): Promise<void> {
try {
const user = await CoreUser.instance.getProfile(this.userId, this.courseId);
if (user.address) {
this.formattedAddress = CoreUserHelper.instance.formatAddress(user.address, user.city, user.country);
this.encodedAddress = CoreTextUtils.instance.buildAddressURL(user.address);
}
this.hasContact = !!(user.email || user.phone1 || user.phone2 || user.city || user.country || user.address);
this.hasDetails = !!(user.url || user.interests || (user.customfields && user.customfields.length > 0));
this.user = user;
this.title = user.fullname;
} catch (error) {
CoreDomUtils.instance.showErrorModalDefault(error, 'core.user.errorloaduser', true);
}
}
/**
* Refresh the user data.
*
* @param event Event.
* @return Promise resolved when done.
*/
async refreshUser(event?: CustomEvent<IonRefresher>): Promise<void> {
await CoreUtils.instance.ignoreErrors(CoreUser.instance.invalidateUserCache(this.userId));
await this.fetchUser();
event?.detail.complete();
if (this.user) {
CoreEvents.trigger<CoreUserProfileRefreshedData>(CoreUserProvider.PROFILE_REFRESHED, {
courseId: this.courseId,
userId: this.userId,
user: this.user,
}, this.siteId);
}
}
}

View File

@ -0,0 +1,90 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title *ngIf="title">{{ title }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!userLoaded" (ionRefresh)="refreshUser($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<core-loading [hideUntil]="userLoaded">
<ion-list *ngIf="user && !isDeleted && isEnrolled">
<ion-item class="ion-text-center core-user-profile-maininfo">
<core-user-avatar [user]="user" [userId]="user.id" [linkProfile]="false" [checkOnline]="true">
<div class="core-icon-foreground">
<ion-icon *ngIf="canChangeProfilePicture" name="fa-pen" (click)="changeProfilePicture()">
</ion-icon>
</div>
</core-user-avatar>
<ion-label>
<h2>{{ user.fullname }}</h2>
<p *ngIf="user.address">{{ user.address }}</p>
<p *ngIf="rolesFormatted" class="ion-text-wrap">
<strong>{{ 'core.user.roles' | translate}}</strong>{{'core.labelsep' | translate}}
{{ rolesFormatted }}
</p>
</ion-label>
</ion-item>
<ion-grid class="core-user-communication-handlers"
*ngIf="(communicationHandlers && communicationHandlers.length) || isLoadingHandlers">
<ion-row class="ion-no-padding justify-content-between"
*ngIf="communicationHandlers && communicationHandlers.length">
<ion-col *ngFor="let handler of communicationHandlers" class="ion-align-self-center ion-text-center">
<a (click)="handlerClicked($event, handler)" [ngClass]="['core-user-profile-handler', handler.class]"
title="{{handler.title | translate}}">
<ion-icon [name]="handler.icon" slot="start"></ion-icon>
<p>{{handler.title | translate}}</p>
</a>
</ion-col>
</ion-row>
<ion-row class="ion-no-padding">
<ion-col class="ion-text-center core-loading-handlers" *ngIf="isLoadingHandlers">
<ion-spinner></ion-spinner>
</ion-col>
</ion-row>
</ion-grid>
<ion-item button class="ion-text-wrap core-user-profile-handler" (click)="openUserDetails()"
title="{{ 'core.user.details' | translate }}">
<ion-icon name="fa-user" slot="start"></ion-icon>
<ion-label>
<h2>{{ 'core.user.details' | translate }}</h2>
</ion-label>
</ion-item>
<ion-item class="ion-text-center core-loading-handlers" *ngIf="isLoadingHandlers">
<ion-spinner></ion-spinner>
</ion-item>
<ion-item button *ngFor="let handler of newPageHandlers" class="ion-text-wrap" (click)="handlerClicked($event, handler)"
[ngClass]="['core-user-profile-handler', handler.class]" [hidden]="handler.hidden"
title="{{ handler.title | translate }}">
<ion-label>
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start"></ion-icon>
<h2>{{ handler.title | translate }}</h2>
</ion-label>
</ion-item>
<ion-item *ngIf="actionHandlers && actionHandlers.length">
<ion-button *ngFor="let handler of actionHandlers" expand="block" fill="outline"
[ngClass]="['core-user-profile-handler', handler.class]" (click)="handlerClicked($event, handler)"
[hidden]="handler.hidden" title="{{ handler.title | translate }}" [disabled]="handler.spinner">
<ion-label>
<ion-icon *ngIf="handler.icon" [name]="handler.icon" slot="start"></ion-icon>
<span>{{ handler.title | translate }}</span>
</ion-label>
<ion-spinner *ngIf="handler.spinner"></ion-spinner>
</ion-button>
</ion-item>
</ion-list>
<core-empty-box *ngIf="!user && !isDeleted && isEnrolled" icon="fa-user"
[message]=" 'core.user.detailsnotavailable' | translate">
</core-empty-box>
<core-empty-box *ngIf="isDeleted" icon="fa-user" [message]="'core.userdeleted' | translate"></core-empty-box>
<core-empty-box *ngIf="!isEnrolled" icon="fa-user" [message]="'core.notenrolledprofile' | translate"></core-empty-box>
</core-loading>
</ion-content>

View File

@ -0,0 +1,47 @@
// (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 { RouterModule, Routes } from '@angular/router';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreUserProfilePage } from './profile.page';
const routes: Routes = [
{
path: '',
component: CoreUserProfilePage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
],
declarations: [
CoreUserProfilePage,
],
exports: [RouterModule],
})
export class CoreUserProfilePageModule {}

View File

@ -0,0 +1,288 @@
// (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, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { IonRefresher, NavController } from '@ionic/angular';
import { Subscription } from 'rxjs';
import { CoreSite } from '@classes/site';
import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { Translate } from '@singletons';
import { CoreEventObserver, CoreEvents } from '@singletons/events';
import {
CoreUser,
CoreUserProfile,
CoreUserProfilePictureUpdatedData,
CoreUserProfileRefreshedData,
CoreUserProvider,
} from '@features/user/services/user';
import { CoreUserHelper } from '@features/user/services/user-helper';
import { CoreUserDelegate, CoreUserDelegateService, CoreUserProfileHandlerData } from '@features/user/services/user-delegate';
import { CoreFileUploaderHelper } from '@features/fileuploader/services/fileuploader-helper';
import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreUtils } from '@services/utils/utils';
@Component({
selector: 'page-core-user-profile',
templateUrl: 'profile.html',
styleUrls: ['profile.scss'],
})
export class CoreUserProfilePage implements OnInit, OnDestroy {
protected courseId!: number;
protected userId!: number;
protected site?: CoreSite;
protected obsProfileRefreshed: CoreEventObserver;
protected subscription?: Subscription;
userLoaded = false;
isLoadingHandlers = false;
user?: CoreUserProfile;
title?: string;
isDeleted = false;
isEnrolled = true;
canChangeProfilePicture = false;
rolesFormatted?: string;
actionHandlers: CoreUserProfileHandlerData[] = [];
newPageHandlers: CoreUserProfileHandlerData[] = [];
communicationHandlers: CoreUserProfileHandlerData[] = [];
constructor(
protected route: ActivatedRoute,
protected navCtrl: NavController,
) {
this.obsProfileRefreshed = CoreEvents.on<CoreUserProfileRefreshedData>(CoreUserProvider.PROFILE_REFRESHED, (data) => {
if (!this.user || !data.user) {
return;
}
this.user.email = data.user.email;
this.user.address = CoreUserHelper.instance.formatAddress('', data.user.city, data.user.country);
}, CoreSites.instance.getCurrentSiteId());
}
/**
* On init.
*/
async ngOnInit(): Promise<void> {
this.site = CoreSites.instance.getCurrentSite();
this.userId = this.route.snapshot.queryParams['userId'];
this.courseId = this.route.snapshot.queryParams['courseId'];
if (!this.site) {
return;
}
// Allow to change the profile image only in the app profile page.
this.canChangeProfilePicture =
(!this.courseId || this.courseId == this.site.getSiteHomeId()) &&
this.userId == this.site.getUserId() &&
this.site.canUploadFiles() &&
CoreUser.instance.canUpdatePictureInSite(this.site) &&
!CoreUser.instance.isUpdatePictureDisabledInSite(this.site);
try {
await this.fetchUser();
try {
await CoreUser.instance.logView(this.userId, this.courseId, this.user!.fullname);
} catch (error) {
this.isDeleted = error?.errorcode === 'userdeleted';
this.isEnrolled = error?.errorcode !== 'notenrolledprofile';
}
} finally {
this.userLoaded = true;
}
}
/**
* Fetches the user and updates the view.
*/
async fetchUser(): Promise<void> {
try {
const user = await CoreUser.instance.getProfile(this.userId, this.courseId);
user.address = CoreUserHelper.instance.formatAddress('', user.city, user.country);
this.rolesFormatted = 'roles' in user ? CoreUserHelper.instance.formatRoleList(user.roles) : '';
this.user = user;
this.title = user.fullname;
// If there's already a subscription, unsubscribe because we'll get a new one.
this.subscription?.unsubscribe();
this.subscription = CoreUserDelegate.instance.getProfileHandlersFor(user, this.courseId).subscribe((handlers) => {
this.actionHandlers = [];
this.newPageHandlers = [];
this.communicationHandlers = [];
handlers.forEach((handler) => {
switch (handler.type) {
case CoreUserDelegateService.TYPE_COMMUNICATION:
this.communicationHandlers.push(handler.data);
break;
case CoreUserDelegateService.TYPE_ACTION:
this.actionHandlers.push(handler.data);
break;
case CoreUserDelegateService.TYPE_NEW_PAGE:
default:
this.newPageHandlers.push(handler.data);
break;
}
});
this.isLoadingHandlers = !CoreUserDelegate.instance.areHandlersLoaded(user.id);
});
await this.checkUserImageUpdated();
} catch (error) {
// Error is null for deleted users, do not show the modal.
CoreDomUtils.instance.showErrorModal(error);
}
}
/**
* Check if current user image has changed.
*
* @return Promise resolved when done.
*/
protected async checkUserImageUpdated(): Promise<void> {
if (!this.site || !this.site.getInfo() || !this.user) {
return;
}
if (this.userId != this.site.getUserId() || this.user.profileimageurl == this.site.getInfo()!.userpictureurl) {
// Not current user or hasn't changed.
return;
}
// The current user image received is different than the one stored in site info. Assume the image was updated.
// Update the site info to get the right avatar in there.
try {
await CoreSites.instance.updateSiteInfo(this.site.getId());
} catch {
// Cannot update site info. Assume the profile image is the right one.
CoreEvents.trigger<CoreUserProfilePictureUpdatedData>(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
userId: this.userId,
picture: this.user.profileimageurl,
}, this.site.getId());
}
if (this.user.profileimageurl != this.site.getInfo()!.userpictureurl) {
// The image is still different, this means that the good one is the one in site info.
await this.refreshUser();
} else {
// Now they're the same, send event to use the right avatar in the rest of the app.
CoreEvents.trigger<CoreUserProfilePictureUpdatedData>(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
userId: this.userId,
picture: this.user.profileimageurl,
}, this.site.getId());
}
}
/**
* Opens dialog to change profile picture.
*/
async changeProfilePicture(): Promise<void> {
const maxSize = -1;
const title = Translate.instance.instant('core.user.newpicture');
const mimetypes = CoreMimetypeUtils.instance.getGroupMimeInfo('image', 'mimetypes');
let modal: CoreIonLoadingElement | undefined;
try {
const result = await CoreFileUploaderHelper.instance.selectAndUploadFile(maxSize, title, mimetypes);
modal = await CoreDomUtils.instance.showModalLoading('core.sending', true);
const profileImageURL = await CoreUser.instance.changeProfilePicture(result.itemid, this.userId, this.site!.getId());
CoreEvents.trigger<CoreUserProfilePictureUpdatedData>(CoreUserProvider.PROFILE_PICTURE_UPDATED, {
userId: this.userId,
picture: profileImageURL,
}, this.site!.getId());
CoreSites.instance.updateSiteInfo(this.site!.getId());
this.refreshUser();
} catch (error) {
CoreDomUtils.instance.showErrorModal(error);
} finally {
modal?.dismiss();
}
}
/**
* Refresh the user.
*
* @param event Event.
* @return Promise resolved when done.
*/
async refreshUser(event?: CustomEvent<IonRefresher>): Promise<void> {
await CoreUtils.instance.ignoreErrors(Promise.all([
CoreUser.instance.invalidateUserCache(this.userId),
// @todo this.coursesProvider.invalidateUserNavigationOptions(),
// this.coursesProvider.invalidateUserAdministrationOptions()
]));
await this.fetchUser();
event?.detail.complete();
if (this.user) {
CoreEvents.trigger<CoreUserProfileRefreshedData>(CoreUserProvider.PROFILE_REFRESHED, {
courseId: this.courseId,
userId: this.userId,
user: this.user,
}, this.site?.getId());
}
}
/**
* Open the page with the user details.
*/
openUserDetails(): void {
// @todo: Navigate out of split view if this page is in the right pane.
this.navCtrl.navigateForward(['../about'], {
relativeTo: this.route,
queryParams: {
courseId: this.courseId,
userId: this.userId,
},
});
}
/**
* A handler was clicked.
*
* @param event Click event.
* @param handler Handler that was clicked.
*/
handlerClicked(event: Event, handler: CoreUserProfileHandlerData): void {
// @todo: Pass the right navCtrl if this page is in the right pane of split view.
handler.action(event, this.user!, this.courseId);
}
/**
* Page destroyed.
*/
ngOnDestroy(): void {
this.subscription?.unsubscribe();
this.obsProfileRefreshed.off();
}
}

View File

@ -0,0 +1,87 @@
:host {
.core-user-profile-maininfo::part(native) {
flex-direction: column;
}
::ng-deep {
core-user-avatar {
display: block;
--core-avatar-size: var(--core-large-avatar-size);
img {
margin: 0;
}
.contact-status {
width: 24px !important;
height: 24px !important;
}
.core-icon-foreground {
position: absolute;
right: 0;
bottom: 0;
line-height: 30px;
text-align: center;
width: 30px;
height: 30px;
border-radius: 50%;
background-color:var(--background);
:host-context([dir="rtl"]) & {
left: 0;
right: unset;
}
}
}
}
}
:host-context([dir="rtl"]) ::ng-deep core-user-avatar .core-icon-foreground {
left: 0;
right: unset;
}
// @todo
// .core-user-communication-handlers {
// background: $list-background-color;
// border-bottom: 1px solid $list-border-color;
// @include darkmode() {
// background: $core-dark-item-bg-color;
// }
// .core-user-profile-handler {
// background: $list-background-color;
// border: 0;
// color: $core-user-profile-communication-icons-color;
// @include darkmode() {
// background: $core-dark-item-bg-color;
// }
// p {
// margin: 0;
// }
// .icon {
// border-radius: 50%;
// width: 32px;
// height: 32px;
// max-width: 32px;
// font-size: 22px;
// line-height: 32px;
// color: white;
// background-color: $core-user-profile-communication-icons-color;
// margin-bottom: 5px;
// }
// }
// }
// .core-user-profile-handler {
// ion-spinner {
// @include margin(null, null, null, 0.3em);
// }
// }

View File

@ -0,0 +1,90 @@
// (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 { CoreSiteSchema } from '@services/sites';
import { CoreUserBasicData } from '../user';
/**
* Database variables for CoreUser service.
*/
export const USERS_TABLE_NAME = 'users';
export const SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreUserProvider',
version: 1,
canBeCleared: [USERS_TABLE_NAME],
tables: [
{
name: USERS_TABLE_NAME,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true,
},
{
name: 'fullname',
type: 'TEXT',
},
{
name: 'profileimageurl',
type: 'TEXT',
},
],
},
],
};
/**
* Database variables for CoreUserOffline service.
*/
export const PREFERENCES_TABLE_NAME = 'user_preferences';
export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
name: 'CoreUserOfflineProvider',
version: 1,
tables: [
{
name: PREFERENCES_TABLE_NAME,
columns: [
{
name: 'name',
type: 'TEXT',
unique: true,
notNull: true,
},
{
name: 'value',
type: 'TEXT',
},
{
name: 'onlinevalue',
type: 'TEXT',
},
],
},
],
};
/**
* Data stored in DB for users.
*/
export type CoreUserDBRecord = CoreUserBasicData;
/**
* Structure of offline user preferences.
*/
export type CoreUserPreferenceDBRecord = {
name: string;
value: string;
onlinevalue: string;
};

View File

@ -0,0 +1,79 @@
// (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 } from '@angular/core';
import { Params } from '@angular/router';
import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreNavHelper } from '@services/nav-helper';
import { makeSingleton } from '@singletons';
/**
* Handler to treat links to user profiles.
*/
@Injectable({ providedIn: 'root' })
export class CoreUserProfileLinkHandlerService extends CoreContentLinksHandlerBase {
name = 'CoreUserProfileLinkHandler';
// Match user/view.php and user/profile.php but NOT grade/report/user/.
pattern = /((\/user\/view\.php)|(\/user\/profile\.php)).*([?&]id=\d+)/;
/**
* Get the list of actions for a link (url).
*
* @param siteIds List of sites the URL belongs to.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @param data Extra data to handle the URL.
* @return List of (or promise resolved with list of) actions.
*/
getActions(
siteIds: string[],
url: string,
params: Params,
courseId?: number, // eslint-disable-line @typescript-eslint/no-unused-vars
data?: unknown, // eslint-disable-line @typescript-eslint/no-unused-vars
): CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
return [{
action: (siteId): void => {
const pageParams = {
courseId: params.course,
userId: parseInt(params.id, 10),
};
CoreNavHelper.instance.goInSite('/user', pageParams, siteId);
},
}];
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
* If not defined, defaults to true.
*
* @param siteId The site ID.
* @param url The URL to treat.
* @param params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
* @param courseId Course ID related to the URL. Optional but recommended.
* @return Whether the handler is enabled for the URL and site.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isEnabled(siteId: string, url: string, params: Params, courseId?: number): boolean | Promise<boolean> {
return url.indexOf('/grade/report/') == -1;
}
}
export class CoreUserProfileLinkHandler extends makeSingleton(CoreUserProfileLinkHandlerService) {}

View File

@ -0,0 +1,78 @@
// (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 } from '@angular/core';
import { CoreUserDelegateService, CoreUserProfileHandler, CoreUserProfileHandlerData } from '../user-delegate';
import { CoreSites } from '@services/sites';
import { CoreUtils } from '@services/utils/utils';
import { CoreUserProfile } from '../user';
import { makeSingleton } from '@singletons';
/**
* Handler to send a email to a user.
*/
@Injectable({ providedIn: 'root' })
export class CoreUserProfileMailHandlerService implements CoreUserProfileHandler {
name = 'CoreUserProfileMail';
priority = 700;
type = CoreUserDelegateService.TYPE_COMMUNICATION;
/**
* Check if handler is enabled.
*
* @return Always enabled.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Check if handler is enabled for this user in this context.
*
* @param user User to check.
* @param courseId Course ID.
* @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
* @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
* @return Promise resolved with true if enabled, resolved with false otherwise.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async isEnabledForUser(user: CoreUserProfile, courseId: number, navOptions?: unknown, admOptions?: unknown): Promise<boolean> {
return user.id != CoreSites.instance.getCurrentSiteUserId() && !!user.email;
}
/**
* Returns the data needed to render the handler.
*
* @return Data needed to render the handler.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getDisplayData(user: CoreUserProfile, courseId: number): CoreUserProfileHandlerData {
return {
icon: 'mail',
title: 'core.user.sendemail',
class: 'core-user-profile-mail',
action: (event: Event, user: CoreUserProfile): void => {
event.preventDefault();
event.stopPropagation();
CoreUtils.instance.openInBrowser('mailto:' + user.email);
},
};
}
}
export class CoreUserProfileMailHandler extends makeSingleton(CoreUserProfileMailHandlerService) {}

View File

@ -0,0 +1,53 @@
// (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 } from '@angular/core';
import { CoreCronHandler } from '@services/cron';
import { makeSingleton } from '@singletons';
import { CoreUserSync } from '../user-sync';
/**
* Synchronization cron handler.
*/
@Injectable({ providedIn: 'root' })
export class CoreUserSyncCronHandlerService implements CoreCronHandler {
name = 'CoreUserSyncCronHandler';
/**
* Execute the process.
* Receives the ID of the site affected, undefined for all sites.
*
* @param siteId ID of the site affected, undefined for all sites.
* @param force Wether the execution is forced (manual sync).
* @return Promise resolved when done, rejected if failure.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
execute(siteId?: string, force?: boolean): Promise<void> {
return CoreUserSync.instance.syncPreferences(siteId);
}
/**
* Get the time between consecutive executions.
*
* @return Time between consecutive executions (in ms).
*/
getInterval(): number {
return 300000; // 5 minutes.
}
}
export class CoreUserSyncCronHandler extends makeSingleton(CoreUserSyncCronHandlerService) {}

View File

@ -0,0 +1,98 @@
// (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, Type } from '@angular/core';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTagAreaHandler } from '@features/tag/services/tag-area-delegate';
import { CoreUserTagAreaComponent } from '@features/user/components/tag-area/tag-area';
import { CoreTagFeedElement } from '@features/tag/services/tag-helper';
import { CoreUserBasicData } from '../user';
import { makeSingleton } from '@singletons';
/**
* Handler to support tags.
*/
@Injectable({ providedIn: 'root' })
export class CoreUserTagAreaHandlerService implements CoreTagAreaHandler {
name = 'CoreUserTagAreaHandler';
type = 'core/user';
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
async isEnabled(): Promise<boolean> {
return true;
}
/**
* Parses the rendered content of a tag index and returns the items.
*
* @param content Rendered content.
* @return Area items (or promise resolved with the items).
*/
parseContent(content: string): CoreUserTagFeedElement[] {
const items: CoreUserTagFeedElement[] = [];
const element = CoreDomUtils.instance.convertToElement(content);
Array.from(element.querySelectorAll('div.user-box')).forEach((userbox: HTMLElement) => {
const avatarLink = userbox.querySelector('a:first-child');
if (!avatarLink) {
return;
}
const profileUrl = avatarLink.getAttribute('href') || '';
const match = profileUrl.match(/.*\/user\/(?:profile|view)\.php\?id=(\d+)/);
if (!match) {
return;
}
const avatarImg = avatarLink.querySelector('img.userpicture');
const avatarUrl = avatarImg ? avatarImg.getAttribute('src') : '';
items.push({
avatarUrl,
heading: userbox.innerText,
details: [],
user: {
id: Number(match[1]),
profileimageurl: avatarUrl || '',
fullname: userbox.innerText,
},
});
});
return items;
}
/**
* Get the component to use to display items.
*
* @param injector Injector.
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): Type<unknown> | Promise<Type<unknown>> {
return CoreUserTagAreaComponent;
}
}
export class CoreUserTagAreaHandler extends makeSingleton(CoreUserTagAreaHandlerService) {}
export type CoreUserTagFeedElement = CoreTagFeedElement & {
user: CoreUserBasicData;
};

View File

@ -0,0 +1,282 @@
// (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 } from '@angular/core';
import { Subject, BehaviorSubject } from 'rxjs';
import { CoreDelegate, CoreDelegateHandler } from '@classes/delegate';
import { CoreUtils } from '@services/utils/utils';
import { CoreEvents } from '@singletons/events';
import { CoreUserProfile } from './user';
import { makeSingleton } from '@singletons';
/**
* Interface that all user profile handlers must implement.
*/
export interface CoreUserProfileHandler extends CoreDelegateHandler {
/**
* The highest priority is displayed first.
*/
priority: number;
/**
* A type should be specified among these:
* - TYPE_COMMUNICATION: will be displayed under the user avatar. Should have icon. Spinner not used.
* - TYPE_NEW_PAGE: will be displayed as a list of items. Should have icon. Spinner not used.
* Default value if none is specified.
* - TYPE_ACTION: will be displayed as a button and should not redirect to any state. Spinner use is recommended.
*/
type: string;
/**
* Whether or not the handler is enabled for a user.
*
* @param user User object.
* @param courseId Course ID where to show.
* @param navOptions Navigation options for the course.
* @param admOptions Admin options for the course.
* @return Whether or not the handler is enabled for a user.
*/
isEnabledForUser(user: CoreUserProfile, courseId: number, navOptions?: unknown, admOptions?: unknown): Promise<boolean>;
/**
* Returns the data needed to render the handler.
*
* @param user User object.
* @param courseId Course ID where to show.
* @return Data to be shown.
*/
getDisplayData(user: CoreUserProfile, courseId: number): CoreUserProfileHandlerData;
}
/**
* Data needed to render a user profile handler. It's returned by the handler.
*/
export interface CoreUserProfileHandlerData {
/**
* Title to display.
*/
title: string;
/**
* Name of the icon to display. Mandatory for TYPE_COMMUNICATION.
*/
icon?: string;
/**
* Additional class to add to the HTML.
*/
class?: string;
/**
* If enabled, element will be hidden. Only for TYPE_NEW_PAGE and TYPE_ACTION.
*/
hidden?: boolean;
/**
* If enabled will show an spinner. Only for TYPE_ACTION.
*/
spinner?: boolean;
/**
* Action to do when clicked.
*
* @param event Click event.
* @param user User object.
* @param courseId Course ID being viewed. If not defined, site context.
*/
action(event: Event, user: CoreUserProfile, courseId?: number): void;
}
/**
* Data returned by the delegate for each handler.
*/
export interface CoreUserProfileHandlerToDisplay {
/**
* Name of the handler.
*/
name?: string;
/**
* Data to display.
*/
data: CoreUserProfileHandlerData;
/**
* The highest priority is displayed first.
*/
priority?: number;
/**
* The type of the handler. See CoreUserProfileHandler.
*/
type: string;
}
/**
* Service to interact with plugins to be shown in user profile. Provides functions to register a plugin
* and notify an update in the data.
*/
@Injectable({ providedIn: 'root' })
export class CoreUserDelegateService extends CoreDelegate<CoreUserProfileHandler> {
/**
* User profile handler type for communication.
*/
static readonly TYPE_COMMUNICATION = 'communication';
/**
* User profile handler type for new page.
*/
static readonly TYPE_NEW_PAGE = 'newpage';
/**
* User profile handler type for actions.
*/
static readonly TYPE_ACTION = 'action';
/**
* Update handler information event.
*/
static readonly UPDATE_HANDLER_EVENT = 'CoreUserDelegate_update_handler_event';
protected featurePrefix = 'CoreUserDelegate_';
// Hold the handlers and the observable to notify them for each user.
protected userHandlers: {
[userId: number]: {
loaded: boolean; // Whether the handlers are loaded.
handlers: CoreUserProfileHandlerToDisplay[]; // List of handlers.
observable: Subject<CoreUserProfileHandlerToDisplay[]>; // Observale to notify the handlers.
};
} = {};
constructor() {
super('CoreUserDelegate', true);
CoreEvents.on<CoreUserUpdateHandlerData>(CoreUserDelegateService.UPDATE_HANDLER_EVENT, (data) => {
if (!data || !data.handler || !this.userHandlers[data.userId]) {
return;
}
// Search the handler.
const handler = this.userHandlers[data.userId].handlers.find((userHandler) => userHandler.name == data.handler);
if (!handler) {
return;
}
// Update the data and notify.
Object.assign(handler.data, data.data);
this.userHandlers[data.userId].observable.next(this.userHandlers[data.userId].handlers);
});
}
/**
* Check if handlers are loaded.
*
* @return True if handlers are loaded, false otherwise.
*/
areHandlersLoaded(userId: number): boolean {
return this.userHandlers[userId]?.loaded;
}
/**
* Clear current user handlers.
*
* @param userId The user to clear.
*/
clearUserHandlers(userId: number): void {
const userData = this.userHandlers[userId];
if (userData) {
userData.handlers = [];
userData.observable.next([]);
userData.loaded = false;
}
}
/**
* Get the profile handlers for a user.
*
* @param user The user object.
* @param courseId The course ID.
* @return Resolved with the handlers.
*/
getProfileHandlersFor(user: CoreUserProfile, courseId: number): Subject<CoreUserProfileHandlerToDisplay[]> {
// Initialize the user handlers if it isn't initialized already.
if (!this.userHandlers[user.id]) {
this.userHandlers[user.id] = {
loaded: false,
handlers: [],
observable: new BehaviorSubject<CoreUserProfileHandlerToDisplay[]>([]),
};
}
this.calculateUserHandlers(user, courseId);
return this.userHandlers[user.id].observable;
}
/**
* Get the profile handlers for a user.
*
* @param user The user object.
* @param courseId The course ID.
* @return Promise resolved when done.
*/
protected async calculateUserHandlers(user: CoreUserProfile, courseId: number): Promise<void> {
// @todo: Get Course admin/nav options.
let navOptions;
let admOptions;
const userData = this.userHandlers[user.id];
userData.handlers = [];
await CoreUtils.instance.allPromises(Object.keys(this.enabledHandlers).map(async (name) => {
// Checks if the handler is enabled for the user.
const handler = this.handlers[name];
try {
const enabled = await handler.isEnabledForUser(user, courseId, navOptions, admOptions);
if (enabled) {
userData.handlers.push({
name: name,
data: handler.getDisplayData(user, courseId),
priority: handler.priority || 0,
type: handler.type || CoreUserDelegateService.TYPE_NEW_PAGE,
});
}
} catch {
// Nothing to do here, it is not enabled for this user.
}
}));
// Sort them by priority.
userData.handlers.sort((a, b) => (b.priority || 0) - (a.priority || 0));
userData.loaded = true;
userData.observable.next(userData.handlers);
}
}
export class CoreUserDelegate extends makeSingleton(CoreUserDelegateService) {}
/**
* Data passed to UPDATE_HANDLER_EVENT event.
*/
export type CoreUserUpdateHandlerData = {
handler: string; // Name of the handler.
userId: number; // User affected.
data: Record<string, unknown>; // Data to set to the handler.
};

View File

@ -0,0 +1,65 @@
// (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 } from '@angular/core';
import { makeSingleton, Translate } from '@singletons';
import { CoreUserRole } from './user';
/**
* Service that provides some features regarding users information.
*/
@Injectable({ providedIn: 'root' })
export class CoreUserHelperProvider {
/**
* Formats a user address, concatenating address, city and country.
*
* @param address Address.
* @param city City.
* @param country Country.
* @return Formatted address.
*/
formatAddress(address?: string, city?: string, country?: string): string {
const separator = Translate.instance.instant('core.listsep');
let values = [address, city, country];
values = values.filter((value) => value && value.length > 0);
return values.join(separator + ' ');
}
/**
* Formats a user role list, translating and concatenating them.
*
* @param roles List of user roles.
* @return The formatted roles.
*/
formatRoleList(roles?: CoreUserRole[]): string {
if (!roles || roles.length <= 0) {
return '';
}
const separator = Translate.instance.instant('core.listsep');
return roles.map((value) => {
const translation = Translate.instance.instant('core.user.' + value.shortname);
return translation.indexOf('core.user.') < 0 ? translation : value.shortname;
}).join(separator + ' ');
}
}
export class CoreUserHelper extends makeSingleton(CoreUserHelperProvider) {}

View File

@ -0,0 +1,81 @@
// (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 } from '@angular/core';
import { CoreSites } from '@services/sites';
import { makeSingleton } from '@singletons';
import { PREFERENCES_TABLE_NAME, CoreUserPreferenceDBRecord } from './database/user';
/**
* Service to handle offline user preferences.
*/
@Injectable({ providedIn: 'root' })
export class CoreUserOfflineProvider {
/**
* Get preferences that were changed offline.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with list of preferences.
*/
async getChangedPreferences(siteId?: string): Promise<CoreUserPreferenceDBRecord[]> {
const site = await CoreSites.instance.getSite(siteId);
return site.getDb().getRecordsSelect(PREFERENCES_TABLE_NAME, 'value != onlineValue');
}
/**
* Get an offline preference.
*
* @param name Name of the preference.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with the preference, rejected if not found.
*/
async getPreference(name: string, siteId?: string): Promise<CoreUserPreferenceDBRecord> {
const site = await CoreSites.instance.getSite(siteId);
return site.getDb().getRecord(PREFERENCES_TABLE_NAME, { name });
}
/**
* Set an offline preference.
*
* @param name Name of the preference.
* @param value Value of the preference.
* @param onlineValue Online value of the preference. If undefined, preserve previously stored value.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when done.
*/
async setPreference(name: string, value: string, onlineValue?: string, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
if (typeof onlineValue == 'undefined') {
const preference = await this.getPreference(name, site.id);
onlineValue = preference.onlinevalue;
}
const record: CoreUserPreferenceDBRecord = {
name,
value,
onlinevalue: onlineValue,
};
await site.getDb().insertRecord(PREFERENCES_TABLE_NAME, record);
}
}
export class CoreUserOffline extends makeSingleton(CoreUserOfflineProvider) {}

View File

@ -0,0 +1,209 @@
// (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, 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 { makeSingleton } from '@singletons';
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.
*
* @return The component (or promise resolved with component) to use, undefined if not found.
*/
getComponent(): 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 | undefined>;
}
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 CoreUserProfileFieldDelegateService 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(
field: AuthEmailSignupProfileField | CoreUserProfileField,
signup: boolean,
): Promise<Type<unknown> | undefined> {
const type = this.getType(field);
try {
if (signup) {
return await this.executeFunction(type, 'getComponent', []);
} else {
return await this.executeFunctionOnEnabled(type, 'getComponent', []);
}
} 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 | undefined> {
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)));
}
}
export class CoreUserProfileFieldDelegate extends makeSingleton(CoreUserProfileFieldDelegateService) {}

Some files were not shown because too many files have changed in this diff Show More