MOBILE-2317 user: Implement user profile page
parent
c2946cc797
commit
2b19b5172d
|
@ -115,7 +115,8 @@
|
|||
}
|
||||
|
||||
> img:first-child,
|
||||
ion-avatar img {
|
||||
ion-avatar img,
|
||||
img {
|
||||
display: block;
|
||||
margin: auto;
|
||||
width: 90px;
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
|
@ -196,14 +196,14 @@ export class CoreFileUploaderDelegate {
|
|||
if (mimetypes) {
|
||||
if (!handler.getSupportedMimetypes) {
|
||||
// Handler doesn't implement a required function, don't add it.
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
supportedMimetypes = handler.getSupportedMimetypes(mimetypes);
|
||||
|
||||
if (!supportedMimetypes.length) {
|
||||
// Handler doesn't support any mimetype, don't add it.
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
{
|
||||
|
||||
"details": "Details",
|
||||
"detailsnotavailable": "The details of this user are not available to you.",
|
||||
"editingteacher": "Teacher",
|
||||
"errorloaduser": "Error loading user.",
|
||||
"manager": "Manager",
|
||||
"newpicture": "New picture",
|
||||
"roles": "Roles",
|
||||
"student": "Student",
|
||||
"teacher": "Non-editing teacher"
|
||||
}
|
|
@ -4,4 +4,35 @@
|
|||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="userLoaded" (ionRefresh)="refreshUser($event)">
|
||||
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||
</ion-refresher>
|
||||
<core-loading [hideUntil]="userLoaded" class="core-loading-center">
|
||||
<ion-list *ngIf="user && !isDeleted">
|
||||
<ion-item text-center>
|
||||
<div class="item-avatar-center">
|
||||
<img *ngIf="user.profileimageurl" class="avatar" [src]="user.profileimageurl" core-external-content alt="{{ 'core.pictureof' | translate:{$a: user.fullname} }}" role="presentation">
|
||||
<img *ngIf="!user.profileimageurl" class="avatar" src="assets/img/user-avatar.png" alt="{{ 'core.pictureof' | translate:{$a: user.fullname} }}" role="presentation">
|
||||
<ion-icon name="create" class="core-icon-foreground" *ngIf="canChangeProfilePicture" (click)="changeProfilePicture()"></ion-icon>
|
||||
</div>
|
||||
<h2><core-format-text [text]="user.fullname"></core-format-text></h2>
|
||||
<p><core-format-text *ngIf="user.address" [text]="user.address"></core-format-text></p>
|
||||
<p *ngIf="user.roles">
|
||||
<strong>{{ 'core.user.roles' | translate}}</strong>{{'core.labelsep' | translate}}
|
||||
<core-format-text [text]="user.roles"></core-format-text>
|
||||
</p>
|
||||
</ion-item>
|
||||
|
||||
<a ion-item text-wrap class="core-user-profile-handler" [navPush]="CoreUserAboutPage" [navParams]="{courseId: courseId, userId: userId}" title="{{ 'core.user.details' | translate }}">
|
||||
<ion-icon name="person" item-start></ion-icon>
|
||||
<h2>{{ 'core.user.details' | translate }}</h2>
|
||||
</a>
|
||||
<ion-item text-center *ngIf="isLoadingHandlers">
|
||||
<ion-spinner></ion-spinner>
|
||||
</ion-item>
|
||||
</ion-list>
|
||||
<core-empty-box *ngIf="!user && !isDeleted" icon="person" [message]=" 'core.user.detailsnotavailable' | translate"></core-empty-box>
|
||||
|
||||
<core-empty-box *ngIf="isDeleted" icon="person" [message]="'core.userdeleted' | translate"></core-empty-box>
|
||||
</core-loading>
|
||||
</ion-content>
|
|
@ -14,8 +14,10 @@
|
|||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreUserProfilePage } from './profile';
|
||||
import { CoreDirectivesModule } from '../../../../directives/directives.module';
|
||||
import { CoreComponentsModule } from '../../../../components/components.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -23,7 +25,9 @@ import { CoreDirectivesModule } from '../../../../directives/directives.module';
|
|||
],
|
||||
imports: [
|
||||
CoreDirectivesModule,
|
||||
CoreComponentsModule,
|
||||
IonicPageModule.forChild(CoreUserProfilePage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class CoreUserProfilePageModule {}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
page-core-user-profile {
|
||||
|
||||
.core-icon-foreground {
|
||||
position: relative;
|
||||
left: 60px;
|
||||
bottom: 30px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
|
@ -13,8 +13,16 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { IonicPage } from 'ionic-angular';
|
||||
import { IonicPage, NavParams } from 'ionic-angular';
|
||||
import { CoreUserProvider } from '../../providers/user';
|
||||
import { CoreUserHelperProvider } from '../../providers/helper';
|
||||
import { CoreDomUtilsProvider } from '../../../../providers/utils/dom';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreCoursesProvider } from '../../../courses/providers/courses';
|
||||
import { CoreEventsProvider } from '../../../../providers/events';
|
||||
import { CoreSitesProvider } from '../../../../providers/sites';
|
||||
import { CoreMimetypeUtilsProvider } from '../../../../providers/utils/mimetype';
|
||||
import { CoreFileUploaderHelperProvider } from '../../../fileuploader/providers/helper';
|
||||
|
||||
/**
|
||||
* Page that displays an user profile page.
|
||||
|
@ -25,4 +33,126 @@ import { CoreUserProvider } from '../../providers/user';
|
|||
templateUrl: 'profile.html',
|
||||
})
|
||||
export class CoreUserProfilePage {
|
||||
protected courseId: number;
|
||||
protected userId: number;
|
||||
protected site;
|
||||
protected obsProfileRefreshed: any;
|
||||
|
||||
userLoaded: boolean = false;
|
||||
isLoadingHandlers: boolean = false;
|
||||
user: any = {};
|
||||
title: string;
|
||||
isDeleted: boolean = false;
|
||||
canChangeProfilePicture: boolean = false;
|
||||
|
||||
constructor(private navParams: NavParams, private userProvider: CoreUserProvider, private userHelper: CoreUserHelperProvider,
|
||||
private domUtils: CoreDomUtilsProvider, private translate: TranslateService, private eventsProvider: CoreEventsProvider,
|
||||
private coursesProvider: CoreCoursesProvider, private sitesProvider: CoreSitesProvider,
|
||||
private mimetypeUtils: CoreMimetypeUtilsProvider, private fileUploaderHelper: CoreFileUploaderHelperProvider) {
|
||||
this.userId = navParams.get('userId');
|
||||
this.courseId = navParams.get('courseId');
|
||||
|
||||
this.site = this.sitesProvider.getCurrentSite();
|
||||
|
||||
// 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() &&
|
||||
this.site.wsAvailable('core_user_update_picture') &&
|
||||
!this.userProvider.isUpdatePictureDisabledInSite(this.site);
|
||||
|
||||
this.obsProfileRefreshed = eventsProvider.on(CoreUserProvider.PROFILE_REFRESHED, (data) => {
|
||||
if (typeof data.user != "undefined") {
|
||||
this.user.email = data.user.email;
|
||||
this.user.address = this.userHelper.formatAddress("", data.user.city, data.user.country);
|
||||
}
|
||||
}, sitesProvider.getCurrentSiteId());
|
||||
}
|
||||
|
||||
/**
|
||||
* View loaded.
|
||||
*/
|
||||
ionViewDidLoad() {
|
||||
this.fetchUser().then(() => {
|
||||
return this.userProvider.logView(this.userId, this.courseId).catch((error) => {
|
||||
this.isDeleted = error === this.translate.instant('core.userdeleted');
|
||||
});
|
||||
}).finally(() => {
|
||||
this.userLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the user and updates the view.
|
||||
*/
|
||||
fetchUser() : Promise<any> {
|
||||
return this.userProvider.getProfile(this.userId, this.courseId).then((user) => {
|
||||
|
||||
user.address = this.userHelper.formatAddress("", user.city, user.country);
|
||||
user.roles = this.userHelper.formatRoleList(user.roles);
|
||||
|
||||
this.user = user;
|
||||
this.title = user.fullname;
|
||||
|
||||
this.isLoadingHandlers = true;
|
||||
|
||||
}).catch((error) => {
|
||||
this.domUtils.showErrorModalDefault(error, 'core.user.errorloaduser', true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens dialog to change profile picture.
|
||||
*/
|
||||
changeProfilePicture(){
|
||||
let maxSize = -1,
|
||||
title = this.translate.instant('core.user.newpicture'),
|
||||
mimetypes = this.mimetypeUtils.getGroupMimeInfo('image', 'mimetypes');
|
||||
|
||||
return this.fileUploaderHelper.selectAndUploadFile(maxSize, title, mimetypes).then((result) => {
|
||||
let modal = this.domUtils.showModalLoading('core.sending', true);
|
||||
|
||||
return this.userProvider.changeProfilePicture(result.itemid, this.userId).then((profileImageURL) => {
|
||||
this.eventsProvider.trigger(CoreUserProvider.PROFILE_PICTURE_UPDATED, {userId: this.userId, picture: profileImageURL});
|
||||
this.sitesProvider.updateSiteInfo(this.site.getId());
|
||||
this.refreshUser();
|
||||
}).finally(function() {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch((message) => {
|
||||
if (message) {
|
||||
this.domUtils.showErrorModal(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the user.
|
||||
*
|
||||
* @param {any} refresher Refresher.
|
||||
*/
|
||||
refreshUser(refresher?: any) {
|
||||
let promises = [];
|
||||
|
||||
promises.push(this.userProvider.invalidateUserCache(this.userId));
|
||||
promises.push(this.coursesProvider.invalidateUserNavigationOptions());
|
||||
promises.push(this.coursesProvider.invalidateUserAdministrationOptions());
|
||||
|
||||
Promise.all(promises).finally(() => {
|
||||
this.fetchUser().finally(() => {
|
||||
this.eventsProvider.trigger(CoreUserProvider.PROFILE_REFRESHED, {courseId: this.courseId, userId: this.userId,
|
||||
user: this.user}, this.site.getId());
|
||||
refresher && refresher.complete();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Page destroyed.
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.obsProfileRefreshed && this.obsProfileRefreshed.off();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { CoreLoggerProvider } from '../../../providers/logger';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* Service that provides some features regarding users information.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CoreUserHelperProvider {
|
||||
protected logger;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private translate: TranslateService) {
|
||||
this.logger = logger.getInstance('CoreUserHelperProvider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a user address, concatenating address, city and country.
|
||||
*
|
||||
* @param {string} address Address.
|
||||
* @param {string} city City.
|
||||
* @param {string} country Country.
|
||||
* @return {string} Formatted address.
|
||||
*/
|
||||
formatAddress(address: string, city: string, country: string) : string {
|
||||
let separator = this.translate.instant('core.listsep'),
|
||||
values = [address, city, country];
|
||||
|
||||
values = values.filter((value) => {
|
||||
return value && value.length > 0;
|
||||
});
|
||||
|
||||
return values.join(separator + " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a user role list, translating and concatenating them.
|
||||
*
|
||||
* @param {any[]} [roles] List of user roles.
|
||||
* @return {string} The formatted roles.
|
||||
*/
|
||||
formatRoleList(roles?: any[]) : string {
|
||||
if (!roles || roles.length <= 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let separator = this.translate.instant('core.listsep');
|
||||
|
||||
roles.map((value) => {
|
||||
console.error(value);
|
||||
let translation = this.translate.instant('core.user.' + value.shortname);
|
||||
return translation.indexOf('core.user.') < 0 ? translation : value.shortname;
|
||||
});
|
||||
|
||||
return roles.join(separator + " ");
|
||||
}
|
||||
}
|
|
@ -13,12 +13,221 @@
|
|||
// limitations under the License.
|
||||
|
||||
import { Injectable } from '@angular/core';
|
||||
import { CoreLoggerProvider } from '../../../providers/logger';
|
||||
import { CoreSite } from '../../../classes/site';
|
||||
import { CoreSitesProvider } from '../../../providers/sites';
|
||||
import { CoreUtilsProvider } from '../../../providers/utils/utils';
|
||||
|
||||
/**
|
||||
* Service to provide user functionalities.
|
||||
*/
|
||||
@Injectable()
|
||||
export class CoreUserProvider {
|
||||
public static PROFILE_REFRESHED = 'CoreUserProfileRefreshed';
|
||||
public static PROFILE_PICTURE_UPDATED = 'CoreUserProfilePictureUpdated';
|
||||
protected ROOT_CACHE_KEY = 'mmUser:';
|
||||
|
||||
constructor() {}
|
||||
// Variables for database.
|
||||
protected USERS_TABLE = 'users';
|
||||
protected tablesSchema = [
|
||||
{
|
||||
name: this.USERS_TABLE,
|
||||
columns: [
|
||||
{
|
||||
name: 'id',
|
||||
type: 'INTEGER',
|
||||
primaryKey: true
|
||||
},
|
||||
{
|
||||
name: 'fullname',
|
||||
type: 'TEXT'
|
||||
},
|
||||
{
|
||||
name: 'profileimageurl',
|
||||
type: 'TEXT'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
protected logger;
|
||||
|
||||
constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider) {
|
||||
this.logger = logger.getInstance('CoreUserProvider');
|
||||
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the given user profile picture.
|
||||
*
|
||||
* @param {number} draftItemId New picture draft item id.
|
||||
* @param {number} id User ID.
|
||||
* @return {Promise<any>} Promise resolve with the new profileimageurl
|
||||
*/
|
||||
changeProfilePicture(draftItemId: number, userId: number): Promise<any> {
|
||||
var data = {
|
||||
'draftitemid': draftItemId,
|
||||
'delete': 0,
|
||||
'userid': userId
|
||||
};
|
||||
|
||||
return this.sitesProvider.getCurrentSite().write('core_user_update_picture', data).then((result) => {
|
||||
if (!result.success) {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
return result.profileimageurl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile. The type of profile retrieved depends on the params.
|
||||
*
|
||||
* @param {number} userId User's ID.
|
||||
* @param {number} [courseId] Course ID to get course profile, undefined or 0 to get site profile.
|
||||
* @param {boolean} [forceLocal] True to retrieve the user data from local DB, false to retrieve it from WS.
|
||||
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
|
||||
* @return {Promise<any>} Promise resolved with the user data.
|
||||
*/
|
||||
getProfile(userId: number, courseId: number, forceLocal = false, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
if (forceLocal) {
|
||||
return this.getUserFromLocalDb(userId, siteId).catch(function() {
|
||||
return this.getUserFromWS(userId, courseId, siteId);
|
||||
});
|
||||
}
|
||||
return this.getUserFromWS(userId, courseId, siteId).catch(function() {
|
||||
return this.getUserFromLocalDb(userId, siteId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates user WS calls.
|
||||
*
|
||||
* @param {number} userId User ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getUserCacheKey(userId): string {
|
||||
return this.ROOT_CACHE_KEY + 'data:' + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user basic information from local DB.
|
||||
*
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
|
||||
* @return {Promise<any>} Promise resolve when the user is retrieved.
|
||||
*/
|
||||
protected getUserFromLocalDb(userId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.getDb().getRecord(this.USERS_TABLE, {id: userId});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile from WS.
|
||||
*
|
||||
* @param {number} userId User ID.
|
||||
* @param {number} [courseId] Course ID to get course profile, undefined or 0 to get site profile.
|
||||
* @param {string} [siteId] ID of the site. If not defined, use current site.
|
||||
* @return {Promise<any>} Promise resolve when the user is retrieved.
|
||||
*/
|
||||
protected getUserFromWS(userId: number, courseId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
let presets = {
|
||||
cacheKey: this.getUserCacheKey(userId)
|
||||
},
|
||||
wsName, data;
|
||||
|
||||
// Determine WS and data to use.
|
||||
if (courseId && courseId != site.getSiteHomeId()) {
|
||||
this.logger.debug(`Get participant with ID '${userId}' in course '${courseId}`);
|
||||
wsName = 'core_user_get_course_user_profiles';
|
||||
data = {
|
||||
"userlist[0][userid]": userId,
|
||||
"userlist[0][courseid]": courseId
|
||||
};
|
||||
} else {
|
||||
this.logger.debug(`Get user with ID '${userId}'`);
|
||||
wsName = 'core_user_get_users_by_field';
|
||||
data = {
|
||||
'field': 'id',
|
||||
'values[0]': userId
|
||||
};
|
||||
}
|
||||
|
||||
return site.read(wsName, data, presets).then((users) => {
|
||||
if (users.length == 0) {
|
||||
return Promise.reject('Cannot retrieve user info.');
|
||||
}
|
||||
|
||||
var user = users.shift();
|
||||
if (user.country) {
|
||||
user.country = this.utils.getCountryName(user.country);
|
||||
}
|
||||
this.storeUser(user.id, user.fullname, user.profileimageurl);
|
||||
return user;
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates user WS calls.
|
||||
*
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} [siteId] Site Id. If not defined, use current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateUserCache(userId: number, siteId?: string) : Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getUserCacheKey(userId));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if update profile picture is disabled in a certain site.
|
||||
*
|
||||
* @param {CoreSite} [site] Site. If not defined, use current site.
|
||||
* @return {boolean} True if disabled, false otherwise.
|
||||
*/
|
||||
isUpdatePictureDisabledInSite(site?: CoreSite) : boolean {
|
||||
site = site || this.sitesProvider.getCurrentSite();
|
||||
return site.isFeatureDisabled('$mmUserDelegate_picture');
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Log User Profile View in Moodle.
|
||||
* @param {number} userId User ID.
|
||||
* @param {number} courseId Course ID.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
logView(userId: number, courseId?: number) : Promise<any> {
|
||||
return this.sitesProvider.getCurrentSite().write('core_user_view_user_profile', {
|
||||
userid: userId,
|
||||
courseid: courseId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store user basic information in local DB to be retrieved if the WS call fails.
|
||||
*
|
||||
* @param {number} userId User ID.
|
||||
* @param {string} fullname User full name.
|
||||
* @param {string} avatar User avatar URL.
|
||||
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
|
||||
* @return {Promise<any>} Promise resolve when the user is stored.
|
||||
*/
|
||||
protected storeUser(userId: number, fullname: string, avatar: string, siteId?: string) {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
let userRecord = {
|
||||
id: userId,
|
||||
fullname: fullname,
|
||||
profileimageurl: avatar
|
||||
};
|
||||
|
||||
return site.getDb().insertOrUpdateRecord(this.USERS_TABLE, userRecord, {id: userId});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CoreUserDelegate } from './providers/delegate';
|
||||
import { CoreUserProvider } from './providers/user';
|
||||
import { CoreUserHelperProvider } from './providers/helper';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
|
@ -23,7 +24,8 @@ import { CoreUserProvider } from './providers/user';
|
|||
],
|
||||
providers: [
|
||||
CoreUserDelegate,
|
||||
CoreUserProvider
|
||||
CoreUserProvider,
|
||||
CoreUserHelperProvider
|
||||
]
|
||||
})
|
||||
export class CoreUserModule {}
|
||||
|
|
Loading…
Reference in New Issue